@quantiya/codevibe-codex-plugin 2.0.11 → 2.0.13

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.
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+
7
+ const OUR_HOOK_FILES = new Set([
8
+ 'session-start.sh',
9
+ 'user-prompt.sh',
10
+ 'pre-tool-use.sh',
11
+ 'permission-request.sh',
12
+ 'post-tool-use.sh',
13
+ 'stop.sh',
14
+ ]);
15
+
16
+ function hookScriptPath(command) {
17
+ if (typeof command !== 'string') return null;
18
+ const match = command.trim().match(/^bash\s+(?:"([^"]+)"|'([^']+)'|(\S+))$/);
19
+ return match ? (match[1] || match[2] || match[3]) : null;
20
+ }
21
+
22
+ function isOurCommand(command) {
23
+ const scriptPath = hookScriptPath(command);
24
+ if (!scriptPath) return false;
25
+
26
+ const filename = scriptPath.slice(scriptPath.lastIndexOf('/') + 1);
27
+ if (!OUR_HOOK_FILES.has(filename) || !scriptPath.endsWith(`/hooks/${filename}`)) {
28
+ return false;
29
+ }
30
+
31
+ if (scriptPath.includes('/codevibe-codex-plugin/hooks/')) {
32
+ return true;
33
+ }
34
+
35
+ // Development links can use task-specific repository names rather than the
36
+ // canonical package directory. Keep this deliberately limited to CodeVibe's
37
+ // agent-worktree layout so an unrelated hook with the same filename survives.
38
+ return /\/\.agent-worktrees\/[^/]+\/codevibe-codex-[^/]+\/hooks\/[^/]+$/.test(scriptPath);
39
+ }
40
+
41
+ function stripOurHooksFromEntry(entry) {
42
+ if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) {
43
+ return entry;
44
+ }
45
+ const filtered = entry.hooks.filter((hook) => !(hook && isOurCommand(hook.command)));
46
+ if (filtered.length === entry.hooks.length) return entry;
47
+ if (filtered.length === 0) return null;
48
+ return { ...entry, hooks: filtered };
49
+ }
50
+
51
+ function ownedCommands(entries) {
52
+ return entries
53
+ .flatMap((entry) => entry && typeof entry === 'object' && Array.isArray(entry.hooks)
54
+ ? entry.hooks
55
+ : [])
56
+ .map((hook) => hook && hook.command)
57
+ .filter(isOurCommand)
58
+ .sort();
59
+ }
60
+
61
+ function mergeHookConfig(existingObj, nextObj, existed = true) {
62
+ const existingHooks = existingObj && typeof existingObj === 'object'
63
+ && existingObj.hooks && typeof existingObj.hooks === 'object'
64
+ ? existingObj.hooks
65
+ : {};
66
+ const nextHooks = nextObj && typeof nextObj === 'object'
67
+ && nextObj.hooks && typeof nextObj.hooks === 'object'
68
+ ? nextObj.hooks
69
+ : {};
70
+
71
+ let allPresent = existed;
72
+ for (const key of Object.keys(nextHooks)) {
73
+ const existingEntries = Array.isArray(existingHooks[key]) ? existingHooks[key] : [];
74
+ const nextEntries = Array.isArray(nextHooks[key]) ? nextHooks[key] : [];
75
+ if (JSON.stringify(ownedCommands(existingEntries)) !== JSON.stringify(ownedCommands(nextEntries))) {
76
+ allPresent = false;
77
+ break;
78
+ }
79
+ }
80
+ if (allPresent) {
81
+ return { outcome: 'already_installed', config: existingObj };
82
+ }
83
+
84
+ const mergedHooks = { ...existingHooks };
85
+ for (const key of Object.keys(nextHooks)) {
86
+ const existingEntries = Array.isArray(existingHooks[key]) ? existingHooks[key] : [];
87
+ const cleanedExisting = existingEntries
88
+ .map(stripOurHooksFromEntry)
89
+ .filter((entry) => entry !== null && entry !== undefined);
90
+ const nextEntries = Array.isArray(nextHooks[key]) ? nextHooks[key] : [];
91
+ mergedHooks[key] = [...cleanedExisting, ...nextEntries];
92
+ }
93
+
94
+ return {
95
+ outcome: existed ? 'merged' : 'fresh_install',
96
+ config: existingObj && typeof existingObj === 'object'
97
+ ? { ...existingObj, hooks: mergedHooks }
98
+ : { hooks: mergedHooks },
99
+ };
100
+ }
101
+
102
+ function cleanHookConfig(existingObj) {
103
+ if (!existingObj || typeof existingObj !== 'object'
104
+ || !existingObj.hooks || typeof existingObj.hooks !== 'object') {
105
+ return existingObj;
106
+ }
107
+
108
+ const hooks = { ...existingObj.hooks };
109
+ for (const key of Object.keys(hooks)) {
110
+ if (!Array.isArray(hooks[key])) continue;
111
+ hooks[key] = hooks[key]
112
+ .map(stripOurHooksFromEntry)
113
+ .filter((entry) => entry !== null && entry !== undefined);
114
+ }
115
+ return { ...existingObj, hooks };
116
+ }
117
+
118
+ function writeAtomic(target, value) {
119
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
120
+ try {
121
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2));
122
+ fs.renameSync(temporary, target);
123
+ } catch (error) {
124
+ try { fs.unlinkSync(temporary); } catch {}
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ function runMerge() {
130
+ const target = process.env.CV_EXISTING;
131
+ let existingObj = null;
132
+ let existed = false;
133
+ try {
134
+ if (fs.existsSync(target)) {
135
+ existed = true;
136
+ existingObj = JSON.parse(fs.readFileSync(target, 'utf8'));
137
+ }
138
+ } catch (error) {
139
+ process.stderr.write(`PARSE_EXISTING_FAILED:${error.message || String(error)}`);
140
+ return 2;
141
+ }
142
+
143
+ let nextObj;
144
+ try {
145
+ nextObj = JSON.parse(process.env.CV_NEW);
146
+ } catch (error) {
147
+ process.stderr.write(`PARSE_NEW_FAILED:${error.message || String(error)}`);
148
+ return 3;
149
+ }
150
+
151
+ const result = mergeHookConfig(existingObj, nextObj, existed);
152
+ if (result.outcome !== 'already_installed') {
153
+ try {
154
+ writeAtomic(target, result.config);
155
+ } catch (error) {
156
+ process.stderr.write(`WRITE_FAILED:${error.message || String(error)}`);
157
+ return 4;
158
+ }
159
+ }
160
+ process.stdout.write(`OUTCOME:${result.outcome}\n`);
161
+ return 0;
162
+ }
163
+
164
+ function runCleanup() {
165
+ const target = process.env.CV_HOOKS_FILE;
166
+ try {
167
+ const existingObj = JSON.parse(fs.readFileSync(target, 'utf8'));
168
+ writeAtomic(target, cleanHookConfig(existingObj));
169
+ return 0;
170
+ } catch (error) {
171
+ process.stderr.write(`CLEAN_FAILED:${error.message || String(error)}`);
172
+ return 1;
173
+ }
174
+ }
175
+
176
+ if (require.main === module) {
177
+ const action = process.argv[2];
178
+ const exitCode = action === 'merge'
179
+ ? runMerge()
180
+ : action === 'cleanup'
181
+ ? runCleanup()
182
+ : 64;
183
+ process.exit(exitCode);
184
+ }
185
+
186
+ module.exports = {
187
+ cleanHookConfig,
188
+ isOurCommand,
189
+ mergeHookConfig,
190
+ };
@@ -259,55 +259,10 @@ cleanup() {
259
259
  # Only remove if no other codevibe-codex sessions are running
260
260
  if [ "$(pgrep -f 'codevibe-codex' | wc -l)" -le 1 ]; then
261
261
  log "Removing CodeVibe hooks from $HOME/.codex/hooks.json"
262
- # Cleanup mirrors install: same ownership predicate + the
263
- # same INNER-hooks[] split so a matcher entry mixing user
264
- # hooks with ours keeps the user hook. Same per-process
265
- # tmp suffix avoids two concurrent cleanups racing.
266
- if CV_HOOKS_FILE="$HOME/.codex/hooks.json" node -e '
267
- const fs = require("fs");
268
- const OUR_HOOK_FILES = new Set([
269
- "session-start.sh",
270
- "user-prompt.sh",
271
- "pre-tool-use.sh",
272
- "post-tool-use.sh",
273
- "stop.sh",
274
- ]);
275
- const OWN_PATH_MARKER = "codevibe-codex-plugin/hooks/";
276
- const isOurCommand = (command) => {
277
- if (typeof command !== "string") return false;
278
- if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
279
- for (const f of OUR_HOOK_FILES) {
280
- if (command.endsWith("/" + f)) return true;
281
- }
282
- return false;
283
- };
284
- const stripOurHooksFromEntry = (entry) => {
285
- if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) return entry;
286
- const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
287
- if (filtered.length === entry.hooks.length) return entry;
288
- if (filtered.length === 0) return null;
289
- return { ...entry, hooks: filtered };
290
- };
291
- try {
292
- const target = process.env.CV_HOOKS_FILE;
293
- const data = JSON.parse(fs.readFileSync(target, "utf8"));
294
- if (data && typeof data === "object" && data.hooks && typeof data.hooks === "object") {
295
- for (const k of Object.keys(data.hooks)) {
296
- if (Array.isArray(data.hooks[k])) {
297
- data.hooks[k] = data.hooks[k]
298
- .map(stripOurHooksFromEntry)
299
- .filter((entry) => entry !== null && entry !== undefined);
300
- }
301
- }
302
- }
303
- const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
304
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
305
- fs.renameSync(tmp, target);
306
- } catch (e) {
307
- process.stderr.write("CLEAN_FAILED:" + (e && e.message ? e.message : String(e)));
308
- process.exit(1);
309
- }
310
- ' 2>>"$LOG_FILE"; then
262
+ # The shared helper keeps install and cleanup ownership rules in
263
+ # lockstep, including task-named CodeVibe agent worktrees.
264
+ if CV_HOOKS_FILE="$HOME/.codex/hooks.json" \
265
+ node "$PLUGIN_DIR/libexec/codex-hook-config.js" cleanup 2>>"$LOG_FILE"; then
311
266
  :
312
267
  else
313
268
  log "WARN: failed to clean CodeVibe hooks from hooks.json (left as-is)"
@@ -539,9 +494,9 @@ _CV_HOOKS_REASON=""
539
494
  # keeps the user hook (only the CodeVibe hooks get stripped, not
540
495
  # the surrounding entry).
541
496
  #
542
- # - Ownership predicate: path-prefix `codevibe-codex-plugin/hooks/`
543
- # AND filename tail in the allow-list of our hook script names.
544
- # Rejects user paths like /Users/codevibe-codex/foo.sh.
497
+ # - Ownership predicate: canonical package paths or CodeVibe's strict
498
+ # `.agent-worktrees/<agent>/codevibe-codex-*/hooks/` layout, plus a
499
+ # filename allowlist. Rejects unrelated user hook paths.
545
500
  #
546
501
  # - Per-process tmp file (.<pid>.<time>.tmp): two concurrent installs
547
502
  # won't clobber each other's intermediate state. Final rename is
@@ -555,112 +510,8 @@ _CV_HOOKS_REASON=""
555
510
  GENERATED_HOOKS=$(generate_hooks_json)
556
511
  if INSTALLER_OUTPUT=$(CV_EXISTING="$CODEX_HOOKS_FILE" \
557
512
  CV_NEW="$GENERATED_HOOKS" \
558
- node -e '
559
- const fs = require("fs");
560
- const OUR_HOOK_FILES = new Set([
561
- "session-start.sh",
562
- "user-prompt.sh",
563
- "pre-tool-use.sh",
564
- "permission-request.sh",
565
- "post-tool-use.sh",
566
- "stop.sh",
567
- ]);
568
- const OWN_PATH_MARKER = "codevibe-codex-plugin/hooks/";
569
- const isOurCommand = (command) => {
570
- if (typeof command !== "string") return false;
571
- if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
572
- for (const f of OUR_HOOK_FILES) {
573
- if (command.endsWith("/" + f)) return true;
574
- }
575
- return false;
576
- };
577
- // Strip our hooks from a single matcher entrys inner hooks[] array.
578
- // Returns null if NO non-owned hooks remain (whole entry should be
579
- // dropped); otherwise returns a new entry with only the user-owned
580
- // hooks. Returns the original entry unchanged if it contains none of
581
- // ours (preserves identity for unrelated entries).
582
- const stripOurHooksFromEntry = (entry) => {
583
- if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) {
584
- return entry;
585
- }
586
- const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
587
- if (filtered.length === entry.hooks.length) return entry;
588
- if (filtered.length === 0) return null;
589
- return { ...entry, hooks: filtered };
590
- };
591
- const ownedCommands = (entries) => entries
592
- .flatMap((entry) => entry && typeof entry === "object" && Array.isArray(entry.hooks)
593
- ? entry.hooks
594
- : [])
595
- .map((hook) => hook && hook.command)
596
- .filter(isOurCommand)
597
- .sort();
598
-
599
- let existingObj = null;
600
- let existed = false;
601
- try {
602
- if (fs.existsSync(process.env.CV_EXISTING)) {
603
- existed = true;
604
- existingObj = JSON.parse(fs.readFileSync(process.env.CV_EXISTING, "utf8"));
605
- }
606
- } catch (e) {
607
- process.stderr.write("PARSE_EXISTING_FAILED:" + (e && e.message ? e.message : String(e)));
608
- process.exit(2);
609
- }
610
- let nextObj;
611
- try {
612
- nextObj = JSON.parse(process.env.CV_NEW);
613
- } catch (e) {
614
- process.stderr.write("PARSE_NEW_FAILED:" + (e && e.message ? e.message : String(e)));
615
- process.exit(3);
616
- }
617
-
618
- const existingHooks = (existingObj && typeof existingObj === "object" &&
619
- existingObj.hooks && typeof existingObj.hooks === "object") ? existingObj.hooks : {};
620
- const nextHooks = (nextObj && typeof nextObj === "object" &&
621
- nextObj.hooks && typeof nextObj.hooks === "object") ? nextObj.hooks : {};
622
- let allPresent = existed;
623
- for (const k of Object.keys(nextHooks)) {
624
- const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
625
- const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
626
- if (JSON.stringify(ownedCommands(existingArr)) !== JSON.stringify(ownedCommands(nextArr))) {
627
- allPresent = false;
628
- break;
629
- }
630
- }
631
- if (allPresent) {
632
- process.stdout.write("OUTCOME:already_installed\n");
633
- process.exit(0);
634
- }
635
-
636
- const mergedHooks = { ...existingHooks };
637
- for (const k of Object.keys(nextHooks)) {
638
- const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
639
- const cleanedExisting = existingArr
640
- .map(stripOurHooksFromEntry)
641
- .filter((entry) => entry !== null && entry !== undefined);
642
- const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
643
- mergedHooks[k] = [...cleanedExisting, ...nextArr];
644
- }
645
-
646
- const out = (existingObj && typeof existingObj === "object")
647
- ? { ...existingObj, hooks: mergedHooks }
648
- : { hooks: mergedHooks };
649
- const outcome = existed ? "merged" : "fresh_install";
650
- const target = process.env.CV_EXISTING;
651
- // Per-process tmp suffix avoids two concurrent installs both writing
652
- // ${target}.tmp and one mvs the others half-written file.
653
- const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
654
- try {
655
- fs.writeFileSync(tmp, JSON.stringify(out, null, 2));
656
- fs.renameSync(tmp, target);
657
- } catch (e) {
658
- try { fs.unlinkSync(tmp); } catch {}
659
- process.stderr.write("WRITE_FAILED:" + (e && e.message ? e.message : String(e)));
660
- process.exit(4);
661
- }
662
- process.stdout.write("OUTCOME:" + outcome + "\n");
663
- ' 2>>"$LOG_FILE"); then
513
+ node "$PLUGIN_DIR/libexec/codex-hook-config.js" merge \
514
+ 2>>"$LOG_FILE"); then
664
515
  INSTALLER_RC=0
