refactor: package code organization wave 8 (#2252)

## Summary

Wave 8 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`),
after #2166.

### Peels
| New module | Parent |
|---|---|
| `app/api/board-workflows.ts` | `legacy.ts` |
| `app/api/remote.ts` | `legacy.ts` |
| `app/api/memory.ts` | `legacy.ts` |
| `types/docker-nodes.ts` | `types.ts` |
| `task-store/task-store-helpers.ts` | `remaining-ops-10.ts` (rename) |

### CI fix (main unblocker)
FN-8142 requires pi SDK **0.80.8+** (`ModelRuntime`,
`CredentialStore.list`). Floored `@earendil-works/pi-*` to `^0.80.8` so
lockfile resolves past 0.80.6.

Public import paths stay stable via re-exports.

### LOC
- `legacy.ts` ~10975 → ~10561
- `types.ts` ~7349 → ~6928

## Test plan
- [x] core / dashboard typecheck + eslint on peels
- [x] api-tasks (66)
- [x] CI merge gate (Lint / Typecheck / Build / Gate)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added dashboard support for multi-lane board workflows, including
workflow metadata retrieval and task custom-field updates.
* Added dashboard memory client capabilities for reading/writing memory,
managing memory files, compaction/dreaming, insights, auditing, backend
status, and retrieval testing.
* Added remote connectivity controls, including tunnel start/stop,
provider activation, token generation, URL sharing, and QR generation.
* Added Docker node configuration validation plus secure redaction for
sensitive fields in responses.
* **Improvements**
* Consolidated dashboard API exports into dedicated modules and
preserved the legacy API surface via re-exports.
* **Chores**
  * Updated related agent dependency versions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-16 22:36:51 -07:00
committed by GitHub
parent daa34fbc38
commit da7c68c1dc
9 changed files with 1142 additions and 976 deletions

View File

@@ -102,7 +102,7 @@ import { claimNextToolFailureRetryImpl, clearNearDuplicateReferencesToFailSoftIm
import { addPrInfoImpl, addSteeringCommentImpl, archiveAllDoneImpl, cleanupStaleMergeQueueRowsImpl, clearCompletionHandoffAcceptedMarkerImpl, clearDoneTransientFieldsImpl, clearStaleExecutionStartBranchReferencesImpl, computeWorkflowColumnsGraduationReportImpl, deleteTaskCommentImpl, deleteTaskDocumentImpl, emitUsageEventImpl, enqueueMergeQueueImpl, getAgentLogCountImpl, getAgentLogsImpl, getArtifactImpl, getArtifactsImpl, getAttachmentImpl, getCompletionHandoffAcceptedMarkerImpl, getTaskDocumentImpl, getTaskDocumentRevisionsImpl, getTaskDocumentsImpl, insertArtifactRowImpl, linkGithubIssueImpl, listWorkflowWorkItemsForTaskSyncImpl, moveToDoneImpl, parseDependenciesFromPromptImpl, parseFileScopeFromPromptImpl, parseStepsFromPromptImpl, peekMergeQueueHeadImpl, peekMergeQueueImpl, readPreArchiveColumnFromTaskFileImpl, recordPluginActivationImpl, recordRunAuditEventBackendImpl, removePrInfoByNumberImpl, resolvePrimaryPrInfoImpl, resolveUnarchiveTargetColumnImpl, rewriteLineageChildrenForRemovalImpl, runGitCommandImpl, stopWatchingImpl, syncAgentTaskLinkOnReassignmentImpl, updateArtifactImpl, updateGithubTrackingImpl, updatePrInfoByNumberImpl, updateTaskCommentImpl, upsertPrInfoByNumberImpl, writeArtifactDataImpl } from "./task-store/remaining-ops-7.js";
import { approveCliAutonomyImpl, approveWorkflowCliCommandImpl, cleanupOrphanedMaterializedStepsImpl, consumePluginGateVerdictsImpl, getAgentLogsByTimeRangeImpl, getDatabaseHealthImpl, getDistributedTaskIdAllocatorImpl, getExperimentSessionStoreImpl, getInReviewDurationEventsImpl, getMissionStoreImpl, getPluginStoreImpl, getSecretsStoreImpl, getSettingsSyncImpl, getTaskMergedTaskIdsImpl, getTaskWorkflowSelectionImpl, getImportTranslationImpl, recordImportTranslationImpl, pruneImportTranslationsImpl, type ImportTranslationCacheKey, type ImportTranslationCacheEntry, getVerificationCacheHitImpl, getWorkflowDefinitionImpl, healthCheckImpl, importLegacyAgentLogsOnceImpl, insertWorkflowDefinitionSyncImpl, isCliAutonomyApprovedImpl, isPluginInstalledImpl, isWorkflowCliCommandApprovedImpl, listWorkflowDefinitionsImpl, materializeExplicitWorkflowStepsImpl, materializeWorkflowStepsImpl, migrateActiveArchivedTasksToArchiveDbImpl, migrateLegacyArchiveEntriesToArchiveDbImpl, nextWorkflowDefinitionIdImpl, occupantsByColumnForWorkflowImpl, parseWorkflowLayoutImpl, pruneAgentLogFilesImpl, purgeTaskWorkflowSelectionRowsImpl, readAllWorkflowDefinitionsImpl, readRawProjectSettingsImpl, recordPluginGateVerdictImpl, recordVerificationCachePassImpl, removeMaterializedSelectionImpl, resolvePluginWorkflowStepImpl, resolveTaskWorkflowIrSyncImpl, revokeCliAutonomyImpl, selectTaskWorkflowAndReconcileImpl, writeTaskWorkflowSelectionImpl, getTaskWorkflowSelectionAsyncImpl, } from "./task-store/remaining-ops-8.js";
import { getTaskCommitAssociationsByLineageIdImpl, replaceLegacyTaskCommitAssociationsImpl } from "./task-store/task-commit-associations.js";
import { addTaskCommentImpl, applyBuiltInPromptOverridesSyncImpl, areAllDependenciesDoneImpl, artifactStoredNameImpl, assertWorkflowIrTraitsValidImpl, clearActivityLogImpl, clearTaskWorkflowSelectionImpl, deleteTaskByIdImpl, getDefaultWorkflowIdImpl, getInsightStoreImpl, getMergeQueuedTaskIdsImpl, getMergeRequestRecordImpl, getMergeRequestRecordAsyncImpl, getResearchStoreImpl, getTaskIdFromDirImpl, getTodoStoreImpl, getWorkflowWorkItemByIdentityImpl, hasActiveTaskImpl, invalidateConfigCacheAfterMigrationImpl, isTaskIdConflictErrorImpl, listLegacyAutoMergeStampCandidatesImpl, readTaskRowFromDbImpl, recordBranchGroupMemberLandedImpl, refreshDatabaseHealthImpl, resolveEffectiveWorkflowIdSyncImpl, resolveTaskCustomFieldDefsSyncImpl, resolveWorkflowBypassGuardsImpl, serializeConfigForDiskImpl, setPluginWorkflowStepTemplatesImpl, shouldSkipWorkflowMovePoliciesImpl, suppressWatcherImpl, upsertTaskWithFtsRecoveryImpl } from "./task-store/remaining-ops-10.js";
import { addTaskCommentImpl, applyBuiltInPromptOverridesSyncImpl, areAllDependenciesDoneImpl, artifactStoredNameImpl, assertWorkflowIrTraitsValidImpl, clearActivityLogImpl, clearTaskWorkflowSelectionImpl, deleteTaskByIdImpl, getDefaultWorkflowIdImpl, getInsightStoreImpl, getMergeQueuedTaskIdsImpl, getMergeRequestRecordImpl, getMergeRequestRecordAsyncImpl, getResearchStoreImpl, getTaskIdFromDirImpl, getTodoStoreImpl, getWorkflowWorkItemByIdentityImpl, hasActiveTaskImpl, invalidateConfigCacheAfterMigrationImpl, isTaskIdConflictErrorImpl, listLegacyAutoMergeStampCandidatesImpl, readTaskRowFromDbImpl, recordBranchGroupMemberLandedImpl, refreshDatabaseHealthImpl, resolveEffectiveWorkflowIdSyncImpl, resolveTaskCustomFieldDefsSyncImpl, resolveWorkflowBypassGuardsImpl, serializeConfigForDiskImpl, setPluginWorkflowStepTemplatesImpl, shouldSkipWorkflowMovePoliciesImpl, suppressWatcherImpl, upsertTaskWithFtsRecoveryImpl } from "./task-store/task-store-helpers.js";
import { getTaskSelectClauseImpl2, createTaskPersistSerializationContextImpl, getTaskPersistValuesImpl, getTaskPatchDescriptorsImpl, normalizeTaskFromDiskImpl, writeTaskJsonFileImpl, rowToPrEntityImpl, generatePrEntityIdImpl, readTaskForMoveImpl, rowToMergeQueueEntryImpl, rowToMergeRequestRecordImpl, rowToCompletionHandoffMarkerImpl, rowToWorkflowWorkItemImpl, rowToRunAuditEventImpl } from "./task-store/task-row-mappers.js";
import { getTaskSelectClauseWithActivityLogLimitImpl, getChangedTaskColumnsImpl, getSoftDeletedWriteConflictImpl, readTaskJsonImpl, writeConfigImpl, _maybeAutoArchiveSameAgentDuplicateBackendImpl, updateBranchGroupImpl, updatePrEntityImpl, listTasksForGithubTrackingReconcileImpl, listTasksForGitlabTrackingReconcileImpl, renewCheckoutLeaseImpl, updateTaskAtomicImpl, getWorkflowPromptOverridesImpl, updateWorkflowSettingValuesImpl, cancelActiveWorkflowWorkItemsForTaskImpl, setCompletionHandoffAcceptedMarkerImpl, reconcileLegacyAutoMergeStampsImpl, recoverExpiredMergeQueueLeasesImpl, rewriteDependentsForRemovalImpl, cleanupBranchForTaskImpl, addAttachmentImpl, deleteAttachmentImpl, registerArtifactImpl, updatePrInfoImpl, unlinkGithubIssueImpl, cleanupArchivedTasksImpl, generatePromptFromArchiveEntryImpl, listWorkflowOccupantTaskIdsImpl, evacuateCustomColumnsToLegacyImpl, listApprovedCliAutonomyAdaptersImpl, closeImpl, getActivityLogImpl } from "./task-store/remaining-ops-2.js";
import { getOrCreateForProjectImpl, listGoalCitationsImpl, atomicWriteTaskJsonWithAuditImpl, duplicateTaskImpl, listStrandedRefinementsImpl, tryClaimCheckoutImpl, evaluateWorkflowMovePoliciesImpl, recordRunAuditEventImpl, getRunAuditEventsImpl, getWorkflowParitySummaryImpl, dequeueMergeQueueOnColumnExitImpl, updateIssueInfoImpl, listWorkflowStepsImpl, getWorkflowStepImpl, createWorkflowDefinitionImpl, countActiveInCapacitySlotSyncImpl, countActiveInCapacitySlotAsyncImpl, generateSpecifiedPromptImpl, recordActivityImpl, getEvalStoreImpl } from "./task-store/remaining-ops-1.js";

View File

@@ -1,10 +1,13 @@
/**
* remaining-ops-10 operations.
* Task-store helper operations (DB row, merge request, workflow IR).
*
* FNXC:StoreModularization 2026-06-25-00:00:
* Extracted from the monolithic packages/core/src/store.ts as a pure
* behavior-preserving refactor. Each function receives the TaskStore
* instance as its first parameter and performs byte-identical work.
*
* FNXC:CodeOrganization 2026-07-16-20:00:
* Renamed from remaining-ops-10.ts (domain: task-store helper ops).
*/
import { TaskStore } from "../store.js";

View File

@@ -5068,6 +5068,70 @@ export interface SettingsSyncResult {
error?: string;
}
import type {
DockerNodeConfig,
DockerNodeVolumeMount,
DockerNodeContainerResourceConfig,
DockerNodeHostConfig,
DockerNodePersistenceConfig,
NodeVersionInfo,
NodeVersionInfoInput,
DockerNodeStatus,
DockerHostConfig,
DockerResourceSizing,
DockerVolumeMount,
DockerExtraCli,
ManagedDockerNode,
ManagedDockerNodeInput,
ManagedDockerNodeUpdate,
MeshConfigGeneratorInput,
FullProvisioningInput,
MeshConnectionConfig,
DockerContextInfo,
DockerConnectivityResult,
DockerContainerInspectResult,
DockerNodeImageConfig,
DockerNodeResourceConfig,
DockerProvisionInput,
DockerProvisionResult,
} from "./types/docker-nodes.js";
export type {
DockerNodeConfig,
DockerNodeVolumeMount,
DockerNodeContainerResourceConfig,
DockerNodeHostConfig,
DockerNodePersistenceConfig,
NodeVersionInfo,
NodeVersionInfoInput,
DockerNodeStatus,
DockerHostConfig,
DockerResourceSizing,
DockerVolumeMount,
DockerExtraCli,
ManagedDockerNode,
ManagedDockerNodeInput,
ManagedDockerNodeUpdate,
MeshConfigGeneratorInput,
FullProvisioningInput,
MeshConnectionConfig,
DockerContextInfo,
DockerConnectivityResult,
DockerContainerInspectResult,
DockerNodeImageConfig,
DockerNodeResourceConfig,
DockerProvisionInput,
DockerProvisionResult,
};
import {
validateDockerNodeConfig,
sanitizeDockerNodeConfigForResponse
} from "./types/docker-nodes.js";
export {
validateDockerNodeConfig,
sanitizeDockerNodeConfigForResponse,
};
/** A runtime node that can host project execution (local machine or remote host) */
export interface NodeConfig {
/** Unique node ID (e.g., "node_abc123") */
@@ -5102,355 +5166,8 @@ export interface NodeConfig {
updatedAt: string;
}
/** Persisted configuration for a Docker-managed Fusion node. */
export interface DockerNodeConfig {
/** Docker image name (e.g., "runfusion/fusion:latest") */
image: string;
/** Container name (defaults to "fusion-{nodeId}") */
containerName?: string;
/** Volume mount definitions */
volumeMounts: DockerNodeVolumeMount[];
/** Environment variable overrides (key-value pairs) */
environment: Record<string, string>;
/** Resource limits */
resources?: DockerNodeContainerResourceConfig;
/** Docker host connection settings */
host?: DockerNodeHostConfig;
/** Optional CLI tools to include in the container */
extraClis?: string[];
/** Persistent storage configuration */
persistence?: DockerNodePersistenceConfig;
/** Config version counter — starts at 1, auto-incremented on every update */
configVersion: number;
/** ISO timestamp of last config change (auto-set on update) */
lastUpdated?: string;
}
export interface DockerNodeVolumeMount {
/** Host path or named volume */
hostPath: string;
/** Container mount path */
containerPath: string;
/** "rw" (default) or "ro" */
mode?: "rw" | "ro";
/** "volume" (default) for named volumes, "bind" for host bind mounts */
type?: "volume" | "bind";
}
export interface DockerNodeContainerResourceConfig {
/** Memory limit in bytes (e.g., 2147483648 for 2GB) */
memoryBytes?: number;
/** CPU count limit (e.g., 2.0 for two cores) */
cpuCount?: number;
/** PIDs limit */
pidsLimit?: number;
}
export interface DockerNodeHostConfig {
/** Docker context name (for named Docker context selection) */
contextName?: string;
/** Explicit Docker host URL (e.g., "tcp://192.168.1.100:2376") */
dockerHost?: string;
/** Path to TLS CA cert */
tlsCaCert?: string;
/** Path to TLS client cert */
tlsCert?: string;
/** Path to TLS client key */
tlsKey?: string;
/** Whether to verify TLS (default: true) */
tlsVerify?: boolean;
}
export interface DockerNodePersistenceConfig {
/** Named Docker volume for Fusion data */
volumeName?: string;
/** Whether to retain the volume when the node is deleted (default: false) */
retainOnDelete?: boolean;
}
export function validateDockerNodeConfig(config: unknown): {
valid: boolean;
config?: DockerNodeConfig;
errors?: string[];
} {
const errors: string[] = [];
if (!config || typeof config !== "object" || Array.isArray(config)) {
return { valid: false, errors: ["config must be an object"] };
}
const candidate = config as Record<string, unknown>;
if (typeof candidate.image !== "string" || !candidate.image.trim()) {
errors.push("image must be a non-empty string");
}
if (!Array.isArray(candidate.volumeMounts)) {
errors.push("volumeMounts must be an array");
} else {
candidate.volumeMounts.forEach((mount, index) => {
if (!mount || typeof mount !== "object" || Array.isArray(mount)) {
errors.push(`volumeMounts[${index}] must be an object`);
return;
}
const mountCandidate = mount as Record<string, unknown>;
if (typeof mountCandidate.hostPath !== "string") {
errors.push(`volumeMounts[${index}].hostPath must be a string`);
}
if (typeof mountCandidate.containerPath !== "string") {
errors.push(`volumeMounts[${index}].containerPath must be a string`);
}
if (mountCandidate.mode !== undefined && mountCandidate.mode !== "rw" && mountCandidate.mode !== "ro") {
errors.push(`volumeMounts[${index}].mode must be "rw" or "ro"`);
}
if (mountCandidate.type !== undefined && mountCandidate.type !== "volume" && mountCandidate.type !== "bind") {
errors.push(`volumeMounts[${index}].type must be "volume" or "bind"`);
}
});
}
if (!candidate.environment || typeof candidate.environment !== "object" || Array.isArray(candidate.environment)) {
errors.push("environment must be an object");
} else {
for (const [key, value] of Object.entries(candidate.environment)) {
if (typeof key !== "string" || typeof value !== "string") {
errors.push(`environment.${key} must be a string value`);
}
}
}
if (typeof candidate.configVersion !== "number" || !Number.isFinite(candidate.configVersion) || candidate.configVersion < 1) {
errors.push("configVersion must be a number >= 1");
}
const validateOptionalObject = (
fieldName: string,
value: unknown,
validators: Array<[string, (value: unknown) => boolean, string]>,
) => {
if (value === undefined) return;
if (!value || typeof value !== "object" || Array.isArray(value)) {
errors.push(`${fieldName} must be an object`);
return;
}
const typed = value as Record<string, unknown>;
for (const [prop, test, message] of validators) {
if (typed[prop] !== undefined && !test(typed[prop])) {
errors.push(`${fieldName}.${prop} ${message}`);
}
}
};
validateOptionalObject("resources", candidate.resources, [
["memoryBytes", (value) => typeof value === "number" && Number.isFinite(value), "must be a number"],
["cpuCount", (value) => typeof value === "number" && Number.isFinite(value), "must be a number"],
["pidsLimit", (value) => typeof value === "number" && Number.isFinite(value), "must be a number"],
]);
validateOptionalObject("host", candidate.host, [
["contextName", (value) => typeof value === "string", "must be a string"],
["dockerHost", (value) => typeof value === "string", "must be a string"],
["tlsCaCert", (value) => typeof value === "string", "must be a string"],
["tlsCert", (value) => typeof value === "string", "must be a string"],
["tlsKey", (value) => typeof value === "string", "must be a string"],
["tlsVerify", (value) => typeof value === "boolean", "must be a boolean"],
]);
validateOptionalObject("persistence", candidate.persistence, [
["volumeName", (value) => typeof value === "string", "must be a string"],
["retainOnDelete", (value) => typeof value === "boolean", "must be a boolean"],
]);
if (candidate.extraClis !== undefined) {
if (!Array.isArray(candidate.extraClis) || candidate.extraClis.some((item) => typeof item !== "string")) {
errors.push("extraClis must be an array of strings");
}
}
if (candidate.containerName !== undefined && typeof candidate.containerName !== "string") {
errors.push("containerName must be a string");
}
if (candidate.lastUpdated !== undefined && typeof candidate.lastUpdated !== "string") {
errors.push("lastUpdated must be a string");
}
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true, config: candidate as unknown as DockerNodeConfig };
}
export function sanitizeDockerNodeConfigForResponse(config: DockerNodeConfig): DockerNodeConfig {
const clone = structuredClone(config);
const sensitivePattern = /API_KEY|SECRET|TOKEN|PASSWORD/i;
for (const [key, value] of Object.entries(clone.environment)) {
if (sensitivePattern.test(key) && typeof value === "string") {
clone.environment[key] = "***";
}
}
if (clone.host?.tlsKey) {
clone.host.tlsKey = "***";
}
return clone;
}
/** Version information tracked per node for plugin synchronization */
export interface NodeVersionInfo {
/** Core Fusion application version (semver string, e.g., "0.1.0") */
appVersion: string;
/** Map of plugin-id → semver version string for all installed plugins */
pluginVersions: Record<string, string>;
/** ISO-8601 timestamp of the last sync operation */
lastSyncedAt: string;
}
/** Input for updating node version info. appVersion is optional and will be auto-filled if not provided. */
export type NodeVersionInfoInput = Omit<NodeVersionInfo, "appVersion"> & {
/** Core Fusion application version. If not provided, will be auto-filled with the current app version. */
appVersion?: string;
};
/** Lifecycle status of a managed Docker node. */
export type DockerNodeStatus = "creating" | "running" | "stopped" | "error" | "recreating" | "deleting";
/** Docker daemon connection settings for provisioning a managed node container. */
export interface DockerHostConfig {
/** Docker host URI (for example: tcp://192.168.1.50:2376 or unix:///var/run/docker.sock). */
host?: string;
/** Named Docker context to target. */
context?: string;
/** Whether to verify Docker daemon TLS certificates. */
tlsVerify?: boolean;
/** Path to Docker daemon CA certificate. */
tlsCaPath?: string;
/** Path to Docker client certificate. */
tlsCertPath?: string;
/** Path to Docker client private key. */
tlsKeyPath?: string;
}
/** Container CPU and memory limit settings for managed Docker nodes. */
export interface DockerResourceSizing {
/** Memory limit in MB (for example: 4096). */
memoryMB?: number;
/** CPU limit (for example: 2.0). */
cpus?: number;
/** Swap limit in MB (0 = unlimited swap, Docker default behavior). */
memorySwapMB?: number;
}
/** A single bind mount definition for a managed Docker node container. */
export interface DockerVolumeMount {
/** Absolute path on the host machine. */
hostPath: string;
/** Path inside the container. */
containerPath: string;
/** Mount mode. Defaults to read/write when omitted. */
mode?: "ro" | "rw";
}
/** Optional additional CLI tools installed in the managed Docker node image. */
export type DockerExtraCli = "claude-cli" | "droid-cli";
/** Persisted definition and lifecycle metadata for a managed Docker node. */
export interface ManagedDockerNode {
/** Unique managed Docker node ID (for example: dn_abc123). */
id: string;
/** Linked mesh node ID after registration, or null while provisioning. */
nodeId: string | null;
/** Display name (unique across managed Docker nodes). */
name: string;
/** Docker image repository/name (for example: runfusion/fusion). */
imageName: string;
/** Docker image tag (for example: latest or 0.2.0). */
imageTag: string;
/** Provisioned container ID, or null before container creation. */
containerId: string | null;
/** Current managed Docker lifecycle status. */
status: DockerNodeStatus;
/** Docker daemon host/context configuration used for operations. */
hostConfig: DockerHostConfig;
/** Environment variables injected into the container. */
envVars: Record<string, string>;
/** Bind mounts configured for this container. */
volumeMounts: DockerVolumeMount[];
/** Resource limits for this container. */
resourceSizing: DockerResourceSizing;
/** Optional extra CLI tools included in provisioning. */
extraClis: DockerExtraCli[];
/** Whether storage volumes persist across container recreation. */
persistentStorage: boolean;
/** Reachable URL for mesh/node registration once running. */
reachableUrl: string | null;
/** API key for the managed node, auto-generated or user-provided. */
apiKey: string | null;
/** Last provisioning/runtime error message when status is error. */
errorMessage: string | null;
/** ISO-8601 creation timestamp. */
createdAt: string;
/** ISO-8601 last update timestamp. */
updatedAt: string;
}
/** Input for creating a managed Docker node record. */
export type ManagedDockerNodeInput = Omit<
ManagedDockerNode,
"id" | "containerId" | "status" | "createdAt" | "updatedAt" | "errorMessage"
>;
/** Partial update payload for managed Docker nodes. */
export type ManagedDockerNodeUpdate = Partial<
Omit<ManagedDockerNode, "id" | "createdAt">
>;
/** Input to the mesh configuration generation process. */
export interface MeshConfigGeneratorInput {
/** The managed Docker node record (from FN-3107). */
managedNode: ManagedDockerNode;
/** The orchestrating node's URL (e.g., "http://192.168.1.10:4040"). */
orchestratorUrl: string;
/** The orchestrating node's API key for authentication. */
orchestratorApiKey: string;
/** Optional user-provided API key. If omitted, one is auto-generated. */
nodeApiKey?: string;
/** Optional container port override. If omitted, defaults to 4041. */
containerPort?: number;
}
/** Input to the end-to-end provision-and-register flow. */
export interface FullProvisioningInput {
/** The managed Docker node to configure and register. */
managedNode: ManagedDockerNode;
/** The orchestrating node's URL. */
orchestratorUrl: string;
/** The orchestrating node's API key. */
orchestratorApiKey: string;
/** Optional user-provided API key for the new node. */
nodeApiKey?: string;
/** Optional container port override. */
containerPort?: number;
}
/** Configuration bundle needed for a new node to join the mesh. */
export interface MeshConnectionConfig {
/** API key for authenticating to this node. Auto-generated if not provided by user. */
nodeApiKey: string;
/** The URL the orchestrating node uses to reach the new container. */
reachableUrl: string;
/** Orchestrating node's URL, pushed to the container so it knows its mesh parent. */
orchestratorUrl: string;
/** Orchestrating node's API key for inbound settings sync authentication. */
orchestratorApiKey: string;
/** Port the container's Fusion server will listen on. */
containerPort: number;
/** Environment variables assembled from the above for injection into the container. */
envVars: Record<string, string>;
}
/** Result of applying mesh config to a provisioned node. */
export interface MeshConfigResult {
@@ -5466,144 +5183,6 @@ export interface MeshConfigResult {
error?: string;
}
/** Information about a discovered Docker context */
export interface DockerContextInfo {
/** Context name (e.g., "default", "my-remote") */
name: string;
/** Human-readable description */
description?: string;
/** Docker host URI for this context (e.g., "tcp://192.168.1.50:2376") */
dockerHost?: string;
/** Whether this is the currently active context */
isCurrentContext: boolean;
/** Whether this context has a connection error */
isError?: boolean;
/** Error message if the context is unreachable */
errorMessage?: string;
}
/** Result of testing Docker daemon connectivity */
export interface DockerConnectivityResult {
/** Whether the connection succeeded */
success: boolean;
/** Docker Engine version string */
dockerVersion?: string;
/** Docker API version string */
apiVersion?: string;
/** Docker Engine OS/arch info */
operatingSystem?: string;
/** Error message if connection failed */
error?: string;
/** Whether the target is the local Docker daemon */
isLocalDaemon: boolean;
}
/** Minimal container inspection result from Docker */
export interface DockerContainerInspectResult {
/** Container ID */
id: string;
/** Container name (with leading / stripped) */
name: string;
/** Container status string (e.g., "running", "exited") */
status: string;
/** Image name/tag */
image: string;
/** Creation timestamp (Unix epoch) */
created: number;
/** Detailed container state */
state: {
running: boolean;
paused: boolean;
restarting: boolean;
dead: boolean;
error?: string;
exitCode?: number;
startedAt?: string;
finishedAt?: string;
};
/** Optional exposed ports summary */
ports?: Record<string, string>;
}
/** Configuration for the Fusion Docker image to use for provisioning */
export interface DockerNodeImageConfig {
/** Image name (e.g., "runfusion/fusion" or "ghcr.io/runfusion/fusion") */
image: string;
/** Image tag (e.g., "latest", "0.14.1") */
tag: string;
/** Whether to pull the image before creating the container */
pullImage: boolean;
/** Optional registry authentication — username */
registryUsername?: string;
/** Optional registry authentication — password/token */
registryPassword?: string;
}
/** Resource constraints for a provisioned Docker container */
export interface DockerNodeResourceConfig {
/** CPU limit in cores (e.g., 2 = 2 CPUs). Undefined = unlimited */
cpuLimit?: number;
/** Memory limit in megabytes. Undefined = unlimited */
memoryLimitMb?: number;
/** Memory swap limit in megabytes. -1 = unlimited swap. Undefined = default */
memorySwapMb?: number;
}
/** Input for provisioning a new Docker-based Fusion node */
export interface DockerProvisionInput {
/** Display name for the node (must be unique) */
nodeName: string;
/** Docker host configuration — where to create the container */
hostConfig: DockerHostConfig;
/** Image configuration — which Fusion image to use */
imageConfig: DockerNodeImageConfig;
/** Resource constraints for the container */
resourceConfig?: DockerNodeResourceConfig;
/** Environment variables to set in the container (KEY=VALUE strings) */
environment?: string[];
/** Volume mount specifications (e.g., ["fusion-data:/data", "/host/path:/container/path"]) */
volumeMounts?: string[];
/** Named volume for persistent Fusion data storage. If provided, mounted at /data */
persistentVolume?: string;
/** Optional extra CLI tools to include in the container (e.g., ["claude", "droid"]) */
extraClis?: string[];
/** The URL/hostname where this node will be reachable by other nodes */
reachableUrl?: string;
/** Whether to auto-generate an API key for this node */
autoGenerateApiKey: boolean;
/** Explicit API key to use (if autoGenerateApiKey is false) */
apiKey?: string;
/** Maximum concurrent tasks for this node (default: 2) */
maxConcurrent?: number;
/** Optional Docker network to attach the container to */
network?: string;
/** Optional container labels (key-value pairs) */
labels?: Record<string, string>;
}
/** Result of a Docker node provisioning operation */
export interface DockerProvisionResult {
/** Whether provisioning succeeded */
success: boolean;
/** The container ID created by Docker */
containerId?: string;
/** The container name (generated or specified) */
containerName?: string;
/** The registered node ID in CentralCore */
nodeId?: string;
/** The API key generated or assigned for this node */
apiKey?: string;
/** The port mapping (if applicable) */
portMapping?: string;
/** Error message if provisioning failed */
error?: string;
/** The stage at which failure occurred (for error reporting) */
failedStage?: "image-pull" | "container-create" | "container-start" | "node-register" | "config-apply";
/** Duration of the provisioning operation in ms */
durationMs?: number;
}
/** A single plugin's version information for sync comparison */
export interface PluginVersionEntry {
/** Plugin ID (matches PluginManifest.id) */
pluginId: string;

View File

@@ -0,0 +1,493 @@
/**
* FNXC:CodeOrganization 2026-07-16-20:00:
* Docker node provision/status domain types peeled from types.ts.
*/
/** Persisted configuration for a Docker-managed Fusion node. */
export interface DockerNodeConfig {
/** Docker image name (e.g., "runfusion/fusion:latest") */
image: string;
/** Container name (defaults to "fusion-{nodeId}") */
containerName?: string;
/** Volume mount definitions */
volumeMounts: DockerNodeVolumeMount[];
/** Environment variable overrides (key-value pairs) */
environment: Record<string, string>;
/** Resource limits */
resources?: DockerNodeContainerResourceConfig;
/** Docker host connection settings */
host?: DockerNodeHostConfig;
/** Optional CLI tools to include in the container */
extraClis?: string[];
/** Persistent storage configuration */
persistence?: DockerNodePersistenceConfig;
/** Config version counter — starts at 1, auto-incremented on every update */
configVersion: number;
/** ISO timestamp of last config change (auto-set on update) */
lastUpdated?: string;
}
export interface DockerNodeVolumeMount {
/** Host path or named volume */
hostPath: string;
/** Container mount path */
containerPath: string;
/** "rw" (default) or "ro" */
mode?: "rw" | "ro";
/** "volume" (default) for named volumes, "bind" for host bind mounts */
type?: "volume" | "bind";
}
export interface DockerNodeContainerResourceConfig {
/** Memory limit in bytes (e.g., 2147483648 for 2GB) */
memoryBytes?: number;
/** CPU count limit (e.g., 2.0 for two cores) */
cpuCount?: number;
/** PIDs limit */
pidsLimit?: number;
}
export interface DockerNodeHostConfig {
/** Docker context name (for named Docker context selection) */
contextName?: string;
/** Explicit Docker host URL (e.g., "tcp://192.168.1.100:2376") */
dockerHost?: string;
/** Path to TLS CA cert */
tlsCaCert?: string;
/** Path to TLS client cert */
tlsCert?: string;
/** Path to TLS client key */
tlsKey?: string;
/** Whether to verify TLS (default: true) */
tlsVerify?: boolean;
}
export interface DockerNodePersistenceConfig {
/** Named Docker volume for Fusion data */
volumeName?: string;
/** Whether to retain the volume when the node is deleted (default: false) */
retainOnDelete?: boolean;
}
export function validateDockerNodeConfig(config: unknown): {
valid: boolean;
config?: DockerNodeConfig;
errors?: string[];
} {
const errors: string[] = [];
if (!config || typeof config !== "object" || Array.isArray(config)) {
return { valid: false, errors: ["config must be an object"] };
}
const candidate = config as Record<string, unknown>;
if (typeof candidate.image !== "string" || !candidate.image.trim()) {
errors.push("image must be a non-empty string");
}
if (!Array.isArray(candidate.volumeMounts)) {
errors.push("volumeMounts must be an array");
} else {
candidate.volumeMounts.forEach((mount, index) => {
if (!mount || typeof mount !== "object" || Array.isArray(mount)) {
errors.push(`volumeMounts[${index}] must be an object`);
return;
}
const mountCandidate = mount as Record<string, unknown>;
if (typeof mountCandidate.hostPath !== "string") {
errors.push(`volumeMounts[${index}].hostPath must be a string`);
}
if (typeof mountCandidate.containerPath !== "string") {
errors.push(`volumeMounts[${index}].containerPath must be a string`);
}
if (mountCandidate.mode !== undefined && mountCandidate.mode !== "rw" && mountCandidate.mode !== "ro") {
errors.push(`volumeMounts[${index}].mode must be "rw" or "ro"`);
}
if (mountCandidate.type !== undefined && mountCandidate.type !== "volume" && mountCandidate.type !== "bind") {
errors.push(`volumeMounts[${index}].type must be "volume" or "bind"`);
}
});
}
if (!candidate.environment || typeof candidate.environment !== "object" || Array.isArray(candidate.environment)) {
errors.push("environment must be an object");
} else {
for (const [key, value] of Object.entries(candidate.environment)) {
if (typeof key !== "string" || typeof value !== "string") {
errors.push(`environment.${key} must be a string value`);
}
}
}
if (typeof candidate.configVersion !== "number" || !Number.isFinite(candidate.configVersion) || candidate.configVersion < 1) {
errors.push("configVersion must be a number >= 1");
}
const validateOptionalObject = (
fieldName: string,
value: unknown,
validators: Array<[string, (value: unknown) => boolean, string]>,
) => {
if (value === undefined) return;
if (!value || typeof value !== "object" || Array.isArray(value)) {
errors.push(`${fieldName} must be an object`);
return;
}
const typed = value as Record<string, unknown>;
for (const [prop, test, message] of validators) {
if (typed[prop] !== undefined && !test(typed[prop])) {
errors.push(`${fieldName}.${prop} ${message}`);
}
}
};
validateOptionalObject("resources", candidate.resources, [
["memoryBytes", (value) => typeof value === "number" && Number.isFinite(value), "must be a number"],
["cpuCount", (value) => typeof value === "number" && Number.isFinite(value), "must be a number"],
["pidsLimit", (value) => typeof value === "number" && Number.isFinite(value), "must be a number"],
]);
validateOptionalObject("host", candidate.host, [
["contextName", (value) => typeof value === "string", "must be a string"],
["dockerHost", (value) => typeof value === "string", "must be a string"],
["tlsCaCert", (value) => typeof value === "string", "must be a string"],
["tlsCert", (value) => typeof value === "string", "must be a string"],
["tlsKey", (value) => typeof value === "string", "must be a string"],
["tlsVerify", (value) => typeof value === "boolean", "must be a boolean"],
]);
validateOptionalObject("persistence", candidate.persistence, [
["volumeName", (value) => typeof value === "string", "must be a string"],
["retainOnDelete", (value) => typeof value === "boolean", "must be a boolean"],
]);
if (candidate.extraClis !== undefined) {
if (!Array.isArray(candidate.extraClis) || candidate.extraClis.some((item) => typeof item !== "string")) {
errors.push("extraClis must be an array of strings");
}
}
if (candidate.containerName !== undefined && typeof candidate.containerName !== "string") {
errors.push("containerName must be a string");
}
if (candidate.lastUpdated !== undefined && typeof candidate.lastUpdated !== "string") {
errors.push("lastUpdated must be a string");
}
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true, config: candidate as unknown as DockerNodeConfig };
}
export function sanitizeDockerNodeConfigForResponse(config: DockerNodeConfig): DockerNodeConfig {
const clone = structuredClone(config);
const sensitivePattern = /API_KEY|SECRET|TOKEN|PASSWORD/i;
for (const [key, value] of Object.entries(clone.environment)) {
if (sensitivePattern.test(key) && typeof value === "string") {
clone.environment[key] = "***";
}
}
if (clone.host?.tlsKey) {
clone.host.tlsKey = "***";
}
return clone;
}
/** Version information tracked per node for plugin synchronization */
export interface NodeVersionInfo {
/** Core Fusion application version (semver string, e.g., "0.1.0") */
appVersion: string;
/** Map of plugin-id → semver version string for all installed plugins */
pluginVersions: Record<string, string>;
/** ISO-8601 timestamp of the last sync operation */
lastSyncedAt: string;
}
/** Input for updating node version info. appVersion is optional and will be auto-filled if not provided. */
export type NodeVersionInfoInput = Omit<NodeVersionInfo, "appVersion"> & {
/** Core Fusion application version. If not provided, will be auto-filled with the current app version. */
appVersion?: string;
};
/** Lifecycle status of a managed Docker node. */
export type DockerNodeStatus = "creating" | "running" | "stopped" | "error" | "recreating" | "deleting";
/** Docker daemon connection settings for provisioning a managed node container. */
export interface DockerHostConfig {
/** Docker host URI (for example: tcp://192.168.1.50:2376 or unix:///var/run/docker.sock). */
host?: string;
/** Named Docker context to target. */
context?: string;
/** Whether to verify Docker daemon TLS certificates. */
tlsVerify?: boolean;
/** Path to Docker daemon CA certificate. */
tlsCaPath?: string;
/** Path to Docker client certificate. */
tlsCertPath?: string;
/** Path to Docker client private key. */
tlsKeyPath?: string;
}
/** Container CPU and memory limit settings for managed Docker nodes. */
export interface DockerResourceSizing {
/** Memory limit in MB (for example: 4096). */
memoryMB?: number;
/** CPU limit (for example: 2.0). */
cpus?: number;
/** Swap limit in MB (0 = unlimited swap, Docker default behavior). */
memorySwapMB?: number;
}
/** A single bind mount definition for a managed Docker node container. */
export interface DockerVolumeMount {
/** Absolute path on the host machine. */
hostPath: string;
/** Path inside the container. */
containerPath: string;
/** Mount mode. Defaults to read/write when omitted. */
mode?: "ro" | "rw";
}
/** Optional additional CLI tools installed in the managed Docker node image. */
export type DockerExtraCli = "claude-cli" | "droid-cli";
/** Persisted definition and lifecycle metadata for a managed Docker node. */
export interface ManagedDockerNode {
/** Unique managed Docker node ID (for example: dn_abc123). */
id: string;
/** Linked mesh node ID after registration, or null while provisioning. */
nodeId: string | null;
/** Display name (unique across managed Docker nodes). */
name: string;
/** Docker image repository/name (for example: runfusion/fusion). */
imageName: string;
/** Docker image tag (for example: latest or 0.2.0). */
imageTag: string;
/** Provisioned container ID, or null before container creation. */
containerId: string | null;
/** Current managed Docker lifecycle status. */
status: DockerNodeStatus;
/** Docker daemon host/context configuration used for operations. */
hostConfig: DockerHostConfig;
/** Environment variables injected into the container. */
envVars: Record<string, string>;
/** Bind mounts configured for this container. */
volumeMounts: DockerVolumeMount[];
/** Resource limits for this container. */
resourceSizing: DockerResourceSizing;
/** Optional extra CLI tools included in provisioning. */
extraClis: DockerExtraCli[];
/** Whether storage volumes persist across container recreation. */
persistentStorage: boolean;
/** Reachable URL for mesh/node registration once running. */
reachableUrl: string | null;
/** API key for the managed node, auto-generated or user-provided. */
apiKey: string | null;
/** Last provisioning/runtime error message when status is error. */
errorMessage: string | null;
/** ISO-8601 creation timestamp. */
createdAt: string;
/** ISO-8601 last update timestamp. */
updatedAt: string;
}
/** Input for creating a managed Docker node record. */
export type ManagedDockerNodeInput = Omit<
ManagedDockerNode,
"id" | "containerId" | "status" | "createdAt" | "updatedAt" | "errorMessage"
>;
/** Partial update payload for managed Docker nodes. */
export type ManagedDockerNodeUpdate = Partial<
Omit<ManagedDockerNode, "id" | "createdAt">
>;
/** Input to the mesh configuration generation process. */
export interface MeshConfigGeneratorInput {
/** The managed Docker node record (from FN-3107). */
managedNode: ManagedDockerNode;
/** The orchestrating node's URL (e.g., "http://192.168.1.10:4040"). */
orchestratorUrl: string;
/** The orchestrating node's API key for authentication. */
orchestratorApiKey: string;
/** Optional user-provided API key. If omitted, one is auto-generated. */
nodeApiKey?: string;
/** Optional container port override. If omitted, defaults to 4041. */
containerPort?: number;
}
/** Input to the end-to-end provision-and-register flow. */
export interface FullProvisioningInput {
/** The managed Docker node to configure and register. */
managedNode: ManagedDockerNode;
/** The orchestrating node's URL. */
orchestratorUrl: string;
/** The orchestrating node's API key. */
orchestratorApiKey: string;
/** Optional user-provided API key for the new node. */
nodeApiKey?: string;
/** Optional container port override. */
containerPort?: number;
}
/** Configuration bundle needed for a new node to join the mesh. */
export interface MeshConnectionConfig {
/** API key for authenticating to this node. Auto-generated if not provided by user. */
nodeApiKey: string;
/** The URL the orchestrating node uses to reach the new container. */
reachableUrl: string;
/** Orchestrating node's URL, pushed to the container so it knows its mesh parent. */
orchestratorUrl: string;
/** Orchestrating node's API key for inbound settings sync authentication. */
orchestratorApiKey: string;
/** Port the container's Fusion server will listen on. */
containerPort: number;
/** Environment variables assembled from the above for injection into the container. */
envVars: Record<string, string>;
}
/** Information about a discovered Docker context */
export interface DockerContextInfo {
/** Context name (e.g., "default", "my-remote") */
name: string;
/** Human-readable description */
description?: string;
/** Docker host URI for this context (e.g., "tcp://192.168.1.50:2376") */
dockerHost?: string;
/** Whether this is the currently active context */
isCurrentContext: boolean;
/** Whether this context has a connection error */
isError?: boolean;
/** Error message if the context is unreachable */
errorMessage?: string;
}
/** Result of testing Docker daemon connectivity */
export interface DockerConnectivityResult {
/** Whether the connection succeeded */
success: boolean;
/** Docker Engine version string */
dockerVersion?: string;
/** Docker API version string */
apiVersion?: string;
/** Docker Engine OS/arch info */
operatingSystem?: string;
/** Error message if connection failed */
error?: string;
/** Whether the target is the local Docker daemon */
isLocalDaemon: boolean;
}
/** Minimal container inspection result from Docker */
export interface DockerContainerInspectResult {
/** Container ID */
id: string;
/** Container name (with leading / stripped) */
name: string;
/** Container status string (e.g., "running", "exited") */
status: string;
/** Image name/tag */
image: string;
/** Creation timestamp (Unix epoch) */
created: number;
/** Detailed container state */
state: {
running: boolean;
paused: boolean;
restarting: boolean;
dead: boolean;
error?: string;
exitCode?: number;
startedAt?: string;
finishedAt?: string;
};
/** Optional exposed ports summary */
ports?: Record<string, string>;
}
/** Configuration for the Fusion Docker image to use for provisioning */
export interface DockerNodeImageConfig {
/** Image name (e.g., "runfusion/fusion" or "ghcr.io/runfusion/fusion") */
image: string;
/** Image tag (e.g., "latest", "0.14.1") */
tag: string;
/** Whether to pull the image before creating the container */
pullImage: boolean;
/** Optional registry authentication — username */
registryUsername?: string;
/** Optional registry authentication — password/token */
registryPassword?: string;
}
/** Resource constraints for a provisioned Docker container */
export interface DockerNodeResourceConfig {
/** CPU limit in cores (e.g., 2 = 2 CPUs). Undefined = unlimited */
cpuLimit?: number;
/** Memory limit in megabytes. Undefined = unlimited */
memoryLimitMb?: number;
/** Memory swap limit in megabytes. -1 = unlimited swap. Undefined = default */
memorySwapMb?: number;
}
/** Input for provisioning a new Docker-based Fusion node */
export interface DockerProvisionInput {
/** Display name for the node (must be unique) */
nodeName: string;
/** Docker host configuration — where to create the container */
hostConfig: DockerHostConfig;
/** Image configuration — which Fusion image to use */
imageConfig: DockerNodeImageConfig;
/** Resource constraints for the container */
resourceConfig?: DockerNodeResourceConfig;
/** Environment variables to set in the container (KEY=VALUE strings) */
environment?: string[];
/** Volume mount specifications (e.g., ["fusion-data:/data", "/host/path:/container/path"]) */
volumeMounts?: string[];
/** Named volume for persistent Fusion data storage. If provided, mounted at /data */
persistentVolume?: string;
/** Optional extra CLI tools to include in the container (e.g., ["claude", "droid"]) */
extraClis?: string[];
/** The URL/hostname where this node will be reachable by other nodes */
reachableUrl?: string;
/** Whether to auto-generate an API key for this node */
autoGenerateApiKey: boolean;
/** Explicit API key to use (if autoGenerateApiKey is false) */
apiKey?: string;
/** Maximum concurrent tasks for this node (default: 2) */
maxConcurrent?: number;
/** Optional Docker network to attach the container to */
network?: string;
/** Optional container labels (key-value pairs) */
labels?: Record<string, string>;
}
/** Result of a Docker node provisioning operation */
export interface DockerProvisionResult {
/** Whether provisioning succeeded */
success: boolean;
/** The container ID created by Docker */
containerId?: string;
/** The container name (generated or specified) */
containerName?: string;
/** The registered node ID in CentralCore */
nodeId?: string;
/** The API key generated or assigned for this node */
apiKey?: string;
/** The port mapping (if applicable) */
portMapping?: string;
/** Error message if provisioning failed */
error?: string;
/** The stage at which failure occurred (for error reporting) */
failedStage?: "image-pull" | "container-create" | "container-start" | "node-register" | "config-apply";
/** Duration of the provisioning operation in ms */
durationMs?: number;
}
/** A single plugin's version information for sync comparison */

View File

@@ -0,0 +1,105 @@
/**
* FNXC:CodeOrganization 2026-07-16-20:00:
* Board workflow client types/API peeled from legacy.ts.
*/
import type {
Task,
WorkflowFieldDefinition,
WorkflowFieldType,
WorkflowFieldOption,
WorkflowFieldRender,
WorkflowSettingDefinition,
WorkflowSettingType,
WorkflowSettingOption,
WorkflowSettingRender,
WorkflowSettingRejection,
} from "@fusion/core";
import { api } from "./client.js";
import type { FetchOptions } from "./client.js";
import { withProjectId } from "./health.js";
import { dedupe } from "./dedupe.js";
// Workflow field/setting declaration types re-exported from @fusion/core so
// WorkflowSettingsPanel can import them from `../api` (KTD-13/14, U6/KTD-1).
export type {
WorkflowFieldDefinition,
WorkflowFieldType,
WorkflowFieldOption,
WorkflowFieldRender,
WorkflowSettingDefinition,
WorkflowSettingType,
WorkflowSettingOption,
WorkflowSettingRender,
WorkflowSettingRejection,
};
/** Resolved trait flags for a board column (subset the client cares about). */
export interface BoardWorkflowColumnFlags {
countsTowardWip?: boolean;
complete?: boolean;
archived?: boolean;
hiddenFromBoard?: boolean;
hold?: boolean;
intake?: boolean;
mergeBlocker?: boolean;
humanReview?: boolean;
[key: string]: boolean | undefined;
}
export interface BoardWorkflowColumn {
id: string;
name: string;
flags: BoardWorkflowColumnFlags;
}
export interface BoardWorkflowDefinition {
id: string;
name: string;
/** Optional compact custom workflow icon; built-ins render the Fusion mark by id. */
icon?: string;
columns: BoardWorkflowColumn[];
/** Custom field definitions declared by this workflow (U13/KTD-14). Absent on
* workflows with no fields, or from older servers. */
fields?: WorkflowFieldDefinition[];
}
export interface BoardWorkflowsPayload {
flagEnabled: boolean;
defaultWorkflowId: string;
workflows: BoardWorkflowDefinition[];
taskWorkflowIds: Record<string, string>;
}
/** A typed custom-field rejection surfaced by the PATCH endpoint (KTD-13). */
export interface CustomFieldRejection {
code: "no-fields-defined" | "unknown-field" | "type-mismatch" | "enum-violation";
fieldId: string;
detail: string;
}
/**
* Patch a task's custom field values (U13/KTD-14). The server validates the
* patch against the task's workflow field schema and returns the updated task;
* a validation failure surfaces as a 400 carrying `{ fieldId, code, detail }`.
* A `null` value for a field deletes it.
*/
export function updateTaskCustomFields(
id: string,
customFields: Record<string, unknown>,
projectId?: string,
): Promise<Task> {
return api<Task>(withProjectId(`/tasks/${id}/custom-fields`, projectId), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ customFields }),
});
}
/** Fetch the multi-lane board metadata (U9). When the flag is OFF the server
* returns `{ flagEnabled: false }` and the board renders its legacy form. */
export function fetchBoardWorkflows(projectId?: string, options?: FetchOptions): Promise<BoardWorkflowsPayload> {
const path = withProjectId("/tasks/board-workflows", projectId);
return dedupe(path, () => api<BoardWorkflowsPayload>(path), options);
}

View File

@@ -75,15 +75,6 @@ import type {
DockerNodeStatus,
ProjectNodePathMapping,
ApprovalRequestStatus,
WorkflowFieldDefinition,
WorkflowFieldType,
WorkflowFieldOption,
WorkflowFieldRender,
WorkflowSettingDefinition,
WorkflowSettingType,
WorkflowSettingOption,
WorkflowSettingRender,
WorkflowSettingRejection,
CommitAssociationDiffBackfillReport,
} from "@fusion/core";
// Consumers import backfill report types from the legacy API barrel.
@@ -231,6 +222,79 @@ export {
installUpdate,
} from "./settings.js";
export type { UpdateInstallResponse } from "./settings.js";
/*
* FNXC:CodeOrganization 2026-07-16-20:00:
* Preserve legacy board/remote/memory imports while implementations live in satellites.
*/
export {
updateTaskCustomFields,
fetchBoardWorkflows,
} from "./board-workflows.js";
export type {
BoardWorkflowColumnFlags,
BoardWorkflowColumn,
BoardWorkflowDefinition,
BoardWorkflowsPayload,
CustomFieldRejection,
WorkflowFieldDefinition,
WorkflowFieldType,
WorkflowFieldOption,
WorkflowFieldRender,
WorkflowSettingDefinition,
WorkflowSettingType,
WorkflowSettingOption,
WorkflowSettingRender,
WorkflowSettingRejection,
} from "./board-workflows.js";
export {
fetchRemoteSettings,
updateRemoteSettings,
fetchRemoteStatus,
installCloudflared,
activateRemoteProvider,
startRemoteTunnel,
stopRemoteTunnel,
killExternalTunnel,
regenerateRemotePersistentToken,
generateShortLivedRemoteToken,
fetchRemoteUrl,
fetchRemoteQr,
} from "./remote.js";
export type {
RemoteSettings,
RemoteStatus,
} from "./remote.js";
export {
fetchMemory,
saveMemory,
fetchMemoryFiles,
fetchMemoryFile,
saveMemoryFile,
compactMemory,
triggerMemoryDreams,
fetchMemoryInsights,
saveMemoryInsights,
triggerInsightExtraction,
fetchMemoryAudit,
fetchMemoryStats,
fetchMemoryBackendStatus,
installQmd,
testMemoryRetrieval,
} from "./memory.js";
export type {
MemoryFileInfo,
MemoryAuditReport,
MemoryBackendCapabilities,
MemoryBackendStatus,
MemorySearchResult,
MemoryRetrievalTestResult,
QmdInstallResult,
} from "./memory.js";
import type { MemoryFileInfo } from "./memory.js";
import { api, ApiRequestError, buildApiUrl, looksLikeHtml, proxyApi } from "./client.js";
import type { FetchOptions } from "./client.js";
import { withProjectId } from "./health.js";
@@ -256,484 +320,6 @@ export type {
SkillFileContent,
};
/** Resolved trait flags for a board column (subset the client cares about). */
export interface BoardWorkflowColumnFlags {
countsTowardWip?: boolean;
complete?: boolean;
archived?: boolean;
hiddenFromBoard?: boolean;
hold?: boolean;
intake?: boolean;
mergeBlocker?: boolean;
humanReview?: boolean;
[key: string]: boolean | undefined;
}
export interface BoardWorkflowColumn {
id: string;
name: string;
flags: BoardWorkflowColumnFlags;
}
// WorkflowFieldDefinition, WorkflowFieldType, WorkflowFieldOption, WorkflowFieldRender
// are re-exported from @fusion/core above (KTD-13/14).
export type { WorkflowFieldDefinition, WorkflowFieldType, WorkflowFieldOption, WorkflowFieldRender };
// Workflow-settings (U6/KTD-1) declaration types re-exported from @fusion/core so
// the WorkflowSettingsPanel imports them from `../api` like the field types.
export type { WorkflowSettingDefinition, WorkflowSettingType, WorkflowSettingOption, WorkflowSettingRender, WorkflowSettingRejection };
export interface BoardWorkflowDefinition {
id: string;
name: string;
/** Optional compact custom workflow icon; built-ins render the Fusion mark by id. */
icon?: string;
columns: BoardWorkflowColumn[];
/** Custom field definitions declared by this workflow (U13/KTD-14). Absent on
* workflows with no fields, or from older servers. */
fields?: WorkflowFieldDefinition[];
}
export interface BoardWorkflowsPayload {
flagEnabled: boolean;
defaultWorkflowId: string;
workflows: BoardWorkflowDefinition[];
taskWorkflowIds: Record<string, string>;
}
/** A typed custom-field rejection surfaced by the PATCH endpoint (KTD-13). */
export interface CustomFieldRejection {
code: "no-fields-defined" | "unknown-field" | "type-mismatch" | "enum-violation";
fieldId: string;
detail: string;
}
/**
* Patch a task's custom field values (U13/KTD-14). The server validates the
* patch against the task's workflow field schema and returns the updated task;
* a validation failure surfaces as a 400 carrying `{ fieldId, code, detail }`.
* A `null` value for a field deletes it.
*/
export function updateTaskCustomFields(
id: string,
customFields: Record<string, unknown>,
projectId?: string,
): Promise<Task> {
return api<Task>(withProjectId(`/tasks/${id}/custom-fields`, projectId), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ customFields }),
});
}
/** Fetch the multi-lane board metadata (U9). When the flag is OFF the server
* returns `{ flagEnabled: false }` and the board renders its legacy form. */
export function fetchBoardWorkflows(projectId?: string, options?: FetchOptions): Promise<BoardWorkflowsPayload> {
const path = withProjectId("/tasks/board-workflows", projectId);
return dedupe(path, () => api<BoardWorkflowsPayload>(path), options);
}
export interface RemoteSettings {
remoteActiveProvider: "tailscale" | "cloudflare" | null;
remoteTailscaleEnabled: boolean;
remoteTailscaleHostname: string;
remoteTailscaleTargetPort: number;
remoteTailscaleAcceptRoutes: boolean;
remoteCloudflareEnabled: boolean;
remoteCloudflareQuickTunnel: boolean;
remoteCloudflareTunnelName: string;
remoteCloudflareTunnelToken: string | null;
remoteCloudflareIngressUrl: string;
remotePersistentToken: string | null;
remoteShortLivedEnabled: boolean;
remoteShortLivedTtlMs: number;
remoteShortLivedMaxTtlMs: number;
remoteRememberLastRunning: boolean;
remoteWasRunningOnShutdown: boolean;
remoteLastStartedProvider: "tailscale" | "cloudflare" | null;
}
export interface RemoteStatus {
provider: "tailscale" | "cloudflare" | null;
state: "stopped" | "starting" | "running" | "stopping" | "failed";
url: string | null;
lastError: string | null;
lastErrorCode?: string | null;
cloudflaredAvailable?: boolean | null;
externalTunnel?: {
provider: "tailscale" | "cloudflare";
url: string | null;
} | null;
restore?: {
outcome: "applied" | "skipped" | "failed";
reason: string;
at: string;
provider: "tailscale" | "cloudflare" | null;
message?: string;
};
}
export function fetchRemoteSettings(projectId?: string): Promise<{ settings: RemoteSettings }> {
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId));
}
export function updateRemoteSettings(
settings: Partial<RemoteSettings>,
projectId?: string,
): Promise<{ settings: RemoteSettings }> {
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId), {
method: "PUT",
body: JSON.stringify(settings),
});
}
export function fetchRemoteStatus(projectId?: string): Promise<RemoteStatus> {
return api<RemoteStatus>(withProjectId("/remote/status", projectId));
}
export function installCloudflared(projectId?: string): Promise<{ success: boolean; command: string; error?: string }> {
return api(withProjectId("/remote/install-cloudflared", projectId), {
method: "POST",
});
}
export function activateRemoteProvider(provider: "tailscale" | "cloudflare", projectId?: string): Promise<{ activeProvider: "tailscale" | "cloudflare" }> {
return api<{ activeProvider: "tailscale" | "cloudflare" }>(withProjectId("/remote/provider/activate", projectId), {
method: "POST",
body: JSON.stringify({ provider }),
});
}
export function startRemoteTunnel(projectId?: string): Promise<{ state: "starting" | "running"; provider: string }> {
return api<{ state: "starting" | "running"; provider: string }>(withProjectId("/remote/tunnel/start", projectId), {
method: "POST",
});
}
export function stopRemoteTunnel(projectId?: string): Promise<{ state: "stopped"; provider: string | null }> {
return api<{ state: "stopped"; provider: string | null }>(withProjectId("/remote/tunnel/stop", projectId), {
method: "POST",
});
}
export function killExternalTunnel(projectId?: string): Promise<{ ok: boolean }> {
return api<{ ok: boolean }>(withProjectId("/remote/tunnel/kill-external", projectId), {
method: "POST",
});
}
export function regenerateRemotePersistentToken(projectId?: string): Promise<{ token: string; maskedToken: string }> {
return api<{ token: string; maskedToken: string }>(withProjectId("/remote/token/persistent/regenerate", projectId), {
method: "POST",
});
}
export function generateShortLivedRemoteToken(ttlMs: number, projectId?: string): Promise<{ token: string; expiresAt: string; ttlMs: number }> {
return api<{ token: string; expiresAt: string; ttlMs: number }>(withProjectId("/remote/token/short-lived/generate", projectId), {
method: "POST",
body: JSON.stringify({ ttlMs }),
});
}
type RemoteAuthTokenType = "persistent" | "short-lived";
type RemoteLinkRequestOptions = {
projectId?: string;
tokenType?: RemoteAuthTokenType;
ttlMs?: number;
};
function buildRemoteAuthQuery(
format: "text" | "image/svg" | null,
tokenType: RemoteAuthTokenType,
ttlMs?: number,
): string {
const params = new URLSearchParams();
if (format) {
params.set("format", format);
}
params.set("tokenType", tokenType);
if (tokenType === "short-lived" && typeof ttlMs === "number" && Number.isFinite(ttlMs)) {
params.set("ttlMs", String(ttlMs));
}
const query = params.toString();
return query ? `?${query}` : "";
}
export function fetchRemoteUrl(
options: RemoteLinkRequestOptions = {},
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }> {
const { projectId, tokenType = "persistent", ttlMs } = options;
const query = buildRemoteAuthQuery(null, tokenType, ttlMs);
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }>(withProjectId(`/remote/url${query}`, projectId));
}
export function fetchRemoteQr(
format: "text" | "image/svg" = "text",
options: RemoteLinkRequestOptions = {},
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }> {
const { projectId, tokenType = "persistent", ttlMs } = options;
const query = buildRemoteAuthQuery(format, tokenType, ttlMs);
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }>(withProjectId(`/remote/qr${query}`, projectId));
}
export function fetchMemory(projectId?: string): Promise<{ content: string }> {
return api<{ content: string }>(withProjectId("/memory", projectId));
}
export function saveMemory(content: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/memory", projectId), {
method: "PUT",
body: JSON.stringify({ content }),
});
}
export interface MemoryFileInfo {
path: string;
label: string;
layer: "long-term" | "daily" | "dreams";
size: number;
updatedAt: string;
}
export function fetchMemoryFiles(projectId?: string): Promise<{ files: MemoryFileInfo[] }> {
return api<{ files: MemoryFileInfo[] }>(withProjectId("/memory/files", projectId));
}
export function fetchMemoryFile(path: string, projectId?: string): Promise<{ path: string; content: string }> {
const query = `path=${encodeURIComponent(path)}`;
return api<{ path: string; content: string }>(withProjectId(`/memory/file?${query}`, projectId));
}
export function saveMemoryFile(path: string, content: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/memory/file", projectId), {
method: "PUT",
body: JSON.stringify({ path, content }),
});
}
/**
* Compact memory content using AI to distill it down to the most important insights.
* Reads one memory file, compacts it via AI, and writes the result back.
*
* Backwards-compatible call patterns:
* - compactMemory(projectId?)
* - compactMemory(path, projectId?)
*
* @param pathOrProjectId - Memory file path or legacy projectId-only argument
* @param projectId - Optional project ID for multi-project support
* @returns Promise resolving to the compacted memory content
*/
export function compactMemory(
pathOrProjectId?: string,
projectId?: string,
): Promise<{ path?: string; content: string }> {
let path: string | undefined;
let effectiveProjectId = projectId;
if (projectId !== undefined) {
path = pathOrProjectId;
} else if (typeof pathOrProjectId === "string" && pathOrProjectId.trim().length > 0) {
const trimmed = pathOrProjectId.trim();
const looksLikeMemoryPath = trimmed.includes("/") || trimmed.endsWith(".md") || trimmed.startsWith(".");
if (looksLikeMemoryPath) {
path = trimmed;
} else {
effectiveProjectId = trimmed;
}
}
return api<{ path?: string; content: string }>(withProjectId("/memory/compact", effectiveProjectId), {
method: "POST",
body: JSON.stringify(path ? { path } : {}),
});
}
/**
* Trigger manual memory dream processing.
* Synthesizes daily notes into dreams and promotes durable lessons to long-term memory.
*
* @param projectId - Optional project ID for multi-project support
* @returns Promise resolving to dream processing result
*/
export function triggerMemoryDreams(projectId?: string): Promise<{
success: boolean;
summary?: string;
dreamsWritten?: boolean;
longTermUpdatesWritten?: boolean;
error?: string;
}> {
return api(withProjectId("/memory/dream", projectId), {
method: "POST",
});
}
/** Memory audit report type (mirrors @fusion/core MemoryAuditReport) */
export interface MemoryAuditReport {
generatedAt: string;
workingMemory: {
exists: boolean;
size: number;
sectionCount: number;
lastModified?: string;
};
insightsMemory: {
exists: boolean;
size: number;
insightCount: number;
categories: Record<string, number>;
lastUpdated?: string;
};
extraction: {
runAt: string;
success: boolean;
insightCount: number;
duplicateCount: number;
skippedCount: number;
summary: string;
error?: string;
};
pruning: {
applied: boolean;
reason: string;
sizeDelta: number;
originalSize: number;
newSize: number;
};
checks: Array<{
id: string;
name: string;
passed: boolean;
details: string;
}>;
health: "healthy" | "warning" | "issues";
}
/**
* Fetch memory insights content.
* Returns { content: string | null, exists: boolean }.
* content is null when no insights file exists yet.
*/
export function fetchMemoryInsights(projectId?: string): Promise<{ content: string | null; exists: boolean }> {
return api<{ content: string | null; exists: boolean }>(withProjectId("/memory/insights", projectId));
}
/**
* Save memory insights content.
* The insights file stores parsed long-term memory grouped by category.
*/
export function saveMemoryInsights(content: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/memory/insights", projectId), {
method: "PUT",
body: JSON.stringify({ content }),
});
}
/**
* Trigger AI-powered insight extraction from working memory.
* Reads working memory, generates insights via AI, merges/prunes existing insights,
* and generates an audit report.
*
* Returns: { success: boolean, summary: string, insightCount: number, pruned: boolean }
*/
export function triggerInsightExtraction(projectId?: string): Promise<{ success: boolean; summary: string; insightCount: number; pruned: boolean }> {
return api<{ success: boolean; summary: string; insightCount: number; pruned: boolean }>(withProjectId("/memory/extract", projectId), {
method: "POST",
});
}
/**
* Fetch memory audit report.
* The audit checks working memory and insights memory state, extraction history,
* and generates health recommendations.
*/
export function fetchMemoryAudit(projectId?: string): Promise<MemoryAuditReport> {
return api<MemoryAuditReport>(withProjectId("/memory/audit", projectId));
}
/**
* Fetch quick memory stats (lightweight, no AI).
* Useful for dashboard displays showing memory size and insight counts.
*
* Returns: { workingMemorySize: number, insightsSize: number, insightsExists: boolean }
*/
export function fetchMemoryStats(projectId?: string): Promise<{ workingMemorySize: number; insightsSize: number; insightsExists: boolean }> {
return api<{ workingMemorySize: number; insightsSize: number; insightsExists: boolean }>(withProjectId("/memory/stats", projectId));
}
/**
* Memory backend capabilities returned by the backend status API.
*/
export interface MemoryBackendCapabilities {
readable: boolean;
writable: boolean;
supportsAtomicWrite: boolean;
hasConflictResolution: boolean;
persistent: boolean;
}
/**
* Memory backend status response from GET /api/memory/backend
*/
export interface MemoryBackendStatus {
/** The effective backend type after runtime resolution */
currentBackend: string;
/** Capabilities of the effective backend */
capabilities: MemoryBackendCapabilities;
/** List of registered backend types available */
availableBackends: string[];
/** Whether the qmd CLI is available on PATH */
qmdAvailable?: boolean;
/** Suggested install command when qmd is unavailable */
qmdInstallCommand?: string;
}
export interface MemorySearchResult {
path: string;
lineStart: number;
lineEnd: number;
snippet: string;
score: number;
backend: string;
}
export interface MemoryRetrievalTestResult {
query: string;
qmdAvailable: boolean;
usedFallback: boolean;
qmdInstallCommand: string;
results: MemorySearchResult[];
}
export interface QmdInstallResult {
success: boolean;
qmdAvailable: boolean;
qmdInstallCommand: string;
}
/**
* Fetch the current memory backend status and capabilities.
* Use this to determine which backend is active and what operations it supports.
*/
export function fetchMemoryBackendStatus(projectId?: string): Promise<MemoryBackendStatus> {
return api<MemoryBackendStatus>(withProjectId("/memory/backend", projectId));
}
export function installQmd(projectId?: string): Promise<QmdInstallResult> {
return api<QmdInstallResult>(withProjectId("/memory/install-qmd", projectId), {
method: "POST",
});
}
export function testMemoryRetrieval(query: string, projectId?: string): Promise<MemoryRetrievalTestResult> {
return api<MemoryRetrievalTestResult>(withProjectId("/memory/test", projectId), {
method: "POST",
body: JSON.stringify({ query }),
});
}
/** Fetch global (user-level) settings from ~/.fusion/settings.json */
export function fetchGlobalSettings(options?: FetchOptions): Promise<GlobalSettings> {
return dedupe("/settings/global", () => api<GlobalSettings>("/settings/global"), options);
}

View File

@@ -0,0 +1,249 @@
/**
* FNXC:CodeOrganization 2026-07-16-20:00:
* Memory client API peeled from legacy.ts.
*/
import { api } from "./client.js";
import { withProjectId } from "./health.js";
export function fetchMemory(projectId?: string): Promise<{ content: string }> {
return api<{ content: string }>(withProjectId("/memory", projectId));
}
export function saveMemory(content: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/memory", projectId), {
method: "PUT",
body: JSON.stringify({ content }),
});
}
export interface MemoryFileInfo {
path: string;
label: string;
layer: "long-term" | "daily" | "dreams";
size: number;
updatedAt: string;
}
export function fetchMemoryFiles(projectId?: string): Promise<{ files: MemoryFileInfo[] }> {
return api<{ files: MemoryFileInfo[] }>(withProjectId("/memory/files", projectId));
}
export function fetchMemoryFile(path: string, projectId?: string): Promise<{ path: string; content: string }> {
const query = `path=${encodeURIComponent(path)}`;
return api<{ path: string; content: string }>(withProjectId(`/memory/file?${query}`, projectId));
}
export function saveMemoryFile(path: string, content: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/memory/file", projectId), {
method: "PUT",
body: JSON.stringify({ path, content }),
});
}
/**
* Compact memory content using AI to distill it down to the most important insights.
* Reads one memory file, compacts it via AI, and writes the result back.
*
* FNXC:CodeOrganization 2026-07-17-00:00:
* Call contract is explicit: first arg is always an optional memory path, second is
* optional projectId. Do not overload a single string as path-or-projectId — that
* dropped project scoping for values that did not look like paths (e.g. bare project ids).
*
* @param path - Optional memory file path to compact (omit to compact default)
* @param projectId - Optional project ID for multi-project support
* @returns Promise resolving to the compacted memory content
*/
export function compactMemory(
path?: string,
projectId?: string,
): Promise<{ path?: string; content: string }> {
return api<{ path?: string; content: string }>(withProjectId("/memory/compact", projectId), {
method: "POST",
body: JSON.stringify(path ? { path } : {}),
});
}
/**
* Trigger manual memory dream processing.
* Synthesizes daily notes into dreams and promotes durable lessons to long-term memory.
*
* @param projectId - Optional project ID for multi-project support
* @returns Promise resolving to dream processing result
*/
export function triggerMemoryDreams(projectId?: string): Promise<{
success: boolean;
summary?: string;
dreamsWritten?: boolean;
longTermUpdatesWritten?: boolean;
error?: string;
}> {
return api(withProjectId("/memory/dream", projectId), {
method: "POST",
});
}
/** Memory audit report type (mirrors @fusion/core MemoryAuditReport) */
export interface MemoryAuditReport {
generatedAt: string;
workingMemory: {
exists: boolean;
size: number;
sectionCount: number;
lastModified?: string;
};
insightsMemory: {
exists: boolean;
size: number;
insightCount: number;
categories: Record<string, number>;
lastUpdated?: string;
};
extraction: {
runAt: string;
success: boolean;
insightCount: number;
duplicateCount: number;
skippedCount: number;
summary: string;
error?: string;
};
pruning: {
applied: boolean;
reason: string;
sizeDelta: number;
originalSize: number;
newSize: number;
};
checks: Array<{
id: string;
name: string;
passed: boolean;
details: string;
}>;
health: "healthy" | "warning" | "issues";
}
/**
* Fetch memory insights content.
* Returns { content: string | null, exists: boolean }.
* content is null when no insights file exists yet.
*/
export function fetchMemoryInsights(projectId?: string): Promise<{ content: string | null; exists: boolean }> {
return api<{ content: string | null; exists: boolean }>(withProjectId("/memory/insights", projectId));
}
/**
* Save memory insights content.
* The insights file stores parsed long-term memory grouped by category.
*/
export function saveMemoryInsights(content: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/memory/insights", projectId), {
method: "PUT",
body: JSON.stringify({ content }),
});
}
/**
* Trigger AI-powered insight extraction from working memory.
* Reads working memory, generates insights via AI, merges/prunes existing insights,
* and generates an audit report.
*
* Returns: { success: boolean, summary: string, insightCount: number, pruned: boolean }
*/
export function triggerInsightExtraction(projectId?: string): Promise<{ success: boolean; summary: string; insightCount: number; pruned: boolean }> {
return api<{ success: boolean; summary: string; insightCount: number; pruned: boolean }>(withProjectId("/memory/extract", projectId), {
method: "POST",
});
}
/**
* Fetch memory audit report.
* The audit checks working memory and insights memory state, extraction history,
* and generates health recommendations.
*/
export function fetchMemoryAudit(projectId?: string): Promise<MemoryAuditReport> {
return api<MemoryAuditReport>(withProjectId("/memory/audit", projectId));
}
/**
* Fetch quick memory stats (lightweight, no AI).
* Useful for dashboard displays showing memory size and insight counts.
*
* Returns: { workingMemorySize: number, insightsSize: number, insightsExists: boolean }
*/
export function fetchMemoryStats(projectId?: string): Promise<{ workingMemorySize: number; insightsSize: number; insightsExists: boolean }> {
return api<{ workingMemorySize: number; insightsSize: number; insightsExists: boolean }>(withProjectId("/memory/stats", projectId));
}
/**
* Memory backend capabilities returned by the backend status API.
*/
export interface MemoryBackendCapabilities {
readable: boolean;
writable: boolean;
supportsAtomicWrite: boolean;
hasConflictResolution: boolean;
persistent: boolean;
}
/**
* Memory backend status response from GET /api/memory/backend
*/
export interface MemoryBackendStatus {
/** The effective backend type after runtime resolution */
currentBackend: string;
/** Capabilities of the effective backend */
capabilities: MemoryBackendCapabilities;
/** List of registered backend types available */
availableBackends: string[];
/** Whether the qmd CLI is available on PATH */
qmdAvailable?: boolean;
/** Suggested install command when qmd is unavailable */
qmdInstallCommand?: string;
}
export interface MemorySearchResult {
path: string;
lineStart: number;
lineEnd: number;
snippet: string;
score: number;
backend: string;
}
export interface MemoryRetrievalTestResult {
query: string;
qmdAvailable: boolean;
usedFallback: boolean;
qmdInstallCommand: string;
results: MemorySearchResult[];
}
export interface QmdInstallResult {
success: boolean;
qmdAvailable: boolean;
qmdInstallCommand: string;
}
/**
* Fetch the current memory backend status and capabilities.
* Use this to determine which backend is active and what operations it supports.
*/
export function fetchMemoryBackendStatus(projectId?: string): Promise<MemoryBackendStatus> {
return api<MemoryBackendStatus>(withProjectId("/memory/backend", projectId));
}
export function installQmd(projectId?: string): Promise<QmdInstallResult> {
return api<QmdInstallResult>(withProjectId("/memory/install-qmd", projectId), {
method: "POST",
});
}
export function testMemoryRetrieval(query: string, projectId?: string): Promise<MemoryRetrievalTestResult> {
return api<MemoryRetrievalTestResult>(withProjectId("/memory/test", projectId), {
method: "POST",
body: JSON.stringify({ query }),
});
}
/** Fetch global (user-level) settings from ~/.fusion/settings.json */

View File

@@ -0,0 +1,151 @@
/**
* FNXC:CodeOrganization 2026-07-16-20:00:
* Remote access / tunnel client API peeled from legacy.ts.
*/
import { api } from "./client.js";
import { withProjectId } from "./health.js";
export interface RemoteSettings {
remoteActiveProvider: "tailscale" | "cloudflare" | null;
remoteTailscaleEnabled: boolean;
remoteTailscaleHostname: string;
remoteTailscaleTargetPort: number;
remoteTailscaleAcceptRoutes: boolean;
remoteCloudflareEnabled: boolean;
remoteCloudflareQuickTunnel: boolean;
remoteCloudflareTunnelName: string;
remoteCloudflareTunnelToken: string | null;
remoteCloudflareIngressUrl: string;
remotePersistentToken: string | null;
remoteShortLivedEnabled: boolean;
remoteShortLivedTtlMs: number;
remoteShortLivedMaxTtlMs: number;
remoteRememberLastRunning: boolean;
remoteWasRunningOnShutdown: boolean;
remoteLastStartedProvider: "tailscale" | "cloudflare" | null;
}
export interface RemoteStatus {
provider: "tailscale" | "cloudflare" | null;
state: "stopped" | "starting" | "running" | "stopping" | "failed";
url: string | null;
lastError: string | null;
lastErrorCode?: string | null;
cloudflaredAvailable?: boolean | null;
externalTunnel?: {
provider: "tailscale" | "cloudflare";
url: string | null;
} | null;
restore?: {
outcome: "applied" | "skipped" | "failed";
reason: string;
at: string;
provider: "tailscale" | "cloudflare" | null;
message?: string;
};
}
export function fetchRemoteSettings(projectId?: string): Promise<{ settings: RemoteSettings }> {
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId));
}
export function updateRemoteSettings(
settings: Partial<RemoteSettings>,
projectId?: string,
): Promise<{ settings: RemoteSettings }> {
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId), {
method: "PUT",
body: JSON.stringify(settings),
});
}
export function fetchRemoteStatus(projectId?: string): Promise<RemoteStatus> {
return api<RemoteStatus>(withProjectId("/remote/status", projectId));
}
export function installCloudflared(projectId?: string): Promise<{ success: boolean; command: string; error?: string }> {
return api(withProjectId("/remote/install-cloudflared", projectId), {
method: "POST",
});
}
export function activateRemoteProvider(provider: "tailscale" | "cloudflare", projectId?: string): Promise<{ activeProvider: "tailscale" | "cloudflare" }> {
return api<{ activeProvider: "tailscale" | "cloudflare" }>(withProjectId("/remote/provider/activate", projectId), {
method: "POST",
body: JSON.stringify({ provider }),
});
}
export function startRemoteTunnel(projectId?: string): Promise<{ state: "starting" | "running"; provider: string }> {
return api<{ state: "starting" | "running"; provider: string }>(withProjectId("/remote/tunnel/start", projectId), {
method: "POST",
});
}
export function stopRemoteTunnel(projectId?: string): Promise<{ state: "stopped"; provider: string | null }> {
return api<{ state: "stopped"; provider: string | null }>(withProjectId("/remote/tunnel/stop", projectId), {
method: "POST",
});
}
export function killExternalTunnel(projectId?: string): Promise<{ ok: boolean }> {
return api<{ ok: boolean }>(withProjectId("/remote/tunnel/kill-external", projectId), {
method: "POST",
});
}
export function regenerateRemotePersistentToken(projectId?: string): Promise<{ token: string; maskedToken: string }> {
return api<{ token: string; maskedToken: string }>(withProjectId("/remote/token/persistent/regenerate", projectId), {
method: "POST",
});
}
export function generateShortLivedRemoteToken(ttlMs: number, projectId?: string): Promise<{ token: string; expiresAt: string; ttlMs: number }> {
return api<{ token: string; expiresAt: string; ttlMs: number }>(withProjectId("/remote/token/short-lived/generate", projectId), {
method: "POST",
body: JSON.stringify({ ttlMs }),
});
}
type RemoteAuthTokenType = "persistent" | "short-lived";
type RemoteLinkRequestOptions = {
projectId?: string;
tokenType?: RemoteAuthTokenType;
ttlMs?: number;
};
function buildRemoteAuthQuery(
format: "text" | "image/svg" | null,
tokenType: RemoteAuthTokenType,
ttlMs?: number,
): string {
const params = new URLSearchParams();
if (format) {
params.set("format", format);
}
params.set("tokenType", tokenType);
if (tokenType === "short-lived" && typeof ttlMs === "number" && Number.isFinite(ttlMs)) {
params.set("ttlMs", String(ttlMs));
}
const query = params.toString();
return query ? `?${query}` : "";
}
export function fetchRemoteUrl(
options: RemoteLinkRequestOptions = {},
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }> {
const { projectId, tokenType = "persistent", ttlMs } = options;
const query = buildRemoteAuthQuery(null, tokenType, ttlMs);
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }>(withProjectId(`/remote/url${query}`, projectId));
}
export function fetchRemoteQr(
format: "text" | "image/svg" = "text",
options: RemoteLinkRequestOptions = {},
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }> {
const { projectId, tokenType = "persistent", ttlMs } = options;
const query = buildRemoteAuthQuery(format, tokenType, ttlMs);
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }>(withProjectId(`/remote/qr${query}`, projectId));
}

View File

@@ -16,8 +16,8 @@
"packages/core/src/mission-store.ts": 4364,
"packages/core/src/postgres/sqlite-migrator.ts": 2068,
"packages/core/src/store.ts": 2631,
"packages/core/src/types.ts": 7235,
"packages/dashboard/app/api/legacy.ts": 10935,
"packages/core/src/types.ts": 6928,
"packages/dashboard/app/api/legacy.ts": 10561,
"packages/dashboard/app/components/AgentDetailView.tsx": 5552,
"packages/dashboard/app/components/AgentsView.tsx": 2254,
"packages/dashboard/app/components/ChatView.tsx": 3850,