claude-smart 0.2.24 → 0.2.26

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 CHANGED
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
14
14
  </a>
15
15
  <a href="plugin/pyproject.toml">
16
- <img src="https://img.shields.io/badge/version-0.2.24-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.26-green.svg" alt="Version">
17
17
  </a>
18
18
  <a href="plugin/pyproject.toml">
19
19
  <img src="https://img.shields.io/badge/python-%3E%3D3.12-brightgreen.svg" alt="Python">
@@ -81,61 +81,45 @@ Four ways this changes what your coding assistant can do for you:
81
81
  ### Claude Code
82
82
 
83
83
  ```bash
84
- claude plugin marketplace add ReflexioAI/claude-smart
85
- claude plugin install claude-smart@reflexioai
84
+ npx claude-smart install # or: uvx claude-smart install
86
85
  ```
87
86
 
88
- The plugin Setup hook installs its own `uv`, Python 3.12 environment, and
89
- private Node.js/npm runtime under `~/.claude-smart/` when they are missing, so
90
- you do not need to install Python or Node globally for the plugin/dashboard.
87
+ Then restart Claude Code.
91
88
 
92
- If you already have Node.js or uv, these convenience wrappers are equivalent
93
- but require their own runtime to already exist:
89
+ Requires Node.js (for `npx`) or uv (for `uvx`) to already exist.
90
+
91
+ Alternatively, install via Claude Code's plugin marketplace:
94
92
 
95
93
  ```bash
96
- npx claude-smart install # or: uvx claude-smart install
94
+ claude plugin marketplace add ReflexioAI/claude-smart
95
+ claude plugin install claude-smart@reflexioai
97
96
  ```
98
97
 
99
- Then restart Claude Code.
98
+ The plugin Setup hook installs its own `uv`, Python 3.12 environment, and
99
+ private Node.js/npm runtime under `~/.claude-smart/` when they are missing, so
100
+ you do not need to install Python or Node globally for the plugin/dashboard.
100
101
 
101
102
  To uninstall:
102
103
 
103
104
  ```bash
104
- claude plugin uninstall claude-smart@reflexioai
105
+ npx claude-smart uninstall # or: uvx claude-smart uninstall
105
106
  ```
106
107
 
107
- Or, if you already have Node.js or uv:
108
+ Or, if installed via the plugin marketplace:
108
109
 
109
110
  ```bash
110
- npx claude-smart uninstall # or: uvx claude-smart uninstall
111
+ claude plugin uninstall claude-smart@reflexioai
111
112
  ```
112
113
 
113
114
  ### Codex
114
115
 
115
- You need the `codex` CLI on `PATH` and Node.js available for `npx`:
116
-
117
116
  ```bash
118
117
  npx claude-smart install --host codex
119
118
  ```
120
119
 
