copillm 0.1.1 → 0.1.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.
- package/README.md +12 -4
- package/dist/agentconfig/load.js +4 -7
- package/dist/agentconfig/render.js +37 -75
- package/dist/agentconfig/schema.js +7 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,15 @@ A local proxy that exposes OpenAI- and Anthropic-compatible HTTP endpoints backe
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
`copillm` is distributed on npm
|
|
17
|
+
`copillm` is distributed on [npm](https://www.npmjs.com/package/copillm). Install it globally for the most convenient usage:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g copillm
|
|
21
|
+
|
|
22
|
+
copillm --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Alternatively, you can invoke it directly with `npx` without a global install:
|
|
18
26
|
|
|
19
27
|
```bash
|
|
20
28
|
npx copillm --help
|
|
@@ -24,12 +32,12 @@ npx copillm --help
|
|
|
24
32
|
|
|
25
33
|
```bash
|
|
26
34
|
# Authenticate once via the GitHub device flow.
|
|
27
|
-
|
|
35
|
+
copillm login
|
|
28
36
|
|
|
29
37
|
# Launch an agent. copillm starts the local daemon, installs the agent if
|
|
30
38
|
# necessary, and configures the required environment variables.
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
copillm claude
|
|
40
|
+
copillm codex
|
|
33
41
|
```
|
|
34
42
|
|
|
35
43
|
Arguments after the agent name are forwarded to the underlying CLI:
|
package/dist/agentconfig/load.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { parse as parseToml, TomlError } from "smol-toml";
|
|
4
4
|
import { ZodError } from "zod";
|
|
5
5
|
import { getCopillmHome } from "../config/home.js";
|
|
6
|
-
import { AgentTomlSchema
|
|
6
|
+
import { AgentTomlSchema } from "./schema.js";
|
|
7
7
|
export class AgentConfigError extends Error {
|
|
8
8
|
constructor(message) {
|
|
9
9
|
super(message);
|
|
@@ -97,16 +97,13 @@ function mergeAndResolve(input) {
|
|
|
97
97
|
const instructions = instructionsBody !== null && instructionsBody.trim().length > 0
|
|
98
98
|
? { body: instructionsBody }
|
|
99
99
|
: null;
|
|
100
|
-
// Merge mcp.servers map; later layers replace earlier same-named entries
|
|
101
|
-
//
|
|
100
|
+
// Merge mcp.servers map; later layers replace earlier same-named entries.
|
|
101
|
+
// Defaults are always-on: a profile may override a default by name but
|
|
102
|
+
// cannot remove it.
|
|
102
103
|
const servers = {};
|
|
103
104
|
for (const layer of layers) {
|
|
104
105
|
const layerServers = layer.mcp?.servers ?? {};
|
|
105
106
|
for (const [name, value] of Object.entries(layerServers)) {
|
|
106
|
-
if ("inherit" in value && value.inherit === UNSET_SENTINEL) {
|
|
107
|
-
delete servers[name];
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
107
|
servers[name] = expandEnv(value);
|
|
111
108
|
}
|
|
112
109
|
}
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { stringify as stringifyToml } from "smol-toml";
|
|
4
4
|
import { AgentConfigError } from "./load.js";
|
|
5
|
+
import { getCopillmHome } from "../config/home.js";
|
|
5
6
|
import { HASH_COMMENT, HTML_COMMENT, upsertManagedBlock } from "./markerBlock.js";
|
|
6
7
|
// ─── Codex ────────────────────────────────────────────────────────────────
|
|
7
8
|
export function renderCodex(input) {
|
|
@@ -79,93 +80,54 @@ const TOML_IDENT = /^[A-Za-z0-9_-]+$/;
|
|
|
79
80
|
function isValidTomlIdent(name) {
|
|
80
81
|
return TOML_IDENT.test(name);
|
|
81
82
|
}
|
|
83
|
+
// ─── Claude Code ──────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* copillm writes Claude's MCP config to ~/.copillm/claude/mcp.json (a
|
|
86
|
+
* copillm-owned location) and injects `--mcp-config <path>` into the launch
|
|
87
|
+
* argv. This is purely additive: Claude continues to read its own project
|
|
88
|
+
* (./.mcp.json) and user (~/.claude.json) scopes, and copillm never touches
|
|
89
|
+
* cwd. Instructions fan-out is not supported for Claude — put project
|
|
90
|
+
* guidance in your own CLAUDE.md and global guidance in ~/.claude/CLAUDE.md.
|
|
91
|
+
*/
|
|
82
92
|
export function renderClaude(input) {
|
|
83
93
|
const writes = [];
|
|
84
94
|
const notes = [];
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
description: "Claude Code .mcp.json"
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
// 2. CLAUDE.md instruction block.
|
|
97
|
-
if (input.resolved.instructions) {
|
|
98
|
-
const claudeMdPath = path.join(input.cwd, "CLAUDE.md");
|
|
99
|
-
const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, "utf8") : "";
|
|
100
|
-
const next = upsertManagedBlock(existing, input.resolved.instructions.body, HTML_COMMENT);
|
|
101
|
-
if (next !== existing) {
|
|
95
|
+
const cliArgs = [];
|
|
96
|
+
const claudeDir = path.join(getCopillmHome(), "claude");
|
|
97
|
+
const mcpJsonPath = path.join(claudeDir, "mcp.json");
|
|
98
|
+
const serverCount = Object.keys(input.resolved.mcpServers).length;
|
|
99
|
+
if (serverCount > 0) {
|
|
100
|
+
const content = renderClaudeMcp(input.resolved.mcpServers);
|
|
101
|
+
const existing = fs.existsSync(mcpJsonPath) ? fs.readFileSync(mcpJsonPath, "utf8") : null;
|
|
102
|
+
if (existing !== content) {
|
|
102
103
|
writes.push({
|
|
103
|
-
path:
|
|
104
|
-
content
|
|
104
|
+
path: mcpJsonPath,
|
|
105
|
+
content,
|
|
105
106
|
mode: 0o600,
|
|
106
|
-
description: "
|
|
107
|
+
description: "Claude Code mcp.json (copillm-managed)"
|
|
107
108
|
});
|
|
108
109
|
}
|
|
110
|
+
cliArgs.push("--mcp-config", mcpJsonPath);
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
existing = JSON.parse(fs.readFileSync(mcpJsonPath, "utf8"));
|
|
117
|
-
if (typeof existing !== "object" || existing === null) {
|
|
118
|
-
throw new Error("not an object");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
throw new AgentConfigError(`Failed to parse ${mcpJsonPath}: ${error.message}. ` +
|
|
123
|
-
`Fix or remove the file and re-run.`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const managed = new Set(existing._copillmManaged ?? []);
|
|
127
|
-
const userOwned = new Set(Object.keys(existing.mcpServers ?? {}).filter((n) => !managed.has(n)));
|
|
128
|
-
// Detect conflicts: a copillm-managed name collides with a user-owned name.
|
|
129
|
-
const newNames = Object.keys(servers);
|
|
130
|
-
for (const name of newNames) {
|
|
131
|
-
if (userOwned.has(name)) {
|
|
132
|
-
throw new AgentConfigError(`MCP server name "${name}" already exists in ${mcpJsonPath} and is owned by the user ` +
|
|
133
|
-
`(not previously managed by copillm). Rename one side, or move the user's entry ` +
|
|
134
|
-
`into agent.toml so copillm can manage it.`);
|
|
135
|
-
}
|
|
112
|
+
else if (fs.existsSync(mcpJsonPath)) {
|
|
113
|
+
// Profile no longer declares any servers — clear the stale file so we
|
|
114
|
+
// don't keep referencing dead config on the next launch.
|
|
115
|
+
fs.rmSync(mcpJsonPath, { force: true });
|
|
116
|
+
notes.push(`Removed stale ${mcpJsonPath} (no MCP servers in active profile).`);
|
|
136
117
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
nextServers[name] = value;
|
|
118
|
+
if (input.resolved.instructions) {
|
|
119
|
+
notes.push("Claude: instructions fan-out is unsupported (Claude has no out-of-tree " +
|
|
120
|
+
"instructions hook). Move guidance to ~/.claude/CLAUDE.md or your " +
|
|
121
|
+
"project's CLAUDE.md manually.");
|
|
142
122
|
}
|
|
123
|
+
return { writes, envOverlay: {}, cliArgs, notes };
|
|
124
|
+
}
|
|
125
|
+
function renderClaudeMcp(servers) {
|
|
126
|
+
const out = {};
|
|
143
127
|
for (const [name, server] of Object.entries(servers)) {
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
const nextFile = { ...existing };
|
|
147
|
-
if (Object.keys(nextServers).length > 0) {
|
|
148
|
-
nextFile.mcpServers = nextServers;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
delete nextFile.mcpServers;
|
|
152
|
-
}
|
|
153
|
-
if (newNames.length > 0) {
|
|
154
|
-
nextFile._copillmManaged = newNames;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
delete nextFile._copillmManaged;
|
|
158
|
-
}
|
|
159
|
-
const serialized = `${JSON.stringify(nextFile, null, 2)}\n`;
|
|
160
|
-
// Avoid pointless writes when state is unchanged.
|
|
161
|
-
if (fs.existsSync(mcpJsonPath) && fs.readFileSync(mcpJsonPath, "utf8") === serialized) {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
// If we'd produce an empty file (no servers, no other keys), skip writing.
|
|
165
|
-
if (Object.keys(nextFile).length === 0 && !fs.existsSync(mcpJsonPath)) {
|
|
166
|
-
return null;
|
|
128
|
+
out[name] = serverToClaudeShape(server);
|
|
167
129
|
}
|
|
168
|
-
return
|
|
130
|
+
return `${JSON.stringify({ mcpServers: out }, null, 2)}\n`;
|
|
169
131
|
}
|
|
170
132
|
function serverToClaudeShape(server) {
|
|
171
133
|
if (server.transport === "stdio") {
|
|
@@ -3,12 +3,15 @@ import { z } from "zod";
|
|
|
3
3
|
* Schema for `~/.copillm/agent.toml` (global) and `<cwd>/.copillm/agent.toml`
|
|
4
4
|
* (project overlay). See plans/unified-booping-mango.md for design rationale.
|
|
5
5
|
*
|
|
6
|
-
* Sections under `[defaults.*]` apply
|
|
7
|
-
*
|
|
6
|
+
* Sections under `[defaults.*]` always apply, regardless of which profile is
|
|
7
|
+
* active. A profile may override a default by re-declaring an entry with the
|
|
8
|
+
* same key (e.g. `[profiles.work.mcp.servers.<name>]` replaces the same-named
|
|
9
|
+
* `[defaults.mcp.servers.<name>]`). There is no way to *remove* a default from
|
|
10
|
+
* a profile — defaults are intentionally always-on. v1 only wires
|
|
11
|
+
* `instructions` and `mcp` into fan-out — the other
|
|
8
12
|
* sections (`skills`, `agents`, `hooks`, `permissions`) are reserved-but-
|
|
9
13
|
* permissive so users can start populating them without future TOML breaking.
|
|
10
14
|
*/
|
|
11
|
-
export const UNSET_SENTINEL = "@unset";
|
|
12
15
|
const StringRecord = z.record(z.string());
|
|
13
16
|
const McpStdioSchema = z
|
|
14
17
|
.object({
|
|
@@ -28,12 +31,7 @@ const McpHttpSchema = z
|
|
|
28
31
|
scope: z.enum(["project", "user"]).optional()
|
|
29
32
|
})
|
|
30
33
|
.strict();
|
|
31
|
-
const
|
|
32
|
-
.object({
|
|
33
|
-
inherit: z.literal(UNSET_SENTINEL)
|
|
34
|
-
})
|
|
35
|
-
.strict();
|
|
36
|
-
export const McpServerSchema = z.union([McpStdioSchema, McpHttpSchema, McpInheritUnset]);
|
|
34
|
+
export const McpServerSchema = z.union([McpStdioSchema, McpHttpSchema]);
|
|
37
35
|
const InstructionsSchema = z
|
|
38
36
|
.object({
|
|
39
37
|
body: z.string()
|