neagen 0.1.9 → 0.1.10

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.
@@ -7,8 +7,20 @@
7
7
 
8
8
  const PREVIEW_MAX_LINES = 12;
9
9
  const PREVIEW_MAX_LINE_CHARS = 100;
10
- const BODY_MAX_CHARS = 700;
11
10
  const RULE = "─".repeat(44);
11
+ const TERMINAL_CONTROL = /[\u0000-\u0009\u000B-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/gu;
12
+
13
+ /**
14
+ * Keep line feeds readable while making every terminal/format control an
15
+ * explicit code point. In particular, raw ESC never reaches the terminal, so
16
+ * message content cannot apply ANSI styling or rewrite the approval chrome.
17
+ */
18
+ export function renderTerminalSafeText(value) {
19
+ return String(value).replace(TERMINAL_CONTROL, (character) => {
20
+ const codePoint = character.codePointAt(0).toString(16).toUpperCase().padStart(4, "0");
21
+ return `⟦U+${codePoint}⟧`;
22
+ });
23
+ }
12
24
 
13
25
  function formatBytes(size) {
14
26
  if (!Number.isFinite(size)) return "unknown size";
@@ -24,7 +36,7 @@ function clampLine(line) {
24
36
  }
25
37
 
26
38
  function previewBlock(text, { truncated = false } = {}) {
27
- const lines = String(text).split("\n");
39
+ const lines = renderTerminalSafeText(text).split("\n");
28
40
  const shown = lines.slice(0, PREVIEW_MAX_LINES).map((line) => ` │ ${clampLine(line)}`);
29
41
  const clipped = truncated || lines.length > PREVIEW_MAX_LINES;
30
42
  return [
@@ -34,21 +46,14 @@ function previewBlock(text, { truncated = false } = {}) {
34
46
  ];
35
47
  }
36
48
 
37
- function clampBody(body) {
38
- const text = String(body);
39
- return text.length > BODY_MAX_CHARS
40
- ? { text: `${text.slice(0, BODY_MAX_CHARS)}…`, truncated: true }
41
- : { text, truncated: false };
42
- }
43
-
44
49
  function formatSendFilePrompt(input) {
45
50
  const review = input?.review;
46
51
  if (!review || typeof review !== "object") return undefined;
47
- const to = review.to ?? input?.to ?? "connection";
52
+ const to = renderTerminalSafeText(review.to ?? input?.to ?? "connection");
48
53
  const header = [
49
54
  `Send file to ${to}?`,
50
- ` ${review.filename} · ${review.contentType} · ${formatBytes(review.size)}`,
51
- ` sha256 ${String(review.sha256).slice(0, 16)}…`,
55
+ ` ${renderTerminalSafeText(review.filename)} · ${renderTerminalSafeText(review.contentType)} · ${formatBytes(review.size)}`,
56
+ ` sha256 ${renderTerminalSafeText(review.sha256).slice(0, 16)}…`,
52
57
  ];
53
58
  return [
54
59
  ...header,
@@ -58,23 +63,45 @@ function formatSendFilePrompt(input) {
58
63
  }
59
64
 
60
65
  function formatSendMessagePrompt(input) {
61
- if (typeof input?.body !== "string") return undefined;
62
- const { text, truncated } = clampBody(input.body);
66
+ const review = input?.review;
67
+ if (
68
+ !review ||
69
+ typeof review !== "object" ||
70
+ review.version !== "v1" ||
71
+ typeof review.body !== "string" ||
72
+ typeof review.token !== "string" ||
73
+ !/^[0-9a-f]{64}$/.test(review.token)
74
+ ) return undefined;
63
75
  return [
64
- `Send message${input.to ? ` to ${input.to}` : ""}?`,
65
- ...previewBlock(text, { truncated }),
66
- "Approve to send. Deny to cancel.",
76
+ `Send reviewed message to ${renderTerminalSafeText(review.to ?? "connection")}?`,
77
+ ` ${RULE}`,
78
+ renderTerminalSafeText(review.body),
79
+ ` ${RULE}`,
80
+ ` sha256 ${renderTerminalSafeText(review.bodySha256).slice(0, 16)}…`,
81
+ "Control characters are shown as ⟦U+XXXX⟧.",
82
+ "Approve to send this exact body. Deny to cancel.",
67
83
  ].join("\n");
68
84
  }
69
85
 
70
86
  function formatApproveDraftPrompt(input) {
71
- const lines = [`Approve and send draft${input?.draftId ? ` ${String(input.draftId).slice(0, 8)}…` : ""}?`];
72
- if (typeof input?.body === "string") {
73
- const { text, truncated } = clampBody(input.body);
74
- lines.push(...previewBlock(text, { truncated }), "(revised body shown above)");
75
- }
76
- lines.push("Approve to send. Deny to cancel.");
77
- return lines.join("\n");
87
+ const review = input?.review;
88
+ if (
89
+ !review ||
90
+ typeof review !== "object" ||
91
+ review.version !== "v1" ||
92
+ typeof review.body !== "string" ||
93
+ typeof review.token !== "string" ||
94
+ !/^[0-9a-f]{64}$/.test(review.token)
95
+ ) return undefined;
96
+ return [
97
+ `Send reviewed draft to ${renderTerminalSafeText(review.to ?? "connection")}?`,
98
+ ` ${RULE}`,
99
+ renderTerminalSafeText(review.body),
100
+ ` ${RULE}`,
101
+ ` sha256 ${renderTerminalSafeText(review.bodySha256).slice(0, 16)}…`,
102
+ "Control characters are shown as ⟦U+XXXX⟧.",
103
+ "Approve to send this exact body. Deny to cancel.",
104
+ ].join("\n");
78
105
  }
79
106
 
80
107
  const FORMATTERS = {
@@ -40,10 +40,20 @@ async function loadEveTuiRunner() {
40
40
  }
41
41
 
42
42
  function wakeSyncPrompt(ev) {
43
+ if (ev.kind !== "message" && ev.kind !== "file") {
44
+ return null;
45
+ }
43
46
  const noun = ev.kind === "message" ? "a text message" : "a file";
44
47
  return `[mail] You just received new mail (${noun}) from a connection. Call sync_inbox once to fetch it, then tell me in one short line who it is from and what it is.`;
45
48
  }
46
49
 
50
+ function contactWakeText(ev) {
51
+ if (ev.kind === "contact_request") return "New contact request arrived. Ask Neagen to review contact requests.";
52
+ if (ev.kind === "intro_forward_request") return "New intro forwarding request arrived. Ask Neagen to forward or decline it.";
53
+ if (ev.kind === "contact_accepted") return "A contact request was accepted. Ask Neagen to check contact requests and reveal the accepted card.";
54
+ return null;
55
+ }
56
+
47
57
  function normalizeHost(host) {
48
58
  return host.replace(/\/$/, "");
49
59
  }
@@ -417,12 +427,19 @@ export async function runProductTui({ profile } = {}) {
417
427
  let wake = null;
418
428
  try {
419
429
  wake = await subscribeInboxWakeAuto(identity.ownerId, (ev) => {
430
+ const contactText = contactWakeText(ev);
431
+ if (contactText) {
432
+ process.stdout.write(`\n${contactText}\n`);
433
+ return;
434
+ }
420
435
  const noun = ev.kind === "message" ? "a message" : `a file (${ev.filename ?? "unknown"})`;
421
436
  process.stdout.write(`\nNew mail: ${noun} arrived. Fetching it now...\n`);
422
437
  wakeChain = wakeChain
423
438
  .then(async () => {
439
+ const prompt = wakeSyncPrompt(ev);
440
+ if (!prompt) return;
424
441
  const syncSession = client.session();
425
- const response = await syncSession.send({ message: wakeSyncPrompt(ev) });
442
+ const response = await syncSession.send({ message: prompt });
426
443
  for await (const _event of response) {
427
444
  // drain the sync turn stream silently in the background
428
445
  }
@@ -45,7 +45,7 @@ export function stalledSessionFailed() {
45
45
  data: {
46
46
  error: {
47
47
  code: "turn_stalled",
48
- message: "The connection to your Neagen stalled with no reply for a while. Your message may still be processing, so just send it again.",
48
+ message: "The connection to your Neagen stalled. Check the conversation for a saved reply before retrying.",
49
49
  },
50
50
  },
51
51
  };
@@ -60,7 +60,7 @@ export function droppedSessionFailed() {
60
60
  data: {
61
61
  error: {
62
62
  code: "turn_dropped",
63
- message: "The connection to your Neagen dropped mid-reply. Your message may still be processing, so just try again.",
63
+ message: "The connection to your Neagen dropped. Check the conversation for a saved reply before retrying.",
64
64
  },
65
65
  },
66
66
  };
@@ -69,14 +69,14 @@ export function droppedSessionFailed() {
69
69
  // Synthesized failure for a held boundary (session.completed/waiting) that the
70
70
  // stream never confirmed with a commit event or a `done`. The boundary reached
71
71
  // us, but the turn was cut off before the server finished saving and billing it,
72
- // so we must not show success. The reply may have been lost mid-save.
72
+ // so we must not show success. The server abort guard prevents a late save.
73
73
  export function unconfirmedBoundarySessionFailed() {
74
74
  return {
75
75
  type: "session.failed",
76
76
  data: {
77
77
  error: {
78
78
  code: "turn_unconfirmed",
79
- message: "The connection to your Neagen dropped before the reply was saved. Your Neagen's reply may not have been saved, so ask again to be sure.",
79
+ message: "The connection to your Neagen dropped before confirmation. Check the conversation for a saved reply before retrying.",
80
80
  },
81
81
  },
82
82
  };
@@ -318,4 +318,4 @@ export async function readTurnSseStream(response, onEvent) {
318
318
  return collected;
319
319
  }
320
320
 
321
- export { sseEvent };
321
+ export { sseEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neagen",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Agentic networking, in the terminal. Your Neagen represents you in the network: chat, agent-mail, and files, with passkey login and no passwords.",
5
5
  "type": "module",
6
6
  "bin": {