@mirnoorata/codexa 0.2.2 → 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.
Files changed (65) hide show
  1. package/README.md +93 -29
  2. package/dist/cli/hooks.js +11 -6
  3. package/dist/cli/hooks.js.map +1 -1
  4. package/dist/cli.js +13 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/implicit-baseline.d.ts +8 -0
  7. package/dist/implicit-baseline.js +94 -0
  8. package/dist/implicit-baseline.js.map +1 -0
  9. package/dist/init.d.ts +3 -0
  10. package/dist/init.js +124 -15
  11. package/dist/init.js.map +1 -1
  12. package/dist/mcp/compaction.d.ts +1 -0
  13. package/dist/mcp/compaction.js +24 -0
  14. package/dist/mcp/compaction.js.map +1 -1
  15. package/dist/mcp/envelope.d.ts +4 -1
  16. package/dist/mcp/envelope.js +45 -5
  17. package/dist/mcp/envelope.js.map +1 -1
  18. package/dist/mcp/prompts.d.ts +1 -1
  19. package/dist/mcp/prompts.js +5 -2
  20. package/dist/mcp/prompts.js.map +1 -1
  21. package/dist/mcp/tool-registry.d.ts +1 -0
  22. package/dist/mcp/tool-registry.js +5 -0
  23. package/dist/mcp/tool-registry.js.map +1 -1
  24. package/dist/mcp/tools.d.ts +1 -0
  25. package/dist/mcp/tools.js +6 -0
  26. package/dist/mcp/tools.js.map +1 -1
  27. package/dist/mcp-tool-catalog.d.ts +1 -1
  28. package/dist/mcp-tool-catalog.js +1 -1
  29. package/dist/mcp-tool-catalog.js.map +1 -1
  30. package/dist/mcp.js +10 -5
  31. package/dist/mcp.js.map +1 -1
  32. package/dist/query/post-edit/decision.d.ts +1 -0
  33. package/dist/query/post-edit/decision.js +13 -4
  34. package/dist/query/post-edit/decision.js.map +1 -1
  35. package/dist/query/post-edit.js +10 -2
  36. package/dist/query/post-edit.js.map +1 -1
  37. package/dist/task-snapshots.js +29 -0
  38. package/dist/task-snapshots.js.map +1 -1
  39. package/dist/types.d.ts +2 -0
  40. package/dist/types.js.map +1 -1
  41. package/integrations/.claude-plugin/marketplace.json +23 -0
  42. package/integrations/claude-code/.claude-plugin/plugin.json +16 -0
  43. package/integrations/claude-code/.mcp.json +8 -0
  44. package/integrations/claude-code/README.md +177 -0
  45. package/integrations/claude-code/commands/codexa-brief.md +14 -0
  46. package/integrations/claude-code/commands/codexa-impact.md +14 -0
  47. package/integrations/claude-code/commands/codexa-plan.md +20 -0
  48. package/integrations/claude-code/commands/codexa-review.md +23 -0
  49. package/integrations/claude-code/commands/codexa-status.md +10 -0
  50. package/integrations/claude-code/hooks/hooks.json +39 -0
  51. package/integrations/claude-code/scripts/cmd/brief.sh +18 -0
  52. package/integrations/claude-code/scripts/cmd/impact.sh +35 -0
  53. package/integrations/claude-code/scripts/cmd/lib.sh +136 -0
  54. package/integrations/claude-code/scripts/cmd/plan.sh +52 -0
  55. package/integrations/claude-code/scripts/cmd/review.sh +66 -0
  56. package/integrations/claude-code/scripts/cmd/status.sh +52 -0
  57. package/integrations/claude-code/scripts/codexa-mcp.js +111 -0
  58. package/integrations/claude-code/scripts/lib/codexa-repo.sh +773 -0
  59. package/integrations/claude-code/scripts/pre-edit.sh +116 -0
  60. package/integrations/claude-code/scripts/session-start.sh +201 -0
  61. package/integrations/claude-code/scripts/stop.sh +443 -0
  62. package/integrations/claude-code/tests/cmd-smoke.sh +310 -0
  63. package/integrations/claude-code/tests/hook-smoke.sh +1412 -0
  64. package/package.json +4 -2
  65. package/plugins/codexa/.codex-plugin/plugin.json +1 -1
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env bash
2
+ # Unit tests for the /codexa-* slash-command helper scripts. Each test
3
+ # invokes the shell helper with a synthetic "$ARGUMENTS" string and asserts
4
+ # on exit code / stderr. codexa CLI is stubbed so these tests don't require
5
+ # an index.
6
+ #
7
+ # Run: bash integrations/claude-code/tests/cmd-smoke.sh (from the codexa repo root)
8
+
9
+ set -u
10
+
11
+ INTEG_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
12
+ TMP="$(mktemp -d)"
13
+ trap 'rm -rf "$TMP"' EXIT
14
+
15
+ PASS=0
16
+ FAIL=0
17
+
18
+ pass() { PASS=$((PASS + 1)); printf ' PASS %s\n' "$1"; }
19
+ fail() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
20
+ section() { printf '\n== %s ==\n' "$1"; }
21
+
22
+ # Stub codexa CLI: prints each forwarded argument on its own line so tests
23
+ # can grep for exact values without worrying about shell escaping.
24
+ STUB_NODE="$TMP/stub-node"
25
+ STUB_CLI="$TMP/stub-cli.js"
26
+ : > "$STUB_CLI"
27
+ cat >"$STUB_NODE" <<'EOF'
28
+ #!/usr/bin/env bash
29
+ # Skip the first argument (path to the fake CLI), then dump the rest with
30
+ # one arg per line prefixed by `ARG: `.
31
+ shift
32
+ for a in "$@"; do
33
+ printf 'ARG: %s\n' "$a"
34
+ done
35
+ EOF
36
+ chmod +x "$STUB_NODE"
37
+
38
+ # Stub codexa CLI that fails with non-zero exit.
39
+ FAIL_NODE="$TMP/stub-fail"
40
+ cat >"$FAIL_NODE" <<'EOF'
41
+ #!/usr/bin/env bash
42
+ printf 'STUB ERROR: simulated failure\n' >&2
43
+ exit 3
44
+ EOF
45
+ chmod +x "$FAIL_NODE"
46
+
47
+ make_wired_repo() {
48
+ local dir="$1"
49
+ mkdir -p "$dir/.codex/cache/codexa-tasks"
50
+ cat >"$dir/.codex/config.toml" <<'TOML'
51
+ [features]
52
+ hooks = true
53
+ TOML
54
+ }
55
+
56
+ run_cmd() {
57
+ local helper="$1"
58
+ local argstr="$2"
59
+ local workdir="$3"
60
+ local node_bin="${4:-$STUB_NODE}"
61
+ local cli="${5:-$STUB_CLI}"
62
+ local stdout
63
+ local stderr
64
+ stdout="$(mktemp)"
65
+ stderr="$(mktemp)"
66
+ (
67
+ cd "$workdir"
68
+ env -i HOME="$HOME" PATH="$PATH" \
69
+ CLAUDIO_NODE_BIN="$node_bin" CODEXA_CLI="$cli" \
70
+ CLAUDE_PLUGIN_ROOT="$INTEG_ROOT" \
71
+ bash "$INTEG_ROOT/scripts/cmd/$helper" "$argstr"
72
+ ) >"$stdout" 2>"$stderr"
73
+ LAST_RC=$?
74
+ LAST_STDOUT="$(cat "$stdout")"
75
+ LAST_STDERR="$(cat "$stderr")"
76
+ rm -f "$stdout" "$stderr"
77
+ }
78
+
79
+ # ---------- status ----------
80
+ section "status.sh"
81
+
82
+ REPO="$TMP/repo"
83
+ make_wired_repo "$REPO"
84
+ run_cmd "status.sh" "" "$REPO"
85
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: status" \
86
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: $REPO"; then
87
+ pass "status invokes codexa status with absolute repo root"
88
+ else
89
+ fail "status invokes codexa status with absolute repo root" "rc=$LAST_RC stdout='$LAST_STDOUT'"
90
+ fi
91
+
92
+ EMPTY_PARENT="$TMP/empty"
93
+ mkdir -p "$EMPTY_PARENT"
94
+ run_cmd "status.sh" "" "$EMPTY_PARENT"
95
+ if [[ $LAST_RC -ne 0 ]] && printf '%s' "$LAST_STDERR" | grep -q "No codexa-wired"; then
96
+ pass "status exits non-zero outside a wired repo"
97
+ else
98
+ fail "status exits non-zero outside a wired repo" "rc=$LAST_RC stderr='$LAST_STDERR'"
99
+ fi
100
+
101
+ # Parent with exactly one wired child: auto-pick.
102
+ SOLO_PARENT="$TMP/solo"
103
+ mkdir -p "$SOLO_PARENT"
104
+ make_wired_repo "$SOLO_PARENT/only-child"
105
+ run_cmd "status.sh" "" "$SOLO_PARENT"
106
+ if [[ $LAST_RC -eq 0 ]] \
107
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: status" \
108
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: $SOLO_PARENT/only-child"; then
109
+ pass "status auto-picks sole wired child of PWD"
110
+ else
111
+ fail "status auto-picks sole wired child of PWD" "rc=$LAST_RC stdout='$LAST_STDOUT'"
112
+ fi
113
+
114
+ # Parent with multiple wired children: fan out, run status on each.
115
+ FAN_PARENT="$TMP/fan"
116
+ mkdir -p "$FAN_PARENT"
117
+ make_wired_repo "$FAN_PARENT/alpha"
118
+ make_wired_repo "$FAN_PARENT/beta"
119
+ run_cmd "status.sh" "" "$FAN_PARENT"
120
+ alpha_hits=$(printf '%s\n' "$LAST_STDOUT" | grep -Fc "ARG: $FAN_PARENT/alpha")
121
+ beta_hits=$(printf '%s\n' "$LAST_STDOUT" | grep -Fc "ARG: $FAN_PARENT/beta")
122
+ status_hits=$(printf '%s\n' "$LAST_STDOUT" | grep -Fxc "ARG: status")
123
+ if [[ $LAST_RC -eq 0 && $alpha_hits -eq 1 && $beta_hits -eq 1 && $status_hits -eq 2 ]] \
124
+ && printf '%s' "$LAST_STDERR" | grep -q "fanning out status across 2"; then
125
+ pass "status fans out across multiple wired children"
126
+ else
127
+ fail "status fans out across multiple wired children" \
128
+ "rc=$LAST_RC alpha=$alpha_hits beta=$beta_hits status=$status_hits stdout='$LAST_STDOUT' stderr='$LAST_STDERR'"
129
+ fi
130
+
131
+ # Fan-out must propagate a non-zero exit if any child fails.
132
+ run_cmd "status.sh" "" "$FAN_PARENT" "$FAIL_NODE"
133
+ if [[ $LAST_RC -ne 0 ]] && printf '%s' "$LAST_STDERR" | grep -q "simulated failure"; then
134
+ pass "status fan-out propagates non-zero exit when a child fails"
135
+ else
136
+ fail "status fan-out propagates non-zero exit when a child fails" \
137
+ "rc=$LAST_RC stderr='$LAST_STDERR'"
138
+ fi
139
+
140
+ # ---------- brief ----------
141
+ section "brief.sh"
142
+
143
+ run_cmd "brief.sh" "" "$REPO"
144
+ if [[ $LAST_RC -eq 2 ]] && printf '%s' "$LAST_STDERR" | grep -q "Usage"; then
145
+ pass "brief with no arguments prints usage and exits 2"
146
+ else
147
+ fail "brief with no arguments prints usage and exits 2" "rc=$LAST_RC stderr='$LAST_STDERR'"
148
+ fi
149
+
150
+ TASK="fix the scrollbar in frame back-face"
151
+ run_cmd "brief.sh" "$TASK" "$REPO"
152
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: $TASK" \
153
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: --diff"; then
154
+ pass "brief passes the full task string through --task, preserving spaces"
155
+ else
156
+ fail "brief passes the full task string through --task, preserving spaces" "rc=$LAST_RC stdout='$LAST_STDOUT'"
157
+ fi
158
+
159
+ # ---------- plan ----------
160
+ section "plan.sh"
161
+
162
+ run_cmd "plan.sh" "" "$REPO"
163
+ if [[ $LAST_RC -eq 2 ]] && printf '%s' "$LAST_STDERR" | grep -q "Usage"; then
164
+ pass "plan with no arguments prints usage and exits 2"
165
+ else
166
+ fail "plan with no arguments prints usage and exits 2" "rc=$LAST_RC stderr='$LAST_STDERR'"
167
+ fi
168
+
169
+ run_cmd "plan.sh" '"fix auth bug" src/auth.py' "$REPO"
170
+ if [[ $LAST_RC -eq 0 ]] \
171
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: fix auth bug" \
172
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: src/auth.py" \
173
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: --save-snapshot"; then
174
+ pass "plan parses quoted task and one file via shlex"
175
+ else
176
+ fail "plan parses quoted task and one file via shlex" "rc=$LAST_RC stdout='$LAST_STDOUT'"
177
+ fi
178
+
179
+ run_cmd "plan.sh" '"redesign" "web/src/App.tsx" "web/src/styles.css"' "$REPO"
180
+ fileflags=$(printf '%s\n' "$LAST_STDOUT" | grep -Fc "ARG: --file")
181
+ if [[ $LAST_RC -eq 0 && $fileflags -eq 2 ]] \
182
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: web/src/App.tsx" \
183
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: web/src/styles.css"; then
184
+ pass "plan parses multiple quoted file paths"
185
+ else
186
+ fail "plan parses multiple quoted file paths" "rc=$LAST_RC file-flags=$fileflags stdout='$LAST_STDOUT'"
187
+ fi
188
+
189
+ run_cmd "plan.sh" '"fix spaces" "path with spaces/file.ts"' "$REPO"
190
+ if printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: path with spaces/file.ts"; then
191
+ pass "plan preserves a path containing spaces"
192
+ else
193
+ fail "plan preserves a path containing spaces" "stdout='$LAST_STDOUT'"
194
+ fi
195
+
196
+ # Semicolon injection: passes through as one --file value, not as a shell
197
+ # command. Nothing matching `^rm: ` should appear in stdout.
198
+ run_cmd "plan.sh" '"malicious" "src/foo.py; rm -rf /"' "$REPO"
199
+ if printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: src/foo.py; rm -rf /" \
200
+ && ! printf '%s' "$LAST_STDOUT" | grep -q "^rm: "; then
201
+ pass "plan does not execute injected shell metachars in file tokens"
202
+ else
203
+ fail "plan does not execute injected shell metachars in file tokens" "rc=$LAST_RC stdout='$LAST_STDOUT'"
204
+ fi
205
+
206
+ # Path with embedded newline (preserved through shlex via NUL delimiter):
207
+ # cmd_validate_path_token rejects before the CLI is invoked.
208
+ run_cmd "plan.sh" $'"evil" "bad\nnewline"' "$REPO"
209
+ if [[ $LAST_RC -ne 0 ]] && printf '%s' "$LAST_STDERR" | grep -q "control character"; then
210
+ pass "plan rejects path tokens with embedded newlines"
211
+ else
212
+ fail "plan rejects path tokens with embedded newlines" "rc=$LAST_RC stderr='$LAST_STDERR'"
213
+ fi
214
+
215
+ # Unbalanced quote: shlex raises, cmd_shlex_split returns 2.
216
+ run_cmd "plan.sh" '"oops' "$REPO"
217
+ if [[ $LAST_RC -eq 2 ]] && printf '%s' "$LAST_STDERR" | grep -q "argument parse error"; then
218
+ pass "plan rejects unbalanced quotes"
219
+ else
220
+ fail "plan rejects unbalanced quotes" "rc=$LAST_RC stderr='$LAST_STDERR'"
221
+ fi
222
+
223
+ # ---------- review ----------
224
+ section "review.sh"
225
+
226
+ # Without a snapshot: exits 1
227
+ run_cmd "review.sh" "" "$REPO"
228
+ if [[ $LAST_RC -eq 1 ]] && printf '%s' "$LAST_STDERR" | grep -q "No change-plan snapshot"; then
229
+ pass "review refuses to run without a saved snapshot"
230
+ else
231
+ fail "review refuses to run without a saved snapshot" "rc=$LAST_RC stderr='$LAST_STDERR'"
232
+ fi
233
+
234
+ echo '{"taskId":"t","path":"t.json","createdAt":"now"}' >"$REPO/.codex/cache/codexa-tasks/latest.json"
235
+
236
+ run_cmd "review.sh" "--change-type style" "$REPO"
237
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: post-edit-review" \
238
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: --change-type" \
239
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: style"; then
240
+ pass "review uses canonical post-edit-review and passes allowlisted --change-type through"
241
+ else
242
+ fail "review uses canonical post-edit-review and passes allowlisted --change-type through" "rc=$LAST_RC stdout='$LAST_STDOUT'"
243
+ fi
244
+
245
+ run_cmd "review.sh" "--change-type behavior --ran-test tests/test_foo.py" "$REPO"
246
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: --ran-test" \
247
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: tests/test_foo.py"; then
248
+ pass "review passes --ran-test through"
249
+ else
250
+ fail "review passes --ran-test through" "rc=$LAST_RC stdout='$LAST_STDOUT'"
251
+ fi
252
+
253
+ run_cmd "review.sh" "--evil-flag foo" "$REPO"
254
+ if [[ $LAST_RC -ne 0 ]] && printf '%s' "$LAST_STDERR" | grep -q "refusing unknown flag"; then
255
+ pass "review rejects unknown flags"
256
+ else
257
+ fail "review rejects unknown flags" "rc=$LAST_RC stderr='$LAST_STDERR'"
258
+ fi
259
+
260
+ run_cmd "review.sh" "tests/test_foo.py" "$REPO"
261
+ if [[ $LAST_RC -ne 0 ]] && printf '%s' "$LAST_STDERR" | grep -q "positional arguments"; then
262
+ pass "review rejects bare positional arguments"
263
+ else
264
+ fail "review rejects bare positional arguments" "rc=$LAST_RC stderr='$LAST_STDERR'"
265
+ fi
266
+
267
+ # Failed post-edit: the stub returns non-zero; review must propagate it.
268
+ run_cmd "review.sh" "--change-type style" "$REPO" "$FAIL_NODE"
269
+ if [[ $LAST_RC -eq 3 ]] && printf '%s' "$LAST_STDERR" | grep -q "simulated failure"; then
270
+ pass "review propagates codexa CLI non-zero exit"
271
+ else
272
+ fail "review propagates codexa CLI non-zero exit" "rc=$LAST_RC stderr='$LAST_STDERR'"
273
+ fi
274
+
275
+ # ---------- impact ----------
276
+ section "impact.sh"
277
+
278
+ run_cmd "impact.sh" "" "$REPO"
279
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: diff-impact"; then
280
+ pass "impact with no argument falls back to diff-impact"
281
+ else
282
+ fail "impact with no argument falls back to diff-impact" "rc=$LAST_RC stdout='$LAST_STDOUT'"
283
+ fi
284
+
285
+ touch "$REPO/src-x.ts"
286
+ run_cmd "impact.sh" "src-x.ts" "$REPO"
287
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: --file" \
288
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: src-x.ts"; then
289
+ pass "impact with existing path uses --file"
290
+ else
291
+ fail "impact with existing path uses --file" "rc=$LAST_RC stdout='$LAST_STDOUT'"
292
+ fi
293
+
294
+ run_cmd "impact.sh" "SomeSymbolName" "$REPO"
295
+ if [[ $LAST_RC -eq 0 ]] && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: --symbol" \
296
+ && printf '%s' "$LAST_STDOUT" | grep -Fxq "ARG: SomeSymbolName"; then
297
+ pass "impact with non-existing path uses --symbol"
298
+ else
299
+ fail "impact with non-existing path uses --symbol" "rc=$LAST_RC stdout='$LAST_STDOUT'"
300
+ fi
301
+
302
+ run_cmd "impact.sh" $'"bad\nnewline"' "$REPO"
303
+ if [[ $LAST_RC -ne 0 ]] && printf '%s' "$LAST_STDERR" | grep -q "control character"; then
304
+ pass "impact rejects tokens with embedded newlines"
305
+ else
306
+ fail "impact rejects tokens with embedded newlines" "rc=$LAST_RC stderr='$LAST_STDERR'"
307
+ fi
308
+
309
+ printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
310
+ [[ $FAIL -eq 0 ]]