@rse/ase 2.0.2 → 2.0.3

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 (135) hide show
  1. package/dst/ase-agent-definition-core.js +111 -87
  2. package/dst/ase-agent-fleet.js +1 -1
  3. package/dst/ase-agent-runtime-claim.js +5 -3
  4. package/dst/ase-agent-runtime-engine.js +1 -1
  5. package/dst/ase-agent-runtime-phase.js +1 -1
  6. package/dst/ase-agent-runtime-request.js +1 -1
  7. package/dst/ase-agent-runtime.js +3 -5
  8. package/dst/ase-artifact.js +3 -3
  9. package/dst/ase-config-core.js +2 -16
  10. package/dst/ase-config-schema.js +1 -3
  11. package/dst/ase-config.js +2 -0
  12. package/dst/ase-hook.js +12 -12
  13. package/dst/ase-lib-daemon-cli.js +117 -4
  14. package/dst/ase-lib-server-cli.js +4 -5
  15. package/dst/ase-mcp.js +4 -4
  16. package/dst/ase-service-getopt.js +2 -2
  17. package/dst/ase-service-otel.js +4 -4
  18. package/dst/ase-service-skills.js +5 -15
  19. package/dst/ase-service.js +87 -210
  20. package/dst/ase-setup-common.js +1 -2
  21. package/dst/ase-setup-settings.js +6 -9
  22. package/dst/ase-setup.js +2 -3
  23. package/dst/ase-statusline.js +170 -170
  24. package/dst/ase-task-board-flow.js +7 -6
  25. package/dst/ase-task-board-graph.js +5 -4
  26. package/dst/ase-task-board-tui-control.js +41 -35
  27. package/dst/ase-task-board-tui-model.js +185 -167
  28. package/dst/ase-task-board-tui-popup.js +2 -3
  29. package/dst/ase-task-board-tui-view.js +74 -70
  30. package/dst/ase-task-board-web-client.js +2 -2
  31. package/dst/ase-task-board-web-server.js +2 -2
  32. package/dst/ase-task-board.js +1 -1
  33. package/dst/ase-task-format.js +1 -1
  34. package/dst/ase-task-store-client.js +17 -5
  35. package/dst/ase-task-store-core.js +5 -7
  36. package/dst/ase-task-store-issues.js +16 -1
  37. package/dst/ase-task-store-plugin-ase.js +7 -3
  38. package/dst/ase-task-store-plugin-gitea.js +9 -32
  39. package/dst/ase-task-store-plugin-github.js +3 -12
  40. package/dst/ase-task-store-plugin-gitlab.js +2 -18
  41. package/dst/ase-task.js +51 -23
  42. package/dst/ase-util-compat.js +1 -1
  43. package/dst/ase-util-diagram.js +13 -37
  44. package/dst/ase.js +5 -5
  45. package/dst/man/ase-agent-adhoc.1 +1 -1
  46. package/dst/man/ase-agent-condenser.1 +1 -1
  47. package/dst/man/ase-agent-editor.1 +1 -1
  48. package/dst/man/ase-agent-estimator.1 +1 -1
  49. package/dst/man/ase-agent-fleet.1 +1 -1
  50. package/dst/man/ase-agent-implementer.1 +1 -1
  51. package/dst/man/ase-agent-integrator.1 +1 -1
  52. package/dst/man/ase-agent-qualifier.1 +1 -1
  53. package/dst/man/ase-agent-rebooter.1 +1 -1
  54. package/dst/man/ase-agent-relay.1 +1 -1
  55. package/dst/man/ase-agent-researcher.1 +1 -1
  56. package/dst/man/ase-agent.1 +1 -1
  57. package/dst/man/ase-artifact-list.1 +1 -1
  58. package/dst/man/ase-artifact-name.1 +1 -1
  59. package/dst/man/ase-artifact.1 +1 -1
  60. package/dst/man/ase-config-delete.1 +1 -1
  61. package/dst/man/ase-config-edit.1 +1 -1
  62. package/dst/man/ase-config-get.1 +1 -1
  63. package/dst/man/ase-config-init.1 +1 -1
  64. package/dst/man/ase-config-list.1 +1 -1
  65. package/dst/man/ase-config-set.1 +1 -1
  66. package/dst/man/ase-config.1 +1 -1
  67. package/dst/man/ase-hook-permission-request.1 +1 -1
  68. package/dst/man/ase-hook-pre-tool-use.1 +1 -1
  69. package/dst/man/ase-hook-session-end.1 +1 -1
  70. package/dst/man/ase-hook-session-persona.1 +1 -1
  71. package/dst/man/ase-hook-session-start.1 +1 -1
  72. package/dst/man/ase-hook-stop.1 +1 -1
  73. package/dst/man/ase-hook-user-prompt-submit.1 +1 -1
  74. package/dst/man/ase-hook.1 +1 -1
  75. package/dst/man/ase-man.1 +1 -1
  76. package/dst/man/ase-mcp.1 +1 -1
  77. package/dst/man/ase-service-log.1 +1 -1
  78. package/dst/man/ase-service-send.1 +1 -1
  79. package/dst/man/ase-service-start.1 +1 -1
  80. package/dst/man/ase-service-status.1 +1 -1
  81. package/dst/man/ase-service-stop.1 +1 -1
  82. package/dst/man/ase-service.1 +1 -1
  83. package/dst/man/ase-setup-disable.1 +1 -1
  84. package/dst/man/ase-setup-enable.1 +1 -1
  85. package/dst/man/ase-setup-install.1 +1 -1
  86. package/dst/man/ase-setup-mcp.1 +1 -1
  87. package/dst/man/ase-setup-status.1 +1 -1
  88. package/dst/man/ase-setup-statusline.1 +1 -1
  89. package/dst/man/ase-setup-telemetry.1 +1 -1
  90. package/dst/man/ase-setup-uninstall.1 +1 -1
  91. package/dst/man/ase-setup-update.1 +1 -1
  92. package/dst/man/ase-setup.1 +1 -1
  93. package/dst/man/ase-spec-export.1 +1 -1
  94. package/dst/man/ase-spec-lint.1 +1 -1
  95. package/dst/man/ase-spec-preview.1 +1 -1
  96. package/dst/man/ase-spec.1 +1 -1
  97. package/dst/man/ase-statusline.1 +1 -1
  98. package/dst/man/ase-task-assign.1 +1 -1
  99. package/dst/man/ase-task-board.1 +1 -1
  100. package/dst/man/ase-task-delete.1 +1 -1
  101. package/dst/man/ase-task-edit.1 +1 -1
  102. package/dst/man/ase-task-idscheme.1 +1 -1
  103. package/dst/man/ase-task-lifecycle.1 +1 -1
  104. package/dst/man/ase-task-list.1 +1 -1
  105. package/dst/man/ase-task-load.1 +1 -1
  106. package/dst/man/ase-task-newid.1 +1 -1
  107. package/dst/man/ase-task-purge.1 +1 -1
  108. package/dst/man/ase-task-rename.1 +1 -1
  109. package/dst/man/ase-task-request.1 +1 -1
  110. package/dst/man/ase-task-save.1 +1 -1
  111. package/dst/man/ase-task-status.1 +1 -1
  112. package/dst/man/ase-task-store.1 +1 -1
  113. package/dst/man/ase-task-view.1 +1 -1
  114. package/dst/man/ase-task.1 +1 -1
  115. package/dst/man/ase-util-compat.1 +1 -1
  116. package/dst/man/ase-util-diagram.1 +1 -1
  117. package/dst/man/ase-util-meta.1 +1 -1
  118. package/dst/man/ase-util-metric.1 +1 -1
  119. package/dst/man/ase-util-mint.1 +1 -1
  120. package/dst/man/ase-util-worktree.1 +1 -1
  121. package/dst/man/ase-util.1 +1 -1
  122. package/dst/man/ase.1 +1 -1
  123. package/package.json +1 -1
  124. package/plugin/.claude-plugin/plugin.json +1 -1
  125. package/plugin/.codex-plugin/plugin.json +1 -1
  126. package/plugin/.github/plugin/plugin.json +1 -1
  127. package/plugin/meta/ase-skill.md +7 -0
  128. package/plugin/mods/ase-mods.ts +10 -7
  129. package/plugin/skills/ase-code-analyze/SKILL.md +18 -10
  130. package/plugin/skills/ase-code-analyze/help.md +11 -6
  131. package/plugin/skills/ase-code-lint/SKILL.md +37 -23
  132. package/plugin/skills/ase-code-lint/help.md +10 -5
  133. package/plugin/skills/ase-help-intent/data.md +24 -12
  134. package/plugin/skills/ase-repo-review/SKILL.md +5 -3
  135. package/plugin/skills/ase-repo-review/help.md +3 -1
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
- import net from "node:net";
9
8
  import { fileURLToPath } from "node:url";
