skillwiki 0.10.109 → 0.10.111

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 (42) hide show
  1. package/dist/{chunk-JCDAEZVD.js → chunk-5IBLXGLY.js} +5 -3
  2. package/dist/chunk-6OQLBNKR.js +250 -0
  3. package/dist/chunk-GQWCTJTU.js +55 -0
  4. package/dist/{chunk-FYAKMULW.js → chunk-HBJLFPQP.js} +91 -198
  5. package/dist/chunk-I2J3IJB7.js +54 -0
  6. package/dist/{chunk-HMHMCPCE.js → chunk-I6IRJCUJ.js} +12 -57
  7. package/dist/{chunk-QU2IXZRN.js → chunk-JQV5KNPT.js} +8 -6
  8. package/dist/chunk-KWEYA3OE.js +60 -0
  9. package/dist/{chunk-DTTKCCQ4.js → chunk-MRPJ7X7W.js} +5 -3
  10. package/dist/{chunk-LDEZQXH6.js → chunk-OJJBZDPR.js} +306 -116
  11. package/dist/chunk-OSL4J7GF.js +32 -0
  12. package/dist/chunk-PYPPG7RF.js +126 -0
  13. package/dist/chunk-QESNTTVY.js +50 -0
  14. package/dist/chunk-TDBUJUYH.js +109 -0
  15. package/dist/{chunk-GJC2CI3W.js → chunk-UIEQIJXP.js} +108 -336
  16. package/dist/chunk-X5GL2FCU.js +344 -0
  17. package/dist/chunk-ZRGZBAVS.js +98 -0
  18. package/dist/cli.js +352 -297
  19. package/dist/{index-projection-ESBJOBS7.js → index-projection-2K672CBO.js} +5 -3
  20. package/dist/journal-QU5IDXQF.js +13 -0
  21. package/dist/{managed-write-preflight-MAIGWFQJ.js → managed-write-preflight-GRSHQYKC.js} +7 -4
  22. package/dist/migrate-PIB4QA54.js +21 -0
  23. package/dist/ready-S7F53Q2D.js +9 -0
  24. package/dist/runtime-GLMI5OCT.js +15 -0
  25. package/dist/skillwiki-mcp.js +12 -7
  26. package/dist/sources-D2MGP246.js +10 -0
  27. package/dist/upload-HTE62TOV.js +14 -0
  28. package/dist/watcher-MHG46J3R.js +616 -0
  29. package/docs/clip-outbox.md +119 -0
  30. package/package.json +5 -1
  31. package/skills/.claude-plugin/plugin.json +1 -1
  32. package/skills/.codex-plugin/plugin.json +1 -1
  33. package/skills/.cursor-plugin/plugin.json +1 -1
  34. package/skills/package.json +1 -1
  35. package/skills/skills/using-skillwiki/SKILL.md +1 -1
  36. package/skills/skills/wiki-init/SKILL.md +12 -11
  37. package/skills/using-skillwiki/SKILL.md +1 -1
  38. package/skills/wiki-init/SKILL.md +12 -11
  39. package/templates/launchd/com.karlchow.skillwiki-clip-outbox.plist +21 -0
  40. package/templates/web-clipper/llm-wiki-clippings.json +1 -1
  41. package/templates/web-clipper/readme.txt +13 -13
  42. package/dist/sources-QTQSZMST.js +0 -9
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- err,
4
3
  extractFrontmatter,
5
- ok,
6
4
  readPage,
7
5
  scanSensitiveContent,
8
6
  splitFrontmatter
9
- } from "./chunk-GJC2CI3W.js";
7
+ } from "./chunk-X5GL2FCU.js";
8
+ import {
9
+ err,
10
+ ok
11
+ } from "./chunk-UIEQIJXP.js";
10
12
 
11
13
  // src/utils/vault-path-safety.ts
