skillwiki 0.10.110 → 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 (36) hide show
  1. package/dist/{chunk-5MV5SSRM.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-QU2CKXM4.js → chunk-HBJLFPQP.js} +91 -199
  5. package/dist/chunk-I2J3IJB7.js +54 -0
  6. package/dist/{chunk-WPJHDRTP.js → chunk-I6IRJCUJ.js} +12 -57
  7. package/dist/{chunk-NA3TZ4JN.js → chunk-JQV5KNPT.js} +8 -6
  8. package/dist/chunk-KWEYA3OE.js +60 -0
  9. package/dist/{chunk-KX5Y3GSX.js → chunk-MRPJ7X7W.js} +5 -3
  10. package/dist/{chunk-6X6PPQNI.js → chunk-OJJBZDPR.js} +162 -296
  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-R2DJ7IJ5.js → chunk-UIEQIJXP.js} +5 -336
  16. package/dist/chunk-X5GL2FCU.js +344 -0
  17. package/dist/chunk-ZRGZBAVS.js +98 -0
  18. package/dist/cli.js +308 -298
  19. package/dist/{index-projection-6XGESSHV.js → index-projection-2K672CBO.js} +5 -3
  20. package/dist/journal-QU5IDXQF.js +13 -0
  21. package/dist/{managed-write-preflight-JPOBVVSU.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/templates/launchd/com.karlchow.skillwiki-clip-outbox.plist +12 -13
  36. package/dist/sources-CLOF5VAT.js +0 -9
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/clip-outbox/ready.ts
4
+ function buildReadyHint(vaultId, files) {
5
+ const assets = files.filter((f) => f.path.startsWith("raw/assets/"));
6
+ const articles = files.filter((f) => f.path.startsWith("raw/articles/"));
7
+ return { vault_id: vaultId, files: [...assets, ...articles] };
8
+ }
9
+ async function postReadyHint(opts) {
10
+ const fetchImpl = opts.fetchImpl ?? fetch;
11
+ const body = JSON.stringify(opts.hint);
12
+ if (Buffer.byteLength(body) > 32768) return { ok: false, status: 413 };
13
+ const res = await fetchImpl(opts.url, {
14
+ method: "POST",
15
+ headers: {
16
+ Authorization: `Bearer ${opts.token}`,
17
+ "Content-Type": "application/json"
18
+ },
19
+ body,
20
+ signal: AbortSignal.timeout(1e4),
21
+ redirect: "error"
22
+ });
23
+ if (!res.ok) return { ok: false, status: res.status };
24
+ const result = await res.json();
25
+ const paths = result.paths;
26
+ return result.ok === true && Array.isArray(paths) && opts.hint.files.every((f) => paths.includes(f.path)) ? { ok: true } : { ok: false, status: 503 };
27
+ }
28
+
29
+ export {
30
+ buildReadyHint,
31
+ postReadyHint
32
+ };
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isClipOutboxKey,
4
+ isVaultSyncKey
5
+ } from "./chunk-UIEQIJXP.js";
6
+
7
+ // src/utils/home.ts
8
+ import { homedir } from "os";
9
+ import { resolve } from "path";
10
+ function resolveSkillwikiHome(home, env = process.env) {
11
+ for (const candidate of [home, env.HOME, env.USERPROFILE]) {
12
+ if (candidate?.trim()) return resolve(candidate);
13
+ }
14
+ try {
15
+ const fallback = homedir();
16
+ if (fallback?.trim()) return resolve(fallback);
17
+ } catch {
18
+ throw new Error("Could not resolve SkillWiki home directory");
19
+ }
20
+ throw new Error("Could not resolve SkillWiki home directory");
21
+ }
22
+
23
+ // src/utils/dotenv.ts
24
+ import { readFile, writeFile, mkdir } from "fs/promises";
25
+ import { dirname } from "path";
26
+ var CONFIG_KEYS = [
27
+ "WIKI_PATH",
28
+ "WIKI_LANG",
29
+ "SKILLWIKI_HOST_ID",
30
+ "AUTO_COMMIT",
31
+ "BACKUP_ENDPOINT",
32
+ "BACKUP_BUCKET",
33
+ "BACKUP_REGION",
34
+ "BACKUP_ACCESS_KEY_ID",
35
+ "BACKUP_SECRET_ACCESS_KEY"
36
+ ];
37
+ var MCP_AUTH_KEYS = ["SKILLWIKI_MCP_TOKEN", "SKILLWIKI_MCP_URL"];
38
+ var _whitelist = /* @__PURE__ */ new Set([...CONFIG_KEYS, ...MCP_AUTH_KEYS]);
39
+ var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
40
+ var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
41
+ var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
42
+ function isValidWikiProfileKey(key) {
43
+ if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
44
+ return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
45
+ }
46
+ function profileKey(name, suffix) {
47
+ return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
48
+ }
49
+ function parseDotenvText(text) {
50
+ const out = {};
51
+ for (const rawLine of text.split(/\r?\n/)) {
52
+ const line = rawLine.trim();
53
+ if (line.length === 0 || line.startsWith("#")) continue;
54
+ const eq = line.indexOf("=");
55
+ if (eq <= 0) continue;
56
+ const key = line.slice(0, eq).trim();
57
+ const value = line.slice(eq + 1).trim();
58
+ if (!_whitelist.has(key) && !isValidWikiProfileKey(key) && !isVaultSyncKey(key) && !isClipOutboxKey(key)) continue;
59
+ if (value.length === 0) continue;
60
+ out[key] = value;
61
+ }
62
+ return out;
63
+ }
64
+ async function parseDotenvFile(path) {
65
+ let text;
66
+ try {
67
+ text = await readFile(path, "utf8");
68
+ } catch {
69
+ return {};
70
+ }
71
+ return parseDotenvText(text);
72
+ }
73
+ async function writeDotenv(filePath, entries, originalContent) {
74
+ const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
75
+ await mkdir(dirname(filePath), { recursive: true });
76
+ await writeFile(filePath, lines.join("\n") + "\n", "utf8");
77
+ }
78
+ function freshLines(entries) {
79
+ const out = [];
80
+ for (const [key, value] of Object.entries(entries)) {
81
+ if (value !== void 0) out.push(`${key}=${value}`);
82
+ }
83
+ return out;
84
+ }
85
+ function updateLines(originalContent, entries) {
86
+ let rawLines = originalContent.split(/\r?\n/);
87
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
88
+ rawLines = rawLines.slice(0, -1);
89
+ }
90
+ const keysToWrite = new Set(Object.keys(entries));
91
+ const out = [];
92
+ for (const line of rawLines) {
93
+ const trimmed = line.trim();
94
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
95
+ out.push(line);
96
+ continue;
97
+ }
98
+ const eq = trimmed.indexOf("=");
99
+ if (eq <= 0) {
100
+ out.push(line);
101
+ continue;
102
+ }
103
+ const key = trimmed.slice(0, eq).trim();
104
+ if (keysToWrite.has(key)) {
105
+ out.push(`${key}=${entries[key]}`);
106
+ keysToWrite.delete(key);
107
+ } else {
108
+ out.push(line);
109
+ }
110
+ }
111
+ for (const key of keysToWrite) {
112
+ const value = entries[key];
113
+ if (value !== void 0) out.push(`${key}=${value}`);
114
+ }
115
+ return out;
116
+ }
117
+
118
+ export {
119
+ resolveSkillwikiHome,
120
+ CONFIG_KEYS,
121
+ isValidWikiProfileKey,
122
+ profileKey,
123
+ parseDotenvText,
124
+ parseDotenvFile,
125
+ writeDotenv
126
+ };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/delete-intent.ts
4
+ import { mkdir, writeFile, readdir, readFile } from "fs/promises";
5
+ import { join } from "path";
6
+ var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
7
+ var DELETE_INTENT_DIR = "meta/delete-intents";
8
+ function normalizeVaultRelPath(path) {
9
+ const p = path.replace(/\\/g, "/").replace(/^\/+/, "");
10
+ if (!p || p.includes("..") || p.startsWith(".git/")) {
11
+ throw new Error(`invalid vault-relative path: ${path}`);
12
+ }
13
+ return p;
14
+ }
15
+ function pathToIntentFilename(path) {
16
+ const p = normalizeVaultRelPath(path);
17
+ return `${p.replace(/\//g, "__")}.json`;
18
+ }
19
+ function intentHostId() {
20
+ return process.env.SKILLWIKI_HOST_ID ?? process.env.AGENT_HOST_ID ?? "unknown";
21
+ }
22
+ function buildDeleteIntent(input) {
23
+ return {
24
+ schema: DELETE_INTENT_SCHEMA,
25
+ path: normalizeVaultRelPath(input.path),
26
+ action: input.action,
27
+ created: input.created ?? (/* @__PURE__ */ new Date()).toISOString(),
28
+ host: input.host ?? intentHostId(),
29
+ actor: input.actor,
30
+ reason: input.reason,
31
+ source: input.source,
32
+ expires: input.expires ?? null
33
+ };
34
+ }
35
+ async function writeDeleteIntent(vault, intent) {
36
+ const dir = join(vault, DELETE_INTENT_DIR);
37
+ await mkdir(dir, { recursive: true });
38
+ const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
39
+ await writeFile(join(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
40
+ return rel;
41
+ }
42
+
43
+ export {
44
+ DELETE_INTENT_SCHEMA,
45
+ DELETE_INTENT_DIR,
46
+ normalizeVaultRelPath,
47
+ pathToIntentFilename,
48
+ buildDeleteIntent,
49
+ writeDeleteIntent
50
+ };
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PRODUCTION_WATCHER_LABEL,
4
+ evidencePath,
5
+ expandUserPath,
6
+ loadClipCredentials,
7
+ nestedOrEqual,
8
+ resolveClipOutboxConfig,
9
+ validateRuntime
10
+ } from "./chunk-6OQLBNKR.js";
11
+ import {
12
+ parseDotenvFile
13
+ } from "./chunk-PYPPG7RF.js";
14
+ import {
15
+ atomicWriteText
16
+ } from "./chunk-GQWCTJTU.js";
17
+
18
+ // src/clip-outbox/migrate.ts
19
+ import { access, mkdir, readFile } from "fs/promises";
20
+ import { constants } from "fs";
21
+ import { basename, dirname, isAbsolute, join } from "path";
22
+ import { execFileSync } from "child_process";
23
+ var CLIP_MIGRATION_HINT = "Clip channel requires attended setup: run skillwiki clip-outbox install-or-migrate, then skillwiki doctor. Installation/update never sets clip_outbox.enabled=true or loads launchd. Prove deployed SeaweedFS If-None-Match:* create-only before operator enablement.";
24
+ var xml = (s) => s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
25
+ async function packagedPlist() {
26
+ for (const rel of ["../templates/launchd/", "../../templates/launchd/"]) {
27
+ try {
28
+ return await readFile(new URL(`${rel}${PRODUCTION_WATCHER_LABEL}.plist`, import.meta.url), "utf8");
29
+ } catch {
30
+ }
31
+ }
32
+ throw new Error("packaged clip launchd template missing");
33
+ }
34
+ async function stableCliBinary(argvPath = process.argv[1]) {
35
+ const candidates = [argvPath, ...[dirname(process.execPath), ...(process.env.PATH ?? "").split(":")].map((dir) => join(dir, "skillwiki"))];
36
+ for (const candidate of candidates) {
37
+ if (!candidate || !isAbsolute(candidate) || basename(candidate) !== "skillwiki") continue;
38
+ try {
39
+ await access(candidate, constants.X_OK);
40
+ return candidate;
41
+ } catch {
42
+ }
43
+ }
44
+ throw new Error("global npm skillwiki binary not found; install/link the CLI before migrating");
45
+ }
46
+ async function migrateClipOutbox(input) {
47
+ const map = await parseDotenvFile(join(input.home, ".skillwiki", ".env"));
48
+ const resolved = resolveClipOutboxConfig(map, input.home);
49
+ if (!resolved.ok) throw new Error(resolved.message);
50
+ if (input.load && (!resolved.enabled || !input.conditionalCreateProven)) throw new Error("--load requires existing clip_outbox.enabled=true and --conditional-create-proven after the operator's deployed create-only race test");
51
+ if ((input.platform ?? process.platform) !== "darwin") throw new Error("clip launchd migration requires macOS");
52
+ const rawRoot = map["clip_outbox.root"] ?? "~/Obsidian/clip-outbox";
53
+ const root = resolved.enabled ? resolved.config.root : expandUserPath(rawRoot, input.home);
54
+ if (!isAbsolute(root) || root.includes("\0") || rawRoot.split("/").includes("..")) throw new Error("invalid outbox root");
55
+ const mirror = resolved.enabled ? resolved.config.mirrorRoot : expandUserPath(map["clip_outbox.mirror_root"] ?? join(input.home, "wiki"), input.home);
56
+ if (nestedOrEqual(root, mirror)) throw new Error("outbox must be disjoint from mirror");
57
+ const binary = input.binaryPath ?? await stableCliBinary();
58
+ if (!isAbsolute(binary) || !isAbsolute(input.nodePath ?? process.execPath)) throw new Error("absolute executable paths required");
59
+ const logRoot = join(input.home, "Library", "Logs", "SkillWiki");
60
+ const plistPath = join(input.home, "Library", "LaunchAgents", `${PRODUCTION_WATCHER_LABEL}.plist`);
61
+ const template = await packagedPlist();
62
+ let plist = template;
63
+ for (const [key, value] of Object.entries({ NODE_BINARY: input.nodePath ?? process.execPath, CLI_BINARY: binary, OUTBOX_ROOT: root, HOME: input.home, LOG_ROOT: logRoot })) plist = plist.replaceAll(`@${key}@`, xml(value));
64
+ if (input.load) {
65
+ if (!resolved.enabled) throw new Error("--load requires existing clip_outbox.enabled=true and --conditional-create-proven after the operator's deployed create-only race test");
66
+ const creds = await loadClipCredentials(input.home, resolved.config);
67
+ await mkdir(resolved.config.stateRoot, { recursive: true, mode: 448 });
68
+ const proof = await atomicWriteText(evidencePath(resolved.config), JSON.stringify({ verified: true, destination: resolved.config.destination, endpoint: creds.s3.endpoint, testedAt: (/* @__PURE__ */ new Date()).toISOString(), basis: "operator-attested deployed If-None-Match:* race test" }));
69
+ if (!proof.ok) throw new Error("cannot record operator conditional-create attestation");
70
+ await validateRuntime(input.home, resolved.config, creds.s3.endpoint);
71
+ }
72
+ await mkdir(dirname(plistPath), { recursive: true });
73
+ await mkdir(logRoot, { recursive: true });
74
+ const result = await atomicWriteText(plistPath, plist);
75
+ if (!result.ok) throw new Error("cannot refresh clip plist");
76
+ if (input.load) {
77
+ if (typeof process.getuid !== "function") throw new Error("clip launchd load requires macOS");
78
+ const domain = `gui/${process.getuid()}`;
79
+ const launch = input.launch ?? ((args) => {
80
+ execFileSync("launchctl", args, { stdio: "ignore", timeout: 5e3 });
81
+ });
82
+ try {
83
+ launch(["bootout", `${domain}/${PRODUCTION_WATCHER_LABEL}`]);
84
+ } catch {
85
+ }
86
+ launch(["enable", `${domain}/${PRODUCTION_WATCHER_LABEL}`]);
87
+ launch(["bootstrap", domain, plistPath]);
88
+ }
89
+ return { plistPath, loaded: !!input.load, enabled: resolved.enabled, humanHint: `${result.data.changed ? "Refreshed" : "Current"} clip launchd plist: ${plistPath}. ${input.load ? "Operator-gated launchd bootstrap requested." : CLIP_MIGRATION_HINT}` };
90
+ }
91
+ async function refreshInstalledClipPlist(home, opts) {
92
+ const plistPath = join(home, "Library", "LaunchAgents", `${PRODUCTION_WATCHER_LABEL}.plist`);
93
+ try {
94
+ await access(plistPath, constants.F_OK);
95
+ } catch {
96
+ return void 0;
97
+ }
98
+ const result = await migrateClipOutbox({ ...opts, home, load: false });
99
+ return result.humanHint;
100
+ }
101
+
102
+ export {
103
+ CLIP_MIGRATION_HINT,
104
+ xml,
105
+ packagedPlist,
106
+ stableCliBinary,
107
+ migrateClipOutbox,
108
+ refreshInstalledClipPlist
109
+ };
@@ -555,6 +555,9 @@ function inRange(ip, baseStr, prefix) {
555
555
  const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
556
556
  return (ipN & mask) === (baseN & mask);
557
557
  }
558
+ function ipv4InCidr(ip, base, prefix) {
559
+ return inRange(ip, base, prefix);
560
+ }
558
561
  function isBlockedHost(host) {
559
562
  const lower = host.toLowerCase();
560
563
  if (METADATA_HOSTS.has(lower)) return true;
@@ -598,330 +601,6 @@ function systemdPropertyFor(semantic) {
598
601
  return SYSTEMD_PROPERTY_CATALOG[semantic];
599
602
  }
600
603
 
601
- // src/parsers/frontmatter.ts
602
- import yaml from "js-yaml";
603
- var FM_OPEN = /^---\r?\n/;
604
- function splitFrontmatter(text) {
605
- if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
606
- const afterOpen = text.replace(FM_OPEN, "");
607
- const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
608
- if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
609
- const rawFrontmatter = afterOpen.slice(0, closeIdx);
610
- const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
611
- const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
612
- const body = text.slice(bodyStart);
613
- return ok({ rawFrontmatter, body, bodyStart });
614
- }
615
- function extractFrontmatter(text) {
616
- const split = splitFrontmatter(text);
617
- if (!split.ok) return split;
618
- if (!split.data.rawFrontmatter) return ok({});
619
- try {
620
- const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
621
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
622
- return ok(parsed);
623
- } catch (e) {
624
- return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
625
- }
626
- }
627
-
628
- // src/utils/sensitive-content.ts
629
- import { createHash } from "crypto";
630
- var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
631
- var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
632
- var MATCHERS = [
633
- {
634
- kind: "private_key",
635
- re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
636
- },
637
- {
638
- kind: "authorization_header",
639
- re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
640
- valueGroup: 1
641
- },
642
- {
643
- kind: "cookie",
644
- re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
645
- valueGroup: 1
646
- },
647
- {
648
- kind: "jwt",
649
- re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
650
- valueGroup: 1
651
- },
652
- {
653
- kind: "provider_key",
654
- re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
655
- valueGroup: 1
656
- },
657
- {
658
- kind: "access_key",
659
- re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
660
- valueGroup: 1
661
- },
662
- {
663
- kind: "access_key",
664
- re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
665
- valueGroup: 1
666
- },
667
- {
668
- kind: "api_key",
669
- re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
670
- valueGroup: 1
671
- },
672
- {
673
- kind: "password",
674
- re: /\b(?:password|passwd)["']?\s*[:=]\s*[`"']?([^\s`"']{8,})[`"']?/gi,
675
- valueGroup: 1
676
- },
677
- {
678
- kind: "secret",
679
- re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
680
- valueGroup: 1
681
- },
682
- {
683
- kind: "token",
684
- re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
685
- valueGroup: 1
686
- }
687
- ];
688
- function fingerprint(value) {
689
- return createHash("sha256").update(value).digest("hex").slice(0, 12);
690
- }
691
- function lineFor(text, offset) {
692
- return text.slice(0, offset).split(/\r?\n/).length;
693
- }
694
- function redactMarker(kind, value) {
695
- return `[REDACTED:${kind}:${fingerprint(value)}]`;
696
- }
697
- function isSyntheticPlaceholder(value) {
698
- return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
699
- }
700
- function isInsideRedactedMarker(text, start, end) {
701
- for (const marker of text.matchAll(new RegExp(REDACTED_RE.source, "gi"))) {
702
- const markerStart = marker.index ?? 0;
703
- const markerEnd = markerStart + marker[0].length;
704
- if (start >= markerStart && end <= markerEnd) return true;
705
- }
706
- return false;
707
- }
708
- function isNonSecretTokenCapture(value) {
709
- if (value.startsWith("//")) return true;
710
- if (value.includes("/")) return true;
711
- if (/^[a-z0-9.]+(?:-[a-z0-9.]+)+$/.test(value) && !/^[0-9a-f]+(?:-[0-9a-f]+)+$/.test(value)) return true;
712
- return false;
713
- }
714
- var PASSWORD_PROSE_SENTINELS = /* @__PURE__ */ new Set([
715
- "generated",
716
- "configured",
717
- "required",
718
- "optional",
719
- "provided",
720
- "disabled",
721
- "enabled",
722
- "undefined",
723
- "unspecified",
724
- "unknown"
725
- ]);
726
- function isNonSecretPasswordCapture(value) {
727
- return PASSWORD_PROSE_SENTINELS.has(value.toLowerCase());
728
- }
729
- function collectMatches(text) {
730
- const matches = [];
731
- for (const matcher of MATCHERS) {
732
- matcher.re.lastIndex = 0;
733
- for (const m of text.matchAll(matcher.re)) {
734
- const whole = m[0];
735
- const start = m.index ?? 0;
736
- if (REDACTED_RE.test(whole) || isInsideRedactedMarker(text, start, start + whole.length)) continue;
737
- const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
738
- if (isSyntheticPlaceholder(value)) continue;
739
- if (matcher.kind === "token" && isNonSecretTokenCapture(value)) continue;
740
- if (matcher.kind === "password" && isNonSecretPasswordCapture(value)) continue;
741
- const valueOffset = whole.lastIndexOf(value);
742
- const valueStart = start + Math.max(0, valueOffset);
743
- matches.push({
744
- start,
745
- end: start + whole.length,
746
- valueStart,
747
- valueEnd: valueStart + value.length,
748
- kind: matcher.kind
749
- });
750
- }
751
- }
752
- return matches.sort((a, b) => {
753
- if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
754
- return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
755
- });
756
- }
757
- function collapseOverlaps(matches) {
758
- const kept = [];
759
- for (const match of matches) {
760
- const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
761
- if (!overlaps) kept.push(match);
762
- }
763
- return kept;
764
- }
765
- function scanSensitiveContent(text, opts = {}) {
766
- return collapseOverlaps(collectMatches(text)).map((match) => {
767
- const value = text.slice(match.valueStart, match.valueEnd);
768
- const marker = redactMarker(match.kind, value);
769
- const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
770
- const preview = rawPreview.replace(value, marker);
771
- return {
772
- file: opts.file,
773
- line: lineFor(text, match.valueStart),
774
- kind: match.kind,
775
- preview,
776
- fingerprint: fingerprint(value)
777
- };
778
- });
779
- }
780
- function redactSensitiveContent(text, opts = {}) {
781
- const matches = collapseOverlaps(collectMatches(text));
782
- if (matches.length === 0) return { text, changed: false, findings: [] };
783
- let out = "";
784
- let cursor = 0;
785
- for (const match of matches) {
786
- const value = text.slice(match.valueStart, match.valueEnd);
787
- out += text.slice(cursor, match.valueStart);
788
- out += redactMarker(match.kind, value);
789
- cursor = match.valueEnd;
790
- }
791
- out += text.slice(cursor);
792
- return {
793
- text: out,
794
- changed: out !== text,
795
- findings: scanSensitiveContent(text, opts)
796
- };
797
- }
798
-
799
- // src/utils/vault.ts
800
- import { existsSync, readFileSync } from "fs";
801
- import { readFile, readdir, stat } from "fs/promises";
802
- import { join, relative, sep } from "path";
803
- import { execFileSync } from "child_process";
804
- var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
805
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
806
- var DEFAULT_IO_CONCURRENCY = 1;
807
- function vaultIoConcurrency() {
808
- const raw = Number.parseInt(process.env.SKILLWIKI_VAULT_IO_CONCURRENCY ?? "", 10);
809
- return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 64) : DEFAULT_IO_CONCURRENCY;
810
- }
811
- function decodeProcMountPath(value) {
812
- return value.replace(/\\040/g, " ");
813
- }
814
- function isRcloneFuseVaultFromMounts(root, mounts) {
815
- return mounts.split(/\r?\n/).some((line) => {
816
- const parts = line.split(" ");
817
- if (parts.length < 3) return false;
818
- const mountPoint = decodeProcMountPath(parts[1]);
819
- const fsType = parts[2];
820
- return fsType === "fuse.rclone" && (root === mountPoint || root.startsWith(`${mountPoint}/`));
821
- });
822
- }
823
- function resolveReadOnlyVaultRootWithMounts(root, mounts) {
824
- if (/^(1|true|yes)$/i.test(process.env.SKILLWIKI_DISABLE_VAULT_READ_MIRROR ?? "")) {
825
- return { root, mirrored: false };
826
- }
827
- const explicitMirror = process.env.SKILLWIKI_VAULT_READ_MIRROR;
828
- if (explicitMirror && existsSync(join(explicitMirror, "SCHEMA.md"))) {
829
- return { root: explicitMirror, mirrored: explicitMirror !== root };
830
- }
831
- const siblingMirror = `${root}-git`;
832
- if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync(join(siblingMirror, "SCHEMA.md"))) {
833
- return { root: siblingMirror, mirrored: true };
834
- }
835
- return { root, mirrored: false };
836
- }
837
- function resolveReadOnlyVaultRoot(root) {
838
- let mounts = "";
839
- try {
840
- mounts = readFileSync("/proc/mounts", "utf8");
841
- } catch {
842
- }
843
- return resolveReadOnlyVaultRootWithMounts(root, mounts);
844
- }
845
- async function mapWithConcurrency(items, limit, mapper) {
846
- const out = new Array(items.length);
847
- let next = 0;
848
- const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
849
- for (; ; ) {
850
- const index = next++;
851
- if (index >= items.length) return;
852
- out[index] = await mapper(items[index], index);
853
- }
854
- });
855
- await Promise.all(workers);
856
- return out;
857
- }
858
- function filterGitIgnoredRelativePaths(root, relPaths) {
859
- if (relPaths.length === 0 || !existsSync(join(root, ".git"))) {
860
- return /* @__PURE__ */ new Set();
861
- }
862
- try {
863
- const input = relPaths.join("\0") + "\0";
864
- const stdout = execFileSync("git", ["check-ignore", "-z", "--stdin"], {
865
- cwd: root,
866
- input,
867
- encoding: "utf8",
868
- stdio: ["pipe", "pipe", "pipe"]
869
- });
870
- const ignored = stdout.split("\0").filter(Boolean);
871
- return new Set(ignored);
872
- } catch (error) {
873
- if (error && typeof error === "object" && error.status === 1) {
874
- return /* @__PURE__ */ new Set();
875
- }
876
- return /* @__PURE__ */ new Set();
877
- }
878
- }
879
- async function scanVault(root) {
880
- try {
881
- await stat(join(root, "SCHEMA.md"));
882
- } catch {
883
- return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
884
- }
885
- const all = await walk(root);
886
- const rawRels = all.map((p) => ({ absPath: p, relPath: relative(root, p).split(sep).join("/") }));
887
- const ignored = filterGitIgnoredRelativePaths(root, rawRels.map((p) => p.relPath));
888
- const rels = ignored.size > 0 ? rawRels.filter((p) => !ignored.has(p.relPath)) : rawRels;
889
- return ok({
890
- root,
891
- allMarkdown: rels,
892
- typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
893
- raw: rels.filter((p) => p.relPath.startsWith("raw/")),
894
- workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
895
- compound: rels.filter((p) => /^projects\/[^/]+\/compound\//.test(p.relPath))
896
- });
897
- }
898
- async function walk(dir) {
899
- const entries = await readdir(dir, { withFileTypes: true });
900
- const out = [];
901
- const subdirs = [];
902
- for (const e of entries) {
903
- const p = join(dir, e.name);
904
- if (e.isDirectory()) {
905
- if (SKIP_DIRS.has(e.name)) continue;
906
- subdirs.push(p);
907
- } else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
908
- }
909
- const nested = await mapWithConcurrency(subdirs, Math.min(8, vaultIoConcurrency()), walk);
910
- for (const files of nested) out.push(...files);
911
- return out;
912
- }
913
- async function readPage(p) {
914
- return readFile(p.absPath, "utf8");
915
- }
916
- async function readPageCached(p, cache) {
917
- if (!cache) return readPage(p);
918
- const existing = cache.get(p.absPath);
919
- if (existing) return existing;
920
- const pending = readPage(p);
921
- cache.set(p.absPath, pending);
922
- return pending;
923
- }
924
-
925
604
  export {
926
605
  ExitCode,
927
606
  ok,
@@ -938,19 +617,9 @@ export {
938
617
  isClipOutboxKey,
939
618
  parseClipOutboxKeyValue,
940
619
  detectSchema,
620
+ ipv4InCidr,
941
621
  isBlockedHost,
942
622
  getErrorMessage,
943
623
  resolveSessionKind,
944
- systemdPropertyFor,
945
- splitFrontmatter,
946
- extractFrontmatter,
947
- scanSensitiveContent,
948
- redactSensitiveContent,
949
- vaultIoConcurrency,
950
- resolveReadOnlyVaultRoot,
951
- mapWithConcurrency,
952
- filterGitIgnoredRelativePaths,
953
- scanVault,
954
- readPage,
955
- readPageCached
624
+ systemdPropertyFor
956
625
  };