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.
- package/lib/approval-format.mjs +51 -24
- package/lib/eve-product-tui.mjs +18 -1
- package/lib/turn-stream.mjs +5 -5
- package/package.json +1 -1
package/lib/approval-format.mjs
CHANGED
|
@@ -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 =
|
|
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 ${
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 = {
|
package/lib/eve-product-tui.mjs
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/lib/turn-stream.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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