takibibase 1.7.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.
Files changed (4) hide show
  1. package/README.md +14 -9
  2. package/SKILL.md +13 -1
  3. package/package.json +2 -2
  4. package/takibi.mjs +55 -13
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, and `notes` / `notes:append` /
27
- `notes:export` for observers), scoped to its project(s). Task boards need
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
- Copy the `<publicId>.<secret>` shown once.
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
- and notes export/keep/remove need your approval in conversation;
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`/`upload` refuse client-side with a
56
- pointer — Bearer keys can never reach account-only routes (the server
57
- answers those 401, not 403).
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
@@ -42,6 +42,15 @@ error hints.
42
42
  and `takibi tasks artifact add <taskId> <url> [--note …]`.
43
43
  - `takibi doc list | get <id> | text <id>` — metadata, then converted text.
44
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.
45
54
  - `takibi notes append --problem "…" [--tried …] [--worked …] [--failed …] [--next-time …] [--source <id>]`
46
55
  — save an end-of-run debrief or tool quirk (`--problem` or a bare
47
56
  positional; `--project <tag>` scopes it; `--source` is repeatable).
@@ -131,7 +140,10 @@ abstains honestly instead of answering halfway.
131
140
  - Notes are opt-in per profile: append, export, keep, and remove may 403
132
141
  with missing-capability on keys without the notes caps — ask the owner
133
142
  to enable them in the profile editor.
134
- - Account-only routes (upload, delete, retry, download originals, PATCH
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
135
147
  docs/projects) are never the agent's to call — ask the account owner.
136
148
 
137
149
  ## Failure table
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.7.0",
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
@@ -18,8 +18,9 @@
18
18
  * `folders` lists granted folders via GET /v1/projects/:id/folders the
19
19
  * same way (single-grant keys may omit --project; the CLI defaults it).
20
20
  * - There is no GET /v1/tasks/:id, so `tasks get` lists and filters.
21
- * - Account-only routes (doc download, uploads, deletes) answer 401, not
22
- * 403, to Bearer callers; `doc download` refuses client-side with a hint.
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.
23
24
  *
24
25
  * Exit codes: 0 ok (honest abstains included), 1 transport/API error,
25
26
  * 2 usage error.
@@ -33,10 +34,10 @@
33
34
  */
34
35
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, writeSync } from 'node:fs';
35
36
  import { homedir } from 'node:os';
36
- import { join } from 'node:path';
37
+ import { basename, join } from 'node:path';
37
38
 
38
39
  /** Baked fallback; the published package re-reads package.json next door. */
39
- const BAKED_VERSION = '1.7.0';
40
+ const BAKED_VERSION = '1.8.0';
40
41
  const CLI_INFO = (() => {
41
42
  try {
42
43
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
@@ -268,7 +269,7 @@ async function resolveProjectFlag(value, ctx) {
268
269
 
269
270
  function hintFor(status, serverMessage) {
270
271
  const msg = serverMessage || '';
271
- 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>.';
272
273
  if (status === 401) {
273
274
  if (/login required/i.test(msg)) return 'That route is account-only. Ask the account owner; never the agent.';
274
275
  if (/no longer valid/i.test(msg)) return 'That key is revoked or expired. Ask the account owner to mint a fresh one.';
@@ -279,6 +280,7 @@ function hintFor(status, serverMessage) {
279
280
  // The full-collection message is displayable verbatim (contract §11) —
280
281
  // it already prints as the error, so no generic hint on top of it.
281
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.';
282
284
  return 'Outside this key’s grant, or an owner/orchestrator-only move. Check `tasks get`, or ask the account owner.';
283
285
  }
284
286
  if (status === 404) return 'Bad id, or outside this key’s grant (the server hides the difference).';
@@ -300,12 +302,13 @@ function hintFor(status, serverMessage) {
300
302
  * One API call. Verbose logs method + URL + status only — headers (which
301
303
  * carry the key) are never logged, printed, or interpolated anywhere.
302
304
  */
303
- 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 } = {}) {
304
306
  const qs = query ? `?${query.toString()}` : '';
305
307
  const url = `${baseUrl}${path}${qs}`;
306
308
  const headers = { accept: 'application/json', 'user-agent': USER_AGENT };
307
309
  if (key) headers.authorization = `Bearer ${key}`;
308
310
  if (body !== null) headers['content-type'] = 'application/json';
311
+ // form (multipart) carries its own content-type with boundary — never set it here.
309
312
  if (verbose) err(`→ ${method} ${url}`);
310
313
  const started = Date.now();
311
314
  let res;
@@ -313,7 +316,7 @@ async function api(method, path, { query = null, body = null, key = null, baseUr
313
316
  res = await fetch(url, {
314
317
  method,
315
318
  headers,
316
- body: body === null ? undefined : JSON.stringify(body),
319
+ body: body === null ? (form ?? undefined) : JSON.stringify(body),
317
320
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
318
321
  });
319
322
  } catch (e) {
@@ -364,6 +367,7 @@ Commands:
364
367
  doc list Documents (metadata)
365
368
  doc get <id> One document's metadata
366
369
  doc text <id> Converted text of one document
370
+ doc upload <file> File one document (needs the Uploads grant)
367
371
  notes append "problem" Save an agent note (debriefs, tool quirks)
368
372
  notes list Review queue with note IDs and current versions
369
373
  notes list --all Inventory, including flagged notes
@@ -382,7 +386,7 @@ Global options (accepted before or after the command):
382
386
  then ~/.takibi/projects; single-grant keys
383
387
  may omit it entirely. Notes send the raw
384
388
  value as the project tag (no resolution).
385
- --folder <uuid> Folder scope (ask, search, doc list only).
389
+ --folder <uuid> Folder scope (ask, search, doc list, doc upload).
386
390
  --json JSON instead of human-readable output.
387
391
  --verbose Log method + URL + status to stderr (never the key).
388
392
  -h, --help This help. --version prints the CLI version.
@@ -392,6 +396,7 @@ Per-command options:
392
396
  tasks list: --all (include archived)
393
397
  tasks artifact add: --note <text> --size <bytes> --hash <hex>
394
398
  doc list: --limit <n> (1-100) doc text: --max-chars <n>
399
+ doc upload: <file> (pdf, md, txt, docx, max 10MB)
395
400
  notes append: --problem <text> (or a bare positional),
396
401
  --tried/--worked/--failed/--next-time <text>, --source <id> (repeatable),
397
402
  --run-id <id> (retry-safe within the same run)
@@ -412,9 +417,10 @@ to probe the API and \`takibi projects\` to see this key's scope
412
417
  Agents: \`takibi skill --install\` puts the takibi-use skill where
413
418
  assistants look for it.
414
419
 
415
- Reads are free. Export stamps included notes. Task claim/status/artifact writes need owner approval in
416
- conversation. Account-only routes (upload, delete, retry, download originals,
417
- PATCH docs/projects) are never the agent's to call — ask the account owner.
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.
418
424
  `;
419
425
 
420
426
  /**
@@ -780,7 +786,43 @@ async function cmdDoc(tokens, globals) {
780
786
  if (data.truncated) err('(truncated — the server caps converted text; narrow with search/ask.)');
781
787
  return;
782
788
  }
783
- if (sub === 'download' || sub === 'delete' || sub === 'retry' || sub === 'upload') {
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') {
784
826
  // Account-only by design, and unreachable with a Bearer key: refuse
785
827
  // here with the pointer instead of burning a doomed request that the
786
828
  // server would answer 401 (account routes never see Bearer callers).
@@ -788,7 +830,7 @@ async function cmdDoc(tokens, globals) {
788
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.',
789
831
  });
790
832
  }
791
- 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.`);
792
834
  }
793
835
 
794
836
  const NOTE_SUBS = ['append', 'list', 'search', 'export', 'keep', 'remove'];