hey-axi 0.3.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 +2 -0
- package/package.json +1 -1
- package/skills/hey-axi/references/commands.md +3 -3
- package/src/cli.js +1 -1
- package/src/errors.js +21 -4
- package/src/shape.js +14 -4
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# hey-axi
|
|
2
2
|
|
|
3
|
+
[](https://skills.sh/TangentialSolutions/hey-axi)
|
|
4
|
+
|
|
3
5
|
An agent-friendly wrapper for [HEY CLI](https://github.com/basecamp/hey-cli) (`hey`), Basecamp's command line for HEY email, calendars, todos, habits, time tracking and journals.
|
|
4
6
|
|
|
5
7
|
hey-axi runs `hey`, asks for its JSON response envelope, and prints it as [TOON](https://github.com/toon-format/toon). It follows the [AXI](https://axi.md) principles: a live home view, minimal default fields, truncated long text, definitive empty states, and unknown flags rejected up front. On our synthetic benchmark that's **~89% fewer tokens than `hey --json`** ([docs/benchmarks.md](docs/benchmarks.md)). It also adds a few safety rails. It covers **every command in HEY CLI v1.7.0** (154 runnable command paths, including the `login`/`logout` aliases, plus the `box <id>`-style shortcuts) by routing from a snapshot of HEY's own `hey commands --json` catalog. See [CHANGELOG.md](CHANGELOG.md) for what changed in each release.
|
package/package.json
CHANGED
|
@@ -32,7 +32,7 @@ Flags every command accepts: `--account <id|all>`, `--json`, `--quiet`, `--field
|
|
|
32
32
|
| `calendar list` | List calendars | | fields: id, name, kind, owned |
|
|
33
33
|
| `clip create` | Save text from an email entry | `--content <v>` | |
|
|
34
34
|
| `clip delete` | Delete a saved clip | | |
|
|
35
|
-
| `clip list` | List the newest page of passages clipped from email | | fields: id,
|
|
35
|
+
| `clip list` | List the newest page of passages clipped from email | | fields: id, topic_id, at |
|
|
36
36
|
| `collection` | List and manage email collections | `--all` `--limit <v>` `--page <v>` | fields: id, topic_id, from, subject |
|
|
37
37
|
| `collection add` | Add email threads to a collection | `--to <v>` | |
|
|
38
38
|
| `collection create` | Create a collection | `--summary <v>` | |
|
|
@@ -79,7 +79,7 @@ Flags every command accepts: `--account <id|all>`, `--json`, `--quiet`, `--field
|
|
|
79
79
|
| `habit list` | List habits | `--all` `--date <v>` `--limit <v>` | fields: id, title, days |
|
|
80
80
|
| `habit uncomplete` | Remove a habit completion for a date | `--date <v>` | |
|
|
81
81
|
| `ignore` | Ignore email threads | | |
|
|
82
|
-
| `journal list` | List journal entries | `--all` `--calendar <v>` `--ends-on <v>` `--limit <v>` `--starts-on <v>` | fields: id,
|
|
82
|
+
| `journal list` | List journal entries | `--all` `--calendar <v>` `--ends-on <v>` `--limit <v>` `--starts-on <v>` | fields: id, date |
|
|
83
83
|
| `journal read` | Read a journal entry (default: today) | | |
|
|
84
84
|
| `journal write` | Write or edit a journal entry (default: today) | `--content <v>` `--content-html <v>` | |
|
|
85
85
|
| `label` | List and manage email labels | `--all` `--limit <v>` `--page <v>` | fields: id, topic_id, from, subject |
|
|
@@ -122,7 +122,7 @@ Flags every command accepts: `--account <id|all>`, `--json`, `--quiet`, `--field
|
|
|
122
122
|
| `skill install` | Install the hey skill globally for your coding agents | | |
|
|
123
123
|
| `snippet create` | Create a reusable email snippet | `--content <v>` `--content-html <v>` `--name <v>` | |
|
|
124
124
|
| `snippet delete` | Delete a reusable email snippet | | |
|
|
125
|
-
| `snippet list` | List reusable email snippets | | fields: id, name
|
|
125
|
+
| `snippet list` | List reusable email snippets | | fields: id, name |
|
|
126
126
|
| `snippet update` | Update a reusable email snippet | `--content <v>` `--content-html <v>` `--name <v>` | |
|
|
127
127
|
| `spam` | Mark email threads as spam | | |
|
|
128
128
|
| `stop-ignoring` | Stop ignoring email threads | | |
|
package/src/cli.js
CHANGED
|
@@ -568,7 +568,7 @@ if (!full && !quiet && !plainText && shaped && typeof shaped === "object" && sha
|
|
|
568
568
|
? `Run \`${withSelectors(`hey-axi ${sibling.path} --help`, carry)}\` to see how to add one`
|
|
569
569
|
: sibling
|
|
570
570
|
? `Run \`${withSelectors(`hey-axi ${sibling.path} <id>`, carry)}\` to see one in full`
|
|
571
|
-
: shaped.empty ? `Run
|
|
571
|
+
: shaped.empty ? `Run \`${withSelectors(`hey-axi ${path} --help`, carry)}\` to check the filters` : `Run \`${commandLine} --fields all\` to see every field`;
|
|
572
572
|
shaped.help = [next, ...help];
|
|
573
573
|
}
|
|
574
574
|
}
|
package/src/errors.js
CHANGED
|
@@ -38,11 +38,19 @@ const NOISE = /^(warning:|panic:|fatal error:|\[signal |runtime error|\s+at |gor
|
|
|
38
38
|
// Programming-language errors from inside HEY or its libraries say nothing an agent can
|
|
39
39
|
// act on: they are dropped, and the failure's own message (by kind) stands alone.
|
|
40
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;
|
|
41
47
|
// Low-level network codes become plain words.
|
|
42
48
|
const NETWORK = [
|
|
43
49
|
[/\bECONNRESET\b/, "the connection was reset"], [/\bECONNREFUSED\b/, "the connection was refused"],
|
|
44
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"],
|
|
45
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"],
|
|
46
54
|
];
|
|
47
55
|
|
|
48
56
|
export function cleanLine(text) {
|
|
@@ -51,10 +59,14 @@ export function cleanLine(text) {
|
|
|
51
59
|
let line = lines[0].trim().replace(/^(error|Error|ERROR):\s*/, "");
|
|
52
60
|
const network = NETWORK.find(([pattern]) => pattern.test(line));
|
|
53
61
|
if (network) return network[1];
|
|
54
|
-
if (INTERNAL.test(line)) return "";
|
|
62
|
+
if (INTERNAL.test(line) || DEPENDENCY.test(line)) return "";
|
|
55
63
|
// Library prefixes ("axios:", "fetch failed:", "Get \"https://…\":") and raw URLs add nothing.
|
|
56
|
-
line = line.replace(/^(axios|undici|fetch failed|request failed|(get|post|put|patch|delete) "[^"]*"):?\s*/i, "")
|
|
57
|
-
|
|
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 "";
|
|
58
70
|
return heyToAxi(line.length > 200 ? `${line.slice(0, 200)}…` : line);
|
|
59
71
|
}
|
|
60
72
|
|
|
@@ -72,7 +84,12 @@ const DEBUG_KEYS = /stack|trace|backtrace|panic|goroutine|debug|caller|frames?$/
|
|
|
72
84
|
// HEY's error meta without debugging detail: stack-like keys are dropped and
|
|
73
85
|
// multi-line strings are cut to their one meaningful line.
|
|
74
86
|
export function cleanMeta(value, depth = 0) {
|
|
75
|
-
if (typeof value === "string")
|
|
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
|
+
}
|
|
76
93
|
if (!value || typeof value !== "object" || depth > 3) return depth > 3 ? undefined : value;
|
|
77
94
|
if (Array.isArray(value)) return value.slice(0, 20).map((item) => cleanMeta(item, depth + 1)).filter((item) => item !== undefined && item !== "");
|
|
78
95
|
const out = {};
|
package/src/shape.js
CHANGED
|
@@ -38,7 +38,7 @@ export const LIST_FIELDS = {
|
|
|
38
38
|
"event week": EVENTS,
|
|
39
39
|
"todo list": ["id", "title", "starts_at", "completed_at"],
|
|
40
40
|
"habit list": ["id", "title=title|name", "days"],
|
|
41
|
-
"journal list": ["id", "starts_at"
|
|
41
|
+
"journal list": ["id", "date=date|starts_at"],
|
|
42
42
|
"draft list": ["id", "subject", "summary", "at=updated_at|created_at"],
|
|
43
43
|
"contact list": ["id", "name", "email_address"],
|
|
44
44
|
"label list": NAMED,
|
|
@@ -46,8 +46,8 @@ export const LIST_FIELDS = {
|
|
|
46
46
|
"workflow list": NAMED,
|
|
47
47
|
"calendar list": ["id", "name", "kind", "owned"],
|
|
48
48
|
"attachment list": ["id", "filename", "content_type", "byte_size"],
|
|
49
|
-
"clip list": ["id", "
|
|
50
|
-
"snippet list": ["id", "name"
|
|
49
|
+
"clip list": ["id", "topic_id=topic_id|topic.id", "at=created_at"],
|
|
50
|
+
"snippet list": ["id", "name"],
|
|
51
51
|
"timetrack list": ["id", "starts_at", "ends_at", "category"],
|
|
52
52
|
"timetrack categories": ["id", "title"],
|
|
53
53
|
"account list": ["id", "email=email|email_address", "name", "active"],
|
|
@@ -55,6 +55,14 @@ export const LIST_FIELDS = {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
// Detail views keep their long text (truncated), but drop URLs and duplicated fields.
|
|
58
|
+
// Lists whose entries are long-form content: the list shows ids and dates only, and a help
|
|
59
|
+
// line says how to get the content (AXI principle 2: long text belongs in detail views).
|
|
60
|
+
export const CONTENT_LISTS = {
|
|
61
|
+
"journal list": (line, carry) => `Run \`${withSelectors("hey-axi journal read <date>", carry)}\` to read one entry, or \`${line} --fields id,starts_at,content\` to include previews`,
|
|
62
|
+
"clip list": (line) => `Run \`${line} --fields id,content\` to include each clip's content`,
|
|
63
|
+
"snippet list": (line) => `Run \`${line} --fields id,name,content\` to include each snippet's content`,
|
|
64
|
+
};
|
|
65
|
+
|
|
58
66
|
export const DETAIL_FIELDS = {
|
|
59
67
|
"thread read": ["id", "at=created_at", "from=creator.name|sender.name", "email=creator.email_address|sender.email_address", "to=recipients.to[].email_address", "cc=recipients.cc[].email_address", "body=body|summary"],
|
|
60
68
|
};
|
|
@@ -279,7 +287,8 @@ export function listSize(envelope, shown, container = null) {
|
|
|
279
287
|
export function countLine({ shown, total, more, complete, all = false }) {
|
|
280
288
|
if (total !== undefined) return `${shown} of ${total} total`;
|
|
281
289
|
if (more) return `${shown} shown; more available`;
|
|
282
|
-
|
|
290
|
+
// An empty page is only "0 total" when HEY says nothing is left (or --all fetched everything).
|
|
291
|
+
return complete || all ? `${shown} total` : `${shown} shown; no more pages reported`;
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
// Append the invocation's selectors (`--account 2`) to a suggested command, unless the
|
|
@@ -372,6 +381,7 @@ export function shapeEnvelope(envelope, { path, fields = null, commandLine, carr
|
|
|
372
381
|
const more = (size || multiSize) && moreHelp(size || multiSize, { commandLine, pageFlags });
|
|
373
382
|
if (more) help.push(more);
|
|
374
383
|
if (truncated) help.push(`Run \`${commandLine} --full\` to see complete content`);
|
|
384
|
+
if (CONTENT_LISTS[path] && !fields && !empty && found) help.push(CONTENT_LISTS[path](commandLine, carry));
|
|
375
385
|
if (help.length) out.help = help;
|
|
376
386
|
if (enveloped && !quiet && envelope.meta && typeof envelope.meta === "object") {
|
|
377
387
|
const meta = { ...envelope.meta };
|
package/src/version.js
CHANGED