takibibase 1.11.0 → 1.14.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 +25 -7
- package/package.json +1 -1
- package/takibi.mjs +156 -14
package/SKILL.md
CHANGED
|
@@ -41,14 +41,31 @@ error hints.
|
|
|
41
41
|
- `takibi tasks list | get <id> | claim <id> | status <id> <todo|in_progress|review|done> [--note …]`
|
|
42
42
|
and `takibi tasks artifact add <taskId> <url> [--note …]`. The status
|
|
43
43
|
`--note` is the review handoff note (max 1000 chars); leaving review
|
|
44
|
-
clears it.
|
|
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.
|
|
45
53
|
- `takibi tasks handoff <id> <profile> [--review] [--note "…"]` — hand a
|
|
46
54
|
card you hold to another profile on the same board (name or UUID; an
|
|
47
55
|
unknown name errors with the eligible names — never guess). Use
|
|
48
|
-
`--review` to
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
- `takibi tasks follow-up <id> --title "…"` — spin a fresh linked todo
|
|
62
|
+
off any card, including a finished one (`--body`, `--due <date>`,
|
|
63
|
+
`--assignee` for an exact board name or UUID — unknown names error
|
|
64
|
+
with the eligible list, and naming anyone other than yourself needs
|
|
65
|
+
Orchestrator). The new card lands in the source's project at To do,
|
|
66
|
+
unassigned unless `--assignee` names someone; the source keeps its
|
|
67
|
+
status, holder, and history, and nothing carries over. Never move a
|
|
68
|
+
done card back to To do to continue it — follow up instead.
|
|
52
69
|
- `@name` in a task names the handoff target: when your part is
|
|
53
70
|
finished, run `tasks handoff <id> name` (usually with `--review
|
|
54
71
|
--note`). The mention itself assigns nothing — the handoff does.
|
|
@@ -146,12 +163,13 @@ abstains honestly instead of answering halfway.
|
|
|
146
163
|
- Notes export/keep/remove need owner approval already given in the
|
|
147
164
|
conversation. Export only creates a draft; verify it before adding
|
|
148
165
|
content to Sources.
|
|
149
|
-
- Task claim/status/handoff/artifact/escalation writes only with owner approval
|
|
166
|
+
- Task claim/status/handoff/artifact/escalation/follow-up writes only with owner approval
|
|
150
167
|
already given in conversation. Claim-first: a plain key must claim a card before
|
|
151
168
|
moving or touching it; only orchestrators/owners accept (`review→done`),
|
|
152
169
|
assign or release cards they do not hold, or archive. A holder may hand
|
|
153
170
|
its own card to another eligible profile (`tasks handoff`). Title/body edits need the create cap —
|
|
154
|
-
claim-only keys can drive a card but cannot rewrite its text.
|
|
171
|
+
claim-only keys can drive a card but cannot rewrite its text. Follow-ups need
|
|
172
|
+
the create cap too, and any granted creator may spin one off a card it does not hold.
|
|
155
173
|
- Boards need a whole-collection grant: folder-only or tag-only keys 403
|
|
156
174
|
on every task route (the full-collection server message says so verbatim
|
|
157
175
|
when the key holds the required cap; keys lacking the cap get the generic
|
package/package.json
CHANGED
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.14.0';
|
|
41
41
|
const CLI_INFO = (() => {
|
|
42
42
|
try {
|
|
43
43
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
@@ -364,9 +364,12 @@ Commands:
|
|
|
364
364
|
tasks claim <id> Claim an open to-do card (atomic, one winner)
|
|
365
365
|
tasks status <id> <st> Move a card: todo | in_progress | review | done [--note <text>]
|
|
366
366
|
tasks handoff <id> <profile> Hand a card you hold to another profile [--review --note <text>]
|
|
367
|
+
tasks follow-up <id> Spin a fresh linked todo off any card [--title/--body/--assignee/--due]
|
|
367
368
|
tasks escalate <id> Ask the Founder for help on a held card [--reason <text>]
|
|
368
369
|
tasks deescalate <id> Clear your escalation ("never mind")
|
|
369
|
-
tasks artifact add <taskId> <url> Attach a link
|
|
370
|
+
tasks artifact add <taskId> <url> Attach a link artifact [--note/--size/--hash]
|
|
371
|
+
tasks artifact add <taskId> --markdown <file|-> Attach an inline markdown note [--title/--note]
|
|
372
|
+
tasks artifact show <taskId> <artifactId> Print one markdown note's content
|
|
370
373
|
doc list Documents (metadata)
|
|
371
374
|
doc get <id> One document's metadata
|
|
372
375
|
doc text <id> Converted text of one document
|
|
@@ -398,10 +401,15 @@ Per-command options:
|
|
|
398
401
|
ask/search: -q, --query <text> (or a bare positional), -k <n> (ask 1-12, search 1-20)
|
|
399
402
|
tasks list: --all (include archived)
|
|
400
403
|
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
|
|
402
|
-
review in the same call, --note <text> leaves the review note with it
|
|
404
|
+
tasks handoff: <profile> is a name or UUID on this board; --review submits your work
|
|
405
|
+
for review in the same call, --note <text> leaves the review note with it
|
|
403
406
|
tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
|
|
404
|
-
tasks
|
|
407
|
+
tasks follow-up: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
|
|
408
|
+
--assignee <name-or-uuid>, --due <date>; the source card is never modified
|
|
409
|
+
tasks get: sent-back cards carry previousReviewNote, the last handoff note
|
|
410
|
+
tasks artifact add <url>: --note <text> --size <bytes> --hash <hex> (links only)
|
|
411
|
+
tasks artifact add --markdown <file|->: --title <text> --note <text> (content from file or stdin -, max 50000 chars; title max 140 chars)
|
|
412
|
+
tasks artifact show: prints the raw markdown (links carry no content)
|
|
405
413
|
doc list: --limit <n> (1-100) doc text: --max-chars <n>
|
|
406
414
|
doc upload: <file> (pdf, md, txt, docx, max 10MB)
|
|
407
415
|
notes append: --problem <text> (or a bare positional),
|
|
@@ -424,7 +432,7 @@ to probe the API and \`takibi projects\` to see this key's scope
|
|
|
424
432
|
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
425
433
|
assistants look for it.
|
|
426
434
|
|
|
427
|
-
Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation writes and doc uploads
|
|
435
|
+
Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
|
|
428
436
|
need owner approval in conversation (uploads also need the Uploads grant on the key).
|
|
429
437
|
Account-only routes (delete, retry, download originals, PATCH docs/projects)
|
|
430
438
|
are never the agent's to call — ask the account owner.
|
|
@@ -464,7 +472,7 @@ function parseArgv(argv) {
|
|
|
464
472
|
i = ni;
|
|
465
473
|
}
|
|
466
474
|
else if (a.startsWith('--folder=')) globals.folder = a.slice('--folder='.length);
|
|
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') {
|
|
475
|
+
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 === '--body' || a === '--assignee' || a === '--due' || 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') {
|
|
468
476
|
rest.push(a);
|
|
469
477
|
if (a !== '--all' && a !== '--review' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
|
|
470
478
|
const [v, ni] = takeValue(a, i);
|
|
@@ -543,18 +551,48 @@ const taskLine = (t) => {
|
|
|
543
551
|
if (t.escalationReason) bits.push(`· escalated: ${t.escalationReason}`);
|
|
544
552
|
if (t.blocked) bits.push(`· blocked: ${t.blockedReason || 'no reason given'}`);
|
|
545
553
|
if (t.artifacts?.length) bits.push(`· ${t.artifacts.length} artifact${t.artifacts.length === 1 ? '' : 's'}`);
|
|
554
|
+
if (t.followups?.length) bits.push(`· ${t.followups.length} follow-up${t.followups.length === 1 ? '' : 's'}`);
|
|
546
555
|
if (t.archivedAt) bits.push('· archived');
|
|
547
556
|
if (t.dueAt) bits.push(`· due ${t.dueAt}`);
|
|
548
557
|
return bits.join(' ');
|
|
549
558
|
};
|
|
550
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Display title for a task artifact: the explicit title when set, else
|
|
562
|
+
* the content's first ATX heading, else a fixed fallback. Local duplicate
|
|
563
|
+
* of artifactTitle in apps/web/src/tasks/artifacts.ts — this file ships
|
|
564
|
+
* standalone and cannot import TS.
|
|
565
|
+
*/
|
|
566
|
+
function artifactTitle(title, content) {
|
|
567
|
+
const explicit = title?.trim() ?? '';
|
|
568
|
+
if (explicit !== '') return explicit;
|
|
569
|
+
for (const line of (content ?? '').split('\n')) {
|
|
570
|
+
const text = /^#{1,6}\s+(.*)$/.exec(line.trim())?.[1] ?? '';
|
|
571
|
+
if (text !== '') return text;
|
|
572
|
+
}
|
|
573
|
+
return 'Untitled note';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** One artifact row: links print the URL exactly as before; markdown
|
|
577
|
+
* notes (null URL) print the display title — never the literal null. */
|
|
578
|
+
const artifactRow = (a) =>
|
|
579
|
+
a.kind === 'markdown' || a.url == null
|
|
580
|
+
? `${artifactTitle(a.title, a.content ?? null)}${a.note ? ` — ${a.note}` : ''} [${a.id}]`
|
|
581
|
+
: `${a.url}${a.note ? ` — ${a.note}` : ''} [${a.id}]`;
|
|
582
|
+
|
|
551
583
|
function renderTask(t) {
|
|
552
584
|
out(taskLine(t));
|
|
553
585
|
if (t.reviewNote) out(`review: ${t.reviewNote}`);
|
|
586
|
+
if (t.previousReviewNote) out(`previous review note: ${t.previousReviewNote}`);
|
|
554
587
|
if (t.body) out(`\n${t.body}`);
|
|
588
|
+
if (t.followsUpOn) out(`follow-up of: ${t.followsUpOn.title} [${t.followsUpOn.id}]`);
|
|
589
|
+
if (t.followups?.length) {
|
|
590
|
+
out('follow-ups:');
|
|
591
|
+
for (const f of t.followups) out(` - ${f.title} · ${f.status}${f.assigneeName ? ` · ${f.assigneeName}` : ''}${f.archivedAt ? ' · archived' : ''} [${f.id}]`);
|
|
592
|
+
}
|
|
555
593
|
if (t.artifacts?.length) {
|
|
556
594
|
out('');
|
|
557
|
-
for (const a of t.artifacts) out(` - ${a
|
|
595
|
+
for (const a of t.artifacts) out(` - ${artifactRow(a)}`);
|
|
558
596
|
}
|
|
559
597
|
err(`created ${t.createdAt} · updated ${t.updatedAt}`);
|
|
560
598
|
}
|
|
@@ -648,13 +686,13 @@ async function listTasks(ctx, includeArchived) {
|
|
|
648
686
|
* eligibility); names resolve exactly against GET /v1/tasks/assignees for
|
|
649
687
|
* this board — same exact-match-or-error rule as --project names.
|
|
650
688
|
*/
|
|
651
|
-
async function resolveAssignee(value, ctx) {
|
|
689
|
+
async function resolveAssignee(value, ctx, label = 'tasks handoff') {
|
|
652
690
|
const v = String(value).trim();
|
|
653
691
|
if (UUID_RE.test(v)) return { id: v };
|
|
654
692
|
const data = await api('GET', '/v1/tasks/assignees', {
|
|
655
693
|
...ctx,
|
|
656
694
|
query: qparams([['projectId', ctx.projectId]]),
|
|
657
|
-
label
|
|
695
|
+
label,
|
|
658
696
|
});
|
|
659
697
|
const eligible = data?.assignees ?? [];
|
|
660
698
|
const matches = eligible.filter((a) => a.name === v);
|
|
@@ -766,13 +804,73 @@ async function cmdTasks(tokens, globals) {
|
|
|
766
804
|
return;
|
|
767
805
|
}
|
|
768
806
|
if (sub === 'artifact' || sub === 'artifacts') {
|
|
769
|
-
const
|
|
770
|
-
|
|
807
|
+
const LINK_USAGE = 'Usage: takibi tasks artifact add <taskId> <url> [--note <t> --size <n> --hash <h>]';
|
|
808
|
+
const MD_USAGE = 'Usage: takibi tasks artifact add <taskId> --markdown <file|-> [--title <t>] [--note <t>]';
|
|
809
|
+
const [verb, taskId, ...after] = rest;
|
|
810
|
+
if (verb === 'show') {
|
|
811
|
+
const [artifactId, extra] = after;
|
|
812
|
+
if (!taskId || !artifactId || extra) throw usageError('Usage: takibi tasks artifact show <taskId> <artifactId>');
|
|
813
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
814
|
+
const data = await api('GET', `/v1/tasks/${taskId}/artifacts/${artifactId}`, {
|
|
815
|
+
...ctx,
|
|
816
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
817
|
+
label: 'tasks artifact show',
|
|
818
|
+
});
|
|
819
|
+
// --json prints the server envelope even for links (content null);
|
|
820
|
+
// human output refuses instead of printing an empty page.
|
|
821
|
+
if (ctx.json) {
|
|
822
|
+
out(JSON.stringify(data, null, 2));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (data.artifact.content == null) {
|
|
826
|
+
throw new CliError('That artifact is a link — it carries no stored content to print.');
|
|
827
|
+
}
|
|
828
|
+
out(data.artifact.content);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const [url, ...flagTokens] = after;
|
|
832
|
+
if (verb !== 'add' || !taskId || !url) throw usageError(LINK_USAGE);
|
|
833
|
+
if (url === '--markdown') {
|
|
834
|
+
const [file, ...mdFlags] = flagTokens;
|
|
835
|
+
if (!file) throw usageError(MD_USAGE);
|
|
836
|
+
const md = {};
|
|
837
|
+
for (let i = 0; i < mdFlags.length; i++) {
|
|
838
|
+
const f = mdFlags[i];
|
|
839
|
+
if (f === '--title' || f === '--note') md[f.slice(2)] = mdFlags[++i];
|
|
840
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${MD_USAGE}`);
|
|
841
|
+
}
|
|
842
|
+
if (md.title !== undefined && md.title === '') throw usageError(`--title needs non-empty text. ${MD_USAGE}`);
|
|
843
|
+
if (md.note !== undefined && md.note === '') throw usageError(`--note needs non-empty text. ${MD_USAGE}`);
|
|
844
|
+
let content;
|
|
845
|
+
if (file === '-') {
|
|
846
|
+
content = readFileSync(0, 'utf8');
|
|
847
|
+
} else {
|
|
848
|
+
try {
|
|
849
|
+
content = readFileSync(file, 'utf8');
|
|
850
|
+
} catch {
|
|
851
|
+
throw usageError(`No such file: ${file}.`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (!content.trim()) throw usageError(`${file === '-' ? 'That stdin' : file} is empty — nothing to attach.`);
|
|
855
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
856
|
+
const data = await api('POST', `/v1/tasks/${taskId}/artifacts`, {
|
|
857
|
+
...ctx,
|
|
858
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
859
|
+
body: { kind: 'markdown', title: md.title, content, note: md.note },
|
|
860
|
+
label: 'tasks artifact add',
|
|
861
|
+
});
|
|
862
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
863
|
+
else {
|
|
864
|
+
out(`attached ${data.artifact.id}`);
|
|
865
|
+
out(` ${artifactTitle(data.artifact.title, content)}${data.artifact.note ? ` — ${data.artifact.note}` : ''}`);
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
771
869
|
const flags = {};
|
|
772
870
|
for (let i = 0; i < flagTokens.length; i++) {
|
|
773
871
|
const f = flagTokens[i];
|
|
774
872
|
if (f === '--note' || f === '--size' || f === '--hash') flags[f.slice(2)] = flagTokens[++i];
|
|
775
|
-
else throw usageError(`Unexpected ${JSON.stringify(f)}.
|
|
873
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${LINK_USAGE}`);
|
|
776
874
|
}
|
|
777
875
|
if (flags.note !== undefined && flags.note === '') throw usageError('--note needs non-empty text.');
|
|
778
876
|
if (flags.size !== undefined) {
|
|
@@ -828,7 +926,51 @@ async function cmdTasks(tokens, globals) {
|
|
|
828
926
|
else renderTask(data.task);
|
|
829
927
|
return;
|
|
830
928
|
}
|
|
831
|
-
|
|
929
|
+
if (sub === 'follow-up') {
|
|
930
|
+
const usage = 'Usage: takibi tasks follow-up <id> --title <text> [--body <text>] [--assignee <name-or-uuid>] [--due <date>]';
|
|
931
|
+
const [id, ...flagTokens] = rest;
|
|
932
|
+
if (!id || id.startsWith('--')) throw usageError(usage);
|
|
933
|
+
let title;
|
|
934
|
+
let body;
|
|
935
|
+
let assignee;
|
|
936
|
+
let due;
|
|
937
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
938
|
+
const f = flagTokens[i];
|
|
939
|
+
if (f === '--title') title = flagTokens[++i];
|
|
940
|
+
else if (f === '--body') body = flagTokens[++i];
|
|
941
|
+
else if (f === '--assignee') assignee = flagTokens[++i];
|
|
942
|
+
else if (f === '--due') due = flagTokens[++i];
|
|
943
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
944
|
+
}
|
|
945
|
+
// Validate everything before touching the key or network, mirroring
|
|
946
|
+
// the server limits so failures name the flag instead of 400ing.
|
|
947
|
+
if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
948
|
+
if (title.length > 300) throw usageError('--title is max 300 chars.');
|
|
949
|
+
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
950
|
+
if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
|
|
951
|
+
if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
|
|
952
|
+
// No --project: the endpoint scopes by the source card's own project.
|
|
953
|
+
const ctx = await ctxFor(globals, { needKey: true, resolveProject: false });
|
|
954
|
+
if (assignee !== undefined) {
|
|
955
|
+
const v = String(assignee).trim();
|
|
956
|
+
if (!UUID_RE.test(v)) {
|
|
957
|
+
// Names resolve against the source card's own board.
|
|
958
|
+
const src = await api('GET', `/v1/tasks/${id}`, { ...ctx, label: 'tasks follow-up' });
|
|
959
|
+
assignee = (await resolveAssignee(v, { ...ctx, projectId: src.task.projectId }, 'tasks follow-up')).id;
|
|
960
|
+
} else {
|
|
961
|
+
assignee = v;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const data = await api('POST', `/v1/tasks/${id}/follow-ups`, {
|
|
965
|
+
...ctx,
|
|
966
|
+
body: { title: title.trim(), body, assigneeProfileId: assignee, dueAt: due },
|
|
967
|
+
label: 'tasks follow-up',
|
|
968
|
+
});
|
|
969
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
970
|
+
else renderTask(data.task);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
|
|
832
974
|
}
|
|
833
975
|
|
|
834
976
|
async function cmdDoc(tokens, globals) {
|