10
9
  import { spawn } from "node:child_process";
11
10
  import Hapi from "@hapi/hapi";
@@ -13,7 +12,6 @@ import { ofetch } from "ofetch";
13
12
  import { isMap } from "yaml";
14
13
  import prettyMs from "pretty-ms";
15
14
  import * as v from "valibot";
16
- import { Tail } from "tail";
17
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
16
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
19
17
  import { ConfigMCP } from "./ase-config.js";
@@ -21,6 +19,7 @@ import { Config } from "./ase-config-core.js";
21
19
  import { configSchema } from "./ase-config-schema.js";
22
20
  import { ensureAseGitignore } from "./ase-config-scope.js";
23
21
  import { isLogLevel } from "./ase-lib-log.js";
22
+ import { Daemon } from "./ase-lib-daemon-cli.js";
24
23
  import { CompatMCP } from "./ase-util-compat.js";
25
24
  import { DiagramMCP } from "./ase-util-diagram.js";
26
25
  import { Task, TaskMCP } from "./ase-task.js";
@@ -76,14 +75,7 @@ const PORT_ENV = "ASE_SERVICE_PORT";
76
75
  const LEVEL_ENV = "ASE_SERVICE_LOG_LEVEL";
77
76
  const IDLE_MS = 30 * 60 * 1000;
