@ottimis/jack-provider-sdk 0.9.0 → 0.13.0

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/src/provider.ts CHANGED
@@ -17,7 +17,9 @@
17
17
  */
18
18
 
19
19
  import type { AgentBackend, AgentPermissionMode, AgentQueryOptions, McpServerSpec } from './backend'
20
+ import type { ProviderDefaultsApi } from './defaults'
20
21
  import type { HostServices } from './host'
22
+ import type { OneshotApi } from './oneshot'
21
23
  import type { ProfilesApi } from './profiles'
22
24
  import type { SandboxApi } from './sandbox'
23
25
  import type { UsageApi } from './usage'
@@ -38,7 +40,13 @@ export type ProviderId = string
38
40
  * commands carry `body` + `filePath`, builtin and wire-sourced ones
39
41
  * don't (they don't *have* a markdown file behind them).
40
42
  */
41
- export type SlashCommandScope = 'builtin' | 'wire' | 'user' | 'project' | (string & {})
43
+ export type SlashCommandScope =
44
+ | 'builtin'
45
+ | 'wire'
46
+ | 'user'
47
+ | 'project'
48
+ | 'jack-builtin'
49
+ | (string & {})
42
50
 
43
51
  /**
44
52
  * Common surface every slash command def carries regardless of source.
@@ -51,7 +59,7 @@ type SlashCommandDefBase = {
51
59
  }
52
60
 
53
61
  /**
54
- * Slash-command definition surfaced by a provider. Three sources can
62
+ * Slash-command definition surfaced by a provider. Four sources can
55
63
  * coexist (see {@link SlashCommandSupport}):
56
64
  *
57
65
  * - `'builtin'` — static catalog the runtime intercepts. The renderer
@@ -64,6 +72,14 @@ type SlashCommandDefBase = {
64
72
  * `filePath` + `body` are required so the renderer can offer "open
65
73
  * in editor" affordances and the host can expand `$ARGUMENTS` /
66
74
  * `$N` placeholders.
75
+ * - `'jack-builtin'` — host-shipped slash command pack distributed
76
+ * inside the Jack app bundle (e.g. `/changelog-turn`,
77
+ * `/save-decision` for the user-data-tables feature). Read-only
78
+ * for the user (no edit/delete), expanded the same way as
79
+ * `'user'/'project'` (`$ARGUMENTS` + `$N` substitution). The body
80
+ * comes from `resources/slash-commands/builtin/<name>.md`; the
81
+ * renderer treats the catalog like any file-sourced command but
82
+ * hides authoring affordances behind the `readonly` flag.
67
83
  *
68
84
  * Discriminated by `scope` so consumers narrow before reading the
69
85
  * file-only fields. Replaces the legacy uniform shape that forced
@@ -74,6 +90,31 @@ export type SlashCommandDef =
74
90
  | (SlashCommandDefBase & { scope: 'builtin' })
75
91
  | (SlashCommandDefBase & { scope: 'wire' })
76
92
  | (SlashCommandDefBase & { scope: 'user' | 'project'; body: string; filePath: string })
93
+ | (SlashCommandDefBase & { scope: 'jack-builtin'; body: string; readonly: true })
94
+
95
+ /**
96
+ * Input shape for {@link SlashCommandSupport.createCommand}. The host
97
+ * collects these fields from a dialog form and hands them verbatim to
98
+ * the provider, which writes the markdown file in its native layout
99
+ * (Claude: `~/.claude/commands/<name>.md` for user scope,
100
+ * `<projectPath>/.claude/commands/<name>.md` for project).
101
+ *
102
+ * `name` may include subdirectory namespacing via `:` — e.g.
103
+ * `git:review` → file at `git/review.md` under the scope root.
104
+ * Provider validates the name (regex `[a-z][a-z0-9:-]*` typically) and
105
+ * rejects with an error when the file already exists (caller decides
106
+ * whether to retry with `overwrite`, omitted in v1 to avoid
107
+ * accidental clobbering).
108
+ */
109
+ export type CreateSlashCommandInput = {
110
+ name: string
111
+ scope: 'user' | 'project'
112
+ description?: string
113
+ argumentHint?: string
114
+ body: string
115
+ /** Required when `scope === 'project'`. */
116
+ projectPath?: string
117
+ }
77
118
 
