takibibase 1.7.0 → 1.11.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 +42 -8
- package/package.json +2 -2
- package/takibi.mjs +169 -20
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
|
@@ -38,10 +38,37 @@ error hints.
|
|
|
38
38
|
`-k 1-12`, `--json`.
|
|
39
39
|
- `takibi search -q "…"` — ranked chunks (snippets + metadata, not full
|
|
40
40
|
text). Same flags, `-k 1-20`.
|
|
41
|
-
- `takibi tasks list | get <id> | claim <id> | status <id> <todo|in_progress|review|done
|
|
42
|
-
and `takibi tasks artifact add <taskId> <url> [--note …]`.
|
|
41
|
+
- `takibi tasks list | get <id> | claim <id> | status <id> <todo|in_progress|review|done> [--note …]`
|
|
42
|
+
and `takibi tasks artifact add <taskId> <url> [--note …]`. The status
|
|
43
|
+
`--note` is the review handoff note (max 1000 chars); leaving review
|
|
44
|
+
clears it.
|
|
45
|
+
- `takibi tasks handoff <id> <profile> [--review] [--note "…"]` — hand a
|
|
46
|
+
card you hold to another profile on the same board (name or UUID; an
|
|
47
|
+
unknown name errors with the eligible names — never guess). Use
|
|
48
|
+
`--review` to move it to review in the same call and `--note` to leave
|
|
49
|
+
the review note with it: after the handoff you no longer hold the card,
|
|
50
|
+
so a follow-up `status` call would be refused. You cannot release a card
|
|
51
|
+
to nobody or hand on done/archived cards.
|
|
52
|
+
- `@name` in a task names the handoff target: when your part is
|
|
53
|
+
finished, run `tasks handoff <id> name` (usually with `--review
|
|
54
|
+
--note`). The mention itself assigns nothing — the handoff does.
|
|
55
|
+
- `takibi tasks escalate <id> --reason "…"` — ask the Founder for help
|
|
56
|
+
on a card you hold (reason required, max 500 chars; raising never
|
|
57
|
+
unassigns you). The flag clears on done, archive, or assignee change;
|
|
58
|
+
`takibi tasks deescalate <id>` retracts your flag ("never mind").
|
|
59
|
+
An orchestrator key can also clear any flag (audited) — treat that as
|
|
60
|
+
acknowledgment, not retraction.
|
|
43
61
|
- `takibi doc list | get <id> | text <id>` — metadata, then converted text.
|
|
44
62
|
`doc download` is account-only; the CLI says so — use `doc text`.
|
|
63
|
+
- `takibi doc upload <file> [--project …] [--folder <uuid>]` — file one
|
|
64
|
+
document (pdf, md, txt, docx, max 10MB). Needs the Uploads grant
|
|
65
|
+
(`documents:ingest`) on the key plus owner approval in conversation;
|
|
66
|
+
without the grant the server 403s. Same checks as app uploads: secret
|
|
67
|
+
scan, quality gate, dedupe, daily budgets. One file per call — the
|
|
68
|
+
server 400s multi-file batches on keys. Folder-scoped keys must pass
|
|
69
|
+
`--folder` with a granted folder (unfiled needs a project-wide grant).
|
|
70
|
+
Held files report their reason — tell the owner instead of retrying in
|
|
71
|
+
a loop.
|
|
45
72
|
- `takibi notes append --problem "…" [--tried …] [--worked …] [--failed …] [--next-time …] [--source <id>]`
|
|
46
73
|
— save an end-of-run debrief or tool quirk (`--problem` or a bare
|
|
47
74
|
positional; `--project <tag>` scopes it; `--source` is repeatable).
|
|
@@ -119,10 +146,11 @@ abstains honestly instead of answering halfway.
|
|
|
119
146
|
- Notes export/keep/remove need owner approval already given in the
|
|
120
147
|
conversation. Export only creates a draft; verify it before adding
|
|
121
148
|
content to Sources.
|
|
122
|
-
- Task claim/status/artifact writes only with owner approval
|
|
123
|
-
given in conversation. Claim-first: a plain key must claim a card before
|
|
149
|
+
- Task claim/status/handoff/artifact/escalation writes only with owner approval
|
|
150
|
+
already given in conversation. Claim-first: a plain key must claim a card before
|
|
124
151
|
moving or touching it; only orchestrators/owners accept (`review→done`),
|
|
125
|
-
assign
|
|
152
|
+
assign or release cards they do not hold, or archive. A holder may hand
|
|
153
|
+
its own card to another eligible profile (`tasks handoff`). Title/body edits need the create cap —
|
|
126
154
|
claim-only keys can drive a card but cannot rewrite its text.
|
|
127
155
|
- Boards need a whole-collection grant: folder-only or tag-only keys 403
|
|
128
156
|
on every task route (the full-collection server message says so verbatim
|
|
@@ -131,7 +159,10 @@ abstains honestly instead of answering halfway.
|
|
|
131
159
|
- Notes are opt-in per profile: append, export, keep, and remove may 403
|
|
132
160
|
with missing-capability on keys without the notes caps — ask the owner
|
|
133
161
|
to enable them in the profile editor.
|
|
134
|
-
-
|
|
162
|
+
- `doc upload` only with owner approval already given in conversation,
|
|
163
|
+
and only on keys holding the Uploads grant — ask the owner to enable it
|
|
164
|
+
in the profile editor when the server 403s.
|
|
165
|
+
- Account-only routes (delete, retry, download originals, PATCH
|
|
135
166
|
docs/projects) are never the agent's to call — ask the account owner.
|
|
136
167
|
|
|
137
168
|
## Failure table
|
|
@@ -141,8 +172,11 @@ abstains honestly instead of answering halfway.
|
|
|
141
172
|
capability (notes are opt-in), or folder-/tag-only key on a board route.
|
|
142
173
|
404: bad id (the server hides grant gaps as 404 too).
|
|
143
174
|
- 409 on claim: someone already holds the card — the message names them.
|
|
175
|
+
- Handoff 403: you do not hold the card (claim it, or ask an orchestrator)
|
|
176
|
+
or you tried to release it to nobody. 422: the target lacks a
|
|
177
|
+
whole-collection grant on this board, or the card is done/archived.
|
|
144
178
|
- 422 SECRET_BLOCKED: the secret filter fired — it covers task
|
|
145
|
-
title/body/blockedReason/artifact text as
|
|
146
|
-
tokens, and credentials and retry.
|
|
179
|
+
title/body/blockedReason/reviewNote/escalationReason/artifact text as
|
|
180
|
+
well as notes. Strip keys, tokens, and credentials and retry.
|
|
147
181
|
- 429: minute throttle (slow down) or daily budget spent (resets tomorrow).
|
|
148
182
|
- Cannot-reach errors: check the network and service status; `takibi version` probes it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takibibase",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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 tasks.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,
|
|
22
|
-
*
|
|
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.
|
|
40
|
+
const BAKED_VERSION = '1.11.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) {
|
|
@@ -359,11 +362,15 @@ Commands:
|
|
|
359
362
|
tasks list Board cards for the project
|
|
360
363
|
tasks get <id> One card with body + artifacts
|
|
361
364
|
tasks claim <id> Claim an open to-do card (atomic, one winner)
|
|
362
|
-
tasks status <id> <st> Move a card: todo | in_progress | review | done
|
|
365
|
+
tasks status <id> <st> Move a card: todo | in_progress | review | done [--note <text>]
|
|
366
|
+
tasks handoff <id> <profile> Hand a card you hold to another profile [--review --note <text>]
|
|
367
|
+
tasks escalate <id> Ask the Founder for help on a held card [--reason <text>]
|
|
368
|
+
tasks deescalate <id> Clear your escalation ("never mind")
|
|
363
369
|
tasks artifact add <taskId> <url> Attach a link-only artifact
|
|
364
370
|
doc list Documents (metadata)
|
|
365
371
|
doc get <id> One document's metadata
|
|
366
372
|
doc text <id> Converted text of one document
|
|
373
|
+
doc upload <file> File one document (needs the Uploads grant)
|
|
367
374
|
notes append "problem" Save an agent note (debriefs, tool quirks)
|
|
368
375
|
notes list Review queue with note IDs and current versions
|
|
369
376
|
notes list --all Inventory, including flagged notes
|
|
@@ -382,7 +389,7 @@ Global options (accepted before or after the command):
|
|
|
382
389
|
then ~/.takibi/projects; single-grant keys
|
|
383
390
|
may omit it entirely. Notes send the raw
|
|
384
391
|
value as the project tag (no resolution).
|
|
385
|
-
--folder <uuid> Folder scope (ask, search, doc list
|
|
392
|
+
--folder <uuid> Folder scope (ask, search, doc list, doc upload).
|
|
386
393
|
--json JSON instead of human-readable output.
|
|
387
394
|
--verbose Log method + URL + status to stderr (never the key).
|
|
388
395
|
-h, --help This help. --version prints the CLI version.
|
|
@@ -390,8 +397,13 @@ Global options (accepted before or after the command):
|
|
|
390
397
|
Per-command options:
|
|
391
398
|
ask/search: -q, --query <text> (or a bare positional), -k <n> (ask 1-12, search 1-20)
|
|
392
399
|
tasks list: --all (include archived)
|
|
400
|
+
tasks status: --note <text> (review handoff note; cleared when the card leaves review)
|
|
401
|
+
tasks handoff: <profile> is a name or UUID on this board; --review moves the card to
|
|
402
|
+
review in the same call, --note <text> leaves the review note with it
|
|
403
|
+
tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
|
|
393
404
|
tasks artifact add: --note <text> --size <bytes> --hash <hex>
|
|
394
405
|
doc list: --limit <n> (1-100) doc text: --max-chars <n>
|
|
406
|
+
doc upload: <file> (pdf, md, txt, docx, max 10MB)
|
|
395
407
|
notes append: --problem <text> (or a bare positional),
|
|
396
408
|
--tried/--worked/--failed/--next-time <text>, --source <id> (repeatable),
|
|
397
409
|
--run-id <id> (retry-safe within the same run)
|
|
@@ -412,9 +424,10 @@ to probe the API and \`takibi projects\` to see this key's scope
|
|
|
412
424
|
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
413
425
|
assistants look for it.
|
|
414
426
|
|
|
415
|
-
Reads are free. Export stamps included notes. Task claim/status/artifact writes
|
|
416
|
-
|
|
417
|
-
|
|
427
|
+
Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation writes and doc uploads
|
|
428
|
+
need owner approval in conversation (uploads also need the Uploads grant on the key).
|
|
429
|
+
Account-only routes (delete, retry, download originals, PATCH docs/projects)
|
|
430
|
+
are never the agent's to call — ask the account owner.
|
|
418
431
|
`;
|
|
419
432
|
|
|
420
433
|
/**
|
|
@@ -451,9 +464,9 @@ function parseArgv(argv) {
|
|
|
451
464
|
i = ni;
|
|
452
465
|
}
|
|
453
466
|
else if (a.startsWith('--folder=')) globals.folder = a.slice('--folder='.length);
|
|
454
|
-
else if (a === '--query' || a === '-q' || a === '--q' || a === '-k' || a === '--limit' || a === '--max-chars' || a === '--all' || a === '--add' || a === '--note' || a === '--size' || a === '--hash' || a === '--problem' || a === '--tried' || a === '--worked' || a === '--failed' || a === '--next-time' || a === '--source' || a === '--since' || a === '--run-id' || a === '--install' || a === '--force' || a === '--agents' || a === '--claude' || a === '--codex' || a === '--dir') {
|
|
467
|
+
else if (a === '--query' || a === '-q' || a === '--q' || a === '-k' || a === '--limit' || a === '--max-chars' || a === '--all' || a === '--review' || a === '--add' || a === '--note' || a === '--reason' || a === '--size' || a === '--hash' || a === '--problem' || a === '--tried' || a === '--worked' || a === '--failed' || a === '--next-time' || a === '--source' || a === '--since' || a === '--run-id' || a === '--install' || a === '--force' || a === '--agents' || a === '--claude' || a === '--codex' || a === '--dir') {
|
|
455
468
|
rest.push(a);
|
|
456
|
-
if (a !== '--all' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
|
|
469
|
+
if (a !== '--all' && a !== '--review' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
|
|
457
470
|
const [v, ni] = takeValue(a, i);
|
|
458
471
|
rest.push(v);
|
|
459
472
|
i = ni;
|
|
@@ -527,6 +540,7 @@ function renderSearch(results, q) {
|
|
|
527
540
|
|
|
528
541
|
const taskLine = (t) => {
|
|
529
542
|
const bits = [t.id, `· ${t.status}`, `· ${t.title}`, `· ${t.assigneeName ?? 'unassigned'}`];
|
|
543
|
+
if (t.escalationReason) bits.push(`· escalated: ${t.escalationReason}`);
|
|
530
544
|
if (t.blocked) bits.push(`· blocked: ${t.blockedReason || 'no reason given'}`);
|
|
531
545
|
if (t.artifacts?.length) bits.push(`· ${t.artifacts.length} artifact${t.artifacts.length === 1 ? '' : 's'}`);
|
|
532
546
|
if (t.archivedAt) bits.push('· archived');
|
|
@@ -536,6 +550,7 @@ const taskLine = (t) => {
|
|
|
536
550
|
|
|
537
551
|
function renderTask(t) {
|
|
538
552
|
out(taskLine(t));
|
|
553
|
+
if (t.reviewNote) out(`review: ${t.reviewNote}`);
|
|
539
554
|
if (t.body) out(`\n${t.body}`);
|
|
540
555
|
if (t.artifacts?.length) {
|
|
541
556
|
out('');
|
|
@@ -628,6 +643,32 @@ async function listTasks(ctx, includeArchived) {
|
|
|
628
643
|
return data.tasks ?? [];
|
|
629
644
|
}
|
|
630
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Handoff target → {id}. UUIDs pass straight through (the server checks
|
|
648
|
+
* eligibility); names resolve exactly against GET /v1/tasks/assignees for
|
|
649
|
+
* this board — same exact-match-or-error rule as --project names.
|
|
650
|
+
*/
|
|
651
|
+
async function resolveAssignee(value, ctx) {
|
|
652
|
+
const v = String(value).trim();
|
|
653
|
+
if (UUID_RE.test(v)) return { id: v };
|
|
654
|
+
const data = await api('GET', '/v1/tasks/assignees', {
|
|
655
|
+
...ctx,
|
|
656
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
657
|
+
label: 'tasks handoff',
|
|
658
|
+
});
|
|
659
|
+
const eligible = data?.assignees ?? [];
|
|
660
|
+
const matches = eligible.filter((a) => a.name === v);
|
|
661
|
+
if (matches.length > 1) {
|
|
662
|
+
throw new CliError(`More than one profile on this board is named ${JSON.stringify(v)}.`, {
|
|
663
|
+
hint: `Pass the profile UUID instead: ${matches.map((a) => a.id).join(', ')}.`,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (matches[0]) return matches[0];
|
|
667
|
+
throw new CliError(`No profile named ${JSON.stringify(v)} can take cards on this board.`, {
|
|
668
|
+
hint: eligible.length > 0 ? `Eligible: ${eligible.map((a) => a.name).join(', ')}.` : 'No profile holds this whole board yet — ask the account owner.',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
631
672
|
async function cmdTasks(tokens, globals) {
|
|
632
673
|
if (globals.folder) throw usageError('--folder does not apply to tasks: cards are project-wide, folder grants stay a KB-read concept.');
|
|
633
674
|
const [sub, ...rest] = tokens;
|
|
@@ -672,20 +713,58 @@ async function cmdTasks(tokens, globals) {
|
|
|
672
713
|
return;
|
|
673
714
|
}
|
|
674
715
|
if (sub === 'status') {
|
|
675
|
-
const [id, to,
|
|
676
|
-
if (!id || !to
|
|
716
|
+
const [id, to, ...flagTokens] = rest;
|
|
717
|
+
if (!id || !to) throw usageError('Usage: takibi tasks status <id> <todo|in_progress|review|done> [--note <text>]');
|
|
677
718
|
if (!TASK_STATUSES.includes(to)) throw usageError(`Status is one of ${TASK_STATUSES.join(' | ')}, not ${JSON.stringify(to)}.`);
|
|
719
|
+
let note;
|
|
720
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
721
|
+
const f = flagTokens[i];
|
|
722
|
+
if (f === '--note') note = flagTokens[++i];
|
|
723
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. Usage: takibi tasks status <id> <todo|in_progress|review|done> [--note <text>]`);
|
|
724
|
+
}
|
|
725
|
+
if (note !== undefined && note === '') throw usageError('--note needs non-empty text.');
|
|
678
726
|
const ctx = await ctxFor(globals, { needKey: true });
|
|
679
727
|
const data = await api('PATCH', `/v1/tasks/${id}`, {
|
|
680
728
|
...ctx,
|
|
681
729
|
query: qparams([['projectId', ctx.projectId]]),
|
|
682
|
-
body: { status: to },
|
|
730
|
+
body: { status: to, reviewNote: note },
|
|
683
731
|
label: 'tasks status',
|
|
684
732
|
});
|
|
685
733
|
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
686
734
|
else renderTask(data.task);
|
|
687
735
|
return;
|
|
688
736
|
}
|
|
737
|
+
if (sub === 'handoff') {
|
|
738
|
+
const usage = 'Usage: takibi tasks handoff <id> <profile-name-or-uuid> [--review] [--note <text>]';
|
|
739
|
+
const [id, target, ...flagTokens] = rest;
|
|
740
|
+
if (!id || !target || target.startsWith('--')) throw usageError(usage);
|
|
741
|
+
let review = false;
|
|
742
|
+
let note;
|
|
743
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
744
|
+
const f = flagTokens[i];
|
|
745
|
+
if (f === '--review') review = true;
|
|
746
|
+
else if (f === '--note') note = flagTokens[++i];
|
|
747
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
748
|
+
}
|
|
749
|
+
if (note !== undefined && note === '') throw usageError('--note needs non-empty text.');
|
|
750
|
+
// After the handoff this key no longer holds the card, so the note can
|
|
751
|
+
// only ride the same call — and only onto a card moving to review.
|
|
752
|
+
if (note !== undefined && !review) throw usageError('--note rides with --review: the handoff and the review note go in one call.');
|
|
753
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
754
|
+
const assignee = await resolveAssignee(target, ctx);
|
|
755
|
+
const body = { assigneeProfileId: assignee.id };
|
|
756
|
+
if (review) body.status = 'review';
|
|
757
|
+
if (note !== undefined) body.reviewNote = note;
|
|
758
|
+
const data = await api('PATCH', `/v1/tasks/${id}`, {
|
|
759
|
+
...ctx,
|
|
760
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
761
|
+
body,
|
|
762
|
+
label: 'tasks handoff',
|
|
763
|
+
});
|
|
764
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
765
|
+
else renderTask(data.task);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
689
768
|
if (sub === 'artifact' || sub === 'artifacts') {
|
|
690
769
|
const [verb, taskId, url, ...flagTokens] = rest;
|
|
691
770
|
if (verb !== 'add' || !taskId || !url) throw usageError('Usage: takibi tasks artifact add <taskId> <url> [--note <t> --size <n> --hash <h>]');
|
|
@@ -715,7 +794,41 @@ async function cmdTasks(tokens, globals) {
|
|
|
715
794
|
}
|
|
716
795
|
return;
|
|
717
796
|
}
|
|
718
|
-
|
|
797
|
+
if (sub === 'escalate') {
|
|
798
|
+
const [id, ...flagTokens] = rest;
|
|
799
|
+
if (!id) throw usageError('Usage: takibi tasks escalate <id> --reason <text>');
|
|
800
|
+
let reason;
|
|
801
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
802
|
+
const f = flagTokens[i];
|
|
803
|
+
if (f === '--reason') reason = flagTokens[++i];
|
|
804
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. Usage: takibi tasks escalate <id> --reason <text>`);
|
|
805
|
+
}
|
|
806
|
+
if (reason === undefined || reason === '') throw usageError('--reason needs non-empty text: what do you need from the Founder?');
|
|
807
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
808
|
+
const data = await api('POST', `/v1/tasks/${id}/escalate`, {
|
|
809
|
+
...ctx,
|
|
810
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
811
|
+
body: { reason },
|
|
812
|
+
label: 'tasks escalate',
|
|
813
|
+
});
|
|
814
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
815
|
+
else renderTask(data.task);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (sub === 'deescalate') {
|
|
819
|
+
const [id, extra] = rest;
|
|
820
|
+
if (!id || extra) throw usageError('Usage: takibi tasks deescalate <id>');
|
|
821
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
822
|
+
const data = await api('DELETE', `/v1/tasks/${id}/escalation`, {
|
|
823
|
+
...ctx,
|
|
824
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
825
|
+
label: 'tasks deescalate',
|
|
826
|
+
});
|
|
827
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
828
|
+
else renderTask(data.task);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | escalate | deescalate | artifact add.`);
|
|
719
832
|
}
|
|
720
833
|
|
|
721
834
|
async function cmdDoc(tokens, globals) {
|
|
@@ -780,7 +893,43 @@ async function cmdDoc(tokens, globals) {
|
|
|
780
893
|
if (data.truncated) err('(truncated — the server caps converted text; narrow with search/ask.)');
|
|
781
894
|
return;
|
|
782
895
|
}
|
|
783
|
-
if (sub === '
|
|
896
|
+
if (sub === 'upload') {
|
|
897
|
+
const [path, extra] = rest;
|
|
898
|
+
if (!path || extra) throw usageError('Usage: takibi doc upload <file> [--project <name-or-uuid>] [--folder <uuid>]');
|
|
899
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
900
|
+
let bytes;
|
|
901
|
+
try {
|
|
902
|
+
bytes = readFileSync(path);
|
|
903
|
+
} catch {
|
|
904
|
+
throw usageError(`No such file: ${path}.`);
|
|
905
|
+
}
|
|
906
|
+
if (bytes.length === 0) throw usageError(`${path} is empty — nothing to upload.`);
|
|
907
|
+
if (bytes.length > 10 * 1024 * 1024) {
|
|
908
|
+
throw new CliError(`${path} is over the 10MB per-file limit.`, { hint: 'Split it and upload the parts.' });
|
|
909
|
+
}
|
|
910
|
+
const form = new FormData();
|
|
911
|
+
form.set('file', new File([bytes], basename(path)));
|
|
912
|
+
const data = await api('POST', '/v1/documents', {
|
|
913
|
+
...ctx,
|
|
914
|
+
query: qparams([
|
|
915
|
+
['projectId', ctx.projectId],
|
|
916
|
+
['folderId', ctx.folder],
|
|
917
|
+
]),
|
|
918
|
+
form,
|
|
919
|
+
label: 'doc upload',
|
|
920
|
+
});
|
|
921
|
+
if (ctx.json) {
|
|
922
|
+
out(JSON.stringify(data, null, 2));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
for (const d of data.documents ?? []) {
|
|
926
|
+
out(`uploaded ${d.id} · ${d.name} · ${d.status}${d.quarantineReason ? ` — ${d.quarantineReason}` : ''}`);
|
|
927
|
+
}
|
|
928
|
+
for (const r of data.refused ?? []) out(`refused ${r.name} — ${r.reason}${r.existingId ? ` (see ${r.existingId})` : ''}`);
|
|
929
|
+
if (!(data.documents?.length) && !(data.refused?.length)) out('Nothing uploaded.');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (sub === 'download' || sub === 'delete' || sub === 'retry') {
|
|
784
933
|
// Account-only by design, and unreachable with a Bearer key: refuse
|
|
785
934
|
// here with the pointer instead of burning a doomed request that the
|
|
786
935
|
// server would answer 401 (account routes never see Bearer callers).
|
|
@@ -788,7 +937,7 @@ async function cmdDoc(tokens, globals) {
|
|
|
788
937
|
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
938
|
});
|
|
790
939
|
}
|
|
791
|
-
throw usageError(`Unknown doc command ${JSON.stringify(sub)}. Use list | get | text.`);
|
|
940
|
+
throw usageError(`Unknown doc command ${JSON.stringify(sub)}. Use list | get | text | upload.`);
|
|
792
941
|
}
|
|
793
942
|
|
|
794
943
|
const NOTE_SUBS = ['append', 'list', 'search', 'export', 'keep', 'remove'];
|