@rse/ase 0.0.18 → 0.0.19
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-statusline.js +134 -0
- package/dst/ase.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
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 fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { execaSync } from "execa";
|
|
10
|
+
import { Config, configSchema, parseScope } from "./ase-config.js";
|
|
11
|
+
/* read stdin into a single string */
|
|
12
|
+
const readStdin = async () => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of process.stdin)
|
|
15
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
16
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
17
|
+
};
|
|
18
|
+
/* detect terminal column width via /dev/tty (stdout is a pipe under Claude Code) */
|
|
19
|
+
const detectTermWidth = () => {
|
|
20
|
+
let width = 0;
|
|
21
|
+
try {
|
|
22
|
+
const tty = fs.openSync("/dev/tty", "r");
|
|
23
|
+
const out = execFileSync("tput", ["cols"], { stdio: [tty, "pipe", "ignore"] });
|
|
24
|
+
fs.closeSync(tty);
|
|
25
|
+
width = parseInt(out.toString("utf8").trim()) || 0;
|
|
26
|
+
}
|
|
27
|
+
catch (_e) {
|
|
28
|
+
/* no controlling terminal */
|
|
29
|
+
}
|
|
30
|
+
return width;
|
|
31
|
+
};
|
|
32
|
+
/* command-line handling */
|
|
33
|
+
export default class StatuslineCommand {
|
|
34
|
+
log;
|
|
35
|
+
constructor(log) {
|
|
36
|
+
this.log = log;
|
|
37
|
+
}
|
|
38
|
+
/* register commands */
|
|
39
|
+
register(program) {
|
|
40
|
+
program
|
|
41
|
+
.command("statusline")
|
|
42
|
+
.description("Render Claude Code statusline from stdin JSON")
|
|
43
|
+
.action(async () => {
|
|
44
|
+
/* read all of stdin */
|
|
45
|
+
const input = await readStdin();
|
|
46
|
+
/* parse JSON data */
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
data = JSON.parse(input);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
this.log.write("error", `statusline: invalid JSON on stdin: ${message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
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";
|
|
118
|
+
}
|
|
119
|
+
output += `${barColor}◔ context: ${bar} ${pct}%${RESET}\n`;
|
|
120
|
+
/* send output */
|
|
121
|
+
process.stdout.write(output);
|
|
122
|
+
/* optionally publish task id to the calling tmux pane as a per-pane user
|
|
123
|
+
option, so someone (like claudeX) can pick it up via #{@ase_task_id} */
|
|
124
|
+
if (process.env.TMUX !== undefined
|
|
125
|
+
&& process.env.TMUX !== ""
|
|
126
|
+
&& process.env.TMUX_PANE !== undefined
|
|
127
|
+
&& process.env.TMUX_PANE !== "") {
|
|
128
|
+
const tid = taskId !== "" ? taskId : "default";
|
|
129
|
+
execaSync("tmux", ["set-option", "-p", "-t", process.env.TMUX_PANE,
|
|
130
|
+
"@ase_task_id", tid], { stdio: "ignore", reject: false });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dst/ase.js
CHANGED
|
@@ -12,6 +12,7 @@ import MCPCommand from "./ase-mcp.js";
|
|
|
12
12
|
import HookCommand from "./ase-hook.js";
|
|
13
13
|
import DiagramCommand from "./ase-diagram.js";
|
|
14
14
|
import SetupCommand from "./ase-setup.js";
|
|
15
|
+
import StatuslineCommand from "./ase-statusline.js";
|
|
15
16
|
import TaskCommand from "./ase-task.js";
|
|
16
17
|
import pkg from "../package.json" with { type: "json" };
|
|
17
18
|
/* globally initialize logger */
|
|
@@ -47,6 +48,7 @@ const main = async () => {
|
|
|
47
48
|
new HookCommand(log).register(program);
|
|
48
49
|
new DiagramCommand(log).register(program);
|
|
49
50
|
new SetupCommand(log).register(program);
|
|
51
|
+
new StatuslineCommand(log).register(program);
|
|
50
52
|
new TaskCommand(log).register(program);
|
|
51
53
|
/* parse program arguments */
|
|
52
54
|
await program.parseAsync(process.argv);
|
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.
|
|
9
|
+
"version": "0.0.19",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|