claude-smart 0.2.25 → 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.25-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,66 +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, enables
122
- Codex's `plugin_hooks` feature, installs `claude-smart` into Codex's local
123
- plugin cache, and enables it in `~/.codex/config.toml`. Hooks are not active in
124
- already-running Codex windows; after the command finishes:
125
-
126
- 1. Fully quit and reopen Codex in your project so hooks reload.
127
- 2. Run `/plugins` only if you want to verify `claude-smart` shows as installed
128
- from the **ReflexioAI** marketplace.
129
-
130
- If you install or toggle `claude-smart` manually from `/plugins`, still run
131
- `npx claude-smart install --host codex` once afterward so `plugin_hooks` is
132
- enabled and the cache/config are prepared.
133
-
134
- Do not create a `~/plugins/claude-smart` symlink for a normal `npx` install;
135
- that symlink is only for plugin development from a cloned checkout.
136
-
137
- Installing from a clone is only for plugin development; see
138
- [DEVELOPER.md](./DEVELOPER.md#developing-locally).
139
-
140
- Codex and Claude Code intentionally share the same `CLAUDE_SMART_*` environment
141
- variables, `~/.reflexio/` data, `~/.claude-smart/` session buffers, backend,
142
- dashboard, and learned skills/preferences.
120
+ Then fully quit and reopen Codex so hooks reload.
143
121
 
122
+ Requires the `codex` CLI on `PATH` and Node.js (for `npx`).
144
123
 
145
124
  To uninstall:
146
125
 
@@ -148,11 +127,10 @@ To uninstall:
148
127
  npx claude-smart uninstall --host codex
149
128
  ```
150
129
 
151
- This removes the Codex marketplace registration, installed plugin config, and
152
- Codex plugin cache. Local data under `~/.reflexio/` and `~/.claude-smart/` is
153
- 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.
154
133
 
155
- Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-locally).
156
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.
157
135
 
158
136
  ---
@@ -200,9 +178,7 @@ Under the hood: hooks watch your turns, tool calls, and assistant replies, auto-
200
178
  ```
201
179
 
202
180
  That signals a preference (`p…`) or skill (`s…`) materially shaped the reply.
203
- Standalone wrappers like `✨abc123✨` are not claude-smart citations and will not
204
- link back to dashboard entries. Open the interaction's detail page in the
205
- [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.
206
182
 
207
183
  See [ARCHITECTURE.md](./ARCHITECTURE.md) for hooks, data flow, and reflexio details.
208
184
 
@@ -211,16 +187,17 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for hooks, data flow, and reflexio deta
211
187
  ## Commands
212
188
 
213
189
  Claude Code installs these as plugin slash commands. Codex does not currently
214
- support these plugin-provided slash commands, so run the equivalent shell command
215
- directly, or ask Codex to run it.
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.
216
193
 
217
- | Claude Code | Codex / shell | What it does |
194
+ | Claude Code | Codex request | What it does |
218
195
  | --- | --- | --- |
219
- | `/claude-smart: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. |
220
- | `/claude-smart: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. |
221
- | `/claude-smart: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. |
222
- | `/claude-smart: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). |
223
- | `/claude-smart: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. |
224
201
 
225
202
  ---
226
203
 
@@ -235,7 +212,7 @@ Advanced users can tune claude-smart via environment variables — see [DEVELOPE
235
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`. |
236
213
  | `~/.reflexio/.env` | Provider config — `CLAUDE_SMART_USE_LOCAL_CLI`, `CLAUDE_SMART_USE_LOCAL_EMBEDDING`, any optional API keys. |
237
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. |
238
- | `~/.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`. |
239
216
  | `~/.codex/plugins/cache/reflexioai/claude-smart/<version>/` | Codex's cached install of the `claude-smart` plugin from the `ReflexioAI` marketplace. |
240
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. |
241
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. |
@@ -205,9 +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",
208
+ " 3. codex features enable hooks && codex features enable plugin_hooks",
209
209
  " 4. Installs claude-smart into Codex's plugin cache and enables it",
210
- " 5. Restart Codex.",
210
+ " 5. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
211
+ " 6. Restart Codex.",
211
212
  "",
212
213
  "Update:",
213
214
  " npx claude-smart update Update to the latest version",
@@ -359,6 +360,227 @@ function setCodexPluginEnabled() {
359
360
  writeFileSync(CODEX_CONFIG_PATH, next);
360
361
  }
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
+
362
584
  function codexPluginVersion(pluginRoot) {
363
585
  try {
364
586
  const manifest = JSON.parse(
@@ -515,13 +737,26 @@ async function runInstallCodex() {
515
737
  process.exit(code);
516
738
  }
517
739
 
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);
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
+ }
522
755
  }
523
756
 
524
757
  let cacheDir = null;
758
+ let trustedHookCount = 0;
759
+ let trustError = null;
525
760
  try {
526
761
  cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
527
762
  process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
@@ -530,9 +765,31 @@ async function runInstallCodex() {
530
765
  `error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
531
766
  );
532
767
  process.stderr.write(
533
- `Open Codex, run /plugins, and install claude-smart from the ${CODEX_MARKETPLACE_DISPLAY_NAME} marketplace manually.\n`,
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`,
534
789
  );
535
790
  process.exit(1);
791
+ } else {
792
+ process.stdout.write(`Trusted and enabled ${trustedHookCount} claude-smart Codex hooks.\n`);
536
793
  }
537
794
 
538
795
  const added = seedReflexioEnv();
@@ -544,7 +801,7 @@ async function runInstallCodex() {
544
801
  [
545
802
  "",
546
803
  "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.`,
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.`,
548
805
  "Local data is shared with Claude Code under ~/.reflexio/ and ~/.claude-smart/.",
549
806
  "",
550
807
  ].join("\n"),
@@ -570,7 +827,7 @@ async function runUninstallCodex() {
570
827
  [
571
828
  "",
572
829
  "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.",
830
+ "Codex's global hook feature flags and local data under ~/.reflexio/ and ~/.claude-smart/ were left in place.",
574
831
  "",
575
832
  ].join("\n"),
576
833
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.25",
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.25",
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.25",
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.25"
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
@@ -337,6 +338,191 @@ def _set_codex_plugin_enabled(path: Path) -> bool:
337
338
  return True
338
339
 
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
+
340
526
  def _codex_plugin_version(plugin_root: Path) -> str | None:
341
527
  try:
342
528
  manifest = json.loads(
@@ -401,21 +587,28 @@ def _cleanup_codex_install_state() -> bool:
401
587
 
402
588
 
403
589
  def _enable_codex_plugin_hooks() -> tuple[bool, str]:
404
- """Enable Codex's ``plugin_hooks`` feature, falling back to editing config.toml.
590
+ """Enable Codex hook feature flags, falling back to editing config.toml.
405
591
 
406
592
  Returns:
407
593
  tuple[bool, str]: ``(success, message)``. The message describes
408
- either the successful path taken (CLI vs. direct config write)
409
- or the failure mode.
594
+ either the successful path taken (CLI vs. direct config write)
595
+ or the failure mode.
410
596
  """
411
- result = _run_codex(["features", "enable", "plugin_hooks"])
412
- if result.returncode == 0:
413
- return True, "codex features enable plugin_hooks"
414
- if _set_toml_feature(_CODEX_CONFIG_PATH, "plugin_hooks", True):
415
- return True, f"set plugin_hooks = true in {_CODEX_CONFIG_PATH}"
416
- return False, (
417
- result.stderr or result.stdout or "could not update Codex config"
418
- ).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)
419
612
 
420
613
 
421
614
  def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
@@ -501,22 +694,41 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
501
694
 
502
695
  installed = False
503
696
  install_msg = "marketplace registration failed"
697
+ trusted = False
698
+ trust_msg = "plugin was not installed"
504
699
  if registered:
505
700
  installed, install_msg = _install_codex_plugin_cache(
506
701
  marketplace_root / _CODEX_LOCAL_PLUGIN_PATH
507
702
  )
508
703
  if installed:
509
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")
510
716
  else:
511
717
  sys.stderr.write(f"warning: {install_msg}\n")
512
718
 
513
- if registered and installed:
719
+ if registered and installed and trusted:
514
720
  sys.stdout.write(
515
721
  "\nclaude-smart Codex support is installed.\n"
516
- "Restart Codex so the installed plugin and hooks reload. /plugins should "
722
+ "Restart Codex so the installed plugin and trusted hooks reload. /plugins should "
517
723
  f"show claude-smart as installed from the {_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace. "
518
724
  "Uninstall removes the plugin cache and marketplace registration but leaves "
519
- "shared claude-smart data and Codex's global plugin_hooks feature intact.\n"
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"
520
732
  )
521
733
  elif registered:
522
734
  sys.stdout.write(
@@ -531,7 +743,7 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
531
743
  "then fully quit and reopen Codex, run /plugins, install claude-smart from the "
532
744
  f"{_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex so hooks reload.\n"
533
745
  )
534
- return 0 if hooks_ok and registered and installed else 1
746
+ return 0 if hooks_ok and registered and installed and trusted else 1
535
747
 
536
748
 
537
749
  def cmd_install(args: argparse.Namespace) -> int:
@@ -674,7 +886,7 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
674
886
  sys.stderr.write(f"warning: could not update {_CODEX_CONFIG_PATH}\n")
675
887
  sys.stdout.write(
676
888
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply. "
677
- "Codex's global plugin_hooks feature and local data under ~/.reflexio "
889
+ "Codex's global hook feature flags and local data under ~/.reflexio "
678
890
  "and ~/.claude-smart were left in place.\n"
679
891
  )
680
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.25"
422
+ version = "0.2.26"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },