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/AGENT.md +316 -0
- package/LICENSE.md +65 -0
- package/README.md +434 -0
- package/SKILL.md +17 -0
- package/dist/index.js +782 -0
- package/package.json +53 -0
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
|
+
});
|