copillm 0.2.8 → 0.3.0-beta.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.
Files changed (50) hide show
  1. package/README.md +70 -2
  2. package/dist/agentconfig/load.js +9 -1
  3. package/dist/agentconfig/render.js +8 -5
  4. package/dist/agentconfig/schema.js +6 -0
  5. package/dist/auth/accountManager.js +118 -0
  6. package/dist/auth/accounts.js +161 -0
  7. package/dist/auth/copilotToken.js +92 -23
  8. package/dist/auth/credentials.js +216 -40
  9. package/dist/auth/deviceFlow.js +110 -23
  10. package/dist/auth/githubIdentity.js +14 -10
  11. package/dist/cli/agentEnv.js +15 -9
  12. package/dist/cli/auth/runAuth.js +206 -9
  13. package/dist/cli/commands/agents/claude.js +22 -2
  14. package/dist/cli/commands/agents/codex.js +22 -2
  15. package/dist/cli/commands/agents/copilot.js +25 -4
  16. package/dist/cli/commands/agents/pi.js +22 -2
  17. package/dist/cli/commands/agents/shared.js +57 -0
  18. package/dist/cli/commands/auth.js +58 -7
  19. package/dist/cli/commands/daemon.js +79 -17
  20. package/dist/cli/commands/models.js +0 -5
  21. package/dist/cli/copillmFlags.js +8 -0
  22. package/dist/cli/daemon/lifecycle.js +26 -0
  23. package/dist/cli/daemon/probes.js +99 -33
  24. package/dist/cli/daemon/runDaemon.js +21 -2
  25. package/dist/cli/index.js +12 -0
  26. package/dist/cli/integrations/claudeExport.js +6 -4
  27. package/dist/cli/integrations/refreshCodex.js +5 -2
  28. package/dist/cli/integrations/refreshPi.js +5 -2
  29. package/dist/cli/packageInfo.js +1 -1
  30. package/dist/cli/shared/devMode.js +98 -0
  31. package/dist/config/accountId.js +44 -0
  32. package/dist/config/config.js +13 -2
  33. package/dist/config/home.js +69 -0
  34. package/dist/integrations/claude/cache.js +5 -2
  35. package/dist/integrations/claude/settingsConflict.js +5 -2
  36. package/dist/integrations/codex/init.js +31 -10
  37. package/dist/integrations/pi/init.js +8 -17
  38. package/dist/models/anthropicDefaults.js +13 -4
  39. package/dist/models/discovery.js +141 -15
  40. package/dist/server/accountResolver.js +85 -0
  41. package/dist/server/debugInfo.js +69 -24
  42. package/dist/server/errors.js +18 -0
  43. package/dist/server/proxy.js +40 -8
  44. package/dist/server/routes/debug.js +11 -1
  45. package/dist/server/routes/models.js +12 -6
  46. package/dist/server/routes/proxyForward.js +3 -3
  47. package/dist/server/routes/shared.js +66 -21
  48. package/dist/server/upstream/copilotClient.js +1 -30
  49. package/dist/server/upstream/retryPolicy.js +99 -0
  50. package/package.json +4 -1
