dsh-eda-mcp 0.6.2 → 0.7.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.
package/lib/client.js CHANGED
@@ -1097,6 +1097,22 @@ function NumberField(props) {
1097
1097
  }
1098
1098
  ) });
1099
1099
  }
1100
+ function TextField(props) {
1101
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: props.label, hint: props.hint, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1102
+ "input",
1103
+ {
1104
+ type: "text",
1105
+ value: props.value ?? "",
1106
+ placeholder: props.placeholder,
1107
+ disabled: props.disabled,
1108
+ onChange: (event) => {
1109
+ const text = event.target.value.trim();
1110
+ props.onChange(text === "" ? void 0 : text);
1111
+ },
1112
+ style: inputStyle
1113
+ }
1114
+ ) });
1115
+ }
1100
1116
  function BooleanField(props) {
1101
1117
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { label: props.label, hint: props.hint, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1102
1118
  import_dsh_client_ui_primitives.Switch,
@@ -1235,6 +1251,20 @@ function EdaMcpSettingsCard(props) {
1235
1251
  }
1236
1252
  }
1237
1253
  ),
1254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1255
+ TextField,
1256
+ {
1257
+ label: t("settings.edaExecutable"),
1258
+ hint: t("settings.edaExecutableHint"),
1259
+ value: draft.edaExecutable,
1260
+ placeholder: "lceda-pro.exe",
1261
+ disabled,
1262
+ onChange: (value) => {
1263
+ clearNotice();
1264
+ update("edaExecutable", value);
1265
+ }
1266
+ }
1267
+ ),
1238
1268
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1239
1269
  BooleanField,
