@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.
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 +357 -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} +8 -1
  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 +269 -81
  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 +231 -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 +81 -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,509 @@
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 os from "node:os";
7
+ import { ofetch } from "ofetch";
8
+ import { Agent } from "undici";
9
+ import * as v from "valibot";
10
+ import { WebSocket } from "ws";
11
+ import { isScalar } from "yaml";
12
+ import { Config } from "./ase-config-core.js";
13
+ import { configSchema } from "./ase-config-schema.js";
14
+ import { parseScope } from "./ase-config-scope.js";
15
+ import * as TaskFormat from "./ase-task-format.js";
16
+ import { urlHost, isLoopback } from "./ase-lib-server-bind.js";
17
+ import { storeSchema } from "./ase-task-store-server-cli.js";
18
+ import { relaySchema } from "./ase-agent-relay-server-cli.js";
19
+ import * as Relay from "./ase-agent-relay-core.js";
20
+ /* redact an agent relay URL for its use in messages: its user information, its
21
+ path (as it potentially embeds the token), and its query (unless it is the
22
+ well-known "?insecure"), so that no token ever lands in the logs */
23
+ export const redactURL = (url) => {
24
+ const m = /^([^:/?#]+:\/\/)?([^/?#]*)([^?#]*)(.*)$/s.exec(url);
25
+ if (m === null)
26
+ return "***";
27
+ const [, scheme = "", authority, path, rest] = m;
28
+ return scheme +
29
+ authority.replace(/^[^@]*@/, "***@") +
30
+ (path === "" || path === "/" ? path : "/***") +
31
+ (rest === "" || rest === "?insecure" ? rest : `${rest[0]}***`);
32
+ };
33
+ /* resolve the bearer token of the agent relay: the token embedded in the URL,
34
+ else $ASE_AGENT_RELAY_TOKEN, else "project.relay.token", else the token of the
35
+ locally started agent relay server (or of the locally started task store server
36
+ co-hosting the agent relay), but only if the URL addresses exactly this server;
37
+ a token potentially committed or redirected is warned about */
38
+ const relayToken = (log, relay, token, embedded, url) => {
39
+ const repoScoped = relay.scope === "project" || relay.scope.startsWith("task:");
40
+ if (embedded !== undefined) {
41
+ if (repoScoped)
42
+ log.write("warning", `agent: token embedded in "project.relay.url" on scope "${relay.scope}" ` +
43
+ "might be committed -- use $ASE_AGENT_RELAY_TOKEN or \"project.relay.token\" on scope \"user\" instead");
44
+ return embedded;
45
+ }
46
+ /* warn about a user token being sent to a non-loopback host selected on
47
+ a repository-supplied scope, as a cloned repository could redirect it this way */
48
+ const redirected = () => {
49
+ if (repoScoped && !isLoopback(url.hostname))
50
+ log.write("warning", `agent: sending token to non-loopback host "${url.host}" selected by ` +
51
+ `"project.relay.url" on scope "${relay.scope}" -- verify you trust this host` +
52
+ (url.protocol === "ase:" ? " (token is transmitted in plaintext)" : ""));
53
+ };
54
+ const env = process.env.ASE_AGENT_RELAY_TOKEN;
55
+ if (env !== undefined && env !== "") {
56
+ redirected();
57
+ return env;
58
+ }
59
+ if (token.value !== "") {
60
+ if (token.scope !== "user")
61
+ log.write("warning", `agent: "project.relay.token" found on scope "${token.scope}" ` +
62
+ "-- configure it on scope \"user\" only");
63
+ redirected();
64
+ return token.value;
65
+ }
66
+ /* fall back to the token of the locally started agent relay server, or of the
67
+ locally started task store server co-hosting it, but only if the URL addresses exactly this server */
68
+ const servers = [
69
+ { name: "relay", cfg: new Config("relay", relaySchema, log, parseScope("user")) },
70
+ { name: "store", cfg: new Config("store", storeSchema, log, parseScope("user")) }
71
+ ];
72
+ for (const { name, cfg } of servers) {
73
+ cfg.read();
74
+ const scalar = (key) => {
75
+ const val = cfg.get(key);
76
+ return val === undefined ? "" : String(isScalar(val) ? val.value : val);
77
+ };
78
+ if (name === "store" && scalar("relay") !== "true")
79
+ continue;
80
+ const tok = scalar("token");
81
+ const address = scalar("address") || "127.0.0.1";
82
+ const wildcard = address === "0.0.0.0" || address === "::";
83
+ const host = urlHost(address).toLowerCase();
84
+ const loopback = isLoopback(url.hostname);
85
+ if (tok !== "" && scalar("port") === url.port
86
+ && (url.hostname.toLowerCase() === host || (loopback && (wildcard || isLoopback(host)))))
87
+ return tok;
88
+ }
89
+ throw new Error(`agent: no token for agent relay "${redactURL(relay.value)}" ` +
90
+ "(set $ASE_AGENT_RELAY_TOKEN, \"project.relay.token\" on scope \"user\", or \"/<token>\" in the URL)");
91
+ };
92
+ /* parse an agent relay URL "ase://<addr>:<port>[/<token>]" (selecting an agent relay
93
+ server via HTTP) or "ases://<addr>:<port>[/<token>][?insecure]" (selecting it via
94
+ HTTPS, optionally without certificate verification) */
95
+ export const parseRelayURL = (value) => {
96
+ /* parse via WHATWG URL, as it supports bracketed IPv6 addresses (e.g. "[::1]")
97
+ and retains the brackets in "hostname" for direct reuse in the HTTP URL */
98
+ const unsupported = () => new Error(`agent: unsupported agent relay URL "${redactURL(value)}" ` +
99
+ "(expected: \"ase://<addr>:<port>[/<token>]\" or \"ases://<addr>:<port>[/<token>][?insecure]\")");
100
+ const m = /^(ases?):\/\//.exec(value);
101
+ if (m === null)
102
+ throw unsupported();
103
+ const secure = m[1] === "ases";
104
+ let url;
105
+ try {
106
+ url = new URL(value);
107
+ }
108
+ catch (_err) {
109
+ throw unsupported();
110
+ }
111
+ let p;
112
+ if (url.hostname === "" || url.port === "" || url.username !== "" || url.password !== ""
113
+ || url.hash !== "" || (url.search !== "" && !(secure && url.search === "?insecure"))
114
+ || (p = /^(?:\/([^/]+))?$/.exec(url.pathname)) === null)
115
+ throw unsupported();
116
+ let embedded;
117
+ try {
118
+ embedded = p[1] !== undefined ? decodeURIComponent(p[1]) : undefined;
119
+ }
120
+ catch (_err) {
121
+ throw unsupported();
122
+ }
123
+ return { url, secure, embedded, insecure: url.search === "?insecure" };
124
+ };
125
+ /* resolve the agent relay of the project: the URL (see "parseRelayURL") is taken from
126
+ the given override (the "--agent-relay" option), else from "project.relay.url",
127
+ where no URL at all (null) disables the passing of the presence of the agents */
128
+ export const relaySpec = (log, override) => {
129
+ const cfg = new Config("config", configSchema, log);
130
+ cfg.read();
131
+ const scoped = (key) => {
132
+ const entry = cfg.getScoped(key);
133
+ if (entry === undefined)
134
+ return { value: "", scope: "" };
135
+ return {
136
+ value: String(isScalar(entry.value) ? entry.value.value : entry.value),
137
+ scope: Config.scopeLabel(entry.scope)
138
+ };
139
+ };
140
+ const relay = override !== undefined && override !== "" ? { value: override, scope: "option" } : scoped("project.relay.url");
141
+ if (relay.value === "")
142
+ return null;
143
+ const { url, secure, embedded, insecure } = parseRelayURL(relay.value);
144
+ /* require an explicit project id, as a basename-derived one
145
+ likely collides with unrelated projects on a shared server */
146
+ const prjId = scoped("project.id").value;
147
+ if (prjId === "")
148
+ throw new Error("agent: agent relay requires an explicit \"project.id\" " +
149
+ "(set it via \"ase config --scope project set project.id <id>\")");
150
+ if (!TaskFormat.ID_RE.test(prjId))
151
+ throw new Error(`agent: configured "project.id" "${prjId}" must match [A-Za-z0-9_-]+`);
152
+ return {
153
+ prjId,
154
+ base: `${secure ? "https" : "http"}://${url.hostname}:${url.port}`,
155
+ token: relayToken(log, relay, scoped("project.relay.token"), embedded, url),
156
+ insecure
157
+ };
158
+ };
159
+ /* an error of the agent relay: its HTTP status (or zero for an unreachable agent relay) */
160
+ export class AgentRelayError extends Error {
161
+ status;
162
+ constructor(message, status = 0, options) {
163
+ super(message, options);
164
+ this.status = status;
165
+ }
166
+ }
167
+ /* the interval of the keepalive pings of the subscription (where a connection without
168
+ any traffic for this interval is pinged and without any traffic for twice this interval
169
+ is terminated), the timeout of its WebSocket handshake, and the minimum and maximum
170
+ delay of its reconnection (doubled on each failed connection attempt in a row) */
171
+ const KEEPALIVE_MS = 15 * 1000;
172
+ const HANDSHAKE_MS = 10 * 1000;
173
+ const RECONNECT_MIN_MS = 2 * 1000;
174
+ const RECONNECT_MAX_MS = 60 * 1000;
175
+ /* the client of the agent relay: the announcing and removal of an agent,
176
+ the listing of the agents, and the subscription to their changes */
177
+ export class AgentRelayClient {
178
+ log;
179
+ spec;
180
+ dispatcher;
181
+ constructor(log, spec) {
182
+ this.log = log;
183
+ this.spec = spec;
184
+ this.dispatcher = spec.insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined;
185
+ }
186
+ /* the base URL of the agent relay */
187
+ get base() {
188
+ return this.spec.base;
189
+ }
190
+ /* perform a request, raising an error response or a failed connection */
191
+ async request(method, url, body) {
192
+ const r = await ofetch.raw(`${this.spec.base}/projects/${this.spec.prjId}/agents${url}`, {
193
+ method,
194
+ headers: {
195
+ Authorization: `Bearer ${this.spec.token}`,
196
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {})
197
+ },
198
+ body: body !== undefined ? JSON.stringify(body) : undefined,
199
+ dispatcher: this.dispatcher,
200
+ signal: AbortSignal.timeout(5000),
201
+ ignoreResponseError: true
202
+ }).catch((err) => {
203
+ let cause = err;
204
+ while (cause instanceof Error && cause.cause instanceof Error)
205
+ cause = cause.cause;
206
+ throw new AgentRelayError(`agent relay "${this.spec.base}" unreachable: ${cause instanceof Error ? cause.message : String(cause)}`, 0, { cause: err });
207
+ });
208
+ if (r.status < 200 || r.status >= 300) {
209
+ const d = r._data;
210
+ throw new AgentRelayError(`agent relay "${this.spec.base}": ` +
211
+ `${d?.detail ?? d?.title ?? "request failed"} (HTTP ${r.status})`, r.status);
212
+ }
213
+ return (r._data ?? {});
214
+ }
215
+ /* announce the presence of an agent, remove it, and list all agents */
216
+ async announce(id, presence) {
217
+ await this.request("PUT", `/${id}`, presence);
218
+ }
219
+ async remove(id) {
220
+ await this.request("DELETE", `/${id}`);
221
+ }
222
+ async list() {
223
+ return (await this.request("GET", "")).agents;
224
+ }
225
+ /* subscribe to the agents of the project via the WebSocket event endpoint,
226
+ reconnecting after a connection loss, a failed or timed out handshake, or
227
+ a connection without any traffic despite keepalive pings (e.g. a half-open one
228
+ after a network partition), with a growing delay while the connection attempts
229
+ keep failing, where a handshake rejected by the agent relay (e.g. for an invalid
230
+ token) is warned about once per reason, where the agents are unknown while disconnected,
231
+ reporting the agents on every change and each change of the connection state
232
+ (initially disconnected); returns a function to unsubscribe */
233
+ subscribe(onAgents, onState) {
234
+ const url = `${this.spec.base.replace(/^http/, "ws")}/projects/${this.spec.prjId}/agents/events`;
235
+ const agents = new Map();
236
+ let ws = null;
237
+ let timer = null;
238
+ let stopped = false;
239
+ let delay = RECONNECT_MIN_MS;
240
+ let rejected = "";
241
+ const connect = () => {
242
+ const sock = new WebSocket(url, {
243
+ headers: { Authorization: `Bearer ${this.spec.token}` },
244
+ rejectUnauthorized: !this.spec.insecure,
245
+ handshakeTimeout: HANDSHAKE_MS
246
+ });
247
+ ws = sock;
248
+ /* keep the connection alive: record any traffic (the frames, and the pings
249
+ of the agent relay and the pongs of our own pings), and terminate the
250
+ connection if there was none since the previous (then unanswered) ping */
251
+ let traffic = true;
252
+ let watchdog = null;
253
+ const alive = () => { traffic = true; };
254
+ sock.on("ping", alive);
255
+ sock.on("pong", alive);
256
+ sock.on("open", () => {
257
+ watchdog = setInterval(() => {
258
+ if (!traffic) {
259
+ this.log.write("debug", `agent relay "${this.spec.base}": events: connection unresponsive -- reconnecting`);
260
+ sock.terminate();
261
+ return;
262
+ }
263
+ traffic = false;
264
+ sock.ping();
265
+ }, KEEPALIVE_MS);
266
+ delay = RECONNECT_MIN_MS;
267
+ rejected = "";
268
+ onState?.(true);
269
+ });
270
+ /* apply the validated event frames to the agents, where an invalid
271
+ frame is warned about (once per connection) and ignored */
272
+ let warned = false;
273
+ sock.on("message", (data) => {
274
+ traffic = true;
275
+ try {
276
+ let frame;
277
+ try {
278
+ const result = v.safeParse(Relay.frameSchema, JSON.parse(String(data)));
279
+ if (!result.success)
280
+ throw new Error(`${result.issues[0].message}` +
281
+ (result.issues[0].path !== undefined ? ` (at "${v.getDotPath(result.issues[0]) ?? ""}")` : ""));
282
+ frame = result.output;
283
+ }
284
+ catch (err) {
285
+ this.log.write(warned ? "debug" : "warning", `agent relay "${this.spec.base}": events: ` +
286
+ `invalid event frame ignored: ${err instanceof Error ? err.message : String(err)}`);
287
+ warned = true;
288
+ return;
289
+ }
290
+ if (frame.reset === true)
291
+ agents.clear();
292
+ for (const [id, agent] of Object.entries(frame.agents ?? {}))
293
+ agents.set(id, { ...agent, recent: Relay.appendLines(agents.get(id)?.recent ?? [], agent.recent) });
294
+ for (const id of frame.removed ?? [])
295
+ agents.delete(id);
296
+ onAgents([...agents.values()]);
297
+ }
298
+ catch (err) {
299
+ /* never let a failure escape the listener (and crash the process) */
300
+ this.log.write("warning", `agent relay "${this.spec.base}": events: ` +
301
+ `failed to process event frame: ${err instanceof Error ? err.message : String(err)}`);
302
+ }
303
+ });
304
+ sock.on("error", (err) => {
305
+ const rejection = /^Unexpected server response: 4\d\d$/.test(err.message);
306
+ this.log.write(rejection && err.message !== rejected ? "warning" : "debug", `agent relay "${this.spec.base}": events: ${err.message}`);
307
+ if (rejection)
308
+ rejected = err.message;
309
+ });
310
+ sock.on("close", () => {
311
+ if (watchdog !== null)
312
+ clearInterval(watchdog);
313
+ ws = null;
314
+ if (!stopped) {
315
+ if (agents.size > 0) {
316
+ agents.clear();
317
+ onAgents([]);
318
+ }
319
+ onState?.(false);
320
+ timer = setTimeout(connect, delay);
321
+ delay = Math.min(delay * 2, RECONNECT_MAX_MS);
322
+ }
323
+ });
324
+ };
325
+ connect();
326
+ return () => {
327
+ stopped = true;
328
+ if (timer !== null)
329
+ clearTimeout(timer);
330
+ ws?.close();
331
+ };
332
+ }
333
+ }
334
+ /* the interval of the heartbeat, the coalescing delay of the processing log lines,
335
+ the minimum and maximum back-off delay after a rejection, and the
336
+ maximum duration of the removal of the presence on stop */
337
+ const HEARTBEAT_MS = 10 * 1000;
338
+ const COALESCE_MS = 250;
339
+ const BACKOFF_MIN_MS = 60 * 1000;
340
+ const BACKOFF_MAX_MS = 15 * 60 * 1000;
341
+ const STOP_MS = 2 * 1000;
342
+ /* the user name and id of the agent process (with an unknown user id, e.g. on Windows, as -1) */
343
+ const processUser = () => {
344
+ try {
345
+ const info = os.userInfo();
346
+ return { name: info.username, uid: info.uid };
347
+ }
348
+ catch (_err) {
349
+ return { name: process.env.USER ?? process.env.USERNAME ?? "", uid: process.getuid?.() ?? -1 };
350
+ }
351
+ };
352
+ /* the presence of a running agent, announced to the agent relay: immediately
353
+ on every change of its state, tasks, and usage, coalesced on new processing log lines,
354
+ and periodically as its heartbeat, each time with only the processing log lines
355
+ added since the previous successful announcement (kept until announced, bounded
356
+ while the agent relay is unreachable), where an unreachable agent relay is
357
+ warned about once per outage only and never disturbs the agent itself, and where
358
+ a rejected announcement (a client error other than a timeout) backs off exponentially,
359
+ and a permanently rejected one (also other than an authentication failure or a
360
+ rate limit) additionally drops its lines */
361
+ export class AgentPresenceReporter {
362
+ log;
363
+ client;
364
+ kind;
365
+ name;
366
+ mode;
367
+ request;
368
+ prompt;
369
+ states;
370
+ title;
371
+ description;
372
+ id;
373
+ started = Date.now();
374
+ cwd = process.cwd();
375
+ user = processUser();
376
+ state = "waiting";
377
+ tasks = [];
378
+ usage = null;
379
+ lines = [];
380
+ beat = null;
381
+ delay = null;
382
+ flight = null;
383
+ again = false;
384
+ failing = false;
385
+ reason = "";
386
+ backoff = 0;
387
+ resume = 0;
388
+ stopped = false;
389
+ constructor(log, client, kind, name, mode = "", request = "", prompt = "required", states, title = "", description = "") {
390
+ this.log = log;
391
+ this.client = client;
392
+ this.kind = kind;
393
+ this.name = name;
394
+ this.mode = mode;
395
+ this.request = request;
396
+ this.prompt = prompt;
397
+ this.states = states;
398
+ this.title = title;
399
+ this.description = description;
400
+ this.id = `${os.hostname()}-${process.pid}-${name}`
401
+ .replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 128);
402
+ }
403
+ /* start announcing the agent (initially waiting) */
404
+ start() {
405
+ this.beat = setInterval(() => { this.send(); }, HEARTBEAT_MS);
406
+ this.beat.unref();
407
+ this.send();
408
+ }
409
+ /* update the tasks the agent is busy with (and hence its state) */
410
+ busy(tasks) {
411
+ this.tasks = [...tasks].sort(TaskFormat.compareIds);
412
+ this.state = this.tasks.length > 0 ? "busy" : "waiting";
413
+ this.send();
414
+ }
415
+ /* record the usage of the agent tool by a finished job (as the last one, and into the total) */
416
+ used(usage) {
417
+ this.usage = { total: this.usage !== null ? Relay.addUsage(this.usage.total, usage) : usage, last: usage };
418
+ this.send();
419
+ }
420
+ /* record a processing log line (of a task, or of the agent itself) */
421
+ line(level, task, msg) {
422
+ this.lines.push({ time: Date.now(), level, task, msg: msg.length > Relay.MSG_MAX ? `${msg.slice(0, Relay.MSG_MAX - 1)}…` : msg });
423
+ if (this.lines.length > Relay.LINES_MAX)
424
+ this.lines.splice(0, this.lines.length - Relay.LINES_MAX);
425
+ if (this.delay === null && !this.stopped)
426
+ this.delay = setTimeout(() => {
427
+ this.delay = null;
428
+ this.send();
429
+ }, COALESCE_MS);
430
+ }
431
+ /* announce the current presence, serialized with an ongoing announcement,
432
+ but not while backing off after a rejection */
433
+ send() {
434
+ if (this.stopped || Date.now() < this.resume)
435
+ return;
436
+ if (this.flight !== null) {
437
+ this.again = true;
438
+ return;
439
+ }
440
+ const lines = this.lines;
441
+ this.lines = [];
442
+ const presence = {
443
+ kind: this.kind, name: this.name, mode: this.mode, host: os.hostname(), pid: process.pid,
444
+ cwd: this.cwd, user: this.user.name, uid: this.user.uid, started: this.started,
445
+ state: this.state, tasks: this.tasks, usage: this.usage, lines
446
+ };
447
+ if (this.title !== "")
448
+ presence.title = this.title;
449
+ if (this.description !== "")
450
+ presence.description = this.description;
451
+ if (this.request !== "")
452
+ presence.request = this.request;
453
+ if (this.request !== "" && this.prompt !== "required")
454
+ presence.prompt = this.prompt;
455
+ if (this.states !== undefined)
456
+ presence.states = this.states;
457
+ this.flight = this.client.announce(this.id, presence).then(() => {
458
+ if (this.failing)
459
+ this.log.write("info", `agent: agent relay "${this.client.base}" accepting presence of agent again`);
460
+ this.failing = false;
461
+ this.reason = "";
462
+ this.backoff = 0;
463
+ this.resume = 0;
464
+ }).catch((err) => {
465
+ const status = err instanceof AgentRelayError ? err.status : 0;
466
+ const rejected = status >= 400 && status < 500 && status !== 408;
467
+ const permanent = rejected && status !== 401 && status !== 429;
468
+ const message = err instanceof Error ? err.message : String(err);
469
+ /* on a rejection (a client error other than a timeout), back off exponentially
470
+ (as a repetition is likely rejected again, e.g. by the limits of the agent relay
471
+ or for an invalid token), and on a permanent rejection (also other than an
472
+ authentication failure or a rate limit) drop the rejected lines (as they
473
+ potentially caused it), else keep the not announced lines (in front of the
474
+ meanwhile added ones) for the next announcement */
475
+ if (rejected) {
476
+ this.backoff = Math.min(Math.max(this.backoff * 2, BACKOFF_MIN_MS), BACKOFF_MAX_MS);
477
+ this.resume = Date.now() + this.backoff;
478
+ }
479
+ if (!permanent)
480
+ this.lines = [...lines, ...this.lines].slice(-Relay.LINES_MAX);
481
+ if (!this.failing || (rejected && message !== this.reason))
482
+ this.log.write("warning", `agent: ${message} ` + (rejected ?
483
+ `(presence of agent rejected -- retrying in ${Math.round(this.backoff / 1000)}s at the earliest)` :
484
+ "(presence of agent not passed until reachable again)"));
485
+ this.failing = true;
486
+ this.reason = message;
487
+ }).finally(() => {
488
+ this.flight = null;
489
+ if (this.again) {
490
+ this.again = false;
491
+ this.send();
492
+ }
493
+ });
494
+ }
495
+ /* stop announcing the agent and remove its presence, bounded in time, so an
496
+ unresponsive agent relay never delays the shutdown of the agent (a presence
497
+ not removed in time is swept by the agent relay as stale anyway) */
498
+ async stop() {
499
+ this.stopped = true;
500
+ if (this.beat !== null)
501
+ clearInterval(this.beat);
502
+ if (this.delay !== null)
503
+ clearTimeout(this.delay);
504
+ const remove = Promise.resolve(this.flight)
505
+ .then(() => this.client.remove(this.id))
506
+ .catch(() => { });
507
+ await Promise.race([remove, new Promise((resolve) => { setTimeout(resolve, STOP_MS).unref(); })]);
508
+ }
509
+ }