@ryanreh99/skills-sync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/assets/contracts/build/bundle.schema.json +76 -0
  4. package/dist/assets/contracts/inputs/config.schema.json +13 -0
  5. package/dist/assets/contracts/inputs/mcp-servers.schema.json +56 -0
  6. package/dist/assets/contracts/inputs/pack-manifest.schema.json +33 -0
  7. package/dist/assets/contracts/inputs/pack-sources.schema.json +47 -0
  8. package/dist/assets/contracts/inputs/profile.schema.json +21 -0
  9. package/dist/assets/contracts/inputs/upstreams.schema.json +45 -0
  10. package/dist/assets/contracts/runtime/targets.schema.json +120 -0
  11. package/dist/assets/contracts/state/upstreams-lock.schema.json +38 -0
  12. package/dist/assets/manifests/targets.linux.json +27 -0
  13. package/dist/assets/manifests/targets.macos.json +27 -0
  14. package/dist/assets/manifests/targets.windows.json +27 -0
  15. package/dist/assets/seed/config.json +3 -0
  16. package/dist/assets/seed/packs/personal/mcp/servers.json +20 -0
  17. package/dist/assets/seed/packs/personal/pack.json +7 -0
  18. package/dist/assets/seed/packs/personal/sources.json +31 -0
  19. package/dist/assets/seed/profiles/personal.json +4 -0
  20. package/dist/assets/seed/upstreams.json +23 -0
  21. package/dist/cli.js +532 -0
  22. package/dist/index.js +27 -0
  23. package/dist/lib/adapters/claude.js +49 -0
  24. package/dist/lib/adapters/codex.js +239 -0
  25. package/dist/lib/adapters/common.js +114 -0
  26. package/dist/lib/adapters/copilot.js +53 -0
  27. package/dist/lib/adapters/cursor.js +53 -0
  28. package/dist/lib/adapters/gemini.js +52 -0
  29. package/dist/lib/agents.js +888 -0
  30. package/dist/lib/bindings.js +510 -0
  31. package/dist/lib/build.js +190 -0
  32. package/dist/lib/bundle.js +165 -0
  33. package/dist/lib/config.js +324 -0
  34. package/dist/lib/core.js +447 -0
  35. package/dist/lib/detect.js +56 -0
  36. package/dist/lib/doctor.js +504 -0
  37. package/dist/lib/init.js +292 -0
  38. package/dist/lib/inventory.js +235 -0
  39. package/dist/lib/manage.js +463 -0
  40. package/dist/lib/mcp-config.js +264 -0
  41. package/dist/lib/profile-transfer.js +221 -0
  42. package/dist/lib/upstreams.js +782 -0
  43. package/docs/agent-storage-map.md +153 -0
  44. package/docs/architecture.md +117 -0
  45. package/docs/changelog.md +12 -0
  46. package/docs/commands.md +94 -0
  47. package/docs/contracts.md +112 -0
  48. package/docs/homebrew.md +46 -0
  49. package/docs/quickstart.md +14 -0
  50. package/docs/roadmap.md +5 -0
  51. package/docs/security.md +32 -0
  52. package/docs/user-guide.md +257 -0
  53. package/package.json +61 -0
