Skip to content

Commit 35df05f

Browse files
ralyodioclaude
andcommitted
fix(tui): add /install openspec and stop ANTHROPIC_API_KEY hijacking claude sessions
- /install openspec (and /install spec) installs @fission-ai/openspec via npm global; idempotent, with a live install marker on the /spec help line. - openSession scrubs ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN + nested-session markers for the claude engine only, so passthrough sessions use Claude Code's own stored login instead of an inherited key that triggers the "enable models" screen and exits. opencode/aider keep the key (they use it as a provider key). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1aa644e commit 35df05f

3 files changed

Lines changed: 57 additions & 6 deletions

File tree

src/engines.mjs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ export const ENGINES = {
1616
desc: "Claude Code — Anthropic's agentic CLI",
1717
bin: "claude",
1818
install: { cmd: "npm", args: ["install", "-g", "@anthropic-ai/claude-code"] },
19+
// Claude Code authenticates via its own stored login (~/.claude). An
20+
// inherited ANTHROPIC_API_KEY hijacks that subscription auth — and if the
21+
// key can't serve the models, Claude Code shows an "enable models" screen
22+
// and exits straight back to the mosh prompt. Nested-session markers make a
23+
// fresh launch think it's running inside another Claude. Drop both so the
24+
// passthrough session starts clean on its own auth. (opencode/aider legitimately
25+
// use ANTHROPIC_API_KEY as a provider key, so we only scrub it for claude.)
26+
stripEnv: [
27+
"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN",
28+
"CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION",
29+
],
1930
},
2031
codex: {
2132
desc: "Codex — OpenAI's coding CLI",
@@ -72,8 +83,13 @@ export function engineList() {
7283
*/
7384
export function openSession(engine, args = []) {
7485
return new Promise((resolve) => {
86+
let env = process.env;
87+
if (engine.stripEnv?.length) {
88+
env = { ...process.env };
89+
for (const k of engine.stripEnv) delete env[k];
90+
}
7591
let child;
76-
try { child = spawn(engine.bin, args, { stdio: "inherit" }); }
92+
try { child = spawn(engine.bin, args, { stdio: "inherit", env }); }
7793
catch (e) { resolve({ ok: false, error: e }); return; }
7894
child.on("error", (e) => resolve({ ok: false, error: e }));
7995
child.on("exit", (code, signal) => resolve({ ok: true, code, signal }));

src/spec.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { spawn } from "node:child_process";
88
import { isInstalled } from "./engines.mjs";
99

10+
export { isInstalled };
11+
1012
export const OPENSPEC = {
1113
pkg: "@fission-ai/openspec",
1214
bin: "openspec",
@@ -25,6 +27,26 @@ export function resolveSpec(args = []) {
2527
return { cmd: "npx", args: ["-y", `${OPENSPEC.pkg}@latest`, ...args], viaNpx: true };
2628
}
2729

30+
/** True when the `openspec` binary is resolvable on PATH. */
31+
export function isSpecInstalled() {
32+
return isInstalled(OPENSPEC.bin);
33+
}
34+
35+
/**
36+
* Install the OpenSpec CLI globally (same deal as engines — we drive it, don't
37+
* vendor it). stdio inherited so npm's own progress owns the terminal. Resolves
38+
* { ok, code } | { ok:false, error }.
39+
*/
40+
export function installSpec() {
41+
return new Promise((resolve) => {
42+
let child;
43+
try { child = spawn(OPENSPEC.install.cmd, OPENSPEC.install.args, { stdio: "inherit" }); }
44+
catch (e) { resolve({ ok: false, error: e }); return; }
45+
child.on("error", (e) => resolve({ ok: false, error: e }));
46+
child.on("exit", (code) => resolve({ ok: true, code }));
47+
});
48+
}
49+
2850
/**
2951
* Run openspec with stdio inherited so it fully owns the terminal (its own
3052
* prompts/output — full passthrough). Resolves { ok, code, viaNpx }.

src/tui.mjs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import fs from "node:fs";
88
import os from "node:os";
99
import path from "node:path";
1010
import { ENGINES, resolveEngine, engineStatus, openSession } from "./engines.mjs";
11-
import { runSpec } from "./spec.mjs";
11+
import { runSpec, installSpec, isSpecInstalled, OPENSPEC } from "./spec.mjs";
1212
import { compile, run } from "./interpreter.mjs";
1313
import { defaultCommands } from "./commands.mjs";
1414
import { banner, hr, acid, ash, bone, dim, ok, err, info } from "./ui.mjs";
@@ -68,8 +68,9 @@ function printHelp() {
6868
bone(" commands"),
6969
` ${acid("/agents")} list coding engines`,
7070
` ${acid("/agents <name>")} open a session (claude · codex · gemini · aider · opencode)`,
71-
` ${acid("/install <name>")} install an engine`,
72-
` ${acid("/spec [init|…]")} spec-driven dev — writes openspec/ to your repo (OpenSpec)`,
71+
` ${acid("/install <name>")} install an engine (or ${bone("openspec")})`,
72+
` ${acid("/spec [init|…]")} spec-driven dev — writes openspec/ to your repo (OpenSpec)` +
73+
ash(isSpecInstalled() ? " ●" : " ○ /install openspec"),
7374
` ${acid("/run <file.mosh>")} run a moshscript program`,
7475
` ${acid("/help")} this`,
7576
` ${acid("/quit")} leave the pit (or Ctrl-D)`,
@@ -107,6 +108,16 @@ function installEngine(key) {
107108
});
108109
}
109110

111+
async function installOpenSpec() {
112+
if (isSpecInstalled()) { console.log(ok("openspec already installed. 🤘 try /spec init")); return; }
113+
console.log(info(`installing openspec: ${OPENSPEC.install.cmd} ${OPENSPEC.install.args.join(" ")}`));
114+
console.log(hr());
115+
const r = await installSpec();
116+
console.log(hr());
117+
if (!r.ok) { console.log(err(`install failed: ${r.error?.message || r.error}`)); return; }
118+
console.log(r.code === 0 ? ok("openspec installed. 🤘 try /spec init") : err(`install exited ${r.code}`));
119+
}
120+
110121
async function openSpec(args) {
111122
console.log(info(`openspec — spec-driven dev, saved to ${bone("openspec/")} in your repo. hand-off to its CLI…`));
112123
console.log(hr());
@@ -160,9 +171,11 @@ export async function tui() {
160171
continue;
161172
}
162173
if (cmd === "install") {
163-
if (!rest[0]) { console.log(err("usage: /install <engine>")); continue; }
174+
if (!rest[0]) { console.log(err("usage: /install <engine|openspec>")); continue; }
175+
const target = rest[0].toLowerCase();
164176
rl.close();
165-
await installEngine(rest[0].toLowerCase());
177+
if (target === "openspec" || target === "spec") await installOpenSpec();
178+
else await installEngine(target);
166179
rl = mkrl();
167180
continue;
168181
}

0 commit comments

Comments
 (0)