@rse/ase 0.0.48 → 0.0.49
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 +85 -33
- package/dst/ase-mcp.js +26 -20
- package/dst/ase-service.js +8 -2
- package/dst/ase-statusline.js +17 -21
- 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/meta/ase-persona.md +1 -1
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +2 -2
- package/plugin/skills/ase-code-explain/SKILL.md +1 -1
- package/plugin/skills/ase-code-lint/SKILL.md +179 -298
- package/plugin/skills/ase-docs-proofread/SKILL.md +29 -103
- 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 +1 -1
- package/plugin/skills/ase-task-rename/SKILL.md +92 -0
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();
|
|
@@ -198,10 +242,8 @@ export default class HookCommand {
|
|
|
198
242
|
this.writeAgentStatus("ready");
|
|
199
243
|
/* safety net: clear any lingering "agent.skill" marker so a
|
|
200
244
|
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)) {
|
|
245
|
+
const sessionId = this.readSessionIdFromStdin();
|
|
246
|
+
if (this.isValidSessionId(sessionId)) {
|
|
205
247
|
try {
|
|
206
248
|
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
|
|
207
249
|
cfg.lock(() => {
|
|
@@ -220,14 +262,10 @@ export default class HookCommand {
|
|
|
220
262
|
}
|
|
221
263
|
/* handler for "ase hook session-end" (both tools) */
|
|
222
264
|
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
265
|
/* determine session id */
|
|
228
|
-
const sessionId =
|
|
266
|
+
const sessionId = this.readSessionIdFromStdin();
|
|
229
267
|
/* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
|
|
230
|
-
if (
|
|
268
|
+
if (this.isValidSessionId(sessionId)) {
|
|
231
269
|
const dir = path.join(os.homedir(), ".ase", "session", sessionId);
|
|
232
270
|
try {
|
|
233
271
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -238,18 +276,32 @@ export default class HookCommand {
|
|
|
238
276
|
}
|
|
239
277
|
return 0;
|
|
240
278
|
}
|
|
279
|
+
/* pick the session id from a parsed payload (Claude Code uses
|
|
280
|
+
snake_case fields, Copilot CLI uses camelCase fields) */
|
|
281
|
+
pickSessionId(input) {
|
|
282
|
+
return input.session_id ?? input.sessionId ?? "";
|
|
283
|
+
}
|
|
284
|
+
/* read session id from stdin JSON payload */
|
|
285
|
+
readSessionIdFromStdin() {
|
|
286
|
+
const stdin = fs.readFileSync(0, "utf8");
|
|
287
|
+
const input = this.parseJSON(stdin, v.object({
|
|
288
|
+
session_id: v.optional(v.string()),
|
|
289
|
+
sessionId: v.optional(v.string())
|
|
290
|
+
}));
|
|
291
|
+
return this.pickSessionId(input);
|
|
292
|
+
}
|
|
241
293
|
/* read the session-scoped "agent.skill" config value */
|
|
242
294
|
readActiveSkill(sessionId) {
|
|
243
|
-
if (
|
|
295
|
+
if (!this.isValidSessionId(sessionId))
|
|
244
296
|
return "";
|
|
245
297
|
try {
|
|
246
298
|
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
|
|
247
299
|
let val = "";
|
|
248
300
|
cfg.lock(() => {
|
|
249
301
|
cfg.read();
|
|
250
|
-
const
|
|
251
|
-
if (typeof
|
|
252
|
-
val =
|
|
302
|
+
const skill = cfg.get("agent.skill");
|
|
303
|
+
if (typeof skill === "string")
|
|
304
|
+
val = skill;
|
|
253
305
|
});
|
|
254
306
|
return val;
|
|
255
307
|
}
|
|
@@ -262,21 +314,21 @@ export default class HookCommand {
|
|
|
262
314
|
const spec = toolSpecs[tool];
|
|
263
315
|
/* read tool invocation information */
|
|
264
316
|
const stdin = fs.readFileSync(0, "utf8");
|
|
265
|
-
const input =
|
|
317
|
+
const input = this.parseJSON(stdin, v.looseObject({
|
|
318
|
+
session_id: v.optional(v.string()),
|
|
319
|
+
sessionId: v.optional(v.string())
|
|
320
|
+
}));
|
|
266
321
|
/* determine whether to auto-approve the tool invocation
|
|
267
322
|
(field names and value shapes differ between tools) */
|
|
268
323
|
const toolName = typeof input[spec.toolNameField] === "string" ?
|
|
269
324
|
input[spec.toolNameField] : "";
|
|
270
325
|
let toolInput = {};
|
|
271
326
|
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
|
-
}
|
|
327
|
+
if (spec.toolInputIsString && typeof rawInput === "string")
|
|
328
|
+
toolInput = this.parseJSON(rawInput, v.object({
|
|
329
|
+
command: v.optional(v.string()),
|
|
330
|
+
skill: v.optional(v.string())
|
|
331
|
+
}));
|
|
280
332
|
else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
|
|
281
333
|
toolInput = rawInput;
|
|
282
334
|
let approve = false;
|
|
@@ -294,9 +346,9 @@ export default class HookCommand {
|
|
|
294
346
|
reason = "ASE MCP tool invocation auto-approved";
|
|
295
347
|
}
|
|
296
348
|
else if (toolName === "Edit") {
|
|
297
|
-
const sessionId =
|
|
349
|
+
const sessionId = this.pickSessionId(input);
|
|
298
350
|
const activeSkill = this.readActiveSkill(sessionId);
|
|
299
|
-
if (activeSkill === "ase-docs-proofread") {
|
|
351
|
+
if (activeSkill === "ase-docs-proofread" || activeSkill === "ase-code-lint") {
|
|
300
352
|
approve = true;
|
|
301
353
|
reason = `${activeSkill}: user already consented via AskUserQuestion`;
|
|
302
354
|
}
|
|
@@ -329,7 +381,7 @@ export default class HookCommand {
|
|
|
329
381
|
register(program) {
|
|
330
382
|
/* default for --tool derived from ASE_TOOL environment variable */
|
|
331
383
|
const envTool = process.env.ASE_TOOL ?? "";
|
|
332
|
-
const toolDflt = envTool !== "" ? envTool : "claude";
|
|
384
|
+
const toolDflt = envTool !== "" ? this.parseTool(envTool) : "claude";
|
|
333
385
|
/* register CLI top-level command "ase hook" */
|
|
334
386
|
const hookCmd = program
|
|
335
387
|
.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 */
|
|
@@ -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.49",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|