claude-simple 0.2.27 → 0.2.30
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 +17 -1
- package/bin/edge-ctl.js +151 -4
- package/client.js +29 -4
- package/dist/addon-fit.min.js +0 -0
- package/dist/app.0.2.30.min.js +1308 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.30.min.css +2012 -0
- package/dist/xterm.min.css +0 -0
- package/dist/xterm.min.js +0 -0
- package/gateway.js +0 -0
- package/package.json +2 -2
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +54 -22
- package/public/api.js +8 -5
- package/public/app.js +7 -5
- package/public/base.css +0 -0
- package/public/cache.js +19 -0
- package/public/chat.js +110 -19
- package/public/files.js +26 -10
- package/public/git.js +2 -2
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +2 -2
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/markdown.js +16 -0
- package/public/math.js +145 -7
- package/public/mobile.js +0 -0
- package/public/notes.js +316 -0
- package/public/notifications.js +0 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +1 -1
- package/public/router.js +4 -0
- package/public/scroll-follow.js +78 -0
- package/public/settings.js +41 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +0 -0
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +0 -0
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +79 -21
- package/public/shell/session-nav.js +70 -21
- package/public/shell/templates.js +3 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +15 -2
- package/public/state.js +7 -7
- package/public/style.css +161 -0
- package/public/sw.js +10 -5
- package/public/terminal.js +0 -0
- package/public/wake-lock.js +0 -0
- package/public/workspace-path.js +61 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agent-quota.js +228 -0
- package/src/agents/claude.js +0 -0
- package/src/agents/codex.js +2 -0
- package/src/agents/cursor.js +2 -1
- package/src/agents/gemini.js +2 -1
- package/src/agents/grok.js +22 -1
- package/src/agents/meta-agent.js +7 -4
- package/src/agents/models.js +21 -12
- package/src/agents/sessions.js +270 -81
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +111 -51
- package/src/auto-update.js +2 -0
- package/src/collab.js +3 -0
- package/src/platform.js +133 -0
- package/src/project-api-path.js +14 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/chat-broadcast.js +12 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/port-websocket.js +80 -0
- package/src/runtime/protocol.js +0 -0
- package/src/session-history.js +31 -0
- package/dist/app.0.2.27.min.js +0 -1182
- package/dist/style.0.2.27.min.css +0 -1851
package/src/agents/models.js
CHANGED
|
@@ -11,12 +11,12 @@ import { pickAllowedEnv, loadAgentEnvFile, readSettingsJsonEnv } from '../agent-
|
|
|
11
11
|
/** Static fallbacks when local cache/config is missing. */
|
|
12
12
|
export const FALLBACK_MODELS = {
|
|
13
13
|
claude: {
|
|
14
|
-
model: '
|
|
14
|
+
model: 'sonnet',
|
|
15
15
|
models: [
|
|
16
|
-
{ value: '
|
|
17
|
-
{ value: '
|
|
18
|
-
{ value: '
|
|
19
|
-
{ value: '
|
|
16
|
+
{ value: 'fable', label: 'fable' },
|
|
17
|
+
{ value: 'opus', label: 'opus' },
|
|
18
|
+
{ value: 'sonnet', label: 'sonnet' },
|
|
19
|
+
{ value: 'haiku', label: 'haiku' },
|
|
20
20
|
],
|
|
21
21
|
},
|
|
22
22
|
codex: {
|
|
@@ -228,14 +228,22 @@ async function discoverClaude(claudeConfigDirs = []) {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
// Third-party gateways only have the models named in env/settings.
|
|
232
|
+
// Official Claude Code accepts session aliases (fable, opus, sonnet, haiku)
|
|
233
|
+
// or a full model id; settings.model stays the default, not the whole list.
|
|
234
|
+
const customGateway = !!envMerged.ANTHROPIC_BASE_URL;
|
|
235
|
+
let models;
|
|
236
|
+
let source;
|
|
237
|
+
if (customGateway && found.length) {
|
|
238
|
+
models = uniqModels(found);
|
|
239
|
+
source = 'anthropic-compat';
|
|
236
240
|
} else {
|
|
237
|
-
models =
|
|
238
|
-
|
|
241
|
+
models = uniqModels([
|
|
242
|
+
...FALLBACK_MODELS.claude.models.map(m => ({ ...m })),
|
|
243
|
+
...found,
|
|
244
|
+
]);
|
|
245
|
+
source = found.length ? 'alias+settings' : 'alias';
|
|
246
|
+
if (!defaultModel) defaultModel = FALLBACK_MODELS.claude.model;
|
|
239
247
|
}
|
|
240
248
|
|
|
241
249
|
if (!defaultModel) defaultModel = models[0]?.value || FALLBACK_MODELS.claude.model;
|
|
@@ -381,6 +389,7 @@ async function listAgyModels(cliPath) {
|
|
|
381
389
|
// agy renders its model picker through a TTY. `script` supplies a tiny
|
|
382
390
|
// pseudo-terminal while keeping this request non-interactive.
|
|
383
391
|
const command = cliPath || 'agy';
|
|
392
|
+
if (process.platform === 'win32') return [];
|
|
384
393
|
const scriptArgs = process.platform === 'darwin'
|
|
385
394
|
? ['-q', '/dev/null', command, 'models']
|
|
386
395
|
: ['-q', '-c', `${command} models`, '/dev/null'];
|
package/src/agents/sessions.js
CHANGED
|
@@ -4,6 +4,7 @@ import readline from 'readline';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
+
import { pathsEqual } from '../platform.js';
|
|
7
8
|
|
|
8
9
|
// ─── Claude ───────────────────────────────────────────────────────────────────
|
|
9
10
|
|
|
@@ -105,7 +106,7 @@ async function scanClaudeConfigDir(configDir, nameMap, filterCwd) {
|
|
|
105
106
|
const filePath = path.join(projectDir, fileEntry.name);
|
|
106
107
|
try {
|
|
107
108
|
const [stat, meta] = await Promise.all([fs.stat(filePath), readClaudeSessionMeta(filePath)]);
|
|
108
|
-
if (filterCwd && meta.cwd
|
|
109
|
+
if (filterCwd && !pathsEqual(meta.cwd, filterCwd)) return;
|
|
109
110
|
const sessionId = meta.sessionId || fileEntry.name.replace('.jsonl', '');
|
|
110
111
|
const firstMsg = await readClaudeFirstMessage(filePath);
|
|
111
112
|
sessions.push({
|
|
@@ -286,13 +287,20 @@ async function findCodexRollouts(codexHome) {
|
|
|
286
287
|
return files;
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
const codexMetadata = new Map();
|
|
289
291
|
async function readCodexMeta(filePath) {
|
|
292
|
+
const stat = await fs.stat(filePath);
|
|
293
|
+
const cached = codexMetadata.get(filePath);
|
|
294
|
+
if (cached?.size === stat.size && cached?.mtimeMs === stat.mtimeMs) return cached.meta;
|
|
290
295
|
let id = null, cwd = null, ts = null, turnCount = 0;
|
|
291
296
|
try {
|
|
292
297
|
// Full scan: meta is early; turnCount needs whole file
|
|
293
298
|
const stream = fsSync.createReadStream(filePath);
|
|
294
299
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
295
300
|
for await (const line of rl) {
|
|
301
|
+
const head = line.slice(0, 400);
|
|
302
|
+
if (!/"type"\s*:\s*"(?:session_meta|turn_context)"/.test(head) &&
|
|
303
|
+
!/"(?:role|type)"\s*:\s*"(?:user|user_message)"/.test(head)) continue;
|
|
296
304
|
try {
|
|
297
305
|
const d = JSON.parse(line);
|
|
298
306
|
if (d.type === 'session_meta' && d.payload) {
|
|
@@ -316,7 +324,10 @@ async function readCodexMeta(filePath) {
|
|
|
316
324
|
const m = path.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
317
325
|
if (m) id = m[1];
|
|
318
326
|
}
|
|
319
|
-
|
|
327
|
+
const meta = { id, cwd, ts, turnCount };
|
|
328
|
+
codexMetadata.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, meta });
|
|
329
|
+
while (codexMetadata.size > 2000) codexMetadata.delete(codexMetadata.keys().next().value);
|
|
330
|
+
return meta;
|
|
320
331
|
}
|
|
321
332
|
|
|
322
333
|
export async function scanCodexSessions(codexHome, filterCwd = null) {
|
|
@@ -325,25 +336,27 @@ export async function scanCodexSessions(codexHome, filterCwd = null) {
|
|
|
325
336
|
const rollouts = await findCodexRollouts(home);
|
|
326
337
|
const sessions = [];
|
|
327
338
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
339
|
+
for (let offset = 0; offset < rollouts.length; offset += 8) {
|
|
340
|
+
await Promise.all(rollouts.slice(offset, offset + 8).map(async filePath => {
|
|
341
|
+
try {
|
|
342
|
+
const [stat, meta] = await Promise.all([fs.stat(filePath), readCodexMeta(filePath)]);
|
|
343
|
+
if (!meta.id) return;
|
|
344
|
+
if (filterCwd && meta.cwd && !pathsEqual(meta.cwd, filterCwd)) return;
|
|
345
|
+
const idx = index.get(meta.id);
|
|
346
|
+
sessions.push({
|
|
347
|
+
sessionId: meta.id,
|
|
348
|
+
agent: 'codex',
|
|
349
|
+
cwd: meta.cwd || null,
|
|
350
|
+
configDir: home,
|
|
351
|
+
display: idx?.thread_name || meta.id.slice(0, 8),
|
|
352
|
+
updatedAt: stat.mtimeMs,
|
|
353
|
+
hasMemory: false,
|
|
354
|
+
filePath,
|
|
355
|
+
turnCount: meta.turnCount || 0,
|
|
356
|
+
});
|
|
357
|
+
} catch {}
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
347
360
|
return sessions;
|
|
348
361
|
}
|
|
349
362
|
|
|
@@ -361,12 +374,36 @@ export async function findCodexSessionFile(sessionId, codexHome) {
|
|
|
361
374
|
return null;
|
|
362
375
|
}
|
|
363
376
|
|
|
377
|
+
const codexTranscripts = new Map();
|
|
378
|
+
|
|
364
379
|
export async function parseCodexMessages(filePath) {
|
|
365
|
-
const
|
|
380
|
+
const stat = await fs.stat(filePath);
|
|
381
|
+
let entry = codexTranscripts.get(filePath);
|
|
382
|
+
if (entry?.loading) {
|
|
383
|
+
await entry.loading;
|
|
384
|
+
return parseCodexMessages(filePath);
|
|
385
|
+
}
|
|
386
|
+
if (!entry || entry.ino !== stat.ino || stat.size < entry.size ||
|
|
387
|
+
(stat.size === entry.size && stat.mtimeMs !== entry.mtimeMs)) {
|
|
388
|
+
entry = { ino: stat.ino, size: 0, offset: 0, messages: [], seenAssistant: new Set(), seenCalls: new Set() };
|
|
389
|
+
codexTranscripts.set(filePath, entry);
|
|
390
|
+
}
|
|
391
|
+
codexTranscripts.delete(filePath);
|
|
392
|
+
codexTranscripts.set(filePath, entry);
|
|
393
|
+
while (codexTranscripts.size > 8) codexTranscripts.delete(codexTranscripts.keys().next().value);
|
|
394
|
+
if (entry.size === stat.size && entry.mtimeMs === stat.mtimeMs) return entry.messages;
|
|
395
|
+
const loading = updateCodexTranscript(filePath, entry, stat);
|
|
396
|
+
entry.loading = loading;
|
|
397
|
+
try { await loading; } finally { entry.loading = null; }
|
|
398
|
+
return entry.messages;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function updateCodexTranscript(filePath, entry, stat) {
|
|
402
|
+
const messages = entry.messages;
|
|
366
403
|
// Codex writes the same assistant turn twice:
|
|
367
404
|
// response_item (message/assistant) AND event_msg (agent_message)
|
|
368
405
|
// Prefer response_item; only use event_msg when that text wasn't already seen.
|
|
369
|
-
const seenAssistant =
|
|
406
|
+
const seenAssistant = entry.seenAssistant;
|
|
370
407
|
|
|
371
408
|
const assistantKey = (text, ts) => {
|
|
372
409
|
const t = String(text || '').trim();
|
|
@@ -389,54 +426,81 @@ export async function parseCodexMessages(filePath) {
|
|
|
389
426
|
};
|
|
390
427
|
|
|
391
428
|
try {
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
429
|
+
if (stat.size <= entry.offset) return;
|
|
430
|
+
const stream = fsSync.createReadStream(filePath, { start: entry.offset, end: stat.size - 1 });
|
|
431
|
+
let pending = Buffer.alloc(0);
|
|
432
|
+
for await (const chunk of stream) {
|
|
433
|
+
pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
|
434
|
+
let start = 0;
|
|
435
|
+
let end;
|
|
436
|
+
while ((end = pending.indexOf(10, start)) !== -1) {
|
|
437
|
+
const line = pending.subarray(start, end).toString('utf8');
|
|
438
|
+
entry.offset += end - start + 1;
|
|
439
|
+
start = end + 1;
|
|
440
|
+
// Reasoning and tool outputs dominate large rollouts. Do not JSON-parse
|
|
441
|
+
// their multi-megabyte payloads when building the visible transcript.
|
|
442
|
+
const head = line.slice(0, 400);
|
|
443
|
+
if (!/"type"\s*:\s*"(?:response_item|event_msg)"/.test(head) ||
|
|
444
|
+
!/"type"\s*:\s*"(?:message|agent_message|function_call|command_execution|custom_tool_call)"/.test(head)) continue;
|
|
445
|
+
let d;
|
|
446
|
+
try { d = JSON.parse(line); } catch { continue; }
|
|
447
|
+
|
|
448
|
+
if (d.type === 'response_item' && d.payload) {
|
|
449
|
+
const p = d.payload;
|
|
450
|
+
if (p.type === 'message' && p.role === 'user') {
|
|
451
|
+
const texts = (p.content || [])
|
|
452
|
+
.filter(c => c.type === 'input_text' || c.type === 'text')
|
|
453
|
+
.map(c => c.text || '')
|
|
454
|
+
.filter(Boolean);
|
|
455
|
+
const text = texts.join('\n').trim();
|
|
456
|
+
// skip huge injected system/env blobs
|
|
457
|
+
if (text && !text.startsWith('# AGENTS.md') && !text.startsWith('<environment_context')
|
|
458
|
+
&& !text.startsWith('<permissions')) {
|
|
459
|
+
seenAssistant.clear();
|
|
460
|
+
messages.push({ role: 'user', type: 'text', content: text, ts: d.timestamp });
|
|
461
|
+
}
|
|
462
|
+
} else if (p.type === 'message' && p.role === 'assistant') {
|
|
463
|
+
const texts = (p.content || [])
|
|
464
|
+
.filter(c => c.type === 'output_text' || c.type === 'text')
|
|
465
|
+
.map(c => c.text || '')
|
|
466
|
+
.filter(Boolean);
|
|
467
|
+
pushAssistant(texts.join('\n'), d.timestamp);
|
|
468
|
+
} else if (p.type === 'function_call') {
|
|
469
|
+
if (p.call_id && entry.seenCalls.has(p.call_id)) continue;
|
|
470
|
+
if (p.call_id) entry.seenCalls.add(p.call_id);
|
|
471
|
+
let input = p.arguments;
|
|
472
|
+
try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
|
|
473
|
+
messages.push({ role: 'tool', type: 'tool_use', name: p.name || 'function', id: p.call_id, input: input ?? {}, ts: d.timestamp });
|
|
474
|
+
} else if (p.type === 'command_execution' || p.type === 'custom_tool_call') {
|
|
475
|
+
if (p.call_id && entry.seenCalls.has(p.call_id)) continue;
|
|
476
|
+
if (p.call_id) entry.seenCalls.add(p.call_id);
|
|
477
|
+
let input = p.input;
|
|
478
|
+
try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
|
|
479
|
+
messages.push({
|
|
480
|
+
role: 'tool',
|
|
481
|
+
type: 'tool_use',
|
|
482
|
+
name: p.type === 'command_execution' ? 'Bash' : (p.name || 'tool'),
|
|
483
|
+
id: p.call_id,
|
|
484
|
+
input: p.type === 'command_execution' ? { command: p.command || '' } : (input ?? {}),
|
|
485
|
+
ts: d.timestamp,
|
|
486
|
+
});
|
|
410
487
|
}
|
|
411
|
-
} else if (p.type === 'message' && p.role === 'assistant') {
|
|
412
|
-
const texts = (p.content || [])
|
|
413
|
-
.filter(c => c.type === 'output_text' || c.type === 'text')
|
|
414
|
-
.map(c => c.text || '')
|
|
415
|
-
.filter(Boolean);
|
|
416
|
-
pushAssistant(texts.join('\n'), d.timestamp);
|
|
417
|
-
} else if (p.type === 'function_call') {
|
|
418
|
-
let input = p.arguments;
|
|
419
|
-
try { input = typeof input === 'string' ? JSON.parse(input) : input; } catch {}
|
|
420
|
-
messages.push({ role: 'tool', type: 'tool_use', name: p.name || 'function', input: input ?? {}, ts: d.timestamp });
|
|
421
|
-
} else if (p.type === 'command_execution' || p.type === 'custom_tool_call') {
|
|
422
|
-
messages.push({
|
|
423
|
-
role: 'tool',
|
|
424
|
-
type: 'tool_use',
|
|
425
|
-
name: p.type === 'command_execution' ? 'Bash' : (p.name || 'tool'),
|
|
426
|
-
input: { command: p.command || p.input || '' },
|
|
427
|
-
ts: d.timestamp,
|
|
428
|
-
});
|
|
429
488
|
}
|
|
430
|
-
}
|
|
431
489
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
490
|
+
// Secondary channel — skip if response_item already recorded the same body
|
|
491
|
+
if (d.type === 'event_msg' && d.payload?.type === 'agent_message') {
|
|
492
|
+
const text = d.payload.message || d.payload.text;
|
|
493
|
+
pushAssistant(text, d.timestamp);
|
|
494
|
+
}
|
|
436
495
|
}
|
|
496
|
+
pending = pending.subarray(start);
|
|
437
497
|
}
|
|
438
|
-
|
|
439
|
-
|
|
498
|
+
entry.size = stat.size;
|
|
499
|
+
entry.mtimeMs = stat.mtimeMs;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
codexTranscripts.delete(filePath);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
440
504
|
}
|
|
441
505
|
|
|
442
506
|
function replaceSessionId(value, sourceSessionId, targetSessionId) {
|
|
@@ -496,6 +560,105 @@ async function forkCodexNativeSession(sourceSessionId, targetSessionId, codexHom
|
|
|
496
560
|
return targetFile;
|
|
497
561
|
}
|
|
498
562
|
|
|
563
|
+
function grokAssistantText(record) {
|
|
564
|
+
if (record?.type !== 'assistant') return '';
|
|
565
|
+
return extractGrokText(record.content).trim();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function grokUpdateKind(record) {
|
|
569
|
+
const update = record?.params?.update || record?.update || {};
|
|
570
|
+
return update.sessionUpdate || update.type || '';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function grokUpdateText(record) {
|
|
574
|
+
const update = record?.params?.update || record?.update || {};
|
|
575
|
+
return extractGrokText(update.content).trim();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Grok resumes from a session directory, not a synthetic chat transcript.
|
|
580
|
+
* Fork by cloning the native files and cutting both journals at the selected
|
|
581
|
+
* assistant reply so tool results and encrypted reasoning stay intact.
|
|
582
|
+
*/
|
|
583
|
+
async function forkGrokNativeSession(sourceSessionId, targetSessionId, grokHome, assistantText, occurrence) {
|
|
584
|
+
const sourceDir = await findGrokSessionDir(sourceSessionId, grokHome);
|
|
585
|
+
if (!sourceDir) throw new Error('Grok source session was not found');
|
|
586
|
+
|
|
587
|
+
const sourceHistory = await fs.readFile(path.join(sourceDir, 'chat_history.jsonl'), 'utf8');
|
|
588
|
+
const historyOut = [];
|
|
589
|
+
let foundForkPoint = false;
|
|
590
|
+
let seenMatches = 0;
|
|
591
|
+
for (const line of sourceHistory.split(/\r?\n/)) {
|
|
592
|
+
if (!line.trim()) continue;
|
|
593
|
+
let record;
|
|
594
|
+
try { record = JSON.parse(line); } catch { continue; }
|
|
595
|
+
historyOut.push(JSON.stringify(replaceSessionId(record, sourceSessionId, targetSessionId)));
|
|
596
|
+
const text = grokAssistantText(record);
|
|
597
|
+
if (text && text === assistantText && ++seenMatches === occurrence) {
|
|
598
|
+
foundForkPoint = true;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!foundForkPoint) {
|
|
603
|
+
throw new Error('Selected Grok assistant message was not found in the native transcript');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const targetDir = path.join(path.dirname(sourceDir), targetSessionId);
|
|
607
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
608
|
+
const histTmp = path.join(targetDir, `chat_history.jsonl.tmp-${crypto.randomUUID()}`);
|
|
609
|
+
await fs.writeFile(histTmp, `${historyOut.join('\n')}\n`, 'utf8');
|
|
610
|
+
await fs.rename(histTmp, path.join(targetDir, 'chat_history.jsonl'));
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const sourceUpdates = await fs.readFile(path.join(sourceDir, 'updates.jsonl'), 'utf8');
|
|
614
|
+
const updatesOut = [];
|
|
615
|
+
let acc = '';
|
|
616
|
+
let seenUpdateMatches = 0;
|
|
617
|
+
let matched = false;
|
|
618
|
+
for (const line of sourceUpdates.split(/\r?\n/)) {
|
|
619
|
+
if (!line.trim()) continue;
|
|
620
|
+
let record;
|
|
621
|
+
try { record = JSON.parse(line); } catch { continue; }
|
|
622
|
+
const kind = grokUpdateKind(record);
|
|
623
|
+
if (matched && (kind === 'user_message_chunk' || kind === 'agent_message_chunk')) break;
|
|
624
|
+
updatesOut.push(JSON.stringify(replaceSessionId(record, sourceSessionId, targetSessionId)));
|
|
625
|
+
if (kind === 'agent_message_chunk') {
|
|
626
|
+
acc = `${acc}${grokUpdateText(record)}`;
|
|
627
|
+
if (acc.trim() === assistantText && ++seenUpdateMatches === occurrence) matched = true;
|
|
628
|
+
} else if (kind === 'user_message_chunk' || kind === 'turn_completed') {
|
|
629
|
+
acc = '';
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const updTmp = path.join(targetDir, `updates.jsonl.tmp-${crypto.randomUUID()}`);
|
|
633
|
+
await fs.writeFile(updTmp, updatesOut.length ? `${updatesOut.join('\n')}\n` : '', 'utf8');
|
|
634
|
+
await fs.rename(updTmp, path.join(targetDir, 'updates.jsonl'));
|
|
635
|
+
} catch {}
|
|
636
|
+
|
|
637
|
+
const copyNames = ['system_prompt.txt', 'prompt_context.json', 'resources_state.json', 'announcement_state.json', 'signals.json'];
|
|
638
|
+
for (const name of copyNames) {
|
|
639
|
+
try {
|
|
640
|
+
const raw = await fs.readFile(path.join(sourceDir, name), 'utf8');
|
|
641
|
+
let out = raw;
|
|
642
|
+
if (name.endsWith('.json')) {
|
|
643
|
+
try { out = JSON.stringify(replaceSessionId(JSON.parse(raw), sourceSessionId, targetSessionId), null, 2); }
|
|
644
|
+
catch { out = raw.replaceAll(sourceSessionId, targetSessionId); }
|
|
645
|
+
}
|
|
646
|
+
await fs.writeFile(path.join(targetDir, name), out, 'utf8');
|
|
647
|
+
} catch {}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
const summaryRaw = await fs.readFile(path.join(sourceDir, 'summary.json'), 'utf8');
|
|
652
|
+
const summary = replaceSessionId(JSON.parse(summaryRaw), sourceSessionId, targetSessionId);
|
|
653
|
+
if (summary.info && typeof summary.info === 'object') summary.info.id = targetSessionId;
|
|
654
|
+
else summary.info = { ...(summary.info || {}), id: targetSessionId };
|
|
655
|
+
summary.updated_at = new Date().toISOString();
|
|
656
|
+
await fs.writeFile(path.join(targetDir, 'summary.json'), JSON.stringify(summary, null, 2), 'utf8');
|
|
657
|
+
} catch {}
|
|
658
|
+
|
|
659
|
+
return targetDir;
|
|
660
|
+
}
|
|
661
|
+
|
|
499
662
|
// ─── Grok ─────────────────────────────────────────────────────────────────────
|
|
500
663
|
|
|
501
664
|
export async function scanGrokSessions(grokHome, filterCwd = null) {
|
|
@@ -512,7 +675,7 @@ export async function scanGrokSessions(grokHome, filterCwd = null) {
|
|
|
512
675
|
// dir name is encodeURIComponent(cwd)
|
|
513
676
|
let cwd;
|
|
514
677
|
try { cwd = decodeURIComponent(dirEntry.name); } catch { cwd = dirEntry.name; }
|
|
515
|
-
if (filterCwd && cwd
|
|
678
|
+
if (filterCwd && !pathsEqual(cwd, filterCwd)) return;
|
|
516
679
|
|
|
517
680
|
const projectDir = path.join(sessionsRoot, dirEntry.name);
|
|
518
681
|
let sessionDirs;
|
|
@@ -1092,7 +1255,7 @@ export async function scanGeminiSessions(geminiHome, filterCwd = null) {
|
|
|
1092
1255
|
records.set(sessionId, existing);
|
|
1093
1256
|
}));
|
|
1094
1257
|
} catch {}
|
|
1095
|
-
return [...records.values()].filter(s => !filterCwd || s.cwd
|
|
1258
|
+
return [...records.values()].filter(s => !filterCwd || pathsEqual(s.cwd, filterCwd));
|
|
1096
1259
|
}
|
|
1097
1260
|
|
|
1098
1261
|
export async function parseGeminiMessages(filePath) {
|
|
@@ -1230,7 +1393,7 @@ export async function scanCursorSessions(cursorHome, filterCwd = null) {
|
|
|
1230
1393
|
});
|
|
1231
1394
|
}));
|
|
1232
1395
|
} catch {}
|
|
1233
|
-
return sessions.filter(s => !filterCwd || s.cwd
|
|
1396
|
+
return sessions.filter(s => !filterCwd || pathsEqual(s.cwd, filterCwd));
|
|
1234
1397
|
}
|
|
1235
1398
|
|
|
1236
1399
|
async function loadCursorContext(sessionId, cursorHome) {
|
|
@@ -1654,10 +1817,10 @@ export async function readSessionMemoryFile(sessionId, agent, fileId, opts) {
|
|
|
1654
1817
|
}
|
|
1655
1818
|
|
|
1656
1819
|
/** Return bounded raw JSONL for a particular agent session, for Meta Agent analysis. */
|
|
1657
|
-
export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset = 0, maxLines = 500 } = {}) {
|
|
1820
|
+
export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset = 0, maxLines = 500, tail = false, maxChars = 24000 } = {}) {
|
|
1658
1821
|
if (!agent) {
|
|
1659
1822
|
for (const candidate of ['claude', 'codex', 'grok', 'gemini', 'cursor']) {
|
|
1660
|
-
const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset, maxLines });
|
|
1823
|
+
const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset, maxLines, tail, maxChars });
|
|
1661
1824
|
if (found) return found;
|
|
1662
1825
|
}
|
|
1663
1826
|
return null;
|
|
@@ -1673,20 +1836,35 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
|
|
|
1673
1836
|
file = dir ? path.join(dir, 'chat_history.jsonl') : null;
|
|
1674
1837
|
} else file = await findClaudeSessionFile(sessionId, claudeConfigDirs);
|
|
1675
1838
|
if (!file) return null;
|
|
1676
|
-
const start = Math.max(0, Number(offset) || 0);
|
|
1677
|
-
const limit = Math.min(Math.max(1, Number(maxLines) ||
|
|
1839
|
+
const start = Math.max(0, Math.floor(Number(offset) || 0));
|
|
1840
|
+
const limit = Math.min(Math.max(1, Math.floor(Number(maxLines) || 100)), 500);
|
|
1841
|
+
const budget = Math.min(Math.max(1000, Number(maxChars) || 24000), 64000);
|
|
1678
1842
|
const lines = [];
|
|
1679
|
-
let n = 0;
|
|
1680
|
-
let hasMore = false;
|
|
1843
|
+
let n = 0, chars = 0, hasMore = false, clipped = false;
|
|
1681
1844
|
const stream = fsSync.createReadStream(file, { encoding: 'utf8' });
|
|
1682
1845
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1846
|
+
try {
|
|
1847
|
+
for await (const line of rl) {
|
|
1848
|
+
const index = n++;
|
|
1849
|
+
if (!tail && index < start) continue;
|
|
1850
|
+
if (!tail && (lines.length >= limit || chars >= budget)) { hasMore = true; break; }
|
|
1851
|
+
// Bound huge tool output rows as well as total response size. Clipping
|
|
1852
|
+
// is explicit: a clipped line must not be mistaken for complete JSON.
|
|
1853
|
+
const cap = tail ? budget : budget - chars;
|
|
1854
|
+
const text = line.slice(0, cap);
|
|
1855
|
+
if (text.length < line.length) clipped = true;
|
|
1856
|
+
lines.push(text); chars += text.length;
|
|
1857
|
+
if (tail) while (lines.length > limit || (chars > budget && lines.length > 1)) {
|
|
1858
|
+
chars -= lines.shift().length;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
} finally { rl.close(); stream.destroy(); }
|
|
1688
1862
|
const stat = await fs.stat(file).catch(() => null);
|
|
1689
|
-
|
|
1863
|
+
const actualOffset = tail ? Math.max(0, n - lines.length) : start;
|
|
1864
|
+
return { agent: a, fileName: path.basename(file), offset: actualOffset, lines,
|
|
1865
|
+
nextOffset: actualOffset + lines.length, truncated: hasMore, clipped,
|
|
1866
|
+
tail, size: stat?.size || 0, updatedAt: stat?.mtimeMs || null };
|
|
1867
|
+
|
|
1690
1868
|
}
|
|
1691
1869
|
|
|
1692
1870
|
export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
|
|
@@ -1796,6 +1974,17 @@ export async function convertSession(sourceSessionId, sourceAgent, targetAgent,
|
|
|
1796
1974
|
} catch {}
|
|
1797
1975
|
return { targetSessionId };
|
|
1798
1976
|
}
|
|
1977
|
+
|
|
1978
|
+
if (ta === 'grok' && sourceAgent.toLowerCase() === 'grok' && Number.isInteger(upTo)) {
|
|
1979
|
+
const selected = messages[messages.length - 1];
|
|
1980
|
+
if (selected?.role !== 'assistant' || !selected.content?.trim()) {
|
|
1981
|
+
throw new Error('A Grok fork must end at a persisted assistant message');
|
|
1982
|
+
}
|
|
1983
|
+
const occurrence = messages.filter(message => message.role === 'assistant'
|
|
1984
|
+
&& message.content?.trim() === selected.content.trim()).length;
|
|
1985
|
+
await forkGrokNativeSession(sourceSessionId, targetSessionId, grokHome, selected.content.trim(), occurrence);
|
|
1986
|
+
return { targetSessionId };
|
|
1987
|
+
}
|
|
1799
1988
|
|
|
1800
1989
|
if (ta === 'claude') {
|
|
1801
1990
|
const configDir = claudeConfigDirs?.[0] || path.join(os.homedir(), '.claude');
|
|
File without changes
|