@rse/ase 0.0.50 → 0.0.52

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-setup.js CHANGED
@@ -8,6 +8,9 @@ import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { execa } from "execa";
10
10
  import which from "which";
11
+ import * as dotenvx from "@dotenvx/dotenvx";
12
+ import Table from "cli-table3";
13
+ import chalk from "chalk";
11
14
  import Version from "./ase-version.js";
12
15
  const toolSpecs = {
13
16
  "claude": { cli: "claude", label: "Claude Code" },
@@ -96,7 +99,8 @@ export default class SetupCommand {
96
99
  /* run a sub-process, suppressing output on success and emitting it on failure */
97
100
  async run(cmd, args, opts = {}) {
98
101
  const { cwd, quiet = false, retries = 1, ignoreError } = opts;
99
- this.log.write("info", `setup: $ ${cmd} ${args.join(" ")}` +
102
+ const argsLog = args.map((arg) => arg.replace(/(_KEY=)(\S+)/, (_, k, v) => k + "*".repeat(v.length)));
103
+ this.log.write("info", `setup: $ ${cmd} ${argsLog.join(" ")}` +
100
104
  (cwd !== undefined ? ` (cwd: ${cwd})` : ""));
101
105
  for (let i = 0; i < retries; i++) {
102
106
  const final = (i === retries - 1);
@@ -240,6 +244,416 @@ export default class SetupCommand {
240
244
  }
241
245
  return 0;
242
246
  }
247
+ /* handler for "ase setup mcp list" */
248
+ async doMcpList() {
249
+ const table = new Table({
250
+ head: ["ID", "NAME", "VERS", "MCP", "KEY", "SKILLS"],
251
+ colWidths: [16, 16, 8, 21, 17, 20],
252
+ wordWrap: true,
253
+ chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
254
+ style: { head: ["blue"] }
255
+ });
256
+ for (const handle of this.mcpServers)
257
+ table.push([
258
+ chalk.bold(handle.id),
259
+ handle.name,
260
+ handle.version ?? "(unknown)",
261
+ handle.server,
262
+ handle.env.join(", "),
263
+ handle.skills.join(", ")
264
+ ]);
265
+ process.stdout.write(`${table.toString()}\n`);
266
+ return 0;
267
+ }
268
+ /* handler for "ase setup mcp activate|deactivate [<servers>]" */
269
+ async doMcp(action, tool, servers) {
270
+ await this.ensureTool(toolSpecs[tool].cli);
271
+ /* source .env files into the environment so the per-server
272
+ API keys (ASE_MCP_KEY_<XXX>) can live in a .env file instead
273
+ of the exported interactive shell environment */
274
+ dotenvx.config({ quiet: true, ignore: ["MISSING_ENV_FILE"] });
275
+ /* resolve the comma-separated list of server ids, with an empty
276
+ list or the literal "all" expanding to every registered server
277
+ id; track whether the ids were explicitly given on the CLI */
278
+ const known = this.mcpServers.map((handle) => handle.id);
279
+ const explicit = servers.trim() !== "" && servers.trim() !== "all";
280
+ const ids = explicit ?
281
+ servers.split(",").map((s) => s.trim()).filter((s) => s !== "") : known;
282
+ for (const id of ids)
283
+ if (this.mcpServers.find((handle) => handle.id === id) === undefined)
284
+ throw new Error(`unknown MCP server "${id}" ` +
285
+ `(known: ${known.join(", ")})`);
286
+ /* dispatch each selected server to its dedicated handler */
287
+ for (const id of ids) {
288
+ /* find handle */
289
+ const handle = this.mcpServers.find((handle) => handle.id === id);
290
+ /* determine information and action */
291
+ let envKey = "";
292
+ let envVal = "";
293
+ if (action === "activate") {
294
+ /* on activation, require at least one of the per-server API
295
+ key environment variables (ASE_MCP_KEY_<XXX>) to
296
+ be set; skip the server when its id was only
297
+ implicitly selected (empty list or "all"), but fail
298
+ hard when it was given explicitly on the CLI */
299
+ envKey = handle.env.find((name) => (process.env[`ASE_MCP_KEY_${name}`] ?? "") !== "") ?? "";
300
+ if (envKey === "") {
301
+ const vars = handle.env.map((name) => `ASE_MCP_KEY_${name}`).join(", ");
302
+ if (explicit)
303
+ throw new Error(`none of ${vars} set: ` +
304
+ `cannot activate MCP server "${handle.server}"`);
305
+ this.log.write("info", `setup: mcp: activate: [${id}]: none of ${vars} set: ` +
306
+ `skipping MCP server "${handle.server}" (${handle.name})`);
307
+ continue;
308
+ }
309
+ envVal = process.env[`ASE_MCP_KEY_${envKey}`] ?? "";
310
+ }
311
+ /* probe whether the MCP server is currently registered with the tool */
312
+ const installed = await this.mcpInstalled(tool, handle.server);
313
+ if (action === "activate") {
314
+ /* on activation, remove a stale registration first so the
315
+ handler can re-create it cleanly */
316
+ if (installed) {
317
+ this.log.write("info", `setup: mcp: activate: [${id}]: MCP server "${handle.server}" ` +
318
+ "already registered: removing stale registration first");
319
+ await this.mcpRemove(tool, handle.server);
320
+ }
321
+ }
322
+ else if (!installed) {
323
+ /* on deactivation, skip the removal of an absent server */
324
+ this.log.write("info", `setup: mcp: deactivate: [${id}]: MCP server "${handle.server}" ` +
325
+ "not registered: skipping removal");
326
+ continue;
327
+ }
328
+ /* call the handler */
329
+ this.log.write("info", `setup: mcp: ${action}: [${id}]: MCP server "${handle.server}" ` +
330
+ `(name: ${handle.name}${handle.version ? (", version: " + handle.version) : ""})`);
331
+ await handle.handler(handle, tool, action, envKey, envVal);
332
+ }
333
+ return 0;
334
+ }
335
+ /* probe whether an MCP server is currently registered with the tool
336
+ by inspecting the exit code of "<cli> mcp get <name>" */
337
+ async mcpInstalled(tool, name) {
338
+ const result = await execa(toolSpecs[tool].cli, ["mcp", "get", name], { stdio: "ignore", reject: false });
339
+ return result.exitCode === 0;
340
+ }
341
+ /* register an MCP server with the tool, supporting both the "stdio"
342
+ (a local subprocess command) and "http" (a remote URL, optionally
343
+ with HTTP headers) transports; the per-tool command line differs
344
+ between Claude Code and GitHub Copilot CLI */
345
+ async mcpAdd(tool, name, env, transport) {
346
+ const args = ["mcp", "add"];
347
+ if (tool === "claude") {
348
+ args.push("--scope", "user");
349
+ args.push("--transport", transport.type);
350
+ if (transport.type === "stdio") {
351
+ for (const [key, val] of Object.entries(env))
352
+ args.push("-e", `${key}=${val}`);
353
+ args.push("--", name, ...transport.command);
354
+ }
355
+ else {
356
+ for (const [key, val] of Object.entries(transport.headers ?? {}))
357
+ args.push("--header", `${key}: ${val}`);
358
+ args.push(name, transport.url);
359
+ }
360
+ }
361
+ else {
362
+ /* GitHub Copilot CLI implies the stdio transport when the
363
+ command is provided after "--"; only "http"/"sse" servers
364
+ need an explicit "--transport" flag and take the URL as a
365
+ positional argument */
366
+ if (transport.type === "stdio") {
367
+ args.push(name);
368
+ for (const [key, val] of Object.entries(env))
369
+ args.push("--env", `${key}=${val}`);
370
+ args.push("--", ...transport.command);
371
+ }
372
+ else {
373
+ args.push("--transport", "http");
374
+ for (const [key, val] of Object.entries(transport.headers ?? {}))
375
+ args.push("--header", `${key}: ${val}`);
376
+ args.push(name, transport.url);
377
+ }
378
+ }
379
+ await this.run(toolSpecs[tool].cli, args);
380
+ }
381
+ /* unregister an MCP server from the tool; the per-tool command line
382
+ differs between Claude Code and GitHub Copilot CLI */
383
+ async mcpRemove(tool, name) {
384
+ const args = tool === "claude" ?
385
+ ["mcp", "remove", "--scope", "user", name] :
386
+ ["mcp", "remove", name];
387
+ await this.run(toolSpecs[tool].cli, args, { ignoreError: `MCP server "${name}" not registered` });
388
+ }
389
+ /* registry of pre-defined MCP servers: maps each server id onto its
390
+ dedicated handler which performs the activate/deactivate operation */
391
+ mcpServers = [
392
+ {
393
+ id: "openai-chatgpt",
394
+ name: "OpenAI ChatGPT",
395
+ version: "5.5",
396
+ env: ["OPENAI_CHATGPT", "OPENROUTER"],
397
+ server: "chat-openai-chatgpt",
398
+ skills: ["ase-meta-chat", "ase-meta-quorum"],
399
+ handler: async (spec, tool, action, envKey, envVal) => {
400
+ if (action === "activate") {
401
+ if (envKey === "OPENROUTER")
402
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
403
+ type: "stdio", command: [
404
+ "npx", "-y", "mcp-to-openai",
405
+ "--service", spec.name,
406
+ "--mcp-tool", "query",
407
+ "--openai-url", "https://openrouter.ai/api/v1",
408
+ "--openai-api", "completion",
409
+ "--openai-model", "openai/gpt-5.5"
410
+ ]
411
+ });
412
+ else
413
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
414
+ type: "stdio", command: [
415
+ "npx", "-y", "mcp-to-openai",
416
+ "--service", spec.name,
417
+ "--mcp-tool", "query",
418
+ "--openai-url", "https://api.openai.com/v1",
419
+ "--openai-api", "responses",
420
+ "--openai-model", "gpt-5.5"
421
+ ]
422
+ });
423
+ }
424
+ else
425
+ await this.mcpRemove(tool, spec.server);
426
+ }
427
+ },
428
+ {
429
+ id: "google-gemini",
430
+ name: "Google Gemini",
431
+ version: "3.5",
432
+ env: ["GOOGLE_GEMINI", "OPENROUTER"],
433
+ server: "chat-google-gemini",
434
+ skills: ["ase-meta-chat", "ase-meta-quorum"],
435
+ handler: async (spec, tool, action, envKey, envVal) => {
436
+ if (action === "activate") {
437
+ if (envKey === "OPENROUTER")
438
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
439
+ type: "stdio", command: [
440
+ "npx", "-y", "mcp-to-openai",
441
+ "--service", spec.name,
442
+ "--mcp-tool", "query",
443
+ "--openai-url", "https://openrouter.ai/api/v1",
444
+ "--openai-api", "completion",
445
+ "--openai-model", "google/gemini-3.5-flash"
446
+ ]
447
+ });
448
+ else
449
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
450
+ type: "stdio", command: [
451
+ "npx", "-y", "mcp-to-openai",
452
+ "--service", spec.name,
453
+ "--mcp-tool", "query",
454
+ "--openai-url", "https://generativelanguage.googleapis.com/v1beta/openai/",
455
+ "--openai-api", "completion",
456
+ "--openai-model", "gemini-3.5-flash"
457
+ ]
458
+ });
459
+ }
460
+ else
461
+ await this.mcpRemove(tool, spec.server);
462
+ }
463
+ },
464
+ {
465
+ id: "deepseek",
466
+ name: "DeepSeek",
467
+ version: "4.0",
468
+ env: ["DEEPSEEK", "OPENROUTER"],
469
+ server: "chat-deepseek",
470
+ skills: ["ase-meta-chat", "ase-meta-quorum"],
471
+ handler: async (spec, tool, action, envKey, envVal) => {
472
+ if (action === "activate") {
473
+ if (envKey === "OPENROUTER")
474
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
475
+ type: "stdio",
476
+ command: [
477
+ "npx", "-y", "mcp-to-openai",
478
+ "--service", spec.name,
479
+ "--mcp-tool", "query",
480
+ "--openai-url", "https://openrouter.ai/api/v1",
481
+ "--openai-api", "completion",
482
+ "--openai-model", "deepseek/deepseek-v4-flash"
483
+ ]
484
+ });
485
+ else
486
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
487
+ type: "stdio", command: [
488
+ "npx", "-y", "mcp-to-openai",
489
+ "--service", spec.name,
490
+ "--mcp-tool", "query",
491
+ "--openai-url", "https://api.deepseek.com/v1",
492
+ "--openai-api", "completion",
493
+ "--openai-model", "deepseek-v4-flash"
494
+ ]
495
+ });
496
+ }
497
+ else
498
+ await this.mcpRemove(tool, spec.server);
499
+ }
500
+ },
501
+ {
502
+ id: "xai-grok",
503
+ name: "xAI Grok",
504
+ version: "4.3",
505
+ env: ["XAI_GROK", "OPENROUTER"],
506
+ server: "chat-xai-grok",
507
+ skills: ["ase-meta-chat", "ase-meta-quorum"],
508
+ handler: async (spec, tool, action, envKey, envVal) => {
509
+ if (action === "activate") {
510
+ if (envKey === "OPENROUTER")
511
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
512
+ type: "stdio", command: [
513
+ "npx", "-y", "mcp-to-openai",
514
+ "--service", spec.name,
515
+ "--mcp-tool", "query",
516
+ "--openai-url", "https://openrouter.ai/api/v1",
517
+ "--openai-api", "completion",
518
+ "--openai-model", "x-ai/grok-4.3"
519
+ ]
520
+ });
521
+ else
522
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
523
+ type: "stdio", command: [
524
+ "npx", "-y", "mcp-to-openai",
525
+ "--service", spec.name,
526
+ "--mcp-tool", "query",
527
+ "--openai-url", "https://api.x.ai/v1",
528
+ "--openai-api", "completion",
529
+ "--openai-model", "grok-4.3"
530
+ ]
531
+ });
532
+ }
533
+ else
534
+ await this.mcpRemove(tool, spec.server);
535
+ }
536
+ },
537
+ {
538
+ id: "alibaba-qwen",
539
+ name: "Alibaba Qwen",
540
+ version: "3.7",
541
+ env: ["ALIBABA_QWEN", "OPENROUTER"],
542
+ server: "chat-alibaba-qwen",
543
+ skills: ["ase-meta-chat", "ase-meta-quorum"],
544
+ handler: async (spec, tool, action, envKey, envVal) => {
545
+ if (action === "activate") {
546
+ if (envKey === "OPENROUTER")
547
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
548
+ type: "stdio", command: [
549
+ "npx", "-y", "mcp-to-openai",
550
+ "--service", spec.name,
551
+ "--mcp-tool", "query",
552
+ "--openai-url", "https://openrouter.ai/api/v1",
553
+ "--openai-api", "completion",
554
+ "--openai-model", "qwen/qwen3.7-max"
555
+ ]
556
+ });
557
+ else
558
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
559
+ type: "stdio", command: [
560
+ "npx", "-y", "mcp-to-openai",
561
+ "--service", spec.name,
562
+ "--mcp-tool", "query",
563
+ "--openai-url", "https://dashscope.aliyuncs.com/compatible-mode/v1",
564
+ "--openai-api", "completion",
565
+ "--openai-model", "qwen3.7-max"
566
+ ]
567
+ });
568
+ }
569
+ else
570
+ await this.mcpRemove(tool, spec.server);
571
+ }
572
+ },
573
+ {
574
+ id: "zai-glm",
575
+ name: "Z.AI GLM",
576
+ version: "5.1",
577
+ env: ["ZAI_GLM", "OPENROUTER"],
578
+ server: "chat-zai-glm",
579
+ skills: ["ase-meta-chat", "ase-meta-quorum"],
580
+ handler: async (spec, tool, action, envKey, envVal) => {
581
+ if (action === "activate") {
582
+ if (envKey === "OPENROUTER")
583
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
584
+ type: "stdio", command: [
585
+ "npx", "-y", "mcp-to-openai",
586
+ "--service", spec.name,
587
+ "--mcp-tool", "query",
588
+ "--openai-url", "https://openrouter.ai/api/v1",
589
+ "--openai-api", "completion",
590
+ "--openai-model", "z-ai/glm-5.1"
591
+ ]
592
+ });
593
+ else
594
+ await this.mcpAdd(tool, spec.server, { OPENAI_KEY: envVal }, {
595
+ type: "stdio", command: [
596
+ "npx", "-y", "mcp-to-openai",
597
+ "--service", spec.name,
598
+ "--mcp-tool", "query",
599
+ "--openai-url", "https://api.z.ai/api/paas/v4/",
600
+ "--openai-api", "completion",
601
+ "--openai-model", "glm-5.1"
602
+ ]
603
+ });
604
+ }
605
+ else
606
+ await this.mcpRemove(tool, spec.server);
607
+ }
608
+ },
609
+ {
610
+ id: "brave",
611
+ name: "Brave",
612
+ version: "latest",
613
+ env: ["BRAVE"],
614
+ server: "search-brave",
615
+ skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
616
+ handler: async (spec, tool, action, _envKey, envVal) => {
617
+ if (action === "activate")
618
+ await this.mcpAdd(tool, spec.server, {
619
+ "BRAVE_API_KEY": envVal,
620
+ "BRAVE_MCP_ENABLED_TOOLS": "brave_web_search"
621
+ }, { type: "stdio", command: ["npx", "-y", "@brave/brave-search-mcp-server"] });
622
+ else
623
+ await this.mcpRemove(tool, spec.server);
624
+ }
625
+ },
626
+ {
627
+ id: "perplexity",
628
+ name: "Perplexity",
629
+ version: "latest",
630
+ env: ["PERPLEXITY"],
631
+ server: "search-perplexity",
632
+ skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
633
+ handler: async (spec, tool, action, _envKey, envVal) => {
634
+ if (action === "activate")
635
+ await this.mcpAdd(tool, spec.server, {
636
+ "PERPLEXITY_API_KEY": envVal
637
+ }, { type: "stdio", command: ["npx", "-y", "@perplexity-ai/mcp-server"] });
638
+ else
639
+ await this.mcpRemove(tool, spec.server);
640
+ }
641
+ },
642
+ {
643
+ id: "exa",
644
+ name: "Exa",
645
+ version: "latest",
646
+ env: ["EXA"],
647
+ server: "search-exa",
648
+ skills: ["ase-meta-search", "ase-meta-evaluate", "ase-arch-discover"],
649
+ handler: async (spec, tool, action, _envKey, envVal) => {
650
+ if (action === "activate")
651
+ await this.mcpAdd(tool, spec.server, {}, { type: "http", url: `https://mcp.exa.ai/mcp?exaApiKey=${envVal}` });
652
+ else
653
+ await this.mcpRemove(tool, spec.server);
654
+ }
655
+ }
656
+ ];
243
657
  /* parse and validate the --tool option */
