@rse/ase 1.0.7 → 2.0.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.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +357 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +8 -1
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +269 -81
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +231 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +81 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
package/dst/ase-setup.js CHANGED
@@ -3,175 +3,72 @@
3
3
  ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
- import fs from "node:fs/promises";
7
- import os from "node:os";
8
6
  import path from "node:path";
9
7
  import { fileURLToPath } from "node:url";
10
8
  import { execa } from "execa";
11
- import which from "which";
12
- import * as dotenvx from "@dotenvx/dotenvx";
13
9
  import chalk from "chalk";
14
- import writeFileAtomic from "write-file-atomic";
15
- import { mkdirp } from "mkdirp";
16
- import JsonAsty from "json-asty";
17
- import Version from "./ase-version.js";
18
- import { renderTable } from "./ase-table.js";
19
- const toolSpecs = {
20
- "claude": { cli: "claude", label: "Anthropic Claude Code CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
21
- "copilot": { cli: "copilot", label: "GitHub Copilot CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
22
- "codex": { cli: "codex", label: "OpenAI Codex CLI", pInstall: "add", pRemove: "remove", pUpdate: "upgrade" }
23
- };
10
+ import Version from "./ase-lib-version.js";
11
+ import { renderTable } from "./ase-lib-table.js";
12
+ import { SetupMcp } from "./ase-setup-mcp.js";
13
+ import { SetupSettings } from "./ase-setup-settings.js";
14
+ import { toolSpecs, parseTool, parseScope, requireClaudeScope, SetupRunner } from "./ase-setup-common.js";
24
15
  /* CLI command "ase setup" */
25
16
  export default class SetupCommand {
26
17
  log;
18
+ runner;
19
+ mcp;
20
+ settings;
27
21
  constructor(log) {
28
22
  this.log = log;
23
+ this.runner = new SetupRunner(log);
24
+ this.mcp = new SetupMcp(log, this.runner);
25
+ this.settings = new SetupSettings(log);
29
26
  }
30
- /* ensure a tool is available */
31
- async ensureTool(tool) {
32
- return which(tool).catch(() => {
33
- throw new Error(`mandatory tool "${tool}" not found in $PATH`);
34
- });
35
- }
36
- /* determine whether a global "npm" operation requires "sudo" by
37
- checking whether the npm global install root is writable by the
38
- current user; on Windows or when already running as root, no
39
- elevation is needed */
40
- async npmGlobalNeedsSudo() {
41
- /* Windows has no "sudo" concept here */
42
- if (process.platform === "win32")
43
- return false;
44
- /* already running as root */
45
- const getuid = process.getuid;
46
- if (typeof getuid === "function" && getuid.call(process) === 0)
47
- return false;
48
- /* determine the npm global prefix and probe writability of the
49
- directories that "npm -g" actually mutates */
50
- let prefix;
51
- try {
52
- const result = await execa("npm", ["prefix", "-g"], { stdio: "pipe" });
53
- prefix = result.stdout.trim();
54
- }
55
- catch {
56
- /* if we cannot determine the prefix, fall back to "no sudo" */
57
- return false;
58
- }
59
- if (prefix === "")
60
- return false;
61
- const candidates = [
62
- prefix,
63
- path.join(prefix, "bin"),
64
- path.join(prefix, "lib", "node_modules")
65
- ];
66
- const accessible = (dir, mode) => fs.access(dir, mode).then(() => true, () => false);
67
- for (const dir of candidates) {
68
- /* a writable directory needs no elevation at all */
69
- if (await accessible(dir, fs.constants.W_OK))
70
- continue;
71
- /* the directory exists, but is not writable: require sudo */
72
- if (await accessible(dir, fs.constants.F_OK))
73
- return true;
74
- /* the directory does not exist: require sudo unless it can
75
- be created inside a writable parent directory */
76
- if (!(await accessible(path.dirname(dir), fs.constants.W_OK)))
77
- return true;
78
- }
79
- return false;
80
- }
81
- /* build the (cmd, args) pair for an "npm" invocation, prefixing
82
- with "sudo" when necessary for global operations */
83
- async npmCmd(args, global) {
84
- if (global && await this.npmGlobalNeedsSudo()) {
85
- const sudo = await which("sudo").catch(() => "");
86
- if (sudo !== "") {
87
- this.log.write("info", "setup: npm global install root not writable: using \"sudo\"");
88
- return { cmd: "sudo", args: ["npm", ...args] };
89
- }
90
- this.log.write("warning", "setup: npm global install root is not writable by current user " +
91
- "and \"sudo\" not found in $PATH: attempting without elevation");
92
- }
93
- return { cmd: "npm", args };
94
- }
95
- /* run a sub-process, suppressing output on success and emitting it on failure */
96
- async run(cmd, args, opts = {}) {
97
- const { cwd, quiet = false, retries = 1, ignoreError } = opts;
98
- const argsLog = args.map((arg) => arg.replace(/(\w*key=)([^\s&]+)/gi, (_, k, v) => k + "*".repeat(v.length)));
99
- this.log.write("info", `setup: $ ${cmd} ${argsLog.join(" ")}` +
100
- (cwd !== undefined ? ` (cwd: ${cwd})` : ""));
101
- for (let i = 0; i < retries; i++) {
102
- const final = (i === retries - 1);
103
- try {
104
- await execa(cmd, args, { stdio: quiet ? "ignore" : "pipe", cwd });
105
- return;
106
- }
107
- catch (err) {
108
- if (!final) {
109
- this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${args.join(" ")}": retrying...`);
110
- await new Promise((resolve) => setTimeout(resolve, 1000));
111
- continue;
112
- }
113
- if (ignoreError !== undefined) {
114
- this.log.write("info", `setup: ${ignoreError} (skipped)`);
115
- return;
116
- }
117
- const e = err;
118
- const exitCode = typeof e.exitCode === "number" ? e.exitCode : -1;
119
- this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
120
- if (typeof e.stdout === "string" && e.stdout.length > 0) {
121
- this.log.write("error", "setup: command failed: stdout:");
122
- process.stdout.write(e.stdout);
123
- }
124
- if (typeof e.stderr === "string" && e.stderr.length > 0) {
125
- this.log.write("error", "setup: command failed: stderr:");
126
- process.stderr.write(e.stderr);
127
- }
128
- throw err;
129
- }
130
- }
131
- }
132
- /* handler for "ase setup install" (both tools) */
27
+ /* handler for "ase setup install" (all tools) */
133
28
  async doInstall(tool, dev, scope) {
134
- this.requireClaudeScope(tool, scope);
29
+ requireClaudeScope(tool, scope);
135
30
  const spec = toolSpecs[tool];
136
- await this.ensureTool("npm");
137
- await this.ensureTool(spec.cli);
31
+ await this.runner.ensureTool("npm");
32
+ await this.runner.ensureTool(spec.cli);
138
33
  this.log.write("info", `setup: install${dev ? "[dev]" : ""}: used ASE version: ${Version.current()}`);
139
34
  this.log.write("info", `setup: install${dev ? "[dev]" : ""}: ` +
140
35
  `installing ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote/bundled"})`);
141
36
  const pkgdir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
142
- const source = dev ? path.resolve(pkgdir, "..") : pkgdir;
37
+ const source = !dev ? pkgdir : (process.env.ASE_SETUP_DEV_BASEDIR ?? path.resolve(pkgdir, ".."));
143
38
  const scopeArgs = tool === "claude" && scope !== "user" ? ["--scope", scope] : [];
144
- await this.run(spec.cli, ["plugin", "marketplace", "add", source, ...scopeArgs]);
145
- await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
39
+ await this.runner.run(spec.cli, ["plugin", "marketplace", "add", source, ...scopeArgs]);
40
+ await this.runner.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
146
41
  /* select the plugin-shipped ASE output style (Anthropic Claude Code CLI only) */
147
42
  if (tool === "claude")
148
- await this.outputStyleActivate(`install${dev ? "[dev]" : ""}`, scope);
43
+ await this.settings.outputStyleActivate(`install${dev ? "[dev]" : ""}`, scope);
149
44
  return 0;
150
45
  }
151
- /* handler for "ase setup update" (both tools) */
46
+ /* handler for "ase setup update" (all tools) */
152
47
  async doUpdate(tool, force, dev, scope) {
153
- this.requireClaudeScope(tool, scope);
48
+ requireClaudeScope(tool, scope);
154
49
  const spec = toolSpecs[tool];
155
- await this.ensureTool("npm");
156
- await this.ensureTool(spec.cli);
50
+ await this.runner.ensureTool("npm");
51
+ await this.runner.ensureTool(spec.cli);
157
52
  const scopeArgs = tool === "claude" && scope !== "user" ? ["--scope", scope] : [];
158
53
  /* best-effort stop of background service */
159
54
  this.log.write("info", `setup: update${dev ? "[dev]" : ""}: ` +
160
55
  "stopping potentially running ASE service");
161
- await this.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
56
+ await this.runner.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
162
57
  if (dev) {
163
58
  /* update ASE CLI Tool */
164
59
  this.log.write("info", `setup: update[dev]: used ASE version: ${Version.current()}`);
165
60
  this.log.write("info", "setup: update[dev]: re-build ASE CLI tool (origin: local)");
166
- const tooldir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
167
- await this.run("npm", ["install"], { cwd: tooldir });
168
- await this.run("npm", ["start", "build"], { cwd: tooldir });
61
+ const pkgdir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
62
+ const tooldir = process.env.ASE_SETUP_DEV_BASEDIR !== undefined ?
63
+ path.join(process.env.ASE_SETUP_DEV_BASEDIR, "tool") : pkgdir;
64
+ await this.runner.run("npm", ["install"], { cwd: tooldir });
65
+ await this.runner.run("npm", ["start", "build"], { cwd: tooldir });
169
66
  /* in development mode the local plugin files are already current
170
67
  but there is no version change in the plugin manifest,
171
68
  so just re-install the plugin to let the tool update its copy */
172
69
  this.log.write("info", `setup: update[dev]: re-install ASE ${spec.label} plugin (origin: local)`);
173
- await this.run(spec.cli, ["plugin", spec.pRemove, "ase@ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin not installed` });
174
- await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
70
+ await this.runner.run(spec.cli, ["plugin", spec.pRemove, "ase@ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin not installed` });
71
+ await this.runner.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
175
72
  }
176
73
  else {
177
74
  /* perform NPM version check */
@@ -184,32 +81,32 @@ export default class SetupCommand {
184
81
  }
185
82
  /* update ASE CLI tool */
186
83
  this.log.write("info", `setup: update: updating ASE CLI tool: ${current} -> ${latest}`);
187
- const updateCmd = await this.npmCmd(["update", "-g", "@rse/ase"], true);
188
- await this.run(updateCmd.cmd, updateCmd.args);
84
+ const updateCmd = await this.runner.npmCmd(["update", "-g", "@rse/ase"], true);
85
+ await this.runner.run(updateCmd.cmd, updateCmd.args);
189
86
  /* update ASE plugin (refresh the marketplace snapshot, then
190
87
  update the plugin itself; the OpenAI Codex CLI has no
191
88
  "plugin update" subcommand, so re-install it instead;
192
89
  "plugin marketplace update" has no "--scope" option at all,
193
90
  so it never receives one) */
194
91
  this.log.write("info", `setup: update: updating ASE ${spec.label} plugin`);
195
- await this.run(spec.cli, ["plugin", "marketplace", spec.pUpdate, "ase"]);
92
+ await this.runner.run(spec.cli, ["plugin", "marketplace", spec.pUpdate, "ase"]);
196
93
  if (tool !== "codex")
197
- await this.run(spec.cli, ["plugin", "update", "ase@ase", ...scopeArgs]);
94
+ await this.runner.run(spec.cli, ["plugin", "update", "ase@ase", ...scopeArgs]);
198
95
  else {
199
- await this.run(spec.cli, ["plugin", spec.pRemove, "ase@ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin not installed` });
200
- await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
96
+ await this.runner.run(spec.cli, ["plugin", spec.pRemove, "ase@ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin not installed` });
97
+ await this.runner.run(spec.cli, ["plugin", spec.pInstall, "ase@ase", ...scopeArgs], { retries: 3 });
201
98
  }
202
99
  }
203
100
  /* select the plugin-shipped ASE output style (Anthropic Claude Code CLI only) */
204
101
  if (tool === "claude")
205
- await this.outputStyleActivate(`update${dev ? "[dev]" : ""}`, scope);
102
+ await this.settings.outputStyleActivate(`update${dev ? "[dev]" : ""}`, scope);
206
103
  return 0;
207
104
  }
208
- /* handler for "ase setup enable|disable" (both tools) */
105
+ /* handler for "ase setup enable|disable" (all tools) */
209
106
  async doToggle(tool, scope, action) {
210
- this.requireClaudeScope(tool, scope);
107
+ requireClaudeScope(tool, scope);
211
108
  const spec = toolSpecs[tool];
212
- await this.ensureTool(spec.cli);
109
+ await this.runner.ensureTool(spec.cli);
213
110
  this.log.write("info", `setup: ${action}: ${action === "enable" ? "enabling" : "disabling"} ` +
214
111
  `ASE ${spec.label} plugin`);
215
112
  /* the GitHub Copilot CLI and OpenAI Codex CLI have no "plugin
@@ -219,40 +116,40 @@ export default class SetupCommand {
219
116
  const args = tool === "claude" ?
220
117
  ["plugin", action, "ase@ase", ...scopeArgs] :
221
118
  ["plugin", action === "enable" ? spec.pInstall : spec.pRemove, "ase@ase"];
222
- await this.run(spec.cli, args, { retries: tool === "claude" ? 1 : 3 });
119
+ await this.runner.run(spec.cli, args, { retries: tool === "claude" ? 1 : 3 });
223
120
  return 0;
224
121
  }
225
- /* handler for "ase setup uninstall" (both tools) */
122
+ /* handler for "ase setup uninstall" (all tools) */
226
123
  async doUninstall(tool, dev, scope) {
227
- this.requireClaudeScope(tool, scope);
124
+ requireClaudeScope(tool, scope);
228
125
  const spec = toolSpecs[tool];
229
- await this.ensureTool("npm");
230
- await this.ensureTool(spec.cli);
126
+ await this.runner.ensureTool("npm");
127
+ await this.runner.ensureTool(spec.cli);
231
128
  const scopeArgs = tool === "claude" && scope !== "user" ? ["--scope", scope] : [];
232
129
  /* best-effort stop of background service */
233
130
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
234
131
  "stopping potentially running ASE service");
235
- await this.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
132
+ await this.runner.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
236
133
  /* deselect the plugin-shipped ASE output style (Anthropic Claude Code CLI only) */
237
134
  if (tool === "claude")
238
- await this.outputStyleDeactivate(`uninstall${dev ? "[dev]" : ""}`, scope);
135
+ await this.settings.outputStyleDeactivate(`uninstall${dev ? "[dev]" : ""}`, scope);
239
136
  /* uninstall ASE plugin */
240
137
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
241
138
  `uninstalling ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote/bundled"})`);
242
- await this.run(spec.cli, ["plugin", spec.pRemove, "ase@ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin not installed` });
243
- await this.run(spec.cli, ["plugin", "marketplace", "remove", "ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin marketplace not registered` });
139
+ await this.runner.run(spec.cli, ["plugin", spec.pRemove, "ase@ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin not installed` });
140
+ await this.runner.run(spec.cli, ["plugin", "marketplace", "remove", "ase", ...scopeArgs], { ignoreError: `ASE ${spec.label} plugin marketplace not registered` });
244
141
  /* uninstall ASE CLI tool (non-development only) */
245
142
  if (!dev) {
246
143
  this.log.write("info", "setup: uninstall: uninstalling ASE CLI tool (origin: remote)");
247
- const uninstallCmd = await this.npmCmd(["uninstall", "-g", "@rse/ase"], true);
248
- await this.run(uninstallCmd.cmd, uninstallCmd.args);
144
+ const uninstallCmd = await this.runner.npmCmd(["uninstall", "-g", "@rse/ase"], true);
145
+ await this.runner.run(uninstallCmd.cmd, uninstallCmd.args);
249
146
  }
250
147
  return 0;
251
148
  }
252
149
  /* handler for "ase setup status" */
253
150
  async doStatus(tool) {
254
151
  const spec = toolSpecs[tool];
255
- await this.ensureTool(spec.cli);
152
+ await this.runner.ensureTool(spec.cli);
256
153
  this.log.write("info", `setup: status: probing ASE registrations for ${spec.label}`);
257
154
  const rows = [];
258
155
  /* report the ASE plugin registrations, with an absent plugin
@@ -265,19 +162,22 @@ export default class SetupCommand {
265
162
  rows.push(["PLUGIN", chalk.bold("ase@ase"), plugin.scope, plugin.status]);
266
163
  /* report the registered MCP servers, probed concurrently and
267
164
  silently skipping every server which is not registered at all */
268
- const scopes = await Promise.all(this.mcpServers.map((handle) => this.mcpScope(tool, handle.server)));
269
- for (let i = 0; i < this.mcpServers.length; i++) {
165
+ const scopes = await Promise.all(this.mcp.mcpServers.map((handle) => this.mcp.mcpScope(tool, handle.server)));
166
+ for (let i = 0; i < this.mcp.mcpServers.length; i++) {
270
167
  const scope = scopes[i];
271
168
  if (scope === undefined)
272
169
  continue;
273
- rows.push(["MCP", chalk.bold(this.mcpServers[i].server), scope, "registered"]);
170
+ rows.push(["MCP", chalk.bold(this.mcp.mcpServers[i].server), scope, "registered"]);
274
171
  }
275
172
  /* report the ASE output style selections */
276
- for (const outputStyle of await this.outputStyleStatus(tool))
173
+ for (const outputStyle of await this.settings.outputStyleStatus(tool))
277
174
  rows.push(["OUTPUTSTYLE", chalk.bold(outputStyle.file), outputStyle.scope, outputStyle.status]);
278
175
  /* report the ASE statusline registrations */
279
- for (const statusline of await this.statuslineStatus(tool))
176
+ for (const statusline of await this.settings.statuslineStatus(tool))
280
177
  rows.push(["STATUSLINE", chalk.bold(statusline.file), statusline.scope, statusline.status]);
178
+ /* report the ASE telemetry configurations */
179
+ for (const telemetry of await this.settings.telemetryStatus(tool))
180
+ rows.push(["TELEMETRY", chalk.bold(telemetry.file), telemetry.scope, telemetry.status]);
281
181
  process.stdout.write(renderTable(["KIND", "ID", "SCOPE", "STATUS"], rows));
282
182
  return 0;
283
183
  }
@@ -289,14 +189,16 @@ export default class SetupCommand {
289
189
  const entries = [];
290
190
  if (tool === "claude") {
291
191
  /* the Anthropic Claude Code CLI renders one indented block per
292
- plugin, carrying an explicit "Scope:" and "Status:" field */
192
+ plugin, carrying an explicit "Scope:" and "Status:" field
193
+ (field labels can consist of multiple words, e.g. the
194
+ "Read from:" field of directory-based marketplaces) */
293
195
  for (let i = 0; i < lines.length; i++) {
294
196
  if (!/(^|\s)ase@ase\s*$/.test(lines[i]))
295
197
  continue;
296
198
  let scope = "(unknown)";
297
199
  let status = "(unknown)";
298
200
  for (let j = i + 1; j < lines.length; j++) {
299
- const m = lines[j].match(/^\s+(\w+):\s*(.+?)\s*$/);
201
+ const m = lines[j].match(/^\s+(\w[\w ]*):\s*(.+?)\s*$/);
300
202
  if (m === null)
301
203
  break;
302
204
  /* strip the leading "✔"/"✘" glyph of the status value */
@@ -327,696 +229,6 @@ export default class SetupCommand {
327
229
  }
328
230
  return entries;
329
231
  }
330
- /* handler for "ase setup mcp list" */
331
- async doMcpList() {
332
- const rows = [];
333
- for (const handle of this.mcpServers)
334
- rows.push([
335
- chalk.bold(handle.id),
336
- handle.name,
337
- handle.version ?? "(unknown)",
338
- handle.server,
339
- handle.env.join(", "),
340
- handle.skills.join(", ")
341
- ]);
342
- process.stdout.write(renderTable(["ID", "NAME", "VERS", "MCP", "KEY", "SKILLS"], rows));
343
- return 0;
344
- }
345
- /* handler for "ase setup mcp activate|deactivate [<servers>]" */
346
- async doMcp(action, tool, servers, scope) {
347
- this.requireClaudeScope(tool, scope);
348
- await this.ensureTool(toolSpecs[tool].cli);
349
- /* source .env files into the environment so the per-server
350
- API keys (ASE_MCP_KEY_<XXX>) can live in a .env file instead
351
- of the exported interactive shell environment */
352
- dotenvx.config({ quiet: true, ignore: ["MISSING_ENV_FILE"] });
353
- /* resolve the comma-separated list of server ids, with an empty
354
- list or the literal "all" expanding to every registered server
355
- id; track whether the ids were explicitly given on the CLI */
356
- const known = this.mcpServers.map((handle) => handle.id);
357
- const explicit = servers.trim() !== "" && servers.trim() !== "all";
358
- const ids = explicit ?
359
- servers.split(",").map((s) => s.trim()).filter((s) => s !== "") : known;
360
- for (const id of ids)
361
- if (this.mcpServers.find((handle) => handle.id === id) === undefined)
362
- throw new Error(`unknown MCP server "${id}" ` +
363
- `(known: ${known.join(", ")})`);
364
- /* dispatch each selected server to its dedicated handler */
365
- for (const id of ids) {
366
- /* find handle */
367
- const handle = this.mcpServers.find((handle) => handle.id === id);
368
- /* determine information and action */
369
- let envKey = "";
370
- let envVal = "";
371
- if (action === "activate") {
372
- /* on activation, require at least one of the per-server API
373
- key environment variables (ASE_MCP_KEY_<XXX>) to
374
- be set; skip the server when its id was only
375
- implicitly selected (empty list or "all"), but fail
376
- hard when it was given explicitly on the CLI */
377
- envKey = handle.env.find((name) => (process.env[`ASE_MCP_KEY_${name}`] ?? "") !== "") ?? "";
378
- if (envKey === "") {
379
- const vars = handle.env.map((name) => `ASE_MCP_KEY_${name}`).join(", ");
380
- if (explicit)
381
- throw new Error(`none of ${vars} set: ` +
382
- `cannot activate MCP server "${handle.server}"`);
383
- this.log.write("info", `setup: mcp: activate: [${id}]: none of ${vars} set: ` +
384
- `skipping MCP server "${handle.server}" (${handle.name})`);
385
- continue;
386
- }
387
- envVal = process.env[`ASE_MCP_KEY_${envKey}`] ?? "";
388
- }
389
- /* probe whether the MCP server is currently registered with the tool */
390
- const installed = await this.mcpInstalled(tool, handle.server);
391
- if (action === "activate") {
392
- /* on activation, remove a stale registration first so the
393
- handler can re-create it cleanly */
394
- if (installed) {
395
- this.log.write("info", `setup: mcp: activate: [${id}]: MCP server "${handle.server}" ` +
396
- "already registered: removing stale registration first");
397
- await this.mcpRemove(tool, handle.server, scope);
398
- }
399
- }
400
- else if (!installed) {
401
- /* on deactivation, skip the removal of an absent server */
402
- this.log.write("info", `setup: mcp: deactivate: [${id}]: MCP server "${handle.server}" ` +
403
- "not registered: skipping removal");
404
- continue;
405
- }
406
- /* call the handler */
407
- this.log.write("info", `setup: mcp: ${action}: [${id}]: MCP server "${handle.server}" ` +
408
- `(name: ${handle.name}${handle.version ? (", version: " + handle.version) : ""})`);
409
- await handle.handler(handle, tool, scope, action, envKey, envVal);
410
- }
411
- return 0;
412
- }
413
- /* probe whether an MCP server is currently registered with the tool
414
- by inspecting the exit code of "<cli> mcp get <name>" */
415
- async mcpInstalled(tool, name) {
416
- const result = await execa(toolSpecs[tool].cli, ["mcp", "get", name], { stdio: "ignore", reject: false });
417
- return result.exitCode === 0;
418
- }
419
- /* probe the registration scope of an MCP server by scraping the
420
- human-readable output of "<cli> mcp get <name>"; returns "undefined"
421
- for a server which is not registered with the tool at all */
422
- async mcpScope(tool, name) {
423
- const result = await execa(toolSpecs[tool].cli, ["mcp", "get", name], { stdio: "pipe", reject: false });
424
- if (result.exitCode !== 0)
425
- return undefined;
426
- /* the Anthropic Claude Code CLI reports "Scope: <scope> config (...)",
427
- the GitHub Copilot CLI reports "Source: <scope>", and the
428
- OpenAI Codex CLI reports no scope information at all */
429
- const m = (result.stdout ?? "").match(/^\s*(?:Scope|Source):\s*(\S+)/m);
430
- return m !== null ? m[1].toLowerCase() : "(n/a)";
431
- }
432
- /* register an MCP server with the tool, supporting both the "stdio"
433
- (a local subprocess command) and "http" (a remote URL, optionally
434
- with HTTP headers) transports; the per-tool command line differs
435
- between Anthropic Claude Code CLI, GitHub Copilot CLI, and OpenAI Codex CLI */
436
- async mcpAdd(tool, name, env, transport, scope) {
437
- const args = ["mcp", "add"];
438
- if (tool === "claude") {
439
- if (scope !== "user")
440
- args.push("--scope", scope);
441
- args.push("--transport", transport.type);
442
- if (transport.type === "stdio") {
443
- for (const [key, val] of Object.entries(env))
444
- args.push("-e", `${key}=${val}`);
445
- args.push("--", name, ...transport.command);
446
- }
447
- else {
448
- for (const [key, val] of Object.entries(transport.headers ?? {}))
449
- args.push("--header", `${key}: ${val}`);
450
- args.push(name, transport.url);
451
- }
452
- }
453
- else {
454
- /* the GitHub Copilot CLI and OpenAI Codex CLI both take the
455
- server name as a positional argument and imply the stdio
456
- transport when the command is provided after "--"; for "http"
457
- servers the GitHub Copilot CLI needs an explicit
458
- "--transport" flag and takes the URL positionally, while the
459
- OpenAI Codex CLI takes it via the "--url" option */
460
- if (transport.type === "stdio") {
461
- args.push(name);
462
- for (const [key, val] of Object.entries(env))
463
- args.push("--env", `${key}=${val}`);
464
- args.push("--", ...transport.command);
465
- }
466
- else {
467
- if (tool === "copilot")
468
- args.push("--transport", "http");
469
- for (const [key, val] of Object.entries(transport.headers ?? {}))
470
- args.push("--header", `${key}: ${val}`);
471
- if (tool === "copilot")
472
- args.push(name, transport.url);
473
- else
474
- args.push(name, "--url", transport.url);
475
- }
476
- }
477
- await this.run(toolSpecs[tool].cli, args);
478
- }
479
- /* unregister an MCP server from the tool; the per-tool command line
480
- differs between Anthropic Claude Code CLI, GitHub Copilot CLI, and OpenAI Codex CLI */
481
- async mcpRemove(tool, name, scope) {
482
- const scopeArgs = tool === "claude" && scope !== "user" ? ["--scope", scope] : [];
483
- const args = ["mcp", "remove", ...scopeArgs, name];
484
- await this.run(toolSpecs[tool].cli, args, { ignoreError: `MCP server "${name}" not registered` });
485
- }
486
- /* build a chat-model MCP handler from the per-model direct and
487
- OPENROUTER url/api/model triples, factoring out the shared
488
- mcp-to-openai stdio scaffold common to all chat-model servers */
489
- chatMcpHandler(direct, router) {
490
- return async (spec, tool, scope, action, envKey, envVal) => {
491
- if (action === "activate")
492
- await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
493
- type: "stdio", command: [
494
- "npx", "-y", "mcp-to-openai",
495
- "--service", spec.name,
496
- "--mcp-tool", "query",
497
- ...(envKey === "OPENROUTER" ? [
498
- "--openai-url", "https://openrouter.ai/api/v1",
499
- "--openai-api", "completion",
500
- "--openai-model", router.model
501
- ] : [
502
- "--openai-url", direct.url,
503
- "--openai-api", direct.api,
504
- "--openai-model", direct.model
505
- ])
506
- ]
507
- }, scope);
508
- else
509
- await this.mcpRemove(tool, spec.server, scope);
510
- };
511
- }
512
- /* build a chat-model MCP handler which bridges to a locally
513
- installed AI agent harness CLI via the mcp-to-harness stdio
514
- scaffold; the API key environment variable carries no key at all,
515
- but instead the harness model identifier, with the special value
516
- "default" selecting the default model of the harness */
517
- harnessMcpHandler(harness) {
518
- return async (spec, tool, scope, action, _envKey, envVal) => {
519
- if (action === "activate")
520
- await this.mcpAdd(tool, spec.server, {}, {
521
- type: "stdio", command: [
522
- "npx", "-y", "mcp-to-harness",
523
- "--service", spec.name,
524
- "--mcp-tool", "query",
525
- "--harness", harness,
526
- ...(envVal !== "default" ? [
527
- "--harness-model", envVal
528
- ] : [])
529
- ]
530
- }, scope);
531
- else
532
- await this.mcpRemove(tool, spec.server, scope);
533
- };
534
- }
535
- /* registry of pre-defined MCP servers: maps each server id onto its
536
- dedicated handler which performs the activate/deactivate operation */
537
- mcpServers = [
538
- {
539
- id: "openai-chatgpt",
540
- name: "OpenAI ChatGPT",
541
- version: "5.5",
542
- env: ["OPENAI_CHATGPT", "OPENROUTER"],
543
- server: "chat-openai-chatgpt",
544
- skills: ["ase-meta-chat", "ase-meta-quorum"],
545
- handler: this.chatMcpHandler({ url: "https://api.openai.com/v1", api: "responses", model: "gpt-5.5" }, { model: "openai/gpt-5.5" })
546
- },
547
- {
548
- id: "google-gemini",
549
- name: "Google Gemini",
550
- version: "3.5",
551
- env: ["GOOGLE_GEMINI", "OPENROUTER"],
552
- server: "chat-google-gemini",
553
- skills: ["ase-meta-chat", "ase-meta-quorum"],
554
- handler: this.chatMcpHandler({ url: "https://generativelanguage.googleapis.com/v1beta/openai/", api: "completion", model: "gemini-3.5-flash" }, { model: "google/gemini-3.5-flash" })
555
- },
556
- {
557
- id: "deepseek",
558
- name: "DeepSeek",
559
- version: "4.0",
560
- env: ["DEEPSEEK", "OPENROUTER"],
561
- server: "chat-deepseek",
562
- skills: ["ase-meta-chat", "ase-meta-quorum"],
563
- handler: this.chatMcpHandler({ url: "https://api.deepseek.com/v1", api: "completion", model: "deepseek-v4-flash" }, { model: "deepseek/deepseek-v4-flash" })
564
- },
565
- {
566
- id: "xai-grok",
567
- name: "xAI Grok",
568
- version: "4.3",
569
- env: ["XAI_GROK", "OPENROUTER"],
570
- server: "chat-xai-grok",
571
- skills: ["ase-meta-chat", "ase-meta-quorum"],
572
- handler: this.chatMcpHandler({ url: "https://api.x.ai/v1", api: "completion", model: "grok-4.3" }, { model: "x-ai/grok-4.3" })
573
- },
574
- {
575
- id: "alibaba-qwen",
576
- name: "Alibaba Qwen",
577
- version: "3.7",
578
- env: ["ALIBABA_QWEN", "OPENROUTER"],
579
- server: "chat-alibaba-qwen",
580
- skills: ["ase-meta-chat", "ase-meta-quorum"],
581
- handler: this.chatMcpHandler({ url: "https://dashscope.aliyuncs.com/compatible-mode/v1", api: "completion", model: "qwen3.7-max" }, { model: "qwen/qwen3.7-max" })
582
- },
583
- {
584
- id: "zai-glm",
585
- name: "Z.AI GLM",
586
- version: "5.1",
587
- env: ["ZAI_GLM", "OPENROUTER"],
588
- server: "chat-zai-glm",
589
- skills: ["ase-meta-chat", "ase-meta-quorum"],
590
- handler: this.chatMcpHandler({ url: "https://api.z.ai/api/paas/v4/", api: "completion", model: "glm-5.1" }, { model: "z-ai/glm-5.1" })
591
- },
592
- {
593
- id: "anthropic-claude",
594
- name: "Anthropic Claude",
595
- version: "latest",
596
- env: ["ANTHROPIC_CLAUDE"],
597
- server: "chat-anthropic-claude",
598
- skills: ["ase-meta-chat", "ase-meta-quorum"],
599
- handler: this.harnessMcpHandler("claude")
600
- },
601
- {
602
- id: "openai-codex",
603
- name: "OpenAI Codex",
604
- version: "latest",
605
- env: ["OPENAI_CODEX"],
606
- server: "chat-openai-codex",
607
- skills: ["ase-meta-chat", "ase-meta-quorum"],
608
- handler: this.harnessMcpHandler("codex")
609
- },
610
- {
611
- id: "github-copilot",
612
- name: "GitHub Copilot",
613
- version: "latest",
614
- env: ["GITHUB_COPILOT"],
615
- server: "chat-github-copilot",
616
- skills: ["ase-meta-chat", "ase-meta-quorum"],
617
- handler: this.harnessMcpHandler("copilot")
618
- },
619
- {
620
- id: "brave",
621
- name: "Brave",
622
- version: "latest",
623
- env: ["BRAVE"],
624
- server: "search-brave",
625
- skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
626
- handler: async (spec, tool, scope, action, _envKey, envVal) => {
627
- if (action === "activate")
628
- await this.mcpAdd(tool, spec.server, {
629
- "BRAVE_API_KEY": envVal,
630
- "BRAVE_MCP_ENABLED_TOOLS": "brave_web_search"
631
- }, { type: "stdio", command: ["npx", "-y", "@brave/brave-search-mcp-server"] }, scope);
632
- else
633
- await this.mcpRemove(tool, spec.server, scope);
634
- }
635
- },
636
- {
637
- id: "perplexity",
638
- name: "Perplexity",
639
- version: "latest",
640
- env: ["PERPLEXITY"],
641
- server: "search-perplexity",
642
- skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
643
- handler: async (spec, tool, scope, action, _envKey, envVal) => {
644
- if (action === "activate")
645
- await this.mcpAdd(tool, spec.server, {
646
- "PERPLEXITY_API_KEY": envVal
647
- }, { type: "stdio", command: ["npx", "-y", "@perplexity-ai/mcp-server"] }, scope);
648
- else
649
- await this.mcpRemove(tool, spec.server, scope);
650
- }
651
- },
652
- {
653
- id: "exa",
654
- name: "Exa",
655
- version: "latest",
656
- env: ["EXA"],
657
- server: "search-exa",
658
- skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
659
- handler: async (spec, tool, scope, action, _envKey, envVal) => {
660
- if (action === "activate")
661
- await this.mcpAdd(tool, spec.server, {}, { type: "http", url: `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(envVal)}` }, scope);
662
- else
663
- await this.mcpRemove(tool, spec.server, scope);
664
- }
665
- }
666
- ];
667
- /* the default "ase statusline" format lines used when the user does
668
- not override them with positional arguments to "activate" */
669
- statuslineFormatDflt = [
670
- "<blue>%u</blue> <red>%p</red> <black>%T</black> %s",
671
- "%m %e %t",
672
- "%P %h %B %c"
673
- ];
674
- /* resolve the tool settings file for a given installation scope */
675
- settingsFile(tool, scope) {
676
- if (tool === "copilot") {
677
- const home = process.env.COPILOT_HOME ?? "";
678
- const base = home !== "" ? home : path.join(os.homedir(), ".copilot");
679
- return path.join(base, "settings.json");
680
- }
681
- else if (scope === "project")
682
- return path.join(process.cwd(), ".claude", "settings.json");
683
- else if (scope === "local")
684
- return path.join(process.cwd(), ".claude", "settings.local.json");
685
- else
686
- return path.join(os.homedir(), ".claude", "settings.json");
687
- }
688
- /* reject the statusline operation for tools without a scriptable
689
- statusline mechanism (only the OpenAI Codex CLI lacks one) */
690
- requireStatuslineTool(tool) {
691
- if (tool === "codex")
692
- throw new Error("statusline configuration is not supported for --tool codex " +
693
- `(the ${toolSpecs[tool].label} has no scriptable statusline mechanism)`);
694
- }
695
- /* reject a non-default --scope for the GitHub Copilot CLI, which has a
696
- single per-user settings file and thus no scope concept */
697
- requireStatuslineScope(tool, scope) {
698
- if (tool === "copilot" && scope !== "user")
699
- throw new Error("--scope is only supported for --tool claude " +
700
- `(the ${toolSpecs[tool].label} has a single per-user settings file)`);
701
- }
702
- /* build the "ase statusline ..." command string from the activate
703
- options and the effective format lines */
704
- statuslineCommand(tool, opts, format) {
705
- const parts = ["ase", "statusline", "--tool", tool, "-w", String(opts.width), "-m", String(opts.margin),
706
- "-p", String(opts.padding)];
707
- if (!opts.icons)
708
- parts.push("--no-icons");
709
- if (!opts.labels)
710
- parts.push("--no-labels");
711
- const lines = format.length > 0 ? format : this.statuslineFormatDflt;
712
- for (const line of lines)
713
- parts.push(`'${line.replace(/'/g, "'\\''")}'`);
714
- return parts.join(" ");
715
- }
716
- /* determine whether an existing "statusLine" value object is owned by us */
717
- statuslineIsOwned(member) {
718
- const obj = member.query("/ * [ pos() == 2 ]")[0];
719
- if (obj === undefined)
720
- return false;
721
- for (const m of obj.query("/ member")) {
722
- const key = m.query("/ string [ pos() == 1 ]")[0];
723
- if (key !== undefined && key.get("value") === "command") {
724
- const val = m.query("/ string [ pos() == 2 ]")[0];
725
- const cmd = val !== undefined ? String(val.get("value") ?? "") : "";
726
- return cmd.startsWith("ase statusline");
727
- }
728
- }
729
- return false;
730
- }
731
- /* locate a top-level member node by key within the root object */
732
- settingsFindMember(root, name) {
733
- for (const m of root.query("/ member")) {
734
- const key = m.query("/ string [ pos() == 1 ]")[0];
735
- if (key !== undefined && key.get("value") === name)
736
- return m;
737
- }
738
- return undefined;
739
- }
740
- /* build the "statusLine" member subtree natively inside the target
741
- AST, reproducing the exact whitespace tokens json-asty emits for a
742
- canonically 4-space-indented settings.json */
743
- statuslineBuildMember(root, command) {
744
- const member = (key, val, epilog) => {
745
- const type = typeof val === "number" ? "number" : "string";
746
- const body = typeof val === "number" ? String(val) : JSON.stringify(val);
747
- const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
748
- const v = root.create(type).set({ body, value: val });
749
- const m = root.create("member");
750
- if (epilog !== undefined)
751
- m.set({ epilog });
752
- return m.add(k, v);
753
- };
754
- const obj = root.create("object").set({ prolog: "{\n ", epilog: "\n }\n" });
755
- obj.add(member("type", "command", ",\n "), member("command", command, ",\n "), member("padding", 0));
756
- const key = root.create("string").set({
757
- body: JSON.stringify("statusLine"),
758
- value: "statusLine",
759
- epilog: ": "
760
- });
761
- return root.create("member").add(key, obj);
762
- }
763
- /* normalize the previous-last member so a following ",\n " comma reads cleanly */
764
- settingsMakeNonLast(member) {
765
- const val = member.query("/ * [ pos() == 2 ]")[0];
766
- if (val !== undefined) {
767
- const ep = val.get("epilog");
768
- if (typeof ep === "string" && ep.endsWith("\n"))
769
- val.set({ epilog: ep.replace(/\n$/, "") });
770
- }
771
- member.set({ epilog: ",\n " });
772
- }
773
- /* append a member to the root object, repairing the whitespace at
774
- the seam: a scalar value carries no pre-"}" newline of its own
775
- (a container value carries it in its own epilog already), so the
776
- root epilog has to supply it */
777
- settingsAddMember(root, member, scalar) {
778
- const members = root.query("/ member");
779
- if (members.length === 0)
780
- root.set({ prolog: "{\n " });
781
- else
782
- this.settingsMakeNonLast(members[members.length - 1]);
783
- root.set({ epilog: scalar ? "\n}\n" : "}\n" });
784
- root.add(member);
785
- }
786
- /* remove a member from the root object, repairing the whitespace at the seam */
787
- settingsRemoveMember(root, target) {
788
- const members = root.query("/ member");
789
- const wasLast = members[members.length - 1] === target;
790
- root.del(target);
791
- if (wasLast) {
792
- if (members.length > 1) {
793
- /* promote the new last member: strip its comma epilog and
794
- restore the pre-"}" newline into the root epilog when the
795
- new-last value is a scalar (a container value carries the
796
- newline in its own epilog already) */
797
- const newLast = members[members.length - 2];
798
- newLast.set({ epilog: undefined });
799
- const val = newLast.query("/ * [ pos() == 2 ]")[0];
800
- const ep = val !== undefined ? val.get("epilog") : undefined;
801
- root.set({ epilog: (typeof ep === "string" && ep.endsWith("\n")) ? "}\n" : "\n}\n" });
802
- }
803
- else
804
- root.set({ epilog: "}\n" });
805
- }
806
- }
807
- /* read a settings.json file into a JSON-ASTy AST */
808
- async settingsReadAst(file) {
809
- let text = "";
810
- try {
811
- text = await fs.readFile(file, "utf8");
812
- }
813
- catch {
814
- /* missing file: start from an empty object */
815
- }
816
- if (text.trim() === "")
817
- text = "{}";
818
- return JsonAsty.parse(text);
819
- }
820
- /* write a JSON-ASTy AST back to a settings.json file */
821
- async settingsWriteAst(file, root) {
822
- await mkdirp(path.dirname(file));
823
- const text = JsonAsty.unparse(root);
824
- await writeFileAtomic(file, text, { encoding: "utf8" });
825
- }
826
- /* handler for "ase setup statusline activate" */
827
- async doStatuslineActivate(tool, scope, opts, format) {
828
- this.requireStatuslineTool(tool);
829
- this.requireStatuslineScope(tool, scope);
830
- const file = this.settingsFile(tool, scope);
831
- const command = this.statuslineCommand(tool, opts, format);
832
- const root = await this.settingsReadAst(file);
833
- const existing = this.settingsFindMember(root, "statusLine");
834
- if (existing !== undefined) {
835
- /* preserve a foreign, hand-crafted statusLine: skip and warn */
836
- if (!this.statuslineIsOwned(existing)) {
837
- this.log.write("warning", "setup: statusline: activate: a non-ASE \"statusLine\" " +
838
- `is already present in ${file}: preserving it (skipped)`);
839
- return 0;
840
- }
841
- /* replace the value object in place, preserving its epilog */
842
- const valNew = this.statuslineBuildMember(root, command).query("/ object")[0];
843
- const valOld = existing.query("/ * [ pos() == 2 ]")[0];
844
- valNew.set({ epilog: valOld.get("epilog") });
845
- existing.del(valOld).add(valNew);
846
- this.log.write("info", `setup: statusline: activate: updating ASE "statusLine" in ${file}`);
847
- }
848
- else {
849
- /* insert a fresh statusLine member */
850
- this.settingsAddMember(root, this.statuslineBuildMember(root, command), false);
851
- this.log.write("info", `setup: statusline: activate: adding ASE "statusLine" to ${file}`);
852
- }
853
- await this.settingsWriteAst(file, root);
854
- return 0;
855
- }
856
- /* handler for "ase setup statusline deactivate" */
857
- async doStatuslineDeactivate(tool, scope) {
858
- this.requireStatuslineTool(tool);
859
- this.requireStatuslineScope(tool, scope);
860
- const file = this.settingsFile(tool, scope);
861
- /* a missing settings file means nothing to remove */
862
- try {
863
- await fs.access(file);
864
- }
865
- catch {
866
- this.log.write("info", `setup: statusline: deactivate: no settings file ${file} (skipped)`);
867
- return 0;
868
- }
869
- /* read file */
870
- const root = await this.settingsReadAst(file);
871
- const target = this.settingsFindMember(root, "statusLine");
872
- if (target === undefined) {
873
- this.log.write("info", `setup: statusline: deactivate: no "statusLine" in ${file} (skipped)`);
874
- return 0;
875
- }
876
- /* preserve a foreign, hand-crafted statusLine: skip and warn */
877
- if (!this.statuslineIsOwned(target)) {
878
- this.log.write("warning", "setup: statusline: deactivate: a non-ASE \"statusLine\" " +
879
- `is present in ${file}: preserving it (skipped)`);
880
- return 0;
881
- }
882
- /* remove the member and repair the whitespace at the seam */
883
- this.settingsRemoveMember(root, target);
884
- await this.settingsWriteAst(file, root);
885
- this.log.write("info", `setup: statusline: deactivate: removing ASE "statusLine" from ${file}`);
886
- return 0;
887
- }
888
- /* probe the ASE statusline registrations by inspecting the tool
889
- settings files of all applicable installation scopes */
890
- async statuslineStatus(tool) {
891
- /* the OpenAI Codex CLI has no scriptable statusline mechanism at all */
892
- if (tool === "codex")
893
- return [];
894
- const home = os.homedir();
895
- const scopes = tool === "claude" ? ["user", "project", "local"] : ["user"];
896
- const entries = [];
897
- for (const scope of scopes) {
898
- const file = this.settingsFile(tool, scope);
899
- let root;
900
- try {
901
- root = await this.settingsReadAst(file);
902
- }
903
- catch {
904
- /* an unparsable settings file carries no usable state */
905
- continue;
906
- }
907
- const member = this.settingsFindMember(root, "statusLine");
908
- if (member === undefined)
909
- continue;
910
- entries.push({
911
- file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
912
- scope: tool === "claude" ? scope : "(n/a)",
913
- status: this.statuslineIsOwned(member) ? "activated" : "foreign"
914
- });
915
- }
916
- return entries;
917
- }
918
- /* determine whether an existing "outputStyle" value is owned by us */
919
- outputStyleIsOwned(member) {
920
- const val = member.query("/ string [ pos() == 2 ]")[0];
921
- return val !== undefined && val.get("value") === "ase:ase-terse";
922
- }
923
- /* build the "outputStyle" member subtree natively inside the target AST */
924
- outputStyleBuildMember(root) {
925
- const key = root.create("string").set({ body: JSON.stringify("outputStyle"), value: "outputStyle", epilog: ": " });
926
- const val = root.create("string").set({ body: JSON.stringify("ase:ase-terse"), value: "ase:ase-terse" });
927
- return root.create("member").add(key, val);
928
- }
929
- /* activate the plugin-shipped ASE output style by selecting it in
930
- the settings file of the scope (Anthropic Claude Code CLI only:
931
- the other tools have no output style concept, so the session-start
932
- hook injects the style into their session context instead) */
933
- async outputStyleActivate(action, scope) {
934
- const file = this.settingsFile("claude", scope);
935
- const root = await this.settingsReadAst(file);
936
- const existing = this.settingsFindMember(root, "outputStyle");
937
- if (existing !== undefined) {
938
- /* preserve a foreign, hand-selected outputStyle: skip and warn */
939
- if (!this.outputStyleIsOwned(existing))
940
- this.log.write("warning", `setup: ${action}: a non-ASE "outputStyle" ` +
941
- `is already selected in ${file}: preserving it (skipped)`);
942
- return;
943
- }
944
- this.settingsAddMember(root, this.outputStyleBuildMember(root), true);
945
- await this.settingsWriteAst(file, root);
946
- this.log.write("info", `setup: ${action}: selecting ASE "outputStyle" in ${file}`);
947
- }
948
- /* deactivate the plugin-shipped ASE output style by removing its
949
- selection from the settings file of the scope (Anthropic Claude Code CLI only) */
950
- async outputStyleDeactivate(action, scope) {
951
- const file = this.settingsFile("claude", scope);
952
- /* a missing settings file means nothing to remove */
953
- try {
954
- await fs.access(file);
955
- }
956
- catch {
957
- return;
958
- }
959
- const root = await this.settingsReadAst(file);
960
- const target = this.settingsFindMember(root, "outputStyle");
961
- if (target === undefined)
962
- return;
963
- /* preserve a foreign, hand-selected outputStyle: skip and warn */
964
- if (!this.outputStyleIsOwned(target)) {
965
- this.log.write("warning", `setup: ${action}: a non-ASE "outputStyle" ` +
966
- `is selected in ${file}: preserving it (skipped)`);
967
- return;
968
- }
969
- this.settingsRemoveMember(root, target);
970
- await this.settingsWriteAst(file, root);
971
- this.log.write("info", `setup: ${action}: deselecting ASE "outputStyle" in ${file}`);
972
- }
973
- /* probe the output style selections by inspecting the tool
974
- settings files of all installation scopes (Anthropic Claude Code CLI only) */
975
- async outputStyleStatus(tool) {
976
- if (tool !== "claude")
977
- return [];
978
- const home = os.homedir();
979
- const scopes = ["user", "project", "local"];
980
- const entries = [];
981
- for (const scope of scopes) {
982
- const file = this.settingsFile(tool, scope);
983
- let root;
984
- try {
985
- root = await this.settingsReadAst(file);
986
- }
987
- catch {
988
- /* an unparsable settings file carries no usable state */
989
- continue;
990
- }
991
- const member = this.settingsFindMember(root, "outputStyle");
992
- if (member === undefined)
993
- continue;
994
- entries.push({
995
- file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
996
- scope,
997
- status: this.outputStyleIsOwned(member) ? "selected" : "foreign"
998
- });
999
- }
1000
- return entries;
1001
- }
1002
- /* parse and validate the --tool option */
1003
- parseTool(value) {
1004
- if (value !== "claude" && value !== "copilot" && value !== "codex")
1005
- throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
1006
- return value;
1007
- }
1008
- /* parse and validate the --scope option */
1009
- parseScope(value) {
1010
- if (value !== "user" && value !== "project" && value !== "local")
1011
- throw new Error(`invalid --scope value: "${value}" (expected "user", "project", or "local")`);
1012
- return value;
1013
- }
1014
- /* reject a non-default --scope for tools which have no scope concept
1015
- at all (only Anthropic Claude Code CLI supports plugin/MCP scopes) */
1016
- requireClaudeScope(tool, scope) {
1017
- if (tool !== "claude" && scope !== "user")
1018
- throw new Error("--scope is only supported for --tool claude");
1019
- }
1020
232
  /* register commands */
1021
233
  register(program) {
1022
234
  /* default for --dev derived from ASE_SETUP_DEV environment variable */
@@ -1043,54 +255,62 @@ export default class SetupCommand {
1043
255
  .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
1044
256
  .option("-d, --dev", "use local working copy instead of remote/bundled repository", devDflt)
1045
257
  .action(async (opts) => {
1046
- process.exit(await this.doInstall(this.parseTool(opts.tool), opts.dev, this.parseScope(opts.scope)));
258
+ process.exit(await this.doInstall(parseTool(opts.tool), opts.dev, parseScope(opts.scope)));
1047
259
  });
1048
260
  /* register CLI sub-command "ase setup update" */
1049
261
  setupCmd
1050
262
  .command("update")
1051
263
  .description("update the ASE tool and the ASE plugin for a tool")
1052
264
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1053
- .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
265
+ .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\", default: auto-detected)")
1054
266
  .option("-f, --force", "always perform the update, even if already at latest version", false)
1055
267
  .option("-d, --dev", "use local working copy instead of remote/bundled repository", devDflt)
1056
268
  .action(async (opts) => {
1057
- process.exit(await this.doUpdate(this.parseTool(opts.tool), opts.force, opts.dev, this.parseScope(opts.scope)));
269
+ const tool = parseTool(opts.tool);
270
+ const scope = opts.scope === undefined ? await this.settings.detectPluginScope(tool) : parseScope(opts.scope);
271
+ process.exit(await this.doUpdate(tool, opts.force, opts.dev, scope));
1058
272
  });
1059
273
  /* register CLI sub-command "ase setup uninstall" */
1060
274
  setupCmd
1061
275
  .command("uninstall")
1062
276
  .description("uninstall the ASE plugin for a tool and the ASE tool")
1063
277
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1064
- .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
278
+ .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\", default: auto-detected)")
1065
279
  .option("-d, --dev", "use local working copy instead of remote/bundled repository", devDflt)
1066
280
  .action(async (opts) => {
1067
- process.exit(await this.doUninstall(this.parseTool(opts.tool), opts.dev, this.parseScope(opts.scope)));
281
+ const tool = parseTool(opts.tool);
282
+ const scope = opts.scope === undefined ? await this.settings.detectPluginScope(tool) : parseScope(opts.scope);
283
+ process.exit(await this.doUninstall(tool, opts.dev, scope));
1068
284
  });
1069
285
  /* register CLI sub-command "ase setup enable" */
1070
286
  setupCmd
1071
287
  .command("enable")
1072
288
  .description("enable the ASE plugin for a tool")
1073
289
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1074
- .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
290
+ .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\", default: auto-detected)")
1075
291
  .action(async (opts) => {
1076
- process.exit(await this.doToggle(this.parseTool(opts.tool), this.parseScope(opts.scope), "enable"));
292
+ const tool = parseTool(opts.tool);
293
+ const scope = opts.scope === undefined ? await this.settings.detectPluginScope(tool) : parseScope(opts.scope);
294
+ process.exit(await this.doToggle(tool, scope, "enable"));
1077
295
  });
1078
296
  /* register CLI sub-command "ase setup disable" */
1079
297
  setupCmd
1080
298
  .command("disable")
1081
299
  .description("disable the ASE plugin for a tool")
1082
300
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1083
- .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
301
+ .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\", default: auto-detected)")
1084
302
  .action(async (opts) => {
1085
- process.exit(await this.doToggle(this.parseTool(opts.tool), this.parseScope(opts.scope), "disable"));
303
+ const tool = parseTool(opts.tool);
304
+ const scope = opts.scope === undefined ? await this.settings.detectPluginScope(tool) : parseScope(opts.scope);
305
+ process.exit(await this.doToggle(tool, scope, "disable"));
1086
306
  });
1087
307
  /* register CLI sub-command "ase setup status" */
1088
308
  setupCmd
1089
309
  .command("status")
1090
- .description("report the ASE plugin, MCP server, output style, and statusline registrations for a tool")
310
+ .description("report the ASE plugin, MCP server, output style, statusline, and telemetry registrations for a tool")
1091
311
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1092
312
  .action(async (opts) => {
1093
- process.exit(await this.doStatus(this.parseTool(opts.tool)));
313
+ process.exit(await this.doStatus(parseTool(opts.tool)));
1094
314
  });
1095
315
  /* register CLI sub-command "ase setup mcp" */
1096
316
  const mcpCmd = setupCmd
@@ -1105,7 +325,7 @@ export default class SetupCommand {
1105
325
  .command("list")
1106
326
  .description("list all available pre-defined MCP server names")
1107
327
  .action(async () => {
1108
- process.exit(await this.doMcpList());
328
+ process.exit(await this.mcp.doMcpList());
1109
329
  });
1110
330
  /* register CLI sub-command "ase setup mcp activate" */
1111
331
  mcpCmd
@@ -1114,7 +334,7 @@ export default class SetupCommand {
1114
334
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1115
335
  .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
1116
336
  .action(async (servers, opts) => {
1117
- process.exit(await this.doMcp("activate", this.parseTool(opts.tool), servers ?? "all", this.parseScope(opts.scope)));
337
+ process.exit(await this.mcp.doMcp("activate", parseTool(opts.tool), servers ?? "all", parseScope(opts.scope)));
1118
338
  });
1119
339
  /* register CLI sub-command "ase setup mcp deactivate" */
1120
340
  mcpCmd
@@ -1123,7 +343,7 @@ export default class SetupCommand {
1123
343
  .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
1124
344
  .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
1125
345
  .action(async (servers, opts) => {
1126
- process.exit(await this.doMcp("deactivate", this.parseTool(opts.tool), servers ?? "all", this.parseScope(opts.scope)));
346
+ process.exit(await this.mcp.doMcp("deactivate", parseTool(opts.tool), servers ?? "all", parseScope(opts.scope)));
1127
347
  });
1128
348
  /* parser for the non-negative integer "ase setup statusline" options */
1129
349
  const parseNonNeg = (name) => (value) => {
@@ -1152,7 +372,7 @@ export default class SetupCommand {
1152
372
  .option("--no-icons", "disable icons in placeholder rendering")
1153
373
  .option("--no-labels", "disable labels in front of bold values")
1154
374
  .action(async (format, opts) => {
1155
- process.exit(await this.doStatuslineActivate(this.parseTool(opts.tool), this.parseScope(opts.scope), { width: opts.width, margin: opts.margin, padding: opts.padding, icons: opts.icons, labels: opts.labels }, format ?? []));
375
+ process.exit(await this.settings.doStatuslineActivate(parseTool(opts.tool), parseScope(opts.scope), { width: opts.width, margin: opts.margin, padding: opts.padding, icons: opts.icons, labels: opts.labels }, format ?? []));
1156
376
  });
1157
377
  /* register CLI sub-command "ase setup statusline deactivate" */
1158
378
  statuslineCmd
@@ -1161,7 +381,31 @@ export default class SetupCommand {
1161
381
  .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\"; \"codex\" is unsupported)", toolDflt)
1162
382
  .option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
1163
383
  .action(async (opts) => {
1164
- process.exit(await this.doStatuslineDeactivate(this.parseTool(opts.tool), this.parseScope(opts.scope)));
384
+ process.exit(await this.settings.doStatuslineDeactivate(parseTool(opts.tool), parseScope(opts.scope)));
385
+ });
386
+ /* register CLI sub-command "ase setup telemetry" */
387
+ const telemetryCmd = setupCmd
388
+ .command("telemetry")
389
+ .description("activate or deactivate the ASE telemetry (AI usage accounting) for a tool")
390
+ .action(() => {
391
+ telemetryCmd.outputHelp();
392
+ process.exit(1);
393
+ });
394
+ /* register CLI sub-command "ase setup telemetry activate" */
395
+ telemetryCmd
396
+ .command("activate")
397
+ .description("activate the ASE telemetry (OpenTelemetry export to the ASE ingress)")
398
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
399
+ .action(async (opts) => {
400
+ process.exit(await this.settings.doTelemetryActivate(parseTool(opts.tool)));
401
+ });
402
+ /* register CLI sub-command "ase setup telemetry deactivate" */
403
+ telemetryCmd
404
+ .command("deactivate")
405
+ .description("deactivate the ASE telemetry")
406
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
407
+ .action(async (opts) => {
408
+ process.exit(await this.settings.doTelemetryDeactivate(parseTool(opts.tool)));
1165
409
  });
1166
410
  }
1167
411
  }