665
516
  else
666
517
  INSTALLER_RC=$?
@@ -331,8 +331,8 @@ ifeq ($(strip $(foreach prefix,$(NO_LOAD),\
331
331
  endif
332
332
 
333
333
  quiet_cmd_regen_makefile = ACTION Regenerating $@
334
- cmd_regen_makefile = cd $(srcdir); /opt/homebrew/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/hendryyeh/Library/Caches/node-gyp/24.4.0" "-Dnode_gyp_dir=/opt/homebrew/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/hendryyeh/Library/Caches/node-gyp/24.4.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/hendryyeh/Workspace/CodeVibe/codevibe-codex-plugin/node_modules/fs-ext" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/hendryyeh/Workspace/CodeVibe/codevibe-codex-plugin/node_modules/fs-ext/build/config.gypi -I/opt/homebrew/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/hendryyeh/Library/Caches/node-gyp/24.4.0/include/node/common.gypi "--toplevel-dir=." binding.gyp
335
- Makefile: $(srcdir)/binding.gyp $(srcdir)/../../../../../Library/Caches/node-gyp/24.4.0/include/node/common.gypi $(srcdir)/build/config.gypi $(srcdir)/../../../../../../../opt/homebrew/lib/node_modules/npm/node_modules/node-gyp/addon.gypi
334
+ cmd_regen_makefile = cd $(srcdir); /opt/homebrew/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/hendryyeh/Library/Caches/node-gyp/24.4.0" "-Dnode_gyp_dir=/opt/homebrew/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/hendryyeh/Library/Caches/node-gyp/24.4.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/hendryyeh/Workspace/CodeVibe/.agent-worktrees/codex-1/codevibe-codex-open-prompt-resolution/node_modules/fs-ext" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/hendryyeh/Workspace/CodeVibe/.agent-worktrees/codex-1/codevibe-codex-open-prompt-resolution/node_modules/fs-ext/build/config.gypi -I/opt/homebrew/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/hendryyeh/Library/Caches/node-gyp/24.4.0/include/node/common.gypi "--toplevel-dir=." binding.gyp
335
+ Makefile: $(srcdir)/../../../../../../../Library/Caches/node-gyp/24.4.0/include/node/common.gypi $(srcdir)/build/config.gypi $(srcdir)/binding.gyp $(srcdir)/../../../../../../../../../opt/homebrew/lib/node_modules/npm/node_modules/node-gyp/addon.gypi
336
336
  $(call do_cmd,regen_makefile)
337
337
 
338
338
  # "all" is a concatenation of the "all" targets from all the included
@@ -490,7 +490,7 @@
490
490
  "python": "/opt/homebrew/opt/python@3.14/bin/python3.14",
491
491
  "standalone_static_library": 1,
492
492
  "global_prefix": "/opt/homebrew",
493
- "local_prefix": "/Users/hendryyeh/Workspace/CodeVibe/codevibe-codex-plugin",
493
+ "local_prefix": "/Users/hendryyeh/Workspace/CodeVibe/.agent-worktrees/codex-1/codevibe-codex-open-prompt-resolution",
494
494
  "globalconfig": "/opt/homebrew/etc/npmrc",
495
495
  "init_module": "/Users/hendryyeh/.npm-init.js",
496
496
  "userconfig": "/Users/hendryyeh/.npmrc",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-codex-plugin",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
4
4
  "description": "Control OpenAI Codex CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
5
5
  "main": "dist/server.js",
6
6
  "codevibe": {
@@ -53,6 +53,7 @@
53
53
  "dotenv": "^16.6.1",
54
54
  "express": "^5.1.0",
55
55
  "graphql": "^16.12.0",
56
+ "shell-quote": "1.9.0",
56
57
  "uuid": "^13.0.0",
57
58
  "ws": "^8.18.3"
58
59
  },
@@ -63,6 +64,7 @@
63
64
  "dotenv",
64
65
  "express",
65
66
  "graphql",
67
+ "shell-quote",
66
68
  "uuid",
67
69
  "ws"
68
70
  ],
@@ -85,6 +87,7 @@
85
87
  "dotenv",
86
88
  "express",
87
89
  "graphql",
90
+ "shell-quote",
88
91
  "uuid",
89
92
  "ws"
90
93
  ]