skillwiki 0.10.110 → 0.10.112
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/chunk-25OXNFDV.js +133 -0
- package/dist/{chunk-R2DJ7IJ5.js → chunk-7POLZOFY.js} +44 -375
- package/dist/chunk-ABRYBO4T.js +275 -0
- package/dist/{chunk-6X6PPQNI.js → chunk-APKDTEVV.js} +280 -446
- package/dist/{chunk-5MV5SSRM.js → chunk-CKQPUHM2.js} +5 -3
- package/dist/chunk-DOHWNPAZ.js +60 -0
- package/dist/chunk-EE7W3GLA.js +55 -0
- package/dist/chunk-FCA3RRNF.js +60 -0
- package/dist/{chunk-KX5Y3GSX.js → chunk-IR2DI4Y5.js} +5 -3
- package/dist/chunk-K3M6OSUW.js +77 -0
- package/dist/{chunk-WPJHDRTP.js → chunk-KJG2CSYD.js} +12 -57
- package/dist/chunk-LOVLZNLX.js +111 -0
- package/dist/chunk-OSL4J7GF.js +32 -0
- package/dist/{chunk-QU2CKXM4.js → chunk-Q6EZF37H.js} +91 -199
- package/dist/chunk-QESNTTVY.js +50 -0
- package/dist/{chunk-NA3TZ4JN.js → chunk-RR6VDWSI.js} +8 -6
- package/dist/chunk-SKOLE6US.js +126 -0
- package/dist/chunk-TKX6PSWZ.js +344 -0
- package/dist/chunk-ZRGZBAVS.js +98 -0
- package/dist/cli.js +359 -305
- package/dist/{index-projection-6XGESSHV.js → index-projection-UNZG5UJZ.js} +5 -3
- package/dist/journal-5IW3TZ5F.js +13 -0
- package/dist/{managed-write-preflight-JPOBVVSU.js → managed-write-preflight-L7IRQ76C.js} +7 -4
- package/dist/mcp-auth-env-X5MI5IX6.js +25 -0
- package/dist/migrate-RO4BJP5T.js +22 -0
- package/dist/mirror-H5BTBAON.js +151 -0
- package/dist/ready-S7F53Q2D.js +9 -0
- package/dist/runtime-VMFYW2K3.js +20 -0
- package/dist/skillwiki-mcp.js +14 -7
- package/dist/sources-AOYP6ROO.js +10 -0
- package/dist/upload-HTE62TOV.js +14 -0
- package/dist/watcher-MGHQD3UI.js +526 -0
- package/docs/clip-outbox.md +119 -0
- package/package.json +5 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/.cursor-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/skills/wiki-init/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/wiki-init/SKILL.md +3 -0
- package/templates/launchd/com.karlchow.skillwiki-clip-outbox.plist +12 -13
- package/templates/launchd/com.karlchow.skillwiki-mirror-sse.plist +20 -0
- package/dist/sources-CLOF5VAT.js +0 -9
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteText
|
|
4
|
+
} from "./chunk-EE7W3GLA.js";
|
|
5
|
+
import {
|
|
6
|
+
PRODUCTION_MIRROR_LABEL,
|
|
7
|
+
PRODUCTION_WATCHER_LABEL,
|
|
8
|
+
evidencePath,
|
|
9
|
+
expandUserPath,
|
|
10
|
+
loadClipCredentials,
|
|
11
|
+
nestedOrEqual,
|
|
12
|
+
resolveClipOutboxConfig,
|
|
13
|
+
validateRuntime
|
|
14
|
+
} from "./chunk-ABRYBO4T.js";
|
|
15
|
+
import {
|
|
16
|
+
parseDotenvFile
|
|
17
|
+
} from "./chunk-SKOLE6US.js";
|
|
18
|
+
|
|
19
|
+
// src/clip-outbox/migrate.ts
|
|
20
|
+
import { access, mkdir, readFile } from "fs/promises";
|
|
21
|
+
import { constants } from "fs";
|
|
22
|
+
import { basename, dirname, isAbsolute, join } from "path";
|
|
23
|
+
import { execFileSync } from "child_process";
|
|
24
|
+
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 --load. Inbound SSE uses a separate --load-mirror and never watches ~/wiki as clip input.";
|
|
25
|
+
var xml = (s) => s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
26
|
+
async function packagedPlist(label = PRODUCTION_WATCHER_LABEL) {
|
|
27
|
+
for (const rel of ["../templates/launchd/", "../../templates/launchd/"]) {
|
|
28
|
+
try {
|
|
29
|
+
return await readFile(new URL(`${rel}${label}.plist`, import.meta.url), "utf8");
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error("packaged clip launchd template missing");
|
|
34
|
+
}
|
|
35
|
+
async function stableCliBinary(argvPath = process.argv[1]) {
|
|
36
|
+
const candidates = [argvPath, ...[dirname(process.execPath), ...(process.env.PATH ?? "").split(":")].map((dir) => join(dir, "skillwiki"))];
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (!candidate || !isAbsolute(candidate) || basename(candidate) !== "skillwiki") continue;
|
|
39
|
+
try {
|
|
40
|
+
await access(candidate, constants.X_OK);
|
|
41
|
+
return candidate;
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error("global npm skillwiki binary not found; install/link the CLI before migrating");
|
|
46
|
+
}
|
|
47
|
+
async function migrateClipOutbox(input) {
|
|
48
|
+
const map = await parseDotenvFile(join(input.home, ".skillwiki", ".env"));
|
|
49
|
+
const resolved = resolveClipOutboxConfig(map, input.home);
|
|
50
|
+
if (!resolved.ok) throw new Error(resolved.message);
|
|
51
|
+
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");
|
|
52
|
+
if (input.loadMirror && !resolved.enabled) throw new Error("--load-mirror requires existing clip_outbox.enabled=true; upgrades never auto-load the inbound SSE listener");
|
|
53
|
+
if ((input.platform ?? process.platform) !== "darwin") throw new Error("clip launchd migration requires macOS");
|
|
54
|
+
const rawRoot = map["clip_outbox.root"] ?? "~/Obsidian/clip-outbox";
|
|
55
|
+
const root = resolved.enabled ? resolved.config.root : expandUserPath(rawRoot, input.home);
|
|
56
|
+
if (!isAbsolute(root) || root.includes("\0") || rawRoot.split("/").includes("..")) throw new Error("invalid outbox root");
|
|
57
|
+
const mirror = resolved.enabled ? resolved.config.mirrorRoot : expandUserPath(map["clip_outbox.mirror_root"] ?? join(input.home, "wiki"), input.home);
|
|
58
|
+
if (nestedOrEqual(root, mirror)) throw new Error("outbox must be disjoint from mirror");
|
|
59
|
+
const binary = input.binaryPath ?? await stableCliBinary();
|
|
60
|
+
if (!isAbsolute(binary) || !isAbsolute(input.nodePath ?? process.execPath)) throw new Error("absolute executable paths required");
|
|
61
|
+
const logRoot = join(input.home, "Library", "Logs", "SkillWiki");
|
|
62
|
+
const plistPath = join(input.home, "Library", "LaunchAgents", `${PRODUCTION_WATCHER_LABEL}.plist`);
|
|
63
|
+
const mirrorPlistPath = join(input.home, "Library", "LaunchAgents", `${PRODUCTION_MIRROR_LABEL}.plist`);
|
|
64
|
+
const replacements = { NODE_BINARY: input.nodePath ?? process.execPath, CLI_BINARY: binary, OUTBOX_ROOT: root, HOME: input.home, LOG_ROOT: logRoot };
|
|
65
|
+
const render = async (label) => {
|
|
66
|
+
let plist = await packagedPlist(label);
|
|
67
|
+
for (const [key, value] of Object.entries(replacements)) plist = plist.replaceAll(`@${key}@`, xml(value));
|
|
68
|
+
if (label === PRODUCTION_MIRROR_LABEL && plist.includes("<key>WatchPaths</key>")) throw new Error("inbound SSE plist must not watch the frozen mirror");
|
|
69
|
+
return plist;
|
|
70
|
+
};
|
|
71
|
+
const watcherPlist = await render(PRODUCTION_WATCHER_LABEL);
|
|
72
|
+
const mirrorPlist = await render(PRODUCTION_MIRROR_LABEL);
|
|
73
|
+
if (input.load) {
|
|
74
|
+
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");
|
|
75
|
+
const creds = await loadClipCredentials(input.home, resolved.config);
|
|
76
|
+
await mkdir(resolved.config.stateRoot, { recursive: true, mode: 448 });
|
|
77
|
+
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" }));
|
|
78
|
+
if (!proof.ok) throw new Error("cannot record operator conditional-create attestation");
|
|
79
|
+
await validateRuntime(input.home, resolved.config, creds.s3.endpoint);
|
|
80
|
+
}
|
|
81
|
+
await mkdir(dirname(plistPath), { recursive: true });
|
|
82
|
+
await mkdir(logRoot, { recursive: true });
|
|
83
|
+
const result = await atomicWriteText(plistPath, watcherPlist);
|
|
84
|
+
if (!result.ok) throw new Error("cannot refresh clip plist");
|
|
85
|
+
const mirrorResult = await atomicWriteText(mirrorPlistPath, mirrorPlist);
|
|
86
|
+
if (!mirrorResult.ok) throw new Error("cannot refresh inbound SSE plist");
|
|
87
|
+
const bootstrap = (label, path) => {
|
|
88
|
+
if (typeof process.getuid !== "function") throw new Error("clip launchd load requires macOS");
|
|
89
|
+
const domain = `gui/${process.getuid()}`;
|
|
90
|
+
const launch = input.launch ?? ((args) => {
|
|
91
|
+
execFileSync("launchctl", args, { stdio: "ignore", timeout: 5e3 });
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
launch(["bootout", `${domain}/${label}`]);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
launch(["enable", `${domain}/${label}`]);
|
|
98
|
+
launch(["bootstrap", domain, path]);
|
|
99
|
+
};
|
|
100
|
+
if (input.load) bootstrap(PRODUCTION_WATCHER_LABEL, plistPath);
|
|
101
|
+
if (input.loadMirror) bootstrap(PRODUCTION_MIRROR_LABEL, mirrorPlistPath);
|
|
102
|
+
const loadedBits = [
|
|
103
|
+
input.load ? "Operator-gated clip watcher bootstrap requested." : void 0,
|
|
104
|
+
input.loadMirror ? "Operator-gated inbound SSE bootstrap requested." : void 0
|
|
105
|
+
].filter(Boolean).join(" ");
|
|
106
|
+
return {
|
|
107
|
+
plistPath,
|
|
108
|
+
mirrorPlistPath,
|
|
109
|
+
loaded: !!input.load,
|
|
110
|
+
mirrorLoaded: !!input.loadMirror,
|
|
111
|
+
enabled: resolved.enabled,
|
|
112
|
+
humanHint: `${result.data.changed ? "Refreshed" : "Current"} clip launchd plist: ${plistPath}. ${mirrorResult.data.changed ? "Refreshed" : "Current"} inbound SSE plist: ${mirrorPlistPath}. ${loadedBits || CLIP_MIGRATION_HINT}`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function refreshInstalledClipPlist(home, opts) {
|
|
116
|
+
const plistPath = join(home, "Library", "LaunchAgents", `${PRODUCTION_WATCHER_LABEL}.plist`);
|
|
117
|
+
try {
|
|
118
|
+
await access(plistPath, constants.F_OK);
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
const result = await migrateClipOutbox({ ...opts, home, load: false });
|
|
123
|
+
return result.humanHint;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
CLIP_MIGRATION_HINT,
|
|
128
|
+
xml,
|
|
129
|
+
packagedPlist,
|
|
130
|
+
stableCliBinary,
|
|
131
|
+
migrateClipOutbox,
|
|
132
|
+
refreshInstalledClipPlist
|
|
133
|
+
};
|
|
@@ -73,6 +73,48 @@ function err(error, detail) {
|
|
|
73
73
|
return detail === void 0 ? { ok: false, error } : { ok: false, error, detail };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// ../shared/src/blocked-hosts.ts
|
|
77
|
+
var METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
78
|
+
"metadata.google.internal",
|
|
79
|
+
"metadata"
|
|
80
|
+
]);
|
|
81
|
+
var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
|
|
82
|
+
function ipv4ToInt(ip) {
|
|
83
|
+
const parts = ip.split(".");
|
|
84
|
+
if (parts.length !== 4) return null;
|
|
85
|
+
let n = 0;
|
|
86
|
+
for (const p of parts) {
|
|
87
|
+
const v = Number(p);
|
|
88
|
+
if (!Number.isInteger(v) || v < 0 || v > 255) return null;
|
|
89
|
+
n = (n << 8) + v;
|
|
90
|
+
}
|
|
91
|
+
return n >>> 0;
|
|
92
|
+
}
|
|
93
|
+
function inRange(ip, baseStr, prefix) {
|
|
94
|
+
const ipN = ipv4ToInt(ip);
|
|
95
|
+
const baseN = ipv4ToInt(baseStr);
|
|
96
|
+
if (ipN === null || baseN === null) return false;
|
|
97
|
+
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
98
|
+
return (ipN & mask) === (baseN & mask);
|
|
99
|
+
}
|
|
100
|
+
function ipv4InCidr(ip, base, prefix) {
|
|
101
|
+
return inRange(ip, base, prefix);
|
|
102
|
+
}
|
|
103
|
+
function isBlockedHost(host) {
|
|
104
|
+
const lower = host.toLowerCase();
|
|
105
|
+
if (METADATA_HOSTS.has(lower)) return true;
|
|
106
|
+
if (METADATA_IPS.has(host)) return true;
|
|
107
|
+
if (lower === "::1") return true;
|
|
108
|
+
if (lower.startsWith("fe80:")) return true;
|
|
109
|
+
if (ipv4ToInt(host) === null) return false;
|
|
110
|
+
if (inRange(host, "10.0.0.0", 8)) return true;
|
|
111
|
+
if (inRange(host, "172.16.0.0", 12)) return true;
|
|
112
|
+
if (inRange(host, "192.168.0.0", 16)) return true;
|
|
113
|
+
if (inRange(host, "169.254.0.0", 16)) return true;
|
|
114
|
+
if (inRange(host, "127.0.0.0", 8)) return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
76
118
|
// ../shared/src/session-kind.ts
|
|
77
119
|
function resolveSessionKind(input = {}) {
|
|
78
120
|
const env = input.env ?? {};
|
|
@@ -531,45 +573,6 @@ function detectSchema(fm) {
|
|
|
531
573
|
return { schema: null };
|
|
532
574
|
}
|
|
533
575
|
|
|
534
|
-
// ../shared/src/blocked-hosts.ts
|
|
535
|
-
var METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
536
|
-
"metadata.google.internal",
|
|
537
|
-
"metadata"
|
|
538
|
-
]);
|
|
539
|
-
var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
|
|
540
|
-
function ipv4ToInt(ip) {
|
|
541
|
-
const parts = ip.split(".");
|
|
542
|
-
if (parts.length !== 4) return null;
|
|
543
|
-
let n = 0;
|
|
544
|
-
for (const p of parts) {
|
|
545
|
-
const v = Number(p);
|
|
546
|
-
if (!Number.isInteger(v) || v < 0 || v > 255) return null;
|
|
547
|
-
n = (n << 8) + v;
|
|
548
|
-
}
|
|
549
|
-
return n >>> 0;
|
|
550
|
-
}
|
|
551
|
-
function inRange(ip, baseStr, prefix) {
|
|
552
|
-
const ipN = ipv4ToInt(ip);
|
|
553
|
-
const baseN = ipv4ToInt(baseStr);
|
|
554
|
-
if (ipN === null || baseN === null) return false;
|
|
555
|
-
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
556
|
-
return (ipN & mask) === (baseN & mask);
|
|
557
|
-
}
|
|
558
|
-
function isBlockedHost(host) {
|
|
559
|
-
const lower = host.toLowerCase();
|
|
560
|
-
if (METADATA_HOSTS.has(lower)) return true;
|
|
561
|
-
if (METADATA_IPS.has(host)) return true;
|
|
562
|
-
if (lower === "::1") return true;
|
|
563
|
-
if (lower.startsWith("fe80:")) return true;
|
|
564
|
-
if (ipv4ToInt(host) === null) return false;
|
|
565
|
-
if (inRange(host, "10.0.0.0", 8)) return true;
|
|
566
|
-
if (inRange(host, "172.16.0.0", 12)) return true;
|
|
567
|
-
if (inRange(host, "192.168.0.0", 16)) return true;
|
|
568
|
-
if (inRange(host, "169.254.0.0", 16)) return true;
|
|
569
|
-
if (inRange(host, "127.0.0.0", 8)) return true;
|
|
570
|
-
return false;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
576
|
// ../shared/src/error-message.ts
|
|
574
577
|
function getErrorMessage(e) {
|
|
575
578
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -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
|
};
|