local-commander-agent 0.3.52 → 0.3.53

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.
@@ -1,162 +1,162 @@
1
- import { execFile } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { lstat, open } from "node:fs/promises";
4
- import { lstatSync, readFileSync } from "node:fs";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { promisify } from "node:util";
8
- import { agentName, contextSummary } from "./monitor-context.mjs";
9
-
10
- const execute = promisify(execFile);
11
- const uuid = value => typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
12
- const text = (value, max = 160) => typeof value === "string" && value.length <= max && !/[\u0000-\u001f\u007f-\u009f]/u.test(value.replace(/\n/g, " "));
13
- const model = value => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/.test(value);
14
- const time = value => typeof value === "string" && /^\d{4}-\d\d-\d\dT[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
15
- const pid = value => Number.isInteger(value) && value > 0 && value <= 0xffffffff;
16
- const invalid = () => { throw Error("AGENT_OBSERVATION_INVALID"); };
17
- const shortId = id => /^rt-/.test(id) && uuid(id.slice(3)) ? id.slice(3, 11) : uuid(id) ? id.slice(0, 8) :
18
- createHash("sha256").update(id).digest("hex").slice(0, 8);
19
-
20
- // Read coordination facts only; never open identity/capability/enrollment files.
21
- export async function readState(file) {
22
- for (const directory of [path.dirname(path.dirname(file)), path.dirname(file)]) {
23
- const stat = await lstat(directory);
24
- if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
25
- }
26
- const stat = await lstat(file);
27
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) invalid();
28
- const handle = await open(file, "r");
29
- try {
30
- const buffer = Buffer.alloc(1024 * 1024 + 1);
31
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
32
- if (bytesRead > 1024 * 1024) invalid();
33
- return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
34
- } finally { await handle.close(); }
35
- }
36
-
37
- export function presenceRows(raw, computer, now = Date.now()) {
38
- if (raw?.version !== 1 || !Array.isArray(raw.runtimes) || raw.runtimes.length > 64 || (raw.messages !== undefined && (!Array.isArray(raw.messages) || raw.messages.length > 512))) invalid();
39
- return raw.runtimes.flatMap(runtime => {
40
- if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(runtime?.id ?? "") || !text(runtime.label, 80) || !text(runtime.node, 80) ||
41
- !Number.isSafeInteger(runtime.lastSeenAt) || !Number.isInteger(runtime.leaseMs) || runtime.leaseMs < 1000 ||
42
- runtime.leaseMs > 30 * 60_000 || typeof runtime.stopped !== "boolean" ||
43
- !(runtime.activity === null || text(runtime.activity)) || runtime.lastSeenAt > now + 5000) invalid();
44
- const age = now - runtime.lastSeenAt;
45
- if (runtime.stopped || age > runtime.leaseMs * 3) return [];
46
- return [{ id: runtime.id, displayId: shortId(runtime.id), kind: "agent", computer: runtime.node === "this machine" ? computer : runtime.node.replace(/\n/g, " "),
47
- pid: null, name: /^[A-Za-z][A-Za-z0-9]{0,7}$/.test(runtime.name ?? "") ? runtime.name : agentName(runtime.id),
48
- label: runtime.label.replace(/\n/g, " "), activity: (runtime.activity ?? "presence reported; no activity supplied").replace(/\n/g, " "),
49
- context: contextSummary({ project: runtime.project, taskId: runtime.taskId }), phase: ["working", "analysis", "message", "idle"].includes(runtime.phase) ? runtime.phase : "working",
50
- unread: Array.isArray(raw.messages) ? raw.messages.filter(message => message.to === runtime.id && message.expiresAt > now && Array.isArray(message.ackedBy) && !message.ackedBy.includes(runtime.id)).length : 0,
51
- state: age <= runtime.leaseMs ? "REPORTING" : "STALE", model: model(runtime.model) ? runtime.model : null,
52
- modelSource: model(runtime.model) ? "reported" : null, executionIds: [], startedAt: null }];
53
- });
54
- }
55
-
56
- export function mailboxRows(raw, computer) {
57
- if (raw?.version !== 2 || !Array.isArray(raw.workers) || raw.workers.length > 64 ||
58
- !Array.isArray(raw.assignments) || raw.assignments.length > 256) invalid();
59
- const facts = attribution => {
60
- const fact = attribution?.model;
61
- if (fact === null || fact === undefined) return null;
62
- if (!model(fact.value) || !time(fact.confirmedAt) || !text(fact.evidenceRef, 96) ||
63
- !["registered_integration", "adapter_receipt"].includes(fact.source)) invalid();
64
- return fact.value;
65
- };
66
- const assignments = new Map();
67
- const workerIds = new Set(raw.workers.map(worker => worker?.workerId));
68
- const executionIds = raw.assignments.flatMap(assignment => assignment?.execution?.executionId ? [assignment.execution.executionId] : []);
69
- if (workerIds.size !== raw.workers.length || new Set(executionIds).size !== executionIds.length) invalid();
70
- for (const assignment of raw.assignments) {
71
- if (!uuid(assignment?.workerId) || !workerIds.has(assignment.workerId) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(assignment.taskId ?? "") || !time(assignment.updatedAt) ||
72
- !["assigned", "launching", "running", "unknown", "exited", "not_started"].includes(assignment.phase) ||
73
- !["chatgpt", "claude", "cursor"].includes(assignment.selectedRuntime)) invalid();
74
- if (assignment.execution && (!(assignment.execution.pid === null || pid(assignment.execution.pid)) ||
75
- !(assignment.execution.executionId === null || uuid(assignment.execution.executionId)) || !time(assignment.execution.confirmedAt))) invalid();
76
- facts(assignment.attribution);
77
- const previous = assignments.get(assignment.workerId);
78
- if (!previous || Date.parse(previous.updatedAt) <= Date.parse(assignment.updatedAt)) assignments.set(assignment.workerId, assignment);
79
- }
80
- return raw.workers.map(worker => {
81
- if (!uuid(worker?.workerId) || !(worker.runtimeId === null || /^rt-[a-f0-9-]{36}$/i.test(worker.runtimeId))) invalid();
82
- const assignment = assignments.get(worker.workerId);
83
- const active = assignment && !["exited", "not_started"].includes(assignment.phase);
84
- const attributedModel = facts(assignment?.attribution) ?? facts(worker.attribution);
85
- return { id: worker.runtimeId ?? worker.workerId, displayId: shortId(worker.runtimeId ?? worker.workerId), kind: "agent", computer,
86
- pid: active ? assignment.execution?.pid ?? null : null, name: agentName(worker.runtimeId ?? worker.workerId),
87
- context: contextSummary({ taskId: assignment?.taskId }), phase: "working",
88
- activity: active ? "task " + assignment.taskId : "registered; no active assignment",
89
- state: !active ? "REGISTERED" : assignment.phase === "unknown" ? "UNKNOWN" : assignment.phase.toUpperCase(),
90
- model: attributedModel, modelSource: attributedModel ? "confirmed" : null,
91
- executionIds: raw.assignments.filter(item => item.workerId === worker.workerId && item.execution?.executionId).map(item => item.execution.executionId),
92
- confirmedAt: active ? assignment.execution?.confirmedAt ?? null : null, startedAt: null };
93
- });
94
- }
95
-
96
- export function hostRows(raw, computer, previous = new Map()) {
97
- if (!Array.isArray(raw) || raw.length > 64) invalid();
98
- const seen = new Set();
99
- const rows = raw.map(host => {
100
- if (!host || !["codex", "claude", "cursor-agent"].includes(host.name) || !pid(host.pid) ||
101
- !time(host.startedAt) || !Number.isFinite(host.cpuMs) || host.cpuMs < 0) invalid();
102
- const id = createHash("sha256").update(JSON.stringify([computer, host.name, host.pid, host.startedAt])).digest("hex");
103
- if (seen.has(id)) invalid();
104
- seen.add(id);
105
- const before = previous.get(id);
106
- previous.set(id, host.cpuMs);
107
- const cpu = before !== undefined && host.cpuMs > before ? Math.round(host.cpuMs - before) : 0;
108
- return { id, displayId: id.slice(0, 8), kind: "host", computer, pid: host.pid, name: agentName(id),
109
- activity: host.name + " host" + (cpu ? " | CPU +" + cpu + "ms" : " | waiting or external work"),
110
- state: "OBSERVED", model: null, modelSource: null, executionIds: [], startedAt: host.startedAt };
111
- });
112
- for (const id of previous.keys()) if (!seen.has(id)) previous.delete(id);
113
- return rows;
114
- }
115
-
116
- // ponytail: observe at most 64 named hosts; add host paging if the local fleet exceeds this ceiling.
117
- const census = `$ErrorActionPreference = 'Stop'
118
- $rows = @(Get-Process -Name codex,claude,cursor-agent -ErrorAction SilentlyContinue | Sort-Object Id | Select-Object -First 64 | ForEach-Object {
119
- try { @{ name = $_.ProcessName.ToLowerInvariant(); pid = $_.Id; startedAt = $_.StartTime.ToUniversalTime().ToString('o'); cpuMs = $_.TotalProcessorTime.TotalMilliseconds } } catch {}
120
- })
121
- ConvertTo-Json -InputObject $rows -Compress`;
122
-
123
- export function presenceDirectory(projectRoot = process.cwd()) {
124
- let state = process.env.LCMCP_OAUTH_STATE_PATH;
125
- if (!state) try {
126
- const file = path.resolve(projectRoot, process.env.LCMCP_CONFIG?.trim() || "local-commander.config.json");
127
- const stat = lstatSync(file);
128
- if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 65536)
129
- state = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, "")).auth?.oauthStatePath;
130
- } catch { /* Missing/unreadable local configuration leaves observations unavailable. */ }
131
- return typeof state === "string" && state.trim() ? path.join(path.dirname(path.resolve(projectRoot, state)), ".local-commander") : path.join(projectRoot, ".local-commander");
132
- }
133
-
134
- export async function readAgentActivity(directory, { previousHosts = new Map(), signal, projectRoot = process.cwd() } = {}) {
135
- const computer = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "PC";
136
- const rows = [], unavailable = [];
137
- const stateDirectory = process.env.LC_AGENT_STATE_DIR ?? path.join(path.dirname(directory), "Agent");
138
- const inputs = [
139
- ["presence", path.join(presenceDirectory(projectRoot), "agents.json"), presenceRows],
140
- ["workers", path.join(stateDirectory, ".local-commander", "fabric", "mailbox.json"), mailboxRows]
141
- ];
142
- if (!path.isAbsolute(stateDirectory) || /[\u0000-\u001f]/u.test(stateDirectory)) return { rows, unavailable: ["state_directory"] };
143
- await Promise.all(inputs.map(async ([source, file, project]) => {
144
- try { rows.push(...project(await readState(file), computer)); }
145
- catch (error) { if (error.code !== "ENOENT") unavailable.push(source); }
146
- }));
147
- if (process.platform === "win32") {
148
- try {
149
- const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
150
- const { stdout } = await execute(path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
151
- ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(census, "utf16le").toString("base64")],
152
- { windowsHide: true, shell: false, timeout: 4000, maxBuffer: 128 * 1024, signal, env: { SystemRoot: systemRoot, WINDIR: systemRoot } });
153
- rows.push(...hostRows(JSON.parse(stdout), computer, previousHosts));
154
- } catch { unavailable.push("hosts"); }
155
- } else unavailable.push("hosts_unsupported");
156
- // Coalesce only a confirmed assignment/PID that predates the current host instance.
157
- const agents = rows.filter(row => row.kind === "agent");
158
- const merged = rows.filter(host => host.kind !== "host" || !agents.some(agent => agent.pid === host.pid && agent.confirmedAt &&
159
- Date.parse(host.startedAt) <= Date.parse(agent.confirmedAt)));
160
- return { rows: merged.sort((a, b) => a.computer.localeCompare(b.computer) || a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)),
161
- unavailable: unavailable.sort() };
162
- }
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, open } from "node:fs/promises";
4
+ import { lstatSync, readFileSync } from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { agentName, contextSummary } from "./monitor-context.mjs";
9
+
10
+ const execute = promisify(execFile);
11
+ const uuid = value => typeof value === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
12
+ const text = (value, max = 160) => typeof value === "string" && value.length <= max && !/[\u0000-\u001f\u007f-\u009f]/u.test(value.replace(/\n/g, " "));
13
+ const model = value => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/.test(value);
14
+ const time = value => typeof value === "string" && /^\d{4}-\d\d-\d\dT[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
15
+ const pid = value => Number.isInteger(value) && value > 0 && value <= 0xffffffff;
16
+ const invalid = () => { throw Error("AGENT_OBSERVATION_INVALID"); };
17
+ const shortId = id => /^rt-/.test(id) && uuid(id.slice(3)) ? id.slice(3, 11) : uuid(id) ? id.slice(0, 8) :
18
+ createHash("sha256").update(id).digest("hex").slice(0, 8);
19
+
20
+ // Read coordination facts only; never open identity/capability/enrollment files.
21
+ export async function readState(file) {
22
+ for (const directory of [path.dirname(path.dirname(file)), path.dirname(file)]) {
23
+ const stat = await lstat(directory);
24
+ if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
25
+ }
26
+ const stat = await lstat(file);
27
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) invalid();
28
+ const handle = await open(file, "r");
29
+ try {
30
+ const buffer = Buffer.alloc(1024 * 1024 + 1);
31
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
32
+ if (bytesRead > 1024 * 1024) invalid();
33
+ return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
34
+ } finally { await handle.close(); }
35
+ }
36
+
37
+ export function presenceRows(raw, computer, now = Date.now()) {
38
+ if (raw?.version !== 1 || !Array.isArray(raw.runtimes) || raw.runtimes.length > 64 || (raw.messages !== undefined && (!Array.isArray(raw.messages) || raw.messages.length > 512))) invalid();
39
+ return raw.runtimes.flatMap(runtime => {
40
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(runtime?.id ?? "") || !text(runtime.label, 80) || !text(runtime.node, 80) ||
41
+ !Number.isSafeInteger(runtime.lastSeenAt) || !Number.isInteger(runtime.leaseMs) || runtime.leaseMs < 1000 ||
42
+ runtime.leaseMs > 30 * 60_000 || typeof runtime.stopped !== "boolean" ||
43
+ !(runtime.activity === null || text(runtime.activity)) || runtime.lastSeenAt > now + 5000) invalid();
44
+ const age = now - runtime.lastSeenAt;
45
+ if (runtime.stopped || age > runtime.leaseMs * 3) return [];
46
+ return [{ id: runtime.id, displayId: shortId(runtime.id), kind: "agent", computer: runtime.node === "this machine" ? computer : runtime.node.replace(/\n/g, " "),
47
+ pid: null, name: /^[A-Za-z][A-Za-z0-9]{0,7}$/.test(runtime.name ?? "") ? runtime.name : agentName(runtime.id),
48
+ label: runtime.label.replace(/\n/g, " "), activity: (runtime.activity ?? "presence reported; no activity supplied").replace(/\n/g, " "),
49
+ context: contextSummary({ project: runtime.project, taskId: runtime.taskId }), phase: ["working", "analysis", "message", "idle"].includes(runtime.phase) ? runtime.phase : "working",
50
+ unread: Array.isArray(raw.messages) ? raw.messages.filter(message => message.to === runtime.id && message.expiresAt > now && Array.isArray(message.ackedBy) && !message.ackedBy.includes(runtime.id)).length : 0,
51
+ state: age <= runtime.leaseMs ? "REPORTING" : "STALE", model: model(runtime.model) ? runtime.model : null,
52
+ modelSource: model(runtime.model) ? "reported" : null, executionIds: [], startedAt: null }];
53
+ });
54
+ }
55
+
56
+ export function mailboxRows(raw, computer) {
57
+ if (raw?.version !== 2 || !Array.isArray(raw.workers) || raw.workers.length > 64 ||
58
+ !Array.isArray(raw.assignments) || raw.assignments.length > 256) invalid();
59
+ const facts = attribution => {
60
+ const fact = attribution?.model;
61
+ if (fact === null || fact === undefined) return null;
62
+ if (!model(fact.value) || !time(fact.confirmedAt) || !text(fact.evidenceRef, 96) ||
63
+ !["registered_integration", "adapter_receipt"].includes(fact.source)) invalid();
64
+ return fact.value;
65
+ };
66
+ const assignments = new Map();
67
+ const workerIds = new Set(raw.workers.map(worker => worker?.workerId));
68
+ const executionIds = raw.assignments.flatMap(assignment => assignment?.execution?.executionId ? [assignment.execution.executionId] : []);
69
+ if (workerIds.size !== raw.workers.length || new Set(executionIds).size !== executionIds.length) invalid();
70
+ for (const assignment of raw.assignments) {
71
+ if (!uuid(assignment?.workerId) || !workerIds.has(assignment.workerId) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(assignment.taskId ?? "") || !time(assignment.updatedAt) ||
72
+ !["assigned", "launching", "running", "unknown", "exited", "not_started"].includes(assignment.phase) ||
73
+ !["chatgpt", "claude", "cursor"].includes(assignment.selectedRuntime)) invalid();
74
+ if (assignment.execution && (!(assignment.execution.pid === null || pid(assignment.execution.pid)) ||
75
+ !(assignment.execution.executionId === null || uuid(assignment.execution.executionId)) || !time(assignment.execution.confirmedAt))) invalid();
76
+ facts(assignment.attribution);
77
+ const previous = assignments.get(assignment.workerId);
78
+ if (!previous || Date.parse(previous.updatedAt) <= Date.parse(assignment.updatedAt)) assignments.set(assignment.workerId, assignment);
79
+ }
80
+ return raw.workers.map(worker => {
81
+ if (!uuid(worker?.workerId) || !(worker.runtimeId === null || /^rt-[a-f0-9-]{36}$/i.test(worker.runtimeId))) invalid();
82
+ const assignment = assignments.get(worker.workerId);
83
+ const active = assignment && !["exited", "not_started"].includes(assignment.phase);
84
+ const attributedModel = facts(assignment?.attribution) ?? facts(worker.attribution);
85
+ return { id: worker.runtimeId ?? worker.workerId, displayId: shortId(worker.runtimeId ?? worker.workerId), kind: "agent", computer,
86
+ pid: active ? assignment.execution?.pid ?? null : null, name: agentName(worker.runtimeId ?? worker.workerId),
87
+ context: contextSummary({ taskId: assignment?.taskId }), phase: "working",
88
+ activity: active ? "task " + assignment.taskId : "registered; no active assignment",
89
+ state: !active ? "REGISTERED" : assignment.phase === "unknown" ? "UNKNOWN" : assignment.phase.toUpperCase(),
90
+ model: attributedModel, modelSource: attributedModel ? "confirmed" : null,
91
+ executionIds: raw.assignments.filter(item => item.workerId === worker.workerId && item.execution?.executionId).map(item => item.execution.executionId),
92
+ confirmedAt: active ? assignment.execution?.confirmedAt ?? null : null, startedAt: null };
93
+ });
94
+ }
95
+
96
+ export function hostRows(raw, computer, previous = new Map()) {
97
+ if (!Array.isArray(raw) || raw.length > 64) invalid();
98
+ const seen = new Set();
99
+ const rows = raw.map(host => {
100
+ if (!host || !["codex", "claude", "cursor-agent"].includes(host.name) || !pid(host.pid) ||
101
+ !time(host.startedAt) || !Number.isFinite(host.cpuMs) || host.cpuMs < 0) invalid();
102
+ const id = createHash("sha256").update(JSON.stringify([computer, host.name, host.pid, host.startedAt])).digest("hex");
103
+ if (seen.has(id)) invalid();
104
+ seen.add(id);
105
+ const before = previous.get(id);
106
+ previous.set(id, host.cpuMs);
107
+ const cpu = before !== undefined && host.cpuMs > before ? Math.round(host.cpuMs - before) : 0;
108
+ return { id, displayId: id.slice(0, 8), kind: "host", computer, pid: host.pid, name: agentName(id),
109
+ activity: host.name + " host" + (cpu ? " | CPU +" + cpu + "ms" : " | waiting or external work"),
110
+ state: "OBSERVED", model: null, modelSource: null, executionIds: [], startedAt: host.startedAt };
111
+ });
112
+ for (const id of previous.keys()) if (!seen.has(id)) previous.delete(id);
113
+ return rows;
114
+ }
115
+
116
+ // ponytail: observe at most 64 named hosts; add host paging if the local fleet exceeds this ceiling.
117
+ const census = `$ErrorActionPreference = 'Stop'
118
+ $rows = @(Get-Process -Name codex,claude,cursor-agent -ErrorAction SilentlyContinue | Sort-Object Id | Select-Object -First 64 | ForEach-Object {
119
+ try { @{ name = $_.ProcessName.ToLowerInvariant(); pid = $_.Id; startedAt = $_.StartTime.ToUniversalTime().ToString('o'); cpuMs = $_.TotalProcessorTime.TotalMilliseconds } } catch {}
120
+ })
121
+ ConvertTo-Json -InputObject $rows -Compress`;
122
+
123
+ export function presenceDirectory(projectRoot = process.cwd()) {
124
+ let state = process.env.LCMCP_OAUTH_STATE_PATH;
125
+ if (!state) try {
126
+ const file = path.resolve(projectRoot, process.env.LCMCP_CONFIG?.trim() || "local-commander.config.json");
127
+ const stat = lstatSync(file);
128
+ if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 65536)
129
+ state = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, "")).auth?.oauthStatePath;
130
+ } catch { /* Missing/unreadable local configuration leaves observations unavailable. */ }
131
+ return typeof state === "string" && state.trim() ? path.join(path.dirname(path.resolve(projectRoot, state)), ".local-commander") : path.join(projectRoot, ".local-commander");
132
+ }
133
+
134
+ export async function readAgentActivity(directory, { previousHosts = new Map(), signal, projectRoot = process.cwd() } = {}) {
135
+ const computer = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64) || "PC";
136
+ const rows = [], unavailable = [];
137
+ const stateDirectory = process.env.LC_AGENT_STATE_DIR ?? path.join(path.dirname(directory), "Agent");
138
+ const inputs = [
139
+ ["presence", path.join(presenceDirectory(projectRoot), "agents.json"), presenceRows],
140
+ ["workers", path.join(stateDirectory, ".local-commander", "fabric", "mailbox.json"), mailboxRows]
141
+ ];
142
+ if (!path.isAbsolute(stateDirectory) || /[\u0000-\u001f]/u.test(stateDirectory)) return { rows, unavailable: ["state_directory"] };
143
+ await Promise.all(inputs.map(async ([source, file, project]) => {
144
+ try { rows.push(...project(await readState(file), computer)); }
145
+ catch (error) { if (error.code !== "ENOENT") unavailable.push(source); }
146
+ }));
147
+ if (process.platform === "win32") {
148
+ try {
149
+ const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
150
+ const { stdout } = await execute(path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
151
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(census, "utf16le").toString("base64")],
152
+ { windowsHide: true, shell: false, timeout: 4000, maxBuffer: 128 * 1024, signal, env: { SystemRoot: systemRoot, WINDIR: systemRoot } });
153
+ rows.push(...hostRows(JSON.parse(stdout), computer, previousHosts));
154
+ } catch { unavailable.push("hosts"); }
155
+ } else unavailable.push("hosts_unsupported");
156
+ // Coalesce only a confirmed assignment/PID that predates the current host instance.
157
+ const agents = rows.filter(row => row.kind === "agent");
158
+ const merged = rows.filter(host => host.kind !== "host" || !agents.some(agent => agent.pid === host.pid && agent.confirmedAt &&
159
+ Date.parse(host.startedAt) <= Date.parse(agent.confirmedAt)));
160
+ return { rows: merged.sort((a, b) => a.computer.localeCompare(b.computer) || a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)),
161
+ unavailable: unavailable.sort() };
162
+ }
@@ -1,7 +1,7 @@
1
- export const AGENT_NAMES: readonly string[];
2
- export function agentName(id: string): string;
3
- export interface MonitorContext { project: string | null; directory: string | null; taskId: string | null; pr: number | null; inferred: boolean }
4
- export function contextSummary(meta?: Record<string, unknown>): MonitorContext;
5
- export function commandSummary(tool: string, meta?: Record<string, unknown>): string;
6
-
7
- export function commandPreview(tool: string, meta?: Record<string, unknown>): string;
1
+ export const AGENT_NAMES: readonly string[];
2
+ export function agentName(id: string): string;
3
+ export interface MonitorContext { project: string | null; directory: string | null; taskId: string | null; pr: number | null; inferred: boolean }
4
+ export function contextSummary(meta?: Record<string, unknown>): MonitorContext;
5
+ export function commandSummary(tool: string, meta?: Record<string, unknown>): string;
6
+
7
+ export function commandPreview(tool: string, meta?: Record<string, unknown>): string;
@@ -1,65 +1,65 @@
1
- import { createHash } from "node:crypto";
2
-
3
- // Transliteration keeps terminal widths predictable. Random runtime IDs select the pool without a model or handshake.
4
- export const AGENT_NAMES = Object.freeze([
5
- "Avi", "Noam", "Yael", "Leah", "Ezra", "Eli", "Tamar", "Dalia", "Ruth", "Maya", "Oren", "Shira", "Eitan", "Lior", "Hila", "Amir",
6
- "Ivan", "Yuri", "Olga", "Anna", "Boris", "Nina", "Pavel", "Irina", "Vera", "Daria", "Misha", "Sasha", "Vadim", "Roman", "Anya", "Lev",
7
- "Omar", "Layla", "Ali", "Nadia", "Salim", "Hana", "Rami", "Zain", "Noor", "Amal", "Samir", "Farah", "Malik", "Yasmin", "Adil", "Hadi",
8
- "Arjun", "Priya", "Ravi", "Anika", "Asha", "Dev", "Kiran", "Meera", "Nisha", "Rohan", "Diya", "Neha", "Anil", "Sana", "Vijay", "Isha",
9
- "Wei", "Lin", "Mei", "Chen", "Li", "Jian", "Ming", "Hua", "Yan", "Tao", "Jun", "Lan", "Yue", "Lei", "Hong", "Qian",
10
- "Pickle", "Waffle", "Noodle", "Spud", "Mochi", "Taco", "Doodle", "Widget", "Boop", "Bytey", "Glitch", "Sprock", "Wombat", "Goose", "Ninja", "Otter", "Quokka", "Squid", "Sushi", "Pogo", "Zippy", "Nifty", "Fuzzy", "Gizmo", "Bloop", "Blip", "Niblet", "Pixel", "Bugsy", "Wobble", "Muppet", "Puddle", "Cookie", "Muffin", "Bagel", "Nacho", "Tater", "Churro", "Snappy", "Peppy", "Plonk", "Slinky", "Tofu", "Nugget", "Dumplo", "Popper", "Kebab", "Sizzle"
11
- ]);
12
- export function agentName(id) {
13
- const hash = createHash("sha256").update(id).digest();
14
- return AGENT_NAMES[hash.readUInt16BE(0) % AGENT_NAMES.length] + hash.subarray(2, 3).toString("hex");
15
- }
16
- const identifier = value => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(value);
17
- export function contextSummary(meta = {}) {
18
- const source = typeof (meta.cwd ?? meta.path ?? meta.project) === "string" ? (meta.cwd ?? meta.path ?? meta.project) : "";
19
- const normalized = source.replaceAll("\\", "/").replace(/\/+$/, "");
20
- const safe = normalized.length <= 1024 && /^[A-Za-z0-9 _./:@()+-]*$/.test(normalized) &&
21
- !/(?:^|\/)(?:\.env(?:\.[^/]*)?|credentials[^/]*|\.npmrc|agent-identities\.json)(?:\/|$)/i.test(normalized);
22
- const directory = safe && normalized ? normalized.slice(-96) : null;
23
- const project = directory ? directory.split("/").at(-1) : null;
24
- const taskMatch = safe && normalized.match(/\/planning\/tasks\/([A-Za-z0-9][A-Za-z0-9_.:-]{0,95})\.md$/);
25
- const taskId = identifier(meta.taskId) ? meta.taskId : taskMatch ? taskMatch[1] : null;
26
- const branchPr = safe && normalized.match(/(?:^|[/_-])pr[-_]?(\d{1,7})(?:[/_.-]|$)/i);
27
- const explicitPr = Number.isSafeInteger(meta.pr) && meta.pr > 0 && meta.pr <= 9_999_999 ? meta.pr : null;
28
- const args = Array.isArray(meta.args) ? meta.args : [];
29
- const ghPr = meta.command === "gh" && args[0] === "pr" && ["view", "checkout", "diff", "checks", "merge"].includes(args[1]) && /^\d{1,7}$/.test(args[2] ?? "") ? Number(args[2]) : null;
30
- return { project, directory, taskId, pr: explicitPr ?? ghPr ?? (branchPr ? Number(branchPr[1]) : null), inferred: !explicitPr && !identifier(meta.taskId) };
31
- }
32
- export function commandSummary(tool, meta = {}) {
33
- if (tool === "agent_message" && ["send", "inbox", "ack"].includes(meta.action)) {
34
- const kind = ["status", "question", "answer", "handoff", "result_summary", "blocker"].includes(meta.kind) ? " " + meta.kind : "";
35
- const target = identifier(meta.to) ? " -> " + meta.to.slice(0, 8) : "";
36
- return "agent_message " + meta.action + kind + target;
37
- }
38
- if (!["command_run", "process_start"].includes(tool)) return tool;
39
- const candidate = typeof meta.command === "string" ? meta.command.replaceAll("\\", "/").split("/").at(-1).replace(/\.(exe|cmd|bat|com)$/i, "").toLowerCase() : "";
40
- const name = ["node", "npm", "npx", "git", "rg", "powershell", "pwsh", "cmd", "tsc", "python", "dotnet", "gh"].includes(candidate) ? candidate : "command";
41
- const args = Array.isArray(meta.args) ? meta.args : [];
42
- if (name === "git" && ["status", "diff", "log", "show", "fetch", "pull", "push", "commit"].includes(args[0])) return name + " " + args[0];
43
- if (name === "npm" && ["test", "install", "ci", "pack", "publish"].includes(args[0])) return name + " " + args[0];
44
- if (name === "npm" && args[0] === "run" && ["test", "build", "typecheck", "lint", "dev", "start"].includes(args[1])) return "npm run " + args[1];
45
- return name + (args.length ? " [args]" : "");
46
- }
47
-
48
- /** Diagnostic preview only. Never used to construct or execute a command. */
49
- export function commandPreview(tool, meta = {}) {
50
- if (!["command_run", "process_start"].includes(tool)) return commandSummary(tool, meta);
51
- const name = typeof meta.command === "string" && /^[A-Za-z0-9_.-]{1,40}$/.test(meta.command) ? meta.command : "command";
52
- const args = Array.isArray(meta.args) ? meta.args.slice(0, 24) : [];
53
- let hideNext = false, inline = false;
54
- const safe = args.map(arg => {
55
- if (inline || hideNext) { hideNext = false; return "[redacted]"; }
56
- if (typeof arg !== "string") return "[value]";
57
- if (/^(?:-e|--eval|-p|--print|-c|-command|--command|-encodedcommand|\/c|\/k)$/i.test(arg) &&
58
- /^(?:node|python|bash|sh|powershell|pwsh|cmd)(?:\.[a-z]+)?$/i.test(name)) inline = true;
59
- if (/^(?:[-/]+(?:.*(?:token|password|passwd|secret|credential|authorization|api[-_]?key)|auth|key|header|env|config|data|data-raw|data-binary|message|prompt)|-[uHpmd])$/i.test(arg)) hideNext = true;
60
- if (/(?:token|password|passwd|secret|credential|authorization|api[-_]?key)\s*[=:]|(?:^|[/\\])(?:\.env(?:\.|$)|credentials|\.npmrc)|:\/\/|[\x00-\x1f\x7f]|[A-Za-z0-9+/_=-]{40,}/i.test(arg)) return "[redacted]";
61
- // ponytail: printable simple argv only; embedded scripts and complex values stay private.
62
- return arg.length <= 96 && /^[A-Za-z0-9 _./\\:@()+=-]+$/.test(arg) ? (arg.includes(" ") ? JSON.stringify(arg) : arg) : "[value]";
63
- });
64
- return [name, ...safe, ...(Array.isArray(meta.args) && meta.args.length > 24 ? ["…"] : [])].join(" ").slice(0, 240);
65
- }
1
+ import { createHash } from "node:crypto";
2
+
3
+ // Transliteration keeps terminal widths predictable. Random runtime IDs select the pool without a model or handshake.
4
+ export const AGENT_NAMES = Object.freeze([
5
+ "Avi", "Noam", "Yael", "Leah", "Ezra", "Eli", "Tamar", "Dalia", "Ruth", "Maya", "Oren", "Shira", "Eitan", "Lior", "Hila", "Amir",
6
+ "Ivan", "Yuri", "Olga", "Anna", "Boris", "Nina", "Pavel", "Irina", "Vera", "Daria", "Misha", "Sasha", "Vadim", "Roman", "Anya", "Lev",
7
+ "Omar", "Layla", "Ali", "Nadia", "Salim", "Hana", "Rami", "Zain", "Noor", "Amal", "Samir", "Farah", "Malik", "Yasmin", "Adil", "Hadi",
8
+ "Arjun", "Priya", "Ravi", "Anika", "Asha", "Dev", "Kiran", "Meera", "Nisha", "Rohan", "Diya", "Neha", "Anil", "Sana", "Vijay", "Isha",
9
+ "Wei", "Lin", "Mei", "Chen", "Li", "Jian", "Ming", "Hua", "Yan", "Tao", "Jun", "Lan", "Yue", "Lei", "Hong", "Qian",
10
+ "Pickle", "Waffle", "Noodle", "Spud", "Mochi", "Taco", "Doodle", "Widget", "Boop", "Bytey", "Glitch", "Sprock", "Wombat", "Goose", "Ninja", "Otter", "Quokka", "Squid", "Sushi", "Pogo", "Zippy", "Nifty", "Fuzzy", "Gizmo", "Bloop", "Blip", "Niblet", "Pixel", "Bugsy", "Wobble", "Muppet", "Puddle", "Cookie", "Muffin", "Bagel", "Nacho", "Tater", "Churro", "Snappy", "Peppy", "Plonk", "Slinky", "Tofu", "Nugget", "Dumplo", "Popper", "Kebab", "Sizzle"
11
+ ]);
12
+ export function agentName(id) {
13
+ const hash = createHash("sha256").update(id).digest();
14
+ return AGENT_NAMES[hash.readUInt16BE(0) % AGENT_NAMES.length] + hash.subarray(2, 3).toString("hex");
15
+ }
16
+ const identifier = value => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/.test(value);
17
+ export function contextSummary(meta = {}) {
18
+ const source = typeof (meta.cwd ?? meta.path ?? meta.project) === "string" ? (meta.cwd ?? meta.path ?? meta.project) : "";
19
+ const normalized = source.replaceAll("\\", "/").replace(/\/+$/, "");
20
+ const safe = normalized.length <= 1024 && /^[A-Za-z0-9 _./:@()+-]*$/.test(normalized) &&
21
+ !/(?:^|\/)(?:\.env(?:\.[^/]*)?|credentials[^/]*|\.npmrc|agent-identities\.json)(?:\/|$)/i.test(normalized);
22
+ const directory = safe && normalized ? normalized.slice(-96) : null;
23
+ const project = directory ? directory.split("/").at(-1) : null;
24
+ const taskMatch = safe && normalized.match(/\/planning\/tasks\/([A-Za-z0-9][A-Za-z0-9_.:-]{0,95})\.md$/);
25
+ const taskId = identifier(meta.taskId) ? meta.taskId : taskMatch ? taskMatch[1] : null;
26
+ const branchPr = safe && normalized.match(/(?:^|[/_-])pr[-_]?(\d{1,7})(?:[/_.-]|$)/i);
27
+ const explicitPr = Number.isSafeInteger(meta.pr) && meta.pr > 0 && meta.pr <= 9_999_999 ? meta.pr : null;
28
+ const args = Array.isArray(meta.args) ? meta.args : [];
29
+ const ghPr = meta.command === "gh" && args[0] === "pr" && ["view", "checkout", "diff", "checks", "merge"].includes(args[1]) && /^\d{1,7}$/.test(args[2] ?? "") ? Number(args[2]) : null;
30
+ return { project, directory, taskId, pr: explicitPr ?? ghPr ?? (branchPr ? Number(branchPr[1]) : null), inferred: !explicitPr && !identifier(meta.taskId) };
31
+ }
32
+ export function commandSummary(tool, meta = {}) {
33
+ if (tool === "agent_message" && ["send", "inbox", "ack"].includes(meta.action)) {
34
+ const kind = ["status", "question", "answer", "handoff", "result_summary", "blocker"].includes(meta.kind) ? " " + meta.kind : "";
35
+ const target = identifier(meta.to) ? " -> " + meta.to.slice(0, 8) : "";
36
+ return "agent_message " + meta.action + kind + target;
37
+ }
38
+ if (!["command_run", "process_start"].includes(tool)) return tool;
39
+ const candidate = typeof meta.command === "string" ? meta.command.replaceAll("\\", "/").split("/").at(-1).replace(/\.(exe|cmd|bat|com)$/i, "").toLowerCase() : "";
40
+ const name = ["node", "npm", "npx", "git", "rg", "powershell", "pwsh", "cmd", "tsc", "python", "dotnet", "gh"].includes(candidate) ? candidate : "command";
41
+ const args = Array.isArray(meta.args) ? meta.args : [];
42
+ if (name === "git" && ["status", "diff", "log", "show", "fetch", "pull", "push", "commit"].includes(args[0])) return name + " " + args[0];
43
+ if (name === "npm" && ["test", "install", "ci", "pack", "publish"].includes(args[0])) return name + " " + args[0];
44
+ if (name === "npm" && args[0] === "run" && ["test", "build", "typecheck", "lint", "dev", "start"].includes(args[1])) return "npm run " + args[1];
45
+ return name + (args.length ? " [args]" : "");
46
+ }
47
+
48
+ /** Diagnostic preview only. Never used to construct or execute a command. */
49
+ export function commandPreview(tool, meta = {}) {
50
+ if (!["command_run", "process_start"].includes(tool)) return commandSummary(tool, meta);
51
+ const name = typeof meta.command === "string" && /^[A-Za-z0-9_.-]{1,40}$/.test(meta.command) ? meta.command : "command";
52
+ const args = Array.isArray(meta.args) ? meta.args.slice(0, 24) : [];
53
+ let hideNext = false, inline = false;
54
+ const safe = args.map(arg => {
55
+ if (inline || hideNext) { hideNext = false; return "[redacted]"; }
56
+ if (typeof arg !== "string") return "[value]";
57
+ if (/^(?:-e|--eval|-p|--print|-c|-command|--command|-encodedcommand|\/c|\/k)$/i.test(arg) &&
58
+ /^(?:node|python|bash|sh|powershell|pwsh|cmd)(?:\.[a-z]+)?$/i.test(name)) inline = true;
59
+ if (/^(?:[-/]+(?:.*(?:token|password|passwd|secret|credential|authorization|api[-_]?key)|auth|key|header|env|config|data|data-raw|data-binary|message|prompt)|-[uHpmd])$/i.test(arg)) hideNext = true;
60
+ if (/(?:token|password|passwd|secret|credential|authorization|api[-_]?key)\s*[=:]|(?:^|[/\\])(?:\.env(?:\.|$)|credentials|\.npmrc)|:\/\/|[\x00-\x1f\x7f]|[A-Za-z0-9+/_=-]{40,}/i.test(arg)) return "[redacted]";
61
+ // ponytail: printable simple argv only; embedded scripts and complex values stay private.
62
+ return arg.length <= 96 && /^[A-Za-z0-9 _./\\:@()+=-]+$/.test(arg) ? (arg.includes(" ") ? JSON.stringify(arg) : arg) : "[value]";
63
+ });
64
+ return [name, ...safe, ...(Array.isArray(meta.args) && meta.args.length > 24 ? ["…"] : [])].join(" ").slice(0, 240);
65
+ }