@rse/ase 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dst/ase-config.js CHANGED
@@ -325,14 +325,15 @@ export class Config {
325
325
  /* enumerate all full dotted leaf paths from the attached valibot schema */
326
326
  schemaLeafPaths() {
327
327
  const unwrap = (s) => {
328
- while (s !== undefined && s !== null && (s.type === "optional" || s.type === "nullish"
329
- || s.type === "nullable" || s.type === "undefinedable"))
330
- s = s.wrapped;
331
- return s;
328
+ let cur = s;
329
+ while (cur !== undefined && cur !== null && (cur.type === "optional" || cur.type === "nullish"
330
+ || cur.type === "nullable" || cur.type === "undefinedable"))
331
+ cur = cur.wrapped;
332
+ return cur ?? null;
332
333
  };
333
334
  const walk = (s, prefix) => {
334
335
  const u = unwrap(s);
335
- if (u !== undefined && u !== null
336
+ if (u !== null
336
337
  && (u.type === "object" || u.type === "strict_object" || u.type === "loose_object")
337
338
  && u.entries !== undefined) {
338
339
  const paths = [];
package/dst/ase-hook.js CHANGED
@@ -79,7 +79,7 @@ export default class HookCommand {
79
79
  const versionHints = [];
80
80
  if (versionCurrentPlugin !== versionCurrentTool)
81
81
  versionHints.push("**WARNING:** version *mismatch*: " +
82
- `tool: **${versionCurrentPlugin}**, plugin: **${versionCurrentTool}**`);
82
+ `tool: **${versionCurrentTool}**, plugin: **${versionCurrentPlugin}**`);
83
83
  if (versionCurrentTool !== versionLatestTool)
84
84
  versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
85
85
  if (process.env.ASE_SETUP_DEV !== undefined)
@@ -91,22 +91,25 @@ export default class HookCommand {
91
91
  const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
92
92
  /* determine session id */
93
93
  const sessionId = input.session_id ?? input.sessionId ?? "";
94
- /* establish config context */
95
- const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
94
+ /* establish config context (session-scoped only if a valid sessionId is present) */
95
+ const hasSession = /^[A-Za-z0-9._-]+$/.test(sessionId);
96
+ const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
96
97
  try {
97
98
  cfg.read();
98
99
  }
99
100
  catch (_e) {
100
101
  /* best-effort: ignore failures */
101
102
  }
102
- /* determine task id */
103
+ /* determine task id (only persist when scoped to a real session) */
103
104
  const taskId = process.env.ASE_TASK_ID ?? "default";
104
- try {
105
- cfg.set("agent.task", taskId);
106
- cfg.write();
107
- }
108
- catch (_e) {
109
- /* best-effort: ignore failures */
105
+ if (hasSession) {
106
+ try {
107
+ cfg.set("agent.task", taskId);
108
+ cfg.write();
109
+ }
110
+ catch (_e) {
111
+ /* best-effort: ignore failures */
112
+ }
110
113
  }
111
114
  /* determine project id */
112
115
  const cwd = input.cwd ?? process.cwd();
package/dst/ase-mcp.js CHANGED
@@ -5,42 +5,11 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
- import axios from "axios";
9
- import * as v from "valibot";
10
8
  import { execa } from "execa";
11
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
10
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
11
  import { Config, configSchema } from "./ase-config.js";
14
- const HOST = "127.0.0.1";
15
- /* schema for ".ase/service.yaml" (same shape as in ase-service.ts) */
16
- const serviceSchema = v.nullish(v.strictObject({
17
- port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
18
- }));
19
- /* distinguish ECONNREFUSED from other Axios transport errors */
20
- const isConnRefused = (err) => {
21
- const e = err;
22
- return e?.code === "ECONNREFUSED" || e?.cause?.code === "ECONNREFUSED";
23
- };
24
- /* probe the service and verify ASE identity banner */
25
- const probe = async (port, projectId) => {
26
- try {
27
- const r = await axios.request({
28
- method: "OPTIONS",
29
- url: `http://${HOST}:${port}/`,
30
- timeout: 2000,
31
- validateStatus: () => true
32
- });
33
- if (r.status < 200 || r.status >= 300)
34
- return false;
35
- const d = r.data;
36
- return d?.ase === true && d?.projectId === projectId;
37
- }
38
- catch (err) {
39
- if (isConnRefused(err))
40
- return null;
41
- throw err;
42
- }
43
- };
12
+ import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service-probe.js";
44
13
  /* CLI command "ase mcp" */
45
14
  export default class MCPCommand {
46
15
  log;
@@ -0,0 +1,38 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+ import axios from "axios";
7
+ import * as v from "valibot";
8
+ /* shared service host */
9
+ export const SERVICE_HOST = "127.0.0.1";
10
+ /* schema for ".ase/service.yaml" */
11
+ export const serviceSchema = v.nullish(v.strictObject({
12
+ port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
13
+ }));
14
+ /* distinguish ECONNREFUSED from other Axios transport errors */
15
+ export const isConnRefused = (err) => {
16
+ const e = err;
17
+ return e?.code === "ECONNREFUSED" || e?.cause?.code === "ECONNREFUSED";
18
+ };
19
+ /* probe the service and verify ASE identity banner */
20
+ export const probe = async (port, projectId) => {
21
+ try {
22
+ const r = await axios.request({
23
+ method: "OPTIONS",
24
+ url: `http://${SERVICE_HOST}:${port}/`,
25
+ timeout: 2000,
26
+ validateStatus: () => true
27
+ });
28
+ if (r.status < 200 || r.status >= 300)
29
+ return false;
30
+ const d = r.data;
31
+ return d?.ase === true && d?.projectId === projectId;
32
+ }
33
+ catch (err) {
34
+ if (isConnRefused(err))
35
+ return null;
36
+ throw err;
37
+ }
38
+ };
@@ -11,7 +11,6 @@ import { spawn } from "node:child_process";
11
11
  import Hapi from "@hapi/hapi";
12
12
  import axios from "axios";
13
13
  import { isMap, isScalar } from "yaml";
14
- import * as v from "valibot";
15
14
  import prettyMs from "pretty-ms";
16
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
16
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -20,19 +19,15 @@ import { DateTime } from "luxon";
20
19
  import { Config, configSchema, parseScope } from "./ase-config.js";
21
20
  import { renderDiagram, detectTermWidth, detectTermHeight } from "./ase-diagram.js";
22
21
  import { taskLoad, taskSave, taskDelete, taskList } from "./ase-task.js";
22
+ import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service-probe.js";
23
23
  import pkg from "../package.json" with { type: "json" };
24
24
  const SERVE_ENV = "ASE_SERVICE_SERVE";
25
25
  const PORT_ENV = "ASE_SERVICE_PORT";
26
- const HOST = "127.0.0.1";
27
26
  const IDLE_MS = 30 * 60 * 1000;
28
27
  const TICK_MS = 60 * 1000;
29
28
  const PORT_MIN = 42000;
30
29
  const PORT_MAX = 44000;
31
30
  const PORT_TRIES = 20;
32
- /* schema for ".ase/service.yaml" */
33
- const serviceSchema = v.nullish(v.strictObject({
34
- port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
35
- }));
36
31
  /* try binding a single candidate port to verify availability */
37
32
  const tryBind = (port) => {
38
33
  return new Promise((resolve) => {
@@ -72,31 +67,6 @@ const clearPort = (svc) => {
72
67
  else
73
68
  svc.write();
74
69
  };
75
- /* distinguish ECONNREFUSED from other Axios transport errors */
76
- const isConnRefused = (err) => {
77
- const e = err;
78
- return e?.code === "ECONNREFUSED" || e?.cause?.code === "ECONNREFUSED";
79
- };
80
- /* probe the service and verify ASE identity banner */
81
- const probe = async (port, projectId) => {
82
- try {
83
- const r = await axios.request({
84
- method: "OPTIONS",
85
- url: `http://${HOST}:${port}/`,
86
- timeout: 2000,
87
- validateStatus: () => true
88
- });
89
- if (r.status < 200 || r.status >= 300)
90
- return false;
91
- const d = r.data;
92
- return d?.ase === true && d?.projectId === projectId;
93
- }
94
- catch (err) {
95
- if (isConnRefused(err))
96
- return null;
97
- throw err;
98
- }
99
- };
100
70
  /* spawn the current executable detached as a background service */
101
71
  const spawnDetached = (aseDir, port) => {
102
72
  fs.mkdirSync(aseDir, { recursive: true });
@@ -234,9 +204,9 @@ export default class ServiceCommand {
234
204
  "control-flow/branching/concurrency as a Flowchart, " +
235
205
  "state-machine/states/transitions as an UML State Diagram, " +
236
206
  "data-flow/actors/messages/protocols as an UML Sequence Diagram, " +
237
- "data-structure/classes/methods as an UML Class Diagram " +
207
+ "data-structure/classes/methods as an UML Class Diagram, " +
238
208
  "data-model/entities/relationships as an ER Diagram, or " +
239
- "metrics/distributions/time-series as a XY-Charts. " +
209
+ "metrics/distributions/time-series as an XY-Chart. " +
240
210
  "Pass the Mermaid diagram specification as `diagram`. " +
241
211
  "Returns the rendered art as `text`.",
242
212
  inputSchema: {
@@ -288,15 +258,33 @@ export default class ServiceCommand {
288
258
  });
289
259
  mcp.registerTool("task_list", {
290
260
  title: "ASE task list",
291
- description: "List all persisted task `id`s. " +
292
- "Returns the ids as `text`, one per line, in lexicographic order; " +
293
- "returns an empty string if no tasks exist.",
294
- inputSchema: {}
295
- }, async () => {
261
+ description: "List all persisted tasks. " +
262
+ "Returns a `tasks` array (in lexicographic `id` order) where each item has the " +
263
+ "task `id`. If `verbose` is `true`, each item additionally has an `mtime` field " +
264
+ "(last modification time of the task's `plan.md`, formatted as `YYYY-MM-DD HH:MM`). " +
265
+ "Returns an empty array if no tasks exist.",
266
+ inputSchema: {
267
+ verbose: z.boolean().optional()
268
+ .describe("if true, also include the `mtime` field per task (default: false)")
269
+ },
270
+ outputSchema: {
271
+ tasks: z.array(z.object({
272
+ id: z.string().describe("task identifier"),
273
+ mtime: z.string().optional()
274
+ .describe("plan.md modification time (`YYYY-MM-DD HH:MM`); only present if `verbose` is true")
275
+ })).describe("all persisted tasks in lexicographic id order")
276
+ }
277
+ }, async (args) => {
296
278
  try {
297
- const ids = taskList();
279
+ const verbose = args.verbose ?? false;
280
+ const items = taskList(verbose);
281
+ const tasks = verbose ?
282
+ items.map((item) => ({ id: item.id, mtime: item.mtime })) :
283
+ items.map((item) => ({ id: item.id }));
284
+ const result = { tasks };
298
285
  return {
299
- content: [{ type: "text", text: ids.join("\n") }]
286
+ structuredContent: result,
287
+ content: [{ type: "text", text: JSON.stringify(result) }]
300
288
  };
301
289
  }
302
290
  catch (err) {
package/dst/ase-setup.js CHANGED
@@ -55,15 +55,16 @@ export default class SetupCommand {
55
55
  this.log.write("info", `setup: ${ignoreError} (skipped)`);
56
56
  return;
57
57
  }
58
- const exitCode = typeof err?.exitCode === "number" ? err.exitCode : -1;
58
+ const e = err;
59
+ const exitCode = typeof e.exitCode === "number" ? e.exitCode : -1;
59
60
  this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
60
- if (typeof err?.stdout === "string" && err.stdout.length > 0) {
61
+ if (typeof e.stdout === "string" && e.stdout.length > 0) {
61
62
  this.log.write("error", "setup: command failed: stdout:");
62
- process.stdout.write(err.stdout);
63
+ process.stdout.write(e.stdout);
63
64
  }
64
- if (typeof err?.stderr === "string" && err.stderr.length > 0) {
65
+ if (typeof e.stderr === "string" && e.stderr.length > 0) {
65
66
  this.log.write("error", "setup: command failed: stderr:");
66
- process.stderr.write(err.stderr);
67
+ process.stderr.write(e.stderr);
67
68
  }
68
69
  throw err;
69
70
  }
@@ -11,6 +11,7 @@ import { InvalidArgumentError } from "commander";
11
11
  import { execaSync } from "execa";
12
12
  import { Chalk } from "chalk";
13
13
  import { Config, configSchema, parseScope } from "./ase-config.js";
14
+ import pkg from "../package.json" with { type: "json" };
14
15
  /* forced-color chalk instance: stdout is a pipe under Claude Code,
15
16
  so chalk auto-detection would yield level 0; force level 1 to keep
16
17
  emitting ANSI sequences as the original implementation did */
@@ -36,15 +37,25 @@ const readStdin = async () => {
36
37
  /* detect terminal column width via /dev/tty (stdout is a pipe under Claude Code) */
37
38
  const detectTermWidth = () => {
38
39
  let width = 0;
40
+ let tty = null;
39
41
  try {
40
- const tty = fs.openSync("/dev/tty", "r");
42
+ tty = fs.openSync("/dev/tty", "r");
41
43
  const out = execFileSync("tput", ["cols"], { stdio: [tty, "pipe", "ignore"] });
42
- fs.closeSync(tty);
43
- width = parseInt(out.toString("utf8").trim()) || 0;
44
+ width = Number.parseInt(out.toString("utf8").trim(), 10) || 0;
44
45
  }
45
46
  catch (_e) {
46
47
  /* no controlling terminal */
47
48
  }
49
+ finally {
50
+ if (tty !== null) {
51
+ try {
52
+ fs.closeSync(tty);
53
+ }
54
+ catch (_e) {
55
+ /* best-effort */
56
+ }
57
+ }
58
+ }
48
59
  return width;
49
60
  };
50
61
  /* format a token count as a compact human-readable string (e.g. 334k, 104.9M) */
@@ -287,6 +298,7 @@ export default class StatuslineCommand {
287
298
  /* identifier to renderer map: each callback fetches its own information
288
299
  directly from data (or via the lazy helpers above for shared values) */
289
300
  const renderers = {
301
+ /* ==== SCOPE ==== */
290
302
  u: () => {
291
303
  const user = process.env.USER ?? process.env.LOGNAME ?? "unknown";
292
304
  emit(`${prefix("※", "user")}${c.bold(user)}`);
@@ -301,6 +313,7 @@ export default class StatuslineCommand {
301
313
  emit(`${prefix("◉", "task")}${c.bold(taskId)}`);
302
314
  },
303
315
  s: () => emit(`${prefix("⏻", "session")}${c.bold(getSession())}`),
316
+ /* ==== MODEL ==== */
304
317
  m: () => {
305
318
  const model = data.model?.display_name ?? "";
306
319
  emit(`${prefix("⚙", "model")}${c.bold(model)}`);
@@ -313,11 +326,18 @@ export default class StatuslineCommand {
313
326
  const thinking = (data.thinking?.enabled ?? false) === true ? "yes" : "no";
314
327
  emit(`${prefix("⚛", "thinking")}${c.bold(thinking)}`);
315
328
  },
329
+ /* ==== OUTPUT ==== */
330
+ O: () => {
331
+ const styleName = data.output_style?.name ?? "";
332
+ if (styleName !== "")
333
+ emit(`${prefix("≡", "style")}${c.bold(styleName)}`);
334
+ },
316
335
  P: () => {
317
336
  const { persona } = getCfg();
318
337
  if (persona !== "")
319
338
  emit(`${prefix("☯", "persona")}${c.bold(persona)}`);
320
339
  },
340
+ /* ==== CONTEXT ==== */
321
341
  c: () => {
322
342
  const pct = Math.floor(data.context_window?.used_percentage ?? 0);
323
343
  const barSize = 20;
@@ -325,38 +345,15 @@ export default class StatuslineCommand {
325
345
  const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
326
346
  emit(`${prefix("◔", "context")}${bar} ${pct}%`);
327
347
  },
328
- a: () => {
329
- const linesAdded = data.cost?.total_lines_added ?? 0;
330
- emit(`${prefix("⊕", "added")}${c.bold(linesAdded)}`);
331
- },
332
- r: () => {
333
- const linesRemoved = data.cost?.total_lines_removed ?? 0;
334
- emit(`${prefix("⊖", "removed")}${c.bold(linesRemoved)}`);
335
- },
336
348
  C: () => {
337
- const ctxIn = data.context_window?.current_usage?.input_tokens ?? 0;
338
- const ctxCcIn = data.context_window?.current_usage?.cache_creation_input_tokens ?? 0;
339
- const ctxCrIn = data.context_window?.current_usage?.cache_read_input_tokens ?? 0;
340
- const tokensCur = ctxIn + ctxCcIn + ctxCrIn;
341
- if (tokensCur > 0)
342
- emit(`${prefix("◇", "tokens")}${c.bold(formatTokens(tokensCur))}`);
343
- },
344
- L: () => {
345
- const pct = Math.floor(data.context_window?.used_percentage ?? 0);
346
- const ctxIn = data.context_window?.current_usage?.input_tokens ?? 0;
347
- const ctxCcIn = data.context_window?.current_usage?.cache_creation_input_tokens ?? 0;
348
- const ctxCrIn = data.context_window?.current_usage?.cache_read_input_tokens ?? 0;
349
- const tokensCur = ctxIn + ctxCcIn + ctxCrIn;
350
- const tokensLim = pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0;
351
- if (tokensLim > 0)
352
- emit(`${prefix("◆", "limit")}${c.bold(formatTokens(tokensLim))}`);
353
- },
354
- N: () => {
355
- const tokensCum = (data.context_window?.total_input_tokens ?? 0) +
349
+ const context = Math.floor(data.context_window?.used_percentage ?? 0);
350
+ const tokensCur = (data.context_window?.total_input_tokens ?? 0) +
356
351
  (data.context_window?.total_output_tokens ?? 0);
357
- if (tokensCum > 0)
358
- emit(`${prefix("Σ", "total")}${c.bold(formatTokens(tokensCum))}`);
352
+ const tokensLim = context > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / context) : 0;
353
+ if (tokensLim > 0)
354
+ emit(`${prefix("◆", "tokens")}${c.bold(formatTokens(tokensCur) + "/" + formatTokens(tokensLim))}`);
359
355
  },
356
+ /* ==== RATE LIMITS ==== */
360
357
  S: () => {
361
358
  const pct5h = data.rate_limits?.five_hour?.used_percentage;
362
359
  if (pct5h !== undefined)
@@ -379,6 +376,7 @@ export default class StatuslineCommand {
379
376
  if (s !== "")
380
377
  emit(`${prefix("⏱", "weekly-resets")}${c.bold(s)}`);
381
378
  },
379
+ /* ==== COSTS ==== */
382
380
  H: () => {
383
381
  const sessDurMs = data.cost?.total_duration_ms ?? 0;
384
382
  if (sessDurMs > 0)
@@ -389,6 +387,15 @@ export default class StatuslineCommand {
389
387
  if (sessCost !== undefined)
390
388
  emit(`${prefix("$", "cost")}${c.bold(formatCostUsd(sessCost))}`);
391
389
  },
390
+ /* ==== VERSION CONTROL ==== */
391
+ a: () => {
392
+ const linesAdded = data.cost?.total_lines_added ?? 0;
393
+ emit(`${prefix("⊕", "added")}${c.bold(linesAdded)}`);
394
+ },
395
+ r: () => {
396
+ const linesRemoved = data.cost?.total_lines_removed ?? 0;
397
+ emit(`${prefix("⊖", "removed")}${c.bold(linesRemoved)}`);
398
+ },
392
399
  b: () => {
393
400
  const g = getGit();
394
401
  const label = g.branch !== "" ? g.branch : "no git";
@@ -409,20 +416,22 @@ export default class StatuslineCommand {
409
416
  if (cwd !== "")
410
417
  emit(`${prefix("▶", "cwd")}${c.bold(cwd)}`);
411
418
  },
419
+ /* ==== RESOURCES ==== */
412
420
  M: () => {
413
421
  const m = getMem();
414
422
  if (m.total > 0)
415
423
  emit(`${prefix("⛁", "mem")}${c.bold(`${formatBytes(m.used)}/${formatBytes(m.total)}`)}`);
416
424
  },
425
+ /* ==== VERSIONS ==== */
417
426
  V: () => {
418
427
  const ccVersion = data.version ?? "";
428
+ const aseVersion = pkg.version ?? "";
429
+ let version = "";
419
430
  if (ccVersion !== "")
420
- emit(`${prefix("⎈", "version")}${c.bold(ccVersion)}`);
421
- },
422
- o: () => {
423
- const styleName = data.output_style?.name ?? "";
424
- if (styleName !== "")
425
- emit(`${prefix("≡", "style")}${c.bold(styleName)}`);
431
+ version += `claude/${ccVersion}`;
432
+ if (aseVersion !== "")
433
+ version += `${version !== "" ? " " : ""}ase/${aseVersion}`;
434
+ emit(`${prefix("⎈", "version")}${c.bold(version)}`);
426
435
  }
427
436
  };
428
437
  /* walk each template line and render */
package/dst/ase-task.js CHANGED
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import os from "node:os";
8
8
  import fs from "node:fs";
9
9
  import { execaSync } from "execa";
10
+ import { DateTime } from "luxon";
10
11
  /* validate the task id to keep it safe as a filename component */
11
12
  const validateId = (id) => {
12
13
  if (typeof id !== "string" || id.length === 0)
@@ -45,12 +46,14 @@ export const taskDelete = (id) => {
45
46
  fs.rmSync(path.dirname(file), { recursive: true, force: true });
46
47
  return true;
47
48
  };
48
- /* list all persisted task ids in lexicographic order */
49
- export const taskList = () => {
49
+ /* list all persisted tasks in lexicographic id order; if verbose is true,
50
+ each entry's `mtime` is set to the `plan.md` modification time formatted
51
+ as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
52
+ export const taskList = (verbose = false) => {
50
53
  const dir = path.join(os.homedir(), ".ase", "task");
51
54
  if (!fs.existsSync(dir))
52
55
  return [];
53
- const ids = [];
56
+ const out = [];
54
57
  for (const entry of fs.readdirSync(dir)) {
55
58
  if (!/^[A-Za-z0-9-]+$/.test(entry))
56
59
  continue;
@@ -60,10 +63,11 @@ export const taskList = () => {
60
63
  const st = fs.statSync(file);
61
64
  if (!st.isFile())
62
65
  continue;
63
- ids.push(entry);
66
+ const mtime = verbose ? DateTime.fromJSDate(st.mtime).toFormat("yyyy-LL-dd HH:mm") : undefined;
67
+ out.push({ id: entry, mtime });
64
68
  }
65
- ids.sort();
66
- return ids;
69
+ out.sort((a, b) => a.id.localeCompare(b.id));
70
+ return out;
67
71
  };
68
72
  /* purge tasks whose modification time is older than the given cutoff in
69
73
  milliseconds; returns the list of removed task ids */
@@ -119,10 +123,15 @@ export default class TaskCommand {
119
123
  task
120
124
  .command("list")
121
125
  .description("List all persisted task ids, one per line")
122
- .action(() => {
123
- const ids = taskList();
124
- for (const id of ids)
125
- process.stdout.write(`${id}\n`);
126
+ .option("-v, --verbose", "also show the plan.md modification time as (YYYY-MM-DD HH:MM)")
127
+ .action((opts) => {
128
+ const items = taskList(opts.verbose ?? false);
129
+ for (const item of items) {
130
+ if (opts.verbose)
131
+ process.stdout.write(`${item.id}\t(${item.mtime})\n`);
132
+ else
133
+ process.stdout.write(`${item.id}\n`);
134
+ }
126
135
  process.exit(0);
127
136
  });
128
137
  /* register CLI sub-command "ase task load" */
@@ -3,7 +3,7 @@
3
3
  ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
- import { execa } from "execa";
6
+ import updateNotifier from "update-notifier";
7
7
  import pkg from "../package.json" with { type: "json" };
8
8
  /* determination of current and available ASE versions */
9
9
  export default class Version {
@@ -13,15 +13,10 @@ export default class Version {
13
13
  }
14
14
  /* return latest ASE version available on the NPM registry */
15
15
  static async latest() {
16
- let latest = "";
17
- try {
18
- const r = await execa("npm", ["view", "@rse/ase", "version"], { stdio: ["ignore", "pipe", "pipe"] });
19
- latest = r.stdout.trim();
20
- }
21
- catch (err) {
22
- const message = err instanceof Error ? err.message : String(err);
23
- throw new Error(`failed to query latest ASE version: ${message}`, { cause: err });
24
- }
25
- return latest;
16
+ const notifier = updateNotifier({
17
+ pkg,
18
+ updateCheckInterval: 1000 * 60 * 60
19
+ });
20
+ return notifier.update?.latest ?? Version.current();
26
21
  }
27
22
  }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.25",
9
+ "version": "0.0.27",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,8 +18,8 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.59.2",
22
- "@typescript-eslint/eslint-plugin": "8.59.2",
21
+ "@typescript-eslint/parser": "8.59.3",
22
+ "@typescript-eslint/eslint-plugin": "8.59.3",
23
23
  "eslint-plugin-promise": "7.3.0",
24
24
  "eslint-plugin-import": "2.32.0",
25
25
  "neostandard": "0.13.0",
@@ -30,18 +30,19 @@
30
30
  "nodemon": "3.1.14",
31
31
  "shx": "0.4.0",
32
32
 
33
- "@types/node": "25.6.0",
33
+ "@types/node": "25.8.0",
34
34
  "@types/luxon": "3.7.1",
35
- "@types/which": "3.0.4"
35
+ "@types/which": "3.0.4",
36
+ "@types/update-notifier": "6.0.8"
36
37
  },
37
38
  "dependencies": {
38
39
  "commander": "14.0.3",
39
- "yaml": "2.8.4",
40
+ "yaml": "2.9.0",
40
41
  "valibot": "1.4.0",
41
42
  "execa": "9.6.1",
42
43
  "mkdirp": "3.0.1",
43
44
  "@hapi/hapi": "21.4.9",
44
- "axios": "1.16.0",
45
+ "axios": "1.16.1",
45
46
  "beautiful-mermaid": "1.1.3",
46
47
  "cli-table3": "0.6.5",
47
48
  "chalk": "5.6.2",
@@ -49,11 +50,12 @@
49
50
  "luxon": "3.7.2",
50
51
  "@modelcontextprotocol/sdk": "1.29.0",
51
52
  "zod": "4.4.3",
52
- "which": "6.0.1"
53
+ "which": "7.0.0",
54
+ "update-notifier": "7.3.1"
53
55
  },
54
56
  "engines": {
55
57
  "npm": ">=10.0.0",
56
- "node": ">=20.0.0"
58
+ "node": ">=22.0.0"
57
59
  },
58
60
  "upd": [ "!eslint", "!@eslint/js" ],
59
61
  "files": [