offrouter-adapter-claude 0.2.0 → 0.2.2
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/{src/hooks/pre-tool-use.ts → dist/hooks/pre-tool-use.d.ts} +4 -8
- package/dist/hooks/pre-tool-use.d.ts.map +1 -0
- package/dist/hooks/pre-tool-use.js +14 -0
- package/dist/hooks/pre-tool-use.js.map +1 -0
- package/dist/hooks/user-prompt-submit.d.ts +56 -0
- package/dist/hooks/user-prompt-submit.d.ts.map +1 -0
- package/dist/hooks/user-prompt-submit.js +177 -0
- package/dist/hooks/user-prompt-submit.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +116 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +446 -0
- package/dist/install.js.map +1 -0
- package/dist/rollback.d.ts +23 -0
- package/dist/rollback.d.ts.map +1 -0
- package/dist/rollback.js +245 -0
- package/dist/rollback.js.map +1 -0
- package/package.json +9 -4
- package/src/hooks/user-prompt-submit.test.ts +0 -222
- package/src/hooks/user-prompt-submit.ts +0 -262
- package/src/index.ts +0 -49
- package/src/install.test.ts +0 -589
- package/src/install.ts +0 -595
- package/src/plugin.test.ts +0 -150
- package/src/rollback.ts +0 -329
- package/tsconfig.json +0 -10
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code UserPromptSubmit hook adapter.
|
|
3
|
-
*
|
|
4
|
-
* V1 boundary: do not claim to intercept or replace Claude Code's primary
|
|
5
|
-
* model. This handler injects audit-safe routing guidance for OffRouter-
|
|
6
|
-
* managed delegation (or blocks profiles OffRouter refuses to serve).
|
|
7
|
-
*/
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
ROUTE_PROTOCOL_VERSION,
|
|
11
|
-
routeRequest,
|
|
12
|
-
type PolicyConfig,
|
|
13
|
-
type ProviderCandidate,
|
|
14
|
-
type RouteDecision,
|
|
15
|
-
type RouteConstraints,
|
|
16
|
-
type RouteRequest,
|
|
17
|
-
type RouteTask,
|
|
18
|
-
} from "offrouter-core";
|
|
19
|
-
import { z } from "zod";
|
|
20
|
-
|
|
21
|
-
const HONESTY_SENTENCE =
|
|
22
|
-
"Claude Code's primary model is unchanged in V1; OffRouter routes delegated work.";
|
|
23
|
-
|
|
24
|
-
type InferredRouteTask = Pick<
|
|
25
|
-
RouteTask,
|
|
26
|
-
"kind" | "risk" | "requiresTools" | "requiresImages" | "requiresJson"
|
|
27
|
-
>;
|
|
28
|
-
|
|
29
|
-
const ClaudeUserPromptSubmitSchema = z
|
|
30
|
-
.object({
|
|
31
|
-
hook_event_name: z.literal("UserPromptSubmit"),
|
|
32
|
-
prompt: z.string().min(1),
|
|
33
|
-
cwd: z.string().optional(),
|
|
34
|
-
session_id: z.string().optional(),
|
|
35
|
-
transcript_path: z.string().optional(),
|
|
36
|
-
permission_mode: z.string().optional(),
|
|
37
|
-
})
|
|
38
|
-
.passthrough();
|
|
39
|
-
|
|
40
|
-
export type ClaudeUserPromptSubmitPayload = z.infer<
|
|
41
|
-
typeof ClaudeUserPromptSubmitSchema
|
|
42
|
-
>;
|
|
43
|
-
|
|
44
|
-
export interface UserPromptSubmitOptions {
|
|
45
|
-
profile: string;
|
|
46
|
-
policy: PolicyConfig;
|
|
47
|
-
candidates: ProviderCandidate[];
|
|
48
|
-
trustedWorkspace?: boolean;
|
|
49
|
-
routeConstraints?: Partial<RouteConstraints>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface UserPromptSubmitHookOutput {
|
|
53
|
-
decision?: "block";
|
|
54
|
-
reason?: string;
|
|
55
|
-
hookSpecificOutput?: {
|
|
56
|
-
hookEventName: "UserPromptSubmit";
|
|
57
|
-
additionalContext: string;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface HookRunResult {
|
|
62
|
-
exitCode: number;
|
|
63
|
-
stdout: string;
|
|
64
|
-
stderr: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function promptDigest(prompt: string): string {
|
|
68
|
-
return `sha256:${createHash("sha256").update(prompt).digest("hex")}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function auditSafePromptPreview(digest: string, prompt: string): string {
|
|
72
|
-
return `redacted:${digest.replace("sha256:", "").slice(0, 12)} chars=${prompt.length}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function inferTask(prompt: string): InferredRouteTask {
|
|
76
|
-
const normalized = prompt.toLowerCase();
|
|
77
|
-
let kind: RouteTask["kind"] = "other";
|
|
78
|
-
if (/\b(review|pr|diff|audit)\b/.test(normalized)) {
|
|
79
|
-
kind = "review";
|
|
80
|
-
} else if (/\b(plan|spec|roadmap|design)\b/.test(normalized)) {
|
|
81
|
-
kind = "plan";
|
|
82
|
-
} else if (/\b(implement|code|fix|test|refactor|debug)\b/.test(normalized)) {
|
|
83
|
-
kind = "code";
|
|
84
|
-
} else if (/\b(research|latest|look up|web)\b/.test(normalized)) {
|
|
85
|
-
kind = "research";
|
|
86
|
-
} else if (/\b(image|screenshot|diagram|draw)\b/.test(normalized)) {
|
|
87
|
-
kind = "image";
|
|
88
|
-
} else if (/\b(explain|summari[sz]e)\b/.test(normalized)) {
|
|
89
|
-
kind = "explain";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let risk: RouteTask["risk"] = "low";
|
|
93
|
-
if (
|
|
94
|
-
/\b(production|deploy|delete|migrate|credential|secret|payment|security|auth|database|data loss|rm -rf)\b/.test(
|
|
95
|
-
normalized,
|
|
96
|
-
)
|
|
97
|
-
) {
|
|
98
|
-
risk = "high";
|
|
99
|
-
} else if (
|
|
100
|
-
/\b(refactor|install|write|commit|merge|api key|profile|hook)\b/.test(
|
|
101
|
-
normalized,
|
|
102
|
-
)
|
|
103
|
-
) {
|
|
104
|
-
risk = "medium";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
kind,
|
|
109
|
-
risk,
|
|
110
|
-
requiresTools:
|
|
111
|
-
kind === "code" ||
|
|
112
|
-
kind === "review" ||
|
|
113
|
-
/\b(run|test|build|edit|write|git|shell|command)\b/.test(normalized),
|
|
114
|
-
requiresImages: kind === "image",
|
|
115
|
-
requiresJson: /\b(json|schema|structured)\b/.test(normalized),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function buildRouteRequest(
|
|
120
|
-
payload: ClaudeUserPromptSubmitPayload,
|
|
121
|
-
options: UserPromptSubmitOptions,
|
|
122
|
-
): RouteRequest {
|
|
123
|
-
const prompt = payload.prompt;
|
|
124
|
-
const digest = promptDigest(prompt);
|
|
125
|
-
const inferredTask = inferTask(prompt);
|
|
126
|
-
return {
|
|
127
|
-
protocolVersion: ROUTE_PROTOCOL_VERSION,
|
|
128
|
-
requestId: `claude_hook_${digest.replace("sha256:", "").slice(0, 12)}`,
|
|
129
|
-
harness: {
|
|
130
|
-
kind: "claude",
|
|
131
|
-
profile: options.profile,
|
|
132
|
-
},
|
|
133
|
-
task: {
|
|
134
|
-
...inferredTask,
|
|
135
|
-
promptPreview: auditSafePromptPreview(digest, prompt),
|
|
136
|
-
promptDigest: digest,
|
|
137
|
-
},
|
|
138
|
-
workspace: {
|
|
139
|
-
cwd: payload.cwd ?? process.cwd(),
|
|
140
|
-
trusted: options.trustedWorkspace === true,
|
|
141
|
-
},
|
|
142
|
-
constraints: {
|
|
143
|
-
subscriptionFirst: options.routeConstraints?.subscriptionFirst ?? true,
|
|
144
|
-
allowApiKeyFallback:
|
|
145
|
-
options.routeConstraints?.allowApiKeyFallback ?? false,
|
|
146
|
-
maxCostUsd: options.routeConstraints?.maxCostUsd,
|
|
147
|
-
localOnly: options.routeConstraints?.localOnly,
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function formatAllowedContext(decision: RouteDecision): string {
|
|
153
|
-
const lines = [HONESTY_SENTENCE];
|
|
154
|
-
|
|
155
|
-
if (decision.route) {
|
|
156
|
-
lines.push(
|
|
157
|
-
`OffRouter recommended delegated route ${decision.route.provider}/${decision.route.model} (${decision.route.authTier}).`,
|
|
158
|
-
);
|
|
159
|
-
lines.push(`Reason: ${decision.reason}. ${decision.explanation}`.trim());
|
|
160
|
-
} else if (decision.needsConfiguration) {
|
|
161
|
-
lines.push(
|
|
162
|
-
"OffRouter needs configuration before it can select a delegated route.",
|
|
163
|
-
);
|
|
164
|
-
lines.push(decision.explanation);
|
|
165
|
-
} else if (decision.blocked) {
|
|
166
|
-
// Profile denials are surfaced as decision:block by the caller; other
|
|
167
|
-
// blocked outcomes still provide context rather than claiming primary
|
|
168
|
-
// model substitution.
|
|
169
|
-
lines.push(
|
|
170
|
-
`OffRouter could not select a delegated route (${decision.reason}).`,
|
|
171
|
-
);
|
|
172
|
-
lines.push(decision.explanation);
|
|
173
|
-
} else {
|
|
174
|
-
lines.push("OffRouter could not select a delegated route.");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Never include raw prompt text; decision fields are already audit-safe.
|
|
178
|
-
return lines.filter(Boolean).join("\n");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function invalidHookOutput(detail?: string): UserPromptSubmitHookOutput {
|
|
182
|
-
const reason = detail
|
|
183
|
-
? `Invalid Claude hook JSON: ${detail}`
|
|
184
|
-
: "Invalid Claude hook JSON";
|
|
185
|
-
return {
|
|
186
|
-
decision: "block",
|
|
187
|
-
reason,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isProfileDenial(decision: RouteDecision): boolean {
|
|
192
|
-
return (
|
|
193
|
-
decision.blocked === true &&
|
|
194
|
-
(decision.policyDenials ?? []).some(
|
|
195
|
-
(d) =>
|
|
196
|
-
d.code === "work_profile_denied" ||
|
|
197
|
-
d.code === "profile_not_allowlisted",
|
|
198
|
-
)
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Pure UserPromptSubmit handler used by tests and the stdio runner.
|
|
204
|
-
*/
|
|
205
|
-
export function handleUserPromptSubmit(
|
|
206
|
-
input: unknown,
|
|
207
|
-
options: UserPromptSubmitOptions,
|
|
208
|
-
): UserPromptSubmitHookOutput {
|
|
209
|
-
const parsed = ClaudeUserPromptSubmitSchema.safeParse(input);
|
|
210
|
-
if (!parsed.success) {
|
|
211
|
-
return invalidHookOutput("payload did not match UserPromptSubmit schema");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const request = buildRouteRequest(parsed.data, options);
|
|
215
|
-
const decision = routeRequest(request, options.candidates, options.policy);
|
|
216
|
-
|
|
217
|
-
// Denied / disallowed profiles block closed at the hook boundary.
|
|
218
|
-
if (isProfileDenial(decision)) {
|
|
219
|
-
const denial = (decision.policyDenials ?? []).find(
|
|
220
|
-
(d) =>
|
|
221
|
-
d.code === "work_profile_denied" ||
|
|
222
|
-
d.code === "profile_not_allowlisted",
|
|
223
|
-
);
|
|
224
|
-
return {
|
|
225
|
-
decision: "block",
|
|
226
|
-
reason:
|
|
227
|
-
denial?.message ??
|
|
228
|
-
`Profile ${options.profile} is denied by OffRouter policy.`,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
hookSpecificOutput: {
|
|
234
|
-
hookEventName: "UserPromptSubmit",
|
|
235
|
-
additionalContext: formatAllowedContext(decision),
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Stdio-style runner: parse stdin JSON, handle, emit a single JSON object on
|
|
242
|
-
* stdout. Always exit 0 so Claude receives structured block/allow decisions.
|
|
243
|
-
*/
|
|
244
|
-
export async function runUserPromptSubmitHook(
|
|
245
|
-
stdin: string,
|
|
246
|
-
options: UserPromptSubmitOptions,
|
|
247
|
-
): Promise<HookRunResult> {
|
|
248
|
-
let output: UserPromptSubmitHookOutput;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const raw: unknown = JSON.parse(stdin);
|
|
252
|
-
output = handleUserPromptSubmit(raw, options);
|
|
253
|
-
} catch {
|
|
254
|
-
output = invalidHookOutput();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
exitCode: 0,
|
|
259
|
-
stdout: JSON.stringify(output),
|
|
260
|
-
stderr: "",
|
|
261
|
-
};
|
|
262
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* offrouter-adapter-claude public surface.
|
|
3
|
-
* Hook handlers for Claude Code personal profiles.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const ADAPTER_CLAUDE_PACKAGE = "offrouter-adapter-claude" as const;
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
handleUserPromptSubmit,
|
|
10
|
-
runUserPromptSubmitHook,
|
|
11
|
-
} from "./hooks/user-prompt-submit.js";
|
|
12
|
-
export type {
|
|
13
|
-
ClaudeUserPromptSubmitPayload,
|
|
14
|
-
HookRunResult,
|
|
15
|
-
UserPromptSubmitHookOutput,
|
|
16
|
-
UserPromptSubmitOptions,
|
|
17
|
-
} from "./hooks/user-prompt-submit.js";
|
|
18
|
-
|
|
19
|
-
export { handlePreToolUse } from "./hooks/pre-tool-use.js";
|
|
20
|
-
export type { PreToolUseHookOutput } from "./hooks/pre-tool-use.js";
|
|
21
|
-
|
|
22
|
-
export {
|
|
23
|
-
CLAUDE_INSTALL_STATE_REL,
|
|
24
|
-
ClaudeInstallStateSchema,
|
|
25
|
-
defaultPluginSourceDir,
|
|
26
|
-
evaluateInstallProfilePolicy,
|
|
27
|
-
formatInstallDiffText,
|
|
28
|
-
installClaudeProfile,
|
|
29
|
-
INSTALLED_PLUGIN_REL,
|
|
30
|
-
isClaudeHarnessProfile,
|
|
31
|
-
isSafeProfileId,
|
|
32
|
-
isWorkLikeProfile,
|
|
33
|
-
listAllowlistedPersonalClaudeProfiles,
|
|
34
|
-
resolveClaudeProfileDir,
|
|
35
|
-
} from "./install.js";
|
|
36
|
-
export type {
|
|
37
|
-
ClaudeInstallOptions,
|
|
38
|
-
ClaudeInstallResult,
|
|
39
|
-
ClaudeInstallState,
|
|
40
|
-
InstallChangeKind,
|
|
41
|
-
InstallFileChange,
|
|
42
|
-
ResolveClaudeProfileDirOptions,
|
|
43
|
-
} from "./install.js";
|
|
44
|
-
|
|
45
|
-
export { rollbackClaudeProfile } from "./rollback.js";
|
|
46
|
-
export type {
|
|
47
|
-
ClaudeRollbackOptions,
|
|
48
|
-
ClaudeRollbackResult,
|
|
49
|
-
} from "./rollback.js";
|