cssprobe-cli 0.2.26 → 0.3.1

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.
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ // Flow script runner: loads a user script in the daemon process, injects the
3
+ // interaction API, captures console output and returns a result envelope.
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.loadFlowScript = loadFlowScript;
9
+ exports.runFlow = runFlow;
10
+ const path_1 = __importDefault(require("path"));
11
+ const util_1 = __importDefault(require("util"));
12
+ const url_1 = require("url");
13
+ const interactions_1 = require("../browser/interactions");
14
+ const analyzer_1 = require("../engine/analyzer");
15
+ const renderer_1 = require("../engine/renderer");
16
+ const evalSerializer_1 = require("../engine/evalSerializer");
17
+ const screenshot_1 = require("../utils/screenshot");
18
+ const FLOW_DEFAULT_TIMEOUT = 300_000;
19
+ const CAPTURED_CONSOLE_METHODS = ['log', 'info', 'warn', 'error'];
20
+ // tsc compiles `import()` to `require()` under module=commonjs, which cannot
21
+ // load ESM scripts; a Function-constructed import keeps the native loader.
22
+ const nativeImport = new Function('p', 'return import(p)');
23
+ async function loadFlowScript(absPath) {
24
+ const mod = await nativeImport(`${(0, url_1.pathToFileURL)(absPath).href}?v=${Date.now()}`);
25
+ const fn = mod?.default;
26
+ if (typeof fn !== 'function')
27
+ throw new Error(`Flow script must default-export an async function: ${absPath}`);
28
+ return fn;
29
+ }
30
+ function captureConsole(logs) {
31
+ const originals = new Map();
32
+ for (const method of CAPTURED_CONSOLE_METHODS) {
33
+ originals.set(method, console[method]);
34
+ console[method] = (...args) => {
35
+ logs.push(`[${method}] ${util_1.default.format(...args)}`);
36
+ };
37
+ }
38
+ return () => {
39
+ for (const [method, original] of originals) {
40
+ console[method] = original;
41
+ }
42
+ };
43
+ }
44
+ async function withTimeout(promise, timeoutMs) {
45
+ if (!timeoutMs || timeoutMs <= 0)
46
+ return promise;
47
+ let timer;
48
+ try {
49
+ return await Promise.race([
50
+ promise,
51
+ new Promise((_, reject) => {
52
+ timer = setTimeout(() => reject(new Error(`Flow timed out after ${timeoutMs}ms (best-effort abort)`)), timeoutMs);
53
+ }),
54
+ ]);
55
+ }
56
+ finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+ function requirePage(launcher) {
61
+ const page = launcher.getPage();
62
+ if (!page)
63
+ throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
64
+ return page;
65
+ }
66
+ async function flowWaitFor(page, target, opts) {
67
+ if (typeof target === 'string') {
68
+ const state = opts?.state ?? 'visible';
69
+ const timeout = opts?.timeout ?? 30_000;
70
+ const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'selector', selector: target, state }, timeout);
71
+ return { waitedFor: 'selector', state, elapsedMs };
72
+ }
73
+ if ('js' in target) {
74
+ const timeout = target.timeout ?? 30_000;
75
+ const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'js', expression: target.js }, timeout);
76
+ return { waitedFor: 'js', elapsedMs };
77
+ }
78
+ if (target.networkIdle) {
79
+ const timeout = target.timeout ?? 30_000;
80
+ const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'networkIdle' }, timeout);
81
+ return { waitedFor: 'networkidle', elapsedMs };
82
+ }
83
+ throw new Error('waitFor requires a selector string, { js }, or { networkIdle: true }');
84
+ }
85
+ function buildFlowApi(launcher, logs, baseDir) {
86
+ const page = requirePage(launcher);
87
+ return {
88
+ page,
89
+ click: (selector, opts) => (0, interactions_1.clickElement)(page, selector, opts),
90
+ hover: (selector, opts) => (0, interactions_1.hoverElement)(page, selector, opts),
91
+ type: (selector, text, opts) => (0, interactions_1.typeInto)(page, selector, text, opts),
92
+ waitFor: (target, opts) => flowWaitFor(page, target, opts),
93
+ inspect: async (selector, opts) => {
94
+ const snapshot = await launcher.collect({
95
+ rootSelector: selector,
96
+ downDepth: 6,
97
+ maxNodes: 60,
98
+ upTo: 'html',
99
+ });
100
+ const findings = (0, analyzer_1.analyze)(snapshot);
101
+ if (opts?.format === 'markdown')
102
+ return (0, renderer_1.renderMarkdown)(snapshot, findings);
103
+ return { snapshot, findings };
104
+ },
105
+ screenshot: async (opts) => {
106
+ const buffer = await page.screenshot({ fullPage: opts?.fullPage });
107
+ const out = opts?.path ? path_1.default.resolve(baseDir, opts.path) : undefined;
108
+ return (0, screenshot_1.saveScreenshotFile)(`data:image/png;base64,${buffer.toString('base64')}`, out);
109
+ },
110
+ log: (...args) => {
111
+ logs.push(util_1.default.format(...args));
112
+ },
113
+ };
114
+ }
115
+ function renderEnvelope(env) {
116
+ const lines = [];
117
+ if (env.ok) {
118
+ lines.push(`Flow completed in ${env.elapsedMs}ms`);
119
+ if (env.result !== undefined)
120
+ lines.push(`Result: ${JSON.stringify(env.result, null, 2)}`);
121
+ }
122
+ else {
123
+ lines.push(`Flow failed in ${env.elapsedMs}ms: ${env.error}`);
124
+ }
125
+ if (env.logs.length) {
126
+ lines.push('', 'Logs:');
127
+ lines.push(...env.logs.map(line => ` ${line}`));
128
+ }
129
+ return lines.join('\n');
130
+ }
131
+ async function runFlow(launcher, absPath, opts = {}) {
132
+ const logs = [];
133
+ const started = Date.now();
134
+ const restore = captureConsole(logs);
135
+ let env;
136
+ try {
137
+ const fn = await loadFlowScript(absPath);
138
+ const api = buildFlowApi(launcher, logs, path_1.default.dirname(absPath));
139
+ const result = await withTimeout(Promise.resolve(fn(api)), opts.timeout ?? FLOW_DEFAULT_TIMEOUT);
140
+ env = { ok: true, logs, elapsedMs: Date.now() - started };
141
+ if (result !== undefined)
142
+ env.result = (0, evalSerializer_1.serializeEvalResult)(result);
143
+ }
144
+ catch (e) {
145
+ env = { ok: false, logs, error: e.message, elapsedMs: Date.now() - started };
146
+ }
147
+ finally {
148
+ restore();
149
+ }
150
+ return {
151
+ text: opts.json ? JSON.stringify(env, null, 2) : renderEnvelope(env),
152
+ exitCode: env.ok ? 0 : 1,
153
+ };
154
+ }
155
+ //# sourceMappingURL=flowRunner.js.map
@@ -0,0 +1 @@
1
+ export declare function serializeEvalResult(value: unknown): unknown;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // Self-contained serializer for eval results.
3
+ // Runs inside the page via JSHandle.evaluate (function source is transferred),
4
+ // and is unit-testable in Node — keep it free of external references.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.serializeEvalResult = serializeEvalResult;
7
+ function serializeEvalResult(value) {
8
+ const seen = new WeakSet();
9
+ const walk = (v, depth) => {
10
+ if (v === null || typeof v !== 'object') {
11
+ if (typeof v === 'function')
12
+ return `[Function: ${v.name || 'anonymous'}]`;
13
+ if (typeof v === 'bigint')
14
+ return `${v}n`;
15
+ return v;
16
+ }
17
+ if (typeof Node !== 'undefined' && v instanceof Node) {
18
+ if (v.nodeType === 1) {
19
+ const el = v;
20
+ return {
21
+ tag: el.tagName.toLowerCase(),
22
+ id: el.id,
23
+ classes: Array.from(el.classList),
24
+ text: (el.textContent ?? '').trim().slice(0, 120),
25
+ };
26
+ }
27
+ return `[${v.nodeName}]`;
28
+ }
29
+ if (seen.has(v))
30
+ return '[Circular]';
31
+ if (depth >= 6)
32
+ return '[Depth limit]';
33
+ seen.add(v);
34
+ if (Array.isArray(v))
35
+ return v.map(item => walk(item, depth + 1));
36
+ const out = {};
37
+ for (const [key, val] of Object.entries(v))
38
+ out[key] = walk(val, depth + 1);
39
+ return out;
40
+ };
41
+ return walk(value, 0);
42
+ }
43
+ //# sourceMappingURL=evalSerializer.js.map
package/dist/help.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
2
+ "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n click <selector> click an element with real mouse events (works with custom react components)\n hover <selector> hover an element with real mouse events\n type <selector> <text> type text into an element (fill by default; --sequential for real key events)\n wait-for [selector] wait for a condition: selector state, js predicate, or network idle\n run <file> run a multi-step flow script (default-exports an async function receiving the api object)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
3
3
  "commands": {
4
4
  "open": {
5
5
  "help": "cssprobe-cli open [url]\n\nOpen browser in session mode (non-blocking)\n\nArguments:\n [url] url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --headed show the browser window\n --viewport viewport size as wxh (e.g. 1280x720)\n --state path to saved state file (cookies + localstorage)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
@@ -97,6 +97,64 @@
97
97
  },
98
98
  "args": []
99
99
  },
