@agimon-ai/po-help 0.0.0-alpha.0
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/apiContracts.cjs +17 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +13 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/server.cjs +323 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +318 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +88 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +763 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +133 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +732 -0
- package/dist/index.mjs.map +1 -0
- package/llms.txt +15 -0
- package/package.json +117 -0
- package/src/prompts/po-develop-native-extension/SKILL.md +63 -0
- package/src/prompts/po-use-chatgpt-instructions/SKILL.md +66 -0
- package/src/prompts/po-use-help/SKILL.md +40 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import { DOOM_HELP_MAX_DIAGNOSTICS, DOOM_HELP_MAX_DIAGNOSTIC_LENGTH, DOOM_HELP_MAX_SKILLS } from "@agimon-ai/po-core/help";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { poHomeDirectory } from "@agimon-ai/po-core/piSettings";
|
|
8
|
+
//#region src/services/llmsContent/index.ts
|
|
9
|
+
const MAX_LLMS_BYTES = 1048576;
|
|
10
|
+
const MARKDOWN_LINK_PATTERN = /!?\[[^\]]*\]\(\s*<?([^\s)>]+)>?(?:\s+[^)]*)?\)/gu;
|
|
11
|
+
const MARKDOWN_REFERENCE_LINK_PATTERN = /^\s{0,3}\[[^\]\r\n]+\]:\s*<?([^\s>]+)>?/gmu;
|
|
12
|
+
const MARKDOWN_AUTOLINK_PATTERN = /<((?:\.{0,2}\/|[A-Za-z][A-Za-z0-9+.-]*:)[^<>\s]+)>/gu;
|
|
13
|
+
const HTML_LINK_PATTERN = /\b(?:href|src)\s*=\s*["']([^"']+)["']/giu;
|
|
14
|
+
const URI_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
|
|
15
|
+
const NESTED_PATH_ENCODING_PATTERN = /%(?:25|2e|2f|3a|5c)/iu;
|
|
16
|
+
const SAFE_EXTERNAL_SCHEMES = /* @__PURE__ */ new Set([
|
|
17
|
+
"http:",
|
|
18
|
+
"https:",
|
|
19
|
+
"mailto:"
|
|
20
|
+
]);
|
|
21
|
+
function externalScheme(target) {
|
|
22
|
+
if (!URI_SCHEME_PATTERN.test(target)) return void 0;
|
|
23
|
+
return target.slice(0, target.indexOf(":") + 1).toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function decodeRelativeTarget(rawTarget) {
|
|
26
|
+
let decoded = rawTarget;
|
|
27
|
+
try {
|
|
28
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
29
|
+
const next = decodeURIComponent(decoded);
|
|
30
|
+
if (next === decoded) break;
|
|
31
|
+
decoded = next;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`llms.txt contains an invalid encoded link '${rawTarget}'.`);
|
|
35
|
+
}
|
|
36
|
+
if (NESTED_PATH_ENCODING_PATTERN.test(decoded)) throw new Error(`llms.txt contains an unsafe nested encoding in link '${rawTarget}'.`);
|
|
37
|
+
return decoded;
|
|
38
|
+
}
|
|
39
|
+
function validateRelativeLink(rawTarget) {
|
|
40
|
+
if (!rawTarget || rawTarget.startsWith("#")) return;
|
|
41
|
+
const rawScheme = externalScheme(rawTarget);
|
|
42
|
+
if (rawScheme) {
|
|
43
|
+
if (!SAFE_EXTERNAL_SCHEMES.has(rawScheme)) throw new Error(`llms.txt contains unsupported link scheme '${rawScheme}'.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const decoded = decodeRelativeTarget(rawTarget);
|
|
47
|
+
const decodedScheme = externalScheme(decoded);
|
|
48
|
+
if (decodedScheme) {
|
|
49
|
+
if (!SAFE_EXTERNAL_SCHEMES.has(decodedScheme)) throw new Error(`llms.txt contains unsupported link scheme '${decodedScheme}'.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const withoutSuffix = decoded.split(/[?#]/u, 1)[0]?.replaceAll("\\", "/") ?? "";
|
|
53
|
+
if (withoutSuffix.startsWith("/") || withoutSuffix.split("/").some((segment) => segment === "..")) throw new Error(`llms.txt link '${rawTarget}' escapes the package root.`);
|
|
54
|
+
}
|
|
55
|
+
function containsUnsafeControl(text) {
|
|
56
|
+
for (const character of text) {
|
|
57
|
+
const code = character.codePointAt(0) ?? 0;
|
|
58
|
+
if (code >= 0 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function validateLlmsBytes(bytes) {
|
|
63
|
+
if (bytes.byteLength === 0) throw new Error("llms.txt is empty.");
|
|
64
|
+
if (bytes.byteLength > 1048576) throw new Error(`llms.txt exceeds ${MAX_LLMS_BYTES} bytes.`);
|
|
65
|
+
let text;
|
|
66
|
+
try {
|
|
67
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("llms.txt is not valid UTF-8.");
|
|
70
|
+
}
|
|
71
|
+
if (text.startsWith("")) text = text.slice(1);
|
|
72
|
+
if (containsUnsafeControl(text)) throw new Error("llms.txt contains unsafe control bytes.");
|
|
73
|
+
const firstContentLine = text.split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim();
|
|
74
|
+
if (!firstContentLine || !/^#\s+\S/u.test(firstContentLine)) throw new Error("llms.txt must begin with a Markdown H1 heading.");
|
|
75
|
+
for (const pattern of [
|
|
76
|
+
MARKDOWN_LINK_PATTERN,
|
|
77
|
+
MARKDOWN_REFERENCE_LINK_PATTERN,
|
|
78
|
+
MARKDOWN_AUTOLINK_PATTERN,
|
|
79
|
+
HTML_LINK_PATTERN
|
|
80
|
+
]) for (const match of text.matchAll(pattern)) validateRelativeLink(match[1] ?? "");
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
function renderHelpSkillWrapper(descriptor, index) {
|
|
84
|
+
return `---\nname: ${JSON.stringify(descriptor.name)}\ndescription: ${JSON.stringify(descriptor.description)}\n---\n\n# Package Help\n\nRead the validated package Help index at ${JSON.stringify(index.filePath)}.\nResolve relative links in that index against ${JSON.stringify(index.referenceBase)}.\nOpen only the linked documents relevant to the current request, as needed; do not recursively load the whole reference set.\n`;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/services/helpActivation/index.ts
|
|
88
|
+
const MAX_RESOLUTION_CONCURRENCY = 4;
|
|
89
|
+
const FAILURE_MESSAGE = "No package Help source could be activated.";
|
|
90
|
+
const CANCELLATION_MESSAGE = "Help activation was cancelled.";
|
|
91
|
+
var HelpActivationError = class extends Error {
|
|
92
|
+
constructor(message = FAILURE_MESSAGE) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "HelpActivationError";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
function cloneContribution(contribution) {
|
|
98
|
+
return {
|
|
99
|
+
source: contribution.source,
|
|
100
|
+
moduleUrl: contribution.moduleUrl,
|
|
101
|
+
skills: contribution.skills.map((skill) => ({ ...skill }))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function cloneState(state) {
|
|
105
|
+
return {
|
|
106
|
+
activation: state.activation,
|
|
107
|
+
skills: state.skills.map((skill) => ({ ...skill })),
|
|
108
|
+
diagnostics: state.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function diagnostic(source, code, error) {
|
|
112
|
+
return {
|
|
113
|
+
source,
|
|
114
|
+
code,
|
|
115
|
+
message: ((error instanceof Error ? error.message : String(error)) || "Unknown Help activation failure.").slice(0, DOOM_HELP_MAX_DIAGNOSTIC_LENGTH)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function contributionFingerprint(contributions) {
|
|
119
|
+
return JSON.stringify(contributions.map((contribution) => ({
|
|
120
|
+
source: contribution.source,
|
|
121
|
+
moduleUrl: contribution.moduleUrl,
|
|
122
|
+
skills: contribution.skills
|
|
123
|
+
})));
|
|
124
|
+
}
|
|
125
|
+
var DefaultHelpActivationService = class {
|
|
126
|
+
dependencies;
|
|
127
|
+
listeners = /* @__PURE__ */ new Set();
|
|
128
|
+
contributions = [];
|
|
129
|
+
fingerprint = "[]";
|
|
130
|
+
state = {
|
|
131
|
+
activation: "inactive",
|
|
132
|
+
skills: [],
|
|
133
|
+
diagnostics: []
|
|
134
|
+
};
|
|
135
|
+
settledState = {
|
|
136
|
+
activation: "inactive",
|
|
137
|
+
skills: [],
|
|
138
|
+
diagnostics: []
|
|
139
|
+
};
|
|
140
|
+
generation = 0;
|
|
141
|
+
controller;
|
|
142
|
+
disposed = false;
|
|
143
|
+
constructor(dependencies) {
|
|
144
|
+
this.dependencies = dependencies;
|
|
145
|
+
}
|
|
146
|
+
getState() {
|
|
147
|
+
return cloneState(this.state);
|
|
148
|
+
}
|
|
149
|
+
replaceContributions(contributions) {
|
|
150
|
+
if (this.disposed) return;
|
|
151
|
+
const next = contributions.map(cloneContribution).sort((left, right) => left.source.localeCompare(right.source));
|
|
152
|
+
const fingerprint = contributionFingerprint(next);
|
|
153
|
+
if (fingerprint === this.fingerprint) return;
|
|
154
|
+
const previous = new Map(this.contributions.map((entry) => [entry.source, contributionFingerprint([entry])]));
|
|
155
|
+
const retained = new Set(next.filter((entry) => previous.get(entry.source) === contributionFingerprint([entry])).map((entry) => entry.source));
|
|
156
|
+
const skills = this.settledState.skills.filter((entry) => retained.has(entry.source));
|
|
157
|
+
const diagnostics = this.settledState.diagnostics.filter((entry) => !entry.source || retained.has(entry.source));
|
|
158
|
+
this.settledState = {
|
|
159
|
+
activation: skills.length === 0 ? "inactive" : diagnostics.length > 0 ? "degraded" : "active",
|
|
160
|
+
skills,
|
|
161
|
+
diagnostics
|
|
162
|
+
};
|
|
163
|
+
this.contributions = next;
|
|
164
|
+
this.fingerprint = fingerprint;
|
|
165
|
+
if (this.state.activation !== "inactive") this.activate().catch((error) => {
|
|
166
|
+
if (error instanceof HelpActivationError) return;
|
|
167
|
+
this.dependencies.onBackgroundError(error);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
subscribe(listener) {
|
|
171
|
+
this.listeners.add(listener);
|
|
172
|
+
return () => this.listeners.delete(listener);
|
|
173
|
+
}
|
|
174
|
+
publish(state) {
|
|
175
|
+
this.state = cloneState(state);
|
|
176
|
+
if (state.activation !== "activating") this.settledState = cloneState(state);
|
|
177
|
+
this.dependencies.publisher.publish(this.state);
|
|
178
|
+
for (const listener of this.listeners) listener(cloneState(this.state));
|
|
179
|
+
return this.getState();
|
|
180
|
+
}
|
|
181
|
+
async loadSources(signal) {
|
|
182
|
+
const contributions = this.contributions;
|
|
183
|
+
const outcomes = Array.from({ length: contributions.length });
|
|
184
|
+
let cursor = 0;
|
|
185
|
+
const worker = async () => {
|
|
186
|
+
while (cursor < contributions.length) {
|
|
187
|
+
const index = cursor;
|
|
188
|
+
cursor += 1;
|
|
189
|
+
const contribution = contributions[index];
|
|
190
|
+
if (!contribution) return;
|
|
191
|
+
if (signal.aborted) throw new HelpActivationError(CANCELLATION_MESSAGE);
|
|
192
|
+
try {
|
|
193
|
+
const resolved = await this.dependencies.resolver.resolve(contribution, signal);
|
|
194
|
+
if (signal.aborted) throw new HelpActivationError(CANCELLATION_MESSAGE);
|
|
195
|
+
const skills = await this.dependencies.materializer.materialize(contribution, resolved, signal);
|
|
196
|
+
outcomes[index] = {
|
|
197
|
+
source: contribution.source,
|
|
198
|
+
loaded: {
|
|
199
|
+
contribution,
|
|
200
|
+
index: resolved,
|
|
201
|
+
skills
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (signal.aborted) throw new HelpActivationError(CANCELLATION_MESSAGE);
|
|
206
|
+
outcomes[index] = {
|
|
207
|
+
source: contribution.source,
|
|
208
|
+
error
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const count = Math.min(MAX_RESOLUTION_CONCURRENCY, contributions.length);
|
|
214
|
+
await Promise.all(Array.from({ length: count }, () => worker()));
|
|
215
|
+
return outcomes;
|
|
216
|
+
}
|
|
217
|
+
async activate(externalSignal) {
|
|
218
|
+
if (this.disposed) throw new HelpActivationError("Help runtime is disposed.");
|
|
219
|
+
this.controller?.abort("Help activation generation replaced.");
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
this.controller = controller;
|
|
222
|
+
const generation = ++this.generation;
|
|
223
|
+
const abortFromExternal = () => controller.abort(externalSignal?.reason);
|
|
224
|
+
externalSignal?.addEventListener("abort", abortFromExternal, { once: true });
|
|
225
|
+
if (externalSignal?.aborted) abortFromExternal();
|
|
226
|
+
const settledState = cloneState(this.settledState);
|
|
227
|
+
this.publish({
|
|
228
|
+
activation: "activating",
|
|
229
|
+
skills: settledState.skills,
|
|
230
|
+
diagnostics: settledState.diagnostics
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
const outcomes = await this.loadSources(controller.signal);
|
|
234
|
+
if (controller.signal.aborted || generation !== this.generation) throw new HelpActivationError(CANCELLATION_MESSAGE);
|
|
235
|
+
const diagnostics = [];
|
|
236
|
+
const loaded = [];
|
|
237
|
+
let aggregateBytes = 0;
|
|
238
|
+
for (const outcome of outcomes) {
|
|
239
|
+
if (!outcome) continue;
|
|
240
|
+
if (outcome.error || !outcome.loaded) {
|
|
241
|
+
diagnostics.push(diagnostic(outcome.source, "HELP_SOURCE_FAILED", outcome.error));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (aggregateBytes + outcome.loaded.index.byteLength > 4194304) {
|
|
245
|
+
diagnostics.push(diagnostic(outcome.source, "HELP_AGGREGATE_LIMIT", "The aggregate llms.txt byte limit was exceeded."));
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
aggregateBytes += outcome.loaded.index.byteLength;
|
|
249
|
+
loaded.push(outcome.loaded);
|
|
250
|
+
}
|
|
251
|
+
const skills = [];
|
|
252
|
+
const names = /* @__PURE__ */ new Set();
|
|
253
|
+
for (const source of loaded) for (const skill of source.skills) {
|
|
254
|
+
if (skills.length >= DOOM_HELP_MAX_SKILLS) {
|
|
255
|
+
diagnostics.push(diagnostic(source.contribution.source, "HELP_SKILL_LIMIT", "The Help skill limit was reached."));
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
if (names.has(skill.name)) {
|
|
259
|
+
diagnostics.push(diagnostic(source.contribution.source, "HELP_SKILL_COLLISION", `Help skill '${skill.name}' was already provided by an earlier package source.`));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
names.add(skill.name);
|
|
263
|
+
skills.push({ ...skill });
|
|
264
|
+
}
|
|
265
|
+
const boundedDiagnostics = diagnostics.slice(0, DOOM_HELP_MAX_DIAGNOSTICS);
|
|
266
|
+
if (skills.length === 0) {
|
|
267
|
+
this.publish({
|
|
268
|
+
activation: "inactive",
|
|
269
|
+
skills: [],
|
|
270
|
+
diagnostics: boundedDiagnostics
|
|
271
|
+
});
|
|
272
|
+
throw new HelpActivationError();
|
|
273
|
+
}
|
|
274
|
+
return this.publish({
|
|
275
|
+
activation: boundedDiagnostics.length > 0 ? "degraded" : "active",
|
|
276
|
+
skills,
|
|
277
|
+
diagnostics: boundedDiagnostics
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (generation === this.generation && this.state.activation === "activating") this.publish(settledState);
|
|
281
|
+
throw error;
|
|
282
|
+
} finally {
|
|
283
|
+
externalSignal?.removeEventListener("abort", abortFromExternal);
|
|
284
|
+
if (this.controller === controller) this.controller = void 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
deactivate() {
|
|
288
|
+
this.generation += 1;
|
|
289
|
+
this.controller?.abort("Help deactivated.");
|
|
290
|
+
this.controller = void 0;
|
|
291
|
+
return this.publish({
|
|
292
|
+
activation: "inactive",
|
|
293
|
+
skills: [],
|
|
294
|
+
diagnostics: []
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
dispose() {
|
|
298
|
+
if (this.disposed) return;
|
|
299
|
+
this.deactivate();
|
|
300
|
+
this.disposed = true;
|
|
301
|
+
this.listeners.clear();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/services/helpStorage/index.ts
|
|
306
|
+
const PACKAGE_MANIFEST = "package.json";
|
|
307
|
+
const HELP_INDEX_PATH$1 = "llms.txt";
|
|
308
|
+
const CACHE_SCHEMA_VERSION = 1;
|
|
309
|
+
const PRIVATE_FILE_MODE = 384;
|
|
310
|
+
const PRIVATE_DIRECTORY_MODE = 448;
|
|
311
|
+
const COMPLETION_FILE = ".complete";
|
|
312
|
+
const CACHE_METADATA_FILE = "metadata.json";
|
|
313
|
+
const WRAPPER_DIRECTORY = "wrappers";
|
|
314
|
+
const SKILL_FILE = "SKILL.md";
|
|
315
|
+
const EXACT_SEMVER_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
316
|
+
const NPM_PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u;
|
|
317
|
+
function assertNotAborted(signal) {
|
|
318
|
+
if (signal.aborted) throw new Error("Help activation was cancelled.");
|
|
319
|
+
}
|
|
320
|
+
function isRecord$1(value) {
|
|
321
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
322
|
+
}
|
|
323
|
+
function isContained(root, candidate) {
|
|
324
|
+
const relative = path.relative(root, candidate);
|
|
325
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
326
|
+
}
|
|
327
|
+
function readManifest(filePath) {
|
|
328
|
+
let parsed;
|
|
329
|
+
try {
|
|
330
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw new Error(`Cannot read Help package manifest at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
333
|
+
}
|
|
334
|
+
if (!isRecord$1(parsed)) throw new Error(`Help package manifest at ${filePath} must contain an object.`);
|
|
335
|
+
return parsed;
|
|
336
|
+
}
|
|
337
|
+
function resolveHelpPackageIdentity(moduleUrl, expectedSource) {
|
|
338
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(expectedSource)) throw new Error(`Help source '${expectedSource}' must be a valid lowercase npm package name.`);
|
|
339
|
+
let modulePath;
|
|
340
|
+
try {
|
|
341
|
+
const url = new URL(moduleUrl);
|
|
342
|
+
if (url.protocol !== "file:") throw new Error(`Help module URL must use file:, received ${url.protocol}`);
|
|
343
|
+
modulePath = fs.realpathSync(fileURLToPath(url));
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw new Error(`Invalid Help contributor module URL '${moduleUrl}': ${error instanceof Error ? error.message : String(error)}`);
|
|
346
|
+
}
|
|
347
|
+
if (!fs.lstatSync(modulePath).isFile()) throw new Error(`Help contributor module is not a regular file: ${modulePath}`);
|
|
348
|
+
let directory = path.dirname(modulePath);
|
|
349
|
+
while (true) {
|
|
350
|
+
const manifestPath = path.join(directory, PACKAGE_MANIFEST);
|
|
351
|
+
if (fs.existsSync(manifestPath)) {
|
|
352
|
+
const manifest = readManifest(manifestPath);
|
|
353
|
+
const source = manifest.name;
|
|
354
|
+
const version = manifest.version;
|
|
355
|
+
if (source !== expectedSource) throw new Error(`Help source '${expectedSource}' does not match nearest package '${String(source)}'.`);
|
|
356
|
+
if (typeof version !== "string" || !EXACT_SEMVER_PATTERN.test(version)) throw new Error(`Help package '${expectedSource}' must declare an exact semantic version.`);
|
|
357
|
+
const packageRoot = fs.realpathSync(directory);
|
|
358
|
+
if (!isContained(packageRoot, modulePath)) throw new Error("Help contributor module escapes its resolved package root.");
|
|
359
|
+
return {
|
|
360
|
+
source: expectedSource,
|
|
361
|
+
version,
|
|
362
|
+
packageRoot,
|
|
363
|
+
modulePath
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const parent = path.dirname(directory);
|
|
367
|
+
if (parent === directory) break;
|
|
368
|
+
directory = parent;
|
|
369
|
+
}
|
|
370
|
+
throw new Error(`Cannot find a package manifest for Help contributor ${modulePath}.`);
|
|
371
|
+
}
|
|
372
|
+
function defaultHelpCacheRoot(homeDirectory = os.homedir()) {
|
|
373
|
+
return path.join(poHomeDirectory(homeDirectory), "llms-cache");
|
|
374
|
+
}
|
|
375
|
+
function sha256Hex(bytes) {
|
|
376
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
377
|
+
}
|
|
378
|
+
function parseCacheMetadata(value, identity) {
|
|
379
|
+
if (!isRecord$1(value)) return void 0;
|
|
380
|
+
if (value.schemaVersion !== CACHE_SCHEMA_VERSION || value.source !== identity.source || value.version !== identity.version || value.path !== HELP_INDEX_PATH$1 || typeof value.integrity !== "string" || typeof value.digest !== "string" || typeof value.referenceBase !== "string") return;
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
function regularFile(filePath) {
|
|
384
|
+
try {
|
|
385
|
+
const metadata = fs.lstatSync(filePath);
|
|
386
|
+
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
387
|
+
} catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
var HelpIndexCache = class {
|
|
392
|
+
root;
|
|
393
|
+
constructor(root = defaultHelpCacheRoot()) {
|
|
394
|
+
this.root = root;
|
|
395
|
+
}
|
|
396
|
+
entryDirectory(identity) {
|
|
397
|
+
const key = sha256Hex(`${identity.source}\0${identity.version}\0${HELP_INDEX_PATH$1}`);
|
|
398
|
+
return path.join(this.root, key);
|
|
399
|
+
}
|
|
400
|
+
discard(identity) {
|
|
401
|
+
fs.rmSync(this.entryDirectory(identity), {
|
|
402
|
+
recursive: true,
|
|
403
|
+
force: true
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
read(identity) {
|
|
407
|
+
const directory = this.entryDirectory(identity);
|
|
408
|
+
const completionPath = path.join(directory, COMPLETION_FILE);
|
|
409
|
+
const indexPath = path.join(directory, HELP_INDEX_PATH$1);
|
|
410
|
+
const metadataPath = path.join(directory, CACHE_METADATA_FILE);
|
|
411
|
+
if (!regularFile(completionPath) || !regularFile(indexPath) || !regularFile(metadataPath)) return void 0;
|
|
412
|
+
try {
|
|
413
|
+
const metadata = parseCacheMetadata(JSON.parse(fs.readFileSync(metadataPath, "utf8")), identity);
|
|
414
|
+
if (!metadata) return void 0;
|
|
415
|
+
const bytes = fs.readFileSync(indexPath);
|
|
416
|
+
if (sha256Hex(bytes) !== metadata.digest) return void 0;
|
|
417
|
+
if (fs.readFileSync(completionPath, "utf8").trim() !== metadata.digest) return void 0;
|
|
418
|
+
return {
|
|
419
|
+
bytes,
|
|
420
|
+
filePath: indexPath,
|
|
421
|
+
integrity: metadata.integrity,
|
|
422
|
+
digest: metadata.digest,
|
|
423
|
+
referenceBase: metadata.referenceBase
|
|
424
|
+
};
|
|
425
|
+
} catch {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
publish(identity, bytes, integrity, referenceBase, signal) {
|
|
430
|
+
assertNotAborted(signal);
|
|
431
|
+
const existing = this.read(identity);
|
|
432
|
+
if (existing) return existing;
|
|
433
|
+
const directory = this.entryDirectory(identity);
|
|
434
|
+
fs.mkdirSync(this.root, {
|
|
435
|
+
mode: PRIVATE_DIRECTORY_MODE,
|
|
436
|
+
recursive: true
|
|
437
|
+
});
|
|
438
|
+
if (fs.existsSync(directory)) fs.rmSync(directory, {
|
|
439
|
+
recursive: true,
|
|
440
|
+
force: true
|
|
441
|
+
});
|
|
442
|
+
const temporary = `${directory}.${process.pid}.${randomUUID()}.tmp`;
|
|
443
|
+
const digest = sha256Hex(bytes);
|
|
444
|
+
const metadata = {
|
|
445
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
446
|
+
source: identity.source,
|
|
447
|
+
version: identity.version,
|
|
448
|
+
path: HELP_INDEX_PATH$1,
|
|
449
|
+
integrity,
|
|
450
|
+
digest,
|
|
451
|
+
referenceBase
|
|
452
|
+
};
|
|
453
|
+
try {
|
|
454
|
+
fs.mkdirSync(temporary, { mode: PRIVATE_DIRECTORY_MODE });
|
|
455
|
+
fs.writeFileSync(path.join(temporary, HELP_INDEX_PATH$1), bytes, { mode: PRIVATE_FILE_MODE });
|
|
456
|
+
fs.writeFileSync(path.join(temporary, CACHE_METADATA_FILE), `${JSON.stringify(metadata, null, 2)}\n`, { mode: PRIVATE_FILE_MODE });
|
|
457
|
+
assertNotAborted(signal);
|
|
458
|
+
fs.writeFileSync(path.join(temporary, COMPLETION_FILE), `${digest}\n`, { mode: PRIVATE_FILE_MODE });
|
|
459
|
+
try {
|
|
460
|
+
fs.renameSync(temporary, directory);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
const winner = this.read(identity);
|
|
463
|
+
if (winner) return winner;
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
bytes: Uint8Array.from(bytes),
|
|
468
|
+
filePath: path.join(directory, HELP_INDEX_PATH$1),
|
|
469
|
+
integrity,
|
|
470
|
+
digest,
|
|
471
|
+
referenceBase
|
|
472
|
+
};
|
|
473
|
+
} finally {
|
|
474
|
+
fs.rmSync(temporary, {
|
|
475
|
+
recursive: true,
|
|
476
|
+
force: true
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var DefaultHelpSkillMaterializer = class {
|
|
482
|
+
cacheRoot;
|
|
483
|
+
constructor(cacheRoot = defaultHelpCacheRoot()) {
|
|
484
|
+
this.cacheRoot = cacheRoot;
|
|
485
|
+
}
|
|
486
|
+
async materialize(contribution, index, signal) {
|
|
487
|
+
const skills = [];
|
|
488
|
+
for (const descriptor of contribution.skills) {
|
|
489
|
+
assertNotAborted(signal);
|
|
490
|
+
const key = sha256Hex(`${index.identity.source}\0${index.identity.version}\0${descriptor.name}\0${descriptor.description}\0${index.digest}`);
|
|
491
|
+
const directory = path.join(this.cacheRoot, WRAPPER_DIRECTORY, key);
|
|
492
|
+
const filePath = path.join(directory, SKILL_FILE);
|
|
493
|
+
const content = renderHelpSkillWrapper(descriptor, index);
|
|
494
|
+
if (!regularFile(filePath) || fs.readFileSync(filePath, "utf8") !== content) {
|
|
495
|
+
fs.mkdirSync(path.dirname(directory), {
|
|
496
|
+
mode: PRIVATE_DIRECTORY_MODE,
|
|
497
|
+
recursive: true
|
|
498
|
+
});
|
|
499
|
+
if (fs.existsSync(directory)) fs.rmSync(directory, {
|
|
500
|
+
recursive: true,
|
|
501
|
+
force: true
|
|
502
|
+
});
|
|
503
|
+
const temporary = `${directory}.${process.pid}.${randomUUID()}.tmp`;
|
|
504
|
+
try {
|
|
505
|
+
fs.mkdirSync(temporary, { mode: PRIVATE_DIRECTORY_MODE });
|
|
506
|
+
fs.writeFileSync(path.join(temporary, SKILL_FILE), content, { mode: PRIVATE_FILE_MODE });
|
|
507
|
+
assertNotAborted(signal);
|
|
508
|
+
try {
|
|
509
|
+
fs.renameSync(temporary, directory);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (!regularFile(filePath) || fs.readFileSync(filePath, "utf8") !== content) throw error;
|
|
512
|
+
}
|
|
513
|
+
} finally {
|
|
514
|
+
fs.rmSync(temporary, {
|
|
515
|
+
recursive: true,
|
|
516
|
+
force: true
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
skills.push({
|
|
521
|
+
source: contribution.source,
|
|
522
|
+
name: descriptor.name,
|
|
523
|
+
description: descriptor.description,
|
|
524
|
+
filePath,
|
|
525
|
+
baseDir: directory
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return skills;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/services/llmsResolver/index.ts
|
|
533
|
+
const HELP_INDEX_PATH = "llms.txt";
|
|
534
|
+
const UNPKG_ORIGIN = "https://unpkg.com";
|
|
535
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 5e3;
|
|
536
|
+
const MAX_METADATA_BYTES = 524288;
|
|
537
|
+
const MAX_REDIRECTS = 3;
|
|
538
|
+
const SRI_PATTERN = /^sha256-([A-Za-z0-9+/]+={0,2})$/u;
|
|
539
|
+
function defaultFetch(input, init) {
|
|
540
|
+
return fetch(input, init);
|
|
541
|
+
}
|
|
542
|
+
function isRecord(value) {
|
|
543
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
544
|
+
}
|
|
545
|
+
function isRegularFile(filePath) {
|
|
546
|
+
try {
|
|
547
|
+
const metadata = fs.lstatSync(filePath);
|
|
548
|
+
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
549
|
+
} catch {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function unpkgPackagePath(source, version) {
|
|
554
|
+
return `${source.split("/").map((segment) => segment.startsWith("@") ? `@${encodeURIComponent(segment.slice(1))}` : encodeURIComponent(segment)).join("/")}@${encodeURIComponent(version)}`;
|
|
555
|
+
}
|
|
556
|
+
function exactUnpkgBase(source, version) {
|
|
557
|
+
return `${UNPKG_ORIGIN}/${unpkgPackagePath(source, version)}/`;
|
|
558
|
+
}
|
|
559
|
+
function contentLength(response) {
|
|
560
|
+
const raw = response.headers.get("content-length");
|
|
561
|
+
if (!raw) return void 0;
|
|
562
|
+
const parsed = Number.parseInt(raw, 10);
|
|
563
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
564
|
+
}
|
|
565
|
+
function timeoutSignal(parent, timeoutMs) {
|
|
566
|
+
const controller = new AbortController();
|
|
567
|
+
const abort = () => controller.abort(parent.reason);
|
|
568
|
+
parent.addEventListener("abort", abort, { once: true });
|
|
569
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Help download timed out.")), timeoutMs);
|
|
570
|
+
if (parent.aborted) abort();
|
|
571
|
+
return {
|
|
572
|
+
signal: controller.signal,
|
|
573
|
+
dispose() {
|
|
574
|
+
clearTimeout(timer);
|
|
575
|
+
parent.removeEventListener("abort", abort);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async function fetchExact(fetcher, initialUrl, signal) {
|
|
580
|
+
let url = initialUrl;
|
|
581
|
+
const expected = new URL(initialUrl);
|
|
582
|
+
for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect += 1) {
|
|
583
|
+
const response = await fetcher(url, {
|
|
584
|
+
signal,
|
|
585
|
+
redirect: "manual"
|
|
586
|
+
});
|
|
587
|
+
if (response.status < 300 || response.status >= 400) return response;
|
|
588
|
+
const location = response.headers.get("location");
|
|
589
|
+
if (!location) throw new Error(`Unpkg redirected ${url} without a location.`);
|
|
590
|
+
const next = new URL(location, url);
|
|
591
|
+
if (next.origin !== UNPKG_ORIGIN || next.pathname !== expected.pathname || next.search !== expected.search) throw new Error(`Unpkg redirect escaped the exact package/version path: ${next.href}`);
|
|
592
|
+
url = next.href;
|
|
593
|
+
}
|
|
594
|
+
throw new Error(`Unpkg exceeded ${MAX_REDIRECTS} redirects.`);
|
|
595
|
+
}
|
|
596
|
+
function parseMetadata(text) {
|
|
597
|
+
let parsed;
|
|
598
|
+
try {
|
|
599
|
+
parsed = JSON.parse(text);
|
|
600
|
+
} catch {
|
|
601
|
+
throw new Error("Unpkg metadata is not valid JSON.");
|
|
602
|
+
}
|
|
603
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.files)) throw new Error("Unpkg metadata has no files list.");
|
|
604
|
+
const match = parsed.files.find((entry) => isRecord(entry) && entry.path === `/${HELP_INDEX_PATH}`);
|
|
605
|
+
if (!isRecord(match) || typeof match.integrity !== "string") throw new Error(`Unpkg package does not publish /${HELP_INDEX_PATH}.`);
|
|
606
|
+
const size = typeof match.size === "number" ? match.size : void 0;
|
|
607
|
+
if (size !== void 0 && (!Number.isSafeInteger(size) || size < 0 || size > 1048576)) throw new Error(`Unpkg /${HELP_INDEX_PATH} exceeds the Help size limit.`);
|
|
608
|
+
if (!SRI_PATTERN.test(match.integrity)) throw new Error("Unpkg llms.txt metadata does not contain SHA-256 SRI.");
|
|
609
|
+
return {
|
|
610
|
+
path: match.path,
|
|
611
|
+
...size === void 0 ? {} : { size },
|
|
612
|
+
integrity: match.integrity
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function verifySri(bytes, integrity) {
|
|
616
|
+
const expected = SRI_PATTERN.exec(integrity)?.[1];
|
|
617
|
+
if (!expected) throw new Error("Invalid llms.txt SRI metadata.");
|
|
618
|
+
if (createHash("sha256").update(bytes).digest("base64") !== expected) throw new Error("Downloaded llms.txt failed SRI verification.");
|
|
619
|
+
}
|
|
620
|
+
var DefaultHelpIndexResolver = class {
|
|
621
|
+
cache;
|
|
622
|
+
fetcher;
|
|
623
|
+
timeoutMs;
|
|
624
|
+
constructor(options = {}) {
|
|
625
|
+
this.cache = options.cache ?? new HelpIndexCache();
|
|
626
|
+
this.fetcher = options.fetch ?? defaultFetch;
|
|
627
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
628
|
+
}
|
|
629
|
+
async resolve(contribution, signal) {
|
|
630
|
+
if (signal.aborted) throw new Error("Help activation was cancelled.");
|
|
631
|
+
const identity = resolveHelpPackageIdentity(contribution.moduleUrl, contribution.source);
|
|
632
|
+
const localPath = path.join(identity.packageRoot, HELP_INDEX_PATH);
|
|
633
|
+
if (fs.existsSync(localPath)) {
|
|
634
|
+
if (!isRegularFile(localPath)) throw new Error(`Bundled ${HELP_INDEX_PATH} must be a regular file.`);
|
|
635
|
+
const bytes = fs.readFileSync(localPath);
|
|
636
|
+
validateLlmsBytes(bytes);
|
|
637
|
+
return {
|
|
638
|
+
identity,
|
|
639
|
+
location: "local",
|
|
640
|
+
filePath: localPath,
|
|
641
|
+
referenceBase: identity.packageRoot,
|
|
642
|
+
byteLength: bytes.byteLength,
|
|
643
|
+
digest: sha256Hex(bytes)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const cached = this.cache.read(identity);
|
|
647
|
+
if (cached) try {
|
|
648
|
+
validateLlmsBytes(cached.bytes);
|
|
649
|
+
return {
|
|
650
|
+
identity,
|
|
651
|
+
location: "cache",
|
|
652
|
+
filePath: cached.filePath,
|
|
653
|
+
referenceBase: cached.referenceBase,
|
|
654
|
+
byteLength: cached.bytes.byteLength,
|
|
655
|
+
digest: cached.digest
|
|
656
|
+
};
|
|
657
|
+
} catch {
|
|
658
|
+
this.cache.discard(identity);
|
|
659
|
+
}
|
|
660
|
+
const timed = timeoutSignal(signal, this.timeoutMs);
|
|
661
|
+
try {
|
|
662
|
+
const referenceBase = exactUnpkgBase(identity.source, identity.version);
|
|
663
|
+
const metadataResponse = await fetchExact(this.fetcher, `${referenceBase}?meta`, timed.signal);
|
|
664
|
+
const metadataLength = contentLength(metadataResponse);
|
|
665
|
+
if (metadataLength !== void 0 && metadataLength > MAX_METADATA_BYTES) throw new Error("Unpkg metadata exceeds the Help metadata size limit.");
|
|
666
|
+
if (!metadataResponse.ok) throw new Error(`Unpkg metadata request failed with HTTP ${metadataResponse.status}.`);
|
|
667
|
+
const metadataText = await metadataResponse.text();
|
|
668
|
+
if (Buffer.byteLength(metadataText, "utf8") > MAX_METADATA_BYTES) throw new Error("Unpkg metadata exceeds the Help metadata size limit.");
|
|
669
|
+
const metadata = parseMetadata(metadataText);
|
|
670
|
+
const indexResponse = await fetchExact(this.fetcher, `${referenceBase}${HELP_INDEX_PATH}`, timed.signal);
|
|
671
|
+
const indexLength = contentLength(indexResponse);
|
|
672
|
+
if (indexLength !== void 0 && indexLength > 1048576) throw new Error(`Downloaded ${HELP_INDEX_PATH} exceeds ${MAX_LLMS_BYTES} bytes.`);
|
|
673
|
+
if (!indexResponse.ok) throw new Error(`Unpkg llms.txt request failed with HTTP ${indexResponse.status}.`);
|
|
674
|
+
const bytes = new Uint8Array(await indexResponse.arrayBuffer());
|
|
675
|
+
validateLlmsBytes(bytes);
|
|
676
|
+
verifySri(bytes, metadata.integrity);
|
|
677
|
+
const published = this.cache.publish(identity, bytes, metadata.integrity, referenceBase, timed.signal);
|
|
678
|
+
return {
|
|
679
|
+
identity,
|
|
680
|
+
location: "remote",
|
|
681
|
+
filePath: published.filePath,
|
|
682
|
+
referenceBase: published.referenceBase,
|
|
683
|
+
byteLength: published.bytes.byteLength,
|
|
684
|
+
digest: published.digest
|
|
685
|
+
};
|
|
686
|
+
} finally {
|
|
687
|
+
timed.dispose();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/services/helpRuntime/index.ts
|
|
693
|
+
function reportAsync(error) {
|
|
694
|
+
queueMicrotask(() => {
|
|
695
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function createHelpRuntime(service, options = {}) {
|
|
699
|
+
const cacheRoot = options.cacheRoot ?? defaultHelpCacheRoot();
|
|
700
|
+
const activation = new DefaultHelpActivationService({
|
|
701
|
+
resolver: new DefaultHelpIndexResolver({
|
|
702
|
+
cache: new HelpIndexCache(cacheRoot),
|
|
703
|
+
fetch: options.fetch,
|
|
704
|
+
timeoutMs: options.timeoutMs
|
|
705
|
+
}),
|
|
706
|
+
materializer: new DefaultHelpSkillMaterializer(cacheRoot),
|
|
707
|
+
publisher: { publish: (snapshot) => service.publish(snapshot) },
|
|
708
|
+
onBackgroundError(error) {
|
|
709
|
+
if (options.onBackgroundError) options.onBackgroundError(error);
|
|
710
|
+
else reportAsync(error);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
const synchronize = (contributions) => {
|
|
714
|
+
activation.replaceContributions(contributions);
|
|
715
|
+
};
|
|
716
|
+
const unsubscribe = service.subscribeContributions((contributions) => synchronize(contributions));
|
|
717
|
+
synchronize(service.listContributions());
|
|
718
|
+
let disposed = false;
|
|
719
|
+
return {
|
|
720
|
+
activation,
|
|
721
|
+
dispose() {
|
|
722
|
+
if (disposed) return;
|
|
723
|
+
disposed = true;
|
|
724
|
+
unsubscribe();
|
|
725
|
+
activation.dispose();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
//#endregion
|
|
730
|
+
export { DefaultHelpActivationService, DefaultHelpIndexResolver, DefaultHelpSkillMaterializer, HelpIndexCache, createHelpRuntime, defaultHelpCacheRoot };
|
|
731
|
+
|
|
732
|
+
//# sourceMappingURL=index.mjs.map
|