agy-statusblocks 0.0.0-stage → 0.1.0

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,2 @@
1
+ import type { Segment } from '../types.js';
2
+ export declare const stateSegment: Segment;
@@ -0,0 +1,35 @@
1
+ import { color, c, visibleLength } from '../colors.js';
2
+ const STATE_CONFIG = {
3
+ idle: { label: '● IDLE', color: c.green },
4
+ working: { label: '● RUNNING', color: c.geminiCyan },
5
+ tool_use: { label: '● TOOL USE', color: c.yellow },
6
+ reviewing: { label: '● REVIEW', color: c.purple },
7
+ error: { label: '● ERROR', color: c.red },
8
+ };
9
+ export const stateSegment = {
10
+ id: 'state',
11
+ priority: 35,
12
+ enabled: (data) => Boolean(data.agent_state || data.task_count || data.artifact_count || data.subagents?.length),
13
+ render(data) {
14
+ const rawState = (data.agent_state || 'idle').toLowerCase();
15
+ const stateCfg = STATE_CONFIG[rawState] || { label: `● ${rawState.toUpperCase()}`, color: c.dim };
16
+ const line1 = color(stateCfg.label, stateCfg.color, c.bold);
17
+ // Line 2: orchestration telemetry
18
+ const counters = [];
19
+ if (data.task_count != null && data.task_count > 0) {
20
+ counters.push(`${data.task_count} task${data.task_count === 1 ? '' : 's'}`);
21
+ }
22
+ if (data.subagents && data.subagents.length > 0) {
23
+ counters.push(`${data.subagents.length} subagent${data.subagents.length === 1 ? '' : 's'}`);
24
+ }
25
+ if (data.artifact_count != null && data.artifact_count > 0) {
26
+ counters.push(`${data.artifact_count} artifact${data.artifact_count === 1 ? '' : 's'}`);
27
+ }
28
+ const line2 = counters.length > 0
29
+ ? color(counters.join(' · '), c.dim)
30
+ : color('orchestration idle', c.dim);
31
+ const lines = [line1, line2];
32
+ const width = Math.max(...lines.map(visibleLength));
33
+ return { id: 'state', priority: 35, width, lines };
34
+ },
35
+ };
@@ -0,0 +1,2 @@
1
+ import type { Segment } from '../types.js';
2
+ export declare const usageSegment: Segment;
@@ -0,0 +1,193 @@
1
+ import { color, c, visibleLength, padRight, pctColor, renderBar } from '../colors.js';
2
+ import { readRemoteLimits } from '../remote-usage.js';
3
+ const MS_PER_DAY = 86400000;
4
+ const MS_PER_HOUR = 3600000;
5
+ const MS_PER_MIN = 60000;
6
+ function formatResetTime(epochSec) {
7
+ if (!epochSec)
8
+ return '';
9
+ const ms = epochSec * 1000 - Date.now();
10
+ if (ms <= 0)
11
+ return 'now';
12
+ const d = Math.floor(ms / MS_PER_DAY);
13
+ const h = Math.floor((ms % MS_PER_DAY) / MS_PER_HOUR);
14
+ const m = Math.floor((ms % MS_PER_HOUR) / MS_PER_MIN);
15
+ if (d > 0)
16
+ return `${d}d${h > 0 ? `${h}h` : ''}${m > 0 ? `${m}m` : ''}`;
17
+ if (h > 0)
18
+ return `${h}h${m > 0 ? `${m}m` : ''}`;
19
+ return `${m}m`;
20
+ }
21
+ function clampPercent(v) {
22
+ return Math.min(999, Math.max(0, Math.round(v ?? 0)));
23
+ }
24
+ function parseResetTime(v) {
25
+ if (typeof v === 'number' && Number.isFinite(v)) {
26
+ return v > 1e11 ? Math.floor(v / 1000) : v;
27
+ }
28
+ if (typeof v === 'string') {
29
+ const ms = Date.parse(v);
30
+ if (Number.isFinite(ms))
31
+ return Math.floor(ms / 1000);
32
+ }
33
+ return 0;
34
+ }
35
+ function inferWindowSuffix(resetSec) {
36
+ if (!resetSec || resetSec <= 0)
37
+ return '';
38
+ if (resetSec > 18 * 3600)
39
+ return '7d';
40
+ return '5h';
41
+ }
42
+ function shortenBucketLabel(rawKey, resetSec = 0) {
43
+ const k = rawKey.trim();
44
+ const lower = k.toLowerCase();
45
+ const explicitWin = lower.includes('weekly') || lower.includes('7d') || lower.includes('7-day')
46
+ ? '7d'
47
+ : lower.includes('5h') || lower.includes('5-hour')
48
+ ? '5h'
49
+ : inferWindowSuffix(resetSec);
50
+ if (lower.includes('claude') && (lower.includes('gpt') || lower.includes('pool') || (!lower.includes('sonnet') && !lower.includes('opus') && !lower.includes('haiku')))) {
51
+ return explicitWin ? `claude ${explicitWin}` : 'claude';
52
+ }
53
+ if (lower.includes('sonnet')) {
54
+ const ver = k.match(/\d+(?:\.\d+)?/)?.[0] ?? '';
55
+ return ver ? `sonnet ${ver}` : 'sonnet';
56
+ }
57
+ if (lower.includes('opus')) {
58
+ const ver = k.match(/\d+(?:\.\d+)?/)?.[0] ?? '';
59
+ return ver ? `opus ${ver}` : 'opus';
60
+ }
61
+ if (lower.includes('haiku')) {
62
+ const ver = k.match(/\d+(?:\.\d+)?/)?.[0] ?? '';
63
+ return ver ? `haiku ${ver}` : 'haiku';
64
+ }
65
+ if (lower.includes('flash'))
66
+ return explicitWin === '7d' ? 'flash 7d' : 'flash';
67
+ if (lower.includes('argon'))
68
+ return 'argon';
69
+ if (lower.includes('pro'))
70
+ return explicitWin === '7d' ? 'pro 7d' : 'pro';
71
+ if (lower.includes('gemini'))
72
+ return explicitWin ? `gemini ${explicitWin}` : 'gemini';
73
+ const cleaned = k.replace(/\s*\(.*?\)\s*/g, '').replace(/^gemini\s+/i, '').replace(/^claude\s+/i, '').trim();
74
+ return cleaned.length > 12 ? cleaned.slice(0, 12) : (cleaned || k);
75
+ }
76
+ function bucketPool(key) {
77
+ const k = key.toLowerCase();
78
+ if (k.includes('claude') || k.includes('sonnet') || k.includes('opus') || k.includes('haiku') || k.includes('gpt'))
79
+ return 'claude';
80
+ if (k.includes('argon'))
81
+ return 'argon';
82
+ if (k.includes('gemini') || k.includes('flash') || k.includes('pro'))
83
+ return 'gemini';
84
+ return 'other';
85
+ }
86
+ function isActiveModelBucket(key, data) {
87
+ const k = key.toLowerCase();
88
+ const mName = (data.model?.display_name ?? '').toLowerCase();
89
+ const mId = (data.model?.id ?? '').toLowerCase();
90
+ if (k && (mName.includes(k) || mId.includes(k)))
91
+ return true;
92
+ for (const token of ['sonnet', 'opus', 'haiku', 'flash', 'argon', 'pro']) {
93
+ if (k.includes(token) && (mName.includes(token) || mId.includes(token)))
94
+ return true;
95
+ }
96
+ // If active model is Sonnet/Opus and bucket is a shared Claude/GPT pool
97
+ if ((mName.includes('claude') || mName.includes('sonnet') || mName.includes('opus')) && bucketPool(key) === 'claude') {
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ function extractLimits(data) {
103
+ const out = [];
104
+ // 1. Quota map if provided natively by Antigravity (includes Gemini + Argon + Claude/GPT pools)
105
+ if (data.quota && typeof data.quota === 'object') {
106
+ const entries = Object.entries(data.quota)
107
+ .filter(([, bucket]) => bucket && typeof bucket === 'object' && !bucket.disabled);
108
+ // Sort active model's bucket first, then prefer showing distinct pools (Gemini/Argon + Claude), then by highest utilization
109
+ entries.sort(([keyA, a], [keyB, b]) => {
110
+ const actA = isActiveModelBucket(keyA, data) ? 1 : 0;
111
+ const actB = isActiveModelBucket(keyB, data) ? 1 : 0;
112
+ if (actA !== actB)
113
+ return actB - actA;
114
+ const pctA = typeof a.remaining_fraction === 'number' ? (1 - a.remaining_fraction) * 100 : (a.used_percentage ?? 0);
115
+ const pctB = typeof b.remaining_fraction === 'number' ? (1 - b.remaining_fraction) * 100 : (b.used_percentage ?? 0);
116
+ return pctB - pctA;
117
+ });
118
+ const seenLabels = new Set();
119
+ for (const [key, bucket] of entries) {
120
+ const rawPct = typeof bucket.remaining_fraction === 'number'
121
+ ? (1 - bucket.remaining_fraction) * 100
122
+ : bucket.used_percentage;
123
+ const pct = clampPercent(rawPct);
124
+ const nowSec = Math.floor(Date.now() / 1000);
125
+ const resetsAt = typeof bucket.reset_in_seconds === 'number' && bucket.reset_in_seconds > 0
126
+ ? nowSec + bucket.reset_in_seconds
127
+ : parseResetTime(bucket.reset_time ?? bucket.resets_at);
128
+ const resetSec = resetsAt > nowSec ? resetsAt - nowSec : (bucket.reset_in_seconds ?? 0);
129
+ const label = shortenBucketLabel(key, resetSec);
130
+ if (seenLabels.has(label))
131
+ continue;
132
+ seenLabels.add(label);
133
+ out.push({ label, percent: pct, resetsAt });
134
+ }
135
+ }
136
+ // 2. Rate limits if provided
137
+ const rl = data.rate_limits;
138
+ if (rl) {
139
+ if (rl.five_hour) {
140
+ out.push({
141
+ label: '5h',
142
+ percent: clampPercent(rl.five_hour.used_percentage),
143
+ resetsAt: parseResetTime(rl.five_hour.resets_at),
144
+ });
145
+ }
146
+ if (rl.seven_day) {
147
+ out.push({
148
+ label: '7d',
149
+ percent: clampPercent(rl.seven_day.used_percentage),
150
+ resetsAt: parseResetTime(rl.seven_day.resets_at),
151
+ });
152
+ }
153
+ if (rl.daily) {
154
+ out.push({
155
+ label: 'daily',
156
+ percent: clampPercent(rl.daily.used_percentage),
157
+ resetsAt: parseResetTime(rl.daily.resets_at),
158
+ });
159
+ }
160
+ }
161
+ // 3. Fallback to cached remote usage
162
+ if (out.length === 0) {
163
+ const remote = readRemoteLimits();
164
+ if (remote && remote.length > 0) {
165
+ const now = Date.now() / 1000;
166
+ return remote.map(l => (l.resetsAt > 0 && l.resetsAt <= now ? { ...l, percent: 0 } : l));
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ export const usageSegment = {
172
+ id: 'usage',
173
+ priority: 25,
174
+ enabled: (data) => extractLimits(data).length > 0,
175
+ render(data) {
176
+ const limits = extractLimits(data);
177
+ if (limits.length === 0) {
178
+ return { id: 'usage', priority: 25, width: 0, lines: [''] };
179
+ }
180
+ const BAR_WIDTH = 8;
181
+ const rows = limits.slice(0, 2).map(l => {
182
+ const bar = renderBar(l.percent, BAR_WIDTH);
183
+ const pctStr = color(`${l.percent}%`, pctColor(l.percent), c.bold);
184
+ const reset = l.resetsAt ? color(`↻ ${formatResetTime(l.resetsAt)}`, c.dim) : '';
185
+ const lbl = color(l.label, c.dim);
186
+ const parts = [bar, pctStr, reset, lbl].filter(Boolean);
187
+ return parts.join(color(' · ', c.dim));
188
+ });
189
+ const width = Math.max(...rows.map(visibleLength));
190
+ const paddedRows = rows.map(r => padRight(r, width));
191
+ return { id: 'usage', priority: 25, width, lines: paddedRows };
192
+ },
193
+ };
@@ -0,0 +1 @@
1
+ export declare function spawnDetached(execPath: string, args: string[]): void;
package/dist/spawn.js ADDED
@@ -0,0 +1,13 @@
1
+ import { spawn } from 'child_process';
2
+ export function spawnDetached(execPath, args) {
3
+ try {
4
+ const child = spawn(execPath, args, {
5
+ detached: true,
6
+ stdio: 'ignore',
7
+ });
8
+ child.unref();
9
+ }
10
+ catch {
11
+ // Best-effort detached spawn
12
+ }
13
+ }
@@ -0,0 +1,83 @@
1
+ export interface AgyQuotaBucket {
2
+ remaining_fraction?: number;
3
+ remaining_amount?: number;
4
+ reset_time?: string;
5
+ reset_in_seconds?: number;
6
+ disabled?: boolean;
7
+ used_percentage?: number;
8
+ resets_at?: number | string;
9
+ cycle_mode?: string;
10
+ }
11
+ export interface AgyStatusData {
12
+ product?: string;
13
+ conversation_id?: string;
14
+ agent_state?: 'idle' | 'working' | 'tool_use' | 'reviewing' | 'error' | string;
15
+ model: {
16
+ id?: string;
17
+ display_name: string;
18
+ effort?: string;
19
+ };
20
+ context_window: {
21
+ context_window_size: number;
22
+ used_percentage?: number | null;
23
+ remaining_percentage?: number | null;
24
+ total_input_tokens?: number;
25
+ total_output_tokens?: number;
26
+ current_usage?: unknown;
27
+ };
28
+ rate_limits?: {
29
+ five_hour?: {
30
+ used_percentage?: number;
31
+ resets_at?: number | string;
32
+ };
33
+ seven_day?: {
34
+ used_percentage?: number;
35
+ resets_at?: number | string;
36
+ };
37
+ daily?: {
38
+ used_percentage?: number;
39
+ resets_at?: number | string;
40
+ };
41
+ };
42
+ quota?: Record<string, AgyQuotaBucket>;
43
+ current_dir?: string;
44
+ workspace?: {
45
+ current_dir: string;
46
+ project_dir?: string;
47
+ };
48
+ vcs?: {
49
+ type?: string;
50
+ branch?: string;
51
+ dirty?: boolean;
52
+ };
53
+ artifact_count?: number;
54
+ task_count?: number;
55
+ subagents?: unknown[];
56
+ plan_tier?: string;
57
+ terminal_width?: number;
58
+ cost?: {
59
+ total_usd?: number;
60
+ subagent_usd?: number;
61
+ estimated?: number | boolean;
62
+ };
63
+ effort?: {
64
+ level: string;
65
+ };
66
+ }
67
+ export interface Block {
68
+ id: string;
69
+ priority: number;
70
+ width: number;
71
+ lines: string[];
72
+ }
73
+ export interface Segment {
74
+ id: string;
75
+ priority: number;
76
+ render(data: AgyStatusData, allocWidth: number): Block;
77
+ enabled(data: AgyStatusData): boolean;
78
+ }
79
+ export interface StatusBlocksConfig {
80
+ segments?: string[];
81
+ stackWithDefault?: boolean;
82
+ remoteUsage?: boolean;
83
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { isValidVersion, isNewerVersion } from './version.js';
4
+ const REGISTRY_URL = 'https://registry.npmjs.org/agy-statusblocks/latest';
5
+ async function main() {
6
+ let installed = '';
7
+ try {
8
+ installed = readFileSync(new URL('./.version', import.meta.url), 'utf8').trim();
9
+ }
10
+ catch { /* unstamped install */ }
11
+ let latest = '';
12
+ try {
13
+ const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(10000) });
14
+ if (!res.ok)
15
+ return;
16
+ const body = (await res.json());
17
+ if (typeof body.version === 'string')
18
+ latest = body.version.trim();
19
+ }
20
+ catch {
21
+ return;
22
+ }
23
+ if (!isValidVersion(latest))
24
+ return;
25
+ if (isValidVersion(installed) && !isNewerVersion(latest, installed))
26
+ return;
27
+ execSync(`npx -y agy-statusblocks@${latest} update`, {
28
+ stdio: 'ignore',
29
+ windowsHide: true,
30
+ timeout: 300000,
31
+ });
32
+ }
33
+ main().catch(() => { });
@@ -0,0 +1,2 @@
1
+ export declare function isValidVersion(v: string): boolean;
2
+ export declare function isNewerVersion(a: string, b: string): boolean;
@@ -0,0 +1,15 @@
1
+ const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
2
+ export function isValidVersion(v) {
3
+ return VERSION_RE.test(v);
4
+ }
5
+ export function isNewerVersion(a, b) {
6
+ if (!isValidVersion(a) || !isValidVersion(b))
7
+ return false;
8
+ const pa = a.split(/[-+]/)[0].split('.').map(Number);
9
+ const pb = b.split(/[-+]/)[0].split('.').map(Number);
10
+ for (let i = 0; i < 3; i++) {
11
+ if (pa[i] !== pb[i])
12
+ return pa[i] > pb[i];
13
+ }
14
+ return false;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "agy-statusblocks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Opinionated, adaptive block-based status line for Antigravity CLI (agy) with bin-packing layout, rate limit tracking, and live agent telemetry",
5
+ "type": "module",
6
+ "bin": {
7
+ "agy-statusblocks": "dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.test.*",
13
+ "postinstall.cjs"
14
+ ],
15
+ "scripts": {
16
+ "prepare": "npm run build",
17
+ "postinstall": "node postinstall.cjs",
18
+ "prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
19
+ "build": "tsc",
20
+ "dev": "tsc --watch",
21
+ "test": "vitest run",
22
+ "preview": "node dist/cli.js preview"
23
+ },
24
+ "keywords": [
25
+ "antigravity",
26
+ "agy",
27
+ "gemini",
28
+ "statusline",
29
+ "status-bar",
30
+ "terminal",
31
+ "cli",
32
+ "ansi",
33
+ "box-drawing",
34
+ "rate-limit",
35
+ "bin-packing",
36
+ "layout"
37
+ ],
38
+ "homepage": "https://github.com/okanerturan/agy-statusblocks#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/okanerturan/agy-statusblocks/issues"
41
+ },
42
+ "author": "Okan Turan",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/okanerturan/agy-statusblocks"
46
+ },
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "license": "MIT",
51
+ "devDependencies": {
52
+ "@types/node": "^22.0.0",
53
+ "typescript": "^5.7.0",
54
+ "vitest": "^4.1.0"
55
+ }
56
+ }
@@ -0,0 +1,17 @@
1
+ // Auto-update ~/.gemini/antigravity-cli/statusblocks/ on npm install/update (if already installed via init)
2
+ const { existsSync } = require('fs');
3
+ const { execFileSync } = require('child_process');
4
+ const { join } = require('path');
5
+ const { homedir } = require('os');
6
+
7
+ try {
8
+ const dest = join(homedir(), '.gemini', 'antigravity-cli', 'statusblocks');
9
+ if (existsSync(dest)) {
10
+ execFileSync(process.execPath, [join(__dirname, 'dist', 'cli.js'), 'update'], {
11
+ stdio: 'ignore',
12
+ windowsHide: true,
13
+ });
14
+ }
15
+ } catch {
16
+ // Best-effort — don't fail the install
17
+ }