@addai/node 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/agent-auth.d.ts +9 -0
- package/dist/agent-auth.js +96 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.js +164 -0
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +406 -0
- package/dist/claude-binary.d.ts +16 -0
- package/dist/claude-binary.js +145 -0
- package/dist/claude-config.d.ts +11 -0
- package/dist/claude-config.js +144 -0
- package/dist/claude-print.d.ts +35 -0
- package/dist/claude-print.js +312 -0
- package/dist/claude-spawn.d.ts +40 -0
- package/dist/claude-spawn.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +137 -0
- package/dist/codex-binary.d.ts +4 -0
- package/dist/codex-binary.js +70 -0
- package/dist/codex-spawn.d.ts +47 -0
- package/dist/codex-spawn.js +320 -0
- package/dist/command-runner.d.ts +9 -0
- package/dist/command-runner.js +592 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +38 -0
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +292 -0
- package/dist/diskguard.d.ts +96 -0
- package/dist/diskguard.js +435 -0
- package/dist/error-classify.d.ts +20 -0
- package/dist/error-classify.js +159 -0
- package/dist/events.d.ts +26 -0
- package/dist/events.js +97 -0
- package/dist/gemini-binary.d.ts +4 -0
- package/dist/gemini-binary.js +64 -0
- package/dist/gemini-spawn.d.ts +52 -0
- package/dist/gemini-spawn.js +369 -0
- package/dist/git-identity.d.ts +18 -0
- package/dist/git-identity.js +104 -0
- package/dist/grok-binary.d.ts +4 -0
- package/dist/grok-binary.js +70 -0
- package/dist/grok-spawn.d.ts +41 -0
- package/dist/grok-spawn.js +495 -0
- package/dist/guardrails.d.ts +32 -0
- package/dist/guardrails.js +125 -0
- package/dist/hang-watchdog.d.ts +17 -0
- package/dist/hang-watchdog.js +48 -0
- package/dist/harness-registry.d.ts +55 -0
- package/dist/harness-registry.js +180 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.js +95 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +440 -0
- package/dist/install.d.ts +63 -0
- package/dist/install.js +467 -0
- package/dist/jsonl-tail.d.ts +20 -0
- package/dist/jsonl-tail.js +192 -0
- package/dist/kimi-binary.d.ts +4 -0
- package/dist/kimi-binary.js +68 -0
- package/dist/kimi-spawn.d.ts +47 -0
- package/dist/kimi-spawn.js +319 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +169 -0
- package/dist/mcp-config.d.ts +23 -0
- package/dist/mcp-config.js +133 -0
- package/dist/memory-capture.d.ts +20 -0
- package/dist/memory-capture.js +45 -0
- package/dist/memory-pack.d.ts +34 -0
- package/dist/memory-pack.js +171 -0
- package/dist/model-filter.d.ts +4 -0
- package/dist/model-filter.js +34 -0
- package/dist/node-health.d.ts +18 -0
- package/dist/node-health.js +74 -0
- package/dist/pairing.d.ts +20 -0
- package/dist/pairing.js +129 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +48 -0
- package/dist/precompact-hook.d.ts +27 -0
- package/dist/precompact-hook.js +136 -0
- package/dist/projects.d.ts +25 -0
- package/dist/projects.js +255 -0
- package/dist/prompt-keystrokes.d.ts +3 -0
- package/dist/prompt-keystrokes.js +56 -0
- package/dist/pty-helper.d.ts +26 -0
- package/dist/pty-helper.js +156 -0
- package/dist/request-pump.d.ts +12 -0
- package/dist/request-pump.js +249 -0
- package/dist/run-retry.d.ts +28 -0
- package/dist/run-retry.js +81 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +192 -0
- package/dist/session-runner.d.ts +130 -0
- package/dist/session-runner.js +2271 -0
- package/dist/sleep-detector.d.ts +11 -0
- package/dist/sleep-detector.js +32 -0
- package/dist/state-migrate.d.ts +8 -0
- package/dist/state-migrate.js +91 -0
- package/dist/store.d.ts +5 -0
- package/dist/store.js +82 -0
- package/dist/supabase-client.d.ts +15 -0
- package/dist/supabase-client.js +119 -0
- package/dist/time-context.d.ts +10 -0
- package/dist/time-context.js +71 -0
- package/dist/tokens.d.ts +4 -0
- package/dist/tokens.js +33 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.js +82 -0
- package/dist/tui/console-capture.d.ts +13 -0
- package/dist/tui/console-capture.js +62 -0
- package/dist/tui/dashboard.d.ts +42 -0
- package/dist/tui/dashboard.js +205 -0
- package/dist/tui/data.d.ts +57 -0
- package/dist/tui/data.js +62 -0
- package/dist/tui/harnesses.d.ts +16 -0
- package/dist/tui/harnesses.js +354 -0
- package/dist/tui/render.d.ts +21 -0
- package/dist/tui/render.js +116 -0
- package/dist/tui/requests.d.ts +18 -0
- package/dist/tui/requests.js +127 -0
- package/dist/tui/run.d.ts +13 -0
- package/dist/tui/run.js +178 -0
- package/dist/tui/transcript.d.ts +39 -0
- package/dist/tui/transcript.js +234 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +314 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +3 -0
- package/dist/win.d.ts +67 -0
- package/dist/win.js +317 -0
- package/package.json +60 -0
- package/scripts/fix-pty-helper.js +28 -0
- package/scripts/precompact-capture.js +292 -0
- package/scripts/smoke-test.sh +74 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Best-effort probe of the binaries the runtime knows about. The result
|
|
3
|
+
// is JSON-shaped and posted up via runtime_heartbeat so the vault /
|
|
4
|
+
// studio UI can show live model lists, login status, and active
|
|
5
|
+
// account. Failures are silent — a missing binary or unauth'd CLI just
|
|
6
|
+
// won't show as an agent option in the pickers.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.codexHome = codexHome;
|
|
42
|
+
exports.codexAuthFromStore = codexAuthFromStore;
|
|
43
|
+
exports.probeCapabilities = probeCapabilities;
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const claude_binary_1 = require("./claude-binary");
|
|
49
|
+
const kimi_binary_1 = require("./kimi-binary");
|
|
50
|
+
const gemini_binary_1 = require("./gemini-binary");
|
|
51
|
+
const grok_binary_1 = require("./grok-binary");
|
|
52
|
+
const codex_binary_1 = require("./codex-binary");
|
|
53
|
+
const win_1 = require("./win");
|
|
54
|
+
const harness_registry_1 = require("./harness-registry");
|
|
55
|
+
function readDaemonVersion() {
|
|
56
|
+
try {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
+
const pkg = require('../package.json');
|
|
59
|
+
return pkg.version ?? 'unknown';
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function runCmd(cmd, args, timeoutMs = 5000) {
|
|
66
|
+
return new Promise(resolve => {
|
|
67
|
+
// On win32 the discovered binary may be an npm .cmd shim, which Node
|
|
68
|
+
// refuses to execFile without a shell (EINVAL, thrown SYNCHRONOUSLY).
|
|
69
|
+
// Resolve the shim to its node script first, and catch the sync throw —
|
|
70
|
+
// a single failing probe must not take down the whole heartbeat.
|
|
71
|
+
try {
|
|
72
|
+
const inv = (0, win_1.resolveCliInvocation)(cmd, args);
|
|
73
|
+
(0, child_process_1.execFile)(inv.file, inv.args, { timeout: timeoutMs, maxBuffer: 256 * 1024 }, (err, stdout, stderr) => {
|
|
74
|
+
resolve({ ok: !err, stdout: (stdout ?? '').trim(), stderr: (stderr ?? '').trim() });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
resolve({ ok: false, stdout: '', stderr: err.message ?? String(err) });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Curated model lists baked into the daemon. The CLIs don't expose a
|
|
83
|
+
// stable "list models" command; the daemon reports whatever the
|
|
84
|
+
// installed daemon version knows about, so bumping the daemon refreshes
|
|
85
|
+
// the UI's dropdown.
|
|
86
|
+
const CLAUDE_MODELS = [
|
|
87
|
+
'claude-opus-4-7',
|
|
88
|
+
'claude-sonnet-4-6',
|
|
89
|
+
'claude-haiku-4-5',
|
|
90
|
+
'claude-opus-4-1',
|
|
91
|
+
'claude-sonnet-4-5',
|
|
92
|
+
];
|
|
93
|
+
const CODEX_MODELS = [
|
|
94
|
+
'gpt-5',
|
|
95
|
+
'gpt-5-mini',
|
|
96
|
+
'o4',
|
|
97
|
+
'o4-mini',
|
|
98
|
+
];
|
|
99
|
+
const KIMI_MODELS = [
|
|
100
|
+
'kimi-k2-instruct',
|
|
101
|
+
'kimi-k2-base',
|
|
102
|
+
'moonshot-v1-128k',
|
|
103
|
+
'moonshot-v1-32k',
|
|
104
|
+
];
|
|
105
|
+
const GEMINI_MODELS = [
|
|
106
|
+
'gemini-2.5-flash',
|
|
107
|
+
'gemini-2.5-pro',
|
|
108
|
+
'gemini-2.0-flash',
|
|
109
|
+
'gemini-2.0-pro',
|
|
110
|
+
];
|
|
111
|
+
const GROK_MODELS = [
|
|
112
|
+
'grok-4.5',
|
|
113
|
+
'grok-composer-2.5-fast',
|
|
114
|
+
];
|
|
115
|
+
async function probeClaude() {
|
|
116
|
+
const bin = (0, claude_binary_1.findClaudeBinary)();
|
|
117
|
+
if (!bin)
|
|
118
|
+
return { available: false, authed: false };
|
|
119
|
+
const ver = await runCmd(bin, ['--version']);
|
|
120
|
+
if (!ver.ok || !ver.stdout)
|
|
121
|
+
return { available: false, authed: false };
|
|
122
|
+
const version = ver.stdout.replace(/^claude\s+/i, '').trim() || undefined;
|
|
123
|
+
// `claude auth status` prints JSON describing the logged-in account.
|
|
124
|
+
// Example: {"loggedIn":true,"authMethod":"claude.ai","email":"a@b.c","subscriptionType":"max"}
|
|
125
|
+
const auth = await runCmd(bin, ['auth', 'status']);
|
|
126
|
+
let authShape = { authed: false };
|
|
127
|
+
if (auth.ok && auth.stdout) {
|
|
128
|
+
try {
|
|
129
|
+
const j = JSON.parse(auth.stdout);
|
|
130
|
+
authShape = {
|
|
131
|
+
authed: j.loggedIn === true,
|
|
132
|
+
account: typeof j.email === 'string' ? j.email : undefined,
|
|
133
|
+
accountKind: typeof j.authMethod === 'string' ? j.authMethod : undefined,
|
|
134
|
+
plan: typeof j.subscriptionType === 'string' ? j.subscriptionType : undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch { /* fall through unauthed */ }
|
|
138
|
+
}
|
|
139
|
+
return { available: true, version, models: CLAUDE_MODELS, ...authShape };
|
|
140
|
+
}
|
|
141
|
+
/** Codex's credential store: $CODEX_HOME (or ~/.codex) + /auth.json. */
|
|
142
|
+
function codexHome() {
|
|
143
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read auth state straight out of codex's credential file.
|
|
147
|
+
*
|
|
148
|
+
* `codex login status` cannot be trusted as the sole signal: on some
|
|
149
|
+
* installs it prints its "Logged in using ChatGPT" line only when stdio is
|
|
150
|
+
* a TTY and stays completely silent (exit 0, both streams empty) when the
|
|
151
|
+
* daemon runs it with pipes — so a logged-in node reported itself logged
|
|
152
|
+
* out forever, and remote logins "failed" verification after succeeding
|
|
153
|
+
* (live incident, Lennys-Mac-mini 2026-07-29). The file is written by the
|
|
154
|
+
* login flow itself, which makes it the fact rather than the commentary.
|
|
155
|
+
*
|
|
156
|
+
* Shape (values redacted):
|
|
157
|
+
* { auth_mode: 'chatgpt', OPENAI_API_KEY: null,
|
|
158
|
+
* tokens: { id_token, access_token, refresh_token, account_id },
|
|
159
|
+
* last_refresh }
|
|
160
|
+
*
|
|
161
|
+
* Never throws: an unreadable or malformed store simply isn't proof of a
|
|
162
|
+
* login. Same approach probeKimi already takes for its OAuth store.
|
|
163
|
+
*/
|
|
164
|
+
function codexAuthFromStore(home) {
|
|
165
|
+
if (!home)
|
|
166
|
+
return { authed: false };
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(fs.readFileSync(path.join(home, 'auth.json'), 'utf8'));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return { authed: false };
|
|
173
|
+
}
|
|
174
|
+
const tokens = (parsed.tokens ?? null);
|
|
175
|
+
const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
176
|
+
if (tokens && (nonEmpty(tokens.access_token) || nonEmpty(tokens.refresh_token))) {
|
|
177
|
+
return { authed: true, accountKind: 'ChatGPT' };
|
|
178
|
+
}
|
|
179
|
+
if (nonEmpty(parsed.OPENAI_API_KEY))
|
|
180
|
+
return { authed: true, accountKind: 'api-key' };
|
|
181
|
+
return { authed: false };
|
|
182
|
+
}
|
|
183
|
+
async function probeCodex() {
|
|
184
|
+
// Resolve the real binary path — bare 'codex' relies on PATH search,
|
|
185
|
+
// which misses npm-global dirs under launchd and never finds .cmd shims
|
|
186
|
+
// on Windows.
|
|
187
|
+
const bin = (0, codex_binary_1.findCodexBinary)() ?? 'codex';
|
|
188
|
+
const ver = await runCmd(bin, ['--version']);
|
|
189
|
+
if (!ver.ok || !ver.stdout)
|
|
190
|
+
return { available: false, authed: false };
|
|
191
|
+
// `codex login status` is text-only AND writes to stderr (not stdout)
|
|
192
|
+
// when run headlessly. Logged-in output starts with "Logged in" and
|
|
193
|
+
// usually mentions how (e.g. "Logged in using ChatGPT"). When it speaks,
|
|
194
|
+
// it carries the nicest detail — how the account was authenticated — so
|
|
195
|
+
// prefer it, but never let its silence outvote the credential store.
|
|
196
|
+
const login = await runCmd(bin, ['login', 'status']);
|
|
197
|
+
const loginText = `${login.stdout}\n${login.stderr}`;
|
|
198
|
+
let authShape = { authed: false };
|
|
199
|
+
if (login.ok && /^\s*logged in/im.test(loginText)) {
|
|
200
|
+
const via = /using\s+(.+?)(?:[.\s]|$)/i.exec(loginText)?.[1]?.trim();
|
|
201
|
+
authShape = { authed: true, accountKind: via || 'codex' };
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
authShape = codexAuthFromStore(codexHome());
|
|
205
|
+
}
|
|
206
|
+
return { available: true, version: ver.stdout, models: CODEX_MODELS, ...authShape };
|
|
207
|
+
}
|
|
208
|
+
async function probeKimi() {
|
|
209
|
+
const bin = (0, kimi_binary_1.findKimiBinary)();
|
|
210
|
+
if (!bin)
|
|
211
|
+
return { available: false, authed: false };
|
|
212
|
+
const ver = await runCmd(bin, ['--version']);
|
|
213
|
+
if (!ver.ok || !ver.stdout)
|
|
214
|
+
return { available: false, authed: false };
|
|
215
|
+
const version = ver.stdout.replace(/^kimi\s+/i, '').trim() || undefined;
|
|
216
|
+
// Auth signals, in order: env API key; kimi-code's OAuth credential
|
|
217
|
+
// store (~/.kimi-code/credentials/*.json with a refresh/access token —
|
|
218
|
+
// written by `kimi login`'s device-code flow); legacy ~/.kimi/config.
|
|
219
|
+
const envKey = process.env.MOONSHOT_API_KEY || process.env.KIMI_API_KEY;
|
|
220
|
+
let hasOauth = false;
|
|
221
|
+
try {
|
|
222
|
+
const credDir = path.join(os.homedir(), '.kimi-code', 'credentials');
|
|
223
|
+
for (const f of fs.readdirSync(credDir)) {
|
|
224
|
+
if (!f.endsWith('.json'))
|
|
225
|
+
continue;
|
|
226
|
+
const cred = JSON.parse(fs.readFileSync(path.join(credDir, f), 'utf-8'));
|
|
227
|
+
// Require a NON-EMPTY token, not just a present field — a logged-out
|
|
228
|
+
// CLI can leave the cred file with empty-string tokens.
|
|
229
|
+
const nonEmpty = (v) => typeof v === 'string' && v.length > 0;
|
|
230
|
+
if (nonEmpty(cred.refresh_token) || nonEmpty(cred.access_token)) {
|
|
231
|
+
hasOauth = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch { /* no cred dir = not oauth-authed */ }
|
|
237
|
+
// Legacy ~/.kimi/config: the actual bug is an EMPTY/whitespace-only file
|
|
238
|
+
// (logged-out or half-installed CLI) showing the agent green and routing
|
|
239
|
+
// dispatch to a dead provider. Reject only that. Deliberately NOT
|
|
240
|
+
// keyword-gated: a working legacy login may store its token under any key
|
|
241
|
+
// (a bare `sk-…`, `"key":`, `"auth":`), and a keyword allowlist would flip
|
|
242
|
+
// a VALID config to unauthed and drop kimi off the ladder — a false-negative
|
|
243
|
+
// worse than the bug. A non-empty, non-empty-container file is trusted.
|
|
244
|
+
let hasConfig = false;
|
|
245
|
+
try {
|
|
246
|
+
const raw = fs.readFileSync(path.join(os.homedir(), '.kimi', 'config'), 'utf-8').trim();
|
|
247
|
+
hasConfig = raw.length > 0 && raw !== '{}' && raw !== '[]';
|
|
248
|
+
}
|
|
249
|
+
catch { /* missing / unreadable = not config-authed */ }
|
|
250
|
+
const authed = !!envKey || hasOauth || hasConfig;
|
|
251
|
+
return {
|
|
252
|
+
available: true, version, models: KIMI_MODELS,
|
|
253
|
+
authed,
|
|
254
|
+
accountKind: authed ? (envKey ? 'api-key' : hasOauth ? 'kimi-code' : 'config') : undefined,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function probeGemini() {
|
|
258
|
+
const bin = (0, gemini_binary_1.findGeminiBinary)();
|
|
259
|
+
if (!bin)
|
|
260
|
+
return { available: false, authed: false };
|
|
261
|
+
const ver = await runCmd(bin, ['--version']);
|
|
262
|
+
if (!ver.ok || !ver.stdout)
|
|
263
|
+
return { available: false, authed: false };
|
|
264
|
+
const version = ver.stdout.replace(/^gemini\s+/i, '').trim() || undefined;
|
|
265
|
+
// Gemini CLI stores creds at ~/.gemini/oauth_creds.json and the
|
|
266
|
+
// active account email at ~/.gemini/google_accounts.json (`.active`).
|
|
267
|
+
const home = os.homedir();
|
|
268
|
+
const credsPath = path.join(home, '.gemini', 'oauth_creds.json');
|
|
269
|
+
const acctPath = path.join(home, '.gemini', 'google_accounts.json');
|
|
270
|
+
let authed = false;
|
|
271
|
+
let account;
|
|
272
|
+
let accountKind;
|
|
273
|
+
try {
|
|
274
|
+
if (fs.existsSync(credsPath)) {
|
|
275
|
+
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf-8'));
|
|
276
|
+
const accessTok = typeof creds.access_token === 'string' ? creds.access_token : '';
|
|
277
|
+
const refreshTok = typeof creds.refresh_token === 'string' ? creds.refresh_token : '';
|
|
278
|
+
// Non-empty access token is the floor. Google oauth_creds carry an
|
|
279
|
+
// expiry_date (ms epoch); an expired access token is still usable IFF a
|
|
280
|
+
// refresh_token exists (the CLI refreshes transparently). Only downgrade
|
|
281
|
+
// to unauthed when expired AND there's nothing to refresh with. Absent /
|
|
282
|
+
// non-numeric expiry is treated as valid to avoid false-negatives on a
|
|
283
|
+
// working login.
|
|
284
|
+
const expiryMs = typeof creds.expiry_date === 'number' ? creds.expiry_date : undefined;
|
|
285
|
+
const expired = expiryMs !== undefined && expiryMs <= Date.now();
|
|
286
|
+
authed = accessTok.length > 0 && (!expired || refreshTok.length > 0);
|
|
287
|
+
if (authed)
|
|
288
|
+
accountKind = 'google';
|
|
289
|
+
}
|
|
290
|
+
if (fs.existsSync(acctPath)) {
|
|
291
|
+
const accts = JSON.parse(fs.readFileSync(acctPath, 'utf-8'));
|
|
292
|
+
if (typeof accts.active === 'string')
|
|
293
|
+
account = accts.active;
|
|
294
|
+
}
|
|
295
|
+
// API-key / Vertex logins live in ~/.gemini/.env (the CLI reads it).
|
|
296
|
+
if (!authed) {
|
|
297
|
+
const envPath = path.join(home, '.gemini', '.env');
|
|
298
|
+
if (fs.existsSync(envPath)) {
|
|
299
|
+
const env = fs.readFileSync(envPath, 'utf-8');
|
|
300
|
+
const has = (v) => new RegExp(`^${v}=.+`, 'm').test(env);
|
|
301
|
+
if (has('GOOGLE_API_KEY') && has('GOOGLE_GENAI_USE_VERTEXAI')) {
|
|
302
|
+
authed = true;
|
|
303
|
+
accountKind = 'vertex';
|
|
304
|
+
}
|
|
305
|
+
else if (has('GEMINI_API_KEY')) {
|
|
306
|
+
authed = true;
|
|
307
|
+
accountKind = 'api-key';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch { /* unreadable creds = unauthed */ }
|
|
313
|
+
return {
|
|
314
|
+
available: true, version, models: GEMINI_MODELS,
|
|
315
|
+
authed, account, accountKind,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function probeGrok() {
|
|
319
|
+
const bin = (0, grok_binary_1.findGrokBinary)();
|
|
320
|
+
if (!bin)
|
|
321
|
+
return { available: false, authed: false };
|
|
322
|
+
const ver = await runCmd(bin, ['--version']);
|
|
323
|
+
if (!ver.ok || !ver.stdout)
|
|
324
|
+
return { available: false, authed: false };
|
|
325
|
+
// "grok 0.2.101 (5bc4...) [stable]" → keep the semver.
|
|
326
|
+
const version = /(\d+\.\d+\.\d+)/.exec(ver.stdout)?.[1] ?? ver.stdout;
|
|
327
|
+
// The account lives in ~/.grok/auth.json, keyed by issuer::user-id. Each
|
|
328
|
+
// entry carries the OAuth token (`key`), a `refresh_token`, and the
|
|
329
|
+
// signed-in `email`. Presence of a refresh_token = the CLI can keep the
|
|
330
|
+
// session alive, so we treat that as authed (mirrors gemini's probe).
|
|
331
|
+
const authPath = path.join(os.homedir(), '.grok', 'auth.json');
|
|
332
|
+
let authShape = { authed: false };
|
|
333
|
+
try {
|
|
334
|
+
if (fs.existsSync(authPath)) {
|
|
335
|
+
const obj = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
336
|
+
// Require a NON-EMPTY token. A logged-out CLI can leave an entry whose
|
|
337
|
+
// refresh_token/key are empty strings, which the old typeof-string check
|
|
338
|
+
// accepted → agent shown green and the provider-ladder routed to a dead
|
|
339
|
+
// provider. refresh_token means the session can be kept alive; a bare
|
|
340
|
+
// key also counts.
|
|
341
|
+
const nonEmpty = (v) => typeof v === 'string' && v.length > 0;
|
|
342
|
+
const entry = Object.values(obj).find(e => e && (nonEmpty(e.refresh_token) || nonEmpty(e.key)));
|
|
343
|
+
if (entry) {
|
|
344
|
+
authShape = {
|
|
345
|
+
authed: true,
|
|
346
|
+
account: typeof entry.email === 'string' ? entry.email : undefined,
|
|
347
|
+
accountKind: 'grok.com',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch { /* unreadable creds = unauthed */ }
|
|
353
|
+
return { available: true, version, models: GROK_MODELS, ...authShape };
|
|
354
|
+
}
|
|
355
|
+
async function probeGit() {
|
|
356
|
+
const ver = await runCmd('git', ['--version']);
|
|
357
|
+
if (!ver.ok || !ver.stdout)
|
|
358
|
+
return { available: false, authed: false };
|
|
359
|
+
// Git creds live per-repo via helpers — there's no global "logged in"
|
|
360
|
+
// notion. Mark authed=true so the UI doesn't grey out a working git.
|
|
361
|
+
return {
|
|
362
|
+
available: true,
|
|
363
|
+
version: ver.stdout.replace(/^git version\s+/i, '').trim(),
|
|
364
|
+
authed: true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async function probeCapabilities() {
|
|
368
|
+
// Binary lookups cache misses; a CLI installed mid-daemon-life (by the
|
|
369
|
+
// command runner or by hand) must show up on the next probe, not after
|
|
370
|
+
// a daemon restart.
|
|
371
|
+
(0, harness_registry_1.resetBinaryCaches)();
|
|
372
|
+
// Each probe is individually shielded: one throwing probe (e.g. a broken
|
|
373
|
+
// CLI install) must not reject the whole Promise.all and kill every
|
|
374
|
+
// heartbeat — that would report the runtime permanently offline.
|
|
375
|
+
const shield = (p, fallback) => p.catch(() => fallback);
|
|
376
|
+
const unavailable = { available: false, authed: false };
|
|
377
|
+
const [claude, codex, kimi, gemini, grok, git] = await Promise.all([
|
|
378
|
+
shield(probeClaude(), unavailable),
|
|
379
|
+
shield(probeCodex(), unavailable),
|
|
380
|
+
shield(probeKimi(), unavailable),
|
|
381
|
+
shield(probeGemini(), unavailable),
|
|
382
|
+
shield(probeGrok(), unavailable),
|
|
383
|
+
shield(probeGit(), unavailable),
|
|
384
|
+
]);
|
|
385
|
+
// Decorate each harness with its registry facts (efforts, installability,
|
|
386
|
+
// login strategy) so Studio and the TUI render from one source of truth.
|
|
387
|
+
const deco = (id, probe) => ({
|
|
388
|
+
...probe,
|
|
389
|
+
efforts: harness_registry_1.HARNESSES[id].efforts,
|
|
390
|
+
installable: (0, harness_registry_1.isInstallable)(id),
|
|
391
|
+
login_strategy: harness_registry_1.HARNESSES[id].login.strategy,
|
|
392
|
+
login_methods: [
|
|
393
|
+
harness_registry_1.HARNESSES[id].login.strategy,
|
|
394
|
+
...(harness_registry_1.HARNESSES[id].extraLogins ?? []).map(m => m.method),
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
daemon_version: readDaemonVersion(),
|
|
399
|
+
claude: deco('claude', claude),
|
|
400
|
+
codex: deco('codex', codex),
|
|
401
|
+
kimi: deco('kimi', kimi),
|
|
402
|
+
gemini: deco('gemini', gemini),
|
|
403
|
+
grok: deco('grok', grok),
|
|
404
|
+
git,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the Claude Code CLI binary. Mirrors the resolution strategy
|
|
3
|
+
* the desktop app uses, so the runtime spawns the same Claude the
|
|
4
|
+
* user already has installed.
|
|
5
|
+
*
|
|
6
|
+
* Returns `null` when claude isn't installed — callers MUST NOT spawn a
|
|
7
|
+
* bare `'claude'` literal, which throws an async ENOENT with no 'error'
|
|
8
|
+
* listener attached in time and takes the whole daemon down (observed on
|
|
9
|
+
* a Windows box with only grok installed: a self-improve/capture distil
|
|
10
|
+
* request tried to run the Haiku summarizer via claude → crash). Mirrors
|
|
11
|
+
* findCodexBinary/findGrokBinary, which already return `null`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findClaudeBinary(): string | null;
|
|
14
|
+
/** Encode a working directory the same way Claude does for its
|
|
15
|
+
* per-project transcript folder (`~/.claude/projects/<encoded>/`). */
|
|
16
|
+
export declare function encodeProjectPath(dirPath: string): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.findClaudeBinary = findClaudeBinary;
|
|
37
|
+
exports.encodeProjectPath = encodeProjectPath;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
42
|
+
/**
|
|
43
|
+
* Find the Claude Code CLI binary. Mirrors the resolution strategy
|
|
44
|
+
* the desktop app uses, so the runtime spawns the same Claude the
|
|
45
|
+
* user already has installed.
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when claude isn't installed — callers MUST NOT spawn a
|
|
48
|
+
* bare `'claude'` literal, which throws an async ENOENT with no 'error'
|
|
49
|
+
* listener attached in time and takes the whole daemon down (observed on
|
|
50
|
+
* a Windows box with only grok installed: a self-improve/capture distil
|
|
51
|
+
* request tried to run the Haiku summarizer via claude → crash). Mirrors
|
|
52
|
+
* findCodexBinary/findGrokBinary, which already return `null`.
|
|
53
|
+
*/
|
|
54
|
+
function findClaudeBinary() {
|
|
55
|
+
if (IS_WINDOWS) {
|
|
56
|
+
try {
|
|
57
|
+
// `where` lists every match; npm installs three shims side by side
|
|
58
|
+
// (extensionless bash script, .cmd, .ps1). Prefer a native .exe
|
|
59
|
+
// (installer builds), then .cmd — never the extensionless one,
|
|
60
|
+
// which CreateProcess can't run.
|
|
61
|
+
const lines = (0, child_process_1.execSync)('where claude', { encoding: 'utf-8', timeout: 5000 })
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.map(l => l.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
const result = lines.find(l => /\.exe$/i.test(l)) ??
|
|
66
|
+
lines.find(l => /\.cmd$/i.test(l)) ??
|
|
67
|
+
lines.find(l => /\.bat$/i.test(l));
|
|
68
|
+
if (result && fs.existsSync(result))
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
const appData = process.env.APPDATA || '';
|
|
73
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
74
|
+
const userProfile = process.env.USERPROFILE || '';
|
|
75
|
+
const candidates = [
|
|
76
|
+
path.join(userProfile, '.local', 'bin', 'claude.exe'), // native installer default
|
|
77
|
+
path.join(appData, 'npm', 'claude.cmd'),
|
|
78
|
+
path.join(localAppData, 'npm', 'claude.cmd'),
|
|
79
|
+
path.join(userProfile, '.npm-global', 'claude.cmd'),
|
|
80
|
+
path.join(userProfile, 'AppData', 'Roaming', 'npm', 'claude.cmd'),
|
|
81
|
+
];
|
|
82
|
+
for (const p of candidates) {
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(p))
|
|
85
|
+
return p;
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const shell = process.env.SHELL || '/bin/zsh';
|
|
92
|
+
// Resolve through the user's shell. We try an *interactive* login shell
|
|
93
|
+
// (`-i -l`) so that a `claude` alias/function defined in the interactive
|
|
94
|
+
// section of ~/.zshrc is visible — a plain `-l -c` shell is
|
|
95
|
+
// non-interactive and never sources it, which is how the runtime ended up
|
|
96
|
+
// spawning the bare `claude` literal (ENOENT) on machines where claude is
|
|
97
|
+
// only reachable via an alias. `command -v` follows aliases/functions to
|
|
98
|
+
// their target path; fall back to `which` for older setups.
|
|
99
|
+
const probes = [
|
|
100
|
+
`${shell} -i -l -c "command -v claude" 2>/dev/null`,
|
|
101
|
+
`${shell} -l -c "command -v claude" 2>/dev/null`,
|
|
102
|
+
`${shell} -l -c "which claude"`,
|
|
103
|
+
];
|
|
104
|
+
for (const cmd of probes) {
|
|
105
|
+
try {
|
|
106
|
+
const result = (0, child_process_1.execSync)(cmd, { encoding: 'utf-8', timeout: 5000 })
|
|
107
|
+
.trim()
|
|
108
|
+
.split('\n')
|
|
109
|
+
.pop()
|
|
110
|
+
.trim();
|
|
111
|
+
if (result && result.startsWith('/') && fs.existsSync(result))
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
}
|
|
116
|
+
const home = process.env.HOME || '';
|
|
117
|
+
const candidates = [
|
|
118
|
+
`${home}/.local/bin/claude`, // native installer default
|
|
119
|
+
`${home}/.claude/local/claude`, // local / migrate-installer install
|
|
120
|
+
'/usr/local/bin/claude',
|
|
121
|
+
'/opt/homebrew/bin/claude',
|
|
122
|
+
`${home}/.npm-global/bin/claude`,
|
|
123
|
+
`${home}/.bun/bin/claude`,
|
|
124
|
+
`${home}/.volta/bin/claude`,
|
|
125
|
+
`${home}/.nvm/current/bin/claude`,
|
|
126
|
+
];
|
|
127
|
+
for (const p of candidates) {
|
|
128
|
+
try {
|
|
129
|
+
if (fs.existsSync(p))
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Not found by any probe. Return null (NOT a bare 'claude' literal): a
|
|
136
|
+
// bare spawn would ENOENT and crash the daemon. The old fallback never
|
|
137
|
+
// actually worked — if PATH could run 'claude', the probes above (same
|
|
138
|
+
// env) would have found it.
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/** Encode a working directory the same way Claude does for its
|
|
142
|
+
* per-project transcript folder (`~/.claude/projects/<encoded>/`). */
|
|
143
|
+
function encodeProjectPath(dirPath) {
|
|
144
|
+
return dirPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make sure ~/.claude/settings.json has skipDangerousModePermissionPrompt: true.
|
|
3
|
+
* Idempotent: if the key is already true, no write.
|
|
4
|
+
*/
|
|
5
|
+
export declare function ensureBypassPermissionsAccepted(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Add an entry to ~/.claude.json `.projects[cwd]` marking the cwd as
|
|
8
|
+
* trusted and onboarding-seen, so claude TUI doesn't block on either
|
|
9
|
+
* dialog when launched in this directory. Existing fields are preserved.
|
|
10
|
+
*/
|
|
11
|
+
export declare function preTrustProject(cwd: string): void;
|