agy-statusline 1.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 radioman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Optimized Statusline for Antigravity CLI
2
+
3
+ <p align="center">
4
+ <img src="assets/logo.png" alt="Optimized Statusline for Antigravity CLI Logo" width="250" />
5
+ </p>
6
+
7
+ An optimized custom statusline implementation for the Antigravity CLI, featuring token scaling, dynamic context warnings, and a 20-step progress bar.
8
+
9
+ ## Features
10
+
11
+ - **High Performance**: Native startup under Bun (`~3ms`) or Node.js (`~15ms`), fitting safely below the CLI's `150ms` execution timeout.
12
+ - **Token Scale & Rounding**: Displays cumulative input and output token counts in `k` scale, rounded to the nearest integer.
13
+ - **Dynamic Context Warnings**: Colorizes context usage text automatically based on total input token counts:
14
+ - **Cyan**: Normal ($<160\text{k}$ tokens)
15
+ - **Orange**: Warning ($\ge 160\text{k}$ tokens)
16
+ - **Red**: Alert ($\ge 200\text{k}$ tokens)
17
+ - **20-Step Progress Bar**: Visualizes context usage percentage in 5% increments (`[■■■■□□□□□□□□□□□□□□□□]`).
18
+ - **Compact & Split Layout**: Calibrates spaces dynamically according to your `terminal_width` to push the folder and model information cleanly to the right side.
19
+
20
+ ## Layout Structure
21
+
22
+ ```
23
+ ? for shortcuts • {progress_bar} {used_pct}% of {limit} • in {input}k | out {output}k {cwd} • {model_name}
24
+ ```
25
+
26
+ ---
27
+
28
+ ## How It Works
29
+
30
+ 1. **Input**: The Antigravity CLI pipes a JSON metadata payload containing session details (active model, workspace, context token consumption, terminal size) into the script via standard input (`stdin`).
31
+ 2. **Parsing**: The script parses the incoming JSON stream. If `stdin` is empty or times out (safety threshold: `150ms`), it falls back to defaults or reads from the environment variable (`process.env.ANTIGRAVITY_SOURCE_METADATA`).
32
+ 3. **Calculations**:
33
+ - Calculates cumulative token usage (`total_input_tokens + total_output_tokens`) and rounds the values to integer `k` scales.
34
+ - Selects the statusline alert color (Cyan, Orange, or Red) based on total token limits.
35
+ - Strips ANSI escape codes from formatting templates to measure the true printable character width.
36
+ - Subtracts the printed width from the target `terminal_width` to determine the required padding spaces.
37
+ 4. **Output**: Writes the aligned statusline directly to standard output (`stdout`) with no trailing newline, enabling the CLI shell wrapper to render it cleanly at the bottom of the prompt.
38
+
39
+ ---
40
+
41
+ ## Installation & Setup
42
+
43
+ ### Installation via NPM
44
+
45
+ You can install this statusline directly from the npm registry and configure it automatically with a single command:
46
+
47
+ 1. **Install Globally**:
48
+ ```bash
49
+ npm install -g agy-statusline
50
+ ```
51
+ *(or `bun install -g agy-statusline` if using Bun)*
52
+
53
+ 2. **Run Automatic Configuration**:
54
+ ```bash
55
+ agy-statusline-setup
56
+ ```
57
+ *(This automatically locates your `settings.json` configuration file and updates your `statusLine` configuration block).*
58
+
59
+ 3. **Verify Installation** (Optional):
60
+ Run the diagnostics doctor to verify everything is working perfectly:
61
+ ```bash
62
+ agy-statusline-setup doctor
63
+ ```
64
+
65
+ ### Local Setup & Automatic Installation
66
+
67
+ If you cloned this repository locally, you can set it up using the included `setup.js` script:
68
+
69
+ Run the setup script using Bun (or Node.js):
70
+
71
+ ```bash
72
+ bun setup.js
73
+ ```
74
+
75
+ This will automatically:
76
+ 1. Run `bun link` to register the `agy-statusline` command globally.
77
+ 2. Locate your Antigravity CLI `settings.json` configuration file.
78
+ 3. Add or update the `statusLine` configuration block automatically (handling the UTF-8 BOM check correctly):
79
+ ```json
80
+ "statusLine": {
81
+ "type": "command",
82
+ "command": "agy-statusline",
83
+ "enabled": true
84
+ }
85
+ ```
86
+
87
+ ### Manual Local Installation (Alternative)
88
+
89
+ If you prefer manual setup from the local directory:
90
+ 1. **Link Globally**: Run `bun link` in the repository folder.
91
+ 2. **Configure Settings**: Open your Antigravity CLI config file located at `~/.gemini/antigravity-cli/settings.json` (or `C:\Users\<username>\.gemini\antigravity-cli\settings.json` on Windows), making sure to write without a UTF-8 BOM, and set:
92
+ ```json
93
+ "statusLine": {
94
+ "type": "command",
95
+ "command": "agy-statusline",
96
+ "enabled": true
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Local Testing
103
+
104
+ You can simulate how the statusline renders using the provided mock payload (`agy_statusline_stdin.json`).
105
+
106
+ ### Windows (PowerShell)
107
+ ```powershell
108
+ Get-Content .\agy_statusline_stdin.json -Raw | bun .\statusline.js
109
+ ```
110
+
111
+ ### macOS/Linux
112
+ ```bash
113
+ cat ./agy_statusline_stdin.json | bun ./statusline.js
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Diagnostic Doctor Check
119
+
120
+ You can run the built-in diagnostic tool to verify that the script is linked, the CLI settings are properly configured without a BOM, and the simulation executes correctly:
121
+
122
+ ```bash
123
+ bun setup.js doctor
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Testing & Coverage
129
+
130
+ This project features a comprehensive test suite written using Node's native test runner to maintain zero external runtime dependencies.
131
+
132
+ ### Run Tests
133
+
134
+ You can run the full integration test suite (which validates rendering layout, token math, threshold coloring, path contraction, and error fallbacks):
135
+
136
+ ```bash
137
+ npm test
138
+ ```
139
+
140
+ *Note: This command runs both the Node.js test runner and the Doctor diagnostics script, which also runs automatically as a Husky pre-commit hook.*
141
+
142
+ ### Test Coverage Report
143
+
144
+ To run the tests with a code coverage report (requires Node.js v20+):
145
+
146
+ ```bash
147
+ node --test --experimental-test-coverage
148
+ ```
Binary file
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "agy-statusline",
3
+ "version": "1.1.1",
4
+ "description": "Optimized custom statusline implementation for the Antigravity CLI",
5
+ "main": "statusline.js",
6
+ "logo": "assets/logo.png",
7
+ "icon": "assets/logo.png",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/pkradioman/agy-statusline.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/pkradioman/agy-statusline/issues"
14
+ },
15
+ "homepage": "https://github.com/pkradioman/agy-statusline#readme",
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "scripts": {
20
+ "prepare": "husky",
21
+ "test": "node --test && node setup.js doctor",
22
+ "release": "commit-and-tag-version"
23
+ },
24
+ "bin": {
25
+ "agy-statusline": "statusline.js",
26
+ "agy-statusline-setup": "setup.js"
27
+ },
28
+ "files": [
29
+ "statusline.js",
30
+ "setup.js",
31
+ "README.md",
32
+ "LICENSE",
33
+ "assets/logo.png"
34
+ ],
35
+ "keywords": [
36
+ "antigravity",
37
+ "statusline",
38
+ "customizer"
39
+ ],
40
+ "author": "radioman",
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "@commitlint/cli": "^21.0.2",
44
+ "@commitlint/config-conventional": "^21.0.2",
45
+ "commit-and-tag-version": "^12.7.3",
46
+ "husky": "^9.1.7"
47
+ }
48
+ }
package/setup.js ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { execSync } = require('child_process');
6
+
7
+ // 0. Handle doctor CLI parameter
8
+ if (process.argv.includes('doctor') || process.argv.includes('--doctor')) {
9
+ runDoctor();
10
+ process.exit(0);
11
+ }
12
+
13
+ console.log('🚀 Setting up Antigravity statusline...');
14
+
15
+ // 1. Run bun link to register agy-statusline globally (if in local repository dev environment)
16
+ let isLocalDev = fs.existsSync(path.join(__dirname, '.git'));
17
+ if (isLocalDev) {
18
+ try {
19
+ console.log('🔗 Running "bun link" to register the binary globally...');
20
+ execSync('bun link', { stdio: 'inherit' });
21
+ console.log('✅ Registered "agy-statusline" successfully.');
22
+ } catch (error) {
23
+ console.warn('⚠️ Failed to run "bun link". Proceeding with settings configuration anyway...');
24
+ }
25
+ } else {
26
+ console.log('📦 Running from global package installation, skipping "bun link".');
27
+ }
28
+
29
+ // 2. Locate settings.json
30
+ const homeDir = os.homedir();
31
+ const settingsPath = path.join(homeDir, '.gemini', 'antigravity-cli', 'settings.json');
32
+
33
+ if (!fs.existsSync(settingsPath)) {
34
+ console.error(`❌ Antigravity settings file not found at: ${settingsPath}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ // 3. Read and update settings.json
39
+ try {
40
+ const rawSettings = fs.readFileSync(settingsPath, 'utf8').trim();
41
+ // Strip UTF-8 BOM if present
42
+ const cleanSettings = rawSettings.replace(/^\uFEFF/, '');
43
+ const settings = JSON.parse(cleanSettings);
44
+
45
+ // Initialize statusLine block if it doesn't exist
46
+ if (!settings.statusLine) {
47
+ settings.statusLine = {};
48
+ }
49
+
50
+ settings.statusLine.type = 'command';
51
+ settings.statusLine.command = 'agy-statusline';
52
+ settings.statusLine.enabled = true;
53
+
54
+ // Convert back to string and write to file (default write has no BOM)
55
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
56
+ console.log(`✅ Successfully updated settings at: ${settingsPath}`);
57
+ console.log('🎉 Setup complete! Restart your shell or CLI to see changes.');
58
+ } catch (error) {
59
+ console.error(`❌ Failed to update settings.json: ${error.message}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ function runDoctor() {
64
+ console.log('🩺 Running Antigravity Statusline Doctor...');
65
+ let health = true;
66
+
67
+ // 1. Check Runtime
68
+ console.log(`\n1. Checking JavaScript Runtime...`);
69
+ console.log(` Running under: ${process.release ? process.release.name : 'Bun'} (${process.version})`);
70
+
71
+ // 2. Check settings.json location and contents
72
+ console.log(`\n2. Checking settings.json...`);
73
+ const sPath = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'settings.json');
74
+ if (!fs.existsSync(sPath)) {
75
+ console.error(` ❌ settings.json NOT found at: ${sPath}`);
76
+ health = false;
77
+ } else {
78
+ console.log(` Found settings.json at: ${sPath}`);
79
+ try {
80
+ const rawBytes = fs.readFileSync(sPath);
81
+ // Check for UTF-8 BOM (0xEF, 0xBB, 0xBF)
82
+ if (rawBytes.length >= 3 && rawBytes[0] === 0xEF && rawBytes[1] === 0xBB && rawBytes[2] === 0xBF) {
83
+ console.error(` ❌ WARNING: settings.json contains a UTF-8 BOM (Byte Order Mark). This will cause the CLI to crash or reset!`);
84
+ health = false;
85
+ } else {
86
+ console.log(` ✅ UTF-8 BOM Check: Passed (No BOM found)`);
87
+ }
88
+
89
+ const settings = JSON.parse(rawBytes.toString('utf8').trim());
90
+ if (settings.statusLine) {
91
+ console.log(` ✅ statusLine block exists`);
92
+ console.log(` Enabled: ${settings.statusLine.enabled}`);
93
+ console.log(` Type: ${settings.statusLine.type}`);
94
+ console.log(` Command: ${settings.statusLine.command}`);
95
+
96
+ if (settings.statusLine.command !== 'agy-statusline') {
97
+ console.error(` ❌ WARNING: statusLine.command is set to "${settings.statusLine.command}" instead of "agy-statusline"`);
98
+ health = false;
99
+ }
100
+ } else {
101
+ console.error(` ❌ statusLine configuration is missing in settings.json`);
102
+ health = false;
103
+ }
104
+ } catch (e) {
105
+ console.error(` ❌ Failed to parse settings.json: ${e.message}`);
106
+ health = false;
107
+ }
108
+ }
109
+
110
+ // 3. Check global executable in PATH
111
+ console.log(`\n3. Checking PATH for agy-statusline command...`);
112
+ const pathDirs = (process.env.PATH || '').split(path.delimiter);
113
+ let binaryFound = false;
114
+ const isWindows = process.platform === 'win32';
115
+ const binaryNames = isWindows ? ['agy-statusline.exe', 'agy-statusline.cmd', 'agy-statusline.ps1', 'agy-statusline'] : ['agy-statusline'];
116
+
117
+ for (const dir of pathDirs) {
118
+ for (const binName of binaryNames) {
119
+ const fullPath = path.join(dir, binName);
120
+ if (fs.existsSync(fullPath)) {
121
+ console.log(` ✅ Found executable shim at: ${fullPath}`);
122
+ binaryFound = true;
123
+ break;
124
+ }
125
+ }
126
+ if (binaryFound) break;
127
+ }
128
+
129
+ if (!binaryFound) {
130
+ console.error(` ❌ "agy-statusline" was NOT found in your system PATH.`);
131
+ console.error(` Make sure your global Bun/NPM binary folder is in your PATH environment variable.`);
132
+ health = false;
133
+ }
134
+
135
+ // 4. Try running the statusline script with a mock input
136
+ console.log(`\n4. Simulating Statusline Run...`);
137
+ const mockPayloadPath = path.join(__dirname, 'agy_statusline_stdin.json');
138
+ if (!fs.existsSync(mockPayloadPath)) {
139
+ console.error(` ❌ Mock input payload NOT found at: ${mockPayloadPath}`);
140
+ health = false;
141
+ } else {
142
+ try {
143
+ const mockInput = fs.readFileSync(mockPayloadPath, 'utf8');
144
+ const runtime = process.release ? 'node' : 'bun';
145
+ const scriptPath = path.join(__dirname, 'statusline.js');
146
+ const output = execSync(`${runtime} "${scriptPath}"`, { input: mockInput, encoding: 'utf8' });
147
+ console.log(` ✅ Test Run Successful! Output:`);
148
+ console.log(` ${output}`);
149
+ } catch (e) {
150
+ console.error(` ❌ Test Run Failed: ${e.message}`);
151
+ health = false;
152
+ }
153
+ }
154
+
155
+ console.log('\n---');
156
+ if (health) {
157
+ console.log('🎉 Statusline Health: EXCELLENT (All checks passed)');
158
+ } else {
159
+ console.error('❌ Statusline Health: PROBLEMS DETECTED (See details above)');
160
+ }
161
+ }
package/statusline.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bun
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ let timeout;
6
+
7
+ if (require.main === module) {
8
+ let inputData = '';
9
+ process.stdin.setEncoding('utf8');
10
+
11
+ process.stdin.on('data', (chunk) => {
12
+ inputData += chunk;
13
+ });
14
+
15
+ process.stdin.on('end', () => {
16
+ renderStatusline(inputData, 'end_event');
17
+ });
18
+
19
+ // To prevent hanging if stdin is not closed or redirected, set a safety timeout.
20
+ timeout = setTimeout(() => {
21
+ renderStatusline(inputData, 'timeout');
22
+ process.exit(0);
23
+ }, 150);
24
+ }
25
+
26
+ function renderStatusline(jsonStr, triggerSource) {
27
+ clearTimeout(timeout);
28
+
29
+ // Default values
30
+ let used_pct = 0;
31
+ let size = 0;
32
+ let model_name = 'Antigravity';
33
+ let current_dir = process.cwd();
34
+ let terminal_width = 80;
35
+ let input_tokens = 0;
36
+ let output_tokens = 0;
37
+ let errorLog = '';
38
+
39
+ // Read from environment fallback if stdin is empty
40
+ if (process.env.ANTIGRAVITY_SOURCE_METADATA) {
41
+ try {
42
+ let envMeta = process.env.ANTIGRAVITY_SOURCE_METADATA.replace(/^\uFEFF/, '');
43
+ const meta = JSON.parse(envMeta);
44
+ if (meta.model && (meta.model.display_name || meta.model.id)) {
45
+ model_name = meta.model.display_name || meta.model.id;
46
+ }
47
+ if (meta.workspace && meta.workspace.current_dir) {
48
+ current_dir = meta.workspace.current_dir;
49
+ }
50
+ } catch (e) {
51
+ errorLog += `[Env Parse Err: ${e.message}] `;
52
+ }
53
+ }
54
+
55
+ if (jsonStr) {
56
+ try {
57
+ jsonStr = jsonStr.replace(/^\uFEFF/, '').trim();
58
+ if (jsonStr) {
59
+ const data = JSON.parse(jsonStr);
60
+
61
+ if (data.model) {
62
+ model_name = data.model.display_name || data.model.id || model_name;
63
+ }
64
+ if (data.workspace && data.workspace.current_dir) {
65
+ current_dir = data.workspace.current_dir;
66
+ } else if (data.cwd) {
67
+ current_dir = data.cwd;
68
+ }
69
+ if (data.terminal_width !== undefined) {
70
+ terminal_width = Number(data.terminal_width) || 80;
71
+ }
72
+
73
+ if (data.context_window) {
74
+ let cw = data.context_window;
75
+ used_pct = cw.used_percentage !== undefined ? Math.round(cw.used_percentage) : 0;
76
+ size = cw.context_window_size !== undefined ? cw.context_window_size : 0;
77
+ input_tokens = cw.total_input_tokens !== undefined ? cw.total_input_tokens : 0;
78
+ output_tokens = cw.total_output_tokens !== undefined ? cw.total_output_tokens : 0;
79
+ }
80
+
81
+ // Save to file for debugging
82
+ const userProfile = process.env.USERPROFILE || process.env.HOME || '';
83
+ const targetDir = path.join(userProfile, 'temp');
84
+ if (fs.existsSync(targetDir)) {
85
+ fs.writeFileSync(path.join(targetDir, 'agy_statusline_stdin.txt'), jsonStr, 'utf8');
86
+ }
87
+ }
88
+ } catch (e) {
89
+ errorLog += `[Stdin Parse Err: ${e.message}. Content: ${JSON.stringify(jsonStr)}] `;
90
+ }
91
+ }
92
+
93
+ // Format size
94
+ let formatted_size = String(size);
95
+ if (size >= 1048576) {
96
+ formatted_size = `${Math.floor(size / 1048576)}M`;
97
+ } else if (size >= 1024) {
98
+ formatted_size = `${Math.floor(size / 1024)}k`;
99
+ }
100
+
101
+ // ANSI color codes
102
+ const RESET = '\x1b[0m';
103
+ const BOLD = '\x1b[1m';
104
+ const GREY = '\x1b[90m';
105
+ const CYAN = '\x1b[36m';
106
+ const BLUE = '\x1b[34m';
107
+ const GREEN = '\x1b[32m';
108
+ const RED = '\x1b[31m';
109
+ const ORANGE = '\x1b[38;5;208m';
110
+
111
+ // Determine color of context info
112
+ let CONTEXT_COLOR = CYAN;
113
+ if (input_tokens >= 200000) {
114
+ CONTEXT_COLOR = RED;
115
+ } else if (input_tokens >= 160000) {
116
+ CONTEXT_COLOR = ORANGE;
117
+ }
118
+
119
+ const formatK = (n) => {
120
+ return `${Math.round(n / 1000)}k`;
121
+ };
122
+
123
+ let filled = Math.min(20, Math.max(0, Math.ceil(used_pct / 5)));
124
+ let empty = 20 - filled;
125
+ let bar = `[${'■'.repeat(filled)}${'□'.repeat(empty)}]`;
126
+
127
+ // Strip ANSI codes to calculate actual printed length
128
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
129
+
130
+ // Form left part and calculate its clean length
131
+ let left_str = `${GREY}? for shortcuts${RESET}${GREY} • ${RESET}${CONTEXT_COLOR}${bar} ${used_pct}% of ${formatted_size}${RESET}${GREY} • ${RESET}${CONTEXT_COLOR}in ${formatK(input_tokens)}${RESET}${GREY} | ${RESET}${CONTEXT_COLOR}out ${formatK(output_tokens)}${RESET}`;
132
+ let plain_left_len = stripAnsi(left_str).length;
133
+
134
+ // Calculate clean length of the right part excluding the directory (bullet + model name)
135
+ let right_sans_dir = `${GREY} • ${RESET}${BOLD}${model_name}${RESET}`;
136
+ let plain_right_sans_dir_len = stripAnsi(right_sans_dir).length;
137
+
138
+ // Remaining width for the directory path (with a safety buffer of 2)
139
+ let max_dir_len = terminal_width - plain_left_len - plain_right_sans_dir_len - 2;
140
+
141
+ // Contract directory path dynamically to fit remaining space
142
+ let contracted_dir = contractPath(current_dir, max_dir_len);
143
+
144
+ // Form final right part using the contracted directory
145
+ let right_str = `${GREEN}${contracted_dir}${RESET}${right_sans_dir}`;
146
+ let plain_right_len = stripAnsi(right_str).length;
147
+
148
+ // Calculate padding size using clean lengths
149
+ let padding_size = terminal_width - plain_left_len - plain_right_len;
150
+
151
+ let status = '';
152
+ if (padding_size > 0) {
153
+ let padding = ' '.repeat(padding_size);
154
+ status = `${left_str}${padding}${right_str}`;
155
+ } else {
156
+ status = `${left_str}${GREY} • ${RESET}${right_str}`;
157
+ }
158
+
159
+ process.stdout.write(status);
160
+
161
+ // Write debug log
162
+ const userProfile = process.env.USERPROFILE || process.env.HOME || '';
163
+ const logFile = path.join(userProfile, 'temp', 'statusline_debug.log');
164
+ const logEntry = `${new Date().toISOString()} - Executed via ${triggerSource}. Model: ${model_name}. Cwd: ${current_dir}. Errs: ${errorLog}\n`;
165
+ try {
166
+ fs.appendFileSync(logFile, logEntry, 'utf8');
167
+ } catch(e) {}
168
+ }
169
+
170
+ function contractPath(p, maxLen) {
171
+ if (p.length <= maxLen) return p;
172
+
173
+ // 1. Try substituting home directory with ~
174
+ const home = process.env.USERPROFILE || process.env.HOME || '';
175
+ if (home && p.startsWith(home)) {
176
+ p = p.replace(home, '~');
177
+ }
178
+ if (p.length <= maxLen) return p;
179
+
180
+ // 2. If still too long, truncate it in the middle or keep the last N characters with ...
181
+ if (maxLen <= 10) return p; // Don't truncate if allowed length is too small to be readable
182
+
183
+ // Split by path separator
184
+ const sep = p.includes('\\') ? '\\' : '/';
185
+ const segments = p.split(sep);
186
+
187
+ if (segments.length <= 2) {
188
+ return '...' + p.slice(-(maxLen - 3));
189
+ }
190
+
191
+ let first = segments[0];
192
+ let last = segments[segments.length - 1];
193
+
194
+ // Keep adding segments from the end as long as we stay under maxLen
195
+ let middle = '...';
196
+ let result = first + sep + middle + sep + last;
197
+
198
+ for (let i = segments.length - 2; i > 0; i--) {
199
+ let candidate = first + sep + middle + sep + segments[i] + sep + last;
200
+ if (candidate.length <= maxLen) {
201
+ last = segments[i] + sep + last;
202
+ } else {
203
+ break;
204
+ }
205
+ }
206
+
207
+ result = first + sep + middle + sep + last;
208
+ if (result.length > maxLen) {
209
+ return '...' + p.slice(-(maxLen - 3));
210
+ }
211
+ return result;
212
+ }
213
+
214
+ if (typeof module !== 'undefined' && module.exports) {
215
+ module.exports = {
216
+ renderStatusline,
217
+ };
218
+ }