artifact-cafe 0.0.7 → 0.0.8

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.
Files changed (2) hide show
  1. package/dist/index.js +378 -13
  2. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -112,8 +112,9 @@ var anchorSchema = z2.discriminatedUnion("type", [
112
112
  pointAnchorSchema,
113
113
  textAnchorSchema
114
114
  ]);
115
+ var displayNameSchema = z2.string().trim().min(1).max(64);
115
116
  var guestSchema = z2.object({
116
- displayName: z2.string().min(1).max(64),
117
+ displayName: displayNameSchema,
117
118
  guestId: z2.string().min(16)
118
119
  });
119
120
  var createCommentThreadSchema = z2.object({
@@ -207,8 +208,10 @@ var ownArtifactSchema = z4.object({
207
208
  // Owned artifacts normally never expire; non-null only while an
208
209
  // expiry is still set (e.g. an unclaimed publish surfaced here later).
209
210
  expiresAt: z4.string().nullable(),
210
- // SPEC-2 §8.4 — the containing folder's name for workspace-owned
211
- // artifacts; always null for personal artifacts.
211
+ // SPEC-2 §8.4 — the containing folder for workspace-owned artifacts;
212
+ // both null for personal artifacts. `folderId` drives the dashboard's
213
+ // move control and folder filter; `folderName` is the display label.
214
+ folderId: z4.string().nullable(),
212
215
  folderName: z4.string().nullable(),
213
216
  // Whether a publish token is still active — revocable from the
214
217
  // dashboard (§6.18); the token itself is never returned.
@@ -655,7 +658,7 @@ function createApiClient(apiUrl2, publishToken) {
655
658
  });
656
659
  return request(
657
660
  `${base}/api/v1/artifacts/${artifactId}/comments/export?${params}`,
658
- { method: "GET" },
661
+ { headers, method: "GET" },
659
662
  (d) => commentExportSchema.parse(d)
660
663
  );
661
664
  },
@@ -669,7 +672,7 @@ function createApiClient(apiUrl2, publishToken) {
669
672
  async getArtifact(artifactId) {
670
673
  return request(
671
674
  `${base}/api/v1/artifacts/${artifactId}`,
672
- { method: "GET" },
675
+ { headers, method: "GET" },
673
676
  (d) => artifactResponseSchema.parse(d)
674
677
  );
675
678
  },
@@ -705,6 +708,34 @@ function createApiClient(apiUrl2, publishToken) {
705
708
  (d) => meResponseSchema.parse(d)
706
709
  );
707
710
  },
711
+ // SPEC §6.7 — reopen a resolved thread. Same auth as resolve.
712
+ async reopenThread(threadId) {
713
+ return request(
714
+ `${base}/api/v1/comment-threads/${threadId}/reopen`,
715
+ { body: "{}", headers, method: "POST" },
716
+ (d) => commentThreadSchema.parse(d)
717
+ );
718
+ },
719
+ // SPEC §6.5 — post a reply into a thread. Authorized by this client's
720
+ // credential: the artifact's publish token (the reply renders as the
721
+ // agent) or the owner's account key (renders as the owner). No guest
722
+ // payload — the credential is the identity.
723
+ async replyThread(threadId, body) {
724
+ return request(
725
+ `${base}/api/v1/comment-threads/${threadId}/messages`,
726
+ { body: JSON.stringify({ body }), headers, method: "POST" },
727
+ (d) => commentThreadSchema.parse(d)
728
+ );
729
+ },
730
+ // SPEC §6.6 — mark a thread resolved. Authorized by this client's
731
+ // credential: the artifact's publish token, or the owner's account key.
732
+ async resolveThread(threadId) {
733
+ return request(
734
+ `${base}/api/v1/comment-threads/${threadId}/resolve`,
735
+ { body: "{}", headers, method: "POST" },
736
+ (d) => commentThreadSchema.parse(d)
737
+ );
738
+ },
708
739
  // SPEC §6.16 — set a password (string) or clear it (null). Authorized
709
740
  // by the publish token this client was created with.
710
741
  async setProtection(artifactId, password) {
@@ -1067,8 +1098,8 @@ function printThread(t) {
1067
1098
  console.log(` \u21B3 at ${t.anchor.elementPath}`);
1068
1099
  }
1069
1100
  console.log(indent(t.body, " "));
1070
- for (const reply of t.replies) {
1071
- console.log(indent(`${reply.author}: ${reply.body}`, " "));
1101
+ for (const reply2 of t.replies) {
1102
+ console.log(indent(`${reply2.author}: ${reply2.body}`, " "));
1072
1103
  }
1073
1104
  console.log("");
1074
1105
  }
@@ -1486,6 +1517,11 @@ function printCliError(err, json) {
1486
1517
  if (err.retryAfter) {
1487
1518
  console.error(` Retry in ${err.retryAfter}s.`);
1488
1519
  }
1520
+ if (err.code === "password_required") {
1521
+ console.error(
1522
+ " Run `artifact-cafe login` as the owner, or run from the published folder (its publish token unlocks it)."
1523
+ );
1524
+ }
1489
1525
  }
1490
1526
  return;
1491
1527
  }
@@ -1625,7 +1661,7 @@ function pickArtifact(artifacts) {
1625
1661
  );
1626
1662
  });
