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 +33 -50
- package/bin/claude-smart.js +325 -9
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +2 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/skills/claude-smart/SKILL.md +32 -0
- package/plugin/src/claude_smart/cli.py +299 -18
- package/plugin/uv.lock +1 -1
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.
|
|
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
|
|
85
|
-
claude plugin install claude-smart@reflexioai
|
|
84
|
+
npx claude-smart install # or: uvx claude-smart install
|
|
86
85
|
```
|
|
87
86
|
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
94
|
+
claude plugin marketplace add ReflexioAI/claude-smart
|
|
95
|
+
claude plugin install claude-smart@reflexioai
|
|
97
96
|
```
|
|
98
97
|
|
|
99
|
-
|
|
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
|
|
105
|
+
npx claude-smart uninstall # or: uvx claude-smart uninstall
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
Or, if
|
|
108
|
+
Or, if installed via the plugin marketplace:
|
|
108
109
|
|
|
109
110
|
```bash
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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.
|
|
209
|
-
|
|
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
|
|
194
|
+
| Claude Code | Codex request | What it does |
|
|
212
195
|
| --- | --- | --- |
|
|
213
|
-
| `/dashboard` | `
|
|
214
|
-
| `/show` | `
|
|
215
|
-
| `/learn [note]` | `
|
|
216
|
-
| `/restart` | `
|
|
217
|
-
| `/clear-all` | `
|
|
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,
|
|
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`).
|
|
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
|
|
package/bin/claude-smart.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
488
|
-
`
|
|
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
|
|
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.
|
|
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",
|
package/plugin/pyproject.toml
CHANGED
|
@@ -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
|
|
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
|
-
|
|
356
|
-
|
|
594
|
+
either the successful path taken (CLI vs. direct config write)
|
|
595
|
+
or the failure mode.
|
|
357
596
|
"""
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|