agy-statusblocks 0.0.0-stage → 0.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 Okan Turan
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 CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # agy-statusblocks
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/agy-statusblocks)](https://www.npmjs.com/package/agy-statusblocks)
4
+ [![license](https://img.shields.io/npm/l/agy-statusblocks)](package.json)
5
+
6
+ Adaptive, block-based status line for [Antigravity CLI](https://antigravity.google) (`agy`). Cards reflow into a pyramid layout based on available terminal width using an exhaustive bin-packing algorithm.
7
+
8
+ **Zero runtime dependencies.** Pure Node.js built-ins only.
9
+
10
+ ## Install
11
+
12
+ Requires Node.js ≥ 20. Works on macOS, Linux, and Windows.
13
+
14
+ ```sh
15
+ npx agy-statusblocks init
16
+ ```
17
+
18
+ This copies the renderer to `~/.gemini/antigravity-cli/statusblocks/` and writes `statusLine.command` into `~/.gemini/antigravity-cli/settings.json` (creating it if needed). Restart Antigravity CLI (`agy`) to activate.
19
+
20
+ ### Update
21
+
22
+ ```sh
23
+ npx agy-statusblocks@latest update
24
+ ```
25
+
26
+ Updates happen automatically — the statusline checks for new versions daily in the background. The `postinstall` hook also auto-updates when you run `npm update -g agy-statusblocks`. Set `AGY_STATUSBLOCKS_NO_UPDATE=1` to disable the background check.
27
+
28
+ ## Cards
29
+
30
+ | Card | Shows |
31
+ |------|-------|
32
+ | **context** | Context window fill bar, percentage, used/total token count (supports 1M/2M context) |
33
+ | **model** | Active model name, tilde-shortened directory, reasoning effort level, plan tier, estimated cost |
34
+ | **usage** | Rate limit utilization (5-hour, daily, and 7-day windows) with reset countdowns |
35
+ | **state** | Agent state pill (`● IDLE`, `● RUNNING`, `● TOOL USE`, `● REVIEW`) and live task/subagent/artifact counters |
36
+ | **git** | Branch, staged/modified file counts, lines added/removed |
37
+
38
+ ## Layout
39
+
40
+ Cards are bin-packed into rows for optimal fit — blocks can be freely reordered across rows. Rows are sorted narrowest-on-top (pyramid shape). The algorithm tries every possible row assignment and picks the layout with fewest rows and most balanced widths.
41
+
42
+ On narrow/split-pane terminals, the layout degrades gracefully to stacked single-block rows.
43
+
44
+ ## Configure
45
+
46
+ `~/.agy-statusblocks.json`:
47
+
48
+ ```json
49
+ {
50
+ "segments": ["context", "model", "usage", "state", "git"]
51
+ }
52
+ ```
53
+
54
+ Or via environment:
55
+
56
+ ```sh
57
+ AGY_STATUSBLOCKS_SEGMENTS=context,model,usage
58
+ ```
59
+
60
+ ## Preview
61
+
62
+ ```sh
63
+ npx agy-statusblocks preview
64
+ ```
65
+
66
+ Check the installed package version with `agy-statusblocks --version` (or `-v`).
67
+
68
+ ## Development
69
+
70
+ ```sh
71
+ npm run build # compile TypeScript
72
+ npm run dev # watch mode
73
+ npm test # run vitest suite
74
+ npm run preview # render with mock data
75
+ ```
76
+
77
+ ## License
78
+
79
+ [MIT](./LICENSE)
@@ -0,0 +1,9 @@
1
+ export declare function hashKey(str: string): string;
2
+ export declare function safeKey(str: string): string;
3
+ export declare function readCacheEntry(name: string): {
4
+ ts: number;
5
+ value: unknown;
6
+ } | undefined;
7
+ export declare function readCache<T>(name: string, ttlMs: number): T | undefined;
8
+ export declare function writeCache(name: string, value: unknown): void;
9
+ export declare function acquireMarker(name: string, ttlMs: number): boolean;
package/dist/cache.js ADDED
@@ -0,0 +1,65 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, renameSync, statSync, unlinkSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { tmpdir } from 'os';
4
+ import { createHash } from 'crypto';
5
+ function cacheDir(create) {
6
+ const override = process.env['AGY_STATUSBLOCKS_CACHE_DIR'] || process.env['CLAUDE_STATUSBLOCKS_CACHE_DIR'];
7
+ const uid = typeof process.getuid === 'function' ? `-${process.getuid()}` : '';
8
+ const dir = override || join(tmpdir(), `claude-statusblocks${uid}`);
9
+ if (create)
10
+ mkdirSync(dir, { recursive: true });
11
+ return dir;
12
+ }
13
+ export function hashKey(str) {
14
+ return createHash('sha256').update(str).digest('hex').slice(0, 12);
15
+ }
16
+ export function safeKey(str) {
17
+ return str.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
18
+ }
19
+ export function readCacheEntry(name) {
20
+ try {
21
+ const raw = JSON.parse(readFileSync(join(cacheDir(false), name), 'utf8'));
22
+ if (typeof raw?.ts !== 'number')
23
+ return undefined;
24
+ return { ts: raw.ts, value: raw.value };
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ export function readCache(name, ttlMs) {
31
+ const entry = readCacheEntry(name);
32
+ if (entry === undefined || Date.now() - entry.ts > ttlMs)
33
+ return undefined;
34
+ return entry.value;
35
+ }
36
+ export function writeCache(name, value) {
37
+ try {
38
+ const dir = cacheDir(true);
39
+ const tmp = join(dir, `${name}.tmp-${process.pid}`);
40
+ writeFileSync(tmp, JSON.stringify({ ts: Date.now(), value }));
41
+ renameSync(tmp, join(dir, name));
42
+ }
43
+ catch {
44
+ // Best-effort
45
+ }
46
+ }
47
+ export function acquireMarker(name, ttlMs) {
48
+ try {
49
+ const path = join(cacheDir(true), name);
50
+ try {
51
+ writeFileSync(path, String(Date.now()), { flag: 'wx' });
52
+ return true;
53
+ }
54
+ catch {
55
+ if (Date.now() - statSync(path).mtimeMs <= ttlMs)
56
+ return false;
57
+ unlinkSync(path);
58
+ writeFileSync(path, String(Date.now()), { flag: 'wx' });
59
+ return true;
60
+ }
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync, rmSync, existsSync } from 'fs';
3
+ import { join, dirname, basename, sep } from 'path';
4
+ import { homedir } from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ import { render } from './layout.js';
7
+ import { loadConfig } from './config.js';
8
+ import { color, c } from './colors.js';
9
+ import { isValidVersion, isNewerVersion } from './version.js';
10
+ const PACKAGE_VERSION = (() => {
11
+ const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
12
+ if (typeof manifest.version !== 'string' || !isValidVersion(manifest.version)) {
13
+ throw new Error('package.json contains an invalid version');
14
+ }
15
+ return manifest.version;
16
+ })();
17
+ function settingsPath() {
18
+ return join(homedir(), '.gemini', 'antigravity-cli', 'settings.json');
19
+ }
20
+ function installDir() {
21
+ return join(homedir(), '.gemini', 'antigravity-cli', 'statusblocks');
22
+ }
23
+ function statusLineCommand(dest) {
24
+ return `node "${join(dest, 'index.js').split(sep).join('/')}"`;
25
+ }
26
+ function isOurCommand(cmd) {
27
+ return cmd.includes('agy-statusblocks') || /[\\/]statusblocks[\\/]index\.js/.test(cmd);
28
+ }
29
+ function readSettings() {
30
+ try {
31
+ return JSON.parse(readFileSync(settingsPath(), 'utf8'));
32
+ }
33
+ catch (err) {
34
+ if (err.code === 'ENOENT')
35
+ return {};
36
+ throw new Error(`could not parse ${settingsPath()}: ${err instanceof Error ? err.message : err}`);
37
+ }
38
+ }
39
+ function writeSettings(s) {
40
+ const file = settingsPath();
41
+ mkdirSync(dirname(file), { recursive: true });
42
+ writeFileSync(file, JSON.stringify(s, null, 2) + '\n');
43
+ }
44
+ const VERSION_STAMP = '.version';
45
+ const KEEP_FILES = new Set(['.last-update-check', VERSION_STAMP]);
46
+ function isInstallable(path) {
47
+ return !/\.test\./.test(basename(path));
48
+ }
49
+ function installedVersion(dest) {
50
+ try {
51
+ return readFileSync(join(dest, VERSION_STAMP), 'utf8').trim();
52
+ }
53
+ catch {
54
+ return '';
55
+ }
56
+ }
57
+ function installFiles() {
58
+ const dest = installDir();
59
+ const distDir = dirname(fileURLToPath(import.meta.url));
60
+ mkdirSync(dest, { recursive: true });
61
+ cpSync(distDir, dest, { recursive: true, force: true, filter: isInstallable });
62
+ for (const rel of readdirSync(dest, { recursive: true })) {
63
+ if (KEEP_FILES.has(rel))
64
+ continue;
65
+ if (existsSync(join(distDir, rel)) && isInstallable(rel))
66
+ continue;
67
+ rmSync(join(dest, rel), { recursive: true, force: true });
68
+ }
69
+ writeFileSync(join(dest, VERSION_STAMP), PACKAGE_VERSION);
70
+ return dest;
71
+ }
72
+ function runInit() {
73
+ console.log(`\n${color('agy-statusblocks', c.gemini, c.bold)} setup\n`);
74
+ try {
75
+ const dest = installFiles();
76
+ const command = statusLineCommand(dest);
77
+ const settings = readSettings();
78
+ const oldCommand = settings.statusLine?.command;
79
+ settings.statusLine = {
80
+ ...(settings.statusLine ?? {}),
81
+ type: 'command',
82
+ command,
83
+ enabled: true,
84
+ };
85
+ writeSettings(settings);
86
+ if (oldCommand && oldCommand !== command)
87
+ console.log(` Replaced: ${color(oldCommand, c.dim)}`);
88
+ console.log(` Installed: ${color(command, c.green)}`);
89
+ console.log(` Files: ${color(dest, c.dim)}`);
90
+ console.log(` Settings: ${color('~/.gemini/antigravity-cli/settings.json', c.dim)}`);
91
+ console.log(`\n Run ${color('npx agy-statusblocks update', c.cyan)} to update later.\n`);
92
+ }
93
+ catch (err) {
94
+ console.error(` Error: Could not update ${settingsPath()}`);
95
+ console.error(` ${err instanceof Error ? err.message : err}\n`);
96
+ process.exit(1);
97
+ }
98
+ }
99
+ function runUpdate(force) {
100
+ console.log(`\n${color('agy-statusblocks', c.gemini, c.bold)} update\n`);
101
+ try {
102
+ const installed = installedVersion(installDir());
103
+ if (!force && isNewerVersion(installed, PACKAGE_VERSION)) {
104
+ console.log(` Kept: installed ${color(installed, c.green)} is newer than this package (${PACKAGE_VERSION})`);
105
+ console.log(` ${color('Run with --force to downgrade.', c.dim)}\n`);
106
+ return;
107
+ }
108
+ const dest = installFiles();
109
+ console.log(` Updated: ${color(dest, c.green)}`);
110
+ try {
111
+ const settings = readSettings();
112
+ const cmd = String(settings.statusLine?.command ?? '');
113
+ const command = statusLineCommand(dest);
114
+ if (cmd && isOurCommand(cmd) && cmd !== command) {
115
+ settings.statusLine = { type: 'command', ...settings.statusLine, command };
116
+ console.log(` Migrated: ${color(command, c.cyan)}`);
117
+ writeSettings(settings);
118
+ }
119
+ }
120
+ catch { /* settings update is best-effort */ }
121
+ console.log();
122
+ }
123
+ catch (err) {
124
+ console.error(` Error: ${err instanceof Error ? err.message : err}\n`);
125
+ process.exit(1);
126
+ }
127
+ }
128
+ function runUninstall() {
129
+ const settings = readSettings();
130
+ if (settings.statusLine) {
131
+ settings.statusLine.command = '';
132
+ settings.statusLine.type = '';
133
+ writeSettings(settings);
134
+ }
135
+ console.log(color('✓ agy-statusblocks uninstalled and reverted to built-in status line', c.green));
136
+ }
137
+ function runPreview() {
138
+ process.env['AGY_STATUSBLOCKS_NO_REMOTE'] = '1';
139
+ const config = loadConfig();
140
+ const now = Date.now() / 1000;
141
+ const mockData = {
142
+ product: 'antigravity',
143
+ conversation_id: 'preview-session-1234',
144
+ agent_state: 'idle',
145
+ model: {
146
+ id: 'gemini-3.8-flash',
147
+ display_name: 'Gemini 3.8 Flash (Medium)',
148
+ },
149
+ context_window: {
150
+ context_window_size: 1048576,
151
+ used_percentage: 12.4,
152
+ total_input_tokens: 124000,
153
+ total_output_tokens: 6000,
154
+ },
155
+ quota: {
156
+ 'Gemini 3.8 Flash': {
157
+ remaining_fraction: 0.82,
158
+ reset_in_seconds: Math.floor(3600 * 2.5),
159
+ },
160
+ 'Claude Sonnet 5.5': {
161
+ remaining_fraction: 0.64,
162
+ reset_in_seconds: Math.floor(3600 * 4.2),
163
+ },
164
+ 'Claude Opus 5.5': {
165
+ remaining_fraction: 0.90,
166
+ reset_in_seconds: Math.floor(3600 * 4.2),
167
+ },
168
+ },
169
+ current_dir: join(homedir(), 'code', 'my-project'),
170
+ workspace: {
171
+ current_dir: join(homedir(), 'code', 'my-project'),
172
+ },
173
+ vcs: {
174
+ type: 'git',
175
+ branch: 'main',
176
+ dirty: false,
177
+ },
178
+ plan_tier: 'Consumer',
179
+ task_count: 0,
180
+ subagents: [],
181
+ artifact_count: 1,
182
+ cost: {
183
+ estimated: 0.005,
184
+ },
185
+ effort: {
186
+ level: 'medium',
187
+ },
188
+ };
189
+ console.log(`\n${color('agy-statusblocks', c.bold, c.gemini)} preview\n`);
190
+ for (const width of [120, 80, 50]) {
191
+ console.log(color('─'.repeat(width), c.gray));
192
+ console.log(`${width} cols:`);
193
+ console.log(render(mockData, width, config));
194
+ console.log();
195
+ }
196
+ }
197
+ function showHelp() {
198
+ console.log(`
199
+ ${color('agy-statusblocks', c.bold, c.gemini)} — block-based status line for Antigravity CLI (agy)
200
+
201
+ ${color('Usage:', c.bold)}
202
+ agy-statusblocks init Install into Antigravity CLI settings
203
+ agy-statusblocks update Update installed files (--force to downgrade)
204
+ agy-statusblocks preview Preview with mock data at various widths
205
+ agy-statusblocks uninstall Revert to built-in statusline
206
+ agy-statusblocks --version Print the installed package version
207
+ agy-statusblocks help Show this help
208
+
209
+ ${color('Blocks:', c.bold)}
210
+ context Context window fill bar, percentage, token counts
211
+ model Model name, directory, reasoning effort, tier, cost
212
+ usage 5-hour, daily, and 7-day rate limit utilization
213
+ state Agent state pill and task/subagent/artifact counters
214
+ git Branch, staged/modified counts, lines added/removed
215
+
216
+ ${color('Customize:', c.bold)}
217
+ ~/.agy-statusblocks.json: { "segments": ["context", "model", "usage", "state", "git"] }
218
+ Env vars: AGY_STATUSBLOCKS_SEGMENTS=context,model,usage
219
+ `);
220
+ }
221
+ const cmd = process.argv[2];
222
+ switch (cmd) {
223
+ case 'init':
224
+ runInit();
225
+ break;
226
+ case 'update':
227
+ runUpdate(process.argv.includes('--force'));
228
+ break;
229
+ case 'preview':
230
+ runPreview();
231
+ break;
232
+ case 'uninstall':
233
+ runUninstall();
234
+ break;
235
+ case '--version':
236
+ case '-v':
237
+ console.log(PACKAGE_VERSION);
238
+ break;
239
+ case 'help':
240
+ case '--help':
241
+ case '-h':
242
+ showHelp();
243
+ break;
244
+ default:
245
+ if (process.stdin.isTTY) {
246
+ showHelp();
247
+ }
248
+ else {
249
+ import('./index.js');
250
+ }
251
+ }
@@ -0,0 +1,24 @@
1
+ export declare const c: {
2
+ reset: string;
3
+ bold: string;
4
+ dim: string;
5
+ italic: string;
6
+ red: string;
7
+ green: string;
8
+ yellow: string;
9
+ blue: string;
10
+ magenta: string;
11
+ cyan: string;
12
+ white: string;
13
+ gray: string;
14
+ gemini: string;
15
+ geminiCyan: string;
16
+ orange: string;
17
+ purple: string;
18
+ };
19
+ export declare function color(text: string, ...codes: string[]): string;
20
+ export declare function pctColor(pct: number): string;
21
+ export declare function stripAnsi(str: string): string;
22
+ export declare function visibleLength(str: string): number;
23
+ export declare function padRight(str: string, targetLen: number): string;
24
+ export declare function renderBar(pct: number, width: number): string;
package/dist/colors.js ADDED
@@ -0,0 +1,42 @@
1
+ const ESC = '\x1b[';
2
+ export const c = {
3
+ reset: `${ESC}0m`,
4
+ bold: `${ESC}1m`,
5
+ dim: `${ESC}2m`,
6
+ italic: `${ESC}3m`,
7
+ red: `${ESC}31m`,
8
+ green: `${ESC}32m`,
9
+ yellow: `${ESC}33m`,
10
+ blue: `${ESC}34m`,
11
+ magenta: `${ESC}35m`,
12
+ cyan: `${ESC}36m`,
13
+ white: `${ESC}37m`,
14
+ gray: `${ESC}90m`,
15
+ gemini: `${ESC}38;2;66;133;244m`, // Google blue #4285F4
16
+ geminiCyan: `${ESC}38;2;77;208;225m`, // Cyan #4DD0E1
17
+ orange: `${ESC}38;2;217;119;87m`,
18
+ purple: `${ESC}38;2;168;85;247m`,
19
+ };
20
+ export function color(text, ...codes) {
21
+ return codes.join('') + text + c.reset;
22
+ }
23
+ export function pctColor(pct) {
24
+ return pct >= 90 ? c.red : pct >= 70 ? c.yellow : c.green;
25
+ }
26
+ export function stripAnsi(str) {
27
+ // eslint-disable-next-line no-control-regex
28
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
29
+ }
30
+ export function visibleLength(str) {
31
+ return stripAnsi(str).length;
32
+ }
33
+ export function padRight(str, targetLen) {
34
+ const vLen = visibleLength(str);
35
+ if (vLen >= targetLen)
36
+ return str;
37
+ return str + ' '.repeat(targetLen - vLen);
38
+ }
39
+ export function renderBar(pct, width) {
40
+ const filled = Math.min(width, Math.max(0, Math.round(pct * width / 100)));
41
+ return color('█'.repeat(filled), pctColor(pct)) + color('▒'.repeat(width - filled), c.gray);
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { StatusBlocksConfig } from './types.js';
2
+ export declare function loadConfig(): StatusBlocksConfig;
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ export function loadConfig() {
5
+ const envSegments = process.env['AGY_STATUSBLOCKS_SEGMENTS'];
6
+ const config = {};
7
+ try {
8
+ const raw = readFileSync(join(homedir(), '.agy-statusblocks.json'), 'utf8');
9
+ const parsed = JSON.parse(raw);
10
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
11
+ if (Array.isArray(parsed.segments))
12
+ config.segments = parsed.segments.filter((s) => typeof s === 'string');
13
+ if (typeof parsed.remoteUsage === 'boolean')
14
+ config.remoteUsage = parsed.remoteUsage;
15
+ if (typeof parsed.stackWithDefault === 'boolean')
16
+ config.stackWithDefault = parsed.stackWithDefault;
17
+ }
18
+ }
19
+ catch { /* no config file — use defaults */ }
20
+ if (envSegments)
21
+ config.segments = envSegments.split(',');
22
+ return config;
23
+ }
@@ -0,0 +1 @@
1
+ export declare function getOAuthToken(): string | null;
@@ -0,0 +1,44 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ function parseToken(raw) {
6
+ try {
7
+ const creds = JSON.parse(raw);
8
+ const oauth = creds?.claudeAiOauth;
9
+ if (!oauth || typeof oauth.accessToken !== 'string' || !oauth.accessToken)
10
+ return null;
11
+ if (typeof oauth.expiresAt === 'number' && oauth.expiresAt < Date.now())
12
+ return null;
13
+ return oauth.accessToken;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function getOAuthToken() {
20
+ try {
21
+ const configDir = process.env['CLAUDE_CONFIG_DIR'] || join(homedir(), '.claude');
22
+ const raw = readFileSync(join(configDir, '.credentials.json'), 'utf8');
23
+ const token = parseToken(raw);
24
+ if (token)
25
+ return token;
26
+ }
27
+ catch {
28
+ // No credentials file
29
+ }
30
+ if (process.platform === 'darwin') {
31
+ try {
32
+ const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', {
33
+ encoding: 'utf8',
34
+ stdio: ['pipe', 'pipe', 'ignore'],
35
+ timeout: 5000,
36
+ });
37
+ return parseToken(raw.trim());
38
+ }
39
+ catch {
40
+ // Keychain read failed or locked
41
+ }
42
+ }
43
+ return null;
44
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};