@rse/ase 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +351 -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} +3 -0
  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 +251 -76
  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 +225 -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 +79 -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-mcp.js CHANGED
@@ -7,7 +7,10 @@ import { fileURLToPath } from "node:url";
7
7
  import { execa } from "execa";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
10
- import { SERVICE_HOST as HOST, probe, loadServiceContext } from "./ase-service.js";
10
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
11
+ import { sessionState } from "./ase-config-core.js";
12
+ import ServiceCommand from "./ase-service.js";
13
+ import { SERVICE_HOST as HOST, probe, isConnRefused, loadServiceContext } from "./ase-service.js";
11
14
  /* CLI command "ase mcp" */
12
15
  export default class MCPCommand {
13
16
  log;
@@ -42,14 +45,60 @@ export default class MCPCommand {
42
45
  throw new Error(`mcp: service not responding on port ${ctx.port} after start`);
43
46
  return { projectId: ctx.projectId, port: ctx.port };
44
47
  }
48
+ /* determine the GitHub Copilot CLI session id from the environment: unlike
49
+ the hook handlers, which receive it in their event payload, the bridge can
50
+ only learn it from the environment of its MCP server process, where GitHub
51
+ Copilot CLI (and only it) exports both COPILOT_PLUGIN_ROOT and the session id
52
+ in COPILOT_AGENT_SESSION_ID; an empty result (always the case under the
53
+ other tools) disables the project directory lookup */
54
+ sessionIdFromEnv() {
55
+ if ((process.env.COPILOT_PLUGIN_ROOT ?? "") === "")
56
+ return "";
57
+ const value = process.env.COPILOT_AGENT_SESSION_ID ?? "";
58
+ return /^[A-Za-z0-9._-]+$/.test(value) ? value : "";
59
+ }
60
+ /* read the project directory which the session-start hook recorded for a
61
+ session; the session scope resolves below the home directory and hence
62
+ does not itself depend on the working directory to be correct already */
63
+ readSessionProjectDir(sessionId) {
64
+ try {
65
+ /* read without locking: this is polled until the session-start
66
+ hook has recorded the directory, and locking would both create
67
+ the session files prematurely and contend with that very hook --
68
+ writers use atomic replacement, so a plain read can never
69
+ observe a partial file */
70
+ const state = sessionState(this.log, sessionId);
71
+ state.read();
72
+ const value = state.getExplicit("basedir");
73
+ return typeof value === "string" && value !== "" ? value : null;
74
+ }
75
+ catch (_e) {
76
+ /* best-effort: an unreadable session config just defers the lookup */
77
+ return null;
78
+ }
79
+ }
45
80
  /* coerce an unknown thrown value into an Error */
46
81
  asError(e) {
47
82
  return e instanceof Error ? e : new Error(String(e));
48
83
  }
49
84
  /* bridge stdio to a Streamable HTTP MCP endpoint on the local service */
50
85
  async runBridge() {
51
- /* ensure the service is running */
52
- let { projectId, port } = await this.ensureService();
86
+ /* determine whether the service has to be deferred: GitHub Copilot CLI
87
+ starts the MCP server in the plugin installation directory and reveals
88
+ the project only to its hooks, so the service of the project can be
89
+ started only once the session-start hook recorded the project directory
90
+ (GitHub Copilot CLI only -- the other tools already start the bridge
91
+ inside the project, so their service is started right away) */
92
+ const sessionId = this.sessionIdFromEnv();
93
+ const deferred = sessionId !== "";
94
+ /* ensure the service is running (unless deferred) */
95
+ let projectId = "";
96
+ let port = null;
97
+ if (!deferred) {
98
+ const ctx = await this.ensureService();
99
+ projectId = ctx.projectId;
100
+ port = ctx.port;
101
+ }
53
102
  /* create MCP STDIO server (lives for the entire bridge lifetime) */
54
103
  const server = new StdioServerTransport();
55
104
  /* track active client and bridge-level closed state */
@@ -78,6 +127,8 @@ export default class MCPCommand {
78
127
  };
79
128
  /* (re-)connect the HTTP client to the service */
80
129
  const connectClient = async () => {
130
+ if (port === null)
131
+ throw new Error("mcp: no service port known");
81
132
  const url = new URL(`http://${HOST}:${port}/mcp`);
82
133
  const next = new StreamableHTTPClientTransport(url);
83
134
  next.onmessage = (msg) => {
@@ -149,31 +200,133 @@ export default class MCPCommand {
149
200
  pending.push(msg);
150
201
  return;
151
202
  }
152
- client.send(msg).catch((err) => {
153
- this.log.write("error", `mcp: http send: ${this.asError(err).message}`);
203
+ const target = client;
204
+ target.send(msg).catch((err) => {
205
+ if (!isConnRefused(err)) {
206
+ this.log.write("error", `mcp: http send: ${this.asError(err).message}`);
207
+ return;
208
+ }
209
+ /* service is gone: detach the dead client, recover, and re-send */
210
+ if (client === target) {
211
+ closedByUs.add(target);
212
+ client = null;
213
+ target.close().catch(() => { });
214
+ triggerReconnect("http connection refused");
215
+ }
216
+ sendToClient(msg);
154
217
  });
155
218
  };
219
+ /* while deferred, answer all messages in-process through the same MCP
220
+ server the service would use -- this covers the MCP handshake and
221
+ the tool listing, which GitHub Copilot CLI insists on before it even
222
+ runs the session-start hook -- except the tool calls, which are held
223
+ back in the pending buffer until the service is connected */
224
+ let local = null;
225
+ if (deferred) {
226
+ const [localClient, localServer] = InMemoryTransport.createLinkedPair();
227
+ await ServiceCommand.buildMcpServer(this.log, { projectId: "", port: 0 }, Date.now()).connect(localServer);
228
+ localClient.onmessage = (msg) => {
229
+ server.send(msg).catch((err) => {
230
+ this.log.write("error", `mcp: stdout send: ${this.asError(err).message}`);
231
+ });
232
+ };
233
+ await localClient.start();
234
+ local = localClient;
235
+ }
236
+ /* stop deferring: switch into the recorded project directory (if any),
237
+ so the service is looked up -- and if needed started -- inside the
238
+ project, then connect to it and flush the held tool calls */
239
+ const POLL_INTERVAL_MS = 250;
240
+ const FALLBACK_MS = 30_000;
241
+ let pollTimer = null;
242
+ let fallbackTimer = null;
243
+ const undefer = (dir) => {
244
+ if (local === null)
245
+ return;
246
+ local.close().catch(() => { });
247
+ local = null;
248
+ if (pollTimer !== null)
249
+ clearInterval(pollTimer);
250
+ if (fallbackTimer !== null)
251
+ clearTimeout(fallbackTimer);
252
+ if (dir === null)
253
+ this.log.write("warning", "mcp: no project directory recorded by session-start hook " +
254
+ `-- falling back to the current directory "${process.cwd()}"`);
255
+ else {
256
+ try {
257
+ process.chdir(dir);
258
+ }
259
+ catch (err) {
260
+ this.log.write("warning", `mcp: cannot switch to recorded project directory "${dir}": ` +
261
+ `${this.asError(err).message} -- falling back to the current directory`);
262
+ }
263
+ }
264
+ server.onmessage = sendToClient;
265
+ const run = async () => {
266
+ const ctx = await this.ensureService();
267
+ port = ctx.port;
268
+ projectId = ctx.projectId;
269
+ await connectClient();
270
+ this.log.write("info", `mcp: connected to service of project "${projectId}" on port ${port}`);
271
+ };
272
+ run().catch((err) => {
273
+ triggerReconnect(`deferred connect failed: ${this.asError(err).message}`);
274
+ });
275
+ };
276
+ /* look for the recorded project directory */
277
+ const checkProjectDir = () => {
278
+ const dir = this.readSessionProjectDir(sessionId);
279
+ if (dir !== null)
280
+ undefer(dir);
281
+ };
156
282
  /* wire stdio server */
157
- server.onmessage = sendToClient;
283
+ if (!deferred)
284
+ server.onmessage = sendToClient;
285
+ else
286
+ server.onmessage = (msg) => {
287
+ if (local === null)
288
+ sendToClient(msg);
289
+ else if ("method" in msg && msg.method === "tools/call") {
290
+ /* hold back tool calls, and in case the session-start hook
291
+ never records a project directory, do not hold them back
292
+ forever (measured from the first held call and not from
293
+ the start, as an interactive session can idle arbitrarily
294
+ long before the hook runs with its first prompt) */
295
+ sendToClient(msg);
296
+ if (fallbackTimer === null)
297
+ fallbackTimer = setTimeout(() => { undefer(null); }, FALLBACK_MS);
298
+ checkProjectDir();
299
+ }
300
+ else
301
+ local.send(msg).catch((err) => {
302
+ this.log.write("error", `mcp: local send: ${this.asError(err).message}`);
303
+ });
304
+ };
158
305
  server.onerror = (err) => {
159
306
  this.log.write("error", `mcp: stdio: ${err.message}`);
160
307
  };
161
308
  server.onclose = () => {
162
309
  shutdown().catch(() => { });
163
310
  };
164
- /* start server and initial client */
311
+ /* start server and initial client (or the polling for the recorded project directory) */
165
312
  await server.start();
166
- try {
167
- await connectClient();
313
+ if (!deferred) {
314
+ try {
315
+ await connectClient();
316
+ }
317
+ catch (err) {
318
+ /* service vanished between probe and connect — recover instead of crashing */
319
+ triggerReconnect(`initial connect failed: ${this.asError(err).message}`);
320
+ }
168
321
  }
169
- catch (err) {
170
- /* service vanished between probe and connect — recover instead of crashing */
171
- triggerReconnect(`initial connect failed: ${this.asError(err).message}`);
322
+ else {
323
+ pollTimer = setInterval(checkProjectDir, POLL_INTERVAL_MS);
324
+ pollTimer.unref();
172
325
  }
173
326
  /* periodically probe the service; trigger reconnect if it is gone */
174
327
  const HEALTH_INTERVAL_MS = 30_000;
175
328
  const healthTimer = setInterval(async () => {
176
- if (bridgeDone || reconnecting)
329
+ if (bridgeDone || reconnecting || port === null)
177
330
  return;
178
331
  try {
179
332
  const match = await probe(port, projectId);
@@ -72,6 +72,75 @@ const unquote = (s) => {
72
72
  }
73
73
  return out;
74
74
  };
75
+ /* slice the verbatim trailing arguments from the raw input by skipping all
76
+ leading option tokens (and the separate value tokens they take), mirroring
77
+ the pass-through semantics of commander on the verbatim text */
78
+ const verbatimArgs = (input, flagTakesValue) => {
79
+ const ranges = tokenizeRanges(input);
80
+ let idx = 0;
81
+ while (idx < ranges.length) {
82
+ const tok = unquote(input.slice(ranges[idx].start, ranges[idx].end));
83
+ if (tok === "--") {
84
+ idx++;
85
+ break;
86
+ }
87
+ if (!tok.startsWith("-") || tok === "-")
88
+ break;
89
+ idx++;
90
+ if (flagTakesValue.get(tok) === true && idx < ranges.length)
91
+ idx++;
92
+ }
93
+ return idx < ranges.length ? input.slice(ranges[idx].start) : "";
94
+ };
95
+ /* convert a dashed long option name into its camel-cased commander key */
96
+ const camelKey = (long) => long.replace(/-(.)/g, (_, c) => c.toUpperCase());
97
+ /* parse the options specification into commander options, plus the
98
+ information needed for list validation and verbatim argument slicing */
99
+ const parseSpec = (spec) => {
100
+ const tokens = spec.split(/\s+/).filter((e) => e.length > 0);
101
+ const re = /^--([A-Za-z][A-Za-z0-9-]*)(?:\|-([A-Za-z]))?(?:=(\((.*)\)(\.\.\.)?|.*))?$/;
102
+ const internals = new Set();
103
+ const flagTakesValue = new Map();
104
+ const options = [];
105
+ const listOpts = [];
106
+ for (const tok of tokens) {
107
+ const m = re.exec(tok);
108
+ if (m === null)
109
+ throw new Error(`invalid spec token "${tok}"`);
110
+ const long = m[1];
111
+ const short = m[2] ?? null;
112
+ const valuePart = m[3] ?? null;
113
+ const choicePart = m[4] ?? null;
114
+ const listMarker = m[5] ?? null;
115
+ const takesValue = valuePart !== null;
116
+ const choices = choicePart !== null ? choicePart.split("|") : null;
117
+ const isList = listMarker !== null;
118
+ const dflt = choices !== null ? choices[0] : valuePart;
119
+ flagTakesValue.set(`--${long}`, takesValue);
120
+ if (short !== null)
121
+ flagTakesValue.set(`-${short}`, takesValue);
122
+ const head = short !== null ? `-${short}, --${long}` : `--${long}`;
123
+ const flags = takesValue ? `${head} <value>` : head;
124
+ const opt = new Option(flags);
125
+ if (takesValue) {
126
+ if (choices !== null && !isList)
127
+ opt.choices(choices);
128
+ opt.default(dflt);
129
+ }
130
+ else
131
+ opt.default(false);
132
+ if (choices !== null && isList)
133
+ listOpts.push({ long, choices });
134
+ if (long.startsWith("int-")) {
135
+ /* internal option: hide from usage help and remember
136
+ its camel-cased key for the info rendering */
137
+ opt.hideHelp();
138
+ internals.add(camelKey(long));
139
+ }
140
+ options.push(opt);
141
+ }
142
+ return { options, internals, flagTakesValue, listOpts };
143
+ };
75
144
  /* MCP registration entry point for the option-parser tool */
76
145
  export class GetoptMCP {
77
146
  register(mcp) {
@@ -92,9 +161,9 @@ export class GetoptMCP {
92
161
  name: z.string()
93
162
  .describe("Name of the caller (e.g. skill name), used in error messages"),
94
163
  spec: z.string()
95
- .describe("Whitespace-separated option spec, e.g. `--foo/-f --bar --baz/-b=BAZ`"),
164
+ .describe("Whitespace-separated option spec, e.g. `--foo|-f --bar --baz|-b=BAZ`"),
96
165
  args: z.union([z.string(), z.array(z.string())])
97
- .describe("Arguments to parse (string is split on whitespace)")
166
+ .describe("Arguments to parse (string is split shell-like, honoring quotes)")
98
167
  }
99
168
  }, async (args) => {
100
169
  let helpText = "";
@@ -102,11 +171,11 @@ export class GetoptMCP {
102
171
  /* normalize args */
103
172
  const argsRaw = typeof args.args === "string" ? args.args : null;
104
173
  const argsVec = typeof args.args === "string" ?
105
- shParse(args.args)
106
- .map((e) => typeof e === "string" ? e :
107
- (e !== null && typeof e === "object" && "op" in e && e.op === "glob" ?
108
- e.pattern : null))
109
- .filter((e) => e !== null) :
174
+ shParse(args.args, (key) => `$${key}`)
175
+ .flatMap((e) => typeof e === "string" ? [e] :
176
+ "pattern" in e ? [e.pattern] :
177
+ "op" in e ? [e.op] :
178
+ `#${e.comment}`.split(/\s+/).filter((s) => s.length > 0)) :
110
179
  args.args;
111
180
  /* build a fresh commander program */
112
181
  const cmd = new Command(args.name)
@@ -118,49 +187,10 @@ export class GetoptMCP {
118
187
  writeOut: (str) => { helpText += str; },
119
188
  writeErr: () => { }
120
189
  });
121
- /* tokenize spec and add one option per token */
122
- const tokens = args.spec.split(/\s+/).filter((e) => e.length > 0);
123
- const re = /^--([A-Za-z][A-Za-z0-9-]*)(?:\|-([A-Za-z]))?(?:=(\((.*)\)(\.\.\.)?|.*))?$/;
124
- const camelKey = (long) => long.replace(/-(.)/g, (_, c) => c.toUpperCase());
125
- const internals = new Set();
126
- const flagTakesValue = new Map();
127
- const listOpts = [];
128
- for (const tok of tokens) {
129
- const m = re.exec(tok);
130
- if (m === null)
131
- throw new Error(`invalid spec token "${tok}"`);
132
- const long = m[1];
133
- const short = m[2] ?? null;
134
- const valuePart = m[3] ?? null;
135
- const choicePart = m[4] ?? null;
136
- const listMarker = m[5] ?? null;
137
- const takesValue = valuePart !== null;
138
- const choices = choicePart !== null ? choicePart.split("|") : null;
139
- const isList = listMarker !== null;
140
- const dflt = choices !== null ? choices[0] : valuePart;
141
- flagTakesValue.set(`--${long}`, takesValue);
142
- if (short !== null)
143
- flagTakesValue.set(`-${short}`, takesValue);
144
- const head = short !== null ? `-${short}, --${long}` : `--${long}`;
145
- const flags = takesValue ? `${head} <value>` : head;
146
- const opt = new Option(flags);
147
- if (takesValue) {
148
- if (choices !== null && !isList)
149
- opt.choices(choices);
150
- opt.default(dflt);
151
- }
152
- else
153
- opt.default(false);
154
- if (choices !== null && isList)
155
- listOpts.push({ long, choices });
156
- if (long.startsWith("int-")) {
157
- /* internal option: hide from usage help and remember
158
- its camel-cased key for the info rendering below */
159
- opt.hideHelp();
160
- internals.add(camelKey(long));
161
- }
190
+ /* parse spec and add its options */
191
+ const { options, internals, flagTakesValue, listOpts } = parseSpec(args.spec);
192
+ for (const opt of options)
162
193
  cmd.addOption(opt);
163
- }
164
194
  /* parse args */
165
195
  cmd.parse(argsVec, { from: "user" });
166
196
  /* validate comma-separated list-of-choices options (only
@@ -180,37 +210,9 @@ export class GetoptMCP {
180
210
  }
181
211
  }
182
212
  /* compute verbatim trailing argument string */
183
- let argsVerbatim = "";
184
- if (argsRaw !== null) {
185
- /* tokenize raw input into [start,end) ranges, preserving quotes */
186
- const ranges = tokenizeRanges(argsRaw);
187
- /* walk the raw ranges, consuming leading option tokens (and any
188
- separate value tokens they take) until the first positional
189
- is reached, then slice the original input from there -- this
190
- mirrors commander's pass-through semantics while staying on
191
- the verbatim text and is robust against value-consuming
192
- options and shell-operator characters in the input */
193
- let idx = 0;
194
- while (idx < ranges.length) {
195
- const tok = unquote(argsRaw.slice(ranges[idx].start, ranges[idx].end));
196
- if (tok === "--") {
197
- idx++;
198
- break;
199
- }
200
- if (!tok.startsWith("-") || tok === "-")
201
- break;
202
- let consumesNext = false;
203
- if (/^--[^=]+$/.test(tok) || /^-[^-]$/.test(tok))
204
- consumesNext = flagTakesValue.get(tok) === true;
205
- idx++;
206
- if (consumesNext && idx < ranges.length)
207
- idx++;
208
- }
209
- if (idx < ranges.length)
210
- argsVerbatim = argsRaw.slice(ranges[idx].start);
211
- }
212
- else
213
- argsVerbatim = shQuote(cmd.args);
213
+ const argsVerbatim = argsRaw !== null ?
214
+ verbatimArgs(argsRaw, flagTakesValue) :
215
+ shQuote(cmd.args);
214
216
  /* build markdown info rendering of parsed options */
215
217
  const opts = cmd.opts();
216
218
  const info = Object.entries(opts)
@@ -6,13 +6,9 @@
6
6
  import { z } from "zod";
7
7
  /* the reusable functionality */
8
8
  export class Markdown {
9
- /* prepare Markdown for improved rendering by rewriting
10
- unordered bullet paragraphs -- replacing the "-"/"*" bullet marker with
11
- "◯" and splitting multi-line inline code spans into per-line single-line
12
- spans (so each physical line carries its own closed backtick span).
13
- Fenced code blocks (``` / ~~~) are detected line-wise and passed
14
- through entirely verbatim, so neither the inline-span splitting nor the
15
- bullet-marker rewriting ever touches their contents. */
9
+ /* prepare Markdown for improved rendering: rewrite escaped-backtick code
10
+ spans, split multi-line code spans into per-line spans, and replace
11
+ "-"/"*" bullet markers with "◯", passing fenced code blocks verbatim */
16
12
  static prepare(text) {
17
13
  if (typeof text !== "string")
18
14
  throw new Error("markdown: text must be a string");
@@ -89,9 +85,14 @@ export class Markdown {
89
85
  let j = 0;
90
86
  while (j < text.length) {
91
87
  const ch = text[j];
92
- if (ch !== "`") {
88
+ if (ch === "\\" && text[j + 1] === "`") {
93
89
  /* literal backslash-escaped backtick *outside* any span
94
90
  is left verbatim for later scanning */
91
+ pre += "\\`";
92
+ j += 2;
93
+ continue;
94
+ }
95
+ if (ch !== "`") {
95
96
  pre += ch;
96
97
  j++;
97
98
  continue;
@@ -137,9 +138,15 @@ export class Markdown {
137
138
  inner += c;
138
139
  k++;
139
140
  }
140
- if (!closed || !escaped) {
141
- /* not an escaped-backtick span: emit the opening run
142
- verbatim and continue scanning from just after it */
141
+ if (closed && !escaped) {
142
+ /* regular span: emit it verbatim and continue after it */
143
+ pre += text.slice(j, k + open);
144
+ j = k + open;
145
+ continue;
146
+ }
147
+ if (!closed) {
148
+ /* unmatched opening run: emit it verbatim and continue
149
+ scanning from just after it */
143
150
  pre += "`".repeat(open);
144
151
  j += open;
145
152
  continue;