takibibase 1.14.1 → 1.15.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 +6 -5
  2. package/SKILL.md +13 -3
  3. package/package.json +2 -2
  4. package/takibi.mjs +103 -22
package/README.md CHANGED
@@ -5,14 +5,14 @@ npx takibibase … # zero-install: always the latest release
5
5
  npm i -g takibibase # or install once, then run `takibi …`
6
6
  ```
7
7
 
8
- Needs Node.js 22+. This directory is also the npm package source:
8
+ Needs Node.js 20+. This directory is also the npm package source:
9
9
  `takibi.mjs` stays a single zero-dependency file so the
10
10
  `/downloads/takibi.mjs` copy and the published package never drift.
11
11
 
12
12
  Thin client-side enablement for agents. The CLI calls the project, task,
13
13
  document, and Notes API routes; its behavior is documented in `SKILL.md`.
14
14
 
15
- - `takibi.mjs` — the CLI. Single-file Node ≥ 22, zero dependencies.
15
+ - `takibi.mjs` — the CLI. Single-file Node ≥ 20, zero dependencies.
16
16
  Run it from a checkout as `node tools/takibi/takibi.mjs …`, or alias it:
17
17
  `alias takibi='node <checkout>/tools/takibi/takibi.mjs'`.
18
18
  - `SKILL.md` — the `takibi-use` skill. Short on purpose; the CLI carries
@@ -44,8 +44,10 @@ document, and Notes API routes; its behavior is documented in `SKILL.md`.
44
44
  ## Give an agent
45
45
 
46
46
  Key file in place + `takibi skill --install` (puts the `takibi-use` skill
47
- into the detected agent skills dirs; `--dir` overrides, `--force`
48
- overwrites). Zero-install alternative: `takibi skill` prints the skill —
47
+ into the detected agent skills dirs; repeat `--dir <skills-root>` per
48
+ custom box (`--dir` replaces detection unless a harness flag is also
49
+ passed), `--force` refreshes an older copy). Zero-install alternative:
50
+ `takibi skill` prints the skill —
49
51
  paste it into the agent's first prompt. Acceptance: a fresh agent asks,
50
52
  searches, lists tasks, appends/searches notes, and uses `notes list --all`
51
53
  to find the current version before curation. Reads are free; task writes,
@@ -54,7 +56,6 @@ conversation; account-only routes stay yours.
54
56
 
55
57
  ## Known edges
56
58
 
57
- - `tasks get` filters the board list client-side (no single-task GET).
58
59
  - `doc download`/`delete`/`retry` refuse client-side with a pointer —
59
60
  Bearer keys can never reach account-only routes (the server answers
60
61
  those 401, not 403). `doc upload` is the exception: keys holding the
package/SKILL.md CHANGED
@@ -66,6 +66,15 @@ error hints.
66
66
  unassigned unless `--assignee` names someone; the source keeps its
67
67
  status, holder, and history, and nothing carries over. Never move a
68
68
  done card back to To do to continue it — follow up instead.
69
+ - `takibi tasks create --title "…"` — open a fresh card on the board
70
+ (`--body`, `--due <date>`, `--assignee` with the same name rules as
71
+ follow-up). Needs the create cap; naming anyone other than yourself
72
+ needs Orchestrator. The card lands at To do, unassigned unless
73
+ `--assignee` names someone — claim it before driving it.
74
+ - `takibi tasks update <id> [--title …] [--body …] [--due …]` — amend a
75
+ card without spinning a follow-up (at least one flag; empty `--due`
76
+ clears the date). A holder edits its own card; an orchestrator may
77
+ edit any card on the board. Title/body edits need the create cap.
69
78
  - `@name` in a task names the handoff target: the FIRST explicit
70
79
  `assign to @name` (or the first @name clearly meaning "who gets the
71
80
  card next"). Later @ names describing someone else's downstream step
@@ -166,13 +175,14 @@ abstains honestly instead of answering halfway.
166
175
  - Notes export/keep/remove need owner approval already given in the
167
176
  conversation. Export only creates a draft; verify it before adding
168
177
  content to Sources.
169
- - Task claim/status/handoff/artifact/escalation/follow-up writes only with owner approval
178
+ - Task create/update/claim/status/handoff/artifact/escalation/follow-up writes only with owner approval
170
179
  already given in conversation. Claim-first: a plain key must claim a card before
171
180
  moving or touching it; only orchestrators/owners accept (`review→done`),
172
181
  assign or release cards they do not hold, or archive. A holder may hand
173
182
  its own card to another eligible profile (`tasks handoff`). Title/body edits need the create cap —
174
- claim-only keys can drive a card but cannot rewrite its text. Follow-ups need
175
- the create cap too, and any granted creator may spin one off a card it does not hold.
183
+ claim-only keys can drive a card but cannot rewrite its text. Creates and
184
+ follow-ups need the create cap too, and any granted creator may spin a
185
+ follow-up off a card it does not hold.
176
186
  - Boards need a whole-collection grant: folder-only or tag-only keys 403
177
187
  on every task route (the full-collection server message says so verbatim
178
188
  when the key holds the required cap; keys lacking the cap get the generic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.14.1",
3
+ "version": "1.15.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": {
@@ -12,7 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "engines": {
15
- "node": ">=22"
15
+ "node": ">=20"
16
16
  },
17
17
  "scripts": {
18
18
  "test": "node --test notes.test.mjs skill.test.mjs tasks.test.mjs upload.test.mjs"
package/takibi.mjs CHANGED
@@ -17,7 +17,9 @@
17
17
  * fallback and for custom aliases. UUIDs pass straight through.
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
- * - There is no GET /v1/tasks/:id, so `tasks get` lists and filters.
20
+ * - Task ids are board-wide: `tasks get` reads GET /v1/tasks/:id straight
21
+ * off (no --project needed); `tasks create` posts POST /v1/tasks and
22
+ * `tasks update` patches PATCH /v1/tasks/:id, both project-scoped.
21
23
  * - Account-only routes (doc download, deletes) answer 401, not 403, to
22
24
  * Bearer [REDACTED]; `doc download` refuses client-side with a hint. Uploads
23
25
  * accept keys holding documents:ingest (`doc upload`); keys without it 403.
@@ -37,7 +39,7 @@ import { homedir } from 'node:os';
37
39
  import { basename, join } from 'node:path';
38
40
 
39
41
  /** Baked fallback; the published package re-reads package.json next door. */
40
- const BAKED_VERSION = '1.14.1';
42
+ const BAKED_VERSION = '1.15.0';
41
43
  const CLI_INFO = (() => {
42
44
  try {
43
45
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
@@ -145,6 +147,10 @@ async function maybeNotifyUpdate({ json = false } = {}) {
145
147
  ? 'hint: download the latest takibi.mjs from https://takibibase.com/downloads/takibi.mjs'
146
148
  : 'hint: `npm i -g takibibase@latest`, or `npx takibibase@latest` for one-shot runs.',
147
149
  );
150
+ // Stale skills misroute crews (the upgrade bumps skill copy too), so
151
+ // the version nudge carries the refresh recipe. Single-file copies
152
+ // carry no SKILL.md to install — nothing to refresh there.
153
+ if (!CLI_INFO.singleFile) err('hint: then `takibi skill --install --force` refreshes agent skills (repeat `--dir <root>` per custom box).');
148
154
  } catch {
149
155
  // Offline, broken cache, slow registry: stay silent, run the command.
150
156
  }
@@ -361,8 +367,10 @@ Commands:
361
367
  search -q "..." Ranked chunks (snippets + metadata, no full text)
362
368
  tasks list Board cards for the project
363
369
  tasks get <id> One card with body + artifacts
370
+ tasks create Open a fresh card [--title/--body/--assignee/--due]
364
371
  tasks claim <id> Claim an open to-do card (atomic, one winner)
365
372
  tasks status <id> <st> Move a card: todo | in_progress | review | done [--note <text>]
373
+ tasks update <id> Amend a card's title, body, or due date [--title/--body/--due]
366
374
  tasks handoff <id> <profile> Hand a card you hold to another profile [--review --note <text>]
367
375
  tasks follow-up <id> Spin a fresh linked todo off any card [--title/--body/--assignee/--due]
368
376
  tasks escalate <id> Ask the Founder for help on a held card [--reason <text>]
@@ -406,6 +414,11 @@ Per-command options:
406
414
  tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
407
415
  tasks follow-up: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
408
416
  --assignee <name-or-uuid>, --due <date>; the source card is never modified
417
+ tasks create: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
418
+ --assignee <name-or-uuid>, --due <date>; needs the create cap,
419
+ lands at To do unassigned unless --assignee names someone
420
+ tasks update: --title/--body/--due (at least one; same limits as create;
421
+ empty --due clears the date)
409
422
  tasks get: sent-back cards carry previousReviewNote, the last handoff note
410
423
  tasks artifact add <url>: --note <text> --size <bytes> --hash <hex> (links only)
411
424
  tasks artifact add --markdown <file|->: --title <text> --note <text> (content from file or stdin -, max 50000 chars; title max 140 chars)
@@ -422,6 +435,7 @@ Per-command options:
422
435
  notes keep/remove: <id> <expected-version> from notes list --all
423
436
  projects: --add <name> <uuid>
424
437
  skill: --install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]
438
+ (repeat --dir per custom box; --dir alone skips detection)
425
439
 
426
440
  Setup: save the owner-provided key (one <publicId>.<secret> line) to
427
441
  ~/.takibi/key (chmod 600). The API defaults to Takibi's servers; only
@@ -432,7 +446,7 @@ to probe the API and \`takibi projects\` to see this key's scope
432
446
  Agents: \`takibi skill --install\` puts the takibi-use skill where
433
447
  assistants look for it.
434
448
 
435
- Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
449
+ Reads are free. Export stamps included notes. Task create/update/claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
436
450
  need owner approval in conversation (uploads also need the Uploads grant on the key).
437
451
  Account-only routes (delete, retry, download originals, PATCH docs/projects)
438
452
  are never the agent's to call — ask the account owner.
@@ -729,16 +743,47 @@ async function cmdTasks(tokens, globals) {
729
743
  if (sub === 'get') {
730
744
  const [id, extra] = rest;
731
745
  if (!id || extra) throw usageError('Usage: takibi tasks get <id>');
746
+ // Ids are board-wide: no --project to resolve (same as follow-up).
747
+ const ctx = await ctxFor(globals, { needKey: true, resolveProject: false });
748
+ const data = await api('GET', `/v1/tasks/${id}`, { ...ctx, label: 'tasks get' });
749
+ if (ctx.json) out(JSON.stringify({ task: data.task }, null, 2));
750
+ else renderTask(data.task);
751
+ return;
752
+ }
753
+ if (sub === 'create') {
754
+ const usage = 'Usage: takibi tasks create --title <text> [--body <text>] [--assignee <name-or-uuid>] [--due <date>]';
755
+ let title;
756
+ let body;
757
+ let assignee;
758
+ let due;
759
+ for (let i = 0; i < rest.length; i++) {
760
+ const f = rest[i];
761
+ if (f === '--title') title = rest[++i];
762
+ else if (f === '--body') body = rest[++i];
763
+ else if (f === '--assignee') assignee = rest[++i];
764
+ else if (f === '--due') due = rest[++i];
765
+ else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
766
+ }
767
+ // Validate everything before touching the key or network, mirroring
768
+ // the server limits so failures name the flag instead of 400ing.
769
+ if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
770
+ if (title.trim().length > 300) throw usageError('--title is max 300 chars.');
771
+ if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
772
+ if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
773
+ if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
732
774
  const ctx = await ctxFor(globals, { needKey: true });
733
- let tasks = await listTasks(ctx, false);
734
- let found = tasks.find((t) => t.id === id);
735
- if (!found) {
736
- tasks = await listTasks(ctx, true);
737
- found = tasks.find((t) => t.id === id);
775
+ if (assignee !== undefined) {
776
+ const v = String(assignee).trim();
777
+ assignee = UUID_RE.test(v) ? v : (await resolveAssignee(v, ctx, 'tasks create')).id;
738
778
  }
739
- if (!found) throw new CliError(`tasks get: No such task in this project (${id}).`, { status: 404, hint: hintFor(404, '') });
740
- if (ctx.json) out(JSON.stringify({ task: found }, null, 2));
741
- else renderTask(found);
779
+ const data = await api('POST', '/v1/tasks', {
780
+ ...ctx,
781
+ query: qparams([['projectId', ctx.projectId]]),
782
+ body: { title: title.trim(), body, assigneeProfileId: assignee, dueAt: due },
783
+ label: 'tasks create',
784
+ });
785
+ if (ctx.json) out(JSON.stringify(data, null, 2));
786
+ else renderTask(data.task);
742
787
  return;
743
788
  }
744
789
  if (sub === 'claim') {
@@ -803,6 +848,41 @@ async function cmdTasks(tokens, globals) {
803
848
  else renderTask(data.task);
804
849
  return;
805
850
  }
851
+ if (sub === 'update') {
852
+ const usage = 'Usage: takibi tasks update <id> [--title <text>] [--body <text>] [--due <date>]';
853
+ const [id, ...flagTokens] = rest;
854
+ if (!id || id.startsWith('--')) throw usageError(usage);
855
+ let title;
856
+ let body;
857
+ let due;
858
+ for (let i = 0; i < flagTokens.length; i++) {
859
+ const f = flagTokens[i];
860
+ if (f === '--title') title = flagTokens[++i];
861
+ else if (f === '--body') body = flagTokens[++i];
862
+ else if (f === '--due') due = flagTokens[++i];
863
+ else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
864
+ }
865
+ if (title === undefined && body === undefined && due === undefined) {
866
+ throw usageError(`Nothing to change — pass at least one of --title, --body, --due. ${usage}`);
867
+ }
868
+ if (title !== undefined && title.trim() === '') throw usageError('--title needs non-empty text.');
869
+ if (title !== undefined && title.trim().length > 300) throw usageError('--title is max 300 chars.');
870
+ if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
871
+ // An explicit empty --due clears the date (the server treats '' and
872
+ // null as clear). Create keeps rejecting it: omit and clear are the
873
+ // same on POST.
874
+ if (due !== undefined && due.trim() === '') due = null;
875
+ const ctx = await ctxFor(globals, { needKey: true });
876
+ const data = await api('PATCH', `/v1/tasks/${id}`, {
877
+ ...ctx,
878
+ query: qparams([['projectId', ctx.projectId]]),
879
+ body: { title: title?.trim(), body, dueAt: due },
880
+ label: 'tasks update',
881
+ });
882
+ if (ctx.json) out(JSON.stringify(data, null, 2));
883
+ else renderTask(data.task);
884
+ return;
885
+ }
806
886
  if (sub === 'artifact' || sub === 'artifacts') {
807
887
  const LINK_USAGE = 'Usage: takibi tasks artifact add <taskId> <url> [--note <t> --size <n> --hash <h>]';
808
888
  const MD_USAGE = 'Usage: takibi tasks artifact add <taskId> --markdown <file|-> [--title <t>] [--note <t>]';
@@ -945,7 +1025,7 @@ async function cmdTasks(tokens, globals) {
945
1025
  // Validate everything before touching the key or network, mirroring
946
1026
  // the server limits so failures name the flag instead of 400ing.
947
1027
  if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
948
- if (title.length > 300) throw usageError('--title is max 300 chars.');
1028
+ if (title.trim().length > 300) throw usageError('--title is max 300 chars.');
949
1029
  if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
950
1030
  if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
951
1031
  if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
@@ -970,7 +1050,7 @@ async function cmdTasks(tokens, globals) {
970
1050
  else renderTask(data.task);
971
1051
  return;
972
1052
  }
973
- throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
1053
+ throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | create | claim | status | update | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
974
1054
  }
975
1055
 
976
1056
  async function cmdDoc(tokens, globals) {
@@ -1437,7 +1517,7 @@ const SKILL_TARGETS = [
1437
1517
  { flag: '--codex', harness: '.codex', dir: '.codex/skills' },
1438
1518
  ];
1439
1519
 
1440
- const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]]';
1520
+ const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]…] (repeat --dir per custom box)';
1441
1521
 
1442
1522
  /** First-run skill nudge. Stderr only (stdout stays clean), best-effort,
1443
1523
  * never fails the command. Skipped for --json, in CI, with
@@ -1464,7 +1544,7 @@ function maybeNudgeSkill({ json = false } = {}) {
1464
1544
  // Read-only home: nudge once per process run instead of daily.
1465
1545
  }
1466
1546
  err('takibi: no takibi-use skill installed for your agents.');
1467
- err('hint: `takibi skill --install` puts it where assistants look.');
1547
+ err('hint: `takibi skill --install` puts it where assistants look (repeat `--dir <skills-root>` per custom box; `--force` refreshes).');
1468
1548
  } catch {
1469
1549
  // Nudges never fail commands.
1470
1550
  }
@@ -1473,19 +1553,20 @@ function maybeNudgeSkill({ json = false } = {}) {
1473
1553
  function cmdSkill(tokens, globals) {
1474
1554
  let install = false;
1475
1555
  let force = false;
1476
- let customRoot = null;
1556
+ const customRoots = [];
1477
1557
  const picked = new Set();
1478
1558
  for (let i = 0; i < tokens.length; i++) {
1479
1559
  const t = tokens[i];
1480
1560
  if (t === '--install') install = true;
1481
1561
  else if (t === '--force') force = true;
1482
1562
  else if (t === '--dir') {
1483
- customRoot = tokens[++i];
1484
- if (!customRoot) throw usageError('`--dir` needs a skills root. ' + SKILL_USAGE);
1563
+ const root = tokens[++i];
1564
+ if (!root) throw usageError('`--dir` needs a skills root. ' + SKILL_USAGE);
1565
+ customRoots.push(root);
1485
1566
  } else if (t === '--agents' || t === '--claude' || t === '--codex') picked.add(t);
1486
1567
  else throw usageError(`Unexpected ${JSON.stringify(t)}. ${SKILL_USAGE}`);
1487
1568
  }
1488
- if (!install && (force || picked.size > 0 || customRoot)) {
1569
+ if (!install && (force || picked.size > 0 || customRoots.length > 0)) {
1489
1570
  throw usageError(`Those flags need \`--install\`. ${SKILL_USAGE}`);
