@addorimprove/prompt 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/prompt.js +556 -0
  2. package/package.json +18 -0
package/dist/prompt.js ADDED
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/router.ts
6
+ var VALUE_FLAGS = new Set(["q", "name", "parent", "format", "file", "f", "base-url"]);
7
+ function parseArgs(argv) {
8
+ if (argv.length === 0)
9
+ return { command: "help", positionals: [], flags: {} };
10
+ const [command, ...rest] = argv;
11
+ const positionals = [];
12
+ const flags = {};
13
+ for (let i = 0;i < rest.length; i++) {
14
+ const tok = rest[i];
15
+ if (tok.startsWith("--")) {
16
+ const body = tok.slice(2);
17
+ const eq = body.indexOf("=");
18
+ if (eq >= 0) {
19
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
20
+ continue;
21
+ }
22
+ if (VALUE_FLAGS.has(body) && i + 1 < rest.length && !rest[i + 1].startsWith("-")) {
23
+ flags[body] = rest[++i];
24
+ } else
25
+ flags[body] = true;
26
+ } else if (tok.startsWith("-") && tok.length > 1) {
27
+ const name = tok.slice(1);
28
+ if (VALUE_FLAGS.has(name) && i + 1 < rest.length && !rest[i + 1].startsWith("-")) {
29
+ flags[name] = rest[++i];
30
+ } else
31
+ flags[name] = true;
32
+ } else {
33
+ positionals.push(tok);
34
+ }
35
+ }
36
+ return { command, positionals, flags };
37
+ }
38
+ function flagStr(flags, ...names) {
39
+ for (const n of names) {
40
+ const v = flags[n];
41
+ if (typeof v === "string")
42
+ return v;
43
+ }
44
+ return;
45
+ }
46
+
47
+ // src/api.ts
48
+ class ApiError extends Error {
49
+ status;
50
+ code;
51
+ constructor(status, code, message) {
52
+ super(message);
53
+ this.status = status;
54
+ this.code = code;
55
+ this.name = "ApiError";
56
+ }
57
+ }
58
+
59
+ class ApiClient {
60
+ cfg;
61
+ constructor(cfg) {
62
+ this.cfg = cfg;
63
+ }
64
+ async request(method, path, body) {
65
+ const f = this.cfg.fetchImpl ?? fetch;
66
+ const headers = { "x-api-key": this.cfg.apiKey };
67
+ if (body !== undefined)
68
+ headers["content-type"] = "application/json";
69
+ const res = await f(`${this.cfg.baseUrl}/api/v1/me${path}`, {
70
+ method,
71
+ headers,
72
+ body: body === undefined ? undefined : JSON.stringify(body)
73
+ });
74
+ if (!res.ok) {
75
+ let code = "http", message = res.statusText;
76
+ try {
77
+ const j = await res.json();
78
+ if (j?.error) {
79
+ code = j.error.code ?? code;
80
+ message = j.error.message ?? message;
81
+ }
82
+ } catch {}
83
+ throw new ApiError(res.status, code, message);
84
+ }
85
+ if (res.status === 204)
86
+ return;
87
+ return res.json();
88
+ }
89
+ getMe() {
90
+ return this.request("GET", "");
91
+ }
92
+ listDocs(q) {
93
+ const qs = q ? `?q=${encodeURIComponent(q)}` : "";
94
+ return this.request("GET", `/docs${qs}`);
95
+ }
96
+ getDoc(id) {
97
+ return this.request("GET", `/docs/${id}`);
98
+ }
99
+ getVersion(id, label) {
100
+ return this.request("GET", `/docs/${id}/versions/${encodeURIComponent(label)}`);
101
+ }
102
+ getComments(id, label) {
103
+ return this.request("GET", `/docs/${id}/versions/${encodeURIComponent(label)}/comments`);
104
+ }
105
+ createDoc(body) {
106
+ return this.request("POST", "/docs", body);
107
+ }
108
+ addVersion(id, body) {
109
+ return this.request("POST", `/docs/${id}/versions`, body);
110
+ }
111
+ deleteCurrentKey() {
112
+ return this.request("DELETE", "/keys/current");
113
+ }
114
+ }
115
+ function describeApiError(e) {
116
+ switch (e.status) {
117
+ case 401:
118
+ return { message: "Not logged in. Run 'prompt login'.", exitCode: 4 };
119
+ case 404:
120
+ return { message: "Not found (or not yours).", exitCode: 1 };
121
+ case 409:
122
+ return { message: `Label conflict — ${e.message}`, exitCode: 1 };
123
+ case 400:
124
+ return { message: e.message, exitCode: 1 };
125
+ default:
126
+ return { message: `Request failed (${e.status}): ${e.message}`, exitCode: 1 };
127
+ }
128
+ }
129
+
130
+ // src/config.ts
131
+ import { homedir } from "node:os";
132
+ import { join } from "node:path";
133
+ import { mkdirSync, readFileSync, writeFileSync, rmSync, chmodSync, existsSync } from "node:fs";
134
+ var DEFAULT_BASE_URL = "https://app.photosharingapp.com";
135
+ function configDir() {
136
+ const xdg = process.env.XDG_CONFIG_HOME;
137
+ return xdg ? join(xdg, "prompt") : join(homedir(), ".config", "prompt");
138
+ }
139
+ function configPath() {
140
+ return join(configDir(), "config.json");
141
+ }
142
+ function readConfig() {
143
+ const p = configPath();
144
+ if (!existsSync(p))
145
+ return null;
146
+ try {
147
+ return JSON.parse(readFileSync(p, "utf8"));
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ function writeConfig(cfg) {
153
+ mkdirSync(configDir(), { recursive: true });
154
+ const p = configPath();
155
+ writeFileSync(p, JSON.stringify(cfg, null, 2), { mode: 384 });
156
+ chmodSync(p, 384);
157
+ }
158
+ function saveCredential(cfg) {
159
+ writeConfig({ ...readConfig() ?? {}, ...cfg });
160
+ }
161
+ function clearCredential() {
162
+ rmSync(configPath(), { force: true });
163
+ }
164
+ function resolveApiKey() {
165
+ return process.env.MD_PROMPT_API_KEY ?? readConfig()?.apiKey ?? null;
166
+ }
167
+ function resolveBaseUrl(flag) {
168
+ return flag ?? process.env.MD_PROMPT_BASE_URL ?? readConfig()?.baseUrl ?? DEFAULT_BASE_URL;
169
+ }
170
+
171
+ // src/render.ts
172
+ import { spawnSync } from "node:child_process";
173
+ import { mkdtempSync, writeFileSync as writeFileSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
174
+ import { tmpdir } from "node:os";
175
+ import { join as join2 } from "node:path";
176
+ import { createInterface } from "node:readline";
177
+ function renderDocList(docs) {
178
+ if (docs.length === 0)
179
+ return "No documents.";
180
+ return docs.map((d) => `${String(d.id).padEnd(6)} ${d.name}${d.openCommentCount ? ` (${d.openCommentCount} open comments)` : ""}`).join(`
181
+ `);
182
+ }
183
+ function renderDocTree(doc) {
184
+ const latest = doc.latest?.label;
185
+ const lines = doc.versions.map((v) => {
186
+ const tags = [
187
+ v.commentCount ? `${v.commentCount} comments` : "",
188
+ v.label === latest ? "latest" : ""
189
+ ].filter(Boolean).join(", ");
190
+ return ` ${v.label.padEnd(10)} ${tags}`;
191
+ });
192
+ return `#${doc.id} ${doc.name}
193
+ ${lines.join(`
194
+ `)}`;
195
+ }
196
+ function renderComments(comments) {
197
+ if (comments.length === 0)
198
+ return "No comments.";
199
+ return `${comments.length} comment(s):
200
+ ${JSON.stringify(comments, null, 2)}`;
201
+ }
202
+ function openEditor(template, ext = "md") {
203
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
204
+ const dir = mkdtempSync(join2(tmpdir(), "prompts-edit-"));
205
+ const file = join2(dir, `draft.${ext}`);
206
+ writeFileSync2(file, template, "utf8");
207
+ try {
208
+ const r = spawnSync(editor, [file], { stdio: "inherit" });
209
+ if (r.status !== 0)
210
+ throw new Error("editor exited non-zero");
211
+ return readFileSync2(file, "utf8");
212
+ } finally {
213
+ rmSync2(dir, { recursive: true, force: true });
214
+ }
215
+ }
216
+ function confirm(question) {
217
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
218
+ return new Promise((resolve) => {
219
+ rl.question(`${question} [y/N] `, (ans) => {
220
+ rl.close();
221
+ resolve(/^y(es)?$/i.test(ans.trim()));
222
+ });
223
+ });
224
+ }
225
+
226
+ // src/auth.ts
227
+ import { createServer } from "node:http";
228
+ import { spawn } from "node:child_process";
229
+
230
+ // src/pkce.ts
231
+ import { randomBytes, createHash } from "node:crypto";
232
+ function randomToken(bytes = 32) {
233
+ return randomBytes(bytes).toString("base64url");
234
+ }
235
+ function pkceChallengeS256(verifier) {
236
+ return createHash("sha256").update(verifier).digest("base64url");
237
+ }
238
+ function generatePkce() {
239
+ const verifier = randomToken(32);
240
+ return { verifier, challenge: pkceChallengeS256(verifier), method: "S256" };
241
+ }
242
+
243
+ // src/auth.ts
244
+ function buildAuthorizeUrl(p) {
245
+ const u = new URL("/cli/auth", p.baseUrl);
246
+ u.searchParams.set("state", p.state);
247
+ u.searchParams.set("code_challenge", p.challenge);
248
+ u.searchParams.set("code_challenge_method", "S256");
249
+ u.searchParams.set("redirect_uri", p.redirectUri);
250
+ return u.toString();
251
+ }
252
+ function parseCallback(reqUrl) {
253
+ const u = new URL(reqUrl, "http://127.0.0.1");
254
+ return {
255
+ code: u.searchParams.get("code") ?? undefined,
256
+ state: u.searchParams.get("state") ?? undefined,
257
+ error: u.searchParams.get("error") ?? undefined
258
+ };
259
+ }
260
+ async function exchangeCodeForKey(p) {
261
+ const f = p.fetchImpl ?? fetch;
262
+ const res = await f(`${p.baseUrl}/api/v1/cli/token`, {
263
+ method: "POST",
264
+ headers: { "content-type": "application/json" },
265
+ body: JSON.stringify({ code: p.code, code_verifier: p.codeVerifier, redirect_uri: p.redirectUri })
266
+ });
267
+ if (!res.ok) {
268
+ let message = res.statusText;
269
+ try {
270
+ const j = await res.json();
271
+ if (j?.error?.message)
272
+ message = j.error.message;
273
+ } catch {}
274
+ throw new Error(`login failed: ${message}`);
275
+ }
276
+ const { key } = await res.json();
277
+ return key;
278
+ }
279
+ function openBrowser(url) {
280
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
281
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
282
+ try {
283
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
284
+ } catch {}
285
+ }
286
+ var CALLBACK_TIMEOUT_MS = 2 * 60 * 1000;
287
+ async function login(baseUrlFlag) {
288
+ const baseUrl = resolveBaseUrl(baseUrlFlag);
289
+ const { verifier, challenge } = generatePkce();
290
+ const state = randomToken(16);
291
+ const { code, redirectUri } = await new Promise((resolve, reject) => {
292
+ let redirectUri2 = "";
293
+ const timer = setTimeout(() => {
294
+ server.close();
295
+ reject(new Error("login timed out (no browser callback within 2 minutes)"));
296
+ }, CALLBACK_TIMEOUT_MS);
297
+ timer.unref();
298
+ const server = createServer((req, res) => {
299
+ const { code: code2, state: gotState, error } = parseCallback(req.url ?? "");
300
+ const finish = (status, msg) => {
301
+ res.writeHead(status, { "content-type": "text/html" });
302
+ res.end(`<html><body style="font-family:sans-serif"><h2>${msg}</h2><p>You can close this tab and return to the terminal.</p></body></html>`);
303
+ };
304
+ const fail = (err, status, msg) => {
305
+ finish(status, msg);
306
+ clearTimeout(timer);
307
+ server.close();
308
+ reject(err);
309
+ };
310
+ if (error)
311
+ return fail(new Error(`authorization ${error}`), 400, "Authorization was cancelled.");
312
+ if (gotState !== state)
313
+ return fail(new Error("state mismatch"), 400, "State mismatch — aborting.");
314
+ if (!code2)
315
+ return fail(new Error("missing code"), 400, "Missing authorization code.");
316
+ finish(200, "Logged in to Prompt CLI.");
317
+ clearTimeout(timer);
318
+ server.close();
319
+ resolve({ code: code2, redirectUri: redirectUri2 });
320
+ });
321
+ server.on("error", (e) => {
322
+ clearTimeout(timer);
323
+ reject(e);
324
+ });
325
+ server.listen(0, "127.0.0.1", () => {
326
+ const addr = server.address();
327
+ if (addr === null || typeof addr === "string") {
328
+ clearTimeout(timer);
329
+ server.close();
330
+ return reject(new Error("could not bind loopback port"));
331
+ }
332
+ redirectUri2 = `http://127.0.0.1:${addr.port}/callback`;
333
+ const authorizeUrl = buildAuthorizeUrl({ baseUrl, state, challenge, redirectUri: redirectUri2 });
334
+ console.log("Opening your browser to authorize the Prompt CLI...");
335
+ console.log(`If it does not open, visit:
336
+ ${authorizeUrl}
337
+ `);
338
+ openBrowser(authorizeUrl);
339
+ });
340
+ });
341
+ const key = await exchangeCodeForKey({ baseUrl, code, codeVerifier: verifier, redirectUri });
342
+ saveCredential({ apiKey: key, baseUrl: baseUrlFlag ?? readConfig()?.baseUrl });
343
+ const client = new ApiClient({ baseUrl, apiKey: key });
344
+ const me = await client.getMe();
345
+ saveCredential({ user: me });
346
+ console.log(`Logged in as ${me.name} <${me.email}>.`);
347
+ }
348
+ async function whoami(baseUrlFlag) {
349
+ const apiKey = resolveApiKey();
350
+ if (!apiKey) {
351
+ console.error("Not logged in. Run 'prompt login'.");
352
+ process.exitCode = 4;
353
+ return;
354
+ }
355
+ const client = new ApiClient({ baseUrl: resolveBaseUrl(baseUrlFlag), apiKey });
356
+ const me = await client.getMe();
357
+ console.log(`${me.name} <${me.email}> (id: ${me.id})`);
358
+ }
359
+ async function logout(baseUrlFlag) {
360
+ const apiKey = resolveApiKey();
361
+ if (apiKey) {
362
+ try {
363
+ const client = new ApiClient({ baseUrl: resolveBaseUrl(baseUrlFlag), apiKey });
364
+ await client.deleteCurrentKey();
365
+ } catch {}
366
+ }
367
+ clearCredential();
368
+ console.log("Logged out.");
369
+ }
370
+
371
+ // src/commands.ts
372
+ function requireClient(flags) {
373
+ const apiKey = resolveApiKey();
374
+ if (!apiKey) {
375
+ console.error("Not logged in. Run 'prompt login'.");
376
+ process.exitCode = 4;
377
+ return null;
378
+ }
379
+ return new ApiClient({ baseUrl: resolveBaseUrl(flagStr(flags, "base-url")), apiKey });
380
+ }
381
+ function out(json, data, human) {
382
+ console.log(json ? JSON.stringify(data, null, 2) : human);
383
+ }
384
+ async function readContent(flags, template) {
385
+ const file = flagStr(flags, "file", "f");
386
+ if (file)
387
+ return (await import("node:fs")).readFileSync(file, "utf8");
388
+ return openEditor(template);
389
+ }
390
+ async function run(args) {
391
+ const json = flags(args, "json");
392
+ const baseFlag = flagStr(args.flags, "base-url");
393
+ switch (args.command) {
394
+ case "login":
395
+ return login(baseFlag);
396
+ case "logout":
397
+ return logout(baseFlag);
398
+ case "whoami":
399
+ return whoami(baseFlag);
400
+ case "ls": {
401
+ const client = requireClient(args.flags);
402
+ if (!client)
403
+ return;
404
+ const { docs } = await client.listDocs(flagStr(args.flags, "q"));
405
+ return out(json, docs, renderDocList(docs));
406
+ }
407
+ case "view": {
408
+ const client = requireClient(args.flags);
409
+ if (!client)
410
+ return;
411
+ const id = Number(args.positionals[0]);
412
+ if (!Number.isInteger(id)) {
413
+ console.error("usage: prompt view <id> [label]");
414
+ process.exitCode = 1;
415
+ return;
416
+ }
417
+ const label = args.positionals[1];
418
+ if (label) {
419
+ const v = await client.getVersion(id, label);
420
+ return out(json, v, v.content);
421
+ }
422
+ const doc = await client.getDoc(id);
423
+ if (flags(args, "tree"))
424
+ return out(json, doc, renderDocTree(doc));
425
+ return out(json, doc, `${renderDocTree(doc)}
426
+
427
+ --- latest (${doc.latest?.label}) ---
428
+ ${doc.latest?.content ?? "(no versions)"}`);
429
+ }
430
+ case "comments": {
431
+ const client = requireClient(args.flags);
432
+ if (!client)
433
+ return;
434
+ const id = Number(args.positionals[0]);
435
+ const label = args.positionals[1];
436
+ if (!Number.isInteger(id) || !label) {
437
+ console.error("usage: prompt comments <id> <label>");
438
+ process.exitCode = 1;
439
+ return;
440
+ }
441
+ const { comments } = await client.getComments(id, label);
442
+ return out(json, comments, renderComments(comments));
443
+ }
444
+ case "new": {
445
+ const client = requireClient(args.flags);
446
+ if (!client)
447
+ return;
448
+ const name = flagStr(args.flags, "name");
449
+ if (!name) {
450
+ console.error("usage: prompt new --name <name> [-f file] [--format mdx|html]");
451
+ process.exitCode = 1;
452
+ return;
453
+ }
454
+ const format = flagStr(args.flags, "format") ?? "mdx";
455
+ const content = await readContent(args.flags, `# ${name}
456
+ `);
457
+ if (!flags(args, "yes") && !flags(args, "y") && !await confirm(`Create document "${name}"?`)) {
458
+ console.log("Aborted.");
459
+ return;
460
+ }
461
+ const r = await client.createDoc({ name, content, format });
462
+ return out(json, r, `Created #${r.id} (${r.label}).`);
463
+ }
464
+ case "iterate": {
465
+ const client = requireClient(args.flags);
466
+ if (!client)
467
+ return;
468
+ const id = Number(args.positionals[0]);
469
+ if (!Number.isInteger(id)) {
470
+ console.error("usage: prompt iterate <id> [--parent label] [-f file]");
471
+ process.exitCode = 1;
472
+ return;
473
+ }
474
+ let parent = flagStr(args.flags, "parent");
475
+ if (!parent) {
476
+ const doc = await client.getDoc(id);
477
+ parent = doc.latest?.label;
478
+ }
479
+ if (!parent) {
480
+ console.error("no parent version found; pass --parent <label>");
481
+ process.exitCode = 1;
482
+ return;
483
+ }
484
+ const format = flagStr(args.flags, "format") ?? "mdx";
485
+ const content = await readContent(args.flags, "");
486
+ if (!flags(args, "yes") && !flags(args, "y") && !await confirm(`Add iterate version off ${parent} on #${id}?`)) {
487
+ console.log("Aborted.");
488
+ return;
489
+ }
490
+ const r = await client.addVersion(id, { intent: "iterate", parentLabel: parent, content, format });
491
+ return out(json, r, `Added ${r.label}.`);
492
+ }
493
+ case "branch": {
494
+ const client = requireClient(args.flags);
495
+ if (!client)
496
+ return;
497
+ const id = Number(args.positionals[0]);
498
+ const parent = args.positionals[1];
499
+ if (!Number.isInteger(id) || !parent) {
500
+ console.error("usage: prompt branch <id> <parentLabel> [-f file]");
501
+ process.exitCode = 1;
502
+ return;
503
+ }
504
+ const format = flagStr(args.flags, "format") ?? "mdx";
505
+ const content = await readContent(args.flags, "");
506
+ if (!flags(args, "yes") && !flags(args, "y") && !await confirm(`Branch off ${parent} on #${id}?`)) {
507
+ console.log("Aborted.");
508
+ return;
509
+ }
510
+ const r = await client.addVersion(id, { intent: "branch", parentLabel: parent, content, format });
511
+ return out(json, r, `Branched → ${r.label}.`);
512
+ }
513
+ case "help":
514
+ default:
515
+ console.log(HELP);
516
+ }
517
+ }
518
+ function flags(args, name) {
519
+ return args.flags[name] === true;
520
+ }
521
+ var HELP = `prompt — terminal client for the markdown-notes app
522
+
523
+ Usage: prompt <command> [args] [flags] (or: npx @addorimprove/prompt <command>)
524
+
525
+ login Authorize via browser (PKCE) and store a key
526
+ logout Revoke the stored key and remove it locally
527
+ whoami Show the logged-in account
528
+
529
+ ls [-q <query>] [--json] List/search your documents
530
+ view <id> [label] [--tree] Show a doc's version tree + latest, or a version's content
531
+ comments <id> <label> Show a version's comments
532
+
533
+ new --name <n> [-f file] [--format mdx|html] Create a document
534
+ iterate <id> [--parent label] [-f file] Add a version on the same line
535
+ branch <id> <parentLabel> [-f file] Fork a new line from a version
536
+
537
+ Global flags: --base-url <url>, --json, -y/--yes
538
+ Env: MD_PROMPT_API_KEY, MD_PROMPT_BASE_URL (shared with the 'prompt' skill)`;
539
+
540
+ // bin/prompt.ts
541
+ async function main() {
542
+ const args = parseArgs(process.argv.slice(2));
543
+ try {
544
+ await run(args);
545
+ } catch (err) {
546
+ if (err instanceof ApiError) {
547
+ const { message, exitCode } = describeApiError(err);
548
+ console.error(message);
549
+ process.exitCode = exitCode;
550
+ } else {
551
+ console.error(err instanceof Error ? err.message : String(err));
552
+ process.exitCode = 1;
553
+ }
554
+ }
555
+ }
556
+ main();
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@addorimprove/prompt",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": { "prompt": "dist/prompt.js" },
6
+ "files": ["dist"],
7
+ "scripts": {
8
+ "test": "bun test",
9
+ "build": "bun build ./bin/prompt.ts --target=node --outfile=dist/prompt.js && chmod +x dist/prompt.js",
10
+ "dev": "bun run ./bin/prompt.ts"
11
+ },
12
+ "engines": { "node": ">=18" },
13
+ "devDependencies": {
14
+ "@types/node": "^20",
15
+ "bun-types": "^1.3.8",
16
+ "typescript": "^5"
17
+ }
18
+ }