@@ -0,0 +1,239 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { CODEX_MCP_BLOCK_END, CODEX_MCP_BLOCK_START, logWarn, readJsonFile } from "../core.js";
4
+ import { linkDirectoryProjection } from "./common.js";
5
+
6
+ function tomlString(value) {
7
+ return JSON.stringify(String(value ?? ""));
8
+ }
9
+
10
+ function tomlArray(values) {
11
+ const items = (Array.isArray(values) ? values : []).map((value) => tomlString(value));
12
+ return `[${items.join(", ")}]`;
13
+ }
14
+
15
+ function tomlInlineTable(values) {
16
+ const entries = values && typeof values === "object" && !Array.isArray(values) ? values : {};
17
+ const keys = Object.keys(entries).sort((left, right) => left.localeCompare(right));
18
+ const pairs = keys.map((key) => `${tomlTableKey(key)} = ${tomlString(entries[key])}`);
19
+ return `{ ${pairs.join(", ")} }`;
20
+ }
21
+
22
+ function tomlTableKey(key) {
23
+ return JSON.stringify(String(key));
24
+ }
25
+
26
+ function isTomlTableHeader(value) {
27
+ return /^\[\[?.+\]\]?$/.test(value);
28
+ }
29
+
30
+ function isCodexMcpTableHeader(value) {
31
+ return /^\[mcp_servers\./.test(value);
32
+ }
33
+
34
+ export async function discoverBundledSkills(bundleSkillsPath, currentPath = "", entries = []) {
35
+ const absolutePath = currentPath
36
+ ? path.join(bundleSkillsPath, currentPath.split("/").join(path.sep))
37
+ : bundleSkillsPath;
38
+ const children = await fs.readdir(absolutePath, { withFileTypes: true });
39
+ let hasSkill = false;
40
+ for (const child of children) {
41
+ if (child.isFile() && child.name === "SKILL.md") {
42
+ hasSkill = true;
43
+ break;
44
+ }
45
+ }
46
+ if (hasSkill && currentPath.length > 0) {
47
+ entries.push(currentPath);
48
+ }
49
+
50
+ const dirs = children
51
+ .filter((child) => child.isDirectory())
52
+ .map((child) => child.name)
53
+ .sort((left, right) => left.localeCompare(right));
54
+ for (const directory of dirs) {
55
+ const next = currentPath.length > 0 ? `${currentPath}/${directory}` : directory;
56
+ await discoverBundledSkills(bundleSkillsPath, next, entries);
57
+ }
58
+ return entries;
59
+ }
60
+
61
+ export function renderCodexConfigToml(canonicalMcp, bundledSkills = []) {
62
+ const servers = canonicalMcp?.mcpServers ?? {};
63
+ const names = Object.keys(servers).sort((left, right) => left.localeCompare(right));
64
+ const lines = [
65
+ "# Generated by skills-sync. Managed file.",
66
+ "# Source: ~/.skills-sync/internal/common/mcp.json",
67
+ 'model = "gpt-5-codex"',
68
+ 'approval_policy = "on-request"',
69
+ 'sandbox_mode = "workspace-write"',
70
+ ""
71
+ ];
72
+
73
+ const sortedSkills = [...bundledSkills].sort((left, right) => left.localeCompare(right));
74
+ for (const skill of sortedSkills) {
75
+ lines.push("[[skills.config]]");
76
+ lines.push(`path = ${tomlString(`~/.codex/skills/vendor_imports/${skill}`)}`);
77
+ lines.push("enabled = true");
78
+ lines.push("");
79
+ }
80
+
81
+ for (const name of names) {
82
+ const server = servers[name] ?? {};
83
+ lines.push(`[mcp_servers.${tomlTableKey(name)}]`);
84
+ if (typeof server.url === "string" && server.url.trim().length > 0) {
85
+ lines.push(`url = ${tomlString(server.url)}`);
86
+ } else {
87
+ lines.push('transport = "stdio"');
88
+ lines.push(`command = ${tomlString(server.command)}`);
89
+ lines.push(`args = ${tomlArray(server.args)}`);
90
+ if (server.env && typeof server.env === "object" && !Array.isArray(server.env) && Object.keys(server.env).length > 0) {
91
+ lines.push(`env = ${tomlInlineTable(server.env)}`);
92
+ }
93
+ }
94
+ lines.push("");
95
+ }
96
+
97
+ return `${lines.join("\n").trimEnd()}\n`;
98
+ }
99
+
100
+ export function renderCodexMcpTables(canonicalMcp) {
101
+ const servers = canonicalMcp?.mcpServers ?? {};
102
+ const names = Object.keys(servers).sort((left, right) => left.localeCompare(right));
103
+ const lines = [];
104
+
105
+ for (const name of names) {
106
+ const server = servers[name] ?? {};
107
+ lines.push(`[mcp_servers.${tomlTableKey(name)}]`);
108
+ if (typeof server.url === "string" && server.url.trim().length > 0) {
109
+ lines.push(`url = ${tomlString(server.url)}`);
110
+ } else {
111
+ lines.push('transport = "stdio"');
112
+ lines.push(`command = ${tomlString(server.command)}`);
113
+ lines.push(`args = ${tomlArray(server.args)}`);
114
+ if (server.env && typeof server.env === "object" && !Array.isArray(server.env) && Object.keys(server.env).length > 0) {
115
+ lines.push(`env = ${tomlInlineTable(server.env)}`);
116
+ }
117
+ }
118
+ lines.push("");
119
+ }
120
+
121
+ return `${lines.join("\n").trimEnd()}\n`;
122
+ }
123
+
124
+ export function extractCodexMcpTables(content) {
125
+ const normalized = String(content ?? "").replace(/\r\n/g, "\n");
126
+ const lines = normalized.split("\n");
127
+ const blocks = [];
128
+
129
+ let index = 0;
130
+ while (index < lines.length) {
131
+ const trimmed = lines[index].trim();
132
+ if (!isCodexMcpTableHeader(trimmed)) {
133
+ index += 1;
134
+ continue;
135
+ }
136
+
137
+ const block = [lines[index]];
138
+ index += 1;
139
+ while (index < lines.length) {
140
+ const candidate = lines[index].trim();
141
+ if (isTomlTableHeader(candidate)) {
142
+ break;
143
+ }
144
+ block.push(lines[index]);
145
+ index += 1;
146
+ }
147
+ blocks.push(block.join("\n").trimEnd());
148
+ }
149
+
150
+ if (blocks.length === 0) {
151
+ return "";
152
+ }
153
+ return `${blocks.join("\n\n").trimEnd()}\n`;
154
+ }
155
+
156
+ export function renderCodexConfigTomlFromLocal(localContent, canonicalMcp) {
157
+ const normalized = String(localContent ?? "").replace(/\r\n/g, "\n");
158
+ const lines = normalized.split("\n");
159
+ const preserved = [];
160
+
161
+ let index = 0;
162
+ while (index < lines.length) {
163
+ const trimmed = lines[index].trim();
164
+
165
+ if (trimmed === CODEX_MCP_BLOCK_START) {
166
+ index += 1;
167
+ while (index < lines.length && lines[index].trim() !== CODEX_MCP_BLOCK_END) {
168
+ index += 1;
169
+ }
170
+ if (index < lines.length) {
171
+ index += 1;
172
+ }
173
+ continue;
174
+ }
175
+
176
+ if (isCodexMcpTableHeader(trimmed)) {
177
+ index += 1;
178
+ while (index < lines.length) {
179
+ const candidate = lines[index].trim();
180
+ if (isTomlTableHeader(candidate)) {
181
+ break;
182
+ }
183
+ index += 1;
184
+ }
185
+ continue;
186
+ }
187
+
188
+ preserved.push(lines[index]);
189
+ index += 1;
190
+ }
191
+
192
+ const base = preserved.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
193
+ const mcpTables = renderCodexMcpTables(canonicalMcp).trimEnd();
194
+ return `${base.length > 0 ? `${base}\n\n` : ""}${mcpTables}\n`;
195
+ }
196
+
197
+ export async function projectCodexFromBundle(options) {
198
+ const {
199
+ runtimeInternalRoot,
200
+ bundleSkillsPath,
201
+ bundleMcpPath,
202
+ packRoot,
203
+ localConfigPath = null,
204
+ canOverride = false
205
+ } = options;
206
+ const runtimeRoot = path.join(runtimeInternalRoot, ".codex");
207
+ await fs.ensureDir(runtimeRoot);
208
+
209
+ const skillsMethod = await linkDirectoryProjection(bundleSkillsPath, path.join(runtimeRoot, "skills"));
210
+ await linkDirectoryProjection(bundleSkillsPath, path.join(runtimeRoot, "vendor_imports", "skills"));
211
+
212
+ const canonicalMcp = await readJsonFile(bundleMcpPath);
213
+ let codexToml = null;
214
+
215
+ if (!canOverride && localConfigPath && (await fs.pathExists(localConfigPath))) {
216
+ try {
217
+ const localContent = await fs.readFile(localConfigPath, "utf8");
218
+ codexToml = renderCodexConfigTomlFromLocal(localContent, canonicalMcp);
219
+ } catch (error) {
220
+ logWarn(`Failed to seed Codex runtime config from local settings: ${error.message}`);
221
+ }
222
+ }
223
+
224
+ if (!codexToml) {
225
+ const bundledSkills = await discoverBundledSkills(bundleSkillsPath);
226
+ codexToml = renderCodexConfigToml(canonicalMcp, bundledSkills);
227
+ }
228
+
229
+ const runtimeConfigPath = path.join(runtimeRoot, "config.toml");
230
+ await fs.writeFile(runtimeConfigPath, codexToml, "utf8");
231
+ const mcpMethod = "generated";
232
+
233
+ const overrideSource = path.join(packRoot, "tool-overrides", "codex");
234
+ if (await fs.pathExists(overrideSource)) {
235
+ await fs.copy(overrideSource, path.join(runtimeRoot, "tool-overrides"));
236
+ }
237
+
238
+ return { skillsMethod, mcpMethod };
239
+ }
@@ -0,0 +1,114 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { createDirectoryBinding, createFileBinding, detectOsName } from "../core.js";
4
+
5
+ export async function linkDirectoryProjection(sourcePath, targetPath) {
6
+ await fs.ensureDir(path.dirname(targetPath));
7
+ if (await fs.pathExists(targetPath)) {
8
+ await fs.remove(targetPath);
9
+ }
10
+
11
+ try {
12
+ const osName = detectOsName();
13
+ return await createDirectoryBinding(sourcePath, targetPath, osName);
14
+ } catch {
15
+ await fs.copy(sourcePath, targetPath);
16
+ return "copy";
17
+ }
18
+ }
19
+
20
+ export async function linkFileProjection(sourcePath, targetPath) {
21
+ await fs.ensureDir(path.dirname(targetPath));
22
+ if (await fs.pathExists(targetPath)) {
23
+ await fs.remove(targetPath);
24
+ }
25
+
26
+ try {
27
+ const osName = detectOsName();
28
+ const binding = await createFileBinding(sourcePath, targetPath, osName);
29
+ return binding.method;
30
+ } catch {
31
+ await fs.copyFile(sourcePath, targetPath);
32
+ return "copy";
33
+ }
34
+ }
35
+
36
+ export async function discoverBundledSkills(bundleSkillsPath, currentPath = "", entries = []) {
37
+ const absolutePath = currentPath
38
+ ? path.join(bundleSkillsPath, currentPath.split("/").join(path.sep))
39
+ : bundleSkillsPath;
40
+ const children = await fs.readdir(absolutePath, { withFileTypes: true });
41
+
42
+ let hasSkill = false;
43
+ for (const child of children) {
44
+ if (child.isFile() && child.name === "SKILL.md") {
45
+ hasSkill = true;
46
+ break;
47
+ }
48
+ }
49
+ if (hasSkill && currentPath.length > 0) {
50
+ entries.push(currentPath);
51
+ }
52
+
53
+ const dirs = children
54
+ .filter((child) => child.isDirectory())
55
+ .map((child) => child.name)
56
+ .sort((left, right) => left.localeCompare(right));
57
+ for (const directory of dirs) {
58
+ const next = currentPath.length > 0 ? `${currentPath}/${directory}` : directory;
59
+ await discoverBundledSkills(bundleSkillsPath, next, entries);
60
+ }
61
+ return entries;
62
+ }
63
+
64
+ function nextVendorAliasName(relativeSkillPath, usedNames) {
65
+ const flattened = relativeSkillPath.split("/").join("__");
66
+ const base = `vendor__${flattened}`;
67
+ let candidate = base;
68
+ let suffix = 2;
69
+ while (usedNames.has(candidate)) {
70
+ candidate = `${base}__${suffix}`;
71
+ suffix += 1;
72
+ }
73
+ return candidate;
74
+ }
75
+
76
+ export async function projectTopLevelSkills(bundleSkillsPath, runtimeSkillsPath) {
77
+ await fs.copy(bundleSkillsPath, runtimeSkillsPath);
78
+ const discovered = await discoverBundledSkills(bundleSkillsPath);
79
+ const topLevelEntries = await fs.readdir(runtimeSkillsPath, { withFileTypes: true });
80
+ const usedNames = new Set(topLevelEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
81
+
82
+ for (const relativeSkillPath of discovered) {
83
+ if (!relativeSkillPath.includes("/")) {
84
+ continue;
85
+ }
86
+ const sourceSkillPath = path.join(bundleSkillsPath, relativeSkillPath.split("/").join(path.sep));
87
+ const aliasName = nextVendorAliasName(relativeSkillPath, usedNames);
88
+ await linkDirectoryProjection(sourceSkillPath, path.join(runtimeSkillsPath, aliasName));
89
+ usedNames.add(aliasName);
90
+ }
91
+ }
92
+
93
+ export async function projectCommonFromBundle({ runtimeInternalRoot, bundleSkillsPath, bundleMcpPath }) {
94
+ const commonRoot = path.join(runtimeInternalRoot, "common");
95
+ await fs.ensureDir(commonRoot);
96
+ const skillsMethod = await linkDirectoryProjection(bundleSkillsPath, path.join(commonRoot, "skills"));
97
+ const mcpMethod = await linkFileProjection(bundleMcpPath, path.join(commonRoot, "mcp.json"));
98
+ return { skillsMethod, mcpMethod };
99
+ }
100
+
101
+ export async function projectToolFromBundle({ tool, runtimeInternalRoot, bundleSkillsPath, bundleMcpPath, packRoot }) {
102
+ const toolDist = path.join(runtimeInternalRoot, tool);
103
+ await fs.ensureDir(toolDist);
104
+
105
+ const skillsMethod = await linkDirectoryProjection(bundleSkillsPath, path.join(toolDist, "skills"));
106
+ const mcpMethod = await linkFileProjection(bundleMcpPath, path.join(toolDist, "mcp.json"));
107
+
108
+ const overrideSource = path.join(packRoot, "tool-overrides", tool);
109
+ if (await fs.pathExists(overrideSource)) {
110
+ await fs.copy(overrideSource, path.join(toolDist, "tool-overrides"));
111
+ }
112
+
113
+ return { skillsMethod, mcpMethod };
114
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { logWarn, readJsonFile } from "../core.js";
4
+ import { projectTopLevelSkills } from "./common.js";
5
+
6
+ export async function projectCopilotFromBundle(options) {
7
+ const {
8
+ runtimeInternalRoot,
9
+ bundleSkillsPath,
10
+ bundleMcpPath,
11
+ packRoot,
12
+ localConfigPath = null,
13
+ canOverride = false
14
+ } = options;
15
+ const runtimeRoot = path.join(runtimeInternalRoot, ".copilot");
16
+ await fs.ensureDir(runtimeRoot);
17
+
18
+ const runtimeSkillsPath = path.join(runtimeRoot, "skills");
19
+ await fs.remove(runtimeSkillsPath);
20
+ await projectTopLevelSkills(bundleSkillsPath, runtimeSkillsPath);
21
+ const skillsMethod = "copy+aliases";
22
+
23
+ const canonicalMcp = await readJsonFile(bundleMcpPath);
24
+ let projected = canonicalMcp;
25
+
26
+ if (!canOverride && localConfigPath && (await fs.pathExists(localConfigPath))) {
27
+ try {
28
+ const existing = await readJsonFile(localConfigPath);
29
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
30
+ throw new Error("expected JSON object root");
31
+ }
32
+ projected = {
33
+ ...existing,
34
+ mcpServers: canonicalMcp?.mcpServers ?? {}
35
+ };
36
+ } catch (error) {
37
+ logWarn(`Failed to seed Copilot runtime config from local settings: ${error.message}`);
38
+ }
39
+ }
40
+
41
+ const runtimeConfigPath = path.join(runtimeRoot, "mcp-config.json");
42
+ await fs.writeFile(runtimeConfigPath, `${JSON.stringify(projected, null, 2)}\n`, "utf8");
43
+
44
+ const overrideSource = path.join(packRoot, "tool-overrides", "copilot");
45
+ if (await fs.pathExists(overrideSource)) {
46
+ await fs.copy(overrideSource, path.join(runtimeRoot, "tool-overrides"));
47
+ }
48
+
49
+ return {
50
+ skillsMethod,
51
+ mcpMethod: "generated"
52
+ };
53
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { logWarn, readJsonFile } from "../core.js";
4
+ import { projectTopLevelSkills } from "./common.js";
5
+
6
+ export async function projectCursorFromBundle(options) {
7
+ const {
8
+ runtimeInternalRoot,
9
+ bundleSkillsPath,
10
+ bundleMcpPath,
11
+ packRoot,
12
+ localConfigPath = null,
13
+ canOverride = false
14
+ } = options;
15
+ const runtimeRoot = path.join(runtimeInternalRoot, ".cursor");
16
+ await fs.ensureDir(runtimeRoot);
17
+
18
+ const runtimeSkillsPath = path.join(runtimeRoot, "skills");
19
+ await fs.remove(runtimeSkillsPath);
20
+ await projectTopLevelSkills(bundleSkillsPath, runtimeSkillsPath);
21
+ const skillsMethod = "copy+aliases";
22
+
23
+ const canonicalMcp = await readJsonFile(bundleMcpPath);
24
+ let projected = canonicalMcp;
25
+
26
+ if (!canOverride && localConfigPath && (await fs.pathExists(localConfigPath))) {
27
+ try {
28
+ const existing = await readJsonFile(localConfigPath);
29
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
30
+ throw new Error("expected JSON object root");
31
+ }
32
+ projected = {
33
+ ...existing,
34
+ mcpServers: canonicalMcp?.mcpServers ?? {}
35
+ };
36
+ } catch (error) {
37
+ logWarn(`Failed to seed Cursor runtime config from local settings: ${error.message}`);
38
+ }
39
+ }
40
+
41
+ const runtimeConfigPath = path.join(runtimeRoot, "mcp.json");
42
+ await fs.writeFile(runtimeConfigPath, `${JSON.stringify(projected, null, 2)}\n`, "utf8");
43
+
44
+ const overrideSource = path.join(packRoot, "tool-overrides", "cursor");
45
+ if (await fs.pathExists(overrideSource)) {
46
+ await fs.copy(overrideSource, path.join(runtimeRoot, "tool-overrides"));
47
+ }
48
+
49
+ return {
50
+ skillsMethod,
51
+ mcpMethod: "generated"
52
+ };
53
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { logWarn, readJsonFile } from "../core.js";
4
+ import { linkDirectoryProjection, projectTopLevelSkills } from "./common.js";
5
+
6
+ export async function projectGeminiFromBundle(options) {
7
+ const {
8
+ runtimeInternalRoot,
9
+ bundleSkillsPath,
10
+ bundleMcpPath,
11
+ packRoot,
12
+ localConfigPath = null,
13
+ canOverride = false
14
+ } = options;
15
+ const runtimeRoot = path.join(runtimeInternalRoot, ".gemini");
16
+ await fs.ensureDir(runtimeRoot);
17
+
18
+ const runtimeSkillsPath = path.join(runtimeRoot, "skills");
19
+ await fs.remove(runtimeSkillsPath);
20
+ await projectTopLevelSkills(bundleSkillsPath, runtimeSkillsPath);
21
+ const skillsMethod = "copy+aliases";
22
+ await linkDirectoryProjection(bundleSkillsPath, path.join(runtimeRoot, "vendor_imports", "skills"));
23
+
24
+ const canonicalMcp = await readJsonFile(bundleMcpPath);
25
+ let projected = canonicalMcp;
26
+
27
+ if (!canOverride && localConfigPath && (await fs.pathExists(localConfigPath))) {
28
+ try {
29
+ const existing = await readJsonFile(localConfigPath);
30
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
31
+ throw new Error("expected JSON object root");
32
+ }
33
+ projected = {
34
+ ...existing,
35
+ mcpServers: canonicalMcp?.mcpServers ?? {}
36
+ };
37
+ } catch (error) {
38
+ logWarn(`Failed to seed Gemini runtime config from local settings: ${error.message}`);
39
+ }
40
+ }
41
+
42
+ const runtimeSettingsPath = path.join(runtimeRoot, "settings.json");
43
+ await fs.writeFile(runtimeSettingsPath, `${JSON.stringify(projected, null, 2)}\n`, "utf8");
44
+ const mcpMethod = "generated";
45
+
46
+ const overrideSource = path.join(packRoot, "tool-overrides", "gemini");
47
+ if (await fs.pathExists(overrideSource)) {
48
+ await fs.copy(overrideSource, path.join(runtimeRoot, "tool-overrides"));
49
+ }
50
+
51
+ return { skillsMethod, mcpMethod };
52
+ }