@rse/ase 0.9.43 → 0.9.45
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 +65 -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 +12 -10
- 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 +15 -4
- package/plugin/agents/ase-docs-proofread.md +9 -1
- package/plugin/agents/ase-meta-chat.md +19 -13
- package/plugin/agents/ase-meta-diagram.md +1 -1
- package/plugin/agents/ase-meta-search.md +1 -2
- package/plugin/etc/eslint.mjs +0 -2
- package/plugin/meta/ase-common-task.md +145 -0
- package/plugin/meta/ase-constitution.md +5 -2
- package/plugin/meta/ase-control.md +1 -1
- package/plugin/meta/ase-dialog.md +7 -7
- package/plugin/meta/ase-format-arch.md +12 -12
- package/plugin/meta/ase-format-spec.md +23 -23
- package/plugin/meta/ase-format-task.md +9 -11
- package/plugin/meta/ase-getopt.md +1 -1
- package/plugin/meta/ase-persona.md +3 -3
- package/plugin/meta/ase-skill.md +3 -3
- package/plugin/meta/ase-tenets.md +4 -4
- package/plugin/package.json +8 -8
- package/plugin/skills/ase-arch-analyze/SKILL.md +2 -3
- 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 +11 -11
- package/plugin/skills/ase-code-lint/SKILL.md +31 -13
- package/plugin/skills/ase-code-refactor/SKILL.md +13 -13
- package/plugin/skills/ase-code-refactor/help.md +1 -1
- package/plugin/skills/ase-code-resolve/SKILL.md +10 -10
- package/plugin/skills/ase-code-resolve/help.md +1 -1
- package/plugin/skills/ase-docs-distill/SKILL.md +6 -6
- package/plugin/skills/ase-docs-distill/help.md +6 -4
- package/plugin/skills/ase-docs-proofread/SKILL.md +23 -8
- package/plugin/skills/ase-meta-brainstorm/help.md +1 -1
- package/plugin/skills/ase-meta-changelog/SKILL.md +9 -5
- package/plugin/skills/ase-meta-commit/SKILL.md +8 -6
- package/plugin/skills/ase-meta-compat/SKILL.md +14 -5
- package/plugin/skills/ase-meta-diaboli/SKILL.md +6 -6
- package/plugin/skills/ase-meta-diff/SKILL.md +10 -10
- package/plugin/skills/ase-meta-evaluate/SKILL.md +6 -9
- package/plugin/skills/ase-meta-persona/SKILL.md +1 -1
- package/plugin/skills/ase-meta-quorum/SKILL.md +43 -12
- package/plugin/skills/ase-meta-search/SKILL.md +1 -1
- package/plugin/skills/ase-meta-search/help.md +4 -2
- package/plugin/skills/ase-meta-steelman/SKILL.md +5 -5
- package/plugin/skills/ase-meta-steelman/help.md +5 -3
- package/plugin/skills/ase-meta-why/SKILL.md +2 -2
- package/plugin/skills/ase-meta-why/help.md +3 -1
- package/plugin/skills/ase-sync-export/SKILL.md +20 -17
- package/plugin/skills/ase-sync-import/SKILL.md +20 -16
- package/plugin/skills/ase-sync-import/help.md +2 -2
- package/plugin/skills/ase-sync-reconcile/SKILL.md +10 -10
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-condense/SKILL.md +39 -85
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-edit/SKILL.md +43 -51
- package/plugin/skills/ase-task-grill/SKILL.md +11 -13
- package/plugin/skills/ase-task-id/SKILL.md +7 -3
- package/plugin/skills/ase-task-implement/SKILL.md +18 -111
- package/plugin/skills/ase-task-implement/help.md +2 -2
- package/plugin/skills/ase-task-list/SKILL.md +3 -0
- package/plugin/skills/ase-task-preflight/SKILL.md +22 -115
- package/plugin/skills/ase-task-reboot/SKILL.md +39 -71
- package/plugin/skills/ase-task-reboot/help.md +2 -1
- package/plugin/skills/ase-task-view/SKILL.md +7 -7
package/dst/ase-service.js
CHANGED
|
@@ -67,6 +67,23 @@ const TICK_MS = 60 * 1000;
|
|
|
67
67
|
const PORT_MIN = 42000;
|
|
68
68
|
const PORT_MAX = 44000;
|
|
69
69
|
const PORT_TRIES = 20;
|
|
70
|
+
/* load the optional "config.yaml" and "service.yaml" files and derive
|
|
71
|
+
the service identity context (project id, port, service config) */
|
|
72
|
+
export const loadServiceContext = (log) => {
|
|
73
|
+
/* load files */
|
|
74
|
+
const cfg = new Config("config", configSchema, log);
|
|
75
|
+
cfg.read();
|
|
76
|
+
const svc = new Config("service", serviceSchema, log);
|
|
77
|
+
svc.read();
|
|
78
|
+
/* determine project id */
|
|
79
|
+
const projectId = cfg.get("project.id") ?? path.basename(process.cwd());
|
|
80
|
+
/* determine service port */
|
|
81
|
+
const port = svc.get("port") ?? null;
|
|
82
|
+
/* determine path to ".ase" directory */
|
|
83
|
+
const aseDir = path.dirname(svc.filename);
|
|
84
|
+
/* return context information */
|
|
85
|
+
return { projectId, port, svc, aseDir };
|
|
86
|
+
};
|
|
70
87
|
/* reusable functionality: port allocation, persistence, and process spawning */
|
|
71
88
|
export class Service {
|
|
72
89
|
/* try binding a single candidate port to verify availability */
|
|
@@ -135,21 +152,23 @@ export class Service {
|
|
|
135
152
|
fd = fs.openSync(logFile, "r");
|
|
136
153
|
const size = fs.fstatSync(fd).size;
|
|
137
154
|
const CHUNK = 8192;
|
|
138
|
-
const
|
|
155
|
+
const chunks = [];
|
|
139
156
|
let pos = size;
|
|
140
|
-
let tail = "";
|
|
141
157
|
let count = 0;
|
|
142
158
|
while (pos > 0 && count <= lines) {
|
|
143
159
|
const n = Math.min(CHUNK, pos);
|
|
160
|
+
const buf = Buffer.alloc(n);
|
|
144
161
|
pos -= n;
|
|
145
162
|
fs.readSync(fd, buf, 0, n, pos);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (chunk.charCodeAt(i) === 10)
|
|
163
|
+
chunks.unshift(buf);
|
|
164
|
+
for (let i = 0; i < n; i++)
|
|
165
|
+
if (buf[i] === 10)
|
|
150
166
|
count++;
|
|
151
167
|
}
|
|
152
|
-
|
|
168
|
+
/* decode once after re-assembly to avoid splitting
|
|
169
|
+
multi-byte UTF-8 sequences at chunk boundaries */
|
|
170
|
+
const all = Buffer.concat(chunks).toString("utf8")
|
|
171
|
+
.split("\n").filter((l) => l.length > 0);
|
|
153
172
|
return all.slice(-lines).join("\n");
|
|
154
173
|
}
|
|
155
174
|
catch {
|
|
@@ -193,26 +212,7 @@ export default class ServiceCommand {
|
|
|
193
212
|
}
|
|
194
213
|
/* load optional ".ase/config.yaml" and ".ase/service.yaml" files */
|
|
195
214
|
loadContext() {
|
|
196
|
-
|
|
197
|
-
const cfg = new Config("config", configSchema, this.log);
|
|
198
|
-
cfg.read();
|
|
199
|
-
const svc = new Config("service", serviceSchema, this.log);
|
|
200
|
-
svc.read();
|
|
201
|
-
/* determine project id */
|
|
202
|
-
const rawId = cfg.get("project.id");
|
|
203
|
-
const projectId = rawId ?? path.basename(process.cwd());
|
|
204
|
-
/* determine service port */
|
|
205
|
-
const rawPort = svc.get("port");
|
|
206
|
-
const port = rawPort ?? null;
|
|
207
|
-
/* determine path to ".ase" directory */
|
|
208
|
-
const aseDir = path.dirname(svc.filename);
|
|
209
|
-
/* return context information */
|
|
210
|
-
return {
|
|
211
|
-
projectId,
|
|
212
|
-
port,
|
|
213
|
-
svc,
|
|
214
|
-
aseDir
|
|
215
|
-
};
|
|
215
|
+
return loadServiceContext(this.log);
|
|
216
216
|
}
|
|
217
217
|
/* service-side: bind HAPI server until "/stop" command is received or idle timeout happens */
|
|
218
218
|
async runService(ctx) {
|
|
@@ -433,7 +433,7 @@ export default class ServiceCommand {
|
|
|
433
433
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
434
434
|
if (exited)
|
|
435
435
|
break;
|
|
436
|
-
const s = await probe(port, ctx.projectId);
|
|
436
|
+
const s = await probe(port, ctx.projectId).catch(() => null);
|
|
437
437
|
if (s === true) {
|
|
438
438
|
this.log.write("info", `service: started on port ${port}`);
|
|
439
439
|
child.unref();
|
package/dst/ase-setup.js
CHANGED
|
@@ -104,7 +104,7 @@ export default class SetupCommand {
|
|
|
104
104
|
/* run a sub-process, suppressing output on success and emitting it on failure */
|
|
105
105
|
async run(cmd, args, opts = {}) {
|
|
106
106
|
const { cwd, quiet = false, retries = 1, ignoreError } = opts;
|
|
107
|
-
const argsLog = args.map((arg) => arg.replace(/(
|
|
107
|
+
const argsLog = args.map((arg) => arg.replace(/(\w*key=)([^\s&]+)/gi, (_, k, v) => k + "*".repeat(v.length)));
|
|
108
108
|
this.log.write("info", `setup: $ ${cmd} ${argsLog.join(" ")}` +
|
|
109
109
|
(cwd !== undefined ? ` (cwd: ${cwd})` : ""));
|
|
110
110
|
for (let i = 0; i < retries; i++) {
|
|
@@ -182,7 +182,7 @@ export default class SetupCommand {
|
|
|
182
182
|
/* best-effort stop of background service */
|
|
183
183
|
this.log.write("info", `setup: update${dev ? "[dev]" : ""}: ` +
|
|
184
184
|
"stopping potentially running ASE service");
|
|
185
|
-
await this.run("ase", ["service", "stop"], { quiet: true });
|
|
185
|
+
await this.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
|
|
186
186
|
if (dev) {
|
|
187
187
|
/* update ASE CLI Tool */
|
|
188
188
|
this.log.write("info", `setup: update[dev]: used ASE version: ${Version.current()}`);
|
|
@@ -226,33 +226,20 @@ export default class SetupCommand {
|
|
|
226
226
|
}
|
|
227
227
|
return 0;
|
|
228
228
|
}
|
|
229
|
-
/* handler for "ase setup enable" (both tools) */
|
|
230
|
-
async
|
|
229
|
+
/* handler for "ase setup enable|disable" (both tools) */
|
|
230
|
+
async doToggle(tool, scope, action) {
|
|
231
231
|
this.requireClaudeScope(tool, scope);
|
|
232
232
|
const spec = toolSpecs[tool];
|
|
233
233
|
await this.ensureTool(spec.cli);
|
|
234
|
-
this.log.write("info", `setup:
|
|
234
|
+
this.log.write("info", `setup: ${action}: ${action === "enable" ? "enabling" : "disabling"} ` +
|
|
235
|
+
`ASE ${spec.label} plugin`);
|
|
235
236
|
/* the GitHub Copilot CLI and OpenAI Codex CLI have no "plugin
|
|
236
|
-
enable" subcommand, so (re-)install
|
|
237
|
+
enable"/"plugin disable" subcommand, so (re-)install or
|
|
238
|
+
uninstall the plugin instead */
|
|
237
239
|
const scopeArgs = scope !== "user" ? ["--scope", scope] : [];
|
|
238
240
|
const args = tool === "claude" ?
|
|
239
|
-
["plugin",
|
|
240
|
-
["plugin", spec.pInstall, "ase@ase"];
|
|
241
|
-
await this.run(spec.cli, args, { retries: tool === "claude" ? 1 : 3 });
|
|
242
|
-
return 0;
|
|
243
|
-
}
|
|
244
|
-
/* handler for "ase setup disable" (both tools) */
|
|
245
|
-
async doDisable(tool, scope) {
|
|
246
|
-
this.requireClaudeScope(tool, scope);
|
|
247
|
-
const spec = toolSpecs[tool];
|
|
248
|
-
await this.ensureTool(spec.cli);
|
|
249
|
-
this.log.write("info", `setup: disable: disabling ASE ${spec.label} plugin`);
|
|
250
|
-
/* the GitHub Copilot CLI and OpenAI Codex CLI have no "plugin
|
|
251
|
-
disable" subcommand, so uninstall the plugin instead */
|
|
252
|
-
const scopeArgs = scope !== "user" ? ["--scope", scope] : [];
|
|
253
|
-
const args = tool === "claude" ?
|
|
254
|
-
["plugin", "disable", "ase@ase", ...scopeArgs] :
|
|
255
|
-
["plugin", spec.pRemove, "ase@ase"];
|
|
241
|
+
["plugin", action, "ase@ase", ...scopeArgs] :
|
|
242
|
+
["plugin", action === "enable" ? spec.pInstall : spec.pRemove, "ase@ase"];
|
|
256
243
|
await this.run(spec.cli, args, { retries: tool === "claude" ? 1 : 3 });
|
|
257
244
|
return 0;
|
|
258
245
|
}
|
|
@@ -266,7 +253,7 @@ export default class SetupCommand {
|
|
|
266
253
|
/* best-effort stop of background service */
|
|
267
254
|
this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
|
|
268
255
|
"stopping potentially running ASE service");
|
|
269
|
-
await this.run("ase", ["service", "stop"], { quiet: true });
|
|
256
|
+
await this.run("ase", ["service", "stop"], { quiet: true, ignoreError: "ASE service not running" });
|
|
270
257
|
/* uninstall ASE plugin */
|
|
271
258
|
this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
|
|
272
259
|
`uninstalling ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote/bundled"})`);
|
|
@@ -466,6 +453,29 @@ export default class SetupCommand {
|
|
|
466
453
|
await this.mcpRemove(tool, spec.server, scope);
|
|
467
454
|
};
|
|
468
455
|
}
|
|
456
|
+
/* build a chat-model MCP handler which bridges to a locally
|
|
457
|
+
installed AI agent harness CLI via the mcp-to-harness stdio
|
|
458
|
+
scaffold; the API key environment variable carries no key at all,
|
|
459
|
+
but instead the harness model identifier, with the special value
|
|
460
|
+
"default" selecting the default model of the harness */
|
|
461
|
+
harnessMcpHandler(harness) {
|
|
462
|
+
return async (spec, tool, scope, action, _envKey, envVal) => {
|
|
463
|
+
if (action === "activate")
|
|
464
|
+
await this.mcpAdd(tool, spec.server, {}, {
|
|
465
|
+
type: "stdio", command: [
|
|
466
|
+
"npx", "-y", "mcp-to-harness",
|
|
467
|
+
"--service", spec.name,
|
|
468
|
+
"--mcp-tool", "query",
|
|
469
|
+
"--harness", harness,
|
|
470
|
+
...(envVal !== "default" ? [
|
|
471
|
+
"--harness-model", envVal
|
|
472
|
+
] : [])
|
|
473
|
+
]
|
|
474
|
+
}, scope);
|
|
475
|
+
else
|
|
476
|
+
await this.mcpRemove(tool, spec.server, scope);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
469
479
|
/* registry of pre-defined MCP servers: maps each server id onto its
|
|
470
480
|
dedicated handler which performs the activate/deactivate operation */
|
|
471
481
|
mcpServers = [
|
|
@@ -523,6 +533,33 @@ export default class SetupCommand {
|
|
|
523
533
|
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
524
534
|
handler: this.chatMcpHandler({ url: "https://api.z.ai/api/paas/v4/", api: "completion", model: "glm-5.1" }, { model: "z-ai/glm-5.1" })
|
|
525
535
|
},
|
|
536
|
+
{
|
|
537
|
+
id: "anthropic-claude",
|
|
538
|
+
name: "Anthropic Claude",
|
|
539
|
+
version: "latest",
|
|
540
|
+
env: ["ANTHROPIC_CLAUDE"],
|
|
541
|
+
server: "chat-anthropic-claude",
|
|
542
|
+
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
543
|
+
handler: this.harnessMcpHandler("claude")
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "openai-codex",
|
|
547
|
+
name: "OpenAI Codex",
|
|
548
|
+
version: "latest",
|
|
549
|
+
env: ["OPENAI_CODEX"],
|
|
550
|
+
server: "chat-openai-codex",
|
|
551
|
+
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
552
|
+
handler: this.harnessMcpHandler("codex")
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: "github-copilot",
|
|
556
|
+
name: "GitHub Copilot",
|
|
557
|
+
version: "latest",
|
|
558
|
+
env: ["GITHUB_COPILOT"],
|
|
559
|
+
server: "chat-github-copilot",
|
|
560
|
+
skills: ["ase-meta-chat", "ase-meta-quorum"],
|
|
561
|
+
handler: this.harnessMcpHandler("copilot")
|
|
562
|
+
},
|
|
526
563
|
{
|
|
527
564
|
id: "brave",
|
|
528
565
|
name: "Brave",
|
|
@@ -565,7 +602,7 @@ export default class SetupCommand {
|
|
|
565
602
|
skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
|
|
566
603
|
handler: async (spec, tool, scope, action, _envKey, envVal) => {
|
|
567
604
|
if (action === "activate")
|
|
568
|
-
await this.mcpAdd(tool, spec.server, {}, { type: "http", url: `https://mcp.exa.ai/mcp?exaApiKey=${envVal}` }, scope);
|
|
605
|
+
await this.mcpAdd(tool, spec.server, {}, { type: "http", url: `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(envVal)}` }, scope);
|
|
569
606
|
else
|
|
570
607
|
await this.mcpRemove(tool, spec.server, scope);
|
|
571
608
|
}
|
|
@@ -699,7 +736,7 @@ export default class SetupCommand {
|
|
|
699
736
|
async statuslineWriteAst(file, root) {
|
|
700
737
|
await mkdirp(path.dirname(file));
|
|
701
738
|
const text = JsonAsty.unparse(root);
|
|
702
|
-
writeFileAtomic
|
|
739
|
+
await writeFileAtomic(file, text, { encoding: "utf8" });
|
|
703
740
|
}
|
|
704
741
|
/* handler for "ase setup statusline activate" */
|
|
705
742
|
async doStatuslineActivate(tool, scope, opts, format) {
|
|
@@ -863,7 +900,7 @@ export default class SetupCommand {
|
|
|
863
900
|
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
864
901
|
.option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
|
|
865
902
|
.action(async (opts) => {
|
|
866
|
-
process.exit(await this.
|
|
903
|
+
process.exit(await this.doToggle(this.parseTool(opts.tool), this.parseScope(opts.scope), "enable"));
|
|
867
904
|
});
|
|
868
905
|
/* register CLI sub-command "ase setup disable" */
|
|
869
906
|
setupCmd
|
|
@@ -872,7 +909,7 @@ export default class SetupCommand {
|
|
|
872
909
|
.option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
|
|
873
910
|
.option("-s, --scope <scope>", "target scope (\"user\", \"project\", or \"local\")", "user")
|
|
874
911
|
.action(async (opts) => {
|
|
875
|
-
process.exit(await this.
|
|
912
|
+
process.exit(await this.doToggle(this.parseTool(opts.tool), this.parseScope(opts.scope), "disable"));
|
|
876
913
|
});
|
|
877
914
|
/* register CLI sub-command "ase setup mcp" */
|
|
878
915
|
const mcpCmd = setupCmd
|
package/dst/ase-skills.js
CHANGED
|
@@ -16,7 +16,7 @@ export class Skills {
|
|
|
16
16
|
static httpActive = 0;
|
|
17
17
|
static httpQueue = [];
|
|
18
18
|
static async httpLimit(fn) {
|
|
19
|
-
|
|
19
|
+
while (Skills.httpActive >= Skills.HTTP_CONCURRENCY)
|
|
20
20
|
await new Promise((resolve) => Skills.httpQueue.push(resolve));
|
|
21
21
|
Skills.httpActive++;
|
|
22
22
|
try {
|
|
@@ -295,7 +295,7 @@ export class Skills {
|
|
|
295
295
|
We therefore default a missing `recentness` to the decay value at
|
|
296
296
|
one half-life (`0.5`), a conservative midpoint that keeps the
|
|
297
297
|
entry rankable without rewarding the missing date. */
|
|
298
|
-
const lifespan = (!Number.isNaN(cMs) && !Number.isNaN(uMs)) ? Math.max(
|
|
298
|
+
const lifespan = (!Number.isNaN(cMs) && !Number.isNaN(uMs)) ? Math.max(1, uMs - cMs) : 1;
|
|
299
299
|
const recentness = !Number.isNaN(uMs) ? Math.exp(-Math.max(0, (now - uMs) / msPerDay) / halfLife) : 0.5;
|
|
300
300
|
let rank = d * s * lifespan * recentness;
|
|
301
301
|
/* hard, caller-tunable staleness penalty on top of the soft
|
package/dst/ase-statusline.js
CHANGED
|
@@ -11,6 +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-stdout.js";
|
|
14
15
|
import pkg from "../package.json" with { type: "json" };
|
|
15
16
|
/* forced-color chalk instance: stdout is a pipe under Anthropic Claude Code CLI,
|
|
16
17
|
so chalk auto-detection would yield level 0; force level 1 to keep
|
|
@@ -58,18 +59,22 @@ const detectTermWidth = () => {
|
|
|
58
59
|
}
|
|
59
60
|
return width;
|
|
60
61
|
};
|
|
61
|
-
/* format a
|
|
62
|
-
const
|
|
62
|
+
/* format a count as a compact human-readable string with the given scaling base */
|
|
63
|
+
const formatScaled = (n, base) => {
|
|
63
64
|
if (!Number.isFinite(n) || n < 0)
|
|
64
65
|
return "0";
|
|
65
|
-
if (n >=
|
|
66
|
-
return `${(n /
|
|
67
|
-
if (n >=
|
|
68
|
-
return `${(n /
|
|
69
|
-
if (n >=
|
|
70
|
-
return `${(n /
|
|
66
|
+
if (n >= base ** 3)
|
|
67
|
+
return `${(n / base ** 3).toFixed(1)}G`;
|
|
68
|
+
if (n >= base ** 2)
|
|
69
|
+
return `${(n / base ** 2).toFixed(1)}M`;
|
|
70
|
+
if (n >= base)
|
|
71
|
+
return `${(n / base).toFixed(1)}k`;
|
|
71
72
|
return `${n}`;
|
|
72
73
|
};
|
|
74
|
+
/* format a token count as a compact human-readable string (e.g. 334k, 104.9M) */
|
|
75
|
+
const formatTokens = (n) => formatScaled(n, 1000);
|
|
76
|
+
/* format a byte count as a compact human-readable string (e.g. 33.2G, 512M) */
|
|
77
|
+
const formatBytes = (n) => formatScaled(n, 1024);
|
|
73
78
|
/* format a millisecond duration as a compact human-readable string (e.g. 6d 12hr 7m, 4hr 27m, 12m 30s) */
|
|
74
79
|
const formatDurationMs = (ms) => {
|
|
75
80
|
if (!Number.isFinite(ms) || ms < 0)
|
|
@@ -122,33 +127,18 @@ const formatCostUsd = (n) => {
|
|
|
122
127
|
return "$0.00";
|
|
123
128
|
return `$${n.toFixed(2)}`;
|
|
124
129
|
};
|
|
125
|
-
/* format a byte count as a compact human-readable string (e.g. 33.2G, 512M) */
|
|
126
|
-
const formatBytes = (n) => {
|
|
127
|
-
if (!Number.isFinite(n) || n < 0)
|
|
128
|
-
return "0";
|
|
129
|
-
if (n >= 1024 ** 3)
|
|
130
|
-
return `${(n / 1024 ** 3).toFixed(1)}G`;
|
|
131
|
-
if (n >= 1024 ** 2)
|
|
132
|
-
return `${(n / 1024 ** 2).toFixed(1)}M`;
|
|
133
|
-
if (n >= 1024)
|
|
134
|
-
return `${(n / 1024).toFixed(1)}k`;
|
|
135
|
-
return `${n}`;
|
|
136
|
-
};
|
|
137
130
|
/* probe local git status for the given working directory */
|
|
138
131
|
const probeGit = (cwd) => {
|
|
132
|
+
const gitCmd = (...args) => execFileSync("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "ignore"], timeout: 1000 }).toString("utf8");
|
|
139
133
|
try {
|
|
140
|
-
const branch =
|
|
141
|
-
|
|
142
|
-
const porc = execFileSync("git", ["-C", cwd, "status", "--porcelain"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1000 })
|
|
143
|
-
.toString("utf8");
|
|
134
|
+
const branch = gitCmd("rev-parse", "--abbrev-ref", "HEAD").trim();
|
|
135
|
+
const porc = gitCmd("status", "--porcelain");
|
|
144
136
|
const lines = porc.split("\n").filter((l) => l.length > 0);
|
|
145
137
|
const untracked = lines.filter((l) => l.startsWith("??")).length;
|
|
146
|
-
const dirty = lines.length > 0;
|
|
147
138
|
let added = 0;
|
|
148
139
|
let removed = 0;
|
|
149
140
|
try {
|
|
150
|
-
const shortstat =
|
|
151
|
-
.toString("utf8");
|
|
141
|
+
const shortstat = gitCmd("diff", "--shortstat", "HEAD");
|
|
152
142
|
const mAdd = shortstat.match(/(\d+)\s+insertion/);
|
|
153
143
|
const mDel = shortstat.match(/(\d+)\s+deletion/);
|
|
154
144
|
if (mAdd !== null)
|
|
@@ -159,10 +149,10 @@ const probeGit = (cwd) => {
|
|
|
159
149
|
catch (_e) {
|
|
160
150
|
/* no HEAD yet or git failure; leave counts at 0 */
|
|
161
151
|
}
|
|
162
|
-
return { branch,
|
|
152
|
+
return { branch, untracked, added, removed };
|
|
163
153
|
}
|
|
164
154
|
catch (_e) {
|
|
165
|
-
return { branch: "",
|
|
155
|
+
return { branch: "", untracked: 0, added: 0, removed: 0 };
|
|
166
156
|
}
|
|
167
157
|
};
|
|
168
158
|
/* probe local memory usage in bytes (used/total) using OS-portable helpers */
|
|
@@ -190,9 +180,11 @@ export default class StatuslineCommand {
|
|
|
190
180
|
}
|
|
191
181
|
/* register commands */
|
|
192
182
|
register(program) {
|
|
193
|
-
/* default for --tool derived from ASE_TOOL environment variable
|
|
183
|
+
/* default for --tool derived from ASE_TOOL environment variable
|
|
184
|
+
(validated lazily by "parseTool" in the action, so an invalid
|
|
185
|
+
value cannot break unrelated "ase" commands at startup) */
|
|
194
186
|
const envTool = process.env.ASE_TOOL ?? "";
|
|
195
|
-
const toolDflt = envTool !== "" ?
|
|
187
|
+
const toolDflt = envTool !== "" ? envTool : "claude";
|
|
196
188
|
program
|
|
197
189
|
.command("statusline")
|
|
198
190
|
.description("Render Anthropic Claude Code CLI or GitHub Copilot CLI statusline from stdin JSON")
|
|
@@ -339,7 +331,7 @@ export default class StatuslineCommand {
|
|
|
339
331
|
emit(`${prefix("⚒", "effort")}${c.bold(effort)}`);
|
|
340
332
|
},
|
|
341
333
|
t: () => {
|
|
342
|
-
const thinking =
|
|
334
|
+
const thinking = data.thinking?.enabled === true ? "yes" : "no";
|
|
343
335
|
emit(`${prefix("⚛", "thinking")}${c.bold(thinking)}`);
|
|
344
336
|
},
|
|
345
337
|
/* ==== OUTPUT ==== */
|
|
@@ -355,7 +347,7 @@ export default class StatuslineCommand {
|
|
|
355
347
|
},
|
|
356
348
|
/* ==== CONTEXT ==== */
|
|
357
349
|
c: () => {
|
|
358
|
-
const pct = Math.floor(data.context_window?.used_percentage ?? 0);
|
|
350
|
+
const pct = Math.min(100, Math.max(0, Math.floor(data.context_window?.used_percentage ?? 0)));
|
|
359
351
|
const barSize = 20;
|
|
360
352
|
const filled = Math.round(pct / 100 * barSize);
|
|
361
353
|
const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
|
|
@@ -485,7 +477,7 @@ export default class StatuslineCommand {
|
|
|
485
477
|
col = 0;
|
|
486
478
|
}
|
|
487
479
|
/* send output */
|
|
488
|
-
|
|
480
|
+
await writeStdout(out);
|
|
489
481
|
/* optionally publish task id to the calling tmux pane as a per-pane user
|
|
490
482
|
option, so something (like claudeX) can pick it up via #{@ase_task_id} */
|
|
491
483
|
if (process.env.TMUX !== undefined
|
package/dst/ase-task.js
CHANGED
|
@@ -10,6 +10,7 @@ import { DateTime } from "luxon";
|
|
|
10
10
|
import picomatch from "picomatch";
|
|
11
11
|
import { isScalar } from "yaml";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
+
import { LRUCache } from "lru-cache";
|
|
13
14
|
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
14
15
|
import { Markdown } from "./ase-markdown.js";
|
|
15
16
|
import { writeStdout } from "./ase-stdout.js";
|
|
@@ -24,19 +25,36 @@ export class Task {
|
|
|
24
25
|
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
25
26
|
throw new Error("task: id must match [A-Za-z0-9_-]+");
|
|
26
27
|
}
|
|
28
|
+
/* validate the session id to keep it safe as a config scope term */
|
|
29
|
+
static validateSession(session) {
|
|
30
|
+
if (typeof session !== "string" || session.length === 0)
|
|
31
|
+
throw new Error("task: session must be a non-empty string");
|
|
32
|
+
if (!/^[A-Za-z0-9_-]+$/.test(session))
|
|
33
|
+
throw new Error("task: session must match [A-Za-z0-9_-]+");
|
|
34
|
+
}
|
|
35
|
+
/* cached project root determination (TTL-bounded, as the ASE
|
|
36
|
+
service is long-running and the Git context can change) */
|
|
37
|
+
static projectRootCache = new LRUCache({ max: 4, ttl: 10 * 1000 });
|
|
27
38
|
/* determine the project root (Git top-level if inside a Git
|
|
28
|
-
working tree, otherwise the current working directory)
|
|
39
|
+
working tree, otherwise the current working directory);
|
|
40
|
+
cached, as each determination spawns a Git subprocess */
|
|
29
41
|
static projectRoot() {
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
const cached = Task.projectRootCache.get(cwd);
|
|
44
|
+
if (cached !== undefined)
|
|
45
|
+
return cached;
|
|
46
|
+
let root = cwd;
|
|
30
47
|
try {
|
|
31
48
|
const result = execaSync("git", ["rev-parse", "--show-toplevel"], { stderr: "ignore" });
|
|
32
49
|
const top = result.stdout.trim();
|
|
33
50
|
if (top !== "")
|
|
34
|
-
|
|
51
|
+
root = top;
|
|
35
52
|
}
|
|
36
53
|
catch {
|
|
37
54
|
/* not inside a Git working tree */
|
|
38
55
|
}
|
|
39
|
-
|
|
56
|
+
Task.projectRootCache.set(cwd, root);
|
|
57
|
+
return root;
|
|
40
58
|
}
|
|
41
59
|
/* read the configured "basedir" anchor and "files" miniglob spec for
|
|
42
60
|
task storage; "basedir" is project-root-relative (POSIX, defaults
|
|
@@ -166,10 +184,9 @@ export class Task {
|
|
|
166
184
|
fs.rmSync(oldFile, { force: true });
|
|
167
185
|
return true;
|
|
168
186
|
}
|
|
169
|
-
/*
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
static list(log, verbose = false) {
|
|
187
|
+
/* scan the task base directory (after eager migration) for
|
|
188
|
+
"TASK-<id>.md" files matching the configured "files" miniglob */
|
|
189
|
+
static scan(log) {
|
|
173
190
|
if (Task.needsMigration(log))
|
|
174
191
|
Task.migrateAll(log);
|
|
175
192
|
const { basedir, files } = Task.spec(log);
|
|
@@ -186,41 +203,37 @@ export class Task {
|
|
|
186
203
|
const st = fs.statSync(file);
|
|
187
204
|
if (!st.isFile())
|
|
188
205
|
continue;
|
|
189
|
-
|
|
190
|
-
out.push({ id: m[1], mtime });
|
|
206
|
+
out.push({ id: m[1], file, st });
|
|
191
207
|
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/* list all persisted tasks in lexicographic id order; if verbose is true,
|
|
211
|
+
each entry's `mtime` is set to the task file's modification time
|
|
212
|
+
formatted as "YYYY-MM-DD HH:MM", otherwise it is left undefined */
|
|
213
|
+
static list(log, verbose = false) {
|
|
214
|
+
const out = Task.scan(log).map((entry) => ({
|
|
215
|
+
id: entry.id,
|
|
216
|
+
mtime: verbose ? DateTime.fromJSDate(entry.st.mtime).toFormat("yyyy-LL-dd HH:mm") : undefined
|
|
217
|
+
}));
|
|
192
218
|
out.sort((a, b) => a.id.localeCompare(b.id));
|
|
193
219
|
return out;
|
|
194
220
|
}
|
|
195
221
|
/* purge tasks whose modification time is older than the given cutoff in
|
|
196
222
|
milliseconds; returns the list of removed task ids */
|
|
197
223
|
static purge(log, maxAgeMs) {
|
|
198
|
-
if (Task.needsMigration(log))
|
|
199
|
-
Task.migrateAll(log);
|
|
200
|
-
const { basedir, files } = Task.spec(log);
|
|
201
|
-
const dir = path.join(Task.projectRoot(), basedir);
|
|
202
|
-
if (!fs.existsSync(dir))
|
|
203
|
-
return [];
|
|
204
|
-
const isMatch = picomatch(files, { dot: true });
|
|
205
224
|
const cutoff = Date.now() - maxAgeMs;
|
|
206
225
|
const removed = [];
|
|
207
|
-
for (const entry of
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const file = path.join(dir, entry);
|
|
212
|
-
const st = fs.statSync(file);
|
|
213
|
-
if (!st.isFile())
|
|
214
|
-
continue;
|
|
215
|
-
if (st.mtimeMs < cutoff) {
|
|
216
|
-
fs.rmSync(file, { force: true });
|
|
217
|
-
removed.push(m[1]);
|
|
226
|
+
for (const entry of Task.scan(log)) {
|
|
227
|
+
if (entry.st.mtimeMs < cutoff) {
|
|
228
|
+
fs.rmSync(entry.file, { force: true });
|
|
229
|
+
removed.push(entry.id);
|
|
218
230
|
}
|
|
219
231
|
}
|
|
220
232
|
return removed;
|
|
221
233
|
}
|
|
222
234
|
/* get the active task id for a given session, or empty string if none */
|
|
223
235
|
static getId(log, session) {
|
|
236
|
+
Task.validateSession(session);
|
|
224
237
|
const scope = parseScope(`session:${session}`);
|
|
225
238
|
const cfg = new Config("config", configSchema, log, scope);
|
|
226
239
|
cfg.read();
|
|
@@ -231,6 +244,7 @@ export class Task {
|
|
|
231
244
|
}
|
|
232
245
|
/* set the active task id for a given session */
|
|
233
246
|
static setId(log, session, id) {
|
|
247
|
+
Task.validateSession(session);
|
|
234
248
|
const scope = parseScope(`session:${session}`);
|
|
235
249
|
const cfg = new Config("config", configSchema, log, scope);
|
|
236
250
|
cfg.lock(() => {
|
|
@@ -357,10 +371,8 @@ export default class TaskCommand {
|
|
|
357
371
|
const day = 24 * hour;
|
|
358
372
|
const month = 30 * day;
|
|
359
373
|
const year = 365 * day;
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
unit === "m" ? month :
|
|
363
|
-
year;
|
|
374
|
+
const factors = { h: hour, d: day, m: month, y: year };
|
|
375
|
+
const factor = factors[unit];
|
|
364
376
|
const removed = Task.purge(this.log, n * factor);
|
|
365
377
|
if (removed.length === 0)
|
|
366
378
|
this.log.write("info", "task: no tasks to purge");
|
|
@@ -371,6 +383,14 @@ export default class TaskCommand {
|
|
|
371
383
|
});
|
|
372
384
|
}
|
|
373
385
|
}
|
|
386
|
+
/* render a caught error as an MCP tool error result */
|
|
387
|
+
const mcpError = (err) => {
|
|
388
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
389
|
+
return {
|
|
390
|
+
isError: true,
|
|
391
|
+
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
392
|
+
};
|
|
393
|
+
};
|
|
374
394
|
/* MCP registration entry point for task tools */
|
|
375
395
|
export class TaskMCP {
|
|
376
396
|
log;
|
|
@@ -412,11 +432,7 @@ export class TaskMCP {
|
|
|
412
432
|
};
|
|
413
433
|
}
|
|
414
434
|
catch (err) {
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
isError: true,
|
|
418
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
419
|
-
};
|
|
435
|
+
return mcpError(err);
|
|
420
436
|
}
|
|
421
437
|
});
|
|
422
438
|
/* task load */
|
|
@@ -437,11 +453,7 @@ export class TaskMCP {
|
|
|
437
453
|
};
|
|
438
454
|
}
|
|
439
455
|
catch (err) {
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
isError: true,
|
|
443
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
444
|
-
};
|
|
456
|
+
return mcpError(err);
|
|
445
457
|
}
|
|
446
458
|
});
|
|
447
459
|
/* task save */
|
|
@@ -463,11 +475,7 @@ export class TaskMCP {
|
|
|
463
475
|
};
|
|
464
476
|
}
|
|
465
477
|
catch (err) {
|
|
466
|
-
|
|
467
|
-
return {
|
|
468
|
-
isError: true,
|
|
469
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
470
|
-
};
|
|
478
|
+
return mcpError(err);
|
|
471
479
|
}
|
|
472
480
|
});
|
|
473
481
|
/* task delete */
|
|
@@ -490,18 +498,15 @@ export class TaskMCP {
|
|
|
490
498
|
};
|
|
491
499
|
}
|
|
492
500
|
catch (err) {
|
|
493
|
-
|
|
494
|
-
return {
|
|
495
|
-
isError: true,
|
|
496
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
497
|
-
};
|
|
501
|
+
return mcpError(err);
|
|
498
502
|
}
|
|
499
503
|
});
|
|
500
504
|
/* task rename */
|
|
501
505
|
mcp.registerTool("ase_task_rename", {
|
|
502
506
|
title: "ASE task rename",
|
|
503
|
-
description: "Rename a previously persisted task from `old` to `new` by
|
|
504
|
-
"task `TASK-<id>.md` file
|
|
507
|
+
description: "Rename a previously persisted task from `old` to `new` by moving the " +
|
|
508
|
+
"task `TASK-<id>.md` file and rewriting its embedded task heading. " +
|
|
509
|
+
"Returns a status `text` indicating whether the rename succeeded. " +
|
|
505
510
|
"Fails with an error if the target id already exists.",
|
|
506
511
|
inputSchema: {
|
|
507
512
|
old: z.string()
|
|
@@ -520,11 +525,7 @@ export class TaskMCP {
|
|
|
520
525
|
};
|
|
521
526
|
}
|
|
522
527
|
catch (err) {
|
|
523
|
-
|
|
524
|
-
return {
|
|
525
|
-
isError: true,
|
|
526
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
527
|
-
};
|
|
528
|
+
return mcpError(err);
|
|
528
529
|
}
|
|
529
530
|
});
|
|
530
531
|
/* task id get/set */
|
|
@@ -556,11 +557,7 @@ export class TaskMCP {
|
|
|
556
557
|
};
|
|
557
558
|
}
|
|
558
559
|
catch (err) {
|
|
559
|
-
|
|
560
|
-
return {
|
|
561
|
-
isError: true,
|
|
562
|
-
content: [{ type: "text", text: `ERROR: ${message}` }]
|
|
563
|
-
};
|
|
560
|
+
return mcpError(err);
|
|
564
561
|
}
|
|
565
562
|
});
|
|
566
563
|
}
|
package/dst/ase-version.js
CHANGED
|
@@ -11,7 +11,8 @@ export default class Version {
|
|
|
11
11
|
static current() {
|
|
12
12
|
return pkg.version;
|
|
13
13
|
}
|
|
14
|
-
/* return latest ASE version
|
|
14
|
+
/* return latest ASE version known from the NPM registry
|
|
15
|
+
(cached background check, falling back to the current version) */
|
|
15
16
|
static async latest() {
|
|
16
17
|
const notifier = updateNotifier({
|
|
17
18
|
pkg,
|