oh-my-opencode 4.12.0 → 4.12.1
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/dist/cli/index.js +36 -17
- package/dist/cli-node/index.js +36 -17
- package/dist/index.js +361 -254
- package/dist/skills/ultraresearch/SKILL.md +11 -2
- package/dist/tools/background-task/constants.d.ts +1 -1
- package/dist/tui.js +9 -3
- package/package.json +13 -13
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +327 -171
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +275 -31
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +11 -0
- package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +12 -0
- package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +34 -0
- package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +40 -3
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +6 -5
- package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +49 -87
- package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +48 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +1 -0
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +142 -0
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/dist/cli.js +94 -0
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +17 -0
- package/packages/omo-codex/plugin/components/teammode/package.json +26 -0
- package/packages/omo-codex/plugin/components/teammode/src/cli.ts +12 -0
- package/packages/omo-codex/plugin/components/teammode/src/codex-hook.ts +125 -0
- package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +116 -0
- package/packages/omo-codex/plugin/components/teammode/tsconfig.json +25 -0
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +17 -0
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +17 -0
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +30 -12
- package/packages/omo-codex/plugin/package.json +2 -1
- package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +1 -0
- package/packages/omo-codex/plugin/skills/ultraresearch/SKILL.md +11 -2
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +36 -0
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +2 -1
- package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +1 -1
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +24 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/ultraresearch-skill-contract.test.mjs +48 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +14 -1
- package/packages/shared-skills/skills/ultraresearch/SKILL.md +11 -2
|
@@ -103,13 +103,10 @@ async function resolveOrProvisionCommand(
|
|
|
103
103
|
nodeSupport: CodegraphNodeSupport,
|
|
104
104
|
): Promise<ResolutionResult> {
|
|
105
105
|
const resolved = deps.resolveCommand({ env, homeDir, provisioned: () => provisionedBinFromInstallDir(config.install_dir) });
|
|
106
|
-
if (resolved.exists) {
|
|
107
|
-
if (codegraphCommandRequiresSupportedLocalNode(resolved) && !nodeSupport.supported) {
|
|
108
|
-
return { kind: "unsupported-node" };
|
|
109
|
-
}
|
|
106
|
+
if (resolved.exists && canUseResolvedCommand(resolved, nodeSupport)) {
|
|
110
107
|
return { kind: "resolved", resolution: resolved };
|
|
111
108
|
}
|
|
112
|
-
if (
|
|
109
|
+
if (resolved.exists && config.auto_provision === false) return { kind: "unsupported-node" };
|
|
113
110
|
if (config.auto_provision === false) return { error: "codegraph binary unavailable and auto_provision is disabled", kind: "unavailable", source: resolved.source };
|
|
114
111
|
|
|
115
112
|
const installDir = config.install_dir ?? join(homeDir, ".omo", "codegraph");
|
|
@@ -120,6 +117,10 @@ async function resolveOrProvisionCommand(
|
|
|
120
117
|
return { kind: "resolved", resolution: { argsPrefix: [], command: provisioned.binPath, exists: true, source: "provisioned" } };
|
|
121
118
|
}
|
|
122
119
|
|
|
120
|
+
function canUseResolvedCommand(resolved: CodegraphCommandResolution, nodeSupport: CodegraphNodeSupport): boolean {
|
|
121
|
+
return !codegraphCommandRequiresSupportedLocalNode(resolved) || nodeSupport.supported;
|
|
122
|
+
}
|
|
123
|
+
|
|
123
124
|
function decideStartupAction(status: CodegraphCommandResult): { readonly kind: "init" } | { readonly kind: "skip"; readonly reason: string } | { readonly kind: "sync" } {
|
|
124
125
|
if (status.timedOut) return { kind: "skip", reason: "status timed out" };
|
|
125
126
|
const text = `${status.stdout}\n${status.stderr ?? ""}`.toLowerCase();
|
|
@@ -9,6 +9,7 @@ import { runCodegraphCli } from "../src/cli.ts";
|
|
|
9
9
|
import {
|
|
10
10
|
executeCodegraphSessionStartHook,
|
|
11
11
|
resolveCodegraphCommandInvocation,
|
|
12
|
+
runCodegraphPostToolUseHook,
|
|
12
13
|
runCodegraphSessionStartWorker,
|
|
13
14
|
type WorkerSpawnInvocation,
|
|
14
15
|
} from "../src/hook.ts";
|
|
@@ -48,6 +49,54 @@ describe("CodeGraph SessionStart hook", () => {
|
|
|
48
49
|
}
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
it("#given CodeGraph MCP reports an uninitialized project #when PostToolUse fires #then it emits OMO global-store init guidance", async () => {
|
|
53
|
+
// given
|
|
54
|
+
const output = runCodegraphPostToolUseHook(
|
|
55
|
+
{
|
|
56
|
+
cwd: "/Users/me/project",
|
|
57
|
+
tool_name: "codegraph.codegraph_status",
|
|
58
|
+
tool_response: {
|
|
59
|
+
error: [
|
|
60
|
+
"Tool execution failed: CodeGraph not initialized in /Users/me/project.",
|
|
61
|
+
"Run 'codegraph init' in that project first.",
|
|
62
|
+
].join(" "),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{ homeDir: "/Users/me" },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// when
|
|
69
|
+
const parsed = JSON.parse(output);
|
|
70
|
+
|
|
71
|
+
// then
|
|
72
|
+
expect(parsed).toEqual({
|
|
73
|
+
hookSpecificOutput: {
|
|
74
|
+
hookEventName: "PostToolUse",
|
|
75
|
+
additionalContext: expect.stringContaining('"/Users/me/.omo/codegraph/projects/project-'),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(parsed.hookSpecificOutput.additionalContext).toContain('run `codegraph init` from "/Users/me/project"');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("#given real CodeGraph status output has no MCP path phrase #when PostToolUse fires #then it emits OMO global-store init guidance", async () => {
|
|
82
|
+
// given
|
|
83
|
+
const output = runCodegraphPostToolUseHook(
|
|
84
|
+
{
|
|
85
|
+
cwd: "/Users/me/project",
|
|
86
|
+
tool_name: "mcp__codegraph__codegraph_status",
|
|
87
|
+
tool_response: ['Project: /Users/me/project', "Not initialized", 'Run "codegraph init" to initialize'].join("\n"),
|
|
88
|
+
},
|
|
89
|
+
{ homeDir: "/Users/me" },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// when
|
|
93
|
+
const parsed = JSON.parse(output);
|
|
94
|
+
|
|
95
|
+
// then
|
|
96
|
+
expect(parsed.hookSpecificOutput.additionalContext).toContain('CodeGraph is not initialized for "/Users/me/project"');
|
|
97
|
+
expect(parsed.hookSpecificOutput.additionalContext).toContain('"/Users/me/.omo/codegraph/projects/project-');
|
|
98
|
+
});
|
|
99
|
+
|
|
51
100
|
it("#given CodeGraph is disabled by Codex SOT config #when SessionStart fires #then it skips without spawning", async () => {
|
|
52
101
|
// given
|
|
53
102
|
const stdout: string[] = [];
|
|
@@ -198,93 +247,6 @@ describe("CodeGraph SessionStart hook", () => {
|
|
|
198
247
|
}
|
|
199
248
|
});
|
|
200
249
|
|
|
201
|
-
it("#given an unsupported local Node and a PATH CodeGraph command #when worker runs #then it skips without touching the workspace", async () => {
|
|
202
|
-
// given
|
|
203
|
-
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-"));
|
|
204
|
-
const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-home-"));
|
|
205
|
-
const outcomes: unknown[] = [];
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
// when
|
|
209
|
-
const result = await runCodegraphSessionStartWorker({
|
|
210
|
-
cwd: workspace,
|
|
211
|
-
env: { HOME: homeDir },
|
|
212
|
-
nodeVersion: "26.3.0",
|
|
213
|
-
logOutcome: (outcome) => outcomes.push(outcome),
|
|
214
|
-
deps: {
|
|
215
|
-
resolveCommand: () => {
|
|
216
|
-
return { argsPrefix: [], command: "/usr/local/bin/codegraph", exists: true, source: "path" };
|
|
217
|
-
},
|
|
218
|
-
ensureProvisioned: () => {
|
|
219
|
-
throw new Error("ensureProvisioned should not run on unsupported Node");
|
|
220
|
-
},
|
|
221
|
-
prepareWorkspace: () => {
|
|
222
|
-
throw new Error("prepareWorkspace should not run on unsupported Node");
|
|
223
|
-
},
|
|
224
|
-
runCommand: () => {
|
|
225
|
-
throw new Error("runCommand should not run on unsupported Node");
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// then
|
|
231
|
-
expect(result).toEqual({ action: "skipped-unsupported-node" });
|
|
232
|
-
expect(existsSync(join(workspace, ".codegraph"))).toBe(false);
|
|
233
|
-
expect(outcomes).toEqual([{ action: "skipped-unsupported-node", projectRoot: workspace }]);
|
|
234
|
-
} finally {
|
|
235
|
-
rmSync(workspace, { recursive: true, force: true });
|
|
236
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("#given an unsupported local Node but bundled CodeGraph resolves through CODEGRAPH_NODE_BIN #when worker runs #then it bootstraps with the compatible runtime", async () => {
|
|
241
|
-
// given
|
|
242
|
-
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-compatible-node-"));
|
|
243
|
-
const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-compatible-node-home-"));
|
|
244
|
-
const nodeBin = "/opt/node22/bin/node";
|
|
245
|
-
const calls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
|
|
246
|
-
const outcomes: unknown[] = [];
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
// when
|
|
250
|
-
const result = await runCodegraphSessionStartWorker({
|
|
251
|
-
cwd: workspace,
|
|
252
|
-
env: { CODEGRAPH_NODE_BIN: nodeBin, HOME: homeDir },
|
|
253
|
-
nodeVersion: "26.3.0",
|
|
254
|
-
logOutcome: (outcome) => outcomes.push(outcome),
|
|
255
|
-
deps: {
|
|
256
|
-
ensureGitignored: () => true,
|
|
257
|
-
ensureProvisioned: () => {
|
|
258
|
-
throw new Error("provisioning should not run when bundled CodeGraph resolved");
|
|
259
|
-
},
|
|
260
|
-
prepareWorkspace: () => ({
|
|
261
|
-
dataDir: join(homeDir, ".omo/codegraph/projects/test"),
|
|
262
|
-
dataRoot: join(homeDir, ".omo/codegraph"),
|
|
263
|
-
linked: true,
|
|
264
|
-
mode: "global-linked",
|
|
265
|
-
projectLink: join(workspace, ".codegraph"),
|
|
266
|
-
}),
|
|
267
|
-
resolveCommand: () => ({ argsPrefix: ["codegraph.js"], command: nodeBin, exists: true, source: "bundled" }),
|
|
268
|
-
runCommand: (_projectRoot, command, args) => {
|
|
269
|
-
calls.push({ args, command });
|
|
270
|
-
return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// then
|
|
276
|
-
expect(result).toEqual({ action: "initialized" });
|
|
277
|
-
expect(calls).toEqual([
|
|
278
|
-
{ args: ["codegraph.js", "status", "--json"], command: nodeBin },
|
|
279
|
-
{ args: ["codegraph.js", "init"], command: nodeBin },
|
|
280
|
-
]);
|
|
281
|
-
expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "bundled", timedOut: false }]);
|
|
282
|
-
} finally {
|
|
283
|
-
rmSync(workspace, { recursive: true, force: true });
|
|
284
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
250
|
it("#given CodeGraph cannot be resolved or provisioned #when worker runs #then it logs a graceful skip", async () => {
|
|
289
251
|
// given
|
|
290
252
|
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-"));
|
|
@@ -35,7 +35,7 @@ describe("CodeGraph provisioned launcher Node guard", () => {
|
|
|
35
35
|
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node25-"));
|
|
36
36
|
const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node25-home-"));
|
|
37
37
|
const installDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node25-install-"));
|
|
38
|
-
const binPath = join(installDir, "bin", "codegraph");
|
|
38
|
+
const binPath = join(installDir, "bin", process.platform === "win32" ? "codegraph.cmd" : "codegraph");
|
|
39
39
|
const calls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
|
|
40
40
|
const outcomes: unknown[] = [];
|
|
41
41
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { runCodegraphServe } from "../src/serve.ts";
|
|
5
|
+
|
|
6
|
+
describe("runCodegraphServe provisioning", () => {
|
|
7
|
+
it("#given CodeGraph is unresolved #when serving MCP #then provisions CodeGraph before spawning", async () => {
|
|
8
|
+
// given
|
|
9
|
+
const binPath = join("/tmp/home/.omo/codegraph", "bin", "codegraph");
|
|
10
|
+
const calls: Array<{
|
|
11
|
+
readonly args: readonly string[];
|
|
12
|
+
readonly command: string;
|
|
13
|
+
readonly env: Record<string, string | undefined>;
|
|
14
|
+
}> = [];
|
|
15
|
+
const stderr: string[] = [];
|
|
16
|
+
|
|
17
|
+
// when
|
|
18
|
+
const exitCode = await runCodegraphServe({
|
|
19
|
+
config: { codegraph: { enabled: true }, sources: [], warnings: [] },
|
|
20
|
+
env: { PATH: "/bin" },
|
|
21
|
+
homeDir: "/tmp/home",
|
|
22
|
+
nodeVersion: "22.14.0",
|
|
23
|
+
buildEnv: () => ({}),
|
|
24
|
+
resolve: () => ({ argsPrefix: [], command: "codegraph", exists: false, source: "path" }),
|
|
25
|
+
ensureProvisioned: (options) =>
|
|
26
|
+
Promise.resolve({
|
|
27
|
+
binPath: join(options.installDir ?? "/tmp/home/.omo/codegraph", "bin", "codegraph"),
|
|
28
|
+
provisioned: true,
|
|
29
|
+
}),
|
|
30
|
+
runProcess: (command, args, options) => {
|
|
31
|
+
calls.push({ args, command, env: options.env });
|
|
32
|
+
return Promise.resolve(0);
|
|
33
|
+
},
|
|
34
|
+
stderr: { write: (chunk: string) => stderr.push(chunk) },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// then
|
|
38
|
+
expect(exitCode).toBe(0);
|
|
39
|
+
expect(stderr).toEqual([]);
|
|
40
|
+
expect(calls).toEqual([
|
|
41
|
+
{
|
|
42
|
+
args: ["serve", "--mcp"],
|
|
43
|
+
command: binPath,
|
|
44
|
+
env: { PATH: "/bin" },
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -17,6 +17,7 @@ describe("runCodegraphServe", () => {
|
|
|
17
17
|
|
|
18
18
|
// when
|
|
19
19
|
const exitCode = await runCodegraphServe({
|
|
20
|
+
config: { codegraph: { auto_provision: false, enabled: true }, sources: [], warnings: [] },
|
|
20
21
|
env: { PATH: "/bin" },
|
|
21
22
|
buildEnv: () => ({}),
|
|
22
23
|
resolve: () => ({ argsPrefix: [], command: "codegraph", exists: false, source: "path" }),
|
package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { runCodegraphSessionStartWorker } from "../src/hook.ts";
|
|
7
|
+
|
|
8
|
+
describe("CodeGraph SessionStart worker Node support", () => {
|
|
9
|
+
it("#given an unsupported local Node and a PATH CodeGraph command with auto provisioning disabled #when worker runs #then it skips without touching the workspace", async () => {
|
|
10
|
+
// given
|
|
11
|
+
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-"));
|
|
12
|
+
const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-home-"));
|
|
13
|
+
const outcomes: unknown[] = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// when
|
|
17
|
+
const result = await runCodegraphSessionStartWorker({
|
|
18
|
+
config: { codegraph: { auto_provision: false, enabled: true }, sources: [], warnings: [] },
|
|
19
|
+
cwd: workspace,
|
|
20
|
+
env: { HOME: homeDir },
|
|
21
|
+
nodeVersion: "26.3.0",
|
|
22
|
+
logOutcome: (outcome) => outcomes.push(outcome),
|
|
23
|
+
deps: {
|
|
24
|
+
resolveCommand: () => {
|
|
25
|
+
return { argsPrefix: [], command: "/usr/local/bin/codegraph", exists: true, source: "path" };
|
|
26
|
+
},
|
|
27
|
+
ensureProvisioned: () => {
|
|
28
|
+
throw new Error("ensureProvisioned should not run on unsupported Node");
|
|
29
|
+
},
|
|
30
|
+
prepareWorkspace: () => {
|
|
31
|
+
throw new Error("prepareWorkspace should not run on unsupported Node");
|
|
32
|
+
},
|
|
33
|
+
runCommand: () => {
|
|
34
|
+
throw new Error("runCommand should not run on unsupported Node");
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// then
|
|
40
|
+
expect(result).toEqual({ action: "skipped-unsupported-node" });
|
|
41
|
+
expect(existsSync(join(workspace, ".codegraph"))).toBe(false);
|
|
42
|
+
expect(outcomes).toEqual([{ action: "skipped-unsupported-node", projectRoot: workspace }]);
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
45
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("#given an unsupported local Node and a PATH CodeGraph command #when auto provisioning succeeds #then it bootstraps with the provisioned binary", async () => {
|
|
50
|
+
// given
|
|
51
|
+
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-provision-"));
|
|
52
|
+
const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-provision-home-"));
|
|
53
|
+
const binPath = join(homeDir, ".omo", "codegraph", "bin", "codegraph");
|
|
54
|
+
const calls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
|
|
55
|
+
const outcomes: unknown[] = [];
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// when
|
|
59
|
+
const result = await runCodegraphSessionStartWorker({
|
|
60
|
+
cwd: workspace,
|
|
61
|
+
env: { HOME: homeDir },
|
|
62
|
+
nodeVersion: "26.3.0",
|
|
63
|
+
logOutcome: (outcome) => outcomes.push(outcome),
|
|
64
|
+
deps: {
|
|
65
|
+
ensureGitignored: () => true,
|
|
66
|
+
ensureProvisioned: () => Promise.resolve({ binPath, provisioned: true }),
|
|
67
|
+
prepareWorkspace: () => ({
|
|
68
|
+
dataDir: join(homeDir, ".omo/codegraph/projects/test"),
|
|
69
|
+
dataRoot: join(homeDir, ".omo/codegraph"),
|
|
70
|
+
linked: true,
|
|
71
|
+
mode: "global-linked",
|
|
72
|
+
projectLink: join(workspace, ".codegraph"),
|
|
73
|
+
}),
|
|
74
|
+
resolveCommand: () => ({ argsPrefix: [], command: "/usr/local/bin/codegraph", exists: true, source: "path" }),
|
|
75
|
+
runCommand: (_projectRoot, command, args) => {
|
|
76
|
+
calls.push({ args, command });
|
|
77
|
+
return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// then
|
|
83
|
+
expect(result).toEqual({ action: "initialized" });
|
|
84
|
+
expect(calls).toEqual([
|
|
85
|
+
{ args: ["status", "--json"], command: binPath },
|
|
86
|
+
{ args: ["init"], command: binPath },
|
|
87
|
+
]);
|
|
88
|
+
expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "provisioned", timedOut: false }]);
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
91
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("#given an unsupported local Node but bundled CodeGraph resolves through CODEGRAPH_NODE_BIN #when worker runs #then it bootstraps with the compatible runtime", async () => {
|
|
96
|
+
// given
|
|
97
|
+
const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-compatible-node-"));
|
|
98
|
+
const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-compatible-node-home-"));
|
|
99
|
+
const nodeBin = "/opt/node22/bin/node";
|
|
100
|
+
const calls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
|
|
101
|
+
const outcomes: unknown[] = [];
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// when
|
|
105
|
+
const result = await runCodegraphSessionStartWorker({
|
|
106
|
+
cwd: workspace,
|
|
107
|
+
env: { CODEGRAPH_NODE_BIN: nodeBin, HOME: homeDir },
|
|
108
|
+
nodeVersion: "26.3.0",
|
|
109
|
+
logOutcome: (outcome) => outcomes.push(outcome),
|
|
110
|
+
deps: {
|
|
111
|
+
ensureGitignored: () => true,
|
|
112
|
+
ensureProvisioned: () => {
|
|
113
|
+
throw new Error("provisioning should not run when bundled CodeGraph resolved");
|
|
114
|
+
},
|
|
115
|
+
prepareWorkspace: () => ({
|
|
116
|
+
dataDir: join(homeDir, ".omo/codegraph/projects/test"),
|
|
117
|
+
dataRoot: join(homeDir, ".omo/codegraph"),
|
|
118
|
+
linked: true,
|
|
119
|
+
mode: "global-linked",
|
|
120
|
+
projectLink: join(workspace, ".codegraph"),
|
|
121
|
+
}),
|
|
122
|
+
resolveCommand: () => ({ argsPrefix: ["codegraph.js"], command: nodeBin, exists: true, source: "bundled" }),
|
|
123
|
+
runCommand: (_projectRoot, command, args) => {
|
|
124
|
+
calls.push({ args, command });
|
|
125
|
+
return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// then
|
|
131
|
+
expect(result).toEqual({ action: "initialized" });
|
|
132
|
+
expect(calls).toEqual([
|
|
133
|
+
{ args: ["codegraph.js", "status", "--json"], command: nodeBin },
|
|
134
|
+
{ args: ["codegraph.js", "init"], command: nodeBin },
|
|
135
|
+
]);
|
|
136
|
+
expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "bundled", timedOut: false }]);
|
|
137
|
+
} finally {
|
|
138
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
139
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// components/teammode/src/codex-hook.ts
|
|
4
|
+
var CREATE_THREAD_TOOL_NAMES = new Set(["create_thread", "codex_app.create_thread"]);
|
|
5
|
+
function parsePostToolUsePayload(raw) {
|
|
6
|
+
if (raw.trim().length === 0)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
return isPostToolUsePayload(parsed) ? parsed : null;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof SyntaxError)
|
|
13
|
+
return null;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function runPostToolUseHook(payload) {
|
|
18
|
+
if (payload.hook_event_name !== "PostToolUse")
|
|
19
|
+
return "";
|
|
20
|
+
if (!CREATE_THREAD_TOOL_NAMES.has(payload.tool_name))
|
|
21
|
+
return "";
|
|
22
|
+
const threadReference = extractThreadCreationReference(payload.tool_response);
|
|
23
|
+
if (threadReference === null)
|
|
24
|
+
return "";
|
|
25
|
+
const output = {
|
|
26
|
+
hookSpecificOutput: {
|
|
27
|
+
hookEventName: "PostToolUse",
|
|
28
|
+
additionalContext: threadTitleReminder(threadReference)
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return `${JSON.stringify(output)}
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
async function runTeammodeHookCli(stdin, stdout) {
|
|
35
|
+
const payload = parsePostToolUsePayload(await readAll(stdin));
|
|
36
|
+
if (payload === null)
|
|
37
|
+
return;
|
|
38
|
+
const output = runPostToolUseHook(payload);
|
|
39
|
+
if (output.length > 0)
|
|
40
|
+
stdout.write(output);
|
|
41
|
+
}
|
|
42
|
+
function threadTitleReminder(threadReference) {
|
|
43
|
+
const id = formatIdentifier(threadReference.id);
|
|
44
|
+
return threadReference.kind === "thread" ? `THREAD ID ${id}: CALL codex_app.set_thread_title NOW. USE THE REAL TASK/ROLE.` : `PENDING WORKTREE ID ${id}: CALL codex_app.set_thread_title AS SOON AS THREAD ID EXISTS. USE THE REAL TASK/ROLE.`;
|
|
45
|
+
}
|
|
46
|
+
function formatIdentifier(value) {
|
|
47
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
48
|
+
return normalized.length <= 200 ? normalized : `${normalized.slice(0, 197)}...`;
|
|
49
|
+
}
|
|
50
|
+
function extractThreadCreationReference(toolResponse) {
|
|
51
|
+
if (!isRecord(toolResponse))
|
|
52
|
+
return null;
|
|
53
|
+
const threadId = toolResponse["threadId"];
|
|
54
|
+
if (typeof threadId === "string" && threadId.trim().length > 0) {
|
|
55
|
+
return { kind: "thread", id: threadId };
|
|
56
|
+
}
|
|
57
|
+
const pendingWorktreeId = toolResponse["pendingWorktreeId"];
|
|
58
|
+
if (typeof pendingWorktreeId === "string" && pendingWorktreeId.trim().length > 0) {
|
|
59
|
+
return { kind: "pendingWorktree", id: pendingWorktreeId };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function isPostToolUsePayload(value) {
|
|
64
|
+
if (!isRecord(value))
|
|
65
|
+
return false;
|
|
66
|
+
return value["hook_event_name"] === "PostToolUse" && typeof value["session_id"] === "string" && typeof value["tool_name"] === "string" && Object.hasOwn(value, "tool_input") && Object.hasOwn(value, "tool_response") && optionalString(value["turn_id"]) && optionalString(value["cwd"]) && optionalString(value["model"]) && optionalString(value["permission_mode"]) && optionalString(value["tool_use_id"]) && (value["transcript_path"] === undefined || value["transcript_path"] === null || typeof value["transcript_path"] === "string");
|
|
67
|
+
}
|
|
68
|
+
function optionalString(value) {
|
|
69
|
+
return value === undefined || typeof value === "string";
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
function readAll(stdin) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let data = "";
|
|
77
|
+
stdin.setEncoding("utf8");
|
|
78
|
+
stdin.on("data", (chunk) => {
|
|
79
|
+
data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
|
80
|
+
});
|
|
81
|
+
stdin.once("error", reject);
|
|
82
|
+
stdin.once("end", () => resolve(data));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// components/teammode/src/cli.ts
|
|
87
|
+
var [command, subcommand] = process.argv.slice(2);
|
|
88
|
+
if (command === "hook" && subcommand === "post-tool-use") {
|
|
89
|
+
await runTeammodeHookCli(process.stdin, process.stdout);
|
|
90
|
+
} else {
|
|
91
|
+
process.stderr.write(`Usage: omo-teammode hook post-tool-use
|
|
92
|
+
`);
|
|
93
|
+
process.exitCode = 2;
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "^(create_thread|codex_app\\.create_thread)$",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
|
|
10
|
+
"timeout": 10,
|
|
11
|
+
"statusMessage": "(OmO) Checking Thread Title Hygiene"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sisyphuslabs/codex-teammode",
|
|
3
|
+
"version": "4.12.1",
|
|
4
|
+
"description": "Codex team-mode hook component that keeps background thread titles descriptive after create_thread.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"hooks",
|
|
10
|
+
"skills"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build src/cli.ts --target node --format esm --outfile dist/cli.js",
|
|
14
|
+
"test": "bun test test/*.test.ts",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^25.9.3",
|
|
19
|
+
"bun-types": "^1.3.1",
|
|
20
|
+
"typescript": "^6.0.3",
|
|
21
|
+
"vitest": "^4.1.8"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runTeammodeHookCli } from "./codex-hook.js";
|
|
4
|
+
|
|
5
|
+
const [command, subcommand] = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (command === "hook" && subcommand === "post-tool-use") {
|
|
8
|
+
await runTeammodeHookCli(process.stdin, process.stdout);
|
|
9
|
+
} else {
|
|
10
|
+
process.stderr.write("Usage: omo-teammode hook post-tool-use\n");
|
|
11
|
+
process.exitCode = 2;
|
|
12
|
+
}
|