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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # hey-axi
2
2
 
3
+ [![skills.sh](https://skills.sh/b/TangentialSolutions/hey-axi)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hey-axi",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Agent-ergonomic AXI wrapper for Basecamp's HEY CLI: TOON output, send-to-draft safety, and a shareable agent skill",
5
5
  "keywords": [
6
6
  "axi",
@@ -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, content, topic_id, at |
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, starts_at, content |
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, content |
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 \`hey-axi ${path} --help\` to check the filters` : `Run \`${commandLine} --fields all\` to see every field`;
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, "").replace(/\bhttps?:\/\/\S+/g, "the HEY server").trim();
57
- if (!line) return "";
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") return /[\r\n\u001b]/.test(value) || value.length > 200 ? cleanLine(value) : value;
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", "content"],
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", "content", "topic_id", "at=created_at"],
50
- "snippet list": ["id", "name", "content"],
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
- return complete || all || shown === 0 ? `${shown} total` : `${shown} shown; no more pages reported`;
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
@@ -1,3 +1,3 @@
1
1
  // Leaf module: no imports, so `hey-axi --version` answers before the CLI graph loads.
2
2
  // test/version.test.js keeps this in sync with package.json.
3
- export const VERSION = "0.3.0";
3
+ export const VERSION = "0.3.1";