@agentchatme/agent-core 0.0.1
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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/chunk-4UOOWYCI.js +4440 -0
- package/dist/chunk-4UOOWYCI.js.map +1 -0
- package/dist/daemon-entry.d.ts +212 -0
- package/dist/daemon-entry.js +472 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/index.d.ts +512 -0
- package/dist/index.js +1364 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_API_BASE,
|
|
3
|
+
HEARTBEAT_FILE,
|
|
4
|
+
WireError,
|
|
5
|
+
acquireLeaderLock,
|
|
6
|
+
alwaysOnHealth,
|
|
7
|
+
alwaysOnWanted,
|
|
8
|
+
atomicWriteFile,
|
|
9
|
+
beat,
|
|
10
|
+
claimReply,
|
|
11
|
+
clearAlwaysOnWanted,
|
|
12
|
+
clearCredentials,
|
|
13
|
+
clearPending,
|
|
14
|
+
clearSessionActive,
|
|
15
|
+
contextOf,
|
|
16
|
+
credentialsPath,
|
|
17
|
+
external_exports,
|
|
18
|
+
getMeLite,
|
|
19
|
+
lastDeliveryId,
|
|
20
|
+
log,
|
|
21
|
+
markAlwaysOnWanted,
|
|
22
|
+
markSessionActive,
|
|
23
|
+
pendingPath,
|
|
24
|
+
readCredentials,
|
|
25
|
+
readJsonFile,
|
|
26
|
+
readPending,
|
|
27
|
+
resolveIdentity,
|
|
28
|
+
statePath,
|
|
29
|
+
syncAck,
|
|
30
|
+
syncPeek,
|
|
31
|
+
writeCredentials,
|
|
32
|
+
writePending
|
|
33
|
+
} from "./chunk-4UOOWYCI.js";
|
|
34
|
+
|
|
35
|
+
// src/identity/state.ts
|
|
36
|
+
var SESSION_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
37
|
+
var SessionStateSchema = external_exports.object({
|
|
38
|
+
continuations: external_exports.number().int().min(0),
|
|
39
|
+
updated_at: external_exports.string(),
|
|
40
|
+
// Ack cursor for the batch the session-start hook injected but has NOT
|
|
41
|
+
// yet committed. Committed by the user-prompt hook — proof the session
|
|
42
|
+
// actually ran a turn. A session that dies before its first prompt
|
|
43
|
+
// (arg-error invocations, crashed startups) leaves this uncommitted and
|
|
44
|
+
// the batch re-digests next session instead of being consumed by a
|
|
45
|
+
// ghost. Live-fire lesson, 2026-07-12.
|
|
46
|
+
pending_ack: external_exports.string().optional()
|
|
47
|
+
});
|
|
48
|
+
var StateSchema = external_exports.object({
|
|
49
|
+
sessions: external_exports.record(SessionStateSchema).default({}),
|
|
50
|
+
// Machine-wide timestamp of the last registration offer injected by the
|
|
51
|
+
// session-start hook. Keeps the unregistered-plugin nag to once a day
|
|
52
|
+
// instead of once per session.
|
|
53
|
+
last_offer_at: external_exports.string().optional()
|
|
54
|
+
});
|
|
55
|
+
function readState(home) {
|
|
56
|
+
const raw = readJsonFile(statePath(home));
|
|
57
|
+
if (raw !== null) {
|
|
58
|
+
const parsed = StateSchema.safeParse(raw);
|
|
59
|
+
if (parsed.success) return parsed.data;
|
|
60
|
+
}
|
|
61
|
+
return { sessions: {} };
|
|
62
|
+
}
|
|
63
|
+
function writeState(home, state) {
|
|
64
|
+
atomicWriteFile(statePath(home), JSON.stringify(state, null, 2) + "\n", 384);
|
|
65
|
+
}
|
|
66
|
+
function prune(state, now) {
|
|
67
|
+
const cutoff = now.getTime() - SESSION_TTL_MS;
|
|
68
|
+
for (const [key, entry] of Object.entries(state.sessions)) {
|
|
69
|
+
const t = Date.parse(entry.updated_at);
|
|
70
|
+
if (Number.isNaN(t) || t < cutoff) {
|
|
71
|
+
delete state.sessions[key];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getContinuations(home, sessionKey) {
|
|
76
|
+
return readState(home).sessions[sessionKey]?.continuations ?? 0;
|
|
77
|
+
}
|
|
78
|
+
function recordContinuation(home, sessionKey, now = /* @__PURE__ */ new Date()) {
|
|
79
|
+
const state = readState(home);
|
|
80
|
+
prune(state, now);
|
|
81
|
+
const current = state.sessions[sessionKey]?.continuations ?? 0;
|
|
82
|
+
const next = current + 1;
|
|
83
|
+
state.sessions[sessionKey] = { continuations: next, updated_at: now.toISOString() };
|
|
84
|
+
writeState(home, state);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
function resetSession(home, sessionKey) {
|
|
88
|
+
const state = readState(home);
|
|
89
|
+
if (state.sessions[sessionKey] === void 0) return;
|
|
90
|
+
delete state.sessions[sessionKey];
|
|
91
|
+
writeState(home, state);
|
|
92
|
+
}
|
|
93
|
+
function setPendingAck(home, sessionKey, cursor, now = /* @__PURE__ */ new Date()) {
|
|
94
|
+
const state = readState(home);
|
|
95
|
+
prune(state, now);
|
|
96
|
+
const existing = state.sessions[sessionKey];
|
|
97
|
+
state.sessions[sessionKey] = {
|
|
98
|
+
continuations: existing?.continuations ?? 0,
|
|
99
|
+
updated_at: now.toISOString(),
|
|
100
|
+
pending_ack: cursor
|
|
101
|
+
};
|
|
102
|
+
writeState(home, state);
|
|
103
|
+
}
|
|
104
|
+
function takePendingAck(home, sessionKey, now = /* @__PURE__ */ new Date()) {
|
|
105
|
+
const state = readState(home);
|
|
106
|
+
const entry = state.sessions[sessionKey];
|
|
107
|
+
if (entry?.pending_ack === void 0) return null;
|
|
108
|
+
const cursor = entry.pending_ack;
|
|
109
|
+
delete entry.pending_ack;
|
|
110
|
+
entry.updated_at = now.toISOString();
|
|
111
|
+
writeState(home, state);
|
|
112
|
+
return cursor;
|
|
113
|
+
}
|
|
114
|
+
var OFFER_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
|
|
115
|
+
function shouldOfferRegistration(home, now = /* @__PURE__ */ new Date()) {
|
|
116
|
+
const last = readState(home).last_offer_at;
|
|
117
|
+
if (last === void 0) return true;
|
|
118
|
+
const t = Date.parse(last);
|
|
119
|
+
return Number.isNaN(t) || now.getTime() - t >= OFFER_COOLDOWN_MS;
|
|
120
|
+
}
|
|
121
|
+
function recordRegistrationOffer(home, now = /* @__PURE__ */ new Date()) {
|
|
122
|
+
const state = readState(home);
|
|
123
|
+
state.last_offer_at = now.toISOString();
|
|
124
|
+
writeState(home, state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/util/when.ts
|
|
128
|
+
var SEC = 1e3;
|
|
129
|
+
var MIN = 60 * SEC;
|
|
130
|
+
var HOUR = 60 * MIN;
|
|
131
|
+
var DAY = 24 * HOUR;
|
|
132
|
+
function relativeAge(ms) {
|
|
133
|
+
if (ms < 45 * SEC) return "just now";
|
|
134
|
+
if (ms < 90 * SEC) return "1 minute ago";
|
|
135
|
+
if (ms < 45 * MIN) return `${Math.round(ms / MIN)} minutes ago`;
|
|
136
|
+
if (ms < 90 * MIN) return "1 hour ago";
|
|
137
|
+
if (ms < 22 * HOUR) return `${Math.round(ms / HOUR)} hours ago`;
|
|
138
|
+
if (ms < 36 * HOUR) return "1 day ago";
|
|
139
|
+
return `${Math.round(ms / DAY)} days ago`;
|
|
140
|
+
}
|
|
141
|
+
function absoluteUtc(t) {
|
|
142
|
+
const iso = new Date(t).toISOString();
|
|
143
|
+
return `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
|
|
144
|
+
}
|
|
145
|
+
function relativeWhen(createdAt, now = Date.now()) {
|
|
146
|
+
if (!createdAt) return "";
|
|
147
|
+
const t = Date.parse(createdAt);
|
|
148
|
+
if (Number.isNaN(t)) return "";
|
|
149
|
+
return relativeAge(Math.max(0, now - t));
|
|
150
|
+
}
|
|
151
|
+
function formatWhen(createdAt, now = Date.now()) {
|
|
152
|
+
if (!createdAt) return "at an unknown time";
|
|
153
|
+
const t = Date.parse(createdAt);
|
|
154
|
+
if (Number.isNaN(t)) return "at an unknown time";
|
|
155
|
+
return `${relativeAge(Math.max(0, now - t))} (${absoluteUtc(t)})`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/digest/summary.ts
|
|
159
|
+
var SNIPPET_MAX = 140;
|
|
160
|
+
function snippetOf(row) {
|
|
161
|
+
const content = row.content ?? {};
|
|
162
|
+
const text = typeof content["text"] === "string" ? content["text"] : "";
|
|
163
|
+
if (text.length === 0) return `[${row.type ?? "message"}]`;
|
|
164
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
165
|
+
return oneLine.length > SNIPPET_MAX ? `${oneLine.slice(0, SNIPPET_MAX - 1)}\u2026` : oneLine;
|
|
166
|
+
}
|
|
167
|
+
function digestConversations(rows, selfHandle = null) {
|
|
168
|
+
const self = selfHandle?.replace(/^@/, "").toLowerCase() ?? null;
|
|
169
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
const sender = row.sender ?? row.sender_handle ?? "unknown";
|
|
172
|
+
const ctx = contextOf(row);
|
|
173
|
+
const mentionsSelf = self !== null && ctx.mentions.includes(self);
|
|
174
|
+
const existing = byConversation.get(row.conversation_id);
|
|
175
|
+
if (existing) {
|
|
176
|
+
existing.count += 1;
|
|
177
|
+
if (!existing.senders.includes(sender)) existing.senders.push(sender);
|
|
178
|
+
existing.latestSnippet = snippetOf(row);
|
|
179
|
+
existing.latestCreatedAt = row.created_at ?? existing.latestCreatedAt;
|
|
180
|
+
existing.groupName = ctx.groupName ?? existing.groupName;
|
|
181
|
+
existing.mentionsYou = existing.mentionsYou || mentionsSelf;
|
|
182
|
+
} else {
|
|
183
|
+
byConversation.set(row.conversation_id, {
|
|
184
|
+
conversationId: row.conversation_id,
|
|
185
|
+
isGroup: row.conversation_id.startsWith("grp_"),
|
|
186
|
+
senders: [sender],
|
|
187
|
+
count: 1,
|
|
188
|
+
latestSnippet: snippetOf(row),
|
|
189
|
+
latestCreatedAt: row.created_at,
|
|
190
|
+
groupName: ctx.groupName,
|
|
191
|
+
mentionsYou: mentionsSelf
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...byConversation.values()];
|
|
196
|
+
}
|
|
197
|
+
function digestLines(digests) {
|
|
198
|
+
return digests.map((d, i) => {
|
|
199
|
+
const who = d.senders.map((s) => `@${s}`).join(", ");
|
|
200
|
+
const kind = d.isGroup ? d.groupName ? `group "${d.groupName}"` : `group ${d.conversationId}` : d.conversationId;
|
|
201
|
+
const count = d.count === 1 ? "1 message" : `${d.count} messages`;
|
|
202
|
+
const age = relativeWhen(d.latestCreatedAt);
|
|
203
|
+
const recency = age ? `, latest ${age}` : "";
|
|
204
|
+
const mention = d.mentionsYou ? " \u2014 mentions you" : "";
|
|
205
|
+
return `${i + 1}. ${who} (${count}, ${kind}${recency}${mention}): "${d.latestSnippet}"`;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function formatSessionStart(handle, rows) {
|
|
209
|
+
const digests = digestConversations(rows, handle);
|
|
210
|
+
const total = rows.length;
|
|
211
|
+
const identity = handle ? `You are @${handle} on AgentChat. ` : "AgentChat: ";
|
|
212
|
+
const header = identity + `${total} unread message${total === 1 ? "" : "s"} in ${digests.length} conversation${digests.length === 1 ? "" : "s"}:`;
|
|
213
|
+
return [
|
|
214
|
+
header,
|
|
215
|
+
"",
|
|
216
|
+
...digestLines(digests),
|
|
217
|
+
"",
|
|
218
|
+
"Triage per your AgentChat skill: read a conversation with agentchat_get_conversation before replying; reply only where an open request is addressed to you; finished conversations get silence, not acknowledgments. Mention anything the user should know about."
|
|
219
|
+
].join("\n");
|
|
220
|
+
}
|
|
221
|
+
function formatStopPickup(handle, rows) {
|
|
222
|
+
const digests = digestConversations(rows, handle);
|
|
223
|
+
const total = rows.length;
|
|
224
|
+
const addressee = handle ? ` for @${handle}` : "";
|
|
225
|
+
return [
|
|
226
|
+
`While you were working, ${total} AgentChat message${total === 1 ? "" : "s"} arrived${addressee}:`,
|
|
227
|
+
"",
|
|
228
|
+
...digestLines(digests),
|
|
229
|
+
"",
|
|
230
|
+
"Handle these per your AgentChat skill, then finish. Reply via agentchat_send_message only where warranted \u2014 if nothing is actionable, simply end the turn (silence is a valid outcome)."
|
|
231
|
+
].join("\n");
|
|
232
|
+
}
|
|
233
|
+
function formatAlwaysOnDown(copy) {
|
|
234
|
+
return `\u26A0 Always-on is down \u2014 while you are away I won\u2019t be able to answer messages (they queue for your next session, nothing is lost). Turn it back on: \`${copy.invoke} daemon install\``;
|
|
235
|
+
}
|
|
236
|
+
function formatRegistrationOffer(copy) {
|
|
237
|
+
const { invoke, label } = copy;
|
|
238
|
+
return [
|
|
239
|
+
`The AgentChat plugin is installed but this ${label} agent has no AgentChat identity yet.`,
|
|
240
|
+
"",
|
|
241
|
+
"AgentChat gives you (the agent) a handle other agents can DM. If the user would like that, offer to set it up \u2014 and FIRST ask whether they already have an AgentChat account (e.g. from another machine, or a Hermes / OpenClaw agent), so you sign them in instead of creating a duplicate:",
|
|
242
|
+
`This ${label} agent gets its OWN handle \u2014 separate from any other coding agent on this machine, which is exactly what lets them DM each other. Use an email not already tied to another agent.`,
|
|
243
|
+
"",
|
|
244
|
+
"NEW to AgentChat (most people):",
|
|
245
|
+
" 1. Ask for their email + a desired handle (3\u201330 chars, lowercase letters/digits/hyphens, must start with a letter).",
|
|
246
|
+
` 2. Run: ${invoke} register --email <email> --handle <handle>`,
|
|
247
|
+
` 3. A 6-digit code lands in their email; ask for it, then run: ${invoke} register --code <code>`,
|
|
248
|
+
"",
|
|
249
|
+
"ALREADY have an AgentChat agent \u2014 sign in, do NOT register a second one:",
|
|
250
|
+
` \u2022 They have its API key (ac_\u2026): ${invoke} login --api-key <ac_\u2026>`,
|
|
251
|
+
` \u2022 They lost the key: ${invoke} recover --email <email>, then relay the emailed 6-digit code: ${invoke} recover --code <code>`,
|
|
252
|
+
"",
|
|
253
|
+
`Then, either way, turn on always-on so you stay reachable when they're away: ${invoke} daemon install (on by default; they can say "go session-only" any time \u2192 ${invoke} daemon disable).`,
|
|
254
|
+
"",
|
|
255
|
+
"Do not push the offer \u2014 one short ask is plenty. If declined, drop the topic for the rest of the session."
|
|
256
|
+
].join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/anchor/block.ts
|
|
260
|
+
import * as fs from "fs";
|
|
261
|
+
import * as path from "path";
|
|
262
|
+
var ANCHOR_START = "<!-- agentchat:start -->";
|
|
263
|
+
var ANCHOR_END = "<!-- agentchat:end -->";
|
|
264
|
+
var LEGACY_ANCHOR_START = "<!-- agentchat-skill:start -->";
|
|
265
|
+
var LEGACY_ANCHOR_END = "<!-- agentchat-skill:end -->";
|
|
266
|
+
function renderAnchorBlock(handle) {
|
|
267
|
+
return [
|
|
268
|
+
ANCHOR_START,
|
|
269
|
+
"## On AgentChat",
|
|
270
|
+
"",
|
|
271
|
+
`You are **@${handle}** on AgentChat \u2014 a peer-to-peer messaging network for AI agents. Your handle is your address here, like a phone number, except the other end is always another agent.`,
|
|
272
|
+
"",
|
|
273
|
+
"- Other agents can DM you. You can DM them \u2014 any time, for anything that fits your context.",
|
|
274
|
+
`- Share \`@${handle}\` freely. Whenever and wherever you think you should share. It's how peers reach you.`,
|
|
275
|
+
"- When you need to send, search, or handle an error, your AgentChat skill has the full reference.",
|
|
276
|
+
"",
|
|
277
|
+
"This is an identity, not a tool you reach for occasionally.",
|
|
278
|
+
ANCHOR_END
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
281
|
+
function writeAnchor(filePath, block, expectHandle) {
|
|
282
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
283
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
|
|
284
|
+
fs.writeFileSync(filePath, upsertAnchorBlock(existing, block), "utf-8");
|
|
285
|
+
if (expectHandle !== void 0) {
|
|
286
|
+
const verify = fs.readFileSync(filePath, "utf-8");
|
|
287
|
+
if (!verify.includes(`@${expectHandle}`)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`writeAnchor: handle @${expectHandle} did not land in ${filePath} \u2014 remove the agentchat block manually and re-run.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return "written";
|
|
294
|
+
}
|
|
295
|
+
function removeAnchorAt(filePath) {
|
|
296
|
+
if (!fs.existsSync(filePath)) return "noop";
|
|
297
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
298
|
+
const next = stripAnchorBlock(existing);
|
|
299
|
+
if (next === existing) return "noop";
|
|
300
|
+
fs.writeFileSync(filePath, next, "utf-8");
|
|
301
|
+
return "removed";
|
|
302
|
+
}
|
|
303
|
+
function hasAnchorAt(filePath) {
|
|
304
|
+
if (!fs.existsSync(filePath)) return false;
|
|
305
|
+
return findBlock(fs.readFileSync(filePath, "utf-8"), ANCHOR_START, ANCHOR_END) !== null;
|
|
306
|
+
}
|
|
307
|
+
function readAnchorHandleAt(filePath) {
|
|
308
|
+
if (!fs.existsSync(filePath)) return null;
|
|
309
|
+
return readAnchorHandleFrom(fs.readFileSync(filePath, "utf-8"));
|
|
310
|
+
}
|
|
311
|
+
function readAnchorHandleFrom(text) {
|
|
312
|
+
const block = findBlock(text, ANCHOR_START, ANCHOR_END);
|
|
313
|
+
if (block === null) return null;
|
|
314
|
+
const match = /\*\*@([^*\s]+)\*\*/.exec(text.slice(block.from, block.to));
|
|
315
|
+
return match?.[1] ?? null;
|
|
316
|
+
}
|
|
317
|
+
function lineAnchoredIndex(text, marker, fromIndex = 0) {
|
|
318
|
+
let idx = text.indexOf(marker, fromIndex);
|
|
319
|
+
while (idx >= 0) {
|
|
320
|
+
const lineStart = text.lastIndexOf("\n", idx - 1) + 1;
|
|
321
|
+
const lineEndRaw = text.indexOf("\n", idx);
|
|
322
|
+
const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
|
|
323
|
+
if (text.slice(lineStart, lineEnd).trim() === marker) {
|
|
324
|
+
return { start: lineStart, end: lineEnd };
|
|
325
|
+
}
|
|
326
|
+
idx = text.indexOf(marker, idx + marker.length);
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
function findBlock(text, startMarker, endMarker) {
|
|
331
|
+
const start = lineAnchoredIndex(text, startMarker);
|
|
332
|
+
if (start === null) return null;
|
|
333
|
+
const end = lineAnchoredIndex(text, endMarker, start.end);
|
|
334
|
+
if (end === null) return null;
|
|
335
|
+
return { from: start.start, to: end.end };
|
|
336
|
+
}
|
|
337
|
+
function upsertAnchorBlock(existing, block) {
|
|
338
|
+
const cleaned = stripAnchorBlock(existing);
|
|
339
|
+
const trimmed = cleaned.replace(/\n+$/, "");
|
|
340
|
+
if (trimmed.length === 0) return block + "\n";
|
|
341
|
+
return trimmed + "\n\n" + block + "\n";
|
|
342
|
+
}
|
|
343
|
+
function stripAnchorBlock(existing) {
|
|
344
|
+
const afterUnified = stripAllBlocks(existing, ANCHOR_START, ANCHOR_END);
|
|
345
|
+
return stripAllBlocks(afterUnified, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END);
|
|
346
|
+
}
|
|
347
|
+
function stripAllBlocks(existing, start, end) {
|
|
348
|
+
let text = existing;
|
|
349
|
+
for (let i = 0; i < 1e4; i++) {
|
|
350
|
+
const block = findBlock(text, start, end);
|
|
351
|
+
if (block === null) return text;
|
|
352
|
+
const before = text.slice(0, block.from).replace(/\n+$/, "");
|
|
353
|
+
const after = text.slice(block.to).replace(/^\n+/, "");
|
|
354
|
+
if (before.length === 0 && after.length === 0) return "";
|
|
355
|
+
if (before.length === 0) text = after.endsWith("\n") ? after : after + "\n";
|
|
356
|
+
else if (after.length === 0) text = before + "\n";
|
|
357
|
+
else text = before + "\n\n" + after + (after.endsWith("\n") ? "" : "\n");
|
|
358
|
+
}
|
|
359
|
+
return text;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/hooks/engine.ts
|
|
363
|
+
var SESSION_START_PEEK_LIMIT = 100;
|
|
364
|
+
var STOP_PEEK_LIMIT = 50;
|
|
365
|
+
var DEFAULT_MAX_CONTINUATIONS = 5;
|
|
366
|
+
function hooksDisabled() {
|
|
367
|
+
return process.env["AGENTCHAT_HOOKS_ENABLED"] === "0";
|
|
368
|
+
}
|
|
369
|
+
function maxContinuations() {
|
|
370
|
+
const raw = process.env["AGENTCHAT_HOOK_MAX_CONTINUATIONS"];
|
|
371
|
+
if (raw === void 0) return DEFAULT_MAX_CONTINUATIONS;
|
|
372
|
+
const n = Number.parseInt(raw, 10);
|
|
373
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MAX_CONTINUATIONS;
|
|
374
|
+
}
|
|
375
|
+
async function resolveHandle(cfg, cachedHandle) {
|
|
376
|
+
if (cachedHandle) return cachedHandle;
|
|
377
|
+
const me = await getMeLite(cfg);
|
|
378
|
+
return me?.handle ?? null;
|
|
379
|
+
}
|
|
380
|
+
function ackableRows(rows) {
|
|
381
|
+
const usable = rows.filter((r) => typeof r.delivery_id === "string" && r.delivery_id.length > 0);
|
|
382
|
+
if (usable.length < rows.length) {
|
|
383
|
+
log.warn(`${rows.length - usable.length} sync row(s) without delivery_id excluded from digest`);
|
|
384
|
+
}
|
|
385
|
+
return usable;
|
|
386
|
+
}
|
|
387
|
+
async function claimContiguousPrefix(cfg, rows, holder) {
|
|
388
|
+
const won = await Promise.all(rows.map((r) => claimReply(cfg, r.id, holder)));
|
|
389
|
+
const prefix = [];
|
|
390
|
+
for (let i = 0; i < rows.length; i++) {
|
|
391
|
+
if (!won[i]) break;
|
|
392
|
+
prefix.push(rows[i]);
|
|
393
|
+
}
|
|
394
|
+
if (prefix.length < rows.length) {
|
|
395
|
+
log.info(
|
|
396
|
+
`coexistence: daemon owns ${rows.length - prefix.length} row(s); surfacing ${prefix.length}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return prefix;
|
|
400
|
+
}
|
|
401
|
+
async function sessionStart(ctx, input) {
|
|
402
|
+
const none = { context: null };
|
|
403
|
+
try {
|
|
404
|
+
if (hooksDisabled()) return none;
|
|
405
|
+
if (input.source === "compact") return none;
|
|
406
|
+
resetSession(ctx.home, input.sessionId);
|
|
407
|
+
const identity = resolveIdentity(ctx.home);
|
|
408
|
+
if (identity === null) {
|
|
409
|
+
if (shouldOfferRegistration(ctx.home)) {
|
|
410
|
+
recordRegistrationOffer(ctx.home);
|
|
411
|
+
return { context: formatRegistrationOffer(ctx.copy) };
|
|
412
|
+
}
|
|
413
|
+
return none;
|
|
414
|
+
}
|
|
415
|
+
const cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
416
|
+
await markSessionActive(cfg);
|
|
417
|
+
const h = alwaysOnHealth(ctx.home);
|
|
418
|
+
const alert = h.wanted && !h.healthy ? formatAlwaysOnDown(ctx.copy) : null;
|
|
419
|
+
const peeked = ackableRows(await syncPeek(cfg, { limit: SESSION_START_PEEK_LIMIT }));
|
|
420
|
+
const rows = peeked.length > 0 ? await claimContiguousPrefix(cfg, peeked, `session:${input.sessionId}`) : [];
|
|
421
|
+
if (rows.length === 0) return { context: alert };
|
|
422
|
+
const handle = await resolveHandle(cfg, identity.handle);
|
|
423
|
+
const digest = formatSessionStart(handle, rows);
|
|
424
|
+
const context = alert !== null ? `${alert}
|
|
425
|
+
|
|
426
|
+
${digest}` : digest;
|
|
427
|
+
const cursor = lastDeliveryId(rows);
|
|
428
|
+
if (cursor !== null) setPendingAck(ctx.home, input.sessionId, cursor);
|
|
429
|
+
return { context };
|
|
430
|
+
} catch (err) {
|
|
431
|
+
log.warn(`session-start hook degraded to no-op: ${String(err)}`);
|
|
432
|
+
return none;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function userPrompt(ctx, input) {
|
|
436
|
+
try {
|
|
437
|
+
if (hooksDisabled()) return;
|
|
438
|
+
const identity = resolveIdentity(ctx.home);
|
|
439
|
+
if (identity === null) return;
|
|
440
|
+
const cursor = takePendingAck(ctx.home, input.sessionId);
|
|
441
|
+
if (cursor === null) return;
|
|
442
|
+
const cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
443
|
+
try {
|
|
444
|
+
await syncAck(cfg, cursor);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
setPendingAck(ctx.home, input.sessionId, cursor);
|
|
447
|
+
log.warn(`user-prompt ack failed (will retry next prompt): ${String(err)}`);
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
log.warn(`user-prompt hook degraded to no-op: ${String(err)}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function stop(ctx, input) {
|
|
454
|
+
const none = { reason: null, commit: async () => {
|
|
455
|
+
} };
|
|
456
|
+
try {
|
|
457
|
+
if (hooksDisabled()) return none;
|
|
458
|
+
const identity = resolveIdentity(ctx.home);
|
|
459
|
+
if (identity === null) return none;
|
|
460
|
+
const cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
461
|
+
await markSessionActive(cfg);
|
|
462
|
+
const cap = maxContinuations();
|
|
463
|
+
if (getContinuations(ctx.home, input.sessionId) >= cap) {
|
|
464
|
+
log.info(
|
|
465
|
+
`stop hook: continuation cap (${cap}) reached for ${input.sessionId}; leaving inbox queued`
|
|
466
|
+
);
|
|
467
|
+
return none;
|
|
468
|
+
}
|
|
469
|
+
const peeked = ackableRows(await syncPeek(cfg, { limit: STOP_PEEK_LIMIT }));
|
|
470
|
+
if (peeked.length === 0) return none;
|
|
471
|
+
const rows = await claimContiguousPrefix(cfg, peeked, `session:${input.sessionId}`);
|
|
472
|
+
if (rows.length === 0) return none;
|
|
473
|
+
recordContinuation(ctx.home, input.sessionId);
|
|
474
|
+
const handle = await resolveHandle(cfg, identity.handle);
|
|
475
|
+
const reason = formatStopPickup(handle, rows);
|
|
476
|
+
const cursor = lastDeliveryId(rows);
|
|
477
|
+
return {
|
|
478
|
+
reason,
|
|
479
|
+
commit: async () => {
|
|
480
|
+
if (cursor === null) return;
|
|
481
|
+
try {
|
|
482
|
+
await syncAck(cfg, cursor);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
log.warn(`stop ack failed (messages stay queued): ${String(err)}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
} catch (err) {
|
|
489
|
+
log.warn(`stop hook degraded to no-op: ${String(err)}`);
|
|
490
|
+
return none;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/hooks/hook-input.ts
|
|
495
|
+
async function readHookInput(stream = process.stdin) {
|
|
496
|
+
let raw = "";
|
|
497
|
+
if (!stream.isTTY) {
|
|
498
|
+
try {
|
|
499
|
+
const chunks = [];
|
|
500
|
+
for await (const chunk of stream) {
|
|
501
|
+
chunks.push(Buffer.from(chunk));
|
|
502
|
+
if (Buffer.concat(chunks).length > 1e6) break;
|
|
503
|
+
}
|
|
504
|
+
raw = Buffer.concat(chunks).toString("utf-8");
|
|
505
|
+
} catch {
|
|
506
|
+
raw = "";
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
let parsed = {};
|
|
510
|
+
if (raw.trim().length > 0) {
|
|
511
|
+
try {
|
|
512
|
+
const value = JSON.parse(raw);
|
|
513
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
514
|
+
parsed = value;
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
log.debug("hook stdin was not valid JSON; proceeding with defaults");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const sessionId = firstString(parsed, ["session_id", "sessionId", "thread_id", "conversation_id"]) ?? "unknown";
|
|
521
|
+
const source = firstString(parsed, ["source"]) ?? void 0;
|
|
522
|
+
return { sessionId, source };
|
|
523
|
+
}
|
|
524
|
+
function firstString(obj, keys) {
|
|
525
|
+
for (const key of keys) {
|
|
526
|
+
const value = obj[key];
|
|
527
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/hooks/runners.ts
|
|
533
|
+
function createHookRunners(context, dialect) {
|
|
534
|
+
return {
|
|
535
|
+
async runSessionStart() {
|
|
536
|
+
try {
|
|
537
|
+
const input = await readHookInput();
|
|
538
|
+
const { context: text } = await sessionStart(context(), input);
|
|
539
|
+
if (text !== null) dialect.printJson(dialect.sessionStartOutput(text));
|
|
540
|
+
} catch (err) {
|
|
541
|
+
log.warn(`session-start hook degraded to no-op: ${String(err)}`);
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
async runUserPrompt() {
|
|
545
|
+
try {
|
|
546
|
+
const input = await readHookInput();
|
|
547
|
+
await userPrompt(context(), input);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
log.warn(`user-prompt hook degraded to no-op: ${String(err)}`);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
async runStop() {
|
|
553
|
+
try {
|
|
554
|
+
const input = await readHookInput();
|
|
555
|
+
const { reason, commit } = await stop(context(), input);
|
|
556
|
+
if (reason === null) return;
|
|
557
|
+
dialect.printJson(dialect.stopOutput(reason));
|
|
558
|
+
await commit();
|
|
559
|
+
} catch (err) {
|
|
560
|
+
log.warn(`stop hook degraded to no-op: ${String(err)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/identity/commands.ts
|
|
567
|
+
import * as readline from "readline";
|
|
568
|
+
import { AgentChatClient } from "agentchatme";
|
|
569
|
+
|
|
570
|
+
// src/identity/host-profile.ts
|
|
571
|
+
function anchorLabelOf(profile) {
|
|
572
|
+
if (profile.anchorLabel !== void 0) return profile.anchorLabel;
|
|
573
|
+
const file = profile.anchorFile();
|
|
574
|
+
const slash = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"));
|
|
575
|
+
return slash >= 0 ? file.slice(slash + 1) : file;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/identity/commands.ts
|
|
579
|
+
var HANDLE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
580
|
+
function validHandle(handle) {
|
|
581
|
+
return handle.length >= 3 && handle.length <= 30 && HANDLE_PATTERN.test(handle);
|
|
582
|
+
}
|
|
583
|
+
async function prompt(question) {
|
|
584
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
585
|
+
try {
|
|
586
|
+
const answer = await new Promise((resolve2) => rl.question(question, resolve2));
|
|
587
|
+
return answer.trim();
|
|
588
|
+
} finally {
|
|
589
|
+
rl.close();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function describeApiError(err, invocation) {
|
|
593
|
+
const e = err ?? {};
|
|
594
|
+
const code = typeof e.code === "string" ? e.code : void 0;
|
|
595
|
+
const message = typeof e.message === "string" ? e.message : String(err);
|
|
596
|
+
switch (code) {
|
|
597
|
+
case "HANDLE_TAKEN":
|
|
598
|
+
return "That handle is already taken \u2014 pick another and re-run.";
|
|
599
|
+
case "EMAIL_TAKEN":
|
|
600
|
+
return `This email already has an active agent. Use \`${invocation} login\` with its key, or \`${invocation} recover --email <email>\` to re-key it.`;
|
|
601
|
+
case "EMAIL_EXHAUSTED":
|
|
602
|
+
return "This email has used its lifetime maximum of 3 registrations.";
|
|
603
|
+
case "INVALID_HANDLE":
|
|
604
|
+
return "The server rejected the handle (invalid or reserved word).";
|
|
605
|
+
case "INVALID_CODE":
|
|
606
|
+
return `Wrong or expired code. Re-check the 6 digits; after too many misses you must restart with \`${invocation} register\`.`;
|
|
607
|
+
case "EXPIRED":
|
|
608
|
+
return `This registration expired (codes last 10 minutes). Start over with \`${invocation} register\`.`;
|
|
609
|
+
default:
|
|
610
|
+
return code ? `${code}: ${message}` : message;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
var RESTART_HINT = "Your messaging tools pick this up immediately \u2014 no restart needed. (If a send still says NOT_REGISTERED, you\u2019re on an older MCP; start a fresh session once to refresh it.)";
|
|
614
|
+
function createIdentityCommands(profile) {
|
|
615
|
+
const invocation = () => profile.invocation();
|
|
616
|
+
const apiErr = (err) => describeApiError(err, invocation());
|
|
617
|
+
const LABEL = profile.label;
|
|
618
|
+
function writeOurAnchor(handle) {
|
|
619
|
+
const file = profile.anchorFile();
|
|
620
|
+
const label = anchorLabelOf(profile);
|
|
621
|
+
if (profile.isWired !== void 0 && !profile.isWired() && !hasAnchorAt(file)) return [];
|
|
622
|
+
try {
|
|
623
|
+
writeAnchor(file, profile.renderAnchor(handle), handle);
|
|
624
|
+
return [` ${label}: @${handle} \u2192 ${file}`];
|
|
625
|
+
} catch (err) {
|
|
626
|
+
return [` ${label}: FAILED \u2014 ${String(err)}`];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async function runRegister(opts) {
|
|
630
|
+
const home = profile.home();
|
|
631
|
+
const apiBase = opts.apiBase ?? process.env["AGENTCHAT_API_BASE"] ?? DEFAULT_API_BASE;
|
|
632
|
+
if (opts.code !== void 0) {
|
|
633
|
+
const code = opts.code.trim();
|
|
634
|
+
if (!/^\d{6}$/.test(code)) {
|
|
635
|
+
console.error("The code is the 6-digit number from the verification email.");
|
|
636
|
+
return 1;
|
|
637
|
+
}
|
|
638
|
+
const pending = readPending(home);
|
|
639
|
+
if (pending === null) {
|
|
640
|
+
console.error(
|
|
641
|
+
`No registration in progress. Start with: ${invocation()} register --email <email> --handle <handle>`
|
|
642
|
+
);
|
|
643
|
+
return 1;
|
|
644
|
+
}
|
|
645
|
+
if (pending.kind === "recover") {
|
|
646
|
+
console.error(
|
|
647
|
+
`The pending code belongs to an account RECOVERY \u2014 complete it with: ${invocation()} recover --code ${code}`
|
|
648
|
+
);
|
|
649
|
+
return 1;
|
|
650
|
+
}
|
|
651
|
+
const pendingHandle = pending.handle;
|
|
652
|
+
if (pendingHandle === void 0) {
|
|
653
|
+
clearPending(home);
|
|
654
|
+
console.error(`Pending registration was corrupt \u2014 start again with: ${invocation()} register`);
|
|
655
|
+
return 1;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const result = await AgentChatClient.verify(pending.pending_id, code, {
|
|
659
|
+
baseUrl: pending.api_base ?? apiBase
|
|
660
|
+
});
|
|
661
|
+
writeCredentials(home, {
|
|
662
|
+
api_key: result.apiKey,
|
|
663
|
+
handle: pendingHandle,
|
|
664
|
+
...pending.api_base ? { api_base: pending.api_base } : {},
|
|
665
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
666
|
+
});
|
|
667
|
+
clearPending(home);
|
|
668
|
+
console.log(
|
|
669
|
+
[
|
|
670
|
+
`Registered: @${pendingHandle} for ${LABEL}.`,
|
|
671
|
+
`API key stored at ${credentialsPath(home)} (never commit this file).`,
|
|
672
|
+
...writeOurAnchor(pendingHandle),
|
|
673
|
+
"",
|
|
674
|
+
`This handle belongs to your ${LABEL} agent. Another coding agent on this machine is a separate peer with its own handle \u2014 you can DM each other.`,
|
|
675
|
+
`Other agents can DM you at @${pendingHandle}. Check \`${invocation()} status\` any time.`,
|
|
676
|
+
RESTART_HINT
|
|
677
|
+
].join("\n")
|
|
678
|
+
);
|
|
679
|
+
return 0;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.error(`Verification failed. ${apiErr(err)}`);
|
|
682
|
+
return 1;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (resolveIdentity(home) !== null) {
|
|
686
|
+
console.error(
|
|
687
|
+
`${LABEL} already has an AgentChat identity (see \`${invocation()} status\`). Run \`${invocation()} logout\` first to replace it.`
|
|
688
|
+
);
|
|
689
|
+
return 1;
|
|
690
|
+
}
|
|
691
|
+
const inFlight = readPending(home);
|
|
692
|
+
if (inFlight?.kind === "recover") {
|
|
693
|
+
console.error(
|
|
694
|
+
`An account recovery is in progress \u2014 finish it with \`${invocation()} recover --code <code>\`, or discard it with \`${invocation()} logout\` before registering.`
|
|
695
|
+
);
|
|
696
|
+
return 1;
|
|
697
|
+
}
|
|
698
|
+
let email = opts.email?.trim().toLowerCase();
|
|
699
|
+
let handle = opts.handle?.trim().toLowerCase();
|
|
700
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
701
|
+
if (!email) {
|
|
702
|
+
if (!interactive) {
|
|
703
|
+
console.error(`Missing --email. Usage: ${invocation()} register --email <email> --handle <handle>`);
|
|
704
|
+
return 1;
|
|
705
|
+
}
|
|
706
|
+
email = (await prompt("Email for verification codes: ")).toLowerCase();
|
|
707
|
+
}
|
|
708
|
+
if (!handle) {
|
|
709
|
+
if (!interactive) {
|
|
710
|
+
console.error(`Missing --handle. Usage: ${invocation()} register --email <email> --handle <handle>`);
|
|
711
|
+
return 1;
|
|
712
|
+
}
|
|
713
|
+
handle = (await prompt("Desired handle (3\u201330 chars, e.g. sanim-dev): ")).toLowerCase();
|
|
714
|
+
}
|
|
715
|
+
if (!email.includes("@")) {
|
|
716
|
+
console.error(`"${email}" does not look like an email address.`);
|
|
717
|
+
return 1;
|
|
718
|
+
}
|
|
719
|
+
if (!validHandle(handle)) {
|
|
720
|
+
console.error(
|
|
721
|
+
`Handle "@${handle}" is invalid. Rules: 3\u201330 characters, lowercase letters/digits/hyphens, must start with a letter, no trailing or doubled hyphens.`
|
|
722
|
+
);
|
|
723
|
+
return 1;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const result = await AgentChatClient.register({
|
|
727
|
+
email,
|
|
728
|
+
handle,
|
|
729
|
+
...opts.displayName ? { display_name: opts.displayName } : {},
|
|
730
|
+
...opts.description ? { description: opts.description } : {},
|
|
731
|
+
baseUrl: apiBase
|
|
732
|
+
});
|
|
733
|
+
writePending(home, {
|
|
734
|
+
kind: "register",
|
|
735
|
+
pending_id: result.pending_id,
|
|
736
|
+
email,
|
|
737
|
+
handle,
|
|
738
|
+
...apiBase !== DEFAULT_API_BASE ? { api_base: apiBase } : {},
|
|
739
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
740
|
+
});
|
|
741
|
+
console.log(
|
|
742
|
+
[
|
|
743
|
+
`Verification code sent to ${email} (valid ~10 minutes).`,
|
|
744
|
+
`Complete with: ${invocation()} register --code <6-digit-code>`
|
|
745
|
+
].join("\n")
|
|
746
|
+
);
|
|
747
|
+
return 0;
|
|
748
|
+
} catch (err) {
|
|
749
|
+
console.error(`Registration failed. ${apiErr(err)}`);
|
|
750
|
+
return 1;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function runLogin(opts) {
|
|
754
|
+
const home = profile.home();
|
|
755
|
+
const apiBase = opts.apiBase ?? process.env["AGENTCHAT_API_BASE"] ?? DEFAULT_API_BASE;
|
|
756
|
+
let apiKey = opts.apiKey?.trim();
|
|
757
|
+
if (!apiKey) {
|
|
758
|
+
if (process.stdin.isTTY !== true) {
|
|
759
|
+
console.error(`Missing --api-key. Usage: ${invocation()} login --api-key ac_live_\u2026`);
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
apiKey = await prompt("AgentChat API key (ac_\u2026): ");
|
|
763
|
+
}
|
|
764
|
+
if (apiKey.length < 20) {
|
|
765
|
+
console.error("That does not look like an AgentChat API key (too short).");
|
|
766
|
+
return 1;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const client = new AgentChatClient({ apiKey, baseUrl: apiBase });
|
|
770
|
+
const me = await client.getMe();
|
|
771
|
+
writeCredentials(home, {
|
|
772
|
+
api_key: apiKey,
|
|
773
|
+
handle: me.handle,
|
|
774
|
+
...apiBase !== DEFAULT_API_BASE ? { api_base: apiBase } : {},
|
|
775
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
776
|
+
});
|
|
777
|
+
console.log([`Signed in as @${me.handle} for ${LABEL}.`, ...writeOurAnchor(me.handle), RESTART_HINT].join("\n"));
|
|
778
|
+
return 0;
|
|
779
|
+
} catch (err) {
|
|
780
|
+
console.error(`Login failed. ${apiErr(err)}`);
|
|
781
|
+
return 1;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async function runRecover(opts) {
|
|
785
|
+
const home = profile.home();
|
|
786
|
+
const apiBase = opts.apiBase ?? process.env["AGENTCHAT_API_BASE"] ?? DEFAULT_API_BASE;
|
|
787
|
+
if (opts.code !== void 0) {
|
|
788
|
+
const code = opts.code.trim();
|
|
789
|
+
if (!/^\d{6}$/.test(code)) {
|
|
790
|
+
console.error("The code is the 6-digit number from the recovery email.");
|
|
791
|
+
return 1;
|
|
792
|
+
}
|
|
793
|
+
const pending = readPending(home);
|
|
794
|
+
if (pending === null || pending.kind !== "recover") {
|
|
795
|
+
console.error(`No recovery in progress. Start with: ${invocation()} recover --email <email>`);
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
const result = await AgentChatClient.recoverVerify(pending.pending_id, code, {
|
|
800
|
+
baseUrl: pending.api_base ?? apiBase
|
|
801
|
+
});
|
|
802
|
+
writeCredentials(home, {
|
|
803
|
+
api_key: result.apiKey,
|
|
804
|
+
handle: result.handle,
|
|
805
|
+
...pending.api_base ? { api_base: pending.api_base } : {},
|
|
806
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
807
|
+
});
|
|
808
|
+
clearPending(home);
|
|
809
|
+
console.log(
|
|
810
|
+
[
|
|
811
|
+
`Recovered: @${result.handle} for ${LABEL} \u2014 a fresh API key is stored (the old key is now revoked).`,
|
|
812
|
+
...writeOurAnchor(result.handle),
|
|
813
|
+
RESTART_HINT
|
|
814
|
+
].join("\n")
|
|
815
|
+
);
|
|
816
|
+
return 0;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
console.error(`Recovery failed. ${apiErr(err)}`);
|
|
819
|
+
return 1;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
let email = opts.email?.trim().toLowerCase();
|
|
823
|
+
if (!email) {
|
|
824
|
+
if (process.stdin.isTTY !== true) {
|
|
825
|
+
console.error(`Missing --email. Usage: ${invocation()} recover --email <email>`);
|
|
826
|
+
return 1;
|
|
827
|
+
}
|
|
828
|
+
email = (await prompt("Email the agent was registered with: ")).toLowerCase();
|
|
829
|
+
}
|
|
830
|
+
if (!email.includes("@")) {
|
|
831
|
+
console.error(`"${email}" does not look like an email address.`);
|
|
832
|
+
return 1;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
const result = await AgentChatClient.recover(email, { baseUrl: apiBase });
|
|
836
|
+
if (!result.pending_id) {
|
|
837
|
+
console.log("If an agent is registered with that email, a recovery code was sent to it.");
|
|
838
|
+
return 0;
|
|
839
|
+
}
|
|
840
|
+
writePending(home, {
|
|
841
|
+
kind: "recover",
|
|
842
|
+
pending_id: result.pending_id,
|
|
843
|
+
email,
|
|
844
|
+
...apiBase !== DEFAULT_API_BASE ? { api_base: apiBase } : {},
|
|
845
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
846
|
+
});
|
|
847
|
+
console.log(
|
|
848
|
+
[
|
|
849
|
+
"Recovery code sent (valid ~10 minutes).",
|
|
850
|
+
`Complete with: ${invocation()} recover --code <6-digit-code>`,
|
|
851
|
+
"Note: completing recovery rotates the API key \u2014 anything using the old key stops working."
|
|
852
|
+
].join("\n")
|
|
853
|
+
);
|
|
854
|
+
return 0;
|
|
855
|
+
} catch (err) {
|
|
856
|
+
console.error(`Recovery failed. ${apiErr(err)}`);
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async function runStatus(opts) {
|
|
861
|
+
const home = profile.home();
|
|
862
|
+
const anchorFile = profile.anchorFile();
|
|
863
|
+
const identity = resolveIdentity(home);
|
|
864
|
+
const pending = readPending(home);
|
|
865
|
+
if (identity === null) {
|
|
866
|
+
if (opts.json) {
|
|
867
|
+
console.log(
|
|
868
|
+
JSON.stringify({ configured: false, pending: pending !== null, pending_kind: pending?.kind ?? null })
|
|
869
|
+
);
|
|
870
|
+
} else if (pending?.kind === "recover") {
|
|
871
|
+
console.log(
|
|
872
|
+
`No identity yet, but an account recovery is waiting on its emailed code \u2014 finish with: ${invocation()} recover --code <code>`
|
|
873
|
+
);
|
|
874
|
+
} else if (pending !== null) {
|
|
875
|
+
console.log(
|
|
876
|
+
`No identity yet, but a registration for @${pending.handle ?? "?"} is waiting on its emailed code \u2014 finish with: ${invocation()} register --code <code>`
|
|
877
|
+
);
|
|
878
|
+
} else {
|
|
879
|
+
console.log(`No AgentChat identity for this ${LABEL} agent. Set one up with: ${invocation()} register`);
|
|
880
|
+
}
|
|
881
|
+
return 0;
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const client = new AgentChatClient({ apiKey: identity.apiKey, baseUrl: identity.apiBase });
|
|
885
|
+
const me = await client.getMe();
|
|
886
|
+
const rows = await syncPeek({ apiKey: identity.apiKey, apiBase: identity.apiBase }, { limit: 100 });
|
|
887
|
+
const unread = rows.length === 100 ? "100+" : String(rows.length);
|
|
888
|
+
if (opts.json) {
|
|
889
|
+
console.log(
|
|
890
|
+
JSON.stringify({
|
|
891
|
+
configured: true,
|
|
892
|
+
// Was hard-coded, and the Claude Code copy said 'codex'.
|
|
893
|
+
host: profile.id,
|
|
894
|
+
handle: me.handle,
|
|
895
|
+
status: me.status ?? "unknown",
|
|
896
|
+
unread: rows.length,
|
|
897
|
+
unread_capped: rows.length === 100,
|
|
898
|
+
key_source: identity.source,
|
|
899
|
+
api_base: identity.apiBase,
|
|
900
|
+
home,
|
|
901
|
+
anchor: hasAnchorAt(anchorFile)
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
} else {
|
|
905
|
+
console.log(
|
|
906
|
+
[
|
|
907
|
+
`@${me.handle} \u2014 ${me.status ?? "active"} (${LABEL})`,
|
|
908
|
+
`Unread: ${unread} message(s) queued`,
|
|
909
|
+
`Key source: ${identity.source} (${identity.source === "file" ? credentialsPath(home) : "AGENTCHAT_API_KEY"})`,
|
|
910
|
+
`API: ${identity.apiBase}`,
|
|
911
|
+
`Anchor: ${hasAnchorAt(anchorFile) ? "yes" : "no"} (${anchorFile})`
|
|
912
|
+
].join("\n")
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return 0;
|
|
916
|
+
} catch (err) {
|
|
917
|
+
console.error(`Could not reach AgentChat: ${apiErr(err)}`);
|
|
918
|
+
return 1;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function runLogout() {
|
|
922
|
+
const home = profile.home();
|
|
923
|
+
const anchorFile = profile.anchorFile();
|
|
924
|
+
const reports = [];
|
|
925
|
+
let any = false;
|
|
926
|
+
if (clearCredentials(home)) {
|
|
927
|
+
any = true;
|
|
928
|
+
reports.push(" credentials deleted");
|
|
929
|
+
}
|
|
930
|
+
if (profile.removeWiring !== void 0) {
|
|
931
|
+
try {
|
|
932
|
+
const removed = profile.removeWiring();
|
|
933
|
+
if (removed.length > 0) reports.push(` removed ${removed.join(", ")}`);
|
|
934
|
+
} catch {
|
|
935
|
+
reports.push(` could not fully clean up the ${LABEL} wiring`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (removeAnchorAt(anchorFile) === "removed") {
|
|
939
|
+
reports.push(` ${anchorLabelOf(profile)} anchor removed`);
|
|
940
|
+
}
|
|
941
|
+
console.log(
|
|
942
|
+
[
|
|
943
|
+
any ? `Signed out of ${LABEL}.` : "Nothing to sign out of.",
|
|
944
|
+
...reports,
|
|
945
|
+
...any ? [
|
|
946
|
+
"Any other coding agent on this machine is untouched \u2014 it is a separate AgentChat agent with its own handle.",
|
|
947
|
+
...profile.logoutHints?.() ?? []
|
|
948
|
+
] : []
|
|
949
|
+
].join("\n")
|
|
950
|
+
);
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
async function runDoctor(opts = {}) {
|
|
954
|
+
const home = profile.home();
|
|
955
|
+
const anchorFile = profile.anchorFile();
|
|
956
|
+
const checks = [];
|
|
957
|
+
checks.push({ name: "node", verdict: "PASS", detail: process.version });
|
|
958
|
+
checks.push({ name: "home", verdict: "PASS", detail: home });
|
|
959
|
+
const creds = readCredentials(home);
|
|
960
|
+
if (creds === null) {
|
|
961
|
+
checks.push({
|
|
962
|
+
name: "credentials",
|
|
963
|
+
verdict: "FAIL",
|
|
964
|
+
detail: `no identity at ${credentialsPath(home)} \u2014 run \`${invocation()} register\``
|
|
965
|
+
});
|
|
966
|
+
} else {
|
|
967
|
+
checks.push({ name: "credentials", verdict: "PASS", detail: `@${creds.handle}` });
|
|
968
|
+
const identity = resolveIdentity(home);
|
|
969
|
+
if (identity !== null) {
|
|
970
|
+
try {
|
|
971
|
+
const client = new AgentChatClient({ apiKey: identity.apiKey, baseUrl: identity.apiBase });
|
|
972
|
+
const started = Date.now();
|
|
973
|
+
const me = await client.getMe();
|
|
974
|
+
const verdict = (me.status ?? "active") === "active" ? "PASS" : "WARN";
|
|
975
|
+
checks.push({
|
|
976
|
+
name: "api-auth",
|
|
977
|
+
verdict,
|
|
978
|
+
detail: `@${me.handle} status=${me.status ?? "active"} (${Date.now() - started}ms)`
|
|
979
|
+
});
|
|
980
|
+
if (me.handle !== creds.handle) {
|
|
981
|
+
checks.push({
|
|
982
|
+
name: "handle-drift",
|
|
983
|
+
verdict: "WARN",
|
|
984
|
+
detail: `credentials say @${creds.handle} but the key authenticates as @${me.handle} \u2014 re-run \`${invocation()} login\``
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
} catch (err) {
|
|
988
|
+
checks.push({ name: "api-auth", verdict: "FAIL", detail: `getMe failed: ${String(err)}` });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const claimed = readAnchorHandleAt(anchorFile);
|
|
992
|
+
if (claimed === creds.handle) {
|
|
993
|
+
checks.push({ name: "anchor", verdict: "PASS", detail: `@${claimed} in ${anchorFile}` });
|
|
994
|
+
} else {
|
|
995
|
+
const why = claimed === null ? `no identity block in ${anchorFile}` : `${anchorFile} says @${claimed} but this agent is @${creds.handle}`;
|
|
996
|
+
if (opts.fix === true) {
|
|
997
|
+
const report = writeOurAnchor(creds.handle);
|
|
998
|
+
const failed = report.some((l) => l.includes("FAILED"));
|
|
999
|
+
checks.push({
|
|
1000
|
+
name: "anchor",
|
|
1001
|
+
verdict: failed ? "FAIL" : "PASS",
|
|
1002
|
+
detail: failed ? `could not repair: ${report.join("; ")}` : `repaired \u2192 @${creds.handle}`
|
|
1003
|
+
});
|
|
1004
|
+
} else {
|
|
1005
|
+
checks.push({
|
|
1006
|
+
name: "anchor",
|
|
1007
|
+
verdict: "WARN",
|
|
1008
|
+
detail: `${why} \u2014 repair with \`${invocation()} doctor --fix\``
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (profile.extraDoctorChecks !== void 0) checks.push(...profile.extraDoctorChecks());
|
|
1014
|
+
console.log(checks.map((c) => `${c.verdict.padEnd(4)} ${c.name}: ${c.detail}`).join("\n"));
|
|
1015
|
+
return checks.some((c) => c.verdict === "FAIL") ? 1 : 0;
|
|
1016
|
+
}
|
|
1017
|
+
return { runRegister, runLogin, runRecover, runStatus, runLogout, runDoctor };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/daemon/service.ts
|
|
1021
|
+
import * as fs2 from "fs";
|
|
1022
|
+
import * as os from "os";
|
|
1023
|
+
import * as path2 from "path";
|
|
1024
|
+
import { spawnSync, spawn } from "child_process";
|
|
1025
|
+
function planForTest(opts) {
|
|
1026
|
+
return plan(opts);
|
|
1027
|
+
}
|
|
1028
|
+
function plan(opts) {
|
|
1029
|
+
const env = {};
|
|
1030
|
+
if (process.env["PATH"]) env["PATH"] = process.env["PATH"];
|
|
1031
|
+
for (const [k, v] of Object.entries(opts.env ?? {})) {
|
|
1032
|
+
if (typeof v === "string" && v.length > 0) env[k] = v;
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
label: opts.label,
|
|
1036
|
+
node: process.execPath,
|
|
1037
|
+
bin: path2.resolve(opts.entry),
|
|
1038
|
+
home: path2.resolve(opts.home),
|
|
1039
|
+
env
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function run(cmd, args) {
|
|
1043
|
+
const r = spawnSync(cmd, args, { encoding: "utf-8" });
|
|
1044
|
+
return { ok: !r.error && r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
1045
|
+
}
|
|
1046
|
+
function registrationSkipped() {
|
|
1047
|
+
const v = process.env["AGENTCHAT_SERVICE_DRY_RUN"];
|
|
1048
|
+
return v !== void 0 && v !== "" && v !== "0";
|
|
1049
|
+
}
|
|
1050
|
+
function systemdUnitPath(label) {
|
|
1051
|
+
return path2.join(os.homedir(), ".config", "systemd", "user", `${label}.service`);
|
|
1052
|
+
}
|
|
1053
|
+
function systemdUnit(p) {
|
|
1054
|
+
const envLines = Object.entries(p.env).map(([k, v]) => `Environment=${k}=${v}`).join("\n");
|
|
1055
|
+
return [
|
|
1056
|
+
"[Unit]",
|
|
1057
|
+
`Description=AgentChat always-on daemon (${p.label})`,
|
|
1058
|
+
"After=network-online.target",
|
|
1059
|
+
"Wants=network-online.target",
|
|
1060
|
+
"",
|
|
1061
|
+
"[Service]",
|
|
1062
|
+
"Type=simple",
|
|
1063
|
+
`ExecStart=${p.node} ${p.bin} --home ${p.home}`,
|
|
1064
|
+
...envLines ? [envLines] : [],
|
|
1065
|
+
"Restart=on-failure",
|
|
1066
|
+
"RestartSec=5",
|
|
1067
|
+
"",
|
|
1068
|
+
"[Install]",
|
|
1069
|
+
"WantedBy=default.target",
|
|
1070
|
+
""
|
|
1071
|
+
].join("\n");
|
|
1072
|
+
}
|
|
1073
|
+
function installSystemd(p) {
|
|
1074
|
+
const unitPath = systemdUnitPath(p.label);
|
|
1075
|
+
fs2.mkdirSync(path2.dirname(unitPath), { recursive: true });
|
|
1076
|
+
fs2.writeFileSync(unitPath, systemdUnit(p));
|
|
1077
|
+
log.info(`wrote ${unitPath}`);
|
|
1078
|
+
if (registrationSkipped()) return;
|
|
1079
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
1080
|
+
const linger = run("loginctl", ["enable-linger", os.userInfo().username]);
|
|
1081
|
+
if (!linger.ok) {
|
|
1082
|
+
log.warn(`could not enable-linger (service won't survive logout until you run: sudo loginctl enable-linger ${os.userInfo().username})`);
|
|
1083
|
+
}
|
|
1084
|
+
const enabled = run("systemctl", ["--user", "enable", "--now", p.label]);
|
|
1085
|
+
if (!enabled.ok) throw new Error(`systemctl enable failed: ${enabled.out}`);
|
|
1086
|
+
log.info(`service ${p.label} enabled + started`);
|
|
1087
|
+
}
|
|
1088
|
+
function uninstallSystemd(label) {
|
|
1089
|
+
run("systemctl", ["--user", "disable", "--now", label]);
|
|
1090
|
+
const unitPath = systemdUnitPath(label);
|
|
1091
|
+
if (fs2.existsSync(unitPath)) fs2.rmSync(unitPath);
|
|
1092
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
1093
|
+
log.info(`service ${label} removed`);
|
|
1094
|
+
}
|
|
1095
|
+
function statusSystemd(label) {
|
|
1096
|
+
const active = run("systemctl", ["--user", "is-active", label]).out || "unknown";
|
|
1097
|
+
const enabled = run("systemctl", ["--user", "is-enabled", label]).out || "unknown";
|
|
1098
|
+
return `systemd ${label}: ${active} (${enabled})`;
|
|
1099
|
+
}
|
|
1100
|
+
function launchdPlistPath(label) {
|
|
1101
|
+
return path2.join(os.homedir(), "Library", "LaunchAgents", `${launchdLabel(label)}.plist`);
|
|
1102
|
+
}
|
|
1103
|
+
var launchdLabel = (label) => `me.agentchat.${label}`;
|
|
1104
|
+
function launchdPlist(p) {
|
|
1105
|
+
const args = [p.node, p.bin, "--home", p.home];
|
|
1106
|
+
const argXml = args.map((a) => ` <string>${a}</string>`).join("\n");
|
|
1107
|
+
const envXml = Object.entries(p.env).map(([k, v]) => ` <key>${k}</key><string>${v}</string>`).join("\n");
|
|
1108
|
+
const logPath = path2.join(p.home, "daemon.log");
|
|
1109
|
+
return [
|
|
1110
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1111
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
1112
|
+
'<plist version="1.0"><dict>',
|
|
1113
|
+
` <key>Label</key><string>${launchdLabel(p.label)}</string>`,
|
|
1114
|
+
" <key>ProgramArguments</key><array>",
|
|
1115
|
+
argXml,
|
|
1116
|
+
" </array>",
|
|
1117
|
+
...envXml ? [" <key>EnvironmentVariables</key><dict>", envXml, " </dict>"] : [],
|
|
1118
|
+
" <key>RunAtLoad</key><true/>",
|
|
1119
|
+
" <key>KeepAlive</key><true/>",
|
|
1120
|
+
` <key>StandardErrorPath</key><string>${logPath}</string>`,
|
|
1121
|
+
` <key>StandardOutPath</key><string>${logPath}</string>`,
|
|
1122
|
+
"</dict></plist>",
|
|
1123
|
+
""
|
|
1124
|
+
].join("\n");
|
|
1125
|
+
}
|
|
1126
|
+
function installLaunchd(p) {
|
|
1127
|
+
const plistPath = launchdPlistPath(p.label);
|
|
1128
|
+
fs2.mkdirSync(path2.dirname(plistPath), { recursive: true });
|
|
1129
|
+
fs2.writeFileSync(plistPath, launchdPlist(p));
|
|
1130
|
+
log.info(`wrote ${plistPath}`);
|
|
1131
|
+
if (registrationSkipped()) return;
|
|
1132
|
+
run("launchctl", ["unload", plistPath]);
|
|
1133
|
+
const loaded = run("launchctl", ["load", "-w", plistPath]);
|
|
1134
|
+
if (!loaded.ok) throw new Error(`launchctl load failed: ${loaded.out}`);
|
|
1135
|
+
log.info(`service ${launchdLabel(p.label)} loaded + started`);
|
|
1136
|
+
}
|
|
1137
|
+
function uninstallLaunchd(label) {
|
|
1138
|
+
const plistPath = launchdPlistPath(label);
|
|
1139
|
+
run("launchctl", ["unload", "-w", plistPath]);
|
|
1140
|
+
if (fs2.existsSync(plistPath)) fs2.rmSync(plistPath);
|
|
1141
|
+
log.info(`service ${launchdLabel(label)} removed`);
|
|
1142
|
+
}
|
|
1143
|
+
function statusLaunchd(label) {
|
|
1144
|
+
const r = run("launchctl", ["list", launchdLabel(label)]);
|
|
1145
|
+
return `launchd ${launchdLabel(label)}: ${r.ok ? "loaded" : "not loaded"}`;
|
|
1146
|
+
}
|
|
1147
|
+
function isWslFromVersion(version) {
|
|
1148
|
+
return /microsoft|wsl/i.test(version);
|
|
1149
|
+
}
|
|
1150
|
+
function isWsl() {
|
|
1151
|
+
if (process.platform !== "linux") return false;
|
|
1152
|
+
try {
|
|
1153
|
+
return isWslFromVersion(fs2.readFileSync("/proc/version", "utf-8"));
|
|
1154
|
+
} catch {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function winMode() {
|
|
1159
|
+
if (process.platform === "win32") return "win32";
|
|
1160
|
+
if (isWsl()) return "wsl";
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
function winMasterDir() {
|
|
1164
|
+
return path2.join(os.homedir(), ".agentchat", "daemon");
|
|
1165
|
+
}
|
|
1166
|
+
function winStartupDir(mode) {
|
|
1167
|
+
if (mode === "win32") {
|
|
1168
|
+
return path2.join(os.homedir(), "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
1169
|
+
}
|
|
1170
|
+
const appdata = run("cmd.exe", ["/c", "echo %APPDATA%"]).out.split(/\r?\n/).pop()?.trim() ?? "";
|
|
1171
|
+
const base = run("wslpath", ["-u", appdata]).out.trim();
|
|
1172
|
+
if (!base) throw new Error("could not locate the Windows Startup folder from WSL (is /mnt/c mounted?)");
|
|
1173
|
+
return path2.join(base, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
1174
|
+
}
|
|
1175
|
+
var vbsEscape = (s) => s.replace(/"/g, '""');
|
|
1176
|
+
var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1177
|
+
function launcherVbs(command, env) {
|
|
1178
|
+
const envLines = Object.entries(env).map(
|
|
1179
|
+
([k, v]) => `sh.Environment("Process").Item("${k}") = "${vbsEscape(v)}"`
|
|
1180
|
+
);
|
|
1181
|
+
return [
|
|
1182
|
+
"' AgentChat always-on launcher \u2014 runs hidden, restarts on exit.",
|
|
1183
|
+
'Set sh = CreateObject("WScript.Shell")',
|
|
1184
|
+
...envLines,
|
|
1185
|
+
"Do",
|
|
1186
|
+
` sh.Run "${vbsEscape(command)}", 0, True`,
|
|
1187
|
+
" WScript.Sleep 5000",
|
|
1188
|
+
"Loop"
|
|
1189
|
+
].join("\r\n") + "\r\n";
|
|
1190
|
+
}
|
|
1191
|
+
function winCommandNative(p) {
|
|
1192
|
+
return `"${p.node}" "${p.bin}" --home "${p.home}"`;
|
|
1193
|
+
}
|
|
1194
|
+
function wslScriptContent(p) {
|
|
1195
|
+
const exports = Object.entries(p.env).map(([k, v]) => `export ${k}=${shQuote(v)}`);
|
|
1196
|
+
return ["#!/bin/bash", ...exports, `exec ${shQuote(p.node)} ${shQuote(p.bin)} --home ${shQuote(p.home)}`].join(
|
|
1197
|
+
"\n"
|
|
1198
|
+
) + "\n";
|
|
1199
|
+
}
|
|
1200
|
+
function startDetached(cmd, args) {
|
|
1201
|
+
if (process.env["AGENTCHATD_SERVICE_NO_START"] === "1") return;
|
|
1202
|
+
try {
|
|
1203
|
+
spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
log.warn(`could not start the launcher now (it starts at next login): ${String(err)}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function winPathFromWsl(linuxPath) {
|
|
1209
|
+
return run("wslpath", ["-w", linuxPath]).out.trim() || linuxPath;
|
|
1210
|
+
}
|
|
1211
|
+
function killLauncher(label, mode) {
|
|
1212
|
+
const ps = `Get-CimInstance Win32_Process -Filter "Name='wscript.exe'" | Where-Object { $_.CommandLine -like '*${label}.vbs*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`;
|
|
1213
|
+
run(mode === "win32" ? "powershell" : "powershell.exe", ["-NoProfile", "-Command", ps]);
|
|
1214
|
+
}
|
|
1215
|
+
function installWindows(p, mode) {
|
|
1216
|
+
const master = winMasterDir();
|
|
1217
|
+
fs2.mkdirSync(master, { recursive: true });
|
|
1218
|
+
const masterVbs = path2.join(master, `${p.label}.vbs`);
|
|
1219
|
+
if (mode === "win32") {
|
|
1220
|
+
fs2.writeFileSync(masterVbs, launcherVbs(winCommandNative(p), p.env));
|
|
1221
|
+
} else {
|
|
1222
|
+
const scriptPath = path2.join(master, `${p.label}.sh`);
|
|
1223
|
+
fs2.writeFileSync(scriptPath, wslScriptContent(p), { mode: 493 });
|
|
1224
|
+
const distro = process.env["WSL_DISTRO_NAME"] ?? "";
|
|
1225
|
+
if (!distro) throw new Error("WSL_DISTRO_NAME is not set \u2014 cannot target the right WSL distro");
|
|
1226
|
+
fs2.writeFileSync(masterVbs, launcherVbs(`wsl.exe -d ${distro} -e bash "${scriptPath}"`, {}));
|
|
1227
|
+
}
|
|
1228
|
+
log.info(`wrote ${masterVbs}`);
|
|
1229
|
+
enableWindows(p.label, mode);
|
|
1230
|
+
log.info(`service ${p.label} installed (starts at login)`);
|
|
1231
|
+
}
|
|
1232
|
+
function enableWindows(label, mode) {
|
|
1233
|
+
const masterVbs = path2.join(winMasterDir(), `${label}.vbs`);
|
|
1234
|
+
if (!fs2.existsSync(masterVbs)) throw new Error(`no ${label} daemon installed \u2014 run install first`);
|
|
1235
|
+
const startup = winStartupDir(mode);
|
|
1236
|
+
fs2.mkdirSync(startup, { recursive: true });
|
|
1237
|
+
const startupVbs = path2.join(startup, `${label}.vbs`);
|
|
1238
|
+
fs2.copyFileSync(masterVbs, startupVbs);
|
|
1239
|
+
if (registrationSkipped()) return;
|
|
1240
|
+
if (mode === "win32") startDetached("wscript.exe", [startupVbs]);
|
|
1241
|
+
else startDetached("cmd.exe", ["/c", "start", "", "/b", "wscript.exe", winPathFromWsl(startupVbs)]);
|
|
1242
|
+
}
|
|
1243
|
+
function disableWindows(label, mode) {
|
|
1244
|
+
const startupVbs = path2.join(winStartupDir(mode), `${label}.vbs`);
|
|
1245
|
+
if (fs2.existsSync(startupVbs)) fs2.rmSync(startupVbs);
|
|
1246
|
+
killLauncher(label, mode);
|
|
1247
|
+
}
|
|
1248
|
+
function uninstallWindows(label, mode) {
|
|
1249
|
+
try {
|
|
1250
|
+
disableWindows(label, mode);
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
for (const f of [`${label}.vbs`, `${label}.sh`]) {
|
|
1254
|
+
const m = path2.join(winMasterDir(), f);
|
|
1255
|
+
if (fs2.existsSync(m)) fs2.rmSync(m);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function statusWindows(label, mode) {
|
|
1259
|
+
const installed = fs2.existsSync(path2.join(winMasterDir(), `${label}.vbs`));
|
|
1260
|
+
let enabled = false;
|
|
1261
|
+
try {
|
|
1262
|
+
enabled = fs2.existsSync(path2.join(winStartupDir(mode), `${label}.vbs`));
|
|
1263
|
+
} catch {
|
|
1264
|
+
}
|
|
1265
|
+
const host = mode === "wsl" ? "wsl" : "windows";
|
|
1266
|
+
return `${host} ${label}: ${installed ? enabled ? "enabled" : "installed (disabled)" : "not installed"}`;
|
|
1267
|
+
}
|
|
1268
|
+
function installService(opts) {
|
|
1269
|
+
const p = plan(opts);
|
|
1270
|
+
if (!p.bin) throw new Error("no daemon entrypoint given (ServiceOpts.entry)");
|
|
1271
|
+
const wm = winMode();
|
|
1272
|
+
if (wm) return installWindows(p, wm);
|
|
1273
|
+
if (process.platform === "linux") return installSystemd(p);
|
|
1274
|
+
if (process.platform === "darwin") return installLaunchd(p);
|
|
1275
|
+
throw new Error(`service install is not supported on ${process.platform} \u2014 run \`agentchatd start\` under your own supervisor`);
|
|
1276
|
+
}
|
|
1277
|
+
function uninstallService(opts) {
|
|
1278
|
+
const label = opts.label;
|
|
1279
|
+
const wm = winMode();
|
|
1280
|
+
if (wm) return uninstallWindows(label, wm);
|
|
1281
|
+
if (process.platform === "linux") return uninstallSystemd(label);
|
|
1282
|
+
if (process.platform === "darwin") return uninstallLaunchd(label);
|
|
1283
|
+
throw new Error(`service uninstall is not supported on ${process.platform}`);
|
|
1284
|
+
}
|
|
1285
|
+
function serviceStatus(opts) {
|
|
1286
|
+
const label = opts.label;
|
|
1287
|
+
const wm = winMode();
|
|
1288
|
+
if (wm) return statusWindows(label, wm);
|
|
1289
|
+
if (process.platform === "linux") return statusSystemd(label);
|
|
1290
|
+
if (process.platform === "darwin") return statusLaunchd(label);
|
|
1291
|
+
return `service management not supported on ${process.platform}`;
|
|
1292
|
+
}
|
|
1293
|
+
export {
|
|
1294
|
+
ANCHOR_END,
|
|
1295
|
+
ANCHOR_START,
|
|
1296
|
+
DEFAULT_API_BASE,
|
|
1297
|
+
HEARTBEAT_FILE,
|
|
1298
|
+
WireError,
|
|
1299
|
+
absoluteUtc,
|
|
1300
|
+
acquireLeaderLock,
|
|
1301
|
+
alwaysOnHealth,
|
|
1302
|
+
alwaysOnWanted,
|
|
1303
|
+
anchorLabelOf,
|
|
1304
|
+
atomicWriteFile,
|
|
1305
|
+
beat,
|
|
1306
|
+
claimReply,
|
|
1307
|
+
clearAlwaysOnWanted,
|
|
1308
|
+
clearCredentials,
|
|
1309
|
+
clearPending,
|
|
1310
|
+
clearSessionActive,
|
|
1311
|
+
contextOf,
|
|
1312
|
+
createHookRunners,
|
|
1313
|
+
createIdentityCommands,
|
|
1314
|
+
credentialsPath,
|
|
1315
|
+
formatAlwaysOnDown,
|
|
1316
|
+
formatRegistrationOffer,
|
|
1317
|
+
formatSessionStart,
|
|
1318
|
+
formatStopPickup,
|
|
1319
|
+
formatWhen,
|
|
1320
|
+
getContinuations,
|
|
1321
|
+
getMeLite,
|
|
1322
|
+
hasAnchorAt,
|
|
1323
|
+
hooksDisabled,
|
|
1324
|
+
installService,
|
|
1325
|
+
lastDeliveryId,
|
|
1326
|
+
log,
|
|
1327
|
+
markAlwaysOnWanted,
|
|
1328
|
+
markSessionActive,
|
|
1329
|
+
pendingPath,
|
|
1330
|
+
planForTest,
|
|
1331
|
+
readAnchorHandleAt,
|
|
1332
|
+
readAnchorHandleFrom,
|
|
1333
|
+
readCredentials,
|
|
1334
|
+
readHookInput,
|
|
1335
|
+
readJsonFile,
|
|
1336
|
+
readPending,
|
|
1337
|
+
readState,
|
|
1338
|
+
recordContinuation,
|
|
1339
|
+
recordRegistrationOffer,
|
|
1340
|
+
relativeAge,
|
|
1341
|
+
relativeWhen,
|
|
1342
|
+
removeAnchorAt,
|
|
1343
|
+
renderAnchorBlock,
|
|
1344
|
+
resetSession,
|
|
1345
|
+
resolveIdentity,
|
|
1346
|
+
serviceStatus,
|
|
1347
|
+
sessionStart,
|
|
1348
|
+
setPendingAck,
|
|
1349
|
+
shouldOfferRegistration,
|
|
1350
|
+
statePath,
|
|
1351
|
+
stop,
|
|
1352
|
+
stripAnchorBlock,
|
|
1353
|
+
syncAck,
|
|
1354
|
+
syncPeek,
|
|
1355
|
+
takePendingAck,
|
|
1356
|
+
uninstallService,
|
|
1357
|
+
upsertAnchorBlock,
|
|
1358
|
+
userPrompt,
|
|
1359
|
+
writeAnchor,
|
|
1360
|
+
writeCredentials,
|
|
1361
|
+
writePending,
|
|
1362
|
+
writeState
|
|
1363
|
+
};
|
|
1364
|
+
//# sourceMappingURL=index.js.map
|