auto-mode 0.2.0 → 0.4.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/README.md +58 -91
- package/dist/{classify-locally-rTUwxBZi.js → classify-locally-BcyBikYr.js} +744 -44
- package/dist/cli.js +88 -15
- package/dist/index.d.ts +33 -5
- package/dist/index.js +1 -1
- package/mods/auto-mode/.claude-plugin/plugin.json +14 -0
- package/mods/auto-mode/hooks/build-prompt-context.claude-check.ts +94 -0
- package/mods/auto-mode/hooks/build-prompt-context.ts +37 -0
- package/mods/auto-mode/hooks/hooks.json +1 -0
- package/mods/auto-mode/hooks/parse-decision.ts +51 -0
- package/mods/auto-mode/hooks/register.claude-check.ts +1016 -0
- package/mods/auto-mode/hooks/register.ts +154 -0
- package/mods/auto-mode/hooks/testing.d.ts +37 -0
- package/mods/auto-mode/hooks/types.ts +117 -0
- package/mods/auto-mode/tsconfig.json +12 -0
- package/package.json +7 -4
- package/policy/decision.md +31 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { parseArgs } from "node:util";
|
|
2
|
+
import { c as SETTINGS_PATHS, d as renderVerdict, f as parsePayload, g as loadConfig, i as loadClaudeRules, l as SETUP_NOTES, n as classifyWithModel, o as loadPolicy, s as EVENT_NOTES, t as classifyLocally, u as buildHookConfig, v as resolveConfigPath } from "./classify-locally-BcyBikYr.js";
|
|
4
3
|
import { text } from "node:stream/consumers";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
//#region src/config/build-jev-only-config.ts
|
|
6
|
+
function buildJevOnlyConfig(config) {
|
|
7
|
+
if (config.provider.protocol !== "system-one") return null;
|
|
8
|
+
return {
|
|
9
|
+
...config,
|
|
10
|
+
provider: {
|
|
11
|
+
...config.provider,
|
|
12
|
+
timeoutMs: Math.min(config.provider.timeoutMs, 5e3)
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
5
17
|
//#region src/cli.ts
|
|
6
18
|
const USAGE = `auto-mode — a permission classifier that runs as a hook
|
|
7
19
|
|
|
@@ -13,13 +25,16 @@ Usage:
|
|
|
13
25
|
Options:
|
|
14
26
|
--classifier <path> Use this framework file instead of the shipped one
|
|
15
27
|
--rules <path> Use this rule list instead of the shipped one
|
|
28
|
+
--event <event> With init: pre-tool-use, the default, or permission-request
|
|
16
29
|
--explain With run: also write the reasoning to stderr
|
|
17
30
|
--local-only With run: skip the model tier
|
|
31
|
+
--jev-only With run: require Jev and cap its API timeout at 5 seconds
|
|
32
|
+
--evaluation-deadline <unix-ms> With --jev-only: share the helper and API deadline
|
|
18
33
|
`;
|
|
19
34
|
function readStdin() {
|
|
20
35
|
return text(process.stdin);
|
|
21
36
|
}
|
|
22
|
-
async function run(explain, localOnly) {
|
|
37
|
+
async function run(explain, localOnly, jevOnly, deadline) {
|
|
23
38
|
const raw = await readStdin();
|
|
24
39
|
let body;
|
|
25
40
|
try {
|
|
@@ -29,21 +44,60 @@ async function run(explain, localOnly) {
|
|
|
29
44
|
}
|
|
30
45
|
const payload = parsePayload(body);
|
|
31
46
|
if (payload === null) return printNote(explain, "not a tool gate this hook judges, so no verdict");
|
|
32
|
-
const
|
|
47
|
+
const loaded = await loadConfig();
|
|
48
|
+
const config = jevOnly ? buildJevOnlyConfig(loaded) : loaded;
|
|
49
|
+
if (config === null) return printNote(true, "Jev-only evaluation requires system-one; deferring to the harness");
|
|
50
|
+
let configured = null;
|
|
51
|
+
if (config.provider.protocol === "system-one") try {
|
|
52
|
+
configured = await loadClaudeRules(config.claudeSettingsPath);
|
|
53
|
+
} catch {
|
|
54
|
+
if (config.onFailure === "deny") writeVerdict(payload.event, {
|
|
55
|
+
kind: "deny",
|
|
56
|
+
rule: "Classifier Unavailable",
|
|
57
|
+
reason: "Claude settings unreadable"
|
|
58
|
+
});
|
|
59
|
+
return printNote(true, "Claude settings unreadable; classifier unavailable");
|
|
60
|
+
}
|
|
61
|
+
const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(payload);
|
|
33
62
|
if (local.kind === "allow") {
|
|
34
|
-
|
|
63
|
+
writeVerdict(payload.event, { kind: "allow" });
|
|
35
64
|
return printNote(explain, `allowed by ${local.exception} (${payload.harness}, local)`);
|
|
36
65
|
}
|
|
37
66
|
if (localOnly) return printNote(explain, `${payload.toolName} needs the model tier, which --local-only skipped`);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
const deadlineAt = deadline === void 0 ? void 0 : Number(deadline);
|
|
68
|
+
if (deadlineAt !== void 0 && (!jevOnly || !Number.isSafeInteger(deadlineAt) || deadlineAt <= 0)) return printNote(true, "invalid Jev evaluation deadline; deferring to the harness");
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const stopEvaluation = () => {
|
|
71
|
+
controller.abort();
|
|
72
|
+
};
|
|
73
|
+
if (jevOnly) {
|
|
74
|
+
process.on("SIGTERM", stopEvaluation);
|
|
75
|
+
process.on("SIGINT", stopEvaluation);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const outcome = await classifyWithModel(payload, config, {
|
|
79
|
+
deadlineAt,
|
|
80
|
+
signal: controller.signal
|
|
81
|
+
});
|
|
82
|
+
if (outcome.verdict !== null) writeVerdict(payload.event, outcome.verdict);
|
|
83
|
+
return printNote(explain || outcome.unavailable === true, outcome.note);
|
|
84
|
+
} finally {
|
|
85
|
+
process.off("SIGTERM", stopEvaluation);
|
|
86
|
+
process.off("SIGINT", stopEvaluation);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function writeVerdict(event, verdict) {
|
|
90
|
+
const rendered = renderVerdict(event, verdict);
|
|
91
|
+
if (rendered !== null) process.stdout.write(rendered);
|
|
42
92
|
}
|
|
43
93
|
function printNote(explain, message) {
|
|
44
94
|
if (explain) process.stderr.write(`auto-mode: ${message}\n`);
|
|
45
95
|
return 0;
|
|
46
96
|
}
|
|
97
|
+
const HOOK_EVENTS = {
|
|
98
|
+
"pre-tool-use": "PreToolUse",
|
|
99
|
+
"permission-request": "PermissionRequest"
|
|
100
|
+
};
|
|
47
101
|
async function main(argv) {
|
|
48
102
|
const args = parseArgs({
|
|
49
103
|
args: [...argv],
|
|
@@ -51,8 +105,11 @@ async function main(argv) {
|
|
|
51
105
|
options: {
|
|
52
106
|
classifier: { type: "string" },
|
|
53
107
|
rules: { type: "string" },
|
|
108
|
+
event: { type: "string" },
|
|
54
109
|
explain: { type: "boolean" },
|
|
55
110
|
"local-only": { type: "boolean" },
|
|
111
|
+
"jev-only": { type: "boolean" },
|
|
112
|
+
"evaluation-deadline": { type: "string" },
|
|
56
113
|
help: {
|
|
57
114
|
type: "boolean",
|
|
58
115
|
short: "h"
|
|
@@ -64,24 +121,40 @@ async function main(argv) {
|
|
|
64
121
|
process.stdout.write(USAGE);
|
|
65
122
|
return 0;
|
|
66
123
|
}
|
|
67
|
-
if (command === "run")
|
|
124
|
+
if (command === "run") try {
|
|
125
|
+
return await run(args.values.explain === true, args.values["local-only"] === true, args.values["jev-only"] === true, args.values["evaluation-deadline"]);
|
|
126
|
+
} catch {
|
|
127
|
+
process.stderr.write("auto-mode: configuration unreadable; deferring to the harness\n");
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
68
130
|
if (command === "init") {
|
|
69
131
|
const [, harness] = args.positionals;
|
|
70
132
|
if (harness !== "claude" && harness !== "codex" && harness !== "muse") {
|
|
71
133
|
process.stderr.write("auto-mode init: name a harness — claude, codex or muse\n");
|
|
72
134
|
return 2;
|
|
73
135
|
}
|
|
136
|
+
const event = HOOK_EVENTS[args.values.event ?? "pre-tool-use"];
|
|
137
|
+
if (event === void 0) {
|
|
138
|
+
process.stderr.write("auto-mode init: --event takes pre-tool-use or permission-request\n");
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
if (event === "PermissionRequest" && harness !== "claude") {
|
|
142
|
+
process.stderr.write(`auto-mode init: only Claude Code sends PermissionRequest\n`);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
74
145
|
process.stdout.write(`# Add this to ${SETTINGS_PATHS[harness]}\n`);
|
|
75
146
|
process.stdout.write(`# Configuration lives at ${resolveConfigPath()}\n`);
|
|
76
|
-
process.stdout.write(`${buildHookConfig(harness, `${process.execPath} ${process.argv[1] ?? "auto-mode"} run
|
|
77
|
-
for (const line of SETUP_NOTES[harness]) process.stdout.write(`# ${line}\n`);
|
|
147
|
+
process.stdout.write(`${buildHookConfig(harness, `${process.execPath} ${process.argv[1] ?? "auto-mode"} run`, event)}\n`);
|
|
148
|
+
for (const line of [...SETUP_NOTES[harness], ...EVENT_NOTES[event]]) process.stdout.write(`# ${line}\n`);
|
|
78
149
|
return 0;
|
|
79
150
|
}
|
|
80
151
|
if (command === "print-prompt") {
|
|
152
|
+
const config = await loadConfig();
|
|
153
|
+
const framework = config.provider.protocol === "system-one" ? "decision.md" : "classifier.md";
|
|
81
154
|
const prompt = await loadPolicy({
|
|
82
|
-
classifierPath: args.values.classifier,
|
|
83
|
-
rulesPath: args.values.rules
|
|
84
|
-
});
|
|
155
|
+
classifierPath: args.values.classifier ?? config.classifierPath,
|
|
156
|
+
rulesPath: args.values.rules ?? config.rulesPath
|
|
157
|
+
}, framework);
|
|
85
158
|
process.stdout.write(`${prompt}\n`);
|
|
86
159
|
return 0;
|
|
87
160
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
//#region src/config/types.d.ts
|
|
2
|
+
interface EvaluationOptions {
|
|
3
|
+
readonly deadlineAt?: number | undefined;
|
|
4
|
+
readonly signal?: Readonly<AbortSignal> | undefined;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
1
7
|
//#region src/config/config.d.ts
|
|
2
8
|
interface ProviderConfig {
|
|
9
|
+
readonly protocol?: 'messages' | 'system-one';
|
|
3
10
|
readonly baseURL: string;
|
|
4
11
|
readonly model: string;
|
|
5
12
|
readonly apiKeyEnv?: string | undefined;
|
|
@@ -14,16 +21,35 @@ interface Config {
|
|
|
14
21
|
readonly rulesPath?: string | undefined;
|
|
15
22
|
readonly transcriptEntries: number;
|
|
16
23
|
readonly onFailure: 'defer' | 'deny';
|
|
24
|
+
readonly claudeSettingsPath?: string | null | undefined;
|
|
25
|
+
readonly minConfidence?: number | undefined;
|
|
17
26
|
}
|
|
18
27
|
export declare const DEFAULT_CONFIG: Config;
|
|
19
28
|
export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
|
|
20
29
|
export declare function resolveConfigPath(): string;
|
|
21
30
|
export declare function loadConfig(path?: string): Promise<Config>;
|
|
22
|
-
export declare function resolveApiKey(provider: ProviderConfig): Promise<string | null>;
|
|
31
|
+
export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
|
|
23
32
|
//#endregion
|
|
24
33
|
//#region src/harness/types.d.ts
|
|
25
34
|
type Harness = 'claude' | 'codex' | 'muse';
|
|
26
35
|
type HookEvent = 'PreToolUse' | 'PermissionRequest';
|
|
36
|
+
interface UserTask {
|
|
37
|
+
readonly text: string;
|
|
38
|
+
readonly origin: 'composer' | 'bridge' | 'sdk';
|
|
39
|
+
}
|
|
40
|
+
interface DecisionContext {
|
|
41
|
+
readonly agentID: string | null;
|
|
42
|
+
readonly originalUserTask: UserTask | null;
|
|
43
|
+
readonly delegatedTask: {
|
|
44
|
+
readonly text: string;
|
|
45
|
+
readonly origin: 'agent.spawn';
|
|
46
|
+
} | null;
|
|
47
|
+
readonly lastDirectUserMessage: UserTask | null;
|
|
48
|
+
readonly omittedTaskContext: readonly {
|
|
49
|
+
readonly field: 'originalUserTask' | 'delegatedTask';
|
|
50
|
+
readonly reason: 'unavailable' | 'budget';
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
27
53
|
interface HookPayload {
|
|
28
54
|
readonly harness: Harness;
|
|
29
55
|
readonly event: HookEvent;
|
|
@@ -32,6 +58,7 @@ interface HookPayload {
|
|
|
32
58
|
readonly toolName: string;
|
|
33
59
|
readonly toolInput: Readonly<Record<string, unknown>>;
|
|
34
60
|
readonly transcriptPath?: string | undefined;
|
|
61
|
+
readonly decisionContext?: DecisionContext | undefined;
|
|
35
62
|
readonly raw: Readonly<Record<string, unknown>>;
|
|
36
63
|
}
|
|
37
64
|
type Verdict = {
|
|
@@ -51,18 +78,19 @@ export declare function detectHarness(payload: Readonly<Record<string, unknown>>
|
|
|
51
78
|
export declare function parsePayload(body: unknown): HookPayload | null;
|
|
52
79
|
//#endregion
|
|
53
80
|
//#region src/harness/render-verdict.d.ts
|
|
54
|
-
export declare function renderVerdict(event: HookEvent, verdict: Verdict): string;
|
|
81
|
+
export declare function renderVerdict(event: HookEvent, verdict: Verdict): string | null;
|
|
55
82
|
//#endregion
|
|
56
83
|
//#region src/install/hook-config.d.ts
|
|
57
|
-
export declare function buildHookConfig(harness: Harness, command: string): string;
|
|
84
|
+
export declare function buildHookConfig(harness: Harness, command: string, event?: HookEvent): string;
|
|
58
85
|
export declare const SETTINGS_PATHS: Readonly<Record<Harness, string>>;
|
|
59
86
|
//#endregion
|
|
60
87
|
//#region src/model/classify-with-model.d.ts
|
|
61
88
|
interface ModelOutcome {
|
|
62
89
|
readonly verdict: Verdict | null;
|
|
63
90
|
readonly note: string;
|
|
91
|
+
readonly unavailable?: boolean;
|
|
64
92
|
}
|
|
65
|
-
export declare function classifyWithModel(payload: HookPayload, config: Config): Promise<ModelOutcome>;
|
|
93
|
+
export declare function classifyWithModel(payload: HookPayload, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
|
|
66
94
|
//#endregion
|
|
67
95
|
//#region src/model/parse-verdict.d.ts
|
|
68
96
|
export declare function parseModelVerdict(text: string): Verdict;
|
|
@@ -72,7 +100,7 @@ interface PolicyPaths {
|
|
|
72
100
|
readonly classifierPath?: string | undefined;
|
|
73
101
|
readonly rulesPath?: string | undefined;
|
|
74
102
|
}
|
|
75
|
-
export declare function loadPolicy(paths?: PolicyPaths): Promise<string>;
|
|
103
|
+
export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md'): Promise<string>;
|
|
76
104
|
//#endregion
|
|
77
105
|
//#region src/rules/classify-locally.d.ts
|
|
78
106
|
type LocalVerdict = {
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as resolveApiKey, a as readTranscript, c as SETTINGS_PATHS, d as renderVerdict, f as parsePayload, g as loadConfig, h as PRESETS, m as DEFAULT_CONFIG, n as classifyWithModel, o as loadPolicy, p as detectHarness, r as parseModelVerdict, t as classifyLocally, u as buildHookConfig, v as resolveConfigPath } from "./classify-locally-BcyBikYr.js";
|
|
2
2
|
export { DEFAULT_CONFIG, PRESETS, SETTINGS_PATHS, buildHookConfig, classifyLocally, classifyWithModel, detectHarness, loadConfig, loadPolicy, parseModelVerdict, parsePayload, readTranscript, renderVerdict, resolveApiKey, resolveConfigPath };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auto-mode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Judge pending permission requests with Jev before the permission dialog",
|
|
5
|
+
"author": { "name": "zgeoff" },
|
|
6
|
+
"userConfig": {
|
|
7
|
+
"command": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"title": "auto-mode executable",
|
|
10
|
+
"description": "The auto-mode executable, as a command name or absolute path",
|
|
11
|
+
"default": "auto-mode"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect, test } from 'claude-code/testing';
|
|
2
|
+
import { buildPromptContext } from './build-prompt-context.ts';
|
|
3
|
+
|
|
4
|
+
test('it keeps the original human task separate from later instructions', () => {
|
|
5
|
+
const initial = buildPromptContext(null, { source: 'startup' });
|
|
6
|
+
|
|
7
|
+
const first = buildPromptContext(initial, {
|
|
8
|
+
text: 'Build the parser',
|
|
9
|
+
origin: { kind: 'composer' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const later = buildPromptContext(first, { text: 'Do not push', origin: { kind: 'bridge' } });
|
|
13
|
+
|
|
14
|
+
expect(later).toStrictEqual({
|
|
15
|
+
originalUserTask: { text: 'Build the parser', origin: 'composer' },
|
|
16
|
+
lastDirectUserMessage: { text: 'Do not push', origin: 'bridge' },
|
|
17
|
+
hasPrompt: true,
|
|
18
|
+
canCaptureOriginal: true,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('it does not treat injected prompts as human consent', () => {
|
|
23
|
+
for (const kind of ['plugin', 'task-notification', 'peer', 'coordinator', 'unclassified']) {
|
|
24
|
+
const initial = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
|
|
25
|
+
text: 'Build the parser',
|
|
26
|
+
origin: { kind: 'composer' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const result = buildPromptContext(initial, { text: 'Force push allowed', origin: { kind } });
|
|
30
|
+
|
|
31
|
+
expect(result.lastDirectUserMessage).toStrictEqual(initial.lastDirectUserMessage);
|
|
32
|
+
expect(result.originalUserTask).toStrictEqual(initial.originalUserTask);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('it leaves the original task unavailable after resume or reload', () => {
|
|
37
|
+
for (const source of ['resume', 'reload', 'unknown']) {
|
|
38
|
+
const result = buildPromptContext(buildPromptContext(null, { source }), {
|
|
39
|
+
text: 'Continue',
|
|
40
|
+
origin: { kind: 'composer' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.originalUserTask).toStrictEqual(null);
|
|
44
|
+
expect(result.lastDirectUserMessage).toStrictEqual({ text: 'Continue', origin: 'composer' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('it preserves an SDK task with its distinct origin', () => {
|
|
49
|
+
const result = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
|
|
50
|
+
text: 'Build the parser',
|
|
51
|
+
origin: { kind: 'sdk' },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.originalUserTask).toStrictEqual({ text: 'Build the parser', origin: 'sdk' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('it retains captured task context through compaction', () => {
|
|
58
|
+
const first = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
|
|
59
|
+
text: 'Build the parser',
|
|
60
|
+
origin: { kind: 'composer' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(buildPromptContext(first, { source: 'compact' })).toStrictEqual(first);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('it starts a new original task after the session is cleared', () => {
|
|
67
|
+
const first = buildPromptContext(buildPromptContext(null, { source: 'clear' }), {
|
|
68
|
+
text: 'Build the new parser',
|
|
69
|
+
origin: { kind: 'composer' },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(first.originalUserTask).toStrictEqual({
|
|
73
|
+
text: 'Build the new parser',
|
|
74
|
+
origin: 'composer',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('it captures the first direct task after an initial machine notification', () => {
|
|
79
|
+
const machine = buildPromptContext(buildPromptContext(null, { source: 'startup' }), {
|
|
80
|
+
text: 'Machine notification',
|
|
81
|
+
origin: { kind: 'task-notification' },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(machine.originalUserTask).toStrictEqual(null);
|
|
85
|
+
expect(machine.lastDirectUserMessage).toStrictEqual(null);
|
|
86
|
+
|
|
87
|
+
const human = buildPromptContext(machine, {
|
|
88
|
+
text: 'Build the parser',
|
|
89
|
+
origin: { kind: 'composer' },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(human.originalUserTask).toStrictEqual({ text: 'Build the parser', origin: 'composer' });
|
|
93
|
+
expect(human.lastDirectUserMessage).toStrictEqual(human.originalUserTask);
|
|
94
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PromptContext, UserTask } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function buildPromptContext(
|
|
4
|
+
previous: PromptContext | null,
|
|
5
|
+
input:
|
|
6
|
+
| { readonly source: string }
|
|
7
|
+
| { readonly text: string; readonly origin: { readonly kind: string } },
|
|
8
|
+
): PromptContext {
|
|
9
|
+
if ('source' in input) {
|
|
10
|
+
if (input.source === 'compact' && previous !== null) {
|
|
11
|
+
return previous;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
originalUserTask: null,
|
|
16
|
+
lastDirectUserMessage: null,
|
|
17
|
+
hasPrompt: false,
|
|
18
|
+
canCaptureOriginal: input.source === 'startup' || input.source === 'clear',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const origin = input.origin.kind;
|
|
23
|
+
|
|
24
|
+
const lastDirectUserMessage: UserTask | null =
|
|
25
|
+
origin === 'composer' || origin === 'bridge' || origin === 'sdk'
|
|
26
|
+
? { text: input.text, origin }
|
|
27
|
+
: (previous?.lastDirectUserMessage ?? null);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
originalUserTask:
|
|
31
|
+
previous?.originalUserTask ??
|
|
32
|
+
(previous?.canCaptureOriginal === true ? lastDirectUserMessage : null),
|
|
33
|
+
lastDirectUserMessage,
|
|
34
|
+
hasPrompt: true,
|
|
35
|
+
canCaptureOriginal: previous?.canCaptureOriginal ?? false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.ts"] }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface Decision {
|
|
2
|
+
readonly decision: 'allow' | 'deny';
|
|
3
|
+
readonly reason?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function parseDecision(stdout: string): Decision | null {
|
|
7
|
+
let body: unknown;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
body = JSON.parse(stdout);
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof body !== 'object' || body === null || !('hookSpecificOutput' in body)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const output = body.hookSpecificOutput;
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
typeof output !== 'object' ||
|
|
23
|
+
output === null ||
|
|
24
|
+
!('hookEventName' in output) ||
|
|
25
|
+
output.hookEventName !== 'PermissionRequest' ||
|
|
26
|
+
!('decision' in output)
|
|
27
|
+
) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const value = output.decision;
|
|
32
|
+
|
|
33
|
+
if (typeof value !== 'object' || value === null || !('behavior' in value)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value.behavior === 'allow') {
|
|
38
|
+
return { decision: 'allow' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
value.behavior === 'deny' &&
|
|
43
|
+
'message' in value &&
|
|
44
|
+
typeof value.message === 'string' &&
|
|
45
|
+
value.message.trim() !== ''
|
|
46
|
+
) {
|
|
47
|
+
return { decision: 'deny', reason: value.message };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|