@adhdev/daemon-core 0.9.82-rc.160 → 0.9.82-rc.161
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/cli-adapter-types.d.ts +14 -1
- package/dist/commands/mesh-coordinator.d.ts +72 -1
- package/dist/config/chat-history.d.ts +2 -0
- package/dist/config/mesh-config.d.ts +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4923 -1411
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4975 -1476
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +35 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -1
- package/dist/providers/contracts.d.ts +48 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +1 -1
- package/dist/providers/native-history/claude-cli-transcript.d.ts +1 -1
- package/dist/providers/native-history/codex-cli-transcript.d.ts +1 -1
- package/dist/providers/native-history/dispatcher.d.ts +24 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +2 -0
- package/dist/providers/spec/adapter.d.ts +56 -0
- package/dist/providers/spec/cli-adapter.d.ts +76 -0
- package/dist/providers/spec/driver.d.ts +148 -0
- package/dist/providers/spec/evaluator.d.ts +47 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +39 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +507 -0
- package/dist/providers/spec/types.d.ts +211 -0
- package/dist/repo-mesh-types.d.ts +33 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/package.json +2 -1
- package/src/cli-adapter-types.ts +15 -1
- package/src/commands/chat-commands.ts +150 -12
- package/src/commands/cli-manager.ts +11 -0
- package/src/commands/mesh-coordinator.ts +235 -1
- package/src/commands/router.ts +238 -50
- package/src/config/chat-history.ts +11 -3
- package/src/config/mesh-config.ts +16 -1
- package/src/index.ts +19 -0
- package/src/mesh/coordinator-prompt.ts +164 -8
- package/src/mesh/coordinator-registry.ts +50 -4
- package/src/providers/cli-provider-instance.ts +8 -3
- package/src/providers/contracts.ts +53 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +2 -2
- package/src/providers/native-history/claude-cli-transcript.ts +1 -1
- package/src/providers/native-history/codex-cli-transcript.ts +1 -1
- package/src/providers/native-history/dispatcher.ts +227 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +7 -0
- package/src/providers/provider-loader.ts +126 -3
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +13 -0
- package/src/providers/spec/adapter.ts +153 -0
- package/src/providers/spec/cli-adapter.ts +318 -0
- package/src/providers/spec/driver.ts +498 -0
- package/src/providers/spec/evaluator.ts +268 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +612 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +507 -0
- package/src/providers/spec/schema.json +210 -0
- package/src/providers/spec/types.ts +230 -0
- package/src/repo-mesh-types.ts +33 -1
- package/src/sessions/registry.ts +3 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-aware dispatcher for native conversation history.
|
|
3
|
+
*
|
|
4
|
+
* The four existing readers (claude/codex/antigravity/hermes) already know
|
|
5
|
+
* how to parse each agent's on-disk format. This dispatcher just resolves
|
|
6
|
+
* the right file given a workspace + sessionId hint, then hands it off.
|
|
7
|
+
*
|
|
8
|
+
* Wired into ProviderModule.scripts.readNativeHistory by provider-loader
|
|
9
|
+
* when spec.json declares native_history.reader. The script signature
|
|
10
|
+
* matches what chat-history.ts expects (see callProviderNativeHistoryRead).
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { readSession as readClaudeCliSession } from './claude-cli-transcript.js';
|
|
18
|
+
import { readSession as readCodexCliSession } from './codex-cli-transcript.js';
|
|
19
|
+
import { readSession as readAntigravityCliSession } from './antigravity-cli-transcript.js';
|
|
20
|
+
import { readSession as readHermesCliSession } from './hermes-cli-transcript.js';
|
|
21
|
+
|
|
22
|
+
export type ReaderId = 'claude-cli' | 'codex-cli' | 'antigravity-cli' | 'hermes-cli';
|
|
23
|
+
|
|
24
|
+
export interface NativeHistoryInput {
|
|
25
|
+
agentType?: string;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
providerSessionId?: string;
|
|
28
|
+
historySessionId?: string;
|
|
29
|
+
workspace?: string;
|
|
30
|
+
format?: string;
|
|
31
|
+
watchPath?: string;
|
|
32
|
+
args?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface NativeHistoryResult {
|
|
36
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string }>;
|
|
37
|
+
providerSessionId?: string;
|
|
38
|
+
sourcePath: string;
|
|
39
|
+
sourceMtimeMs: number;
|
|
40
|
+
nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeHistoryInput) => NativeHistoryResult | null {
|
|
44
|
+
return (input: NativeHistoryInput) => {
|
|
45
|
+
const workspace = input.workspace || '';
|
|
46
|
+
const sessionId = input.sessionId || input.historySessionId || '';
|
|
47
|
+
// Caller may pass a providerSessionId (the *provider's own* id, e.g.
|
|
48
|
+
// the uuid claude writes into the jsonl basename). When present, the
|
|
49
|
+
// dispatcher only returns a transcript whose file id matches —
|
|
50
|
+
// otherwise the newest_by_mtime fallback can surface a different
|
|
51
|
+
// session's chat (round 9 part b: the user reported "previous chat
|
|
52
|
+
// shows up before I type anything" on every provider).
|
|
53
|
+
const requestedProviderSid = input.providerSessionId || '';
|
|
54
|
+
|
|
55
|
+
const sourcePath = resolveSourcePath(reader, workspace, sessionId);
|
|
56
|
+
if (!sourcePath) return null;
|
|
57
|
+
|
|
58
|
+
const session = readByReader(reader, sourcePath, sessionId, workspace);
|
|
59
|
+
if (!session) return null;
|
|
60
|
+
|
|
61
|
+
if (requestedProviderSid && session.providerSessionId && session.providerSessionId !== requestedProviderSid) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
messages: session.messages.map((m: any) => ({
|
|
67
|
+
role: normalizeRole(m.role),
|
|
68
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
69
|
+
receivedAt: typeof m.receivedAt === 'number' ? m.receivedAt : Date.parse(m.timestamp || '') || Date.now(),
|
|
70
|
+
kind: typeof m.kind === 'string' ? m.kind : 'standard',
|
|
71
|
+
})),
|
|
72
|
+
providerSessionId: session.providerSessionId,
|
|
73
|
+
sourcePath: session.sourcePath,
|
|
74
|
+
sourceMtimeMs: session.sourceMtimeMs,
|
|
75
|
+
nativeHistoryCoverage: (session as any).nativeHistoryCoverage || 'full',
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// Per-provider path resolution
|
|
82
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string): string | null {
|
|
85
|
+
switch (reader) {
|
|
86
|
+
case 'claude-cli': return resolveClaudePath(workspace, sessionId);
|
|
87
|
+
case 'codex-cli': return resolveCodexPath(workspace);
|
|
88
|
+
case 'antigravity-cli': return resolveAntigravityPath(workspace);
|
|
89
|
+
case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveClaudePath(workspace: string, sessionId: string): string | null {
|
|
94
|
+
// claude stores per-cwd: ~/.claude/projects/<cwd-as-dashes>/<uuid>.jsonl
|
|
95
|
+
const dir = path.join(os.homedir(), '.claude', 'projects', cwdAsDashes(workspace));
|
|
96
|
+
if (!fs.existsSync(dir)) return null;
|
|
97
|
+
if (sessionId) {
|
|
98
|
+
const candidate = path.join(dir, `${sessionId}.jsonl`);
|
|
99
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
100
|
+
}
|
|
101
|
+
// No exact match: return null. The newest_by_mtime fallback used to
|
|
102
|
+
// grab whichever transcript was touched most recently, but that
|
|
103
|
+
// surfaced a different (often the user's *external* claude session
|
|
104
|
+
// in the same workspace) chat in the dashboard before the user
|
|
105
|
+
// typed anything. The right answer is to show nothing until either
|
|
106
|
+
// the daemon's sessionId actually maps to a file or the caller
|
|
107
|
+
// passes the correct providerSessionId (round 9 part b).
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveCodexPath(workspace: string): string | null {
|
|
112
|
+
void workspace;
|
|
113
|
+
// codex stores by UTC date: ~/.codex/sessions/<year>/<month>/<day>/<file>.jsonl
|
|
114
|
+
const now = new Date();
|
|
115
|
+
const dir = path.join(
|
|
116
|
+
os.homedir(), '.codex', 'sessions',
|
|
117
|
+
String(now.getUTCFullYear()),
|
|
118
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
119
|
+
String(now.getUTCDate()).padStart(2, '0'),
|
|
120
|
+
);
|
|
121
|
+
if (fs.existsSync(dir)) {
|
|
122
|
+
const f = newestRecentFile(dir, /\.jsonl$/);
|
|
123
|
+
if (f) return f;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveAntigravityPath(workspace: string): string | null {
|
|
129
|
+
void workspace;
|
|
130
|
+
// agy brain/<uuid>/.system_generated/logs/transcript.jsonl
|
|
131
|
+
const brainRoot = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'brain');
|
|
132
|
+
if (!fs.existsSync(brainRoot)) return null;
|
|
133
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
134
|
+
const entries = fs.readdirSync(brainRoot, { withFileTypes: true })
|
|
135
|
+
.filter(e => e.isDirectory())
|
|
136
|
+
.map(e => ({ p: path.join(brainRoot, e.name), mtime: safeMtime(path.join(brainRoot, e.name)) }))
|
|
137
|
+
.filter(e => e.mtime >= cutoff)
|
|
138
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
const t = path.join(e.p, '.system_generated', 'logs', 'transcript.jsonl');
|
|
141
|
+
if (fs.existsSync(t)) return t;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveHermesPath(workspace: string, sessionId: string): string | null {
|
|
147
|
+
void workspace; void sessionId;
|
|
148
|
+
// Hermes ≥ 0.14 persists all chat to ~/.hermes/state.db (SQLite). The
|
|
149
|
+
// db file is the "path" we hand to the reader — it pulls the newest
|
|
150
|
+
// source='cli' session inside readSession.
|
|
151
|
+
const dbPath = path.join(os.homedir(), '.hermes', 'state.db');
|
|
152
|
+
if (fs.existsSync(dbPath)) return dbPath;
|
|
153
|
+
// Legacy fallback: pre-db hermes wrote per-session JSON dumps.
|
|
154
|
+
const dir = path.join(os.homedir(), '.hermes', 'sessions');
|
|
155
|
+
if (!fs.existsSync(dir)) return null;
|
|
156
|
+
return newestRecentFile(dir, /^session_.*\.json$/);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
// Reader dispatch
|
|
161
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function readByReader(
|
|
164
|
+
reader: ReaderId,
|
|
165
|
+
sourcePath: string,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
workspace: string,
|
|
168
|
+
): any | null {
|
|
169
|
+
switch (reader) {
|
|
170
|
+
case 'claude-cli': return readClaudeCliSession(sourcePath);
|
|
171
|
+
case 'codex-cli': return readCodexCliSession(sourcePath);
|
|
172
|
+
case 'antigravity-cli': return readAntigravityCliSession(sourcePath, sessionId || undefined, workspace || undefined);
|
|
173
|
+
case 'hermes-cli': return readHermesCliSession(sourcePath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
// Helpers
|
|
179
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function cwdAsDashes(cwd: string): string {
|
|
182
|
+
if (!cwd) return '';
|
|
183
|
+
return cwd.replace(/\//g, '-');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function newestFile(dir: string, pattern: RegExp): string | null {
|
|
187
|
+
try {
|
|
188
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
189
|
+
.filter(e => e.isFile() && pattern.test(e.name))
|
|
190
|
+
.map(e => ({ p: path.join(dir, e.name), mtime: safeMtime(path.join(dir, e.name)) }))
|
|
191
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
192
|
+
return entries[0]?.p ?? null;
|
|
193
|
+
} catch { return null; }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Like newestFile but only returns a candidate when its mtime is within
|
|
198
|
+
* the recent activity window. Prevents the dashboard from surfacing a
|
|
199
|
+
* prior session's transcript when the daemon's sessionId doesn't match
|
|
200
|
+
* any file on disk (e.g. claude allocates its own uuid; daemon and
|
|
201
|
+
* agent disagree about what the "current" session is).
|
|
202
|
+
*/
|
|
203
|
+
const RECENT_WINDOW_MS = 5 * 60 * 1000;
|
|
204
|
+
function newestRecentFile(dir: string, pattern: RegExp): string | null {
|
|
205
|
+
try {
|
|
206
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
207
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
208
|
+
.filter(e => e.isFile() && pattern.test(e.name))
|
|
209
|
+
.map(e => ({ p: path.join(dir, e.name), mtime: safeMtime(path.join(dir, e.name)) }))
|
|
210
|
+
.filter(e => e.mtime >= cutoff)
|
|
211
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
212
|
+
return entries[0]?.p ?? null;
|
|
213
|
+
} catch { return null; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function safeMtime(p: string): number {
|
|
217
|
+
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeRole(r: any): 'user' | 'assistant' | 'system' {
|
|
221
|
+
const s = String(r ?? '').toLowerCase();
|
|
222
|
+
if (s === 'user' || s === 'human') return 'user';
|
|
223
|
+
if (s === 'assistant' || s === 'ai' || s === 'model') return 'assistant';
|
|
224
|
+
// daemon's chat schema rejects 'tool'/'function' — surface as assistant.
|
|
225
|
+
if (s === 'tool' || s === 'tool_result' || s === 'function') return 'assistant';
|
|
226
|
+
return 'system';
|
|
227
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes Agent — native history adapter.
|
|
3
|
+
*
|
|
4
|
+
* Hermes persists state in ~/.hermes/state.db (SQLite, better-sqlite3):
|
|
5
|
+
* sessions(id, source, started_at, ended_at, message_count, ...)
|
|
6
|
+
* messages(id, session_id, role, content, timestamp, ...)
|
|
7
|
+
*
|
|
8
|
+
* Earlier releases dumped per-session JSON files under ~/.hermes/sessions/;
|
|
9
|
+
* recent hermes (≥ 0.14) writes only to state.db. We read straight from
|
|
10
|
+
* the db when available and fall back to scanning legacy JSON dumps for
|
|
11
|
+
* archived sessions.
|
|
12
|
+
*
|
|
13
|
+
* The dispatcher hands us a sourcePath (the file the resolveHermesPath
|
|
14
|
+
* function picks). For the SQLite case the "path" is the db file itself
|
|
15
|
+
* and we resolve the most recent cli-source session inside readSession.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
|
|
23
|
+
export interface NativeHistoryMessage {
|
|
24
|
+
id: string;
|
|
25
|
+
role: 'user' | 'assistant' | 'system';
|
|
26
|
+
content: string;
|
|
27
|
+
receivedAt: number;
|
|
28
|
+
kind?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NativeHistorySession {
|
|
32
|
+
messages: NativeHistoryMessage[];
|
|
33
|
+
providerSessionId: string;
|
|
34
|
+
source: 'provider-native';
|
|
35
|
+
sourcePath: string;
|
|
36
|
+
sourceMtimeMs: number;
|
|
37
|
+
nativeHistoryCoverage: 'full';
|
|
38
|
+
workspace?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NativeHistorySessionMeta {
|
|
42
|
+
historySessionId: string;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
sourcePath: string;
|
|
45
|
+
sourceMtimeMs: number;
|
|
46
|
+
messageCount: number;
|
|
47
|
+
firstMessageAt: number;
|
|
48
|
+
lastMessageAt: number;
|
|
49
|
+
sessionTitle?: string;
|
|
50
|
+
preview?: string;
|
|
51
|
+
workspace?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const HERMES_STATE_DB = path.join(os.homedir(), '.hermes', 'state.db');
|
|
55
|
+
const HERMES_LEGACY_SESSIONS_DIR = path.join(os.homedir(), '.hermes', 'sessions');
|
|
56
|
+
|
|
57
|
+
function statMtimeMs(p: string): number {
|
|
58
|
+
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function openDb(): any | null {
|
|
62
|
+
if (!fs.existsSync(HERMES_STATE_DB)) return null;
|
|
63
|
+
try {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
65
|
+
const Database = require('better-sqlite3');
|
|
66
|
+
return new Database(HERMES_STATE_DB, { readonly: true, fileMustExist: true });
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadMessagesForSession(db: any, sessionId: string): NativeHistoryMessage[] {
|
|
73
|
+
const rows: any[] = db.prepare(
|
|
74
|
+
`SELECT id, role, content, timestamp
|
|
75
|
+
FROM messages
|
|
76
|
+
WHERE session_id = ? AND content IS NOT NULL AND content != ''
|
|
77
|
+
ORDER BY timestamp ASC, id ASC`,
|
|
78
|
+
).all(sessionId);
|
|
79
|
+
const out: NativeHistoryMessage[] = [];
|
|
80
|
+
for (const r of rows) {
|
|
81
|
+
const role = normalizeHermesRole(r.role);
|
|
82
|
+
// Hermes stores some tool/system rows under role='tool'; surface as
|
|
83
|
+
// 'assistant' so they don't get dropped on the daemon's chat schema
|
|
84
|
+
// validation (role must be user/assistant/system).
|
|
85
|
+
out.push({
|
|
86
|
+
id: String(r.id),
|
|
87
|
+
role,
|
|
88
|
+
content: String(r.content),
|
|
89
|
+
receivedAt: Math.floor(Number(r.timestamp) * 1000),
|
|
90
|
+
kind: 'standard',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readSession(sessionPath: string): NativeHistorySession | null {
|
|
97
|
+
if (!sessionPath) return null;
|
|
98
|
+
|
|
99
|
+
// Path mode A: SQLite — sourcePath is the state.db path. Pick the
|
|
100
|
+
// newest source='cli' session that has at least one persisted message.
|
|
101
|
+
if (sessionPath === HERMES_STATE_DB) {
|
|
102
|
+
const db = openDb();
|
|
103
|
+
if (!db) return null;
|
|
104
|
+
try {
|
|
105
|
+
const row: any = db.prepare(
|
|
106
|
+
`SELECT id, started_at FROM sessions
|
|
107
|
+
WHERE source = 'cli' AND message_count > 0
|
|
108
|
+
ORDER BY started_at DESC LIMIT 1`,
|
|
109
|
+
).get();
|
|
110
|
+
if (!row) return null;
|
|
111
|
+
const messages = loadMessagesForSession(db, row.id);
|
|
112
|
+
if (messages.length === 0) return null;
|
|
113
|
+
return {
|
|
114
|
+
messages,
|
|
115
|
+
providerSessionId: String(row.id),
|
|
116
|
+
source: 'provider-native',
|
|
117
|
+
sourcePath: sessionPath,
|
|
118
|
+
sourceMtimeMs: statMtimeMs(sessionPath),
|
|
119
|
+
nativeHistoryCoverage: 'full',
|
|
120
|
+
};
|
|
121
|
+
} finally {
|
|
122
|
+
try { db.close(); } catch { /* ignore */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Path mode B: legacy JSON dump — keep the old reader for archived
|
|
127
|
+
// sessions that pre-date the db format.
|
|
128
|
+
if (!path.isAbsolute(sessionPath) || !fs.existsSync(sessionPath)) return null;
|
|
129
|
+
let raw: any;
|
|
130
|
+
try { raw = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); } catch { return null; }
|
|
131
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
132
|
+
const rawMessages = Array.isArray(raw.messages) ? raw.messages : [];
|
|
133
|
+
if (rawMessages.length === 0) return null;
|
|
134
|
+
const sourceMtimeMs = statMtimeMs(sessionPath);
|
|
135
|
+
const sessionStart = Number(raw.session_start) || sourceMtimeMs - rawMessages.length * 1000;
|
|
136
|
+
const messages: NativeHistoryMessage[] = [];
|
|
137
|
+
for (let i = 0; i < rawMessages.length; i += 1) {
|
|
138
|
+
const m = rawMessages[i];
|
|
139
|
+
const content = typeof m?.content === 'string' ? m.content : '';
|
|
140
|
+
if (!content) continue;
|
|
141
|
+
messages.push({
|
|
142
|
+
id: typeof m?.id === 'string' ? m.id : `msg_${i}`,
|
|
143
|
+
role: normalizeHermesRole(m?.role),
|
|
144
|
+
content,
|
|
145
|
+
receivedAt: sessionStart + i * 1000,
|
|
146
|
+
kind: 'standard',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (messages.length === 0) return null;
|
|
150
|
+
const sessionId = typeof raw.session_id === 'string' && raw.session_id
|
|
151
|
+
? raw.session_id
|
|
152
|
+
: path.basename(sessionPath, '.json').replace(/^session_/, '');
|
|
153
|
+
return {
|
|
154
|
+
messages,
|
|
155
|
+
providerSessionId: sessionId,
|
|
156
|
+
source: 'provider-native',
|
|
157
|
+
sourcePath: sessionPath,
|
|
158
|
+
sourceMtimeMs,
|
|
159
|
+
nativeHistoryCoverage: 'full',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function listSessions(_watchPath: string): Promise<NativeHistorySessionMeta[]> {
|
|
164
|
+
const out: NativeHistorySessionMeta[] = [];
|
|
165
|
+
|
|
166
|
+
const db = openDb();
|
|
167
|
+
if (db) {
|
|
168
|
+
try {
|
|
169
|
+
const rows: any[] = db.prepare(
|
|
170
|
+
`SELECT id, started_at, ended_at, message_count, title
|
|
171
|
+
FROM sessions
|
|
172
|
+
WHERE source = 'cli' AND message_count > 0
|
|
173
|
+
ORDER BY started_at DESC LIMIT 100`,
|
|
174
|
+
).all();
|
|
175
|
+
const mtime = statMtimeMs(HERMES_STATE_DB);
|
|
176
|
+
for (const r of rows) {
|
|
177
|
+
out.push({
|
|
178
|
+
historySessionId: String(r.id),
|
|
179
|
+
sessionId: String(r.id),
|
|
180
|
+
sourcePath: HERMES_STATE_DB,
|
|
181
|
+
sourceMtimeMs: mtime,
|
|
182
|
+
messageCount: Number(r.message_count) || 0,
|
|
183
|
+
firstMessageAt: Math.floor(Number(r.started_at) * 1000),
|
|
184
|
+
lastMessageAt: Math.floor(Number(r.ended_at || r.started_at) * 1000),
|
|
185
|
+
sessionTitle: typeof r.title === 'string' ? r.title : undefined,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
try { db.close(); } catch { /* ignore */ }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (fs.existsSync(HERMES_LEGACY_SESSIONS_DIR)) {
|
|
194
|
+
for (const name of fs.readdirSync(HERMES_LEGACY_SESSIONS_DIR)) {
|
|
195
|
+
if (!/^session_.*\.json$/.test(name)) continue;
|
|
196
|
+
const p = path.join(HERMES_LEGACY_SESSIONS_DIR, name);
|
|
197
|
+
const mtime = statMtimeMs(p);
|
|
198
|
+
if (!mtime) continue;
|
|
199
|
+
let raw: any;
|
|
200
|
+
try { raw = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { continue; }
|
|
201
|
+
const msgs = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
202
|
+
if (msgs.length === 0) continue;
|
|
203
|
+
const sessionId = typeof raw.session_id === 'string' && raw.session_id
|
|
204
|
+
? raw.session_id
|
|
205
|
+
: name.replace(/^session_/, '').replace(/\.json$/, '');
|
|
206
|
+
out.push({
|
|
207
|
+
historySessionId: sessionId,
|
|
208
|
+
sessionId,
|
|
209
|
+
sourcePath: p,
|
|
210
|
+
sourceMtimeMs: mtime,
|
|
211
|
+
messageCount: msgs.length,
|
|
212
|
+
firstMessageAt: Number(raw.session_start) || mtime,
|
|
213
|
+
lastMessageAt: Number(raw.last_updated) || mtime,
|
|
214
|
+
sessionTitle: typeof raw.title === 'string' ? raw.title : undefined,
|
|
215
|
+
preview: typeof msgs[0]?.content === 'string' ? String(msgs[0].content).slice(0, 80) : undefined,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
out.sort((a, b) => b.sourceMtimeMs - a.sourceMtimeMs);
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeHermesRole(r: any): 'user' | 'assistant' | 'system' {
|
|
225
|
+
const s = String(r ?? '').toLowerCase();
|
|
226
|
+
if (s === 'user' || s === 'human') return 'user';
|
|
227
|
+
if (s === 'assistant' || s === 'ai' || s === 'model') return 'assistant';
|
|
228
|
+
if (s === 'tool' || s === 'tool_result' || s === 'function') return 'assistant';
|
|
229
|
+
return 'system';
|
|
230
|
+
}
|
|
@@ -21,3 +21,10 @@ export {
|
|
|
21
21
|
readSession as readAntigravityCliSession,
|
|
22
22
|
listSessions as listAntigravityCliSessions,
|
|
23
23
|
} from './antigravity-cli-transcript.js';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
readSession as readHermesCliSession,
|
|
27
|
+
listSessions as listHermesCliSessions,
|
|
28
|
+
} from './hermes-cli-transcript.js';
|
|
29
|
+
|
|
30
|
+
export { createNativeHistoryDispatcher, type ReaderId } from './dispatcher.js';
|
|
@@ -1084,6 +1084,125 @@ export class ProviderLoader {
|
|
|
1084
1084
|
};
|
|
1085
1085
|
}
|
|
1086
1086
|
|
|
1087
|
+
// (spec migration) Late-binding spec.json native-history hook. Runs
|
|
1088
|
+
// *after* every script-loading path (compatibility / defaultScriptDir /
|
|
1089
|
+
// overrides) so it deterministically wins over a legacy v1 scripts.js
|
|
1090
|
+
// export. Three modes, picked by spec.json's native_history block:
|
|
1091
|
+
// 1. source — declarative jsonl/sqlite executor (new-provider path,
|
|
1092
|
+
// no daemon change needed for new on-disk formats)
|
|
1093
|
+
// 2. override_path — provider-supplied reader file (escape hatch for
|
|
1094
|
+
// exotic formats); module default-exports a reader fn
|
|
1095
|
+
// 3. reader — built-in reader id (claude-cli / codex-cli /
|
|
1096
|
+
// antigravity-cli / hermes-cli), kept for backwards
|
|
1097
|
+
// compatibility with the four shipped providers
|
|
1098
|
+
if (providerDir) {
|
|
1099
|
+
try {
|
|
1100
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1101
|
+
const fs = require('node:fs');
|
|
1102
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1103
|
+
const path = require('node:path');
|
|
1104
|
+
// Pick the right spec file for the detected CLI version. Resolution
|
|
1105
|
+
// order:
|
|
1106
|
+
// 1. compatibility[i].spec where ideVersion matches currentVersion
|
|
1107
|
+
// (lets a provider ship specs/2.0.json, specs/2.1.json, etc.
|
|
1108
|
+
// alongside the matching scriptDir)
|
|
1109
|
+
// 2. specs/default.json — explicit fallback
|
|
1110
|
+
// 3. spec.json — legacy single-spec layout
|
|
1111
|
+
// Missing files fall through silently to the next candidate.
|
|
1112
|
+
const candidates: string[] = [];
|
|
1113
|
+
if (Array.isArray((base as any).compatibility)) {
|
|
1114
|
+
for (const entry of (base as any).compatibility) {
|
|
1115
|
+
if (typeof entry?.spec !== 'string') continue;
|
|
1116
|
+
// If currentVersion is unknown (cli-manager hasn't probed yet)
|
|
1117
|
+
// we still let compatibility entries that don't pin a version
|
|
1118
|
+
// through, plus any entry whose pin matches.
|
|
1119
|
+
const matches = !entry.ideVersion
|
|
1120
|
+
|| (currentVersion && this.matchesVersion(currentVersion, entry.ideVersion))
|
|
1121
|
+
|| !currentVersion;
|
|
1122
|
+
if (matches) candidates.push(path.join(providerDir, entry.spec));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
candidates.push(path.join(providerDir, 'specs', 'default.json'));
|
|
1126
|
+
candidates.push(path.join(providerDir, 'spec.json'));
|
|
1127
|
+
const specPath = candidates.find((p: string) => fs.existsSync(p));
|
|
1128
|
+
if (specPath) {
|
|
1129
|
+
// Hand the resolved spec path off to route.ts via a hidden field
|
|
1130
|
+
// so the routing layer doesn't have to repeat the candidate walk.
|
|
1131
|
+
(resolved as any)._resolvedSpecPath = specPath;
|
|
1132
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1133
|
+
const { loadSpec } = require('./spec/loader.js');
|
|
1134
|
+
const r = loadSpec(specPath);
|
|
1135
|
+
// Stub each control_bar entry as a provider.scripts.<id>. The
|
|
1136
|
+
// upstream invoke_provider_script gate checks that the script
|
|
1137
|
+
// name exists on provider.scripts before calling adapter.invokeScript;
|
|
1138
|
+
// for spec providers the *actual* dispatch happens inside
|
|
1139
|
+
// SpecCliAdapter.invokeScript which maps the name to control_bar.
|
|
1140
|
+
// The stub is just a presence marker so the gate doesn't reject.
|
|
1141
|
+
if (r.ok) {
|
|
1142
|
+
const controls = r.spec.control_bar ?? [];
|
|
1143
|
+
if (controls.length > 0) {
|
|
1144
|
+
resolved.scripts = { ...(resolved.scripts || {}) };
|
|
1145
|
+
for (const ctl of controls) {
|
|
1146
|
+
if (!(resolved.scripts as any)[ctl.id]) {
|
|
1147
|
+
(resolved.scripts as any)[ctl.id] = (..._args: unknown[]) => ({
|
|
1148
|
+
__spec_control: true,
|
|
1149
|
+
controlId: ctl.id,
|
|
1150
|
+
actionType: ctl.action.type,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const nh = r.ok ? r.spec.native_history : undefined;
|
|
1157
|
+
if (nh) {
|
|
1158
|
+
let reader: ((input: any) => any) | null = null;
|
|
1159
|
+
let format = 'spec';
|
|
1160
|
+
|
|
1161
|
+
if (nh.source) {
|
|
1162
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1163
|
+
const { executeNativeHistory } = require('./spec/native-history-executor.js');
|
|
1164
|
+
format = `spec-${nh.source.kind}`;
|
|
1165
|
+
reader = (input: any) => executeNativeHistory(nh, input);
|
|
1166
|
+
} else if (nh.override_path) {
|
|
1167
|
+
const overrideFile = path.resolve(providerDir, nh.override_path);
|
|
1168
|
+
if (fs.existsSync(overrideFile)) {
|
|
1169
|
+
try {
|
|
1170
|
+
registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
|
|
1171
|
+
delete require.cache[require.resolve(overrideFile)];
|
|
1172
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1173
|
+
const mod = require(overrideFile);
|
|
1174
|
+
const fn = typeof mod === 'function' ? mod : (mod && typeof mod.default === 'function' ? mod.default : null);
|
|
1175
|
+
if (fn) {
|
|
1176
|
+
format = 'spec-override';
|
|
1177
|
+
reader = (input: any) => fn(input);
|
|
1178
|
+
}
|
|
1179
|
+
} catch { /* fall through — leave native unavailable */ }
|
|
1180
|
+
}
|
|
1181
|
+
} else if (nh.reader) {
|
|
1182
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1183
|
+
const { createNativeHistoryDispatcher } = require('./native-history/dispatcher.js');
|
|
1184
|
+
const dispatch = createNativeHistoryDispatcher(nh.reader);
|
|
1185
|
+
format = nh.reader;
|
|
1186
|
+
reader = (input: any) => dispatch(input);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (reader) {
|
|
1190
|
+
resolved.scripts = { ...(resolved.scripts || {}) };
|
|
1191
|
+
(resolved.scripts as any).readNativeHistory = reader;
|
|
1192
|
+
(resolved as any).nativeHistory = {
|
|
1193
|
+
format,
|
|
1194
|
+
watchPath: undefined,
|
|
1195
|
+
scripts: { readSession: 'readNativeHistory' },
|
|
1196
|
+
mode: 'native-source',
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
} catch {
|
|
1202
|
+
// Best-effort — spec wiring failure must not break legacy providers.
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1087
1206
|
return resolved;
|
|
1088
1207
|
}
|
|
1089
1208
|
|
|
@@ -1094,13 +1213,17 @@ export class ProviderLoader {
|
|
|
1094
1213
|
private loadScriptsFromDir(type: string, scriptDir: string): Partial<ProviderScripts> | null {
|
|
1095
1214
|
const providerDir = this.findProviderDirInternal(type);
|
|
1096
1215
|
if (!providerDir) {
|
|
1097
|
-
this
|
|
1216
|
+
// No provider dir for this type — a spec-only provider with no
|
|
1217
|
+
// legacy scripts/v1 layout is a normal configuration, not a
|
|
1218
|
+
// problem to surface at INFO. resolve() calls this on every
|
|
1219
|
+
// request; INFO spam every 200ms drowns out the real signal.
|
|
1220
|
+
this.debugLog(`[loadScriptsFromDir] ${type}: providerDir not found`);
|
|
1098
1221
|
return null;
|
|
1099
1222
|
}
|
|
1100
1223
|
|
|
1101
1224
|
const dir = path.join(providerDir, scriptDir);
|
|
1102
1225
|
if (!fs.existsSync(dir)) {
|
|
1103
|
-
this.
|
|
1226
|
+
this.debugLog(`[loadScriptsFromDir] ${type}: dir not found: ${dir}`);
|
|
1104
1227
|
return null;
|
|
1105
1228
|
}
|
|
1106
1229
|
|
|
@@ -1121,7 +1244,7 @@ export class ProviderLoader {
|
|
|
1121
1244
|
try {
|
|
1122
1245
|
delete require.cache[require.resolve(scriptsJs)];
|
|
1123
1246
|
const loaded = require(scriptsJs);
|
|
1124
|
-
this.
|
|
1247
|
+
this.debugLog(`[loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
|
|
1125
1248
|
this.scriptsCache.set(dir, loaded);
|
|
1126
1249
|
return loaded;
|
|
1127
1250
|
} catch (e) {
|
|
@@ -306,6 +306,19 @@
|
|
|
306
306
|
"instructions": { "type": "string" },
|
|
307
307
|
"template": { "type": "string" }
|
|
308
308
|
}
|
|
309
|
+
},
|
|
310
|
+
"systemPromptInjection": {
|
|
311
|
+
"description": "How the coordinator system prompt reaches the launched CLI. Replaces hard-coded router branches per CLI type; users may override the prompt body or the injection mechanism here.",
|
|
312
|
+
"oneOf": [
|
|
313
|
+
{ "type": "object", "additionalProperties": false, "required": ["mode", "flag"],
|
|
314
|
+
"properties": { "mode": { "const": "cli_arg" }, "flag": { "type": "string" } } },
|
|
315
|
+
{ "type": "object", "additionalProperties": false, "required": ["mode", "flag", "template"],
|
|
316
|
+
"properties": { "mode": { "const": "config_override" }, "flag": { "type": "string" }, "template": { "type": "string" } } },
|
|
317
|
+
{ "type": "object", "additionalProperties": false, "required": ["mode", "path"],
|
|
318
|
+
"properties": { "mode": { "const": "context_file" }, "path": { "type": "string" }, "wrapper": { "type": "string" } } },
|
|
319
|
+
{ "type": "object", "additionalProperties": false, "required": ["mode", "name"],
|
|
320
|
+
"properties": { "mode": { "const": "env_var" }, "name": { "type": "string" } } }
|
|
321
|
+
]
|
|
309
322
|
}
|
|
310
323
|
}
|
|
311
324
|
},
|