@@ -0,0 +1,98 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ /**
4
+ * Dev-mode isolation.
5
+ *
6
+ * Running a locally-built copillm against the SAME `~/.copillm` home and port as
7
+ * a globally-installed production daemon is a footgun: `stop` reads
8
+ * `~/.copillm/copillm.pid` and would kill the production daemon, and `start`
9
+ * sees the production lock and reports "already running" instead of launching
10
+ * your dev code.
11
+ *
12
+ * Dev mode redirects the runtime onto a separate `COPILLM_HOME` (and a distinct
13
+ * default port) so a dev daemon and a production daemon can run side by side
14
+ * without ever touching each other's lock, config, model cache, or port. This
15
+ * is the mechanism that lets you develop copillm WHILE using copillm.
16
+ *
17
+ * The override is implemented by setting the same `COPILLM_HOME` / `COPILLM_PORT`
18
+ * env vars the rest of the codebase already reads (see `src/config/home.ts` and
19
+ * `src/config/config.ts`). Because detached daemons and spawned agents inherit
20
+ * `process.env`, the isolation propagates to every child process for free.
21
+ *
22
+ * Activated by the global `--dev` flag or by exporting `COPILLM_DEV=1`. The
23
+ * concrete locations are overridable via `COPILLM_DEV_HOME` / `COPILLM_DEV_PORT`.
24
+ * An explicitly-set `COPILLM_HOME` / `COPILLM_PORT` always wins — dev mode never
25
+ * clobbers a home or port the user pinned on purpose.
26
+ */
27
+ export const DEV_HOME_DIRNAME = ".copillm-dev";
28
+ export const DEFAULT_DEV_PORT = 4142;
29
+ // Set once dev mode has been applied for this process, so command surfaces
30
+ // (start banner, status) can annotate output without re-deriving intent.
31
+ let devModeActive = false;
32
+ /** Whether dev mode has been applied to this process. */
33
+ export function isDevModeActive() {
34
+ return devModeActive;
35
+ }
36
+ function isTruthyEnv(value) {
37
+ if (value === undefined) {
38
+ return false;
39
+ }
40
+ return /^(1|true|yes|on)$/i.test(value.trim());
41
+ }
42
+ function nonEmptyEnv(value) {
43
+ if (value === undefined) {
44
+ return null;
45
+ }
46
+ const trimmed = value.trim();
47
+ return trimmed.length > 0 ? trimmed : null;
48
+ }
49
+ /**
50
+ * Whether dev mode was requested, via the `--dev` flag (passed in) or the
51
+ * `COPILLM_DEV` env var.
52
+ */
53
+ export function isDevModeRequested(flag) {
54
+ return Boolean(flag) || isTruthyEnv(process.env.COPILLM_DEV);
55
+ }
56
+ /** The isolated dev home: `COPILLM_DEV_HOME` if set, else `~/.copillm-dev`. */
57
+ export function resolveDevHome() {
58
+ const override = nonEmptyEnv(process.env.COPILLM_DEV_HOME);
59
+ if (override) {
60
+ return path.resolve(override);
61
+ }
62
+ return path.join(os.homedir(), DEV_HOME_DIRNAME);
63
+ }
64
+ /** The isolated dev port: `COPILLM_DEV_PORT` if set, else `4142`. */
65
+ export function resolveDevPort() {
66
+ const override = nonEmptyEnv(process.env.COPILLM_DEV_PORT);
67
+ if (override) {
68
+ return override;
69
+ }
70
+ return String(DEFAULT_DEV_PORT);
71
+ }
72
+ /**
73
+ * Apply dev-mode isolation to `process.env` when requested. Idempotent and
74
+ * safe to call multiple times.
75
+ *
76
+ * - No-op when dev mode is not requested.
77
+ * - Sets `COPILLM_HOME` to the dev home ONLY when it is not already set, so an
78
+ * explicit `COPILLM_HOME` always wins.
79
+ * - Sets `COPILLM_PORT` to the dev port ONLY when it is not already set, so an
80
+ * explicit `COPILLM_PORT` always wins.
81
+ */
82
+ export function applyDevModeEnv(flag) {
83
+ if (!isDevModeRequested(flag)) {
84
+ return { active: false, home: null, port: null };
85
+ }
86
+ if (!nonEmptyEnv(process.env.COPILLM_HOME)) {
87
+ process.env.COPILLM_HOME = resolveDevHome();
88
+ }
89
+ if (!nonEmptyEnv(process.env.COPILLM_PORT)) {
90
+ process.env.COPILLM_PORT = resolveDevPort();
91
+ }
92
+ devModeActive = true;
93
+ return {
94
+ active: true,
95
+ home: process.env.COPILLM_HOME ?? null,
96
+ port: process.env.COPILLM_PORT ?? null
97
+ };
98
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Account-id validation, shared by the `auth` layer (which writes the accounts
3
+ * index) and the `models` layer (which embeds the id in a per-account model
4
+ * cache filename). Lives in `config` so both layers can import it without
5
+ * crossing a forbidden import boundary.
6
+ *
7
+ * An account id is embedded in filenames (`credentials.<id>.json`,
8
+ * `models.cache.<id>.json`) and in a keychain account string, so it must be a
9
+ * safe single path segment. GitHub logins are `[A-Za-z0-9-]`; copillm also
10
+ * permits `.` and `_` for synthetic ids.
11
+ */
12
+ export const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
13
+ export const MAX_ACCOUNT_ID_LENGTH = 64;
14
+ export class InvalidAccountIdError extends Error {
15
+ accountId;
16
+ constructor(accountId, reason) {
17
+ super(`Invalid account id "${accountId}": ${reason}`);
18
+ this.accountId = accountId;
19
+ this.name = "InvalidAccountIdError";
20
+ }
21
+ }
22
+ /**
23
+ * Validate an account id before it is used in a filename or keychain key.
24
+ * Throws `InvalidAccountIdError` on anything that isn't a safe path segment.
25
+ */
26
+ export function assertValidAccountId(accountId) {
27
+ if (accountId.length === 0) {
28
+ throw new InvalidAccountIdError(accountId, "id must not be empty.");
29
+ }
30
+ if (accountId.length > MAX_ACCOUNT_ID_LENGTH) {
31
+ throw new InvalidAccountIdError(accountId, `id must be at most ${MAX_ACCOUNT_ID_LENGTH} characters.`);
32
+ }
33
+ if (!ACCOUNT_ID_PATTERN.test(accountId)) {
34
+ throw new InvalidAccountIdError(accountId, "id may only contain letters, digits, '.', '_' and '-', and must start with a letter or digit.");
35
+ }
36
+ }
37
+ /**
38
+ * Non-throwing variant for hot paths (e.g. parsing an optional account prefix
39
+ * out of a request URL) where an invalid id should just mean "not an account
40
+ * prefix" rather than an error.
41
+ */
42
+ export function isValidAccountId(accountId) {
43
+ return accountId.length > 0 && accountId.length <= MAX_ACCOUNT_ID_LENGTH && ACCOUNT_ID_PATTERN.test(accountId);
44
+ }
@@ -23,7 +23,7 @@ export function loadConfig() {
23
23
  const file = configReadPath();
24
24
  if (!fs.existsSync(file)) {
25
25
  saveConfig(DEFAULT_CONFIG);
26
- return DEFAULT_CONFIG;
26
+ return applyEnvOverrides(DEFAULT_CONFIG);
27
27
  }
28
28
  const raw = readFileSync(file, "utf8");
29
29
  let parsed;
@@ -33,7 +33,7 @@ export function loadConfig() {
33
33
  catch (error) {
34
34
  throw new Error(`Invalid YAML in config file: ${file}`, { cause: error });
35
35
  }
36
- return parseConfigValue(parsed, file);
36
+ return applyEnvOverrides(parseConfigValue(parsed, file));
37
37
  }
38
38
  export function saveConfig(config) {
39
39
  ensureAppHome();
@@ -49,3 +49,14 @@ function parseConfigValue(value, source) {
49
49
  const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
50
50
  throw new Error(`Invalid config schema in ${source}: ${issues}`);
51
51
  }
52
+ function applyEnvOverrides(config) {
53
+ const port = process.env.COPILLM_PORT;
54
+ if (port === undefined || port.trim().length === 0) {
55
+ return config;
56
+ }
57
+ const parsedPort = Number(port.trim());
58
+ if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
59
+ throw new Error("Invalid COPILLM_PORT: expected an integer between 1 and 65535.");
60
+ }
61
+ return { ...config, preferredPort: parsedPort };
62
+ }
@@ -20,6 +20,29 @@ export function credentialsPath() {
20
20
  export function credentialsReadPath() {
21
21
  return resolveReadablePath("credentials.json");
22
22
  }
23
+ /**
24
+ * Path to the multi-account index (`accounts.json`). Metadata only — never
25
+ * holds a token. Absent on single-account installs, which keep using the
26
+ * legacy `credentials.json` / keychain entry as the implicit default account.
27
+ */
28
+ export function accountsIndexPath() {
29
+ return path.join(getCopillmHome(), "accounts.json");
30
+ }
31
+ export function accountsIndexReadPath() {
32
+ return resolveReadablePath("accounts.json");
33
+ }
34
+ /**
35
+ * Plaintext-fallback credential file for a *named* (non-default) account. The
36
+ * default account keeps the legacy `credentials.json` path for backward
37
+ * compatibility; additional accounts are namespaced by id so their tokens
38
+ * never collide with — or overwrite — the pre-existing default.
39
+ */
40
+ export function accountCredentialsPath(accountId) {
41
+ return path.join(getCopillmHome(), `credentials.${accountId}.json`);
42
+ }
43
+ export function accountCredentialsReadPath(accountId) {
44
+ return resolveReadablePath(`credentials.${accountId}.json`);
45
+ }
23
46
  export function lockPath() {
24
47
  return path.join(getCopillmHome(), "copillm.pid");
25
48
  }
@@ -32,9 +55,55 @@ export function modelsCachePath() {
32
55
  export function modelsCacheReadPath() {
33
56
  return resolveReadablePath("models.cache.json");
34
57
  }
58
+ /**
59
+ * Per-account model-discovery cache. Different accounts can be entitled to
60
+ * different model catalogs, so each named account caches into its own
61
+ * `models.cache.<id>.json`. The primary/legacy account keeps the shared
62
+ * `models.cache.json` (above), so single-account installs are unaffected.
63
+ */
64
+ export function accountModelsCachePath(accountId) {
65
+ return path.join(getCopillmHome(), `models.cache.${accountId}.json`);
66
+ }
67
+ export function accountModelsCacheReadPath(accountId) {
68
+ return resolveReadablePath(`models.cache.${accountId}.json`);
69
+ }
35
70
  export function debugLogPath() {
36
71
  return path.join(getCopillmHome(), "debug.log");
37
72
  }
73
+ /**
74
+ * The directory pi (`@earendil-works/pi-coding-agent`) reads its config from.
75
+ *
76
+ * pi exposes this via the `PI_CODING_AGENT_DIR` env var — its own `getAgentDir()`
77
+ * treats the value as the agent dir directly (equivalent to `~/.pi/agent`).
78
+ * copillm owns this path: it defaults to `<COPILLM_HOME>/pi/agent` so copillm
79
+ * never writes into the user's real `~/.pi`, and dev mode relocates it for free
80
+ * via COPILLM_HOME. An explicitly-set `PI_CODING_AGENT_DIR` always wins.
81
+ */
82
+ export function piAgentDir() {
83
+ const overridden = process.env.PI_CODING_AGENT_DIR;
84
+ if (overridden && overridden.trim().length > 0) {
85
+ return path.resolve(overridden.trim());
86
+ }
87
+ return path.join(getCopillmHome(), "pi", "agent");
88
+ }
89
+ /**
90
+ * The config home Claude Code reads (its `~/.claude` equivalent), exposed by
91
+ * Claude Code as the `CLAUDE_CONFIG_DIR` env var.
92
+ *
93
+ * copillm owns this path: it defaults to `<COPILLM_HOME>/claude/home` and copillm
94
+ * exports `CLAUDE_CONFIG_DIR` to it when launching Claude (see
95
+ * `buildClaudeEnvBundle`). This keeps copillm out of the user's real `~/.claude`
96
+ * — copillm-launched Claude gets a deterministic, copillm-owned config home, and
97
+ * dev mode relocates it for free via COPILLM_HOME. An explicitly-set
98
+ * `CLAUDE_CONFIG_DIR` always wins.
99
+ */
100
+ export function claudeConfigDir() {
101
+ const overridden = process.env.CLAUDE_CONFIG_DIR;
102
+ if (overridden && overridden.trim().length > 0) {
103
+ return path.resolve(overridden.trim());
104
+ }
105
+ return path.join(getCopillmHome(), "claude", "home");
106
+ }
38
107
  function resolveReadablePath(fileName) {
39
108
  const canonical = path.join(getCopillmHome(), fileName);
40
109
  if (fs.existsSync(canonical)) {
@@ -1,8 +1,11 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
3
+ import { claudeConfigDir } from "../../config/home.js";
4
4
  export function claudeGatewayCachePath() {
5
- return path.join(os.homedir(), ".claude", "cache", "gateway-models.json");
5
+ // Claude stores the gateway model-picker cache under its config home
6
+ // (CLAUDE_CONFIG_DIR). copillm owns that home, so we clear the copillm-owned
7
+ // copy — never the user's real ~/.claude.
8
+ return path.join(claudeConfigDir(), "cache", "gateway-models.json");
6
9
  }
7
10
  export function clearClaudeGatewayCache() {
8
11
  const target = claudeGatewayCachePath();
@@ -1,8 +1,11 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
2
+ import { claudeConfigDir } from "../../config/home.js";
3
3
  import path from "node:path";
4
4
  export function claudeSettingsPath() {
5
- return path.join(os.homedir(), ".claude", "settings.json");
5
+ // copillm-launched Claude reads settings from its copillm-owned config home
6
+ // (CLAUDE_CONFIG_DIR), so the conflict check inspects that file — not the
7
+ // user's real ~/.claude/settings.json.
8
+ return path.join(claudeConfigDir(), "settings.json");
6
9
  }
7
10
  export function detectClaudeSettingsConflicts(launcherEnv, settingsPathOverride) {
8
11
  const settingsPath = settingsPathOverride ?? claudeSettingsPath();
@@ -1,6 +1,5 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { CopilotTokenManager } from "../../auth/copilotToken.js";
4
3
  import { loadStoredCredential } from "../../auth/credentials.js";
5
4
  import { loadConfig } from "../../config/config.js";
6
5
  import { listModelsUnion } from "../../models/discovery.js";
@@ -8,20 +7,14 @@ import { ensureSecureDirectory, writeFileSecureAtomic } from "../../config/fsSec
8
7
  import { buildCodexCatalog } from "../../server/codexSchema.js";
9
8
  import { inspectLock } from "../../server/lock.js";
10
9
  export async function generateCodexHome(options) {
11
- const config = loadConfig();
12
- const creds = await loadStoredCredential();
13
- if (!creds) {
14
- throw new Error("Not authenticated. Run `copillm login` first.");
15
- }
16
- const tokenManager = new CopilotTokenManager(creds.token);
17
- await tokenManager.ensureToken(false);
18
- const discovery = await listModelsUnion(config.accountType, creds.token, 3);
10
+ const { discovery } = await resolveStartContext(options.precomputed, options.account);
19
11
  const catalog = buildCodexCatalog(discovery.models);
20
12
  if (catalog.models.length === 0) {
21
13
  throw new Error("No Codex-eligible models found in the live catalog.");
22
14
  }
23
15
  const port = options.port;
24
- const proxyUrl = `http://127.0.0.1:${port}/codex/v1`;
16
+ const prefix = options.pathPrefix ?? "";
17
+ const proxyUrl = `http://127.0.0.1:${port}${prefix}/codex/v1`;
25
18
  const baseUrl = proxyUrl;
26
19
  const defaultModel = options.model ?? pickDefaultModel(catalog.models.map((model) => model.slug));
27
20
  if (!catalog.models.some((model) => model.slug === defaultModel)) {
@@ -47,6 +40,34 @@ export async function generateCodexHome(options) {
47
40
  exportCommand: `CODEX_HOME=${absOutDir} codex`
48
41
  };
49
42
  }
43
+ /**
44
+ * Load credentials / config / model discovery once. When the caller has
45
+ * already done this work (e.g. `copillm start` orchestrating multiple init
46
+ * steps), it can pass them in via `precomputed` to skip the work.
47
+ *
48
+ * Exported so `generatePiHome` (and any future agent init) can use the
49
+ * same loader and the same "if precomputed, reuse it" contract.
50
+ */
51
+ export async function resolveStartContext(precomputed, account) {
52
+ if (precomputed) {
53
+ return precomputed;
54
+ }
55
+ const config = loadConfig();
56
+ if (account) {
57
+ const discovery = await listModelsUnion(account.accountType, account.githubToken, 3, undefined, account.cacheId);
58
+ return {
59
+ config,
60
+ creds: { token: account.githubToken, accountType: account.accountType, source: "session" },
61
+ discovery
62
+ };
63
+ }
64
+ const creds = await loadStoredCredential();
65
+ if (!creds) {
66
+ throw new Error("Not authenticated. Run `copillm login` first.");
67
+ }
68
+ const discovery = await listModelsUnion(config.accountType, creds.token, 3);
69
+ return { config, creds, discovery };
70
+ }
50
71
  function pickDefaultModel(slugs) {
51
72
  const preferred = ["gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.4", "gpt-5.2", "claude-opus-4.5", "claude-sonnet-4.6"];
52
73
  for (const candidate of preferred) {
@@ -1,20 +1,10 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
- import { CopilotTokenManager } from "../../auth/copilotToken.js";
5
- import { loadStoredCredential } from "../../auth/credentials.js";
6
- import { loadConfig } from "../../config/config.js";
7
- import { listModelsUnion } from "../../models/discovery.js";
8
3
  import { ensureSecureDirectory, writeFileSecureAtomic } from "../../config/fsSecurity.js";
4
+ import { piAgentDir } from "../../config/home.js";
5
+ import { resolveStartContext } from "../codex/init.js";
9
6
  export async function generatePiHome(options) {
10
- const config = loadConfig();
11
- const creds = await loadStoredCredential();
12
- if (!creds) {
13
- throw new Error("Not authenticated. Run `copillm login` first.");
14
- }
15
- const tokenManager = new CopilotTokenManager(creds.token);
16
- await tokenManager.ensureToken(false);
17
- const discovery = await listModelsUnion(config.accountType, creds.token, 3);
7
+ const { discovery } = await resolveStartContext(options.precomputed, options.account);
18
8
  const eligible = discovery.models.filter(isPickerEligible);
19
9
  // Split the catalog by which upstream endpoint each model supports. Models
20
10
  // that advertise `/chat/completions` flow through copillm's Anthropic surface
@@ -30,9 +20,10 @@ export async function generatePiHome(options) {
30
20
  if (anthropicEligible.length === 0 && responsesEligible.length === 0) {
31
21
  throw new Error("No models discovered for pi config.");
32
22
  }
33
- const proxyUrl = `http://127.0.0.1:${options.port}/anthropic`;
23
+ const prefix = options.pathPrefix ?? "";
24
+ const proxyUrl = `http://127.0.0.1:${options.port}${prefix}/anthropic`;
34
25
  // OpenAI SDK posts to `<baseUrl>/responses`, so the baseUrl must include `/v1`.
35
- const responsesProxyUrl = `http://127.0.0.1:${options.port}/codex/v1`;
26
+ const responsesProxyUrl = `http://127.0.0.1:${options.port}${prefix}/codex/v1`;
36
27
  const providerId = options.providerId.trim().length > 0 ? options.providerId : "copillm";
37
28
  const responsesProviderId = `${providerId}-responses`;
38
29
  const providers = {};
@@ -79,9 +70,9 @@ export async function generatePiHome(options) {
79
70
  export function defaultOutputDir(home) {
80
71
  return path.join(home, "pi");
81
72
  }
82
- /** Absolute path to `~/.pi/agent/models.json`. Honors $HOME for tests. */
73
+ /** Absolute path to pi's `models.json`, under the copillm-owned pi agent dir. */
83
74
  export function piModelsJsonPath() {
84
- return path.join(os.homedir(), ".pi", "agent", "models.json");
75
+ return path.join(piAgentDir(), "models.json");
85
76
  }
86
77
  /**
87
78
  * Eligibility filter shared by both pi providers. Mirrors the gating in
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { z } from "zod";
3
- import { modelsCacheReadPath } from "../config/home.js";
3
+ import { accountModelsCacheReadPath, modelsCacheReadPath } from "../config/home.js";
4
+ import { assertValidAccountId } from "../config/accountId.js";
4
5
  export const ANTHROPIC_FAMILIES = ["opus", "sonnet", "haiku"];
5
6
  const SUFFIX_BLOCKLIST = [
6
7
  "-high",
@@ -32,8 +33,15 @@ export function computeAnthropicDefaults(modelIds) {
32
33
  haiku: pickPlainLatest(byFamily.haiku)
33
34
  };
34
35
  }
35
- export function readModelIdsFromCache() {
36
- const file = modelsCacheReadPath();
36
+ export function readModelIdsFromCache(accountId) {
37
+ let file;
38
+ if (accountId === undefined) {
39
+ file = modelsCacheReadPath();
40
+ }
41
+ else {
42
+ assertValidAccountId(accountId);
43
+ file = accountModelsCacheReadPath(accountId);
44
+ }
37
45
  if (!fs.existsSync(file)) {
38
46
  return [];
39
47
  }
@@ -51,8 +59,9 @@ export function readModelIdsFromCache() {
51
59
  }
52
60
  export function buildClaudeExportCommand(input) {
53
61
  const token = input.callerSecret ?? "copillm-local";
62
+ const prefix = input.pathPrefix ?? "";
54
63
  const parts = [
55
- `ANTHROPIC_BASE_URL=http://127.0.0.1:${input.port}/anthropic`,
64
+ `ANTHROPIC_BASE_URL=http://127.0.0.1:${input.port}${prefix}/anthropic`,
56
65
  `ANTHROPIC_AUTH_TOKEN=${token}`
57
66
  ];
58
67
  if (input.defaults.opus) {