oh-my-codex 0.17.3 → 0.18.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 (158) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +1 -0
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2940 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +4 -0
  11. package/crates/omx-sparkshell/src/main.rs +738 -29
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +479 -238
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/explore.test.js +23 -0
  24. package/dist/cli/__tests__/explore.test.js.map +1 -1
  25. package/dist/cli/__tests__/index.test.js +123 -5
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/launch-fallback.test.js +76 -0
  28. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  29. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  30. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  31. package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
  32. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  33. package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
  34. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  35. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  36. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  37. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  38. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  39. package/dist/cli/api.d.ts +26 -0
  40. package/dist/cli/api.d.ts.map +1 -0
  41. package/dist/cli/api.js +153 -0
  42. package/dist/cli/api.js.map +1 -0
  43. package/dist/cli/explore.d.ts +2 -0
  44. package/dist/cli/explore.d.ts.map +1 -1
  45. package/dist/cli/explore.js +43 -1
  46. package/dist/cli/explore.js.map +1 -1
  47. package/dist/cli/index.d.ts +10 -4
  48. package/dist/cli/index.d.ts.map +1 -1
  49. package/dist/cli/index.js +128 -10
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/native-assets.d.ts +2 -1
  52. package/dist/cli/native-assets.d.ts.map +1 -1
  53. package/dist/cli/native-assets.js +1 -0
  54. package/dist/cli/native-assets.js.map +1 -1
  55. package/dist/cli/sparkshell.d.ts.map +1 -1
  56. package/dist/cli/sparkshell.js +20 -3
  57. package/dist/cli/sparkshell.js.map +1 -1
  58. package/dist/config/generator.d.ts.map +1 -1
  59. package/dist/config/generator.js +90 -0
  60. package/dist/config/generator.js.map +1 -1
  61. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  62. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  63. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  64. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  65. package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
  66. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  67. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  68. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  69. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  70. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  71. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  72. package/dist/hooks/keyword-registry.js +1 -0
  73. package/dist/hooks/keyword-registry.js.map +1 -1
  74. package/dist/hud/__tests__/reconcile.test.js +2 -2
  75. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  76. package/dist/hud/__tests__/tmux.test.js +23 -18
  77. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  78. package/dist/hud/tmux.d.ts.map +1 -1
  79. package/dist/hud/tmux.js +7 -6
  80. package/dist/hud/tmux.js.map +1 -1
  81. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  82. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  83. package/dist/mcp/bootstrap.d.ts +3 -1
  84. package/dist/mcp/bootstrap.d.ts.map +1 -1
  85. package/dist/mcp/bootstrap.js +71 -2
  86. package/dist/mcp/bootstrap.js.map +1 -1
  87. package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
  88. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  89. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  90. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  91. package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
  92. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  93. package/dist/scripts/build-api.d.ts +2 -0
  94. package/dist/scripts/build-api.d.ts.map +1 -0
  95. package/dist/scripts/build-api.js +44 -0
  96. package/dist/scripts/build-api.js.map +1 -0
  97. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  98. package/dist/scripts/codex-native-hook.js +208 -8
  99. package/dist/scripts/codex-native-hook.js.map +1 -1
  100. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  101. package/dist/scripts/codex-native-pre-post.js +89 -24
  102. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  103. package/dist/scripts/notify-dispatcher.js +88 -0
  104. package/dist/scripts/notify-dispatcher.js.map +1 -1
  105. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  106. package/dist/scripts/notify-hook/team-dispatch.js +27 -9
  107. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  108. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  109. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  110. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  111. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
  112. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  113. package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
  114. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  115. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  116. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  117. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  118. package/dist/scripts/run-provider-advisor.js +9 -3
  119. package/dist/scripts/run-provider-advisor.js.map +1 -1
  120. package/dist/scripts/smoke-packed-install.d.ts +1 -1
  121. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  122. package/dist/scripts/smoke-packed-install.js +2 -0
  123. package/dist/scripts/smoke-packed-install.js.map +1 -1
  124. package/dist/team/__tests__/runtime.test.js +2 -2
  125. package/dist/team/__tests__/runtime.test.js.map +1 -1
  126. package/dist/team/__tests__/tmux-session.test.js +96 -19
  127. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  128. package/dist/team/tmux-session.d.ts +1 -0
  129. package/dist/team/tmux-session.d.ts.map +1 -1
  130. package/dist/team/tmux-session.js +34 -10
  131. package/dist/team/tmux-session.js.map +1 -1
  132. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  133. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  134. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  135. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  136. package/package.json +4 -3
  137. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  138. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  139. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
  140. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  141. package/prompts/researcher.md +15 -10
  142. package/skills/best-practice-research/SKILL.md +83 -0
  143. package/skills/deep-interview/SKILL.md +1 -0
  144. package/skills/ralplan/SKILL.md +1 -1
  145. package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
  146. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  147. package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
  148. package/src/scripts/build-api.ts +48 -0
  149. package/src/scripts/codex-native-hook.ts +262 -10
  150. package/src/scripts/codex-native-pre-post.ts +103 -24
  151. package/src/scripts/notify-dispatcher.ts +97 -0
  152. package/src/scripts/notify-hook/team-dispatch.ts +27 -8
  153. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  154. package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
  155. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  156. package/src/scripts/run-provider-advisor.ts +11 -3
  157. package/src/scripts/smoke-packed-install.ts +2 -0
  158. package/templates/catalog-manifest.json +7 -0
