@nuniapp/cli 0.1.4 → 0.1.6

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 (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.mjs +1862 -58
  3. package/package.json +5 -2
package/dist/index.mjs CHANGED
@@ -1,37 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { join, resolve } from "node:path";
2
+ import { join, relative, resolve } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
- import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { createInterface } from "node:readline";
5
8
  //#region package.json
6
- var version = "0.1.4";
9
+ var version$1 = "0.1.6";
7
10
  //#endregion
8
- //#region ../shared/src/id.ts
9
- const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
10
- const ID_LENGTH = 22;
11
- const PREFIX = "nuni_";
12
- const PROJECT_ID_PATTERN = new RegExp(`^${PREFIX}[${ALPHABET}]{${ID_LENGTH}}$`);
13
- /** Random base58 string using the platform CSPRNG (browser, Node, Convex). */
14
- function randomBase58(length) {
15
- const bytes = new Uint8Array(length);
16
- crypto.getRandomValues(bytes);
17
- let out = "";
18
- for (const byte of bytes) {
19
- let b = byte;
20
- while (b >= 232) {
21
- const retry = /* @__PURE__ */ new Uint8Array(1);
22
- crypto.getRandomValues(retry);
23
- b = retry[0];
24
- }
25
- out += ALPHABET[b % 58];
26
- }
27
- return out;
28
- }
29
- /** Public project identifier, safe to commit into source code. */
30
- function generateProjectId() {
31
- return PREFIX + randomBase58(ID_LENGTH);
32
- }
33
- function isProjectId(value) {
34
- return typeof value === "string" && PROJECT_ID_PATTERN.test(value);
11
+ //#region ../shared/src/path.ts
12
+ /** URL that opens the page with the comment focused by the widget. */
13
+ function commentUrl(page, id) {
14
+ const [pathname, hashRoute] = page.path.split("#");
15
+ const origin = new URL(page.origin).origin;
16
+ let url = new URL(pathname ?? "/", origin);
17
+ if (url.origin !== origin) url = new URL("/", origin);
18
+ const params = new URLSearchParams(page.search ?? "");
19
+ params.set("nuni", id);
20
+ url.search = params.toString();
21
+ if (hashRoute) url.hash = hashRoute;
22
+ return url.toString();
35
23
  }
36
24
  //#endregion
37
25
  //#region ../shared/src/prompt.ts
@@ -65,10 +53,1733 @@ Steps:
65
53
  6. Do not add Nuni to server-only code, and do not load it more than once.
66
54
  7. If the site sends a Content-Security-Policy, allow connect-src https://*.convex.cloud wss://*.convex.cloud https://*.convex.site (and script-src cdn.jsdelivr.net for the script tag).
67
55
  8. Start the dev server, open the site, and confirm the Nuni button appears in the bottom-right corner.
56
+ 9. Tell the user they can let you read and resolve comments later: run npx ${PACKAGES.cli}@latest login once, then add the MCP server (for Claude Code: claude mcp add nuni -- npx -y ${PACKAGES.cli}@latest mcp). Do not run login yourself unless asked.
68
57
 
69
58
  Docs: ${DOCS_URL}/quickstart`;
70
59
  }
71
60
  //#endregion
61
+ //#region ../shared/src/comment-prompt.ts
62
+ /** A code fence longer than any backtick run inside `content`. */
63
+ function fence(content, lang = "") {
64
+ const longest = Math.max(0, ...Array.from(content.matchAll(/`+/g), (m) => m[0].length));
65
+ const ticks = "`".repeat(Math.max(3, longest + 1));
66
+ return `${ticks}${lang}\n${content}\n${ticks}`;
67
+ }
68
+ function quote(text) {
69
+ return text.split("\n").map((line) => line ? `> ${line}` : ">").join("\n");
70
+ }
71
+ function inline(text) {
72
+ const clean = text.replace(/\s+/g, " ").trim();
73
+ return clean.includes("`") ? `"${clean}"` : `\`${clean}\``;
74
+ }
75
+ /**
76
+ * One comment as a ready-to-paste task for a coding agent. Single source of
77
+ * truth for the widget, dashboard, CLI and MCP server.
78
+ */
79
+ function buildCommentPrompt(comment, options = {}) {
80
+ const includeContext = options.includeContext ?? true;
81
+ const { anchor, page } = comment;
82
+ const out = [];
83
+ out.push("Fix this piece of feedback that was left on the live site with Nuni.", "", "## Comment", "", quote(comment.body), "", `From ${comment.authorName} on ${new Date(comment.createdAt).toISOString().slice(0, 10)}` + (comment.status === "resolved" ? " (already resolved)" : ""));
84
+ if (comment.replies?.length) {
85
+ out.push("", `## Replies (${comment.replies.length})`, "");
86
+ for (const reply of comment.replies) out.push(`${reply.authorName}${reply.isOwner ? " (owner)" : ""}:`, quote(reply.body), "");
87
+ out.pop();
88
+ }
89
+ out.push("", "## Where", "");
90
+ out.push(`- Page: ${page.title ? `${page.title} ` : ""}(${page.path})`);
91
+ out.push(`- Open it: ${commentUrl(page, comment._id)}`);
92
+ out.push(`- Viewport: ${comment.viewport.w}×${comment.viewport.h} at ${comment.viewport.dpr}x`);
93
+ if (includeContext && comment.userAgent) out.push(`- Browser: ${comment.userAgent}`);
94
+ out.push("", "## Element", "");
95
+ out.push(`- Tag: <${anchor.tag}>`);
96
+ if (anchor.text) out.push(`- Text: ${inline(anchor.text)}`);
97
+ if (anchor.quote) out.push(`- Selected text: ${inline(anchor.quote.exact)}`);
98
+ if (anchor.region) {
99
+ const pct = (n) => `${Math.round(n * 100)}%`;
100
+ const r = anchor.region;
101
+ out.push(`- Area: ${pct(r.w)} × ${pct(r.h)} of the element, from ${pct(r.x)} left and ${pct(r.y)} top`);
102
+ }
103
+ if (anchor.componentName) out.push(`- React component: ${anchor.componentName}`);
104
+ if (anchor.selectors.testId) out.push(`- Test ID: ${inline(anchor.selectors.testId)}`);
105
+ if (anchor.selectors.id) out.push(`- ID: ${inline(anchor.selectors.id)}`);
106
+ if (anchor.selectors.css) out.push(`- CSS selector: ${inline(anchor.selectors.css)}`);
107
+ out.push(`- DOM path: ${inline(anchor.selectors.path)}`);
108
+ const labels = Object.entries(anchor.attrs).filter(([name]) => name !== "class" && name !== "style").map(([name, value]) => `${name}="${value}"`);
109
+ if (labels.length) out.push(`- Attributes: ${inline(labels.join(" "))}`);
110
+ const context = includeContext ? comment.context : void 0;
111
+ if (context?.dom?.html) out.push("", fence(context.dom.html, "html"));
112
+ const styles = Object.entries(context?.dom?.styles ?? {});
113
+ if (styles.length) out.push("", "Computed styles:", "", fence(styles.map(([k, v]) => `${k}: ${v};`).join("\n"), "css"));
114
+ if (context?.console?.length) {
115
+ out.push("", `## Console (${context.console.length})`, "");
116
+ out.push(fence(context.console.map((e) => `[${e.level}] ${e.message}`).join("\n"), "text"));
117
+ }
118
+ if (context?.network?.length) {
119
+ out.push("", `## Failed requests (${context.network.length})`, "");
120
+ for (const r of context.network) out.push(`- ${r.method} ${r.url} → ${r.status === 0 ? "network error" : r.status}`);
121
+ }
122
+ if (includeContext && comment.screenshotUrl) out.push("", "## Screenshot", "", comment.screenshotUrl);
123
+ out.push("", "## Steps", "", "1. Find the code that renders this element. Search for the text, the component name, the test ID or the selector.", "2. Make the change the comment asks for, and keep the fix focused on it.", "3. Tell me what you changed, so the comment can be resolved in Nuni.", ` If the Nuni CLI is signed in to this project (\`npx ${PACKAGES.cli}@latest login\`), resolve it with \`npx ${PACKAGES.cli}@latest resolve ${comment._id}\` or the \`resolve_comment\` MCP tool.`);
124
+ return out.join("\n");
125
+ }
126
+ //#endregion
127
+ //#region ../shared/src/id.ts
128
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
129
+ const ID_LENGTH = 22;
130
+ const PREFIX = "nuni_";
131
+ const PROJECT_ID_PATTERN = new RegExp(`^${PREFIX}[${ALPHABET}]{${ID_LENGTH}}$`);
132
+ /** Random base58 string using the platform CSPRNG (browser, Node, Convex). */
133
+ function randomBase58(length) {
134
+ const bytes = new Uint8Array(length);
135
+ crypto.getRandomValues(bytes);
136
+ let out = "";
137
+ for (const byte of bytes) {
138
+ let b = byte;
139
+ while (b >= 232) {
140
+ const retry = /* @__PURE__ */ new Uint8Array(1);
141
+ crypto.getRandomValues(retry);
142
+ b = retry[0];
143
+ }
144
+ out += ALPHABET[b % 58];
145
+ }
146
+ return out;
147
+ }
148
+ /** Public project identifier, safe to commit into source code. */
149
+ function generateProjectId() {
150
+ return PREFIX + randomBase58(ID_LENGTH);
151
+ }
152
+ function isProjectId(value) {
153
+ return typeof value === "string" && PROJECT_ID_PATTERN.test(value);
154
+ }
155
+ //#endregion
156
+ //#region src/credentials.ts
157
+ /** ~/.config/nuni, or $XDG_CONFIG_HOME/nuni, or %APPDATA%\nuni on Windows. */
158
+ function configDir() {
159
+ if (process.env.NUNI_CONFIG_DIR) return process.env.NUNI_CONFIG_DIR;
160
+ const base = process.env.XDG_CONFIG_HOME || process.platform === "win32" && process.env.APPDATA || join(homedir(), ".config");
161
+ return join(base, "nuni");
162
+ }
163
+ function credentialsPath() {
164
+ return join(configDir(), "credentials.json");
165
+ }
166
+ function read() {
167
+ try {
168
+ const value = JSON.parse(readFileSync(credentialsPath(), "utf8"));
169
+ return value && typeof value === "object" ? value : {};
170
+ } catch {
171
+ return {};
172
+ }
173
+ }
174
+ function write(store) {
175
+ const dir = configDir();
176
+ if (!existsSync(dir)) mkdirSync(dir, {
177
+ recursive: true,
178
+ mode: 448
179
+ });
180
+ const path = credentialsPath();
181
+ writeFileSync(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
182
+ chmodSync(path, 384);
183
+ }
184
+ /** The session for a project: $NUNI_TOKEN, or the one saved by `nuni login`. */
185
+ function credentialFor(publicId) {
186
+ if (process.env.NUNI_TOKEN) return { token: process.env.NUNI_TOKEN };
187
+ const saved = read()[publicId];
188
+ if (!saved?.token) return null;
189
+ if (saved.expiresAt && saved.expiresAt < Date.now()) return null;
190
+ return saved;
191
+ }
192
+ function saveCredential(publicId, credential) {
193
+ write({
194
+ ...read(),
195
+ [publicId]: credential
196
+ });
197
+ }
198
+ function removeCredential(publicId) {
199
+ const store = read();
200
+ if (!store[publicId]) return false;
201
+ delete store[publicId];
202
+ write(store);
203
+ return true;
204
+ }
205
+ //#endregion
206
+ //#region src/env.ts
207
+ /**
208
+ * Backend URLs, set at build time (see tsdown.config.ts). The env vars
209
+ * override them, for local development and tests.
210
+ */
211
+ const CONVEX_URL = process.env.NUNI_CONVEX_URL ?? "https://greedy-giraffe-33.convex.cloud";
212
+ const CONVEX_SITE_URL = process.env.NUNI_CONVEX_SITE_URL ?? "https://greedy-giraffe-33.convex.site";
213
+ const APP_URL = process.env.NUNI_APP_URL ?? "https://nuni.praveenjuge.com";
214
+ //#endregion
215
+ //#region src/format.ts
216
+ function timeAgo(at, now = Date.now()) {
217
+ const seconds = Math.max(0, Math.round((now - at) / 1e3));
218
+ if (seconds < 60) return "just now";
219
+ const steps = [
220
+ [60, "minute"],
221
+ [24, "hour"],
222
+ [30, "day"],
223
+ [12, "month"],
224
+ [Infinity, "year"]
225
+ ];
226
+ let value = seconds / 60;
227
+ for (const [size, unit] of steps) {
228
+ if (value < size) {
229
+ const n = Math.floor(value);
230
+ return `${n} ${unit}${n === 1 ? "" : "s"} ago`;
231
+ }
232
+ value /= size;
233
+ }
234
+ return "a long time ago";
235
+ }
236
+ function oneLine(text, max = 120) {
237
+ const clean = text.replace(/\s+/g, " ").trim();
238
+ return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
239
+ }
240
+ /** A compact, numbered list: enough to pick a comment, not the full context. */
241
+ function formatCommentList(comments, options) {
242
+ if (!comments.length) return `No ${options.status} comments.`;
243
+ const lines = [`${comments.length}${options.more ? "+" : ""} ${options.status} comment${comments.length === 1 ? "" : "s"}, newest first:`, ""];
244
+ comments.forEach((c, i) => {
245
+ const element = c.anchor.text ? `<${c.anchor.tag}> "${oneLine(c.anchor.text, 60)}"` : `<${c.anchor.tag}>`;
246
+ lines.push(`${i + 1}. ${c._id} · ${c.page.path} · ${c.authorName} · ${timeAgo(c.createdAt)}`, ` "${oneLine(c.body)}"`, c.anchor.quote ? ` On the text "${oneLine(c.anchor.quote.exact, 60)}" in ${element}` : ` ${c.anchor.region ? "On an area of" : "On"} ${element}`);
247
+ });
248
+ lines.push("", options.detailHint);
249
+ return lines.join("\n");
250
+ }
251
+ //#endregion
252
+ //#region src/project.ts
253
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
254
+ "node_modules",
255
+ ".git",
256
+ "dist",
257
+ "build",
258
+ "out",
259
+ "coverage",
260
+ "vendor",
261
+ "target",
262
+ ".next",
263
+ ".nuxt",
264
+ ".output",
265
+ ".svelte-kit",
266
+ ".astro",
267
+ ".turbo",
268
+ ".vercel",
269
+ ".cache"
270
+ ]);
271
+ const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|html?|php|erb|liquid|hbs|njk|mdx?)$/i;
272
+ const ID_IN_TEXT = /nuni_[1-9A-HJ-NP-Za-km-z]{22}/g;
273
+ const MAX_FILES = 5e3;
274
+ const MAX_FILE_BYTES = 524288;
275
+ /**
276
+ * Project IDs used in this codebase, found by scanning source files (the ID
277
+ * is committed in code, next to <Nuni /> or the script tag).
278
+ */
279
+ function findProjectIds(cwd) {
280
+ const found = /* @__PURE__ */ new Map();
281
+ const queue = [cwd];
282
+ let files = 0;
283
+ while (queue.length && files < MAX_FILES) {
284
+ const dir = queue.shift();
285
+ let entries;
286
+ try {
287
+ entries = readdirSync(dir).sort();
288
+ } catch {
289
+ continue;
290
+ }
291
+ for (const name of entries) {
292
+ const path = join(dir, name);
293
+ let stat;
294
+ try {
295
+ stat = statSync(path);
296
+ } catch {
297
+ continue;
298
+ }
299
+ if (stat.isDirectory()) {
300
+ if (!SKIP_DIRS.has(name) && !name.startsWith(".")) queue.push(path);
301
+ continue;
302
+ }
303
+ if (!SOURCE_FILE.test(name) || stat.size > MAX_FILE_BYTES) continue;
304
+ if (++files > MAX_FILES) break;
305
+ let text;
306
+ try {
307
+ text = readFileSync(path, "utf8");
308
+ } catch {
309
+ continue;
310
+ }
311
+ for (const [id] of text.matchAll(ID_IN_TEXT)) if (isProjectId(id) && !found.has(id)) found.set(id, relative(cwd, path));
312
+ }
313
+ }
314
+ return Array.from(found, ([id, file]) => ({
315
+ id,
316
+ file
317
+ }));
318
+ }
319
+ var ProjectError = class extends Error {};
320
+ /**
321
+ * The project to work on: --project, then $NUNI_PROJECT, then the one ID
322
+ * found in the code.
323
+ */
324
+ function resolveProject(flag, cwd) {
325
+ const explicit = flag ?? process.env.NUNI_PROJECT;
326
+ if (explicit) {
327
+ if (!isProjectId(explicit)) throw new ProjectError(`"${explicit}" is not a valid Nuni project ID (expected nuni_ followed by 22 characters).`);
328
+ return {
329
+ id: explicit,
330
+ source: flag ? "--project" : "NUNI_PROJECT"
331
+ };
332
+ }
333
+ const found = findProjectIds(cwd);
334
+ if (found.length === 1) return {
335
+ id: found[0].id,
336
+ source: found[0].file
337
+ };
338
+ if (!found.length) throw new ProjectError(`No Nuni project ID found in ${cwd}. Run this in the project that uses Nuni, or pass --project nuni_...`);
339
+ throw new ProjectError(`Found more than one Nuni project ID. Pass one with --project:\n${found.map((p) => ` ${p.id} (${p.file})`).join("\n")}`);
340
+ }
341
+ //#endregion
342
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/values/base64.js
343
+ var lookup = [];
344
+ var revLookup = [];
345
+ var Arr = Uint8Array;
346
+ var code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
347
+ for (var i = 0, len = code.length; i < len; ++i) {
348
+ lookup[i] = code[i];
349
+ revLookup[code.charCodeAt(i)] = i;
350
+ }
351
+ revLookup["-".charCodeAt(0)] = 62;
352
+ revLookup["_".charCodeAt(0)] = 63;
353
+ function getLens(b64) {
354
+ var len = b64.length;
355
+ if (len % 4 > 0) throw new Error("Invalid string. Length must be a multiple of 4");
356
+ var validLen = b64.indexOf("=");
357
+ if (validLen === -1) validLen = len;
358
+ var placeHoldersLen = validLen === len ? 0 : 4 - validLen % 4;
359
+ return [validLen, placeHoldersLen];
360
+ }
361
+ function _byteLength(_b64, validLen, placeHoldersLen) {
362
+ return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen;
363
+ }
364
+ function toByteArray(b64) {
365
+ var tmp;
366
+ var lens = getLens(b64);
367
+ var validLen = lens[0];
368
+ var placeHoldersLen = lens[1];
369
+ var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen));
370
+ var curByte = 0;
371
+ var len = placeHoldersLen > 0 ? validLen - 4 : validLen;
372
+ var i = 0;
373
+ for (; i < len; i += 4) {
374
+ tmp = revLookup[b64.charCodeAt(i)] << 18 | revLookup[b64.charCodeAt(i + 1)] << 12 | revLookup[b64.charCodeAt(i + 2)] << 6 | revLookup[b64.charCodeAt(i + 3)];
375
+ arr[curByte++] = tmp >> 16 & 255;
376
+ arr[curByte++] = tmp >> 8 & 255;
377
+ arr[curByte++] = tmp & 255;
378
+ }
379
+ if (placeHoldersLen === 2) {
380
+ tmp = revLookup[b64.charCodeAt(i)] << 2 | revLookup[b64.charCodeAt(i + 1)] >> 4;
381
+ arr[curByte++] = tmp & 255;
382
+ }
383
+ if (placeHoldersLen === 1) {
384
+ tmp = revLookup[b64.charCodeAt(i)] << 10 | revLookup[b64.charCodeAt(i + 1)] << 4 | revLookup[b64.charCodeAt(i + 2)] >> 2;
385
+ arr[curByte++] = tmp >> 8 & 255;
386
+ arr[curByte++] = tmp & 255;
387
+ }
388
+ return arr;
389
+ }
390
+ function tripletToBase64(num) {
391
+ return lookup[num >> 18 & 63] + lookup[num >> 12 & 63] + lookup[num >> 6 & 63] + lookup[num & 63];
392
+ }
393
+ function encodeChunk(uint8, start, end) {
394
+ var tmp;
395
+ var output = [];
396
+ for (var i = start; i < end; i += 3) {
397
+ tmp = (uint8[i] << 16 & 16711680) + (uint8[i + 1] << 8 & 65280) + (uint8[i + 2] & 255);
398
+ output.push(tripletToBase64(tmp));
399
+ }
400
+ return output.join("");
401
+ }
402
+ function fromByteArray(uint8) {
403
+ var tmp;
404
+ var len = uint8.length;
405
+ var extraBytes = len % 3;
406
+ var parts = [];
407
+ var maxChunkLength = 16383;
408
+ for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) parts.push(encodeChunk(uint8, i, i + maxChunkLength > len2 ? len2 : i + maxChunkLength));
409
+ if (extraBytes === 1) {
410
+ tmp = uint8[len - 1];
411
+ parts.push(lookup[tmp >> 2] + lookup[tmp << 4 & 63] + "==");
412
+ } else if (extraBytes === 2) {
413
+ tmp = (uint8[len - 2] << 8) + uint8[len - 1];
414
+ parts.push(lookup[tmp >> 10] + lookup[tmp >> 4 & 63] + lookup[tmp << 2 & 63] + "=");
415
+ }
416
+ return parts.join("");
417
+ }
418
+ //#endregion
419
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/common/index.js
420
+ function parseArgs$1(args) {
421
+ if (args === void 0) return {};
422
+ if (!isSimpleObject(args)) throw new Error(`The arguments to a Convex function must be an object. Received: ${args}`);
423
+ return args;
424
+ }
425
+ function validateDeploymentUrl(deploymentUrl) {
426
+ if (typeof deploymentUrl === "undefined") throw new Error(`Client created with undefined deployment address. If you used an environment variable, check that it's set.`);
427
+ if (typeof deploymentUrl !== "string") throw new Error(`Invalid deployment address: found ${deploymentUrl}".`);
428
+ if (!(deploymentUrl.startsWith("http:") || deploymentUrl.startsWith("https:"))) throw new Error(`Invalid deployment address: Must start with "https://" or "http://". Found "${deploymentUrl}".`);
429
+ try {
430
+ new URL(deploymentUrl);
431
+ } catch {
432
+ throw new Error(`Invalid deployment address: "${deploymentUrl}" is not a valid URL. If you believe this URL is correct, use the \`skipConvexDeploymentUrlCheck\` option to bypass this.`);
433
+ }
434
+ if (deploymentUrl.endsWith(".convex.site")) throw new Error(`Invalid deployment address: "${deploymentUrl}" ends with .convex.site, which is used for HTTP Actions. Convex deployment URLs typically end with .convex.cloud? If you believe this URL is correct, use the \`skipConvexDeploymentUrlCheck\` option to bypass this.`);
435
+ }
436
+ function isSimpleObject(value) {
437
+ const isObject = typeof value === "object";
438
+ const prototype = Object.getPrototypeOf(value);
439
+ const isSimple = prototype === null || prototype === Object.prototype || prototype?.constructor?.name === "Object";
440
+ return isObject && isSimple;
441
+ }
442
+ //#endregion
443
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/values/value.js
444
+ const LITTLE_ENDIAN = true;
445
+ const MIN_INT64 = BigInt("-9223372036854775808");
446
+ const MAX_INT64 = BigInt("9223372036854775807");
447
+ const ZERO = BigInt("0");
448
+ const EIGHT = BigInt("8");
449
+ const TWOFIFTYSIX = BigInt("256");
450
+ const COMMIT_TS_UNRESOLVED = "This commit timestamp is unresolved: its value is assigned when the mutation commits. Read the document after the mutation completes to get its value.";
451
+ var CommitTsPlaceholder = class {
452
+ [Symbol.toPrimitive](hint) {
453
+ if (hint === "string") return this.toString();
454
+ throw new Error(COMMIT_TS_UNRESOLVED);
455
+ }
456
+ valueOf() {
457
+ throw new Error(COMMIT_TS_UNRESOLVED);
458
+ }
459
+ toJSON() {
460
+ throw new Error(COMMIT_TS_UNRESOLVED);
461
+ }
462
+ toString() {
463
+ return "[unresolved commit timestamp]";
464
+ }
465
+ };
466
+ const commitTsPlaceholder = new CommitTsPlaceholder();
467
+ function isSpecial(n) {
468
+ return Number.isNaN(n) || !Number.isFinite(n) || Object.is(n, -0);
469
+ }
470
+ function slowBigIntToBase64(value) {
471
+ if (value < ZERO) value -= MIN_INT64 + MIN_INT64;
472
+ let hex = value.toString(16);
473
+ if (hex.length % 2 === 1) hex = "0" + hex;
474
+ const bytes = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(8));
475
+ let i = 0;
476
+ for (const hexByte of hex.match(/.{2}/g).reverse()) {
477
+ bytes.set([parseInt(hexByte, 16)], i++);
478
+ value >>= EIGHT;
479
+ }
480
+ return fromByteArray(bytes);
481
+ }
482
+ function slowBase64ToBigInt(encoded) {
483
+ const integerBytes = toByteArray(encoded);
484
+ if (integerBytes.byteLength !== 8) throw new Error(`Received ${integerBytes.byteLength} bytes, expected 8 for $integer`);
485
+ let value = ZERO;
486
+ let power = ZERO;
487
+ for (const byte of integerBytes) {
488
+ value += BigInt(byte) * TWOFIFTYSIX ** power;
489
+ power++;
490
+ }
491
+ if (value > MAX_INT64) value += MIN_INT64 + MIN_INT64;
492
+ return value;
493
+ }
494
+ function modernBigIntToBase64(value) {
495
+ if (value < MIN_INT64 || MAX_INT64 < value) throw new Error(`BigInt ${value} does not fit into a 64-bit signed integer.`);
496
+ const buffer = /* @__PURE__ */ new ArrayBuffer(8);
497
+ new DataView(buffer).setBigInt64(0, value, true);
498
+ return fromByteArray(new Uint8Array(buffer));
499
+ }
500
+ function modernBase64ToBigInt(encoded) {
501
+ const integerBytes = toByteArray(encoded);
502
+ if (integerBytes.byteLength !== 8) throw new Error(`Received ${integerBytes.byteLength} bytes, expected 8 for $integer`);
503
+ return new DataView(integerBytes.buffer).getBigInt64(0, true);
504
+ }
505
+ const bigIntToBase64 = DataView.prototype.setBigInt64 ? modernBigIntToBase64 : slowBigIntToBase64;
506
+ const base64ToBigInt = DataView.prototype.getBigInt64 ? modernBase64ToBigInt : slowBase64ToBigInt;
507
+ const MAX_IDENTIFIER_LEN = 1024;
508
+ function validateObjectField(k) {
509
+ if (k.length > MAX_IDENTIFIER_LEN) throw new Error(`Field name ${k} exceeds maximum field name length ${MAX_IDENTIFIER_LEN}.`);
510
+ if (k.startsWith("$")) throw new Error(`Field name ${k} starts with a '$', which is reserved.`);
511
+ for (let i = 0; i < k.length; i += 1) {
512
+ const charCode = k.charCodeAt(i);
513
+ if (charCode < 32 || charCode >= 127) throw new Error(`Field name ${k} has invalid character '${k[i]}': Field names can only contain non-control ASCII characters`);
514
+ }
515
+ }
516
+ function jsonToConvex(value) {
517
+ if (value === null) return value;
518
+ if (typeof value === "boolean") return value;
519
+ if (typeof value === "number") return value;
520
+ if (typeof value === "string") return value;
521
+ if (Array.isArray(value)) return value.map((value2) => jsonToConvex(value2));
522
+ if (typeof value !== "object") throw new Error(`Unexpected type of ${value}`);
523
+ const entries = Object.entries(value);
524
+ if (entries.length === 1) {
525
+ const key = entries[0][0];
526
+ if (key === "$bytes") {
527
+ if (typeof value.$bytes !== "string") throw new Error(`Malformed $bytes field on ${value}`);
528
+ return toByteArray(value.$bytes).buffer;
529
+ }
530
+ if (key === "$integer") {
531
+ if (typeof value.$integer !== "string") throw new Error(`Malformed $integer field on ${value}`);
532
+ return base64ToBigInt(value.$integer);
533
+ }
534
+ if (key === "$float") {
535
+ if (typeof value.$float !== "string") throw new Error(`Malformed $float field on ${value}`);
536
+ const floatBytes = toByteArray(value.$float);
537
+ if (floatBytes.byteLength !== 8) throw new Error(`Received ${floatBytes.byteLength} bytes, expected 8 for $float`);
538
+ const float = new DataView(floatBytes.buffer).getFloat64(0, LITTLE_ENDIAN);
539
+ if (!isSpecial(float)) throw new Error(`Float ${float} should be encoded as a number`);
540
+ return float;
541
+ }
542
+ if (key === "$commitTs") {
543
+ if (value.$commitTs !== null) throw new Error(`Malformed $commitTs field on ${value}`);
544
+ return commitTsPlaceholder;
545
+ }
546
+ if (key === "$set") throw new Error(`Received a Set which is no longer supported as a Convex type.`);
547
+ if (key === "$map") throw new Error(`Received a Map which is no longer supported as a Convex type.`);
548
+ }
549
+ const out = {};
550
+ for (const [k, v] of Object.entries(value)) {
551
+ validateObjectField(k);
552
+ out[k] = jsonToConvex(v);
553
+ }
554
+ return out;
555
+ }
556
+ const MAX_VALUE_FOR_ERROR_LEN = 16384;
557
+ function stringifyValueForError(value) {
558
+ const str = JSON.stringify(value, (_key, value2) => {
559
+ if (value2 === void 0) return "undefined";
560
+ if (typeof value2 === "bigint") return `${value2.toString()}n`;
561
+ return value2;
562
+ });
563
+ if (str.length > MAX_VALUE_FOR_ERROR_LEN) {
564
+ const rest = "[...truncated]";
565
+ let truncateAt = 16370;
566
+ const codePoint = str.codePointAt(truncateAt - 1);
567
+ if (codePoint !== void 0 && codePoint > 65535) truncateAt -= 1;
568
+ return str.substring(0, truncateAt) + rest;
569
+ }
570
+ return str;
571
+ }
572
+ function convexToJsonInternal(value, originalValue, context, includeTopLevelUndefined) {
573
+ if (value === void 0) {
574
+ const contextText = context && ` (present at path ${context} in original object ${stringifyValueForError(originalValue)})`;
575
+ throw new Error(`undefined is not a valid Convex value${contextText}. To learn about Convex's supported types, see https://docs.convex.dev/using/types.`);
576
+ }
577
+ if (value === null) return value;
578
+ if (typeof value === "bigint") {
579
+ if (value < MIN_INT64 || MAX_INT64 < value) throw new Error(`BigInt ${value} does not fit into a 64-bit signed integer.`);
580
+ return { $integer: bigIntToBase64(value) };
581
+ }
582
+ if (typeof value === "number") {
583
+ if (isSpecial(value)) {
584
+ const buffer = /* @__PURE__ */ new ArrayBuffer(8);
585
+ new DataView(buffer).setFloat64(0, value, LITTLE_ENDIAN);
586
+ return { $float: fromByteArray(new Uint8Array(buffer)) };
587
+ } else return value;
588
+ }
589
+ if (typeof value === "boolean") return value;
590
+ if (typeof value === "string") return value;
591
+ if (value instanceof ArrayBuffer) return { $bytes: fromByteArray(new Uint8Array(value)) };
592
+ if (value instanceof CommitTsPlaceholder) return { $commitTs: null };
593
+ if (Array.isArray(value)) return value.map((value2, i) => convexToJsonInternal(value2, originalValue, context + `[${i}]`, false));
594
+ if (value instanceof Set) throw new Error(errorMessageForUnsupportedType(context, "Set", [...value], originalValue));
595
+ if (value instanceof Map) throw new Error(errorMessageForUnsupportedType(context, "Map", [...value], originalValue));
596
+ if (!isSimpleObject(value)) {
597
+ const theType = value?.constructor?.name;
598
+ const typeName = theType ? `${theType} ` : "";
599
+ throw new Error(errorMessageForUnsupportedType(context, typeName, value, originalValue));
600
+ }
601
+ const out = {};
602
+ const entries = Object.entries(value);
603
+ entries.sort(([k1, _v1], [k2, _v2]) => k1 === k2 ? 0 : k1 < k2 ? -1 : 1);
604
+ for (const [k, v] of entries) if (v !== void 0) {
605
+ validateObjectField(k);
606
+ out[k] = convexToJsonInternal(v, originalValue, context + `.${k}`, false);
607
+ } else if (includeTopLevelUndefined) {
608
+ validateObjectField(k);
609
+ out[k] = convexOrUndefinedToJsonInternal(v, originalValue, context + `.${k}`);
610
+ }
611
+ return out;
612
+ }
613
+ function errorMessageForUnsupportedType(context, typeName, value, originalValue) {
614
+ if (context) return `${typeName}${stringifyValueForError(value)} is not a supported Convex type (present at path ${context} in original object ${stringifyValueForError(originalValue)}). To learn about Convex's supported types, see https://docs.convex.dev/using/types.`;
615
+ else return `${typeName}${stringifyValueForError(value)} is not a supported Convex type.`;
616
+ }
617
+ function convexOrUndefinedToJsonInternal(value, originalValue, context) {
618
+ if (value === void 0) return { $undefined: null };
619
+ else {
620
+ if (originalValue === void 0) throw new Error(`Programming error. Current value is ${stringifyValueForError(value)} but original value is undefined`);
621
+ return convexToJsonInternal(value, originalValue, context, false);
622
+ }
623
+ }
624
+ function convexToJson(value) {
625
+ return convexToJsonInternal(value, value, "", false);
626
+ }
627
+ //#endregion
628
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/values/errors.js
629
+ var __defProp$2 = Object.defineProperty;
630
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
631
+ enumerable: true,
632
+ configurable: true,
633
+ writable: true,
634
+ value
635
+ }) : obj[key] = value;
636
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
637
+ var _a;
638
+ var _b;
639
+ const IDENTIFYING_FIELD = Symbol.for("ConvexError");
640
+ var ConvexError = class extends (_b = Error, _a = IDENTIFYING_FIELD, _b) {
641
+ constructor(data) {
642
+ super(typeof data === "string" ? data : stringifyValueForError(data));
643
+ __publicField$2(this, "name", "ConvexError");
644
+ __publicField$2(this, "data");
645
+ __publicField$2(this, _a, true);
646
+ this.data = data;
647
+ }
648
+ };
649
+ //#endregion
650
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/index.js
651
+ const version = "1.46.0";
652
+ //#endregion
653
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/server/functionName.js
654
+ const functionName = Symbol.for("functionName");
655
+ //#endregion
656
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/server/components/paths.js
657
+ const toReferencePath = Symbol.for("toReferencePath");
658
+ function extractReferencePath(reference) {
659
+ return reference[toReferencePath] ?? null;
660
+ }
661
+ function isFunctionHandle(s) {
662
+ return s.startsWith("function://");
663
+ }
664
+ function getFunctionAddress(functionReference) {
665
+ let functionAddress;
666
+ if (typeof functionReference === "string") {
667
+ if (isFunctionHandle(functionReference)) functionAddress = { functionHandle: functionReference };
668
+ else functionAddress = { name: functionReference };
669
+ } else if (functionReference[functionName]) functionAddress = { name: functionReference[functionName] };
670
+ else {
671
+ const referencePath = extractReferencePath(functionReference);
672
+ if (!referencePath) throw new Error(`${functionReference} is not a functionReference`);
673
+ functionAddress = { reference: referencePath };
674
+ }
675
+ return functionAddress;
676
+ }
677
+ //#endregion
678
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/server/api.js
679
+ function getFunctionName(functionReference) {
680
+ const address = getFunctionAddress(functionReference);
681
+ if (address.name === void 0) {
682
+ if (address.functionHandle !== void 0) throw new Error(`Expected function reference like "api.file.func" or "internal.file.func", but received function handle ${address.functionHandle}`);
683
+ else if (address.reference !== void 0) throw new Error(`Expected function reference in the current component like "api.file.func" or "internal.file.func", but received reference ${address.reference}`);
684
+ throw new Error(`Expected function reference like "api.file.func" or "internal.file.func", but received ${JSON.stringify(address)}`);
685
+ }
686
+ if (typeof functionReference === "string") return functionReference;
687
+ const name = functionReference[functionName];
688
+ if (!name) throw new Error(`${functionReference} is not a functionReference`);
689
+ return name;
690
+ }
691
+ function createApi(pathParts = []) {
692
+ return new Proxy({}, { get(_, prop) {
693
+ if (typeof prop === "string") return createApi([...pathParts, prop]);
694
+ else if (prop === functionName) {
695
+ if (pathParts.length < 2) {
696
+ const found = ["api", ...pathParts].join(".");
697
+ throw new Error(`API path is expected to be of the form \`api.moduleName.functionName\`. Found: \`${found}\``);
698
+ }
699
+ const path = pathParts.slice(0, -1).join("/");
700
+ const exportName = pathParts[pathParts.length - 1];
701
+ if (exportName === "default") return path;
702
+ else return path + ":" + exportName;
703
+ } else if (prop === Symbol.toStringTag) return "FunctionReference";
704
+ else return;
705
+ } });
706
+ }
707
+ const anyApi = createApi();
708
+ //#endregion
709
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/server/components/index.js
710
+ function createChildComponents(root, pathParts) {
711
+ return new Proxy({}, { get(_, prop) {
712
+ if (typeof prop === "string") return createChildComponents(root, [...pathParts, prop]);
713
+ else if (prop === toReferencePath) {
714
+ if (pathParts.length < 1) {
715
+ const found = [root, ...pathParts].join(".");
716
+ throw new Error(`API path is expected to be of the form \`${root}.childComponent.functionName\`. Found: \`${found}\``);
717
+ }
718
+ return `_reference/childComponent/` + pathParts.join("/");
719
+ } else return;
720
+ } });
721
+ }
722
+ const componentsGeneric = () => createChildComponents("components", []);
723
+ //#endregion
724
+ //#region ../backend/convex/_generated/api.js
725
+ /**
726
+ * Generated `api` utility.
727
+ *
728
+ * THIS CODE IS AUTOMATICALLY GENERATED.
729
+ *
730
+ * To regenerate, run `npx convex dev`.
731
+ * @module
732
+ */
733
+ /**
734
+ * A utility for referencing Convex functions in your app's API.
735
+ *
736
+ * Usage:
737
+ * ```js
738
+ * const myFunctionReference = api.myModule.myFunction;
739
+ * ```
740
+ */
741
+ const api = anyApi;
742
+ componentsGeneric();
743
+ //#endregion
744
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/browser/logging.js
745
+ var __defProp$1 = Object.defineProperty;
746
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
747
+ enumerable: true,
748
+ configurable: true,
749
+ writable: true,
750
+ value
751
+ }) : obj[key] = value;
752
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
753
+ const INFO_COLOR = "color:rgb(0, 145, 255)";
754
+ function prefix_for_source(source) {
755
+ switch (source) {
756
+ case "query": return "Q";
757
+ case "mutation": return "M";
758
+ case "action": return "A";
759
+ case "any": return "?";
760
+ }
761
+ }
762
+ var DefaultLogger = class {
763
+ constructor(options) {
764
+ __publicField$1(this, "_onLogLineFuncs");
765
+ __publicField$1(this, "_verbose");
766
+ this._onLogLineFuncs = {};
767
+ this._verbose = options.verbose;
768
+ }
769
+ addLogLineListener(func) {
770
+ let id = Math.random().toString(36).substring(2, 15);
771
+ for (let i = 0; i < 10; i++) {
772
+ if (this._onLogLineFuncs[id] === void 0) break;
773
+ id = Math.random().toString(36).substring(2, 15);
774
+ }
775
+ this._onLogLineFuncs[id] = func;
776
+ return () => {
777
+ delete this._onLogLineFuncs[id];
778
+ };
779
+ }
780
+ logVerbose(...args) {
781
+ if (this._verbose) for (const func of Object.values(this._onLogLineFuncs)) func("debug", `${(/* @__PURE__ */ new Date()).toISOString()}`, ...args);
782
+ }
783
+ log(...args) {
784
+ for (const func of Object.values(this._onLogLineFuncs)) func("info", ...args);
785
+ }
786
+ warn(...args) {
787
+ for (const func of Object.values(this._onLogLineFuncs)) func("warn", ...args);
788
+ }
789
+ error(...args) {
790
+ for (const func of Object.values(this._onLogLineFuncs)) func("error", ...args);
791
+ }
792
+ };
793
+ function instantiateDefaultLogger(options) {
794
+ const logger = new DefaultLogger(options);
795
+ logger.addLogLineListener((level, ...args) => {
796
+ switch (level) {
797
+ case "debug":
798
+ console.debug(...args);
799
+ break;
800
+ case "info":
801
+ console.log(...args);
802
+ break;
803
+ case "warn":
804
+ console.warn(...args);
805
+ break;
806
+ case "error":
807
+ console.error(...args);
808
+ break;
809
+ default: console.log(...args);
810
+ }
811
+ });
812
+ return logger;
813
+ }
814
+ function instantiateNoopLogger(options) {
815
+ return new DefaultLogger(options);
816
+ }
817
+ function logForFunction(logger, type, source, udfPath, message) {
818
+ const prefix = prefix_for_source(source);
819
+ if (typeof message === "object") message = `ConvexError ${JSON.stringify(message.errorData, null, 2)}`;
820
+ if (type === "info") {
821
+ const match = message.match(/^\[.*?\] /);
822
+ if (match === null) {
823
+ logger.error(`[CONVEX ${prefix}(${udfPath})] Could not parse console.log`);
824
+ return;
825
+ }
826
+ const level = message.slice(1, match[0].length - 2);
827
+ const args = message.slice(match[0].length);
828
+ logger.log(`%c[CONVEX ${prefix}(${udfPath})] [${level}]`, INFO_COLOR, args);
829
+ } else logger.error(`[CONVEX ${prefix}(${udfPath})] ${message}`);
830
+ }
831
+ //#endregion
832
+ //#region ../../node_modules/.bun/convex@1.46.0+0f58469d5b3bd39f/node_modules/convex/dist/esm/browser/http_client.js
833
+ var __defProp = Object.defineProperty;
834
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
835
+ enumerable: true,
836
+ configurable: true,
837
+ writable: true,
838
+ value
839
+ }) : obj[key] = value;
840
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
841
+ var ConvexHttpClient = class {
842
+ /**
843
+ * Create a new {@link ConvexHttpClient}.
844
+ *
845
+ * @param address - The url of your Convex deployment, often provided
846
+ * by an environment variable. E.g. `https://small-mouse-123.convex.cloud`.
847
+ * @param options - An object of options.
848
+ * - `skipConvexDeploymentUrlCheck` - Skip validating that the Convex deployment URL looks like
849
+ * `https://happy-animal-123.convex.cloud` or localhost. This can be useful if running a self-hosted
850
+ * Convex backend that uses a different URL.
851
+ * - `logger` - A logger or a boolean. If not provided, logs to the console.
852
+ * You can construct your own logger to customize logging to log elsewhere
853
+ * or not log at all, or use `false` as a shorthand for a no-op logger.
854
+ * A logger is an object with 4 methods: log(), warn(), error(), and logVerbose().
855
+ * These methods can receive multiple arguments of any types, like console.log().
856
+ * - `auth` - A JWT containing identity claims accessible in Convex functions.
857
+ * This identity may expire so it may be necessary to call `setAuth()` later,
858
+ * but for short-lived clients it's convenient to specify this value here.
859
+ * - `fetch` - A custom fetch implementation to use for all HTTP requests made by this client.
860
+ */
861
+ constructor(address, options) {
862
+ __publicField(this, "address");
863
+ __publicField(this, "auth");
864
+ __publicField(this, "adminAuth");
865
+ __publicField(this, "encodedTsPromise");
866
+ __publicField(this, "debug");
867
+ __publicField(this, "fetchOptions");
868
+ __publicField(this, "fetch");
869
+ __publicField(this, "logger");
870
+ __publicField(this, "mutationQueue", []);
871
+ __publicField(this, "isProcessingQueue", false);
872
+ if (typeof options === "boolean") throw new Error("skipConvexDeploymentUrlCheck as the second argument is no longer supported. Please pass an options object, `{ skipConvexDeploymentUrlCheck: true }`.");
873
+ if ((options ?? {}).skipConvexDeploymentUrlCheck !== true) validateDeploymentUrl(address);
874
+ this.logger = options?.logger === false ? instantiateNoopLogger({ verbose: false }) : options?.logger !== true && options?.logger ? options.logger : instantiateDefaultLogger({ verbose: false });
875
+ this.address = address;
876
+ this.debug = true;
877
+ this.auth = void 0;
878
+ this.adminAuth = void 0;
879
+ this.fetch = options?.fetch;
880
+ if (options?.auth) this.setAuth(options.auth);
881
+ }
882
+ /**
883
+ * Obtain the {@link ConvexHttpClient}'s URL to its backend.
884
+ * @deprecated Use url, which returns the url without /api at the end.
885
+ *
886
+ * @returns The URL to the Convex backend, including the client's API version.
887
+ */
888
+ backendUrl() {
889
+ return `${this.address}/api`;
890
+ }
891
+ /**
892
+ * Return the address for this client, useful for creating a new client.
893
+ *
894
+ * Not guaranteed to match the address with which this client was constructed:
895
+ * it may be canonicalized.
896
+ */
897
+ get url() {
898
+ return this.address;
899
+ }
900
+ /**
901
+ * Set the authentication token to be used for subsequent queries and mutations.
902
+ *
903
+ * Should be called whenever the token changes (i.e. due to expiration and refresh).
904
+ *
905
+ * @param value - JWT-encoded OpenID Connect identity token.
906
+ */
907
+ setAuth(value) {
908
+ this.clearAuth();
909
+ this.auth = value;
910
+ }
911
+ /**
912
+ * Set admin auth token to allow calling internal queries, mutations, and actions
913
+ * and acting as an identity.
914
+ *
915
+ * @internal
916
+ */
917
+ setAdminAuth(token, actingAsIdentity) {
918
+ this.clearAuth();
919
+ if (actingAsIdentity !== void 0) {
920
+ const bytes = new TextEncoder().encode(JSON.stringify(actingAsIdentity));
921
+ const actingAsIdentityEncoded = btoa(String.fromCodePoint(...bytes));
922
+ this.adminAuth = `${token}:${actingAsIdentityEncoded}`;
923
+ } else this.adminAuth = token;
924
+ }
925
+ /**
926
+ * Clear the current authentication token if set.
927
+ */
928
+ clearAuth() {
929
+ this.auth = void 0;
930
+ this.adminAuth = void 0;
931
+ }
932
+ /**
933
+ * Sets whether the result log lines should be printed on the console or not.
934
+ *
935
+ * @internal
936
+ */
937
+ setDebug(debug) {
938
+ this.debug = debug;
939
+ }
940
+ /**
941
+ * Used to customize the fetch behavior in some runtimes.
942
+ *
943
+ * @internal
944
+ */
945
+ setFetchOptions(fetchOptions) {
946
+ this.fetchOptions = fetchOptions;
947
+ }
948
+ /**
949
+ * This API is experimental: it may change or disappear.
950
+ *
951
+ * Execute a Convex query function at the same timestamp as every other
952
+ * consistent query execution run by this HTTP client.
953
+ *
954
+ * This doesn't make sense for long-lived ConvexHttpClients as Convex
955
+ * backends can read a limited amount into the past: beyond 30 seconds
956
+ * in the past may not be available.
957
+ *
958
+ * Create a new client to use a consistent time.
959
+ *
960
+ * @param name - The name of the query.
961
+ * @param args - The arguments object for the query. If this is omitted,
962
+ * the arguments will be `{}`.
963
+ * @returns A promise of the query's result.
964
+ *
965
+ * @deprecated This API is experimental: it may change or disappear.
966
+ */
967
+ async consistentQuery(query, ...args) {
968
+ const queryArgs = parseArgs$1(args[0]);
969
+ const timestampPromise = this.getTimestamp();
970
+ return await this.queryInner(query, queryArgs, { timestampPromise });
971
+ }
972
+ async getTimestamp() {
973
+ if (this.encodedTsPromise) return this.encodedTsPromise;
974
+ return this.encodedTsPromise = this.getTimestampInner();
975
+ }
976
+ async getTimestampInner() {
977
+ const localFetch = this.fetch || fetch;
978
+ const headers = {
979
+ "Content-Type": "application/json",
980
+ "Convex-Client": `npm-${version}`
981
+ };
982
+ const response = await localFetch(`${this.address}/api/query_ts`, {
983
+ ...this.fetchOptions,
984
+ method: "POST",
985
+ headers
986
+ });
987
+ if (!response.ok) throw new Error(await response.text());
988
+ const { ts } = await response.json();
989
+ return ts;
990
+ }
991
+ /**
992
+ * Execute a Convex query function.
993
+ *
994
+ * @param name - The name of the query.
995
+ * @param args - The arguments object for the query. If this is omitted,
996
+ * the arguments will be `{}`.
997
+ * @returns A promise of the query's result.
998
+ */
999
+ async query(query, ...args) {
1000
+ const queryArgs = parseArgs$1(args[0]);
1001
+ return await this.queryInner(query, queryArgs, {});
1002
+ }
1003
+ async queryInner(query, queryArgs, options) {
1004
+ const name = getFunctionName(query);
1005
+ const args = [convexToJson(queryArgs)];
1006
+ const headers = {
1007
+ "Content-Type": "application/json",
1008
+ "Convex-Client": `npm-${version}`
1009
+ };
1010
+ if (this.adminAuth) headers["Authorization"] = `Convex ${this.adminAuth}`;
1011
+ else if (this.auth) headers["Authorization"] = `Bearer ${this.auth}`;
1012
+ const localFetch = this.fetch || fetch;
1013
+ const timestamp = options.timestampPromise ? await options.timestampPromise : void 0;
1014
+ const body = JSON.stringify({
1015
+ path: name,
1016
+ format: "convex_encoded_json",
1017
+ args,
1018
+ ...timestamp ? { ts: timestamp } : {}
1019
+ });
1020
+ const response = await localFetch(timestamp ? `${this.address}/api/query_at_ts` : `${this.address}/api/query`, {
1021
+ ...this.fetchOptions,
1022
+ body,
1023
+ method: "POST",
1024
+ headers
1025
+ });
1026
+ if (!response.ok && response.status !== 560) throw new Error(await response.text());
1027
+ const respJSON = await response.json();
1028
+ if (this.debug) for (const line of respJSON.logLines ?? []) logForFunction(this.logger, "info", "query", name, line);
1029
+ switch (respJSON.status) {
1030
+ case "success": return jsonToConvex(respJSON.value);
1031
+ case "error":
1032
+ if (respJSON.errorData !== void 0) throw forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage));
1033
+ throw new Error(respJSON.errorMessage);
1034
+ default: throw new Error(`Invalid response: ${JSON.stringify(respJSON)}`);
1035
+ }
1036
+ }
1037
+ async mutationInner(mutation, mutationArgs) {
1038
+ const name = getFunctionName(mutation);
1039
+ const body = JSON.stringify({
1040
+ path: name,
1041
+ format: "convex_encoded_json",
1042
+ args: [convexToJson(mutationArgs)]
1043
+ });
1044
+ const headers = {
1045
+ "Content-Type": "application/json",
1046
+ "Convex-Client": `npm-${version}`
1047
+ };
1048
+ if (this.adminAuth) headers["Authorization"] = `Convex ${this.adminAuth}`;
1049
+ else if (this.auth) headers["Authorization"] = `Bearer ${this.auth}`;
1050
+ const response = await (this.fetch || fetch)(`${this.address}/api/mutation`, {
1051
+ ...this.fetchOptions,
1052
+ body,
1053
+ method: "POST",
1054
+ headers
1055
+ });
1056
+ if (!response.ok && response.status !== 560) throw new Error(await response.text());
1057
+ const respJSON = await response.json();
1058
+ if (this.debug) for (const line of respJSON.logLines ?? []) logForFunction(this.logger, "info", "mutation", name, line);
1059
+ switch (respJSON.status) {
1060
+ case "success": return jsonToConvex(respJSON.value);
1061
+ case "error":
1062
+ if (respJSON.errorData !== void 0) throw forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage));
1063
+ throw new Error(respJSON.errorMessage);
1064
+ default: throw new Error(`Invalid response: ${JSON.stringify(respJSON)}`);
1065
+ }
1066
+ }
1067
+ async processMutationQueue() {
1068
+ if (this.isProcessingQueue) return;
1069
+ this.isProcessingQueue = true;
1070
+ while (this.mutationQueue.length > 0) {
1071
+ const { mutation, args, resolve, reject } = this.mutationQueue.shift();
1072
+ try {
1073
+ resolve(await this.mutationInner(mutation, args));
1074
+ } catch (error) {
1075
+ reject(error);
1076
+ }
1077
+ }
1078
+ this.isProcessingQueue = false;
1079
+ }
1080
+ enqueueMutation(mutation, args) {
1081
+ return new Promise((resolve, reject) => {
1082
+ this.mutationQueue.push({
1083
+ mutation,
1084
+ args,
1085
+ resolve,
1086
+ reject
1087
+ });
1088
+ this.processMutationQueue();
1089
+ });
1090
+ }
1091
+ /**
1092
+ * Execute a Convex mutation function. Mutations are queued by default.
1093
+ *
1094
+ * @param name - The name of the mutation.
1095
+ * @param args - The arguments object for the mutation. If this is omitted,
1096
+ * the arguments will be `{}`.
1097
+ * @param options - An optional object containing
1098
+ * @returns A promise of the mutation's result.
1099
+ */
1100
+ async mutation(mutation, ...args) {
1101
+ const [fnArgs, options] = args;
1102
+ const mutationArgs = parseArgs$1(fnArgs);
1103
+ if (!options?.skipQueue) return await this.enqueueMutation(mutation, mutationArgs);
1104
+ else return await this.mutationInner(mutation, mutationArgs);
1105
+ }
1106
+ /**
1107
+ * Execute a Convex action function. Actions are not queued.
1108
+ *
1109
+ * @param name - The name of the action.
1110
+ * @param args - The arguments object for the action. If this is omitted,
1111
+ * the arguments will be `{}`.
1112
+ * @returns A promise of the action's result.
1113
+ */
1114
+ async action(action, ...args) {
1115
+ const actionArgs = parseArgs$1(args[0]);
1116
+ const name = getFunctionName(action);
1117
+ const body = JSON.stringify({
1118
+ path: name,
1119
+ format: "convex_encoded_json",
1120
+ args: [convexToJson(actionArgs)]
1121
+ });
1122
+ const headers = {
1123
+ "Content-Type": "application/json",
1124
+ "Convex-Client": `npm-${version}`
1125
+ };
1126
+ if (this.adminAuth) headers["Authorization"] = `Convex ${this.adminAuth}`;
1127
+ else if (this.auth) headers["Authorization"] = `Bearer ${this.auth}`;
1128
+ const response = await (this.fetch || fetch)(`${this.address}/api/action`, {
1129
+ ...this.fetchOptions,
1130
+ body,
1131
+ method: "POST",
1132
+ headers
1133
+ });
1134
+ if (!response.ok && response.status !== 560) throw new Error(await response.text());
1135
+ const respJSON = await response.json();
1136
+ if (this.debug) for (const line of respJSON.logLines ?? []) logForFunction(this.logger, "info", "action", name, line);
1137
+ switch (respJSON.status) {
1138
+ case "success": return jsonToConvex(respJSON.value);
1139
+ case "error":
1140
+ if (respJSON.errorData !== void 0) throw forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage));
1141
+ throw new Error(respJSON.errorMessage);
1142
+ default: throw new Error(`Invalid response: ${JSON.stringify(respJSON)}`);
1143
+ }
1144
+ }
1145
+ /**
1146
+ * Execute a Convex function of an unknown type. These function calls are not queued.
1147
+ *
1148
+ * @param name - The name of the function.
1149
+ * @param args - The arguments object for the function. If this is omitted,
1150
+ * the arguments will be `{}`.
1151
+ * @returns A promise of the function's result.
1152
+ *
1153
+ * @internal
1154
+ */
1155
+ async function(anyFunction, componentPath, ...args) {
1156
+ const functionArgs = parseArgs$1(args[0]);
1157
+ const name = typeof anyFunction === "string" ? anyFunction : getFunctionName(anyFunction);
1158
+ const body = JSON.stringify({
1159
+ componentPath,
1160
+ path: name,
1161
+ format: "convex_encoded_json",
1162
+ args: convexToJson(functionArgs)
1163
+ });
1164
+ const headers = {
1165
+ "Content-Type": "application/json",
1166
+ "Convex-Client": `npm-${version}`
1167
+ };
1168
+ if (this.adminAuth) headers["Authorization"] = `Convex ${this.adminAuth}`;
1169
+ else if (this.auth) headers["Authorization"] = `Bearer ${this.auth}`;
1170
+ const response = await (this.fetch || fetch)(`${this.address}/api/function`, {
1171
+ ...this.fetchOptions,
1172
+ body,
1173
+ method: "POST",
1174
+ headers
1175
+ });
1176
+ if (!response.ok && response.status !== 560) throw new Error(await response.text());
1177
+ const respJSON = await response.json();
1178
+ if (this.debug) for (const line of respJSON.logLines ?? []) logForFunction(this.logger, "info", "any", name, line);
1179
+ switch (respJSON.status) {
1180
+ case "success": return jsonToConvex(respJSON.value);
1181
+ case "error":
1182
+ if (respJSON.errorData !== void 0) throw forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage));
1183
+ throw new Error(respJSON.errorMessage);
1184
+ default: throw new Error(`Invalid response: ${JSON.stringify(respJSON)}`);
1185
+ }
1186
+ }
1187
+ };
1188
+ function forwardErrorData(errorData, error) {
1189
+ error.data = jsonToConvex(errorData);
1190
+ return error;
1191
+ }
1192
+ //#endregion
1193
+ //#region src/remote.ts
1194
+ var RemoteError = class extends Error {
1195
+ code;
1196
+ constructor(message, code) {
1197
+ super(message);
1198
+ this.code = code;
1199
+ }
1200
+ };
1201
+ /** The message from a Convex error, with a stable code. */
1202
+ function wrap(error) {
1203
+ const data = error?.data;
1204
+ if (data?.message) return new RemoteError(data.message, data.code ?? "error");
1205
+ const message = error instanceof Error ? error.message : String(error);
1206
+ if (/ArgumentValidationError|Value does not match/.test(message)) return new RemoteError("That is not a valid comment ID", "invalid");
1207
+ return new RemoteError(message, "network");
1208
+ }
1209
+ async function call(fn) {
1210
+ try {
1211
+ return await fn();
1212
+ } catch (error) {
1213
+ throw wrap(error);
1214
+ }
1215
+ }
1216
+ async function postJson(path, body) {
1217
+ let res;
1218
+ try {
1219
+ res = await fetch(`${CONVEX_SITE_URL}${path}`, {
1220
+ method: "POST",
1221
+ headers: { "Content-Type": "application/json" },
1222
+ body: JSON.stringify(body)
1223
+ });
1224
+ } catch (error) {
1225
+ throw new RemoteError(`Could not reach Nuni (${error instanceof Error ? error.message : String(error)})`, "network");
1226
+ }
1227
+ const data = await res.json().catch(() => ({}));
1228
+ return {
1229
+ status: res.status,
1230
+ data
1231
+ };
1232
+ }
1233
+ function createRemote() {
1234
+ const client = new ConvexHttpClient(CONVEX_URL, {
1235
+ skipConvexDeploymentUrlCheck: !/\.convex\.cloud\/?$/.test(CONVEX_URL),
1236
+ logger: false
1237
+ });
1238
+ return {
1239
+ async startLogin(publicId, clientName) {
1240
+ const { status, data } = await postJson("/cli/login/start", {
1241
+ publicId,
1242
+ client: clientName
1243
+ });
1244
+ if (status !== 201) throw new RemoteError(String(data.message ?? `Sign-in failed (${status})`), String(data.code ?? "error"));
1245
+ return data;
1246
+ },
1247
+ async pollLogin(deviceSecret) {
1248
+ const { status, data } = await postJson("/cli/login/poll", { deviceSecret });
1249
+ if (status === 429) return { status: "slow_down" };
1250
+ if (status >= 500) throw new RemoteError(String(data.message ?? "Server error"), "server");
1251
+ return data;
1252
+ },
1253
+ whoami: (publicId, token) => call(() => client.query(api.sessions.validate, {
1254
+ publicId,
1255
+ sessionToken: token
1256
+ })),
1257
+ logout: (token) => call(async () => {
1258
+ await client.mutation(api.sessions.revokeOwn, { sessionToken: token });
1259
+ }),
1260
+ listComments: (publicId, token, options = {}) => call(async () => {
1261
+ return await client.query(api.comments.listForAgent, {
1262
+ publicId,
1263
+ sessionToken: token,
1264
+ ...options
1265
+ });
1266
+ }),
1267
+ getComment: (publicId, token, id) => call(async () => {
1268
+ return await client.query(api.comments.getForOwner, {
1269
+ publicId,
1270
+ id,
1271
+ sessionToken: token
1272
+ });
1273
+ }),
1274
+ setStatus: (token, id, status) => call(async () => {
1275
+ const fn = status === "resolved" ? api.comments.resolve : api.comments.reopen;
1276
+ await client.mutation(fn, {
1277
+ id,
1278
+ sessionToken: token
1279
+ });
1280
+ }),
1281
+ reply: (publicId, token, id, body) => call(async () => {
1282
+ await client.mutation(api.replies.replyAsOwner, {
1283
+ publicId,
1284
+ commentId: id,
1285
+ body,
1286
+ sessionToken: token
1287
+ });
1288
+ })
1289
+ };
1290
+ }
1291
+ //#endregion
1292
+ //#region src/mcp.ts
1293
+ /**
1294
+ * A Model Context Protocol server over stdio (newline-delimited JSON-RPC),
1295
+ * so coding agents can read and resolve comments:
1296
+ *
1297
+ * claude mcp add nuni -- npx -y @nuniapp/cli@latest mcp
1298
+ *
1299
+ * Tools only. Small on purpose, with no dependencies: initialize, ping,
1300
+ * tools/list and tools/call.
1301
+ */
1302
+ const PROTOCOL_VERSIONS = [
1303
+ "2025-11-25",
1304
+ "2025-06-18",
1305
+ "2025-03-26",
1306
+ "2024-11-05"
1307
+ ];
1308
+ const LOGIN$1 = `npx ${PACKAGES.cli}@latest login`;
1309
+ var ToolError = class extends Error {};
1310
+ const text = (value) => ({ content: [{
1311
+ type: "text",
1312
+ text: value
1313
+ }] });
1314
+ function str(args, name) {
1315
+ const value = args[name];
1316
+ if (typeof value !== "string" || !value.trim()) throw new ToolError(`"${name}" is required`);
1317
+ return value.trim();
1318
+ }
1319
+ async function fetchImage(url) {
1320
+ try {
1321
+ const res = await fetch(url);
1322
+ if (!res.ok) return null;
1323
+ const mimeType = res.headers.get("Content-Type")?.split(";")[0] ?? "image/webp";
1324
+ return {
1325
+ data: Buffer.from(await res.arrayBuffer()).toString("base64"),
1326
+ mimeType
1327
+ };
1328
+ } catch {
1329
+ return null;
1330
+ }
1331
+ }
1332
+ function createMcpServer(options) {
1333
+ const { remote } = options;
1334
+ let project = null;
1335
+ function session() {
1336
+ try {
1337
+ project ??= resolveProject(options.project, options.cwd).id;
1338
+ } catch (error) {
1339
+ if (error instanceof ProjectError) throw new ToolError(error.message);
1340
+ throw error;
1341
+ }
1342
+ const credential = credentialFor(project);
1343
+ if (!credential) throw new ToolError(`Not signed in to the Nuni project ${project}. Ask the user to run \`${LOGIN$1}\` in this project (it opens the Nuni dashboard to approve), then try again.`);
1344
+ return {
1345
+ publicId: project,
1346
+ token: credential.token
1347
+ };
1348
+ }
1349
+ const tools = [
1350
+ {
1351
+ name: "list_comments",
1352
+ title: "List Nuni comments",
1353
+ description: "List feedback comments left on the live site with Nuni, newest first. Each has an id, the page, the author and the element it is pinned to. Use get_comment for the full context.",
1354
+ inputSchema: {
1355
+ type: "object",
1356
+ properties: {
1357
+ status: {
1358
+ type: "string",
1359
+ enum: ["open", "resolved"],
1360
+ description: "Defaults to open."
1361
+ },
1362
+ page: {
1363
+ type: "string",
1364
+ description: "Only comments on this path, for example /pricing."
1365
+ },
1366
+ limit: {
1367
+ type: "integer",
1368
+ minimum: 1,
1369
+ maximum: 50
1370
+ }
1371
+ },
1372
+ additionalProperties: false
1373
+ },
1374
+ annotations: { readOnlyHint: true },
1375
+ async run(args) {
1376
+ const { publicId, token } = session();
1377
+ const status = args.status === "resolved" ? "resolved" : "open";
1378
+ const page = await remote.listComments(publicId, token, {
1379
+ status,
1380
+ path: typeof args.page === "string" ? args.page : void 0,
1381
+ limit: typeof args.limit === "number" ? args.limit : 20
1382
+ });
1383
+ return text(formatCommentList(page.comments, {
1384
+ status,
1385
+ more: Boolean(page.cursor),
1386
+ detailHint: "Call get_comment with an id for the element, DOM, styles, console errors, failed requests and screenshot."
1387
+ }));
1388
+ }
1389
+ },
1390
+ {
1391
+ name: "get_comment",
1392
+ title: "Get a Nuni comment",
1393
+ description: "One comment with everything needed to fix it: the comment, the page link, the element (text, selectors, React component), its HTML and styles, console errors, failed requests and a screenshot.",
1394
+ inputSchema: {
1395
+ type: "object",
1396
+ properties: { id: {
1397
+ type: "string",
1398
+ description: "The comment id."
1399
+ } },
1400
+ required: ["id"],
1401
+ additionalProperties: false
1402
+ },
1403
+ annotations: { readOnlyHint: true },
1404
+ async run(args) {
1405
+ const { publicId, token } = session();
1406
+ const comment = await remote.getComment(publicId, token, str(args, "id"));
1407
+ if (!comment) throw new ToolError("No comment with that id in this project");
1408
+ const content = [{
1409
+ type: "text",
1410
+ text: buildCommentPrompt(comment)
1411
+ }];
1412
+ if (comment.screenshotUrl) {
1413
+ const image = await (options.fetchImage ?? fetchImage)(comment.screenshotUrl);
1414
+ if (image) content.push({
1415
+ type: "image",
1416
+ ...image
1417
+ });
1418
+ }
1419
+ return { content };
1420
+ }
1421
+ },
1422
+ {
1423
+ name: "reply_to_comment",
1424
+ title: "Reply to a Nuni comment",
1425
+ description: "Reply in a comment's thread as the project owner, for example to ask a question or say what changed. Everyone on the site can read replies.",
1426
+ inputSchema: {
1427
+ type: "object",
1428
+ properties: {
1429
+ id: {
1430
+ type: "string",
1431
+ description: "The comment id."
1432
+ },
1433
+ body: {
1434
+ type: "string",
1435
+ description: "The reply."
1436
+ }
1437
+ },
1438
+ required: ["id", "body"],
1439
+ additionalProperties: false
1440
+ },
1441
+ async run(args) {
1442
+ const { publicId, token } = session();
1443
+ const id = str(args, "id");
1444
+ await remote.reply(publicId, token, id, str(args, "body"));
1445
+ return text(`Replied to ${id}.`);
1446
+ }
1447
+ },
1448
+ {
1449
+ name: "resolve_comment",
1450
+ title: "Resolve a Nuni comment",
1451
+ description: "Mark a comment as resolved once the change it asks for is made. Visitors see it as resolved on the site. Add a note to say what changed; it is posted as a reply.",
1452
+ inputSchema: {
1453
+ type: "object",
1454
+ properties: {
1455
+ id: { type: "string" },
1456
+ note: {
1457
+ type: "string",
1458
+ description: "Optional: what was changed, posted as a reply first."
1459
+ }
1460
+ },
1461
+ required: ["id"],
1462
+ additionalProperties: false
1463
+ },
1464
+ annotations: { idempotentHint: true },
1465
+ async run(args) {
1466
+ const { publicId, token } = session();
1467
+ const id = str(args, "id");
1468
+ await remote.setStatus(token, id, "resolved");
1469
+ if (typeof args.note === "string" && args.note.trim()) await remote.reply(publicId, token, id, args.note.trim());
1470
+ return text(`Resolved ${id}.`);
1471
+ }
1472
+ },
1473
+ {
1474
+ name: "reopen_comment",
1475
+ title: "Reopen a Nuni comment",
1476
+ description: "Reopen a resolved comment.",
1477
+ inputSchema: {
1478
+ type: "object",
1479
+ properties: { id: { type: "string" } },
1480
+ required: ["id"],
1481
+ additionalProperties: false
1482
+ },
1483
+ annotations: { idempotentHint: true },
1484
+ async run(args) {
1485
+ const { token } = session();
1486
+ const id = str(args, "id");
1487
+ await remote.setStatus(token, id, "open");
1488
+ return text(`Reopened ${id}.`);
1489
+ }
1490
+ }
1491
+ ];
1492
+ async function callTool(params) {
1493
+ const tool = tools.find((t) => t.name === params.name);
1494
+ if (!tool) throw new RpcError(-32602, `Unknown tool: ${String(params.name)}`);
1495
+ try {
1496
+ return await tool.run(params.arguments ?? {});
1497
+ } catch (error) {
1498
+ if (error instanceof ToolError || error instanceof RemoteError) return {
1499
+ content: [{
1500
+ type: "text",
1501
+ text: error instanceof RemoteError && error.code === "unauthenticated" ? `${error.message}. Ask the user to run \`${LOGIN$1}\` in this project, then try again.` : error.message
1502
+ }],
1503
+ isError: true
1504
+ };
1505
+ throw error;
1506
+ }
1507
+ }
1508
+ /** Handle one message. Notifications get no response (null). */
1509
+ async function handle(message) {
1510
+ const id = message.id ?? null;
1511
+ const isNotification = message.id === void 0;
1512
+ try {
1513
+ let result;
1514
+ switch (message.method) {
1515
+ case "initialize": {
1516
+ const requested = String(message.params?.protocolVersion ?? "");
1517
+ result = {
1518
+ protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
1519
+ capabilities: { tools: {} },
1520
+ serverInfo: {
1521
+ name: "nuni",
1522
+ title: "Nuni",
1523
+ version: options.version
1524
+ },
1525
+ instructions: "Nuni comments are feedback pinned to elements on the live site. List them, read one with its full context, fix the code, then resolve it."
1526
+ };
1527
+ break;
1528
+ }
1529
+ case "ping":
1530
+ result = {};
1531
+ break;
1532
+ case "tools/list":
1533
+ result = { tools: tools.map(({ run: _run, ...tool }) => tool) };
1534
+ break;
1535
+ case "tools/call":
1536
+ result = await callTool(message.params ?? {});
1537
+ break;
1538
+ default:
1539
+ if (isNotification) return null;
1540
+ throw new RpcError(-32601, `Method not found: ${message.method}`);
1541
+ }
1542
+ return isNotification ? null : {
1543
+ jsonrpc: "2.0",
1544
+ id,
1545
+ result
1546
+ };
1547
+ } catch (error) {
1548
+ if (isNotification) return null;
1549
+ const rpc = error instanceof RpcError ? error : new RpcError(-32603, error instanceof Error ? error.message : String(error));
1550
+ return {
1551
+ jsonrpc: "2.0",
1552
+ id,
1553
+ error: {
1554
+ code: rpc.code,
1555
+ message: rpc.message
1556
+ }
1557
+ };
1558
+ }
1559
+ }
1560
+ return {
1561
+ handle,
1562
+ tools
1563
+ };
1564
+ }
1565
+ var RpcError = class extends Error {
1566
+ code;
1567
+ constructor(code, message) {
1568
+ super(message);
1569
+ this.code = code;
1570
+ }
1571
+ };
1572
+ /** Serve on stdin/stdout until stdin closes. Logs go to stderr only. */
1573
+ async function serveStdio(server) {
1574
+ const write = (message) => {
1575
+ process.stdout.write(`${JSON.stringify(message)}\n`);
1576
+ };
1577
+ const pending = /* @__PURE__ */ new Set();
1578
+ const lines = createInterface({
1579
+ input: process.stdin,
1580
+ crlfDelay: Infinity
1581
+ });
1582
+ for await (const line of lines) {
1583
+ if (!line.trim()) continue;
1584
+ let message;
1585
+ try {
1586
+ message = JSON.parse(line);
1587
+ } catch {
1588
+ write({
1589
+ jsonrpc: "2.0",
1590
+ id: null,
1591
+ error: {
1592
+ code: -32700,
1593
+ message: "Parse error"
1594
+ }
1595
+ });
1596
+ continue;
1597
+ }
1598
+ for (const request of Array.isArray(message) ? message : [message]) {
1599
+ const task = server.handle(request).then((response) => {
1600
+ if (response) write(response);
1601
+ });
1602
+ pending.add(task);
1603
+ task.finally(() => pending.delete(task));
1604
+ }
1605
+ }
1606
+ await Promise.all(pending);
1607
+ }
1608
+ //#endregion
1609
+ //#region src/commands.ts
1610
+ const LOGIN = `npx ${PACKAGES.cli}@latest login`;
1611
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1612
+ var CommandError = class extends Error {};
1613
+ function signedIn(ctx) {
1614
+ const project = resolveProject(ctx.project, ctx.cwd);
1615
+ const credential = credentialFor(project.id);
1616
+ if (!credential) throw new CommandError(`Not signed in to ${project.id}. Run \`${LOGIN}\` first.`);
1617
+ return {
1618
+ publicId: project.id,
1619
+ token: credential.token
1620
+ };
1621
+ }
1622
+ /** Try to open the approval page, quietly: there may be no browser at all. */
1623
+ function openInBrowser(url) {
1624
+ if (!process.stdout.isTTY || process.env.CI || process.env.SSH_CONNECTION) return;
1625
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
1626
+ "/c",
1627
+ "start",
1628
+ "",
1629
+ url
1630
+ ]] : ["xdg-open", [url]];
1631
+ try {
1632
+ const child = spawn(command, args, {
1633
+ stdio: "ignore",
1634
+ detached: true
1635
+ });
1636
+ child.on("error", () => {});
1637
+ child.unref();
1638
+ } catch {}
1639
+ }
1640
+ async function login(ctx) {
1641
+ const project = resolveProject(ctx.project, ctx.cwd);
1642
+ const client = `nuni-cli ${ctx.version} (${process.platform}; node ${process.versions.node})`;
1643
+ const start = await ctx.remote.startLogin(project.id, client);
1644
+ const url = `${APP_URL}/dashboard/cli?code=${start.userCode}`;
1645
+ if (ctx.json) ctx.out(JSON.stringify({
1646
+ status: "waiting",
1647
+ url,
1648
+ code: start.userCode
1649
+ }));
1650
+ else ctx.out(`Signing in to ${project.id} (${project.source}).
1651
+
1652
+ Open ${url}
1653
+ Code ${start.userCode}
1654
+
1655
+ Check that the code matches, then approve. Waiting…`);
1656
+ (ctx.openBrowser ?? openInBrowser)(url);
1657
+ let delay = ctx.pollMs ?? 2e3;
1658
+ for (;;) {
1659
+ await sleep(delay);
1660
+ const result = await ctx.remote.pollLogin(start.deviceSecret);
1661
+ if (result.status === "pending") continue;
1662
+ if (result.status === "slow_down") {
1663
+ delay *= 2;
1664
+ continue;
1665
+ }
1666
+ if (result.status === "approved") {
1667
+ saveCredential(project.id, {
1668
+ token: result.token,
1669
+ ownerName: result.ownerName,
1670
+ projectName: result.projectName,
1671
+ expiresAt: result.expiresAt
1672
+ });
1673
+ if (ctx.json) ctx.out(JSON.stringify({
1674
+ status: "signed_in",
1675
+ project: project.id,
1676
+ owner: result.ownerName
1677
+ }));
1678
+ else ctx.out(`Signed in as ${result.ownerName} to ${result.projectName}. Saved to ${credentialsPath()}`);
1679
+ return 0;
1680
+ }
1681
+ const message = result.status === "denied" ? "The sign-in was denied." : "The code expired. Run login again.";
1682
+ if (ctx.json) ctx.out(JSON.stringify({
1683
+ status: result.status,
1684
+ message
1685
+ }));
1686
+ else ctx.err(message);
1687
+ return 1;
1688
+ }
1689
+ }
1690
+ async function logout(ctx) {
1691
+ const project = resolveProject(ctx.project, ctx.cwd);
1692
+ const credential = credentialFor(project.id);
1693
+ if (credential && !process.env.NUNI_TOKEN) await ctx.remote.logout(credential.token).catch(() => {});
1694
+ const removed = removeCredential(project.id);
1695
+ ctx.out(removed ? `Signed out of ${project.id}.` : `Not signed in to ${project.id}.`);
1696
+ return 0;
1697
+ }
1698
+ async function whoami(ctx) {
1699
+ const project = resolveProject(ctx.project, ctx.cwd);
1700
+ const credential = credentialFor(project.id);
1701
+ const result = credential ? await ctx.remote.whoami(project.id, credential.token) : { valid: false };
1702
+ if (ctx.json) ctx.out(JSON.stringify({
1703
+ project: project.id,
1704
+ signedIn: result.valid,
1705
+ owner: result.ownerName ?? null
1706
+ }));
1707
+ else if (result.valid) ctx.out(`Signed in to ${project.id} as ${result.ownerName}.`);
1708
+ else ctx.out(`Not signed in to ${project.id}. Run \`${LOGIN}\`.`);
1709
+ return result.valid ? 0 : 1;
1710
+ }
1711
+ async function listComments(ctx, options) {
1712
+ const { publicId, token } = signedIn(ctx);
1713
+ if (options.status && options.status !== "open" && options.status !== "resolved") throw new CommandError(`--status must be open or resolved`);
1714
+ const status = options.status ?? "open";
1715
+ const limit = options.limit ? Number(options.limit) : 20;
1716
+ if (!Number.isInteger(limit) || limit < 1) throw new CommandError(`--limit must be a positive number`);
1717
+ const page = await ctx.remote.listComments(publicId, token, {
1718
+ status,
1719
+ path: options.page,
1720
+ limit
1721
+ });
1722
+ if (ctx.json) {
1723
+ ctx.out(JSON.stringify(page, null, 2));
1724
+ return 0;
1725
+ }
1726
+ ctx.out(formatCommentList(page.comments, {
1727
+ status,
1728
+ more: Boolean(page.cursor),
1729
+ detailHint: `Full context: npx ${PACKAGES.cli}@latest comment <id>`
1730
+ }));
1731
+ return 0;
1732
+ }
1733
+ async function showComment(ctx, id, options) {
1734
+ if (!id) throw new CommandError("Pass a comment id: comment <id>");
1735
+ const { publicId, token } = signedIn(ctx);
1736
+ const comment = await ctx.remote.getComment(publicId, token, id);
1737
+ if (!comment) throw new CommandError(`No comment ${id} in ${publicId}.`);
1738
+ let saved = null;
1739
+ if (options.saveScreenshot && comment.screenshotUrl) {
1740
+ const image = await fetchImage(comment.screenshotUrl);
1741
+ if (image) {
1742
+ const dir = resolve(ctx.cwd, options.saveScreenshot);
1743
+ mkdirSync(dir, { recursive: true });
1744
+ const ext = image.mimeType.split("/")[1] ?? "webp";
1745
+ saved = join(dir, `nuni-${comment._id}.${ext}`);
1746
+ writeFileSync(saved, Buffer.from(image.data, "base64"));
1747
+ }
1748
+ }
1749
+ if (ctx.json) {
1750
+ ctx.out(JSON.stringify({
1751
+ ...comment,
1752
+ screenshotFile: saved
1753
+ }, null, 2));
1754
+ return 0;
1755
+ }
1756
+ ctx.out(buildCommentPrompt(comment));
1757
+ if (saved) ctx.out(`\nScreenshot saved to ${saved}`);
1758
+ return 0;
1759
+ }
1760
+ async function reply(ctx, id, body) {
1761
+ if (!id || !body?.trim()) throw new CommandError("Pass a comment id and a message: reply <id> \"message\"");
1762
+ const { publicId, token } = signedIn(ctx);
1763
+ await ctx.remote.reply(publicId, token, id, body);
1764
+ ctx.out(ctx.json ? JSON.stringify({
1765
+ id,
1766
+ replied: true
1767
+ }) : `Replied to ${id}.`);
1768
+ return 0;
1769
+ }
1770
+ async function setStatus(ctx, id, status, note) {
1771
+ const verb = status === "resolved" ? "resolve" : "reopen";
1772
+ if (!id) throw new CommandError(`Pass a comment id: ${verb} <id>`);
1773
+ const { publicId, token } = signedIn(ctx);
1774
+ await ctx.remote.setStatus(token, id, status);
1775
+ if (note?.trim()) await ctx.remote.reply(publicId, token, id, note);
1776
+ ctx.out(ctx.json ? JSON.stringify({
1777
+ id,
1778
+ status
1779
+ }) : `${status === "resolved" ? "Resolved" : "Reopened"} ${id}.`);
1780
+ return 0;
1781
+ }
1782
+ //#endregion
72
1783
  //#region src/detect.ts
