@rse/ase 0.9.7 → 0.9.9
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-getopt.js +71 -5
- package/dst/ase-hook.js +6 -21
- package/dst/ase-markdown.js +32 -11
- package/dst/ase-mcp.js +22 -8
- package/dst/ase-notify.js +32 -0
- package/dst/ase-service.js +5 -2
- package/dst/ase-setup.js +45 -131
- package/dst/ase-skills.js +17 -13
- package/dst/ase-statusline.js +8 -12
- package/dst/ase-task.js +32 -23
- package/package.json +3 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-docs-proofread.md +2 -2
- package/plugin/meta/ase-constitution.md +7 -0
- package/plugin/meta/ase-control.md +24 -3
- package/plugin/meta/ase-dialog.md +105 -7
- package/plugin/meta/ase-format-task.md +2 -2
- package/plugin/meta/ase-getopt.md +31 -22
- package/plugin/meta/ase-skill.md +87 -12
- package/plugin/package.json +2 -2
- package/plugin/skills/ase-arch-analyze/SKILL.md +88 -89
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-arch-discover/SKILL.md +56 -34
- package/plugin/skills/ase-arch-discover/help.md +1 -1
- package/plugin/skills/ase-code-analyze/SKILL.md +6 -5
- package/plugin/skills/ase-code-analyze/help.md +2 -2
- package/plugin/skills/ase-code-craft/SKILL.md +83 -71
- package/plugin/skills/ase-code-craft/help.md +2 -2
- package/plugin/skills/ase-code-explain/SKILL.md +1 -1
- package/plugin/skills/ase-code-explain/help.md +1 -1
- package/plugin/skills/ase-code-insight/SKILL.md +1 -1
- package/plugin/skills/ase-code-insight/help.md +1 -1
- package/plugin/skills/ase-code-lint/SKILL.md +35 -18
- package/plugin/skills/ase-code-lint/help.md +2 -2
- package/plugin/skills/ase-code-refactor/SKILL.md +81 -70
- package/plugin/skills/ase-code-refactor/help.md +2 -2
- package/plugin/skills/ase-code-resolve/SKILL.md +83 -70
- package/plugin/skills/ase-code-resolve/help.md +3 -3
- package/plugin/skills/ase-docs-distill/SKILL.md +1 -1
- package/plugin/skills/ase-docs-distill/help.md +4 -4
- package/plugin/skills/ase-docs-proofread/SKILL.md +36 -19
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +29 -8
- package/plugin/skills/ase-meta-brainstorm/help.md +7 -11
- package/plugin/skills/ase-meta-changelog/help.md +1 -1
- package/plugin/skills/ase-meta-chat/help.md +1 -1
- package/plugin/skills/ase-meta-commit/help.md +1 -1
- package/plugin/skills/ase-meta-diaboli/help.md +2 -2
- package/plugin/skills/ase-meta-diff/SKILL.md +6 -5
- package/plugin/skills/ase-meta-diff/help.md +11 -12
- package/plugin/skills/ase-meta-evaluate/SKILL.md +10 -9
- package/plugin/skills/ase-meta-evaluate/help.md +2 -2
- package/plugin/skills/ase-meta-persona/help.md +1 -1
- package/plugin/skills/ase-meta-quorum/SKILL.md +15 -5
- package/plugin/skills/ase-meta-quorum/help.md +1 -1
- package/plugin/skills/ase-meta-review/SKILL.md +3 -4
- package/plugin/skills/ase-meta-review/help.md +5 -5
- package/plugin/skills/ase-meta-search/SKILL.md +9 -8
- package/plugin/skills/ase-meta-search/help.md +1 -1
- package/plugin/skills/ase-meta-steelman/SKILL.md +1 -1
- package/plugin/skills/ase-meta-steelman/help.md +2 -2
- package/plugin/skills/ase-meta-why/SKILL.md +16 -10
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-task-condense/SKILL.md +36 -19
- package/plugin/skills/ase-task-condense/help.md +3 -3
- package/plugin/skills/ase-task-delete/SKILL.md +6 -3
- package/plugin/skills/ase-task-delete/help.md +2 -2
- package/plugin/skills/ase-task-edit/SKILL.md +61 -36
- package/plugin/skills/ase-task-edit/help.md +4 -4
- package/plugin/skills/ase-task-grill/SKILL.md +57 -26
- package/plugin/skills/ase-task-grill/help.md +3 -3
- package/plugin/skills/ase-task-id/SKILL.md +11 -2
- package/plugin/skills/ase-task-id/help.md +2 -2
- package/plugin/skills/ase-task-implement/SKILL.md +40 -17
- package/plugin/skills/ase-task-implement/help.md +2 -2
- package/plugin/skills/ase-task-list/SKILL.md +1 -1
- package/plugin/skills/ase-task-list/help.md +2 -2
- package/plugin/skills/ase-task-preflight/SKILL.md +44 -22
- package/plugin/skills/ase-task-preflight/help.md +3 -3
- package/plugin/skills/ase-task-reboot/SKILL.md +31 -20
- package/plugin/skills/ase-task-reboot/help.md +2 -2
- package/plugin/skills/ase-task-rename/SKILL.md +5 -3
- package/plugin/skills/ase-task-rename/help.md +2 -2
- package/plugin/skills/ase-task-view/help.md +26 -7
package/dst/ase-getopt.js
CHANGED
|
@@ -19,7 +19,9 @@ export class GetoptMCP {
|
|
|
19
19
|
"containing those remaining tokens (quotes preserved), and " +
|
|
20
20
|
"`info` is a markdown rendering of the parsed options in the " +
|
|
21
21
|
"form `key: **value**, key: **value**, ...` for printing at " +
|
|
22
|
-
"the top of a skill."
|
|
22
|
+
"the top of a skill. Options whose long name starts with " +
|
|
23
|
+
"`int-` are treated as internal and are excluded from both " +
|
|
24
|
+
"the usage help and the `info` rendering.",
|
|
23
25
|
inputSchema: {
|
|
24
26
|
name: z.string()
|
|
25
27
|
.describe("Name of the caller (e.g. skill name), used in error messages"),
|
|
@@ -53,6 +55,8 @@ export class GetoptMCP {
|
|
|
53
55
|
/* tokenize spec and add one option per token */
|
|
54
56
|
const tokens = args.spec.split(/\s+/).filter((e) => e.length > 0);
|
|
55
57
|
const re = /^--([A-Za-z][A-Za-z0-9-]*)(?:\|-([A-Za-z]))?(?:=(\((.*)\)(\.\.\.)?|.*))?$/;
|
|
58
|
+
const internals = new Set();
|
|
59
|
+
const flagTakesValue = new Map();
|
|
56
60
|
for (const tok of tokens) {
|
|
57
61
|
const m = re.exec(tok);
|
|
58
62
|
if (m === null)
|
|
@@ -66,6 +70,9 @@ export class GetoptMCP {
|
|
|
66
70
|
const choices = choicePart !== null ? choicePart.split("|") : null;
|
|
67
71
|
const isList = listMarker !== null;
|
|
68
72
|
const dflt = choices !== null ? choices[0] : valuePart;
|
|
73
|
+
flagTakesValue.set(`--${long}`, takesValue);
|
|
74
|
+
if (short !== null)
|
|
75
|
+
flagTakesValue.set(`-${short}`, takesValue);
|
|
69
76
|
const head = short !== null ? `-${short}, --${long}` : `--${long}`;
|
|
70
77
|
const flags = takesValue ? `${head} <value>` : head;
|
|
71
78
|
const opt = new Option(flags);
|
|
@@ -76,6 +83,12 @@ export class GetoptMCP {
|
|
|
76
83
|
}
|
|
77
84
|
else
|
|
78
85
|
opt.default(false);
|
|
86
|
+
if (long !== undefined && long.startsWith("int-")) {
|
|
87
|
+
/* internal option: hide from usage help and remember
|
|
88
|
+
its camel-cased key for the info rendering below */
|
|
89
|
+
opt.hideHelp();
|
|
90
|
+
internals.add(long.replace(/-(.)/g, (_, c) => c.toUpperCase()));
|
|
91
|
+
}
|
|
79
92
|
cmd.addOption(opt);
|
|
80
93
|
}
|
|
81
94
|
/* parse args */
|
|
@@ -134,17 +147,70 @@ export class GetoptMCP {
|
|
|
134
147
|
}
|
|
135
148
|
ranges.push({ start, end: i });
|
|
136
149
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
/* helper function: strip surrounding quotes/escapes from a raw
|
|
151
|
+
range so it can be compared against an option flag spelling */
|
|
152
|
+
const unquote = (s) => {
|
|
153
|
+
let out = "";
|
|
154
|
+
let j = 0;
|
|
155
|
+
while (j < s.length) {
|
|
156
|
+
const ch = s[j];
|
|
157
|
+
if (ch === "\"" || ch === "'") {
|
|
158
|
+
const quote = ch;
|
|
159
|
+
j++;
|
|
160
|
+
while (j < s.length && s[j] !== quote) {
|
|
161
|
+
if (s[j] === "\\" && j + 1 < s.length) {
|
|
162
|
+
out += s[j + 1];
|
|
163
|
+
j += 2;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
out += s[j];
|
|
167
|
+
j++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
j++;
|
|
171
|
+
}
|
|
172
|
+
else if (ch === "\\" && j + 1 < s.length) {
|
|
173
|
+
out += s[j + 1];
|
|
174
|
+
j += 2;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
out += ch;
|
|
178
|
+
j++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
};
|
|
183
|
+
/* walk the raw ranges, consuming leading option tokens (and any
|
|
184
|
+
separate value tokens they take) until the first positional
|
|
185
|
+
is reached, then slice the original input from there -- this
|
|
186
|
+
mirrors commander's pass-through semantics while staying on
|
|
187
|
+
the verbatim text and is robust against value-consuming
|
|
188
|
+
options and shell-operator characters in the input */
|
|
189
|
+
let idx = 0;
|
|
190
|
+
while (idx < ranges.length) {
|
|
191
|
+
const tok = unquote(argsRaw.slice(ranges[idx].start, ranges[idx].end));
|
|
192
|
+
if (tok === "--") {
|
|
193
|
+
idx++;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (!tok.startsWith("-") || tok === "-")
|
|
197
|
+
break;
|
|
198
|
+
let consumesNext = false;
|
|
199
|
+
if (/^--[^=]+$/.test(tok) || /^-[^-]$/.test(tok))
|
|
200
|
+
consumesNext = flagTakesValue.get(tok) === true;
|
|
201
|
+
idx++;
|
|
202
|
+
if (consumesNext && idx < ranges.length)
|
|
203
|
+
idx++;
|
|
141
204
|
}
|
|
205
|
+
if (idx < ranges.length)
|
|
206
|
+
argsVerbatim = argsRaw.slice(ranges[idx].start);
|
|
142
207
|
}
|
|
143
208
|
else
|
|
144
209
|
argsVerbatim = cmd.args.join(" ");
|
|
145
210
|
/* build markdown info rendering of parsed options */
|
|
146
211
|
const opts = cmd.opts();
|
|
147
212
|
const info = Object.entries(opts)
|
|
213
|
+
.filter(([k]) => !internals.has(k))
|
|
148
214
|
.map(([k, v]) => `${k}: **${shQuote([String(v)])}**`)
|
|
149
215
|
.join(", ");
|
|
150
216
|
/* build result */
|
package/dst/ase-hook.js
CHANGED
|
@@ -92,7 +92,7 @@ export default class HookCommand {
|
|
|
92
92
|
return text.replace(/@(\S+)/g, (match, ref) => {
|
|
93
93
|
let resolved = ref;
|
|
94
94
|
if (resolved.startsWith("~/"))
|
|
95
|
-
resolved = path.join(
|
|
95
|
+
resolved = path.join(os.homedir(), resolved.slice(2));
|
|
96
96
|
const abs = path.isAbsolute(resolved) ? resolved : path.resolve(baseDir, resolved);
|
|
97
97
|
if (visited.has(abs))
|
|
98
98
|
return match;
|
|
@@ -265,24 +265,6 @@ export default class HookCommand {
|
|
|
265
265
|
/* handler for "ase hook stop" (both tools) */
|
|
266
266
|
doStop(_tool) {
|
|
267
267
|
this.writeAgentStatus("ready");
|
|
268
|
-
/* safety net: clear any lingering "agent.skill" marker so a
|
|
269
|
-
crashed or aborted skill loop does not leave information active */
|
|
270
|
-
const sessionId = this.readSessionIdFromStdin();
|
|
271
|
-
if (this.isValidSessionId(sessionId)) {
|
|
272
|
-
try {
|
|
273
|
-
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
|
|
274
|
-
cfg.lock(() => {
|
|
275
|
-
cfg.read();
|
|
276
|
-
if (typeof cfg.get("agent.skill") === "string") {
|
|
277
|
-
cfg.delete("agent.skill");
|
|
278
|
-
cfg.write();
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
catch (_e) {
|
|
283
|
-
/* best-effort: ignore failures */
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
268
|
return 0;
|
|
287
269
|
}
|
|
288
270
|
/* handler for "ase hook session-end" (both tools) */
|
|
@@ -334,6 +316,9 @@ export default class HookCommand {
|
|
|
334
316
|
return "";
|
|
335
317
|
}
|
|
336
318
|
}
|
|
319
|
+
/* the edit-capable skills whose active state lets the pre-tool-use
|
|
320
|
+
hook auto-approve subsequent "Edit" invocations */
|
|
321
|
+
editCapableSkills = ["ase-code-lint", "ase-docs-proofread"];
|
|
337
322
|
/* handler for "ase hook pre-tool-use" (both tools) */
|
|
338
323
|
doPreToolUse(tool) {
|
|
339
324
|
const spec = toolSpecs[tool];
|
|
@@ -377,9 +362,9 @@ export default class HookCommand {
|
|
|
377
362
|
else if (toolName === "Edit") {
|
|
378
363
|
const sessionId = this.pickSessionId(input);
|
|
379
364
|
const activeSkill = this.readActiveSkill(sessionId);
|
|
380
|
-
if (activeSkill
|
|
365
|
+
if (this.editCapableSkills.includes(activeSkill)) {
|
|
381
366
|
approve = true;
|
|
382
|
-
reason = `${activeSkill}:
|
|
367
|
+
reason = `${activeSkill}: edit auto-approved for active edit-capable skill`;
|
|
383
368
|
}
|
|
384
369
|
}
|
|
385
370
|
/* emit permission decision (or stay silent to defer to default flow).
|
package/dst/ase-markdown.js
CHANGED
|
@@ -65,8 +65,8 @@ export class Markdown {
|
|
|
65
65
|
/* apply the inline-span and bullet-marker rewriting passes to a chunk of
|
|
66
66
|
non-fenced Markdown text (see prepare() for fenced-block handling) */
|
|
67
67
|
static rewrite(text) {
|
|
68
|
-
/* PASS 1: rewrite
|
|
69
|
-
|
|
68
|
+
/* PASS 1: rewrite inline code spans that carry backslash-escaped
|
|
69
|
+
backticks (`\``) into CommonMark-correct spans. */
|
|
70
70
|
{
|
|
71
71
|
let pre = "";
|
|
72
72
|
let j = 0;
|
|
@@ -79,10 +79,19 @@ export class Markdown {
|
|
|
79
79
|
j++;
|
|
80
80
|
continue;
|
|
81
81
|
}
|
|
82
|
-
/*
|
|
83
|
-
|
|
82
|
+
/* measure the full opening backtick run: a code-span
|
|
83
|
+
delimiter is a *run* of backticks and a span opened by a
|
|
84
|
+
run of N backticks is closed only by a run of exactly N
|
|
85
|
+
backticks, so the opening-run length determines what we
|
|
86
|
+
scan for as the closing delimiter */
|
|
87
|
+
let open = 0;
|
|
88
|
+
while (j + open < text.length && text[j + open] === "`")
|
|
89
|
+
open++;
|
|
90
|
+
/* scan the opening backtick-run span, capturing its raw
|
|
91
|
+
inner content up to the matching unescaped closing run of
|
|
92
|
+
exactly `open` backticks */
|
|
84
93
|
let inner = "";
|
|
85
|
-
let k = j +
|
|
94
|
+
let k = j + open;
|
|
86
95
|
let closed = false;
|
|
87
96
|
let escaped = false;
|
|
88
97
|
while (k < text.length) {
|
|
@@ -94,17 +103,29 @@ export class Markdown {
|
|
|
94
103
|
continue;
|
|
95
104
|
}
|
|
96
105
|
if (c === "`") {
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
/* measure this backtick run and treat it as the
|
|
107
|
+
closing delimiter only if it matches the opening
|
|
108
|
+
run length exactly; a shorter or longer run is
|
|
109
|
+
literal content of the span */
|
|
110
|
+
let runLen = 0;
|
|
111
|
+
while (k + runLen < text.length && text[k + runLen] === "`")
|
|
112
|
+
runLen++;
|
|
113
|
+
if (runLen === open) {
|
|
114
|
+
closed = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
inner += "`".repeat(runLen);
|
|
118
|
+
k += runLen;
|
|
119
|
+
continue;
|
|
99
120
|
}
|
|
100
121
|
inner += c;
|
|
101
122
|
k++;
|
|
102
123
|
}
|
|
103
124
|
if (!closed || !escaped) {
|
|
104
|
-
/* not an escaped-backtick span: emit the opening
|
|
125
|
+
/* not an escaped-backtick span: emit the opening run
|
|
105
126
|
verbatim and continue scanning from just after it */
|
|
106
|
-
pre += "`";
|
|
107
|
-
j
|
|
127
|
+
pre += "`".repeat(open);
|
|
128
|
+
j += open;
|
|
108
129
|
continue;
|
|
109
130
|
}
|
|
110
131
|
/* un-escape inner `\`` into literal backticks, then choose a
|
|
@@ -124,7 +145,7 @@ export class Markdown {
|
|
|
124
145
|
const fence = "`".repeat(maxRun + 1);
|
|
125
146
|
const pad = (content.startsWith("`") || content.endsWith("`")) ? " " : "";
|
|
126
147
|
pre += `${fence}${pad}${content}${pad}${fence}`;
|
|
127
|
-
j = k +
|
|
148
|
+
j = k + open;
|
|
128
149
|
}
|
|
129
150
|
text = pre;
|
|
130
151
|
}
|
package/dst/ase-mcp.js
CHANGED
|
@@ -62,15 +62,21 @@ export default class MCPCommand {
|
|
|
62
62
|
const server = new StdioServerTransport();
|
|
63
63
|
/* track active client and bridge-level closed state */
|
|
64
64
|
let client = null;
|
|
65
|
-
let closedByUs = false; /* set when we initiated the client close */
|
|
66
65
|
let bridgeDone = false; /* set when stdio side closes */
|
|
67
66
|
let reconnecting = false; /* set while a reconnect chain is active */
|
|
67
|
+
/* mark the individual transports we intentionally closed, so their
|
|
68
|
+
onclose is not mistaken for an unexpected connection loss; using
|
|
69
|
+
a per-transport set (instead of a single bridge-wide flag) avoids
|
|
70
|
+
suppressing a legitimate retry when a freshly-created connection
|
|
71
|
+
fails during the brief window right after we initiated a close */
|
|
72
|
+
const closedByUs = new WeakSet();
|
|
68
73
|
/* cleanly shut down the whole bridge */
|
|
69
74
|
const shutdown = async () => {
|
|
70
75
|
if (bridgeDone)
|
|
71
76
|
return;
|
|
72
77
|
bridgeDone = true;
|
|
73
|
-
|
|
78
|
+
if (client !== null)
|
|
79
|
+
closedByUs.add(client);
|
|
74
80
|
const timeout = new Promise((resolve) => setTimeout(resolve, 3000));
|
|
75
81
|
await Promise.race([
|
|
76
82
|
Promise.allSettled([server.close(), client?.close()]),
|
|
@@ -92,7 +98,7 @@ export default class MCPCommand {
|
|
|
92
98
|
};
|
|
93
99
|
/* service closed the connection — try to recover */
|
|
94
100
|
next.onclose = () => {
|
|
95
|
-
if (client !== next || closedByUs || bridgeDone || reconnecting)
|
|
101
|
+
if (client !== next || closedByUs.has(next) || bridgeDone || reconnecting)
|
|
96
102
|
return;
|
|
97
103
|
triggerReconnect("http connection lost");
|
|
98
104
|
};
|
|
@@ -110,15 +116,17 @@ export default class MCPCommand {
|
|
|
110
116
|
try {
|
|
111
117
|
const ctx = await this.ensureService();
|
|
112
118
|
port = ctx.port;
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
const stale = client;
|
|
120
|
+
client = null;
|
|
121
|
+
if (stale !== null) {
|
|
122
|
+
closedByUs.add(stale);
|
|
123
|
+
await stale.close();
|
|
124
|
+
}
|
|
115
125
|
await connectClient();
|
|
116
|
-
closedByUs = false;
|
|
117
126
|
reconnecting = false;
|
|
118
127
|
this.log.write("info", "mcp: reconnected to service");
|
|
119
128
|
}
|
|
120
129
|
catch (err) {
|
|
121
|
-
closedByUs = false;
|
|
122
130
|
this.log.write("error", `mcp: reconnect failed: ${this.asError(err).message}`);
|
|
123
131
|
reconnect(attempt + 1).catch(() => { });
|
|
124
132
|
}
|
|
@@ -143,7 +151,13 @@ export default class MCPCommand {
|
|
|
143
151
|
};
|
|
144
152
|
/* start server and initial client */
|
|
145
153
|
await server.start();
|
|
146
|
-
|
|
154
|
+
try {
|
|
155
|
+
await connectClient();
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
/* service vanished between probe and connect — recover instead of crashing */
|
|
159
|
+
triggerReconnect(`initial connect failed: ${this.asError(err).message}`);
|
|
160
|
+
}
|
|
147
161
|
/* periodically probe the service; trigger reconnect if it is gone */
|
|
148
162
|
const HEALTH_INTERVAL_MS = 30_000;
|
|
149
163
|
const healthTimer = setInterval(async () => {
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { z } from "zod";
|
|
7
|
+
/* MCP registration entry point for user notification tool */
|
|
8
|
+
export class NotifyMCP {
|
|
9
|
+
register(mcp) {
|
|
10
|
+
mcp.registerTool("ase_notify", {
|
|
11
|
+
title: "ASE user notification",
|
|
12
|
+
description: "Display a `message` to the user in the terminal of the agent harness. " +
|
|
13
|
+
"The display happens deterministically through the accompanying PostToolUse hook, " +
|
|
14
|
+
"independent of any assistant text output. " +
|
|
15
|
+
"Use this to surface intermediate skill progress or status information " +
|
|
16
|
+
"which otherwise would not be visible to the user before the final response. " +
|
|
17
|
+
"Returns a status `text` acknowledging the notification.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
message: z.string().min(1).max(4096)
|
|
20
|
+
.describe("message to display to the user (plain text, up to 4096 characters)")
|
|
21
|
+
}
|
|
22
|
+
}, async (args) => {
|
|
23
|
+
/* the tool itself is a deliberate no-op: the user-visible display
|
|
24
|
+
happens in the PostToolUse hook ("ase hook post-tool-use"),
|
|
25
|
+
which receives this tool call's input from the agent harness
|
|
26
|
+
and reflects the message back as a "systemMessage" */
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: "notify: OK: message displayed to user" }]
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dst/ase-service.js
CHANGED
|
@@ -43,7 +43,7 @@ export const isConnRefused = (err) => {
|
|
|
43
43
|
/* probe the service and verify ASE identity banner */
|
|
44
44
|
export const probe = async (port, projectId) => {
|
|
45
45
|
try {
|
|
46
|
-
const r = await ofetch.raw(`http://${
|
|
46
|
+
const r = await ofetch.raw(`http://${HOST}:${port}/`, {
|
|
47
47
|
method: "OPTIONS",
|
|
48
48
|
signal: AbortSignal.timeout(2000),
|
|
49
49
|
ignoreResponseError: true
|
|
@@ -509,12 +509,15 @@ export default class ServiceCommand {
|
|
|
509
509
|
if (ctx.port === null)
|
|
510
510
|
throw new Error("service not running (no port configured after auto-start)");
|
|
511
511
|
}
|
|
512
|
-
|
|
512
|
+
let match = await probe(ctx.port, ctx.projectId);
|
|
513
513
|
if (match !== true) {
|
|
514
514
|
await this.doStart();
|
|
515
515
|
ctx = this.loadContext();
|
|
516
516
|
if (ctx.port === null)
|
|
517
517
|
throw new Error("service not running (no port configured after auto-start)");
|
|
518
|
+
match = await probe(ctx.port, ctx.projectId);
|
|
519
|
+
if (match !== true)
|
|
520
|
+
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
518
521
|
}
|
|
519
522
|
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, {
|
|
520
523
|
method: "POST",
|
package/dst/ase-setup.js
CHANGED
|
@@ -107,11 +107,19 @@ export default class SetupCommand {
|
|
|
107
107
|
try {
|
|
108
108
|
if (quiet) {
|
|
109
109
|
const result = await execa(cmd, args, { stdio: "ignore", cwd, reject: false });
|
|
110
|
-
if (typeof result.exitCode === "number" && result.exitCode !== 0
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
if (typeof result.exitCode === "number" && result.exitCode !== 0) {
|
|
111
|
+
if (!final) {
|
|
112
|
+
this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${args.join(" ")}" ` +
|
|
113
|
+
`(exit code: ${result.exitCode}): retrying...`);
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ignoreError !== undefined) {
|
|
118
|
+
this.log.write("info", `setup: ${ignoreError} (skipped)`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.log.write("error", `setup: command failed: exit code: ${result.exitCode}`);
|
|
122
|
+
throw new Error(`command "${cmd} ${args.join(" ")}" failed (exit code: ${result.exitCode})`);
|
|
115
123
|
}
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
@@ -386,6 +394,32 @@ export default class SetupCommand {
|
|
|
386
394
|
["mcp", "remove", name];
|
|
387
395
|
await this.run(toolSpecs[tool].cli, args, { ignoreError: `MCP server "${name}" not registered` });
|
|
388
396
|
}
|
|
397
|
+
/* build a chat-model MCP handler from the per-model direct and
|
|
398
|
+
OPENROUTER url/api/model triples, factoring out the shared
|
|
399
|
+
mcp-to-openai stdio scaffold common to all chat-model servers */
|
|
400
|
+
chatMcpHandler(direct, router) {
|
|
401
|
+
return async (spec, tool, action, envKey, envVal) => {
|
|
402
|
+
if (action === "activate")
|
|
403
|
+
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
404
|
+
type: "stdio", command: [
|
|
405
|
+
"npx", "-y", "mcp-to-openai",
|
|
406
|
+
"--service", spec.name,
|
|
407
|
+
"--mcp-tool", "query",
|
|
408
|
+
...(envKey === "OPENROUTER" ? [
|
|
409
|
+
"--openai-url", "https://openrouter.ai/api/v1",
|
|
410
|
+
"--openai-api", "completion",
|
|
411
|
+
"--openai-model", router.model
|
|
412
|
+
] : [
|
|
413
|
+
"--openai-url", direct.url,
|
|
414
|
+
"--openai-api", direct.api,
|
|
415
|
+
"--openai-model", direct.model
|
|
416
|
+
])
|
|
417
|
+
]
|
|
418
|
+
});
|
|
419
|
+
else
|
|
420
|
+
await this.mcpRemove(tool, spec.server);
|
|
421
|
+
};
|
|
422
|
+
}
|
|
389
423
|
/* registry of pre-defined MCP servers: maps each server id onto its
|
|
390
424
|
dedicated handler which performs the activate/deactivate operation */
|
|
391
425
|
mcpServers = [
|
|
@@ -396,27 +430,7 @@ export default class SetupCommand {
|
|
|
396
430
|
env: ["OPENAI_CHATGPT", "OPENROUTER"],
|
|
397
431
|
server: "chat-openai-chatgpt",
|
|
398
432
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
399
|
-
handler:
|
|
400
|
-
if (action === "activate")
|
|
401
|
-
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
402
|
-
type: "stdio", command: [
|
|
403
|
-
"npx", "-y", "mcp-to-openai",
|
|
404
|
-
"--service", spec.name,
|
|
405
|
-
"--mcp-tool", "query",
|
|
406
|
-
...(envKey === "OPENROUTER" ? [
|
|
407
|
-
"--openai-url", "https://openrouter.ai/api/v1",
|
|
408
|
-
"--openai-api", "completion",
|
|
409
|
-
"--openai-model", "openai/gpt-5.5"
|
|
410
|
-
] : [
|
|
411
|
-
"--openai-url", "https://api.openai.com/v1",
|
|
412
|
-
"--openai-api", "responses",
|
|
413
|
-
"--openai-model", "gpt-5.5"
|
|
414
|
-
])
|
|
415
|
-
]
|
|
416
|
-
});
|
|
417
|
-
else
|
|
418
|
-
await this.mcpRemove(tool, spec.server);
|
|
419
|
-
}
|
|
433
|
+
handler: this.chatMcpHandler({ url: "https://api.openai.com/v1", api: "responses", model: "gpt-5.5" }, { model: "openai/gpt-5.5" })
|
|
420
434
|
},
|
|
421
435
|
{
|
|
422
436
|
id: "google-gemini",
|
|
@@ -425,27 +439,7 @@ export default class SetupCommand {
|
|
|
425
439
|
env: ["GOOGLE_GEMINI", "OPENROUTER"],
|
|
426
440
|
server: "chat-google-gemini",
|
|
427
441
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
428
|
-
handler:
|
|
429
|
-
if (action === "activate")
|
|
430
|
-
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
431
|
-
type: "stdio", command: [
|
|
432
|
-
"npx", "-y", "mcp-to-openai",
|
|
433
|
-
"--service", spec.name,
|
|
434
|
-
"--mcp-tool", "query",
|
|
435
|
-
...(envKey === "OPENROUTER" ? [
|
|
436
|
-
"--openai-url", "https://openrouter.ai/api/v1",
|
|
437
|
-
"--openai-api", "completion",
|
|
438
|
-
"--openai-model", "google/gemini-3.5-flash"
|
|
439
|
-
] : [
|
|
440
|
-
"--openai-url", "https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
441
|
-
"--openai-api", "completion",
|
|
442
|
-
"--openai-model", "gemini-3.5-flash"
|
|
443
|
-
])
|
|
444
|
-
]
|
|
445
|
-
});
|
|
446
|
-
else
|
|
447
|
-
await this.mcpRemove(tool, spec.server);
|
|
448
|
-
}
|
|
442
|
+
handler: this.chatMcpHandler({ url: "https://generativelanguage.googleapis.com/v1beta/openai/", api: "completion", model: "gemini-3.5-flash" }, { model: "google/gemini-3.5-flash" })
|
|
449
443
|
},
|
|
450
444
|
{
|
|
451
445
|
id: "deepseek",
|
|
@@ -454,27 +448,7 @@ export default class SetupCommand {
|
|
|
454
448
|
env: ["DEEPSEEK", "OPENROUTER"],
|
|
455
449
|
server: "chat-deepseek",
|
|
456
450
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
457
|
-
handler:
|
|
458
|
-
if (action === "activate")
|
|
459
|
-
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
460
|
-
type: "stdio", command: [
|
|
461
|
-
"npx", "-y", "mcp-to-openai",
|
|
462
|
-
"--service", spec.name,
|
|
463
|
-
"--mcp-tool", "query",
|
|
464
|
-
...(envKey === "OPENROUTER" ? [
|
|
465
|
-
"--openai-url", "https://openrouter.ai/api/v1",
|
|
466
|
-
"--openai-api", "completion",
|
|
467
|
-
"--openai-model", "deepseek/deepseek-v4-flash"
|
|
468
|
-
] : [
|
|
469
|
-
"--openai-url", "https://api.deepseek.com/v1",
|
|
470
|
-
"--openai-api", "completion",
|
|
471
|
-
"--openai-model", "deepseek-v4-flash"
|
|
472
|
-
])
|
|
473
|
-
]
|
|
474
|
-
});
|
|
475
|
-
else
|
|
476
|
-
await this.mcpRemove(tool, spec.server);
|
|
477
|
-
}
|
|
451
|
+
handler: this.chatMcpHandler({ url: "https://api.deepseek.com/v1", api: "completion", model: "deepseek-v4-flash" }, { model: "deepseek/deepseek-v4-flash" })
|
|
478
452
|
},
|
|
479
453
|
{
|
|
480
454
|
id: "xai-grok",
|
|
@@ -483,27 +457,7 @@ export default class SetupCommand {
|
|
|
483
457
|
env: ["XAI_GROK", "OPENROUTER"],
|
|
484
458
|
server: "chat-xai-grok",
|
|
485
459
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
486
|
-
handler:
|
|
487
|
-
if (action === "activate")
|
|
488
|
-
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
489
|
-
type: "stdio", command: [
|
|
490
|
-
"npx", "-y", "mcp-to-openai",
|
|
491
|
-
"--service", spec.name,
|
|
492
|
-
"--mcp-tool", "query",
|
|
493
|
-
...(envKey === "OPENROUTER" ? [
|
|
494
|
-
"--openai-url", "https://openrouter.ai/api/v1",
|
|
495
|
-
"--openai-api", "completion",
|
|
496
|
-
"--openai-model", "x-ai/grok-4.3"
|
|
497
|
-
] : [
|
|
498
|
-
"--openai-url", "https://api.x.ai/v1",
|
|
499
|
-
"--openai-api", "completion",
|
|
500
|
-
"--openai-model", "grok-4.3"
|
|
501
|
-
])
|
|
502
|
-
]
|
|
503
|
-
});
|
|
504
|
-
else
|
|
505
|
-
await this.mcpRemove(tool, spec.server);
|
|
506
|
-
}
|
|
460
|
+
handler: this.chatMcpHandler({ url: "https://api.x.ai/v1", api: "completion", model: "grok-4.3" }, { model: "x-ai/grok-4.3" })
|
|
507
461
|
},
|
|
508
462
|
{
|
|
509
463
|
id: "alibaba-qwen",
|
|
@@ -512,27 +466,7 @@ export default class SetupCommand {
|
|
|
512
466
|
env: ["ALIBABA_QWEN", "OPENROUTER"],
|
|
513
467
|
server: "chat-alibaba-qwen",
|
|
514
468
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
515
|
-
handler:
|
|
516
|
-
if (action === "activate")
|
|
517
|
-
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
518
|
-
type: "stdio", command: [
|
|
519
|
-
"npx", "-y", "mcp-to-openai",
|
|
520
|
-
"--service", spec.name,
|
|
521
|
-
"--mcp-tool", "query",
|
|
522
|
-
...(envKey === "OPENROUTER" ? [
|
|
523
|
-
"--openai-url", "https://openrouter.ai/api/v1",
|
|
524
|
-
"--openai-api", "completion",
|
|
525
|
-
"--openai-model", "qwen/qwen3.7-max"
|
|
526
|
-
] : [
|
|
527
|
-
"--openai-url", "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
528
|
-
"--openai-api", "completion",
|
|
529
|
-
"--openai-model", "qwen3.7-max"
|
|
530
|
-
])
|
|
531
|
-
]
|
|
532
|
-
});
|
|
533
|
-
else
|
|
534
|
-
await this.mcpRemove(tool, spec.server);
|
|
535
|
-
}
|
|
469
|
+
handler: this.chatMcpHandler({ url: "https://dashscope.aliyuncs.com/compatible-mode/v1", api: "completion", model: "qwen3.7-max" }, { model: "qwen/qwen3.7-max" })
|
|
536
470
|
},
|
|
537
471
|
{
|
|
538
472
|
id: "zai-glm",
|
|
@@ -541,27 +475,7 @@ export default class SetupCommand {
|
|
|
541
475
|
env: ["ZAI_GLM", "OPENROUTER"],
|
|
542
476
|
server: "chat-zai-glm",
|
|
543
477
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
544
|
-
handler:
|
|
545
|
-
if (action === "activate")
|
|
546
|
-
await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
|
|
547
|
-
type: "stdio", command: [
|
|
548
|
-
"npx", "-y", "mcp-to-openai",
|
|
549
|
-
"--service", spec.name,
|
|
550
|
-
"--mcp-tool", "query",
|
|
551
|
-
...(envKey === "OPENROUTER" ? [
|
|
552
|
-
"--openai-url", "https://openrouter.ai/api/v1",
|
|
553
|
-
"--openai-api", "completion",
|
|
554
|
-
"--openai-model", "z-ai/glm-5.1"
|
|
555
|
-
] : [
|
|
556
|
-
"--openai-url", "https://api.z.ai/api/paas/v4/",
|
|
557
|
-
"--openai-api", "completion",
|
|
558
|
-
"--openai-model", "glm-5.1"
|
|
559
|
-
])
|
|
560
|
-
]
|
|
561
|
-
});
|
|
562
|
-
else
|
|
563
|
-
await this.mcpRemove(tool, spec.server);
|
|
564
|
-
}
|
|
478
|
+
handler: this.chatMcpHandler({ url: "https://api.z.ai/api/paas/v4/", api: "completion", model: "glm-5.1" }, { model: "z-ai/glm-5.1" })
|
|
565
479
|
},
|
|
566
480
|
{
|
|
567
481
|
id: "brave",
|