12
14
  import { lstatSync, realpathSync } from "fs";
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parseDotenvFile,
4
+ resolveSkillwikiHome
5
+ } from "./chunk-PYPPG7RF.js";
6
+ import {
7
+ CLIP_OUTBOX_DEFAULTS,
8
+ isClipOutboxKey,
9
+ parseClipOutboxKeyValue
10
+ } from "./chunk-UIEQIJXP.js";
11
+
12
+ // src/clip-outbox/runtime.ts
13
+ import { existsSync as existsSync2 } from "fs";
14
+ import { lstat, readFile } from "fs/promises";
15
+ import { join as join2 } from "path";
16
+ import { execFileSync } from "child_process";
17
+ import { z } from "zod";
18
+
19
+ // src/utils/clip-outbox-config.ts
20
+ import { realpathSync, statSync, existsSync, accessSync, constants } from "fs";
21
+ import { homedir } from "os";
22
+ import { isAbsolute, join, resolve, sep } from "path";
23
+ var CLIP_NOT_CONFIGURED = "clip channel not configured";
24
+ var PRODUCTION_WATCHER_LABEL = CLIP_OUTBOX_DEFAULTS.watcher_label;
25
+ var RETIRED_WATCHER_LABELS = [
26
+ "com.karlchow.wiki-dev-outbox",
27
+ "com.karlchow.wiki-dev-sse",
28
+ "com.karlchow.wiki-dev-hourly",
29
+ "com.karlchow.skillwiki-note-outbox"
30
+ ];
31
+ function expandUserPath(raw, home) {
32
+ if (raw === "~") return home;
33
+ if (raw.startsWith("~/")) return resolve(home, raw.slice(2));
34
+ return raw;
35
+ }
36
+ function canonicalPath(raw, home) {
37
+ const expanded = expandUserPath(raw.trim(), home);
38
+ if (expanded.includes("\0")) return { ok: false, message: "path contains NUL" };
39
+ if (expanded.includes("://") || expanded.includes("@")) {
40
+ return { ok: false, message: "path must not include URL credentials" };
41
+ }
42
+ if (!isAbsolute(expanded)) return { ok: false, message: "must be an absolute path" };
43
+ try {
44
+ const real = existsSync(expanded) ? realpathSync(expanded) : resolve(expanded);
45
+ if (real.split(sep).some((seg) => seg === "..")) {
46
+ return { ok: false, message: "path must not contain traversal" };
47
+ }
48
+ return { ok: true, path: real };
49
+ } catch (error) {
50
+ return { ok: false, message: error instanceof Error ? error.message : "invalid path" };
51
+ }
52
+ }
53
+ function nestedOrEqual(a, b) {
54
+ if (a === b) return true;
55
+ const prefixA = a.endsWith(sep) ? a : a + sep;
56
+ const prefixB = b.endsWith(sep) ? b : b + sep;
57
+ return prefixA.startsWith(prefixB) || prefixB.startsWith(prefixA);
58
+ }
59
+ function intField(value, fallback) {
60
+ return Number(value && value.length > 0 ? value : fallback);
61
+ }
62
+ function requireWritableDir(path, label) {
63
+ try {
64
+ const st = statSync(path);
65
+ if (!st.isDirectory()) return `${label} is not a directory`;
66
+ accessSync(path, constants.W_OK);
67
+ return void 0;
68
+ } catch {
69
+ return `${label} is missing or not writable`;
70
+ }
71
+ }
72
+ function resolveClipOutboxConfig(map, home) {
73
+ const present = Object.keys(map).filter(isClipOutboxKey);
74
+ for (const key of present) {
75
+ const parsed = parseClipOutboxKeyValue(key, map[key] ?? "");
76
+ if (!parsed.ok) return { ok: false, message: `${key}: ${parsed.message}` };
77
+ }
78
+ const enabled = (map["clip_outbox.enabled"] ?? CLIP_OUTBOX_DEFAULTS.enabled) === "true";
79
+ if (!enabled) {
80
+ return { ok: true, enabled: false };
81
+ }
82
+ const required = [
83
+ "clip_outbox.root",
84
+ "clip_outbox.state_root",
85
+ "clip_outbox.mirror_root",
86
+ "clip_outbox.ready_url",
87
+ "clip_outbox.s3_credential_ref",
88
+ "clip_outbox.ready_credential_ref"
89
+ ];
90
+ for (const key of required) {
91
+ if (!map[key]) return { ok: false, message: `${key} is required when clip_outbox.enabled=true` };
92
+ }
93
+ const d8Absent = map["clip_outbox.d8_note_absent"] === "true";
94
+ const d8RootRaw = map["clip_outbox.d8_note_root"];
95
+ if (d8Absent && d8RootRaw) {
96
+ return { ok: false, message: "clip_outbox.d8_note_root and clip_outbox.d8_note_absent cannot both be set" };
97
+ }
98
+ if (!d8Absent && !d8RootRaw) {
99
+ return { ok: false, message: "clip_outbox.d8_note_root or clip_outbox.d8_note_absent=true is required when enabled" };
100
+ }
101
+ const root = canonicalPath(map["clip_outbox.root"], home);
102
+ const stateRoot = canonicalPath(map["clip_outbox.state_root"], home);
103
+ const mirrorRoot = canonicalPath(map["clip_outbox.mirror_root"], home);
104
+ if (!root.ok) return { ok: false, message: `clip_outbox.root: ${root.message}` };
105
+ if (!stateRoot.ok) return { ok: false, message: `clip_outbox.state_root: ${stateRoot.message}` };
106
+ if (!mirrorRoot.ok) return { ok: false, message: `clip_outbox.mirror_root: ${mirrorRoot.message}` };
107
+ let d8NoteRoot = null;
108
+ if (d8RootRaw) {
109
+ const d8 = canonicalPath(d8RootRaw, home);
110
+ if (!d8.ok) return { ok: false, message: `clip_outbox.d8_note_root: ${d8.message}` };
111
+ d8NoteRoot = d8.path;
112
+ }
113
+ if (nestedOrEqual(root.path, mirrorRoot.path)) {
114
+ return { ok: false, message: "clip_outbox.root must be outside the frozen mirror" };
115
+ }
116
+ if (nestedOrEqual(root.path, stateRoot.path) || nestedOrEqual(stateRoot.path, mirrorRoot.path)) {
117
+ return { ok: false, message: "clip_outbox.state_root must be disjoint from outbox and mirror" };
118
+ }
119
+ if (d8NoteRoot && (nestedOrEqual(root.path, d8NoteRoot) || nestedOrEqual(stateRoot.path, d8NoteRoot))) {
120
+ return { ok: false, message: "clip outbox must not equal or nest the D8 note outbox" };
121
+ }
122
+ const rootErr = requireWritableDir(root.path, "clip_outbox.root");
123
+ if (rootErr) return { ok: false, message: rootErr };
124
+ const stateErr = requireWritableDir(stateRoot.path, "clip_outbox.state_root");
125
+ if (stateErr) {
126
+ if (existsSync(stateRoot.path)) return { ok: false, message: stateErr };
127
+ const parentErr = requireWritableDir(resolve(stateRoot.path, ".."), "clip_outbox.state_root parent");
128
+ if (parentErr) return { ok: false, message: stateErr };
129
+ }
130
+ if (!existsSync(mirrorRoot.path)) {
131
+ return { ok: false, message: "clip_outbox.mirror_root does not exist" };
132
+ }
133
+ const vaultId = map["clip_outbox.vault_id"] ?? CLIP_OUTBOX_DEFAULTS.vault_id;
134
+ if (vaultId !== "central") return { ok: false, message: "clip_outbox.vault_id must be central" };
135
+ const destination = map["clip_outbox.destination"] ?? CLIP_OUTBOX_DEFAULTS.destination;
136
+ if (destination !== CLIP_OUTBOX_DEFAULTS.destination) {
137
+ return { ok: false, message: "clip_outbox.destination must be seaweed-wiki:cloud/wiki" };
138
+ }
139
+ const mode = map["clip_outbox.mode"] ?? CLIP_OUTBOX_DEFAULTS.mode;
140
+ if (mode !== "event-watch") return { ok: false, message: "clip_outbox.mode must be event-watch" };
141
+ const watcherLabel = map["clip_outbox.watcher_label"] ?? CLIP_OUTBOX_DEFAULTS.watcher_label;
142
+ if (watcherLabel !== PRODUCTION_WATCHER_LABEL) {
143
+ return { ok: false, message: "clip_outbox.watcher_label must be the production clip watcher" };
144
+ }
145
+ const imageTypes = (map["clip_outbox.image_types"] ?? CLIP_OUTBOX_DEFAULTS.image_types).split(",").map((p) => p.trim()).filter(Boolean);
146
+ return {
147
+ ok: true,
148
+ enabled: true,
149
+ config: {
150
+ enabled: true,
151
+ root: root.path,
152
+ stateRoot: stateRoot.path,
153
+ mirrorRoot: mirrorRoot.path,
154
+ d8NoteRoot,
155
+ d8NoteAbsent: d8Absent,
156
+ vaultId: "central",
157
+ allowedPrefixes: ["raw/articles", "raw/assets"],
158
+ destination: "seaweed-wiki:cloud/wiki",
159
+ mode: "event-watch",
160
+ pollFallbackEnabled: (map["clip_outbox.poll_fallback_enabled"] ?? CLIP_OUTBOX_DEFAULTS.poll_fallback_enabled) === "true",
161
+ reconcileSeconds: intField(map["clip_outbox.reconcile_seconds"], CLIP_OUTBOX_DEFAULTS.reconcile_seconds),
162
+ debounceMs: intField(map["clip_outbox.debounce_ms"], CLIP_OUTBOX_DEFAULTS.debounce_ms),
163
+ stableHashMs: intField(map["clip_outbox.stable_hash_ms"], CLIP_OUTBOX_DEFAULTS.stable_hash_ms),
164
+ maxArticleBytes: intField(map["clip_outbox.max_article_bytes"], CLIP_OUTBOX_DEFAULTS.max_article_bytes),
165
+ maxImages: intField(map["clip_outbox.max_images"], CLIP_OUTBOX_DEFAULTS.max_images),
166
+ maxImageBytes: intField(map["clip_outbox.max_image_bytes"], CLIP_OUTBOX_DEFAULTS.max_image_bytes),
167
+ maxBundleBytes: intField(map["clip_outbox.max_bundle_bytes"], CLIP_OUTBOX_DEFAULTS.max_bundle_bytes),
168
+ maxPixels: intField(map["clip_outbox.max_pixels"], CLIP_OUTBOX_DEFAULTS.max_pixels),
169
+ downloadConcurrency: intField(map["clip_outbox.download_concurrency"], CLIP_OUTBOX_DEFAULTS.download_concurrency),
170
+ imageTypes,
171
+ maxRedirects: intField(map["clip_outbox.max_redirects"], CLIP_OUTBOX_DEFAULTS.max_redirects),
172
+ requestTimeoutMs: intField(map["clip_outbox.request_timeout_ms"], CLIP_OUTBOX_DEFAULTS.request_timeout_ms),
173
+ localizeTimeoutMs: intField(map["clip_outbox.localize_timeout_ms"], CLIP_OUTBOX_DEFAULTS.localize_timeout_ms),
174
+ readyUrl: map["clip_outbox.ready_url"],
175
+ s3CredentialRef: map["clip_outbox.s3_credential_ref"],
176
+ readyCredentialRef: map["clip_outbox.ready_credential_ref"],
177
+ watcherLabel: PRODUCTION_WATCHER_LABEL
178
+ }
179
+ };
180
+ }
181
+ function clipOutboxConfigPath(home) {
182
+ return join(resolveSkillwikiHome(home), ".skillwiki", ".env");
183
+ }
184
+ async function loadClipOutboxConfig(home) {
185
+ const map = await parseDotenvFile(clipOutboxConfigPath(home));
186
+ return resolveClipOutboxConfig(map, home || homedir());
187
+ }
188
+
189
+ // src/clip-outbox/runtime.ts
190
+ var s3Schema = z.object({ endpoint: z.string().url(), region: z.string().default("us-east-1"), accessKeyId: z.string().min(1), secretAccessKey: z.string().min(1), sessionToken: z.string().optional() }).strict();
191
+ async function loadClipCredentials(home, config, env = process.env) {
192
+ let providers = {};
193
+ const path = join2(home, ".skillwiki", "clip-outbox-credentials.json");
194
+ try {
195
+ const st = await lstat(path);
196
+ if (!st.isFile() || st.isSymbolicLink() || (st.mode & 63) !== 0 || st.uid !== process.getuid?.()) throw new Error("protected credential file required");
197
+ providers = JSON.parse(await readFile(path, "utf8"));
198
+ } catch (e) {
199
+ if (e.code !== "ENOENT") throw new Error("clip credential provider unreadable");
200
+ }
201
+ try {
202
+ const rawS3 = env[config.s3CredentialRef] ?? providers[config.s3CredentialRef];
203
+ const s3 = s3Schema.parse(typeof rawS3 === "string" ? JSON.parse(rawS3) : rawS3);
204
+ const endpoint = new URL(s3.endpoint);
205
+ if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error();
206
+ const hostConfig = await parseDotenvFile(join2(home, ".skillwiki", ".env"));
207
+ const readyToken = env[config.readyCredentialRef] ?? providers[config.readyCredentialRef];
208
+ if (typeof readyToken !== "string" || !readyToken || config.readyCredentialRef === "SKILLWIKI_MCP_TOKEN" || readyToken === env.SKILLWIKI_MCP_TOKEN || readyToken === hostConfig.SKILLWIKI_MCP_TOKEN || config.readyCredentialRef === config.s3CredentialRef) throw new Error();
209
+ return { s3, readyToken };
210
+ } catch {
211
+ throw new Error("clip credential references missing or invalid; use a separate file-ingress token");
212
+ }
213
+ }
214
+ var evidencePath = (config) => join2(config.stateRoot, "conditional-create.json");
215
+ async function conditionalCreateProven(config, endpoint) {
216
+ try {
217
+ const proof = JSON.parse(await readFile(evidencePath(config), "utf8"));
218
+ return proof.verified === true && proof.destination === config.destination && proof.endpoint === endpoint && typeof proof.testedAt === "string" && Number.isFinite(Date.parse(proof.testedAt));
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+ async function validateRuntime(home, config, endpoint) {
224
+ const resolved = await loadClipOutboxConfig(home);
225
+ if (!resolved.ok || !resolved.enabled || JSON.stringify(resolved.config) !== JSON.stringify(config)) throw new Error("clip configuration changed; restart after attended review");
226
+ if (!existsSync2(join2(config.mirrorRoot, ".WIKI_GIT_FROZEN"))) throw new Error("frozen mirror marker missing");
227
+ const map = await parseDotenvFile(join2(home, ".skillwiki", ".env"));
228
+ if (map["vault_sync.push_enabled"] !== "false") throw new Error("wiki-push must remain disabled");
229
+ try {
230
+ execFileSync("launchctl", ["list", "com.karlchow.wiki-push"], { stdio: "ignore", timeout: 2e3 });
231
+ throw new Error("wiki-push is loaded");
232
+ } catch (e) {
233
+ if (!e.status) throw e;
234
+ }
235
+ if (!await conditionalCreateProven(config, endpoint)) throw new Error("conditional-create evidence missing for this endpoint and destination");
236
+ }
237
+
238
+ export {
239
+ CLIP_NOT_CONFIGURED,
240
+ PRODUCTION_WATCHER_LABEL,
241
+ RETIRED_WATCHER_LABELS,
242
+ expandUserPath,
243
+ nestedOrEqual,
244
+ resolveClipOutboxConfig,
245
+ loadClipOutboxConfig,
246
+ loadClipCredentials,
247
+ evidencePath,
248
+ conditionalCreateProven,
249
+ validateRuntime
250
+ };
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ err,
4
+ ok
5
+ } from "./chunk-UIEQIJXP.js";
6
+
7
+ // src/utils/atomic-write.ts
8
+ import { randomBytes } from "crypto";
9
+ import { open, readFile, rename, unlink } from "fs/promises";
10
+ import { basename, dirname, join } from "path";
11
+ async function readExisting(path) {
12
+ try {
13
+ return await readFile(path, "utf8");
14
+ } catch (error) {
15
+ if (error.code === "ENOENT") return null;
16
+ throw error;
17
+ }
18
+ }
19
+ async function atomicWriteText(path, text) {
20
+ let existing;
21
+ try {
22
+ existing = await readExisting(path);
23
+ } catch (error) {
24
+ return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
25
+ }
26
+ if (existing === text) return ok({ changed: false, existed: true });
27
+ const tmp = join(
28
+ dirname(path),
29
+ `.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
30
+ );
31
+ try {
32
+ const handle = await open(tmp, "wx");
33
+ try {
34
+ await handle.writeFile(text, "utf8");
35
+ try {
36
+ await handle.sync();
37
+ } catch {
38
+ }
39
+ } finally {
40
+ await handle.close();
41
+ }
42
+ await rename(tmp, path);
43
+ return ok({ changed: true, existed: existing !== null });
44
+ } catch (error) {
45
+ try {
46
+ await unlink(tmp);
47
+ } catch {
48
+ }
49
+ return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
50
+ }
51
+ }
52
+
53
+ export {
54
+ atomicWriteText
55
+ };