121
- The helper registers the bundled **ReflexioAI** marketplace with Codex and
122
- enables Codex plugin hooks. Then:
123
-
124
- 1. Fully quit and reopen Codex in your project.
125
- 2. Run `/plugins`.
126
- 3. Install `claude-smart` from the **ReflexioAI** marketplace.
127
- 4. Restart Codex again so hooks reload.
128
-
129
- Do not create a `~/plugins/claude-smart` symlink for a normal `npx` install;
130
- that symlink is only for plugin development from a cloned checkout.
131
-
132
- Installing from a clone is only for plugin development; see
133
- [DEVELOPER.md](./DEVELOPER.md#developing-locally).
134
-
135
- Codex and Claude Code intentionally share the same `CLAUDE_SMART_*` environment
136
- variables, `~/.reflexio/` data, `~/.claude-smart/` session buffers, backend,
137
- dashboard, and learned skills/preferences.
120
+ Then fully quit and reopen Codex so hooks reload.
138
121
 
122
+ Requires the `codex` CLI on `PATH` and Node.js (for `npx`).
139
123
 
140
124
  To uninstall:
141
125
 
@@ -143,11 +127,10 @@ To uninstall:
143
127
  npx claude-smart uninstall --host codex
144
128
  ```
145
129
 
146
- This removes the Codex marketplace registration, installed plugin config, and
147
- Codex plugin cache. Local data under `~/.reflexio/` and `~/.claude-smart/` is
148
- left in place remove manually if desired. Restart Codex after uninstalling.
130
+ Restart Codex after uninstalling. Learned data under `~/.reflexio/` and `~/.claude-smart/` is preserved and shared with Claude Code, so you can switch between hosts without losing skills or preferences.
131
+
132
+ Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-locally) for what the installer does, manual toggles via `/plugins`, and clone-based development.
149
133
 
150
- Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-locally).
151
134
  > **Not supported:** Claude Code Cowork, claude.ai/code web, or remote Codex environments without local plugin hooks — they run outside your local machine, so the local backend/dashboard and `~/.reflexio/` aren't reachable.
152
135
 
153
136
  ---
@@ -186,7 +169,7 @@ claude-smart builds three artifacts as you work and injects the relevant ones in
186
169
 
187
170
  Skills clean themselves up: correct the same thing twice and they merge; change your mind and the old one is archived.
188
171
 
189
- Under the hood: hooks watch your turns, tool calls, and assistant replies, auto-flagging corrections (or anything you flag with `/learn`). At session end (or on `/learn`), [reflexio](https://github.com/ReflexioAI/reflexio) — the self-improving engine that powers claude-smart — extracts preferences and project-specific skills, then rolls durable patterns into shared skills. On each new user prompt, claude-smart searches for matching context and injects only the relevant hits. Run `/show` or the equivalent CLI command to audit the current learned state. Everything runs on your machine.
172
+ Under the hood: hooks watch your turns, tool calls, and assistant replies, auto-flagging corrections (or anything you flag with `/claude-smart:learn`). At session end (or on `/claude-smart:learn`), [reflexio](https://github.com/ReflexioAI/reflexio) — the self-improving engine that powers claude-smart — extracts preferences and project-specific skills, then rolls durable patterns into shared skills. On each new user prompt, claude-smart searches for matching context and injects only the relevant hits. Run `/claude-smart:show` or the equivalent CLI command to audit the current learned state. Everything runs on your machine.
190
173
 
191
174
  **Citations.** At the end of a reply, the assistant may append a short marker:
192
175
 
@@ -195,9 +178,7 @@ Under the hood: hooks watch your turns, tool calls, and assistant replies, auto-
195
178
  ```
196
179
 
197
180
  That signals a preference (`p…`) or skill (`s…`) materially shaped the reply.
198
- Standalone wrappers like `✨abc123✨` are not claude-smart citations and will not
199
- link back to dashboard entries. Open the interaction's detail page in the
200
- [dashboard](#dashboard) to see the exact cited item.
181
+ Open the session's detail page in the [dashboard](http://localhost:3001) to see the exact cited item.
201
182
 
202
183
  See [ARCHITECTURE.md](./ARCHITECTURE.md) for hooks, data flow, and reflexio details.
203
184
 
@@ -205,16 +186,18 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for hooks, data flow, and reflexio deta
205
186
 
206
187
  ## Commands
207
188
 
208
- Claude Code installs these as slash commands. In Codex, run the equivalent shell
209
- command directly, or ask Codex to run it.
189
+ Claude Code installs these as plugin slash commands. Codex does not currently
190
+ support plugin-provided slash commands, so claude-smart ships a Codex skill that
191
+ maps requests like "claude-smart show" or "run claude-smart learn" to the
192
+ equivalent action.
210
193
 
211
- | Claude Code | Codex / shell | What it does |
194
+ | Claude Code | Codex request | What it does |
212
195
  | --- | --- | --- |
213
- | `/dashboard` | `bash ~/.reflexio/plugin-root/scripts/dashboard-open.sh` | Open the dashboard in your browser, auto-starting the reflexio backend and dashboard services if they aren't already running. |
214
- | `/show` | `bash ~/.reflexio/plugin-root/scripts/cli.sh show` | Print current project-specific skills, shared skills, and the current project's preferences so you can audit learned state manually. |
215
- | `/learn [note]` | `bash ~/.reflexio/plugin-root/scripts/cli.sh learn --note "optional note"` | Flag the most recent turn as a correction (for cases the automatic heuristic missed) and force reflexio to run extraction *now* on the session's unpublished interactions. The optional note becomes the correction description the extractor sees. |
216
- | `/restart` | `bash ~/.reflexio/plugin-root/scripts/cli.sh restart` | Restart the reflexio backend and dashboard to pick up new changes (e.g. after upgrading the plugin or editing local reflexio code). |
217
- | `/clear-all` | `bash ~/.reflexio/plugin-root/scripts/cli.sh clear-all --yes` | **Destructive.** Delete *all* reflexio interactions, preferences, and skills. Use when you want to wipe learned state and start fresh. |
196
+ | `/claude-smart:dashboard` | `open claude-smart dashboard` | Open the dashboard in your browser, auto-starting the reflexio backend and dashboard services if they aren't already running. |
197
+ | `/claude-smart:show` | `claude-smart show` | Print current project-specific skills, shared skills, and the current project's preferences so you can audit learned state manually. |
198
+ | `/claude-smart:learn [note]` | `claude-smart learn with note "optional note"` | Flag the most recent turn as a correction (for cases the automatic heuristic missed) and force reflexio to run extraction *now* on the session's unpublished interactions. The optional note becomes the correction description the extractor sees. |
199
+ | `/claude-smart:restart` | `restart claude-smart` | Restart the reflexio backend and dashboard to pick up new changes (e.g. after upgrading the plugin or editing local reflexio code). |
200
+ | `/claude-smart:clear-all` | `clear all claude-smart learnings` | **Destructive.** Delete *all* reflexio interactions, preferences, and skills. Use when you want to wipe learned state and start fresh. |
218
201
 
219
202
  ---
220
203
 
@@ -229,9 +212,9 @@ Advanced users can tune claude-smart via environment variables — see [DEVELOPE
229
212
  | `~/.reflexio/data/reflexio.db` | Source of truth for learned preferences, skills, interactions, full-text indexes, and embedding tables (plus `.db-shm` / `.db-wal` WAL sidecars). Inspect with `sqlite3`. |
230
213
  | `~/.reflexio/.env` | Provider config — `CLAUDE_SMART_USE_LOCAL_CLI`, `CLAUDE_SMART_USE_LOCAL_EMBEDDING`, any optional API keys. |
231
214
  | `.claude/settings.local.json` or `~/.claude/settings.json` | Claude Code hook environment, such as `CLAUDE_SMART_ENABLE_OPTIMIZER`; use project-local settings for one repo or user settings for all projects. |
232
- | `~/.codex/config.toml` | Codex feature flags, including `plugin_hooks = true` after `claude-smart install --host codex`. |
215
+ | `~/.codex/config.toml` | Codex plugin state, hook feature flags, and per-hook trust entries after `claude-smart install --host codex`. |
233
216
  | `~/.codex/plugins/cache/reflexioai/claude-smart/<version>/` | Codex's cached install of the `claude-smart` plugin from the `ReflexioAI` marketplace. |
234
- | `~/.reflexio/plugin-root` | Self-healed symlink to the active plugin dir (managed by `ensure-plugin-root.sh` — written on install, refreshed each `SessionStart`). Slash commands resolve through it, so don't delete it; if you do, the next session will recreate it. |
217
+ | `~/.reflexio/plugin-root` | Self-healed symlink to the active plugin dir (managed by `ensure-plugin-root.sh` — written on install, refreshed each `SessionStart`). Claude Code slash commands and Codex shell-command helpers resolve through it, so don't delete it; if you do, the next session will recreate it. |
235
218
  | `~/.claude-smart/sessions/{session_id}.jsonl` | Per-session buffer. User turns, assistant turns, tool invocations, `{"published_up_to": N}` watermarks. Safe to inspect and safe to delete — everything past the latest watermark has already been written to reflexio's DB. |
236
219
  | `~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/` | Cached ONNX weights (~86 MB, downloaded once). Delete to force a re-download. |
237
220
 
@@ -205,8 +205,10 @@ function printHelp() {
205
205
  "Codex install:",
206
206
  ` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
207
207
  " 2. codex plugin marketplace add <copied marketplace>",
208
- " 3. codex features enable plugin_hooks",
209
- " 4. Fully quit and reopen Codex, run /plugins, install claude-smart, then restart Codex.",
208
+ " 3. codex features enable hooks && codex features enable plugin_hooks",
209
+ " 4. Installs claude-smart into Codex's plugin cache and enables it",
210
+ " 5. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
211
+ " 6. Restart Codex.",
210
212
  "",
211
213
  "Update:",
212
214
  " npx claude-smart update Update to the latest version",
@@ -344,6 +346,271 @@ function cleanupCodexInstallState() {
344
346
  }
345
347
  }
346
348
 
349
+ function setCodexPluginEnabled() {
350
+ const sectionName = `plugins."${CODEX_PLUGIN_ID}"`;
351
+ removeTomlSections(CODEX_CONFIG_PATH, { exact: new Set([sectionName]) });
352
+ const existing = existsSync(CODEX_CONFIG_PATH)
353
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
354
+ : "";
355
+ let next = existing;
356
+ if (next && !next.endsWith("\n")) next += "\n";
357
+ if (next.trim()) next += "\n";
358
+ next += `[${sectionName}]\nenabled = true\n`;
359
+ mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
360
+ writeFileSync(CODEX_CONFIG_PATH, next);
361
+ }
362
+
363
+ function tomlDottedQuoted(name) {
364
+ return `"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
365
+ }
366
+
367
+ function setTomlFeature(feature, value) {
368
+ // Minimal port of `_set_toml_feature` in plugin/src/claude_smart/cli.py:
369
+ // ensures `[features]\n<feature> = <bool>\n` is present in
370
+ // ~/.codex/config.toml, replacing any prior value for the same key.
371
+ const desired = `${feature} = ${value ? "true" : "false"}`;
372
+ const sectionRe = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/;
373
+ const featureRe = new RegExp(`^\\s*${feature.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`);
374
+ const text = existsSync(CODEX_CONFIG_PATH)
375
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
376
+ : "";
377
+ const lines = text.split("\n");
378
+ let inFeatures = false;
379
+ let featuresIdx = null;
380
+ let insertIdx = null;
381
+ let changed = false;
382
+ const out = [];
383
+ for (const line of lines) {
384
+ const sectionMatch = line.match(sectionRe);
385
+ if (sectionMatch) {
386
+ if (inFeatures && insertIdx === null) insertIdx = out.length;
387
+ inFeatures = sectionMatch[1].trim() === "features";
388
+ if (inFeatures) featuresIdx = out.length;
389
+ out.push(line);
390
+ continue;
391
+ }
392
+ if (inFeatures && featureRe.test(line)) {
393
+ out.push(desired);
394
+ changed = changed || line !== desired;
395
+ continue;
396
+ }
397
+ out.push(line);
398
+ }
399
+ if (featuresIdx === null) {
400
+ if (out.length && out[out.length - 1].trim()) out.push("");
401
+ out.push("[features]", desired);
402
+ changed = true;
403
+ } else {
404
+ const sectionEnd = insertIdx !== null ? insertIdx : out.length;
405
+ let hasFeature = false;
406
+ for (let i = featuresIdx + 1; i < sectionEnd; i++) {
407
+ if (featureRe.test(out[i])) { hasFeature = true; break; }
408
+ }
409
+ if (!hasFeature) {
410
+ const idx = insertIdx !== null ? insertIdx : out.length;
411
+ out.splice(idx, 0, desired);
412
+ changed = true;
413
+ }
414
+ }
415
+ if (!changed && text.endsWith("\n")) return true;
416
+ mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
417
+ let payload = out.join("\n");
418
+ if (!payload.endsWith("\n")) payload += "\n";
419
+ writeFileSync(CODEX_CONFIG_PATH, payload);
420
+ return true;
421
+ }
422
+
423
+ function setCodexHookStates(states) {
424
+ const entries = Object.entries(states);
425
+ if (entries.length === 0) return false;
426
+ removeTomlSections(CODEX_CONFIG_PATH, {
427
+ exact: new Set(),
428
+ prefixes: [`hooks.state."${CODEX_PLUGIN_ID}:`],
429
+ });
430
+ const existing = existsSync(CODEX_CONFIG_PATH)
431
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
432
+ : "";
433
+ let next = existing;
434
+ if (next && !next.endsWith("\n")) next += "\n";
435
+ if (!next.includes("[hooks.state]")) {
436
+ if (next.trim()) next += "\n";
437
+ next += "[hooks.state]\n";
438
+ }
439
+ if (next.trim()) next += "\n";
440
+ for (const [key, currentHash] of entries.sort(([a], [b]) => a.localeCompare(b))) {
441
+ next += `[hooks.state.${tomlDottedQuoted(key)}]\n`;
442
+ next += "enabled = true\n";
443
+ next += `trusted_hash = "${currentHash}"\n\n`;
444
+ }
445
+ mkdirSync(dirname(CODEX_CONFIG_PATH), { recursive: true });
446
+ writeFileSync(CODEX_CONFIG_PATH, next.trimEnd() + "\n");
447
+ return true;
448
+ }
449
+
450
+ function createCodexAppServerClient(child) {
451
+ // A single long-lived stdout listener that demultiplexes JSON-RPC responses
452
+ // by id. Avoids losing messages between sequential requests.
453
+ const pending = new Map();
454
+ let buffer = "";
455
+ let exited = false;
456
+
457
+ const onData = (chunk) => {
458
+ buffer += chunk.toString();
459
+ let newline;
460
+ while ((newline = buffer.indexOf("\n")) >= 0) {
461
+ const line = buffer.slice(0, newline);
462
+ buffer = buffer.slice(newline + 1);
463
+ if (!line.trim()) continue;
464
+ let message;
465
+ try {
466
+ message = JSON.parse(line);
467
+ } catch {
468
+ continue;
469
+ }
470
+ const entry = pending.get(message.id);
471
+ if (!entry) continue;
472
+ pending.delete(message.id);
473
+ clearTimeout(entry.timer);
474
+ if (message.error) {
475
+ entry.reject(new Error(JSON.stringify(message.error)));
476
+ } else {
477
+ entry.resolve(message);
478
+ }
479
+ }
480
+ };
481
+ const onExit = () => {
482
+ exited = true;
483
+ for (const entry of pending.values()) {
484
+ clearTimeout(entry.timer);
485
+ entry.reject(new Error("Codex app-server exited before responding"));
486
+ }
487
+ pending.clear();
488
+ };
489
+ child.stdout.on("data", onData);
490
+ child.on("exit", onExit);
491
+
492
+ return {
493
+ request(id, method, params, timeoutMs) {
494
+ return new Promise((resolve, reject) => {
495
+ if (exited) {
496
+ reject(new Error("Codex app-server exited before responding"));
497
+ return;
498
+ }
499
+ const timer = setTimeout(() => {
500
+ pending.delete(id);
501
+ reject(new Error(`Codex app-server ${method} timed out`));
502
+ }, timeoutMs);
503
+ pending.set(id, { resolve, reject, timer });
504
+ child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
505
+ });
506
+ },
507
+ notify(method, params) {
508
+ if (exited) return;
509
+ child.stdin.write(JSON.stringify({ method, params }) + "\n");
510
+ },
511
+ close() {
512
+ child.stdout.off("data", onData);
513
+ child.off("exit", onExit);
514
+ },
515
+ };
516
+ }
517
+
518
+ async function listCodexPluginHooks(cwd) {
519
+ const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
520
+ stdio: ["pipe", "pipe", "ignore"],
521
+ });
522
+ const client = createCodexAppServerClient(child);
523
+ try {
524
+ await client.request(
525
+ 1,
526
+ "initialize",
527
+ {
528
+ clientInfo: {
529
+ name: "claude_smart_installer",
530
+ title: "claude-smart installer",
531
+ version: "0.0.0",
532
+ },
533
+ capabilities: { experimentalApi: true },
534
+ },
535
+ CODEX_CLI_TIMEOUT_MS,
536
+ );
537
+ client.notify("initialized", {});
538
+ const response = await client.request(
539
+ 2,
540
+ "hooks/list",
541
+ { cwds: [cwd] },
542
+ CODEX_CLI_TIMEOUT_MS,
543
+ );
544
+ const hooks = response.result?.data?.[0]?.hooks;
545
+ if (!Array.isArray(hooks)) {
546
+ throw new Error("Codex app-server hook metadata was malformed");
547
+ }
548
+ return hooks.filter(
549
+ (hook) =>
550
+ hook &&
551
+ (hook.pluginId === CODEX_PLUGIN_ID ||
552
+ String(hook.key || "").startsWith(`${CODEX_PLUGIN_ID}:`)),
553
+ );
554
+ } finally {
555
+ client.close();
556
+ child.stdin.destroy();
557
+ child.stdout.destroy();
558
+ child.kill("SIGTERM");
559
+ child.unref();
560
+ }
561
+ }
562
+
563
+ async function trustCodexPluginHooks(cwd) {
564
+ const hooks = await listCodexPluginHooks(cwd);
565
+ const states = {};
566
+ for (const hook of hooks) {
567
+ if (
568
+ typeof hook.key === "string" &&
569
+ hook.key.startsWith(`${CODEX_PLUGIN_ID}:`) &&
570
+ typeof hook.currentHash === "string"
571
+ ) {
572
+ states[hook.key] = hook.currentHash;
573
+ }
574
+ }
575
+ if (Object.keys(states).length === 0) {
576
+ throw new Error("Codex did not report trust hashes for claude-smart hooks");
577
+ }
578
+ if (!setCodexHookStates(states)) {
579
+ throw new Error(`could not write claude-smart hook trust state to ${CODEX_CONFIG_PATH}`);
580
+ }
581
+ return Object.keys(states).length;
582
+ }
583
+
584
+ function codexPluginVersion(pluginRoot) {
585
+ try {
586
+ const manifest = JSON.parse(
587
+ readFileSync(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"),
588
+ );
589
+ return typeof manifest.version === "string" && manifest.version
590
+ ? manifest.version
591
+ : null;
592
+ } catch {
593
+ return null;
594
+ }
595
+ }
596
+
597
+ function installCodexPluginCache(pluginRoot) {
598
+ const version = codexPluginVersion(pluginRoot);
599
+ if (!version) {
600
+ throw new Error(`missing version in ${join(pluginRoot, ".codex-plugin", "plugin.json")}`);
601
+ }
602
+ const cacheDir = join(CODEX_PLUGIN_CACHE_DIR, version);
603
+ rmSync(cacheDir, { recursive: true, force: true });
604
+ mkdirSync(dirname(cacheDir), { recursive: true });
605
+ cpSync(pluginRoot, cacheDir, {
606
+ recursive: true,
607
+ force: true,
608
+ verbatimSymlinks: false,
609
+ });
610
+ setCodexPluginEnabled();
611
+ return cacheDir;
612
+ }
613
+
347
614
  async function runUpdate() {
348
615
  if (!hasClaudeCli()) {
349
616
  process.stderr.write(
@@ -470,10 +737,59 @@ async function runInstallCodex() {
470
737
  process.exit(code);
471
738
  }
472
739
 
473
- code = await runCodex(["features", "enable", "plugin_hooks"]);
474
- if (code !== 0) {
475
- process.stderr.write("error: could not enable Codex plugin_hooks feature.\n");
476
- process.exit(code);
740
+ for (const feature of ["hooks", "plugin_hooks"]) {
741
+ code = await runCodex(["features", "enable", feature]);
742
+ if (code !== 0) {
743
+ // Older Codex builds may not recognize the `hooks` feature name; fall
744
+ // through to writing the flag directly under [features] in config.toml.
745
+ try {
746
+ setTomlFeature(feature, true);
747
+ process.stdout.write(`Enabled Codex ${feature} via ${CODEX_CONFIG_PATH}.\n`);
748
+ } catch (err) {
749
+ process.stderr.write(
750
+ `error: could not enable Codex ${feature} feature: ${err && err.message ? err.message : err}\n`,
751
+ );
752
+ process.exit(code);
753
+ }
754
+ }
755
+ }
756
+
757
+ let cacheDir = null;
758
+ let trustedHookCount = 0;
759
+ let trustError = null;
760
+ try {
761
+ cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
762
+ process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
763
+ } catch (err) {
764
+ process.stderr.write(
765
+ `error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
766
+ );
767
+ process.stderr.write(
768
+ `Open Codex, run /plugins, install claude-smart from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex.\n`,
769
+ );
770
+ process.exit(1);
771
+ }
772
+
773
+ for (let attempt = 0; attempt < 2; attempt++) {
774
+ try {
775
+ trustedHookCount = await trustCodexPluginHooks(process.cwd());
776
+ trustError = null;
777
+ break;
778
+ } catch (err) {
779
+ trustError = err;
780
+ if (attempt === 0) await new Promise((r) => setTimeout(r, 500));
781
+ }
782
+ }
783
+ if (trustError) {
784
+ process.stderr.write(
785
+ `warning: ${trustError && trustError.message ? trustError.message : trustError}\n`,
786
+ );
787
+ process.stderr.write(
788
+ `Fully quit and reopen Codex in this repo, run /hooks, trust the claude-smart hooks, and restart Codex.\n`,
789
+ );
790
+ process.exit(1);
791
+ } else {
792
+ process.stdout.write(`Trusted and enabled ${trustedHookCount} claude-smart Codex hooks.\n`);
477
793
  }
478
794
 
479
795
  const added = seedReflexioEnv();
@@ -484,8 +800,8 @@ async function runInstallCodex() {
484
800
  process.stdout.write(
485
801
  [
486
802
  "",
487
- "claude-smart Codex support is prepared.",
488
- `Fully quit and reopen Codex, run /plugins, install claude-smart from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, then restart Codex so hooks reload.`,
803
+ "claude-smart Codex support is installed.",
804
+ `Restart Codex so the installed plugin and trusted hooks reload. /plugins should show claude-smart as installed from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace.`,
489
805
  "Local data is shared with Claude Code under ~/.reflexio/ and ~/.claude-smart/.",
490
806
  "",
491
807
  ].join("\n"),
@@ -511,7 +827,7 @@ async function runUninstallCodex() {
511
827
  [
512
828
  "",
513
829
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply.",
514
- "Codex's global plugin_hooks feature and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
830
+ "Codex's global hook feature flags and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
515
831
  "",
516
832
  ].join("\n"),
517
833
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
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.24",
3
+ "version": "0.2.26",
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.24",
3
+ "version": "0.2.26",
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",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.24"
3
+ version = "0.2.26"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: claude-smart
3
+ description: Codex-only — use when the user asks to run claude-smart commands in Codex, including claude-smart show, learn, restart, dashboard, clear-all, or slash-like requests such as /claude-smart:learn. In Claude Code the native plugin slash commands handle these; do not invoke this skill there.
4
+ ---
5
+
6
+ # claude-smart Commands In Codex
7
+
8
+ Codex does not currently support plugin-provided slash commands. When the user
9
+ asks for a claude-smart command, run the equivalent shell command through the
10
+ active plugin root. This skill is Codex-specific; under Claude Code the native
11
+ `/claude-smart:*` slash commands already exist, so do not fall back to the
12
+ shell commands there.
13
+
14
+ ## Command Map
15
+
16
+ - Dashboard: `bash ~/.reflexio/plugin-root/scripts/dashboard-open.sh`
17
+ - Show learned state: `bash ~/.reflexio/plugin-root/scripts/cli.sh show`
18
+ - Learn from the latest turn: `bash ~/.reflexio/plugin-root/scripts/cli.sh learn`
19
+ - Learn with a note: `bash ~/.reflexio/plugin-root/scripts/cli.sh learn --note "<note>"`
20
+ - Restart backend/dashboard: `bash ~/.reflexio/plugin-root/scripts/cli.sh restart`
21
+ - Clear all learned state: `bash ~/.reflexio/plugin-root/scripts/cli.sh clear-all --yes`
22
+
23
+ ## Behavior
24
+
25
+ 1. Treat slash-like prompts such as `/claude-smart:show` as requests to run the
26
+ matching shell command above.
27
+ 2. For `learn`, preserve any user-provided note exactly as the note text.
28
+ 3. For `clear-all`, require explicit confirmation before running it because it
29
+ deletes all reflexio interactions, preferences, and skills.
30
+ 4. If `~/.reflexio/plugin-root` is missing or broken, tell the user to restart
31
+ Codex after installing claude-smart, then rerun the command.
32
+ 5. After running a command, summarize the important output concisely.
@@ -25,6 +25,7 @@ import argparse
25
25
  import json
26
26
  import os
27
27
  import re
28
+ import select
28
29
  import shutil
29
30
  import subprocess
30
31
  import sys
@@ -315,6 +316,244 @@ def _remove_toml_sections(
315
316
  return True
316
317
 
317
318
 
319
+ def _set_codex_plugin_enabled(path: Path) -> bool:
320
+ """Write Codex's installed-plugin config section for claude-smart."""
321
+ section_name = f'plugins."{_CODEX_PLUGIN_ID}"'
322
+ if not _remove_toml_sections(path, exact={section_name}):
323
+ return False
324
+ try:
325
+ text = path.read_text() if path.exists() else ""
326
+ except OSError:
327
+ return False
328
+ if text and not text.endswith("\n"):
329
+ text += "\n"
330
+ if text.strip():
331
+ text += "\n"
332
+ text += f"[{section_name}]\nenabled = true\n"
333
+ try:
334
+ path.parent.mkdir(parents=True, exist_ok=True)
335
+ path.write_text(text)
336
+ except OSError:
337
+ return False
338
+ return True
339
+
340
+
341
+ def _toml_dotted_quoted(name: str) -> str:
342
+ """Quote a TOML bare-key segment whose name may contain ``"`` or ``\\``."""
343
+ return '"' + name.replace("\\", "\\\\").replace('"', '\\"') + '"'
344
+
345
+
346
+ def _set_codex_hook_states(path: Path, states: dict[str, str]) -> bool:
347
+ """Trust and enable Codex hook state entries for the given hook hashes."""
348
+ if not states:
349
+ return False
350
+ if not _remove_toml_sections(
351
+ path,
352
+ exact=set(),
353
+ prefixes=(f'hooks.state."{_CODEX_PLUGIN_ID}:',),
354
+ ):
355
+ return False
356
+ try:
357
+ text = path.read_text() if path.exists() else ""
358
+ except OSError:
359
+ return False
360
+ if text and not text.endswith("\n"):
361
+ text += "\n"
362
+ if "[hooks.state]" not in text:
363
+ if text.strip():
364
+ text += "\n"
365
+ text += "[hooks.state]\n"
366
+ if text.strip():
367
+ text += "\n"
368
+ for key, current_hash in sorted(states.items()):
369
+ text += (
370
+ f"[hooks.state.{_toml_dotted_quoted(key)}]\n"
371
+ "enabled = true\n"
372
+ f'trusted_hash = "{current_hash}"\n\n'
373
+ )
374
+ try:
375
+ path.parent.mkdir(parents=True, exist_ok=True)
376
+ path.write_text(text.rstrip() + "\n")
377
+ except OSError:
378
+ return False
379
+ return True
380
+
381
+
382
+ def _read_codex_app_server_response(
383
+ proc: subprocess.Popen[str], response_id: int, deadline: float
384
+ ) -> dict[str, object]:
385
+ """Read JSON-RPC lines from ``proc.stdout`` until ``response_id`` arrives.
386
+
387
+ Skips unrelated notifications and non-JSON output. Raises ``RuntimeError``
388
+ if the child exits, ``TimeoutError`` once ``deadline`` is reached, or
389
+ re-raises Codex's own error payload as ``RuntimeError``.
390
+ """
391
+ if proc.stdout is None:
392
+ raise RuntimeError("Codex app-server stdout pipe is not available")
393
+ while time.monotonic() < deadline:
394
+ ready, _, _ = select.select([proc.stdout], [], [], 0.2)
395
+ if not ready:
396
+ if proc.poll() is not None:
397
+ raise RuntimeError("Codex app-server exited before responding")
398
+ continue
399
+ line = proc.stdout.readline()
400
+ if not line:
401
+ continue
402
+ try:
403
+ message = json.loads(line)
404
+ except json.JSONDecodeError:
405
+ continue
406
+ if message.get("id") == response_id:
407
+ if "error" in message:
408
+ raise RuntimeError(str(message["error"]))
409
+ return message
410
+ raise TimeoutError("Codex app-server did not respond in time")
411
+
412
+
413
+ def _codex_app_server_request(
414
+ proc: subprocess.Popen[str],
415
+ response_id: int,
416
+ method: str,
417
+ params: dict[str, object],
418
+ deadline: float,
419
+ ) -> dict[str, object]:
420
+ """Send one JSON-RPC request to the Codex app-server and await the response."""
421
+ if proc.stdin is None:
422
+ raise RuntimeError("Codex app-server stdin pipe is not available")
423
+ proc.stdin.write(
424
+ json.dumps({"id": response_id, "method": method, "params": params})
425
+ )
426
+ proc.stdin.write("\n")
427
+ proc.stdin.flush()
428
+ return _read_codex_app_server_response(proc, response_id, deadline)
429
+
430
+
431
+ def _list_codex_plugin_hooks(cwd: Path) -> tuple[bool, list[dict[str, object]], str]:
432
+ """Ask Codex for discovered hook metadata, including current trust hashes."""
433
+ try:
434
+ popen = subprocess.Popen(
435
+ ["codex", "app-server", "--listen", "stdio://"],
436
+ stdin=subprocess.PIPE,
437
+ stdout=subprocess.PIPE,
438
+ stderr=subprocess.DEVNULL,
439
+ text=True,
440
+ )
441
+ except OSError as exc:
442
+ return False, [], f"could not start Codex app-server: {exc}"
443
+
444
+ proc = popen
445
+ try:
446
+ deadline = time.monotonic() + _CODEX_CLI_TIMEOUT_SECONDS
447
+ _codex_app_server_request(
448
+ proc,
449
+ 1,
450
+ "initialize",
451
+ {
452
+ "clientInfo": {
453
+ "name": "claude_smart_installer",
454
+ "title": "claude-smart installer",
455
+ "version": "0.0.0",
456
+ },
457
+ "capabilities": {"experimentalApi": True},
458
+ },
459
+ deadline,
460
+ )
461
+ if proc.stdin is None:
462
+ return False, [], "Codex app-server stdin pipe is not available"
463
+ proc.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n")
464
+ proc.stdin.flush()
465
+ response = _codex_app_server_request(
466
+ proc,
467
+ 2,
468
+ "hooks/list",
469
+ {"cwds": [str(cwd)]},
470
+ deadline,
471
+ )
472
+ except (OSError, RuntimeError, TimeoutError) as exc:
473
+ return False, [], str(exc)
474
+ finally:
475
+ proc.terminate()
476
+ try:
477
+ proc.wait(timeout=2)
478
+ except subprocess.TimeoutExpired:
479
+ proc.kill()
480
+ proc.wait(timeout=2)
481
+
482
+ result = response.get("result")
483
+ data = result.get("data") if isinstance(result, dict) else None
484
+ if not isinstance(data, list) or not data:
485
+ return False, [], "Codex app-server returned no hook metadata"
486
+ hooks = data[0].get("hooks") if isinstance(data[0], dict) else None
487
+ if not isinstance(hooks, list):
488
+ return False, [], "Codex app-server hook metadata was malformed"
489
+ plugin_hooks = [
490
+ hook
491
+ for hook in hooks
492
+ if isinstance(hook, dict)
493
+ and (
494
+ hook.get("pluginId") == _CODEX_PLUGIN_ID
495
+ or str(hook.get("key", "")).startswith(f"{_CODEX_PLUGIN_ID}:")
496
+ )
497
+ ]
498
+ return True, plugin_hooks, f"found {len(plugin_hooks)} claude-smart hooks"
499
+
500
+
501
+ def _trust_codex_plugin_hooks(cwd: Path) -> tuple[bool, str]:
502
+ """Seed Codex per-hook trust state for the installed claude-smart plugin."""
503
+ ok, hooks, message = _list_codex_plugin_hooks(cwd)
504
+ if not ok:
505
+ return False, message
506
+ states: dict[str, str] = {}
507
+ for hook in hooks:
508
+ key = hook.get("key")
509
+ current_hash = hook.get("currentHash")
510
+ if (
511
+ isinstance(key, str)
512
+ and key.startswith(f"{_CODEX_PLUGIN_ID}:")
513
+ and isinstance(current_hash, str)
514
+ ):
515
+ states[key] = current_hash
516
+ if not states:
517
+ return False, "Codex did not report trust hashes for claude-smart hooks"
518
+ if not _set_codex_hook_states(_CODEX_CONFIG_PATH, states):
519
+ return (
520
+ False,
521
+ f"could not write claude-smart hook trust state to {_CODEX_CONFIG_PATH}",
522
+ )
523
+ return True, f"trusted and enabled {len(states)} claude-smart Codex hooks"
524
+
525
+
526
+ def _codex_plugin_version(plugin_root: Path) -> str | None:
527
+ try:
528
+ manifest = json.loads(
529
+ (plugin_root / ".codex-plugin" / "plugin.json").read_text()
530
+ )
531
+ except (OSError, json.JSONDecodeError):
532
+ return None
533
+ version = manifest.get("version")
534
+ return version if isinstance(version, str) and version else None
535
+
536
+
537
+ def _install_codex_plugin_cache(plugin_root: Path) -> tuple[bool, str]:
538
+ """Best-effort local install equivalent to selecting Install in /plugins."""
539
+ version = _codex_plugin_version(plugin_root)
540
+ if not version:
541
+ return (
542
+ False,
543
+ f"missing version in {plugin_root / '.codex-plugin' / 'plugin.json'}",
544
+ )
545
+ cache_dir = _CODEX_PLUGIN_CACHE_DIR / version
546
+ try:
547
+ shutil.rmtree(cache_dir, ignore_errors=True)
548
+ cache_dir.parent.mkdir(parents=True, exist_ok=True)
549
+ shutil.copytree(plugin_root, cache_dir, symlinks=False, ignore=_COPYTREE_IGNORE)
550
+ except OSError as exc:
551
+ return False, f"could not write Codex plugin cache: {exc}"
552
+ if not _set_codex_plugin_enabled(_CODEX_CONFIG_PATH):
553
+ return False, f"could not enable {_CODEX_PLUGIN_ID} in {_CODEX_CONFIG_PATH}"
554
+ return True, f"installed Codex plugin cache at {cache_dir}"
555
+
556
+
318
557
  def _cleanup_codex_install_state() -> bool:
319
558
  """Remove Codex's local install artifacts while preserving shared learning data.
320
559
 
@@ -348,21 +587,28 @@ def _cleanup_codex_install_state() -> bool:
348
587
 
349
588
 
350
589
  def _enable_codex_plugin_hooks() -> tuple[bool, str]:
351
- """Enable Codex's ``plugin_hooks`` feature, falling back to editing config.toml.
590
+ """Enable Codex hook feature flags, falling back to editing config.toml.
352
591
 
353
592
  Returns:
354
593
  tuple[bool, str]: ``(success, message)``. The message describes
355
- either the successful path taken (CLI vs. direct config write)
356
- or the failure mode.
594
+ either the successful path taken (CLI vs. direct config write)
595
+ or the failure mode.
357
596
  """
358
- result = _run_codex(["features", "enable", "plugin_hooks"])
359
- if result.returncode == 0:
360
- return True, "codex features enable plugin_hooks"
361
- if _set_toml_feature(_CODEX_CONFIG_PATH, "plugin_hooks", True):
362
- return True, f"set plugin_hooks = true in {_CODEX_CONFIG_PATH}"
363
- return False, (
364
- result.stderr or result.stdout or "could not update Codex config"
365
- ).strip()
597
+ messages: list[str] = []
598
+ for feature in ("hooks", "plugin_hooks"):
599
+ result = _run_codex(["features", "enable", feature])
600
+ if result.returncode == 0:
601
+ messages.append(f"codex features enable {feature}")
602
+ continue
603
+ if _set_toml_feature(_CODEX_CONFIG_PATH, feature, True):
604
+ messages.append(f"set {feature} = true in {_CODEX_CONFIG_PATH}")
605
+ continue
606
+ detail = (
607
+ result.stderr or result.stdout or "could not update Codex config"
608
+ ).strip()
609
+ prior = f" (succeeded earlier: {'; '.join(messages)})" if messages else ""
610
+ return False, f"{feature}: {detail}{prior}"
611
+ return True, "; ".join(messages)
366
612
 
367
613
 
368
614
  def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
@@ -446,14 +692,49 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
446
692
  else:
447
693
  sys.stderr.write(f"warning: {registration_msg}\n")
448
694
 
695
+ installed = False
696
+ install_msg = "marketplace registration failed"
697
+ trusted = False
698
+ trust_msg = "plugin was not installed"
449
699
  if registered:
700
+ installed, install_msg = _install_codex_plugin_cache(
701
+ marketplace_root / _CODEX_LOCAL_PLUGIN_PATH
702
+ )
703
+ if installed:
704
+ sys.stdout.write(f"{install_msg}.\n")
705
+ trusted, trust_msg = _trust_codex_plugin_hooks(Path.cwd())
706
+ if not trusted:
707
+ # The app-server can race against the just-installed cache.
708
+ # Retry once before giving up; the manual /hooks recovery is
709
+ # available either way.
710
+ time.sleep(0.5)
711
+ trusted, trust_msg = _trust_codex_plugin_hooks(Path.cwd())
712
+ if trusted:
713
+ sys.stdout.write(f"{trust_msg}.\n")
714
+ else:
715
+ sys.stderr.write(f"warning: {trust_msg}\n")
716
+ else:
717
+ sys.stderr.write(f"warning: {install_msg}\n")
718
+
719
+ if registered and installed and trusted:
720
+ sys.stdout.write(
721
+ "\nclaude-smart Codex support is installed.\n"
722
+ "Restart Codex so the installed plugin and trusted hooks reload. /plugins should "
723
+ f"show claude-smart as installed from the {_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace. "
724
+ "Uninstall removes the plugin cache and marketplace registration but leaves "
725
+ "shared claude-smart data and Codex's global hook feature flags intact.\n"
726
+ )
727
+ elif registered and installed:
728
+ sys.stdout.write(
729
+ "\nclaude-smart Codex support is installed, but hook trust could not be completed.\n"
730
+ "Fully quit and reopen Codex in this repo, run /hooks, trust the claude-smart hooks, "
731
+ "and restart Codex so hooks reload.\n"
732
+ )
733
+ elif registered:
450
734
  sys.stdout.write(
451
- "\nclaude-smart Codex support is prepared.\n"
735
+ "\nclaude-smart Codex marketplace is prepared, but automatic plugin install failed.\n"
452
736
  "Fully quit and reopen Codex in this repo, run /plugins, install claude-smart from "
453
- f"the {_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace if it is not already installed, "
454
- "then restart Codex so hooks reload. Uninstall removes the marketplace "
455
- "registration but leaves shared claude-smart data and Codex's global "
456
- "plugin_hooks feature intact.\n"
737
+ f"the {_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex so hooks reload.\n"
457
738
  )
458
739
  else:
459
740
  sys.stdout.write(
@@ -462,7 +743,7 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
462
743
  "then fully quit and reopen Codex, run /plugins, install claude-smart from the "
463
744
  f"{_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex so hooks reload.\n"
464
745
  )
465
- return 0 if hooks_ok and registered else 1
746
+ return 0 if hooks_ok and registered and installed and trusted else 1
466
747
 
467
748
 
468
749
  def cmd_install(args: argparse.Namespace) -> int:
@@ -605,7 +886,7 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
605
886
  sys.stderr.write(f"warning: could not update {_CODEX_CONFIG_PATH}\n")
606
887
  sys.stdout.write(
607
888
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply. "
608
- "Codex's global plugin_hooks feature and local data under ~/.reflexio "
889
+ "Codex's global hook feature flags and local data under ~/.reflexio "
609
890
  "and ~/.claude-smart were left in place.\n"
610
891
  )
611
892
  return 0
package/plugin/uv.lock CHANGED
@@ -419,7 +419,7 @@ wheels = [
419
419
 
420
420
  [[package]]
421
421
  name = "claude-smart"
422
- version = "0.2.24"
422
+ version = "0.2.26"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },