@songsid/agend 2.1.4-beta.6 → 2.1.4-beta.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-endpoint.js +1 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/antigravity.d.ts +12 -8
- package/dist/backend/antigravity.js +189 -42
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +87 -23
- package/dist/backend/claude-code.js +361 -46
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +18 -0
- package/dist/backend/codex.js +143 -7
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/gemini-cli.js +2 -2
- package/dist/backend/gemini-cli.js.map +1 -1
- package/dist/backend/grok.js +2 -2
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +52 -2
- package/dist/backend/kiro.js +267 -12
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode.js +17 -4
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +122 -3
- package/dist/backend/types.js +5 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-outage.d.ts +61 -0
- package/dist/backend-outage.js +71 -0
- package/dist/backend-outage.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +44 -2
- package/dist/channel/adapters/discord.js +529 -89
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +30 -0
- package/dist/channel/adapters/telegram.js +221 -19
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/agy-mcp-launcher.d.ts +2 -0
- package/dist/channel/agy-mcp-launcher.js +28 -0
- package/dist/channel/agy-mcp-launcher.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +2 -1
- package/dist/channel/ipc-bridge.js +33 -4
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/markdown-chunk.d.ts +42 -0
- package/dist/channel/markdown-chunk.js +205 -0
- package/dist/channel/markdown-chunk.js.map +1 -0
- package/dist/channel/mcp-server.js +28 -25
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +6 -4
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-queue.js +64 -8
- package/dist/channel/message-queue.js.map +1 -1
- package/dist/channel/types.d.ts +31 -1
- package/dist/classic-channel-manager.d.ts +65 -0
- package/dist/classic-channel-manager.js +172 -3
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +124 -83
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +16 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +12 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +6 -0
- package/dist/cross-instance-envelope.js +30 -0
- package/dist/cross-instance-envelope.js.map +1 -0
- package/dist/daemon.d.ts +506 -11
- package/dist/daemon.js +2162 -313
- package/dist/daemon.js.map +1 -1
- package/dist/doctor.d.ts +41 -0
- package/dist/doctor.js +267 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fleet-context.d.ts +63 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-manager.d.ts +462 -12
- package/dist/fleet-manager.js +3027 -634
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-slim.d.ts +10 -0
- package/dist/fleet-yaml-slim.js +59 -0
- package/dist/fleet-yaml-slim.js.map +1 -0
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +4 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
- package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
- package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +172 -8
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
- package/dist/instance-lifecycle.d.ts +99 -0
- package/dist/instance-lifecycle.js +357 -11
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +74 -3
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +1022 -310
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.d.ts +91 -0
- package/dist/login-flows.js +170 -0
- package/dist/login-flows.js.map +1 -0
- package/dist/login-manager.d.ts +63 -0
- package/dist/login-manager.js +134 -0
- package/dist/login-manager.js.map +1 -0
- package/dist/network-family.d.ts +18 -0
- package/dist/network-family.js +20 -0
- package/dist/network-family.js.map +1 -0
- package/dist/outbound-handlers.d.ts +16 -1
- package/dist/outbound-handlers.js +174 -40
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +11 -3
- package/dist/outbound-schemas.js +11 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-input-residue.d.ts +52 -0
- package/dist/pane-input-residue.js +107 -0
- package/dist/pane-input-residue.js.map +1 -0
- package/dist/process-memory.d.ts +6 -0
- package/dist/process-memory.js +45 -0
- package/dist/process-memory.js.map +1 -1
- package/dist/reply-dedup.d.ts +7 -8
- package/dist/reply-dedup.js +0 -0
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-progress.d.ts +2 -0
- package/dist/restart-progress.js +3 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/db.d.ts +12 -0
- package/dist/scheduler/db.js +59 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-installer.d.ts +11 -0
- package/dist/service-installer.js +84 -18
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/spawn-gate.d.ts +30 -0
- package/dist/spawn-gate.js +79 -0
- package/dist/spawn-gate.js.map +1 -0
- package/dist/steer-capability.d.ts +1 -0
- package/dist/steer-capability.js +6 -0
- package/dist/steer-capability.js.map +1 -0
- package/dist/storm-window.d.ts +83 -0
- package/dist/storm-window.js +251 -0
- package/dist/storm-window.js.map +1 -0
- package/dist/tips.d.ts +44 -0
- package/dist/tips.js +355 -0
- package/dist/tips.js.map +1 -0
- package/dist/tmux-manager.d.ts +16 -0
- package/dist/tmux-manager.js +110 -25
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-progress.d.ts +40 -0
- package/dist/tool-progress.js +289 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/topic-commands.d.ts +45 -6
- package/dist/topic-commands.js +394 -66
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +15 -2
- package/dist/transcript-monitor.js +63 -17
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +131 -0
- package/dist/transcript-sources.js +580 -0
- package/dist/transcript-sources.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/ui/dashboard.html +55 -32
- package/dist/ui/settings.html +200 -60
- package/dist/ui/view.html +147 -30
- package/dist/usage/format-rich.d.ts +1 -1
- package/dist/usage/format-rich.js +28 -24
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +7 -0
- package/dist/usage/i18n-keys.js +34 -0
- package/dist/usage/i18n-keys.js.map +1 -0
- package/dist/usage/i18n.d.ts +5 -0
- package/dist/usage/i18n.js +27 -0
- package/dist/usage/i18n.js.map +1 -0
- package/dist/usage/providers.d.ts +21 -0
- package/dist/usage/providers.js +153 -75
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +11 -3
- package/dist/usage/usage-api.js +61 -24
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/workflow-templates/default.md +2 -1
- package/package.json +2 -2
|
@@ -1,4 +1,47 @@
|
|
|
1
1
|
import { type CliBackend, type CliBackendConfig, type ErrorPattern, type RuntimeDialog, type StartupDialog } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Take `lockPath` via exclusive create, recording {pid, token} so a stale lock
|
|
4
|
+
* can be attributed. Returns the token to release with, or null when the lock
|
|
5
|
+
* could not be acquired within the retry budget.
|
|
6
|
+
*
|
|
7
|
+
* A held lock is NEVER auto-reclaimed. Reclaiming under concurrency cannot be
|
|
8
|
+
* made atomic with the primitives available (rename/link both leave a window
|
|
9
|
+
* where a reclaimed-and-reacquired live lock gets stolen), and every claude.json
|
|
10
|
+
* update is best-effort with a fallback — a skipped trust write is picked up by
|
|
11
|
+
* the startup-dialog dismisser. A lock left by a crashed holder therefore means
|
|
12
|
+
* skipped updates until it is removed manually (delete the .agend.lock file);
|
|
13
|
+
* the crash window is one writeFileSync inside a ~1ms critical section. When
|
|
14
|
+
* the recorded owner PID is verifiably dead we skip immediately instead of
|
|
15
|
+
* burning the whole retry budget waiting for a release that can never come.
|
|
16
|
+
*/
|
|
17
|
+
export declare function acquireClaudeJsonLock(lockPath: string, maxAttempts?: number): string | null;
|
|
18
|
+
/** Release a lock taken by acquireClaudeJsonLock, only if we still own it. */
|
|
19
|
+
export declare function releaseClaudeJsonLock(lockPath: string, token: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Claude's session-resume prompt with the cursor on the destructive default
|
|
22
|
+
* (captured from the 2.1.261 binary: "❯ 1. Resume from summary (recommended)" /
|
|
23
|
+
* " 2. Resume full session as-is"). Anchored to the selector + option number so
|
|
24
|
+
* transcript prose quoting the sentence never matches.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CLAUDE_RESUME_PROMPT_DEFAULT: RegExp;
|
|
27
|
+
/**
|
|
28
|
+
* The resume prompt's two-option menu in ANY arrangement: two consecutive
|
|
29
|
+
* numbered "Resume …" rows. Matches the default shape too, so it must be
|
|
30
|
+
* ordered AFTER the exact entry; on its own it means "a resume menu we do not
|
|
31
|
+
* know how to navigate" — hold, never press.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CLAUDE_RESUME_PROMPT_MENU: RegExp;
|
|
34
|
+
/**
|
|
35
|
+
* Is the resume menu the CURRENT interactive region of the pane, and is the
|
|
36
|
+
* cursor on the destructive default? Bottom-anchored on purpose: the LAST pair
|
|
37
|
+
* of option rows must be followed only by the dialog's own footer (at most two
|
|
38
|
+
* rows) — a quoted capture of the menu in a transcript is followed by more
|
|
39
|
+
* transcript and, above all, by the real `❯` input row, so it is not active.
|
|
40
|
+
*/
|
|
41
|
+
export declare function claudeResumeMenuState(pane: string): {
|
|
42
|
+
active: boolean;
|
|
43
|
+
defaultCursor: boolean;
|
|
44
|
+
};
|
|
2
45
|
export declare class ClaudeCodeBackend implements CliBackend {
|
|
3
46
|
private instanceDir;
|
|
4
47
|
readonly binaryName = "claude";
|
|
@@ -23,20 +66,28 @@ export declare class ClaudeCodeBackend implements CliBackend {
|
|
|
23
66
|
getReadyPattern(): RegExp;
|
|
24
67
|
/**
|
|
25
68
|
* The live spinner line, which is on screen only while generating. Captured from
|
|
26
|
-
*
|
|
69
|
+
* running panes and their pipe-pane recordings:
|
|
27
70
|
*
|
|
28
71
|
* working → `✢ Accomplishing… (11m 26s · ↓ 38.0k tokens)`
|
|
29
72
|
* idle → `✻ Worked for 6m 49s`
|
|
30
73
|
*
|
|
31
74
|
* Both panes also showed `❯` and the `ok` statusline, which is why neither of
|
|
32
|
-
* those can carry the distinction. The discriminator is the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
75
|
+
* those can carry the distinction. The discriminator is the rotating glyph plus
|
|
76
|
+
* Claude's one-word spinner label ending in an ellipsis. The completed line is
|
|
77
|
+
* past tense with no ellipsis. The verb and glyph both rotate, so neither
|
|
78
|
+
* specific value is matched.
|
|
35
79
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
80
|
+
* Claude Code 2.1.239 paints that line incrementally. For roughly the first
|
|
81
|
+
* three seconds — and briefly during later repaints — capture-pane can see only
|
|
82
|
+
* `* Nesting…` before `(3s · ↓ 9 tokens)` is appended. It also emits hyphenated
|
|
83
|
+
* labels such as `Razzle-dazzling…` and `Sock-hopping…`. Requiring the elapsed
|
|
84
|
+
* counter and a letters-only verb made both real shapes look idle. Since `❯`
|
|
85
|
+
* remains visible throughout generation, a settled capture in either shape
|
|
86
|
+
* retired Cancel and the progress bubble while the turn was still running.
|
|
87
|
+
*
|
|
88
|
+
* The animation cycles through six measured frames, including one plain ASCII
|
|
89
|
+
* asterisk. The older leading class required a non-ASCII glyph, so sampling a
|
|
90
|
+
* continuously working pane 50 times at 250ms produced:
|
|
40
91
|
*
|
|
41
92
|
* ✻ 11/11 ✽ 10/10 ✢ 10/10 · 9/9 ✶ 4/4 * 0/6 ← U+002A never matched
|
|
42
93
|
*
|
|
@@ -46,19 +97,13 @@ export declare class ClaudeCodeBackend implements CliBackend {
|
|
|
46
97
|
* retired cancel buttons mid-turn. 12% of frames, so it hit some turns and not
|
|
47
98
|
* others.
|
|
48
99
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Deliberately narrow, because the cost is asymmetric. Missing the spinner just
|
|
57
|
-
* restores today's behaviour; matching prose that happens to sit on a *stable*
|
|
58
|
-
* pane would hold the instance in `working` forever — no auto-pause, no cancel
|
|
59
|
-
* button retirement, and eventually a bogus hang alert. So the line must start
|
|
60
|
-
* with a non-ASCII, non-letter glyph and carry a single word ending in `…`:
|
|
61
|
-
* `- Something… (5s)` and `I waited… (30s) for the build` are both rejected.
|
|
100
|
+
* Deliberately narrow, because the cost is asymmetric. Matching prose that
|
|
101
|
+
* happens to sit on a *stable* pane would hold the instance in `working`
|
|
102
|
+
* forever — no auto-pause, no cancel-button retirement, and eventually a bogus
|
|
103
|
+
* hang alert. Therefore this accepts only Claude's six measured spinner glyphs,
|
|
104
|
+
* exactly one word (optionally hyphenated), and the Unicode ellipsis. Markdown
|
|
105
|
+
* prose such as `* bullet point…`, `- Something…`, and a completed
|
|
106
|
+
* `✻ Worked for 6m 49s` remain rejected.
|
|
62
107
|
*
|
|
63
108
|
* Note when this actually decides anything: while the CLI really is generating,
|
|
64
109
|
* the elapsed counter ticks, the pane changes, and motion already reports
|
|
@@ -79,14 +124,33 @@ export declare class ClaudeCodeBackend implements CliBackend {
|
|
|
79
124
|
getEffortLevels(): string[];
|
|
80
125
|
getModelSwitchStrategy(): "runtime" | "restart";
|
|
81
126
|
listModels(): Promise<import("./types.js").ModelOption[]>;
|
|
127
|
+
/**
|
|
128
|
+
* Full account catalog from `GET /v1/models` using the same OAuth token the
|
|
129
|
+
* usage panel resolves. Each model also gets an `<id>[1m]` 1M-context
|
|
130
|
+
* variant: the API cannot say which plans may use it, so the variants are
|
|
131
|
+
* offered and a wrong pick surfaces through the model_error pattern above.
|
|
132
|
+
* Any failure (no token, network, non-200) degrades to [] — the static
|
|
133
|
+
* aliases from listModels() always remain available.
|
|
134
|
+
*/
|
|
135
|
+
listApiModels(): Promise<import("./types.js").ModelOption[]>;
|
|
82
136
|
probeCLIEnv(): Promise<{
|
|
83
137
|
version: string | undefined;
|
|
84
138
|
models: import("./types.js").ModelOption[];
|
|
139
|
+
apiModels: import("./types.js").ModelOption[];
|
|
85
140
|
}>;
|
|
86
141
|
cleanup(_config: CliBackendConfig): void;
|
|
87
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Pre-accept Claude Code's workspace trust dialog for this working directory.
|
|
144
|
+
* The interactive TUI blocks a fresh workspace on "Do you trust the files in
|
|
145
|
+
* this folder?" and nothing in tmux answers it, so the process exits and the
|
|
146
|
+
* instance crash-loops. No CLI flag skips it interactively (verified on
|
|
147
|
+
* 2.1.250 — only -p / non-TTY stdout bypass the dialog); the recognised
|
|
148
|
+
* signal is projects[<cwd>].hasTrustDialogAccepted in claude.json.
|
|
149
|
+
*/
|
|
150
|
+
preTrust(workingDirectory: string): void;
|
|
151
|
+
/** Pre-approve ANTHROPIC_API_KEY in claude.json to skip the interactive prompt */
|
|
88
152
|
private preApproveApiKey;
|
|
89
|
-
/** Check if user has an active OAuth session in
|
|
153
|
+
/** Check if user has an active OAuth session in claude.json */
|
|
90
154
|
private hasOAuthSession;
|
|
91
155
|
private writeStatusLineScript;
|
|
92
156
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { join, resolve } from "node:path";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { resolveBinary, shellQuote, validateModel, warnIfModelMismatch } from "./types.js";
|
|
5
5
|
/** Mirror Claude Code's ~/.claude/projects key for a working directory. */
|
|
@@ -23,6 +23,169 @@ function claudeProjectKey(cwd) {
|
|
|
23
23
|
}
|
|
24
24
|
return `${sanitized.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
|
|
25
25
|
}
|
|
26
|
+
/** Claude Code's top-level config file, honoring CLAUDE_CONFIG_DIR. */
|
|
27
|
+
function claudeJsonPath() {
|
|
28
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
29
|
+
return configDir ? join(configDir, ".claude.json") : join(homedir(), ".claude.json");
|
|
30
|
+
}
|
|
31
|
+
function sleepSync(ms) {
|
|
32
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Take `lockPath` via exclusive create, recording {pid, token} so a stale lock
|
|
36
|
+
* can be attributed. Returns the token to release with, or null when the lock
|
|
37
|
+
* could not be acquired within the retry budget.
|
|
38
|
+
*
|
|
39
|
+
* A held lock is NEVER auto-reclaimed. Reclaiming under concurrency cannot be
|
|
40
|
+
* made atomic with the primitives available (rename/link both leave a window
|
|
41
|
+
* where a reclaimed-and-reacquired live lock gets stolen), and every claude.json
|
|
42
|
+
* update is best-effort with a fallback — a skipped trust write is picked up by
|
|
43
|
+
* the startup-dialog dismisser. A lock left by a crashed holder therefore means
|
|
44
|
+
* skipped updates until it is removed manually (delete the .agend.lock file);
|
|
45
|
+
* the crash window is one writeFileSync inside a ~1ms critical section. When
|
|
46
|
+
* the recorded owner PID is verifiably dead we skip immediately instead of
|
|
47
|
+
* burning the whole retry budget waiting for a release that can never come.
|
|
48
|
+
*/
|
|
49
|
+
export function acquireClaudeJsonLock(lockPath, maxAttempts = 40) {
|
|
50
|
+
const token = `${process.pid}.${Math.random().toString(16).slice(2)}`;
|
|
51
|
+
const payload = JSON.stringify({ pid: process.pid, token });
|
|
52
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(lockPath, payload, { flag: "wx", mode: 0o600 });
|
|
55
|
+
return token;
|
|
56
|
+
}
|
|
57
|
+
catch { /* already held — fall through to the holder check */ }
|
|
58
|
+
let ownerPid = null;
|
|
59
|
+
try {
|
|
60
|
+
ownerPid = Number(JSON.parse(readFileSync(lockPath, "utf-8")).pid) || null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
continue; /* vanished (retry the create now) or content not written yet (retry) */
|
|
64
|
+
}
|
|
65
|
+
if (ownerPid !== null) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(ownerPid, 0);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
// ESRCH = the holder is dead and will never release — give up now.
|
|
71
|
+
// EPERM means alive under another user: keep waiting like any holder.
|
|
72
|
+
if (err.code === "ESRCH")
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
sleepSync(25);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/** Release a lock taken by acquireClaudeJsonLock, only if we still own it. */
|
|
81
|
+
export function releaseClaudeJsonLock(lockPath, token) {
|
|
82
|
+
try {
|
|
83
|
+
const holder = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
84
|
+
if (holder.token === token)
|
|
85
|
+
unlinkSync(lockPath);
|
|
86
|
+
}
|
|
87
|
+
catch { /* already gone or replaced — nothing of ours to release */ }
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read-modify-write claude.json under a lock file, so concurrent instance
|
|
91
|
+
* spawns don't drop each other's updates, then replace it via atomic rename so
|
|
92
|
+
* a reader never sees a torn file. The file and its lock are created 0600 —
|
|
93
|
+
* claude.json holds OAuth account data, and the rename would otherwise replace
|
|
94
|
+
* the CLI's own 0600 file with the temp file's default 0644/umask mode.
|
|
95
|
+
* `mutate` returns false to skip the write. If the lock cannot be acquired the
|
|
96
|
+
* update is SKIPPED, never performed unlocked (last-write-wins against a
|
|
97
|
+
* concurrent holder could drop that holder's entry); an existing-but-
|
|
98
|
+
* unparseable file is likewise left untouched — never clobber the user's whole
|
|
99
|
+
* config over a parse error. Both give up to the getStartupDialogs() fallback.
|
|
100
|
+
*/
|
|
101
|
+
function updateClaudeJson(mutate) {
|
|
102
|
+
const path = claudeJsonPath();
|
|
103
|
+
// The parent must exist before both the lock create and the rename — on a
|
|
104
|
+
// fresh machine (no ~/.claude.json yet) an ENOENT here previously made every
|
|
105
|
+
// lock attempt fail, silently degrading to unlocked last-write-wins.
|
|
106
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
107
|
+
const lockPath = `${path}.agend.lock`;
|
|
108
|
+
const token = acquireClaudeJsonLock(lockPath);
|
|
109
|
+
if (token === null)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
let cfg = {};
|
|
113
|
+
let raw = null;
|
|
114
|
+
try {
|
|
115
|
+
raw = readFileSync(path, "utf-8");
|
|
116
|
+
}
|
|
117
|
+
catch { /* missing file — create a minimal one */ }
|
|
118
|
+
if (raw !== null) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
122
|
+
return;
|
|
123
|
+
cfg = parsed;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!mutate(cfg))
|
|
130
|
+
return;
|
|
131
|
+
const tempPath = join(dirname(path), `.claude.json.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
132
|
+
try {
|
|
133
|
+
writeFileSync(tempPath, JSON.stringify(cfg, null, 2), { flag: "wx", mode: 0o600 });
|
|
134
|
+
chmodSync(tempPath, 0o600); // mode above is masked by umask; make 0600 unconditional
|
|
135
|
+
renameSync(tempPath, path);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
try {
|
|
139
|
+
unlinkSync(tempPath);
|
|
140
|
+
}
|
|
141
|
+
catch { /* already renamed */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
releaseClaudeJsonLock(lockPath, token);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Claude's session-resume prompt with the cursor on the destructive default
|
|
150
|
+
* (captured from the 2.1.261 binary: "❯ 1. Resume from summary (recommended)" /
|
|
151
|
+
* " 2. Resume full session as-is"). Anchored to the selector + option number so
|
|
152
|
+
* transcript prose quoting the sentence never matches.
|
|
153
|
+
*/
|
|
154
|
+
export const CLAUDE_RESUME_PROMPT_DEFAULT = /^[ \t]*[❯›][ \t]*1\.[ \t]*Resume from summary \(recommended\)/m;
|
|
155
|
+
/**
|
|
156
|
+
* The resume prompt's two-option menu in ANY arrangement: two consecutive
|
|
157
|
+
* numbered "Resume …" rows. Matches the default shape too, so it must be
|
|
158
|
+
* ordered AFTER the exact entry; on its own it means "a resume menu we do not
|
|
159
|
+
* know how to navigate" — hold, never press.
|
|
160
|
+
*/
|
|
161
|
+
export const CLAUDE_RESUME_PROMPT_MENU = /^[ \t]*[❯›]?[ \t]*\d\.[ \t]*Resume (?:full session as-is|from summary)[^\n]*\n[ \t]*[❯›]?[ \t]*\d\.[ \t]*Resume (?:full session as-is|from summary)/m;
|
|
162
|
+
const RESUME_OPTION_ROW = /^[ \t]*[❯›]?[ \t]*\d\.[ \t]*Resume (?:full session as-is|from summary)/;
|
|
163
|
+
const RESUME_DIALOG_FOOTER = /Enter to confirm|Esc to cancel|↑↓|to select|to navigate/i;
|
|
164
|
+
/**
|
|
165
|
+
* Is the resume menu the CURRENT interactive region of the pane, and is the
|
|
166
|
+
* cursor on the destructive default? Bottom-anchored on purpose: the LAST pair
|
|
167
|
+
* of option rows must be followed only by the dialog's own footer (at most two
|
|
168
|
+
* rows) — a quoted capture of the menu in a transcript is followed by more
|
|
169
|
+
* transcript and, above all, by the real `❯` input row, so it is not active.
|
|
170
|
+
*/
|
|
171
|
+
export function claudeResumeMenuState(pane) {
|
|
172
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
173
|
+
let start = -1;
|
|
174
|
+
for (let i = rows.length - 2; i >= 0; i--) {
|
|
175
|
+
if (RESUME_OPTION_ROW.test(rows[i]) && RESUME_OPTION_ROW.test(rows[i + 1])) {
|
|
176
|
+
start = i;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (start < 0)
|
|
181
|
+
return { active: false, defaultCursor: false };
|
|
182
|
+
const trailing = rows.slice(start + 2).filter(r => r.trim().length > 0);
|
|
183
|
+
const active = trailing.length <= 2 && trailing.every(r => RESUME_DIALOG_FOOTER.test(r));
|
|
184
|
+
const defaultCursor = /^[ \t]*[❯›][ \t]*1\.[ \t]*Resume from summary \(recommended\)/.test(rows[start]);
|
|
185
|
+
return { active, defaultCursor };
|
|
186
|
+
}
|
|
187
|
+
const resumeDefaultActive = (pane) => { const s = claudeResumeMenuState(pane); return s.active && s.defaultCursor; };
|
|
188
|
+
const resumeMenuActive = (pane) => claudeResumeMenuState(pane).active;
|
|
26
189
|
export class ClaudeCodeBackend {
|
|
27
190
|
instanceDir;
|
|
28
191
|
binaryName = "claude";
|
|
@@ -58,7 +221,7 @@ export class ClaudeCodeBackend {
|
|
|
58
221
|
if (config.model) {
|
|
59
222
|
const model = validateModel(config.model);
|
|
60
223
|
warnIfModelMismatch("claude-code", model);
|
|
61
|
-
cmd += ` --model ${model}`;
|
|
224
|
+
cmd += ` --model ${shellQuote(model)}`;
|
|
62
225
|
}
|
|
63
226
|
// Additive system prompt: append fleet instructions without overriding Claude's built-in prompt
|
|
64
227
|
const instrFile = join(this.instanceDir, "fleet-instructions.md");
|
|
@@ -123,20 +286,28 @@ export class ClaudeCodeBackend {
|
|
|
123
286
|
}
|
|
124
287
|
/**
|
|
125
288
|
* The live spinner line, which is on screen only while generating. Captured from
|
|
126
|
-
*
|
|
289
|
+
* running panes and their pipe-pane recordings:
|
|
127
290
|
*
|
|
128
291
|
* working → `✢ Accomplishing… (11m 26s · ↓ 38.0k tokens)`
|
|
129
292
|
* idle → `✻ Worked for 6m 49s`
|
|
130
293
|
*
|
|
131
294
|
* Both panes also showed `❯` and the `ok` statusline, which is why neither of
|
|
132
|
-
* those can carry the distinction. The discriminator is the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
295
|
+
* those can carry the distinction. The discriminator is the rotating glyph plus
|
|
296
|
+
* Claude's one-word spinner label ending in an ellipsis. The completed line is
|
|
297
|
+
* past tense with no ellipsis. The verb and glyph both rotate, so neither
|
|
298
|
+
* specific value is matched.
|
|
135
299
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
300
|
+
* Claude Code 2.1.239 paints that line incrementally. For roughly the first
|
|
301
|
+
* three seconds — and briefly during later repaints — capture-pane can see only
|
|
302
|
+
* `* Nesting…` before `(3s · ↓ 9 tokens)` is appended. It also emits hyphenated
|
|
303
|
+
* labels such as `Razzle-dazzling…` and `Sock-hopping…`. Requiring the elapsed
|
|
304
|
+
* counter and a letters-only verb made both real shapes look idle. Since `❯`
|
|
305
|
+
* remains visible throughout generation, a settled capture in either shape
|
|
306
|
+
* retired Cancel and the progress bubble while the turn was still running.
|
|
307
|
+
*
|
|
308
|
+
* The animation cycles through six measured frames, including one plain ASCII
|
|
309
|
+
* asterisk. The older leading class required a non-ASCII glyph, so sampling a
|
|
310
|
+
* continuously working pane 50 times at 250ms produced:
|
|
140
311
|
*
|
|
141
312
|
* ✻ 11/11 ✽ 10/10 ✢ 10/10 · 9/9 ✶ 4/4 * 0/6 ← U+002A never matched
|
|
142
313
|
*
|
|
@@ -146,19 +317,13 @@ export class ClaudeCodeBackend {
|
|
|
146
317
|
* retired cancel buttons mid-turn. 12% of frames, so it hit some turns and not
|
|
147
318
|
* others.
|
|
148
319
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* Deliberately narrow, because the cost is asymmetric. Missing the spinner just
|
|
157
|
-
* restores today's behaviour; matching prose that happens to sit on a *stable*
|
|
158
|
-
* pane would hold the instance in `working` forever — no auto-pause, no cancel
|
|
159
|
-
* button retirement, and eventually a bogus hang alert. So the line must start
|
|
160
|
-
* with a non-ASCII, non-letter glyph and carry a single word ending in `…`:
|
|
161
|
-
* `- Something… (5s)` and `I waited… (30s) for the build` are both rejected.
|
|
320
|
+
* Deliberately narrow, because the cost is asymmetric. Matching prose that
|
|
321
|
+
* happens to sit on a *stable* pane would hold the instance in `working`
|
|
322
|
+
* forever — no auto-pause, no cancel-button retirement, and eventually a bogus
|
|
323
|
+
* hang alert. Therefore this accepts only Claude's six measured spinner glyphs,
|
|
324
|
+
* exactly one word (optionally hyphenated), and the Unicode ellipsis. Markdown
|
|
325
|
+
* prose such as `* bullet point…`, `- Something…`, and a completed
|
|
326
|
+
* `✻ Worked for 6m 49s` remain rejected.
|
|
162
327
|
*
|
|
163
328
|
* Note when this actually decides anything: while the CLI really is generating,
|
|
164
329
|
* the elapsed counter ticks, the pane changes, and motion already reports
|
|
@@ -166,7 +331,7 @@ export class ClaudeCodeBackend {
|
|
|
166
331
|
* an in-progress spinner — which is exactly the hang this is meant to surface.
|
|
167
332
|
*/
|
|
168
333
|
getBusyPattern() {
|
|
169
|
-
return /^[ \t]*
|
|
334
|
+
return /^[ \t]*[✻✽✢·✶*][ \t]+\p{L}+(?:-\p{L}+)*…(?:[ \t]+\([^\n]*)?[ \t]*$/mu;
|
|
170
335
|
}
|
|
171
336
|
getContextUsage() {
|
|
172
337
|
try {
|
|
@@ -192,18 +357,63 @@ export class ClaudeCodeBackend {
|
|
|
192
357
|
getErrorPatterns() {
|
|
193
358
|
return [
|
|
194
359
|
{ pattern: /API Error: Rate limit/i, type: "rate_limit", action: "failover", message: "API rate limit reached" },
|
|
195
|
-
|
|
360
|
+
// pause (not just notify): an auth-expired CLI keeps accepting queued work
|
|
361
|
+
// it can never answer. The pause is lifted by /login's post-success
|
|
362
|
+
// restart, and the lifecycle double-checks with the token-free probe first.
|
|
363
|
+
{ pattern: /Login expired|Not logged in|Please run \/login/i, type: "auth_error", action: "pause", message: "Claude login expired — needs re-login (/login)" },
|
|
196
364
|
{ pattern: /API Error: Authentication/i, type: "auth_error", action: "pause", message: "Authentication error" },
|
|
365
|
+
// A bad /model choice previously failed silently (live-reported): the CLI
|
|
366
|
+
// prints these and idles without answering. notify (not restart): the
|
|
367
|
+
// session is healthy, only the model selection needs changing.
|
|
368
|
+
{
|
|
369
|
+
// Claude 2.1.250 emits `unrecognized_model` for an unknown ID and
|
|
370
|
+
// `model_access` / the plan sentence when the ID exists but the
|
|
371
|
+
// account is not entitled to it. Keep the prose alternatives narrow:
|
|
372
|
+
// the error monitor also sees normal conversation in pane scrollback.
|
|
373
|
+
pattern: /\[claude-code:(?:unrecognized_model|model_access)\]|There's an issue with the selected model|\bis not available on your [^\n]{0,80},\s*or ask your admin to enable this model/i,
|
|
374
|
+
type: "model_error",
|
|
375
|
+
action: "notify",
|
|
376
|
+
message: "Selected Claude model unavailable — Claude Code may be using a fallback; use /model to choose another",
|
|
377
|
+
},
|
|
197
378
|
{ pattern: /API Error: Overloaded/i, type: "rate_limit", action: "notify", message: "API overloaded" },
|
|
198
379
|
{ pattern: /credit balance is too low/i, type: "quota", action: "pause", message: "Insufficient API credits" },
|
|
199
380
|
];
|
|
200
381
|
}
|
|
201
382
|
getStartupDialogs() {
|
|
202
383
|
return [
|
|
384
|
+
// A corrupt claude.json shows a modal whose only options are "1. Exit and
|
|
385
|
+
// fix manually" / "2. Reset with default configuration" (captured live on
|
|
386
|
+
// 2.1.250). Neither is dismissable in a useful direction: Enter exits
|
|
387
|
+
// (crash loop), reset wipes the user's config. The modal's ❯ selector
|
|
388
|
+
// would satisfy the ready pattern, so without this entry the instance
|
|
389
|
+
// reports ready and queued messages get typed into the modal. Must stay
|
|
390
|
+
// FIRST so no dismiss pattern grabs the pane before it.
|
|
391
|
+
{
|
|
392
|
+
pattern: /Configuration error[\s\S]{0,800}?contains invalid JSON/,
|
|
393
|
+
keys: [],
|
|
394
|
+
description: "Claude corrupt-config modal — not dismissable, reporting config_error",
|
|
395
|
+
fatal: { type: "config_error", action: "pause", message: "claude.json is corrupt — Claude Code cannot start. Fix or remove the file (Claude backs the corrupt copy up under <config>/backups/), then wake the instance." },
|
|
396
|
+
},
|
|
203
397
|
// Session resume prompt must be checked BEFORE ready pattern, because ❯ in
|
|
204
398
|
// "❯ 1. Resume from summary" would falsely match the ready pattern /❯/.
|
|
205
|
-
|
|
399
|
+
// Only when the cursor is verifiably on option 1 ("❯ 1. Resume from
|
|
400
|
+
// summary (recommended)") do we know that Down+Enter lands on "Resume
|
|
401
|
+
// full session as-is". Any other shape is held by the guard below.
|
|
402
|
+
{ pattern: CLAUDE_RESUME_PROMPT_DEFAULT, isActive: resumeDefaultActive, keys: ["Down", "Enter"], description: "Claude session resume prompt — select 'Resume full session as-is'", blocksDelivery: true },
|
|
403
|
+
// The same prompt in a shape we do not know how to navigate (cursor
|
|
404
|
+
// elsewhere, wording changed, options reordered): recognised by its
|
|
405
|
+
// two-option menu structure, never answered — a blind Enter would take
|
|
406
|
+
// the default and drop the full context. Hold the pane, report for a human.
|
|
407
|
+
{ pattern: CLAUDE_RESUME_PROMPT_MENU, isActive: resumeMenuActive, keys: [], holdOnly: true, blocksDelivery: true, description: "Claude session resume prompt (unrecognised variant) — holding for a human, never auto-selecting" },
|
|
206
408
|
{ pattern: /[❯›]\s*\d+\.\s*No/m, keys: ["Down", "Enter"], description: "Claude 'No, exit' confirmation — navigate to Yes" },
|
|
409
|
+
// The 2.1.250 workspace-trust dialog has no numbered options — the cursor
|
|
410
|
+
// sits on "❯ No, exit" above "Yes, I trust this folder" (captured live).
|
|
411
|
+
// It must be matched BEFORE the generic /I trust/ Enter fallback below:
|
|
412
|
+
// that pattern also matches this screen's "Yes, I trust this folder" text,
|
|
413
|
+
// and a bare Enter there confirms "No, exit" — the dialog fallback itself
|
|
414
|
+
// used to exit the CLI. preTrust() normally prevents the dialog entirely;
|
|
415
|
+
// this is the recovery path when the config write was skipped.
|
|
416
|
+
{ pattern: /[❯›]\s*No, exit/m, keys: ["Down", "Enter"], description: "Claude workspace trust dialog — navigate to 'Yes, I trust this folder'" },
|
|
207
417
|
{ pattern: /I accept|I trust/i, keys: ["Enter"], description: "Claude 'Yes, I accept' trust dialog" },
|
|
208
418
|
{ pattern: /Resume Session/i, keys: ["Escape"], description: "Claude resume session picker — start fresh" },
|
|
209
419
|
];
|
|
@@ -211,11 +421,24 @@ export class ClaudeCodeBackend {
|
|
|
211
421
|
getRuntimeDialogs() {
|
|
212
422
|
return [
|
|
213
423
|
{
|
|
214
|
-
// Claude Code shows a session resume prompt when session is old/large
|
|
424
|
+
// Claude Code shows a session resume prompt when session is old/large —
|
|
425
|
+
// sometimes only after loading the session, i.e. after the startup scan
|
|
426
|
+
// has already moved on, which is why it is in the runtime table too.
|
|
215
427
|
// Default cursor is on summary; move down to preserve the full context.
|
|
216
|
-
pattern:
|
|
428
|
+
pattern: CLAUDE_RESUME_PROMPT_DEFAULT,
|
|
429
|
+
isActive: resumeDefaultActive,
|
|
217
430
|
keys: ["Down", "Enter"],
|
|
218
431
|
description: "Claude session resume prompt — select 'Resume full session as-is'",
|
|
432
|
+
blocksDelivery: true,
|
|
433
|
+
},
|
|
434
|
+
// Same variant guard as getStartupDialogs: recognise, hold, report — never Enter.
|
|
435
|
+
{
|
|
436
|
+
pattern: CLAUDE_RESUME_PROMPT_MENU,
|
|
437
|
+
isActive: resumeMenuActive,
|
|
438
|
+
keys: [],
|
|
439
|
+
holdOnly: true,
|
|
440
|
+
blocksDelivery: true,
|
|
441
|
+
description: "Claude session resume prompt (unrecognised variant) — holding for a human, never auto-selecting",
|
|
219
442
|
},
|
|
220
443
|
];
|
|
221
444
|
}
|
|
@@ -241,39 +464,131 @@ export class ClaudeCodeBackend {
|
|
|
241
464
|
{ id: "Fable", label: "Fable", description: "Claude Fable 5" },
|
|
242
465
|
];
|
|
243
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Full account catalog from `GET /v1/models` using the same OAuth token the
|
|
469
|
+
* usage panel resolves. Each model also gets an `<id>[1m]` 1M-context
|
|
470
|
+
* variant: the API cannot say which plans may use it, so the variants are
|
|
471
|
+
* offered and a wrong pick surfaces through the model_error pattern above.
|
|
472
|
+
* Any failure (no token, network, non-200) degrades to [] — the static
|
|
473
|
+
* aliases from listModels() always remain available.
|
|
474
|
+
*/
|
|
475
|
+
async listApiModels() {
|
|
476
|
+
try {
|
|
477
|
+
const { getClaudeOAuthToken } = await import("../usage/providers.js");
|
|
478
|
+
const token = await getClaudeOAuthToken();
|
|
479
|
+
if (!token)
|
|
480
|
+
return [];
|
|
481
|
+
const controller = new AbortController();
|
|
482
|
+
const timer = setTimeout(() => controller.abort(), 8_000);
|
|
483
|
+
timer.unref?.();
|
|
484
|
+
const res = await fetch("https://api.anthropic.com/v1/models?limit=100", {
|
|
485
|
+
headers: {
|
|
486
|
+
Authorization: `Bearer ${token}`,
|
|
487
|
+
"anthropic-version": "2023-06-01",
|
|
488
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
489
|
+
},
|
|
490
|
+
signal: controller.signal,
|
|
491
|
+
});
|
|
492
|
+
clearTimeout(timer);
|
|
493
|
+
if (!res.ok)
|
|
494
|
+
return [];
|
|
495
|
+
const body = await res.json();
|
|
496
|
+
const options = [];
|
|
497
|
+
for (const model of body.data ?? []) {
|
|
498
|
+
if (!model.id)
|
|
499
|
+
continue;
|
|
500
|
+
options.push({ id: model.id, label: model.id, ...(model.display_name ? { description: model.display_name } : {}) });
|
|
501
|
+
options.push({ id: `${model.id}[1m]`, label: `${model.id}[1m]`, description: "1M context — plan-dependent" });
|
|
502
|
+
}
|
|
503
|
+
return options;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
244
509
|
async probeCLIEnv() {
|
|
245
510
|
const { probeCliVersion } = await import("./types.js");
|
|
246
|
-
return {
|
|
511
|
+
return {
|
|
512
|
+
version: probeCliVersion(this.binaryPath),
|
|
513
|
+
models: await this.listModels(),
|
|
514
|
+
apiModels: await this.listApiModels(),
|
|
515
|
+
};
|
|
247
516
|
}
|
|
248
517
|
cleanup(_config) {
|
|
249
518
|
// mcp-config.json is in instance dir, cleaned up when instance is deleted
|
|
250
519
|
}
|
|
251
|
-
/**
|
|
520
|
+
/**
|
|
521
|
+
* Pre-accept Claude Code's workspace trust dialog for this working directory.
|
|
522
|
+
* The interactive TUI blocks a fresh workspace on "Do you trust the files in
|
|
523
|
+
* this folder?" and nothing in tmux answers it, so the process exits and the
|
|
524
|
+
* instance crash-loops. No CLI flag skips it interactively (verified on
|
|
525
|
+
* 2.1.250 — only -p / non-TTY stdout bypass the dialog); the recognised
|
|
526
|
+
* signal is projects[<cwd>].hasTrustDialogAccepted in claude.json.
|
|
527
|
+
*/
|
|
528
|
+
preTrust(workingDirectory) {
|
|
529
|
+
const cwd = workingDirectory?.trim();
|
|
530
|
+
if (!cwd)
|
|
531
|
+
return;
|
|
532
|
+
const literal = resolve(cwd);
|
|
533
|
+
let canonical = literal;
|
|
534
|
+
try {
|
|
535
|
+
canonical = realpathSync(literal);
|
|
536
|
+
}
|
|
537
|
+
catch { /* not created yet — trust the literal path */ }
|
|
538
|
+
// Trust both spellings when they differ: Claude keys projects by the cwd's
|
|
539
|
+
// realpath but falls back to the unresolved absolute path (see
|
|
540
|
+
// claudeProjectKey above), and which one the TUI sees depends on how tmux
|
|
541
|
+
// entered the directory.
|
|
542
|
+
const paths = canonical === literal ? [canonical] : [canonical, literal];
|
|
543
|
+
try {
|
|
544
|
+
updateClaudeJson(cfg => {
|
|
545
|
+
let projects = cfg.projects;
|
|
546
|
+
if (!projects || typeof projects !== "object" || Array.isArray(projects)) {
|
|
547
|
+
projects = {};
|
|
548
|
+
cfg.projects = projects;
|
|
549
|
+
}
|
|
550
|
+
let changed = false;
|
|
551
|
+
for (const p of paths) {
|
|
552
|
+
const existing = projects[p];
|
|
553
|
+
const entry = existing && typeof existing === "object" && !Array.isArray(existing)
|
|
554
|
+
? existing
|
|
555
|
+
: {};
|
|
556
|
+
if (entry.hasTrustDialogAccepted === true)
|
|
557
|
+
continue;
|
|
558
|
+
entry.hasTrustDialogAccepted = true;
|
|
559
|
+
projects[p] = entry;
|
|
560
|
+
changed = true;
|
|
561
|
+
}
|
|
562
|
+
return changed;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
catch { /* best effort — startup dialog auto-dismiss remains the fallback */ }
|
|
566
|
+
}
|
|
567
|
+
/** Pre-approve ANTHROPIC_API_KEY in claude.json to skip the interactive prompt */
|
|
252
568
|
preApproveApiKey(_config) {
|
|
253
569
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
254
570
|
if (!apiKey)
|
|
255
571
|
return;
|
|
256
572
|
const fingerprint = apiKey.length > 20 ? apiKey.slice(-20) : apiKey;
|
|
257
|
-
const claudeJsonPath = join(homedir(), ".claude.json");
|
|
258
|
-
let claudeCfg = {};
|
|
259
573
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
574
|
+
updateClaudeJson(cfg => {
|
|
575
|
+
const existing = cfg.customApiKeyResponses;
|
|
576
|
+
const approved = existing?.approved ?? [];
|
|
577
|
+
if (approved.includes(fingerprint))
|
|
578
|
+
return false;
|
|
579
|
+
cfg.customApiKeyResponses = {
|
|
580
|
+
approved: [...approved, fingerprint],
|
|
581
|
+
rejected: existing?.rejected ?? [],
|
|
582
|
+
};
|
|
583
|
+
return true;
|
|
584
|
+
});
|
|
271
585
|
}
|
|
586
|
+
catch { /* best effort — the startup prompt remains the fallback */ }
|
|
272
587
|
}
|
|
273
|
-
/** Check if user has an active OAuth session in
|
|
588
|
+
/** Check if user has an active OAuth session in claude.json */
|
|
274
589
|
hasOAuthSession() {
|
|
275
590
|
try {
|
|
276
|
-
const cfg = JSON.parse(readFileSync(
|
|
591
|
+
const cfg = JSON.parse(readFileSync(claudeJsonPath(), "utf-8"));
|
|
277
592
|
return !!cfg.oauthAccount?.accountUuid;
|
|
278
593
|
}
|
|
279
594
|
catch {
|