@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.
- 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 +228 -111
- 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-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
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
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
|
-
/*
|
|
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
|
-
|
|
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, "..")
|
|
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" (
|
|
46
|
+
/* handler for "ase setup update" (all tools) */
|
|
152
47
|
async doUpdate(tool, force, dev, scope) {
|
|
153
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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" (
|
|
105
|
+
/* handler for "ase setup enable|disable" (all tools) */
|
|
209
106
|
async doToggle(tool, scope, action) {
|
|
210
|
-
|
|
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" (
|
|
122
|
+
/* handler for "ase setup uninstall" (all tools) */
|
|
226
123
|
async doUninstall(tool, dev, scope) {
|
|
227
|
-
|
|
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
|
|
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(
|
|
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\"
|
|
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
|
-
|
|
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\"
|
|
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
|
-
|
|
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\"
|
|
290
|
+
.option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\", default: auto-detected)")
|
|
1075
291
|
.action(async (opts) => {
|
|
1076
|
-
|
|
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\"
|
|
301
|
+
.option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\", default: auto-detected)")
|
|
1084
302
|
.action(async (opts) => {
|
|
1085
|
-
|
|
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
|
|
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(
|
|
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",
|
|
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",
|
|
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(
|
|
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(
|
|
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
|
}
|