@rse/ase 0.0.26 → 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
  }
@@ -37,15 +37,25 @@ const readStdin = async () => {
37
37
  /* detect terminal column width via /dev/tty (stdout is a pipe under Claude Code) */
38
38
  const detectTermWidth = () => {
39
39
  let width = 0;
40
+ let tty = null;
40
41
  try {
41
- const tty = fs.openSync("/dev/tty", "r");
42
+ tty = fs.openSync("/dev/tty", "r");
42
43
  const out = execFileSync("tput", ["cols"], { stdio: [tty, "pipe", "ignore"] });
43
- fs.closeSync(tty);
44
- width = parseInt(out.toString("utf8").trim()) || 0;
44
+ width = Number.parseInt(out.toString("utf8").trim(), 10) || 0;
45
45
  }
46
46
  catch (_e) {
47
47
  /* no controlling terminal */
48
48
  }
49
+ finally {
50
+ if (tty !== null) {
51
+ try {
52
+ fs.closeSync(tty);
53
+ }
54
+ catch (_e) {
55
+ /* best-effort */
56
+ }
57
+ }
58
+ }
49
59
  return width;
50
60
  };
51
61
  /* format a token count as a compact human-readable string (e.g. 334k, 104.9M) */
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" */
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.26",
9
+ "version": "0.0.27",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -30,7 +30,7 @@
30
30
  "nodemon": "3.1.14",
31
31
  "shx": "0.4.0",
32
32
 
33
- "@types/node": "25.7.0",
33
+ "@types/node": "25.8.0",
34
34
  "@types/luxon": "3.7.1",
35
35
  "@types/which": "3.0.4",
36
36
  "@types/update-notifier": "6.0.8"
@@ -42,7 +42,7 @@
42
42
  "execa": "9.6.1",
43
43
  "mkdirp": "3.0.1",
44
44
  "@hapi/hapi": "21.4.9",
45
- "axios": "1.16.0",
45
+ "axios": "1.16.1",
46
46
  "beautiful-mermaid": "1.1.3",
47
47
  "cli-table3": "0.6.5",
48
48
  "chalk": "5.6.2",