@oxbit/cli 0.11.3 → 0.11.5

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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import { parseArgs } from "node:util";
3
4
  import { createInterface } from "node:readline";
4
5
  import { mkdir, readFile as readFile$1, writeFile as writeFile$1, chmod, rename, stat, rm, readdir, realpath, unlink, open, access, cp } from "node:fs/promises";
5
6
  import { join, dirname, isAbsolute, sep, basename, resolve } from "node:path";
6
7
  import { randomBytes, createHash, randomUUID } from "node:crypto";
7
8
  import { homedir } from "node:os";
8
- import { spawn } from "node:child_process";
9
9
  import { createServer } from "node:http";
10
10
  async function* lines(chunks) {
11
11
  let buffer = "";
@@ -1124,6 +1124,73 @@ Additional focus: ${focus}` : "");
1124
1124
  }
1125
1125
  }
1126
1126
  }
1127
+ const OXBIT_CLI_PROVIDER = "oxbit-cli";
1128
+ const ACP_PROVIDERS = [
1129
+ {
1130
+ id: OXBIT_CLI_PROVIDER,
1131
+ name: "Oxbit Agent CLI",
1132
+ command: "oxbit-agent",
1133
+ args: ["acp"],
1134
+ setup: "Runs the oxbit-agent CLI that ships with Oxbit. It reads providers, keys, MCP servers, skills and hooks from ~/.oxbit/agent. Elsewhere, install @oxbit/agent from npm.",
1135
+ url: "https://github.com/yannelli/oxbit/tree/main/apps/agent",
1136
+ registryId: "oxbit-agent"
1137
+ },
1138
+ {
1139
+ id: "codex",
1140
+ name: "Codex ACP",
1141
+ command: "npx",
1142
+ args: ["-y", "@agentclientprotocol/codex-acp@2.2.2"],
1143
+ setup: "Uses the Codex ACP adapter. Sign in through an advertised authentication method, or use your existing Codex credentials.",
1144
+ url: "https://github.com/agentclientprotocol/codex-acp",
1145
+ registryId: "codex-acp"
1146
+ },
1147
+ {
1148
+ id: "claude",
1149
+ name: "Claude Agent",
1150
+ command: "npx",
1151
+ args: ["-y", "@agentclientprotocol/claude-agent-acp@0.89.1"],
1152
+ setup: "Uses the Claude Agent ACP adapter. Sign in through an advertised authentication method, or use your existing Claude Code credentials.",
1153
+ url: "https://github.com/agentclientprotocol/claude-agent-acp",
1154
+ registryId: "claude-acp"
1155
+ },
1156
+ {
1157
+ id: "gemini",
1158
+ name: "Gemini CLI",
1159
+ command: "npx",
1160
+ args: ["-y", "@google/gemini-cli@0.63.0", "--acp"],
1161
+ setup: "Runs Gemini CLI in ACP mode. Sign in through an advertised authentication method, or set GEMINI_API_KEY in the runtime environment.",
1162
+ url: "https://github.com/google-gemini/gemini-cli",
1163
+ registryId: "gemini"
1164
+ },
1165
+ {
1166
+ id: "copilot",
1167
+ name: "GitHub Copilot",
1168
+ command: "npx",
1169
+ args: ["-y", "@github/copilot@1.0.95", "--acp"],
1170
+ setup: "Runs GitHub Copilot CLI in ACP mode. Sign in through an advertised authentication method, or run copilot and use /login first.",
1171
+ url: "https://github.com/github/copilot-cli",
1172
+ registryId: "github-copilot-cli"
1173
+ },
1174
+ {
1175
+ id: "cursor",
1176
+ name: "Cursor ACP",
1177
+ command: "agent",
1178
+ args: ["acp"],
1179
+ setup: "Install Cursor CLI and run agent login first. If your executable is cursor-agent, change the command below.",
1180
+ url: "https://cursor.com/docs/cli/acp",
1181
+ registryId: "cursor"
1182
+ },
1183
+ {
1184
+ id: "amp",
1185
+ name: "Amp Agent ACP",
1186
+ command: "npx",
1187
+ args: ["-y", "amp-acp@0.10.0"],
1188
+ setup: "Uses the community Amp ACP adapter. Install Amp CLI and run amp login first. Set AMP_CLI_PATH in the runtime environment if needed.",
1189
+ url: "https://github.com/tao12345666333/amp-acp",
1190
+ registryId: "amp-acp"
1191
+ }
1192
+ ];
1193
+ const acpPreset = (id2) => ACP_PROVIDERS.find((provider2) => provider2.id === id2);
1127
1194
  const OXBIT_SUBAGENT_TOOL = "task";
1128
1195
  const FORWARDED_HOOKS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure", "PreCompact", "PostCompact"]);
1129
1196
  const childCallId = (spawnId, id2) => `${spawnId}/${id2}`;
@@ -4796,19 +4863,18 @@ class FileCatalogCache {
4796
4863
  }
4797
4864
  }
4798
4865
  function agentVersion() {
4799
- return "0.11.3";
4866
+ return "0.11.5";
4800
4867
  }
4801
4868
  const clone = (message2) => JSON.parse(JSON.stringify(message2));
4802
- class MemoryConnection {
4803
- server;
4869
+ class AcpClient {
4804
4870
  client;
4805
4871
  listeners = /* @__PURE__ */ new Set();
4806
4872
  permission;
4807
4873
  waiting = /* @__PURE__ */ new Map();
4808
4874
  /** The initialize result. */
4809
4875
  initialized = {};
4810
- constructor(factory) {
4811
- this.client = new JsonRpcPeer((message2) => this.server.receive(clone(message2)), {
4876
+ constructor(send) {
4877
+ this.client = new JsonRpcPeer(send, {
4812
4878
  request: (method, params) => this.serve(method, params),
4813
4879
  notification: (method, params) => {
4814
4880
  if (method !== "session/update") return;
@@ -4820,16 +4886,12 @@ class MemoryConnection {
4820
4886
  }
4821
4887
  }
4822
4888
  });
4823
- this.server = new AcpServer((message2) => this.client.receive(clone(message2)), factory);
4824
4889
  }
4825
- /** Connects and sends initialize. Without fs or terminal capabilities the server serves them locally. */
4826
- static async open(factory, capabilities = {}) {
4827
- const connection = new MemoryConnection(factory);
4828
- connection.initialized = await connection.request(
4890
+ async initialize(capabilities) {
4891
+ this.initialized = await this.request(
4829
4892
  "initialize",
4830
4893
  { protocolVersion: 1, clientCapabilities: capabilities, clientInfo: { name: "oxbit-agent", version: agentVersion() } }
4831
4894
  );
4832
- return connection;
4833
4895
  }
4834
4896
  request(method, params = {}) {
4835
4897
  return this.client.request(method, params);
@@ -4856,8 +4918,14 @@ class MemoryConnection {
4856
4918
  setMode(sessionId, modeId) {
4857
4919
  return this.request("session/set_mode", { sessionId, modeId });
4858
4920
  }
4859
- setModel(sessionId, modelId) {
4860
- return this.request("session/set_model", { sessionId, modelId });
4921
+ /** session/set_model, else the `model` config option for agents without that method. */
4922
+ async setModel(sessionId, modelId) {
4923
+ try {
4924
+ return await this.request("session/set_model", { sessionId, modelId });
4925
+ } catch (error) {
4926
+ if (error.code !== -32601) throw error;
4927
+ return this.setConfigOption(sessionId, "model", modelId);
4928
+ }
4861
4929
  }
4862
4930
  setConfigOption(sessionId, configId, value2) {
4863
4931
  return this.request("session/set_config_option", { sessionId, configId, value: value2 });
@@ -4873,9 +4941,6 @@ class MemoryConnection {
4873
4941
  if (this.permission === handler2) this.permission = void 0;
4874
4942
  };
4875
4943
  }
4876
- close() {
4877
- return this.server.close("The connection was closed");
4878
- }
4879
4944
  async serve(method, params) {
4880
4945
  if (method !== "session/request_permission") throw Object.assign(new Error(`Method not found: ${method}`), { code: -32601 });
4881
4946
  const handler2 = this.permission;
@@ -4892,6 +4957,115 @@ class MemoryConnection {
4892
4957
  }
4893
4958
  }
4894
4959
  }
4960
+ class MemoryConnection extends AcpClient {
4961
+ server;
4962
+ constructor(factory) {
4963
+ super((message2) => this.server.receive(clone(message2)));
4964
+ this.server = new AcpServer((message2) => this.client.receive(clone(message2)), factory);
4965
+ }
4966
+ /** Connects and sends initialize. Without fs or terminal capabilities the server serves them locally. */
4967
+ static async open(factory, capabilities = {}) {
4968
+ const connection = new MemoryConnection(factory);
4969
+ await connection.initialize(capabilities);
4970
+ return connection;
4971
+ }
4972
+ close() {
4973
+ return this.server.close("The connection was closed");
4974
+ }
4975
+ }
4976
+ class ProcessConnection extends AcpClient {
4977
+ constructor(child, rewrite) {
4978
+ super((message2) => void (child.stdin.writable && child.stdin.write(`${JSON.stringify(message2)}
4979
+ `)));
4980
+ this.child = child;
4981
+ this.rewrite = rewrite;
4982
+ child.stdin.on("error", () => {
4983
+ });
4984
+ child.stderr.on("data", (chunk) => void (this.stderr = (this.stderr + chunk).slice(-4e3)));
4985
+ createInterface({ input: child.stdout, crlfDelay: Infinity }).on("line", (line) => {
4986
+ if (!line.trim()) return;
4987
+ let message2;
4988
+ try {
4989
+ message2 = JSON.parse(line);
4990
+ } catch {
4991
+ return;
4992
+ }
4993
+ this.client.receive(message2);
4994
+ });
4995
+ this.exited = new Promise((resolve2) => child.once("close", (code, signal) => {
4996
+ this.client.close(`The agent exited (${signal ?? `code ${code}`})${this.stderr.trim() ? `: ${this.stderr.trim().split("\n").at(-1)}` : ""}`);
4997
+ resolve2();
4998
+ }));
4999
+ }
5000
+ child;
5001
+ rewrite;
5002
+ static CLOSE_GRACE_MS = 2e3;
5003
+ stderr = "";
5004
+ exited;
5005
+ /** Starts `agent` in `cwd` and sends initialize. `rewrite` replaces error replies, such as an auth_required error with sign-in steps. */
5006
+ static async open(agent, cwd, options = {}) {
5007
+ const child = spawn(agent.command, agent.args, { cwd, env: agent.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
5008
+ const failed2 = new Promise((_, reject) => child.once("error", (error) => reject(new Error(`Could not start ${agent.command}: ${error.message}`))));
5009
+ const connection = new ProcessConnection(child, options.rewrite);
5010
+ try {
5011
+ await Promise.race([connection.initialize(options.capabilities ?? {}), failed2]);
5012
+ } catch (error) {
5013
+ await connection.close();
5014
+ throw error;
5015
+ }
5016
+ return connection;
5017
+ }
5018
+ async request(method, params = {}) {
5019
+ try {
5020
+ return await super.request(method, params);
5021
+ } catch (error) {
5022
+ throw this.rewrite?.(error) ?? error;
5023
+ }
5024
+ }
5025
+ async close() {
5026
+ if (this.child.pid === void 0 || this.child.exitCode !== null || this.child.signalCode !== null) return;
5027
+ this.child.stdin.end();
5028
+ const timer = setTimeout(() => this.child.kill(), ProcessConnection.CLOSE_GRACE_MS);
5029
+ await this.exited;
5030
+ clearTimeout(timer);
5031
+ }
5032
+ }
5033
+ const CLAUDE_PLAN = { id: "claude", name: "Claude plan", inUse: "Using Claude plan" };
5034
+ const CLAUDE_SIGN_IN = "Sign in with your Claude plan first: oxbit-agent login claude";
5035
+ const INSTALL = "Install Claude Code first: https://code.claude.com/docs/en/setup";
5036
+ const CLAUDE_MODES = { ask: "default", "accept-edits": "acceptEdits", "full-auto": "bypassPermissions" };
5037
+ function claudeAgent(env) {
5038
+ const preset = acpPreset("claude");
5039
+ const { ANTHROPIC_API_KEY: _key, ANTHROPIC_AUTH_TOKEN: _token, ...rest } = env;
5040
+ return { command: preset.command, args: [...preset.args], env: rest };
5041
+ }
5042
+ const openClaude = (agent, cwd) => ProcessConnection.open(agent, cwd, {
5043
+ rewrite: (error) => error.code === -32e3 && /auth/i.test(error.message) ? Object.assign(new Error(CLAUDE_SIGN_IN), { code: error.code }) : error
5044
+ });
5045
+ const run$1 = (args, env, stdio) => new Promise((resolve2, reject) => {
5046
+ const child = spawn("claude", args, { env, stdio: stdio === "inherit" ? "inherit" : ["ignore", "pipe", "ignore"] });
5047
+ let stdout = "";
5048
+ child.stdout?.on("data", (chunk) => void (stdout += chunk));
5049
+ child.once("error", (error) => reject(error.code === "ENOENT" ? Object.assign(new Error(INSTALL), { missing: true }) : error));
5050
+ child.once("close", (code) => resolve2({ code, stdout }));
5051
+ });
5052
+ async function claudeStatus(env) {
5053
+ try {
5054
+ const { stdout } = await run$1(["auth", "status", "--json"], claudeAgent(env).env, "pipe");
5055
+ const status2 = JSON.parse(stdout);
5056
+ return {
5057
+ installed: true,
5058
+ loggedIn: status2?.loggedIn === true,
5059
+ ...status2?.authMethod === "claude.ai" && typeof status2.subscriptionType === "string" ? { plan: status2.subscriptionType } : {}
5060
+ };
5061
+ } catch (error) {
5062
+ return { installed: !error.missing, loggedIn: false };
5063
+ }
5064
+ }
5065
+ const planText = (status2) => !status2.installed ? "Claude Code not installed" : !status2.loggedIn ? "not signed in" : status2.plan ? `signed in with Claude ${status2.plan[0].toUpperCase()}${status2.plan.slice(1)}` : "signed in";
5066
+ async function claudeAuth(command, env) {
5067
+ return (await run$1(["auth", command], claudeAgent(env).env, "inherit")).code ?? 1;
5068
+ }
4895
5069
  function globSource(glob2) {
4896
5070
  let source = "";
4897
5071
  for (let index = 0; index < glob2.length; index++) {
@@ -5194,10 +5368,11 @@ function nodeMcp(config, log) {
5194
5368
  clientInfo: { name: "oxbit-agent", version: agentVersion() }
5195
5369
  });
5196
5370
  }
5197
- const PROVIDER_IDS = PROVIDERS.map((spec) => spec.id);
5371
+ const PROVIDER_IDS = [...PROVIDERS.map((spec) => spec.id), CLAUDE_PLAN.id];
5198
5372
  const LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5199
5373
  const OLLAMA$1 = "http://127.0.0.1:11434";
5200
5374
  const DETECTED = ["anthropic", "openai", "openrouter"];
5375
+ const isClaude = (choice) => choice.id === CLAUDE_PLAN.id;
5201
5376
  function normalizeBase(input2) {
5202
5377
  let url;
5203
5378
  try {
@@ -5237,6 +5412,11 @@ async function methodFor(spec, input2, signedIn, baseUrl) {
5237
5412
  if (ids.includes("api-key") && (ids.length === 1 || await keyFor(spec, input2, baseUrl))) return "api-key";
5238
5413
  return ids.includes("none") ? "none" : ids.find((id2) => id2 !== "chatgpt") ?? ids[0];
5239
5414
  }
5415
+ async function activeProvider(input2) {
5416
+ if ((input2.provider || input2.settings.provider) !== CLAUDE_PLAN.id) return selectProvider(input2);
5417
+ const model = input2.model || input2.settings.providers?.[CLAUDE_PLAN.id]?.model;
5418
+ return { id: CLAUDE_PLAN.id, ...model ? { model } : {} };
5419
+ }
5240
5420
  async function selectProvider(input2) {
5241
5421
  const { settings, env } = input2;
5242
5422
  const signedIn = await chatgptSignedIn(input2.auth);
@@ -5252,6 +5432,7 @@ async function selectProvider(input2) {
5252
5432
  }
5253
5433
  const name = input2.provider || settings.provider || detected || "ollama";
5254
5434
  const spec = providerSpec(name);
5435
+ if (name === CLAUDE_PLAN.id) throw new Error(`${CLAUDE_PLAN.name} runs Claude Agent and has no model provider settings`);
5255
5436
  if (!spec) throw new Error(`Unknown provider ${name}. Use one of: ${PROVIDER_IDS.join(", ")}`);
5256
5437
  const baseUrl = baseFor(spec, settings, env);
5257
5438
  const method = await methodFor(spec, input2, signedIn, baseUrl);
@@ -6159,13 +6340,19 @@ function methodList() {
6159
6340
  lines2.push(` ${spec.name.padEnd(18)} ${method.label.padEnd(24)} ${command}`);
6160
6341
  if (method.detail) lines2.push(` ${"".padEnd(18)} ${method.detail}`);
6161
6342
  }
6343
+ lines2.push(
6344
+ ` ${CLAUDE_PLAN.name.padEnd(18)} ${"Claude Code sign-in".padEnd(24)} oxbit-agent login claude`,
6345
+ ` ${"".padEnd(18)} Runs Claude Agent, which uses Claude Code's own sign-in.`
6346
+ );
6162
6347
  return `${lines2.join("\n")}
6163
6348
  `;
