@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,118 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
import { ModelDirectoryCache, delegationEnv, modelDirectoryContextKey } from '@khorsheed/dsh-local-agent';
|
|
4
|
+
import { ClaudeControlRequests } from './control-requests.js';
|
|
5
|
+
/** Initialize's native picker vocabulary, not an account entitlement claim. */
|
|
6
|
+
export function claudeDirectory(result) {
|
|
7
|
+
const models = result['models'];
|
|
8
|
+
if (models === undefined)
|
|
9
|
+
return { entries: [], complete: false, customInput: true, unsupported: true, reason: 'This Claude CLI does not expose a native model directory' };
|
|
10
|
+
if (!Array.isArray(models))
|
|
11
|
+
throw new Error('Claude initialize returned an invalid model directory');
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
for (const candidate of models) {
|
|
14
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
15
|
+
continue;
|
|
16
|
+
const model = candidate;
|
|
17
|
+
const value = typeof model['value'] === 'string' ? model['value'].trim() : '';
|
|
18
|
+
if (value === '' || entries.has(value))
|
|
19
|
+
continue;
|
|
20
|
+
const levels = model['supportedEffortLevels'];
|
|
21
|
+
const options = Array.isArray(levels) ? levels.flatMap(level => typeof level === 'string' && level !== '' ? [{ value: level, label: level }] : []) : undefined;
|
|
22
|
+
entries.set(value, {
|
|
23
|
+
value, label: typeof model['displayName'] === 'string' ? model['displayName'] : value, source: 'native',
|
|
24
|
+
...typeof model['resolvedModel'] === 'string' ? { resolvedModel: model['resolvedModel'] } : {},
|
|
25
|
+
...typeof model['description'] === 'string' ? { description: model['description'] } : {},
|
|
26
|
+
...options === undefined ? {} : { reasoning: { options } },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return { entries: [...entries.values()], complete: true, customInput: true, ...entries.has('default') ? { defaultModel: 'default' } : {} };
|
|
30
|
+
}
|
|
31
|
+
/** Scoped native control probe. Never syncs credentials, writes settings or submits a user turn. */
|
|
32
|
+
export class ClaudeModelCatalog {
|
|
33
|
+
deps;
|
|
34
|
+
cache;
|
|
35
|
+
constructor(deps) {
|
|
36
|
+
this.deps = deps;
|
|
37
|
+
this.cache = new ModelDirectoryCache({
|
|
38
|
+
load: (key, signal) => {
|
|
39
|
+
const context = JSON.parse(key);
|
|
40
|
+
return this.probe(context.homeDir, context.cwd, signal);
|
|
41
|
+
},
|
|
42
|
+
...deps.ttlMs === undefined ? {} : { ttlMs: deps.ttlMs },
|
|
43
|
+
onError: error => deps.warn?.(`Claude model discovery failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
key(homeDir, cwd = homeDir) {
|
|
47
|
+
const files = [join(homeDir, 'settings.json'), join(homeDir, '.credentials.json'), join(homeDir, '.claude.json')];
|
|
48
|
+
for (let directory = cwd;; directory = dirname(directory)) {
|
|
49
|
+
files.push(join(directory, '.claude', 'settings.json'), join(directory, '.claude', 'settings.local.json'));
|
|
50
|
+
if (dirname(directory) === directory)
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return modelDirectoryContextKey({ provider: 'claude-code', homeDir, cwd, cli: [this.deps.cli ?? 'claude'], files, ...this.deps.baseUrl === undefined ? {} : { endpoint: this.deps.baseUrl } });
|
|
54
|
+
}
|
|
55
|
+
read(homeDir, cwd) { return this.cache.read(this.key(homeDir, cwd)); }
|
|
56
|
+
refresh(homeDir, cwd) { return this.cache.refresh(this.key(homeDir, cwd)); }
|
|
57
|
+
follow(homeDir, cwd, signal) { return this.cache.follow(() => this.key(homeDir, cwd), signal); }
|
|
58
|
+
invalidate(homeDir, cwd) { this.cache.invalidate(this.key(homeDir, cwd)); }
|
|
59
|
+
dispose() { this.cache.dispose(); }
|
|
60
|
+
async probe(homeDir, cwd, signal) {
|
|
61
|
+
const child = this.deps.spawn({
|
|
62
|
+
argv: [this.deps.cli ?? 'claude', '-p', '--verbose', '--input-format', 'stream-json', '--output-format', 'stream-json',
|
|
63
|
+
'--no-session-persistence', '--settings', '{"disableAllHooks":true}', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--'],
|
|
64
|
+
cwd, stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' }, graceMs: 2_000,
|
|
65
|
+
env: delegationEnv({ CLAUDE_CONFIG_DIR: homeDir, ...this.deps.baseUrl === undefined ? {} : { ANTHROPIC_BASE_URL: this.deps.baseUrl } }),
|
|
66
|
+
});
|
|
67
|
+
const controls = new ClaudeControlRequests(frame => {
|
|
68
|
+
if (child.stdin === undefined)
|
|
69
|
+
throw new Error('Claude discovery stdin is unavailable');
|
|
70
|
+
child.stdin.write(JSON.stringify(frame) + '\n');
|
|
71
|
+
}, this.deps.timeoutMs ?? 5_000);
|
|
72
|
+
const decoder = new StringDecoder('utf8');
|
|
73
|
+
let buffer = '';
|
|
74
|
+
child.stdout?.on('data', (chunk) => {
|
|
75
|
+
buffer += decoder.write(chunk);
|
|
76
|
+
if (buffer.length > 1_048_576) {
|
|
77
|
+
controls.close();
|
|
78
|
+
child.terminate();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let index;
|
|
82
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
83
|
+
const line = buffer.slice(0, index);
|
|
84
|
+
buffer = buffer.slice(index + 1);
|
|
85
|
+
let event;
|
|
86
|
+
try {
|
|
87
|
+
event = JSON.parse(line);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (controls.accept(event))
|
|
93
|
+
continue;
|
|
94
|
+
if (event['type'] === 'control_request' && typeof event['request_id'] === 'string') {
|
|
95
|
+
child.stdin?.write(JSON.stringify({ type: 'control_response', response: { subtype: 'error', request_id: event['request_id'], error: 'Model discovery does not execute tools' } }) + '\n');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
child.stderr?.on('data', () => { });
|
|
100
|
+
void child.done.then(() => controls.close(), () => controls.close());
|
|
101
|
+
let cleanupTimer;
|
|
102
|
+
try {
|
|
103
|
+
return claudeDirectory(await controls.request({ subtype: 'initialize', hooks: {} }, signal));
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
controls.close();
|
|
107
|
+
child.stdin?.end();
|
|
108
|
+
await Promise.race([child.done.catch(() => { }), new Promise(resolve => {
|
|
109
|
+
cleanupTimer = setTimeout(resolve, 1_000);
|
|
110
|
+
cleanupTimer.unref();
|
|
111
|
+
})]);
|
|
112
|
+
if (cleanupTimer !== undefined)
|
|
113
|
+
clearTimeout(cleanupTimer);
|
|
114
|
+
child.terminate();
|
|
115
|
+
await child.waitForExit().catch(() => { });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code scoped-home provisioning. Unlike codex (whose credential store
|
|
3
|
+
* must be pinned to a file) and kimi (which needs a provider config),
|
|
4
|
+
* claude's `CLAUDE_CONFIG_DIR` scoped home needs nothing beyond the
|
|
5
|
+
* directory itself: login via `claude auth login` writes `.claude.json`
|
|
6
|
+
* (with the `oauthAccount` record) into it, and the real credential lands in
|
|
7
|
+
* the macOS keychain under a hashed entry or — on Linux, despite upstream
|
|
8
|
+
* bug #47661 — in the default `~/.claude/.credentials.json` while the config
|
|
9
|
+
* stays scoped. Sign-out removes the scoped config so `status` reports not
|
|
10
|
+
* authenticated; the keychain entry on macOS persists until the CLI removes
|
|
11
|
+
* it, which the framework's file-based logout cannot reach.
|
|
12
|
+
* @module @khorsheed/dsh-local-agent-claude-code/provision
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Provision the scoped home directory. Claude creates it lazily on first
|
|
16
|
+
* use, but creating it eagerly keeps the harness's homeDir contract uniform
|
|
17
|
+
* with the other harnesses. When `proxyUrl` is configured, the scoped
|
|
18
|
+
* `settings.json` gains an env block pointing the CLI's own traffic at it —
|
|
19
|
+
* claude reads proxy settings from this file, so the child works even when
|
|
20
|
+
* the host process environment carries no proxy (a supervisor-spawned
|
|
21
|
+
* instance's env is not the user's shell env). An existing settings file is
|
|
22
|
+
* merged, not overwritten; only the two proxy keys are managed.
|
|
23
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
24
|
+
* @param proxyUrl - optional HTTP proxy URL for the child CLI's traffic.
|
|
25
|
+
*/
|
|
26
|
+
export declare function provisionClaudeHome(homeDir: string, proxyUrl?: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Read the scoped settings' configured model: the `model` key of
|
|
29
|
+
* `<homeDir>/settings.json` — the model a delegation round pins when the
|
|
30
|
+
* scoped settings name one at all. Claude's own default model is decided by
|
|
31
|
+
* the CLI and is deliberately NOT guessed here. Only the scoped file is
|
|
32
|
+
* consulted (never the host's `~/.claude`); absent or malformed content
|
|
33
|
+
* yields undefined.
|
|
34
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
35
|
+
* @returns the configured model identifier, or undefined when none is set.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readClaudeConfiguredModel(homeDir: string): Promise<string | undefined>;
|
|
38
|
+
/** Read the scoped native effort setting without changing the settings file. */
|
|
39
|
+
export declare function readClaudeConfiguredEffort(homeDir: string): Promise<string | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Write (or, with undefined, remove) the scoped settings.json `model` key,
|
|
42
|
+
* merging the rest of the file exactly as {@link provisionClaudeHome} merges
|
|
43
|
+
* its env block. Only the live driver calls this: a resident runtime needs
|
|
44
|
+
* its member's effective model IN THE FILE, because a `--resume` respawn
|
|
45
|
+
* restores the session's own stored model over the `--model` flag. The exec
|
|
46
|
+
* path never calls it — its `--model` flag always applies, and the file
|
|
47
|
+
* stays exactly as the person edited it.
|
|
48
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
49
|
+
* @param model - the model to scratch in, or undefined to remove the key.
|
|
50
|
+
*/
|
|
51
|
+
export declare function writeClaudeScopedModel(homeDir: string, model: string | undefined): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Sign out of the scoped account: remove the scoped config file, so
|
|
54
|
+
* `/<name> status` reports not authenticated and the next login authorizes
|
|
55
|
+
* a fresh account. The macOS keychain entry (a hashed
|
|
56
|
+
* `Claude Code-credentials-<sha256(path)[:8]>` record keyed to the scoped
|
|
57
|
+
* home path) is NOT removed — the CLI's `claude auth logout` owns that, and
|
|
58
|
+
* the harness logout contract is file-based; the stale entry is harmless
|
|
59
|
+
* because a fresh login rewrites the same hashed slot.
|
|
60
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
61
|
+
*/
|
|
62
|
+
export declare function claudeLogout(homeDir: string): Promise<void>;
|
|
63
|
+
//# sourceMappingURL=provision.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code scoped-home provisioning. Unlike codex (whose credential store
|
|
3
|
+
* must be pinned to a file) and kimi (which needs a provider config),
|
|
4
|
+
* claude's `CLAUDE_CONFIG_DIR` scoped home needs nothing beyond the
|
|
5
|
+
* directory itself: login via `claude auth login` writes `.claude.json`
|
|
6
|
+
* (with the `oauthAccount` record) into it, and the real credential lands in
|
|
7
|
+
* the macOS keychain under a hashed entry or — on Linux, despite upstream
|
|
8
|
+
* bug #47661 — in the default `~/.claude/.credentials.json` while the config
|
|
9
|
+
* stays scoped. Sign-out removes the scoped config so `status` reports not
|
|
10
|
+
* authenticated; the keychain entry on macOS persists until the CLI removes
|
|
11
|
+
* it, which the framework's file-based logout cannot reach.
|
|
12
|
+
* @module @khorsheed/dsh-local-agent-claude-code/provision
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/** The scoped config file holding the oauthAccount auth marker. */
|
|
17
|
+
const SCOPED_CONFIG = '.claude.json';
|
|
18
|
+
/** The scoped settings file carrying the CLI's env block. */
|
|
19
|
+
const SCOPED_SETTINGS = 'settings.json';
|
|
20
|
+
/**
|
|
21
|
+
* Provision the scoped home directory. Claude creates it lazily on first
|
|
22
|
+
* use, but creating it eagerly keeps the harness's homeDir contract uniform
|
|
23
|
+
* with the other harnesses. When `proxyUrl` is configured, the scoped
|
|
24
|
+
* `settings.json` gains an env block pointing the CLI's own traffic at it —
|
|
25
|
+
* claude reads proxy settings from this file, so the child works even when
|
|
26
|
+
* the host process environment carries no proxy (a supervisor-spawned
|
|
27
|
+
* instance's env is not the user's shell env). An existing settings file is
|
|
28
|
+
* merged, not overwritten; only the two proxy keys are managed.
|
|
29
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
30
|
+
* @param proxyUrl - optional HTTP proxy URL for the child CLI's traffic.
|
|
31
|
+
*/
|
|
32
|
+
export async function provisionClaudeHome(homeDir, proxyUrl) {
|
|
33
|
+
await mkdir(homeDir, { recursive: true });
|
|
34
|
+
if (proxyUrl === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const path = join(homeDir, SCOPED_SETTINGS);
|
|
37
|
+
let settings = {};
|
|
38
|
+
try {
|
|
39
|
+
settings = JSON.parse(await readFile(path, 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Absent or malformed: start fresh (a malformed file is replaced, not merged).
|
|
43
|
+
}
|
|
44
|
+
const env = (settings.env ?? {});
|
|
45
|
+
if (env.https_proxy === proxyUrl && env.http_proxy === proxyUrl)
|
|
46
|
+
return;
|
|
47
|
+
env.https_proxy = proxyUrl;
|
|
48
|
+
env.http_proxy = proxyUrl;
|
|
49
|
+
settings.env = env;
|
|
50
|
+
await writeFile(path, JSON.stringify(settings, undefined, 2) + '\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read the scoped settings' configured model: the `model` key of
|
|
54
|
+
* `<homeDir>/settings.json` — the model a delegation round pins when the
|
|
55
|
+
* scoped settings name one at all. Claude's own default model is decided by
|
|
56
|
+
* the CLI and is deliberately NOT guessed here. Only the scoped file is
|
|
57
|
+
* consulted (never the host's `~/.claude`); absent or malformed content
|
|
58
|
+
* yields undefined.
|
|
59
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
60
|
+
* @returns the configured model identifier, or undefined when none is set.
|
|
61
|
+
*/
|
|
62
|
+
export async function readClaudeConfiguredModel(homeDir) {
|
|
63
|
+
let text;
|
|
64
|
+
try {
|
|
65
|
+
text = await readFile(join(homeDir, SCOPED_SETTINGS), 'utf8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const settings = JSON.parse(text);
|
|
72
|
+
return typeof settings.model === 'string' && settings.model !== '' ? settings.model : undefined;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Read the scoped native effort setting without changing the settings file. */
|
|
79
|
+
export async function readClaudeConfiguredEffort(homeDir) {
|
|
80
|
+
try {
|
|
81
|
+
const settings = JSON.parse(await readFile(join(homeDir, SCOPED_SETTINGS), 'utf8'));
|
|
82
|
+
return typeof settings.effortLevel === 'string' && settings.effortLevel.trim() !== '' ? settings.effortLevel.trim() : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Write (or, with undefined, remove) the scoped settings.json `model` key,
|
|
90
|
+
* merging the rest of the file exactly as {@link provisionClaudeHome} merges
|
|
91
|
+
* its env block. Only the live driver calls this: a resident runtime needs
|
|
92
|
+
* its member's effective model IN THE FILE, because a `--resume` respawn
|
|
93
|
+
* restores the session's own stored model over the `--model` flag. The exec
|
|
94
|
+
* path never calls it — its `--model` flag always applies, and the file
|
|
95
|
+
* stays exactly as the person edited it.
|
|
96
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
97
|
+
* @param model - the model to scratch in, or undefined to remove the key.
|
|
98
|
+
*/
|
|
99
|
+
export async function writeClaudeScopedModel(homeDir, model) {
|
|
100
|
+
const path = join(homeDir, SCOPED_SETTINGS);
|
|
101
|
+
let settings = {};
|
|
102
|
+
try {
|
|
103
|
+
settings = JSON.parse(await readFile(path, 'utf8'));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Absent or malformed: start fresh (a malformed file is replaced, not merged).
|
|
107
|
+
}
|
|
108
|
+
if (model === undefined) {
|
|
109
|
+
if (!('model' in settings))
|
|
110
|
+
return;
|
|
111
|
+
delete settings['model'];
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
if (settings['model'] === model)
|
|
115
|
+
return;
|
|
116
|
+
settings['model'] = model;
|
|
117
|
+
}
|
|
118
|
+
await writeFile(path, JSON.stringify(settings, undefined, 2) + '\n');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Sign out of the scoped account: remove the scoped config file, so
|
|
122
|
+
* `/<name> status` reports not authenticated and the next login authorizes
|
|
123
|
+
* a fresh account. The macOS keychain entry (a hashed
|
|
124
|
+
* `Claude Code-credentials-<sha256(path)[:8]>` record keyed to the scoped
|
|
125
|
+
* home path) is NOT removed — the CLI's `claude auth logout` owns that, and
|
|
126
|
+
* the harness logout contract is file-based; the stale entry is harmless
|
|
127
|
+
* because a fresh login rewrites the same hashed slot.
|
|
128
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
129
|
+
*/
|
|
130
|
+
export async function claudeLogout(homeDir) {
|
|
131
|
+
await rm(join(homeDir, SCOPED_CONFIG), { force: true });
|
|
132
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code session records adapter: scans the scoped home's `projects/`
|
|
3
|
+
* session files (one JSONL per session under `projects/<cwd-slug>/`, the
|
|
4
|
+
* slug being the workspace path with separators replaced by dashes), plus
|
|
5
|
+
* the transcript model read-back ({@link readClaudeTranscriptModel}) the
|
|
6
|
+
* model broker's `lastObserved` backstop runs on. The
|
|
7
|
+
* slug is a LOSSY encoding of the cwd — different paths can collide on the
|
|
8
|
+
* same slug — so the listed cwd always comes from the file CONTENT (the
|
|
9
|
+
* first `user` event's `cwd` field), never from the directory name. Only the
|
|
10
|
+
* head of each file is read — the first user event carries the id, cwd, and
|
|
11
|
+
* timestamp the listing needs; the rest of the file (conversation) is not
|
|
12
|
+
* parsed. Files are append-only with torn-tail tolerance, and contents are
|
|
13
|
+
* always untrusted input.
|
|
14
|
+
* @module @khorsheed/dsh-local-agent-claude-code/records
|
|
15
|
+
*/
|
|
16
|
+
import { execFile } from 'node:child_process';
|
|
17
|
+
import type { LocalAgentSessionRecord } from '@khorsheed/dsh-local-agent';
|
|
18
|
+
/** Test-swappable process exec (the keychain read goes through `security`). */
|
|
19
|
+
export declare const internals: {
|
|
20
|
+
exec: typeof execFile.__promisify__;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The scoped `.claude.json`'s modification stamp (epoch ms), undefined when
|
|
24
|
+
* absent. A completed login rewrites this file, so the stamp distinguishes a
|
|
25
|
+
* fresh login from a leftover marker (a revoked token's record lingers).
|
|
26
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
27
|
+
* @returns the marker's mtime, or undefined when no scoped config exists.
|
|
28
|
+
*/
|
|
29
|
+
export declare function claudeCredentialStamp(homeDir: string): Promise<number | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Reconcile the keychain credential with `<homeDir>/.credentials.json`,
|
|
32
|
+
* NEWER WINS. Claude 2.1.236 on macOS WRITES the scoped login to the hashed
|
|
33
|
+
* keychain entry but READS the credentials file at runtime (the same
|
|
34
|
+
* write/read split as the Linux #47661 bug) — a login that lands only in the
|
|
35
|
+
* keychain still answers "Not logged in", so the file has to be kept current.
|
|
36
|
+
*
|
|
37
|
+
* The file is not this process's private mirror, though: a containerized
|
|
38
|
+
* round bind-mounts this very directory read-write (the unit has no keychain,
|
|
39
|
+
* so the CLI inside it reads and writes the file), and the pre-spawn sync runs
|
|
40
|
+
* against the HOST path moments before the unit reads it through the mount.
|
|
41
|
+
* An unconditional mirror therefore makes this function the one participant
|
|
42
|
+
* that can move the credential chain BACKWARD: it hands the next run a refresh
|
|
43
|
+
* token the other writer already spent, the endpoint rejects it, and the CLI
|
|
44
|
+
* answers by CLEARING the file — which logs the instance out, because that
|
|
45
|
+
* file is the instance's credential. So the keychain blob is written only when
|
|
46
|
+
* it is not older than the file's. Undefined expiry on either side keeps the
|
|
47
|
+
* pre-newer-wins behavior (the keychain writes), and a file that is not USABLE
|
|
48
|
+
* never wins.
|
|
49
|
+
*
|
|
50
|
+
* Only a USABLE blob (non-empty token, see `readKeychainCredential`) is
|
|
51
|
+
* mirrored; with none in the keychain, a file holding an empty-token shell —
|
|
52
|
+
* debris from an earlier first-match read, or a cleared credential — is
|
|
53
|
+
* REMOVED, so the runtime says "Not logged in" instead of the misleading
|
|
54
|
+
* "OAuth session expired". Idempotent and content-compare before write.
|
|
55
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
56
|
+
* @param log - warn channel for the two decisions that used to be silent.
|
|
57
|
+
* @returns true when a usable credential is in the file after the call.
|
|
58
|
+
*/
|
|
59
|
+
export declare function syncClaudeCredentialFile(homeDir: string, log?: (message: string) => void): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the scoped home holds usable Claude Code credentials: the scoped
|
|
62
|
+
* `.claude.json` carries an `oauthAccount` record once a device-code login
|
|
63
|
+
* has completed, AND the keychain credential is not past its stored expiry
|
|
64
|
+
* (when the expiry is readable — a server-side REVOCATION is not visible
|
|
65
|
+
* locally; providers report those through `localAgent.reportAuthFailure`).
|
|
66
|
+
* The real credential lives in the macOS keychain under a hashed entry
|
|
67
|
+
* (`Claude Code-credentials-<sha256(path)[:8]>`, hashed from the config-dir
|
|
68
|
+
* path) or in the default `~/.claude/.credentials.json` on Linux (upstream
|
|
69
|
+
* bug #47661: `CLAUDE_CONFIG_DIR` does not isolate the credentials file
|
|
70
|
+
* there — it writes the scoped dir but READS the default home, so Linux
|
|
71
|
+
* credentials may exist even when the scoped home has none). Spawning the
|
|
72
|
+
* CLI to probe auth is avoided; the platform differences are documented in
|
|
73
|
+
* the README.
|
|
74
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
75
|
+
* @param log - warn channel handed to the credential reconcile below.
|
|
76
|
+
* @returns true when a completed login is recorded and not locally expired.
|
|
77
|
+
*/
|
|
78
|
+
export declare function claudeAuthenticated(homeDir: string, log?: (message: string) => void): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Parse the head of a session file for the listing fields. The first `user`
|
|
81
|
+
* event carries `sessionId`, `cwd`, and `timestamp`; its `cwd` is the only
|
|
82
|
+
* trustworthy workspace source (the directory slug is lossy). Unknown or
|
|
83
|
+
* malformed values yield an undefined record rather than failing the list.
|
|
84
|
+
* @param text - the raw session file head (only the head is read).
|
|
85
|
+
* @returns the record, or undefined when no user event is found.
|
|
86
|
+
*/
|
|
87
|
+
export declare function projectRecord(text: string): LocalAgentSessionRecord | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Read the scoped home's session files. The listing walks the projects
|
|
90
|
+
* directory and reads each jsonl head; a torn or malformed file is skipped
|
|
91
|
+
* rather than failing the whole list.
|
|
92
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
93
|
+
* @returns the parsed records, empty when no sessions exist yet.
|
|
94
|
+
*/
|
|
95
|
+
export declare function listClaudeSessions(homeDir: string): Promise<readonly LocalAgentSessionRecord[]>;
|
|
96
|
+
/**
|
|
97
|
+
* The model a member's OWN CLI transcript names — the history backstop for
|
|
98
|
+
* the broker's `lastObserved` when no delegation record observed one (every
|
|
99
|
+
* member whose rounds predate the live-settle report). The live-settle
|
|
100
|
+
* channel remains primary (the record's `observedModel` wins when present);
|
|
101
|
+
* this reader only answers "what did this session last run" from the CLI's
|
|
102
|
+
* own files, read-only — nothing is backfilled into `delegations.jsonl`.
|
|
103
|
+
*
|
|
104
|
+
* Location rule: with `cliSessionId`, the main transcript
|
|
105
|
+
* `projects/<cwd-slug>/<cliSessionId>.jsonl` (found by NAME across the
|
|
106
|
+
* project dirs — the slug is lossy, so it is never computed); when it is
|
|
107
|
+
* absent, a sidechain `agent-*.jsonl` whose head names the session id
|
|
108
|
+
* (bounded candidate count) answers instead. Without `cliSessionId` (the
|
|
109
|
+
* settings card's memberless read), the NEWEST transcript across the
|
|
110
|
+
* projects tree (mtime, bounded dir/file scan) answers; a newest file that
|
|
111
|
+
* names no model yields to the next, up to a few tries. Every failure —
|
|
112
|
+
* missing tree, unreadable file, no model in the read window — is
|
|
113
|
+
* `undefined`, never a throw.
|
|
114
|
+
* @param homeDir - the `claude-code` harness's scoped home.
|
|
115
|
+
* @param cliSessionId - the delegation record's CLI session id, when the
|
|
116
|
+
* member's own transcript is wanted.
|
|
117
|
+
* @returns the model identifier, or undefined.
|
|
118
|
+
*/
|
|
119
|
+
export declare function readClaudeTranscriptModel(homeDir: string, cliSessionId?: string): Promise<string | undefined>;
|
|
120
|
+
//# sourceMappingURL=records.d.ts.map
|