takibibase 1.2.0 → 1.3.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.
Files changed (4) hide show
  1. package/README.md +16 -10
  2. package/SKILL.md +28 -17
  3. package/package.json +2 -2
  4. package/takibi.mjs +150 -12
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`, `notes`), scoped to its
26
- project(s). Copy the `<publicId>.<secret>` shown once.
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. Optional: one base-URL line in `~/.takibi/config` (default
29
- `http://localhost:3849`). Env overrides: `$TAKIBI_KEY_FILE`,
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 + the skill text (or installed skill). Acceptance: a
41
- fresh agent asks, searches, lists tasks, appends/searches notes, and
42
- uses `notes list --all` to find the current version before curation.
43
- Reads are free; task writes and notes export/keep/remove need your approval
44
- in conversation; account-only routes stay yours.
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 tools/takibi/takibi.mjs …` from the takibi-base checkout
15
+ or `node <path>/takibi.mjs …` for the single-file download
16
16
  (use the alias when it exists).
17
- - Key: the founder saves one `<publicId>.<secret>` line to `~/.takibi/key`
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 `http://localhost:3849` (local boot). Override with
21
- `$TAKIBI_BASE_URL` or one URL line in `~/.takibi/config`.
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`). Silence it with
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,7 +37,7 @@ 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 founder-only; the CLI says so — use `doc text`.
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).
@@ -103,21 +102,33 @@ prose presented as sourced.
103
102
 
104
103
  - Reads are free: ask, search, tasks list/get, doc list/get/text,
105
104
  notes list/search. Agents may append their own debriefs (auto-expire).
106
- - Notes export/keep/remove need founder approval already given in the
105
+ - Notes export/keep/remove need owner approval already given in the
107
106
  conversation. Export only creates a draft; verify it before adding
108
107
  content to Sources.
109
- - Task claim/status/artifact writes only with founder approval already
108
+ - Task claim/status/artifact writes only with owner approval already
110
109
  given in conversation. Claim-first: a plain key must claim a card before
111
- moving or touching it; only orchestrators/founders accept (`review→done`),
112
- assign others, or archive.
113
- - Founder-only routes (upload, delete, retry, download originals, PATCH
114
- docs/projects) are never the agent's to call — ask the founder.
110
+ moving or touching it; only orchestrators/owners accept (`review→done`),
111
+ assign others, or archive. Title/body edits need the create cap —
112
+ claim-only keys can drive a card but cannot rewrite its text.
113
+ - Boards need a whole-collection grant: folder-only or tag-only keys 403
114
+ on every task route (the full-collection server message says so verbatim
115
+ when the key holds the required cap; keys lacking the cap get the generic
116
+ missing-capability 403 first).
117
+ - Notes are opt-in per profile: append, export, keep, and remove may 403
118
+ with missing-capability on keys without the notes caps — ask the owner
119
+ to enable them in the profile editor.
120
+ - Account-only routes (upload, delete, retry, download originals, PATCH
121
+ docs/projects) are never the agent's to call — ask the account owner.
115
122
 
116
123
  ## Failure table
117
124
 
118
- - 401: key wrong/missing/revoked/disabled — or a founder-only route
119
- (the CLI names it). 403: outside the grant or orchestrator-only.
125
+ - 401: key wrong/missing/revoked/disabled — or an account-only route
126
+ (the CLI names it). 403: outside the grant, orchestrator-only, missing
127
+ capability (notes are opt-in), or folder-/tag-only key on a board route.
120
128
  404: bad id (the server hides grant gaps as 404 too).
121
129
  - 409 on claim: someone already holds the card — the message names them.
130
+ - 422 SECRET_BLOCKED: the secret filter fired — it covers task
131
+ title/body/blockedReason/artifact text as well as notes. Strip keys,
132
+ tokens, and credentials and retry.
122
133
  - 429: minute throttle (slow down) or daily budget spent (resets tomorrow).
123
- - Cannot-reach errors: the API is not booted; `takibi version` probes it.
134
+ - 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.2.0",
3
+ "version": "1.3.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 localhost:3849.
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.2.0';
37
+ const BAKED_VERSION = '1.3.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 = 'http://localhost:3849';
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), in CI, and
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 || process.env.TAKIBI_NO_UPDATE_CHECK === '1') return;
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) return 'Outside this key’s grant, or an owner/orchestrator-only move. Check `tasks get`, or ask the account owner.';
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 from the note and retry. Secrets never belong in notes.';
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: 'Is the API booted? `takibi version` probes it without needing a key.',
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`);
@@ -367,6 +371,8 @@ Commands:
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,
@@ -391,12 +397,16 @@ Per-command options:
391
397
  notes export: --since <ts> or --note <uuid> (repeatable; up to 50)