244
658
  parseTool(value) {
245
659
  if (value !== "claude" && value !== "copilot")
@@ -306,5 +720,36 @@ export default class SetupCommand {
306
720
  .action(async (opts) => {
307
721
  process.exit(await this.doDisable(this.parseTool(opts.tool)));
308
722
  });
723
+ /* register CLI sub-command "ase setup mcp" */
724
+ const mcpCmd = setupCmd
725
+ .command("mcp")
726
+ .description("activate or deactivate pre-defined MCP servers for a tool")
727
+ .action(() => {
728
+ mcpCmd.outputHelp();
729
+ process.exit(1);
730
+ });
731
+ /* register CLI sub-command "ase setup mcp list" */
732
+ mcpCmd
733
+ .command("list")
734
+ .description("list all available pre-defined MCP server names")
735
+ .action(async () => {
736
+ process.exit(await this.doMcpList());
737
+ });
738
+ /* register CLI sub-command "ase setup mcp activate" */
739
+ mcpCmd
740
+ .command("activate [servers]")
741
+ .description("activate pre-defined MCP servers (comma-separated list, or \"all\")")
742
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
743
+ .action(async (servers, opts) => {
744
+ process.exit(await this.doMcp("activate", this.parseTool(opts.tool), servers ?? "all"));
745
+ });
746
+ /* register CLI sub-command "ase setup mcp deactivate" */
747
+ mcpCmd
748
+ .command("deactivate [servers]")
749
+ .description("deactivate pre-defined MCP servers (comma-separated list, or \"all\")")
750
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
751
+ .action(async (servers, opts) => {
752
+ process.exit(await this.doMcp("deactivate", this.parseTool(opts.tool), servers ?? "all"));
753
+ });
309
754
  }
310
755
  }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.50",
