@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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import fsSync from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { DEFAULT_HOST, DEFAULT_PORT, DATA_DIRS, PID_FILE } from './constants.js';
|
|
11
|
+
import { runIntegrationSync, IntegrationAgent } from './integrations/sync.js';
|
|
12
|
+
import { installIntegration } from './integrations/install.js';
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('manager')
|
|
18
|
+
.description('Supervise local AI agents with Manager');
|
|
19
|
+
|
|
20
|
+
const daemonCommand = program.command('daemon').description('Manage the managerd background service');
|
|
21
|
+
|
|
22
|
+
daemonCommand
|
|
23
|
+
.command('start')
|
|
24
|
+
.description('Run managerd in the foreground')
|
|
25
|
+
.action(async () => {
|
|
26
|
+
const { startDaemon } = await import('./daemon/index.js');
|
|
27
|
+
await startDaemon();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
daemonCommand
|
|
31
|
+
.command('status')
|
|
32
|
+
.description('Check daemon health')
|
|
33
|
+
.option('--host <host>', 'Daemon host override', DEFAULT_HOST)
|
|
34
|
+
.option('--port <port>', 'Daemon port override', String(DEFAULT_PORT))
|
|
35
|
+
.action(async (opts) => {
|
|
36
|
+
const result = await queryDaemon('/status', opts.host, Number(opts.port));
|
|
37
|
+
if (!result.ok) {
|
|
38
|
+
console.error(chalk.red('Daemon unavailable:'), result.error ?? 'unknown');
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.green('managerd running'));
|
|
43
|
+
console.log('Uptime:', Math.round(result.uptimeMs / 1000), 's');
|
|
44
|
+
console.log('Codex connector:', result.codex);
|
|
45
|
+
console.log('Recent sessions:');
|
|
46
|
+
for (const session of result.sessions ?? []) {
|
|
47
|
+
console.log(` - ${session.id} [${session.agentType}] ${session.status}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
daemonCommand
|
|
52
|
+
.command('stop')
|
|
53
|
+
.description('Stop a running managerd instance')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
const pid = await readPidFile();
|
|
56
|
+
if (!pid) {
|
|
57
|
+
console.log('managerd is not running.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 'SIGTERM');
|
|
62
|
+
console.log(`Sent SIGTERM to managerd (pid ${pid}).`);
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
console.error(chalk.red('Failed to stop managerd:'), err?.message ?? err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
daemonCommand
|
|
69
|
+
.command('logs')
|
|
70
|
+
.description('Show managerd logs')
|
|
71
|
+
.option('--follow', 'Stream logs until interrupted')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
await tailLogs(Boolean(opts.follow));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
daemonCommand
|
|
77
|
+
.command('install')
|
|
78
|
+
.description('Install managerd as a user-level systemd service')
|
|
79
|
+
.action(async () => {
|
|
80
|
+
await installSystemdService();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const agentCommand = program.command('agent').description('Interact with local agents via managerd');
|
|
84
|
+
|
|
85
|
+
agentCommand
|
|
86
|
+
.command('launch <agent>')
|
|
87
|
+
.description('Launch an agent session through the daemon')
|
|
88
|
+
.option('--cmd <path>', 'Override executable path')
|
|
89
|
+
.option('--arg <value...>', 'Arguments passed to the agent')
|
|
90
|
+
.option('--raw', 'Skip automatic Codex flag injection (testing only)')
|
|
91
|
+
.option('--max-restarts <n>', 'Maximum automatic restarts (default: 3)', '3')
|
|
92
|
+
.option('--no-auto-restart', 'Disable automatic restarts')
|
|
93
|
+
.option('--host <host>', 'Daemon host override', DEFAULT_HOST)
|
|
94
|
+
.option('--port <port>', 'Daemon port override', String(DEFAULT_PORT))
|
|
95
|
+
.action(async (agent: string, opts) => {
|
|
96
|
+
if (!['codex', 'gemini'].includes(agent)) {
|
|
97
|
+
console.error(chalk.red('Supported agents: codex, gemini'));
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const maxRestarts = Number(opts.maxRestarts ?? 3);
|
|
102
|
+
const payload: any = {
|
|
103
|
+
agent,
|
|
104
|
+
command: opts.cmd,
|
|
105
|
+
args: opts.arg ?? [],
|
|
106
|
+
autoRestart: opts.autoRestart !== false,
|
|
107
|
+
maxRestarts: Number.isFinite(maxRestarts) ? maxRestarts : 3,
|
|
108
|
+
};
|
|
109
|
+
if (agent === 'codex') {
|
|
110
|
+
payload.disableManagedFlags = Boolean(opts.raw);
|
|
111
|
+
}
|
|
112
|
+
const response = await callDaemon('/rpc/launch', payload, opts.host, Number(opts.port));
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
console.error(chalk.red('Launch failed:'), response.error ?? 'unknown');
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { result } = response;
|
|
119
|
+
console.log(chalk.green(`${agent} session launched`));
|
|
120
|
+
console.log('Local session ID:', result.localSessionId);
|
|
121
|
+
console.log('Tool session ID:', result.toolSessionId);
|
|
122
|
+
console.log('Command:', `${result.command} ${result.args.join(' ')}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
agentCommand
|
|
126
|
+
.command('ingest-gemini <sessionFile>')
|
|
127
|
+
.description('Ingest a Gemini CLI session JSON file into managerd')
|
|
128
|
+
.option('--session-id <id>', 'Override Gemini session id')
|
|
129
|
+
.option('--host <host>', 'Daemon host override', DEFAULT_HOST)
|
|
130
|
+
.option('--port <port>', 'Daemon port override', String(DEFAULT_PORT))
|
|
131
|
+
.action(async (sessionFile, opts) => {
|
|
132
|
+
const absolute = path.resolve(sessionFile);
|
|
133
|
+
let contents: string;
|
|
134
|
+
try {
|
|
135
|
+
contents = await fs.readFile(absolute, 'utf8');
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
console.error(chalk.red('Unable to read session file:'), err?.message ?? err);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let data: any;
|
|
142
|
+
try {
|
|
143
|
+
data = JSON.parse(contents);
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
console.error(chalk.red('Session file is not valid JSON:'), err?.message ?? err);
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const sessionId =
|
|
150
|
+
opts.sessionId ?? data.sessionId ?? data.id ?? inferSessionIdFromPath(absolute);
|
|
151
|
+
const payload = {
|
|
152
|
+
sessionId,
|
|
153
|
+
filePath: absolute,
|
|
154
|
+
data,
|
|
155
|
+
};
|
|
156
|
+
const response = await callDaemon('/rpc/gemini/ingest', payload, opts.host, Number(opts.port));
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
console.error(chalk.red('Gemini ingestion failed:'), response.error ?? 'unknown');
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk.green('Gemini session ingested'));
|
|
163
|
+
if (response.inserted != null) {
|
|
164
|
+
console.log('Events inserted:', response.inserted);
|
|
165
|
+
}
|
|
166
|
+
if (response.warnings?.length) {
|
|
167
|
+
console.warn(chalk.yellow('Warnings:'));
|
|
168
|
+
response.warnings.forEach((warning: string) => console.warn(' -', warning));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
program
|
|
173
|
+
.command('sessions')
|
|
174
|
+
.description('List locally tracked sessions')
|
|
175
|
+
.option('--host <host>', 'Daemon host override', DEFAULT_HOST)
|
|
176
|
+
.option('--port <port>', 'Daemon port override', String(DEFAULT_PORT))
|
|
177
|
+
.action(async (opts) => {
|
|
178
|
+
const result = await queryDaemon('/sessions', opts.host, Number(opts.port));
|
|
179
|
+
if (!result.ok) {
|
|
180
|
+
console.error(chalk.red('Unable to read sessions:'), result.error ?? 'unknown');
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!result.sessions?.length) {
|
|
185
|
+
console.log('No sessions recorded yet.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const rows = result.sessions.map((session: any) => ({
|
|
189
|
+
id: session.id.slice(0, 8),
|
|
190
|
+
agent: session.agentType,
|
|
191
|
+
status: session.status,
|
|
192
|
+
command: session.command,
|
|
193
|
+
started: formatDate(session.createdAt ?? session.created_at),
|
|
194
|
+
updated: formatDate(session.updatedAt ?? session.updated_at),
|
|
195
|
+
}));
|
|
196
|
+
const header = ['ID', 'Agent', 'Status', 'Started', 'Updated', 'Command'];
|
|
197
|
+
const lines = [header, ...rows.map((row: any) => [row.id, row.agent, row.status, row.started, row.updated, row.command])];
|
|
198
|
+
const widths = header.map((_, index) => Math.max(...lines.map((line) => String(line[index] ?? '').length)));
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
const formatted = line
|
|
201
|
+
.map((value, idx) => String(value ?? '').padEnd(widths[idx]))
|
|
202
|
+
.join(' ');
|
|
203
|
+
console.log(formatted);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
program
|
|
208
|
+
.command('sessions:events <sessionId>')
|
|
209
|
+
.description('View events captured for a session (ingests Codex transcripts on demand)')
|
|
210
|
+
.option('--host <host>', 'Daemon host override', DEFAULT_HOST)
|
|
211
|
+
.option('--port <port>', 'Daemon port override', String(DEFAULT_PORT))
|
|
212
|
+
.option('--limit <n>', 'Limit number of events', '20')
|
|
213
|
+
.option('--json', 'Print raw JSON')
|
|
214
|
+
.action(async (sessionId, opts) => {
|
|
215
|
+
const payload = {
|
|
216
|
+
sessionId,
|
|
217
|
+
limit: Number(opts.limit ?? 20),
|
|
218
|
+
};
|
|
219
|
+
const response = await callDaemon('/rpc/sessions/events', payload, opts.host, Number(opts.port));
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
console.error(chalk.red('Failed to fetch events:'), response.error ?? 'unknown');
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const { events, warnings, session } = response;
|
|
226
|
+
if (session) {
|
|
227
|
+
console.log(chalk.bold(`Session ${session.id}`));
|
|
228
|
+
console.log(`Agent: ${session.agentType}`);
|
|
229
|
+
if (session.command) {
|
|
230
|
+
console.log(`Command: ${session.command}`);
|
|
231
|
+
}
|
|
232
|
+
console.log('Started:', formatDate(session.createdAt ?? session.created_at));
|
|
233
|
+
console.log('Status:', session.status);
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
if (warnings?.length) {
|
|
237
|
+
console.warn(chalk.yellow('Warnings:'));
|
|
238
|
+
for (const warning of warnings) {
|
|
239
|
+
console.warn('-', warning);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (opts.json) {
|
|
243
|
+
console.log(JSON.stringify(events, null, 2));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!events.length) {
|
|
247
|
+
console.log('No events recorded yet.');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
for (const event of events) {
|
|
251
|
+
renderEvent(event);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const integrationsCommand = program.command('integrations').description('Inspect agent integrations');
|
|
256
|
+
|
|
257
|
+
integrationsCommand
|
|
258
|
+
.command('sync <agent>')
|
|
259
|
+
.description('Check local integration status (read-only)')
|
|
260
|
+
.option('--json', 'Output JSON report')
|
|
261
|
+
.action(async (agent: string, opts) => {
|
|
262
|
+
if (!['codex', 'claude', 'gemini'].includes(agent)) {
|
|
263
|
+
console.error(chalk.red('Supported agents: codex, claude, gemini'));
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const report = await runIntegrationSync(agent as IntegrationAgent);
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
console.log(JSON.stringify(report, null, 2));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
renderIntegrationReport(report);
|
|
274
|
+
} catch (err: any) {
|
|
275
|
+
console.error(chalk.red('Failed to run sync:'), err.message ?? err);
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
integrationsCommand
|
|
281
|
+
.command('install <agent>')
|
|
282
|
+
.description('Install helper assets for an integration')
|
|
283
|
+
.action(async (agent: string) => {
|
|
284
|
+
if (!['codex', 'claude', 'gemini'].includes(agent)) {
|
|
285
|
+
console.error(chalk.red('Supported agents: codex, claude, gemini'));
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const message = await installIntegration(agent as IntegrationAgent);
|
|
291
|
+
console.log(chalk.green('Install complete:'), message);
|
|
292
|
+
} catch (err: any) {
|
|
293
|
+
console.error(chalk.red('Install failed:'), err?.message ?? err);
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
program.parseAsync(process.argv);
|
|
299
|
+
|
|
300
|
+
async function queryDaemon(pathname: string, host = DEFAULT_HOST, port = DEFAULT_PORT) {
|
|
301
|
+
try {
|
|
302
|
+
const res = await fetch(`http://${host}:${port}${pathname}`);
|
|
303
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
304
|
+
return await res.json();
|
|
305
|
+
} catch (err: any) {
|
|
306
|
+
return { ok: false, error: err?.message ?? 'connection_error' };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function callDaemon(pathname: string, payload: any, host = DEFAULT_HOST, port = DEFAULT_PORT) {
|
|
311
|
+
try {
|
|
312
|
+
const res = await fetch(`http://${host}:${port}${pathname}`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify(payload),
|
|
315
|
+
headers: { 'content-type': 'application/json' },
|
|
316
|
+
});
|
|
317
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
318
|
+
return await res.json();
|
|
319
|
+
} catch (err: any) {
|
|
320
|
+
return { ok: false, error: err?.message ?? 'connection_error' };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderEvent(event: any) {
|
|
325
|
+
const time = event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : 'unknown time';
|
|
326
|
+
const header = `[${time}] ${event.role ? event.role.toUpperCase() : event.eventType}`;
|
|
327
|
+
if (event.eventType === 'message') {
|
|
328
|
+
console.log(header);
|
|
329
|
+
console.log(` ${event.text ?? event.summary ?? JSON.stringify(event.content)}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (event.eventType === 'tool_use') {
|
|
333
|
+
console.log(`${header} TOOL ${event.toolName ?? ''}`.trim());
|
|
334
|
+
console.log(` Input: ${truncate(event.toolInput ?? '', 200)}`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (event.eventType === 'tool_result') {
|
|
338
|
+
console.log(`${header} TOOL RESULT`);
|
|
339
|
+
console.log(` Output: ${truncate(event.toolOutput ?? '', 200)}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (event.eventType === 'error') {
|
|
343
|
+
console.log(`${header} ERROR ${event.errorType ?? ''}`.trim());
|
|
344
|
+
console.log(` ${event.errorMessage}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
console.log(header);
|
|
348
|
+
console.log(' ', JSON.stringify(event));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function truncate(value: string, max = 200) {
|
|
352
|
+
if (!value) return '';
|
|
353
|
+
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function inferSessionIdFromPath(filePath: string): string {
|
|
357
|
+
const base = path.basename(filePath);
|
|
358
|
+
return base.replace(/\.[^.]+$/, '') || `gemini-${Date.now()}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function formatDate(value?: string): string {
|
|
362
|
+
if (!value) return '-';
|
|
363
|
+
const date = new Date(value);
|
|
364
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
365
|
+
return date.toLocaleString();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function readPidFile(): Promise<number | null> {
|
|
369
|
+
try {
|
|
370
|
+
const contents = await fs.readFile(PID_FILE, 'utf8');
|
|
371
|
+
const pid = Number(contents.trim());
|
|
372
|
+
return Number.isFinite(pid) ? pid : null;
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function tailLogs(follow: boolean) {
|
|
379
|
+
const logPath = path.join(DATA_DIRS.logs, 'managerd.log');
|
|
380
|
+
try {
|
|
381
|
+
await fs.access(logPath);
|
|
382
|
+
} catch {
|
|
383
|
+
console.log('Logs not found yet. Run managerd once to create logs.');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const printCurrent = async () => {
|
|
388
|
+
const text = await fs.readFile(logPath, 'utf8');
|
|
389
|
+
if (text) process.stdout.write(text);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (!follow) {
|
|
393
|
+
await printCurrent();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let position = 0;
|
|
398
|
+
const stats = await fs.stat(logPath);
|
|
399
|
+
if (stats.size > 0) {
|
|
400
|
+
await printCurrent();
|
|
401
|
+
position = stats.size;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.log(chalk.gray('--- streaming managerd logs (Ctrl+C to stop) ---'));
|
|
405
|
+
const watcher = fsSync.watch(logPath, async (event) => {
|
|
406
|
+
if (event !== 'change') return;
|
|
407
|
+
try {
|
|
408
|
+
const handle = await fs.open(logPath, 'r');
|
|
409
|
+
const current = await handle.stat();
|
|
410
|
+
if (current.size > position) {
|
|
411
|
+
const length = current.size - position;
|
|
412
|
+
const buffer = Buffer.alloc(length);
|
|
413
|
+
await handle.read(buffer, 0, length, position);
|
|
414
|
+
process.stdout.write(buffer.toString());
|
|
415
|
+
position = current.size;
|
|
416
|
+
}
|
|
417
|
+
await handle.close();
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(chalk.red('Failed to read logs:'), err instanceof Error ? err.message : err);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const cleanup = () => watcher.close();
|
|
424
|
+
process.on('SIGINT', () => {
|
|
425
|
+
cleanup();
|
|
426
|
+
process.exit(0);
|
|
427
|
+
});
|
|
428
|
+
process.on('SIGTERM', () => {
|
|
429
|
+
cleanup();
|
|
430
|
+
process.exit(0);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function installSystemdService() {
|
|
435
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
436
|
+
await fs.mkdir(serviceDir, { recursive: true });
|
|
437
|
+
const servicePath = path.join(serviceDir, 'managerd.service');
|
|
438
|
+
const execStart = `${process.execPath} ${getManagerdEntry()}`;
|
|
439
|
+
const workingDir = path.dirname(getManagerdEntry());
|
|
440
|
+
const unit = `[Unit]\nDescription=Justevery Manager Daemon\nAfter=network.target\n\n[Service]\nExecStart=${execStart}\nRestart=on-failure\nEnvironment=MANAGERD_STORAGE=memory\nWorkingDirectory=${workingDir}\n\n[Install]\nWantedBy=default.target\n`;
|
|
441
|
+
await fs.writeFile(servicePath, unit, 'utf8');
|
|
442
|
+
console.log('Installed systemd service to', servicePath);
|
|
443
|
+
console.log('Run the following commands to enable it:');
|
|
444
|
+
console.log(' systemctl --user daemon-reload');
|
|
445
|
+
console.log(' systemctl --user enable --now managerd.service');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getManagerdEntry(): string {
|
|
449
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
450
|
+
return path.resolve(here, 'managerd.js');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function renderIntegrationReport(report: Awaited<ReturnType<typeof runIntegrationSync>>) {
|
|
454
|
+
console.log(chalk.bold(`Integration: ${report.agent}`));
|
|
455
|
+
console.log('Status:', report.status);
|
|
456
|
+
console.log('\nChecks:');
|
|
457
|
+
for (const check of report.checks) {
|
|
458
|
+
const prefix = statusIcon(check.status);
|
|
459
|
+
const detail = check.detail ? ` — ${check.detail}` : '';
|
|
460
|
+
console.log(` ${prefix} ${check.name}${detail}`);
|
|
461
|
+
}
|
|
462
|
+
if (report.manualSteps.length) {
|
|
463
|
+
console.log('\nManual steps:');
|
|
464
|
+
report.manualSteps.forEach((step, idx) => {
|
|
465
|
+
console.log(` ${idx + 1}. ${step}`);
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
console.log('\nNo manual steps required.');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function statusIcon(status: string) {
|
|
473
|
+
switch (status) {
|
|
474
|
+
case 'ok':
|
|
475
|
+
return chalk.green('✔');
|
|
476
|
+
case 'missing':
|
|
477
|
+
return chalk.red('✖');
|
|
478
|
+
case 'warn':
|
|
479
|
+
return chalk.yellow('!');
|
|
480
|
+
default:
|
|
481
|
+
return chalk.gray('-');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { DEFAULT_CODEX_HISTORY, SessionStatus } from '../constants.js';
|
|
6
|
+
import { SessionStore } from '../storage/session-store.js';
|
|
7
|
+
|
|
8
|
+
export interface CodexLaunchRequest {
|
|
9
|
+
command?: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
disableManagedFlags?: boolean;
|
|
12
|
+
autoRestart?: boolean;
|
|
13
|
+
maxRestarts?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CodexLaunchResponse {
|
|
17
|
+
localSessionId: string;
|
|
18
|
+
toolSessionId: string;
|
|
19
|
+
command: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CodexConnector {
|
|
24
|
+
#store: SessionStore;
|
|
25
|
+
#sessions = new Map<
|
|
26
|
+
string,
|
|
27
|
+
{
|
|
28
|
+
command: string;
|
|
29
|
+
args: string[];
|
|
30
|
+
env: NodeJS.ProcessEnv;
|
|
31
|
+
autoRestart: boolean;
|
|
32
|
+
maxRestarts: number;
|
|
33
|
+
restarts: number;
|
|
34
|
+
child?: ReturnType<typeof spawn>;
|
|
35
|
+
}
|
|
36
|
+
>();
|
|
37
|
+
|
|
38
|
+
constructor(store: SessionStore) {
|
|
39
|
+
this.#store = store;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async launch(request: CodexLaunchRequest = {}): Promise<CodexLaunchResponse> {
|
|
43
|
+
const command = request.command ?? process.env.MANAGER_CODEX_COMMAND ?? 'codex';
|
|
44
|
+
const extraArgs = request.args ?? [];
|
|
45
|
+
const session = this.#store.createSession('codex', command, extraArgs);
|
|
46
|
+
const codexHistoryDir = process.env.MANAGER_CODEX_HISTORY ?? DEFAULT_CODEX_HISTORY;
|
|
47
|
+
const sessionDir = path.join(codexHistoryDir, session.toolSessionId);
|
|
48
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const enforcedFlags = request.disableManagedFlags || process.env.MANAGER_CODEX_DISABLE_FLAGS === '1'
|
|
51
|
+
? []
|
|
52
|
+
: ['--output_dir', sessionDir, '--output_format', 'json'];
|
|
53
|
+
const env = {
|
|
54
|
+
...process.env,
|
|
55
|
+
MANAGER_SESSION_ID: session.id,
|
|
56
|
+
};
|
|
57
|
+
const state = {
|
|
58
|
+
command,
|
|
59
|
+
args: [...extraArgs, ...enforcedFlags],
|
|
60
|
+
env,
|
|
61
|
+
autoRestart: request.autoRestart !== false,
|
|
62
|
+
maxRestarts: request.maxRestarts ?? 3,
|
|
63
|
+
restarts: 0,
|
|
64
|
+
};
|
|
65
|
+
this.#sessions.set(session.id, state);
|
|
66
|
+
this.spawnProcess(session.id, state);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
localSessionId: session.id,
|
|
70
|
+
toolSessionId: session.toolSessionId,
|
|
71
|
+
command,
|
|
72
|
+
args: [...extraArgs, ...enforcedFlags],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private spawnProcess(
|
|
77
|
+
sessionId: string,
|
|
78
|
+
state: {
|
|
79
|
+
command: string;
|
|
80
|
+
args: string[];
|
|
81
|
+
env: NodeJS.ProcessEnv;
|
|
82
|
+
autoRestart: boolean;
|
|
83
|
+
maxRestarts: number;
|
|
84
|
+
restarts: number;
|
|
85
|
+
child?: ReturnType<typeof spawn>;
|
|
86
|
+
},
|
|
87
|
+
) {
|
|
88
|
+
const child = spawn(state.command, state.args, {
|
|
89
|
+
env: state.env,
|
|
90
|
+
stdio: 'ignore',
|
|
91
|
+
});
|
|
92
|
+
state.child = child;
|
|
93
|
+
this.#store.updateStatus(sessionId, SessionStatus.Running);
|
|
94
|
+
|
|
95
|
+
child.once('exit', (code) => this.handleExit(sessionId, state, code));
|
|
96
|
+
child.once('error', (err) => {
|
|
97
|
+
console.error('[codex] failed to launch', err);
|
|
98
|
+
this.handleExit(sessionId, state, -1);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private handleExit(
|
|
103
|
+
sessionId: string,
|
|
104
|
+
state: {
|
|
105
|
+
command: string;
|
|
106
|
+
args: string[];
|
|
107
|
+
env: NodeJS.ProcessEnv;
|
|
108
|
+
autoRestart: boolean;
|
|
109
|
+
maxRestarts: number;
|
|
110
|
+
restarts: number;
|
|
111
|
+
child?: ReturnType<typeof spawn>;
|
|
112
|
+
},
|
|
113
|
+
code: number | null,
|
|
114
|
+
) {
|
|
115
|
+
const exitCode = code ?? -1;
|
|
116
|
+
const successful = exitCode === 0;
|
|
117
|
+
this.#store.updateStatus(sessionId, successful ? SessionStatus.Completed : SessionStatus.Failed, exitCode);
|
|
118
|
+
if (state.autoRestart && !successful && state.restarts < state.maxRestarts) {
|
|
119
|
+
state.restarts += 1;
|
|
120
|
+
const delay = Math.min(1000 * state.restarts, 5000);
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
console.warn(`[codex] restarting session ${sessionId} attempt ${state.restarts}`);
|
|
123
|
+
this.spawnProcess(sessionId, state);
|
|
124
|
+
}, delay);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.#sessions.delete(sessionId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function describeCodexConnector(): string {
|
|
132
|
+
const command = process.env.MANAGER_CODEX_COMMAND ?? 'codex';
|
|
133
|
+
const dir = process.env.MANAGER_CODEX_HISTORY ?? DEFAULT_CODEX_HISTORY;
|
|
134
|
+
return `command=${command} historyDir=${dir}`;
|
|
135
|
+
}
|