@rse/ase 0.9.10 → 0.9.12

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.
Files changed (41) hide show
  1. package/dst/ase-compat.js +68 -0
  2. package/dst/ase-hook.js +158 -67
  3. package/dst/ase-service.js +2 -0
  4. package/dst/ase-setup.js +52 -23
  5. package/dst/ase-statusline.js +7 -3
  6. package/dst/ase.js +2 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/mcp.json +8 -0
  10. package/plugin/.codex-plugin/plugin.json +17 -0
  11. package/plugin/.github/plugin/plugin.json +1 -1
  12. package/plugin/hooks/hooks-codex.json +67 -0
  13. package/plugin/meta/ase-dialog.md +20 -0
  14. package/plugin/package.json +1 -1
  15. package/plugin/skills/ase-arch-analyze/SKILL.md +6 -1
  16. package/plugin/skills/ase-arch-discover/SKILL.md +2 -2
  17. package/plugin/skills/ase-code-analyze/SKILL.md +12 -5
  18. package/plugin/skills/ase-code-craft/SKILL.md +6 -3
  19. package/plugin/skills/ase-code-explain/SKILL.md +6 -1
  20. package/plugin/skills/ase-code-insight/SKILL.md +6 -1
  21. package/plugin/skills/ase-code-lint/SKILL.md +9 -3
  22. package/plugin/skills/ase-code-refactor/SKILL.md +6 -3
  23. package/plugin/skills/ase-code-resolve/SKILL.md +13 -9
  24. package/plugin/skills/ase-docs-proofread/SKILL.md +9 -3
  25. package/plugin/skills/ase-meta-brainstorm/SKILL.md +3 -4
  26. package/plugin/skills/ase-meta-changelog/SKILL.md +16 -5
  27. package/plugin/skills/ase-meta-chat/SKILL.md +6 -1
  28. package/plugin/skills/ase-meta-commit/SKILL.md +5 -0
  29. package/plugin/skills/ase-meta-compat/SKILL.md +288 -0
  30. package/plugin/skills/ase-meta-compat/help.md +67 -0
  31. package/plugin/skills/ase-meta-diaboli/SKILL.md +5 -1
  32. package/plugin/skills/ase-meta-diff/SKILL.md +1 -1
  33. package/plugin/skills/ase-meta-evaluate/SKILL.md +20 -12
  34. package/plugin/skills/ase-meta-persona/SKILL.md +6 -1
  35. package/plugin/skills/ase-meta-quorum/SKILL.md +15 -3
  36. package/plugin/skills/ase-meta-steelman/SKILL.md +5 -1
  37. package/plugin/skills/ase-task-delete/SKILL.md +6 -1
  38. package/plugin/skills/ase-task-grill/SKILL.md +30 -9
  39. package/plugin/skills/ase-task-id/SKILL.md +10 -3
  40. package/plugin/skills/ase-task-reboot/SKILL.md +2 -2
  41. package/plugin/skills/ase-task-rename/SKILL.md +15 -1
