@aipermission/mcp 0.2.38 → 0.2.39

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 CHANGED
@@ -18,6 +18,10 @@ target/profile/action permission model as future connectors.
18
18
 
19
19
  `@aipermission/mcp` is the official MCP bridge package. The unscoped `aipermission` npm package is only a placeholder that points users here.
20
20
 
21
+ The bridge requires Node.js 20+ and npm/npx in the AI client's child-process
22
+ environment. GUI clients and native Windows shells can use a different `PATH`
23
+ from your interactive terminal.
24
+
21
25
  The package includes MCP Registry metadata:
22
26
 
23
27
  - `mcpName` in `package.json`
@@ -26,14 +30,37 @@ The package includes MCP Registry metadata:
26
30
  ## Install
27
31
 
28
32
  ```bash
29
- npx -y @aipermission/mcp init \
33
+ npx -y @aipermission/mcp setup \
30
34
  --provider codex \
35
+ --scope user \
31
36
  --name aipermission
32
37
  ```
33
38
 
34
- The init command prompts for your AIPermission API token and writes the MCP client configuration for the selected provider. Generated runtime configs pin the exact package version that wrote them; re-run init when you intentionally upgrade a client.
39
+ The setup command prompts for your AIPermission API token, writes the MCP client
40
+ configuration, and installs the native operator skill for the selected client.
41
+ Generated runtime configs pin the exact package version that wrote them; re-run
42
+ setup when you intentionally upgrade a client. Use `init` when you only want the
43
+ MCP config, or `install-skill` when you only want the skill.
44
+
45
+ Check both paths without printing the bearer token:
46
+
47
+ ```bash
48
+ npx -y @aipermission/mcp doctor --client codex --scope user
49
+ ```
35
50
 
36
- The generated MCP config contains a bearer token. Keep it private. For project-local configs such as `.mcp.json`, `.cursor/mcp.json`, and `.vscode/mcp.json`, the init command refuses to write into files already tracked by Git unless `--force` is passed. For untracked project-local configs, it adds both the final file and its crash-safe temporary-file pattern to `.git/info/exclude` before writing. Symbolic-link config destinations are rejected instead of being silently replaced; use `--print` and update a symlink-managed config through its owning tool. If a token config is committed or shared, revoke that token in the AIPermission UI.
51
+ The generated MCP config contains a bearer token. Keep it private. For
52
+ project-local configs such as `.mcp.json`, `.cursor/mcp.json`, and
53
+ `.vscode/mcp.json`, setup refuses to write into files already tracked by Git
54
+ unless `--force` is passed. For untracked project-local configs, it locally
55
+ excludes and verifies four paths: the final config, crash-safe temporary-file
56
+ pattern, Windows staging-directory pattern, and update lock. Config and skill
57
+ destinations reject symbolic links or junctions rather than following them.
58
+ Use `init --print` and update a link-managed config through its owning tool.
59
+ Print mode does not read or print a bearer token: it emits `YOUR_TOKEN_HERE`,
60
+ writes no config or skill, and requires you to replace the placeholder through
61
+ the client's private environment or config mechanism. Install the skill
62
+ separately when needed. If a token config is committed or shared, revoke that
63
+ token in the AIPermission UI.
37
64
 
38
65
  ## Manual Config
39
66
 
