@nuniapp/cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.mjs +1862 -58
- 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 {
|
|
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.
|
|
9
|
+
var version$1 = "0.1.5";
|
|
7
10
|
//#endregion
|
|
8
|
-
//#region ../shared/src/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
298
|
-
prompt
|
|
299
|
-
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
2196
|
+
run(process.argv.slice(2)).then((code) => {
|
|
2197
|
+
process.exitCode = code;
|
|
2198
|
+
});
|
|
395
2199
|
//#endregion
|
|
396
2200
|
export {};
|