73
1784
  function readPackage(cwd) {
74
1785
  try {
@@ -285,7 +1996,7 @@ init({ project: "${projectId}" })`;
285
1996
  }
286
1997
  //#endregion
287
1998
  //#region src/cli.ts
288
- const VERSION = version;
1999
+ const VERSION = version$1;
289
2000
  const HELP = `nuni ${VERSION}: pinned comments on your live site
290
2001
 
291
2002
  Usage
@@ -293,38 +2004,81 @@ Usage
293
2004
  npx @nuniapp/cli@latest prompt [--id <project-id>]
294
2005
  npx @nuniapp/cli@latest id
295
2006
 
2007
+ npx @nuniapp/cli@latest login sign in to this project's comments
2008
+ npx @nuniapp/cli@latest comments [--status open|resolved] [--page /path] [--limit 20]
2009
+ npx @nuniapp/cli@latest comment <id> [--save-screenshot <dir>]
2010
+ npx @nuniapp/cli@latest reply <id> "message"
2011
+ npx @nuniapp/cli@latest resolve <id> [--note "what changed"]
2012
+ npx @nuniapp/cli@latest reopen <id>
2013
+ npx @nuniapp/cli@latest whoami | logout
2014
+ npx @nuniapp/cli@latest mcp MCP server for coding agents (stdio)
2015
+
296
2016
  Commands
297
- init Generate a project ID and print the install steps for this project
298
- prompt Print the ready-made prompt for Codex, Claude Code, Cursor, etc.
299
- id Print a new project ID
2017
+ init Generate a project ID and print the install steps for this project
2018
+ prompt Print the ready-made prompt for Codex, Claude Code, Cursor, etc.
2019
+ id Print a new project ID
2020
+ login Approve this terminal in the Nuni dashboard (works over SSH too)
2021
+ comments List comments, newest first
2022
+ comment One comment with the element, DOM, styles, console and screenshot
2023
+ reply Reply in a comment's thread, as the owner
2024
+ resolve Mark a comment resolved (with an optional note); reopen does the opposite
2025
+ mcp Run the MCP server: claude mcp add nuni -- npx -y @nuniapp/cli@latest mcp
2026
+
2027
+ The project comes from --project, $NUNI_PROJECT, or the nuni_ ID in your code.
2028
+ Add --json to any command for machine-readable output.
300
2029
 
301
2030
  Docs: ${DOCS_URL}`;
302
2031
  const bold = (s) => process.stdout.isTTY ? `\x1b[1m${s}\x1b[22m` : s;
303
2032
  const dim = (s) => process.stdout.isTTY ? `\x1b[2m${s}\x1b[22m` : s;
304
- function run(argv) {
305
- const { values, positionals } = parseArgs({
306
- args: argv,
307
- allowPositionals: true,
308
- options: {
309
- id: { type: "string" },
310
- cwd: { type: "string" },
311
- json: {
312
- type: "boolean",
313
- default: false
314
- },
315
- help: {
316
- type: "boolean",
317
- short: "h",
318
- default: false
319
- },
320
- version: {
321
- type: "boolean",
322
- short: "v",
323
- default: false
324
- }
325
- }
326
- });
2033
+ const OPTIONS = {
2034
+ id: { type: "string" },
2035
+ project: {
2036
+ type: "string",
2037
+ short: "p"
2038
+ },
2039
+ cwd: { type: "string" },
2040
+ status: { type: "string" },
2041
+ page: { type: "string" },
2042
+ limit: { type: "string" },
2043
+ "save-screenshot": { type: "string" },
2044
+ note: { type: "string" },
2045
+ json: {
2046
+ type: "boolean",
2047
+ default: false
2048
+ },
2049
+ help: {
2050
+ type: "boolean",
2051
+ short: "h",
2052
+ default: false
2053
+ },
2054
+ version: {
2055
+ type: "boolean",
2056
+ short: "v",
2057
+ default: false
2058
+ }
2059
+ };
2060
+ const ACCOUNT_COMMANDS = /* @__PURE__ */ new Set([
2061
+ "login",
2062
+ "logout",
2063
+ "whoami",
2064
+ "comments",
2065
+ "comment",
2066
+ "reply",
2067
+ "resolve",
2068
+ "reopen",
2069
+ "mcp"
2070
+ ]);
2071
+ async function run(argv, options = {}) {
2072
+ let parsed;
2073
+ try {
2074
+ parsed = parse(argv);
2075
+ } catch (error) {
2076
+ console.error(error instanceof Error ? error.message : String(error));
2077
+ return 1;
2078
+ }
2079
+ const { values, positionals } = parsed;
327
2080
  const command = positionals[0] ?? "init";
2081
+ if (ACCOUNT_COMMANDS.has(command) && !values.help) return runAccountCommand(command, positionals, values, options);
328
2082
  if (values.version) {
329
2083
  console.log(VERSION);
330
2084
  return 0;
@@ -389,8 +2143,58 @@ ${dim(`Docs: ${DOCS_URL}`)}
389
2143
  `);
390
2144
  return 0;
391
2145
  }
2146
+ function parse(argv) {
2147
+ return parseArgs({
2148
+ args: argv,
2149
+ allowPositionals: true,
2150
+ options: OPTIONS
2151
+ });
2152
+ }
2153
+ async function runAccountCommand(command, positionals, values, options) {
2154
+ const arg = positionals[1];
2155
+ const ctx = {
2156
+ remote: options.remote ?? createRemote(),
2157
+ version: VERSION,
2158
+ cwd: resolve(values.cwd ?? process.cwd()),
2159
+ project: values.project ?? values.id,
2160
+ json: values.json,
2161
+ out: options.out ?? ((line) => console.log(line)),
2162
+ err: options.err ?? ((line) => console.error(line)),
2163
+ pollMs: options.pollMs,
2164
+ openBrowser: options.openBrowser
2165
+ };
2166
+ try {
2167
+ switch (command) {
2168
+ case "login": return await login(ctx);
2169
+ case "logout": return await logout(ctx);
2170
+ case "whoami": return await whoami(ctx);
2171
+ case "comments": return await listComments(ctx, values);
2172
+ case "comment": return await showComment(ctx, arg, { saveScreenshot: values["save-screenshot"] });
2173
+ case "reply": return await reply(ctx, arg, positionals[2]);
2174
+ case "resolve": return await setStatus(ctx, arg, "resolved", values.note);
2175
+ case "reopen": return await setStatus(ctx, arg, "open");
2176
+ default:
2177
+ await serveStdio(createMcpServer({
2178
+ remote: ctx.remote,
2179
+ version: VERSION,
2180
+ project: ctx.project,
2181
+ cwd: ctx.cwd
2182
+ }));
2183
+ return 0;
2184
+ }
2185
+ } catch (error) {
2186
+ if (error instanceof CommandError || error instanceof ProjectError || error instanceof RemoteError) {
2187
+ const hint = error instanceof RemoteError && error.code === "unauthenticated" ? ` Run \`npx ${PACKAGES.cli}@latest login\`.` : "";
2188
+ ctx.err(`${error.message.replace(/\.$/, "")}.${hint}`);
2189
+ return 1;
2190
+ }
2191
+ throw error;
2192
+ }
2193
+ }
392
2194
  //#endregion
393
2195
  //#region src/index.ts
394
- process.exitCode = run(process.argv.slice(2));
2196
+ run(process.argv.slice(2)).then((code) => {
2197
+ process.exitCode = code;
2198
+ });
395
2199
  //#endregion
396
2200
  export {};