6164
6349
  }
6165
6350
  async function login({ config, auth, io, input: input2 }, rest) {
6166
6351
  if (rest[0] === "chatgpt" && rest.length === 1) return loginChatGPT({ config, auth, io });
6352
+ if (rest[0] === CLAUDE_PLAN.id && rest.length === 1) return loginClaude({ config, io });
6167
6353
  if (rest.length) throw new UsageError(`Unknown login method ${rest.join(" ")}. Run oxbit-agent login to list the methods`);
6168
6354
  if (!input2.provider) return void io.stdout.write(methodList()), 0;
6355
+ if (input2.provider === CLAUDE_PLAN.id) return loginClaude({ config, io });
6169
6356
  const spec = providerSpec(input2.provider);
6170
6357
  if (!spec) throw new UsageError(`Unknown provider ${input2.provider}`);
6171
6358
  const choice = await apiKeyChoice(input2, spec.id);
@@ -6208,9 +6395,20 @@ ${CHATGPT_LABELS.manage}: ${CHATGPT.usageUrl}
6208
6395
  `);
6209
6396
  return 0;
6210
6397
  }
6398
+ async function loginClaude({ config, io }) {
6399
+ io.stderr.write("Signing in to Claude Code with `claude auth login`\n");
6400
+ const code = await claudeAuth("login", io.env);
6401
+ if (code !== 0) return code;
6402
+ if (!config.settings.provider) await config.saveSettings({ provider: CLAUDE_PLAN.id });
6403
+ else if (config.settings.provider !== CLAUDE_PLAN.id)
6404
+ io.stdout.write(`settings.json selects ${config.settings.provider}; run with --provider claude to use your Claude plan.
6405
+ `);
6406
+ return 0;
6407
+ }
6211
6408
  async function logout({ auth, io, input: input2 }, rest) {
6212
6409
  if (rest.length > 1) throw new UsageError(`Unexpected argument ${rest[1]}`);
6213
6410
  const target = rest[0] ?? input2.provider;
6411
+ if (target === CLAUDE_PLAN.id) return claudeAuth("logout", io.env);
6214
6412
  const removed = target === "chatgpt" ? [] : await removeApiKeys(auth, input2, target);
6215
6413
  if (!target || target === "chatgpt") {
6216
6414
  const revoked = await signOutChatGPT(auth, fetchOr(io.fetch));
@@ -6221,7 +6419,9 @@ async function logout({ auth, io, input: input2 }, rest) {
6221
6419
  return 0;
6222
6420
  }
6223
6421
  async function models({ io, input: input2 }) {
6224
- const choice = await selectProvider(input2);
6422
+ const active = await activeProvider(input2);
6423
+ if (isClaude(active)) return claudeModels(io, active);
6424
+ const choice = active;
6225
6425
  if (!choice.ready) throw missingKey(choice);
6226
6426
  const list2 = await choice.runtime.catalog();
6227
6427
  io.stderr.write(`${choice.runtime.label}
@@ -6234,6 +6434,23 @@ async function models({ io, input: input2 }) {
6234
6434
  }
6235
6435
  return 0;
6236
6436
  }
