copillm 0.2.3 → 0.2.4

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 (45) hide show
  1. package/README.md +1 -0
  2. package/dist/agentconfig/apply.js +4 -3
  3. package/dist/agentconfig/load.js +34 -1
  4. package/dist/agentconfig/schema.js +22 -0
  5. package/dist/agents/registry.js +80 -6
  6. package/dist/cli/auth/ensure.js +30 -0
  7. package/dist/cli/auth/runAuth.js +38 -0
  8. package/dist/cli/commands/agents/claude.js +47 -0
  9. package/dist/cli/commands/agents/codex.js +48 -0
  10. package/dist/cli/commands/agents/copilot.js +49 -0
  11. package/dist/cli/commands/agents/pi.js +47 -0
  12. package/dist/cli/commands/agents/shared.js +28 -0
  13. package/dist/cli/commands/auth.js +99 -0
  14. package/dist/cli/commands/daemon.js +357 -0
  15. package/dist/cli/commands/env.js +135 -0
  16. package/dist/cli/commands/models.js +80 -0
  17. package/dist/cli/configCommands.js +10 -0
  18. package/dist/cli/copillmFlags.js +101 -0
  19. package/dist/cli/daemon/ensureRunning.js +66 -0
  20. package/dist/cli/daemon/lifecycle.js +61 -0
  21. package/dist/cli/daemon/probes.js +68 -0
  22. package/dist/cli/daemon/runDaemon.js +102 -0
  23. package/dist/cli/daemon/spawnEnv.js +12 -0
  24. package/dist/cli/index.js +43 -0
  25. package/dist/cli/integrations/banner.js +51 -0
  26. package/dist/cli/integrations/claudeExport.js +14 -0
  27. package/dist/cli/integrations/refreshCodex.js +19 -0
  28. package/dist/cli/integrations/refreshPi.js +17 -0
  29. package/dist/cli/shared/backends.js +31 -0
  30. package/dist/cli/shared/debug.js +44 -0
  31. package/dist/cli/shared/deprecation.js +7 -0
  32. package/dist/cli/shared/exitCodes.js +9 -0
  33. package/dist/cli/shared/output.js +14 -0
  34. package/dist/cli/shared/parseAgent.js +6 -0
  35. package/dist/cli.js +1 -1355
  36. package/dist/server/errors.js +195 -0
  37. package/dist/server/proxy.js +50 -885
  38. package/dist/server/routes/debug.js +65 -0
  39. package/dist/server/routes/health.js +32 -0
  40. package/dist/server/routes/models.js +41 -0
  41. package/dist/server/routes/proxyForward.js +108 -0
  42. package/dist/server/routes/shared.js +161 -0
  43. package/dist/server/upstream/copilotClient.js +137 -0
  44. package/dist/server/upstream/streaming.js +146 -0
  45. package/package.json +7 -2
package/README.md CHANGED
@@ -38,6 +38,7 @@ copillm login
38
38
  # necessary, and configures the required environment variables.
39
39
  copillm claude
40
40
  copillm codex