100
+ "click": {
101
+ "help": "cssprobe-cli click <selector>\n\nClick an element with real mouse events (works with custom React components)\n\nArguments:\n <selector> css selector\nOptions:\n --nth 1-based match index (default: first match)\n --force skip actionability checks\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
102
+ "flags": {
103
+ "nth": "string",
104
+ "force": "boolean",
105
+ "timeout": "string"
106
+ },
107
+ "args": [
108
+ "selector"
109
+ ]
110
+ },
111
+ "hover": {
112
+ "help": "cssprobe-cli hover <selector>\n\nHover an element with real mouse events\n\nArguments:\n <selector> css selector\nOptions:\n --nth 1-based match index (default: first match)\n --force skip actionability checks\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
113
+ "flags": {
114
+ "nth": "string",
115
+ "force": "boolean",
116
+ "timeout": "string"
117
+ },
118
+ "args": [
119
+ "selector"
120
+ ]
121
+ },
122
+ "type": {
123
+ "help": "cssprobe-cli type <selector> <text>\n\nType text into an element (fill by default; --sequential for real key events)\n\nArguments:\n <selector> css selector\n <text> text to type\nOptions:\n --press key to press after typing (e.g. enter, tab, control+a)\n --sequential type with real key events (for custom editors)\n --clear clear existing content before sequential typing\n --delay delay between keystrokes in ms (--sequential, default 0)\n --nth 1-based match index (default: first match)\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
124
+ "flags": {
125
+ "press": "string",
126
+ "sequential": "boolean",
127
+ "clear": "boolean",
128
+ "delay": "string",
129
+ "nth": "string",
130
+ "timeout": "string"
131
+ },
132
+ "args": [
133
+ "selector",
134
+ "text"
135
+ ]
136
+ },
137
+ "wait-for": {
138
+ "help": "cssprobe-cli wait-for [selector]\n\nWait for a condition: selector state, JS predicate, or network idle\n\nArguments:\n [selector] css selector to wait for\nOptions:\n --state selector state to wait for (default visible)\n --js js predicate, e.g. \"() => document.queryselector('.x')?.offsetheight > 100\"\n --network-idle wait until the page is network-idle\n --timeout timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
139
+ "flags": {
140
+ "state": "string",
141
+ "js": "string",
142
+ "network-idle": "boolean",
143
+ "timeout": "string"
144
+ },
145
+ "args": [
146
+ "selector"
147
+ ]
148
+ },
149
+ "run": {
150
+ "help": "cssprobe-cli run <file>\n\nRun a multi-step flow script (default-exports an async function receiving the API object)\n\nArguments:\n <file> flow script path (.js / .mjs / .cjs)\nOptions:\n --timeout whole-script timeout in ms (default 300000, 0 = unlimited)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
151
+ "flags": {
152
+ "timeout": "string"
153
+ },
154
+ "args": [
155
+ "file"
156
+ ]
157
+ },
100
158
  "state-import": {
101
159
  "help": "cssprobe-cli state-import [file]\n\nImport cookies from Netscape format file\n\nArguments:\n [file] netscape cookie file path (reads from stdin if omitted)\nOptions:\n --out output state file path (default: ~/.cssprobe-cli/states/imported.json)\n --name custom name for the state file (e.g. mysite or mysite.json)\n --merge existing state file to merge into\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
102
160
  "flags": {
@@ -173,6 +231,27 @@
173
231
  "path": "string"
174
232
  },
175
233
  "args": []
234
+ },
235
+ "mcp": {
236
+ "help": "cssprobe-cli mcp \n\nStart MCP server (stdio transport) for AI clients\n\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
237
+ "flags": {},
238
+ "args": []
239
+ },
240
+ "mcp-install": {
241
+ "help": "cssprobe-cli mcp-install \n\nInstall MCP server config into AI client configs\n\nOptions:\n --target client target: auto, all, none, claude, cursor, codex, opencode, hermes, gemini (default: auto)\n --local write project-local config instead of global\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
242
+ "flags": {
243
+ "target": "string",
244
+ "local": "boolean"
245
+ },
246
+ "args": []
247
+ },
248
+ "mcp-uninstall": {
249
+ "help": "cssprobe-cli mcp-uninstall \n\nRemove MCP server config from AI client configs\n\nOptions:\n --target client target: auto, all, none, claude, cursor, codex, opencode, hermes, gemini (default: auto)\n --local remove project-local config instead of global\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
250
+ "flags": {
251
+ "target": "string",
252
+ "local": "boolean"
253
+ },
254
+ "args": []
176
255
  }
