@rse/ase 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -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
+ }