promptvc 0.1.6 → 0.1.8

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
@@ -5,15 +5,17 @@ Local-first, prompt-by-prompt diff tracking for AI coding sessions.
5
5
  ## Requirements
6
6
 
7
7
  - Node.js 22+ (use nvm: `nvm install 22 && nvm use 22`)
8
+ - npm 11.5.1 (set `PROMPTVC_EXPECTED_NPM_VERSION` to override)
8
9
  - Git
9
- - Codex CLI
10
- - jq (required for per-prompt capture)
10
+ - Codex CLI 0.80.0 (set `PROMPTVC_EXPECTED_CODEX_VERSION` to override)
11
+ - Claude Code (for Claude session capture)
12
+ - jq (optional; legacy hook fallback)
11
13
  - macOS/Linux
12
14
  - Windows: Git Bash required (run `promptvc` + `codex` in Git Bash)
13
15
 
14
16
  ### Windows notes
15
17
 
16
- Install `jq` (pick one):
18
+ If the legacy shell hook fallback is used, install `jq` (pick one):
17
19
 
18
20
  ```bash
19
21
  winget install jqlang.jq
@@ -39,15 +41,20 @@ npm install -g promptvc
39
41
  promptvc config
40
42
  ```
41
43
 
42
- This command finds the installed notify hook and updates `~/.codex/config.toml`. If it cannot edit the file, it prints a ready-to-paste snippet.
44
+ This command finds the installed notify hooks, verifies Codex/npm versions, and updates `~/.codex/config.toml` plus `~/.claude/settings.json`. If it cannot edit a file, it prints a ready-to-paste snippet.
45
+
46
+ If you need to bypass the version guard, set:
47
+
48
+ ```bash
49
+ PROMPTVC_ALLOW_VERSION_MISMATCH=1
50
+ ```
43
51
 
44
52
  ### Manual setup
45
53
 
46
54
  Add to `~/.codex/config.toml`:
47
55
 
48
56
  ```toml
49
- [hooks]
50
- notify = "/absolute/path/to/promptvc/hooks/codex-notify.sh"
57
+ notify = ["/absolute/path/to/promptvc/hooks/codex-notify.sh"]
51
58
  ```
52
59
 
53
60
  If installed globally via npm, the hook is typically at:
@@ -56,6 +63,49 @@ If installed globally via npm, the hook is typically at:
56
63
  $(npm root -g)/promptvc/hooks/codex-notify.sh
