@rse/ase 0.9.47 → 0.9.48
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-artifact.js +3 -1
- package/dst/ase-compat.js +1 -1
- package/dst/ase-config.js +31 -12
- package/dst/ase-diagram.js +10 -15
- package/dst/ase-getopt.js +66 -60
- package/dst/ase-hook.js +29 -60
- package/dst/ase-kv.js +18 -43
- package/dst/ase-mcp.js +1 -1
- package/dst/ase-meta.js +1 -1
- package/dst/ase-service.js +4 -4
- package/dst/ase-setup.js +17 -34
- package/dst/ase-skills.js +1 -1
- package/dst/ase-statusline.js +14 -14
- package/dst/ase-stdio.js +25 -0
- package/dst/ase-task.js +11 -11
- package/dst/ase.js +1 -1
- package/package.json +1 -1
- 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/package.json +1 -1
- package/plugin/skills/ase-help-intent/SKILL.md +3 -8
package/dst/ase-artifact.js
CHANGED
|
@@ -10,7 +10,7 @@ import { isScalar } from "yaml";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { Config, configSchema } from "./ase-config.js";
|
|
12
12
|
import { Task } from "./ase-task.js";
|
|
13
|
-
import { writeStdout } from "./ase-
|
|
13
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
14
14
|
/* the recognized artifact kinds, in descending precedence order;
|
|
15
15
|
"othr" is the implicit catch-all and is always resolved last */
|
|
16
16
|
export const artifactKinds = ["spec", "arch", "code", "docs", "infr", "othr"];
|
|
@@ -141,6 +141,8 @@ export class Artifact {
|
|
|
141
141
|
const basedir = Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
|
|
142
142
|
.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
|
|
143
143
|
.replace(/^\.$/, "");
|
|
144
|
+
if (basedir.split("/").includes(".."))
|
|
145
|
+
throw new Error(`artifact: configured "basedir" "${basedir}" must not escape the project root`);
|
|
144
146
|
const files = Artifact.configString(cfg, `project.artifact.${kind}.files`);
|
|
145
147
|
return { basedir, files };
|
|
146
148
|
}
|
package/dst/ase-compat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import { writeStdout } from "./ase-
|
|
6
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
7
7
|
/* the canonical expected values for every ase-meta-compat probe,
|
|
8
8
|
keyed by "<category>/<probe-name>" as used in the skill */
|
|
9
9
|
const EXPECTED = {
|
package/dst/ase-config.js
CHANGED
|
@@ -14,7 +14,8 @@ import Table from "cli-table3";
|
|
|
14
14
|
import writeFileAtomic from "write-file-atomic";
|
|
15
15
|
import lockfile from "proper-lockfile";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
-
import {
|
|
17
|
+
import { LRUCache } from "lru-cache";
|
|
18
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
18
19
|
/* classification taxonomy */
|
|
19
20
|
export const projectClassification = {
|
|
20
21
|
boxing: ["white", "grey", "black"]
|
|
@@ -93,20 +94,33 @@ const parseScopeTerm = (value) => {
|
|
|
93
94
|
else if (value === "project")
|
|
94
95
|
return { kind: "project" };
|
|
95
96
|
const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
|
|
96
|
-
if (m !== null)
|
|
97
|
+
if (m !== null) {
|
|
98
|
+
if (m[2] === "." || m[2] === "..")
|
|
99
|
+
throw new Error(`invalid --scope term "${value}": id must not be "." or ".."`);
|
|
97
100
|
return { kind: m[1], id: m[2] };
|
|
101
|
+
}
|
|
98
102
|
throw new Error(`invalid --scope term "${value}" ` +
|
|
99
103
|
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
100
104
|
};
|
|
101
|
-
/* determine the Git top-level directory, if inside a Git repository
|
|
105
|
+
/* determine the Git top-level directory, if inside a Git repository;
|
|
106
|
+
cached per working directory ("" ≡ not inside a Git working tree), as
|
|
107
|
+
each determination spawns a Git subprocess and the Git context can
|
|
108
|
+
change over the lifetime of the long-running ASE service */
|
|
109
|
+
const gitToplevelCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
|
|
102
110
|
const gitToplevel = () => {
|
|
111
|
+
const cwd = process.cwd();
|
|
112
|
+
const cached = gitToplevelCache.get(cwd);
|
|
113
|
+
if (cached !== undefined)
|
|
114
|
+
return cached === "" ? null : cached;
|
|
115
|
+
let top = "";
|
|
103
116
|
try {
|
|
104
|
-
|
|
105
|
-
return result.stdout.trim() || null;
|
|
117
|
+
top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
|
|
106
118
|
}
|
|
107
119
|
catch {
|
|
108
|
-
|
|
120
|
+
/* not inside a Git working tree */
|
|
109
121
|
}
|
|
122
|
+
gitToplevelCache.set(cwd, top);
|
|
123
|
+
return top === "" ? null : top;
|
|
110
124
|
};
|
|
111
125
|
/* detect whether a project context exists, i.e. either we are inside
|
|
112
126
|
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
@@ -154,6 +168,11 @@ export const parseScope = (value) => {
|
|
|
154
168
|
terms.unshift({ kind: "default" });
|
|
155
169
|
return terms;
|
|
156
170
|
};
|
|
171
|
+
/* schema for a single artifact kind's "basedir"/"files" specification */
|
|
172
|
+
const artifactSchema = v.optional(v.strictObject({
|
|
173
|
+
basedir: v.optional(v.string()),
|
|
174
|
+
files: v.optional(v.string())
|
|
175
|
+
}));
|
|
157
176
|
/* schema for ".ase/config.yaml" */
|
|
158
177
|
export const configSchema = v.nullish(v.strictObject({
|
|
159
178
|
project: v.optional(v.strictObject({
|
|
@@ -161,12 +180,12 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
161
180
|
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
162
181
|
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
163
182
|
artifact: v.optional(v.strictObject({
|
|
164
|
-
spec:
|
|
165
|
-
arch:
|
|
166
|
-
code:
|
|
167
|
-
docs:
|
|
168
|
-
infr:
|
|
169
|
-
task:
|
|
183
|
+
spec: artifactSchema,
|
|
184
|
+
arch: artifactSchema,
|
|
185
|
+
code: artifactSchema,
|
|
186
|
+
docs: artifactSchema,
|
|
187
|
+
infr: artifactSchema,
|
|
188
|
+
task: artifactSchema
|
|
170
189
|
}))
|
|
171
190
|
})),
|
|
172
191
|
agent: v.optional(v.strictObject({
|
package/dst/ase-diagram.js
CHANGED
|
@@ -7,7 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import { InvalidArgumentError } from "commander";
|
|
8
8
|
import { renderMermaidASCII, renderMermaidSVG } from "beautiful-mermaid";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import
|
|
10
|
+
import { readStdin } from "./ase-stdio.js";
|
|
11
11
|
/* custom argument parser for Commander: non-negative integer */
|
|
12
12
|
const parseInteger = (name) => (value) => {
|
|
13
13
|
const n = Number.parseInt(value, 10);
|
|
@@ -150,6 +150,8 @@ export class Diagram {
|
|
|
150
150
|
clipping below are meaningful only for ASCII art) */
|
|
151
151
|
if (opts.format === "svg")
|
|
152
152
|
return renderMermaidSVG(src);
|
|
153
|
+
/* determine theme colors (gray shades once colors are enabled) */
|
|
154
|
+
const colored = opts.colorMode !== "none";
|
|
153
155
|
/* create diagram rendering */
|
|
154
156
|
let out = renderMermaidASCII(src, {
|
|
155
157
|
useAscii: opts.ascii,
|
|
@@ -157,20 +159,13 @@ export class Diagram {
|
|
|
157
159
|
paddingY: opts.nodeMarginY,
|
|
158
160
|
boxBorderPadding: opts.nodePadding,
|
|
159
161
|
colorMode: opts.colorMode,
|
|
160
|
-
theme:
|
|
162
|
+
theme: {
|
|
161
163
|
fg: "#000000",
|
|
162
|
-
border: "#a0a0a0",
|
|
163
|
-
junction: "#a0a0a0",
|
|
164
|
-
arrow: "#404040",
|
|
165
|
-
line: "#707070",
|
|
166
|
-
corner: "#707070"
|
|
167
|
-
} : {
|
|
168
|
-
fg: "#000000",
|
|
169
|
-
border: "#000000",
|
|
170
|
-
junction: "#000000",
|
|
171
|
-
arrow: "#000000",
|
|
172
|
-
line: "#000000",
|
|
173
|
-
corner: "#000000"
|
|
164
|
+
border: colored ? "#a0a0a0" : "#000000",
|
|
165
|
+
junction: colored ? "#a0a0a0" : "#000000",
|
|
166
|
+
arrow: colored ? "#404040" : "#000000",
|
|
167
|
+
line: colored ? "#707070" : "#000000",
|
|
168
|
+
corner: colored ? "#707070" : "#000000"
|
|
174
169
|
}
|
|
175
170
|
});
|
|
176
171
|
/* optionally clip diagram rendering */
|
|
@@ -248,7 +243,7 @@ export default class DiagramCommand {
|
|
|
248
243
|
}
|
|
249
244
|
}
|
|
250
245
|
else
|
|
251
|
-
src = await
|
|
246
|
+
src = await readStdin();
|
|
252
247
|
if (src.trim() === "") {
|
|
253
248
|
this.log.write("error", "diagram: empty Mermaid diagram specification");
|
|
254
249
|
process.exit(1);
|
package/dst/ase-getopt.js
CHANGED
|
@@ -6,6 +6,70 @@
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { Command, Option } from "commander";
|
|
8
8
|
import { parse as shParse, quote as shQuote } from "shell-quote";
|
|
9
|
+
/* tokenize a raw input string into [start,end) token ranges, preserving
|
|
10
|
+
the quoting so the original text can later be sliced verbatim */
|
|
11
|
+
const tokenizeRanges = (input) => {
|
|
12
|
+
const ranges = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < input.length) {
|
|
15
|
+
while (i < input.length && /\s/.test(input[i]))
|
|
16
|
+
i++;
|
|
17
|
+
if (i >= input.length)
|
|
18
|
+
break;
|
|
19
|
+
const start = i;
|
|
20
|
+
while (i < input.length && !/\s/.test(input[i])) {
|
|
21
|
+
const ch = input[i];
|
|
22
|
+
if (ch === "\"" || ch === "'") {
|
|
23
|
+
const quote = ch;
|
|
24
|
+
i++;
|
|
25
|
+
while (i < input.length && input[i] !== quote) {
|
|
26
|
+
if (input[i] === "\\" && i + 1 < input.length)
|
|
27
|
+
i++;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
if (i < input.length)
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
ranges.push({ start, end: i });
|
|
37
|
+
}
|
|
38
|
+
return ranges;
|
|
39
|
+
};
|
|
40
|
+
/* strip surrounding quotes/escapes from a raw token range so it can be
|
|
41
|
+
compared against an option flag spelling */
|
|
42
|
+
const unquote = (s) => {
|
|
43
|
+
let out = "";
|
|
44
|
+
let j = 0;
|
|
45
|
+
while (j < s.length) {
|
|
46
|
+
const ch = s[j];
|
|
47
|
+
if (ch === "\"" || ch === "'") {
|
|
48
|
+
const quote = ch;
|
|
49
|
+
j++;
|
|
50
|
+
while (j < s.length && s[j] !== quote) {
|
|
51
|
+
if (s[j] === "\\" && j + 1 < s.length) {
|
|
52
|
+
out += s[j + 1];
|
|
53
|
+
j += 2;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
out += s[j];
|
|
57
|
+
j++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
j++;
|
|
61
|
+
}
|
|
62
|
+
else if (ch === "\\" && j + 1 < s.length) {
|
|
63
|
+
out += s[j + 1];
|
|
64
|
+
j += 2;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out += ch;
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
9
73
|
/* MCP registration entry point for the option-parser tool */
|
|
10
74
|
export class GetoptMCP {
|
|
11
75
|
register(mcp) {
|
|
@@ -117,65 +181,7 @@ export class GetoptMCP {
|
|
|
117
181
|
let argsVerbatim = "";
|
|
118
182
|
if (argsRaw !== null) {
|
|
119
183
|
/* tokenize raw input into [start,end) ranges, preserving quotes */
|
|
120
|
-
const ranges =
|
|
121
|
-
let i = 0;
|
|
122
|
-
while (i < argsRaw.length) {
|
|
123
|
-
while (i < argsRaw.length && /\s/.test(argsRaw[i]))
|
|
124
|
-
i++;
|
|
125
|
-
if (i >= argsRaw.length)
|
|
126
|
-
break;
|
|
127
|
-
const start = i;
|
|
128
|
-
while (i < argsRaw.length && !/\s/.test(argsRaw[i])) {
|
|
129
|
-
const ch = argsRaw[i];
|
|
130
|
-
if (ch === "\"" || ch === "'") {
|
|
131
|
-
const quote = ch;
|
|
132
|
-
i++;
|
|
133
|
-
while (i < argsRaw.length && argsRaw[i] !== quote) {
|
|
134
|
-
if (argsRaw[i] === "\\" && i + 1 < argsRaw.length)
|
|
135
|
-
i++;
|
|
136
|
-
i++;
|
|
137
|
-
}
|
|
138
|
-
if (i < argsRaw.length)
|
|
139
|
-
i++;
|
|
140
|
-
}
|
|
141
|
-
else
|
|
142
|
-
i++;
|
|
143
|
-
}
|
|
144
|
-
ranges.push({ start, end: i });
|
|
145
|
-
}
|
|
146
|
-
/* helper function: strip surrounding quotes/escapes from a raw
|
|
147
|
-
range so it can be compared against an option flag spelling */
|
|
148
|
-
const unquote = (s) => {
|
|
149
|
-
let out = "";
|
|
150
|
-
let j = 0;
|
|
151
|
-
while (j < s.length) {
|
|
152
|
-
const ch = s[j];
|
|
153
|
-
if (ch === "\"" || ch === "'") {
|
|
154
|
-
const quote = ch;
|
|
155
|
-
j++;
|
|
156
|
-
while (j < s.length && s[j] !== quote) {
|
|
157
|
-
if (s[j] === "\\" && j + 1 < s.length) {
|
|
158
|
-
out += s[j + 1];
|
|
159
|
-
j += 2;
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
out += s[j];
|
|
163
|
-
j++;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
j++;
|
|
167
|
-
}
|
|
168
|
-
else if (ch === "\\" && j + 1 < s.length) {
|
|
169
|
-
out += s[j + 1];
|
|
170
|
-
j += 2;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
out += ch;
|
|
174
|
-
j++;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
178
|
-
};
|
|
184
|
+
const ranges = tokenizeRanges(argsRaw);
|
|
179
185
|
/* walk the raw ranges, consuming leading option tokens (and any
|
|
180
186
|
separate value tokens they take) until the first positional
|
|
181
187
|
is reached, then slice the original input from there -- this
|
|
@@ -202,7 +208,7 @@ export class GetoptMCP {
|
|
|
202
208
|
argsVerbatim = argsRaw.slice(ranges[idx].start);
|
|
203
209
|
}
|
|
204
210
|
else
|
|
205
|
-
argsVerbatim = cmd.args
|
|
211
|
+
argsVerbatim = shQuote(cmd.args);
|
|
206
212
|
/* build markdown info rendering of parsed options */
|
|
207
213
|
const opts = cmd.opts();
|
|
208
214
|
const info = Object.entries(opts)
|
package/dst/ase-hook.js
CHANGED
|
@@ -8,11 +8,10 @@ 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 getStdin from "get-stdin";
|
|
12
11
|
import * as v from "valibot";
|
|
13
12
|
import Version from "./ase-version.js";
|
|
14
13
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
15
|
-
import { writeStdout } from "./ase-
|
|
14
|
+
import { readStdin, writeStdout } from "./ase-stdio.js";
|
|
16
15
|
const addonMcpServers = [
|
|
17
16
|
"chat-alibaba-qwen",
|
|
18
17
|
"chat-deepseek",
|
|
@@ -86,14 +85,9 @@ export default class HookCommand {
|
|
|
86
85
|
isValidSessionId(id) {
|
|
87
86
|
return /^[A-Za-z0-9._-]+$/.test(id);
|
|
88
87
|
}
|
|
89
|
-
/* read the entire stdin payload asynchronously */
|
|
90
|
-
readStdin() {
|
|
91
|
-
/* best-effort: treat an unreadable/closed stdin as empty */
|
|
92
|
-
return getStdin().catch(() => "");
|
|
93
|
-
}
|
|
94
88
|
/* drain and discard the stdin event payload */
|
|
95
89
|
async drainStdin() {
|
|
96
|
-
await
|
|
90
|
+
await readStdin().catch(() => "");
|
|
97
91
|
}
|
|
98
92
|
/* best-effort JSON parse with valibot schema validation: returns
|
|
99
93
|
an empty object on blank input, malformed JSON, or schema
|
|
@@ -211,7 +205,7 @@ export default class HookCommand {
|
|
|
211
205
|
const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
|
|
212
206
|
/* read session information (Anthropic Claude Code CLI uses snake_case fields,
|
|
213
207
|
GitHub Copilot CLI uses camelCase fields) */
|
|
214
|
-
const stdin = await
|
|
208
|
+
const stdin = await readStdin().catch(() => "");
|
|
215
209
|
const input = this.parseJSON(stdin, v.object({
|
|
216
210
|
session_id: v.optional(v.string()),
|
|
217
211
|
sessionId: v.optional(v.string()),
|
|
@@ -273,7 +267,13 @@ export default class HookCommand {
|
|
|
273
267
|
`export ASE_SESSION_ID=${quote([sessionId])}\n` +
|
|
274
268
|
`export ASE_HEADLESS=${quote([headless])}\n` +
|
|
275
269
|
`export ASE_AGENT_TOOL=${quote([tool])}\n`;
|
|
276
|
-
|
|
270
|
+
try {
|
|
271
|
+
fs.appendFileSync(envFile, script, "utf8");
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
275
|
+
this.log.write("warning", `hook: failed to write environment file: ${message}`);
|
|
276
|
+
}
|
|
277
277
|
}
|
|
278
278
|
/* prepend ASE information to constitution markdown */
|
|
279
279
|
md =
|
|
@@ -371,7 +371,7 @@ export default class HookCommand {
|
|
|
371
371
|
}
|
|
372
372
|
/* read session id from stdin JSON payload */
|
|
373
373
|
async readSessionIdFromStdin() {
|
|
374
|
-
const stdin = await
|
|
374
|
+
const stdin = await readStdin().catch(() => "");
|
|
375
375
|
const input = this.parseJSON(stdin, v.object({
|
|
376
376
|
session_id: v.optional(v.string()),
|
|
377
377
|
sessionId: v.optional(v.string())
|
|
@@ -458,7 +458,7 @@ export default class HookCommand {
|
|
|
458
458
|
loosely-typed input object shared by the tool-approval handlers */
|
|
459
459
|
async readHookInput(tool) {
|
|
460
460
|
const spec = toolSpecs[tool];
|
|
461
|
-
const stdin = await
|
|
461
|
+
const stdin = await readStdin().catch(() => "");
|
|
462
462
|
const input = this.parseJSON(stdin, v.looseObject({
|
|
463
463
|
session_id: v.optional(v.string()),
|
|
464
464
|
sessionId: v.optional(v.string())
|
|
@@ -543,53 +543,22 @@ export default class HookCommand {
|
|
|
543
543
|
hookCmd.outputHelp();
|
|
544
544
|
process.exit(1);
|
|
545
545
|
});
|
|
546
|
-
/* register CLI sub-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
hookCmd
|
|
564
|
-
.command("pre-tool-use")
|
|
565
|
-
.description("handle tool PreToolUse hook event")
|
|
566
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
567
|
-
.action(async (opts) => {
|
|
568
|
-
process.exitCode = await this.doPreToolUse(this.parseTool(opts.tool));
|
|
569
|
-
});
|
|
570
|
-
/* register CLI sub-command "ase hook permission-request" */
|
|
571
|
-
hookCmd
|
|
572
|
-
.command("permission-request")
|
|
573
|
-
.description("handle tool PermissionRequest hook event (Codex CLI)")
|
|
574
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
575
|
-
.action(async (opts) => {
|
|
576
|
-
process.exitCode = await this.doPermissionRequest(this.parseTool(opts.tool));
|
|
577
|
-
});
|
|
578
|
-
/* register CLI sub-command "ase hook user-prompt-submit" */
|
|
579
|
-
hookCmd
|
|
580
|
-
.command("user-prompt-submit")
|
|
581
|
-
.description("handle UserPromptSubmit hook event (mark agent as busy)")
|
|
582
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
583
|
-
.action(async (opts) => {
|
|
584
|
-
process.exitCode = await this.doUserPromptSubmit(this.parseTool(opts.tool));
|
|
585
|
-
});
|
|
586
|
-
/* register CLI sub-command "ase hook stop" */
|
|
587
|
-
hookCmd
|
|
588
|
-
.command("stop")
|
|
589
|
-
.description("handle Stop hook event (mark agent as ready)")
|
|
590
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
591
|
-
.action(async (opts) => {
|
|
592
|
-
process.exitCode = await this.doStop(this.parseTool(opts.tool));
|
|
593
|
-
});
|
|
546
|
+
/* register CLI sub-commands "ase hook <event>" */
|
|
547
|
+
const subCmds = [
|
|
548
|
+
{ name: "session-start", desc: "handle SessionStart hook event", handler: (tool) => this.doSessionStart(tool) },
|
|
549
|
+
{ name: "session-end", desc: "handle SessionEnd hook event", handler: (tool) => this.doSessionEnd(tool) },
|
|
550
|
+
{ name: "pre-tool-use", desc: "handle tool PreToolUse hook event", handler: (tool) => this.doPreToolUse(tool) },
|
|
551
|
+
{ name: "permission-request", desc: "handle tool PermissionRequest hook event", handler: (tool) => this.doPermissionRequest(tool) },
|
|
552
|
+
{ name: "user-prompt-submit", desc: "handle UserPromptSubmit hook event", handler: (tool) => this.doUserPromptSubmit(tool) },
|
|
553
|
+
{ name: "stop", desc: "handle Stop hook event", handler: (tool) => this.doStop(tool) }
|
|
554
|
+
];
|
|
555
|
+
for (const { name, desc, handler } of subCmds)
|
|
556
|
+
hookCmd
|
|
557
|
+
.command(name)
|
|
558
|
+
.description(desc)
|
|
559
|
+
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
560
|
+
.action(async (opts) => {
|
|
561
|
+
process.exitCode = await handler(this.parseTool(opts.tool));
|
|
562
|
+
});
|
|
594
563
|
}
|
|
595
564
|
}
|
package/dst/ase-kv.js
CHANGED
|
@@ -30,10 +30,10 @@ export class KV {
|
|
|
30
30
|
KV.validateKey(key);
|
|
31
31
|
return KV.store.has(key);
|
|
32
32
|
}
|
|
33
|
-
/* get a value by key; returns undefined if no value is stored */
|
|
33
|
+
/* get a deep copy of a value by key; returns undefined if no value is stored */
|
|
34
34
|
static get(key) {
|
|
35
35
|
KV.validateKey(key);
|
|
36
|
-
return KV.store.get(key);
|
|
36
|
+
return structuredClone(KV.store.get(key));
|
|
37
37
|
}
|
|
38
38
|
/* set a value under the given key; overwrites any existing value */
|
|
39
39
|
static set(key, val) {
|
|
@@ -103,6 +103,17 @@ export class KV {
|
|
|
103
103
|
throw new Error(`invalid command "${String(c.command)}"`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
/* extract the message of a caught error */
|
|
107
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
108
|
+
/* execute a single key/value command and render it as an MCP tool result */
|
|
109
|
+
const mcpToolExec = (c) => {
|
|
110
|
+
try {
|
|
111
|
+
return { content: [{ type: "text", text: KV.execute(c) }] };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return { isError: true, content: [{ type: "text", text: `ERROR: ${errorMessage(err)}` }] };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
106
117
|
/* MCP registration entry point for in-memory key/value tools */
|
|
107
118
|
export class KVMCP {
|
|
108
119
|
register(mcp) {
|
|
@@ -115,16 +126,7 @@ export class KVMCP {
|
|
|
115
126
|
key: z.string()
|
|
116
127
|
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
|
|
117
128
|
}
|
|
118
|
-
}, async (args) => {
|
|
119
|
-
try {
|
|
120
|
-
const text = KV.execute({ command: "get", key: args.key });
|
|
121
|
-
return { content: [{ type: "text", text }] };
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
126
|
-
}
|
|
127
|
-
});
|
|
129
|
+
}, async (args) => mcpToolExec({ command: "get", key: args.key }));
|
|
128
130
|
/* key/value set */
|
|
129
131
|
mcp.registerTool("ase_kv_set", {
|
|
130
132
|
title: "ASE key/value set",
|
|
@@ -137,16 +139,7 @@ export class KVMCP {
|
|
|
137
139
|
val: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown())])
|
|
138
140
|
.describe("arbitrary JSON-compatible value to store under `key`")
|
|
139
141
|
}
|
|
140
|
-
}, async (args) => {
|
|
141
|
-
try {
|
|
142
|
-
const text = KV.execute({ command: "set", key: args.key, val: args.val });
|
|
143
|
-
return { content: [{ type: "text", text }] };
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
147
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
148
|
-
}
|
|
149
|
-
});
|
|
142
|
+
}, async (args) => mcpToolExec({ command: "set", key: args.key, val: args.val }));
|
|
150
143
|
/* key/value clear */
|
|
151
144
|
mcp.registerTool("ase_kv_clear", {
|
|
152
145
|
title: "ASE key/value clear",
|
|
@@ -158,16 +151,7 @@ export class KVMCP {
|
|
|
158
151
|
prefix: z.string().optional()
|
|
159
152
|
.describe("if given, only remove keys starting with this prefix (otherwise remove all keys)")
|
|
160
153
|
}
|
|
161
|
-
}, async (args) => {
|
|
162
|
-
try {
|
|
163
|
-
const text = KV.execute({ command: "clear", prefix: args.prefix });
|
|
164
|
-
return { content: [{ type: "text", text }] };
|
|
165
|
-
}
|
|
166
|
-
catch (err) {
|
|
167
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
168
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
169
|
-
}
|
|
170
|
-
});
|
|
154
|
+
}, async (args) => mcpToolExec({ command: "clear", prefix: args.prefix }));
|
|
171
155
|
/* key/value delete */
|
|
172
156
|
mcp.registerTool("ase_kv_delete", {
|
|
173
157
|
title: "ASE key/value delete",
|
|
@@ -177,16 +161,7 @@ export class KVMCP {
|
|
|
177
161
|
key: z.string()
|
|
178
162
|
.describe("key identifier (non-empty, no whitespace-only, no control characters, up to 1024 characters)")
|
|
179
163
|
}
|
|
180
|
-
}, async (args) => {
|
|
181
|
-
try {
|
|
182
|
-
const text = KV.execute({ command: "delete", key: args.key });
|
|
183
|
-
return { content: [{ type: "text", text }] };
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
187
|
-
return { isError: true, content: [{ type: "text", text: `ERROR: ${message}` }] };
|
|
188
|
-
}
|
|
189
|
-
});
|
|
164
|
+
}, async (args) => mcpToolExec({ command: "delete", key: args.key }));
|
|
190
165
|
/* key/value batch */
|
|
191
166
|
mcp.registerTool("ase_kv_batch", {
|
|
192
167
|
title: "ASE key/value batch",
|
|
@@ -226,7 +201,7 @@ export class KVMCP {
|
|
|
226
201
|
results.push(KV.execute(c));
|
|
227
202
|
}
|
|
228
203
|
catch (err) {
|
|
229
|
-
const message =
|
|
204
|
+
const message = errorMessage(err);
|
|
230
205
|
if (tx) {
|
|
231
206
|
if (snapshot !== null)
|
|
232
207
|
KV.restore(snapshot);
|
package/dst/ase-mcp.js
CHANGED
|
@@ -23,7 +23,7 @@ export default class MCPCommand {
|
|
|
23
23
|
let ctx = this.loadContext();
|
|
24
24
|
/* fast path: already running */
|
|
25
25
|
if (ctx.port !== null) {
|
|
26
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
26
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
27
27
|
if (match === true)
|
|
28
28
|
return { projectId: ctx.projectId, port: ctx.port };
|
|
29
29
|
}
|
package/dst/ase-meta.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { writeStdout } from "./ase-
|
|
9
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
10
10
|
/* reusable functionality: resolve and read plugin "meta/" files */
|
|
11
11
|
export class Meta {
|
|
12
12
|
/* determine the plugin "meta/" directory; the build process copies
|
package/dst/ase-service.js
CHANGED
|
@@ -494,7 +494,7 @@ export default class ServiceCommand {
|
|
|
494
494
|
process.stdout.write("service: not running (no port configured)\n");
|
|
495
495
|
return 1;
|
|
496
496
|
}
|
|
497
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
497
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
498
498
|
if (match === true) {
|
|
499
499
|
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.commandRequest("status", 2000));
|
|
500
500
|
const d = r._data;
|
|
@@ -513,13 +513,13 @@ export default class ServiceCommand {
|
|
|
513
513
|
/* send command: POST /command with the arbitrary cmd token */
|
|
514
514
|
async doSend(cmd) {
|
|
515
515
|
let ctx = this.loadContext();
|
|
516
|
-
if (ctx.port === null || await probe(ctx.port, ctx.projectId) !== true) {
|
|
516
|
+
if (ctx.port === null || await probe(ctx.port, ctx.projectId).catch(() => null) !== true) {
|
|
517
517
|
/* auto-start the service once, then re-check */
|
|
518
518
|
await this.doStart();
|
|
519
519
|
ctx = this.loadContext();
|
|
520
520
|
if (ctx.port === null)
|
|
521
521
|
throw new Error("service not running (no port configured after auto-start)");
|
|
522
|
-
if (await probe(ctx.port, ctx.projectId) !== true)
|
|
522
|
+
if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
|
|
523
523
|
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
524
524
|
}
|
|
525
525
|
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.commandRequest(cmd, 5000), responseType: "text" });
|
|
@@ -536,7 +536,7 @@ export default class ServiceCommand {
|
|
|
536
536
|
this.log.write("info", "service: not running (no port configured)");
|
|
537
537
|
return 0;
|
|
538
538
|
}
|
|
539
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
539
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
540
540
|
if (match === false) {
|
|
541
541
|
this.log.write("info", `service: not running (port ${ctx.port} in use by foreign service)`);
|
|
542
542
|
return 1;
|
package/dst/ase-setup.js
CHANGED
|
@@ -356,29 +356,13 @@ export default class SetupCommand {
|
|
|
356
356
|
args.push(name, transport.url);
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
-
else if (tool === "copilot") {
|
|
360
|
-
/* GitHub Copilot CLI implies the stdio transport when the
|
|
361
|
-
command is provided after "--"; only "http"/"sse" servers
|
|
362
|
-
need an explicit "--transport" flag and take the URL as a
|
|
363
|
-
positional argument */
|
|
364
|
-
if (transport.type === "stdio") {
|
|
365
|
-
args.push(name);
|
|
366
|
-
for (const [key, val] of Object.entries(env))
|
|
367
|
-
args.push("--env", `${key}=${val}`);
|
|
368
|
-
args.push("--", ...transport.command);
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
args.push("--transport", "http");
|
|
372
|
-
for (const [key, val] of Object.entries(transport.headers ?? {}))
|
|
373
|
-
args.push("--header", `${key}: ${val}`);
|
|
374
|
-
args.push(name, transport.url);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
359
|
else {
|
|
378
|
-
/*
|
|
379
|
-
positional argument and
|
|
380
|
-
command is provided after "--"; "http"
|
|
381
|
-
|
|
360
|
+
/* the GitHub Copilot CLI and OpenAI Codex CLI both take the
|
|
361
|
+
server name as a positional argument and imply the stdio
|
|
362
|
+
transport when the command is provided after "--"; for "http"
|
|
363
|
+
servers the GitHub Copilot CLI needs an explicit
|
|
364
|
+
"--transport" flag and takes the URL positionally, while the
|
|
365
|
+
OpenAI Codex CLI takes it via the "--url" option */
|
|
382
366
|
if (transport.type === "stdio") {
|
|
383
367
|
args.push(name);
|
|
384
368
|
for (const [key, val] of Object.entries(env))
|
|
@@ -386,9 +370,14 @@ export default class SetupCommand {
|
|
|
386
370
|
args.push("--", ...transport.command);
|
|
387
371
|
}
|
|
388
372
|
else {
|
|
373
|
+
if (tool === "copilot")
|
|
374
|
+
args.push("--transport", "http");
|
|
389
375
|
for (const [key, val] of Object.entries(transport.headers ?? {}))
|
|
390
376
|
args.push("--header", `${key}: ${val}`);
|
|
391
|
-
|
|
377
|
+
if (tool === "copilot")
|
|
378
|
+
args.push(name, transport.url);
|
|
379
|
+
else
|
|
380
|
+
args.push(name, "--url", transport.url);
|
|
392
381
|
}
|
|
393
382
|
}
|
|
394
383
|
await this.run(toolSpecs[tool].cli, args);
|
|
@@ -657,24 +646,18 @@ export default class SetupCommand {
|
|
|
657
646
|
AST, reproducing the exact whitespace tokens json-asty emits for a
|
|
658
647
|
canonically 4-space-indented settings.json */
|
|
659
648
|
statuslineBuildMember(root, command) {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
const m = root.create("member");
|
|
664
|
-
if (epilog !== undefined)
|
|
665
|
-
m.set({ epilog });
|
|
666
|
-
return m.add(k, v);
|
|
667
|
-
};
|
|
668
|
-
const numMember = (key, val, epilog) => {
|
|
649
|
+
const member = (key, val, epilog) => {
|
|
650
|
+
const type = typeof val === "number" ? "number" : "string";
|
|
651
|
+
const body = typeof val === "number" ? String(val) : JSON.stringify(val);
|
|
669
652
|
const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
|
|
670
|
-
const v = root.create(
|
|
653
|
+
const v = root.create(type).set({ body, value: val });
|
|
671
654
|
const m = root.create("member");
|
|
672
655
|
if (epilog !== undefined)
|
|
673
656
|
m.set({ epilog });
|
|
674
657
|
return m.add(k, v);
|
|
675
658
|
};
|
|
676
659
|
const obj = root.create("object").set({ prolog: "{\n ", epilog: "\n }\n" });
|
|
677
|
-
obj.add(
|
|
660
|
+
obj.add(member("type", "command", ",\n "), member("command", command, ",\n "), member("padding", 0));
|
|
678
661
|
const key = root.create("string").set({
|
|
679
662
|
body: JSON.stringify("statusLine"),
|
|
680
663
|
value: "statusLine",
|
package/dst/ase-skills.js
CHANGED
|
@@ -54,7 +54,7 @@ export class Skills {
|
|
|
54
54
|
}
|
|
55
55
|
/* fetch GitHub stars given a repository URL (or empty string) */
|
|
56
56
|
static async fetchStars(repository) {
|
|
57
|
-
const m =
|
|
57
|
+
const m = /^(?:.*?:\/\/)?(?:[^@/]*@)?github\.com[:/]([^/]+\/[^/#?]+?)(?:\.git)?(?:[/#?].*)?$/.exec(repository);
|
|
58
58
|
if (m === null)
|
|
59
59
|
return "N.A.";
|
|
60
60
|
try {
|
package/dst/ase-statusline.js
CHANGED
|
@@ -11,7 +11,7 @@ import { InvalidArgumentError } from "commander";
|
|
|
11
11
|
import { execaSync } from "execa";
|
|
12
12
|
import { Chalk } from "chalk";
|
|
13
13
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
14
|
-
import { writeStdout } from "./ase-
|
|
14
|
+
import { readStdin, writeStdout } from "./ase-stdio.js";
|
|
15
15
|
import pkg from "../package.json" with { type: "json" };
|
|
16
16
|
/* forced-color chalk instance: stdout is a pipe under Anthropic Claude Code CLI,
|
|
17
17
|
so chalk auto-detection would yield level 0; force level 1 to keep
|
|
@@ -28,13 +28,6 @@ const parseInteger = (name) => (value) => {
|
|
|
28
28
|
throw new InvalidArgumentError(`${name} must be a non-negative integer`);
|
|
29
29
|
return n;
|
|
30
30
|
};
|
|
31
|
-
/* read stdin into a single string */
|
|
32
|
-
const readStdin = async () => {
|
|
33
|
-
const chunks = [];
|
|
34
|
-
for await (const chunk of process.stdin)
|
|
35
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
36
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
37
|
-
};
|
|
38
31
|
/* detect terminal column width via /dev/tty (stdout is a pipe under Anthropic Claude Code CLI) */
|
|
39
32
|
const detectTermWidth = () => {
|
|
40
33
|
let width = 0;
|
|
@@ -218,8 +211,7 @@ export default class StatuslineCommand {
|
|
|
218
211
|
}
|
|
219
212
|
catch (err) {
|
|
220
213
|
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
-
|
|
222
|
-
process.exit(1);
|
|
214
|
+
throw new Error(`statusline: invalid JSON on stdin: ${message}`);
|
|
223
215
|
}
|
|
224
216
|
/* normalize Copilot CLI's top-level "cwd" into the
|
|
225
217
|
"workspace.current_dir" structure shared with Anthropic Claude Code CLI */
|
|
@@ -340,10 +332,18 @@ export default class StatuslineCommand {
|
|
|
340
332
|
emit(`${prefix("◔", "context")}${bar} ${pct}%`);
|
|
341
333
|
},
|
|
342
334
|
C: () => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
/* probe the live-context fields first and fall back to the
|
|
336
|
+
cumulative ones, so each harness contributes its own view:
|
|
337
|
+
GitHub Copilot CLI reports the live window separately (its
|
|
338
|
+
"total_*" are session sums), whereas Anthropic Claude Code CLI
|
|
339
|
+
folds it into "total_input_tokens". Output tokens stay out --
|
|
340
|
+
"used_percentage" is input-side only. */
|
|
341
|
+
const cw = data.context_window;
|
|
342
|
+
const pct = cw?.used_percentage ?? 0;
|
|
343
|
+
const tokensCur = cw?.current_context_tokens ??
|
|
344
|
+
cw?.last_call_input_tokens ?? cw?.total_input_tokens ?? 0;
|
|
345
|
+
const tokensLim = cw?.displayed_context_limit ?? cw?.context_window_size ??
|
|
346
|
+
(pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0);
|
|
347
347
|
if (tokensLim > 0)
|
|
348
348
|
emit(`${prefix("◆", "tokens")}${c.bold(formatTokens(tokensCur) + "/" + formatTokens(tokensLim))}`);
|
|
349
349
|
},
|
package/dst/ase-stdio.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import getStdin from "get-stdin";
|
|
7
|
+
/* read the entire stdin payload as a UTF-8 string, treating an
|
|
8
|
+
interactive terminal as empty input so callers never block on a
|
|
9
|
+
user which is not going to type anything */
|
|
10
|
+
export const readStdin = () => {
|
|
11
|
+
return getStdin();
|
|
12
|
+
};
|
|
13
|
+
/* write a string to stdout, awaiting the write callback which only
|
|
14
|
+
fires once the data has been flushed to the underlying pipe, so a
|
|
15
|
+
subsequent "process.exit" cannot truncate the output */
|
|
16
|
+
export const writeStdout = (text) => {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
process.stdout.write(text, (err) => {
|
|
19
|
+
if (err)
|
|
20
|
+
reject(err);
|
|
21
|
+
else
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
};
|
package/dst/ase-task.js
CHANGED
|
@@ -13,7 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
import { LRUCache } from "lru-cache";
|
|
14
14
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
15
15
|
import { Markdown } from "./ase-markdown.js";
|
|
16
|
-
import { writeStdout } from "./ase-
|
|
16
|
+
import { readStdin, writeStdout } from "./ase-stdio.js";
|
|
17
17
|
/* reusable functionality: persisted task plans under
|
|
18
18
|
<project>/<basedir>/TASK-<id>.md (driven by the
|
|
19
19
|
"project.artifact.task.{basedir,files}" configuration) */
|
|
@@ -56,11 +56,18 @@ export class Task {
|
|
|
56
56
|
Task.projectRootCache.set(cwd, root);
|
|
57
57
|
return root;
|
|
58
58
|
}
|
|
59
|
+
/* cached task storage specification (TTL-bounded, mirroring the project
|
|
60
|
+
root cache, as each read parses the whole layered YAML config chain) */
|
|
61
|
+
static specCache = new LRUCache({ max: 4, ttl: 2 * 1000 });
|
|
59
62
|
/* read the configured "basedir" anchor and "files" miniglob spec for
|
|
60
63
|
task storage; "basedir" is project-root-relative (POSIX, defaults
|
|
61
64
|
to ".ase/task") and "files" constrains the task filenames
|
|
62
65
|
(defaults to "*.md") */
|
|
63
66
|
static spec(log) {
|
|
67
|
+
const root = Task.projectRoot();
|
|
68
|
+
const cached = Task.specCache.get(root);
|
|
69
|
+
if (cached !== undefined)
|
|
70
|
+
return cached;
|
|
64
71
|
const cfg = new Config("config", configSchema, log);
|
|
65
72
|
cfg.read();
|
|
66
73
|
const read = (key) => {
|
|
@@ -74,7 +81,9 @@ export class Task {
|
|
|
74
81
|
if (basedir.split("/").includes(".."))
|
|
75
82
|
throw new Error(`task: configured "basedir" "${basedir}" must not escape the project root`);
|
|
76
83
|
const files = read("project.artifact.task.files") || "*.md";
|
|
77
|
-
|
|
84
|
+
const result = { basedir, files };
|
|
85
|
+
Task.specCache.set(root, result);
|
|
86
|
+
return result;
|
|
78
87
|
}
|
|
79
88
|
/* resolve the on-disk base directory for task storage */
|
|
80
89
|
static baseDir(log) {
|
|
@@ -245,15 +254,6 @@ export class Task {
|
|
|
245
254
|
});
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
|
-
/* read all of stdin as a UTF-8 string */
|
|
249
|
-
const readStdin = () => {
|
|
250
|
-
return new Promise((resolve, reject) => {
|
|
251
|
-
const chunks = [];
|
|
252
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
253
|
-
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
254
|
-
process.stdin.on("error", (err) => reject(err));
|
|
255
|
-
});
|
|
256
|
-
};
|
|
257
257
|
/* CLI command "ase task" */
|
|
258
258
|
export default class TaskCommand {
|
|
259
259
|
log;
|
package/dst/ase.js
CHANGED
|
@@ -60,7 +60,7 @@ const main = async () => {
|
|
|
60
60
|
await program.parseAsync(process.argv);
|
|
61
61
|
/* gracefully terminate */
|
|
62
62
|
await log.close();
|
|
63
|
-
process.exit(0);
|
|
63
|
+
process.exit(process.exitCode ?? 0);
|
|
64
64
|
};
|
|
65
65
|
main().catch(async (err) => {
|
|
66
66
|
if (err instanceof CommanderError) {
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "https://ase.tools",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "https://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.9.
|
|
9
|
+
"version": "0.9.48",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
package/plugin/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "https://ase.tools",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "https://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.9.
|
|
9
|
+
"version": "0.9.48",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -102,16 +102,11 @@ catalog you match <intent/> against:
|
|
|
102
102
|
<template>
|
|
103
103
|
<ase-tpl-head title="SKILL COMMAND PROPOSAL"/>
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
○ <intent/>
|
|
107
|
-
|
|
108
|
-
● **COMMAND**:
|
|
109
|
-
⌘ `<command/>`
|
|
110
|
-
|
|
111
|
-
● **RATIONALE**:
|
|
112
|
-
○ <rationale/>
|
|
105
|
+
❯ `<command/>`
|
|
113
106
|
|
|
114
107
|
<ase-tpl-foot title="SKILL COMMAND PROPOSAL"/>
|
|
108
|
+
|
|
109
|
+
**RATIONALE**: <rationale/>
|
|
115
110
|
</template>
|
|
116
111
|
|
|
117
112
|
4. *Dispatch Command*:
|