78
77
  const TICK_MS = 60 * 1000;
79
- const PORT_MIN = 42000;
80
- const PORT_MAX = 44000;
81
- const PORT_TRIES = 20;
82
- /* bounds for the append-only ".ase/service.log" file:
83
- maximum tolerated size, number of tail lines surviving a trim,
84
- and maximum length of the MCP tool call arguments logged per request */
85
- const LOG_MAX_SIZE = 1024 * 1024;
86
- const LOG_KEEP_LINES = 2000;
78
+ /* maximum length of the MCP tool call arguments logged per request */
87
79
  const LOG_ARGS_MAX = 200;
88
80
  /* load the optional "config.yaml" and "service.yaml" files and derive
89
81
  the service identity context (project id, port, preferred port, service config) */
@@ -105,33 +97,8 @@ export const loadServiceContext = (log) => {
105
97
  /* return context information */
106
98
  return { projectId, port, portPref, svc, aseDir };
107
99
  };
108
- /* reusable functionality: port allocation, persistence, and process spawning */
100
+ /* reusable functionality: port persistence and process spawning */
109
101
  export class Service {
110
- /* try binding a single candidate port to verify availability */
111
- static tryBind(port) {
112
- return new Promise((resolve) => {
113
- const s = net.createServer();
114
- s.once("error", () => {
115
- resolve(false);
116
- });
117
- s.once("listening", () => {
118
- s.close(() => resolve(true));
119
- });
120
- s.listen(port, HOST);
121
- });
122
- }
123
- /* allocate the preferred port, if given and available,
124
- else a fresh random port in PORT_MIN..PORT_MAX */
125
- static async allocatePort(pref = null) {
126
- if (pref !== null && await Service.tryBind(pref))
127
- return pref;
128
- for (let i = 0; i < PORT_TRIES; i++) {
129
- const p = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1));
130
- if (await Service.tryBind(p))
131
- return p;
132
- }
133
- throw new Error(`failed to allocate a port in ${PORT_MIN}..${PORT_MAX} after ${PORT_TRIES} attempts`);
134
- }
135
102
  /* persist an allocated port into ".ase/service.yaml" */