177
256
  },
178
257
  "booleanOptions": [
@@ -182,6 +261,10 @@
182
261
  "brief",
183
262
  "layout",
184
263
  "full-page",
264
+ "force",
265
+ "sequential",
266
+ "clear",
267
+ "network-idle",
185
268
  "local"
186
269
  ]
187
270
  }
@@ -1,3 +1,5 @@
1
1
  export { buildSkillMd, buildPitfallsMd } from './skill-template';
2
2
  export { ALL_TARGETS, getTarget, listTargetIds, resolveTargetFlag } from './targets/registry';
3
3
  export type { AgentTarget, Location, TargetId, WriteResult } from './targets/types';
4
+ export { installMcpConfig, uninstallMcpConfig, listMcpClientIds, MCP_SERVER_NAME } from './mcp';
5
+ export type { McpCommandResult, McpFileResult } from './mcp';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveTargetFlag = exports.listTargetIds = exports.getTarget = exports.ALL_TARGETS = exports.buildPitfallsMd = exports.buildSkillMd = void 0;
3
+ exports.MCP_SERVER_NAME = exports.listMcpClientIds = exports.uninstallMcpConfig = exports.installMcpConfig = exports.resolveTargetFlag = exports.listTargetIds = exports.getTarget = exports.ALL_TARGETS = exports.buildPitfallsMd = exports.buildSkillMd = void 0;
4
4
  var skill_template_1 = require("./skill-template");
