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.
- package/dist/{chunk-5MV5SSRM.js → chunk-5IBLXGLY.js} +5 -3
- package/dist/chunk-6OQLBNKR.js +250 -0
- package/dist/chunk-GQWCTJTU.js +55 -0
- package/dist/{chunk-QU2CKXM4.js → chunk-HBJLFPQP.js} +91 -199
- package/dist/chunk-I2J3IJB7.js +54 -0
- package/dist/{chunk-WPJHDRTP.js → chunk-I6IRJCUJ.js} +12 -57
- package/dist/{chunk-NA3TZ4JN.js → chunk-JQV5KNPT.js} +8 -6
- package/dist/chunk-KWEYA3OE.js +60 -0
- package/dist/{chunk-KX5Y3GSX.js → chunk-MRPJ7X7W.js} +5 -3
- package/dist/{chunk-6X6PPQNI.js → chunk-OJJBZDPR.js} +162 -296
- package/dist/chunk-OSL4J7GF.js +32 -0
- package/dist/chunk-PYPPG7RF.js +126 -0
- package/dist/chunk-QESNTTVY.js +50 -0
- package/dist/chunk-TDBUJUYH.js +109 -0
- package/dist/{chunk-R2DJ7IJ5.js → chunk-UIEQIJXP.js} +5 -336
- package/dist/chunk-X5GL2FCU.js +344 -0
- package/dist/chunk-ZRGZBAVS.js +98 -0
- package/dist/cli.js +308 -298
- package/dist/{index-projection-6XGESSHV.js → index-projection-2K672CBO.js} +5 -3
- package/dist/journal-QU5IDXQF.js +13 -0
- package/dist/{managed-write-preflight-JPOBVVSU.js → managed-write-preflight-GRSHQYKC.js} +7 -4
- package/dist/migrate-PIB4QA54.js +21 -0
- package/dist/ready-S7F53Q2D.js +9 -0
- package/dist/runtime-GLMI5OCT.js +15 -0
- package/dist/skillwiki-mcp.js +12 -7
- package/dist/sources-D2MGP246.js +10 -0
- package/dist/upload-HTE62TOV.js +14 -0
- package/dist/watcher-MHG46J3R.js +616 -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/templates/launchd/com.karlchow.skillwiki-clip-outbox.plist +12 -13
- 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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
};
|