skillwiki 0.10.111 → 0.10.113
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-I6IRJCUJ.js → chunk-3TKNAOJH.js} +2 -2
- package/dist/chunk-5D5HFQ76.js +111 -0
- package/dist/{chunk-OJJBZDPR.js → chunk-5RHR4FEK.js} +183 -211
- package/dist/{chunk-GQWCTJTU.js → chunk-6VUK2FTX.js} +1 -1
- package/dist/{chunk-JQV5KNPT.js → chunk-CBT35FAW.js} +3 -3
- package/dist/{chunk-HBJLFPQP.js → chunk-FQERKMD7.js} +458 -18
- package/dist/{chunk-PYPPG7RF.js → chunk-IJKJGQKL.js} +1 -1
- package/dist/{chunk-MRPJ7X7W.js → chunk-IJWZDQDH.js} +3 -3
- package/dist/{chunk-X5GL2FCU.js → chunk-JL2SCLXT.js} +1 -1
- package/dist/chunk-K3M6OSUW.js +77 -0
- package/dist/{chunk-5IBLXGLY.js → chunk-K3YZESND.js} +2 -2
- package/dist/chunk-OGEAFXON.js +60 -0
- package/dist/{chunk-6OQLBNKR.js → chunk-OWWWO2XS.js} +29 -4
- package/dist/{chunk-KWEYA3OE.js → chunk-SEY4N4CT.js} +1 -1
- package/dist/{chunk-UIEQIJXP.js → chunk-VNSZCK5V.js} +47 -44
- package/dist/{chunk-TDBUJUYH.js → chunk-XJ5VEIH7.js} +42 -18
- package/dist/cli.js +586 -122
- package/dist/{index-projection-2K672CBO.js → index-projection-RQSOEDXM.js} +5 -5
- package/dist/{journal-QU5IDXQF.js → journal-V5XHBEI5.js} +3 -3
- package/dist/{managed-write-preflight-GRSHQYKC.js → managed-write-preflight-2BMHZWVJ.js} +7 -7
- package/dist/mcp-auth-env-QONKW4EO.js +25 -0
- package/dist/{migrate-PIB4QA54.js → migrate-SRRCXI2K.js} +6 -5
- package/dist/mirror-H5BTBAON.js +151 -0
- package/dist/runtime-LQZIG3XN.js +20 -0
- package/dist/skillwiki-mcp.js +14 -12
- package/dist/sources-E6KHDRDE.js +10 -0
- package/dist/{watcher-MHG46J3R.js → watcher-VJ3TOU6J.js} +20 -110
- package/package.json +1 -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-ingest/SKILL.md +47 -0
- package/skills/skills/wiki-init/SKILL.md +3 -0
- package/skills/skills/wiki-lint/SKILL.md +7 -0
- package/skills/using-skillwiki/SKILL.md +1 -1
- package/skills/wiki-ingest/SKILL.md +47 -0
- package/skills/wiki-init/SKILL.md +3 -0
- package/skills/wiki-lint/SKILL.md +7 -0
- package/templates/launchd/com.karlchow.skillwiki-mirror-sse.plist +20 -0
- package/dist/chunk-I2J3IJB7.js +0 -54
- package/dist/runtime-GLMI5OCT.js +0 -15
- package/dist/sources-D2MGP246.js +0 -10
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/clip-outbox/files.ts
|
|
4
|
+
import { constants } from "fs";
|
|
5
|
+
import { lstat, mkdir, open, realpath } from "fs/promises";
|
|
6
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
7
|
+
async function readRegular(root, path, maxBytes) {
|
|
8
|
+
const rel = relative(root, path);
|
|
9
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel)) throw new Error("unsafe input path");
|
|
10
|
+
let current = resolve(root);
|
|
11
|
+
for (const part of ["", ...rel.split(sep)]) {
|
|
12
|
+
current = resolve(current, part);
|
|
13
|
+
const st = await lstat(current);
|
|
14
|
+
if (st.isSymbolicLink()) throw new Error("symlink input refused");
|
|
15
|
+
if (current !== path && !st.isDirectory()) throw new Error("invalid input parent");
|
|
16
|
+
}
|
|
17
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
18
|
+
try {
|
|
19
|
+
const before = await handle.stat();
|
|
20
|
+
if (!before.isFile() || before.size > maxBytes) throw new Error("input type or size refused");
|
|
21
|
+
const bytes = Buffer.alloc(before.size + 1);
|
|
22
|
+
let bytesRead = 0;
|
|
23
|
+
while (bytesRead < bytes.length) {
|
|
24
|
+
const chunk = await handle.read(bytes, bytesRead, bytes.length - bytesRead, bytesRead);
|
|
25
|
+
if (chunk.bytesRead === 0) break;
|
|
26
|
+
bytesRead += chunk.bytesRead;
|
|
27
|
+
}
|
|
28
|
+
const after = await handle.stat();
|
|
29
|
+
if (await realpath(path) !== path) throw new Error("input path changed during read");
|
|
30
|
+
const leaf = await lstat(path);
|
|
31
|
+
if (leaf.ino !== before.ino || leaf.dev !== before.dev) throw new Error("input identity changed during read");
|
|
32
|
+
if (bytesRead !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) throw new Error("input changed during read");
|
|
33
|
+
return bytes.subarray(0, bytesRead);
|
|
34
|
+
} finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function writeRegularExclusive(root, rel, body) {
|
|
39
|
+
const dest = resolve(root, ...rel.split("/"));
|
|
40
|
+
const checked = relative(root, dest);
|
|
41
|
+
if (!checked || checked.startsWith(`..${sep}`) || checked === ".." || isAbsolute(checked)) throw new Error("unsafe dest path");
|
|
42
|
+
let current = resolve(root);
|
|
43
|
+
const parts = checked.split(sep);
|
|
44
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
45
|
+
current = resolve(current, parts[i]);
|
|
46
|
+
try {
|
|
47
|
+
const st = await lstat(current);
|
|
48
|
+
if (st.isSymbolicLink()) throw new Error("symlink dest refused");
|
|
49
|
+
if (!st.isDirectory()) throw new Error("invalid dest parent");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== "ENOENT") throw error;
|
|
52
|
+
await mkdir(current);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const leaf = resolve(current, parts[parts.length - 1]);
|
|
56
|
+
if (dirname(leaf) !== current) throw new Error("unsafe dest path");
|
|
57
|
+
try {
|
|
58
|
+
const handle = await open(leaf, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 420);
|
|
59
|
+
try {
|
|
60
|
+
await handle.writeFile(body);
|
|
61
|
+
} finally {
|
|
62
|
+
await handle.close();
|
|
63
|
+
}
|
|
64
|
+
return "created";
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code !== "EEXIST") throw error;
|
|
67
|
+
const st = await lstat(leaf);
|
|
68
|
+
if (st.isSymbolicLink() || !st.isFile() || st.size !== body.length) return "conflict";
|
|
69
|
+
const existing = await readRegular(root, leaf, body.length);
|
|
70
|
+
return existing.equals(body) ? "idempotent" : "conflict";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
readRegular,
|
|
76
|
+
writeRegularExclusive
|
|
77
|
+
};
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
readPage,
|
|
5
5
|
scanSensitiveContent,
|
|
6
6
|
splitFrontmatter
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-JL2SCLXT.js";
|
|
8
8
|
import {
|
|
9
9
|
err,
|
|
10
10
|
ok
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-VNSZCK5V.js";
|
|
12
12
|
|
|
13
13
|
// src/utils/vault-path-safety.ts
|
|
14
14
|
import { lstatSync, realpathSync } from "fs";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseDotenvFile,
|
|
4
|
+
resolveSkillwikiHome
|
|
5
|
+
} from "./chunk-IJKJGQKL.js";
|
|
6
|
+
|
|
7
|
+
// src/utils/mcp-auth-env.ts
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
var MCP_URL_ENV = "SKILLWIKI_MCP_URL";
|
|
10
|
+
var MCP_TOKEN_ENV = "SKILLWIKI_MCP_TOKEN";
|
|
11
|
+
var MCP_HOST_ID_ENV = "SKILLWIKI_HOST_ID";
|
|
12
|
+
function nonEmptyEnvValue(value) {
|
|
13
|
+
if (typeof value !== "string") return void 0;
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
16
|
+
}
|
|
17
|
+
function mcpAuthFromEnv(env) {
|
|
18
|
+
return {
|
|
19
|
+
url: nonEmptyEnvValue(env[MCP_URL_ENV]),
|
|
20
|
+
token: nonEmptyEnvValue(env[MCP_TOKEN_ENV]),
|
|
21
|
+
hostId: nonEmptyEnvValue(env[MCP_HOST_ID_ENV])
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function resolveMcpAuthEnv(input) {
|
|
25
|
+
const processAuth = mcpAuthFromEnv(input.env ?? process.env);
|
|
26
|
+
const dotenv = await parseDotenvFile(join(resolveSkillwikiHome(input.home, input.env), ".skillwiki", ".env"));
|
|
27
|
+
const fileAuth = mcpAuthFromEnv(dotenv);
|
|
28
|
+
return {
|
|
29
|
+
url: processAuth.url ?? fileAuth.url,
|
|
30
|
+
token: processAuth.token ?? fileAuth.token,
|
|
31
|
+
hostId: processAuth.hostId ?? fileAuth.hostId
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function mergeMcpAuthIntoEnv(home, env) {
|
|
35
|
+
const auth = await resolveMcpAuthEnv({ home, env });
|
|
36
|
+
const merged = { ...env };
|
|
37
|
+
if (auth.url && !nonEmptyEnvValue(env[MCP_URL_ENV])) merged[MCP_URL_ENV] = auth.url;
|
|
38
|
+
if (auth.token && !nonEmptyEnvValue(env[MCP_TOKEN_ENV])) merged[MCP_TOKEN_ENV] = auth.token;
|
|
39
|
+
if (auth.hostId && !nonEmptyEnvValue(env[MCP_HOST_ID_ENV])) merged[MCP_HOST_ID_ENV] = auth.hostId;
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
function redactMcpSecret(text, secret) {
|
|
43
|
+
if (!secret || secret.length === 0 || !text.includes(secret)) return text;
|
|
44
|
+
return text.split(secret).join("[REDACTED]");
|
|
45
|
+
}
|
|
46
|
+
function isMcpOnlyLeaf(input) {
|
|
47
|
+
return !input.resolvedPath && Boolean(input.token) && !input.vaultSyncInstalled;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
MCP_URL_ENV,
|
|
52
|
+
MCP_TOKEN_ENV,
|
|
53
|
+
MCP_HOST_ID_ENV,
|
|
54
|
+
nonEmptyEnvValue,
|
|
55
|
+
mcpAuthFromEnv,
|
|
56
|
+
resolveMcpAuthEnv,
|
|
57
|
+
mergeMcpAuthIntoEnv,
|
|
58
|
+
redactMcpSecret,
|
|
59
|
+
isMcpOnlyLeaf
|
|
60
|
+
};
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
defaultLookup
|
|
4
|
+
} from "./chunk-5D5HFQ76.js";
|
|
2
5
|
import {
|
|
3
6
|
parseDotenvFile,
|
|
4
7
|
resolveSkillwikiHome
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-IJKJGQKL.js";
|
|
6
9
|
import {
|
|
7
10
|
CLIP_OUTBOX_DEFAULTS,
|
|
11
|
+
ipv4InCidr,
|
|
8
12
|
isClipOutboxKey,
|
|
9
13
|
parseClipOutboxKeyValue
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-VNSZCK5V.js";
|
|
11
15
|
|
|
12
16
|
// src/clip-outbox/runtime.ts
|
|
13
17
|
import { existsSync as existsSync2 } from "fs";
|
|
14
18
|
import { lstat, readFile } from "fs/promises";
|
|
19
|
+
import { isIP } from "net";
|
|
15
20
|
import { join as join2 } from "path";
|
|
16
21
|
import { execFileSync } from "child_process";
|
|
17
22
|
import { z } from "zod";
|
|
@@ -22,6 +27,7 @@ import { homedir } from "os";
|
|
|
22
27
|
import { isAbsolute, join, resolve, sep } from "path";
|
|
23
28
|
var CLIP_NOT_CONFIGURED = "clip channel not configured";
|
|
24
29
|
var PRODUCTION_WATCHER_LABEL = CLIP_OUTBOX_DEFAULTS.watcher_label;
|
|
30
|
+
var PRODUCTION_MIRROR_LABEL = "com.karlchow.skillwiki-mirror-sse";
|
|
25
31
|
var RETIRED_WATCHER_LABELS = [
|
|
26
32
|
"com.karlchow.wiki-dev-outbox",
|
|
27
33
|
"com.karlchow.wiki-dev-sse",
|
|
@@ -188,6 +194,23 @@ async function loadClipOutboxConfig(home) {
|
|
|
188
194
|
|
|
189
195
|
// src/clip-outbox/runtime.ts
|
|
190
196
|
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();
|
|
197
|
+
function isRfc1918OrLoopback(address) {
|
|
198
|
+
const version = isIP(address);
|
|
199
|
+
if (version === 4) {
|
|
200
|
+
return ipv4InCidr(address, "10.0.0.0", 8) || ipv4InCidr(address, "172.16.0.0", 12) || ipv4InCidr(address, "192.168.0.0", 16) || ipv4InCidr(address, "127.0.0.0", 8);
|
|
201
|
+
}
|
|
202
|
+
return version === 6 && address.toLowerCase() === "::1";
|
|
203
|
+
}
|
|
204
|
+
async function assertClipS3Endpoint(endpoint, lookup = defaultLookup) {
|
|
205
|
+
const url = new URL(endpoint);
|
|
206
|
+
if (url.username || url.password || url.search || url.hash) throw new Error("invalid clip S3 endpoint");
|
|
207
|
+
if (url.protocol === "https:") return;
|
|
208
|
+
if (url.protocol !== "http:") throw new Error("invalid clip S3 endpoint");
|
|
209
|
+
const addresses = isIP(url.hostname) ? [url.hostname] : await lookup(url.hostname);
|
|
210
|
+
if (addresses.length === 0 || !addresses.every(isRfc1918OrLoopback)) {
|
|
211
|
+
throw new Error("http clip S3 endpoint must resolve only to RFC1918 or loopback");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
191
214
|
async function loadClipCredentials(home, config, env = process.env) {
|
|
192
215
|
let providers = {};
|
|
193
216
|
const path = join2(home, ".skillwiki", "clip-outbox-credentials.json");
|
|
@@ -201,8 +224,7 @@ async function loadClipCredentials(home, config, env = process.env) {
|
|
|
201
224
|
try {
|
|
202
225
|
const rawS3 = env[config.s3CredentialRef] ?? providers[config.s3CredentialRef];
|
|
203
226
|
const s3 = s3Schema.parse(typeof rawS3 === "string" ? JSON.parse(rawS3) : rawS3);
|
|
204
|
-
|
|
205
|
-
if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error();
|
|
227
|
+
await assertClipS3Endpoint(s3.endpoint);
|
|
206
228
|
const hostConfig = await parseDotenvFile(join2(home, ".skillwiki", ".env"));
|
|
207
229
|
const readyToken = env[config.readyCredentialRef] ?? providers[config.readyCredentialRef];
|
|
208
230
|
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();
|
|
@@ -238,11 +260,14 @@ async function validateRuntime(home, config, endpoint) {
|
|
|
238
260
|
export {
|
|
239
261
|
CLIP_NOT_CONFIGURED,
|
|
240
262
|
PRODUCTION_WATCHER_LABEL,
|
|
263
|
+
PRODUCTION_MIRROR_LABEL,
|
|
241
264
|
RETIRED_WATCHER_LABELS,
|
|
242
265
|
expandUserPath,
|
|
243
266
|
nestedOrEqual,
|
|
244
267
|
resolveClipOutboxConfig,
|
|
245
268
|
loadClipOutboxConfig,
|
|
269
|
+
isRfc1918OrLoopback,
|
|
270
|
+
assertClipS3Endpoint,
|
|
246
271
|
loadClipCredentials,
|
|
247
272
|
evidencePath,
|
|
248
273
|
conditionalCreateProven,
|
|
@@ -62,7 +62,8 @@ var ExitCode = {
|
|
|
62
62
|
APPROVAL_MISMATCH: 57,
|
|
63
63
|
TARGET_DRIFT: 58,
|
|
64
64
|
RECOVERY_EVIDENCE_MISSING: 59,
|
|
65
|
-
SYNC_PARTIAL_DECLINE: 60
|
|
65
|
+
SYNC_PARTIAL_DECLINE: 60,
|
|
66
|
+
HTML_RENDER_NOT_OPTED_IN: 61
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
// ../shared/src/json-output.ts
|
|
@@ -73,6 +74,48 @@ function err(error, detail) {
|
|
|
73
74
|
return detail === void 0 ? { ok: false, error } : { ok: false, error, detail };
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// ../shared/src/blocked-hosts.ts
|
|
78
|
+
var METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
79
|
+
"metadata.google.internal",
|
|
80
|
+
"metadata"
|
|
81
|
+
]);
|
|
82
|
+
var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
|
|
83
|
+
function ipv4ToInt(ip) {
|
|
84
|
+
const parts = ip.split(".");
|
|
85
|
+
if (parts.length !== 4) return null;
|
|
86
|
+
let n = 0;
|
|
87
|
+
for (const p of parts) {
|
|
88
|
+
const v = Number(p);
|
|
89
|
+
if (!Number.isInteger(v) || v < 0 || v > 255) return null;
|
|
90
|
+
n = (n << 8) + v;
|
|
91
|
+
}
|
|
92
|
+
return n >>> 0;
|
|
93
|
+
}
|
|
94
|
+
function inRange(ip, baseStr, prefix) {
|
|
95
|
+
const ipN = ipv4ToInt(ip);
|
|
96
|
+
const baseN = ipv4ToInt(baseStr);
|
|
97
|
+
if (ipN === null || baseN === null) return false;
|
|
98
|
+
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
99
|
+
return (ipN & mask) === (baseN & mask);
|
|
100
|
+
}
|
|
101
|
+
function ipv4InCidr(ip, base, prefix) {
|
|
102
|
+
return inRange(ip, base, prefix);
|
|
103
|
+
}
|
|
104
|
+
function isBlockedHost(host) {
|
|
105
|
+
const lower = host.toLowerCase();
|
|
106
|
+
if (METADATA_HOSTS.has(lower)) return true;
|
|
107
|
+
if (METADATA_IPS.has(host)) return true;
|
|
108
|
+
if (lower === "::1") return true;
|
|
109
|
+
if (lower.startsWith("fe80:")) return true;
|
|
110
|
+
if (ipv4ToInt(host) === null) return false;
|
|
111
|
+
if (inRange(host, "10.0.0.0", 8)) return true;
|
|
112
|
+
if (inRange(host, "172.16.0.0", 12)) return true;
|
|
113
|
+
if (inRange(host, "192.168.0.0", 16)) return true;
|
|
114
|
+
if (inRange(host, "169.254.0.0", 16)) return true;
|
|
115
|
+
if (inRange(host, "127.0.0.0", 8)) return true;
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
// ../shared/src/session-kind.ts
|
|
77
120
|
function resolveSessionKind(input = {}) {
|
|
78
121
|
const env = input.env ?? {};
|
|
@@ -154,7 +197,9 @@ var TypedKnowledgeSchema = z.object({
|
|
|
154
197
|
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
155
198
|
provenance_projects: z.array(wikilink).optional(),
|
|
156
199
|
work_items: z.array(wikilink).optional(),
|
|
157
|
-
stale_ttl: z.number().int().positive().optional()
|
|
200
|
+
stale_ttl: z.number().int().positive().optional(),
|
|
201
|
+
output_standard: z.literal("v2").optional(),
|
|
202
|
+
html_render: z.boolean().optional()
|
|
158
203
|
}).superRefine((v, ctx) => {
|
|
159
204
|
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
160
205
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
@@ -531,48 +576,6 @@ function detectSchema(fm) {
|
|
|
531
576
|
return { schema: null };
|
|
532
577
|
}
|
|
533
578
|
|
|
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 ipv4InCidr(ip, base, prefix) {
|
|
559
|
-
return inRange(ip, base, prefix);
|
|
560
|
-
}
|
|
561
|
-
function isBlockedHost(host) {
|
|
562
|
-
const lower = host.toLowerCase();
|
|
563
|
-
if (METADATA_HOSTS.has(lower)) return true;
|
|
564
|
-
if (METADATA_IPS.has(host)) return true;
|
|
565
|
-
if (lower === "::1") return true;
|
|
566
|
-
if (lower.startsWith("fe80:")) return true;
|
|
567
|
-
if (ipv4ToInt(host) === null) return false;
|
|
568
|
-
if (inRange(host, "10.0.0.0", 8)) return true;
|
|
569
|
-
if (inRange(host, "172.16.0.0", 12)) return true;
|
|
570
|
-
if (inRange(host, "192.168.0.0", 16)) return true;
|
|
571
|
-
if (inRange(host, "169.254.0.0", 16)) return true;
|
|
572
|
-
if (inRange(host, "127.0.0.0", 8)) return true;
|
|
573
|
-
return false;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
579
|
// ../shared/src/error-message.ts
|
|
577
580
|
function getErrorMessage(e) {
|
|
578
581
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
atomicWriteText
|
|
4
|
+
} from "./chunk-6VUK2FTX.js";
|
|
5
|
+
import {
|
|
6
|
+
PRODUCTION_MIRROR_LABEL,
|
|
3
7
|
PRODUCTION_WATCHER_LABEL,
|
|
4
8
|
evidencePath,
|
|
5
9
|
expandUserPath,
|
|
@@ -7,25 +11,22 @@ import {
|
|
|
7
11
|
nestedOrEqual,
|
|
8
12
|
resolveClipOutboxConfig,
|
|
9
13
|
validateRuntime
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-OWWWO2XS.js";
|
|
11
15
|
import {
|
|
12
16
|
parseDotenvFile
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import {
|
|
15
|
-
atomicWriteText
|
|
16
|
-
} from "./chunk-GQWCTJTU.js";
|
|
17
|
+
} from "./chunk-IJKJGQKL.js";
|
|
17
18
|
|
|
18
19
|
// src/clip-outbox/migrate.ts
|
|
19
20
|
import { access, mkdir, readFile } from "fs/promises";
|
|
20
21
|
import { constants } from "fs";
|
|
21
22
|
import { basename, dirname, isAbsolute, join } from "path";
|
|
22
23
|
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
|
|
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.";
|
|
24
25
|
var xml = (s) => s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
25
|
-
async function packagedPlist() {
|
|
26
|
+
async function packagedPlist(label = PRODUCTION_WATCHER_LABEL) {
|
|
26
27
|
for (const rel of ["../templates/launchd/", "../../templates/launchd/"]) {
|
|
27
28
|
try {
|
|
28
|
-
return await readFile(new URL(`${rel}${
|
|
29
|
+
return await readFile(new URL(`${rel}${label}.plist`, import.meta.url), "utf8");
|
|
29
30
|
} catch {
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -48,6 +49,7 @@ async function migrateClipOutbox(input) {
|
|
|
48
49
|
const resolved = resolveClipOutboxConfig(map, input.home);
|
|
49
50
|
if (!resolved.ok) throw new Error(resolved.message);
|
|
50
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");
|
|
51
53
|
if ((input.platform ?? process.platform) !== "darwin") throw new Error("clip launchd migration requires macOS");
|
|
52
54
|
const rawRoot = map["clip_outbox.root"] ?? "~/Obsidian/clip-outbox";
|
|
53
55
|
const root = resolved.enabled ? resolved.config.root : expandUserPath(rawRoot, input.home);
|
|
@@ -58,9 +60,16 @@ async function migrateClipOutbox(input) {
|
|
|
58
60
|
if (!isAbsolute(binary) || !isAbsolute(input.nodePath ?? process.execPath)) throw new Error("absolute executable paths required");
|
|
59
61
|
const logRoot = join(input.home, "Library", "Logs", "SkillWiki");
|
|
60
62
|
const plistPath = join(input.home, "Library", "LaunchAgents", `${PRODUCTION_WATCHER_LABEL}.plist`);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
64
73
|
if (input.load) {
|
|
65
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");
|
|
66
75
|
const creds = await loadClipCredentials(input.home, resolved.config);
|
|
@@ -71,22 +80,37 @@ async function migrateClipOutbox(input) {
|
|
|
71
80
|
}
|
|
72
81
|
await mkdir(dirname(plistPath), { recursive: true });
|
|
73
82
|
await mkdir(logRoot, { recursive: true });
|
|
74
|
-
const result = await atomicWriteText(plistPath,
|
|
83
|
+
const result = await atomicWriteText(plistPath, watcherPlist);
|
|
75
84
|
if (!result.ok) throw new Error("cannot refresh clip plist");
|
|
76
|
-
|
|
85
|
+
const mirrorResult = await atomicWriteText(mirrorPlistPath, mirrorPlist);
|
|
86
|
+
if (!mirrorResult.ok) throw new Error("cannot refresh inbound SSE plist");
|
|
87
|
+
const bootstrap = (label, path) => {
|
|
77
88
|
if (typeof process.getuid !== "function") throw new Error("clip launchd load requires macOS");
|
|
78
89
|
const domain = `gui/${process.getuid()}`;
|
|
79
90
|
const launch = input.launch ?? ((args) => {
|
|
80
91
|
execFileSync("launchctl", args, { stdio: "ignore", timeout: 5e3 });
|
|
81
92
|
});
|
|
82
93
|
try {
|
|
83
|
-
launch(["bootout", `${domain}/${
|
|
94
|
+
launch(["bootout", `${domain}/${label}`]);
|
|
84
95
|
} catch {
|
|
85
96
|
}
|
|
86
|
-
launch(["enable", `${domain}/${
|
|
87
|
-
launch(["bootstrap", domain,
|
|
88
|
-
}
|
|
89
|
-
|
|
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
|
+
};
|
|
90
114
|
}
|
|
91
115
|
async function refreshInstalledClipPlist(home, opts) {
|
|
92
116
|
const plistPath = join(home, "Library", "LaunchAgents", `${PRODUCTION_WATCHER_LABEL}.plist`);
|