slimwiki 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.
package/dist/index.js ADDED
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import open from "open";
8
+ import { setTimeout as sleep } from "timers/promises";
9
+
10
+ // src/lib/api-client.ts
11
+ var createApiClient = (opts) => {
12
+ const { apiBase, token } = opts;
13
+ const base = apiBase.replace(/\/$/, "");
14
+ const buildUrl = (path, query) => {
15
+ const url = new URL(path.startsWith("/") ? `${base}${path}` : path);
16
+ if (query) {
17
+ for (const [k, v] of Object.entries(query)) {
18
+ if (v !== void 0 && v !== null && v !== "") {
19
+ url.searchParams.set(k, String(v));
20
+ }
21
+ }
22
+ }
23
+ return url.toString();
24
+ };
25
+ const request = async (method, path, init) => {
26
+ const headers = {
27
+ Accept: "application/json"
28
+ };
29
+ if (token) headers.Authorization = `Bearer ${token}`;
30
+ const fetchInit = { method, headers };
31
+ if (init?.body !== void 0) {
32
+ headers["Content-Type"] = "application/json";
33
+ fetchInit.body = JSON.stringify(init.body);
34
+ }
35
+ const url = buildUrl(path, init?.query);
36
+ const res = await fetch(url, fetchInit);
37
+ if (res.status === 204) return void 0;
38
+ const contentType = res.headers.get("content-type") ?? "";
39
+ const isJson = contentType.includes("application/json");
40
+ const payload = isJson ? await res.json().catch(() => ({})) : await res.text();
41
+ if (!res.ok) {
42
+ const message = typeof payload === "object" && (payload?.error || payload?.message) || typeof payload === "string" && payload || `HTTP ${res.status} ${res.statusText}`;
43
+ if (res.status === 401) {
44
+ throw new Error(
45
+ "Not authenticated. Run `slimwiki auth login` or set SLIMWIKI_TOKEN."
46
+ );
47
+ }
48
+ throw new Error(message);
49
+ }
50
+ return payload;
51
+ };
52
+ return {
53
+ apiBase: base,
54
+ request,
55
+ get: (path, query) => request("GET", path, { query }),
56
+ post: (path, body) => request("POST", path, { body }),
57
+ delete: (path) => request("DELETE", path)
58
+ };
59
+ };
60
+ var fromAuth = (auth) => createApiClient({ apiBase: auth.apiBase, token: auth.token });
61
+
62
+ // src/lib/config.ts
63
+ import envPaths from "env-paths";
64
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
65
+ import { dirname, join } from "path";
66
+ var PATHS = envPaths("slimwiki", { suffix: "" });
67
+ var CREDENTIALS_FILE = join(PATHS.config, "credentials.json");
68
+ var DEFAULT_API_BASE = "https://slimwiki.com";
69
+ var credentialsPath = () => CREDENTIALS_FILE;
70
+ var readCredentials = async () => {
71
+ try {
72
+ const raw = await readFile(CREDENTIALS_FILE, "utf8");
73
+ return JSON.parse(raw);
74
+ } catch (err) {
75
+ if (err?.code === "ENOENT") return null;
76
+ throw err;
77
+ }
78
+ };
79
+ var writeCredentials = async (creds) => {
80
+ await mkdir(dirname(CREDENTIALS_FILE), { recursive: true });
81
+ await writeFile(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), {
82
+ mode: 384
83
+ });
84
+ };
85
+ var deleteCredentials = async () => {
86
+ try {
87
+ await rm(CREDENTIALS_FILE);
88
+ } catch (err) {
89
+ if (err?.code !== "ENOENT") throw err;
90
+ }
91
+ };
92
+ var resolveAuth = async (opts = {}) => {
93
+ const envToken = process.env.SLIMWIKI_TOKEN;
94
+ if (envToken) {
95
+ return {
96
+ apiBase: opts.apiBaseOverride ?? process.env.SLIMWIKI_API_BASE ?? DEFAULT_API_BASE,
97
+ token: envToken,
98
+ fromEnv: true,
99
+ credentials: null
100
+ };
101
+ }
102
+ const creds = await readCredentials();
103
+ if (!creds?.token) return null;
104
+ return {
105
+ apiBase: opts.apiBaseOverride ?? creds.apiBase ?? DEFAULT_API_BASE,
106
+ token: creds.token,
107
+ fromEnv: false,
108
+ credentials: creds
109
+ };
110
+ };
111
+ var requireAuth = async (opts = {}) => {
112
+ const auth = await resolveAuth(opts);
113
+ if (!auth) {
114
+ throw new Error(
115
+ "Not authenticated. Run `slimwiki auth login` or set SLIMWIKI_TOKEN."
116
+ );
117
+ }
118
+ return auth;
119
+ };
120
+
121
+ // src/lib/output.ts
122
+ var isHuman = (cmd) => {
123
+ return Boolean(cmd.optsWithGlobals().human);
124
+ };
125
+ var apiBaseOverride = (cmd) => {
126
+ const v = cmd.optsWithGlobals().apiBase;
127
+ return typeof v === "string" && v.length > 0 ? v : void 0;
128
+ };
129
+ var writeResult = (cmd, value, human) => {
130
+ if (isHuman(cmd) && human) {
131
+ process.stdout.write(human(value));
132
+ if (!human(value).endsWith("\n")) process.stdout.write("\n");
133
+ return;
134
+ }
135
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
136
+ };
137
+ var writeError = (err) => {
138
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error";
139
+ process.stderr.write(JSON.stringify({ error: message }) + "\n");
140
+ };
141
+ var exitWithError = (err, code = 1) => {
142
+ writeError(err);
143
+ process.exit(code);
144
+ };
145
+
146
+ // src/commands/auth.ts
147
+ var POLL_TIMEOUT_PADDING_MS = 5e3;
148
+ var runLogin = async (cmd, opts) => {
149
+ const apiBase = opts.apiBase ?? apiBaseOverride(cmd) ?? DEFAULT_API_BASE;
150
+ const client = createApiClient({ apiBase });
151
+ const init = await client.post("/api/v1/auth/cli/init");
152
+ process.stderr.write(
153
+ `
154
+ Open this URL in your browser to authorize the SlimWiki CLI:
155
+
156
+ ${init.verificationUrl}
157
+
158
+ Verification code (must match the one shown in the browser):
159
+
160
+ ${init.userCode}
161
+
162
+ `
163
+ );
164
+ open(init.verificationUrl).catch(() => void 0);
165
+ const intervalMs = Math.max(1, init.pollInterval) * 1e3;
166
+ const deadline = Date.now() + init.expiresIn * 1e3 + POLL_TIMEOUT_PADDING_MS;
167
+ let approved = null;
168
+ while (Date.now() < deadline) {
169
+ const result = await client.post("/api/v1/auth/cli/poll", {
170
+ deviceCode: init.deviceCode
171
+ });
172
+ if (result.status === "approved") {
173
+ approved = result;
174
+ break;
175
+ }
176
+ if (result.status === "expired") {
177
+ throw new Error(
178
+ "CLI authorization code has expired. Please re-run `slimwiki auth login`."
179
+ );
180
+ }
181
+ await sleep(intervalMs);
182
+ }
183
+ if (!approved) {
184
+ throw new Error(
185
+ "Timed out waiting for browser approval. Please re-run `slimwiki auth login`."
186
+ );
187
+ }
188
+ await writeCredentials({
189
+ apiBase,
190
+ token: approved.token,
191
+ expiresAt: approved.expiresAt,
192
+ user: approved.user,
193
+ account: approved.account,
194
+ wiki: approved.wiki
195
+ });
196
+ writeResult(
197
+ cmd,
198
+ {
199
+ status: "ok",
200
+ user: approved.user,
201
+ account: approved.account,
202
+ wiki: approved.wiki,
203
+ apiBase,
204
+ credentialsPath: credentialsPath()
205
+ },
206
+ (v) => `Logged in as ${v.user.email}
207
+ ` + (v.account ? `Account: ${v.account.slug}
208
+ ` : "") + (v.wiki ? `Wiki: ${v.wiki.slug}
209
+ ` : "") + `Saved to ${v.credentialsPath}
210
+ `
211
+ );
212
+ };
213
+ var runLogout = async (cmd) => {
214
+ const auth = await resolveAuth({ apiBaseOverride: apiBaseOverride(cmd) });
215
+ if (auth?.token && !auth.fromEnv) {
216
+ const client = fromAuth(auth);
217
+ await client.post("/api/auth/sign-out").catch(() => void 0);
218
+ }
219
+ await deleteCredentials();
220
+ writeResult(cmd, { status: "ok" }, () => "Logged out.\n");
221
+ };
222
+ var runStatus = async (cmd) => {
223
+ const auth = await resolveAuth({ apiBaseOverride: apiBaseOverride(cmd) });
224
+ if (!auth) {
225
+ writeResult(
226
+ cmd,
227
+ { authenticated: false },
228
+ () => "Not authenticated. Run `slimwiki auth login`.\n"
229
+ );
230
+ return;
231
+ }
232
+ let user = auth.credentials?.user ?? null;
233
+ let account = auth.credentials?.account ?? null;
234
+ let wiki = auth.credentials?.wiki ?? null;
235
+ if (auth.fromEnv) {
236
+ try {
237
+ const me = await fromAuth(auth).get("/api/v1/users/me");
238
+ user = me.user;
239
+ account = me.account ?? null;
240
+ wiki = me.wiki ?? null;
241
+ } catch {
242
+ }
243
+ }
244
+ writeResult(
245
+ cmd,
246
+ {
247
+ authenticated: true,
248
+ apiBase: auth.apiBase,
249
+ source: auth.fromEnv ? "env" : "credentials-file",
250
+ user,
251
+ account,
252
+ wiki
253
+ },
254
+ (v) => `Authenticated as ${v.user?.email ?? "(unknown)"}
255
+ API: ${v.apiBase}
256
+ Source: ${v.source}
257
+ ` + (v.account ? `Account: ${v.account.slug}
258
+ ` : "") + (v.wiki ? `Wiki: ${v.wiki.slug}
259
+ ` : "")
260
+ );
261
+ };
262
+ var registerAuthCommands = (program2) => {
263
+ const auth = program2.command("auth").description("Authenticate the CLI");
264
+ auth.command("login").description("Open a browser to authorize the CLI").option(
265
+ "--api-base <url>",
266
+ "SlimWiki API base URL (default https://slimwiki.com)"
267
+ ).action(async function() {
268
+ try {
269
+ await runLogin(this, this.opts());
270
+ } catch (err) {
271
+ exitWithError(err);
272
+ }
273
+ });
274
+ auth.command("logout").description("Revoke the CLI session and delete saved credentials").action(async function() {
275
+ try {
276
+ await runLogout(this);
277
+ } catch (err) {
278
+ exitWithError(err);
279
+ }
280
+ });
281
+ auth.command("status").description("Show the cached session details").action(async function() {
282
+ try {
283
+ await runStatus(this);
284
+ } catch (err) {
285
+ exitWithError(err);
286
+ }
287
+ });
288
+ };
289
+
290
+ // src/commands/account.ts
291
+ var formatTable = (rows) => {
292
+ if (rows.length === 0) return "No accounts found.\n";
293
+ const widths = {
294
+ slug: Math.max(4, ...rows.map((r) => r.slug.length)),
295
+ name: Math.max(4, ...rows.map((r) => (r.name ?? "").length))
296
+ };
297
+ const lines = [
298
+ `${"slug".padEnd(widths.slug)} ${"name".padEnd(widths.name)}`
299
+ ];
300
+ for (const r of rows) {
301
+ lines.push(
302
+ `${r.slug.padEnd(widths.slug)} ${(r.name ?? "").padEnd(widths.name)}`
303
+ );
304
+ }
305
+ return lines.join("\n") + "\n";
306
+ };
307
+ var registerAccountCommands = (program2) => {
308
+ const account = program2.command("account").description("Inspect SlimWiki accounts");
309
+ account.command("list").description("List the accounts the current user belongs to").action(async function() {
310
+ try {
311
+ const auth = await requireAuth({ apiBaseOverride: apiBaseOverride(this) });
312
+ const client = fromAuth(auth);
313
+ const rows = await client.get("/api/v1/accounts");
314
+ const stripped = rows.map((r) => ({
315
+ slug: r.slug,
316
+ name: r.name ?? null
317
+ }));
318
+ writeResult(this, stripped, formatTable);
319
+ } catch (err) {
320
+ exitWithError(err);
321
+ }
322
+ });
323
+ };
324
+
325
+ // src/lib/selectors.ts
326
+ var findAccountBySlug = async (client, slug) => {
327
+ const accounts = await client.get("/api/v1/accounts");
328
+ const match = accounts.find((a) => a.slug === slug);
329
+ if (!match) {
330
+ throw new Error(
331
+ `Account not found for slug "${slug}". Run \`slimwiki account list\` to see available accounts.`
332
+ );
333
+ }
334
+ return match;
335
+ };
336
+ var findWikiInAccount = async (client, accountId, slug) => {
337
+ const wikis = await client.get("/api/v1/wikis", {
338
+ accountId
339
+ });
340
+ const match = wikis.find((w) => w.slug === slug);
341
+ if (!match) {
342
+ throw new Error(
343
+ `Wiki not found for slug "${slug}" in this account. Run \`slimwiki wiki list\` to see available wikis.`
344
+ );
345
+ }
346
+ return match;
347
+ };
348
+ var resolveAccount = async (auth, client, flags) => {
349
+ const slug = flags.account ?? process.env.SLIMWIKI_ACCOUNT_SLUG ?? auth.credentials?.account?.slug;
350
+ if (slug) return findAccountBySlug(client, slug);
351
+ const me = await client.get("/api/v1/users/me");
352
+ if (!me.account) {
353
+ throw new Error(
354
+ "No account is associated with this session. Use --account or run `slimwiki account list`."
355
+ );
356
+ }
357
+ return {
358
+ id: me.account.id,
359
+ slug: me.account.slug,
360
+ name: me.account.name ?? null
361
+ };
362
+ };
363
+ var resolveWiki = async (auth, client, flags, account) => {
364
+ const account_ = account ?? await resolveAccount(auth, client, flags);
365
+ const slug = flags.wiki ?? process.env.SLIMWIKI_WIKI_SLUG ?? auth.credentials?.wiki?.slug;
366
+ if (slug) return findWikiInAccount(client, account_.id, slug);
367
+ const me = await client.get("/api/v1/users/me");
368
+ if (me.wiki && me.account) {
369
+ return {
370
+ id: me.wiki.id,
371
+ slug: me.wiki.slug,
372
+ name: me.wiki.name ?? null,
373
+ accountId: me.account.id
374
+ };
375
+ }
376
+ throw new Error(
377
+ "No default wiki found for this account. Use --wiki or run `slimwiki wiki list`."
378
+ );
379
+ };
380
+ var resolveParent = async (client, wikiId, reference) => {
381
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
382
+ reference
383
+ );
384
+ if (isUuid) return reference;
385
+ const matches = await client.get(`/api/v1/pages/wiki/${wikiId}`, { archived: false });
386
+ const page = matches.pages.find(
387
+ (p) => p.slug === reference || p.pageHash === reference
388
+ );
389
+ if (!page) {
390
+ throw new Error(`Parent page not found for "${reference}".`);
391
+ }
392
+ return page.id;
393
+ };
394
+
395
+ // src/commands/wiki.ts
396
+ var formatTable2 = (rows) => {
397
+ if (rows.length === 0) return "No wikis found.\n";
398
+ const widths = {
399
+ slug: Math.max(4, ...rows.map((r) => r.slug.length)),
400
+ name: Math.max(4, ...rows.map((r) => (r.name ?? "").length))
401
+ };
402
+ const lines = [
403
+ `${"slug".padEnd(widths.slug)} ${"name".padEnd(widths.name)}`
404
+ ];
405
+ for (const r of rows) {
406
+ lines.push(
407
+ `${r.slug.padEnd(widths.slug)} ${(r.name ?? "").padEnd(widths.name)}`
408
+ );
409
+ }
410
+ return lines.join("\n") + "\n";
411
+ };
412
+ var registerWikiCommands = (program2) => {
413
+ const wiki = program2.command("wiki").description("Inspect wikis");
414
+ wiki.command("list").description("List wikis in the current (or selected) account").option("--account <slug>", "Account slug").action(async function(opts) {
415
+ try {
416
+ const auth = await requireAuth({ apiBaseOverride: apiBaseOverride(this) });
417
+ const client = fromAuth(auth);
418
+ const account = await resolveAccount(auth, client, {
419
+ account: opts.account
420
+ });
421
+ const rows = await client.get("/api/v1/wikis", {
422
+ accountId: account.id
423
+ });
424
+ const stripped = rows.map((r) => ({
425
+ slug: r.slug,
426
+ name: r.name ?? null
427
+ }));
428
+ writeResult(this, stripped, formatTable2);
429
+ } catch (err) {
430
+ exitWithError(err);
431
+ }
432
+ });
433
+ };
434
+
435
+ // src/commands/page.ts
436
+ import { readFile as readFile2 } from "fs/promises";
437
+
438
+ // src/lib/tiptap-schema.ts
439
+ import { z } from "zod";
440
+ var HeadingAttrs = z.object({
441
+ level: z.union([z.literal(1), z.literal(2), z.literal(3)])
442
+ });
443
+ var ImageAttrs = z.object({
444
+ src: z.string().min(1),
445
+ alt: z.string().optional(),
446
+ title: z.string().optional()
447
+ }).passthrough();
448
+ var VideoAttrs = z.object({ src: z.string().min(1) }).passthrough();
449
+ var AttachmentAttrs = z.object({ src: z.string().min(1).optional(), name: z.string().optional() }).passthrough();
450
+ var CodeBlockAttrs = z.object({ language: z.string().nullable().optional() }).passthrough();
451
+ var TaskItemAttrs = z.object({ checked: z.boolean().optional() }).passthrough();
452
+ var MathAttrs = z.object({ latex: z.string() }).passthrough();
453
+ var PageMentionAttrs = z.object({ pageId: z.string(), pageSlug: z.string().optional() }).passthrough();
454
+ var LinkMarkAttrs = z.object({ href: z.string().min(1), target: z.string().optional() }).passthrough();
455
+ var TextStyleAttrs = z.object({ color: z.string().optional() }).passthrough();
456
+ var Mark = z.discriminatedUnion("type", [
457
+ z.object({ type: z.literal("bold") }).passthrough(),
458
+ z.object({ type: z.literal("italic") }).passthrough(),
459
+ z.object({ type: z.literal("underline") }).passthrough(),
460
+ z.object({ type: z.literal("strike") }).passthrough(),
461
+ z.object({ type: z.literal("code") }).passthrough(),
462
+ z.object({ type: z.literal("highlight") }).passthrough(),
463
+ z.object({ type: z.literal("link"), attrs: LinkMarkAttrs }).passthrough(),
464
+ z.object({ type: z.literal("textStyle"), attrs: TextStyleAttrs.optional() }).passthrough()
465
+ ]);
466
+ var Node = z.lazy(
467
+ () => z.union([
468
+ z.object({
469
+ type: z.literal("text"),
470
+ text: z.string(),
471
+ marks: z.array(Mark).optional()
472
+ }).passthrough(),
473
+ z.object({ type: z.literal("hardBreak") }).passthrough(),
474
+ z.object({
475
+ type: z.literal("paragraph"),
476
+ attrs: z.unknown().optional(),
477
+ content: z.array(Node).optional()
478
+ }).passthrough(),
479
+ z.object({
480
+ type: z.literal("heading"),
481
+ attrs: HeadingAttrs,
482
+ content: z.array(Node).optional()
483
+ }).passthrough(),
484
+ z.object({
485
+ type: z.literal("blockquote"),
486
+ content: z.array(Node).optional()
487
+ }).passthrough(),
488
+ z.object({
489
+ type: z.literal("bulletList"),
490
+ content: z.array(Node).optional()
491
+ }).passthrough(),
492
+ z.object({
493
+ type: z.literal("orderedList"),
494
+ attrs: z.object({ start: z.number().optional() }).passthrough().optional(),
495
+ content: z.array(Node).optional()
496
+ }).passthrough(),
497
+ z.object({
498
+ type: z.literal("listItem"),
499
+ content: z.array(Node).optional()
500
+ }).passthrough(),
501
+ z.object({
502
+ type: z.literal("taskList"),
503
+ content: z.array(Node).optional()
504
+ }).passthrough(),
505
+ z.object({
506
+ type: z.literal("taskItem"),
507
+ attrs: TaskItemAttrs.optional(),
508
+ content: z.array(Node).optional()
509
+ }).passthrough(),
510
+ z.object({
511
+ type: z.literal("codeBlock"),
512
+ attrs: CodeBlockAttrs.optional(),
513
+ content: z.array(Node).optional()
514
+ }).passthrough(),
515
+ z.object({ type: z.literal("horizontalRule") }).passthrough(),
516
+ z.object({ type: z.literal("image"), attrs: ImageAttrs }).passthrough(),
517
+ z.object({ type: z.literal("video"), attrs: VideoAttrs }).passthrough(),
518
+ z.object({ type: z.literal("attachment"), attrs: AttachmentAttrs }).passthrough(),
519
+ z.object({
520
+ type: z.literal("math"),
521
+ attrs: MathAttrs
522
+ }).passthrough(),
523
+ z.object({
524
+ type: z.literal("pageMention"),
525
+ attrs: PageMentionAttrs
526
+ }).passthrough(),
527
+ z.object({
528
+ type: z.literal("table"),
529
+ content: z.array(Node).optional()
530
+ }).passthrough(),
531
+ z.object({
532
+ type: z.literal("tableRow"),
533
+ content: z.array(Node).optional()
534
+ }).passthrough(),
535
+ z.object({
536
+ type: z.literal("tableCell"),
537
+ content: z.array(Node).optional()
538
+ }).passthrough(),
539
+ z.object({
540
+ type: z.literal("tableHeader"),
541
+ content: z.array(Node).optional()
542
+ }).passthrough()
543
+ ])
544
+ );
545
+ var TiptapDocSchema = z.object({
546
+ type: z.literal("doc"),
547
+ content: z.array(Node)
548
+ }).passthrough();
549
+
550
+ // src/commands/page.ts
551
+ var readStdin = async () => {
552
+ const chunks = [];
553
+ for await (const chunk of process.stdin) {
554
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
555
+ }
556
+ return Buffer.concat(chunks).toString("utf8");
557
+ };
558
+ var parseDoc = (raw) => {
559
+ let parsed;
560
+ try {
561
+ parsed = JSON.parse(raw);
562
+ } catch (err) {
563
+ throw new Error(`Could not parse JSON content: ${err?.message ?? "invalid JSON"}`);
564
+ }
565
+ const result = TiptapDocSchema.safeParse(parsed);
566
+ if (!result.success) {
567
+ const first = result.error.issues[0];
568
+ const path = first?.path?.length ? `at ${first.path.join(".")} ` : "";
569
+ throw new Error(
570
+ `Tiptap doc validation failed ${path}\u2014 ${first?.message ?? "schema mismatch"}`
571
+ );
572
+ }
573
+ return result.data;
574
+ };
575
+ var formatListTable = (payload) => {
576
+ const rows = "rows" in payload ? payload.rows : payload.pages;
577
+ if (rows.length === 0) return "No pages found.\n";
578
+ const widths = {
579
+ slug: Math.max(4, ...rows.map((r) => r.slug.length)),
580
+ title: Math.max(5, ...rows.map((r) => r.title.length))
581
+ };
582
+ const lines = [
583
+ `${"slug".padEnd(widths.slug)} ${"title".padEnd(widths.title)} pageHash`
584
+ ];
585
+ for (const r of rows) {
586
+ lines.push(
587
+ `${r.slug.padEnd(widths.slug)} ${r.title.padEnd(widths.title)} ${r.pageHash}`
588
+ );
589
+ }
590
+ lines.push(`
591
+ Total: ${payload.total}`);
592
+ return lines.join("\n") + "\n";
593
+ };
594
+ var runList = async (cmd, opts) => {
595
+ const auth = await requireAuth({ apiBaseOverride: apiBaseOverride(cmd) });
596
+ const client = fromAuth(auth);
597
+ const wiki = await resolveWiki(auth, client, {
598
+ account: opts.account,
599
+ wiki: opts.wiki
600
+ });
601
+ const archived = opts.archived ?? false;
602
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : 20;
603
+ const offset = opts.offset ? Number.parseInt(opts.offset, 10) : 0;
604
+ if (opts.search) {
605
+ const raw = await client.get(
606
+ `/api/v1/pages/wiki/${wiki.id}/search`,
607
+ { q: opts.search, limit, offset, archived }
608
+ );
609
+ const result2 = {
610
+ rows: raw.rows.map(
611
+ (r) => ({
612
+ title: r.title,
613
+ slug: r.slug,
614
+ pageHash: r.pageHash,
615
+ updatedAt: r.updatedAt
616
+ })
617
+ ),
618
+ total: raw.total
619
+ };
620
+ writeResult(cmd, result2, formatListTable);
621
+ return;
622
+ }
623
+ const pageNumber = Math.max(1, Math.floor(offset / 20) + 1);
624
+ const result = await client.get(
625
+ `/api/v1/pages/wiki/${wiki.id}`,
626
+ { page: pageNumber, archived }
627
+ );
628
+ const sliceStart = offset - (pageNumber - 1) * 20;
629
+ const trimmed = result.pages.slice(sliceStart, sliceStart + limit).map((p) => ({
630
+ title: p.title,
631
+ slug: p.slug,
632
+ pageHash: p.pageHash,
633
+ updatedAt: p.updatedAt
634
+ }));
635
+ writeResult(
636
+ cmd,
637
+ { rows: trimmed, total: result.total },
638
+ formatListTable
639
+ );
640
+ };
641
+ var runGet = async (cmd, identifier, opts) => {
642
+ const auth = await requireAuth({ apiBaseOverride: apiBaseOverride(cmd) });
643
+ const client = fromAuth(auth);
644
+ const wiki = await resolveWiki(auth, client, opts);
645
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
646
+ identifier
647
+ );
648
+ const isPageHash = /^[A-Za-z0-9_-]{12}$/.test(identifier);
649
+ const query = {};
650
+ if (isUuid) query.id = identifier;
651
+ else if (isPageHash) query.pageHash = identifier;
652
+ else query.slug = identifier;
653
+ const page = await client.get(
654
+ `/api/v1/pages/${wiki.id}`,
655
+ query
656
+ );
657
+ writeResult(
658
+ cmd,
659
+ page,
660
+ (p) => `${p.title}
661
+ ${"-".repeat(p.title.length)}
662
+ slug: ${p.slug}
663
+ pageHash: ${p.pageHash}
664
+ archived: ${p.archived ? "yes" : "no"}
665
+ updated: ${p.updatedAt}
666
+
667
+ [content omitted in --human view; use JSON output for full body]
668
+ `
669
+ );
670
+ };
671
+ var runCreate = async (cmd, opts) => {
672
+ const sources = [opts.file, opts.content, opts.stdin].filter(
673
+ (v) => v !== void 0 && v !== false
674
+ );
675
+ if (sources.length !== 1) {
676
+ throw new Error(
677
+ "Provide exactly one of --file <path>, --content <jsonString>, or --stdin."
678
+ );
679
+ }
680
+ let raw;
681
+ if (opts.file) raw = await readFile2(opts.file, "utf8");
682
+ else if (opts.content) raw = opts.content;
683
+ else raw = await readStdin();
684
+ const doc = parseDoc(raw);
685
+ const auth = await requireAuth({ apiBaseOverride: apiBaseOverride(cmd) });
686
+ const client = fromAuth(auth);
687
+ const wiki = await resolveWiki(auth, client, {
688
+ account: opts.account,
689
+ wiki: opts.wiki
690
+ });
691
+ let parentId;
692
+ if (opts.parent) parentId = await resolveParent(client, wiki.id, opts.parent);
693
+ const page = await client.post(
694
+ `/api/v1/pages?updateIndexTree=true${parentId ? `&parentId=${parentId}` : ""}`,
695
+ {
696
+ wikiId: wiki.id,
697
+ title: opts.title,
698
+ slug: "",
699
+ content: doc.content
700
+ }
701
+ );
702
+ writeResult(
703
+ cmd,
704
+ page,
705
+ (p) => `Created "${p.title}"
706
+ slug: ${p.slug}
707
+ pageHash: ${p.pageHash}
708
+ `
709
+ );
710
+ };
711
+ var registerPageCommands = (program2) => {
712
+ const page = program2.command("page").description("Inspect and create pages");
713
+ page.command("list").description("List pages in a wiki, optionally filtered by --search").option("--account <slug>", "Account slug").option("--wiki <slug>", "Wiki slug").option("--search <query>", "Filter pages whose title or content matches the query").option("--archived", "Include archived pages", false).option("--limit <n>", "Maximum number of rows to return").option("--offset <n>", "Number of rows to skip").action(async function(opts) {
714
+ try {
715
+ await runList(this, opts);
716
+ } catch (err) {
717
+ exitWithError(err);
718
+ }
719
+ });
720
+ page.command("get <identifier>").description("Fetch one page by slug, pageHash, or id (full content included)").option("--account <slug>", "Account slug").option("--wiki <slug>", "Wiki slug").action(async function(identifier, opts) {
721
+ try {
722
+ await runGet(this, identifier, opts);
723
+ } catch (err) {
724
+ exitWithError(err);
725
+ }
726
+ });
727
+ page.command("create").description("Create a page from a Tiptap JSON doc").requiredOption("--title <title>", "Page title").option("--account <slug>", "Account slug").option("--wiki <slug>", "Wiki slug").option("--parent <ref>", "Parent page (UUID, pageHash, or slug)").option("--file <path>", "Read Tiptap JSON from this file").option("--content <json>", "Inline Tiptap JSON string").option("--stdin", "Read Tiptap JSON from stdin", false).action(async function(opts) {
728
+ try {
729
+ await runCreate(this, opts);
730
+ } catch (err) {
731
+ exitWithError(err);
732
+ }
733
+ });
734
+ };
735
+
736
+ // src/commands/agent-help.ts
737
+ import { readFile as readFile3 } from "fs/promises";
738
+ import { fileURLToPath } from "url";
739
+ import { dirname as dirname2, join as join2 } from "path";
740
+ var findAgentDoc = async () => {
741
+ const here = dirname2(fileURLToPath(import.meta.url));
742
+ const candidates = [
743
+ join2(here, "..", "AGENT.md"),
744
+ // dist/index.js → ../AGENT.md
745
+ join2(here, "..", "..", "AGENT.md")
746
+ // dev: src/commands → ../../AGENT.md
747
+ ];
748
+ for (const path of candidates) {
749
+ try {
750
+ return await readFile3(path, "utf8");
751
+ } catch {
752
+ }
753
+ }
754
+ throw new Error(
755
+ "AGENT.md not found. Reinstall the CLI or read https://github.com/slimwiki/slimwiki/blob/main/cli/AGENT.md."
756
+ );
757
+ };
758
+ var registerAgentHelpCommand = (program2) => {
759
+ program2.command("agent-help").description("Print the agent contract (AGENT.md) to stdout").action(async () => {
760
+ try {
761
+ const doc = await findAgentDoc();
762
+ process.stdout.write(doc);
763
+ if (!doc.endsWith("\n")) process.stdout.write("\n");
764
+ } catch (err) {
765
+ exitWithError(err);
766
+ }
767
+ });
768
+ };
769
+
770
+ // src/index.ts
771
+ var VERSION = "0.1.0";
772
+ var program = new Command();
773
+ program.name("slimwiki").description("Agent-friendly CLI for SlimWiki").version(VERSION).option("--human", "human-readable output (default: JSON)", false).option("--api-base <url>", "Override the SlimWiki API base URL");
774
+ registerAuthCommands(program);
775
+ registerAccountCommands(program);
776
+ registerWikiCommands(program);
777
+ registerPageCommands(program);
778
+ registerAgentHelpCommand(program);
779
+ program.parseAsync(process.argv).catch((err) => {
780
+ writeError(err);
781
+ process.exit(1);
782
+ });