@phnx-labs/agents-cli 1.20.51 → 1.20.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.
- package/CHANGELOG.md +71 -0
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +8 -1
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +82 -3
- package/dist/commands/factory.js +8 -0
- package/dist/commands/feed.d.ts +38 -0
- package/dist/commands/feed.js +274 -0
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.d.ts +22 -0
- package/dist/commands/secrets.js +176 -42
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/teams.js +4 -0
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +21 -22
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.d.ts +15 -0
- package/dist/lib/cloud/rush.js +10 -16
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +19 -0
- package/dist/lib/crabbox/lease.js +22 -11
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +32 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +291 -101
- package/dist/lib/exec.js +20 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +180 -0
- package/dist/lib/feed.js +627 -0
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +529 -14
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -3
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +76 -21
- package/dist/lib/secrets/icloud-import.d.ts +79 -0
- package/dist/lib/secrets/icloud-import.js +203 -0
- package/dist/lib/secrets/index.d.ts +142 -2
- package/dist/lib/secrets/index.js +695 -30
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +32 -2
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +50 -1
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +20 -2
- package/dist/lib/versions.js +38 -48
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox liveness sweep and GC.
|
|
3
|
+
*
|
|
4
|
+
* A box whose owning agent is no longer alive is a ghost: pending messages will
|
|
5
|
+
* never be drained, and any feed block tied to that box is stale. This module
|
|
6
|
+
* archives dead-box messages and prunes old consumed entries so the spool stays
|
|
7
|
+
* bounded under fleet load.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { getMailboxRootDir } from './state.js';
|
|
12
|
+
import { mailboxDir, isValidMailboxId, readMessage, } from './mailbox.js';
|
|
13
|
+
import { listBlocks, removeBlock } from './feed.js';
|
|
14
|
+
const DEFAULT_MAX_CONSUMED_AGE_MINUTES = 24 * 60;
|
|
15
|
+
function consumedAgeMinutes(file, now) {
|
|
16
|
+
try {
|
|
17
|
+
const stat = fs.statSync(file);
|
|
18
|
+
return (now.getTime() - stat.mtimeMs) / 60_000;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function jsonFiles(dir) {
|
|
25
|
+
let names;
|
|
26
|
+
try {
|
|
27
|
+
names = fs.readdirSync(dir);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return names.filter((n) => n.endsWith('.json')).sort();
|
|
33
|
+
}
|
|
34
|
+
function archiveAllPending(boxDir, reason) {
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const dir of [path.join(boxDir, 'inbox'), path.join(boxDir, 'processing')]) {
|
|
37
|
+
for (const name of jsonFiles(dir)) {
|
|
38
|
+
const src = path.join(dir, name);
|
|
39
|
+
const dest = path.join(boxDir, 'consumed', name);
|
|
40
|
+
try {
|
|
41
|
+
const msg = readMessage(src);
|
|
42
|
+
if (msg) {
|
|
43
|
+
msg.dropped = reason;
|
|
44
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
45
|
+
fs.writeFileSync(tmp, JSON.stringify(msg, null, 2), 'utf-8');
|
|
46
|
+
fs.renameSync(tmp, dest);
|
|
47
|
+
fs.unlinkSync(src);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
fs.renameSync(src, dest);
|
|
51
|
+
}
|
|
52
|
+
n++;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// ignore racing writers
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
61
|
+
function pruneConsumed(boxDir, maxAgeMinutes, now) {
|
|
62
|
+
let n = 0;
|
|
63
|
+
const consumed = path.join(boxDir, 'consumed');
|
|
64
|
+
for (const name of jsonFiles(consumed)) {
|
|
65
|
+
const file = path.join(consumed, name);
|
|
66
|
+
if (consumedAgeMinutes(file, now) >= maxAgeMinutes) {
|
|
67
|
+
try {
|
|
68
|
+
fs.unlinkSync(file);
|
|
69
|
+
n++;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sweep all mailboxes. For dead boxes (not in `activeBoxIds`), archive every
|
|
80
|
+
* pending message as `dropped: dead` and remove any feed block tied to that
|
|
81
|
+
* mailbox. For live boxes, drop expired messages and prune old consumed files.
|
|
82
|
+
*/
|
|
83
|
+
export function gcMailbox(activeBoxIds, options = {}) {
|
|
84
|
+
const root = options.root ?? getMailboxRootDir();
|
|
85
|
+
const feedRoot = options.feedRoot;
|
|
86
|
+
const now = options.now ?? new Date();
|
|
87
|
+
const maxConsumedAgeMinutes = options.maxConsumedAgeMinutes ?? DEFAULT_MAX_CONSUMED_AGE_MINUTES;
|
|
88
|
+
const result = {
|
|
89
|
+
boxesScanned: 0,
|
|
90
|
+
deadBoxes: 0,
|
|
91
|
+
messagesDroppedExpired: 0,
|
|
92
|
+
messagesDroppedDead: 0,
|
|
93
|
+
consumedPruned: 0,
|
|
94
|
+
blocksRemoved: 0,
|
|
95
|
+
};
|
|
96
|
+
let boxNames;
|
|
97
|
+
try {
|
|
98
|
+
boxNames = fs.readdirSync(root);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
// Pre-compute dead blocks so we remove each once, regardless of how many
|
|
104
|
+
// messages reference it.
|
|
105
|
+
const blocksToRemove = new Set();
|
|
106
|
+
for (const block of listBlocks(feedRoot)) {
|
|
107
|
+
if (!activeBoxIds.has(block.mailboxId)) {
|
|
108
|
+
blocksToRemove.add(block.blockId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const name of boxNames) {
|
|
112
|
+
if (!isValidMailboxId(name))
|
|
113
|
+
continue;
|
|
114
|
+
result.boxesScanned++;
|
|
115
|
+
const boxDir = mailboxDir(name, root);
|
|
116
|
+
if (!activeBoxIds.has(name)) {
|
|
117
|
+
result.deadBoxes++;
|
|
118
|
+
result.messagesDroppedDead += archiveAllPending(boxDir, 'dead');
|
|
119
|
+
result.consumedPruned += pruneConsumed(boxDir, maxConsumedAgeMinutes, now);
|
|
120
|
+
// Also prune the empty box dir if it is now empty.
|
|
121
|
+
try {
|
|
122
|
+
for (const sub of ['inbox', 'processing', 'consumed']) {
|
|
123
|
+
const subdir = path.join(boxDir, sub);
|
|
124
|
+
if (fs.existsSync(subdir) && fs.readdirSync(subdir).length === 0) {
|
|
125
|
+
fs.rmdirSync(subdir);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (fs.existsSync(boxDir) && fs.readdirSync(boxDir).length === 0) {
|
|
129
|
+
fs.rmdirSync(boxDir);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Live box: lazy expiry is handled by drain/peek, but sweep here for GC metrics.
|
|
138
|
+
// We still need to read expired messages; reuse readMessage directly to avoid
|
|
139
|
+
// importing sweepExpired and duplicating logic.
|
|
140
|
+
for (const sub of ['inbox', 'processing']) {
|
|
141
|
+
const dir = path.join(boxDir, sub);
|
|
142
|
+
for (const file of jsonFiles(dir)) {
|
|
143
|
+
const msg = readMessage(path.join(dir, file));
|
|
144
|
+
if (msg && msg.expiresAt) {
|
|
145
|
+
const ts = Date.parse(msg.expiresAt);
|
|
146
|
+
if (!Number.isNaN(ts) && ts <= now.getTime()) {
|
|
147
|
+
result.messagesDroppedExpired++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
result.consumedPruned += pruneConsumed(boxDir, maxConsumedAgeMinutes, now);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const blockId of blocksToRemove) {
|
|
156
|
+
if (removeBlock(blockId, feedRoot)) {
|
|
157
|
+
result.blocksRemoved++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
package/dist/lib/mailbox.d.ts
CHANGED
|
@@ -8,8 +8,21 @@ export interface MailboxMessage {
|
|
|
8
8
|
from?: string;
|
|
9
9
|
/** ISO-8601 creation time. */
|
|
10
10
|
ts: string;
|
|
11
|
+
/** Optional ISO-8601 expiry time. Expired messages are dropped, not delivered. */
|
|
12
|
+
expiresAt?: string;
|
|
11
13
|
/** The message body. */
|
|
12
14
|
text: string;
|
|
15
|
+
/**
|
|
16
|
+
* The feed block this message answers. Set by `agents message` when the
|
|
17
|
+
* target has an open block, so the drain can surface consumed/continued
|
|
18
|
+
* receipts back to the feed store.
|
|
19
|
+
*/
|
|
20
|
+
blockId?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Drop reason when a message is archived without delivery (expired, dead box, etc.).
|
|
23
|
+
* Set by the TTL/liveness layer, not by writers.
|
|
24
|
+
*/
|
|
25
|
+
dropped?: string;
|
|
13
26
|
}
|
|
14
27
|
/**
|
|
15
28
|
* A mailboxId must be a single, separator-free path segment. Real ids (session
|
|
@@ -31,7 +44,18 @@ export declare function enqueue(boxDir: string, msg: {
|
|
|
31
44
|
to: string;
|
|
32
45
|
text: string;
|
|
33
46
|
from?: string;
|
|
47
|
+
blockId?: string;
|
|
48
|
+
ttlSeconds?: number;
|
|
34
49
|
}): string;
|
|
50
|
+
/** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
|
|
51
|
+
export declare function readMessage(file: string): MailboxMessage | null;
|
|
52
|
+
/** True when a message has a parsed expiry in the past. */
|
|
53
|
+
export declare function isExpired(msg: MailboxMessage, now?: Date): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Move expired messages from inbox/ and processing/ into consumed/ with a
|
|
56
|
+
* `dropped: expired` marker. Called by drain/peek before returning messages.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sweepExpired(boxDir: string, boxId?: string, now?: Date): number;
|
|
35
59
|
/**
|
|
36
60
|
* Drain the box: return every pending message addressed to it, in FIFO order,
|
|
37
61
|
* removing them from the queue. Claim-first (inbox → processing → consumed) so
|
|
@@ -40,8 +64,8 @@ export declare function enqueue(boxDir: string, msg: {
|
|
|
40
64
|
*
|
|
41
65
|
* `boxId` defaults to the box's directory name — the id it was created under.
|
|
42
66
|
*/
|
|
43
|
-
export declare function drain(boxDir: string, boxId?: string): MailboxMessage[];
|
|
67
|
+
export declare function drain(boxDir: string, boxId?: string, now?: Date): MailboxMessage[];
|
|
44
68
|
/** Read pending messages (inbox + in-flight) without consuming them. FIFO. */
|
|
45
|
-
export declare function peek(boxDir: string, boxId?: string): MailboxMessage[];
|
|
69
|
+
export declare function peek(boxDir: string, boxId?: string, now?: Date): MailboxMessage[];
|
|
46
70
|
/** Delete pending (not-yet-claimed) inbox messages. Returns the count removed. */
|
|
47
71
|
export declare function clear(boxDir: string): number;
|
package/dist/lib/mailbox.js
CHANGED
|
@@ -19,6 +19,7 @@ import * as fs from 'fs';
|
|
|
19
19
|
import * as path from 'path';
|
|
20
20
|
import { randomUUID } from 'crypto';
|
|
21
21
|
import { getMailboxRootDir } from './state.js';
|
|
22
|
+
import { recordMessageReceipt } from './feed.js';
|
|
22
23
|
/**
|
|
23
24
|
* A mailboxId must be a single, separator-free path segment. Real ids (session
|
|
24
25
|
* UUID / teams agentId / loop runId) already satisfy this; rejecting anything
|
|
@@ -68,13 +69,18 @@ export function enqueue(boxDir, msg) {
|
|
|
68
69
|
assertValidMailboxId(msg.to);
|
|
69
70
|
ensureDirs(boxDir);
|
|
70
71
|
const msgId = newMsgId();
|
|
72
|
+
const now = new Date();
|
|
71
73
|
const record = {
|
|
72
74
|
msgId,
|
|
73
75
|
to: msg.to,
|
|
74
76
|
from: msg.from,
|
|
75
|
-
ts:
|
|
77
|
+
ts: now.toISOString(),
|
|
76
78
|
text: msg.text,
|
|
79
|
+
blockId: msg.blockId,
|
|
77
80
|
};
|
|
81
|
+
if (msg.ttlSeconds != null && msg.ttlSeconds > 0) {
|
|
82
|
+
record.expiresAt = new Date(now.getTime() + msg.ttlSeconds * 1000).toISOString();
|
|
83
|
+
}
|
|
78
84
|
const target = path.join(inboxDir(boxDir), `${msgId}.json`);
|
|
79
85
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
80
86
|
fs.writeFileSync(tmp, JSON.stringify(record, null, 2), 'utf-8');
|
|
@@ -82,7 +88,7 @@ export function enqueue(boxDir, msg) {
|
|
|
82
88
|
return msgId;
|
|
83
89
|
}
|
|
84
90
|
/** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
|
|
85
|
-
function readMessage(file) {
|
|
91
|
+
export function readMessage(file) {
|
|
86
92
|
let raw;
|
|
87
93
|
try {
|
|
88
94
|
raw = fs.readFileSync(file, 'utf-8');
|
|
@@ -101,7 +107,62 @@ function readMessage(file) {
|
|
|
101
107
|
if (typeof m?.msgId !== 'string' || typeof m?.to !== 'string' || typeof m?.text !== 'string') {
|
|
102
108
|
return null;
|
|
103
109
|
}
|
|
104
|
-
return { msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text };
|
|
110
|
+
return { msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text, expiresAt: m.expiresAt, blockId: m.blockId, dropped: m.dropped };
|
|
111
|
+
}
|
|
112
|
+
/** True when a message has a parsed expiry in the past. */
|
|
113
|
+
export function isExpired(msg, now = new Date()) {
|
|
114
|
+
if (!msg.expiresAt)
|
|
115
|
+
return false;
|
|
116
|
+
const ts = Date.parse(msg.expiresAt);
|
|
117
|
+
return !Number.isNaN(ts) && ts <= now.getTime();
|
|
118
|
+
}
|
|
119
|
+
function archiveDropped(boxDir, name, reason) {
|
|
120
|
+
const src = path.join(inboxDir(boxDir), name);
|
|
121
|
+
const dest = path.join(consumedDir(boxDir), name);
|
|
122
|
+
try {
|
|
123
|
+
const msg = readMessage(src);
|
|
124
|
+
if (msg) {
|
|
125
|
+
msg.dropped = reason;
|
|
126
|
+
fs.writeFileSync(`${dest}.tmp`, JSON.stringify(msg, null, 2), 'utf-8');
|
|
127
|
+
fs.renameSync(`${dest}.tmp`, dest);
|
|
128
|
+
fs.unlinkSync(src);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// corrupt — just move it out of inbox
|
|
132
|
+
fs.renameSync(src, dest);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// best-effort
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Move expired messages from inbox/ and processing/ into consumed/ with a
|
|
141
|
+
* `dropped: expired` marker. Called by drain/peek before returning messages.
|
|
142
|
+
*/
|
|
143
|
+
export function sweepExpired(boxDir, boxId = path.basename(boxDir), now = new Date()) {
|
|
144
|
+
ensureDirs(boxDir);
|
|
145
|
+
let n = 0;
|
|
146
|
+
for (const dir of [inboxDir(boxDir), processingDir(boxDir)]) {
|
|
147
|
+
for (const name of jsonFiles(dir)) {
|
|
148
|
+
const msg = readMessage(path.join(dir, name));
|
|
149
|
+
if (msg && msg.to === boxId && isExpired(msg, now)) {
|
|
150
|
+
try {
|
|
151
|
+
const dest = path.join(consumedDir(boxDir), name);
|
|
152
|
+
msg.dropped = 'expired';
|
|
153
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
154
|
+
fs.writeFileSync(tmp, JSON.stringify(msg, null, 2), 'utf-8');
|
|
155
|
+
fs.renameSync(tmp, dest);
|
|
156
|
+
fs.unlinkSync(path.join(dir, name));
|
|
157
|
+
n++;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// ignore racing claimers
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return n;
|
|
105
166
|
}
|
|
106
167
|
function jsonFiles(dir) {
|
|
107
168
|
let names;
|
|
@@ -118,6 +179,9 @@ function jsonFiles(dir) {
|
|
|
118
179
|
* to this box, then archive to `consumed/`. Returns the message iff valid AND
|
|
119
180
|
* addressed here; a mismatched/corrupt file is archived (dropped) so it never
|
|
120
181
|
* loops. Returns null when the file vanished (a racing drain took it).
|
|
182
|
+
*
|
|
183
|
+
* When the message carries a `blockId`, the consumed event is surfaced back to
|
|
184
|
+
* the feed store so the operator can see delivery confirmation.
|
|
121
185
|
*/
|
|
122
186
|
function consumeClaimed(boxDir, name, expectedTo) {
|
|
123
187
|
const src = path.join(processingDir(boxDir), name);
|
|
@@ -131,6 +195,15 @@ function consumeClaimed(boxDir, name, expectedTo) {
|
|
|
131
195
|
}
|
|
132
196
|
if (!msg || msg.to !== expectedTo)
|
|
133
197
|
return null; // dropped (corrupt or wrong box)
|
|
198
|
+
if (msg.blockId) {
|
|
199
|
+
try {
|
|
200
|
+
const feedRoot = process.env.AGENTS_FEED_DIR;
|
|
201
|
+
recordMessageReceipt(msg.blockId, { msgId: msg.msgId, status: 'consumed', at: new Date().toISOString(), from: msg.from }, feedRoot);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Receipt surfacing is best-effort; never stall delivery.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
134
207
|
return msg;
|
|
135
208
|
}
|
|
136
209
|
/**
|
|
@@ -141,8 +214,9 @@ function consumeClaimed(boxDir, name, expectedTo) {
|
|
|
141
214
|
*
|
|
142
215
|
* `boxId` defaults to the box's directory name — the id it was created under.
|
|
143
216
|
*/
|
|
144
|
-
export function drain(boxDir, boxId = path.basename(boxDir)) {
|
|
217
|
+
export function drain(boxDir, boxId = path.basename(boxDir), now = new Date()) {
|
|
145
218
|
ensureDirs(boxDir);
|
|
219
|
+
sweepExpired(boxDir, boxId, now);
|
|
146
220
|
const out = [];
|
|
147
221
|
// 1. Recover orphans left in processing/ by a prior interrupted drain.
|
|
148
222
|
for (const name of jsonFiles(processingDir(boxDir))) {
|
|
@@ -167,7 +241,8 @@ export function drain(boxDir, boxId = path.basename(boxDir)) {
|
|
|
167
241
|
return out;
|
|
168
242
|
}
|
|
169
243
|
/** Read pending messages (inbox + in-flight) without consuming them. FIFO. */
|
|
170
|
-
export function peek(boxDir, boxId = path.basename(boxDir)) {
|
|
244
|
+
export function peek(boxDir, boxId = path.basename(boxDir), now = new Date()) {
|
|
245
|
+
sweepExpired(boxDir, boxId, now);
|
|
171
246
|
const out = [];
|
|
172
247
|
for (const dir of [processingDir(boxDir), inboxDir(boxDir)]) {
|
|
173
248
|
for (const name of jsonFiles(dir)) {
|
package/dist/lib/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
|
+
import * as os from 'os';
|
|
14
15
|
import { getMcpDir, getUserMcpDir, getProjectAgentsDir, getVersionsDir } from './state.js';
|
|
15
16
|
import { getBinaryPath, getVersionHomePath } from './versions.js';
|
|
16
17
|
import { IS_WINDOWS, needsWindowsShell } from './platform/index.js';
|
|
@@ -287,6 +288,26 @@ export async function registerMcpCommandToTargets(targets, name, commandSpec, sc
|
|
|
287
288
|
function registerMcpCommand(agentId, name, commandSpec, scope, transport, options = {}) {
|
|
288
289
|
try {
|
|
289
290
|
validateMcpServerName(name);
|
|
291
|
+
if (agentId === 'hermes' || agentId === 'forge') {
|
|
292
|
+
const server = {
|
|
293
|
+
name,
|
|
294
|
+
path: '',
|
|
295
|
+
config: {
|
|
296
|
+
name,
|
|
297
|
+
transport: transport === 'http' ? 'http' : 'stdio',
|
|
298
|
+
...(transport === 'http'
|
|
299
|
+
? { url: commandSpec.command }
|
|
300
|
+
: { command: commandSpec.command, args: commandSpec.args }),
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
if (agentId === 'hermes') {
|
|
304
|
+
installMcpToHermesConfig(server, options.home || os.homedir());
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
installMcpToForgeConfig(server, options.home || os.homedir());
|
|
308
|
+
}
|
|
309
|
+
return { success: true };
|
|
310
|
+
}
|
|
290
311
|
const bin = options.binary || AGENTS[agentId].cliCommand;
|
|
291
312
|
const commandArgs = [commandSpec.command, ...commandSpec.args];
|
|
292
313
|
const args = agentId === 'claude'
|
|
@@ -410,6 +431,59 @@ function installMcpToFactoryConfig(server, versionHome) {
|
|
|
410
431
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
411
432
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
412
433
|
}
|
|
434
|
+
function installMcpToHermesConfig(server, versionHome) {
|
|
435
|
+
const configPath = path.join(versionHome, '.hermes', 'config.yaml');
|
|
436
|
+
let config = {};
|
|
437
|
+
if (fs.existsSync(configPath)) {
|
|
438
|
+
const parsed = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
439
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
440
|
+
config = parsed;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!config.mcp_servers || typeof config.mcp_servers !== 'object' || Array.isArray(config.mcp_servers)) {
|
|
444
|
+
config.mcp_servers = {};
|
|
445
|
+
}
|
|
446
|
+
const mcpServers = config.mcp_servers;
|
|
447
|
+
if (server.config.transport === 'stdio') {
|
|
448
|
+
mcpServers[server.name] = {
|
|
449
|
+
command: server.config.command,
|
|
450
|
+
args: server.config.args || [],
|
|
451
|
+
...(server.config.env && { env: server.config.env }),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
mcpServers[server.name] = {
|
|
456
|
+
url: server.config.url,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
460
|
+
fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
|
|
461
|
+
}
|
|
462
|
+
function installMcpToForgeConfig(server, versionHome) {
|
|
463
|
+
const configPath = path.join(versionHome, '.forge', '.mcp.json');
|
|
464
|
+
let config = {};
|
|
465
|
+
if (fs.existsSync(configPath)) {
|
|
466
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
467
|
+
}
|
|
468
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
|
469
|
+
config.mcpServers = {};
|
|
470
|
+
}
|
|
471
|
+
const mcpServers = config.mcpServers;
|
|
472
|
+
if (server.config.transport === 'stdio') {
|
|
473
|
+
mcpServers[server.name] = {
|
|
474
|
+
command: server.config.command,
|
|
475
|
+
args: server.config.args || [],
|
|
476
|
+
...(server.config.env && { env: server.config.env }),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
mcpServers[server.name] = {
|
|
481
|
+
url: server.config.url,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
485
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
486
|
+
}
|
|
413
487
|
function installMcpToOpenCodeConfig(server, versionHome) {
|
|
414
488
|
const configPath = path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
415
489
|
let config = {};
|
|
@@ -505,6 +579,14 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
|
|
|
505
579
|
installMcpToFactoryConfig(server, versionHome);
|
|
506
580
|
applied.push(server.name);
|
|
507
581
|
}
|
|
582
|
+
else if (agentId === 'hermes') {
|
|
583
|
+
installMcpToHermesConfig(server, versionHome);
|
|
584
|
+
applied.push(server.name);
|
|
585
|
+
}
|
|
586
|
+
else if (agentId === 'forge') {
|
|
587
|
+
installMcpToForgeConfig(server, versionHome);
|
|
588
|
+
applied.push(server.name);
|
|
589
|
+
}
|
|
508
590
|
}
|
|
509
591
|
catch (err) {
|
|
510
592
|
const message = err.message;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical agent memory resource — accumulated facts/preferences/knowledge
|
|
3
|
+
* distinct from `rules` (instructions / AGENTS.md persona).
|
|
4
|
+
*
|
|
5
|
+
* Layout (project > user > system layering):
|
|
6
|
+
* ~/.agents/memory/MEMORY.md always-read index
|
|
7
|
+
* ~/.agents/memory/<slug>.md individual facts
|
|
8
|
+
* ~/.agents/.system/memory/ system layer
|
|
9
|
+
* <project>/.agents/memory/ project layer
|
|
10
|
+
*
|
|
11
|
+
* Sync fans out into each capable agent's version home under an agent-specific
|
|
12
|
+
* target dir (see memoryTargetDir).
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentId } from './types.js';
|
|
15
|
+
export interface MemoryFact {
|
|
16
|
+
/** Filename without .md (slug). */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Absolute path to the fact file. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Layer that wins for this name. */
|
|
21
|
+
layer: 'project' | 'user' | 'system';
|
|
22
|
+
/** First non-empty line of the body (for list display). */
|
|
23
|
+
summary: string;
|
|
24
|
+
}
|
|
25
|
+
export interface MemoryLayerDir {
|
|
26
|
+
layer: 'project' | 'user' | 'system';
|
|
27
|
+
dir: string;
|
|
28
|
+
}
|
|
29
|
+
/** User-layer memory root (~/.agents/memory/). */
|
|
30
|
+
export declare function getUserMemoryDir(): string;
|
|
31
|
+
/** System-layer memory root (~/.agents/.system/memory/). */
|
|
32
|
+
export declare function getSystemMemoryDir(): string;
|
|
33
|
+
/** Project-layer memory root when a project agents dir exists. */
|
|
34
|
+
export declare function getProjectMemoryDir(cwd?: string): string | null;
|
|
35
|
+
/** Ensure the user memory dir exists (creates MEMORY.md index if missing). */
|
|
36
|
+
export declare function ensureUserMemoryDir(): string;
|
|
37
|
+
/** Layer dirs highest-priority first. */
|
|
38
|
+
export declare function getMemoryLayerDirs(cwd?: string): MemoryLayerDir[];
|
|
39
|
+
/** List memory facts with project > user > system override on name. */
|
|
40
|
+
export declare function listMemoryFacts(cwd?: string): MemoryFact[];
|
|
41
|
+
/** Read one fact by name (winning layer). */
|
|
42
|
+
export declare function readMemoryFact(name: string, cwd?: string): MemoryFact | null;
|
|
43
|
+
/** Write a fact into the user layer. Returns the absolute path. */
|
|
44
|
+
export declare function addMemoryFact(name: string, body: string): string;
|
|
45
|
+
/** Remove a fact from the user layer. Returns true if a file was deleted. */
|
|
46
|
+
export declare function removeMemoryFact(name: string): boolean;
|
|
47
|
+
/** Rebuild MEMORY.md index from sibling fact files in a single layer dir. */
|
|
48
|
+
export declare function rebuildMemoryIndex(dir: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Per-agent target directory (relative to version home) for synced memory.
|
|
51
|
+
* Claude/Codex/OpenClaw/Grok get native-ish paths; others get a generic memory/.
|
|
52
|
+
*/
|
|
53
|
+
export declare function memoryTargetDir(agent: AgentId): string;
|
|
54
|
+
/** Copy canonical layered memory into one version home. Returns fact names written. */
|
|
55
|
+
export declare function syncMemoryToVersionHome(agent: AgentId, versionHome: string, cwd?: string): string[];
|