@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/lock.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents.lock` — a deterministic SHA-256 manifest of the resolved resource set
|
|
3
|
+
* at a project root, plus the `--frozen` verification that fails closed on any
|
|
4
|
+
* drift. This is the reproducible-CI slice of the governance work (#337); org
|
|
5
|
+
* version pins and bundle signing are deferred follow-ups.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Content hashing REUSES `fingerprintDir` / `fingerprintFile`
|
|
9
|
+
* (src/lib/staleness/fingerprint.ts) — the same two-tier fingerprinter every
|
|
10
|
+
* staleness checker uses. No new recursive file walker is introduced here.
|
|
11
|
+
* - The resource SET is the layered, resolved source union (project > user >
|
|
12
|
+
* system > extras), the same precedence `resolveResource` / `syncResourcesToVersion`
|
|
13
|
+
* read from. It is enumerated agent-independently on purpose: the lock must be
|
|
14
|
+
* reproducible on a fresh CI checkout that has no installed agent VERSIONS, so
|
|
15
|
+
* it snapshots the SOURCES sync copies FROM, not any per-version home.
|
|
16
|
+
*
|
|
17
|
+
* Lock shape (JSON): `{ "version": 1, "resources": { "<relpath>": "<sha256>" } }`,
|
|
18
|
+
* keys sorted so the file is byte-stable across machines and runs.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { fingerprintDir, fingerprintFile } from './staleness/fingerprint.js';
|
|
23
|
+
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
|
|
24
|
+
export const LOCK_FILENAME = 'agents.lock';
|
|
25
|
+
export const LOCK_VERSION = 1;
|
|
26
|
+
/**
|
|
27
|
+
* Resource kinds captured by the lock — the file/dir-backed layered resources
|
|
28
|
+
* shared across every agent. Presence-only kinds handled specially by the sync
|
|
29
|
+
* differ (plugins/promptcuts) are intentionally out of this first slice.
|
|
30
|
+
*/
|
|
31
|
+
export const LOCK_KINDS = [
|
|
32
|
+
'commands',
|
|
33
|
+
'skills',
|
|
34
|
+
'hooks',
|
|
35
|
+
'rules',
|
|
36
|
+
'mcp',
|
|
37
|
+
'permissions',
|
|
38
|
+
'subagents',
|
|
39
|
+
];
|
|
40
|
+
/** Normalise a filesystem-relative path to POSIX separators for stable keys. */
|
|
41
|
+
function toPosix(rel) {
|
|
42
|
+
return rel.split(path.sep).join('/');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hash every resolved source into a flat `relpath -> sha256` map. Directories are
|
|
46
|
+
* expanded with `fingerprintDir` (every contained file, sorted); single files use
|
|
47
|
+
* `fingerprintFile`. Unreadable / missing sources are skipped — enumeration only
|
|
48
|
+
* ever hands us paths that existed at scan time.
|
|
49
|
+
*/
|
|
50
|
+
export function hashLockSources(sources) {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const src of sources) {
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.statSync(src.path);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (stat.isDirectory()) {
|
|
61
|
+
for (const fp of fingerprintDir(src.path)) {
|
|
62
|
+
const rel = toPosix(path.relative(src.path, fp.path));
|
|
63
|
+
out[`${src.key}/${rel}`] = fp.sha256;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (stat.isFile()) {
|
|
67
|
+
const fp = fingerprintFile(src.path);
|
|
68
|
+
if (fp)
|
|
69
|
+
out[src.key] = fp.sha256;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** Build a fully-sorted lock object from a resolved source set. */
|
|
75
|
+
export function buildLock(sources) {
|
|
76
|
+
return { version: LOCK_VERSION, resources: sortRecord(hashLockSources(sources)) };
|
|
77
|
+
}
|
|
78
|
+
/** Return a new record with keys inserted in sorted order (deterministic JSON). */
|
|
79
|
+
function sortRecord(record) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const key of Object.keys(record).sort())
|
|
82
|
+
out[key] = record[key];
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Serialise a lock to the canonical, sorted, newline-terminated JSON form. */
|
|
86
|
+
export function serializeLock(lock) {
|
|
87
|
+
return JSON.stringify({ version: lock.version, resources: sortRecord(lock.resources) }, null, 2) + '\n';
|
|
88
|
+
}
|
|
89
|
+
/** Absolute path to the lockfile for a project root. */
|
|
90
|
+
export function lockPath(projectRoot) {
|
|
91
|
+
return path.join(projectRoot, LOCK_FILENAME);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read + validate an existing lock. Returns null ONLY when the file is absent —
|
|
95
|
+
* a present-but-malformed lock throws, so `--frozen` fails closed instead of
|
|
96
|
+
* silently treating corruption as "no lock".
|
|
97
|
+
*/
|
|
98
|
+
export function readLock(projectRoot) {
|
|
99
|
+
const p = lockPath(projectRoot);
|
|
100
|
+
if (!fs.existsSync(p))
|
|
101
|
+
return null;
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
throw new Error(`${LOCK_FILENAME} is not valid JSON: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
110
|
+
throw new Error(`${LOCK_FILENAME} is malformed (expected an object).`);
|
|
111
|
+
}
|
|
112
|
+
const obj = parsed;
|
|
113
|
+
if (obj.version !== LOCK_VERSION) {
|
|
114
|
+
throw new Error(`${LOCK_FILENAME} version ${String(obj.version)} is unsupported (expected ${LOCK_VERSION}).`);
|
|
115
|
+
}
|
|
116
|
+
if (!obj.resources || typeof obj.resources !== 'object') {
|
|
117
|
+
throw new Error(`${LOCK_FILENAME} is missing a resources map.`);
|
|
118
|
+
}
|
|
119
|
+
return { version: LOCK_VERSION, resources: obj.resources };
|
|
120
|
+
}
|
|
121
|
+
/** Write the lock to `<projectRoot>/agents.lock`, returning the path written. */
|
|
122
|
+
export function writeLock(projectRoot, lock) {
|
|
123
|
+
const p = lockPath(projectRoot);
|
|
124
|
+
fs.writeFileSync(p, serializeLock(lock));
|
|
125
|
+
return p;
|
|
126
|
+
}
|
|
127
|
+
/** Diff a recorded lock against a freshly-computed one. */
|
|
128
|
+
export function diffLock(expected, actual) {
|
|
129
|
+
const e = expected.resources;
|
|
130
|
+
const a = actual.resources;
|
|
131
|
+
const added = [];
|
|
132
|
+
const removed = [];
|
|
133
|
+
const changed = [];
|
|
134
|
+
for (const k of Object.keys(a))
|
|
135
|
+
if (!(k in e))
|
|
136
|
+
added.push(k);
|
|
137
|
+
for (const k of Object.keys(e)) {
|
|
138
|
+
if (!(k in a))
|
|
139
|
+
removed.push(k);
|
|
140
|
+
else if (e[k] !== a[k])
|
|
141
|
+
changed.push(k);
|
|
142
|
+
}
|
|
143
|
+
added.sort();
|
|
144
|
+
removed.sort();
|
|
145
|
+
changed.sort();
|
|
146
|
+
return { added, removed, changed };
|
|
147
|
+
}
|
|
148
|
+
/** True when a diff has no added/removed/changed entries. */
|
|
149
|
+
export function lockDiffIsClean(diff) {
|
|
150
|
+
return diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0;
|
|
151
|
+
}
|
|
152
|
+
/** Verify a recorded lock against the live resolved sources. */
|
|
153
|
+
export function verifyLock(expected, sources) {
|
|
154
|
+
return diffLock(expected, buildLock(sources));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The project root the lockfile lives at: the parent of the discovered project
|
|
158
|
+
* `.agents/` dir, or the cwd itself when none is found.
|
|
159
|
+
*/
|
|
160
|
+
export function resolveProjectRoot(cwd = process.cwd()) {
|
|
161
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
162
|
+
return projectDir ? path.dirname(projectDir) : path.resolve(cwd);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Enumerate the resolved resource sources for a cwd: for each lockable kind, the
|
|
166
|
+
* winning entry per name across layers (project > user > system > extras), keyed
|
|
167
|
+
* `<kind>/<entryName>`. This is a single-level `readdir` per kind dir — the
|
|
168
|
+
* recursive fingerprinting is delegated to `fingerprintDir`.
|
|
169
|
+
*
|
|
170
|
+
* Entry names keep their extension in the key, so a hook script `foo.sh` and its
|
|
171
|
+
* sidecar `foo.yaml` stay distinct (a name-stripped enumerator would collide
|
|
172
|
+
* them). Dotfiles at the kind-dir top level are skipped as noise.
|
|
173
|
+
*/
|
|
174
|
+
export function enumerateLockSources(cwd = process.cwd()) {
|
|
175
|
+
const roots = [];
|
|
176
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
177
|
+
if (projectDir)
|
|
178
|
+
roots.push(projectDir);
|
|
179
|
+
roots.push(getUserAgentsDir());
|
|
180
|
+
roots.push(getSystemAgentsDir());
|
|
181
|
+
for (const extra of getEnabledExtraRepos())
|
|
182
|
+
roots.push(extra.dir);
|
|
183
|
+
const sources = [];
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
for (const kind of LOCK_KINDS) {
|
|
186
|
+
for (const root of roots) {
|
|
187
|
+
const kindDir = path.join(root, kind);
|
|
188
|
+
let entries;
|
|
189
|
+
try {
|
|
190
|
+
entries = fs.readdirSync(kindDir, { withFileTypes: true });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (entry.name.startsWith('.'))
|
|
197
|
+
continue;
|
|
198
|
+
const key = `${kind}/${entry.name}`;
|
|
199
|
+
if (seen.has(key))
|
|
200
|
+
continue; // project layer wins on same name
|
|
201
|
+
seen.add(key);
|
|
202
|
+
sources.push({ path: path.join(kindDir, entry.name), key });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return sources.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
207
|
+
}
|
package/dist/lib/loop.js
CHANGED
|
@@ -26,6 +26,8 @@ import { buildExecCommand, buildExecEnv } from './exec.js';
|
|
|
26
26
|
import { extractUsageEvents } from './budget/enforce.js';
|
|
27
27
|
import { parseTimeout } from './routines.js';
|
|
28
28
|
import { writeCheckpoint } from './checkpoint.js';
|
|
29
|
+
import { mailboxDir } from './mailbox.js';
|
|
30
|
+
import { composeWin32CommandLine } from './platform/index.js';
|
|
29
31
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30
32
|
/** Path to a run's loop-signal.json. */
|
|
31
33
|
export function loopSignalPath(runDir) {
|
|
@@ -129,8 +131,14 @@ export function defaultRunIteration(options) {
|
|
|
129
131
|
const cwd = execOptions.cwd || process.cwd();
|
|
130
132
|
const model = execOptions.model ?? `${execOptions.agent}-default`;
|
|
131
133
|
return new Promise((resolve, reject) => {
|
|
134
|
+
// DEP0190-safe shell spawn: on the win32 shell path compose ONE fully-quoted
|
|
135
|
+
// command line and pass an EMPTY args array (see composeWin32CommandLine) so
|
|
136
|
+
// Node never concatenates the args array — which carries the re-injected
|
|
137
|
+
// prompt — into the cmd.exe line unescaped.
|
|
132
138
|
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
133
|
-
const
|
|
139
|
+
const spawnCommand = useShell ? composeWin32CommandLine(executable, args) : executable;
|
|
140
|
+
const spawnArgs = useShell ? [] : args;
|
|
141
|
+
const child = spawn(spawnCommand, spawnArgs, {
|
|
134
142
|
cwd,
|
|
135
143
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
136
144
|
env,
|
|
@@ -207,6 +215,9 @@ export async function runLoop(execOptions, loop, ctx, deps) {
|
|
|
207
215
|
process.stderr.write(`[loop] WARNING: cross-iteration conversation continuity applies to claude only. ` +
|
|
208
216
|
`Each ${ctx.agent} iteration runs as an independent fresh conversation (no /continue handoff).\n`);
|
|
209
217
|
}
|
|
218
|
+
// Surface the run-level mailbox id (otherwise undiscoverable — runId is not in
|
|
219
|
+
// the session registry) so an operator can message the loop mid-flight.
|
|
220
|
+
process.stderr.write(`[loop] mailbox: agents message ${ctx.runId} "<text>"\n`);
|
|
210
221
|
let tokens = ctx.startTokens ?? 0;
|
|
211
222
|
let lastSignal;
|
|
212
223
|
let stopSignal = false;
|
|
@@ -271,6 +282,10 @@ export async function runLoop(execOptions, loop, ctx, deps) {
|
|
|
271
282
|
AGENTS_RUN_DIR: ctx.runDir,
|
|
272
283
|
AGENTS_LOOP_SIGNAL: loopSignalPath(ctx.runDir),
|
|
273
284
|
AGENTS_LOOP_ITERATION: String(iteration),
|
|
285
|
+
// Every iteration is a fresh session, so key the mailbox by the stable
|
|
286
|
+
// run id — one box for the whole loop. Overrides the per-iteration
|
|
287
|
+
// AGENTS_MAILBOX_DIR that buildExecEnv would derive from sessionId.
|
|
288
|
+
AGENTS_MAILBOX_DIR: mailboxDir(ctx.runId),
|
|
274
289
|
},
|
|
275
290
|
};
|
|
276
291
|
let result;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This machine's stable, human-readable id. Lives in a dependency-free leaf so
|
|
3
|
+
* low-level modules (state.ts) can key per-device paths off it without importing
|
|
4
|
+
* the secrets/session-sync layer (which would create a cycle).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a raw hostname into a stable device id: first label only,
|
|
8
|
+
* lowercased, non-alphanumerics collapsed to hyphens. `zion.tail…ts.net` and
|
|
9
|
+
* `ZION` both become `zion`. The single source for this transform — machineId()
|
|
10
|
+
* and cross-machine session grouping must agree or the local machine won't
|
|
11
|
+
* match its own registry key.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeHost(raw: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* This machine's stable, human-readable id, used as its R2 sync prefix, session
|
|
16
|
+
* mirror directory, `agents devices` self-key, and per-device config folder.
|
|
17
|
+
* Tailnet hostnames (zion, yosemite-s0, mac-mini) are already unique and
|
|
18
|
+
* readable; we lowercase and strip any domain suffix. Overridable via
|
|
19
|
+
* AGENTS_SYNC_MACHINE_ID for tests and unusual setups.
|
|
20
|
+
*/
|
|
21
|
+
export declare function machineId(): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This machine's stable, human-readable id. Lives in a dependency-free leaf so
|
|
3
|
+
* low-level modules (state.ts) can key per-device paths off it without importing
|
|
4
|
+
* the secrets/session-sync layer (which would create a cycle).
|
|
5
|
+
*/
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a raw hostname into a stable device id: first label only,
|
|
9
|
+
* lowercased, non-alphanumerics collapsed to hyphens. `zion.tail…ts.net` and
|
|
10
|
+
* `ZION` both become `zion`. The single source for this transform — machineId()
|
|
11
|
+
* and cross-machine session grouping must agree or the local machine won't
|
|
12
|
+
* match its own registry key.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeHost(raw) {
|
|
15
|
+
return raw.split('.')[0].trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-') || 'unknown';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* This machine's stable, human-readable id, used as its R2 sync prefix, session
|
|
19
|
+
* mirror directory, `agents devices` self-key, and per-device config folder.
|
|
20
|
+
* Tailnet hostnames (zion, yosemite-s0, mac-mini) are already unique and
|
|
21
|
+
* readable; we lowercase and strip any domain suffix. Overridable via
|
|
22
|
+
* AGENTS_SYNC_MACHINE_ID for tests and unusual setups.
|
|
23
|
+
*/
|
|
24
|
+
export function machineId() {
|
|
25
|
+
return normalizeHost(process.env.AGENTS_SYNC_MACHINE_ID || os.hostname());
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an `agents message <target>` argument to exactly one destination —
|
|
3
|
+
* a cloud task, one live local/teams/loop agent, or an error. The anti-misroute
|
|
4
|
+
* rule lives here: a target that matches zero or more-than-one live agent is
|
|
5
|
+
* NEVER guessed; the caller reports it. Pure (no I/O) so it is unit-testable.
|
|
6
|
+
*/
|
|
7
|
+
import type { ActiveSession } from './session/active.js';
|
|
8
|
+
export type MessageResolution = {
|
|
9
|
+
kind: 'cloud';
|
|
10
|
+
id: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'local';
|
|
13
|
+
id: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'none';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'ambiguous';
|
|
18
|
+
candidates: Array<{
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The mailbox id a live session's box is keyed by. Teams stamp a durable
|
|
25
|
+
* `agentId` (== the Claude session id for Claude teammates); a bare run has
|
|
26
|
+
* only its `sessionId`. The spawn-time `AGENTS_MAILBOX_DIR` wiring must key the
|
|
27
|
+
* box by this same id — this is the single source of truth for both sides.
|
|
28
|
+
*/
|
|
29
|
+
export declare function mailboxIdForActiveSession(s: ActiveSession): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve `target` against the live sessions. Exact id matches win over prefix
|
|
32
|
+
* matches; results are de-duped by canonical mailbox id (collapsed subagents
|
|
33
|
+
* share one). `isCloudTask` is consulted first so a cloud task id routes to the
|
|
34
|
+
* cloud provider.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveMessageTarget(target: string, sessions: ActiveSession[], isCloudTask: (id: string) => boolean): MessageResolution;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mailbox id a live session's box is keyed by. Teams stamp a durable
|
|
3
|
+
* `agentId` (== the Claude session id for Claude teammates); a bare run has
|
|
4
|
+
* only its `sessionId`. The spawn-time `AGENTS_MAILBOX_DIR` wiring must key the
|
|
5
|
+
* box by this same id — this is the single source of truth for both sides.
|
|
6
|
+
*/
|
|
7
|
+
export function mailboxIdForActiveSession(s) {
|
|
8
|
+
return s.agentId ?? s.sessionId;
|
|
9
|
+
}
|
|
10
|
+
function labelFor(s) {
|
|
11
|
+
return s.label ?? s.topic ?? s.teamName ?? s.host ?? s.context;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve `target` against the live sessions. Exact id matches win over prefix
|
|
15
|
+
* matches; results are de-duped by canonical mailbox id (collapsed subagents
|
|
16
|
+
* share one). `isCloudTask` is consulted first so a cloud task id routes to the
|
|
17
|
+
* cloud provider.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveMessageTarget(target, sessions, isCloudTask) {
|
|
20
|
+
if (isCloudTask(target))
|
|
21
|
+
return { kind: 'cloud', id: target };
|
|
22
|
+
// An empty target would make every `startsWith` prefix match — never guess.
|
|
23
|
+
if (target.length === 0)
|
|
24
|
+
return { kind: 'none' };
|
|
25
|
+
const exact = sessions.filter((s) => s.sessionId === target || s.agentId === target);
|
|
26
|
+
const chosen = exact.length > 0
|
|
27
|
+
? exact
|
|
28
|
+
: sessions.filter((s) => Boolean(s.sessionId?.startsWith(target)) || Boolean(s.agentId?.startsWith(target)));
|
|
29
|
+
// De-dupe by canonical mailbox id (one box per logical agent).
|
|
30
|
+
const byId = new Map();
|
|
31
|
+
for (const s of chosen) {
|
|
32
|
+
const id = mailboxIdForActiveSession(s);
|
|
33
|
+
if (id && !byId.has(id))
|
|
34
|
+
byId.set(id, s);
|
|
35
|
+
}
|
|
36
|
+
const ids = [...byId.keys()];
|
|
37
|
+
if (ids.length === 0)
|
|
38
|
+
return { kind: 'none' };
|
|
39
|
+
if (ids.length === 1)
|
|
40
|
+
return { kind: 'local', id: ids[0] };
|
|
41
|
+
return {
|
|
42
|
+
kind: 'ambiguous',
|
|
43
|
+
candidates: [...byId.entries()].map(([id, s]) => ({ id, label: labelFor(s) })),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** A single mailbox message. `text` may embed `host:/path` clip tokens. */
|
|
2
|
+
export interface MailboxMessage {
|
|
3
|
+
/** Unique, time-sortable id. Also the on-disk filename stem. */
|
|
4
|
+
msgId: string;
|
|
5
|
+
/** The mailboxId this message is addressed to (anti-misroute stamp). */
|
|
6
|
+
to: string;
|
|
7
|
+
/** Who sent it (operator label / agent id / host). Optional. */
|
|
8
|
+
from?: string;
|
|
9
|
+
/** ISO-8601 creation time. */
|
|
10
|
+
ts: string;
|
|
11
|
+
/** The message body. */
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A mailboxId must be a single, separator-free path segment. Real ids (session
|
|
16
|
+
* UUID / teams agentId / loop runId) already satisfy this; rejecting anything
|
|
17
|
+
* else fails loud instead of silently misrouting — a message whose id contained
|
|
18
|
+
* a `/` would nest under a different dir than the `to` stamp it is matched
|
|
19
|
+
* against, and would be dropped with no error. Also blocks `.`/`..` traversal
|
|
20
|
+
* once `agents message` starts accepting external target ids.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isValidMailboxId(mailboxId: string): boolean;
|
|
23
|
+
export declare function assertValidMailboxId(mailboxId: string): void;
|
|
24
|
+
/** Absolute path to a box. `root` override is for tests. */
|
|
25
|
+
export declare function mailboxDir(mailboxId: string, root?: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Enqueue a message into `boxDir` atomically. Returns the msgId. The `to` field
|
|
28
|
+
* is stamped so a drain can refuse a message that lands in the wrong box.
|
|
29
|
+
*/
|
|
30
|
+
export declare function enqueue(boxDir: string, msg: {
|
|
31
|
+
to: string;
|
|
32
|
+
text: string;
|
|
33
|
+
from?: string;
|
|
34
|
+
}): string;
|
|
35
|
+
/**
|
|
36
|
+
* Drain the box: return every pending message addressed to it, in FIFO order,
|
|
37
|
+
* removing them from the queue. Claim-first (inbox → processing → consumed) so
|
|
38
|
+
* an interrupted drain is recovered on the next call (at-least-once). Corrupt
|
|
39
|
+
* files and messages addressed to a different box are dropped, not returned.
|
|
40
|
+
*
|
|
41
|
+
* `boxId` defaults to the box's directory name — the id it was created under.
|
|
42
|
+
*/
|
|
43
|
+
export declare function drain(boxDir: string, boxId?: string): MailboxMessage[];
|
|
44
|
+
/** Read pending messages (inbox + in-flight) without consuming them. FIFO. */
|
|
45
|
+
export declare function peek(boxDir: string, boxId?: string): MailboxMessage[];
|
|
46
|
+
/** Delete pending (not-yet-claimed) inbox messages. Returns the count removed. */
|
|
47
|
+
export declare function clear(boxDir: string): number;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent mailbox — a file-spool that lets a message reach an already-running
|
|
3
|
+
* agent mid-flight. One box per logical agent, keyed by a unique-per-launch id
|
|
4
|
+
* (session UUID / teams agentId / loop runId), so boxes are disjoint and a
|
|
5
|
+
* message can never reach the wrong agent (see docs / plan velvety-conjuring-popcorn).
|
|
6
|
+
*
|
|
7
|
+
* Layout: <root>/<mailboxId>/{inbox,processing,consumed}/<msgId>.json
|
|
8
|
+
* inbox/ pending, written by `agents message`
|
|
9
|
+
* processing/ claimed by a drain (claim-first = crash-safe)
|
|
10
|
+
* consumed/ archived after delivery (and dropped mismatches)
|
|
11
|
+
*
|
|
12
|
+
* A box has a SINGLE consumer — the owning agent, whose tool calls (and thus
|
|
13
|
+
* hook-driven drains) are sequential. Writers may be concurrent; each enqueue
|
|
14
|
+
* is atomic (temp-write + rename), so a drain never observes a partial file.
|
|
15
|
+
* Delivery is at-least-once: an interrupted drain leaves the message in
|
|
16
|
+
* processing/, and the next drain recovers it. Consumers dedup by `msgId`.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
import { getMailboxRootDir } from './state.js';
|
|
22
|
+
/**
|
|
23
|
+
* A mailboxId must be a single, separator-free path segment. Real ids (session
|
|
24
|
+
* UUID / teams agentId / loop runId) already satisfy this; rejecting anything
|
|
25
|
+
* else fails loud instead of silently misrouting — a message whose id contained
|
|
26
|
+
* a `/` would nest under a different dir than the `to` stamp it is matched
|
|
27
|
+
* against, and would be dropped with no error. Also blocks `.`/`..` traversal
|
|
28
|
+
* once `agents message` starts accepting external target ids.
|
|
29
|
+
*/
|
|
30
|
+
export function isValidMailboxId(mailboxId) {
|
|
31
|
+
return /^[A-Za-z0-9._-]+$/.test(mailboxId) && mailboxId !== '.' && mailboxId !== '..';
|
|
32
|
+
}
|
|
33
|
+
export function assertValidMailboxId(mailboxId) {
|
|
34
|
+
if (!isValidMailboxId(mailboxId)) {
|
|
35
|
+
throw new Error(`Invalid mailboxId ${JSON.stringify(mailboxId)}: must be a single path segment ` +
|
|
36
|
+
`matching [A-Za-z0-9._-] (no separators, not '.'/'..').`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Absolute path to a box. `root` override is for tests. */
|
|
40
|
+
export function mailboxDir(mailboxId, root = getMailboxRootDir()) {
|
|
41
|
+
assertValidMailboxId(mailboxId);
|
|
42
|
+
return path.join(root, mailboxId);
|
|
43
|
+
}
|
|
44
|
+
function inboxDir(boxDir) { return path.join(boxDir, 'inbox'); }
|
|
45
|
+
function processingDir(boxDir) { return path.join(boxDir, 'processing'); }
|
|
46
|
+
function consumedDir(boxDir) { return path.join(boxDir, 'consumed'); }
|
|
47
|
+
/** Create the three sub-buckets. Idempotent. */
|
|
48
|
+
function ensureDirs(boxDir) {
|
|
49
|
+
fs.mkdirSync(inboxDir(boxDir), { recursive: true });
|
|
50
|
+
fs.mkdirSync(processingDir(boxDir), { recursive: true });
|
|
51
|
+
fs.mkdirSync(consumedDir(boxDir), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
let seq = 0;
|
|
54
|
+
/**
|
|
55
|
+
* `<epochMs>-<seq>-<rand>` — sorts by filename in FIFO order. The per-process
|
|
56
|
+
* monotonic `seq` breaks ties within the same millisecond (so a single writer's
|
|
57
|
+
* order is preserved); `rand` keeps it unique across processes/hosts.
|
|
58
|
+
*/
|
|
59
|
+
function newMsgId() {
|
|
60
|
+
const s = String(seq++).padStart(6, '0');
|
|
61
|
+
return `${Date.now()}-${s}-${randomUUID().slice(0, 8)}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Enqueue a message into `boxDir` atomically. Returns the msgId. The `to` field
|
|
65
|
+
* is stamped so a drain can refuse a message that lands in the wrong box.
|
|
66
|
+
*/
|
|
67
|
+
export function enqueue(boxDir, msg) {
|
|
68
|
+
assertValidMailboxId(msg.to);
|
|
69
|
+
ensureDirs(boxDir);
|
|
70
|
+
const msgId = newMsgId();
|
|
71
|
+
const record = {
|
|
72
|
+
msgId,
|
|
73
|
+
to: msg.to,
|
|
74
|
+
from: msg.from,
|
|
75
|
+
ts: new Date().toISOString(),
|
|
76
|
+
text: msg.text,
|
|
77
|
+
};
|
|
78
|
+
const target = path.join(inboxDir(boxDir), `${msgId}.json`);
|
|
79
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
80
|
+
fs.writeFileSync(tmp, JSON.stringify(record, null, 2), 'utf-8');
|
|
81
|
+
fs.renameSync(tmp, target); // atomic on a single filesystem
|
|
82
|
+
return msgId;
|
|
83
|
+
}
|
|
84
|
+
/** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
|
|
85
|
+
function readMessage(file) {
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const m = parsed;
|
|
101
|
+
if (typeof m?.msgId !== 'string' || typeof m?.to !== 'string' || typeof m?.text !== 'string') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return { msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text };
|
|
105
|
+
}
|
|
106
|
+
function jsonFiles(dir) {
|
|
107
|
+
let names;
|
|
108
|
+
try {
|
|
109
|
+
names = fs.readdirSync(dir);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return names.filter((n) => n.endsWith('.json')).sort();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Consume a file already sitting in `processing/`: read, verify it is addressed
|
|
118
|
+
* to this box, then archive to `consumed/`. Returns the message iff valid AND
|
|
119
|
+
* addressed here; a mismatched/corrupt file is archived (dropped) so it never
|
|
120
|
+
* loops. Returns null when the file vanished (a racing drain took it).
|
|
121
|
+
*/
|
|
122
|
+
function consumeClaimed(boxDir, name, expectedTo) {
|
|
123
|
+
const src = path.join(processingDir(boxDir), name);
|
|
124
|
+
const dest = path.join(consumedDir(boxDir), name);
|
|
125
|
+
const msg = readMessage(src);
|
|
126
|
+
try {
|
|
127
|
+
fs.renameSync(src, dest);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null; // already archived/claimed elsewhere
|
|
131
|
+
}
|
|
132
|
+
if (!msg || msg.to !== expectedTo)
|
|
133
|
+
return null; // dropped (corrupt or wrong box)
|
|
134
|
+
return msg;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Drain the box: return every pending message addressed to it, in FIFO order,
|
|
138
|
+
* removing them from the queue. Claim-first (inbox → processing → consumed) so
|
|
139
|
+
* an interrupted drain is recovered on the next call (at-least-once). Corrupt
|
|
140
|
+
* files and messages addressed to a different box are dropped, not returned.
|
|
141
|
+
*
|
|
142
|
+
* `boxId` defaults to the box's directory name — the id it was created under.
|
|
143
|
+
*/
|
|
144
|
+
export function drain(boxDir, boxId = path.basename(boxDir)) {
|
|
145
|
+
ensureDirs(boxDir);
|
|
146
|
+
const out = [];
|
|
147
|
+
// 1. Recover orphans left in processing/ by a prior interrupted drain.
|
|
148
|
+
for (const name of jsonFiles(processingDir(boxDir))) {
|
|
149
|
+
const msg = consumeClaimed(boxDir, name, boxId);
|
|
150
|
+
if (msg)
|
|
151
|
+
out.push(msg);
|
|
152
|
+
}
|
|
153
|
+
// 2. Claim and consume pending inbox messages.
|
|
154
|
+
for (const name of jsonFiles(inboxDir(boxDir))) {
|
|
155
|
+
const from = path.join(inboxDir(boxDir), name);
|
|
156
|
+
const to = path.join(processingDir(boxDir), name);
|
|
157
|
+
try {
|
|
158
|
+
fs.renameSync(from, to); // atomic claim
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
continue; // vanished — a racing drain took it
|
|
162
|
+
}
|
|
163
|
+
const msg = consumeClaimed(boxDir, name, boxId);
|
|
164
|
+
if (msg)
|
|
165
|
+
out.push(msg);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/** Read pending messages (inbox + in-flight) without consuming them. FIFO. */
|
|
170
|
+
export function peek(boxDir, boxId = path.basename(boxDir)) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const dir of [processingDir(boxDir), inboxDir(boxDir)]) {
|
|
173
|
+
for (const name of jsonFiles(dir)) {
|
|
174
|
+
const msg = readMessage(path.join(dir, name));
|
|
175
|
+
if (msg && msg.to === boxId)
|
|
176
|
+
out.push(msg);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** Delete pending (not-yet-claimed) inbox messages. Returns the count removed. */
|
|
182
|
+
export function clear(boxDir) {
|
|
183
|
+
let n = 0;
|
|
184
|
+
for (const name of jsonFiles(inboxDir(boxDir))) {
|
|
185
|
+
try {
|
|
186
|
+
fs.unlinkSync(path.join(inboxDir(boxDir), name));
|
|
187
|
+
n++;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// already gone — ignore
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return n;
|
|
194
|
+
}
|
package/dist/lib/mcp.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface McpTargetOperationResult {
|
|
|
39
39
|
* Parse an MCP server config from a YAML file.
|
|
40
40
|
*/
|
|
41
41
|
export declare function parseMcpServerConfig(filePath: string): McpYamlConfig | null;
|
|
42
|
+
/**
|
|
43
|
+
* Validate an MCP server name. Rejects names that could be misinterpreted as
|
|
44
|
+
* command-line options or that contain characters unsafe for argv/identifier use.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateMcpServerName(name: string): void;
|
|
42
47
|
/**
|
|
43
48
|
* List all MCP server configs from ~/.agents/mcp/.
|
|
44
49
|
*/
|