omp-multi-harness 0.1.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.
- package/LICENSE +21 -0
- package/README.md +351 -0
- package/package.json +76 -0
- package/scripts/cli.ts +164 -0
- package/scripts/setup/claude.ts +41 -0
- package/scripts/setup/codex.ts +35 -0
- package/scripts/setup/omp.ts +167 -0
- package/scripts/setup/toolchain.ts +81 -0
- package/scripts/setup/types.ts +76 -0
- package/scripts/setup.ts +116 -0
- package/src/agents/availability.ts +106 -0
- package/src/agents/claude-events.ts +125 -0
- package/src/agents/claude.ts +226 -0
- package/src/agents/codex-events.ts +149 -0
- package/src/agents/codex.ts +236 -0
- package/src/agents/types.ts +81 -0
- package/src/commands/agents.ts +140 -0
- package/src/commands/delegate-command.ts +159 -0
- package/src/commands/harness-setup.ts +94 -0
- package/src/commands/sessions.ts +394 -0
- package/src/config/load.ts +78 -0
- package/src/config/schema.ts +249 -0
- package/src/index.ts +129 -0
- package/src/process/executable.ts +49 -0
- package/src/process/jsonl.ts +124 -0
- package/src/process/process-error.ts +178 -0
- package/src/process/redact.ts +120 -0
- package/src/process/spawn-agent.ts +218 -0
- package/src/routing/handoff.ts +59 -0
- package/src/routing/prompt.ts +72 -0
- package/src/routing/route.ts +286 -0
- package/src/runs/lock.ts +158 -0
- package/src/runs/registry.ts +379 -0
- package/src/runs/ring-buffer.ts +81 -0
- package/src/runs/types.ts +141 -0
- package/src/sessions/resume.ts +163 -0
- package/src/sessions/store.ts +273 -0
- package/src/tools/agent-runs.ts +169 -0
- package/src/tools/ask-agent.ts +230 -0
- package/src/tools/delegate.ts +196 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** `/agents` — availability + auth report, and `/agents auth <codex|claude>`. */
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { detect, isReady } from "../agents/availability.ts";
|
|
4
|
+
import type { AgentAvailability } from "../agents/types.ts";
|
|
5
|
+
import type { AgentName, MultiHarnessConfig } from "../config/schema.ts";
|
|
6
|
+
import { isWriteLockHeld, writeLockHolder } from "../runs/lock.ts";
|
|
7
|
+
import type { RunRegistry } from "../runs/types.ts";
|
|
8
|
+
|
|
9
|
+
const DISPLAY: Record<AgentName, string> = { codex: "Codex", claude: "Claude Code" };
|
|
10
|
+
const LOGIN: Record<AgentName, string> = { codex: "codex login", claude: "claude auth login" };
|
|
11
|
+
|
|
12
|
+
/** Exact remediation text from _spec/10 EXECUTABLE_NOT_FOUND — never paraphrase. */
|
|
13
|
+
const EXECUTABLE_FIX: Record<AgentName, string> = {
|
|
14
|
+
codex: "Install the Codex CLI, or set multiHarness.codex.executable to its full path.",
|
|
15
|
+
claude: "Install the Claude Code CLI, or set multiHarness.claude.executable to its full path.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Workspace/write-lock state as already resolved by the caller — kept pure, no fs/lock reads here. */
|
|
19
|
+
export interface WorkspaceStatus {
|
|
20
|
+
cwd: string;
|
|
21
|
+
writeLockHeld: boolean;
|
|
22
|
+
/** Holder label (agent name / run id) — safe to display, never a path or credential. */
|
|
23
|
+
writeLockHolder?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Aggregate run counts from the registry, if one was supplied. */
|
|
27
|
+
export interface RunCounts {
|
|
28
|
+
running: number;
|
|
29
|
+
finished: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isExecutableMissing(a: AgentAvailability): boolean {
|
|
33
|
+
return !a.available && !!a.reason && a.reason.includes("not found on PATH");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function statusWord(a: AgentAvailability): string {
|
|
37
|
+
if (isExecutableMissing(a)) return "unavailable — executable not found";
|
|
38
|
+
if (!a.available) return `unavailable — ${a.reason ?? "unknown reason"}`;
|
|
39
|
+
if (a.auth === "logged-out") return "unavailable — not authenticated";
|
|
40
|
+
if (a.auth === "unknown") return "installed, auth not checked";
|
|
41
|
+
return "ready";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pure renderer for `/agents` — no process spawning, no UI calls. Takes already-resolved
|
|
46
|
+
* availability plus workspace/run state so it is directly unit-testable (_spec/07 `/agents`,
|
|
47
|
+
* _spec/12 criterion B: must degrade gracefully, never throw, when an agent is missing).
|
|
48
|
+
*/
|
|
49
|
+
export function renderAgentsReport(items: AgentAvailability[], workspace: WorkspaceStatus, runs?: RunCounts): string {
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
for (const a of items) {
|
|
52
|
+
lines.push(DISPLAY[a.agent]);
|
|
53
|
+
lines.push(` executable: ${a.executablePath ?? "not found"}`);
|
|
54
|
+
lines.push(` version: ${a.version ?? "-"}`);
|
|
55
|
+
lines.push(` auth: ${a.authDetail}`);
|
|
56
|
+
lines.push(` status: ${statusWord(a)}`);
|
|
57
|
+
if (isExecutableMissing(a)) lines.push(` fix: ${EXECUTABLE_FIX[a.agent]}`);
|
|
58
|
+
lines.push("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lockState = workspace.writeLockHeld
|
|
62
|
+
? `held by ${workspace.writeLockHolder ?? "unknown"}`
|
|
63
|
+
: "free";
|
|
64
|
+
lines.push(`Workspace: ${workspace.cwd} (write lock: ${lockState})`);
|
|
65
|
+
// Omit the Runs line entirely when we don't have a registry to ask — printing "0 running"
|
|
66
|
+
// would claim knowledge we don't have.
|
|
67
|
+
if (runs) lines.push(`Runs: ${runs.running} running, ${runs.finished} finished`);
|
|
68
|
+
|
|
69
|
+
const ready = items.filter(isReady).map((a) => DISPLAY[a.agent]);
|
|
70
|
+
lines.push(ready.length > 0 ? `Ready: ${ready.join(", ")}` : "Ready: none — see the auth hints above");
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderAuth(items: AgentAvailability[]): string {
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
for (const a of items) {
|
|
77
|
+
lines.push(`${DISPLAY[a.agent].padEnd(12)} ${a.authDetail}`);
|
|
78
|
+
}
|
|
79
|
+
const loggedOut = items.filter((a) => a.available && a.auth !== "ok");
|
|
80
|
+
if (loggedOut.length > 0) {
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push("Run these yourself — this extension never logs in on your behalf:");
|
|
83
|
+
for (const a of loggedOut) lines.push(` ${LOGIN[a.agent]}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("Each CLI keeps its own login; OMP's credentials are never shared with them.");
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function runCounts(registry: RunRegistry): RunCounts {
|
|
91
|
+
const all = registry.list();
|
|
92
|
+
let running = 0;
|
|
93
|
+
let finished = 0;
|
|
94
|
+
for (const r of all) {
|
|
95
|
+
if (r.status === "running" || r.status === "queued") running++;
|
|
96
|
+
else finished++;
|
|
97
|
+
}
|
|
98
|
+
return { running, finished };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `getRegistry` is OPTIONAL and defaults to absent so the existing two-argument call site in
|
|
103
|
+
* `src/index.ts` (`registerAgentsCommand(pi, getConfig)`) keeps working unchanged. Passing a
|
|
104
|
+
* third argument turns on the `Runs:` line in the report.
|
|
105
|
+
*/
|
|
106
|
+
export function registerAgentsCommand(
|
|
107
|
+
pi: ExtensionAPI,
|
|
108
|
+
getConfig: () => MultiHarnessConfig,
|
|
109
|
+
getRegistry?: () => RunRegistry,
|
|
110
|
+
): void {
|
|
111
|
+
pi.registerCommand("agents", {
|
|
112
|
+
description: "Show Codex / Claude Code availability and authentication",
|
|
113
|
+
getArgumentCompletions: (prefix: string) =>
|
|
114
|
+
["auth", "auth codex", "auth claude"]
|
|
115
|
+
.filter((v) => v.startsWith(prefix))
|
|
116
|
+
.map((v) => ({ value: v, label: v })),
|
|
117
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
118
|
+
const config = getConfig();
|
|
119
|
+
const argv = args.trim().split(/\s+/).filter(Boolean);
|
|
120
|
+
const authOnly = argv[0] === "auth";
|
|
121
|
+
const wanted = argv[1] as AgentName | undefined;
|
|
122
|
+
|
|
123
|
+
const names: AgentName[] = wanted === "codex" || wanted === "claude" ? [wanted] : ["codex", "claude"];
|
|
124
|
+
const items = await Promise.all(names.map((n) => detect(n, config[n], { cwd: ctx.cwd, force: true })));
|
|
125
|
+
|
|
126
|
+
if (authOnly) {
|
|
127
|
+
ctx.ui.notify(renderAuth(items), "info");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const workspace: WorkspaceStatus = {
|
|
132
|
+
cwd: ctx.cwd,
|
|
133
|
+
writeLockHeld: isWriteLockHeld(ctx.cwd),
|
|
134
|
+
writeLockHolder: writeLockHolder(ctx.cwd),
|
|
135
|
+
};
|
|
136
|
+
const runs = getRegistry ? runCounts(getRegistry()) : undefined;
|
|
137
|
+
ctx.ui.notify(renderAgentsReport(items, workspace, runs), "info");
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** `/codex <task>` and `/claude <task>` — direct delegation, bypassing routing. */
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { MODE_DEFAULT_READ_ONLY, type AgentMode, type AgentName } from "../agents/types.ts";
|
|
4
|
+
import { isValidModelToken, type MultiHarnessConfig } from "../config/schema.ts";
|
|
5
|
+
import { AgentError } from "../process/process-error.ts";
|
|
6
|
+
import { buildHandoff, summarize, truncateMiddle } from "../routing/handoff.ts";
|
|
7
|
+
import type { RunRegistry } from "../runs/types.ts";
|
|
8
|
+
import { deliverBackgroundResult } from "../tools/ask-agent.ts";
|
|
9
|
+
|
|
10
|
+
const MODES = new Set<AgentMode>(["analyze", "plan", "implement", "debug", "review", "test"]);
|
|
11
|
+
|
|
12
|
+
export interface ParsedCommand {
|
|
13
|
+
task: string;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
newSession: boolean;
|
|
16
|
+
background: boolean;
|
|
17
|
+
model?: string;
|
|
18
|
+
mode?: AgentMode;
|
|
19
|
+
errors: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Flags are stripped from the front; everything else is the task, verbatim. */
|
|
23
|
+
export function parseDelegateArgs(args: string): ParsedCommand {
|
|
24
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
25
|
+
const result: ParsedCommand = { task: "", newSession: false, background: false, errors: [] };
|
|
26
|
+
let i = 0;
|
|
27
|
+
|
|
28
|
+
for (; i < tokens.length; i++) {
|
|
29
|
+
const token = tokens[i]!;
|
|
30
|
+
if (!token.startsWith("--")) break;
|
|
31
|
+
switch (token) {
|
|
32
|
+
case "--read-only":
|
|
33
|
+
result.readOnly = true;
|
|
34
|
+
break;
|
|
35
|
+
case "--write":
|
|
36
|
+
result.readOnly = false;
|
|
37
|
+
break;
|
|
38
|
+
case "--new":
|
|
39
|
+
result.newSession = true;
|
|
40
|
+
break;
|
|
41
|
+
case "--bg":
|
|
42
|
+
result.background = true;
|
|
43
|
+
break;
|
|
44
|
+
case "--model": {
|
|
45
|
+
const value = tokens[++i];
|
|
46
|
+
if (!value) result.errors.push("--model needs a value");
|
|
47
|
+
else if (!isValidModelToken(value)) result.errors.push(`--model ${value}: not a valid model token`);
|
|
48
|
+
else result.model = value;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "--mode": {
|
|
52
|
+
const value = tokens[++i];
|
|
53
|
+
if (value && MODES.has(value as AgentMode)) result.mode = value as AgentMode;
|
|
54
|
+
else result.errors.push(`--mode ${value ?? ""}: expected one of ${[...MODES].join(", ")}`);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
result.errors.push(`unknown flag ${token}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result.task = tokens.slice(i).join(" ");
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DelegateCommandDeps {
|
|
67
|
+
pi: ExtensionAPI;
|
|
68
|
+
agent: AgentName;
|
|
69
|
+
getConfig: () => MultiHarnessConfig;
|
|
70
|
+
/** Taken as a getter so this module never imports the registry implementation. */
|
|
71
|
+
getRegistry: () => RunRegistry;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function registerDelegateCommand({ pi, agent, getConfig, getRegistry }: DelegateCommandDeps): void {
|
|
75
|
+
pi.registerCommand(agent, {
|
|
76
|
+
description: `Delegate a task directly to ${agent === "codex" ? "Codex" : "Claude Code"}`,
|
|
77
|
+
getArgumentCompletions: (prefix: string) =>
|
|
78
|
+
["--read-only", "--write", "--new", "--bg", "--model", "--mode"]
|
|
79
|
+
.filter((f) => f.startsWith(prefix))
|
|
80
|
+
.map((f) => ({ value: f, label: f })),
|
|
81
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
82
|
+
const config = getConfig();
|
|
83
|
+
const parsed = parseDelegateArgs(args);
|
|
84
|
+
|
|
85
|
+
if (parsed.errors.length > 0) {
|
|
86
|
+
ctx.ui.notify(parsed.errors.join("\n"), "error");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!parsed.task) {
|
|
90
|
+
ctx.ui.notify(
|
|
91
|
+
`Usage: /${agent} [--read-only] [--write] [--new] [--bg] [--mode <mode>] [--model <id>] <task>`,
|
|
92
|
+
"warning",
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!config.enabled || !config[agent].enabled) {
|
|
97
|
+
ctx.ui.notify(`${agent} is disabled in config (multiHarness.${agent}.enabled).`, "error");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const readOnly = parsed.readOnly ?? (parsed.mode ? MODE_DEFAULT_READ_ONLY[parsed.mode] : false);
|
|
101
|
+
|
|
102
|
+
// Through the registry, not straight to the adapter: that is what gives a slash command
|
|
103
|
+
// the same write lock, concurrency cap, and `/sessions` row as a tool call.
|
|
104
|
+
const registry = getRegistry();
|
|
105
|
+
const started = registry.start({
|
|
106
|
+
agent,
|
|
107
|
+
task: buildHandoff({ task: parsed.task, mode: parsed.mode, maxChars: config.limits.maxHandoffChars }),
|
|
108
|
+
summary: summarize(parsed.task),
|
|
109
|
+
cwd: ctx.cwd,
|
|
110
|
+
mode: parsed.mode,
|
|
111
|
+
readOnly,
|
|
112
|
+
model: parsed.model,
|
|
113
|
+
continueSession: !parsed.newSession,
|
|
114
|
+
background: parsed.background,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (parsed.background) {
|
|
118
|
+
// Detached by design; `.catch` is mandatory (_spec/01 §2).
|
|
119
|
+
registry
|
|
120
|
+
.wait(started.id)
|
|
121
|
+
.then((finished) => {
|
|
122
|
+
if (finished) deliverBackgroundResult({ pi, ctx, run: finished, config });
|
|
123
|
+
})
|
|
124
|
+
.catch((e) => pi.logger.warn?.(`[multi-harness] background run ${started.id}: ${(e as Error).message}`));
|
|
125
|
+
|
|
126
|
+
ctx.ui.notify(`Started ${agent} run ${started.id} in the background. Track it with /sessions.`, "info");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const unsubscribe = registry.subscribe((run) => {
|
|
131
|
+
if (run.id === started.id) ctx.ui.setStatus("multi-harness", `${agent}: ${run.phase}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const finished = await registry.wait(started.id);
|
|
136
|
+
if (!finished) {
|
|
137
|
+
ctx.ui.notify(`Run ${started.id} disappeared from the registry.`, "error");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (finished.status !== "done") {
|
|
141
|
+
ctx.ui.notify(`${finished.errorCode ?? finished.status}: ${finished.errorMessage ?? finished.status}`, "error");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { text } = truncateMiddle(finished.output ?? "", config.limits.maxOutputChars);
|
|
146
|
+
// Relayed as `agent` attribution so consumers can tell it from user-typed text,
|
|
147
|
+
// and so the supervisor can act on it.
|
|
148
|
+
await pi.sendUserMessage(text, { attribution: "agent" });
|
|
149
|
+
} catch (e) {
|
|
150
|
+
const message =
|
|
151
|
+
e instanceof AgentError ? `${e.code}: ${e.message}` : `Unexpected failure: ${(e as Error).message}`;
|
|
152
|
+
ctx.ui.notify(message, "error");
|
|
153
|
+
} finally {
|
|
154
|
+
unsubscribe();
|
|
155
|
+
ctx.ui.setStatus("multi-harness", undefined);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** `/harness-setup` — the setup checklist from _spec/14, rendered inside a session. */
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { detect } from "../agents/availability.ts";
|
|
6
|
+
import { agentDir, configPaths } from "../config/load.ts";
|
|
7
|
+
import type { MultiHarnessConfig } from "../config/schema.ts";
|
|
8
|
+
|
|
9
|
+
type Row = { ok: boolean | null; label: string; detail: string; fix?: string };
|
|
10
|
+
|
|
11
|
+
function render(rows: Row[]): string {
|
|
12
|
+
const glyph = (ok: boolean | null) => (ok === null ? "!" : ok ? "✔" : "✘");
|
|
13
|
+
const body = rows.map((r) => ` ${glyph(r.ok)} ${r.label.padEnd(30)} ${r.detail}`);
|
|
14
|
+
const fixes = rows.filter((r) => r.ok !== true && r.fix);
|
|
15
|
+
if (fixes.length > 0) {
|
|
16
|
+
body.push("");
|
|
17
|
+
body.push("Fixes:");
|
|
18
|
+
for (const r of fixes) body.push(` · ${r.fix}`);
|
|
19
|
+
}
|
|
20
|
+
body.push("");
|
|
21
|
+
body.push("Full checklist outside a session: `bun run doctor`");
|
|
22
|
+
return body.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerHarnessSetupCommand(
|
|
26
|
+
pi: ExtensionAPI,
|
|
27
|
+
getConfig: () => MultiHarnessConfig,
|
|
28
|
+
getSources: () => string[],
|
|
29
|
+
): void {
|
|
30
|
+
pi.registerCommand("harness-setup", {
|
|
31
|
+
description: "Check multi-harness setup: CLIs, authentication, config, install",
|
|
32
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
33
|
+
const config = getConfig();
|
|
34
|
+
const [codex, claude] = await Promise.all([
|
|
35
|
+
detect("codex", config.codex, { cwd: ctx.cwd, force: true }),
|
|
36
|
+
detect("claude", config.claude, { cwd: ctx.cwd, force: true }),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const link = join(agentDir(), "extensions", "multi-harness");
|
|
40
|
+
const sources = getSources();
|
|
41
|
+
const { user } = configPaths(ctx.cwd);
|
|
42
|
+
const models = ctx.models.list().length;
|
|
43
|
+
|
|
44
|
+
const rows: Row[] = [
|
|
45
|
+
{
|
|
46
|
+
ok: models > 0,
|
|
47
|
+
label: "OMP provider auth",
|
|
48
|
+
detail: models > 0 ? `${models} model(s) available` : "no authenticated models",
|
|
49
|
+
fix: models > 0 ? undefined : "run `omp` and use /login (OMP's own login, never shared with the workers)",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
ok: config.routing.mode === "rules" ? true : models > 0,
|
|
53
|
+
label: "Router model (auto)",
|
|
54
|
+
detail:
|
|
55
|
+
config.routing.mode === "rules"
|
|
56
|
+
? "rules mode — no router model needed"
|
|
57
|
+
: models > 0
|
|
58
|
+
? `${config.routing.model} (falls back to rules on failure)`
|
|
59
|
+
: "unavailable — auto routing falls back to rules",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
ok: codex.available && codex.auth === "ok",
|
|
63
|
+
label: "Codex",
|
|
64
|
+
detail: codex.available ? `${codex.version ?? "?"} · ${codex.authDetail}` : (codex.reason ?? "unavailable"),
|
|
65
|
+
fix: codex.available && codex.auth !== "ok" ? "codex login" : codex.available ? undefined : "install the Codex CLI",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
ok: claude.available && claude.auth === "ok",
|
|
69
|
+
label: "Claude Code",
|
|
70
|
+
detail: claude.available ? `${claude.version ?? "?"} · ${claude.authDetail}` : (claude.reason ?? "unavailable"),
|
|
71
|
+
fix:
|
|
72
|
+
claude.available && claude.auth !== "ok"
|
|
73
|
+
? "claude auth login"
|
|
74
|
+
: claude.available
|
|
75
|
+
? undefined
|
|
76
|
+
: "install Claude Code",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
ok: sources.length > 0 ? true : null,
|
|
80
|
+
label: "Config",
|
|
81
|
+
detail: sources.length > 0 ? sources.join(", ") : `no config file — defaults in use (${user})`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
ok: existsSync(link) ? true : null,
|
|
85
|
+
label: "Installed into OMP",
|
|
86
|
+
detail: existsSync(link) ? link : "not linked — running from -e or a project path",
|
|
87
|
+
fix: existsSync(link) ? undefined : `ln -s <repo> "${link}"`,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
ctx.ui.notify(render(rows), "info");
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|