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.
- package/README.md +70 -2
- package/dist/agentconfig/load.js +9 -1
- package/dist/agentconfig/render.js +8 -5
- package/dist/agentconfig/schema.js +6 -0
- package/dist/auth/accountManager.js +118 -0
- package/dist/auth/accounts.js +161 -0
- package/dist/auth/copilotToken.js +92 -23
- package/dist/auth/credentials.js +216 -40
- package/dist/auth/deviceFlow.js +110 -23
- package/dist/auth/githubIdentity.js +14 -10
- package/dist/cli/agentEnv.js +15 -9
- package/dist/cli/auth/runAuth.js +206 -9
- package/dist/cli/commands/agents/claude.js +22 -2
- package/dist/cli/commands/agents/codex.js +22 -2
- package/dist/cli/commands/agents/copilot.js +25 -4
- package/dist/cli/commands/agents/pi.js +22 -2
- package/dist/cli/commands/agents/shared.js +57 -0
- package/dist/cli/commands/auth.js +58 -7
- package/dist/cli/commands/daemon.js +79 -17
- package/dist/cli/commands/models.js +0 -5
- package/dist/cli/copillmFlags.js +8 -0
- package/dist/cli/daemon/lifecycle.js +26 -0
- package/dist/cli/daemon/probes.js +99 -33
- package/dist/cli/daemon/runDaemon.js +21 -2
- package/dist/cli/index.js +12 -0
- package/dist/cli/integrations/claudeExport.js +6 -4
- package/dist/cli/integrations/refreshCodex.js +5 -2
- package/dist/cli/integrations/refreshPi.js +5 -2
- package/dist/cli/packageInfo.js +1 -1
- package/dist/cli/shared/devMode.js +98 -0
- package/dist/config/accountId.js +44 -0
- package/dist/config/config.js +13 -2
- package/dist/config/home.js +69 -0
- package/dist/integrations/claude/cache.js +5 -2
- package/dist/integrations/claude/settingsConflict.js +5 -2
- package/dist/integrations/codex/init.js +31 -10
- package/dist/integrations/pi/init.js +8 -17
- package/dist/models/anthropicDefaults.js +13 -4
- package/dist/models/discovery.js +141 -15
- package/dist/server/accountResolver.js +85 -0
- package/dist/server/debugInfo.js +69 -24
- package/dist/server/errors.js +18 -0
- package/dist/server/proxy.js +40 -8
- package/dist/server/routes/debug.js +11 -1
- package/dist/server/routes/models.js +12 -6
- package/dist/server/routes/proxyForward.js +3 -3
- package/dist/server/routes/shared.js +66 -21
- package/dist/server/upstream/copilotClient.js +1 -30
- package/dist/server/upstream/retryPolicy.js +99 -0
- 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
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/config/home.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
2
|
+
import { claudeConfigDir } from "../../config/home.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
export function claudeSettingsPath() {
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
73
|
+
/** Absolute path to pi's `models.json`, under the copillm-owned pi agent dir. */
|
|
83
74
|
export function piModelsJsonPath() {
|
|
84
|
-
return path.join(
|
|
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
|
-
|
|
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) {
|