takibibase 1.0.0 → 1.0.2

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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2026, Takibi (also known as Takibi Base, takibibase.com).
1
+ Copyright (c) 2026, Deian Isac (operating as Takibi / Takibi Base, takibibase.com).
2
2
  All rights reserved.
3
3
 
4
4
  This software is proprietary to Takibi. You may install and run it
package/README.md CHANGED
@@ -9,11 +9,12 @@ Needs Node.js 22+. 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
- Thin client-side enablement for agents. Zero changes to `apps/api`,
13
- `apps/web`, schema, or grants — this directory is the whole feature.
12
+ Thin client-side enablement for agents. Its only server pairing is
13
+ `GET /v1/projects`, which lists a key's granted collections — everything
14
+ else lives in this directory.
14
15
 
15
16
  - `takibi.mjs` — the CLI. Single-file Node ≥ 22, zero dependencies.
16
- Run it as `node tools/takibi/takibi.mjs …`, or alias it:
17
+ Run it from a checkout as `node tools/takibi/takibi.mjs …`, or alias it:
17
18
  `alias takibi='node <checkout>/tools/takibi/takibi.mjs'`.
18
19
  - `SKILL.md` — the `takibi-use` skill. Short on purpose; the CLI carries
19
20
  the contract. Install it into your harness's skills dir, or paste it
@@ -28,10 +29,11 @@ Thin client-side enablement for agents. Zero changes to `apps/api`,
28
29
  3. Optional: one base-URL line in `~/.takibi/config` (default
29
30
  `http://localhost:3849`). Env overrides: `$TAKIBI_KEY_FILE`,
30
31
  `$TAKIBI_BASE_URL`.
31
- 4. `takibi projects --add <name> <uuid>` per project (UUIDs are in
32
- Settings → Projects). `GET /v1/projects` is account-only, so names
33
- live in `~/.takibi/projects` as `name=uuid` lines; UUIDs also work
34
- directly in `--project`, and single-grant keys may omit it.
32
+ 4. `takibi projects` — lists the projects this key can reach, straight
33
+ from the API. Names resolve in `--project` (UUIDs work too, from
34
+ Settings → Projects); single-grant keys may omit it. Only for offline
35
+ use: `takibi projects --add <name> <uuid>` keeps local aliases in
36
+ `~/.takibi/projects`.
35
37
  5. `takibi version` — probes the API without needing the key.
36
38
 
37
39
  ## Give an agent
package/SKILL.md CHANGED
@@ -19,9 +19,9 @@ error hints.
19
19
  committed. If a command says the key is missing, stop and ask.
20
20
  - Base URL defaults to `http://localhost:3849` (local boot). Override with
21
21
  `$TAKIBI_BASE_URL` or one URL line in `~/.takibi/config`.
22
- - Projects: `takibi projects` lists saved names. `--project` takes a name
23
- or a UUID; single-grant keys may omit it. New name? Ask the founder for
24
- the UUID once, then `takibi projects --add <name> <uuid>`.
22
+ - Projects: `takibi projects` lists what this key can reach. `--project`
23
+ takes a name or a UUID; single-grant keys may omit it.
24
+ `takibi projects --add <name> <uuid>` keeps local aliases for offline use.
25
25
  - First probe: `takibi version` (needs no key; shows build + `jev` status).
26
26
 
27
27
  ## Commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Thin CLI for the Takibi API: ask, search, tasks, docs. Single file, zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
package/takibi.mjs CHANGED
@@ -11,9 +11,10 @@
11
11
  * - Base URL: $TAKIBI_BASE_URL or ~/.takibi/config, else localhost:3849.
12
12
  *
13
13
  * Pure-client deviations the server forces (no apps/api changes allowed):
14
- * - GET /v1/projects is account-only, so `projects` reads a LOCAL map
15
- * (~/.takibi/projects, `name=uuid` lines) and --project resolves names
16
- * through it. UUIDs pass straight through.
14
+ * - Keys list their own granted scope via GET /v1/projects, so `projects`
15
+ * reads the API first and --project resolves names through it. A LOCAL
16
+ * map (~/.takibi/projects, `name=uuid` lines) remains as the offline
17
+ * fallback and for custom aliases. UUIDs pass straight through.
17
18
  * - There is no GET /v1/tasks/:id, so `tasks get` lists and filters.
