@rse/ase 1.0.6 → 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.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /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 {
|
|
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
|
-
/*
|
|
52
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
|
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
|
-
.
|
|
107
|
-
|
|
108
|
-
e.
|
|
109
|
-
|
|
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
|
-
/*
|
|
122
|
-
const
|
|
123
|
-
const
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
"
|
|
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 (
|
|
141
|
-
/*
|
|
142
|
-
|
|
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;
|