takibibase 1.0.0 → 1.0.3

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,10 +19,14 @@ 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
+ - 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`.
26
30
 
27
31
  ## Commands
28
32
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "takibibase",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Thin CLI for the Takibi API: ask, search, tasks, docs. Single file, zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "takibi": "./takibi.mjs"
7
+ "takibi": "takibi.mjs"
8
8
  },
9
9
  "files": [
10
10
  "takibi.mjs",
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.
@@ -21,21 +22,24 @@
21
22
  * Exit codes: 0 ok (honest abstains included), 1 transport/API error,
22
23
  * 2 usage error.
23
24
  */
24
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeSync } from 'node:fs';
25
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, writeSync } from 'node:fs';
25
26
  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 CLI_VERSION = (() => {
30
+ const BAKED_VERSION = '1.0.3';
31
+ const CLI_INFO = (() => {
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) {
35
+ return { version: pkg.version, singleFile: false };
36
+ }
34
37
  } catch {
35
38
  // Single-file download: no package.json travels with takibi.mjs.
36
39
  }
37
- return BAKED_VERSION;
40
+ return { version: BAKED_VERSION, singleFile: true };
38
41
  })();
42
+ const CLI_VERSION = CLI_INFO.version;
39
43
  const DEFAULT_BASE_URL = 'http://localhost:3849';
40
44
  const REQUEST_TIMEOUT_MS = 90_000;
41
45
  const USER_AGENT = `takibibase/${CLI_VERSION}`;
@@ -65,6 +69,78 @@ const usageError = (message) => new CliError(message, { exitCode: 2 });
65
69
 
66
70
  const takibiDir = () => join(homedir(), '.takibi');
67
71
 
72
+ const UPDATE_CHECK_TTL_MS = 86_400_000;
73
+ const REGISTRY_URL = (process.env.TAKIBI_REGISTRY_URL ?? '').trim().replace(/\/+$/, '') || 'https://registry.npmjs.org';
74
+
75
+ /** Numeric dotted-version compare; false on anything unparseable. */
76
+ function versionBehind(current, latest) {
77
+ const parts = (v) => String(v ?? '').trim().split('.');
78
+ const c = parts(current);
79
+ const l = parts(latest);
80
+ for (let i = 0; i < Math.max(c.length, l.length); i++) {
81
+ const a = i < c.length ? Number(c[i]) : 0;
82
+ const b = i < l.length ? Number(l[i]) : 0;
83
+ if (!Number.isInteger(a) || !Number.isInteger(b)) return false;
84
+ if (b > a) return true;
85
+ if (a > b) return false;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ /** Once-a-day latest-version poll. Stderr only (stdout stays clean),
91
+ * best-effort, never fails the command. Failures are cached too, so an
92
+ * unreachable registry costs one slow run a day, not every invocation.
93
+ * Skipped for --json (errors there are a single JSON object), in CI, and
94
+ * with TAKIBI_NO_UPDATE_CHECK=1. */
95
+ async function maybeNotifyUpdate({ json = false } = {}) {
96
+ try {
97
+ if (json || process.env.CI || process.env.TAKIBI_NO_UPDATE_CHECK === '1') return;
98
+ const path = join(takibiDir(), 'update-check');
99
+ let cached = null;
100
+ try {
101
+ cached = JSON.parse(readFileSync(path, 'utf8'));
102
+ } catch {
103
+ // First run: no cache yet.
104
+ }
105
+ const fresh = cached && typeof cached.at === 'number' && Date.now() - cached.at < UPDATE_CHECK_TTL_MS;
106
+ let latest = null;
107
+ if (fresh) {
108
+ if (typeof cached.latest !== 'string' || !cached.latest) return;
109
+ latest = cached.latest;
110
+ } else {
111
+ try {
112
+ const res = await fetch(`${REGISTRY_URL}/takibibase/latest`, { signal: AbortSignal.timeout(1200) });
113
+ const v = res.ok ? (await res.json())?.version : null;
114
+ if (typeof v === 'string' && v) latest = v;
115
+ } catch {
116
+ // Offline: fall through below.
117
+ }
118
+ if (!latest && cached && typeof cached.latest === 'string' && cached.latest) {
119
+ // Failed refresh keeps the last known version: a timeout or 5xx must
120
+ // not delete a nudge we already knew about. The fresh timestamp still
121
+ // stops us hammering the registry.
122
+ latest = cached.latest;
123
+ }
124
+ try {
125
+ mkdirSync(takibiDir(), { recursive: true });
126
+ writeFileSync(path, JSON.stringify({ at: Date.now(), latest }));
127
+ } catch {
128
+ // Read-only home: notifying still works, caching just retries daily.
129
+ }
130
+ if (!latest) return;
131
+ }
132
+ if (!versionBehind(CLI_VERSION, latest)) return;
133
+ err(`takibi: ${latest} is available (you have ${CLI_VERSION}).`);
134
+ err(
135
+ CLI_INFO.singleFile
136
+ ? 'hint: download the latest takibi.mjs from https://takibibase.com/downloads/takibi.mjs'
137
+ : 'hint: `npm i -g takibibase@latest`, or `npx takibibase@latest` for one-shot runs.',
138
+ );
139
+ } catch {
140
+ // Offline, broken cache, slow registry: stay silent, run the command.
141
+ }
142
+ }
143
+
68
144
  /** First non-blank, non-comment line of a file, or null when missing. */
69
145
  function firstLine(path) {
70
146
  if (!existsSync(path)) return null;
@@ -126,11 +202,50 @@ function loadProjectMap() {
126
202
  return { map, warnings, path };
127
203
  }
128
204
 
129
- /** --project value → uuid-or-undefined. Names resolve via the local map. */
130
- function resolveProjectFlag(value) {
205
+ /** Failures that mean "answer from the local map instead": old servers that
206
+ * still gate the list account-only, and an unreachable API when a key is on
207
+ * disk (offline mode). Config errors — bad base URL, malformed key file —
208
+ * are the truth and rethrow. */
209
+ function isListFallbackable(e) {
210
+ if (!(e instanceof CliError)) return false;
211
+ if (e.status === 401 && /login required/i.test(e.message)) return true;
212
+ return e.status == null && /^Cannot reach the API at /.test(e.message);
213
+ }
214
+
215
+ /** --project value → uuid-or-undefined. Names resolve via the API list first, then the local map. */
216
+ async function resolveProjectFlag(value, ctx) {
131
217
  if (value === undefined || value === null || value === '') return undefined;
132
218
  const v = String(value).trim();
133
219
  if (UUID_RE.test(v)) return v;
220
+ if (ctx?.key && ctx?.baseUrl) {
221
+ let data;
222
+ try {
223
+ data = await api('GET', '/v1/projects', { baseUrl: ctx.baseUrl, key: ctx.key, verbose: ctx.verbose, label: 'projects' });
224
+ } catch (e) {
225
+ if (!isListFallbackable(e)) throw e;
226
+ const { map } = loadProjectMap();
227
+ const hit = map.get(v);
228
+ if (hit) return hit;
229
+ throw e; // Offline and unmapped: the reachability error beats "Unknown project".
230
+ }
231
+ // Collection names are not unique: duplicates must not silently resolve.
232
+ const matches = (data?.projects ?? []).filter((p) => p.name === v);
233
+ if (matches.length > 1) {
234
+ throw new CliError(`More than one project is named ${JSON.stringify(v)}.`, {
235
+ hint: 'Pass --project <uuid> instead; `takibi projects` lists ids.',
236
+ });
237
+ }
238
+ if (matches[0]?.id) return matches[0].id;
239
+ // Online but unnamed in the grant list: local aliases still apply, and the
240
+ // server gates the resolved id on the real call.
241
+ const { map } = loadProjectMap();
242
+ const alias = map.get(v);
243
+ if (alias) return alias;
244
+ const serverNames = (data?.projects ?? []).map((p) => p.name).filter(Boolean);
245
+ throw new CliError(`Unknown project ${JSON.stringify(v)}.`, {
246
+ hint: serverNames.length > 0 ? `This key reaches: ${serverNames.join(', ')}.` : 'This key reaches no projects.',
247
+ });
248
+ }
134
249
  const { map } = loadProjectMap();
135
250
  const hit = map.get(v);
136
251
  if (hit) return hit;
@@ -138,8 +253,8 @@ function resolveProjectFlag(value) {
138
253
  throw new CliError(`Unknown project ${JSON.stringify(v)}.`, {
139
254
  hint:
140
255
  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>`.',
