@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 +6 -5
- package/dst/ase-hook.js +13 -10
- package/dst/ase-mcp.js +1 -32
- package/dst/ase-service-probe.js +38 -0
- package/dst/ase-service.js +28 -40
- package/dst/ase-setup.js +6 -5
- package/dst/ase-statusline.js +47 -38
- package/dst/ase-task.js +19 -10
- package/dst/ase-version.js +6 -11
- package/package.json +11 -9
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 !==
|
|
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: **${
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dst/ase-service.js
CHANGED
|
@@ -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
|
|
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
|
|
292
|
-
"Returns
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
61
|
+
if (typeof e.stdout === "string" && e.stdout.length > 0) {
|
|
61
62
|
this.log.write("error", "setup: command failed: stdout:");
|
|
62
|
-
process.stdout.write(
|
|
63
|
+
process.stdout.write(e.stdout);
|
|
63
64
|
}
|
|
64
|
-
if (typeof
|
|
65
|
+
if (typeof e.stderr === "string" && e.stderr.length > 0) {
|
|
65
66
|
this.log.write("error", "setup: command failed: stderr:");
|
|
66
|
-
process.stderr.write(
|
|
67
|
+
process.stderr.write(e.stderr);
|
|
67
68
|
}
|
|
68
69
|
throw err;
|
|
69
70
|
}
|
package/dst/ase-statusline.js
CHANGED
|
@@ -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
|
-
|
|
42
|
+
tty = fs.openSync("/dev/tty", "r");
|
|
41
43
|
const out = execFileSync("tput", ["cols"], { stdio: [tty, "pipe", "ignore"] });
|
|
42
|
-
|
|
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
|
|
338
|
-
const
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
66
|
+
const mtime = verbose ? DateTime.fromJSDate(st.mtime).toFormat("yyyy-LL-dd HH:mm") : undefined;
|
|
67
|
+
out.push({ id: entry, mtime });
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
return
|
|
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
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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/dst/ase-version.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
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.
|
|
22
|
-
"@typescript-eslint/eslint-plugin": "8.59.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
53
|
+
"which": "7.0.0",
|
|
54
|
+
"update-notifier": "7.3.1"
|
|
53
55
|
},
|
|
54
56
|
"engines": {
|
|
55
57
|
"npm": ">=10.0.0",
|
|
56
|
-
"node": ">=
|
|
58
|
+
"node": ">=22.0.0"
|
|
57
59
|
},
|
|
58
60
|
"upd": [ "!eslint", "!@eslint/js" ],
|
|
59
61
|
"files": [
|