9
+ "version": "0.0.52",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,8 +18,8 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.59.4",
22
- "@typescript-eslint/eslint-plugin": "8.59.4",
21
+ "@typescript-eslint/parser": "8.60.0",
22
+ "@typescript-eslint/eslint-plugin": "8.60.0",
23
23
  "eslint-plugin-promise": "7.3.0",
24
24
  "eslint-plugin-import": "2.32.0",
25
25
  "neostandard": "0.13.0",
@@ -41,8 +41,9 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "commander": "14.0.3",
44
+ "@dotenvx/dotenvx": "1.69.0",
44
45
  "yaml": "2.9.0",
45
- "valibot": "1.4.0",
46
+ "valibot": "1.4.1",
46
47
  "execa": "9.6.1",
47
48
  "mkdirp": "3.0.1",
48
49
  "@hapi/hapi": "21.4.9",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ase",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "Agentic Software Engineering (ASE)",
5
5
  "keywords": [ "agentic", "software", "engineering" ],
6
6
  "homepage": "https://ase.tools",
@@ -1,43 +1,54 @@
1
1
  ---
2
2
  name: ase-meta-chat
3
3
  description: "Query Foreign LLM for Chat via MCP Tool"
4
- effort: low
4
+ effort: high
5
5
  tools:
6
- - "mcp__chat-openai-chatgpt__chat-with-openai-chatgpt"
7
- - "mcp__chat-google-gemini__chat-with-google-gemini"
8
- - "mcp__chat-deepseek__chat-with-deepseek"
9
- - "mcp__chat-xai-grok__chat-with-xai-grok"
6
+ - mcp__chat-openai-chatgpt__query
7
+ - mcp__chat-google-gemini__query
8
+ - mcp__chat-deepseek__query
9
+ - mcp__chat-xai-grok__query
10
+ - mcp__chat-zai-glm__query
11
+ - mcp__chat-alibaba-qwen__query
10
12
  ---