18
19
  * - Account-only routes (doc download, uploads, deletes) answer 401, not
19
20
  * 403, to Bearer callers; `doc download` refuses client-side with a hint.
@@ -26,11 +27,11 @@ import { homedir } from 'node:os';
26
27
  import { join } from 'node:path';
27
28
 
28
29
  /** Baked fallback; the published package re-reads package.json next door. */
29
- const BAKED_VERSION = '1.0.0';
30
+ const BAKED_VERSION = '1.0.2';
30
31
  const CLI_VERSION = (() => {
31
32
  try {
32
33
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
33
- if (typeof pkg.version === 'string' && pkg.version.length > 0) return pkg.version;
34
+ if (pkg.name === 'takibibase' && typeof pkg.version === 'string' && pkg.version.length > 0) return pkg.version;
34
35
  } catch {
35
36
  // Single-file download: no package.json travels with takibi.mjs.
36
37
  }
@@ -126,11 +127,50 @@ function loadProjectMap() {
126
127
  return { map, warnings, path };
127
128
  }
128
129
 
129
- /** --project value → uuid-or-undefined. Names resolve via the local map. */
130
- function resolveProjectFlag(value) {
130
+ /** Failures that mean "answer from the local map instead": old servers that
131
+ * still gate the list account-only, and an unreachable API when a key is on
132
+ * disk (offline mode). Config errors — bad base URL, malformed key file —
133
+ * are the truth and rethrow. */
134
+ function isListFallbackable(e) {
135
+ if (!(e instanceof CliError)) return false;
136
+ if (e.status === 401 && /login required/i.test(e.message)) return true;
137
+ return e.status == null && /^Cannot reach the API at /.test(e.message);
138
+ }
139
+
140
+ /** --project value → uuid-or-undefined. Names resolve via the API list first, then the local map. */
141
+ async function resolveProjectFlag(value, ctx) {
131
142
  if (value === undefined || value === null || value === '') return undefined;
132
143
  const v = String(value).trim();
133
144
  if (UUID_RE.test(v)) return v;
145
+ if (ctx?.key && ctx?.baseUrl) {
146
+ let data;
147
+ try {
148
+ data = await api('GET', '/v1/projects', { baseUrl: ctx.baseUrl, key: ctx.key, verbose: ctx.verbose, label: 'projects' });
149
+ } catch (e) {
150
+ if (!isListFallbackable(e)) throw e;
151
+ const { map } = loadProjectMap();
152
+ const hit = map.get(v);
153
+ if (hit) return hit;
154
+ throw e; // Offline and unmapped: the reachability error beats "Unknown project".
155
+ }
156
+ // Collection names are not unique: duplicates must not silently resolve.
157
+ const matches = (data?.projects ?? []).filter((p) => p.name === v);
158
+ if (matches.length > 1) {
159
+ throw new CliError(`More than one project is named ${JSON.stringify(v)}.`, {
160
+ hint: 'Pass --project <uuid> instead; `takibi projects` lists ids.',
161
+ });
162
+ }
163
+ if (matches[0]?.id) return matches[0].id;
164
+ // Online but unnamed in the grant list: local aliases still apply, and the
165
+ // server gates the resolved id on the real call.
166
+ const { map } = loadProjectMap();
167
+ const alias = map.get(v);
168
+ if (alias) return alias;
169
+ const serverNames = (data?.projects ?? []).map((p) => p.name).filter(Boolean);
170
+ throw new CliError(`Unknown project ${JSON.stringify(v)}.`, {
171
+ hint: serverNames.length > 0 ? `This key reaches: ${serverNames.join(', ')}.` : 'This key reaches no projects.',
172
+ });
173
+ }
134
174
  const { map } = loadProjectMap();
135
175
  const hit = map.get(v);
136
176
  if (hit) return hit;
@@ -138,8 +178,8 @@ function resolveProjectFlag(value) {
138
178
  throw new CliError(`Unknown project ${JSON.stringify(v)}.`, {
139
179
  hint:
140
180
  known.length > 0
141
- ? `Known names: ${known.join(', ')}. Pass a project UUID, or ask the account owner for this one.`
142
- : 'Pass a project UUID, or ask the account owner to add names via `takibi projects --add <name> <uuid>`.',
181
+ ? `Known names: ${known.join(', ')}. Run \`takibi projects\` to see what this key can reach, or pass a project UUID.`
182
+ : 'Run `takibi projects` to see what this key can reach, or pass a project UUID.',
143
183
  });
144
184
  }
145
185
 
@@ -231,12 +271,13 @@ Commands:
231
271
  doc list Documents (metadata)
232
272
  doc get <id> One document's metadata
233
273
  doc text <id> Converted text of one document
234
- projects Saved project names (-> uuids for --project)
274
+ projects Granted projects (names -> uuids for --project)
235
275
  version What build is serving + jev wired|unwired (no key needed)
236
276
 
237
277
  Global options (accepted before or after the command):
238
- --project <name-or-uuid> Project scope. Names resolve via ~/.takibi/projects;
239
- single-grant keys may omit it entirely.
278
+ --project <name-or-uuid> Project scope. Names resolve via the API list,
279
+ then ~/.takibi/projects; single-grant keys
280
+ may omit it entirely.
240
281
  --folder <uuid> Folder scope (ask, search, doc list only).
241
282
  --json Raw server JSON instead of human-readable output.
242
283
  --verbose Log method + URL + status to stderr (never the key).
@@ -250,9 +291,10 @@ Per-command options:
250
291
  projects: --add <name> <uuid>
251
292
 
252
293
  Setup: save the owner-provided key (one <publicId>.<secret> line) to
253
- ~/.takibi/key (chmod 600), optional base URL line to ~/.takibi/config,
254
- project names via \`takibi projects --add\`. Env overrides: $TAKIBI_KEY_FILE,
255
- $TAKIBI_BASE_URL. Then \`takibi version\` to probe the API.
294
+ ~/.takibi/key (chmod 600), optional base URL line to ~/.takibi/config.
295
+ Env overrides: $TAKIBI_KEY_FILE, $TAKIBI_BASE_URL. Then \`takibi version\`
296
+ to probe the API and \`takibi projects\` to see this key's scope
297
+ (\`projects --add\` keeps local aliases for offline use).
256
298
 
257
299
  Reads are free. Task claim/status/artifact writes need owner approval in
258
300
  conversation. Account-only routes (upload, delete, retry, download originals,
@@ -404,10 +446,10 @@ function renderDoc(d) {
404
446
  }
405
447
 
406
448
  /** Shared context: baseUrl always; key for every command except version/help. */
407
- function ctxFor(globals, { needKey }) {
449
+ async function ctxFor(globals, { needKey }) {
408
450
  const baseUrl = resolveBaseUrl();
409
451
  const key = needKey ? resolveKey() : null;
410
- const projectId = resolveProjectFlag(globals.project);
452
+ const projectId = await resolveProjectFlag(globals.project, { baseUrl, key, verbose: globals.verbose });
411
453
  const { warnings } = loadProjectMap();
412
454
  for (const w of warnings) err(`warning: ${w}`);
413
455
  if (globals.folder !== undefined && globals.folder !== null && globals.folder !== '' && !UUID_RE.test(globals.folder)) {
@@ -418,7 +460,7 @@ function ctxFor(globals, { needKey }) {
418
460
 
419
461
  async function cmdAsk(tokens, globals) {
420
462
  const { q, k } = parseQueryArgs(tokens, { kMin: 1, kMax: 12 });
421
- const ctx = ctxFor(globals, { needKey: true });
463
+ const ctx = await ctxFor(globals, { needKey: true });
422
464
  // GET carries the contract; POST only when q outgrows a sane URL.
423
465
  const data =
424
466
  q.length > 1500
@@ -443,7 +485,7 @@ async function cmdAsk(tokens, globals) {
443
485
 
444
486
  async function cmdSearch(tokens, globals) {
445
487
  const { q, k } = parseQueryArgs(tokens, { kMin: 1, kMax: 20 });
446
- const ctx = ctxFor(globals, { needKey: true });
488
+ const ctx = await ctxFor(globals, { needKey: true });
447
489
  const data = await api('GET', '/v1/search', {
448
490
  ...ctx,
449
491
  query: qparams([
@@ -479,7 +521,7 @@ async function cmdTasks(tokens, globals) {
479
521
  if (t === '--all') includeArchived = true;
480
522
  else throw usageError(`Unexpected ${JSON.stringify(t)}. Usage: takibi tasks list [--all]`);
481
523
  }
482
- const ctx = ctxFor(globals, { needKey: true });
524
+ const ctx = await ctxFor(globals, { needKey: true });
483
525
  const tasks = await listTasks(ctx, includeArchived);
484
526
  if (ctx.json) {
485
527
  out(JSON.stringify({ tasks }, null, 2));
@@ -492,7 +534,7 @@ async function cmdTasks(tokens, globals) {
492
534
  if (sub === 'get') {
493
535
  const [id, extra] = rest;
494
536
  if (!id || extra) throw usageError('Usage: takibi tasks get <id>');
495
- const ctx = ctxFor(globals, { needKey: true });
537
+ const ctx = await ctxFor(globals, { needKey: true });
496
538
  let tasks = await listTasks(ctx, false);
497
539
  let found = tasks.find((t) => t.id === id);
498
540
  if (!found) {
@@ -507,7 +549,7 @@ async function cmdTasks(tokens, globals) {
507
549
  if (sub === 'claim') {
508
550
  const [id, extra] = rest;
509
551
  if (!id || extra) throw usageError('Usage: takibi tasks claim <id>');
510
- const ctx = ctxFor(globals, { needKey: true });
552
+ const ctx = await ctxFor(globals, { needKey: true });
511
553
  const data = await api('POST', `/v1/tasks/${id}/claim`, { ...ctx, query: qparams([['projectId', ctx.projectId]]), label: 'tasks claim' });
512
554
  if (ctx.json) out(JSON.stringify(data, null, 2));
513
555
  else renderTask(data.task);
@@ -517,7 +559,7 @@ async function cmdTasks(tokens, globals) {
517
559
  const [id, to, extra] = rest;
518
560
  if (!id || !to || extra) throw usageError('Usage: takibi tasks status <id> <todo|in_progress|review|done>');
519
561
  if (!TASK_STATUSES.includes(to)) throw usageError(`Status is one of ${TASK_STATUSES.join(' | ')}, not ${JSON.stringify(to)}.`);
520
- const ctx = ctxFor(globals, { needKey: true });
562
+ const ctx = await ctxFor(globals, { needKey: true });
521
563
  const data = await api('PATCH', `/v1/tasks/${id}`, {
522
564
  ...ctx,
523
565
  query: qparams([['projectId', ctx.projectId]]),
@@ -543,7 +585,7 @@ async function cmdTasks(tokens, globals) {
543
585
  if (!Number.isInteger(n) || n < 0) throw usageError('--size takes an integer >= 0 (asserted bytes).');
544
586
  flags.size = n;
545
587
  }
546
- const ctx = ctxFor(globals, { needKey: true });
588
+ const ctx = await ctxFor(globals, { needKey: true });
547
589
  const data = await api('POST', `/v1/tasks/${taskId}/artifacts`, {
548
590
  ...ctx,
549
591
  query: qparams([['projectId', ctx.projectId]]),
@@ -568,7 +610,7 @@ async function cmdDoc(tokens, globals) {
568
610
  if (rest[i] === '--limit') limit = rest[++i];
569
611
  else throw usageError(`Unexpected ${JSON.stringify(rest[i])}. Usage: takibi doc list [--limit <n>]`);
570
612
  }
571
- const ctx = ctxFor(globals, { needKey: true });
613
+ const ctx = await ctxFor(globals, { needKey: true });
572
614
  const data = await api('GET', '/v1/documents', {
573
615
  ...ctx,
574
616
  query: qparams([
@@ -595,7 +637,7 @@ async function cmdDoc(tokens, globals) {
595
637
  if (sub === 'text' && flagTokens[i] === '--max-chars') maxChars = flagTokens[++i];
596
638
  else throw usageError(`Unexpected ${JSON.stringify(flagTokens[i])}. Usage: takibi doc ${sub} <id>${sub === 'text' ? ' [--max-chars <n>]' : ''}`);
597
639
  }
598
- const ctx = ctxFor(globals, { needKey: true });
640
+ const ctx = await ctxFor(globals, { needKey: true });
599
641
  if (sub === 'get') {
600
642
  const data = await api('GET', `/v1/documents/${id}`, { ...ctx, query: qparams([['projectId', ctx.projectId]]), label: 'doc get' });
601
643
  if (ctx.json) out(JSON.stringify(data, null, 2));
@@ -659,14 +701,42 @@ async function cmdProjects(tokens, globals) {
659
701
  return;
660
702
  }
661
703
  if (tokens.length > 0) throw usageError(`Unexpected ${JSON.stringify(tokens[0])}. Usage: takibi projects [--add <name> <uuid>]`);
662
- const entries = [...map.entries()].map(([name, id]) => ({ name, id }));
704
+ let entries = null;
705
+ try {
706
+ const key = resolveKey();
707
+ const data = await api('GET', '/v1/projects', { baseUrl: resolveBaseUrl(), key, verbose: globals.verbose, label: 'projects' });
708
+ const seen = new Set();
709
+ entries = [];
710
+ for (const p of data?.projects ?? []) {
711
+ if (p?.id && !seen.has(p.id)) {
712
+ seen.add(p.id);
713
+ entries.push({ name: p.name, id: p.id });
714
+ }
715
+ }
716
+ // Local aliases stay visible alongside the server list.
717
+ for (const [name, id] of map.entries()) {
718
+ if (!seen.has(id)) {
719
+ seen.add(id);
720
+ entries.push({ name, id });
721
+ }
722
+ }
723
+ } catch (e) {
724
+ if (e instanceof CliError && /No API key file/.test(e.message)) {
725
+ entries = null; // Keyless: answer from the map below, like before.
726
+ } else if (isListFallbackable(e)) {
727
+ entries = null;
728
+ } else {
729
+ throw e;
730
+ }
731
+ }
732
+ if (entries === null) entries = [...map.entries()].map(([name, id]) => ({ name, id }));
663
733
  if (globals.json) {
664
734
  out(JSON.stringify({ projects: entries }, null, 2));
665
735
  return;
666
736
  }
667
737
  if (!entries.length) {
668
- out('No project names saved.');
669
- err(`(GET /v1/projects is account-only, so names live in ${path} as name=uuid lines.\nAdd them with \`takibi projects --add <name> <uuid>\` (ask the account owner for UUIDs),\nor pass --project <uuid> directly. Single-grant keys may omit --project.)`);
738
+ out('No projects found.');
739
+ err(`(This key sees no granted projects, or the API is unreachable and ${path} is empty.\nAdd names with \`takibi projects --add <name> <uuid>\`, or pass --project <uuid> directly. Single-grant keys may omit --project.)`);
670
740
  return;
671
741
  }
672
742
  for (const { name, id } of entries) out(`${name} → ${id}`);
@@ -682,7 +752,7 @@ async function cmdVersion(globals) {
682
752
  async function main(argv) {
683
753
  const { globals, rest } = parseArgv(argv);
684
754
  if (globals.version) {
685
- out(`takibibase ${CLI_VERSION}`);
755
+ out(`takibi ${CLI_VERSION}`);
686
756
  return;
687
757
  }
688
758
  const [cmd, ...tokens] = rest;