firmwareloop 0.0.12__tar.gz → 0.0.13__tar.gz

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 (31) hide show
  1. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/PKG-INFO +10 -8
  2. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/README.md +9 -7
  3. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/firmwareloop.egg-info/PKG-INFO +10 -8
  4. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/pyproject.toml +1 -1
  5. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/__init__.py +1 -1
  6. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/acceptance-scenario.ps1 +11 -8
  7. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/common/fw.psm1 +9 -2
  8. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/doctor.ps1 +10 -3
  9. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/fw_mcp_server.py +107 -29
  10. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/setup-agent-mcp.ps1 +68 -26
  11. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/test-backends.ps1 +21 -8
  12. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/test.ps1 +4 -3
  13. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/LICENSE +0 -0
  14. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/firmwareloop.egg-info/SOURCES.txt +0 -0
  15. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/firmwareloop.egg-info/dependency_links.txt +0 -0
  16. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/firmwareloop.egg-info/entry_points.txt +0 -0
  17. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/firmwareloop.egg-info/requires.txt +0 -0
  18. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/firmwareloop.egg-info/top_level.txt +0 -0
  19. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/setup.cfg +0 -0
  20. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/build.ps1 +0 -0
  21. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/can.ps1 +0 -0
  22. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/check-qoder-mcp.ps1 +0 -0
  23. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/common/build-backends.psm1 +0 -0
  24. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/common/uart_probe.py +0 -0
  25. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/flash.ps1 +0 -0
  26. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/instrument_cli.py +0 -0
  27. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/lib/__init__.py +0 -0
  28. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/lib/instruments.py +0 -0
  29. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/logic_capture.ps1 +0 -0
  30. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/logic_decode.ps1 +0 -0
  31. {firmwareloop-0.0.12 → firmwareloop-0.0.13}/tools/reset.ps1 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.0.12
3
+ Version: 0.0.13
4
4
  Summary: AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)
5
5
  Author-email: WeilinZhang <19523330249@163.com>
6
6
  License: MIT
@@ -135,10 +135,12 @@ claude mcp add --scope user fwloop -- fwloop
135
135
  claude mcp add --scope user agentic-hil -- agentic-hil mcp-stdio
136
136
  ```
137
137
 
138
- 3. **Qoder IDE**:
139
- ```bash
140
- qoder.cmd mcp add --global fwloop -- fwloop
141
- ```
138
+ 3. **Qoder / Qoder CN**:
139
+ `fwloop setup` 会直接写入 IDE 设置页读取的用户级文件:
140
+ - 国际版:`~/.qoder/mcp.json`
141
+ - 国内版:`~/.qoder-cn/mcp.json`
142
+
143
+ 不要依赖 `qoder.cmd --add-mcp`:它写到 `%APPDATA%\Qoder\User\mcp.json`,设置页看不到。
142
144
 
143
145
  4. **Cursor / 其他 IDE**(在工程根目录的 `.mcp.json` 中并联配置):
144
146
  ```json
@@ -180,7 +182,7 @@ fwloop init
180
182
 
181
183
  | 命令 | 适用级别 | 核心作用 | 使用时机 |
182
184
  |---|---|---|---|
183
- | **`fwloop setup`** | **系统全局级** | 给各大 Agent(Antigravity / Claude Code)配置全局 MCP 与同步技能包 | **全电脑只需跑 1 次** |
185
+ | **`fwloop setup`** | **系统全局级** | 给各大 Agent(Antigravity / Claude Code / Qoder / Qoder CN)配置全局 MCP 与同步技能包 | **全电脑只需跑 1 次** |
184
186
  | **`fwloop init`** | **工程项目级** | 在当前单片机代码目录下生成 `AGENTS.md`、`GEMINI.md`、`CLAUDE.md` 与台架配置 | **每个新单片机项目跑 1 次** |
185
187
  | **`fwloop doctor`** | **环境诊断** | 检查编译器(Keil/GCC)、Python 环境、串口与探针连接健康度 | 随时排查环境时使用 |
186
188
  | **`fwloop update`** | **自动更新** | 一键自动拉取最新代码并热重载依赖 | 升级工具版本时使用 |
@@ -336,7 +338,7 @@ claude mcp remove --scope user agentic-hil
336
338
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
337
339
  ├── demo-make/ 示例固件(Make;构建测试载体)
338
340
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
339
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.0.12)
341
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.0.13)
340
342
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
341
343
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
342
344
  ├── CLAUDE.md Claude Code CLI 指南
@@ -346,7 +348,7 @@ claude mcp remove --scope user agentic-hil
346
348
 
347
349
  ---
348
350
 
349
- ## 能力验证状态(v0.0.12)
351
+ ## 能力验证状态(v0.0.13)
350
352
 
351
353
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
352
354
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -107,10 +107,12 @@ claude mcp add --scope user fwloop -- fwloop
107
107
  claude mcp add --scope user agentic-hil -- agentic-hil mcp-stdio