256
+ ? `Known names: ${known.join(', ')}. Run \`takibi projects\` to see what this key can reach, or pass a project UUID.`
257
+ : 'Run `takibi projects` to see what this key can reach, or pass a project UUID.',
143
258
  });
144
259
  }
145
260
 
@@ -231,12 +346,13 @@ Commands:
231
346
  doc list Documents (metadata)
232
347
  doc get <id> One document's metadata
233
348
  doc text <id> Converted text of one document
234
- projects Saved project names (-> uuids for --project)
349
+ projects Granted projects (names -> uuids for --project)
235
350
  version What build is serving + jev wired|unwired (no key needed)
236
351
 
237
352
  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.
353
+ --project <name-or-uuid> Project scope. Names resolve via the API list,
354
+ then ~/.takibi/projects; single-grant keys
355
+ may omit it entirely.
240
356
  --folder <uuid> Folder scope (ask, search, doc list only).
241
357
  --json Raw server JSON instead of human-readable output.
242
358
  --verbose Log method + URL + status to stderr (never the key).
@@ -250,9 +366,10 @@ Per-command options:
250
366
  projects: --add <name> <uuid>
251
367
 
252
368
  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.
369
+ ~/.takibi/key (chmod 600), optional base URL line to ~/.takibi/config.
370
+ Env overrides: $TAKIBI_KEY_FILE, $TAKIBI_BASE_URL. Then \`takibi version\`
371
+ to probe the API and \`takibi projects\` to see this key's scope
372
+ (\`projects --add\` keeps local aliases for offline use).
256
373
 
257
374
  Reads are free. Task claim/status/artifact writes need owner approval in
258
375
  conversation. Account-only routes (upload, delete, retry, download originals,
@@ -404,10 +521,10 @@ function renderDoc(d) {
404
521
  }
405
522
 
406
523
  /** Shared context: baseUrl always; key for every command except version/help. */
407
- function ctxFor(globals, { needKey }) {
524
+ async function ctxFor(globals, { needKey }) {
408
525
  const baseUrl = resolveBaseUrl();
409
526
  const key = needKey ? resolveKey() : null;
410
- const projectId = resolveProjectFlag(globals.project);
527
+ const projectId = await resolveProjectFlag(globals.project, { baseUrl, key, verbose: globals.verbose });
411
528
  const { warnings } = loadProjectMap();
412
529
  for (const w of warnings) err(`warning: ${w}`);
413
530
  if (globals.folder !== undefined && globals.folder !== null && globals.folder !== '' && !UUID_RE.test(globals.folder)) {
@@ -418,7 +535,7 @@ function ctxFor(globals, { needKey }) {
418
535
 
419
536
  async function cmdAsk(tokens, globals) {
420
537
  const { q, k } = parseQueryArgs(tokens, { kMin: 1, kMax: 12 });
421
- const ctx = ctxFor(globals, { needKey: true });
538
+ const ctx = await ctxFor(globals, { needKey: true });
422
539
  // GET carries the contract; POST only when q outgrows a sane URL.
423
540
  const data =
424
541
  q.length > 1500
@@ -443,7 +560,7 @@ async function cmdAsk(tokens, globals) {
443
560
 
444
561
  async function cmdSearch(tokens, globals) {
445
562
  const { q, k } = parseQueryArgs(tokens, { kMin: 1, kMax: 20 });
446
- const ctx = ctxFor(globals, { needKey: true });
563
+ const ctx = await ctxFor(globals, { needKey: true });
447
564
  const data = await api('GET', '/v1/search', {
448
565
  ...ctx,
449
566
  query: qparams([
@@ -479,7 +596,7 @@ async function cmdTasks(tokens, globals) {
479
596
  if (t === '--all') includeArchived = true;
480
597
  else throw usageError(`Unexpected ${JSON.stringify(t)}. Usage: takibi tasks list [--all]`);
481
598
  }
482
- const ctx = ctxFor(globals, { needKey: true });
599
+ const ctx = await ctxFor(globals, { needKey: true });
483
600
  const tasks = await listTasks(ctx, includeArchived);
484
601
  if (ctx.json) {
485
602
  out(JSON.stringify({ tasks }, null, 2));
@@ -492,7 +609,7 @@ async function cmdTasks(tokens, globals) {
492
609
  if (sub === 'get') {
493
610
  const [id, extra] = rest;
494
611
  if (!id || extra) throw usageError('Usage: takibi tasks get <id>');
495
- const ctx = ctxFor(globals, { needKey: true });
612
+ const ctx = await ctxFor(globals, { needKey: true });
496
613
  let tasks = await listTasks(ctx, false);
497
614
  let found = tasks.find((t) => t.id === id);
498
615
  if (!found) {
@@ -507,7 +624,7 @@ async function cmdTasks(tokens, globals) {
507
624
  if (sub === 'claim') {
508
625
  const [id, extra] = rest;
509
626
  if (!id || extra) throw usageError('Usage: takibi tasks claim <id>');
510
- const ctx = ctxFor(globals, { needKey: true });
627
+ const ctx = await ctxFor(globals, { needKey: true });
511
628
  const data = await api('POST', `/v1/tasks/${id}/claim`, { ...ctx, query: qparams([['projectId', ctx.projectId]]), label: 'tasks claim' });
512
629
  if (ctx.json) out(JSON.stringify(data, null, 2));
513
630
  else renderTask(data.task);
@@ -517,7 +634,7 @@ async function cmdTasks(tokens, globals) {
517
634
  const [id, to, extra] = rest;
518
635
  if (!id || !to || extra) throw usageError('Usage: takibi tasks status <id> <todo|in_progress|review|done>');
519
636
  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 });
637
+ const ctx = await ctxFor(globals, { needKey: true });
521
638
  const data = await api('PATCH', `/v1/tasks/${id}`, {
522
639
  ...ctx,
523
640
  query: qparams([['projectId', ctx.projectId]]),
@@ -543,7 +660,7 @@ async function cmdTasks(tokens, globals) {
543
660
  if (!Number.isInteger(n) || n < 0) throw usageError('--size takes an integer >= 0 (asserted bytes).');
544
661
  flags.size = n;
545
662
  }
546
- const ctx = ctxFor(globals, { needKey: true });
663
+ const ctx = await ctxFor(globals, { needKey: true });
547
664
  const data = await api('POST', `/v1/tasks/${taskId}/artifacts`, {
548
665
  ...ctx,
549
666
  query: qparams([['projectId', ctx.projectId]]),
@@ -568,7 +685,7 @@ async function cmdDoc(tokens, globals) {
568
685
  if (rest[i] === '--limit') limit = rest[++i];
569
686
  else throw usageError(`Unexpected ${JSON.stringify(rest[i])}. Usage: takibi doc list [--limit <n>]`);
570
687
  }
571
- const ctx = ctxFor(globals, { needKey: true });
688
+ const ctx = await ctxFor(globals, { needKey: true });
572
689
  const data = await api('GET', '/v1/documents', {
573
690
  ...ctx,
574
691
  query: qparams([
@@ -595,7 +712,7 @@ async function cmdDoc(tokens, globals) {
595
712
  if (sub === 'text' && flagTokens[i] === '--max-chars') maxChars = flagTokens[++i];
596
713
  else throw usageError(`Unexpected ${JSON.stringify(flagTokens[i])}. Usage: takibi doc ${sub} <id>${sub === 'text' ? ' [--max-chars <n>]' : ''}`);
597
714
  }
598
- const ctx = ctxFor(globals, { needKey: true });
715
+ const ctx = await ctxFor(globals, { needKey: true });
599
716
  if (sub === 'get') {
600
717
  const data = await api('GET', `/v1/documents/${id}`, { ...ctx, query: qparams([['projectId', ctx.projectId]]), label: 'doc get' });
601
718
  if (ctx.json) out(JSON.stringify(data, null, 2));
@@ -659,14 +776,42 @@ async function cmdProjects(tokens, globals) {
659
776
  return;
660
777
  }
661
778
  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 }));
779
+ let entries = null;
780
+ try {
781
+ const key = resolveKey();
782
+ const data = await api('GET', '/v1/projects', { baseUrl: resolveBaseUrl(), key, verbose: globals.verbose, label: 'projects' });
783
+ const seen = new Set();
784
+ entries = [];
785
+ for (const p of data?.projects ?? []) {
786
+ if (p?.id && !seen.has(p.id)) {
787
+ seen.add(p.id);
788
+ entries.push({ name: p.name, id: p.id });
789
+ }
790
+ }
791
+ // Local aliases stay visible alongside the server list.
792
+ for (const [name, id] of map.entries()) {
793
+ if (!seen.has(id)) {
794
+ seen.add(id);
795
+ entries.push({ name, id });
796
+ }
797
+ }
798
+ } catch (e) {
799
+ if (e instanceof CliError && /No API key file/.test(e.message)) {
800
+ entries = null; // Keyless: answer from the map below, like before.
801
+ } else if (isListFallbackable(e)) {
802
+ entries = null;
803
+ } else {
804
+ throw e;
805
+ }
806
+ }
807
+ if (entries === null) entries = [...map.entries()].map(([name, id]) => ({ name, id }));
663
808
  if (globals.json) {
664
809
  out(JSON.stringify({ projects: entries }, null, 2));
665
810
  return;
666
811
  }
667
812
  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.)`);
813
+ out('No projects found.');
814
+ 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
815
  return;
671
816
  }
672
817
  for (const { name, id } of entries) out(`${name} → ${id}`);
@@ -682,9 +827,11 @@ async function cmdVersion(globals) {
682
827
  async function main(argv) {
683
828
  const { globals, rest } = parseArgv(argv);
684
829
  if (globals.version) {
685
- out(`takibibase ${CLI_VERSION}`);
830
+ out(`takibi ${CLI_VERSION}`);
831
+ await maybeNotifyUpdate({ json: globals.json });
686
832
  return;
687
833
  }
834
+ await maybeNotifyUpdate({ json: globals.json });
688
835
  const [cmd, ...tokens] = rest;
689
836
  if (globals.help || !cmd) {
690
837
  out(HELP);