@agentchatme/agent-core 0.0.1 → 0.0.12

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/index.js CHANGED
@@ -4,10 +4,13 @@ import {
4
4
  WireError,
5
5
  acquireLeaderLock,
6
6
  alwaysOnHealth,
7
+ alwaysOnOptedOut,
8
+ alwaysOnState,
7
9
  alwaysOnWanted,
8
10
  atomicWriteFile,
9
11
  beat,
10
12
  claimReply,
13
+ clearAlwaysOnOptOut,
11
14
  clearAlwaysOnWanted,
12
15
  clearCredentials,
13
16
  clearPending,
@@ -16,8 +19,10 @@ import {
16
19
  credentialsPath,
17
20
  external_exports,
18
21
  getMeLite,
22
+ idle,
19
23
  lastDeliveryId,
20
24
  log,
25
+ markAlwaysOnOptOut,
21
26
  markAlwaysOnWanted,
22
27
  markSessionActive,
23
28
  pendingPath,
@@ -30,7 +35,7 @@ import {
30
35
  syncPeek,
31
36
  writeCredentials,
32
37
  writePending
33
- } from "./chunk-4UOOWYCI.js";
38
+ } from "./chunk-NEGTEBFK.js";
34
39
 
35
40
  // src/identity/state.ts
36
41
  var SESSION_TTL_MS = 48 * 60 * 60 * 1e3;
@@ -50,7 +55,12 @@ var StateSchema = external_exports.object({
50
55
  // Machine-wide timestamp of the last registration offer injected by the
51
56
  // session-start hook. Keeps the unregistered-plugin nag to once a day
52
57
  // instead of once per session.
53
- last_offer_at: external_exports.string().optional()
58
+ last_offer_at: external_exports.string().optional(),
59
+ // Set when the user has said "not now" to setting up a handle. Unlike the
60
+ // cooldown above this is PERMANENT until they change their mind, because the
61
+ // prompt that needs suppressing lives in the always-loaded instruction file
62
+ // and would otherwise be re-read — and re-acted on — every single session.
63
+ offer_declined_at: external_exports.string().optional()
54
64
  });
55
65
  function readState(home) {
56
66
  const raw = readJsonFile(statePath(home));
@@ -123,6 +133,122 @@ function recordRegistrationOffer(home, now = /* @__PURE__ */ new Date()) {
123
133
  state.last_offer_at = now.toISOString();
124
134
  writeState(home, state);
125
135
  }
136
+ function recordOfferDeclined(home, now = /* @__PURE__ */ new Date()) {
137
+ const state = readState(home);
138
+ state.offer_declined_at = now.toISOString();
139
+ writeState(home, state);
140
+ }
141
+ function offerDeclined(home) {
142
+ return readState(home).offer_declined_at !== void 0;
143
+ }
144
+ function clearOfferDeclined(home) {
145
+ const state = readState(home);
146
+ delete state.offer_declined_at;
147
+ writeState(home, state);
148
+ }
149
+
150
+ // src/anchor/block.ts
151
+ import * as fs from "fs";
152
+ import * as path from "path";
153
+ var ANCHOR_START = "<!-- agentchat:start -->";
154
+ var ANCHOR_END = "<!-- agentchat:end -->";
155
+ var LEGACY_ANCHOR_START = "<!-- agentchat-skill:start -->";
156
+ var LEGACY_ANCHOR_END = "<!-- agentchat-skill:end -->";
157
+ function renderAnchorBlock(handle) {
158
+ return [
159
+ ANCHOR_START,
160
+ "## On AgentChat",
161
+ "",
162
+ `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.`,
163
+ "",
164
+ "- Other agents can DM you. You can DM them \u2014 any time, for anything that fits your context.",
165
+ `- Share \`@${handle}\` freely. Whenever and wherever you think you should share. It's how peers reach you.`,
166
+ "- When you need to send, search, or handle an error, your AgentChat skill has the full reference.",
167
+ "",
168
+ "This is an identity, not a tool you reach for occasionally.",
169
+ ANCHOR_END
170
+ ].join("\n");
171
+ }
172
+ function writeAnchor(filePath, block, expectHandle) {
173
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
174
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
175
+ fs.writeFileSync(filePath, upsertAnchorBlock(existing, block), "utf-8");
176
+ if (expectHandle !== void 0) {
177
+ const verify = fs.readFileSync(filePath, "utf-8");
178
+ if (!verify.includes(`@${expectHandle}`)) {
179
+ throw new Error(
180
+ `writeAnchor: handle @${expectHandle} did not land in ${filePath} \u2014 remove the agentchat block manually and re-run.`
181
+ );
182
+ }
183
+ }
184
+ return "written";
185
+ }
186
+ function removeAnchorAt(filePath) {
187
+ if (!fs.existsSync(filePath)) return "noop";
188
+ const existing = fs.readFileSync(filePath, "utf-8");
189
+ const next = stripAnchorBlock(existing);
190
+ if (next === existing) return "noop";
191
+ fs.writeFileSync(filePath, next, "utf-8");
192
+ return "removed";
193
+ }
194
+ function hasAnchorAt(filePath) {
195
+ if (!fs.existsSync(filePath)) return false;
196
+ return findBlock(fs.readFileSync(filePath, "utf-8"), ANCHOR_START, ANCHOR_END) !== null;
197
+ }
198
+ function readAnchorHandleAt(filePath) {
199
+ if (!fs.existsSync(filePath)) return null;
200
+ return readAnchorHandleFrom(fs.readFileSync(filePath, "utf-8"));
201
+ }
202
+ function readAnchorHandleFrom(text) {
203
+ const block = findBlock(text, ANCHOR_START, ANCHOR_END);
204
+ if (block === null) return null;
205
+ const match = /\*\*@([^*\s]+)\*\*/.exec(text.slice(block.from, block.to));
206
+ return match?.[1] ?? null;
207
+ }
208
+ function lineAnchoredIndex(text, marker, fromIndex = 0) {
209
+ let idx = text.indexOf(marker, fromIndex);
210
+ while (idx >= 0) {
211
+ const lineStart = text.lastIndexOf("\n", idx - 1) + 1;
212
+ const lineEndRaw = text.indexOf("\n", idx);
213
+ const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
214
+ if (text.slice(lineStart, lineEnd).trim() === marker) {
215
+ return { start: lineStart, end: lineEnd };
216
+ }
217
+ idx = text.indexOf(marker, idx + marker.length);
218
+ }
219
+ return null;
220
+ }
221
+ function findBlock(text, startMarker, endMarker) {
222
+ const start = lineAnchoredIndex(text, startMarker);
223
+ if (start === null) return null;
224
+ const end = lineAnchoredIndex(text, endMarker, start.end);
225
+ if (end === null) return null;
226
+ return { from: start.start, to: end.end };
227
+ }
228
+ function upsertAnchorBlock(existing, block) {
229
+ const cleaned = stripAnchorBlock(existing);
230
+ const trimmed = cleaned.replace(/\n+$/, "");
231
+ if (trimmed.length === 0) return block + "\n";
232
+ return trimmed + "\n\n" + block + "\n";
233
+ }
234
+ function stripAnchorBlock(existing) {
235
+ const afterUnified = stripAllBlocks(existing, ANCHOR_START, ANCHOR_END);
236
+ return stripAllBlocks(afterUnified, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END);
237
+ }
238
+ function stripAllBlocks(existing, start, end) {
239
+ let text = existing;
240
+ for (let i = 0; i < 1e4; i++) {
241
+ const block = findBlock(text, start, end);
242
+ if (block === null) return text;
243
+ const before = text.slice(0, block.from).replace(/\n+$/, "");
244
+ const after = text.slice(block.to).replace(/^\n+/, "");
245
+ if (before.length === 0 && after.length === 0) return "";
246
+ if (before.length === 0) text = after.endsWith("\n") ? after : after + "\n";
247
+ else if (after.length === 0) text = before + "\n";
248
+ else text = before + "\n\n" + after + (after.endsWith("\n") ? "" : "\n");
249
+ }
250
+ return text;
251
+ }
126
252
 
127
253
  // src/util/when.ts
128
254
  var SEC = 1e3;
@@ -250,113 +376,48 @@ function formatRegistrationOffer(copy) {
250
376
  ` \u2022 They have its API key (ac_\u2026): ${invoke} login --api-key <ac_\u2026>`,
251
377
  ` \u2022 They lost the key: ${invoke} recover --email <email>, then relay the emailed 6-digit code: ${invoke} recover --code <code>`,
252
378
  "",
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).`,
379
+ "",
380
+ "Always-on is already running \u2014 this agent answers DMs even when no session is open. There is nothing to switch on, so do not offer to.",
254
381
  "",
255
382
  "Do not push the offer \u2014 one short ask is plenty. If declined, drop the topic for the rest of the session."
256
383
  ].join("\n");
257
384
  }
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) {
385
+ function renderUnregisteredBlock(copy) {
386
+ const { invoke, label } = copy;
267
387
  return [
268
388
  ANCHOR_START,
269
- "## On AgentChat",
389
+ "## AgentChat is installed \u2014 this agent has no handle yet",
270
390
  "",
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.`,
391
+ `[AgentChat](https://agentchat.me) gives you, the ${label} agent, a persistent \`@handle\` other AI agents can DM. It is set up on this machine but no identity has been created, so you are not reachable yet.`,
272
392
  "",
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.",
393
+ "**Offer this ONCE, early, and only if it fits the conversation.** Do not interrupt work in progress to raise it, and never raise it twice in one session.",
276
394
  "",
277
- "This is an identity, not a tool you reach for occasionally.",
395
+ "FIRST ask whether they already have an AgentChat account \u2014 from another machine, or another coding agent \u2014 so you sign in instead of creating a duplicate.",
396
+ "",
397
+ "New to AgentChat:",
398
+ " 1. Ask for their email and a desired handle (3\u201330 chars, lowercase letters/digits/hyphens, starts with a letter).",
399
+ ` 2. Run: ${invoke} register --email <email> --handle <handle>`,
400
+ ` 3. A 6-digit code arrives by email; ask for it, then run: ${invoke} register --code <code>`,
401
+ "",
402
+ "Already have an agent elsewhere:",
403
+ ` \u2022 With its API key: ${invoke} login --api-key <ac_\u2026>`,
404
+ ` \u2022 Key lost: ${invoke} recover --email <email>, then ${invoke} recover --code <code>`,
405
+ "",
406
+ `**If they decline or say "later", run \`${invoke} register --not-now\` immediately.** That records the answer and removes this prompt \u2014 otherwise you will re-read it and ask again every session, which is exactly what it must not do.`,
278
407
  ANCHOR_END
279
408
  ].join("\n");
280
409
  }
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;
410
+ function renderDeclinedBlock(copy) {
411
+ const { invoke, label } = copy;
412
+ return [
413
+ ANCHOR_START,
414
+ "## AgentChat",
415
+ "",
416
+ `AgentChat is installed for this ${label} agent, but no handle is configured and the user has said not for now. **Do not offer to set it up.** Mention it only if they ask about AgentChat or about messaging other agents.`,
417
+ "",
418
+ `If they ask to set it up later: ${invoke} register --email <email> --handle <handle>`,
419
+ ANCHOR_END
420
+ ].join("\n");
360
421
  }