392
398
  notes keep/remove: <id> <expected-version> from notes list --all
393
399
  projects: --add <name> <uuid>
400
+ skill: --install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]
394
401
 
395
402
  Setup: save the owner-provided key (one <publicId>.<secret> line) to
396
- ~/.takibi/key (chmod 600), optional base URL line to ~/.takibi/config.
403
+ ~/.takibi/key (chmod 600). The API defaults to Takibi's servers; only
404
+ local setups need a base URL line in ~/.takibi/config.
397
405
  Env overrides: $TAKIBI_KEY_FILE, $TAKIBI_BASE_URL. Then \`takibi version\`
398
406
  to probe the API and \`takibi projects\` to see this key's scope
399
407
  (\`projects --add\` keeps local aliases for offline use).
408
+ Agents: \`takibi skill --install\` puts the takibi-use skill where
409
+ assistants look for it.
400
410
 
401
411
  Reads are free. Export stamps included notes. Task claim/status/artifact writes need owner approval in
402
412
  conversation. Account-only routes (upload, delete, retry, download originals,
@@ -437,9 +447,9 @@ function parseArgv(argv) {
437
447
  i = ni;
438
448
  }
439
449
  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') {
450
+ 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
451
  rest.push(a);
442
- if (a !== '--all') {
452
+ if (a !== '--all' && a !== '--install' && a !== '--force' && a !== '--agents' && a !== '--claude' && a !== '--codex') {
443
453
  const [v, ni] = takeValue(a, i);
444
454
  rest.push(v);
445
455
  i = ni;
@@ -1075,6 +1085,132 @@ async function cmdVersion(globals) {
1075
1085
  else out(`${data.service ?? 'takibi-api'} ${data.version ?? '?'} · jev ${data.jev ?? '?'} · via ${baseUrl}`);
1076
1086
  }
1077
1087
 
1088
+ const SKILL_NAME = 'takibi-use';
1089
+ const SKILL_FILE = 'SKILL.md';
1090
+ const DOCS_AGENTS_URL = 'https://takibibase.com/docs/agents';
1091
+
1092
+ /** Test hook: redirect ~ for skill installs only (key resolution always uses the real home). */
1093
+ const skillHome = () => process.env.TAKIBI_HOME || homedir();
1094
+
1095
+ /** The skill text travelling next to the CLI in the published package; null in the lone-file copy. */
1096
+ function skillText() {
1097
+ try {
1098
+ return readFileSync(new URL('./SKILL.md', import.meta.url), 'utf8');
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+
1104
+ const SKILL_TARGETS = [
1105
+ { flag: '--agents', harness: '.agents', dir: '.agents/skills' },
1106
+ { flag: '--claude', harness: '.claude', dir: '.claude/skills' },
1107
+ { flag: '--codex', harness: '.codex', dir: '.codex/skills' },
1108
+ ];
1109
+
1110
+ const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]]';
1111
+
1112
+ /** First-run skill nudge. Stderr only (stdout stays clean), best-effort,
1113
+ * never fails the command. Skipped for --json, in CI, with
1114
+ * TAKIBI_NO_SKILL_NUDGE=1, when any standard skills dir already holds the
1115
+ * skill, and within a day of the last nudge. */
1116
+ function maybeNudgeSkill({ json = false } = {}) {
1117
+ try {
1118
+ if (json || process.env.CI || process.env.TAKIBI_NO_SKILL_NUDGE === '1') return;
1119
+ const home = skillHome();
1120
+ if (SKILL_TARGETS.some((t) => existsSync(join(home, t.dir, SKILL_NAME, SKILL_FILE)))) return;
1121
+ const path = join(home, '.takibi', 'skill-nudge');
1122
+ let lastAt = 0;
1123
+ try {
1124
+ const cached = JSON.parse(readFileSync(path, 'utf8'));
1125
+ if (typeof cached?.at === 'number') lastAt = cached.at;
1126
+ } catch {
1127
+ // First run: no marker yet.
1128
+ }
1129
+ if (Date.now() - lastAt < UPDATE_CHECK_TTL_MS) return;
1130
+ try {
1131
+ mkdirSync(join(home, '.takibi'), { recursive: true });
1132
+ writeFileSync(path, JSON.stringify({ at: Date.now() }));
1133
+ } catch {
1134
+ // Read-only home: nudge once per process run instead of daily.
1135
+ }
1136
+ err('takibi: no takibi-use skill installed for your agents.');
1137
+ err('hint: `takibi skill --install` puts it where assistants look.');
1138
+ } catch {
1139
+ // Nudges never fail commands.
1140
+ }
1141
+ }
1142
+
1143
+ function cmdSkill(tokens, globals) {
1144
+ let install = false;
1145
+ let force = false;
1146
+ let customRoot = null;
1147
+ const picked = new Set();
1148
+ for (let i = 0; i < tokens.length; i++) {
1149
+ const t = tokens[i];
1150
+ if (t === '--install') install = true;
1151
+ else if (t === '--force') force = true;
1152
+ else if (t === '--dir') {
1153
+ customRoot = tokens[++i];
1154
+ if (!customRoot) throw usageError('`--dir` needs a skills root. ' + SKILL_USAGE);
1155
+ } else if (t === '--agents' || t === '--claude' || t === '--codex') picked.add(t);
1156
+ else throw usageError(`Unexpected ${JSON.stringify(t)}. ${SKILL_USAGE}`);
1157
+ }
1158
+ if (!install && (force || picked.size > 0 || customRoot)) {
1159
+ throw usageError(`Those flags need \`--install\`. ${SKILL_USAGE}`);
1160
+ }
1161
+ const text = skillText();
1162
+ if (!install) {
1163
+ if (text !== null) {
1164
+ out(text.trimEnd());
1165
+ return;
1166
+ }
1167
+ 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}`);
1168
+ return;
1169
+ }
1170
+ if (text === null) {
1171
+ throw new CliError('This single-file copy carries no SKILL.md to install.', {
1172
+ hint: `Install the package first (\`npm i -g takibibase\`), then \`takibi skill --install\`. Docs: ${DOCS_AGENTS_URL}.`,
1173
+ });
1174
+ }
1175
+ const home = skillHome();
1176
+ let targets;
1177
+ if (customRoot && picked.size === 0) targets = [];
1178
+ else if (picked.size === 0) {
1179
+ const detected = SKILL_TARGETS.filter((t) => existsSync(join(home, t.harness)));
1180
+ targets = detected.length > 0 ? detected : [SKILL_TARGETS[0]];
1181
+ } else {
1182
+ targets = SKILL_TARGETS.filter((t) => picked.has(t.flag));
1183
+ }
1184
+ if (customRoot) targets = [...targets, { flag: '--dir', custom: true }];
1185
+ const results = [];
1186
+ for (const t of targets) {
1187
+ const dir = t.custom ? join(customRoot, SKILL_NAME) : join(home, t.dir, SKILL_NAME);
1188
+ const dest = join(dir, SKILL_FILE);
1189
+ if (existsSync(dest)) {
1190
+ let same = false;
1191
+ try {
1192
+ same = readFileSync(dest, 'utf8') === text;
1193
+ } catch {
1194
+ same = false;
1195
+ }
1196
+ if (same) {
1197
+ results.push({ path: dest, status: 'already-installed' });
1198
+ continue;
1199
+ }
1200
+ if (!force) {
1201
+ throw new CliError(`${dest} already holds a different skill file.`, {
1202
+ hint: 'Re-run with --force to overwrite it.',
1203
+ });
1204
+ }
1205
+ }
1206
+ mkdirSync(dir, { recursive: true });
1207
+ writeFileSync(dest, text);
1208
+ results.push({ path: dest, status: 'installed' });
1209
+ }
1210
+ if (globals.json) out(JSON.stringify({ skill: SKILL_NAME, results }, null, 2));
1211
+ else for (const r of results) out(`${r.status === 'installed' ? 'installed' : 'already installed'} ${r.path}`);
1212
+ }
1213
+
1078
1214
  async function main(argv) {
1079
1215
  const { globals, rest } = parseArgv(argv);
1080
1216
  if (globals.version) {
@@ -1088,6 +1224,7 @@ async function main(argv) {
1088
1224
  out(HELP);
1089
1225
  return;
1090
1226
  }
1227
+ if (cmd !== 'skill') maybeNudgeSkill({ json: globals.json });
1091
1228
  if (cmd === 'ask') return cmdAsk(tokens, globals);
1092
1229
  if (cmd === 'search') return cmdSearch(tokens, globals);
1093
1230
  if (cmd === 'tasks' || cmd === 'task') return cmdTasks(tokens, globals);
@@ -1095,6 +1232,7 @@ async function main(argv) {
1095
1232
  if (cmd === 'notes' || cmd === 'note') return cmdNotes(tokens, globals);
1096
1233
  if (cmd === 'projects') return cmdProjects(tokens, globals);
1097
1234
  if (cmd === 'version') return cmdVersion(globals);
1235
+ if (cmd === 'skill') return cmdSkill(tokens, globals);
1098
1236
  throw usageError(`Unknown command ${JSON.stringify(cmd)}. See \`takibi --help\`.`);
1099
1237
  }
1100
1238