@agentconnect.md/daemon 1.0.0-rc.210 → 1.0.0-rc.212
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/dist/acp-host-CuMoxt_h.js +10410 -0
- package/dist/acp-host-CuMoxt_h.js.map +1 -0
- package/dist/{agent-list-D-RwgYN7.js → agent-list-CoYaBcDx.js} +2 -2
- package/dist/{agent-list-D-RwgYN7.js.map → agent-list-CoYaBcDx.js.map} +1 -1
- package/dist/{bridge-C8-TYXsj.js → bridge-DoUrmBw9.js} +5 -2
- package/dist/{bridge-C8-TYXsj.js.map → bridge-DoUrmBw9.js.map} +1 -1
- package/dist/chat-CMmt04kZ.js +78 -0
- package/dist/chat-CMmt04kZ.js.map +1 -0
- package/dist/commander-B5QVEFzH.js +2965 -0
- package/dist/commander-B5QVEFzH.js.map +1 -0
- package/dist/esm-DrGCKNjx.js +1516 -0
- package/dist/esm-DrGCKNjx.js.map +1 -0
- package/dist/execAsync-C-9b-eWN.js +12 -0
- package/dist/execAsync-C-9b-eWN.js.map +1 -0
- package/dist/getMachineId-bsd-CfqSzACM.js +29 -0
- package/dist/getMachineId-bsd-CfqSzACM.js.map +1 -0
- package/dist/getMachineId-darwin-CDKafrUs.js +26 -0
- package/dist/getMachineId-darwin-CDKafrUs.js.map +1 -0
- package/dist/getMachineId-linux-DG3AvXtg.js +22 -0
- package/dist/getMachineId-linux-DG3AvXtg.js.map +1 -0
- package/dist/getMachineId-unsupported-BIPpTXqb.js +17 -0
- package/dist/getMachineId-unsupported-BIPpTXqb.js.map +1 -0
- package/dist/getMachineId-win-DNO3uByP.js +28 -0
- package/dist/getMachineId-win-DNO3uByP.js.map +1 -0
- package/dist/{git-credential-DWH0EUV6.js → git-credential-BfkyzKn2.js} +3 -2
- package/dist/{git-credential-DWH0EUV6.js.map → git-credential-BfkyzKn2.js.map} +1 -1
- package/dist/gitcred-server-Dcsq6QJe.js +236 -0
- package/dist/gitcred-server-Dcsq6QJe.js.map +1 -0
- package/dist/index.js +66381 -101142
- package/dist/index.js.map +1 -1
- package/dist/ipc-2Xbxe7uu.js +35 -0
- package/dist/ipc-2Xbxe7uu.js.map +1 -0
- package/dist/load-agents-nWFFmN5-.js +316 -0
- package/dist/load-agents-nWFFmN5-.js.map +1 -0
- package/dist/lock-CA9Bm8_s.js +66 -0
- package/dist/lock-CA9Bm8_s.js.map +1 -0
- package/dist/login-hti12388.js +229 -0
- package/dist/login-hti12388.js.map +1 -0
- package/dist/paths-CcRUjFjy.js +2 -0
- package/dist/paths-DEcUO8lC.js +43 -0
- package/dist/paths-DEcUO8lC.js.map +1 -0
- package/dist/rolldown-runtime-CE-6LUnI.js +44 -0
- package/dist/run-foreground-Bzas4Kly.js +83554 -0
- package/dist/run-foreground-Bzas4Kly.js.map +1 -0
- package/dist/run-foreground-DJyg3onO.js +2 -0
- package/dist/schemas-DujcoO0q.js +4357 -0
- package/dist/schemas-DujcoO0q.js.map +1 -0
- package/dist/service-6w0WKhdx.js +2 -0
- package/dist/service-BQVw7wbz.js +245 -0
- package/dist/service-BQVw7wbz.js.map +1 -0
- package/dist/src-DDtlVutj.js +811 -0
- package/dist/src-DDtlVutj.js.map +1 -0
- package/dist/version-DGaRXjBK.js +7 -0
- package/dist/version-DGaRXjBK.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/mcp/ipc.ts
|
|
2
|
+
/** Frame a message for the wire: compact JSON + a single trailing newline. */
|
|
3
|
+
function encodeFrame(msg) {
|
|
4
|
+
return JSON.stringify(msg) + "\n";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Split a buffer of newline-delimited frames into parsed objects plus the
|
|
8
|
+
* trailing partial line. Callers keep `rest` and prepend it to the next chunk.
|
|
9
|
+
*
|
|
10
|
+
* Per-line tolerant: a malformed (non-JSON) line is skipped, not thrown — so one
|
|
11
|
+
* bad frame can't crash a stream reader or discard the good frames batched
|
|
12
|
+
* alongside it in the same chunk. `onError` lets callers log dropped lines.
|
|
13
|
+
*/
|
|
14
|
+
function decodeFrames(buf, onError) {
|
|
15
|
+
const parts = buf.split("\n");
|
|
16
|
+
const rest = parts.pop() ?? "";
|
|
17
|
+
const messages = [];
|
|
18
|
+
for (const line of parts) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed) continue;
|
|
21
|
+
try {
|
|
22
|
+
messages.push(JSON.parse(trimmed));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
onError?.(trimmed, err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
messages,
|
|
29
|
+
rest
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { encodeFrame as n, decodeFrames as t };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=ipc-2Xbxe7uu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-2Xbxe7uu.js","names":[],"sources":["../src/mcp/ipc.ts"],"sourcesContent":["import type { ToolDescriptor } from './tools.js'\n\n/**\n * Tiny newline-delimited JSON-RPC-ish protocol spoken over the daemon's MCP\n * control socket. The `agentconnect mcp-bridge` subprocess is the client; the\n * daemon is the server. Each request carries the per-session `token` minted at\n * `session/new` so the daemon can resolve which channel/thread/agent it belongs\n * to. This is daemon-internal IPC — NOT a daemon↔CP protocol frame.\n */\nexport interface IpcListToolsReq {\n id: number\n token: string\n op: 'listTools'\n}\n\nexport interface IpcCallToolReq {\n id: number\n token: string\n op: 'callTool'\n name: string\n args: Record<string, unknown>\n}\n\nexport type IpcRequest = IpcListToolsReq | IpcCallToolReq\n\nexport interface IpcListToolsResult {\n tools: ToolDescriptor[]\n}\n\nexport interface IpcResponse {\n id: number\n ok: boolean\n /** On `listTools`: IpcListToolsResult. On `callTool`: the tool's plain result. */\n result?: unknown\n error?: string\n}\n\n/** Frame a message for the wire: compact JSON + a single trailing newline. */\nexport function encodeFrame(msg: IpcRequest | IpcResponse): string {\n return JSON.stringify(msg) + '\\n'\n}\n\n/**\n * Split a buffer of newline-delimited frames into parsed objects plus the\n * trailing partial line. Callers keep `rest` and prepend it to the next chunk.\n *\n * Per-line tolerant: a malformed (non-JSON) line is skipped, not thrown — so one\n * bad frame can't crash a stream reader or discard the good frames batched\n * alongside it in the same chunk. `onError` lets callers log dropped lines.\n */\nexport function decodeFrames<T>(\n buf: string,\n onError?: (line: string, err: unknown) => void\n): { messages: T[]; rest: string } {\n const parts = buf.split('\\n')\n const rest = parts.pop() ?? ''\n const messages: T[] = []\n for (const line of parts) {\n const trimmed = line.trim()\n if (!trimmed) continue\n try {\n messages.push(JSON.parse(trimmed) as T)\n } catch (err) {\n onError?.(trimmed, err)\n }\n }\n return { messages, rest }\n}\n"],"mappings":";;AAsCA,SAAgB,YAAY,KAAuC;CACjE,OAAO,KAAK,UAAU,GAAG,IAAI;AAC/B;;;;;;;;;AAUA,SAAgB,aACd,KACA,SACiC;CACjC,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,MAAM,OAAO,MAAM,IAAI,KAAK;CAC5B,MAAM,WAAgB,CAAC;CACvB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,KAAK;EAC1B,IAAI,CAAC,SAAS;EACd,IAAI;GACF,SAAS,KAAK,KAAK,MAAM,OAAO,CAAM;EACxC,SAAS,KAAK;GACZ,UAAU,SAAS,GAAG;EACxB;CACF;CACA,OAAO;EAAE;EAAU;CAAK;AAC1B"}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { d as number, f as object, g as string, h as record, i as boolean, l as literal, o as discriminatedUnion, r as array, t as _enum } from "./schemas-DujcoO0q.js";
|
|
2
|
+
import { l as resolveRoot, r as defaultAgentsDir, t as configPath } from "./paths-DEcUO8lC.js";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
//#region src/config/config-schema.ts
|
|
6
|
+
const RuntimeDefSchema = object({
|
|
7
|
+
command: string(),
|
|
8
|
+
args: array(string()).default([]),
|
|
9
|
+
env: array(object({
|
|
10
|
+
name: string(),
|
|
11
|
+
value: string()
|
|
12
|
+
})).default([])
|
|
13
|
+
});
|
|
14
|
+
const ConfigSchema = object({
|
|
15
|
+
version: literal(1),
|
|
16
|
+
daemonId: string().optional(),
|
|
17
|
+
webAppUrl: string().optional(),
|
|
18
|
+
controlPlane: object({
|
|
19
|
+
enabled: boolean().default(true),
|
|
20
|
+
url: string().optional(),
|
|
21
|
+
key: string().optional(),
|
|
22
|
+
heartbeatMs: number().int().default(15e3)
|
|
23
|
+
}).default({
|
|
24
|
+
enabled: false,
|
|
25
|
+
heartbeatMs: 15e3
|
|
26
|
+
}),
|
|
27
|
+
agentsDir: string().optional(),
|
|
28
|
+
runtimes: record(string(), RuntimeDefSchema).optional(),
|
|
29
|
+
logging: object({ level: _enum([
|
|
30
|
+
"trace",
|
|
31
|
+
"debug",
|
|
32
|
+
"info",
|
|
33
|
+
"warn",
|
|
34
|
+
"error"
|
|
35
|
+
]).default("info") }).default({ level: "info" }),
|
|
36
|
+
limits: object({
|
|
37
|
+
maxAgents: number().int().default(8),
|
|
38
|
+
maxConcurrentSessions: number().int().default(32),
|
|
39
|
+
agentIdleTimeoutMs: number().int().default(9e5),
|
|
40
|
+
idleSweepMs: number().int().default(6e4),
|
|
41
|
+
shutdownDrainMs: number().int().default(25e3),
|
|
42
|
+
cancelBackstopMs: number().int().default(3e4),
|
|
43
|
+
agentStartAttempts: number().int().min(1).default(3),
|
|
44
|
+
agentStartBackoffMs: number().int().min(0).default(500),
|
|
45
|
+
maxAttachmentBytes: number().int().default(8 * 1024 * 1024)
|
|
46
|
+
}).default({
|
|
47
|
+
maxAgents: 8,
|
|
48
|
+
maxConcurrentSessions: 32,
|
|
49
|
+
agentIdleTimeoutMs: 9e5,
|
|
50
|
+
idleSweepMs: 6e4,
|
|
51
|
+
shutdownDrainMs: 25e3,
|
|
52
|
+
cancelBackstopMs: 3e4,
|
|
53
|
+
agentStartAttempts: 3,
|
|
54
|
+
agentStartBackoffMs: 500,
|
|
55
|
+
maxAttachmentBytes: 8 * 1024 * 1024
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/config/load-config.ts
|
|
60
|
+
function loadConfig(opts = {}) {
|
|
61
|
+
const root = resolveRoot(opts.root);
|
|
62
|
+
const file = opts.configPath ?? configPath(root);
|
|
63
|
+
let raw;
|
|
64
|
+
if (existsSync(file)) raw = JSON.parse(readFileSync(file, "utf8"));
|
|
65
|
+
else if (opts.autoCreate) {
|
|
66
|
+
raw = { version: 1 };
|
|
67
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
68
|
+
writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
|
|
69
|
+
} else if (opts.optional) raw = { version: 1 };
|
|
70
|
+
else throw new Error(`config not found: ${file} (create it, pass --config, or run \`agentconnect login\`)`);
|
|
71
|
+
const cfg = ConfigSchema.parse(raw);
|
|
72
|
+
const o = opts.overrides ?? {};
|
|
73
|
+
if (o.daemonId) cfg.daemonId = o.daemonId;
|
|
74
|
+
if (o.logLevel) cfg.logging.level = o.logLevel;
|
|
75
|
+
if (o.maxAgents !== void 0) cfg.limits.maxAgents = o.maxAgents;
|
|
76
|
+
if (o.cpUrl) cfg.controlPlane.url = o.cpUrl;
|
|
77
|
+
if (o.cpKey) cfg.controlPlane.key = o.cpKey;
|
|
78
|
+
if (o.cpUrl || o.cpKey) cfg.controlPlane.enabled = true;
|
|
79
|
+
if (o.noCp) cfg.controlPlane.enabled = false;
|
|
80
|
+
cfg.agentsDir = o.agentsDir ?? cfg.agentsDir ?? defaultAgentsDir(root);
|
|
81
|
+
return cfg;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Persist a (freshly-minted) `daemonId` back into config.json so it is stable
|
|
85
|
+
* per install. Best-effort: a write failure is swallowed (the daemon still runs
|
|
86
|
+
* with the in-memory id this session).
|
|
87
|
+
*/
|
|
88
|
+
function persistDaemonId(root, daemonId) {
|
|
89
|
+
try {
|
|
90
|
+
const file = configPath(resolveRoot(root));
|
|
91
|
+
const raw = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : { version: 1 };
|
|
92
|
+
raw.daemonId = daemonId;
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
94
|
+
writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/agents/agent-schema.ts
|
|
99
|
+
const BindMatchSchema = discriminatedUnion("kind", [
|
|
100
|
+
object({ kind: literal("mention") }),
|
|
101
|
+
object({ kind: literal("dm") }),
|
|
102
|
+
object({
|
|
103
|
+
kind: literal("keyword"),
|
|
104
|
+
value: string()
|
|
105
|
+
}),
|
|
106
|
+
object({ kind: literal("auto") })
|
|
107
|
+
]);
|
|
108
|
+
const BindRuleConfigSchema = object({
|
|
109
|
+
channel: string().optional(),
|
|
110
|
+
thread: string().optional(),
|
|
111
|
+
match: BindMatchSchema
|
|
112
|
+
});
|
|
113
|
+
const SlackConfigSchema = object({
|
|
114
|
+
botToken: string(),
|
|
115
|
+
appToken: string(),
|
|
116
|
+
signingSecret: string().optional(),
|
|
117
|
+
botUserId: string().optional(),
|
|
118
|
+
allowedUserIds: array(string()).default([]),
|
|
119
|
+
bindRules: array(BindRuleConfigSchema).default([])
|
|
120
|
+
});
|
|
121
|
+
const IntegrationSchema = object({
|
|
122
|
+
id: string(),
|
|
123
|
+
platform: literal("slack"),
|
|
124
|
+
slack: SlackConfigSchema
|
|
125
|
+
});
|
|
126
|
+
/** A scheduled trigger for THIS agent: every `schedule` tick, prompt the agent
|
|
127
|
+
* with `trigger`. `target` is optional output routing — when present the daemon
|
|
128
|
+
* posts the trigger into that channel and the session replies in its thread;
|
|
129
|
+
* absent ⇒ headless fire (no platform output). `origin:"cp"` marks CP-pushed
|
|
130
|
+
* entries (written by cron/upsert, pruned by drop.crons); hand-authored entries
|
|
131
|
+
* have no origin and are never touched by the CP. */
|
|
132
|
+
const CronDefSchema = object({
|
|
133
|
+
id: string(),
|
|
134
|
+
schedule: string(),
|
|
135
|
+
target: object({
|
|
136
|
+
platform: literal("slack"),
|
|
137
|
+
channel: string(),
|
|
138
|
+
integrationId: string().optional()
|
|
139
|
+
}).optional(),
|
|
140
|
+
trigger: string(),
|
|
141
|
+
enabled: boolean().default(true),
|
|
142
|
+
origin: literal("cp").optional()
|
|
143
|
+
});
|
|
144
|
+
const AgentSchema = object({
|
|
145
|
+
id: string(),
|
|
146
|
+
name: string(),
|
|
147
|
+
status: _enum([
|
|
148
|
+
"active",
|
|
149
|
+
"inactive",
|
|
150
|
+
"paused"
|
|
151
|
+
]).default("active"),
|
|
152
|
+
runtime: string(),
|
|
153
|
+
description: string().optional(),
|
|
154
|
+
reasoningEffort: string().optional(),
|
|
155
|
+
executionMode: string().optional(),
|
|
156
|
+
fastMode: boolean().optional(),
|
|
157
|
+
runtimeOverrides: object({
|
|
158
|
+
model: string().optional(),
|
|
159
|
+
env: array(object({
|
|
160
|
+
name: string(),
|
|
161
|
+
value: string()
|
|
162
|
+
})).default([])
|
|
163
|
+
}).optional(),
|
|
164
|
+
workspace: object({
|
|
165
|
+
mode: _enum(["git-repo", "from-scratch"]),
|
|
166
|
+
path: string(),
|
|
167
|
+
gitRepo: string().optional(),
|
|
168
|
+
gitBranch: string().default("main"),
|
|
169
|
+
gitCredential: _enum(["github-app"]).optional(),
|
|
170
|
+
pullOnNewSession: boolean().default(true),
|
|
171
|
+
skills: array(string()).default([])
|
|
172
|
+
}),
|
|
173
|
+
integrations: array(IntegrationSchema).default([]),
|
|
174
|
+
output: object({ mode: _enum([
|
|
175
|
+
"low",
|
|
176
|
+
"medium",
|
|
177
|
+
"high"
|
|
178
|
+
]).default("low") }).default({ mode: "low" }),
|
|
179
|
+
permissions: object({
|
|
180
|
+
policy: _enum(["ask", "auto"]).default("ask"),
|
|
181
|
+
autoApprove: array(string()).default([])
|
|
182
|
+
}).default({
|
|
183
|
+
policy: "ask",
|
|
184
|
+
autoApprove: []
|
|
185
|
+
}),
|
|
186
|
+
crons: array(CronDefSchema).default([])
|
|
187
|
+
});
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/agents/agent-env.ts
|
|
190
|
+
function parseDotenv(text) {
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const rawLine of text.split("\n")) {
|
|
193
|
+
const line = rawLine.replace(/\r$/, "").trim();
|
|
194
|
+
if (!line || line.startsWith("#")) continue;
|
|
195
|
+
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
196
|
+
const eq = body.indexOf("=");
|
|
197
|
+
if (eq === -1) continue;
|
|
198
|
+
const key = body.slice(0, eq).trim();
|
|
199
|
+
if (!key) continue;
|
|
200
|
+
let value = body.slice(eq + 1).trim();
|
|
201
|
+
const q = value[0];
|
|
202
|
+
if (value.length >= 2 && (q === "\"" || q === "'") && value.at(-1) === q) value = value.slice(1, -1);
|
|
203
|
+
out[key] = value;
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function loadDotenv(dir) {
|
|
208
|
+
const file = join(dir, ".env");
|
|
209
|
+
if (!existsSync(file)) return {};
|
|
210
|
+
return parseDotenv(readFileSync(file, "utf8"));
|
|
211
|
+
}
|
|
212
|
+
const VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
213
|
+
function interpolateString(value, source, path, ctx) {
|
|
214
|
+
return value.replace(VAR_RE, (_m, name) => {
|
|
215
|
+
const resolved = source[name];
|
|
216
|
+
if (resolved === void 0) throw new Error(`agent "${ctx.id}": unresolved \${${name}} in ${path} (.env: ${ctx.envPath})`);
|
|
217
|
+
return resolved;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function walk(node, source, path, ctx) {
|
|
221
|
+
if (typeof node === "string") return interpolateString(node, source, path, ctx);
|
|
222
|
+
if (Array.isArray(node)) {
|
|
223
|
+
for (let i = 0; i < node.length; i++) node[i] = walk(node[i], source, `${path}[${i}]`, ctx);
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
if (node && typeof node === "object") {
|
|
227
|
+
const obj = node;
|
|
228
|
+
for (const key of Object.keys(obj)) obj[key] = walk(obj[key], source, path ? `${path}.${key}` : key, ctx);
|
|
229
|
+
return node;
|
|
230
|
+
}
|
|
231
|
+
return node;
|
|
232
|
+
}
|
|
233
|
+
function interpolateAgent(agent, source, ctx) {
|
|
234
|
+
walk(agent, source, "", ctx);
|
|
235
|
+
}
|
|
236
|
+
function agentChildEnv(agent) {
|
|
237
|
+
return {
|
|
238
|
+
...agent.env,
|
|
239
|
+
...Object.fromEntries((agent.runtimeOverrides?.env ?? []).map((e) => [e.name, e.value]))
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/agents/load-agents.ts
|
|
244
|
+
const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
245
|
+
const MAX_DEPTH = 4;
|
|
246
|
+
function parseAgentFile(file) {
|
|
247
|
+
const dir = dirname(file);
|
|
248
|
+
const env = loadDotenv(dir);
|
|
249
|
+
const agent = AgentSchema.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
250
|
+
interpolateAgent(agent, {
|
|
251
|
+
...process.env,
|
|
252
|
+
...env
|
|
253
|
+
}, {
|
|
254
|
+
id: agent.id,
|
|
255
|
+
envPath: join(dir, ".env")
|
|
256
|
+
});
|
|
257
|
+
if (!isAbsolute(agent.workspace.path)) agent.workspace.path = resolve(dir, agent.workspace.path);
|
|
258
|
+
return {
|
|
259
|
+
...agent,
|
|
260
|
+
dir,
|
|
261
|
+
env
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function findAgentFiles(dir, depth = 0) {
|
|
265
|
+
if (depth > MAX_DEPTH || !existsSync(dir)) return [];
|
|
266
|
+
let entries;
|
|
267
|
+
try {
|
|
268
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
if (entries.some((e) => e.isFile() && e.name === "agent.json")) return [join(dir, "agent.json")];
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (!entry.isDirectory()) continue;
|
|
276
|
+
if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
277
|
+
out.push(...findAgentFiles(join(dir, entry.name), depth + 1));
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function discoverAgents(agentsDir) {
|
|
282
|
+
return findAgentFiles(agentsDir).map((file) => {
|
|
283
|
+
try {
|
|
284
|
+
return {
|
|
285
|
+
agent: parseAgentFile(file),
|
|
286
|
+
dir: dirname(file)
|
|
287
|
+
};
|
|
288
|
+
} catch (err) {
|
|
289
|
+
throw new Error(`invalid agent.json at ${file}: ${err.message}`);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function loadAgents(agentsDir) {
|
|
294
|
+
return discoverAgents(agentsDir).map((d) => d.agent).filter((a) => a.status === "active");
|
|
295
|
+
}
|
|
296
|
+
function selectAgent(agentsDir, name) {
|
|
297
|
+
const agents = discoverAgents(agentsDir).map((d) => d.agent);
|
|
298
|
+
if (name) {
|
|
299
|
+
const match = agents.find((a) => a.id === name);
|
|
300
|
+
if (!match) {
|
|
301
|
+
const available = agents.map((a) => a.id).sort().join(", ") || "(none)";
|
|
302
|
+
throw new Error(`agent "${name}" not found in ${agentsDir}. Available: ${available}`);
|
|
303
|
+
}
|
|
304
|
+
return match;
|
|
305
|
+
}
|
|
306
|
+
if (agents.length === 0) throw new Error(`no agent.json found in ${agentsDir}`);
|
|
307
|
+
if (agents.length > 1) {
|
|
308
|
+
const ids = agents.map((a) => a.id).sort().join(", ");
|
|
309
|
+
throw new Error(`multiple agents found in ${agentsDir}: ${ids}; use --agent <name> to specify one`);
|
|
310
|
+
}
|
|
311
|
+
return agents[0];
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
export { agentChildEnv as a, ConfigSchema as c, selectAgent as i, findAgentFiles as n, loadConfig as o, loadAgents as r, persistDaemonId as s, discoverAgents as t };
|
|
315
|
+
|
|
316
|
+
//# sourceMappingURL=load-agents-nWFFmN5-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-agents-nWFFmN5-.js","names":["z.object","z.string","z.array","z.literal","z\n .object","z.boolean","z.number","z.record","z.enum","z\n .number","z.discriminatedUnion","z.object","z.literal","z.string","z.array","z\n .object","z.boolean","z.enum"],"sources":["../src/config/config-schema.ts","../src/config/load-config.ts","../src/agents/agent-schema.ts","../src/agents/agent-env.ts","../src/agents/load-agents.ts"],"sourcesContent":["import { z } from 'zod'\n\nexport const RuntimeDefSchema = z.object({\n command: z.string(),\n args: z.array(z.string()).default([]),\n env: z.array(z.object({ name: z.string(), value: z.string() })).default([])\n})\nexport type RuntimeDef = z.infer<typeof RuntimeDefSchema>\n\nexport const ConfigSchema = z.object({\n version: z.literal(1),\n daemonId: z.string().optional(),\n // Base URL of the Web App console; used to build the §9.1 \"details\" deep link\n // (`<webAppUrl>/sessions/<sessionId>`). Unset ⇒ the daemon adopts the URL the CP sends\n // down on `auth/ok` (the CP is authoritative for its own console origin); only truly\n // absent when neither is set. A local config value wins over the CP-provided one.\n webAppUrl: z.string().optional(),\n controlPlane: z\n .object({\n enabled: z.boolean().default(true),\n url: z.string().optional(),\n key: z.string().optional(), // CP API key (opaque); sent as `apiKey` on the auth frame\n heartbeatMs: z.number().int().default(15000)\n })\n .default({ enabled: false, heartbeatMs: 15000 }),\n agentsDir: z.string().optional(), // resolved against root if absent\n runtimes: z.record(z.string(), RuntimeDefSchema).optional(),\n logging: z\n .object({ level: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info') })\n .default({ level: 'info' }),\n limits: z\n .object({\n maxAgents: z.number().int().default(8),\n maxConcurrentSessions: z.number().int().default(32),\n agentIdleTimeoutMs: z.number().int().default(900_000),\n // How often the idle sweep runs: reaps idle ACP adapter children back to\n // `provisioned` (§7.2) and TTL-closes idle sessions (§7.3). Keep well below\n // agentIdleTimeoutMs so a host lingers at most one interval past its window.\n idleSweepMs: z.number().int().default(60_000),\n // SIGTERM/daemon-drain grace window: in-flight turns get this long to finish\n // before the daemon cancels stragglers and tears children down (§2.5/§5.3).\n shutdownDrainMs: z.number().int().default(25_000),\n // §7.3 force-cancel backstop: after `!stop` we send session/cancel and wait\n // this long; if the turn still hasn't yielded, we force-stop the host.\n cancelBackstopMs: z.number().int().default(30_000),\n // How many times to (re)try launching an agent's ACP host — spawn + the\n // `initialize` handshake — before giving up and surfacing the failure to the\n // session. Covers transient failures (a resource race, a slow cold start). A\n // deterministic failure (missing binary) just burns all attempts then reports.\n agentStartAttempts: z.number().int().min(1).default(3),\n // Fixed backoff between agent-start attempts.\n agentStartBackoffMs: z.number().int().min(0).default(500),\n // Cap (bytes) for inlining an inbound attachment into the ACP prompt.\n // Files larger than this are passed as a resource_link pointer, never\n // downloaded/base64'd — bounds daemon RSS and the prompt frame size.\n maxAttachmentBytes: z\n .number()\n .int()\n .default(8 * 1024 * 1024)\n })\n .default({\n maxAgents: 8,\n maxConcurrentSessions: 32,\n agentIdleTimeoutMs: 900_000,\n idleSweepMs: 60_000,\n shutdownDrainMs: 25_000,\n cancelBackstopMs: 30_000,\n agentStartAttempts: 3,\n agentStartBackoffMs: 500,\n maxAttachmentBytes: 8 * 1024 * 1024\n })\n})\nexport type Config = z.infer<typeof ConfigSchema>\n","import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { ConfigSchema, type Config } from './config-schema.js'\nimport { resolveRoot, configPath, defaultAgentsDir } from '../paths.js'\n\nexport interface FlatOverrides {\n cpUrl?: string\n cpKey?: string\n noCp?: boolean\n daemonId?: string\n logLevel?: Config['logging']['level']\n agentsDir?: string\n maxAgents?: number\n}\n\nexport function loadConfig(\n opts: { root?: string; configPath?: string; overrides?: FlatOverrides; optional?: boolean; autoCreate?: boolean } = {}\n): Config {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n // `optional` (used by `chat`) lets the daemon run with zero config: a missing\n // config.json yields the schema defaults, and runtimes fall back to the ACP registry.\n // `autoCreate` (used by `run`) goes a step further and writes that empty config to\n // disk so the daemon runs fully local (control plane disabled by default) and the\n // user has a file to edit later — no `agentconnect login` required.\n let raw: unknown\n if (existsSync(file)) {\n raw = JSON.parse(readFileSync(file, 'utf8'))\n } else if (opts.autoCreate) {\n raw = { version: 1 }\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n } else if (opts.optional) {\n raw = { version: 1 }\n } else {\n throw new Error(`config not found: ${file} (create it, pass --config, or run \\`agentconnect login\\`)`)\n }\n const cfg = ConfigSchema.parse(raw) // throws on invalid\n\n const o = opts.overrides ?? {}\n if (o.daemonId) cfg.daemonId = o.daemonId\n if (o.logLevel) cfg.logging.level = o.logLevel\n if (o.maxAgents !== undefined) cfg.limits.maxAgents = o.maxAgents\n if (o.cpUrl) cfg.controlPlane.url = o.cpUrl\n if (o.cpKey) cfg.controlPlane.key = o.cpKey\n // Passing --cp-url/--cp-key implies \"connect to the CP\" (it defaults off),\n // so the one-line onboarding command works without a config edit. --no-cp wins.\n if (o.cpUrl || o.cpKey) cfg.controlPlane.enabled = true\n if (o.noCp) cfg.controlPlane.enabled = false\n\n cfg.agentsDir = o.agentsDir ?? cfg.agentsDir ?? defaultAgentsDir(root)\n return cfg\n}\n\n/**\n * Persist a (freshly-minted) `daemonId` back into config.json so it is stable\n * per install. Best-effort: a write failure is swallowed (the daemon still runs\n * with the in-memory id this session).\n */\nexport function persistDaemonId(root: string | undefined, daemonId: string): void {\n try {\n const file = configPath(resolveRoot(root))\n const raw = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n raw.daemonId = daemonId\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n } catch {\n // ignore — non-fatal\n }\n}\n","import { z } from 'zod'\n\nexport const BindMatchSchema = z.discriminatedUnion('kind', [\n z.object({ kind: z.literal('mention') }),\n z.object({ kind: z.literal('dm') }),\n z.object({ kind: z.literal('keyword'), value: z.string() }),\n z.object({ kind: z.literal('auto') })\n])\nexport type BindMatch = z.infer<typeof BindMatchSchema>\n\nexport const BindRuleConfigSchema = z.object({\n channel: z.string().optional(), // absent = any channel\n thread: z.string().optional(),\n match: BindMatchSchema\n})\nexport type BindRuleConfig = z.infer<typeof BindRuleConfigSchema>\n\nexport const SlackConfigSchema = z.object({\n botToken: z.string(),\n appToken: z.string(),\n signingSecret: z.string().optional(),\n botUserId: z.string().optional(), // filled at connect via auth.test if absent\n allowedUserIds: z.array(z.string()).default([]),\n bindRules: z.array(BindRuleConfigSchema).default([])\n})\nexport type SlackConfig = z.infer<typeof SlackConfigSchema>\n\nexport const IntegrationSchema = z.object({\n id: z.string(),\n platform: z.literal('slack'),\n slack: SlackConfigSchema\n})\nexport type Integration = z.infer<typeof IntegrationSchema>\n\n/** A scheduled trigger for THIS agent: every `schedule` tick, prompt the agent\n * with `trigger`. `target` is optional output routing — when present the daemon\n * posts the trigger into that channel and the session replies in its thread;\n * absent ⇒ headless fire (no platform output). `origin:\"cp\"` marks CP-pushed\n * entries (written by cron/upsert, pruned by drop.crons); hand-authored entries\n * have no origin and are never touched by the CP. */\nexport const CronDefSchema = z.object({\n id: z.string(),\n schedule: z.string(),\n // integrationId picks which of the agent's integrations posts the anchor;\n // absent (legacy defs) ⇒ the agent's first integration.\n target: z\n .object({ platform: z.literal('slack'), channel: z.string(), integrationId: z.string().optional() })\n .optional(),\n trigger: z.string(),\n enabled: z.boolean().default(true),\n origin: z.literal('cp').optional()\n})\nexport type CronDef = z.infer<typeof CronDefSchema>\n\nexport const AgentSchema = z.object({\n id: z.string(),\n name: z.string(),\n status: z.enum(['active', 'inactive', 'paused']).default('active'),\n runtime: z.string(),\n // System-prompt seed + runtime knobs. Settable in agent.json and overlaid by\n // the CP spec (agent/upsert + register/ok roster) — see cp/cp-agent-registry.ts.\n description: z.string().optional(),\n reasoningEffort: z.string().optional(),\n executionMode: z.string().optional(),\n // Runtime fast mode (ACP `model_config` toggle, claude/codex). Absent ⇒ leave\n // the runtime's own default; the daemon only pushes an explicit on/off.\n fastMode: z.boolean().optional(),\n runtimeOverrides: z\n .object({\n model: z.string().optional(),\n env: z.array(z.object({ name: z.string(), value: z.string() })).default([])\n })\n .optional(),\n workspace: z.object({\n mode: z.enum(['git-repo', 'from-scratch']),\n path: z.string(),\n gitRepo: z.string().optional(), // full cloneable address (e.g. https://github.com/acme/infra)\n gitBranch: z.string().default('main'),\n // Remote-git credential mode. Absent ⇒ anonymous (public repos). 'github-app' ⇒\n // clone/fetch/push authenticate via the local credential helper backed by\n // CP-minted short-lived installation tokens — nothing durable on this host.\n gitCredential: z.enum(['github-app']).optional(),\n pullOnNewSession: z.boolean().default(true),\n skills: z.array(z.string()).default([])\n }),\n integrations: z.array(IntegrationSchema).default([]),\n // zod 4: nested .default({}) does not apply inner field defaults — use explicit full literal\n output: z.object({ mode: z.enum(['low', 'medium', 'high']).default('low') }).default({ mode: 'low' }),\n permissions: z\n .object({ policy: z.enum(['ask', 'auto']).default('ask'), autoApprove: z.array(z.string()).default([]) })\n .default({ policy: 'ask', autoApprove: [] }),\n crons: z.array(CronDefSchema).default([])\n})\nexport type Agent = z.infer<typeof AgentSchema>\n","import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Agent } from './agent-schema.js'\n\n// Minimal .env parser: KEY=VALUE per line, `#` comments, blank lines,\n// optional `export ` prefix, matching single/double quotes stripped.\n// No variable expansion within the file. Last duplicate key wins.\nexport function parseDotenv(text: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const rawLine of text.split('\\n')) {\n const line = rawLine.replace(/\\r$/, '').trim()\n if (!line || line.startsWith('#')) continue\n const body = line.startsWith('export ') ? line.slice(7).trim() : line\n const eq = body.indexOf('=')\n if (eq === -1) continue\n const key = body.slice(0, eq).trim()\n if (!key) continue\n let value = body.slice(eq + 1).trim()\n const q = value[0]\n if (value.length >= 2 && (q === '\"' || q === \"'\") && value.at(-1) === q) {\n value = value.slice(1, -1)\n }\n out[key] = value\n }\n return out\n}\n\n// Parse `<dir>/.env`, or {} if the file does not exist.\nexport function loadDotenv(dir: string): Record<string, string> {\n const file = join(dir, '.env')\n if (!existsSync(file)) return {}\n return parseDotenv(readFileSync(file, 'utf8'))\n}\n\nconst VAR_RE = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g\n\nfunction interpolateString(\n value: string,\n source: Record<string, string | undefined>,\n path: string,\n ctx: { id: string; envPath: string }\n): string {\n return value.replace(VAR_RE, (_m, name: string) => {\n const resolved = source[name]\n if (resolved === undefined) {\n throw new Error(`agent \"${ctx.id}\": unresolved \\${${name}} in ${path} (.env: ${ctx.envPath})`)\n }\n return resolved\n })\n}\n\nfunction walk(\n node: unknown,\n source: Record<string, string | undefined>,\n path: string,\n ctx: { id: string; envPath: string }\n): unknown {\n if (typeof node === 'string') return interpolateString(node, source, path, ctx)\n if (Array.isArray(node)) {\n for (let i = 0; i < node.length; i++) node[i] = walk(node[i], source, `${path}[${i}]`, ctx)\n return node\n }\n if (node && typeof node === 'object') {\n const obj = node as Record<string, unknown>\n for (const key of Object.keys(obj)) {\n obj[key] = walk(obj[key], source, path ? `${path}.${key}` : key, ctx)\n }\n return node\n }\n return node\n}\n\n// Rewrite every ${VAR} in every string of `agent` (in place). Throws on the\n// first unresolved variable.\nexport function interpolateAgent(\n agent: Agent,\n source: Record<string, string | undefined>,\n ctx: { id: string; envPath: string }\n): void {\n walk(agent, source, '', ctx)\n}\n\n// Per-agent env overlay applied on top of runtime.env when spawning the ACP\n// child. .env values, then runtimeOverrides.env (which wins).\nexport function agentChildEnv(agent: {\n env: Record<string, string>\n runtimeOverrides?: { env?: { name: string; value: string }[] }\n}): Record<string, string> {\n return {\n ...agent.env,\n ...Object.fromEntries((agent.runtimeOverrides?.env ?? []).map((e) => [e.name, e.value]))\n }\n}\n","import { readFileSync, readdirSync, existsSync } from 'node:fs'\nimport { join, resolve, isAbsolute, dirname } from 'node:path'\nimport { AgentSchema, type Agent } from './agent-schema.js'\nimport { loadDotenv, interpolateAgent } from './agent-env.js'\n\nconst IGNORED_DIRS = new Set(['node_modules', '.git'])\nconst MAX_DEPTH = 4\n\n// Agent plus loader-derived data: its directory and parsed .env.\nexport type LoadedAgent = Agent & { dir: string; env: Record<string, string> }\n\n// Parse one agent.json, interpolate ${VAR} from the sibling .env (+ process.env),\n// then resolve workspace.path relative to the agent dir.\nfunction parseAgentFile(file: string): LoadedAgent {\n const dir = dirname(file)\n const env = loadDotenv(dir)\n const agent = AgentSchema.parse(JSON.parse(readFileSync(file, 'utf8')))\n interpolateAgent(agent, { ...process.env, ...env }, { id: agent.id, envPath: join(dir, '.env') })\n if (!isAbsolute(agent.workspace.path)) {\n agent.workspace.path = resolve(dir, agent.workspace.path)\n }\n return { ...agent, dir, env }\n}\n\n// Bounded recursive walk: collect every agent.json under `dir`. A directory that\n// contains an agent.json is treated as a leaf (we do not recurse into it), so an\n// agent's own workspace checkout can't masquerade as nested agents.\nexport function findAgentFiles(dir: string, depth = 0): string[] {\n if (depth > MAX_DEPTH || !existsSync(dir)) return []\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return []\n }\n if (entries.some((e) => e.isFile() && e.name === 'agent.json')) {\n return [join(dir, 'agent.json')]\n }\n const out: string[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue\n out.push(...findAgentFiles(join(dir, entry.name), depth + 1))\n }\n return out\n}\n\n// All parsed agents under `agentsDir`, no status filter. `dir` is the directory\n// holding each agent.json.\nexport function discoverAgents(agentsDir: string): { agent: LoadedAgent; dir: string }[] {\n return findAgentFiles(agentsDir).map((file) => {\n try {\n return { agent: parseAgentFile(file), dir: dirname(file) }\n } catch (err) {\n throw new Error(`invalid agent.json at ${file}: ${(err as Error).message}`)\n }\n })\n}\n\n// Active agents only — the daemon's multi-agent path.\nexport function loadAgents(agentsDir: string): LoadedAgent[] {\n return discoverAgents(agentsDir)\n .map((d) => d.agent)\n .filter((a) => a.status === 'active')\n}\n\n// Resolve a single agent from `agentsDir`. With `name`, matches the agent `id`.\n// Without `name`: requires exactly one discovered agent. Used by `chat` and by\n// `run --agent`. Does NOT filter by status.\nexport function selectAgent(agentsDir: string, name?: string): LoadedAgent {\n const agents = discoverAgents(agentsDir).map((d) => d.agent)\n if (name) {\n const match = agents.find((a) => a.id === name)\n if (!match) {\n const available =\n agents\n .map((a) => a.id)\n .sort()\n .join(', ') || '(none)'\n throw new Error(`agent \"${name}\" not found in ${agentsDir}. Available: ${available}`)\n }\n return match\n }\n if (agents.length === 0) throw new Error(`no agent.json found in ${agentsDir}`)\n if (agents.length > 1) {\n const ids = agents\n .map((a) => a.id)\n .sort()\n .join(', ')\n throw new Error(`multiple agents found in ${agentsDir}: ${ids}; use --agent <name> to specify one`)\n }\n return agents[0]!\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { i as lockPath } from "./paths-DEcUO8lC.js";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
//#region src/lock.ts
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a foreground daemon is started while another live daemon already
|
|
7
|
+
* holds the per-root lock. Two daemons sharing one Slack app token each open a
|
|
8
|
+
* Socket Mode connection; Slack round-robins events across connections (it does
|
|
9
|
+
* not broadcast), so each instance receives only a fraction of messages and the
|
|
10
|
+
* rest appear silently dropped — no error, no log on the instance you're watching.
|
|
11
|
+
*/
|
|
12
|
+
var DaemonAlreadyRunningError = class extends Error {
|
|
13
|
+
pid;
|
|
14
|
+
lockFile;
|
|
15
|
+
constructor(pid, lockFile) {
|
|
16
|
+
super(`another agentconnect daemon is already running (pid ${pid}; lock ${lockFile}). Stop it first — two daemons sharing one Slack app token split Socket Mode events between them, so messages appear to be silently dropped.`);
|
|
17
|
+
this.pid = pid;
|
|
18
|
+
this.lockFile = lockFile;
|
|
19
|
+
this.name = "DaemonAlreadyRunningError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** True if a process with `pid` exists. EPERM ⇒ it exists but is owned by another user. */
|
|
23
|
+
function pidAlive(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return err.code === "EPERM";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort single-instance guard for the foreground daemon, keyed by `root`.
|
|
33
|
+
* A lock file holding a live pid blocks (throws DaemonAlreadyRunningError); a
|
|
34
|
+
* stale lock (dead pid, garbage, or absent) is reclaimed. `release()` removes the
|
|
35
|
+
* file only if we still own it. There is a small TOCTOU window — this guards
|
|
36
|
+
* against accidental double-starts (the launchd service + a manual `run`), not a
|
|
37
|
+
* determined concurrent race.
|
|
38
|
+
*
|
|
39
|
+
* Scoped per-root, not per-app-token: the common collision is two daemons sharing
|
|
40
|
+
* one `~/.agentconnect`. Distinct roots that happen to carry the same token would
|
|
41
|
+
* still collide on Slack but not here.
|
|
42
|
+
*/
|
|
43
|
+
function acquireSingletonLock(root, deps = {}) {
|
|
44
|
+
const file = lockPath(root);
|
|
45
|
+
const myPid = deps.pid ?? process.pid;
|
|
46
|
+
try {
|
|
47
|
+
const prev = Number.parseInt(readFileSync(file, "utf8").trim(), 10);
|
|
48
|
+
if (Number.isInteger(prev) && prev !== myPid && pidAlive(prev)) throw new DaemonAlreadyRunningError(prev, file);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof DaemonAlreadyRunningError) throw err;
|
|
51
|
+
}
|
|
52
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
53
|
+
writeFileSync(file, `${myPid}\n`, "utf8");
|
|
54
|
+
let released = false;
|
|
55
|
+
return { release() {
|
|
56
|
+
if (released) return;
|
|
57
|
+
released = true;
|
|
58
|
+
try {
|
|
59
|
+
if (Number.parseInt(readFileSync(file, "utf8").trim(), 10) === myPid) unlinkSync(file);
|
|
60
|
+
} catch {}
|
|
61
|
+
} };
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { DaemonAlreadyRunningError, acquireSingletonLock };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=lock-CA9Bm8_s.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-CA9Bm8_s.js","names":[],"sources":["../src/lock.ts"],"sourcesContent":["import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { lockPath } from './paths.js'\n\n/**\n * Thrown when a foreground daemon is started while another live daemon already\n * holds the per-root lock. Two daemons sharing one Slack app token each open a\n * Socket Mode connection; Slack round-robins events across connections (it does\n * not broadcast), so each instance receives only a fraction of messages and the\n * rest appear silently dropped — no error, no log on the instance you're watching.\n */\nexport class DaemonAlreadyRunningError extends Error {\n constructor(\n readonly pid: number,\n readonly lockFile: string\n ) {\n super(\n `another agentconnect daemon is already running (pid ${pid}; lock ${lockFile}). ` +\n `Stop it first — two daemons sharing one Slack app token split Socket Mode ` +\n `events between them, so messages appear to be silently dropped.`\n )\n this.name = 'DaemonAlreadyRunningError'\n }\n}\n\nexport interface DaemonLock {\n release(): void\n}\n\n/** True if a process with `pid` exists. EPERM ⇒ it exists but is owned by another user. */\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === 'EPERM'\n }\n}\n\n/**\n * Best-effort single-instance guard for the foreground daemon, keyed by `root`.\n * A lock file holding a live pid blocks (throws DaemonAlreadyRunningError); a\n * stale lock (dead pid, garbage, or absent) is reclaimed. `release()` removes the\n * file only if we still own it. There is a small TOCTOU window — this guards\n * against accidental double-starts (the launchd service + a manual `run`), not a\n * determined concurrent race.\n *\n * Scoped per-root, not per-app-token: the common collision is two daemons sharing\n * one `~/.agentconnect`. Distinct roots that happen to carry the same token would\n * still collide on Slack but not here.\n */\nexport function acquireSingletonLock(root: string, deps: { pid?: number } = {}): DaemonLock {\n const file = lockPath(root)\n const myPid = deps.pid ?? process.pid\n try {\n const prev = Number.parseInt(readFileSync(file, 'utf8').trim(), 10)\n if (Number.isInteger(prev) && prev !== myPid && pidAlive(prev)) {\n throw new DaemonAlreadyRunningError(prev, file)\n }\n } catch (err) {\n if (err instanceof DaemonAlreadyRunningError) throw err\n // ENOENT / unreadable / unparseable → no live holder, reclaim below.\n }\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, `${myPid}\\n`, 'utf8')\n\n let released = false\n return {\n release() {\n if (released) return\n released = true\n try {\n if (Number.parseInt(readFileSync(file, 'utf8').trim(), 10) === myPid) unlinkSync(file)\n } catch {\n // already removed, or no longer ours — nothing to do.\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,IAAa,4BAAb,cAA+C,MAAM;CAExC;CACA;CAFX,YACE,KACA,UACA;EACA,MACE,uDAAuD,IAAI,SAAS,SAAS,6IAG/E;EAPS,KAAA,MAAA;EACA,KAAA,WAAA;EAOT,KAAK,OAAO;CACd;AACF;;AAOA,SAAS,SAAS,KAAsB;CACtC,IAAI;EACF,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACT,SAAS,KAAK;EACZ,OAAQ,IAA8B,SAAS;CACjD;AACF;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,MAAc,OAAyB,CAAC,GAAe;CAC1F,MAAM,OAAO,SAAS,IAAI;CAC1B,MAAM,QAAQ,KAAK,OAAO,QAAQ;CAClC,IAAI;EACF,MAAM,OAAO,OAAO,SAAS,aAAa,MAAM,MAAM,CAAC,CAAC,KAAK,GAAG,EAAE;EAClE,IAAI,OAAO,UAAU,IAAI,KAAK,SAAS,SAAS,SAAS,IAAI,GAC3D,MAAM,IAAI,0BAA0B,MAAM,IAAI;CAElD,SAAS,KAAK;EACZ,IAAI,eAAe,2BAA2B,MAAM;CAEtD;CACA,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CAC5C,cAAc,MAAM,GAAG,MAAM,KAAK,MAAM;CAExC,IAAI,WAAW;CACf,OAAO,EACL,UAAU;EACR,IAAI,UAAU;EACd,WAAW;EACX,IAAI;GACF,IAAI,OAAO,SAAS,aAAa,MAAM,MAAM,CAAC,CAAC,KAAK,GAAG,EAAE,MAAM,OAAO,WAAW,IAAI;EACvF,QAAQ,CAER;CACF,EACF;AACF"}
|