78
119
  /**
79
120
  * Parsed envelope a provider's CLI may wrap slash commands in when it logs
@@ -148,6 +189,71 @@ export type SlashCommandSupport = {
148
189
  sessionId: string,
149
190
  callback: (commands: SlashCommandDef[]) => void
150
191
  ): () => void
192
+ /**
193
+ * Authoring: create a new file-sourced slash command on disk. Only
194
+ * meaningful for providers that surface file-based commands (Claude
195
+ * `.claude/commands/*.md`); providers without an on-disk format leave
196
+ * this undefined and the host hides the "+ New" affordance.
197
+ *
198
+ * Contract:
199
+ * - Validates `input.name` against the provider's naming convention
200
+ * (typical regex: `[a-z][a-z0-9:-]*` with `:` for subdirectory
201
+ * namespacing).
202
+ * - Resolves the target file path under the scope root, creating
203
+ * intermediate directories as needed.
204
+ * - Refuses overwrite when the file already exists (throws an
205
+ * error with a stable code so the host can render a clear
206
+ * conflict message).
207
+ * - Writes frontmatter (`description`, `argument-hint`) plus the
208
+ * body verbatim. Returns the absolute file path.
209
+ *
210
+ * The host calls this from `provider:slash-commands:create` IPC and
211
+ * the new file is picked up by {@link subscribeFsChanges} so the
212
+ * renderer's palette refreshes without a manual reload.
213
+ */
214
+ createCommand?(input: CreateSlashCommandInput): Promise<{ filePath: string }>
215
+ /**
216
+ * Authoring: delete a file-sourced slash command. The host passes the
217
+ * absolute `filePath` previously returned in a {@link SlashCommandDef}.
218
+ * `projectPath` (when provided) lets the provider validate
219
+ * project-scoped deletes too — without it, only files inside the user
220
+ * root are accepted (delete-by-path on a project file requires the
221
+ * caller to thread the project path through, which the host knows
222
+ * from the active session's cwd).
223
+ *
224
+ * Contract:
225
+ * - Verifies that `filePath` is contained inside one of the provider's
226
+ * known scope roots (path normalisation + `path.relative()` check)
227
+ * to prevent the host from accidentally requesting deletion of a
228
+ * file outside the slash-commands tree.
229
+ * - Deletes the file. Idempotent: a missing file is treated as a
230
+ * successful no-op (no `ENOENT` thrown).
231
+ *
232
+ * Like {@link createCommand}, omitting this field hides the "Delete"
233
+ * affordance for file-sourced rows in the renderer.
234
+ */
235
+ deleteCommand?(filePath: string, projectPath?: string): Promise<{ ok: true }>
236
+ /**
237
+ * Subscribe to filesystem changes in the provider's user/project
238
+ * command roots. Distinct from {@link subscribeToWireCommands}: this
239
+ * is fs-driven (markdown files added/edited/deleted on disk),
240
+ * whereas the wire variant is provider-pushed runtime state.
241
+ *
242
+ * Contract:
243
+ * - The provider invokes the callback whenever a `.md` file under
244
+ * a known root is added, modified, or deleted (debounce is the
245
+ * provider's concern; the host treats the callback as "your
246
+ * cached list is stale, refetch").
247
+ * - The callback receives no payload — it's a stale-flag, not a
248
+ * diff. The host responds by re-running `scanCommands` and
249
+ * emitting `slashCommands:changed` to its renderers.
250
+ * - Returns an unsubscribe function. The host calls it on shutdown
251
+ * or provider switch.
252
+ * - Optional. Providers without a file-based source (Codex,
253
+ * Gemini today) leave this undefined and the host's cache is
254
+ * invalidated only on session boundary events.
255
+ */
256
+ subscribeFsChanges?(callback: () => void): () => void
151
257
  }
152
258
 
153
259
  /**
@@ -170,6 +276,19 @@ export type ReadSessionTranscriptOptions = {
170
276
  * current consumers.
171
277
  */
172
278
  includeSystemMessages?: boolean
279
+ /**
280
+ * Provider-config root for transcript lookup (Claude `CLAUDE_CONFIG_DIR`,
281
+ * Codex `CODEX_HOME`, …). When set, the provider reads transcripts from
282
+ * `<configDir>/<provider-native-subpath>` instead of its implicit default
283
+ * — required when the session was spawned under a non-default
284
+ * {@link ProviderProfile} so the JSONL/rollout file resolves correctly.
285
+ *
286
+ * The host resolves this from the session's pinned `profile_id` and
287
+ * passes it verbatim. Providers without a profile concept ignore the
288
+ * field; providers with profiles MUST treat it as authoritative when
289
+ * present.
290
+ */
291
+ configDir?: string
173
292
  }
