@just-every/manager 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/chunk-KD5PYPXI.js +1277 -0
- package/dist/chunk-ZQKKKD3J.js +38 -0
- package/dist/cli.js +593 -0
- package/dist/daemon-DTD73K4H.js +7 -0
- package/dist/managerd.js +11 -0
- package/package.json +39 -0
- package/src/cli.ts +483 -0
- package/src/connectors/claude.ts +5 -0
- package/src/connectors/codex.ts +135 -0
- package/src/connectors/gemini.ts +106 -0
- package/src/constants.ts +32 -0
- package/src/daemon/http-server.ts +234 -0
- package/src/daemon/index.ts +61 -0
- package/src/ingestion/claude-ingestor.ts +170 -0
- package/src/ingestion/codex-ingestor.test.ts +94 -0
- package/src/ingestion/codex-ingestor.ts +161 -0
- package/src/ingestion/gemini-ingestor.ts +223 -0
- package/src/integrations/install.ts +38 -0
- package/src/integrations/sync.ts +173 -0
- package/src/managerd.ts +7 -0
- package/src/storage/event-store.ts +153 -0
- package/src/storage/session-store.test.ts +34 -0
- package/src/storage/session-store.ts +231 -0
- package/src/utils/fs.ts +19 -0
- package/src/utils/logger.ts +13 -0
- package/src/watchers/gemini-log-watcher.ts +141 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DATA_DIRS,
|
|
3
|
+
DB_PATH,
|
|
4
|
+
DEFAULT_CODEX_HISTORY,
|
|
5
|
+
DEFAULT_CODEX_TRANSCRIPT,
|
|
6
|
+
DEFAULT_HOST,
|
|
7
|
+
DEFAULT_PORT,
|
|
8
|
+
PID_FILE,
|
|
9
|
+
__require
|
|
10
|
+
} from "./chunk-ZQKKKD3J.js";
|
|
11
|
+
|
|
12
|
+
// src/daemon/index.ts
|
|
13
|
+
import process2 from "process";
|
|
14
|
+
import fs8 from "fs/promises";
|
|
15
|
+
|
|
16
|
+
// src/daemon/http-server.ts
|
|
17
|
+
import http from "http";
|
|
18
|
+
import { URL } from "url";
|
|
19
|
+
|
|
20
|
+
// src/storage/session-store.ts
|
|
21
|
+
import path from "path";
|
|
22
|
+
import fs2 from "fs";
|
|
23
|
+
import { randomUUID } from "crypto";
|
|
24
|
+
|
|
25
|
+
// src/utils/fs.ts
|
|
26
|
+
import fs from "fs/promises";
|
|
27
|
+
async function ensureDir(dir) {
|
|
28
|
+
await fs.mkdir(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/storage/session-store.ts
|
|
32
|
+
var cachedDatabase = null;
|
|
33
|
+
function loadDatabase() {
|
|
34
|
+
if (cachedDatabase) return cachedDatabase;
|
|
35
|
+
try {
|
|
36
|
+
cachedDatabase = __require("better-sqlite3");
|
|
37
|
+
return cachedDatabase;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error("better-sqlite3 native bindings are unavailable. Rebuild dependencies or set MANAGERD_STORAGE=memory.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
var SqliteSessionStore = class {
|
|
43
|
+
db;
|
|
44
|
+
constructor(dbPath = DB_PATH) {
|
|
45
|
+
const dir = path.dirname(dbPath);
|
|
46
|
+
if (!fs2.existsSync(dir)) {
|
|
47
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
const Database = loadDatabase();
|
|
50
|
+
this.db = new Database(dbPath);
|
|
51
|
+
this.db.pragma("journal_mode = WAL");
|
|
52
|
+
this.migrate();
|
|
53
|
+
}
|
|
54
|
+
migrate() {
|
|
55
|
+
this.db.exec(`
|
|
56
|
+
CREATE TABLE IF NOT EXISTS local_sessions (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
agent_type TEXT NOT NULL,
|
|
59
|
+
tool_session_id TEXT NOT NULL,
|
|
60
|
+
status TEXT NOT NULL,
|
|
61
|
+
command TEXT NOT NULL,
|
|
62
|
+
args TEXT NOT NULL,
|
|
63
|
+
exit_code INTEGER,
|
|
64
|
+
created_at TEXT NOT NULL,
|
|
65
|
+
updated_at TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_local_sessions_agent_type ON local_sessions(agent_type);
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
createSession(agentType, command, args, options = {}) {
|
|
71
|
+
const id = randomUUID();
|
|
72
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
73
|
+
const toolSessionId = options.toolSessionId ?? id;
|
|
74
|
+
this.db.prepare(`
|
|
75
|
+
INSERT INTO local_sessions (id, agent_type, tool_session_id, status, command, args, exit_code, created_at, updated_at)
|
|
76
|
+
VALUES (@id, @agentType, @toolSessionId, @status, @command, @args, NULL, @createdAt, @updatedAt)
|
|
77
|
+
`).run({
|
|
78
|
+
id,
|
|
79
|
+
agentType,
|
|
80
|
+
toolSessionId,
|
|
81
|
+
status: "launching" /* Launching */,
|
|
82
|
+
command,
|
|
83
|
+
args: JSON.stringify(args),
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
agentType,
|
|
90
|
+
toolSessionId,
|
|
91
|
+
status: "launching" /* Launching */,
|
|
92
|
+
command,
|
|
93
|
+
args,
|
|
94
|
+
createdAt: now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
exitCode: null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
updateStatus(id, status, exitCode = null) {
|
|
100
|
+
this.db.prepare(`
|
|
101
|
+
UPDATE local_sessions
|
|
102
|
+
SET status=@status, exit_code=@exitCode, updated_at=@updatedAt
|
|
103
|
+
WHERE id=@id
|
|
104
|
+
`).run({ id, status, exitCode, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
105
|
+
}
|
|
106
|
+
listRecent(limit = 10) {
|
|
107
|
+
const rows = this.db.prepare(`SELECT * FROM local_sessions ORDER BY datetime(updated_at) DESC LIMIT ?`).all(limit);
|
|
108
|
+
return rows.map((row) => this.denormalize(row));
|
|
109
|
+
}
|
|
110
|
+
get(id) {
|
|
111
|
+
const row = this.db.prepare(`SELECT * FROM local_sessions WHERE id=? LIMIT 1`).get(id);
|
|
112
|
+
return row ? this.denormalize(row) : null;
|
|
113
|
+
}
|
|
114
|
+
denormalize(row) {
|
|
115
|
+
return {
|
|
116
|
+
id: row.id,
|
|
117
|
+
agentType: row.agent_type,
|
|
118
|
+
toolSessionId: row.tool_session_id,
|
|
119
|
+
status: row.status,
|
|
120
|
+
command: row.command,
|
|
121
|
+
args: JSON.parse(row.args ?? "[]"),
|
|
122
|
+
createdAt: row.created_at,
|
|
123
|
+
updatedAt: row.updated_at,
|
|
124
|
+
exitCode: row.exit_code ?? null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
findByToolSession(agentType, toolSessionId) {
|
|
128
|
+
const row = this.db.prepare(
|
|
129
|
+
`SELECT * FROM local_sessions WHERE agent_type = @agentType AND tool_session_id = @toolSessionId LIMIT 1`
|
|
130
|
+
).get({ agentType, toolSessionId });
|
|
131
|
+
return row ? this.denormalize(row) : null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var MemorySessionStore = class {
|
|
135
|
+
records = /* @__PURE__ */ new Map();
|
|
136
|
+
toolIndex = /* @__PURE__ */ new Map();
|
|
137
|
+
createSession(agentType, command, args, options = {}) {
|
|
138
|
+
const id = randomUUID();
|
|
139
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
140
|
+
const record = {
|
|
141
|
+
id,
|
|
142
|
+
agentType,
|
|
143
|
+
toolSessionId: options.toolSessionId ?? id,
|
|
144
|
+
status: "launching" /* Launching */,
|
|
145
|
+
command,
|
|
146
|
+
args,
|
|
147
|
+
createdAt: now,
|
|
148
|
+
updatedAt: now,
|
|
149
|
+
exitCode: null
|
|
150
|
+
};
|
|
151
|
+
this.records.set(id, record);
|
|
152
|
+
this.toolIndex.set(this.toolKey(agentType, record.toolSessionId), record);
|
|
153
|
+
return record;
|
|
154
|
+
}
|
|
155
|
+
updateStatus(id, status, exitCode = null) {
|
|
156
|
+
const record = this.records.get(id);
|
|
157
|
+
if (!record) return;
|
|
158
|
+
record.status = status;
|
|
159
|
+
record.exitCode = exitCode;
|
|
160
|
+
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
161
|
+
this.toolIndex.set(this.toolKey(record.agentType, record.toolSessionId), record);
|
|
162
|
+
}
|
|
163
|
+
listRecent(limit = 10) {
|
|
164
|
+
return Array.from(this.records.values()).sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1).slice(0, limit);
|
|
165
|
+
}
|
|
166
|
+
get(id) {
|
|
167
|
+
return this.records.get(id) ?? null;
|
|
168
|
+
}
|
|
169
|
+
findByToolSession(agentType, toolSessionId) {
|
|
170
|
+
return this.toolIndex.get(this.toolKey(agentType, toolSessionId)) ?? null;
|
|
171
|
+
}
|
|
172
|
+
toolKey(agentType, toolSessionId) {
|
|
173
|
+
return `${agentType}:${toolSessionId}`;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
function createSessionStore() {
|
|
177
|
+
if (process.env.MANAGERD_STORAGE === "memory") {
|
|
178
|
+
return new MemorySessionStore();
|
|
179
|
+
}
|
|
180
|
+
return new SqliteSessionStore();
|
|
181
|
+
}
|
|
182
|
+
async function ensureBaseDirs(dir) {
|
|
183
|
+
await ensureDir(dir);
|
|
184
|
+
await ensureDir(path.join(dir, "logs"));
|
|
185
|
+
await ensureDir(path.join(dir, "sessions"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/connectors/codex.ts
|
|
189
|
+
import { spawn } from "child_process";
|
|
190
|
+
import fs3 from "fs/promises";
|
|
191
|
+
import path2 from "path";
|
|
192
|
+
var CodexConnector = class {
|
|
193
|
+
#store;
|
|
194
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
195
|
+
constructor(store) {
|
|
196
|
+
this.#store = store;
|
|
197
|
+
}
|
|
198
|
+
async launch(request = {}) {
|
|
199
|
+
const command = request.command ?? process.env.MANAGER_CODEX_COMMAND ?? "codex";
|
|
200
|
+
const extraArgs = request.args ?? [];
|
|
201
|
+
const session = this.#store.createSession("codex", command, extraArgs);
|
|
202
|
+
const codexHistoryDir = process.env.MANAGER_CODEX_HISTORY ?? DEFAULT_CODEX_HISTORY;
|
|
203
|
+
const sessionDir = path2.join(codexHistoryDir, session.toolSessionId);
|
|
204
|
+
await fs3.mkdir(sessionDir, { recursive: true });
|
|
205
|
+
const enforcedFlags = request.disableManagedFlags || process.env.MANAGER_CODEX_DISABLE_FLAGS === "1" ? [] : ["--output_dir", sessionDir, "--output_format", "json"];
|
|
206
|
+
const env = {
|
|
207
|
+
...process.env,
|
|
208
|
+
MANAGER_SESSION_ID: session.id
|
|
209
|
+
};
|
|
210
|
+
const state = {
|
|
211
|
+
command,
|
|
212
|
+
args: [...extraArgs, ...enforcedFlags],
|
|
213
|
+
env,
|
|
214
|
+
autoRestart: request.autoRestart !== false,
|
|
215
|
+
maxRestarts: request.maxRestarts ?? 3,
|
|
216
|
+
restarts: 0
|
|
217
|
+
};
|
|
218
|
+
this.#sessions.set(session.id, state);
|
|
219
|
+
this.spawnProcess(session.id, state);
|
|
220
|
+
return {
|
|
221
|
+
localSessionId: session.id,
|
|
222
|
+
toolSessionId: session.toolSessionId,
|
|
223
|
+
command,
|
|
224
|
+
args: [...extraArgs, ...enforcedFlags]
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
spawnProcess(sessionId, state) {
|
|
228
|
+
const child = spawn(state.command, state.args, {
|
|
229
|
+
env: state.env,
|
|
230
|
+
stdio: "ignore"
|
|
231
|
+
});
|
|
232
|
+
state.child = child;
|
|
233
|
+
this.#store.updateStatus(sessionId, "running" /* Running */);
|
|
234
|
+
child.once("exit", (code) => this.handleExit(sessionId, state, code));
|
|
235
|
+
child.once("error", (err) => {
|
|
236
|
+
console.error("[codex] failed to launch", err);
|
|
237
|
+
this.handleExit(sessionId, state, -1);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
handleExit(sessionId, state, code) {
|
|
241
|
+
const exitCode = code ?? -1;
|
|
242
|
+
const successful = exitCode === 0;
|
|
243
|
+
this.#store.updateStatus(sessionId, successful ? "completed" /* Completed */ : "failed" /* Failed */, exitCode);
|
|
244
|
+
if (state.autoRestart && !successful && state.restarts < state.maxRestarts) {
|
|
245
|
+
state.restarts += 1;
|
|
246
|
+
const delay = Math.min(1e3 * state.restarts, 5e3);
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
console.warn(`[codex] restarting session ${sessionId} attempt ${state.restarts}`);
|
|
249
|
+
this.spawnProcess(sessionId, state);
|
|
250
|
+
}, delay);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.#sessions.delete(sessionId);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function describeCodexConnector() {
|
|
257
|
+
const command = process.env.MANAGER_CODEX_COMMAND ?? "codex";
|
|
258
|
+
const dir = process.env.MANAGER_CODEX_HISTORY ?? DEFAULT_CODEX_HISTORY;
|
|
259
|
+
return `command=${command} historyDir=${dir}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/connectors/gemini.ts
|
|
263
|
+
import { spawn as spawn2 } from "child_process";
|
|
264
|
+
import path3 from "path";
|
|
265
|
+
import fs4 from "fs/promises";
|
|
266
|
+
var GeminiConnector = class {
|
|
267
|
+
#store;
|
|
268
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
269
|
+
constructor(store) {
|
|
270
|
+
this.#store = store;
|
|
271
|
+
}
|
|
272
|
+
async launch(request = {}) {
|
|
273
|
+
const command = request.command ?? process.env.MANAGER_GEMINI_COMMAND ?? "gemini";
|
|
274
|
+
const args = request.args ?? [];
|
|
275
|
+
const session = this.#store.createSession("gemini", command, args);
|
|
276
|
+
const sessionLogDir = process.env.MANAGER_GEMINI_LOG_DIR ?? path3.join(process.env.HOME || "", ".gemini", "sessions");
|
|
277
|
+
await fs4.mkdir(sessionLogDir, { recursive: true });
|
|
278
|
+
const env = {
|
|
279
|
+
...process.env,
|
|
280
|
+
MANAGER_SESSION_ID: session.id,
|
|
281
|
+
MANAGER_GEMINI_SESSION_ID: session.toolSessionId,
|
|
282
|
+
GEMINI_SESSION_LOG: path3.join(sessionLogDir, `${session.toolSessionId}.jsonl`)
|
|
283
|
+
};
|
|
284
|
+
const state = {
|
|
285
|
+
command,
|
|
286
|
+
args,
|
|
287
|
+
env,
|
|
288
|
+
autoRestart: request.autoRestart !== false,
|
|
289
|
+
maxRestarts: request.maxRestarts ?? 3,
|
|
290
|
+
restarts: 0
|
|
291
|
+
};
|
|
292
|
+
this.#sessions.set(session.id, state);
|
|
293
|
+
this.spawnProcess(session.id, state);
|
|
294
|
+
return {
|
|
295
|
+
localSessionId: session.id,
|
|
296
|
+
toolSessionId: session.toolSessionId,
|
|
297
|
+
command,
|
|
298
|
+
args
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
spawnProcess(sessionId, state) {
|
|
302
|
+
const child = spawn2(state.command, state.args, {
|
|
303
|
+
env: state.env,
|
|
304
|
+
stdio: "ignore"
|
|
305
|
+
});
|
|
306
|
+
state.child = child;
|
|
307
|
+
this.#store.updateStatus(sessionId, "running" /* Running */);
|
|
308
|
+
child.once("exit", (code) => this.handleExit(sessionId, state, code));
|
|
309
|
+
child.once("error", (err) => {
|
|
310
|
+
console.error("[gemini] failed to launch", err);
|
|
311
|
+
this.handleExit(sessionId, state, -1);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
handleExit(sessionId, state, code) {
|
|
315
|
+
const exitCode = code ?? -1;
|
|
316
|
+
const successful = exitCode === 0;
|
|
317
|
+
this.#store.updateStatus(sessionId, successful ? "completed" /* Completed */ : "failed" /* Failed */, exitCode);
|
|
318
|
+
if (state.autoRestart && !successful && state.restarts < state.maxRestarts) {
|
|
319
|
+
state.restarts += 1;
|
|
320
|
+
const delay = Math.min(1e3 * state.restarts, 5e3);
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
console.warn(`[gemini] restarting session ${sessionId} attempt ${state.restarts}`);
|
|
323
|
+
this.spawnProcess(sessionId, state);
|
|
324
|
+
}, delay);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.#sessions.delete(sessionId);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
function describeGeminiConnector() {
|
|
331
|
+
const command = process.env.MANAGER_GEMINI_COMMAND ?? "gemini";
|
|
332
|
+
const logDir = process.env.MANAGER_GEMINI_LOG_DIR ?? path3.join(process.env.HOME || "", ".gemini", "sessions");
|
|
333
|
+
return `command=${command} logDir=${logDir}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/storage/event-store.ts
|
|
337
|
+
import path4 from "path";
|
|
338
|
+
var SqliteEventStore = class {
|
|
339
|
+
db;
|
|
340
|
+
constructor(dbPath = DB_PATH) {
|
|
341
|
+
const dir = path4.dirname(dbPath);
|
|
342
|
+
ensureDir(dir);
|
|
343
|
+
const Database = loadSqlite();
|
|
344
|
+
this.db = new Database(dbPath);
|
|
345
|
+
this.db.pragma("journal_mode = WAL");
|
|
346
|
+
this.migrate();
|
|
347
|
+
}
|
|
348
|
+
migrate() {
|
|
349
|
+
this.db.exec(`
|
|
350
|
+
CREATE TABLE IF NOT EXISTS agent_events (
|
|
351
|
+
id TEXT PRIMARY KEY,
|
|
352
|
+
session_id TEXT NOT NULL,
|
|
353
|
+
event_type TEXT NOT NULL,
|
|
354
|
+
sequence INTEGER NOT NULL,
|
|
355
|
+
timestamp INTEGER NOT NULL,
|
|
356
|
+
role TEXT,
|
|
357
|
+
text TEXT,
|
|
358
|
+
tool_name TEXT,
|
|
359
|
+
tool_input TEXT,
|
|
360
|
+
tool_output TEXT,
|
|
361
|
+
error_type TEXT,
|
|
362
|
+
error_message TEXT,
|
|
363
|
+
raw_payload TEXT,
|
|
364
|
+
source_file TEXT
|
|
365
|
+
);
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_agent_events_session ON agent_events(session_id, sequence);
|
|
367
|
+
`);
|
|
368
|
+
}
|
|
369
|
+
insert(events) {
|
|
370
|
+
if (!events.length) return;
|
|
371
|
+
const stmt = this.db.prepare(`
|
|
372
|
+
INSERT OR IGNORE INTO agent_events (
|
|
373
|
+
id, session_id, event_type, sequence, timestamp,
|
|
374
|
+
role, text, tool_name, tool_input, tool_output,
|
|
375
|
+
error_type, error_message, raw_payload, source_file
|
|
376
|
+
) VALUES (@id, @sessionId, @eventType, @sequence, @timestamp,
|
|
377
|
+
@role, @text, @toolName, @toolInput, @toolOutput,
|
|
378
|
+
@errorType, @errorMessage, @rawPayload, @sourceFile)
|
|
379
|
+
`);
|
|
380
|
+
const tx = this.db.transaction((records) => {
|
|
381
|
+
for (const record of records) {
|
|
382
|
+
stmt.run(record);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
tx(events);
|
|
386
|
+
}
|
|
387
|
+
listBySession(sessionId, limit = 20) {
|
|
388
|
+
const rows = this.db.prepare("SELECT * FROM agent_events WHERE session_id = ? ORDER BY sequence DESC LIMIT ?").all(sessionId, limit);
|
|
389
|
+
return rows.map((row) => ({
|
|
390
|
+
id: row.id,
|
|
391
|
+
sessionId: row.session_id,
|
|
392
|
+
eventType: row.event_type,
|
|
393
|
+
sequence: row.sequence,
|
|
394
|
+
timestamp: row.timestamp,
|
|
395
|
+
role: row.role,
|
|
396
|
+
text: row.text,
|
|
397
|
+
toolName: row.tool_name,
|
|
398
|
+
toolInput: row.tool_input,
|
|
399
|
+
toolOutput: row.tool_output,
|
|
400
|
+
errorType: row.error_type,
|
|
401
|
+
errorMessage: row.error_message,
|
|
402
|
+
rawPayload: row.raw_payload,
|
|
403
|
+
sourceFile: row.source_file
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
maxSequence(sessionId) {
|
|
407
|
+
const row = this.db.prepare("SELECT MAX(sequence) as max FROM agent_events WHERE session_id = ?").get(sessionId);
|
|
408
|
+
return row?.max ?? 0;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
var MemoryEventStore = class {
|
|
412
|
+
events = /* @__PURE__ */ new Map();
|
|
413
|
+
insert(records) {
|
|
414
|
+
if (!records.length) return;
|
|
415
|
+
for (const record of records) {
|
|
416
|
+
const sessionEvents = this.events.get(record.sessionId) ?? [];
|
|
417
|
+
if (sessionEvents.some((existing) => existing.id === record.id)) continue;
|
|
418
|
+
sessionEvents.push(record);
|
|
419
|
+
this.events.set(record.sessionId, sessionEvents);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
listBySession(sessionId, limit = 20) {
|
|
423
|
+
const events = this.events.get(sessionId) ?? [];
|
|
424
|
+
return [...events].sort((a, b) => b.sequence - a.sequence).slice(0, limit);
|
|
425
|
+
}
|
|
426
|
+
maxSequence(sessionId) {
|
|
427
|
+
const events = this.events.get(sessionId) ?? [];
|
|
428
|
+
if (!events.length) return 0;
|
|
429
|
+
return Math.max(...events.map((event) => event.sequence));
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
function loadSqlite() {
|
|
433
|
+
try {
|
|
434
|
+
return __require("better-sqlite3");
|
|
435
|
+
} catch (err) {
|
|
436
|
+
throw new Error("better-sqlite3 native bindings are unavailable. Rebuild dependencies or set MANAGERD_STORAGE=memory.");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function createEventStore() {
|
|
440
|
+
if (process.env.MANAGERD_STORAGE === "memory") {
|
|
441
|
+
return new MemoryEventStore();
|
|
442
|
+
}
|
|
443
|
+
return new SqliteEventStore();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/ingestion/codex-ingestor.ts
|
|
447
|
+
import path5 from "path";
|
|
448
|
+
import fs5 from "fs/promises";
|
|
449
|
+
var CodexIngestor = class {
|
|
450
|
+
constructor(eventStore) {
|
|
451
|
+
this.eventStore = eventStore;
|
|
452
|
+
}
|
|
453
|
+
async ingest(session) {
|
|
454
|
+
const historyDir = process.env.MANAGER_CODEX_HISTORY ?? DEFAULT_CODEX_HISTORY;
|
|
455
|
+
const sessionDir = path5.join(historyDir, session.toolSessionId);
|
|
456
|
+
const transcriptPath = path5.join(
|
|
457
|
+
sessionDir,
|
|
458
|
+
process.env.MANAGER_CODEX_TRANSCRIPT ?? DEFAULT_CODEX_TRANSCRIPT
|
|
459
|
+
);
|
|
460
|
+
const warnings = [];
|
|
461
|
+
const stats = await fs5.stat(transcriptPath).catch(() => null);
|
|
462
|
+
if (!stats) {
|
|
463
|
+
warnings.push(`Transcript file not found at ${transcriptPath}`);
|
|
464
|
+
return { events: 0, warnings };
|
|
465
|
+
}
|
|
466
|
+
const content = await fs5.readFile(transcriptPath, "utf8");
|
|
467
|
+
let parsed;
|
|
468
|
+
try {
|
|
469
|
+
parsed = JSON.parse(content);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
warnings.push(`Failed to parse ${transcriptPath}: ${err.message}`);
|
|
472
|
+
return { events: 0, warnings };
|
|
473
|
+
}
|
|
474
|
+
const existingMax = this.eventStore.maxSequence(session.id);
|
|
475
|
+
const sourceFile = path5.basename(transcriptPath);
|
|
476
|
+
const records = normalizeCodexTranscript(session.id, parsed, existingMax, sourceFile);
|
|
477
|
+
this.eventStore.insert(records);
|
|
478
|
+
return { events: records.length, warnings };
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
function normalizeCodexTranscript(sessionId, transcript, startSequence, sourceFile = DEFAULT_CODEX_TRANSCRIPT) {
|
|
482
|
+
const records = [];
|
|
483
|
+
let sequence = startSequence;
|
|
484
|
+
for (const message of transcript.messages ?? []) {
|
|
485
|
+
const timestamp = message.timestamp ?? Date.now();
|
|
486
|
+
if (typeof message.content === "string") {
|
|
487
|
+
sequence += 1;
|
|
488
|
+
records.push({
|
|
489
|
+
id: buildEventId(sessionId, sequence),
|
|
490
|
+
sessionId,
|
|
491
|
+
eventType: "message",
|
|
492
|
+
sequence,
|
|
493
|
+
timestamp,
|
|
494
|
+
role: message.role,
|
|
495
|
+
text: message.content,
|
|
496
|
+
rawPayload: JSON.stringify(message),
|
|
497
|
+
sourceFile
|
|
498
|
+
});
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
for (const block of message.content ?? []) {
|
|
502
|
+
if (block.type === "text") {
|
|
503
|
+
sequence += 1;
|
|
504
|
+
records.push({
|
|
505
|
+
id: buildEventId(sessionId, sequence),
|
|
506
|
+
sessionId,
|
|
507
|
+
eventType: "message",
|
|
508
|
+
sequence,
|
|
509
|
+
timestamp,
|
|
510
|
+
role: message.role,
|
|
511
|
+
text: block.text,
|
|
512
|
+
rawPayload: JSON.stringify(block),
|
|
513
|
+
sourceFile
|
|
514
|
+
});
|
|
515
|
+
} else if (block.type === "tool_use") {
|
|
516
|
+
sequence += 1;
|
|
517
|
+
records.push({
|
|
518
|
+
id: buildEventId(sessionId, sequence),
|
|
519
|
+
sessionId,
|
|
520
|
+
eventType: "tool_use",
|
|
521
|
+
sequence,
|
|
522
|
+
timestamp,
|
|
523
|
+
role: message.role,
|
|
524
|
+
toolName: block.name,
|
|
525
|
+
toolInput: safeStringify(block.input),
|
|
526
|
+
rawPayload: JSON.stringify(block),
|
|
527
|
+
sourceFile
|
|
528
|
+
});
|
|
529
|
+
} else if (block.type === "tool_result") {
|
|
530
|
+
sequence += 1;
|
|
531
|
+
records.push({
|
|
532
|
+
id: buildEventId(sessionId, sequence),
|
|
533
|
+
sessionId,
|
|
534
|
+
eventType: "tool_result",
|
|
535
|
+
sequence,
|
|
536
|
+
timestamp,
|
|
537
|
+
role: message.role,
|
|
538
|
+
toolOutput: safeStringify(block.content),
|
|
539
|
+
rawPayload: JSON.stringify(block),
|
|
540
|
+
sourceFile
|
|
541
|
+
});
|
|
542
|
+
} else if (block.type === "error") {
|
|
543
|
+
sequence += 1;
|
|
544
|
+
records.push({
|
|
545
|
+
id: buildEventId(sessionId, sequence),
|
|
546
|
+
sessionId,
|
|
547
|
+
eventType: "error",
|
|
548
|
+
sequence,
|
|
549
|
+
timestamp,
|
|
550
|
+
role: message.role,
|
|
551
|
+
errorType: block.error?.type ?? "unknown",
|
|
552
|
+
errorMessage: block.error?.message ?? "Unknown error",
|
|
553
|
+
rawPayload: JSON.stringify(block),
|
|
554
|
+
sourceFile
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return records;
|
|
560
|
+
}
|
|
561
|
+
function buildEventId(sessionId, sequence) {
|
|
562
|
+
return `${sessionId}:${sequence}`;
|
|
563
|
+
}
|
|
564
|
+
function safeStringify(input) {
|
|
565
|
+
try {
|
|
566
|
+
return JSON.stringify(input);
|
|
567
|
+
} catch {
|
|
568
|
+
return "";
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/ingestion/claude-ingestor.ts
|
|
573
|
+
var ClaudeIngestor = class {
|
|
574
|
+
constructor(store, events) {
|
|
575
|
+
this.store = store;
|
|
576
|
+
this.events = events;
|
|
577
|
+
}
|
|
578
|
+
ensureSession(sessionId) {
|
|
579
|
+
const existing = this.store.findByToolSession("claude", sessionId);
|
|
580
|
+
if (existing) {
|
|
581
|
+
return existing;
|
|
582
|
+
}
|
|
583
|
+
return this.store.createSession("claude", "claude-code", [], { toolSessionId: sessionId });
|
|
584
|
+
}
|
|
585
|
+
ingest(hook) {
|
|
586
|
+
const session = this.ensureSession(hook.sessionId);
|
|
587
|
+
const timestamp = parseTimestamp(hook.timestamp);
|
|
588
|
+
const baseSequence = this.events.maxSequence(session.id);
|
|
589
|
+
const record = mapClaudeHookToEvent(session.id, baseSequence + 1, hook, timestamp);
|
|
590
|
+
if (!record) {
|
|
591
|
+
return { inserted: 0 };
|
|
592
|
+
}
|
|
593
|
+
this.events.insert([record]);
|
|
594
|
+
if (record.eventType === "session_start") {
|
|
595
|
+
this.store.updateStatus(session.id, "running" /* Running */);
|
|
596
|
+
}
|
|
597
|
+
if (record.eventType === "session_end") {
|
|
598
|
+
this.store.updateStatus(session.id, "completed" /* Completed */);
|
|
599
|
+
}
|
|
600
|
+
return { inserted: 1 };
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
function parseTimestamp(value) {
|
|
604
|
+
if (!value) return Date.now();
|
|
605
|
+
const ms = Date.parse(value);
|
|
606
|
+
return Number.isNaN(ms) ? Date.now() : ms;
|
|
607
|
+
}
|
|
608
|
+
function mapClaudeHookToEvent(sessionId, sequence, hook, timestamp) {
|
|
609
|
+
const payload = hook.payload ?? {};
|
|
610
|
+
const type = (hook.event || "").toLowerCase();
|
|
611
|
+
switch (type) {
|
|
612
|
+
case "sessionstart":
|
|
613
|
+
return {
|
|
614
|
+
id: buildEventId2(sessionId, sequence),
|
|
615
|
+
sessionId,
|
|
616
|
+
eventType: "session_start",
|
|
617
|
+
sequence,
|
|
618
|
+
timestamp,
|
|
619
|
+
role: "system",
|
|
620
|
+
text: payload.summary ?? null,
|
|
621
|
+
rawPayload: JSON.stringify(hook),
|
|
622
|
+
sourceFile: "claude-hook"
|
|
623
|
+
};
|
|
624
|
+
case "sessionend":
|
|
625
|
+
return {
|
|
626
|
+
id: buildEventId2(sessionId, sequence),
|
|
627
|
+
sessionId,
|
|
628
|
+
eventType: "session_end",
|
|
629
|
+
sequence,
|
|
630
|
+
timestamp,
|
|
631
|
+
role: "system",
|
|
632
|
+
text: payload.summary ?? "Session ended",
|
|
633
|
+
rawPayload: JSON.stringify(hook),
|
|
634
|
+
sourceFile: "claude-hook"
|
|
635
|
+
};
|
|
636
|
+
case "userpromptsubmit":
|
|
637
|
+
case "userprompt":
|
|
638
|
+
return {
|
|
639
|
+
id: buildEventId2(sessionId, sequence),
|
|
640
|
+
sessionId,
|
|
641
|
+
eventType: "user_prompt",
|
|
642
|
+
sequence,
|
|
643
|
+
timestamp,
|
|
644
|
+
role: "user",
|
|
645
|
+
text: payload.prompt ?? payload.text ?? "",
|
|
646
|
+
rawPayload: JSON.stringify(hook),
|
|
647
|
+
sourceFile: "claude-hook"
|
|
648
|
+
};
|
|
649
|
+
case "pretooluse":
|
|
650
|
+
return {
|
|
651
|
+
id: buildEventId2(sessionId, sequence),
|
|
652
|
+
sessionId,
|
|
653
|
+
eventType: "tool_use",
|
|
654
|
+
sequence,
|
|
655
|
+
timestamp,
|
|
656
|
+
role: "assistant",
|
|
657
|
+
text: payload.commentary ?? null,
|
|
658
|
+
toolName: payload.toolName ?? payload.tool?.name ?? "unknown_tool",
|
|
659
|
+
toolInput: safeStringify2(payload.input ?? payload.toolInput),
|
|
660
|
+
rawPayload: JSON.stringify(hook),
|
|
661
|
+
sourceFile: "claude-hook"
|
|
662
|
+
};
|
|
663
|
+
case "posttooluse":
|
|
664
|
+
return {
|
|
665
|
+
id: buildEventId2(sessionId, sequence),
|
|
666
|
+
sessionId,
|
|
667
|
+
eventType: "tool_result",
|
|
668
|
+
sequence,
|
|
669
|
+
timestamp,
|
|
670
|
+
role: "assistant",
|
|
671
|
+
text: payload.commentary ?? null,
|
|
672
|
+
toolName: payload.toolName ?? payload.tool?.name ?? "unknown_tool",
|
|
673
|
+
toolOutput: safeStringify2(payload.result ?? payload.toolOutput ?? payload.output),
|
|
674
|
+
errorType: payload.error?.type ?? null,
|
|
675
|
+
errorMessage: payload.error?.message ?? null,
|
|
676
|
+
rawPayload: JSON.stringify(hook),
|
|
677
|
+
sourceFile: "claude-hook"
|
|
678
|
+
};
|
|
679
|
+
case "permissionrequest":
|
|
680
|
+
return {
|
|
681
|
+
id: buildEventId2(sessionId, sequence),
|
|
682
|
+
sessionId,
|
|
683
|
+
eventType: "permission_request",
|
|
684
|
+
sequence,
|
|
685
|
+
timestamp,
|
|
686
|
+
role: "assistant",
|
|
687
|
+
text: payload.reason ?? payload.prompt ?? "Permission requested",
|
|
688
|
+
toolInput: safeStringify2(payload.details),
|
|
689
|
+
rawPayload: JSON.stringify(hook),
|
|
690
|
+
sourceFile: "claude-hook"
|
|
691
|
+
};
|
|
692
|
+
case "notification":
|
|
693
|
+
return {
|
|
694
|
+
id: buildEventId2(sessionId, sequence),
|
|
695
|
+
sessionId,
|
|
696
|
+
eventType: "notification",
|
|
697
|
+
sequence,
|
|
698
|
+
timestamp,
|
|
699
|
+
role: "system",
|
|
700
|
+
text: payload.message ?? payload.text ?? "Notification",
|
|
701
|
+
errorType: payload.severity ?? null,
|
|
702
|
+
rawPayload: JSON.stringify(hook),
|
|
703
|
+
sourceFile: "claude-hook"
|
|
704
|
+
};
|
|
705
|
+
default:
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function buildEventId2(sessionId, sequence) {
|
|
710
|
+
return `${sessionId}:claude:${sequence}`;
|
|
711
|
+
}
|
|
712
|
+
function safeStringify2(input) {
|
|
713
|
+
if (input == null) return null;
|
|
714
|
+
try {
|
|
715
|
+
return JSON.stringify(input);
|
|
716
|
+
} catch {
|
|
717
|
+
return String(input);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/ingestion/gemini-ingestor.ts
|
|
722
|
+
var GeminiIngestor = class {
|
|
723
|
+
constructor(events) {
|
|
724
|
+
this.events = events;
|
|
725
|
+
}
|
|
726
|
+
ingest(session, payload) {
|
|
727
|
+
const warnings = [];
|
|
728
|
+
const blocks = collectGeminiBlocks(payload);
|
|
729
|
+
if (!blocks.length) {
|
|
730
|
+
warnings.push("No Gemini messages found in payload");
|
|
731
|
+
return { inserted: 0, warnings };
|
|
732
|
+
}
|
|
733
|
+
const startSequence = this.events.maxSequence(session.id);
|
|
734
|
+
const records = [];
|
|
735
|
+
let sequence = startSequence;
|
|
736
|
+
for (const block of blocks) {
|
|
737
|
+
sequence += 1;
|
|
738
|
+
const record = mapGeminiBlock(session.id, sequence, block);
|
|
739
|
+
if (record) {
|
|
740
|
+
records.push(record);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (records.length) {
|
|
744
|
+
this.events.insert(records);
|
|
745
|
+
}
|
|
746
|
+
return { inserted: records.length, warnings };
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
function collectGeminiBlocks(payload) {
|
|
750
|
+
const sourceArrays = [payload.messages, payload.events, payload.chats].filter(
|
|
751
|
+
Array.isArray
|
|
752
|
+
);
|
|
753
|
+
const blocks = [];
|
|
754
|
+
for (const messages of sourceArrays) {
|
|
755
|
+
for (const message of messages) {
|
|
756
|
+
blocks.push(...normalizeGeminiMessage(message));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return blocks;
|
|
760
|
+
}
|
|
761
|
+
function normalizeGeminiMessage(message) {
|
|
762
|
+
const role = message.role || inferRoleFromType(message.type);
|
|
763
|
+
const timestamp = message.timestamp;
|
|
764
|
+
const blocks = [];
|
|
765
|
+
const type = (message.type || "").toLowerCase();
|
|
766
|
+
if (type.includes("tool_call") || type === "tool_use") {
|
|
767
|
+
blocks.push({
|
|
768
|
+
kind: "tool_use",
|
|
769
|
+
role: role ?? "assistant",
|
|
770
|
+
toolName: message.toolName ?? message.tool?.name,
|
|
771
|
+
toolInput: message.toolArgs ?? message.tool?.input ?? message.content,
|
|
772
|
+
timestamp,
|
|
773
|
+
raw: message
|
|
774
|
+
});
|
|
775
|
+
return blocks;
|
|
776
|
+
}
|
|
777
|
+
if (type.includes("tool_result") || type === "tool_execution_result") {
|
|
778
|
+
blocks.push({
|
|
779
|
+
kind: "tool_result",
|
|
780
|
+
role: role ?? "assistant",
|
|
781
|
+
toolName: message.toolName ?? message.tool?.name,
|
|
782
|
+
toolOutput: message.toolResult ?? message.tool?.output ?? message.content,
|
|
783
|
+
timestamp,
|
|
784
|
+
raw: message
|
|
785
|
+
});
|
|
786
|
+
return blocks;
|
|
787
|
+
}
|
|
788
|
+
if (typeof message.text === "string") {
|
|
789
|
+
blocks.push({
|
|
790
|
+
kind: "message",
|
|
791
|
+
role: role ?? "assistant",
|
|
792
|
+
text: message.text,
|
|
793
|
+
timestamp,
|
|
794
|
+
raw: message
|
|
795
|
+
});
|
|
796
|
+
return blocks;
|
|
797
|
+
}
|
|
798
|
+
if (typeof message.content === "string") {
|
|
799
|
+
blocks.push({
|
|
800
|
+
kind: "message",
|
|
801
|
+
role: role ?? "assistant",
|
|
802
|
+
text: message.content,
|
|
803
|
+
timestamp,
|
|
804
|
+
raw: message
|
|
805
|
+
});
|
|
806
|
+
return blocks;
|
|
807
|
+
}
|
|
808
|
+
if (Array.isArray(message.content)) {
|
|
809
|
+
for (const part of message.content) {
|
|
810
|
+
if (typeof part === "string") {
|
|
811
|
+
blocks.push({ kind: "message", role: role ?? "assistant", text: part, timestamp, raw: message });
|
|
812
|
+
} else if (part && typeof part === "object") {
|
|
813
|
+
const blockType = (part.type || "").toLowerCase();
|
|
814
|
+
if (blockType === "tool_use") {
|
|
815
|
+
blocks.push({
|
|
816
|
+
kind: "tool_use",
|
|
817
|
+
role: role ?? "assistant",
|
|
818
|
+
toolName: part.name,
|
|
819
|
+
toolInput: part.input,
|
|
820
|
+
timestamp,
|
|
821
|
+
raw: part
|
|
822
|
+
});
|
|
823
|
+
} else if (blockType === "tool_result") {
|
|
824
|
+
blocks.push({
|
|
825
|
+
kind: "tool_result",
|
|
826
|
+
role: role ?? "assistant",
|
|
827
|
+
toolName: part.name,
|
|
828
|
+
toolOutput: part.content ?? part.result,
|
|
829
|
+
timestamp,
|
|
830
|
+
raw: part
|
|
831
|
+
});
|
|
832
|
+
} else if (blockType === "text" && typeof part.text === "string") {
|
|
833
|
+
blocks.push({ kind: "message", role: role ?? "assistant", text: part.text, timestamp, raw: part });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return blocks;
|
|
838
|
+
}
|
|
839
|
+
if (message.message) {
|
|
840
|
+
blocks.push({ kind: "message", role: role ?? "assistant", text: message.message, timestamp, raw: message });
|
|
841
|
+
}
|
|
842
|
+
return blocks;
|
|
843
|
+
}
|
|
844
|
+
function mapGeminiBlock(sessionId, sequence, block) {
|
|
845
|
+
const timestamp = block.timestamp ? parseTimestamp2(block.timestamp) : Date.now();
|
|
846
|
+
const base = {
|
|
847
|
+
id: `${sessionId}:gemini:${sequence}`,
|
|
848
|
+
sessionId,
|
|
849
|
+
eventType: block.kind,
|
|
850
|
+
sequence,
|
|
851
|
+
timestamp,
|
|
852
|
+
role: block.role ?? "assistant",
|
|
853
|
+
rawPayload: block.raw ? safeStringify3(block.raw) : null,
|
|
854
|
+
sourceFile: "gemini-ingest"
|
|
855
|
+
};
|
|
856
|
+
if (block.kind === "message") {
|
|
857
|
+
return { ...base, text: block.text ?? "" };
|
|
858
|
+
}
|
|
859
|
+
if (block.kind === "tool_use") {
|
|
860
|
+
return { ...base, toolName: block.toolName ?? "unknown_tool", toolInput: safeStringify3(block.toolInput) };
|
|
861
|
+
}
|
|
862
|
+
if (block.kind === "tool_result") {
|
|
863
|
+
return { ...base, toolName: block.toolName ?? "unknown_tool", toolOutput: safeStringify3(block.toolOutput) };
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
function inferRoleFromType(type) {
|
|
868
|
+
if (!type) return void 0;
|
|
869
|
+
const value = type.toLowerCase();
|
|
870
|
+
if (value.includes("user")) return "user";
|
|
871
|
+
if (value.includes("system")) return "system";
|
|
872
|
+
return "assistant";
|
|
873
|
+
}
|
|
874
|
+
function safeStringify3(input) {
|
|
875
|
+
if (input == null) return null;
|
|
876
|
+
try {
|
|
877
|
+
return JSON.stringify(input);
|
|
878
|
+
} catch {
|
|
879
|
+
return String(input);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function parseTimestamp2(value) {
|
|
883
|
+
const ms = Date.parse(value);
|
|
884
|
+
return Number.isNaN(ms) ? Date.now() : ms;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/watchers/gemini-log-watcher.ts
|
|
888
|
+
import path7 from "path";
|
|
889
|
+
import fs7 from "fs/promises";
|
|
890
|
+
|
|
891
|
+
// src/utils/logger.ts
|
|
892
|
+
import pino from "pino";
|
|
893
|
+
import path6 from "path";
|
|
894
|
+
import fs6 from "fs";
|
|
895
|
+
if (!fs6.existsSync(DATA_DIRS.logs)) {
|
|
896
|
+
fs6.mkdirSync(DATA_DIRS.logs, { recursive: true });
|
|
897
|
+
}
|
|
898
|
+
var logger = pino({
|
|
899
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
900
|
+
base: void 0
|
|
901
|
+
}, pino.destination(path6.join(DATA_DIRS.logs, "managerd.log")));
|
|
902
|
+
|
|
903
|
+
// src/watchers/gemini-log-watcher.ts
|
|
904
|
+
var GeminiLogWatcher = class {
|
|
905
|
+
#store;
|
|
906
|
+
#ingestor;
|
|
907
|
+
#rootDir;
|
|
908
|
+
#interval;
|
|
909
|
+
#timer;
|
|
910
|
+
#seen = /* @__PURE__ */ new Map();
|
|
911
|
+
constructor(store, ingestor, options = {}) {
|
|
912
|
+
this.#store = store;
|
|
913
|
+
this.#ingestor = ingestor;
|
|
914
|
+
this.#rootDir = options.rootDir ?? process.env.MANAGER_GEMINI_LOG_ROOT ?? path7.join(process.env.HOME || "", ".gemini", "tmp");
|
|
915
|
+
this.#interval = options.intervalMs ?? 5e3;
|
|
916
|
+
}
|
|
917
|
+
start() {
|
|
918
|
+
if (this.#timer) return;
|
|
919
|
+
this.#timer = setInterval(() => {
|
|
920
|
+
this.scan().catch((err) => logger.warn({ err }, "gemini log scan failed"));
|
|
921
|
+
}, this.#interval);
|
|
922
|
+
void this.scan();
|
|
923
|
+
}
|
|
924
|
+
stop() {
|
|
925
|
+
if (!this.#timer) return;
|
|
926
|
+
clearInterval(this.#timer);
|
|
927
|
+
this.#timer = void 0;
|
|
928
|
+
}
|
|
929
|
+
async scan() {
|
|
930
|
+
let dirs = [];
|
|
931
|
+
try {
|
|
932
|
+
const rootEntries = await fs7.readdir(this.#rootDir, { withFileTypes: true });
|
|
933
|
+
for (const entry of rootEntries) {
|
|
934
|
+
if (entry.isDirectory()) {
|
|
935
|
+
const chatsDir = path7.join(this.#rootDir, entry.name, "chats");
|
|
936
|
+
dirs.push(chatsDir);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
if (err?.code !== "ENOENT") {
|
|
941
|
+
logger.debug({ err }, "gemini root not accessible");
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
for (const dir of dirs) {
|
|
946
|
+
let files = [];
|
|
947
|
+
try {
|
|
948
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
949
|
+
files = entries.filter((d) => d.isFile() && /session-.*\.(jsonl?|log)$/i.test(d.name)).map((d) => path7.join(dir, d.name));
|
|
950
|
+
} catch (err) {
|
|
951
|
+
if (err?.code !== "ENOENT") {
|
|
952
|
+
logger.debug({ err }, "failed to read gemini chat dir");
|
|
953
|
+
}
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
for (const filePath of files) {
|
|
957
|
+
await this.processFile(filePath);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async processFile(filePath) {
|
|
962
|
+
let stats;
|
|
963
|
+
try {
|
|
964
|
+
stats = await fs7.stat(filePath);
|
|
965
|
+
} catch {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const lastProcessed = this.#seen.get(filePath) ?? 0;
|
|
969
|
+
if (stats.mtimeMs <= lastProcessed) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
let text;
|
|
973
|
+
try {
|
|
974
|
+
text = await fs7.readFile(filePath, "utf8");
|
|
975
|
+
} catch (err) {
|
|
976
|
+
logger.warn({ err }, "failed to read gemini session file");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const payload = parseGeminiFile(text);
|
|
980
|
+
if (!payload) {
|
|
981
|
+
logger.warn({ filePath }, "unable to parse gemini session file");
|
|
982
|
+
this.#seen.set(filePath, stats.mtimeMs);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const sessionId = payload.sessionId ?? inferSessionIdFromPath(filePath);
|
|
986
|
+
const session = this.#store.findByToolSession("gemini", sessionId) ?? this.#store.createSession("gemini", "gemini-cli", [], { toolSessionId: sessionId });
|
|
987
|
+
try {
|
|
988
|
+
this.#ingestor.ingest(session, payload);
|
|
989
|
+
this.#seen.set(filePath, stats.mtimeMs);
|
|
990
|
+
} catch (err) {
|
|
991
|
+
logger.error({ err }, "gemini ingestion failed");
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
function parseGeminiFile(contents) {
|
|
996
|
+
const trimmed = contents.trim();
|
|
997
|
+
if (!trimmed) return null;
|
|
998
|
+
try {
|
|
999
|
+
const parsed = JSON.parse(trimmed);
|
|
1000
|
+
if (Array.isArray(parsed)) {
|
|
1001
|
+
return { messages: parsed };
|
|
1002
|
+
}
|
|
1003
|
+
return parsed;
|
|
1004
|
+
} catch {
|
|
1005
|
+
const lines = trimmed.split(/\r?\n/).filter(Boolean);
|
|
1006
|
+
const messages = [];
|
|
1007
|
+
for (const line of lines) {
|
|
1008
|
+
try {
|
|
1009
|
+
messages.push(JSON.parse(line));
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (!messages.length) return null;
|
|
1014
|
+
return { messages };
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function inferSessionIdFromPath(filePath) {
|
|
1018
|
+
const base = path7.basename(filePath);
|
|
1019
|
+
return base.replace(/\.[^.]+$/, "") || `gemini-${Date.now()}`;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/daemon/http-server.ts
|
|
1023
|
+
var ManagerDaemon = class {
|
|
1024
|
+
#store;
|
|
1025
|
+
#codex;
|
|
1026
|
+
#gemini;
|
|
1027
|
+
#events;
|
|
1028
|
+
#codexIngestor;
|
|
1029
|
+
#claudeIngestor;
|
|
1030
|
+
#geminiIngestor;
|
|
1031
|
+
#geminiWatcher;
|
|
1032
|
+
#server;
|
|
1033
|
+
#startedAt = Date.now();
|
|
1034
|
+
#host;
|
|
1035
|
+
#port;
|
|
1036
|
+
constructor(store = createSessionStore(), options = {}) {
|
|
1037
|
+
this.#store = store;
|
|
1038
|
+
this.#codex = new CodexConnector(this.#store);
|
|
1039
|
+
this.#gemini = new GeminiConnector(this.#store);
|
|
1040
|
+
this.#events = createEventStore();
|
|
1041
|
+
this.#codexIngestor = new CodexIngestor(this.#events);
|
|
1042
|
+
this.#claudeIngestor = new ClaudeIngestor(this.#store, this.#events);
|
|
1043
|
+
this.#geminiIngestor = new GeminiIngestor(this.#events);
|
|
1044
|
+
this.#geminiWatcher = new GeminiLogWatcher(this.#store, this.#geminiIngestor);
|
|
1045
|
+
this.#host = options.host ?? DEFAULT_HOST;
|
|
1046
|
+
this.#port = options.port ?? DEFAULT_PORT;
|
|
1047
|
+
}
|
|
1048
|
+
async start() {
|
|
1049
|
+
if (this.#server) return;
|
|
1050
|
+
this.#server = http.createServer((req, res) => {
|
|
1051
|
+
this.route(req, res).catch((err) => {
|
|
1052
|
+
logger.error({ err }, "unhandled daemon error");
|
|
1053
|
+
this.writeJson(res, 500, { ok: false, error: "internal_error" });
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
await new Promise((resolve) => {
|
|
1057
|
+
this.#server.listen(this.#port, this.#host, () => {
|
|
1058
|
+
logger.info({ host: this.#host, port: this.#port }, "managerd listening");
|
|
1059
|
+
resolve();
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
this.#geminiWatcher.start();
|
|
1063
|
+
}
|
|
1064
|
+
async stop() {
|
|
1065
|
+
this.#geminiWatcher.stop();
|
|
1066
|
+
if (!this.#server) return;
|
|
1067
|
+
await new Promise((resolve, reject) => {
|
|
1068
|
+
this.#server.close((err) => err ? reject(err) : resolve());
|
|
1069
|
+
});
|
|
1070
|
+
this.#server = void 0;
|
|
1071
|
+
}
|
|
1072
|
+
async route(req, res) {
|
|
1073
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1074
|
+
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
1075
|
+
return this.writeJson(res, 200, { ok: true });
|
|
1076
|
+
}
|
|
1077
|
+
if (req.method === "GET" && url.pathname === "/status") {
|
|
1078
|
+
const sessions = this.#store.listRecent(5);
|
|
1079
|
+
return this.writeJson(res, 200, {
|
|
1080
|
+
ok: true,
|
|
1081
|
+
uptimeMs: Date.now() - this.#startedAt,
|
|
1082
|
+
codex: describeCodexConnector(),
|
|
1083
|
+
gemini: describeGeminiConnector(),
|
|
1084
|
+
runningSessions: sessions.filter((s) => s.status === "running" /* Running */).length,
|
|
1085
|
+
sessions
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (req.method === "GET" && url.pathname === "/sessions") {
|
|
1089
|
+
return this.writeJson(res, 200, { ok: true, sessions: this.#store.listRecent(25) });
|
|
1090
|
+
}
|
|
1091
|
+
if (req.method === "POST" && url.pathname === "/rpc/launch") {
|
|
1092
|
+
const body = await this.parseJson(req);
|
|
1093
|
+
const agent = body?.agent ?? "codex";
|
|
1094
|
+
if (agent === "codex") {
|
|
1095
|
+
const result = await this.#codex.launch({
|
|
1096
|
+
command: body?.command,
|
|
1097
|
+
args: body?.args,
|
|
1098
|
+
disableManagedFlags: body?.disableManagedFlags,
|
|
1099
|
+
autoRestart: body?.autoRestart !== false,
|
|
1100
|
+
maxRestarts: body?.maxRestarts ?? 3
|
|
1101
|
+
});
|
|
1102
|
+
return this.writeJson(res, 200, { ok: true, result });
|
|
1103
|
+
}
|
|
1104
|
+
if (agent === "gemini") {
|
|
1105
|
+
const result = await this.#gemini.launch({
|
|
1106
|
+
command: body?.command,
|
|
1107
|
+
args: body?.args,
|
|
1108
|
+
autoRestart: body?.autoRestart !== false,
|
|
1109
|
+
maxRestarts: body?.maxRestarts ?? 3
|
|
1110
|
+
});
|
|
1111
|
+
return this.writeJson(res, 200, { ok: true, result });
|
|
1112
|
+
}
|
|
1113
|
+
return this.writeJson(res, 400, { ok: false, error: "unsupported_agent" });
|
|
1114
|
+
}
|
|
1115
|
+
if (req.method === "POST" && url.pathname === "/hooks/claude") {
|
|
1116
|
+
return this.handleClaudeHook(req, res);
|
|
1117
|
+
}
|
|
1118
|
+
if (req.method === "POST" && url.pathname === "/rpc/gemini/ingest") {
|
|
1119
|
+
return this.handleGeminiIngest(req, res);
|
|
1120
|
+
}
|
|
1121
|
+
if (req.method === "POST" && url.pathname === "/rpc/sessions/events") {
|
|
1122
|
+
const body = await this.parseJson(req);
|
|
1123
|
+
const sessionId = body?.sessionId;
|
|
1124
|
+
const limit = Number(body?.limit ?? 20);
|
|
1125
|
+
if (!sessionId) {
|
|
1126
|
+
return this.writeJson(res, 400, { ok: false, error: "missing_session_id" });
|
|
1127
|
+
}
|
|
1128
|
+
const session = this.#store.get(sessionId);
|
|
1129
|
+
if (!session) {
|
|
1130
|
+
return this.writeJson(res, 404, { ok: false, error: "session_not_found" });
|
|
1131
|
+
}
|
|
1132
|
+
let warnings = [];
|
|
1133
|
+
try {
|
|
1134
|
+
const result = await this.#codexIngestor.ingest(session);
|
|
1135
|
+
warnings = result.warnings;
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
warnings.push(`Ingestion failed: ${err.message}`);
|
|
1138
|
+
}
|
|
1139
|
+
const events = this.#events.listBySession(sessionId, limit);
|
|
1140
|
+
return this.writeJson(res, 200, { ok: true, session, events, warnings });
|
|
1141
|
+
}
|
|
1142
|
+
this.writeJson(res, 404, { ok: false, error: "not_found" });
|
|
1143
|
+
}
|
|
1144
|
+
async handleClaudeHook(_req, res) {
|
|
1145
|
+
const body = await this.parseJson(_req);
|
|
1146
|
+
const incoming = Array.isArray(body?.events) ? body.events : [body];
|
|
1147
|
+
if (!incoming.length) {
|
|
1148
|
+
return this.writeJson(res, 400, { ok: false, error: "no_events" });
|
|
1149
|
+
}
|
|
1150
|
+
const results = incoming.map((raw) => {
|
|
1151
|
+
if (!raw || typeof raw !== "object") {
|
|
1152
|
+
return { ok: false, error: "invalid_event" };
|
|
1153
|
+
}
|
|
1154
|
+
const eventName = raw.event ?? raw.type;
|
|
1155
|
+
const sessionId = raw.sessionId ?? raw.session_id;
|
|
1156
|
+
if (!eventName || !sessionId) {
|
|
1157
|
+
return { ok: false, error: "missing_fields", sessionId };
|
|
1158
|
+
}
|
|
1159
|
+
const hookEvent = {
|
|
1160
|
+
event: String(eventName),
|
|
1161
|
+
sessionId: String(sessionId),
|
|
1162
|
+
timestamp: raw.timestamp,
|
|
1163
|
+
payload: raw.payload ?? raw.data ?? raw
|
|
1164
|
+
};
|
|
1165
|
+
try {
|
|
1166
|
+
const result = this.#claudeIngestor.ingest(hookEvent);
|
|
1167
|
+
return { ok: true, sessionId: hookEvent.sessionId, inserted: result.inserted };
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
logger.error({ err }, "claude hook ingestion failed");
|
|
1170
|
+
return { ok: false, sessionId: hookEvent.sessionId, error: err?.message ?? "ingest_failed" };
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
const ok = results.every((item) => item.ok);
|
|
1174
|
+
return this.writeJson(res, ok ? 202 : 207, { ok, results });
|
|
1175
|
+
}
|
|
1176
|
+
async handleGeminiIngest(req, res) {
|
|
1177
|
+
const body = await this.parseJson(req);
|
|
1178
|
+
const data = body?.data;
|
|
1179
|
+
const fileHint = body?.filePath;
|
|
1180
|
+
if (!data || typeof data !== "object") {
|
|
1181
|
+
return this.writeJson(res, 400, { ok: false, error: "missing_payload" });
|
|
1182
|
+
}
|
|
1183
|
+
const providedId = body?.sessionId ?? data.sessionId ?? data["id"];
|
|
1184
|
+
const sessionId = (providedId || fileHint || `gemini-${Date.now()}`).toString();
|
|
1185
|
+
let session = this.#store.findByToolSession("gemini", sessionId);
|
|
1186
|
+
if (!session) {
|
|
1187
|
+
session = this.#store.createSession("gemini", "gemini-cli", [], { toolSessionId: sessionId });
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const result = this.#geminiIngestor.ingest(session, data);
|
|
1191
|
+
return this.writeJson(res, 200, { ok: true, inserted: result.inserted, warnings: result.warnings });
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
logger.error({ err }, "gemini ingest failed");
|
|
1194
|
+
return this.writeJson(res, 500, { ok: false, error: err?.message ?? "ingest_failed" });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async parseJson(req) {
|
|
1198
|
+
const chunks = [];
|
|
1199
|
+
for await (const chunk of req) {
|
|
1200
|
+
chunks.push(chunk);
|
|
1201
|
+
}
|
|
1202
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1203
|
+
if (!raw) return {};
|
|
1204
|
+
try {
|
|
1205
|
+
return JSON.parse(raw);
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
logger.warn({ err }, "failed to parse json");
|
|
1208
|
+
return {};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
writeJson(res, status, payload) {
|
|
1212
|
+
const body = JSON.stringify(payload);
|
|
1213
|
+
res.writeHead(status, {
|
|
1214
|
+
"content-type": "application/json",
|
|
1215
|
+
"content-length": Buffer.byteLength(body)
|
|
1216
|
+
});
|
|
1217
|
+
res.end(body);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// src/daemon/index.ts
|
|
1222
|
+
async function startDaemon() {
|
|
1223
|
+
await ensureBaseDirs(DATA_DIRS.root);
|
|
1224
|
+
await ensurePidAvailable();
|
|
1225
|
+
const daemon = new ManagerDaemon();
|
|
1226
|
+
let cleanedUp = false;
|
|
1227
|
+
const cleanup = async () => {
|
|
1228
|
+
if (cleanedUp) return;
|
|
1229
|
+
cleanedUp = true;
|
|
1230
|
+
await daemon.stop().catch(() => {
|
|
1231
|
+
});
|
|
1232
|
+
await removePidFile();
|
|
1233
|
+
};
|
|
1234
|
+
process2.on("SIGINT", async () => {
|
|
1235
|
+
logger.info("received SIGINT, shutting down");
|
|
1236
|
+
await cleanup();
|
|
1237
|
+
process2.exit(0);
|
|
1238
|
+
});
|
|
1239
|
+
process2.on("SIGTERM", async () => {
|
|
1240
|
+
logger.info("received SIGTERM, shutting down");
|
|
1241
|
+
await cleanup();
|
|
1242
|
+
process2.exit(0);
|
|
1243
|
+
});
|
|
1244
|
+
process2.on("exit", () => {
|
|
1245
|
+
void cleanup();
|
|
1246
|
+
});
|
|
1247
|
+
await daemon.start();
|
|
1248
|
+
await fs8.writeFile(PID_FILE, String(process2.pid), "utf8");
|
|
1249
|
+
logger.info("managerd is running. Press Ctrl+C to exit.");
|
|
1250
|
+
}
|
|
1251
|
+
async function ensurePidAvailable() {
|
|
1252
|
+
try {
|
|
1253
|
+
const contents = await fs8.readFile(PID_FILE, "utf8");
|
|
1254
|
+
const pid = Number(contents.trim());
|
|
1255
|
+
if (Number.isFinite(pid) && isProcessAlive(pid)) {
|
|
1256
|
+
throw new Error(`managerd already running (pid ${pid})`);
|
|
1257
|
+
}
|
|
1258
|
+
await removePidFile();
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function removePidFile() {
|
|
1263
|
+
await fs8.unlink(PID_FILE).catch(() => {
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function isProcessAlive(pid) {
|
|
1267
|
+
try {
|
|
1268
|
+
process2.kill(pid, 0);
|
|
1269
|
+
return true;
|
|
1270
|
+
} catch {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
export {
|
|
1276
|
+
startDaemon
|
|
1277
|
+
};
|