claude-smart 0.2.25 → 0.2.27

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.
@@ -12,6 +12,7 @@
12
12
  "use strict";
13
13
 
14
14
  const { execSync, spawn } = require("child_process");
15
+ const crypto = require("crypto");
15
16
  const {
16
17
  appendFileSync,
17
18
  cpSync,
@@ -21,7 +22,8 @@ const {
21
22
  rmSync,
22
23
  writeFileSync,
23
24
  } = require("fs");
24
- const { homedir } = require("os");
25
+ const https = require("https");
26
+ const { homedir, tmpdir } = require("os");
25
27
  const { dirname, join } = require("path");
26
28
 
27
29
  const DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart";
@@ -52,6 +54,8 @@ const CODEX_REQUIRED_FILES = [
52
54
  ".agents/plugins/marketplace.json",
53
55
  "plugin/.codex-plugin/plugin.json",
54
56
  "plugin/hooks/codex-hooks.json",
57
+ "plugin/scripts/codex-claude-compat.py",
58
+ "plugin/scripts/codex-hook.js",
55
59
  "plugin/scripts/_codex_env.sh",
56
60
  ];
57
61
  const CODEX_CLI_TIMEOUT_MS = 30_000;
@@ -175,7 +179,10 @@ function seedReflexioEnv() {
175
179
  const existing = existsSync(REFLEXIO_ENV_PATH)
176
180
  ? readFileSync(REFLEXIO_ENV_PATH, "utf8")
177
181
  : "";
178
- const flags = ["CLAUDE_SMART_USE_LOCAL_CLI", "CLAUDE_SMART_USE_LOCAL_EMBEDDING"];
182
+ const flags = [
183
+ "CLAUDE_SMART_USE_LOCAL_CLI",
184
+ "CLAUDE_SMART_USE_LOCAL_EMBEDDING",
185
+ ];
179
186
  const missing = flags.filter((f) => !new RegExp(`^${f}=`, "m").test(existing));
180
187
  if (missing.length === 0) return [];
181
188
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
@@ -184,6 +191,246 @@ function seedReflexioEnv() {
184
191
  return missing;
185
192
  }
186
193
 
194
+ function isWindows() {
195
+ return process.platform === "win32";
196
+ }
197
+
198
+ function runChecked(command, args, options = {}) {
199
+ return new Promise((resolve) => {
200
+ const child = spawn(command, args, {
201
+ cwd: options.cwd,
202
+ env: options.env || process.env,
203
+ shell: isWindows() && /\.(?:cmd|bat)$/i.test(command),
204
+ stdio: "inherit",
205
+ windowsHide: true,
206
+ });
207
+ child.on("exit", (code) => resolve(typeof code === "number" ? code : 1));
208
+ child.on("error", () => resolve(1));
209
+ });
210
+ }
211
+
212
+ function downloadFile(url, dest) {
213
+ return new Promise((resolve, reject) => {
214
+ const request = https.get(url, (response) => {
215
+ if (
216
+ response.statusCode &&
217
+ response.statusCode >= 300 &&
218
+ response.statusCode < 400 &&
219
+ response.headers.location
220
+ ) {
221
+ downloadFile(new URL(response.headers.location, url).toString(), dest)
222
+ .then(resolve, reject);
223
+ response.resume();
224
+ return;
225
+ }
226
+ if (response.statusCode !== 200) {
227
+ response.resume();
228
+ reject(new Error(`download failed (${response.statusCode}) for ${url}`));
229
+ return;
230
+ }
231
+ const chunks = [];
232
+ response.on("data", (chunk) => chunks.push(chunk));
233
+ response.on("end", () => {
234
+ writeFileSync(dest, Buffer.concat(chunks));
235
+ resolve();
236
+ });
237
+ });
238
+ request.on("error", reject);
239
+ request.setTimeout(120_000, () => request.destroy(new Error(`download timed out for ${url}`)));
240
+ });
241
+ }
242
+
243
+ function resolveCommand(names, extraDirs = []) {
244
+ const pathParts = [
245
+ ...extraDirs,
246
+ ...(process.env.PATH || "").split(process.platform === "win32" ? ";" : ":"),
247
+ ].filter(Boolean);
248
+ for (const dir of pathParts) {
249
+ for (const name of names) {
250
+ const candidate = join(dir, name);
251
+ if (existsSync(candidate)) return candidate;
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+
257
+ function privateNodeRoot() {
258
+ return join(homedir(), ".claude-smart", "node", "current");
259
+ }
260
+
261
+ function privateNodeBinDirs() {
262
+ const root = privateNodeRoot();
263
+ return [join(root, "bin"), root];
264
+ }
265
+
266
+ function resolvePrivateNode() {
267
+ return resolveCommand(isWindows() ? ["node.exe", "node"] : ["node"], privateNodeBinDirs());
268
+ }
269
+
270
+ function resolvePrivateNpm() {
271
+ return resolveCommand(
272
+ isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"],
273
+ privateNodeBinDirs(),
274
+ );
275
+ }
276
+
277
+ function runtimeEnv(extraDirs = []) {
278
+ const delimiter = process.platform === "win32" ? ";" : ":";
279
+ const dirs = [
280
+ ...extraDirs,
281
+ ...privateNodeBinDirs(),
282
+ join(homedir(), ".local", "bin"),
283
+ join(homedir(), ".cargo", "bin"),
284
+ ];
285
+ return {
286
+ ...process.env,
287
+ PATH: `${dirs.join(delimiter)}${delimiter}${process.env.PATH || ""}`,
288
+ };
289
+ }
290
+
291
+ async function ensureWindowsPrivateNode() {
292
+ if (!isWindows()) return null;
293
+ const existing = resolvePrivateNode();
294
+ const existingNpm = resolvePrivateNpm();
295
+ if (existing && existingNpm) return { node: existing, npm: existingNpm };
296
+
297
+ const major = process.env.CLAUDE_SMART_NODE_LTS_MAJOR || "22";
298
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
299
+ const baseUrl = process.env.CLAUDE_SMART_NODE_BASE_URL || `https://nodejs.org/dist/latest-v${major}.x`;
300
+ const nodeRoot = join(homedir(), ".claude-smart", "node");
301
+ const temp = join(tmpdir(), `claude-smart-node-${process.pid}`);
302
+ mkdirSync(nodeRoot, { recursive: true });
303
+ rmSync(temp, { recursive: true, force: true });
304
+ mkdirSync(temp, { recursive: true });
305
+
306
+ const sumsPath = join(temp, "SHASUMS256.txt");
307
+ await downloadFile(`${baseUrl}/SHASUMS256.txt`, sumsPath);
308
+ const sums = readFileSync(sumsPath, "utf8");
309
+ const match = sums
310
+ .split(/\r?\n/)
311
+ .map((line) => line.trim().split(/\s+/))
312
+ .find((parts) => parts[1] && new RegExp(`^node-v[^ ]+-win-${arch}\\.zip$`).test(parts[1]));
313
+ if (!match) throw new Error(`could not resolve Node.js win-${arch} archive from ${baseUrl}`);
314
+ const [expectedHash, archiveName] = match;
315
+ const archivePath = join(temp, archiveName);
316
+ await downloadFile(`${baseUrl}/${archiveName}`, archivePath);
317
+ const actualHash = crypto.createHash("sha256").update(readFileSync(archivePath)).digest("hex");
318
+ if (actualHash !== expectedHash) {
319
+ throw new Error(`Node.js checksum verification failed for ${archiveName}`);
320
+ }
321
+
322
+ const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
323
+ if (!powershell) throw new Error("PowerShell is required to extract private Node.js on Windows");
324
+ const extractDir = join(temp, "extract");
325
+ mkdirSync(extractDir, { recursive: true });
326
+ const code = await runChecked(
327
+ powershell,
328
+ [
329
+ "-NoProfile",
330
+ "-ExecutionPolicy",
331
+ "Bypass",
332
+ "-Command",
333
+ "$ProgressPreference='SilentlyContinue'; Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DEST_DIR -Force",
334
+ ],
335
+ { env: { ...process.env, ARCHIVE_PATH: archivePath, DEST_DIR: extractDir } },
336
+ );
337
+ if (code !== 0) throw new Error(`Node.js archive extraction failed for ${archiveName}`);
338
+ const extracted = join(extractDir, archiveName.replace(/\.zip$/, ""));
339
+ const current = privateNodeRoot();
340
+ rmSync(current, { recursive: true, force: true });
341
+ cpSync(extracted, current, { recursive: true, force: true });
342
+ rmSync(temp, { recursive: true, force: true });
343
+
344
+ const node = resolvePrivateNode();
345
+ const npm = resolvePrivateNpm();
346
+ if (!node || !npm) throw new Error("private Node.js install completed but node/npm are not usable");
347
+ return { node, npm };
348
+ }
349
+
350
+ function resolveUv() {
351
+ return resolveCommand(isWindows() ? ["uv.exe", "uv"] : ["uv"], [
352
+ join(homedir(), ".local", "bin"),
353
+ join(homedir(), ".cargo", "bin"),
354
+ ]);
355
+ }
356
+
357
+ async function ensureWindowsUv() {
358
+ let uv = resolveUv();
359
+ if (uv) return uv;
360
+ const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
361
+ if (!powershell) throw new Error("PowerShell is required to install uv on Windows");
362
+ const code = await runChecked(powershell, [
363
+ "-NoProfile",
364
+ "-ExecutionPolicy",
365
+ "Bypass",
366
+ "-Command",
367
+ "irm https://astral.sh/uv/install.ps1 | iex",
368
+ ]);
369
+ if (code !== 0) throw new Error("uv install via PowerShell failed");
370
+ uv = resolveUv();
371
+ if (!uv) throw new Error("uv install reported success but uv.exe was not found");
372
+ return uv;
373
+ }
374
+
375
+ function quoteCommandPart(part) {
376
+ return `"${String(part).replace(/"/g, '\\"')}"`;
377
+ }
378
+
379
+ function patchCodexHooksForWindows(pluginRoot, nodePath) {
380
+ const hookPath = join(pluginRoot, "hooks", "codex-hooks.json");
381
+ const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
382
+ const runner = join(pluginRoot, "scripts", "codex-hook.js");
383
+ const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" ");
384
+ const sessionHooks = parsed.hooks.SessionStart?.[0]?.hooks || [];
385
+ if (sessionHooks[0]) sessionHooks[0].command = command("ensure-root");
386
+ if (sessionHooks[1]) sessionHooks[1].command = command("backend");
387
+ if (sessionHooks[2]) sessionHooks[2].command = command("dashboard");
388
+ if (sessionHooks[3]) sessionHooks[3].command = command("hook", "session-start");
389
+ const userPrompt = parsed.hooks.UserPromptSubmit?.[0]?.hooks?.[0];
390
+ if (userPrompt) userPrompt.command = command("hook", "user-prompt");
391
+ const postTool = parsed.hooks.PostToolUse?.[0]?.hooks?.[0];
392
+ if (postTool) postTool.command = command("hook", "post-tool");
393
+ const stop = parsed.hooks.Stop?.[0]?.hooks?.[0];
394
+ if (stop) stop.command = command("hook", "stop");
395
+ writeFileSync(hookPath, JSON.stringify(parsed, null, 2) + "\n");
396
+ }
397
+
398
+ function ensureWindowsPluginRoot(pluginRoot) {
399
+ const reflexioDir = dirname(REFLEXIO_ENV_PATH);
400
+ const link = join(reflexioDir, "plugin-root");
401
+ mkdirSync(reflexioDir, { recursive: true });
402
+ rmSync(link, { recursive: true, force: true });
403
+ try {
404
+ require("fs").symlinkSync(pluginRoot, link, "junction");
405
+ } catch {
406
+ writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
407
+ }
408
+ }
409
+
410
+ async function bootstrapWindowsCodexCache(pluginRoot) {
411
+ if (!isWindows()) return;
412
+ process.stdout.write("Preparing Windows Codex runtime for claude-smart hooks...\n");
413
+ const nodeRuntime = await ensureWindowsPrivateNode();
414
+ patchCodexHooksForWindows(pluginRoot, nodeRuntime.node);
415
+ ensureWindowsPluginRoot(pluginRoot);
416
+ const uv = await ensureWindowsUv();
417
+ const env = runtimeEnv([dirname(uv), ...privateNodeBinDirs()]);
418
+ let code = await runChecked(
419
+ uv,
420
+ ["sync", "--locked", "--python", "3.12", "--quiet"],
421
+ { cwd: pluginRoot, env },
422
+ );
423
+ if (code !== 0) throw new Error(`uv sync failed in ${pluginRoot}`);
424
+
425
+ const dashboardDir = join(pluginRoot, "dashboard");
426
+ if (existsSync(dashboardDir)) {
427
+ code = await runChecked(nodeRuntime.npm, ["ci"], { cwd: dashboardDir, env });
428
+ if (code !== 0) throw new Error(`npm ci failed in ${dashboardDir}`);
429
+ code = await runChecked(nodeRuntime.npm, ["run", "build"], { cwd: dashboardDir, env });
430
+ if (code !== 0) throw new Error(`npm run build failed in ${dashboardDir}`);
431
+ }
432
+ }
433
+
187
434
  function printHelp() {
188
435
  process.stdout.write(
189
436
  [
@@ -205,9 +452,10 @@ function printHelp() {
205
452
  "Codex install:",
206
453
  ` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
207
454
  " 2. codex plugin marketplace add <copied marketplace>",
208
- " 3. codex features enable plugin_hooks",
455
+ " 3. codex features enable hooks && codex features enable plugin_hooks",
209
456
  " 4. Installs claude-smart into Codex's plugin cache and enables it",
210
- " 5. Restart Codex.",
457
+ " 5. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
458
+ " 6. Restart Codex.",
211
459
  "",
212
460
  "Update:",
213
461
  " npx claude-smart update Update to the latest version",
@@ -359,6 +607,227 @@ function setCodexPluginEnabled() {
359
607
  writeFileSync(CODEX_CONFIG_PATH, next);
360
608
  }
361
609
 
610
+ function tomlDottedQuoted(name) {
611
+ return `"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
612
+ }
613
+
614
+ function setTomlFeature(feature, value) {
615
+ // Minimal port of `_set_toml_feature` in plugin/src/claude_smart/cli.py:
616
+ // ensures `[features]\n<feature> = <bool>\n` is present in
617
+ // ~/.codex/config.toml, replacing any prior value for the same key.
618
+ const desired = `${feature} = ${value ? "true" : "false"}`;
619
+ const sectionRe = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/;
620
+ const featureRe = new RegExp(`^\\s*${feature.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`);
621
+ const text = existsSync(CODEX_CONFIG_PATH)
622
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
623
+ : "";
624
+ const lines = text.split("\n");
625
+ let inFeatures = false;
626
+ let featuresIdx = null;
627
+ let insertIdx = null;
628
+ let changed = false;
629
+ const out = [];
630
+ for (const line of lines) {
631
+ const sectionMatch = line.match(sectionRe);
632
+ if (sectionMatch) {
633
+ if (inFeatures && insertIdx === null) insertIdx = out.length;
634
+ inFeatures = sectionMatch[1].trim() === "features";
635
+ if (inFeatures) featuresIdx = out.length;
636
+ out.push(line);
637
+ continue;
638
+ }
639
+ if (inFeatures && featureRe.test(line)) {
640
+ out.push(desired);
641
+ changed = changed || line !== desired;
642
+ continue;
643
+ }
644
+ out.push(line);
645
+ }
646
+ if (featuresIdx === null) {
647
+ if (out.length && out[out.length - 1].trim()) out.push("");
648
+ out.push("[features]", desired);
649
+ changed = true;
650
+ } else {
651
+ const sectionEnd = insertIdx !== null ? insertIdx : out.length;
652
+ let hasFeature = false;
653
+ for (let i = featuresIdx + 1; i < sectionEnd; i++) {
654
+ if (featureRe.test(out[i])) { hasFeature = true; break; }
655
+ }
656
+ if (!hasFeature) {
657
+ const idx = insertIdx !== null ? insertIdx : out.length;
658
+ out.splice(idx, 0, desired);
659
+ changed = true;
660
+ }
661
+ }
662
+ if (!changed && text.endsWith("\n")) return true;
663
+ mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
664
+ let payload = out.join("\n");
665
+ if (!payload.endsWith("\n")) payload += "\n";
666
+ writeFileSync(CODEX_CONFIG_PATH, payload);
667
+ return true;
668
+ }
669
+
670
+ function setCodexHookStates(states) {
671
+ const entries = Object.entries(states);
672
+ if (entries.length === 0) return false;
673
+ removeTomlSections(CODEX_CONFIG_PATH, {
674
+ exact: new Set(),
675
+ prefixes: [`hooks.state."${CODEX_PLUGIN_ID}:`],
676
+ });
677
+ const existing = existsSync(CODEX_CONFIG_PATH)
678
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
679
+ : "";
680
+ let next = existing;
681
+ if (next && !next.endsWith("\n")) next += "\n";
682
+ if (!next.includes("[hooks.state]")) {
683
+ if (next.trim()) next += "\n";
684
+ next += "[hooks.state]\n";
685
+ }
686
+ if (next.trim()) next += "\n";
687
+ for (const [key, currentHash] of entries.sort(([a], [b]) => a.localeCompare(b))) {
688
+ next += `[hooks.state.${tomlDottedQuoted(key)}]\n`;
689
+ next += "enabled = true\n";
690
+ next += `trusted_hash = "${currentHash}"\n\n`;
691
+ }
692
+ mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
693
+ writeFileSync(CODEX_CONFIG_PATH, next.trimEnd() + "\n");
694
+ return true;
695
+ }
696
+
697
+ function createCodexAppServerClient(child) {
698
+ // A single long-lived stdout listener that demultiplexes JSON-RPC responses
699
+ // by id. Avoids losing messages between sequential requests.
700
+ const pending = new Map();
701
+ let buffer = "";
702
+ let exited = false;
703
+
704
+ const onData = (chunk) => {
705
+ buffer += chunk.toString();
706
+ let newline;
707
+ while ((newline = buffer.indexOf("\n")) >= 0) {
708
+ const line = buffer.slice(0, newline);
709
+ buffer = buffer.slice(newline + 1);
710
+ if (!line.trim()) continue;
711
+ let message;
712
+ try {
713
+ message = JSON.parse(line);
714
+ } catch {
715
+ continue;
716
+ }
717
+ const entry = pending.get(message.id);
718
+ if (!entry) continue;
719
+ pending.delete(message.id);
720
+ clearTimeout(entry.timer);
721
+ if (message.error) {
722
+ entry.reject(new Error(JSON.stringify(message.error)));
723
+ } else {
724
+ entry.resolve(message);
725
+ }
726
+ }
727
+ };
728
+ const onExit = () => {
729
+ exited = true;
730
+ for (const entry of pending.values()) {
731
+ clearTimeout(entry.timer);
732
+ entry.reject(new Error("Codex app-server exited before responding"));
733
+ }
734
+ pending.clear();
735
+ };
736
+ child.stdout.on("data", onData);
737
+ child.on("exit", onExit);
738
+
739
+ return {
740
+ request(id, method, params, timeoutMs) {
741
+ return new Promise((resolve, reject) => {
742
+ if (exited) {
743
+ reject(new Error("Codex app-server exited before responding"));
744
+ return;
745
+ }
746
+ const timer = setTimeout(() => {
747
+ pending.delete(id);
748
+ reject(new Error(`Codex app-server ${method} timed out`));
749
+ }, timeoutMs);
750
+ pending.set(id, { resolve, reject, timer });
751
+ child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
752
+ });
753
+ },
754
+ notify(method, params) {
755
+ if (exited) return;
756
+ child.stdin.write(JSON.stringify({ method, params }) + "\n");
757
+ },
758
+ close() {
759
+ child.stdout.off("data", onData);
760
+ child.off("exit", onExit);
761
+ },
762
+ };
763
+ }
764
+
765
+ async function listCodexPluginHooks(cwd) {
766
+ const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
767
+ stdio: ["pipe", "pipe", "ignore"],
768
+ });
769
+ const client = createCodexAppServerClient(child);
770
+ try {
771
+ await client.request(
772
+ 1,
773
+ "initialize",
774
+ {
775
+ clientInfo: {
776
+ name: "claude_smart_installer",
777
+ title: "claude-smart installer",
778
+ version: "0.0.0",
779
+ },
780
+ capabilities: { experimentalApi: true },
781
+ },
782
+ CODEX_CLI_TIMEOUT_MS,
783
+ );
784
+ client.notify("initialized", {});
785
+ const response = await client.request(
786
+ 2,
787
+ "hooks/list",
788
+ { cwds: [cwd] },
789
+ CODEX_CLI_TIMEOUT_MS,
790
+ );
791
+ const hooks = response.result?.data?.[0]?.hooks;
792
+ if (!Array.isArray(hooks)) {
793
+ throw new Error("Codex app-server hook metadata was malformed");
794
+ }
795
+ return hooks.filter(
796
+ (hook) =>
797
+ hook &&
798
+ (hook.pluginId === CODEX_PLUGIN_ID ||
799
+ String(hook.key || "").startsWith(`${CODEX_PLUGIN_ID}:`)),
800
+ );
801
+ } finally {
802
+ client.close();
803
+ child.stdin.destroy();
804
+ child.stdout.destroy();
805
+ child.kill("SIGTERM");
806
+ child.unref();
807
+ }
808
+ }
809
+
810
+ async function trustCodexPluginHooks(cwd) {
811
+ const hooks = await listCodexPluginHooks(cwd);
812
+ const states = {};
813
+ for (const hook of hooks) {
814
+ if (
815
+ typeof hook.key === "string" &&
816
+ hook.key.startsWith(`${CODEX_PLUGIN_ID}:`) &&
817
+ typeof hook.currentHash === "string"
818
+ ) {
819
+ states[hook.key] = hook.currentHash;
820
+ }
821
+ }
822
+ if (Object.keys(states).length === 0) {
823
+ throw new Error("Codex did not report trust hashes for claude-smart hooks");
824
+ }
825
+ if (!setCodexHookStates(states)) {
826
+ throw new Error(`could not write claude-smart hook trust state to ${CODEX_CONFIG_PATH}`);
827
+ }
828
+ return Object.keys(states).length;
829
+ }
830
+
362
831
  function codexPluginVersion(pluginRoot) {
363
832
  try {
364
833
  const manifest = JSON.parse(
@@ -515,24 +984,60 @@ async function runInstallCodex() {
515
984
  process.exit(code);
516
985
  }
517
986
 
518
- code = await runCodex(["features", "enable", "plugin_hooks"]);
519
- if (code !== 0) {
520
- process.stderr.write("error: could not enable Codex plugin_hooks feature.\n");
521
- process.exit(code);
987
+ for (const feature of ["hooks", "plugin_hooks"]) {
988
+ code = await runCodex(["features", "enable", feature]);
989
+ if (code !== 0) {
990
+ // Older Codex builds may not recognize the `hooks` feature name; fall
991
+ // through to writing the flag directly under [features] in config.toml.
992
+ try {
993
+ setTomlFeature(feature, true);
994
+ process.stdout.write(`Enabled Codex ${feature} via ${CODEX_CONFIG_PATH}.\n`);
995
+ } catch (err) {
996
+ process.stderr.write(
997
+ `error: could not enable Codex ${feature} feature: ${err && err.message ? err.message : err}\n`,
998
+ );
999
+ process.exit(code);
1000
+ }
1001
+ }
522
1002
  }
523
1003
 
524
1004
  let cacheDir = null;
1005
+ let trustedHookCount = 0;
1006
+ let trustError = null;
525
1007
  try {
526
1008
  cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
527
1009
  process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
1010
+ await bootstrapWindowsCodexCache(cacheDir);
528
1011
  } catch (err) {
529
1012
  process.stderr.write(
530
1013
  `error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
531
1014
  );
532
1015
  process.stderr.write(
533
- `Open Codex, run /plugins, and install claude-smart from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace manually.\n`,
1016
+ `Open Codex, run /plugins, install claude-smart from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex.\n`,
1017
+ );
1018
+ process.exit(1);
1019
+ }
1020
+
1021
+ for (let attempt = 0; attempt < 2; attempt++) {
1022
+ try {
1023
+ trustedHookCount = await trustCodexPluginHooks(process.cwd());
1024
+ trustError = null;
1025
+ break;
1026
+ } catch (err) {
1027
+ trustError = err;
1028
+ if (attempt === 0) await new Promise((r) => setTimeout(r, 500));
1029
+ }
1030
+ }
1031
+ if (trustError) {
1032
+ process.stderr.write(
1033
+ `warning: ${trustError && trustError.message ? trustError.message : trustError}\n`,
1034
+ );
1035
+ process.stderr.write(
1036
+ `Fully quit and reopen Codex in this repo, run /hooks, trust the claude-smart hooks, and restart Codex.\n`,
534
1037
  );
535
1038
  process.exit(1);
1039
+ } else {
1040
+ process.stdout.write(`Trusted and enabled ${trustedHookCount} claude-smart Codex hooks.\n`);
536
1041
  }
537
1042
 
538
1043
  const added = seedReflexioEnv();
@@ -544,7 +1049,7 @@ async function runInstallCodex() {
544
1049
  [
545
1050
  "",
546
1051
  "claude-smart Codex support is installed.",
547
- `Restart Codex so the installed plugin and hooks reload. /plugins should show claude-smart as installed from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace.`,
1052
+ `Restart Codex so the installed plugin and trusted hooks reload. /plugins should show claude-smart as installed from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace.`,
548
1053
  "Local data is shared with Claude Code under ~/.reflexio/ and ~/.claude-smart/.",
549
1054
  "",
550
1055
  ].join("\n"),
@@ -570,7 +1075,7 @@ async function runUninstallCodex() {
570
1075
  [
571
1076
  "",
572
1077
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply.",
573
- "Codex's global plugin_hooks feature and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
1078
+ "Codex's global hook feature flags and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
574
1079
  "",
575
1080
  ].join("\n"),
576
1081
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -17,6 +17,7 @@
17
17
  "playbook",
18
18
  "learning"
19
19
  ],
20
+ "skills": "./skills/",
20
21
  "hooks": "./hooks/codex-hooks.json",
21
22
  "interface": {
22
23
  "displayName": "claude-smart",