174
293
 
175
294
  /**
@@ -298,6 +417,15 @@ export type CapabilityMatrix = {
298
417
  * sandbox request returns a clear error.
299
418
  */
300
419
  sandbox: boolean
420
+ /**
421
+ * Provider exposes a non-agentic single-shot completion via
422
+ * {@link JackProvider.oneshot}. When `false` the host hides any UI
423
+ * affordance that depends on it (e.g. CommitComposer's "AI commit
424
+ * message" button is disabled with an explanatory tooltip).
425
+ *
426
+ * When `true`, {@link JackProvider.oneshot} MUST be defined.
427
+ */
428
+ oneshot: boolean
301
429
  /**
302
430
  * Permission modes the provider actually supports. Drives the
303
431
  * Shift-Tab cycle in the renderer (`MessageInputBar`) and any
@@ -736,6 +864,27 @@ export type JackProvider = {
736
864
  * mode for this provider's sessions.
737
865
  */
738
866
  sandbox?: SandboxApi
867
+ /**
868
+ * One-shot completion capability — non-agentic, no tools, no session.
869
+ * See {@link OneshotApi}. Optional; when undefined `capabilities.oneshot`
870
+ * MUST be `false` and the host disables any UI affordance that relies
871
+ * on this primitive (e.g. CommitComposer's AI commit message button).
872
+ */
873
+ oneshot?: OneshotApi
874
+ /**
875
+ * User-configurable defaults applied to newly-created sessions —
876
+ * which model, reasoning-effort tier, and permission mode the host
877
+ * should pre-fill on the session row when the user spawns a new
878
+ * session against this provider. See {@link ProviderDefaultsApi}.
879
+ *
880
+ * Optional + presence-based. A provider that omits the field doesn't
881
+ * appear in `Settings → Provider defaults` and no pre-fill happens for
882
+ * its sessions (the runtime falls back to its built-in default model /
883
+ * effort / permission_mode at spawn time). Catalog-only contract: the
884
+ * provider declares the legal values, the host owns storage (kv) and
885
+ * resolution.
886
+ */
887
+ defaults?: ProviderDefaultsApi
739
888
  /**
740
889
  * Optional one-shot activation hook. Called once by the host during
741
890
  * registration with a {@link HostServices} bag scoped to this
package/src/sandbox.ts CHANGED
@@ -31,30 +31,36 @@
31
31
  */
32
32
 
33
33
  /**
34
- * Mount the provider's host-side config directory into the container.
35
- * Most providers persist auth + sessions + per-user settings in a dotfile
36
- * dir under `$HOME` (Claude `~/.claude`, Codex `~/.codex`, Gemini
37
- * `~/.gemini`). The host mounts this dir into the container at
38
- * {@link containerPath} so the CLI inside the container has access to the
39
- * same auth state as the host.
34
+ * Mount a named Docker volume into the container at {@link containerPath}.
35
+ * The pattern Anthropic recommends for CLI config dirs (see
36
+ * https://code.claude.com/docs/en/devcontainer#persist-authentication-and-settings-across-rebuilds):
37
+ * the volume is auto-created on demand, persists across container
38
+ * restarts, and isolates writes from the host filesystem entirely.
39
+ * Best fit for `~/.claude`, `~/.codex`, `~/.gemini` since they hold auth
40
+ * tokens, session JSONLs, and CLI-mutated settings.
40
41
  *
41
- * Read-only by default the container shouldn't be writing back to the
42
- * user's persistent config from inside the sandbox. Set `readOnly: false`
43
- * only when the provider's CLI genuinely needs to mutate state inside the
44
- * config dir (e.g. session JSONL append).
42
+ * Read-only is recommended whenever the CLI doesn't genuinely need to
43
+ * mutate state. Set `readOnly: false` when the CLI writes back — Claude
44
+ * writes session-env, project history, MCP additions; Codex appends
45
+ * thread JSONL; etc.
45
46
  */
