takibibase 1.14.1 → 1.15.1
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 +6 -5
- package/SKILL.md +13 -3
- package/package.json +2 -2
- package/takibi.mjs +114 -26
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
|
|
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 ≥
|
|
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
|
|
48
|
-
|
|
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.
|
|
175
|
-
the create cap too, and any granted creator may spin
|
|
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.
|
|
3
|
+
"version": "1.15.1",
|
|
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": ">=
|
|
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
|
-
* -
|
|
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.
|
|
42
|
+
const BAKED_VERSION = '1.15.1';
|
|
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
|
}
|
|
@@ -302,7 +308,7 @@ function hintFor(status, serverMessage) {
|
|
|
302
308
|
* One API call. Verbose logs method + URL + status only — headers (which
|
|
303
309
|
* carry the key) are never logged, printed, or interpolated anywhere.
|
|
304
310
|
*/
|
|
305
|
-
async function api(method, path, { query = null, body = null, form = null, key = null, baseUrl, verbose = false, label = null } = {}) {
|
|
311
|
+
async function api(method, path, { query = null, body = null, form = null, key = null, baseUrl, verbose = false, label = null, blockHint = null } = {}) {
|
|
306
312
|
const qs = query ? `?${query.toString()}` : '';
|
|
307
313
|
const url = `${baseUrl}${path}${qs}`;
|
|
308
314
|
const headers = { accept: 'application/json', 'user-agent': USER_AGENT };
|
|
@@ -330,6 +336,13 @@ async function api(method, path, { query = null, body = null, form = null, key =
|
|
|
330
336
|
try {
|
|
331
337
|
data = await res.json();
|
|
332
338
|
} catch {
|
|
339
|
+
const responseType = res.headers.get('content-type') ?? '';
|
|
340
|
+
if (res.status === 403 && responseType.includes('text/html')) {
|
|
341
|
+
throw new CliError(`${label ?? path}: blocked by network security before reaching the API (HTTP 403).`, {
|
|
342
|
+
status: res.status,
|
|
343
|
+
hint: blockHint ?? 'The hosting filter flagged this request. Reword anything command-like and retry.',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
333
346
|
throw new CliError(`Unexpected response from ${label ?? path} (HTTP ${res.status}, not JSON).`, {
|
|
334
347
|
status: res.status,
|
|
335
348
|
hint: `Is ${baseUrl} the Takibi API? \`takibi version\` says what is serving there.`,
|
|
@@ -361,8 +374,10 @@ Commands:
|
|
|
361
374
|
search -q "..." Ranked chunks (snippets + metadata, no full text)
|
|
362
375
|
tasks list Board cards for the project
|
|
363
376
|
tasks get <id> One card with body + artifacts
|
|
377
|
+
tasks create Open a fresh card [--title/--body/--assignee/--due]
|
|
364
378
|
tasks claim <id> Claim an open to-do card (atomic, one winner)
|
|
365
379
|
tasks status <id> <st> Move a card: todo | in_progress | review | done [--note <text>]
|
|
380
|
+
tasks update <id> Amend a card's title, body, or due date [--title/--body/--due]
|
|
366
381
|
tasks handoff <id> <profile> Hand a card you hold to another profile [--review --note <text>]
|
|
367
382
|
tasks follow-up <id> Spin a fresh linked todo off any card [--title/--body/--assignee/--due]
|
|
368
383
|
tasks escalate <id> Ask the Founder for help on a held card [--reason <text>]
|
|
@@ -406,6 +421,11 @@ Per-command options:
|
|
|
406
421
|
tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
|
|
407
422
|
tasks follow-up: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
|
|
408
423
|
--assignee <name-or-uuid>, --due <date>; the source card is never modified
|
|
424
|
+
tasks create: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
|
|
425
|
+
--assignee <name-or-uuid>, --due <date>; needs the create cap,
|
|
426
|
+
lands at To do unassigned unless --assignee names someone
|
|
427
|
+
tasks update: --title/--body/--due (at least one; same limits as create;
|
|
428
|
+
empty --due clears the date)
|
|
409
429
|
tasks get: sent-back cards carry previousReviewNote, the last handoff note
|
|
410
430
|
tasks artifact add <url>: --note <text> --size <bytes> --hash <hex> (links only)
|
|
411
431
|
tasks artifact add --markdown <file|->: --title <text> --note <text> (content from file or stdin -, max 50000 chars; title max 140 chars)
|
|
@@ -422,6 +442,7 @@ Per-command options:
|
|
|
422
442
|
notes keep/remove: <id> <expected-version> from notes list --all
|
|
423
443
|
projects: --add <name> <uuid>
|
|
424
444
|
skill: --install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]
|
|
445
|
+
(repeat --dir per custom box; --dir alone skips detection)
|
|
425
446
|
|
|
426
447
|
Setup: save the owner-provided key (one <publicId>.<secret> line) to
|
|
427
448
|
~/.takibi/key (chmod 600). The API defaults to Takibi's servers; only
|
|
@@ -432,7 +453,7 @@ to probe the API and \`takibi projects\` to see this key's scope
|
|
|
432
453
|
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
433
454
|
assistants look for it.
|
|
434
455
|
|
|
435
|
-
Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
|
|
456
|
+
Reads are free. Export stamps included notes. Task create/update/claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
|
|
436
457
|
need owner approval in conversation (uploads also need the Uploads grant on the key).
|
|
437
458
|
Account-only routes (delete, retry, download originals, PATCH docs/projects)
|
|
438
459
|
are never the agent's to call — ask the account owner.
|
|
@@ -729,16 +750,47 @@ async function cmdTasks(tokens, globals) {
|
|
|
729
750
|
if (sub === 'get') {
|
|
730
751
|
const [id, extra] = rest;
|
|
731
752
|
if (!id || extra) throw usageError('Usage: takibi tasks get <id>');
|
|
753
|
+
// Ids are board-wide: no --project to resolve (same as follow-up).
|
|
754
|
+
const ctx = await ctxFor(globals, { needKey: true, resolveProject: false });
|
|
755
|
+
const data = await api('GET', `/v1/tasks/${id}`, { ...ctx, label: 'tasks get' });
|
|
756
|
+
if (ctx.json) out(JSON.stringify({ task: data.task }, null, 2));
|
|
757
|
+
else renderTask(data.task);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (sub === 'create') {
|
|
761
|
+
const usage = 'Usage: takibi tasks create --title <text> [--body <text>] [--assignee <name-or-uuid>] [--due <date>]';
|
|
762
|
+
let title;
|
|
763
|
+
let body;
|
|
764
|
+
let assignee;
|
|
765
|
+
let due;
|
|
766
|
+
for (let i = 0; i < rest.length; i++) {
|
|
767
|
+
const f = rest[i];
|
|
768
|
+
if (f === '--title') title = rest[++i];
|
|
769
|
+
else if (f === '--body') body = rest[++i];
|
|
770
|
+
else if (f === '--assignee') assignee = rest[++i];
|
|
771
|
+
else if (f === '--due') due = rest[++i];
|
|
772
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
773
|
+
}
|
|
774
|
+
// Validate everything before touching the key or network, mirroring
|
|
775
|
+
// the server limits so failures name the flag instead of 400ing.
|
|
776
|
+
if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
777
|
+
if (title.trim().length > 300) throw usageError('--title is max 300 chars.');
|
|
778
|
+
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
779
|
+
if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
|
|
780
|
+
if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
|
|
732
781
|
const ctx = await ctxFor(globals, { needKey: true });
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
tasks = await listTasks(ctx, true);
|
|
737
|
-
found = tasks.find((t) => t.id === id);
|
|
782
|
+
if (assignee !== undefined) {
|
|
783
|
+
const v = String(assignee).trim();
|
|
784
|
+
assignee = UUID_RE.test(v) ? v : (await resolveAssignee(v, ctx, 'tasks create')).id;
|
|
738
785
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
786
|
+
const data = await api('POST', '/v1/tasks', {
|
|
787
|
+
...ctx,
|
|
788
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
789
|
+
body: { title: title.trim(), body, assigneeProfileId: assignee, dueAt: due },
|
|
790
|
+
label: 'tasks create',
|
|
791
|
+
});
|
|
792
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
793
|
+
else renderTask(data.task);
|
|
742
794
|
return;
|
|
743
795
|
}
|
|
744
796
|
if (sub === 'claim') {
|
|
@@ -803,6 +855,41 @@ async function cmdTasks(tokens, globals) {
|
|
|
803
855
|
else renderTask(data.task);
|
|
804
856
|
return;
|
|
805
857
|
}
|
|
858
|
+
if (sub === 'update') {
|
|
859
|
+
const usage = 'Usage: takibi tasks update <id> [--title <text>] [--body <text>] [--due <date>]';
|
|
860
|
+
const [id, ...flagTokens] = rest;
|
|
861
|
+
if (!id || id.startsWith('--')) throw usageError(usage);
|
|
862
|
+
let title;
|
|
863
|
+
let body;
|
|
864
|
+
let due;
|
|
865
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
866
|
+
const f = flagTokens[i];
|
|
867
|
+
if (f === '--title') title = flagTokens[++i];
|
|
868
|
+
else if (f === '--body') body = flagTokens[++i];
|
|
869
|
+
else if (f === '--due') due = flagTokens[++i];
|
|
870
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
871
|
+
}
|
|
872
|
+
if (title === undefined && body === undefined && due === undefined) {
|
|
873
|
+
throw usageError(`Nothing to change — pass at least one of --title, --body, --due. ${usage}`);
|
|
874
|
+
}
|
|
875
|
+
if (title !== undefined && title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
876
|
+
if (title !== undefined && title.trim().length > 300) throw usageError('--title is max 300 chars.');
|
|
877
|
+
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
878
|
+
// An explicit empty --due clears the date (the server treats '' and
|
|
879
|
+
// null as clear). Create keeps rejecting it: omit and clear are the
|
|
880
|
+
// same on POST.
|
|
881
|
+
if (due !== undefined && due.trim() === '') due = null;
|
|
882
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
883
|
+
const data = await api('PATCH', `/v1/tasks/${id}`, {
|
|
884
|
+
...ctx,
|
|
885
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
886
|
+
body: { title: title?.trim(), body, dueAt: due },
|
|
887
|
+
label: 'tasks update',
|
|
888
|
+
});
|
|
889
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
890
|
+
else renderTask(data.task);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
806
893
|
if (sub === 'artifact' || sub === 'artifacts') {
|
|
807
894
|
const LINK_USAGE = 'Usage: takibi tasks artifact add <taskId> <url> [--note <t> --size <n> --hash <h>]';
|
|
808
895
|
const MD_USAGE = 'Usage: takibi tasks artifact add <taskId> --markdown <file|-> [--title <t>] [--note <t>]';
|
|
@@ -945,7 +1032,7 @@ async function cmdTasks(tokens, globals) {
|
|
|
945
1032
|
// Validate everything before touching the key or network, mirroring
|
|
946
1033
|
// the server limits so failures name the flag instead of 400ing.
|
|
947
1034
|
if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
948
|
-
if (title.length > 300) throw usageError('--title is max 300 chars.');
|
|
1035
|
+
if (title.trim().length > 300) throw usageError('--title is max 300 chars.');
|
|
949
1036
|
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
950
1037
|
if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
|
|
951
1038
|
if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
|
|
@@ -970,7 +1057,7 @@ async function cmdTasks(tokens, globals) {
|
|
|
970
1057
|
else renderTask(data.task);
|
|
971
1058
|
return;
|
|
972
1059
|
}
|
|
973
|
-
throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
|
|
1060
|
+
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
1061
|
}
|
|
975
1062
|
|
|
976
1063
|
async function cmdDoc(tokens, globals) {
|
|
@@ -1049,16 +1136,16 @@ async function cmdDoc(tokens, globals) {
|
|
|
1049
1136
|
if (bytes.length > 10 * 1024 * 1024) {
|
|
1050
1137
|
throw new CliError(`${path} is over the 10MB per-file limit.`, { hint: 'Split it and upload the parts.' });
|
|
1051
1138
|
}
|
|
1052
|
-
|
|
1053
|
-
form.set('file', new File([bytes], basename(path)));
|
|
1139
|
+
// Base64 keeps file bytes opaque to edge filters, which pattern-match raw multipart bodies.
|
|
1054
1140
|
const data = await api('POST', '/v1/documents', {
|
|
1055
1141
|
...ctx,
|
|
1056
1142
|
query: qparams([
|
|
1057
1143
|
['projectId', ctx.projectId],
|
|
1058
1144
|
['folderId', ctx.folder],
|
|
1059
1145
|
]),
|
|
1060
|
-
|
|
1146
|
+
body: { name: basename(path), contentBase64: bytes.toString('base64') },
|
|
1061
1147
|
label: 'doc upload',
|
|
1148
|
+
blockHint: 'The hosting filter flagged this file. Reword command-like text and upload it again.',
|
|
1062
1149
|
});
|
|
1063
1150
|
if (ctx.json) {
|
|
1064
1151
|
out(JSON.stringify(data, null, 2));
|
|
@@ -1437,7 +1524,7 @@ const SKILL_TARGETS = [
|
|
|
1437
1524
|
{ flag: '--codex', harness: '.codex', dir: '.codex/skills' },
|
|
1438
1525
|
];
|
|
1439
1526
|
|
|
1440
|
-
const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]]';
|
|
1527
|
+
const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]…] (repeat --dir per custom box)';
|
|
1441
1528
|
|
|
1442
1529
|
/** First-run skill nudge. Stderr only (stdout stays clean), best-effort,
|
|
1443
1530
|
* never fails the command. Skipped for --json, in CI, with
|
|
@@ -1464,7 +1551,7 @@ function maybeNudgeSkill({ json = false } = {}) {
|
|
|
1464
1551
|
// Read-only home: nudge once per process run instead of daily.
|
|
1465
1552
|
}
|
|
1466
1553
|
err('takibi: no takibi-use skill installed for your agents.');
|
|
1467
|
-
err('hint: `takibi skill --install` puts it where assistants look.');
|
|
1554
|
+
err('hint: `takibi skill --install` puts it where assistants look (repeat `--dir <skills-root>` per custom box; `--force` refreshes).');
|
|
1468
1555
|
} catch {
|
|
1469
1556
|
// Nudges never fail commands.
|
|
1470
1557
|
}
|
|
@@ -1473,19 +1560,20 @@ function maybeNudgeSkill({ json = false } = {}) {
|
|
|
1473
1560
|
function cmdSkill(tokens, globals) {
|
|
1474
1561
|
let install = false;
|
|
1475
1562
|
let force = false;
|
|
1476
|
-
|
|
1563
|
+
const customRoots = [];
|
|
1477
1564
|
const picked = new Set();
|
|
1478
1565
|
for (let i = 0; i < tokens.length; i++) {
|
|
1479
1566
|
const t = tokens[i];
|
|
1480
1567
|
if (t === '--install') install = true;
|
|
1481
1568
|
else if (t === '--force') force = true;
|
|
1482
1569
|
else if (t === '--dir') {
|
|
1483
|
-
|
|
1484
|
-
if (!
|
|
1570
|
+
const root = tokens[++i];
|
|
1571
|
+
if (!root) throw usageError('`--dir` needs a skills root. ' + SKILL_USAGE);
|
|
1572
|
+
customRoots.push(root);
|
|
1485
1573
|
} else if (t === '--agents' || t === '--claude' || t === '--codex') picked.add(t);
|
|
1486
1574
|
else throw usageError(`Unexpected ${JSON.stringify(t)}. ${SKILL_USAGE}`);
|
|
1487
1575
|
}
|
|
1488
|
-
if (!install && (force || picked.size > 0 ||
|
|
1576
|
+
if (!install && (force || picked.size > 0 || customRoots.length > 0)) {
|
|
1489
1577
|
throw usageError(`Those flags need \`--install\`. ${SKILL_USAGE}`);
|
|
1490
1578
|
}
|
|
1491
1579
|
const text = skillText();
|
|
@@ -1504,17 +1592,17 @@ function cmdSkill(tokens, globals) {
|
|
|
1504
1592
|
}
|
|
1505
1593
|
const home = skillHome();
|
|
1506
1594
|
let targets;
|
|
1507
|
-
if (
|
|
1595
|
+
if (customRoots.length > 0 && picked.size === 0) targets = [];
|
|
1508
1596
|
else if (picked.size === 0) {
|
|
1509
1597
|
const detected = SKILL_TARGETS.filter((t) => existsSync(join(home, t.harness)));
|
|
1510
1598
|
targets = detected.length > 0 ? detected : [SKILL_TARGETS[0]];
|
|
1511
1599
|
} else {
|
|
1512
1600
|
targets = SKILL_TARGETS.filter((t) => picked.has(t.flag));
|
|
1513
1601
|
}
|
|
1514
|
-
|
|
1602
|
+
for (const root of customRoots) targets.push({ flag: '--dir', custom: true, root });
|
|
1515
1603
|
const results = [];
|
|
1516
1604
|
for (const t of targets) {
|
|
1517
|
-
const dir = t.custom ? join(
|
|
1605
|
+
const dir = t.custom ? join(t.root, SKILL_NAME) : join(home, t.dir, SKILL_NAME);
|
|
1518
1606
|
const dest = join(dir, SKILL_FILE);
|
|
1519
1607
|
if (existsSync(dest)) {
|
|
1520
1608
|
let same = false;
|