hey-axi 0.2.0 → 0.3.0
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 +29 -15
- package/package.json +1 -1
- package/skills/hey-axi/SKILL.md +48 -45
- package/skills/hey-axi/references/commands.md +36 -33
- 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 +111 -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 +184 -53
- package/src/text.js +3 -3
- package/src/version.js +1 -1
package/src/shape.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
// Shape HEY's JSON
|
|
2
|
-
// -
|
|
1
|
+
// Shape HEY's JSON for agents (AXI principles 2-5 and 9):
|
|
2
|
+
// - at most four default fields per list command, `--fields a,b,c` to choose, `--fields all`
|
|
3
3
|
// - long text truncated with its total size, `--full` to get HEY's output untouched
|
|
4
|
-
// -
|
|
5
|
-
//
|
|
4
|
+
// - every list gets a `count` (`N of T total`, `N total`, or `N shown; more available`)
|
|
5
|
+
// and a definitive `empty` line when it has no rows, enveloped or not
|
|
6
|
+
// - HEY's breadcrumbs as `help` lines that name hey-axi commands and carry the
|
|
7
|
+
// invocation's selectors (--account, --base-url)
|
|
6
8
|
import { heyToAxi } from "./text.js";
|
|
7
9
|
|
|
8
10
|
// A field is "key" or "key=path", where path is dotted (`creator.name`), may index an
|
|
9
11
|
// array (`messages.0.creator.name`), may map over one (`recipients.to[].email_address`,
|
|
10
12
|
// joined with ", "), and may list fallbacks (`name|subject`).
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// Box rows keep both ids: `id` (the box item) feeds seen/unseen/move/label, `topic_id`
|
|
14
|
+
// (the thread) feeds thread read/reply/forward. `seen` and `at` are one --fields away.
|
|
15
|
+
const POSTINGS = ["id", "topic_id", "from=creator.name|sender.name|alternative_sender_name", "subject=name|subject"];
|
|
16
|
+
const EVENTS = ["id", "title", "starts_at", "ends_at"];
|
|
13
17
|
const NAMED = ["id", "name"];
|
|
14
18
|
|
|
15
19
|
export const LIST_FIELDS = {
|
|
@@ -24,11 +28,11 @@ export const LIST_FIELDS = {
|
|
|
24
28
|
"set-aside view": POSTINGS,
|
|
25
29
|
"set-aside group view": POSTINGS,
|
|
26
30
|
"contact threads": POSTINGS,
|
|
27
|
-
"bubble list": [
|
|
31
|
+
"bubble list": ["id", "topic_id", "subject=name|subject", "bubbles_up=bubble_up_schedule.bubble_up_at"],
|
|
28
32
|
"box list": ["id", "kind", "name"],
|
|
29
|
-
search: ["
|
|
30
|
-
"screener list": ["id", "
|
|
31
|
-
"screener history": ["id", "status", "
|
|
33
|
+
search: ["topic_id", "subject", "from=messages.0.creator.name|creator.name", "at=updated_at|created_at"],
|
|
34
|
+
"screener list": ["id", "email_address", "subject", "topic_id"],
|
|
35
|
+
"screener history": ["id", "status", "email_address=email_address|petitioner.email_address", "decided=decided|updated_at"],
|
|
32
36
|
"event list": EVENTS,
|
|
33
37
|
"event day": EVENTS,
|
|
34
38
|
"event week": EVENTS,
|
|
@@ -44,7 +48,7 @@ export const LIST_FIELDS = {
|
|
|
44
48
|
"attachment list": ["id", "filename", "content_type", "byte_size"],
|
|
45
49
|
"clip list": ["id", "content", "topic_id", "at=created_at"],
|
|
46
50
|
"snippet list": ["id", "name", "content"],
|
|
47
|
-
"timetrack list": ["id", "starts_at", "ends_at", "category"
|
|
51
|
+
"timetrack list": ["id", "starts_at", "ends_at", "category"],
|
|
48
52
|
"timetrack categories": ["id", "title"],
|
|
49
53
|
"account list": ["id", "email=email|email_address", "name", "active"],
|
|
50
54
|
"set-aside group list": ["id", "thread_count"],
|
|
@@ -88,12 +92,15 @@ export function parseField(spec) {
|
|
|
88
92
|
return index === -1 ? { key: spec, path: spec } : { key: spec.slice(0, index), path: spec.slice(index + 1) };
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
export const MAX_LIST_FIELDS = 4;
|
|
96
|
+
|
|
97
|
+
// Fields for a list without a declared schema: up to four familiar keys, or the first
|
|
98
|
+
// four keys when none of the familiar ones are there.
|
|
92
99
|
function heuristicFields(items) {
|
|
93
|
-
const keys = new Set(items.flatMap((item) => (item && typeof item === "object" ? Object.keys(item) : [])));
|
|
94
|
-
if (keys.
|
|
95
|
-
const picked = HEURISTIC_KEYS.filter((key) => keys.
|
|
96
|
-
return picked.length >= 2 ? picked :
|
|
100
|
+
const keys = [...new Set(items.flatMap((item) => (item && typeof item === "object" ? Object.keys(item) : [])))];
|
|
101
|
+
if (keys.length <= MAX_LIST_FIELDS) return null;
|
|
102
|
+
const picked = HEURISTIC_KEYS.filter((key) => keys.includes(key)).slice(0, MAX_LIST_FIELDS);
|
|
103
|
+
return picked.length >= 2 ? picked : keys.slice(0, MAX_LIST_FIELDS);
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
// Project items onto fields. Returns { rows, unknown } where unknown lists requested
|
|
@@ -127,15 +134,22 @@ export function truncateDeep(value, limit, onTruncate, list = false) {
|
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
const isObjectList = (value) => Array.isArray(value) && value.every((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
130
|
-
|
|
137
|
+
// URLs and paging fields (summarized by `count` and the "see the rest" help) are left out.
|
|
138
|
+
const DROP_CONTAINER_KEY = (key) => /(^|_)url$|^signed_stream_name$|^(total_count|next_page|next_cursor|has_more)$/.test(key);
|
|
131
139
|
|
|
132
|
-
// Find the collection in HEY's `data`: the array itself
|
|
133
|
-
// inside a container object (e.g. a box with its
|
|
140
|
+
// Find the collection in HEY's `data`: the array itself (of objects or of plain
|
|
141
|
+
// values), the one array of objects inside a container object (e.g. a box with its
|
|
142
|
+
// postings), or, when a container holds several, all of them (`lists`).
|
|
134
143
|
export function findList(data) {
|
|
135
144
|
if (isObjectList(data)) return { list: data };
|
|
136
|
-
if (
|
|
145
|
+
if (Array.isArray(data)) return { list: data, primitive: true };
|
|
146
|
+
if (data && typeof data === "object") {
|
|
137
147
|
const keys = Object.keys(data).filter((key) => isObjectList(data[key]));
|
|
138
148
|
if (keys.length === 1) return { list: data[keys[0]], key: keys[0], container: data };
|
|
149
|
+
if (keys.length > 1) return { lists: keys.map((key) => ({ key, list: data[key] })), container: data, list: keys.flatMap((key) => data[key]) };
|
|
150
|
+
// One list of plain values next to other fields ({items: ["a","b"], total_count: 40}).
|
|
151
|
+
const plain = Object.keys(data).filter((key) => Array.isArray(data[key]));
|
|
152
|
+
if (keys.length === 0 && plain.length === 1) return { list: data[plain[0]], key: plain[0], container: data, primitive: true };
|
|
139
153
|
}
|
|
140
154
|
return null;
|
|
141
155
|
}
|
|
@@ -170,6 +184,38 @@ export function shapeData(data, { path, fields = null }) {
|
|
|
170
184
|
if (!found) {
|
|
171
185
|
return { data: truncateDeep(data, DETAIL_TEXT_LIMIT, mark), truncated, empty: false };
|
|
172
186
|
}
|
|
187
|
+
if (found.primitive && !found.container) {
|
|
188
|
+
return { data: truncateDeep(found.list, LIST_TEXT_LIMIT, mark, true), truncated, empty: found.list.length === 0 };
|
|
189
|
+
}
|
|
190
|
+
if (found.primitive) {
|
|
191
|
+
const shaped = {};
|
|
192
|
+
for (const [name, value] of Object.entries(found.container)) {
|
|
193
|
+
if (name === found.key) shaped[name] = truncateDeep(value, LIST_TEXT_LIMIT, mark, true);
|
|
194
|
+
else if (fields === "all" || !DROP_CONTAINER_KEY(name)) shaped[name] = truncateDeep(value, DETAIL_TEXT_LIMIT, mark);
|
|
195
|
+
}
|
|
196
|
+
return { data: shaped, truncated, empty: found.list.length === 0 };
|
|
197
|
+
}
|
|
198
|
+
if (found.lists) {
|
|
199
|
+
// Several collections side by side: each gets the minimal schema of its own items.
|
|
200
|
+
const shaped = {};
|
|
201
|
+
for (const [name, value] of Object.entries(found.container)) {
|
|
202
|
+
const entry = found.lists.find((item) => item.key === name);
|
|
203
|
+
if (entry && Array.isArray(fields) && fields.length) {
|
|
204
|
+
// Explicit fields must exist in at least one of the lists.
|
|
205
|
+
const unknown = fields.filter((field) => !found.lists.some(({ list }) => project(list, [field], { strict: false }).rows.some((row) => Object.keys(row).length)));
|
|
206
|
+
if (unknown.length) throw new FieldError(unknown, [...new Set(found.lists.flatMap(({ list }) => list.slice(0, 5).flatMap((item) => Object.keys(item || {}))))]);
|
|
207
|
+
}
|
|
208
|
+
if (entry) {
|
|
209
|
+
const specs = fields === "all" ? null : Array.isArray(fields) && fields.length ? fields : heuristicFields(entry.list);
|
|
210
|
+
const rows = specs ? project(entry.list, specs).rows : entry.list;
|
|
211
|
+
shaped[name] = truncateDeep(rows, specs ? LIST_TEXT_LIMIT : DETAIL_TEXT_LIMIT, mark, Boolean(specs));
|
|
212
|
+
} else if (fields === "all" || !DROP_CONTAINER_KEY(name)) {
|
|
213
|
+
// Content next to the lists is kept (long text cut with its size), never dropped.
|
|
214
|
+
shaped[name] = truncateDeep(value, DETAIL_TEXT_LIMIT, mark);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { data: shaped, truncated, empty: found.list.length === 0 };
|
|
218
|
+
}
|
|
173
219
|
|
|
174
220
|
const { list, key, container } = found;
|
|
175
221
|
let rows = list;
|
|
@@ -197,7 +243,8 @@ export function shapeData(data, { path, fields = null }) {
|
|
|
197
243
|
shaped = {};
|
|
198
244
|
for (const [name, value] of Object.entries(container)) {
|
|
199
245
|
if (name === key) shaped[name] = rows;
|
|
200
|
-
else if (fields === "all" ||
|
|
246
|
+
else if (fields === "all" || !DROP_CONTAINER_KEY(name)) {
|
|
247
|
+
// Content next to the list is kept (long text cut with its size), never dropped.
|
|
201
248
|
shaped[name] = truncateDeep(value, DETAIL_TEXT_LIMIT, mark);
|
|
202
249
|
}
|
|
203
250
|
}
|
|
@@ -205,47 +252,131 @@ export function shapeData(data, { path, fields = null }) {
|
|
|
205
252
|
return { data: shaped, truncated, empty: list.length === 0 };
|
|
206
253
|
}
|
|
207
254
|
|
|
208
|
-
|
|
209
|
-
|
|
255
|
+
const MORE_NOTICE = /more (are )?available|more results|use --all|--page/i;
|
|
256
|
+
const TOTAL_NOTICE = /\b\d[\d,]* of (\d[\d,]*)\b/;
|
|
257
|
+
|
|
258
|
+
// What HEY told us about the size of a list: { shown, total?, more, next? }. Sources, in
|
|
259
|
+
// order: meta.total_count, a "Showing N of T" notice, and HEY's paging signals
|
|
260
|
+
// (next_page, has_more, a "more available" notice). With none of them the list is complete.
|
|
261
|
+
export function listSize(envelope, shown, container = null) {
|
|
262
|
+
// HEY puts paging in meta, or (box and other postings listings) next to the list in data.
|
|
263
|
+
const ownMeta = envelope && typeof envelope.meta === "object" && envelope.meta ? envelope.meta : {};
|
|
264
|
+
const side = container && typeof container === "object" ? Object.fromEntries(["total_count", "next_page", "next_cursor", "has_more"].filter((key) => key in container).map((key) => [key, container[key]])) : {};
|
|
265
|
+
const meta = { ...side, ...ownMeta };
|
|
266
|
+
if (typeof meta.total_count === "number" && meta.total_count < shown) delete meta.total_count;
|
|
267
|
+
const next = meta.next_page ?? meta.next_cursor ?? null;
|
|
268
|
+
const noticed = String(envelope?.notice || "").match(TOTAL_NOTICE);
|
|
269
|
+
const total = typeof meta.total_count === "number" ? meta.total_count : noticed ? Number(noticed[1].replace(/,/g, "")) : undefined;
|
|
270
|
+
const more = total !== undefined ? total > shown : Boolean(next || meta.has_more || MORE_NOTICE.test(envelope?.notice || ""));
|
|
271
|
+
// Complete only when HEY says so: a total, has_more: false, or a null next_page.
|
|
272
|
+
const complete = total !== undefined ? !more : !more && (meta.has_more === false || ("next_page" in meta && meta.next_page === null) || ("next_cursor" in meta && meta.next_cursor === null));
|
|
273
|
+
return { shown, total, more, next, complete };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// `count: 25 of 847 total` / `count: 8 total` / `count: 25 shown; more available` /
|
|
277
|
+
// `count: 25 shown; no more pages reported` (no total and no explicit end signal:
|
|
278
|
+
// hey-axi does not claim the list is complete).
|
|
279
|
+
export function countLine({ shown, total, more, complete, all = false }) {
|
|
280
|
+
if (total !== undefined) return `${shown} of ${total} total`;
|
|
281
|
+
if (more) return `${shown} shown; more available`;
|
|
282
|
+
return complete || all || shown === 0 ? `${shown} total` : `${shown} shown; no more pages reported`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Append the invocation's selectors (`--account 2`) to a suggested command, unless the
|
|
286
|
+
// command already names them.
|
|
287
|
+
export function withSelectors(command, carry = []) {
|
|
288
|
+
const extra = [];
|
|
289
|
+
for (let i = 0; i < carry.length; i += 2) {
|
|
290
|
+
if (!command.includes(carry[i])) extra.push(carry[i], carry[i + 1]);
|
|
291
|
+
}
|
|
292
|
+
return extra.length ? `${command} ${extra.join(" ")}` : command;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Carry the selectors into every `hey-axi …` command quoted inside a text (a hint, a
|
|
296
|
+
// string breadcrumb, an error's help line).
|
|
297
|
+
export function carrySelectors(text, carry = []) {
|
|
298
|
+
if (typeof text !== "string" || !carry.length) return text;
|
|
299
|
+
if (/^hey-axi \S/.test(text) && !/[`'"]/.test(text)) return withSelectors(text, carry);
|
|
300
|
+
// `…` may contain quotes ("<query>"); '…' and "…" may not contain their own quote.
|
|
301
|
+
let out = text.replace(/`(hey-axi [^`]+)`/g, (match, command) => `\`${withSelectors(command, carry)}\``);
|
|
302
|
+
out = out.replace(/(^|[^`\w])(['"])(hey-axi [^'"`]+?)\2/g, (match, lead, quote, command) => `${lead}${quote}${withSelectors(command, carry)}${quote}`);
|
|
303
|
+
// Unquoted "Run hey-axi …" to the end of the sentence.
|
|
304
|
+
return out.replace(/(\bRun:? )(hey-axi [^`'".;,\n]+?)(?=[.;,]?\s*$|[.;,]\s)/g, (match, run, command) => `${run}${withSelectors(command.trim(), carry)}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function breadcrumbHelp(crumb, carry) {
|
|
308
|
+
if (typeof crumb === "string") return carrySelectors(heyToAxi(crumb), carry);
|
|
210
309
|
if (!crumb || typeof crumb !== "object" || !crumb.command) return null;
|
|
211
310
|
const description = crumb.description ? ` to ${crumb.description.charAt(0).toLowerCase()}${crumb.description.slice(1)}` : "";
|
|
212
|
-
return `Run \`${heyToAxi(crumb.command)}\`${description}`;
|
|
311
|
+
return `Run \`${withSelectors(heyToAxi(crumb.command), carry)}\`${description}`;
|
|
213
312
|
}
|
|
214
313
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
let truncated = false;
|
|
221
|
-
const data = truncateDeep(envelope, DETAIL_TEXT_LIMIT, () => { truncated = true; });
|
|
222
|
-
if (!truncated) return data;
|
|
223
|
-
const help = [`Run \`${commandLine} --full\` to see complete content`];
|
|
224
|
-
return data && typeof data === "object" && !Array.isArray(data) ? { ...data, help } : { data, help };
|
|
225
|
-
}
|
|
314
|
+
// The help line that reveals the rest of a truncated list (AXI principle 9).
|
|
315
|
+
// The command line without one flag and its value (`--page 1`, `--page=1`).
|
|
316
|
+
function dropFlag(commandLine, name) {
|
|
317
|
+
return commandLine.replace(new RegExp(`\\s--${name}(=\\S+|\\s+(?!-)\\S+)?(?=\\s|$)`, "g"), "");
|
|
318
|
+
}
|
|
226
319
|
|
|
227
|
-
|
|
320
|
+
export function moreHelp(size, { commandLine: line, pageFlags = [] }) {
|
|
321
|
+
if (!size.more) return null;
|
|
322
|
+
// The hint replaces paging flags already on the line instead of repeating them.
|
|
323
|
+
const commandLine = dropFlag(dropFlag(line, "page"), "all");
|
|
324
|
+
const all = size.total !== undefined ? `all ${size.total}` : "all of them";
|
|
325
|
+
if (pageFlags.includes("all")) return `Run \`${commandLine} --all\` for ${all}`;
|
|
326
|
+
if (size.next && pageFlags.includes("page")) return `Run \`${commandLine} --page ${size.next}\` for the next page`;
|
|
327
|
+
if (pageFlags.includes("limit")) return `Run \`${dropFlag(commandLine, "limit")} --limit <n>\` to show more`;
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Shape whatever HEY printed. options: { path, fields, commandLine, carry, pageFlags, quiet }.
|
|
332
|
+
// carry: selector args from the invocation (["--account", "2"]) for suggested commands
|
|
333
|
+
// pageFlags: which of all/page/limit the command takes, for the "see the rest" hint
|
|
334
|
+
// quiet: drop HEY's summary, notice, breadcrumbs and meta; keep hey-axi's count,
|
|
335
|
+
// empty state and --full/--all hints
|
|
336
|
+
// Returns the object to print.
|
|
337
|
+
export function shapeEnvelope(envelope, { path, fields = null, commandLine, carry = [], pageFlags = [], quiet = false, all = false }) {
|
|
338
|
+
const enveloped = envelope && typeof envelope === "object" && !Array.isArray(envelope) && "data" in envelope;
|
|
339
|
+
const body = enveloped ? envelope.data : envelope;
|
|
340
|
+
const { data, truncated, empty } = shapeData(body, { path, fields });
|
|
341
|
+
const found = findList(body);
|
|
228
342
|
const out = {};
|
|
229
|
-
if (envelope.ok !== undefined) out.ok = envelope.ok;
|
|
230
|
-
if (envelope.summary) out.summary = envelope.summary;
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
343
|
+
if (enveloped && envelope.ok !== undefined) out.ok = envelope.ok;
|
|
344
|
+
if (enveloped && envelope.summary && !quiet) out.summary = envelope.summary;
|
|
345
|
+
const size = found && !found.lists ? listSize(enveloped ? envelope : null, found.list.length, found.container) : null;
|
|
346
|
+
if (size) out.count = countLine({ ...size, all });
|
|
347
|
+
let multiSize = null;
|
|
348
|
+
if (found?.lists) {
|
|
349
|
+
// Several lists: each one's length, plus whatever HEY says about the whole result.
|
|
350
|
+
multiSize = listSize(enveloped ? envelope : null, found.list.length, found.container);
|
|
351
|
+
const parts = found.lists.map(({ key, list }) => `${list.length} ${key}`).join(", ");
|
|
352
|
+
out.count = multiSize.total !== undefined ? `${parts} (${multiSize.total} total)` : multiSize.more ? `${parts}; more available` : parts;
|
|
353
|
+
}
|
|
354
|
+
const nothing = body === null || body === undefined || (typeof body === "object" && !Array.isArray(body) && Object.keys(body).length === 0);
|
|
355
|
+
if (!enveloped && !found && !truncated && !nothing) return data;
|
|
239
356
|
out.data = data;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
out
|
|
357
|
+
const reported = size || multiSize;
|
|
358
|
+
if (empty && reported && (reported.more || reported.total > 0)) {
|
|
359
|
+
// An empty page while HEY reports more is not "nothing".
|
|
360
|
+
out.empty = `0 results on this page for \`${commandLine}\`; HEY reports ${reported.total > 0 ? `${reported.total} in total` : "more"}`;
|
|
361
|
+
} else if (empty) out.empty = `0 results for \`${commandLine}\``;
|
|
362
|
+
else if (nothing && !(enveloped && envelope.summary)) out.empty = `no data returned for \`${commandLine}\``;
|
|
363
|
+
const help = [];
|
|
364
|
+
if (enveloped && !quiet) {
|
|
365
|
+
for (const [key, value] of Object.entries(envelope)) {
|
|
366
|
+
if (["ok", "data", "summary", "meta", "breadcrumbs", "notice"].includes(key)) continue;
|
|
367
|
+
out[key] = value;
|
|
368
|
+
}
|
|
369
|
+
if (envelope.notice) out.notice = carrySelectors(heyToAxi(envelope.notice), carry);
|
|
370
|
+
help.push(...(Array.isArray(envelope.breadcrumbs) ? envelope.breadcrumbs : []).map((crumb) => breadcrumbHelp(crumb, carry)).filter(Boolean));
|
|
244
371
|
}
|
|
245
|
-
|
|
246
|
-
|
|
372
|
+
const more = (size || multiSize) && moreHelp(size || multiSize, { commandLine, pageFlags });
|
|
373
|
+
if (more) help.push(more);
|
|
247
374
|
if (truncated) help.push(`Run \`${commandLine} --full\` to see complete content`);
|
|
248
375
|
if (help.length) out.help = help;
|
|
249
|
-
if (
|
|
376
|
+
if (enveloped && !quiet && envelope.meta && typeof envelope.meta === "object") {
|
|
377
|
+
const meta = { ...envelope.meta };
|
|
378
|
+
for (const key of ["total_count", "pages_fetched", "next_page", "next_cursor", "has_more"]) delete meta[key];
|
|
379
|
+
if (Object.keys(meta).length) out.meta = meta;
|
|
380
|
+
}
|
|
250
381
|
return out;
|
|
251
382
|
}
|
package/src/text.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Small text helpers shared by output, help and errors.
|
|
2
2
|
|
|
3
3
|
// Point command suggestions at hey-axi instead of the wrapped CLI: "hey thread read 1"
|
|
4
|
-
// becomes "hey-axi thread read 1". Only `hey` followed by a lowercase command word
|
|
5
|
-
// rewritten, so prose ("Hey there", "hey.com") is left alone.
|
|
4
|
+
// becomes "hey-axi thread read 1". Only `hey` followed by a lowercase command word or a
|
|
5
|
+
// flag is rewritten, so prose ("Hey there", "hey.com") is left alone.
|
|
6
6
|
export function heyToAxi(text) {
|
|
7
7
|
if (typeof text !== "string") return text;
|
|
8
|
-
return text.replace(/(^|[\s`'"(])hey (?=[a-z][a-z-]
|
|
8
|
+
return text.replace(/(^|[\s`'"(])hey (?=[a-z][a-z-]*|--?[a-z])/g, "$1hey-axi ");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// Quote an argv word for display in a suggested command line.
|
package/src/version.js
CHANGED