1490
1571
  }
1491
1572
  const text = skillText();
@@ -1504,17 +1585,17 @@ function cmdSkill(tokens, globals) {
1504
1585
  }
1505
1586
  const home = skillHome();
1506
1587
  let targets;
1507
- if (customRoot && picked.size === 0) targets = [];
1588
+ if (customRoots.length > 0 && picked.size === 0) targets = [];
1508
1589
  else if (picked.size === 0) {
1509
1590
  const detected = SKILL_TARGETS.filter((t) => existsSync(join(home, t.harness)));
1510
1591
  targets = detected.length > 0 ? detected : [SKILL_TARGETS[0]];
1511
1592
  } else {
1512
1593
  targets = SKILL_TARGETS.filter((t) => picked.has(t.flag));
1513
1594
  }
1514
- if (customRoot) targets = [...targets, { flag: '--dir', custom: true }];
1595
+ for (const root of customRoots) targets.push({ flag: '--dir', custom: true, root });
1515
1596
  const results = [];
1516
1597
  for (const t of targets) {
1517
- const dir = t.custom ? join(customRoot, SKILL_NAME) : join(home, t.dir, SKILL_NAME);
1598
+ const dir = t.custom ? join(t.root, SKILL_NAME) : join(home, t.dir, SKILL_NAME);
1518
1599
  const dest = join(dir, SKILL_FILE);
1519
1600
  if (existsSync(dest)) {
1520
1601
  let same = false;