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 +1 -1
- package/README.md +9 -7
- package/SKILL.md +3 -3
- package/package.json +1 -1
- package/takibi.mjs +100 -30
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,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
|
|
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
26
|
|
|
27
27
|
## Commands
|
package/package.json
CHANGED
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.
|
|
@@ -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.
|
|
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
|
-
/**
|
|
130
|
-
|
|
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(', ')}.
|
|
142
|
-
: '
|
|
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
|
|
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
|
|
239
|
-
single-grant keys
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
669
|
-
err(`(
|
|
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(`
|
|
755
|
+
out(`takibi ${CLI_VERSION}`);
|
|
686
756
|
return;
|
|
687
757
|
}
|
|
688
758
|
const [cmd, ...tokens] = rest;
|