108
108
  ```
109
109
 
110
- 3. **Qoder IDE**:
111
- ```bash
112
- qoder.cmd mcp add --global fwloop -- fwloop
113
- ```
110
+ 3. **Qoder / Qoder CN**:
111
+ `fwloop setup` 会直接写入 IDE 设置页读取的用户级文件:
112
+ - 国际版:`~/.qoder/mcp.json`
113
+ - 国内版:`~/.qoder-cn/mcp.json`
114
+
115
+ 不要依赖 `qoder.cmd --add-mcp`:它写到 `%APPDATA%\Qoder\User\mcp.json`,设置页看不到。
114
116
 
115
117
  4. **Cursor / 其他 IDE**(在工程根目录的 `.mcp.json` 中并联配置):
116
118
  ```json
@@ -152,7 +154,7 @@ fwloop init
152
154
 
153
155
  | 命令 | 适用级别 | 核心作用 | 使用时机 |
154
156
  |---|---|---|---|
155
- | **`fwloop setup`** | **系统全局级** | 给各大 Agent(Antigravity / Claude Code)配置全局 MCP 与同步技能包 | **全电脑只需跑 1 次** |
157
+ | **`fwloop setup`** | **系统全局级** | 给各大 Agent(Antigravity / Claude Code / Qoder / Qoder CN)配置全局 MCP 与同步技能包 | **全电脑只需跑 1 次** |
156
158
  | **`fwloop init`** | **工程项目级** | 在当前单片机代码目录下生成 `AGENTS.md`、`GEMINI.md`、`CLAUDE.md` 与台架配置 | **每个新单片机项目跑 1 次** |
157
159
  | **`fwloop doctor`** | **环境诊断** | 检查编译器(Keil/GCC)、Python 环境、串口与探针连接健康度 | 随时排查环境时使用 |
158
160
  | **`fwloop update`** | **自动更新** | 一键自动拉取最新代码并热重载依赖 | 升级工具版本时使用 |
@@ -308,7 +310,7 @@ claude mcp remove --scope user agentic-hil
308
310
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
309
311
  ├── demo-make/ 示例固件(Make;构建测试载体)
310
312
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
311
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.0.12)
313
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.0.13)
312
314
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
313
315
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
314
316
  ├── CLAUDE.md Claude Code CLI 指南
@@ -318,7 +320,7 @@ claude mcp remove --scope user agentic-hil
318
320
 
319
321
  ---
320
322
 
321
- ## 能力验证状态(v0.0.12)
323
+ ## 能力验证状态(v0.0.13)
322
324
 
323
325
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
324
326
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.0.12
3
+ Version: 0.0.13
4
4
  Summary: AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)
5
5
  Author-email: WeilinZhang <19523330249@163.com>
6
6
  License: MIT
@@ -135,10 +135,12 @@ claude mcp add --scope user fwloop -- fwloop
135
135
  claude mcp add --scope user agentic-hil -- agentic-hil mcp-stdio
136
136
  ```
137
137
 
138
- 3. **Qoder IDE**:
139
- ```bash
140
- qoder.cmd mcp add --global fwloop -- fwloop
141
- ```
138
+ 3. **Qoder / Qoder CN**:
139
+ `fwloop setup` 会直接写入 IDE 设置页读取的用户级文件:
140
+ - 国际版:`~/.qoder/mcp.json`
141
+ - 国内版:`~/.qoder-cn/mcp.json`
142
+
143
+ 不要依赖 `qoder.cmd --add-mcp`:它写到 `%APPDATA%\Qoder\User\mcp.json`,设置页看不到。
142
144
 
143
145
  4. **Cursor / 其他 IDE**(在工程根目录的 `.mcp.json` 中并联配置):
144
146
  ```json
@@ -180,7 +182,7 @@ fwloop init
180
182
 
181
183
  | 命令 | 适用级别 | 核心作用 | 使用时机 |
182
184
  |---|---|---|---|
183
- | **`fwloop setup`** | **系统全局级** | 给各大 Agent(Antigravity / Claude Code)配置全局 MCP 与同步技能包 | **全电脑只需跑 1 次** |
185
+ | **`fwloop setup`** | **系统全局级** | 给各大 Agent(Antigravity / Claude Code / Qoder / Qoder CN)配置全局 MCP 与同步技能包 | **全电脑只需跑 1 次** |
184
186
  | **`fwloop init`** | **工程项目级** | 在当前单片机代码目录下生成 `AGENTS.md`、`GEMINI.md`、`CLAUDE.md` 与台架配置 | **每个新单片机项目跑 1 次** |
185
187
  | **`fwloop doctor`** | **环境诊断** | 检查编译器(Keil/GCC)、Python 环境、串口与探针连接健康度 | 随时排查环境时使用 |
186
188
  | **`fwloop update`** | **自动更新** | 一键自动拉取最新代码并热重载依赖 | 升级工具版本时使用 |
@@ -336,7 +338,7 @@ claude mcp remove --scope user agentic-hil
336
338
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
337
339
  ├── demo-make/ 示例固件(Make;构建测试载体)
