@rse/ase 1.0.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +357 -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} +8 -1
- 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 +269 -81
- 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 +231 -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 +81 -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
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import writeFileAtomic from "write-file-atomic";
|
|
10
|
+
import { mkdirp } from "mkdirp";
|
|
11
|
+
import JsonAsty from "json-asty";
|
|
12
|
+
import { toolSpecs } from "./ase-setup-common.js";
|
|
13
|
+
import { telemetryEndpoint, telemetryEnabled } from "./ase-service-otel.js";
|
|
14
|
+
/* settings file handling of "ase setup statusline", "ase setup telemetry", and the output style */
|
|
15
|
+
export class SetupSettings {
|
|
16
|
+
log;
|
|
17
|
+
constructor(log) {
|
|
18
|
+
this.log = log;
|
|
19
|
+
}
|
|
20
|
+
/* the default "ase statusline" format lines used when the user does
|
|
21
|
+
not override them with positional arguments to "activate" */
|
|
22
|
+
statuslineFormatDflt = [
|
|
23
|
+
"<blue>%u</blue> <red>%p</red> <black>%T</black> %s",
|
|
24
|
+
"%m %e %t",
|
|
25
|
+
"%P %h %B %c"
|
|
26
|
+
];
|
|
27
|
+
/* resolve the tool settings file for a given installation scope */
|
|
28
|
+
settingsFile(tool, scope) {
|
|
29
|
+
if (tool === "copilot") {
|
|
30
|
+
const home = process.env.COPILOT_HOME ?? "";
|
|
31
|
+
const base = home !== "" ? home : path.join(os.homedir(), ".copilot");
|
|
32
|
+
return path.join(base, "settings.json");
|
|
33
|
+
}
|
|
34
|
+
else if (scope === "project")
|
|
35
|
+
return path.join(process.cwd(), ".claude", "settings.json");
|
|
36
|
+
else if (scope === "local")
|
|
37
|
+
return path.join(process.cwd(), ".claude", "settings.local.json");
|
|
38
|
+
else
|
|
39
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
40
|
+
}
|
|
41
|
+
/* reject the statusline operation for tools without a scriptable
|
|
42
|
+
statusline mechanism (only the OpenAI Codex CLI lacks one) */
|
|
43
|
+
requireStatuslineTool(tool) {
|
|
44
|
+
if (tool === "codex")
|
|
45
|
+
throw new Error("statusline configuration is not supported for --tool codex " +
|
|
46
|
+
`(the ${toolSpecs[tool].label} has no scriptable statusline mechanism)`);
|
|
47
|
+
}
|
|
48
|
+
/* reject a non-default --scope for the GitHub Copilot CLI, which has a
|
|
49
|
+
single per-user settings file and thus no scope concept */
|
|
50
|
+
requireStatuslineScope(tool, scope) {
|
|
51
|
+
if (tool === "copilot" && scope !== "user")
|
|
52
|
+
throw new Error("--scope is only supported for --tool claude " +
|
|
53
|
+
`(the ${toolSpecs[tool].label} has a single per-user settings file)`);
|
|
54
|
+
}
|
|
55
|
+
/* build the "ase statusline ..." command string from the activate
|
|
56
|
+
options and the effective format lines */
|
|
57
|
+
statuslineCommand(tool, opts, format) {
|
|
58
|
+
const parts = ["ase", "statusline", "--tool", tool, "-w", String(opts.width), "-m", String(opts.margin),
|
|
59
|
+
"-p", String(opts.padding)];
|
|
60
|
+
if (!opts.icons)
|
|
61
|
+
parts.push("--no-icons");
|
|
62
|
+
if (!opts.labels)
|
|
63
|
+
parts.push("--no-labels");
|
|
64
|
+
const lines = format.length > 0 ? format : this.statuslineFormatDflt;
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
/* Copilot CLI on Windows spawns via cmd.exe, where single
|
|
67
|
+
quotes do not quote -- replace with double quotes */
|
|
68
|
+
if (process.platform === "win32" && tool === "copilot")
|
|
69
|
+
parts.push(`"${line.replace(/"/g, "\\\"")}"`);
|
|
70
|
+
else
|
|
71
|
+
parts.push(`'${line.replace(/'/g, "'\\''")}'`);
|
|
72
|
+
}
|
|
73
|
+
return parts.join(" ");
|
|
74
|
+
}
|
|
75
|
+
/* determine whether an existing "statusLine" value object is owned by us */
|
|
76
|
+
statuslineIsOwned(member) {
|
|
77
|
+
const obj = member.query("/ * [ pos() == 2 ]")[0];
|
|
78
|
+
if (obj === undefined)
|
|
79
|
+
return false;
|
|
80
|
+
for (const m of obj.query("/ member")) {
|
|
81
|
+
const key = m.query("/ string [ pos() == 1 ]")[0];
|
|
82
|
+
if (key !== undefined && key.get("value") === "command") {
|
|
83
|
+
const val = m.query("/ string [ pos() == 2 ]")[0];
|
|
84
|
+
const cmd = val !== undefined ? String(val.get("value") ?? "") : "";
|
|
85
|
+
return cmd.startsWith("ase statusline");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/* locate a top-level member node by key within the root object */
|
|
91
|
+
settingsFindMember(root, name) {
|
|
92
|
+
for (const m of root.query("/ member")) {
|
|
93
|
+
const key = m.query("/ string [ pos() == 1 ]")[0];
|
|
94
|
+
if (key !== undefined && key.get("value") === name)
|
|
95
|
+
return m;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/* build the "statusLine" member subtree natively inside the target
|
|
100
|
+
AST, reproducing the exact whitespace tokens json-asty emits for a
|
|
101
|
+
canonically 4-space-indented settings.json */
|
|
102
|
+
statuslineBuildMember(root, command) {
|
|
103
|
+
const member = (key, val, epilog) => {
|
|
104
|
+
const type = typeof val === "number" ? "number" : "string";
|
|
105
|
+
const body = typeof val === "number" ? String(val) : JSON.stringify(val);
|
|
106
|
+
const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
|
|
107
|
+
const v = root.create(type).set({ body, value: val });
|
|
108
|
+
const m = root.create("member");
|
|
109
|
+
if (epilog !== undefined)
|
|
110
|
+
m.set({ epilog });
|
|
111
|
+
return m.add(k, v);
|
|
112
|
+
};
|
|
113
|
+
const obj = root.create("object").set({ prolog: "{\n ", epilog: "\n }\n" });
|
|
114
|
+
obj.add(member("type", "command", ",\n "), member("command", command, ",\n "), member("padding", 0));
|
|
115
|
+
const key = root.create("string").set({
|
|
116
|
+
body: JSON.stringify("statusLine"),
|
|
117
|
+
value: "statusLine",
|
|
118
|
+
epilog: ": "
|
|
119
|
+
});
|
|
120
|
+
return root.create("member").add(key, obj);
|
|
121
|
+
}
|
|
122
|
+
/* normalize the previous-last member so a following ",\n " comma reads cleanly */
|
|
123
|
+
settingsMakeNonLast(member) {
|
|
124
|
+
const val = member.query("/ * [ pos() == 2 ]")[0];
|
|
125
|
+
if (val !== undefined) {
|
|
126
|
+
const ep = val.get("epilog");
|
|
127
|
+
if (typeof ep === "string" && ep.endsWith("\n"))
|
|
128
|
+
val.set({ epilog: ep.replace(/\n$/, "") });
|
|
129
|
+
}
|
|
130
|
+
member.set({ epilog: ",\n " });
|
|
131
|
+
}
|
|
132
|
+
/* append a member to the root object, repairing the whitespace at
|
|
133
|
+
the seam: a scalar value carries no pre-"}" newline of its own
|
|
134
|
+
(a container value carries it in its own epilog already), so the
|
|
135
|
+
root epilog has to supply it */
|
|
136
|
+
settingsAddMember(root, member, scalar) {
|
|
137
|
+
const members = root.query("/ member");
|
|
138
|
+
if (members.length === 0)
|
|
139
|
+
root.set({ prolog: "{\n " });
|
|
140
|
+
else
|
|
141
|
+
this.settingsMakeNonLast(members[members.length - 1]);
|
|
142
|
+
root.set({ epilog: scalar ? "\n}\n" : "}\n" });
|
|
143
|
+
root.add(member);
|
|
144
|
+
}
|
|
145
|
+
/* remove a member from the root object, repairing the whitespace at the seam */
|
|
146
|
+
settingsRemoveMember(root, target) {
|
|
147
|
+
const members = root.query("/ member");
|
|
148
|
+
const wasLast = members[members.length - 1] === target;
|
|
149
|
+
root.del(target);
|
|
150
|
+
if (wasLast) {
|
|
151
|
+
if (members.length > 1) {
|
|
152
|
+
/* promote the new last member: strip its comma epilog and
|
|
153
|
+
restore the pre-"}" newline into the root epilog when the
|
|
154
|
+
new-last value is a scalar (a container value carries the
|
|
155
|
+
newline in its own epilog already) */
|
|
156
|
+
const newLast = members[members.length - 2];
|
|
157
|
+
newLast.set({ epilog: undefined });
|
|
158
|
+
const val = newLast.query("/ * [ pos() == 2 ]")[0];
|
|
159
|
+
const ep = val !== undefined ? val.get("epilog") : undefined;
|
|
160
|
+
root.set({ epilog: (typeof ep === "string" && ep.endsWith("\n")) ? "}\n" : "\n}\n" });
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
root.set({ prolog: "{", epilog: "}\n" });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/* read a settings.json file into a JSON-ASTy AST */
|
|
167
|
+
async settingsReadAst(file) {
|
|
168
|
+
let text = "";
|
|
169
|
+
try {
|
|
170
|
+
text = await fs.readFile(file, "utf8");
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
/* missing file: start from an empty object */
|
|
174
|
+
if (err.code !== "ENOENT")
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
if (text.trim() === "")
|
|
178
|
+
text = "{}";
|
|
179
|
+
return JsonAsty.parse(text);
|
|
180
|
+
}
|
|
181
|
+
/* write a JSON-ASTy AST back to a settings.json file */
|
|
182
|
+
async settingsWriteAst(file, root) {
|
|
183
|
+
await mkdirp(path.dirname(file));
|
|
184
|
+
const text = JsonAsty.unparse(root);
|
|
185
|
+
await writeFileAtomic(file, text, { encoding: "utf8" });
|
|
186
|
+
}
|
|
187
|
+
/* handler for "ase setup statusline activate" */
|
|
188
|
+
async doStatuslineActivate(tool, scope, opts, format) {
|
|
189
|
+
this.requireStatuslineTool(tool);
|
|
190
|
+
this.requireStatuslineScope(tool, scope);
|
|
191
|
+
const file = this.settingsFile(tool, scope);
|
|
192
|
+
const command = this.statuslineCommand(tool, opts, format);
|
|
193
|
+
const root = await this.settingsReadAst(file);
|
|
194
|
+
const existing = this.settingsFindMember(root, "statusLine");
|
|
195
|
+
if (existing !== undefined) {
|
|
196
|
+
/* preserve a foreign, hand-crafted statusLine: skip and warn */
|
|
197
|
+
if (!this.statuslineIsOwned(existing)) {
|
|
198
|
+
this.log.write("warning", "setup: statusline: activate: a non-ASE \"statusLine\" " +
|
|
199
|
+
`is already present in ${file}: preserving it (skipped)`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
/* replace the value object in place, preserving its epilog */
|
|
203
|
+
const valNew = this.statuslineBuildMember(root, command).query("/ object")[0];
|
|
204
|
+
const valOld = existing.query("/ * [ pos() == 2 ]")[0];
|
|
205
|
+
valNew.set({ epilog: valOld.get("epilog") });
|
|
206
|
+
existing.del(valOld).add(valNew);
|
|
207
|
+
this.log.write("info", `setup: statusline: activate: updating ASE "statusLine" in ${file}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
/* insert a fresh statusLine member */
|
|
211
|
+
this.settingsAddMember(root, this.statuslineBuildMember(root, command), false);
|
|
212
|
+
this.log.write("info", `setup: statusline: activate: adding ASE "statusLine" to ${file}`);
|
|
213
|
+
}
|
|
214
|
+
await this.settingsWriteAst(file, root);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
/* handler for "ase setup statusline deactivate" */
|
|
218
|
+
async doStatuslineDeactivate(tool, scope) {
|
|
219
|
+
this.requireStatuslineTool(tool);
|
|
220
|
+
this.requireStatuslineScope(tool, scope);
|
|
221
|
+
const file = this.settingsFile(tool, scope);
|
|
222
|
+
/* a missing settings file means nothing to remove */
|
|
223
|
+
try {
|
|
224
|
+
await fs.access(file);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
this.log.write("info", `setup: statusline: deactivate: no settings file ${file} (skipped)`);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
/* read file */
|
|
231
|
+
const root = await this.settingsReadAst(file);
|
|
232
|
+
const target = this.settingsFindMember(root, "statusLine");
|
|
233
|
+
if (target === undefined) {
|
|
234
|
+
this.log.write("info", `setup: statusline: deactivate: no "statusLine" in ${file} (skipped)`);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
/* preserve a foreign, hand-crafted statusLine: skip and warn */
|
|
238
|
+
if (!this.statuslineIsOwned(target)) {
|
|
239
|
+
this.log.write("warning", "setup: statusline: deactivate: a non-ASE \"statusLine\" " +
|
|
240
|
+
`is present in ${file}: preserving it (skipped)`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
/* remove the member and repair the whitespace at the seam */
|
|
244
|
+
this.settingsRemoveMember(root, target);
|
|
245
|
+
await this.settingsWriteAst(file, root);
|
|
246
|
+
this.log.write("info", `setup: statusline: deactivate: removing ASE "statusLine" from ${file}`);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
/* probe the ASE statusline registrations by inspecting the tool
|
|
250
|
+
settings files of all applicable installation scopes */
|
|
251
|
+
async statuslineStatus(tool) {
|
|
252
|
+
/* the OpenAI Codex CLI has no scriptable statusline mechanism at all */
|
|
253
|
+
if (tool === "codex")
|
|
254
|
+
return [];
|
|
255
|
+
const home = os.homedir();
|
|
256
|
+
const scopes = tool === "claude" ? ["user", "project", "local"] : ["user"];
|
|
257
|
+
const entries = [];
|
|
258
|
+
for (const scope of scopes) {
|
|
259
|
+
const file = this.settingsFile(tool, scope);
|
|
260
|
+
let root;
|
|
261
|
+
try {
|
|
262
|
+
root = await this.settingsReadAst(file);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
/* an unparsable settings file carries no usable state */
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const member = this.settingsFindMember(root, "statusLine");
|
|
269
|
+
if (member === undefined)
|
|
270
|
+
continue;
|
|
271
|
+
entries.push({
|
|
272
|
+
file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
|
|
273
|
+
scope: tool === "claude" ? scope : "(n/a)",
|
|
274
|
+
status: this.statuslineIsOwned(member) ? "activated" : "foreign"
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return entries;
|
|
278
|
+
}
|
|
279
|
+
/* determine whether an existing "outputStyle" value is owned by us */
|
|
280
|
+
outputStyleIsOwned(member) {
|
|
281
|
+
const val = member.query("/ string [ pos() == 2 ]")[0];
|
|
282
|
+
return val !== undefined && val.get("value") === "ase:ase-terse";
|
|
283
|
+
}
|
|
284
|
+
/* build the "outputStyle" member subtree natively inside the target AST */
|
|
285
|
+
outputStyleBuildMember(root) {
|
|
286
|
+
const key = root.create("string").set({ body: JSON.stringify("outputStyle"), value: "outputStyle", epilog: ": " });
|
|
287
|
+
const val = root.create("string").set({ body: JSON.stringify("ase:ase-terse"), value: "ase:ase-terse" });
|
|
288
|
+
return root.create("member").add(key, val);
|
|
289
|
+
}
|
|
290
|
+
/* activate the plugin-shipped ASE output style by selecting it in
|
|
291
|
+
the settings file of the scope (Anthropic Claude Code CLI only:
|
|
292
|
+
the other tools have no output style concept, so the session-start
|
|
293
|
+
hook injects the style into their session context instead) */
|
|
294
|
+
async outputStyleActivate(action, scope) {
|
|
295
|
+
const file = this.settingsFile("claude", scope);
|
|
296
|
+
const root = await this.settingsReadAst(file);
|
|
297
|
+
const existing = this.settingsFindMember(root, "outputStyle");
|
|
298
|
+
if (existing !== undefined) {
|
|
299
|
+
/* preserve a foreign, hand-selected outputStyle: skip and warn */
|
|
300
|
+
if (!this.outputStyleIsOwned(existing))
|
|
301
|
+
this.log.write("warning", `setup: ${action}: a non-ASE "outputStyle" ` +
|
|
302
|
+
`is already selected in ${file}: preserving it (skipped)`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.settingsAddMember(root, this.outputStyleBuildMember(root), true);
|
|
306
|
+
await this.settingsWriteAst(file, root);
|
|
307
|
+
this.log.write("info", `setup: ${action}: selecting ASE "outputStyle" in ${file}`);
|
|
308
|
+
}
|
|
309
|
+
/* deactivate the plugin-shipped ASE output style by removing its
|
|
310
|
+
selection from the settings file of the scope (Anthropic Claude Code CLI only) */
|
|
311
|
+
async outputStyleDeactivate(action, scope) {
|
|
312
|
+
const file = this.settingsFile("claude", scope);
|
|
313
|
+
/* a missing settings file means nothing to remove */
|
|
314
|
+
try {
|
|
315
|
+
await fs.access(file);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const root = await this.settingsReadAst(file);
|
|
321
|
+
const target = this.settingsFindMember(root, "outputStyle");
|
|
322
|
+
if (target === undefined)
|
|
323
|
+
return;
|
|
324
|
+
/* preserve a foreign, hand-selected outputStyle: skip and warn */
|
|
325
|
+
if (!this.outputStyleIsOwned(target)) {
|
|
326
|
+
this.log.write("warning", `setup: ${action}: a non-ASE "outputStyle" ` +
|
|
327
|
+
`is selected in ${file}: preserving it (skipped)`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.settingsRemoveMember(root, target);
|
|
331
|
+
await this.settingsWriteAst(file, root);
|
|
332
|
+
this.log.write("info", `setup: ${action}: deselecting ASE "outputStyle" in ${file}`);
|
|
333
|
+
}
|
|
334
|
+
/* probe the output style selections by inspecting the tool
|
|
335
|
+
settings files of all installation scopes (Anthropic Claude Code CLI only) */
|
|
336
|
+
async outputStyleStatus(tool) {
|
|
337
|
+
if (tool !== "claude")
|
|
338
|
+
return [];
|
|
339
|
+
const home = os.homedir();
|
|
340
|
+
const scopes = ["user", "project", "local"];
|
|
341
|
+
const entries = [];
|
|
342
|
+
for (const scope of scopes) {
|
|
343
|
+
const file = this.settingsFile(tool, scope);
|
|
344
|
+
let root;
|
|
345
|
+
try {
|
|
346
|
+
root = await this.settingsReadAst(file);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
/* an unparsable settings file carries no usable state */
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const member = this.settingsFindMember(root, "outputStyle");
|
|
353
|
+
if (member === undefined)
|
|
354
|
+
continue;
|
|
355
|
+
entries.push({
|
|
356
|
+
file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
|
|
357
|
+
scope,
|
|
358
|
+
status: this.outputStyleIsOwned(member) ? "selected" : "foreign"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return entries;
|
|
362
|
+
}
|
|
363
|
+
/* auto-detect the scope in which the ASE plugin is currently
|
|
364
|
+
installed, so an omitted --scope targets the existing installation
|
|
365
|
+
instead of blindly assuming the per-user scope; only the Anthropic
|
|
366
|
+
Claude Code CLI has a scope concept, so every other tool resolves to
|
|
367
|
+
"user"; the scopes are probed from the strongest ("local") down to
|
|
368
|
+
the weakest ("user"), and a scope counts as a match when its
|
|
369
|
+
settings file registers the plugin under "enabledPlugins" (the value
|
|
370
|
+
may be true or false, since even a disabled plugin is registered in
|
|
371
|
+
that scope) */
|
|
372
|
+
async detectPluginScope(tool) {
|
|
373
|
+
if (tool !== "claude")
|
|
374
|
+
return "user";
|
|
375
|
+
const scopes = ["local", "project", "user"];
|
|
376
|
+
for (const scope of scopes) {
|
|
377
|
+
const file = this.settingsFile(tool, scope);
|
|
378
|
+
/* read and parse the settings file, skipping the scope on any
|
|
379
|
+
read or parse error (a missing or malformed file simply
|
|
380
|
+
cannot register the plugin) */
|
|
381
|
+
let settings;
|
|
382
|
+
try {
|
|
383
|
+
const text = await fs.readFile(file, "utf8");
|
|
384
|
+
settings = JSON.parse(text);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
/* a match is the presence of the "ase@ase" key under
|
|
390
|
+
"enabledPlugins", regardless of its true/false value */
|
|
391
|
+
const plugins = settings?.enabledPlugins;
|
|
392
|
+
if (plugins !== undefined && plugins !== null
|
|
393
|
+
&& Object.prototype.hasOwnProperty.call(plugins, "ase@ase")) {
|
|
394
|
+
if (scope !== "user")
|
|
395
|
+
this.log.write("info", `setup: detected ASE plugin installation in scope "${scope}"`);
|
|
396
|
+
return scope;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return "user";
|
|
400
|
+
}
|
|
401
|
+
/* the marker lines delimiting the ASE-generated telemetry block
|
|
402
|
+
in the OpenAI Codex CLI configuration and in the shell profiles */
|
|
403
|
+
telemetryMarkBegin = "# BEGIN ASE telemetry (generated)";
|
|
404
|
+
telemetryMarkEnd = "# END ASE telemetry (generated)";
|
|
405
|
+
/* the table headers of a (foreign) OpenAI Codex CLI telemetry configuration */
|
|
406
|
+
telemetryCodexHeader = /^\s*\[\s*otel\s*[.\]]/;
|
|
407
|
+
/* the telemetry environment variables of the Anthropic Claude Code CLI */
|
|
408
|
+
telemetryClaudeEnv(url, token) {
|
|
409
|
+
return {
|
|
410
|
+
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
|
|
411
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
412
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
|
413
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: url,
|
|
414
|
+
OTEL_EXPORTER_OTLP_HEADERS: `Authorization=Bearer ${token}`,
|
|
415
|
+
OTEL_LOGS_EXPORT_INTERVAL: "5000"
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/* determine the telemetry variables of the Anthropic Claude Code CLI present in an "env" object */
|
|
419
|
+
telemetryClaudePresent(env) {
|
|
420
|
+
return Object.keys(this.telemetryClaudeEnv("", ""))
|
|
421
|
+
.filter((name) => this.settingsFindMember(env, name) !== undefined);
|
|
422
|
+
}
|
|
423
|
+
/* the shell assignments of a (foreign) GitHub Copilot CLI telemetry configuration */
|
|
424
|
+
telemetryCopilotAssign = /^(?!\s*#).*\b(?:COPILOT_OTEL_ENABLED|OTEL_EXPORTER_OTLP_ENDPOINT)=/;
|
|
425
|
+
/* the telemetry environment variables of the GitHub Copilot CLI, rendered
|
|
426
|
+
as a shell function wrapping the "copilot" command, with the variables
|
|
427
|
+
as single-quoted prefix assignments, so that they are passed to the
|
|
428
|
+
GitHub Copilot CLI only and not exported to every other program
|
|
429
|
+
(the "function" keyword prevents the expansion of an alias "copilot") */
|
|
430
|
+
telemetryCopilotFunc(url, token) {
|
|
431
|
+
const env = {
|
|
432
|
+
COPILOT_OTEL_ENABLED: "true",
|
|
433
|
+
COPILOT_OTEL_EXPORTER_TYPE: "otlp-http",
|
|
434
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: url,
|
|
435
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
|
436
|
+
OTEL_EXPORTER_OTLP_HEADERS: `Authorization=Bearer ${token}`
|
|
437
|
+
};
|
|
438
|
+
return [
|
|
439
|
+
"function copilot {",
|
|
440
|
+
...Object.entries(env).map(([name, value]) => ` ${name}='${value.replace(/'/g, "'\\''")}' \\`),
|
|
441
|
+
" command copilot \"$@\"",
|
|
442
|
+
"}"
|
|
443
|
+
];
|
|
444
|
+
}
|
|
445
|
+
/* determine a foreign GitHub Copilot CLI telemetry configuration in the
|
|
446
|
+
environment, i.e., an "OTEL_EXPORTER_OTLP_ENDPOINT" other than the URL of
|
|
447
|
+
the ASE ingress (as the ASE configuration itself exports no variables) */
|
|
448
|
+
telemetryCopilotForeignEnv() {
|
|
449
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
|
|
450
|
+
if (endpoint === "")
|
|
451
|
+
return undefined;
|
|
452
|
+
const { url } = telemetryEndpoint(this.log, false);
|
|
453
|
+
return endpoint !== url ? endpoint : undefined;
|
|
454
|
+
}
|
|
455
|
+
/* the telemetry configuration of the OpenAI Codex CLI */
|
|
456
|
+
telemetryCodexConf(url, token) {
|
|
457
|
+
return [
|
|
458
|
+
"[otel]",
|
|
459
|
+
`exporter = { otlp-http = { endpoint = ${JSON.stringify(url + "/v1/logs")}, protocol = "json", ` +
|
|
460
|
+
`headers = { "authorization" = ${JSON.stringify("Bearer " + token)} } } }`
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
/* resolve the OpenAI Codex CLI configuration file */
|
|
464
|
+
telemetryCodexFile() {
|
|
465
|
+
const home = process.env.CODEX_HOME ?? "";
|
|
466
|
+
const base = home !== "" ? home : path.join(os.homedir(), ".codex");
|
|
467
|
+
return path.join(base, "config.toml");
|
|
468
|
+
}
|
|
469
|
+
/* resolve the shell profile of the login shell of the user
|
|
470
|
+
(undefined for shells other than "zsh" and "bash") */
|
|
471
|
+
telemetryShellProfile() {
|
|
472
|
+
const shell = path.basename(process.env.SHELL ?? "");
|
|
473
|
+
if (shell === "zsh") {
|
|
474
|
+
const zdotdir = process.env.ZDOTDIR ?? "";
|
|
475
|
+
return path.join(zdotdir !== "" ? zdotdir : os.homedir(), ".zshrc");
|
|
476
|
+
}
|
|
477
|
+
else if (shell === "bash")
|
|
478
|
+
return path.join(os.homedir(), ".bashrc");
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
/* resolve all candidate shell profiles which can carry the
|
|
482
|
+
ASE-generated telemetry block (for deactivation and status) */
|
|
483
|
+
telemetryShellProfiles() {
|
|
484
|
+
const files = [path.join(os.homedir(), ".zshrc"), path.join(os.homedir(), ".bashrc")];
|
|
485
|
+
const file = this.telemetryShellProfile();
|
|
486
|
+
if (file !== undefined && !files.includes(file))
|
|
487
|
+
files.unshift(file);
|
|
488
|
+
return files;
|
|
489
|
+
}
|
|
490
|
+
/* abbreviate the home directory of a file path as "~" */
|
|
491
|
+
telemetryFileDisplay(file) {
|
|
492
|
+
const home = os.homedir();
|
|
493
|
+
return file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file;
|
|
494
|
+
}
|
|
495
|
+
/* determine whether the telemetry variables of an "env" object point to the ASE ingress,
|
|
496
|
+
i.e., whether its "OTEL_EXPORTER_OTLP_ENDPOINT" is the URL of the ASE ingress or its
|
|
497
|
+
"OTEL_EXPORTER_OTLP_HEADERS" carries the bearer token of the ASE ingress (the latter
|
|
498
|
+
still recognizing a configuration of a since changed "telemetry.service.port") */
|
|
499
|
+
telemetryClaudeIsOwned(env) {
|
|
500
|
+
const value = (name) => {
|
|
501
|
+
const val = this.settingsFindMember(env, name)?.query("/ string [ pos() == 2 ]")[0];
|
|
502
|
+
return val !== undefined ? String(val.get("value") ?? "") : undefined;
|
|
503
|
+
};
|
|
504
|
+
const { url, token } = telemetryEndpoint(this.log, false);
|
|
505
|
+
const ours = this.telemetryClaudeEnv(url, token);
|
|
506
|
+
return value("OTEL_EXPORTER_OTLP_ENDPOINT") === ours.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
507
|
+
|| (token !== "" && value("OTEL_EXPORTER_OTLP_HEADERS") === ours.OTEL_EXPORTER_OTLP_HEADERS);
|
|
508
|
+
}
|
|
509
|
+
/* re-normalize the whitespace tokens of a nested object at nesting
|
|
510
|
+
level "depth" to the canonical 4-space indentation, with "trailing"
|
|
511
|
+
being the whitespace following its closing brace (a scalar last
|
|
512
|
+
value leaves the pre-"}" newline to the object epilog, a container
|
|
513
|
+
last value carries it in its own epilog) */
|
|
514
|
+
settingsNormalizeObject(obj, depth, trailing) {
|
|
515
|
+
const indent = (level) => " ".repeat(4 * level);
|
|
516
|
+
const members = obj.query("/ member");
|
|
517
|
+
if (members.length === 0) {
|
|
518
|
+
obj.set({ prolog: "{", epilog: `}${trailing}` });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
let scalar = true;
|
|
522
|
+
for (let i = 0; i < members.length; i++) {
|
|
523
|
+
const last = i === members.length - 1;
|
|
524
|
+
members[i].set({ epilog: last ? undefined : `,\n${indent(depth + 1)}` });
|
|
525
|
+
const val = members[i].query("/ * [ pos() == 2 ]")[0];
|
|
526
|
+
if (val === undefined)
|
|
527
|
+
continue;
|
|
528
|
+
const container = val.type() === "object" || val.type() === "array";
|
|
529
|
+
if (container) {
|
|
530
|
+
const ep = String(val.get("epilog") ?? "").replace(/\s+$/, "");
|
|
531
|
+
val.set({ epilog: last ? `${ep}\n${indent(depth)}` : ep });
|
|
532
|
+
}
|
|
533
|
+
else
|
|
534
|
+
val.set({ epilog: undefined });
|
|
535
|
+
if (last)
|
|
536
|
+
scalar = !container;
|
|
537
|
+
}
|
|
538
|
+
obj.set({
|
|
539
|
+
prolog: `{\n${indent(depth + 1)}`,
|
|
540
|
+
epilog: `${scalar ? `\n${indent(depth)}` : ""}}${trailing}`
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/* determine the whitespace following the closing brace of the value
|
|
544
|
+
of a top-level member, depending on whether it is the last member */
|
|
545
|
+
settingsTrailing(root, member) {
|
|
546
|
+
const members = root.query("/ member");
|
|
547
|
+
return members[members.length - 1] === member ? "\n" : "";
|
|
548
|
+
}
|
|
549
|
+
/* read a text file (undefined for a missing file) */
|
|
550
|
+
async textRead(file) {
|
|
551
|
+
try {
|
|
552
|
+
return await fs.readFile(file, "utf8");
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
if (err.code === "ENOENT")
|
|
556
|
+
return undefined;
|
|
557
|
+
throw err;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/* split a text into the lines before, within, and after the
|
|
561
|
+
ASE-generated marked block ("body" is undefined without a block) */
|
|
562
|
+
markedBlockSplit(text) {
|
|
563
|
+
const lines = text === "" ? [] : text.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
564
|
+
const begin = lines.findIndex((line) => line.trim() === this.telemetryMarkBegin);
|
|
565
|
+
if (begin < 0)
|
|
566
|
+
return { head: lines, body: undefined, tail: [] };
|
|
567
|
+
const end = lines.findIndex((line, i) => i > begin && line.trim() === this.telemetryMarkEnd);
|
|
568
|
+
if (end < 0)
|
|
569
|
+
throw new Error(`unterminated ASE-generated block (missing line "${this.telemetryMarkEnd}")`);
|
|
570
|
+
return { head: lines.slice(0, begin), body: lines.slice(begin + 1, end), tail: lines.slice(end + 1) };
|
|
571
|
+
}
|
|
572
|
+
/* maintain the ASE-generated marked block of a text file: with
|
|
573
|
+
"content", replace an existing block in place or append a new
|
|
574
|
+
one (separated by an empty line), without "content", remove the
|
|
575
|
+
block together with one adjacent empty separator line; the
|
|
576
|
+
optional "foreign" predicate, applied to the lines outside the
|
|
577
|
+
block, vetoes the operation */
|
|
578
|
+
async markedBlockUpdate(file, content, foreign) {
|
|
579
|
+
const text = await this.textRead(file);
|
|
580
|
+
if (text === undefined && content === undefined)
|
|
581
|
+
return "missing";
|
|
582
|
+
const { head, body, tail } = this.markedBlockSplit(text ?? "");
|
|
583
|
+
if (foreign !== undefined && foreign([...head, ...tail]))
|
|
584
|
+
return "foreign";
|
|
585
|
+
let lines;
|
|
586
|
+
let result;
|
|
587
|
+
if (content !== undefined) {
|
|
588
|
+
const block = [this.telemetryMarkBegin, ...content, this.telemetryMarkEnd];
|
|
589
|
+
if (body !== undefined) {
|
|
590
|
+
lines = [...head, ...block, ...tail];
|
|
591
|
+
result = "updated";
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const sep = head.length > 0 && head[head.length - 1].trim() !== "" ? [""] : [];
|
|
595
|
+
lines = [...head, ...sep, ...block];
|
|
596
|
+
result = "added";
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
if (body === undefined)
|
|
601
|
+
return "absent";
|
|
602
|
+
if (head.length > 0 && head[head.length - 1].trim() === "")
|
|
603
|
+
head.pop();
|
|
604
|
+
else if (tail.length > 0 && tail[0].trim() === "")
|
|
605
|
+
tail.shift();
|
|
606
|
+
lines = [...head, ...tail];
|
|
607
|
+
result = "removed";
|
|
608
|
+
}
|
|
609
|
+
await mkdirp(path.dirname(file));
|
|
610
|
+
await writeFileAtomic(file, lines.length > 0 ? lines.join("\n") + "\n" : "", { encoding: "utf8" });
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
/* handler for "ase setup telemetry activate" */
|
|
614
|
+
async doTelemetryActivate(tool) {
|
|
615
|
+
if (!telemetryEnabled(this.log))
|
|
616
|
+
throw new Error("AI usage accounting is disabled -- enable it first with " +
|
|
617
|
+
"\"ase config set telemetry.enabled true\" (on scope \"user\" or \"project\")");
|
|
618
|
+
const { url, token } = telemetryEndpoint(this.log, true);
|
|
619
|
+
if (tool === "claude")
|
|
620
|
+
return this.telemetryActivateClaude(url, token);
|
|
621
|
+
else if (tool === "codex")
|
|
622
|
+
return this.telemetryActivateCodex(url, token);
|
|
623
|
+
else
|
|
624
|
+
return this.telemetryActivateCopilot(url, token);
|
|
625
|
+
}
|
|
626
|
+
/* handler for "ase setup telemetry deactivate" */
|
|
627
|
+
async doTelemetryDeactivate(tool) {
|
|
628
|
+
if (tool === "claude")
|
|
629
|
+
return this.telemetryDeactivateClaude();
|
|
630
|
+
else if (tool === "codex")
|
|
631
|
+
return this.telemetryDeactivateBlock(this.telemetryCodexFile());
|
|
632
|
+
else {
|
|
633
|
+
for (const file of this.telemetryShellProfiles())
|
|
634
|
+
await this.telemetryDeactivateBlock(file);
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/* activate the telemetry of the Anthropic Claude Code CLI through
|
|
639
|
+
the "env" member of its user-level settings file */
|
|
640
|
+
async telemetryActivateClaude(url, token) {
|
|
641
|
+
const file = this.settingsFile("claude", "user");
|
|
642
|
+
const root = await this.settingsReadAst(file);
|
|
643
|
+
/* reject a non-object "env" member */
|
|
644
|
+
let member = this.settingsFindMember(root, "env");
|
|
645
|
+
let env = member?.query("/ * [ pos() == 2 ]")[0];
|
|
646
|
+
if (member !== undefined && (env === undefined || env.type() !== "object")) {
|
|
647
|
+
this.log.write("warning", "setup: telemetry: activate: a non-object \"env\" " +
|
|
648
|
+
`is present in ${file}: preserving it (skipped)`);
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
/* preserve a foreign, hand-crafted telemetry configuration (any of the variables
|
|
652
|
+
not pointing to the ASE ingress), as deactivation removes all variables: skip and warn */
|
|
653
|
+
const present = env !== undefined ? this.telemetryClaudePresent(env) : [];
|
|
654
|
+
if (env !== undefined && present.length > 0 && !this.telemetryClaudeIsOwned(env)) {
|
|
655
|
+
this.log.write("warning", "setup: telemetry: activate: a non-ASE telemetry configuration " +
|
|
656
|
+
`(${present.map((name) => `"${name}"`).join(", ")}) is already present in ${file}: ` +
|
|
657
|
+
"preserving it (skipped)");
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
/* insert a fresh (still empty) "env" member */
|
|
661
|
+
if (member === undefined || env === undefined) {
|
|
662
|
+
env = root.create("object").set({ prolog: "{", epilog: "}" });
|
|
663
|
+
member = root.create("member").add(root.create("string").set({ body: JSON.stringify("env"), value: "env", epilog: ": " }), env);
|
|
664
|
+
this.settingsAddMember(root, member, false);
|
|
665
|
+
}
|
|
666
|
+
/* add missing and replace existing variables in place */
|
|
667
|
+
for (const [name, value] of Object.entries(this.telemetryClaudeEnv(url, token))) {
|
|
668
|
+
const val = root.create("string").set({ body: JSON.stringify(value), value });
|
|
669
|
+
const existing = this.settingsFindMember(env, name);
|
|
670
|
+
if (existing !== undefined) {
|
|
671
|
+
const old = existing.query("/ * [ pos() == 2 ]")[0];
|
|
672
|
+
if (old !== undefined)
|
|
673
|
+
existing.del(old);
|
|
674
|
+
existing.add(val);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
const key = root.create("string").set({ body: JSON.stringify(name), value: name, epilog: ": " });
|
|
678
|
+
env.add(root.create("member").add(key, val));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this.settingsNormalizeObject(env, 1, this.settingsTrailing(root, member));
|
|
682
|
+
await this.settingsWriteAst(file, root);
|
|
683
|
+
this.log.write("info", `setup: telemetry: activate: ${present.length > 0 ? "updating" : "adding"} ` +
|
|
684
|
+
`ASE telemetry in "env" of ${file}`);
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
/* deactivate the telemetry of the Anthropic Claude Code CLI */
|
|
688
|
+
async telemetryDeactivateClaude() {
|
|
689
|
+
const file = this.settingsFile("claude", "user");
|
|
690
|
+
/* a missing settings file means nothing to remove */
|
|
691
|
+
try {
|
|
692
|
+
await fs.access(file);
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
this.log.write("info", `setup: telemetry: deactivate: no settings file ${file} (skipped)`);
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
/* locate the ASE telemetry configuration */
|
|
699
|
+
const root = await this.settingsReadAst(file);
|
|
700
|
+
const member = this.settingsFindMember(root, "env");
|
|
701
|
+
const env = member?.query("/ * [ pos() == 2 ]")[0];
|
|
702
|
+
const present = env !== undefined && env.type() === "object" ? this.telemetryClaudePresent(env) : [];
|
|
703
|
+
if (member === undefined || env === undefined || present.length === 0) {
|
|
704
|
+
this.log.write("info", `setup: telemetry: deactivate: no telemetry in "env" of ${file} (skipped)`);
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
/* preserve a foreign, hand-crafted telemetry configuration: skip and warn */
|
|
708
|
+
if (!this.telemetryClaudeIsOwned(env)) {
|
|
709
|
+
this.log.write("warning", "setup: telemetry: deactivate: a non-ASE telemetry configuration " +
|
|
710
|
+
`(${present.map((name) => `"${name}"`).join(", ")}) is present in ${file}: ` +
|
|
711
|
+
"preserving it (skipped)");
|
|
712
|
+
return 0;
|
|
713
|
+
}
|
|
714
|
+
/* remove the variables and, if it became empty, the "env" member */
|
|
715
|
+
for (const name of Object.keys(this.telemetryClaudeEnv("", ""))) {
|
|
716
|
+
const target = this.settingsFindMember(env, name);
|
|
717
|
+
if (target !== undefined)
|
|
718
|
+
env.del(target);
|
|
719
|
+
}
|
|
720
|
+
if (env.query("/ member").length === 0)
|
|
721
|
+
this.settingsRemoveMember(root, member);
|
|
722
|
+
else
|
|
723
|
+
this.settingsNormalizeObject(env, 1, this.settingsTrailing(root, member));
|
|
724
|
+
await this.settingsWriteAst(file, root);
|
|
725
|
+
this.log.write("info", `setup: telemetry: deactivate: removing ASE telemetry from "env" of ${file}`);
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
/* activate the telemetry of the OpenAI Codex CLI through the
|
|
729
|
+
ASE-generated block in its configuration file */
|
|
730
|
+
async telemetryActivateCodex(url, token) {
|
|
731
|
+
const file = this.telemetryCodexFile();
|
|
732
|
+
const result = await this.markedBlockUpdate(file, this.telemetryCodexConf(url, token), (lines) => lines.some((line) => this.telemetryCodexHeader.test(line)));
|
|
733
|
+
if (result === "foreign") {
|
|
734
|
+
this.log.write("warning", "setup: telemetry: activate: a non-ASE \"[otel]\" configuration " +
|
|
735
|
+
`is already present in ${file}: preserving it (skipped)`);
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
this.log.write("info", `setup: telemetry: activate: ${result === "updated" ? "updating" : "adding"} ` +
|
|
739
|
+
`ASE telemetry block in ${file}`);
|
|
740
|
+
return 0;
|
|
741
|
+
}
|
|
742
|
+
/* activate the telemetry of the GitHub Copilot CLI through the
|
|
743
|
+
ASE-generated block of a "copilot" shell function in the shell profile */
|
|
744
|
+
async telemetryActivateCopilot(url, token) {
|
|
745
|
+
const lines = this.telemetryCopilotFunc(url, token);
|
|
746
|
+
const file = this.telemetryShellProfile();
|
|
747
|
+
if (file === undefined)
|
|
748
|
+
throw new Error("telemetry configuration for --tool copilot supports only the shell profiles " +
|
|
749
|
+
`of "zsh" and "bash" (current shell: "${process.env.SHELL ?? ""}"): ` +
|
|
750
|
+
`please define the following shell function manually:\n${lines.join("\n")}`);
|
|
751
|
+
/* preserve a foreign, hand-crafted telemetry configuration: skip and warn */
|
|
752
|
+
const endpoint = this.telemetryCopilotForeignEnv();
|
|
753
|
+
if (endpoint !== undefined) {
|
|
754
|
+
this.log.write("warning", "setup: telemetry: activate: a non-ASE \"OTEL_EXPORTER_OTLP_ENDPOINT\" " +
|
|
755
|
+
`("${endpoint}") is already present in the environment: preserving it (skipped)`);
|
|
756
|
+
return 0;
|
|
757
|
+
}
|
|
758
|
+
const result = await this.markedBlockUpdate(file, lines, (rest) => rest.some((line) => this.telemetryCopilotAssign.test(line)));
|
|
759
|
+
if (result === "foreign") {
|
|
760
|
+
this.log.write("warning", "setup: telemetry: activate: a non-ASE \"COPILOT_OTEL_ENABLED\" or " +
|
|
761
|
+
`"OTEL_EXPORTER_OTLP_ENDPOINT" is already present in ${file}: preserving it (skipped)`);
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
this.log.write("info", `setup: telemetry: activate: ${result === "updated" ? "updating" : "adding"} ` +
|
|
765
|
+
`ASE telemetry block in ${file}`);
|
|
766
|
+
this.log.write("info", "setup: telemetry: activate: a new shell session is required " +
|
|
767
|
+
`for the "copilot" shell function of ${toolSpecs.copilot.label} to take effect`);
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
/* deactivate the telemetry of the OpenAI Codex CLI or GitHub Copilot
|
|
771
|
+
CLI by removing the ASE-generated block from a file */
|
|
772
|
+
async telemetryDeactivateBlock(file) {
|
|
773
|
+
const result = await this.markedBlockUpdate(file);
|
|
774
|
+
if (result === "missing")
|
|
775
|
+
this.log.write("info", `setup: telemetry: deactivate: no file ${file} (skipped)`);
|
|
776
|
+
else if (result === "absent")
|
|
777
|
+
this.log.write("info", `setup: telemetry: deactivate: no ASE telemetry block in ${file} (skipped)`);
|
|
778
|
+
else
|
|
779
|
+
this.log.write("info", `setup: telemetry: deactivate: removing ASE telemetry block from ${file}`);
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
/* probe the ASE telemetry configurations of a tool */
|
|
783
|
+
async telemetryStatus(tool) {
|
|
784
|
+
const entries = [];
|
|
785
|
+
if (tool === "claude") {
|
|
786
|
+
const file = this.settingsFile("claude", "user");
|
|
787
|
+
let root;
|
|
788
|
+
try {
|
|
789
|
+
root = await this.settingsReadAst(file);
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
/* an unparsable settings file carries no usable state */
|
|
793
|
+
return entries;
|
|
794
|
+
}
|
|
795
|
+
const env = this.settingsFindMember(root, "env")?.query("/ * [ pos() == 2 ]")[0];
|
|
796
|
+
const present = env !== undefined && env.type() === "object" ? this.telemetryClaudePresent(env) : [];
|
|
797
|
+
if (env !== undefined && present.length > 0)
|
|
798
|
+
entries.push({
|
|
799
|
+
file: this.telemetryFileDisplay(file),
|
|
800
|
+
scope: "user",
|
|
801
|
+
status: this.telemetryClaudeIsOwned(env) ? "activated" : "foreign"
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
const files = tool === "codex" ? [this.telemetryCodexFile()] : this.telemetryShellProfiles();
|
|
806
|
+
for (const file of files) {
|
|
807
|
+
let block;
|
|
808
|
+
try {
|
|
809
|
+
const text = await this.textRead(file);
|
|
810
|
+
if (text === undefined)
|
|
811
|
+
continue;
|
|
812
|
+
block = this.markedBlockSplit(text);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
/* an unreadable file or malformed block carries no usable state */
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const foreign = tool === "codex" ? this.telemetryCodexHeader : this.telemetryCopilotAssign;
|
|
819
|
+
let status = "";
|
|
820
|
+
if (block.body !== undefined)
|
|
821
|
+
status = "activated";
|
|
822
|
+
else if ([...block.head, ...block.tail].some((line) => foreign.test(line)))
|
|
823
|
+
status = "foreign";
|
|
824
|
+
if (status !== "")
|
|
825
|
+
entries.push({ file: this.telemetryFileDisplay(file), scope: "(n/a)", status });
|
|
826
|
+
}
|
|
827
|
+
if (tool === "copilot" && this.telemetryCopilotForeignEnv() !== undefined)
|
|
828
|
+
entries.push({ file: "$OTEL_EXPORTER_OTLP_ENDPOINT", scope: "(n/a)", status: "foreign" });
|
|
829
|
+
}
|
|
830
|
+
return entries;
|
|
831
|
+
}
|
|
832
|
+
/* determine whether the ASE telemetry configuration of a tool is activated */
|
|
833
|
+
async telemetryActive(tool) {
|
|
834
|
+
const entries = await this.telemetryStatus(tool);
|
|
835
|
+
return entries.some((entry) => entry.status === "activated");
|
|
836
|
+
}
|
|
837
|
+
}
|