copillm 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/agentconfig/apply.js +4 -3
- package/dist/agentconfig/load.js +34 -1
- package/dist/agentconfig/schema.js +22 -0
- package/dist/agents/registry.js +80 -6
- package/dist/cli/auth/ensure.js +30 -0
- package/dist/cli/auth/runAuth.js +38 -0
- package/dist/cli/commands/agents/claude.js +47 -0
- package/dist/cli/commands/agents/codex.js +48 -0
- package/dist/cli/commands/agents/copilot.js +49 -0
- package/dist/cli/commands/agents/pi.js +47 -0
- package/dist/cli/commands/agents/shared.js +28 -0
- package/dist/cli/commands/auth.js +99 -0
- package/dist/cli/commands/daemon.js +358 -0
- package/dist/cli/commands/env.js +135 -0
- package/dist/cli/commands/models.js +80 -0
- package/dist/cli/configCommands.js +10 -0
- package/dist/cli/copillmFlags.js +111 -0
- package/dist/cli/daemon/ensureRunning.js +65 -0
- package/dist/cli/daemon/lifecycle.js +61 -0
- package/dist/cli/daemon/probes.js +68 -0
- package/dist/cli/daemon/runDaemon.js +102 -0
- package/dist/cli/daemon/selfSpawn.js +15 -0
- package/dist/cli/daemon/spawnEnv.js +12 -0
- package/dist/cli/index.js +41 -0
- package/dist/cli/integrations/banner.js +51 -0
- package/dist/cli/integrations/claudeExport.js +14 -0
- package/dist/cli/integrations/refreshCodex.js +19 -0
- package/dist/cli/integrations/refreshPi.js +17 -0
- package/dist/cli/packageInfo.js +29 -0
- package/dist/cli/shared/backends.js +31 -0
- package/dist/cli/shared/debug.js +44 -0
- package/dist/cli/shared/deprecation.js +7 -0
- package/dist/cli/shared/exitCodes.js +9 -0
- package/dist/cli/shared/output.js +14 -0
- package/dist/cli/shared/parseAgent.js +6 -0
- package/dist/cli/updateNotifier.js +223 -0
- package/dist/cli.js +1 -1355
- package/dist/server/errors.js +195 -0
- package/dist/server/proxy.js +50 -885
- package/dist/server/routes/debug.js +65 -0
- package/dist/server/routes/health.js +32 -0
- package/dist/server/routes/models.js +41 -0
- package/dist/server/routes/proxyForward.js +108 -0
- package/dist/server/routes/shared.js +161 -0
- package/dist/server/upstream/copilotClient.js +137 -0
- package/dist/server/upstream/streaming.js +146 -0
- 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) {
|
package/dist/agentconfig/load.js
CHANGED
|
@@ -113,7 +113,40 @@ function mergeAndResolve(input) {
|
|
|
113
113
|
hooks: mergeRecord(layers, "hooks"),
|
|
114
114
|
permissions: mergeRecord(layers, "permissions")
|
|
115
115
|
};
|
|
116
|
-
|
|
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(),
|
package/dist/agents/registry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
63
|
-
* (case-insensitive)
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
+
}
|