@@ -1,4 +1,5 @@
1
1
  use crate::exec::CommandOutput;
2
+ use crate::redaction::redact_output;
2
3
  use std::borrow::Cow;
3
4
  use std::env;
4
5
  use std::path::Path;
@@ -111,8 +112,10 @@ fn command_basename(command: &str) -> Cow<'_, str> {
111
112
  pub fn build_summary_prompt(command: &[String], output: &CommandOutput) -> String {
112
113
  let executable = command.first().map(String::as_str).unwrap_or("unknown");
113
114
  let family = select_command_family(executable);
114
- let stdout_text = output.stdout_text();
115
- let stderr_text = output.stderr_text();
115
+ let redacted = redact_output(output);
116
+ let safe_output = &redacted.output;
117
+ let stdout_text = safe_output.stdout_text();
118
+ let stderr_text = safe_output.stderr_text();
116
119
  let stdout_lines = count_lines(&stdout_text);
117
120
  let stderr_lines = count_lines(&stderr_text);
118
121
  let stdout_excerpt = truncate_for_prompt(&stdout_text, "stdout");
@@ -141,7 +144,7 @@ pub fn build_summary_prompt(command: &[String], output: &CommandOutput) -> Strin
141
144
  family_pattern = family.pattern,
142
145
  family_description = family.description,
143
146
  family_what_it_does = family.what_it_does,
144
- exit_code = output.exit_code(),
147
+ exit_code = safe_output.exit_code(),
145
148
  stdout_lines = stdout_lines,
146
149
  stdout_bytes = stdout_text.len(),
147
150
  stderr_lines = stderr_lines,
@@ -308,6 +311,25 @@ mod tests {
308
311
  assert!(prompt.contains("<<<STDERR"));
309
312
  }
310
313
 
314
+ #[test]
315
+ fn prompt_redacts_secret_like_stdout_and_stderr_before_embedding() {
316
+ let output = CommandOutput {
317
+ status: ok_status(),
318
+ stdout: b"API_TOKEN=super-secret\nsk-prod-secret\nvisible\n".to_vec(),
319
+ stderr: b"Authorization: Bearer bearer-secret\nghp_secret\n".to_vec(),
320
+ };
321
+
322
+ let prompt = build_summary_prompt(&["env".into()], &output);
323
+
324
+ assert!(prompt.contains("API_TOKEN=[REDACTED]"));
325
+ assert!(prompt.contains("Authorization: Bearer [REDACTED]"));
326
+ assert!(prompt.contains("visible"));
327
+ assert!(!prompt.contains("super-secret"));
328
+ assert!(!prompt.contains("sk-prod-secret"));
329
+ assert!(!prompt.contains("bearer-secret"));
330
+ assert!(!prompt.contains("ghp_secret"));
331
+ }
332
+
311
333
  #[test]
312
334
  fn prompt_truncates_large_streams_before_embedding_them() {
313
335
  let _guard = env_lock();
@@ -0,0 +1,241 @@
1
+ use crate::exec::CommandOutput;
2
+
3
+ #[derive(Debug, Clone)]
4
+ pub(crate) struct RedactedOutput {
5
+ pub(crate) output: CommandOutput,
6
+ pub(crate) count: usize,
7
+ }
8
+
9
+ pub(crate) fn redact_output(output: &CommandOutput) -> RedactedOutput {
10
+ let (stdout, stdout_count) = redact_bytes(&output.stdout);
11
+ let (stderr, stderr_count) = redact_bytes(&output.stderr);
12
+ RedactedOutput {
13
+ output: CommandOutput {
14
+ status: output.status,
15
+ stdout,
16
+ stderr,
17
+ },
18
+ count: stdout_count + stderr_count,
19
+ }
20
+ }
21
+
22
+ fn redact_bytes(bytes: &[u8]) -> (Vec<u8>, usize) {
23
+ let (text, count) = redact_text(&String::from_utf8_lossy(bytes));
24
+ (text.into_bytes(), count)
25
+ }
26
+
27
+ fn redact_text(text: &str) -> (String, usize) {
28
+ let mut count = 0;
29
+ let mut lines = Vec::new();
30
+ for line in text.lines() {
31
+ let mut redacted = line.to_string();
32
+ count += redact_authorization_headers(&mut redacted);
33
+ count += redact_key_value_secrets(&mut redacted);
34
+ count += redact_secret_markers(&mut redacted);
35
+ lines.push(redacted);
36
+ }
37
+ let mut rendered = lines.join("\n");
38
+ if text.ends_with('\n') {
39
+ rendered.push('\n');
40
+ }
41
+ (rendered, count)
42
+ }
43
+
44
+ fn redact_authorization_headers(text: &mut String) -> usize {
45
+ let mut count = 0;
46
+ let mut search_start = 0;
47
+ while search_start < text.len() {
48
+ let Some(relative_start) = text[search_start..]
49
+ .to_ascii_lowercase()
50
+ .find("authorization: bearer ")
51
+ else {
52
+ break;
53
+ };
54
+ let start = search_start + relative_start;
55
+ let value_start = start + "authorization: bearer ".len();
56
+ let end = find_secret_end(text, value_start);
57
+ if text.get(value_start..end) == Some("[REDACTED]") {
58
+ search_start = end;
59
+ continue;
60
+ }
61
+ text.replace_range(value_start..end, "[REDACTED]");
62
+ count += 1;
63
+ search_start = value_start + "[REDACTED]".len();
64
+ }
65
+ count
66
+ }
67
+
68
+ fn redact_key_value_secrets(text: &mut String) -> usize {
69
+ let mut count = 0;
70
+ let secret_keys = [
71
+ "access_token",
72
+ "api_key",
73
+ "apikey",
74
+ "auth_token",
75
+ "password",
76
+ "secret",
77
+ "token",
78
+ ];
79
+ let mut search_start = 0;
80
+ loop {
81
+ if search_start >= text.len() {
82
+ break;
83
+ }
84
+ let lower = text[search_start..].to_ascii_lowercase();
85
+ let Some((relative_key_start, key)) = secret_keys
86
+ .iter()
87
+ .filter_map(|key| lower.find(key).map(|index| (index, *key)))
88
+ .min_by_key(|(index, _)| *index)
89
+ else {
90
+ break;
91
+ };
92
+ let key_start = search_start + relative_key_start;
93
+ let key_end = key_start + key.len();
94
+ let after_key = &text[key_end..];
95
+ let Some(delimiter_offset) = after_key.char_indices().find_map(|(offset, ch)| match ch {
96
+ '=' | ':' => Some(offset),
97
+ '"' | '\'' | ' ' | '\t' => None,
98
+ _ => Some(usize::MAX),
99
+ }) else {
100
+ break;
101
+ };
102
+ if delimiter_offset == usize::MAX {
103
+ search_start = key_end;
104
+ continue;
105
+ }
106
+ let delimiter = key_end + delimiter_offset;
107
+ let mut value_start = delimiter + 1;
108
+ while text
109
+ .as_bytes()
110
+ .get(value_start)
111
+ .is_some_and(u8::is_ascii_whitespace)
112
+ {
113
+ value_start += 1;
114
+ }
115
+ let quote = text
116
+ .as_bytes()
117
+ .get(value_start)
118
+ .copied()
119
+ .filter(|byte| *byte == b'"' || *byte == b'\'');
120
+ if quote.is_some() {
121
+ value_start += 1;
122
+ }
123
+ let mut value_end = if let Some(quote) = quote {
124
+ text.as_bytes()
125
+ .get(value_start..)
126
+ .and_then(|tail| tail.iter().position(|byte| *byte == quote))
127
+ .map(|offset| value_start + offset)
128
+ .unwrap_or_else(|| find_secret_end(text, value_start))
129
+ } else {
130
+ find_secret_end(text, value_start)
131
+ };
132
+ if value_end <= value_start {
133
+ search_start = key_end;
134
+ continue;
135
+ }
136
+ if quote.is_none() {
137
+ value_end = value_end
138
+ .min(
139
+ text[value_start..]
140
+ .find(',')
141
+ .map(|offset| value_start + offset)
142
+ .unwrap_or(text.len()),
143
+ )
144
+ .min(
145
+ text[value_start..]
146
+ .find('}')
147
+ .map(|offset| value_start + offset)
148
+ .unwrap_or(text.len()),
149
+ );
150
+ }
151
+ text.replace_range(value_start..value_end, "[REDACTED]");
152
+ count += 1;
153
+ search_start = value_start + "[REDACTED]".len();
154
+ }
155
+ count
156
+ }
157
+
158
+ fn redact_secret_markers(text: &mut String) -> usize {
159
+ let mut count = 0;
160
+ for marker in ["sk-", "ghp_", "xoxb-"] {
161
+ while let Some(start) = text.find(marker) {
162
+ let end = find_secret_end(text, start);
163
+ text.replace_range(start..end, "[REDACTED]");
164
+ count += 1;
165
+ }
166
+ }
167
+ count
168
+ }
169
+
170
+ fn find_secret_end(text: &str, start: usize) -> usize {
171
+ text[start..]
172
+ .char_indices()
173
+ .find_map(|(offset, ch)| {
174
+ if ch.is_whitespace() || matches!(ch, ',' | ';' | ')' | ']' | '}') {
175
+ Some(start + offset)
176
+ } else {
177
+ None
178
+ }
179
+ })
180
+ .unwrap_or(text.len())
181
+ }
182
+
183
+ #[cfg(test)]
184
+ mod tests {
185
+ use super::redact_output;
186
+ use crate::exec::CommandOutput;
187
+ use std::process::Command;
188
+
189
+ fn ok_status() -> std::process::ExitStatus {
190
+ Command::new("sh")
191
+ .arg("-c")
192
+ .arg("exit 0")
193
+ .status()
194
+ .expect("status")
195
+ }
196
+
197
+ #[test]
198
+ fn redacts_secret_like_stdout_and_stderr() {
199
+ let output = CommandOutput {
200
+ status: ok_status(),
201
+ stdout: b"API_TOKEN=secret-value\nplain\nsk-live123\n".to_vec(),
202
+ stderr: b"Authorization: Bearer bearer-secret\nghp_secret123\n".to_vec(),
203
+ };
204
+
205
+ let redacted = redact_output(&output);
206
+ let stdout = String::from_utf8(redacted.output.stdout).expect("stdout utf8");
207
+ let stderr = String::from_utf8(redacted.output.stderr).expect("stderr utf8");
208
+
209
+ assert_eq!(redacted.count, 4);
210
+ assert!(stdout.contains("API_TOKEN=[REDACTED]"));
211
+ assert!(stdout.contains("[REDACTED]"));
212
+ assert!(stderr.contains("Authorization: Bearer [REDACTED]"));
213
+ assert!(!stdout.contains("secret-value"));
214
+ assert!(!stdout.contains("sk-live123"));
215
+ assert!(!stderr.contains("bearer-secret"));
216
+ assert!(!stderr.contains("ghp_secret123"));
217
+ }
218
+
219
+ #[test]
220
+ fn redacts_json_colon_and_repeated_secret_forms() {
221
+ let output = CommandOutput {
222
+ status: ok_status(),
223
+ stdout:
224
+ br#"{"access_token":"tok-1","api_key":"sk-json"} password: hunter2 sk-one sk-two"#
225
+ .to_vec(),
226
+ stderr: b"Authorization: Bearer first Authorization: Bearer second\n".to_vec(),
227
+ };
228
+
229
+ let redacted = redact_output(&output);
230
+ let stdout = String::from_utf8(redacted.output.stdout).expect("stdout utf8");
231
+ let stderr = String::from_utf8(redacted.output.stderr).expect("stderr utf8");
232
+
233
+ for secret in [
234
+ "tok-1", "sk-json", "hunter2", "sk-one", "sk-two", "first", "second",
235
+ ] {
236
+ assert!(!stdout.contains(secret), "stdout leaked {secret}: {stdout}");
237
+ assert!(!stderr.contains(secret), "stderr leaked {secret}: {stderr}");
238
+ }
239
+ assert!(redacted.count >= 7);
240
+ }
241
+ }