57
64
  ```
58
65
 
66
+ Add (or merge) into `~/.claude/settings.json`:
67
+
68
+ ```json
69
+ {
70
+ "hooks": {
71
+ "SessionEnd": [
72
+ {
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "/absolute/path/to/promptvc/hooks/claude-notify.sh"
77
+ }
78
+ ]
79
+ }
80
+ ],
81
+ "Stop": [
82
+ {
83
+ "hooks": [
84
+ {
85
+ "type": "command",
86
+ "command": "/absolute/path/to/promptvc/hooks/claude-notify.sh"
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ }
92
+ }
93
+ ```
94
+
95
+ If installed globally via npm, the hook is typically at:
96
+
97
+ ```bash
98
+ $(npm root -g)/promptvc/hooks/claude-notify.sh
99
+ ```
100
+
101
+ ## Version guard
102
+
103
+ PromptVC checks Codex and npm versions during `promptvc config` and `promptvc init`.
104
+
105
+ - Expected Codex: `0.80.0` (override with `PROMPTVC_EXPECTED_CODEX_VERSION`)
106
+ - Expected npm: `11.5.1` (override with `PROMPTVC_EXPECTED_NPM_VERSION`)
107
+ - Bypass the guard: `PROMPTVC_ALLOW_VERSION_MISMATCH=1`
108
+
59
109
  ## Usage
60
110
 
61
111
  Initialize a repo:
@@ -92,4 +142,4 @@ promptvc-codex
92
142
  ## Troubleshooting
93
143
 
94
144
  - If `promptvc` resolves to a Python shim, run `which promptvc` and ensure your npm global bin is ahead of pyenv on PATH.
95
- - Ensure `jq` is installed and available on PATH.
145
+ - If you’re using the legacy shell hook fallback, ensure `jq` is installed and available on PATH.
@@ -0,0 +1,16 @@
1
+ export type CodexVersionInfo = {
2
+ version: string | null;
3
+ raw: string | null;
4
+ binaryPath: string | null;
5
+ };
6
+ export declare const EXPECTED_CODEX_VERSION = "0.80.0";
7
+ export declare const NOTIFY_ARRAY_MIN_VERSION = "0.80.0";
8
+ export declare function getCodexVersionInfo(): CodexVersionInfo;
9
+ export declare function compareSemver(a: string, b: string): number;
10
+ export declare function getNotifyConfigLine(hookPath: string, codexVersion: string | null): {
11
+ line: string;
12
+ usesArray: boolean;
13
+ useHooksSection: boolean;
14
+ };
15
+ export declare function getCodexVersionWarnings(codexVersion: string | null): string[];
16
+ //# sourceMappingURL=codexVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexVersion.d.ts","sourceRoot":"","sources":["../src/codexVersion.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,sBAAsB,WAAW,CAAC;AAC/C,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAkBjD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAkBtD;AASD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU1D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;CAC1B,CAQA;AAED,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAW7E"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NOTIFY_ARRAY_MIN_VERSION = exports.EXPECTED_CODEX_VERSION = void 0;
4
+ exports.getCodexVersionInfo = getCodexVersionInfo;
5
+ exports.compareSemver = compareSemver;
6
+ exports.getNotifyConfigLine = getNotifyConfigLine;
7
+ exports.getCodexVersionWarnings = getCodexVersionWarnings;
8
+ const child_process_1 = require("child_process");
9
+ exports.EXPECTED_CODEX_VERSION = '0.80.0';
10
+ exports.NOTIFY_ARRAY_MIN_VERSION = '0.80.0';
11
+ const VERSION_PATTERN = /\b(\d+\.\d+\.\d+)\b/;
12
+ function parseVersion(raw) {
13
+ const match = raw.match(VERSION_PATTERN);
14
+ return match ? match[1] : null;
15
+ }
16
+ function getCodexBinaryPath() {
17
+ const command = process.platform === 'win32' ? 'where' : 'which';
18
+ const result = (0, child_process_1.spawnSync)(command, ['codex'], { encoding: 'utf8' });
19
+ if (result.status !== 0)
20
+ return null;
21
+ const output = (result.stdout || '').trim();
22
+ if (!output)
23
+ return null;
24
+ return output.split(/\r?\n/)[0] || null;
25
+ }
26
+ function getCodexVersionInfo() {
27
+ const commands = [['--version'], ['version']];
28
+ let raw = null;
29
+ for (const args of commands) {
30
+ const result = (0, child_process_1.spawnSync)('codex', args, { encoding: 'utf8' });
31
+ if (result.status !== 0)
32
+ continue;
33
+ const output = ((result.stdout || '') + (result.stderr || '')).trim();
34
+ if (output) {
35
+ raw = output;
36
+ break;
37
+ }
38
+ }
39
+ const version = raw ? parseVersion(raw) : null;
40
+ return {
41
+ version,
42
+ raw,
43
+ binaryPath: getCodexBinaryPath(),
44
+ };
45
+ }
46
+ function toSemverParts(version) {
47
+ return version.split('.').map((part) => {
48
+ const value = Number.parseInt(part, 10);
49
+ return Number.isFinite(value) ? value : 0;
50
+ });
51
+ }
52
+ function compareSemver(a, b) {
53
+ const aParts = toSemverParts(a);
54
+ const bParts = toSemverParts(b);
55
+ const maxLen = Math.max(aParts.length, bParts.length);
56
+ for (let i = 0; i < maxLen; i += 1) {
57
+ const left = aParts[i] ?? 0;
58
+ const right = bParts[i] ?? 0;
59
+ if (left !== right)
60
+ return left > right ? 1 : -1;
61
+ }
62
+ return 0;
63
+ }
64
+ function getNotifyConfigLine(hookPath, codexVersion) {
65
+ const isLegacy = codexVersion ? compareSemver(codexVersion, exports.NOTIFY_ARRAY_MIN_VERSION) < 0 : false;
66
+ const useArray = !isLegacy;
67
+ return {
68
+ line: useArray ? `notify = ["${hookPath}"]` : `notify = "${hookPath}"`,
69
+ usesArray: useArray,
70
+ useHooksSection: isLegacy,
71
+ };
72
+ }
73
+ function getCodexVersionWarnings(codexVersion) {
74
+ if (!codexVersion) {
75
+ return ['Codex version not detected. Run `codex --version` to verify your installation.'];
76
+ }
77
+ if (codexVersion !== exports.EXPECTED_CODEX_VERSION) {
78
+ return [
79
+ `PromptVC is tested with Codex ${exports.EXPECTED_CODEX_VERSION}, detected ${codexVersion}.`,
80
+ 'If hooks do not fire, install the tested version.',
81
+ ];
82
+ }
83
+ return [];
84
+ }
85
+ //# sourceMappingURL=codexVersion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexVersion.js","sourceRoot":"","sources":["../src/codexVersion.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA8GA,eAAO,MAAM,gBAAgB,QAAa,OAAO,CAAC,IAAI,CAsFrD,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA0SA,eAAO,MAAM,gBAAgB,QAAa,OAAO,CAAC,IAAI,CAmPrD,CAAC"}
package/dist/config.js CHANGED
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
@@ -41,13 +8,17 @@ const fs_1 = __importDefault(require("fs"));
41
8
  const os_1 = __importDefault(require("os"));
42
9
  const path_1 = __importDefault(require("path"));
43
10
  const child_process_1 = require("child_process");
44
- const readline = __importStar(require("readline/promises"));
45
11
  const kleur_1 = __importDefault(require("kleur"));
46
12
  const branding_1 = require("./branding");
13
+ const toolVersions_1 = require("./toolVersions");
14
+ const prompt_1 = require("./prompt");
47
15
  const getCodexConfigPath = () => path_1.default.join(os_1.default.homedir(), '.codex', 'config.toml');
48
16
  const getCodexDir = () => path_1.default.join(os_1.default.homedir(), '.codex');
49
17
  const getNotifyHookPath = () => path_1.default.resolve(__dirname, '..', 'hooks', 'codex-notify.sh');
50
18
  const isWindows = process.platform === 'win32';
19
+ const getClaudeSettingsPath = () => path_1.default.join(os_1.default.homedir(), '.claude', 'settings.json');
20
+ const getClaudeDir = () => path_1.default.join(os_1.default.homedir(), '.claude');
21
+ const getClaudeHookPath = () => path_1.default.resolve(__dirname, '..', 'hooks', 'claude-notify.sh');
51
22
  const isCodexInstalled = () => {
52
23
  const codexDir = getCodexDir();
53
24
  return fs_1.default.existsSync(codexDir);
@@ -83,33 +54,154 @@ const normalizeHookPathForToml = (hookPath) => {
83
54
  }
84
55
  return hookPath.replace(/\\/g, '/');
85
56
  };
86
- const hasJq = () => {
87
- const result = (0, child_process_1.spawnSync)('jq', ['--version'], { stdio: 'ignore' });
88
- if (result.error) {
57
+ const normalizeHookPathForCommand = (hookPath) => {
58
+ if (!isWindows) {
59
+ return hookPath;
60
+ }
61
+ return hookPath.replace(/\\/g, '/');
62
+ };
63
+ const isClaudeInstalled = () => {
64
+ const claudeDir = getClaudeDir();
65
+ return fs_1.default.existsSync(claudeDir);
66
+ };
67
+ const hasClaudeConfig = () => {
68
+ const configPath = getClaudeSettingsPath();
69
+ return fs_1.default.existsSync(configPath);
70
+ };
71
+ const isClaudeMatcherEntry = (entry) => {
72
+ if (!entry || typeof entry !== 'object') {
89
73
  return false;
90
74
  }
91
- return result.status === 0;
75
+ return 'matcher' in entry || 'hooks' in entry;
76
+ };
77
+ const isClaudeHookEntry = (entry) => {
78
+ if (!entry || typeof entry !== 'object') {
79
+ return false;
80
+ }
81
+ return 'type' in entry || 'command' in entry;
92
82
  };
93
- const promptYesNo = async (message) => {
94
- if (!process.stdin.isTTY) {
83
+ const hasClaudeHook = (settings, eventName, hookPath) => {
84
+ const hooks = settings?.hooks?.[eventName];
85
+ if (!Array.isArray(hooks)) {
95
86
  return false;
96
87
  }
97
- const rl = readline.createInterface({
98
- input: process.stdin,
99
- output: process.stdout,
88
+ return hooks.some((entry) => {
89
+ if (isClaudeMatcherEntry(entry)) {
90
+ return Array.isArray(entry.hooks)
91
+ && entry.hooks.some((hook) => hook?.type === 'command' && hook.command === hookPath);
92
+ }
93
+ return isClaudeHookEntry(entry) && entry.type === 'command' && entry.command === hookPath;
100
94
  });
95
+ };
96
+ const getClaudeHookStatus = (hookPath) => {
97
+ const configPath = getClaudeSettingsPath();
98
+ if (!fs_1.default.existsSync(configPath)) {
99
+ return { stop: false, sessionEnd: false };
100
+ }
101
101
  try {
102
- const answer = await rl.question(message);
103
- return ['y', 'yes'].includes(answer.trim().toLowerCase());
102
+ const settings = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
103
+ return {
104
+ stop: hasClaudeHook(settings, 'Stop', hookPath),
105
+ sessionEnd: hasClaudeHook(settings, 'SessionEnd', hookPath),
106
+ };
107
+ }
108
+ catch {
109
+ return { stop: false, sessionEnd: false };
104
110
  }
105
- finally {
106
- rl.close();
111
+ };
112
+ const normalizeClaudeEventHooks = (entries) => {
113
+ const normalized = [];
114
+ const directHooks = [];
115
+ let changed = false;
116
+ for (const entry of entries) {
117
+ if (isClaudeMatcherEntry(entry)) {
118
+ const entryHooks = Array.isArray(entry.hooks) ? [...entry.hooks] : [];
119
+ if (!Array.isArray(entry.hooks)) {
120
+ changed = true;
121
+ }
122
+ normalized.push({ ...entry, hooks: entryHooks });
123
+ continue;
124
+ }
125
+ if (isClaudeHookEntry(entry)) {
126
+ directHooks.push(entry);
127
+ continue;
128
+ }
129
+ normalized.push(entry);
130
+ }
131
+ if (directHooks.length > 0) {
132
+ let targetIndex = normalized.findIndex((entry) => entry.matcher == null || entry.matcher === '');
133
+ if (targetIndex === -1) {
134
+ normalized.push({ hooks: [...directHooks] });
135
+ }
136
+ else {
137
+ const mergedHooks = Array.isArray(normalized[targetIndex].hooks)
138
+ ? [...normalized[targetIndex].hooks]
139
+ : [];
140
+ for (const hook of directHooks) {
141
+ const exists = mergedHooks.some((existing) => existing?.type === hook?.type && existing?.command === hook?.command);
142
+ if (!exists) {
143
+ mergedHooks.push(hook);
144
+ }
145
+ }
146
+ normalized[targetIndex] = { ...normalized[targetIndex], hooks: mergedHooks };
147
+ }
148
+ changed = true;
149
+ }
150
+ return { entries: normalized, changed };
151
+ };
152
+ const upsertClaudeEventHook = (settings, eventName, hookPath) => {
153
+ const updated = settings && typeof settings === 'object' ? { ...settings } : {};
154
+ const hooks = updated.hooks && typeof updated.hooks === 'object' ? { ...updated.hooks } : {};
155
+ const existingEntries = Array.isArray(hooks[eventName]) ? [...hooks[eventName]] : [];
156
+ const normalized = normalizeClaudeEventHooks(existingEntries);
157
+ const eventEntries = normalized.entries;
158
+ let changed = normalized.changed;
159
+ let entryIndex = eventEntries.findIndex((entry) => entry.matcher == null || entry.matcher === '');
160
+ if (entryIndex === -1) {
161
+ eventEntries.push({ hooks: [] });
162
+ entryIndex = eventEntries.length - 1;
163
+ changed = true;
164
+ }
165
+ const entry = { ...eventEntries[entryIndex] };
166
+ const entryHooks = Array.isArray(entry.hooks) ? [...entry.hooks] : [];
167
+ const hasHook = entryHooks.some((hook) => hook?.type === 'command' && hook.command === hookPath);
168
+ if (!hasHook) {
169
+ entryHooks.push({ type: 'command', command: hookPath });
170
+ changed = true;
107
171
  }
172
+ entry.hooks = entryHooks;
173
+ eventEntries[entryIndex] = entry;
174
+ hooks[eventName] = eventEntries;
175
+ updated.hooks = hooks;
176
+ return { updated, changed };
108
177
  };
109
- const upsertNotifyHook = (content, hookPath) => {
178
+ const hasJq = () => {
179
+ const result = (0, child_process_1.spawnSync)('jq', ['--version'], { stdio: 'ignore' });
180
+ if (result.error) {
181
+ return false;
182
+ }
183
+ return result.status === 0;
184
+ };
185
+ const upsertNotifyHook = (content, notifyLine) => {
186
+ const usesCrlf = content.includes('\r\n');
187
+ const normalized = content.replace(/\r\n/g, '\n');
188
+ if (!normalized.trim()) {
189
+ const fresh = `${notifyLine}\n`;
190
+ return usesCrlf ? fresh.replace(/\n/g, '\r\n') : fresh;
191
+ }
192
+ const lines = normalized.split('\n');
193
+ const filtered = lines.filter((line) => !/^\s*notify\s*=/.test(line));
194
+ let insertIndex = filtered.findIndex((line) => /^\s*\[/.test(line));
195
+ if (insertIndex === -1)
196
+ insertIndex = filtered.length;
197
+ filtered.splice(insertIndex, 0, notifyLine);
198
+ let updated = filtered.join('\n');
199
+ updated = updated.replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
200
+ return usesCrlf ? updated.replace(/\n/g, '\r\n') : updated;
201
+ };
202
+ const upsertNotifyHookLegacy = (content, notifyLine) => {
110
203
  const usesCrlf = content.includes('\r\n');
111
204
  const normalized = content.replace(/\r\n/g, '\n');
112
- const notifyLine = `notify = "${hookPath}"`;
113
205
  if (!normalized.trim()) {
114
206
  const fresh = `[hooks]\n${notifyLine}\n`;
115
207
  return usesCrlf ? fresh.replace(/\n/g, '\r\n') : fresh;
@@ -149,7 +241,7 @@ const runConfigCommand = async () => {
149
241
  console.log(' - winget install jqlang.jq');
150
242
  console.log(' - choco install jq');
151
243
  console.log(' - scoop install jq');
152
- const shouldInstall = await promptYesNo('Install jq now? (y/N): ');
244
+ const shouldInstall = await (0, prompt_1.promptYesNo)('Install jq now? (y/N): ');
153
245
  if (shouldInstall) {
154
246
  console.log('');
155
247
  console.log('Run one of the commands above, then re-run `promptvc config`.');
@@ -160,14 +252,49 @@ const runConfigCommand = async () => {
160
252
  }
161
253
  const hookPath = getNotifyHookPath();
162
254
  const hookPathForConfig = normalizeHookPathForToml(hookPath);
255
+ const claudeHookPath = getClaudeHookPath();
256
+ const claudeHookPathForConfig = normalizeHookPathForCommand(claudeHookPath);
257
+ const codexInfo = (0, toolVersions_1.getCodexVersionInfo)();
258
+ const npmInfo = (0, toolVersions_1.getNpmVersionInfo)();
259
+ const notifyConfig = (0, toolVersions_1.getNotifyConfigLine)(hookPathForConfig, codexInfo.version);
163
260
  const configPath = getCodexConfigPath();
164
- const snippet = `[hooks]\nnotify = "${hookPathForConfig}"\n`;
261
+ const snippet = notifyConfig.useHooksSection
262
+ ? `[hooks]\n${notifyConfig.line}\n`
263
+ : `${notifyConfig.line}\n`;
264
+ const claudeConfigPath = getClaudeSettingsPath();
265
+ const claudeSnippet = JSON.stringify({
266
+ hooks: {
267
+ SessionEnd: [
268
+ {
269
+ hooks: [
270
+ {
271
+ type: 'command',
272
+ command: claudeHookPathForConfig,
273
+ },
274
+ ],
275
+ },
276
+ ],
277
+ Stop: [
278
+ {
279
+ hooks: [
280
+ {
281
+ type: 'command',
282
+ command: claudeHookPathForConfig,
283
+ },
284
+ ],
285
+ },
286
+ ],
287
+ },
288
+ }, null, 2);
165
289
  console.log(kleur_1.default.bold().cyan('PromptVC Config'));
166
290
  console.log((0, branding_1.renderLogo)());
167
291
  console.log('');
168
292
  // Check Codex installation status
169
293
  const codexInstalled = isCodexInstalled();
170
294
  const codexConfigured = isCodexConfigured();
295
+ const claudeInstalled = isClaudeInstalled();
296
+ const claudeStatus = getClaudeHookStatus(claudeHookPathForConfig);
297
+ const claudeConfigured = claudeStatus.stop && claudeStatus.sessionEnd;
171
298
  console.log(`${kleur_1.default.bold('Codex status:')}`);
172
299
  if (codexInstalled) {
173
300
  console.log(` ${kleur_1.default.green('✓')} Codex is installed (${getCodexDir()} found)`);
@@ -183,18 +310,98 @@ const runConfigCommand = async () => {
183
310
  }
184
311
  else {
185
312
  console.log(` ${kleur_1.default.red('✗')} Codex not found (${getCodexDir()} does not exist)`);
186
- console.log(` ${kleur_1.default.yellow('→')} Install Codex from: https://www.codex.chat/`);
313
+ console.log(` ${kleur_1.default.yellow('→')} Install Codex: npm i -g @openai/codex`);
187
314
  }
188
315
  console.log('');
189
- console.log(`${kleur_1.default.bold('Hook path:')} ${hookPathForConfig}`);
316
+ console.log(`${kleur_1.default.bold('Codex hook path:')} ${hookPathForConfig}`);
190
317
  console.log(`${kleur_1.default.bold('Codex config:')} ${configPath}`);
318
+ console.log('');
319
+ console.log(`${kleur_1.default.bold('Claude status:')}`);
320
+ if (claudeInstalled) {
321
+ console.log(` ${kleur_1.default.green('✓')} Claude config directory detected (${getClaudeDir()} found)`);
322
+ if (claudeConfigured) {
323
+ console.log(` ${kleur_1.default.green('✓')} Claude is already configured with PromptVC`);
324
+ }
325
+ else if (claudeStatus.stop || claudeStatus.sessionEnd) {
326
+ console.log(` ${kleur_1.default.yellow('○')} Claude config partially configured (Stop: ${claudeStatus.stop ? 'yes' : 'no'}, SessionEnd: ${claudeStatus.sessionEnd ? 'yes' : 'no'})`);
327
+ }
328
+ else if (hasClaudeConfig()) {
329
+ console.log(` ${kleur_1.default.yellow('○')} Claude config exists but PromptVC not configured`);
330
+ }
331
+ else {
332
+ console.log(` ${kleur_1.default.yellow('○')} Claude config not found, will be created`);
333
+ }
334
+ }
335
+ else {
336
+ console.log(` ${kleur_1.default.red('✗')} Claude config directory not found (${getClaudeDir()} does not exist)`);
337
+ console.log(` ${kleur_1.default.yellow('→')} Install Claude Code to use this integration`);
338
+ }
339
+ console.log('');
340
+ console.log(`${kleur_1.default.bold('Claude hook path:')} ${claudeHookPathForConfig}`);
341
+ console.log(`${kleur_1.default.bold('Claude config:')} ${claudeConfigPath}`);
342
+ if (codexInfo.binaryPath) {
343
+ console.log(`${kleur_1.default.bold('Codex binary:')} ${codexInfo.binaryPath}`);
344
+ }
345
+ if (codexInfo.version) {
346
+ console.log(`${kleur_1.default.bold('Codex version:')} ${codexInfo.version}`);
347
+ }
348
+ else if (codexInfo.raw) {
349
+ console.log(`${kleur_1.default.bold('Codex version:')} ${codexInfo.raw}`);
350
+ }
351
+ if (npmInfo.binaryPath) {
352
+ console.log(`${kleur_1.default.bold('npm binary:')} ${npmInfo.binaryPath}`);
353
+ }
354
+ if (npmInfo.version) {
355
+ console.log(`${kleur_1.default.bold('npm version:')} ${npmInfo.version}`);
356
+ }
357
+ else if (npmInfo.raw) {
358
+ console.log(`${kleur_1.default.bold('npm version:')} ${npmInfo.raw}`);
359
+ }
360
+ const warnings = (0, toolVersions_1.getCodexVersionWarnings)(codexInfo.version);
361
+ if (warnings.length > 0) {
362
+ for (const warning of warnings) {
363
+ console.log(kleur_1.default.yellow(`Warning: ${warning}`));
364
+ }
365
+ }
366
+ const npmWarnings = (0, toolVersions_1.getNpmVersionWarnings)(npmInfo.version);
367
+ if (npmWarnings.length > 0) {
368
+ for (const warning of npmWarnings) {
369
+ console.log(kleur_1.default.yellow(`Warning: ${warning}`));
370
+ }
371
+ }
372
+ if (notifyConfig.useHooksSection) {
373
+ console.log(kleur_1.default.yellow('Warning: Codex is using the legacy [hooks] notify format; upgrade recommended.'));
374
+ }
375
+ console.log('');
376
+ const codexMismatch = (0, toolVersions_1.isVersionMismatch)(codexInfo.version, toolVersions_1.EXPECTED_CODEX_VERSION);
377
+ const npmMismatch = (0, toolVersions_1.isVersionMismatch)(npmInfo.version, toolVersions_1.EXPECTED_NPM_VERSION);
378
+ if ((codexMismatch || npmMismatch) && !(0, toolVersions_1.allowVersionMismatch)()) {
379
+ const reason = codexMismatch && npmMismatch
380
+ ? 'Codex and npm versions do not match the expected versions.'
381
+ : codexMismatch
382
+ ? 'Codex version does not match the expected version.'
383
+ : 'npm version does not match the expected version.';
384
+ console.log(kleur_1.default.yellow(`Warning: ${reason}`));
385
+ console.log(kleur_1.default.yellow('Set PROMPTVC_ALLOW_VERSION_MISMATCH=1 to bypass this check.'));
386
+ const shouldContinue = await (0, prompt_1.promptYesNo)('Continue anyway? (y/N): ');
387
+ if (!shouldContinue) {
388
+ process.exit(1);
389
+ }
390
+ console.log('');
391
+ }
191
392
  if (!fs_1.default.existsSync(hookPath)) {
192
393
  console.log(kleur_1.default.yellow('Warning: notify hook not found at this path.'));
193
394
  }
395
+ if (!fs_1.default.existsSync(claudeHookPath)) {
396
+ console.log(kleur_1.default.yellow('Warning: Claude notify hook not found at this path.'));
397
+ }
398
+ let updatedSomething = false;
194
399
  try {
195
400
  fs_1.default.mkdirSync(path_1.default.dirname(configPath), { recursive: true });
196
401
  const existing = fs_1.default.existsSync(configPath) ? fs_1.default.readFileSync(configPath, 'utf-8') : '';
197
- const updated = upsertNotifyHook(existing, hookPathForConfig);
402
+ const updated = notifyConfig.useHooksSection
403
+ ? upsertNotifyHookLegacy(existing, notifyConfig.line)
404
+ : upsertNotifyHook(existing, notifyConfig.line);
198
405
  fs_1.default.writeFileSync(configPath, updated, 'utf-8');
199
406
  console.log('');
200
407
  if (codexConfigured) {
@@ -208,7 +415,7 @@ const runConfigCommand = async () => {
208
415
  console.log(kleur_1.default.yellow('Note: Codex installation not detected. Install Codex to use this integration.'));
209
416
  }
210
417
  console.log('');
211
- console.log(kleur_1.default.bgBlue('Run "promptvc init" to initialize Prompt Version Control in your repository.'));
418
+ updatedSomething = true;
212
419
  }
213
420
  catch (error) {
214
421
  console.log('');
@@ -216,6 +423,40 @@ const runConfigCommand = async () => {
216
423
  console.log(`Add this to ${configPath}:`);
217
424
  console.log(snippet);
218
425
  }
426
+ try {
427
+ fs_1.default.mkdirSync(path_1.default.dirname(claudeConfigPath), { recursive: true });
428
+ const existingRaw = fs_1.default.existsSync(claudeConfigPath) ? fs_1.default.readFileSync(claudeConfigPath, 'utf-8') : '';
429
+ const existing = existingRaw.trim().length === 0 ? '{}' : existingRaw;
430
+ const parsed = JSON.parse(existing);
431
+ const stopResult = upsertClaudeEventHook(parsed, 'Stop', claudeHookPathForConfig);
432
+ const sessionEndResult = upsertClaudeEventHook(stopResult.updated, 'SessionEnd', claudeHookPathForConfig);
433
+ const claudeChanged = stopResult.changed || sessionEndResult.changed || existing.trim().length === 0;
434
+ if (claudeChanged) {
435
+ fs_1.default.writeFileSync(claudeConfigPath, JSON.stringify(sessionEndResult.updated, null, 2) + '\n', 'utf-8');
436
+ }
437
+ console.log('');
438
+ if (claudeConfigured) {
439
+ console.log(kleur_1.default.green('OK: Claude config verified (already configured).'));
440
+ }
441
+ else {
442
+ console.log(kleur_1.default.green('OK: Claude config updated successfully.'));
443
+ }
444
+ if (!claudeInstalled) {
445
+ console.log('');
446
+ console.log(kleur_1.default.yellow('Note: Claude installation not detected. Install Claude Code to use this integration.'));
447
+ }
448
+ console.log('');
449
+ updatedSomething = true;
450
+ }
451
+ catch (error) {
452
+ console.log('');
453
+ console.log(kleur_1.default.yellow('Manual setup required for Claude.'));
454
+ console.log(`Merge this into ${claudeConfigPath}:`);
455
+ console.log(claudeSnippet);
456
+ }
457
+ if (updatedSomething) {
458
+ console.log(kleur_1.default.bgBlue('Run "promptvc init" to initialize Prompt Version Control in your repository.'));
459
+ }
219
460
  };
220
461
  exports.runConfigCommand = runConfigCommand;
221
462
  //# sourceMappingURL=config.js.map