@suwujs/codex-vault 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/README.zh-CN.md +10 -3
- package/bin/cli.js +352 -5
- package/package.json +1 -1
- package/plugin/VERSION +1 -1
- package/plugin/hooks/classify-message.py +62 -15
- package/vault/.codex-vault/hooks/classify-message.py +62 -15
package/README.md
CHANGED
|
@@ -25,14 +25,22 @@ Agent: "You're working on the API redesign. Last session you decided
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# From your project directory:
|
|
28
|
-
|
|
29
|
-
bash /tmp/codex-vault/plugin/install.sh # integrated mode — installs vault + hooks into your project
|
|
28
|
+
npx @suwujs/codex-vault init
|
|
30
29
|
claude # or: codex
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
Fill in `vault/brain/North Star.md` with your goals, then start talking.
|
|
34
33
|
|
|
34
|
+
<details>
|
|
35
|
+
<summary>Alternative: install from source</summary>
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/sukbearai/codex-vault.git /tmp/codex-vault
|
|
39
|
+
bash /tmp/codex-vault/plugin/install.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
35
42
|
> **Standalone mode**: run `install.sh` from inside the codex-vault repo itself to use `vault/` as the working directory.
|
|
43
|
+
</details>
|
|
36
44
|
|
|
37
45
|
## How It Works
|
|
38
46
|
|
|
@@ -167,7 +175,6 @@ See [docs/usage.md](docs/usage.md) — 7 real scenarios from first session to pr
|
|
|
167
175
|
## Inspired By
|
|
168
176
|
|
|
169
177
|
- [llm-wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) by Karpathy — the pattern
|
|
170
|
-
- [obsidian-mind](https://github.com/sukbearai/obsidian-mind) — full-featured implementation for engineer performance tracking
|
|
171
178
|
|
|
172
179
|
## License
|
|
173
180
|
|
package/README.zh-CN.md
CHANGED
|
@@ -62,14 +62,22 @@ LLM agent 每次对话都从零开始。上次做的决策忘了,讨论过的
|
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
# 在你的项目目录下:
|
|
65
|
-
|
|
66
|
-
bash /tmp/codex-vault/plugin/install.sh # 集成模式 — 自动安装 vault + hooks
|
|
65
|
+
npx @suwujs/codex-vault init
|
|
67
66
|
claude # 或: codex
|
|
68
67
|
```
|
|
69
68
|
|
|
70
69
|
填好 `vault/brain/North Star.md`,开始对话。
|
|
71
70
|
|
|
71
|
+
<details>
|
|
72
|
+
<summary>其他安装方式:从源码安装</summary>
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/sukbearai/codex-vault.git /tmp/codex-vault
|
|
76
|
+
bash /tmp/codex-vault/plugin/install.sh
|
|
77
|
+
```
|
|
78
|
+
|
|
72
79
|
> **独立模式**:在 codex-vault 仓库内运行 `install.sh`,以 `vault/` 为工作目录。
|
|
80
|
+
</details>
|
|
73
81
|
|
|
74
82
|
## 工作原理
|
|
75
83
|
|
|
@@ -148,7 +156,6 @@ vault/
|
|
|
148
156
|
## 灵感来源
|
|
149
157
|
|
|
150
158
|
- [llm-wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) by Karpathy — 这个模式的起源
|
|
151
|
-
- [obsidian-mind](https://github.com/sukbearai/obsidian-mind) — 面向工程师效能追踪的完整实现
|
|
152
159
|
|
|
153
160
|
## License
|
|
154
161
|
|
package/bin/cli.js
CHANGED
|
@@ -28,8 +28,11 @@ switch (cmd) {
|
|
|
28
28
|
break;
|
|
29
29
|
|
|
30
30
|
case 'upgrade':
|
|
31
|
+
cmdUpgrade();
|
|
32
|
+
break;
|
|
33
|
+
|
|
31
34
|
case 'uninstall':
|
|
32
|
-
|
|
35
|
+
cmdUninstall();
|
|
33
36
|
break;
|
|
34
37
|
|
|
35
38
|
default:
|
|
@@ -43,20 +46,23 @@ function printHelp() {
|
|
|
43
46
|
|
|
44
47
|
Usage:
|
|
45
48
|
codex-vault init Install vault into current directory (default)
|
|
46
|
-
codex-vault upgrade Upgrade vault
|
|
47
|
-
codex-vault uninstall Remove vault
|
|
49
|
+
codex-vault upgrade Upgrade existing vault to latest version
|
|
50
|
+
codex-vault uninstall Remove vault integration (preserves vault data)
|
|
48
51
|
codex-vault -v, --version Print version
|
|
49
52
|
codex-vault -h, --help Print this help`);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
function
|
|
53
|
-
// Check bash availability
|
|
55
|
+
function assertBash() {
|
|
54
56
|
const bashCheck = spawnSync('bash', ['--version'], { stdio: 'ignore' });
|
|
55
57
|
if (bashCheck.error) {
|
|
56
58
|
console.error('Error: bash is not available.');
|
|
57
59
|
console.error('On Windows, please install Git Bash or WSL.');
|
|
58
60
|
process.exit(1);
|
|
59
61
|
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runInit() {
|
|
65
|
+
assertBash();
|
|
60
66
|
|
|
61
67
|
// Check if already installed
|
|
62
68
|
const versionFile = path.join(process.cwd(), 'vault', '.codex-vault', 'version');
|
|
@@ -88,3 +94,344 @@ function runInit() {
|
|
|
88
94
|
fs.writeFileSync(versionFile, VERSION + '\n');
|
|
89
95
|
console.log(`\ncodex-vault v${VERSION} installed successfully.`);
|
|
90
96
|
}
|
|
97
|
+
|
|
98
|
+
function cmdUpgrade() {
|
|
99
|
+
assertBash();
|
|
100
|
+
|
|
101
|
+
// Check if installed
|
|
102
|
+
const versionFile = path.join(process.cwd(), 'vault', '.codex-vault', 'version');
|
|
103
|
+
if (!fs.existsSync(versionFile)) {
|
|
104
|
+
console.error('codex-vault is not installed in this directory.');
|
|
105
|
+
console.error('Run "codex-vault init" first.');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
|
|
110
|
+
|
|
111
|
+
// Already up to date?
|
|
112
|
+
if (installedVersion === VERSION) {
|
|
113
|
+
console.log(`Already at v${VERSION}.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`Upgrading: v${installedVersion} → v${VERSION}`);
|
|
118
|
+
|
|
119
|
+
// Backup hooks directory
|
|
120
|
+
const hooksDir = path.join(process.cwd(), 'vault', '.codex-vault', 'hooks');
|
|
121
|
+
const backupDir = path.join(process.cwd(), 'vault', '.codex-vault', `backup-${installedVersion}`, 'hooks');
|
|
122
|
+
const backupRoot = path.join(process.cwd(), 'vault', '.codex-vault', `backup-${installedVersion}`);
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(hooksDir)) {
|
|
125
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
126
|
+
fs.cpSync(hooksDir, backupDir, { recursive: true });
|
|
127
|
+
console.log(`Hooks backed up to vault/.codex-vault/backup-${installedVersion}/hooks/`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Run install.sh
|
|
131
|
+
const result = spawnSync('bash', [INSTALL_SH], {
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
stdio: 'inherit',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (result.error) {
|
|
137
|
+
console.error('Failed to run install.sh:', result.error.message);
|
|
138
|
+
if (fs.existsSync(backupRoot)) {
|
|
139
|
+
console.error(`Backup available at: vault/.codex-vault/backup-${installedVersion}/`);
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (result.status !== 0) {
|
|
145
|
+
console.error('install.sh exited with errors.');
|
|
146
|
+
if (fs.existsSync(backupRoot)) {
|
|
147
|
+
console.error(`Backup available at: vault/.codex-vault/backup-${installedVersion}/`);
|
|
148
|
+
}
|
|
149
|
+
process.exit(result.status);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Update version file
|
|
153
|
+
fs.writeFileSync(versionFile, VERSION + '\n');
|
|
154
|
+
|
|
155
|
+
console.log(`\nUpgraded to v${VERSION} successfully.`);
|
|
156
|
+
if (fs.existsSync(backupRoot)) {
|
|
157
|
+
console.log(`Backup at: vault/.codex-vault/backup-${installedVersion}/`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// uninstall
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
function cmdUninstall() {
|
|
166
|
+
const cwd = process.cwd();
|
|
167
|
+
const versionFile = path.join(cwd, 'vault', '.codex-vault', 'version');
|
|
168
|
+
|
|
169
|
+
// 1. Check installation
|
|
170
|
+
if (!fs.existsSync(versionFile)) {
|
|
171
|
+
console.error('codex-vault is not installed in this directory.');
|
|
172
|
+
console.error('Nothing to uninstall.');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
|
|
177
|
+
console.log(`Uninstalling codex-vault v${installedVersion}...`);
|
|
178
|
+
console.log('NOTE: vault/ data (brain/, work/, sources/) is preserved.\n');
|
|
179
|
+
|
|
180
|
+
// 2. Remove vault/.codex-vault/ (hooks + version + backups)
|
|
181
|
+
const codexVaultDir = path.join(cwd, 'vault', '.codex-vault');
|
|
182
|
+
if (fs.existsSync(codexVaultDir)) {
|
|
183
|
+
fs.rmSync(codexVaultDir, { recursive: true, force: true });
|
|
184
|
+
console.log(' [x] Removed vault/.codex-vault/');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 3. Clean .claude/settings.json
|
|
188
|
+
cleanHooksJson(path.join(cwd, '.claude', 'settings.json'), '.claude/settings.json');
|
|
189
|
+
|
|
190
|
+
// 4. Clean .codex/hooks.json
|
|
191
|
+
cleanHooksJson(path.join(cwd, '.codex', 'hooks.json'), '.codex/hooks.json');
|
|
192
|
+
|
|
193
|
+
// 5. Clean .codex/config.toml
|
|
194
|
+
cleanCodexConfigToml(cwd);
|
|
195
|
+
|
|
196
|
+
// 6. Remove skills
|
|
197
|
+
const SKILL_NAMES = ['dump', 'ingest', 'recall', 'wrap-up'];
|
|
198
|
+
removeSkills(path.join(cwd, '.claude'), '.claude', SKILL_NAMES);
|
|
199
|
+
removeSkills(path.join(cwd, '.codex'), '.codex', SKILL_NAMES);
|
|
200
|
+
|
|
201
|
+
// 7. Clean CLAUDE.md
|
|
202
|
+
cleanInstructionFile(path.join(cwd, 'CLAUDE.md'), 'CLAUDE.md');
|
|
203
|
+
|
|
204
|
+
// 8. Clean AGENTS.md
|
|
205
|
+
cleanAgentsMd(cwd);
|
|
206
|
+
|
|
207
|
+
// Summary
|
|
208
|
+
console.log('\ncodex-vault has been uninstalled.');
|
|
209
|
+
console.log('Vault data preserved at vault/');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// helpers
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remove codex-vault hook entries from a JSON hooks file
|
|
218
|
+
* (.claude/settings.json or .codex/hooks.json).
|
|
219
|
+
*/
|
|
220
|
+
function cleanHooksJson(filePath, label) {
|
|
221
|
+
if (!fs.existsSync(filePath)) return;
|
|
222
|
+
|
|
223
|
+
let data;
|
|
224
|
+
try {
|
|
225
|
+
data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.log(` [!] Could not parse ${label} — skipping (${e.message})`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!data.hooks || typeof data.hooks !== 'object') return;
|
|
232
|
+
|
|
233
|
+
let changed = false;
|
|
234
|
+
for (const event of Object.keys(data.hooks)) {
|
|
235
|
+
const entries = data.hooks[event];
|
|
236
|
+
if (!Array.isArray(entries)) continue;
|
|
237
|
+
|
|
238
|
+
const filtered = entries.filter((entry) => {
|
|
239
|
+
const hooks = entry.hooks || [];
|
|
240
|
+
// Keep the entry only if none of its hook commands belong to codex-vault
|
|
241
|
+
const isVaultEntry = hooks.some(
|
|
242
|
+
(h) => typeof h.command === 'string' && h.command.includes('codex-vault/hooks/')
|
|
243
|
+
);
|
|
244
|
+
return !isVaultEntry;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (filtered.length !== entries.length) {
|
|
248
|
+
changed = true;
|
|
249
|
+
if (filtered.length === 0) {
|
|
250
|
+
delete data.hooks[event];
|
|
251
|
+
} else {
|
|
252
|
+
data.hooks[event] = filtered;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!changed) return;
|
|
258
|
+
|
|
259
|
+
// If hooks object is now empty, remove it
|
|
260
|
+
if (Object.keys(data.hooks).length === 0) {
|
|
261
|
+
delete data.hooks;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If entire JSON is now empty, delete the file
|
|
265
|
+
if (Object.keys(data).length === 0) {
|
|
266
|
+
fs.unlinkSync(filePath);
|
|
267
|
+
console.log(` [x] Removed ${label} (empty after cleanup)`);
|
|
268
|
+
cleanEmptyParents(path.dirname(filePath));
|
|
269
|
+
} else {
|
|
270
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
271
|
+
console.log(` [x] Cleaned codex-vault hooks from ${label}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Remove codex_hooks feature flag from .codex/config.toml.
|
|
277
|
+
*/
|
|
278
|
+
function cleanCodexConfigToml(cwd) {
|
|
279
|
+
const filePath = path.join(cwd, '.codex', 'config.toml');
|
|
280
|
+
if (!fs.existsSync(filePath)) return;
|
|
281
|
+
|
|
282
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
283
|
+
const original = content;
|
|
284
|
+
|
|
285
|
+
// Remove lines containing codex_hooks
|
|
286
|
+
content = content.replace(/^.*codex_hooks.*\n?/gm, '');
|
|
287
|
+
|
|
288
|
+
// Remove empty [features] section (only whitespace/empty lines after header until next section or EOF)
|
|
289
|
+
content = content.replace(/^\[features\]\s*\n(?=\[|$)/gm, '');
|
|
290
|
+
// Also handle [features] at very end of file with nothing after it
|
|
291
|
+
content = content.replace(/^\[features\]\s*$/gm, '');
|
|
292
|
+
|
|
293
|
+
// Trim trailing whitespace
|
|
294
|
+
content = content.replace(/\n+$/, content.trim() === '' ? '' : '\n');
|
|
295
|
+
|
|
296
|
+
if (content === original) return;
|
|
297
|
+
|
|
298
|
+
if (content.trim() === '') {
|
|
299
|
+
fs.unlinkSync(filePath);
|
|
300
|
+
console.log(' [x] Removed .codex/config.toml (empty after cleanup)');
|
|
301
|
+
cleanEmptyParents(path.dirname(filePath));
|
|
302
|
+
} else {
|
|
303
|
+
fs.writeFileSync(filePath, content);
|
|
304
|
+
console.log(' [x] Cleaned codex_hooks from .codex/config.toml');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Remove codex-vault skill directories from an agent config dir.
|
|
310
|
+
*/
|
|
311
|
+
function removeSkills(agentDir, label, skillNames) {
|
|
312
|
+
const skillsDir = path.join(agentDir, 'skills');
|
|
313
|
+
if (!fs.existsSync(skillsDir)) return;
|
|
314
|
+
|
|
315
|
+
let removed = 0;
|
|
316
|
+
for (const name of skillNames) {
|
|
317
|
+
const skillDir = path.join(skillsDir, name);
|
|
318
|
+
if (fs.existsSync(skillDir)) {
|
|
319
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
320
|
+
removed++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (removed === 0) return;
|
|
325
|
+
|
|
326
|
+
console.log(` [x] Removed ${removed} skills from ${label}/skills/`);
|
|
327
|
+
|
|
328
|
+
// Clean up empty directories
|
|
329
|
+
if (isDirEmpty(skillsDir)) {
|
|
330
|
+
fs.rmdirSync(skillsDir);
|
|
331
|
+
if (isDirEmpty(agentDir)) {
|
|
332
|
+
fs.rmdirSync(agentDir);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Remove the # Codex-Vault section from CLAUDE.md.
|
|
339
|
+
* The section may be preceded by a --- separator (appended by install.sh).
|
|
340
|
+
* Removes from the separator (or heading) through EOF or the next --- + # heading.
|
|
341
|
+
*/
|
|
342
|
+
function cleanInstructionFile(filePath, label) {
|
|
343
|
+
if (!fs.existsSync(filePath)) return;
|
|
344
|
+
|
|
345
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
346
|
+
const lines = content.split('\n');
|
|
347
|
+
|
|
348
|
+
// Find the "# Codex-Vault" heading
|
|
349
|
+
let sectionStart = -1;
|
|
350
|
+
for (let i = 0; i < lines.length; i++) {
|
|
351
|
+
if (/^# Codex-Vault/.test(lines[i])) {
|
|
352
|
+
sectionStart = i;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (sectionStart === -1) return;
|
|
358
|
+
|
|
359
|
+
// Check if preceded by --- separator (possibly with blank lines between)
|
|
360
|
+
let cutStart = sectionStart;
|
|
361
|
+
for (let i = sectionStart - 1; i >= 0; i--) {
|
|
362
|
+
if (lines[i].trim() === '') continue;
|
|
363
|
+
if (lines[i].trim() === '---') {
|
|
364
|
+
cutStart = i;
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Find the end: next --- followed by # heading, or EOF
|
|
370
|
+
let cutEnd = lines.length;
|
|
371
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
372
|
+
if (lines[i].trim() === '---' && i + 1 < lines.length && /^# /.test(lines[i + 1])) {
|
|
373
|
+
cutEnd = i;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Build remaining content
|
|
379
|
+
const before = lines.slice(0, cutStart);
|
|
380
|
+
const after = lines.slice(cutEnd);
|
|
381
|
+
let remaining = before.concat(after).join('\n');
|
|
382
|
+
|
|
383
|
+
// Trim trailing whitespace/newlines
|
|
384
|
+
remaining = remaining.replace(/\s+$/, '');
|
|
385
|
+
|
|
386
|
+
if (remaining === '') {
|
|
387
|
+
fs.unlinkSync(filePath);
|
|
388
|
+
console.log(` [x] Removed ${label} (empty after cleanup)`);
|
|
389
|
+
} else {
|
|
390
|
+
fs.writeFileSync(filePath, remaining + '\n');
|
|
391
|
+
console.log(` [x] Cleaned codex-vault section from ${label}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Handle AGENTS.md: if it's just "@CLAUDE.md", delete it.
|
|
397
|
+
* Otherwise apply the same section-removal logic as CLAUDE.md.
|
|
398
|
+
*/
|
|
399
|
+
function cleanAgentsMd(cwd) {
|
|
400
|
+
const filePath = path.join(cwd, 'AGENTS.md');
|
|
401
|
+
if (!fs.existsSync(filePath)) return;
|
|
402
|
+
|
|
403
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
404
|
+
|
|
405
|
+
// If content is just @CLAUDE.md (possibly with whitespace), delete the file
|
|
406
|
+
if (content.trim() === '@CLAUDE.md') {
|
|
407
|
+
fs.unlinkSync(filePath);
|
|
408
|
+
console.log(' [x] Removed AGENTS.md (@CLAUDE.md reference)');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Otherwise, apply the same section-removal logic
|
|
413
|
+
cleanInstructionFile(filePath, 'AGENTS.md');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Check if a directory is empty.
|
|
418
|
+
*/
|
|
419
|
+
function isDirEmpty(dirPath) {
|
|
420
|
+
try {
|
|
421
|
+
const entries = fs.readdirSync(dirPath);
|
|
422
|
+
return entries.length === 0;
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Remove empty parent directories up to (but not including) cwd.
|
|
430
|
+
*/
|
|
431
|
+
function cleanEmptyParents(dirPath) {
|
|
432
|
+
const cwd = process.cwd();
|
|
433
|
+
if (dirPath === cwd || !dirPath.startsWith(cwd)) return;
|
|
434
|
+
if (isDirEmpty(dirPath)) {
|
|
435
|
+
fs.rmdirSync(dirPath);
|
|
436
|
+
}
|
|
437
|
+
}
|
package/package.json
CHANGED
package/plugin/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
|
@@ -17,6 +17,7 @@ SIGNALS = [
|
|
|
17
17
|
{
|
|
18
18
|
"name": "DECISION",
|
|
19
19
|
"message": "DECISION detected — suggest the user run /dump to capture this decision",
|
|
20
|
+
"auto_message": "DECISION detected — execute /dump now to capture this decision from the user's message",
|
|
20
21
|
"patterns": [
|
|
21
22
|
"decided", "deciding", "decision", "we chose", "agreed to",
|
|
22
23
|
"let's go with", "the call is", "we're going with",
|
|
@@ -25,6 +26,7 @@ SIGNALS = [
|
|
|
25
26
|
{
|
|
26
27
|
"name": "WIN",
|
|
27
28
|
"message": "WIN detected — suggest the user run /dump to record this achievement",
|
|
29
|
+
"auto_message": "WIN detected — execute /dump now to record this achievement from the user's message",
|
|
28
30
|
"patterns": [
|
|
29
31
|
"achieved", "won", "praised",
|
|
30
32
|
"kudos", "shoutout", "great feedback", "recognized",
|
|
@@ -33,6 +35,7 @@ SIGNALS = [
|
|
|
33
35
|
{
|
|
34
36
|
"name": "PROJECT UPDATE",
|
|
35
37
|
"message": "PROJECT UPDATE detected — suggest the user run /dump to log this progress",
|
|
38
|
+
"auto_message": "PROJECT UPDATE detected — execute /dump now to log this progress from the user's message",
|
|
36
39
|
"patterns": [
|
|
37
40
|
"project update", "sprint", "milestone",
|
|
38
41
|
"shipped", "shipping", "launched", "launching",
|
|
@@ -44,6 +47,7 @@ SIGNALS = [
|
|
|
44
47
|
{
|
|
45
48
|
"name": "QUERY",
|
|
46
49
|
"message": "QUERY detected — suggest the user run /recall to check existing knowledge first",
|
|
50
|
+
"auto_message": "QUERY detected — execute /recall now to search vault for relevant information before answering",
|
|
47
51
|
"patterns": [
|
|
48
52
|
"what is", "how does", "why did", "compare", "analyze",
|
|
49
53
|
"explain the", "what's the difference", "summarize the",
|
|
@@ -53,6 +57,7 @@ SIGNALS = [
|
|
|
53
57
|
{
|
|
54
58
|
"name": "INGEST",
|
|
55
59
|
"message": "INGEST detected — suggest the user run /ingest to process the source",
|
|
60
|
+
"auto_message": "INGEST detected — execute /ingest now to process the source from the user's message",
|
|
56
61
|
"patterns": [
|
|
57
62
|
"ingest", "process this", "read this article",
|
|
58
63
|
"summarize this", "new source", "clip this", "web clip",
|
|
@@ -90,6 +95,23 @@ def _find_vault_root():
|
|
|
90
95
|
return None
|
|
91
96
|
|
|
92
97
|
|
|
98
|
+
def _read_mode():
|
|
99
|
+
"""Read classify mode from vault config. Default: suggest."""
|
|
100
|
+
vault_root = _find_vault_root()
|
|
101
|
+
if not vault_root:
|
|
102
|
+
return "suggest"
|
|
103
|
+
config_path = os.path.join(vault_root, ".codex-vault", "config.json")
|
|
104
|
+
try:
|
|
105
|
+
with open(config_path) as f:
|
|
106
|
+
config = json.load(f)
|
|
107
|
+
mode = config.get("classify_mode", "suggest")
|
|
108
|
+
if mode in ("suggest", "auto"):
|
|
109
|
+
return mode
|
|
110
|
+
except (OSError, ValueError, KeyError):
|
|
111
|
+
pass
|
|
112
|
+
return "suggest"
|
|
113
|
+
|
|
114
|
+
|
|
93
115
|
def _get_changed_files(vault_root):
|
|
94
116
|
"""Get list of changed/new .md files relative to vault root."""
|
|
95
117
|
files = set()
|
|
@@ -172,9 +194,10 @@ def _check_vault_integrity(vault_root):
|
|
|
172
194
|
return warnings
|
|
173
195
|
|
|
174
196
|
|
|
175
|
-
def classify(prompt):
|
|
197
|
+
def classify(prompt, mode="suggest"):
|
|
176
198
|
p = prompt.lower()
|
|
177
|
-
|
|
199
|
+
key = "auto_message" if mode == "auto" else "message"
|
|
200
|
+
return [s[key] for s in SIGNALS if _match(s["patterns"], p)]
|
|
178
201
|
|
|
179
202
|
|
|
180
203
|
def is_session_end(prompt):
|
|
@@ -192,39 +215,63 @@ def main():
|
|
|
192
215
|
if not isinstance(prompt, str) or not prompt:
|
|
193
216
|
sys.exit(0)
|
|
194
217
|
|
|
195
|
-
|
|
218
|
+
signal_messages = []
|
|
219
|
+
session_end_messages = []
|
|
196
220
|
|
|
197
221
|
try:
|
|
222
|
+
mode = _read_mode()
|
|
223
|
+
|
|
198
224
|
# Regular signal classification
|
|
199
|
-
signals = classify(prompt)
|
|
200
|
-
|
|
225
|
+
signals = classify(prompt, mode)
|
|
226
|
+
signal_messages.extend(signals)
|
|
201
227
|
|
|
202
|
-
# Session-end check
|
|
228
|
+
# Session-end check (always suggest mode — never auto-execute wrap-up)
|
|
203
229
|
if is_session_end(prompt):
|
|
204
230
|
vault_root = _find_vault_root()
|
|
205
231
|
if vault_root:
|
|
206
232
|
integrity_warnings = _check_vault_integrity(vault_root)
|
|
207
233
|
if integrity_warnings:
|
|
208
|
-
|
|
234
|
+
session_end_messages.append(
|
|
209
235
|
"SESSION END — vault integrity check found issues:\n"
|
|
210
236
|
+ "\n".join(f" - {w}" for w in integrity_warnings)
|
|
211
237
|
+ "\nFix these before wrapping up."
|
|
212
238
|
)
|
|
213
239
|
else:
|
|
214
|
-
|
|
240
|
+
session_end_messages.append("SESSION END — vault integrity check passed.")
|
|
215
241
|
except Exception:
|
|
216
242
|
sys.exit(0)
|
|
217
243
|
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
parts = []
|
|
245
|
+
|
|
246
|
+
if signal_messages:
|
|
247
|
+
hints = "\n".join(f"- {s}" for s in signal_messages)
|
|
248
|
+
if mode == "auto":
|
|
249
|
+
parts.append(
|
|
250
|
+
"Auto-execute the following skills based on user intent:\n"
|
|
251
|
+
+ hints
|
|
252
|
+
+ "\n\nExecute the skill immediately with the user's message as input. Do not ask for confirmation."
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
parts.append(
|
|
256
|
+
"Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
|
|
257
|
+
+ hints
|
|
258
|
+
+ "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if session_end_messages:
|
|
262
|
+
hints = "\n".join(f"- {s}" for s in session_end_messages)
|
|
263
|
+
parts.append(
|
|
264
|
+
"Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
|
|
265
|
+
+ hints
|
|
266
|
+
+ "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if parts:
|
|
270
|
+
context = "\n\n".join(parts)
|
|
220
271
|
output = {
|
|
221
272
|
"hookSpecificOutput": {
|
|
222
273
|
"hookEventName": "UserPromptSubmit",
|
|
223
|
-
"additionalContext":
|
|
224
|
-
"Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
|
|
225
|
-
+ hints
|
|
226
|
-
+ "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
|
|
227
|
-
)
|
|
274
|
+
"additionalContext": context
|
|
228
275
|
}
|
|
229
276
|
}
|
|
230
277
|
json.dump(output, sys.stdout)
|
|
@@ -17,6 +17,7 @@ SIGNALS = [
|
|
|
17
17
|
{
|
|
18
18
|
"name": "DECISION",
|
|
19
19
|
"message": "DECISION detected — suggest the user run /dump to capture this decision",
|
|
20
|
+
"auto_message": "DECISION detected — execute /dump now to capture this decision from the user's message",
|
|
20
21
|
"patterns": [
|
|
21
22
|
"decided", "deciding", "decision", "we chose", "agreed to",
|
|
22
23
|
"let's go with", "the call is", "we're going with",
|
|
@@ -25,6 +26,7 @@ SIGNALS = [
|
|
|
25
26
|
{
|
|
26
27
|
"name": "WIN",
|
|
27
28
|
"message": "WIN detected — suggest the user run /dump to record this achievement",
|
|
29
|
+
"auto_message": "WIN detected — execute /dump now to record this achievement from the user's message",
|
|
28
30
|
"patterns": [
|
|
29
31
|
"achieved", "won", "praised",
|
|
30
32
|
"kudos", "shoutout", "great feedback", "recognized",
|
|
@@ -33,6 +35,7 @@ SIGNALS = [
|
|
|
33
35
|
{
|
|
34
36
|
"name": "PROJECT UPDATE",
|
|
35
37
|
"message": "PROJECT UPDATE detected — suggest the user run /dump to log this progress",
|
|
38
|
+
"auto_message": "PROJECT UPDATE detected — execute /dump now to log this progress from the user's message",
|
|
36
39
|
"patterns": [
|
|
37
40
|
"project update", "sprint", "milestone",
|
|
38
41
|
"shipped", "shipping", "launched", "launching",
|
|
@@ -44,6 +47,7 @@ SIGNALS = [
|
|
|
44
47
|
{
|
|
45
48
|
"name": "QUERY",
|
|
46
49
|
"message": "QUERY detected — suggest the user run /recall to check existing knowledge first",
|
|
50
|
+
"auto_message": "QUERY detected — execute /recall now to search vault for relevant information before answering",
|
|
47
51
|
"patterns": [
|
|
48
52
|
"what is", "how does", "why did", "compare", "analyze",
|
|
49
53
|
"explain the", "what's the difference", "summarize the",
|
|
@@ -53,6 +57,7 @@ SIGNALS = [
|
|
|
53
57
|
{
|
|
54
58
|
"name": "INGEST",
|
|
55
59
|
"message": "INGEST detected — suggest the user run /ingest to process the source",
|
|
60
|
+
"auto_message": "INGEST detected — execute /ingest now to process the source from the user's message",
|
|
56
61
|
"patterns": [
|
|
57
62
|
"ingest", "process this", "read this article",
|
|
58
63
|
"summarize this", "new source", "clip this", "web clip",
|
|
@@ -90,6 +95,23 @@ def _find_vault_root():
|
|
|
90
95
|
return None
|
|
91
96
|
|
|
92
97
|
|
|
98
|
+
def _read_mode():
|
|
99
|
+
"""Read classify mode from vault config. Default: suggest."""
|
|
100
|
+
vault_root = _find_vault_root()
|
|
101
|
+
if not vault_root:
|
|
102
|
+
return "suggest"
|
|
103
|
+
config_path = os.path.join(vault_root, ".codex-vault", "config.json")
|
|
104
|
+
try:
|
|
105
|
+
with open(config_path) as f:
|
|
106
|
+
config = json.load(f)
|
|
107
|
+
mode = config.get("classify_mode", "suggest")
|
|
108
|
+
if mode in ("suggest", "auto"):
|
|
109
|
+
return mode
|
|
110
|
+
except (OSError, ValueError, KeyError):
|
|
111
|
+
pass
|
|
112
|
+
return "suggest"
|
|
113
|
+
|
|
114
|
+
|
|
93
115
|
def _get_changed_files(vault_root):
|
|
94
116
|
"""Get list of changed/new .md files relative to vault root."""
|
|
95
117
|
files = set()
|
|
@@ -172,9 +194,10 @@ def _check_vault_integrity(vault_root):
|
|
|
172
194
|
return warnings
|
|
173
195
|
|
|
174
196
|
|
|
175
|
-
def classify(prompt):
|
|
197
|
+
def classify(prompt, mode="suggest"):
|
|
176
198
|
p = prompt.lower()
|
|
177
|
-
|
|
199
|
+
key = "auto_message" if mode == "auto" else "message"
|
|
200
|
+
return [s[key] for s in SIGNALS if _match(s["patterns"], p)]
|
|
178
201
|
|
|
179
202
|
|
|
180
203
|
def is_session_end(prompt):
|
|
@@ -192,39 +215,63 @@ def main():
|
|
|
192
215
|
if not isinstance(prompt, str) or not prompt:
|
|
193
216
|
sys.exit(0)
|
|
194
217
|
|
|
195
|
-
|
|
218
|
+
signal_messages = []
|
|
219
|
+
session_end_messages = []
|
|
196
220
|
|
|
197
221
|
try:
|
|
222
|
+
mode = _read_mode()
|
|
223
|
+
|
|
198
224
|
# Regular signal classification
|
|
199
|
-
signals = classify(prompt)
|
|
200
|
-
|
|
225
|
+
signals = classify(prompt, mode)
|
|
226
|
+
signal_messages.extend(signals)
|
|
201
227
|
|
|
202
|
-
# Session-end check
|
|
228
|
+
# Session-end check (always suggest mode — never auto-execute wrap-up)
|
|
203
229
|
if is_session_end(prompt):
|
|
204
230
|
vault_root = _find_vault_root()
|
|
205
231
|
if vault_root:
|
|
206
232
|
integrity_warnings = _check_vault_integrity(vault_root)
|
|
207
233
|
if integrity_warnings:
|
|
208
|
-
|
|
234
|
+
session_end_messages.append(
|
|
209
235
|
"SESSION END — vault integrity check found issues:\n"
|
|
210
236
|
+ "\n".join(f" - {w}" for w in integrity_warnings)
|
|
211
237
|
+ "\nFix these before wrapping up."
|
|
212
238
|
)
|
|
213
239
|
else:
|
|
214
|
-
|
|
240
|
+
session_end_messages.append("SESSION END — vault integrity check passed.")
|
|
215
241
|
except Exception:
|
|
216
242
|
sys.exit(0)
|
|
217
243
|
|
|
218
|
-
|
|
219
|
-
|
|
244
|
+
parts = []
|
|
245
|
+
|
|
246
|
+
if signal_messages:
|
|
247
|
+
hints = "\n".join(f"- {s}" for s in signal_messages)
|
|
248
|
+
if mode == "auto":
|
|
249
|
+
parts.append(
|
|
250
|
+
"Auto-execute the following skills based on user intent:\n"
|
|
251
|
+
+ hints
|
|
252
|
+
+ "\n\nExecute the skill immediately with the user's message as input. Do not ask for confirmation."
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
parts.append(
|
|
256
|
+
"Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
|
|
257
|
+
+ hints
|
|
258
|
+
+ "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if session_end_messages:
|
|
262
|
+
hints = "\n".join(f"- {s}" for s in session_end_messages)
|
|
263
|
+
parts.append(
|
|
264
|
+
"Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
|
|
265
|
+
+ hints
|
|
266
|
+
+ "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if parts:
|
|
270
|
+
context = "\n\n".join(parts)
|
|
220
271
|
output = {
|
|
221
272
|
"hookSpecificOutput": {
|
|
222
273
|
"hookEventName": "UserPromptSubmit",
|
|
223
|
-
"additionalContext":
|
|
224
|
-
"Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
|
|
225
|
-
+ hints
|
|
226
|
-
+ "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
|
|
227
|
-
)
|
|
274
|
+
"additionalContext": context
|
|
228
275
|
}
|
|
229
276
|
}
|
|
230
277
|
json.dump(output, sys.stdout)
|