46
47
  export type SandboxConfigMount = {
47
48
  /**
48
- * Absolute host path. Provider implementations resolve this lazily call
49
- * `os.homedir()` + `path.join(...)` at the time `configMount` is read,
50
- * not at module-load time, so test environments and per-process HOME
51
- * overrides work correctly.
49
+ * Docker volume name. The host auto-creates the volume if missing
50
+ * (via `docker volume create <name>`). Use a stable, namespaced name
51
+ * like `jack-sandbox-<provider>-config` so volumes can be inspected
52
+ * / pruned predictably from the Docker CLI.
53
+ *
54
+ * Volumes are NOT scoped per session by default — sharing one volume
55
+ * across sandbox sessions of the same provider is the common case
56
+ * and matches Anthropic's reference. If you need per-session
57
+ * isolation, embed the session id in the name.
52
58
  */
53
- hostPath: string
59
+ readonly volumeName: string
54
60
  /** Absolute container path. */
55
- containerPath: string
56
- /** When `true`, the host adds `:ro` to the bind. Default: `true` recommended. */
57
- readOnly: boolean
61
+ readonly containerPath: string
62
+ /** When `true`, the host adds `:ro` to the bind. */
63
+ readonly readOnly: boolean
58
64
  }
59
65
 
60
66
  /**
@@ -87,11 +93,17 @@ export interface SandboxApi {
87
93
  readonly binaryName: string
88
94
 
89
95
  /**
90
- * Mount the provider's host-side config directory into the container.
91
- * Optional — providers that are stateless on the host (none today)
92
- * leave this undefined.
96
+ * Mount provider-side config artifacts (directories and/or files) into
97
+ * the container. Optional — providers that are stateless on the host
98
+ * (none today) leave this undefined or pass an empty array.
99
+ *
100
+ * Multiple entries support providers whose CLI splits state across more
101
+ * than one path (e.g. Claude needs both `~/.claude/` for the dotfile dir
102
+ * and `~/.claude.json` for the main config file). Order is preserved
103
+ * but mounts are independent — if two entries overlap, Docker resolves
104
+ * them in declaration order.
93
105
  */
94
- readonly configMount?: SandboxConfigMount
106
+ readonly configMounts?: readonly SandboxConfigMount[]
95
107
 
96
108
  /**
97
109
  * Optional environment extras to inject into the container. Layered AFTER
@@ -105,4 +117,54 @@ export interface SandboxApi {
105
117
  * sandbox even when the user has it on globally.
106
118
  */
107
119
  envExtras?(): Record<string, string>
120
+
121
+ /**
122
+ * Optional spawn-time setup hook. Runs once on the host before the
123
+ * container starts and lets the provider produce per-session artifacts
124
+ * (e.g. a sanitized `settings.json` with hooks stripped, a generated
125
+ * MCP manifest) and mount them into the container alongside the static
126
+ * {@link configMounts}.
127
+ *
128
+ * Returned `extraMounts` are appended to {@link configMounts} in
129
+ * declaration order. The `cleanup` callback (if provided) is invoked
130
+ * after the container exits so the provider can unlink temp files.
131
+ *
132
+ * Errors thrown here propagate as spawn failures — keep the work fast
133
+ * and synchronous-friendly (file I/O, not network calls).
134
+ */
135
+ prepareSpawn?(
136
+ ctx: SandboxSpawnContext
137
+ ): SandboxSpawnSetup | Promise<SandboxSpawnSetup>
138
+ }
139
+
140
+ /**
141
+ * Context passed to {@link SandboxApi.prepareSpawn}. Identifies the Jack
142
+ * session and the project root being mounted at `/workspace`. Providers
143
+ * use these to namespace temp files (one settings overlay per session)
144
+ * and avoid collisions across concurrent sandbox sessions.
145
+ */
146
+ export interface SandboxSpawnContext {
147
+ /** Stable per-session id. Safe to embed in temp filenames. */
148
+ readonly sessionId: string
149
+ /** Absolute host path mounted at `/workspace` inside the container. */
150
+ readonly projectPath: string
151
+ }
152
+
153
+ /**
154
+ * Return value of {@link SandboxApi.prepareSpawn}. Both fields optional —
155
+ * a no-op setup just returns `{}`.
156
+ */
157
+ export interface SandboxSpawnSetup {
158
+ /**
159
+ * Mounts to merge with the provider's static {@link configMounts}.
160
+ * Useful for overlaying generated files (e.g. a sanitized settings.json
161
+ * mounted on top of a config-dir mount shadows the original entry).
162
+ */
163
+ readonly extraMounts?: readonly SandboxConfigMount[]
164
+ /**
165
+ * Optional teardown. Invoked once after the container exits, even if
166
+ * the spawn fails after `prepareSpawn` resolved. Errors are logged but
167
+ * not propagated — cleanup is best-effort.
168
+ */
169
+ cleanup?(): void | Promise<void>
108
170
  }