41
+ copillm copilot # GitHub Copilot CLI, signed in with copillm's token
41
42
  ```
42
43
 
43
44
  Arguments after the agent name are forwarded to the underlying CLI:
@@ -12,14 +12,14 @@ import { backupIfMismatch } from "./markerBlock.js";
12
12
  */
13
13
  export function applyAgentConfig(opts) {
14
14
  if (opts.skip) {
15
- return { active: null, writes: [], envOverlay: {}, cliArgs: [], notes: [], sources: [] };
15
+ return { active: null, writes: [], envOverlay: {}, cliArgs: [], notes: [], sources: [], yolo: null };
16
16
  }
17
17
  const load = loadAgentConfig({
18
18
  cwd: opts.cwd,
19
19
  profileOverride: opts.profileOverride ?? null
20
20
  });
21
21
  if (!load) {
22
- return { active: null, writes: [], envOverlay: {}, cliArgs: [], notes: [], sources: [] };
22
+ return { active: null, writes: [], envOverlay: {}, cliArgs: [], notes: [], sources: [], yolo: null };
23
23
  }
24
24
  const rendered = planRender(opts, load);
25
25
  // Phase 2: write. By this point all validation passed and the renderer
@@ -35,7 +35,8 @@ export function applyAgentConfig(opts) {
35
35
  envOverlay: rendered.envOverlay,
36
36
  cliArgs: rendered.cliArgs,
37
37
  notes: rendered.notes,
38
- sources: load.sources
38
+ sources: load.sources,
39
+ yolo: load.resolved.yolo
39
40
  };
40
41
  }
41
42
  export function formatApplyNotes(result, agent) {
@@ -113,7 +113,40 @@ function mergeAndResolve(input) {
113
113
  hooks: mergeRecord(layers, "hooks"),
114
114
  permissions: mergeRecord(layers, "permissions")
115
115
  };
116
- return { instructions, mcpServers: servers, reserved };
116
+ const yolo = mergeYolo(layers);
117
+ return { instructions, mcpServers: servers, yolo, reserved };
118
+ }
119
+ /**
120
+ * Layer yolo blocks across defaults + active profile. Later layers (project
121
+ * over global, profile over defaults) override earlier ones at the field
122
+ * level: `enabled` is replaced wholesale, `agents.<id>` is merged per-key so
123
+ * a profile can toggle a single agent without clearing the rest.
124
+ *
125
+ * Returns null when no layer declared `[...yolo]`, so callers can distinguish
126
+ * "config has no opinion" from "config explicitly said false".
127
+ */
128
+ function mergeYolo(layers) {
129
+ let saw = false;
130
+ let enabled;
131
+ const agents = {};
132
+ for (const layer of layers) {
133
+ const y = layer.yolo;
134
+ if (!y)
135
+ continue;
136
+ saw = true;
137
+ if (y.enabled !== undefined)
138
+ enabled = y.enabled;
139
+ if (y.agents)
140
+ Object.assign(agents, y.agents);
141
+ }
142
+ if (!saw)
143
+ return null;
144
+ const out = {};
145
+ if (enabled !== undefined)
146
+ out.enabled = enabled;
147
+ if (Object.keys(agents).length > 0)
148
+ out.agents = agents;
149
+ return out;
117
150
  }
118
151
  function mergeRecord(layers, key) {
119
152
  const out = {};
@@ -43,10 +43,32 @@ const McpSchema = z
43
43
  })
44
44
  .strict();
45
45
  const PassthroughRecord = z.record(z.unknown());
46
+ /**
47
+ * Per-agent yolo overrides. Keys must match the `AgentName` union in
48
+ * `src/integrations/registry.ts`; unknown keys are rejected so typos surface
49
+ * at config-load time rather than silently doing nothing.
50
+ */
51
+ const YoloAgentsSchema = z
52
+ .object({
53
+ claude: z.boolean().optional(),
54
+ codex: z.boolean().optional(),
55
+ copilot: z.boolean().optional(),
56
+ pi: z.boolean().optional()
57
+ })
58
+ .strict();
59
+ const YoloSchema = z
60
+ .object({
61
+ /** Profile-wide default applied to every supported agent unless overridden. */
62
+ enabled: z.boolean().optional(),
63
+ /** Per-agent overrides; takes precedence over `enabled`. */
64
+ agents: YoloAgentsSchema.optional()
65
+ })
66
+ .strict();
46
67
  const SectionSchema = z
47
68
  .object({
48
69
  instructions: InstructionsSchema.optional(),
49
70
  mcp: McpSchema.optional(),
71
+ yolo: YoloSchema.optional(),
50
72
  // v1 reserved sections: validated as objects but not interpreted.
51
73
  skills: PassthroughRecord.optional(),
52
74
  agents: PassthroughRecord.optional(),
@@ -53,20 +53,94 @@ export function applyYolo(options) {
53
53
  }
54
54
  case "unsupported": {
55
55
  const warn = options.warn ?? ((line) => process.stderr.write(`${line}\n`));
56
- warn(`copillm: --yolo ignored for ${options.agent} (${spec.reason})`);
56
+ const sourceSuffix = options.source ? `; source: ${describeSource(options.source)}` : "";
57
+ warn(`copillm: --yolo ignored for ${options.agent} (${spec.reason}${sourceSuffix})`);
57
58
  return args;
58
59
  }
59
60
  }
60
61
  }
61
62
  /**
62
- * Read the `COPILLM_YOLO` env var as a boolean. Accepts "1", "true", "yes"
63
- * (case-insensitive) as truthy; everything else (including unset) is false.
63
+ * Tri-state read of `COPILLM_YOLO`:
64
+ * - "1" | "true" | "yes" (case-insensitive) true (explicit on)
65
+ * - "0" | "false" | "no" (case-insensitive) → false (explicit off; can
66
+ * veto config-driven yolo but not the explicit --yolo flag)
67
+ * - unset / empty / anything else → undefined (no opinion)
68
+ *
69
+ * Returning undefined for "unset" lets `resolveYoloWithSource` fall through
70
+ * to the config layers; previously the env var only had a truthy path.
64
71
  */
65
72
  export function yoloFromEnv(env = process.env) {
66
73
  const raw = env.COPILLM_YOLO?.trim().toLowerCase();
67
- return raw === "1" || raw === "true" || raw === "yes";
74
+ if (raw === undefined || raw === "")
75
+ return undefined;
76
+ if (raw === "1" || raw === "true" || raw === "yes")
77
+ return true;
78
+ if (raw === "0" || raw === "false" || raw === "no")
79
+ return false;
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Precedence (top wins):
84
+ * 1. --yolo CLI flag
85
+ * 2. COPILLM_YOLO env var (tri-state — explicit off counts)
86
+ * 3. profile.agents[<agent>]
87
+ * 4. profile.enabled
88
+ * 5. defaults.agents[<agent>] (folded into profile view by mergeYolo)
89
+ * 6. defaults.enabled (ditto)
90
+ * 7. off
91
+ *
92
+ * Because `mergeYolo` already collapses defaults+profile into a single layer
93
+ * with profile-wins semantics, we only need to consult one merged view here.
94
+ * The source label distinguishes "profile" vs "defaults" only when we know
95
+ * the profile name (callers pass it through `profile.profileName`).
96
+ */
97
+ export function resolveYoloWithSource(input) {
98
+ if (input.flag) {
99
+ return { value: true, source: "flag", label: "--yolo flag" };
100
+ }
101
+ const fromEnv = yoloFromEnv(input.env ?? process.env);
102
+ if (fromEnv !== undefined) {
103
+ return { value: fromEnv, source: "env", label: "COPILLM_YOLO env" };
104
+ }
105
+ const y = input.profile?.yolo;
106
+ if (y) {
107
+ const profileLabel = input.profile?.profileName
108
+ ? `profile "${input.profile.profileName}"`
109
+ : "agent.toml";
110
+ const perAgent = y.agents?.[input.agent];
111
+ if (perAgent !== undefined) {
112
+ return { value: perAgent, source: "profile.agents", label: `${profileLabel} (agents.${input.agent})` };
113
+ }
114
+ if (y.enabled !== undefined) {
115
+ return { value: y.enabled, source: "profile.enabled", label: `${profileLabel} (enabled)` };
116
+ }
117
+ }
118
+ return { value: false, source: "off", label: "default off" };
68
119
  }
69
- /** Combine the per-launch flag with the env var fallback. */
120
+ /**
121
+ * Back-compat shim for callers that don't (yet) thread the merged profile
122
+ * through. Kept so older entry points keep compiling; new code should prefer
123
+ * `resolveYoloWithSource` and pass the result's `value` + `source` into
124
+ * `applyYolo` so unsupported-agent warnings carry attribution.
125
+ */
70
126
  export function resolveYolo(flag, env = process.env) {
71
- return Boolean(flag) || yoloFromEnv(env);
127
+ return resolveYoloWithSource({ agent: "claude", flag, env }).value;
128
+ }
129
+ function describeSource(source) {
130
+ switch (source) {
131
+ case "flag":
132
+ return "--yolo flag";
133
+ case "env":
134
+ return "COPILLM_YOLO env";
135
+ case "profile.agents":
136
+ return "profile agents map";
137
+ case "profile.enabled":
138
+ return "profile enabled";
139
+ case "defaults.agents":
140
+ return "defaults agents map";
141
+ case "defaults.enabled":
142
+ return "defaults enabled";
143
+ case "off":
144
+ return "default off";
145
+ }
72
146
  }
@@ -0,0 +1,30 @@
1
+ import { inspectStoredCredential, saveStoredCredential } from "../../auth/credentials.js";
2
+ import { loginViaDeviceFlow } from "../../auth/deviceFlow.js";
3
+ import { ensureAuthenticatedInteractive as ensureAuthenticatedInteractiveImpl } from "../../auth/ensureAuthenticated.js";
4
+ import { choose, confirm } from "../../auth/interactivePrompt.js";
5
+ import { loadConfig } from "../../config/config.js";
6
+ import { describeBackend } from "../shared/backends.js";
7
+ /**
8
+ * Build the default dependency bundle for ensureAuthenticatedInteractive.
9
+ * Lives here (rather than inside the auth module) so the auth module stays
10
+ * UI-framework-agnostic and tests can supply alternative implementations.
11
+ */
12
+ export function defaultEnsureAuthDeps() {
13
+ return {
14
+ inspectStoredCredential,
15
+ isTty: () => process.stdin.isTTY === true,
16
+ confirm,
17
+ choose,
18
+ loginViaDeviceFlow,
19
+ loadAccountType: () => loadConfig().accountType,
20
+ saveStoredCredential,
21
+ describeBackend,
22
+ print: (line) => process.stdout.write(line),
23
+ setEnv: (key, value) => {
24
+ process.env[key] = value;
25
+ }
26
+ };
27
+ }
28
+ export async function ensureAuthenticatedInteractive() {
29
+ return ensureAuthenticatedInteractiveImpl(defaultEnsureAuthDeps());
30
+ }
@@ -0,0 +1,38 @@
1
+ import { clearStoredCredential, saveStoredCredential } from "../../auth/credentials.js";
2
+ import { loginViaDeviceFlow } from "../../auth/deviceFlow.js";
3
+ import { loadConfig } from "../../config/config.js";
4
+ import { inspectLock, releaseLock } from "../../server/lock.js";
5
+ import { stopByPid } from "../daemon/lifecycle.js";
6
+ import { describeBackend } from "../shared/backends.js";
7
+ import { writeCommandOutput } from "../shared/output.js";
8
+ export async function runAuthLogin(opts, options) {
9
+ if (options.forceSession) {
10
+ process.env.COPILLM_FORCE_SESSION_BACKEND = "1";
11
+ }
12
+ const config = loadConfig();
13
+ const token = await loginViaDeviceFlow();
14
+ const saveMode = options.forceSession ? "session" : "auto";
15
+ const backend = await saveStoredCredential(token, config.accountType, { mode: saveMode });
16
+ writeCommandOutput(opts, `Login succeeded. Credentials stored via ${describeBackend(backend)}.`, {
17
+ status: "ok",
18
+ action: "login",
19
+ credential_backend: backend
20
+ });
21
+ }
22
+ export async function runAuthLogout(opts) {
23
+ const result = await clearStoredCredential();
24
+ const lockState = inspectLock();
25
+ if (lockState.state === "running") {
26
+ await stopByPid(lockState.lock.pid);
27
+ }
28
+ else if (lockState.state === "stale") {
29
+ releaseLock();
30
+ }
31
+ const credentialStatus = result.removed ? "removed" : "not present";
32
+ writeCommandOutput(opts, `Logged out. Credentials ${credentialStatus} from ${describeBackend(result.backend)}.`, {
33
+ status: "ok",
34
+ action: "logout",
35
+ credential_backend: result.backend,
36
+ credential_removed: result.removed
37
+ });
38
+ }
@@ -0,0 +1,47 @@
1
+ import { applyAgentConfig, formatApplyNotes } from "../../../agentconfig/apply.js";
2
+ import { detectClaudeSettingsConflicts, formatSettingsConflictWarning } from "../../../integrations/claude/settingsConflict.js";
3
+ import { processCopillmArgs } from "../../copillmFlags.js";
4
+ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
5
+ import { launchAgent } from "../../launchAgent.js";
6
+ import { buildClaudeExportCommand } from "../../integrations/claudeExport.js";
7
+ import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
8
+ import { applyYoloForLaunch } from "./shared.js";
9
+ export function register(program) {
10
+ program
11
+ .command("claude")
12
+ .description("Launch Claude Code against copillm (auto-starts daemon, downloads claude if missing)")
13
+ .allowUnknownOption(true)
14
+ .helpOption(false)
15
+ .argument("[args...]", "Args forwarded to claude")
16
+ .action(async (forwardedArgs) => {
17
+ const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
18
+ const debug = resolveCopillmDebug(opts.copillmDebug);
19
+ enableRuntimeDebug(debug);
20
+ const lock = await ensureDaemonRunningForLauncher({ debug });
21
+ const claude = buildClaudeExportCommand(lock.port, null);
22
+ const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_CLAUDE_VERSION ?? undefined;
23
+ const conflicts = detectClaudeSettingsConflicts(claude.bundle.env);
24
+ for (const line of formatSettingsConflictWarning(conflicts)) {
25
+ process.stderr.write(`${line}\n`);
26
+ }
27
+ const applyResult = applyAgentConfig({
28
+ agent: "claude",
29
+ cwd: process.cwd(),
30
+ profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
31
+ skip: Boolean(opts.copillmNoConfig)
32
+ });
33
+ for (const line of formatApplyNotes(applyResult, "claude")) {
34
+ process.stderr.write(`${line}\n`);
35
+ }
36
+ const env = { ...claude.bundle.env, ...applyResult.envOverlay };
37
+ const baseArgs = [...forwarded, ...applyResult.cliArgs];
38
+ const args = applyYoloForLaunch({ agent: "claude", flag: opts.yolo, applyResult, baseArgs });
39
+ const exitCode = await launchAgent({
40
+ agent: "claude",
41
+ args,
42
+ env,
43
+ pinnedSpec
44
+ });
45
+ process.exit(exitCode);
46
+ });
47
+ }
@@ -0,0 +1,48 @@
1
+ import { applyAgentConfig, formatApplyNotes } from "../../../agentconfig/apply.js";
2
+ import { buildCodexEnvBundle } from "../../agentEnv.js";
3
+ import { processCopillmArgs } from "../../copillmFlags.js";
4
+ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
5
+ import { launchAgent } from "../../launchAgent.js";
6
+ import { refreshCodexHome } from "../../integrations/refreshCodex.js";
7
+ import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
8
+ import { applyYoloForLaunch } from "./shared.js";
9
+ export function register(program) {
10
+ program
11
+ .command("codex")
12
+ .description("Launch Codex CLI against copillm (auto-starts daemon, downloads codex if missing)")
13
+ .allowUnknownOption(true)
14
+ .helpOption(false)
15
+ .argument("[args...]", "Args forwarded to codex")
16
+ .action(async (forwardedArgs) => {
17
+ const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
18
+ const debug = resolveCopillmDebug(opts.copillmDebug);
19
+ enableRuntimeDebug(debug);
20
+ const lock = await ensureDaemonRunningForLauncher({ debug });
21
+ const codex = await refreshCodexHome(lock.port, null);
22
+ if (!codex) {
23
+ throw new Error("Failed to prepare Codex home (see warning above).");
24
+ }
25
+ const bundle = buildCodexEnvBundle(codex.outDir);
26
+ const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_CODEX_VERSION ?? undefined;
27
+ const applyResult = applyAgentConfig({
28
+ agent: "codex",
29
+ cwd: process.cwd(),
30
+ codexHomeDir: codex.outDir,
31
+ profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
32
+ skip: Boolean(opts.copillmNoConfig)
33
+ });
34
+ for (const line of formatApplyNotes(applyResult, "codex")) {
35
+ process.stderr.write(`${line}\n`);
36
+ }
37
+ const env = { ...bundle.env, ...applyResult.envOverlay };
38
+ const baseArgs = [...forwarded, ...applyResult.cliArgs];
39
+ const args = applyYoloForLaunch({ agent: "codex", flag: opts.yolo, applyResult, baseArgs });
40
+ const exitCode = await launchAgent({
41
+ agent: "codex",
42
+ args,
43
+ env,
44
+ pinnedSpec
45
+ });
46
+ process.exit(exitCode);
47
+ });
48
+ }
@@ -0,0 +1,49 @@
1
+ import { applyAgentConfig, formatApplyNotes } from "../../../agentconfig/apply.js";
2
+ import { loadStoredCredential } from "../../../auth/credentials.js";
3
+ import { processCopillmArgs } from "../../copillmFlags.js";
4
+ import { launchAgent } from "../../launchAgent.js";
5
+ import { applyYoloForLaunch } from "./shared.js";
6
+ export function register(program) {
7
+ program
8
+ .command("copilot")
9
+ .description("Launch GitHub Copilot CLI reusing copillm's stored GitHub token (no second device flow)")
10
+ .allowUnknownOption(true)
11
+ .helpOption(false)
12
+ .argument("[args...]", "Args forwarded to copilot")
13
+ .action(async (forwardedArgs) => {
14
+ const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
15
+ const credential = await loadStoredCredential();
16
+ if (!credential) {
17
+ process.stderr.write("copillm: no stored GitHub credential — run `copillm auth login` first.\n");
18
+ process.exit(1);
19
+ return;
20
+ }
21
+ const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_COPILOT_VERSION ?? undefined;
22
+ const applyResult = applyAgentConfig({
23
+ agent: "copilot",
24
+ cwd: process.cwd(),
25
+ profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
26
+ skip: Boolean(opts.copillmNoConfig)
27
+ });
28
+ for (const line of formatApplyNotes(applyResult, "copilot")) {
29
+ process.stderr.write(`${line}\n`);
30
+ }
31
+ // Inject the stored GitHub OAuth token into the child env only — never
32
+ // export to the parent shell and never persist. Copilot CLI honours
33
+ // COPILOT_GITHUB_TOKEN ahead of its own stored credentials, so this
34
+ // short-circuits its device-flow login when copillm already has a token.
35
+ const env = {
36
+ ...applyResult.envOverlay,
37
+ COPILOT_GITHUB_TOKEN: credential.token
38
+ };
39
+ const baseArgs = [...forwarded, ...applyResult.cliArgs];
40
+ const args = applyYoloForLaunch({ agent: "copilot", flag: opts.yolo, applyResult, baseArgs });
41
+ const exitCode = await launchAgent({
42
+ agent: "copilot",
43
+ args,
44
+ env,
45
+ pinnedSpec
46
+ });
47
+ process.exit(exitCode);
48
+ });
49
+ }
@@ -0,0 +1,47 @@
1
+ import { applyAgentConfig, formatApplyNotes } from "../../../agentconfig/apply.js";
2
+ import { buildPiEnvBundle } from "../../agentEnv.js";
3
+ import { processCopillmArgs } from "../../copillmFlags.js";
4
+ import { ensureDaemonRunningForLauncher } from "../../daemon/ensureRunning.js";
5
+ import { launchAgent } from "../../launchAgent.js";
6
+ import { refreshPiHome } from "../../integrations/refreshPi.js";
7
+ import { enableRuntimeDebug, resolveCopillmDebug } from "../../shared/debug.js";
8
+ import { applyYoloForLaunch } from "./shared.js";
9
+ export function register(program) {
10
+ program
11
+ .command("pi")
12
+ .description("Launch pi coding agent against copillm (auto-starts daemon, downloads pi if missing)")
13
+ .allowUnknownOption(true)
14
+ .helpOption(false)
15
+ .argument("[args...]", "Args forwarded to pi")
16
+ .action(async (forwardedArgs) => {
17
+ const { opts, forwarded } = processCopillmArgs(forwardedArgs ?? []);
18
+ const debug = resolveCopillmDebug(opts.copillmDebug);
19
+ enableRuntimeDebug(debug);
20
+ const lock = await ensureDaemonRunningForLauncher({ debug });
21
+ const pi = await refreshPiHome(lock.port);
22
+ if (!pi) {
23
+ throw new Error("Failed to prepare pi models.json (see warning above).");
24
+ }
25
+ const bundle = buildPiEnvBundle(pi.outDir);
26
+ const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_PI_VERSION ?? undefined;
27
+ const applyResult = applyAgentConfig({
28
+ agent: "pi",
29
+ cwd: process.cwd(),
30
+ profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
31
+ skip: Boolean(opts.copillmNoConfig)
32
+ });
33
+ for (const line of formatApplyNotes(applyResult, "pi")) {
34
+ process.stderr.write(`${line}\n`);
35
+ }
36
+ const env = { ...bundle.env, ...applyResult.envOverlay };
37
+ const baseArgs = [...forwarded, ...applyResult.cliArgs];
38
+ const args = applyYoloForLaunch({ agent: "pi", flag: opts.yolo, applyResult, baseArgs });
39
+ const exitCode = await launchAgent({
40
+ agent: "pi",
41
+ args,
42
+ env,
43
+ pinnedSpec
44
+ });
45
+ process.exit(exitCode);
46
+ });
47
+ }
@@ -0,0 +1,28 @@
1
+ import { applyYolo, resolveYoloWithSource } from "../../../agents/registry.js";
2
+ /**
3
+ * Shared yolo wiring for the four agent subcommands. Resolves precedence
4
+ * (flag > env > profile > defaults > off), runs `applyYolo` with source
5
+ * attribution so the unsupported-agent warning carries traceable origin
6
+ * info, and emits a one-line stderr notice when yolo was turned on by a
7
+ * config layer rather than the explicit --yolo flag (so users aren't
8
+ * surprised by silently-skipped approvals).
9
+ */
10
+ export function applyYoloForLaunch(params) {
11
+ const decision = resolveYoloWithSource({
12
+ agent: params.agent,
13
+ flag: params.flag,
14
+ profile: {
15
+ yolo: params.applyResult.yolo,
16
+ profileName: params.applyResult.active
17
+ }
18
+ });
19
+ if (decision.value && decision.source !== "flag" && decision.source !== "env") {
20
+ process.stderr.write(`copillm: yolo enabled for ${params.agent} via ${decision.label}\n`);
21
+ }
22
+ return applyYolo({
23
+ agent: params.agent,
24
+ userArgs: params.baseArgs,
25
+ yolo: decision.value,
26
+ source: decision.source
27
+ });
28
+ }
@@ -0,0 +1,99 @@
1
+ import { inspectStoredCredential } from "../../auth/credentials.js";
2
+ import { inspectGithubIdentity } from "../../auth/githubIdentity.js";
3
+ import { ensureAuthenticatedInteractive } from "../auth/ensure.js";
4
+ import { runAuthLogin, runAuthLogout } from "../auth/runAuth.js";
5
+ import { formatHumanAuthStatusLine } from "../shared/backends.js";
6
+ import { emitDeprecation } from "../shared/deprecation.js";
7
+ // Re-export for callers (e.g. start command) that need the interactive prompt.
8
+ export { ensureAuthenticatedInteractive };
9
+ export function register(program) {
10
+ program
11
+ .command("login")
12
+ .description("[deprecated] Use `copillm auth login`")
13
+ .option("--json", "JSON output")
14
+ .action(async (opts) => {
15
+ emitDeprecation(opts, "login", "auth login");
16
+ await runAuthLogin(opts, { forceSession: false });
17
+ });
18
+ program
19
+ .command("logout")
20
+ .description("[deprecated] Use `copillm auth logout`")
21
+ .option("--json", "JSON output")
22
+ .action(async (opts) => {
23
+ emitDeprecation(opts, "logout", "auth logout");
24
+ await runAuthLogout(opts);
25
+ });
26
+ const auth = program.command("auth").description("Authentication commands");
27
+ auth
28
+ .command("login")
29
+ .description("Authenticate with GitHub")
30
+ .option("--json", "JSON output")
31
+ // Undocumented test seam: force the session-only backend regardless of
32
+ // whether the OS keychain is available. Equivalent to setting
33
+ // COPILLM_FORCE_SESSION_BACKEND=1 for the duration of this command.
34
+ .option("--force-session", "(test-only) force the session-only backend", false)
35
+ .action(async (opts) => {
36
+ await runAuthLogin(opts, { forceSession: Boolean(opts.forceSession) });
37
+ });
38
+ auth
39
+ .command("logout")
40
+ .description("Clear credentials and stop running daemon")
41
+ .option("--json", "JSON output")
42
+ .action(async (opts) => {
43
+ await runAuthLogout(opts);
44
+ });
45
+ auth
46
+ .command("status")
47
+ .description("Report whether a credential is stored (token is never printed)")
48
+ .option("--json", "JSON output")
49
+ .option("--no-user", "Skip the GitHub /user lookup that fetches the login name")
50
+ .action(async (opts) => {
51
+ let info;
52
+ try {
53
+ info = await inspectStoredCredential();
54
+ }
55
+ catch (error) {
56
+ const message = error instanceof Error ? error.message : "unknown_error";
57
+ if (opts.json) {
58
+ process.stdout.write(JSON.stringify({ status: "error", error: message }, null, 2) + "\n");
59
+ }
60
+ else {
61
+ process.stderr.write(`auth status error: ${message}\n`);
62
+ }
63
+ process.exit(1);
64
+ }
65
+ // commander's --no-user toggles opts.user to false; when the flag is
66
+ // omitted opts.user is undefined and we treat that as "fetch by default".
67
+ const userLookupEnabled = info.stored && opts.user !== false;
68
+ let identity = null;
69
+ if (userLookupEnabled) {
70
+ // inspectGithubIdentity is designed to return null on any failure, but
71
+ // we wrap defensively at the CLI level too: a regression in the wrapper,
72
+ // or a platform-specific fetch error path (e.g. Node 22 on macOS has
73
+ // surfaced uncaught socket rejections from privileged-port ECONNREFUSED),
74
+ // must never break the auth-status command. Status output should always
75
+ // succeed even when the network is broken.
76
+ try {
77
+ identity = await inspectGithubIdentity();
78
+ }
79
+ catch {
80
+ identity = null;
81
+ }
82
+ }
83
+ if (opts.json) {
84
+ process.stdout.write(JSON.stringify({
85
+ status: info.stored ? "logged_in" : "logged_out",
86
+ stored: info.stored,
87
+ backend: info.backend,
88
+ user: identity
89
+ }, null, 2) + "\n");
90
+ }
91
+ else if (info.stored) {
92
+ process.stdout.write(`${formatHumanAuthStatusLine(info.backend, identity)}\n`);
93
+ }
94
+ else {
95
+ process.stdout.write("not logged in\n");
96
+ }
97
+ process.exit(info.stored ? 0 : 2);
98
+ });
99
+ }