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/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');
@@ -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. */
@@ -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);
@@ -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 });