@@ -0,0 +1,68 @@
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
+ /* the canonical expected values for every ase-meta-compat probe,
7
+ keyed by "<category>/<probe-name>" as used in the skill */
8
+ const EXPECTED = {
9
+ /* xml-placeholders */
10
+ "xml-placeholders/get-and-set": "42",
11
+ "xml-placeholders/self-ref": "pre_42",
12
+ "xml-placeholders/overwrite": "99",
13
+ "xml-placeholders/indexed": "+1,-1",
14
+ "xml-placeholders/nested-attr": "20",
15
+ "xml-placeholders/entity": "○",
16
+ /* control-flow */
17
+ "control-flow/branch": "mid",
18
+ "control-flow/while-sum": "15",
19
+ "control-flow/for-order": "-x-y-z",
20
+ "control-flow/while-break": "4",
21
+ "control-flow/step-skip": "X",
22
+ "control-flow/expand-subst": "[K:V]",
23
+ /* regex */
24
+ "regex/getopt-dash": "yes",
25
+ "regex/getopt-nodash": "no",
26
+ "regex/anchored-int": "yes,no",
27
+ "regex/alternation": "yes",
28
+ "regex/capture": "foo",
29
+ "regex/whitespace": "yes",
30
+ "regex/complex": "yes",
31
+ /* arithmetic */
32
+ "arithmetic/increment": "9",
33
+ "arithmetic/product-sum": "5.00",
34
+ "arithmetic/percentage": "0.43",
35
+ "arithmetic/bar-width": "54",
36
+ "arithmetic/threshold": "yes",
37
+ "arithmetic/round-half": "3"
38
+ };
39
+ /* format the expected values as "<id>: <value>\n" lines */
40
+ const formatExpected = () => Object.entries(EXPECTED).map(([id, value]) => `${id}: ${value}`).join("\n") + "\n";
41
+ /* CLI command "ase compat" */
42
+ export default class CompatCommand {
43
+ register(program) {
44
+ program
45
+ .command("compat")
46
+ .description("Output expected probe values for the ase-meta-compat self-test skill")
47
+ .action(() => {
48
+ process.stdout.write(formatExpected());
49
+ });
50
+ }
51
+ }
52
+ /* MCP tool "ase_compat" — lets the skill call this without a Bash tool */
53
+ export class CompatMCP {
54
+ register(mcp) {
55
+ mcp.registerTool("ase_compat", {
56
+ title: "ASE compat expected values",
57
+ description: "Return the canonical expected probe values for the ase-meta-compat " +
58
+ "self-test skill as \"<id>: <value>\" lines, one per probe. " +
59
+ "Call this AFTER recording all actual probe results so the LLM " +
60
+ "cannot see the expected values before running the probes.",
61
+ inputSchema: {}
62
+ }, async () => {
63
+ return {
64
+ content: [{ type: "text", text: formatExpected() }]
65
+ };
66
+ });
67
+ }
68
+ }
package/dst/ase-hook.js CHANGED
@@ -8,6 +8,7 @@ import fs from "node:fs";
8
8
  import os from "node:os";
9
9
  import { execaSync } from "execa";
10
10
  import { quote } from "shell-quote";
11
+ import getStdin from "get-stdin";
11
12
  import * as v from "valibot";
12
13
  import Version from "./ase-version.js";
13
14
  import { Config, configSchema, parseScope } from "./ase-config.js";
@@ -41,7 +42,8 @@ const toolSpecs = {
41
42
  mcpToolNamePattern: /^mcp__plugin_ase_ase__.+/,
42
43
  addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
43
44
  preToolUseWrapped: true,
44
- preToolUseEvent: "PreToolUse"
45
+ preToolUseEvent: "PreToolUse",
46
+ approvalEvent: "PreToolUse"
45
47
  },
46
48
  "copilot": {
47
49
  toolNameField: "toolName",
@@ -51,7 +53,19 @@ const toolSpecs = {
51
53
  mcpToolNamePattern: /^ase-.+/,
52
54
  addonMcpToolNamePattern: addonMcpToolNamePattern("", "-.+"),
53
55
  preToolUseWrapped: false,
54
- preToolUseEvent: "preToolUse"
56
+ preToolUseEvent: "preToolUse",
57
+ approvalEvent: "PreToolUse"
58
+ },
59
+ "codex": {
60
+ toolNameField: "tool_name",
61
+ toolInputField: "tool_input",
62
+ toolInputIsString: false,
63
+ bashToolName: "Bash",
64
+ mcpToolNamePattern: /^mcp__ase__.+/,
65
+ addonMcpToolNamePattern: addonMcpToolNamePattern("mcp__", "__.+"),
66
+ preToolUseWrapped: true,
67
+ preToolUseEvent: "PreToolUse",
68
+ approvalEvent: "PermissionRequest"
55
69
  }
56
70
  };
57
71
  /* CLI command "ase hook" */
