@rse/ase 0.9.42 → 0.9.44
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 +28 -24
- package/dst/ase-config.js +27 -36
- package/dst/ase-diagram.js +15 -35
- package/dst/ase-getopt.js +10 -15
- package/dst/ase-hook.js +29 -27
- package/dst/ase-kv.js +24 -29
- package/dst/ase-log.js +10 -3
- package/dst/ase-markdown.js +135 -102
- package/dst/ase-mcp.js +3 -10
- package/dst/ase-meta.js +6 -4
- package/dst/ase-persona.js +7 -11
- package/dst/ase-service.js +28 -28
- package/dst/ase-setup.js +15 -28
- package/dst/ase-skills.js +2 -2
- package/dst/ase-statusline.js +26 -34
- package/dst/ase-task.js +60 -63
- package/dst/ase-version.js +2 -1
- package/dst/ase.js +7 -11
- package/package.json +13 -12
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +2 -2
- package/plugin/agents/ase-code-lint.md +4 -4
- package/plugin/agents/ase-docs-proofread.md +1 -1
- package/plugin/agents/ase-meta-chat.md +6 -6
- package/plugin/agents/ase-meta-diagram.md +1 -1
- package/plugin/agents/ase-meta-search.md +1 -1
- package/plugin/etc/eslint.mjs +0 -2
- package/plugin/meta/ase-constitution.md +1 -1
- package/plugin/meta/ase-control.md +1 -1
- package/plugin/meta/ase-dialog.md +2 -2
- package/plugin/meta/ase-format-arch.md +8 -8
- package/plugin/meta/ase-format-task.md +8 -10
- package/plugin/meta/ase-getopt.md +1 -1
- package/plugin/meta/ase-skill.md +1 -1
- package/plugin/package.json +8 -8
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-arch-discover/SKILL.md +2 -2
- package/plugin/skills/ase-arch-discover/help.md +3 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +0 -1
- package/plugin/skills/ase-code-craft/SKILL.md +4 -4
- package/plugin/skills/ase-code-lint/SKILL.md +12 -10
- package/plugin/skills/ase-code-refactor/SKILL.md +6 -6
- package/plugin/skills/ase-code-refactor/help.md +1 -1
- package/plugin/skills/ase-code-resolve/SKILL.md +4 -4
- package/plugin/skills/ase-code-resolve/help.md +1 -1
- package/plugin/skills/ase-docs-distill/help.md +6 -4
- package/plugin/skills/ase-docs-proofread/SKILL.md +5 -5
- package/plugin/skills/ase-meta-brainstorm/help.md +1 -1
- package/plugin/skills/ase-meta-changelog/SKILL.md +4 -3
- package/plugin/skills/ase-meta-commit/SKILL.md +3 -1
- package/plugin/skills/ase-meta-compat/SKILL.md +11 -2
- package/plugin/skills/ase-meta-diaboli/SKILL.md +4 -4
- package/plugin/skills/ase-meta-diff/SKILL.md +10 -10
- package/plugin/skills/ase-meta-persona/SKILL.md +1 -1
- package/plugin/skills/ase-meta-quorum/SKILL.md +10 -8
- package/plugin/skills/ase-meta-search/help.md +4 -2
- package/plugin/skills/ase-meta-steelman/SKILL.md +2 -2
- package/plugin/skills/ase-meta-steelman/help.md +5 -3
- package/plugin/skills/ase-meta-why/SKILL.md +1 -1
- package/plugin/skills/ase-meta-why/help.md +3 -1
- package/plugin/skills/ase-sync-export/SKILL.md +13 -10
- package/plugin/skills/ase-sync-import/help.md +2 -2
- package/plugin/skills/ase-sync-reconcile/SKILL.md +1 -1
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-condense/SKILL.md +6 -7
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-edit/SKILL.md +2 -2
- package/plugin/skills/ase-task-grill/SKILL.md +1 -1
- package/plugin/skills/ase-task-implement/help.md +2 -2
- package/plugin/skills/ase-task-preflight/SKILL.md +1 -1
- package/plugin/skills/ase-task-reboot/SKILL.md +13 -9
- package/plugin/skills/ase-task-reboot/help.md +2 -1
package/dst/ase-artifact.js
CHANGED
|
@@ -28,8 +28,9 @@ export class Artifact {
|
|
|
28
28
|
}
|
|
29
29
|
/* translate a single ".gitignore" line into a picomatch-backed rule,
|
|
30
30
|
honoring the anchored-vs-floating, directory-only, and negation
|
|
31
|
-
semantics of ".gitignore" patterns
|
|
32
|
-
|
|
31
|
+
semantics of ".gitignore" patterns; anchored patterns resolve
|
|
32
|
+
relative to the "base" directory holding the ".gitignore" */
|
|
33
|
+
static compileIgnoreRule(line, base) {
|
|
33
34
|
let pattern = line.trim();
|
|
34
35
|
if (pattern === "" || pattern.startsWith("#"))
|
|
35
36
|
return null;
|
|
@@ -46,18 +47,19 @@ export class Artifact {
|
|
|
46
47
|
const anchored = pattern.includes("/");
|
|
47
48
|
if (pattern.startsWith("/"))
|
|
48
49
|
pattern = pattern.slice(1);
|
|
49
|
-
const glob = anchored ? pattern : `**/${pattern}`;
|
|
50
|
+
const glob = anchored ? (base === "" ? pattern : `${base}/${pattern}`) : `**/${pattern}`;
|
|
50
51
|
const isMatch = picomatch(glob, { dot: true });
|
|
51
52
|
return { matcher: (p) => isMatch(p), negated, dirOnly };
|
|
52
53
|
}
|
|
53
|
-
/* load the ".gitignore" rules located directly in a directory
|
|
54
|
-
|
|
54
|
+
/* load the ".gitignore" rules located directly in a directory,
|
|
55
|
+
given as absolute "dir" and project-relative "relDir" */
|
|
56
|
+
static loadIgnoreRules(dir, relDir) {
|
|
55
57
|
const file = path.join(dir, ".gitignore");
|
|
56
58
|
if (!fs.existsSync(file))
|
|
57
59
|
return [];
|
|
58
60
|
const rules = [];
|
|
59
61
|
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
60
|
-
const rule = Artifact.compileIgnoreRule(line);
|
|
62
|
+
const rule = Artifact.compileIgnoreRule(line, relDir);
|
|
61
63
|
if (rule !== null)
|
|
62
64
|
rules.push(rule);
|
|
63
65
|
}
|
|
@@ -83,7 +85,7 @@ export class Artifact {
|
|
|
83
85
|
const root = Task.projectRoot();
|
|
84
86
|
const files = new Set();
|
|
85
87
|
const walk = (dir, relDir, inherited) => {
|
|
86
|
-
const rules = [...inherited, ...Artifact.loadIgnoreRules(dir)];
|
|
88
|
+
const rules = [...inherited, ...Artifact.loadIgnoreRules(dir, relDir)];
|
|
87
89
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
88
90
|
if (entry.name === ".git")
|
|
89
91
|
continue;
|
|
@@ -135,9 +137,7 @@ export class Artifact {
|
|
|
135
137
|
/* read the configured "basedir" anchor and "files" miniglob spec
|
|
136
138
|
for a single kind; "basedir" is project-root-relative (POSIX,
|
|
137
139
|
"" ≡ project root) and "files" resolves relative to "basedir" */
|
|
138
|
-
static spec(
|
|
139
|
-
const cfg = new Config("config", configSchema, log);
|
|
140
|
-
cfg.read();
|
|
140
|
+
static spec(cfg, kind) {
|
|
141
141
|
const basedir = Artifact.configString(cfg, `project.artifact.${kind}.basedir`)
|
|
142
142
|
.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
|
|
143
143
|
.replace(/^\.$/, "");
|
|
@@ -163,10 +163,12 @@ export class Artifact {
|
|
|
163
163
|
/* resolve the requested kinds to project-relative file lists */
|
|
164
164
|
static list(log, kinds) {
|
|
165
165
|
const all = Artifact.universe();
|
|
166
|
+
const cfg = new Config("config", configSchema, log);
|
|
167
|
+
cfg.read();
|
|
166
168
|
/* raw-resolve all five configured kinds */
|
|
167
169
|
const raw = new Map();
|
|
168
170
|
for (const kind of configuredKinds) {
|
|
169
|
-
const { basedir, files } = Artifact.spec(
|
|
171
|
+
const { basedir, files } = Artifact.spec(cfg, kind);
|
|
170
172
|
raw.set(kind, Artifact.resolveKind(basedir, files, all));
|
|
171
173
|
}
|
|
172
174
|
/* partition the universe by descending precedence */
|
|
@@ -174,8 +176,9 @@ export class Artifact {
|
|
|
174
176
|
const part = new Map();
|
|
175
177
|
for (const kind of configuredKinds) {
|
|
176
178
|
const own = [];
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
const resolved = raw.get(kind);
|
|
180
|
+
for (const file of all) {
|
|
181
|
+
if (resolved.has(file) && !claimed.has(file)) {
|
|
179
182
|
own.push(file);
|
|
180
183
|
claimed.add(file);
|
|
181
184
|
}
|
|
@@ -198,7 +201,11 @@ export class Artifact {
|
|
|
198
201
|
const file = filename.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
199
202
|
if (file === "")
|
|
200
203
|
throw new Error("artifact: filename must not be empty");
|
|
201
|
-
|
|
204
|
+
if (file.split("/").includes(".."))
|
|
205
|
+
throw new Error("artifact: filename must not contain \"..\" segments");
|
|
206
|
+
const cfg = new Config("config", configSchema, log);
|
|
207
|
+
cfg.read();
|
|
208
|
+
const { basedir } = Artifact.spec(cfg, kind);
|
|
202
209
|
return basedir === "" ? file : `${basedir}/${file}`;
|
|
203
210
|
}
|
|
204
211
|
}
|
|
@@ -248,6 +255,11 @@ export default class ArtifactCommand {
|
|
|
248
255
|
});
|
|
249
256
|
}
|
|
250
257
|
}
|
|
258
|
+
/* render a caught error as an MCP tool error result */
|
|
259
|
+
const mcpToolError = (err) => ({
|
|
260
|
+
isError: true,
|
|
261
|
+
content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
|
|
262
|
+
});
|
|
251
263
|
/* MCP registration entry point for artifact tools */
|
|
252
264
|
export class ArtifactMCP {
|
|
253
265
|
log;
|
|
@@ -285,11 +297,7 @@ export class ArtifactMCP {
|
|
|
285
297
|
};
|
|
286
298
|
}
|
|
287
299
|
catch (err) {
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
isError: true,
|
|
291
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
292
|
-
};
|
|
300
|
+
return mcpToolError(err);
|
|
293
301
|
}
|
|
294
302
|
});
|
|
295
303
|
mcp.registerTool("ase_artifact_name", {
|
|
@@ -319,11 +327,7 @@ export class ArtifactMCP {
|
|
|
319
327
|
};
|
|
320
328
|
}
|
|
321
329
|
catch (err) {
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
isError: true,
|
|
325
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
326
|
-
};
|
|
330
|
+
return mcpToolError(err);
|
|
327
331
|
}
|
|
328
332
|
});
|
|
329
333
|
}
|
package/dst/ase-config.js
CHANGED
|
@@ -97,17 +97,21 @@ const parseScopeTerm = (value) => {
|
|
|
97
97
|
throw new Error(`invalid --scope term "${value}" ` +
|
|
98
98
|
"(expected: \"user\", \"project\", \"task:<id>\", or \"session:<id>\")");
|
|
99
99
|
};
|
|
100
|
-
/*
|
|
101
|
-
|
|
102
|
-
const hasProjectContext = () => {
|
|
100
|
+
/* determine the Git top-level directory, if inside a Git repository */
|
|
101
|
+
const gitToplevel = () => {
|
|
103
102
|
try {
|
|
104
103
|
const result = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" });
|
|
105
|
-
|
|
106
|
-
return true;
|
|
104
|
+
return result.stdout.trim() || null;
|
|
107
105
|
}
|
|
108
106
|
catch {
|
|
109
|
-
|
|
107
|
+
return null;
|
|
110
108
|
}
|
|
109
|
+
};
|
|
110
|
+
/* detect whether a project context exists, i.e. either we are inside
|
|
111
|
+
a Git working tree or a ".ase" directory is present at or above cwd */
|
|
112
|
+
const hasProjectContext = () => {
|
|
113
|
+
if (gitToplevel() !== null)
|
|
114
|
+
return true;
|
|
111
115
|
let dir = fs.realpathSync(process.cwd());
|
|
112
116
|
for (;;) {
|
|
113
117
|
if (fs.existsSync(path.join(dir, ".ase")))
|
|
@@ -226,14 +230,14 @@ export class Config {
|
|
|
226
230
|
else if (term.kind === "project") {
|
|
227
231
|
const rel = path.join(".ase", `${name}.yaml`);
|
|
228
232
|
const cwd = process.cwd();
|
|
229
|
-
const top =
|
|
233
|
+
const top = gitToplevel();
|
|
230
234
|
const found = top !== null ?
|
|
231
235
|
this.findUpward(cwd, top, rel) :
|
|
232
236
|
(fs.existsSync(path.join(cwd, rel)) ? path.join(cwd, rel) : null);
|
|
233
237
|
return found ?? path.join(top ?? cwd, rel);
|
|
234
238
|
}
|
|
235
239
|
else if (term.kind === "task") {
|
|
236
|
-
const top =
|
|
240
|
+
const top = gitToplevel() ?? process.cwd();
|
|
237
241
|
return path.join(top, ".ase", "task", term.id, `${name}.yaml`);
|
|
238
242
|
}
|
|
239
243
|
else
|
|
@@ -258,18 +262,6 @@ export class Config {
|
|
|
258
262
|
}
|
|
259
263
|
return null;
|
|
260
264
|
}
|
|
261
|
-
/* determine the Git top-level directory, if inside a Git repository */
|
|
262
|
-
gitToplevel() {
|
|
263
|
-
try {
|
|
264
|
-
const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
|
|
265
|
-
stderr: "ignore"
|
|
266
|
-
});
|
|
267
|
-
return result.stdout.trim() || null;
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
265
|
/* read the full scope chain into memory; the requested mode applies
|
|
274
266
|
to the target scope only, inherited scopes are always lenient */
|
|
275
267
|
read(mode = "lenient") {
|
|
@@ -538,8 +530,8 @@ export default class ConfigCommand {
|
|
|
538
530
|
"\"user\" is always implicitly included and \"project\" is implicitly " +
|
|
539
531
|
"included whenever a project context (Git repo or upward \".ase\" directory) exists")
|
|
540
532
|
.description("manage ASE configuration")
|
|
541
|
-
.action((
|
|
542
|
-
|
|
533
|
+
.action(() => {
|
|
534
|
+
configCmd.outputHelp();
|
|
543
535
|
process.exit(1);
|
|
544
536
|
});
|
|
545
537
|
/* register CLI sub-command "ase config init" */
|
|
@@ -662,6 +654,11 @@ export default class ConfigCommand {
|
|
|
662
654
|
});
|
|
663
655
|
}
|
|
664
656
|
}
|
|
657
|
+
/* render a caught error as an MCP tool error result */
|
|
658
|
+
const mcpToolError = (err) => ({
|
|
659
|
+
isError: true,
|
|
660
|
+
content: [{ type: "text", text: `ERROR: ${err instanceof Error ? err.message : String(err)}` }]
|
|
661
|
+
});
|
|
665
662
|
/* MCP registration entry point for layered YAML configuration access */
|
|
666
663
|
export class ConfigMCP {
|
|
667
664
|
log;
|
|
@@ -687,17 +684,13 @@ export class ConfigMCP {
|
|
|
687
684
|
try {
|
|
688
685
|
const scope = parseScope(args.scope);
|
|
689
686
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
690
|
-
|
|
691
|
-
cfg.
|
|
692
|
-
|
|
693
|
-
const val = cfg.get(args.key);
|
|
694
|
-
text = val === undefined ? "" : JSON.stringify(val);
|
|
695
|
-
});
|
|
687
|
+
cfg.read();
|
|
688
|
+
const val = cfg.get(args.key);
|
|
689
|
+
const text = val === undefined ? "" : JSON.stringify(val);
|
|
696
690
|
return { content: [{ type: "text", text }] };
|
|
697
691
|
}
|
|
698
692
|
catch (err) {
|
|
699
|
-
|
|
700
|
-
return { isError: true, content: [{ type: "text", text: `config_get: ERROR: ${message}` }] };
|
|
693
|
+
return mcpToolError(err);
|
|
701
694
|
}
|
|
702
695
|
});
|
|
703
696
|
/* config set */
|
|
@@ -723,11 +716,10 @@ export class ConfigMCP {
|
|
|
723
716
|
cfg.set(args.key, args.val);
|
|
724
717
|
cfg.write();
|
|
725
718
|
});
|
|
726
|
-
return { content: [{ type: "text", text: `
|
|
719
|
+
return { content: [{ type: "text", text: `OK: stored "${args.key}" on scope "${args.scope}"` }] };
|
|
727
720
|
}
|
|
728
721
|
catch (err) {
|
|
729
|
-
|
|
730
|
-
return { isError: true, content: [{ type: "text", text: `config_set: ERROR: ${message}` }] };
|
|
722
|
+
return mcpToolError(err);
|
|
731
723
|
}
|
|
732
724
|
});
|
|
733
725
|
/* config delete */
|
|
@@ -750,11 +742,10 @@ export class ConfigMCP {
|
|
|
750
742
|
cfg.delete(args.key);
|
|
751
743
|
cfg.write();
|
|
752
744
|
});
|
|
753
|
-
return { content: [{ type: "text", text: `
|
|
745
|
+
return { content: [{ type: "text", text: `OK: removed "${args.key}" on scope "${args.scope}"` }] };
|
|
754
746
|
}
|
|
755
747
|
catch (err) {
|
|
756
|
-
|
|
757
|
-
return { isError: true, content: [{ type: "text", text: `config_delete: ERROR: ${message}` }] };
|
|
748
|
+
return mcpToolError(err);
|
|
758
749
|
}
|
|
759
750
|
});
|
|
760
751
|
}
|
package/dst/ase-diagram.js
CHANGED
|
@@ -7,6 +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 getStdin from "get-stdin";
|
|
10
11
|
/* custom argument parser for Commander: non-negative integer */
|
|
11
12
|
const parseInteger = (name) => (value) => {
|
|
12
13
|
const n = Number.parseInt(value, 10);
|
|
@@ -211,13 +212,6 @@ export class Diagram {
|
|
|
211
212
|
return out;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
|
-
/* read stdin into a single string */
|
|
215
|
-
const readStdin = async () => {
|
|
216
|
-
const chunks = [];
|
|
217
|
-
for await (const chunk of process.stdin)
|
|
218
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
219
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
220
|
-
};
|
|
221
215
|
/* command-line handling */
|
|
222
216
|
export default class DiagramCommand {
|
|
223
217
|
log;
|
|
@@ -241,12 +235,20 @@ export default class DiagramCommand {
|
|
|
241
235
|
.option("--terminal-width <n>", "width of terminal of <n> characters (for diagram clipping)", parseInteger("--terminal-width"), Diagram.detectTermWidth())
|
|
242
236
|
.option("--terminal-height <n>", "height of terminal of <n> lines (for diagram clipping)", parseInteger("--terminal-height"), Diagram.detectTermHeight())
|
|
243
237
|
.action(async (opts) => {
|
|
244
|
-
/* fetch Mermaid diagram specification from stdin */
|
|
238
|
+
/* fetch Mermaid diagram specification from file or stdin */
|
|
245
239
|
let src;
|
|
246
|
-
if (opts.input !== undefined)
|
|
247
|
-
|
|
240
|
+
if (opts.input !== undefined) {
|
|
241
|
+
try {
|
|
242
|
+
src = fs.readFileSync(opts.input, "utf8");
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
246
|
+
this.log.write("error", `diagram: failed to read input file: ${message}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
248
250
|
else
|
|
249
|
-
src = await
|
|
251
|
+
src = await getStdin();
|
|
250
252
|
if (src.trim() === "") {
|
|
251
253
|
this.log.write("error", "diagram: empty Mermaid diagram specification");
|
|
252
254
|
process.exit(1);
|
|
@@ -254,18 +256,7 @@ export default class DiagramCommand {
|
|
|
254
256
|
/* create diagram rendering */
|
|
255
257
|
let out;
|
|
256
258
|
try {
|
|
257
|
-
out = Diagram.render(src,
|
|
258
|
-
format: opts.format,
|
|
259
|
-
ascii: opts.ascii ?? false,
|
|
260
|
-
colorMode: opts.colorMode,
|
|
261
|
-
nodeMarginX: opts.nodeMarginX,
|
|
262
|
-
nodeMarginY: opts.nodeMarginY,
|
|
263
|
-
nodePadding: opts.nodePadding,
|
|
264
|
-
diagramClipX: opts.diagramClipX,
|
|
265
|
-
diagramClipY: opts.diagramClipY,
|
|
266
|
-
terminalWidth: opts.terminalWidth,
|
|
267
|
-
terminalHeight: opts.terminalHeight
|
|
268
|
-
});
|
|
259
|
+
out = Diagram.render(src, opts);
|
|
269
260
|
}
|
|
270
261
|
catch (err) {
|
|
271
262
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -321,18 +312,7 @@ export class DiagramMCP {
|
|
|
321
312
|
}
|
|
322
313
|
}, async (args) => {
|
|
323
314
|
try {
|
|
324
|
-
const out = Diagram.render(args.diagram,
|
|
325
|
-
format: args.format,
|
|
326
|
-
ascii: args.ascii,
|
|
327
|
-
colorMode: args.colorMode,
|
|
328
|
-
nodeMarginX: args.nodeMarginX,
|
|
329
|
-
nodeMarginY: args.nodeMarginY,
|
|
330
|
-
nodePadding: args.nodePadding,
|
|
331
|
-
diagramClipX: args.diagramClipX,
|
|
332
|
-
diagramClipY: args.diagramClipY,
|
|
333
|
-
terminalWidth: args.terminalWidth,
|
|
334
|
-
terminalHeight: args.terminalHeight
|
|
335
|
-
});
|
|
315
|
+
const out = Diagram.render(args.diagram, args);
|
|
336
316
|
return {
|
|
337
317
|
content: [{ type: "text", text: out }]
|
|
338
318
|
};
|
package/dst/ase-getopt.js
CHANGED
|
@@ -55,8 +55,10 @@ export class GetoptMCP {
|
|
|
55
55
|
/* tokenize spec and add one option per token */
|
|
56
56
|
const tokens = args.spec.split(/\s+/).filter((e) => e.length > 0);
|
|
57
57
|
const re = /^--([A-Za-z][A-Za-z0-9-]*)(?:\|-([A-Za-z]))?(?:=(\((.*)\)(\.\.\.)?|.*))?$/;
|
|
58
|
+
const camelKey = (long) => long.replace(/-(.)/g, (_, c) => c.toUpperCase());
|
|
58
59
|
const internals = new Set();
|
|
59
60
|
const flagTakesValue = new Map();
|
|
61
|
+
const listOpts = [];
|
|
60
62
|
for (const tok of tokens) {
|
|
61
63
|
const m = re.exec(tok);
|
|
62
64
|
if (m === null)
|
|
@@ -83,32 +85,25 @@ export class GetoptMCP {
|
|
|
83
85
|
}
|
|
84
86
|
else
|
|
85
87
|
opt.default(false);
|
|
86
|
-
if (
|
|
88
|
+
if (choices !== null && isList)
|
|
89
|
+
listOpts.push({ long, choices });
|
|
90
|
+
if (long.startsWith("int-")) {
|
|
87
91
|
/* internal option: hide from usage help and remember
|
|
88
92
|
its camel-cased key for the info rendering below */
|
|
89
93
|
opt.hideHelp();
|
|
90
|
-
internals.add(long
|
|
94
|
+
internals.add(camelKey(long));
|
|
91
95
|
}
|
|
92
96
|
cmd.addOption(opt);
|
|
93
97
|
}
|
|
94
98
|
/* parse args */
|
|
95
99
|
cmd.parse(argsVec, { from: "user" });
|
|
96
|
-
/* validate comma-separated list-of-choices options
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const m = re.exec(tok);
|
|
100
|
-
if (m === null)
|
|
101
|
-
continue;
|
|
102
|
-
const long = m[1];
|
|
103
|
-
const choicePart = m[4] ?? null;
|
|
104
|
-
const listMarker = m[5] ?? null;
|
|
105
|
-
if (choicePart !== null && listMarker !== null)
|
|
106
|
-
listOpts.push({ long, choices: choicePart.split("|") });
|
|
107
|
-
}
|
|
100
|
+
/* validate comma-separated list-of-choices options (only
|
|
101
|
+
the first supplied token is checked here -- skills
|
|
102
|
+
validate the remaining tokens as they consume them) */
|
|
108
103
|
if (listOpts.length > 0) {
|
|
109
104
|
const opts = cmd.opts();
|
|
110
105
|
for (const { long, choices } of listOpts) {
|
|
111
|
-
const key = long
|
|
106
|
+
const key = camelKey(long);
|
|
112
107
|
const v = opts[key];
|
|
113
108
|
if (typeof v !== "string")
|
|
114
109
|
continue;
|
package/dst/ase-hook.js
CHANGED
|
@@ -217,16 +217,13 @@ export default class HookCommand {
|
|
|
217
217
|
const cfg = new Config("config", configSchema, this.log, hasSession ? parseScope(`session:${sessionId}`) : parseScope(undefined));
|
|
218
218
|
/* determine task id (only persist when scoped to a real session) */
|
|
219
219
|
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
220
|
-
|
|
221
|
-
cfg.
|
|
222
|
-
|
|
220
|
+
cfg.lock(() => {
|
|
221
|
+
cfg.read();
|
|
222
|
+
if (hasSession) {
|
|
223
223
|
cfg.set("agent.task", taskId);
|
|
224
224
|
cfg.write();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
cfg.lock(() => {
|
|
228
|
-
cfg.read();
|
|
229
|
-
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
230
227
|
/* initialize agent activity status */
|
|
231
228
|
this.writeAgentStatus("ready");
|
|
232
229
|
/* determine project id */
|
|
@@ -333,19 +330,19 @@ export default class HookCommand {
|
|
|
333
330
|
"@ase_agent_status", icon], { stdio: "ignore", reject: false });
|
|
334
331
|
}
|
|
335
332
|
}
|
|
336
|
-
/* handler for "ase hook user-prompt-submit" (
|
|
333
|
+
/* handler for "ase hook user-prompt-submit" (all tools) */
|
|
337
334
|
async doUserPromptSubmit(_tool) {
|
|
338
335
|
await this.drainStdin();
|
|
339
336
|
this.writeAgentStatus("busy");
|
|
340
337
|
return 0;
|
|
341
338
|
}
|
|
342
|
-
/* handler for "ase hook stop" (
|
|
339
|
+
/* handler for "ase hook stop" (all tools) */
|
|
343
340
|
async doStop(_tool) {
|
|
344
341
|
await this.drainStdin();
|
|
345
342
|
this.writeAgentStatus("ready");
|
|
346
343
|
return 0;
|
|
347
344
|
}
|
|
348
|
-
/* handler for "ase hook session-end" (
|
|
345
|
+
/* handler for "ase hook session-end" (all tools) */
|
|
349
346
|
async doSessionEnd(_tool) {
|
|
350
347
|
/* determine session id */
|
|
351
348
|
const sessionId = await this.readSessionIdFromStdin();
|
|
@@ -432,7 +429,9 @@ export default class HookCommand {
|
|
|
432
429
|
}));
|
|
433
430
|
else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
|
|
434
431
|
toolInput = rawInput;
|
|
435
|
-
|
|
432
|
+
const command = toolInput.command ?? "";
|
|
433
|
+
if (toolName === spec.bashToolName && /^ase(\s|$)/.test(command)
|
|
434
|
+
&& !/[;&|<>`\n]|\$\(/.test(command))
|
|
436
435
|
return { approve: true, reason: "ASE CLI invocation auto-approved" };
|
|
437
436
|
else if (toolName === "Skill" && /^(?:ase:)?ase-.+/.test(toolInput.skill ?? ""))
|
|
438
437
|
return { approve: true, reason: "ASE skill invocation auto-approved" };
|
|
@@ -450,6 +449,17 @@ export default class HookCommand {
|
|
|
450
449
|
}
|
|
451
450
|
return { approve: false, reason: "" };
|
|
452
451
|
}
|
|
452
|
+
/* read the hook event payload from stdin and parse it into the
|
|
453
|
+
loosely-typed input object shared by the tool-approval handlers */
|
|
454
|
+
async readHookInput(tool) {
|
|
455
|
+
const spec = toolSpecs[tool];
|
|
456
|
+
const stdin = await this.readStdin();
|
|
457
|
+
const input = this.parseJSON(stdin, v.looseObject({
|
|
458
|
+
session_id: v.optional(v.string()),
|
|
459
|
+
sessionId: v.optional(v.string())
|
|
460
|
+
}));
|
|
461
|
+
return { spec, input };
|
|
462
|
+
}
|
|
453
463
|
/* handler for "ase hook pre-tool-use" (all tools).
|
|
454
464
|
For Anthropic Claude Code CLI and GitHub Copilot CLI this is where ASE tool
|
|
455
465
|
invocations are auto-approved (via "permissionDecision: allow").
|
|
@@ -459,13 +469,8 @@ export default class HookCommand {
|
|
|
459
469
|
still drain stdin, as Codex treats a non-draining hook as a hard
|
|
460
470
|
error. */
|
|
461
471
|
async doPreToolUse(tool) {
|
|
462
|
-
const spec = toolSpecs[tool];
|
|
463
472
|
/* read tool invocation information */
|
|
464
|
-
const
|
|
465
|
-
const input = this.parseJSON(stdin, v.looseObject({
|
|
466
|
-
session_id: v.optional(v.string()),
|
|
467
|
-
sessionId: v.optional(v.string())
|
|
468
|
-
}));
|
|
473
|
+
const { spec, input } = await this.readHookInput(tool);
|
|
469
474
|
/* Codex auto-approves through "PermissionRequest", not here */
|
|
470
475
|
if (spec.approvalEvent !== "PreToolUse")
|
|
471
476
|
return 0;
|
|
@@ -496,13 +501,8 @@ export default class HookCommand {
|
|
|
496
501
|
Staying silent (or returning a non-approval) defers to Codex's
|
|
497
502
|
normal approval flow. */
|
|
498
503
|
async doPermissionRequest(tool) {
|
|
499
|
-
const spec = toolSpecs[tool];
|
|
500
504
|
/* read tool invocation information */
|
|
501
|
-
const
|
|
502
|
-
const input = this.parseJSON(stdin, v.looseObject({
|
|
503
|
-
session_id: v.optional(v.string()),
|
|
504
|
-
sessionId: v.optional(v.string())
|
|
505
|
-
}));
|
|
505
|
+
const { spec, input } = await this.readHookInput(tool);
|
|
506
506
|
/* determine whether to auto-approve the tool invocation */
|
|
507
507
|
const { approve } = this.decideApproval(tool, spec, input);
|
|
508
508
|
/* emit the Codex "PermissionRequest" approval decision */
|
|
@@ -525,13 +525,15 @@ export default class HookCommand {
|
|
|
525
525
|
}
|
|
526
526
|
/* register commands */
|
|
527
527
|
register(program) {
|
|
528
|
-
/* default for --tool derived from ASE_TOOL environment variable
|
|
528
|
+
/* default for --tool derived from ASE_TOOL environment variable
|
|
529
|
+
(validated lazily by "parseTool" in each action, so an invalid
|
|
530
|
+
value cannot break unrelated "ase" commands at startup) */
|
|
529
531
|
const envTool = process.env.ASE_TOOL ?? "";
|
|
530
|
-
const toolDflt = envTool !== "" ?
|
|
532
|
+
const toolDflt = envTool !== "" ? envTool : "claude";
|
|
531
533
|
/* register CLI top-level command "ase hook" */
|
|
532
534
|
const hookCmd = program
|
|
533
535
|
.command("hook")
|
|
534
|
-
.description("Anthropic Claude Code CLI
|
|
536
|
+
.description("Anthropic Claude Code CLI, GitHub Copilot CLI, and OpenAI Codex CLI hook entry points")
|
|
535
537
|
.action(() => {
|
|
536
538
|
hookCmd.outputHelp();
|
|
537
539
|
process.exit(1);
|