11
13
 
12
- 1. **Determine LLM and Query**:
14
+ @../meta/ase-control.md
13
15
 
14
- Set <llm>$ARGUMENTS[0]</llm>.
15
- Set <query/> to the second and following tokens in `$ARGUMENTS`.
16
+ 1. Treat `$ARGUMENTS` as a single whitespace-separated string.
17
+ Set <llm/> to the *first* token.
18
+ Set <query/> to the *second and all following* tokens.
16
19
  You *MUST* *NOT* output anything related to this step.
17
20
 
18
- 2. **Determine MCP Tool**:
21
+ 2. Set <server></server> (set to empty).
19
22
 
20
- Use the <llm/> to determine the corresponding MCP tool <tool/>, from
21
- the following list of potentially available MCP tool:
23
+ <if condition="<llm/> is equal 'chatgpt'"> <server>chat-openai-chatgpt</server></if>
24
+ <if condition="<llm/> is equal 'gemini'"> <server>chat-google-gemini</server> </if>
25
+ <if condition="<llm/> is equal 'deepseek'"><server>chat-deepseek</server> </if>
26
+ <if condition="<llm/> is equal 'grok'"> <server>chat-xai-grok</server> </if>
27
+ <if condition="<llm/> is equal 'glm'"> <server>chat-zai-glm</server> </if>
28
+ <if condition="<llm/> is equal 'qwen'"> <server>chat-alibaba-qwen</server> </if>
22
29
 
23
- - **OpenAI ChatGPT** (<llm/> `chatgpt`): MCP <tool/> `chat-with-openai-chatgpt`
24
- - **Google Gemini** (<llm/> `gemini`): MCP <tool/> `chat-with-google-gemini`
25
- - **DeepSeek** (<llm/> `deepseek`): MCP <tool/> `chat-with-deepseek`
26
- - **xAI Grok** (<llm/> `grok`): MCP <tool/> `chat-with-xai-grok`
30
+ <if condition="<server/> is empty">
31
+ You *MUST* output the following <template/> and immediately *STOP* processing
32
+ (do *NOT* continue with any further step and do *NOT* call any MCP tool):
27
33
 
28
- You *MUST* *NOT* output anything related to this step, except if the
29
- MCP tool <tool/> cannot be determined (because the corresponding
30
- MCP server is not available or currently disabled), just output the
34
+ <template>
35
+ ERROR: unknown LLM `<llm/>` (has to be one of: chatgpt, gemini, deepseek, grok, glm, qwen)
36
+ </template>
37
+ </if>
38
+
39
+ 3. Check whether the MCP server <server/> is available (because perhaps
40
+ it is currently disabled or not configured at all).
41
+
42
+ You *MUST* *NOT* output anything related to this step, except if
43
+ the MCP server <server/> is not available, then just output the
31
44
  following <template/> and immediately *STOP* processing:
32
45
 
33
46
  <template>
34
- ERROR: LLM `<llm/>` required MCP tool `<tool/>`, but this is (currently) not available.
47
+ ERROR: LLM `<llm/>` requires MCP server `<server/>`, but it is (currently) not available!
35
48
  </template>
36
49
 
37
- 3. **Call MCP Tool**:
38
-
39
- Else, call the MCP tool with `<tool/>(content: <query/>)` and
40
- then return its result *verbatim* and *without any modifications*.
41
- Especially, do *NOT* add or remove any text from the agent response
42
- on your own and do not interpret the result in any way.
43
-
50
+ 4. Now call the MCP tool `query(query: <query/>)` from the MCP server
51
+ <server/> and then return its result `text` *verbatim* and
52
+ *without any modifications*. Especially, do *NOT* add or remove
53
+ any text to the MCP server response on your own and do not
54
+ interpret the result in any way.
@@ -3,7 +3,7 @@ name: ase-meta-diagram
3
3
  description: "Diagram Rendering"
4
4
  tools:
5
5
  - "mcp__plugin_ase_ase__diagram"
6
- effort: low
6
+ effort: high
7
7
  ---
8
8
 
9
9
  Your role is to render a *single* diagram, with *deterministic* and
@@ -7,6 +7,11 @@ tools:
7
7
  - "WebSearch"
8
8
  model: sonnet
9
9
  effort: low
10
+ tools:
11
+ - "mcp__search-perplexity__perplexity_search"
12
+ - "mcp__search-brave__brave_web_search"
13
+ - "mcp__search-exa__web_search_exa"
14
+ - "WebSearch"
10
15
  ---
11
16
 
12
17
  Just perform the given *query* `$ARGUMENTS` and
@@ -4,19 +4,19 @@
4
4
  ## Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  ##
6
6
 
7
- # lint project
7
+ # [plugin] lint project
8
8
  lint
9
9
  markdownlint-cli2 --config etc/markdownlint.yaml skills/**/*.md
10
10
 
11
- # build project
11
+ # [plugin] build project
12
12
  build : lint
13
13
  true
14
14
 
15
- # remove all generated files
15
+ # [plugin] remove all generated files
16
16
  clean
17
17
  true
18
18
 
19
- # remove all built files
19
+ # [plugin] remove all built files
20
20
  distclean: clean
21
21
  shx rm -f package-lock.json
22
22
  shx rm -rf node_modules
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.50",
9
+ "version": "0.0.52",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -78,6 +78,8 @@ by querying *multiple* AIs for an *optimal consensus*.
78
78
  <expand name="agent" arg1="Google Gemini" arg2="gemini"></expand>
79
79
  <expand name="agent" arg1="DeepSeek" arg2="deepseek"></expand>
80
80
  <expand name="agent" arg1="xAI Grok" arg2="grok"></expand>
81
+ <expand name="agent" arg1="Z.AI GLM" arg2="glm"></expand>
82
+ <expand name="agent" arg1="Alibaba Qwen" arg2="qwen"></expand>
81
83
 
82
84
  You *MUST* *NOT* output anything in this step.
83
85
 
@@ -8,8 +8,10 @@ user-invocable: true
8
8
  disable-model-invocation: false
9
9
  effort: low
10
10
  allowed-tools:
11
- - "mcp__perplexity__perplexity_search"
12
- - "mcp__brave__brave_web_search"
11
+ - "mcp__search-perplexity__perplexity_search"
12
+ - "mcp__search-brave__brave_web_search"
13
+ - "mcp__search-exa__web_search_exa"
14
+ - "WebSearch"
13
15
  - "Agent"
14
16
  ---
15
17
 
