@codemcp/ade-harnesses 0.2.0 → 0.2.3
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/dist/writers/kiro.js +1 -1
- package/package.json +20 -12
- package/.prettierignore +0 -1
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-format.log +0 -6
- package/.turbo/turbo-lint.log +0 -4
- package/.turbo/turbo-test.log +0 -23
- package/.turbo/turbo-typecheck.log +0 -4
- package/eslint.config.mjs +0 -40
- package/src/index.spec.ts +0 -45
- package/src/index.ts +0 -47
- package/src/permission-policy.ts +0 -173
- package/src/skills-installer.ts +0 -54
- package/src/types.ts +0 -12
- package/src/util.ts +0 -221
- package/src/writers/claude-code.spec.ts +0 -320
- package/src/writers/claude-code.ts +0 -107
- package/src/writers/cline.spec.ts +0 -212
- package/src/writers/cline.ts +0 -24
- package/src/writers/copilot.spec.ts +0 -258
- package/src/writers/copilot.ts +0 -105
- package/src/writers/cursor.spec.ts +0 -219
- package/src/writers/cursor.ts +0 -95
- package/src/writers/kiro.spec.ts +0 -228
- package/src/writers/kiro.ts +0 -89
- package/src/writers/opencode.spec.ts +0 -258
- package/src/writers/opencode.ts +0 -67
- package/src/writers/roo-code.spec.ts +0 -197
- package/src/writers/roo-code.ts +0 -71
- package/src/writers/universal.spec.ts +0 -134
- package/src/writers/universal.ts +0 -84
- package/src/writers/windsurf.spec.ts +0 -178
- package/src/writers/windsurf.ts +0 -89
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -7
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.vitest.json +0 -7
- package/vitest.config.ts +0 -5
package/src/util.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import type { GitHook, LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// JSON helpers
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
/** Read a JSON file, returning `{}` if missing or unparseable. */
|
|
10
|
-
export async function readJsonOrEmpty(
|
|
11
|
-
path: string
|
|
12
|
-
): Promise<Record<string, unknown>> {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(await readFile(path, "utf-8"));
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Write a JSON object with trailing newline. Creates parent dirs. */
|
|
21
|
-
export async function writeJson(path: string, data: unknown): Promise<void> {
|
|
22
|
-
await mkdir(dirname(path), { recursive: true });
|
|
23
|
-
await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Server entry transform — each harness overrides only what differs
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
/** Minimal MCP entry: command + args + optional env. */
|
|
31
|
-
function baseEntry(server: McpServerEntry) {
|
|
32
|
-
return {
|
|
33
|
-
command: server.command,
|
|
34
|
-
args: server.args,
|
|
35
|
-
...(Object.keys(server.env).length > 0 ? { env: server.env } : {})
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type ServerTransform = (
|
|
40
|
-
server: McpServerEntry
|
|
41
|
-
) => Record<string, unknown>;
|
|
42
|
-
|
|
43
|
-
/** Standard mcpServers entry (cursor, universal, claude-code). */
|
|
44
|
-
export const standardEntry: ServerTransform = baseEntry;
|
|
45
|
-
|
|
46
|
-
/** Adds `type: "stdio"` (copilot). */
|
|
47
|
-
export const stdioEntry: ServerTransform = (s) => ({
|
|
48
|
-
type: "stdio",
|
|
49
|
-
...baseEntry(s)
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/** Adds `alwaysAllow` (cline, roo-code, windsurf). */
|
|
53
|
-
export const alwaysAllowEntry: ServerTransform = (s) => ({
|
|
54
|
-
...baseEntry(s),
|
|
55
|
-
alwaysAllow: s.allowedTools ?? ["*"]
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// MCP JSON writer — covers 7 of 9 harnesses
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
interface WriteMcpServersOpts {
|
|
63
|
-
/** Full path to the JSON file. */
|
|
64
|
-
path: string;
|
|
65
|
-
/** Key in the JSON that holds the server map. Default: `"mcpServers"`. */
|
|
66
|
-
key?: string;
|
|
67
|
-
/** Transform each McpServerEntry into the harness-specific shape. */
|
|
68
|
-
transform?: ServerTransform;
|
|
69
|
-
/** Extra top-level fields to merge (e.g. `$schema`). */
|
|
70
|
-
defaults?: Record<string, unknown>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Merge MCP server entries into an existing JSON config file.
|
|
75
|
-
* Creates the file (and parent dirs) if missing.
|
|
76
|
-
*/
|
|
77
|
-
export async function writeMcpServers(
|
|
78
|
-
servers: McpServerEntry[],
|
|
79
|
-
opts: WriteMcpServersOpts
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
if (servers.length === 0) return;
|
|
82
|
-
|
|
83
|
-
const key = opts.key ?? "mcpServers";
|
|
84
|
-
const transform = opts.transform ?? standardEntry;
|
|
85
|
-
|
|
86
|
-
const existing = await readJsonOrEmpty(opts.path);
|
|
87
|
-
const map = (existing[key] as Record<string, unknown>) ?? {};
|
|
88
|
-
|
|
89
|
-
for (const server of servers) {
|
|
90
|
-
map[server.ref] = transform(server);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = { ...(opts.defaults ?? {}), ...existing, [key]: map };
|
|
94
|
-
await writeJson(opts.path, result);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// Instructions → flat rules file (windsurf, cline, roo-code)
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Write instructions as a plain text rules file.
|
|
103
|
-
* Skips if no instructions.
|
|
104
|
-
*/
|
|
105
|
-
export async function writeRulesFile(
|
|
106
|
-
instructions: string[],
|
|
107
|
-
path: string
|
|
108
|
-
): Promise<void> {
|
|
109
|
-
if (instructions.length === 0) return;
|
|
110
|
-
const lines = instructions.flatMap((i) => [i, ""]);
|
|
111
|
-
await mkdir(dirname(path), { recursive: true });
|
|
112
|
-
await writeFile(path, lines.join("\n"), "utf-8");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
// Instructions → agent markdown with YAML frontmatter
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
|
|
119
|
-
interface AgentMdOpts {
|
|
120
|
-
/** Full path to the .md file. */
|
|
121
|
-
path: string;
|
|
122
|
-
/** Extra YAML frontmatter lines (after name/description, before `---`). */
|
|
123
|
-
extraFrontmatter?: string[];
|
|
124
|
-
/** Fallback body when instructions are empty. */
|
|
125
|
-
fallbackBody?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Write an agent markdown file with YAML frontmatter.
|
|
130
|
-
* Shared by claude-code, copilot, and opencode.
|
|
131
|
-
*/
|
|
132
|
-
export async function writeAgentMd(
|
|
133
|
-
config: LogicalConfig,
|
|
134
|
-
opts: AgentMdOpts
|
|
135
|
-
): Promise<void> {
|
|
136
|
-
if (config.instructions.length === 0 && config.mcp_servers.length === 0)
|
|
137
|
-
return;
|
|
138
|
-
|
|
139
|
-
const fm: string[] = [
|
|
140
|
-
"---",
|
|
141
|
-
"name: ade",
|
|
142
|
-
"description: ADE — Agentic Development Environment agent with project conventions and tools"
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
if (opts.extraFrontmatter) {
|
|
146
|
-
fm.push(...opts.extraFrontmatter);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
fm.push("---");
|
|
150
|
-
|
|
151
|
-
const body =
|
|
152
|
-
config.instructions.length > 0
|
|
153
|
-
? config.instructions.join("\n\n")
|
|
154
|
-
: (opts.fallbackBody ?? "");
|
|
155
|
-
|
|
156
|
-
const content = fm.join("\n") + "\n\n" + body + "\n";
|
|
157
|
-
await mkdir(dirname(opts.path), { recursive: true });
|
|
158
|
-
await writeFile(opts.path, content, "utf-8");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
// Inline skill SKILL.md writer (used by claude-code)
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Git hook installer
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Write git hook scripts to `.git/hooks/<phase>`.
|
|
171
|
-
* Files are created with executable permissions (0o755).
|
|
172
|
-
* No-op when the hooks array is empty.
|
|
173
|
-
*/
|
|
174
|
-
export async function writeGitHooks(
|
|
175
|
-
hooks: GitHook[] | undefined,
|
|
176
|
-
projectRoot: string
|
|
177
|
-
): Promise<void> {
|
|
178
|
-
if (!hooks) return;
|
|
179
|
-
for (const hook of hooks) {
|
|
180
|
-
const hookPath = join(projectRoot, ".git", "hooks", hook.phase);
|
|
181
|
-
await writeFile(hookPath, hook.script, { mode: 0o755 });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export async function writeInlineSkills(
|
|
186
|
-
config: LogicalConfig,
|
|
187
|
-
projectRoot: string
|
|
188
|
-
): Promise<string[]> {
|
|
189
|
-
const modified: string[] = [];
|
|
190
|
-
|
|
191
|
-
for (const skill of config.skills) {
|
|
192
|
-
if (!("body" in skill)) continue;
|
|
193
|
-
|
|
194
|
-
const skillDir = join(projectRoot, ".ade", "skills", skill.name);
|
|
195
|
-
const skillPath = join(skillDir, "SKILL.md");
|
|
196
|
-
|
|
197
|
-
const frontmatter = [
|
|
198
|
-
"---",
|
|
199
|
-
`name: ${skill.name}`,
|
|
200
|
-
`description: ${skill.description}`,
|
|
201
|
-
"---"
|
|
202
|
-
].join("\n");
|
|
203
|
-
|
|
204
|
-
const expected = `${frontmatter}\n\n${skill.body}\n`;
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
const existing = await readFile(skillPath, "utf-8");
|
|
208
|
-
if (existing !== expected) {
|
|
209
|
-
modified.push(skill.name);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
} catch {
|
|
213
|
-
// File doesn't exist yet — fall through to write
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
await mkdir(skillDir, { recursive: true });
|
|
217
|
-
await writeFile(skillPath, expected, "utf-8");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return modified;
|
|
221
|
-
}
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import type {
|
|
6
|
-
AutonomyProfile,
|
|
7
|
-
LogicalConfig,
|
|
8
|
-
PermissionPolicy
|
|
9
|
-
} from "@codemcp/ade-core";
|
|
10
|
-
import { claudeCodeWriter } from "./claude-code.js";
|
|
11
|
-
import { writeInlineSkills } from "../util.js";
|
|
12
|
-
|
|
13
|
-
function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
|
|
14
|
-
switch (profile) {
|
|
15
|
-
case "rigid":
|
|
16
|
-
return {
|
|
17
|
-
profile,
|
|
18
|
-
capabilities: {
|
|
19
|
-
read: "ask",
|
|
20
|
-
edit_write: "ask",
|
|
21
|
-
search_list: "ask",
|
|
22
|
-
bash_safe: "ask",
|
|
23
|
-
bash_unsafe: "ask",
|
|
24
|
-
web: "ask",
|
|
25
|
-
task_agent: "ask"
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
case "sensible-defaults":
|
|
29
|
-
return {
|
|
30
|
-
profile,
|
|
31
|
-
capabilities: {
|
|
32
|
-
read: "allow",
|
|
33
|
-
edit_write: "allow",
|
|
34
|
-
search_list: "allow",
|
|
35
|
-
bash_safe: "allow",
|
|
36
|
-
bash_unsafe: "ask",
|
|
37
|
-
web: "ask",
|
|
38
|
-
task_agent: "allow"
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
case "max-autonomy":
|
|
42
|
-
return {
|
|
43
|
-
profile,
|
|
44
|
-
capabilities: {
|
|
45
|
-
read: "allow",
|
|
46
|
-
edit_write: "allow",
|
|
47
|
-
search_list: "allow",
|
|
48
|
-
bash_safe: "allow",
|
|
49
|
-
bash_unsafe: "allow",
|
|
50
|
-
web: "ask",
|
|
51
|
-
task_agent: "allow"
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe("claudeCodeWriter", () => {
|
|
58
|
-
let dir: string;
|
|
59
|
-
|
|
60
|
-
beforeEach(async () => {
|
|
61
|
-
dir = await mkdtemp(join(tmpdir(), "ade-harness-cc-"));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(async () => {
|
|
65
|
-
await rm(dir, { recursive: true, force: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("has correct metadata", () => {
|
|
69
|
-
expect(claudeCodeWriter.id).toBe("claude-code");
|
|
70
|
-
expect(claudeCodeWriter.label).toBe("Claude Code");
|
|
71
|
-
expect(claudeCodeWriter.description).toBeTruthy();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("writes .claude/agents/ade.md custom agent", async () => {
|
|
75
|
-
const config: LogicalConfig = {
|
|
76
|
-
mcp_servers: [
|
|
77
|
-
{
|
|
78
|
-
ref: "workflows",
|
|
79
|
-
command: "npx",
|
|
80
|
-
args: ["-y", "@codemcp/workflows"],
|
|
81
|
-
env: {}
|
|
82
|
-
}
|
|
83
|
-
],
|
|
84
|
-
instructions: ["Use workflow files.", "Follow conventions."],
|
|
85
|
-
cli_actions: [],
|
|
86
|
-
knowledge_sources: [],
|
|
87
|
-
skills: [],
|
|
88
|
-
git_hooks: [],
|
|
89
|
-
setup_notes: []
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
await claudeCodeWriter.install(config, dir);
|
|
93
|
-
|
|
94
|
-
const content = await readFile(
|
|
95
|
-
join(dir, ".claude", "agents", "ade.md"),
|
|
96
|
-
"utf-8"
|
|
97
|
-
);
|
|
98
|
-
expect(content).toContain("name: ade");
|
|
99
|
-
expect(content).toContain("description:");
|
|
100
|
-
expect(content).toContain("Use workflow files.");
|
|
101
|
-
expect(content).toContain("Follow conventions.");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("writes .mcp.json with MCP servers", async () => {
|
|
105
|
-
const config: LogicalConfig = {
|
|
106
|
-
mcp_servers: [
|
|
107
|
-
{
|
|
108
|
-
ref: "@codemcp/workflows",
|
|
109
|
-
command: "npx",
|
|
110
|
-
args: ["-y", "@codemcp/workflows"],
|
|
111
|
-
env: {}
|
|
112
|
-
}
|
|
113
|
-
],
|
|
114
|
-
instructions: [],
|
|
115
|
-
cli_actions: [],
|
|
116
|
-
knowledge_sources: [],
|
|
117
|
-
skills: [],
|
|
118
|
-
git_hooks: [],
|
|
119
|
-
setup_notes: []
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
await claudeCodeWriter.install(config, dir);
|
|
123
|
-
|
|
124
|
-
const raw = await readFile(join(dir, ".mcp.json"), "utf-8");
|
|
125
|
-
const parsed = JSON.parse(raw);
|
|
126
|
-
expect(parsed.mcpServers["@codemcp/workflows"]).toEqual({
|
|
127
|
-
command: "npx",
|
|
128
|
-
args: ["-y", "@codemcp/workflows"]
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("forwards explicit MCP tool permissions using Claude rule names", async () => {
|
|
133
|
-
const config: LogicalConfig = {
|
|
134
|
-
mcp_servers: [
|
|
135
|
-
{
|
|
136
|
-
ref: "workflows",
|
|
137
|
-
command: "npx",
|
|
138
|
-
args: ["-y", "@codemcp/workflows"],
|
|
139
|
-
env: {},
|
|
140
|
-
allowedTools: ["use_skill", "whats_next"]
|
|
141
|
-
}
|
|
142
|
-
],
|
|
143
|
-
instructions: [],
|
|
144
|
-
cli_actions: [],
|
|
145
|
-
knowledge_sources: [],
|
|
146
|
-
skills: [],
|
|
147
|
-
git_hooks: [],
|
|
148
|
-
setup_notes: []
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
await claudeCodeWriter.install(config, dir);
|
|
152
|
-
|
|
153
|
-
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
154
|
-
const settings = JSON.parse(raw);
|
|
155
|
-
expect(settings.permissions.allow).toEqual(
|
|
156
|
-
expect.arrayContaining([
|
|
157
|
-
"mcp__workflows__use_skill",
|
|
158
|
-
"mcp__workflows__whats_next"
|
|
159
|
-
])
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("does not invent wildcard MCP permission rules", async () => {
|
|
164
|
-
const config: LogicalConfig = {
|
|
165
|
-
mcp_servers: [
|
|
166
|
-
{
|
|
167
|
-
ref: "workflows",
|
|
168
|
-
command: "npx",
|
|
169
|
-
args: ["-y", "@codemcp/workflows"],
|
|
170
|
-
env: {}
|
|
171
|
-
}
|
|
172
|
-
],
|
|
173
|
-
instructions: [],
|
|
174
|
-
cli_actions: [],
|
|
175
|
-
knowledge_sources: [],
|
|
176
|
-
skills: [],
|
|
177
|
-
git_hooks: [],
|
|
178
|
-
setup_notes: []
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
await claudeCodeWriter.install(config, dir);
|
|
182
|
-
|
|
183
|
-
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
184
|
-
const settings = JSON.parse(raw);
|
|
185
|
-
expect(settings.permissions.allow ?? []).toEqual([]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("keeps web on ask for rigid autonomy without broad built-in allows", async () => {
|
|
189
|
-
const config: LogicalConfig = {
|
|
190
|
-
mcp_servers: [],
|
|
191
|
-
instructions: [],
|
|
192
|
-
cli_actions: [],
|
|
193
|
-
knowledge_sources: [],
|
|
194
|
-
skills: [],
|
|
195
|
-
git_hooks: [],
|
|
196
|
-
setup_notes: [],
|
|
197
|
-
permission_policy: autonomyPolicy("rigid")
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
await claudeCodeWriter.install(config, dir);
|
|
201
|
-
|
|
202
|
-
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
203
|
-
const settings = JSON.parse(raw);
|
|
204
|
-
expect(settings.permissions.allow ?? []).toEqual([]);
|
|
205
|
-
expect(settings.permissions.ask).toEqual(
|
|
206
|
-
expect.arrayContaining(["WebFetch", "WebSearch"])
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("maps sensible-defaults to Claude built-in permission rules", async () => {
|
|
211
|
-
const config: LogicalConfig = {
|
|
212
|
-
mcp_servers: [],
|
|
213
|
-
instructions: [],
|
|
214
|
-
cli_actions: [],
|
|
215
|
-
knowledge_sources: [],
|
|
216
|
-
skills: [],
|
|
217
|
-
git_hooks: [],
|
|
218
|
-
setup_notes: [],
|
|
219
|
-
permission_policy: autonomyPolicy("sensible-defaults")
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
await claudeCodeWriter.install(config, dir);
|
|
223
|
-
|
|
224
|
-
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
225
|
-
const settings = JSON.parse(raw);
|
|
226
|
-
expect(settings.permissions.allow).toEqual(
|
|
227
|
-
expect.arrayContaining(["Read", "Edit", "Glob", "Grep", "TodoWrite"])
|
|
228
|
-
);
|
|
229
|
-
expect(settings.permissions.allow).not.toContain("Bash");
|
|
230
|
-
expect(settings.permissions.ask).toEqual(
|
|
231
|
-
expect.arrayContaining(["WebFetch", "WebSearch"])
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("maps max-autonomy to broad Claude built-in permission rules while preserving web ask", async () => {
|
|
236
|
-
const config: LogicalConfig = {
|
|
237
|
-
mcp_servers: [],
|
|
238
|
-
instructions: [],
|
|
239
|
-
cli_actions: [],
|
|
240
|
-
knowledge_sources: [],
|
|
241
|
-
skills: [],
|
|
242
|
-
git_hooks: [],
|
|
243
|
-
setup_notes: [],
|
|
244
|
-
permission_policy: autonomyPolicy("max-autonomy")
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
await claudeCodeWriter.install(config, dir);
|
|
248
|
-
|
|
249
|
-
const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
|
|
250
|
-
const settings = JSON.parse(raw);
|
|
251
|
-
expect(settings.permissions.allow).toEqual(
|
|
252
|
-
expect.arrayContaining([
|
|
253
|
-
"Read",
|
|
254
|
-
"Edit",
|
|
255
|
-
"Bash",
|
|
256
|
-
"Glob",
|
|
257
|
-
"Grep",
|
|
258
|
-
"TodoWrite"
|
|
259
|
-
])
|
|
260
|
-
);
|
|
261
|
-
expect(settings.permissions.ask).toEqual(
|
|
262
|
-
expect.arrayContaining(["WebFetch", "WebSearch"])
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("includes agentskills server from mcp_servers", async () => {
|
|
267
|
-
const config: LogicalConfig = {
|
|
268
|
-
mcp_servers: [
|
|
269
|
-
{
|
|
270
|
-
ref: "agentskills",
|
|
271
|
-
command: "npx",
|
|
272
|
-
args: ["-y", "@codemcp/skills-server"],
|
|
273
|
-
env: {}
|
|
274
|
-
}
|
|
275
|
-
],
|
|
276
|
-
instructions: [],
|
|
277
|
-
cli_actions: [],
|
|
278
|
-
knowledge_sources: [],
|
|
279
|
-
skills: [{ name: "my-skill", description: "A skill", body: "Do stuff." }],
|
|
280
|
-
git_hooks: [],
|
|
281
|
-
setup_notes: []
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
await claudeCodeWriter.install(config, dir);
|
|
285
|
-
|
|
286
|
-
const raw = await readFile(join(dir, ".mcp.json"), "utf-8");
|
|
287
|
-
const parsed = JSON.parse(raw);
|
|
288
|
-
expect(parsed.mcpServers["agentskills"]).toEqual({
|
|
289
|
-
command: "npx",
|
|
290
|
-
args: ["-y", "@codemcp/skills-server"]
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("writes inline SKILL.md files via writeInlineSkills", async () => {
|
|
295
|
-
const config: LogicalConfig = {
|
|
296
|
-
mcp_servers: [],
|
|
297
|
-
instructions: [],
|
|
298
|
-
cli_actions: [],
|
|
299
|
-
knowledge_sources: [],
|
|
300
|
-
skills: [
|
|
301
|
-
{
|
|
302
|
-
name: "tanstack-architecture",
|
|
303
|
-
description: "TanStack architecture conventions",
|
|
304
|
-
body: "# Architecture\n\nUse file-based routing."
|
|
305
|
-
}
|
|
306
|
-
],
|
|
307
|
-
git_hooks: [],
|
|
308
|
-
setup_notes: []
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
await writeInlineSkills(config, dir);
|
|
312
|
-
|
|
313
|
-
const skillMd = await readFile(
|
|
314
|
-
join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"),
|
|
315
|
-
"utf-8"
|
|
316
|
-
);
|
|
317
|
-
expect(skillMd).toContain("name: tanstack-architecture");
|
|
318
|
-
expect(skillMd).toContain("# Architecture");
|
|
319
|
-
});
|
|
320
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
3
|
-
import type { HarnessWriter } from "../types.js";
|
|
4
|
-
import {
|
|
5
|
-
readJsonOrEmpty,
|
|
6
|
-
writeJson,
|
|
7
|
-
writeMcpServers,
|
|
8
|
-
writeAgentMd,
|
|
9
|
-
writeGitHooks
|
|
10
|
-
} from "../util.js";
|
|
11
|
-
import { allowsCapability, keepsWebOnAsk } from "../permission-policy.js";
|
|
12
|
-
|
|
13
|
-
export const claudeCodeWriter: HarnessWriter = {
|
|
14
|
-
id: "claude-code",
|
|
15
|
-
label: "Claude Code",
|
|
16
|
-
description:
|
|
17
|
-
"Anthropic's CLI agent — .claude/agents/ade.md + .mcp.json + .claude/settings.json",
|
|
18
|
-
async install(config: LogicalConfig, projectRoot: string) {
|
|
19
|
-
await writeAgentMd(config, {
|
|
20
|
-
path: join(projectRoot, ".claude", "agents", "ade.md"),
|
|
21
|
-
fallbackBody: "ADE — Agentic Development Environment agent."
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
await writeMcpServers(config.mcp_servers, {
|
|
25
|
-
path: join(projectRoot, ".mcp.json")
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await writeClaudeSettings(config, projectRoot);
|
|
29
|
-
await writeGitHooks(config.git_hooks, projectRoot);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
async function writeClaudeSettings(
|
|
34
|
-
config: LogicalConfig,
|
|
35
|
-
projectRoot: string
|
|
36
|
-
): Promise<void> {
|
|
37
|
-
const settingsPath = join(projectRoot, ".claude", "settings.json");
|
|
38
|
-
const existing = await readJsonOrEmpty(settingsPath);
|
|
39
|
-
const existingPerms = (existing.permissions as Record<string, unknown>) ?? {};
|
|
40
|
-
const existingAllow = asStringArray(existingPerms.allow);
|
|
41
|
-
const existingAsk = asStringArray(existingPerms.ask);
|
|
42
|
-
|
|
43
|
-
const autonomyRules = getClaudeAutonomyRules(config);
|
|
44
|
-
const mcpRules = getClaudeMcpAllowRules(config);
|
|
45
|
-
const allowRules = [
|
|
46
|
-
...new Set([...existingAllow, ...autonomyRules.allow, ...mcpRules])
|
|
47
|
-
];
|
|
48
|
-
const askRules = [...new Set([...existingAsk, ...autonomyRules.ask])];
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
allowRules.length === 0 &&
|
|
52
|
-
askRules.length === 0 &&
|
|
53
|
-
config.mcp_servers.length === 0
|
|
54
|
-
) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await writeJson(settingsPath, {
|
|
59
|
-
...existing,
|
|
60
|
-
permissions: {
|
|
61
|
-
...existingPerms,
|
|
62
|
-
...(allowRules.length > 0 ? { allow: allowRules } : {}),
|
|
63
|
-
...(askRules.length > 0 ? { ask: askRules } : {})
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function asStringArray(value: unknown): string[] {
|
|
69
|
-
return Array.isArray(value)
|
|
70
|
-
? value.filter((entry): entry is string => typeof entry === "string")
|
|
71
|
-
: [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function getClaudeMcpAllowRules(config: LogicalConfig): string[] {
|
|
75
|
-
const allowRules: string[] = [];
|
|
76
|
-
|
|
77
|
-
for (const server of config.mcp_servers) {
|
|
78
|
-
const allowedTools = server.allowedTools;
|
|
79
|
-
if (!allowedTools || allowedTools.includes("*")) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const tool of allowedTools) {
|
|
84
|
-
allowRules.push(`mcp__${server.ref}__${tool}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return allowRules;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getClaudeAutonomyRules(config: LogicalConfig): {
|
|
92
|
-
allow: string[];
|
|
93
|
-
ask: string[];
|
|
94
|
-
} {
|
|
95
|
-
const ask = keepsWebOnAsk(config) ? ["WebFetch", "WebSearch"] : [];
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
allow: [
|
|
99
|
-
...(allowsCapability(config, "read") ? ["Read"] : []),
|
|
100
|
-
...(allowsCapability(config, "edit_write") ? ["Edit"] : []),
|
|
101
|
-
...(allowsCapability(config, "search_list") ? ["Glob", "Grep"] : []),
|
|
102
|
-
...(allowsCapability(config, "bash_unsafe") ? ["Bash"] : []),
|
|
103
|
-
...(allowsCapability(config, "task_agent") ? ["TodoWrite"] : [])
|
|
104
|
-
],
|
|
105
|
-
ask
|
|
106
|
-
};
|
|
107
|
-
}
|