offrouter-adapter-claude 0.2.1 → 0.3.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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Rollback for profile-scoped Claude install state.
3
+ * Restores overwritten plugin files and removes files created by OffRouter.
4
+ */
5
+ import { access, mkdir, readFile, rmdir, unlink, writeFile, } from "node:fs/promises";
6
+ import { constants } from "node:fs";
7
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import { homedir as osHomedir } from "node:os";
9
+ import { CLAUDE_INSTALL_STATE_REL, ClaudeInstallStateSchema, INSTALLED_PLUGIN_REL, resolveClaudeProfileDir, } from "./install.js";
10
+ async function pathExists(path) {
11
+ try {
12
+ await access(path, constants.F_OK);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ function changeSummary(kind, relativePath) {
20
+ switch (kind) {
21
+ case "create":
22
+ return `+ ${relativePath}`;
23
+ case "update":
24
+ return `~ ${relativePath}`;
25
+ case "unchanged":
26
+ return `= ${relativePath}`;
27
+ case "remove":
28
+ return `- ${relativePath}`;
29
+ default:
30
+ return `? ${relativePath}`;
31
+ }
32
+ }
33
+ async function loadState(statePath) {
34
+ if (!(await pathExists(statePath))) {
35
+ return {
36
+ ok: false,
37
+ errorCode: "no_install_state",
38
+ message: `No install state at ${statePath}. Nothing to uninstall.`,
39
+ };
40
+ }
41
+ try {
42
+ const raw = await readFile(statePath, "utf8");
43
+ const state = ClaudeInstallStateSchema.parse(JSON.parse(raw));
44
+ return { ok: true, state };
45
+ }
46
+ catch (err) {
47
+ const detail = err instanceof Error ? err.message : String(err);
48
+ return {
49
+ ok: false,
50
+ errorCode: "invalid_install_state",
51
+ message: `Invalid install state at ${statePath}: ${detail}`,
52
+ };
53
+ }
54
+ }
55
+ function isWindowsAbsolutePath(path) {
56
+ return /^[A-Za-z]:[\\/]/.test(path);
57
+ }
58
+ function isInsideOrSame(root, candidate) {
59
+ const rel = relative(root, candidate);
60
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
61
+ }
62
+ function resolveProfileRelativePath(profileDir, relativePath) {
63
+ if (relativePath.trim() === "" ||
64
+ relativePath.includes("\0") ||
65
+ isAbsolute(relativePath) ||
66
+ isWindowsAbsolutePath(relativePath)) {
67
+ return null;
68
+ }
69
+ const parts = relativePath.split(/[\\/]+/);
70
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
71
+ return null;
72
+ }
73
+ const root = resolve(profileDir);
74
+ const candidate = resolve(root, ...parts);
75
+ if (!isInsideOrSame(root, candidate)) {
76
+ return null;
77
+ }
78
+ return candidate;
79
+ }
80
+ function validateRollbackState(profileDir, state) {
81
+ if (state.pluginRootRelative !== INSTALLED_PLUGIN_REL) {
82
+ return {
83
+ ok: false,
84
+ message: `Invalid plugin root in install state: ${state.pluginRootRelative}`,
85
+ };
86
+ }
87
+ const pluginRoot = resolveProfileRelativePath(profileDir, state.pluginRootRelative);
88
+ if (!pluginRoot) {
89
+ return {
90
+ ok: false,
91
+ message: `Invalid plugin root in install state: ${state.pluginRootRelative}`,
92
+ };
93
+ }
94
+ const filePaths = new Map();
95
+ const pluginPrefix = `${INSTALLED_PLUGIN_REL}/`;
96
+ for (const relativePath of Object.keys(state.files)) {
97
+ if (!relativePath.startsWith(pluginPrefix)) {
98
+ return {
99
+ ok: false,
100
+ message: `Install state path is outside plugin root: ${relativePath}`,
101
+ };
102
+ }
103
+ const abs = resolveProfileRelativePath(profileDir, relativePath);
104
+ if (!abs) {
105
+ return {
106
+ ok: false,
107
+ message: `Invalid install state path: ${relativePath}`,
108
+ };
109
+ }
110
+ filePaths.set(relativePath, abs);
111
+ }
112
+ return { ok: true, filePaths, pluginRoot };
113
+ }
114
+ function collectParentDirsUntil(filePath, stopDir, out) {
115
+ const stop = resolve(stopDir);
116
+ let dir = dirname(filePath);
117
+ while (isInsideOrSame(stop, dir)) {
118
+ out.add(dir);
119
+ if (dir === stop)
120
+ break;
121
+ const parent = dirname(dir);
122
+ if (parent === dir)
123
+ break;
124
+ dir = parent;
125
+ }
126
+ }
127
+ /**
128
+ * Restore files tracked by claude-install-state.json for a profile.
129
+ */
130
+ export async function rollbackClaudeProfile(options) {
131
+ const { profile, dryRun = false, env = process.env, homedir = osHomedir, } = options;
132
+ const profileDir = options.profileDir ?? resolveClaudeProfileDir(profile, { env, homedir });
133
+ const statePath = join(profileDir, CLAUDE_INSTALL_STATE_REL);
134
+ const loaded = await loadState(statePath);
135
+ if (!loaded.ok) {
136
+ return {
137
+ ok: false,
138
+ dryRun,
139
+ profile,
140
+ profileDir,
141
+ changes: [],
142
+ message: loaded.message,
143
+ errorCode: loaded.errorCode,
144
+ };
145
+ }
146
+ const { state } = loaded;
147
+ const validated = validateRollbackState(profileDir, state);
148
+ if (!validated.ok) {
149
+ return {
150
+ ok: false,
151
+ dryRun,
152
+ profile,
153
+ profileDir,
154
+ changes: [],
155
+ message: validated.message,
156
+ errorCode: "invalid_install_state",
157
+ };
158
+ }
159
+ const changes = [];
160
+ const emptyDirsToRemove = new Set();
161
+ // Restore/remove plugin files in reverse path length so nested files go first.
162
+ const entries = Object.entries(state.files).sort((a, b) => b[0].length - a[0].length);
163
+ for (const [relativePath, fileState] of entries) {
164
+ const abs = validated.filePaths.get(relativePath);
165
+ const previous = fileState.previous;
166
+ if (previous === null) {
167
+ // File was created by install; remove it on rollback.
168
+ const existed = await pathExists(abs);
169
+ const kind = existed ? "remove" : "unchanged";
170
+ changes.push({
171
+ path: abs,
172
+ relativePath,
173
+ kind,
174
+ summary: changeSummary(kind, relativePath),
175
+ });
176
+ if (!dryRun && existed) {
177
+ collectParentDirsUntil(abs, validated.pluginRoot, emptyDirsToRemove);
178
+ await unlink(abs);
179
+ }
180
+ continue;
181
+ }
182
+ let current = null;
183
+ if (await pathExists(abs)) {
184
+ current = await readFile(abs, "utf8");
185
+ }
186
+ let kind;
187
+ if (current === previous) {
188
+ kind = "unchanged";
189
+ }
190
+ else if (current === null) {
191
+ kind = "create"; // restore missing prior content
192
+ }
193
+ else {
194
+ kind = "update";
195
+ }
196
+ changes.push({
197
+ path: abs,
198
+ relativePath,
199
+ kind,
200
+ summary: changeSummary(kind, relativePath),
201
+ });
202
+ if (!dryRun && kind !== "unchanged") {
203
+ await mkdir(dirname(abs), { recursive: true });
204
+ await writeFile(abs, previous, "utf8");
205
+ }
206
+ }
207
+ // Drop install state last
208
+ const stateChangeKind = "remove";
209
+ changes.push({
210
+ path: statePath,
211
+ relativePath: CLAUDE_INSTALL_STATE_REL,
212
+ kind: stateChangeKind,
213
+ summary: changeSummary(stateChangeKind, CLAUDE_INSTALL_STATE_REL),
214
+ });
215
+ if (!dryRun) {
216
+ await unlink(statePath);
217
+ for (const dir of [...emptyDirsToRemove].sort((a, b) => b.length - a.length)) {
218
+ try {
219
+ await rmdir(dir);
220
+ }
221
+ catch {
222
+ // ignore non-empty or already-removed directories
223
+ }
224
+ }
225
+ // Best-effort remove only the empty install-state directory.
226
+ try {
227
+ await rmdir(join(profileDir, ".offrouter"));
228
+ }
229
+ catch {
230
+ // ignore
231
+ }
232
+ }
233
+ const applied = changes.filter((c) => c.kind !== "unchanged");
234
+ return {
235
+ ok: true,
236
+ dryRun,
237
+ profile,
238
+ profileDir,
239
+ changes,
240
+ message: dryRun
241
+ ? `Dry run: would roll back ${applied.length} change(s) for profile ${profile}.`
242
+ : `Rolled back OffRouter Claude install for profile ${profile} (${applied.length} change(s)).`,
243
+ };
244
+ }
245
+ //# sourceMappingURL=rollback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rollback.js","sourceRoot":"","sources":["../src/rollback.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,MAAM,EACN,KAAK,EACL,QAAQ,EACR,KAAK,EACL,MAAM,EACN,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,GAIxB,MAAM,cAAc,CAAC;AAqBtB,KAAK,UAAU,UAAU,CAAC,IAAY;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,IAAuB,EAAE,YAAoB;IAClE,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,KAAK,YAAY,EAAE,CAAC;QAC7B,KAAK,QAAQ;YACX,OAAO,KAAK,YAAY,EAAE,CAAC;QAC7B,KAAK,WAAW;YACd,OAAO,KAAK,YAAY,EAAE,CAAC;QAC7B,KAAK,QAAQ;YACX,OAAO,KAAK,YAAY,EAAE,CAAC;QAC7B;YACE,OAAO,KAAK,YAAY,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,SAAiB;IAQxC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QACnC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,SAAS,EAAE,kBAAkB;YAC7B,OAAO,EAAE,uBAAuB,SAAS,yBAAyB;SACnE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC9C,MAAM,KAAK,GAAG,wBAAwB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChE,OAAO;YACL,EAAE,EAAE,KAAK;YACT,SAAS,EAAE,uBAAuB;YAClC,OAAO,EAAE,4BAA4B,SAAS,KAAK,MAAM,EAAE;SAC5D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB,CAAC,IAAY;IACzC,OAAO,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,SAAiB;IACrD,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACtC,OAAO,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,0BAA0B,CACjC,UAAkB,EAClB,YAAoB;IAEpB,IACE,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE;QAC1B,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3B,UAAU,CAAC,YAAY,CAAC;QACxB,qBAAqB,CAAC,YAAY,CAAC,EACnC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,qBAAqB,CAC5B,UAAkB,EAClB,KAAyB;IAIzB,IAAI,KAAK,CAAC,kBAAkB,KAAK,oBAAoB,EAAE,CAAC;QACtD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,yCAAyC,KAAK,CAAC,kBAAkB,EAAE;SAC7E,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,0BAA0B,CAC3C,UAAU,EACV,KAAK,CAAC,kBAAkB,CACzB,CAAC;IACF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,yCAAyC,KAAK,CAAC,kBAAkB,EAAE;SAC7E,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,MAAM,YAAY,GAAG,GAAG,oBAAoB,GAAG,CAAC;IAChD,KAAK,MAAM,YAAY,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC3C,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,8CAA8C,YAAY,EAAE;aACtE,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,0BAA0B,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACjE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,+BAA+B,YAAY,EAAE;aACvD,CAAC;QACJ,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAC7C,CAAC;AAED,SAAS,sBAAsB,CAC7B,QAAgB,EAChB,OAAe,EACf,GAAgB;IAEhB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5B,OAAO,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACb,IAAI,GAAG,KAAK,IAAI;YAAE,MAAM;QACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAA8B;IAE9B,MAAM,EACJ,OAAO,EACP,MAAM,GAAG,KAAK,EACd,GAAG,GAAG,OAAO,CAAC,GAAG,EACjB,OAAO,GAAG,SAAS,GACpB,GAAG,OAAO,CAAC;IACZ,MAAM,UAAU,GACd,OAAO,CAAC,UAAU,IAAI,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3E,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,wBAAwB,CAAC,CAAC;IAE7D,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM;YACN,OAAO;YACP,UAAU;YACV,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IACzB,MAAM,SAAS,GAAG,qBAAqB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAC3D,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAClB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM;YACN,OAAO;YACP,UAAU;YACV,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,SAAS,EAAE,uBAAuB;SACnC,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;IAE5C,+EAA+E;IAC/E,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAC9C,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CACpC,CAAC;IAEF,KAAK,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,IAAI,OAAO,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,CAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC;QAEpC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,sDAAsD;YACtD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;YACtC,MAAM,IAAI,GAAsB,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;YACjE,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,GAAG;gBACT,YAAY;gBACZ,IAAI;gBACJ,OAAO,EAAE,aAAa,CAAC,IAAI,EAAE,YAAY,CAAC;aAC3C,CAAC,CAAC;YACH,IAAI,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC;gBACvB,sBAAsB,CAAC,GAAG,EAAE,SAAS,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;gBACrE,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,MAAM,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,OAAO,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,IAAuB,CAAC;QAC5B,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,IAAI,GAAG,WAAW,CAAC;QACrB,CAAC;aAAM,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YAC5B,IAAI,GAAG,QAAQ,CAAC,CAAC,gCAAgC;QACnD,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,QAAQ,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,GAAG;YACT,YAAY;YACZ,IAAI;YACJ,OAAO,EAAE,aAAa,CAAC,IAAI,EAAE,YAAY,CAAC;SAC3C,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YACpC,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,MAAM,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,0BAA0B;IAC1B,MAAM,eAAe,GAAsB,QAAQ,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC;QACX,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,wBAAwB;QACtC,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,aAAa,CAAC,eAAe,EAAE,wBAAwB,CAAC;KAClE,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QACxB,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC,IAAI,CAC3C,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAC9B,EAAE,CAAC;YACF,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC;gBACP,kDAAkD;YACpD,CAAC;QACH,CAAC;QACD,6DAA6D;QAC7D,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;IAC9D,OAAO;QACL,EAAE,EAAE,IAAI;QACR,MAAM;QACN,OAAO;QACP,UAAU;QACV,OAAO;QACP,OAAO,EAAE,MAAM;YACb,CAAC,CAAC,4BAA4B,OAAO,CAAC,MAAM,0BAA0B,OAAO,GAAG;YAChF,CAAC,CAAC,oDAAoD,OAAO,KAAK,OAAO,CAAC,MAAM,cAAc;KACjG,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offrouter-adapter-claude",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -9,7 +9,12 @@
9
9
  "test": "vitest run --root ../.. packages/adapter-claude/src"
10
10
  },
