becki 0.5.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/LICENSE +32 -0
- package/README.md +57 -0
- package/bin/becki.js +31 -0
- package/dist/cli.js +77 -0
- package/dist/client.js +286 -0
- package/dist/commands.js +321 -0
- package/dist/config.js +138 -0
- package/dist/entry.js +14 -0
- package/dist/handoff.js +54 -0
- package/dist/loops.js +127 -0
- package/dist/menu.js +108 -0
- package/dist/prompt.js +121 -0
- package/dist/screens/_shared.js +67 -0
- package/dist/screens/account.js +35 -0
- package/dist/screens/backfills.js +92 -0
- package/dist/screens/diagnostics.js +87 -0
- package/dist/screens/git.js +211 -0
- package/dist/screens/install-token.js +65 -0
- package/dist/screens/logs.js +74 -0
- package/dist/screens/status.js +77 -0
- package/dist/screens/subscription.js +60 -0
- package/dist/screens/vault.js +155 -0
- package/dist/screens/watch-folders.js +406 -0
- package/dist/setup.js +71 -0
- package/dist/splash.js +27 -0
- package/dist/theme.js +38 -0
- package/dist/vault.js +357 -0
- package/package.json +59 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* git.tsx — manage a GitHub personal access token for cloud git ingest.
|
|
4
|
+
*
|
|
5
|
+
* Stores the PAT at ${BECKI_HOME}/secrets.json (mode 0600). The token is
|
|
6
|
+
* never sent to the Becki backend from this CLI — that's Core v1.3 (#193).
|
|
7
|
+
* For now the local becki-mcp can use it for direct GitHub API calls when
|
|
8
|
+
* a watch folder is also a github.com origin.
|
|
9
|
+
*
|
|
10
|
+
* Light validation: HEAD https://api.github.com/user with the token; expect
|
|
11
|
+
* 200. No-op on failure (we don't have offline-clean validation).
|
|
12
|
+
*/
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { Box, Text, useInput } from 'ink';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { PATHS } from '../client.js';
|
|
18
|
+
import { ScreenHeader, Kv, BackFooter, OkLine, ErrorLine, Loading } from './_shared.js';
|
|
19
|
+
import { theme } from '../theme.js';
|
|
20
|
+
const PROVIDERS = {
|
|
21
|
+
github: {
|
|
22
|
+
id: 'github',
|
|
23
|
+
label: 'GitHub',
|
|
24
|
+
validateUrl: 'https://api.github.com/user',
|
|
25
|
+
authHeaders: (t) => ({
|
|
26
|
+
Authorization: `Bearer ${t}`,
|
|
27
|
+
'User-Agent': 'becki-shell',
|
|
28
|
+
Accept: 'application/vnd.github+json',
|
|
29
|
+
}),
|
|
30
|
+
extractUser: (j) => j?.login,
|
|
31
|
+
helpUrl: 'github.com → Settings → Developer settings → Personal access tokens',
|
|
32
|
+
},
|
|
33
|
+
gitlab: {
|
|
34
|
+
id: 'gitlab',
|
|
35
|
+
label: 'GitLab',
|
|
36
|
+
validateUrl: 'https://gitlab.com/api/v4/user',
|
|
37
|
+
authHeaders: (t) => ({ 'PRIVATE-TOKEN': t, 'User-Agent': 'becki-shell' }),
|
|
38
|
+
extractUser: (j) => j?.username,
|
|
39
|
+
helpUrl: 'gitlab.com → Edit profile → Access tokens',
|
|
40
|
+
},
|
|
41
|
+
bitbucket: {
|
|
42
|
+
id: 'bitbucket',
|
|
43
|
+
label: 'Bitbucket',
|
|
44
|
+
validateUrl: 'https://api.bitbucket.org/2.0/user',
|
|
45
|
+
// Bitbucket API tokens use Basic with username:app_password OR Bearer for
|
|
46
|
+
// workspace access tokens. We treat the input as a Bearer token here —
|
|
47
|
+
// workspace access tokens are the recommended flow per Atlassian docs.
|
|
48
|
+
authHeaders: (t) => ({ Authorization: `Bearer ${t}`, 'User-Agent': 'becki-shell' }),
|
|
49
|
+
extractUser: (j) => j?.username
|
|
50
|
+
?? j?.nickname,
|
|
51
|
+
helpUrl: 'bitbucket.org → Personal settings → App passwords (or Workspace access tokens)',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const PROVIDER_ORDER = ['github', 'gitlab', 'bitbucket'];
|
|
55
|
+
const SECRETS_PATH = join(PATHS.beckiHome, 'secrets.json');
|
|
56
|
+
function loadSecrets() {
|
|
57
|
+
try {
|
|
58
|
+
if (!existsSync(SECRETS_PATH))
|
|
59
|
+
return {};
|
|
60
|
+
const raw = JSON.parse(readFileSync(SECRETS_PATH, 'utf8'));
|
|
61
|
+
// Auto-migrate legacy github_pat → git_tokens.github so the UI is
|
|
62
|
+
// consistent. We don't write back until the user does something — the
|
|
63
|
+
// legacy fields stay readable in the file for one minor version.
|
|
64
|
+
if (raw.github_pat && !raw.git_tokens?.github) {
|
|
65
|
+
raw.git_tokens = { ...(raw.git_tokens ?? {}) };
|
|
66
|
+
raw.git_tokens.github = {
|
|
67
|
+
token: raw.github_pat,
|
|
68
|
+
login: raw.github_login,
|
|
69
|
+
validated_at: raw.github_pat_validated_at ?? Date.now(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return raw;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function saveSecrets(s) {
|
|
79
|
+
mkdirSync(PATHS.beckiHome, { recursive: true });
|
|
80
|
+
writeFileSync(SECRETS_PATH, JSON.stringify(s, null, 2) + '\n', { mode: 0o600 });
|
|
81
|
+
try {
|
|
82
|
+
chmodSync(SECRETS_PATH, 0o600);
|
|
83
|
+
}
|
|
84
|
+
catch { /* best effort */ }
|
|
85
|
+
}
|
|
86
|
+
async function validatePat(provider, token) {
|
|
87
|
+
const spec = PROVIDERS[provider];
|
|
88
|
+
try {
|
|
89
|
+
const r = await fetch(spec.validateUrl, { headers: spec.authHeaders(token) });
|
|
90
|
+
if (r.status === 401)
|
|
91
|
+
return { ok: false, reason: 'token invalid (401)' };
|
|
92
|
+
if (r.status === 403)
|
|
93
|
+
return { ok: false, reason: 'token forbidden (403) — check scopes' };
|
|
94
|
+
if (!r.ok)
|
|
95
|
+
return { ok: false, reason: `${spec.label} ${r.status}` };
|
|
96
|
+
const j = await r.json();
|
|
97
|
+
return { ok: true, login: spec.extractUser(j) };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { ok: false, reason: err.message };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function mask(t) {
|
|
104
|
+
if (t.length <= 12)
|
|
105
|
+
return t;
|
|
106
|
+
return `${t.slice(0, 6)}…${t.slice(-4)}`;
|
|
107
|
+
}
|
|
108
|
+
export function GitScreen({ onBack }) {
|
|
109
|
+
const [secrets, setSecrets] = useState(loadSecrets());
|
|
110
|
+
const [mode, setMode] = useState('view');
|
|
111
|
+
const [provider, setProvider] = useState('github');
|
|
112
|
+
const [input, setInput] = useState('');
|
|
113
|
+
const [msg, setMsg] = useState(null);
|
|
114
|
+
const [cursor, setCursor] = useState(0); // selects which provider row in view mode
|
|
115
|
+
const tokenFor = (p) => secrets.git_tokens?.[p];
|
|
116
|
+
const handleSave = async (token) => {
|
|
117
|
+
setMode('validating');
|
|
118
|
+
const result = await validatePat(provider, token);
|
|
119
|
+
if (!result.ok) {
|
|
120
|
+
setMsg({ kind: 'err', text: result.reason ?? 'validation failed' });
|
|
121
|
+
setMode('edit');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const next = {
|
|
125
|
+
...secrets,
|
|
126
|
+
git_tokens: {
|
|
127
|
+
...(secrets.git_tokens ?? {}),
|
|
128
|
+
[provider]: {
|
|
129
|
+
token,
|
|
130
|
+
login: result.login,
|
|
131
|
+
validated_at: Date.now(),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
saveSecrets(next);
|
|
137
|
+
setSecrets(next);
|
|
138
|
+
setMsg({ kind: 'ok', text: `${PROVIDERS[provider].label} saved · validated as ${result.login ?? 'unknown'}` });
|
|
139
|
+
setMode('view');
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
setMsg({ kind: 'err', text: `save failed: ${err.message}` });
|
|
143
|
+
setMode('edit');
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const handleDelete = (p) => {
|
|
147
|
+
if (!secrets.git_tokens?.[p])
|
|
148
|
+
return;
|
|
149
|
+
const nextTokens = { ...(secrets.git_tokens ?? {}) };
|
|
150
|
+
delete nextTokens[p];
|
|
151
|
+
const next = { ...secrets, git_tokens: nextTokens };
|
|
152
|
+
saveSecrets(next);
|
|
153
|
+
setSecrets(next);
|
|
154
|
+
setMsg({ kind: 'ok', text: `${PROVIDERS[p].label} token removed` });
|
|
155
|
+
};
|
|
156
|
+
useInput((typed, key) => {
|
|
157
|
+
if (mode === 'validating')
|
|
158
|
+
return;
|
|
159
|
+
if (mode === 'edit') {
|
|
160
|
+
if (key.escape) {
|
|
161
|
+
setMode('view');
|
|
162
|
+
setInput('');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (key.return) {
|
|
166
|
+
if (input.trim())
|
|
167
|
+
handleSave(input.trim());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key.backspace || key.delete) {
|
|
171
|
+
setInput((s) => s.slice(0, -1));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (typed && !key.ctrl && !key.meta)
|
|
175
|
+
setInput((s) => s + typed);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// view mode — cursor moves between provider rows
|
|
179
|
+
if (key.escape || key.leftArrow) {
|
|
180
|
+
onBack();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (key.upArrow || typed === 'k')
|
|
184
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
185
|
+
else if (key.downArrow || typed === 'j')
|
|
186
|
+
setCursor((c) => Math.min(PROVIDER_ORDER.length - 1, c + 1));
|
|
187
|
+
else if (typed === 'e' || key.return) {
|
|
188
|
+
const target = PROVIDER_ORDER[cursor];
|
|
189
|
+
setProvider(target);
|
|
190
|
+
setMode('edit');
|
|
191
|
+
setInput('');
|
|
192
|
+
setMsg(null);
|
|
193
|
+
}
|
|
194
|
+
else if (typed === 'd') {
|
|
195
|
+
const target = PROVIDER_ORDER[cursor];
|
|
196
|
+
if (tokenFor(target))
|
|
197
|
+
handleDelete(target);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
if (mode === 'edit') {
|
|
201
|
+
const spec = PROVIDERS[provider];
|
|
202
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: `Paste ${spec.label} token`, subtitle: spec.helpUrl }), _jsxs(Box, { children: [_jsx(Text, { color: theme.gold, children: "\u25B8 " }), _jsx(Text, { color: theme.text, children: '*'.repeat(input.length) }), _jsx(Text, { color: theme.gold, children: "_" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "enter validate+save \u00B7 esc cancel" }) })] }));
|
|
203
|
+
}
|
|
204
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Git", subtitle: "provider tokens (cloud ingest \u2014 wired in Core v1.3)" }), _jsx(Kv, { k: "Stored at", v: SECRETS_PATH }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: PROVIDER_ORDER.map((p, i) => {
|
|
205
|
+
const sel = i === cursor;
|
|
206
|
+
const entry = tokenFor(p);
|
|
207
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(Text, { color: sel ? theme.gold : theme.gray, children: sel ? '▸ ' : ' ' }) }), _jsx(Box, { width: 12, children: _jsx(Text, { color: sel ? theme.gold : theme.text, bold: sel, children: PROVIDERS[p].label }) }), _jsx(Box, { width: 16, children: _jsx(Text, { color: entry ? theme.text : 'yellow', children: entry ? mask(entry.token) : 'not set' }) }), _jsx(Box, { width: 20, children: _jsx(Text, { color: theme.gray, children: entry?.login ? `@${entry.login}` : '—' }) }), _jsx(Text, { color: theme.gray, dimColor: true, children: entry?.validated_at
|
|
208
|
+
? new Date(entry.validated_at).toLocaleDateString()
|
|
209
|
+
: '' })] }, p));
|
|
210
|
+
}) }), mode === 'validating' && (_jsx(Box, { marginTop: 1, children: _jsx(Loading, { label: `validating against ${PROVIDERS[provider].validateUrl}…` }) })), _jsx(Box, { marginTop: 1, children: msg && (msg.kind === 'ok' ? _jsx(OkLine, { message: msg.text }) : _jsx(ErrorLine, { message: msg.text })) }), _jsx(BackFooter, { extra: "\u2191/\u2193 pick provider \u00B7 e/enter add or replace \u00B7 d delete" })] }));
|
|
211
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* install-token.tsx — show, regenerate, copy the local install token.
|
|
4
|
+
*
|
|
5
|
+
* Token lives at ${BECKI_HOME}/mcp-ingest-token (0600). Regenerating issues
|
|
6
|
+
* a fresh token via the register-mcp-install edge function — requires user
|
|
7
|
+
* JWT, which the local CLI doesn't hold. So "regenerate" defers to the web
|
|
8
|
+
* portal; this screen shows + masks + copies what's on disk.
|
|
9
|
+
*/
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { Box, Text, useInput } from 'ink';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { platform } from 'node:os';
|
|
14
|
+
import { resolveAuth, PATHS } from '../client.js';
|
|
15
|
+
import { ScreenHeader, Kv, BackFooter, OkLine } from './_shared.js';
|
|
16
|
+
import { theme } from '../theme.js';
|
|
17
|
+
function copyToClipboard(text) {
|
|
18
|
+
try {
|
|
19
|
+
const cmd = platform() === 'darwin' ? 'pbcopy' :
|
|
20
|
+
platform() === 'win32' ? 'clip' :
|
|
21
|
+
'xclip';
|
|
22
|
+
const args = platform() === 'linux' ? ['-selection', 'clipboard'] : [];
|
|
23
|
+
const proc = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
24
|
+
proc.stdin?.write(text);
|
|
25
|
+
proc.stdin?.end();
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function openInBrowser(url) {
|
|
33
|
+
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'cmd' : 'xdg-open';
|
|
34
|
+
const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
35
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
36
|
+
}
|
|
37
|
+
function mask(token) {
|
|
38
|
+
if (token.length <= 12)
|
|
39
|
+
return token;
|
|
40
|
+
return `${token.slice(0, 6)}…${token.slice(-4)}`;
|
|
41
|
+
}
|
|
42
|
+
export function InstallTokenScreen({ onBack }) {
|
|
43
|
+
const auth = resolveAuth();
|
|
44
|
+
const [revealed, setRevealed] = useState(false);
|
|
45
|
+
const [copied, setCopied] = useState(false);
|
|
46
|
+
const [opened, setOpened] = useState(false);
|
|
47
|
+
const tokenPresent = !!auth.ingestToken;
|
|
48
|
+
const tokenRaw = auth.ingestToken ?? '';
|
|
49
|
+
useInput((input, key) => {
|
|
50
|
+
if (key.escape || key.leftArrow)
|
|
51
|
+
onBack();
|
|
52
|
+
else if (input === 's')
|
|
53
|
+
setRevealed((r) => !r);
|
|
54
|
+
else if (input === 'c' && tokenPresent) {
|
|
55
|
+
const ok = copyToClipboard(tokenRaw);
|
|
56
|
+
if (ok)
|
|
57
|
+
setCopied(true);
|
|
58
|
+
}
|
|
59
|
+
else if (input === 'r') {
|
|
60
|
+
openInBrowser('https://www.becki.io/account');
|
|
61
|
+
setOpened(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Install Token", subtitle: "authenticates becki-mcp to the Becki backend" }), _jsx(Kv, { k: "Path", v: PATHS.ingestToken }), _jsx(Kv, { k: "Status", v: tokenPresent ? 'present' : 'missing', color: tokenPresent ? 'green' : 'yellow' }), tokenPresent && (_jsx(Kv, { k: "Token", v: revealed ? tokenRaw : mask(tokenRaw) })), !tokenPresent && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "No token. Generate one at becki.io/account, then paste into:" }) })), !tokenPresent && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: theme.gray, children: PATHS.ingestToken }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [copied && _jsx(OkLine, { message: "copied to clipboard" }), opened && _jsx(OkLine, { message: "opened becki.io/account in your browser" })] }), _jsx(BackFooter, { extra: tokenPresent ? 's show/hide · c copy · r regenerate (browser)' : 'r regenerate (browser)' })] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* logs.tsx — show the tail of becki-mcp's log file.
|
|
4
|
+
*
|
|
5
|
+
* becki-mcp doesn't currently write a structured log file (it writes to
|
|
6
|
+
* stderr, which AI clients capture). When it does (planned in Core v0.7),
|
|
7
|
+
* this screen tails ${BECKI_HOME}/logs/becki-mcp.log. Until then, the
|
|
8
|
+
* screen surfaces that and offers a one-key "open log dir" handoff.
|
|
9
|
+
*/
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { Box, Text, useInput } from 'ink';
|
|
12
|
+
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { platform } from 'node:os';
|
|
16
|
+
import { PATHS } from '../client.js';
|
|
17
|
+
import { ScreenHeader, Kv, BackFooter, OkLine } from './_shared.js';
|
|
18
|
+
import { theme } from '../theme.js';
|
|
19
|
+
const LOG_DIR = join(PATHS.beckiHome, 'logs');
|
|
20
|
+
const LOG_PATH = join(LOG_DIR, 'becki-mcp.log');
|
|
21
|
+
const TAIL_LINES = 30;
|
|
22
|
+
function readTail() {
|
|
23
|
+
if (!existsSync(LOG_PATH))
|
|
24
|
+
return { lines: [], mtime: null, size: 0 };
|
|
25
|
+
const stat = statSync(LOG_PATH);
|
|
26
|
+
// Read last ~64KB to extract tail without loading huge files.
|
|
27
|
+
const slice = Math.min(stat.size, 64 * 1024);
|
|
28
|
+
const buf = Buffer.alloc(slice);
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const fd = fs.openSync(LOG_PATH, 'r');
|
|
32
|
+
try {
|
|
33
|
+
fs.readSync(fd, buf, 0, slice, stat.size - slice);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
fs.closeSync(fd);
|
|
37
|
+
}
|
|
38
|
+
const lines = buf.toString('utf8').split('\n').filter((l) => l.length > 0);
|
|
39
|
+
return { lines: lines.slice(-TAIL_LINES), mtime: stat.mtimeMs, size: stat.size };
|
|
40
|
+
}
|
|
41
|
+
function openDir(dir) {
|
|
42
|
+
try {
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
catch { /* ignore */ }
|
|
46
|
+
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'explorer' : 'xdg-open';
|
|
47
|
+
spawn(cmd, [dir], { detached: true, stdio: 'ignore' }).unref();
|
|
48
|
+
}
|
|
49
|
+
function fmtSize(bytes) {
|
|
50
|
+
if (bytes < 1024)
|
|
51
|
+
return `${bytes} B`;
|
|
52
|
+
if (bytes < 1024 * 1024)
|
|
53
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
54
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
55
|
+
}
|
|
56
|
+
export function LogsScreen({ onBack }) {
|
|
57
|
+
const [data, setData] = useState(readTail());
|
|
58
|
+
const [opened, setOpened] = useState(false);
|
|
59
|
+
useInput((input, key) => {
|
|
60
|
+
if (key.escape || key.leftArrow) {
|
|
61
|
+
onBack();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (input === 'r')
|
|
65
|
+
setData(readTail());
|
|
66
|
+
else if (input === 'o') {
|
|
67
|
+
openDir(LOG_DIR);
|
|
68
|
+
setOpened(true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Logs", subtitle: `tail of ${LOG_PATH}` }), _jsx(Kv, { k: "Path", v: LOG_PATH }), _jsx(Kv, { k: "Size", v: data.size > 0 ? fmtSize(data.size) : 'absent', color: data.size > 0 ? theme.text : theme.gray }), _jsx(Kv, { k: "Modified", v: data.mtime ? new Date(data.mtime).toLocaleString() : '—' }), _jsx(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: theme.gray, paddingX: 1, children: data.lines.length === 0
|
|
72
|
+
? _jsx(Text, { color: theme.gray, dimColor: true, children: "(no log file \u2014 becki-mcp writes to stderr today; structured logging ships v0.7)" })
|
|
73
|
+
: data.lines.map((l, i) => _jsx(Text, { color: theme.text, children: l }, i)) }), _jsx(Box, { marginTop: 1, children: opened && _jsx(OkLine, { message: "opened log directory" }) }), _jsx(BackFooter, { extra: "r refresh \u00B7 o open log dir" })] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir, platform } from 'node:os';
|
|
6
|
+
import { resolveAuth, PATHS, listProjects } from '../client.js';
|
|
7
|
+
import { ScreenHeader, Kv, BackFooter, Loading, ErrorLine, useAsync } from './_shared.js';
|
|
8
|
+
import { theme } from '../theme.js';
|
|
9
|
+
async function loadStatus() {
|
|
10
|
+
const auth = resolveAuth();
|
|
11
|
+
const cachePath = join(PATHS.beckiHome, 'cache.db');
|
|
12
|
+
const cacheExists = existsSync(cachePath);
|
|
13
|
+
const cacheSize = cacheExists ? statSync(cachePath).size : 0;
|
|
14
|
+
// Vault count via the SECURITY DEFINER project-rollup RPC (anon + explicit
|
|
15
|
+
// user_id bypasses RLS properly; the previous anon SELECT on
|
|
16
|
+
// vault_embeddings returned 0 because RLS evaluated auth.uid()=null).
|
|
17
|
+
let vaultCount = null;
|
|
18
|
+
let projectCount = 0;
|
|
19
|
+
if (auth.userId) {
|
|
20
|
+
try {
|
|
21
|
+
const projects = await listProjects(auth.userId);
|
|
22
|
+
projectCount = projects.length;
|
|
23
|
+
vaultCount = projects.reduce((sum, p) => sum + (p.atomic_count ?? 0), 0);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
vaultCount = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Detect the Mac.app's presence — true iff resolveBeckiHome() landed on
|
|
30
|
+
// the legacy Mac path (which only happens when that path actually exists).
|
|
31
|
+
const legacyMacPath = join(homedir(), 'Library', 'Application Support', 'Becki');
|
|
32
|
+
const studioDetected = platform() === 'darwin' && PATHS.beckiHome === legacyMacPath;
|
|
33
|
+
let studioDbs = [];
|
|
34
|
+
if (studioDetected) {
|
|
35
|
+
try {
|
|
36
|
+
studioDbs = readdirSync(PATHS.beckiHome)
|
|
37
|
+
.filter((f) => /\.(db|sqlite|sqlite3)$/i.test(f) && f !== 'cache.db')
|
|
38
|
+
.sort();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
studioDbs = [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
installTokenSet: !!auth.ingestToken,
|
|
46
|
+
userId: auth.userId,
|
|
47
|
+
beckiHome: PATHS.beckiHome,
|
|
48
|
+
cacheDbExists: cacheExists,
|
|
49
|
+
cacheDbSize: cacheSize,
|
|
50
|
+
lastDigestAt: null, // wired via becki-mcp cache.db read in a follow-up
|
|
51
|
+
vaultCount,
|
|
52
|
+
projectCount,
|
|
53
|
+
tokensToday: 0,
|
|
54
|
+
tokensTodayLimit: 80_000,
|
|
55
|
+
studioDetected,
|
|
56
|
+
studioDbs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function fmtSize(bytes) {
|
|
60
|
+
if (bytes < 1024)
|
|
61
|
+
return `${bytes} B`;
|
|
62
|
+
if (bytes < 1024 * 1024)
|
|
63
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
64
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
65
|
+
}
|
|
66
|
+
export function StatusScreen({ onBack }) {
|
|
67
|
+
const { loading, data, error, refresh } = useAsync(loadStatus);
|
|
68
|
+
useInput((input, key) => {
|
|
69
|
+
if (key.escape || key.leftArrow)
|
|
70
|
+
onBack();
|
|
71
|
+
else if (input === 'r')
|
|
72
|
+
refresh();
|
|
73
|
+
});
|
|
74
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Status", subtitle: "becki-mcp + vault snapshot" }), loading && _jsx(Loading, {}), error && _jsx(ErrorLine, { message: error }), data && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Kv, { k: "Becki home", v: data.beckiHome }), _jsx(Kv, { k: "Install token", v: data.installTokenSet ? 'set' : 'missing — run becki-mcp init or generate at /account', color: data.installTokenSet ? theme.text : 'yellow' }), _jsx(Kv, { k: "User ID", v: data.userId ?? 'not set', color: data.userId ? theme.text : 'yellow' }), _jsx(Kv, { k: "Core cache DB", v: data.cacheDbExists ? `present · ${fmtSize(data.cacheDbSize)}` : 'not yet created', color: data.cacheDbExists ? theme.text : theme.gray }), _jsx(Kv, { k: "Vault entries", v: data.vaultCount === null ? '— (auth required)' : `~${data.vaultCount.toLocaleString()} across ${data.projectCount} project${data.projectCount === 1 ? '' : 's'}`, color: data.vaultCount === null ? theme.gray : theme.text }), _jsx(Kv, { k: "Last digest", v: data.lastDigestAt ?? 'never (or not tracked yet — wired in v0.6)', color: data.lastDigestAt ? theme.text : theme.gray }), _jsx(Kv, { k: "Session-ingest budget", v: `${data.tokensToday.toLocaleString()} / ${data.tokensTodayLimit.toLocaleString()} Haiku tokens today` }), data.studioDetected && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.gold, children: "\u25C6 Studio (Mac app) detected" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(Text, { color: theme.gray, children: "Becki shares the same Becki home dir, but Studio keeps" }), _jsx(Text, { color: theme.gray, children: "its own SQLite stores. Visible here (not managed by `becki`):" }), data.studioDbs.length === 0
|
|
75
|
+
? _jsxs(Text, { color: theme.gray, dimColor: true, children: [" (no other .db files in ", data.beckiHome, ")"] })
|
|
76
|
+
: data.studioDbs.map((f) => (_jsxs(Text, { color: theme.gray, dimColor: true, children: [" \u00B7 ", f] }, f))), _jsx(Text, { color: theme.gray, dimColor: true, children: "Vault entries above counts what's in NeuraVault cloud (shared)." })] })] }))] })), _jsx(BackFooter, { extra: "r refresh" })] }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* subscription.tsx — display tier + status, open Stripe Customer Portal.
|
|
4
|
+
*
|
|
5
|
+
* Reads becki.subscriptions via the anon API scoped to auth.userId.
|
|
6
|
+
* Stripe Portal session is created server-side via the existing
|
|
7
|
+
* create-portal-session edge function; user JWT required for that call,
|
|
8
|
+
* so this screen instead deep-links to /account where the JWT lives.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Box, Text, useInput } from 'ink';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { platform } from 'node:os';
|
|
14
|
+
import { resolveAuth, SUPABASE_URL, SUPABASE_ANON_KEY } from '../client.js';
|
|
15
|
+
import { ScreenHeader, Kv, BackFooter, Loading, ErrorLine, OkLine, useAsync } from './_shared.js';
|
|
16
|
+
import { theme } from '../theme.js';
|
|
17
|
+
async function loadSubscription(userId) {
|
|
18
|
+
if (!userId)
|
|
19
|
+
return null;
|
|
20
|
+
const r = await fetch(`${SUPABASE_URL}/rest/v1/subscriptions?select=plan,status,current_period_end,cancel_at_period_end,is_comped&user_id=eq.${userId}`, {
|
|
21
|
+
headers: {
|
|
22
|
+
apikey: SUPABASE_ANON_KEY,
|
|
23
|
+
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
24
|
+
'Accept-Profile': 'becki',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (!r.ok)
|
|
28
|
+
throw new Error(`subscriptions ${r.status}`);
|
|
29
|
+
const rows = (await r.json());
|
|
30
|
+
return rows[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
function openInBrowser(url) {
|
|
33
|
+
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'cmd' : 'xdg-open';
|
|
34
|
+
const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
35
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
36
|
+
}
|
|
37
|
+
export function SubscriptionScreen({ onBack }) {
|
|
38
|
+
const auth = resolveAuth();
|
|
39
|
+
const { loading, data, error } = useAsync(() => loadSubscription(auth.userId), [auth.userId]);
|
|
40
|
+
const [opened, setOpened] = React.useState(false);
|
|
41
|
+
useInput((input, key) => {
|
|
42
|
+
if (key.escape || key.leftArrow)
|
|
43
|
+
onBack();
|
|
44
|
+
else if (input === 'p') {
|
|
45
|
+
openInBrowser('https://www.becki.io/account');
|
|
46
|
+
setOpened(true);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const planLabel = (p) => p === 'core' ? 'Core' : p === 'pro' ? 'Studio' : p === 'team' ? 'Team' : 'none';
|
|
50
|
+
const statusColor = (s) => {
|
|
51
|
+
if (s === 'active' || s === 'trialing')
|
|
52
|
+
return 'green';
|
|
53
|
+
if (s === 'past_due')
|
|
54
|
+
return 'yellow';
|
|
55
|
+
if (s === 'canceled' || s === 'unpaid')
|
|
56
|
+
return 'red';
|
|
57
|
+
return theme.gray;
|
|
58
|
+
};
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Subscription", subtitle: "becki.io/account hosts the full billing portal" }), loading && _jsx(Loading, {}), error && _jsx(ErrorLine, { message: error }), !loading && !auth.userId && (_jsx(Text, { color: "yellow", children: "not signed in \u2014 open /account to sign in" })), data && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Kv, { k: "Plan", v: planLabel(data.plan) }), _jsx(Kv, { k: "Status", v: data.is_comped ? 'comped' : (data.status ?? 'none'), color: data.is_comped ? 'green' : statusColor(data.status) }), _jsx(Kv, { k: "Renews", v: data.current_period_end ? new Date(data.current_period_end).toLocaleDateString() : '—' }), _jsx(Kv, { k: "Cancel at period end", v: data.cancel_at_period_end ? 'yes' : 'no' })] })), data === null && !loading && auth.userId && (_jsx(Text, { color: "yellow", children: "no subscription yet \u2014 visit /account to pick a plan" })), _jsx(Box, { marginTop: 1, children: opened && _jsx(OkLine, { message: "opened becki.io/account in your browser" }) }), _jsx(BackFooter, { extra: "p open billing portal" })] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* vault.tsx — high-level vault stats + JSON export.
|
|
4
|
+
*
|
|
5
|
+
* Counts vault entries via the anon API (scoped to auth.userId), exports the
|
|
6
|
+
* full vault as a JSON file in the user's home directory. The "delete-by-
|
|
7
|
+
* source" and "purge cache" actions are scaffolded but defer to /account
|
|
8
|
+
* (which has the user JWT needed for destructive RPCs).
|
|
9
|
+
*/
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { Box, Text, useInput } from 'ink';
|
|
12
|
+
import { existsSync, writeFileSync, statSync, unlinkSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { resolveAuth, SUPABASE_URL, SUPABASE_ANON_KEY, PATHS, listProjects } from '../client.js';
|
|
16
|
+
import { ScreenHeader, Kv, BackFooter, Loading, ErrorLine, OkLine, useAsync } from './_shared.js';
|
|
17
|
+
import { theme } from '../theme.js';
|
|
18
|
+
async function loadVault(userId) {
|
|
19
|
+
// becki-mcp Core's cache (only present if user has run `becki-mcp init`
|
|
20
|
+
// from npm — Studio's bundled mcp uses mcp-runtime/ instead).
|
|
21
|
+
const cachePath = join(PATHS.beckiHome, 'cache.db');
|
|
22
|
+
const coreCacheSize = existsSync(cachePath) ? statSync(cachePath).size : 0;
|
|
23
|
+
// Studio Mac.app's actual data store. When present this is by far the
|
|
24
|
+
// largest local artifact and represents real user data — separate from
|
|
25
|
+
// anything becki-mcp Core does. Show it so users don't think their data
|
|
26
|
+
// is "absent" when it's sitting in becki.sqlite.
|
|
27
|
+
const studioSqlitePath = join(PATHS.beckiHome, 'becki.sqlite');
|
|
28
|
+
const studioSqliteSize = existsSync(studioSqlitePath) ? statSync(studioSqlitePath).size : 0;
|
|
29
|
+
if (!userId)
|
|
30
|
+
return { count: null, projectCount: 0, coreCacheSize, studioSqliteSize };
|
|
31
|
+
try {
|
|
32
|
+
// Same SECURITY DEFINER RPC the Status screen now uses — bypasses RLS
|
|
33
|
+
// properly via explicit pp_user_id. Previously this hit
|
|
34
|
+
// /rest/v1/vault_embeddings directly with the anon key and got 0
|
|
35
|
+
// because RLS evaluated auth.uid()=null before the WHERE clause.
|
|
36
|
+
const projects = await listProjects(userId);
|
|
37
|
+
const count = projects.reduce((sum, p) => sum + (p.atomic_count ?? 0), 0);
|
|
38
|
+
return { count, projectCount: projects.length, coreCacheSize, studioSqliteSize };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { count: null, projectCount: 0, coreCacheSize, studioSqliteSize };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Export every vault row the user owns to a JSON file.
|
|
46
|
+
*
|
|
47
|
+
* Iterates known source types and calls list_vault_rows (SECURITY DEFINER,
|
|
48
|
+
* takes pp_user_id) per type — the direct /rest/v1/vault_embeddings query
|
|
49
|
+
* the previous implementation used returned 0 rows under anon auth because
|
|
50
|
+
* RLS evaluated auth.uid()=null before the user_id filter ran.
|
|
51
|
+
*
|
|
52
|
+
* 5000-row cap per type is a soft safety against runaway responses; if any
|
|
53
|
+
* single type hits the cap we record it in the result so the UI can warn.
|
|
54
|
+
*/
|
|
55
|
+
async function exportVault(userId) {
|
|
56
|
+
const SOURCE_TYPES = [
|
|
57
|
+
'decision', 'dead_end', 'commitment', 'open_loop', 'note', 'ask', 'code', 'meeting',
|
|
58
|
+
];
|
|
59
|
+
const PER_TYPE_CAP = 5000;
|
|
60
|
+
const all = [];
|
|
61
|
+
const truncatedTypes = [];
|
|
62
|
+
for (const type of SOURCE_TYPES) {
|
|
63
|
+
const r = await fetch(`${SUPABASE_URL}/rest/v1/rpc/list_vault_rows`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
apikey: SUPABASE_ANON_KEY,
|
|
68
|
+
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
69
|
+
'Content-Profile': 'becki',
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
p_project_id: null,
|
|
73
|
+
p_since: null,
|
|
74
|
+
p_until: null,
|
|
75
|
+
p_types: [type],
|
|
76
|
+
p_limit: PER_TYPE_CAP,
|
|
77
|
+
p_user_id: userId,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
if (!r.ok)
|
|
81
|
+
throw new Error(`list_vault_rows[${type}] ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
82
|
+
const rows = (await r.json());
|
|
83
|
+
all.push(...rows);
|
|
84
|
+
if (rows.length >= PER_TYPE_CAP)
|
|
85
|
+
truncatedTypes.push(type);
|
|
86
|
+
}
|
|
87
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
88
|
+
const path = join(homedir(), `becki-vault-${stamp}.json`);
|
|
89
|
+
writeFileSync(path, JSON.stringify(all, null, 2), 'utf8');
|
|
90
|
+
return { path, rows: all.length, truncatedTypes };
|
|
91
|
+
}
|
|
92
|
+
function fmtSize(bytes) {
|
|
93
|
+
if (bytes < 1024)
|
|
94
|
+
return `${bytes} B`;
|
|
95
|
+
if (bytes < 1024 * 1024)
|
|
96
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
97
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
98
|
+
}
|
|
99
|
+
export function VaultScreen({ onBack }) {
|
|
100
|
+
const auth = resolveAuth();
|
|
101
|
+
const { loading, data, error, refresh } = useAsync(() => loadVault(auth.userId), [auth.userId]);
|
|
102
|
+
const [exportState, setExportState] = useState({ kind: 'idle' });
|
|
103
|
+
const runExport = async () => {
|
|
104
|
+
if (!auth.userId) {
|
|
105
|
+
setExportState({ kind: 'err', msg: 'need to be signed in (visit /account)' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
setExportState({ kind: 'running' });
|
|
109
|
+
try {
|
|
110
|
+
const { path, rows, truncatedTypes } = await exportVault(auth.userId);
|
|
111
|
+
const note = truncatedTypes.length > 0
|
|
112
|
+
? ` (capped at 5000 for: ${truncatedTypes.join(', ')} — older rows omitted)`
|
|
113
|
+
: '';
|
|
114
|
+
setExportState({ kind: 'done', msg: `${rows} rows → ${path}${note}` });
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
setExportState({ kind: 'err', msg: err.message });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const purgeCache = () => {
|
|
121
|
+
const cachePath = join(PATHS.beckiHome, 'cache.db');
|
|
122
|
+
try {
|
|
123
|
+
if (existsSync(cachePath)) {
|
|
124
|
+
unlinkSync(cachePath);
|
|
125
|
+
setExportState({ kind: 'done', msg: `purged ${cachePath}` });
|
|
126
|
+
refresh();
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
setExportState({ kind: 'err', msg: 'cache.db already absent' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
setExportState({ kind: 'err', msg: err.message });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
useInput((input, key) => {
|
|
137
|
+
if (exportState.kind === 'running')
|
|
138
|
+
return;
|
|
139
|
+
if (key.escape || key.leftArrow) {
|
|
140
|
+
onBack();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (input === 'e')
|
|
144
|
+
void runExport();
|
|
145
|
+
else if (input === 'p')
|
|
146
|
+
purgeCache();
|
|
147
|
+
else if (input === 'r')
|
|
148
|
+
refresh();
|
|
149
|
+
});
|
|
150
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Vault", subtitle: "cloud-side entries + local cache" }), loading && _jsx(Loading, {}), error && _jsx(ErrorLine, { message: error }), data && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Kv, { k: "Entries (cloud)", v: data.count === null
|
|
151
|
+
? '— (auth required)'
|
|
152
|
+
: `~${data.count.toLocaleString()} across ${data.projectCount} project${data.projectCount === 1 ? '' : 's'}` }), data.studioSqliteSize > 0 && (_jsx(Kv, { k: "Studio local DB", v: `${fmtSize(data.studioSqliteSize)} · becki.sqlite (Mac app, GRDB)`, color: theme.text })), _jsx(Kv, { k: "Core daemon cache", v: data.coreCacheSize > 0
|
|
153
|
+
? `${fmtSize(data.coreCacheSize)} · cache.db`
|
|
154
|
+
: 'absent (becki-mcp from npm not initialized — Studio uses its own runtime)', color: data.coreCacheSize > 0 ? theme.text : theme.gray }), _jsx(Kv, { k: "Becki home", v: PATHS.beckiHome }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.gray, dimColor: true, children: ["~count derives from viz substrate atomic_count; consolidation can fold ", '<', "5% into aggregates. Exact count via a dedicated RPC ships in v0.6 (#195)."] }) }), data.studioSqliteSize > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "Studio's becki.sqlite is the Mac app's primary store (meetings, transcripts, project state). Cloud entries above are the shared NeuraVault, written by both Studio and Core." }) }))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [exportState.kind === 'running' && _jsx(Loading, { label: "exporting\u2026" }), exportState.kind === 'done' && _jsx(OkLine, { message: exportState.msg }), exportState.kind === 'err' && _jsx(ErrorLine, { message: exportState.msg })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "delete-by-source needs user JWT \u2014 manage at becki.io/account" }) }), _jsx(BackFooter, { extra: "e export json \u00B7 p purge cache \u00B7 r refresh" })] }));
|
|
155
|
+
}
|