1240
1270
  {
@@ -1417,6 +1447,8 @@ var zh = {
1417
1447
  "settings.toolCallTimeoutMsHint": "\u5355\u6B21 EDA API \u8C03\u7528\u8D85\u65F6\uFF0C\u9ED8\u8BA4 60000\u3002",
1418
1448
  "settings.allowRawApi": "\u5141\u8BB8\u901A\u7528 eda_api_call",
1419
1449
  "settings.allowRawApiHint": "\u5F00\u542F\u540E MCP \u5DE5\u5177 eda_api_call \u53EF\u8C03\u7528\u767D\u540D\u5355\u5185\u7684 JLCEDA \u6269\u5C55 API\u3002",
1450
+ "settings.edaExecutable": "\u5609\u7ACB\u521BEDA \u53EF\u6267\u884C\u6587\u4EF6\uFF08\u53EF\u9009\uFF09",
1451
+ "settings.edaExecutableHint": "\u7559\u7A7A\u5373\u81EA\u52A8\u63A2\u6D4B\uFF1A\u73AF\u5883\u53D8\u91CF \u2192 \u6CE8\u518C\u8868\u767B\u8BB0\u7684\u6587\u4EF6\u5173\u8054 \u2192 \u5E38\u89C1\u5B89\u88C5\u76EE\u5F55 \u2192 PATH\u3002\u53EA\u6709\u81EA\u52A8\u63A2\u6D4B\u5931\u8D25\u65F6\u624D\u9700\u8981\u586B\u5B8C\u6574\u8DEF\u5F84\u3002",
1420
1452
  "settings.save": "\u4FDD\u5B58",
1421
1453
  "settings.saving": "\u4FDD\u5B58\u4E2D\u2026",
1422
1454
  "settings.saved": "\u5DF2\u4FDD\u5B58",
@@ -1445,6 +1477,8 @@ var en = {
1445
1477
  "settings.toolCallTimeoutMsHint": "Timeout for one EDA API call; default 60000.",
1446
1478
  "settings.allowRawApi": "Allow generic eda_api_call",
1447
1479
  "settings.allowRawApiHint": "Expose the eda_api_call tool restricted to the allow-listed JLCEDA APIs.",
1480
+ "settings.edaExecutable": "JLCEDA Pro executable (optional)",
1481
+ "settings.edaExecutableHint": "Leave empty to auto-detect: environment variable, the file-association command in the registry, conventional install directories, then PATH. Set a full path only when auto-detection fails.",
1448
1482
  "settings.save": "Save",
1449
1483
  "settings.saving": "Saving\u2026",
1450
1484
  "settings.saved": "Saved",
package/lib/index.js CHANGED
@@ -492,7 +492,9 @@ var Config = z.object({
492
492
  enabled: z.boolean().default(true).volatile(),
493
493
  announceToAgent: z.boolean().default(true).volatile(),
494
494
  toolCallTimeoutMs: z.number().min(1e3).max(3e5).default(6e4).volatile(),
495
- allowRawApi: z.boolean().default(true).volatile()
495
+ allowRawApi: z.boolean().default(true).volatile(),
496
+ /** Optional path to lceda-pro.exe; empty means "let eda_launch discover it". */
497
+ edaExecutable: z.string().default("").volatile()
496
498
  });
497
499
  var FIBER_DISPOSED = 4;
498
500
  var FIBER_UNLOADING = 5;
@@ -544,7 +546,9 @@ function bridgeLaunchFor(active) {
544
546
  env: {
545
547
  ...runtime.extraEnv,
546
548
  DSH_EDA_MCP_ALLOW_RAW_API: allowRawApi ? "1" : "0",
547
- DSH_EDA_MCP_TOOL_TIMEOUT_MS: String(toolCallTimeoutMs)
549
+ DSH_EDA_MCP_TOOL_TIMEOUT_MS: String(toolCallTimeoutMs),
550
+ // eda_launch falls back to the registry / install dirs / PATH when empty.
551
+ DSH_EDA_EXECUTABLE: typeof active.edaExecutable === "string" ? active.edaExecutable : ""
548
552
  },
549
553
  cwd: dirname2(BRIDGE_PATH),
550
554
  toolCallTimeoutMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-eda-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "DeepSeek Harness (DSH) plugin — connects DSH to JLCEDA Pro (EasyEDA Pro) via a local MCP bridge and draws schematics through the JLCEDA extension API.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {
@@ -49,6 +49,7 @@
49
49
  "test",
50
50
  "docs",
51
51
  "jlc-extension",
52
+ "dsh-eda-bridge-extension-*.zip",
52
53
  "dsh-eda-bridge-extension.zip",
53
54
  "cordis.patch.yml",
54
55
  "CHANGELOG.md",
@@ -72,9 +73,10 @@
72
73
  "scripts": {
73
74
  "build": "node build.mjs",
74
75
  "typecheck": "tsc --noEmit",
75
- "test": "node --experimental-strip-types test/node-runtime.test.ts && node --experimental-strip-types test/config-values.test.ts && node --experimental-strip-types test/design-rules.test.ts && node --experimental-strip-types test/seams.test.ts",
76
+ "test": "node --experimental-strip-types test/node-runtime.test.ts && node --experimental-strip-types test/config-values.test.ts && node --experimental-strip-types test/design-rules.test.ts && node --experimental-strip-types test/seams.test.ts && node --experimental-strip-types test/eda-launch.test.ts",
76
77
  "seam-check": "node scripts/seam-check.mjs",
77
- "check": "pnpm typecheck && pnpm test && pnpm build",
78
+ "contract-check": "node scripts/contract-check.mjs",
79
+ "check": "pnpm typecheck && pnpm test && pnpm contract-check && pnpm build",
78
80
  "docs": "node scripts/gen-tools-doc.mjs",
79
81
  "prepublishOnly": "pnpm check",
80
82
  "prepare": "pnpm build"
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Launching JLCEDA Pro (EasyEDA Pro) from a tool call.
3
+ *
4
+ * Nothing here is machine-specific: the executable is resolved from an explicit
5
+ * override, an environment variable, the registry entry the installer creates, a
6
+ * list of conventional install directories, and finally PATH. Every step is
7
+ * skipped when its inputs are absent.
8
+ *
9
+ * String operations only (no regex literals): the discovery rules stay readable
10
+ * and the resolution order is unit-tested without spawning anything.
11
+ */
12
+
13
+ export type Platform = 'win32' | 'darwin' | 'linux' | (string & {})
14
+
15
+ /** Everything the resolver needs from the outside world. */
16
+ export interface LaunchProbe {
17
+ platform: Platform
18
+ env: Record<string, string | undefined>
19
+ exists: (path: string) => boolean
20
+ /** Raw stdout of the registry query, or undefined when the key does not exist. */
21
+ registryQuery: (key: string, valueName?: string) => string | undefined
22
+ }
23
+
24
+ export interface ResolveOptions {
25
+ executable?: string
26
+ project?: string
27
+ }
28
+
29
+ export type ExecutableSource = 'config' | 'env' | 'registry' | 'install-dir' | 'path'
30
+
31
+ export interface ResolvedLaunch {
32
+ executable: string
33
+ source: ExecutableSource
34
+ }
35
+
36
+ export const EXECUTABLE_ENV_KEYS = ['DSH_EDA_EXECUTABLE', 'JLCEDA_EXECUTABLE'] as const
37
+ export const EXECUTABLE_NAMES = ['lceda-pro.exe', 'lceda-pro', 'LCEDA Pro'] as const
38
+ export const PROJECT_EXTENSIONS = ['.eprj2', '.eprj'] as const
39
+
40
+ const CN_PRO_EDITION = '嘉立创EDA专业版'
41
+ const CLASSES = 'Software\\Classes'
42
+
43
+ export const INSTALL_SUFFIXES = [
44
+ 'lceda-pro',
45
+ 'Program Files\\lceda-pro',
46
+ 'Program Files (x86)\\lceda-pro',
47
+ 'Program Files\\' + CN_PRO_EDITION,
48
+ 'Program Files (x86)\\' + CN_PRO_EDITION,
49
+ ] as const
50
+
51
+ /** Strip trailing path separators from a directory. */
52
+ function trimSeparator(value: string): string {
53
+ let end = value.length
54
+ while (end > 0) {
55
+ const ch = value[end - 1]
56
+ if (ch !== '\\' && ch !== '/') break
57
+ end -= 1
58
+ }
59
+ return value.slice(0, end)
60
+ }
61
+
62
+ /** Strip leading path separators. */
63
+ function trimLeadingSeparators(value: string): string {
64
+ let start = 0
65
+ while (start < value.length && (value[start] === '\\' || value[start] === '/')) start += 1
66
+ return value.slice(start)
67
+ }
68
+
69
+ function joinPath(platform: Platform, ...parts: string[]): string {
70
+ const separator = platform === 'win32' ? '\\' : '/'
71
+ return parts
72
+ .filter(part => part !== '')
73
+ .map((part, index) => (index === 0 ? part : trimLeadingSeparators(part)))
74
+ .join(separator)
75
+ }
76
+
77
+ /** Split an install suffix into path segments, on either separator. */
78
+ function splitSegments(value: string): string[] {
79
+ return value.split('\\').join('/').split('/').filter(part => part !== '')
80
+ }
81
+
82
+ /**
83
+ * Pull the executable out of a registered open command such as
84
+ * `"D:\\lceda-pro\\lceda-pro.exe" "%1"` or `C:\\App\\lceda-pro.exe %1`.
85
+ */
86
+ export function parseExecutableFromCommand(command: string | undefined): string | undefined {
87
+ const text = (command ?? '').trim()
88
+ if (text === '') return undefined
89
+ if (text.startsWith('"')) {
90
+ const end = text.indexOf('"', 1)
91
+ if (end === -1) return undefined
92
+ const quoted = text.slice(1, end).trim()
93
+ return quoted === '' ? undefined : quoted
94
+ }
95
+ const space = text.indexOf(' ')
96
+ const tab = text.indexOf(String.fromCharCode(9))
97
+ const stops = [space, tab].filter(index => index > 0)
98
+ const end = stops.length === 0 ? text.length : Math.min(...stops)
99
+ const first = text.slice(0, end).trim()
100
+ return first === '' ? undefined : first
101
+ }
102
+
103
+ /**
104
+ * Decode `reg` output. The tool writes in the console code page (GBK on a Chinese
105
+ * Windows), and progid names are non-ASCII, so a UTF-8 decode can produce
106
+ * replacement characters that no longer match a registry key. When that happens,
107
+ * decode again as GBK (both decoders ship with the Node the host provides).
108
+ */
109
+ export function decodeRegistryOutput(raw: Uint8Array): string {
110
+ const utf8 = new TextDecoder('utf-8').decode(raw)
111
+ if (!utf8.includes(String.fromCharCode(0xFFFD))) return utf8
112
+ try {
113
+ return new TextDecoder('gbk').decode(raw)
114
+ } catch {
115
+ return utf8
116
+ }
117
+ }
118
+
119
+ /** Take the string payload of a registry line (`... REG_SZ <value>`). */
120
+ export function parseRegStringValue(output: string | undefined): string | undefined {
121
+ for (const raw of (output ?? '').split(String.fromCharCode(10))) {
122
+ const line = raw.trim()
123
+ const index = line.toUpperCase().indexOf('REG_SZ')
124
+ if (index === -1) continue
125
+ const value = line.slice(index + 'REG_SZ'.length).trim()
126
+ // `reg query` prints "(value not set)" for an unset default; it is not a value.
127
+ if (value === '' || value.startsWith('(')) continue
128
+ return value
129
+ }
130
+ return undefined
131
+ }
132
+
133
+ /** Value names of a registry listing (the first column of every value line). */
134
+ export function parseRegValueNames(output: string | undefined): string[] {
135
+ const names: string[] = []
136
+ for (const raw of (output ?? '').split(String.fromCharCode(10))) {
137
+ const line = raw.trim()
138
+ const marker = line.indexOf('REG_')
139
+ if (marker <= 0) continue
140
+ const name = line.slice(0, marker).trim()
141
+ if (name !== '' && !name.startsWith('HKEY')) names.push(name)
142
+ }
143
+ return names
144
+ }
145
+
146
+ function firstExisting(probe: LaunchProbe, candidates: string[]): string | undefined {
147
+ for (const candidate of candidates) {
148
+ if (candidate !== '' && probe.exists(candidate)) return candidate
149
+ }
150
+ return undefined
151
+ }
152
+
153
+ /** Candidate executables derived from environment variables (no registry access). */
154
+ export function envCandidates(probe: LaunchProbe): string[] {
155
+ const env = probe.env
156
+ const roots = [
157
+ env.ProgramFiles,
158
+ env['ProgramFiles(x86)'],
159
+ env.LOCALAPPDATA === undefined ? undefined : joinPath(probe.platform, env.LOCALAPPDATA, 'Programs'),
160
+ env.APPDATA === undefined ? undefined : joinPath(probe.platform, env.APPDATA, 'Programs'),
161
+ env.SystemDrive,
162
+ env.USERPROFILE,
163
+ env.HOME,
164
+ ].filter((root): root is string => typeof root === 'string' && root !== '')
165
+
166
+ const candidates: string[] = []
167
+ for (const root of roots) {
168
+ for (const suffix of INSTALL_SUFFIXES) {
169
+ for (const name of EXECUTABLE_NAMES) {
170
+ candidates.push(joinPath(probe.platform, trimSeparator(root), ...splitSegments(suffix), name))
171
+ }
172
+ }
173
+ }
174
+ return candidates
175
+ }
176
+
177
+ /** Look for the executable in the directories listed on PATH. */
178
+ export function pathCandidates(probe: LaunchProbe): string[] {
179
+ const raw = probe.env.PATH ?? probe.env.Path ?? ''
180
+ const separator = probe.platform === 'win32' ? ';' : ':'
181
+ return raw
182
+ .split(separator)
183
+ .filter(dir => dir.trim() !== '')
184
+ .flatMap(dir => EXECUTABLE_NAMES.map(name => joinPath(probe.platform, trimSeparator(dir), name)))
185
+ }
186
+
187
+ /**
188
+ * Ask the registry where the installer registered the project handler.
189
+ *
190
+ * The extension's default progid is often empty while `OpenWithProgids` lists the
191
+ * real one, so both are consulted. HKCU wins over HKLM; HKCR is skipped because
192
+ * it merges the two.
193
+ */
194
+ export function registryCandidates(probe: LaunchProbe): string[] {
195
+ if (probe.platform !== 'win32') return []
196
+ const found: string[] = []
197
+ for (const hive of ['HKCU', 'HKLM']) {
198
+ for (const extension of PROJECT_EXTENSIONS) {
199
+ const base = hive + '\\' + CLASSES + '\\' + extension
200
+ const defaults = [parseRegStringValue(probe.registryQuery(base, '')), parseRegStringValue(probe.registryQuery(base))]
201
+ const progids = [
202
+ ...defaults.filter((value): value is string => value !== undefined),
203
+ ...parseRegValueNames(probe.registryQuery(base + '\\OpenWithProgids')),
204
+ ]
205
+ for (const progid of progids) {
206
+ if (progid.startsWith('Applications' + '\\')) continue
207
+ const command = parseRegStringValue(
208
+ probe.registryQuery(hive + '\\' + CLASSES + '\\' + progid + '\\shell\\open\\command'),
209
+ )
210
+ const executable = parseExecutableFromCommand(command)
211
+ if (executable !== undefined) found.push(executable)
212
+ }
213
+ }
214
+ }
215
+ return found
216
+ }
217
+
218
+ /** Resolve the executable, or undefined when nothing on this machine matches. */
219
+ export function resolveExecutable(probe: LaunchProbe, options: ResolveOptions = {}): ResolvedLaunch | undefined {
220
+ const override = options.executable?.trim()
221
+ if (override !== undefined && override !== '' && probe.exists(override)) {
222
+ return { executable: override, source: 'config' }
223
+ }
224
+
225
+ for (const key of EXECUTABLE_ENV_KEYS) {
226
+ const value = probe.env[key]?.trim()
227
+ if (value !== undefined && value !== '' && probe.exists(value)) return { executable: value, source: 'env' }
228
+ }
229
+
230
+ const fromRegistry = firstExisting(probe, registryCandidates(probe))
231
+ if (fromRegistry !== undefined) return { executable: fromRegistry, source: 'registry' }
232
+
233
+ const fromInstallDir = firstExisting(probe, envCandidates(probe))
234
+ if (fromInstallDir !== undefined) return { executable: fromInstallDir, source: 'install-dir' }
235
+
236
+ const fromPath = firstExisting(probe, pathCandidates(probe))
237
+ if (fromPath !== undefined) return { executable: fromPath, source: 'path' }
238
+
239
+ return undefined
240
+ }
241
+
242
+ /** Arguments used to open the app (and optionally a project) on each platform. */
243
+ export function buildLaunchArgs(platform: Platform, executable: string, project?: string): { command: string; args: string[] } {
244
+ const target = project?.trim()
245
+ const hasProject = target !== undefined && target !== ''
246
+ if (platform === 'darwin') {
247
+ return hasProject
248
+ ? { command: 'open', args: ['-a', executable, target as string] }
249
+ : { command: 'open', args: ['-a', executable] }
250
+ }
251
+ return hasProject ? { command: executable, args: [target as string] } : { command: executable, args: [] }
252
+ }
253
+
254
+ /**
255
+ * Windows script used to hand the launch to the running Explorer shell.
256
+ *
257
+ * Spawning `lceda-pro.exe` directly from a host process (or even through
258
+ * `Start-Process`) makes it start and exit immediately, while double-clicking the
259
+ * same executable works. Explorer runs in the interactive desktop, so executing a
260
+ * script through it reproduces the double-click context; the script then starts the
261
+ * app with the optional project argument.
262
+ */
263
+ export function buildLaunchScript(executable: string, project?: string): string {
264
+ const target = project?.trim()
265
+ const args = target === undefined || target === '' ? '' : ' "' + target + '"'
266
+ return ['@echo off', 'start "" "' + executable + '"' + args, ''].join(String.fromCharCode(13, 10))
267
+ }