copillm 0.2.9 → 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 +69 -1
- package/dist/agentconfig/load.js +9 -1
- package/dist/agentconfig/schema.js +6 -0
- package/dist/auth/accountManager.js +118 -0
- package/dist/auth/accounts.js +161 -0
- package/dist/auth/credentials.js +196 -67
- package/dist/cli/agentEnv.js +2 -1
- 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 +27 -1
- package/dist/cli/copillmFlags.js +8 -0
- package/dist/cli/daemon/runDaemon.js +21 -2
- package/dist/cli/integrations/claudeExport.js +6 -4
- package/dist/cli/integrations/refreshCodex.js +4 -2
- package/dist/cli/integrations/refreshPi.js +4 -2
- package/dist/cli/packageInfo.js +1 -1
- package/dist/config/accountId.js +44 -0
- package/dist/config/home.js +35 -0
- package/dist/integrations/codex/init.js +12 -3
- package/dist/integrations/pi/init.js +4 -3
- package/dist/models/anthropicDefaults.js +13 -4
- package/dist/models/discovery.js +32 -10
- package/dist/server/accountResolver.js +85 -0
- package/dist/server/proxy.js +40 -8
- package/dist/server/routes/debug.js +7 -0
- package/dist/server/routes/models.js +5 -5
- package/dist/server/routes/proxyForward.js +3 -3
- package/dist/server/routes/shared.js +66 -21
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
|
|
|
5
5
|
import { launchAgent } from "../../launchAgent.js";
|
|
6
6
|
import { buildClaudeExportCommand } from "../../integrations/claudeExport.js";
|
|
7
7
|
import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
|
|
8
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
8
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
9
9
|
export function register(program) {
|
|
10
10
|
program
|
|
11
11
|
.command("claude")
|
|
@@ -16,9 +16,29 @@ export function register(program) {
|
|
|
16
16
|
.action(async (forwardedArgs) => {
|
|
17
17
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
18
18
|
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
19
|
+
let launchAccount;
|
|
20
|
+
try {
|
|
21
|
+
launchAccount = await resolveLaunchAccount({
|
|
22
|
+
flag: opts.copillmAccount,
|
|
23
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (launchAccount) {
|
|
34
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
35
|
+
}
|
|
19
36
|
enableRuntimeDebug(debug);
|
|
20
37
|
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
21
|
-
const claude = buildClaudeExportCommand(lock.port, null
|
|
38
|
+
const claude = buildClaudeExportCommand(lock.port, null, {
|
|
39
|
+
pathPrefix: launchAccount?.pathPrefix,
|
|
40
|
+
cacheId: launchAccount?.cacheId
|
|
41
|
+
});
|
|
22
42
|
const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_CLAUDE_VERSION ?? undefined;
|
|
23
43
|
const conflicts = detectClaudeSettingsConflicts(claude.bundle.env);
|
|
24
44
|
for (const line of formatSettingsConflictWarning(conflicts)) {
|
|
@@ -5,7 +5,7 @@ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
|
|
|
5
5
|
import { launchAgent } from "../../launchAgent.js";
|
|
6
6
|
import { refreshCodexHome } from "../../integrations/refreshCodex.js";
|
|
7
7
|
import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
|
|
8
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
8
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
9
9
|
export function register(program) {
|
|
10
10
|
program
|
|
11
11
|
.command("codex")
|
|
@@ -16,9 +16,29 @@ export function register(program) {
|
|
|
16
16
|
.action(async (forwardedArgs) => {
|
|
17
17
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
18
18
|
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
19
|
+
let launchAccount;
|
|
20
|
+
try {
|
|
21
|
+
launchAccount = await resolveLaunchAccount({
|
|
22
|
+
flag: opts.copillmAccount,
|
|
23
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (launchAccount) {
|
|
34
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
35
|
+
}
|
|
19
36
|
enableRuntimeDebug(debug);
|
|
20
37
|
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
21
|
-
const codex = await refreshCodexHome(lock.port, null
|
|
38
|
+
const codex = await refreshCodexHome(lock.port, null, undefined, {
|
|
39
|
+
pathPrefix: launchAccount?.pathPrefix,
|
|
40
|
+
account: launchAccount?.account
|
|
41
|
+
});
|
|
22
42
|
if (!codex) {
|
|
23
43
|
throw new Error("Failed to prepare Codex home (see warning above).");
|
|
24
44
|
}
|
|
@@ -2,7 +2,7 @@ import { applyAgentConfig, formatApplyNotes } from "../../../agentconfig/apply.j
|
|
|
2
2
|
import { loadStoredCredential } from "../../../auth/credentials.js";
|
|
3
3
|
import { processCopillmArgs } from "../../copillmFlags.js";
|
|
4
4
|
import { launchAgent } from "../../launchAgent.js";
|
|
5
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
5
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
6
6
|
export function register(program) {
|
|
7
7
|
program
|
|
8
8
|
.command("copilot")
|
|
@@ -12,8 +12,29 @@ export function register(program) {
|
|
|
12
12
|
.argument("[args...]", "Args forwarded to copilot")
|
|
13
13
|
.action(async (forwardedArgs) => {
|
|
14
14
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let launchAccount;
|
|
16
|
+
try {
|
|
17
|
+
launchAccount = await resolveLaunchAccount({
|
|
18
|
+
flag: opts.copillmAccount,
|
|
19
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (launchAccount) {
|
|
30
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
31
|
+
}
|
|
32
|
+
// Copilot CLI talks to GitHub directly with the account's OAuth token,
|
|
33
|
+
// so account selection picks which token to inject (not a URL prefix).
|
|
34
|
+
const githubToken = launchAccount
|
|
35
|
+
? launchAccount.account.githubToken
|
|
36
|
+
: (await loadStoredCredential())?.token ?? null;
|
|
37
|
+
if (!githubToken) {
|
|
17
38
|
process.stderr.write("copillm: no stored GitHub credential — run `copillm auth login` first.\n");
|
|
18
39
|
process.exit(1);
|
|
19
40
|
return;
|
|
@@ -34,7 +55,7 @@ export function register(program) {
|
|
|
34
55
|
// short-circuits its device-flow login when copillm already has a token.
|
|
35
56
|
const env = {
|
|
36
57
|
...applyResult.envOverlay,
|
|
37
|
-
COPILOT_GITHUB_TOKEN:
|
|
58
|
+
COPILOT_GITHUB_TOKEN: githubToken
|
|
38
59
|
};
|
|
39
60
|
const baseArgs = [...forwarded, ...applyResult.cliArgs];
|
|
40
61
|
const args = applyYoloForLaunch({ agent: "copilot", flag: opts.yolo, applyResult, baseArgs });
|
|
@@ -5,7 +5,7 @@ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
|
|
|
5
5
|
import { launchAgent } from "../../launchAgent.js";
|
|
6
6
|
import { refreshPiHome } from "../../integrations/refreshPi.js";
|
|
7
7
|
import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
|
|
8
|
-
import { applyYoloForLaunch } from "./shared.js";
|
|
8
|
+
import { applyYoloForLaunch, formatLaunchAccountNotice, resolveLaunchAccount } from "./shared.js";
|
|
9
9
|
export function register(program) {
|
|
10
10
|
program
|
|
11
11
|
.command("pi")
|
|
@@ -16,9 +16,29 @@ export function register(program) {
|
|
|
16
16
|
.action(async (forwardedArgs) => {
|
|
17
17
|
const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
|
|
18
18
|
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
19
|
+
let launchAccount;
|
|
20
|
+
try {
|
|
21
|
+
launchAccount = await resolveLaunchAccount({
|
|
22
|
+
flag: opts.copillmAccount,
|
|
23
|
+
envValue: process.env.COPILLM_ACCOUNT,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.stderr.write(`copillm: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (launchAccount) {
|
|
34
|
+
process.stderr.write(`${formatLaunchAccountNotice(launchAccount)}\n`);
|
|
35
|
+
}
|
|
19
36
|
enableRuntimeDebug(debug);
|
|
20
37
|
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
21
|
-
const pi = await refreshPiHome(lock.port
|
|
38
|
+
const pi = await refreshPiHome(lock.port, undefined, {
|
|
39
|
+
pathPrefix: launchAccount?.pathPrefix,
|
|
40
|
+
account: launchAccount?.account
|
|
41
|
+
});
|
|
22
42
|
if (!pi) {
|
|
23
43
|
throw new Error("Failed to prepare pi models.json (see warning above).");
|
|
24
44
|
}
|
|
@@ -1,4 +1,61 @@
|
|
|
1
1
|
import { applyYolo, resolveYoloWithSource } from "../../../agents/registry.js";
|
|
2
|
+
import { loadAgentConfig } from "../../../agentconfig/load.js";
|
|
3
|
+
import { findAccount } from "../../../auth/accounts.js";
|
|
4
|
+
import { assertValidAccountId } from "../../../config/accountId.js";
|
|
5
|
+
import { loadStoredCredentialForAccount } from "../../../auth/credentials.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve which account a launch targets, applying precedence
|
|
8
|
+
* `--account` > `COPILLM_ACCOUNT` > the active profile's `account` > default.
|
|
9
|
+
* Returns null for the default account (no prefix — today's behaviour).
|
|
10
|
+
*
|
|
11
|
+
* Throws a user-facing Error when a requested account is malformed, not
|
|
12
|
+
* registered, or has no stored credential, so the launcher can fail fast
|
|
13
|
+
* before starting the daemon or the agent.
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveLaunchAccount(input) {
|
|
16
|
+
let requested;
|
|
17
|
+
let source;
|
|
18
|
+
if (input.flag && input.flag.trim().length > 0) {
|
|
19
|
+
requested = input.flag.trim();
|
|
20
|
+
source = "flag";
|
|
21
|
+
}
|
|
22
|
+
else if (input.envValue && input.envValue.trim().length > 0) {
|
|
23
|
+
requested = input.envValue.trim();
|
|
24
|
+
source = "env";
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const config = loadAgentConfig({ cwd: input.cwd, profileOverride: input.profileOverride });
|
|
28
|
+
const pinned = config?.resolved.account ?? null;
|
|
29
|
+
if (pinned) {
|
|
30
|
+
requested = pinned;
|
|
31
|
+
source = "profile";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!requested) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
assertValidAccountId(requested);
|
|
38
|
+
const record = findAccount(requested);
|
|
39
|
+
if (!record) {
|
|
40
|
+
throw new Error(`Unknown account "${requested}". Run \`copillm auth status\` to list accounts.`);
|
|
41
|
+
}
|
|
42
|
+
const credential = await loadStoredCredentialForAccount(requested);
|
|
43
|
+
if (!credential) {
|
|
44
|
+
throw new Error(`No stored credential for account "${requested}". Run \`copillm auth login --as ${requested}\`.`);
|
|
45
|
+
}
|
|
46
|
+
const cacheId = record.storage === "legacy" ? undefined : requested;
|
|
47
|
+
return {
|
|
48
|
+
accountId: requested,
|
|
49
|
+
pathPrefix: `/${requested}`,
|
|
50
|
+
cacheId,
|
|
51
|
+
account: { accountType: credential.accountType, githubToken: credential.token, cacheId },
|
|
52
|
+
source: source
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function formatLaunchAccountNotice(resolved) {
|
|
56
|
+
const from = resolved.source === "flag" ? "--account" : resolved.source === "env" ? "COPILLM_ACCOUNT" : "profile";
|
|
57
|
+
return `copillm: using account "${resolved.accountId}" (from ${from})`;
|
|
58
|
+
}
|
|
2
59
|
/**
|
|
3
60
|
* Shared yolo wiring for the four agent subcommands. Resolves precedence
|
|
4
61
|
* (flag > env > profile > defaults > off), runs `applyYolo` with source
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { inspectStoredCredential, loadStoredCredentialForStatus } from "../../auth/credentials.js";
|
|
2
|
+
import { readAccountsIndex } from "../../auth/accounts.js";
|
|
2
3
|
import { inspectGithubIdentity } from "../../auth/githubIdentity.js";
|
|
3
4
|
import { ensureAuthenticatedInteractive } from "../auth/ensure.js";
|
|
4
|
-
import { runAuthLogin, runAuthLogout } from "../auth/runAuth.js";
|
|
5
|
+
import { runAuthLogin, runAuthLogout, runAuthStatusList, runAuthSwitch } from "../auth/runAuth.js";
|
|
5
6
|
import { formatHumanAuthStatusLine } from "../shared/backends.js";
|
|
6
7
|
import { emitDeprecation } from "../shared/deprecation.js";
|
|
8
|
+
const ACCOUNT_TYPES = ["individual", "business", "enterprise"];
|
|
9
|
+
function parseAccountType(value) {
|
|
10
|
+
if (ACCOUNT_TYPES.includes(value)) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Invalid --account-type "${value}". Expected one of: ${ACCOUNT_TYPES.join(", ")}.`);
|
|
14
|
+
}
|
|
7
15
|
// Re-export for callers (e.g. start command) that need the interactive prompt.
|
|
8
16
|
export { ensureAuthenticatedInteractive };
|
|
9
17
|
export function register(program) {
|
|
@@ -28,6 +36,8 @@ export function register(program) {
|
|
|
28
36
|
.command("login")
|
|
29
37
|
.description("Authenticate with GitHub")
|
|
30
38
|
.option("--json", "JSON output")
|
|
39
|
+
.option("--as <account>", "Name this account (enables multiple accounts)")
|
|
40
|
+
.option("--account-type <type>", "Account plan type: individual | business | enterprise", parseAccountType)
|
|
31
41
|
// Undocumented test seam: force the session-only backend regardless of
|
|
32
42
|
// whether the OS keychain is available. Equivalent to setting
|
|
33
43
|
// COPILLM_FORCE_SESSION_BACKEND=1 for the duration of this command.
|
|
@@ -39,9 +49,19 @@ export function register(program) {
|
|
|
39
49
|
.command("logout")
|
|
40
50
|
.description("Clear credentials and stop running daemon")
|
|
41
51
|
.option("--json", "JSON output")
|
|
52
|
+
.option("--account <account>", "Log out a specific account (default: the default account)")
|
|
53
|
+
.option("--all", "Log out of every account")
|
|
42
54
|
.action(async (opts) => {
|
|
43
55
|
await runAuthLogout(opts);
|
|
44
56
|
});
|
|
57
|
+
auth
|
|
58
|
+
.command("switch")
|
|
59
|
+
.argument("<account>", "Account id to make the default")
|
|
60
|
+
.description("Set the default account")
|
|
61
|
+
.option("--json", "JSON output")
|
|
62
|
+
.action(async (account, opts) => {
|
|
63
|
+
await runAuthSwitch(opts, account);
|
|
64
|
+
});
|
|
45
65
|
auth
|
|
46
66
|
.command("status")
|
|
47
67
|
.description("Report whether a credential is stored (token is never printed)")
|
|
@@ -51,6 +71,12 @@ export function register(program) {
|
|
|
51
71
|
// commander's --no-user toggles opts.user to false; when the flag is
|
|
52
72
|
// omitted opts.user is undefined and we treat that as "fetch by default".
|
|
53
73
|
const wantUserLookup = opts.user !== false;
|
|
74
|
+
// Multi-account installs (an accounts index exists) get the per-account
|
|
75
|
+
// listing. Single-account installs keep the exact original output below.
|
|
76
|
+
if (readAccountsIndex()) {
|
|
77
|
+
const { anyStored } = await runAuthStatusList(opts);
|
|
78
|
+
process.exit(anyStored ? 0 : 2);
|
|
79
|
+
}
|
|
54
80
|
// Two paths to minimize keychain probes:
|
|
55
81
|
// - With user lookup (default): `loadStoredCredentialForStatus()`
|
|
56
82
|
// does ONE keychain read that yields backend + token. Pass the
|
package/dist/cli/copillmFlags.js
CHANGED
|
@@ -40,6 +40,14 @@ export const COPILLM_FLAGS = [
|
|
|
40
40
|
kind: "swallow",
|
|
41
41
|
description: "Override active profile for this launch"
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
flag: "--copillm-account",
|
|
45
|
+
aliases: ["--account"],
|
|
46
|
+
takesValue: true,
|
|
47
|
+
dest: "copillmAccount",
|
|
48
|
+
kind: "swallow",
|
|
49
|
+
description: "Route this launch at a specific copillm account"
|
|
50
|
+
},
|
|
43
51
|
{
|
|
44
52
|
flag: "--copillm-no-config",
|
|
45
53
|
aliases: ["--no-config"],
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadStoredCredential } from "../../auth/credentials.js";
|
|
3
|
+
import { readAccountsIndex } from "../../auth/accounts.js";
|
|
3
4
|
import { CopilotTokenManager } from "../../auth/copilotToken.js";
|
|
4
5
|
import { loadConfig } from "../../config/config.js";
|
|
5
6
|
import { acquireLock, LockAlreadyRunningError, releaseLock } from "../../server/lock.js";
|
|
6
7
|
import { startProxyServer } from "../../server/proxy.js";
|
|
8
|
+
import { DaemonAccountResolver } from "../../server/accountResolver.js";
|
|
7
9
|
import { installProcessSafetyNet } from "../processSafetyNet.js";
|
|
8
10
|
import { getRootLogger } from "../shared/debug.js";
|
|
9
11
|
import { withTimeout } from "./lifecycle.js";
|
|
@@ -30,6 +32,22 @@ export async function runDaemon(options) {
|
|
|
30
32
|
}
|
|
31
33
|
const tokenManager = new CopilotTokenManager(creds.token);
|
|
32
34
|
await tokenManager.ensureToken(false);
|
|
35
|
+
// Build the default account's resolved identity. With no accounts index this
|
|
36
|
+
// is the legacy single account (accountId null, legacy model cache). With an
|
|
37
|
+
// index, it reflects the configured default account's id, plan type, and
|
|
38
|
+
// storage scheme — so model discovery and the cache key stay correct.
|
|
39
|
+
const accountsIndex = readAccountsIndex();
|
|
40
|
+
const defaultRecord = accountsIndex
|
|
41
|
+
? accountsIndex.accounts.find((account) => account.id === accountsIndex.defaultAccount) ?? null
|
|
42
|
+
: null;
|
|
43
|
+
const defaultAccount = {
|
|
44
|
+
accountId: defaultRecord?.id ?? null,
|
|
45
|
+
githubToken: creds.token,
|
|
46
|
+
tokenManager,
|
|
47
|
+
accountType: defaultRecord?.accountType ?? config.accountType,
|
|
48
|
+
cacheId: defaultRecord && defaultRecord.storage === "namespaced" ? defaultRecord.id : undefined
|
|
49
|
+
};
|
|
50
|
+
const accountResolver = new DaemonAccountResolver({ default: defaultAccount });
|
|
33
51
|
const callerSecret = config.requireCallerSecret ? randomUUID() : null;
|
|
34
52
|
if (callerSecret) {
|
|
35
53
|
process.stdout.write(`Caller secret: ${callerSecret}\n`);
|
|
@@ -53,6 +71,7 @@ export async function runDaemon(options) {
|
|
|
53
71
|
port,
|
|
54
72
|
config,
|
|
55
73
|
tokenManager,
|
|
74
|
+
accountResolver,
|
|
56
75
|
callerSecret,
|
|
57
76
|
logger,
|
|
58
77
|
debug: Boolean(options?.debug),
|
|
@@ -70,7 +89,7 @@ export async function runDaemon(options) {
|
|
|
70
89
|
}
|
|
71
90
|
}
|
|
72
91
|
if (!server || selectedPort === null) {
|
|
73
|
-
|
|
92
|
+
accountResolver.clearAll();
|
|
74
93
|
throw new Error(`No available port in configured range (${ports[0]}-${ports[ports.length - 1]}).`);
|
|
75
94
|
}
|
|
76
95
|
installProcessSafetyNet(logger);
|
|
@@ -87,7 +106,7 @@ export async function runDaemon(options) {
|
|
|
87
106
|
logger.warn({ err: error }, "graceful shutdown timed out");
|
|
88
107
|
}
|
|
89
108
|
finally {
|
|
90
|
-
|
|
109
|
+
accountResolver.clearAll();
|
|
91
110
|
releaseLock();
|
|
92
111
|
process.exit(0);
|
|
93
112
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { buildClaudeEnvBundle } from "../agentEnv.js";
|
|
2
2
|
import { buildClaudeExportCommand as buildClaudeExport, computeAnthropicDefaults, readModelIdsFromCache } from "../../models/anthropicDefaults.js";
|
|
3
|
-
export function buildClaudeExportCommand(port, callerSecret) {
|
|
4
|
-
const
|
|
3
|
+
export function buildClaudeExportCommand(port, callerSecret, opts) {
|
|
4
|
+
const pathPrefix = opts?.pathPrefix ?? "";
|
|
5
|
+
const modelIds = readModelIdsFromCache(opts?.cacheId);
|
|
5
6
|
const defaults = computeAnthropicDefaults(modelIds);
|
|
6
7
|
const command = buildClaudeExport({
|
|
7
8
|
port,
|
|
8
9
|
callerSecret,
|
|
9
10
|
defaults,
|
|
10
|
-
enableGatewayDiscovery: true
|
|
11
|
+
enableGatewayDiscovery: true,
|
|
12
|
+
pathPrefix
|
|
11
13
|
});
|
|
12
|
-
const bundle = buildClaudeEnvBundle({ port, callerSecret, defaults, enableGatewayDiscovery: true });
|
|
14
|
+
const bundle = buildClaudeEnvBundle({ port, callerSecret, defaults, enableGatewayDiscovery: true, pathPrefix });
|
|
13
15
|
return { command, defaults, bundle };
|
|
14
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getCopillmHome } from "../../config/home.js";
|
|
2
2
|
import { defaultOutputDir, generateCodexHome } from "../../integrations/codex/init.js";
|
|
3
|
-
export async function refreshCodexHome(port, model, precomputed) {
|
|
3
|
+
export async function refreshCodexHome(port, model, precomputed, opts) {
|
|
4
4
|
try {
|
|
5
5
|
const home = getCopillmHome();
|
|
6
6
|
return await generateCodexHome({
|
|
@@ -9,7 +9,9 @@ export async function refreshCodexHome(port, model, precomputed) {
|
|
|
9
9
|
port,
|
|
10
10
|
providerId: "copillm",
|
|
11
11
|
reasoningEffort: null,
|
|
12
|
-
precomputed
|
|
12
|
+
precomputed,
|
|
13
|
+
pathPrefix: opts?.pathPrefix,
|
|
14
|
+
account: opts?.account
|
|
13
15
|
});
|
|
14
16
|
}
|
|
15
17
|
catch (error) {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { getCopillmHome } from "../../config/home.js";
|
|
2
2
|
import { defaultOutputDir as defaultPiOutputDir, generatePiHome } from "../../integrations/pi/init.js";
|
|
3
|
-
export async function refreshPiHome(port, precomputed) {
|
|
3
|
+
export async function refreshPiHome(port, precomputed, opts) {
|
|
4
4
|
try {
|
|
5
5
|
const home = getCopillmHome();
|
|
6
6
|
return await generatePiHome({
|
|
7
7
|
outDir: defaultPiOutputDir(home),
|
|
8
8
|
port,
|
|
9
9
|
providerId: "copillm",
|
|
10
|
-
precomputed
|
|
10
|
+
precomputed,
|
|
11
|
+
pathPrefix: opts?.pathPrefix,
|
|
12
|
+
account: opts?.account
|
|
11
13
|
});
|
|
12
14
|
}
|
|
13
15
|
catch (error) {
|
package/dist/cli/packageInfo.js
CHANGED
|
@@ -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/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,6 +55,18 @@ 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
|
}
|
|
@@ -7,13 +7,14 @@ import { ensureSecureDirectory, writeFileSecureAtomic } from "../../config/fsSec
|
|
|
7
7
|
import { buildCodexCatalog } from "../../server/codexSchema.js";
|
|
8
8
|
import { inspectLock } from "../../server/lock.js";
|
|
9
9
|
export async function generateCodexHome(options) {
|
|
10
|
-
const { discovery } = await resolveStartContext(options.precomputed);
|
|
10
|
+
const { discovery } = await resolveStartContext(options.precomputed, options.account);
|
|
11
11
|
const catalog = buildCodexCatalog(discovery.models);
|
|
12
12
|
if (catalog.models.length === 0) {
|
|
13
13
|
throw new Error("No Codex-eligible models found in the live catalog.");
|
|
14
14
|
}
|
|
15
15
|
const port = options.port;
|
|
16
|
-
const
|
|
16
|
+
const prefix = options.pathPrefix ?? "";
|
|
17
|
+
const proxyUrl = `http://127.0.0.1:${port}${prefix}/codex/v1`;
|
|
17
18
|
const baseUrl = proxyUrl;
|
|
18
19
|
const defaultModel = options.model ?? pickDefaultModel(catalog.models.map((model) => model.slug));
|
|
19
20
|
if (!catalog.models.some((model) => model.slug === defaultModel)) {
|
|
@@ -47,11 +48,19 @@ export async function generateCodexHome(options) {
|
|
|
47
48
|
* Exported so `generatePiHome` (and any future agent init) can use the
|
|
48
49
|
* same loader and the same "if precomputed, reuse it" contract.
|
|
49
50
|
*/
|
|
50
|
-
export async function resolveStartContext(precomputed) {
|
|
51
|
+
export async function resolveStartContext(precomputed, account) {
|
|
51
52
|
if (precomputed) {
|
|
52
53
|
return precomputed;
|
|
53
54
|
}
|
|
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
|
+
}
|
|
55
64
|
const creds = await loadStoredCredential();
|
|
56
65
|
if (!creds) {
|
|
57
66
|
throw new Error("Not authenticated. Run `copillm login` first.");
|
|
@@ -4,7 +4,7 @@ import { ensureSecureDirectory, writeFileSecureAtomic } from "../../config/fsSec
|
|
|
4
4
|
import { piAgentDir } from "../../config/home.js";
|
|
5
5
|
import { resolveStartContext } from "../codex/init.js";
|
|
6
6
|
export async function generatePiHome(options) {
|
|
7
|
-
const { discovery } = await resolveStartContext(options.precomputed);
|
|
7
|
+
const { discovery } = await resolveStartContext(options.precomputed, options.account);
|
|
8
8
|
const eligible = discovery.models.filter(isPickerEligible);
|
|
9
9
|
// Split the catalog by which upstream endpoint each model supports. Models
|
|
10
10
|
// that advertise `/chat/completions` flow through copillm's Anthropic surface
|
|
@@ -20,9 +20,10 @@ export async function generatePiHome(options) {
|
|
|
20
20
|
if (anthropicEligible.length === 0 && responsesEligible.length === 0) {
|
|
21
21
|
throw new Error("No models discovered for pi config.");
|
|
22
22
|
}
|
|
23
|
-
const
|
|
23
|
+
const prefix = options.pathPrefix ?? "";
|
|
24
|
+
const proxyUrl = `http://127.0.0.1:${options.port}${prefix}/anthropic`;
|
|
24
25
|
// OpenAI SDK posts to `<baseUrl>/responses`, so the baseUrl must include `/v1`.
|
|
25
|
-
const responsesProxyUrl = `http://127.0.0.1:${options.port}/codex/v1`;
|
|
26
|
+
const responsesProxyUrl = `http://127.0.0.1:${options.port}${prefix}/codex/v1`;
|
|
26
27
|
const providerId = options.providerId.trim().length > 0 ? options.providerId : "copillm";
|
|
27
28
|
const responsesProviderId = `${providerId}-responses`;
|
|
28
29
|
const providers = {};
|
|
@@ -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) {
|