oh-my-codex 0.14.0 → 0.14.2
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/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/README.md +14 -8
- package/crates/omx-explore/src/main.rs +94 -1
- package/crates/omx-sparkshell/src/codex_bridge.rs +59 -12
- package/crates/omx-sparkshell/tests/execution.rs +48 -0
- package/dist/cli/__tests__/explore.test.js +33 -1
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +11 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +5 -0
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +139 -25
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +30 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +32 -7
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +8 -6
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +23 -0
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +65 -5
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +360 -26
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +18 -3
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +7 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +25 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +11 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +159 -394
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +3 -1
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts +37 -9
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +204 -26
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +51 -14
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +35 -10
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +1 -0
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +61 -7
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +56 -0
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js +31 -0
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js +43 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js +38 -0
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +108 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +16 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +34 -8
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +7 -25
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +60 -0
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +177 -0
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +36 -18
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/state-server.d.ts +17 -0
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +55 -1
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/notifications/__tests__/index.test.js +0 -3
- package/dist/notifications/__tests__/index.test.js.map +1 -1
- package/dist/notifications/__tests__/session-status.test.js +90 -0
- package/dist/notifications/__tests__/session-status.test.js.map +1 -1
- package/dist/notifications/session-status.d.ts +2 -0
- package/dist/notifications/session-status.d.ts.map +1 -1
- package/dist/notifications/session-status.js +19 -4
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/question/__tests__/deep-interview.test.js +44 -0
- package/dist/question/__tests__/deep-interview.test.js.map +1 -1
- package/dist/question/__tests__/renderer.test.js +192 -12
- package/dist/question/__tests__/renderer.test.js.map +1 -1
- package/dist/question/__tests__/state.test.js +21 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/deep-interview.d.ts +3 -0
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js +18 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/renderer.d.ts +4 -2
- package/dist/question/renderer.d.ts.map +1 -1
- package/dist/question/renderer.js +87 -18
- package/dist/question/renderer.js.map +1 -1
- package/dist/runtime/__tests__/run-outcome.test.js +38 -0
- package/dist/runtime/__tests__/run-outcome.test.js.map +1 -1
- package/dist/runtime/__tests__/run-state.test.d.ts +2 -0
- package/dist/runtime/__tests__/run-state.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/run-state.test.js +37 -0
- package/dist/runtime/__tests__/run-state.test.js.map +1 -0
- package/dist/runtime/run-loop.d.ts +5 -1
- package/dist/runtime/run-loop.d.ts.map +1 -1
- package/dist/runtime/run-loop.js +8 -3
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +18 -0
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js +156 -7
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +13 -3
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/runtime/terminal-lifecycle.d.ts +11 -0
- package/dist/runtime/terminal-lifecycle.d.ts.map +1 -0
- package/dist/runtime/terminal-lifecycle.js +52 -0
- package/dist/runtime/terminal-lifecycle.js.map +1 -0
- package/dist/scripts/__tests__/codex-native-hook.test.js +370 -56
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.d.ts +2 -0
- package/dist/scripts/__tests__/postinstall.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/postinstall.test.js +178 -0
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +115 -56
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/postinstall.d.ts +22 -0
- package/dist/scripts/postinstall.d.ts.map +1 -0
- package/dist/scripts/postinstall.js +105 -0
- package/dist/scripts/postinstall.js.map +1 -0
- package/dist/state/__tests__/operations.test.js +60 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +18 -1
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js +6 -0
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +108 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +18 -4
- package/dist/team/runtime.js.map +1 -1
- package/dist/utils/__tests__/dep-versions.test.js +25 -8
- package/dist/utils/__tests__/dep-versions.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +45 -0
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +22 -7
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +3 -2
- package/prompts/architect.md +4 -0
- package/prompts/code-reviewer.md +3 -0
- package/skills/code-review/SKILL.md +94 -28
- package/skills/deep-interview/SKILL.md +91 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +468 -64
- package/src/scripts/__tests__/postinstall.test.ts +210 -0
- package/src/scripts/codex-native-hook.ts +136 -53
- package/src/scripts/postinstall-bootstrap.js +23 -0
- package/src/scripts/postinstall.ts +161 -0
- package/templates/AGENTS.md +1 -1
- package/templates/model-instructions/explore-lightweight-AGENTS.md +11 -0
- package/templates/model-instructions/sparkshell-lightweight-AGENTS.md +10 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { writeUserInstallStamp } from "../../cli/update.js";
|
|
7
|
+
import {
|
|
8
|
+
isGlobalInstallLifecycle,
|
|
9
|
+
runPostinstall,
|
|
10
|
+
} from "../postinstall.js";
|
|
11
|
+
|
|
12
|
+
describe("isGlobalInstallLifecycle", () => {
|
|
13
|
+
it("accepts npm_config_global=true", () => {
|
|
14
|
+
assert.equal(isGlobalInstallLifecycle({ npm_config_global: "true" }), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("accepts npm_config_location=global", () => {
|
|
18
|
+
assert.equal(isGlobalInstallLifecycle({ npm_config_location: "global" }), true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects local installs", () => {
|
|
22
|
+
assert.equal(isGlobalInstallLifecycle({ npm_config_global: "false" }), false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("runPostinstall", () => {
|
|
27
|
+
it("runs interactive setup only for bumped global installs", async () => {
|
|
28
|
+
const root = await mkdtemp(join(tmpdir(), "omx-postinstall-"));
|
|
29
|
+
const stampPath = join(root, ".codex", ".omx", "install-state.json");
|
|
30
|
+
const logs: string[] = [];
|
|
31
|
+
let setupCalls = 0;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await runPostinstall({
|
|
35
|
+
env: { npm_config_global: "true" },
|
|
36
|
+
getCurrentVersion: async () => "0.14.1",
|
|
37
|
+
isInteractive: () => true,
|
|
38
|
+
log: (message) => logs.push(message),
|
|
39
|
+
readStamp: async () => ({
|
|
40
|
+
installed_version: "0.14.0",
|
|
41
|
+
setup_completed_version: "0.14.0",
|
|
42
|
+
updated_at: "2026-04-20T00:00:00.000Z",
|
|
43
|
+
}),
|
|
44
|
+
runSetup: async () => {
|
|
45
|
+
setupCalls += 1;
|
|
46
|
+
},
|
|
47
|
+
writeStamp: async (stamp) => writeUserInstallStamp(stamp, stampPath),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(result.status, "setup-ran");
|
|
51
|
+
assert.equal(setupCalls, 1);
|
|
52
|
+
assert.match(logs.join("\n"), /Launching interactive setup/);
|
|
53
|
+
|
|
54
|
+
const stamp = JSON.parse(await readFile(stampPath, "utf-8")) as {
|
|
55
|
+
installed_version: string;
|
|
56
|
+
setup_completed_version: string;
|
|
57
|
+
};
|
|
58
|
+
assert.equal(stamp.installed_version, "0.14.1");
|
|
59
|
+
assert.equal(stamp.setup_completed_version, "0.14.1");
|
|
60
|
+
} finally {
|
|
61
|
+
await rm(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("records the installed version and prints a hint when no TTY is available", async () => {
|
|
66
|
+
const root = await mkdtemp(join(tmpdir(), "omx-postinstall-"));
|
|
67
|
+
const stampPath = join(root, ".codex", ".omx", "install-state.json");
|
|
68
|
+
const logs: string[] = [];
|
|
69
|
+
let setupCalls = 0;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await runPostinstall({
|
|
73
|
+
env: { npm_config_global: "true" },
|
|
74
|
+
getCurrentVersion: async () => "0.14.1",
|
|
75
|
+
isInteractive: () => false,
|
|
76
|
+
log: (message) => logs.push(message),
|
|
77
|
+
readStamp: async () => ({
|
|
78
|
+
installed_version: "0.14.0",
|
|
79
|
+
setup_completed_version: "0.14.0",
|
|
80
|
+
updated_at: "2026-04-20T00:00:00.000Z",
|
|
81
|
+
}),
|
|
82
|
+
runSetup: async () => {
|
|
83
|
+
setupCalls += 1;
|
|
84
|
+
},
|
|
85
|
+
writeStamp: async (stamp) => writeUserInstallStamp(stamp, stampPath),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(result.status, "hinted");
|
|
89
|
+
assert.equal(setupCalls, 0);
|
|
90
|
+
assert.match(logs.join("\n"), /Run `omx setup` \(interactive\) or `omx update`/);
|
|
91
|
+
|
|
92
|
+
const stamp = JSON.parse(await readFile(stampPath, "utf-8")) as {
|
|
93
|
+
installed_version: string;
|
|
94
|
+
setup_completed_version: string;
|
|
95
|
+
};
|
|
96
|
+
assert.equal(stamp.installed_version, "0.14.1");
|
|
97
|
+
assert.equal(stamp.setup_completed_version, "0.14.0");
|
|
98
|
+
} finally {
|
|
99
|
+
await rm(root, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("skips local installs", async () => {
|
|
104
|
+
let setupCalls = 0;
|
|
105
|
+
const result = await runPostinstall({
|
|
106
|
+
env: { npm_config_global: "false" },
|
|
107
|
+
getCurrentVersion: async () => "0.14.1",
|
|
108
|
+
isInteractive: () => true,
|
|
109
|
+
runSetup: async () => {
|
|
110
|
+
setupCalls += 1;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.equal(result.status, "noop-local");
|
|
115
|
+
assert.equal(setupCalls, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not rerun setup when the installed version matches the saved stamp", async () => {
|
|
119
|
+
let setupCalls = 0;
|
|
120
|
+
const result = await runPostinstall({
|
|
121
|
+
env: { npm_config_global: "true" },
|
|
122
|
+
getCurrentVersion: async () => "0.14.1",
|
|
123
|
+
isInteractive: () => true,
|
|
124
|
+
readStamp: async () => ({
|
|
125
|
+
installed_version: "0.14.1",
|
|
126
|
+
setup_completed_version: "0.14.1",
|
|
127
|
+
updated_at: "2026-04-20T00:00:00.000Z",
|
|
128
|
+
}),
|
|
129
|
+
runSetup: async () => {
|
|
130
|
+
setupCalls += 1;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.equal(result.status, "noop-same-version");
|
|
135
|
+
assert.equal(setupCalls, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("warns and exits cleanly when setup fails", async () => {
|
|
139
|
+
const warnings: string[] = [];
|
|
140
|
+
const result = await runPostinstall({
|
|
141
|
+
env: { npm_config_global: "true" },
|
|
142
|
+
getCurrentVersion: async () => "0.14.1",
|
|
143
|
+
isInteractive: () => true,
|
|
144
|
+
readStamp: async () => ({
|
|
145
|
+
installed_version: "0.14.0",
|
|
146
|
+
setup_completed_version: "0.14.0",
|
|
147
|
+
updated_at: "2026-04-20T00:00:00.000Z",
|
|
148
|
+
}),
|
|
149
|
+
runSetup: async () => {
|
|
150
|
+
throw new Error("boom");
|
|
151
|
+
},
|
|
152
|
+
warn: (message) => warnings.push(message),
|
|
153
|
+
writeStamp: async () => {},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.equal(result.status, "setup-failed");
|
|
157
|
+
assert.match(warnings.join("\n"), /non-fatal error: boom/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("runs interactive setup from the npm install prefix instead of the package dir or INIT_CWD", async () => {
|
|
161
|
+
const installRoot = await mkdtemp(join(tmpdir(), "omx-postinstall-install-root-"));
|
|
162
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "omx-postinstall-package-root-"));
|
|
163
|
+
const initCwd = await mkdtemp(join(tmpdir(), "omx-postinstall-init-cwd-"));
|
|
164
|
+
const originalCwd = process.cwd();
|
|
165
|
+
const scopeFile = join(installRoot, ".omx", "setup-scope.json");
|
|
166
|
+
const packageScopeFile = join(packageRoot, ".omx", "setup-scope.json");
|
|
167
|
+
const initCwdScopeFile = join(initCwd, ".omx", "setup-scope.json");
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
process.chdir(packageRoot);
|
|
171
|
+
|
|
172
|
+
const result = await runPostinstall({
|
|
173
|
+
env: {
|
|
174
|
+
npm_config_global: "true",
|
|
175
|
+
npm_config_prefix: installRoot,
|
|
176
|
+
INIT_CWD: initCwd,
|
|
177
|
+
},
|
|
178
|
+
getCurrentVersion: async () => "0.14.1",
|
|
179
|
+
isInteractive: () => true,
|
|
180
|
+
readStamp: async () => ({
|
|
181
|
+
installed_version: "0.14.0",
|
|
182
|
+
setup_completed_version: "0.14.0",
|
|
183
|
+
updated_at: "2026-04-20T00:00:00.000Z",
|
|
184
|
+
}),
|
|
185
|
+
runSetup: async () => {
|
|
186
|
+
await mkdir(join(process.cwd(), ".omx"), { recursive: true });
|
|
187
|
+
await writeFile(
|
|
188
|
+
join(process.cwd(), ".omx", "setup-scope.json"),
|
|
189
|
+
JSON.stringify({ scope: "project" }),
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
writeStamp: async () => {},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
assert.equal(result.status, "setup-ran");
|
|
196
|
+
assert.equal(process.cwd(), packageRoot);
|
|
197
|
+
assert.equal(
|
|
198
|
+
JSON.parse(await readFile(scopeFile, "utf-8")).scope,
|
|
199
|
+
"project",
|
|
200
|
+
);
|
|
201
|
+
await assert.rejects(() => readFile(packageScopeFile, "utf-8"));
|
|
202
|
+
await assert.rejects(() => readFile(initCwdScopeFile, "utf-8"));
|
|
203
|
+
} finally {
|
|
204
|
+
process.chdir(originalCwd);
|
|
205
|
+
await rm(installRoot, { recursive: true, force: true });
|
|
206
|
+
await rm(packageRoot, { recursive: true, force: true });
|
|
207
|
+
await rm(initCwd, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from "child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
4
4
|
import { join, resolve } from "path";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
5
6
|
import { readModeState, readModeStateForSession, updateModeState } from "../modes/base.js";
|
|
6
7
|
import {
|
|
7
8
|
listActiveSkills,
|
|
@@ -9,7 +10,12 @@ import {
|
|
|
9
10
|
} from "../state/skill-active.js";
|
|
10
11
|
import { readSubagentSessionSummary } from "../subagents/tracker.js";
|
|
11
12
|
import { resolveCanonicalTeamStateRoot } from "../team/state-root.js";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
isSessionStateUsable,
|
|
15
|
+
readSessionState,
|
|
16
|
+
readUsableSessionState,
|
|
17
|
+
reconcileNativeSessionStart,
|
|
18
|
+
} from "../hooks/session.js";
|
|
13
19
|
import {
|
|
14
20
|
appendTeamEvent,
|
|
15
21
|
readTeamLeaderAttention,
|
|
@@ -43,6 +49,7 @@ import {
|
|
|
43
49
|
import type { HookEventEnvelope } from "../hooks/extensibility/types.js";
|
|
44
50
|
import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
|
|
45
51
|
import { reconcileHudForPromptSubmit } from "../hud/reconcile.js";
|
|
52
|
+
import { shellEscapeSingle } from "../hud/tmux.js";
|
|
46
53
|
import { onSessionStart as buildWikiSessionStartContext } from "../wiki/lifecycle.js";
|
|
47
54
|
import { readAutoresearchCompletionStatus, readAutoresearchModeState } from "../autoresearch/skill-validation.js";
|
|
48
55
|
import { shouldContinueRun } from "../runtime/run-loop.js";
|
|
@@ -56,6 +63,7 @@ import {
|
|
|
56
63
|
type TriageStateFile,
|
|
57
64
|
} from "../hooks/triage-state.js";
|
|
58
65
|
import { isPendingDeepInterviewQuestionEnforcement } from "../question/deep-interview.js";
|
|
66
|
+
import { resolveOmxCliEntryPath } from "../utils/paths.js";
|
|
59
67
|
|
|
60
68
|
type CodexHookEventName =
|
|
61
69
|
| "SessionStart"
|
|
@@ -203,15 +211,26 @@ function readPromptText(payload: CodexHookPayload): string {
|
|
|
203
211
|
function sanitizePayloadForHookContext(
|
|
204
212
|
payload: CodexHookPayload,
|
|
205
213
|
hookEventName: CodexHookEventName,
|
|
214
|
+
canonicalSessionId = "",
|
|
206
215
|
): CodexHookPayload {
|
|
207
|
-
if (hookEventName !== "UserPromptSubmit") return payload;
|
|
208
|
-
|
|
209
216
|
const sanitized = { ...payload };
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
|
|
218
|
+
if (hookEventName === "UserPromptSubmit") {
|
|
219
|
+
delete sanitized.prompt;
|
|
220
|
+
delete sanitized.input;
|
|
221
|
+
delete sanitized.user_prompt;
|
|
222
|
+
delete sanitized.userPrompt;
|
|
223
|
+
delete sanitized.text;
|
|
224
|
+
return sanitized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (hookEventName === "Stop") {
|
|
228
|
+
delete sanitized.stop_hook_active;
|
|
229
|
+
delete sanitized.stopHookActive;
|
|
230
|
+
delete sanitized.sessionId;
|
|
231
|
+
sanitized.session_id = canonicalSessionId.trim() || safeString(payload.session_id ?? payload.sessionId).trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
215
234
|
return sanitized;
|
|
216
235
|
}
|
|
217
236
|
|
|
@@ -219,13 +238,14 @@ function buildBaseContext(
|
|
|
219
238
|
cwd: string,
|
|
220
239
|
payload: CodexHookPayload,
|
|
221
240
|
hookEventName: CodexHookEventName,
|
|
241
|
+
canonicalSessionId = "",
|
|
222
242
|
): Record<string, unknown> {
|
|
223
243
|
return {
|
|
224
244
|
cwd,
|
|
225
245
|
project_path: cwd,
|
|
226
246
|
transcript_path: safeString(payload.transcript_path ?? payload.transcriptPath) || null,
|
|
227
247
|
source: safeString(payload.source),
|
|
228
|
-
payload: sanitizePayloadForHookContext(payload, hookEventName),
|
|
248
|
+
payload: sanitizePayloadForHookContext(payload, hookEventName, canonicalSessionId),
|
|
229
249
|
};
|
|
230
250
|
}
|
|
231
251
|
|
|
@@ -264,17 +284,28 @@ async function readActiveRalphState(
|
|
|
264
284
|
stateDir: string,
|
|
265
285
|
preferredSessionId?: string,
|
|
266
286
|
): Promise<Record<string, unknown> | null> {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
287
|
+
const cwd = resolve(stateDir, "..", "..");
|
|
288
|
+
const [rawSessionInfo, usableSessionInfo] = await Promise.all([
|
|
289
|
+
readSessionState(cwd),
|
|
290
|
+
readUsableSessionState(cwd),
|
|
291
|
+
]);
|
|
292
|
+
const currentOmxSessionId = safeString(usableSessionInfo?.session_id).trim();
|
|
293
|
+
const staleCurrentSessionId = rawSessionInfo && !isSessionStateUsable(rawSessionInfo, cwd)
|
|
294
|
+
? safeString(rawSessionInfo.session_id).trim()
|
|
295
|
+
: "";
|
|
269
296
|
const sessionCandidates = [...new Set([
|
|
270
297
|
safeString(preferredSessionId).trim(),
|
|
271
298
|
currentOmxSessionId,
|
|
272
299
|
].filter(Boolean))];
|
|
273
300
|
|
|
301
|
+
// Ralph Stop stays authoritative-scope-only once the Stop payload is session-bound.
|
|
302
|
+
// That is intentionally stricter than generic state MCP reads: do not scan sibling
|
|
303
|
+
// session scopes or fall back to root when a current/explicit session is in play.
|
|
274
304
|
for (const sessionId of sessionCandidates) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
305
|
+
if (staleCurrentSessionId && sessionId === staleCurrentSessionId) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const sessionScoped = await readStopSessionPinnedState("ralph-state.json", cwd, sessionId);
|
|
278
309
|
if (sessionScoped?.active === true && shouldContinueRun(sessionScoped)) {
|
|
279
310
|
return sessionScoped;
|
|
280
311
|
}
|
|
@@ -287,17 +318,6 @@ async function readActiveRalphState(
|
|
|
287
318
|
return direct;
|
|
288
319
|
}
|
|
289
320
|
|
|
290
|
-
const sessionsRoot = join(stateDir, "sessions");
|
|
291
|
-
if (!existsSync(sessionsRoot)) return null;
|
|
292
|
-
const entries = await readdir(sessionsRoot, { withFileTypes: true }).catch(() => []);
|
|
293
|
-
for (const entry of entries) {
|
|
294
|
-
if (!entry.isDirectory()) continue;
|
|
295
|
-
const candidate = await readJsonIfExists(join(sessionsRoot, entry.name, "ralph-state.json"));
|
|
296
|
-
if (candidate?.active === true && shouldContinueRun(candidate)) {
|
|
297
|
-
return candidate;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
321
|
return null;
|
|
302
322
|
}
|
|
303
323
|
|
|
@@ -399,32 +419,55 @@ function resolveSessionOwnerPid(payload: CodexHookPayload): number {
|
|
|
399
419
|
return process.pid;
|
|
400
420
|
}
|
|
401
421
|
|
|
402
|
-
|
|
403
|
-
let repoRoot = "";
|
|
422
|
+
function tryReadGitValue(cwd: string, args: string[]): string | null {
|
|
404
423
|
try {
|
|
405
|
-
|
|
424
|
+
const value = execFileSync("git", args, {
|
|
406
425
|
cwd,
|
|
407
426
|
encoding: "utf-8",
|
|
408
427
|
stdio: ["ignore", "pipe", "ignore"],
|
|
409
428
|
windowsHide: true,
|
|
410
429
|
}).trim();
|
|
430
|
+
return value || null;
|
|
411
431
|
} catch {
|
|
412
|
-
return
|
|
432
|
+
return null;
|
|
413
433
|
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isPathIgnoredByGit(cwd: string, path: string): boolean {
|
|
437
|
+
try {
|
|
438
|
+
execFileSync("git", ["check-ignore", "-q", path], {
|
|
439
|
+
cwd,
|
|
440
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
441
|
+
windowsHide: true,
|
|
442
|
+
});
|
|
443
|
+
return true;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function ensureOmxLocalIgnoreEntry(cwd: string): Promise<{ changed: boolean; excludePath?: string }> {
|
|
450
|
+
const repoRoot = tryReadGitValue(cwd, ["rev-parse", "--show-toplevel"]);
|
|
414
451
|
if (!repoRoot) return { changed: false };
|
|
452
|
+
if (isPathIgnoredByGit(repoRoot, ".omx/")) {
|
|
453
|
+
return { changed: false };
|
|
454
|
+
}
|
|
415
455
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
456
|
+
const excludePathValue = tryReadGitValue(repoRoot, ["rev-parse", "--git-path", "info/exclude"]);
|
|
457
|
+
if (!excludePathValue) return { changed: false };
|
|
458
|
+
const excludePath = resolve(repoRoot, excludePathValue);
|
|
459
|
+
|
|
460
|
+
const existing = existsSync(excludePath)
|
|
461
|
+
? await readFile(excludePath, "utf-8")
|
|
419
462
|
: "";
|
|
420
463
|
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
421
464
|
if (lines.includes(".omx/")) {
|
|
422
|
-
return { changed: false,
|
|
465
|
+
return { changed: false, excludePath };
|
|
423
466
|
}
|
|
424
467
|
|
|
425
468
|
const next = `${existing}${existing.endsWith("\n") || existing.length === 0 ? "" : "\n"}.omx/\n`;
|
|
426
|
-
await writeFile(
|
|
427
|
-
return { changed: true,
|
|
469
|
+
await writeFile(excludePath, next);
|
|
470
|
+
return { changed: true, excludePath };
|
|
428
471
|
}
|
|
429
472
|
|
|
430
473
|
async function buildSessionStartContext(
|
|
@@ -433,9 +476,9 @@ async function buildSessionStartContext(
|
|
|
433
476
|
): Promise<string | null> {
|
|
434
477
|
const sections: string[] = [];
|
|
435
478
|
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
438
|
-
sections.push(`Added .omx/ to ${
|
|
479
|
+
const localIgnoreResult = await ensureOmxLocalIgnoreEntry(cwd);
|
|
480
|
+
if (localIgnoreResult.changed) {
|
|
481
|
+
sections.push(`Added .omx/ to ${localIgnoreResult.excludePath} to keep local OMX state out of source control without mutating tracked repo ignores.`);
|
|
439
482
|
}
|
|
440
483
|
|
|
441
484
|
const modeSummaries: string[] = [];
|
|
@@ -523,7 +566,17 @@ async function buildSessionStartContext(
|
|
|
523
566
|
return sections.length > 0 ? sections.join("\n\n") : null;
|
|
524
567
|
}
|
|
525
568
|
|
|
526
|
-
function
|
|
569
|
+
function buildDeepInterviewQuestionBridgeInstruction(cwd: string): string {
|
|
570
|
+
const omxBin = resolveOmxCliEntryPath({ cwd }) || process.argv[1] || "omx";
|
|
571
|
+
const bridgeCommand = `${shellEscapeSingle(process.execPath)} ${shellEscapeSingle(omxBin)} question`;
|
|
572
|
+
return `Deep-interview must ask each interview round via \`omx question\`; do not fall back to \`request_user_input\` or plain-text questioning. After starting \`omx question\` in a background terminal, wait for that terminal to finish and read the JSON answer before continuing the interview. If bare \`omx question\` is unavailable in this reused session, use the current-session CLI bridge command: \`${bridgeCommand}\`. Stop remains blocked while a deep-interview question obligation is pending.`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function buildAdditionalContextMessage(
|
|
576
|
+
prompt: string,
|
|
577
|
+
skillState?: SkillActiveState | null,
|
|
578
|
+
cwd: string = process.cwd(),
|
|
579
|
+
): string | null {
|
|
527
580
|
if (!prompt) return null;
|
|
528
581
|
const promptPriorityMessage = buildPromptPriorityMessage(prompt);
|
|
529
582
|
const matches = detectKeywords(prompt);
|
|
@@ -543,7 +596,7 @@ function buildAdditionalContextMessage(prompt: string, skillState?: SkillActiveS
|
|
|
543
596
|
? "Prompt-side `$ralph` activation seeds Ralph workflow state only; it does not invoke `omx ralph`. Use `omx ralph --prd ...` only when you explicitly want the PRD-gated CLI startup path."
|
|
544
597
|
: null;
|
|
545
598
|
const deepInterviewPromptActivationNote = skillState?.initialized_mode === "deep-interview"
|
|
546
|
-
?
|
|
599
|
+
? buildDeepInterviewQuestionBridgeInstruction(cwd)
|
|
547
600
|
: null;
|
|
548
601
|
const combinedTransitionMessage = (() => {
|
|
549
602
|
if (!skillState?.transition_message) return null;
|
|
@@ -997,7 +1050,11 @@ async function buildDeepInterviewQuestionStopOutput(
|
|
|
997
1050
|
threadId: string,
|
|
998
1051
|
): Promise<{ output: Record<string, unknown>; obligationId: string } | null> {
|
|
999
1052
|
const modeState = await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId);
|
|
1000
|
-
if (!modeState
|
|
1053
|
+
if (!modeState) return null;
|
|
1054
|
+
|
|
1055
|
+
const questionEnforcement = safeObject(modeState.question_enforcement);
|
|
1056
|
+
const hasPendingQuestionObligation = isPendingDeepInterviewQuestionEnforcement(questionEnforcement);
|
|
1057
|
+
if (modeState.active !== true && !hasPendingQuestionObligation) return null;
|
|
1001
1058
|
|
|
1002
1059
|
const phase = formatPhase(modeState.current_phase, "planning");
|
|
1003
1060
|
if (TERMINAL_MODE_PHASES.has(phase.toLowerCase()) || phase === "completing") {
|
|
@@ -1013,8 +1070,7 @@ async function buildDeepInterviewQuestionStopOutput(
|
|
|
1013
1070
|
if (!blocker) return null;
|
|
1014
1071
|
}
|
|
1015
1072
|
|
|
1016
|
-
|
|
1017
|
-
if (!isPendingDeepInterviewQuestionEnforcement(questionEnforcement)) {
|
|
1073
|
+
if (!hasPendingQuestionObligation) {
|
|
1018
1074
|
return null;
|
|
1019
1075
|
}
|
|
1020
1076
|
|
|
@@ -1148,6 +1204,7 @@ async function returnPersistentStopBlock(
|
|
|
1148
1204
|
signatureValue: string,
|
|
1149
1205
|
output: Record<string, unknown> | null,
|
|
1150
1206
|
canonicalSessionId?: string,
|
|
1207
|
+
options: { allowRepeatDuringStopHook?: boolean } = { allowRepeatDuringStopHook: true },
|
|
1151
1208
|
): Promise<Record<string, unknown> | null> {
|
|
1152
1209
|
return await maybeReturnRepeatableStopOutput(
|
|
1153
1210
|
payload,
|
|
@@ -1155,7 +1212,7 @@ async function returnPersistentStopBlock(
|
|
|
1155
1212
|
buildRepeatableStopSignature(payload, signatureKind, signatureValue, canonicalSessionId),
|
|
1156
1213
|
output,
|
|
1157
1214
|
canonicalSessionId,
|
|
1158
|
-
|
|
1215
|
+
options,
|
|
1159
1216
|
);
|
|
1160
1217
|
}
|
|
1161
1218
|
|
|
@@ -1438,6 +1495,7 @@ async function buildStopHookOutput(
|
|
|
1438
1495
|
safeString(ultraworkOutput.stopReason),
|
|
1439
1496
|
ultraworkOutput,
|
|
1440
1497
|
canonicalSessionId,
|
|
1498
|
+
{ allowRepeatDuringStopHook: false },
|
|
1441
1499
|
);
|
|
1442
1500
|
}
|
|
1443
1501
|
|
|
@@ -1587,19 +1645,37 @@ export async function dispatchCodexNativeHook(
|
|
|
1587
1645
|
const nativeSessionId = safeString(payload.session_id ?? payload.sessionId).trim();
|
|
1588
1646
|
const threadId = safeString(payload.thread_id ?? payload.threadId).trim();
|
|
1589
1647
|
const turnId = safeString(payload.turn_id ?? payload.turnId).trim();
|
|
1590
|
-
|
|
1648
|
+
const currentSessionState = await readUsableSessionState(cwd);
|
|
1649
|
+
let canonicalSessionId = safeString(currentSessionState?.session_id).trim();
|
|
1650
|
+
let resolvedNativeSessionId = nativeSessionId;
|
|
1591
1651
|
|
|
1592
1652
|
if (hookEventName === "SessionStart" && nativeSessionId) {
|
|
1593
1653
|
const sessionState = await reconcileNativeSessionStart(cwd, nativeSessionId, {
|
|
1594
1654
|
pid: options.sessionOwnerPid ?? resolveSessionOwnerPid(payload),
|
|
1595
1655
|
});
|
|
1596
1656
|
canonicalSessionId = safeString(sessionState.session_id).trim();
|
|
1657
|
+
resolvedNativeSessionId = safeString(sessionState.native_session_id).trim() || nativeSessionId;
|
|
1597
1658
|
} else if (!canonicalSessionId) {
|
|
1598
|
-
canonicalSessionId = safeString(
|
|
1659
|
+
canonicalSessionId = safeString(currentSessionState?.session_id).trim();
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (hookEventName === "Stop") {
|
|
1663
|
+
const stopCanonicalSessionId = await resolveInternalSessionIdForPayload(
|
|
1664
|
+
cwd,
|
|
1665
|
+
readPayloadSessionId(payload),
|
|
1666
|
+
);
|
|
1667
|
+
if (stopCanonicalSessionId) {
|
|
1668
|
+
canonicalSessionId = stopCanonicalSessionId;
|
|
1669
|
+
}
|
|
1670
|
+
if (canonicalSessionId && safeString(currentSessionState?.session_id).trim() === canonicalSessionId) {
|
|
1671
|
+
resolvedNativeSessionId =
|
|
1672
|
+
safeString(currentSessionState?.native_session_id).trim() || resolvedNativeSessionId;
|
|
1673
|
+
}
|
|
1599
1674
|
}
|
|
1600
1675
|
|
|
1601
1676
|
const eventSessionId = canonicalSessionId || nativeSessionId || undefined;
|
|
1602
1677
|
const sessionIdForState = canonicalSessionId || nativeSessionId;
|
|
1678
|
+
let outputJson: Record<string, unknown> | null = null;
|
|
1603
1679
|
|
|
1604
1680
|
if (hookEventName === "UserPromptSubmit") {
|
|
1605
1681
|
const prompt = readPromptText(payload);
|
|
@@ -1684,10 +1760,10 @@ export async function dispatchCodexNativeHook(
|
|
|
1684
1760
|
}
|
|
1685
1761
|
|
|
1686
1762
|
if (omxEventName) {
|
|
1687
|
-
const baseContext = buildBaseContext(cwd, payload, hookEventName
|
|
1688
|
-
if (
|
|
1689
|
-
baseContext.native_session_id =
|
|
1690
|
-
baseContext.codex_session_id =
|
|
1763
|
+
const baseContext = buildBaseContext(cwd, payload, hookEventName!, canonicalSessionId);
|
|
1764
|
+
if (resolvedNativeSessionId) {
|
|
1765
|
+
baseContext.native_session_id = resolvedNativeSessionId;
|
|
1766
|
+
baseContext.codex_session_id = resolvedNativeSessionId;
|
|
1691
1767
|
}
|
|
1692
1768
|
if (canonicalSessionId) {
|
|
1693
1769
|
baseContext.omx_session_id = canonicalSessionId;
|
|
@@ -1705,11 +1781,10 @@ export async function dispatchCodexNativeHook(
|
|
|
1705
1781
|
await dispatchHookEvent(event, { cwd });
|
|
1706
1782
|
}
|
|
1707
1783
|
|
|
1708
|
-
let outputJson: Record<string, unknown> | null = null;
|
|
1709
1784
|
if (hookEventName === "SessionStart" || hookEventName === "UserPromptSubmit") {
|
|
1710
1785
|
const additionalContext = hookEventName === "SessionStart"
|
|
1711
1786
|
? await buildSessionStartContext(cwd, canonicalSessionId || nativeSessionId)
|
|
1712
|
-
: (buildAdditionalContextMessage(readPromptText(payload), skillState) ?? triageAdditionalContext);
|
|
1787
|
+
: (buildAdditionalContextMessage(readPromptText(payload), skillState, cwd) ?? triageAdditionalContext);
|
|
1713
1788
|
if (additionalContext) {
|
|
1714
1789
|
outputJson = {
|
|
1715
1790
|
hookSpecificOutput: {
|
|
@@ -1742,6 +1817,14 @@ interface NativeHookCliReadResult {
|
|
|
1742
1817
|
parseError: Error | null;
|
|
1743
1818
|
}
|
|
1744
1819
|
|
|
1820
|
+
export function isCodexNativeHookMainModule(
|
|
1821
|
+
moduleUrl: string,
|
|
1822
|
+
argv1: string | undefined,
|
|
1823
|
+
): boolean {
|
|
1824
|
+
if (!argv1) return false;
|
|
1825
|
+
return moduleUrl === pathToFileURL(argv1).href;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1745
1828
|
async function readStdinJson(): Promise<NativeHookCliReadResult> {
|
|
1746
1829
|
const chunks: Buffer[] = [];
|
|
1747
1830
|
for await (const chunk of process.stdin) {
|
|
@@ -1786,7 +1869,7 @@ export async function runCodexNativeHookCli(): Promise<void> {
|
|
|
1786
1869
|
}
|
|
1787
1870
|
}
|
|
1788
1871
|
|
|
1789
|
-
if (import.meta.url
|
|
1872
|
+
if (isCodexNativeHookMainModule(import.meta.url, process.argv[1])) {
|
|
1790
1873
|
runCodexNativeHookCli().catch((error) => {
|
|
1791
1874
|
process.stderr.write(
|
|
1792
1875
|
`[omx] codex-native-hook failed: ${
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const distScriptPath = join(__dirname, "..", "..", "dist", "scripts", "postinstall.js");
|
|
8
|
+
|
|
9
|
+
if (!existsSync(distScriptPath)) {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const moduleUrl = pathToFileURL(distScriptPath).href;
|
|
14
|
+
try {
|
|
15
|
+
const postinstallModule = await import(moduleUrl);
|
|
16
|
+
if (typeof postinstallModule.main === "function") {
|
|
17
|
+
await postinstallModule.main();
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.warn(
|
|
21
|
+
`[omx] Postinstall bootstrap skipped after a non-fatal error: ${error instanceof Error ? error.message : String(error)}`,
|
|
22
|
+
);
|
|
23
|
+
}
|