@neuralnomads/codenomad-dev 0.20.0-dev-20260916-59d450c1 → 0.20.0-dev-20260917-aa51cbb9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api-types.js.map +1 -1
- package/dist/opencode/compatibility/contract-fixture.js +15 -0
- package/dist/opencode/compatibility/contract-fixture.js.map +1 -0
- package/dist/opencode/compatibility/events.js +9 -0
- package/dist/opencode/compatibility/events.js.map +1 -0
- package/dist/opencode/compatibility/location.js +89 -0
- package/dist/opencode/compatibility/location.js.map +1 -0
- package/dist/opencode/compatibility/location.test.js +92 -0
- package/dist/opencode/compatibility/location.test.js.map +1 -0
- package/dist/opencode/compatibility/negotiate.js +73 -0
- package/dist/opencode/compatibility/negotiate.js.map +1 -0
- package/dist/opencode/compatibility/negotiate.test.js +61 -0
- package/dist/opencode/compatibility/negotiate.test.js.map +1 -0
- package/dist/opencode/compatibility/proxy-locations.js +137 -0
- package/dist/opencode/compatibility/proxy-locations.js.map +1 -0
- package/dist/opencode/compatibility/proxy-locations.test.js +42 -0
- package/dist/opencode/compatibility/proxy-locations.test.js.map +1 -0
- package/dist/opencode/compatibility/proxy.js +60 -0
- package/dist/opencode/compatibility/proxy.js.map +1 -0
- package/dist/opencode/compatibility/proxy.test.js +47 -0
- package/dist/opencode/compatibility/proxy.test.js.map +1 -0
- package/dist/opencode/compatibility/requests.js +54 -0
- package/dist/opencode/compatibility/requests.js.map +1 -0
- package/dist/opencode/compatibility/runtime.js +26 -0
- package/dist/opencode/compatibility/runtime.js.map +1 -0
- package/dist/opencode/compatibility/transport.js +112 -0
- package/dist/opencode/compatibility/transport.js.map +1 -0
- package/dist/opencode/compatibility/transport.test.js +83 -0
- package/dist/opencode/compatibility/transport.test.js.map +1 -0
- package/dist/opencode/session-pruning/location.js +16 -0
- package/dist/opencode/session-pruning/location.js.map +1 -0
- package/dist/opencode/session-pruning/package.test.js +37 -0
- package/dist/opencode/session-pruning/package.test.js.map +1 -0
- package/dist/opencode/session-pruning/plugin.js +5 -2
- package/dist/opencode/session-pruning/plugin.js.map +1 -1
- package/dist/opencode/session-pruning/preview-store.js +3 -2
- package/dist/opencode/session-pruning/preview-store.js.map +1 -1
- package/dist/opencode/session-pruning/pruning.test.js +7 -5
- package/dist/opencode/session-pruning/pruning.test.js.map +1 -1
- package/dist/opencode/session-pruning/service.js +5 -4
- package/dist/opencode/session-pruning/service.js.map +1 -1
- package/dist/opencode/session-pruning/service.test.js +65 -53
- package/dist/opencode/session-pruning/service.test.js.map +1 -1
- package/dist/permissions/opencode-replier.js +2 -2
- package/dist/permissions/opencode-replier.js.map +1 -1
- package/dist/permissions/opencode-replier.test.js +1 -1
- package/dist/permissions/opencode-replier.test.js.map +1 -1
- package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
- package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
- package/dist/plugins/session-pruning/plugin.mjs +422 -271
- package/dist/server/__tests__/instance-proxy-legacy.test.js +111 -0
- package/dist/server/__tests__/instance-proxy-legacy.test.js.map +1 -0
- package/dist/server/__tests__/instance-proxy.test.js +44 -34
- package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
- package/dist/server/http-server.js +107 -238
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/routes/session-pruning.js +4 -2
- package/dist/server/routes/session-pruning.js.map +1 -1
- package/dist/server/routes/session-pruning.test.js +16 -2
- package/dist/server/routes/session-pruning.test.js.map +1 -1
- package/dist/server/routes/worktrees.js +1 -0
- package/dist/server/routes/worktrees.js.map +1 -1
- package/dist/server/routes/worktrees.test.js +54 -1
- package/dist/server/routes/worktrees.test.js.map +1 -1
- package/dist/workspaces/host-opencode-service.test.js +56 -7
- package/dist/workspaces/host-opencode-service.test.js.map +1 -1
- package/dist/workspaces/instance-client.js.map +1 -1
- package/dist/workspaces/instance-events.js +71 -57
- package/dist/workspaces/instance-events.js.map +1 -1
- package/dist/workspaces/instance-events.test.js +122 -9
- package/dist/workspaces/instance-events.test.js.map +1 -1
- package/dist/workspaces/manager.js +24 -39
- package/dist/workspaces/manager.js.map +1 -1
- package/dist/workspaces/manager.test.js +43 -7
- package/dist/workspaces/manager.test.js.map +1 -1
- package/dist/workspaces/opencode-cli-service.js +56 -29
- package/dist/workspaces/opencode-cli-service.js.map +1 -1
- package/dist/workspaces/opencode-connection-generation.test.js +57 -0
- package/dist/workspaces/opencode-connection-generation.test.js.map +1 -0
- package/dist/workspaces/opencode-service.js +70 -16
- package/dist/workspaces/opencode-service.js.map +1 -1
- package/dist/workspaces/opencode-service.test.js +3 -4
- package/dist/workspaces/opencode-service.test.js.map +1 -1
- package/dist/workspaces/worktree-session-evacuation.js +31 -11
- package/dist/workspaces/worktree-session-evacuation.js.map +1 -1
- package/dist/workspaces/worktree-session-evacuation.test.js +211 -0
- package/dist/workspaces/worktree-session-evacuation.test.js.map +1 -1
- package/dist/workspaces/wsl-opencode-service.test.js +64 -24
- package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
- package/package.json +3 -3
- package/public/assets/{App-CchaYx5f.js → App-BROhwPo9.js} +7 -7
- package/public/assets/App-BROhwPo9.js.map +1 -0
- package/public/assets/{FilesTab-D48ghuKz.js → FilesTab-DY3Vr-3r.js} +2 -2
- package/public/assets/{FilesTab-D48ghuKz.js.map → FilesTab-DY3Vr-3r.js.map} +1 -1
- package/public/assets/{GitChangesTab-CCmRJHOz.js → GitChangesTab-BD6CJngE.js} +2 -2
- package/public/assets/{GitChangesTab-CCmRJHOz.js.map → GitChangesTab-BD6CJngE.js.map} +1 -1
- package/public/assets/StatusTab-C-_2yaPa.js +2 -0
- package/public/assets/StatusTab-C-_2yaPa.js.map +1 -0
- package/public/assets/{align-justify-CGBrnO86.js → align-justify-DI8VTZ4G.js} +2 -2
- package/public/assets/{align-justify-CGBrnO86.js.map → align-justify-DI8VTZ4G.js.map} +1 -1
- package/public/assets/{diff-viewer-CmJjbHh5.js → diff-viewer-CGPsYEr3.js} +2 -2
- package/public/assets/{diff-viewer-CmJjbHh5.js.map → diff-viewer-CGPsYEr3.js.map} +1 -1
- package/public/assets/main-Cbx7KdTr.js +38 -0
- package/public/assets/main-Cbx7KdTr.js.map +1 -0
- package/public/assets/{markdown-CwQB_x0O.js → markdown-BWnjyvnX.js} +2 -2
- package/public/assets/{markdown-CwQB_x0O.js.map → markdown-BWnjyvnX.js.map} +1 -1
- package/public/assets/{startup-state-settings-card-CsVbusKE.js → startup-state-settings-card-C4E2Itop.js} +2 -2
- package/public/assets/{startup-state-settings-card-CsVbusKE.js.map → startup-state-settings-card-C4E2Itop.js.map} +1 -1
- package/public/assets/{terminal-square-C2cUF_bs.js → terminal-square-C3wutdV0.js} +2 -2
- package/public/assets/{terminal-square-C2cUF_bs.js.map → terminal-square-C3wutdV0.js.map} +1 -1
- package/public/assets/{tool-call-Cv5pDAZT.js → tool-call-DNsAQ_m4.js} +4 -4
- package/public/assets/{tool-call-Cv5pDAZT.js.map → tool-call-DNsAQ_m4.js.map} +1 -1
- package/public/assets/unified-picker-tB7y17Qz.js.map +1 -1
- package/public/assets/{wrap-text-GxclkjbF.js → wrap-text-CkBxzt04.js} +2 -2
- package/public/assets/{wrap-text-GxclkjbF.js.map → wrap-text-CkBxzt04.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-CchaYx5f.js.map +0 -1
- package/public/assets/StatusTab-D4793GJu.js +0 -2
- package/public/assets/StatusTab-D4793GJu.js.map +0 -1
- package/public/assets/main-CYyZsCqM.js +0 -38
- package/public/assets/main-CYyZsCqM.js.map +0 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
## Prerequisites
|
|
22
22
|
|
|
23
|
-
- **OpenCode V2**: Install a compatible `opencode2` CLI. CodeNomad uses the CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to OpenCode's externally owned global daemon, then validates its authenticated loopback
|
|
23
|
+
- **OpenCode V2**: Install a compatible `opencode2` CLI. CodeNomad uses the CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to OpenCode's externally owned global daemon, then validates its authenticated loopback `/api/status` response rather than enforcing an exact CLI version string.
|
|
24
24
|
- **OpenCode data**: The global daemon owns its platform-default storage, database, and service registration. Configured startup environment applies only when CodeNomad starts a missing daemon; an existing daemon is unchanged.
|
|
25
25
|
- **Windows to WSL**: A configured WSL UNC binary uses Linux `service status`, `service start`, and `service get password`; Windows must have WSL localhost forwarding enabled to reach its loopback service.
|
|
26
26
|
- Node.js 18+ and npm (for running or building from source).
|
|
@@ -232,7 +232,7 @@ Explicit **Stop Workspace** evicts that location and its resources from the glob
|
|
|
232
232
|
|
|
233
233
|
CodeNomad holds one shared OpenCode V2 `client.event.subscribe()` stream. It routes native location-scoped events to logical workspaces and multiplexes them with CodeNomad events over `GET /api/events` for browser `EventSource` clients.
|
|
234
234
|
|
|
235
|
-
The stream is volatile and has no replay guarantee. After reconnecting, clients must refetch authoritative sessions and pending permission
|
|
235
|
+
The stream is volatile and has no replay guarantee. After reconnecting, clients must refetch authoritative sessions and pending permission and Form requests; file and config consumers must also refetch after `filesystem.changed` and `config.updated` invalidations.
|
|
236
236
|
|
|
237
237
|
### Provider Plan Usage
|
|
238
238
|
|
package/dist/api-types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-types.js","sourceRoot":"","sources":["../src/api-types.ts"],"names":[],"mappings":"AAqQA,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AA8E/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,sBAA+B,CAAA","sourcesContent":["import type {\n AgentModelSelection,\n AgentModelSelections,\n ModelPreference,\n OpenCodeBinary,\n Preferences,\n RecentFolder,\n} from \"./config/schema\"\nimport type { OpenCodeEvent } from \"@opencode-ai/client\"\n\n/**\n * Canonical HTTP/SSE contract for the CLI server.\n * These types are consumed by both the CLI implementation and any UI clients.\n */\n\nexport type WorkspaceStatus = \"starting\" | \"ready\" | \"stopped\" | \"error\"\n\nexport interface WorkspaceDescriptor {\n id: string\n /** Correlates creation events with the client request that initiated them. */\n requestId?: string\n /** Absolute path on the server host. */\n path: string\n name?: string\n status: WorkspaceStatus\n /** PID/port are populated when the workspace is running. */\n pid?: number\n port?: number\n /** Canonical proxy path the CLI exposes for this instance. */\n proxyPath: string\n /** Identifier of the binary resolved from config. */\n binaryId: string\n binaryLabel: string\n binaryVersion?: string\n createdAt: string\n updatedAt: string\n /** Present when `status` is \"error\". */\n error?: string\n}\n\nexport interface WorkspaceCreateRequest {\n path: string\n name?: string\n requestId?: string\n}\n\nexport interface WorkspaceCloneRequest {\n repositoryUrl: string\n destinationPath: string\n cleanup?: boolean\n}\n\nexport interface WorkspaceCloneResponse {\n path: string\n}\n\nexport type WorkspaceCreateResponse = WorkspaceDescriptor & {\n /** True when this request did not own creation of the returned workspace. */\n reused?: true\n}\nexport type WorkspaceListResponse = WorkspaceDescriptor[]\nexport type WorkspaceDetailResponse = WorkspaceDescriptor\n\nexport interface WorkspaceDeleteResponse {\n id: string\n status: WorkspaceStatus\n}\n\nexport interface ProviderUsageWindow {\n usedPercent: number | null\n remainingPercent: number | null\n windowSeconds: number | null\n resetAt: number | null\n valueLabel?: string\n}\n\nexport interface ProviderUsageResponse {\n requestedProviderId: string\n providerId: string | null\n providerName: string\n modelId?: string\n supported: boolean\n configured: boolean\n ok: boolean\n windows: Record<string, ProviderUsageWindow>\n fetchedAt: number\n}\n\nexport type WorktreeKind = \"root\" | \"worktree\"\n\nexport interface WorktreeDescriptor {\n /** Stable identifier used by CodeNomad + clients (\"root\" for the selected workspace folder). */\n slug: string\n /** Absolute directory path on the server host. */\n directory: string\n kind: WorktreeKind\n /** Optional VCS branch name when available. */\n branch?: string\n}\n\nexport interface WorktreeListResponse {\n worktrees: WorktreeDescriptor[]\n /** True when the workspace folder resolves to a Git repository. */\n isGitRepo?: boolean\n}\n\nexport interface WorktreeCreateRequest {\n slug: string\n /** Optional branch name (defaults to slug). */\n branch?: string\n}\n\nexport type GitChangeKind = \"added\" | \"modified\" | \"deleted\" | \"renamed\" | \"copied\" | \"untracked\" | \"unmerged\"\n\nexport interface WorktreeGitStatusEntry {\n path: string\n originalPath?: string | null\n stagedStatus: GitChangeKind | null\n stagedAdditions: number\n stagedDeletions: number\n unstagedStatus: GitChangeKind | null\n unstagedAdditions: number\n unstagedDeletions: number\n}\n\nexport type WorktreeGitStatusResponse = WorktreeGitStatusEntry[]\n\nexport type WorktreeGitDiffScope = \"staged\" | \"unstaged\"\n\nexport interface WorktreeGitPathsRequest {\n paths: string[]\n}\n\nexport interface WorktreeGitMutationResponse {\n ok: true\n}\n\nexport interface WorktreeGitCommitRequest {\n message: string\n}\n\nexport interface WorktreeGitCommitResponse {\n ok: true\n commitSha?: string\n}\n\nexport interface WorktreeGitDiffResponse {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n before: string\n after: string\n isBinary?: boolean\n}\n\nexport interface WorktreeGitDiffRequest {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface WorkspaceLogEntry {\n workspaceId: string\n timestamp: string\n level: LogLevel\n message: string\n}\n\nexport interface FileSystemEntry {\n name: string\n /**\n * Path identifier for the entry. Relative to the server root in restricted\n * single-root listings (\".\" represents the root itself); absolute in\n * unrestricted, drives, and multi-root top-level listings.\n */\n path: string\n /** Absolute path when available (unrestricted and multi-root listings). */\n absolutePath?: string\n type: \"file\" | \"directory\"\n size?: number\n /** ISO timestamp of last modification when available. */\n modifiedAt?: string\n}\n\nexport type FileSystemScope = \"restricted\" | \"unrestricted\"\nexport type FileSystemPathKind = \"relative\" | \"absolute\" | \"drives\"\n\nexport interface FileSystemListingMetadata {\n scope: FileSystemScope\n /**\n * Canonical identifier of the current view:\n * - \".\" for restricted single-root listings\n * - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root\n * - absolute path otherwise\n */\n currentPath: string\n /** Optional parent path if navigation upward is allowed. */\n parentPath?: string\n /** Absolute path representing the root or origin point for this listing. */\n rootPath: string\n /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */\n homePath: string\n /** Human-friendly label for the current path. */\n displayPath: string\n /** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */\n pathKind: FileSystemPathKind\n}\n\nexport interface FileSystemListResponse {\n entries: FileSystemEntry[]\n metadata: FileSystemListingMetadata\n}\n\nexport interface FileSystemCreateFolderRequest {\n /**\n * Path identifier for the currently browsed directory.\n * Matches the `path` parameter used for `/api/filesystem`.\n */\n parentPath?: string\n /** Single folder name (no separators). */\n name: string\n}\n\nexport interface FileSystemCreateFolderResponse {\n /**\n * Path identifier that can be passed back to `/api/filesystem` to browse the new folder.\n * Relative for restricted listings and absolute for unrestricted listings.\n */\n path: string\n /** Absolute folder path on the server host. */\n absolutePath: string\n}\n\nexport interface FileSystemFileContentResponse {\n path: string\n contents: string\n encoding: \"utf-8\" | \"base64\"\n}\n\nexport interface ConfigFileDescriptor {\n id: string\n label: string\n path: string\n language: string\n}\n\nexport type ConfigFileListResponse = ConfigFileDescriptor[]\n\nexport interface ConfigFileContentResponse {\n id: string\n path: string\n contents: string\n exists: boolean\n}\n\nexport interface ConfigFileContentRequest {\n contents: string\n}\n\nexport const WINDOWS_DRIVES_ROOT = \"__drives__\"\n\nexport interface WorkspaceFileResponse {\n workspaceId: string\n relativePath: string\n /** UTF-8 file contents; binary files should be base64 encoded by the caller. */\n contents: string\n encoding?: \"utf-8\" | \"base64\"\n}\n\nexport type WorkspaceFileSearchResponse = FileSystemEntry[]\n\nexport interface InstanceData {\n messageHistory: string[]\n agentModelSelections: AgentModelSelection\n}\n\nexport type InstanceStreamStatus = \"connecting\" | \"connected\" | \"error\" | \"disconnected\"\n\nexport type InstanceStreamEvent = OpenCodeEvent\n\nexport type SideCarKind = \"port\"\n\nexport type SideCarPrefixMode = \"strip\" | \"preserve\"\n\nexport type SideCarStatus = \"running\" | \"stopped\"\n\nexport interface SideCar {\n id: string\n kind: SideCarKind\n name: string\n port: number\n insecure: boolean\n prefixMode: SideCarPrefixMode\n status: SideCarStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface PreviewSession {\n token: string\n sessionId: string\n targetUrl: string\n proxyUrl: string\n createdAt: string\n}\n\nexport interface BinaryRecord {\n id: string\n path: string\n label: string\n version?: string\n\n /** Indicates that this binary will be picked when workspaces omit an explicit choice. */\n isDefault: boolean\n lastValidatedAt?: string\n validationError?: string\n}\n\nexport type SettingsOwner = string\nexport type SettingsBucket = Record<string, unknown>\nexport type SettingsDoc = Record<string, unknown>\n\nexport interface BinaryListResponse {\n binaries: BinaryRecord[]\n}\n\nexport interface BinaryCreateRequest {\n path: string\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryUpdateRequest {\n label?: string\n makeDefault?: boolean\n}\n\nexport const OPENCODE_V2_REQUIRED_ERROR_CODE = \"opencode_v2_required\" as const\n\nexport interface BinaryValidationResult {\n valid: boolean\n version?: string\n error?: string\n errorCode?: typeof OPENCODE_V2_REQUIRED_ERROR_CODE\n}\n\nexport interface OpenCodeUpdateStatus {\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean | null\n canUpgrade: boolean\n checkError?: \"update_check_failed\"\n}\n\nexport interface OpenCodeUpdateResponse {\n success: boolean\n version: string\n}\n\nexport interface SpeechSegment {\n startMs: number\n endMs: number\n text: string\n}\n\nexport interface SpeechCapabilitiesResponse {\n available: boolean\n configured: boolean\n provider: string\n supportsStt: boolean\n supportsTts: boolean\n supportsStreamingTts: boolean\n baseUrl?: string\n sttModel: string\n ttsModel: string\n ttsVoice: string\n ttsFormats: string[]\n streamingTtsFormats: string[]\n separateProviders?: boolean\n sttConfigured?: boolean\n ttsConfigured?: boolean\n sttBaseUrl?: string\n ttsBaseUrl?: string\n}\n\nexport interface SpeechTranscriptionResponse {\n text: string\n language?: string\n durationMs?: number\n segments?: SpeechSegment[]\n}\n\nexport interface SpeechSynthesisResponse {\n audioBase64: string\n mimeType: string\n}\n\nexport interface YoloStateResponse {\n enabled: boolean\n}\n\nexport interface RemoteServerProfile {\n id: string\n name: string\n baseUrl: string\n skipTlsVerify: boolean\n createdAt: string\n updatedAt: string\n lastConnectedAt?: string\n}\n\nexport interface RemoteServerProbeRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteServerProbeResponse {\n ok: boolean\n reachable: boolean\n normalizedUrl: string\n skipTlsVerify: boolean\n requiresAuth: boolean\n authenticated: boolean\n error?: string\n errorCode?: string\n}\n\nexport interface RemoteProxySessionCreateRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteProxySessionCreateResponse {\n sessionId: string\n windowUrl: string\n}\n\nexport type WorkspaceEventType =\n | \"workspace.created\"\n | \"workspace.started\"\n | \"workspace.error\"\n | \"workspace.stopped\"\n | \"workspace.log\"\n | \"sidecar.updated\"\n | \"sidecar.removed\"\n | \"storage.configChanged\"\n | \"storage.stateChanged\"\n | \"instance.dataChanged\"\n | \"instance.event\"\n | \"instance.eventStatus\"\n | \"yolo.stateChanged\"\n | \"yolo.autoAccepted\"\n\nexport type WorkspaceEventPayload =\n | { type: \"workspace.created\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.started\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.error\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.stopped\"; workspaceId: string; reason?: \"deleted\" | \"stopped\" }\n | { type: \"workspace.log\"; entry: WorkspaceLogEntry }\n | { type: \"sidecar.updated\"; sidecar: SideCar }\n | { type: \"sidecar.removed\"; sidecarId: string }\n | { type: \"storage.configChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"storage.stateChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"instance.dataChanged\"; instanceId: string; data: InstanceData }\n | { type: \"instance.event\"; instanceId: string; event: InstanceStreamEvent }\n | { type: \"instance.eventStatus\"; instanceId: string; status: InstanceStreamStatus; generation: number; reason?: string }\n | { type: \"yolo.stateChanged\"; instanceId: string; sessionId: string; enabled: boolean }\n | { type: \"yolo.autoAccepted\"; instanceId: string; sessionId: string; permissionId: string }\n\nexport interface NetworkAddress {\n ip: string\n family: \"ipv4\" | \"ipv6\"\n scope: \"external\" | \"internal\" | \"loopback\"\n /** Remote URL using the server's remote protocol/port for this IP. */\n remoteUrl: string\n}\n\nexport interface LatestReleaseInfo {\n version: string\n tag: string\n url: string\n channel: \"stable\" | \"dev\"\n publishedAt?: string\n notes?: string\n}\n\nexport interface UiMeta {\n version?: string\n source: \"bundled\" | \"downloaded\" | \"previous\" | \"override\" | \"dev-proxy\" | \"missing\"\n}\n\nexport interface SupportMeta {\n supported: boolean\n message?: string\n minServerVersion?: string\n latestServerVersion?: string\n latestServerUrl?: string\n}\n\nexport interface ServerMeta {\n /** URL desktop apps should use to connect (prefers loopback HTTP when enabled). */\n localUrl: string\n /** URL remote clients should use (prefers HTTPS when enabled). */\n remoteUrl?: string\n /** SSE endpoint advertised to clients (`/api/events` by default). */\n eventsUrl: string\n /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */\n host: string\n /** Listening mode derived from host binding. */\n listeningMode: \"local\" | \"all\"\n /** Actual local port in use after binding. */\n localPort: number\n /** Actual remote port in use after binding (when remoteUrl is set). */\n remotePort?: number\n /** Display label for the host (e.g., hostname or friendly name). */\n hostLabel: string\n /** Absolute path of the filesystem root exposed to clients. */\n workspaceRoot: string\n /** Reachable addresses for this server, external first. */\n addresses: NetworkAddress[]\n serverVersion?: string\n ui?: UiMeta\n support?: SupportMeta\n /** Optional update info (dev channel only). */\n update?: LatestReleaseInfo | null\n}\n\nexport type {\n Preferences,\n ModelPreference,\n AgentModelSelections,\n RecentFolder,\n OpenCodeBinary,\n}\nexport type { PruneRequest, PruneResult } from \"./opencode/session-pruning/contract\"\n"]}
|
|
1
|
+
{"version":3,"file":"api-types.js","sourceRoot":"","sources":["../src/api-types.ts"],"names":[],"mappings":"AAqQA,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AA8E/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,sBAA+B,CAAA","sourcesContent":["import type {\n AgentModelSelection,\n AgentModelSelections,\n ModelPreference,\n OpenCodeBinary,\n Preferences,\n RecentFolder,\n} from \"./config/schema\"\nimport type { OpenCodeEvent } from \"@opencode/client\"\n\n/**\n * Canonical HTTP/SSE contract for the CLI server.\n * These types are consumed by both the CLI implementation and any UI clients.\n */\n\nexport type WorkspaceStatus = \"starting\" | \"ready\" | \"stopped\" | \"error\"\n\nexport interface WorkspaceDescriptor {\n id: string\n /** Correlates creation events with the client request that initiated them. */\n requestId?: string\n /** Absolute path on the server host. */\n path: string\n name?: string\n status: WorkspaceStatus\n /** PID/port are populated when the workspace is running. */\n pid?: number\n port?: number\n /** Canonical proxy path the CLI exposes for this instance. */\n proxyPath: string\n /** Identifier of the binary resolved from config. */\n binaryId: string\n binaryLabel: string\n binaryVersion?: string\n createdAt: string\n updatedAt: string\n /** Present when `status` is \"error\". */\n error?: string\n}\n\nexport interface WorkspaceCreateRequest {\n path: string\n name?: string\n requestId?: string\n}\n\nexport interface WorkspaceCloneRequest {\n repositoryUrl: string\n destinationPath: string\n cleanup?: boolean\n}\n\nexport interface WorkspaceCloneResponse {\n path: string\n}\n\nexport type WorkspaceCreateResponse = WorkspaceDescriptor & {\n /** True when this request did not own creation of the returned workspace. */\n reused?: true\n}\nexport type WorkspaceListResponse = WorkspaceDescriptor[]\nexport type WorkspaceDetailResponse = WorkspaceDescriptor\n\nexport interface WorkspaceDeleteResponse {\n id: string\n status: WorkspaceStatus\n}\n\nexport interface ProviderUsageWindow {\n usedPercent: number | null\n remainingPercent: number | null\n windowSeconds: number | null\n resetAt: number | null\n valueLabel?: string\n}\n\nexport interface ProviderUsageResponse {\n requestedProviderId: string\n providerId: string | null\n providerName: string\n modelId?: string\n supported: boolean\n configured: boolean\n ok: boolean\n windows: Record<string, ProviderUsageWindow>\n fetchedAt: number\n}\n\nexport type WorktreeKind = \"root\" | \"worktree\"\n\nexport interface WorktreeDescriptor {\n /** Stable identifier used by CodeNomad + clients (\"root\" for the selected workspace folder). */\n slug: string\n /** Absolute directory path on the server host. */\n directory: string\n kind: WorktreeKind\n /** Optional VCS branch name when available. */\n branch?: string\n}\n\nexport interface WorktreeListResponse {\n worktrees: WorktreeDescriptor[]\n /** True when the workspace folder resolves to a Git repository. */\n isGitRepo?: boolean\n}\n\nexport interface WorktreeCreateRequest {\n slug: string\n /** Optional branch name (defaults to slug). */\n branch?: string\n}\n\nexport type GitChangeKind = \"added\" | \"modified\" | \"deleted\" | \"renamed\" | \"copied\" | \"untracked\" | \"unmerged\"\n\nexport interface WorktreeGitStatusEntry {\n path: string\n originalPath?: string | null\n stagedStatus: GitChangeKind | null\n stagedAdditions: number\n stagedDeletions: number\n unstagedStatus: GitChangeKind | null\n unstagedAdditions: number\n unstagedDeletions: number\n}\n\nexport type WorktreeGitStatusResponse = WorktreeGitStatusEntry[]\n\nexport type WorktreeGitDiffScope = \"staged\" | \"unstaged\"\n\nexport interface WorktreeGitPathsRequest {\n paths: string[]\n}\n\nexport interface WorktreeGitMutationResponse {\n ok: true\n}\n\nexport interface WorktreeGitCommitRequest {\n message: string\n}\n\nexport interface WorktreeGitCommitResponse {\n ok: true\n commitSha?: string\n}\n\nexport interface WorktreeGitDiffResponse {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n before: string\n after: string\n isBinary?: boolean\n}\n\nexport interface WorktreeGitDiffRequest {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface WorkspaceLogEntry {\n workspaceId: string\n timestamp: string\n level: LogLevel\n message: string\n}\n\nexport interface FileSystemEntry {\n name: string\n /**\n * Path identifier for the entry. Relative to the server root in restricted\n * single-root listings (\".\" represents the root itself); absolute in\n * unrestricted, drives, and multi-root top-level listings.\n */\n path: string\n /** Absolute path when available (unrestricted and multi-root listings). */\n absolutePath?: string\n type: \"file\" | \"directory\"\n size?: number\n /** ISO timestamp of last modification when available. */\n modifiedAt?: string\n}\n\nexport type FileSystemScope = \"restricted\" | \"unrestricted\"\nexport type FileSystemPathKind = \"relative\" | \"absolute\" | \"drives\"\n\nexport interface FileSystemListingMetadata {\n scope: FileSystemScope\n /**\n * Canonical identifier of the current view:\n * - \".\" for restricted single-root listings\n * - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root\n * - absolute path otherwise\n */\n currentPath: string\n /** Optional parent path if navigation upward is allowed. */\n parentPath?: string\n /** Absolute path representing the root or origin point for this listing. */\n rootPath: string\n /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */\n homePath: string\n /** Human-friendly label for the current path. */\n displayPath: string\n /** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */\n pathKind: FileSystemPathKind\n}\n\nexport interface FileSystemListResponse {\n entries: FileSystemEntry[]\n metadata: FileSystemListingMetadata\n}\n\nexport interface FileSystemCreateFolderRequest {\n /**\n * Path identifier for the currently browsed directory.\n * Matches the `path` parameter used for `/api/filesystem`.\n */\n parentPath?: string\n /** Single folder name (no separators). */\n name: string\n}\n\nexport interface FileSystemCreateFolderResponse {\n /**\n * Path identifier that can be passed back to `/api/filesystem` to browse the new folder.\n * Relative for restricted listings and absolute for unrestricted listings.\n */\n path: string\n /** Absolute folder path on the server host. */\n absolutePath: string\n}\n\nexport interface FileSystemFileContentResponse {\n path: string\n contents: string\n encoding: \"utf-8\" | \"base64\"\n}\n\nexport interface ConfigFileDescriptor {\n id: string\n label: string\n path: string\n language: string\n}\n\nexport type ConfigFileListResponse = ConfigFileDescriptor[]\n\nexport interface ConfigFileContentResponse {\n id: string\n path: string\n contents: string\n exists: boolean\n}\n\nexport interface ConfigFileContentRequest {\n contents: string\n}\n\nexport const WINDOWS_DRIVES_ROOT = \"__drives__\"\n\nexport interface WorkspaceFileResponse {\n workspaceId: string\n relativePath: string\n /** UTF-8 file contents; binary files should be base64 encoded by the caller. */\n contents: string\n encoding?: \"utf-8\" | \"base64\"\n}\n\nexport type WorkspaceFileSearchResponse = FileSystemEntry[]\n\nexport interface InstanceData {\n messageHistory: string[]\n agentModelSelections: AgentModelSelection\n}\n\nexport type InstanceStreamStatus = \"connecting\" | \"connected\" | \"error\" | \"disconnected\"\n\nexport type InstanceStreamEvent = OpenCodeEvent\n\nexport type SideCarKind = \"port\"\n\nexport type SideCarPrefixMode = \"strip\" | \"preserve\"\n\nexport type SideCarStatus = \"running\" | \"stopped\"\n\nexport interface SideCar {\n id: string\n kind: SideCarKind\n name: string\n port: number\n insecure: boolean\n prefixMode: SideCarPrefixMode\n status: SideCarStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface PreviewSession {\n token: string\n sessionId: string\n targetUrl: string\n proxyUrl: string\n createdAt: string\n}\n\nexport interface BinaryRecord {\n id: string\n path: string\n label: string\n version?: string\n\n /** Indicates that this binary will be picked when workspaces omit an explicit choice. */\n isDefault: boolean\n lastValidatedAt?: string\n validationError?: string\n}\n\nexport type SettingsOwner = string\nexport type SettingsBucket = Record<string, unknown>\nexport type SettingsDoc = Record<string, unknown>\n\nexport interface BinaryListResponse {\n binaries: BinaryRecord[]\n}\n\nexport interface BinaryCreateRequest {\n path: string\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryUpdateRequest {\n label?: string\n makeDefault?: boolean\n}\n\nexport const OPENCODE_V2_REQUIRED_ERROR_CODE = \"opencode_v2_required\" as const\n\nexport interface BinaryValidationResult {\n valid: boolean\n version?: string\n error?: string\n errorCode?: typeof OPENCODE_V2_REQUIRED_ERROR_CODE\n}\n\nexport interface OpenCodeUpdateStatus {\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean | null\n canUpgrade: boolean\n checkError?: \"update_check_failed\"\n}\n\nexport interface OpenCodeUpdateResponse {\n success: boolean\n version: string\n}\n\nexport interface SpeechSegment {\n startMs: number\n endMs: number\n text: string\n}\n\nexport interface SpeechCapabilitiesResponse {\n available: boolean\n configured: boolean\n provider: string\n supportsStt: boolean\n supportsTts: boolean\n supportsStreamingTts: boolean\n baseUrl?: string\n sttModel: string\n ttsModel: string\n ttsVoice: string\n ttsFormats: string[]\n streamingTtsFormats: string[]\n separateProviders?: boolean\n sttConfigured?: boolean\n ttsConfigured?: boolean\n sttBaseUrl?: string\n ttsBaseUrl?: string\n}\n\nexport interface SpeechTranscriptionResponse {\n text: string\n language?: string\n durationMs?: number\n segments?: SpeechSegment[]\n}\n\nexport interface SpeechSynthesisResponse {\n audioBase64: string\n mimeType: string\n}\n\nexport interface YoloStateResponse {\n enabled: boolean\n}\n\nexport interface RemoteServerProfile {\n id: string\n name: string\n baseUrl: string\n skipTlsVerify: boolean\n createdAt: string\n updatedAt: string\n lastConnectedAt?: string\n}\n\nexport interface RemoteServerProbeRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteServerProbeResponse {\n ok: boolean\n reachable: boolean\n normalizedUrl: string\n skipTlsVerify: boolean\n requiresAuth: boolean\n authenticated: boolean\n error?: string\n errorCode?: string\n}\n\nexport interface RemoteProxySessionCreateRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteProxySessionCreateResponse {\n sessionId: string\n windowUrl: string\n}\n\nexport type WorkspaceEventType =\n | \"workspace.created\"\n | \"workspace.started\"\n | \"workspace.error\"\n | \"workspace.stopped\"\n | \"workspace.log\"\n | \"sidecar.updated\"\n | \"sidecar.removed\"\n | \"storage.configChanged\"\n | \"storage.stateChanged\"\n | \"instance.dataChanged\"\n | \"instance.event\"\n | \"instance.eventStatus\"\n | \"yolo.stateChanged\"\n | \"yolo.autoAccepted\"\n\nexport type WorkspaceEventPayload =\n | { type: \"workspace.created\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.started\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.error\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.stopped\"; workspaceId: string; reason?: \"deleted\" | \"stopped\" }\n | { type: \"workspace.log\"; entry: WorkspaceLogEntry }\n | { type: \"sidecar.updated\"; sidecar: SideCar }\n | { type: \"sidecar.removed\"; sidecarId: string }\n | { type: \"storage.configChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"storage.stateChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"instance.dataChanged\"; instanceId: string; data: InstanceData }\n | { type: \"instance.event\"; instanceId: string; event: InstanceStreamEvent }\n | { type: \"instance.eventStatus\"; instanceId: string; status: InstanceStreamStatus; generation: number; reason?: string }\n | { type: \"yolo.stateChanged\"; instanceId: string; sessionId: string; enabled: boolean }\n | { type: \"yolo.autoAccepted\"; instanceId: string; sessionId: string; permissionId: string }\n\nexport interface NetworkAddress {\n ip: string\n family: \"ipv4\" | \"ipv6\"\n scope: \"external\" | \"internal\" | \"loopback\"\n /** Remote URL using the server's remote protocol/port for this IP. */\n remoteUrl: string\n}\n\nexport interface LatestReleaseInfo {\n version: string\n tag: string\n url: string\n channel: \"stable\" | \"dev\"\n publishedAt?: string\n notes?: string\n}\n\nexport interface UiMeta {\n version?: string\n source: \"bundled\" | \"downloaded\" | \"previous\" | \"override\" | \"dev-proxy\" | \"missing\"\n}\n\nexport interface SupportMeta {\n supported: boolean\n message?: string\n minServerVersion?: string\n latestServerVersion?: string\n latestServerUrl?: string\n}\n\nexport interface ServerMeta {\n /** URL desktop apps should use to connect (prefers loopback HTTP when enabled). */\n localUrl: string\n /** URL remote clients should use (prefers HTTPS when enabled). */\n remoteUrl?: string\n /** SSE endpoint advertised to clients (`/api/events` by default). */\n eventsUrl: string\n /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */\n host: string\n /** Listening mode derived from host binding. */\n listeningMode: \"local\" | \"all\"\n /** Actual local port in use after binding. */\n localPort: number\n /** Actual remote port in use after binding (when remoteUrl is set). */\n remotePort?: number\n /** Display label for the host (e.g., hostname or friendly name). */\n hostLabel: string\n /** Absolute path of the filesystem root exposed to clients. */\n workspaceRoot: string\n /** Reachable addresses for this server, external first. */\n addresses: NetworkAddress[]\n serverVersion?: string\n ui?: UiMeta\n support?: SupportMeta\n /** Optional update info (dev channel only). */\n update?: LatestReleaseInfo | null\n}\n\nexport type {\n Preferences,\n ModelPreference,\n AgentModelSelections,\n RecentFolder,\n OpenCodeBinary,\n}\nexport type { PruneRequest, PruneResult } from \"./opencode/session-pruning/contract\"\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Minimal structural excerpts from the published earlier V2 OpenAPI document.
|
|
2
|
+
// For test fixtures only; real-runtime tests also negotiate full native schemas.
|
|
3
|
+
export const legacyContractFixture = {
|
|
4
|
+
paths: {
|
|
5
|
+
"/api/session/{sessionID}/rename": { post: {} },
|
|
6
|
+
"/api/form/request": { get: {} },
|
|
7
|
+
"/api/session/{sessionID}/form/{formID}/cancel": { post: {} },
|
|
8
|
+
"/api/session/{sessionID}/wait": { post: {} },
|
|
9
|
+
"/api/session/{sessionID}/fork": { post: { requestBody: { content: { "application/json": { schema: { properties: { boundary: {} } } } } } } },
|
|
10
|
+
"/api/session/{sessionID}/command": { post: { requestBody: { content: { "application/json": { schema: { properties: { command: {} } } } } } } },
|
|
11
|
+
"/api/session/{sessionID}/permission/{requestID}/reply": { post: { requestBody: { content: { "application/json": { schema: { properties: { reply: {} } } } } } } },
|
|
12
|
+
},
|
|
13
|
+
components: { schemas: { "Session.Inbox.User": { properties: { timeCreated: { type: "number" } } } } },
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=contract-fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-fixture.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/contract-fixture.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,iFAAiF;AACjF,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,KAAK,EAAE;QACL,iCAAiC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QAC/C,mBAAmB,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;QAChC,+CAA+C,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QAC7D,+BAA+B,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QAC7C,+BAA+B,EAAE,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QAC7I,kCAAkC,EAAE,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QAC/I,uDAAuD,EAAE,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;KACnK;IACD,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,oBAAoB,EAAE,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,EAAE;CACvG,CAAA","sourcesContent":["// Minimal structural excerpts from the published earlier V2 OpenAPI document.\n// For test fixtures only; real-runtime tests also negotiate full native schemas.\nexport const legacyContractFixture = {\n paths: {\n \"/api/session/{sessionID}/rename\": { post: {} },\n \"/api/form/request\": { get: {} },\n \"/api/session/{sessionID}/form/{formID}/cancel\": { post: {} },\n \"/api/session/{sessionID}/wait\": { post: {} },\n \"/api/session/{sessionID}/fork\": { post: { requestBody: { content: { \"application/json\": { schema: { properties: { boundary: {} } } } } } } },\n \"/api/session/{sessionID}/command\": { post: { requestBody: { content: { \"application/json\": { schema: { properties: { command: {} } } } } } } },\n \"/api/session/{sessionID}/permission/{requestID}/reply\": { post: { requestBody: { content: { \"application/json\": { schema: { properties: { reply: {} } } } } } } },\n },\n components: { schemas: { \"Session.Inbox.User\": { properties: { timeCreated: { type: \"number\" } } } } },\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function normalizeRuntimeEvent(event) {
|
|
2
|
+
// Native metadata/durable identity is retained. Catalog/content changes stay
|
|
3
|
+
// invalidation intents; no synthetic upstream durable records are invented.
|
|
4
|
+
if (event.type === "session.permissions.updated") {
|
|
5
|
+
return { ...event, type: "session.permissions" };
|
|
6
|
+
}
|
|
7
|
+
return event;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/events.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CAAC,KAAoB;IACxD,6EAA6E;IAC7E,4EAA4E;IAC5E,IAAK,KAAK,CAAC,IAAe,KAAK,6BAA6B,EAAE,CAAC;QAC7D,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,qBAAqB,EAAmB,CAAA;IACnE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import type { OpenCodeEvent } from \"@opencode/client\"\n\nexport function normalizeRuntimeEvent(event: OpenCodeEvent): OpenCodeEvent {\n // Native metadata/durable identity is retained. Catalog/content changes stay\n // invalidation intents; no synthetic upstream durable records are invented.\n if ((event.type as string) === \"session.permissions.updated\") {\n return { ...event, type: \"session.permissions\" } as OpenCodeEvent\n }\n return event\n}\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readLocationRef } from "../session-pruning/location";
|
|
2
|
+
export { readLocationRef, sameLocation } from "../session-pruning/location";
|
|
3
|
+
// An explicit CodeNomad context channel survives the generated modern client's
|
|
4
|
+
// field selection. Never forward this header to OpenCode unchanged. The guarded
|
|
5
|
+
// proxy must authorize its complete location before transport serialization.
|
|
6
|
+
export const LOCATION_CONTEXT_HEADER = "x-codenomad-location";
|
|
7
|
+
export function locationRequestOptions(location, options) {
|
|
8
|
+
const resolved = readLocationRef(location);
|
|
9
|
+
return resolved.workspaceID === undefined && !options?.includeDirectory ? undefined : {
|
|
10
|
+
headers: { [LOCATION_CONTEXT_HEADER]: encodeURIComponent(JSON.stringify(resolved)) },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function readLocationContext(value, profile) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeof value !== "string" || value.length > 16_384)
|
|
17
|
+
throw new Error("Invalid location context");
|
|
18
|
+
const input = JSON.parse(decodeURIComponent(value));
|
|
19
|
+
if (!input || typeof input !== "object" || Object.keys(input).some(key => key !== "directory" && key !== "workspaceID")) {
|
|
20
|
+
throw new Error("Invalid location context");
|
|
21
|
+
}
|
|
22
|
+
const location = readLocationRef(input);
|
|
23
|
+
if (location.workspaceID !== undefined && profile !== "legacy")
|
|
24
|
+
throw new Error("Unsupported workspace selector");
|
|
25
|
+
return location;
|
|
26
|
+
}
|
|
27
|
+
export async function moveSessionToLocation(client, sessionID, location) {
|
|
28
|
+
await client.session.move({ sessionID, directory: location.directory }, locationRequestOptions(location));
|
|
29
|
+
}
|
|
30
|
+
// The selected transport calls this AFTER authorization. Only the explicitly
|
|
31
|
+
// supported wire slots are adapted; this is not a general header-to-body merge.
|
|
32
|
+
export function applyLocationContext(url, method, body, headers, profile) {
|
|
33
|
+
const location = readLocationContext(headers.get(LOCATION_CONTEXT_HEADER) ?? undefined, profile);
|
|
34
|
+
headers.delete(LOCATION_CONTEXT_HEADER);
|
|
35
|
+
if (!location)
|
|
36
|
+
return body;
|
|
37
|
+
const input = body && typeof body === "object" && !Array.isArray(body) ? body : undefined;
|
|
38
|
+
const assertDirectory = (directory) => {
|
|
39
|
+
if (directory !== location.directory)
|
|
40
|
+
throw new Error("Location context does not match request");
|
|
41
|
+
};
|
|
42
|
+
if (method === "POST" && /^\/api\/session\/[^/]+\/move\/?$/.test(url.pathname)) {
|
|
43
|
+
assertDirectory(input?.directory);
|
|
44
|
+
if (input?.workspaceID !== undefined && input.workspaceID !== location.workspaceID)
|
|
45
|
+
throw new Error("Conflicting workspace selector");
|
|
46
|
+
return { ...input, workspaceID: location.workspaceID };
|
|
47
|
+
}
|
|
48
|
+
if (method === "POST" && /^\/api\/(?:experimental\/)?session(?:\/import)?\/?$/.test(url.pathname)) {
|
|
49
|
+
const destination = readLocationRef(input?.location);
|
|
50
|
+
assertDirectory(destination.directory);
|
|
51
|
+
if (destination.workspaceID !== undefined && destination.workspaceID !== location.workspaceID)
|
|
52
|
+
throw new Error("Conflicting workspace selector");
|
|
53
|
+
return { ...input, location: { ...destination, workspaceID: location.workspaceID } };
|
|
54
|
+
}
|
|
55
|
+
if (/^\/api\/session\/global\/form(?:\/[^/]+(?:\/(?:reply|cancel|state))?)?\/?$/.test(url.pathname)) {
|
|
56
|
+
assertDirectory(decodeURIComponent(headers.get("x-opencode-directory") ?? ""));
|
|
57
|
+
if (location.workspaceID !== undefined)
|
|
58
|
+
headers.set("x-opencode-workspace", location.workspaceID);
|
|
59
|
+
return body;
|
|
60
|
+
}
|
|
61
|
+
if (method === "GET" && /^\/api\/session\/?$/.test(url.pathname)) {
|
|
62
|
+
// The cursor is the complete native query, not a location selector to amend.
|
|
63
|
+
if (url.searchParams.has("cursor"))
|
|
64
|
+
throw new Error("Cursor already carries session list scope");
|
|
65
|
+
assertDirectory(url.searchParams.get("directory"));
|
|
66
|
+
if (url.searchParams.has("workspace"))
|
|
67
|
+
throw new Error("Conflicting workspace selector");
|
|
68
|
+
if (location.workspaceID !== undefined)
|
|
69
|
+
url.searchParams.set("workspace", location.workspaceID);
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
if (/^\/api\/credential\/[^/]+(?:\/activate)?\/?$/.test(url.pathname)) {
|
|
73
|
+
// Modern credential mutations are global; legacy invalidation is scoped.
|
|
74
|
+
// The generated modern client has no location query to preserve for us.
|
|
75
|
+
if (profile !== "legacy")
|
|
76
|
+
return body;
|
|
77
|
+
if (url.searchParams.has("location[directory]"))
|
|
78
|
+
assertDirectory(url.searchParams.get("location[directory]"));
|
|
79
|
+
url.searchParams.set("location[directory]", location.directory);
|
|
80
|
+
}
|
|
81
|
+
const directory = url.searchParams.get("location[directory]");
|
|
82
|
+
assertDirectory(directory);
|
|
83
|
+
if (url.searchParams.has("location[workspace]"))
|
|
84
|
+
throw new Error("Conflicting workspace selector");
|
|
85
|
+
if (location.workspaceID !== undefined)
|
|
86
|
+
url.searchParams.set("location[workspace]", location.workspaceID);
|
|
87
|
+
return body;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"location.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/location.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LocationRef, OpenCodeClient } from \"@opencode/client\"\nimport type { ContractProfile } from \"./runtime\"\nimport { readLocationRef } from \"../session-pruning/location\"\nexport { readLocationRef, sameLocation } from \"../session-pruning/location\"\n\n// An explicit CodeNomad context channel survives the generated modern client's\n// field selection. Never forward this header to OpenCode unchanged. The guarded\n// proxy must authorize its complete location before transport serialization.\nexport const LOCATION_CONTEXT_HEADER = \"x-codenomad-location\"\n\nexport function locationRequestOptions(location: LocationRef, options?: { includeDirectory?: boolean }): { headers: Record<string, string> } | undefined {\n const resolved = readLocationRef(location)\n return resolved.workspaceID === undefined && !options?.includeDirectory ? undefined : {\n headers: { [LOCATION_CONTEXT_HEADER]: encodeURIComponent(JSON.stringify(resolved)) },\n }\n}\n\nexport function readLocationContext(value: string | string[] | undefined, profile: ContractProfile): LocationRef | undefined {\n if (value === undefined) return undefined\n if (typeof value !== \"string\" || value.length > 16_384) throw new Error(\"Invalid location context\")\n const input: unknown = JSON.parse(decodeURIComponent(value))\n if (!input || typeof input !== \"object\" || Object.keys(input).some(key => key !== \"directory\" && key !== \"workspaceID\")) {\n throw new Error(\"Invalid location context\")\n }\n const location = readLocationRef(input)\n if (location.workspaceID !== undefined && profile !== \"legacy\") throw new Error(\"Unsupported workspace selector\")\n return location\n}\n\nexport async function moveSessionToLocation(client: OpenCodeClient, sessionID: string, location: LocationRef): Promise<void> {\n await client.session.move({ sessionID, directory: location.directory }, locationRequestOptions(location))\n}\n\n// The selected transport calls this AFTER authorization. Only the explicitly\n// supported wire slots are adapted; this is not a general header-to-body merge.\nexport function applyLocationContext(url: URL, method: string, body: unknown, headers: Headers, profile: ContractProfile): unknown {\n const location = readLocationContext(headers.get(LOCATION_CONTEXT_HEADER) ?? undefined, profile)\n headers.delete(LOCATION_CONTEXT_HEADER)\n if (!location) return body\n const input = body && typeof body === \"object\" && !Array.isArray(body) ? body as Record<string, unknown> : undefined\n const assertDirectory = (directory: unknown) => {\n if (directory !== location.directory) throw new Error(\"Location context does not match request\")\n }\n if (method === \"POST\" && /^\\/api\\/session\\/[^/]+\\/move\\/?$/.test(url.pathname)) {\n assertDirectory(input?.directory)\n if (input?.workspaceID !== undefined && input.workspaceID !== location.workspaceID) throw new Error(\"Conflicting workspace selector\")\n return { ...input, workspaceID: location.workspaceID }\n }\n if (method === \"POST\" && /^\\/api\\/(?:experimental\\/)?session(?:\\/import)?\\/?$/.test(url.pathname)) {\n const destination = readLocationRef(input?.location)\n assertDirectory(destination.directory)\n if (destination.workspaceID !== undefined && destination.workspaceID !== location.workspaceID) throw new Error(\"Conflicting workspace selector\")\n return { ...input, location: { ...destination, workspaceID: location.workspaceID } }\n }\n if (/^\\/api\\/session\\/global\\/form(?:\\/[^/]+(?:\\/(?:reply|cancel|state))?)?\\/?$/.test(url.pathname)) {\n assertDirectory(decodeURIComponent(headers.get(\"x-opencode-directory\") ?? \"\"))\n if (location.workspaceID !== undefined) headers.set(\"x-opencode-workspace\", location.workspaceID)\n return body\n }\n if (method === \"GET\" && /^\\/api\\/session\\/?$/.test(url.pathname)) {\n // The cursor is the complete native query, not a location selector to amend.\n if (url.searchParams.has(\"cursor\")) throw new Error(\"Cursor already carries session list scope\")\n assertDirectory(url.searchParams.get(\"directory\"))\n if (url.searchParams.has(\"workspace\")) throw new Error(\"Conflicting workspace selector\")\n if (location.workspaceID !== undefined) url.searchParams.set(\"workspace\", location.workspaceID)\n return body\n }\n if (/^\\/api\\/credential\\/[^/]+(?:\\/activate)?\\/?$/.test(url.pathname)) {\n // Modern credential mutations are global; legacy invalidation is scoped.\n // The generated modern client has no location query to preserve for us.\n if (profile !== \"legacy\") return body\n if (url.searchParams.has(\"location[directory]\")) assertDirectory(url.searchParams.get(\"location[directory]\"))\n url.searchParams.set(\"location[directory]\", location.directory)\n }\n const directory = url.searchParams.get(\"location[directory]\")\n assertDirectory(directory)\n if (url.searchParams.has(\"location[workspace]\")) throw new Error(\"Conflicting workspace selector\")\n if (location.workspaceID !== undefined) url.searchParams.set(\"location[workspace]\", location.workspaceID)\n return body\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { OpenCode } from "@opencode/client";
|
|
4
|
+
import { createRuntimeFetch } from "./transport";
|
|
5
|
+
import { rememberRuntime } from "./runtime";
|
|
6
|
+
import { LOCATION_CONTEXT_HEADER, applyLocationContext, locationRequestOptions, moveSessionToLocation, readLocationContext, readLocationRef, sameLocation, } from "./location";
|
|
7
|
+
test("identity context survives real modern generated move serialization", async () => {
|
|
8
|
+
const original = { directory: "/work/100% 工作", workspaceID: "workspace-1" };
|
|
9
|
+
let calls = 0;
|
|
10
|
+
const client = OpenCode.make({ baseUrl: "http://localhost:1234", fetch: async (input, init) => {
|
|
11
|
+
calls++;
|
|
12
|
+
const request = new Request(input, init);
|
|
13
|
+
const before = await request.json();
|
|
14
|
+
assert.deepEqual(before, { directory: original.directory });
|
|
15
|
+
const headers = new Headers(request.headers);
|
|
16
|
+
const after = applyLocationContext(new URL(request.url), request.method, before, headers, "legacy");
|
|
17
|
+
assert.deepEqual(after, original);
|
|
18
|
+
assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false);
|
|
19
|
+
return new Response(null, { status: 204 });
|
|
20
|
+
} });
|
|
21
|
+
await moveSessionToLocation(client, "s", original);
|
|
22
|
+
assert.equal(calls, 1);
|
|
23
|
+
});
|
|
24
|
+
test("context parser refuses malformed, duplicate, unknown and modern identity selectors", () => {
|
|
25
|
+
const encoded = locationRequestOptions({ directory: "/repo", workspaceID: "w" }).headers[LOCATION_CONTEXT_HEADER];
|
|
26
|
+
assert.deepEqual(readLocationContext(encoded, "legacy"), { directory: "/repo", workspaceID: "w" });
|
|
27
|
+
for (const profile of ["modern", "unknown"])
|
|
28
|
+
assert.throws(() => readLocationContext(encoded, profile), /Unsupported/);
|
|
29
|
+
assert.throws(() => readLocationContext([encoded, encoded], "legacy"));
|
|
30
|
+
assert.throws(() => readLocationContext("%", "legacy"));
|
|
31
|
+
assert.throws(() => readLocationContext(encodeURIComponent('{"directory":"/repo","workspace":"w"}'), "legacy"));
|
|
32
|
+
assert.throws(() => readLocationRef({ directory: "/repo", workspaceID: null }));
|
|
33
|
+
assert.equal(sameLocation({ directory: "/repo", workspaceID: "one" }, { directory: "/repo", workspaceID: "two" }), false);
|
|
34
|
+
});
|
|
35
|
+
test("global Forms and catalog requests preserve explicit identity without forwarding private headers", () => {
|
|
36
|
+
const ref = { directory: "/工作/100% ready", workspaceID: "worktree" };
|
|
37
|
+
const options = locationRequestOptions(ref);
|
|
38
|
+
const headers = new Headers({ ...options.headers, "x-opencode-directory": encodeURIComponent(ref.directory) });
|
|
39
|
+
applyLocationContext(new URL("http://localhost/api/session/global/form/f/reply"), "POST", { answer: {} }, headers, "legacy");
|
|
40
|
+
assert.equal(headers.get("x-opencode-workspace"), "worktree");
|
|
41
|
+
assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false);
|
|
42
|
+
const url = new URL("http://localhost/api/form");
|
|
43
|
+
url.searchParams.set("location[directory]", ref.directory);
|
|
44
|
+
applyLocationContext(url, "GET", undefined, new Headers(options.headers), "legacy");
|
|
45
|
+
assert.equal(url.searchParams.get("location[workspace]"), "worktree");
|
|
46
|
+
const foreign = new URL("http://localhost/api/form?location[directory]=/other");
|
|
47
|
+
assert.throws(() => applyLocationContext(foreign, "GET", undefined, new Headers(options.headers), "legacy"), /does not match/);
|
|
48
|
+
assert.throws(() => applyLocationContext(url, "GET", undefined, new Headers(options.headers), "modern"), /Unsupported/);
|
|
49
|
+
});
|
|
50
|
+
test("real generated list, RPC, eviction and credential calls use their distinct legacy wire locations", async () => {
|
|
51
|
+
const endpoint = { url: "http://localhost:1234" };
|
|
52
|
+
rememberRuntime(endpoint, { version: "2.0.3", pid: 1, discovery: "health" });
|
|
53
|
+
const location = { directory: "/repo", workspaceID: "wrk_one" };
|
|
54
|
+
const calls = [];
|
|
55
|
+
const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {
|
|
56
|
+
const request = new Request(input, init);
|
|
57
|
+
assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false);
|
|
58
|
+
const url = new URL(request.url);
|
|
59
|
+
calls.push(url);
|
|
60
|
+
if (url.pathname === "/api/session") {
|
|
61
|
+
assert.equal(url.searchParams.get("directory"), "/repo");
|
|
62
|
+
assert.equal(url.searchParams.get("workspace"), "wrk_one");
|
|
63
|
+
assert.equal(url.searchParams.has("location[directory]"), false);
|
|
64
|
+
return Response.json({ data: [], cursor: {} });
|
|
65
|
+
}
|
|
66
|
+
assert.equal(url.searchParams.get("location[directory]"), "/repo");
|
|
67
|
+
assert.equal(url.searchParams.get("location[workspace]"), "wrk_one");
|
|
68
|
+
return url.pathname.startsWith("/api/rpc/") ? Response.json({ output: true }) : new Response(null, { status: 204 });
|
|
69
|
+
}) });
|
|
70
|
+
const options = locationRequestOptions(location);
|
|
71
|
+
await client.session.list({ directory: location.directory }, options);
|
|
72
|
+
await client.rpc.call({ rpcID: "fixture", method: "check", input: {}, location: { directory: location.directory } }, options);
|
|
73
|
+
await client.debug.location.evict({ location: { directory: location.directory } }, options);
|
|
74
|
+
await client.credential.remove({ credentialID: "credential" }, options);
|
|
75
|
+
assert.equal(calls.length, 4);
|
|
76
|
+
await assert.rejects(client.session.list({ cursor: "native-cursor" }, options));
|
|
77
|
+
assert.equal(calls.length, 4, "conflicting cursor context never dispatches");
|
|
78
|
+
});
|
|
79
|
+
test("directory-only credential context is native-scoped on legacy and global on modern", async () => {
|
|
80
|
+
for (const version of ["2.0.3", "2.0.4"]) {
|
|
81
|
+
const endpoint = { url: "http://localhost:1234" };
|
|
82
|
+
rememberRuntime(endpoint, { version, pid: 1, discovery: version === "2.0.3" ? "health" : "status" });
|
|
83
|
+
const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {
|
|
84
|
+
const request = new Request(input, init);
|
|
85
|
+
assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false);
|
|
86
|
+
assert.equal(new URL(request.url).search, version === "2.0.3" ? "?location%5Bdirectory%5D=%2Frepo" : "");
|
|
87
|
+
return new Response(null, { status: 204 });
|
|
88
|
+
}) });
|
|
89
|
+
await client.credential.remove({ credentialID: "credential" }, locationRequestOptions({ directory: "/repo" }, { includeDirectory: true }));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=location.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"location.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/location.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { OpenCode } from \"@opencode/client\"\nimport { createRuntimeFetch } from \"./transport\"\nimport { rememberRuntime } from \"./runtime\"\nimport {\n LOCATION_CONTEXT_HEADER, applyLocationContext, locationRequestOptions,\n moveSessionToLocation, readLocationContext, readLocationRef, sameLocation,\n} from \"./location\"\n\ntest(\"identity context survives real modern generated move serialization\", async () => {\n const original = { directory: \"/work/100% 工作\", workspaceID: \"workspace-1\" }\n let calls = 0\n const client = OpenCode.make({ baseUrl: \"http://localhost:1234\", fetch: async (input, init) => {\n calls++\n const request = new Request(input, init)\n const before = await request.json()\n assert.deepEqual(before, { directory: original.directory })\n const headers = new Headers(request.headers)\n const after = applyLocationContext(new URL(request.url), request.method, before, headers, \"legacy\")\n assert.deepEqual(after, original)\n assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false)\n return new Response(null, { status: 204 })\n } })\n await moveSessionToLocation(client, \"s\", original)\n assert.equal(calls, 1)\n})\n\ntest(\"context parser refuses malformed, duplicate, unknown and modern identity selectors\", () => {\n const encoded = locationRequestOptions({ directory: \"/repo\", workspaceID: \"w\" })!.headers[LOCATION_CONTEXT_HEADER]\n assert.deepEqual(readLocationContext(encoded, \"legacy\"), { directory: \"/repo\", workspaceID: \"w\" })\n for (const profile of [\"modern\", \"unknown\"] as const) assert.throws(() => readLocationContext(encoded, profile), /Unsupported/)\n assert.throws(() => readLocationContext([encoded, encoded], \"legacy\"))\n assert.throws(() => readLocationContext(\"%\", \"legacy\"))\n assert.throws(() => readLocationContext(encodeURIComponent('{\"directory\":\"/repo\",\"workspace\":\"w\"}'), \"legacy\"))\n assert.throws(() => readLocationRef({ directory: \"/repo\", workspaceID: null }))\n assert.equal(sameLocation({ directory: \"/repo\", workspaceID: \"one\" }, { directory: \"/repo\", workspaceID: \"two\" }), false)\n})\n\ntest(\"global Forms and catalog requests preserve explicit identity without forwarding private headers\", () => {\n const ref = { directory: \"/工作/100% ready\", workspaceID: \"worktree\" }\n const options = locationRequestOptions(ref)!\n const headers = new Headers({ ...options.headers, \"x-opencode-directory\": encodeURIComponent(ref.directory) })\n applyLocationContext(new URL(\"http://localhost/api/session/global/form/f/reply\"), \"POST\", { answer: {} }, headers, \"legacy\")\n assert.equal(headers.get(\"x-opencode-workspace\"), \"worktree\")\n assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false)\n const url = new URL(\"http://localhost/api/form\")\n url.searchParams.set(\"location[directory]\", ref.directory)\n applyLocationContext(url, \"GET\", undefined, new Headers(options.headers), \"legacy\")\n assert.equal(url.searchParams.get(\"location[workspace]\"), \"worktree\")\n const foreign = new URL(\"http://localhost/api/form?location[directory]=/other\")\n assert.throws(() => applyLocationContext(foreign, \"GET\", undefined, new Headers(options.headers), \"legacy\"), /does not match/)\n assert.throws(() => applyLocationContext(url, \"GET\", undefined, new Headers(options.headers), \"modern\"), /Unsupported/)\n})\n\ntest(\"real generated list, RPC, eviction and credential calls use their distinct legacy wire locations\", async () => {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version: \"2.0.3\", pid: 1, discovery: \"health\" })\n const location = { directory: \"/repo\", workspaceID: \"wrk_one\" }\n const calls: URL[] = []\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {\n const request = new Request(input, init)\n assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false)\n const url = new URL(request.url)\n calls.push(url)\n if (url.pathname === \"/api/session\") {\n assert.equal(url.searchParams.get(\"directory\"), \"/repo\")\n assert.equal(url.searchParams.get(\"workspace\"), \"wrk_one\")\n assert.equal(url.searchParams.has(\"location[directory]\"), false)\n return Response.json({ data: [], cursor: {} })\n }\n assert.equal(url.searchParams.get(\"location[directory]\"), \"/repo\")\n assert.equal(url.searchParams.get(\"location[workspace]\"), \"wrk_one\")\n return url.pathname.startsWith(\"/api/rpc/\") ? Response.json({ output: true }) : new Response(null, { status: 204 })\n }) })\n const options = locationRequestOptions(location)\n await client.session.list({ directory: location.directory }, options)\n await client.rpc.call({ rpcID: \"fixture\", method: \"check\", input: {}, location: { directory: location.directory } }, options)\n await client.debug.location.evict({ location: { directory: location.directory } }, options)\n await client.credential.remove({ credentialID: \"credential\" }, options)\n assert.equal(calls.length, 4)\n await assert.rejects(client.session.list({ cursor: \"native-cursor\" }, options))\n assert.equal(calls.length, 4, \"conflicting cursor context never dispatches\")\n})\n\ntest(\"directory-only credential context is native-scoped on legacy and global on modern\", async () => {\n for (const version of [\"2.0.3\", \"2.0.4\"]) {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version, pid: 1, discovery: version === \"2.0.3\" ? \"health\" : \"status\" })\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {\n const request = new Request(input, init)\n assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false)\n assert.equal(new URL(request.url).search, version === \"2.0.3\" ? \"?location%5Bdirectory%5D=%2Frepo\" : \"\")\n return new Response(null, { status: 204 })\n }) })\n await client.credential.remove({ credentialID: \"credential\" }, locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true }))\n }\n})\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Service } from "@opencode/client/service";
|
|
2
|
+
import { runtimeIdentity } from "./runtime";
|
|
3
|
+
function object(value) {
|
|
4
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
5
|
+
}
|
|
6
|
+
// Unknown release numbers are not a startup/version gate. Read their declared
|
|
7
|
+
// HTTP contract before selecting a serializer; never trial a write operation.
|
|
8
|
+
export async function negotiateRuntime(endpoint, fetcher, signal) {
|
|
9
|
+
const response = await fetcher(new URL("/openapi.json", endpoint.url), {
|
|
10
|
+
headers: Service.headers(endpoint), redirect: "error",
|
|
11
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
await response.body?.cancel();
|
|
15
|
+
throw new Error(`Cannot negotiate OpenCode contract (HTTP ${response.status})`);
|
|
16
|
+
}
|
|
17
|
+
const reader = response.body?.getReader();
|
|
18
|
+
if (!reader)
|
|
19
|
+
throw new Error("Missing OpenCode contract");
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let length = 0;
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const chunk = await reader.read();
|
|
25
|
+
if (chunk.done)
|
|
26
|
+
break;
|
|
27
|
+
length += chunk.value.byteLength;
|
|
28
|
+
if (length > 8 * 1024 * 1024)
|
|
29
|
+
throw new Error("OpenCode contract exceeds limit");
|
|
30
|
+
chunks.push(chunk.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
await reader.cancel().catch(() => { });
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
reader.releaseLock();
|
|
39
|
+
}
|
|
40
|
+
const document = object(JSON.parse(Buffer.concat(chunks, length).toString("utf8")));
|
|
41
|
+
const paths = object(document.paths);
|
|
42
|
+
const resolve = (input) => {
|
|
43
|
+
const schema = object(input);
|
|
44
|
+
const ref = schema.$ref;
|
|
45
|
+
return typeof ref === "string" && ref.startsWith("#/components/schemas/")
|
|
46
|
+
? object(document.components?.schemas?.[ref.slice("#/components/schemas/".length)]) : schema;
|
|
47
|
+
};
|
|
48
|
+
const has = (path, method) => Boolean(object(paths[path])[method]);
|
|
49
|
+
const body = (path, field) => {
|
|
50
|
+
const operation = object(object(paths[path]).post);
|
|
51
|
+
return field in object(resolve(operation.requestBody?.content?.["application/json"]?.schema).properties);
|
|
52
|
+
};
|
|
53
|
+
const session = "/api/session/{sessionID}";
|
|
54
|
+
const inbox = object(resolve(document.components?.schemas?.["Session.Inbox.User"]).properties);
|
|
55
|
+
const modern = has(session, "patch") && has("/api/form", "get")
|
|
56
|
+
&& has(`${session}/form/{formID}`, "delete") && has(`/api/experimental/session/{sessionID}/wait`, "post")
|
|
57
|
+
&& body(`${session}/fork`, "before") && body(`${session}/command`, "name")
|
|
58
|
+
&& body(`${session}/permission/{requestID}/reply`, "decision")
|
|
59
|
+
&& "time" in inbox && !("timeCreated" in inbox);
|
|
60
|
+
const legacy = has(`${session}/rename`, "post") && has("/api/form/request", "get")
|
|
61
|
+
&& has(`${session}/form/{formID}/cancel`, "post") && has(`${session}/wait`, "post")
|
|
62
|
+
&& body(`${session}/fork`, "boundary") && body(`${session}/command`, "command")
|
|
63
|
+
&& body(`${session}/permission/{requestID}/reply`, "reply")
|
|
64
|
+
&& "timeCreated" in inbox && !("time" in inbox);
|
|
65
|
+
if (modern === legacy)
|
|
66
|
+
throw new Error(`Unsupported OpenCode contract: ${runtimeIdentity(endpoint)?.version ?? "unknown"}`);
|
|
67
|
+
const profile = modern ? "modern" : "legacy";
|
|
68
|
+
const identity = runtimeIdentity(endpoint);
|
|
69
|
+
if (identity?.contract)
|
|
70
|
+
identity.contract.profile = profile;
|
|
71
|
+
return profile;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=negotiate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiate.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/negotiate.ts"],"names":[],"mappings":"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","sourcesContent":["import { Service, type Endpoint } from \"@opencode/client/service\"\nimport { runtimeIdentity, type ContractProfile } from \"./runtime\"\n\ntype ObjectValue = Record<string, any>\nfunction object(value: unknown): ObjectValue {\n return value && typeof value === \"object\" && !Array.isArray(value) ? value : {}\n}\n\n// Unknown release numbers are not a startup/version gate. Read their declared\n// HTTP contract before selecting a serializer; never trial a write operation.\nexport async function negotiateRuntime(endpoint: Endpoint, fetcher: typeof fetch, signal: AbortSignal): Promise<Exclude<ContractProfile, \"unknown\">> {\n const response = await fetcher(new URL(\"/openapi.json\", endpoint.url), {\n headers: Service.headers(endpoint), redirect: \"error\",\n signal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),\n })\n if (!response.ok) {\n await response.body?.cancel()\n throw new Error(`Cannot negotiate OpenCode contract (HTTP ${response.status})`)\n }\n const reader = response.body?.getReader()\n if (!reader) throw new Error(\"Missing OpenCode contract\")\n const chunks: Uint8Array[] = []\n let length = 0\n try {\n while (true) {\n const chunk = await reader.read()\n if (chunk.done) break\n length += chunk.value.byteLength\n if (length > 8 * 1024 * 1024) throw new Error(\"OpenCode contract exceeds limit\")\n chunks.push(chunk.value)\n }\n } catch (error) { await reader.cancel().catch(() => {}); throw error }\n finally { reader.releaseLock() }\n const document = object(JSON.parse(Buffer.concat(chunks, length).toString(\"utf8\")))\n const paths = object(document.paths)\n const resolve = (input: unknown) => {\n const schema = object(input)\n const ref = schema.$ref\n return typeof ref === \"string\" && ref.startsWith(\"#/components/schemas/\")\n ? object(document.components?.schemas?.[ref.slice(\"#/components/schemas/\".length)]) : schema\n }\n const has = (path: string, method: string) => Boolean(object(paths[path])[method])\n const body = (path: string, field: string) => {\n const operation = object(object(paths[path]).post)\n return field in object(resolve(operation.requestBody?.content?.[\"application/json\"]?.schema).properties)\n }\n const session = \"/api/session/{sessionID}\"\n const inbox = object(resolve(document.components?.schemas?.[\"Session.Inbox.User\"]).properties)\n const modern = has(session, \"patch\") && has(\"/api/form\", \"get\")\n && has(`${session}/form/{formID}`, \"delete\") && has(`/api/experimental/session/{sessionID}/wait`, \"post\")\n && body(`${session}/fork`, \"before\") && body(`${session}/command`, \"name\")\n && body(`${session}/permission/{requestID}/reply`, \"decision\")\n && \"time\" in inbox && !(\"timeCreated\" in inbox)\n const legacy = has(`${session}/rename`, \"post\") && has(\"/api/form/request\", \"get\")\n && has(`${session}/form/{formID}/cancel`, \"post\") && has(`${session}/wait`, \"post\")\n && body(`${session}/fork`, \"boundary\") && body(`${session}/command`, \"command\")\n && body(`${session}/permission/{requestID}/reply`, \"reply\")\n && \"timeCreated\" in inbox && !(\"time\" in inbox)\n if (modern === legacy) throw new Error(`Unsupported OpenCode contract: ${runtimeIdentity(endpoint)?.version ?? \"unknown\"}`)\n const profile = modern ? \"modern\" : \"legacy\"\n const identity = runtimeIdentity(endpoint)\n if (identity?.contract) identity.contract.profile = profile\n return profile\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { createRuntimeFetch } from "./transport";
|
|
4
|
+
import { rememberRuntime } from "./runtime";
|
|
5
|
+
import { legacyContractFixture } from "./contract-fixture";
|
|
6
|
+
test("unknown contracts perform authenticated bounded read negotiation before any mutation", async () => {
|
|
7
|
+
for (const kind of ["unauthorized", "unrecognized", "oversized"]) {
|
|
8
|
+
const endpoint = { url: "http://127.0.0.1:4321", auth: { type: "basic", username: "opencode", password: "fixture" } };
|
|
9
|
+
rememberRuntime(endpoint, { version: "2.0.100", pid: 1, discovery: "status" });
|
|
10
|
+
let calls = 0, cancelled = false;
|
|
11
|
+
const transport = createRuntimeFetch(endpoint, async (input, init) => {
|
|
12
|
+
calls++;
|
|
13
|
+
assert.equal(new URL(String(input)).pathname, "/openapi.json");
|
|
14
|
+
assert.equal(init?.method ?? "GET", "GET");
|
|
15
|
+
assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:fixture").toString("base64")}`);
|
|
16
|
+
assert.equal(init?.redirect, "error");
|
|
17
|
+
if (kind === "unauthorized")
|
|
18
|
+
return new Response(null, { status: 401 });
|
|
19
|
+
if (kind === "unrecognized")
|
|
20
|
+
return Response.json({ paths: {} });
|
|
21
|
+
return new Response(new ReadableStream({
|
|
22
|
+
start(controller) { controller.enqueue(new Uint8Array(8 * 1024 * 1024 + 1)); },
|
|
23
|
+
cancel() { cancelled = true; },
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
await assert.rejects(transport(`${endpoint.url}/api/session/s`, { method: "PATCH", body: '{"title":"fixture"}' }), /contract|HTTP 401/);
|
|
27
|
+
assert.equal(calls, 1, "negotiation failure must never dispatch a mutation");
|
|
28
|
+
if (kind === "oversized")
|
|
29
|
+
assert.equal(cancelled, true);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
test("shared negotiation has independent subscriber cancellation and never retries a mutation", async () => {
|
|
33
|
+
const endpoint = { url: "http://127.0.0.1:4321" };
|
|
34
|
+
rememberRuntime(endpoint, { version: "next-contract", pid: 1, discovery: "health" });
|
|
35
|
+
let complete;
|
|
36
|
+
let negotiationSignal;
|
|
37
|
+
const calls = [];
|
|
38
|
+
const transport = createRuntimeFetch(endpoint, async (input, init) => {
|
|
39
|
+
const pathname = new URL(String(input)).pathname;
|
|
40
|
+
calls.push(pathname);
|
|
41
|
+
if (pathname === "/openapi.json") {
|
|
42
|
+
negotiationSignal = init?.signal;
|
|
43
|
+
return new Promise(resolve => { complete = resolve; });
|
|
44
|
+
}
|
|
45
|
+
assert.equal(pathname, "/api/session/s/rename");
|
|
46
|
+
return new Response(null, { status: 204 });
|
|
47
|
+
});
|
|
48
|
+
const first = new AbortController(), third = new AbortController();
|
|
49
|
+
const a = transport(`${endpoint.url}/api/location`, { signal: first.signal });
|
|
50
|
+
const b = transport(`${endpoint.url}/api/session/s`, { method: "PATCH", body: '{"title":"fixture"}' });
|
|
51
|
+
const c = transport(`${endpoint.url}/api/location`, { signal: third.signal });
|
|
52
|
+
first.abort();
|
|
53
|
+
third.abort();
|
|
54
|
+
await assert.rejects(a, { name: "AbortError" });
|
|
55
|
+
await assert.rejects(c, { name: "AbortError" });
|
|
56
|
+
assert.equal(negotiationSignal?.aborted, false);
|
|
57
|
+
complete(Response.json(legacyContractFixture));
|
|
58
|
+
assert.equal((await b).status, 204);
|
|
59
|
+
assert.deepEqual(calls, ["/openapi.json", "/api/session/s/rename"]);
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=negotiate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiate.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/negotiate.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { createRuntimeFetch } from \"./transport\"\nimport { rememberRuntime } from \"./runtime\"\nimport { legacyContractFixture } from \"./contract-fixture\"\n\ntest(\"unknown contracts perform authenticated bounded read negotiation before any mutation\", async () => {\n for (const kind of [\"unauthorized\", \"unrecognized\", \"oversized\"] as const) {\n const endpoint = { url: \"http://127.0.0.1:4321\", auth: { type: \"basic\" as const, username: \"opencode\", password: \"fixture\" } }\n rememberRuntime(endpoint, { version: \"2.0.100\", pid: 1, discovery: \"status\" })\n let calls = 0, cancelled = false\n const transport = createRuntimeFetch(endpoint, async (input, init) => {\n calls++\n assert.equal(new URL(String(input)).pathname, \"/openapi.json\")\n assert.equal(init?.method ?? \"GET\", \"GET\")\n assert.equal(new Headers(init?.headers).get(\"authorization\"), `Basic ${Buffer.from(\"opencode:fixture\").toString(\"base64\")}`)\n assert.equal(init?.redirect, \"error\")\n if (kind === \"unauthorized\") return new Response(null, { status: 401 })\n if (kind === \"unrecognized\") return Response.json({ paths: {} })\n return new Response(new ReadableStream({\n start(controller) { controller.enqueue(new Uint8Array(8 * 1024 * 1024 + 1)) },\n cancel() { cancelled = true },\n }))\n })\n await assert.rejects(transport(`${endpoint.url}/api/session/s`, { method: \"PATCH\", body: '{\"title\":\"fixture\"}' }), /contract|HTTP 401/)\n assert.equal(calls, 1, \"negotiation failure must never dispatch a mutation\")\n if (kind === \"oversized\") assert.equal(cancelled, true)\n }\n})\n\ntest(\"shared negotiation has independent subscriber cancellation and never retries a mutation\", async () => {\n const endpoint = { url: \"http://127.0.0.1:4321\" }\n rememberRuntime(endpoint, { version: \"next-contract\", pid: 1, discovery: \"health\" })\n let complete!: (response: Response) => void\n let negotiationSignal: AbortSignal | null | undefined\n const calls: string[] = []\n const transport = createRuntimeFetch(endpoint, async (input, init) => {\n const pathname = new URL(String(input)).pathname\n calls.push(pathname)\n if (pathname === \"/openapi.json\") {\n negotiationSignal = init?.signal\n return new Promise<Response>(resolve => { complete = resolve })\n }\n assert.equal(pathname, \"/api/session/s/rename\")\n return new Response(null, { status: 204 })\n })\n const first = new AbortController(), third = new AbortController()\n const a = transport(`${endpoint.url}/api/location`, { signal: first.signal })\n const b = transport(`${endpoint.url}/api/session/s`, { method: \"PATCH\", body: '{\"title\":\"fixture\"}' })\n const c = transport(`${endpoint.url}/api/location`, { signal: third.signal })\n first.abort()\n third.abort()\n await assert.rejects(a, { name: \"AbortError\" })\n await assert.rejects(c, { name: \"AbortError\" })\n assert.equal(negotiationSignal?.aborted, false)\n complete(Response.json(legacyContractFixture))\n assert.equal((await b).status, 204)\n assert.deepEqual(calls, [\"/openapi.json\", \"/api/session/s/rename\"])\n})\n"]}
|