338
340
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
339
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.0.12)
341
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.0.13)
340
342
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
341
343
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
342
344
  ├── CLAUDE.md Claude Code CLI 指南
@@ -346,7 +348,7 @@ claude mcp remove --scope user agentic-hil
346
348
 
347
349
  ---
348
350
 
349
- ## 能力验证状态(v0.0.12)
351
+ ## 能力验证状态(v0.0.13)
350
352
 
351
353
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
352
354
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firmwareloop"
7
- version = "0.0.12"
7
+ version = "0.0.13"
8
8
  description = "AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -2,4 +2,4 @@
2
2
  FirmwareLoop - AI Agent Firmware Development and Lab Automation Platform.
3
3
  """
4
4
 
5
- __version__ = "0.0.12"
5
+ __version__ = "0.0.13"
@@ -94,12 +94,12 @@ $report = [ordered]@{
94
94
  # ---- 1. build --------------------------------------------------------------------
95
95
  $build = Invoke-Tool 'build.ps1' @('-Configuration', 'Debug', '-Json')
96
96
  $report.steps.build = [ordered]@{
97
- ok = $build.ok
98
- errors = $build.errors
99
- warnings = $build.warnings
100
- artifact = $build.artifact
101
- sha256 = $build.artifact_sha256
102
- detail = if ($build.ok) { $null } else { $build.diagnostics }
97
+ ok = [bool]($build.PSObject.Properties['ok'] -and $build.ok)
98
+ errors = if ($build.PSObject.Properties['errors']) { $build.errors } else { 1 }
99
+ warnings = if ($build.PSObject.Properties['warnings']) { $build.warnings } else { 0 }
100
+ artifact = if ($build.PSObject.Properties['artifact']) { $build.artifact } else { $null }
101
+ sha256 = if ($build.PSObject.Properties['artifact_sha256']) { $build.artifact_sha256 } else { $null }
102
+ detail = if ($build.PSObject.Properties['diagnostics']) { $build.diagnostics } elseif ($build.PSObject.Properties['error']) { $build.error } else { $null }
103
103
  }
104
104
 
105
105
  # --- evidence: build.json + hardware.json (Spec §25; identity probing in real
@@ -114,8 +114,11 @@ if ($Mode -eq 'real') {
114
114
  $ahilTool = $null
115
115
  $g2 = Get-Command agentic-hil -ErrorAction SilentlyContinue
116
116
  if ($g2) { $ahilTool = $g2.Source }
117
- if (-not $ahilTool -and (Test-Path -LiteralPath (Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'))) {
118
- $ahilTool = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
117
+ if (-not $ahilTool) {
118
+ $c1 = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
119
+ $c2 = Join-Path $repoRoot '.venv/bin/agentic-hil'
120
+ if (Test-Path -LiteralPath $c1) { $ahilTool = $c1 }
121
+ elseif (Test-Path -LiteralPath $c2) { $ahilTool = $c2 }
119
122
  }
120
123
  if ($ahilTool) {
121
124
  $probes = Invoke-FwProcess -FilePath $ahilTool -Arguments @('debugger-probes') -WorkingDirectory $repoRoot -TimeoutMs 60000
@@ -76,18 +76,25 @@ function New-FwError {
76
76
  function Resolve-FwPython {
77
77
  <#
78
78
  .SYNOPSIS
79
- Locate a project Python: prefer .venv\Scripts\python.exe relative to
80
- repo root, then PATH. Never hardcoded absolute paths (Spec §25).
79
+ Locate a project Python: prefer .venv\Scripts\python.exe (Windows) or
80
+ .venv/bin/python (Linux/macOS) relative to repo root, then PATH.
81
+ Never hardcoded absolute paths (Spec §25).
81
82
  #>
82
83
  param([string]$RepoRoot)
83
84
  $candidates = @()
84
85
  if ($RepoRoot) {
85
86
  $candidates += (Join-Path $RepoRoot '.venv\Scripts\python.exe')
87
+ $candidates += (Join-Path $RepoRoot '.venv\bin\python')
88
+ $candidates += (Join-Path $RepoRoot '.venv/bin/python')
86
89
  $candidates += (Join-Path $RepoRoot 'venv\Scripts\python.exe')
90
+ $candidates += (Join-Path $RepoRoot 'venv\bin\python')
91
+ $candidates += (Join-Path $RepoRoot 'venv/bin/python')
87
92
  }
88
93
  foreach ($path in $candidates) {
89
94
  if (Test-Path -LiteralPath $path) { return (Resolve-Path -LiteralPath $path).Path }
90
95
  }
96
+ $fromPath = Get-Command python3 -ErrorAction SilentlyContinue
97
+ if ($fromPath) { return $fromPath.Source }
91
98
  $fromPath = Get-Command python -ErrorAction SilentlyContinue
92
99
  if ($fromPath) { return $fromPath.Source }
93
100
  return $null
@@ -19,13 +19,20 @@ Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
19
19
 
20
20
  $repoRoot = Get-FwRepoRoot
21
21
  $python = Resolve-FwPython -RepoRoot $repoRoot
22
- $venvBin = if ($python -and $python -match '.venv[\\/]Scripts[\\/]python.exe$') { Split-Path $python } else { $null }
22
+ $venvBin = if ($python -and ($python -match '[\\/]\.venv[\\/](Scripts|bin)[\\/]python(\.exe)?$')) { Split-Path $python } else { $null }
23
23
 
24
24
  function Test-Tool {
25
25
  param([string]$Name)
26
26
  # prefer the project venv bin dir (tools installed without polluting PATH)
27
- $candidate = if ($venvBin) { Join-Path $venvBin ($Name + '.exe') } else { $null }
28
- $cmd = if ($candidate -and (Test-Path -LiteralPath $candidate)) {
27
+ $candidate = $null
28
+ if ($venvBin) {
29
+ if (Test-Path -LiteralPath (Join-Path $venvBin ($Name + '.exe'))) {
30
+ $candidate = Join-Path $venvBin ($Name + '.exe')
31
+ } elseif (Test-Path -LiteralPath (Join-Path $venvBin $Name)) {
32
+ $candidate = Join-Path $venvBin $Name
33
+ }
34
+ }
35
+ $cmd = if ($candidate) {
29
36
  Get-Item -LiteralPath $candidate
30
37
  } else {
31
38
  Get-Command $Name -ErrorAction SilentlyContinue
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.0.12).
3
+ fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.0.13).
4
4
 
5
5
  Exposes high-level firmware engineering and hardware management tools to AI Coding
6
6
  Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, Cursor) via the Model Context
@@ -1160,7 +1160,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1160
1160
  },
1161
1161
  "serverInfo": {
1162
1162
  "name": "firmwareloop",
1163
- "version": "0.0.12"
1163
+ "version": "0.0.13"
1164
1164
  }
1165
1165
  }
1166
1166
  }
@@ -1252,7 +1252,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1252
1252
  def handle_cli_update() -> int:
1253
1253
  """Handle `firmwareloop update` command."""
1254
1254
  print("============================================================")
1255
- print(" FirmwareLoop Auto-Updater (v0.0.12)")
1255
+ print(" FirmwareLoop Auto-Updater (v0.0.13)")
1256
1256
  print("============================================================")
1257
1257
 
1258
1258
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -1291,20 +1291,28 @@ def handle_cli_update() -> int:
1291
1291
  print("============================================================")
1292
1292
  return 0
1293
1293
  else:
1294
- print("[*] Global / uv-managed installation detected.")
1294
+ print("[*] Global installation detected.")
1295
1295
  uv_path = shutil.which("uv")
1296
1296
  if uv_path:
1297
- print("[*] Refreshing uv cache for firmwareloop...")
1297
+ print("[*] Upgrading firmwareloop via `uv tool upgrade firmwareloop`...")
1298
1298
  try:
1299
- subprocess.run([uv_path, "cache", "clean", "firmwareloop"], capture_output=True)
1300
- print("[+] uv cache refreshed! The next run will automatically fetch the latest release from GitHub.")
1301
- return 0
1299
+ res = subprocess.run([uv_path, "tool", "upgrade", "firmwareloop"])
1300
+ if res.returncode == 0:
1301
+ print("\n============================================================")
1302
+ print("[+] FirmwareLoop has been upgraded to the latest version!")
1303
+ print("============================================================")
1304
+ return 0
1305
+ else:
1306
+ print(f"[-] uv tool upgrade returned code {res.returncode}")
1307
+ return res.returncode
1302
1308
  except Exception as e:
1303
- print(f"[-] Error refreshing uv cache: {e}")
1309
+ print(f"[-] Error upgrading via uv tool: {e}")
1304
1310
  return 1
1305
1311
  else:
1306
- print("[*] Upgrading via pip...")
1312
+ print("[*] Upgrading via pip (`pip install --upgrade firmwareloop`)...")
1307
1313
  res = subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "firmwareloop"])
1314
+ if res.returncode == 0:
1315
+ print("[+] FirmwareLoop has been upgraded to the latest version!")
1308
1316
  return res.returncode
1309
1317
 
1310
1318
 
@@ -1316,10 +1324,78 @@ def handle_cli_doctor() -> int:
1316
1324
  return res.returncode
1317
1325
 
1318
1326
 
1327
+ QODER_USER_MCP_SERVERS = {
1328
+ "firmwareloop": {"command": "fwloop", "type": "stdio"},
1329
+ "agentic-hil": {"command": "agentic-hil", "args": ["mcp-stdio"], "type": "stdio"},
1330
+ }
1331
+
1332
+ QODER_USER_MCP_EDITIONS = (
1333
+ ("Qoder", ".qoder"),
1334
+ ("Qoder CN", ".qoder-cn"),
1335
+ )
1336
+
1337
+
1338
+ def upsert_qoder_mcp_json(mcp_path: str, servers: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
1339
+ """Merge MCP servers into a Qoder user-level ~/.qoder[-cn]/mcp.json without dropping existing entries."""
1340
+ payload = dict(servers or QODER_USER_MCP_SERVERS)
1341
+ data: Dict[str, Any] = {}
1342
+ if os.path.exists(mcp_path):
1343
+ try:
1344
+ with open(mcp_path, "r", encoding="utf-8-sig") as f:
1345
+ loaded = json.load(f)
1346
+ if isinstance(loaded, dict):
1347
+ data = loaded
1348
+ except Exception:
1349
+ data = {}
1350
+ mcp_servers = data.get("mcpServers")
1351
+ if not isinstance(mcp_servers, dict):
1352
+ mcp_servers = {}
1353
+ mcp_servers.update(payload)
1354
+ data["mcpServers"] = mcp_servers
1355
+ parent = os.path.dirname(mcp_path)
1356
+ if parent:
1357
+ os.makedirs(parent, exist_ok=True)
1358
+ with open(mcp_path, "w", encoding="utf-8") as f:
1359
+ json.dump(data, f, indent=2)
1360
+ f.write("\n")
1361
+ return data
1362
+
1363
+
1364
+ def configure_qoder_user_mcp(user_home: str) -> List[Dict[str, Any]]:
1365
+ """Write firmwareloop MCP entries into installed Qoder / Qoder CN user mcp.json files."""
1366
+ results: List[Dict[str, Any]] = []
1367
+ for label, folder in QODER_USER_MCP_EDITIONS:
1368
+ edition_dir = os.path.join(user_home, folder)
1369
+ mcp_file = os.path.join(edition_dir, "mcp.json")
1370
+ if not os.path.isdir(edition_dir):
1371
+ results.append({
1372
+ "edition": label,
1373
+ "ok": False,
1374
+ "path": mcp_file,
1375
+ "reason": "not_installed",
1376
+ })
1377
+ continue
1378
+ try:
1379
+ upsert_qoder_mcp_json(mcp_file)
1380
+ results.append({
1381
+ "edition": label,
1382
+ "ok": True,
1383
+ "path": mcp_file,
1384
+ })
1385
+ except Exception as exc:
1386
+ results.append({
1387
+ "edition": label,
1388
+ "ok": False,
1389
+ "path": mcp_file,
1390
+ "reason": str(exc),
1391
+ })
1392
+ return results
1393
+
1394
+
1319
1395
  def handle_cli_setup() -> int:
1320
1396
  """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, and Qoder IDE."""
1321
1397
  print("============================================================")
1322
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.0.12)")
1398
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.0.13)")
1323
1399
  print("============================================================")
1324
1400
  print("")
1325
1401
 
@@ -1372,22 +1448,24 @@ def handle_cli_setup() -> int:
1372
1448
  except Exception as e:
1373
1449
  print(f" - Global MCP Config: [Warning] Could not update .claude.json: {e}")
1374
1450
 
1375
- # 3. Qoder IDE
1376
- print("\n[3] Qoder IDE:")
1377
- qoder_cmd = shutil.which("qoder.cmd") or shutil.which("qoder")
1378
- if qoder_cmd:
1379
- try:
1380
- r1 = subprocess.run([qoder_cmd, "--add-mcp", json.dumps({"name": "firmwareloop", "command": "fwloop"})], shell=True, capture_output=True, text=True)
1381
- r2 = subprocess.run([qoder_cmd, "--add-mcp", json.dumps({"name": "agentic-hil", "command": "agentic-hil", "args": ["mcp-stdio"]})], shell=True, capture_output=True, text=True)
1382
- if r1.returncode == 0 and r2.returncode == 0:
1383
- print(" - User Profile MCP: [OK] Automatically registered to Qoder User Profile")
1384
- configured_agents.append("Qoder IDE")
1385
- else:
1386
- print(f" - User Profile MCP: [Notice] Qoder CLI output: {r1.stdout.strip()}")
1387
- except Exception as e:
1388
- print(f" - User Profile MCP: [Warning] Could not invoke qoder CLI: {e}")
1451
+ # 3. Qoder / Qoder CN user-level MCP
1452
+ # IDE settings read ~/.qoder/mcp.json and ~/.qoder-cn/mcp.json.
1453
+ # qoder.cmd --add-mcp writes the VS Code-style %APPDATA%\Qoder\User\mcp.json, which the settings page does not show.
1454
+ print("\n[3] Qoder IDE / Qoder CN:")
1455
+ qoder_results = configure_qoder_user_mcp(user_home)
1456
+ qoder_ok = [item for item in qoder_results if item.get("ok")]
1457
+ if qoder_ok:
1458
+ for item in qoder_ok:
1459
+ print(f" - {item['edition']}: [OK] Written to {item['path']}")
1460
+ configured_agents.append("Qoder")
1389
1461
  else:
1390
- print(" - User Profile MCP: Qoder CLI not in PATH (Workspace .mcp.json supported)")
1462
+ print(" - User MCP: neither ~/.qoder nor ~/.qoder-cn found (Workspace .mcp.json still supported)")
1463
+ for item in qoder_results:
1464
+ if item.get("ok"):
1465
+ continue
1466
+ if item.get("reason") == "not_installed":
1467
+ continue
1468
+ print(f" - {item['edition']}: [Warning] Could not write {item['path']}: {item.get('reason')}")
1391
1469
 
1392
1470
  # 4. Global Skills Sync for Antigravity and Claude Code
1393
1471
  ag_skills_dir = os.path.join(user_home, ".gemini", "antigravity-cli", "skills")
@@ -1422,7 +1500,7 @@ def handle_cli_setup() -> int:
1422
1500
  def handle_cli_init() -> int:
1423
1501
  """Handle `firmwareloop init` command."""
1424
1502
  print("============================================================")
1425
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.0.12)")
1503
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.0.13)")
1426
1504
  print("============================================================")
1427
1505
  cwd = os.getcwd()
1428
1506
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
@@ -1443,7 +1521,7 @@ def handle_cli_init() -> int:
1443
1521
 
1444
1522
 
1445
1523
  def print_cli_help() -> None:
1446
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.0.12)
1524
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.0.13)
1447
1525
 
1448
1526
  Usage:
1449
1527
  fwloop [command] (or: firmwareloop [command])
@@ -1474,7 +1552,7 @@ def main() -> None:
1474
1552
  elif cmd in ["setup", "register"]:
1475
1553
  sys.exit(handle_cli_setup())
1476
1554
  elif cmd in ["version", "-v", "--version"]:
1477
- print("FirmwareLoop v0.0.12")
1555
+ print("FirmwareLoop v0.0.13")
1478
1556
  sys.exit(0)
1479
1557
  elif cmd in ["help", "-h", "--help"]:
1480
1558
  print_cli_help()
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Dual-Tier MCP setup and registration helper (v0.0.12).
3
+ Dual-Tier MCP setup and registration helper (v0.0.13).
4
4
  Inspects installed AI coding agents (Qoder, Claude Code, Antigravity, Cursor)
5
5
  and configures global MCP / skills or prints exact registration commands.
6
6
 
@@ -87,31 +87,69 @@ $report.agents.claude_code = [ordered]@{
87
87
  )
88
88
  }
89
89
 
90
- # 2. Qoder IDE / CLI 全局 User Profile MCP 自动配置
91
- $qoderCmd = $null
92
- foreach ($name in @('qoder.cmd', 'qoder', 'qodercli')) {
93
- $c = Get-Command $name -ErrorAction SilentlyContinue
94
- if ($c) { $qoderCmd = $c; break }
90
+ # 2. Qoder / Qoder CN user-level MCP (~/.qoder/mcp.json, ~/.qoder-cn/mcp.json)
91
+ # The IDE settings page reads these files. qoder.cmd --add-mcp writes
92
+ # %APPDATA%\Qoder\User\mcp.json, which the settings UI does not show.
93
+ function Merge-FwQoderUserMcp {
94
+ param([string]$McpPath)
95
+ $data = [ordered]@{ mcpServers = [ordered]@{} }
96
+ if (Test-Path -LiteralPath $McpPath) {
97
+ try {
98
+ $existing = Get-Content -LiteralPath $McpPath -Raw -Encoding UTF8 | ConvertFrom-Json
99
+ if ($existing.PSObject.Properties['mcpServers'] -and $existing.mcpServers) {
100
+ $data.mcpServers = [ordered]@{}
101
+ $existing.mcpServers.PSObject.Properties | ForEach-Object {
102
+ $data.mcpServers[$_.Name] = $_.Value
103
+ }
104
+ }
105
+ } catch {
106
+ $data = [ordered]@{ mcpServers = [ordered]@{} }
107
+ }
108
+ }
109
+ $data.mcpServers['firmwareloop'] = [pscustomobject]@{ command = 'fwloop'; type = 'stdio' }
110
+ $data.mcpServers['agentic-hil'] = [pscustomobject]@{ command = 'agentic-hil'; args = @('mcp-stdio'); type = 'stdio' }
111
+ $parent = Split-Path -Parent $McpPath
112
+ if (-not (Test-Path -LiteralPath $parent)) {
113
+ New-Item -ItemType Directory -Force -Path $parent | Out-Null
114
+ }
115
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
116
+ [System.IO.File]::WriteAllText($McpPath, ((ConvertTo-Json -InputObject $data -Depth 10) + [Environment]::NewLine), $utf8NoBom)
95
117
  }
96
- $qoderConfigured = $false
97
- if ($qoderCmd) {
98
- try {
99
- $fwJsonStr = '{"name":"firmwareloop","command":"fwloop"}'
100
- $ahilJsonStr = '{"name":"agentic-hil","command":"agentic-hil","args":["mcp-stdio"]}'
101
- & $qoderCmd.Source --add-mcp $fwJsonStr | Out-Null
102
- & $qoderCmd.Source --add-mcp $ahilJsonStr | Out-Null
103
- $qoderConfigured = $true
104
- } catch {
105
- # ignore non-fatal error
118
+
119
+ $qoderEditions = @(
120
+ [ordered]@{ name = 'Qoder'; dirName = '.qoder' },
121
+ [ordered]@{ name = 'Qoder CN'; dirName = '.qoder-cn' }
122
+ )
123
+ $qoderWritten = @()
124
+ $qoderResults = @()
125
+ foreach ($edition in $qoderEditions) {
126
+ $editionDir = Join-Path $env:USERPROFILE $edition.dirName
127
+ $mcpFile = Join-Path $editionDir 'mcp.json'
128
+ $entry = [ordered]@{
129
+ name = $edition.name
130
+ path = $mcpFile
131
+ detected = (Test-Path -LiteralPath $editionDir)
132
+ configured = $false
133
+ }
134
+ if ($entry.detected) {
135
+ try {
136
+ Merge-FwQoderUserMcp -McpPath $mcpFile
137
+ $entry.configured = $true
138
+ $qoderWritten += $mcpFile
139
+ } catch {
140
+ $entry.error = $_.Exception.Message
141
+ }
106
142
  }
143
+ $qoderResults += [pscustomobject]$entry
107
144
  }
145
+ $qoderConfigured = ($qoderWritten.Count -gt 0)
108
146
  $report.agents.qoder = [ordered]@{
109
- detected = ($null -ne $qoderCmd)
110
- path = if ($qoderCmd) { $qoderCmd.Source } else { $null }
147
+ detected = ($qoderResults | Where-Object { $_.detected } | Measure-Object).Count -gt 0
111
148
  user_profile_configured = $qoderConfigured
149
+ written_files = $qoderWritten
150
+ editions = $qoderResults
112
151
  registration_commands = @(
113
- "qoder.cmd --add-mcp '{\`"name\`":\`"firmwareloop\`",\`"command\`":\`"fwloop\`"}'",
114
- "qoder.cmd --add-mcp '{\`"name\`":\`"agentic-hil\`",\`"command\`":\`"agentic-hil\`",\`"args\`":[\`"mcp-stdio\`"]}'"
152
+ "fwloop setup # writes ~/.qoder/mcp.json and ~/.qoder-cn/mcp.json"
115
153
  )
116
154
  }
117
155
 
@@ -204,7 +242,7 @@ if ($Json) {
204
242
  Write-FwJson ([pscustomobject]$report)
205
243
  } else {
206
244
  Write-Host "============================================================" -ForegroundColor Cyan
207
- Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.0.10)" -ForegroundColor Green
245
+ Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.0.13)" -ForegroundColor Green
208
246
  Write-Host "============================================================" -ForegroundColor Cyan
209
247
  Write-Host ""
210
248
  Write-Host "[1] Google Antigravity / Gemini CLI:" -ForegroundColor Yellow
@@ -221,11 +259,15 @@ if ($Json) {
221
259
  Write-Host " $($report.agents.claude_code.registration_commands[0])"
222
260
  Write-Host " $($report.agents.claude_code.registration_commands[1])"
223
261
  Write-Host ""
224
- Write-Host "[3] Qoder IDE:" -ForegroundColor Yellow
225
- if ($qoderConfigured) {
226
- Write-Host " - User Profile MCP: [OK] Automatically registered to Qoder User Profile" -ForegroundColor Green
227
- } else {
228
- Write-Host " - User Profile MCP: Run $($report.agents.qoder.registration_commands[0])"
262
+ Write-Host "[3] Qoder IDE / Qoder CN:" -ForegroundColor Yellow
263
+ foreach ($edition in $qoderResults) {
264
+ if ($edition.configured) {
265
+ Write-Host " - $($edition.name): [OK] Written to $($edition.path)" -ForegroundColor Green
266
+ } elseif ($edition.detected) {
267
+ Write-Host " - $($edition.name): [Warning] Could not write $($edition.path)"
268
+ } else {
269
+ Write-Host " - $($edition.name): not installed ($($edition.path))"
270
+ }
229
271
  }
230
272
  Write-Host " - Workspace: Project root '.mcp.json' is automatically detected"
231
273
  Write-Host ""
@@ -64,11 +64,20 @@ foreach ($c in $cases) {
64
64
  }
65
65
 
66
66
  # ---- 2. fake executable execution (keil / iar / west / esp-idf) ----------------
67
+ $runningOnWindows = $IsWindows -or ($null -ne $env:OS -and $env:OS -like "*Windows*")
68
+ $ext = if ($runningOnWindows) { '.cmd' } else { '.sh' }
69
+
70
+ if (-not $runningOnWindows) {
71
+ Get-ChildItem -LiteralPath $fake -Filter *.sh | ForEach-Object {
72
+ try { & chmod +x $_.FullName } catch { }
73
+ }
74
+ }
75
+
67
76
  $execCases = @(
68
- @{ name = 'keil'; args = @('-Backend', 'keil', '-SourceDir', 'tests/backend/fixtures/keil-proj', '-BackendTool', (Join-Path $fake 'UV4.cmd'), '-Json') },
69
- @{ name = 'iar'; args = @('-Backend', 'iar', '-SourceDir', 'tests/backend/fixtures/iar-proj', '-BackendTool', (Join-Path $fake 'IarBuild.cmd'), '-Json') },
70
- @{ name = 'zephyr'; args = @('-Backend', 'zephyr', '-BackendTool', (Join-Path $fake 'west.cmd'), '-Json') },
71
- @{ name = 'esp-idf'; args = @('-Backend', 'esp-idf', '-BackendTool', (Join-Path $fake 'idf.py.cmd'), '-Json') }
77
+ @{ name = 'keil'; args = @('-Backend', 'keil', '-SourceDir', 'tests/backend/fixtures/keil-proj', '-BackendTool', (Join-Path $fake "UV4$ext"), '-Json') },
78
+ @{ name = 'iar'; args = @('-Backend', 'iar', '-SourceDir', 'tests/backend/fixtures/iar-proj', '-BackendTool', (Join-Path $fake "IarBuild$ext"), '-Json') },
79
+ @{ name = 'zephyr'; args = @('-Backend', 'zephyr', '-BackendTool', (Join-Path $fake "west$ext"), '-Json') },
80
+ @{ name = 'esp-idf'; args = @('-Backend', 'esp-idf', '-BackendTool', (Join-Path $fake "idf.py$ext"), '-Json') }
72
81
  )
73
82
  foreach ($c in $execCases) {
74
83
  $r = Invoke-Build -ToolArgs $c.args
@@ -80,11 +89,15 @@ foreach ($c in $execCases) {
80
89
  }
81
90
 
82
91
  # ---- 3. cmake real build still green -------------------------------------------
83
- $r = Invoke-Build -ToolArgs @('-Configuration', 'Debug', '-Clean', '-Json')
92
+ $r = Invoke-Build -ToolArgs @('-Backend', 'cmake', '-Configuration', 'Debug', '-Clean', '-Json')
84
93
  $jsonLine = ($r.stdout -split "`r?`n" | Where-Object { $_.TrimStart().StartsWith('{') } | Select-Object -Last 1)
85
- $j = $jsonLine | ConvertFrom-Json
86
- Assert -Name 'real.cmake' -Ok ($r.exit_code -eq 0 -and $j.ok -and $j.artifacts.primary.path -like '*firmware.elf*') `
87
- -Detail "exit=$($r.exit_code) artifact=$($j.artifacts.primary.path)"
94
+ $j = if ($jsonLine) { try { $jsonLine | ConvertFrom-Json } catch { $null } } else { $null }
95
+ $hasArtifact = $false
96
+ if ($j -and $j.PSObject.Properties['ok'] -and $j.ok -and $j.PSObject.Properties['artifacts'] -and $j.artifacts.PSObject.Properties['primary'] -and $j.artifacts.primary.PSObject.Properties['path']) {
97
+ $hasArtifact = $j.artifacts.primary.path -like '*firmware.elf*'
98
+ }
99
+ Assert -Name 'real.cmake' -Ok ($r.exit_code -eq 0 -and $hasArtifact) `
100
+ -Detail "exit=$($r.exit_code) output=$jsonLine"
88
101
 
89
102
  # ---- summary -------------------------------------------------------------------
90
103
  $failed = @($results | Where-Object { -not $_.ok }).Count
@@ -45,8 +45,9 @@ $env:FW_RUN_DIR = $runsDir
45
45
  $doctor = Invoke-FwProcess -FilePath 'pwsh' -Arguments @('-NoProfile', '-NonInteractive', '-File', (Join-Path $PSScriptRoot 'doctor.ps1'), '-Json') -WorkingDirectory $repoRoot -TimeoutMs 120000
46
46
  if ($doctor.exit_code -in @(0, 1)) {
47
47
  try {
48
- $d = $doctor.stdout | ConvertFrom-Json
49
- if ($d.checks) {
48
+ $jsonLine = ($doctor.stdout -split "`r?`n" | Where-Object { $_.TrimStart().StartsWith('{') } | Select-Object -Last 1)
49
+ $d = if ($jsonLine) { try { $jsonLine | ConvertFrom-Json } catch { $null } } else { $null }
50
+ if ($d -and $d.PSObject.Properties['checks'] -and $d.checks) {
50
51
  $deps = [ordered]@{}
51
52
  foreach ($p in $d.checks.PSObject.Properties) {
52
53
  $v = $p.Value
@@ -55,7 +56,7 @@ if ($doctor.exit_code -in @(0, 1)) {
55
56
  }
56
57
  $deps | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $runsDir 'dependencies.json') -Encoding utf8
57
58
  }
58
- if ($d.host) {
59
+ if ($d -and $d.PSObject.Properties['host'] -and $d.host) {
59
60
  $d.host | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $runsDir 'environment.json') -Encoding utf8
60
61
  }
61
62
  } catch {
File without changes
File without changes