1627
1663
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1628
- return new Promise((resolve, reject) => {
1664
+ return new Promise((resolve2, reject) => {
1629
1665
  rl.question(`
1630
1666
  Link which? [1-${artifacts.length}]: `, (answer) => {
1631
1667
  rl.close();
@@ -1634,7 +1670,7 @@ Link which? [1-${artifacts.length}]: `, (answer) => {
1634
1670
  reject(new Error(`Enter a number between 1 and ${artifacts.length}.`));
1635
1671
  return;
1636
1672
  }
1637
- resolve(artifacts[n - 1]);
1673
+ resolve2(artifacts[n - 1]);
1638
1674
  });
1639
1675
  });
1640
1676
  }
@@ -1701,7 +1737,7 @@ function apiUrl() {
1701
1737
  return (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
1702
1738
  }
1703
1739
  function promptSecret(question) {
1704
- return new Promise((resolve) => {
1740
+ return new Promise((resolve2) => {
1705
1741
  const rl = createInterface2({
1706
1742
  input: process.stdin,
1707
1743
  output: process.stdout
@@ -1716,7 +1752,7 @@ function promptSecret(question) {
1716
1752
  rl.question(question, (answer) => {
1717
1753
  rl.close();
1718
1754
  process.stdout.write("\n");
1719
- resolve(answer);
1755
+ resolve2(answer);
1720
1756
  });
1721
1757
  muted = true;
1722
1758
  });
@@ -1871,6 +1907,234 @@ async function protect(opts) {
1871
1907
  );
1872
1908
  }
1873
1909
 
1910
+ // src/reply.ts
1911
+ import path10 from "path";
1912
+ async function readStdin() {
1913
+ if (process.stdin.isTTY) {
1914
+ return "";
1915
+ }
1916
+ const chunks = [];
1917
+ for await (const chunk of process.stdin) {
1918
+ chunks.push(chunk);
1919
+ }
1920
+ return Buffer.concat(chunks).toString("utf8");
1921
+ }
1922
+ async function replyToThread(opts) {
1923
+ const threadId = opts.thread?.trim();
1924
+ if (!threadId) {
1925
+ throw new Error(
1926
+ "Pass the thread id to reply to \u2014 get it from `artifact-cafe comments --json`."
1927
+ );
1928
+ }
1929
+ const body = (opts.body ?? await readStdin()).trim();
1930
+ if (!body) {
1931
+ throw new Error(
1932
+ 'Nothing to post. Pass the reply with --body "\u2026", or pipe it on stdin.'
1933
+ );
1934
+ }
1935
+ const dir = path10.resolve(opts.dir);
1936
+ const target = resolveArtifactTarget(dir, {
1937
+ apiKey: opts.apiKey,
1938
+ artifact: opts.artifact
1939
+ });
1940
+ const client = createApiClient(target.apiUrl, target.credential);
1941
+ let thread = await client.replyThread(threadId, body);
1942
+ if (opts.resolve) {
1943
+ thread = await client.resolveThread(threadId);
1944
+ }
1945
+ if (opts.json) {
1946
+ console.log(
1947
+ JSON.stringify(
1948
+ {
1949
+ id: thread.id,
1950
+ messageCount: thread.messages.length,
1951
+ status: thread.status
1952
+ },
1953
+ null,
1954
+ 2
1955
+ )
1956
+ );
1957
+ return;
1958
+ }
1959
+ const closed = opts.resolve ? " and resolved it" : "";
1960
+ console.log(`\u2713 Replied to ${thread.id}${closed} \u2014 now ${thread.status}.`);
1961
+ }
1962
+
1963
+ // src/resolve.ts
1964
+ import path11 from "path";
1965
+ async function changeThreadStatus(opts) {
1966
+ const threadId = opts.thread?.trim();
1967
+ if (!threadId) {
1968
+ throw new Error(
1969
+ `Pass the thread id to ${opts.action} \u2014 get it from \`artifact-cafe comments --json\`.`
1970
+ );
1971
+ }
1972
+ const dir = path11.resolve(opts.dir);
1973
+ const target = resolveArtifactTarget(dir, {
1974
+ apiKey: opts.apiKey,
1975
+ artifact: opts.artifact
1976
+ });
1977
+ const client = createApiClient(target.apiUrl, target.credential);
1978
+ const thread = opts.action === "resolve" ? await client.resolveThread(threadId) : await client.reopenThread(threadId);
1979
+ if (opts.json) {
1980
+ console.log(
1981
+ JSON.stringify({ id: thread.id, status: thread.status }, null, 2)
1982
+ );
1983
+ return;
1984
+ }
1985
+ const verb = opts.action === "resolve" ? "Resolved" : "Reopened";
1986
+ console.log(`\u2713 ${verb} ${thread.id} \u2014 now ${thread.status}.`);
1987
+ }
1988
+
1989
+ // src/update.ts
1990
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1991
+ import { createRequire } from "module";
1992
+ import { homedir as homedir2 } from "os";
1993
+ import path12 from "path";
1994
+ import { z as z15 } from "zod";
1995
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/artifact-cafe/latest";
1996
+ var SKILL_SOURCE_URL = "https://raw.githubusercontent.com/Artifact-Cafe/skill/main/artifact-cafe/SKILL.md";
1997
+ var SKILL_UPDATE_COMMAND = "npx skills add artifact-cafe/skill --skill artifact-cafe -g";
1998
+ var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
1999
+ var FETCH_TIMEOUT_MS = 2e3;
2000
+ var SKILL_VERSION_PATTERN = /\*\*Skill version:\s*(\d+\.\d+\.\d+)\*\*/;
2001
+ var cacheSchema = z15.object({
2002
+ checkedAt: z15.number(),
2003
+ cliLatest: z15.string().nullable(),
2004
+ skillLatest: z15.string().nullable(),
2005
+ version: z15.literal(1)
2006
+ });
2007
+ function cliVersion() {
2008
+ const pkg = createRequire(import.meta.url)("../package.json");
2009
+ return pkg.version;
2010
+ }
2011
+ function cachePath() {
2012
+ return path12.join(homedir2(), ".artifactcafe", "update-check.json");
2013
+ }
2014
+ function readCache() {
2015
+ try {
2016
+ return cacheSchema.parse(JSON.parse(readFileSync5(cachePath(), "utf8")));
2017
+ } catch {
2018
+ return null;
2019
+ }
2020
+ }
2021
+ function writeCache(cache) {
2022
+ try {
2023
+ mkdirSync3(path12.dirname(cachePath()), { mode: 448, recursive: true });
2024
+ writeFileSync3(cachePath(), `${JSON.stringify(cache, null, 2)}
2025
+ `);
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ function isNewerVersion(candidate, current) {
2030
+ const parse = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
2031
+ const a = parse(candidate);
2032
+ const b = parse(current);
2033
+ for (let i = 0; i < 3; i++) {
2034
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) {
2035
+ return (a[i] ?? 0) > (b[i] ?? 0);
2036
+ }
2037
+ }
2038
+ return false;
2039
+ }
2040
+ async function fetchJson(url) {
2041
+ const res = await fetch(url, {
2042
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2043
+ });
2044
+ if (!res.ok) {
2045
+ throw new Error(`GET ${url} \u2192 ${res.status}`);
2046
+ }
2047
+ return res.json();
2048
+ }
2049
+ async function fetchText(url) {
2050
+ const res = await fetch(url, {
2051
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2052
+ });
2053
+ if (!res.ok) {
2054
+ throw new Error(`GET ${url} \u2192 ${res.status}`);
2055
+ }
2056
+ return res.text();
2057
+ }
2058
+ async function fetchLatestVersions(previous) {
2059
+ const [cli, skill] = await Promise.all([
2060
+ fetchJson(REGISTRY_LATEST_URL).then((body) => z15.object({ version: z15.string() }).parse(body).version).catch(() => null),
2061
+ fetchText(SKILL_SOURCE_URL).then((body) => SKILL_VERSION_PATTERN.exec(body)?.[1] ?? null).catch(() => null)
2062
+ ]);
2063
+ return {
2064
+ checkedAt: Date.now(),
2065
+ cliLatest: cli ?? previous?.cliLatest ?? null,
2066
+ skillLatest: skill ?? previous?.skillLatest ?? null,
2067
+ version: 1
2068
+ };
2069
+ }
2070
+ function installedSkillVersions() {
2071
+ const roots = [homedir2(), process.cwd()];
2072
+ const dirs = [".agents", ".claude"];
2073
+ const versions = [];
2074
+ for (const root of roots) {
2075
+ for (const dir of dirs) {
2076
+ const file = path12.join(root, dir, "skills", "artifact-cafe", "SKILL.md");
2077
+ if (!existsSync3(file)) {
2078
+ continue;
2079
+ }
2080
+ try {
2081
+ const found = SKILL_VERSION_PATTERN.exec(readFileSync5(file, "utf8"));
2082
+ if (found?.[1]) {
2083
+ versions.push(found[1]);
2084
+ }
2085
+ } catch {
2086
+ }
2087
+ }
2088
+ }
2089
+ return versions;
2090
+ }
2091
+ function updateChecksDisabled() {
2092
+ return Boolean(
2093
+ process.env.ARTIFACT_CAFE_NO_UPDATE_CHECK ?? process.env.NO_UPDATE_NOTIFIER ?? process.env.CI
2094
+ );
2095
+ }
2096
+ function startUpdateCheck() {
2097
+ if (updateChecksDisabled()) {
2098
+ return () => Promise.resolve();
2099
+ }
2100
+ const pending = (async () => {
2101
+ const cached = readCache();
2102
+ if (cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
2103
+ return cached;
2104
+ }
2105
+ const fresh = await fetchLatestVersions(cached);
2106
+ writeCache(fresh);
2107
+ return fresh;
2108
+ })().catch(() => null);
2109
+ return async () => {
2110
+ const latest = await pending;
2111
+ if (!latest) {
2112
+ return;
2113
+ }
2114
+ const notices = [];
2115
+ const current = cliVersion();
2116
+ if (latest.cliLatest && isNewerVersion(latest.cliLatest, current)) {
2117
+ notices.push(
2118
+ `[artifact-cafe] CLI update available: ${current} \u2192 ${latest.cliLatest}. Run commands via \`npx artifact-cafe@latest\`; update a global install with \`npm i -g artifact-cafe@latest\`.`
2119
+ );
2120
+ }
2121
+ if (latest.skillLatest) {
2122
+ const stale = installedSkillVersions().filter(
2123
+ (version) => isNewerVersion(latest.skillLatest, version)
2124
+ ).sort((a, b) => isNewerVersion(a, b) ? 1 : -1);
2125
+ if (stale.length > 0) {
2126
+ notices.push(
2127
+ `[artifact-cafe] Skill update available: ${stale[0]} \u2192 ${latest.skillLatest}. Update: \`${SKILL_UPDATE_COMMAND}\` (drop -g for a repo-local copy), then re-read SKILL.md.`
2128
+ );
2129
+ }
2130
+ }
2131
+ if (notices.length > 0) {
2132
+ console.error(`
2133
+ ${notices.join("\n")}`);
2134
+ }
2135
+ };
2136
+ }
2137
+
1874
2138
  // src/workspaces.ts
1875
2139
  async function workspaces(opts) {
1876
2140
  const apiUrl2 = (process.env.ARTIFACT_CAFE_URL ?? DEFAULT_API_URL).replace(
@@ -2292,6 +2556,101 @@ var protect2 = defineCommand({
2292
2556
  }
2293
2557
  }
2294
2558
  });
2559
+ var threadStatusCommand = (action) => defineCommand({
2560
+ args: {
2561
+ "api-key": {
2562
+ description: "Authorize with an account API key (default: stored login)",
2563
+ type: "string"
2564
+ },
2565
+ artifact: {
2566
+ description: "Act on an artifact ID instead of the linked folder",
2567
+ type: "string"
2568
+ },
2569
+ dir: {
2570
+ default: ".",
2571
+ description: "Linked directory to authorize from (default: .)",
2572
+ type: "string"
2573
+ },
2574
+ json: { description: "Machine-readable output", type: "boolean" },
2575
+ thread: {
2576
+ description: "Thread id (from `comments --json`)",
2577
+ required: true,
2578
+ type: "positional"
2579
+ }
2580
+ },
2581
+ meta: {
2582
+ description: action === "resolve" ? "Mark a comment thread resolved" : "Reopen a resolved comment thread",
2583
+ name: action
2584
+ },
2585
+ async run({ args }) {
2586
+ try {
2587
+ await changeThreadStatus({
2588
+ action,
2589
+ apiKey: args["api-key"],
2590
+ artifact: args.artifact,
2591
+ dir: args.dir,
2592
+ json: Boolean(args.json),
2593
+ thread: args.thread
2594
+ });
2595
+ } catch (err) {
2596
+ printCliError(err, Boolean(args.json));
2597
+ process.exit(1);
2598
+ }
2599
+ }
2600
+ });
2601
+ var resolve = threadStatusCommand("resolve");
2602
+ var reopen = threadStatusCommand("reopen");
2603
+ var reply = defineCommand({
2604
+ args: {
2605
+ "api-key": {
2606
+ description: "Authorize with an account API key (default: stored login)",
2607
+ type: "string"
2608
+ },
2609
+ artifact: {
2610
+ description: "Act on an artifact ID instead of the linked folder",
2611
+ type: "string"
2612
+ },
2613
+ body: {
2614
+ description: "Reply text (omit to read from stdin)",
2615
+ type: "string"
2616
+ },
2617
+ dir: {
2618
+ default: ".",
2619
+ description: "Linked directory to authorize from (default: .)",
2620
+ type: "string"
2621
+ },
2622
+ json: { description: "Machine-readable output", type: "boolean" },
2623
+ resolve: {
2624
+ description: "Resolve the thread after replying",
2625
+ type: "boolean"
2626
+ },
2627
+ thread: {
2628
+ description: "Thread id (from `comments --json`)",
2629
+ required: true,
2630
+ type: "positional"
2631
+ }
2632
+ },
2633
+ meta: {
2634
+ description: "Reply to a comment thread (as the agent or owner)",
2635
+ name: "reply"
2636
+ },
2637
+ async run({ args }) {
2638
+ try {
2639
+ await replyToThread({
2640
+ apiKey: args["api-key"],
2641
+ artifact: args.artifact,
2642
+ body: args.body,
2643
+ dir: args.dir,
2644
+ json: Boolean(args.json),
2645
+ resolve: Boolean(args.resolve),
2646
+ thread: args.thread
2647
+ });
2648
+ } catch (err) {
2649
+ printCliError(err, Boolean(args.json));
2650
+ process.exit(1);
2651
+ }
2652
+ }
2653
+ });
2295
2654
  var open = defineCommand({
2296
2655
  args: {
2297
2656
  "api-key": {
@@ -2346,7 +2705,8 @@ var dashboard = defineCommand({
2346
2705
  var main = defineCommand({
2347
2706
  meta: {
2348
2707
  description: "Publish AI-generated artifacts for review \u2014 https://artifact.cafe",
2349
- name: "artifact-cafe"
2708
+ name: "artifact-cafe",
2709
+ version: cliVersion()
2350
2710
  },
2351
2711
  // Bare `artifact-cafe` (no subcommand) → brand banner + usage. citty
2352
2712
  // still calls this parent run *after* a matched subcommand, so bail when
@@ -2371,8 +2731,13 @@ var main = defineCommand({
2371
2731
  open,
2372
2732
  protect: protect2,
2373
2733
  publish: publish2,
2734
+ reopen,
2735
+ reply,
2736
+ resolve,
2374
2737
  whoami: whoami2,
2375
2738
  workspaces: workspaces2
2376
2739
  }
2377
2740
  });
2378
- runMain(main);
2741
+ var finishUpdateCheck = startUpdateCheck();
2742
+ await runMain(main);
2743
+ await finishUpdateCheck();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifact-cafe",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Publish AI-generated artifacts for review on artifact.cafe",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,19 +9,19 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "typecheck": "tsc --noEmit",
15
+ "dev": "tsup --watch"
16
+ },
12
17
  "dependencies": {
13
18
  "citty": "^0.1.6",
14
19
  "zod": "^4.1.0"
15
20
  },
16
21
  "devDependencies": {
22
+ "@artifact-cafe/shared": "workspace:*",
17
23
  "@types/node": "^24.0.0",
18
24
  "tsup": "^8.5.0",
19
- "typescript": "^5.9.0",
20
- "@artifact-cafe/shared": "0.1.0"
21
- },
22
- "scripts": {
23
- "build": "tsup",
24
- "typecheck": "tsc --noEmit",
25
- "dev": "tsup --watch"
25
+ "typescript": "^5.9.0"
26
26
  }
27
- }
27
+ }