@@ -64,6 +78,27 @@ export default class HookCommand {
64
78
  isValidSessionId(id) {
65
79
  return /^[A-Za-z0-9._-]+$/.test(id);
66
80
  }
81
+ /* read the entire stdin payload asynchronously */
82
+ readStdin() {
83
+ /* best-effort: treat an unreadable/closed stdin as empty */
84
+ return getStdin().catch(() => "");
85
+ }
86
+ /* drain and discard the stdin event payload */
87
+ async drainStdin() {
88
+ await this.readStdin();
89
+ }
90
+ /* write to stdout and resolve only once it has been fully
91
+ flushed to the underlying file descriptor */
92
+ writeStdout(text) {
93
+ return new Promise((resolve, reject) => {
94
+ process.stdout.write(text, (err) => {
95
+ if (err)
96
+ reject(err);
97
+ else
98
+ resolve();
99
+ });
100
+ });
101
+ }
67
102
  /* best-effort JSON parse with valibot schema validation: returns
68
103
  an empty object on blank input, malformed JSON, or schema
69
104
  mismatch, so callers can treat the result uniformly. Extra
@@ -110,13 +145,24 @@ export default class HookCommand {
110
145
  return this.expandReferences(content, path.dirname(abs), next);
111
146
  });
112
147
  }
113
- /* handler for "ase hook session-start" (both tools) */
148
+ /* handler for "ase hook session-start" (all tools) */
114
149
  async doSessionStart(tool) {
115
150
  /* determine plugin root (env var name differs per tool) */
116
- const pluginRootVar = tool === "copilot" ? "COPILOT_PLUGIN_ROOT" : "CLAUDE_PLUGIN_ROOT";
117
- const pluginRoot = process.env[pluginRootVar] ?? "";
151
+ let pluginRootVars;
152
+ if (tool === "copilot")
153
+ pluginRootVars = ["COPILOT_PLUGIN_ROOT"];
154
+ else if (tool === "codex")
155
+ pluginRootVars = ["PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
156
+ else
157
+ pluginRootVars = ["CLAUDE_PLUGIN_ROOT"];
158
+ let pluginRoot = "";
159
+ for (const pluginRootVar of pluginRootVars)
160
+ if ((process.env[pluginRootVar] ?? "") !== "") {
161
+ pluginRoot = process.env[pluginRootVar];
162
+ break;
163
+ }
118
164
  if (pluginRoot === "")
119
- throw new Error(`${pluginRootVar} environment variable is not set`);
165
+ throw new Error(`${pluginRootVars.join("/")} environment variable is not set`);
120
166
  /* determine path to external files */
121
167
  const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
122
168
  const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
@@ -152,7 +198,7 @@ export default class HookCommand {
152
198
  const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
153
199
  /* read session information (Claude Code uses snake_case fields,
154
200
  Copilot CLI uses camelCase fields) */
155
- const stdin = fs.readFileSync(0, "utf8");
201
+ const stdin = await this.readStdin();
156
202
  const input = this.parseJSON(stdin, v.object({
157
203
  session_id: v.optional(v.string()),
158
204
  sessionId: v.optional(v.string()),
@@ -228,9 +274,10 @@ export default class HookCommand {
228
274
  /* expand all @<file> references manually */
229
275
  md = this.expandReferences(md, path.dirname(fileMd));
230
276
  /* inject markdown into session context.
231
- Claude Code expects the context nested in "hookSpecificOutput";
232
- Copilot CLI expects a flat top-level "additionalContext" field. */
233
- const payload = tool === "claude" ? {
277
+ Claude Code and OpenAI Codex CLI expect the context nested in
278
+ "hookSpecificOutput"; Copilot CLI expects a flat top-level
279
+ "additionalContext" field. */
280
+ const payload = tool !== "copilot" ? {
234
281
  "hookSpecificOutput": {
235
282
  "hookEventName": "SessionStart",
236
283
  "additionalContext": md
@@ -238,7 +285,7 @@ export default class HookCommand {
238
285
  } : {
239
286
  "additionalContext": md
240
287
  };
241
- process.stdout.write(JSON.stringify(payload));
288
+ await this.writeStdout(JSON.stringify(payload));
242
289
  return 0;
243
290
  }
244
291
  /* publish the agent activity marker to tmux as a per-pane user
@@ -258,19 +305,21 @@ export default class HookCommand {
258
305
  }
259
306
  }
260
307
  /* handler for "ase hook user-prompt-submit" (both tools) */
261
- doUserPromptSubmit(_tool) {
308
+ async doUserPromptSubmit(_tool) {
309
+ await this.drainStdin();
262
310
  this.writeAgentStatus("busy");
263
311
  return 0;
264
312
  }
265
313
  /* handler for "ase hook stop" (both tools) */
266
- doStop(_tool) {
314
+ async doStop(_tool) {
315
+ await this.drainStdin();
267
316
  this.writeAgentStatus("ready");
268
317
  return 0;
269
318
  }
270
319
  /* handler for "ase hook session-end" (both tools) */
271
- doSessionEnd(_tool) {
320
+ async doSessionEnd(_tool) {
272
321
  /* determine session id */
273
- const sessionId = this.readSessionIdFromStdin();
322
+ const sessionId = await this.readSessionIdFromStdin();
274
323
  /* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
275
324
  if (this.isValidSessionId(sessionId)) {
276
325
  const dir = path.join(os.homedir(), ".ase", "session", sessionId);
@@ -289,8 +338,8 @@ export default class HookCommand {
289
338
  return input.session_id ?? input.sessionId ?? "";
290
339
  }
291
340
  /* read session id from stdin JSON payload */
292
- readSessionIdFromStdin() {
293
- const stdin = fs.readFileSync(0, "utf8");
341
+ async readSessionIdFromStdin() {
342
+ const stdin = await this.readStdin();
294
343
  const input = this.parseJSON(stdin, v.object({
295
344
  session_id: v.optional(v.string()),
296
345
  sessionId: v.optional(v.string())
@@ -319,17 +368,12 @@ export default class HookCommand {
319
368
  /* the edit-capable skills whose active state lets the pre-tool-use
320
369
  hook auto-approve subsequent "Edit" invocations */
321
370
  editCapableSkills = ["ase-code-lint", "ase-docs-proofread"];
322
- /* handler for "ase hook pre-tool-use" (both tools) */
323
- doPreToolUse(tool) {
324
- const spec = toolSpecs[tool];
325
- /* read tool invocation information */
326
- const stdin = fs.readFileSync(0, "utf8");
327
- const input = this.parseJSON(stdin, v.looseObject({
328
- session_id: v.optional(v.string()),
329
- sessionId: v.optional(v.string())
330
- }));
331
- /* determine whether to auto-approve the tool invocation
332
- (field names and value shapes differ between tools) */
371
+ /* determine whether an ASE tool invocation described by the parsed
372
+ hook input should be auto-approved, and (if so) the human-readable
373
+ reason. The input field names and value shapes differ between
374
+ tools, but the decision logic is shared by the "pre-tool-use" and
375
+ "permission-request" handlers. */
376
+ decideApproval(spec, input) {
333
377
  const toolName = typeof input[spec.toolNameField] === "string" ?
334
378
  input[spec.toolNameField] : "";
335
379
  let toolInput = {};
@@ -341,32 +385,43 @@ export default class HookCommand {
341
385
  }));
342
386
  else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
343
387
  toolInput = rawInput;
344
- let approve = false;
345
- let reason = "";
346
- if (toolName === spec.bashToolName && /^ase(\s|$)/.test(toolInput.command ?? "")) {
347
- approve = true;
348
- reason = "ASE CLI invocation auto-approved";
349
- }
350
- else if (toolName === "Skill" && /^(?:ase:)?ase-.+/.test(toolInput.skill ?? "")) {
351
- approve = true;
352
- reason = "ASE skill invocation auto-approved";
353
- }
354
- else if (spec.mcpToolNamePattern.test(toolName)) {
355
- approve = true;
356
- reason = "ASE MCP tool invocation auto-approved";
357
- }
358
- else if (spec.addonMcpToolNamePattern.test(toolName)) {
359
- approve = true;
360
- reason = "ASE addon MCP tool invocation auto-approved";
361
- }
388
+ if (toolName === spec.bashToolName && /^ase(\s|$)/.test(toolInput.command ?? ""))
389
+ return { approve: true, reason: "ASE CLI invocation auto-approved" };
390
+ else if (toolName === "Skill" && /^(?:ase:)?ase-.+/.test(toolInput.skill ?? ""))
391
+ return { approve: true, reason: "ASE skill invocation auto-approved" };
392
+ else if (spec.mcpToolNamePattern.test(toolName))
393
+ return { approve: true, reason: "ASE MCP tool invocation auto-approved" };
394
+ else if (spec.addonMcpToolNamePattern.test(toolName))
395
+ return { approve: true, reason: "ASE addon MCP tool invocation auto-approved" };
362
396
  else if (toolName === "Edit") {
363
397
  const sessionId = this.pickSessionId(input);
364
398
  const activeSkill = this.readActiveSkill(sessionId);
365
- if (this.editCapableSkills.includes(activeSkill)) {
366
- approve = true;
367
- reason = `${activeSkill}: edit auto-approved for active edit-capable skill`;
368
- }
399
+ if (this.editCapableSkills.includes(activeSkill))
400
+ return { approve: true, reason: `${activeSkill}: edit auto-approved for active edit-capable skill` };
369
401
  }
402
+ return { approve: false, reason: "" };
403
+ }
404
+ /* handler for "ase hook pre-tool-use" (all tools).
405
+ For Claude Code and Copilot CLI this is where ASE tool
406
+ invocations are auto-approved (via "permissionDecision: allow").
407
+ OpenAI Codex CLI rejects that mechanism in "PreToolUse", so for
408
+ Codex this handler stays silent and approval is granted in the
409
+ separate "permission-request" handler instead -- the handler must
410
+ still drain stdin, as Codex treats a non-draining hook as a hard
411
+ error. */
412
+ async doPreToolUse(tool) {
413
+ const spec = toolSpecs[tool];
414
+ /* read tool invocation information */
415
+ const stdin = await this.readStdin();
416
+ const input = this.parseJSON(stdin, v.looseObject({
417
+ session_id: v.optional(v.string()),
418
+ sessionId: v.optional(v.string())
419
+ }));
420
+ /* Codex auto-approves through "PermissionRequest", not here */
421
+ if (spec.approvalEvent !== "PreToolUse")
422
+ return 0;
423
+ /* determine whether to auto-approve the tool invocation */
424
+ const { approve, reason } = this.decideApproval(spec, input);
370
425
  /* emit permission decision (or stay silent to defer to default flow).
371
426
  Claude Code expects the decision nested in "hookSpecificOutput";
372
427
  Copilot CLI expects flat top-level fields. */
@@ -381,14 +436,42 @@ export default class HookCommand {
381
436
  "permissionDecision": "allow",
382
437
  "permissionDecisionReason": reason
383
438
  };
384
- process.stdout.write(JSON.stringify(payload));
439
+ await this.writeStdout(JSON.stringify(payload));
440
+ }
441
+ return 0;
442
+ }
443
+ /* handler for "ase hook permission-request" (OpenAI Codex CLI only).
444
+ Codex fires this event only when a tool invocation would otherwise
445
+ require interactive user approval, and -- unlike "PreToolUse" --
446
+ honors an auto-approval here through "decision.behavior: allow".
447
+ Staying silent (or returning a non-approval) defers to Codex's
448
+ normal approval flow. */
449
+ async doPermissionRequest(tool) {
450
+ const spec = toolSpecs[tool];
451
+ /* read tool invocation information */
452
+ const stdin = await this.readStdin();
453
+ const input = this.parseJSON(stdin, v.looseObject({
454
+ session_id: v.optional(v.string()),
455
+ sessionId: v.optional(v.string())
456
+ }));
457
+ /* determine whether to auto-approve the tool invocation */
458
+ const { approve } = this.decideApproval(spec, input);
459
+ /* emit the Codex "PermissionRequest" approval decision */
460
+ if (approve) {
461
+ const payload = {
462
+ "hookSpecificOutput": {
463
+ "hookEventName": "PermissionRequest",
464
+ "decision": { "behavior": "allow" }
465
+ }
466
+ };
467
+ await this.writeStdout(JSON.stringify(payload));
385
468
  }
386
469
  return 0;
387
470
  }
388
471
  /* parse and validate the --tool option */
389
472
  parseTool(value) {
390
- if (value !== "claude" && value !== "copilot")
391
- throw new Error(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
473
+ if (value !== "claude" && value !== "copilot" && value !== "codex")
474
+ throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
392
475
  return value;
393
476
  }
394
477
  /* register commands */
@@ -408,41 +491,49 @@ export default class HookCommand {
408
491
  hookCmd
409
492
  .command("session-start")
410
493
  .description("handle SessionStart hook event")
411
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
494
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
412
495
  .action(async (opts) => {
413
- process.exit(await this.doSessionStart(this.parseTool(opts.tool)));
496
+ process.exitCode = await this.doSessionStart(this.parseTool(opts.tool));
414
497
  });
415
498
  /* register CLI sub-command "ase hook session-end" */
416
499
  hookCmd
417
500
  .command("session-end")
418
501
  .description("handle SessionEnd hook event")
419
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
420
- .action((opts) => {
421
- process.exit(this.doSessionEnd(this.parseTool(opts.tool)));
502
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
503
+ .action(async (opts) => {
504
+ process.exitCode = await this.doSessionEnd(this.parseTool(opts.tool));
422
505
  });
423
506
  /* register CLI sub-command "ase hook pre-tool-use" */
424
507
  hookCmd
425
508
  .command("pre-tool-use")
426
509
  .description("handle tool PreToolUse hook event")
427
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
428
- .action((opts) => {
429
- process.exit(this.doPreToolUse(this.parseTool(opts.tool)));
510
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
511
+ .action(async (opts) => {
512
+ process.exitCode = await this.doPreToolUse(this.parseTool(opts.tool));
513
+ });
514
+ /* register CLI sub-command "ase hook permission-request" */
515
+ hookCmd
516
+ .command("permission-request")
517
+ .description("handle tool PermissionRequest hook event (Codex CLI)")
518
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
519
+ .action(async (opts) => {
520
+ process.exitCode = await this.doPermissionRequest(this.parseTool(opts.tool));
430
521
  });
431
522
  /* register CLI sub-command "ase hook user-prompt-submit" */
432
523
  hookCmd
433
524
  .command("user-prompt-submit")
434
525
  .description("handle UserPromptSubmit hook event (mark agent as busy)")
435
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
436
- .action((opts) => {
437
- process.exit(this.doUserPromptSubmit(this.parseTool(opts.tool)));
526
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
527
+ .action(async (opts) => {
528
+ process.exitCode = await this.doUserPromptSubmit(this.parseTool(opts.tool));
438
529
  });
439
530
  /* register CLI sub-command "ase hook stop" */
440
531
  hookCmd
441
532
  .command("stop")
442
533
  .description("handle Stop hook event (mark agent as ready)")
443
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
444
- .action((opts) => {
445
- process.exit(this.doStop(this.parseTool(opts.tool)));
534
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
535
+ .action(async (opts) => {
536
+ process.exitCode = await this.doStop(this.parseTool(opts.tool));
446
537
  });
447
538
  }
448
539
  }
@@ -16,6 +16,7 @@ import * as v from "valibot";
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
18
  import { Config, configSchema, ConfigMCP } from "./ase-config.js";
19
+ import { CompatMCP } from "./ase-compat.js";
19
20
  import { DiagramMCP } from "./ase-diagram.js";
20
21
  import { TaskMCP } from "./ase-task.js";
21
22
  import { MarkdownMCP } from "./ase-markdown.js";
@@ -236,6 +237,7 @@ export default class ServiceCommand {
236
237
  const buildMcpServer = () => {
237
238
  const mcp = new McpServer({ name: "ase", version: pkg.version });
238
239
  new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
240
+ new CompatMCP().register(mcp);
239
241
  new DiagramMCP().register(mcp);
240
242
  new TaskMCP(this.log).register(mcp);
241
243
  new MarkdownMCP().register(mcp);
package/dst/ase-setup.js CHANGED
@@ -13,8 +13,9 @@ import Table from "cli-table3";
13
13
  import chalk from "chalk";
14
14
  import Version from "./ase-version.js";
15
15
  const toolSpecs = {
16
- "claude": { cli: "claude", label: "Claude Code" },
17
- "copilot": { cli: "copilot", label: "Copilot CLI" }
16
+ "claude": { cli: "claude", label: "Claude Code", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
17
+ "copilot": { cli: "copilot", label: "Copilot CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
18
+ "codex": { cli: "codex", label: "OpenAI Codex CLI", pInstall: "add", pRemove: "remove", pUpdate: "upgrade" }
18
19
  };
19
20
  /* CLI command "ase setup" */
20
21
  export default class SetupCommand {
@@ -162,7 +163,7 @@ export default class SetupCommand {
162
163
  const pkgdir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
163
164
  const source = dev ? path.resolve(pkgdir, "..") : pkgdir;
164
165
  await this.run(spec.cli, ["plugin", "marketplace", "add", source]);
165
- await this.run(spec.cli, ["plugin", "install", "ase@ase"], { retries: 3 });
166
+ await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase"], { retries: 3 });
166
167
  return 0;
167
168
  }
168
169
  /* handler for "ase setup update" (both tools) */
@@ -185,8 +186,8 @@ export default class SetupCommand {
185
186
  but there is no version change in the plugin manifest,
186
187
  so just re-install the plugin to let the tool update its copy */
187
188
  this.log.write("info", `setup: update[dev]: re-install ASE ${spec.label} plugin (origin: local)`);
188
- await this.run(spec.cli, ["plugin", "uninstall", "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
189
- await this.run(spec.cli, ["plugin", "install", "ase@ase"], { retries: 3 });
189
+ await this.run(spec.cli, ["plugin", spec.pRemove, "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
190
+ await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase"], { retries: 3 });
190
191
  }
191
192
  else {
192
193
  /* perform NPM version check */
@@ -201,10 +202,17 @@ export default class SetupCommand {
201
202
  this.log.write("info", `setup: update: updating ASE CLI tool: ${current} -> ${latest}`);
202
203
  const updateCmd = await this.npmCmd(["update", "-g", "@rse/ase"], true);
203
204
  await this.run(updateCmd.cmd, updateCmd.args);
204
- /* update ASE plugin */
205
+ /* update ASE plugin (refresh the marketplace snapshot, then
206
+ update the plugin itself; the OpenAI Codex CLI has no
207
+ "plugin update" subcommand, so re-install it instead) */
205
208
  this.log.write("info", `setup: update: updating ASE ${spec.label} plugin`);
206
- await this.run(spec.cli, ["plugin", "marketplace", "update", "ase"]);
207
- await this.run(spec.cli, ["plugin", "update", "ase@ase"]);
209
+ await this.run(spec.cli, ["plugin", "marketplace", spec.pUpdate, "ase"]);
210
+ if (tool !== "codex")
211
+ await this.run(spec.cli, ["plugin", "update", "ase@ase"]);
212
+ else {
213
+ await this.run(spec.cli, ["plugin", spec.pRemove, "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
214
+ await this.run(spec.cli, ["plugin", spec.pInstall, "ase@ase"], { retries: 3 });
215
+ }
208
216
  }
209
217
  return 0;
210
218
  }
@@ -213,9 +221,11 @@ export default class SetupCommand {
213
221
  const spec = toolSpecs[tool];
214
222
  await this.ensureTool(spec.cli);
215
223
  this.log.write("info", `setup: enable: enabling ASE ${spec.label} plugin`);
224
+ /* the GitHub Copilot CLI and OpenAI Codex CLI have no "plugin
225
+ enable" subcommand, so (re-)install the plugin instead */
216
226
  const args = tool === "claude" ?
217
227
  ["plugin", "enable", "ase@ase"] :
218
- ["plugin", "install", "ase@ase"];
228
+ ["plugin", spec.pInstall, "ase@ase"];
219
229
  await this.run(spec.cli, args, { retries: tool === "claude" ? 1 : 3 });
220
230
  return 0;
221
231
  }
@@ -224,9 +234,11 @@ export default class SetupCommand {
224
234
  const spec = toolSpecs[tool];
225
235
  await this.ensureTool(spec.cli);
226
236
  this.log.write("info", `setup: disable: disabling ASE ${spec.label} plugin`);
237
+ /* the GitHub Copilot CLI and OpenAI Codex CLI have no "plugin
238
+ disable" subcommand, so uninstall the plugin instead */
227
239
  const args = tool === "claude" ?
228
240
  ["plugin", "disable", "ase@ase"] :
229
- ["plugin", "uninstall", "ase@ase"];
241
+ ["plugin", spec.pRemove, "ase@ase"];
230
242
  await this.run(spec.cli, args, { retries: tool === "claude" ? 1 : 3 });
231
243
  return 0;
232
244
  }
@@ -242,7 +254,7 @@ export default class SetupCommand {
242
254
  /* uninstall ASE plugin */
243
255
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
244
256
  `uninstalling ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote/bundled"})`);
245
- await this.run(spec.cli, ["plugin", "uninstall", "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
257
+ await this.run(spec.cli, ["plugin", spec.pRemove, "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
246
258
  await this.run(spec.cli, ["plugin", "marketplace", "remove", "ase"], { ignoreError: `ASE ${spec.label} plugin marketplace not registered` });
247
259
  /* uninstall ASE CLI tool (non-development only) */
248
260
  if (!dev) {
@@ -349,7 +361,7 @@ export default class SetupCommand {
349
361
  /* register an MCP server with the tool, supporting both the "stdio"
350
362
  (a local subprocess command) and "http" (a remote URL, optionally
351
363
  with HTTP headers) transports; the per-tool command line differs
352
- between Claude Code and GitHub Copilot CLI */
364
+ between Claude Code, GitHub Copilot CLI, and OpenAI Codex CLI */
353
365
  async mcpAdd(tool, name, env, transport) {
354
366
  const args = ["mcp", "add"];
355
367
  if (tool === "claude") {
@@ -366,7 +378,7 @@ export default class SetupCommand {
366
378
  args.push(name, transport.url);
367
379
  }
368
380
  }
369
- else {
381
+ else if (tool === "copilot") {
370
382
  /* GitHub Copilot CLI implies the stdio transport when the
371
383
  command is provided after "--"; only "http"/"sse" servers
372
384
  need an explicit "--transport" flag and take the URL as a
@@ -384,10 +396,27 @@ export default class SetupCommand {
384
396
  args.push(name, transport.url);
385
397
  }
386
398
  }
399
+ else {
400
+ /* OpenAI Codex CLI takes the server name as the first
401
+ positional argument and implies the stdio transport when the
402
+ command is provided after "--"; "http" servers take the URL
403
+ via the "--url" option (with optional "--header" flags) */
404
+ if (transport.type === "stdio") {
405
+ args.push(name);
406
+ for (const [key, val] of Object.entries(env))
407
+ args.push("--env", `${key}=${val}`);
408
+ args.push("--", ...transport.command);
409
+ }
410
+ else {
411
+ for (const [key, val] of Object.entries(transport.headers ?? {}))
412
+ args.push("--header", `${key}: ${val}`);
413
+ args.push(name, "--url", transport.url);
414
+ }
415
+ }
387
416
  await this.run(toolSpecs[tool].cli, args);
388
417
  }
389
418
  /* unregister an MCP server from the tool; the per-tool command line
390
- differs between Claude Code and GitHub Copilot CLI */
419
+ differs between Claude Code, GitHub Copilot CLI, and OpenAI Codex CLI */
391
420
  async mcpRemove(tool, name) {
392
421
  const args = tool === "claude" ?
393
422
  ["mcp", "remove", "--scope", "user", name] :
@@ -527,8 +556,8 @@ export default class SetupCommand {
527
556
  ];
528
557
  /* parse and validate the --tool option */
529
558
  parseTool(value) {
530
- if (value !== "claude" && value !== "copilot")
531
- throw new Error(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
559
+ if (value !== "claude" && value !== "copilot" && value !== "codex")
560
+ throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
532
561
  return value;
533
562
  }
534
563
  /* register commands */
@@ -551,7 +580,7 @@ export default class SetupCommand {
551
580
  setupCmd
552
581
  .command("install")
553
582
  .description("install the ASE plugin for a tool")
554
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
583
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
555
584
  .option("-d, --dev", "use local working copy instead of remote/bundled repository", devDflt)
556
585
  .action(async (opts) => {
557
586
  process.exit(await this.doInstall(this.parseTool(opts.tool), opts.dev));
@@ -560,7 +589,7 @@ export default class SetupCommand {
560
589
  setupCmd
561
590
  .command("update")
562
591
  .description("update the ASE tool and the ASE plugin for a tool")
563
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
592
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
564
593
  .option("-f, --force", "always perform the update, even if already at latest version", false)
565
594
  .option("-d, --dev", "use local working copy instead of remote/bundled repository", devDflt)
566
595
  .action(async (opts) => {
@@ -570,7 +599,7 @@ export default class SetupCommand {
570
599
  setupCmd
571
600
  .command("uninstall")
572
601
  .description("uninstall the ASE plugin for a tool and the ASE tool")
573
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
602
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
574
603
  .option("-d, --dev", "use local working copy instead of remote/bundled repository", devDflt)
575
604
  .action(async (opts) => {
576
605
  process.exit(await this.doUninstall(this.parseTool(opts.tool), opts.dev));
@@ -579,7 +608,7 @@ export default class SetupCommand {
579
608
  setupCmd
580
609
  .command("enable")
581
610
  .description("enable the ASE plugin for a tool")
582
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
611
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
583
612
  .action(async (opts) => {
584
613
  process.exit(await this.doEnable(this.parseTool(opts.tool)));
585
614
  });
@@ -587,7 +616,7 @@ export default class SetupCommand {
587
616
  setupCmd
588
617
  .command("disable")
589
618
  .description("disable the ASE plugin for a tool")
590
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
619
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
591
620
  .action(async (opts) => {
592
621
  process.exit(await this.doDisable(this.parseTool(opts.tool)));
593
622
  });
@@ -610,7 +639,7 @@ export default class SetupCommand {
610
639
  mcpCmd
611
640
  .command("activate [servers]")
612
641
  .description("activate pre-defined MCP servers (comma-separated list, or \"all\")")
613
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
642
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
614
643
  .action(async (servers, opts) => {
615
644
  process.exit(await this.doMcp("activate", this.parseTool(opts.tool), servers ?? "all"));
616
645
  });
@@ -618,7 +647,7 @@ export default class SetupCommand {
618
647
  mcpCmd
619
648
  .command("deactivate [servers]")
620
649
  .description("deactivate pre-defined MCP servers (comma-separated list, or \"all\")")
621
- .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
650
+ .option("-t, --tool <tool>", "target tool (\"claude\", \"copilot\", or \"codex\")", toolDflt)
622
651
  .action(async (servers, opts) => {
623
652
  process.exit(await this.doMcp("deactivate", this.parseTool(opts.tool), servers ?? "all"));
624
653
  });