takibibase 1.8.0 → 1.13.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/SKILL.md +38 -7
- package/package.json +2 -2
- package/takibi.mjs +211 -15
package/SKILL.md
CHANGED
|
@@ -38,8 +38,35 @@ 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. A sent-back card keeps the last handoff as `previousReviewNote`
|
|
45
|
+
— read it for the last round's feedback without it being pasted into the body.
|
|
46
|
+
- `takibi tasks artifact add <taskId> --markdown <file|-> [--title …] [--note …]`
|
|
47
|
+
— attach an inline markdown note (content from a file or stdin `-`,
|
|
48
|
+
max 50000 chars; title max 140 chars);
|
|
49
|
+
`takibi tasks artifact show <taskId> <artifactId>` prints one note's raw
|
|
50
|
+
markdown. Prefer inline markdown artifacts for text deliverables —
|
|
51
|
+
briefs, drafts, debriefs; external Drive/URLs only for sheets, slides,
|
|
52
|
+
or binary files.
|
|
53
|
+
- `takibi tasks handoff <id> <profile> [--review] [--note "…"]` — hand a
|
|
54
|
+
card you hold to another profile on the same board (name or UUID; an
|
|
55
|
+
unknown name errors with the eligible names — never guess). Use
|
|
56
|
+
`--review` to submit your work for review in the same call (the inbox
|
|
57
|
+
credits you as the finisher) and `--note` to leave the review note with
|
|
58
|
+
it: after the handoff you no longer hold the card, so a follow-up
|
|
59
|
+
`status` call would be refused. You cannot release a card to nobody or
|
|
60
|
+
hand on done/archived cards.
|
|
61
|
+
- `@name` in a task names the handoff target: when your part is
|
|
62
|
+
finished, run `tasks handoff <id> name` (usually with `--review
|
|
63
|
+
--note`). The mention itself assigns nothing — the handoff does.
|
|
64
|
+
- `takibi tasks escalate <id> --reason "…"` — ask the Founder for help
|
|
65
|
+
on a card you hold (reason required, max 500 chars; raising never
|
|
66
|
+
unassigns you). The flag clears on done, archive, or assignee change;
|
|
67
|
+
`takibi tasks deescalate <id>` retracts your flag ("never mind").
|
|
68
|
+
An orchestrator key can also clear any flag (audited) — treat that as
|
|
69
|
+
acknowledgment, not retraction.
|
|
43
70
|
- `takibi doc list | get <id> | text <id>` — metadata, then converted text.
|
|
44
71
|
`doc download` is account-only; the CLI says so — use `doc text`.
|
|
45
72
|
- `takibi doc upload <file> [--project …] [--folder <uuid>]` — file one
|
|
@@ -128,10 +155,11 @@ abstains honestly instead of answering halfway.
|
|
|
128
155
|
- Notes export/keep/remove need owner approval already given in the
|
|
129
156
|
conversation. Export only creates a draft; verify it before adding
|
|
130
157
|
content to Sources.
|
|
131
|
-
- Task claim/status/artifact writes only with owner approval
|
|
132
|
-
given in conversation. Claim-first: a plain key must claim a card before
|
|
158
|
+
- Task claim/status/handoff/artifact/escalation writes only with owner approval
|
|
159
|
+
already given in conversation. Claim-first: a plain key must claim a card before
|
|
133
160
|
moving or touching it; only orchestrators/owners accept (`review→done`),
|
|
134
|
-
assign
|
|
161
|
+
assign or release cards they do not hold, or archive. A holder may hand
|
|
162
|
+
its own card to another eligible profile (`tasks handoff`). Title/body edits need the create cap —
|
|
135
163
|
claim-only keys can drive a card but cannot rewrite its text.
|
|
136
164
|
- Boards need a whole-collection grant: folder-only or tag-only keys 403
|
|
137
165
|
on every task route (the full-collection server message says so verbatim
|
|
@@ -153,8 +181,11 @@ abstains honestly instead of answering halfway.
|
|
|
153
181
|
capability (notes are opt-in), or folder-/tag-only key on a board route.
|
|
154
182
|
404: bad id (the server hides grant gaps as 404 too).
|
|
155
183
|
- 409 on claim: someone already holds the card — the message names them.
|
|
184
|
+
- Handoff 403: you do not hold the card (claim it, or ask an orchestrator)
|
|
185
|
+
or you tried to release it to nobody. 422: the target lacks a
|
|
186
|
+
whole-collection grant on this board, or the card is done/archived.
|
|
156
187
|
- 422 SECRET_BLOCKED: the secret filter fired — it covers task
|
|
157
|
-
title/body/blockedReason/artifact text as
|
|
158
|
-
tokens, and credentials and retry.
|
|
188
|
+
title/body/blockedReason/reviewNote/escalationReason/artifact text as
|
|
189
|
+
well as notes. Strip keys, tokens, and credentials and retry.
|
|
159
190
|
- 429: minute throttle (slow down) or daily budget spent (resets tomorrow).
|
|
160
191
|
- 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.13.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 upload.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
|
@@ -37,7 +37,7 @@ import { homedir } from 'node:os';
|
|
|
37
37
|
import { basename, join } from 'node:path';
|
|
38
38
|
|
|
39
39
|
/** Baked fallback; the published package re-reads package.json next door. */
|
|
40
|
-
const BAKED_VERSION = '1.
|
|
40
|
+
const BAKED_VERSION = '1.13.0';
|
|
41
41
|
const CLI_INFO = (() => {
|
|
42
42
|
try {
|
|
43
43
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
@@ -362,8 +362,13 @@ Commands:
|
|
|
362
362
|
tasks list Board cards for the project
|
|
363
363
|
tasks get <id> One card with body + artifacts
|
|
364
364
|
tasks claim <id> Claim an open to-do card (atomic, one winner)
|
|
365
|
-
tasks status <id> <st> Move a card: todo | in_progress | review | done
|
|
366
|
-
tasks
|
|
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")
|
|
369
|
+
tasks artifact add <taskId> <url> Attach a link artifact [--note/--size/--hash]
|
|
370
|
+
tasks artifact add <taskId> --markdown <file|-> Attach an inline markdown note [--title/--note]
|
|
371
|
+
tasks artifact show <taskId> <artifactId> Print one markdown note's content
|
|
367
372
|
doc list Documents (metadata)
|
|
368
373
|
doc get <id> One document's metadata
|
|
369
374
|
doc text <id> Converted text of one document
|
|
@@ -394,7 +399,14 @@ Global options (accepted before or after the command):
|
|
|
394
399
|
Per-command options:
|
|
395
400
|
ask/search: -q, --query <text> (or a bare positional), -k <n> (ask 1-12, search 1-20)
|
|
396
401
|
tasks list: --all (include archived)
|
|
397
|
-
tasks
|
|
402
|
+
tasks status: --note <text> (review handoff note; cleared when the card leaves review)
|
|
403
|
+
tasks handoff: <profile> is a name or UUID on this board; --review submits your work
|
|
404
|
+
for review in the same call, --note <text> leaves the review note with it
|
|
405
|
+
tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
|
|
406
|
+
tasks get: sent-back cards carry previousReviewNote, the last handoff note
|
|
407
|
+
tasks artifact add <url>: --note <text> --size <bytes> --hash <hex> (links only)
|
|
408
|
+
tasks artifact add --markdown <file|->: --title <text> --note <text> (content from file or stdin -, max 50000 chars; title max 140 chars)
|
|
409
|
+
tasks artifact show: prints the raw markdown (links carry no content)
|
|
398
410
|
doc list: --limit <n> (1-100) doc text: --max-chars <n>
|
|
399
411
|
doc upload: <file> (pdf, md, txt, docx, max 10MB)
|
|
400
412
|
notes append: --problem <text> (or a bare positional),
|
|
@@ -417,7 +429,7 @@ to probe the API and \`takibi projects\` to see this key's scope
|
|
|
417
429
|
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
418
430
|
assistants look for it.
|
|
419
431
|
|
|
420
|
-
Reads are free. Export stamps included notes. Task claim/status/artifact writes and doc uploads
|
|
432
|
+
Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation writes and doc uploads
|
|
421
433
|
need owner approval in conversation (uploads also need the Uploads grant on the key).
|
|
422
434
|
Account-only routes (delete, retry, download originals, PATCH docs/projects)
|
|
423
435
|
are never the agent's to call — ask the account owner.
|
|
@@ -457,9 +469,9 @@ function parseArgv(argv) {
|
|
|
457
469
|
i = ni;
|
|
458
470
|
}
|
|
459
471
|
else if (a.startsWith('--folder=')) globals.folder = a.slice('--folder='.length);
|
|
460
|
-
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') {
|
|
472
|
+
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 === '--markdown' || a === '--title' || 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') {
|
|
461
473
|
rest.push(a);
|
|
462
|
-
if (a !== '--all' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
|
|
474
|
+
if (a !== '--all' && a !== '--review' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
|
|
463
475
|
const [v, ni] = takeValue(a, i);
|
|
464
476
|
rest.push(v);
|
|
465
477
|
i = ni;
|
|
@@ -533,6 +545,7 @@ function renderSearch(results, q) {
|
|
|
533
545
|
|
|
534
546
|
const taskLine = (t) => {
|
|
535
547
|
const bits = [t.id, `· ${t.status}`, `· ${t.title}`, `· ${t.assigneeName ?? 'unassigned'}`];
|
|
548
|
+
if (t.escalationReason) bits.push(`· escalated: ${t.escalationReason}`);
|
|
536
549
|
if (t.blocked) bits.push(`· blocked: ${t.blockedReason || 'no reason given'}`);
|
|
537
550
|
if (t.artifacts?.length) bits.push(`· ${t.artifacts.length} artifact${t.artifacts.length === 1 ? '' : 's'}`);
|
|
538
551
|
if (t.archivedAt) bits.push('· archived');
|
|
@@ -540,12 +553,37 @@ const taskLine = (t) => {
|
|
|
540
553
|
return bits.join(' ');
|
|
541
554
|
};
|
|
542
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Display title for a task artifact: the explicit title when set, else
|
|
558
|
+
* the content's first ATX heading, else a fixed fallback. Local duplicate
|
|
559
|
+
* of artifactTitle in apps/web/src/tasks/artifacts.ts — this file ships
|
|
560
|
+
* standalone and cannot import TS.
|
|
561
|
+
*/
|
|
562
|
+
function artifactTitle(title, content) {
|
|
563
|
+
const explicit = title?.trim() ?? '';
|
|
564
|
+
if (explicit !== '') return explicit;
|
|
565
|
+
for (const line of (content ?? '').split('\n')) {
|
|
566
|
+
const text = /^#{1,6}\s+(.*)$/.exec(line.trim())?.[1] ?? '';
|
|
567
|
+
if (text !== '') return text;
|
|
568
|
+
}
|
|
569
|
+
return 'Untitled note';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** One artifact row: links print the URL exactly as before; markdown
|
|
573
|
+
* notes (null URL) print the display title — never the literal null. */
|
|
574
|
+
const artifactRow = (a) =>
|
|
575
|
+
a.kind === 'markdown' || a.url == null
|
|
576
|
+
? `${artifactTitle(a.title, a.content ?? null)}${a.note ? ` — ${a.note}` : ''} [${a.id}]`
|
|
577
|
+
: `${a.url}${a.note ? ` — ${a.note}` : ''} [${a.id}]`;
|
|
578
|
+
|
|
543
579
|
function renderTask(t) {
|
|
544
580
|
out(taskLine(t));
|
|
581
|
+
if (t.reviewNote) out(`review: ${t.reviewNote}`);
|
|
582
|
+
if (t.previousReviewNote) out(`previous review note: ${t.previousReviewNote}`);
|
|
545
583
|
if (t.body) out(`\n${t.body}`);
|
|
546
584
|
if (t.artifacts?.length) {
|
|
547
585
|
out('');
|
|
548
|
-
for (const a of t.artifacts) out(` - ${a
|
|
586
|
+
for (const a of t.artifacts) out(` - ${artifactRow(a)}`);
|
|
549
587
|
}
|
|
550
588
|
err(`created ${t.createdAt} · updated ${t.updatedAt}`);
|
|
551
589
|
}
|
|
@@ -634,6 +672,32 @@ async function listTasks(ctx, includeArchived) {
|
|
|
634
672
|
return data.tasks ?? [];
|
|
635
673
|
}
|
|
636
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Handoff target → {id}. UUIDs pass straight through (the server checks
|
|
677
|
+
* eligibility); names resolve exactly against GET /v1/tasks/assignees for
|
|
678
|
+
* this board — same exact-match-or-error rule as --project names.
|
|
679
|
+
*/
|
|
680
|
+
async function resolveAssignee(value, ctx) {
|
|
681
|
+
const v = String(value).trim();
|
|
682
|
+
if (UUID_RE.test(v)) return { id: v };
|
|
683
|
+
const data = await api('GET', '/v1/tasks/assignees', {
|
|
684
|
+
...ctx,
|
|
685
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
686
|
+
label: 'tasks handoff',
|
|
687
|
+
});
|
|
688
|
+
const eligible = data?.assignees ?? [];
|
|
689
|
+
const matches = eligible.filter((a) => a.name === v);
|
|
690
|
+
if (matches.length > 1) {
|
|
691
|
+
throw new CliError(`More than one profile on this board is named ${JSON.stringify(v)}.`, {
|
|
692
|
+
hint: `Pass the profile UUID instead: ${matches.map((a) => a.id).join(', ')}.`,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (matches[0]) return matches[0];
|
|
696
|
+
throw new CliError(`No profile named ${JSON.stringify(v)} can take cards on this board.`, {
|
|
697
|
+
hint: eligible.length > 0 ? `Eligible: ${eligible.map((a) => a.name).join(', ')}.` : 'No profile holds this whole board yet — ask the account owner.',
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
637
701
|
async function cmdTasks(tokens, globals) {
|
|
638
702
|
if (globals.folder) throw usageError('--folder does not apply to tasks: cards are project-wide, folder grants stay a KB-read concept.');
|
|
639
703
|
const [sub, ...rest] = tokens;
|
|
@@ -678,28 +742,126 @@ async function cmdTasks(tokens, globals) {
|
|
|
678
742
|
return;
|
|
679
743
|
}
|
|
680
744
|
if (sub === 'status') {
|
|
681
|
-
const [id, to,
|
|
682
|
-
if (!id || !to
|
|
745
|
+
const [id, to, ...flagTokens] = rest;
|
|
746
|
+
if (!id || !to) throw usageError('Usage: takibi tasks status <id> <todo|in_progress|review|done> [--note <text>]');
|
|
683
747
|
if (!TASK_STATUSES.includes(to)) throw usageError(`Status is one of ${TASK_STATUSES.join(' | ')}, not ${JSON.stringify(to)}.`);
|
|
748
|
+
let note;
|
|
749
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
750
|
+
const f = flagTokens[i];
|
|
751
|
+
if (f === '--note') note = flagTokens[++i];
|
|
752
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. Usage: takibi tasks status <id> <todo|in_progress|review|done> [--note <text>]`);
|
|
753
|
+
}
|
|
754
|
+
if (note !== undefined && note === '') throw usageError('--note needs non-empty text.');
|
|
684
755
|
const ctx = await ctxFor(globals, { needKey: true });
|
|
685
756
|
const data = await api('PATCH', `/v1/tasks/${id}`, {
|
|
686
757
|
...ctx,
|
|
687
758
|
query: qparams([['projectId', ctx.projectId]]),
|
|
688
|
-
body: { status: to },
|
|
759
|
+
body: { status: to, reviewNote: note },
|
|
689
760
|
label: 'tasks status',
|
|
690
761
|
});
|
|
691
762
|
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
692
763
|
else renderTask(data.task);
|
|
693
764
|
return;
|
|
694
765
|
}
|
|
766
|
+
if (sub === 'handoff') {
|
|
767
|
+
const usage = 'Usage: takibi tasks handoff <id> <profile-name-or-uuid> [--review] [--note <text>]';
|
|
768
|
+
const [id, target, ...flagTokens] = rest;
|
|
769
|
+
if (!id || !target || target.startsWith('--')) throw usageError(usage);
|
|
770
|
+
let review = false;
|
|
771
|
+
let note;
|
|
772
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
773
|
+
const f = flagTokens[i];
|
|
774
|
+
if (f === '--review') review = true;
|
|
775
|
+
else if (f === '--note') note = flagTokens[++i];
|
|
776
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
777
|
+
}
|
|
778
|
+
if (note !== undefined && note === '') throw usageError('--note needs non-empty text.');
|
|
779
|
+
// After the handoff this key no longer holds the card, so the note can
|
|
780
|
+
// only ride the same call — and only onto a card moving to review.
|
|
781
|
+
if (note !== undefined && !review) throw usageError('--note rides with --review: the handoff and the review note go in one call.');
|
|
782
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
783
|
+
const assignee = await resolveAssignee(target, ctx);
|
|
784
|
+
const body = { assigneeProfileId: assignee.id };
|
|
785
|
+
if (review) body.status = 'review';
|
|
786
|
+
if (note !== undefined) body.reviewNote = note;
|
|
787
|
+
const data = await api('PATCH', `/v1/tasks/${id}`, {
|
|
788
|
+
...ctx,
|
|
789
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
790
|
+
body,
|
|
791
|
+
label: 'tasks handoff',
|
|
792
|
+
});
|
|
793
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
794
|
+
else renderTask(data.task);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
695
797
|
if (sub === 'artifact' || sub === 'artifacts') {
|
|
696
|
-
const
|
|
697
|
-
|
|
798
|
+
const LINK_USAGE = 'Usage: takibi tasks artifact add <taskId> <url> [--note <t> --size <n> --hash <h>]';
|
|
799
|
+
const MD_USAGE = 'Usage: takibi tasks artifact add <taskId> --markdown <file|-> [--title <t>] [--note <t>]';
|
|
800
|
+
const [verb, taskId, ...after] = rest;
|
|
801
|
+
if (verb === 'show') {
|
|
802
|
+
const [artifactId, extra] = after;
|
|
803
|
+
if (!taskId || !artifactId || extra) throw usageError('Usage: takibi tasks artifact show <taskId> <artifactId>');
|
|
804
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
805
|
+
const data = await api('GET', `/v1/tasks/${taskId}/artifacts/${artifactId}`, {
|
|
806
|
+
...ctx,
|
|
807
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
808
|
+
label: 'tasks artifact show',
|
|
809
|
+
});
|
|
810
|
+
// --json prints the server envelope even for links (content null);
|
|
811
|
+
// human output refuses instead of printing an empty page.
|
|
812
|
+
if (ctx.json) {
|
|
813
|
+
out(JSON.stringify(data, null, 2));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (data.artifact.content == null) {
|
|
817
|
+
throw new CliError('That artifact is a link — it carries no stored content to print.');
|
|
818
|
+
}
|
|
819
|
+
out(data.artifact.content);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const [url, ...flagTokens] = after;
|
|
823
|
+
if (verb !== 'add' || !taskId || !url) throw usageError(LINK_USAGE);
|
|
824
|
+
if (url === '--markdown') {
|
|
825
|
+
const [file, ...mdFlags] = flagTokens;
|
|
826
|
+
if (!file) throw usageError(MD_USAGE);
|
|
827
|
+
const md = {};
|
|
828
|
+
for (let i = 0; i < mdFlags.length; i++) {
|
|
829
|
+
const f = mdFlags[i];
|
|
830
|
+
if (f === '--title' || f === '--note') md[f.slice(2)] = mdFlags[++i];
|
|
831
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${MD_USAGE}`);
|
|
832
|
+
}
|
|
833
|
+
if (md.title !== undefined && md.title === '') throw usageError(`--title needs non-empty text. ${MD_USAGE}`);
|
|
834
|
+
if (md.note !== undefined && md.note === '') throw usageError(`--note needs non-empty text. ${MD_USAGE}`);
|
|
835
|
+
let content;
|
|
836
|
+
if (file === '-') {
|
|
837
|
+
content = readFileSync(0, 'utf8');
|
|
838
|
+
} else {
|
|
839
|
+
try {
|
|
840
|
+
content = readFileSync(file, 'utf8');
|
|
841
|
+
} catch {
|
|
842
|
+
throw usageError(`No such file: ${file}.`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (!content.trim()) throw usageError(`${file === '-' ? 'That stdin' : file} is empty — nothing to attach.`);
|
|
846
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
847
|
+
const data = await api('POST', `/v1/tasks/${taskId}/artifacts`, {
|
|
848
|
+
...ctx,
|
|
849
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
850
|
+
body: { kind: 'markdown', title: md.title, content, note: md.note },
|
|
851
|
+
label: 'tasks artifact add',
|
|
852
|
+
});
|
|
853
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
854
|
+
else {
|
|
855
|
+
out(`attached ${data.artifact.id}`);
|
|
856
|
+
out(` ${artifactTitle(data.artifact.title, content)}${data.artifact.note ? ` — ${data.artifact.note}` : ''}`);
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
698
860
|
const flags = {};
|
|
699
861
|
for (let i = 0; i < flagTokens.length; i++) {
|
|
700
862
|
const f = flagTokens[i];
|
|
701
863
|
if (f === '--note' || f === '--size' || f === '--hash') flags[f.slice(2)] = flagTokens[++i];
|
|
702
|
-
else throw usageError(`Unexpected ${JSON.stringify(f)}.
|
|
864
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${LINK_USAGE}`);
|
|
703
865
|
}
|
|
704
866
|
if (flags.note !== undefined && flags.note === '') throw usageError('--note needs non-empty text.');
|
|
705
867
|
if (flags.size !== undefined) {
|
|
@@ -721,7 +883,41 @@ async function cmdTasks(tokens, globals) {
|
|
|
721
883
|
}
|
|
722
884
|
return;
|
|
723
885
|
}
|
|
724
|
-
|
|
886
|
+
if (sub === 'escalate') {
|
|
887
|
+
const [id, ...flagTokens] = rest;
|
|
888
|
+
if (!id) throw usageError('Usage: takibi tasks escalate <id> --reason <text>');
|
|
889
|
+
let reason;
|
|
890
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
891
|
+
const f = flagTokens[i];
|
|
892
|
+
if (f === '--reason') reason = flagTokens[++i];
|
|
893
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. Usage: takibi tasks escalate <id> --reason <text>`);
|
|
894
|
+
}
|
|
895
|
+
if (reason === undefined || reason === '') throw usageError('--reason needs non-empty text: what do you need from the Founder?');
|
|
896
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
897
|
+
const data = await api('POST', `/v1/tasks/${id}/escalate`, {
|
|
898
|
+
...ctx,
|
|
899
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
900
|
+
body: { reason },
|
|
901
|
+
label: 'tasks escalate',
|
|
902
|
+
});
|
|
903
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
904
|
+
else renderTask(data.task);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (sub === 'deescalate') {
|
|
908
|
+
const [id, extra] = rest;
|
|
909
|
+
if (!id || extra) throw usageError('Usage: takibi tasks deescalate <id>');
|
|
910
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
911
|
+
const data = await api('DELETE', `/v1/tasks/${id}/escalation`, {
|
|
912
|
+
...ctx,
|
|
913
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
914
|
+
label: 'tasks deescalate',
|
|
915
|
+
});
|
|
916
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
917
|
+
else renderTask(data.task);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | escalate | deescalate | artifact add | artifact show.`);
|
|
725
921
|
}
|
|
726
922
|
|
|
727
923
|
async function cmdDoc(tokens, globals) {
|