@agentconnect/host 0.2.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/dist/host.d.ts +36 -0
- package/dist/host.js +920 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/observed.d.ts +7 -0
- package/dist/observed.js +69 -0
- package/dist/providers/claude.d.ts +12 -0
- package/dist/providers/claude.js +1188 -0
- package/dist/providers/codex.d.ts +11 -0
- package/dist/providers/codex.js +908 -0
- package/dist/providers/cursor.d.ts +11 -0
- package/dist/providers/cursor.js +866 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +111 -0
- package/dist/providers/local.d.ts +9 -0
- package/dist/providers/local.js +114 -0
- package/dist/providers/utils.d.ts +33 -0
- package/dist/providers/utils.js +284 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { access, mkdir, readFile, rm, writeFile } from 'fs/promises';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { buildInstallCommand, buildInstallCommandAuto, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, debugLog, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
|
|
7
|
+
const CLAUDE_PACKAGE = '@anthropic-ai/claude-code';
|
|
8
|
+
const INSTALL_UNIX = 'curl -fsSL https://claude.ai/install.sh | bash';
|
|
9
|
+
const INSTALL_WINDOWS_PS = 'irm https://claude.ai/install.ps1 | iex';
|
|
10
|
+
const INSTALL_WINDOWS_CMD = 'curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd';
|
|
11
|
+
const DEFAULT_LOGIN = '';
|
|
12
|
+
const DEFAULT_STATUS = '';
|
|
13
|
+
const CLAUDE_MODELS_CACHE_TTL_MS = 60_000;
|
|
14
|
+
const CLAUDE_RECENT_MODELS_CACHE_TTL_MS = 60_000;
|
|
15
|
+
const CLAUDE_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
16
|
+
let claudeModelsCache = null;
|
|
17
|
+
let claudeModelsCacheAt = 0;
|
|
18
|
+
let claudeRecentModelsCache = null;
|
|
19
|
+
let claudeRecentModelsCacheAt = 0;
|
|
20
|
+
let claudeUpdateCache = null;
|
|
21
|
+
let claudeUpdatePromise = null;
|
|
22
|
+
const CLAUDE_LOGIN_CACHE_TTL_MS = 30_000;
|
|
23
|
+
let claudeLoginCache = null;
|
|
24
|
+
let claudeLoginPromise = null;
|
|
25
|
+
function trimOutput(value, limit = 400) {
|
|
26
|
+
const cleaned = value.trim();
|
|
27
|
+
if (!cleaned)
|
|
28
|
+
return '';
|
|
29
|
+
if (cleaned.length <= limit)
|
|
30
|
+
return cleaned;
|
|
31
|
+
return `${cleaned.slice(0, limit)}...`;
|
|
32
|
+
}
|
|
33
|
+
function normalizePath(value) {
|
|
34
|
+
const normalized = value.replace(/\\/g, '/');
|
|
35
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
36
|
+
}
|
|
37
|
+
function parseSemver(value) {
|
|
38
|
+
if (!value)
|
|
39
|
+
return null;
|
|
40
|
+
const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
41
|
+
if (!match)
|
|
42
|
+
return null;
|
|
43
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
44
|
+
}
|
|
45
|
+
function compareSemver(a, b) {
|
|
46
|
+
if (a[0] !== b[0])
|
|
47
|
+
return a[0] - b[0];
|
|
48
|
+
if (a[1] !== b[1])
|
|
49
|
+
return a[1] - b[1];
|
|
50
|
+
return a[2] - b[2];
|
|
51
|
+
}
|
|
52
|
+
async function fetchLatestNpmVersion(pkg) {
|
|
53
|
+
const encoded = encodeURIComponent(pkg);
|
|
54
|
+
const data = await fetchJson(`https://registry.npmjs.org/${encoded}`);
|
|
55
|
+
if (!data || typeof data !== 'object')
|
|
56
|
+
return null;
|
|
57
|
+
const latest = data['dist-tags']?.latest;
|
|
58
|
+
return typeof latest === 'string' ? latest : null;
|
|
59
|
+
}
|
|
60
|
+
async function fetchBrewCaskVersion(cask) {
|
|
61
|
+
if (!commandExists('brew'))
|
|
62
|
+
return null;
|
|
63
|
+
const result = await runCommand('brew', ['info', '--json=v2', '--cask', cask]);
|
|
64
|
+
if (result.code !== 0)
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(result.stdout);
|
|
68
|
+
const version = parsed?.casks?.[0]?.version;
|
|
69
|
+
return typeof version === 'string' ? version : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getClaudeUpdateAction(commandPath) {
|
|
76
|
+
if (!commandPath)
|
|
77
|
+
return null;
|
|
78
|
+
const normalized = normalizePath(commandPath);
|
|
79
|
+
const home = normalizePath(os.homedir());
|
|
80
|
+
if (normalized.startsWith(`${home}/.bun/bin/`)) {
|
|
81
|
+
return {
|
|
82
|
+
command: 'bun',
|
|
83
|
+
args: ['install', '-g', CLAUDE_PACKAGE],
|
|
84
|
+
source: 'bun',
|
|
85
|
+
commandLabel: 'bun install -g @anthropic-ai/claude-code',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (normalized.includes('/node_modules/.bin/')) {
|
|
89
|
+
return {
|
|
90
|
+
command: 'npm',
|
|
91
|
+
args: ['install', '-g', CLAUDE_PACKAGE],
|
|
92
|
+
source: 'npm',
|
|
93
|
+
commandLabel: 'npm install -g @anthropic-ai/claude-code',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (normalized.includes('/cellar/') ||
|
|
97
|
+
normalized.includes('/caskroom/') ||
|
|
98
|
+
normalized.includes('/homebrew/')) {
|
|
99
|
+
return {
|
|
100
|
+
command: 'brew',
|
|
101
|
+
args: ['upgrade', '--cask', 'claude-code'],
|
|
102
|
+
source: 'brew',
|
|
103
|
+
commandLabel: 'brew upgrade --cask claude-code',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (process.platform === 'win32' &&
|
|
107
|
+
(normalized.includes('/program files/claudecode') ||
|
|
108
|
+
normalized.includes('/programdata/claudecode'))) {
|
|
109
|
+
return {
|
|
110
|
+
command: 'winget',
|
|
111
|
+
args: ['upgrade', 'Anthropic.ClaudeCode'],
|
|
112
|
+
source: 'winget',
|
|
113
|
+
commandLabel: 'winget upgrade Anthropic.ClaudeCode',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (normalized.includes('/.local/bin/') ||
|
|
117
|
+
normalized.includes('/.local/share/claude/versions/') ||
|
|
118
|
+
normalized.includes('/.local/share/claude-code/versions/')) {
|
|
119
|
+
if (process.platform === 'win32') {
|
|
120
|
+
return {
|
|
121
|
+
command: 'powershell',
|
|
122
|
+
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', INSTALL_WINDOWS_PS],
|
|
123
|
+
source: 'script',
|
|
124
|
+
commandLabel: INSTALL_WINDOWS_PS,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
command: 'bash',
|
|
129
|
+
args: ['-lc', INSTALL_UNIX],
|
|
130
|
+
source: 'script',
|
|
131
|
+
commandLabel: INSTALL_UNIX,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const DEFAULT_CLAUDE_MODELS = [
|
|
137
|
+
{
|
|
138
|
+
id: 'default',
|
|
139
|
+
displayName: 'Default · Opus 4.5',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'sonnet',
|
|
143
|
+
displayName: 'Sonnet 4.5',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'haiku',
|
|
147
|
+
displayName: 'Haiku 4.5',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'opus',
|
|
151
|
+
displayName: 'Opus',
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
export function getClaudeCommand() {
|
|
155
|
+
const override = process.env.AGENTCONNECT_CLAUDE_COMMAND;
|
|
156
|
+
const base = override || 'claude';
|
|
157
|
+
const resolved = resolveCommandPath(base);
|
|
158
|
+
return resolved || resolveWindowsCommand(base);
|
|
159
|
+
}
|
|
160
|
+
function getClaudeConfigDir() {
|
|
161
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
162
|
+
}
|
|
163
|
+
function fetchJson(url) {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
https
|
|
166
|
+
.get(url, (res) => {
|
|
167
|
+
let data = '';
|
|
168
|
+
res.on('data', (chunk) => {
|
|
169
|
+
data += chunk;
|
|
170
|
+
});
|
|
171
|
+
res.on('end', () => {
|
|
172
|
+
try {
|
|
173
|
+
resolve(JSON.parse(data));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
resolve(null);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
})
|
|
180
|
+
.on('error', () => resolve(null));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function formatClaudeDisplayName(modelId) {
|
|
184
|
+
const value = modelId.trim();
|
|
185
|
+
if (!value.startsWith('claude-'))
|
|
186
|
+
return value;
|
|
187
|
+
const parts = value.replace(/^claude-/, '').split('-').filter(Boolean);
|
|
188
|
+
if (!parts.length)
|
|
189
|
+
return value;
|
|
190
|
+
const family = parts[0];
|
|
191
|
+
const numeric = parts.slice(1).filter((entry) => /^\d+$/.test(entry));
|
|
192
|
+
let version = '';
|
|
193
|
+
if (numeric.length >= 2) {
|
|
194
|
+
version = `${numeric[0]}.${numeric[1]}`;
|
|
195
|
+
}
|
|
196
|
+
else if (numeric.length === 1) {
|
|
197
|
+
version = numeric[0];
|
|
198
|
+
}
|
|
199
|
+
const familyLabel = family.charAt(0).toUpperCase() + family.slice(1);
|
|
200
|
+
return `Claude ${familyLabel}${version ? ` ${version}` : ''}`;
|
|
201
|
+
}
|
|
202
|
+
async function readClaudeStatsModels() {
|
|
203
|
+
const statsPath = path.join(getClaudeConfigDir(), 'stats-cache.json');
|
|
204
|
+
try {
|
|
205
|
+
const raw = await readFile(statsPath, 'utf8');
|
|
206
|
+
const parsed = JSON.parse(raw);
|
|
207
|
+
const usage = parsed?.modelUsage;
|
|
208
|
+
if (!usage || typeof usage !== 'object')
|
|
209
|
+
return [];
|
|
210
|
+
return Object.keys(usage).filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function listClaudeModels() {
|
|
217
|
+
if (claudeModelsCache && Date.now() - claudeModelsCacheAt < CLAUDE_MODELS_CACHE_TTL_MS) {
|
|
218
|
+
return claudeModelsCache;
|
|
219
|
+
}
|
|
220
|
+
const list = DEFAULT_CLAUDE_MODELS.map((entry) => ({
|
|
221
|
+
id: entry.id,
|
|
222
|
+
provider: 'claude',
|
|
223
|
+
displayName: entry.displayName,
|
|
224
|
+
}));
|
|
225
|
+
claudeModelsCache = list;
|
|
226
|
+
claudeModelsCacheAt = Date.now();
|
|
227
|
+
return list;
|
|
228
|
+
}
|
|
229
|
+
export async function listClaudeRecentModels() {
|
|
230
|
+
if (claudeRecentModelsCache &&
|
|
231
|
+
Date.now() - claudeRecentModelsCacheAt < CLAUDE_RECENT_MODELS_CACHE_TTL_MS) {
|
|
232
|
+
return claudeRecentModelsCache;
|
|
233
|
+
}
|
|
234
|
+
const discovered = await readClaudeStatsModels();
|
|
235
|
+
const mapped = [];
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
for (const modelId of discovered) {
|
|
238
|
+
const id = modelId.trim();
|
|
239
|
+
if (!id || seen.has(id))
|
|
240
|
+
continue;
|
|
241
|
+
seen.add(id);
|
|
242
|
+
mapped.push({
|
|
243
|
+
id,
|
|
244
|
+
provider: 'claude',
|
|
245
|
+
displayName: formatClaudeDisplayName(id),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
claudeRecentModelsCache = mapped;
|
|
249
|
+
claudeRecentModelsCacheAt = Date.now();
|
|
250
|
+
return mapped;
|
|
251
|
+
}
|
|
252
|
+
function getClaudeAuthPaths() {
|
|
253
|
+
const home = os.homedir();
|
|
254
|
+
return [
|
|
255
|
+
path.join(home, '.claude.json'),
|
|
256
|
+
path.join(home, '.claude', 'settings.json'),
|
|
257
|
+
path.join(home, '.config', 'claude', 'auth.json'),
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
function resolveClaudeTheme() {
|
|
261
|
+
const raw = process.env.AGENTCONNECT_CLAUDE_THEME;
|
|
262
|
+
return raw && raw.trim() ? raw.trim() : 'dark';
|
|
263
|
+
}
|
|
264
|
+
function resolveClaudeLoginMethod(options) {
|
|
265
|
+
const raw = options?.loginMethod ?? process.env.AGENTCONNECT_CLAUDE_LOGIN_METHOD;
|
|
266
|
+
if (!raw)
|
|
267
|
+
return 'claudeai';
|
|
268
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
269
|
+
if (normalized === 'console')
|
|
270
|
+
return 'console';
|
|
271
|
+
if (normalized === 'claudeai' || normalized === 'claude')
|
|
272
|
+
return 'claudeai';
|
|
273
|
+
return 'claudeai';
|
|
274
|
+
}
|
|
275
|
+
function resolveClaudeLoginExperience(options) {
|
|
276
|
+
const raw = options?.loginExperience ??
|
|
277
|
+
process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE ??
|
|
278
|
+
process.env.AGENTCONNECT_LOGIN_EXPERIENCE;
|
|
279
|
+
if (raw) {
|
|
280
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
281
|
+
if (normalized === 'terminal' || normalized === 'manual')
|
|
282
|
+
return 'terminal';
|
|
283
|
+
if (normalized === 'embedded' || normalized === 'pty')
|
|
284
|
+
return 'embedded';
|
|
285
|
+
}
|
|
286
|
+
if (process.env.AGENTCONNECT_HOST_MODE === 'dev')
|
|
287
|
+
return 'terminal';
|
|
288
|
+
return 'embedded';
|
|
289
|
+
}
|
|
290
|
+
async function resolveClaudeLoginHint(options) {
|
|
291
|
+
const raw = process.env.AGENTCONNECT_CLAUDE_LOGIN_HINT;
|
|
292
|
+
if (raw) {
|
|
293
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
294
|
+
if (normalized === 'setup')
|
|
295
|
+
return 'setup';
|
|
296
|
+
if (normalized === 'login')
|
|
297
|
+
return 'login';
|
|
298
|
+
}
|
|
299
|
+
if (options?.loginExperience) {
|
|
300
|
+
// no-op; keep for future overrides
|
|
301
|
+
}
|
|
302
|
+
const status = await checkClaudeCliStatus();
|
|
303
|
+
return status.loginHint ?? 'login';
|
|
304
|
+
}
|
|
305
|
+
async function createClaudeLoginSettingsFile(loginMethod) {
|
|
306
|
+
if (!loginMethod)
|
|
307
|
+
return null;
|
|
308
|
+
const fileName = `agentconnect-claude-login-${Date.now()}-${Math.random()
|
|
309
|
+
.toString(36)
|
|
310
|
+
.slice(2, 8)}.json`;
|
|
311
|
+
const filePath = path.join(os.tmpdir(), fileName);
|
|
312
|
+
const theme = resolveClaudeTheme();
|
|
313
|
+
const payload = {
|
|
314
|
+
forceLoginMethod: loginMethod,
|
|
315
|
+
theme,
|
|
316
|
+
hasCompletedOnboarding: true,
|
|
317
|
+
};
|
|
318
|
+
await writeFile(filePath, JSON.stringify(payload), 'utf8');
|
|
319
|
+
return filePath;
|
|
320
|
+
}
|
|
321
|
+
async function ensureClaudeOnboardingSettings() {
|
|
322
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
323
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
324
|
+
let settings = {};
|
|
325
|
+
try {
|
|
326
|
+
const raw = await readFile(settingsPath, 'utf8');
|
|
327
|
+
try {
|
|
328
|
+
settings = JSON.parse(raw);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
const code = err?.code;
|
|
336
|
+
if (code && code !== 'ENOENT')
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
let updated = false;
|
|
340
|
+
if (!settings.theme) {
|
|
341
|
+
settings.theme = resolveClaudeTheme();
|
|
342
|
+
updated = true;
|
|
343
|
+
}
|
|
344
|
+
if (settings.hasCompletedOnboarding !== true) {
|
|
345
|
+
settings.hasCompletedOnboarding = true;
|
|
346
|
+
updated = true;
|
|
347
|
+
}
|
|
348
|
+
if (!updated)
|
|
349
|
+
return;
|
|
350
|
+
await mkdir(configDir, { recursive: true });
|
|
351
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
352
|
+
}
|
|
353
|
+
async function loadPtyModule() {
|
|
354
|
+
try {
|
|
355
|
+
const mod = (await import('node-pty'));
|
|
356
|
+
if (typeof mod.spawn === 'function')
|
|
357
|
+
return mod;
|
|
358
|
+
if (mod.default && typeof mod.default.spawn === 'function')
|
|
359
|
+
return mod.default;
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function shellEscape(value) {
|
|
367
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
368
|
+
}
|
|
369
|
+
function cmdEscape(value) {
|
|
370
|
+
if (!value)
|
|
371
|
+
return '""';
|
|
372
|
+
const escaped = value.replace(/"/g, '""');
|
|
373
|
+
return `"${escaped}"`;
|
|
374
|
+
}
|
|
375
|
+
function buildClaudeCommand(command, args, includeLogin = false) {
|
|
376
|
+
const parts = includeLogin ? [command, ...args, '/login'] : [command, ...args];
|
|
377
|
+
return parts.map(shellEscape).join(' ');
|
|
378
|
+
}
|
|
379
|
+
function buildClaudeCmd(command, args, includeLogin = false) {
|
|
380
|
+
const parts = includeLogin ? [command, ...args, '/login'] : [command, ...args];
|
|
381
|
+
return parts.map(cmdEscape).join(' ');
|
|
382
|
+
}
|
|
383
|
+
async function openClaudeLoginTerminal(command, args, includeLogin = false) {
|
|
384
|
+
const message = 'AgentConnect: complete Claude login in this terminal. If login does not start automatically, run /login.';
|
|
385
|
+
if (process.platform === 'win32') {
|
|
386
|
+
const cmdLine = `echo ${message} & ${buildClaudeCmd(command, args, includeLogin)}`;
|
|
387
|
+
await runCommand('cmd', ['/c', 'start', '', 'cmd', '/k', cmdLine], { shell: true });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const loginCommand = buildClaudeCommand(command, args, includeLogin);
|
|
391
|
+
const shellCommand = `printf "%s\\n\\n" ${shellEscape(message)}; ${loginCommand}`;
|
|
392
|
+
if (process.platform === 'darwin') {
|
|
393
|
+
const script = `tell application "Terminal"
|
|
394
|
+
activate
|
|
395
|
+
do script "${shellCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"
|
|
396
|
+
end tell`;
|
|
397
|
+
await runCommand('osascript', ['-e', script]);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (commandExists('x-terminal-emulator')) {
|
|
401
|
+
await runCommand('x-terminal-emulator', ['-e', 'bash', '-lc', shellCommand]);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (commandExists('gnome-terminal')) {
|
|
405
|
+
await runCommand('gnome-terminal', ['--', 'bash', '-lc', shellCommand]);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (commandExists('konsole')) {
|
|
409
|
+
await runCommand('konsole', ['-e', 'bash', '-lc', shellCommand]);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (commandExists('xterm')) {
|
|
413
|
+
await runCommand('xterm', ['-e', 'bash', '-lc', shellCommand]);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
throw new Error('No terminal emulator found to launch Claude login.');
|
|
417
|
+
}
|
|
418
|
+
function maybeAdvanceClaudeOnboarding(output, loginMethod, write) {
|
|
419
|
+
const text = output.toLowerCase();
|
|
420
|
+
if (text.includes('select login method') || text.includes('claude account with subscription')) {
|
|
421
|
+
if (loginMethod === 'console') {
|
|
422
|
+
write('\x1b[B');
|
|
423
|
+
}
|
|
424
|
+
write('\r');
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (text.includes('choose the text style') || text.includes('text style that looks best')) {
|
|
428
|
+
write('\r');
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
if (text.includes('press enter') || text.includes('enter to confirm')) {
|
|
432
|
+
write('\r');
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
async function hasClaudeAuth() {
|
|
438
|
+
if (typeof process.env.CLAUDE_CODE_OAUTH_TOKEN === 'string') {
|
|
439
|
+
return process.env.CLAUDE_CODE_OAUTH_TOKEN.trim().length > 0;
|
|
440
|
+
}
|
|
441
|
+
for (const filePath of getClaudeAuthPaths()) {
|
|
442
|
+
try {
|
|
443
|
+
await access(filePath);
|
|
444
|
+
const raw = await readFile(filePath, 'utf8');
|
|
445
|
+
const parsed = JSON.parse(raw);
|
|
446
|
+
const auth = parsed?.claudeAiOauth;
|
|
447
|
+
if (typeof auth?.accessToken === 'string' && auth.accessToken.trim()) {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
if (typeof parsed.primaryApiKey === 'string' && parsed.primaryApiKey.trim()) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
if (typeof parsed.accessToken === 'string' && parsed.accessToken.trim()) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
if (typeof parsed.token === 'string' && parsed.token.trim()) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
const oauthAccount = parsed.oauthAccount;
|
|
460
|
+
if (typeof oauthAccount?.emailAddress === 'string' && oauthAccount.emailAddress.trim()) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (typeof oauthAccount?.accountUuid === 'string' && oauthAccount.accountUuid.trim()) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
const oauth = parsed.oauth;
|
|
467
|
+
if (typeof oauth?.access_token === 'string' && oauth.access_token.trim()) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// try next path
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
function isClaudeAuthErrorText(value) {
|
|
478
|
+
const text = value.toLowerCase();
|
|
479
|
+
return (text.includes('authentication_error') ||
|
|
480
|
+
text.includes('authentication_failed') ||
|
|
481
|
+
text.includes('oauth token has expired') ||
|
|
482
|
+
text.includes('token has expired') ||
|
|
483
|
+
text.includes('please run /login') ||
|
|
484
|
+
text.includes('unauthorized') ||
|
|
485
|
+
text.includes('api error: 401') ||
|
|
486
|
+
text.includes('status 401') ||
|
|
487
|
+
text.includes('invalid api key'));
|
|
488
|
+
}
|
|
489
|
+
function extractClaudeMessageText(content) {
|
|
490
|
+
if (Array.isArray(content)) {
|
|
491
|
+
return content.map((part) => part?.text ?? '').join(' ');
|
|
492
|
+
}
|
|
493
|
+
return typeof content === 'string' ? content : '';
|
|
494
|
+
}
|
|
495
|
+
function resolveClaudeLoginHintFromSource(apiKeySource) {
|
|
496
|
+
if (apiKeySource && apiKeySource.toLowerCase() === 'none')
|
|
497
|
+
return 'setup';
|
|
498
|
+
return 'login';
|
|
499
|
+
}
|
|
500
|
+
async function checkClaudeCliStatus() {
|
|
501
|
+
if (claudeLoginCache && Date.now() - claudeLoginCache.checkedAt < CLAUDE_LOGIN_CACHE_TTL_MS) {
|
|
502
|
+
return claudeLoginCache.status;
|
|
503
|
+
}
|
|
504
|
+
if (claudeLoginPromise)
|
|
505
|
+
return claudeLoginPromise;
|
|
506
|
+
claudeLoginPromise = (async () => {
|
|
507
|
+
const command = resolveWindowsCommand(getClaudeCommand());
|
|
508
|
+
const args = [
|
|
509
|
+
'--print',
|
|
510
|
+
'--output-format',
|
|
511
|
+
'stream-json',
|
|
512
|
+
'--no-session-persistence',
|
|
513
|
+
'--max-budget-usd',
|
|
514
|
+
'0.01',
|
|
515
|
+
];
|
|
516
|
+
const result = await runCommand(command, args, {
|
|
517
|
+
env: { ...process.env, CI: '1' },
|
|
518
|
+
input: 'ping\n',
|
|
519
|
+
timeoutMs: 8000,
|
|
520
|
+
});
|
|
521
|
+
const output = `${result.stdout}\n${result.stderr}`.trim();
|
|
522
|
+
if (!output)
|
|
523
|
+
return { loggedIn: null };
|
|
524
|
+
let apiKeySource;
|
|
525
|
+
let authError = null;
|
|
526
|
+
let sawAssistant = false;
|
|
527
|
+
let sawSuccess = false;
|
|
528
|
+
const lines = output.split('\n');
|
|
529
|
+
for (const line of lines) {
|
|
530
|
+
const parsed = safeJsonParse(line);
|
|
531
|
+
if (!parsed || typeof parsed !== 'object')
|
|
532
|
+
continue;
|
|
533
|
+
const record = parsed;
|
|
534
|
+
const type = typeof record.type === 'string' ? record.type : '';
|
|
535
|
+
if (type === 'system' && record.subtype === 'init') {
|
|
536
|
+
const source = typeof record.apiKeySource === 'string' ? record.apiKeySource : undefined;
|
|
537
|
+
if (source)
|
|
538
|
+
apiKeySource = source;
|
|
539
|
+
}
|
|
540
|
+
if (type === 'result') {
|
|
541
|
+
const isError = Boolean(record.is_error);
|
|
542
|
+
const resultText = typeof record.result === 'string'
|
|
543
|
+
? record.result
|
|
544
|
+
: typeof record.error === 'string'
|
|
545
|
+
? record.error
|
|
546
|
+
: JSON.stringify(record.error || record);
|
|
547
|
+
if (isError && isClaudeAuthErrorText(resultText)) {
|
|
548
|
+
authError = authError ?? resultText;
|
|
549
|
+
}
|
|
550
|
+
if (!isError)
|
|
551
|
+
sawSuccess = true;
|
|
552
|
+
}
|
|
553
|
+
if (type === 'message') {
|
|
554
|
+
const message = record.message;
|
|
555
|
+
const role = typeof message?.role === 'string' ? message.role : '';
|
|
556
|
+
if (role === 'assistant') {
|
|
557
|
+
const text = extractClaudeMessageText(message?.content);
|
|
558
|
+
const errorText = typeof record.error === 'string'
|
|
559
|
+
? record.error
|
|
560
|
+
: typeof message?.error === 'string'
|
|
561
|
+
? message.error
|
|
562
|
+
: '';
|
|
563
|
+
if (isClaudeAuthErrorText(text) ||
|
|
564
|
+
(errorText && isClaudeAuthErrorText(errorText))) {
|
|
565
|
+
authError = authError ?? (text || errorText);
|
|
566
|
+
}
|
|
567
|
+
else if (text.trim()) {
|
|
568
|
+
sawAssistant = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (authError) {
|
|
574
|
+
return {
|
|
575
|
+
loggedIn: false,
|
|
576
|
+
apiKeySource,
|
|
577
|
+
loginHint: resolveClaudeLoginHintFromSource(apiKeySource),
|
|
578
|
+
authError,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (sawAssistant || sawSuccess) {
|
|
582
|
+
return { loggedIn: true, apiKeySource };
|
|
583
|
+
}
|
|
584
|
+
if (apiKeySource && apiKeySource.toLowerCase() === 'none') {
|
|
585
|
+
return { loggedIn: false, apiKeySource, loginHint: 'setup' };
|
|
586
|
+
}
|
|
587
|
+
return { loggedIn: null, apiKeySource };
|
|
588
|
+
})()
|
|
589
|
+
.then((status) => {
|
|
590
|
+
claudeLoginCache = { checkedAt: Date.now(), status };
|
|
591
|
+
return status;
|
|
592
|
+
})
|
|
593
|
+
.finally(() => {
|
|
594
|
+
claudeLoginPromise = null;
|
|
595
|
+
});
|
|
596
|
+
return claudeLoginPromise;
|
|
597
|
+
}
|
|
598
|
+
function getClaudeUpdateSnapshot(commandPath) {
|
|
599
|
+
if (claudeUpdateCache && Date.now() - claudeUpdateCache.checkedAt < CLAUDE_UPDATE_CACHE_TTL_MS) {
|
|
600
|
+
const action = getClaudeUpdateAction(commandPath);
|
|
601
|
+
return {
|
|
602
|
+
updateAvailable: claudeUpdateCache.updateAvailable,
|
|
603
|
+
latestVersion: claudeUpdateCache.latestVersion,
|
|
604
|
+
updateCheckedAt: claudeUpdateCache.checkedAt,
|
|
605
|
+
updateSource: action?.source ?? 'unknown',
|
|
606
|
+
updateCommand: action?.commandLabel,
|
|
607
|
+
updateMessage: claudeUpdateCache.updateMessage,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return {};
|
|
611
|
+
}
|
|
612
|
+
function ensureClaudeUpdateCheck(currentVersion, commandPath) {
|
|
613
|
+
if (claudeUpdateCache && Date.now() - claudeUpdateCache.checkedAt < CLAUDE_UPDATE_CACHE_TTL_MS) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (claudeUpdatePromise)
|
|
617
|
+
return;
|
|
618
|
+
claudeUpdatePromise = (async () => {
|
|
619
|
+
const action = getClaudeUpdateAction(commandPath || null);
|
|
620
|
+
let latest = null;
|
|
621
|
+
let updateAvailable;
|
|
622
|
+
let updateMessage;
|
|
623
|
+
if (action?.source === 'npm' || action?.source === 'bun') {
|
|
624
|
+
latest = await fetchLatestNpmVersion(CLAUDE_PACKAGE);
|
|
625
|
+
}
|
|
626
|
+
else if (action?.source === 'brew') {
|
|
627
|
+
latest = await fetchBrewCaskVersion('claude-code');
|
|
628
|
+
}
|
|
629
|
+
if (latest && currentVersion) {
|
|
630
|
+
const a = parseSemver(currentVersion);
|
|
631
|
+
const b = parseSemver(latest);
|
|
632
|
+
if (a && b) {
|
|
633
|
+
updateAvailable = compareSemver(a, b) < 0;
|
|
634
|
+
updateMessage = updateAvailable
|
|
635
|
+
? `Update available: ${currentVersion} -> ${latest}`
|
|
636
|
+
: `Up to date (${currentVersion})`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else if (!action) {
|
|
640
|
+
updateMessage = 'Update check unavailable';
|
|
641
|
+
}
|
|
642
|
+
debugLog('Claude', 'update-check', {
|
|
643
|
+
updateAvailable,
|
|
644
|
+
message: updateMessage,
|
|
645
|
+
});
|
|
646
|
+
claudeUpdateCache = {
|
|
647
|
+
checkedAt: Date.now(),
|
|
648
|
+
updateAvailable,
|
|
649
|
+
latestVersion: latest ?? undefined,
|
|
650
|
+
updateMessage,
|
|
651
|
+
};
|
|
652
|
+
})().finally(() => {
|
|
653
|
+
claudeUpdatePromise = null;
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
export async function ensureClaudeInstalled() {
|
|
657
|
+
const command = getClaudeCommand();
|
|
658
|
+
const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
|
|
659
|
+
if (versionCheck.ok) {
|
|
660
|
+
return { installed: true, version: versionCheck.version || undefined };
|
|
661
|
+
}
|
|
662
|
+
if (commandExists(command)) {
|
|
663
|
+
return { installed: true, version: undefined };
|
|
664
|
+
}
|
|
665
|
+
const override = buildInstallCommand('AGENTCONNECT_CLAUDE_INSTALL', '');
|
|
666
|
+
let install = override;
|
|
667
|
+
let packageManager = override.command ? 'unknown' : 'unknown';
|
|
668
|
+
if (!install.command) {
|
|
669
|
+
if (process.platform === 'win32') {
|
|
670
|
+
if (commandExists('powershell')) {
|
|
671
|
+
install = {
|
|
672
|
+
command: 'powershell',
|
|
673
|
+
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', INSTALL_WINDOWS_PS],
|
|
674
|
+
};
|
|
675
|
+
packageManager = 'script';
|
|
676
|
+
}
|
|
677
|
+
else if (commandExists('pwsh')) {
|
|
678
|
+
install = {
|
|
679
|
+
command: 'pwsh',
|
|
680
|
+
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', INSTALL_WINDOWS_PS],
|
|
681
|
+
};
|
|
682
|
+
packageManager = 'script';
|
|
683
|
+
}
|
|
684
|
+
else if (commandExists('cmd') && commandExists('curl')) {
|
|
685
|
+
install = { command: 'cmd', args: ['/c', INSTALL_WINDOWS_CMD] };
|
|
686
|
+
packageManager = 'script';
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
else if (commandExists('bash') && commandExists('curl')) {
|
|
690
|
+
install = { command: 'bash', args: ['-lc', INSTALL_UNIX] };
|
|
691
|
+
packageManager = 'script';
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
const auto = await buildInstallCommandAuto(CLAUDE_PACKAGE);
|
|
695
|
+
install = { command: auto.command, args: auto.args };
|
|
696
|
+
packageManager = auto.packageManager;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (!install.command) {
|
|
700
|
+
return { installed: false, version: undefined, packageManager };
|
|
701
|
+
}
|
|
702
|
+
await runCommand(install.command, install.args, { shell: process.platform === 'win32' });
|
|
703
|
+
const after = await checkCommandVersion(command, [['--version'], ['-V']]);
|
|
704
|
+
return {
|
|
705
|
+
installed: after.ok,
|
|
706
|
+
version: after.version || undefined,
|
|
707
|
+
packageManager,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
export async function getClaudeStatus() {
|
|
711
|
+
const command = getClaudeCommand();
|
|
712
|
+
const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
|
|
713
|
+
const installed = versionCheck.ok || commandExists(command);
|
|
714
|
+
let loggedIn = false;
|
|
715
|
+
if (installed) {
|
|
716
|
+
const status = buildStatusCommand('AGENTCONNECT_CLAUDE_STATUS', DEFAULT_STATUS);
|
|
717
|
+
if (status.command) {
|
|
718
|
+
const statusCommand = resolveWindowsCommand(status.command);
|
|
719
|
+
const result = await runCommand(statusCommand, status.args);
|
|
720
|
+
loggedIn = result.code === 0;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const cliStatus = await checkClaudeCliStatus();
|
|
724
|
+
if (cliStatus.loggedIn === false) {
|
|
725
|
+
loggedIn = false;
|
|
726
|
+
}
|
|
727
|
+
else if (cliStatus.loggedIn === true) {
|
|
728
|
+
loggedIn = true;
|
|
729
|
+
}
|
|
730
|
+
else if (cliStatus.apiKeySource?.toLowerCase() === 'none') {
|
|
731
|
+
loggedIn = false;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
loggedIn = await hasClaudeAuth();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (installed) {
|
|
739
|
+
const resolved = resolveCommandRealPath(command);
|
|
740
|
+
ensureClaudeUpdateCheck(versionCheck.version, resolved || null);
|
|
741
|
+
}
|
|
742
|
+
const resolved = resolveCommandRealPath(command);
|
|
743
|
+
const updateInfo = installed ? getClaudeUpdateSnapshot(resolved || null) : {};
|
|
744
|
+
return { installed, loggedIn, version: versionCheck.version || undefined, ...updateInfo };
|
|
745
|
+
}
|
|
746
|
+
export async function getClaudeFastStatus() {
|
|
747
|
+
const command = getClaudeCommand();
|
|
748
|
+
const installed = commandExists(command);
|
|
749
|
+
const loggedIn = installed ? await hasClaudeAuth() : false;
|
|
750
|
+
return { installed, loggedIn };
|
|
751
|
+
}
|
|
752
|
+
export async function updateClaude() {
|
|
753
|
+
const command = getClaudeCommand();
|
|
754
|
+
if (!commandExists(command)) {
|
|
755
|
+
return { installed: false, loggedIn: false };
|
|
756
|
+
}
|
|
757
|
+
const resolved = resolveCommandRealPath(command);
|
|
758
|
+
const updateOverride = buildStatusCommand('AGENTCONNECT_CLAUDE_UPDATE', '');
|
|
759
|
+
const action = updateOverride.command ? null : getClaudeUpdateAction(resolved || null);
|
|
760
|
+
const updateCommand = updateOverride.command || action?.command || '';
|
|
761
|
+
const updateArgs = updateOverride.command ? updateOverride.args : action?.args || [];
|
|
762
|
+
if (!updateCommand) {
|
|
763
|
+
throw new Error('No update command available. Please update Claude manually.');
|
|
764
|
+
}
|
|
765
|
+
const cmd = resolveWindowsCommand(updateCommand);
|
|
766
|
+
debugLog('Claude', 'update-run', { command: cmd, args: updateArgs });
|
|
767
|
+
const result = await runCommand(cmd, updateArgs, { env: { ...process.env, CI: '1' } });
|
|
768
|
+
debugLog('Claude', 'update-result', {
|
|
769
|
+
code: result.code,
|
|
770
|
+
stdout: trimOutput(result.stdout),
|
|
771
|
+
stderr: trimOutput(result.stderr),
|
|
772
|
+
});
|
|
773
|
+
if (result.code !== 0 && result.code !== null) {
|
|
774
|
+
const message = trimOutput(`${result.stdout}\n${result.stderr}`, 800) || 'Update failed';
|
|
775
|
+
throw new Error(message);
|
|
776
|
+
}
|
|
777
|
+
claudeUpdateCache = null;
|
|
778
|
+
claudeUpdatePromise = null;
|
|
779
|
+
return getClaudeStatus();
|
|
780
|
+
}
|
|
781
|
+
export async function loginClaude(options) {
|
|
782
|
+
const login = buildLoginCommand('AGENTCONNECT_CLAUDE_LOGIN', DEFAULT_LOGIN);
|
|
783
|
+
if (login.command) {
|
|
784
|
+
const command = resolveWindowsCommand(login.command);
|
|
785
|
+
await runCommand(command, login.args);
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
await runClaudeLoginFlow(options);
|
|
789
|
+
}
|
|
790
|
+
const status = await getClaudeStatus();
|
|
791
|
+
return { loggedIn: status.loggedIn };
|
|
792
|
+
}
|
|
793
|
+
async function runClaudeLoginFlow(options) {
|
|
794
|
+
const command = resolveWindowsCommand(getClaudeCommand());
|
|
795
|
+
const loginMethod = resolveClaudeLoginMethod(options);
|
|
796
|
+
const loginExperience = resolveClaudeLoginExperience(options);
|
|
797
|
+
const loginHint = await resolveClaudeLoginHint(options);
|
|
798
|
+
await ensureClaudeOnboardingSettings();
|
|
799
|
+
const settingsPath = await createClaudeLoginSettingsFile(loginMethod);
|
|
800
|
+
const loginTimeoutMs = Number(process.env.AGENTCONNECT_CLAUDE_LOGIN_TIMEOUT_MS || 180_000);
|
|
801
|
+
const loginArgs = settingsPath ? ['--settings', settingsPath] : [];
|
|
802
|
+
const includeLogin = loginHint === 'login';
|
|
803
|
+
let ptyProcess = null;
|
|
804
|
+
let childExited = false;
|
|
805
|
+
const cleanup = async () => {
|
|
806
|
+
if (ptyProcess) {
|
|
807
|
+
try {
|
|
808
|
+
ptyProcess.kill();
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
// ignore
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (settingsPath && loginExperience !== 'terminal') {
|
|
815
|
+
try {
|
|
816
|
+
await rm(settingsPath, { force: true });
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
// ignore
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
try {
|
|
824
|
+
if (loginExperience === 'terminal') {
|
|
825
|
+
await openClaudeLoginTerminal(command, loginArgs, includeLogin);
|
|
826
|
+
if (settingsPath) {
|
|
827
|
+
setTimeout(() => {
|
|
828
|
+
rm(settingsPath, { force: true }).catch(() => { });
|
|
829
|
+
}, loginTimeoutMs);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
const ptyModule = await loadPtyModule();
|
|
834
|
+
if (!ptyModule) {
|
|
835
|
+
throw new Error('Claude login requires node-pty. Reinstall AgentConnect or run `claude /login` manually.');
|
|
836
|
+
}
|
|
837
|
+
const spawnArgs = includeLogin ? [...loginArgs, '/login'] : loginArgs;
|
|
838
|
+
ptyProcess = ptyModule.spawn(command, spawnArgs, {
|
|
839
|
+
name: 'xterm-256color',
|
|
840
|
+
cols: 100,
|
|
841
|
+
rows: 30,
|
|
842
|
+
cwd: os.homedir(),
|
|
843
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
844
|
+
});
|
|
845
|
+
let outputBuffer = '';
|
|
846
|
+
ptyProcess.onData((data) => {
|
|
847
|
+
outputBuffer += data;
|
|
848
|
+
if (outputBuffer.length > 6000) {
|
|
849
|
+
outputBuffer = outputBuffer.slice(-3000);
|
|
850
|
+
}
|
|
851
|
+
const advanced = maybeAdvanceClaudeOnboarding(outputBuffer, loginMethod, (input) => {
|
|
852
|
+
ptyProcess?.write(input);
|
|
853
|
+
});
|
|
854
|
+
if (advanced)
|
|
855
|
+
outputBuffer = '';
|
|
856
|
+
});
|
|
857
|
+
ptyProcess.onExit(() => {
|
|
858
|
+
childExited = true;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
const start = Date.now();
|
|
862
|
+
while (Date.now() - start < loginTimeoutMs) {
|
|
863
|
+
const authed = await hasClaudeAuth();
|
|
864
|
+
if (authed) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (childExited) {
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
871
|
+
}
|
|
872
|
+
throw new Error('Claude login timed out. Finish login in your browser or run `claude` manually to authenticate.');
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
await cleanup();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function safeJsonParse(line) {
|
|
879
|
+
try {
|
|
880
|
+
return JSON.parse(line);
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function mapClaudeModel(model) {
|
|
887
|
+
if (!model)
|
|
888
|
+
return null;
|
|
889
|
+
const value = String(model).toLowerCase();
|
|
890
|
+
if (value === 'default' || value === 'recommended')
|
|
891
|
+
return null;
|
|
892
|
+
if (value === 'claude-default' || value === 'claude-recommended')
|
|
893
|
+
return null;
|
|
894
|
+
if (value.includes('opus'))
|
|
895
|
+
return 'opus';
|
|
896
|
+
if (value.includes('sonnet'))
|
|
897
|
+
return 'sonnet';
|
|
898
|
+
if (value.includes('haiku'))
|
|
899
|
+
return 'haiku';
|
|
900
|
+
if (value.startsWith('claude-'))
|
|
901
|
+
return value.replace('claude-', '');
|
|
902
|
+
return model;
|
|
903
|
+
}
|
|
904
|
+
function extractSessionId(msg) {
|
|
905
|
+
const direct = msg.session_id ?? msg.sessionId;
|
|
906
|
+
if (typeof direct === 'string' && direct)
|
|
907
|
+
return direct;
|
|
908
|
+
const nested = msg.message?.session_id ?? msg.message?.sessionId;
|
|
909
|
+
return typeof nested === 'string' && nested ? nested : null;
|
|
910
|
+
}
|
|
911
|
+
function extractAssistantDelta(msg) {
|
|
912
|
+
const type = String(msg.type ?? '');
|
|
913
|
+
if (type === 'stream_event') {
|
|
914
|
+
const ev = msg.event ?? {};
|
|
915
|
+
if (ev.type === 'content_block_delta') {
|
|
916
|
+
const text = ev.delta?.text;
|
|
917
|
+
return typeof text === 'string' && text ? text : null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (type === 'content_block_delta') {
|
|
921
|
+
const text = msg.delta?.text;
|
|
922
|
+
return typeof text === 'string' && text ? text : null;
|
|
923
|
+
}
|
|
924
|
+
const text = msg.delta?.text;
|
|
925
|
+
return typeof text === 'string' && text ? text : null;
|
|
926
|
+
}
|
|
927
|
+
function extractTextFromContent(content) {
|
|
928
|
+
if (!content)
|
|
929
|
+
return '';
|
|
930
|
+
if (typeof content === 'string')
|
|
931
|
+
return content;
|
|
932
|
+
if (Array.isArray(content)) {
|
|
933
|
+
return content
|
|
934
|
+
.map((part) => {
|
|
935
|
+
if (!part || typeof part !== 'object')
|
|
936
|
+
return '';
|
|
937
|
+
const text = part.text;
|
|
938
|
+
if (typeof text === 'string')
|
|
939
|
+
return text;
|
|
940
|
+
return '';
|
|
941
|
+
})
|
|
942
|
+
.filter(Boolean)
|
|
943
|
+
.join('');
|
|
944
|
+
}
|
|
945
|
+
return '';
|
|
946
|
+
}
|
|
947
|
+
function extractAssistantText(msg) {
|
|
948
|
+
if (String(msg.type ?? '') !== 'assistant')
|
|
949
|
+
return null;
|
|
950
|
+
const content = msg.message?.content;
|
|
951
|
+
if (!Array.isArray(content))
|
|
952
|
+
return null;
|
|
953
|
+
const textBlock = content.find((c) => c && typeof c === 'object' && c.type === 'text');
|
|
954
|
+
const text = textBlock?.text;
|
|
955
|
+
return typeof text === 'string' && text ? text : null;
|
|
956
|
+
}
|
|
957
|
+
function extractResultText(msg) {
|
|
958
|
+
if (String(msg.type ?? '') !== 'result')
|
|
959
|
+
return null;
|
|
960
|
+
const text = msg.result;
|
|
961
|
+
return typeof text === 'string' && text ? text : null;
|
|
962
|
+
}
|
|
963
|
+
export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerDetailLevel, onEvent, signal, }) {
|
|
964
|
+
return new Promise((resolve) => {
|
|
965
|
+
const command = getClaudeCommand();
|
|
966
|
+
const args = [
|
|
967
|
+
'-p',
|
|
968
|
+
'--output-format=stream-json',
|
|
969
|
+
'--verbose',
|
|
970
|
+
'--permission-mode',
|
|
971
|
+
'bypassPermissions',
|
|
972
|
+
];
|
|
973
|
+
const modelValue = mapClaudeModel(model);
|
|
974
|
+
if (modelValue) {
|
|
975
|
+
args.push('--model', modelValue);
|
|
976
|
+
}
|
|
977
|
+
if (resumeSessionId)
|
|
978
|
+
args.push('--resume', resumeSessionId);
|
|
979
|
+
args.push(prompt);
|
|
980
|
+
const child = spawn(command, args, {
|
|
981
|
+
cwd,
|
|
982
|
+
env: { ...process.env },
|
|
983
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
984
|
+
});
|
|
985
|
+
if (signal) {
|
|
986
|
+
signal.addEventListener('abort', () => {
|
|
987
|
+
child.kill('SIGTERM');
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
let aggregated = '';
|
|
991
|
+
let finalSessionId = null;
|
|
992
|
+
let didFinalize = false;
|
|
993
|
+
let sawError = false;
|
|
994
|
+
const toolBlocks = new Map();
|
|
995
|
+
const thinkingBlocks = new Set();
|
|
996
|
+
const includeRaw = providerDetailLevel === 'raw';
|
|
997
|
+
const buildProviderDetail = (eventType, data, raw) => {
|
|
998
|
+
const detail = { eventType };
|
|
999
|
+
if (data && Object.keys(data).length)
|
|
1000
|
+
detail.data = data;
|
|
1001
|
+
if (includeRaw && raw !== undefined)
|
|
1002
|
+
detail.raw = raw;
|
|
1003
|
+
return detail;
|
|
1004
|
+
};
|
|
1005
|
+
const emit = (event) => {
|
|
1006
|
+
if (finalSessionId) {
|
|
1007
|
+
onEvent({ ...event, providerSessionId: finalSessionId });
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
onEvent(event);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
const emitError = (message) => {
|
|
1014
|
+
if (sawError)
|
|
1015
|
+
return;
|
|
1016
|
+
sawError = true;
|
|
1017
|
+
emit({ type: 'error', message });
|
|
1018
|
+
};
|
|
1019
|
+
const emitFinal = (text, providerDetail) => {
|
|
1020
|
+
emit({ type: 'final', text, providerDetail });
|
|
1021
|
+
};
|
|
1022
|
+
const handleLine = (line) => {
|
|
1023
|
+
const parsed = safeJsonParse(line);
|
|
1024
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1025
|
+
if (line.trim()) {
|
|
1026
|
+
emit({ type: 'raw_line', line });
|
|
1027
|
+
}
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const msg = parsed;
|
|
1031
|
+
const sid = extractSessionId(msg);
|
|
1032
|
+
if (sid)
|
|
1033
|
+
finalSessionId = sid;
|
|
1034
|
+
const msgType = String(msg.type ?? '');
|
|
1035
|
+
let handled = false;
|
|
1036
|
+
if (msgType === 'assistant' || msgType === 'user' || msgType === 'system') {
|
|
1037
|
+
const role = msg.message?.role === 'assistant' ||
|
|
1038
|
+
msg.message?.role === 'user' ||
|
|
1039
|
+
msg.message?.role === 'system'
|
|
1040
|
+
? msg.message.role
|
|
1041
|
+
: msgType;
|
|
1042
|
+
const rawContent = msg.message?.content;
|
|
1043
|
+
const content = extractTextFromContent(rawContent);
|
|
1044
|
+
emit({
|
|
1045
|
+
type: 'message',
|
|
1046
|
+
provider: 'claude',
|
|
1047
|
+
role,
|
|
1048
|
+
content,
|
|
1049
|
+
contentParts: rawContent ?? null,
|
|
1050
|
+
providerDetail: buildProviderDetail(msgType, {}, msg),
|
|
1051
|
+
});
|
|
1052
|
+
handled = true;
|
|
1053
|
+
}
|
|
1054
|
+
if (msgType === 'stream_event' && msg.event) {
|
|
1055
|
+
const evType = String(msg.event.type ?? '');
|
|
1056
|
+
const index = typeof msg.event.index === 'number' ? msg.event.index : undefined;
|
|
1057
|
+
const block = msg.event.content_block;
|
|
1058
|
+
if (evType === 'content_block_start' && block && typeof index === 'number') {
|
|
1059
|
+
if (block.type === 'tool_use' || block.type === 'server_tool_use' || block.type === 'mcp_tool_use') {
|
|
1060
|
+
toolBlocks.set(index, { id: block.id, name: block.name });
|
|
1061
|
+
emit({
|
|
1062
|
+
type: 'tool_call',
|
|
1063
|
+
provider: 'claude',
|
|
1064
|
+
name: block.name,
|
|
1065
|
+
callId: block.id,
|
|
1066
|
+
input: block.input,
|
|
1067
|
+
phase: 'start',
|
|
1068
|
+
providerDetail: buildProviderDetail('content_block_start', { blockType: block.type, index, name: block.name, id: block.id }, msg),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (block.type === 'thinking' || block.type === 'redacted_thinking') {
|
|
1072
|
+
thinkingBlocks.add(index);
|
|
1073
|
+
emit({
|
|
1074
|
+
type: 'thinking',
|
|
1075
|
+
provider: 'claude',
|
|
1076
|
+
phase: 'start',
|
|
1077
|
+
text: typeof block.thinking === 'string' ? block.thinking : undefined,
|
|
1078
|
+
providerDetail: buildProviderDetail('content_block_start', { blockType: block.type, index }, msg),
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
handled = true;
|
|
1082
|
+
}
|
|
1083
|
+
if (evType === 'content_block_delta') {
|
|
1084
|
+
const delta = msg.event.delta ?? {};
|
|
1085
|
+
if (delta.type === 'thinking_delta') {
|
|
1086
|
+
emit({
|
|
1087
|
+
type: 'thinking',
|
|
1088
|
+
provider: 'claude',
|
|
1089
|
+
phase: 'delta',
|
|
1090
|
+
text: typeof delta.thinking === 'string' ? delta.thinking : undefined,
|
|
1091
|
+
providerDetail: buildProviderDetail('content_block_delta', { deltaType: delta.type, index }, msg),
|
|
1092
|
+
});
|
|
1093
|
+
handled = true;
|
|
1094
|
+
}
|
|
1095
|
+
if (delta.type === 'input_json_delta') {
|
|
1096
|
+
const tool = typeof index === 'number' ? toolBlocks.get(index) : undefined;
|
|
1097
|
+
emit({
|
|
1098
|
+
type: 'tool_call',
|
|
1099
|
+
provider: 'claude',
|
|
1100
|
+
name: tool?.name,
|
|
1101
|
+
callId: tool?.id,
|
|
1102
|
+
input: delta.partial_json,
|
|
1103
|
+
phase: 'delta',
|
|
1104
|
+
providerDetail: buildProviderDetail('content_block_delta', { deltaType: delta.type, index, name: tool?.name, id: tool?.id }, msg),
|
|
1105
|
+
});
|
|
1106
|
+
handled = true;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (evType === 'content_block_stop' && typeof index === 'number') {
|
|
1110
|
+
if (toolBlocks.has(index)) {
|
|
1111
|
+
const tool = toolBlocks.get(index);
|
|
1112
|
+
emit({
|
|
1113
|
+
type: 'tool_call',
|
|
1114
|
+
provider: 'claude',
|
|
1115
|
+
name: tool?.name,
|
|
1116
|
+
callId: tool?.id,
|
|
1117
|
+
phase: 'completed',
|
|
1118
|
+
providerDetail: buildProviderDetail('content_block_stop', { index, name: tool?.name, id: tool?.id }, msg),
|
|
1119
|
+
});
|
|
1120
|
+
toolBlocks.delete(index);
|
|
1121
|
+
}
|
|
1122
|
+
if (thinkingBlocks.has(index)) {
|
|
1123
|
+
emit({
|
|
1124
|
+
type: 'thinking',
|
|
1125
|
+
provider: 'claude',
|
|
1126
|
+
phase: 'completed',
|
|
1127
|
+
providerDetail: buildProviderDetail('content_block_stop', { index }, msg),
|
|
1128
|
+
});
|
|
1129
|
+
thinkingBlocks.delete(index);
|
|
1130
|
+
}
|
|
1131
|
+
handled = true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const delta = extractAssistantDelta(msg);
|
|
1135
|
+
if (delta) {
|
|
1136
|
+
aggregated += delta;
|
|
1137
|
+
emit({
|
|
1138
|
+
type: 'delta',
|
|
1139
|
+
text: delta,
|
|
1140
|
+
providerDetail: buildProviderDetail(msgType || 'delta', {}, msg),
|
|
1141
|
+
});
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const assistant = extractAssistantText(msg);
|
|
1145
|
+
if (assistant && !aggregated) {
|
|
1146
|
+
aggregated = assistant;
|
|
1147
|
+
emit({
|
|
1148
|
+
type: 'delta',
|
|
1149
|
+
text: assistant,
|
|
1150
|
+
providerDetail: buildProviderDetail(msgType || 'assistant', {}, msg),
|
|
1151
|
+
});
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const result = extractResultText(msg);
|
|
1155
|
+
if (result && !didFinalize && !sawError) {
|
|
1156
|
+
didFinalize = true;
|
|
1157
|
+
emitFinal(aggregated || result, buildProviderDetail('result', {}, msg));
|
|
1158
|
+
handled = true;
|
|
1159
|
+
}
|
|
1160
|
+
if (!handled) {
|
|
1161
|
+
emit({
|
|
1162
|
+
type: 'detail',
|
|
1163
|
+
provider: 'claude',
|
|
1164
|
+
providerDetail: buildProviderDetail(msgType || 'unknown', {}, msg),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
const stdoutParser = createLineParser(handleLine);
|
|
1169
|
+
const stderrParser = createLineParser(handleLine);
|
|
1170
|
+
child.stdout?.on('data', stdoutParser);
|
|
1171
|
+
child.stderr?.on('data', stderrParser);
|
|
1172
|
+
child.on('close', (code) => {
|
|
1173
|
+
if (!didFinalize) {
|
|
1174
|
+
if (code && code !== 0) {
|
|
1175
|
+
emitError(`Claude exited with code ${code}`);
|
|
1176
|
+
}
|
|
1177
|
+
else if (!sawError) {
|
|
1178
|
+
emitFinal(aggregated);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
resolve({ sessionId: finalSessionId });
|
|
1182
|
+
});
|
|
1183
|
+
child.on('error', (err) => {
|
|
1184
|
+
emitError(err?.message ?? 'Claude failed to start');
|
|
1185
|
+
resolve({ sessionId: finalSessionId });
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
}
|