@khorsheed/dsh-local-agent-claude-code 0.1.0-rc.6

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.
Files changed (38) hide show
  1. package/README.en.md +120 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +120 -0
  4. package/cordis.patch.yml +42 -0
  5. package/lib/client.js +1065 -0
  6. package/lib/index.js +3771 -0
  7. package/lib/invariant.js +31 -0
  8. package/lib/types/claude-cli-provider.d.ts +389 -0
  9. package/lib/types/claude-cli-provider.js +1509 -0
  10. package/lib/types/client/SettingsCard.d.ts +78 -0
  11. package/lib/types/client/SettingsCard.js +198 -0
  12. package/lib/types/client/icons.d.ts +18 -0
  13. package/lib/types/client/icons.js +2 -0
  14. package/lib/types/client/index.d.ts +36 -0
  15. package/lib/types/client/index.js +87 -0
  16. package/lib/types/client/locales.d.ts +64 -0
  17. package/lib/types/client/locales.js +55 -0
  18. package/lib/types/control-requests.d.ts +15 -0
  19. package/lib/types/control-requests.js +62 -0
  20. package/lib/types/index.d.ts +81 -0
  21. package/lib/types/index.js +300 -0
  22. package/lib/types/invariant.d.ts +16 -0
  23. package/lib/types/invariant.js +33 -0
  24. package/lib/types/live-driver.d.ts +172 -0
  25. package/lib/types/live-driver.js +1057 -0
  26. package/lib/types/live-switch.d.ts +67 -0
  27. package/lib/types/live-switch.js +85 -0
  28. package/lib/types/model-broker.d.ts +131 -0
  29. package/lib/types/model-broker.js +248 -0
  30. package/lib/types/model-catalog.d.ts +27 -0
  31. package/lib/types/model-catalog.js +118 -0
  32. package/lib/types/provision.d.ts +63 -0
  33. package/lib/types/provision.js +132 -0
  34. package/lib/types/records.d.ts +120 -0
  35. package/lib/types/records.js +687 -0
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +106 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Claude Code harness instantiation of the local-agent family. Registers the
3
+ * `claude-code` harness into the core registry: scoped home under the shared
4
+ * homes root (`CLAUDE_CONFIG_DIR`), login as a manual handoff (claude ≥2.1
5
+ * prints no OAuth URL off a TTY, so `/login` replies with the exact terminal
6
+ * command and the registry watches for the credential), and project-file
7
+ * session records. Delegation mounts through the `claude-local` provider
8
+ * (one-shot `claude -p --output-format json` under the scoped home); the
9
+ * bundle patch's `tool-subagent-claude-code-local` row puts the tool on the
10
+ * profile root, so every agent preset can delegate.
11
+ *
12
+ * @module @khorsheed/dsh-local-agent-claude-code
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import z from '@deepseek-ai/schemastery';
16
+ /** Stable Cordis plugin name; the bundle patch row id. */
17
+ export declare const name = "local-agent-claude-code";
18
+ /** Services required before the harness can register. */
19
+ export declare const inject: string[];
20
+ /** Plugin config: the permission mode fresh delegations default to, plus the live driver. */
21
+ export interface Config {
22
+ /**
23
+ * `claude -p` permission handling; `skip` passes
24
+ * `--dangerously-skip-permissions` so the child can write files without an
25
+ * interactive approval prompt. Defaults to `skip` because a one-shot CLI
26
+ * subagent has no approval surface.
27
+ */
28
+ permissionMode?: 'skip' | 'normal';
29
+ /**
30
+ * The model every delegation round starts the CLI with (`claude -p --model
31
+ * <model>`, the same flag in resident mode). Absent — the default — passes
32
+ * NO model flag at all: the scoped `settings.json`'s own `model`, or the
33
+ * CLI's built-in default with none, decides exactly as it did before this
34
+ * key existed. The settings card writes the same key, so a change applies
35
+ * to the next round without a reload.
36
+ */
37
+ model?: string;
38
+ /**
39
+ * `ANTHROPIC_BASE_URL` for the child CLI; absent inherits the host
40
+ * process environment (a user-level proxy like `https://proxy.example.com/anthropic`
41
+ * is typically exported there). Set when delegations must route through a
42
+ * different endpoint than the login flow used.
43
+ */
44
+ baseUrl?: string;
45
+ /**
46
+ * HTTP proxy for the child CLI's own traffic (model calls AND OAuth
47
+ * refresh), provisioned into the scoped `settings.json` env block. Needed
48
+ * when the host process environment carries no proxy (a supervisor-spawned
49
+ * instance does not inherit the user's shell exports).
50
+ */
51
+ proxyUrl?: string;
52
+ /**
53
+ * Live driver: keep one resident stream-json process per member and drive
54
+ * turns over stdin messages (runtime-level graceful interrupt, same-shape
55
+ * push stream) instead of one `claude -p` process per round. Default off;
56
+ * the exec one-shot stays the fallback whenever the channel cannot come up.
57
+ */
58
+ live?: boolean;
59
+ /** Idle lifetime of an unused resident runtime before reclaim. */
60
+ liveIdleMs?: number;
61
+ /** @deprecated Accepted for old profiles; live output is always incremental. */
62
+ liveMirrorGranularity?: 'event' | 'token';
63
+ /** Model identifiers the settings card saved before (settings field). */
64
+ recentModels?: readonly string[];
65
+ }
66
+ export declare const Config: z;
67
+ /** The permission mode a fresh delegation defaults to. */
68
+ export declare const DEFAULT_PERMISSION_MODE: NonNullable<Config['permissionMode']>;
69
+ /**
70
+ * Settings namespace for the settings-page card. The Cordis config feeds the
71
+ * composition `base` layer, so a field absent from the user layer inherits the
72
+ * YAML value — the card only ever stores deliberate overrides.
73
+ */
74
+ export declare const CLAUDE_SETTINGS_NAMESPACE = "local-agent-claude-code";
75
+ /**
76
+ * Register the Claude Code harness into the local-agent registry.
77
+ * @param ctx - plugin context carrying the registry.
78
+ * @param config - plugin config; `permissionMode` selects the `claude -p` policy.
79
+ */
80
+ export declare function apply(ctx: Context, config: Config): void;
81
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Claude Code harness instantiation of the local-agent family. Registers the
3
+ * `claude-code` harness into the core registry: scoped home under the shared
4
+ * homes root (`CLAUDE_CONFIG_DIR`), login as a manual handoff (claude ≥2.1
5
+ * prints no OAuth URL off a TTY, so `/login` replies with the exact terminal
6
+ * command and the registry watches for the credential), and project-file
7
+ * session records. Delegation mounts through the `claude-local` provider
8
+ * (one-shot `claude -p --output-format json` under the scoped home); the
9
+ * bundle patch's `tool-subagent-claude-code-local` row puts the tool on the
10
+ * profile root, so every agent preset can delegate.
11
+ *
12
+ * @module @khorsheed/dsh-local-agent-claude-code
13
+ */
14
+ import z from '@deepseek-ai/schemastery';
15
+ import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
16
+ import { endpointHost } from '@khorsheed/dsh-local-agent/types';
17
+ import { ClaudeCliProvider, claudeCliVersion } from './claude-cli-provider.js';
18
+ import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
19
+ import { LiveDriverSwitch } from './live-switch.js';
20
+ import { ClaudeModelCatalog } from './model-catalog.js';
21
+ import { ClaudeModelBroker, ClaudeScopedModelMemory } from './model-broker.js';
22
+ import { claudeAuthenticated, claudeCredentialStamp, listClaudeSessions, readClaudeTranscriptModel, syncClaudeCredentialFile } from './records.js';
23
+ import { claudeLogout, provisionClaudeHome, readClaudeConfiguredModel } from './provision.js';
24
+ /** Stable Cordis plugin name; the bundle patch row id. */
25
+ export const name = 'local-agent-claude-code';
26
+ /** Services required before the harness can register. */
27
+ export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
28
+ /**
29
+ * The settings fields (`live`, `liveMirrorGranularity`, `model`,
30
+ * `recentModels`) ride the plugin Config, marked `.volatile()` where the
31
+ * host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
32
+ * there the SettingsForms writes hot-track the running fiber's Volatile
33
+ * references without a remount, and the official one-shot import moves an old
34
+ * settings.yaml section into this row's config. On 0.1.5 the call is absent,
35
+ * the fields stay plain config keys (a semantic superset — the profile row
36
+ * can now preset them too), and the legacy settings namespace below carries
37
+ * the user layer exactly as before. `model` deliberately carries NO default:
38
+ * an unset key must resolve to undefined, which is what keeps the pre-key
39
+ * behavior byte-identical.
40
+ */
41
+ const SETTINGS_FIELDS = {
42
+ live: z.boolean().default(false),
43
+ liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
44
+ model: z.string(),
45
+ recentModels: z.array(z.string()).default([]),
46
+ };
47
+ /** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
48
+ function volatilize(field) {
49
+ return typeof field.volatile === 'function' ? field.volatile() : field;
50
+ }
51
+ // Bare `z` annotation: `z<Config>` fails schemastery 3.18.4's variance under
52
+ // exactOptionalPropertyTypes (TS2375) and dropping the annotation trips
53
+ // TS2742; the interface stays the apply signature's contract.
54
+ export const Config = z.object({
55
+ permissionMode: z.union([
56
+ z.const('skip'),
57
+ z.const('normal'),
58
+ ]),
59
+ model: volatilize(SETTINGS_FIELDS.model),
60
+ baseUrl: z.string(),
61
+ proxyUrl: z.string(),
62
+ live: volatilize(SETTINGS_FIELDS.live),
63
+ liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
64
+ liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
65
+ recentModels: volatilize(SETTINGS_FIELDS.recentModels),
66
+ });
67
+ /** The permission mode a fresh delegation defaults to. */
68
+ export const DEFAULT_PERMISSION_MODE = 'skip';
69
+ /**
70
+ * Settings namespace for the settings-page card. The Cordis config feeds the
71
+ * composition `base` layer, so a field absent from the user layer inherits the
72
+ * YAML value — the card only ever stores deliberate overrides.
73
+ */
74
+ export const CLAUDE_SETTINGS_NAMESPACE = 'local-agent-claude-code';
75
+ /**
76
+ * The card's 0.1.5 schema; field defaults are the innermost layer below
77
+ * `base`. Shared with the Config schema's settings fields above.
78
+ */
79
+ const CLAUDE_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
80
+ /**
81
+ * Register the Claude Code harness into the local-agent registry.
82
+ * @param ctx - plugin context carrying the registry.
83
+ * @param config - plugin config; `permissionMode` selects the `claude -p` policy.
84
+ */
85
+ export function apply(ctx, config) {
86
+ ctx.effect(() => {
87
+ const permissionMode = config.permissionMode ?? DEFAULT_PERMISSION_MODE;
88
+ const baseUrl = config.baseUrl;
89
+ const homeDir = ctx.localAgent.homeDir('claude-code');
90
+ // Claude creates the scoped home lazily; create it eagerly so the
91
+ // harness's homeDir contract is uniform with the other harnesses.
92
+ void provisionClaudeHome(homeDir, config.proxyUrl).catch((error) => {
93
+ ctx.logger.warn(`local-agent-claude-code: scoped home provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
94
+ });
95
+ // The live driver is settings-driven: the settings card's toggle (user
96
+ // layer over the YAML composition base) swaps driver generations without
97
+ // a reload. Toggling OFF drains the retiring generation — new rounds fall
98
+ // back to exec, in-flight rounds finish on their runtime, idle runtimes
99
+ // are reclaimed at once. Legacy granularity settings are accepted but ignored.
100
+ // The settings face hides the two host lines: 0.1.5 serves the legacy
101
+ // namespace scope (register + base); rc.1 reads the row config's volatile
102
+ // fields and re-reads on `settings/document-updated`. Both hot-apply the
103
+ // card's writes without a reload.
104
+ const readSettings = () => {
105
+ const model = vol(config.model);
106
+ const recentModels = vol(config.recentModels);
107
+ return {
108
+ live: vol(config.live) ?? false,
109
+ liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
110
+ ...model === undefined ? {} : { model },
111
+ ...recentModels === undefined ? {} : { recentModels },
112
+ };
113
+ };
114
+ const legacyBase = {};
115
+ for (const field of ['live', 'liveMirrorGranularity', 'model']) {
116
+ const value = vol(config[field]);
117
+ if (value !== undefined)
118
+ legacyBase[field] = value;
119
+ }
120
+ const face = settingsFace(ctx, {
121
+ namespace: CLAUDE_SETTINGS_NAMESPACE,
122
+ legacySchema: CLAUDE_SETTINGS_SCHEMA,
123
+ legacyBase,
124
+ read: readSettings,
125
+ });
126
+ // The model is read PER ROUND, not captured at apply: the settings card
127
+ // writes the same namespace field, so a change has to reach the next
128
+ // delegation without a plugin reload — exactly like the live toggle.
129
+ // Blank is not a model: a whitespace-only value means "unset", which is
130
+ // the pre-key argv.
131
+ const resolveModel = () => {
132
+ const model = face.get().model?.trim();
133
+ return model === undefined || model === '' ? undefined : model;
134
+ };
135
+ // The member model layers above the settings key: the session-level
136
+ // override map (the composer's picker writes it through the broker; both
137
+ // round paths and the live spawn read it) and the scoped-settings.json
138
+ // scratch memory (a live spawn writes the member's effective model into
139
+ // the file because a --resume respawn honors it over the --model flag;
140
+ // the memory keeps the person's own configured value straight across
141
+ // those writes).
142
+ const overrides = new Map();
143
+ const scopedModelMemory = new ClaudeScopedModelMemory();
144
+ // The member's effective model in the family's fixed order: override,
145
+ // then the round's delegation model, then the settings key. The scoped
146
+ // file and the CLI's built-in default decide below that, as they always
147
+ // did — they need no argv representation.
148
+ const effectiveModel = (childSessionId, delegationModel) => {
149
+ const own = overrides.get(childSessionId)?.trim();
150
+ if (own !== undefined && own !== '')
151
+ return own;
152
+ const named = delegationModel?.trim();
153
+ if (named !== undefined && named !== '')
154
+ return named;
155
+ return resolveModel();
156
+ };
157
+ const liveSwitch = new LiveDriverSwitch(ctx, face, {
158
+ ...config.permissionMode === undefined ? {} : { permissionMode: config.permissionMode },
159
+ ...config.baseUrl === undefined ? {} : { baseUrl: config.baseUrl },
160
+ ...config.liveIdleMs === undefined ? {} : { liveIdleMs: config.liveIdleMs },
161
+ model: effectiveModel,
162
+ provisionModel: (home, model) => scopedModelMemory.provision(home, model),
163
+ });
164
+ const modelCatalog = new ClaudeModelCatalog({
165
+ spawn: spec => ctx.subprocess.spawn(spec),
166
+ ...config.baseUrl === undefined ? {} : { baseUrl: config.baseUrl },
167
+ warn: message => ctx.logger.warn(message),
168
+ });
169
+ const modelContext = (childSessionId) => {
170
+ const record = childSessionId === undefined ? undefined : ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId);
171
+ return { home: ctx.localAgent.homeDir('claude-code', record?.scope), cwd: record?.cwd };
172
+ };
173
+ const modelBroker = new ClaudeModelBroker({
174
+ localAgent: ctx.localAgent,
175
+ configurationContext: binding => {
176
+ const home = ctx.localAgent.homeDir('claude-code', binding.scope);
177
+ return { home, directory: refresh => refresh ? modelCatalog.refresh(home, binding.cwd) : modelCatalog.read(home, binding.cwd) };
178
+ },
179
+ settingsModel: resolveModel,
180
+ cliDefault: childSessionId => {
181
+ const context = modelContext(childSessionId);
182
+ return context.home === homeDir ? scopedModelMemory.cliDefault(homeDir) : readClaudeConfiguredModel(context.home);
183
+ },
184
+ directory: (childSessionId, refresh) => {
185
+ const context = modelContext(childSessionId);
186
+ return refresh ? modelCatalog.refresh(context.home, context.cwd) : modelCatalog.read(context.home, context.cwd);
187
+ },
188
+ followDirectory: (childSessionId, signal) => {
189
+ const context = modelContext(childSessionId);
190
+ return modelCatalog.follow(context.home, context.cwd, signal);
191
+ },
192
+ recentModels: () => face.get().recentModels ?? [],
193
+ live: () => face.get().live,
194
+ overrides,
195
+ liveSwitch,
196
+ // The lastObserved backstop: the member's own transcript (or the
197
+ // tree's newest) answers for every round that predates the
198
+ // live-settle report.
199
+ transcriptModel: (cliSessionId, childSessionId) => readClaudeTranscriptModel(modelContext(childSessionId).home, cliSessionId),
200
+ });
201
+ const disposeProvider = ctx.subagents.registerProvider(new ClaudeCliProvider(ctx, permissionMode, baseUrl, liveSwitch.resolve, effectiveModel));
202
+ const disposeHarness = ctx.localAgent.register({
203
+ prepareMember: async ({ binding, childSession, configuration, signal }) => {
204
+ const driver = liveSwitch.resolve(binding.childSessionId);
205
+ if (driver === undefined || driver.disabled)
206
+ throw new Error('Enable the native live harness before preparing a coordinator');
207
+ await driver.prepare({ cwd: binding.cwd, homeDir: ctx.localAgent.homeDir('claude-code', binding.scope), childSession,
208
+ parentSessionId: binding.parentSessionId, configuration: configuration.resolved, }, signal);
209
+ },
210
+ name: 'claude-code',
211
+ displayName: 'Claude Code',
212
+ homeEnvVar: 'CLAUDE_CONFIG_DIR',
213
+ delegationProvider: 'claude-local',
214
+ login: {
215
+ // claude ≥2.1 (verified 2.1.235+) runs its auth only on a TTY: under
216
+ // the pty wrapper it auto-opens the user's browser and prints the
217
+ // OAuth URL as fallback; the page hands back a code the user pastes
218
+ // via /claude-code code <value>. The relay env is scrubbed and the
219
+ // scoped home pinned so the credential lands where delegations read
220
+ // it (the wrapper passes the harness env through).
221
+ // The argv pins the config directory itself (an `env NAME=VALUE`
222
+ // assignment outranks the spawn env), so it is built from the
223
+ // directory being logged in: `/claude-code login --scope <name>`
224
+ // authorizes THAT scope, not the default one.
225
+ pty: {
226
+ command: 'env',
227
+ args: (scopedHome) => ['-u', 'ANTHROPIC_API_KEY', '-u', 'ANTHROPIC_BASE_URL', `CLAUDE_CONFIG_DIR=${scopedHome}`, 'claude', 'auth', 'login'],
228
+ },
229
+ // The watch syncs the keychain credential into the runtime-readable
230
+ // file first (claude 2.1.236 writes keychain but reads the file),
231
+ // then probes.
232
+ watch: async (home) => {
233
+ await syncClaudeCredentialFile(home, message => { ctx.logger.warn(message); });
234
+ return claudeAuthenticated(home, message => { ctx.logger.warn(message); });
235
+ },
236
+ },
237
+ records: { listSessions: homeDir => listClaudeSessions(homeDir) },
238
+ // A NAMED scope's directory is provisioned through this hook when the
239
+ // registry materializes it — the same eager creation and settings the
240
+ // default scope gets from the apply above.
241
+ provision: scopedHome => provisionClaudeHome(scopedHome, config.proxyUrl).then(() => { }),
242
+ // The logger is wired here because this probe is the clobber site the
243
+ // spawn sites do not cover: every status read and every readiness check
244
+ // reconciles the two credential stores through it.
245
+ isAuthenticated: home => claudeAuthenticated(home, message => { ctx.logger.warn(message); }),
246
+ credentialStamp: claudeCredentialStamp,
247
+ logout: claudeLogout,
248
+ // The model surface: the settings card reads the memberless info ("what
249
+ // would a round run with"), the member composer reads and switches a
250
+ // member's session-level override through the same broker.
251
+ modelBroker,
252
+ // The eval snapshot: the permission mode is the plugin config resolved
253
+ // at apply (it selects the spawn flags); the endpoint mirrors the
254
+ // provider's own resolution order — the config item wins over the host
255
+ // process environment's ANTHROPIC_BASE_URL. The model follows the
256
+ // family's fixed order: the plugin config key (it rides every argv)
257
+ // before the scoped settings.json's own `model`. The CLI's own default
258
+ // model is never guessed, so neither one configured means no field.
259
+ // The directory is a PARAMETER, not the apply-time capture: a status
260
+ // read (or an evaluation snapshot) of a named scope must report the
261
+ // settings that scope's rounds would run with.
262
+ effectiveSettings: async (scopedHome) => {
263
+ const effectiveBaseUrl = baseUrl ?? process.env.ANTHROPIC_BASE_URL;
264
+ const baseUrlHost = effectiveBaseUrl !== undefined ? endpointHost(effectiveBaseUrl) : undefined;
265
+ const [scopedModel, cliVersion] = await Promise.all([
266
+ readClaudeConfiguredModel(scopedHome).catch(() => undefined),
267
+ claudeCliVersion(ctx, scopedHome).catch(() => undefined),
268
+ ]);
269
+ const model = resolveModel() ?? scopedModel;
270
+ return {
271
+ drive: face.get().live ? 'live' : 'exec',
272
+ permissionMode,
273
+ baseUrlSet: effectiveBaseUrl !== undefined,
274
+ ...baseUrlHost !== undefined ? { baseUrlHost } : {},
275
+ ...model !== undefined ? { model } : {},
276
+ ...cliVersion !== undefined ? { cliVersion } : {},
277
+ };
278
+ },
279
+ subcommand: (input, invocation) => {
280
+ const [verb] = input.split(/\s+/);
281
+ if (verb === 'sessions') {
282
+ return listClaudeSessions(invocation.agent.ctx.localAgent.homeDir('claude-code'))
283
+ .then(records => ({
284
+ kind: 'success',
285
+ text: records.length === 0
286
+ ? 'No claude sessions in the scoped home yet.'
287
+ : records.map(r => `${r.id} ${r.workDir}`).join('\n'),
288
+ }));
289
+ }
290
+ return undefined;
291
+ },
292
+ });
293
+ return () => {
294
+ modelCatalog.dispose();
295
+ disposeProvider();
296
+ disposeHarness();
297
+ liveSwitch.dispose();
298
+ };
299
+ }, 'local-agent-claude-code: harness');
300
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-claude-code`.
3
+ * @module @khorsheed/dsh-local-agent-claude-code/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "local-agent-claude-code-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - plugin context carrying the invariant registry.
13
+ * @returns the installed registration's disposer.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-claude-code`.
3
+ * @module @khorsheed/dsh-local-agent-claude-code/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-local-agent-claude-code';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'local-agent-claude-code-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * The claude-code harness definition is this bundle's contract with the core
12
+ * registry: whenever a `claude-code` harness registers, its scoped-home
13
+ * variable must be the one this bundle pins.
14
+ */
15
+ const install = (ctx, fail) => {
16
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
17
+ if (eventName !== 'localAgent/harness-added')
18
+ return;
19
+ const harness = args[0];
20
+ if (harness.name !== 'claude-code')
21
+ return;
22
+ if (harness.homeEnvVar !== 'CLAUDE_CONFIG_DIR') {
23
+ fail('claude-code harness registered with a home env other than the local-agent-claude-code contract');
24
+ }
25
+ }, { global: true });
26
+ };
27
+ /**
28
+ * Register this package's invariant companion.
29
+ * @param ctx - plugin context carrying the invariant registry.
30
+ * @returns the installed registration's disposer.
31
+ */
32
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
33
+ /* jscpd:ignore-end */
@@ -0,0 +1,172 @@
1
+ import type { LocalAgentResolvedConfiguration } from '@khorsheed/dsh-local-agent/types';
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { Session } from '@deepseek-ai/dsh-session';
4
+ import { type SubagentRun, type SubagentStartRequest } from '@deepseek-ai/dsh-subagent';
5
+ import type { Config } from './index.ts';
6
+ /** Default idle lifetime of an unused resident runtime before reclaim. */
7
+ export declare const DEFAULT_LIVE_IDLE_MS: number;
8
+ /** Default timeout for the turn's system/init ack (a cold boot can be slow). */
9
+ export declare const DEFAULT_LIVE_INIT_TIMEOUT_MS = 60000;
10
+ /** Bounded wait for an interrupted turn to converge while a run disposes. */
11
+ export declare const DEFAULT_LIVE_DISPOSE_CONVERGE_MS = 5000;
12
+ /** How long a tripped channel breaker stays on the exec fallback. */
13
+ export declare const DEFAULT_LIVE_CHANNEL_RETRY_MS: number;
14
+ /** Default minimum interval between one streaming kind's snapshot messages. */
15
+ export declare const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 50;
16
+ /** Default minimum text growth between one streaming kind's snapshot messages. */
17
+ export declare const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
18
+ /**
19
+ * The stream-json channel could not come up (spawn failure or the first
20
+ * turn's init never arrived). The provider catches exactly this and falls
21
+ * back to the exec one-shot; any other error is a real round failure.
22
+ */
23
+ export declare class LiveChannelUnavailableError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /** Internal timeouts; tests inject small values. */
27
+ export interface ClaudeLiveDriverTimeouts {
28
+ readonly initMs: number;
29
+ readonly convergeMs: number;
30
+ readonly channelRetryMs: number;
31
+ }
32
+ /** How much of the live event stream crosses into the child session. */
33
+ export type ClaudeLiveMirrorGranularity = 'event' | 'token';
34
+ /** Fully resolved inputs for one live round. */
35
+ export interface ClaudeLiveRoundSpec {
36
+ /** Parent Session workspace; also the runtime process cwd. */
37
+ readonly cwd: string;
38
+ /** The `claude-code` harness's scoped home, injected as `CLAUDE_CONFIG_DIR`. */
39
+ readonly homeDir: string;
40
+ /** dsh subagent session recording this delegation (always live on this path). */
41
+ readonly childSession: Session;
42
+ /** The delegating session (member-channel registration). */
43
+ readonly parentSessionId: string;
44
+ /**
45
+ * Resume round: continue the session recorded as `cliSessionId` (also the
46
+ * reattach path after a runtime crash — the respawn passes `--resume`).
47
+ */
48
+ readonly resume?: {
49
+ readonly cliSessionId: string;
50
+ readonly turn: number;
51
+ } | undefined;
52
+ /**
53
+ * The delegation layer's model: the model this DELEGATION requested on its
54
+ * first round, re-requested by every resume round from the record. The
55
+ * spawn binds it when no session-level override outranks it — a delegation
56
+ * naming a model is no longer exec-only; its model binds at spawn like any
57
+ * other layer.
58
+ */
59
+ readonly model?: string | undefined;
60
+ readonly configuration?: LocalAgentResolvedConfiguration;
61
+ /** Fresh round: called with the session id from the turn's system/init. */
62
+ readonly onSessionId?: ((sessionId: string) => void) | undefined;
63
+ }
64
+ /**
65
+ * The claude live driver owns every resident stream-json runtime of this
66
+ * provider generation. Created only when `live: true`; disposed with the
67
+ * plugin.
68
+ */
69
+ export declare class ClaudeLiveDriver {
70
+ private readonly ctx;
71
+ private readonly config;
72
+ private readonly timeouts;
73
+ private readonly runtimes;
74
+ private readonly ensuring;
75
+ private readonly idleTimers;
76
+ /** Per-member round serialization (the resume lock covers resume-vs-resume only). */
77
+ private readonly roundChains;
78
+ private channelBrokenAt;
79
+ private disposed;
80
+ /**
81
+ * Set by drain() (a settings-driven generation handoff): new rounds are
82
+ * refused so the provider falls back to exec, while in-flight rounds finish
83
+ * on their runtime undisturbed. Unlike `disposed`, the driver still serves
84
+ * what it already accepted.
85
+ */
86
+ private draining;
87
+ private readonly disposeController;
88
+ constructor(ctx: Context, config: Pick<Config, 'permissionMode' | 'baseUrl'> & {
89
+ liveIdleMs?: number;
90
+ liveMirrorGranularity?: ClaudeLiveMirrorGranularity;
91
+ /** Maximum batching wait for incremental streaming messages. */
92
+ snapshotMinIntervalMs?: number;
93
+ /** @deprecated Character growth no longer gates live publication. */
94
+ snapshotMinChars?: number;
95
+ /**
96
+ * Resolver for the member's effective model, read at each RUNTIME SPAWN
97
+ * (the resident process is where a live round's CLI starts). Receives
98
+ * the member and the round's delegation-layer model (the delegation's
99
+ * own request, re-requested on resume from the record) and answers in
100
+ * the family's order — session-level override, delegation, settings.
101
+ * Absent, or resolving to nothing, leaves the spawn argv unchanged.
102
+ */
103
+ model?: (childSessionId: string, delegationModel?: string) => string | undefined;
104
+ /**
105
+ * Scratch the spawn's effective model into the scoped settings.json
106
+ * before the process starts (a `--resume` respawn restores the
107
+ * session's stored model over the `--model` flag). index.ts wires the
108
+ * shared {@link ClaudeScopedModelMemory}; absent, the file is untouched.
109
+ */
110
+ provisionModel?: (homeDir: string, model: string | undefined) => Promise<void>;
111
+ }, timeouts?: ClaudeLiveDriverTimeouts);
112
+ /** Whether the channel is in its post-failure cooldown (rounds fall back to exec). */
113
+ get disabled(): boolean;
114
+ /** Count of resident runtimes currently registered (zombie accounting in tests). */
115
+ get liveCount(): number;
116
+ /**
117
+ * True while the member's runtime exists or is being spawned. The settings
118
+ * controller gates a new generation on this: a fresh driver must not serve
119
+ * a member whose retiring generation still hosts the (same) claude session.
120
+ */
121
+ hasRuntime(key: string): boolean;
122
+ /** @deprecated Compatibility no-op: live output is always incremental. */
123
+ setLiveMirrorGranularity(_granularity: ClaudeLiveMirrorGranularity): void;
124
+ /**
125
+ * Drain for a settings-driven generation handoff: refuse new rounds (the
126
+ * provider's catch falls back to exec), let every in-flight round finish on
127
+ * its runtime, then reclaim. Resolves when no runtime or spawn remains.
128
+ * Unlike disposeAll, in-flight work is never interrupted.
129
+ */
130
+ drain(): Promise<void>;
131
+ private clearIdleTimer;
132
+ private armIdleTimer;
133
+ private reclaim;
134
+ /** Reclaim every runtime (plugin unload); no process survives the profile. */
135
+ disposeAll(): Promise<void>;
136
+ private ensureRuntime;
137
+ /** The model a spawn for this round would bind (the argv `--model` value). */
138
+ private spawnModel;
139
+ /**
140
+ * The model the member's resident runtime was spawned with, or undefined
141
+ * when the member has no live runtime (or it binds none). The model broker
142
+ * compares this against a switch's new effective model.
143
+ */
144
+ runtimeModel(childSessionId: string): string | undefined;
145
+ /**
146
+ * Retire the member's resident runtime so the next round respawns with its
147
+ * new effective model (the model broker's switch path). The caller refuses
148
+ * switches while a round is in flight, so the reclaim never kills a run;
149
+ * the CLI session survives on disk and the respawn resumes it.
150
+ */
151
+ retireRuntime(childSessionId: string): Promise<void>;
152
+ /**
153
+ * Spawn the member's resident stream-json process. There is no handshake
154
+ * message — the channel proves itself with the first turn's system/init —
155
+ * so spawn failure is the only breaker trip here; the init wait lives in
156
+ * the round's accept window.
157
+ */
158
+ private spawnRuntime;
159
+ private markChannelBroken;
160
+ /**
161
+ * Drive one delegation round on the member's resident stream-json process.
162
+ * Mirrors the exec run's settlement contract exactly (settleRunResult +
163
+ * turn/end bookkeeping); cancel is the interrupt control request and the
164
+ * process survives.
165
+ */
166
+ /** Initialize native controls without sending a user prompt. Native history starts on its first real turn. */
167
+ prepare(spec: ClaudeLiveRoundSpec, signal: AbortSignal): Promise<void>;
168
+ startRound(request: SubagentStartRequest, spec: ClaudeLiveRoundSpec): Promise<SubagentRun>;
169
+ /** The serialized round body. */
170
+ private startRoundLocked;
171
+ }
172
+ //# sourceMappingURL=live-driver.d.ts.map