@code-yeongyu/senpi 2026.6.28-4 → 2026.6.30
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/CHANGELOG.md +15 -0
- package/README.md +2 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +17 -4
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/hooks/command-runner.d.ts +25 -0
- package/dist/core/extensions/builtin/hooks/command-runner.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/command-runner.js +152 -0
- package/dist/core/extensions/builtin/hooks/command-runner.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/command.d.ts +11 -0
- package/dist/core/extensions/builtin/hooks/command.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/command.js +180 -0
- package/dist/core/extensions/builtin/hooks/command.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/config-loader.d.ts +19 -0
- package/dist/core/extensions/builtin/hooks/config-loader.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/config-loader.js +125 -0
- package/dist/core/extensions/builtin/hooks/config-loader.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/diagnostics.d.ts +10 -0
- package/dist/core/extensions/builtin/hooks/diagnostics.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/diagnostics.js +14 -0
- package/dist/core/extensions/builtin/hooks/diagnostics.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/dispatcher.d.ts +60 -0
- package/dist/core/extensions/builtin/hooks/dispatcher.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/dispatcher.js +161 -0
- package/dist/core/extensions/builtin/hooks/dispatcher.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/handler.d.ts +12 -0
- package/dist/core/extensions/builtin/hooks/handler.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/handler.js +128 -0
- package/dist/core/extensions/builtin/hooks/handler.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/index.d.ts +6 -0
- package/dist/core/extensions/builtin/hooks/index.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/index.js +216 -0
- package/dist/core/extensions/builtin/hooks/index.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/lifecycle-adapter.d.ts +29 -0
- package/dist/core/extensions/builtin/hooks/lifecycle-adapter.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/lifecycle-adapter.js +276 -0
- package/dist/core/extensions/builtin/hooks/lifecycle-adapter.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/matcher.d.ts +7 -0
- package/dist/core/extensions/builtin/hooks/matcher.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/matcher.js +109 -0
- package/dist/core/extensions/builtin/hooks/matcher.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/output-bounds.d.ts +29 -0
- package/dist/core/extensions/builtin/hooks/output-bounds.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/output-bounds.js +47 -0
- package/dist/core/extensions/builtin/hooks/output-bounds.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/output-parser.d.ts +27 -0
- package/dist/core/extensions/builtin/hooks/output-parser.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/output-parser.js +168 -0
- package/dist/core/extensions/builtin/hooks/output-parser.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/plugin-loader.d.ts +11 -0
- package/dist/core/extensions/builtin/hooks/plugin-loader.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/plugin-loader.js +138 -0
- package/dist/core/extensions/builtin/hooks/plugin-loader.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/plugin-manifest.d.ts +47 -0
- package/dist/core/extensions/builtin/hooks/plugin-manifest.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/plugin-manifest.js +86 -0
- package/dist/core/extensions/builtin/hooks/plugin-manifest.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/prompt-adapter.d.ts +30 -0
- package/dist/core/extensions/builtin/hooks/prompt-adapter.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/prompt-adapter.js +59 -0
- package/dist/core/extensions/builtin/hooks/prompt-adapter.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/safety.d.ts +14 -0
- package/dist/core/extensions/builtin/hooks/safety.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/safety.js +179 -0
- package/dist/core/extensions/builtin/hooks/safety.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/schema.d.ts +3 -0
- package/dist/core/extensions/builtin/hooks/schema.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/schema.js +94 -0
- package/dist/core/extensions/builtin/hooks/schema.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/stop-adapter.d.ts +18 -0
- package/dist/core/extensions/builtin/hooks/stop-adapter.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/stop-adapter.js +282 -0
- package/dist/core/extensions/builtin/hooks/stop-adapter.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/tool-adapter.d.ts +11 -0
- package/dist/core/extensions/builtin/hooks/tool-adapter.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/tool-adapter.js +158 -0
- package/dist/core/extensions/builtin/hooks/tool-adapter.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/trust-storage.d.ts +24 -0
- package/dist/core/extensions/builtin/hooks/trust-storage.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/trust-storage.js +102 -0
- package/dist/core/extensions/builtin/hooks/trust-storage.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/trust.d.ts +34 -0
- package/dist/core/extensions/builtin/hooks/trust.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/trust.js +194 -0
- package/dist/core/extensions/builtin/hooks/trust.js.map +1 -0
- package/dist/core/extensions/builtin/hooks/types.d.ts +134 -0
- package/dist/core/extensions/builtin/hooks/types.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/types.js +36 -0
- package/dist/core/extensions/builtin/hooks/types.js.map +1 -0
- package/dist/core/extensions/builtin/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/index.js +2 -0
- package/dist/core/extensions/builtin/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +20 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +17 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +14 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +14 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +38 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/version-check.d.ts +1 -0
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +7 -0
- package/dist/utils/version-check.js.map +1 -1
- package/docs/extensions.md +450 -0
- package/docs/index.md +1 -1
- package/docs/packages.md +9 -6
- package/examples/extensions/README.md +1 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +17 -24
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts +17 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +17 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/groq.models.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/groq.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +0 -24
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +16 -35
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.js +12 -12
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.js +12 -12
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.js +12 -12
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi.models.js +15 -15
- package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi.models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +3 -0
- package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/npm-shrinkwrap.json +9 -9
- package/package.json +4 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { diagnostic } from "./diagnostics.js";
|
|
2
|
+
const TOOL_MATCHER_ALIASES = {
|
|
3
|
+
apply_patch: ["ApplyPatch", "functions.apply_patch"],
|
|
4
|
+
bash: ["Bash", "Shell", "shell", "exec_command", "functions.exec_command"],
|
|
5
|
+
create_goal: ["CreateGoal"],
|
|
6
|
+
edit: ["Edit", "MultiEdit", "multi_edit"],
|
|
7
|
+
find: ["Find", "Glob", "glob", "file_search"],
|
|
8
|
+
get_goal: ["GetGoal"],
|
|
9
|
+
grep: ["Grep", "Search", "grep_app"],
|
|
10
|
+
ls: ["LS", "List", "list"],
|
|
11
|
+
read: ["Read", "open", "read_file"],
|
|
12
|
+
todoread: ["TodoRead"],
|
|
13
|
+
todowrite: ["TodoWrite"],
|
|
14
|
+
update_goal: ["UpdateGoal"],
|
|
15
|
+
web_search: ["WebSearch", "web-search"],
|
|
16
|
+
webfetch: ["WebFetch", "web_fetch", "web-fetch"],
|
|
17
|
+
write: ["Write", "write_file"],
|
|
18
|
+
};
|
|
19
|
+
export function matchingHookHandlers(input, handlers) {
|
|
20
|
+
const subject = matcherSubject(input);
|
|
21
|
+
const selected = [];
|
|
22
|
+
const diagnostics = [];
|
|
23
|
+
for (const handler of handlers) {
|
|
24
|
+
if (handler.event !== input.event) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (subject.kind === "ignored") {
|
|
28
|
+
selected.push(handler);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const match = matchesMatcher(handler, subject.inputs);
|
|
32
|
+
diagnostics.push(...match.diagnostics);
|
|
33
|
+
if (match.matched) {
|
|
34
|
+
selected.push(handler);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { handlers: selected, diagnostics };
|
|
38
|
+
}
|
|
39
|
+
function matcherSubject(input) {
|
|
40
|
+
switch (input.event) {
|
|
41
|
+
case "UserPromptSubmit":
|
|
42
|
+
case "Stop":
|
|
43
|
+
return { kind: "ignored" };
|
|
44
|
+
case "PreToolUse":
|
|
45
|
+
case "PostToolUse":
|
|
46
|
+
return { kind: "inputs", inputs: toolMatcherInputs(input.toolName) };
|
|
47
|
+
case "SessionStart":
|
|
48
|
+
case "PreCompact":
|
|
49
|
+
case "PostCompact":
|
|
50
|
+
return { kind: "inputs", inputs: [input.event] };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function toolMatcherInputs(toolName) {
|
|
54
|
+
const values = new Set();
|
|
55
|
+
addMatcherInput(values, toolName);
|
|
56
|
+
const lowerToolName = toolName.toLowerCase();
|
|
57
|
+
addMatcherInput(values, lowerToolName);
|
|
58
|
+
addMatcherInput(values, toClaudeToolName(lowerToolName));
|
|
59
|
+
for (const alias of TOOL_MATCHER_ALIASES[lowerToolName] ?? []) {
|
|
60
|
+
addMatcherInput(values, alias);
|
|
61
|
+
}
|
|
62
|
+
return [...values];
|
|
63
|
+
}
|
|
64
|
+
function addMatcherInput(values, value) {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (trimmed.length > 0) {
|
|
67
|
+
values.add(trimmed);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function toClaudeToolName(toolName) {
|
|
71
|
+
return toolName
|
|
72
|
+
.split(/[-_]/)
|
|
73
|
+
.filter((part) => part.length > 0)
|
|
74
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
75
|
+
.join("");
|
|
76
|
+
}
|
|
77
|
+
function matchesMatcher(handler, inputs) {
|
|
78
|
+
const matcher = handler.matcher?.trim();
|
|
79
|
+
if (matcher === undefined || matcher.length === 0 || matcher === "*") {
|
|
80
|
+
return { matched: true, diagnostics: [] };
|
|
81
|
+
}
|
|
82
|
+
const literalMatched = matcher
|
|
83
|
+
.split(/[|,]/)
|
|
84
|
+
.map((part) => part.trim())
|
|
85
|
+
.filter((part) => part.length > 0)
|
|
86
|
+
.some((part) => inputs.includes(part));
|
|
87
|
+
try {
|
|
88
|
+
const regex = new RegExp(matcher);
|
|
89
|
+
return { matched: literalMatched || inputs.some((input) => regex.test(input)), diagnostics: [] };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof SyntaxError) {
|
|
93
|
+
return {
|
|
94
|
+
matched: literalMatched,
|
|
95
|
+
diagnostics: [
|
|
96
|
+
diagnostic({
|
|
97
|
+
code: "invalid_matcher",
|
|
98
|
+
event: handler.event,
|
|
99
|
+
message: `Hook matcher is not a valid JavaScript regular expression: ${error.message}`,
|
|
100
|
+
path: `hooks.${handler.event}[${handler.groupIndex}].matcher`,
|
|
101
|
+
severity: "warning",
|
|
102
|
+
}, handler.source),
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/hooks/matcher.ts"],"names":[],"mappings":"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","sourcesContent":["import { diagnostic } from \"./diagnostics.ts\";\nimport type { ExecutableHookHandler, HookDiagnostic, HookInputWire } from \"./types.ts\";\n\nexport type HookMatcherResult = {\n\treadonly handlers: readonly ExecutableHookHandler[];\n\treadonly diagnostics: readonly HookDiagnostic[];\n};\n\ntype MatcherSubject = { readonly kind: \"ignored\" } | { readonly kind: \"inputs\"; readonly inputs: readonly string[] };\n\nconst TOOL_MATCHER_ALIASES: Readonly<Record<string, readonly string[]>> = {\n\tapply_patch: [\"ApplyPatch\", \"functions.apply_patch\"],\n\tbash: [\"Bash\", \"Shell\", \"shell\", \"exec_command\", \"functions.exec_command\"],\n\tcreate_goal: [\"CreateGoal\"],\n\tedit: [\"Edit\", \"MultiEdit\", \"multi_edit\"],\n\tfind: [\"Find\", \"Glob\", \"glob\", \"file_search\"],\n\tget_goal: [\"GetGoal\"],\n\tgrep: [\"Grep\", \"Search\", \"grep_app\"],\n\tls: [\"LS\", \"List\", \"list\"],\n\tread: [\"Read\", \"open\", \"read_file\"],\n\ttodoread: [\"TodoRead\"],\n\ttodowrite: [\"TodoWrite\"],\n\tupdate_goal: [\"UpdateGoal\"],\n\tweb_search: [\"WebSearch\", \"web-search\"],\n\twebfetch: [\"WebFetch\", \"web_fetch\", \"web-fetch\"],\n\twrite: [\"Write\", \"write_file\"],\n} as const;\n\nexport function matchingHookHandlers(\n\tinput: HookInputWire,\n\thandlers: readonly ExecutableHookHandler[],\n): HookMatcherResult {\n\tconst subject = matcherSubject(input);\n\tconst selected: ExecutableHookHandler[] = [];\n\tconst diagnostics: HookDiagnostic[] = [];\n\n\tfor (const handler of handlers) {\n\t\tif (handler.event !== input.event) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (subject.kind === \"ignored\") {\n\t\t\tselected.push(handler);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst match = matchesMatcher(handler, subject.inputs);\n\t\tdiagnostics.push(...match.diagnostics);\n\t\tif (match.matched) {\n\t\t\tselected.push(handler);\n\t\t}\n\t}\n\n\treturn { handlers: selected, diagnostics };\n}\n\nfunction matcherSubject(input: HookInputWire): MatcherSubject {\n\tswitch (input.event) {\n\t\tcase \"UserPromptSubmit\":\n\t\tcase \"Stop\":\n\t\t\treturn { kind: \"ignored\" };\n\t\tcase \"PreToolUse\":\n\t\tcase \"PostToolUse\":\n\t\t\treturn { kind: \"inputs\", inputs: toolMatcherInputs(input.toolName) };\n\t\tcase \"SessionStart\":\n\t\tcase \"PreCompact\":\n\t\tcase \"PostCompact\":\n\t\t\treturn { kind: \"inputs\", inputs: [input.event] };\n\t}\n}\n\nfunction toolMatcherInputs(toolName: string): readonly string[] {\n\tconst values = new Set<string>();\n\taddMatcherInput(values, toolName);\n\n\tconst lowerToolName = toolName.toLowerCase();\n\taddMatcherInput(values, lowerToolName);\n\taddMatcherInput(values, toClaudeToolName(lowerToolName));\n\n\tfor (const alias of TOOL_MATCHER_ALIASES[lowerToolName] ?? []) {\n\t\taddMatcherInput(values, alias);\n\t}\n\n\treturn [...values];\n}\n\nfunction addMatcherInput(values: Set<string>, value: string): void {\n\tconst trimmed = value.trim();\n\tif (trimmed.length > 0) {\n\t\tvalues.add(trimmed);\n\t}\n}\n\nfunction toClaudeToolName(toolName: string): string {\n\treturn toolName\n\t\t.split(/[-_]/)\n\t\t.filter((part) => part.length > 0)\n\t\t.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)\n\t\t.join(\"\");\n}\n\nfunction matchesMatcher(\n\thandler: ExecutableHookHandler,\n\tinputs: readonly string[],\n): { readonly matched: boolean; readonly diagnostics: readonly HookDiagnostic[] } {\n\tconst matcher = handler.matcher?.trim();\n\tif (matcher === undefined || matcher.length === 0 || matcher === \"*\") {\n\t\treturn { matched: true, diagnostics: [] };\n\t}\n\n\tconst literalMatched = matcher\n\t\t.split(/[|,]/)\n\t\t.map((part) => part.trim())\n\t\t.filter((part) => part.length > 0)\n\t\t.some((part) => inputs.includes(part));\n\n\ttry {\n\t\tconst regex = new RegExp(matcher);\n\t\treturn { matched: literalMatched || inputs.some((input) => regex.test(input)), diagnostics: [] };\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tmatched: literalMatched,\n\t\t\t\tdiagnostics: [\n\t\t\t\t\tdiagnostic(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcode: \"invalid_matcher\",\n\t\t\t\t\t\t\tevent: handler.event,\n\t\t\t\t\t\t\tmessage: `Hook matcher is not a valid JavaScript regular expression: ${error.message}`,\n\t\t\t\t\t\t\tpath: `hooks.${handler.event}[${handler.groupIndex}].matcher`,\n\t\t\t\t\t\t\tseverity: \"warning\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\thandler.source,\n\t\t\t\t\t),\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\t\tthrow error;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const DEFAULT_STDOUT_LIMIT_BYTES: number;
|
|
2
|
+
export declare const DEFAULT_STDERR_LIMIT_BYTES: number;
|
|
3
|
+
export type HookOutputPolicy = {
|
|
4
|
+
readonly maxStdoutBytes?: number;
|
|
5
|
+
readonly maxStderrBytes?: number;
|
|
6
|
+
readonly spillDir?: string;
|
|
7
|
+
};
|
|
8
|
+
export type HookStreamSafetyMetadata = {
|
|
9
|
+
readonly originalBytes: number;
|
|
10
|
+
readonly returnedBytes: number;
|
|
11
|
+
readonly redacted: boolean;
|
|
12
|
+
readonly spilled: boolean;
|
|
13
|
+
readonly truncated: boolean;
|
|
14
|
+
readonly spillPath?: string;
|
|
15
|
+
};
|
|
16
|
+
export type HookOutputSafetyMetadata = {
|
|
17
|
+
readonly stdout: HookStreamSafetyMetadata;
|
|
18
|
+
readonly stderr: HookStreamSafetyMetadata;
|
|
19
|
+
};
|
|
20
|
+
export type HookSafeOutput = {
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly safety: HookStreamSafetyMetadata;
|
|
23
|
+
};
|
|
24
|
+
export declare function applyHookOutputSafety(stream: "stderr" | "stdout", text: string, policy: HookOutputPolicy | undefined, capture?: {
|
|
25
|
+
readonly originalBytes: number;
|
|
26
|
+
readonly truncated: boolean;
|
|
27
|
+
}): HookSafeOutput;
|
|
28
|
+
export declare function redactHookTokenValues(text: string, replacement?: string): string;
|
|
29
|
+
//# sourceMappingURL=output-bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-bounds.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/hooks/output-bounds.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,0BAA0B,QAAY,CAAC;AACpD,eAAO,MAAM,0BAA0B,QAAY,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC1C,CAAC;AAEF,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GACvE,cAAc,CAqBhB;AAWD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,SAAe,GAAG,MAAM,CAItF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export const DEFAULT_STDOUT_LIMIT_BYTES = 64 * 1024;
|
|
6
|
+
export const DEFAULT_STDERR_LIMIT_BYTES = 64 * 1024;
|
|
7
|
+
export function applyHookOutputSafety(stream, text, policy, capture) {
|
|
8
|
+
const originalBytes = capture?.originalBytes ?? Buffer.byteLength(text);
|
|
9
|
+
const redactedText = redactHookOutput(text);
|
|
10
|
+
const redacted = redactedText !== text;
|
|
11
|
+
const limit = stream === "stdout" ? policy?.maxStdoutBytes : policy?.maxStderrBytes;
|
|
12
|
+
const maxBytes = limit ?? (stream === "stdout" ? DEFAULT_STDOUT_LIMIT_BYTES : DEFAULT_STDERR_LIMIT_BYTES);
|
|
13
|
+
const redactedBytes = Buffer.byteLength(redactedText);
|
|
14
|
+
const truncated = capture?.truncated === true || redactedBytes > maxBytes;
|
|
15
|
+
const spillPath = truncated ? spillRedactedOutput(stream, redactedText, policy) : undefined;
|
|
16
|
+
const returnedText = truncated ? Buffer.from(redactedText).subarray(0, maxBytes).toString("utf8") : redactedText;
|
|
17
|
+
return {
|
|
18
|
+
safety: {
|
|
19
|
+
originalBytes,
|
|
20
|
+
redacted,
|
|
21
|
+
returnedBytes: Buffer.byteLength(returnedText),
|
|
22
|
+
spilled: spillPath !== undefined,
|
|
23
|
+
...(spillPath === undefined ? {} : { spillPath }),
|
|
24
|
+
truncated,
|
|
25
|
+
},
|
|
26
|
+
text: returnedText,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function redactHookOutput(text) {
|
|
30
|
+
return redactHookTokenValues(text
|
|
31
|
+
.replace(/\b([A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|AUTH)[A-Z0-9_]*)=([^\s'"]+)/g, "$1=[REDACTED]")
|
|
32
|
+
.replace(/\b(Authorization:\s*Bearer\s+)([^\s'"]+)/gi, "$1[REDACTED]")
|
|
33
|
+
.replace(/\b(Bearer\s+)(sk-[A-Za-z0-9._-]+)/g, "$1[REDACTED]"));
|
|
34
|
+
}
|
|
35
|
+
export function redactHookTokenValues(text, replacement = "[REDACTED]") {
|
|
36
|
+
return text
|
|
37
|
+
.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, replacement)
|
|
38
|
+
.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement);
|
|
39
|
+
}
|
|
40
|
+
function spillRedactedOutput(stream, text, policy) {
|
|
41
|
+
const spillDir = policy?.spillDir ?? join(tmpdir(), "senpi-hook-output");
|
|
42
|
+
mkdirSync(spillDir, { recursive: true });
|
|
43
|
+
const path = join(spillDir, `hook-${stream}-${process.pid}-${randomUUID()}.txt`);
|
|
44
|
+
writeFileSync(path, text);
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=output-bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-bounds.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/hooks/output-bounds.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const DEFAULT_STDOUT_LIMIT_BYTES = 64 * 1024;\nexport const DEFAULT_STDERR_LIMIT_BYTES = 64 * 1024;\n\nexport type HookOutputPolicy = {\n\treadonly maxStdoutBytes?: number;\n\treadonly maxStderrBytes?: number;\n\treadonly spillDir?: string;\n};\n\nexport type HookStreamSafetyMetadata = {\n\treadonly originalBytes: number;\n\treadonly returnedBytes: number;\n\treadonly redacted: boolean;\n\treadonly spilled: boolean;\n\treadonly truncated: boolean;\n\treadonly spillPath?: string;\n};\n\nexport type HookOutputSafetyMetadata = {\n\treadonly stdout: HookStreamSafetyMetadata;\n\treadonly stderr: HookStreamSafetyMetadata;\n};\n\nexport type HookSafeOutput = {\n\treadonly text: string;\n\treadonly safety: HookStreamSafetyMetadata;\n};\n\nexport function applyHookOutputSafety(\n\tstream: \"stderr\" | \"stdout\",\n\ttext: string,\n\tpolicy: HookOutputPolicy | undefined,\n\tcapture?: { readonly originalBytes: number; readonly truncated: boolean },\n): HookSafeOutput {\n\tconst originalBytes = capture?.originalBytes ?? Buffer.byteLength(text);\n\tconst redactedText = redactHookOutput(text);\n\tconst redacted = redactedText !== text;\n\tconst limit = stream === \"stdout\" ? policy?.maxStdoutBytes : policy?.maxStderrBytes;\n\tconst maxBytes = limit ?? (stream === \"stdout\" ? DEFAULT_STDOUT_LIMIT_BYTES : DEFAULT_STDERR_LIMIT_BYTES);\n\tconst redactedBytes = Buffer.byteLength(redactedText);\n\tconst truncated = capture?.truncated === true || redactedBytes > maxBytes;\n\tconst spillPath = truncated ? spillRedactedOutput(stream, redactedText, policy) : undefined;\n\tconst returnedText = truncated ? Buffer.from(redactedText).subarray(0, maxBytes).toString(\"utf8\") : redactedText;\n\treturn {\n\t\tsafety: {\n\t\t\toriginalBytes,\n\t\t\tredacted,\n\t\t\treturnedBytes: Buffer.byteLength(returnedText),\n\t\t\tspilled: spillPath !== undefined,\n\t\t\t...(spillPath === undefined ? {} : { spillPath }),\n\t\t\ttruncated,\n\t\t},\n\t\ttext: returnedText,\n\t};\n}\n\nfunction redactHookOutput(text: string): string {\n\treturn redactHookTokenValues(\n\t\ttext\n\t\t\t.replace(/\\b([A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|AUTH)[A-Z0-9_]*)=([^\\s'\"]+)/g, \"$1=[REDACTED]\")\n\t\t\t.replace(/\\b(Authorization:\\s*Bearer\\s+)([^\\s'\"]+)/gi, \"$1[REDACTED]\")\n\t\t\t.replace(/\\b(Bearer\\s+)(sk-[A-Za-z0-9._-]+)/g, \"$1[REDACTED]\"),\n\t);\n}\n\nexport function redactHookTokenValues(text: string, replacement = \"[REDACTED]\"): string {\n\treturn text\n\t\t.replace(/\\bghp_[A-Za-z0-9]{20,}\\b/g, replacement)\n\t\t.replace(/\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/g, replacement);\n}\n\nfunction spillRedactedOutput(stream: \"stderr\" | \"stdout\", text: string, policy: HookOutputPolicy | undefined): string {\n\tconst spillDir = policy?.spillDir ?? join(tmpdir(), \"senpi-hook-output\");\n\tmkdirSync(spillDir, { recursive: true });\n\tconst path = join(spillDir, `hook-${stream}-${process.pid}-${randomUUID()}.txt`);\n\twriteFileSync(path, text);\n\treturn path;\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HookDiagnostic, HookSourceMetadata, SupportedHookEvent } from "./types.ts";
|
|
2
|
+
export type HookOutputParseInput = {
|
|
3
|
+
readonly event: SupportedHookEvent;
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
readonly stdout: string;
|
|
6
|
+
readonly stderr: string;
|
|
7
|
+
readonly source: HookSourceMetadata;
|
|
8
|
+
};
|
|
9
|
+
type HookDecision = "allow" | "approve" | "ask" | "block" | "deny";
|
|
10
|
+
type MutableHookOutput = {
|
|
11
|
+
decision?: HookDecision;
|
|
12
|
+
reason?: string;
|
|
13
|
+
additionalContext?: string;
|
|
14
|
+
updatedInput?: unknown;
|
|
15
|
+
updatedToolOutput?: unknown;
|
|
16
|
+
continue?: boolean;
|
|
17
|
+
stopReason?: string;
|
|
18
|
+
suppressOutput?: boolean;
|
|
19
|
+
systemMessage?: string;
|
|
20
|
+
};
|
|
21
|
+
export type ParsedHookOutput = {
|
|
22
|
+
readonly output: Readonly<MutableHookOutput>;
|
|
23
|
+
readonly diagnostics: readonly HookDiagnostic[];
|
|
24
|
+
};
|
|
25
|
+
export declare function parseHookOutput(input: HookOutputParseInput): ParsedHookOutput;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=output-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/hooks/output-parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEzF,MAAM,MAAM,oBAAoB,GAAG;IAClC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;CACpC,CAAC;AAEF,KAAK,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;AACnE,KAAK,iBAAiB,GAAG;IACxB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;CAChD,CAAC;AAgBF,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,gBAAgB,CAe7E"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { diagnostic } from "./diagnostics.js";
|
|
2
|
+
const SYSTEM_MESSAGE_EVENTS = new Set([
|
|
3
|
+
"PreToolUse",
|
|
4
|
+
"PostToolUse",
|
|
5
|
+
"UserPromptSubmit",
|
|
6
|
+
"SessionStart",
|
|
7
|
+
"Stop",
|
|
8
|
+
]);
|
|
9
|
+
export function parseHookOutput(input) {
|
|
10
|
+
const state = { input, output: {}, diagnostics: [] };
|
|
11
|
+
const stderr = text(input.stderr);
|
|
12
|
+
if (input.exitCode === 2) {
|
|
13
|
+
return { output: { decision: "block", ...(stderr === undefined ? {} : { reason: stderr }) }, diagnostics: [] };
|
|
14
|
+
}
|
|
15
|
+
const stdout = input.stdout.trim();
|
|
16
|
+
if (stdout.length === 0)
|
|
17
|
+
return parsedOutput(state);
|
|
18
|
+
const parsed = parseStdoutJson(stdout, state);
|
|
19
|
+
if (!isRecord(parsed))
|
|
20
|
+
return parsedOutput(state);
|
|
21
|
+
parseUniversal(parsed, state);
|
|
22
|
+
const specific = parseSpecific(parsed.hookSpecificOutput, state);
|
|
23
|
+
if (specific === "mismatched" || specific === "invalid")
|
|
24
|
+
return parsedOutput(state);
|
|
25
|
+
parseEvent(parsed, specific, state);
|
|
26
|
+
return parsedOutput(state);
|
|
27
|
+
}
|
|
28
|
+
function parseStdoutJson(stdout, state) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(stdout);
|
|
31
|
+
if (isRecord(parsed))
|
|
32
|
+
return parsed;
|
|
33
|
+
add(state, "invalid_root", "stdout", "Hook stdout JSON must be an object.");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (!(error instanceof SyntaxError))
|
|
37
|
+
throw error;
|
|
38
|
+
add(state, "invalid_root", "stdout", "Hook stdout must be valid JSON.");
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function parseUniversal(parsed, state) {
|
|
43
|
+
if (typeof parsed.continue === "boolean") {
|
|
44
|
+
state.output.continue = parsed.continue;
|
|
45
|
+
if (state.input.event === "Stop" && parsed.continue === false)
|
|
46
|
+
state.output.decision = "block";
|
|
47
|
+
}
|
|
48
|
+
copyText(parsed.stopReason, "stopReason", state);
|
|
49
|
+
if (typeof parsed.suppressOutput === "boolean")
|
|
50
|
+
state.output.suppressOutput = parsed.suppressOutput;
|
|
51
|
+
if (parsed.systemMessage === undefined)
|
|
52
|
+
return;
|
|
53
|
+
if (SYSTEM_MESSAGE_EVENTS.has(state.input.event)) {
|
|
54
|
+
copyText(parsed.systemMessage, "systemMessage", state);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
add(state, "unsupported_field", "stdout.systemMessage", "Hook systemMessage is not supported for this event.", "warning");
|
|
58
|
+
}
|
|
59
|
+
function parseSpecific(value, state) {
|
|
60
|
+
if (value === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (!isRecord(value)) {
|
|
63
|
+
add(state, "invalid_event_config", "stdout.hookSpecificOutput", "Hook hookSpecificOutput field must be an object.");
|
|
64
|
+
return "invalid";
|
|
65
|
+
}
|
|
66
|
+
const eventName = value.hookEventName;
|
|
67
|
+
if (eventName === undefined || eventName === state.input.event)
|
|
68
|
+
return value;
|
|
69
|
+
add(state, "invalid_event_config", "stdout.hookSpecificOutput.hookEventName", `Hook output event ${String(eventName)} does not match ${state.input.event}.`);
|
|
70
|
+
return "mismatched";
|
|
71
|
+
}
|
|
72
|
+
function parseEvent(parsed, specific, state) {
|
|
73
|
+
switch (state.input.event) {
|
|
74
|
+
case "PreToolUse":
|
|
75
|
+
parsePreToolUse(parsed, specific, state);
|
|
76
|
+
return;
|
|
77
|
+
case "PostToolUse":
|
|
78
|
+
blockOnlyDecision(parsed.decision, "PostToolUse", state);
|
|
79
|
+
copyText(parsed.reason, "reason", state);
|
|
80
|
+
copyText(specific?.additionalContext ?? parsed.additionalContext, "additionalContext", state);
|
|
81
|
+
copyUnknown(specific?.updatedToolOutput ?? parsed.updatedToolOutput, "updatedToolOutput", state);
|
|
82
|
+
return;
|
|
83
|
+
case "UserPromptSubmit":
|
|
84
|
+
blockOnlyDecision(parsed.decision, "UserPromptSubmit", state);
|
|
85
|
+
copyText(parsed.reason, "reason", state);
|
|
86
|
+
copyText(specific?.additionalContext ?? parsed.additionalContext, "additionalContext", state);
|
|
87
|
+
rejectPromptReplacement(specific, state);
|
|
88
|
+
return;
|
|
89
|
+
case "Stop":
|
|
90
|
+
if (parsed.decision === "block")
|
|
91
|
+
state.output.decision = "block";
|
|
92
|
+
else if (parsed.decision !== undefined && parsed.decision !== "continue") {
|
|
93
|
+
add(state, "unsupported_field", "stdout.decision", "Stop only supports decision block or continue.", "warning");
|
|
94
|
+
}
|
|
95
|
+
copyText(parsed.reason, "reason", state);
|
|
96
|
+
copyText(specific?.additionalContext ?? parsed.additionalContext, "additionalContext", state);
|
|
97
|
+
return;
|
|
98
|
+
case "SessionStart":
|
|
99
|
+
copyText(specific?.additionalContext ?? parsed.additionalContext, "additionalContext", state);
|
|
100
|
+
if (parsed.decision !== undefined) {
|
|
101
|
+
add(state, "unsupported_field", "stdout.decision", "SessionStart does not support decisions.", "warning");
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
case "PreCompact":
|
|
105
|
+
case "PostCompact":
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function parsePreToolUse(parsed, specific, state) {
|
|
110
|
+
const decision = preToolUseDecision(specific?.permissionDecision ?? parsed.decision);
|
|
111
|
+
if (decision !== undefined)
|
|
112
|
+
state.output.decision = decision;
|
|
113
|
+
copyText(specific?.permissionDecisionReason ?? parsed.reason, "reason", state);
|
|
114
|
+
copyText(specific?.additionalContext ?? parsed.additionalContext, "additionalContext", state);
|
|
115
|
+
const updatedInput = specific?.updatedInput ?? parsed.updatedInput;
|
|
116
|
+
if (updatedInput === undefined)
|
|
117
|
+
return;
|
|
118
|
+
if (specific?.permissionDecision === "allow") {
|
|
119
|
+
state.output.updatedInput = updatedInput;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
add(state, "unsupported_field", "stdout.hookSpecificOutput.updatedInput", "PreToolUse updatedInput is only applied when permissionDecision is allow.", "warning");
|
|
123
|
+
}
|
|
124
|
+
function blockOnlyDecision(value, event, state) {
|
|
125
|
+
if (value === "block")
|
|
126
|
+
state.output.decision = "block";
|
|
127
|
+
else if (value !== undefined)
|
|
128
|
+
add(state, "unsupported_field", "stdout.decision", `${event} only supports decision block.`, "warning");
|
|
129
|
+
}
|
|
130
|
+
function rejectPromptReplacement(specific, state) {
|
|
131
|
+
for (const field of ["prompt", "updatedPrompt", "replacementPrompt"]) {
|
|
132
|
+
if (specific !== undefined && Object.hasOwn(specific, field)) {
|
|
133
|
+
add(state, "unsupported_field", `stdout.hookSpecificOutput.${field}`, "UserPromptSubmit prompt replacement is not supported.", "warning");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function preToolUseDecision(value) {
|
|
138
|
+
if (value === "allow" || value === "approve" || value === "ask")
|
|
139
|
+
return value;
|
|
140
|
+
if (value === "deny" || value === "block")
|
|
141
|
+
return "deny";
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
function copyText(value, field, state) {
|
|
145
|
+
const normalized = text(value);
|
|
146
|
+
if (normalized !== undefined)
|
|
147
|
+
state.output[field] = normalized;
|
|
148
|
+
}
|
|
149
|
+
function copyUnknown(value, field, state) {
|
|
150
|
+
if (value !== undefined)
|
|
151
|
+
state.output[field] = value;
|
|
152
|
+
}
|
|
153
|
+
function add(state, code, path, message, severity) {
|
|
154
|
+
state.diagnostics.push(diagnostic({ code, event: state.input.event, message, path, ...(severity === undefined ? {} : { severity }) }, state.input.source));
|
|
155
|
+
}
|
|
156
|
+
function text(value) {
|
|
157
|
+
if (typeof value !== "string")
|
|
158
|
+
return undefined;
|
|
159
|
+
const trimmed = value.trim();
|
|
160
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
161
|
+
}
|
|
162
|
+
function isRecord(value) {
|
|
163
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
164
|
+
}
|
|
165
|
+
function parsedOutput(state) {
|
|
166
|
+
return { output: state.output, diagnostics: state.diagnostics };
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=output-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-parser.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/hooks/output-parser.ts"],"names":[],"mappings":"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","sourcesContent":["import { diagnostic } from \"./diagnostics.ts\";\nimport type { HookDiagnostic, HookSourceMetadata, SupportedHookEvent } from \"./types.ts\";\n\nexport type HookOutputParseInput = {\n\treadonly event: SupportedHookEvent;\n\treadonly exitCode: number;\n\treadonly stdout: string;\n\treadonly stderr: string;\n\treadonly source: HookSourceMetadata;\n};\n\ntype HookDecision = \"allow\" | \"approve\" | \"ask\" | \"block\" | \"deny\";\ntype MutableHookOutput = {\n\tdecision?: HookDecision;\n\treason?: string;\n\tadditionalContext?: string;\n\tupdatedInput?: unknown;\n\tupdatedToolOutput?: unknown;\n\tcontinue?: boolean;\n\tstopReason?: string;\n\tsuppressOutput?: boolean;\n\tsystemMessage?: string;\n};\n\nexport type ParsedHookOutput = {\n\treadonly output: Readonly<MutableHookOutput>;\n\treadonly diagnostics: readonly HookDiagnostic[];\n};\n\ntype ParseState = {\n\treadonly input: HookOutputParseInput;\n\treadonly output: MutableHookOutput;\n\treadonly diagnostics: HookDiagnostic[];\n};\n\nconst SYSTEM_MESSAGE_EVENTS = new Set<SupportedHookEvent>([\n\t\"PreToolUse\",\n\t\"PostToolUse\",\n\t\"UserPromptSubmit\",\n\t\"SessionStart\",\n\t\"Stop\",\n]);\n\nexport function parseHookOutput(input: HookOutputParseInput): ParsedHookOutput {\n\tconst state: ParseState = { input, output: {}, diagnostics: [] };\n\tconst stderr = text(input.stderr);\n\tif (input.exitCode === 2) {\n\t\treturn { output: { decision: \"block\", ...(stderr === undefined ? {} : { reason: stderr }) }, diagnostics: [] };\n\t}\n\tconst stdout = input.stdout.trim();\n\tif (stdout.length === 0) return parsedOutput(state);\n\tconst parsed = parseStdoutJson(stdout, state);\n\tif (!isRecord(parsed)) return parsedOutput(state);\n\tparseUniversal(parsed, state);\n\tconst specific = parseSpecific(parsed.hookSpecificOutput, state);\n\tif (specific === \"mismatched\" || specific === \"invalid\") return parsedOutput(state);\n\tparseEvent(parsed, specific, state);\n\treturn parsedOutput(state);\n}\n\nfunction parseStdoutJson(stdout: string, state: ParseState): unknown {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(stdout);\n\t\tif (isRecord(parsed)) return parsed;\n\t\tadd(state, \"invalid_root\", \"stdout\", \"Hook stdout JSON must be an object.\");\n\t} catch (error) {\n\t\tif (!(error instanceof SyntaxError)) throw error;\n\t\tadd(state, \"invalid_root\", \"stdout\", \"Hook stdout must be valid JSON.\");\n\t}\n\treturn undefined;\n}\n\nfunction parseUniversal(parsed: Record<string, unknown>, state: ParseState): void {\n\tif (typeof parsed.continue === \"boolean\") {\n\t\tstate.output.continue = parsed.continue;\n\t\tif (state.input.event === \"Stop\" && parsed.continue === false) state.output.decision = \"block\";\n\t}\n\tcopyText(parsed.stopReason, \"stopReason\", state);\n\tif (typeof parsed.suppressOutput === \"boolean\") state.output.suppressOutput = parsed.suppressOutput;\n\tif (parsed.systemMessage === undefined) return;\n\tif (SYSTEM_MESSAGE_EVENTS.has(state.input.event)) {\n\t\tcopyText(parsed.systemMessage, \"systemMessage\", state);\n\t\treturn;\n\t}\n\tadd(\n\t\tstate,\n\t\t\"unsupported_field\",\n\t\t\"stdout.systemMessage\",\n\t\t\"Hook systemMessage is not supported for this event.\",\n\t\t\"warning\",\n\t);\n}\n\nfunction parseSpecific(\n\tvalue: unknown,\n\tstate: ParseState,\n): Record<string, unknown> | \"invalid\" | \"mismatched\" | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isRecord(value)) {\n\t\tadd(\n\t\t\tstate,\n\t\t\t\"invalid_event_config\",\n\t\t\t\"stdout.hookSpecificOutput\",\n\t\t\t\"Hook hookSpecificOutput field must be an object.\",\n\t\t);\n\t\treturn \"invalid\";\n\t}\n\tconst eventName = value.hookEventName;\n\tif (eventName === undefined || eventName === state.input.event) return value;\n\tadd(\n\t\tstate,\n\t\t\"invalid_event_config\",\n\t\t\"stdout.hookSpecificOutput.hookEventName\",\n\t\t`Hook output event ${String(eventName)} does not match ${state.input.event}.`,\n\t);\n\treturn \"mismatched\";\n}\n\nfunction parseEvent(\n\tparsed: Record<string, unknown>,\n\tspecific: Record<string, unknown> | undefined,\n\tstate: ParseState,\n): void {\n\tswitch (state.input.event) {\n\t\tcase \"PreToolUse\":\n\t\t\tparsePreToolUse(parsed, specific, state);\n\t\t\treturn;\n\t\tcase \"PostToolUse\":\n\t\t\tblockOnlyDecision(parsed.decision, \"PostToolUse\", state);\n\t\t\tcopyText(parsed.reason, \"reason\", state);\n\t\t\tcopyText(specific?.additionalContext ?? parsed.additionalContext, \"additionalContext\", state);\n\t\t\tcopyUnknown(specific?.updatedToolOutput ?? parsed.updatedToolOutput, \"updatedToolOutput\", state);\n\t\t\treturn;\n\t\tcase \"UserPromptSubmit\":\n\t\t\tblockOnlyDecision(parsed.decision, \"UserPromptSubmit\", state);\n\t\t\tcopyText(parsed.reason, \"reason\", state);\n\t\t\tcopyText(specific?.additionalContext ?? parsed.additionalContext, \"additionalContext\", state);\n\t\t\trejectPromptReplacement(specific, state);\n\t\t\treturn;\n\t\tcase \"Stop\":\n\t\t\tif (parsed.decision === \"block\") state.output.decision = \"block\";\n\t\t\telse if (parsed.decision !== undefined && parsed.decision !== \"continue\") {\n\t\t\t\tadd(\n\t\t\t\t\tstate,\n\t\t\t\t\t\"unsupported_field\",\n\t\t\t\t\t\"stdout.decision\",\n\t\t\t\t\t\"Stop only supports decision block or continue.\",\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tcopyText(parsed.reason, \"reason\", state);\n\t\t\tcopyText(specific?.additionalContext ?? parsed.additionalContext, \"additionalContext\", state);\n\t\t\treturn;\n\t\tcase \"SessionStart\":\n\t\t\tcopyText(specific?.additionalContext ?? parsed.additionalContext, \"additionalContext\", state);\n\t\t\tif (parsed.decision !== undefined) {\n\t\t\t\tadd(state, \"unsupported_field\", \"stdout.decision\", \"SessionStart does not support decisions.\", \"warning\");\n\t\t\t}\n\t\t\treturn;\n\t\tcase \"PreCompact\":\n\t\tcase \"PostCompact\":\n\t\t\treturn;\n\t}\n}\n\nfunction parsePreToolUse(\n\tparsed: Record<string, unknown>,\n\tspecific: Record<string, unknown> | undefined,\n\tstate: ParseState,\n): void {\n\tconst decision = preToolUseDecision(specific?.permissionDecision ?? parsed.decision);\n\tif (decision !== undefined) state.output.decision = decision;\n\tcopyText(specific?.permissionDecisionReason ?? parsed.reason, \"reason\", state);\n\tcopyText(specific?.additionalContext ?? parsed.additionalContext, \"additionalContext\", state);\n\tconst updatedInput = specific?.updatedInput ?? parsed.updatedInput;\n\tif (updatedInput === undefined) return;\n\tif (specific?.permissionDecision === \"allow\") {\n\t\tstate.output.updatedInput = updatedInput;\n\t\treturn;\n\t}\n\tadd(\n\t\tstate,\n\t\t\"unsupported_field\",\n\t\t\"stdout.hookSpecificOutput.updatedInput\",\n\t\t\"PreToolUse updatedInput is only applied when permissionDecision is allow.\",\n\t\t\"warning\",\n\t);\n}\n\nfunction blockOnlyDecision(value: unknown, event: string, state: ParseState): void {\n\tif (value === \"block\") state.output.decision = \"block\";\n\telse if (value !== undefined)\n\t\tadd(state, \"unsupported_field\", \"stdout.decision\", `${event} only supports decision block.`, \"warning\");\n}\n\nfunction rejectPromptReplacement(specific: Record<string, unknown> | undefined, state: ParseState): void {\n\tfor (const field of [\"prompt\", \"updatedPrompt\", \"replacementPrompt\"]) {\n\t\tif (specific !== undefined && Object.hasOwn(specific, field)) {\n\t\t\tadd(\n\t\t\t\tstate,\n\t\t\t\t\"unsupported_field\",\n\t\t\t\t`stdout.hookSpecificOutput.${field}`,\n\t\t\t\t\"UserPromptSubmit prompt replacement is not supported.\",\n\t\t\t\t\"warning\",\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction preToolUseDecision(value: unknown): HookDecision | undefined {\n\tif (value === \"allow\" || value === \"approve\" || value === \"ask\") return value;\n\tif (value === \"deny\" || value === \"block\") return \"deny\";\n\treturn undefined;\n}\n\nfunction copyText(\n\tvalue: unknown,\n\tfield: \"additionalContext\" | \"reason\" | \"stopReason\" | \"systemMessage\",\n\tstate: ParseState,\n): void {\n\tconst normalized = text(value);\n\tif (normalized !== undefined) state.output[field] = normalized;\n}\n\nfunction copyUnknown(value: unknown, field: \"updatedToolOutput\", state: ParseState): void {\n\tif (value !== undefined) state.output[field] = value;\n}\n\nfunction add(\n\tstate: ParseState,\n\tcode: \"invalid_event_config\" | \"invalid_root\" | \"unsupported_field\",\n\tpath: string,\n\tmessage: string,\n\tseverity?: \"error\" | \"warning\",\n): void {\n\tstate.diagnostics.push(\n\t\tdiagnostic(\n\t\t\t{ code, event: state.input.event, message, path, ...(severity === undefined ? {} : { severity }) },\n\t\t\tstate.input.source,\n\t\t),\n\t);\n}\n\nfunction text(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\treturn trimmed.length === 0 ? undefined : trimmed;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parsedOutput(state: ParseState): ParsedHookOutput {\n\treturn { output: state.output, diagnostics: state.diagnostics };\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type LoadPluginHookManifestOptions, type PluginHookSourceMetadata } from "./plugin-manifest.ts";
|
|
2
|
+
import type { ExecutableHookHandler, HookDiagnostic, ParsedHookConfig } from "./types.ts";
|
|
3
|
+
export type { LoadPluginHookManifestOptions, PluginHookEnv, PluginHookSourceMetadata } from "./plugin-manifest.ts";
|
|
4
|
+
export type PluginHookManifestLoadResult = {
|
|
5
|
+
readonly sources: readonly PluginHookSourceMetadata[];
|
|
6
|
+
readonly parsed: ParsedHookConfig;
|
|
7
|
+
readonly diagnostics: readonly HookDiagnostic[];
|
|
8
|
+
};
|
|
9
|
+
export declare function loadPluginHookManifest(options: LoadPluginHookManifestOptions): PluginHookManifestLoadResult;
|
|
10
|
+
export declare function selectHookCommandForPlatform(handler: ExecutableHookHandler, platform?: NodeJS.Platform): string;
|
|
11
|
+
//# sourceMappingURL=plugin-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-loader.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/hooks/plugin-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAMN,KAAK,6BAA6B,EAGlC,KAAK,wBAAwB,EAK7B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE1F,YAAY,EAAE,6BAA6B,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAEnH,MAAM,MAAM,4BAA4B,GAAG;IAC1C,QAAQ,CAAC,OAAO,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;CAChD,CAAC;AAOF,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,4BAA4B,CA+C3G;AAED,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC1C,MAAM,CAKR"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { diagnostic } from "./diagnostics.js";
|
|
3
|
+
import { buildPluginEnv, DEFAULT_HOOK_PATH, directoryExists, fileExists, isRecord, MANIFEST_PATH, pluginSource, readJsonFile, resolveContainedPath, sourceForPath, } from "./plugin-manifest.js";
|
|
4
|
+
import { validateHookHandlerSafety } from "./safety.js";
|
|
5
|
+
import { parseHookConfig } from "./schema.js";
|
|
6
|
+
export function loadPluginHookManifest(options) {
|
|
7
|
+
const pluginRoot = resolve(options.pluginRoot);
|
|
8
|
+
const manifestPath = join(pluginRoot, MANIFEST_PATH);
|
|
9
|
+
const env = buildPluginEnv(pluginRoot, options.dataRoot);
|
|
10
|
+
const discoveredAt = options.discoveredAt ?? "pre-session";
|
|
11
|
+
const diagnostics = [];
|
|
12
|
+
const drafts = [];
|
|
13
|
+
if (!directoryExists(pluginRoot)) {
|
|
14
|
+
const source = pluginSource({
|
|
15
|
+
discoveredAt,
|
|
16
|
+
displayOrder: options.displayOrder,
|
|
17
|
+
env,
|
|
18
|
+
manifestPath,
|
|
19
|
+
pluginRoot,
|
|
20
|
+
sourcePath: manifestPath,
|
|
21
|
+
});
|
|
22
|
+
diagnostics.push(diagnostic({ code: "invalid_root", message: `Plugin root does not exist: ${pluginRoot}`, path: "$.pluginRoot" }, source));
|
|
23
|
+
return emptyResult(diagnostics);
|
|
24
|
+
}
|
|
25
|
+
if (fileExists(manifestPath)) {
|
|
26
|
+
const manifest = readJsonFile(manifestPath, manifestPath, sourceForPath(options, env, manifestPath));
|
|
27
|
+
if (manifest.ok) {
|
|
28
|
+
drafts.push(...readManifestHookDrafts(manifest.value, options, env, manifestPath, diagnostics));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
diagnostics.push(manifest.diagnostic);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const defaultPath = resolveContainedPath(pluginRoot, DEFAULT_HOOK_PATH);
|
|
35
|
+
if (options.includeDefaultHooks !== false && defaultPath.ok && fileExists(defaultPath.path)) {
|
|
36
|
+
const source = sourceForPath(options, env, defaultPath.path);
|
|
37
|
+
const config = readJsonFile(defaultPath.path, manifestPath, source);
|
|
38
|
+
if (config.ok) {
|
|
39
|
+
drafts.push({ config: config.value, key: defaultPath.path });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
diagnostics.push(config.diagnostic);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return parseDrafts(drafts, options, env, manifestPath, diagnostics);
|
|
46
|
+
}
|
|
47
|
+
export function selectHookCommandForPlatform(handler, platform = process.platform) {
|
|
48
|
+
if (platform === "win32" && handler.config.commandWindows !== undefined) {
|
|
49
|
+
return handler.config.commandWindows;
|
|
50
|
+
}
|
|
51
|
+
return handler.config.command;
|
|
52
|
+
}
|
|
53
|
+
function readManifestHookDrafts(manifest, options, env, manifestPath, diagnostics) {
|
|
54
|
+
if (!isRecord(manifest)) {
|
|
55
|
+
diagnostics.push(diagnostic({ code: "invalid_root", message: "Plugin manifest must be an object.", path: "$" }, sourceForPath(options, env, manifestPath)));
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const hooks = manifest.hooks;
|
|
59
|
+
if (hooks === undefined) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return normalizeHookDrafts(hooks, "$.hooks", options, env, manifestPath, diagnostics);
|
|
63
|
+
}
|
|
64
|
+
function normalizeHookDrafts(value, path, options, env, manifestPath, diagnostics) {
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
const resolved = resolveContainedPath(options.pluginRoot, value);
|
|
67
|
+
if (!resolved.ok) {
|
|
68
|
+
diagnostics.push(diagnostic({ code: "invalid_root", message: resolved.message, path }, sourceForPath(options, env, manifestPath)));
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
if (!fileExists(resolved.path)) {
|
|
72
|
+
diagnostics.push(diagnostic({ code: "invalid_root", message: `Plugin hook file does not exist: ${resolved.path}`, path }, sourceForPath(options, env, resolved.path)));
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const source = sourceForPath(options, env, resolved.path);
|
|
76
|
+
const parsed = readJsonFile(resolved.path, manifestPath, source);
|
|
77
|
+
if (parsed.ok) {
|
|
78
|
+
return [{ config: parsed.value, key: resolved.path }];
|
|
79
|
+
}
|
|
80
|
+
diagnostics.push(parsed.diagnostic);
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
const drafts = [];
|
|
85
|
+
for (const [index, item] of value.entries()) {
|
|
86
|
+
drafts.push(...normalizeHookDrafts(item, `${path}[${index}]`, options, env, manifestPath, diagnostics));
|
|
87
|
+
}
|
|
88
|
+
return drafts;
|
|
89
|
+
}
|
|
90
|
+
if (isRecord(value)) {
|
|
91
|
+
return [{ config: value, key: `${manifestPath}#${inlineKeyPath(path)}` }];
|
|
92
|
+
}
|
|
93
|
+
diagnostics.push(diagnostic({ code: "invalid_root", message: "Plugin manifest hooks must be a path or hook object.", path }, sourceForPath(options, env, manifestPath)));
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
function parseDrafts(drafts, options, env, manifestPath, diagnostics) {
|
|
97
|
+
const sources = [];
|
|
98
|
+
const executableHandlers = [];
|
|
99
|
+
const parseDiagnostics = [];
|
|
100
|
+
for (const [index, draft] of drafts.entries()) {
|
|
101
|
+
const source = pluginSource({
|
|
102
|
+
discoveredAt: options.discoveredAt ?? "pre-session",
|
|
103
|
+
displayOrder: options.displayOrder + index,
|
|
104
|
+
env,
|
|
105
|
+
manifestPath,
|
|
106
|
+
pluginRoot: resolve(options.pluginRoot),
|
|
107
|
+
sourcePath: draft.key,
|
|
108
|
+
});
|
|
109
|
+
sources.push(source);
|
|
110
|
+
const parsed = parseHookConfig(draft.config, source);
|
|
111
|
+
for (const handler of parsed.executableHandlers) {
|
|
112
|
+
const safetyDiagnostics = validateHookHandlerSafety(handler);
|
|
113
|
+
if (safetyDiagnostics.length === 0) {
|
|
114
|
+
executableHandlers.push(handler);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
parseDiagnostics.push(...safetyDiagnostics);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
parseDiagnostics.push(...parsed.diagnostics);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
diagnostics: [...diagnostics, ...parseDiagnostics],
|
|
124
|
+
parsed: { diagnostics: parseDiagnostics, executableHandlers },
|
|
125
|
+
sources,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function emptyResult(diagnostics) {
|
|
129
|
+
return {
|
|
130
|
+
diagnostics,
|
|
131
|
+
parsed: { diagnostics, executableHandlers: [] },
|
|
132
|
+
sources: [],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function inlineKeyPath(path) {
|
|
136
|
+
return path.replace(/^\$\.?/, "");
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=plugin-loader.js.map
|