@awak-app/simy-cli 0.5.66 → 0.5.67
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/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.67";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -3,6 +3,7 @@ import { lstat, open, readdir, realpath } from 'node:fs/promises';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { boundConversation, CONVERSATION_LIMITS, textContent } from './conversation-context.js';
|
|
6
|
+
import { sessionTitle } from './session-titles.js';
|
|
6
7
|
|
|
7
8
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
9
|
const MAX_PROJECTS = 256;
|
|
@@ -54,21 +55,41 @@ function metadataFromPrefix(bytes, id) {
|
|
|
54
55
|
const text = bytes.toString('utf8');
|
|
55
56
|
const complete = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
56
57
|
if (!complete) return null;
|
|
57
|
-
let cwd = '', title = '', found = false;
|
|
58
|
+
let cwd = '', title = '', customTitle = '', found = false;
|
|
58
59
|
for (const line of complete.split('\n').slice(0, MAX_LINES)) {
|
|
59
60
|
if (!line) continue;
|
|
60
61
|
let item;
|
|
61
62
|
try { item = JSON.parse(line); } catch { return null; }
|
|
62
63
|
if (item.sessionId !== id) return null;
|
|
64
|
+
if (item.type === 'custom-title' && item.isSidechain !== true) { customTitle = sessionTitle(item.customTitle); continue; }
|
|
63
65
|
if (item.type !== 'user' && item.type !== 'assistant') continue;
|
|
64
66
|
if (item.isSidechain === true) return null;
|
|
65
67
|
if (typeof item.cwd !== 'string' || !isAbsolute(item.cwd)) continue;
|
|
66
68
|
if (!cwd) cwd = resolve(item.cwd);
|
|
67
69
|
if (cwd !== resolve(item.cwd)) return null;
|
|
68
|
-
if (!title
|
|
70
|
+
if (!title) title = sessionTitle(item.slug);
|
|
69
71
|
found = true;
|
|
70
72
|
}
|
|
71
|
-
return found ? { cwd, title } : null;
|
|
73
|
+
return found ? { cwd, title: customTitle || title } : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// /rename appends a custom-title envelope, often beyond the bounded prefix.
|
|
77
|
+
// Inspect only those envelopes; conversation text is never used as a title.
|
|
78
|
+
async function latestCustomTitle(handle, size, id) {
|
|
79
|
+
const offset = Math.max(0, size - 64 * 1024);
|
|
80
|
+
const buffer = Buffer.alloc(Math.min(size, 64 * 1024));
|
|
81
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
|
|
82
|
+
let text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
83
|
+
if (offset) text = text.slice(text.indexOf('\n') + 1);
|
|
84
|
+
text = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
85
|
+
let title = '';
|
|
86
|
+
for (const line of text.split('\n')) {
|
|
87
|
+
try {
|
|
88
|
+
const item = JSON.parse(line);
|
|
89
|
+
if (item.type === 'custom-title' && item.sessionId === id && item.isSidechain !== true) title = sessionTitle(item.customTitle);
|
|
90
|
+
} catch { /* Ignore partial or unrelated tail entries. */ }
|
|
91
|
+
}
|
|
92
|
+
return title;
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
async function inspectTranscript(root, projectName, fileName, observedAt, includeContext = false, cacheMetadata = false) {
|
|
@@ -104,6 +125,7 @@ async function inspectTranscript(root, projectName, fileName, observedAt, includ
|
|
|
104
125
|
const buffer = Buffer.allocUnsafe(Math.min(opened.size, MAX_PREFIX_BYTES));
|
|
105
126
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
106
127
|
const metadata = metadataFromPrefix(buffer.subarray(0, bytesRead), id);
|
|
128
|
+
if (metadata) metadata.title = await latestCustomTitle(handle, opened.size, id) || metadata.title;
|
|
107
129
|
if (cacheMetadata) {
|
|
108
130
|
const [after, pathAfter] = await Promise.all([handle.stat({ bigint: true }), lstat(path, { bigint: true })]);
|
|
109
131
|
if (fingerprint(after) === key && fingerprint(pathAfter) === key && pathAfter.isFile() && !pathAfter.isSymbolicLink())
|
|
@@ -4,6 +4,7 @@ import { TERMINAL as TERMINAL_RUNS, controlDelivery, listRecords, readRecord, pr
|
|
|
4
4
|
import { listCodexSessions, openCodexAppServer, readCodexSession, CodexAppServerError } from './codex-app-server.js';
|
|
5
5
|
import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
|
|
6
6
|
import { originalSessionTarget } from './original-session-target.js';
|
|
7
|
+
import { readCodexTitles, sessionTitle } from './session-titles.js';
|
|
7
8
|
|
|
8
9
|
function autoContinueState(record, isRunning, supported) {
|
|
9
10
|
if (record?.autoContinue !== true) return { autoContinueStatus: 'off', autoContinueReason: null };
|
|
@@ -55,7 +56,7 @@ async function liveClaudeHook(record, metadata, inspectProcess) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export async function discoverSessions({ root, openClient = openCodexAppServer, listClaude = listClaudeSessions,
|
|
58
|
-
inspectProcess = processIdentity, now = Date.now(), signal } = {}) {
|
|
59
|
+
inspectProcess = processIdentity, now = Date.now(), signal, listCodexTitles = readCodexTitles } = {}) {
|
|
59
60
|
const assertActive = () => { if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.'); };
|
|
60
61
|
assertActive();
|
|
61
62
|
const records = await listRecords(root);
|
|
@@ -81,6 +82,8 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
81
82
|
// Historical sessions must not fan out hundreds of process-inspection
|
|
82
83
|
// subprocesses. Preserve input ordering while limiting live metadata probes.
|
|
83
84
|
const items = [...byKey.values()], projected = new Array(items.length);
|
|
85
|
+
const codexTitles = items.some(item => item.provider === 'codex' && !sessionTitle(item.title)) ? await listCodexTitles() : new Map();
|
|
86
|
+
assertActive();
|
|
84
87
|
let nextItem = 0;
|
|
85
88
|
const project = async item => {
|
|
86
89
|
const record = recordsByKey.get(`${item.provider}:${item.session}`);
|
|
@@ -97,7 +100,8 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
97
100
|
else if (isRunning === undefined && !record.runtime.isRunning) isRunning = false;
|
|
98
101
|
} catch { /* Unverified process leaves runtime status unknown. */ }
|
|
99
102
|
}
|
|
100
|
-
return { provider: item.provider, session: item.session, cwd: item.cwd,
|
|
103
|
+
return { provider: item.provider, session: item.session, cwd: item.cwd,
|
|
104
|
+
title: sessionTitle(item.title) || (item.provider === 'codex' ? codexTitles.get(item.session) || '' : ''), source: item.source,
|
|
101
105
|
...(typeof isRunning === 'boolean' ? { isRunning } : {}),
|
|
102
106
|
...(item.providerGoal !== undefined ? { providerGoal: item.providerGoal } : {}),
|
|
103
107
|
...(item.goalReadState ? { goalReadState: item.goalReadState } : {}),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
+
const MAX_INDEX_BYTES = 1024 * 1024;
|
|
8
|
+
let cached;
|
|
9
|
+
const fingerprint = info => [info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].join(':');
|
|
10
|
+
|
|
11
|
+
export function sessionTitle(value) {
|
|
12
|
+
return typeof value === 'string' ? value.replace(/[\x00-\x1f\x7f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200) : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Read title metadata only, including when an older CLI cannot list threads.
|
|
16
|
+
// This never reads prompts or changes the provider's continuation capabilities.
|
|
17
|
+
export async function readCodexTitles({ codexHome = process.env.CODEX_HOME || join(homedir(), '.codex') } = {}) {
|
|
18
|
+
const path = join(codexHome, 'session_index.jsonl');
|
|
19
|
+
let handle;
|
|
20
|
+
try {
|
|
21
|
+
const before = await lstat(path, { bigint: true });
|
|
22
|
+
if (!before.isFile() || before.isSymbolicLink()) return new Map();
|
|
23
|
+
const key = fingerprint(before);
|
|
24
|
+
if (cached?.path === path && cached.key === key) return new Map(cached.titles);
|
|
25
|
+
handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
26
|
+
if (fingerprint(await handle.stat({ bigint: true })) !== key) return new Map();
|
|
27
|
+
const size = Number(before.size), offset = Math.max(0, size - MAX_INDEX_BYTES);
|
|
28
|
+
const buffer = Buffer.alloc(Math.min(size, MAX_INDEX_BYTES));
|
|
29
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
|
|
30
|
+
let text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
31
|
+
if (offset) text = text.slice(text.indexOf('\n') + 1);
|
|
32
|
+
text = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
33
|
+
const titles = new Map();
|
|
34
|
+
for (const line of text.split('\n')) {
|
|
35
|
+
try {
|
|
36
|
+
const item = JSON.parse(line);
|
|
37
|
+
if (!UUID.test(item.id || '')) continue;
|
|
38
|
+
const title = sessionTitle(item.thread_name);
|
|
39
|
+
// Appended renames win, including an explicitly cleared title.
|
|
40
|
+
if (typeof item.thread_name === 'string') titles.set(item.id, title);
|
|
41
|
+
} catch { /* A broken entry does not hide other sessions. */ }
|
|
42
|
+
}
|
|
43
|
+
if (fingerprint(await handle.stat({ bigint: true })) !== key || fingerprint(await lstat(path, { bigint: true })) !== key) return new Map();
|
|
44
|
+
cached = { path, key, titles };
|
|
45
|
+
return new Map(titles);
|
|
46
|
+
} catch { return new Map(); }
|
|
47
|
+
finally { await handle?.close(); }
|
|
48
|
+
}
|