6437
+ async function claudeModels(io, choice) {
6438
+ const connection = await openClaude(io.claudeAgent ?? claudeAgent(io.env), io.cwd);
6439
+ try {
6440
+ const state = await connection.newSession({ cwd: io.cwd });
6441
+ const option = state.configOptions?.find((item) => item.id === "model");
6442
+ const list2 = Array.isArray(option?.options) ? option.options : [];
6443
+ const current = choice.model ?? option?.currentValue;
6444
+ io.stderr.write(`${CLAUDE_PLAN.inUse}
6445
+ `);
6446
+ const width = Math.max(0, ...list2.map((model) => model.value.length));
6447
+ for (const model of list2) io.stdout.write(`${model.value === current ? "*" : " "} ${model.value.padEnd(width)} ${model.name}
6448
+ `);
6449
+ return 0;
6450
+ } finally {
6451
+ await connection.close();
6452
+ }
6453
+ }
6237
6454
  const OLLAMA = "http://127.0.0.1:11434";
6238
6455
  const input = (deps) => ({ settings: deps.config.settings, env: deps.env, auth: deps.auth, fetch: deps.fetch });
6239
6456
  const has = (spec, method) => spec.methods.some((item) => item.id === method);
@@ -6249,7 +6466,7 @@ function baseOf(spec, deps) {
6249
6466
  async function status(spec, deps, signedIn) {
6250
6467
  const parts = [];
6251
6468
  const current = deps.provider();
6252
- if (current.id === spec.id) parts.push(current.runtime.method === "chatgpt" ? "in use with your ChatGPT plan" : "in use");
6469
+ if (current.id === spec.id) parts.push(!isClaude(current) && current.runtime.method === "chatgpt" ? "in use with your ChatGPT plan" : "in use");
6253
6470
  if (spec.id === "openai" && signedIn) parts.push("signed in with ChatGPT");
6254
6471
  const base = baseOf(spec, deps);
6255
6472
  let saved = false;
@@ -6273,13 +6490,20 @@ async function apply(deps, id2, message2) {
6273
6490
  }
6274
6491
  async function use(deps, spec) {
6275
6492
  const { config } = deps;
6276
- await selectProvider({ ...input(deps), provider: spec.id });
6493
+ if (spec.id !== CLAUDE_PLAN.id) await selectProvider({ ...input(deps), provider: spec.id });
6277
6494
  const previous = config.settings.provider ?? deps.provider().id;
6278
6495
  const providers = { ...config.settings.providers };
6279
6496
  if (config.settings.model && !providers[previous]?.model) providers[previous] = { ...providers[previous], model: config.settings.model };
6280
6497
  await config.saveSettings({ provider: spec.id, model: void 0, providers });
6281
6498
  return apply(deps, spec.id, `Using ${spec.name}`);
6282
6499
  }
6500
+ async function claudeRow(deps) {
6501
+ const inUse = deps.provider().id === CLAUDE_PLAN.id;
6502
+ const status2 = await claudeStatus(deps.env);
6503
+ const steps = status2.installed && !status2.loggedIn ? "run `oxbit-agent login claude` in a terminal" : void 0;
6504
+ const detail = [inUse && "in use", planText(status2), steps, "Claude Agent with Claude Code's own sign-in"].filter(Boolean).join(", ");
6505
+ return { id: CLAUDE_PLAN.id, section: "Providers", label: CLAUDE_PLAN.name, detail, actions: inUse ? [] : [{ id: "use", label: "Use this provider", reloads: true }] };
6506
+ }
6283
6507
  async function signIn(deps, context) {
6284
6508
  const signal = AbortSignal.any([context.signal, AbortSignal.timeout(SIGN_IN_TIMEOUT_MS)]);
6285
6509
  const { account, enabled } = await signInChatGPT({ config: deps.config, auth: deps.auth, fetch: fetchOr(deps.fetch), signal, open: async (url) => {
@@ -6309,6 +6533,7 @@ const providersPanel = {
6309
6533
  });
6310
6534
  return { id: spec.id, section: "Providers", label: spec.name, detail: text, enabled: !disabled.includes(spec.id), actions };
6311
6535
  }));
6536
+ rows.push(await claudeRow(deps));
6312
6537
  return { id: "providers", title: "Providers", rows, footer: "Unchecked providers are skipped when settings name no provider; --provider and Use still pick them." };
6313
6538
  },
6314
6539
  async toggle({ config }, rowId, enabled) {
@@ -6318,6 +6543,7 @@ const providersPanel = {
6318
6543
  return { message: `${spec.name} is ${enabled ? "on" : "off"} for automatic selection` };
6319
6544
  },
6320
6545
  async act(deps, rowId, actionId, inputs, context) {
6546
+ if (rowId === CLAUDE_PLAN.id && actionId === "use") return use(deps, CLAUDE_PLAN);
6321
6547
  const spec = providerSpec(rowId);
6322
6548
  if (!spec) throw unknownRow(rowId);
6323
6549
  const inUse = deps.provider().id === spec.id;
@@ -6751,7 +6977,7 @@ async function runPrint(options) {
6751
6977
  const note = (text) => stderr.write(`${text}
6752
6978
  `);
6753
6979
  try {
6754
- const state = options.sessionId ? await connection.loadSession(options.sessionId) : await connection.newSession(options.cwd ? { cwd: options.cwd } : {});
6980
+ const state = options.sessionId ? await connection.loadSession(options.sessionId, options.cwd ? { cwd: options.cwd } : {}) : await connection.newSession(options.cwd ? { cwd: options.cwd } : {});
6755
6981
  const { sessionId } = state;
6756
6982
  if (options.mode) await connection.setMode(sessionId, options.mode);
6757
6983
  if (options.model && options.model !== state.models?.currentModelId) await connection.setModel(sessionId, options.model);
@@ -6802,8 +7028,9 @@ const USAGE = `Usage: oxbit-agent [options] [prompt] Terminal UI
6802
7028
  oxbit-agent models List the provider's models
6803
7029
  oxbit-agent login List the sign-in methods
6804
7030
  oxbit-agent login chatgpt Continue with ChatGPT to use your ChatGPT plan
7031
+ oxbit-agent login claude Sign in to Claude Code to use your Claude plan
6805
7032
  oxbit-agent login --provider <id> Save an API key read from stdin
6806
- oxbit-agent logout [provider|chatgpt] Remove saved keys, or sign out of ChatGPT
7033
+ oxbit-agent logout [provider|chatgpt|claude] Remove saved keys, or sign out of ChatGPT or Claude Code
6807
7034
 
6808
7035
  Options:
6809
7036
  -p, --print <prompt> Run one prompt; "-" reads it from stdin
@@ -6869,49 +7096,69 @@ ${USAGE}`);
6869
7096
  case "logout":
6870
7097
  return await logout(commandContext, rest);
6871
7098
  }
6872
- let provider2 = await selectProvider(input2);
7099
+ let provider2 = await activeProvider(input2);
6873
7100
  const deps = { config, auth, env: io.env, fetch: io.fetch, open: io.open, provider: () => provider2 };
6874
7101
  const settings = createSettingsManager(deps);
6875
- const setup = (generateTitles, quiet = false) => ({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: provider2, settings });
7102
+ const claude = () => io.claudeAgent ?? claudeAgent(io.env);
7103
+ const startClaude = () => {
7104
+ io.stderr.write("Starting Claude Agent with npx; the first run downloads it.\n");
7105
+ return openClaude(claude(), io.cwd);
7106
+ };
7107
+ const open2 = (choice, generateTitles, quiet = false) => isClaude(choice) ? startClaude() : MemoryConnection.open(agentFactory({ root: io.cwd, config, generateTitles, log: quiet ? void 0 : log, provider: choice, settings }));
7108
+ const mode = values.mode ?? config.settings.mode;
7109
+ const session = (connection) => isClaude(provider2) ? claudeResume(connection, values, io.cwd) : resumeId(config, values, io.cwd);
6876
7110
  if (command === "acp") {
7111
+ if (isClaude(provider2)) return await relay(claude(), io);
6877
7112
  const rejected = (error) => log(`Unhandled rejection: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
6878
7113
  process.on("unhandledRejection", rejected);
6879
7114
  try {
6880
- await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory(setup(true)), log });
7115
+ await serveStdio({ input: io.stdin, output: io.stdout, factory: agentFactory({ root: io.cwd, config, generateTitles: true, log, provider: provider2, settings }), log });
6881
7116
  } finally {
6882
7117
  process.off("unhandledRejection", rejected);
6883
7118
  }
6884
7119
  return 0;
6885
7120
  }
6886
- const sessionId = await resumeId(config, values, io.cwd);
6887
- const mode = values.mode ?? config.settings.mode;
6888
7121
  if (values.print !== void 0) {
6889
7122
  if (positionals.length) throw new UsageError(`Unexpected argument ${positionals[0]}`);
6890
7123
  const prompt = values.print === "-" ? (await readAll(io.stdin)).trim() : values.print;
6891
7124
  if (!prompt) throw new UsageError("The prompt is empty");
6892
- const connection = await MemoryConnection.open(agentFactory(setup(false)));
7125
+ const connection = await open2(provider2, false);
6893
7126
  const abort = new AbortController();
6894
7127
  const interrupt = () => abort.abort();
6895
7128
  process.once("SIGINT", interrupt);
6896
7129
  try {
6897
- return await runPrint({ connection, prompt, sessionId, mode, model: values.model, stdout: io.stdout, stderr: io.stderr, signal: abort.signal });
7130
+ const sessionId = await session(connection);
7131
+ return await runPrint({
7132
+ connection,
7133
+ prompt,
7134
+ sessionId,
7135
+ cwd: io.cwd,
7136
+ mode: mode && isClaude(provider2) ? CLAUDE_MODES[mode] : mode,
7137
+ model: isClaude(provider2) ? provider2.model : values.model,
7138
+ stdout: io.stdout,
7139
+ stderr: io.stderr,
7140
+ signal: abort.signal
7141
+ });
6898
7142
  } finally {
6899
7143
  process.off("SIGINT", interrupt);
6900
7144
  await connection.close();
6901
7145
  }
6902
7146
  }
6903
7147
  if (!io.stdout.isTTY) throw new UsageError("stdout is not a terminal. Use -p <prompt> for one reply or `oxbit-agent acp` for ACP");
6904
- const [tui, memory] = await Promise.all([import("./assets/index-DU-a4mnM.js"), import("./assets/memory-Bdy8hvTl.js")]);
6905
- const label = () => ({ provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" });
7148
+ const [tui, memory] = await Promise.all([import("./assets/index-Crr8fCGQ.js"), import("./assets/memory-C3ixKimB.js")]);
7149
+ const label = () => isClaude(provider2) ? { provider: CLAUDE_PLAN.inUse } : { provider: provider2.runtime.label, usingChatGPTPlan: provider2.runtime.method === "chatgpt" };
7150
+ const claudeOptions = () => isClaude(provider2) ? { modes: CLAUDE_MODES, model: provider2.model } : {};
6906
7151
  const panels = createSettingsManager({ ...deps, rebuild: async (id2) => {
6907
7152
  await config.reload();
6908
- const next = await selectProvider({ ...input2, settings: config.settings, provider: id2, model: id2 === provider2.id ? provider2.model : void 0 });
6909
- const connection = await MemoryConnection.open(agentFactory({ ...setup(true, true), provider: next }));
7153
+ const next = await activeProvider({ ...input2, settings: config.settings, provider: id2, model: id2 === provider2.id ? provider2.model : void 0 });
7154
+ const connection = await open2(next, true, true);
6910
7155
  provider2 = next;
6911
- await adapter.replace(connection, label());
7156
+ await adapter.replace(connection, label(), claudeOptions());
6912
7157
  } });
6913
- const adapter = memory.memoryTuiConnection(await MemoryConnection.open(agentFactory(setup(true, true))), { ...label(), mode, settings: panels });
7158
+ const first = await open2(provider2, true, true);
7159
+ const adapter = memory.memoryTuiConnection(first, { ...label(), ...claudeOptions(), mode, settings: panels });
6914
7160
  try {
7161
+ const sessionId = await session(first);
6915
7162
  const { sessionId: last } = await tui.run(adapter, {
6916
7163
  cwd: io.cwd,
6917
7164
  sessionId,
@@ -6932,6 +7179,25 @@ ${USAGE}`);
6932
7179
  return error instanceof UsageError ? 2 : 1;
6933
7180
  }
6934
7181
  }
7182
+ async function claudeResume(connection, values, cwd) {
7183
+ if (values.continue && values.resume) throw new UsageError("Use --continue or --resume, not both");
7184
+ if (!values.continue) return values.resume;
7185
+ const [latest] = (await connection.listSessions(cwd)).sessions.filter((item) => item.cwd === cwd).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
7186
+ if (!latest) throw new Error(`No saved session for ${cwd}`);
7187
+ return latest.sessionId;
7188
+ }
7189
+ function relay(agent, io) {
7190
+ return new Promise((resolve2, reject) => {
7191
+ const child = spawn(agent.command, agent.args, { cwd: io.cwd, env: agent.env, stdio: ["pipe", "pipe", "pipe"] });
7192
+ child.once("error", (error) => reject(new Error(`Could not start ${agent.command}: ${error.message}`)));
7193
+ child.stdin.on("error", () => {
7194
+ });
7195
+ io.stdin.pipe(child.stdin);
7196
+ child.stdout.on("data", (chunk) => io.stdout.write(chunk));
7197
+ child.stderr.on("data", (chunk) => io.stderr.write(chunk));
7198
+ child.once("close", (code) => resolve2(code ?? 1));
7199
+ });
7200
+ }
6935
7201
  async function resumeId(config, values, cwd) {
6936
7202
  if (values.continue && values.resume) throw new UsageError("Use --continue or --resume, not both");
6937
7203
  const store = new JsonlSessionStore(config.paths.sessions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxbit/cli",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "The oxbit command: runs the Oxbit runtime and opens the editor in a browser",
5
5
  "type": "module",
6
6
  "bin": {