@@ -47,18 +49,26 @@ Your objective is to *search* the *Internet*/*Web* for the following query:
47
49
  ```
48
50
  </define>
49
51
 
50
- If the MCP tool `mcp__perplexity__perplexity_search` is available, call:
52
+ If the MCP tool `perplexity_search` from the MCP server `search-perplexity` is available:
51
53
  <expand name="agent">
52
- Call the MCP tool `mcp__perplexity__perplexity_search(query: "<query/>")`
54
+ Call the MCP tool `perplexity_search(query: "<query/>")`
55
+ from the MCP server `search-perplexity`.
53
56
  </expand>
54
57
 
55
- If the MCP tool `mcp__brave__brave_web_search` is available, call:
58
+ If the MCP tool `brave_web_search` from the MCP server `search-brave` is available:
56
59
  <expand name="agent">
57
- Call the MCP tool `mcp__brave__brave_web_search(query: "<query/>")`
60
+ Call the MCP tool `brave_web_search(query: "<query/>")`
61
+ from the MCP server `search-brave`.
58
62
  </expand>
59
63
 
64
+ If the MCP tool `web_search_exa` from the MCP server `search-exa` is available:
60
65
  <expand name="agent">
61
- Call the tool `WebSearch(query: "<query/>")`
66
+ Call the MCP tool `web_search_exa(query: "<query/>")`
67
+ from the MCP server `search-exa`.
68
+ </expand>
69
+
70
+ <expand name="agent">
71
+ Call the tool `WebSearch(query: "<query/>")`.
62
72
  </expand>
63
73
 
64
74
  </step>
package/dst/ase-claude.js DELETED
@@ -1,116 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import { execa, execaSync } from "execa";
7
- import which from "which";
8
- import { Config, configSchema, parseScope } from "./ase-config.js";
9
- /* default statusline arguments (claudeX fallback) used when the
10
- "agent.statusline" config variable is empty or unset */
11
- const DEFAULT_STATUSLINE_ARGS = "-w 0 -m 2 '<blue>%u</blue> <red>%p</red> <black>%T</black> %s' '%m %e %t' '%P %c'";
12
- /* CLI command "ase claude" */
13
- export default class ClaudeCommand {
14
- log;
15
- constructor(log) {
16
- this.log = log;
17
- }
18
- /* ensure a tool is available */
19
- async ensureTool(tool) {
20
- return which(tool).catch(() => {
21
- throw new Error(`mandatory tool "${tool}" not found in $PATH`);
22
- });
23
- }
24
- /* resolve the statusline command-line arguments from the layered
25
- ASE configuration ("agent.statusline"), falling back to the
26
- claudeX default if the value is empty or unavailable */
27
- resolveStatuslineArgs() {
28
- let args = "";
29
- try {
30
- const cfg = new Config("config", configSchema, this.log, parseScope("project"));
31
- cfg.read("lenient");
32
- args = String(cfg.get("agent.statusline") ?? "").trim();
33
- }
34
- catch (_e) {
35
- /* cascade unavailable; keep fallback */
36
- }
37
- if (args === "")
38
- args = DEFAULT_STATUSLINE_ARGS;
39
- return args;
40
- }
41
- /* register commands */
42
- register(program) {
43
- program
44
- .command("claude")
45
- .description("start Claude Code with bootstrapped ASE environment and settings")
46
- .passThroughOptions()
47
- .allowUnknownOption()
48
- .argument("[args...]", "arguments forwarded verbatim to the \"claude\" CLI")
49
- .action(async (args) => {
50
- /* ensure Claude Code CLI is available */
51
- await this.ensureTool("claude");
52
- /* bootstrap ASE_TERM_WIDTH from terminal columns */
53
- if (process.env.ASE_TERM_WIDTH === undefined) {
54
- let width = 0;
55
- if (process.stdout.isTTY) {
56
- const cols = process.stdout.columns;
57
- if (typeof cols === "number" && cols > 0)
58
- width = cols;
59
- }
60
- process.env.ASE_TERM_WIDTH = `${width}`;
61
- }
62
- /* bootstrap ASE_TERM_HEIGHT from terminal rows */
63
- if (process.env.ASE_TERM_HEIGHT === undefined) {
64
- let height = 0;
65
- if (process.stdout.isTTY) {
66
- const rows = process.stdout.rows;
67
- if (typeof rows === "number" && rows > 0)
68
- height = rows;
69
- }
70
- process.env.ASE_TERM_HEIGHT = `${height}`;
71
- }
72
- /* bootstrap ASE_TERM_COLORS from "tput colors" */
73
- if (process.env.ASE_TERM_COLORS === undefined) {
74
- let colorMode = "none";
75
- try {
76
- const { stdout } = execaSync("tput", ["colors"], { reject: false });
77
- const n = parseInt(stdout.trim(), 10);
78
- if (!Number.isNaN(n) && n >= 256)
79
- colorMode = "ansi256";
80
- else if (!Number.isNaN(n) && n >= 16)
81
- colorMode = "ansi16";
82
- }
83
- catch (_e) {
84
- /* ignore */
85
- }
86
- process.env.ASE_TERM_COLORS = colorMode;
87
- }
88
- /* resolve statusline arguments (config or claudeX fallback) */
89
- const statuslineArgs = this.resolveStatuslineArgs();
90
- /* build inline Claude Code settings JSON */
91
- const settings = {
92
- env: {
93
- DISABLE_TELEMETRY: "1",
94
- DISABLE_AUTOUPDATER: "1",
95
- DISABLE_BUG_COMMAND: "1",
96
- DISABLE_ERROR_REPORTING: "1"
97
- },
98
- statusLine: {
99
- type: "command",
100
- command: `ase statusline ${statuslineArgs}`,
101
- padding: 0
102
- }
103
- };
104
- const settingsJSON = JSON.stringify(settings);
105
- /* exec Claude Code with the inline settings prepended
106
- and any user-supplied arguments forwarded verbatim */
107
- const result = await execa("claude", ["--settings", settingsJSON, ...args], {
108
- stdio: "inherit",
109
- env: process.env,
110
- reject: false,
111
- windowsHide: false
112
- });
113
- process.exit(result.exitCode ?? 0);
114
- });
115
- }
116
- }
@@ -1,67 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import { z } from "zod";
7
- /* reusable functionality: weighted multi-criteria decision matrix */
8
- export class Decision {
9
- /* compute the per-alternative product-sum (rating) row from a
10
- weighted decision matrix. Each input row has the shape
11
- `[weight, eval_1, eval_2, ..., eval_N]`. For each alternative
12
- column j (1..N), the result is the sum over all rows K of
13
- `weight_K * eval_K_j`. The output array has length N. */
14
- static productSum(matrix) {
15
- if (matrix.length === 0)
16
- return [];
17
- const cols = matrix[0].length;
18
- if (cols < 2)
19
- throw new Error("each row must contain a weight followed by at least one evaluation column");
20
- const N = cols - 1;
21
- const ratings = new Array(N).fill(0);
22
- for (let i = 0; i < matrix.length; i++) {
23
- const row = matrix[i];
24
- if (row.length !== cols)
25
- throw new Error(`row ${i} has ${row.length} columns, expected ${cols}`);
26
- const weight = row[0];
27
- for (let j = 0; j < N; j++)
28
- ratings[j] += weight * row[j + 1];
29
- }
30
- return ratings;
31
- }
32
- }
33
- /* MCP registration entry point for the decision-matrix tool */
34
- export class DecisionMCP {
35
- register(mcp) {
36
- mcp.registerTool("decision_matrix", {
37
- title: "ASE decision matrix",
38
- description: "Compute the per-alternative product-sum (rating) row of a weighted " +
39
- "multi-criteria decision matrix. The input `matrix` is an array of rows, " +
40
- "one row per criterion, where each row has the shape " +
41
- "`[weight, eval_1, eval_2, ..., eval_N]` (i.e. the criterion weight " +
42
- "followed by N evaluation values, one per alternative). For each " +
43
- "alternative column j (1..N), the result is the sum over all rows K of " +
44
- "`weight_K * eval_K_j`. Returns a JSON `text` array of length N with " +
45
- "the raw, unrounded ratings (one per alternative, in the same column " +
46
- "order as the input).",
47
- inputSchema: {
48
- matrix: z.array(z.array(z.number()))
49
- .describe("Decision matrix rows: each row is `[weight, eval_1, ..., eval_N]`")
50
- }
51
- }, async (args) => {
52
- try {
53
- const result = Decision.productSum(args.matrix);
54
- return {
55
- content: [{ type: "text", text: JSON.stringify(result) }]
56
- };
57
- }
58
- catch (err) {
59
- const message = err instanceof Error ? err.message : String(err);
60
- return {
61
- isError: true,
62
- content: [{ type: "text", text: `ERROR: ${message}` }]
63
- };
64
- }
65
- });
66
- }
67
- }
package/dst/ase-foo.js DELETED
@@ -1,21 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- /* command-line handling */
7
- export default class FooCommand {
8
- log;
9
- constructor(log) {
10
- this.log = log;
11
- }
12
- /* register commands */
13
- register(program) {
14
- program
15
- .command("foo")
16
- .description("Print a nice Hello World message")
17
- .action(() => {
18
- process.stdout.write("Hello, World!\n");
19
- });
20
- }
21
- }
package/dst/ase-hello.js DELETED
@@ -1,23 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import chalk from "chalk";
7
- /* command-line handling */
8
- export default class HelloCommand {
9
- log;
10
- constructor(log) {
11
- this.log = log;
12
- }
13
- /* register commands */
14
- register(program) {
15
- program
16
- .command("hello")
17
- .description("Print a nice greeting in red")
18
- .option("-s, --subject <subject>", "subject to greet", "Hello")
19
- .action((opts) => {
20
- process.stdout.write(chalk.red(`${opts.subject} Universe!`) + "\n");
21
- });
22
- }
23
- }
@@ -1,23 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import chalk from "chalk";
7
- /* command-line handling */
8
- export default class HelloxxCommand {
9
- log;
10
- constructor(log) {
11
- this.log = log;
12
- }
13
- /* register commands */
14
- register(program) {
15
- program
16
- .command("helloxx")
17
- .description("Print a Hello World greeting")
18
- .action(() => {
19
- process.stdout.write(chalk.bold.green("Hello, World!") + "\n");
20
- process.exit(0);
21
- });
22
- }
23
- }
package/dst/ase-launch.js DELETED
@@ -1,109 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import { execa, execaSync } from "execa";
7
- import which from "which";
8
- import { Config, configSchema, parseScope } from "./ase-config.js";
9
- /* list of supported tools for "ase launch <tool>" */
10
- const SUPPORTED_TOOLS = ["claude"];
11
- /* default statusline arguments (used when "agent.statusline" is empty/unset) */
12
- const DEFAULT_STATUSLINE_ARGS = "-w 0 -m 2 '<blue>%u</blue> <red>%p</red> <black>%T</black> %s' '%m %e %t' '%P %c'";
13
- /* CLI command "ase launch" */
14
- export default class LaunchCommand {
15
- log;
16
- constructor(log) {
17
- this.log = log;
18
- }
19
- /* resolve the effective "agent.statusline" from the layered
20
- configuration cascade (default < user < project) */
21
- readStatuslineConfig() {
22
- let statusline = "";
23
- try {
24
- const cfg = new Config("config", configSchema, this.log, parseScope(undefined));
25
- cfg.read("lenient");
26
- statusline = String(cfg.get("agent.statusline") ?? "").trim();
27
- }
28
- catch (_e) {
29
- /* cascade unavailable; leave default */
30
- }
31
- /* reject shell metacharacters to prevent command injection through
32
- the configured value when later interpolated into a shell command */
33
- if (statusline !== "" && /[;&|`$\n\r]/.test(statusline))
34
- throw new Error("invalid \"agent.statusline\" configuration value: " +
35
- "must not contain shell metacharacters (; & | ` $ newline)");
36
- return statusline;
37
- }
38
- /* populate ASE_TERM_* environment variables (only if unset),
39
- so downstream tools (e.g. the diagram renderer) get reliable
40
- terminal sizing/color information regardless of the launched tool */
41
- populateTermEnv() {
42
- const tty = process.stdout.isTTY;
43
- if (process.env.ASE_TERM_WIDTH === undefined) {
44
- const cols = tty ? process.stdout.columns : 0;
45
- process.env.ASE_TERM_WIDTH = `${typeof cols === "number" && cols > 0 ? cols : 0}`;
46
- }
47
- if (process.env.ASE_TERM_HEIGHT === undefined) {
48
- const rows = tty ? process.stdout.rows : 0;
49
- process.env.ASE_TERM_HEIGHT = `${typeof rows === "number" && rows > 0 ? rows : 0}`;
50
- }
51
- if (process.env.ASE_TERM_COLORS === undefined) {
52
- let colorMode = "none";
53
- try {
54
- const { stdout } = execaSync("tput", ["colors"], { reject: false });
55
- const n = parseInt(stdout.trim(), 10);
56
- if (!Number.isNaN(n) && n >= 256)
57
- colorMode = "ansi256";
58
- else if (!Number.isNaN(n) && n >= 16)
59
- colorMode = "ansi16";
60
- }
61
- catch (_e) {
62
- /* ignore */
63
- }
64
- process.env.ASE_TERM_COLORS = colorMode;
65
- }
66
- }
67
- /* handler for "ase launch <tool> [<options>...]" */
68
- async run(tool, args) {
69
- /* populate ASE_TERM_* environment variables (always, regardless of tool) */
70
- this.populateTermEnv();
71
- /* dispatch by tool name */
72
- if (tool === "claude") {
73
- await which("claude").catch(() => {
74
- throw new Error("required tool \"claude\" not found in $PATH");
75
- });
76
- let statusline = this.readStatuslineConfig();
77
- if (statusline === "")
78
- statusline = DEFAULT_STATUSLINE_ARGS;
79
- const settingsJson = JSON.stringify({
80
- statusLine: {
81
- type: "command",
82
- command: `ase statusline ${statusline}`,
83
- padding: 0
84
- }
85
- });
86
- const result = await execa("claude", ["--settings", settingsJson, ...args], {
87
- stdio: "inherit",
88
- env: process.env,
89
- reject: false
90
- });
91
- return result.exitCode ?? 0;
92
- }
93
- else
94
- throw new Error(`unsupported tool "${tool}" ` +
95
- `(expected one of: ${SUPPORTED_TOOLS.join(", ")})`);
96
- }
97
- /* register commands */
98
- register(program) {
99
- program
100
- .command("launch <tool> [options...]")
101
- .description("launch a supported tool with bootstrapped environment and settings " +
102
- `(supported tools: ${SUPPORTED_TOOLS.join(", ")})`)
103
- .passThroughOptions()
104
- .allowUnknownOption()
105
- .action(async (tool, options) => {
106
- process.exit(await this.run(tool, options));
107
- });
108
- }
109
- }
package/dst/ase-plan.js DELETED
@@ -1,143 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import path from "node:path";
7
- import os from "node:os";
8
- import fs from "node:fs";
9
- /* validate the plan id to keep it safe as a filename component */
10
- const validateId = (id) => {
11
- if (typeof id !== "string" || id.length === 0)
12
- throw new Error("plan: id must be a non-empty string");
13
- if (!/^[A-Za-z0-9-]+$/.test(id))
14
- throw new Error("plan: id must match [A-Za-z0-9-]+");
15
- };
16
- /* resolve the on-disk path for a given plan id */
17
- const planPath = (id) => {
18
- validateId(id);
19
- return path.join(os.homedir(), ".ase", "plan", `${id}.md`);
20
- };
21
- /* load a plan; returns empty string if no plan exists */
22
- export const planLoad = (id) => {
23
- const file = planPath(id);
24
- if (!fs.existsSync(file))
25
- return "";
26
- return fs.readFileSync(file, "utf8");
27
- };
28
- /* save a plan as UTF-8 text under the given id */
29
- export const planSave = (id, text) => {
30
- if (typeof text !== "string")
31
- throw new Error("plan: text must be a string");
32
- const file = planPath(id);
33
- fs.mkdirSync(path.dirname(file), { recursive: true });
34
- fs.writeFileSync(file, text, "utf8");
35
- };
36
- /* delete a plan by id; returns true if a plan existed and was removed */
37
- export const planDelete = (id) => {
38
- const file = planPath(id);
39
- if (!fs.existsSync(file))
40
- return false;
41
- fs.rmSync(file);
42
- return true;
43
- };
44
- /* purge plans whose modification time is older than the given cutoff in
45
- milliseconds; returns the list of removed plan ids */
46
- export const planPurge = (maxAgeMs) => {
47
- const dir = path.join(os.homedir(), ".ase", "plan");
48
- if (!fs.existsSync(dir))
49
- return [];
50
- const cutoff = Date.now() - maxAgeMs;
51
- const removed = [];
52
- for (const entry of fs.readdirSync(dir)) {
53
- if (!entry.endsWith(".md"))
54
- continue;
55
- const file = path.join(dir, entry);
56
- const st = fs.statSync(file);
57
- if (!st.isFile())
58
- continue;
59
- if (st.mtimeMs < cutoff) {
60
- fs.rmSync(file);
61
- removed.push(entry.slice(0, -3));
62
- }
63
- }
64
- return removed;
65
- };
66
- /* read all of stdin as a UTF-8 string */
67
- const readStdin = () => {
68
- return new Promise((resolve, reject) => {
69
- const chunks = [];
70
- process.stdin.on("data", (chunk) => chunks.push(chunk));
71
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
72
- process.stdin.on("error", (err) => reject(err));
73
- });
74
- };
75
- /* CLI command "ase plan" */
76
- export default class PlanCommand {
77
- log;
78
- constructor(log) {
79
- this.log = log;
80
- }
81
- /* register commands */
82
- register(program) {
83
- /* register CLI top-level command "ase plan" */
84
- const plan = program
85
- .command("plan")
86
- .description("Manage persisted plans under ~/.ase/plan/<id>.md")
87
- .action(() => {
88
- plan.outputHelp();
89
- process.exit(1);
90
- });
91
- /* register CLI sub-command "ase plan load" */
92
- plan
93
- .command("load")
94
- .description("Load a plan by id and write it to stdout")
95
- .argument("<id>", "Plan identifier")
96
- .action((id) => {
97
- const text = planLoad(id);
98
- process.stdout.write(text);
99
- process.exit(0);
100
- });
101
- /* register CLI sub-command "ase plan save" */
102
- plan
103
- .command("save")
104
- .description("Save a plan by id, reading content from stdin")
105
- .argument("<id>", "Plan identifier")
106
- .action(async (id) => {
107
- const text = await readStdin();
108
- planSave(id, text);
109
- this.log.write("info", `plan: saved "${id}"`);
110
- process.exit(0);
111
- });
112
- /* register CLI sub-command "ase plan delete" */
113
- plan
114
- .command("delete")
115
- .description("Delete a plan by id")
116
- .argument("<id>", "Plan identifier")
117
- .action((id) => {
118
- const removed = planDelete(id);
119
- if (removed)
120
- this.log.write("info", `plan: removed "${id}"`);
121
- else
122
- this.log.write("info", `plan: no plan "${id}" to remove`);
123
- process.exit(removed ? 0 : 1);
124
- });
125
- /* register CLI sub-command "ase plan purge" */
126
- plan
127
- .command("purge")
128
- .description("Remove all plans with a modification time older than <days> (default: 31)")
129
- .argument("[<days>]", "Maximum plan age in days", "31")
130
- .action((days) => {
131
- const n = Number.parseInt(days, 10);
132
- if (!Number.isFinite(n) || n < 0)
133
- throw new Error("plan: <days> must be a non-negative integer");
134
- const removed = planPurge(n * 24 * 60 * 60 * 1000);
135
- if (removed.length === 0)
136
- this.log.write("info", "plan: no plans to purge");
137
- else
138
- for (const id of removed)
139
- this.log.write("info", `plan: purged "${id}"`);
140
- process.exit(0);
141
- });
142
- }
143
- }
@@ -1,38 +0,0 @@
1
- /*
2
- ** Agentic Software Engineering (ASE)
3
- ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
- */
6
- import axios from "axios";
7
- import * as v from "valibot";
8
- /* shared service host */
9
- export const SERVICE_HOST = "127.0.0.1";
10
- /* schema for ".ase/service.yaml" */
11
- export const serviceSchema = v.nullish(v.strictObject({
12
- port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
13
- }));
14
- /* distinguish ECONNREFUSED from other Axios transport errors */
15
- export const isConnRefused = (err) => {
16
- const e = err;
17
- return e?.code === "ECONNREFUSED" || e?.cause?.code === "ECONNREFUSED";
18
- };
19
- /* probe the service and verify ASE identity banner */
20
- export const probe = async (port, projectId) => {
21
- try {
22
- const r = await axios.request({
23
- method: "OPTIONS",
24
- url: `http://${SERVICE_HOST}:${port}/`,
25
- timeout: 2000,
26
- validateStatus: () => true
27
- });
28
- if (r.status < 200 || r.status >= 300)
29
- return false;
30
- const d = r.data;
31
- return d?.ase === true && d?.projectId === projectId;
32
- }
33
- catch (err) {
34
- if (isConnRefused(err))
35
- return null;
36
- throw err;
37
- }
38
- };