@modootoday/datalab-extension-mcp 1.7.0 → 1.8.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 (47) hide show
  1. package/README.en.md +55 -9
  2. package/README.md +47 -9
  3. package/dist/browsers-KHRZKQVP.js +119 -0
  4. package/dist/{dist-WMNHKGZR.js → chunk-5S7RFXIU.js} +1032 -442
  5. package/dist/chunk-6JBLBCLU.js +17524 -0
  6. package/dist/chunk-NKUU3DAF.js +15888 -0
  7. package/dist/cli.js +64 -4
  8. package/dist/dist-JICXADYR.js +105 -0
  9. package/dist/index.d.ts +22 -4
  10. package/dist/index.js +2 -1
  11. package/dist/skills-ACWHUGX7.js +101 -0
  12. package/package.json +3 -5
  13. package/skills/datalab-ad-disclosure-check/SKILL.md +74 -0
  14. package/skills/datalab-ad-disclosure-check/references/ad-disclosure-rules.md +98 -0
  15. package/skills/datalab-ad-disclosure-check/references/naver-terms-excerpt.md +28 -0
  16. package/skills/datalab-blog-widget/SKILL.md +65 -0
  17. package/skills/datalab-blog-widget/references/naver-constraints.md +51 -0
  18. package/skills/datalab-blog-widget/references/templates.md +123 -0
  19. package/skills/datalab-card-news/SKILL.md +93 -0
  20. package/skills/datalab-card-news/references/slide-structure.md +48 -0
  21. package/skills/datalab-cta-rewrite/SKILL.md +55 -0
  22. package/skills/datalab-cta-rewrite/references/output-format.md +24 -0
  23. package/skills/datalab-cta-rewrite/references/principles.md +36 -0
  24. package/skills/datalab-material-suggestion/SKILL.md +70 -0
  25. package/skills/datalab-material-suggestion/references/discovery.md +47 -0
  26. package/skills/datalab-material-suggestion/references/output-format.md +41 -0
  27. package/skills/datalab-photo-prompt/SKILL.md +52 -0
  28. package/skills/datalab-photo-prompt/references/prompt-dimensions.md +43 -0
  29. package/skills/datalab-photo-prompt/references/style-vocabulary.md +35 -0
  30. package/skills/datalab-reader-simulation/SKILL.md +83 -0
  31. package/skills/datalab-reader-simulation/references/checklist-format.md +36 -0
  32. package/skills/datalab-research-brief/SKILL.md +77 -0
  33. package/skills/datalab-research-brief/references/brief-format.md +39 -0
  34. package/skills/datalab-social-repurpose/SKILL.md +70 -0
  35. package/skills/datalab-social-repurpose/references/output-format.md +43 -0
  36. package/skills/datalab-social-repurpose/references/platform-specs.md +44 -0
  37. package/skills/datalab-tone-manner/SKILL.md +59 -0
  38. package/skills/datalab-tone-manner/references/analysis-dimensions.md +51 -0
  39. package/skills/datalab-tone-manner/references/spec-format.md +58 -0
  40. package/skills/datalab-tutorial-post/SKILL.md +61 -0
  41. package/skills/datalab-tutorial-post/references/analogy-and-case-templates.md +37 -0
  42. package/skills/datalab-tutorial-post/references/structure-and-length.md +22 -0
  43. package/skills/datalab-tutorial-post/references/tone-and-hooks.md +26 -0
  44. package/skills/datalab-video-script/SKILL.md +84 -0
  45. package/skills/datalab-video-script/references/scene-structure.md +41 -0
  46. package/skills/datalab-video-script/references/writing-for-the-ear.md +22 -0
  47. package/dist/chunk-NYUIYZG6.js +0 -32468
@@ -1,6 +1,6 @@
1
- import "./chunk-MLKGABMK.js";
2
-
3
1
  // ../mcp-installer/dist/index.js
2
+ import { join as joinNative } from "path";
3
+ import { fileURLToPath } from "url";
4
4
  import { spawn as nodeSpawn } from "child_process";
5
5
  import { promises as fs } from "fs";
6
6
  import { homedir } from "os";
@@ -48,6 +48,16 @@ function buildEnv(opts) {
48
48
  }
49
49
  return env;
50
50
  }
51
+ function envPairs(opts) {
52
+ const pairs = [
53
+ `DATALAB_MCP_TOKEN=${opts.token}`,
54
+ `DATALAB_MCP_EXTENSION_ID=${opts.extensionId}`
55
+ ];
56
+ if (isCustomPort(opts.port)) {
57
+ pairs.push(`DATALAB_MCP_PORT=${opts.port}`);
58
+ }
59
+ return pairs;
60
+ }
51
61
  function daemonMcpUrl(opts) {
52
62
  const port = isCustomPort(opts.port) ? opts.port : DEFAULT_PORT;
53
63
  return `http://127.0.0.1:${port}/mcp`;
@@ -70,90 +80,6 @@ function buildFileEntry(opts, platform) {
70
80
  env: buildEnv(opts)
71
81
  };
72
82
  }