5
5
  Object.defineProperty(exports, "buildSkillMd", { enumerable: true, get: function () { return skill_template_1.buildSkillMd; } });
6
6
  Object.defineProperty(exports, "buildPitfallsMd", { enumerable: true, get: function () { return skill_template_1.buildPitfallsMd; } });
@@ -9,4 +9,9 @@ Object.defineProperty(exports, "ALL_TARGETS", { enumerable: true, get: function
9
9
  Object.defineProperty(exports, "getTarget", { enumerable: true, get: function () { return registry_1.getTarget; } });
10
10
  Object.defineProperty(exports, "listTargetIds", { enumerable: true, get: function () { return registry_1.listTargetIds; } });
11
11
  Object.defineProperty(exports, "resolveTargetFlag", { enumerable: true, get: function () { return registry_1.resolveTargetFlag; } });
12
+ var mcp_1 = require("./mcp");
13
+ Object.defineProperty(exports, "installMcpConfig", { enumerable: true, get: function () { return mcp_1.installMcpConfig; } });
14
+ Object.defineProperty(exports, "uninstallMcpConfig", { enumerable: true, get: function () { return mcp_1.uninstallMcpConfig; } });
15
+ Object.defineProperty(exports, "listMcpClientIds", { enumerable: true, get: function () { return mcp_1.listMcpClientIds; } });
16
+ Object.defineProperty(exports, "MCP_SERVER_NAME", { enumerable: true, get: function () { return mcp_1.MCP_SERVER_NAME; } });
12
17
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ import type { Location, TargetId } from './targets/types';
2
+ export declare const MCP_SERVER_NAME = "cssprobe-cli";
3
+ export interface McpFileResult {
4
+ path: string;
5
+ action: 'created' | 'updated' | 'unchanged' | 'removed' | 'not-found' | 'kept';
6
+ agent: string;
7
+ }
8
+ export interface McpCommandResult {
9
+ files: McpFileResult[];
10
+ notes: string[];
11
+ }
12
+ export declare function installMcpConfig(loc: Location, targetFlag?: string): McpCommandResult;
13
+ export declare function uninstallMcpConfig(loc: Location, targetFlag?: string): McpCommandResult;
14
+ export declare function listMcpClientIds(): TargetId[];
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MCP_SERVER_NAME = void 0;
37
+ exports.installMcpConfig = installMcpConfig;
38
+ exports.uninstallMcpConfig = uninstallMcpConfig;
39
+ exports.listMcpClientIds = listMcpClientIds;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const shared_1 = require("./targets/shared");
44
+ const toml_1 = require("./toml");
45
+ exports.MCP_SERVER_NAME = 'cssprobe-cli';
46
+ const jsonEntry = () => ({ type: 'stdio', command: exports.MCP_SERVER_NAME, args: ['mcp'] });
47
+ const opencodeEntry = () => ({ type: 'local', command: [exports.MCP_SERVER_NAME, 'mcp'], enabled: true });
48
+ function home(...parts) {
49
+ return path.join(os.homedir(), ...parts);
50
+ }
51
+ function dirExists(p) {
52
+ try {
53
+ return fs.statSync(p).isDirectory();
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function fileExists(p) {
60
+ try {
61
+ return fs.statSync(p).isFile();
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ function opencodeGlobalPath() {
68
+ const newStyleDir = home('.opencode');
69
+ if (dirExists(newStyleDir) || fileExists(path.join(newStyleDir, 'opencode.json'))) {
70
+ return path.join(newStyleDir, 'opencode.json');
71
+ }
72
+ return home('.config', 'opencode', 'opencode.json');
73
+ }
74
+ const CLIENTS = [
75
+ {
76
+ id: 'claude',
77
+ displayName: 'Claude Code',
78
+ format: 'json',
79
+ supportsLocal: true,
80
+ configPath: (loc) => (loc === 'global' ? home('.claude.json') : path.join(process.cwd(), '.mcp.json')),
81
+ detect: (loc) => (loc === 'global' ? fileExists(home('.claude.json')) || dirExists(home('.claude')) : dirExists(path.join(process.cwd(), '.claude'))),
82
+ serversKey: 'mcpServers',
83
+ buildEntry: jsonEntry,
84
+ },
85
+ {
86
+ id: 'cursor',
87
+ displayName: 'Cursor',
88
+ format: 'json',
89
+ supportsLocal: true,
90
+ configPath: (loc) => (loc === 'global' ? home('.cursor', 'mcp.json') : path.join(process.cwd(), '.cursor', 'mcp.json')),
91
+ detect: (loc) => dirExists(loc === 'global' ? home('.cursor') : path.join(process.cwd(), '.cursor')),
92
+ serversKey: 'mcpServers',
93
+ buildEntry: jsonEntry,
94
+ },
95
+ {
96
+ id: 'gemini',
97
+ displayName: 'Gemini CLI',
98
+ format: 'json',
99
+ supportsLocal: false,
100
+ configPath: () => home('.gemini', 'mcp_settings.json'),
101
+ detect: () => dirExists(home('.gemini')),
102
+ serversKey: 'mcpServers',
103
+ buildEntry: jsonEntry,
104
+ },
105
+ {
106
+ id: 'hermes',
107
+ displayName: 'Hermes',
108
+ format: 'json',
109
+ supportsLocal: false,
110
+ configPath: () => home('.hermes', 'mcp.json'),
111
+ detect: () => dirExists(home('.hermes')),
112
+ serversKey: 'mcpServers',
113
+ buildEntry: jsonEntry,
114
+ },
115
+ {
116
+ id: 'opencode',
117
+ displayName: 'opencode',
118
+ format: 'json',
119
+ supportsLocal: true,
120
+ configPath: (loc) => (loc === 'global' ? opencodeGlobalPath() : path.join(process.cwd(), 'opencode.json')),
121
+ detect: (loc) => dirExists(loc === 'global' ? home('.opencode') : path.join(process.cwd(), '.opencode')) || (loc === 'global' && dirExists(home('.config', 'opencode'))),
122
+ serversKey: 'mcp',
123
+ buildEntry: opencodeEntry,
124
+ },
125
+ {
126
+ id: 'codex',
127
+ displayName: 'Codex CLI',
128
+ format: 'toml',
129
+ supportsLocal: false,
130
+ configPath: () => home('.codex', 'config.toml'),
131
+ detect: () => dirExists(home('.codex')),
132
+ table: `mcp_servers.${exports.MCP_SERVER_NAME}`,
133
+ buildBlock: () => (0, toml_1.buildTomlTable)(`mcp_servers.${exports.MCP_SERVER_NAME}`, { command: exports.MCP_SERVER_NAME, args: ['mcp'] }),
134
+ },
135
+ ];
136
+ function resolveClients(targetFlag, loc) {
137
+ const notes = [];
138
+ const allowed = CLIENTS.filter((c) => loc === 'global' || c.supportsLocal);
139
+ if (targetFlag === 'none')
140
+ return { clients: [], notes };
141
+ if (targetFlag === 'all') {
142
+ if (loc === 'local')
143
+ notes.push('Local location only applies to claude / cursor / opencode; other clients are global-only.');
144
+ return { clients: [...allowed], notes };
145
+ }
146
+ if (targetFlag === 'auto') {
147
+ const detected = allowed.filter((c) => c.detect(loc));
148
+ if (detected.length === 0) {
149
+ notes.push('No MCP client configs detected. Use --target=all or --target=<id,...> to force.');
150
+ }
151
+ return { clients: detected, notes };
152
+ }
153
+ const ids = targetFlag.split(',').map((s) => s.trim()).filter(Boolean);
154
+ const resolved = [];
155
+ const unknown = [];
156
+ for (const id of ids) {
157
+ const client = CLIENTS.find((c) => c.id === id);
158
+ if (!client) {
159
+ unknown.push(id);
160
+ continue;
161
+ }
162
+ if (loc === 'local' && !client.supportsLocal) {
163
+ notes.push(`${client.displayName} has no project-local MCP config; skipped.`);
164
+ continue;
165
+ }
166
+ resolved.push(client);
167
+ }
168
+ if (unknown.length > 0) {
169
+ const known = CLIENTS.map((c) => c.id).join(', ');
170
+ throw new Error(`Unknown --target id(s): ${unknown.join(', ')}. Known: ${known}, plus 'auto' / 'all' / 'none'.`);
171
+ }
172
+ return { clients: resolved, notes };
173
+ }
174
+ function installJson(client, loc) {
175
+ const file = client.configPath(loc);
176
+ if (!fileExists(file)) {
177
+ const data = {};
178
+ data[client.serversKey] = { [exports.MCP_SERVER_NAME]: client.buildEntry() };
179
+ (0, shared_1.atomicWriteFileSync)(file, JSON.stringify(data, null, 2) + '\n');
180
+ return { result: { path: file, action: 'created', agent: client.displayName } };
181
+ }
182
+ const raw = fs.readFileSync(file, 'utf-8');
183
+ let data;
184
+ try {
185
+ data = JSON.parse(raw);
186
+ }
187
+ catch {
188
+ return {
189
+ result: { path: file, action: 'kept', agent: client.displayName },
190
+ note: `${file} is not valid JSON — skipped (left untouched).`,
191
+ };
192
+ }
193
+ const before = JSON.stringify(data[client.serversKey]?.[exports.MCP_SERVER_NAME] ?? null);
194
+ const after = JSON.stringify(client.buildEntry());
195
+ if (before === after) {
196
+ return { result: { path: file, action: 'unchanged', agent: client.displayName } };
197
+ }
198
+ if (!data[client.serversKey])
199
+ data[client.serversKey] = {};
200
+ data[client.serversKey][exports.MCP_SERVER_NAME] = client.buildEntry();
201
+ (0, shared_1.atomicWriteFileSync)(file, JSON.stringify(data, null, 2) + '\n');
202
+ return { result: { path: file, action: 'updated', agent: client.displayName } };
203
+ }
204
+ function uninstallJson(client, loc) {
205
+ const file = client.configPath(loc);
206
+ if (!fileExists(file)) {
207
+ return { result: { path: file, action: 'not-found', agent: client.displayName } };
208
+ }
209
+ const raw = fs.readFileSync(file, 'utf-8');
210
+ let data;
211
+ try {
212
+ data = JSON.parse(raw);
213
+ }
214
+ catch {
215
+ return {
216
+ result: { path: file, action: 'kept', agent: client.displayName },
217
+ note: `${file} is not valid JSON — skipped (left untouched).`,
218
+ };
219
+ }
220
+ if (!data[client.serversKey]?.[exports.MCP_SERVER_NAME]) {
221
+ return { result: { path: file, action: 'not-found', agent: client.displayName } };
222
+ }
223
+ delete data[client.serversKey][exports.MCP_SERVER_NAME];
224
+ if (Object.keys(data[client.serversKey]).length === 0)
225
+ delete data[client.serversKey];
226
+ (0, shared_1.atomicWriteFileSync)(file, JSON.stringify(data, null, 2) + '\n');
227
+ return { result: { path: file, action: 'removed', agent: client.displayName } };
228
+ }
229
+ function installToml(client, loc) {
230
+ const file = client.configPath(loc);
231
+ const existing = fileExists(file) ? fs.readFileSync(file, 'utf-8') : '';
232
+ const { content, action } = (0, toml_1.upsertTomlTable)(existing, client.table, client.buildBlock());
233
+ if (action === 'unchanged') {
234
+ return { result: { path: file, action: 'unchanged', agent: client.displayName } };
235
+ }
236
+ (0, shared_1.atomicWriteFileSync)(file, content);
237
+ return { result: { path: file, action: action === 'inserted' ? 'created' : 'updated', agent: client.displayName } };
238
+ }
239
+ function uninstallToml(client, loc) {
240
+ const file = client.configPath(loc);
241
+ if (!fileExists(file)) {
242
+ return { result: { path: file, action: 'not-found', agent: client.displayName } };
243
+ }
244
+ const existing = fs.readFileSync(file, 'utf-8');
245
+ const { content, action } = (0, toml_1.removeTomlTable)(existing, client.table);
246
+ if (action === 'not-found') {
247
+ return { result: { path: file, action: 'not-found', agent: client.displayName } };
248
+ }
249
+ (0, shared_1.atomicWriteFileSync)(file, content);
250
+ return { result: { path: file, action: 'removed', agent: client.displayName } };
251
+ }
252
+ function run(mode, loc, targetFlag) {
253
+ const { clients, notes } = resolveClients(targetFlag, loc);
254
+ const files = [];
255
+ for (const client of clients) {
256
+ const handled = mode === 'install'
257
+ ? (client.format === 'json' ? installJson(client, loc) : installToml(client, loc))
258
+ : (client.format === 'json' ? uninstallJson(client, loc) : uninstallToml(client, loc));
259
+ files.push(handled.result);
260
+ if (handled.note)
261
+ notes.push(handled.note);
262
+ }
263
+ return { files, notes };
264
+ }
265
+ function installMcpConfig(loc, targetFlag = 'auto') {
266
+ return run('install', loc, targetFlag);
267
+ }
268
+ function uninstallMcpConfig(loc, targetFlag = 'auto') {
269
+ return run('uninstall', loc, targetFlag);
270
+ }
271
+ function listMcpClientIds() {
272
+ return CLIENTS.map((c) => c.id);
273
+ }
274
+ //# sourceMappingURL=mcp.js.map