@askalf/dario 3.38.6 → 4.0.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.
- package/README.md +23 -1
- package/dist/analytics.d.ts +32 -3
- package/dist/analytics.js +48 -3
- package/dist/cli.js +111 -25
- package/dist/config-file.d.ts +157 -0
- package/dist/config-file.js +326 -0
- package/dist/proxy.js +93 -23
- package/dist/tui/app.d.ts +96 -0
- package/dist/tui/app.js +178 -0
- package/dist/tui/input.d.ts +57 -0
- package/dist/tui/input.js +206 -0
- package/dist/tui/layout.d.ts +66 -0
- package/dist/tui/layout.js +152 -0
- package/dist/tui/proxy-client.d.ts +60 -0
- package/dist/tui/proxy-client.js +166 -0
- package/dist/tui/render.d.ts +178 -0
- package/dist/tui/render.js +246 -0
- package/dist/tui/tab.d.ts +89 -0
- package/dist/tui/tab.js +19 -0
- package/dist/tui/tabs/accounts.d.ts +32 -0
- package/dist/tui/tabs/accounts.js +110 -0
- package/dist/tui/tabs/analytics.d.ts +53 -0
- package/dist/tui/tabs/analytics.js +161 -0
- package/dist/tui/tabs/backends.d.ts +19 -0
- package/dist/tui/tabs/backends.js +77 -0
- package/dist/tui/tabs/config.d.ts +35 -0
- package/dist/tui/tabs/config.js +267 -0
- package/dist/tui/tabs/hits.d.ts +34 -0
- package/dist/tui/tabs/hits.js +223 -0
- package/dist/tui/tabs/status.d.ts +45 -0
- package/dist/tui/tabs/status.js +132 -0
- package/dist/tui/tui-app.d.ts +41 -0
- package/dist/tui/tui-app.js +217 -0
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status tab — at-a-glance proxy + auth + config-source view.
|
|
3
|
+
*
|
|
4
|
+
* Read-mostly. On mount: probe /health for proxy reachability; load
|
|
5
|
+
* config-file metadata locally. On any key, return undefined (no
|
|
6
|
+
* mutations from this tab).
|
|
7
|
+
*
|
|
8
|
+
* Layout:
|
|
9
|
+
*
|
|
10
|
+
* ┌─ Proxy ─────────────────────────────────────────┐
|
|
11
|
+
* │ status: running │
|
|
12
|
+
* │ port: 3456 │
|
|
13
|
+
* │ oauth: healthy (expires in 7h 41m) │
|
|
14
|
+
* │ requests: 247 │
|
|
15
|
+
* └─────────────────────────────────────────────────┘
|
|
16
|
+
* ┌─ Config ────────────────────────────────────────┐
|
|
17
|
+
* │ source: ~/.dario/config.json │
|
|
18
|
+
* │ schema: v1 │
|
|
19
|
+
* │ …per-knob effective values (read-only) │
|
|
20
|
+
* └─────────────────────────────────────────────────┘
|
|
21
|
+
*/
|
|
22
|
+
import { fg, dim, brand } from '../render.js';
|
|
23
|
+
import { renderKvRow } from '../layout.js';
|
|
24
|
+
export const StatusTab = {
|
|
25
|
+
id: 'status',
|
|
26
|
+
label: 'Status',
|
|
27
|
+
hotkey: 's',
|
|
28
|
+
initialState() {
|
|
29
|
+
return {
|
|
30
|
+
loading: true,
|
|
31
|
+
health: null,
|
|
32
|
+
configSource: null,
|
|
33
|
+
lastRefreshAt: 0,
|
|
34
|
+
error: null,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
async onMount(_state, ctx) {
|
|
38
|
+
return refreshStatus(ctx);
|
|
39
|
+
},
|
|
40
|
+
onKey(state, key) {
|
|
41
|
+
// `r` triggers a manual refresh by signaling the parent to call
|
|
42
|
+
// onMount again. The parent watches for a sentinel state.
|
|
43
|
+
if (key.name === 'printable' && key.ch === 'r' && !key.ctrl) {
|
|
44
|
+
return { ...state, loading: true };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
},
|
|
48
|
+
render(state, dim_) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const w = dim_.cols;
|
|
51
|
+
if (state.loading && !state.health) {
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(' ' + dim('Loading status…'));
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
// ── Proxy section ──────────────────────────────────────────
|
|
57
|
+
lines.push(' ' + brand('Proxy'));
|
|
58
|
+
if (state.health) {
|
|
59
|
+
lines.push(' ' + renderKvRow('Status', fg('green', state.health.status), w - 4));
|
|
60
|
+
lines.push(' ' + renderKvRow('OAuth', formatOauth(state.health.oauth, state.health.expiresIn), w - 4));
|
|
61
|
+
lines.push(' ' + renderKvRow('Requests', String(state.health.requests ?? 0), w - 4));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push(' ' + renderKvRow('Status', fg('red', 'unreachable — is `dario proxy` running?'), w - 4));
|
|
65
|
+
if (state.error) {
|
|
66
|
+
lines.push(' ' + renderKvRow('Error', dim(state.error), w - 4));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lines.push('');
|
|
70
|
+
// ── Config section ─────────────────────────────────────────
|
|
71
|
+
lines.push(' ' + brand('Config'));
|
|
72
|
+
const sourceLabel = state.configSource === 'file' ? '~/.dario/config.json'
|
|
73
|
+
: state.configSource === 'missing' ? dim('(no file — using defaults)')
|
|
74
|
+
: state.configSource === 'invalid' ? fg('yellow', '(file present but invalid — using defaults)')
|
|
75
|
+
: dim('not loaded');
|
|
76
|
+
lines.push(' ' + renderKvRow('Source', sourceLabel, w - 4));
|
|
77
|
+
lines.push('');
|
|
78
|
+
// ── Footer hint ────────────────────────────────────────────
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(' ' + dim(`Last refresh: ${formatAgo(state.lastRefreshAt)}. Press ${fg('cyan', 'r')} to refresh.`));
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Refresh the Status tab's data — probe /health, load config file
|
|
86
|
+
* metadata. Exported separately so the parent can re-invoke on key
|
|
87
|
+
* 'r' without re-running the full onMount flow.
|
|
88
|
+
*/
|
|
89
|
+
export async function refreshStatus(ctx) {
|
|
90
|
+
const { loadConfig } = await import('../../config-file.js');
|
|
91
|
+
const fileResult = loadConfig();
|
|
92
|
+
let health = null;
|
|
93
|
+
let error = null;
|
|
94
|
+
try {
|
|
95
|
+
const h = await ctx.client.health();
|
|
96
|
+
health = h;
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
error = e.message;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
loading: false,
|
|
103
|
+
health,
|
|
104
|
+
configSource: fileResult.source,
|
|
105
|
+
lastRefreshAt: Date.now(),
|
|
106
|
+
error,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function formatOauth(label, expiresIn) {
|
|
110
|
+
if (label === 'healthy') {
|
|
111
|
+
return fg('green', expiresIn ? `healthy (expires in ${expiresIn})` : 'healthy');
|
|
112
|
+
}
|
|
113
|
+
if (label === 'expired')
|
|
114
|
+
return fg('yellow', 'expired (refresh on next request)');
|
|
115
|
+
if (label === 'broken')
|
|
116
|
+
return fg('red', 'broken — run `dario login`');
|
|
117
|
+
if (label === 'none')
|
|
118
|
+
return dim('no credentials');
|
|
119
|
+
return label;
|
|
120
|
+
}
|
|
121
|
+
function formatAgo(ts) {
|
|
122
|
+
if (ts === 0)
|
|
123
|
+
return 'never';
|
|
124
|
+
const secs = Math.floor((Date.now() - ts) / 1000);
|
|
125
|
+
if (secs < 1)
|
|
126
|
+
return 'just now';
|
|
127
|
+
if (secs < 60)
|
|
128
|
+
return `${secs}s ago`;
|
|
129
|
+
if (secs < 3600)
|
|
130
|
+
return `${Math.floor(secs / 60)}m ago`;
|
|
131
|
+
return `${Math.floor(secs / 3600)}h ago`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level TuiApp — composes the six tabs into a single App<S>.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Render the header + tab strip + active tab body + footer
|
|
6
|
+
* - Route keys: Tab cycles, q quits, hotkeys jump, else delegate
|
|
7
|
+
* - Build per-tab TabContext (client + setState + registerCleanup)
|
|
8
|
+
* - Drive an onTick heartbeat (every 250ms) for tabs that poll
|
|
9
|
+
* - Run per-tab cleanups on tab-switch or App shutdown
|
|
10
|
+
*/
|
|
11
|
+
import { type StatusState } from './tabs/status.js';
|
|
12
|
+
import { type ConfigState } from './tabs/config.js';
|
|
13
|
+
import { type AnalyticsState } from './tabs/analytics.js';
|
|
14
|
+
import { type HitsState } from './tabs/hits.js';
|
|
15
|
+
import { type AccountsState } from './tabs/accounts.js';
|
|
16
|
+
import { type BackendsState } from './tabs/backends.js';
|
|
17
|
+
/**
|
|
18
|
+
* Composite state shape — one slice per tab plus the activeTab index.
|
|
19
|
+
* Each slice is held with its own static type so tabs don't lose
|
|
20
|
+
* type-safety; the parent dispatcher does the type-erasure when
|
|
21
|
+
* routing setState calls.
|
|
22
|
+
*/
|
|
23
|
+
export interface TuiState {
|
|
24
|
+
activeTab: number;
|
|
25
|
+
exiting: boolean;
|
|
26
|
+
status: StatusState;
|
|
27
|
+
config: ConfigState;
|
|
28
|
+
analytics: AnalyticsState;
|
|
29
|
+
hits: HitsState;
|
|
30
|
+
accounts: AccountsState;
|
|
31
|
+
backends: BackendsState;
|
|
32
|
+
}
|
|
33
|
+
export interface TuiAppOpts {
|
|
34
|
+
/** Base URL of the running proxy. Defaults to http://127.0.0.1:3456. */
|
|
35
|
+
proxyUrl?: string;
|
|
36
|
+
/** API key for the proxy (if DARIO_API_KEY is set). */
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
/** dario package version, displayed in the header. */
|
|
39
|
+
version: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function startTuiApp(opts: TuiAppOpts): Promise<void>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level TuiApp — composes the six tabs into a single App<S>.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Render the header + tab strip + active tab body + footer
|
|
6
|
+
* - Route keys: Tab cycles, q quits, hotkeys jump, else delegate
|
|
7
|
+
* - Build per-tab TabContext (client + setState + registerCleanup)
|
|
8
|
+
* - Drive an onTick heartbeat (every 250ms) for tabs that poll
|
|
9
|
+
* - Run per-tab cleanups on tab-switch or App shutdown
|
|
10
|
+
*/
|
|
11
|
+
import { App } from './app.js';
|
|
12
|
+
import { ProxyClient } from './proxy-client.js';
|
|
13
|
+
import { fg, dim } from './render.js';
|
|
14
|
+
import { renderFooter, renderHeader, renderTabStrip } from './layout.js';
|
|
15
|
+
import { StatusTab } from './tabs/status.js';
|
|
16
|
+
import { ConfigTab } from './tabs/config.js';
|
|
17
|
+
import { AnalyticsTab } from './tabs/analytics.js';
|
|
18
|
+
import { HitsTab } from './tabs/hits.js';
|
|
19
|
+
import { AccountsTab } from './tabs/accounts.js';
|
|
20
|
+
import { BackendsTab } from './tabs/backends.js';
|
|
21
|
+
const TICK_MS = 250;
|
|
22
|
+
const TABS = [
|
|
23
|
+
StatusTab,
|
|
24
|
+
ConfigTab,
|
|
25
|
+
AnalyticsTab,
|
|
26
|
+
HitsTab,
|
|
27
|
+
AccountsTab,
|
|
28
|
+
BackendsTab,
|
|
29
|
+
];
|
|
30
|
+
export function startTuiApp(opts) {
|
|
31
|
+
const proxyUrl = opts.proxyUrl ?? 'http://127.0.0.1:3456';
|
|
32
|
+
const client = new ProxyClient({ baseUrl: proxyUrl, apiKey: opts.apiKey });
|
|
33
|
+
// Per-tab cleanup queues. Each entry is the cleanup fns the tab
|
|
34
|
+
// registered during its current mount. When the user switches tabs,
|
|
35
|
+
// we run + clear the OLD tab's cleanups before mounting the new one.
|
|
36
|
+
const cleanupsByTab = new Map();
|
|
37
|
+
for (let i = 0; i < TABS.length; i++)
|
|
38
|
+
cleanupsByTab.set(i, []);
|
|
39
|
+
const initialState = {
|
|
40
|
+
activeTab: 0,
|
|
41
|
+
exiting: false,
|
|
42
|
+
status: StatusTab.initialState(),
|
|
43
|
+
config: ConfigTab.initialState(),
|
|
44
|
+
analytics: AnalyticsTab.initialState(),
|
|
45
|
+
hits: HitsTab.initialState(),
|
|
46
|
+
accounts: AccountsTab.initialState(),
|
|
47
|
+
backends: BackendsTab.initialState(),
|
|
48
|
+
};
|
|
49
|
+
// Forward-declared App ref so the onKey closure can reference it
|
|
50
|
+
// before construction. TS needs the explicit annotation since
|
|
51
|
+
// strict mode disallows referencing a let-binding inside its
|
|
52
|
+
// own initializer expression.
|
|
53
|
+
let app;
|
|
54
|
+
app = new App({
|
|
55
|
+
initialState,
|
|
56
|
+
render: (state, dim_) => renderTui(state, dim_, opts.version, proxyUrl),
|
|
57
|
+
onKey: (state, key) => onKey(state, key, app, client, cleanupsByTab),
|
|
58
|
+
afterFrame: () => { },
|
|
59
|
+
});
|
|
60
|
+
// Mount the initial tab. The first tab's onMount fires before the
|
|
61
|
+
// first redraw — that means the loading state shows briefly until
|
|
62
|
+
// the async data arrives. Acceptable for v4.0; could be optimized
|
|
63
|
+
// by pre-fetching before app.start().
|
|
64
|
+
void mountTab(app, client, cleanupsByTab, initialState.activeTab);
|
|
65
|
+
// Tick heartbeat. Calls the active tab's onTick (if any) every
|
|
66
|
+
// TICK_MS. Each tab decides whether to act.
|
|
67
|
+
const tickInterval = setInterval(() => {
|
|
68
|
+
const s = app.getState();
|
|
69
|
+
const tab = TABS[s.activeTab];
|
|
70
|
+
if (tab.onTick) {
|
|
71
|
+
const ctx = makeContext(app, client, cleanupsByTab, s.activeTab);
|
|
72
|
+
tab.onTick(stateOf(s, s.activeTab), ctx);
|
|
73
|
+
}
|
|
74
|
+
}, TICK_MS);
|
|
75
|
+
// Global cleanup — fires when app.start()'s returned promise resolves
|
|
76
|
+
// (i.e. when App.stop() runs).
|
|
77
|
+
return app.start().finally(() => {
|
|
78
|
+
clearInterval(tickInterval);
|
|
79
|
+
// Run all per-tab cleanups
|
|
80
|
+
for (const fns of cleanupsByTab.values()) {
|
|
81
|
+
for (const fn of fns) {
|
|
82
|
+
try {
|
|
83
|
+
fn();
|
|
84
|
+
}
|
|
85
|
+
catch { /* ignore */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ── Wiring ─────────────────────────────────────────────────────────
|
|
91
|
+
function onKey(state, key, app, client, cleanupsByTab) {
|
|
92
|
+
// ── Global keys ────────────────────────────────────────────
|
|
93
|
+
// q quits — but only when NOT inside an edit field (the Config
|
|
94
|
+
// tab's editor uses 'q' as a literal character).
|
|
95
|
+
const tab = TABS[state.activeTab];
|
|
96
|
+
const inEdit = state.activeTab === 1 /* config */ && state.config.editBuffer !== null;
|
|
97
|
+
if (!inEdit) {
|
|
98
|
+
if (key.name === 'printable' && key.ch === 'q' && !key.ctrl) {
|
|
99
|
+
app.stop();
|
|
100
|
+
return { ...state, exiting: true };
|
|
101
|
+
}
|
|
102
|
+
// Tab cycles forward; Shift+Tab cycles back. Some terminals send
|
|
103
|
+
// Shift+Tab as ESC[Z (BackTab) — parseKeys reports as 'unknown'
|
|
104
|
+
// currently. Listed for v4.x.
|
|
105
|
+
if (key.name === 'tab') {
|
|
106
|
+
return switchTab(state, (state.activeTab + 1) % TABS.length, app, client, cleanupsByTab);
|
|
107
|
+
}
|
|
108
|
+
// Hotkey jump
|
|
109
|
+
if (key.name === 'printable' && !key.ctrl) {
|
|
110
|
+
for (let i = 0; i < TABS.length; i++) {
|
|
111
|
+
if (TABS[i].hotkey === key.ch) {
|
|
112
|
+
return switchTab(state, i, app, client, cleanupsByTab);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Delegate to the active tab's onKey
|
|
118
|
+
const tabSlice = stateOf(state, state.activeTab);
|
|
119
|
+
const nextSlice = tab.onKey?.(tabSlice, key);
|
|
120
|
+
if (nextSlice !== undefined && nextSlice !== tabSlice) {
|
|
121
|
+
return withTabState(state, state.activeTab, nextSlice);
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function switchTab(state, newIdx, app, client, cleanupsByTab) {
|
|
126
|
+
if (newIdx === state.activeTab)
|
|
127
|
+
return state;
|
|
128
|
+
// Run OLD tab's cleanup queue + clear it
|
|
129
|
+
const oldCleanups = cleanupsByTab.get(state.activeTab) ?? [];
|
|
130
|
+
for (const fn of oldCleanups) {
|
|
131
|
+
try {
|
|
132
|
+
fn();
|
|
133
|
+
}
|
|
134
|
+
catch { /* ignore */ }
|
|
135
|
+
}
|
|
136
|
+
cleanupsByTab.set(state.activeTab, []);
|
|
137
|
+
// Call the old tab's onUnmount (synchronous-only)
|
|
138
|
+
const oldTab = TABS[state.activeTab];
|
|
139
|
+
oldTab.onUnmount?.(stateOf(state, state.activeTab));
|
|
140
|
+
// Mount the new tab (fire-and-forget; async state updates via setState)
|
|
141
|
+
void mountTab(app, client, cleanupsByTab, newIdx);
|
|
142
|
+
return { ...state, activeTab: newIdx };
|
|
143
|
+
}
|
|
144
|
+
async function mountTab(app, client, cleanupsByTab, idx) {
|
|
145
|
+
const tab = TABS[idx];
|
|
146
|
+
if (!tab.onMount)
|
|
147
|
+
return;
|
|
148
|
+
const ctx = makeContext(app, client, cleanupsByTab, idx);
|
|
149
|
+
const sliceBefore = stateOf(app.getState(), idx);
|
|
150
|
+
const result = await tab.onMount(sliceBefore, ctx);
|
|
151
|
+
if (result !== undefined) {
|
|
152
|
+
app.setState((s) => withTabState(s, idx, result));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function makeContext(app, client, cleanupsByTab, idx) {
|
|
156
|
+
return {
|
|
157
|
+
client,
|
|
158
|
+
setState: (updater) => {
|
|
159
|
+
app.setState((s) => {
|
|
160
|
+
const currentSlice = stateOf(s, idx);
|
|
161
|
+
const nextSlice = typeof updater === 'function'
|
|
162
|
+
? updater(currentSlice)
|
|
163
|
+
: { ...currentSlice, ...updater };
|
|
164
|
+
return withTabState(s, idx, nextSlice);
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
registerCleanup: (fn) => {
|
|
168
|
+
cleanupsByTab.get(idx)?.push(fn);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Read the state slice for tab `idx`. */
|
|
173
|
+
function stateOf(s, idx) {
|
|
174
|
+
const key = TABS[idx].id;
|
|
175
|
+
return s[key];
|
|
176
|
+
}
|
|
177
|
+
/** Return a new TuiState with the slice for tab `idx` replaced. */
|
|
178
|
+
function withTabState(s, idx, sliceVal) {
|
|
179
|
+
const key = TABS[idx].id;
|
|
180
|
+
return { ...s, [key]: sliceVal };
|
|
181
|
+
}
|
|
182
|
+
// ── Rendering ───────────────────────────────────────────────────
|
|
183
|
+
function renderTui(state, dim_, version, proxyUrl) {
|
|
184
|
+
const cols = dim_.cols;
|
|
185
|
+
const rows = dim_.rows;
|
|
186
|
+
const out = [];
|
|
187
|
+
// Row 1: header
|
|
188
|
+
out.push(renderHeader(cols, { version, status: proxyUrl }));
|
|
189
|
+
// Row 2: tab strip
|
|
190
|
+
const tabLabels = TABS.map(t => t.label);
|
|
191
|
+
out.push(renderTabStrip(cols, tabLabels, state.activeTab));
|
|
192
|
+
out.push(dim('─'.repeat(cols)));
|
|
193
|
+
// Body — passed (cols, rows-5) so the tab knows it has rows 4..rows-2
|
|
194
|
+
const bodyRows = rows - 5;
|
|
195
|
+
const tab = TABS[state.activeTab];
|
|
196
|
+
const slice = stateOf(state, state.activeTab);
|
|
197
|
+
const body = tab.render(slice, { cols, rows: bodyRows });
|
|
198
|
+
out.push(body);
|
|
199
|
+
// Footer — fixed key hints (tab-cycling stays universal; per-tab
|
|
200
|
+
// hints are inside each tab body to keep the global footer stable).
|
|
201
|
+
const footerHints = [
|
|
202
|
+
{ key: 'Tab', label: 'next tab' },
|
|
203
|
+
{ key: 'q', label: 'quit' },
|
|
204
|
+
{ key: 'r', label: 'refresh' },
|
|
205
|
+
];
|
|
206
|
+
// Pad body to fill rows before the footer so the footer's row stays
|
|
207
|
+
// at the bottom (close to it — slight underflow OK; row count
|
|
208
|
+
// depends on each tab's content).
|
|
209
|
+
const bodyLines = body.split('\n').length;
|
|
210
|
+
if (bodyLines < bodyRows) {
|
|
211
|
+
out.push(''.padEnd(bodyRows - bodyLines, '\n'));
|
|
212
|
+
}
|
|
213
|
+
out.push(renderFooter(cols, footerHints));
|
|
214
|
+
// Connect with newlines
|
|
215
|
+
void fg; // silence unused if neither tab uses it through this module
|
|
216
|
+
return out.join('\n');
|
|
217
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|