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 +1 -1
- package/README.md +9 -7
- package/SKILL.md +7 -3
- package/package.json +2 -2
- package/takibi.mjs +180 -33
package/LICENSE
CHANGED
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.
|
|
13
|
-
`
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
23
|
-
or a UUID; single-grant keys may omit it.
|
|
24
|
-
|
|
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.
|
|
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": "
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
30
|
-
const
|
|
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)
|
|
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
|
-
/**
|
|
130
|
-
|
|
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(', ')}.
|
|
142
|
-
: '
|
|
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
|
|
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
|
|
239
|
-
single-grant keys
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
669
|
-
err(`(
|
|
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(`
|
|
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);
|