73
- var CODEX_CHATGPT_NOTE = "ChatGPT \uB370\uC2A4\uD06C\uD1B1\uACFC \uD568\uAED8 \uC5F0\uACB0\uB3FC\uC694.";
74
- function envPairs(opts) {
75
- const pairs = [
76
- `DATALAB_MCP_TOKEN=${opts.token}`,
77
- `DATALAB_MCP_EXTENSION_ID=${opts.extensionId}`
78
- ];
79
- if (isCustomPort(opts.port)) {
80
- pairs.push(`DATALAB_MCP_PORT=${opts.port}`);
81
- }
82
- return pairs;
83
- }
84
- var CLI_HOSTS = [
85
- {
86
- id: "claude",
87
- tier: 1,
88
- displayName: "Claude Code",
89
- bin: "claude",
90
- // 🔴 User scope is mandatory: the default local scope binds the server to
91
- // whatever directory the command was pasted in, which reads as success and
92
- // then does nothing everywhere else.
93
- buildAddArgs(opts) {
94
- const args = ["mcp", "add", SERVER_NAME, "--scope", "user"];
95
- for (const pair of envPairs(opts)) {
96
- args.push("--env", pair);
97
- }
98
- args.push("--", "npx", "-y", packageSpec(opts.version));
99
- return args;
100
- },
101
- buildRemoveArgs() {
102
- return ["mcp", "remove", SERVER_NAME, "--scope", "user"];
103
- }
104
- },
105
- {
106
- id: "codex",
107
- tier: 1,
108
- displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex",
109
- bin: "codex",
110
- // The CLI and the desktop app share one config, so a single registration
111
- // covers both — worth saying out loud in the output.
112
- note: CODEX_CHATGPT_NOTE,
113
- buildAddArgs(opts) {
114
- const args = ["mcp", "add", SERVER_NAME];
115
- for (const pair of envPairs(opts)) {
116
- args.push("--env", pair);
117
- }
118
- args.push("--", "npx", "-y", packageSpec(opts.version));
119
- return args;
120
- },
121
- buildRemoveArgs() {
122
- return ["mcp", "remove", SERVER_NAME];
123
- }
124
- },
125
- {
126
- id: "gemini",
127
- tier: 1,
128
- displayName: "Gemini CLI",
129
- bin: "gemini",
130
- // User scope, for the same cwd-binding reason as the entry above.
131
- buildAddArgs(opts) {
132
- const args = ["mcp", "add", "-s", "user"];
133
- for (const pair of envPairs(opts)) {
134
- args.push("-e", pair);
135
- }
136
- args.push(SERVER_NAME, "npx", "-y", packageSpec(opts.version));
137
- return args;
138
- },
139
- buildRemoveArgs() {
140
- return ["mcp", "remove", "-s", "user", SERVER_NAME];
141
- }
142
- }
143
- ];
144
- var INSTALLABLE_CLIS = [
145
- {
146
- id: "claude",
147
- displayName: "Claude Code",
148
- npmPackage: "@anthropic-ai/claude-code"
149
- },
150
- { id: "gemini", displayName: "Gemini CLI", npmPackage: "@google/gemini-cli" },
151
- {
152
- id: "codex",
153
- displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex",
154
- npmPackage: "@openai/codex"
155
- }
156
- ];
157
83
  function joinPath(io, ...parts) {
158
84
  if (io.platform === "win32") {
159
85
  return parts.join("\\");
@@ -191,6 +117,70 @@ function resolveHostPath(io, spec) {
191
117
  const segments = io.platform === "win32" && spec.win32Segments !== void 0 ? spec.win32Segments : spec.segments;
192
118
  return joinPath(io, root, ...segments);
193
119
  }
120
+ var claudeCliHost = {
121
+ id: "claude",
122
+ tier: 1,
123
+ displayName: "Claude Code",
124
+ bin: "claude",
125
+ // User scope is mandatory: the default local scope binds the server to
126
+ // whatever directory the command was pasted in, which reads as success and
127
+ // then does nothing everywhere else.
128
+ buildAddArgs(opts) {
129
+ const args = ["mcp", "add", SERVER_NAME, "--scope", "user"];
130
+ for (const pair of envPairs(opts)) {
131
+ args.push("--env", pair);
132
+ }
133
+ args.push("--", "npx", "-y", packageSpec(opts.version));
134
+ return args;
135
+ },
136
+ buildRemoveArgs() {
137
+ return ["mcp", "remove", SERVER_NAME, "--scope", "user"];
138
+ }
139
+ };
140
+ var claudeInstallable = {
141
+ id: "claude",
142
+ displayName: "Claude Code",
143
+ npmPackage: "@anthropic-ai/claude-code"
144
+ };
145
+ var claudeSkills = {
146
+ tier: 2,
147
+ skillsDir(io) {
148
+ return resolveHostPath(io, {
149
+ kind: "home",
150
+ segments: [".claude", "skills"]
151
+ });
152
+ }
153
+ };
154
+ function jsonSnippet(topKey, opts, platform) {
155
+ const entry = buildFileEntry(opts, platform);
156
+ return JSON.stringify({ [topKey]: { [SERVER_NAME]: entry } }, null, 2);
157
+ }
158
+ function tomlSnippet(opts) {
159
+ const lines = [
160
+ `[mcp_servers.${SERVER_NAME}]`,
161
+ `command = "npx"`,
162
+ `args = ["-y", "${packageSpec(opts.version)}"]`,
163
+ "",
164
+ `[mcp_servers.${SERVER_NAME}.env]`
165
+ ];
166
+ for (const [key, value] of Object.entries(buildEnv(opts))) {
167
+ lines.push(`${key} = "${value}"`);
168
+ }
169
+ return lines.join("\n");
170
+ }
171
+ async function fileOrParentExists(io, path) {
172
+ if (path === null) {
173
+ return false;
174
+ }
175
+ if (await io.exists(path)) {
176
+ return true;
177
+ }
178
+ const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
179
+ if (cut <= 0) {
180
+ return false;
181
+ }
182
+ return io.exists(path.slice(0, cut));
183
+ }
194
184
  async function msixRoamingDir(io, familyPrefix, appDir) {
195
185
  if (io.platform !== "win32") {
196
186
  return null;
@@ -217,186 +207,144 @@ async function msixRoamingDir(io, familyPrefix, appDir) {
217
207
  }
218
208
  return null;
219
209
  }
220
- var FILE_HOSTS = [
221
- {
222
- id: "claude-desktop",
223
- tier: 2,
224
- displayName: "Claude Desktop",
225
- configKey: "mcpServers",
226
- entryKind: "stdio",
227
- // 🔴 Linux has no documented path, so a snippet host covers it; guessing
228
- // one here would manufacture a false success.
229
- configPath(io) {
230
- return resolveHostPath(io, {
231
- kind: "appData",
232
- segments: ["Claude", "claude_desktop_config.json"],
233
- platforms: ["darwin", "win32"]
234
- });
235
- },
236
- /**
237
- * 🔴 A Store install writes inside its package container instead. The two
238
- * install kinds use entirely different locations and the app does not say
239
- * which it is, so an existing container wins and its absence falls back.
240
- */
241
- async resolveConfigPath(io) {
242
- const dir = await msixRoamingDir(io, "Claude_", "Claude");
243
- if (dir === null) {
244
- return null;
245
- }
246
- return joinPath(io, dir, "claude_desktop_config.json");
247
- }
248
- },
249
- {
250
- id: "cursor",
251
- tier: 2,
252
- displayName: "Cursor",
253
- configKey: "mcpServers",
254
- // Speaks local HTTP MCP, so it connects to the daemon directly and no
255
- // adapter process is spawned for it.
256
- entryKind: "url",
257
- configPath(io) {
258
- return resolveHostPath(io, {
259
- kind: "home",
260
- segments: [".cursor", "mcp.json"]
261
- });
262
- }
263
- },
264
- {
265
- // Documented strict JSON, so a single-key merge is safe. The hygiene floor
266
- // still refuses and falls back to a snippet if an install turns out to
267
- // carry comments.
268
- id: "windsurf",
269
- tier: 2,
270
- displayName: "Windsurf",
271
- configKey: "mcpServers",
272
- entryKind: "stdio",
273
- configPath(io) {
274
- return joinPath(
275
- io,
276
- io.homedir(),
277
- ".codeium",
278
- "windsurf",
279
- "mcp_config.json"
280
- );
281
- }
282
- },
283
- {
284
- // 🔴 A home override env var wins when set. Ignoring it would write beside
285
- // the config the user moved, reporting success the app never sees.
286
- id: "copilot-cli",
287
- tier: 2,
288
- displayName: "GitHub Copilot CLI",
289
- configKey: "mcpServers",
290
- entryKind: "stdio",
291
- configPath(io) {
292
- const home = io.env["COPILOT_HOME"];
293
- if (home !== void 0 && home !== "") {
294
- return joinPath(io, home, "mcp-config.json");
295
- }
296
- return resolveHostPath(io, {
297
- kind: "home",
298
- segments: [".copilot", "mcp-config.json"]
299
- });
300
- }
301
- },
302
- {
303
- // Documented strict-JSON user config, home-relative on every OS.
304
- id: "junie",
305
- tier: 2,
306
- displayName: "JetBrains Junie",
307
- configKey: "mcpServers",
308
- entryKind: "stdio",
309
- configPath(io) {
310
- return resolveHostPath(io, {
311
- kind: "home",
312
- segments: [".junie", "mcp", "mcp.json"]
313
- });
314
- }
315
- },
316
- {
317
- // Documented strict-JSON user settings, home-relative on every OS.
318
- id: "kiro",
319
- tier: 2,
320
- displayName: "Kiro",
321
- configKey: "mcpServers",
322
- entryKind: "stdio",
323
- configPath(io) {
324
- return resolveHostPath(io, {
325
- kind: "home",
326
- segments: [".kiro", "settings", "mcp.json"]
327
- });
328
- }
329
- }
330
- ];
331
- function buildEntryForHost(host, opts, platform) {
332
- if (host.entryKind === "url") {
333
- return buildUrlEntry(opts);
334
- }
335
- return buildFileEntry(opts, platform);
336
- }
337
- function jsonSnippet(topKey, opts, platform) {
338
- const entry = buildFileEntry(opts, platform);
339
- return JSON.stringify({ [topKey]: { [SERVER_NAME]: entry } }, null, 2);
340
- }
341
- function vscodeUserDir(io) {
342
- return resolveHostPath(io, { kind: "appData", segments: ["Code", "User"] });
343
- }
344
- function vscodeConfigPath(io) {
345
- const dir = vscodeUserDir(io);
346
- if (dir === null) {
347
- return null;
348
- }
349
- return joinPath(io, dir, "mcp.json");
350
- }
351
- async function fileOrParentExists(io, path) {
352
- if (path === null) {
210
+ async function msixPackageInstalled(io, familyPrefix) {
211
+ if (io.platform !== "win32") {
353
212
  return false;
354
213
  }
355
- if (await io.exists(path)) {
356
- return true;
214
+ const localAppData = io.env["LOCALAPPDATA"];
215
+ if (localAppData === void 0 || localAppData === "") {
216
+ return false;
357
217
  }
358
- const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
359
- if (cut <= 0) {
218
+ try {
219
+ const names = await io.listDir(joinPath(io, localAppData, "Packages"));
220
+ return names.some((name) => name.startsWith(familyPrefix));
221
+ } catch {
360
222
  return false;
361
223
  }
362
- return io.exists(path.slice(0, cut));
363
224
  }
364
- function tomlSnippet(opts) {
365
- const lines = [
366
- `[mcp_servers.${SERVER_NAME}]`,
367
- `command = "npx"`,
368
- `args = ["-y", "${packageSpec(opts.version)}"]`,
369
- "",
370
- `[mcp_servers.${SERVER_NAME}.env]`
371
- ];
372
- for (const [key, value] of Object.entries(buildEnv(opts))) {
373
- lines.push(`${key} = "${value}"`);
225
+ var claudeDesktopFileHost = {
226
+ id: "claude-desktop",
227
+ tier: 2,
228
+ displayName: "Claude Desktop",
229
+ configKey: "mcpServers",
230
+ entryKind: "stdio",
231
+ // Linux has no documented path, so a snippet host covers it; guessing
232
+ // one here would manufacture a false success.
233
+ configPath(io) {
234
+ return resolveHostPath(io, {
235
+ kind: "appData",
236
+ segments: ["Claude", "claude_desktop_config.json"],
237
+ platforms: ["darwin", "win32"]
238
+ });
239
+ },
240
+ /**
241
+ * A Store install writes inside its package container instead. The two
242
+ * install kinds use entirely different locations and the app does not say
243
+ * which it is, so an existing container wins and its absence falls back.
244
+ */
245
+ async resolveConfigPath(io) {
246
+ const dir = await msixRoamingDir(io, "Claude_", "Claude");
247
+ if (dir === null) {
248
+ return null;
249
+ }
250
+ return joinPath(io, dir, "claude_desktop_config.json");
374
251
  }
375
- return lines.join("\n");
376
- }
377
- function zedConfigPath(io) {
378
- return resolveHostPath(io, {
379
- kind: "xdg",
380
- segments: ["zed", "settings.json"],
381
- win32Segments: ["Zed", "settings.json"]
382
- });
383
- }
384
- var LMSTUDIO_PATH = {
385
- kind: "home",
386
- segments: [".lmstudio", "mcp.json"]
387
252
  };
388
- var WARP_PATH = {
389
- kind: "home",
390
- segments: [".warp", ".mcp.json"]
253
+ var claudeDesktopLinuxSnippetHost = {
254
+ id: "claude-desktop-linux",
255
+ tier: 3,
256
+ displayName: "Claude Desktop (Linux)",
257
+ async detect(io) {
258
+ if (io.platform !== "linux") {
259
+ return false;
260
+ }
261
+ return io.exists(joinPath(io, io.homedir(), ".config", "Claude"));
262
+ },
263
+ detectedPath(io) {
264
+ return joinPath(io, io.homedir(), ".config", "Claude");
265
+ },
266
+ // Guessing a path here would manufacture a false success that only
267
+ // surfaces after a restart.
268
+ reason: "\uB9AC\uB205\uC2A4\uC6A9 \uACF5\uC2DD \uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uD655\uC778\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
269
+ pasteWhere: "Claude Desktop \uACF5\uC2DD \uBB38\uC11C\uAC00 \uC548\uB0B4\uD558\uB294 \uC124\uC815 \uD30C\uC77C(claude_desktop_config.json)\uC5D0 \uC544\uB798 \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
270
+ buildSnippet(opts, platform) {
271
+ return jsonSnippet("mcpServers", opts, platform);
272
+ }
391
273
  };
392
- var OPENCODE_PATH = {
393
- kind: "homeConfig",
394
- segments: ["opencode", "opencode.json"]
274
+ var claudeDesktopSkills = {
275
+ tier: 2,
276
+ skillsDir(io) {
277
+ return resolveHostPath(io, {
278
+ kind: "home",
279
+ segments: [".claude", "skills"]
280
+ });
281
+ }
282
+ };
283
+ var claudeDesktopLinuxSkills = {
284
+ tier: 3,
285
+ reason: "\uB9AC\uB205\uC2A4\uC6A9 Claude \uB370\uC2A4\uD06C\uD1B1\uC758 \uC2A4\uD0AC \uACBD\uB85C\uAC00 \uACF5\uC2DD \uBB38\uC11C\uB85C \uD655\uC778\uB418\uC9C0 \uC54A\uC558\uC5B4\uC694.",
286
+ instruction: "\uD655\uC778\uB418\uBA74 \uC790\uB3D9 \uC124\uCE58 \uB300\uC0C1\uC5D0 \uB123\uC744\uAC8C\uC694."
395
287
  };
396
288
  var CLINE_PATH = {
397
289
  kind: "home",
398
290
  segments: [".cline", "mcp.json"]
399
291
  };
292
+ var clineSnippetHost = {
293
+ // One documented path only. An unverified path from a third-party table
294
+ // would have the user paste into a file nothing reads.
295
+ id: "cline",
296
+ tier: 3,
297
+ displayName: "Cline",
298
+ async detect(io) {
299
+ return fileOrParentExists(io, resolveHostPath(io, CLINE_PATH));
300
+ },
301
+ detectedPath(io) {
302
+ return resolveHostPath(io, CLINE_PATH);
303
+ },
304
+ reason: "\uC124\uC815 \uD30C\uC77C\uC744 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
305
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
306
+ buildSnippet(opts, platform) {
307
+ return jsonSnippet("mcpServers", opts, platform);
308
+ }
309
+ };
310
+ var clineSkills = {
311
+ tier: 2,
312
+ skillsDir(io) {
313
+ return resolveHostPath(io, {
314
+ kind: "home",
315
+ segments: [".cline", "skills"]
316
+ });
317
+ }
318
+ };
319
+ function agentsSkillsDir(io) {
320
+ return resolveHostPath(io, { kind: "home", segments: [".agents", "skills"] });
321
+ }
322
+ var CODEX_CHATGPT_NOTE = "ChatGPT \uB370\uC2A4\uD06C\uD1B1\uACFC \uD568\uAED8 \uC5F0\uACB0\uB3FC\uC694.";
323
+ var codexCliHost = {
324
+ id: "codex",
325
+ tier: 1,
326
+ displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex",
327
+ bin: "codex",
328
+ // The CLI and the desktop app share one config, so a single registration
329
+ // covers both — worth saying out loud in the output.
330
+ note: CODEX_CHATGPT_NOTE,
331
+ buildAddArgs(opts) {
332
+ const args = ["mcp", "add", SERVER_NAME];
333
+ for (const pair of envPairs(opts)) {
334
+ args.push("--env", pair);
335
+ }
336
+ args.push("--", "npx", "-y", packageSpec(opts.version));
337
+ return args;
338
+ },
339
+ buildRemoveArgs() {
340
+ return ["mcp", "remove", SERVER_NAME];
341
+ }
342
+ };
343
+ var codexInstallable = {
344
+ id: "codex",
345
+ displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex",
346
+ npmPackage: "@openai/codex"
347
+ };
400
348
  var CODEX_PATH = {
401
349
  kind: "home",
402
350
  segments: [".codex", "config.toml"]
@@ -408,6 +356,193 @@ function codexConfigPath(io) {
408
356
  }
409
357
  return resolveHostPath(io, CODEX_PATH);
410
358
  }
359
+ var CHATGPT_DESKTOP_PACKAGE = "OpenAI.Codex_";
360
+ var codexConfigOnlySnippetHost = {
361
+ id: "codex-config-only",
362
+ tier: 3,
363
+ displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex (\uC124\uC815 \uD30C\uC77C\uB9CC \uBC1C\uACAC)",
364
+ async detect(io, ctx) {
365
+ if (ctx.cliDetected.has("codex")) {
366
+ return false;
367
+ }
368
+ if (await fileOrParentExists(io, codexConfigPath(io))) {
369
+ return true;
370
+ }
371
+ return msixPackageInstalled(io, CHATGPT_DESKTOP_PACKAGE);
372
+ },
373
+ detectedPath(io) {
374
+ return codexConfigPath(io);
375
+ },
376
+ reason: "Codex \uBA85\uB839\uC5B4(CLI)\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC544 TOML \uC124\uC815 \uD30C\uC77C\uC744 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
377
+ pasteWhere: "\uC544\uB798 \uB0B4\uC6A9\uC744 \uC704 \uC124\uC815 \uD30C\uC77C\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
378
+ // The desktop app, the CLI, and the IDE extension share this one file.
379
+ createHint: "\uD30C\uC77C\uC774 \uC5C6\uC73C\uBA74 ChatGPT \uB370\uC2A4\uD06C\uD1B1\uC5D0\uC11C \uC124\uC815 \u2192 MCP \uC11C\uBC84 \u2192 \uC11C\uBC84 \uCD94\uAC00\uB97C \uD55C \uBC88 \uD558\uBA74 \uB9CC\uB4E4\uC5B4\uC838\uC694. \uD3F4\uB354\uAC00 \uC5C6\uC73C\uBA74 \uC9C1\uC811 \uB9CC\uB4E4\uC5B4\uB3C4 \uB3FC\uC694.",
380
+ bootstrapWhenAbsent: true,
381
+ buildSnippet(opts) {
382
+ return tomlSnippet(opts);
383
+ }
384
+ };
385
+ var codexSkills = {
386
+ tier: 2,
387
+ skillsDir(io) {
388
+ return agentsSkillsDir(io);
389
+ }
390
+ };
391
+ var codexConfigOnlySkills = {
392
+ tier: 3,
393
+ reason: "Codex CLI \uB97C \uCC3E\uC9C0 \uBABB\uD574 \uC2A4\uD0AC \uC704\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC5B4\uC694.",
394
+ instruction: "Codex \uB97C \uC124\uCE58\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694."
395
+ };
396
+ var copilotCliFileHost = {
397
+ // A home override env var wins when set. Ignoring it would write beside
398
+ // the config the user moved, reporting success the app never sees.
399
+ id: "copilot-cli",
400
+ tier: 2,
401
+ displayName: "GitHub Copilot CLI",
402
+ configKey: "mcpServers",
403
+ entryKind: "stdio",
404
+ configPath(io) {
405
+ const home = io.env["COPILOT_HOME"];
406
+ if (home !== void 0 && home !== "") {
407
+ return joinPath(io, home, "mcp-config.json");
408
+ }
409
+ return resolveHostPath(io, {
410
+ kind: "home",
411
+ segments: [".copilot", "mcp-config.json"]
412
+ });
413
+ }
414
+ };
415
+ var copilotCliSkills = {
416
+ tier: 2,
417
+ skillsDir(io) {
418
+ return agentsSkillsDir(io);
419
+ }
420
+ };
421
+ var cursorFileHost = {
422
+ id: "cursor",
423
+ tier: 2,
424
+ displayName: "Cursor",
425
+ configKey: "mcpServers",
426
+ // Speaks local HTTP MCP, so it connects to the daemon directly and no
427
+ // adapter process is spawned for it.
428
+ entryKind: "url",
429
+ configPath(io) {
430
+ return resolveHostPath(io, {
431
+ kind: "home",
432
+ segments: [".cursor", "mcp.json"]
433
+ });
434
+ }
435
+ };
436
+ var cursorSkills = {
437
+ tier: 2,
438
+ skillsDir(io) {
439
+ return agentsSkillsDir(io);
440
+ }
441
+ };
442
+ var geminiCliHost = {
443
+ id: "gemini",
444
+ tier: 1,
445
+ displayName: "Gemini CLI",
446
+ bin: "gemini",
447
+ // User scope, for the same cwd-binding reason as the Claude Code host.
448
+ buildAddArgs(opts) {
449
+ const args = ["mcp", "add", "-s", "user"];
450
+ for (const pair of envPairs(opts)) {
451
+ args.push("-e", pair);
452
+ }
453
+ args.push(SERVER_NAME, "npx", "-y", packageSpec(opts.version));
454
+ return args;
455
+ },
456
+ buildRemoveArgs() {
457
+ return ["mcp", "remove", "-s", "user", SERVER_NAME];
458
+ }
459
+ };
460
+ var geminiInstallable = {
461
+ id: "gemini",
462
+ displayName: "Gemini CLI",
463
+ npmPackage: "@google/gemini-cli"
464
+ };
465
+ var geminiSkills = {
466
+ tier: 2,
467
+ skillsDir(io) {
468
+ return agentsSkillsDir(io);
469
+ }
470
+ };
471
+ var junieFileHost = {
472
+ // Documented strict-JSON user config, home-relative on every OS.
473
+ id: "junie",
474
+ tier: 2,
475
+ displayName: "JetBrains Junie",
476
+ configKey: "mcpServers",
477
+ entryKind: "stdio",
478
+ configPath(io) {
479
+ return resolveHostPath(io, {
480
+ kind: "home",
481
+ segments: [".junie", "mcp", "mcp.json"]
482
+ });
483
+ }
484
+ };
485
+ var junieSkills = {
486
+ tier: 2,
487
+ skillsDir(io) {
488
+ return resolveHostPath(io, {
489
+ kind: "home",
490
+ segments: [".junie", "skills"]
491
+ });
492
+ }
493
+ };
494
+ var kiroFileHost = {
495
+ // Documented strict-JSON user settings, home-relative on every OS.
496
+ id: "kiro",
497
+ tier: 2,
498
+ displayName: "Kiro",
499
+ configKey: "mcpServers",
500
+ entryKind: "stdio",
501
+ configPath(io) {
502
+ return resolveHostPath(io, {
503
+ kind: "home",
504
+ segments: [".kiro", "settings", "mcp.json"]
505
+ });
506
+ }
507
+ };
508
+ var kiroSkills = {
509
+ tier: 2,
510
+ skillsDir(io) {
511
+ return resolveHostPath(io, { kind: "home", segments: [".kiro", "skills"] });
512
+ }
513
+ };
514
+ var LMSTUDIO_PATH = {
515
+ kind: "home",
516
+ segments: [".lmstudio", "mcp.json"]
517
+ };
518
+ var lmstudioSnippetHost = {
519
+ // Same entry shape, but the exact path is only medium-confidence, so it
520
+ // stays a snippet: a wrong detection costs a missing card, while a wrong
521
+ // auto-write would be a false success.
522
+ id: "lmstudio",
523
+ tier: 3,
524
+ displayName: "LM Studio",
525
+ async detect(io) {
526
+ return fileOrParentExists(io, resolveHostPath(io, LMSTUDIO_PATH));
527
+ },
528
+ detectedPath(io) {
529
+ return resolveHostPath(io, LMSTUDIO_PATH);
530
+ },
531
+ reason: "\uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uACF5\uC2DD\uC801\uC73C\uB85C \uD655\uC815\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
532
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
533
+ buildSnippet(opts, platform) {
534
+ return jsonSnippet("mcpServers", opts, platform);
535
+ }
536
+ };
537
+ var lmstudioSkills = {
538
+ tier: 3,
539
+ reason: "LM Studio \uB294 \uC2A4\uD0AC(SKILL.md) \uAC1C\uB150\uC774 \uC5C6\uC5B4\uC694.",
540
+ instruction: "MCP \uC11C\uBC84 \uC5F0\uACB0\uB9CC \uC9C0\uC6D0\uB3FC\uC694."
541
+ };
542
+ var OPENCODE_PATH = {
543
+ kind: "homeConfig",
544
+ segments: ["opencode", "opencode.json"]
545
+ };
411
546
  function opencodeConfigPath(io) {
412
547
  const dir = io.env["OPENCODE_CONFIG_DIR"];
413
548
  if (dir !== void 0 && dir !== "") {
@@ -431,186 +566,276 @@ function opencodeSnippet(opts) {
431
566
  2
432
567
  );
433
568
  }
434
- async function msixPackageInstalled(io, familyPrefix) {
435
- if (io.platform !== "win32") {
436
- return false;
569
+ var opencodeSnippetHost = {
570
+ id: "opencode",
571
+ tier: 3,
572
+ displayName: "OpenCode",
573
+ async detect(io) {
574
+ return fileOrParentExists(io, opencodeConfigPath(io));
575
+ },
576
+ detectedPath(io) {
577
+ return opencodeConfigPath(io);
578
+ },
579
+ reason: "\uB3C4\uAD6C \uC804\uCCB4 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
580
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcp" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
581
+ // Never created for this host: the file is the tool's whole config and a
582
+ // sibling dialect may already hold it, so creating ours would leave two
583
+ // config files with no way for the user to know which wins.
584
+ createHint: "\uC124\uC815 \uD30C\uC77C\uC740 opencode.json \uB610\uB294 opencode.jsonc \uC608\uC694. \uC4F0\uACE0 \uC788\uB294 \uCABD\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
585
+ buildSnippet(opts) {
586
+ return opencodeSnippet(opts);
587
+ }
588
+ };
589
+ var opencodeSkills = {
590
+ tier: 2,
591
+ skillsDir(io) {
592
+ return agentsSkillsDir(io);
593
+ }
594
+ };
595
+ function vscodeUserDir(io) {
596
+ return resolveHostPath(io, { kind: "appData", segments: ["Code", "User"] });
597
+ }
598
+ function vscodeConfigPath(io) {
599
+ const dir = vscodeUserDir(io);
600
+ if (dir === null) {
601
+ return null;
602
+ }
603
+ return joinPath(io, dir, "mcp.json");
604
+ }
605
+ var vscodeSnippetHost = {
606
+ id: "vscode",
607
+ tier: 3,
608
+ displayName: "VS Code",
609
+ async detect(io) {
610
+ const path = vscodeConfigPath(io);
611
+ if (path === null) {
612
+ return false;
613
+ }
614
+ return fileOrParentExists(io, path);
615
+ },
616
+ detectedPath(io) {
617
+ return vscodeConfigPath(io);
618
+ },
619
+ reason: "\uC8FC\uC11D\uC774 \uC788\uB294 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
620
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "servers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
621
+ // The hint names the profile too: a non-default profile keeps its own
622
+ // config elsewhere, so pasting into the path above would not reach it.
623
+ createHint: "\uD30C\uC77C\uC774 \uC5C6\uC73C\uBA74 VS Code \uC5D0\uC11C \uBA85\uB839 \uD314\uB808\uD2B8(Ctrl+Shift+P) \u2192 'MCP: Open User Configuration' \uC744 \uC2E4\uD589\uD558\uBA74 \uB9CC\uB4E4\uC5B4\uC838\uC694. \uAE30\uBCF8 \uD504\uB85C\uD544\uC774 \uC544\uB2C8\uBA74 \uADF8 \uD504\uB85C\uD544\uC758 mcp.json \uC774 User/profiles \uC544\uB798\uC5D0 \uB530\uB85C \uC788\uC73C\uB2C8, \uADF8 \uBA85\uB839\uC73C\uB85C \uC5F4\uB9B0 \uD30C\uC77C\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
624
+ bootstrapWhenAbsent: true,
625
+ buildSnippet(opts, platform) {
626
+ return jsonSnippet("servers", opts, platform);
627
+ }
628
+ };
629
+ var vscodeSkills = {
630
+ tier: 2,
631
+ skillsDir(io) {
632
+ return agentsSkillsDir(io);
633
+ }
634
+ };
635
+ var WARP_PATH = {
636
+ kind: "home",
637
+ segments: [".warp", ".mcp.json"]
638
+ };
639
+ var warpSnippetHost = {
640
+ // Same entry shape, medium-confidence path — snippet, not auto-write.
641
+ id: "warp",
642
+ tier: 3,
643
+ displayName: "Warp",
644
+ async detect(io) {
645
+ return fileOrParentExists(io, resolveHostPath(io, WARP_PATH));
646
+ },
647
+ detectedPath(io) {
648
+ return resolveHostPath(io, WARP_PATH);
649
+ },
650
+ reason: "\uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uACF5\uC2DD\uC801\uC73C\uB85C \uD655\uC815\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
651
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
652
+ buildSnippet(opts, platform) {
653
+ return jsonSnippet("mcpServers", opts, platform);
654
+ }
655
+ };
656
+ var warpSkills = {
657
+ tier: 2,
658
+ skillsDir(io) {
659
+ return agentsSkillsDir(io);
660
+ }
661
+ };
662
+ var windsurfFileHost = {
663
+ // Documented strict JSON, so a single-key merge is safe. The hygiene floor
664
+ // still refuses and falls back to a snippet if an install turns out to
665
+ // carry comments.
666
+ id: "windsurf",
667
+ tier: 2,
668
+ displayName: "Windsurf",
669
+ configKey: "mcpServers",
670
+ entryKind: "stdio",
671
+ configPath(io) {
672
+ return joinPath(
673
+ io,
674
+ io.homedir(),
675
+ ".codeium",
676
+ "windsurf",
677
+ "mcp_config.json"
678
+ );
679
+ }
680
+ };
681
+ var windsurfSkills = {
682
+ tier: 2,
683
+ skillsDir(io) {
684
+ return agentsSkillsDir(io);
685
+ }
686
+ };
687
+ function zedConfigPath(io) {
688
+ return resolveHostPath(io, {
689
+ kind: "xdg",
690
+ segments: ["zed", "settings.json"],
691
+ win32Segments: ["Zed", "settings.json"]
692
+ });
693
+ }
694
+ var zedSnippetHost = {
695
+ id: "zed",
696
+ tier: 3,
697
+ displayName: "Zed",
698
+ async detect(io) {
699
+ const path = zedConfigPath(io);
700
+ if (path === null) {
701
+ return false;
702
+ }
703
+ return fileOrParentExists(io, path);
704
+ },
705
+ detectedPath(io) {
706
+ return zedConfigPath(io);
707
+ },
708
+ reason: "\uC8FC\uC11D\uC774 \uC788\uB294 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
709
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "context_servers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
710
+ buildSnippet(opts, platform) {
711
+ return jsonSnippet("context_servers", opts, platform);
437
712
  }
438
- const localAppData = io.env["LOCALAPPDATA"];
439
- if (localAppData === void 0 || localAppData === "") {
440
- return false;
713
+ };
714
+ var zedSkills = {
715
+ tier: 2,
716
+ skillsDir(io) {
717
+ return agentsSkillsDir(io);
441
718
  }
442
- try {
443
- const names = await io.listDir(joinPath(io, localAppData, "Packages"));
444
- return names.some((name) => name.startsWith(familyPrefix));
445
- } catch {
446
- return false;
719
+ };
720
+ var CLI_HOSTS = [
721
+ claudeCliHost,
722
+ codexCliHost,
723
+ geminiCliHost
724
+ ];
725
+ var INSTALLABLE_CLIS = [
726
+ claudeInstallable,
727
+ geminiInstallable,
728
+ codexInstallable
729
+ ];
730
+ var FILE_HOSTS = [
731
+ claudeDesktopFileHost,
732
+ cursorFileHost,
733
+ windsurfFileHost,
734
+ copilotCliFileHost,
735
+ junieFileHost,
736
+ kiroFileHost
737
+ ];
738
+ function buildEntryForHost(host, opts, platform) {
739
+ if (host.entryKind === "url") {
740
+ return buildUrlEntry(opts);
447
741
  }
742
+ return buildFileEntry(opts, platform);
448
743
  }
449
- var CHATGPT_DESKTOP_PACKAGE = "OpenAI.Codex_";
450
744
  var SNIPPET_HOSTS = [
745
+ vscodeSnippetHost,
746
+ opencodeSnippetHost,
747
+ zedSnippetHost,
748
+ clineSnippetHost,
749
+ lmstudioSnippetHost,
750
+ warpSnippetHost,
751
+ claudeDesktopLinuxSnippetHost,
752
+ codexConfigOnlySnippetHost
753
+ ];
754
+ var SKILL_HOSTS = [
451
755
  {
452
- id: "vscode",
453
- tier: 3,
454
- displayName: "VS Code",
455
- async detect(io) {
456
- const path = vscodeConfigPath(io);
457
- if (path === null) {
458
- return false;
459
- }
460
- return fileOrParentExists(io, path);
461
- },
462
- detectedPath(io) {
463
- return vscodeConfigPath(io);
464
- },
465
- reason: "\uC8FC\uC11D\uC774 \uC788\uB294 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
466
- pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "servers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
467
- // 🔴 The hint names the profile too: a non-default profile keeps its own
468
- // config elsewhere, so pasting into the path above would not reach it.
469
- createHint: "\uD30C\uC77C\uC774 \uC5C6\uC73C\uBA74 VS Code \uC5D0\uC11C \uBA85\uB839 \uD314\uB808\uD2B8(Ctrl+Shift+P) \u2192 'MCP: Open User Configuration' \uC744 \uC2E4\uD589\uD558\uBA74 \uB9CC\uB4E4\uC5B4\uC838\uC694. \uAE30\uBCF8 \uD504\uB85C\uD544\uC774 \uC544\uB2C8\uBA74 \uADF8 \uD504\uB85C\uD544\uC758 mcp.json \uC774 User/profiles \uC544\uB798\uC5D0 \uB530\uB85C \uC788\uC73C\uB2C8, \uADF8 \uBA85\uB839\uC73C\uB85C \uC5F4\uB9B0 \uD30C\uC77C\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
470
- bootstrapWhenAbsent: true,
471
- buildSnippet(opts, platform) {
472
- return jsonSnippet("servers", opts, platform);
473
- }
756
+ id: claudeCliHost.id,
757
+ displayName: claudeCliHost.displayName,
758
+ skills: claudeSkills
474
759
  },
475
760
  {
476
- id: "opencode",
477
- tier: 3,
478
- displayName: "OpenCode",
479
- async detect(io) {
480
- return fileOrParentExists(io, opencodeConfigPath(io));
481
- },
482
- detectedPath(io) {
483
- return opencodeConfigPath(io);
484
- },
485
- reason: "\uB3C4\uAD6C \uC804\uCCB4 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
486
- pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcp" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
487
- // 🔴 Never created for this host: the file is the tool's whole config and a
488
- // sibling dialect may already hold it, so creating ours would leave two
489
- // config files with no way for the user to know which wins.
490
- createHint: "\uC124\uC815 \uD30C\uC77C\uC740 opencode.json \uB610\uB294 opencode.jsonc \uC608\uC694. \uC4F0\uACE0 \uC788\uB294 \uCABD\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
491
- buildSnippet(opts) {
492
- return opencodeSnippet(opts);
493
- }
761
+ id: codexCliHost.id,
762
+ displayName: codexCliHost.displayName,
763
+ skills: codexSkills
494
764
  },
495
765
  {
496
- id: "zed",
497
- tier: 3,
498
- displayName: "Zed",
499
- async detect(io) {
500
- const path = zedConfigPath(io);
501
- if (path === null) {
502
- return false;
503
- }
504
- return fileOrParentExists(io, path);
505
- },
506
- detectedPath(io) {
507
- return zedConfigPath(io);
508
- },
509
- reason: "\uC8FC\uC11D\uC774 \uC788\uB294 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
510
- pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "context_servers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
511
- buildSnippet(opts, platform) {
512
- return jsonSnippet("context_servers", opts, platform);
513
- }
766
+ id: geminiCliHost.id,
767
+ displayName: geminiCliHost.displayName,
768
+ skills: geminiSkills
514
769
  },
515
770
  {
516
- // 🔴 One documented path only. An unverified path from a third-party table
517
- // would have the user paste into a file nothing reads.
518
- id: "cline",
519
- tier: 3,
520
- displayName: "Cline",
521
- async detect(io) {
522
- return fileOrParentExists(io, resolveHostPath(io, CLINE_PATH));
523
- },
524
- detectedPath(io) {
525
- return resolveHostPath(io, CLINE_PATH);
526
- },
527
- reason: "\uC124\uC815 \uD30C\uC77C\uC744 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
528
- pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
529
- buildSnippet(opts, platform) {
530
- return jsonSnippet("mcpServers", opts, platform);
531
- }
771
+ id: claudeDesktopFileHost.id,
772
+ displayName: claudeDesktopFileHost.displayName,
773
+ skills: claudeDesktopSkills
532
774
  },
533
775
  {
534
- // Same entry shape, but the exact path is only medium-confidence, so it
535
- // stays a snippet: a wrong detection costs a missing card, while a wrong
536
- // auto-write would be a false success.
537
- id: "lmstudio",
538
- tier: 3,
539
- displayName: "LM Studio",
540
- async detect(io) {
541
- return fileOrParentExists(io, resolveHostPath(io, LMSTUDIO_PATH));
542
- },
543
- detectedPath(io) {
544
- return resolveHostPath(io, LMSTUDIO_PATH);
545
- },
546
- reason: "\uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uACF5\uC2DD\uC801\uC73C\uB85C \uD655\uC815\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
547
- pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
548
- buildSnippet(opts, platform) {
549
- return jsonSnippet("mcpServers", opts, platform);
550
- }
776
+ id: cursorFileHost.id,
777
+ displayName: cursorFileHost.displayName,
778
+ skills: cursorSkills
551
779
  },
552
780
  {
553
- // Same entry shape, medium-confidence path — snippet, not auto-write.
554
- id: "warp",
555
- tier: 3,
556
- displayName: "Warp",
557
- async detect(io) {
558
- return fileOrParentExists(io, resolveHostPath(io, WARP_PATH));
559
- },
560
- detectedPath(io) {
561
- return resolveHostPath(io, WARP_PATH);
562
- },
563
- reason: "\uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uACF5\uC2DD\uC801\uC73C\uB85C \uD655\uC815\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
564
- pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
565
- buildSnippet(opts, platform) {
566
- return jsonSnippet("mcpServers", opts, platform);
567
- }
781
+ id: windsurfFileHost.id,
782
+ displayName: windsurfFileHost.displayName,
783
+ skills: windsurfSkills
568
784
  },
569
785
  {
570
- id: "claude-desktop-linux",
571
- tier: 3,
572
- displayName: "Claude Desktop (Linux)",
573
- async detect(io) {
574
- if (io.platform !== "linux") {
575
- return false;
576
- }
577
- return io.exists(joinPath(io, io.homedir(), ".config", "Claude"));
578
- },
579
- detectedPath(io) {
580
- return joinPath(io, io.homedir(), ".config", "Claude");
581
- },
582
- // 🔴 Guessing a path here would manufacture a false success that only
583
- // surfaces after a restart.
584
- reason: "\uB9AC\uB205\uC2A4\uC6A9 \uACF5\uC2DD \uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uD655\uC778\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
585
- pasteWhere: "Claude Desktop \uACF5\uC2DD \uBB38\uC11C\uAC00 \uC548\uB0B4\uD558\uB294 \uC124\uC815 \uD30C\uC77C(claude_desktop_config.json)\uC5D0 \uC544\uB798 \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
586
- buildSnippet(opts, platform) {
587
- return jsonSnippet("mcpServers", opts, platform);
588
- }
786
+ id: copilotCliFileHost.id,
787
+ displayName: copilotCliFileHost.displayName,
788
+ skills: copilotCliSkills
589
789
  },
590
790
  {
591
- id: "codex-config-only",
592
- tier: 3,
593
- displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex (\uC124\uC815 \uD30C\uC77C\uB9CC \uBC1C\uACAC)",
594
- async detect(io, ctx) {
595
- if (ctx.cliDetected.has("codex")) {
596
- return false;
597
- }
598
- if (await fileOrParentExists(io, codexConfigPath(io))) {
599
- return true;
600
- }
601
- return msixPackageInstalled(io, CHATGPT_DESKTOP_PACKAGE);
602
- },
603
- detectedPath(io) {
604
- return codexConfigPath(io);
605
- },
606
- reason: "Codex \uBA85\uB839\uC5B4(CLI)\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC544 TOML \uC124\uC815 \uD30C\uC77C\uC744 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
607
- pasteWhere: "\uC544\uB798 \uB0B4\uC6A9\uC744 \uC704 \uC124\uC815 \uD30C\uC77C\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
608
- // The desktop app, the CLI, and the IDE extension share this one file.
609
- createHint: "\uD30C\uC77C\uC774 \uC5C6\uC73C\uBA74 ChatGPT \uB370\uC2A4\uD06C\uD1B1\uC5D0\uC11C \uC124\uC815 \u2192 MCP \uC11C\uBC84 \u2192 \uC11C\uBC84 \uCD94\uAC00\uB97C \uD55C \uBC88 \uD558\uBA74 \uB9CC\uB4E4\uC5B4\uC838\uC694. \uD3F4\uB354\uAC00 \uC5C6\uC73C\uBA74 \uC9C1\uC811 \uB9CC\uB4E4\uC5B4\uB3C4 \uB3FC\uC694.",
610
- bootstrapWhenAbsent: true,
611
- buildSnippet(opts) {
612
- return tomlSnippet(opts);
613
- }
791
+ id: junieFileHost.id,
792
+ displayName: junieFileHost.displayName,
793
+ skills: junieSkills
794
+ },
795
+ {
796
+ id: kiroFileHost.id,
797
+ displayName: kiroFileHost.displayName,
798
+ skills: kiroSkills
799
+ },
800
+ {
801
+ id: vscodeSnippetHost.id,
802
+ displayName: vscodeSnippetHost.displayName,
803
+ skills: vscodeSkills
804
+ },
805
+ {
806
+ id: opencodeSnippetHost.id,
807
+ displayName: opencodeSnippetHost.displayName,
808
+ skills: opencodeSkills
809
+ },
810
+ {
811
+ id: zedSnippetHost.id,
812
+ displayName: zedSnippetHost.displayName,
813
+ skills: zedSkills
814
+ },
815
+ {
816
+ id: clineSnippetHost.id,
817
+ displayName: clineSnippetHost.displayName,
818
+ skills: clineSkills
819
+ },
820
+ {
821
+ id: lmstudioSnippetHost.id,
822
+ displayName: lmstudioSnippetHost.displayName,
823
+ skills: lmstudioSkills
824
+ },
825
+ {
826
+ id: warpSnippetHost.id,
827
+ displayName: warpSnippetHost.displayName,
828
+ skills: warpSkills
829
+ },
830
+ {
831
+ id: claudeDesktopLinuxSnippetHost.id,
832
+ displayName: claudeDesktopLinuxSnippetHost.displayName,
833
+ skills: claudeDesktopLinuxSkills
834
+ },
835
+ {
836
+ id: codexConfigOnlySnippetHost.id,
837
+ displayName: codexConfigOnlySnippetHost.displayName,
838
+ skills: codexConfigOnlySkills
614
839
  }
615
840
  ];
616
841
  var SUPPORTED_APPS = [
@@ -631,6 +856,256 @@ var SUPPORTED_APPS = [
631
856
  { name: "LM Studio", url: "https://lmstudio.ai/" },
632
857
  { name: "Warp", url: "https://www.warp.dev/" }
633
858
  ];
859
+ var MARKER_NAME = ".datalab-installed.json";
860
+ var SOURCE = "@modootoday/datalab-extension-mcp";
861
+ function join(io, ...parts) {
862
+ return io.platform === "win32" ? parts.join("\\") : parts.join("/");
863
+ }
864
+ function dirOf(rel) {
865
+ const cut = rel.lastIndexOf("/");
866
+ return cut === -1 ? "" : rel.slice(0, cut);
867
+ }
868
+ async function readMarker(io, dir) {
869
+ const path = join(io, dir, MARKER_NAME);
870
+ if (!await io.exists(path)) {
871
+ return "none";
872
+ }
873
+ let raw;
874
+ try {
875
+ raw = await io.readFile(path);
876
+ } catch {
877
+ return "bad";
878
+ }
879
+ try {
880
+ const parsed = JSON.parse(raw);
881
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.source !== "string" || !Array.isArray(parsed.files)) {
882
+ return "bad";
883
+ }
884
+ return parsed;
885
+ } catch {
886
+ return "bad";
887
+ }
888
+ }
889
+ async function inspectSkillDir(io, root, slug) {
890
+ const dir = join(io, root, slug);
891
+ if (!await io.exists(dir)) {
892
+ return { kind: "absent" };
893
+ }
894
+ const marker = await readMarker(io, dir);
895
+ if (marker === "none") return { kind: "refused", refusal: "unmarked" };
896
+ if (marker === "bad") return { kind: "refused", refusal: "unreadable" };
897
+ if (marker.source !== SOURCE) return { kind: "refused", refusal: "foreign" };
898
+ return {
899
+ kind: "ours",
900
+ version: marker.version,
901
+ installedAt: marker.installedAt
902
+ };
903
+ }
904
+ async function installSkillDir(io, root, payload, version) {
905
+ const dir = join(io, root, payload.slug);
906
+ const files = [];
907
+ if (await io.exists(dir)) {
908
+ const marker2 = await readMarker(io, dir);
909
+ if (marker2 === "none") {
910
+ return { ok: false, path: dir, refusal: "unmarked", files };
911
+ }
912
+ if (marker2 === "bad") {
913
+ return { ok: false, path: dir, refusal: "unreadable", files };
914
+ }
915
+ if (marker2.source !== SOURCE) {
916
+ return { ok: false, path: dir, refusal: "foreign", files };
917
+ }
918
+ }
919
+ await io.mkdir(dir);
920
+ for (const [rel, content] of payload.files) {
921
+ const sub = dirOf(rel);
922
+ if (sub !== "") {
923
+ await io.mkdir(join(io, dir, ...sub.split("/")));
924
+ }
925
+ await io.writeFile(join(io, dir, ...rel.split("/")), content);
926
+ files.push(rel);
927
+ }
928
+ const marker = {
929
+ source: SOURCE,
930
+ version,
931
+ installedAt: io.now().toISOString(),
932
+ files
933
+ };
934
+ await io.writeFile(
935
+ join(io, dir, MARKER_NAME),
936
+ `${JSON.stringify(marker, null, 2)}
937
+ `
938
+ );
939
+ return { ok: true, path: dir, files };
940
+ }
941
+ async function removeSkillDir(io, root, slug) {
942
+ const dir = join(io, root, slug);
943
+ const removed = [];
944
+ if (!await io.exists(dir)) {
945
+ return { ok: true, path: dir, files: removed };
946
+ }
947
+ const marker = await readMarker(io, dir);
948
+ if (marker === "none") {
949
+ return { ok: false, path: dir, refusal: "unmarked", files: removed };
950
+ }
951
+ if (marker === "bad") {
952
+ return { ok: false, path: dir, refusal: "unreadable", files: removed };
953
+ }
954
+ if (marker.source !== SOURCE) {
955
+ return { ok: false, path: dir, refusal: "foreign", files: removed };
956
+ }
957
+ for (const rel of marker.files) {
958
+ const path = join(io, dir, ...rel.split("/"));
959
+ try {
960
+ await io.unlink(path);
961
+ removed.push(rel);
962
+ } catch {
963
+ }
964
+ }
965
+ try {
966
+ await io.unlink(join(io, dir, MARKER_NAME));
967
+ } catch {
968
+ }
969
+ const subs = [...new Set(marker.files.map(dirOf).filter((d) => d !== ""))];
970
+ for (const sub of subs.sort((a, b) => b.length - a.length)) {
971
+ try {
972
+ await io.rmdir(join(io, dir, ...sub.split("/")));
973
+ } catch {
974
+ }
975
+ }
976
+ try {
977
+ await io.rmdir(dir);
978
+ } catch {
979
+ }
980
+ return { ok: true, path: dir, files: removed };
981
+ }
982
+ var PAYLOAD_DIR = "skills";
983
+ function skillsPayloadRoot() {
984
+ const here = fileURLToPath(new URL(".", import.meta.url));
985
+ return joinNative(here, "..", PAYLOAD_DIR);
986
+ }
987
+ function join2(io, ...parts) {
988
+ return io.platform === "win32" ? parts.join("\\") : parts.join("/");
989
+ }
990
+ async function readPayloads(io, root) {
991
+ if (!await io.exists(root)) {
992
+ return null;
993
+ }
994
+ const out = [];
995
+ for (const slug of await io.listDir(root)) {
996
+ const files = /* @__PURE__ */ new Map();
997
+ const skillMd = join2(io, root, slug, "SKILL.md");
998
+ if (!await io.exists(skillMd)) {
999
+ continue;
1000
+ }
1001
+ files.set("SKILL.md", await io.readFile(skillMd));
1002
+ const refs = join2(io, root, slug, "references");
1003
+ if (await io.exists(refs)) {
1004
+ for (const name of await io.listDir(refs)) {
1005
+ files.set(
1006
+ `references/${name}`,
1007
+ await io.readFile(join2(io, refs, name))
1008
+ );
1009
+ }
1010
+ }
1011
+ out.push({ slug, files });
1012
+ }
1013
+ return out;
1014
+ }
1015
+ var CONDITIONAL = /* @__PURE__ */ new Map([
1016
+ ["datalab-card-news", "\uC0AC\uC9C4 \uD3B8\uC9D1\uAE30 \uCE94\uBC84\uC2A4\uAC00 \uC5F4\uB824 \uC788\uC5B4\uC57C \uC2DC\uC791\uD574\uC694"],
1017
+ ["datalab-video-script", "\uC601\uC0C1 \uD3B8\uC9D1\uAE30 \uD0C0\uC784\uB77C\uC778\uC774 \uC5F4\uB824 \uC788\uC5B4\uC57C \uC2DC\uC791\uD574\uC694"],
1018
+ ["datalab-photo-prompt", "AI \uC0AC\uC9C4 \uC0DD\uC131\uC740 \uD655\uC7A5 \uD328\uB110\uC5D0\uC11C\uB9CC \uB3FC\uC694"]
1019
+ ]);
1020
+ var REFUSAL_MESSAGE = {
1021
+ unmarked: "\uB2E4\uB978 \uB3C4\uAD6C\uAC00 \uB9CC\uB4E0 \uD3F4\uB354\uAC00 \uC788\uC5B4 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uC5B4\uC694.",
1022
+ foreign: "\uB2E4\uB978 \uB3C4\uAD6C\uAC00 \uC124\uCE58\uD55C \uC2A4\uD0AC\uC774 \uC788\uC5B4 \uADF8\uB300\uB85C \uB480\uC5B4\uC694.",
1023
+ unreadable: "\uC124\uCE58 \uD45C\uC2DD\uC744 \uC77D\uC744 \uC218 \uC5C6\uC5B4 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC558\uC5B4\uC694."
1024
+ };
1025
+ async function placeSkills(io, payloadRoot, detectedIds, version, all = false) {
1026
+ const results = [];
1027
+ const staged = await readPayloads(io, payloadRoot);
1028
+ if (staged === null || staged.length === 0) {
1029
+ return results;
1030
+ }
1031
+ const payloads = all ? staged : staged.filter((p) => !CONDITIONAL.has(p.slug));
1032
+ const held = staged.length - payloads.length;
1033
+ for (const host of SKILL_HOSTS) {
1034
+ if (!detectedIds.has(host.id)) {
1035
+ continue;
1036
+ }
1037
+ if (host.skills.tier === 3) {
1038
+ results.push({
1039
+ hostId: host.id,
1040
+ displayName: host.displayName,
1041
+ tier: 3,
1042
+ status: "skipped",
1043
+ surface: "skills",
1044
+ message: `${host.skills.reason} ${host.skills.instruction}`
1045
+ });
1046
+ continue;
1047
+ }
1048
+ const dir = host.skills.skillsDir(io);
1049
+ if (dir === null) {
1050
+ continue;
1051
+ }
1052
+ const refused = [];
1053
+ let placed = 0;
1054
+ for (const payload of payloads) {
1055
+ const out = await installSkillDir(io, dir, payload, version);
1056
+ if (out.ok) {
1057
+ placed += 1;
1058
+ } else {
1059
+ refused.push(`${payload.slug}: ${REFUSAL_MESSAGE[out.refusal ?? ""]}`);
1060
+ }
1061
+ }
1062
+ const heldNote = held > 0 ? ` ${held}\uAC1C\uB294 \uC870\uAC74\uBD80\uB77C \uBE90\uC5B4\uC694 \u2014 skills list \uB85C \uD655\uC778\uD558\uC138\uC694.` : "";
1063
+ results.push({
1064
+ hostId: host.id,
1065
+ displayName: host.displayName,
1066
+ tier: 2,
1067
+ status: placed > 0 ? "success" : "skipped",
1068
+ surface: "skills",
1069
+ message: refused.length > 0 ? `\uC2A4\uD0AC ${placed}\uAC1C \uC124\uCE58. ${refused.join(" ")}${heldNote}` : `\uC2A4\uD0AC ${placed}\uAC1C \uC124\uCE58.${heldNote}`
1070
+ });
1071
+ }
1072
+ return results;
1073
+ }
1074
+ async function stagedSlugs(io, root) {
1075
+ const payloads = await readPayloads(io, root);
1076
+ return payloads === null ? [] : payloads.map((p) => p.slug);
1077
+ }
1078
+ async function removeSkills(io, detectedIds, slugs) {
1079
+ const results = [];
1080
+ for (const host of SKILL_HOSTS) {
1081
+ if (!detectedIds.has(host.id) || host.skills.tier !== 2) {
1082
+ continue;
1083
+ }
1084
+ const dir = host.skills.skillsDir(io);
1085
+ if (dir === null) {
1086
+ continue;
1087
+ }
1088
+ let removed = 0;
1089
+ const kept = [];
1090
+ for (const slug of slugs) {
1091
+ const out = await removeSkillDir(io, dir, slug);
1092
+ if (out.ok) {
1093
+ removed += out.files.length > 0 ? 1 : 0;
1094
+ } else {
1095
+ kept.push(slug);
1096
+ }
1097
+ }
1098
+ results.push({
1099
+ hostId: host.id,
1100
+ displayName: host.displayName,
1101
+ tier: 2,
1102
+ status: "success",
1103
+ surface: "skills",
1104
+ message: kept.length > 0 ? `\uC2A4\uD0AC ${removed}\uAC1C \uC81C\uAC70. ${kept.length}\uAC1C\uB294 \uC6B0\uB9AC\uAC00 \uC124\uCE58\uD55C \uAC83\uC774 \uC544\uB2C8\uB77C \uADF8\uB300\uB85C \uB480\uC5B4\uC694.` : `\uC2A4\uD0AC ${removed}\uAC1C \uC81C\uAC70`
1105
+ });
1106
+ }
1107
+ return results;
1108
+ }
634
1109
  var RESTART_NOTICE = "\uB9C8\uC9C0\uB9C9 \uD55C \uB2E8\uACC4: AI \uC571\uC744 \uC644\uC804\uD788 \uC885\uB8CC\uD588\uB2E4\uAC00 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.\n(Windows: \uC791\uC5C5 \uD45C\uC2DC\uC904 \uD2B8\uB808\uC774 \uC544\uC774\uCF58\uC5D0\uC11C \uC885\uB8CC)";
635
1110
  var NOTHING_CHANGED = "\uC544\uBB34\uAC83\uB3C4 \uBC14\uAFB8\uC9C0 \uC54A\uC558\uC5B4\uC694.";
636
1111
  var UNINSTALL_DONE = "\uC815\uB9AC\uAC00 \uB05D\uB0AC\uC5B4\uC694.";
@@ -1382,6 +1857,17 @@ async function runInstall(opts, io) {
1382
1857
  });
1383
1858
  }
1384
1859
  }
1860
+ if (opts.skills !== false) {
1861
+ results.push(
1862
+ ...await placeSkills(
1863
+ io,
1864
+ skillsPayloadRoot(),
1865
+ new Set(detected.map((d) => d.id)),
1866
+ opts.version,
1867
+ opts.allSkills === true
1868
+ )
1869
+ );
1870
+ }
1385
1871
  io.out("");
1386
1872
  for (const result of results) {
1387
1873
  printResult(io, result);
@@ -1457,6 +1943,16 @@ async function runUninstall(opts, io) {
1457
1943
  });
1458
1944
  }
1459
1945
  }
1946
+ if (opts.skills !== false) {
1947
+ const root = skillsPayloadRoot();
1948
+ results.push(
1949
+ ...await removeSkills(
1950
+ io,
1951
+ new Set(detected.map((d) => d.id)),
1952
+ await stagedSlugs(io, root)
1953
+ )
1954
+ );
1955
+ }
1460
1956
  io.out("");
1461
1957
  for (const result of results) {
1462
1958
  printResult(io, result);
@@ -1470,6 +1966,88 @@ async function runUninstall(opts, io) {
1470
1966
  }
1471
1967
  return 0;
1472
1968
  }
1969
+ function descriptionOf(skillMd) {
1970
+ const fm = /^---\n([\s\S]*?)\n---/.exec(skillMd);
1971
+ const line = /^description:\s*(.*)$/m.exec(fm?.[1] ?? "");
1972
+ return (line?.[1] ?? "").trim();
1973
+ }
1974
+ async function listSkills(io, payloadRoot) {
1975
+ const payloads = await readPayloads(io, payloadRoot);
1976
+ if (payloads === null) return [];
1977
+ return payloads.map((p) => ({
1978
+ slug: p.slug,
1979
+ description: descriptionOf(p.files.get("SKILL.md") ?? "")
1980
+ }));
1981
+ }
1982
+ async function skillStatus(io, payloadRoot) {
1983
+ const shipped = (await listSkills(io, payloadRoot)).map((s) => s.slug);
1984
+ const byDir = /* @__PURE__ */ new Map();
1985
+ const unsupported = [];
1986
+ for (const host of SKILL_HOSTS) {
1987
+ if (host.skills.tier === 3) {
1988
+ unsupported.push({ host: host.displayName, reason: host.skills.reason });
1989
+ continue;
1990
+ }
1991
+ const dir = host.skills.skillsDir(io);
1992
+ if (dir === null) continue;
1993
+ byDir.set(dir, [...byDir.get(dir) ?? [], host.displayName]);
1994
+ }
1995
+ const dirs = [];
1996
+ for (const [dir, hosts] of byDir) {
1997
+ const ours = [];
1998
+ const refused = [];
1999
+ const absent = [];
2000
+ for (const slug of shipped) {
2001
+ const state = await inspectSkillDir(io, dir, slug);
2002
+ if (state.kind === "ours") ours.push({ slug, version: state.version });
2003
+ else if (state.kind === "refused")
2004
+ refused.push({ slug, refusal: state.refusal });
2005
+ else absent.push(slug);
2006
+ }
2007
+ dirs.push({ dir, hosts, ours, refused, absent });
2008
+ }
2009
+ return { dirs, unsupported };
2010
+ }
2011
+ async function targetDirs(io) {
2012
+ const detected = new Set((await detectHosts(io)).map((d) => d.id));
2013
+ const byDir = /* @__PURE__ */ new Map();
2014
+ for (const host of SKILL_HOSTS) {
2015
+ if (host.skills.tier !== 2 || !detected.has(host.id)) continue;
2016
+ const dir = host.skills.skillsDir(io);
2017
+ if (dir === null) continue;
2018
+ byDir.set(dir, [...byDir.get(dir) ?? [], host.displayName]);
2019
+ }
2020
+ return byDir;
2021
+ }
2022
+ async function attachSkill(io, payloadRoot, slug, version) {
2023
+ const payloads = await readPayloads(io, payloadRoot) ?? [];
2024
+ const payload = payloads.find((p) => p.slug === slug);
2025
+ if (payload === void 0) {
2026
+ return { kind: "unknown-slug", shipped: payloads.map((p) => p.slug) };
2027
+ }
2028
+ const outcomes = [];
2029
+ for (const [dir, hosts] of await targetDirs(io)) {
2030
+ const out = await installSkillDir(io, dir, payload, version);
2031
+ outcomes.push(
2032
+ out.ok ? { dir, hosts } : { dir, hosts, refusal: out.refusal }
2033
+ );
2034
+ }
2035
+ return { kind: "moved", outcomes };
2036
+ }
2037
+ async function detachSkill(io, payloadRoot, slug) {
2038
+ const shipped = (await listSkills(io, payloadRoot)).map((s) => s.slug);
2039
+ if (!shipped.includes(slug)) {
2040
+ return { kind: "unknown-slug", shipped };
2041
+ }
2042
+ const outcomes = [];
2043
+ for (const [dir, hosts] of await targetDirs(io)) {
2044
+ const out = await removeSkillDir(io, dir, slug);
2045
+ outcomes.push(
2046
+ out.ok ? { dir, hosts } : { dir, hosts, refusal: out.refusal }
2047
+ );
2048
+ }
2049
+ return { kind: "moved", outcomes };
2050
+ }
1473
2051
  function createNodeIo() {
1474
2052
  return {
1475
2053
  platform: process.platform,
@@ -1500,6 +2078,9 @@ function createNodeIo() {
1500
2078
  async unlink(path) {
1501
2079
  await fs.unlink(path);
1502
2080
  },
2081
+ async rmdir(path) {
2082
+ await fs.rmdir(path);
2083
+ },
1503
2084
  async listDir(dir) {
1504
2085
  try {
1505
2086
  return await fs.readdir(dir);
@@ -1568,47 +2149,56 @@ function createNodeIo() {
1568
2149
  };
1569
2150
  }
1570
2151
  var PACKAGE_NAME = "@modootoday/extension-app-mcp-installer";
2152
+
1571
2153
  export {
1572
- BACKUP_KEEP,
1573
- CLI_HOSTS,
1574
- CODEX_CHATGPT_NOTE,
1575
- DEFAULT_PORT,
1576
- EXTENSION_ID_RE,
1577
- FILE_HOSTS,
1578
- INSTALLABLE_CLIS,
2154
+ printBanner,
1579
2155
  INSTALL_SUBTITLE,
1580
- INVALID_EXTENSION_ID_MESSAGE,
1581
- INVALID_PORT_MESSAGE,
1582
- INVALID_TOKEN_MESSAGE,
1583
- INVALID_VERSION_MESSAGE,
1584
- NOTHING_CHANGED,
1585
- NO_HOSTS_DETECTED,
1586
- PACKAGE_NAME,
1587
- PERMISSION_DENIED_HINT,
1588
- PORT_RE,
1589
- RESTART_NOTICE,
2156
+ UNINSTALL_SUBTITLE,
1590
2157
  SERVER_NAME,
1591
2158
  SERVER_PACKAGE,
2159
+ DEFAULT_PORT,
2160
+ packageSpec,
2161
+ buildEnv,
2162
+ buildFileEntry,
2163
+ CODEX_CHATGPT_NOTE,
2164
+ CLI_HOSTS,
2165
+ INSTALLABLE_CLIS,
2166
+ FILE_HOSTS,
1592
2167
  SNIPPET_HOSTS,
1593
2168
  SUPPORTED_APPS,
1594
- TOKEN_RE,
2169
+ skillsPayloadRoot,
2170
+ RESTART_NOTICE,
2171
+ NOTHING_CHANGED,
1595
2172
  UNINSTALL_DONE,
1596
- UNINSTALL_SUBTITLE,
1597
2173
  UNINSTALL_TOKEN_REMINDER,
2174
+ PERMISSION_DENIED_HINT,
2175
+ NO_HOSTS_DETECTED,
2176
+ TOKEN_PROMPT_GUIDE,
2177
+ TOKEN_PROMPT_QUESTION,
2178
+ TOKEN_REQUIRED_NON_INTERACTIVE,
2179
+ installQuestion,
2180
+ uninstallQuestion,
2181
+ TOKEN_RE,
2182
+ EXTENSION_ID_RE,
2183
+ PORT_RE,
1598
2184
  VERSION_RE,
1599
- buildEnv,
1600
- buildFileEntry,
1601
- createNodeIo,
1602
- detectHosts,
2185
+ INVALID_TOKEN_MESSAGE,
2186
+ INVALID_EXTENSION_ID_MESSAGE,
2187
+ INVALID_PORT_MESSAGE,
2188
+ INVALID_VERSION_MESSAGE,
2189
+ validateInstallOptions,
2190
+ validateUninstallOptions,
2191
+ BACKUP_KEEP,
1603
2192
  formatBackupTimestamp,
1604
- installQuestion,
1605
- packageSpec,
1606
- printBanner,
2193
+ upsertServerKey,
1607
2194
  removeServerKey,
2195
+ detectHosts,
1608
2196
  runInstall,
1609
2197
  runUninstall,
1610
- uninstallQuestion,
1611
- upsertServerKey,
1612
- validateInstallOptions,
1613
- validateUninstallOptions
2198
+ listSkills,
2199
+ skillStatus,
2200
+ attachSkill,
2201
+ detachSkill,
2202
+ createNodeIo,
2203
+ PACKAGE_NAME
1614
2204
  };