@rse/ase 0.0.19 → 0.0.20

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
@@ -22,26 +22,48 @@ export default class SetupCommand {
22
22
  }
23
23
  /* run a sub-process, suppressing output on success and emitting it on failure */
24
24
  async run(cmd, args, opts = {}) {
25
- const { cwd, quiet = false } = opts;
25
+ const { cwd, quiet = false, retries = 1, ignoreError } = opts;
26
26
  this.log.write("info", `setup: $ ${cmd} ${args.join(" ")}` +
27
27
  (cwd !== undefined ? ` (cwd: ${cwd})` : ""));
28
- if (quiet) {
29
- await execa(cmd, args, { stdio: "ignore", cwd, reject: false });
30
- return;
31
- }
32
- await execa(cmd, args, { stdio: "pipe", cwd }).catch((err) => {
33
- const exitCode = typeof err?.exitCode === "number" ? err.exitCode : -1;
34
- this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
35
- if (typeof err?.stdout === "string" && err.stdout.length > 0) {
36
- this.log.write("error", "setup: command failed: stdout:");
37
- process.stdout.write(err.stdout);
28
+ for (let i = 0; i < retries; i++) {
29
+ const final = (i === retries - 1);
30
+ try {
31
+ if (quiet) {
32
+ const result = await execa(cmd, args, { stdio: "ignore", cwd, reject: false });
33
+ if (typeof result.exitCode === "number" && result.exitCode !== 0 && !final) {
34
+ this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${args.join(" ")}" ` +
35
+ `(exit code: ${result.exitCode}): retrying...`);
36
+ await new Promise((resolve) => setTimeout(resolve, 1000));
37
+ continue;
38
+ }
39
+ return;
40
+ }
41
+ await execa(cmd, args, { stdio: "pipe", cwd });
42
+ return;
38
43
  }
39
- if (typeof err?.stderr === "string" && err.stderr.length > 0) {
40
- this.log.write("error", "setup: command failed: stderr:");
41
- process.stderr.write(err.stderr);
44
+ catch (err) {
45
+ if (!final) {
46
+ this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${args.join(" ")}": retrying...`);
47
+ await new Promise((resolve) => setTimeout(resolve, 1000));
48
+ continue;
49
+ }
50
+ if (ignoreError !== undefined) {
51
+ this.log.write("info", `setup: ${ignoreError} (skipped)`);
52
+ return;
53
+ }
54
+ const exitCode = typeof err?.exitCode === "number" ? err.exitCode : -1;
55
+ this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
56
+ if (typeof err?.stdout === "string" && err.stdout.length > 0) {
57
+ this.log.write("error", "setup: command failed: stdout:");
58
+ process.stdout.write(err.stdout);
59
+ }
60
+ if (typeof err?.stderr === "string" && err.stderr.length > 0) {
61
+ this.log.write("error", "setup: command failed: stderr:");
62
+ process.stderr.write(err.stderr);
63
+ }
64
+ throw err;
42
65
  }
43
- throw err;
44
- });
66
+ }
45
67
  }
46
68
  /* handler for "ase setup install" */
47
69
  async doInstall(dev) {
@@ -51,7 +73,7 @@ export default class SetupCommand {
51
73
  `installing ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
52
74
  const source = dev ? process.cwd() : "rse/ase";
53
75
  await this.run("claude", ["plugin", "marketplace", "add", source]);
54
- await this.run("claude", ["plugin", "install", "ase@ase"]);
76
+ await this.run("claude", ["plugin", "install", "ase@ase"], { retries: 3 });
55
77
  return 0;
56
78
  }
57
79
  /* handler for "ase setup update" */
@@ -72,8 +94,8 @@ export default class SetupCommand {
72
94
  but there is no version change in the plugin manifest,
73
95
  so just re-install the plugin to let Claude Code update its copy */
74
96
  this.log.write("info", "setup: update[dev]: re-install ASE Claude Code plugin (origin: local)");
75
- await this.run("claude", ["plugin", "uninstall", "ase@ase"]);
76
- await this.run("claude", ["plugin", "install", "ase@ase"]);
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 });
77
99
  }
78
100
  else {
79
101
  /* perform NPM version check */
@@ -104,8 +126,8 @@ export default class SetupCommand {
104
126
  /* uninstall ASE Claude Code plugin */
105
127
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
106
128
  `uninstalling ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
107
- await this.run("claude", ["plugin", "uninstall", "ase@ase"]);
108
- await this.run("claude", ["plugin", "marketplace", "remove", "ase"]);
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" });
109
131
  /* uninstall ASE CLI tool (non-development only) */
110
132
  if (!dev) {
111
133
  this.log.write("info", "setup: uninstall: uninstalling ASE CLI tool (origin: remote)");
@@ -4,10 +4,28 @@
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
  import fs from "node:fs";
7
+ import os from "node:os";
7
8
  import path from "node:path";
8
9
  import { execFileSync } from "node:child_process";
10
+ import { InvalidArgumentError } from "commander";
9
11
  import { execaSync } from "execa";
12
+ import { Chalk } from "chalk";
10
13
  import { Config, configSchema, parseScope } from "./ase-config.js";
14
+ /* forced-color chalk instance: stdout is a pipe under Claude Code,
15
+ so chalk auto-detection would yield level 0; force level 1 to keep
16
+ emitting ANSI sequences as the original implementation did */
17
+ const c = new Chalk({ level: 1 });
18
+ /* set of valid <color>...</color> markup names (chalk basic foreground colors plus "default") */
19
+ const COLORS = new Set([
20
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "default"
21
+ ]);
22
+ /* custom argument parser for Commander: non-negative integer */
23
+ const parseInteger = (name) => (value) => {
24
+ const n = Number.parseInt(value, 10);
25
+ if (!Number.isFinite(n) || n < 0)
26
+ throw new InvalidArgumentError(`${name} must be a non-negative integer`);
27
+ return n;
28
+ };
11
29
  /* read stdin into a single string */
12
30
  const readStdin = async () => {
13
31
  const chunks = [];
@@ -29,6 +47,114 @@ const detectTermWidth = () => {
29
47
  }
30
48
  return width;
31
49
  };
50
+ /* format a token count as a compact human-readable string (e.g. 334k, 104.9M) */
51
+ const formatTokens = (n) => {
52
+ if (!Number.isFinite(n) || n < 0)
53
+ return "0";
54
+ if (n >= 1_000_000_000)
55
+ return `${(n / 1_000_000_000).toFixed(1)}G`;
56
+ if (n >= 1_000_000)
57
+ return `${(n / 1_000_000).toFixed(1)}M`;
58
+ if (n >= 1_000)
59
+ return `${Math.round(n / 1_000)}k`;
60
+ return `${n}`;
61
+ };
62
+ /* format a millisecond duration as a compact human-readable string (e.g. 6d 12hr 7m, 4hr 27m, 12m 30s) */
63
+ const formatDurationMs = (ms) => {
64
+ if (!Number.isFinite(ms) || ms < 0)
65
+ return "0s";
66
+ const totalSec = Math.floor(ms / 1000);
67
+ const days = Math.floor(totalSec / 86400);
68
+ const hours = Math.floor((totalSec % 86400) / 3600);
69
+ const mins = Math.floor((totalSec % 3600) / 60);
70
+ const secs = totalSec % 60;
71
+ if (days > 0)
72
+ return `${days}d ${hours}hr ${mins}m`;
73
+ if (hours > 0)
74
+ return `${hours}hr ${mins}m`;
75
+ if (mins > 0)
76
+ return `${mins}m ${secs}s`;
77
+ return `${secs}s`;
78
+ };
79
+ /* format a wall-clock duration as elapsed hours+minutes (e.g. 92hr 40m), without day rollover */
80
+ const formatHoursMinutes = (ms) => {
81
+ if (!Number.isFinite(ms) || ms < 0)
82
+ return "0hr 0m";
83
+ const totalMin = Math.floor(ms / 60000);
84
+ const hours = Math.floor(totalMin / 60);
85
+ const mins = totalMin % 60;
86
+ return `${hours}hr ${mins}m`;
87
+ };
88
+ /* format an ISO timestamp as a remaining-time relative to now (e.g. 4hr 27m, 6d 12hr 7m) */
89
+ const formatTimeUntil = (iso) => {
90
+ const target = Date.parse(iso);
91
+ if (!Number.isFinite(target))
92
+ return "";
93
+ const delta = target - Date.now();
94
+ if (delta <= 0)
95
+ return "0m";
96
+ return formatDurationMs(delta);
97
+ };
98
+ /* format a USD cost as a dollar string with 2 decimals (e.g. $54.44) */
99
+ const formatCostUsd = (n) => {
100
+ if (!Number.isFinite(n) || n < 0)
101
+ return "$0.00";
102
+ return `$${n.toFixed(2)}`;
103
+ };
104
+ /* format a byte count as a compact human-readable string (e.g. 33.2G, 512M) */
105
+ const formatBytes = (n) => {
106
+ if (!Number.isFinite(n) || n < 0)
107
+ return "0";
108
+ if (n >= 1024 ** 3)
109
+ return `${(n / 1024 ** 3).toFixed(1)}G`;
110
+ if (n >= 1024 ** 2)
111
+ return `${(n / 1024 ** 2).toFixed(1)}M`;
112
+ if (n >= 1024)
113
+ return `${(n / 1024).toFixed(1)}k`;
114
+ return `${n}`;
115
+ };
116
+ /* probe local git status for the given working directory */
117
+ const probeGit = (cwd) => {
118
+ try {
119
+ const branch = execFileSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1000 })
120
+ .toString("utf8").trim();
121
+ const porc = execFileSync("git", ["-C", cwd, "status", "--porcelain"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1000 })
122
+ .toString("utf8");
123
+ const lines = porc.split("\n").filter((l) => l.length > 0);
124
+ const untracked = lines.filter((l) => l.startsWith("??")).length;
125
+ const dirty = lines.length > 0;
126
+ let added = 0;
127
+ let removed = 0;
128
+ try {
129
+ const shortstat = execFileSync("git", ["-C", cwd, "diff", "--shortstat", "HEAD"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1000 })
130
+ .toString("utf8");
131
+ const mAdd = shortstat.match(/(\d+)\s+insertion/);
132
+ const mDel = shortstat.match(/(\d+)\s+deletion/);
133
+ if (mAdd !== null)
134
+ added = Number.parseInt(mAdd[1], 10);
135
+ if (mDel !== null)
136
+ removed = Number.parseInt(mDel[1], 10);
137
+ }
138
+ catch (_e) {
139
+ /* no HEAD yet or git failure; leave counts at 0 */
140
+ }
141
+ return { branch, dirty, untracked, added, removed };
142
+ }
143
+ catch (_e) {
144
+ return { branch: "", dirty: false, untracked: 0, added: 0, removed: 0 };
145
+ }
146
+ };
147
+ /* probe local memory usage in bytes (used/total) using OS-portable helpers */
148
+ const probeMemory = () => {
149
+ try {
150
+ const total = os.totalmem();
151
+ const free = os.freemem();
152
+ return { used: total - free, total };
153
+ }
154
+ catch (_e) {
155
+ return { used: 0, total: 0 };
156
+ }
157
+ };
32
158
  /* command-line handling */
33
159
  export default class StatuslineCommand {
34
160
  log;
@@ -40,7 +166,15 @@ export default class StatuslineCommand {
40
166
  program
41
167
  .command("statusline")
42
168
  .description("Render Claude Code statusline from stdin JSON")
43
- .action(async () => {
169
+ .option("-w, --width <n>", "force terminal width to <n> characters (0 = auto-detect via /dev/tty)", parseInteger("--width"), 0)
170
+ .option("-m, --margin <n>", "reduce maximum used terminal width by <n> characters on each side", parseInteger("--margin"), 2)
171
+ .option("--no-icons", "disable icons in placeholder rendering")
172
+ .option("--no-labels", "disable labels in front of bold values")
173
+ .argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %P %c %C %L %N %a %r " +
174
+ "%S %D %W %Q %H %X %b %g %G %d %M %V %o placeholders and <color>...</color> markup " +
175
+ "(color: black, red, green, yellow, blue, magenta, cyan, white, default) " +
176
+ "(default: single line \"%m %e %t\")")
177
+ .action(async (lines, opts) => {
44
178
  /* read all of stdin */
45
179
  const input = await readStdin();
46
180
  /* parse JSON data */
@@ -53,78 +187,278 @@ export default class StatuslineCommand {
53
187
  this.log.write("error", `statusline: invalid JSON on stdin: ${message}`);
54
188
  process.exit(1);
55
189
  }
56
- /* fetch information from data */
57
- const dir = path.basename(data.workspace?.current_dir ?? "");
58
- const model = data.model?.display_name ?? "";
59
- const pct = Math.floor(data.context_window?.used_percentage ?? 0);
60
- const effort = data.effort?.level ?? "unknown";
61
- const thinking = (data.thinking?.enabled ?? false) === true ? "yes" : "no";
62
- const sessionId = data.session_id ?? "unknown";
63
- /* optionally determine ASE task id and persona style via in-process Config */
64
- let taskId = process.env.ASE_TASK_ID ?? "";
65
- let persona = process.env.ASE_PERSONA_STYLE ?? "";
66
- try {
67
- const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
68
- cfg.read("lenient");
69
- const t = String(cfg.get("agent.task") ?? "").trim();
70
- const p = String(cfg.get("agent.persona") ?? "").trim();
71
- if (t !== "")
72
- taskId = t;
73
- if (p !== "")
74
- persona = p;
75
- }
76
- catch (_e) {
77
- /* cascade unavailable; keep env-var fallbacks */
78
- }
79
- /* optionally determine terminal width */
80
- const width = detectTermWidth();
81
- /* configure ANSI sequences */
82
- const RESET = "\x1b[0m";
83
- const BOLD = "\x1b[1m";
84
- const BLACK = "\x1b[30m";
85
- const BLUE = "\x1b[34m";
86
- const YELLOW = "\x1b[33m";
87
- const RED = "\x1b[31m";
88
- /* determine context bar information */
89
- const barSize = 20;
90
- const barColor = pct >= 80 ? RED : pct >= 60 ? YELLOW : pct >= 40 ? BLUE : RESET;
91
- const filled = Math.round(pct / 100 * barSize);
92
- const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
93
- /* generate output */
94
- let output = "";
95
- output += `${BLUE}※ user: ${BOLD}${process.env.USER ?? process.env.LOGNAME ?? "unknown"}${RESET} `;
96
- if (width > 0 && width < 30)
97
- output += "\n";
98
- output += `${RED}⚑ project: ${BOLD}${dir}${RESET} `;
99
- if (width > 0 && width < 60)
100
- output += "\n";
101
- if (taskId !== "") {
102
- output += `${BLACK}◉ task: ${BOLD}${taskId}${RESET} `;
103
- if (width > 0 && width < 90)
104
- output += "\n";
105
- }
106
- output += `⏻ session: ${BOLD}${sessionId}${RESET}\n`;
107
- output += `⚙ model: ${BOLD}${model}${RESET} `;
108
- if (width > 0 && width < 30)
109
- output += "\n";
110
- output += `⚒ effort: ${BOLD}${effort}${RESET} `;
111
- if (width > 0 && width < 60)
112
- output += "\n";
113
- output += `⚛ thinking: ${BOLD}${thinking}${RESET}\n`;
114
- if (persona !== "") {
115
- output += `☯ persona: ${BOLD}${persona}${RESET} `;
116
- if (width > 0 && width < 30)
117
- output += "\n";
190
+ /* determine effective terminal width and budget */
191
+ const width = opts.width > 0 ? opts.width : detectTermWidth();
192
+ const budget = width > 0 ? width - 2 * opts.margin : 0;
193
+ /* shared output state and append helper with auto-wrap;
194
+ the helper itself strips ANSI CSI escape sequences to
195
+ measure the raw visible width of the chunk */
196
+ let out = "";
197
+ let col = 0;
198
+ const appendOutput = (ansi) => {
199
+ /* eslint-disable-next-line no-control-regex */
200
+ const raw = ansi.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
201
+ if (budget > 0 && col > 0 && col + raw.length > budget) {
202
+ out += "\n";
203
+ col = 0;
204
+ }
205
+ out += ansi;
206
+ col += raw.length;
207
+ };
208
+ /* active <color> span state: when non-null, renderer/literal output is buffered
209
+ instead of appended directly, and flushed via c[color](buf) on </color> */
210
+ let span = null;
211
+ const emit = (chunk) => {
212
+ if (span !== null)
213
+ span.buf += chunk;
214
+ else
215
+ appendOutput(chunk);
216
+ };
217
+ /* helper to build the "<icon> <label>: " prefix subject to --no-icons / --no-labels */
218
+ const prefix = (icon, label) => {
219
+ const i = opts.icons ? `${icon} ` : "";
220
+ const l = opts.labels ? `${label}: ` : "";
221
+ return `${i}${l}`;
222
+ };
223
+ /* determine effective template lines */
224
+ const tmpl = lines.length > 0 ? lines : ["%m %e %t"];
225
+ /* lazy memoized probes for cross-renderer values: each is computed at most
226
+ once per run and only when first requested by a renderer (or by the
227
+ post-loop tmux publish for the Config cascade) */
228
+ let sessCache = null;
229
+ const getSession = () => {
230
+ if (sessCache === null)
231
+ sessCache = data.session_name ?? data.session_id ?? "unknown";
232
+ return sessCache;
233
+ };
234
+ let cfgCache = null;
235
+ const getCfg = () => {
236
+ if (cfgCache !== null)
237
+ return cfgCache;
238
+ let taskId = process.env.ASE_TASK_ID ?? "";
239
+ let persona = process.env.ASE_PERSONA_STYLE ?? "";
240
+ try {
241
+ const cfg = new Config("config", configSchema, this.log, parseScope(`session:${getSession()}`));
242
+ cfg.read("lenient");
243
+ const t = String(cfg.get("agent.task") ?? "").trim();
244
+ const p = String(cfg.get("agent.persona") ?? "").trim();
245
+ if (t !== "")
246
+ taskId = t;
247
+ if (p !== "")
248
+ persona = p;
249
+ }
250
+ catch (_e) {
251
+ /* cascade unavailable; keep env-var fallbacks */
252
+ }
253
+ cfgCache = { taskId, persona };
254
+ return cfgCache;
255
+ };
256
+ let gitCache = null;
257
+ const getGit = () => {
258
+ if (gitCache === null)
259
+ gitCache = probeGit(data.workspace?.current_dir ?? "");
260
+ return gitCache;
261
+ };
262
+ let memCache = null;
263
+ const getMem = () => {
264
+ if (memCache === null)
265
+ memCache = probeMemory();
266
+ return memCache;
267
+ };
268
+ /* identifier to renderer map: each callback fetches its own information
269
+ directly from data (or via the lazy helpers above for shared values) */
270
+ const renderers = {
271
+ u: () => {
272
+ const user = process.env.USER ?? process.env.LOGNAME ?? "unknown";
273
+ emit(`${prefix("※", "user")}${c.bold(user)}`);
274
+ },
275
+ p: () => {
276
+ const dir = path.basename(data.workspace?.current_dir ?? "");
277
+ emit(`${prefix("⚑", "project")}${c.bold(dir)}`);
278
+ },
279
+ T: () => {
280
+ const { taskId } = getCfg();
281
+ if (taskId !== "")
282
+ emit(`${prefix("◉", "task")}${c.bold(taskId)}`);
283
+ },
284
+ s: () => emit(`${prefix("⏻", "session")}${c.bold(getSession())}`),
285
+ m: () => {
286
+ const model = data.model?.display_name ?? "";
287
+ emit(`${prefix("⚙", "model")}${c.bold(model)}`);
288
+ },
289
+ e: () => {
290
+ const effort = data.effort?.level ?? "unknown";
291
+ emit(`${prefix("⚒", "effort")}${c.bold(effort)}`);
292
+ },
293
+ t: () => {
294
+ const thinking = (data.thinking?.enabled ?? false) === true ? "yes" : "no";
295
+ emit(`${prefix("⚛", "thinking")}${c.bold(thinking)}`);
296
+ },
297
+ P: () => {
298
+ const { persona } = getCfg();
299
+ if (persona !== "")
300
+ emit(`${prefix("☯", "persona")}${c.bold(persona)}`);
301
+ },
302
+ c: () => {
303
+ const pct = Math.floor(data.context_window?.used_percentage ?? 0);
304
+ const barSize = 20;
305
+ const filled = Math.round(pct / 100 * barSize);
306
+ const bar = "█".repeat(filled) + "░".repeat(barSize - filled);
307
+ emit(`${prefix("◔", "context")}${bar} ${pct}%`);
308
+ },
309
+ a: () => {
310
+ const linesAdded = data.cost?.total_lines_added ?? 0;
311
+ emit(`${prefix("⊕", "added")}${c.bold(linesAdded)}`);
312
+ },
313
+ r: () => {
314
+ const linesRemoved = data.cost?.total_lines_removed ?? 0;
315
+ emit(`${prefix("⊖", "removed")}${c.bold(linesRemoved)}`);
316
+ },
317
+ C: () => {
318
+ const ctxIn = data.context_window?.current_usage?.input_tokens ?? 0;
319
+ const ctxCcIn = data.context_window?.current_usage?.cache_creation_input_tokens ?? 0;
320
+ const ctxCrIn = data.context_window?.current_usage?.cache_read_input_tokens ?? 0;
321
+ const tokensCur = ctxIn + ctxCcIn + ctxCrIn;
322
+ if (tokensCur > 0)
323
+ emit(`${prefix("◇", "tokens")}${c.bold(formatTokens(tokensCur))}`);
324
+ },
325
+ L: () => {
326
+ const pct = Math.floor(data.context_window?.used_percentage ?? 0);
327
+ const ctxIn = data.context_window?.current_usage?.input_tokens ?? 0;
328
+ const ctxCcIn = data.context_window?.current_usage?.cache_creation_input_tokens ?? 0;
329
+ const ctxCrIn = data.context_window?.current_usage?.cache_read_input_tokens ?? 0;
330
+ const tokensCur = ctxIn + ctxCcIn + ctxCrIn;
331
+ const tokensLim = pct > 0 && tokensCur > 0 ? Math.round(tokensCur * 100 / pct) : 0;
332
+ if (tokensLim > 0)
333
+ emit(`${prefix("◆", "limit")}${c.bold(formatTokens(tokensLim))}`);
334
+ },
335
+ N: () => {
336
+ const tokensCum = (data.context_window?.total_input_tokens ?? 0) +
337
+ (data.context_window?.total_output_tokens ?? 0);
338
+ if (tokensCum > 0)
339
+ emit(`${prefix("Σ", "total")}${c.bold(formatTokens(tokensCum))}`);
340
+ },
341
+ S: () => {
342
+ const pct5h = data.rate_limits?.five_hour?.used_percentage;
343
+ if (pct5h !== undefined)
344
+ emit(`${prefix("⏲", "session")}${c.bold(`${pct5h.toFixed(1)}%`)}`);
345
+ },
346
+ D: () => {
347
+ const until5h = data.rate_limits?.five_hour?.resets_at ?? "";
348
+ const s = formatTimeUntil(until5h);
349
+ if (s !== "")
350
+ emit(`${prefix("⏱", "session-resets")}${c.bold(s)}`);
351
+ },
352
+ W: () => {
353
+ const pctWk = data.rate_limits?.seven_day?.used_percentage;
354
+ if (pctWk !== undefined)
355
+ emit(`${prefix("⏲", "weekly")}${c.bold(`${pctWk.toFixed(1)}%`)}`);
356
+ },
357
+ Q: () => {
358
+ const untilWk = data.rate_limits?.seven_day?.resets_at ?? "";
359
+ const s = formatTimeUntil(untilWk);
360
+ if (s !== "")
361
+ emit(`${prefix("⏱", "weekly-resets")}${c.bold(s)}`);
362
+ },
363
+ H: () => {
364
+ const sessDurMs = data.cost?.total_duration_ms ?? 0;
365
+ if (sessDurMs > 0)
366
+ emit(`${prefix("⏱", "elapsed")}${c.bold(formatHoursMinutes(sessDurMs))}`);
367
+ },
368
+ X: () => {
369
+ const sessCost = data.cost?.total_cost_usd;
370
+ if (sessCost !== undefined)
371
+ emit(`${prefix("$", "cost")}${c.bold(formatCostUsd(sessCost))}`);
372
+ },
373
+ b: () => {
374
+ const g = getGit();
375
+ const label = g.branch !== "" ? g.branch : "no git";
376
+ emit(`${prefix("⎇", "branch")}${c.bold(label)}`);
377
+ },
378
+ g: () => {
379
+ const g = getGit();
380
+ if (g.branch !== "")
381
+ emit(`${prefix("±", "changed")}${c.bold(`+${g.added}/-${g.removed}`)}`);
382
+ },
383
+ G: () => {
384
+ const g = getGit();
385
+ if (g.branch !== "")
386
+ emit(`${prefix("⁈", "untracked")}${c.bold(String(g.untracked))}`);
387
+ },
388
+ d: () => {
389
+ const cwd = data.workspace?.current_dir ?? "";
390
+ if (cwd !== "")
391
+ emit(`${prefix("▶", "cwd")}${c.bold(cwd)}`);
392
+ },
393
+ M: () => {
394
+ const m = getMem();
395
+ if (m.total > 0)
396
+ emit(`${prefix("⛁", "mem")}${c.bold(`${formatBytes(m.used)}/${formatBytes(m.total)}`)}`);
397
+ },
398
+ V: () => {
399
+ const ccVersion = data.version ?? "";
400
+ if (ccVersion !== "")
401
+ emit(`${prefix("⎈", "version")}${c.bold(ccVersion)}`);
402
+ },
403
+ o: () => {
404
+ const styleName = data.output_style?.name ?? "";
405
+ if (styleName !== "")
406
+ emit(`${prefix("≡", "style")}${c.bold(styleName)}`);
407
+ }
408
+ };
409
+ /* walk each template line and render */
410
+ for (const line of tmpl) {
411
+ let i = 0;
412
+ while (i < line.length) {
413
+ const ch = line[i];
414
+ const next = line[i + 1];
415
+ if (ch === "<") {
416
+ const m = line.slice(i).match(/^<(\/?)([a-z]+)>/);
417
+ if (m !== null && COLORS.has(m[2])) {
418
+ if (m[1] === "/") {
419
+ if (span !== null) {
420
+ const wrapped = span.color === "default" ?
421
+ span.buf :
422
+ (c[span.color])(span.buf);
423
+ span = null;
424
+ appendOutput(wrapped);
425
+ }
426
+ }
427
+ else if (span === null)
428
+ span = { color: m[2], buf: "" };
429
+ i += m[0].length;
430
+ continue;
431
+ }
432
+ }
433
+ if (ch === "%" && next !== undefined && renderers[next] !== undefined) {
434
+ renderers[next]();
435
+ i += 2;
436
+ }
437
+ else {
438
+ emit(ch);
439
+ i += 1;
440
+ }
441
+ }
442
+ /* flush any unterminated span at end of line */
443
+ if (span !== null) {
444
+ const wrapped = span.color === "default" ?
445
+ span.buf :
446
+ (c[span.color])(span.buf);
447
+ span = null;
448
+ appendOutput(wrapped);
449
+ }
450
+ out += "\n";
451
+ col = 0;
118
452
  }
119
- output += `${barColor}◔ context: ${bar} ${pct}%${RESET}\n`;
120
453
  /* send output */
121
- process.stdout.write(output);
454
+ process.stdout.write(out);
122
455
  /* optionally publish task id to the calling tmux pane as a per-pane user
123
456
  option, so someone (like claudeX) can pick it up via #{@ase_task_id} */
124
457
  if (process.env.TMUX !== undefined
125
458
  && process.env.TMUX !== ""
126
459
  && process.env.TMUX_PANE !== undefined
127
460
  && process.env.TMUX_PANE !== "") {
461
+ const { taskId } = getCfg();
128
462
  const tid = taskId !== "" ? taskId : "default";
129
463
  execaSync("tmux", ["set-option", "-p", "-t", process.env.TMUX_PANE,
130
464
  "@ase_task_id", tid], { stdio: "ignore", reject: false });
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.19",
9
+ "version": "0.0.20",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",