artyfax 0.2.2 → 0.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.
- package/README.md +69 -36
- package/dist/cli.js +1510 -206
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { resolve as
|
|
4
|
+
import { readFileSync as readFileSync5, realpathSync } from "fs";
|
|
5
|
+
import { resolve as resolve5 } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
6
7
|
|
|
7
8
|
// src/config.ts
|
|
8
9
|
function getConfig(flags) {
|
|
@@ -11,24 +12,58 @@ function getConfig(flags) {
|
|
|
11
12
|
console.error("No API key. Set ARTYFAX_API_KEY or pass --api-key.");
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
15
|
+
const endpoint = flags.endpoint || process.env.ARTYFAX_ENDPOINT || "https://artyfax.io";
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = new URL(endpoint);
|
|
19
|
+
} catch {
|
|
20
|
+
console.error(`Invalid endpoint: "${endpoint}". Must be an absolute http(s) URL.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
24
|
+
console.error(`Invalid endpoint scheme: "${parsed.protocol}". Use http:// or https://.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "0.0.0.0"]);
|
|
28
|
+
const isLocal = LOCAL_HOSTS.has(parsed.hostname);
|
|
29
|
+
if (parsed.protocol === "http:" && !isLocal) {
|
|
30
|
+
console.error(`Warning: sending your API key over plain HTTP to ${parsed.host}. Use https:// unless this is a trusted local endpoint.`);
|
|
31
|
+
}
|
|
14
32
|
return {
|
|
15
33
|
apiKey,
|
|
16
|
-
endpoint
|
|
34
|
+
endpoint,
|
|
17
35
|
passphrase: process.env.ARTYFAX_SECURE_PASSPHRASE || null
|
|
18
36
|
};
|
|
19
37
|
}
|
|
20
|
-
async function apiFetch(config, path,
|
|
38
|
+
async function apiFetch(config, path, init2) {
|
|
39
|
+
const isFormData = typeof FormData !== "undefined" && init2?.body instanceof FormData;
|
|
21
40
|
const res = await fetch(`${config.endpoint}/api${path}`, {
|
|
22
|
-
...
|
|
41
|
+
...init2,
|
|
23
42
|
headers: {
|
|
24
|
-
"Content-Type": "application/json",
|
|
43
|
+
...isFormData ? {} : { "Content-Type": "application/json" },
|
|
25
44
|
"X-API-Key": config.apiKey,
|
|
26
|
-
|
|
45
|
+
// design-34: identify the CLI on the server side so saved_via can
|
|
46
|
+
// bucket CLI saves correctly. Format mirrors common CLI tools so a
|
|
47
|
+
// server-side regex can stay simple.
|
|
48
|
+
"User-Agent": `artyfax-cli/${VERSION}`,
|
|
49
|
+
...init2?.headers
|
|
27
50
|
}
|
|
28
51
|
});
|
|
29
52
|
if (!res.ok) {
|
|
30
|
-
const
|
|
31
|
-
|
|
53
|
+
const raw = await res.text().catch(() => "");
|
|
54
|
+
let message = `HTTP ${res.status}`;
|
|
55
|
+
if (raw) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
if (parsed.error) message = parsed.error;
|
|
59
|
+
} catch {
|
|
60
|
+
const snippet = raw.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
61
|
+
if (snippet) message = `HTTP ${res.status}: ${snippet}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const err = new Error(message);
|
|
65
|
+
err.status = res.status;
|
|
66
|
+
throw err;
|
|
32
67
|
}
|
|
33
68
|
return res.json();
|
|
34
69
|
}
|
|
@@ -98,7 +133,7 @@ function bufferToBase64(buffer) {
|
|
|
98
133
|
|
|
99
134
|
// src/passphrase.ts
|
|
100
135
|
async function promptPassphrase(prompt = "Passphrase: ") {
|
|
101
|
-
return new Promise((
|
|
136
|
+
return new Promise((resolve6, reject) => {
|
|
102
137
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
103
138
|
process.stderr.write(prompt);
|
|
104
139
|
const cleanup = () => {
|
|
@@ -116,7 +151,7 @@ async function promptPassphrase(prompt = "Passphrase: ") {
|
|
|
116
151
|
if (c2 === "\n" || c2 === "\r") {
|
|
117
152
|
process.stderr.write("\n");
|
|
118
153
|
cleanup();
|
|
119
|
-
|
|
154
|
+
resolve6(input);
|
|
120
155
|
} else if (c2 === "" || c2.length === 0) {
|
|
121
156
|
process.stderr.write("\n");
|
|
122
157
|
cleanup();
|
|
@@ -200,11 +235,11 @@ function stripAnsi(s) {
|
|
|
200
235
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
201
236
|
}
|
|
202
237
|
function confirm(message) {
|
|
203
|
-
return new Promise((
|
|
238
|
+
return new Promise((resolve6) => {
|
|
204
239
|
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
205
240
|
rl.question(`${message} ${c.muted("[y/N]")} `, (answer) => {
|
|
206
241
|
rl.close();
|
|
207
|
-
|
|
242
|
+
resolve6(answer.trim().toLowerCase() === "y");
|
|
208
243
|
});
|
|
209
244
|
});
|
|
210
245
|
}
|
|
@@ -229,6 +264,10 @@ async function save(config, opts) {
|
|
|
229
264
|
content = await encryptSecure(content, sdk);
|
|
230
265
|
}
|
|
231
266
|
try {
|
|
267
|
+
let isDuplicate2 = function(r) {
|
|
268
|
+
return "status" in r && r.status === "duplicate";
|
|
269
|
+
};
|
|
270
|
+
var isDuplicate = isDuplicate2;
|
|
232
271
|
const body = {
|
|
233
272
|
title: opts.title || void 0,
|
|
234
273
|
content: content || void 0,
|
|
@@ -241,12 +280,25 @@ async function save(config, opts) {
|
|
|
241
280
|
if (opts.tags) body.tags = opts.tags.split(",").map((t) => t.trim());
|
|
242
281
|
if (opts.theme) body.theme = opts.theme;
|
|
243
282
|
if (opts.parentId) body.parent_id = opts.parentId;
|
|
283
|
+
if (opts.force) body.force_new = true;
|
|
244
284
|
const result = await apiFetch(
|
|
245
285
|
config,
|
|
246
286
|
"/ingest",
|
|
247
287
|
{ method: "POST", body: JSON.stringify(body) }
|
|
248
288
|
);
|
|
249
289
|
spin.stop();
|
|
290
|
+
if (isDuplicate2(result)) {
|
|
291
|
+
if (opts.json) {
|
|
292
|
+
console.log(JSON.stringify(result));
|
|
293
|
+
} else {
|
|
294
|
+
const date = new Date(result.existing_saved_at).toLocaleDateString();
|
|
295
|
+
console.log(
|
|
296
|
+
`${c.muted("Already saved:")} ${result.existing_slug} ${c.muted(`(${date})`)}
|
|
297
|
+
${c.muted("Use --force to save a new copy.")}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
250
302
|
if (opts.json) {
|
|
251
303
|
console.log(JSON.stringify(result.document));
|
|
252
304
|
} else {
|
|
@@ -261,8 +313,9 @@ async function save(config, opts) {
|
|
|
261
313
|
// src/resolve.ts
|
|
262
314
|
async function resolveSlug(config, slugOrId) {
|
|
263
315
|
if (slugOrId.includes("/")) {
|
|
316
|
+
const encodedSlug = slugOrId.split("/").map(encodeURIComponent).join("/");
|
|
264
317
|
try {
|
|
265
|
-
const doc = await apiFetch(config, `/documents/by-slug/${
|
|
318
|
+
const doc = await apiFetch(config, `/documents/by-slug/${encodedSlug}`);
|
|
266
319
|
return doc;
|
|
267
320
|
} catch {
|
|
268
321
|
}
|
|
@@ -274,6 +327,14 @@ async function resolveSlug(config, slugOrId) {
|
|
|
274
327
|
} catch {
|
|
275
328
|
}
|
|
276
329
|
}
|
|
330
|
+
if (/^[A-Z]+-\d+$/.test(slugOrId)) {
|
|
331
|
+
try {
|
|
332
|
+
const { id } = await apiFetch(config, `/resolve/${encodeURIComponent(slugOrId)}`);
|
|
333
|
+
const doc = await apiFetch(config, `/documents/${id}`);
|
|
334
|
+
return doc;
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
}
|
|
277
338
|
const docs = await apiFetch(
|
|
278
339
|
config,
|
|
279
340
|
`/documents?limit=500`
|
|
@@ -292,7 +353,7 @@ async function resolveSlug(config, slugOrId) {
|
|
|
292
353
|
` + partial.slice(0, 5).map((d) => ` ${d.slug}`).join("\n")
|
|
293
354
|
);
|
|
294
355
|
}
|
|
295
|
-
throw new Error(`Document not found: ${slugOrId}. Try \`
|
|
356
|
+
throw new Error(`Document not found: ${slugOrId}. Try \`artyfax search ${slugOrId}\` or \`artyfax list\``);
|
|
296
357
|
}
|
|
297
358
|
|
|
298
359
|
// src/commands/read.ts
|
|
@@ -375,6 +436,47 @@ async function unsecure(config, slugOrId) {
|
|
|
375
436
|
spin.succeed(`${c.teal("Decrypted:")} ${doc.slug}`);
|
|
376
437
|
}
|
|
377
438
|
|
|
439
|
+
// src/commands/workspace.ts
|
|
440
|
+
async function resolveWorkspaceId(config, raw) {
|
|
441
|
+
const [id] = await resolveWorkspaceIds(config, [raw]);
|
|
442
|
+
return id;
|
|
443
|
+
}
|
|
444
|
+
async function resolveWorkspaceIds(config, inputs) {
|
|
445
|
+
if (inputs.length === 0) return [];
|
|
446
|
+
const data = await apiFetch(config, "/workspaces");
|
|
447
|
+
return inputs.map((raw) => {
|
|
448
|
+
const q = raw.trim().toLowerCase();
|
|
449
|
+
const match = data.workspaces.find(
|
|
450
|
+
(w) => w.id === raw || w.slug.toLowerCase() === q || w.name.toLowerCase() === q
|
|
451
|
+
);
|
|
452
|
+
if (!match) {
|
|
453
|
+
const names = data.workspaces.map((w) => w.name).join(", ") || "none";
|
|
454
|
+
throw new Error(`Workspace not found: ${raw}. Available: ${names}`);
|
|
455
|
+
}
|
|
456
|
+
return match.id;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
async function workspaceList(config, json) {
|
|
460
|
+
const spin = spinner("Loading workspaces\u2026");
|
|
461
|
+
spin.start();
|
|
462
|
+
const data = await apiFetch(config, "/workspaces");
|
|
463
|
+
spin.stop();
|
|
464
|
+
if (json) {
|
|
465
|
+
console.log(JSON.stringify(data.workspaces, null, 2));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (data.workspaces.length === 0) {
|
|
469
|
+
console.log(c.muted("No workspaces."));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const rows = data.workspaces.map((w) => [
|
|
473
|
+
w.name,
|
|
474
|
+
c.muted(w.slug),
|
|
475
|
+
c.muted(w.id)
|
|
476
|
+
]);
|
|
477
|
+
console.log(table(rows, { header: ["Name", c.muted("Slug"), c.muted("ID")] }));
|
|
478
|
+
}
|
|
479
|
+
|
|
378
480
|
// src/commands/list.ts
|
|
379
481
|
async function list(config, opts) {
|
|
380
482
|
const spin = spinner("Loading documents\u2026");
|
|
@@ -384,6 +486,8 @@ async function list(config, opts) {
|
|
|
384
486
|
if (opts.offset) path += `&offset=${opts.offset}`;
|
|
385
487
|
if (opts.archived) path += `&archived=1`;
|
|
386
488
|
if (opts.parentId) path += `&parent_id=${encodeURIComponent(opts.parentId)}`;
|
|
489
|
+
if (opts.tag) path += `&tag=${encodeURIComponent(opts.tag)}`;
|
|
490
|
+
if (opts.workspace) path += `&workspace=${encodeURIComponent(await resolveWorkspaceId(config, opts.workspace))}`;
|
|
387
491
|
const data = await apiFetch(config, path);
|
|
388
492
|
spin.stop();
|
|
389
493
|
if (opts.json) {
|
|
@@ -410,13 +514,12 @@ async function list(config, opts) {
|
|
|
410
514
|
}
|
|
411
515
|
|
|
412
516
|
// src/commands/search.ts
|
|
413
|
-
async function search(config, query, json) {
|
|
517
|
+
async function search(config, query, json, workspace) {
|
|
414
518
|
const spin = spinner(`Searching for "${query}"\u2026`);
|
|
415
519
|
spin.start();
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
);
|
|
520
|
+
let path = `/search?q=${encodeURIComponent(query)}`;
|
|
521
|
+
if (workspace) path += `&workspace=${encodeURIComponent(await resolveWorkspaceId(config, workspace))}`;
|
|
522
|
+
const data = await apiFetch(config, path);
|
|
420
523
|
spin.stop();
|
|
421
524
|
if (json) {
|
|
422
525
|
console.log(JSON.stringify(data.results));
|
|
@@ -427,7 +530,7 @@ async function search(config, query, json) {
|
|
|
427
530
|
return;
|
|
428
531
|
}
|
|
429
532
|
const rows = data.results.map((r) => [
|
|
430
|
-
r.title,
|
|
533
|
+
r.title + (r.visible_via === "project" ? c.muted(" (via project)") : ""),
|
|
431
534
|
c.muted(r.slug),
|
|
432
535
|
c.muted(r.updated_at.slice(0, 10))
|
|
433
536
|
]);
|
|
@@ -524,7 +627,7 @@ async function update(config, slugOrId, file, json) {
|
|
|
524
627
|
spin.text = "Uploading\u2026";
|
|
525
628
|
await apiFetch(config, `/documents/${doc.id}/content`, {
|
|
526
629
|
method: "PATCH",
|
|
527
|
-
body: JSON.stringify({ content })
|
|
630
|
+
body: JSON.stringify({ content, source: "cli" })
|
|
528
631
|
});
|
|
529
632
|
spin.stop();
|
|
530
633
|
if (json) {
|
|
@@ -542,6 +645,7 @@ async function metadata(config, slugOrId, opts) {
|
|
|
542
645
|
const doc = await resolveSlug(config, slugOrId);
|
|
543
646
|
const patch = {};
|
|
544
647
|
if (opts.category !== void 0) patch.category = opts.category;
|
|
648
|
+
if (opts.slug !== void 0) patch.slug = opts.slug;
|
|
545
649
|
if (opts.title !== void 0) patch.title = opts.title;
|
|
546
650
|
if (opts.theme !== void 0) patch.theme = opts.theme;
|
|
547
651
|
if (opts.visibility !== void 0) patch.visibility = opts.visibility;
|
|
@@ -549,7 +653,7 @@ async function metadata(config, slugOrId, opts) {
|
|
|
549
653
|
if (opts.tags !== void 0) patch.tags = opts.tags.split(",").map((t) => t.trim());
|
|
550
654
|
if (Object.keys(patch).length === 0) {
|
|
551
655
|
spin.stop();
|
|
552
|
-
throw new Error("No metadata flags provided. Use --category, --tags, --title, --theme, --visibility, or --archived");
|
|
656
|
+
throw new Error("No metadata flags provided. Use --category, --slug, --tags, --title, --theme, --visibility, or --archived");
|
|
553
657
|
}
|
|
554
658
|
spin.text = "Updating metadata\u2026";
|
|
555
659
|
await apiFetch(config, `/documents/${doc.id}/metadata`, {
|
|
@@ -565,6 +669,104 @@ async function metadata(config, slugOrId, opts) {
|
|
|
565
669
|
}
|
|
566
670
|
}
|
|
567
671
|
|
|
672
|
+
// src/commands/image.ts
|
|
673
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
674
|
+
import { extname, basename, resolve as resolve2 } from "path";
|
|
675
|
+
var MIME_BY_EXT = {
|
|
676
|
+
".png": "image/png",
|
|
677
|
+
".jpg": "image/jpeg",
|
|
678
|
+
".jpeg": "image/jpeg",
|
|
679
|
+
".webp": "image/webp",
|
|
680
|
+
".gif": "image/gif",
|
|
681
|
+
".avif": "image/avif",
|
|
682
|
+
".svg": "image/svg+xml"
|
|
683
|
+
};
|
|
684
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
685
|
+
function formatBytes(bytes) {
|
|
686
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
687
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
688
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
689
|
+
}
|
|
690
|
+
async function resolveDocArg(config, arg) {
|
|
691
|
+
const looksLikeBareId = !arg.includes("/") && !/^[A-Z]+-\d+$/.test(arg);
|
|
692
|
+
if (looksLikeBareId) {
|
|
693
|
+
try {
|
|
694
|
+
return await apiFetch(config, `/documents/${encodeURIComponent(arg)}`);
|
|
695
|
+
} catch (e) {
|
|
696
|
+
if (e.status !== 404) throw e;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return resolveSlug(config, arg);
|
|
700
|
+
}
|
|
701
|
+
function readImageFile(file) {
|
|
702
|
+
const path = resolve2(file);
|
|
703
|
+
let stat;
|
|
704
|
+
try {
|
|
705
|
+
stat = statSync(path);
|
|
706
|
+
} catch {
|
|
707
|
+
throw new Error(`Image not found: ${path}`);
|
|
708
|
+
}
|
|
709
|
+
if (!stat.isFile()) throw new Error(`Not a file: ${path}`);
|
|
710
|
+
const ext = extname(path).toLowerCase();
|
|
711
|
+
const mime = MIME_BY_EXT[ext];
|
|
712
|
+
if (!mime) {
|
|
713
|
+
throw new Error(`Unsupported image extension: ${ext}. Allowed: ${Object.keys(MIME_BY_EXT).join(", ")}`);
|
|
714
|
+
}
|
|
715
|
+
if (stat.size > MAX_BYTES) {
|
|
716
|
+
throw new Error(`Image exceeds 5 MB limit (${formatBytes(stat.size)})`);
|
|
717
|
+
}
|
|
718
|
+
return { bytes: readFileSync2(path), name: basename(path), mime };
|
|
719
|
+
}
|
|
720
|
+
async function uploadImage(config, docId, file, kind) {
|
|
721
|
+
const form = new FormData();
|
|
722
|
+
form.set("file", new File([file.bytes], file.name, { type: file.mime }));
|
|
723
|
+
const query = kind === "cover" ? "?kind=cover" : "";
|
|
724
|
+
return apiFetch(config, `/documents/${docId}/images${query}`, {
|
|
725
|
+
method: "POST",
|
|
726
|
+
body: form
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
async function setCover(config, slugOrId, file, opts) {
|
|
730
|
+
const image = readImageFile(file);
|
|
731
|
+
const spin = spinner("Resolving document\u2026");
|
|
732
|
+
spin.start();
|
|
733
|
+
const doc = await resolveDocArg(config, slugOrId);
|
|
734
|
+
spin.text = `Uploading ${image.name}\u2026`;
|
|
735
|
+
const uploaded = await uploadImage(config, doc.id, image, "cover");
|
|
736
|
+
spin.text = "Setting cover\u2026";
|
|
737
|
+
const patch = { cover_image: uploaded.path };
|
|
738
|
+
if (typeof opts.alt === "string") patch.cover_alt = opts.alt;
|
|
739
|
+
await apiFetch(config, `/documents/${doc.id}/metadata`, {
|
|
740
|
+
method: "PATCH",
|
|
741
|
+
body: JSON.stringify(patch)
|
|
742
|
+
});
|
|
743
|
+
spin.stop();
|
|
744
|
+
if (opts.json) {
|
|
745
|
+
console.log(JSON.stringify({ ok: true, id: doc.id, slug: doc.slug, cover_image: uploaded.path }));
|
|
746
|
+
} else {
|
|
747
|
+
console.log(`${c.teal("Cover set")} on ${c.bright(doc.slug)} ${c.muted(`(${formatBytes(image.bytes.length)})`)}`);
|
|
748
|
+
console.log(c.muted(" Tip: covers render best at 1200\xD7630 (hero + social card shape)."));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function addImage(config, slugOrId, file, opts) {
|
|
752
|
+
const image = readImageFile(file);
|
|
753
|
+
const spin = spinner("Resolving document\u2026");
|
|
754
|
+
spin.start();
|
|
755
|
+
const doc = await resolveDocArg(config, slugOrId);
|
|
756
|
+
spin.text = `Uploading ${image.name}\u2026`;
|
|
757
|
+
const uploaded = await uploadImage(config, doc.id, image, "content");
|
|
758
|
+
spin.stop();
|
|
759
|
+
const alt = typeof opts.alt === "string" ? opts.alt : "";
|
|
760
|
+
const snippet = ``;
|
|
761
|
+
if (opts.json) {
|
|
762
|
+
console.log(JSON.stringify({ ok: true, id: doc.id, slug: doc.slug, path: uploaded.path, markdown: snippet }));
|
|
763
|
+
} else {
|
|
764
|
+
console.log(`${c.teal("Uploaded")} to ${c.bright(doc.slug)} ${c.muted(`(${formatBytes(image.bytes.length)})`)}`);
|
|
765
|
+
console.log(c.muted(" Paste this into the document body:"));
|
|
766
|
+
console.log(` ${snippet}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
568
770
|
// src/commands/share.ts
|
|
569
771
|
async function shareCreate(config, slugOrId, json) {
|
|
570
772
|
const spin = spinner("Resolving document\u2026");
|
|
@@ -669,8 +871,42 @@ async function tagList(config, json) {
|
|
|
669
871
|
console.log(c.muted("No tags found."));
|
|
670
872
|
return;
|
|
671
873
|
}
|
|
672
|
-
const
|
|
673
|
-
|
|
874
|
+
const namespaces = data.namespaces ?? [];
|
|
875
|
+
const simple = data.simple ?? [];
|
|
876
|
+
for (const ns of namespaces) {
|
|
877
|
+
const heading = ns.intent ? `${c.bright(ns.prefix + ":")} ${c.muted(ns.intent)}` : c.bright(ns.prefix + ":");
|
|
878
|
+
console.log("\n" + heading);
|
|
879
|
+
const rows = ns.tags.map((t) => [t.tag, String(t.count)]);
|
|
880
|
+
console.log(table(rows, { header: ["Tag", "Count"] }));
|
|
881
|
+
}
|
|
882
|
+
if (simple.length > 0) {
|
|
883
|
+
console.log("\n" + c.bright("(no namespace)"));
|
|
884
|
+
const rows = simple.map((t) => [t.tag, String(t.count)]);
|
|
885
|
+
console.log(table(rows, { header: ["Tag", "Count"] }));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async function tagDelete(config, tag, json) {
|
|
889
|
+
const spin = spinner(`Deleting tag \u201C${tag}\u201D\u2026`);
|
|
890
|
+
spin.start();
|
|
891
|
+
const data = await apiFetch(
|
|
892
|
+
config,
|
|
893
|
+
"/tags/delete",
|
|
894
|
+
{ method: "POST", body: JSON.stringify({ tag }) }
|
|
895
|
+
);
|
|
896
|
+
spin.stop();
|
|
897
|
+
if (json) {
|
|
898
|
+
console.log(JSON.stringify(data));
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const { documents, annotations, snippets } = data.updated;
|
|
902
|
+
const total = documents + annotations + snippets;
|
|
903
|
+
if (total === 0) {
|
|
904
|
+
console.log(c.muted(`No items carried \u201C${tag}\u201D.`));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
console.log(
|
|
908
|
+
c.teal(`Removed \u201C${tag}\u201D from ${total} item(s)`) + c.muted(` (${documents} docs, ${annotations} annotations, ${snippets} snippets)`)
|
|
909
|
+
);
|
|
674
910
|
}
|
|
675
911
|
|
|
676
912
|
// src/commands/version.ts
|
|
@@ -743,100 +979,337 @@ async function versionRestore(config, slugOrId, yes, json) {
|
|
|
743
979
|
}
|
|
744
980
|
}
|
|
745
981
|
|
|
746
|
-
// src/commands/
|
|
747
|
-
async function
|
|
982
|
+
// src/commands/annotation.ts
|
|
983
|
+
async function annotationList(config, slugOrId, json, opts = {}) {
|
|
748
984
|
const spin = spinner("Resolving document\u2026");
|
|
749
985
|
spin.start();
|
|
750
986
|
const doc = await resolveSlug(config, slugOrId);
|
|
751
987
|
spin.text = "Loading annotations\u2026";
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
)
|
|
988
|
+
const qs = new URLSearchParams();
|
|
989
|
+
qs.set("include_recipients", opts.includeRecipients ? "true" : "false");
|
|
990
|
+
if (opts.since) qs.set("since", opts.since);
|
|
991
|
+
const path = `/annotations/doc/${doc.id}${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
992
|
+
const data = await apiFetch(config, path);
|
|
756
993
|
spin.stop();
|
|
994
|
+
const annotations = Array.isArray(data) ? data : data.annotations ?? [];
|
|
757
995
|
if (json) {
|
|
758
|
-
console.log(JSON.stringify(
|
|
996
|
+
console.log(JSON.stringify(annotations, null, 2));
|
|
759
997
|
return;
|
|
760
998
|
}
|
|
761
|
-
if (
|
|
999
|
+
if (annotations.length === 0) {
|
|
762
1000
|
console.log(c.muted("No annotations found."));
|
|
763
1001
|
return;
|
|
764
1002
|
}
|
|
765
|
-
console.log(
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1003
|
+
console.log(
|
|
1004
|
+
`${c.bright(doc.slug)} \u2014 ${annotations.length} annotation${annotations.length === 1 ? "" : "s"}
|
|
1005
|
+
`
|
|
1006
|
+
);
|
|
1007
|
+
for (const a of annotations) {
|
|
1008
|
+
const typeLabel = a.type === "highlight" ? c.amber("highlight") : a.type === "underline" ? c.amber("underline") : a.type === "strikethrough" ? c.muted("strikethrough") : a.type === "redact" ? c.muted("redact") : a.type === "note" ? c.blue("note") : c.muted(a.type);
|
|
769
1009
|
const date = c.muted(a.created_at.slice(0, 10));
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (a.
|
|
1010
|
+
const authorTag = a.author === "recipient" ? c.muted(` \xB7 ${a.share_link_label ?? "recipient"}`) : "";
|
|
1011
|
+
console.log(` ${typeLabel} ${date}${authorTag}`);
|
|
1012
|
+
if (a.quoted_text) {
|
|
1013
|
+
console.log(` ${c.muted(">")} ${a.quoted_text.slice(0, 80)}${a.quoted_text.length > 80 ? "\u2026" : ""}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (a.note) {
|
|
1016
|
+
console.log(` ${a.note.slice(0, 120)}${a.note.length > 120 ? "\u2026" : ""}`);
|
|
1017
|
+
}
|
|
773
1018
|
console.log();
|
|
774
1019
|
}
|
|
775
1020
|
}
|
|
776
|
-
async function
|
|
777
|
-
const spin = spinner("
|
|
1021
|
+
async function annotationSearch(config, query, json) {
|
|
1022
|
+
const spin = spinner(`Searching annotations for "${query}"\u2026`);
|
|
778
1023
|
spin.start();
|
|
779
|
-
const
|
|
780
|
-
let body;
|
|
781
|
-
if (text) {
|
|
782
|
-
body = text;
|
|
783
|
-
} else {
|
|
784
|
-
if (process.stdin.isTTY) {
|
|
785
|
-
spin.stop();
|
|
786
|
-
throw new Error("No text provided. Use --text or pipe text via stdin.");
|
|
787
|
-
}
|
|
788
|
-
spin.stop();
|
|
789
|
-
const chunks = [];
|
|
790
|
-
for await (const chunk of process.stdin) {
|
|
791
|
-
chunks.push(chunk);
|
|
792
|
-
}
|
|
793
|
-
body = Buffer.concat(chunks).toString("utf-8").trim();
|
|
794
|
-
spin.start();
|
|
795
|
-
}
|
|
796
|
-
if (!body) {
|
|
797
|
-
spin.stop();
|
|
798
|
-
throw new Error("No text provided. Use --text or pipe to stdin");
|
|
799
|
-
}
|
|
800
|
-
spin.text = "Adding annotation\u2026";
|
|
801
|
-
const result = await apiFetch(
|
|
1024
|
+
const data = await apiFetch(
|
|
802
1025
|
config,
|
|
803
|
-
`/annotations/
|
|
804
|
-
{
|
|
805
|
-
method: "POST",
|
|
806
|
-
body: JSON.stringify({ type: "comment", body })
|
|
807
|
-
}
|
|
1026
|
+
`/annotations/search?q=${encodeURIComponent(query)}`
|
|
808
1027
|
);
|
|
809
1028
|
spin.stop();
|
|
1029
|
+
const annotations = Array.isArray(data) ? data : data.annotations ?? [];
|
|
810
1030
|
if (json) {
|
|
811
|
-
console.log(JSON.stringify(
|
|
812
|
-
|
|
813
|
-
console.log(`${c.teal("Added note to")} ${doc.slug}`);
|
|
1031
|
+
console.log(JSON.stringify(annotations, null, 2));
|
|
1032
|
+
return;
|
|
814
1033
|
}
|
|
1034
|
+
if (annotations.length === 0) {
|
|
1035
|
+
console.log(c.muted(`No annotation results for "${query}".`));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const rows = annotations.map((a) => {
|
|
1039
|
+
const text = a.note ?? a.quoted_text ?? "";
|
|
1040
|
+
return [
|
|
1041
|
+
a.type === "highlight" ? c.amber("HL") : a.type === "note" ? c.blue("NT") : c.muted(a.type.slice(0, 2).toUpperCase()),
|
|
1042
|
+
text.slice(0, 60) + (text.length > 60 ? "\u2026" : ""),
|
|
1043
|
+
c.muted(a.created_at.slice(0, 10))
|
|
1044
|
+
];
|
|
1045
|
+
});
|
|
1046
|
+
console.log(table(rows, { header: ["", "Text", c.muted("Date")] }));
|
|
1047
|
+
console.log(c.muted(`
|
|
1048
|
+
${annotations.length} result${annotations.length === 1 ? "" : "s"}`));
|
|
815
1049
|
}
|
|
816
|
-
|
|
817
|
-
|
|
1050
|
+
|
|
1051
|
+
// src/commands/snippet.ts
|
|
1052
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
1053
|
+
import { extname as extname2, basename as basename2, resolve as resolve3 } from "path";
|
|
1054
|
+
var MIME_BY_EXT2 = {
|
|
1055
|
+
".png": "image/png",
|
|
1056
|
+
".jpg": "image/jpeg",
|
|
1057
|
+
".jpeg": "image/jpeg",
|
|
1058
|
+
".webp": "image/webp",
|
|
1059
|
+
".gif": "image/gif",
|
|
1060
|
+
".svg": "image/svg+xml"
|
|
1061
|
+
};
|
|
1062
|
+
function parseTags(raw) {
|
|
1063
|
+
if (!raw) return [];
|
|
1064
|
+
if (Array.isArray(raw)) return raw;
|
|
1065
|
+
try {
|
|
1066
|
+
const parsed = JSON.parse(raw);
|
|
1067
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1068
|
+
} catch {
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function typeBadge(type) {
|
|
1073
|
+
if (type === "quote") return c.amber("quote");
|
|
1074
|
+
if (type === "code") return c.blue("code ");
|
|
1075
|
+
if (type === "image") return c.teal("image");
|
|
1076
|
+
return c.muted(type);
|
|
1077
|
+
}
|
|
1078
|
+
function formatBytes2(bytes) {
|
|
1079
|
+
if (!bytes) return "";
|
|
1080
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1081
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1082
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
1083
|
+
}
|
|
1084
|
+
function safeHostname(url) {
|
|
1085
|
+
if (!url) return "";
|
|
1086
|
+
try {
|
|
1087
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
1088
|
+
} catch {
|
|
1089
|
+
return "";
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async function snippetList(config, options, json) {
|
|
1093
|
+
const spin = spinner("Loading snippets\u2026");
|
|
818
1094
|
spin.start();
|
|
1095
|
+
const qs = new URLSearchParams();
|
|
1096
|
+
if (options.type) qs.set("type", options.type);
|
|
1097
|
+
if (options.starred) qs.set("starred", "1");
|
|
1098
|
+
if (options.tag) qs.set("tag", options.tag);
|
|
1099
|
+
if (options.limit) qs.set("limit", String(options.limit));
|
|
819
1100
|
const data = await apiFetch(
|
|
820
1101
|
config,
|
|
821
|
-
`/
|
|
1102
|
+
`/snippets${qs.toString() ? `?${qs.toString()}` : ""}`
|
|
822
1103
|
);
|
|
823
1104
|
spin.stop();
|
|
824
1105
|
if (json) {
|
|
825
|
-
console.log(JSON.stringify(data
|
|
1106
|
+
console.log(JSON.stringify(data));
|
|
826
1107
|
return;
|
|
827
1108
|
}
|
|
828
|
-
|
|
829
|
-
|
|
1109
|
+
const items = Array.isArray(data) ? data : data.items ?? [];
|
|
1110
|
+
const total = Array.isArray(data) ? data.length : data.total ?? items.length;
|
|
1111
|
+
if (items.length === 0) {
|
|
1112
|
+
console.log(c.muted("No snippets found."));
|
|
830
1113
|
return;
|
|
831
1114
|
}
|
|
832
|
-
const rows =
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1115
|
+
const rows = items.map((s) => {
|
|
1116
|
+
const star = s.starred ? c.amber("\u2605") : " ";
|
|
1117
|
+
const preview = s.type === "image" ? s.alt || "(image)" : s.content.split("\n")[0];
|
|
1118
|
+
const trimmed = preview.length > 60 ? preview.slice(0, 60) + "\u2026" : preview;
|
|
1119
|
+
const source = s.source_site_name || safeHostname(s.source_url);
|
|
1120
|
+
return [
|
|
1121
|
+
star,
|
|
1122
|
+
typeBadge(s.type),
|
|
1123
|
+
c.bright(s.id),
|
|
1124
|
+
trimmed,
|
|
1125
|
+
c.muted(source || ""),
|
|
1126
|
+
c.muted(s.created_at.slice(0, 10))
|
|
1127
|
+
];
|
|
1128
|
+
});
|
|
1129
|
+
console.log(table(rows, { header: ["", "", "ID", "Preview", "Source", c.muted("Date")] }));
|
|
838
1130
|
console.log(c.muted(`
|
|
839
|
-
${
|
|
1131
|
+
${total} snippet${total === 1 ? "" : "s"} total`));
|
|
1132
|
+
}
|
|
1133
|
+
async function snippetSearch(config, query, options, json) {
|
|
1134
|
+
const spin = spinner(`Searching snippets for "${query}"\u2026`);
|
|
1135
|
+
spin.start();
|
|
1136
|
+
const qs = new URLSearchParams({ q: query });
|
|
1137
|
+
if (options.type) qs.set("type", options.type);
|
|
1138
|
+
if (options.limit) qs.set("limit", String(options.limit));
|
|
1139
|
+
const data = await apiFetch(config, `/snippets/search?${qs.toString()}`);
|
|
1140
|
+
spin.stop();
|
|
1141
|
+
const items = Array.isArray(data) ? data : data.items ?? [];
|
|
1142
|
+
if (json) {
|
|
1143
|
+
console.log(JSON.stringify(items));
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (items.length === 0) {
|
|
1147
|
+
console.log(c.muted(`No snippet results for "${query}".`));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
for (const s of items) {
|
|
1151
|
+
const star = s.starred ? c.amber("\u2605 ") : "";
|
|
1152
|
+
const tags = parseTags(s.tags);
|
|
1153
|
+
const tagStr = tags.length ? " " + tags.map((t) => c.muted(`#${t}`)).join(" ") : "";
|
|
1154
|
+
console.log(`${star}${typeBadge(s.type)} ${c.bright(s.id)}${tagStr}`);
|
|
1155
|
+
if (s.type === "image") {
|
|
1156
|
+
console.log(` ${c.muted("alt:")} ${s.alt || "(none)"} ${c.muted(formatBytes2(s.byte_size))}`);
|
|
1157
|
+
} else {
|
|
1158
|
+
const lines = s.content.split("\n").slice(0, 3);
|
|
1159
|
+
for (const line of lines) {
|
|
1160
|
+
const trimmed = line.length > 100 ? line.slice(0, 100) + "\u2026" : line;
|
|
1161
|
+
console.log(` ${trimmed}`);
|
|
1162
|
+
}
|
|
1163
|
+
if (s.content.split("\n").length > 3) console.log(c.muted(" \u2026"));
|
|
1164
|
+
}
|
|
1165
|
+
if (s.note) console.log(` ${c.muted("note:")} ${s.note}`);
|
|
1166
|
+
if (s.source_url) console.log(` ${c.muted("source:")} ${s.source_url}`);
|
|
1167
|
+
console.log();
|
|
1168
|
+
}
|
|
1169
|
+
console.log(c.muted(`${items.length} result${items.length === 1 ? "" : "s"}`));
|
|
1170
|
+
}
|
|
1171
|
+
async function snippetShow(config, id, json) {
|
|
1172
|
+
const spin = spinner("Loading snippet\u2026");
|
|
1173
|
+
spin.start();
|
|
1174
|
+
const s = await apiFetch(config, `/snippets/${id}`);
|
|
1175
|
+
spin.stop();
|
|
1176
|
+
if (json) {
|
|
1177
|
+
console.log(JSON.stringify(s));
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const tags = parseTags(s.tags);
|
|
1181
|
+
console.log(`${typeBadge(s.type)} ${c.bright(s.id)}${s.starred ? " " + c.amber("\u2605") : ""}`);
|
|
1182
|
+
console.log(c.muted(`created ${s.created_at}`));
|
|
1183
|
+
if (tags.length) console.log(c.muted("tags: ") + tags.map((t) => `#${t}`).join(" "));
|
|
1184
|
+
console.log();
|
|
1185
|
+
if (s.type === "image") {
|
|
1186
|
+
console.log(`${c.muted("alt:")} ${s.alt || "(none)"}`);
|
|
1187
|
+
console.log(`${c.muted("mime:")} ${s.mime || "(unknown)"}`);
|
|
1188
|
+
if (s.width && s.height) console.log(`${c.muted("size:")} ${s.width}\xD7${s.height} (${formatBytes2(s.byte_size)})`);
|
|
1189
|
+
console.log(`${c.muted("url:")} ${config.endpoint}/api/snippets/${s.id}/raw`);
|
|
1190
|
+
} else {
|
|
1191
|
+
if (s.type === "code" && s.language) console.log(c.muted(`language: ${s.language}
|
|
1192
|
+
`));
|
|
1193
|
+
console.log(s.content);
|
|
1194
|
+
}
|
|
1195
|
+
if (s.note) {
|
|
1196
|
+
console.log();
|
|
1197
|
+
console.log(c.muted("Note:"));
|
|
1198
|
+
console.log(s.note);
|
|
1199
|
+
}
|
|
1200
|
+
if (s.source_url || s.source_title || s.source_site_name || s.source_author) {
|
|
1201
|
+
console.log();
|
|
1202
|
+
console.log(c.muted("Source:"));
|
|
1203
|
+
if (s.source_title) console.log(` ${s.source_title}`);
|
|
1204
|
+
if (s.source_url) console.log(` ${c.muted(s.source_url)}`);
|
|
1205
|
+
const meta = [s.source_site_name, s.source_author].filter(Boolean).join(" \xB7 ");
|
|
1206
|
+
if (meta) console.log(` ${c.muted(meta)}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async function snippetNew(config, options, json) {
|
|
1210
|
+
if (options.image) {
|
|
1211
|
+
const path = resolve3(options.image);
|
|
1212
|
+
let stat;
|
|
1213
|
+
try {
|
|
1214
|
+
stat = statSync2(path);
|
|
1215
|
+
} catch {
|
|
1216
|
+
throw new Error(`Image not found: ${path}`);
|
|
1217
|
+
}
|
|
1218
|
+
if (!stat.isFile()) throw new Error(`Not a file: ${path}`);
|
|
1219
|
+
const ext = extname2(path).toLowerCase();
|
|
1220
|
+
const mime = MIME_BY_EXT2[ext];
|
|
1221
|
+
if (!mime) {
|
|
1222
|
+
throw new Error(`Unsupported image extension: ${ext}. Allowed: ${Object.keys(MIME_BY_EXT2).join(", ")}`);
|
|
1223
|
+
}
|
|
1224
|
+
if (stat.size > 25 * 1024 * 1024) {
|
|
1225
|
+
throw new Error(`Image exceeds 25 MB limit (${formatBytes2(stat.size)})`);
|
|
1226
|
+
}
|
|
1227
|
+
const spin2 = spinner(`Uploading ${basename2(path)}\u2026`);
|
|
1228
|
+
spin2.start();
|
|
1229
|
+
const bytes = readFileSync3(path);
|
|
1230
|
+
const form = new FormData();
|
|
1231
|
+
form.set("metadata", JSON.stringify({
|
|
1232
|
+
alt: options.alt ?? null,
|
|
1233
|
+
note: options.note ?? null,
|
|
1234
|
+
source_url: options.sourceUrl ?? null,
|
|
1235
|
+
source_title: options.sourceTitle ?? null,
|
|
1236
|
+
source_site_name: options.sourceSiteName ?? null,
|
|
1237
|
+
source_author: options.sourceAuthor ?? null,
|
|
1238
|
+
tags: options.tags ?? [],
|
|
1239
|
+
starred: options.starred ? 1 : 0
|
|
1240
|
+
}));
|
|
1241
|
+
form.set("file", new File([bytes], basename2(path), { type: mime }));
|
|
1242
|
+
const result2 = await apiFetch(config, "/snippets", {
|
|
1243
|
+
method: "POST",
|
|
1244
|
+
body: form
|
|
1245
|
+
});
|
|
1246
|
+
spin2.stop();
|
|
1247
|
+
if (json) {
|
|
1248
|
+
console.log(JSON.stringify(result2));
|
|
1249
|
+
} else {
|
|
1250
|
+
console.log(`${c.teal("Created image snippet")} ${c.bright(result2.id)}`);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
let content = options.text;
|
|
1255
|
+
if (!content && options.file) {
|
|
1256
|
+
content = readFileSync3(resolve3(options.file), "utf-8");
|
|
1257
|
+
}
|
|
1258
|
+
if (!content && !process.stdin.isTTY) {
|
|
1259
|
+
const chunks = [];
|
|
1260
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1261
|
+
content = Buffer.concat(chunks).toString("utf-8");
|
|
1262
|
+
}
|
|
1263
|
+
if (!content) {
|
|
1264
|
+
throw new Error('No content. Provide --text "...", --file <path>, or pipe content via stdin.');
|
|
1265
|
+
}
|
|
1266
|
+
const spin = spinner("Creating snippet\u2026");
|
|
1267
|
+
spin.start();
|
|
1268
|
+
const result = await apiFetch(config, "/snippets", {
|
|
1269
|
+
method: "POST",
|
|
1270
|
+
headers: { "Content-Type": "application/json" },
|
|
1271
|
+
body: JSON.stringify({
|
|
1272
|
+
type: options.type ?? "quote",
|
|
1273
|
+
content: content.trim(),
|
|
1274
|
+
language: options.language ?? null,
|
|
1275
|
+
note: options.note ?? null,
|
|
1276
|
+
source_url: options.sourceUrl ?? null,
|
|
1277
|
+
source_title: options.sourceTitle ?? null,
|
|
1278
|
+
source_site_name: options.sourceSiteName ?? null,
|
|
1279
|
+
source_author: options.sourceAuthor ?? null,
|
|
1280
|
+
tags: options.tags ?? [],
|
|
1281
|
+
starred: options.starred ? 1 : 0
|
|
1282
|
+
})
|
|
1283
|
+
});
|
|
1284
|
+
spin.stop();
|
|
1285
|
+
if (json) {
|
|
1286
|
+
console.log(JSON.stringify(result));
|
|
1287
|
+
} else {
|
|
1288
|
+
console.log(`${c.teal("Created snippet")} ${c.bright(result.id)} ${c.muted(`(${result.type})`)}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
async function snippetDelete(config, id, json) {
|
|
1292
|
+
const spin = spinner("Deleting snippet\u2026");
|
|
1293
|
+
spin.start();
|
|
1294
|
+
await apiFetch(config, `/snippets/${id}`, { method: "DELETE" });
|
|
1295
|
+
spin.stop();
|
|
1296
|
+
if (json) {
|
|
1297
|
+
console.log(JSON.stringify({ deleted: true, id }));
|
|
1298
|
+
} else {
|
|
1299
|
+
console.log(`${c.teal("Deleted snippet")} ${id}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async function snippetStar(config, id, starred, json) {
|
|
1303
|
+
const result = await apiFetch(config, `/snippets/${id}`, {
|
|
1304
|
+
method: "PATCH",
|
|
1305
|
+
headers: { "Content-Type": "application/json" },
|
|
1306
|
+
body: JSON.stringify({ starred: starred ? 1 : 0 })
|
|
1307
|
+
});
|
|
1308
|
+
if (json) {
|
|
1309
|
+
console.log(JSON.stringify(result));
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(`${c.teal(starred ? "Starred" : "Unstarred")} ${id}`);
|
|
1312
|
+
}
|
|
840
1313
|
}
|
|
841
1314
|
|
|
842
1315
|
// src/commands/doctor.ts
|
|
@@ -877,7 +1350,7 @@ async function doctor(config, json) {
|
|
|
877
1350
|
const where = hasProjectSkill ? "project" : "user";
|
|
878
1351
|
checks.push({ name: "Claude skill", status: "ok", detail: `installed (${where}-level)` });
|
|
879
1352
|
} else {
|
|
880
|
-
checks.push({ name: "Claude skill", status: "warn", detail: "not installed (run `
|
|
1353
|
+
checks.push({ name: "Claude skill", status: "warn", detail: "not installed (run `artyfax skill install`)" });
|
|
881
1354
|
}
|
|
882
1355
|
if (json) {
|
|
883
1356
|
console.log(JSON.stringify(checks));
|
|
@@ -901,27 +1374,576 @@ async function doctor(config, json) {
|
|
|
901
1374
|
}
|
|
902
1375
|
}
|
|
903
1376
|
|
|
1377
|
+
// src/commands/work.ts
|
|
1378
|
+
var STATUS_SYMBOLS = {
|
|
1379
|
+
backlog: "\u25CB",
|
|
1380
|
+
up_next: "\u25CE",
|
|
1381
|
+
on_hold: "\u23F8",
|
|
1382
|
+
in_progress: "\u25CF",
|
|
1383
|
+
in_review: "\u25C9",
|
|
1384
|
+
done: "\u2713",
|
|
1385
|
+
cancelled: "\u2717"
|
|
1386
|
+
};
|
|
1387
|
+
var STATUS_LABELS = {
|
|
1388
|
+
backlog: "Backlog",
|
|
1389
|
+
up_next: "Up Next",
|
|
1390
|
+
on_hold: "On Hold",
|
|
1391
|
+
in_progress: "In Progress",
|
|
1392
|
+
in_review: "In Review",
|
|
1393
|
+
done: "Done",
|
|
1394
|
+
cancelled: "Cancelled"
|
|
1395
|
+
};
|
|
1396
|
+
function statusLabel(s) {
|
|
1397
|
+
const sym = STATUS_SYMBOLS[s] || "?";
|
|
1398
|
+
const label = STATUS_LABELS[s] || s;
|
|
1399
|
+
return `${sym} ${label}`;
|
|
1400
|
+
}
|
|
1401
|
+
function progress(item) {
|
|
1402
|
+
if (item.task_total === 0) return c.muted("-");
|
|
1403
|
+
return `${item.task_done}/${item.task_total}`;
|
|
1404
|
+
}
|
|
1405
|
+
async function workBoard(config, json) {
|
|
1406
|
+
const spin = spinner("Loading board\u2026");
|
|
1407
|
+
spin.start();
|
|
1408
|
+
const data = await apiFetch(config, "/projects/work");
|
|
1409
|
+
spin.stop();
|
|
1410
|
+
if (json) {
|
|
1411
|
+
console.log(JSON.stringify(data.work_items, null, 2));
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (data.work_items.length === 0) {
|
|
1415
|
+
console.log(c.muted("No work items. Promote a document in its settings panel."));
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
1419
|
+
for (const item of data.work_items) {
|
|
1420
|
+
const list2 = byStatus.get(item.work_status) || [];
|
|
1421
|
+
list2.push(item);
|
|
1422
|
+
byStatus.set(item.work_status, list2);
|
|
1423
|
+
}
|
|
1424
|
+
for (const status of ["up_next", "on_hold", "in_progress", "in_review", "backlog"]) {
|
|
1425
|
+
const items = byStatus.get(status);
|
|
1426
|
+
if (!items || items.length === 0) continue;
|
|
1427
|
+
console.log(`
|
|
1428
|
+
${c.bright(statusLabel(status))} ${c.muted(`(${items.length})`)}`);
|
|
1429
|
+
for (const item of items) {
|
|
1430
|
+
const id = item.work_id ? c.amber(item.work_id.padEnd(8)) : c.muted("--------");
|
|
1431
|
+
const prog = progress(item);
|
|
1432
|
+
const agent = item.work_agent ? c.muted(`[${item.work_agent}]`) : "";
|
|
1433
|
+
const current = item.task_current ? c.muted(` \u2014 ${item.task_current.slice(0, 50)}`) : "";
|
|
1434
|
+
console.log(` ${id} ${item.title} ${prog} ${agent}${current}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
console.log();
|
|
1438
|
+
}
|
|
1439
|
+
async function workList(config, json, flags) {
|
|
1440
|
+
const spin = spinner("Loading work items\u2026");
|
|
1441
|
+
spin.start();
|
|
1442
|
+
const qs = new URLSearchParams();
|
|
1443
|
+
if (flags.project) qs.set("project_id", flags.project);
|
|
1444
|
+
if (flags.agent) qs.set("agent", flags.agent);
|
|
1445
|
+
if (flags.status) qs.set("status", flags.status);
|
|
1446
|
+
if (flags.type) qs.set("type", flags.type);
|
|
1447
|
+
const data = await apiFetch(config, `/projects/work?${qs}`);
|
|
1448
|
+
spin.stop();
|
|
1449
|
+
if (json) {
|
|
1450
|
+
console.log(JSON.stringify(data.work_items, null, 2));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (data.work_items.length === 0) {
|
|
1454
|
+
console.log(c.muted("No matching work items."));
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const rows = data.work_items.map((item) => [
|
|
1458
|
+
item.work_id ? c.amber(item.work_id) : c.muted("-"),
|
|
1459
|
+
item.title.slice(0, 50),
|
|
1460
|
+
statusLabel(item.work_status),
|
|
1461
|
+
item.work_type || c.muted("-"),
|
|
1462
|
+
item.work_agent || c.muted("-"),
|
|
1463
|
+
progress(item)
|
|
1464
|
+
]);
|
|
1465
|
+
console.log(table(rows, { header: ["ID", "Title", "Status", "Type", "Agent", "Progress"] }));
|
|
1466
|
+
}
|
|
1467
|
+
async function workNext(config, json, agent) {
|
|
1468
|
+
const spin = spinner("Picking next\u2026");
|
|
1469
|
+
spin.start();
|
|
1470
|
+
const data = await apiFetch(config, "/projects/work?status=up_next");
|
|
1471
|
+
spin.stop();
|
|
1472
|
+
const items = data.work_items;
|
|
1473
|
+
if (items.length === 0) {
|
|
1474
|
+
console.log(json ? "null" : c.muted("No items in Up Next."));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
let pick = items[0];
|
|
1478
|
+
if (agent) {
|
|
1479
|
+
const assigned = items.find((i) => i.work_agent === agent.toLowerCase());
|
|
1480
|
+
const unassigned = items.find((i) => !i.work_agent);
|
|
1481
|
+
pick = assigned || unassigned || items[0];
|
|
1482
|
+
}
|
|
1483
|
+
if (json) {
|
|
1484
|
+
console.log(JSON.stringify(pick, null, 2));
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const id = pick.work_id ? c.amber(pick.work_id) : pick.id;
|
|
1488
|
+
console.log(`${id} ${c.bright(pick.title)}`);
|
|
1489
|
+
if (pick.task_current) console.log(c.muted(` Current: ${pick.task_current}`));
|
|
1490
|
+
console.log(c.muted(` Progress: ${progress(pick)} | Agent: ${pick.work_agent || "unassigned"}`));
|
|
1491
|
+
console.log(c.muted(` ID: ${pick.id}`));
|
|
1492
|
+
}
|
|
1493
|
+
async function resolveWorkId(config, idOrWorkId) {
|
|
1494
|
+
const data = await apiFetch(config, `/resolve/${encodeURIComponent(idOrWorkId)}`);
|
|
1495
|
+
return data.id;
|
|
1496
|
+
}
|
|
1497
|
+
async function workSetStatus(config, docId, status, json) {
|
|
1498
|
+
const demote = ["none", "off", "remove"].includes(status.toLowerCase());
|
|
1499
|
+
const spin = spinner(demote ? "Removing from board\u2026" : `Setting status to ${status}\u2026`);
|
|
1500
|
+
spin.start();
|
|
1501
|
+
const resolvedId = await resolveWorkId(config, docId);
|
|
1502
|
+
const data = await apiFetch(config, `/documents/${resolvedId}/metadata`, {
|
|
1503
|
+
method: "PATCH",
|
|
1504
|
+
headers: { "Content-Type": "application/json" },
|
|
1505
|
+
body: JSON.stringify({ work_status: demote ? null : status })
|
|
1506
|
+
});
|
|
1507
|
+
spin.stop();
|
|
1508
|
+
if (json) {
|
|
1509
|
+
console.log(JSON.stringify(data));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
console.log(c.bright(`${data.work_id || docId} \u2192 ${demote ? "removed from board" : statusLabel(status)}`));
|
|
1513
|
+
}
|
|
1514
|
+
function escapeHtml(s) {
|
|
1515
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1516
|
+
}
|
|
1517
|
+
async function workCreate(config, title, flags, json) {
|
|
1518
|
+
const projectId = flags.project;
|
|
1519
|
+
if (!projectId) {
|
|
1520
|
+
console.error(c.rose("Error: --project is required. Work items must belong to a project."));
|
|
1521
|
+
process.exitCode = 1;
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const spin = spinner("Creating work item\u2026");
|
|
1525
|
+
spin.start();
|
|
1526
|
+
const id = crypto.randomUUID();
|
|
1527
|
+
const slugBody = title.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1528
|
+
const slug = `inbox/${slugBody || `item-${id.slice(0, 8)}`}`;
|
|
1529
|
+
const saveRes = await apiFetch(config, "/documents", {
|
|
1530
|
+
method: "POST",
|
|
1531
|
+
headers: { "Content-Type": "application/json" },
|
|
1532
|
+
body: JSON.stringify({
|
|
1533
|
+
id,
|
|
1534
|
+
slug,
|
|
1535
|
+
title,
|
|
1536
|
+
category: "inbox",
|
|
1537
|
+
md_content: `# ${title}
|
|
1538
|
+
`,
|
|
1539
|
+
format: "md",
|
|
1540
|
+
html_content: `<h1>${escapeHtml(title)}</h1>`
|
|
1541
|
+
})
|
|
1542
|
+
});
|
|
1543
|
+
if (saveRes.ok) {
|
|
1544
|
+
const status = flags.status || "up_next";
|
|
1545
|
+
await apiFetch(config, `/documents/${id}/metadata`, {
|
|
1546
|
+
method: "PATCH",
|
|
1547
|
+
headers: { "Content-Type": "application/json" },
|
|
1548
|
+
body: JSON.stringify({
|
|
1549
|
+
project_id: projectId,
|
|
1550
|
+
work_status: status,
|
|
1551
|
+
work_type: flags.type || null,
|
|
1552
|
+
work_agent: flags.agent || null
|
|
1553
|
+
})
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
spin.stop();
|
|
1557
|
+
if (json) {
|
|
1558
|
+
console.log(JSON.stringify({ ok: true, id }));
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
console.log(c.bright(`Created: ${title}`));
|
|
1562
|
+
console.log(c.muted(`ID: ${id}`));
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// src/commands/project.ts
|
|
1566
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync2 } from "node:fs";
|
|
1567
|
+
import { resolve as resolve4 } from "node:path";
|
|
1568
|
+
var ARTYFAX_FILE = "ARTYFAX.md";
|
|
1569
|
+
var USER_MARKER = "<!-- arty:user -->";
|
|
1570
|
+
async function projectList(config, json) {
|
|
1571
|
+
const spin = spinner("Loading projects\u2026");
|
|
1572
|
+
spin.start();
|
|
1573
|
+
const data = await apiFetch(config, "/projects");
|
|
1574
|
+
spin.stop();
|
|
1575
|
+
if (json) {
|
|
1576
|
+
console.log(JSON.stringify(data.projects, null, 2));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (data.projects.length === 0) {
|
|
1580
|
+
console.log(c.muted('No projects. Create one with `artyfax project create "Name" --prefix XX`.'));
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const rows = data.projects.map((p) => [
|
|
1584
|
+
c.amber(p.prefix),
|
|
1585
|
+
p.name,
|
|
1586
|
+
c.muted(String(p.next_work_id - 1) + " items"),
|
|
1587
|
+
p.description ? c.muted(p.description.slice(0, 40)) : ""
|
|
1588
|
+
]);
|
|
1589
|
+
console.log(table(rows, { header: ["Prefix", "Name", "Items", "Description"] }));
|
|
1590
|
+
}
|
|
1591
|
+
async function projectCreate(config, name, prefix, description, json, workspaces) {
|
|
1592
|
+
const spin = spinner("Creating project\u2026");
|
|
1593
|
+
spin.start();
|
|
1594
|
+
const workspace_ids = workspaces?.length ? await resolveWorkspaceIds(config, workspaces) : [];
|
|
1595
|
+
const data = await apiFetch(config, "/projects", {
|
|
1596
|
+
method: "POST",
|
|
1597
|
+
headers: { "Content-Type": "application/json" },
|
|
1598
|
+
body: JSON.stringify({
|
|
1599
|
+
name,
|
|
1600
|
+
prefix,
|
|
1601
|
+
description,
|
|
1602
|
+
workspace_scope: workspace_ids.length ? "scoped" : "global",
|
|
1603
|
+
workspace_ids
|
|
1604
|
+
})
|
|
1605
|
+
});
|
|
1606
|
+
spin.stop();
|
|
1607
|
+
if (json) {
|
|
1608
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
console.log(c.bright(`Project created: ${name} (${c.amber(data.prefix)})`));
|
|
1612
|
+
}
|
|
1613
|
+
var STATUS_ORDER = ["in_progress", "in_review", "up_next", "on_hold", "backlog", "done", "cancelled"];
|
|
1614
|
+
var STATUS_HEADINGS = {
|
|
1615
|
+
in_progress: "In Progress",
|
|
1616
|
+
in_review: "In Review",
|
|
1617
|
+
up_next: "Up Next",
|
|
1618
|
+
on_hold: "On Hold",
|
|
1619
|
+
backlog: "Backlog",
|
|
1620
|
+
done: "Done",
|
|
1621
|
+
cancelled: "Cancelled"
|
|
1622
|
+
};
|
|
1623
|
+
function formatProgress(item) {
|
|
1624
|
+
if (item.task_total === 0) return "";
|
|
1625
|
+
return ` [${item.task_done}/${item.task_total}]`;
|
|
1626
|
+
}
|
|
1627
|
+
function resolveProject(projects, hint) {
|
|
1628
|
+
if (hint) {
|
|
1629
|
+
return projects.find(
|
|
1630
|
+
(p) => p.slug === hint || p.prefix.toLowerCase() === hint.toLowerCase() || p.name.toLowerCase() === hint.toLowerCase()
|
|
1631
|
+
) || null;
|
|
1632
|
+
}
|
|
1633
|
+
if (projects.length === 1) return projects[0];
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
function parseExistingPrefix(filePath) {
|
|
1637
|
+
if (!existsSync2(filePath)) return null;
|
|
1638
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
1639
|
+
const match = content.match(/^# .+\(([A-Z]+)\)/m);
|
|
1640
|
+
return match ? match[1] : null;
|
|
1641
|
+
}
|
|
1642
|
+
function extractUserContent(filePath) {
|
|
1643
|
+
if (!existsSync2(filePath)) return "";
|
|
1644
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
1645
|
+
const matches = [...content.matchAll(new RegExp(`^${USER_MARKER}$`, "gm"))];
|
|
1646
|
+
const match = matches[matches.length - 1];
|
|
1647
|
+
if (!match || match.index == null) return "";
|
|
1648
|
+
return content.substring(match.index + USER_MARKER.length);
|
|
1649
|
+
}
|
|
1650
|
+
function padTable(rows) {
|
|
1651
|
+
const cols = rows[0].length;
|
|
1652
|
+
const widths = Array.from(
|
|
1653
|
+
{ length: cols },
|
|
1654
|
+
(_, i) => Math.max(...rows.map((r) => r[i].length))
|
|
1655
|
+
);
|
|
1656
|
+
const lines = [];
|
|
1657
|
+
for (let r = 0; r < rows.length; r++) {
|
|
1658
|
+
const cells = rows[r].map((cell, i) => ` ${cell.padEnd(widths[i])} `);
|
|
1659
|
+
lines.push(`|${cells.join("|")}|`);
|
|
1660
|
+
if (r === 0) {
|
|
1661
|
+
const sep = widths.map((w) => "-".repeat(w + 2));
|
|
1662
|
+
lines.push(`|${sep.join("|")}|`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return lines.join("\n");
|
|
1666
|
+
}
|
|
1667
|
+
function generateWorkflow(project) {
|
|
1668
|
+
const p = project.prefix;
|
|
1669
|
+
const tbl = padTable([
|
|
1670
|
+
["Action", "CLI", "MCP"],
|
|
1671
|
+
["Board overview", "`artyfax work list`", `\`list_work(project: '${p}')\``],
|
|
1672
|
+
["Read a work item", "`artyfax read <slug>`", `\`get_document('${p}-2', include_annotations: true)\``],
|
|
1673
|
+
["Update content", "`artyfax update <slug> <file>`", `\`patch_document('${p}-2', patches)\``],
|
|
1674
|
+
["Tick a task", '`artyfax task <id> "text" <state>`', `\`update_task('${p}-2', task, state)\``],
|
|
1675
|
+
["Claim and start", "`artyfax work status <id> in_progress`", `\`start_work('${p}-2', agent: '<agent>')\``],
|
|
1676
|
+
["Change status", "`artyfax work status <id> <status>`", `\`update_work_status('${p}-2', status)\``],
|
|
1677
|
+
["Refresh this file", "`artyfax init`", "--"]
|
|
1678
|
+
]);
|
|
1679
|
+
return `## Workflow
|
|
1680
|
+
|
|
1681
|
+
This project tracks work in [Artyfax](https://artyfax.io). Reference this file for the landscape, then use the MCP tools or CLI for live data.
|
|
1682
|
+
|
|
1683
|
+
${tbl}
|
|
1684
|
+
|
|
1685
|
+
The CLI command is \`artyfax\`. The examples above spell it out in full; if you prefer, shorten it to \`arty\` or \`ax\` - they are the same command.
|
|
1686
|
+
|
|
1687
|
+
All MCP tools that take a document ID also accept work IDs (e.g. \`${p}-2\`) directly.
|
|
1688
|
+
|
|
1689
|
+
### Picking Up Agent Work
|
|
1690
|
+
|
|
1691
|
+
When asked for the next task for a specific agent (for example "next Codex task"), use live Artyfax data and check: assigned \`in_progress\`, assigned \`up_next\`, assigned \`backlog\`, top unassigned \`up_next\`, and any work ID implied by the worktree name. If an assigned \`up_next\` item exists, use it. If the plausible choices differ (for example assigned backlog vs unassigned \`up_next\`), confirm before claiming. Never claim \`in_review\`, \`done\`, or another agent's item without explicit direction.
|
|
1692
|
+
|
|
1693
|
+
### Lifecycle
|
|
1694
|
+
|
|
1695
|
+
1. **Pick up work** - read this file for the landscape, then use live MCP/CLI data to select the correct item. Use the agent-specific pickup flow above when the requester names an agent.
|
|
1696
|
+
2. **Claim it before writing code** - \`start_work('${p}-15', agent: '<agent>')\` moves to In Progress, promotes to top of column, and shows your name on the board. Always do this before implementing anything so the board reflects active work. Claiming signals ownership; it is not approval to start coding when the work item still needs clarification.
|
|
1697
|
+
3. **Work, updating the board per task** - read the doc with \`get_document('${p}-15')\`, then for each task: mark it \`active\` when you start it and \`done\` when you finish it via \`update_task('${p}-15', '<task substring>', 'active'|'done')\`. \`active\` surfaces the live current task on the board so other sessions (and the user) can see what is being worked right now, not just a count.
|
|
1698
|
+
4. **Blocked?** - \`update_work_status('${p}-15', 'on_hold')\` and note why
|
|
1699
|
+
5. **Resuming work** - if you pick an item back up after it has left In Progress (it sits in In Review, on_hold, or up_next), call \`update_work_status('${p}-15', 'in_progress')\` *first*, so the board shows it active again while you work. Move it back to In Review only once the new work is finished.
|
|
1700
|
+
6. **Complete** - after pushing the final commit, call \`complete_work('${p}-15')\` to move to In Review. "In Review" means implementation is done and waiting for the user to verify - not that it's perfect. Before completing, every open question and every unticked task must be put to the user - you do not decide their fate yourself. A "stretch", "consider", or "optional" task is still an open question. Surface each one with a structured question/approval tool (\`AskUserQuestion\` where the runtime provides one) so the user chooses what happens: do it now, split it into a new follow-up work item, or drop it. You do not get to judge something "quick and safe" and skip the question. Never move an item to In Review with a dangling task or unanswered question left hanging.
|
|
1701
|
+
7. **Done** - only the user moves parent items from In Review to Done after personal review
|
|
1702
|
+
|
|
1703
|
+
### Thin work items
|
|
1704
|
+
|
|
1705
|
+
When a work item only has a short title/description and no task checklist, don't jump straight into implementation.
|
|
1706
|
+
|
|
1707
|
+
1. Read the title and description.
|
|
1708
|
+
2. Write an \`## Understanding\` section in the Artyfax document with the inferred scope, assumptions, and non-goals.
|
|
1709
|
+
3. Add an \`## Tasks\` checklist before editing code.
|
|
1710
|
+
4. Ask the requester to approve the understanding and task list before starting implementation. Use a structured question/approval tool when the runtime provides one. If not, say so and wait for typed approval.
|
|
1711
|
+
5. After approval, mark the first task \`active\`, then update checkbox progress as the work proceeds.
|
|
1712
|
+
|
|
1713
|
+
If the requester corrects workflow sequencing mid-session, pause implementation, repair the Artyfax document/checklist first, and resume from the checklist rather than local memory.
|
|
1714
|
+
|
|
1715
|
+
Open questions are a completion blocker, not just a planning step. Any unresolved question or unticked task - including ones you uncover while building - must go back to the user through a structured question (\`AskUserQuestion\` where available) before the item can move to In Review. You surface and the user decides; you never resolve an open question on your own authority.
|
|
1716
|
+
|
|
1717
|
+
### Board update cadence
|
|
1718
|
+
|
|
1719
|
+
The board is the cross-session signal - other agents and the user read live progress from it, so it must reflect reality *as you work*, not after the fact.
|
|
1720
|
+
|
|
1721
|
+
- **Target: per task.** Mark each task \`active\` on start and \`done\` on finish via \`update_task\`. It is a single lightweight call (substring match, server reparses progress automatically) - no full-content rewrite, so the cost is negligible. Prefer it over \`patch_document\` for ticking; reserve \`patch_document\` for editing the doc body.
|
|
1722
|
+
- **Floor: per logical section.** If per-task is genuinely too chatty for a fast run of trivial steps, batch a section's ticks in one \`update_task\` sequence the moment that section is done and verified - never later.
|
|
1723
|
+
- **Never batch only at the end.** A board that jumps from 0 to done in one shot defeats the purpose and hides active work from concurrent sessions.
|
|
1724
|
+
- **Cost note:** \`update_task\` is cheap, but each call is a synchronous round trip (MCP calls can't be backgrounded). For a long run of one-line tasks, the per-section floor is the sensible trade; for substantial tasks, always go per task.
|
|
1725
|
+
|
|
1726
|
+
### Keeping this file current
|
|
1727
|
+
|
|
1728
|
+
This file is generated by \`artyfax init\`. Do not hand-edit generated workflow or board sections for durable changes; update the CLI generator/template, then regenerate. Custom notes in the user section are preserved.
|
|
1729
|
+
|
|
1730
|
+
Use Artyfax MCP/CLI for live work updates (status changes, task ticks, assignments). Run \`artyfax init\` to refresh this snapshot when the generated board view is stale or after meaningful board changes.
|
|
1731
|
+
|
|
1732
|
+
### Conventions
|
|
1733
|
+
|
|
1734
|
+
- **Work IDs** (e.g. ${p}-15) are stable. Reference them in commits and changelogs. All MCP tools accept them directly.
|
|
1735
|
+
- **Descriptions**: concise, state what it delivers, not how it works - the MCP tool guidance carries the exact length.
|
|
1736
|
+
- **Statuses**: backlog > up_next > in_progress > in_review > done (or on_hold, cancelled).
|
|
1737
|
+
`;
|
|
1738
|
+
}
|
|
1739
|
+
function generateSaving() {
|
|
1740
|
+
return `## Saving documents
|
|
1741
|
+
|
|
1742
|
+
Artyfax is the default home for documents. When you produce a document for the user, or they hand you one (markdown, HTML, a report, a generated artifact, a story), save it into Artyfax with the MCP tools or the \`artyfax\` CLI - not the local filesystem. Reach for a loose local file only when the user explicitly asks. A standalone HTML page is a first-class document: save it as-is, don't restyle it to match other documents and don't check whether the reader can render it first.
|
|
1743
|
+
|
|
1744
|
+
### Images on documents
|
|
1745
|
+
|
|
1746
|
+
To put a local or freshly generated image on a document, as a cover or inline, use the CLI - not the MCP \`set_cover_image\` tool, whose base64 path truncates large images over the transport and degrades them (its URL option only helps when the image already lives at a public URL).
|
|
1747
|
+
|
|
1748
|
+
- \`artyfax cover <doc> <file>\` - set a document's cover image (renders best at 1200x630).
|
|
1749
|
+
- \`artyfax image <doc> <file>\` - upload an inline image and print the \`\` markdown to paste into the body.
|
|
1750
|
+
|
|
1751
|
+
\`<doc>\` is a doc id, slug, or work id (any document). Accepts png/jpg/webp/gif/avif/svg up to 5 MB. A public image URL in \`\` is re-hosted automatically on save, so only local files need these commands; \`set_cover_image\` with \`image_url\` is still right for a cover already at a public URL.
|
|
1752
|
+
`;
|
|
1753
|
+
}
|
|
1754
|
+
function generateBoard(items) {
|
|
1755
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
1756
|
+
for (const item of items) {
|
|
1757
|
+
const list2 = byStatus.get(item.work_status) || [];
|
|
1758
|
+
list2.push(item);
|
|
1759
|
+
byStatus.set(item.work_status, list2);
|
|
1760
|
+
}
|
|
1761
|
+
const lines = [];
|
|
1762
|
+
for (const status of STATUS_ORDER) {
|
|
1763
|
+
const group = byStatus.get(status);
|
|
1764
|
+
if (!group || group.length === 0) continue;
|
|
1765
|
+
const heading = STATUS_HEADINGS[status] || status;
|
|
1766
|
+
if (status === "done" || status === "cancelled") {
|
|
1767
|
+
lines.push(`## ${heading} (${group.length})`);
|
|
1768
|
+
lines.push("");
|
|
1769
|
+
lines.push(`${group.length} ${status === "done" ? "completed" : "cancelled"} items. Use \`artyfax work list --status ${status}\` or \`list_work(status: '${status}')\` for details.`);
|
|
1770
|
+
} else {
|
|
1771
|
+
lines.push(`## ${heading}`);
|
|
1772
|
+
lines.push("");
|
|
1773
|
+
for (const item of group) {
|
|
1774
|
+
const id = item.work_id || item.id.slice(0, 8);
|
|
1775
|
+
const prog = formatProgress(item);
|
|
1776
|
+
const agent = item.work_agent ? ` @${item.work_agent}` : "";
|
|
1777
|
+
lines.push(`- ${id} ${item.title}${prog}${agent}`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
lines.push("");
|
|
1781
|
+
}
|
|
1782
|
+
return lines.join("\n");
|
|
1783
|
+
}
|
|
1784
|
+
async function init(config, projectHint) {
|
|
1785
|
+
const filePath = resolve4(process.cwd(), ARTYFAX_FILE);
|
|
1786
|
+
const isUpdate = existsSync2(filePath);
|
|
1787
|
+
const spin = spinner(isUpdate ? "Refreshing ARTYFAX.md\u2026" : "Initialising ARTYFAX.md\u2026");
|
|
1788
|
+
spin.start();
|
|
1789
|
+
const existingPrefix = isUpdate ? parseExistingPrefix(filePath) : null;
|
|
1790
|
+
const hint = projectHint || existingPrefix || void 0;
|
|
1791
|
+
const projects = await apiFetch(config, "/projects");
|
|
1792
|
+
const project = resolveProject(projects.projects, hint);
|
|
1793
|
+
if (!project) {
|
|
1794
|
+
spin.stop();
|
|
1795
|
+
if (projects.projects.length === 0) {
|
|
1796
|
+
console.error(c.rose('No projects found. Create one first: artyfax project create "Name" --prefix XX'));
|
|
1797
|
+
} else {
|
|
1798
|
+
const prefixes = projects.projects.map((p) => c.amber(p.prefix)).join(", ");
|
|
1799
|
+
console.error(c.rose(`Specify a project: artyfax init <prefix>`));
|
|
1800
|
+
console.error(c.muted(`Available: ${prefixes}`));
|
|
1801
|
+
}
|
|
1802
|
+
process.exitCode = 1;
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const qs = new URLSearchParams({ project_id: project.id });
|
|
1806
|
+
const data = await apiFetch(config, `/projects/work?${qs}`);
|
|
1807
|
+
spin.stop();
|
|
1808
|
+
const userContent = extractUserContent(filePath);
|
|
1809
|
+
const d = /* @__PURE__ */ new Date();
|
|
1810
|
+
const now = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1811
|
+
const generated = [
|
|
1812
|
+
`# ${project.name} (${project.prefix})`,
|
|
1813
|
+
"",
|
|
1814
|
+
`Last updated: ${now} \xB7 Live data: \`artyfax work list\` or \`list_work(project: '${project.prefix}')\``,
|
|
1815
|
+
"",
|
|
1816
|
+
generateSaving(),
|
|
1817
|
+
generateWorkflow(project),
|
|
1818
|
+
generateBoard(data.work_items),
|
|
1819
|
+
USER_MARKER
|
|
1820
|
+
].join("\n");
|
|
1821
|
+
const content = generated + userContent;
|
|
1822
|
+
writeFileSync(filePath, content);
|
|
1823
|
+
const action = isUpdate ? "Updated" : "Created";
|
|
1824
|
+
console.log(c.bright(`${action} ${ARTYFAX_FILE}`));
|
|
1825
|
+
console.log(c.muted(`${project.name} (${project.prefix}) \xB7 ${data.work_items.length} work items`));
|
|
1826
|
+
if (!isUpdate) {
|
|
1827
|
+
const claudeMd = resolve4(process.cwd(), "CLAUDE.md");
|
|
1828
|
+
if (existsSync2(claudeMd)) {
|
|
1829
|
+
const claudeContent = readFileSync4(claudeMd, "utf-8");
|
|
1830
|
+
if (!claudeContent.includes("@ARTYFAX.md")) {
|
|
1831
|
+
console.log("");
|
|
1832
|
+
console.log(c.muted("Add to CLAUDE.md to give agents the project board:"));
|
|
1833
|
+
console.log(c.amber(" @ARTYFAX.md"));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
904
1839
|
// src/commands/skill.ts
|
|
905
1840
|
async function skillInstall(_project) {
|
|
906
1841
|
console.log(c.amber("Skill installation is not available yet."));
|
|
907
1842
|
console.log(c.muted("The Artyfax skill content is being designed in design-28."));
|
|
908
|
-
console.log(c.muted("Once ready, `
|
|
1843
|
+
console.log(c.muted("Once ready, `artyfax skill install` will write skill files to ~/.claude/skills/artyfax"));
|
|
909
1844
|
}
|
|
910
1845
|
async function skillStatus() {
|
|
911
1846
|
console.log(c.amber("Skill status is not available yet."));
|
|
912
|
-
console.log(c.muted("Run `
|
|
1847
|
+
console.log(c.muted("Run `artyfax doctor` to check basic setup."));
|
|
913
1848
|
}
|
|
914
1849
|
async function skillUpdate() {
|
|
915
1850
|
console.log(c.amber("Skill update is not available yet."));
|
|
916
1851
|
}
|
|
917
1852
|
|
|
1853
|
+
// src/commands/task.ts
|
|
1854
|
+
async function resolveId(config, identifier) {
|
|
1855
|
+
const data = await apiFetch(config, `/resolve/${encodeURIComponent(identifier)}`);
|
|
1856
|
+
return data.id;
|
|
1857
|
+
}
|
|
1858
|
+
async function taskList(config, identifier, json) {
|
|
1859
|
+
const spin = spinner("Loading tasks\u2026");
|
|
1860
|
+
spin.start();
|
|
1861
|
+
const id = await resolveId(config, identifier);
|
|
1862
|
+
const doc = await apiFetch(config, `/documents/${id}/content`);
|
|
1863
|
+
spin.stop();
|
|
1864
|
+
if (doc.format !== "md" || !doc.md_source) {
|
|
1865
|
+
console.error(c.rose("Task listing only supported for markdown documents"));
|
|
1866
|
+
process.exit(1);
|
|
1867
|
+
}
|
|
1868
|
+
const tasks = parseTasks(doc.md_source);
|
|
1869
|
+
if (json) {
|
|
1870
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (tasks.length === 0) {
|
|
1874
|
+
console.log(c.muted("No tasks found"));
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
const stateIcon = { done: c.teal("\u2713"), active: c.amber("~"), pending: c.muted("\u25CB") };
|
|
1878
|
+
for (const t of tasks) {
|
|
1879
|
+
console.log(`${stateIcon[t.state]} ${c.muted(`[${t.index}]`)} ${t.text}`);
|
|
1880
|
+
}
|
|
1881
|
+
const done = tasks.filter((t) => t.state === "done").length;
|
|
1882
|
+
const active = tasks.filter((t) => t.state === "active").length;
|
|
1883
|
+
console.log(c.muted(`
|
|
1884
|
+
${done}/${tasks.length} done${active ? `, ${active} active` : ""}`));
|
|
1885
|
+
}
|
|
1886
|
+
async function taskUpdate(config, identifier, taskText, state, json) {
|
|
1887
|
+
if (!["active", "done", "pending"].includes(state)) {
|
|
1888
|
+
console.error(c.rose(`Invalid state "${state}". Must be: active, done, pending`));
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
const spin = spinner(`Setting task to ${state}\u2026`);
|
|
1892
|
+
spin.start();
|
|
1893
|
+
const id = await resolveId(config, identifier);
|
|
1894
|
+
const doc = await apiFetch(config, `/documents/${id}/content`);
|
|
1895
|
+
const tasks = parseTasks(doc.md_source);
|
|
1896
|
+
const needle = taskText.toLowerCase();
|
|
1897
|
+
const match = tasks.find((t) => t.text.toLowerCase().includes(needle));
|
|
1898
|
+
if (!match) {
|
|
1899
|
+
spin.stop();
|
|
1900
|
+
console.error(c.rose(`No task matching "${taskText}" found`));
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
await apiFetch(config, `/documents/${id}/task/${match.index}`, {
|
|
1904
|
+
method: "PATCH",
|
|
1905
|
+
headers: { "Content-Type": "application/json" },
|
|
1906
|
+
body: JSON.stringify({ state })
|
|
1907
|
+
});
|
|
1908
|
+
spin.stop();
|
|
1909
|
+
if (json) {
|
|
1910
|
+
console.log(JSON.stringify({ ok: true, task_index: match.index, task_text: match.text, state }));
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const stateLabel = { done: c.teal("done"), active: c.amber("active"), pending: c.muted("pending") };
|
|
1914
|
+
console.log(`${stateLabel[state]} ${match.text}`);
|
|
1915
|
+
}
|
|
1916
|
+
function parseTasks(content) {
|
|
1917
|
+
const lines = content.split("\n");
|
|
1918
|
+
const taskRe = /^[\t ]*[-*+]\s+\[([xX~ ])\]\s*(.*)/;
|
|
1919
|
+
const fenceRe = /^\s*(`{3,}|~{3,})/;
|
|
1920
|
+
let inFence = false;
|
|
1921
|
+
const tasks = [];
|
|
1922
|
+
let count = 0;
|
|
1923
|
+
const charToState = { x: "done", X: "done", "~": "active", " ": "pending" };
|
|
1924
|
+
for (const line of lines) {
|
|
1925
|
+
if (fenceRe.test(line)) {
|
|
1926
|
+
inFence = !inFence;
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
if (inFence) continue;
|
|
1930
|
+
const m = line.match(taskRe);
|
|
1931
|
+
if (m) {
|
|
1932
|
+
tasks.push({ index: count, state: charToState[m[1]] || "pending", text: m[2].trim() });
|
|
1933
|
+
count++;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return tasks;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
918
1939
|
// src/cli.ts
|
|
919
|
-
var VERSION = true ? "0.
|
|
1940
|
+
var VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
920
1941
|
function brandedHelp() {
|
|
921
1942
|
return `
|
|
922
1943
|
${c.amber("artyfax")} ${c.muted(`v${VERSION}`)} \u2014 your personal document library
|
|
923
1944
|
|
|
924
|
-
${c.bright("Usage:")}
|
|
1945
|
+
${c.bright("Usage:")} artyfax <command> [options]
|
|
1946
|
+
${c.muted("aliases:")} arty <command> \xB7 ax <command>
|
|
925
1947
|
|
|
926
1948
|
${c.bright("Documents")}
|
|
927
1949
|
save <file> Save a document (markdown or HTML)
|
|
@@ -935,20 +1957,46 @@ ${c.bright("Documents")}
|
|
|
935
1957
|
open <slug> Open document in browser
|
|
936
1958
|
secure <slug> Encrypt a document
|
|
937
1959
|
unsecure <slug> Remove encryption
|
|
1960
|
+
cover <doc> <file> Set a document's cover image from a local file
|
|
1961
|
+
image <doc> <file> Upload a local image, print the markdown to embed
|
|
938
1962
|
|
|
939
1963
|
${c.bright("Sub-resources")}
|
|
940
1964
|
share create <slug> Create a share link
|
|
941
1965
|
share list [slug] List shares
|
|
942
1966
|
share revoke <hash> Revoke a share link
|
|
943
1967
|
cat list List categories
|
|
944
|
-
tag list List tags
|
|
1968
|
+
tag list List tags (grouped by namespace)
|
|
1969
|
+
tag delete <tag> Remove a tag from all docs, annotations, snippets
|
|
945
1970
|
version list <slug> Version history
|
|
946
1971
|
version restore <slug> Restore a previous version
|
|
947
1972
|
|
|
948
1973
|
${c.bright("Annotations")}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1974
|
+
annotation list <slug> List a document's annotations (--include-recipients, --since)
|
|
1975
|
+
annotation search <q> Search annotations
|
|
1976
|
+
|
|
1977
|
+
${c.bright("Snippets")}
|
|
1978
|
+
snippets list List snippets (filter --type --tag --starred)
|
|
1979
|
+
snippets search <query> Full-text search across snippets
|
|
1980
|
+
snippets show <id> Show a snippet's full content
|
|
1981
|
+
snippets new Create a snippet (--text, --file, --image)
|
|
1982
|
+
snippets delete <id> Delete a snippet
|
|
1983
|
+
snippets star <id> Star a snippet
|
|
1984
|
+
snippets unstar <id> Unstar a snippet
|
|
1985
|
+
|
|
1986
|
+
${c.bright("Work")}
|
|
1987
|
+
work Board summary (kanban overview)
|
|
1988
|
+
work list List work items (--project, --agent, --status, --type)
|
|
1989
|
+
work next Pick next Up Next item (--agent)
|
|
1990
|
+
work start <id> Mark In Progress
|
|
1991
|
+
work done <id> Mark Done
|
|
1992
|
+
work status <id> <s> Set arbitrary status ("none" removes from board)
|
|
1993
|
+
work create "title" Quick-create a work item (--project, --type, --agent)
|
|
1994
|
+
|
|
1995
|
+
${c.bright("Projects")}
|
|
1996
|
+
project list List projects
|
|
1997
|
+
project create "name" Create a project (--prefix XX)
|
|
1998
|
+
init [prefix] Generate or refresh ARTYFAX.md for a project board
|
|
1999
|
+
workspace list List workspaces
|
|
952
2000
|
|
|
953
2001
|
${c.bright("Tools")}
|
|
954
2002
|
doctor Verify CLI setup
|
|
@@ -971,10 +2019,10 @@ ${c.bright("Environment")}
|
|
|
971
2019
|
`.trim();
|
|
972
2020
|
}
|
|
973
2021
|
var COMMAND_HELP = {
|
|
974
|
-
save: `${c.amber("
|
|
2022
|
+
save: `${c.amber("artyfax save")} \u2014 save a document
|
|
975
2023
|
|
|
976
|
-
${c.bright("Usage:")}
|
|
977
|
-
|
|
2024
|
+
${c.bright("Usage:")} artyfax save <file> [options]
|
|
2025
|
+
artyfax save --url <url> [options]
|
|
978
2026
|
|
|
979
2027
|
${c.bright("Options:")}
|
|
980
2028
|
--url <url> Save from URL (server-side extraction)
|
|
@@ -986,26 +2034,27 @@ ${c.bright("Options:")}
|
|
|
986
2034
|
--theme <name> Theme override
|
|
987
2035
|
--parent <id> Parent document ID
|
|
988
2036
|
--title <name> Title (default: derived from filename)
|
|
2037
|
+
--force Save a new copy even if the URL was already saved
|
|
989
2038
|
--json JSON output
|
|
990
2039
|
|
|
991
2040
|
${c.bright("Examples:")}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
read: `${c.amber("
|
|
2041
|
+
artyfax save notes.md --category inbox
|
|
2042
|
+
artyfax save --url https://example.com/article --category articles
|
|
2043
|
+
artyfax save report.md --secure --category reports`,
|
|
2044
|
+
read: `${c.amber("artyfax read")} \u2014 read document content
|
|
996
2045
|
|
|
997
|
-
${c.bright("Usage:")}
|
|
2046
|
+
${c.bright("Usage:")} artyfax read <slug>
|
|
998
2047
|
|
|
999
2048
|
Outputs the document's markdown source to stdout. Handles E2EE
|
|
1000
2049
|
decryption automatically (prompts for passphrase if needed).
|
|
1001
2050
|
|
|
1002
2051
|
${c.bright("Examples:")}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
list: `${c.amber("
|
|
2052
|
+
artyfax read inbox/my-doc
|
|
2053
|
+
artyfax read my-doc # partial slug match
|
|
2054
|
+
artyfax read inbox/my-doc > out.md # pipe to file`,
|
|
2055
|
+
list: `${c.amber("artyfax list")} \u2014 list documents
|
|
1007
2056
|
|
|
1008
|
-
${c.bright("Usage:")}
|
|
2057
|
+
${c.bright("Usage:")} artyfax list [options]
|
|
1009
2058
|
|
|
1010
2059
|
${c.bright("Options:")}
|
|
1011
2060
|
--category <name> Filter by category
|
|
@@ -1013,43 +2062,46 @@ ${c.bright("Options:")}
|
|
|
1013
2062
|
--offset <n> Skip first N results
|
|
1014
2063
|
--archived Include archived documents
|
|
1015
2064
|
--parent-id <id> Filter by parent document
|
|
2065
|
+
--tag <tag> Filter to documents carrying this exact tag
|
|
1016
2066
|
--json JSON output
|
|
1017
2067
|
|
|
1018
2068
|
${c.bright("Examples:")}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
2069
|
+
artyfax list --category inbox --limit 10
|
|
2070
|
+
artyfax list --tag role:design
|
|
2071
|
+
artyfax list --archived --json`,
|
|
2072
|
+
get: `${c.amber("artyfax get")} \u2014 document metadata
|
|
1022
2073
|
|
|
1023
|
-
${c.bright("Usage:")}
|
|
2074
|
+
${c.bright("Usage:")} artyfax get <slug>
|
|
1024
2075
|
|
|
1025
2076
|
Outputs full document metadata as JSON. For scripting and inspection.
|
|
1026
2077
|
|
|
1027
2078
|
${c.bright("Examples:")}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
search: `${c.amber("
|
|
2079
|
+
artyfax get inbox/my-doc
|
|
2080
|
+
artyfax get my-doc | jq .category`,
|
|
2081
|
+
search: `${c.amber("artyfax search")} \u2014 full-text search
|
|
1031
2082
|
|
|
1032
|
-
${c.bright("Usage:")}
|
|
2083
|
+
${c.bright("Usage:")} artyfax search <query> [--json]
|
|
1033
2084
|
|
|
1034
2085
|
${c.bright("Examples:")}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
update: `${c.amber("
|
|
2086
|
+
artyfax search "deployment guide"
|
|
2087
|
+
artyfax search cloudflare --json`,
|
|
2088
|
+
update: `${c.amber("artyfax update")} \u2014 push updated content
|
|
1038
2089
|
|
|
1039
|
-
${c.bright("Usage:")}
|
|
1040
|
-
cat file.md |
|
|
2090
|
+
${c.bright("Usage:")} artyfax update <slug> [file]
|
|
2091
|
+
cat file.md | artyfax update <slug>
|
|
1041
2092
|
|
|
1042
2093
|
Reads from file or stdin. Auto-encrypts if the target is a secure document.
|
|
1043
2094
|
|
|
1044
2095
|
${c.bright("Examples:")}
|
|
1045
|
-
|
|
1046
|
-
echo "# New content" |
|
|
1047
|
-
metadata: `${c.amber("
|
|
2096
|
+
artyfax update inbox/my-doc updated.md
|
|
2097
|
+
echo "# New content" | artyfax update inbox/my-doc`,
|
|
2098
|
+
metadata: `${c.amber("artyfax metadata")} \u2014 update document metadata
|
|
1048
2099
|
|
|
1049
|
-
${c.bright("Usage:")}
|
|
2100
|
+
${c.bright("Usage:")} artyfax metadata <slug> [options]
|
|
1050
2101
|
|
|
1051
2102
|
${c.bright("Options:")}
|
|
1052
2103
|
--category <name> Move to category
|
|
2104
|
+
--slug <cat/slug> Rename the URL slug (cosmetic; URLs resolve by id)
|
|
1053
2105
|
--tags <t1,t2> Set tags (comma-separated)
|
|
1054
2106
|
--title <name> Update title
|
|
1055
2107
|
--theme <name> Set theme
|
|
@@ -1058,61 +2110,95 @@ ${c.bright("Options:")}
|
|
|
1058
2110
|
--json JSON output
|
|
1059
2111
|
|
|
1060
2112
|
${c.bright("Examples:")}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
2113
|
+
artyfax metadata inbox/my-doc --category reports --tags "q1,finance"
|
|
2114
|
+
artyfax metadata my-doc --slug plans/rename-safe-urls
|
|
2115
|
+
artyfax metadata my-doc --archived`,
|
|
2116
|
+
cover: `${c.amber("artyfax cover")} \u2014 set a document's cover image
|
|
2117
|
+
|
|
2118
|
+
${c.bright("Usage:")} artyfax cover <doc> <image-file> [options]
|
|
2119
|
+
|
|
2120
|
+
Uploads a local image and sets it as the document's cover. Works for any
|
|
2121
|
+
document (pass a doc id, slug, or work id). Unlike the MCP set_cover_image
|
|
2122
|
+
tool, this carries a full-quality local image with no transport truncation.
|
|
2123
|
+
|
|
2124
|
+
${c.bright("Options:")}
|
|
2125
|
+
--alt "..." Alt text for the cover image
|
|
2126
|
+
--json JSON output
|
|
1064
2127
|
|
|
1065
|
-
${c.bright("
|
|
2128
|
+
${c.bright("Notes:")}
|
|
2129
|
+
Accepts png/jpg/webp/gif/avif/svg up to 5 MB. Covers render best at
|
|
2130
|
+
1200\xD7630 (the hero + social card shape).
|
|
2131
|
+
|
|
2132
|
+
${c.bright("Examples:")}
|
|
2133
|
+
artyfax cover p8KV4dvrgJTA ./hero.png
|
|
2134
|
+
artyfax cover inbox/my-doc ./hero.png --alt "Release banner"`,
|
|
2135
|
+
image: `${c.amber("artyfax image")} \u2014 upload an inline image to a document
|
|
2136
|
+
|
|
2137
|
+
${c.bright("Usage:")} artyfax image <doc> <image-file> [options]
|
|
2138
|
+
|
|
2139
|
+
Uploads a local image into the document's image store and prints the
|
|
2140
|
+
markdown snippet to paste into the body. Works for any document (doc id,
|
|
2141
|
+
slug, or work id).
|
|
2142
|
+
|
|
2143
|
+
${c.bright("Options:")}
|
|
2144
|
+
--alt "..." Alt text for the image
|
|
2145
|
+
--json JSON output
|
|
2146
|
+
|
|
2147
|
+
${c.bright("Examples:")}
|
|
2148
|
+
artyfax image p8KV4dvrgJTA ./screenshot.png
|
|
2149
|
+
artyfax image inbox/my-doc ./diagram.png --alt "Architecture"`,
|
|
2150
|
+
delete: `${c.amber("artyfax delete")} \u2014 delete a document
|
|
2151
|
+
|
|
2152
|
+
${c.bright("Usage:")} artyfax delete <slug> [--yes] [--json]
|
|
1066
2153
|
|
|
1067
2154
|
Prompts for confirmation unless --yes is passed.
|
|
1068
2155
|
|
|
1069
2156
|
${c.bright("Examples:")}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
open: `${c.amber("
|
|
2157
|
+
artyfax delete inbox/old-doc
|
|
2158
|
+
artyfax delete inbox/old-doc --yes # skip confirmation`,
|
|
2159
|
+
open: `${c.amber("artyfax open")} \u2014 open in browser
|
|
1073
2160
|
|
|
1074
|
-
${c.bright("Usage:")}
|
|
2161
|
+
${c.bright("Usage:")} artyfax open <slug>
|
|
1075
2162
|
|
|
1076
2163
|
Opens the document URL in your default browser.`,
|
|
1077
|
-
share: `${c.amber("
|
|
2164
|
+
share: `${c.amber("artyfax share")} \u2014 manage share links
|
|
1078
2165
|
|
|
1079
|
-
${c.bright("Usage:")}
|
|
1080
|
-
|
|
1081
|
-
|
|
2166
|
+
${c.bright("Usage:")} artyfax share create <slug> Create a share link
|
|
2167
|
+
artyfax share list [slug] List all shares (or per-document)
|
|
2168
|
+
artyfax share revoke <hash> Revoke a share link
|
|
1082
2169
|
|
|
1083
2170
|
${c.bright("Examples:")}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
version: `${c.amber("
|
|
2171
|
+
artyfax share create inbox/my-doc
|
|
2172
|
+
artyfax share list --json
|
|
2173
|
+
artyfax share revoke SnP5UJUhP6 --yes`,
|
|
2174
|
+
version: `${c.amber("artyfax version")} \u2014 version history
|
|
1088
2175
|
|
|
1089
|
-
${c.bright("Usage:")}
|
|
1090
|
-
|
|
2176
|
+
${c.bright("Usage:")} artyfax version list <slug> Show version history
|
|
2177
|
+
artyfax version restore <slug> Restore a previous version
|
|
1091
2178
|
|
|
1092
2179
|
${c.bright("Examples:")}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
2180
|
+
artyfax version list inbox/my-doc
|
|
2181
|
+
artyfax version restore inbox/my-doc --yes`,
|
|
2182
|
+
annotation: `${c.amber("artyfax annotation")} \u2014 read a document's annotations (highlights, underlines, strikethroughs, notes, comments, redactions)
|
|
1096
2183
|
|
|
1097
|
-
${c.bright("Usage:")}
|
|
1098
|
-
|
|
1099
|
-
arty note search <query> Search annotations
|
|
2184
|
+
${c.bright("Usage:")} artyfax annotation list <slug> List annotations (--include-recipients, --since <iso>)
|
|
2185
|
+
artyfax annotation search <query> Search annotations
|
|
1100
2186
|
|
|
1101
2187
|
${c.bright("Examples:")}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
doctor: `${c.amber("
|
|
2188
|
+
artyfax annotation list inbox/my-doc
|
|
2189
|
+
artyfax annotation list inbox/my-doc --include-recipients
|
|
2190
|
+
artyfax annotation list inbox/my-doc --since 2026-05-01T00:00:00Z
|
|
2191
|
+
artyfax annotation search "review"`,
|
|
2192
|
+
doctor: `${c.amber("artyfax doctor")} \u2014 verify CLI setup
|
|
1107
2193
|
|
|
1108
2194
|
Checks API key, endpoint, connectivity, E2EE status, and skill installation.
|
|
1109
2195
|
|
|
1110
|
-
${c.bright("Usage:")}
|
|
1111
|
-
skill: `${c.amber("
|
|
2196
|
+
${c.bright("Usage:")} artyfax doctor [--json]`,
|
|
2197
|
+
skill: `${c.amber("artyfax skill")} \u2014 Claude Code skill management
|
|
1112
2198
|
|
|
1113
|
-
${c.bright("Usage:")}
|
|
1114
|
-
|
|
1115
|
-
|
|
2199
|
+
${c.bright("Usage:")} artyfax skill install [--project] Install Artyfax skill
|
|
2200
|
+
artyfax skill status Check installation
|
|
2201
|
+
artyfax skill update Update to latest version`
|
|
1116
2202
|
};
|
|
1117
2203
|
function parseArgs(args) {
|
|
1118
2204
|
const flags = {};
|
|
@@ -1122,13 +2208,22 @@ function parseArgs(args) {
|
|
|
1122
2208
|
for (let i = 0; i < args.length; i++) {
|
|
1123
2209
|
const arg = args[i];
|
|
1124
2210
|
if (arg.startsWith("--")) {
|
|
1125
|
-
const
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
2211
|
+
const body = arg.slice(2);
|
|
2212
|
+
const eq = body.indexOf("=");
|
|
2213
|
+
const key = eq === -1 ? body : body.slice(0, eq);
|
|
2214
|
+
if (key === "") {
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
if (eq !== -1) {
|
|
2218
|
+
flags[key] = body.slice(eq + 1);
|
|
1130
2219
|
} else {
|
|
1131
|
-
|
|
2220
|
+
const next = args[i + 1];
|
|
2221
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
2222
|
+
flags[key] = next;
|
|
2223
|
+
i++;
|
|
2224
|
+
} else {
|
|
2225
|
+
flags[key] = true;
|
|
2226
|
+
}
|
|
1132
2227
|
}
|
|
1133
2228
|
} else if (arg.startsWith("-")) {
|
|
1134
2229
|
flags[arg.slice(1)] = true;
|
|
@@ -1142,7 +2237,7 @@ function parseArgs(args) {
|
|
|
1142
2237
|
}
|
|
1143
2238
|
return { command, subcommand, positional, flags };
|
|
1144
2239
|
}
|
|
1145
|
-
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "
|
|
2240
|
+
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "annotation", "skill", "snippets", "work", "project", "workspace"]);
|
|
1146
2241
|
function isSubResource(cmd) {
|
|
1147
2242
|
return SUB_RESOURCES.has(cmd);
|
|
1148
2243
|
}
|
|
@@ -1168,6 +2263,7 @@ async function main() {
|
|
|
1168
2263
|
process.exit(0);
|
|
1169
2264
|
}
|
|
1170
2265
|
const KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2266
|
+
"init",
|
|
1171
2267
|
"save",
|
|
1172
2268
|
"read",
|
|
1173
2269
|
"list",
|
|
@@ -1179,16 +2275,24 @@ async function main() {
|
|
|
1179
2275
|
"unsecure",
|
|
1180
2276
|
"update",
|
|
1181
2277
|
"metadata",
|
|
2278
|
+
"cover",
|
|
2279
|
+
"image",
|
|
1182
2280
|
"share",
|
|
1183
2281
|
"cat",
|
|
1184
2282
|
"tag",
|
|
1185
2283
|
"version",
|
|
1186
|
-
"
|
|
2284
|
+
"annotation",
|
|
1187
2285
|
"skill",
|
|
1188
|
-
"
|
|
2286
|
+
"snippets",
|
|
2287
|
+
"doctor",
|
|
2288
|
+
"work",
|
|
2289
|
+
"project",
|
|
2290
|
+
"workspace",
|
|
2291
|
+
"task",
|
|
2292
|
+
"tasks"
|
|
1189
2293
|
]);
|
|
1190
2294
|
if (!KNOWN_COMMANDS.has(command)) {
|
|
1191
|
-
error(`Unknown command: ${command}`, `Run \`
|
|
2295
|
+
error(`Unknown command: ${command}`, `Run \`artyfax--help\` for available commands`);
|
|
1192
2296
|
}
|
|
1193
2297
|
if (command === "skill") {
|
|
1194
2298
|
switch (subcommand) {
|
|
@@ -1202,7 +2306,7 @@ async function main() {
|
|
|
1202
2306
|
await skillUpdate();
|
|
1203
2307
|
break;
|
|
1204
2308
|
default:
|
|
1205
|
-
error(`Unknown skill subcommand: ${subcommand || "(none)"}`, "Usage:
|
|
2309
|
+
error(`Unknown skill subcommand: ${subcommand || "(none)"}`, "Usage: artyfax skill <install|status|update>");
|
|
1206
2310
|
}
|
|
1207
2311
|
process.exit(0);
|
|
1208
2312
|
}
|
|
@@ -1212,11 +2316,15 @@ async function main() {
|
|
|
1212
2316
|
process.exit(0);
|
|
1213
2317
|
}
|
|
1214
2318
|
switch (command) {
|
|
2319
|
+
case "init": {
|
|
2320
|
+
await init(config, positional[0]);
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
1215
2323
|
case "save": {
|
|
1216
2324
|
const fileOrUrl = positional[0];
|
|
1217
2325
|
const url = flags.url;
|
|
1218
2326
|
if (!fileOrUrl && !url) {
|
|
1219
|
-
error("Missing file argument", "Usage:
|
|
2327
|
+
error("Missing file argument", "Usage: artyfax save <file> [--secure] [--category <name>]");
|
|
1220
2328
|
}
|
|
1221
2329
|
if (url) {
|
|
1222
2330
|
await save(config, {
|
|
@@ -1230,6 +2338,7 @@ async function main() {
|
|
|
1230
2338
|
docType: flags["doc-type"] || "article",
|
|
1231
2339
|
theme: flags.theme,
|
|
1232
2340
|
parentId: flags.parent,
|
|
2341
|
+
force: !!flags.force,
|
|
1233
2342
|
json
|
|
1234
2343
|
});
|
|
1235
2344
|
break;
|
|
@@ -1240,7 +2349,7 @@ async function main() {
|
|
|
1240
2349
|
}
|
|
1241
2350
|
let content;
|
|
1242
2351
|
try {
|
|
1243
|
-
content =
|
|
2352
|
+
content = readFileSync5(resolve5(fileOrUrl), "utf-8");
|
|
1244
2353
|
} catch {
|
|
1245
2354
|
error(`File not found: ${fileOrUrl}`);
|
|
1246
2355
|
}
|
|
@@ -1255,13 +2364,14 @@ async function main() {
|
|
|
1255
2364
|
docType: flags["doc-type"] || "original",
|
|
1256
2365
|
theme: flags.theme,
|
|
1257
2366
|
parentId: flags.parent,
|
|
2367
|
+
force: !!flags.force,
|
|
1258
2368
|
json
|
|
1259
2369
|
});
|
|
1260
2370
|
break;
|
|
1261
2371
|
}
|
|
1262
2372
|
case "read": {
|
|
1263
2373
|
const slug = positional[0];
|
|
1264
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2374
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax read <slug>");
|
|
1265
2375
|
await read(config, slug, !!flags.secure);
|
|
1266
2376
|
break;
|
|
1267
2377
|
}
|
|
@@ -1282,57 +2392,60 @@ async function main() {
|
|
|
1282
2392
|
offset,
|
|
1283
2393
|
archived: !!flags.archived,
|
|
1284
2394
|
parentId: flags["parent-id"],
|
|
2395
|
+
tag: flags.tag,
|
|
2396
|
+
workspace: flags.workspace,
|
|
1285
2397
|
json
|
|
1286
2398
|
});
|
|
1287
2399
|
break;
|
|
1288
2400
|
}
|
|
1289
2401
|
case "get": {
|
|
1290
2402
|
const slug = positional[0];
|
|
1291
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2403
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax get <slug>");
|
|
1292
2404
|
await get(config, slug);
|
|
1293
2405
|
break;
|
|
1294
2406
|
}
|
|
1295
2407
|
case "search": {
|
|
1296
2408
|
const query = positional.join(" ");
|
|
1297
|
-
if (!query) error("Missing search query", "Usage:
|
|
1298
|
-
await search(config, query, json);
|
|
2409
|
+
if (!query) error("Missing search query", "Usage: artyfax search <query>");
|
|
2410
|
+
await search(config, query, json, flags.workspace);
|
|
1299
2411
|
break;
|
|
1300
2412
|
}
|
|
1301
2413
|
case "delete": {
|
|
1302
2414
|
const slug = positional[0];
|
|
1303
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2415
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax delete <slug>");
|
|
1304
2416
|
await del(config, slug, yes, json);
|
|
1305
2417
|
break;
|
|
1306
2418
|
}
|
|
1307
2419
|
case "open": {
|
|
1308
2420
|
const slug = positional[0];
|
|
1309
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2421
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax open <slug>");
|
|
1310
2422
|
await open(config, slug);
|
|
1311
2423
|
break;
|
|
1312
2424
|
}
|
|
1313
2425
|
case "secure": {
|
|
1314
2426
|
const slug = positional[0];
|
|
1315
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2427
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax secure <slug>");
|
|
1316
2428
|
await secure(config, slug);
|
|
1317
2429
|
break;
|
|
1318
2430
|
}
|
|
1319
2431
|
case "unsecure": {
|
|
1320
2432
|
const slug = positional[0];
|
|
1321
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2433
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax unsecure <slug>");
|
|
1322
2434
|
await unsecure(config, slug);
|
|
1323
2435
|
break;
|
|
1324
2436
|
}
|
|
1325
2437
|
case "update": {
|
|
1326
2438
|
const slug = positional[0];
|
|
1327
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2439
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax update <slug> [file] (reads from stdin if no file)");
|
|
1328
2440
|
await update(config, slug, positional[1], json);
|
|
1329
2441
|
break;
|
|
1330
2442
|
}
|
|
1331
2443
|
case "metadata": {
|
|
1332
2444
|
const slug = positional[0];
|
|
1333
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2445
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax metadata <slug> --category <name> --tags <t1,t2>");
|
|
1334
2446
|
await metadata(config, slug, {
|
|
1335
2447
|
category: flags.category,
|
|
2448
|
+
slug: flags.slug,
|
|
1336
2449
|
tags: flags.tags,
|
|
1337
2450
|
title: flags.title,
|
|
1338
2451
|
theme: flags.theme,
|
|
@@ -1342,11 +2455,25 @@ async function main() {
|
|
|
1342
2455
|
});
|
|
1343
2456
|
break;
|
|
1344
2457
|
}
|
|
2458
|
+
case "cover": {
|
|
2459
|
+
const slug = positional[0];
|
|
2460
|
+
const file = positional[1];
|
|
2461
|
+
if (!slug || !file) error("Missing arguments", 'Usage: artyfax cover <doc> <image-file> [--alt "..."]');
|
|
2462
|
+
await setCover(config, slug, file, { alt: flags.alt, json });
|
|
2463
|
+
break;
|
|
2464
|
+
}
|
|
2465
|
+
case "image": {
|
|
2466
|
+
const slug = positional[0];
|
|
2467
|
+
const file = positional[1];
|
|
2468
|
+
if (!slug || !file) error("Missing arguments", 'Usage: artyfax image <doc> <image-file> [--alt "..."]');
|
|
2469
|
+
await addImage(config, slug, file, { alt: flags.alt, json });
|
|
2470
|
+
break;
|
|
2471
|
+
}
|
|
1345
2472
|
case "share": {
|
|
1346
2473
|
switch (subcommand) {
|
|
1347
2474
|
case "create": {
|
|
1348
2475
|
const slug = positional[0];
|
|
1349
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2476
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax share create <slug>");
|
|
1350
2477
|
await shareCreate(config, slug, json);
|
|
1351
2478
|
break;
|
|
1352
2479
|
}
|
|
@@ -1355,77 +2482,254 @@ async function main() {
|
|
|
1355
2482
|
break;
|
|
1356
2483
|
case "revoke": {
|
|
1357
2484
|
const hash = positional[0];
|
|
1358
|
-
if (!hash) error("Missing share hash", "Usage:
|
|
2485
|
+
if (!hash) error("Missing share hash", "Usage: artyfax share revoke <hash>");
|
|
1359
2486
|
await shareRevoke(config, hash, yes, json);
|
|
1360
2487
|
break;
|
|
1361
2488
|
}
|
|
1362
2489
|
default:
|
|
1363
|
-
error(`Unknown share subcommand: ${subcommand || "(none)"}`, "Usage:
|
|
2490
|
+
error(`Unknown share subcommand: ${subcommand || "(none)"}`, "Usage: artyfax share <create|list|revoke>");
|
|
1364
2491
|
}
|
|
1365
2492
|
break;
|
|
1366
2493
|
}
|
|
1367
2494
|
case "cat": {
|
|
1368
|
-
if (subcommand !== "list") error(`Unknown cat subcommand: ${subcommand || "(none)"}`, "Usage:
|
|
2495
|
+
if (subcommand !== "list") error(`Unknown cat subcommand: ${subcommand || "(none)"}`, "Usage: artyfax cat list");
|
|
1369
2496
|
await catList(config, json);
|
|
1370
2497
|
break;
|
|
1371
2498
|
}
|
|
1372
2499
|
case "tag": {
|
|
1373
|
-
|
|
1374
|
-
|
|
2500
|
+
switch (subcommand) {
|
|
2501
|
+
case "list":
|
|
2502
|
+
await tagList(config, json);
|
|
2503
|
+
break;
|
|
2504
|
+
case "delete": {
|
|
2505
|
+
const tag = positional[0];
|
|
2506
|
+
if (!tag) error("Missing tag argument", "Usage: artyfax tag delete <tag>");
|
|
2507
|
+
await tagDelete(config, tag, json);
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
default:
|
|
2511
|
+
error(`Unknown tag subcommand: ${subcommand || "(none)"}`, "Usage: artyfax tag list | artyfax tag delete <tag>");
|
|
2512
|
+
}
|
|
1375
2513
|
break;
|
|
1376
2514
|
}
|
|
1377
2515
|
case "version": {
|
|
1378
2516
|
switch (subcommand) {
|
|
1379
2517
|
case "list": {
|
|
1380
2518
|
const slug = positional[0];
|
|
1381
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2519
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax version list <slug>");
|
|
1382
2520
|
await versionList(config, slug, json);
|
|
1383
2521
|
break;
|
|
1384
2522
|
}
|
|
1385
2523
|
case "restore": {
|
|
1386
2524
|
const slug = positional[0];
|
|
1387
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
2525
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax version restore <slug>");
|
|
1388
2526
|
await versionRestore(config, slug, yes, json);
|
|
1389
2527
|
break;
|
|
1390
2528
|
}
|
|
1391
2529
|
default:
|
|
1392
|
-
error(`Unknown version subcommand: ${subcommand || "(none)"}`, "Usage:
|
|
2530
|
+
error(`Unknown version subcommand: ${subcommand || "(none)"}`, "Usage: artyfax version <list|restore>");
|
|
1393
2531
|
}
|
|
1394
2532
|
break;
|
|
1395
2533
|
}
|
|
1396
|
-
case "
|
|
2534
|
+
case "annotation": {
|
|
1397
2535
|
switch (subcommand) {
|
|
1398
2536
|
case "list": {
|
|
1399
2537
|
const slug = positional[0];
|
|
1400
|
-
if (!slug) error("Missing slug argument", "Usage:
|
|
1401
|
-
await
|
|
2538
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax annotation list <slug> [--include-recipients] [--since <iso>]");
|
|
2539
|
+
await annotationList(config, slug, json, {
|
|
2540
|
+
includeRecipients: Boolean(flags["include-recipients"]),
|
|
2541
|
+
since: typeof flags.since === "string" ? flags.since : void 0
|
|
2542
|
+
});
|
|
1402
2543
|
break;
|
|
1403
2544
|
}
|
|
1404
|
-
case "
|
|
1405
|
-
const
|
|
1406
|
-
if (!
|
|
1407
|
-
await
|
|
2545
|
+
case "search": {
|
|
2546
|
+
const query = positional.join(" ");
|
|
2547
|
+
if (!query) error("Missing search query", "Usage: artyfax annotation search <query>");
|
|
2548
|
+
await annotationSearch(config, query, json);
|
|
2549
|
+
break;
|
|
2550
|
+
}
|
|
2551
|
+
default:
|
|
2552
|
+
error(`Unknown annotation subcommand: ${subcommand || "(none)"}`, "Usage: artyfax annotation <list|search>");
|
|
2553
|
+
}
|
|
2554
|
+
break;
|
|
2555
|
+
}
|
|
2556
|
+
case "snippets": {
|
|
2557
|
+
const tagsCsv = flags.tags;
|
|
2558
|
+
const parsedTags = tagsCsv ? tagsCsv.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
|
|
2559
|
+
switch (subcommand) {
|
|
2560
|
+
case "list":
|
|
2561
|
+
case void 0: {
|
|
2562
|
+
await snippetList(config, {
|
|
2563
|
+
type: flags.type,
|
|
2564
|
+
starred: !!flags.starred,
|
|
2565
|
+
tag: flags.tag,
|
|
2566
|
+
limit: flags.limit ? Number(flags.limit) : void 0
|
|
2567
|
+
}, json);
|
|
1408
2568
|
break;
|
|
1409
2569
|
}
|
|
1410
2570
|
case "search": {
|
|
1411
2571
|
const query = positional.join(" ");
|
|
1412
|
-
if (!query) error("Missing search query", "Usage:
|
|
1413
|
-
await
|
|
2572
|
+
if (!query) error("Missing search query", "Usage: artyfax snippets search <query>");
|
|
2573
|
+
await snippetSearch(config, query, {
|
|
2574
|
+
type: flags.type,
|
|
2575
|
+
limit: flags.limit ? Number(flags.limit) : void 0
|
|
2576
|
+
}, json);
|
|
2577
|
+
break;
|
|
2578
|
+
}
|
|
2579
|
+
case "show": {
|
|
2580
|
+
const id = positional[0];
|
|
2581
|
+
if (!id) error("Missing snippet id", "Usage: artyfax snippets show <id>");
|
|
2582
|
+
await snippetShow(config, id, json);
|
|
2583
|
+
break;
|
|
2584
|
+
}
|
|
2585
|
+
case "new": {
|
|
2586
|
+
await snippetNew(config, {
|
|
2587
|
+
image: flags.image,
|
|
2588
|
+
type: flags.type ?? void 0,
|
|
2589
|
+
text: flags.text,
|
|
2590
|
+
file: flags.file,
|
|
2591
|
+
language: flags.language,
|
|
2592
|
+
note: flags.note,
|
|
2593
|
+
sourceUrl: flags["source-url"],
|
|
2594
|
+
sourceTitle: flags["source-title"],
|
|
2595
|
+
sourceSiteName: flags["source-site-name"],
|
|
2596
|
+
sourceAuthor: flags["source-author"],
|
|
2597
|
+
alt: flags.alt,
|
|
2598
|
+
tags: parsedTags,
|
|
2599
|
+
starred: !!flags.starred
|
|
2600
|
+
}, json);
|
|
2601
|
+
break;
|
|
2602
|
+
}
|
|
2603
|
+
case "delete": {
|
|
2604
|
+
const id = positional[0];
|
|
2605
|
+
if (!id) error("Missing snippet id", "Usage: artyfax snippets delete <id>");
|
|
2606
|
+
await snippetDelete(config, id, json);
|
|
2607
|
+
break;
|
|
2608
|
+
}
|
|
2609
|
+
case "star": {
|
|
2610
|
+
const id = positional[0];
|
|
2611
|
+
if (!id) error("Missing snippet id", "Usage: artyfax snippets star <id>");
|
|
2612
|
+
await snippetStar(config, id, true, json);
|
|
2613
|
+
break;
|
|
2614
|
+
}
|
|
2615
|
+
case "unstar": {
|
|
2616
|
+
const id = positional[0];
|
|
2617
|
+
if (!id) error("Missing snippet id", "Usage: artyfax snippets unstar <id>");
|
|
2618
|
+
await snippetStar(config, id, false, json);
|
|
1414
2619
|
break;
|
|
1415
2620
|
}
|
|
1416
2621
|
default:
|
|
1417
|
-
error(`Unknown
|
|
2622
|
+
error(`Unknown snippets subcommand: ${subcommand}`, "Usage: artyfax snippets <list|search|show|new|delete|star|unstar>");
|
|
2623
|
+
}
|
|
2624
|
+
break;
|
|
2625
|
+
}
|
|
2626
|
+
case "task":
|
|
2627
|
+
case "tasks": {
|
|
2628
|
+
const id = positional[0];
|
|
2629
|
+
if (!id) error("Missing document or work ID", 'Usage: artyfax tasks <id>\n artyfax task <id> "task text" <active|done|pending>');
|
|
2630
|
+
if (positional.length <= 1) {
|
|
2631
|
+
await taskList(config, id, json);
|
|
2632
|
+
} else {
|
|
2633
|
+
const state = positional[positional.length - 1];
|
|
2634
|
+
const taskText = positional.slice(1, -1).join(" ");
|
|
2635
|
+
if (!taskText) error("Missing task text", 'Usage: artyfax task <id> "task text" <active|done|pending>');
|
|
2636
|
+
await taskUpdate(config, id, taskText, state, json);
|
|
2637
|
+
}
|
|
2638
|
+
break;
|
|
2639
|
+
}
|
|
2640
|
+
case "work": {
|
|
2641
|
+
switch (subcommand) {
|
|
2642
|
+
case "":
|
|
2643
|
+
await workBoard(config, json);
|
|
2644
|
+
break;
|
|
2645
|
+
case "list":
|
|
2646
|
+
await workList(config, json, flags);
|
|
2647
|
+
break;
|
|
2648
|
+
case "next":
|
|
2649
|
+
await workNext(config, json, flags.agent);
|
|
2650
|
+
break;
|
|
2651
|
+
case "start": {
|
|
2652
|
+
const id = positional[0];
|
|
2653
|
+
if (!id) error("Missing work item ID", "Usage: artyfax work start <id>");
|
|
2654
|
+
await workSetStatus(config, id, "in_progress", json);
|
|
2655
|
+
break;
|
|
2656
|
+
}
|
|
2657
|
+
case "done": {
|
|
2658
|
+
const id = positional[0];
|
|
2659
|
+
if (!id) error("Missing work item ID", "Usage: artyfax work done <id>");
|
|
2660
|
+
await workSetStatus(config, id, "done", json);
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2663
|
+
case "status": {
|
|
2664
|
+
const id = positional[0];
|
|
2665
|
+
const status = positional[1];
|
|
2666
|
+
if (!id || !status) error("Missing arguments", "Usage: artyfax work status <id> <status>");
|
|
2667
|
+
await workSetStatus(config, id, status, json);
|
|
2668
|
+
break;
|
|
2669
|
+
}
|
|
2670
|
+
case "create": {
|
|
2671
|
+
const title = positional.join(" ");
|
|
2672
|
+
if (!title) error("Missing title", 'Usage: artyfax work create "Title"');
|
|
2673
|
+
await workCreate(config, title, flags, json);
|
|
2674
|
+
break;
|
|
2675
|
+
}
|
|
2676
|
+
default:
|
|
2677
|
+
error(`Unknown work subcommand: ${subcommand}`, "Usage: artyfax work <list|next|start|done|status|create>");
|
|
2678
|
+
}
|
|
2679
|
+
break;
|
|
2680
|
+
}
|
|
2681
|
+
case "project": {
|
|
2682
|
+
switch (subcommand) {
|
|
2683
|
+
case "list":
|
|
2684
|
+
case "":
|
|
2685
|
+
await projectList(config, json);
|
|
2686
|
+
break;
|
|
2687
|
+
case "create": {
|
|
2688
|
+
const name = positional.join(" ");
|
|
2689
|
+
if (!name) error("Missing project name", 'Usage: artyfax project create "Name" --prefix XX');
|
|
2690
|
+
const prefix = flags.prefix;
|
|
2691
|
+
if (!prefix) error("Missing --prefix", 'Usage: artyfax project create "Name" --prefix XX');
|
|
2692
|
+
const wsFlag = flags.workspaces ?? flags.workspace;
|
|
2693
|
+
const workspaces = wsFlag ? wsFlag.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
2694
|
+
await projectCreate(config, name, prefix, flags.description, json, workspaces);
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
default:
|
|
2698
|
+
error(`Unknown project subcommand: ${subcommand}`, "Usage: artyfax project <list|create>");
|
|
2699
|
+
}
|
|
2700
|
+
break;
|
|
2701
|
+
}
|
|
2702
|
+
case "workspace": {
|
|
2703
|
+
switch (subcommand) {
|
|
2704
|
+
case "list":
|
|
2705
|
+
case "":
|
|
2706
|
+
await workspaceList(config, json);
|
|
2707
|
+
break;
|
|
2708
|
+
default:
|
|
2709
|
+
error(`Unknown workspace subcommand: ${subcommand}`, "Usage: artyfax workspace <list>");
|
|
1418
2710
|
}
|
|
1419
2711
|
break;
|
|
1420
2712
|
}
|
|
1421
2713
|
default:
|
|
1422
|
-
error(`Unknown command: ${command}`, `Run \`
|
|
2714
|
+
error(`Unknown command: ${command}`, `Run \`artyfax--help\` for available commands`);
|
|
1423
2715
|
}
|
|
1424
2716
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2717
|
+
function isDirectRun() {
|
|
2718
|
+
const entry = process.argv[1];
|
|
2719
|
+
if (!entry) return false;
|
|
2720
|
+
const self = fileURLToPath(import.meta.url);
|
|
2721
|
+
try {
|
|
2722
|
+
return realpathSync(entry) === realpathSync(self);
|
|
2723
|
+
} catch {
|
|
2724
|
+
return resolve5(entry) === self;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
if (isDirectRun()) {
|
|
2728
|
+
main().catch((e) => {
|
|
2729
|
+
console.error(c.rose(e.message || String(e)));
|
|
2730
|
+
process.exit(1);
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
1429
2733
|
export {
|
|
1430
2734
|
VERSION,
|
|
1431
2735
|
parseArgs
|