136
103
  static persistPort(svc, port) {
137
104
  ensureAseGitignore(path.dirname(svc.filename));
@@ -156,19 +123,6 @@ export class Service {
156
123
  svc.write();
157
124
  });
158
125
  }
159
- /* trim the log file down to its tail if it grew beyond the size limit */
160
- static trimLog(logFile) {
161
- try {
162
- if (!fs.existsSync(logFile) || fs.statSync(logFile).size <= LOG_MAX_SIZE)
163
- return;
164
- const tail = Service.readLogTail(logFile, LOG_KEEP_LINES);
165
- fs.writeFileSync(logFile, tail.length > 0 ? `${tail}\n` : "");
166
- }
167
- catch {
168
- /* intentionally ignore all trimming errors, as a
169
- non-trimmable log file must never block the service */
170
- }
171
- }
172
126
  /* spawn the current executable detached as a background service */
173
127
  static spawnDetached(aseDir, port, logLevel) {
174
128
  fs.mkdirSync(aseDir, { recursive: true });
@@ -177,7 +131,7 @@ export class Service {
177
131
  /* trim the log before handing it to the service, as the detached
178
132
  service inherits the file descriptor for its entire lifetime and
179
133
  hence cannot rotate the file itself while it is running */
180
- Service.trimLog(logFile);
134
+ Daemon.trimLog(logFile);
181
135
  const fd = fs.openSync(logFile, "a");
182
136
  const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
183
137
  const child = spawn(process.execPath, [entry, "service", "start"], {
@@ -188,68 +142,6 @@ export class Service {
188
142
  fs.closeSync(fd);
189
143
  return { child, logFile };
190
144
  }
191
- /* read the last N non-empty lines of a log file for diagnostics */
192
- static readLogTail(logFile, lines) {
193
- let fd = null;
194
- try {
195
- fd = fs.openSync(logFile, "r");
196
- const size = fs.fstatSync(fd).size;
197
- const CHUNK = 8192;
198
- const chunks = [];
199
- let pos = size;
200
- let count = 0;
201
- while (pos > 0 && count <= lines) {
202
- const n = Math.min(CHUNK, pos);
203
- const buf = Buffer.alloc(n);
204
- pos -= n;
205
- fs.readSync(fd, buf, 0, n, pos);
206
- chunks.unshift(buf);
207
- for (let i = 0; i < n; i++)
208
- if (buf[i] === 10)
209
- count++;
210
- }
211
- /* decode once after re-assembly to avoid splitting
212
- multi-byte UTF-8 sequences at chunk boundaries */
213
- const all = Buffer.concat(chunks).toString("utf8")
214
- .split("\n").filter((l) => l.length > 0);
215
- return all.slice(-lines).join("\n");
216
- }
217
- catch {
218
- return "";
219
- }
220
- finally {
221
- if (fd !== null)
222
- fs.closeSync(fd);
223
- }
224
- }
225
- /* show a log file on stdout, either completely or its last <lines> lines only, and
226
- optionally follow it for newly appended lines (default: the last 10 lines first),
227
- where each line is passed through <transform> (e.g. to colorize parts of it) */
228
- static async showLog(logFile, opts, transform = (line) => line) {
229
- const lines = opts.lines !== undefined ? Number(opts.lines) : null;
230
- if (lines !== null && (!Number.isInteger(lines) || lines < 1))
231
- throw new Error(`invalid number of lines: ${opts.lines}`);
232
- if (!fs.existsSync(logFile))
233
- return 0;
234
- if (opts.follow) {
235
- /* follow until interrupted (on log trimming,
236
- "tail" continues at the new end of the file) */
237
- const tail = new Tail(logFile, { nLines: lines ?? 10 });
238
- tail.on("line", (line) => {
239
- process.stdout.write(`${transform(line)}\n`);
240
- });
241
- return await new Promise((_resolve, reject) => {
242
- tail.on("error", (err) => {
243
- reject(err instanceof Error ? err : new Error(String(err)));
244
- });
245
- });
246
- }
247
- const text = lines !== null ?
248
- Service.readLogTail(logFile, lines) :
249
- fs.readFileSync(logFile, "utf8").replace(/\n$/, "");
250
- process.stdout.write(text.length > 0 ? `${text.split("\n").map(transform).join("\n")}\n` : "");
251
- return 0;
252
- }
253
145
  }
254
146
  /* MCP registration entry point for service-identity tools */
255
147
  export class ServiceMCP {
@@ -332,6 +224,68 @@ export default class ServiceCommand {
332
224
  }
333
225
  return { method: bMethod, info };
334
226
  }
227
+ /* register the endpoints of the AI usage accounting: the usage records forwarded
228
+ by the OTLP ingress, the session end, the task assignment on behalf of a session,
229
+ and the task rename */
230
+ static registerUsageRoutes(server, usage) {
231
+ server.route({
232
+ method: "POST",
233
+ path: "/usage/records",
234
+ options: { payload: { parse: true, allow: "application/json", maxBytes: 32 * 1024 * 1024 } },
235
+ handler: (request, h) => {
236
+ const payload = request.payload;
237
+ if (!Array.isArray(payload?.records))
238
+ return h.response({ error: "missing or invalid 'records' field" }).code(400);
239
+ usage.ingest(payload.records);
240
+ return h.response({ ok: true }).code(200);
241
+ }
242
+ });
243
+ server.route({
244
+ method: "POST",
245
+ path: "/usage/detach",
246
+ options: { payload: { parse: true, allow: "application/json" } },
247
+ handler: (request, h) => {
248
+ const payload = request.payload;
249
+ if (typeof payload?.session !== "string")
250
+ return h.response({ error: "missing or invalid 'session' field" }).code(400);
251
+ usage.detach(payload.session);
252
+ return h.response({ ok: true }).code(200);
253
+ }
254
+ });
255
+ server.route({
256
+ method: "POST",
257
+ path: "/usage/rename",
258
+ options: { payload: { parse: true, allow: "application/json" } },
259
+ handler: (request, h) => {
260
+ const payload = request.payload;
261
+ if (typeof payload?.old !== "string" || typeof payload.new !== "string")
262
+ return h.response({ error: "missing or invalid 'old' or 'new' field" }).code(400);
263
+ try {
264
+ usage.rename(payload.old, payload.new);
265
+ }
266
+ catch (err) {
267
+ return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
268
+ }
269
+ return h.response({ ok: true }).code(200);
270
+ }
271
+ });
272
+ server.route({
273
+ method: "POST",
274
+ path: "/usage/assign",
275
+ options: { payload: { parse: true, allow: "application/json" } },
276
+ handler: async (request, h) => {
277
+ const payload = request.payload;
278
+ if (typeof payload?.id !== "string" || typeof payload.assignee !== "string" || typeof payload.session !== "string")
279
+ return h.response({ error: "missing or invalid 'id', 'assignee', or 'session' field" }).code(400);
280
+ try {
281
+ return h.response(await usage.assign(payload.id, payload.assignee, payload.session)).code(200);
282
+ }
283
+ catch (err) {
284
+ return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
285
+ }
286
+ }
287
+ });
288
+ }
335
289
  /* service-side: bind HAPI server until "/stop" command is received or idle timeout happens
336
290
  (or, in the foreground, until "/stop" command is received or a termination signal arrives) */
337
291
  async runService(ctx, foreground = false) {
@@ -361,6 +315,18 @@ export default class ServiceCommand {
361
315
  const usage = new UsageTracker(this.log, Task.projectRoot(), ctx.projectId, ctx.aseDir, () => {
362
316
  lastActivity = Date.now();
363
317
  });
318
+ /* stop the HTTP server and the usage tracker, clear the persisted port, and exit */
319
+ const stopService = async () => {
320
+ try {
321
+ await server.stop({ timeout: 1000 });
322
+ }
323
+ catch (err) {
324
+ this.log.write("error", `service: stop failed: ${err instanceof Error ? err.message : String(err)}`);
325
+ }
326
+ await usage.stop();
327
+ Service.clearPort(ctx.svc);
328
+ process.exit(0);
329
+ };
364
330
  /* listen to HTTP/REST endpoints */
365
331
  server.route({
366
332
  method: "OPTIONS",
@@ -379,17 +345,7 @@ export default class ServiceCommand {
379
345
  if (!ct.startsWith("application/json"))
380
346
  return h.response({ error: "unsupported media type" }).code(415);
381
347
  this.log.write("info", "service: stop requested");
382
- setImmediate(async () => {
383
- try {
384
- await server.stop({ timeout: 1000 });
385
- }
386
- catch (err) {
387
- const e = err;
388
- this.log.write("error", `service: stop failed: ${e.message}`);
389
- }
390
- await usage.stop();
391
- process.exit(0);
392
- });
348
+ setImmediate(stopService);
393
349
  return h.response({ ok: true }).code(200);
394
350
  }
395
351
  });
@@ -456,66 +412,8 @@ export default class ServiceCommand {
456
412
  return h.response({ error: "unknown command", command: cmd }).code(400);
457
413
  }
458
414
  });
459
- /* listen to the endpoints of the AI usage accounting: the usage records forwarded
460
- by the OTLP ingress, the session end, the task assignment on behalf of a session,
461
- and the task rename */
462
- server.route({
463
- method: "POST",
464
- path: "/usage/records",
465
- options: { payload: { parse: true, allow: "application/json", maxBytes: 32 * 1024 * 1024 } },
466
- handler: (request, h) => {
467
- const payload = request.payload;
468
- if (!Array.isArray(payload?.records))
469
- return h.response({ error: "missing or invalid 'records' field" }).code(400);
470
- usage.ingest(payload.records);
471
- return h.response({ ok: true }).code(200);
472
- }
473
- });
474
- server.route({
475
- method: "POST",
476
- path: "/usage/detach",
477
- options: { payload: { parse: true, allow: "application/json" } },
478
- handler: (request, h) => {
479
- const payload = request.payload;
480
- if (typeof payload?.session !== "string")
481
- return h.response({ error: "missing or invalid 'session' field" }).code(400);
482
- usage.detach(payload.session);
483
- return h.response({ ok: true }).code(200);
484
- }
485
- });
486
- server.route({
487
- method: "POST",
488
- path: "/usage/rename",
489
- options: { payload: { parse: true, allow: "application/json" } },
490
- handler: (request, h) => {
491
- const payload = request.payload;
492
- if (typeof payload?.old !== "string" || typeof payload.new !== "string")
493
- return h.response({ error: "missing or invalid 'old' or 'new' field" }).code(400);
494
- try {
495
- usage.rename(payload.old, payload.new);
496
- }
497
- catch (err) {
498
- return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
499
- }
500
- return h.response({ ok: true }).code(200);
501
- }
502
- });
503
- server.route({
504
- method: "POST",
505
- path: "/usage/assign",
506
- options: { payload: { parse: true, allow: "application/json" } },
507
- handler: async (request, h) => {
508
- const payload = request.payload;
509
- if (typeof payload?.id !== "string" || typeof payload.assignee !== "string" || typeof payload.session !== "string")
510
- return h.response({ error: "missing or invalid 'id', 'assignee', or 'session' field" }).code(400);
511
- try {
512
- return h.response(await usage.assign(payload.id, payload.assignee, payload.session)).code(200);
513
- }
514
- catch (err) {
515
- return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
516
- }
517
- }
518
- });
415
+ /* listen to the endpoints of the AI usage accounting */
416
+ ServiceCommand.registerUsageRoutes(server, usage);
519
417
  /* serve the web board of the project (imported dynamically to keep it out of all
520
418
  other CLI commands; its rendering dependencies marked, MRCS, and ELK load on
521
419
  first request only, its board core dependencies at service startup) */
@@ -549,16 +447,7 @@ export default class ServiceCommand {
549
447
  return;
550
448
  stopping = true;
551
449
  this.log.write("info", `service: received ${signal}, stopping`);
552
- try {
553
- await server.stop({ timeout: 1000 });
554
- }
555
- catch (err) {
556
- const e = err;
557
- this.log.write("error", `service: stop failed: ${e.message}`);
558
- }
559
- await usage.stop();
560
- Service.clearPort(ctx.svc);
561
- process.exit(0);
450
+ await stopService();
562
451
  };
563
452
  process.on("SIGTERM", () => shutdown("SIGTERM"));
564
453
  process.on("SIGINT", () => shutdown("SIGINT"));
@@ -578,25 +467,13 @@ export default class ServiceCommand {
578
467
  if (Date.now() - lastActivity > IDLE_MS) {
579
468
  stopping = true;
580
469
  this.log.write("info", "service: idle timeout reached, stopping");
581
- try {
582
- await server.stop({ timeout: 1000 });
583
- await usage.stop();
584
- Service.clearPort(ctx.svc);
585
- process.exit(0);
586
- }
587
- catch (err) {
588
- const e = err;
589
- this.log.write("error", `service: stop failed: ${e.message}`);
590
- await usage.stop();
591
- Service.clearPort(ctx.svc);
592
- process.exit(1);
593
- }
470
+ await stopService();
594
471
  }
595
472
  }, TICK_MS).unref();
596
473
  }
597
474
  /* allocate the service port, preferring the configured one, else falling back to a random one */
598
475
  async allocatePort(ctx) {
599
- const port = await Service.allocatePort(ctx.portPref);
476
+ const port = await Daemon.allocatePort(ctx.portPref);
600
477
  if (ctx.portPref !== null && port !== ctx.portPref)
601
478
  this.log.write("warning", `service: preferred port ${ctx.portPref} unavailable, falling back to port ${port}`);
602
479
  return port;
@@ -612,7 +489,7 @@ export default class ServiceCommand {
612
489
  if (level !== undefined && isLogLevel(level))
613
490
  this.log.logLevel(level);
614
491
  const raw = process.env[PORT_ENV];
615
- port = raw !== undefined ? Number(raw) : await Service.allocatePort();
492
+ port = raw !== undefined ? Number(raw) : await Daemon.allocatePort();
616
493
  await this.runService({ ...ctx, port });
617
494
  return await new Promise(() => { });
618
495
  }
@@ -668,7 +545,7 @@ export default class ServiceCommand {
668
545
  break;
669
546
  }
670
547
  }
671
- const tail = Service.readLogTail(logFile, 20);
548
+ const tail = Daemon.readLogTail(logFile, 20);
672
549
  const reason = exited ?
673
550
  `service exited during startup (code ${exitCode})` :
674
551
  foreign ?
@@ -755,7 +632,7 @@ export default class ServiceCommand {
755
632
  /* log flow: show the (tail of the) log file and optionally follow it */
756
633
  async doLog(opts) {
757
634
  const ctx = this.loadContext();
758
- return Service.showLog(path.join(ctx.aseDir, "service.log"), opts);
635
+ return Daemon.showLog(path.join(ctx.aseDir, "service.log"), opts);
759
636
  }
760
637
  /* stop flow: no-op if no port configured or connection refused */
761
638
  async doStop() {
@@ -51,8 +51,7 @@ export class SetupRunner {
51
51
  if (process.platform === "win32")
52
52
  return false;
53
53
  /* already running as root */
54
- const getuid = process.getuid;
55
- if (typeof getuid === "function" && getuid.call(process) === 0)
54
+ if (process.getuid?.() === 0)
56
55
  return false;
57
56
  /* determine the npm global prefix and probe writability of the
58
57
  directories that "npm -g" actually mutates */
@@ -252,7 +252,6 @@ export class SetupSettings {
252
252
  /* the OpenAI Codex CLI has no scriptable statusline mechanism at all */
253
253
  if (tool === "codex")
254
254
  return [];
255
- const home = os.homedir();
256
255
  const scopes = tool === "claude" ? ["user", "project", "local"] : ["user"];
257
256
  const entries = [];
258
257
  for (const scope of scopes) {
@@ -269,7 +268,7 @@ export class SetupSettings {
269
268
  if (member === undefined)
270
269
  continue;
271
270
  entries.push({
272
- file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
271
+ file: this.fileDisplay(file),
273
272
  scope: tool === "claude" ? scope : "(n/a)",
274
273
  status: this.statuslineIsOwned(member) ? "activated" : "foreign"
275
274
  });
@@ -336,7 +335,6 @@ export class SetupSettings {
336
335
  async outputStyleStatus(tool) {
337
336
  if (tool !== "claude")
338
337
  return [];
339
- const home = os.homedir();
340
338
  const scopes = ["user", "project", "local"];
341
339
  const entries = [];
342
340
  for (const scope of scopes) {
@@ -353,7 +351,7 @@ export class SetupSettings {
353
351
  if (member === undefined)
354
352
  continue;
355
353
  entries.push({
356
- file: file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file,
354
+ file: this.fileDisplay(file),
357
355
  scope,
358
356
  status: this.outputStyleIsOwned(member) ? "selected" : "foreign"
359
357
  });
@@ -389,8 +387,7 @@ export class SetupSettings {
389
387
  /* a match is the presence of the "ase@ase" key under
390
388
  "enabledPlugins", regardless of its true/false value */
391
389
  const plugins = settings?.enabledPlugins;
392
- if (plugins !== undefined && plugins !== null
393
- && Object.prototype.hasOwnProperty.call(plugins, "ase@ase")) {
390
+ if (plugins !== undefined && plugins !== null && Object.hasOwn(plugins, "ase@ase")) {
394
391
  if (scope !== "user")
395
392
  this.log.write("info", `setup: detected ASE plugin installation in scope "${scope}"`);
396
393
  return scope;
@@ -488,7 +485,7 @@ export class SetupSettings {
488
485
  return files;
489
486
  }
490
487
  /* abbreviate the home directory of a file path as "~" */
491
- telemetryFileDisplay(file) {
488
+ fileDisplay(file) {
492
489
  const home = os.homedir();
493
490
  return file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file;
494
491
  }
@@ -796,7 +793,7 @@ export class SetupSettings {
796
793
  const present = env !== undefined && env.type() === "object" ? this.telemetryClaudePresent(env) : [];
797
794
  if (env !== undefined && present.length > 0)
798
795
  entries.push({
799
- file: this.telemetryFileDisplay(file),
796
+ file: this.fileDisplay(file),
800
797
  scope: "user",
801
798
  status: this.telemetryClaudeIsOwned(env) ? "activated" : "foreign"
802
799
  });
@@ -822,7 +819,7 @@ export class SetupSettings {
822
819
  else if ([...block.head, ...block.tail].some((line) => foreign.test(line)))
823
820
  status = "foreign";
824
821
  if (status !== "")
825
- entries.push({ file: this.telemetryFileDisplay(file), scope: "(n/a)", status });
822
+ entries.push({ file: this.fileDisplay(file), scope: "(n/a)", status });
826
823
  }
827
824
  if (tool === "copilot" && this.telemetryCopilotForeignEnv() !== undefined)
828
825
  entries.push({ file: "$OTEL_EXPORTER_OTLP_ENDPOINT", scope: "(n/a)", status: "foreign" });
package/dst/ase-setup.js CHANGED
@@ -347,10 +347,9 @@ export default class SetupCommand {
347
347
  });
348
348
  /* parser for the non-negative integer "ase setup statusline" options */
349
349
  const parseNonNeg = (name) => (value) => {
350
- const n = Number.parseInt(value, 10);
351
- if (!Number.isFinite(n) || n < 0)
350
+ if (!/^\d+$/.test(value))
352
351
  throw new Error(`invalid --${name} value: "${value}" (expected a non-negative integer)`);
353
- return n;
352
+ return Number.parseInt(value, 10);
354
353
  };
355
354
  /* register CLI sub-command "ase setup statusline" */
356
355
  const statuslineCmd = setupCmd