@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.
- package/README.en.md +120 -0
- package/README.i18n.yaml +6 -0
- package/README.md +120 -0
- package/cordis.patch.yml +42 -0
- package/lib/client.js +1065 -0
- package/lib/index.js +3771 -0
- package/lib/invariant.js +31 -0
- package/lib/types/claude-cli-provider.d.ts +389 -0
- package/lib/types/claude-cli-provider.js +1509 -0
- package/lib/types/client/SettingsCard.d.ts +78 -0
- package/lib/types/client/SettingsCard.js +198 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/index.js +87 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/control-requests.d.ts +15 -0
- package/lib/types/control-requests.js +62 -0
- package/lib/types/index.d.ts +81 -0
- package/lib/types/index.js +300 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/live-driver.d.ts +172 -0
- package/lib/types/live-driver.js +1057 -0
- package/lib/types/live-switch.d.ts +67 -0
- package/lib/types/live-switch.js +85 -0
- package/lib/types/model-broker.d.ts +131 -0
- package/lib/types/model-broker.js +248 -0
- package/lib/types/model-catalog.d.ts +27 -0
- package/lib/types/model-catalog.js +118 -0
- package/lib/types/provision.d.ts +63 -0
- package/lib/types/provision.js +132 -0
- package/lib/types/records.d.ts +120 -0
- package/lib/types/records.js +687 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- 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
|