takibibase 1.5.0 → 1.8.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 +14 -9
- package/SKILL.md +20 -1
- package/package.json +2 -2
- package/takibi.mjs +89 -14
package/README.md
CHANGED
|
@@ -23,10 +23,13 @@ document, and Notes API routes; its behavior is documented in `SKILL.md`.
|
|
|
23
23
|
|
|
24
24
|
1. Mint a key: in the app, Settings → Profiles → new profile with the
|
|
25
25
|
capabilities the crew needs (`search`, `ask`, `tasks`, plus `task:create`
|
|
26
|
-
for authors, `task:assign` for orchestrators,
|
|
27
|
-
`notes:export` for observers
|
|
26
|
+
for authors, `task:assign` for orchestrators, `notes` / `notes:append` /
|
|
27
|
+
`notes:export` for observers, and Uploads for pipelines that file
|
|
28
|
+
documents), scoped to its project(s). Task boards need
|
|
28
29
|
a whole-collection grant — folder-only or tag-only seats cannot use them.
|
|
29
|
-
|
|
30
|
+
Uploads land only in granted collections and pass the same secret scan,
|
|
31
|
+
quality gate, and budgets as app uploads. Copy the `<publicId>.<secret>`
|
|
32
|
+
shown once.
|
|
30
33
|
2. `mkdir -p ~/.takibi && printf '%s\n' '<publicId>.<secret>' > ~/.takibi/key && chmod 600 ~/.takibi/key`
|
|
31
34
|
3. Only for local setups: one base-URL line in `~/.takibi/config`
|
|
32
35
|
(default `https://app.takibibase.com`). Env overrides:
|
|
@@ -45,15 +48,17 @@ into the detected agent skills dirs; `--dir` overrides, `--force`
|
|
|
45
48
|
overwrites). Zero-install alternative: `takibi skill` prints the skill —
|
|
46
49
|
paste it into the agent's first prompt. Acceptance: a fresh agent asks,
|
|
47
50
|
searches, lists tasks, appends/searches notes, and uses `notes list --all`
|
|
48
|
-
to find the current version before curation. Reads are free; task writes
|
|
49
|
-
|
|
50
|
-
account-only routes stay yours.
|
|
51
|
+
to find the current version before curation. Reads are free; task writes,
|
|
52
|
+
notes export/keep/remove, and doc uploads need your approval in
|
|
53
|
+
conversation; account-only routes stay yours.
|
|
51
54
|
|
|
52
55
|
## Known edges
|
|
53
56
|
|
|
54
57
|
- `tasks get` filters the board list client-side (no single-task GET).
|
|
55
|
-
- `doc download`/`delete`/`retry
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
- `doc download`/`delete`/`retry` refuse client-side with a pointer —
|
|
59
|
+
Bearer keys can never reach account-only routes (the server answers
|
|
60
|
+
those 401, not 403). `doc upload` is the exception: keys holding the
|
|
61
|
+
Uploads grant (`documents:ingest`) may file into granted collections
|
|
62
|
+
(keys without it 403).
|
|
58
63
|
- The CLI never prints the key: not in output, errors, `--verbose`
|
|
59
64
|
(method + URL + status only), or exit traces (output is scrubbed).
|
package/SKILL.md
CHANGED
|
@@ -23,6 +23,10 @@ error hints.
|
|
|
23
23
|
- Projects: `takibi projects` lists what this key can reach. `--project`
|
|
24
24
|
takes a name or a UUID; single-grant keys may omit it.
|
|
25
25
|
`takibi projects --add <name> <uuid>` keeps local aliases for offline use.
|
|
26
|
+
- Folders: `takibi folders [--project …]` lists folder names → uuids for
|
|
27
|
+
`--folder` (only folders this key's grants touch; single-grant keys may
|
|
28
|
+
omit `--project`). Discovery flow: `projects` → `folders` → scoped
|
|
29
|
+
`ask`/`search`/`doc list`.
|
|
26
30
|
- First probe: `takibi version` (needs no key; shows build + `jev` status).
|
|
27
31
|
- Update notice: the CLI polls the npm registry once a day and nudges on
|
|
28
32
|
stderr when behind (never on `--json`).
|
|
@@ -38,6 +42,15 @@ error hints.
|
|
|
38
42
|
and `takibi tasks artifact add <taskId> <url> [--note …]`.
|
|
39
43
|
- `takibi doc list | get <id> | text <id>` — metadata, then converted text.
|
|
40
44
|
`doc download` is account-only; the CLI says so — use `doc text`.
|
|
45
|
+
- `takibi doc upload <file> [--project …] [--folder <uuid>]` — file one
|
|
46
|
+
document (pdf, md, txt, docx, max 10MB). Needs the Uploads grant
|
|
47
|
+
(`documents:ingest`) on the key plus owner approval in conversation;
|
|
48
|
+
without the grant the server 403s. Same checks as app uploads: secret
|
|
49
|
+
scan, quality gate, dedupe, daily budgets. One file per call — the
|
|
50
|
+
server 400s multi-file batches on keys. Folder-scoped keys must pass
|
|
51
|
+
`--folder` with a granted folder (unfiled needs a project-wide grant).
|
|
52
|
+
Held files report their reason — tell the owner instead of retrying in
|
|
53
|
+
a loop.
|
|
41
54
|
- `takibi notes append --problem "…" [--tried …] [--worked …] [--failed …] [--next-time …] [--source <id>]`
|
|
42
55
|
— save an end-of-run debrief or tool quirk (`--problem` or a bare
|
|
43
56
|
positional; `--project <tag>` scopes it; `--source` is repeatable).
|
|
@@ -75,6 +88,9 @@ back to `search`, try `doc text` on the hits — then either answer from
|
|
|
75
88
|
evidence or say the evidence is not there. Never fill gaps with generated
|
|
76
89
|
prose presented as sourced.
|
|
77
90
|
|
|
91
|
+
Split bundled asks: one narrow ask per field — a combined question
|
|
92
|
+
abstains honestly instead of answering halfway.
|
|
93
|
+
|
|
78
94
|
## Notes (agent scratchpad, not canon)
|
|
79
95
|
|
|
80
96
|
- When to use: end-of-run debriefs (problem, what you tried, what worked,
|
|
@@ -124,7 +140,10 @@ prose presented as sourced.
|
|
|
124
140
|
- Notes are opt-in per profile: append, export, keep, and remove may 403
|
|
125
141
|
with missing-capability on keys without the notes caps — ask the owner
|
|
126
142
|
to enable them in the profile editor.
|
|
127
|
-
-
|
|
143
|
+
- `doc upload` only with owner approval already given in conversation,
|
|
144
|
+
and only on keys holding the Uploads grant — ask the owner to enable it
|
|
145
|
+
in the profile editor when the server 403s.
|
|
146
|
+
- Account-only routes (delete, retry, download originals, PATCH
|
|
128
147
|
docs/projects) are never the agent's to call — ask the account owner.
|
|
129
148
|
|
|
130
149
|
## Failure table
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takibibase",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Thin CLI for the Takibi API: ask, search, tasks, docs, notes. Single file, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"node": ">=22"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node --test notes.test.mjs skill.test.mjs"
|
|
18
|
+
"test": "node --test notes.test.mjs skill.test.mjs upload.test.mjs"
|
|
19
19
|
},
|
|
20
20
|
"license": "SEE LICENSE IN LICENSE",
|
|
21
21
|
"repository": {
|
package/takibi.mjs
CHANGED
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
* reads the API first and --project resolves names through it. A LOCAL
|
|
16
16
|
* map (~/.takibi/projects, `name=uuid` lines) remains as the offline
|
|
17
17
|
* fallback and for custom aliases. UUIDs pass straight through.
|
|
18
|
+
* `folders` lists granted folders via GET /v1/projects/:id/folders the
|
|
19
|
+
* same way (single-grant keys may omit --project; the CLI defaults it).
|
|
18
20
|
* - There is no GET /v1/tasks/:id, so `tasks get` lists and filters.
|
|
19
|
-
* - Account-only routes (doc download,
|
|
20
|
-
*
|
|
21
|
+
* - Account-only routes (doc download, deletes) answer 401, not 403, to
|
|
22
|
+
* Bearer [REDACTED]; `doc download` refuses client-side with a hint. Uploads
|
|
23
|
+
* accept keys holding documents:ingest (`doc upload`); keys without it 403.
|
|
21
24
|
*
|
|
22
25
|
* Exit codes: 0 ok (honest abstains included), 1 transport/API error,
|
|
23
26
|
* 2 usage error.
|
|
@@ -31,10 +34,10 @@
|
|
|
31
34
|
*/
|
|
32
35
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, writeSync } from 'node:fs';
|
|
33
36
|
import { homedir } from 'node:os';
|
|
34
|
-
import { join } from 'node:path';
|
|
37
|
+
import { basename, join } from 'node:path';
|
|
35
38
|
|
|
36
39
|
/** Baked fallback; the published package re-reads package.json next door. */
|
|
37
|
-
const BAKED_VERSION = '1.
|
|
40
|
+
const BAKED_VERSION = '1.8.0';
|
|
38
41
|
const CLI_INFO = (() => {
|
|
39
42
|
try {
|
|
40
43
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
@@ -266,7 +269,7 @@ async function resolveProjectFlag(value, ctx) {
|
|
|
266
269
|
|
|
267
270
|
function hintFor(status, serverMessage) {
|
|
268
271
|
const msg = serverMessage || '';
|
|
269
|
-
if (status === 400 && /spans projects/i.test(msg)) return 'This key spans projects — pass --project <name-or-uuid>.';
|
|
272
|
+
if (status === 400 && /spans (projects|collections)/i.test(msg)) return 'This key spans projects — pass --project <name-or-uuid>.';
|
|
270
273
|
if (status === 401) {
|
|
271
274
|
if (/login required/i.test(msg)) return 'That route is account-only. Ask the account owner; never the agent.';
|
|
272
275
|
if (/no longer valid/i.test(msg)) return 'That key is revoked or expired. Ask the account owner to mint a fresh one.';
|
|
@@ -277,6 +280,7 @@ function hintFor(status, serverMessage) {
|
|
|
277
280
|
// The full-collection message is displayable verbatim (contract §11) —
|
|
278
281
|
// it already prints as the error, so no generic hint on top of it.
|
|
279
282
|
if (/full collection/i.test(msg)) return null;
|
|
283
|
+
if (/capability/i.test(msg)) return 'That profile lacks the capability for this call — ask the account owner to enable it.';
|
|
280
284
|
return 'Outside this key’s grant, or an owner/orchestrator-only move. Check `tasks get`, or ask the account owner.';
|
|
281
285
|
}
|
|
282
286
|
if (status === 404) return 'Bad id, or outside this key’s grant (the server hides the difference).';
|
|
@@ -298,12 +302,13 @@ function hintFor(status, serverMessage) {
|
|
|
298
302
|
* One API call. Verbose logs method + URL + status only — headers (which
|
|
299
303
|
* carry the key) are never logged, printed, or interpolated anywhere.
|
|
300
304
|
*/
|
|
301
|
-
async function api(method, path, { query = null, body = null, key = null, baseUrl, verbose = false, label = null } = {}) {
|
|
305
|
+
async function api(method, path, { query = null, body = null, form = null, key = null, baseUrl, verbose = false, label = null } = {}) {
|
|
302
306
|
const qs = query ? `?${query.toString()}` : '';
|
|
303
307
|
const url = `${baseUrl}${path}${qs}`;
|
|
304
308
|
const headers = { accept: 'application/json', 'user-agent': USER_AGENT };
|
|
305
309
|
if (key) headers.authorization = `Bearer ${key}`;
|
|
306
310
|
if (body !== null) headers['content-type'] = 'application/json';
|
|
311
|
+
// form (multipart) carries its own content-type with boundary — never set it here.
|
|
307
312
|
if (verbose) err(`→ ${method} ${url}`);
|
|
308
313
|
const started = Date.now();
|
|
309
314
|
let res;
|
|
@@ -311,7 +316,7 @@ async function api(method, path, { query = null, body = null, key = null, baseUr
|
|
|
311
316
|
res = await fetch(url, {
|
|
312
317
|
method,
|
|
313
318
|
headers,
|
|
314
|
-
body: body === null ? undefined : JSON.stringify(body),
|
|
319
|
+
body: body === null ? (form ?? undefined) : JSON.stringify(body),
|
|
315
320
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
316
321
|
});
|
|
317
322
|
} catch (e) {
|
|
@@ -362,6 +367,7 @@ Commands:
|
|
|
362
367
|
doc list Documents (metadata)
|
|
363
368
|
doc get <id> One document's metadata
|
|
364
369
|
doc text <id> Converted text of one document
|
|
370
|
+
doc upload <file> File one document (needs the Uploads grant)
|
|
365
371
|
notes append "problem" Save an agent note (debriefs, tool quirks)
|
|
366
372
|
notes list Review queue with note IDs and current versions
|
|
367
373
|
notes list --all Inventory, including flagged notes
|
|
@@ -370,6 +376,7 @@ Commands:
|
|
|
370
376
|
notes keep <id> <ver> Endorse a note (expected version)
|
|
371
377
|
notes remove <id> <ver> Discard a note (expected version)
|
|
372
378
|
projects Granted projects (names -> uuids for --project)
|
|
379
|
+
folders Granted folders (names -> uuids for --folder)
|
|
373
380
|
version What build is serving + jev wired|unwired (no key needed)
|
|
374
381
|
skill Print the takibi-use agent skill (pipe it to an agent)
|
|
375
382
|
skill --install Install the skill into agent skills dirs
|
|
@@ -379,7 +386,7 @@ Global options (accepted before or after the command):
|
|
|
379
386
|
then ~/.takibi/projects; single-grant keys
|
|
380
387
|
may omit it entirely. Notes send the raw
|
|
381
388
|
value as the project tag (no resolution).
|
|
382
|
-
--folder <uuid> Folder scope (ask, search, doc list
|
|
389
|
+
--folder <uuid> Folder scope (ask, search, doc list, doc upload).
|
|
383
390
|
--json JSON instead of human-readable output.
|
|
384
391
|
--verbose Log method + URL + status to stderr (never the key).
|
|
385
392
|
-h, --help This help. --version prints the CLI version.
|
|
@@ -389,6 +396,7 @@ Per-command options:
|
|
|
389
396
|
tasks list: --all (include archived)
|
|
390
397
|
tasks artifact add: --note <text> --size <bytes> --hash <hex>
|
|
391
398
|
doc list: --limit <n> (1-100) doc text: --max-chars <n>
|
|
399
|
+
doc upload: <file> (pdf, md, txt, docx, max 10MB)
|
|
392
400
|
notes append: --problem <text> (or a bare positional),
|
|
393
401
|
--tried/--worked/--failed/--next-time <text>, --source <id> (repeatable),
|
|
394
402
|
--run-id <id> (retry-safe within the same run)
|
|
@@ -409,9 +417,10 @@ to probe the API and \`takibi projects\` to see this key's scope
|
|
|
409
417
|
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
410
418
|
assistants look for it.
|
|
411
419
|
|
|
412
|
-
Reads are free. Export stamps included notes. Task claim/status/artifact writes
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
Reads are free. Export stamps included notes. Task claim/status/artifact writes and doc uploads
|
|
421
|
+
need owner approval in conversation (uploads also need the Uploads grant on the key).
|
|
422
|
+
Account-only routes (delete, retry, download originals, PATCH docs/projects)
|
|
423
|
+
are never the agent's to call — ask the account owner.
|
|
415
424
|
`;
|
|
416
425
|
|
|
417
426
|
/**
|
|
@@ -566,7 +575,7 @@ async function ctxFor(globals, { needKey, resolveProject = true }) {
|
|
|
566
575
|
const { warnings } = loadProjectMap();
|
|
567
576
|
for (const w of warnings) err(`warning: ${w}`);
|
|
568
577
|
if (globals.folder !== undefined && globals.folder !== null && globals.folder !== '' && !UUID_RE.test(globals.folder)) {
|
|
569
|
-
throw usageError(`--folder takes a folder UUID, not ${JSON.stringify(globals.folder)}. (
|
|
578
|
+
throw usageError(`--folder takes a folder UUID, not ${JSON.stringify(globals.folder)}. (Run \`takibi folders\` for this key's folder names.)`);
|
|
570
579
|
}
|
|
571
580
|
return { baseUrl, key, projectId, folder: globals.folder || undefined, json: globals.json, verbose: globals.verbose };
|
|
572
581
|
}
|
|
@@ -777,7 +786,43 @@ async function cmdDoc(tokens, globals) {
|
|
|
777
786
|
if (data.truncated) err('(truncated — the server caps converted text; narrow with search/ask.)');
|
|
778
787
|
return;
|
|
779
788
|
}
|
|
780
|
-
if (sub === '
|
|
789
|
+
if (sub === 'upload') {
|
|
790
|
+
const [path, extra] = rest;
|
|
791
|
+
if (!path || extra) throw usageError('Usage: takibi doc upload <file> [--project <name-or-uuid>] [--folder <uuid>]');
|
|
792
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
793
|
+
let bytes;
|
|
794
|
+
try {
|
|
795
|
+
bytes = readFileSync(path);
|
|
796
|
+
} catch {
|
|
797
|
+
throw usageError(`No such file: ${path}.`);
|
|
798
|
+
}
|
|
799
|
+
if (bytes.length === 0) throw usageError(`${path} is empty — nothing to upload.`);
|
|
800
|
+
if (bytes.length > 10 * 1024 * 1024) {
|
|
801
|
+
throw new CliError(`${path} is over the 10MB per-file limit.`, { hint: 'Split it and upload the parts.' });
|
|
802
|
+
}
|
|
803
|
+
const form = new FormData();
|
|
804
|
+
form.set('file', new File([bytes], basename(path)));
|
|
805
|
+
const data = await api('POST', '/v1/documents', {
|
|
806
|
+
...ctx,
|
|
807
|
+
query: qparams([
|
|
808
|
+
['projectId', ctx.projectId],
|
|
809
|
+
['folderId', ctx.folder],
|
|
810
|
+
]),
|
|
811
|
+
form,
|
|
812
|
+
label: 'doc upload',
|
|
813
|
+
});
|
|
814
|
+
if (ctx.json) {
|
|
815
|
+
out(JSON.stringify(data, null, 2));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
for (const d of data.documents ?? []) {
|
|
819
|
+
out(`uploaded ${d.id} · ${d.name} · ${d.status}${d.quarantineReason ? ` — ${d.quarantineReason}` : ''}`);
|
|
820
|
+
}
|
|
821
|
+
for (const r of data.refused ?? []) out(`refused ${r.name} — ${r.reason}${r.existingId ? ` (see ${r.existingId})` : ''}`);
|
|
822
|
+
if (!(data.documents?.length) && !(data.refused?.length)) out('Nothing uploaded.');
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (sub === 'download' || sub === 'delete' || sub === 'retry') {
|
|
781
826
|
// Account-only by design, and unreachable with a Bearer key: refuse
|
|
782
827
|
// here with the pointer instead of burning a doomed request that the
|
|
783
828
|
// server would answer 401 (account routes never see Bearer callers).
|
|
@@ -785,7 +830,7 @@ async function cmdDoc(tokens, globals) {
|
|
|
785
830
|
hint: sub === 'download' ? 'Use `takibi doc text <id>` for the converted text; ask the account owner for originals.' : 'Ask the account owner to do this in the app.',
|
|
786
831
|
});
|
|
787
832
|
}
|
|
788
|
-
throw usageError(`Unknown doc command ${JSON.stringify(sub)}. Use list | get | text.`);
|
|
833
|
+
throw usageError(`Unknown doc command ${JSON.stringify(sub)}. Use list | get | text | upload.`);
|
|
789
834
|
}
|
|
790
835
|
|
|
791
836
|
const NOTE_SUBS = ['append', 'list', 'search', 'export', 'keep', 'remove'];
|
|
@@ -1085,6 +1130,35 @@ async function cmdProjects(tokens, globals) {
|
|
|
1085
1130
|
for (const { name, id } of entries) out(`${name} → ${id}`);
|
|
1086
1131
|
}
|
|
1087
1132
|
|
|
1133
|
+
async function cmdFolders(tokens, globals) {
|
|
1134
|
+
if (tokens.length > 0) throw usageError(`Unexpected ${JSON.stringify(tokens[0])}. Usage: takibi folders [--project <name-or-uuid>]`);
|
|
1135
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
1136
|
+
let projectId = ctx.projectId;
|
|
1137
|
+
if (!projectId) {
|
|
1138
|
+
// The server cannot default a path param, so single-grant keys resolve
|
|
1139
|
+
// their one project client-side (ask/search default server-side).
|
|
1140
|
+
const data = await api('GET', '/v1/projects', { baseUrl: ctx.baseUrl, key: ctx.key, verbose: ctx.verbose, label: 'projects' });
|
|
1141
|
+
const ids = [...new Set((data?.projects ?? []).map((p) => p?.id).filter(Boolean))];
|
|
1142
|
+
// Tag-only grants list no collections, so an empty list is ambiguous:
|
|
1143
|
+
// the key may hold nothing, or hold tags. Say both, accurately.
|
|
1144
|
+
if (ids.length === 0) throw new CliError('This key lists no projects.', { hint: 'Tag-only keys cannot list collections — pass --project <uuid> explicitly (ask the account owner for the id), or grant this profile a collection or folder.' });
|
|
1145
|
+
if (ids.length > 1) throw usageError('This key spans projects — pass --project <name-or-uuid>.');
|
|
1146
|
+
projectId = ids[0];
|
|
1147
|
+
}
|
|
1148
|
+
const data = await api('GET', `/v1/projects/${projectId}/folders`, { baseUrl: ctx.baseUrl, key: ctx.key, verbose: ctx.verbose, label: 'folders' });
|
|
1149
|
+
if (ctx.json) {
|
|
1150
|
+
out(JSON.stringify(data, null, 2));
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const entries = data?.folders ?? [];
|
|
1154
|
+
if (!entries.length) {
|
|
1155
|
+
out('No folders found.');
|
|
1156
|
+
err('(This project has no folders this key can reach. Tag-only keys see only folders holding their tagged files.)');
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
for (const f of entries) out(`${f.name} → ${f.id}`);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1088
1162
|
async function cmdVersion(globals) {
|
|
1089
1163
|
const baseUrl = resolveBaseUrl();
|
|
1090
1164
|
const data = await api('GET', '/version', { baseUrl, verbose: globals.verbose, label: 'version' });
|
|
@@ -1238,6 +1312,7 @@ async function main(argv) {
|
|
|
1238
1312
|
if (cmd === 'doc' || cmd === 'docs') return cmdDoc(tokens, globals);
|
|
1239
1313
|
if (cmd === 'notes' || cmd === 'note') return cmdNotes(tokens, globals);
|
|
1240
1314
|
if (cmd === 'projects') return cmdProjects(tokens, globals);
|
|
1315
|
+
if (cmd === 'folders' || cmd === 'folder') return cmdFolders(tokens, globals);
|
|
1241
1316
|
if (cmd === 'version') return cmdVersion(globals);
|
|
1242
1317
|
if (cmd === 'skill') return cmdSkill(tokens, globals);
|
|
1243
1318
|
throw usageError(`Unknown command ${JSON.stringify(cmd)}. See \`takibi --help\`.`);
|