hey-axi 0.2.0 → 0.3.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/README.md +31 -15
- package/package.json +1 -1
- package/skills/hey-axi/SKILL.md +48 -45
- package/skills/hey-axi/references/commands.md +39 -36
- package/src/activity.js +160 -0
- package/src/args.js +75 -10
- package/src/arity.js +94 -0
- package/src/cli.js +373 -136
- package/src/errors.js +128 -31
- package/src/examples.js +57 -0
- package/src/guide.js +21 -2
- package/src/home-path.js +12 -0
- package/src/home.js +48 -16
- package/src/hooks.js +176 -23
- package/src/manifest.json +699 -251
- package/src/policy.js +154 -11
- package/src/router.js +45 -11
- package/src/scope.js +177 -0
- package/src/shape.js +197 -56
- package/src/text.js +3 -3
- package/src/version.js +1 -1
package/src/errors.js
CHANGED
|
@@ -1,28 +1,108 @@
|
|
|
1
|
-
// Turn a failed HEY run into hey-axi's error shape
|
|
2
|
-
// ({ok:false,error,code,hint,meta}
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Turn a failed HEY run into hey-axi's error shape (AXI principle 6):
|
|
2
|
+
// - HEY's own error envelope ({ok:false,error,code,hint,meta}) is kept, with hints
|
|
3
|
+
// rewritten to name hey-axi commands
|
|
4
|
+
// - anything else is translated: one cleaned line of HEY's text (no warnings, stack
|
|
5
|
+
// frames or terminal escapes) under a hey-axi message for the failure kind
|
|
6
|
+
// - plain warning lines HEY prints first (e.g. about the keyring) go to stderr, not stdout
|
|
7
|
+
// - exit codes follow AXI: 2 for usage errors, 1 for everything else that failed
|
|
8
|
+
// A `help` line always says what to do next.
|
|
6
9
|
|
|
7
10
|
import { heyToAxi } from "./text.js";
|
|
8
11
|
|
|
9
|
-
// HEY's exit codes (`hey help exit-codes`) → the next step
|
|
10
|
-
const
|
|
11
|
-
1: "Run `hey-axi <command> --help` to check the arguments",
|
|
12
|
-
2: "Check the id; list commands such as `hey-axi box view imbox` show valid ids (threads use topic_id)",
|
|
13
|
-
3: "
|
|
14
|
-
4: "
|
|
15
|
-
5: "
|
|
16
|
-
6: "
|
|
17
|
-
7: "HEY's API returned an error:
|
|
18
|
-
8: "
|
|
19
|
-
127: "Install the HEY CLI (curl -fsSL https://hey.com/install-cli | bash) or set HEY_BIN",
|
|
12
|
+
// HEY's exit codes (`hey help exit-codes`) → a kind, a message and the next step.
|
|
13
|
+
const KINDS = {
|
|
14
|
+
1: { kind: "command_error", message: "the command failed", help: "Run `hey-axi <command> --help` to check the arguments" },
|
|
15
|
+
2: { kind: "not_found", message: "not found", help: "Check the id; list commands such as `hey-axi box view imbox` show valid ids (threads use topic_id)" },
|
|
16
|
+
3: { kind: "auth", message: "not signed in to HEY", help: "Run `hey-axi auth login --token <token>` if you have a token; otherwise ask the user to run `hey-axi auth login --interactive` in their terminal. Check with `hey-axi auth status`" },
|
|
17
|
+
4: { kind: "forbidden", message: "this account can't do that", help: "Check `hey-axi account list` and --account" },
|
|
18
|
+
5: { kind: "rate_limited", message: "rate limited by HEY", help: "Wait a minute, then retry" },
|
|
19
|
+
6: { kind: "network", message: "couldn't reach HEY", help: "Check the network connection, then retry" },
|
|
20
|
+
7: { kind: "api_error", message: "HEY's API returned an error", help: "Retry later, or run `hey-axi doctor`" },
|
|
21
|
+
8: { kind: "ambiguous", message: "more than one match", help: "Pass an exact id instead of a name" },
|
|
22
|
+
127: { kind: "hey_missing", message: "HEY CLI not found", help: "Install the HEY CLI (curl -fsSL https://hey.com/install-cli | bash) or set HEY_BIN" },
|
|
20
23
|
};
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const USAGE_CODES = new Set(["usage", "invalid_argument", "invalid_flag", "validation"]);
|
|
26
|
+
|
|
27
|
+
// AXI exit code for a failed HEY run.
|
|
28
|
+
export function axiExitCode(status, code) {
|
|
29
|
+
if (status === 0) return 0;
|
|
30
|
+
if (USAGE_CODES.has(code)) return 2;
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
35
|
+
const NOISE = /^(warning:|panic:|fatal error:|\[signal |runtime error|\s+at |goroutine \d|\s*[\w./-]+\.go:\d+|exit status \d+$|\[\w+\]\s)/i;
|
|
36
|
+
|
|
37
|
+
// The one line of HEY's plain-text output worth showing an agent.
|
|
38
|
+
// Programming-language errors from inside HEY or its libraries say nothing an agent can
|
|
39
|
+
// act on: they are dropped, and the failure's own message (by kind) stands alone.
|
|
40
|
+
const INTERNAL = /^(Uncaught )?(TypeError|ReferenceError|SyntaxError|RangeError|InternalError|AssertionError|panic|fatal|runtime error|invalid memory address|nil pointer|unexpected end of JSON|json: |SIGSEGV)\b/i;
|
|
41
|
+
// Any other "<Something>Error:" / "<Something>Exception:" line, a Ruby/Python-style
|
|
42
|
+
// class path (Foo::Bar, foo.bar.BazError) or a traceback is a dependency's own error
|
|
43
|
+
// (SqliteError, PG::ConnectionBad, sqlalchemy.exc.OperationalError): dropped the same way.
|
|
44
|
+
const DEPENDENCY = /^(Uncaught\s+)?([\w$]+(\.[\w$]+|::[\w$]+)*(Error|Exception|Fault)\b|[A-Z]\w*(::[A-Z]\w*)+\b|Traceback\b|Caused by\b|node:internal\b)/;
|
|
45
|
+
// Names of HTTP/database libraries; a short value that is just one of these says nothing.
|
|
46
|
+
const LIBRARY = /\b(undici|axios|node-fetch|got|superagent|sqlite3?|better-sqlite3|sequel|activerecord|faraday|net\/http|go-http-client|libcurl|openssl|electron|node(\.js)?|v8)\b/i;
|
|
47
|
+
// Low-level network codes become plain words.
|
|
48
|
+
const NETWORK = [
|
|
49
|
+
[/\bECONNRESET\b/, "the connection was reset"], [/\bECONNREFUSED\b/, "the connection was refused"],
|
|
50
|
+
[/\bETIMEDOUT\b|\bi\/o timeout\b|context deadline exceeded/i, "the request timed out"], [/\bENOTFOUND\b|no such host/i, "the server name could not be resolved"],
|
|
51
|
+
[/\bEAI_AGAIN\b/, "DNS lookup failed"], [/\bcertificate\b|\bx509\b|\bTLS\b/i, "a TLS/certificate problem"],
|
|
52
|
+
// Local database codes (HEY's cache) become plain words too.
|
|
53
|
+
[/\bSQLITE_(BUSY|LOCKED)\b|database is locked/i, "the local database is busy; retry in a moment"], [/\bSQLITE_[A-Z]+\b/, "a local database error"],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function cleanLine(text) {
|
|
57
|
+
const lines = String(text || "").replace(ANSI, "").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim() && !NOISE.test(line));
|
|
58
|
+
if (!lines.length) return "";
|
|
59
|
+
let line = lines[0].trim().replace(/^(error|Error|ERROR):\s*/, "");
|
|
60
|
+
const network = NETWORK.find(([pattern]) => pattern.test(line));
|
|
61
|
+
if (network) return network[1];
|
|
62
|
+
if (INTERNAL.test(line) || DEPENDENCY.test(line)) return "";
|
|
63
|
+
// Library prefixes ("axios:", "fetch failed:", "Get \"https://…\":") and raw URLs add nothing.
|
|
64
|
+
line = line.replace(/^((axios|undici|node-fetch|got|faraday|sqlite3?|better-sqlite3)(\s*error)?|fetch failed|request failed|(get|post|put|patch|delete) "[^"]*"):?\s*/i, "")
|
|
65
|
+
.replace(/\bhttps?:\/\/\S+/g, "the HEY server")
|
|
66
|
+
// File paths and source locations (/usr/lib/…/x.js:12:3, C:\\…) are internal detail.
|
|
67
|
+
.replace(/(^|\s)(\/|~\/|[A-Za-z]:\\)[^\s:]+(:\d+)*/g, "$1a local file").replace(/\s{2,}/g, " ").trim();
|
|
68
|
+
// A line that is still a dependency error after its prefix came off is dropped too.
|
|
69
|
+
if (!line || INTERNAL.test(line) || DEPENDENCY.test(line) || /^[\w./-]+$/.test(line) && LIBRARY.test(line)) return "";
|
|
70
|
+
return heyToAxi(line.length > 200 ? `${line.slice(0, 200)}…` : line);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// HEY's stderr after a successful run, minus terminal escapes and debug noise (stack
|
|
74
|
+
// frames, panics): warnings and notices such as `next_page: …` stay, on stderr.
|
|
75
|
+
export function warningLines(text) {
|
|
76
|
+
return String(text || "").replace(ANSI, "").split(/\r?\n/).map((line) => line.trimEnd())
|
|
77
|
+
.filter((line) => line.trim() && (/^warning:/i.test(line.trim()) || !NOISE.test(line)))
|
|
78
|
+
.map((line) => heyToAxi(line));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Keys in HEY's error meta that only carry debugging detail.
|
|
82
|
+
const DEBUG_KEYS = /stack|trace|backtrace|panic|goroutine|debug|caller|frames?$/i;
|
|
83
|
+
|
|
84
|
+
// HEY's error meta without debugging detail: stack-like keys are dropped and
|
|
85
|
+
// multi-line strings are cut to their one meaningful line.
|
|
86
|
+
export function cleanMeta(value, depth = 0) {
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
if (/[\r\n\u001b]/.test(value) || value.length > 200) return cleanLine(value);
|
|
89
|
+
// Short values are kept as they are, unless they are a dependency's error or name.
|
|
90
|
+
const bare = value.trim().replace(/^(error|Error|ERROR):\s*/, "");
|
|
91
|
+
return INTERNAL.test(bare) || DEPENDENCY.test(bare) || (/^[\w./@ -]{1,40}$/.test(bare) && LIBRARY.test(bare) && !/\s\w+\s\w+/.test(bare)) ? undefined : value;
|
|
92
|
+
}
|
|
93
|
+
if (!value || typeof value !== "object" || depth > 3) return depth > 3 ? undefined : value;
|
|
94
|
+
if (Array.isArray(value)) return value.slice(0, 20).map((item) => cleanMeta(item, depth + 1)).filter((item) => item !== undefined && item !== "");
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const [key, item] of Object.entries(value)) {
|
|
97
|
+
if (DEBUG_KEYS.test(key)) continue;
|
|
98
|
+
const cleaned = cleanMeta(item, depth + 1);
|
|
99
|
+
if (cleaned !== undefined && cleaned !== "") out[key] = cleaned;
|
|
100
|
+
}
|
|
101
|
+
return Object.keys(out).length ? out : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function warnings(text) {
|
|
105
|
+
return String(text || "").replace(ANSI, "").split(/\r?\n/).filter((line) => /^warning:/i.test(line.trim())).map((line) => line.trim());
|
|
26
106
|
}
|
|
27
107
|
|
|
28
108
|
function findEnvelope(text) {
|
|
@@ -43,19 +123,36 @@ function findEnvelope(text) {
|
|
|
43
123
|
return null;
|
|
44
124
|
}
|
|
45
125
|
|
|
46
|
-
|
|
47
|
-
|
|
126
|
+
// Returns { failure, exitCode, warnings }: the object to print, hey-axi's exit code,
|
|
127
|
+
// and diagnostic lines for stderr.
|
|
128
|
+
export function translateFailure(result, { path } = {}) {
|
|
129
|
+
const raw = KINDS[result.status] || KINDS[1];
|
|
130
|
+
const info = path ? { ...raw, help: raw.help.replace("<command>", path) } : raw;
|
|
131
|
+
if (result.error) {
|
|
132
|
+
return { failure: { ok: false, error: result.error, kind: info.kind, help: info.help }, exitCode: 1, warnings: [] };
|
|
133
|
+
}
|
|
48
134
|
for (const text of [result.stderr, result.stdout]) {
|
|
49
135
|
const found = findEnvelope(text);
|
|
50
136
|
if (!found) continue;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
failure.
|
|
57
|
-
|
|
137
|
+
const { envelope } = found;
|
|
138
|
+
// Even HEY's own envelope is translated: one clean line, no stack frames or escapes.
|
|
139
|
+
const failure = { ok: false, error: cleanLine(String(envelope.error)) || info.message, kind: info.kind };
|
|
140
|
+
if (envelope.code !== undefined && envelope.code !== "" && envelope.code !== "unknown") failure.code = String(envelope.code);
|
|
141
|
+
const meta = envelope.meta === undefined || envelope.meta === "" ? undefined : cleanMeta(envelope.meta);
|
|
142
|
+
if (meta !== undefined) failure.meta = meta;
|
|
143
|
+
const usage = USAGE_CODES.has(envelope.code) || /^usage: /i.test(String(envelope.error));
|
|
144
|
+
if (usage) failure.kind = "usage";
|
|
145
|
+
const hint = envelope.hint ? cleanLine(String(envelope.hint)) : "";
|
|
146
|
+
if (hint && !/^Run 'hey(-axi)? --help'/.test(hint)) failure.hint = hint;
|
|
147
|
+
failure.help = usage ? `Run \`hey-axi ${path || "<command>"} --help\` for its arguments and flags` : info.help;
|
|
148
|
+
return { failure, exitCode: usage ? 2 : axiExitCode(result.status, envelope.code), warnings: warnings(found.prefix) };
|
|
58
149
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
150
|
+
const detail = cleanLine(result.stderr) || cleanLine(result.stdout);
|
|
151
|
+
const failure = { ok: false, error: detail ? `${info.message}: ${detail}` : info.message, kind: info.kind, help: info.help };
|
|
152
|
+
return { failure, exitCode: axiExitCode(result.status), warnings: [...warnings(result.stderr), ...warnings(result.stdout)] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Kept for callers that only need the object (the home view).
|
|
156
|
+
export function heyFailure(result) {
|
|
157
|
+
return translateFailure(result).failure;
|
|
61
158
|
}
|
package/src/examples.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Per-command help details (AXI principle 10): every flag's default, and two or three
|
|
2
|
+
// usage examples for every command. HEY's own examples come first; commands HEY gives
|
|
3
|
+
// fewer than two for get examples built from their USAGE line and flags.
|
|
4
|
+
|
|
5
|
+
import { heyToAxi } from "./text.js";
|
|
6
|
+
import { patternsFor } from "./arity.js";
|
|
7
|
+
import { LIST_FIELDS, parseField } from "./shape.js";
|
|
8
|
+
import { userOnlyPaths } from "./policy.js";
|
|
9
|
+
|
|
10
|
+
// "(default …)" text for one flag, including zero, false and empty defaults.
|
|
11
|
+
export function flagDefault(flag) {
|
|
12
|
+
const value = flag.default;
|
|
13
|
+
if (!flag.value) return `default ${value === "true" ? "true" : "false"}`;
|
|
14
|
+
if (value === undefined || value === "" || value === "[]") return "default: none";
|
|
15
|
+
if (flag.name === "limit" && value === "0") return "default 0: no limit set, HEY returns its first page";
|
|
16
|
+
if (value === "0s") return "default 0s: no timeout";
|
|
17
|
+
return `default ${value}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const placeholder = (flag) => {
|
|
21
|
+
if (flag.type === "int") return "10";
|
|
22
|
+
if (flag.type === "duration") return "10m";
|
|
23
|
+
return `<${flag.name}>`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function synthesized(node) {
|
|
27
|
+
const pattern = patternsFor(node)[0];
|
|
28
|
+
const args = (pattern?.args || []).filter((arg) => arg.required).map((arg) => `<${arg.name}>`);
|
|
29
|
+
const oneOf = pattern?.oneOf?.[0]?.[0];
|
|
30
|
+
const fromHey = (node.examples || [])[0];
|
|
31
|
+
const base = fromHey ? heyToAxi(fromHey) : ["hey-axi", node.path, ...args, ...(oneOf ? [oneOf] : [])].join(" ");
|
|
32
|
+
const out = [base];
|
|
33
|
+
const own = (node.flags || []).filter((flag) => !(pattern?.oneOf || []).flat().includes(`--${flag.name}`));
|
|
34
|
+
const valued = own.find((flag) => flag.value && !/\(required\)$/.test(flag.desc || ""));
|
|
35
|
+
const required = own.filter((flag) => /\(required\)$/.test(flag.desc || ""));
|
|
36
|
+
if (required.length && !fromHey) {
|
|
37
|
+
out[0] = `${base} ${required.map((flag) => `--${flag.name} ${placeholder(flag)}`).join(" ")}`;
|
|
38
|
+
}
|
|
39
|
+
if (valued && !out[0].includes(`--${valued.name}`)) out.push(`${out[0]} --${valued.name} ${placeholder(valued)}`);
|
|
40
|
+
const list = LIST_FIELDS[node.path];
|
|
41
|
+
if (list) out.push(`${out[0]} --fields ${list.slice(0, 2).map((spec) => parseField(spec).key).join(",")}`);
|
|
42
|
+
out.push(`${out[0]} --json`);
|
|
43
|
+
out.push(`${out[0]} --account <id>`);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Two or three examples, HEY's own first.
|
|
48
|
+
export function examplesFor(node) {
|
|
49
|
+
const examples = (node.examples || []).map(heyToAxi);
|
|
50
|
+
for (const example of synthesized(node)) {
|
|
51
|
+
if (examples.length >= 3 || (examples.length >= 2 && (node.examples || []).length >= 2)) break;
|
|
52
|
+
if (!examples.includes(example)) examples.push(example);
|
|
53
|
+
}
|
|
54
|
+
// Person-only commands run only with --interactive, so their examples say so.
|
|
55
|
+
const person = userOnlyPaths().includes(node.path);
|
|
56
|
+
return examples.slice(0, 3).map((example) => (person && !/--(interactive|token|cookie)\b/.test(example) ? `${example} --interactive` : example));
|
|
57
|
+
}
|
package/src/guide.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Static guidance shared by the no-args home view and the generated part of
|
|
2
2
|
// skills/hey-axi/SKILL.md (scripts/gen-skill.js), so the two never drift.
|
|
3
3
|
|
|
4
|
+
import { withSelectors } from "./shape.js";
|
|
5
|
+
|
|
4
6
|
export const DESCRIPTION = "HEY email, calendars and todos for agents: compact TOON over Basecamp's HEY CLI; outgoing mail is saved as a draft unless --allow-send";
|
|
5
7
|
|
|
6
|
-
// How many
|
|
8
|
+
// How many threads the home view (and the session hook) shows by default.
|
|
7
9
|
export const HOME_LIMIT = 10;
|
|
8
10
|
|
|
9
11
|
export const HOME_HELP = [
|
|
@@ -15,13 +17,30 @@ export const HOME_HELP = [
|
|
|
15
17
|
"Run `hey-axi --help` for all commands; lists take --fields a,b / --fields all, long text --full",
|
|
16
18
|
];
|
|
17
19
|
|
|
20
|
+
// The live home view's help: the static lines, pointed at the scope's command, with the
|
|
21
|
+
// scope's --account carried into every suggestion, plus "see the rest" / --full /
|
|
22
|
+
// review-drafts hints when they apply.
|
|
23
|
+
export function homeHelp({ base = "hey-axi box view imbox", carry = [], more = false, total, truncated = false, drafts = false, search = false } = {}) {
|
|
24
|
+
const lines = HOME_HELP.map((line) => {
|
|
25
|
+
if (line.includes("box view imbox --limit 50")) {
|
|
26
|
+
if (!more) return `Run \`${withSelectors("hey-axi box list", carry)}\` for other boxes (feed, papertrail, aside, later)`;
|
|
27
|
+
const count = total !== undefined ? `all ${total}` : "more";
|
|
28
|
+
return search ? `Run \`${base}\` for ${count}` : `Run \`${base} --all\` for ${count} (or --limit <n>)`;
|
|
29
|
+
}
|
|
30
|
+
return line.replace(/`(hey-axi [^`]*)`/, (_, command) => (command === "hey-axi --help" ? `\`${command}\`` : `\`${withSelectors(command, carry)}\``));
|
|
31
|
+
}).filter(Boolean);
|
|
32
|
+
if (truncated) lines.push(`Run \`${base} --full\` to see complete content`);
|
|
33
|
+
if (drafts) lines.unshift(`Run \`${withSelectors("hey-axi draft list", carry)}\` to review drafts saved last session (not sent)`);
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
export const SETUP_HELP = {
|
|
19
38
|
missing: [
|
|
20
39
|
"Install the HEY CLI: curl -fsSL https://hey.com/install-cli | bash (or brew install --cask basecamp/tap/hey)",
|
|
21
40
|
"If it is installed outside PATH, set HEY_BIN=/path/to/hey",
|
|
22
41
|
],
|
|
23
42
|
auth: [
|
|
24
|
-
"Ask the user to run `hey auth login` in their own terminal (it opens a browser)",
|
|
43
|
+
"Ask the user to run `hey-axi auth login --interactive` in their own terminal (it opens a browser), or run `hey-axi auth login --token <token>`",
|
|
25
44
|
"Run `hey-axi auth status` to check afterwards",
|
|
26
45
|
],
|
|
27
46
|
other: ["Run `hey-axi doctor` to find login and configuration problems"],
|
package/src/home-path.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The absolute path of a file, with the user's home directory collapsed to `~`, for
|
|
2
|
+
// paths shown to agents.
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function collapse(path, home = homedir()) {
|
|
7
|
+
if (!path) return path;
|
|
8
|
+
const absolute = resolve(path);
|
|
9
|
+
if (!home) return absolute;
|
|
10
|
+
if (absolute === home) return "~";
|
|
11
|
+
return absolute.startsWith(home.endsWith(sep) ? home : home + sep) ? `~${absolute.slice(home.replace(/[\\/]$/, "").length)}` : absolute;
|
|
12
|
+
}
|
package/src/home.js
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
|
-
// The no-args home view (AXI principles 8 and 10): who this is,
|
|
2
|
-
//
|
|
1
|
+
// The no-args home view (AXI principles 7, 8 and 10): who this is, the directory scope,
|
|
2
|
+
// live mail for that scope with its count, what the last session in this directory did,
|
|
3
|
+
// then the next commands. It is also what the session hook prints. It never fails hard:
|
|
3
4
|
// a missing or signed-out HEY is reported as `status` with the fix (exit 0), so a
|
|
4
5
|
// session hook still gives the agent something useful.
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { shapeData } from "./shape.js";
|
|
7
|
+
import { DESCRIPTION, SETUP_HELP, homeHelp } from "./guide.js";
|
|
8
|
+
import { carrySelectors, countLine, listSize, shapeData, findList } from "./shape.js";
|
|
9
9
|
import { heyFailure } from "./errors.js";
|
|
10
10
|
import { heyToAxi } from "./text.js";
|
|
11
|
+
import { ScopeError, findScope, scopeQuery } from "./scope.js";
|
|
12
|
+
import { lastSession } from "./activity.js";
|
|
13
|
+
import { collapse } from "./home-path.js";
|
|
11
14
|
|
|
12
|
-
export
|
|
13
|
-
return path && home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
14
|
-
}
|
|
15
|
+
export { collapse };
|
|
15
16
|
|
|
16
|
-
export async function homeView(runHey, { execPath = process.argv[1] } = {}) {
|
|
17
|
+
export async function homeView(runHey, { execPath = process.argv[1], cwd = process.cwd() } = {}) {
|
|
17
18
|
const out = { bin: collapse(execPath), description: DESCRIPTION };
|
|
18
|
-
|
|
19
|
+
let scope = null;
|
|
20
|
+
try {
|
|
21
|
+
scope = findScope(cwd);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!(error instanceof ScopeError)) throw error;
|
|
24
|
+
out.status = error.message;
|
|
25
|
+
out.help = ["Fix or delete that file, then run `hey-axi` again", "Run `hey-axi setup scope --help` for the format"];
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
const query = scopeQuery(scope);
|
|
29
|
+
out.scope = query.label;
|
|
30
|
+
const previous = lastSession(scope?.dir || cwd);
|
|
31
|
+
if (previous) out.last_session = previous.line;
|
|
32
|
+
|
|
33
|
+
const result = await runHey(query.argv, ["--json"]);
|
|
19
34
|
if (result.status === 127) {
|
|
20
35
|
out.status = result.error || "HEY CLI not found";
|
|
21
36
|
out.help = SETUP_HELP.missing;
|
|
@@ -30,6 +45,7 @@ export async function homeView(runHey, { execPath = process.argv[1] } = {}) {
|
|
|
30
45
|
out.status = `HEY error: ${heyToAxi(failure.error)}`;
|
|
31
46
|
out.help = [...(failure.hint ? [heyToAxi(failure.hint)] : []), ...SETUP_HELP.other];
|
|
32
47
|
}
|
|
48
|
+
out.help = out.help.map((line) => carrySelectors(line, query.carry || []));
|
|
33
49
|
return out;
|
|
34
50
|
}
|
|
35
51
|
let envelope;
|
|
@@ -40,11 +56,27 @@ export async function homeView(runHey, { execPath = process.argv[1] } = {}) {
|
|
|
40
56
|
out.help = SETUP_HELP.other;
|
|
41
57
|
return out;
|
|
42
58
|
}
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
const body = envelope && typeof envelope === "object" && "data" in envelope ? envelope.data : envelope;
|
|
60
|
+
const found = findList(body);
|
|
61
|
+
const size = listSize(envelope, found ? found.list.length : 0, found?.container);
|
|
62
|
+
// search has no --limit: keep the first `limit` rows here.
|
|
63
|
+
if (found && found.list.length > query.limit) {
|
|
64
|
+
// More rows than shown, but no total unless HEY gave one.
|
|
65
|
+
size.more = true;
|
|
66
|
+
}
|
|
67
|
+
const { data, truncated } = shapeData(body, { path: query.path });
|
|
68
|
+
let threads = Array.isArray(data) ? data : data?.[found?.key];
|
|
69
|
+
if (threads && threads.length > query.limit) threads = threads.slice(0, query.limit);
|
|
70
|
+
size.shown = threads?.length ?? 0;
|
|
71
|
+
if (!threads?.length && (size.more || size.total > 0)) {
|
|
72
|
+
// An empty page while HEY reports more: say exactly that, not "nothing".
|
|
73
|
+
out.count = countLine(size);
|
|
74
|
+
out.mail = `0 threads on this page; HEY reports ${size.total > 0 ? `${size.total} in total` : "more"}`;
|
|
75
|
+
} else if (!threads?.length) out.mail = `0 threads: nothing in ${query.label.split(" (")[0]}`;
|
|
76
|
+
else {
|
|
77
|
+
out.count = countLine(size);
|
|
78
|
+
out.threads = threads;
|
|
79
|
+
}
|
|
80
|
+
out.help = homeHelp({ base: query.base, carry: query.carry, more: size.more, total: size.total, truncated, drafts: previous?.drafts > 0, search: query.path === "search" });
|
|
49
81
|
return out;
|
|
50
82
|
}
|