@rse/ase 2.0.5 → 2.0.7
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-agent-definition-api.d.ts +2 -0
- package/dst/ase-agent-definition-core.js +24 -3
- package/dst/ase-agent-fleet.js +2 -1
- package/dst/ase-agent-runtime-engine.js +22 -4
- package/dst/ase-agent-runtime-tool.js +10 -8
- package/dst/ase-agent-runtime.js +8 -4
- package/dst/ase-config-schema.js +1 -1
- package/dst/ase-man-catalog.js +1 -1
- package/dst/ase-setup-settings.js +111 -1
- package/dst/ase-setup.js +19 -7
- package/dst/ase-spec.js +184 -49
- package/dst/ase-task-board-tui-model.js +8 -1
- package/dst/ase-task-board-tui.js +26 -2
- package/dst/ase-task-board-web-client.js +10 -10
- package/dst/ase-task-store-plugin-gitea.js +3 -3
- package/dst/man/ase-agent-adhoc.md +25 -6
- package/dst/man/ase-agent-condenser.md +7 -0
- package/dst/man/ase-agent-editor.md +7 -0
- package/dst/man/ase-agent-estimator.md +7 -0
- package/dst/man/ase-agent-fleet.md +7 -4
- package/dst/man/ase-agent-implementer.md +7 -0
- package/dst/man/ase-agent-integrator.md +7 -0
- package/dst/man/ase-agent-qualifier.md +7 -0
- package/dst/man/ase-agent-rebooter.md +7 -0
- package/dst/man/ase-agent-researcher.md +7 -0
- package/dst/man/ase-artifact.md +1 -1
- package/dst/man/ase-config.md +5 -3
- package/dst/man/ase-mcp.md +1 -1
- package/dst/man/ase-setup-install.md +10 -1
- package/dst/man/ase-setup-status.md +6 -0
- package/dst/man/ase-setup-uninstall.md +3 -1
- package/dst/man/ase-setup-update.md +3 -0
- package/dst/man/ase-setup.md +4 -4
- package/dst/man/ase-spec-export.md +14 -3
- package/dst/man/ase-spec-init.md +10 -1
- package/dst/man/ase-spec-lint.md +2 -1
- package/dst/man/ase-spec-preview.md +10 -1
- package/dst/man/ase-spec-query.md +96 -0
- package/dst/man/ase-spec.md +32 -11
- package/dst/man/ase.md +3 -3
- package/package.json +20 -17
- package/plugin/.claude-plugin/icon.png +0 -0
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/meta/ase-common-task.md +9 -1
- package/plugin/meta/ase-control.md +14 -0
- package/plugin/meta/ase-format-spec.md +29 -5
- package/plugin/meta/ase-format-specbook.md +427 -47
- package/plugin/meta/ase-format-specbook.yaml +189 -87
- package/plugin/meta/ase-skill.md +7 -0
- package/plugin/mods/ase-mods-common.ts +36 -6
- package/plugin/mods/ase-mods-evaluate.ts +227 -0
- package/plugin/mods/ase-mods-grill.ts +1 -30
- package/plugin/mods/ase-mods-help-browse.ts +18 -13
- package/plugin/mods/ase-mods-help-find.ts +17 -9
- package/plugin/mods/ase-mods-help-hint.ts +1 -1
- package/plugin/mods/ase-mods.ts +23 -17
- package/plugin/skills/ase-code-edit/SKILL.md +53 -16
- package/plugin/skills/ase-code-edit/help.md +26 -13
- package/plugin/skills/ase-code-lint/SKILL.md +7 -3
- package/plugin/skills/ase-help-intent/data.md +170 -28
- package/plugin/skills/ase-help-skill/catalog.md +1 -0
- package/plugin/skills/ase-meta-workflow/workflow.txt +2 -0
- package/plugin/skills/ase-repo-changelog/SKILL.md +29 -10
- package/plugin/skills/ase-repo-changelog/help.md +5 -1
- package/plugin/skills/ase-repo-commit/SKILL.md +4 -1
- package/plugin/skills/ase-repo-dissect/help.md +2 -1
- package/plugin/skills/ase-repo-stage/SKILL.md +459 -0
- package/plugin/skills/ase-repo-stage/help.md +120 -0
- package/plugin/skills/ase-spec-activate/SKILL.md +4 -1
- package/plugin/skills/ase-spec-edit/SKILL.md +20 -12
- package/plugin/skills/ase-spec-edit/help.md +22 -14
- package/plugin/mods/claude-code.d.ts +0 -13440
package/dst/ase-spec.js
CHANGED
|
@@ -16,22 +16,33 @@ import { configSchema } from "./ase-config-schema.js";
|
|
|
16
16
|
import { Task } from "./ase-task.js";
|
|
17
17
|
import { Artifact } from "./ase-artifact.js";
|
|
18
18
|
import { writeStdout } from "./ase-lib-stdio.js";
|
|
19
|
-
/* reusable functionality: initialize, lint, export, and preview the
|
|
19
|
+
/* reusable functionality: initialize, lint, export, query, and preview the
|
|
20
20
|
SpecBook-based project specification, located via the
|
|
21
21
|
"project.artifact.spec.basedir" and "project.artifact.spec.schema"
|
|
22
22
|
configuration */
|
|
23
23
|
export class Spec {
|
|
24
|
-
/* the SpecBook export formats, mirrored here (and checked for completeness
|
|
24
|
+
/* the SpecBook export and query formats, mirrored here (and checked for completeness
|
|
25
25
|
against SpecBook at compile-time) to not load SpecBook for the MCP schema */
|
|
26
26
|
static formats = Object.keys({
|
|
27
27
|
json: true, json5: true, yaml: true, toon: true, html: true, pdf: true, md: true
|
|
28
28
|
});
|
|
29
|
+
static queryFormats = Object.keys({
|
|
30
|
+
json: true, json5: true, yaml: true, toon: true
|
|
31
|
+
});
|
|
32
|
+
/* whether a schema configuration entry is a reference SpecBook resolves
|
|
33
|
+
itself (mirroring its internal check): the bundled "std", "meta", and
|
|
34
|
+
"pm" ones, or a remote "npm:", "github:", or "http[s]://" one */
|
|
35
|
+
static isRemote(entry) {
|
|
36
|
+
return entry === "std" || entry === "meta" || entry === "pm"
|
|
37
|
+
|| (/^(?:npm|github):|^https?:\/\//).test(entry);
|
|
38
|
+
}
|
|
29
39
|
/* resolve the YAML schema configuration files: the whitespace-separated
|
|
30
40
|
entries of the configured "project.artifact.spec.schema", merged in
|
|
31
|
-
the given order, where each entry is either
|
|
32
|
-
full SpecBook standard schema
|
|
33
|
-
"ase-format-specbook.yaml") or a file
|
|
34
|
-
root, and an unset or
|
|
41
|
+
the given order, where each entry is either a reference SpecBook resolves
|
|
42
|
+
itself (like "std" for the full SpecBook standard schema, not the
|
|
43
|
+
agent-compressed plugin meta file "ase-format-specbook.yaml") or a file
|
|
44
|
+
path or glob pattern relative to the project root, and an unset or
|
|
45
|
+
empty value means "std" */
|
|
35
46
|
static configFiles(log) {
|
|
36
47
|
const cfg = new Config("config", configSchema, log);
|
|
37
48
|
cfg.read();
|
|
@@ -40,10 +51,21 @@ export class Spec {
|
|
|
40
51
|
const entries = list.split(/\s+/).filter((entry) => entry !== "");
|
|
41
52
|
if (entries.length === 0)
|
|
42
53
|
entries.push("std");
|
|
43
|
-
return entries.map((entry) => entry
|
|
44
|
-
|
|
54
|
+
return entries.map((entry) => Spec.isRemote(entry) ?
|
|
55
|
+
entry :
|
|
45
56
|
path.resolve(Task.projectRoot(), entry));
|
|
46
57
|
}
|
|
58
|
+
/* the source options of the reading commands: the specification below
|
|
59
|
+
the "spec" artifact base directory, or, under "schema", the effective
|
|
60
|
+
schema configuration itself (to which no base directory applies) */
|
|
61
|
+
static source(log, schema) {
|
|
62
|
+
return {
|
|
63
|
+
config: Spec.configFiles(log),
|
|
64
|
+
basedir: schema ? undefined : Artifact.basedir(log, "spec"),
|
|
65
|
+
gitignore: Spec.gitignore,
|
|
66
|
+
schema
|
|
67
|
+
};
|
|
68
|
+
}
|
|
47
69
|
/* the ASE log level each SpecBook verbosity level maps onto: the
|
|
48
70
|
"none" level carries the messages a bare SpecBook CLI run always
|
|
49
71
|
prints -- the environment notices plus the lifecycle and failure
|
|
@@ -104,15 +126,17 @@ export class Spec {
|
|
|
104
126
|
/* initialize the absent artifact files of the schema configuration below
|
|
105
127
|
the "spec" artifact base directory, letting SpecBook persist its project
|
|
106
128
|
options file ".specbook.yaml" into a throw-away working directory, as
|
|
107
|
-
ASE keeps these options in its own configuration
|
|
108
|
-
|
|
129
|
+
ASE keeps these options in its own configuration, where "sample"
|
|
130
|
+
scaffolds one sample object per allowed object kind into the files */
|
|
131
|
+
static async init(log, sample = false) {
|
|
109
132
|
const basedir = Artifact.basedir(log, "spec");
|
|
110
133
|
const cwd = await fs.promises.mkdtemp(path.join(os.tmpdir(), "ase-spec-"));
|
|
111
134
|
try {
|
|
112
135
|
const result = await Spec.unmarked(Spec.api(log).then((api) => api.init({
|
|
113
136
|
config: Spec.configFiles(log),
|
|
114
137
|
basedir,
|
|
115
|
-
cwd
|
|
138
|
+
cwd,
|
|
139
|
+
sample
|
|
116
140
|
})));
|
|
117
141
|
return result.files.map((file) => Spec.relativize(path.join(basedir, file)));
|
|
118
142
|
}
|
|
@@ -196,38 +220,46 @@ export class Spec {
|
|
|
196
220
|
});
|
|
197
221
|
}
|
|
198
222
|
/* export the specification Markdown files below the "spec" artifact
|
|
199
|
-
base directory into the requested formats, one buffer per format
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
gitignore: Spec.gitignore,
|
|
206
|
-
formats
|
|
223
|
+
base directory into the requested formats, one buffer per format */
|
|
224
|
+
static export(log, formats, options = {}) {
|
|
225
|
+
return Spec.unmarked(Spec.api(log, options.notices).then((api) => api.export({
|
|
226
|
+
...Spec.source(log, options.schema ?? false),
|
|
227
|
+
formats,
|
|
228
|
+
rebase: options.rebase
|
|
207
229
|
})));
|
|
208
230
|
}
|
|
209
231
|
/* export the specification like "export" and then keep the export
|
|
210
232
|
in sync with its sources, handing every fresh set of buffers to
|
|
211
233
|
"onExport", where "outputs" names the files "onExport" writes,
|
|
212
234
|
so an output which is itself an observed source can be refused */
|
|
213
|
-
static watch(log, formats, outputs, onExport) {
|
|
214
|
-
return Spec.unmarked(Spec.api(log).then((api) => api.watch({
|
|
215
|
-
|
|
216
|
-
basedir: Artifact.basedir(log, "spec"),
|
|
217
|
-
gitignore: Spec.gitignore,
|
|
235
|
+
static watch(log, formats, outputs, onExport, options = {}) {
|
|
236
|
+
return Spec.unmarked(Spec.api(log, options.notices).then((api) => api.watch({
|
|
237
|
+
...Spec.source(log, options.schema ?? false),
|
|
218
238
|
formats,
|
|
239
|
+
rebase: options.rebase,
|
|
219
240
|
outputs,
|
|
220
241
|
onExport
|
|
221
242
|
})));
|
|
222
243
|
}
|
|
223
|
-
/*
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
/* query the specification Markdown files below the "spec" artifact
|
|
245
|
+
base directory (or, under "schema", the effective schema configuration)
|
|
246
|
+
with an ASTq selector and its "{name}" parameters, rendering the matched
|
|
247
|
+
nodes in a serialization format and collecting the emitted notices
|
|
248
|
+
(like the lint warnings) if requested */
|
|
249
|
+
static query(log, selector, params, format, notices, schema = false) {
|
|
250
|
+
return Spec.unmarked(Spec.api(log, notices).then(async (api) => {
|
|
251
|
+
const { renderNodes } = await import("@rse/specbook");
|
|
252
|
+
const document = await api.load(Spec.source(log, schema));
|
|
253
|
+
return renderNodes(document.query(selector, params), format);
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
/* serve the HTML export of the specification (or, under "schema", of
|
|
257
|
+
the effective schema configuration) as a live preview, kept in sync
|
|
258
|
+
with its sources and pushed into the connected browsers as an
|
|
259
|
+
in-place document update */
|
|
260
|
+
static preview(log, addr, port, schema = false) {
|
|
227
261
|
return Spec.unmarked(Spec.api(log).then((api) => api.preview({
|
|
228
|
-
|
|
229
|
-
basedir: Artifact.basedir(log, "spec"),
|
|
230
|
-
gitignore: Spec.gitignore,
|
|
262
|
+
...Spec.source(log, schema),
|
|
231
263
|
addr,
|
|
232
264
|
port
|
|
233
265
|
})));
|
|
@@ -244,7 +276,7 @@ export default class SpecCommand {
|
|
|
244
276
|
/* register CLI top-level command "ase spec" */
|
|
245
277
|
const spec = program
|
|
246
278
|
.command("spec")
|
|
247
|
-
.description("Initialize, lint, export, and preview the SpecBook-based project specification")
|
|
279
|
+
.description("Initialize, lint, export, query, and preview the SpecBook-based project specification")
|
|
248
280
|
.action(() => {
|
|
249
281
|
spec.outputHelp();
|
|
250
282
|
process.exit(1);
|
|
@@ -253,8 +285,9 @@ export default class SpecCommand {
|
|
|
253
285
|
spec
|
|
254
286
|
.command("init")
|
|
255
287
|
.description("Initialize the absent specification Markdown files of the SpecBook schema configuration")
|
|
256
|
-
.
|
|
257
|
-
|
|
288
|
+
.option("-s, --sample", "scaffold one sample object per allowed object kind into the created files")
|
|
289
|
+
.action(async (opts) => {
|
|
290
|
+
const files = await Spec.init(this.log, opts.sample === true);
|
|
258
291
|
await writeStdout(files.length > 0 ?
|
|
259
292
|
`initialized artifact file(s): ${files.join(", ")}\n` :
|
|
260
293
|
"no artifact files were created\n");
|
|
@@ -286,36 +319,75 @@ export default class SpecCommand {
|
|
|
286
319
|
.description("Export the specification Markdown files as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
287
320
|
.option("-o, --output <[format:]file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
288
321
|
"from the filename extension unless explicitly prefixed " +
|
|
289
|
-
"(default: \"index.html\"
|
|
322
|
+
"(default: \"index.html\", resp. \"schema.html\" under \"--schema\", " +
|
|
323
|
+
"inside the specification base directory)", (value, previous) => previous.concat(value), new Array())
|
|
290
324
|
.option("-w, --watch", "keep the outputs in sync by re-exporting on every source change")
|
|
325
|
+
.option("-S, --schema", "export the effective schema configuration itself instead of the specification")
|
|
291
326
|
.action(async (opts) => {
|
|
292
327
|
const { parseOutputSpec } = await import("@rse/specbook");
|
|
328
|
+
const schema = opts.schema === true;
|
|
293
329
|
const outputs = (opts.output.length > 0 ? opts.output :
|
|
294
|
-
[path.join(Artifact.basedir(this.log, "spec"), "index.html")])
|
|
330
|
+
[path.join(Artifact.basedir(this.log, "spec"), schema ? "schema.html" : "index.html")])
|
|
331
|
+
.map(parseOutputSpec);
|
|
295
332
|
/* a re-export has to land somewhere it can be picked up again,
|
|
296
333
|
which a one-shot stdout stream cannot provide */
|
|
297
334
|
if (opts.watch === true && outputs.some(({ output }) => output === "-"))
|
|
298
335
|
throw new Error("the watch mode requires regular output files " +
|
|
299
336
|
"(\"-\" for stdout is not supported)");
|
|
300
|
-
/* parse the input once and export each distinct format once
|
|
301
|
-
|
|
337
|
+
/* parse the input once and export each distinct format once, the
|
|
338
|
+
normalized Markdown once per output directory, as its image
|
|
339
|
+
references are re-based onto it (but never for stdout) */
|
|
340
|
+
const rebaseOf = ({ format, output }) => format === "md" && output !== "-" ? path.dirname(path.resolve(output)) : undefined;
|
|
341
|
+
const keyOf = (spec) => `${spec.format}:${rebaseOf(spec) ?? ""}`;
|
|
342
|
+
const distinct = outputs.filter((spec, i) => outputs.findIndex((other) => keyOf(other) === keyOf(spec)) === i);
|
|
302
343
|
const write = async (buffers) => {
|
|
303
|
-
for (const
|
|
304
|
-
const data = buffers[distinct.
|
|
305
|
-
if (output === "-")
|
|
344
|
+
for (const spec of outputs) {
|
|
345
|
+
const data = buffers[distinct.findIndex((other) => keyOf(other) === keyOf(spec))];
|
|
346
|
+
if (spec.output === "-")
|
|
306
347
|
await writeStdout(data);
|
|
307
348
|
else {
|
|
308
|
-
await fs.promises.writeFile(output, data);
|
|
309
|
-
this.log.write("info", `spec: exported specification into "${output}" (${data.length} bytes)`);
|
|
349
|
+
await fs.promises.writeFile(spec.output, data);
|
|
350
|
+
this.log.write("info", `spec: exported specification into "${spec.output}" (${data.length} bytes)`);
|
|
310
351
|
}
|
|
311
352
|
}
|
|
312
353
|
};
|
|
354
|
+
const formats = distinct.map(({ format }) => format);
|
|
355
|
+
const rebase = distinct.map(rebaseOf);
|
|
313
356
|
if (opts.watch === true) {
|
|
314
|
-
await Spec.watch(this.log,
|
|
357
|
+
await Spec.watch(this.log, formats, outputs.map(({ output }) => output), write, { schema, rebase });
|
|
315
358
|
await new Promise(() => { });
|
|
316
359
|
}
|
|
317
360
|
else
|
|
318
|
-
await write(await Spec.export(this.log,
|
|
361
|
+
await write(await Spec.export(this.log, formats, { schema, rebase }));
|
|
362
|
+
});
|
|
363
|
+
/* register CLI sub-command "ase spec query" */
|
|
364
|
+
spec
|
|
365
|
+
.command("query")
|
|
366
|
+
.description("Query the specification Markdown files with an ASTq selector, " +
|
|
367
|
+
"writing the matched nodes as JSON, JSON5, YAML, or TOON")
|
|
368
|
+
.argument("<selector>", "ASTq selector of the nodes to write")
|
|
369
|
+
.option("-P, --param <name=value>", "value of a \"{name}\" parameter of the selector (repeatable)", (value, previous) => previous.concat(value), new Array())
|
|
370
|
+
.option("-f, --format <format>", `output format (${Spec.queryFormats.join(", ")}; ` +
|
|
371
|
+
"default: inferred from the output file like for \"export\", else json)")
|
|
372
|
+
.option("-o, --output <[format:]file>", "output file (\"-\" for stdout)", "-")
|
|
373
|
+
.option("-S, --schema", "query the effective schema configuration itself instead of the specification")
|
|
374
|
+
.action(async (selector, opts) => {
|
|
375
|
+
const { parseQueryOutput } = await import("@rse/specbook");
|
|
376
|
+
const output = parseQueryOutput(opts.output, opts.format);
|
|
377
|
+
const params = {};
|
|
378
|
+
for (const param of opts.param) {
|
|
379
|
+
const m = (/^([^=]+)=(.*)$/s).exec(param);
|
|
380
|
+
if (m === null)
|
|
381
|
+
throw new Error(`invalid query parameter "${param}" (expected "<name>=<value>")`);
|
|
382
|
+
params[m[1]] = m[2];
|
|
383
|
+
}
|
|
384
|
+
const text = await Spec.query(this.log, selector, params, output.format, undefined, opts.schema === true);
|
|
385
|
+
if (output.output === "-")
|
|
386
|
+
await writeStdout(text);
|
|
387
|
+
else {
|
|
388
|
+
await fs.promises.writeFile(output.output, text);
|
|
389
|
+
this.log.write("info", `spec: queried specification into "${output.output}" (${Buffer.byteLength(text)} bytes)`);
|
|
390
|
+
}
|
|
319
391
|
});
|
|
320
392
|
/* register CLI sub-command "ase spec preview" */
|
|
321
393
|
spec
|
|
@@ -323,12 +395,13 @@ export default class SpecCommand {
|
|
|
323
395
|
.description("Serve the HTML export of the specification Markdown files as a live preview")
|
|
324
396
|
.option("-a, --addr <ip-addr>", "IP address to listen on (default: SpecBook preview address)")
|
|
325
397
|
.option("-p, --port <tcp-port>", "TCP port to listen on (default: SpecBook preview port)")
|
|
398
|
+
.option("-S, --schema", "preview the effective schema configuration itself instead of the specification")
|
|
326
399
|
.action(async (opts) => {
|
|
327
400
|
const { previewAddr, previewPort } = await import("@rse/specbook");
|
|
328
401
|
const port = Number(opts.port ?? previewPort);
|
|
329
402
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
330
403
|
throw new Error(`invalid TCP port "${opts.port}"`);
|
|
331
|
-
await Spec.preview(this.log, opts.addr ?? previewAddr, port);
|
|
404
|
+
await Spec.preview(this.log, opts.addr ?? previewAddr, port, opts.schema === true);
|
|
332
405
|
await new Promise(() => { });
|
|
333
406
|
});
|
|
334
407
|
}
|
|
@@ -406,12 +479,15 @@ export class SpecMCP {
|
|
|
406
479
|
"(PDF as a base64-encoded resource). The export fails on any lint diagnostic. " +
|
|
407
480
|
"The `notices` array carries the environment notices emitted during the export " +
|
|
408
481
|
"(like a PDF rendering falling back onto a system-installed browser), which are " +
|
|
409
|
-
"worth reporting to the user verbatim."
|
|
482
|
+
"worth reporting to the user verbatim. With `schema`, the effective SpecBook YAML " +
|
|
483
|
+
"schema configuration itself is exported instead, as a SpecBook document.",
|
|
410
484
|
inputSchema: {
|
|
411
485
|
format: z.enum(Spec.formats).optional()
|
|
412
486
|
.describe("output format (default: inferred from the `output` file extension, else `json`)"),
|
|
413
487
|
output: z.string().optional()
|
|
414
|
-
.describe("output file path (\"-\" or omitted returns the result directly)")
|
|
488
|
+
.describe("output file path (\"-\" or omitted returns the result directly)"),
|
|
489
|
+
schema: z.boolean().optional()
|
|
490
|
+
.describe("if true, export the effective schema configuration instead of the specification (default: false)")
|
|
415
491
|
},
|
|
416
492
|
outputSchema: {
|
|
417
493
|
notices: z.array(z.string())
|
|
@@ -426,7 +502,11 @@ export class SpecMCP {
|
|
|
426
502
|
const spec = args.format !== undefined || args.output === undefined ?
|
|
427
503
|
{ format: args.format ?? "json", output: args.output } :
|
|
428
504
|
parseOutputSpec(args.output);
|
|
429
|
-
|
|
505
|
+
/* the image references of the normalized Markdown are re-based
|
|
506
|
+
onto the directory of the output file */
|
|
507
|
+
const rebase = spec.format === "md" && spec.output !== undefined && spec.output !== "-" ?
|
|
508
|
+
path.dirname(path.resolve(Task.projectRoot(), spec.output)) : undefined;
|
|
509
|
+
const [data] = await Spec.export(this.log, [spec.format], { notices, schema: args.schema ?? false, rebase: [rebase] });
|
|
430
510
|
if (spec.output !== undefined && spec.output !== "-") {
|
|
431
511
|
await fs.promises.writeFile(path.resolve(Task.projectRoot(), spec.output), data);
|
|
432
512
|
return {
|
|
@@ -452,5 +532,60 @@ export class SpecMCP {
|
|
|
452
532
|
return mcpToolError(err);
|
|
453
533
|
}
|
|
454
534
|
});
|
|
535
|
+
mcp.registerTool("ase_specbook_query", {
|
|
536
|
+
title: "ASE SpecBook query",
|
|
537
|
+
description: "Query the SpecBook specification Markdown files of the project (located via the " +
|
|
538
|
+
"`project.artifact.spec.basedir` configuration), strictly validated like for the export, " +
|
|
539
|
+
"with an XPath-like ASTq `selector` (like `// USE-CASE [ @STATUS == 'Done' ]`). " +
|
|
540
|
+
"The root node is of type `specification`, its children are of type `artifact` (attributes " +
|
|
541
|
+
"`created`, `modified`, and `file`), and the objects below are typed by their kind, with " +
|
|
542
|
+
"whitespace dashed (attributes `kind`, `id`, `anchor`, `paren`, `name`, `primary`, " +
|
|
543
|
+
"`description`, `rationale`, `elaboration`, and one per property key), where the query " +
|
|
544
|
+
"function `refs()` yields the objects the references of an object resolve to. A `{name}` " +
|
|
545
|
+
"in the selector is substituted by the corresponding entry of `params`. The matched nodes " +
|
|
546
|
+
"are rendered as a JSON (or JSON5, YAML, or TOON) array (their subtrees included, the " +
|
|
547
|
+
"embedded file contents dropped), written to the `output` file (a relative path resolves " +
|
|
548
|
+
"against the project root) if given, else returned directly. With `schema`, the effective " +
|
|
549
|
+
"SpecBook YAML schema configuration itself is queried instead, as a SpecBook document of " +
|
|
550
|
+
"the bundled meta schema configuration. The `notices` array carries the messages emitted " +
|
|
551
|
+
"during the loading (like the warning diagnostics of the specification).",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
selector: z.string()
|
|
554
|
+
.describe("ASTq selector of the nodes to return"),
|
|
555
|
+
params: z.record(z.string(), z.string()).optional()
|
|
556
|
+
.describe("values of the `{name}` parameters of the selector (default: none)"),
|
|
557
|
+
format: z.enum(Spec.queryFormats).optional()
|
|
558
|
+
.describe("output format (default: inferred from the `output` file extension, else `json`)"),
|
|
559
|
+
output: z.string().optional()
|
|
560
|
+
.describe("output file path (\"-\" or omitted returns the result directly)"),
|
|
561
|
+
schema: z.boolean().optional()
|
|
562
|
+
.describe("if true, query the effective schema configuration instead of the specification (default: false)")
|
|
563
|
+
},
|
|
564
|
+
outputSchema: {
|
|
565
|
+
notices: z.array(z.string())
|
|
566
|
+
.describe("messages emitted during the loading, empty if there were none")
|
|
567
|
+
}
|
|
568
|
+
}, async (args) => {
|
|
569
|
+
const notices = new Array();
|
|
570
|
+
try {
|
|
571
|
+
const { parseQueryOutput } = await import("@rse/specbook");
|
|
572
|
+
const spec = parseQueryOutput(args.output ?? "-", args.format);
|
|
573
|
+
const text = await Spec.query(this.log, args.selector, args.params ?? {}, spec.format, notices, args.schema ?? false);
|
|
574
|
+
if (spec.output !== "-") {
|
|
575
|
+
await fs.promises.writeFile(path.resolve(Task.projectRoot(), spec.output), text);
|
|
576
|
+
return {
|
|
577
|
+
structuredContent: { notices },
|
|
578
|
+
content: [{ type: "text", text: `queried specification into "${spec.output}" (${Buffer.byteLength(text)} bytes)` }]
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
structuredContent: { notices },
|
|
583
|
+
content: [{ type: "text", text }]
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
return mcpToolError(err);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
455
590
|
}
|
|
456
591
|
}
|
|
@@ -947,7 +947,8 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
947
947
|
const viewW = innerW - 4;
|
|
948
948
|
let { x, y } = scroll;
|
|
949
949
|
const box = view === "graph" ? places.get(sel.id) : view === "flow" ? flowPlaces.get(flowSel) : undefined;
|
|
950
|
-
const
|
|
950
|
+
const graph = view === "graph" ? layout?.graph : view === "flow" ? flowLayout?.graph : undefined;
|
|
951
|
+
const height = graph?.height;
|
|
951
952
|
const follow = `${view}:${flowSel}`;
|
|
952
953
|
if (box !== undefined && height !== undefined && (view !== "flow" || followed.current !== follow)) {
|
|
953
954
|
const top = Math.max(0, box.top - 2);
|
|
@@ -965,6 +966,12 @@ export const useBoardState = (log, initial, relay) => {
|
|
|
965
966
|
}
|
|
966
967
|
if (view !== "flow")
|
|
967
968
|
followed.current = "";
|
|
969
|
+
/* keep the viewport within the bounds of the shown graph, as the scroll position is shared
|
|
970
|
+
by both graphs (and else, a position carried over from the other one may show only its tail) */
|
|
971
|
+
if (graph !== undefined) {
|
|
972
|
+
x = Math.max(0, Math.min(Math.max(0, graph.width + 2 - viewW), x));
|
|
973
|
+
y = Math.max(0, Math.min(Math.max(0, graph.height + 1 - viewH), y));
|
|
974
|
+
}
|
|
968
975
|
if (x !== scroll.x || y !== scroll.y)
|
|
969
976
|
setScroll({ x, y });
|
|
970
977
|
/* scroll the viewport of the agent graph freely (within its bounds) */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { render, Box, Text, useInput, useStdin } from "ink";
|
|
8
|
+
import stringWidth from "string-width";
|
|
8
9
|
import { Task } from "./ase-task.js";
|
|
9
10
|
import { buildBoard, toneOf, viewLabels } from "./ase-task-board-core.js";
|
|
10
11
|
import pkg from "../package.json" with { type: "json" };
|
|
@@ -160,6 +161,25 @@ const App = ({ log, initial, project, relay }) => {
|
|
|
160
161
|
});
|
|
161
162
|
return renderScreen(ctx, project);
|
|
162
163
|
};
|
|
164
|
+
/* wrap stdout to erase the tail of each changed line ("CSI <n> G" plus "CSI K" plus
|
|
165
|
+
its styled text) after instead of before its text, as otherwise the line is shown
|
|
166
|
+
blanked in terminals redrawing between the reads of a frame, like tmux popups;
|
|
167
|
+
a text reaching the last column needs no erasing, which would there erase its
|
|
168
|
+
last character, as the cursor rests on it */
|
|
169
|
+
const lateErasing = (stream) => {
|
|
170
|
+
/* eslint-disable-next-line no-control-regex */
|
|
171
|
+
const changed = /\x1b\[(\d+)G\x1b\[K((?:[^\x1b\n]|\x1b\[[\d;:]*m)*)/g;
|
|
172
|
+
const write = (data, ...args) => stream.write(typeof data !== "string" ? data :
|
|
173
|
+
data.replace(changed, (_all, col, text) => `\x1b[${col}G${text}` + (Number(col) - 1 + stringWidth(text) < stream.columns ? "\x1b[K" : "")), ...args);
|
|
174
|
+
return new Proxy(stream, {
|
|
175
|
+
get: (target, prop) => {
|
|
176
|
+
if (prop === "write")
|
|
177
|
+
return write;
|
|
178
|
+
const value = Reflect.get(target, prop, target);
|
|
179
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
};
|
|
163
183
|
/* run the terminal board until the user quits */
|
|
164
184
|
export const runTUI = async (log, relay) => {
|
|
165
185
|
const initial = await buildBoard(log);
|
|
@@ -175,9 +195,13 @@ export const runTUI = async (log, relay) => {
|
|
|
175
195
|
process.once("exit", reset);
|
|
176
196
|
try {
|
|
177
197
|
/* enable the kitty keyboard protocol (if the terminal supports it),
|
|
178
|
-
as only under it the Shift modifier of RETURN is reported
|
|
198
|
+
as only under it the Shift modifier of RETURN is reported, and
|
|
199
|
+
rewrite only the changed lines of a frame, as otherwise every frame
|
|
200
|
+
is erased and redrawn entirely, which flickers in terminals not
|
|
201
|
+
honoring synchronized output (DEC mode 2026), like tmux popups */
|
|
179
202
|
const app = render(h(App, { log, initial, project, relay }), {
|
|
180
|
-
alternateScreen: true, exitOnCtrlC: true, patchConsole: true, kittyKeyboard: { mode: "auto" }
|
|
203
|
+
alternateScreen: true, exitOnCtrlC: true, patchConsole: true, kittyKeyboard: { mode: "auto" },
|
|
204
|
+
incrementalRendering: true, stdout: lateErasing(process.stdout)
|
|
181
205
|
});
|
|
182
206
|
await app.waitUntilExit();
|
|
183
207
|
}
|