takibibase 1.2.0 → 1.5.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 +16 -10
- package/SKILL.md +40 -22
- package/package.json +2 -2
- package/takibi.mjs +165 -20
package/README.md
CHANGED
|
@@ -22,12 +22,15 @@ document, and Notes API routes; its behavior is documented in `SKILL.md`.
|
|
|
22
22
|
## Account-owner setup (once per machine)
|
|
23
23
|
|
|
24
24
|
1. Mint a key: in the app, Settings → Profiles → new profile with the
|
|
25
|
-
capabilities the crew needs (`search`, `ask`, `tasks`, `
|
|
26
|
-
|
|
25
|
+
capabilities the crew needs (`search`, `ask`, `tasks`, plus `task:create`
|
|
26
|
+
for authors, `task:assign` for orchestrators, and `notes` / `notes:append` /
|
|
27
|
+
`notes:export` for observers), scoped to its project(s). Task boards need
|
|
28
|
+
a whole-collection grant — folder-only or tag-only seats cannot use them.
|
|
29
|
+
Copy the `<publicId>.<secret>` shown once.
|
|
27
30
|
2. `mkdir -p ~/.takibi && printf '%s\n' '<publicId>.<secret>' > ~/.takibi/key && chmod 600 ~/.takibi/key`
|
|
28
|
-
3.
|
|
29
|
-
`
|
|
30
|
-
`$TAKIBI_BASE_URL`.
|
|
31
|
+
3. Only for local setups: one base-URL line in `~/.takibi/config`
|
|
32
|
+
(default `https://app.takibibase.com`). Env overrides:
|
|
33
|
+
`$TAKIBI_KEY_FILE`, `$TAKIBI_BASE_URL`.
|
|
31
34
|
4. `takibi projects` — lists the projects this key can reach, straight
|
|
32
35
|
from the API. Names resolve in `--project` (UUIDs work too, from
|
|
33
36
|
Settings → Projects); single-grant keys may omit it. Only for offline
|
|
@@ -37,11 +40,14 @@ document, and Notes API routes; its behavior is documented in `SKILL.md`.
|
|
|
37
40
|
|
|
38
41
|
## Give an agent
|
|
39
42
|
|
|
40
|
-
Key file in place +
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Key file in place + `takibi skill --install` (puts the `takibi-use` skill
|
|
44
|
+
into the detected agent skills dirs; `--dir` overrides, `--force`
|
|
45
|
+
overwrites). Zero-install alternative: `takibi skill` prints the skill —
|
|
46
|
+
paste it into the agent's first prompt. Acceptance: a fresh agent asks,
|
|
47
|
+
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
|
+
and notes export/keep/remove need your approval in conversation;
|
|
50
|
+
account-only routes stay yours.
|
|
45
51
|
|
|
46
52
|
## Known edges
|
|
47
53
|
|
package/SKILL.md
CHANGED
|
@@ -12,21 +12,20 @@ error hints.
|
|
|
12
12
|
## Setup (once)
|
|
13
13
|
|
|
14
14
|
- CLI: `npx takibibase …` (zero-install), `takibi …` (global install),
|
|
15
|
-
or `node
|
|
15
|
+
or `node <path>/takibi.mjs …` for the single-file download
|
|
16
16
|
(use the alias when it exists).
|
|
17
|
-
- Key: the
|
|
17
|
+
- Key: the owner saves one `<publicId>.<secret>` line to `~/.takibi/key`
|
|
18
18
|
(`chmod 600`). The key is never printed, never pasted in chat, never
|
|
19
19
|
committed. If a command says the key is missing, stop and ask.
|
|
20
|
-
- Base URL defaults to `
|
|
21
|
-
`$TAKIBI_BASE_URL` or one URL line in
|
|
20
|
+
- Base URL defaults to Takibi's servers (`https://app.takibibase.com`).
|
|
21
|
+
Only local setups override with `$TAKIBI_BASE_URL` or one URL line in
|
|
22
|
+
`~/.takibi/config`.
|
|
22
23
|
- Projects: `takibi projects` lists what this key can reach. `--project`
|
|
23
24
|
takes a name or a UUID; single-grant keys may omit it.
|
|
24
25
|
`takibi projects --add <name> <uuid>` keeps local aliases for offline use.
|
|
25
26
|
- First probe: `takibi version` (needs no key; shows build + `jev` status).
|
|
26
27
|
- Update notice: the CLI polls the npm registry once a day and nudges on
|
|
27
|
-
stderr when behind (never on `--json`).
|
|
28
|
-
`TAKIBI_NO_UPDATE_CHECK=1`; point it at a mirror with
|
|
29
|
-
`TAKIBI_REGISTRY_URL`.
|
|
28
|
+
stderr when behind (never on `--json`).
|
|
30
29
|
|
|
31
30
|
## Commands
|
|
32
31
|
|
|
@@ -38,24 +37,24 @@ error hints.
|
|
|
38
37
|
- `takibi tasks list | get <id> | claim <id> | status <id> <todo|in_progress|review|done>`
|
|
39
38
|
and `takibi tasks artifact add <taskId> <url> [--note …]`.
|
|
40
39
|
- `takibi doc list | get <id> | text <id>` — metadata, then converted text.
|
|
41
|
-
`doc download` is
|
|
40
|
+
`doc download` is account-only; the CLI says so — use `doc text`.
|
|
42
41
|
- `takibi notes append --problem "…" [--tried …] [--worked …] [--failed …] [--next-time …] [--source <id>]`
|
|
43
42
|
— save an end-of-run debrief or tool quirk (`--problem` or a bare
|
|
44
43
|
positional; `--project <tag>` scopes it; `--source` is repeatable).
|
|
45
44
|
Add `--run-id <run-id>` when a run may retry: the same run ID and body
|
|
46
45
|
return the existing note instead of appending a duplicate.
|
|
47
46
|
- `takibi notes list` — review queue with note IDs, statuses, and current
|
|
48
|
-
versions. `takibi notes list --all` shows the inventory, including
|
|
49
|
-
|
|
47
|
+
versions. `takibi notes list --all` shows the inventory, including
|
|
48
|
+
flagged notes. `--project <tag>` filters either list by the exact project tag.
|
|
50
49
|
- `takibi notes search -q "…"` — top-2 agent notes for the query.
|
|
51
50
|
- `takibi notes export [--since <ts> | --note <uuid> …]` — draft digest
|
|
52
51
|
(markdown, per-sentence note ids). Repeat `--note` to pick up to 50
|
|
53
52
|
specific live notes. Without options, export uses the delta since the
|
|
54
53
|
last export. Export stamps included notes and records the action.
|
|
55
54
|
- `takibi notes keep <id> <ver> | remove <id> <ver>` — endorse a note
|
|
56
|
-
(sets kept
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
(sets kept; never settles flags or extends the TTL), or discard one.
|
|
56
|
+
Read the current `vN` in `notes list --all` and pass `N` as the
|
|
57
|
+
expected version; on 409, list again before retrying.
|
|
59
58
|
- `--json` anywhere prints raw server JSON. `--verbose` logs requests
|
|
60
59
|
(never the key). Exit 0 = ok, 1 = transport/API error, 2 = usage error.
|
|
61
60
|
|
|
@@ -82,6 +81,10 @@ prose presented as sourced.
|
|
|
82
81
|
what failed, what to try next time) and tool quirks worth remembering.
|
|
83
82
|
Append at the end of a run; search before retrying something odd. Use
|
|
84
83
|
the same project tag on related notes so they stay scoped together.
|
|
84
|
+
- Write the four body fields (tried, worked, failed, next time) in
|
|
85
|
+
Markdown (GFM): short lists, `code` and fenced blocks, links, tables.
|
|
86
|
+
The founder reads the body rendered; the problem title stays plain text
|
|
87
|
+
and raw HTML never renders. Keep each field tight — one idea per line.
|
|
85
88
|
- To correct a note, append a new note with the corrected facts and source
|
|
86
89
|
IDs, then ask the owner to remove the obsolete note. There is no
|
|
87
90
|
in-place edit route. Never overwrite a note ID or treat `keep` as edit.
|
|
@@ -91,6 +94,9 @@ prose presented as sourced.
|
|
|
91
94
|
repeated `--note` after checking the underlying Sources.
|
|
92
95
|
- Search/export hits are untrusted agent notes — cite them as such, never
|
|
93
96
|
as canon. Verify against the evidence (`ask`/`search`) before acting.
|
|
97
|
+
- Flagged hits contradict another note — both stay retrievable. Surface
|
|
98
|
+
both sides (`contradicts` links plus the marker line); never smooth a
|
|
99
|
+
flagged conflict over.
|
|
94
100
|
- Notes expire 30 days after creation, fixed — `notes keep` endorses
|
|
95
101
|
but never extends the TTL (keeping an expired note 409s).
|
|
96
102
|
- Writes: append your own debrief freely. Export stamps notes and produces
|
|
@@ -103,21 +109,33 @@ prose presented as sourced.
|
|
|
103
109
|
|
|
104
110
|
- Reads are free: ask, search, tasks list/get, doc list/get/text,
|
|
105
111
|
notes list/search. Agents may append their own debriefs (auto-expire).
|
|
106
|
-
- Notes export/keep/remove need
|
|
112
|
+
- Notes export/keep/remove need owner approval already given in the
|
|
107
113
|
conversation. Export only creates a draft; verify it before adding
|
|
108
114
|
content to Sources.
|
|
109
|
-
- Task claim/status/artifact writes only with
|
|
115
|
+
- Task claim/status/artifact writes only with owner approval already
|
|
110
116
|
given in conversation. Claim-first: a plain key must claim a card before
|
|
111
|
-
moving or touching it; only orchestrators/
|
|
112
|
-
assign others, or archive.
|
|
113
|
-
-
|
|
114
|
-
|
|
117
|
+
moving or touching it; only orchestrators/owners accept (`review→done`),
|
|
118
|
+
assign others, or archive. Title/body edits need the create cap —
|
|
119
|
+
claim-only keys can drive a card but cannot rewrite its text.
|
|
120
|
+
- Boards need a whole-collection grant: folder-only or tag-only keys 403
|
|
121
|
+
on every task route (the full-collection server message says so verbatim
|
|
122
|
+
when the key holds the required cap; keys lacking the cap get the generic
|
|
123
|
+
missing-capability 403 first).
|
|
124
|
+
- Notes are opt-in per profile: append, export, keep, and remove may 403
|
|
125
|
+
with missing-capability on keys without the notes caps — ask the owner
|
|
126
|
+
to enable them in the profile editor.
|
|
127
|
+
- Account-only routes (upload, delete, retry, download originals, PATCH
|
|
128
|
+
docs/projects) are never the agent's to call — ask the account owner.
|
|
115
129
|
|
|
116
130
|
## Failure table
|
|
117
131
|
|
|
118
|
-
- 401: key wrong/missing/revoked/disabled — or
|
|
119
|
-
(the CLI names it). 403: outside the grant
|
|
132
|
+
- 401: key wrong/missing/revoked/disabled — or an account-only route
|
|
133
|
+
(the CLI names it). 403: outside the grant, orchestrator-only, missing
|
|
134
|
+
capability (notes are opt-in), or folder-/tag-only key on a board route.
|
|
120
135
|
404: bad id (the server hides grant gaps as 404 too).
|
|
121
136
|
- 409 on claim: someone already holds the card — the message names them.
|
|
137
|
+
- 422 SECRET_BLOCKED: the secret filter fired — it covers task
|
|
138
|
+
title/body/blockedReason/artifact text as well as notes. Strip keys,
|
|
139
|
+
tokens, and credentials and retry.
|
|
122
140
|
- 429: minute throttle (slow down) or daily budget spent (resets tomorrow).
|
|
123
|
-
- Cannot-reach errors: the
|
|
141
|
+
- 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.5.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"
|
|
18
|
+
"test": "node --test notes.test.mjs skill.test.mjs"
|
|
19
19
|
},
|
|
20
20
|
"license": "SEE LICENSE IN LICENSE",
|
|
21
21
|
"repository": {
|
package/takibi.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* not in output, errors, --verbose, or exit traces (see scrub()).
|
|
9
9
|
* - Origin: never sent. Absent Origin on a Bearer call is correct.
|
|
10
10
|
* - Ask/search take `q`, never `question`.
|
|
11
|
-
* - Base URL: $TAKIBI_BASE_URL or ~/.takibi/config, else
|
|
11
|
+
* - Base URL: $TAKIBI_BASE_URL or ~/.takibi/config, else Takibi's servers.
|
|
12
12
|
*
|
|
13
13
|
* Pure-client deviations the server forces (no apps/api changes allowed):
|
|
14
14
|
* - Keys list their own granted scope via GET /v1/projects, so `projects`
|
|
@@ -34,7 +34,7 @@ import { homedir } from 'node:os';
|
|
|
34
34
|
import { join } from 'node:path';
|
|
35
35
|
|
|
36
36
|
/** Baked fallback; the published package re-reads package.json next door. */
|
|
37
|
-
const BAKED_VERSION = '1.
|
|
37
|
+
const BAKED_VERSION = '1.5.0';
|
|
38
38
|
const CLI_INFO = (() => {
|
|
39
39
|
try {
|
|
40
40
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
@@ -47,7 +47,7 @@ const CLI_INFO = (() => {
|
|
|
47
47
|
return { version: BAKED_VERSION, singleFile: true };
|
|
48
48
|
})();
|
|
49
49
|
const CLI_VERSION = CLI_INFO.version;
|
|
50
|
-
const DEFAULT_BASE_URL = '
|
|
50
|
+
const DEFAULT_BASE_URL = 'https://app.takibibase.com';
|
|
51
51
|
const REQUEST_TIMEOUT_MS = 90_000;
|
|
52
52
|
const USER_AGENT = `takibibase/${CLI_VERSION}`;
|
|
53
53
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -97,11 +97,10 @@ function versionBehind(current, latest) {
|
|
|
97
97
|
/** Once-a-day latest-version poll. Stderr only (stdout stays clean),
|
|
98
98
|
* best-effort, never fails the command. Failures are cached too, so an
|
|
99
99
|
* unreachable registry costs one slow run a day, not every invocation.
|
|
100
|
-
* Skipped for --json (errors there are a single JSON object)
|
|
101
|
-
* with TAKIBI_NO_UPDATE_CHECK=1. */
|
|
100
|
+
* Skipped for --json (errors there are a single JSON object) and in CI. */
|
|
102
101
|
async function maybeNotifyUpdate({ json = false } = {}) {
|
|
103
102
|
try {
|
|
104
|
-
if (json || process.env.CI
|
|
103
|
+
if (json || process.env.CI) return;
|
|
105
104
|
const path = join(takibiDir(), 'update-check');
|
|
106
105
|
let cached = null;
|
|
107
106
|
try {
|
|
@@ -274,11 +273,16 @@ function hintFor(status, serverMessage) {
|
|
|
274
273
|
if (/disabled/i.test(msg)) return 'That profile is disabled. Ask the account owner to enable it.';
|
|
275
274
|
return 'The key is missing or wrong. Check ~/.takibi/key (one <publicId>.<secret> line).';
|
|
276
275
|
}
|
|
277
|
-
if (status === 403)
|
|
276
|
+
if (status === 403) {
|
|
277
|
+
// The full-collection message is displayable verbatim (contract §11) —
|
|
278
|
+
// it already prints as the error, so no generic hint on top of it.
|
|
279
|
+
if (/full collection/i.test(msg)) return null;
|
|
280
|
+
return 'Outside this key’s grant, or an owner/orchestrator-only move. Check `tasks get`, or ask the account owner.';
|
|
281
|
+
}
|
|
278
282
|
if (status === 404) return 'Bad id, or outside this key’s grant (the server hides the difference).';
|
|
279
283
|
if (status === 409) return null; // claim conflicts name the winner already.
|
|
280
284
|
if (status === 422) {
|
|
281
|
-
if (/secret/i.test(msg)) return 'The secret filter fired — strip keys, tokens, and credentials
|
|
285
|
+
if (/secret/i.test(msg)) return 'The secret filter fired — strip keys, tokens, and credentials and retry. Secrets never belong in notes or task text.';
|
|
282
286
|
return 'The server rejected that shape. Check the fields and retry.';
|
|
283
287
|
}
|
|
284
288
|
if (status === 429) {
|
|
@@ -313,7 +317,7 @@ async function api(method, path, { query = null, body = null, key = null, baseUr
|
|
|
313
317
|
} catch (e) {
|
|
314
318
|
const why = e?.name === 'TimeoutError' ? `timed out after ${Math.round(REQUEST_TIMEOUT_MS / 1000)}s` : (e?.message ?? e);
|
|
315
319
|
throw new CliError(`Cannot reach the API at ${baseUrl} (${why}).`, {
|
|
316
|
-
hint: '
|
|
320
|
+
hint: 'Check the network and service status; `takibi version` probes it without needing a key.',
|
|
317
321
|
});
|
|
318
322
|
}
|
|
319
323
|
if (verbose) err(`← ${res.status} in ${Date.now() - started}ms`);
|
|
@@ -360,13 +364,15 @@ Commands:
|
|
|
360
364
|
doc text <id> Converted text of one document
|
|
361
365
|
notes append "problem" Save an agent note (debriefs, tool quirks)
|
|
362
366
|
notes list Review queue with note IDs and current versions
|
|
363
|
-
notes list --all Inventory, including notes
|
|
367
|
+
notes list --all Inventory, including flagged notes
|
|
364
368
|
notes search -q "..." Search agent notes (top 2, untrusted)
|
|
365
369
|
notes export Draft digest of recent notes; stamps exports
|
|
366
370
|
notes keep <id> <ver> Endorse a note (expected version)
|
|
367
371
|
notes remove <id> <ver> Discard a note (expected version)
|
|
368
372
|
projects Granted projects (names -> uuids for --project)
|
|
369
373
|
version What build is serving + jev wired|unwired (no key needed)
|
|
374
|
+
skill Print the takibi-use agent skill (pipe it to an agent)
|
|
375
|
+
skill --install Install the skill into agent skills dirs
|
|
370
376
|
|
|
371
377
|
Global options (accepted before or after the command):
|
|
372
378
|
--project <name-or-uuid> Project scope. Names resolve via the API list,
|
|
@@ -386,17 +392,22 @@ Per-command options:
|
|
|
386
392
|
notes append: --problem <text> (or a bare positional),
|
|
387
393
|
--tried/--worked/--failed/--next-time <text>, --source <id> (repeatable),
|
|
388
394
|
--run-id <id> (retry-safe within the same run)
|
|
395
|
+
Fields render as Markdown (GFM): short lists, code, links, tables.
|
|
389
396
|
notes list: --all (inventory instead of review queue)
|
|
390
397
|
notes search: -q/positional (no -k; top 2)
|
|
391
398
|
notes export: --since <ts> or --note <uuid> (repeatable; up to 50)
|
|
392
399
|
notes keep/remove: <id> <expected-version> from notes list --all
|
|
393
400
|
projects: --add <name> <uuid>
|
|
401
|
+
skill: --install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]
|
|
394
402
|
|
|
395
403
|
Setup: save the owner-provided key (one <publicId>.<secret> line) to
|
|
396
|
-
~/.takibi/key (chmod 600)
|
|
404
|
+
~/.takibi/key (chmod 600). The API defaults to Takibi's servers; only
|
|
405
|
+
local setups need a base URL line in ~/.takibi/config.
|
|
397
406
|
Env overrides: $TAKIBI_KEY_FILE, $TAKIBI_BASE_URL. Then \`takibi version\`
|
|
398
407
|
to probe the API and \`takibi projects\` to see this key's scope
|
|
399
408
|
(\`projects --add\` keeps local aliases for offline use).
|
|
409
|
+
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
410
|
+
assistants look for it.
|
|
400
411
|
|
|
401
412
|
Reads are free. Export stamps included notes. Task claim/status/artifact writes need owner approval in
|
|
402
413
|
conversation. Account-only routes (upload, delete, retry, download originals,
|
|
@@ -437,9 +448,9 @@ function parseArgv(argv) {
|
|
|
437
448
|
i = ni;
|
|
438
449
|
}
|
|
439
450
|
else if (a.startsWith('--folder=')) globals.folder = a.slice('--folder='.length);
|
|
440
|
-
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') {
|
|
451
|
+
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') {
|
|
441
452
|
rest.push(a);
|
|
442
|
-
if (a !== '--all') {
|
|
453
|
+
if (a !== '--all' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
|
|
443
454
|
const [v, ni] = takeValue(a, i);
|
|
444
455
|
rest.push(v);
|
|
445
456
|
i = ni;
|
|
@@ -849,6 +860,8 @@ function renderNotesSearch(data, q) {
|
|
|
849
860
|
const meta = [`note ${r.noteId ?? '?'}`];
|
|
850
861
|
if (r.score !== undefined && r.score !== null) meta.push(`score ${r.score}`);
|
|
851
862
|
if (r.kept) meta.push('kept');
|
|
863
|
+
const linked = [...(Array.isArray(r.contradicts) ? r.contradicts : []), ...(Array.isArray(r.contradictedBy) ? r.contradictedBy : [])];
|
|
864
|
+
if (linked.length) meta.push(`conflicts with ${linked.map((id) => String(id).slice(0, 8)).join(', ')}${r.involvesSource ? ' (Source-involved)' : ''}`);
|
|
852
865
|
if (r.projectTag) meta.push(`tag ${r.projectTag}`);
|
|
853
866
|
if (r.createdAt) meta.push(String(r.createdAt));
|
|
854
867
|
out(` ${meta.join(' · ')}`);
|
|
@@ -897,8 +910,8 @@ async function cmdNotes(tokens, globals) {
|
|
|
897
910
|
if (n && typeof n === 'object' && n.id) {
|
|
898
911
|
out(`saved note ${n.id}${n.version !== undefined && n.version !== null ? ` · v${n.version}` : ''}${data?.duplicate ? ' (duplicate — already stored)' : ''}`);
|
|
899
912
|
if (Array.isArray(data?.redacted) && data.redacted.length) err(`(redacted: ${data.redacted.join(', ')})`);
|
|
900
|
-
if (data?.
|
|
901
|
-
if (data?.
|
|
913
|
+
if (Array.isArray(data?.flagged) && data.flagged.length) err(`(flagged: contradicts ${data.flagged.length} note${data.flagged.length === 1 ? '' : 's'} — both sides stay retrievable)`);
|
|
914
|
+
if (Array.isArray(data?.unflagged) && data.unflagged.length) err(`(settled ${data.unflagged.length} flagged note${data.unflagged.length === 1 ? '' : 's'} in this scope)`);
|
|
902
915
|
} else {
|
|
903
916
|
out(JSON.stringify(data));
|
|
904
917
|
}
|
|
@@ -920,11 +933,12 @@ async function cmdNotes(tokens, globals) {
|
|
|
920
933
|
if (!matching.length) out(`No ${all ? 'inventory' : 'review'} notes${tag ? ` for tag ${tag}` : ''}.`);
|
|
921
934
|
for (const item of matching) {
|
|
922
935
|
const n = item.note ?? {};
|
|
923
|
-
|
|
936
|
+
const flagCount = (Array.isArray(n.contradicts) ? n.contradicts.length : 0) + (Array.isArray(n.contradictedBy) ? n.contradictedBy.length : 0);
|
|
937
|
+
out(`${n.id ?? '?'} · v${n.version ?? '?'} · ${n.status ?? '?'}${n.kept ? ' · kept' : ''}${n.projectTag ? ` · ${n.projectTag}` : ''}${flagCount ? ` · conflicts ${flagCount}` : ''}`);
|
|
924
938
|
out(` ${n.template?.problem ?? '(no problem)'}`);
|
|
925
939
|
if (item.gate !== 'skip' && item.why) out(` ${item.why}`);
|
|
926
940
|
}
|
|
927
|
-
err(`(${matching.length} ${all ? 'inventory' : 'review'} note${matching.length === 1 ? '' : 's'}${all ? '' : ' · use --all for
|
|
941
|
+
err(`(${matching.length} ${all ? 'inventory' : 'review'} note${matching.length === 1 ? '' : 's'}${all ? '' : ' · use --all for the full inventory and current versions'})`);
|
|
928
942
|
return;
|
|
929
943
|
}
|
|
930
944
|
if (sub === 'search') {
|
|
@@ -972,11 +986,14 @@ async function cmdNotes(tokens, globals) {
|
|
|
972
986
|
const bits = [];
|
|
973
987
|
const included = countOf(data?.included);
|
|
974
988
|
if (included !== undefined && included !== null) bits.push(`included ${included}`);
|
|
975
|
-
const
|
|
989
|
+
const conflictsRaw = data?.conflicts;
|
|
990
|
+
const conflicts = Array.isArray(conflictsRaw)
|
|
991
|
+
? conflictsRaw.filter((c) => c?.verdict === 'contradicts').length
|
|
992
|
+
: countOf(conflictsRaw);
|
|
976
993
|
if (conflicts !== undefined && conflicts !== null) bits.push(`conflicts ${conflicts}`);
|
|
977
994
|
const ex = data?.excluded;
|
|
978
|
-
if (ex && (ex.
|
|
979
|
-
bits.push(`
|
|
995
|
+
if (ex && (ex.flagged || ex.expired)) {
|
|
996
|
+
bits.push(`flagged ${ex.flagged ?? 0} · expired ${ex.expired ?? 0}`);
|
|
980
997
|
}
|
|
981
998
|
if (data?.deltaSince) bits.push(`since ${data.deltaSince}`);
|
|
982
999
|
if (bits.length) err(`(${bits.join(' · ')})`);
|
|
@@ -1075,6 +1092,132 @@ async function cmdVersion(globals) {
|
|
|
1075
1092
|
else out(`${data.service ?? 'takibi-api'} ${data.version ?? '?'} · jev ${data.jev ?? '?'} · via ${baseUrl}`);
|
|
1076
1093
|
}
|
|
1077
1094
|
|
|
1095
|
+
const SKILL_NAME = 'takibi-use';
|
|
1096
|
+
const SKILL_FILE = 'SKILL.md';
|
|
1097
|
+
const DOCS_AGENTS_URL = 'https://takibibase.com/docs/agents';
|
|
1098
|
+
|
|
1099
|
+
/** Test hook: redirect ~ for skill installs only (key resolution always uses the real home). */
|
|
1100
|
+
const skillHome = () => process.env.TAKIBI_HOME || homedir();
|
|
1101
|
+
|
|
1102
|
+
/** The skill text travelling next to the CLI in the published package; null in the lone-file copy. */
|
|
1103
|
+
function skillText() {
|
|
1104
|
+
try {
|
|
1105
|
+
return readFileSync(new URL('./SKILL.md', import.meta.url), 'utf8');
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const SKILL_TARGETS = [
|
|
1112
|
+
{ flag: '--agents', harness: '.agents', dir: '.agents/skills' },
|
|
1113
|
+
{ flag: '--claude', harness: '.claude', dir: '.claude/skills' },
|
|
1114
|
+
{ flag: '--codex', harness: '.codex', dir: '.codex/skills' },
|
|
1115
|
+
];
|
|
1116
|
+
|
|
1117
|
+
const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]]';
|
|
1118
|
+
|
|
1119
|
+
/** First-run skill nudge. Stderr only (stdout stays clean), best-effort,
|
|
1120
|
+
* never fails the command. Skipped for --json, in CI, with
|
|
1121
|
+
* TAKIBI_NO_SKILL_NUDGE=1, when any standard skills dir already holds the
|
|
1122
|
+
* skill, and within a day of the last nudge. */
|
|
1123
|
+
function maybeNudgeSkill({ json = false } = {}) {
|
|
1124
|
+
try {
|
|
1125
|
+
if (json || process.env.CI || process.env.TAKIBI_NO_SKILL_NUDGE === '1') return;
|
|
1126
|
+
const home = skillHome();
|
|
1127
|
+
if (SKILL_TARGETS.some((t) => existsSync(join(home, t.dir, SKILL_NAME, SKILL_FILE)))) return;
|
|
1128
|
+
const path = join(home, '.takibi', 'skill-nudge');
|
|
1129
|
+
let lastAt = 0;
|
|
1130
|
+
try {
|
|
1131
|
+
const cached = JSON.parse(readFileSync(path, 'utf8'));
|
|
1132
|
+
if (typeof cached?.at === 'number') lastAt = cached.at;
|
|
1133
|
+
} catch {
|
|
1134
|
+
// First run: no marker yet.
|
|
1135
|
+
}
|
|
1136
|
+
if (Date.now() - lastAt < UPDATE_CHECK_TTL_MS) return;
|
|
1137
|
+
try {
|
|
1138
|
+
mkdirSync(join(home, '.takibi'), { recursive: true });
|
|
1139
|
+
writeFileSync(path, JSON.stringify({ at: Date.now() }));
|
|
1140
|
+
} catch {
|
|
1141
|
+
// Read-only home: nudge once per process run instead of daily.
|
|
1142
|
+
}
|
|
1143
|
+
err('takibi: no takibi-use skill installed for your agents.');
|
|
1144
|
+
err('hint: `takibi skill --install` puts it where assistants look.');
|
|
1145
|
+
} catch {
|
|
1146
|
+
// Nudges never fail commands.
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function cmdSkill(tokens, globals) {
|
|
1151
|
+
let install = false;
|
|
1152
|
+
let force = false;
|
|
1153
|
+
let customRoot = null;
|
|
1154
|
+
const picked = new Set();
|
|
1155
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1156
|
+
const t = tokens[i];
|
|
1157
|
+
if (t === '--install') install = true;
|
|
1158
|
+
else if (t === '--force') force = true;
|
|
1159
|
+
else if (t === '--dir') {
|
|
1160
|
+
customRoot = tokens[++i];
|
|
1161
|
+
if (!customRoot) throw usageError('`--dir` needs a skills root. ' + SKILL_USAGE);
|
|
1162
|
+
} else if (t === '--agents' || t === '--claude' || t === '--codex') picked.add(t);
|
|
1163
|
+
else throw usageError(`Unexpected ${JSON.stringify(t)}. ${SKILL_USAGE}`);
|
|
1164
|
+
}
|
|
1165
|
+
if (!install && (force || picked.size > 0 || customRoot)) {
|
|
1166
|
+
throw usageError(`Those flags need \`--install\`. ${SKILL_USAGE}`);
|
|
1167
|
+
}
|
|
1168
|
+
const text = skillText();
|
|
1169
|
+
if (!install) {
|
|
1170
|
+
if (text !== null) {
|
|
1171
|
+
out(text.trimEnd());
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
out(`# ${SKILL_NAME} — not bundled in this single-file copy\n\nInstall the package and retry:\n npm i -g takibibase && takibi skill --install\nDocs: ${DOCS_AGENTS_URL}`);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (text === null) {
|
|
1178
|
+
throw new CliError('This single-file copy carries no SKILL.md to install.', {
|
|
1179
|
+
hint: `Install the package first (\`npm i -g takibibase\`), then \`takibi skill --install\`. Docs: ${DOCS_AGENTS_URL}.`,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const home = skillHome();
|
|
1183
|
+
let targets;
|
|
1184
|
+
if (customRoot && picked.size === 0) targets = [];
|
|
1185
|
+
else if (picked.size === 0) {
|
|
1186
|
+
const detected = SKILL_TARGETS.filter((t) => existsSync(join(home, t.harness)));
|
|
1187
|
+
targets = detected.length > 0 ? detected : [SKILL_TARGETS[0]];
|
|
1188
|
+
} else {
|
|
1189
|
+
targets = SKILL_TARGETS.filter((t) => picked.has(t.flag));
|
|
1190
|
+
}
|
|
1191
|
+
if (customRoot) targets = [...targets, { flag: '--dir', custom: true }];
|
|
1192
|
+
const results = [];
|
|
1193
|
+
for (const t of targets) {
|
|
1194
|
+
const dir = t.custom ? join(customRoot, SKILL_NAME) : join(home, t.dir, SKILL_NAME);
|
|
1195
|
+
const dest = join(dir, SKILL_FILE);
|
|
1196
|
+
if (existsSync(dest)) {
|
|
1197
|
+
let same = false;
|
|
1198
|
+
try {
|
|
1199
|
+
same = readFileSync(dest, 'utf8') === text;
|
|
1200
|
+
} catch {
|
|
1201
|
+
same = false;
|
|
1202
|
+
}
|
|
1203
|
+
if (same) {
|
|
1204
|
+
results.push({ path: dest, status: 'already-installed' });
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (!force) {
|
|
1208
|
+
throw new CliError(`${dest} already holds a different skill file.`, {
|
|
1209
|
+
hint: 'Re-run with --force to overwrite it.',
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
mkdirSync(dir, { recursive: true });
|
|
1214
|
+
writeFileSync(dest, text);
|
|
1215
|
+
results.push({ path: dest, status: 'installed' });
|
|
1216
|
+
}
|
|
1217
|
+
if (globals.json) out(JSON.stringify({ skill: SKILL_NAME, results }, null, 2));
|
|
1218
|
+
else for (const r of results) out(`${r.status === 'installed' ? 'installed' : 'already installed'} ${r.path}`);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1078
1221
|
async function main(argv) {
|
|
1079
1222
|
const { globals, rest } = parseArgv(argv);
|
|
1080
1223
|
if (globals.version) {
|
|
@@ -1088,6 +1231,7 @@ async function main(argv) {
|
|
|
1088
1231
|
out(HELP);
|
|
1089
1232
|
return;
|
|
1090
1233
|
}
|
|
1234
|
+
if (cmd !== 'skill') maybeNudgeSkill({ json: globals.json });
|
|
1091
1235
|
if (cmd === 'ask') return cmdAsk(tokens, globals);
|
|
1092
1236
|
if (cmd === 'search') return cmdSearch(tokens, globals);
|
|
1093
1237
|
if (cmd === 'tasks' || cmd === 'task') return cmdTasks(tokens, globals);
|
|
@@ -1095,6 +1239,7 @@ async function main(argv) {
|
|
|
1095
1239
|
if (cmd === 'notes' || cmd === 'note') return cmdNotes(tokens, globals);
|
|
1096
1240
|
if (cmd === 'projects') return cmdProjects(tokens, globals);
|
|
1097
1241
|
if (cmd === 'version') return cmdVersion(globals);
|
|
1242
|
+
if (cmd === 'skill') return cmdSkill(tokens, globals);
|
|
1098
1243
|
throw usageError(`Unknown command ${JSON.stringify(cmd)}. See \`takibi --help\`.`);
|
|
1099
1244
|
}
|
|
1100
1245
|
|