ai-agentmanager 1.0.0 → 1.0.2
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/CHANGELOG.md +57 -0
- package/NOTICE +1 -1
- package/README.md +33 -15
- package/SECURITY.md +6 -6
- package/bin/agentmgr-hub.js +1 -1
- package/bin/agentmgr.js +10 -3
- package/docs/architecture.md +1 -1
- package/docs/distributed.md +1 -1
- package/docs/engines.md +5 -5
- package/docs/images/agents.jpg +0 -0
- package/docs/images/conflict-radar.jpg +0 -0
- package/docs/plugins.md +2 -2
- package/package.json +6 -4
- package/public/app.js +180 -9
- package/public/hub/index.html +3 -3
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon.svg +1 -0
- package/public/index.html +25 -6
- package/public/manifest.webmanifest +16 -0
- package/public/share.js +1 -1
- package/public/style.css +36 -2
- package/src/about.js +70 -0
- package/src/brief.js +1 -1
- package/src/cli.js +42 -1
- package/src/conflicts.js +3 -3
- package/src/doctor.js +113 -0
- package/src/engines/antigravity.js +1 -1
- package/src/engines/claude.js +14 -0
- package/src/engines/codex.js +1 -1
- package/src/engines/index.js +9 -1
- package/src/git.js +2 -2
- package/src/inbox.js +1 -1
- package/src/judge.js +12 -0
- package/src/manager.js +29 -7
- package/src/notify.js +1 -1
- package/src/plugins.js +1 -1
- package/src/preview.js +4 -4
- package/src/server.js +4 -1
- package/src/service.js +1 -1
- package/src/sessions.js +2 -2
- package/src/store.js +2 -2
- package/src/tracker.js +5 -1
package/src/doctor.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `agentmgr doctor`: checks what AI Agent Manager needs on this machine and says how to fix
|
|
2
|
+
// what's missing. Each check is { name, status: 'ok' | 'warn' | 'fail' | 'info', detail, fix }.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { HOST, PORT, loadConfig, paths } from './config.js';
|
|
9
|
+
import { api, BASE, isRunning } from './client.js';
|
|
10
|
+
import { ENGINES, installed, notInstalled } from './engines/index.js';
|
|
11
|
+
import { findBrowser } from './livebrowser.js';
|
|
12
|
+
|
|
13
|
+
const run = (bin, args, timeout = 8000) =>
|
|
14
|
+
new Promise((resolve) => {
|
|
15
|
+
execFile(bin, args, { timeout }, (err, stdout, stderr) => resolve({ ok: !err, out: `${stdout}${stderr}`.trim(), err }));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const firstLine = (s) => s.split('\n')[0].trim();
|
|
19
|
+
|
|
20
|
+
function portFree(port) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const srv = net.createServer();
|
|
23
|
+
srv.once('error', () => resolve(false));
|
|
24
|
+
srv.listen(port, HOST, () => srv.close(() => resolve(true)));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether Claude Code has credentials, without reading them: an API key, or a signed-in account. */
|
|
29
|
+
function claudeSignedIn() {
|
|
30
|
+
if (process.env.ANTHROPIC_API_KEY) return 'an API key (ANTHROPIC_API_KEY)';
|
|
31
|
+
try {
|
|
32
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
33
|
+
if (cfg.oauthAccount) return 'a Claude account';
|
|
34
|
+
} catch {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ptyWorks() {
|
|
39
|
+
try {
|
|
40
|
+
const pty = (await import('node-pty')).default;
|
|
41
|
+
return await new Promise((resolve) => {
|
|
42
|
+
const p = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : '/bin/sh', process.platform === 'win32' ? ['/c', 'exit 0'] : ['-c', 'exit 0'], {});
|
|
43
|
+
const t = setTimeout(() => resolve('started but did not finish'), 5000);
|
|
44
|
+
p.onExit(() => {
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
resolve(null);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return err.message;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function doctor() {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
const checks = [];
|
|
57
|
+
const add = (name, status, detail, fix) => checks.push({ name, status, detail, ...(fix ? { fix } : {}) });
|
|
58
|
+
|
|
59
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
60
|
+
add('Node.js', major >= 20 ? 'ok' : 'fail', `${process.versions.node}`, major >= 20 ? null : 'Install Node.js 20 or newer (https://nodejs.org)');
|
|
61
|
+
|
|
62
|
+
const ptyErr = await ptyWorks();
|
|
63
|
+
add('Terminals (node-pty)', ptyErr ? 'fail' : 'ok', ptyErr ? `can't start a terminal: ${ptyErr}` : 'can start terminals',
|
|
64
|
+
ptyErr ? 'Reinstall: npm install -g ai-agentmanager (or npm rebuild node-pty in a git checkout)' : null);
|
|
65
|
+
|
|
66
|
+
// The daemon's view wins: a login service may have a different PATH than this shell.
|
|
67
|
+
const running = await isRunning();
|
|
68
|
+
const daemonEngines = running ? await api('GET', '/engines').catch(() => null) : null;
|
|
69
|
+
for (const engine of Object.values(ENGINES)) {
|
|
70
|
+
const has = daemonEngines ? daemonEngines.find((e) => e.id === engine.id)?.installed : installed(engine, config);
|
|
71
|
+
const main = engine.id === (config.engine ?? 'claude');
|
|
72
|
+
if (!has) {
|
|
73
|
+
add(engine.label, main ? 'fail' : 'info', main ? 'not found' : 'not installed (optional)', main ? notInstalled(engine, config) : null);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const v = await run(engine.bin(config), ['--version']);
|
|
77
|
+
add(engine.label, 'ok', v.ok ? firstLine(v.out) : 'installed');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const signedIn = claudeSignedIn();
|
|
81
|
+
add('Claude sign-in', signedIn ? 'ok' : 'warn', signedIn ? `uses ${signedIn}` : "couldn't tell whether Claude Code is signed in",
|
|
82
|
+
signedIn ? null : 'Run `claude` in a terminal once and sign in');
|
|
83
|
+
|
|
84
|
+
const git = await run('git', ['--version']);
|
|
85
|
+
add('git', git.ok ? 'ok' : 'fail', git.ok ? firstLine(git.out) : 'not found', git.ok ? null : 'Install git (agents work in git worktrees)');
|
|
86
|
+
|
|
87
|
+
const gh = await run('gh', ['auth', 'status']);
|
|
88
|
+
if (gh.err?.code === 'ENOENT') add('GitHub CLI (gh)', 'warn', 'not installed: PRs and checks are not tracked', 'Install it (https://cli.github.com) and run `gh auth login`');
|
|
89
|
+
else add('GitHub CLI (gh)', gh.ok ? 'ok' : 'warn', gh.ok ? 'signed in' : 'not signed in: PRs and checks are not tracked', gh.ok ? null : 'Run `gh auth login`');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(paths.home, { recursive: true });
|
|
93
|
+
fs.accessSync(paths.home, fs.constants.W_OK);
|
|
94
|
+
add('Data folder', 'ok', paths.home);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
add('Data folder', 'fail', `${paths.home}: ${err.message}`, 'Make it writable, or point AGENTMGR_HOME at another folder');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (running) {
|
|
100
|
+
const a = await api('GET', '/about').catch(() => null);
|
|
101
|
+
add('Daemon', 'ok', `running at ${BASE}${a ? ` (version ${a.version}, pid ${a.daemon.pid})` : ''}`);
|
|
102
|
+
} else if (await portFree(PORT)) {
|
|
103
|
+
add('Daemon', 'info', `not running; port ${PORT} is free`, 'Start it with `agentmgr ui`');
|
|
104
|
+
} else {
|
|
105
|
+
add('Daemon', 'fail', `port ${PORT} is used by another program`, 'Stop that program, or set AGENTMGR_PORT to another port');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const chrome = findBrowser(config);
|
|
109
|
+
add('Chrome (live browser tabs)', chrome ? 'ok' : 'warn', chrome ?? 'not found: live browser tabs are unavailable (Lite mode still works)',
|
|
110
|
+
chrome ? null : 'Install Chrome, Chromium, Brave or Edge, or set "browserBin" in config.json');
|
|
111
|
+
|
|
112
|
+
return checks;
|
|
113
|
+
}
|
|
@@ -14,7 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import { norm, tailOf } from '../stuck.js';
|
|
15
15
|
import { extractRefs } from '../tracker.js';
|
|
16
16
|
|
|
17
|
-
// ANTIGRAVITY_CLI_HOME is Agent Manager's own override (tests); agy itself always uses this folder.
|
|
17
|
+
// ANTIGRAVITY_CLI_HOME is AI Agent Manager's own override (tests); agy itself always uses this folder.
|
|
18
18
|
const home = () => process.env.ANTIGRAVITY_CLI_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
19
19
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
20
20
|
const transcript = (id) => path.join(home(), 'brain', id, '.system_generated', 'logs', 'transcript_full.jsonl');
|
package/src/engines/claude.js
CHANGED
|
@@ -84,6 +84,20 @@ export default {
|
|
|
84
84
|
],
|
|
85
85
|
promptChoices: (screen) => promptChoices(screen),
|
|
86
86
|
|
|
87
|
+
/** Lines of Claude Code's own frame (input box, mode and status bar): never a question to the person. */
|
|
88
|
+
screenChrome: [
|
|
89
|
+
/\b(accept edits|bypass permissions|plan mode|auto mode|auto-accept edits) on\b/i,
|
|
90
|
+
/shift\+tab to cycle/i,
|
|
91
|
+
/\? for shortcuts/i,
|
|
92
|
+
/esc to interrupt/i,
|
|
93
|
+
/ctrl\+[a-z] to (expand|edit|show|hide)/i,
|
|
94
|
+
/update available/i,
|
|
95
|
+
/^[─━═-]{3,}/,
|
|
96
|
+
/^\/\w+$/,
|
|
97
|
+
/^[❯>]\s*$/,
|
|
98
|
+
],
|
|
99
|
+
install: 'Install Claude Code (https://docs.claude.com/en/docs/claude-code) and run `claude` once to sign in',
|
|
100
|
+
|
|
87
101
|
trust: (dir) => trustFolder(dir),
|
|
88
102
|
|
|
89
103
|
/** A one-shot, tool-less call; the prompt goes on stdin and the answer comes back as text. */
|
package/src/engines/codex.js
CHANGED
|
@@ -155,7 +155,7 @@ export default {
|
|
|
155
155
|
|
|
156
156
|
/** Full arguments for a launch. Codex takes options after `resume`, and instructions only as text. */
|
|
157
157
|
launchArgs({ extra = [], permissionMode, model, writableDir, instructions, sessionId, resume, prompt }) {
|
|
158
|
-
// Inline mode keeps the conversation in the terminal's scrollback, which Agent Manager replays.
|
|
158
|
+
// Inline mode keeps the conversation in the terminal's scrollback, which AI Agent Manager replays.
|
|
159
159
|
const opts = [...extra, '--no-alt-screen', ...(MODES[permissionMode] ?? MODES.default)];
|
|
160
160
|
if (model) opts.push('--model', model);
|
|
161
161
|
if (writableDir) opts.push('--add-dir', writableDir);
|
package/src/engines/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The agent engines Agent Manager can run. Each agent records its engine; agents from
|
|
1
|
+
// The agent engines AI Agent Manager can run. Each agent records its engine; agents from
|
|
2
2
|
// before engines existed are Claude Code agents.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -23,6 +23,14 @@ export function installed(engine, config) {
|
|
|
23
23
|
return (process.env.PATH ?? '').split(path.delimiter).some((d) => d && fs.existsSync(path.join(d, bin)));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** What to tell the person when an engine's command isn't on this machine. */
|
|
27
|
+
export function notInstalled(engine, config) {
|
|
28
|
+
const bin = engine.bin(config);
|
|
29
|
+
const how = engine.install ?? `Install ${engine.label}`;
|
|
30
|
+
const where = path.isAbsolute(bin) ? `${bin} doesn't exist` : `\`${bin}\` isn't on the daemon's PATH`;
|
|
31
|
+
return `${engine.label} not found: ${where}. ${how}, then run \`agentmgr restart\`. If it's installed somewhere else, set its path in config.json${engine.id === 'claude' ? ' ("claudeBin") or AGENTMGR_CLAUDE_BIN' : ` ("engines": {"${engine.id}": {"bin": "…"}})`}.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
/** Every engine, with whether it can run here, for pickers and the hub. */
|
|
27
35
|
export function listEngines(config) {
|
|
28
36
|
return Object.values(ENGINES).map((e) => ({ id: e.id, label: e.label, installed: installed(e, config), permissionModes: e.permissionModes, supports: e.supports }));
|
package/src/git.js
CHANGED
|
@@ -278,9 +278,9 @@ export async function changes(dir, baseSha) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
const SNAPSHOT_ENV = {
|
|
281
|
-
GIT_AUTHOR_NAME: 'Agent Manager',
|
|
281
|
+
GIT_AUTHOR_NAME: 'AI Agent Manager',
|
|
282
282
|
GIT_AUTHOR_EMAIL: 'agentmgr@localhost',
|
|
283
|
-
GIT_COMMITTER_NAME: 'Agent Manager',
|
|
283
|
+
GIT_COMMITTER_NAME: 'AI Agent Manager',
|
|
284
284
|
GIT_COMMITTER_EMAIL: 'agentmgr@localhost',
|
|
285
285
|
};
|
|
286
286
|
|
package/src/inbox.js
CHANGED
|
@@ -12,7 +12,7 @@ export const questionsPath = (agentId) => path.join(briefDir(agentId), QUESTIONS
|
|
|
12
12
|
export const questionsPrompt = (file) =>
|
|
13
13
|
`When you need a decision from the user that doesn't have to stop all your work (a choice between options, ` +
|
|
14
14
|
`a preference, a missing detail), don't wait in the terminal: add it to ${file} and keep going on whatever you can ` +
|
|
15
|
-
'do meanwhile, using your best default. Agent Manager collects these into the user\'s inbox. One question per heading, ' +
|
|
15
|
+
'do meanwhile, using your best default. AI Agent Manager collects these into the user\'s inbox. One question per heading, ' +
|
|
16
16
|
'never edit or remove earlier ones:\n' +
|
|
17
17
|
'## <the question, one line>\n' +
|
|
18
18
|
'Options: <option A> | <option B> (optional)\n' +
|
package/src/judge.js
CHANGED
|
@@ -78,6 +78,16 @@ export function stuckPrompt(ctx, finding, events, past = []) {
|
|
|
78
78
|
].join('\n');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** The screen without the engine's own frame (input box, mode indicators, status bar). */
|
|
82
|
+
export function stripChrome(screen, patterns = []) {
|
|
83
|
+
if (!patterns.length) return screen;
|
|
84
|
+
return screen
|
|
85
|
+
.split('\n')
|
|
86
|
+
.filter((line) => !patterns.some((re) => re.test(line.trim())))
|
|
87
|
+
.join('\n')
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export function screenPrompt(ctx, screen, past = []) {
|
|
82
92
|
return [
|
|
83
93
|
'You supervise an AI coding agent running in a terminal. It has stopped producing output.',
|
|
@@ -87,6 +97,8 @@ export function screenPrompt(ctx, screen, past = []) {
|
|
|
87
97
|
'- "error": it crashed, hit an error it cannot recover from, or a limit (rate limit, auth, quota)',
|
|
88
98
|
'- "idle": finished a step and is simply waiting for the next instruction, nothing specific to answer',
|
|
89
99
|
'For "question", copy the question in plain words and list the choices it offered, if any.',
|
|
100
|
+
'The tool\'s own input box, mode indicators and status bar (for example "accept edits on", "plan mode on",',
|
|
101
|
+
'"shift+tab to cycle", "? for shortcuts", "update available") are not questions: only the agent\'s own words count.',
|
|
90
102
|
RULES,
|
|
91
103
|
'Shape: {"state": "question|done|error|idle", "confidence": 0.0, "summary": "one line for an inbox", "question": "or empty", "choices": ["..."]}',
|
|
92
104
|
'',
|
package/src/manager.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import pty from 'node-pty';
|
|
6
6
|
import { expandHome, paths } from './config.js';
|
|
7
|
-
import { DEFAULT_ENGINE, ENGINES, engineOf, installed } from './engines/index.js';
|
|
7
|
+
import { DEFAULT_ENGINE, ENGINES, engineOf, installed, notInstalled } from './engines/index.js';
|
|
8
8
|
import {
|
|
9
9
|
DOC_KINDS,
|
|
10
10
|
briefDir,
|
|
@@ -42,7 +42,7 @@ import * as git from './git.js';
|
|
|
42
42
|
import { conflictMessage, findConflicts, related, repoKey, summarize as summarizeConflicts } from './conflicts.js';
|
|
43
43
|
import { answerMessage, loadQuestions, questionsPath, questionsPrompt } from './inbox.js';
|
|
44
44
|
import { STUCK_DEFAULTS, detectStuck } from './stuck.js';
|
|
45
|
-
import { Judge, clashPrompt, finishMessage, hash, reviewPrompt, screenPrompt, stuckPrompt } from './judge.js';
|
|
45
|
+
import { Judge, clashPrompt, finishMessage, hash, reviewPrompt, screenPrompt, stripChrome, stuckPrompt } from './judge.js';
|
|
46
46
|
import { findLocalUrls, findSignInLinks, parseCallback, parseUrl } from './preview.js';
|
|
47
47
|
import { Plugins } from './plugins.js';
|
|
48
48
|
import { runningClaude, scanClaudeSessions } from './sessions.js';
|
|
@@ -191,6 +191,7 @@ export class AgentManager extends EventEmitter {
|
|
|
191
191
|
this.config = config;
|
|
192
192
|
this.plugins = plugins; // policies before actions, listeners after (src/plugins.js)
|
|
193
193
|
this.runtimes = new Map(); // agent id -> { pty, chunks, size, clients, lastOutputAt, busy, stopping, exited }
|
|
194
|
+
this.inputAt = new Map(); // agent id -> when you last typed into its session or a shell
|
|
194
195
|
this.shells = new Map(); // agent id -> ancillary shell runtime in the agent's folder
|
|
195
196
|
this.cursors = new Map(); // session id -> transcript scan cursor
|
|
196
197
|
this.locks = new Map();
|
|
@@ -268,6 +269,7 @@ export class AgentManager extends EventEmitter {
|
|
|
268
269
|
...agent,
|
|
269
270
|
results: this.results(agent, rt, prs),
|
|
270
271
|
priority: agent.priority ?? 2,
|
|
272
|
+
activeAt: this.activeAt(agent, rt),
|
|
271
273
|
projectInfo: this.projectInfo(agent),
|
|
272
274
|
usage,
|
|
273
275
|
// Claude Code's own cost figure from the transcripts (absent until it records one).
|
|
@@ -295,9 +297,22 @@ export class AgentManager extends EventEmitter {
|
|
|
295
297
|
};
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
/**
|
|
301
|
+
* When the agent last did real work or you last worked with it: its conversation,
|
|
302
|
+
* its brief, your typing, or output while busy (not the redraw after a restart).
|
|
303
|
+
*/
|
|
304
|
+
activeAt(agent, rt) {
|
|
305
|
+
const times = [agent.createdAt, agent.brief?.updatedAt, ...(agent.sessions ?? []).map((s) => s.lastMessageAt ?? s.lastActivity)]
|
|
306
|
+
.map((t) => (t ? Date.parse(t) : 0));
|
|
307
|
+
times.push(this.inputAt.get(agent.id) ?? 0);
|
|
308
|
+
if (rt?.pty && rt.busy && rt.lastOutputAt - (rt.startedAt ?? 0) > 60_000) times.push(rt.lastOutputAt);
|
|
309
|
+
const t = Math.max(0, ...times.filter(Number.isFinite));
|
|
310
|
+
return t ? new Date(t).toISOString() : null;
|
|
311
|
+
}
|
|
312
|
+
|
|
298
313
|
/**
|
|
299
314
|
* The end result / status / blockers for an agent: what the agent wrote under
|
|
300
|
-
* "## Results", plus what Agent Manager knows (PRs, checks, prompts, errors).
|
|
315
|
+
* "## Results", plus what AI Agent Manager knows (PRs, checks, prompts, errors).
|
|
301
316
|
*/
|
|
302
317
|
results(agent, rt, prs) {
|
|
303
318
|
const r = agent.brief?.results;
|
|
@@ -749,7 +764,7 @@ export class AgentManager extends EventEmitter {
|
|
|
749
764
|
*/
|
|
750
765
|
start(ref, { fresh = false, sessionId } = {}) {
|
|
751
766
|
const agent = this.mustGet(ref);
|
|
752
|
-
if (this.closing) throw Object.assign(new Error('Agent Manager is shutting down'), { status: 503 });
|
|
767
|
+
if (this.closing) throw Object.assign(new Error('AI Agent Manager is shutting down'), { status: 503 });
|
|
753
768
|
const live = this.runtimes.get(agent.id);
|
|
754
769
|
if (!live?.pty) {
|
|
755
770
|
this.plugins.check('agent.start', { agent: this.auditView(agent), fresh });
|
|
@@ -892,6 +907,11 @@ export class AgentManager extends EventEmitter {
|
|
|
892
907
|
}
|
|
893
908
|
}
|
|
894
909
|
|
|
910
|
+
if (!installed(engine, this.config)) {
|
|
911
|
+
const error = notInstalled(engine, this.config);
|
|
912
|
+
this.patch(agent.id, { status: 'error', error });
|
|
913
|
+
throw Object.assign(new Error(error), { status: 400 });
|
|
914
|
+
}
|
|
895
915
|
let proc;
|
|
896
916
|
try {
|
|
897
917
|
proc = pty.spawn(engine.bin(this.config), args, {
|
|
@@ -1198,6 +1218,8 @@ export class AgentManager extends EventEmitter {
|
|
|
1198
1218
|
}
|
|
1199
1219
|
|
|
1200
1220
|
write(id, data, kind = 'claude') {
|
|
1221
|
+
// Focus reports and terminal replies are not you typing.
|
|
1222
|
+
if (!/^(\x1b\[[\d;?>]*[IOcRnt])+$/.test(data)) this.inputAt.set(id, Date.now());
|
|
1201
1223
|
this.termRuntime(id, kind)?.pty?.write(data);
|
|
1202
1224
|
}
|
|
1203
1225
|
|
|
@@ -1432,7 +1454,7 @@ export class AgentManager extends EventEmitter {
|
|
|
1432
1454
|
|
|
1433
1455
|
// ------------------------------------------------- sessions started elsewhere
|
|
1434
1456
|
|
|
1435
|
-
/** Claude Code sessions started outside Agent Manager (in a terminal), to continue here. */
|
|
1457
|
+
/** Claude Code sessions started outside AI Agent Manager (in a terminal), to continue here. */
|
|
1436
1458
|
scanSessions({ days = 30 } = {}) {
|
|
1437
1459
|
const known = new Set(this.store.list().flatMap((a) => a.sessions.map((x) => x.id)));
|
|
1438
1460
|
return scanClaudeSessions({ days: Math.max(1, Math.min(365, Number(days) || 30)), known, excludeDir: paths.home });
|
|
@@ -1898,7 +1920,7 @@ export class AgentManager extends EventEmitter {
|
|
|
1898
1920
|
session.usage = usage;
|
|
1899
1921
|
changed = true;
|
|
1900
1922
|
}
|
|
1901
|
-
for (const key of ['title', 'lastPrompt', 'cost', 'lastActivity']) {
|
|
1923
|
+
for (const key of ['title', 'lastPrompt', 'cost', 'lastActivity', 'lastMessageAt']) {
|
|
1902
1924
|
if (res[key] != null && JSON.stringify(session[key]) !== JSON.stringify(res[key])) {
|
|
1903
1925
|
session[key] = res[key];
|
|
1904
1926
|
changed = true;
|
|
@@ -2253,7 +2275,7 @@ export class AgentManager extends EventEmitter {
|
|
|
2253
2275
|
async judgeScreen(id, rt) {
|
|
2254
2276
|
const agent = this.store.get(id);
|
|
2255
2277
|
if (!agent) return;
|
|
2256
|
-
const screen = this.screenText(rt);
|
|
2278
|
+
const screen = stripChrome(this.screenText(rt), engineOf(agent).screenChrome);
|
|
2257
2279
|
if (!screen) return;
|
|
2258
2280
|
const h = hash(screen);
|
|
2259
2281
|
if (rt.screenVerdict?.hash === h) return this.applyScreen(id, rt, rt.screenVerdict.data);
|
package/src/notify.js
CHANGED
|
@@ -269,7 +269,7 @@ export class Notifier {
|
|
|
269
269
|
const link = this.linkFor(agent.id);
|
|
270
270
|
const intent = n.includeIntent && agent.intent ? `\n> ${agent.intent.replace(/\s+/g, ' ').slice(0, 200)}` : '';
|
|
271
271
|
const prio = agent.priority <= 1 ? `[P${agent.priority}] ` : '';
|
|
272
|
-
const text = `${icon} ${prio ? `*${prio.trim()}* ` : ''}*${agent.name}*${project} ${what}${intent}${link ? `\n<${link}|Open in Agent Manager>` : ''}`;
|
|
272
|
+
const text = `${icon} ${prio ? `*${prio.trim()}* ` : ''}*${agent.name}*${project} ${what}${intent}${link ? `\n<${link}|Open in AI Agent Manager>` : ''}`;
|
|
273
273
|
const plain = `${icon} ${prio}${agent.name}${project} ${what.replace(/\*/g, '')}${intent}${link ? `\nOpen: ${link}` : ''}`;
|
|
274
274
|
sendSlack(text, n, plain)
|
|
275
275
|
.then(() => this.record(kind, agent, null, urgency))
|
package/src/plugins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Plugins: how a separate package extends Agent Manager without changing it. A plugin is an
|
|
1
|
+
// Plugins: how a separate package extends AI Agent Manager without changing it. A plugin is an
|
|
2
2
|
// ES module listed in config.json "plugins" (the daemon) or AGENTMGR_HUB_PLUGINS (the hub),
|
|
3
3
|
// whose default export (or `register`) receives the API below:
|
|
4
4
|
//
|
package/src/preview.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// On this computer the tab loads the URL directly. From another device each site gets its
|
|
5
5
|
// own HTTPS listener (same certificate and sign-in as remote access) that relays everything
|
|
6
6
|
// to it at the root path, so apps that use absolute paths, redirects and WebSockets (hot
|
|
7
|
-
// reload) keep working. This daemon's own ports are never relayed, and the Agent Manager
|
|
7
|
+
// reload) keep working. This daemon's own ports are never relayed, and the AI Agent Manager
|
|
8
8
|
// session cookie is stripped before forwarding.
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import https from 'node:https';
|
|
@@ -170,7 +170,7 @@ export class Previews {
|
|
|
170
170
|
const tport = targetPort(url);
|
|
171
171
|
const own = new Set([...this.reserved(), ...[...this.listeners.values()].map((l) => l.port)]);
|
|
172
172
|
const self = isLoopback(url) || this.ownHosts().has(url.hostname.replace(/^\[|\]$/g, '').toLowerCase());
|
|
173
|
-
if (self && own.has(tport)) throw Object.assign(new Error("That is Agent Manager's own port"), { status: 400 });
|
|
173
|
+
if (self && own.has(tport)) throw Object.assign(new Error("That is AI Agent Manager's own port"), { status: 400 });
|
|
174
174
|
const key = `${url.protocol}//${url.hostname}:${tport}`;
|
|
175
175
|
const existing = this.listeners.get(key);
|
|
176
176
|
if (existing) return existing.port;
|
|
@@ -223,13 +223,13 @@ export class Previews {
|
|
|
223
223
|
const cookies = req.headers.cookie ?? '';
|
|
224
224
|
const until = this.okCookies.get(cookies);
|
|
225
225
|
if (until && until > Date.now()) return null;
|
|
226
|
-
if (!this.signedIn(cookies)) return [401, 'Sign in to Agent Manager on this machine first, then open this tab again.'];
|
|
226
|
+
if (!this.signedIn(cookies)) return [401, 'Sign in to AI Agent Manager on this machine first, then open this tab again.'];
|
|
227
227
|
if (this.okCookies.size > 100) this.okCookies.clear();
|
|
228
228
|
this.okCookies.set(cookies, Date.now() + 10_000);
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
/** Request headers as the local server expects them: its own Host and Origin, no Agent Manager cookie. */
|
|
232
|
+
/** Request headers as the local server expects them: its own Host and Origin, no AI Agent Manager cookie. */
|
|
233
233
|
forwardHeaders(entry, req, upgrade) {
|
|
234
234
|
const { target } = entry;
|
|
235
235
|
const self = `${target.protocol}//${target.label}`;
|
package/src/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { Previews, parseUrl } from './preview.js';
|
|
|
18
18
|
import { LiveBrowser } from './livebrowser.js';
|
|
19
19
|
import * as remote from './remote.js';
|
|
20
20
|
import { Store } from './store.js';
|
|
21
|
+
import { about, latestRelease } from './about.js';
|
|
21
22
|
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
@@ -337,7 +338,7 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
337
338
|
api.post(
|
|
338
339
|
'/notify/test',
|
|
339
340
|
wrap(async () => {
|
|
340
|
-
await sendSlack(':wave: Test from Agent Manager: alerts will arrive here when an agent needs you.', loadNotify());
|
|
341
|
+
await sendSlack(':wave: Test from AI Agent Manager: alerts will arrive here when an agent needs you.', loadNotify());
|
|
341
342
|
return { ok: true };
|
|
342
343
|
}),
|
|
343
344
|
);
|
|
@@ -370,6 +371,8 @@ export async function startServer({ port = PORT, host = HOST } = {}) {
|
|
|
370
371
|
api.post('/agents/:id/waiting/dismiss', wrap((req) => manager.dismissWaiting(req.params.id)));
|
|
371
372
|
api.post('/agents/:id/review', wrap((req) => manager.requestReview(req.params.id, { force: true })));
|
|
372
373
|
api.post('/agents/:id/review/ack', wrap((req) => manager.ackReview(req.params.id, req.body?.action)));
|
|
374
|
+
api.get('/about', wrap(() => about({ port, config: { home: paths.home, claudeBin: config.claudeBin } })));
|
|
375
|
+
api.get('/about/latest', wrap(() => latestRelease()));
|
|
373
376
|
api.get('/judge', wrap(() => manager.judge.status()));
|
|
374
377
|
// On or off for this daemon, kept in config.json. The judge spends calls on your own engine plan.
|
|
375
378
|
api.put('/judge', wrap((req) => {
|
package/src/service.js
CHANGED
|
@@ -75,7 +75,7 @@ export function unit() {
|
|
|
75
75
|
const env = Object.entries(passEnv()).map(([k, v]) => `Environment=${k}=${v}\n`).join('');
|
|
76
76
|
const cmd = command().map((a) => `"${a.replace(/(["\\])/g, '\\$1')}"`).join(' ');
|
|
77
77
|
return `[Unit]
|
|
78
|
-
Description=Agent Manager daemon (background Claude Code agents)
|
|
78
|
+
Description=AI Agent Manager daemon (background Claude Code agents)
|
|
79
79
|
|
|
80
80
|
[Service]
|
|
81
81
|
ExecStart=${cmd}
|
package/src/sessions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Claude Code sessions started outside Agent Manager (a plain `claude` in a terminal), so they
|
|
1
|
+
// Claude Code sessions started outside AI Agent Manager (a plain `claude` in a terminal), so they
|
|
2
2
|
// can be continued here. Claude Code keeps each session as ~/.claude/projects/<cwd>/<id>.jsonl;
|
|
3
3
|
// the start and end of each file say where it ran, its title, branch and first ask.
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
@@ -85,7 +85,7 @@ export function runningClaude() {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Interactive Claude Code sessions from the last `days`, newest first, leaving out `known`
|
|
88
|
-
* session IDs (Agent Manager's own) and anything run under `excludeDir` (its one-off calls and
|
|
88
|
+
* session IDs (AI Agent Manager's own) and anything run under `excludeDir` (its one-off calls and
|
|
89
89
|
* workspaces). inUse: "open" when a running claude names the session, "maybe" when one runs in
|
|
90
90
|
* its folder and the session was active in the last 10 minutes.
|
|
91
91
|
*/
|
package/src/store.js
CHANGED
|
@@ -12,7 +12,7 @@ const MIGRATIONS = {};
|
|
|
12
12
|
* so a crash mid-write never loses the session IDs.
|
|
13
13
|
*
|
|
14
14
|
* Upgrades: older files are copied to `<file>.v<N>.bak` and migrated one version at a time.
|
|
15
|
-
* A file from a newer Agent Manager is refused rather than overwritten (downgrades would lose
|
|
15
|
+
* A file from a newer AI Agent Manager is refused rather than overwritten (downgrades would lose
|
|
16
16
|
* fields). The file as it was at startup is kept as `<file>.bak`.
|
|
17
17
|
*/
|
|
18
18
|
export class Store {
|
|
@@ -38,7 +38,7 @@ export class Store {
|
|
|
38
38
|
}
|
|
39
39
|
const version = Number(parsed.version ?? 1);
|
|
40
40
|
if (version > STATE_VERSION) {
|
|
41
|
-
throw new Error(`${this.file} was written by a newer Agent Manager (state version ${version}; this one reads up to ${STATE_VERSION}). Upgrade Agent Manager, or restore a backup.`);
|
|
41
|
+
throw new Error(`${this.file} was written by a newer AI Agent Manager (state version ${version}; this one reads up to ${STATE_VERSION}). Upgrade AI Agent Manager, or restore a backup.`);
|
|
42
42
|
}
|
|
43
43
|
if (version < STATE_VERSION) {
|
|
44
44
|
fs.copyFileSync(this.file, `${this.file}.v${version}.bak`);
|
package/src/tracker.js
CHANGED
|
@@ -132,7 +132,7 @@ function textOf(content) {
|
|
|
132
132
|
* subsequent calls only parse newly appended lines.
|
|
133
133
|
*/
|
|
134
134
|
export function scanTranscript(file, cursor, bugPatterns = []) {
|
|
135
|
-
const result = { prs: [], bugs: [], title: null, lastPrompt: null, cost: null, lastActivity: null };
|
|
135
|
+
const result = { prs: [], bugs: [], title: null, lastPrompt: null, cost: null, lastActivity: null, lastMessageAt: null };
|
|
136
136
|
// Token usage per API response, kept on the cursor so incremental scans accumulate.
|
|
137
137
|
cursor.usage ??= new Map();
|
|
138
138
|
let fd;
|
|
@@ -160,6 +160,10 @@ export function scanTranscript(file, cursor, bugPatterns = []) {
|
|
|
160
160
|
} catch {
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
|
+
// The last real message: resuming a session appends bookkeeping lines, which have no timestamp.
|
|
164
|
+
if ((rec.type === 'user' || rec.type === 'assistant') && !rec.isMeta && rec.timestamp > (result.lastMessageAt ?? '')) {
|
|
165
|
+
result.lastMessageAt = rec.timestamp;
|
|
166
|
+
}
|
|
163
167
|
switch (rec.type) {
|
|
164
168
|
case 'pr-link':
|
|
165
169
|
if (rec.prUrl) result.prs.push({ ...prFromUrl(rec.prUrl, 'session'), number: rec.prNumber ?? null });
|