@rse/ase 0.0.26 → 0.0.28
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-diagram.js +184 -115
- package/dst/ase-hook.js +42 -10
- package/dst/ase-mcp.js +1 -32
- package/dst/ase-persona.js +87 -0
- package/dst/ase-service-probe.js +38 -0
- package/dst/ase-service.js +168 -401
- package/dst/ase-setup.js +6 -5
- package/dst/ase-statusline.js +13 -3
- package/dst/ase-task.js +314 -95
- package/dst/ase.js +3 -3
- package/package.json +3 -3
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-diagram.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import { InvalidArgumentError } from "commander";
|
|
8
8
|
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
9
|
+
import { z } from "zod";
|
|
9
10
|
/* custom argument parser for Commander: non-negative integer */
|
|
10
11
|
const parseInteger = (name) => (value) => {
|
|
11
12
|
const n = Number.parseInt(value, 10);
|
|
@@ -19,57 +20,6 @@ const parseColorMode = (name) => (value) => {
|
|
|
19
20
|
throw new InvalidArgumentError(`${name} must be "none", "ansi16", or "ansi256"`);
|
|
20
21
|
return value;
|
|
21
22
|
};
|
|
22
|
-
/* detect terminal column width */
|
|
23
|
-
export const detectTermWidth = () => {
|
|
24
|
-
let width = 0;
|
|
25
|
-
/* attempt 1: query environment variable */
|
|
26
|
-
if (process.env.ASE_TERM_WIDTH !== undefined) {
|
|
27
|
-
const cols = Number.parseInt(process.env.ASE_TERM_WIDTH, 10);
|
|
28
|
-
if (Number.isFinite(cols) && cols > 0)
|
|
29
|
-
width = cols;
|
|
30
|
-
}
|
|
31
|
-
/* attempt 2: query stdout */
|
|
32
|
-
if (width === 0 && process.stdout.isTTY) {
|
|
33
|
-
const cols = process.stdout.columns;
|
|
34
|
-
if (typeof cols === "number" && cols > 0)
|
|
35
|
-
width = cols;
|
|
36
|
-
}
|
|
37
|
-
return width;
|
|
38
|
-
};
|
|
39
|
-
/* detect terminal row height */
|
|
40
|
-
export const detectTermHeight = () => {
|
|
41
|
-
let height = 0;
|
|
42
|
-
/* attempt 1: query environment variable */
|
|
43
|
-
if (process.env.ASE_TERM_HEIGHT !== undefined) {
|
|
44
|
-
const rows = Number.parseInt(process.env.ASE_TERM_HEIGHT, 10);
|
|
45
|
-
if (Number.isFinite(rows) && rows > 0)
|
|
46
|
-
height = rows;
|
|
47
|
-
}
|
|
48
|
-
/* attempt 2: query stdout */
|
|
49
|
-
if (height === 0 && process.stdout.isTTY) {
|
|
50
|
-
const rows = process.stdout.rows;
|
|
51
|
-
if (typeof rows === "number" && rows > 0)
|
|
52
|
-
height = rows;
|
|
53
|
-
}
|
|
54
|
-
return height;
|
|
55
|
-
};
|
|
56
|
-
/* detect terminal color capability */
|
|
57
|
-
const detectColorMode = () => {
|
|
58
|
-
let mode = "none";
|
|
59
|
-
/* attempt 1: query environment variable (explicitly) */
|
|
60
|
-
if (process.env.ASE_TERM_COLORS !== undefined)
|
|
61
|
-
if (process.env.ASE_TERM_COLORS.match(/^(?:none|ansi16|ansi256)$/) !== null)
|
|
62
|
-
mode = process.env.ASE_TERM_COLORS;
|
|
63
|
-
/* attempt 2: query stdout */
|
|
64
|
-
if (mode === "none" && process.stdout.isTTY) {
|
|
65
|
-
const depth = process.stdout.getColorDepth();
|
|
66
|
-
if (depth >= 8)
|
|
67
|
-
mode = "ansi256";
|
|
68
|
-
else if (depth >= 4)
|
|
69
|
-
mode = "ansi16";
|
|
70
|
-
}
|
|
71
|
-
return mode;
|
|
72
|
-
};
|
|
73
23
|
/* truncate a single rendered line to a maximum visible column,
|
|
74
24
|
preserving ANSI escape sequences (CSI ...m) and appending an ANSI
|
|
75
25
|
reset sequence if any styling was active at the truncation point */
|
|
@@ -135,70 +85,124 @@ const visibleWidth = (line) => {
|
|
|
135
85
|
}
|
|
136
86
|
return visible;
|
|
137
87
|
};
|
|
138
|
-
/*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
theme: opts.colorMode !== "none" ? {
|
|
149
|
-
fg: "#000000",
|
|
150
|
-
border: "#a0a0a0",
|
|
151
|
-
junction: "#a0a0a0",
|
|
152
|
-
arrow: "#404040",
|
|
153
|
-
line: "#707070",
|
|
154
|
-
corner: "#707070"
|
|
155
|
-
} : {
|
|
156
|
-
fg: "#000000",
|
|
157
|
-
border: "#000000",
|
|
158
|
-
junction: "#000000",
|
|
159
|
-
arrow: "#000000",
|
|
160
|
-
line: "#000000",
|
|
161
|
-
corner: "#000000"
|
|
88
|
+
/* reusable functionality: Mermaid diagram rendering as Unicode/ASCII art */
|
|
89
|
+
export class Diagram {
|
|
90
|
+
/* detect terminal column width */
|
|
91
|
+
static detectTermWidth() {
|
|
92
|
+
let width = 0;
|
|
93
|
+
/* attempt 1: query environment variable */
|
|
94
|
+
if (process.env.ASE_TERM_WIDTH !== undefined) {
|
|
95
|
+
const cols = Number.parseInt(process.env.ASE_TERM_WIDTH, 10);
|
|
96
|
+
if (Number.isFinite(cols) && cols > 0)
|
|
97
|
+
width = cols;
|
|
162
98
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const maxWidth = termWidth > 0 ? termWidth - opts.diagramClipX : 0;
|
|
169
|
-
const maxHeight = termHeight > 0 ? termHeight - opts.diagramClipY : 0;
|
|
170
|
-
const trailingNL = out.endsWith("\n");
|
|
171
|
-
let lines = (trailingNL ? out.slice(0, -1) : out).split("\n");
|
|
172
|
-
let widthWarn = "";
|
|
173
|
-
let heightWarn = "";
|
|
174
|
-
if (maxWidth > 0) {
|
|
175
|
-
const widest = lines.reduce((m, l) => Math.max(m, visibleWidth(l)), 0);
|
|
176
|
-
if (widest > maxWidth)
|
|
177
|
-
widthWarn =
|
|
178
|
-
`ase diagram: WARNING: rendered diagram width ${widest} exceeds budget ${maxWidth}; ` +
|
|
179
|
-
"rightmost content was clipped. Please regenerate the Mermaid source to fit " +
|
|
180
|
-
`within ${maxWidth} chars by preferring a portrait orientation ` +
|
|
181
|
-
"(\"flowchart TB\", top-to-bottom) over landscape (\"LR\"/\"RL\"/\"BT\"), " +
|
|
182
|
-
"reducing siblings per row, abbreviating node labels, or restructuring " +
|
|
183
|
-
"into nested subgraph hierarchies.";
|
|
184
|
-
lines = lines.map((l) => truncateAnsiLine(l, maxWidth));
|
|
99
|
+
/* attempt 2: query stdout */
|
|
100
|
+
if (width === 0 && process.stdout.isTTY) {
|
|
101
|
+
const cols = process.stdout.columns;
|
|
102
|
+
if (typeof cols === "number" && cols > 0)
|
|
103
|
+
width = cols;
|
|
185
104
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
105
|
+
return width;
|
|
106
|
+
}
|
|
107
|
+
/* detect terminal row height */
|
|
108
|
+
static detectTermHeight() {
|
|
109
|
+
let height = 0;
|
|
110
|
+
/* attempt 1: query environment variable */
|
|
111
|
+
if (process.env.ASE_TERM_HEIGHT !== undefined) {
|
|
112
|
+
const rows = Number.parseInt(process.env.ASE_TERM_HEIGHT, 10);
|
|
113
|
+
if (Number.isFinite(rows) && rows > 0)
|
|
114
|
+
height = rows;
|
|
115
|
+
}
|
|
116
|
+
/* attempt 2: query stdout */
|
|
117
|
+
if (height === 0 && process.stdout.isTTY) {
|
|
118
|
+
const rows = process.stdout.rows;
|
|
119
|
+
if (typeof rows === "number" && rows > 0)
|
|
120
|
+
height = rows;
|
|
193
121
|
}
|
|
194
|
-
|
|
195
|
-
if (widthWarn !== "")
|
|
196
|
-
out += "\n" + widthWarn + "\n";
|
|
197
|
-
if (heightWarn !== "")
|
|
198
|
-
out += "\n" + heightWarn + "\n";
|
|
122
|
+
return height;
|
|
199
123
|
}
|
|
200
|
-
|
|
201
|
-
|
|
124
|
+
/* detect terminal color capability */
|
|
125
|
+
static detectColorMode() {
|
|
126
|
+
let mode = "none";
|
|
127
|
+
/* attempt 1: query environment variable (explicitly) */
|
|
128
|
+
if (process.env.ASE_TERM_COLORS !== undefined)
|
|
129
|
+
if (process.env.ASE_TERM_COLORS.match(/^(?:none|ansi16|ansi256)$/) !== null)
|
|
130
|
+
mode = process.env.ASE_TERM_COLORS;
|
|
131
|
+
/* attempt 2: query stdout */
|
|
132
|
+
if (mode === "none" && process.stdout.isTTY) {
|
|
133
|
+
const depth = process.stdout.getColorDepth();
|
|
134
|
+
if (depth >= 8)
|
|
135
|
+
mode = "ansi256";
|
|
136
|
+
else if (depth >= 4)
|
|
137
|
+
mode = "ansi16";
|
|
138
|
+
}
|
|
139
|
+
return mode;
|
|
140
|
+
}
|
|
141
|
+
/* pure rendering helper: turn a Mermaid source string plus options into
|
|
142
|
+
a rendered Unicode/ASCII diagram string. Throws on render failure. */
|
|
143
|
+
static render(src, opts) {
|
|
144
|
+
/* create diagram rendering */
|
|
145
|
+
let out = renderMermaidASCII(src, {
|
|
146
|
+
useAscii: opts.ascii,
|
|
147
|
+
paddingX: opts.nodeMarginX,
|
|
148
|
+
paddingY: opts.nodeMarginY,
|
|
149
|
+
boxBorderPadding: opts.nodePadding,
|
|
150
|
+
colorMode: opts.colorMode,
|
|
151
|
+
theme: opts.colorMode !== "none" ? {
|
|
152
|
+
fg: "#000000",
|
|
153
|
+
border: "#a0a0a0",
|
|
154
|
+
junction: "#a0a0a0",
|
|
155
|
+
arrow: "#404040",
|
|
156
|
+
line: "#707070",
|
|
157
|
+
corner: "#707070"
|
|
158
|
+
} : {
|
|
159
|
+
fg: "#000000",
|
|
160
|
+
border: "#000000",
|
|
161
|
+
junction: "#000000",
|
|
162
|
+
arrow: "#000000",
|
|
163
|
+
line: "#000000",
|
|
164
|
+
corner: "#000000"
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
/* optionally clip diagram rendering */
|
|
168
|
+
const termWidth = opts.terminalWidth;
|
|
169
|
+
const termHeight = opts.terminalHeight;
|
|
170
|
+
if (termWidth > 0 || termHeight > 0) {
|
|
171
|
+
const maxWidth = termWidth > 0 ? termWidth - opts.diagramClipX : 0;
|
|
172
|
+
const maxHeight = termHeight > 0 ? termHeight - opts.diagramClipY : 0;
|
|
173
|
+
const trailingNL = out.endsWith("\n");
|
|
174
|
+
let lines = (trailingNL ? out.slice(0, -1) : out).split("\n");
|
|
175
|
+
let widthWarn = "";
|
|
176
|
+
let heightWarn = "";
|
|
177
|
+
if (maxWidth > 0) {
|
|
178
|
+
const widest = lines.reduce((m, l) => Math.max(m, visibleWidth(l)), 0);
|
|
179
|
+
if (widest > maxWidth)
|
|
180
|
+
widthWarn =
|
|
181
|
+
`ase diagram: WARNING: rendered diagram width ${widest} exceeds budget ${maxWidth}; ` +
|
|
182
|
+
"rightmost content was clipped. Please regenerate the Mermaid source to fit " +
|
|
183
|
+
`within ${maxWidth} chars by preferring a portrait orientation ` +
|
|
184
|
+
"(\"flowchart TB\", top-to-bottom) over landscape (\"LR\"/\"RL\"/\"BT\"), " +
|
|
185
|
+
"reducing siblings per row, abbreviating node labels, or restructuring " +
|
|
186
|
+
"into nested subgraph hierarchies.";
|
|
187
|
+
lines = lines.map((l) => truncateAnsiLine(l, maxWidth));
|
|
188
|
+
}
|
|
189
|
+
if (maxHeight > 0 && lines.length > maxHeight) {
|
|
190
|
+
const overflow = lines.length - maxHeight;
|
|
191
|
+
heightWarn =
|
|
192
|
+
`ase diagram: WARNING: rendered diagram height ${lines.length} exceeds budget ${maxHeight}; ` +
|
|
193
|
+
`bottom ${overflow} line(s) were clipped. Please regenerate the Mermaid source to fit ` +
|
|
194
|
+
`within ${maxHeight} lines by reducing depth or splitting into multiple diagrams.`;
|
|
195
|
+
lines = lines.slice(0, maxHeight);
|
|
196
|
+
}
|
|
197
|
+
out = lines.join("\n") + (trailingNL ? "\n" : "");
|
|
198
|
+
if (widthWarn !== "")
|
|
199
|
+
out += "\n" + widthWarn + "\n";
|
|
200
|
+
if (heightWarn !== "")
|
|
201
|
+
out += "\n" + heightWarn + "\n";
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
202
206
|
/* read stdin into a single string */
|
|
203
207
|
const readStdin = async () => {
|
|
204
208
|
const chunks = [];
|
|
@@ -219,14 +223,14 @@ export default class DiagramCommand {
|
|
|
219
223
|
.description("Render Mermaid diagram specification as Unicode/ASCII art")
|
|
220
224
|
.option("-i, --input <file>", "read Mermaid source from file instead of stdin")
|
|
221
225
|
.option("-a, --ascii", "emit plain ASCII (+-|) instead of Unicode box-drawing", false)
|
|
222
|
-
.option("-c, --color-mode <mode>", "force color mode (\"none\", \"ansi16\", or \"ansi256\")", parseColorMode("--color-mode"), detectColorMode())
|
|
226
|
+
.option("-c, --color-mode <mode>", "force color mode (\"none\", \"ansi16\", or \"ansi256\")", parseColorMode("--color-mode"), Diagram.detectColorMode())
|
|
223
227
|
.option("--node-margin-x <n>", "horizontal margin between nodes of <n> characters", parseInteger("--node-margin-x"), 3)
|
|
224
228
|
.option("--node-margin-y <n>", "vertical margin between nodes of <n> lines", parseInteger("--node-margin-y"), 3)
|
|
225
229
|
.option("--node-padding <n>", "horizontal and vertical inner node padding with <n> characters", parseInteger("--node-padding"), 1)
|
|
226
230
|
.option("--diagram-clip-x <n>", "extra horizontal clipping of diagram to terminal width minus <n> characters", parseInteger("--diagram-clip-x"), 0)
|
|
227
231
|
.option("--diagram-clip-y <n>", "extra vertical clipping of diagram to terminal height minus <n> lines", parseInteger("--diagram-clip-y"), 0)
|
|
228
|
-
.option("--terminal-width <n>", "width of terminal of <n> characters (for diagram clipping)", parseInteger("--terminal-width"), detectTermWidth())
|
|
229
|
-
.option("--terminal-height <n>", "height of terminal of <n> lines (for diagram clipping)", parseInteger("--terminal-height"), detectTermHeight())
|
|
232
|
+
.option("--terminal-width <n>", "width of terminal of <n> characters (for diagram clipping)", parseInteger("--terminal-width"), Diagram.detectTermWidth())
|
|
233
|
+
.option("--terminal-height <n>", "height of terminal of <n> lines (for diagram clipping)", parseInteger("--terminal-height"), Diagram.detectTermHeight())
|
|
230
234
|
.action(async (opts) => {
|
|
231
235
|
/* fetch Mermaid diagram specification from stdin */
|
|
232
236
|
let src;
|
|
@@ -241,7 +245,7 @@ export default class DiagramCommand {
|
|
|
241
245
|
/* create diagram rendering */
|
|
242
246
|
let out;
|
|
243
247
|
try {
|
|
244
|
-
out =
|
|
248
|
+
out = Diagram.render(src, {
|
|
245
249
|
ascii: opts.ascii ?? false,
|
|
246
250
|
colorMode: opts.colorMode,
|
|
247
251
|
nodeMarginX: opts.nodeMarginX,
|
|
@@ -265,3 +269,68 @@ export default class DiagramCommand {
|
|
|
265
269
|
});
|
|
266
270
|
}
|
|
267
271
|
}
|
|
272
|
+
/* MCP registration entry point for diagram tools */
|
|
273
|
+
export class DiagramMCP {
|
|
274
|
+
register(mcp) {
|
|
275
|
+
mcp.registerTool("diagram", {
|
|
276
|
+
title: "ASE diagram render",
|
|
277
|
+
description: "Render a Mermaid diagram as Unicode/ASCII art. " +
|
|
278
|
+
"Use for visualizing " +
|
|
279
|
+
"structure/layout/components/dependencies as a Flowchart, " +
|
|
280
|
+
"control-flow/branching/concurrency as a Flowchart, " +
|
|
281
|
+
"state-machine/states/transitions as an UML State Diagram, " +
|
|
282
|
+
"data-flow/actors/messages/protocols as an UML Sequence Diagram, " +
|
|
283
|
+
"data-structure/classes/methods as an UML Class Diagram, " +
|
|
284
|
+
"data-model/entities/relationships as an ER Diagram, or " +
|
|
285
|
+
"metrics/distributions/time-series as an XY-Chart. " +
|
|
286
|
+
"Pass the Mermaid diagram specification as `diagram`. " +
|
|
287
|
+
"Returns the rendered art as `text`.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
diagram: z.string()
|
|
290
|
+
.describe("Mermaid diagram specification"),
|
|
291
|
+
ascii: z.boolean().default(false)
|
|
292
|
+
.describe("emit plain ASCII (+-|) instead of Unicode box-drawing characters"),
|
|
293
|
+
colorMode: z.enum(["none", "ansi16", "ansi256"]).default("none")
|
|
294
|
+
.describe("color mode for ANSI escape sequences in the rendered output"),
|
|
295
|
+
nodeMarginX: z.number().int().min(0).default(3)
|
|
296
|
+
.describe("horizontal margin between nodes, in characters"),
|
|
297
|
+
nodeMarginY: z.number().int().min(0).default(3)
|
|
298
|
+
.describe("vertical margin between nodes, in lines"),
|
|
299
|
+
nodePadding: z.number().int().min(0).default(1)
|
|
300
|
+
.describe("inner horizontal and vertical padding within each node, in characters"),
|
|
301
|
+
diagramClipX: z.number().int().min(0).default(0)
|
|
302
|
+
.describe("extra horizontal clipping: subtract this many characters from `terminalWidth`"),
|
|
303
|
+
diagramClipY: z.number().int().min(0).default(0)
|
|
304
|
+
.describe("extra vertical clipping: subtract this many lines from `terminalHeight`"),
|
|
305
|
+
terminalWidth: z.number().int().min(0).default(Diagram.detectTermWidth())
|
|
306
|
+
.describe("terminal width in characters; 0 disables horizontal clipping; defaults to ASE_TERM_WIDTH env var if set"),
|
|
307
|
+
terminalHeight: z.number().int().min(0).default(Diagram.detectTermHeight())
|
|
308
|
+
.describe("terminal height in lines; 0 disables vertical clipping; defaults to ASE_TERM_HEIGHT env var if set")
|
|
309
|
+
}
|
|
310
|
+
}, async (args) => {
|
|
311
|
+
try {
|
|
312
|
+
const out = Diagram.render(args.diagram, {
|
|
313
|
+
ascii: args.ascii,
|
|
314
|
+
colorMode: args.colorMode,
|
|
315
|
+
nodeMarginX: args.nodeMarginX,
|
|
316
|
+
nodeMarginY: args.nodeMarginY,
|
|
317
|
+
nodePadding: args.nodePadding,
|
|
318
|
+
diagramClipX: args.diagramClipX,
|
|
319
|
+
diagramClipY: args.diagramClipY,
|
|
320
|
+
terminalWidth: args.terminalWidth,
|
|
321
|
+
terminalHeight: args.terminalHeight
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: "text", text: out }]
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
329
|
+
return {
|
|
330
|
+
isError: true,
|
|
331
|
+
content: [{ type: "text", text: `diagram: render failed: ${message}` }]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
package/dst/ase-hook.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
8
9
|
import { execaSync } from "execa";
|
|
9
10
|
import Version from "./ase-version.js";
|
|
10
11
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
@@ -79,7 +80,7 @@ export default class HookCommand {
|
|
|
79
80
|
const versionHints = [];
|
|
80
81
|
if (versionCurrentPlugin !== versionCurrentTool)
|
|
81
82
|
versionHints.push("**WARNING:** version *mismatch*: " +
|
|
82
|
-
`tool: **${
|
|
83
|
+
`tool: **${versionCurrentTool}**, plugin: **${versionCurrentPlugin}**`);
|
|
83
84
|
if (versionCurrentTool !== versionLatestTool)
|
|
84
85
|
versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
|
|
85
86
|
if (process.env.ASE_SETUP_DEV !== undefined)
|
|
@@ -91,22 +92,25 @@ export default class HookCommand {
|
|
|
91
92
|
const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
|
|
92
93
|
/* determine session id */
|
|
93
94
|
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
94
|
-
/* establish config context */
|
|
95
|
-
const
|
|
95
|
+
/* establish config context (session-scoped only if a valid sessionId is present) */
|
|
96
|
+
const hasSession = /^[A-Za-z0-9._-]+$/.test(sessionId);
|
|
97
|
+
const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
|
|
96
98
|
try {
|
|
97
99
|
cfg.read();
|
|
98
100
|
}
|
|
99
101
|
catch (_e) {
|
|
100
102
|
/* best-effort: ignore failures */
|
|
101
103
|
}
|
|
102
|
-
/* determine task id */
|
|
104
|
+
/* determine task id (only persist when scoped to a real session) */
|
|
103
105
|
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
if (hasSession) {
|
|
107
|
+
try {
|
|
108
|
+
cfg.set("agent.task", taskId);
|
|
109
|
+
cfg.write();
|
|
110
|
+
}
|
|
111
|
+
catch (_e) {
|
|
112
|
+
/* best-effort: ignore failures */
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
/* determine project id */
|
|
112
116
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -170,6 +174,26 @@ export default class HookCommand {
|
|
|
170
174
|
process.stdout.write(JSON.stringify(payload));
|
|
171
175
|
return 0;
|
|
172
176
|
}
|
|
177
|
+
/* handler for "ase hook session-end" (both tools) */
|
|
178
|
+
doSessionEnd(_tool) {
|
|
179
|
+
/* read session information (Claude Code uses snake_case fields,
|
|
180
|
+
Copilot CLI uses camelCase fields) */
|
|
181
|
+
const stdin = fs.readFileSync(0, "utf8");
|
|
182
|
+
const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
|
|
183
|
+
/* determine session id */
|
|
184
|
+
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
185
|
+
/* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
|
|
186
|
+
if (/^[A-Za-z0-9._-]+$/.test(sessionId)) {
|
|
187
|
+
const dir = path.join(os.homedir(), ".ase", "session", sessionId);
|
|
188
|
+
try {
|
|
189
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
catch (_e) {
|
|
192
|
+
/* best-effort: ignore failures */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
173
197
|
/* handler for "ase hook pre-tool-use" (both tools) */
|
|
174
198
|
doPreToolUse(tool) {
|
|
175
199
|
const spec = toolSpecs[tool];
|
|
@@ -251,6 +275,14 @@ export default class HookCommand {
|
|
|
251
275
|
.action(async (opts) => {
|
|
252
276
|
process.exit(await this.doSessionStart(this.parseTool(opts.tool)));
|
|
253
277
|
});
|
|
278
|
+
/* register CLI sub-command "ase hook session-end" */
|
|
279
|
+
hookCmd
|
|
280
|
+
.command("session-end")
|
|
281
|
+
.description("handle SessionEnd hook event")
|
|
282
|
+
.option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
|
|
283
|
+
.action((opts) => {
|
|
284
|
+
process.exit(this.doSessionEnd(this.parseTool(opts.tool)));
|
|
285
|
+
});
|
|
254
286
|
/* register CLI sub-command "ase hook pre-tool-use" */
|
|
255
287
|
hookCmd
|
|
256
288
|
.command("pre-tool-use")
|
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.js";
|
|
44
13
|
/* CLI command "ase mcp" */
|
|
45
14
|
export default class MCPCommand {
|
|
46
15
|
log;
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { isScalar } from "yaml";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
9
|
+
/* reusable functionality: ASE agent persona style get/set */
|
|
10
|
+
export class Persona {
|
|
11
|
+
/* allowed persona style values */
|
|
12
|
+
static styles = ["writer", "engineer", "telegrapher", "caveman"];
|
|
13
|
+
/* get the effective persona style for an optional session;
|
|
14
|
+
returns the default "engineer" if nothing is configured */
|
|
15
|
+
static get(log, session) {
|
|
16
|
+
const scope = session !== undefined ?
|
|
17
|
+
parseScope(`session:${session}`) :
|
|
18
|
+
parseScope(undefined);
|
|
19
|
+
const cfg = new Config("config", configSchema, log, scope);
|
|
20
|
+
cfg.read();
|
|
21
|
+
const val = cfg.get("agent.persona");
|
|
22
|
+
if (val === undefined)
|
|
23
|
+
return "engineer";
|
|
24
|
+
return String(isScalar(val) ? val.value : val);
|
|
25
|
+
}
|
|
26
|
+
/* set the persona style on the strongest scope of an optional session */
|
|
27
|
+
static set(log, style, session) {
|
|
28
|
+
const scope = session !== undefined ?
|
|
29
|
+
parseScope(`session:${session}`) :
|
|
30
|
+
parseScope(undefined);
|
|
31
|
+
const cfg = new Config("config", configSchema, log, scope);
|
|
32
|
+
cfg.read();
|
|
33
|
+
cfg.set("agent.persona", style);
|
|
34
|
+
cfg.write();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/* MCP registration entry point for persona tools */
|
|
38
|
+
export default class PersonaMCP {
|
|
39
|
+
log;
|
|
40
|
+
constructor(log) {
|
|
41
|
+
this.log = log;
|
|
42
|
+
}
|
|
43
|
+
register(mcp) {
|
|
44
|
+
mcp.registerTool("persona", {
|
|
45
|
+
title: "ASE persona style get/set",
|
|
46
|
+
description: "Get or set the active ASE agent persona `style`. " +
|
|
47
|
+
"If `style` is provided, it sets the persona style, " +
|
|
48
|
+
"otherwise it returns the current persona `style`. " +
|
|
49
|
+
"If `session` is provided, the operation is scoped to that session, " +
|
|
50
|
+
"otherwise it operates on the broadest scope (user/project cascade). " +
|
|
51
|
+
"Allowed styles: \"writer\" (decorative, eloquent, explaining), " +
|
|
52
|
+
"\"engineer\" (brief, factual, accurate), " +
|
|
53
|
+
"\"telegrapher\" (very brief, factual, abbreviating), " +
|
|
54
|
+
"\"caveman\" (ultra brief, rough, stuttering).",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
style: z.enum(Persona.styles).optional()
|
|
57
|
+
.describe("persona style to set; if omitted, the current persona style is returned"),
|
|
58
|
+
session: z.string().optional()
|
|
59
|
+
.describe("session identifier (allowed characters: A-Z, a-z, 0-9, '-'); " +
|
|
60
|
+
"if omitted, the operation is not scoped to a specific session")
|
|
61
|
+
}
|
|
62
|
+
}, async (args) => {
|
|
63
|
+
try {
|
|
64
|
+
if (args.style !== undefined) {
|
|
65
|
+
Persona.set(this.log, args.style, args.session);
|
|
66
|
+
const where = args.session !== undefined ?
|
|
67
|
+
` for session "${args.session}"` : "";
|
|
68
|
+
const msg = `persona: OK: set agent.persona to "${args.style}"${where}`;
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: msg }]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const text = Persona.get(this.log, args.session);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text }]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
return {
|
|
81
|
+
isError: true,
|
|
82
|
+
content: [{ type: "text", text: `persona: ERROR: ${message}` }]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -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
|
+
};
|