hey-axi 0.3.1 → 0.3.2
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/README.md +15 -8
- package/package.json +4 -2
- package/skills/hey-axi/SKILL.md +10 -3
- package/skills/hey-axi/references/commands.md +5 -4
- package/src/args.js +35 -6
- package/src/cli.js +37 -13
- package/src/errors.js +66 -14
- package/src/examples.js +12 -4
- package/src/guide.js +3 -1
- package/src/home.js +12 -3
- package/src/hooks.js +33 -6
- package/src/manifest.json +90 -46
- package/src/policy.js +49 -6
- package/src/router.js +11 -0
- package/src/send.js +121 -0
- package/src/version.js +1 -1
package/src/router.js
CHANGED
|
@@ -38,6 +38,17 @@ export function normalizeCatalog(nodes, types = {}) {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Which HEY the manifest covers, in words: "v1.7.0", or for a snapshot of an unreleased
|
|
42
|
+
// branch ("1.7.0+main.9dfe00f") "v1.7.0 plus the commands on HEY main as of 2026-10-06
|
|
43
|
+
// (commit 9dfe00f, unreleased)".
|
|
44
|
+
export function coverageLabel(manifest) {
|
|
45
|
+
const version = String(manifest.hey_version || "unknown");
|
|
46
|
+
const match = version.match(/^(\d+\.\d+\.\d+)\+([\w.-]+)\.([0-9a-f]{7,})$/);
|
|
47
|
+
if (!match) return /^\d/.test(version) ? `v${version}` : version;
|
|
48
|
+
const date = manifest.hey_commit_date ? ` as of ${manifest.hey_commit_date}` : "";
|
|
49
|
+
return `v${match[1]} plus the commands on HEY ${match[2]}${date} (commit ${match[3]}, unreleased)`;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export function loadBundledManifest() {
|
|
42
53
|
return JSON.parse(readFileSync(new URL("./manifest.json", import.meta.url), "utf8"));
|
|
43
54
|
}
|
package/src/send.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Reading what HEY says about outgoing mail.
|
|
2
|
+
//
|
|
3
|
+
// HEY CLI v1.7.0 answers a send with a summary only. HEY's main branch (unreleased as of
|
|
4
|
+
// 2026-10-06, commit 9dfe00f) also:
|
|
5
|
+
// - names what went out: data.id (the message), data.topic_id (its thread), subject,
|
|
6
|
+
// and delayed (true while Undo Send holds it back; the thread doesn't show it yet)
|
|
7
|
+
// - fails a send HEY refused (usually the sending limit) with code not_delivered (exit 7),
|
|
8
|
+
// naming the draft HEY kept in meta.draft_id; sending again only makes another draft
|
|
9
|
+
// - refuses a --to/--cc/--bcc address it would otherwise drop (code usage,
|
|
10
|
+
// "not a valid email address: X") before anything is sent
|
|
11
|
+
// hey-axi reads both shapes. It also refuses clearly broken addresses itself, before HEY
|
|
12
|
+
// runs, so a v1.7.0 HEY can't silently drop a recipient either.
|
|
13
|
+
|
|
14
|
+
// Commands whose recipient flags HEY checks (compose, reply, forward, draft edit).
|
|
15
|
+
export const RECIPIENT_COMMANDS = new Set(["compose", "reply", "forward", "draft edit"]);
|
|
16
|
+
const RECIPIENT_FLAGS = new Set(["--to", "--cc", "--bcc"]);
|
|
17
|
+
|
|
18
|
+
// Split a recipient list on commas outside quotes, comments and angle brackets
|
|
19
|
+
// ("Bryan, Annie" <annie@example.com> is one recipient).
|
|
20
|
+
export function splitAddresses(text) {
|
|
21
|
+
const out = [];
|
|
22
|
+
let current = "";
|
|
23
|
+
let quoted = false;
|
|
24
|
+
let depth = 0;
|
|
25
|
+
let angled = false;
|
|
26
|
+
for (const char of String(text)) {
|
|
27
|
+
if (char === "\"" && depth === 0) quoted = !quoted;
|
|
28
|
+
else if (!quoted && char === "(") depth += 1;
|
|
29
|
+
else if (!quoted && char === ")" && depth > 0) depth -= 1;
|
|
30
|
+
else if (!quoted && depth === 0 && char === "<") angled = true;
|
|
31
|
+
else if (!quoted && depth === 0 && char === ">") angled = false;
|
|
32
|
+
if (char === "," && !quoted && depth === 0 && !angled) {
|
|
33
|
+
if (current.trim()) out.push(current.trim());
|
|
34
|
+
current = "";
|
|
35
|
+
} else current += char;
|
|
36
|
+
}
|
|
37
|
+
if (current.trim()) out.push(current.trim());
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A deliberately loose check: a local part, an @, and a domain with a dot and a top-level
|
|
42
|
+
// part of two or more characters. Anything it refuses, HEY refuses too (or, before its
|
|
43
|
+
// main branch, silently drops); HEY's own, stricter check still runs after it.
|
|
44
|
+
const ADDRESS = /^[^\s@<>()",;]+@[^\s@<>()",;]+\.[^\s@<>()",;.]{2,}$/u;
|
|
45
|
+
|
|
46
|
+
export function invalidAddress(entry) {
|
|
47
|
+
const angle = entry.match(/<([^<>]*)>\s*$/);
|
|
48
|
+
const address = (angle ? angle[1] : entry).trim();
|
|
49
|
+
return ADDRESS.test(address) ? null : address || entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Returns null, or a refusal (exit 2) naming the first address HEY would drop.
|
|
53
|
+
export function recipientProblem(path, values = []) {
|
|
54
|
+
if (!RECIPIENT_COMMANDS.has(path)) return null;
|
|
55
|
+
for (const [name, value] of values) {
|
|
56
|
+
if (!RECIPIENT_FLAGS.has(name)) continue;
|
|
57
|
+
for (const entry of splitAddresses(value)) {
|
|
58
|
+
const bad = invalidAddress(entry);
|
|
59
|
+
if (bad) return recipientRefusal(path, bad);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function recipientRefusal(path, address) {
|
|
66
|
+
return {
|
|
67
|
+
error: `not a valid email address: ${address}`,
|
|
68
|
+
command: path,
|
|
69
|
+
sent: false,
|
|
70
|
+
reason: "HEY would drop this recipient without saying so (no domain, or no top-level domain); nothing was sent",
|
|
71
|
+
hint: `fix or remove "${address}": every recipient needs a full address such as name@example.com`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// HEY's own refusal of a recipient, as its error line words it.
|
|
76
|
+
export const HEY_BAD_ADDRESS = /^not a valid email address: (.+)$/i;
|
|
77
|
+
|
|
78
|
+
// The draft HEY kept when it refused a send: meta.draft_id, else the id in its hint or message.
|
|
79
|
+
export function keptDraftId(envelope) {
|
|
80
|
+
const fromMeta = envelope?.meta?.draft_id;
|
|
81
|
+
if (fromMeta !== undefined && fromMeta !== null && /^\d+$/.test(String(fromMeta))) return String(fromMeta);
|
|
82
|
+
const text = `${envelope?.hint || ""} ${envelope?.error || ""}`;
|
|
83
|
+
const match = text.match(/\bdraft(?: send)? (\d+)\b/i);
|
|
84
|
+
return match ? match[1] : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The failure for a send HEY refused and kept as a draft: never retry the send.
|
|
88
|
+
export function notDelivered(envelope) {
|
|
89
|
+
const draft = keptDraftId(envelope);
|
|
90
|
+
const failure = {
|
|
91
|
+
ok: false,
|
|
92
|
+
kind: "not_delivered",
|
|
93
|
+
sent: false,
|
|
94
|
+
error: draft
|
|
95
|
+
? `not sent: HEY kept the message as draft ${draft} instead of sending it (usually the account's sending limit)`
|
|
96
|
+
: "not sent: HEY kept the message as a draft instead of sending it (usually the account's sending limit)",
|
|
97
|
+
};
|
|
98
|
+
if (draft) failure.draft_id = draft;
|
|
99
|
+
failure.help = draft
|
|
100
|
+
? [
|
|
101
|
+
`Run \`hey-axi draft show ${draft}\` to review it`,
|
|
102
|
+
`Run \`hey-axi draft send ${draft} --allow-send\` later, once the account can send again; don't repeat the send, which only makes another draft`,
|
|
103
|
+
]
|
|
104
|
+
: ["Run `hey-axi draft list` to find the draft HEY kept; don't repeat the send, which only makes another draft"];
|
|
105
|
+
return failure;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Mark a delivered message's result. `data` is HEY's answer: v1.7.0 names nothing; main
|
|
109
|
+
// names id, topic_id, subject and delayed. Nothing is guessed when HEY leaves it out.
|
|
110
|
+
export function markSent(result) {
|
|
111
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
112
|
+
const data = result.data && typeof result.data === "object" && !Array.isArray(result.data) ? result.data : {};
|
|
113
|
+
const marker = { sent: true };
|
|
114
|
+
if (data.delayed === true) {
|
|
115
|
+
// topic_id is the thread it is on (forward's thread_id names the thread forwarded).
|
|
116
|
+
const thread = data.topic_id;
|
|
117
|
+
marker.held = `Undo Send is holding it back: it goes out when HEY releases it, and until then ${thread !== undefined ? `\`hey-axi thread read ${thread}\`` : "the thread"} won't show it (a thread it starts reads as not found)`;
|
|
118
|
+
}
|
|
119
|
+
const { ok, ...rest } = result;
|
|
120
|
+
return ok === undefined ? { ...marker, ...rest } : { ok, ...marker, ...rest };
|
|
121
|
+
}
|
package/src/version.js
CHANGED