@@ -42,7 +69,7 @@ The generated MCP config contains a bearer token. Keep it private. For project-l
42
69
  "mcpServers": {
43
70
  "aipermission": {
44
71
  "command": "npx",
45
- "args": ["-y", "@aipermission/mcp@0.2.38"],
72
+ "args": ["-y", "@aipermission/mcp@0.2.39"],
46
73
  "env": {
47
74
  "NODE_ENV": "production",
48
75
  "AIPERMISSION_API_URL": "http://localhost:3210",
@@ -53,6 +80,9 @@ The generated MCP config contains a bearer token. Keep it private. For project-l
53
80
  }
54
81
  ```
55
82
 
83
+ Replace `VERSION` with the release you intend to pin. The setup command does
84
+ this automatically.
85
+
56
86
  ## Tools
57
87
 
58
88
  - `list_connector_targets`
@@ -162,30 +192,42 @@ temporary paths, archive staging paths, or local upload contents.
162
192
 
163
193
  ## Operator Skill
164
194
 
165
- Install the optional AIPermission operator instructions for your AI client:
195
+ Install only the AIPermission native operator skill for your AI client:
166
196
 
167
197
  ```bash
168
- npx -y @aipermission/mcp install-skill --client codex
198
+ npx -y @aipermission/mcp install-skill --client codex --scope user
169
199
  ```
170
200
 
171
201
  Supported clients:
172
202
 
173
- - `codex`: `~/.codex/skills/aipermission-operator/SKILL.md`
174
- - `claude-code`: `.claude/rules/aipermission-operator.md`
175
- - `cursor`: `.cursor/rules/aipermission-operator.mdc`
176
- - `vscode`: `.github/instructions/aipermission-operator.instructions.md`
177
- - `windsurf`: `.windsurf/rules/aipermission-operator.md`
178
- - `antigravity`: `.agents/rules/aipermission-operator.md`
179
- - `gemini`: `GEMINI.md`
180
- - `custom`: prints portable Markdown to stdout
203
+ - `codex`: `~/.agents/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
204
+ - `claude-code`: `~/.claude/skills/.../SKILL.md` or `.claude/skills/.../SKILL.md`
205
+ - `cursor`: `~/.cursor/skills/.../SKILL.md` or `.cursor/skills/.../SKILL.md`
206
+ - `vscode` / `copilot`: `~/.copilot/skills/.../SKILL.md` or `.github/skills/.../SKILL.md`
207
+ - `windsurf`: `~/.codeium/windsurf/skills/.../SKILL.md` or `.windsurf/skills/.../SKILL.md`
208
+ - `antigravity`: `~/.gemini/config/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
209
+ - `antigravity-cli`: `~/.gemini/antigravity-cli/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
210
+ - `gemini`: `~/.gemini/skills/.../SKILL.md` or `.gemini/skills/.../SKILL.md`
211
+ - `grok`: `~/.grok/skills/.../SKILL.md` or `.grok/skills/.../SKILL.md`
212
+ - `agents`: `~/.agents/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
213
+ - `custom`: prints the canonical skill Markdown to stdout
181
214
 
182
215
  These instructions teach the agent how to discover connector targets, poll
183
216
  `approval_pending` and `running` connector action requests, handle `stale`
184
217
  approvals by sending a fresh request, avoid blind retries for `outcome_unknown`,
185
218
  write short reasons, use explicit file transfer paths, and avoid printing
186
219
  secrets. The default installer uses the
187
- operator instruction bundled in the npm package; `--source` accepts local file
188
- paths only and rejects HTTP(S) sources.
220
+ operator skill bundled in the npm package; `--source` accepts local file paths
221
+ only and rejects HTTP(S) sources. The MCP server also publishes a concise
222
+ instruction summary during initialization for clients that surface protocol
223
+ instructions.
224
+
225
+ Client-specific home overrides are honored where the client documents them:
226
+ `CODEX_HOME` for Codex MCP config, `CLAUDE_CONFIG_DIR` for Claude user skills,
227
+ `COPILOT_HOME` for Copilot config and skills, `GEMINI_CLI_HOME` for Gemini
228
+ config and skills, and `GROK_HOME` for Grok config and skills. An explicit
229
+ `--home` remains the base home directory and takes precedence over those
230
+ environment overrides.
189
231
 
190
232
  ## Security Boundary
191
233
 
@@ -0,0 +1,81 @@
1
+ const commonPathFlags = {
2
+ home: "value",
3
+ "project-dir": "value",
4
+ };
5
+
6
+ const schemas = {
7
+ init: {
8
+ provider: "value",
9
+ scope: "value",
10
+ name: "value",
11
+ "api-url": "value",
12
+ "token-stdin": "boolean",
13
+ print: "boolean",
14
+ force: "boolean",
15
+ ...commonPathFlags,
16
+ },
17
+ setup: {
18
+ provider: "value",
19
+ scope: "value",
20
+ "skill-scope": "value",
21
+ name: "value",
22
+ "api-url": "value",
23
+ "token-stdin": "boolean",
24
+ print: "boolean",
25
+ force: "boolean",
26
+ "skill-source": "value",
27
+ ...commonPathFlags,
28
+ },
29
+ "install-skill": {
30
+ client: "value",
31
+ scope: "value",
32
+ source: "value",
33
+ ...commonPathFlags,
34
+ },
35
+ doctor: {
36
+ client: "value",
37
+ provider: "value",
38
+ scope: "value",
39
+ "mcp-scope": "value",
40
+ "skill-scope": "value",
41
+ name: "value",
42
+ ...commonPathFlags,
43
+ },
44
+ };
45
+
46
+ export function parseCommandFlags(command, argv = []) {
47
+ const schema = schemas[command];
48
+ if (!schema) throw new Error(`Unknown command schema: ${command}`);
49
+ const result = {};
50
+ for (let index = 0; index < argv.length; index += 1) {
51
+ const argument = argv[index];
52
+ if (!argument.startsWith("--")) throw new Error(`${command} does not accept positional argument: ${argument}`);
53
+ const separator = argument.indexOf("=");
54
+ const rawKey = argument.slice(2, separator < 0 ? undefined : separator);
55
+ if (!rawKey) throw new Error(`${command} received an empty option name`);
56
+ if (rawKey === "token") throw new Error("--token is not supported; use the hidden prompt or --token-stdin");
57
+ const kind = schema[rawKey];
58
+ if (!kind) throw new Error(`Unknown ${command} option: --${rawKey}`);
59
+ const key = camelCase(rawKey);
60
+ if (Object.hasOwn(result, key)) throw new Error(`Duplicate ${command} option: --${rawKey}`);
61
+ const inlineValue = separator < 0 ? undefined : argument.slice(separator + 1);
62
+ if (kind === "boolean") {
63
+ result[key] = parseBooleanOption(rawKey, inlineValue);
64
+ continue;
65
+ }
66
+ const value = inlineValue === undefined ? argv[++index] : inlineValue;
67
+ if (!value || value.startsWith("--")) throw new Error(`--${rawKey} requires a non-empty value`);
68
+ result[key] = value;
69
+ }
70
+ return result;
71
+ }
72
+
73
+ function parseBooleanOption(key, value) {
74
+ if (value === undefined || value === "true") return true;
75
+ if (value === "false") return false;
76
+ throw new Error(`--${key} accepts only true or false`);
77
+ }
78
+
79
+ function camelCase(value) {
80
+ return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
81
+ }
package/dist/cli.js CHANGED
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { getClientCatalog } from "./client-registry.js";
4
+
3
5
  const command = process.argv[2] || "serve";
4
6
 
5
- if (command === "init") {
6
- const { runInit } = await import("./init.js");
7
- await runInit(process.argv.slice(3));
7
+ if (command === "init" || command === "setup") {
8
+ const { runInit, runSetup } = await import("./init.js");
9
+ await (command === "setup" ? runSetup : runInit)(process.argv.slice(3));
8
10
  } else if (command === "install-skill") {
9
11
  const { runInstallSkill } = await import("./install-skill.js");
10
12
  await runInstallSkill(process.argv.slice(3));
13
+ } else if (command === "doctor") {
14
+ const { runDoctor } = await import("./doctor.js");
15
+ const result = await runDoctor(process.argv.slice(3));
16
+ if (!result.ok) process.exitCode = 1;
11
17
  } else if (command === "serve" || command === "server" || command === "start") {
12
18
  await import("./server.js");
13
19
  } else if (command === "--help" || command === "-h" || command === "help") {
@@ -19,25 +25,50 @@ if (command === "init") {
19
25
  }
20
26
 
21
27
  function printHelp() {
28
+ const catalog = getClientCatalog();
29
+ const mcpClients = catalog.filter((client) => client.supportsMCP).map((client) => client.id);
30
+ const skillClients = catalog.filter((client) => client.supportsSkill).map((client) => client.id);
31
+ const doctorClients = catalog.filter((client) => client.supportsMCP && client.supportsSkill).map((client) => client.id);
22
32
  console.log(`aipermission MCP
23
33
 
24
34
  Usage:
25
35
  aipermission-mcp Start the MCP stdio server
26
36
  aipermission-mcp init Configure an AI client interactively
37
+ aipermission-mcp setup Configure MCP and install its native skill
27
38
  aipermission-mcp install-skill Install the operator skill for an AI client
39
+ aipermission-mcp doctor Check MCP config and native skill paths
28
40
 
29
41
  Init flags:
30
- --provider codex|claude-code|cursor|vscode|windsurf|antigravity|gemini|custom
42
+ --provider ${[...mcpClients, "custom"].join("|")}
43
+ --scope user|project
31
44
  --name aipermission
32
45
  --token-stdin
33
46
  --api-url http://localhost:3210
34
- --print
47
+ --print Print a token-placeholder preview; change no files
48
+ --force
49
+ --home /path/to/home
50
+ --project-dir /path/to/workspace
51
+
52
+ Setup-only flags:
53
+ --skill-scope user|project
54
+ --skill-source /path/to/SKILL.md Local file only
35
55
 
36
56
  Install skill flags:
37
- --client codex|claude-code|cursor|vscode|windsurf|antigravity|gemini|custom
57
+ --client ${[...skillClients, "custom"].join("|")}
58
+ --scope user|project
59
+ --home /path/to/home
38
60
  --project-dir /path/to/workspace
39
61
  --source /path/to/SKILL.md Local file only; HTTP(S) sources are rejected
40
62
 
63
+ Doctor flags:
64
+ --client ${doctorClients.join("|")}
65
+ --scope user|project
66
+ --mcp-scope user|project
67
+ --skill-scope user|project
68
+ --name aipermission
69
+ --home /path/to/home
70
+ --project-dir /path/to/workspace
71
+
41
72
  Security:
42
73
  Use the hidden token prompt or --token-stdin. AIPERMISSION_API_URL must point
43
74
  to localhost, 127.0.0.1, or [::1].
@@ -0,0 +1,281 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ const clients = [
5
+ client("codex", "OpenAI Codex", {
6
+ mcp: mcpConfig("user", {
7
+ user: tomlTarget("home", ".codex/config.toml", { homeEnv: "CODEX_HOME", envPath: "config.toml" }),
8
+ project: tomlTarget("project", ".codex/config.toml"),
9
+ }),
10
+ skill: skillConfig("user", {
11
+ user: skillTarget("home", ".agents/skills"),
12
+ project: skillTarget("project", ".agents/skills"),
13
+ }),
14
+ }),
15
+ client("claude-code", "Claude Code", {
16
+ aliases: ["claude", "claude_code"],
17
+ mcp: mcpConfig("project", {
18
+ user: jsonTarget("home", ".claude.json", "mcpServers"),
19
+ project: jsonTarget("project", ".mcp.json", "mcpServers"),
20
+ }),
21
+ skill: skillConfig("project", {
22
+ user: skillTarget("home", ".claude/skills", { homeEnv: "CLAUDE_CONFIG_DIR", envPath: "skills" }),
23
+ project: skillTarget("project", ".claude/skills"),
24
+ }),
25
+ }),
26
+ client("cursor", "Cursor", {
27
+ mcp: mcpConfig("project", {
28
+ user: jsonTarget("home", ".cursor/mcp.json", "mcpServers"),
29
+ project: jsonTarget("project", ".cursor/mcp.json", "mcpServers"),
30
+ }),
31
+ skill: skillConfig("project", {
32
+ user: skillTarget("home", ".cursor/skills"),
33
+ project: skillTarget("project", ".cursor/skills"),
34
+ }),
35
+ }),
36
+ client("vscode", "VS Code", {
37
+ aliases: ["vs-code"],
38
+ mcp: mcpConfig(
39
+ "project",
40
+ { project: jsonTarget("project", ".vscode/mcp.json", "servers") },
41
+ { printScopes: { user: virtualJSONTarget("servers") } },
42
+ ),
43
+ skill: skillConfig("project", {
44
+ user: skillTarget("home", ".copilot/skills"),
45
+ project: skillTarget("project", ".github/skills"),
46
+ }),
47
+ }),
48
+ client("copilot", "GitHub Copilot CLI", {
49
+ aliases: ["copilot-cli", "github-copilot"],
50
+ mcp: mcpConfig(
51
+ "user",
52
+ {
53
+ user: jsonTarget("home", ".copilot/mcp-config.json", "mcpServers", {
54
+ homeEnv: "COPILOT_HOME",
55
+ envPath: "mcp-config.json",
56
+ }),
57
+ project: jsonTarget("project", ".mcp.json", "mcpServers"),
58
+ },
59
+ { serverExtras: { type: "local", tools: ["*"] } },
60
+ ),
61
+ skill: skillConfig("user", {
62
+ user: skillTarget("home", ".copilot/skills", { homeEnv: "COPILOT_HOME", envPath: "skills" }),
63
+ project: skillTarget("project", ".github/skills"),
64
+ }),
65
+ }),
66
+ client("windsurf", "Windsurf Cascade (legacy)", {
67
+ aliases: ["windsurf-legacy", "cascade"],
68
+ mcp: mcpConfig("user", { user: jsonTarget("home", ".codeium/windsurf/mcp_config.json", "mcpServers") }),
69
+ skill: skillConfig("user", {
70
+ user: skillTarget("home", ".codeium/windsurf/skills"),
71
+ project: skillTarget("project", ".windsurf/skills"),
72
+ }),
73
+ }),
74
+ client("antigravity", "Google Antigravity IDE", {
75
+ aliases: ["google-antigravity"],
76
+ mcp: mcpConfig("user", {
77
+ user: jsonTarget("home", ".gemini/config/mcp_config.json", "mcpServers"),
78
+ project: jsonTarget("project", ".agents/mcp_config.json", "mcpServers"),
79
+ }),
80
+ skill: skillConfig("user", {
81
+ user: skillTarget("home", ".gemini/config/skills"),
82
+ project: skillTarget("project", ".agents/skills"),
83
+ }),
84
+ }),
85
+ client("antigravity-cli", "Google Antigravity CLI", {
86
+ aliases: ["agy"],
87
+ mcp: mcpConfig("user", {
88
+ user: jsonTarget("home", ".gemini/antigravity-cli/mcp_config.json", "mcpServers"),
89
+ project: jsonTarget("project", ".agents/mcp_config.json", "mcpServers"),
90
+ }),
91
+ skill: skillConfig("user", {
92
+ user: skillTarget("home", ".gemini/antigravity-cli/skills"),
93
+ project: skillTarget("project", ".agents/skills"),
94
+ }),
95
+ }),
96
+ client("gemini", "Gemini CLI", {
97
+ aliases: ["gemini-cli"],
98
+ mcp: mcpConfig("user", {
99
+ user: jsonTarget("home", ".gemini/settings.json", "mcpServers", {
100
+ homeEnv: "GEMINI_CLI_HOME",
101
+ envPath: "settings.json",
102
+ }),
103
+ project: jsonTarget("project", ".gemini/settings.json", "mcpServers"),
104
+ }),
105
+ skill: skillConfig("user", {
106
+ user: skillTarget("home", ".gemini/skills", { homeEnv: "GEMINI_CLI_HOME", envPath: "skills" }),
107
+ project: skillTarget("project", ".gemini/skills"),
108
+ }),
109
+ }),
110
+ client("grok", "Grok Build CLI", {
111
+ aliases: ["grok-build", "grok-cli"],
112
+ mcp: mcpConfig(
113
+ "user",
114
+ {
115
+ user: tomlTarget("home", ".grok/config.toml", { homeEnv: "GROK_HOME", envPath: "config.toml" }),
116
+ project: tomlTarget("project", ".grok/config.toml"),
117
+ },
118
+ { serverExtras: { startup_timeout_sec: 60 } },
119
+ ),
120
+ skill: skillConfig("user", {
121
+ user: skillTarget("home", ".grok/skills", { homeEnv: "GROK_HOME", envPath: "skills" }),
122
+ project: skillTarget("project", ".grok/skills"),
123
+ }),
124
+ }),
125
+ client("agents", "Agents standard", {
126
+ aliases: ["agents-standard"],
127
+ skill: skillConfig("project", {
128
+ user: skillTarget("home", ".agents/skills"),
129
+ project: skillTarget("project", ".agents/skills"),
130
+ }),
131
+ }),
132
+ client("custom", "Custom / copy-paste"),
133
+ ];
134
+
135
+ export const MCP_PROVIDERS = clients
136
+ .filter((entry) => entry.mcp || entry.id === "custom")
137
+ .map((entry) => ({ id: entry.id, label: entry.label, description: providerDescription(entry) }));
138
+
139
+ export function getClient(value) {
140
+ const normalized = normalizeClientID(value);
141
+ return clients.find((entry) => entry.id === normalized);
142
+ }
143
+
144
+ export function getClientCatalog() {
145
+ return clients.map(({ id, label, aliases, mcp, skill }) => ({
146
+ id,
147
+ label,
148
+ aliases: [...aliases],
149
+ supportsMCP: Boolean(mcp),
150
+ supportsSkill: Boolean(skill),
151
+ }));
152
+ }
153
+
154
+ export function adaptMCPServerConfig(clientValue, config) {
155
+ const clientEntry = getClient(clientValue);
156
+ return { ...config, ...(clientEntry.mcp?.serverExtras || {}) };
157
+ }
158
+
159
+ export function resolveSkillTarget(clientValue, scopeValue, options = {}) {
160
+ return resolveTarget(clientValue, "skill", scopeValue, options, "skill");
161
+ }
162
+
163
+ export function resolveMCPConfigTarget(clientValue, scopeValue, options = {}) {
164
+ return resolveTarget(clientValue, "mcp", scopeValue, options, "MCP config");
165
+ }
166
+
167
+ export function resolveMCPPrintTarget(clientValue, scopeValue, options = {}) {
168
+ const clientEntry = getClient(clientValue);
169
+ if (!clientEntry.mcp) throw new Error(`${clientEntry.label} does not have an MCP config format.`);
170
+ const scope = normalizeScope(scopeValue || clientEntry.mcp.defaultScope);
171
+ const target = clientEntry.mcp.scopes[scope] || clientEntry.mcp.printScopes?.[scope];
172
+ if (!target) return resolveMCPConfigTarget(clientValue, scope, options);
173
+ return resolvedTarget(clientEntry, target, scope, options);
174
+ }
175
+
176
+ export function normalizeClientID(value) {
177
+ const candidate = String(value || "")
178
+ .trim()
179
+ .toLowerCase();
180
+ const clientEntry = clients.find(
181
+ (entry) => entry.id === candidate || entry.label.toLowerCase() === candidate || entry.aliases.includes(candidate),
182
+ );
183
+ if (!clientEntry) throw new Error(`Unknown client: ${value}`);
184
+ return clientEntry.id;
185
+ }
186
+
187
+ export function clientLabel(value) {
188
+ return getClient(value).label;
189
+ }
190
+
191
+ export function normalizeScope(value) {
192
+ const scope = String(value || "")
193
+ .trim()
194
+ .toLowerCase();
195
+ if (scope !== "user" && scope !== "project") throw new Error(`Unknown scope: ${value}. Use user or project.`);
196
+ return scope;
197
+ }
198
+
199
+ function resolveTarget(clientValue, capability, scopeValue, options, capabilityLabel) {
200
+ const clientEntry = getClient(clientValue);
201
+ const config = clientEntry[capability];
202
+ if (!config) throw new Error(`${clientEntry.label} does not have an automatic ${capabilityLabel} target.`);
203
+ const scope = normalizeScope(scopeValue || config.defaultScope);
204
+ const target = config.scopes[scope];
205
+ if (!target) {
206
+ const supported = Object.keys(config.scopes).join(", ");
207
+ throw new Error(`${clientEntry.label} does not support ${scope} ${capabilityLabel} scope. Supported scopes: ${supported}.`);
208
+ }
209
+ return resolvedTarget(clientEntry, target, scope, options);
210
+ }
211
+
212
+ function resolvedTarget(clientEntry, target, scope, options) {
213
+ const usesHomeOverride =
214
+ target.base === "home" && !options.homeDir && Boolean(target.homeEnv && (options.env || process.env)[target.homeEnv]);
215
+ const root = target.base === "project" ? path.resolve(options.projectDir || process.cwd()) : resolveHomeRoot(target, options);
216
+ return {
217
+ ...target,
218
+ client: clientEntry.id,
219
+ label: clientEntry.label,
220
+ scope,
221
+ path: target.virtual ? "" : path.join(root, ...(usesHomeOverride ? target.envSegments : target.segments)),
222
+ trustedRoot: root,
223
+ projectConfig: target.base === "project",
224
+ };
225
+ }
226
+
227
+ function resolveHomeRoot(target, options) {
228
+ if (options.homeDir) return path.resolve(options.homeDir);
229
+ const overridden = target.homeEnv && (options.env || process.env)[target.homeEnv];
230
+ return path.resolve(overridden || os.homedir());
231
+ }
232
+
233
+ function client(id, label, options = {}) {
234
+ return { id, label, aliases: options.aliases || [], mcp: options.mcp || null, skill: options.skill || null };
235
+ }
236
+
237
+ function mcpConfig(defaultScope, scopes, options = {}) {
238
+ return { defaultScope, scopes, printScopes: options.printScopes || {}, serverExtras: options.serverExtras || {} };
239
+ }
240
+
241
+ function skillConfig(defaultScope, scopes) {
242
+ return { defaultScope, scopes };
243
+ }
244
+
245
+ function jsonTarget(base, relativePath, rootKey, options = {}) {
246
+ return target(base, relativePath, { ...options, format: "json", rootKey });
247
+ }
248
+
249
+ function virtualJSONTarget(rootKey) {
250
+ return { base: "home", segments: [], envSegments: [], format: "json", rootKey, virtual: true };
251
+ }
252
+
253
+ function tomlTarget(base, relativePath, options = {}) {
254
+ return target(base, relativePath, { ...options, format: "toml", rootKey: "mcp_servers" });
255
+ }
256
+
257
+ function skillTarget(base, relativePath, options = {}) {
258
+ const suffix = "aipermission-operator/SKILL.md";
259
+ return target(base, `${relativePath}/${suffix}`, {
260
+ ...options,
261
+ envPath: options.envPath ? `${options.envPath}/${suffix}` : undefined,
262
+ });
263
+ }
264
+
265
+ function target(base, relativePath, options = {}) {
266
+ return {
267
+ base,
268
+ segments: relativePath.split("/"),
269
+ homeEnv: options.homeEnv,
270
+ envSegments: (options.envPath || relativePath).split("/"),
271
+ format: options.format,
272
+ rootKey: options.rootKey,
273
+ };
274
+ }
275
+
276
+ function providerDescription(clientEntry) {
277
+ if (!clientEntry.mcp) return "Prints config snippets only";
278
+ const targetEntry = clientEntry.mcp.scopes[clientEntry.mcp.defaultScope];
279
+ const prefix = targetEntry.base === "home" ? "~/" : "./";
280
+ return `Writes ${prefix}${targetEntry.segments.join("/")} (${clientEntry.mcp.defaultScope})`;
281
+ }