@rse/ase 0.9.46 → 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 +3 -2
- package/dst/ase-config.js +36 -16
- package/dst/ase-diagram.js +10 -15
- package/dst/ase-getopt.js +66 -60
- package/dst/ase-hook.js +51 -77
- package/dst/ase-kv.js +49 -79
- package/dst/ase-log.js +13 -5
- package/dst/ase-markdown.js +12 -2
- package/dst/ase-mcp.js +21 -4
- package/dst/ase-meta.js +1 -1
- package/dst/ase-persona.js +1 -3
- package/dst/ase-service.js +17 -27
- package/dst/ase-setup.js +29 -73
- package/dst/ase-skills.js +1 -1
- package/dst/ase-statusline.js +28 -42
- package/dst/ase-stdio.js +25 -0
- package/dst/ase-task.js +25 -34
- package/dst/ase.js +7 -3
- 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/etc/stx.conf +2 -2
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/package.json +1 -1
- package/plugin/skills/{ase-meta-intent → ase-help-intent}/SKILL.md +7 -12
- package/plugin/skills/{ase-meta-intent → ase-help-intent}/help.md +8 -7
- package/plugin/skills/ase-help-skill/SKILL.md +203 -0
- package/plugin/skills/ase-help-skill/catalog.md +60 -0
- package/plugin/skills/ase-help-skill/help.md +83 -0
- package/plugin/skills/ase-task-condense/help.md +3 -3
- package/plugin/skills/ase-task-reboot/help.md +3 -3
- package/plugin/skills/ase-task-view/help.md +3 -3
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,6 +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-stdio.js";
|
|
6
7
|
/* the canonical expected values for every ase-meta-compat probe,
|
|
7
8
|
keyed by "<category>/<probe-name>" as used in the skill */
|
|
8
9
|
const EXPECTED = {
|
|
@@ -44,8 +45,8 @@ export default class CompatCommand {
|
|
|
44
45
|
program
|
|
45
46
|
.command("compat")
|
|
46
47
|
.description("Output expected probe values for the ase-meta-compat self-test skill")
|
|
47
|
-
.action(() => {
|
|
48
|
-
|
|
48
|
+
.action(async () => {
|
|
49
|
+
await writeStdout(formatExpected());
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
}
|
package/dst/ase-config.js
CHANGED
|
@@ -14,6 +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 { LRUCache } from "lru-cache";
|
|
18
|
+
import { writeStdout } from "./ase-stdio.js";
|
|
17
19
|
/* classification taxonomy */
|
|
18
20
|
export const projectClassification = {
|
|
19
21
|
boxing: ["white", "grey", "black"]
|
|
@@ -92,20 +94,33 @@ const parseScopeTerm = (value) => {
|
|
|
92
94
|
else if (value === "project")
|
|
93
95
|
return { kind: "project" };
|
|
94
96
|
const m = /^(session|task):([A-Za-z0-9._-]+)$/.exec(value);
|
|
95
|
-
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 ".."`);
|
|
96
100
|
return { kind: m[1], id: m[2] };
|
|
101
|
+
}
|
|
97
102
|
throw new Error(`invalid --scope term "${value}" ` +
|
|
98
103
|
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
99
104
|
};
|
|
100
|
-
/* 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 });
|
|
101
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 = "";
|
|
102
116
|
try {
|
|
103
|
-
|
|
104
|
-
return result.stdout.trim() || null;
|
|
117
|
+
top = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" }).stdout.trim();
|
|
105
118
|
}
|
|
106
119
|
catch {
|
|
107
|
-
|
|
120
|
+
/* not inside a Git working tree */
|
|
108
121
|
}
|
|
122
|
+
gitToplevelCache.set(cwd, top);
|
|
123
|
+
return top === "" ? null : top;
|
|
109
124
|
};
|
|
110
125
|
/* detect whether a project context exists, i.e. either we are inside
|
|
111
126
|
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
@@ -153,6 +168,11 @@ export const parseScope = (value) => {
|
|
|
153
168
|
terms.unshift({ kind: "default" });
|
|
154
169
|
return terms;
|
|
155
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
|
+
}));
|
|
156
176
|
/* schema for ".ase/config.yaml" */
|
|
157
177
|
export const configSchema = v.nullish(v.strictObject({
|
|
158
178
|
project: v.optional(v.strictObject({
|
|
@@ -160,12 +180,12 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
160
180
|
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
161
181
|
boxing: v.optional(v.picklist(projectClassification.boxing)),
|
|
162
182
|
artifact: v.optional(v.strictObject({
|
|
163
|
-
spec:
|
|
164
|
-
arch:
|
|
165
|
-
code:
|
|
166
|
-
docs:
|
|
167
|
-
infr:
|
|
168
|
-
task:
|
|
183
|
+
spec: artifactSchema,
|
|
184
|
+
arch: artifactSchema,
|
|
185
|
+
code: artifactSchema,
|
|
186
|
+
docs: artifactSchema,
|
|
187
|
+
infr: artifactSchema,
|
|
188
|
+
task: artifactSchema
|
|
169
189
|
}))
|
|
170
190
|
})),
|
|
171
191
|
agent: v.optional(v.strictObject({
|
|
@@ -188,7 +208,7 @@ export class Config {
|
|
|
188
208
|
docs;
|
|
189
209
|
target;
|
|
190
210
|
/* creation */
|
|
191
|
-
constructor(name, schema, log, scope = [{ kind: "project" }]) {
|
|
211
|
+
constructor(name, schema, log, scope = [{ kind: "user" }, { kind: "project" }]) {
|
|
192
212
|
if (scope.length === 0)
|
|
193
213
|
throw new Error("invalid scope: chain must not be empty");
|
|
194
214
|
this.name = name;
|
|
@@ -560,7 +580,7 @@ export default class ConfigCommand {
|
|
|
560
580
|
configCmd
|
|
561
581
|
.command("list")
|
|
562
582
|
.description("list all configured values as flat dotted keys")
|
|
563
|
-
.action((_opts, cmd) => {
|
|
583
|
+
.action(async (_opts, cmd) => {
|
|
564
584
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
565
585
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
566
586
|
cfg.read();
|
|
@@ -573,7 +593,7 @@ export default class ConfigCommand {
|
|
|
573
593
|
const val = isScalar(e.value) ? e.value.value : e.value;
|
|
574
594
|
table.push([e.key, String(val), Config.scopeLabel(e.scope)]);
|
|
575
595
|
}
|
|
576
|
-
|
|
596
|
+
await writeStdout(`${table.toString()}\n`);
|
|
577
597
|
});
|
|
578
598
|
/* register CLI sub-command "ase config edit" */
|
|
579
599
|
configCmd
|
|
@@ -612,7 +632,7 @@ export default class ConfigCommand {
|
|
|
612
632
|
.command("get")
|
|
613
633
|
.description("print the value at a dotted configuration key")
|
|
614
634
|
.argument("<key>", "configuration key (dotted path)")
|
|
615
|
-
.action((key, _opts, cmd) => {
|
|
635
|
+
.action(async (key, _opts, cmd) => {
|
|
616
636
|
const scope = parseScope(cmd.optsWithGlobals().scope);
|
|
617
637
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
618
638
|
cfg.read();
|
|
@@ -621,7 +641,7 @@ export default class ConfigCommand {
|
|
|
621
641
|
throw new Error(`key "${key}" is not set`);
|
|
622
642
|
if (isMap(val))
|
|
623
643
|
throw new Error(`key "${key}" is not a leaf key`);
|
|
624
|
-
|
|
644
|
+
await writeStdout(`${isScalar(val) ? val.value : val}\n`);
|
|
625
645
|
});
|
|
626
646
|
/* register CLI sub-command "ase config set" */
|
|
627
647
|
configCmd
|
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",
|
|
@@ -69,6 +68,13 @@ const toolSpecs = {
|
|
|
69
68
|
approvalEvent: "PermissionRequest"
|
|
70
69
|
}
|
|
71
70
|
};
|
|
71
|
+
/* schema (and derived type) of the tool invocation input fields
|
|
72
|
+
inspected by the tool-approval decision logic */
|
|
73
|
+
const toolInputSchema = v.object({
|
|
74
|
+
command: v.optional(v.string()),
|
|
75
|
+
skill: v.optional(v.string()),
|
|
76
|
+
file_path: v.optional(v.string())
|
|
77
|
+
});
|
|
72
78
|
/* CLI command "ase hook" */
|
|
73
79
|
export default class HookCommand {
|
|
74
80
|
log;
|
|
@@ -79,14 +85,9 @@ export default class HookCommand {
|
|
|
79
85
|
isValidSessionId(id) {
|
|
80
86
|
return /^[A-Za-z0-9._-]+$/.test(id);
|
|
81
87
|
}
|
|
82
|
-
/* read the entire stdin payload asynchronously */
|
|
83
|
-
readStdin() {
|
|
84
|
-
/* best-effort: treat an unreadable/closed stdin as empty */
|
|
85
|
-
return getStdin().catch(() => "");
|
|
86
|
-
}
|
|
87
88
|
/* drain and discard the stdin event payload */
|
|
88
89
|
async drainStdin() {
|
|
89
|
-
await
|
|
90
|
+
await readStdin().catch(() => "");
|
|
90
91
|
}
|
|
91
92
|
/* best-effort JSON parse with valibot schema validation: returns
|
|
92
93
|
an empty object on blank input, malformed JSON, or schema
|
|
@@ -204,7 +205,7 @@ export default class HookCommand {
|
|
|
204
205
|
const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
|
|
205
206
|
/* read session information (Anthropic Claude Code CLI uses snake_case fields,
|
|
206
207
|
GitHub Copilot CLI uses camelCase fields) */
|
|
207
|
-
const stdin = await
|
|
208
|
+
const stdin = await readStdin().catch(() => "");
|
|
208
209
|
const input = this.parseJSON(stdin, v.object({
|
|
209
210
|
session_id: v.optional(v.string()),
|
|
210
211
|
sessionId: v.optional(v.string()),
|
|
@@ -242,16 +243,15 @@ export default class HookCommand {
|
|
|
242
243
|
const projectId = path.basename(projectDir);
|
|
243
244
|
/* determine user id */
|
|
244
245
|
const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
|
|
245
|
-
/* determine
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
boxing = valBoxing;
|
|
246
|
+
/* helper function: determine a setting from the configuration,
|
|
247
|
+
falling back to an environment variable and a default */
|
|
248
|
+
const setting = (key, envVar, dflt) => {
|
|
249
|
+
const val = cfg.get(key);
|
|
250
|
+
return typeof val === "string" ? val : (process.env[envVar] ?? dflt);
|
|
251
|
+
};
|
|
252
|
+
/* determine agent persona style and project boxing transparency */
|
|
253
|
+
const persona = setting("agent.persona", "ASE_PERSONA_STYLE", "engineer");
|
|
254
|
+
const boxing = setting("project.boxing", "ASE_PROJECT_BOXING", "white");
|
|
255
255
|
/* determine headless mode */
|
|
256
256
|
const headless = (process.env.ASE_HEADLESS ?? "false") === "true" ? "true" : "false";
|
|
257
257
|
/* provide ASE information to Anthropic Claude Code CLI shell commands
|
|
@@ -267,7 +267,13 @@ export default class HookCommand {
|
|
|
267
267
|
`export ASE_SESSION_ID=${quote([sessionId])}\n` +
|
|
268
268
|
`export ASE_HEADLESS=${quote([headless])}\n` +
|
|
269
269
|
`export ASE_AGENT_TOOL=${quote([tool])}\n`;
|
|
270
|
-
|
|
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
|
+
}
|
|
271
277
|
}
|
|
272
278
|
/* prepend ASE information to constitution markdown */
|
|
273
279
|
md =
|
|
@@ -365,7 +371,7 @@ export default class HookCommand {
|
|
|
365
371
|
}
|
|
366
372
|
/* read session id from stdin JSON payload */
|
|
367
373
|
async readSessionIdFromStdin() {
|
|
368
|
-
const stdin = await
|
|
374
|
+
const stdin = await readStdin().catch(() => "");
|
|
369
375
|
const input = this.parseJSON(stdin, v.object({
|
|
370
376
|
session_id: v.optional(v.string()),
|
|
371
377
|
sessionId: v.optional(v.string())
|
|
@@ -422,13 +428,12 @@ export default class HookCommand {
|
|
|
422
428
|
let toolInput = {};
|
|
423
429
|
const rawInput = input[spec.toolInputField];
|
|
424
430
|
if (spec.toolInputIsString && typeof rawInput === "string")
|
|
425
|
-
toolInput = this.parseJSON(rawInput,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
toolInput = rawInput;
|
|
431
|
+
toolInput = this.parseJSON(rawInput, toolInputSchema);
|
|
432
|
+
else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null) {
|
|
433
|
+
const result = v.safeParse(toolInputSchema, rawInput);
|
|
434
|
+
if (result.success)
|
|
435
|
+
toolInput = result.output;
|
|
436
|
+
}
|
|
432
437
|
const command = toolInput.command ?? "";
|
|
433
438
|
if (toolName === spec.bashToolName && /^ase(\s|$)/.test(command)
|
|
434
439
|
&& !/[;&|<>`\n]|\$\(/.test(command))
|
|
@@ -453,7 +458,7 @@ export default class HookCommand {
|
|
|
453
458
|
loosely-typed input object shared by the tool-approval handlers */
|
|
454
459
|
async readHookInput(tool) {
|
|
455
460
|
const spec = toolSpecs[tool];
|
|
456
|
-
const stdin = await
|
|
461
|
+
const stdin = await readStdin().catch(() => "");
|
|
457
462
|
const input = this.parseJSON(stdin, v.looseObject({
|
|
458
463
|
session_id: v.optional(v.string()),
|
|
459
464
|
sessionId: v.optional(v.string())
|
|
@@ -538,53 +543,22 @@ export default class HookCommand {
|
|
|
538
543
|
hookCmd.outputHelp();
|
|
539
544
|
process.exit(1);
|
|
540
545
|
});
|
|
541
|
-
/* register CLI sub-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
hookCmd
|
|
559
|
-
.command("pre-tool-use")
|
|
560
|
-
.description("handle tool PreToolUse hook event")
|
|
561
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
562
|
-
.action(async (opts) => {
|
|
563
|
-
process.exitCode = await this.doPreToolUse(this.parseTool(opts.tool));
|
|
564
|
-
});
|
|
565
|
-
/* register CLI sub-command "ase hook permission-request" */
|
|
566
|
-
hookCmd
|
|
567
|
-
.command("permission-request")
|
|
568
|
-
.description("handle tool PermissionRequest hook event (Codex CLI)")
|
|
569
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
570
|
-
.action(async (opts) => {
|
|
571
|
-
process.exitCode = await this.doPermissionRequest(this.parseTool(opts.tool));
|
|
572
|
-
});
|
|
573
|
-
/* register CLI sub-command "ase hook user-prompt-submit" */
|
|
574
|
-
hookCmd
|
|
575
|
-
.command("user-prompt-submit")
|
|
576
|
-
.description("handle UserPromptSubmit hook event (mark agent as busy)")
|
|
577
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
578
|
-
.action(async (opts) => {
|
|
579
|
-
process.exitCode = await this.doUserPromptSubmit(this.parseTool(opts.tool));
|
|
580
|
-
});
|
|
581
|
-
/* register CLI sub-command "ase hook stop" */
|
|
582
|
-
hookCmd
|
|
583
|
-
.command("stop")
|
|
584
|
-
.description("handle Stop hook event (mark agent as ready)")
|
|
585
|
-
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
586
|
-
.action(async (opts) => {
|
|
587
|
-
process.exitCode = await this.doStop(this.parseTool(opts.tool));
|
|
588
|
-
});
|
|
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
|
+
});
|
|
589
563
|
}
|
|
590
564
|
}
|