@rse/ase 0.0.20 → 0.0.22

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.
@@ -0,0 +1,116 @@
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
+ }
@@ -20,7 +20,7 @@ const parseColorMode = (name) => (value) => {
20
20
  return value;
21
21
  };
22
22
  /* detect terminal column width */
23
- const detectTermWidth = () => {
23
+ export const detectTermWidth = () => {
24
24
  let width = 0;
25
25
  /* attempt 1: query environment variable */
26
26
  if (process.env.ASE_TERM_WIDTH !== undefined) {
@@ -37,7 +37,7 @@ const detectTermWidth = () => {
37
37
  return width;
38
38
  };
39
39
  /* detect terminal row height */
40
- const detectTermHeight = () => {
40
+ export const detectTermHeight = () => {
41
41
  let height = 0;
42
42
  /* attempt 1: query environment variable */
43
43
  if (process.env.ASE_TERM_HEIGHT !== undefined) {
@@ -112,6 +112,29 @@ const truncateAnsiLine = (line, budget) => {
112
112
  out += "\x1b[0m";
113
113
  return out;
114
114
  };
115
+ /* measure visible column width of a rendered line, ignoring ANSI escape
116
+ sequences (CSI ...m); mirrors the visibility model of truncateAnsiLine */
117
+ const visibleWidth = (line) => {
118
+ let visible = 0;
119
+ let i = 0;
120
+ while (i < line.length) {
121
+ const ch = line[i];
122
+ if (ch === "\x1b" && line[i + 1] === "[") {
123
+ let j = i + 2;
124
+ while (j < line.length && !/[A-Za-z]/.test(line[j]))
125
+ j++;
126
+ if (j < line.length) {
127
+ i = j + 1;
128
+ continue;
129
+ }
130
+ i++;
131
+ continue;
132
+ }
133
+ visible++;
134
+ i++;
135
+ }
136
+ return visible;
137
+ };
115
138
  /* pure rendering helper: turn a Mermaid source string plus options into
116
139
  a rendered Unicode/ASCII diagram string. Throws on render failure. */
117
140
  export const renderDiagram = (src, opts) => {
@@ -146,11 +169,33 @@ export const renderDiagram = (src, opts) => {
146
169
  const maxHeight = termHeight > 0 ? termHeight - opts.diagramClipY : 0;
147
170
  const trailingNL = out.endsWith("\n");
148
171
  let lines = (trailingNL ? out.slice(0, -1) : out).split("\n");
149
- if (maxWidth > 0)
172
+ let widthWarn = "";
173
+ let heightWarn = "";
174
+ if (maxWidth > 0) {
175
+ const widest = lines.reduce((m, l) => Math.max(m, visibleWidth(l)), 0);
176
+ if (widest > maxWidth)
177
+ widthWarn =
178
+ `ase diagram: WARNING: rendered diagram width ${widest} exceeds budget ${maxWidth}; ` +
179
+ "rightmost content was clipped. Please regenerate the Mermaid source to fit " +
180
+ `within ${maxWidth} chars by preferring a portrait orientation ` +
181
+ "(\"flowchart TB\", top-to-bottom) over landscape (\"LR\"/\"RL\"/\"BT\"), " +
182
+ "reducing siblings per row, abbreviating node labels, or restructuring " +
183
+ "into nested subgraph hierarchies.";
150
184
  lines = lines.map((l) => truncateAnsiLine(l, maxWidth));
151
- if (maxHeight > 0 && lines.length > maxHeight)
185
+ }
186
+ if (maxHeight > 0 && lines.length > maxHeight) {
187
+ const overflow = lines.length - maxHeight;
188
+ heightWarn =
189
+ `ase diagram: WARNING: rendered diagram height ${lines.length} exceeds budget ${maxHeight}; ` +
190
+ `bottom ${overflow} line(s) were clipped. Please regenerate the Mermaid source to fit ` +
191
+ `within ${maxHeight} lines by reducing depth or splitting into multiple diagrams.`;
152
192
  lines = lines.slice(0, maxHeight);
193
+ }
153
194
  out = lines.join("\n") + (trailingNL ? "\n" : "");
195
+ if (widthWarn !== "")
196
+ out += "\n" + widthWarn + "\n";
197
+ if (heightWarn !== "")
198
+ out += "\n" + heightWarn + "\n";
154
199
  }
155
200
  return out;
156
201
  };
@@ -0,0 +1,23 @@
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-hook.js CHANGED
@@ -8,6 +8,26 @@ import fs from "node:fs";
8
8
  import { execaSync } from "execa";
9
9
  import Version from "./ase-version.js";
10
10
  import { Config, configSchema, parseScope } from "./ase-config.js";
11
+ const toolSpecs = {
12
+ "claude": {
13
+ toolNameField: "tool_name",
14
+ toolInputField: "tool_input",
15
+ toolInputIsString: false,
16
+ bashToolName: "Bash",
17
+ mcpToolNamePattern: /^mcp__plugin_ase_ase__.+/,
18
+ preToolUseWrapped: true,
19
+ preToolUseEvent: "PreToolUse"
20
+ },
21
+ "copilot": {
22
+ toolNameField: "toolName",
23
+ toolInputField: "toolArgs",
24
+ toolInputIsString: true,
25
+ bashToolName: "bash",
26
+ mcpToolNamePattern: /^ase-.+/,
27
+ preToolUseWrapped: false,
28
+ preToolUseEvent: "preToolUse"
29
+ }
30
+ };
11
31
  /* CLI command "ase hook" */
12
32
  export default class HookCommand {
13
33
  log;
@@ -38,12 +58,13 @@ export default class HookCommand {
38
58
  return this.expandReferences(content, path.dirname(abs), next);
39
59
  });
40
60
  }
41
- /* handler for "ase hook session-start" */
42
- async doSessionStart() {
43
- /* determine plugin root */
44
- const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? "";
61
+ /* handler for "ase hook session-start" (both tools) */
62
+ async doSessionStart(tool) {
63
+ /* determine plugin root (env var name differs per tool) */
64
+ const pluginRootVar = tool === "copilot" ? "COPILOT_PLUGIN_ROOT" : "CLAUDE_PLUGIN_ROOT";
65
+ const pluginRoot = process.env[pluginRootVar] ?? "";
45
66
  if (pluginRoot === "")
46
- throw new Error("CLAUDE_PLUGIN_ROOT environment variable is not set");
67
+ throw new Error(`${pluginRootVar} environment variable is not set`);
47
68
  /* determine path to external files */
48
69
  const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
49
70
  const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
@@ -64,11 +85,12 @@ export default class HookCommand {
64
85
  if (process.env.ASE_SETUP_DEV !== undefined)
65
86
  versionHints.push("**NOTICE:** *development* setup");
66
87
  const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
67
- /* read session information */
88
+ /* read session information (Claude Code uses snake_case fields,
89
+ Copilot CLI uses camelCase fields) */
68
90
  const stdin = fs.readFileSync(0, "utf8");
69
91
  const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
70
92
  /* determine session id */
71
- const sessionId = input.session_id ?? "";
93
+ const sessionId = input.session_id ?? input.sessionId ?? "";
72
94
  /* establish config context */
73
95
  const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
74
96
  try {
@@ -107,14 +129,18 @@ export default class HookCommand {
107
129
  const val = cfg.get("agent.persona");
108
130
  if (typeof val === "string")
109
131
  persona = val;
110
- /* provide ASE information to Claude Code shell commands */
111
- const envFile = process.env.CLAUDE_ENV_FILE ?? "";
132
+ /* determine headless mode */
133
+ const headless = (process.env.ASE_HEADLESS ?? "false") === "true" ? "true" : "false";
134
+ /* provide ASE information to Claude Code shell commands
135
+ (Claude Code only -- Copilot CLI has no equivalent mechanism) */
136
+ const envFile = tool === "claude" ? (process.env.CLAUDE_ENV_FILE ?? "") : "";
112
137
  if (envFile !== "") {
113
138
  const script = `export ASE_VERSION="${versionCurrentPlugin}"\n` +
114
139
  `export ASE_USER_ID="${userId}"\n` +
115
140
  `export ASE_PROJECT_ID="${projectId}"\n` +
116
141
  `export ASE_TASK_ID="${taskId}"\n` +
117
- `export ASE_SESSION_ID="${sessionId}"\n`;
142
+ `export ASE_SESSION_ID="${sessionId}"\n` +
143
+ `export ASE_HEADLESS="${headless}"\n`;
118
144
  fs.appendFileSync(envFile, script, "utf8");
119
145
  }
120
146
  /* prepend ASE information to constitution markdown */
@@ -126,30 +152,49 @@ export default class HookCommand {
126
152
  `<ase-project-id>${projectId}</ase-project-id>\n` +
127
153
  `<ase-task-id>${taskId}</ase-task-id>\n` +
128
154
  `<ase-session-id>${sessionId}</ase-session-id>\n` +
155
+ `<ase-headless>${headless}</ase-headless>\n` +
129
156
  "\n" + md;
130
157
  /* expand all @<file> references manually */
131
158
  md = this.expandReferences(md, path.dirname(fileMd));
132
- fs.writeFileSync("/tmp/xxx", md, "utf8");
133
- /* inject markdown into session context */
134
- process.stdout.write(JSON.stringify({
159
+ /* inject markdown into session context.
160
+ Claude Code expects the context nested in "hookSpecificOutput";
161
+ Copilot CLI expects a flat top-level "additionalContext" field. */
162
+ const payload = tool === "claude" ? {
135
163
  "hookSpecificOutput": {
136
164
  "hookEventName": "SessionStart",
137
165
  "additionalContext": md
138
166
  }
139
- }));
167
+ } : {
168
+ "additionalContext": md
169
+ };
170
+ process.stdout.write(JSON.stringify(payload));
140
171
  return 0;
141
172
  }
142
- /* handler for "ase hook pre-tool-use" */
143
- doPreToolUse() {
173
+ /* handler for "ase hook pre-tool-use" (both tools) */
174
+ doPreToolUse(tool) {
175
+ const spec = toolSpecs[tool];
144
176
  /* read tool invocation information */
145
177
  const stdin = fs.readFileSync(0, "utf8");
146
178
  const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
147
- /* determine whether to auto-approve the tool invocation */
148
- const toolName = input.tool_name ?? "";
149
- const toolInput = input.tool_input ?? {};
179
+ /* determine whether to auto-approve the tool invocation
180
+ (field names and value shapes differ between tools) */
181
+ const toolName = typeof input[spec.toolNameField] === "string" ?
182
+ input[spec.toolNameField] : "";
183
+ let toolInput = {};
184
+ const rawInput = input[spec.toolInputField];
185
+ if (spec.toolInputIsString && typeof rawInput === "string") {
186
+ try {
187
+ toolInput = JSON.parse(rawInput);
188
+ }
189
+ catch (_e) {
190
+ /* best-effort: leave toolInput empty on parse failure */
191
+ }
192
+ }
193
+ else if (!spec.toolInputIsString && typeof rawInput === "object" && rawInput !== null)
194
+ toolInput = rawInput;
150
195
  let approve = false;
151
196
  let reason = "";
152
- if (toolName === "Bash" && /^ase(\s|$)/.test(toolInput.command ?? "")) {
197
+ if (toolName === spec.bashToolName && /^ase(\s|$)/.test(toolInput.command ?? "")) {
153
198
  approve = true;
154
199
  reason = "ASE CLI invocation auto-approved";
155
200
  }
@@ -157,28 +202,43 @@ export default class HookCommand {
157
202
  approve = true;
158
203
  reason = "ASE skill invocation auto-approved";
159
204
  }
160
- else if (/^mcp__plugin_ase_ase__.+/.test(toolName)) {
205
+ else if (spec.mcpToolNamePattern.test(toolName)) {
161
206
  approve = true;
162
207
  reason = "ASE MCP tool invocation auto-approved";
163
208
  }
164
- /* emit permission decision (or stay silent to defer to default flow) */
209
+ /* emit permission decision (or stay silent to defer to default flow).
210
+ Claude Code expects the decision nested in "hookSpecificOutput";
211
+ Copilot CLI expects flat top-level fields. */
165
212
  if (approve) {
166
- process.stdout.write(JSON.stringify({
213
+ const payload = spec.preToolUseWrapped ? {
167
214
  "hookSpecificOutput": {
168
- "hookEventName": "PreToolUse",
215
+ "hookEventName": spec.preToolUseEvent,
169
216
  "permissionDecision": "allow",
170
217
  "permissionDecisionReason": reason
171
218
  }
172
- }));
219
+ } : {
220
+ "permissionDecision": "allow",
221
+ "permissionDecisionReason": reason
222
+ };
223
+ process.stdout.write(JSON.stringify(payload));
173
224
  }
174
225
  return 0;
175
226
  }
227
+ /* parse and validate the --tool option */
228
+ parseTool(value) {
229
+ if (value !== "claude" && value !== "copilot")
230
+ throw new Error(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
231
+ return value;
232
+ }
176
233
  /* register commands */
177
234
  register(program) {
235
+ /* default for --tool derived from ASE_TOOL environment variable */
236
+ const envTool = process.env.ASE_TOOL ?? "";
237
+ const toolDflt = envTool !== "" ? envTool : "claude";
178
238
  /* register CLI top-level command "ase hook" */
179
239
  const hookCmd = program
180
240
  .command("hook")
181
- .description("Claude Code hook entry points")
241
+ .description("Claude Code and Copilot CLI hook entry points")
182
242
  .action(() => {
183
243
  hookCmd.outputHelp();
184
244
  process.exit(1);
@@ -186,16 +246,18 @@ export default class HookCommand {
186
246
  /* register CLI sub-command "ase hook session-start" */
187
247
  hookCmd
188
248
  .command("session-start")
189
- .description("handle Claude Code SessionStart hook event")
190
- .action(async () => {
191
- process.exit(await this.doSessionStart());
249
+ .description("handle SessionStart hook event")
250
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
251
+ .action(async (opts) => {
252
+ process.exit(await this.doSessionStart(this.parseTool(opts.tool)));
192
253
  });
193
254
  /* register CLI sub-command "ase hook pre-tool-use" */
194
255
  hookCmd
195
256
  .command("pre-tool-use")
196
- .description("handle Claude Code PreToolUse hook event")
197
- .action(() => {
198
- process.exit(this.doPreToolUse());
257
+ .description("handle tool PreToolUse hook event")
258
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
259
+ .action((opts) => {
260
+ process.exit(this.doPreToolUse(this.parseTool(opts.tool)));
199
261
  });
200
262
  }
201
263
  }
@@ -0,0 +1,109 @@
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-log.js CHANGED
@@ -61,7 +61,7 @@ export default class Log {
61
61
  line += `[${levels[idx].name.toUpperCase()}]`;
62
62
  line += `: ${msg}\n`;
63
63
  if (this._logFile === "-")
64
- process.stdout.write(line);
64
+ process.stderr.write(line);
65
65
  else if (this.stream !== null)
66
66
  this.stream.write(line);
67
67
  }
package/dst/ase-mcp.js CHANGED
@@ -86,54 +86,102 @@ export default class MCPCommand {
86
86
  /* bridge stdio to a Streamable HTTP MCP endpoint on the local service */
87
87
  async runBridge() {
88
88
  /* ensure the service is running */
89
- const { port } = await this.ensureService();
90
- /* create MCP HTTP client */
91
- const url = new URL(`http://${HOST}:${port}/mcp`);
92
- const client = new StreamableHTTPClientTransport(url);
93
- /* create MCP STDIO server */
89
+ let { port } = await this.ensureService();
90
+ /* create MCP STDIO server (lives for the entire bridge lifetime) */
94
91
  const server = new StdioServerTransport();
95
- /* handle shutdown */
96
- let closed = false;
92
+ /* track active client and bridge-level closed state */
93
+ let client = null;
94
+ let closedByUs = false; /* set when we initiated the client close */
95
+ let bridgeDone = false; /* set when stdio side closes */
96
+ /* cleanly shut down the whole bridge */
97
97
  const shutdown = async () => {
98
- if (closed)
98
+ if (bridgeDone)
99
99
  return;
100
- closed = true;
100
+ bridgeDone = true;
101
+ closedByUs = true;
101
102
  await Promise.allSettled([
102
103
  server.close(),
103
- client.close()
104
+ client?.close()
104
105
  ]);
105
106
  };
106
- /* connect server to client (forward transport) */
107
+ /* (re-)connect the HTTP client to the service */
108
+ const connectClient = async () => {
109
+ const url = new URL(`http://${HOST}:${port}/mcp`);
110
+ const next = new StreamableHTTPClientTransport(url);
111
+ client = next;
112
+ next.onmessage = (msg) => {
113
+ server.send(msg).catch((_err) => {
114
+ const err = _err instanceof Error ? _err : new Error(String(_err));
115
+ this.log.write("error", `mcp: stdout send: ${err.message}`);
116
+ });
117
+ };
118
+ next.onerror = (err) => {
119
+ this.log.write("error", `mcp: http: ${err.message}`);
120
+ };
121
+ /* service closed the connection — try to recover */
122
+ next.onclose = () => {
123
+ if (closedByUs || bridgeDone)
124
+ return;
125
+ this.log.write("warning", "mcp: http connection lost — reconnecting");
126
+ reconnect().catch(() => { });
127
+ };
128
+ await next.start();
129
+ };
130
+ /* reconnect loop: restart service if needed, then reconnect client */
131
+ const reconnect = async (attempt = 0) => {
132
+ const delay = Math.min(500 * 2 ** attempt, 10000);
133
+ await new Promise((resolve) => setTimeout(resolve, delay));
134
+ if (bridgeDone)
135
+ return;
136
+ try {
137
+ const ctx = await this.ensureService();
138
+ port = ctx.port;
139
+ closedByUs = true;
140
+ await client?.close();
141
+ closedByUs = false;
142
+ await connectClient();
143
+ this.log.write("info", "mcp: reconnected to service");
144
+ }
145
+ catch (_err) {
146
+ const err = _err instanceof Error ? _err : new Error(String(_err));
147
+ this.log.write("error", `mcp: reconnect failed: ${err.message}`);
148
+ reconnect(attempt + 1).catch(() => { });
149
+ }
150
+ };
151
+ /* wire stdio server */
107
152
  server.onmessage = (msg) => {
108
- client.send(msg).catch((_err) => {
153
+ client?.send(msg).catch((_err) => {
109
154
  const err = _err instanceof Error ? _err : new Error(String(_err));
110
155
  this.log.write("error", `mcp: http send: ${err.message}`);
111
156
  });
112
157
  };
113
- server.onerror = (_err) => {
114
- const err = _err instanceof Error ? _err : new Error(String(_err));
158
+ server.onerror = (err) => {
115
159
  this.log.write("error", `mcp: stdio: ${err.message}`);
116
160
  };
117
161
  server.onclose = () => {
118
162
  shutdown().catch(() => { });
119
163
  };
120
- /* connect client to server (backward transport) */
121
- client.onmessage = (msg) => {
122
- server.send(msg).catch((_err) => {
123
- const err = _err instanceof Error ? _err : new Error(String(_err));
124
- this.log.write("error", `mcp: stdout send: ${err.message}`);
125
- });
126
- };
127
- client.onerror = (_err) => {
128
- const err = _err instanceof Error ? _err : new Error(String(_err));
129
- this.log.write("error", `mcp: http: ${err.message}`);
130
- };
131
- client.onclose = () => {
132
- shutdown().catch(() => { });
133
- };
134
- /* start server and client */
164
+ /* start server and initial client */
135
165
  await server.start();
136
- await client.start();
166
+ await connectClient();
167
+ /* periodically probe the service; trigger reconnect if it is gone */
168
+ const HEALTH_INTERVAL_MS = 30_000;
169
+ let reconnecting = false;
170
+ const healthTimer = setInterval(async () => {
171
+ if (bridgeDone || reconnecting)
172
+ return;
173
+ try {
174
+ const { projectId } = this.loadContext();
175
+ const match = await probe(port, projectId);
176
+ if (match !== true) {
177
+ reconnecting = true;
178
+ this.log.write("warning", "mcp: health check failed — reconnecting");
179
+ reconnect().catch(() => { }).finally(() => { reconnecting = false; });
180
+ }
181
+ }
182
+ catch { /* ignore probe errors */ }
183
+ }, HEALTH_INTERVAL_MS);
184
+ healthTimer.unref();
137
185
  /* await stdio to be closed */
138
186
  await new Promise((resolve) => {
139
187
  const done = () => resolve();
@@ -141,6 +189,7 @@ export default class MCPCommand {
141
189
  process.stdin.once("close", done);
142
190
  });
143
191
  /* shutdown services */
192
+ clearInterval(healthTimer);
144
193
  await shutdown();
145
194
  return 0;
146
195
  }
@@ -17,7 +17,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
18
  import { z } from "zod";
19
19
  import { Config, configSchema, parseScope } from "./ase-config.js";
20
- import { renderDiagram } from "./ase-diagram.js";
20
+ import { renderDiagram, detectTermWidth, detectTermHeight } from "./ase-diagram.js";
21
21
  import { taskLoad, taskSave, taskDelete, taskList } from "./ase-task.js";
22
22
  import pkg from "../package.json" with { type: "json" };
23
23
  const SERVE_ENV = "ASE_SERVICE_SERVE";
@@ -231,10 +231,10 @@ export default class ServiceCommand {
231
231
  .describe("extra horizontal clipping: subtract this many characters from `terminalWidth`"),
232
232
  diagramClipY: z.number().int().min(0).default(0)
233
233
  .describe("extra vertical clipping: subtract this many lines from `terminalHeight`"),
234
- terminalWidth: z.number().int().min(0).default(0)
235
- .describe("terminal width in characters; 0 disables horizontal clipping"),
236
- terminalHeight: z.number().int().min(0).default(0)
237
- .describe("terminal height in lines; 0 disables vertical clipping")
234
+ terminalWidth: z.number().int().min(0).default(detectTermWidth())
235
+ .describe("terminal width in characters; 0 disables horizontal clipping; defaults to ASE_TERM_WIDTH env var if set"),
236
+ terminalHeight: z.number().int().min(0).default(detectTermHeight())
237
+ .describe("terminal height in lines; 0 disables vertical clipping; defaults to ASE_TERM_HEIGHT env var if set")
238
238
  }
239
239
  }, async (args) => {
240
240
  try {
@@ -326,7 +326,7 @@ export default class ServiceCommand {
326
326
  const message = err instanceof Error ? err.message : String(err);
327
327
  return {
328
328
  isError: true,
329
- content: [{ type: "text", text: `task_save: FAILED: ${message}` }]
329
+ content: [{ type: "text", text: `task_save: ERROR: ${message}` }]
330
330
  };
331
331
  }
332
332
  });
@@ -394,7 +394,7 @@ export default class ServiceCommand {
394
394
  const val = cfg.get("agent.persona");
395
395
  if (val === undefined)
396
396
  return {
397
- content: [{ type: "text", text: "" }]
397
+ content: [{ type: "text", text: "engineer" }]
398
398
  };
399
399
  const text = String(isScalar(val) ? val.value : val);
400
400
  return {
@@ -412,7 +412,7 @@ export default class ServiceCommand {
412
412
  mcp.registerTool("task_id", {
413
413
  title: "ASE task id get/set",
414
414
  description: "Get or set the active ASE task `id` for a given `session`. " +
415
- "If `id` is provided, it set the task id in the given `session`, " +
415
+ "If `id` is provided, it sets the task id in the given `session`, " +
416
416
  "otherwise it returns the current task `id` of the `session`.",
417
417
  inputSchema: {
418
418
  id: z.string().optional()
@@ -467,6 +467,7 @@ export default class ServiceCommand {
467
467
  method: "GET",
468
468
  path: "/stop",
469
469
  handler: (_request, h) => {
470
+ this.log.write("info", "service: stop requested");
470
471
  setImmediate(async () => {
471
472
  await server.stop({ timeout: 1000 });
472
473
  process.exit(0);
@@ -475,6 +476,21 @@ export default class ServiceCommand {
475
476
  }
476
477
  });
477
478
  const mcpHandler = async (request, h, body) => {
479
+ const b = body;
480
+ const bParams = b?.params;
481
+ const bMethod = typeof b?.method === "string" ? b.method : null;
482
+ const bName = typeof bParams?.name === "string" ? bParams.name : null;
483
+ const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
484
+ let bodyInfo = "";
485
+ if (bMethod !== null) {
486
+ bodyInfo = ` [${bMethod}]`;
487
+ if (bName !== null) {
488
+ bodyInfo += ` ${bName}`;
489
+ if (bArgs !== null)
490
+ bodyInfo += ` ${JSON.stringify(bArgs)}`;
491
+ }
492
+ }
493
+ this.log.write("info", `mcp: ${request.method.toUpperCase()} ${request.path}${bodyInfo}`);
478
494
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
479
495
  const mcp = buildMcpServer();
480
496
  request.raw.res.on("close", () => {
@@ -531,6 +547,7 @@ export default class ServiceCommand {
531
547
  try {
532
548
  await server.start();
533
549
  persistPort(ctx.svc, ctx.port);
550
+ this.log.write("info", `service: listening on port ${ctx.port}`);
534
551
  }
535
552
  catch (err) {
536
553
  const e = err;
@@ -552,6 +569,7 @@ export default class ServiceCommand {
552
569
  return;
553
570
  if (Date.now() - lastActivity > IDLE_MS) {
554
571
  stopping = true;
572
+ this.log.write("info", "service: idle timeout reached, stopping");
555
573
  try {
556
574
  await server.stop({ timeout: 1000 });
557
575
  clearPort(ctx.svc);
package/dst/ase-setup.js CHANGED
@@ -8,6 +8,10 @@ import { fileURLToPath } from "node:url";
8
8
  import { execa } from "execa";
9
9
  import which from "which";
10
10
  import Version from "./ase-version.js";
11
+ const toolSpecs = {
12
+ "claude": { cli: "claude", label: "Claude Code" },
13
+ "copilot": { cli: "copilot", label: "Copilot CLI" }
14
+ };
11
15
  /* CLI command "ase setup" */
12
16
  export default class SetupCommand {
13
17
  log;
@@ -65,21 +69,24 @@ export default class SetupCommand {
65
69
  }
66
70
  }
67
71
  }
68
- /* handler for "ase setup install" */
69
- async doInstall(dev) {
72
+ /* handler for "ase setup install" (both tools) */
73
+ async doInstall(tool, dev) {
74
+ const spec = toolSpecs[tool];
70
75
  await this.ensureTool("npm");
71
- await this.ensureTool("claude");
76
+ await this.ensureTool(spec.cli);
72
77
  this.log.write("info", `setup: install${dev ? "[dev]" : ""}: ` +
73
- `installing ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
74
- const source = dev ? process.cwd() : "rse/ase";
75
- await this.run("claude", ["plugin", "marketplace", "add", source]);
76
- await this.run("claude", ["plugin", "install", "ase@ase"], { retries: 3 });
78
+ `installing ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote"})`);
79
+ const basedir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
80
+ const source = dev ? basedir : "rse/ase";
81
+ await this.run(spec.cli, ["plugin", "marketplace", "add", source]);
82
+ await this.run(spec.cli, ["plugin", "install", "ase@ase"], { retries: 3 });
77
83
  return 0;
78
84
  }
79
- /* handler for "ase setup update" */
80
- async doUpdate(force, dev) {
85
+ /* handler for "ase setup update" (both tools) */
86
+ async doUpdate(tool, force, dev) {
87
+ const spec = toolSpecs[tool];
81
88
  await this.ensureTool("npm");
82
- await this.ensureTool("claude");
89
+ await this.ensureTool(spec.cli);
83
90
  /* best-effort stop of background service */
84
91
  this.log.write("info", `setup: update${dev ? "[dev]" : ""}: ` +
85
92
  "stopping potentially running ASE service");
@@ -92,10 +99,10 @@ export default class SetupCommand {
92
99
  await this.run("npm", ["start", "build"], { cwd: tooldir });
93
100
  /* in development mode the local plugin files are already current
94
101
  but there is no version change in the plugin manifest,
95
- so just re-install the plugin to let Claude Code update its copy */
96
- this.log.write("info", "setup: update[dev]: re-install ASE Claude Code plugin (origin: local)");
97
- await this.run("claude", ["plugin", "uninstall", "ase@ase"], { ignoreError: "ASE Claude Code plugin not installed" });
98
- await this.run("claude", ["plugin", "install", "ase@ase"], { retries: 3 });
102
+ so just re-install the plugin to let the tool update its copy */
103
+ this.log.write("info", `setup: update[dev]: re-install ASE ${spec.label} plugin (origin: local)`);
104
+ await this.run(spec.cli, ["plugin", "uninstall", "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
105
+ await this.run(spec.cli, ["plugin", "install", "ase@ase"], { retries: 3 });
99
106
  }
100
107
  else {
101
108
  /* perform NPM version check */
@@ -108,26 +115,27 @@ export default class SetupCommand {
108
115
  /* update ASE CLI tool */
109
116
  this.log.write("info", `setup: update: updating ASE CLI tool: ${current} -> ${latest}`);
110
117
  await this.run("npm", ["update", "-g", "@rse/ase"]);
111
- /* update ASE Claude Code plugin */
112
- this.log.write("info", "setup: update: updating ASE Claude Code plugin");
113
- await this.run("claude", ["plugin", "marketplace", "update", "ase"]);
114
- await this.run("claude", ["plugin", "update", "ase@ase"]);
118
+ /* update ASE plugin */
119
+ this.log.write("info", `setup: update: updating ASE ${spec.label} plugin`);
120
+ await this.run(spec.cli, ["plugin", "marketplace", "update", "ase"]);
121
+ await this.run(spec.cli, ["plugin", "update", "ase@ase"]);
115
122
  }
116
123
  return 0;
117
124
  }
118
- /* handler for "ase setup uninstall" */
119
- async doUninstall(dev) {
125
+ /* handler for "ase setup uninstall" (both tools) */
126
+ async doUninstall(tool, dev) {
127
+ const spec = toolSpecs[tool];
120
128
  await this.ensureTool("npm");
121
- await this.ensureTool("claude");
129
+ await this.ensureTool(spec.cli);
122
130
  /* best-effort stop of background service */
123
131
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
124
132
  "stopping potentially running ASE service");
125
133
  await this.run("ase", ["service", "stop"], { quiet: true });
126
- /* uninstall ASE Claude Code plugin */
134
+ /* uninstall ASE plugin */
127
135
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
128
- `uninstalling ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
129
- await this.run("claude", ["plugin", "uninstall", "ase@ase"], { ignoreError: "ASE Claude Code plugin not installed" });
130
- await this.run("claude", ["plugin", "marketplace", "remove", "ase"], { ignoreError: "ASE Claude Code plugin marketplace not registered" });
136
+ `uninstalling ASE ${spec.label} plugin (origin: ${dev ? "local" : "remote"})`);
137
+ await this.run(spec.cli, ["plugin", "uninstall", "ase@ase"], { ignoreError: `ASE ${spec.label} plugin not installed` });
138
+ await this.run(spec.cli, ["plugin", "marketplace", "remove", "ase"], { ignoreError: `ASE ${spec.label} plugin marketplace not registered` });
131
139
  /* uninstall ASE CLI tool (non-development only) */
132
140
  if (!dev) {
133
141
  this.log.write("info", "setup: uninstall: uninstalling ASE CLI tool (origin: remote)");
@@ -135,11 +143,20 @@ export default class SetupCommand {
135
143
  }
136
144
  return 0;
137
145
  }
146
+ /* parse and validate the --tool option */
147
+ parseTool(value) {
148
+ if (value !== "claude" && value !== "copilot")
149
+ throw new Error(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
150
+ return value;
151
+ }
138
152
  /* register commands */
139
153
  register(program) {
140
154
  /* default for --dev derived from ASE_SETUP_DEV environment variable */
141
155
  const envDev = process.env.ASE_SETUP_DEV ?? "";
142
156
  const devDflt = envDev !== "" && envDev !== "0" && envDev.toLowerCase() !== "false";
157
+ /* default for --tool derived from ASE_TOOL environment variable */
158
+ const envTool = process.env.ASE_TOOL ?? "";
159
+ const toolDflt = envTool !== "" ? envTool : "claude";
143
160
  /* register CLI top-level command "ase setup" */
144
161
  const setupCmd = program
145
162
  .command("setup")
@@ -151,27 +168,30 @@ export default class SetupCommand {
151
168
  /* register CLI sub-command "ase setup install" */
152
169
  setupCmd
153
170
  .command("install")
154
- .description("install the ASE Claude Code plugin")
171
+ .description("install the ASE plugin for a tool")
172
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
155
173
  .option("-d, --dev", "use local working copy instead of remote repository", devDflt)
156
174
  .action(async (opts) => {
157
- process.exit(await this.doInstall(opts.dev));
175
+ process.exit(await this.doInstall(this.parseTool(opts.tool), opts.dev));
158
176
  });
159
177
  /* register CLI sub-command "ase setup update" */
160
178
  setupCmd
161
179
  .command("update")
162
- .description("update the ASE tool and the ASE Claude Code plugin")
180
+ .description("update the ASE tool and the ASE plugin for a tool")
181
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
163
182
  .option("-f, --force", "always perform the update, even if already at latest version", false)
164
183
  .option("-d, --dev", "use local working copy instead of remote repository", devDflt)
165
184
  .action(async (opts) => {
166
- process.exit(await this.doUpdate(opts.force, opts.dev));
185
+ process.exit(await this.doUpdate(this.parseTool(opts.tool), opts.force, opts.dev));
167
186
  });
168
187
  /* register CLI sub-command "ase setup uninstall" */
169
188
  setupCmd
170
189
  .command("uninstall")
171
- .description("uninstall the ASE Claude Code plugin and the ASE tool")
190
+ .description("uninstall the ASE plugin for a tool and the ASE tool")
191
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
172
192
  .option("-d, --dev", "use local working copy instead of remote repository", devDflt)
173
193
  .action(async (opts) => {
174
- process.exit(await this.doUninstall(opts.dev));
194
+ process.exit(await this.doUninstall(this.parseTool(opts.tool), opts.dev));
175
195
  });
176
196
  }
177
197
  }
@@ -161,11 +161,21 @@ export default class StatuslineCommand {
161
161
  constructor(log) {
162
162
  this.log = log;
163
163
  }
164
+ /* parse and validate the --tool option */
165
+ parseTool(value) {
166
+ if (value !== "claude" && value !== "copilot")
167
+ throw new Error(`invalid --tool value: "${value}" (expected "claude" or "copilot")`);
168
+ return value;
169
+ }
164
170
  /* register commands */
165
171
  register(program) {
172
+ /* default for --tool derived from ASE_TOOL environment variable */
173
+ const envTool = process.env.ASE_TOOL ?? "";
174
+ const toolDflt = envTool !== "" ? envTool : "claude";
166
175
  program
167
176
  .command("statusline")
168
- .description("Render Claude Code statusline from stdin JSON")
177
+ .description("Render Claude Code or GitHub Copilot CLI statusline from stdin JSON")
178
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
169
179
  .option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseInteger("--width"), 0)
170
180
  .option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseInteger("--margin"), 2)
171
181
  .option("--no-icons", "disable icons in placeholder rendering")
@@ -175,6 +185,8 @@ export default class StatuslineCommand {
175
185
  "(color: black, red, green, yellow, blue, magenta, cyan, white, default) " +
176
186
  "(default: single line \"%m %e %t\")")
177
187
  .action(async (lines, opts) => {
188
+ /* validate target tool */
189
+ const tool = this.parseTool(opts.tool);
178
190
  /* read all of stdin */
179
191
  const input = await readStdin();
180
192
  /* parse JSON data */
@@ -187,6 +199,13 @@ export default class StatuslineCommand {
187
199
  this.log.write("error", `statusline: invalid JSON on stdin: ${message}`);
188
200
  process.exit(1);
189
201
  }
202
+ /* normalize Copilot CLI's top-level "cwd" into the
203
+ "workspace.current_dir" structure shared with Claude Code */
204
+ if (tool === "copilot"
205
+ && (data.workspace?.current_dir === undefined || data.workspace.current_dir === "")
206
+ && typeof data.cwd === "string" && data.cwd !== "") {
207
+ data.workspace = { ...(data.workspace ?? {}), current_dir: data.cwd };
208
+ }
190
209
  /* determine effective terminal width and budget */
191
210
  const width = opts.width > 0 ? opts.width : detectTermWidth();
192
211
  const budget = width > 0 ? width - 2 * opts.margin : 0;
@@ -228,7 +247,7 @@ export default class StatuslineCommand {
228
247
  let sessCache = null;
229
248
  const getSession = () => {
230
249
  if (sessCache === null)
231
- sessCache = data.session_name ?? data.session_id ?? "unknown";
250
+ sessCache = data.session_id ?? "unknown";
232
251
  return sessCache;
233
252
  };
234
253
  let cfgCache = null;
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.20",
9
+ "version": "0.0.22",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,9 +18,8 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.59.1",
22
- "@typescript-eslint/eslint-plugin": "8.59.1",
23
- "eslint-plugin-n": "17.24.0",
21
+ "@typescript-eslint/parser": "8.59.2",
22
+ "@typescript-eslint/eslint-plugin": "8.59.2",
24
23
  "eslint-plugin-promise": "7.3.0",
25
24
  "eslint-plugin-import": "2.32.0",
26
25
  "neostandard": "0.13.0",
@@ -38,18 +37,18 @@
38
37
  "dependencies": {
39
38
  "commander": "14.0.3",
40
39
  "yaml": "2.8.4",
41
- "valibot": "1.3.1",
40
+ "valibot": "1.4.0",
42
41
  "execa": "9.6.1",
43
42
  "mkdirp": "3.0.1",
44
- "@hapi/hapi": "21.4.8",
45
- "axios": "1.15.2",
43
+ "@hapi/hapi": "21.4.9",
44
+ "axios": "1.16.0",
46
45
  "beautiful-mermaid": "1.1.3",
47
46
  "cli-table3": "0.6.5",
48
47
  "chalk": "5.6.2",
49
48
  "pretty-ms": "9.3.0",
50
49
  "luxon": "3.7.2",
51
50
  "@modelcontextprotocol/sdk": "1.29.0",
52
- "zod": "4.4.2",
51
+ "zod": "4.4.3",
53
52
  "which": "6.0.1"
54
53
  },
55
54
  "engines": {