@rse/ase 0.9.47 → 0.9.49
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 +76 -13
- package/dst/ase-diagram.js +10 -15
- package/dst/ase-getopt.js +66 -60
- package/dst/ase-guidance.js +89 -0
- package/dst/ase-hook.js +46 -66
- 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 -6
- package/dst/ase-setup.js +24 -39
- package/dst/ase-skills.js +1 -1
- package/dst/ase-statusline.js +42 -18
- 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/meta/ase-common-task.md +9 -0
- package/plugin/meta/ase-constitution.md +17 -6
- package/plugin/meta/ase-getopt.md +11 -1
- package/plugin/meta/ase-skill.md +92 -4
- package/plugin/package.json +1 -1
- package/plugin/skills/ase-arch-analyze/SKILL.md +8 -6
- package/plugin/skills/ase-arch-discover/SKILL.md +12 -0
- package/plugin/skills/ase-code-analyze/SKILL.md +6 -4
- package/plugin/skills/ase-code-lint/SKILL.md +16 -0
- package/plugin/skills/ase-docs-proofread/SKILL.md +10 -0
- package/plugin/skills/ase-help-intent/SKILL.md +3 -8
- package/plugin/skills/ase-help-skill/SKILL.md +10 -2
- package/plugin/skills/ase-help-skill/catalog.md +1 -1
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +8 -0
- package/plugin/skills/ase-meta-commit/SKILL.md +13 -0
- package/plugin/skills/ase-meta-config/SKILL.md +165 -0
- package/plugin/skills/ase-meta-config/help.md +136 -0
- package/plugin/skills/ase-meta-diaboli/help.md +1 -1
- package/plugin/skills/ase-meta-diff/SKILL.md +22 -0
- package/plugin/skills/ase-meta-review/SKILL.md +13 -1
- package/plugin/skills/ase-sync-export/SKILL.md +14 -0
- package/plugin/skills/ase-sync-import/SKILL.md +10 -0
- package/plugin/skills/ase-task-list/SKILL.md +21 -0
- package/plugin/skills/ase-task-view/SKILL.md +21 -0
- package/plugin/skills/ase-meta-persona/SKILL.md +0 -84
- package/plugin/skills/ase-meta-persona/help.md +0 -60
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
|
@@ -22,7 +22,6 @@ import { TaskMCP } from "./ase-task.js";
|
|
|
22
22
|
import { MarkdownMCP } from "./ase-markdown.js";
|
|
23
23
|
import { ArtifactMCP } from "./ase-artifact.js";
|
|
24
24
|
import { KVMCP } from "./ase-kv.js";
|
|
25
|
-
import PersonaMCP from "./ase-persona.js";
|
|
26
25
|
import { TimestampMCP } from "./ase-timestamp.js";
|
|
27
26
|
import { GetoptMCP } from "./ase-getopt.js";
|
|
28
27
|
import { SkillsMCP } from "./ase-skills.js";
|
|
@@ -243,7 +242,6 @@ export default class ServiceCommand {
|
|
|
243
242
|
new MarkdownMCP().register(mcp);
|
|
244
243
|
new ArtifactMCP(this.log).register(mcp);
|
|
245
244
|
new KVMCP().register(mcp);
|
|
246
|
-
new PersonaMCP(this.log).register(mcp);
|
|
247
245
|
new TimestampMCP().register(mcp);
|
|
248
246
|
new GetoptMCP().register(mcp);
|
|
249
247
|
new SkillsMCP().register(mcp);
|
|
@@ -494,7 +492,7 @@ export default class ServiceCommand {
|
|
|
494
492
|
process.stdout.write("service: not running (no port configured)\n");
|
|
495
493
|
return 1;
|
|
496
494
|
}
|
|
497
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
495
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
498
496
|
if (match === true) {
|
|
499
497
|
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.commandRequest("status", 2000));
|
|
500
498
|
const d = r._data;
|
|
@@ -513,13 +511,13 @@ export default class ServiceCommand {
|
|
|
513
511
|
/* send command: POST /command with the arbitrary cmd token */
|
|
514
512
|
async doSend(cmd) {
|
|
515
513
|
let ctx = this.loadContext();
|
|
516
|
-
if (ctx.port === null || await probe(ctx.port, ctx.projectId) !== true) {
|
|
514
|
+
if (ctx.port === null || await probe(ctx.port, ctx.projectId).catch(() => null) !== true) {
|
|
517
515
|
/* auto-start the service once, then re-check */
|
|
518
516
|
await this.doStart();
|
|
519
517
|
ctx = this.loadContext();
|
|
520
518
|
if (ctx.port === null)
|
|
521
519
|
throw new Error("service not running (no port configured after auto-start)");
|
|
522
|
-
if (await probe(ctx.port, ctx.projectId) !== true)
|
|
520
|
+
if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
|
|
523
521
|
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
524
522
|
}
|
|
525
523
|
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.commandRequest(cmd, 5000), responseType: "text" });
|
|
@@ -536,7 +534,7 @@ export default class ServiceCommand {
|
|
|
536
534
|
this.log.write("info", "service: not running (no port configured)");
|
|
537
535
|
return 0;
|
|
538
536
|
}
|
|
539
|
-
const match = await probe(ctx.port, ctx.projectId);
|
|
537
|
+
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
540
538
|
if (match === false) {
|
|
541
539
|
this.log.write("info", `service: not running (port ${ctx.port} in use by foreign service)`);
|
|
542
540
|
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);
|
|
@@ -586,7 +575,7 @@ export default class SetupCommand {
|
|
|
586
575
|
statuslineFormatDflt = [
|
|
587
576
|
"<blue>%u</blue> <red>%p</red> <black>%T</black> %s",
|
|
588
577
|
"%m %e %t",
|
|
589
|
-
"%P %c"
|
|
578
|
+
"%P %h %c"
|
|
590
579
|
];
|
|
591
580
|
/* resolve the tool settings file for a given installation scope */
|
|
592
581
|
statuslineSettingsFile(tool, scope) {
|
|
@@ -618,8 +607,9 @@ export default class SetupCommand {
|
|
|
618
607
|
}
|
|
619
608
|
/* build the "ase statusline ..." command string from the activate
|
|
620
609
|
options and the effective format lines */
|
|
621
|
-
statuslineCommand(opts, format) {
|
|
622
|
-
const parts = ["ase", "statusline", "-w", String(opts.width), "-m", String(opts.margin)
|
|
610
|
+
statuslineCommand(tool, opts, format) {
|
|
611
|
+
const parts = ["ase", "statusline", "--tool", tool, "-w", String(opts.width), "-m", String(opts.margin),
|
|
612
|
+
"-p", String(opts.padding)];
|
|
623
613
|
if (!opts.icons)
|
|
624
614
|
parts.push("--no-icons");
|
|
625
615
|
if (!opts.labels)
|
|
@@ -657,24 +647,18 @@ export default class SetupCommand {
|
|
|
657
647
|
AST, reproducing the exact whitespace tokens json-asty emits for a
|
|
658
648
|
canonically 4-space-indented settings.json */
|
|
659
649
|
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) => {
|
|
650
|
+
const member = (key, val, epilog) => {
|
|
651
|
+
const type = typeof val === "number" ? "number" : "string";
|
|
652
|
+
const body = typeof val === "number" ? String(val) : JSON.stringify(val);
|
|
669
653
|
const k = root.create("string").set({ body: JSON.stringify(key), value: key, epilog: ": " });
|
|
670
|
-
const v = root.create(
|
|
654
|
+
const v = root.create(type).set({ body, value: val });
|
|
671
655
|
const m = root.create("member");
|
|
672
656
|
if (epilog !== undefined)
|
|
673
657
|
m.set({ epilog });
|
|
674
658
|
return m.add(k, v);
|
|
675
659
|
};
|
|
676
660
|
const obj = root.create("object").set({ prolog: "{\n ", epilog: "\n }\n" });
|
|
677
|
-
obj.add(
|
|
661
|
+
obj.add(member("type", "command", ",\n "), member("command", command, ",\n "), member("padding", 0));
|
|
678
662
|
const key = root.create("string").set({
|
|
679
663
|
body: JSON.stringify("statusLine"),
|
|
680
664
|
value: "statusLine",
|
|
@@ -716,7 +700,7 @@ export default class SetupCommand {
|
|
|
716
700
|
this.requireStatuslineTool(tool);
|
|
717
701
|
this.requireStatuslineScope(tool, scope);
|
|
718
702
|
const file = this.statuslineSettingsFile(tool, scope);
|
|
719
|
-
const command = this.statuslineCommand(opts, format);
|
|
703
|
+
const command = this.statuslineCommand(tool, opts, format);
|
|
720
704
|
const root = await this.statuslineReadAst(file);
|
|
721
705
|
const existing = this.statuslineFindMember(root);
|
|
722
706
|
if (existing !== undefined) {
|
|
@@ -940,10 +924,11 @@ export default class SetupCommand {
|
|
|
940
924
|
.option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
|
|
941
925
|
.option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseNonNeg("width"), 0)
|
|
942
926
|
.option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseNonNeg("margin"), 2)
|
|
927
|
+
.option("-p, --padding <n>", "pad each rendered line with <n> spaces on each side", parseNonNeg("padding"), 0)
|
|
943
928
|
.option("--no-icons", "disable icons in placeholder rendering")
|
|
944
929
|
.option("--no-labels", "disable labels in front of bold values")
|
|
945
930
|
.action(async (format, opts) => {
|
|
946
|
-
process.exit(await this.doStatuslineActivate(this.parseTool(opts.tool), this.parseScope(opts.scope), { width: opts.width, margin: opts.margin, icons: opts.icons, labels: opts.labels }, format ?? []));
|
|
931
|
+
process.exit(await this.doStatuslineActivate(this.parseTool(opts.tool), this.parseScope(opts.scope), { width: opts.width, margin: opts.margin, padding: opts.padding, icons: opts.icons, labels: opts.labels }, format ?? []));
|
|
947
932
|
});
|
|
948
933
|
/* register CLI sub-command "ase setup statusline deactivate" */
|
|
949
934
|
statuslineCmd
|
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;
|
|
@@ -196,9 +189,10 @@ export default class StatuslineCommand {
|
|
|
196
189
|
.option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\"; \"codex\" is unsupported)", toolDflt)
|
|
197
190
|
.option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseInteger("--width"), 0)
|
|
198
191
|
.option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseInteger("--margin"), 2)
|
|
192
|
+
.option("-p, --padding <n>", "pad each rendered line with <n> spaces on each side", parseInteger("--padding"), 0)
|
|
199
193
|
.option("--no-icons", "disable icons in placeholder rendering")
|
|
200
194
|
.option("--no-labels", "disable labels in front of bold values")
|
|
201
|
-
.argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %c %C %a %r " +
|
|
195
|
+
.argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %h %c %C %a %r " +
|
|
202
196
|
"%S %D %W %Q %H %X %b %g %G %d %M %V placeholders and <color>...</color> markup " +
|
|
203
197
|
"(color: black, red, green, yellow, blue, magenta, cyan, white, default) " +
|
|
204
198
|
"(default: single line \"%m %e %t\")")
|
|
@@ -218,8 +212,7 @@ export default class StatuslineCommand {
|
|
|
218
212
|
}
|
|
219
213
|
catch (err) {
|
|
220
214
|
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
-
|
|
222
|
-
process.exit(1);
|
|
215
|
+
throw new Error(`statusline: invalid JSON on stdin: ${message}`);
|
|
223
216
|
}
|
|
224
217
|
/* normalize Copilot CLI's top-level "cwd" into the
|
|
225
218
|
"workspace.current_dir" structure shared with Anthropic Claude Code CLI */
|
|
@@ -228,9 +221,11 @@ export default class StatuslineCommand {
|
|
|
228
221
|
&& typeof data.cwd === "string" && data.cwd !== "") {
|
|
229
222
|
data.workspace = { ...(data.workspace ?? {}), current_dir: data.cwd };
|
|
230
223
|
}
|
|
231
|
-
/* determine effective terminal width and budget
|
|
224
|
+
/* determine effective terminal width and budget
|
|
225
|
+
(the padding spaces occupy terminal columns, too,
|
|
226
|
+
so they have to be subtracted from the budget) */
|
|
232
227
|
const width = opts.width > 0 ? opts.width : detectTermWidth();
|
|
233
|
-
const budget = width > 0 ? width - 2 * opts.margin : 0;
|
|
228
|
+
const budget = width > 0 ? width - 2 * (opts.margin + opts.padding) : 0;
|
|
234
229
|
/* shared output state and append helper with auto-wrap;
|
|
235
230
|
the helper itself strips ANSI CSI escape sequences to
|
|
236
231
|
measure the raw visible width of the chunk */
|
|
@@ -272,20 +267,24 @@ export default class StatuslineCommand {
|
|
|
272
267
|
const getCfg = memoize(() => {
|
|
273
268
|
let taskId = process.env.ASE_TASK_ID ?? "";
|
|
274
269
|
let persona = process.env.ASE_PERSONA_STYLE ?? "";
|
|
270
|
+
let guidance = process.env.ASE_GUIDANCE_LEVEL ?? "";
|
|
275
271
|
try {
|
|
276
272
|
const cfg = new Config("config", configSchema, this.log, parseScope(`session:${getSession()}`));
|
|
277
273
|
cfg.read("lenient");
|
|
278
274
|
const t = String(cfg.get("agent.task") ?? "").trim();
|
|
279
275
|
const p = String(cfg.get("agent.persona") ?? "").trim();
|
|
276
|
+
const g = String(cfg.get("agent.guidance") ?? "").trim();
|
|
280
277
|
if (t !== "")
|
|
281
278
|
taskId = t;
|
|
282
279
|
if (p !== "")
|
|
283
280
|
persona = p;
|
|
281
|
+
if (g !== "")
|
|
282
|
+
guidance = g;
|
|
284
283
|
}
|
|
285
284
|
catch (_e) {
|
|
286
285
|
/* cascade unavailable; keep env-var fallbacks */
|
|
287
286
|
}
|
|
288
|
-
return { taskId, persona };
|
|
287
|
+
return { taskId, persona, guidance };
|
|
289
288
|
});
|
|
290
289
|
const getGit = memoize(() => probeGit(data.workspace?.current_dir ?? ""));
|
|
291
290
|
const getMem = memoize(() => probeMemory());
|
|
@@ -331,6 +330,11 @@ export default class StatuslineCommand {
|
|
|
331
330
|
if (persona !== "")
|
|
332
331
|
emit(`${prefix("☯", "persona")}${c.bold(persona)}`);
|
|
333
332
|
},
|
|
333
|
+
h: () => {
|
|
334
|
+
const { guidance } = getCfg();
|
|
335
|
+
if (guidance !== "")
|
|
336
|
+
emit(`${prefix("▶", "guidance")}${c.bold(guidance)}`);
|
|
337
|
+
},
|
|
334
338
|
/* ==== CONTEXT ==== */
|
|
335
339
|
c: () => {
|
|
336
340
|
const pct = Math.min(100, Math.max(0, Math.floor(data.context_window?.used_percentage ?? 0)));
|
|
@@ -340,10 +344,18 @@ export default class StatuslineCommand {
|
|
|
340
344
|
emit(`${prefix("◔", "context")}${bar} ${pct}%`);
|
|
341
345
|
},
|
|
342
346
|
C: () => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
/* probe the live-context fields first and fall back to the
|
|
348
|
+
cumulative ones, so each harness contributes its own view:
|
|
349
|
+
GitHub Copilot CLI reports the live window separately (its
|
|
350
|
+
"total_*" are session sums), whereas Anthropic Claude Code CLI
|
|
351
|
+
folds it into "total_input_tokens". Output tokens stay out --
|
|
352
|
+
"used_percentage" is input-side only. */
|
|
353
|
+
const cw = data.context_window;
|
|
354
|
+
const pct = cw?.used_percentage ?? 0;
|
|
355
|
+
const tokensCur = cw?.current_context_tokens ??
|
|
356
|
+
cw?.last_call_input_tokens ?? cw?.total_input_tokens ?? 0;
|
|
357
|
+
const tokensLim = cw?.displayed_context_limit ?? cw?.context_window_size ??
|
|
358
|
+
(pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0);
|
|
347
359
|
if (tokensLim > 0)
|
|
348
360
|
emit(`${prefix("◆", "tokens")}${c.bold(formatTokens(tokensCur) + "/" + formatTokens(tokensLim))}`);
|
|
349
361
|
},
|
|
@@ -462,6 +474,18 @@ export default class StatuslineCommand {
|
|
|
462
474
|
out += "\n";
|
|
463
475
|
col = 0;
|
|
464
476
|
}
|
|
477
|
+
/* optionally pad all non-empty lines with spaces on both sides
|
|
478
|
+
(the trailing empty chunk after the final line break and any
|
|
479
|
+
intentionally empty line are left untouched, so that no
|
|
480
|
+
whitespace-only line is produced) */
|
|
481
|
+
if (opts.padding > 0) {
|
|
482
|
+
const pad = " ".repeat(opts.padding);
|
|
483
|
+
out = out.split("\n").map((line) => line !== "" ? `${pad}${line}${pad}` : line).join("\n");
|
|
484
|
+
/* for Copilot, prefix with a zero-width non-breaking space
|
|
485
|
+
to prevent the first line to be unindented */
|
|
486
|
+
if (tool === "copilot")
|
|
487
|
+
out = "\u200B" + out;
|
|
488
|
+
}
|
|
465
489
|
/* send output */
|
|
466
490
|
await writeStdout(out);
|
|
467
491
|
/* optionally publish task id to the calling tmux pane as a per-pane user
|
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.49",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|
|
@@ -45,6 +45,15 @@ Task Skill Common Steps
|
|
|
45
45
|
<template>
|
|
46
46
|
⧉ **ASE**: ☻ skill: **<arg1/>**, ▶ ERROR: expected single `[<id>]` argument
|
|
47
47
|
</template>
|
|
48
|
+
|
|
49
|
+
Directly *after* this error <template/>, and *before* stopping,
|
|
50
|
+
give the corrective hint by expanding the following (which,
|
|
51
|
+
depending on the configured <ase-guidance-level/>, may expand into
|
|
52
|
+
nothing and hence emit no output at all):
|
|
53
|
+
|
|
54
|
+
<ase-tpl-hint level="verbose">
|
|
55
|
+
Run `/<arg1/> --help` for the accepted arguments of this skill, or use `/ase-help-intent` to have a fitting command proposed for a free-text intent.
|
|
56
|
+
</ase-tpl-hint>
|
|
48
57
|
</elseif>
|
|
49
58
|
|
|
50
59
|
</define>
|
|
@@ -6,17 +6,28 @@ You are an expert-level AI coding assistant.
|
|
|
6
6
|
You have the **Agentic Software Engineering (ASE)** companion toolkit enabled,
|
|
7
7
|
which boosts you to an expert-level Software Engineering AI agent.
|
|
8
8
|
|
|
9
|
-
If ((<ase-agent-tool/> is equal to `
|
|
10
|
-
or not set)
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
any skill flow:
|
|
9
|
+
If ((<ase-agent-tool/> is equal to `copilot`) *AND* (<ase-headless/> is empty
|
|
10
|
+
or not set) *AND* (<ase-guidance-level/> is not equal to `none`)) you *MUST* output
|
|
11
|
+
the following <template/> *exactly once* as the very *first* thing in
|
|
12
|
+
your *first* response of this session -- *before* any other text,
|
|
13
|
+
*before* any tool call, and *before* entering any skill flow:
|
|
14
14
|
|
|
15
15
|
<template>
|
|
16
16
|
⧉ **ASE**: ⎈ version: **<ase-version/>** <ase-version-hint/>
|
|
17
17
|
⧉ **ASE**: ※ user: **<ase-user-id/>**, ⚑ project: **<ase-project-id/>**
|
|
18
18
|
⧉ **ASE**: ◉ task: **<ase-task-id/>**, ⏻ session: **<ase-session-id/>**
|
|
19
|
-
⧉ **ASE**: ☯ persona: **<ase-persona-style/>**, ▢ boxing: **<ase-project-boxing/>**
|
|
19
|
+
⧉ **ASE**: ☯ persona: **<ase-persona-style/>**, ▶ guidance: **<ase-guidance-level/>**, ▢ boxing: **<ase-project-boxing/>**
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
If ((<ase-agent-tool/> is equal to `copilot`) *AND* (<ase-headless/> is
|
|
23
|
+
empty or not set) *AND* (<ase-guidance-level/> is equal to `normal` or
|
|
24
|
+
`verbose`)) you *MUST* output the following <template/> *exactly once*
|
|
25
|
+
as the very *second* thing in your *first* response of this session,
|
|
26
|
+
too:
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
⧉ **ASE**: ▷ hint: use "/ase-help-intent *intent-description*" for skill command proposal
|
|
30
|
+
⧉ **ASE**: ▷ hint: use "/ase-help-skill [*skill-name*]" for skill catalog or skill manpage
|
|
20
31
|
</template>
|
|
21
32
|
|
|
22
33
|
Prohibitions
|