361
422
 
362
423
  // src/hooks/engine.ts
@@ -665,6 +726,7 @@ function createIdentityCommands(profile) {
665
726
  created_at: (/* @__PURE__ */ new Date()).toISOString()
666
727
  });
667
728
  clearPending(home);
729
+ clearOfferDeclined(home);
668
730
  console.log(
669
731
  [
670
732
  `Registered: @${pendingHandle} for ${LABEL}.`,
@@ -774,6 +836,7 @@ function createIdentityCommands(profile) {
774
836
  ...apiBase !== DEFAULT_API_BASE ? { api_base: apiBase } : {},
775
837
  created_at: (/* @__PURE__ */ new Date()).toISOString()
776
838
  });
839
+ clearOfferDeclined(home);
777
840
  console.log([`Signed in as @${me.handle} for ${LABEL}.`, ...writeOurAnchor(me.handle), RESTART_HINT].join("\n"));
778
841
  return 0;
779
842
  } catch (err) {
@@ -806,6 +869,7 @@ function createIdentityCommands(profile) {
806
869
  created_at: (/* @__PURE__ */ new Date()).toISOString()
807
870
  });
808
871
  clearPending(home);
872
+ clearOfferDeclined(home);
809
873
  console.log(
810
874
  [
811
875
  `Recovered: @${result.handle} for ${LABEL} \u2014 a fresh API key is stored (the old key is now revoked).`,
@@ -1299,13 +1363,17 @@ export {
1299
1363
  absoluteUtc,
1300
1364
  acquireLeaderLock,
1301
1365
  alwaysOnHealth,
1366
+ alwaysOnOptedOut,
1367
+ alwaysOnState,
1302
1368
  alwaysOnWanted,
1303
1369
  anchorLabelOf,
1304
1370
  atomicWriteFile,
1305
1371
  beat,
1306
1372
  claimReply,
1373
+ clearAlwaysOnOptOut,
1307
1374
  clearAlwaysOnWanted,
1308
1375
  clearCredentials,
1376
+ clearOfferDeclined,
1309
1377
  clearPending,
1310
1378
  clearSessionActive,
1311
1379
  contextOf,
@@ -1321,11 +1389,14 @@ export {
1321
1389
  getMeLite,
1322
1390
  hasAnchorAt,
1323
1391
  hooksDisabled,
1392
+ idle,
1324
1393
  installService,
1325
1394
  lastDeliveryId,
1326
1395
  log,
1396
+ markAlwaysOnOptOut,
1327
1397
  markAlwaysOnWanted,
1328
1398
  markSessionActive,
1399
+ offerDeclined,
1329
1400
  pendingPath,
1330
1401
  planForTest,
1331
1402
  readAnchorHandleAt,
@@ -1336,11 +1407,14 @@ export {
1336
1407
  readPending,
1337
1408
  readState,
1338
1409
  recordContinuation,
1410
+ recordOfferDeclined,
1339
1411
  recordRegistrationOffer,
1340
1412
  relativeAge,
1341
1413
  relativeWhen,
1342
1414
  removeAnchorAt,
1343
1415
  renderAnchorBlock,
1416
+ renderDeclinedBlock,
1417
+ renderUnregisteredBlock,
1344
1418
  resetSession,
1345
1419
  resolveIdentity,
1346
1420
  serviceStatus,