11
11
  "dependencies": {
12
- "offrouter-core": "0.2.1",
12
+ "offrouter-core": "0.3.0",
13
13
  "zod": "^3.23.0"
14
- }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ]
15
20
  }
@@ -1,222 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { fileURLToPath } from "node:url";
3
- import { describe, expect, it } from "vitest";
4
- import type { PolicyConfig, ProviderCandidate } from "offrouter-core";
5
- import {
6
- handleUserPromptSubmit,
7
- runUserPromptSubmitHook,
8
- } from "./user-prompt-submit.js";
9
-
10
- const fixturePath = fileURLToPath(
11
- new URL(
12
- "../../../../tests/fixtures/claude/user-prompt-submit.json",
13
- import.meta.url,
14
- ),
15
- );
16
-
17
- const policy: PolicyConfig = {
18
- allowlistedProfiles: ["claude-personal"],
19
- deniedProfilePatterns: ["*-work"],
20
- };
21
-
22
- const candidates: ProviderCandidate[] = [
23
- {
24
- providerId: "fake",
25
- modelId: "fake-coder",
26
- displayName: "Fake Coder",
27
- authTier: "api-key",
28
- authScope: "third-party",
29
- health: "healthy",
30
- supportsTools: true,
31
- supportsStreaming: true,
32
- supportsJson: true,
33
- supportsImages: false,
34
- estimatedCostUsd: 0,
35
- },
36
- ];
37
-
38
- async function fixture(): Promise<string> {
39
- return readFile(fixturePath, "utf8");
40
- }
41
-
42
- function parseStdout(stdout: string): Record<string, unknown> {
43
- expect(stdout.trim()).toBe(stdout);
44
- expect(stdout).not.toContain("\n\n");
45
- return JSON.parse(stdout) as Record<string, unknown>;
46
- }
47
-
48
- describe("Claude UserPromptSubmit hook", () => {
49
- it("injects additionalContext for an allowed claude-personal profile", async () => {
50
- const input = JSON.parse(await fixture());
51
-
52
- const output = handleUserPromptSubmit(input, {
53
- profile: "claude-personal",
54
- policy,
55
- candidates,
56
- trustedWorkspace: true,
57
- routeConstraints: { allowApiKeyFallback: true },
58
- });
59
-
60
- expect(output.decision).toBeUndefined();
61
- expect(output.hookSpecificOutput).toMatchObject({
62
- hookEventName: "UserPromptSubmit",
63
- });
64
- expect(output.hookSpecificOutput?.additionalContext).toContain(
65
- "Claude Code's primary model is unchanged in V1; OffRouter routes delegated work.",
66
- );
67
- expect(output.hookSpecificOutput?.additionalContext).toContain(
68
- "fake/fake-coder",
69
- );
70
- expect(output.hookSpecificOutput?.additionalContext).not.toContain(
71
- input.prompt,
72
- );
73
- });
74
-
75
- it("blocks a denied claude-work profile with a clear reason", async () => {
76
- const input = JSON.parse(await fixture());
77
-
78
- const output = handleUserPromptSubmit(input, {
79
- profile: "claude-work",
80
- policy,
81
- candidates,
82
- trustedWorkspace: true,
83
- routeConstraints: { allowApiKeyFallback: true },
84
- });
85
-
86
- expect(output).toMatchObject({
87
- decision: "block",
88
- });
89
- expect(output.reason).toContain("claude-work");
90
- expect(output.reason).toContain("denied");
91
- });
92
-
93
- it("blocks an unallowlisted profile with a clear reason", async () => {
94
- const input = JSON.parse(await fixture());
95
-
96
- const output = handleUserPromptSubmit(input, {
97
- profile: "claude-personal-extra",
98
- policy,
99
- candidates,
100
- trustedWorkspace: true,
101
- routeConstraints: { allowApiKeyFallback: true },
102
- });
103
-
104
- expect(output).toMatchObject({
105
- decision: "block",
106
- });
107
- expect(output.reason).toContain("claude-personal-extra");
108
- expect(output.reason).toContain("not allowlisted");
109
- });
110
-
111
- it("does not route from an untrusted workspace by default", async () => {
112
- const input = JSON.parse(await fixture());
113
-
114
- const output = handleUserPromptSubmit(input, {
115
- profile: "claude-personal",
116
- policy,
117
- candidates,
118
- routeConstraints: { allowApiKeyFallback: true },
119
- });
120
-
121
- expect(output.decision).toBeUndefined();
122
- expect(output.hookSpecificOutput?.additionalContext).toContain(
123
- "could not select",
124
- );
125
- expect(output.hookSpecificOutput?.additionalContext).toContain("untrusted");
126
- expect(output.hookSpecificOutput?.additionalContext).not.toContain(
127
- "fake/fake-coder",
128
- );
129
- });
130
-
131
- it("does not recommend API-key fallback unless explicitly allowed", async () => {
132
- const input = JSON.parse(await fixture());
133
-
134
- const output = handleUserPromptSubmit(input, {
135
- profile: "claude-personal",
136
- policy,
137
- candidates,
138
- trustedWorkspace: true,
139
- });
140
-
141
- expect(output.decision).toBeUndefined();
142
- expect(output.hookSpecificOutput?.additionalContext).toContain(
143
- "could not select",
144
- );
145
- expect(output.hookSpecificOutput?.additionalContext).toContain(
146
- "allowApiKeyFallback is false",
147
- );
148
- expect(output.hookSpecificOutput?.additionalContext).not.toContain(
149
- "fake/fake-coder",
150
- );
151
- });
152
-
153
- it("continues Claude primary flow when routing needs configuration", async () => {
154
- const input = JSON.parse(await fixture());
155
-
156
- const output = handleUserPromptSubmit(input, {
157
- profile: "claude-personal",
158
- policy,
159
- candidates: [],
160
- trustedWorkspace: true,
161
- });
162
-
163
- expect(output.decision).toBeUndefined();
164
- expect(output.hookSpecificOutput?.additionalContext).toContain(
165
- "needs configuration",
166
- );
167
- });
168
-
169
- it("fails closed for schema-invalid hook JSON", async () => {
170
- const output = handleUserPromptSubmit(
171
- { hook_event_name: "PreToolUse", prompt: "" },
172
- {
173
- profile: "claude-personal",
174
- policy,
175
- candidates,
176
- trustedWorkspace: true,
177
- routeConstraints: { allowApiKeyFallback: true },
178
- },
179
- );
180
-
181
- expect(output).toMatchObject({
182
- decision: "block",
183
- });
184
- expect(output.reason).toContain("Invalid Claude hook JSON");
185
- });
186
-
187
- it("fails closed for invalid hook JSON", async () => {
188
- const result = await runUserPromptSubmitHook("{not-json", {
189
- profile: "claude-personal",
190
- policy,
191
- candidates,
192
- trustedWorkspace: true,
193
- routeConstraints: { allowApiKeyFallback: true },
194
- });
195
-
196
- expect(result.exitCode).toBe(0);
197
- const output = parseStdout(result.stdout);
198
- expect(output).toMatchObject({
199
- decision: "block",
200
- });
201
- expect(output.reason).toContain("Invalid Claude hook JSON");
202
- expect(result.stderr).toBe("");
203
- });
204
-
205
- it("writes valid JSON to stdout with no extra text", async () => {
206
- const result = await runUserPromptSubmitHook(await fixture(), {
207
- profile: "claude-personal",
208
- policy,
209
- candidates,
210
- trustedWorkspace: true,
211
- routeConstraints: { allowApiKeyFallback: true },
212
- });
213
-
214
- expect(result.exitCode).toBe(0);
215
- const output = parseStdout(result.stdout);
216
- expect(output).toHaveProperty("hookSpecificOutput");
217
- expect(result.stdout).not.toContain(
218
- "Explain this repository and suggest the next test to write.",
219
- );
220
- expect(result.stderr).toBe("");
221
- });
222
- });