@rse/ase 0.0.48 → 0.0.50
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 +1 -4
- package/dst/ase-diagram.js +3 -3
- package/dst/ase-getopt.js +3 -3
- package/dst/ase-hook.js +86 -33
- package/dst/ase-mcp.js +26 -20
- package/dst/ase-service.js +8 -2
- package/dst/ase-statusline.js +19 -23
- package/dst/ase-task.js +60 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-code-lint.md +370 -0
- package/plugin/agents/ase-docs-proofread.md +100 -0
- package/plugin/agents/ase-meta-chat.md +38 -5
- package/plugin/agents/ase-meta-diagram.md +60 -0
- package/plugin/agents/ase-meta-search.md +3 -5
- package/plugin/meta/ase-persona.md +1 -1
- package/plugin/meta/ase-skill.md +7 -5
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +8 -7
- package/plugin/skills/ase-code-analyze/SKILL.md +2 -2
- package/plugin/skills/ase-code-craft/SKILL.md +12 -8
- package/plugin/skills/ase-code-explain/SKILL.md +7 -5
- package/plugin/skills/ase-code-insight/SKILL.md +7 -4
- package/plugin/skills/ase-code-lint/SKILL.md +179 -298
- package/plugin/skills/ase-code-refactor/SKILL.md +11 -7
- package/plugin/skills/ase-code-resolve/SKILL.md +18 -11
- package/plugin/skills/ase-docs-proofread/SKILL.md +29 -103
- package/plugin/skills/ase-meta-chat/SKILL.md +22 -38
- package/plugin/skills/ase-meta-evaluate/SKILL.md +1 -1
- package/plugin/skills/ase-meta-persona/SKILL.md +1 -1
- package/plugin/skills/ase-meta-quorum/SKILL.md +58 -27
- package/plugin/skills/ase-meta-search/SKILL.md +39 -13
- package/plugin/skills/ase-task-rename/SKILL.md +92 -0
- package/plugin/skills/ase-meta-diagram/SKILL.md +0 -101
package/dst/ase-config.js
CHANGED
|
@@ -336,10 +336,7 @@ export class Config {
|
|
|
336
336
|
doc.deleteIn(segs);
|
|
337
337
|
progressed = true;
|
|
338
338
|
}
|
|
339
|
-
|
|
340
|
-
/* root-level issue cannot be deleted; skip it and process
|
|
341
|
-
remaining issues so progressed is tracked correctly */
|
|
342
|
-
continue;
|
|
339
|
+
/* root-level issues cannot be deleted; processing continues with the remaining issues */
|
|
343
340
|
}
|
|
344
341
|
if (!progressed)
|
|
345
342
|
return;
|
package/dst/ase-diagram.js
CHANGED
|
@@ -278,9 +278,9 @@ export class DiagramMCP {
|
|
|
278
278
|
"Use for visualizing " +
|
|
279
279
|
"structure/layout/components/dependencies as a Flowchart, " +
|
|
280
280
|
"control-flow/branching/concurrency as a Flowchart, " +
|
|
281
|
-
"state-machine/states/transitions as
|
|
282
|
-
"data-flow/actors/messages/protocols as
|
|
283
|
-
"data-structure/classes/methods as
|
|
281
|
+
"state-machine/states/transitions as a UML State Diagram, " +
|
|
282
|
+
"data-flow/actors/messages/protocols as a UML Sequence Diagram, " +
|
|
283
|
+
"data-structure/classes/methods as a UML Class Diagram, " +
|
|
284
284
|
"data-model/entities/relationships as an ER Diagram, or " +
|
|
285
285
|
"metrics/distributions/time-series as an XY-Chart. " +
|
|
286
286
|
"Pass the Mermaid diagram specification as `diagram`. " +
|
package/dst/ase-getopt.js
CHANGED
|
@@ -108,9 +108,9 @@ export class GetoptMCP {
|
|
|
108
108
|
}
|
|
109
109
|
ranges.push({ start, end: i });
|
|
110
110
|
}
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
const first = ranges[
|
|
111
|
+
const consumed = argsVec.length - cmd.args.length;
|
|
112
|
+
if (cmd.args.length > 0 && consumed >= 0 && consumed < ranges.length) {
|
|
113
|
+
const first = ranges[consumed].start;
|
|
114
114
|
argsVerbatim = argsRaw.slice(first);
|
|
115
115
|
}
|
|
116
116
|
}
|
package/dst/ase-hook.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { execaSync } from "execa";
|
|
10
10
|
import { quote } from "shell-quote";
|
|
11
|
+
import * as v from "valibot";
|
|
11
12
|
import Version from "./ase-version.js";
|
|
12
13
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
13
14
|
const toolSpecs = {
|
|
@@ -36,10 +37,36 @@ export default class HookCommand {
|
|
|
36
37
|
constructor(log) {
|
|
37
38
|
this.log = log;
|
|
38
39
|
}
|
|
40
|
+
/* validate a session id against the accepted character set */
|
|
41
|
+
isValidSessionId(id) {
|
|
42
|
+
return /^[A-Za-z0-9._-]+$/.test(id);
|
|
43
|
+
}
|
|
44
|
+
/* best-effort JSON parse with valibot schema validation: returns
|
|
45
|
+
an empty object on blank input, malformed JSON, or schema
|
|
46
|
+
mismatch, so callers can treat the result uniformly. Extra
|
|
47
|
+
properties in the data are tolerated; only the declared schema
|
|
48
|
+
entries are required to match. */
|
|
49
|
+
parseJSON(text, schema) {
|
|
50
|
+
const empty = {};
|
|
51
|
+
if (text.trim() === "")
|
|
52
|
+
return empty;
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
catch (_e) {
|
|
58
|
+
/* best-effort: return empty object on malformed JSON */
|
|
59
|
+
return empty;
|
|
60
|
+
}
|
|
61
|
+
const result = v.safeParse(schema, raw);
|
|
62
|
+
if (!result.success)
|
|
63
|
+
return empty;
|
|
64
|
+
return result.output;
|
|
65
|
+
}
|
|
39
66
|
/* recursively expand "@<path>" file references in a Markdown text,
|
|
40
67
|
resolving paths relative to the directory of the containing file */
|
|
41
68
|
expandReferences(text, baseDir, visited = new Set()) {
|
|
42
|
-
return text.replace(/@(
|
|
69
|
+
return text.replace(/@(\S+)/g, (match, ref) => {
|
|
43
70
|
let resolved = ref;
|
|
44
71
|
if (resolved.startsWith("~/"))
|
|
45
72
|
resolved = path.join(process.env.HOME ?? "", resolved.slice(2));
|
|
@@ -71,10 +98,23 @@ export default class HookCommand {
|
|
|
71
98
|
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
72
99
|
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
73
100
|
/* read external files */
|
|
74
|
-
|
|
75
|
-
let md
|
|
101
|
+
let pkg;
|
|
102
|
+
let md;
|
|
103
|
+
try {
|
|
104
|
+
pkg = fs.readFileSync(filePkg, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch (_e) {
|
|
107
|
+
throw new Error(`failed to read plugin manifest: ${filePkg}`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
md = fs.readFileSync(fileMd, "utf8");
|
|
111
|
+
}
|
|
112
|
+
catch (_e) {
|
|
113
|
+
throw new Error(`failed to read constitution file: ${fileMd}`);
|
|
114
|
+
}
|
|
76
115
|
/* determine own version */
|
|
77
|
-
const
|
|
116
|
+
const pkgObj = this.parseJSON(pkg, v.object({ version: v.optional(v.string()) }));
|
|
117
|
+
const versionCurrentPlugin = pkgObj.version ?? "";
|
|
78
118
|
const versionCurrentTool = Version.current();
|
|
79
119
|
const versionLatestTool = await Version.latest();
|
|
80
120
|
/* sanity check situation */
|
|
@@ -90,11 +130,15 @@ export default class HookCommand {
|
|
|
90
130
|
/* read session information (Claude Code uses snake_case fields,
|
|
91
131
|
Copilot CLI uses camelCase fields) */
|
|
92
132
|
const stdin = fs.readFileSync(0, "utf8");
|
|
93
|
-
const input =
|
|
133
|
+
const input = this.parseJSON(stdin, v.object({
|
|
134
|
+
session_id: v.optional(v.string()),
|
|
135
|
+
sessionId: v.optional(v.string()),
|
|
136
|
+
cwd: v.optional(v.string())
|
|
137
|
+
}));
|
|
94
138
|
/* determine session id */
|
|
95
|
-
const sessionId =
|
|
139
|
+
const sessionId = this.pickSessionId(input);
|
|
96
140
|
/* establish config context (session-scoped only if a valid sessionId is present) */
|
|
97
|
-
const hasSession =
|
|
141
|
+
const hasSession = this.isValidSessionId(sessionId);
|
|
98
142
|
const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
|
|
99
143
|
cfg.lock(() => {
|
|
100
144
|
cfg.read();
|
|
@@ -103,6 +147,7 @@ export default class HookCommand {
|
|
|
103
147
|
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
104
148
|
if (hasSession)
|
|
105
149
|
cfg.lock(() => {
|
|
150
|
+
cfg.read();
|
|
106
151
|
cfg.set("agent.task", taskId);
|
|
107
152
|
cfg.write();
|
|
108
153
|
});
|
|
@@ -198,10 +243,8 @@ export default class HookCommand {
|
|
|
198
243
|
this.writeAgentStatus("ready");
|
|
199
244
|
/* safety net: clear any lingering "agent.skill" marker so a
|
|
200
245
|
crashed or aborted skill loop does not leave information active */
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
204
|
-
if (/^[A-Za-z0-9._-]+$/.test(sessionId)) {
|
|
246
|
+
const sessionId = this.readSessionIdFromStdin();
|
|
247
|
+
if (this.isValidSessionId(sessionId)) {
|
|
205
248
|
try {
|
|
206
249
|
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
|
|
207
250
|
cfg.lock(() => {
|
|
@@ -220,14 +263,10 @@ export default class HookCommand {
|
|
|
220
263
|
}
|
|
221
264
|
/* handler for "ase hook session-end" (both tools) */
|
|
222
265
|
doSessionEnd(_tool) {
|
|
223
|
-
/* read session information (Claude Code uses snake_case fields,
|
|
224
|
-
Copilot CLI uses camelCase fields) */
|
|
225
|
-
const stdin = fs.readFileSync(0, "utf8");
|
|
226
|
-
const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
|
|
227
266
|
/* determine session id */
|
|
228
|
-
const sessionId =
|
|
267
|
+
const sessionId = this.readSessionIdFromStdin();
|
|
229
268
|
/* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
|
|
230
|
-
if (
|
|
269
|
+
if (this.isValidSessionId(sessionId)) {
|
|
231
270
|
const dir = path.join(os.homedir(), ".ase", "session", sessionId);
|
|
232
271
|
try {
|
|
233
272
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -238,18 +277,32 @@ export default class HookCommand {
|
|
|
238
277
|
}
|
|
239
278
|
return 0;
|
|
240
279
|
}
|
|
280
|
+
/* pick the session id from a parsed payload (Claude Code uses
|
|
281
|
+
snake_case fields, Copilot CLI uses camelCase fields) */
|
|
282
|
+
pickSessionId(input) {
|
|
283
|
+
return input.session_id ?? input.sessionId ?? "";
|
|
284
|
+
}
|
|
285
|
+
/* read session id from stdin JSON payload */
|
|
286
|
+
readSessionIdFromStdin() {
|
|
287
|
+
const stdin = fs.readFileSync(0, "utf8");
|
|
288
|
+
const input = this.parseJSON(stdin, v.object({
|
|
289
|
+
session_id: v.optional(v.string()),
|
|
290
|
+
sessionId: v.optional(v.string())
|
|
291
|
+
}));
|
|
292
|
+
return this.pickSessionId(input);
|
|
293
|
+
}
|
|
241
294
|
/* read the session-scoped "agent.skill" config value */
|
|
242
295
|
readActiveSkill(sessionId) {
|
|
243
|
-
if (
|
|
296
|
+
if (!this.isValidSessionId(sessionId))
|
|
244
297
|
return "";
|
|
245
298
|
try {
|
|
246
299
|
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
|
|
247
300
|
let val = "";
|
|
248
301
|
cfg.lock(() => {
|
|
249
302
|
cfg.read();
|
|
250
|
-
const
|
|
251
|
-
if (typeof
|
|
252
|
-
val =
|
|
303
|
+
const skill = cfg.get("agent.skill");
|
|
304
|
+
if (typeof skill === "string")
|
|
305
|
+
val = skill;
|
|
253
306
|
});
|
|
254
307
|
return val;
|
|
255
308
|
}
|
|
@@ -262,21 +315,21 @@ export default class HookCommand {
|
|
|
262
315
|
const spec = toolSpecs[tool];
|
|
263
316
|
/* read tool invocation information */
|
|
264
317
|
const stdin = fs.readFileSync(0, "utf8");
|
|
265
|
-
const input =
|
|
318
|
+
const input = this.parseJSON(stdin, v.looseObject({
|
|
319
|
+
session_id: v.optional(v.string()),
|
|
320
|
+
sessionId: v.optional(v.string())
|
|
321
|
+
}));
|
|
266
322
|
/* determine whether to auto-approve the tool invocation
|
|
267
323
|
(field names and value shapes differ between tools) */
|
|
268
324
|
const toolName = typeof input[spec.toolNameField] === "string" ?
|
|
269
325
|
input[spec.toolNameField] : "";
|
|
270
326
|
let toolInput = {};
|
|
271
327
|
const rawInput = input[spec.toolInputField];
|
|
272
|
-
if (spec.toolInputIsString && typeof rawInput === "string")
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/* best-effort: leave toolInput empty on parse failure */
|
|
278
|
-
}
|
|
279
|
-
}
|
|
328
|
+
if (spec.toolInputIsString && typeof rawInput === "string")
|
|
329
|
+
toolInput = this.parseJSON(rawInput, v.object({
|
|
330
|
+
command: v.optional(v.string()),
|
|
331
|
+
skill: v.optional(v.string())
|
|
332
|
+
}));
|
|
280
333
|
else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
|
|
281
334
|
toolInput = rawInput;
|
|
282
335
|
let approve = false;
|
|
@@ -294,9 +347,9 @@ export default class HookCommand {
|
|
|
294
347
|
reason = "ASE MCP tool invocation auto-approved";
|
|
295
348
|
}
|
|
296
349
|
else if (toolName === "Edit") {
|
|
297
|
-
const sessionId =
|
|
350
|
+
const sessionId = this.pickSessionId(input);
|
|
298
351
|
const activeSkill = this.readActiveSkill(sessionId);
|
|
299
|
-
if (activeSkill === "ase-docs-proofread") {
|
|
352
|
+
if (activeSkill === "ase-docs-proofread" || activeSkill === "ase-code-lint") {
|
|
300
353
|
approve = true;
|
|
301
354
|
reason = `${activeSkill}: user already consented via AskUserQuestion`;
|
|
302
355
|
}
|
|
@@ -329,7 +382,7 @@ export default class HookCommand {
|
|
|
329
382
|
register(program) {
|
|
330
383
|
/* default for --tool derived from ASE_TOOL environment variable */
|
|
331
384
|
const envTool = process.env.ASE_TOOL ?? "";
|
|
332
|
-
const toolDflt = envTool !== "" ? envTool : "claude";
|
|
385
|
+
const toolDflt = envTool !== "" ? this.parseTool(envTool) : "claude";
|
|
333
386
|
/* register CLI top-level command "ase hook" */
|
|
334
387
|
const hookCmd = program
|
|
335
388
|
.command("hook")
|
package/dst/ase-mcp.js
CHANGED
|
@@ -26,9 +26,9 @@ export default class MCPCommand {
|
|
|
26
26
|
const projectId = rawId ?? path.basename(process.cwd());
|
|
27
27
|
const rawPort = svc.get("port");
|
|
28
28
|
const port = rawPort ?? null;
|
|
29
|
-
return { projectId, port
|
|
29
|
+
return { projectId, port };
|
|
30
30
|
}
|
|
31
|
-
/*
|
|
31
|
+
/* run "ase service start" and wait for the service to come up */
|
|
32
32
|
async ensureService() {
|
|
33
33
|
let ctx = this.loadContext();
|
|
34
34
|
/* fast path: already running */
|
|
@@ -52,6 +52,10 @@ export default class MCPCommand {
|
|
|
52
52
|
throw new Error(`mcp: service not responding on port ${ctx.port} after start`);
|
|
53
53
|
return { projectId: ctx.projectId, port: ctx.port };
|
|
54
54
|
}
|
|
55
|
+
/* coerce an unknown thrown value into an Error */
|
|
56
|
+
asError(e) {
|
|
57
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
58
|
+
}
|
|
55
59
|
/* bridge stdio to a Streamable HTTP MCP endpoint on the local service */
|
|
56
60
|
async runBridge() {
|
|
57
61
|
/* ensure the service is running */
|
|
@@ -60,8 +64,9 @@ export default class MCPCommand {
|
|
|
60
64
|
const server = new StdioServerTransport();
|
|
61
65
|
/* track active client and bridge-level closed state */
|
|
62
66
|
let client = null;
|
|
63
|
-
let closedByUs = false; /*
|
|
64
|
-
let bridgeDone = false; /*
|
|
67
|
+
let closedByUs = false; /* set when we initiated the client close */
|
|
68
|
+
let bridgeDone = false; /* set when stdio side closes */
|
|
69
|
+
let reconnecting = false; /* set while a reconnect chain is active */
|
|
65
70
|
/* cleanly shut down the whole bridge */
|
|
66
71
|
const shutdown = async () => {
|
|
67
72
|
if (bridgeDone)
|
|
@@ -79,11 +84,9 @@ export default class MCPCommand {
|
|
|
79
84
|
const connectClient = async () => {
|
|
80
85
|
const url = new URL(`http://${HOST}:${port}/mcp`);
|
|
81
86
|
const next = new StreamableHTTPClientTransport(url);
|
|
82
|
-
client = next;
|
|
83
87
|
next.onmessage = (msg) => {
|
|
84
|
-
server.send(msg).catch((
|
|
85
|
-
|
|
86
|
-
this.log.write("error", `mcp: stdout send: ${err.message}`);
|
|
88
|
+
server.send(msg).catch((err) => {
|
|
89
|
+
this.log.write("error", `mcp: stdout send: ${this.asError(err).message}`);
|
|
87
90
|
});
|
|
88
91
|
};
|
|
89
92
|
next.onerror = (err) => {
|
|
@@ -91,12 +94,14 @@ export default class MCPCommand {
|
|
|
91
94
|
};
|
|
92
95
|
/* service closed the connection — try to recover */
|
|
93
96
|
next.onclose = () => {
|
|
94
|
-
if (closedByUs || bridgeDone)
|
|
97
|
+
if (client !== next || closedByUs || bridgeDone || reconnecting)
|
|
95
98
|
return;
|
|
99
|
+
reconnecting = true;
|
|
96
100
|
this.log.write("warning", "mcp: http connection lost — reconnecting");
|
|
97
|
-
reconnect().catch(() => { });
|
|
101
|
+
reconnect(0, () => { reconnecting = false; }).catch(() => { });
|
|
98
102
|
};
|
|
99
103
|
await next.start();
|
|
104
|
+
client = next;
|
|
100
105
|
};
|
|
101
106
|
/* reconnect loop: restart service if needed, then reconnect client */
|
|
102
107
|
const reconnect = async (attempt = 0, done) => {
|
|
@@ -111,22 +116,21 @@ export default class MCPCommand {
|
|
|
111
116
|
port = ctx.port;
|
|
112
117
|
closedByUs = true;
|
|
113
118
|
await client?.close();
|
|
114
|
-
closedByUs = false;
|
|
115
119
|
await connectClient();
|
|
120
|
+
closedByUs = false;
|
|
116
121
|
this.log.write("info", "mcp: reconnected to service");
|
|
117
122
|
done?.();
|
|
118
123
|
}
|
|
119
|
-
catch (
|
|
120
|
-
|
|
121
|
-
this.log.write("error", `mcp: reconnect failed: ${err.message}`);
|
|
124
|
+
catch (err) {
|
|
125
|
+
closedByUs = false;
|
|
126
|
+
this.log.write("error", `mcp: reconnect failed: ${this.asError(err).message}`);
|
|
122
127
|
reconnect(attempt + 1, done).catch(() => { });
|
|
123
128
|
}
|
|
124
129
|
};
|
|
125
130
|
/* wire stdio server */
|
|
126
131
|
server.onmessage = (msg) => {
|
|
127
|
-
client?.send(msg).catch((
|
|
128
|
-
|
|
129
|
-
this.log.write("error", `mcp: http send: ${err.message}`);
|
|
132
|
+
client?.send(msg).catch((err) => {
|
|
133
|
+
this.log.write("error", `mcp: http send: ${this.asError(err).message}`);
|
|
130
134
|
});
|
|
131
135
|
};
|
|
132
136
|
server.onerror = (err) => {
|
|
@@ -140,7 +144,6 @@ export default class MCPCommand {
|
|
|
140
144
|
await connectClient();
|
|
141
145
|
/* periodically probe the service; trigger reconnect if it is gone */
|
|
142
146
|
const HEALTH_INTERVAL_MS = 30_000;
|
|
143
|
-
let reconnecting = false;
|
|
144
147
|
const healthTimer = setInterval(async () => {
|
|
145
148
|
if (bridgeDone || reconnecting)
|
|
146
149
|
return;
|
|
@@ -153,7 +156,10 @@ export default class MCPCommand {
|
|
|
153
156
|
reconnect(0, () => { reconnecting = false; }).catch(() => { });
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
|
-
catch {
|
|
159
|
+
catch (err) {
|
|
160
|
+
/* ignore transient probe/context errors but record them */
|
|
161
|
+
this.log.write("debug", `mcp: health check error: ${this.asError(err).message}`);
|
|
162
|
+
}
|
|
157
163
|
}, HEALTH_INTERVAL_MS);
|
|
158
164
|
healthTimer.unref();
|
|
159
165
|
/* await stdio to be closed */
|
|
@@ -165,7 +171,7 @@ export default class MCPCommand {
|
|
|
165
171
|
/* shutdown services */
|
|
166
172
|
clearInterval(healthTimer);
|
|
167
173
|
await shutdown();
|
|
168
|
-
return 0;
|
|
174
|
+
return 0; /* unreachable, kept only to satisfy the Promise<number> return type */
|
|
169
175
|
}
|
|
170
176
|
/* register commands */
|
|
171
177
|
register(program) {
|
package/dst/ase-service.js
CHANGED
|
@@ -258,7 +258,13 @@ export default class ServiceCommand {
|
|
|
258
258
|
handler: (_request, h) => {
|
|
259
259
|
this.log.write("info", "service: stop requested");
|
|
260
260
|
setImmediate(async () => {
|
|
261
|
-
|
|
261
|
+
try {
|
|
262
|
+
await server.stop({ timeout: 1000 });
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const e = err;
|
|
266
|
+
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
267
|
+
}
|
|
262
268
|
process.exit(0);
|
|
263
269
|
});
|
|
264
270
|
return h.response({ ok: true }).code(200);
|
|
@@ -439,7 +445,6 @@ export default class ServiceCommand {
|
|
|
439
445
|
lastErr = new Error(`${reason}${detail}`);
|
|
440
446
|
}
|
|
441
447
|
finally {
|
|
442
|
-
child.removeListener("exit", onExit);
|
|
443
448
|
if (!success && !exited) {
|
|
444
449
|
child.kill("SIGTERM");
|
|
445
450
|
await Promise.race([
|
|
@@ -455,6 +460,7 @@ export default class ServiceCommand {
|
|
|
455
460
|
child.unref();
|
|
456
461
|
}
|
|
457
462
|
}
|
|
463
|
+
child.removeListener("exit", onExit);
|
|
458
464
|
if (!success)
|
|
459
465
|
Service.clearPort(ctx.svc);
|
|
460
466
|
}
|
package/dst/ase-statusline.js
CHANGED
|
@@ -175,7 +175,7 @@ export default class StatuslineCommand {
|
|
|
175
175
|
/* parse and validate the --tool option */
|
|
176
176
|
parseTool(value) {
|
|
177
177
|
if (value !== "claude" && value !== "copilot")
|
|
178
|
-
throw new
|
|
178
|
+
throw new InvalidArgumentError(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
|
|
179
179
|
return value;
|
|
180
180
|
}
|
|
181
181
|
/* register commands */
|
|
@@ -191,8 +191,8 @@ export default class StatuslineCommand {
|
|
|
191
191
|
.option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseInteger("--margin"), 2)
|
|
192
192
|
.option("--no-icons", "disable icons in placeholder rendering")
|
|
193
193
|
.option("--no-labels", "disable labels in front of bold values")
|
|
194
|
-
.argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %P %c %C %
|
|
195
|
-
"%S %D %W %Q %H %X %b %g %G %d %M %V
|
|
194
|
+
.argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %c %C %a %r " +
|
|
195
|
+
"%S %D %W %Q %H %X %b %g %G %d %M %V placeholders and <color>...</color> markup " +
|
|
196
196
|
"(color: black, red, green, yellow, blue, magenta, cyan, white, default) " +
|
|
197
197
|
"(default: single line \"%m %e %t\")")
|
|
198
198
|
.action(async (lines, opts) => {
|
|
@@ -357,7 +357,7 @@ export default class StatuslineCommand {
|
|
|
357
357
|
S: () => {
|
|
358
358
|
const pct5h = data.rate_limits?.five_hour?.used_percentage;
|
|
359
359
|
if (pct5h !== undefined)
|
|
360
|
-
emit(`${prefix("⏲", "session")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
|
|
360
|
+
emit(`${prefix("⏲", "session-usage")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
|
|
361
361
|
},
|
|
362
362
|
D: () => {
|
|
363
363
|
const until5h = data.rate_limits?.five_hour?.resets_at ?? "";
|
|
@@ -368,7 +368,7 @@ export default class StatuslineCommand {
|
|
|
368
368
|
W: () => {
|
|
369
369
|
const pctWk = data.rate_limits?.seven_day?.used_percentage;
|
|
370
370
|
if (pctWk !== undefined)
|
|
371
|
-
emit(`${prefix("⏲", "weekly")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
|
|
371
|
+
emit(`${prefix("⏲", "weekly-usage")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
|
|
372
372
|
},
|
|
373
373
|
Q: () => {
|
|
374
374
|
const untilWk = data.rate_limits?.seven_day?.resets_at ?? "";
|
|
@@ -390,11 +390,11 @@ export default class StatuslineCommand {
|
|
|
390
390
|
/* ==== VERSION CONTROL ==== */
|
|
391
391
|
a: () => {
|
|
392
392
|
const linesAdded = data.cost?.total_lines_added ?? 0;
|
|
393
|
-
emit(`${prefix("⊕", "added")}${c.bold(linesAdded)}`);
|
|
393
|
+
emit(`${prefix("⊕", "added")}${c.bold(String(linesAdded))}`);
|
|
394
394
|
},
|
|
395
395
|
r: () => {
|
|
396
396
|
const linesRemoved = data.cost?.total_lines_removed ?? 0;
|
|
397
|
-
emit(`${prefix("⊖", "removed")}${c.bold(linesRemoved)}`);
|
|
397
|
+
emit(`${prefix("⊖", "removed")}${c.bold(String(linesRemoved))}`);
|
|
398
398
|
},
|
|
399
399
|
b: () => {
|
|
400
400
|
const g = getGit();
|
|
@@ -435,6 +435,15 @@ export default class StatuslineCommand {
|
|
|
435
435
|
}
|
|
436
436
|
};
|
|
437
437
|
/* walk each template line and render */
|
|
438
|
+
const closeSpan = () => {
|
|
439
|
+
if (span !== null) {
|
|
440
|
+
const wrapped = span.color === "default" ?
|
|
441
|
+
span.buf :
|
|
442
|
+
(c[span.color])(span.buf);
|
|
443
|
+
span = null;
|
|
444
|
+
appendOutput(wrapped);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
438
447
|
for (const line of tmpl) {
|
|
439
448
|
let i = 0;
|
|
440
449
|
while (i < line.length) {
|
|
@@ -443,15 +452,8 @@ export default class StatuslineCommand {
|
|
|
443
452
|
if (ch === "<") {
|
|
444
453
|
const m = line.slice(i).match(/^<(\/?)([a-z]+)>/);
|
|
445
454
|
if (m !== null && COLORS.has(m[2])) {
|
|
446
|
-
if (m[1] === "/")
|
|
447
|
-
|
|
448
|
-
const wrapped = span.color === "default" ?
|
|
449
|
-
span.buf :
|
|
450
|
-
(c[span.color])(span.buf);
|
|
451
|
-
span = null;
|
|
452
|
-
appendOutput(wrapped);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
+
if (m[1] === "/")
|
|
456
|
+
closeSpan();
|
|
455
457
|
else if (span === null)
|
|
456
458
|
span = { color: m[2], buf: "" };
|
|
457
459
|
i += m[0].length;
|
|
@@ -468,13 +470,7 @@ export default class StatuslineCommand {
|
|
|
468
470
|
}
|
|
469
471
|
}
|
|
470
472
|
/* flush any unterminated span at end of line */
|
|
471
|
-
|
|
472
|
-
const wrapped = span.color === "default" ?
|
|
473
|
-
span.buf :
|
|
474
|
-
(c[span.color])(span.buf);
|
|
475
|
-
span = null;
|
|
476
|
-
appendOutput(wrapped);
|
|
477
|
-
}
|
|
473
|
+
closeSpan();
|
|
478
474
|
out += "\n";
|
|
479
475
|
col = 0;
|
|
480
476
|
}
|
package/dst/ase-task.js
CHANGED
|
@@ -69,6 +69,21 @@ export class Task {
|
|
|
69
69
|
fs.rmSync(path.dirname(file), { recursive: true, force: true });
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
|
+
/* rename a task by moving the entire task home directory
|
|
73
|
+
<project>/.ase/task/<oldId>/ to <project>/.ase/task/<newId>/;
|
|
74
|
+
returns true on success, false if the source task does not exist;
|
|
75
|
+
throws if the target id already exists */
|
|
76
|
+
static rename(oldId, newId) {
|
|
77
|
+
const oldDir = path.dirname(Task.path(oldId));
|
|
78
|
+
const newDir = path.dirname(Task.path(newId));
|
|
79
|
+
if (!fs.existsSync(oldDir))
|
|
80
|
+
return false;
|
|
81
|
+
if (fs.existsSync(newDir))
|
|
82
|
+
throw new Error(`task: target id "${newId}" already exists`);
|
|
83
|
+
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
|
84
|
+
fs.renameSync(oldDir, newDir);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
72
87
|
/* list all persisted tasks in lexicographic id order; if verbose is true,
|
|
73
88
|
each entry's `mtime` is set to the `plan.md` modification time formatted
|
|
74
89
|
as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
|
|
@@ -227,6 +242,20 @@ export default class TaskCommand {
|
|
|
227
242
|
this.log.write("info", `task: no task "${id}" to remove`);
|
|
228
243
|
process.exit(removed ? 0 : 1);
|
|
229
244
|
});
|
|
245
|
+
/* register CLI sub-command "ase task rename" */
|
|
246
|
+
task
|
|
247
|
+
.command("rename")
|
|
248
|
+
.description("Rename a task from <old> to <new>")
|
|
249
|
+
.argument("<old>", "Old task identifier")
|
|
250
|
+
.argument("<new>", "New task identifier")
|
|
251
|
+
.action((oldId, newId) => {
|
|
252
|
+
const renamed = Task.rename(oldId, newId);
|
|
253
|
+
if (renamed)
|
|
254
|
+
this.log.write("info", `task: renamed "${oldId}" to "${newId}"`);
|
|
255
|
+
else
|
|
256
|
+
this.log.write("info", `task: no task "${oldId}" to rename`);
|
|
257
|
+
process.exit(renamed ? 0 : 1);
|
|
258
|
+
});
|
|
230
259
|
/* register CLI sub-command "ase task purge" */
|
|
231
260
|
task
|
|
232
261
|
.command("purge")
|
|
@@ -289,7 +318,7 @@ export class TaskMCP {
|
|
|
289
318
|
const verbose = args.verbose ?? false;
|
|
290
319
|
const items = Task.list(verbose);
|
|
291
320
|
const tasks = verbose ?
|
|
292
|
-
items.map((item) => ({ id: item.id, mtime: item.mtime })) :
|
|
321
|
+
items.map((item) => ({ id: item.id, mtime: item.mtime ?? "" })) :
|
|
293
322
|
items.map((item) => ({ id: item.id }));
|
|
294
323
|
const result = { tasks };
|
|
295
324
|
return {
|
|
@@ -381,6 +410,36 @@ export class TaskMCP {
|
|
|
381
410
|
};
|
|
382
411
|
}
|
|
383
412
|
});
|
|
413
|
+
/* task rename */
|
|
414
|
+
mcp.registerTool("task_rename", {
|
|
415
|
+
title: "ASE task rename",
|
|
416
|
+
description: "Rename a previously persisted task from `old` to `new` by atomically moving the " +
|
|
417
|
+
"task home directory. Returns a status `text` indicating whether the rename succeeded. " +
|
|
418
|
+
"Fails with an error if the target id already exists.",
|
|
419
|
+
inputSchema: {
|
|
420
|
+
old: z.string()
|
|
421
|
+
.describe("old task identifier (allowed characters: A-Z, a-z, 0-9, '-')"),
|
|
422
|
+
new: z.string()
|
|
423
|
+
.describe("new task identifier (allowed characters: A-Z, a-z, 0-9, '-')")
|
|
424
|
+
}
|
|
425
|
+
}, async (args) => {
|
|
426
|
+
try {
|
|
427
|
+
const renamed = Task.rename(args.old, args.new);
|
|
428
|
+
const msg = renamed ?
|
|
429
|
+
`task_rename: OK: renamed task "${args.old}" to "${args.new}"` :
|
|
430
|
+
"WARNING: task not found";
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: msg }]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
437
|
+
return {
|
|
438
|
+
isError: true,
|
|
439
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
});
|
|
384
443
|
/* task id get/set */
|
|
385
444
|
mcp.registerTool("task_id", {
|
|
386
445
|
title: "ASE task id get/set",
|
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.50",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|