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
package/dist/menu.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* menu.tsx — top-level admin TUI router.
|
|
4
|
+
*
|
|
5
|
+
* Repurposed from launcher (splash → prompt → handoff) into Becki Core's
|
|
6
|
+
* cross-platform Settings panel. Same React + Ink stack; entirely different
|
|
7
|
+
* purpose. See [[becki-shell-repurposed-as-admin-tui]] in Becki.
|
|
8
|
+
*
|
|
9
|
+
* Navigation:
|
|
10
|
+
* ↑/↓ or j/k move
|
|
11
|
+
* 1-9, 0 jump to item by number
|
|
12
|
+
* enter / → open
|
|
13
|
+
* esc / ← back to menu
|
|
14
|
+
* q quit (only on the menu)
|
|
15
|
+
*/
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
18
|
+
import { theme } from './theme.js';
|
|
19
|
+
import { StatusScreen } from './screens/status.js';
|
|
20
|
+
import { AccountScreen } from './screens/account.js';
|
|
21
|
+
import { SubscriptionScreen } from './screens/subscription.js';
|
|
22
|
+
import { WatchFoldersScreen } from './screens/watch-folders.js';
|
|
23
|
+
import { GitScreen } from './screens/git.js';
|
|
24
|
+
import { BackfillsScreen } from './screens/backfills.js';
|
|
25
|
+
import { InstallTokenScreen } from './screens/install-token.js';
|
|
26
|
+
import { VaultScreen } from './screens/vault.js';
|
|
27
|
+
import { LogsScreen } from './screens/logs.js';
|
|
28
|
+
// Quit lives at the end as a discoverable menu item — `q` keystroke still
|
|
29
|
+
// works as the fast-path, but the visible row tells new users where the exit
|
|
30
|
+
// is so the TUI doesn't feel like a trap.
|
|
31
|
+
const ITEMS = [
|
|
32
|
+
{ id: 'status', label: 'Status', hint: 'daemon · last digest · vault count · token usage' },
|
|
33
|
+
{ id: 'account', label: 'Account', hint: 'email · sign-out · open /account' },
|
|
34
|
+
{ id: 'subscription', label: 'Subscription', hint: 'plan · renews · billing portal' },
|
|
35
|
+
{ id: 'watch-folders', label: 'Watch folders', hint: 'list · add · remove · pause' },
|
|
36
|
+
{ id: 'git', label: 'Git', hint: 'GitHub / GitLab / Bitbucket tokens' },
|
|
37
|
+
{ id: 'backfills', label: 'Backfills', hint: 'AI sessions · git history · re-extract' },
|
|
38
|
+
{ id: 'install-token', label: 'Install token', hint: 'show · regenerate · copy' },
|
|
39
|
+
{ id: 'vault', label: 'Vault', hint: 'size · export · purge' },
|
|
40
|
+
{ id: 'logs', label: 'Logs', hint: 'tail recent · open log dir' },
|
|
41
|
+
{ id: 'quit', label: 'Quit', hint: 'exit becki (or press q anywhere on the menu)' },
|
|
42
|
+
];
|
|
43
|
+
export function MenuApp() {
|
|
44
|
+
const { exit } = useApp();
|
|
45
|
+
const [current, setCurrent] = useState('menu');
|
|
46
|
+
const [cursor, setCursor] = useState(0);
|
|
47
|
+
useInput((input, key) => {
|
|
48
|
+
if (current !== 'menu')
|
|
49
|
+
return; // screen owns its own input
|
|
50
|
+
if (key.upArrow || input === 'k') {
|
|
51
|
+
setCursor((c) => (c - 1 + ITEMS.length) % ITEMS.length);
|
|
52
|
+
}
|
|
53
|
+
else if (key.downArrow || input === 'j') {
|
|
54
|
+
setCursor((c) => (c + 1) % ITEMS.length);
|
|
55
|
+
}
|
|
56
|
+
else if (key.return || key.rightArrow) {
|
|
57
|
+
const next = ITEMS[cursor].id;
|
|
58
|
+
if (next === 'quit')
|
|
59
|
+
exit();
|
|
60
|
+
else
|
|
61
|
+
setCurrent(next);
|
|
62
|
+
}
|
|
63
|
+
else if (input === 'q' || (input === 'c' && key.ctrl)) {
|
|
64
|
+
exit();
|
|
65
|
+
}
|
|
66
|
+
else if (/^[1-9]$/.test(input)) {
|
|
67
|
+
const i = Number(input) - 1;
|
|
68
|
+
if (i < ITEMS.length) {
|
|
69
|
+
const next = ITEMS[i].id;
|
|
70
|
+
setCursor(i);
|
|
71
|
+
if (next === 'quit')
|
|
72
|
+
exit();
|
|
73
|
+
else
|
|
74
|
+
setCurrent(next);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (input === '0' && ITEMS.length >= 10) {
|
|
78
|
+
setCursor(9);
|
|
79
|
+
const next = ITEMS[9].id;
|
|
80
|
+
if (next === 'quit')
|
|
81
|
+
exit();
|
|
82
|
+
else
|
|
83
|
+
setCurrent(next);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (current !== 'menu') {
|
|
87
|
+
const onBack = () => setCurrent('menu');
|
|
88
|
+
switch (current) {
|
|
89
|
+
case 'status': return _jsx(StatusScreen, { onBack: onBack });
|
|
90
|
+
case 'account': return _jsx(AccountScreen, { onBack: onBack });
|
|
91
|
+
case 'subscription': return _jsx(SubscriptionScreen, { onBack: onBack });
|
|
92
|
+
case 'watch-folders': return _jsx(WatchFoldersScreen, { onBack: onBack });
|
|
93
|
+
case 'git': return _jsx(GitScreen, { onBack: onBack });
|
|
94
|
+
case 'backfills': return _jsx(BackfillsScreen, { onBack: onBack });
|
|
95
|
+
case 'install-token': return _jsx(InstallTokenScreen, { onBack: onBack });
|
|
96
|
+
case 'vault': return _jsx(VaultScreen, { onBack: onBack });
|
|
97
|
+
case 'logs': return _jsx(LogsScreen, { onBack: onBack });
|
|
98
|
+
case 'quit':
|
|
99
|
+
exit();
|
|
100
|
+
return _jsx(Box, {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.gold, bold: true, children: "\u25C6 BECKI " }), _jsx(Text, { color: theme.gray, children: "local admin \u00B7 v0.5" })] }), ITEMS.map((item, i) => {
|
|
104
|
+
const selected = i === cursor;
|
|
105
|
+
const num = ((i + 1) % 10).toString();
|
|
106
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(Text, { color: selected ? theme.gold : theme.gray, children: selected ? '▸ ' : ' ' }) }), _jsx(Box, { width: 3, children: _jsx(Text, { color: theme.gray, children: num }) }), _jsx(Box, { width: 18, children: _jsx(Text, { color: selected ? theme.gold : theme.text, bold: selected, children: item.label }) }), _jsx(Text, { color: theme.gray, children: item.hint })] }, item.id));
|
|
107
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "\u2191/\u2193 pick \u00B7 1-9 jump \u00B7 enter open \u00B7 q quit" }) })] }));
|
|
108
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* prompt.tsx — the "what are you working on today?" input + the App that ties
|
|
4
|
+
* splash → prompt → (loops manager) → launch banner together.
|
|
5
|
+
*
|
|
6
|
+
* The App is the single Ink tree bin/becki.js renders. It calls `onComplete`
|
|
7
|
+
* with the typed focus (or null if skipped) once the user commits, then shows
|
|
8
|
+
* the launch banner briefly before Ink unmounts and the handoff runs.
|
|
9
|
+
*
|
|
10
|
+
* A focus can be entered three ways: type it freely, press a number (1-9) to
|
|
11
|
+
* jump-select an open thread, or arrow up/down then Enter. Ctrl+L opens the
|
|
12
|
+
* interactive open-threads manager (a detour — quitting it returns here).
|
|
13
|
+
*
|
|
14
|
+
* The input is hand-rolled (not ink-text-input) because digits are reserved
|
|
15
|
+
* for jump-select and Ctrl+L for the loops manager — a text widget would
|
|
16
|
+
* swallow both as literal input.
|
|
17
|
+
*/
|
|
18
|
+
import React, { useState } from 'react';
|
|
19
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
20
|
+
import { theme } from './theme.js';
|
|
21
|
+
import { Splash } from './splash.js';
|
|
22
|
+
import { LoopsApp } from './loops.js';
|
|
23
|
+
import { resolveAuth } from './client.js';
|
|
24
|
+
/** The "what are you working on?" line — free text, number jump, or arrows. */
|
|
25
|
+
function FocusPrompt({ threads, highlightIndex, setHighlightIndex, onOpenLoops, onSubmit, }) {
|
|
26
|
+
const [value, setValue] = useState('');
|
|
27
|
+
useInput((input, key) => {
|
|
28
|
+
// --- Ctrl+L → loops manager -----------------------------------------
|
|
29
|
+
if (key.ctrl && input === 'l') {
|
|
30
|
+
onOpenLoops?.();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// --- Arrow navigation through the open-threads list -----------------
|
|
34
|
+
// Stepping off the top or bottom clears the selection (back to free text).
|
|
35
|
+
if (key.upArrow) {
|
|
36
|
+
const n = threads.length;
|
|
37
|
+
if (n === 0)
|
|
38
|
+
return;
|
|
39
|
+
setHighlightIndex((h) => (h === null ? n - 1 : h === 0 ? null : h - 1));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.downArrow) {
|
|
43
|
+
const n = threads.length;
|
|
44
|
+
if (n === 0)
|
|
45
|
+
return;
|
|
46
|
+
setHighlightIndex((h) => (h === null ? 0 : h === n - 1 ? null : h + 1));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// --- Commit ----------------------------------------------------------
|
|
50
|
+
if (key.return) {
|
|
51
|
+
if (highlightIndex !== null && value.trim() === '') {
|
|
52
|
+
onSubmit(threads[highlightIndex].title);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
onSubmit(value.trim() === '' ? null : value.trim());
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// --- Editing ---------------------------------------------------------
|
|
60
|
+
if (key.backspace || key.delete) {
|
|
61
|
+
setValue((v) => v.slice(0, -1));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// A digit as the FIRST keystroke jumps the highlight to that thread.
|
|
65
|
+
// Once free text exists, digits fall through and append as normal text.
|
|
66
|
+
if (value === '' && /^[1-9]$/.test(input)) {
|
|
67
|
+
const idx = Number(input) - 1;
|
|
68
|
+
if (idx < threads.length) {
|
|
69
|
+
setHighlightIndex(idx);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Any other printable character is free-text focus; typing clears the
|
|
74
|
+
// thread selection so the two inputs never fight.
|
|
75
|
+
if (input && !key.ctrl && !key.meta && !key.escape && !key.tab) {
|
|
76
|
+
setValue((v) => v + input);
|
|
77
|
+
setHighlightIndex(null);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const selected = highlightIndex !== null ? threads[highlightIndex] : null;
|
|
81
|
+
const hint = onOpenLoops
|
|
82
|
+
? '(↑↓ or number to pick · type your own · ⌃L loops · enter)'
|
|
83
|
+
: '(↑↓ or number to pick a thread · or type · enter)';
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [_jsxs(Text, { color: theme.gold, children: ["What are you working on today? ", _jsx(Text, { color: theme.gray, children: hint })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.gold, children: '> ' }), selected && value === '' ? (_jsxs(Text, { color: theme.gold, children: [highlightIndex + 1, " \u00B7", ' ', _jsx(Text, { color: theme.text, children: selected.title })] })) : (_jsxs(Text, { color: theme.text, children: [value, _jsx(Text, { inverse: true, children: " " })] }))] })] }));
|
|
85
|
+
}
|
|
86
|
+
/** The launch banner shown after the prompt resolves, just before handoff. */
|
|
87
|
+
function LaunchBanner({ cli }) {
|
|
88
|
+
const rule = '─'.repeat(Math.max(0, 54 - cli.length));
|
|
89
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [_jsxs(Text, { color: theme.gold, children: ["Launching ", cli, " ", _jsx(Text, { color: theme.gray, children: rule })] }), _jsxs(Text, { color: theme.gray, children: ["Becki is connected to ", cli, " over MCP \u2014 context loads on demand."] })] }));
|
|
90
|
+
}
|
|
91
|
+
export function App({ vault, config, warnings, onComplete }) {
|
|
92
|
+
const { exit } = useApp();
|
|
93
|
+
// If the prompt is disabled, jump straight to the launch banner.
|
|
94
|
+
const [phase, setPhase] = useState(config.skip_prompt ? 'launching' : 'prompt');
|
|
95
|
+
// Which open thread the prompt currently has highlighted (null = free text).
|
|
96
|
+
const [highlightIndex, setHighlightIndex] = useState(null);
|
|
97
|
+
// Vault owner — gates the loops manager (Ctrl+L). Resolved once.
|
|
98
|
+
const [userId] = useState(() => resolveAuth().userId);
|
|
99
|
+
// When skip_prompt is set, resolve immediately on first render.
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
if (config.skip_prompt) {
|
|
102
|
+
onComplete(null);
|
|
103
|
+
const t = setTimeout(() => exit(), 500);
|
|
104
|
+
return () => clearTimeout(t);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
+
}, []);
|
|
109
|
+
const handleSubmit = (focus) => {
|
|
110
|
+
onComplete(focus);
|
|
111
|
+
setPhase('launching');
|
|
112
|
+
// Give the launch banner a beat to render, then unmount Ink so the
|
|
113
|
+
// spawned CLI can take over the inherited TTY cleanly.
|
|
114
|
+
setTimeout(() => exit(), 500);
|
|
115
|
+
};
|
|
116
|
+
// Loops manager — a full-screen detour. Quitting it returns to the prompt.
|
|
117
|
+
if (phase === 'loops' && userId) {
|
|
118
|
+
return _jsx(LoopsApp, { userId: userId, onExit: () => setPhase('prompt') });
|
|
119
|
+
}
|
|
120
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Splash, { vault: vault, config: config, warnings: warnings, highlightIndex: phase === 'prompt' ? highlightIndex : null }), phase === 'prompt' ? (_jsx(FocusPrompt, { threads: vault.openThreads, highlightIndex: highlightIndex, setHighlightIndex: setHighlightIndex, onOpenLoops: userId ? () => setPhase('loops') : undefined, onSubmit: handleSubmit })) : (_jsx(LaunchBanner, { cli: config.cli }))] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* _shared.tsx — small primitives shared by every screen.
|
|
4
|
+
*
|
|
5
|
+
* Screen header (title + back hint), simple key/value rows, async loader hook.
|
|
6
|
+
* Keeps each screen short and visually consistent.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { Box, Text, useInput } from 'ink';
|
|
10
|
+
import { theme } from '../theme.js';
|
|
11
|
+
export function ScreenHeader({ title, subtitle }) {
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: theme.gold, bold: true, children: ["\u25C6 ", title.toUpperCase()] }) }), subtitle && (_jsx(Box, { children: _jsx(Text, { color: theme.gray, children: subtitle }) }))] }));
|
|
13
|
+
}
|
|
14
|
+
export function Kv({ k, v, kWidth = 18, color }) {
|
|
15
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: kWidth, children: _jsx(Text, { color: theme.gray, children: k }) }), _jsx(Text, { color: color ?? theme.text, children: v })] }));
|
|
16
|
+
}
|
|
17
|
+
export function BackFooter({ extra }) {
|
|
18
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.gray, dimColor: true, children: ["esc/\u2190 back to menu", extra ? ` · ${extra}` : ''] }) }));
|
|
19
|
+
}
|
|
20
|
+
/** Closes the screen when the user presses esc or left-arrow. */
|
|
21
|
+
export function useBackOnEsc(onBack, extraHandler) {
|
|
22
|
+
useInput((input, key) => {
|
|
23
|
+
if (key.escape || key.leftArrow) {
|
|
24
|
+
onBack();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
extraHandler?.(input, key);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Tiny data-fetch hook. Re-runs the loader on demand via refresh().
|
|
32
|
+
* Captures errors into state instead of throwing.
|
|
33
|
+
*/
|
|
34
|
+
export function useAsync(loader, deps = []) {
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
const [data, setData] = useState(null);
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
const [tick, setTick] = useState(0);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
loader()
|
|
44
|
+
.then((d) => { if (!cancelled) {
|
|
45
|
+
setData(d);
|
|
46
|
+
setLoading(false);
|
|
47
|
+
} })
|
|
48
|
+
.catch((e) => {
|
|
49
|
+
if (!cancelled) {
|
|
50
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => { cancelled = true; };
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [tick, ...deps]);
|
|
57
|
+
return { loading, data, error, refresh: () => setTick((t) => t + 1) };
|
|
58
|
+
}
|
|
59
|
+
export function Loading({ label = 'loading…' }) {
|
|
60
|
+
return _jsx(Text, { color: theme.gray, children: label });
|
|
61
|
+
}
|
|
62
|
+
export function ErrorLine({ message }) {
|
|
63
|
+
return _jsxs(Text, { color: "red", children: ["! ", message] });
|
|
64
|
+
}
|
|
65
|
+
export function OkLine({ message }) {
|
|
66
|
+
return _jsxs(Text, { color: "green", children: ["\u2713 ", message] });
|
|
67
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* account.tsx — read-only account state + handoff to web for full management.
|
|
4
|
+
*
|
|
5
|
+
* Most account operations (signup, password reset, GitHub OAuth) require a
|
|
6
|
+
* browser and live at becki.io/account. This screen shows what we know
|
|
7
|
+
* locally and opens the web portal in the default browser.
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { Box, useInput } from 'ink';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { platform } from 'node:os';
|
|
13
|
+
import { resolveAuth } from '../client.js';
|
|
14
|
+
import { ScreenHeader, Kv, BackFooter, OkLine } from './_shared.js';
|
|
15
|
+
import { theme } from '../theme.js';
|
|
16
|
+
function openInBrowser(url) {
|
|
17
|
+
const cmd = platform() === 'darwin' ? 'open' :
|
|
18
|
+
platform() === 'win32' ? 'cmd' :
|
|
19
|
+
'xdg-open';
|
|
20
|
+
const args = platform() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
21
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
22
|
+
}
|
|
23
|
+
export function AccountScreen({ onBack }) {
|
|
24
|
+
const auth = resolveAuth();
|
|
25
|
+
const [opened, setOpened] = React.useState(false);
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
if (key.escape || key.leftArrow)
|
|
28
|
+
onBack();
|
|
29
|
+
else if (input === 'o') {
|
|
30
|
+
openInBrowser('https://www.becki.io/account');
|
|
31
|
+
setOpened(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Account", subtitle: "manage email, password, and GitHub at becki.io/account" }), _jsx(Kv, { k: "User ID", v: auth.userId ?? 'not signed in — visit /account in browser', color: auth.userId ? theme.text : 'yellow' }), _jsx(Kv, { k: "Install token", v: auth.ingestToken ? 'present' : 'missing', color: auth.ingestToken ? theme.text : 'yellow' }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: opened && _jsx(OkLine, { message: "opened becki.io/account in your browser" }) }), _jsx(BackFooter, { extra: "o open /account in browser" })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* backfills.tsx — run scheduled or windowed history ingests.
|
|
4
|
+
*
|
|
5
|
+
* Shells out to `becki-mcp bootstrap N` and `becki-mcp digest`. Streams
|
|
6
|
+
* stderr so the user sees progress in real time (each ai-sessions log line
|
|
7
|
+
* lands here). On Win/Linux: same spawn — becki-mcp is the cross-platform
|
|
8
|
+
* binary the user installed alongside `becki`.
|
|
9
|
+
*/
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { Box, Text, useInput } from 'ink';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { ScreenHeader, BackFooter, OkLine, ErrorLine } from './_shared.js';
|
|
14
|
+
import { theme } from '../theme.js';
|
|
15
|
+
const OPTIONS = [
|
|
16
|
+
{ key: '1', label: 'digest (today)', command: ['digest'], description: 'process new AI sessions settled in the last 24h' },
|
|
17
|
+
{ key: '2', label: 'bootstrap 7 days', command: ['bootstrap', '7'], description: '~1m · last week of AI session history' },
|
|
18
|
+
{ key: '3', label: 'bootstrap 30 days', command: ['bootstrap', '30'], description: '~3m · last month' },
|
|
19
|
+
{ key: '4', label: 'bootstrap 90 days', command: ['bootstrap', '90'], description: '~10m · last quarter (recommended first-run)' },
|
|
20
|
+
{ key: '5', label: 'bootstrap full', command: ['bootstrap', '36500'], description: 'everything on disk — token cap will pause mid-way' },
|
|
21
|
+
];
|
|
22
|
+
export function BackfillsScreen({ onBack }) {
|
|
23
|
+
const [cursor, setCursor] = useState(0);
|
|
24
|
+
const [action, setAction] = useState({ kind: 'idle' });
|
|
25
|
+
const [spawnError, setSpawnError] = useState(null);
|
|
26
|
+
const startAction = (opt) => {
|
|
27
|
+
setSpawnError(null);
|
|
28
|
+
const output = [];
|
|
29
|
+
let proc;
|
|
30
|
+
try {
|
|
31
|
+
proc = spawn('becki-mcp', opt.command, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
setSpawnError(err.message);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const onChunk = (buf) => {
|
|
38
|
+
const lines = buf.toString('utf8').split('\n').filter((l) => l.length > 0);
|
|
39
|
+
output.push(...lines);
|
|
40
|
+
// Cap output buffer so Ink doesn't churn re-rendering 10k lines.
|
|
41
|
+
if (output.length > 100)
|
|
42
|
+
output.splice(0, output.length - 100);
|
|
43
|
+
setAction((a) => (a.kind === 'running' ? { ...a, output: [...output] } : a));
|
|
44
|
+
};
|
|
45
|
+
proc.stdout?.on('data', onChunk);
|
|
46
|
+
proc.stderr?.on('data', onChunk);
|
|
47
|
+
proc.on('error', (err) => {
|
|
48
|
+
setSpawnError(`becki-mcp not on PATH? ${err.message}`);
|
|
49
|
+
setAction({ kind: 'idle' });
|
|
50
|
+
});
|
|
51
|
+
proc.on('close', (code) => {
|
|
52
|
+
setAction({ kind: 'done', label: opt.label, output, exitCode: code ?? -1 });
|
|
53
|
+
});
|
|
54
|
+
setAction({ kind: 'running', label: opt.label, proc, output });
|
|
55
|
+
};
|
|
56
|
+
useInput((input, key) => {
|
|
57
|
+
if (action.kind === 'running') {
|
|
58
|
+
// Allow cancel via Ctrl-C / esc.
|
|
59
|
+
if (key.escape || (input === 'c' && key.ctrl)) {
|
|
60
|
+
action.proc.kill('SIGTERM');
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.escape || key.leftArrow) {
|
|
65
|
+
onBack();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.upArrow || input === 'k')
|
|
69
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
70
|
+
else if (key.downArrow || input === 'j')
|
|
71
|
+
setCursor((c) => Math.min(OPTIONS.length - 1, c + 1));
|
|
72
|
+
else if (key.return)
|
|
73
|
+
startAction(OPTIONS[cursor]);
|
|
74
|
+
else if (/^[1-5]$/.test(input)) {
|
|
75
|
+
const i = Number(input) - 1;
|
|
76
|
+
setCursor(i);
|
|
77
|
+
startAction(OPTIONS[i]);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (action.kind === 'running') {
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Backfills \u00B7 running", subtitle: action.label }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.gray, paddingX: 1, children: [action.output.slice(-20).map((line, i) => (_jsx(Text, { color: theme.text, children: line }, i))), action.output.length === 0 && _jsx(Text, { color: theme.gray, children: "(waiting for first output\u2026)" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "esc cancel (SIGTERM)" }) })] }));
|
|
82
|
+
}
|
|
83
|
+
if (action.kind === 'done') {
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Backfills \u00B7 done", subtitle: action.label }), action.exitCode === 0
|
|
85
|
+
? _jsx(OkLine, { message: `finished cleanly (exit 0)` })
|
|
86
|
+
: _jsx(ErrorLine, { message: `exited with code ${action.exitCode}` }), _jsx(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: theme.gray, paddingX: 1, children: action.output.slice(-12).map((line, i) => (_jsx(Text, { color: theme.gray, children: line }, i))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "any key to return to backfills menu" }) })] }));
|
|
87
|
+
}
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Backfills", subtitle: "invokes becki-mcp digest/bootstrap; streams output here" }), OPTIONS.map((o, i) => {
|
|
89
|
+
const sel = i === cursor;
|
|
90
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(Text, { color: sel ? theme.gold : theme.gray, children: sel ? '▸ ' : ' ' }) }), _jsx(Box, { width: 3, children: _jsx(Text, { color: theme.gray, children: o.key }) }), _jsx(Box, { width: 22, children: _jsx(Text, { color: sel ? theme.gold : theme.text, bold: sel, children: o.label }) }), _jsx(Text, { color: theme.gray, children: o.description })] }, o.key));
|
|
91
|
+
}), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.gray, children: "Re-extract operations (advanced):" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(Text, { color: theme.gray, dimColor: true, children: "\u00B7 Decision Reasoning Graph re-run \u2014 scheduled in Core v0.7" }), _jsx(Text, { color: theme.gray, dimColor: true, children: "\u00B7 Commitment dedup re-run \u2014 scheduled in Core v0.7" }), _jsx(Text, { color: theme.gray, dimColor: true, children: "\u00B7 Vault re-embed \u2014 only after model upgrade (v0.7)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.gray, children: "Git history:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "\u00B7 not yet wired \u2014 local git-log walker ships in Core v1.2 (#192)" }) })] }), spawnError && (_jsx(Box, { marginTop: 1, children: _jsx(ErrorLine, { message: spawnError }) })), _jsx(BackFooter, { extra: "\u2191/\u2193 pick \u00B7 enter run \u00B7 1-5 jump+run" })] }));
|
|
92
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* diagnostics.tsx — HTTP reachability probes for Becki's three upstream
|
|
4
|
+
* dependencies.
|
|
5
|
+
*
|
|
6
|
+
* Each probe is a HEAD (or smallest possible GET) with a short timeout. Result
|
|
7
|
+
* is "reachable / unreachable / auth-required" — auth-required (401) still
|
|
8
|
+
* means the endpoint is up, just doesn't accept anon. That's the expected
|
|
9
|
+
* answer for endpoints behind auth.
|
|
10
|
+
*/
|
|
11
|
+
import React, { useState } from 'react';
|
|
12
|
+
import { Box, Text, useInput } from 'ink';
|
|
13
|
+
import { ScreenHeader, BackFooter } from './_shared.js';
|
|
14
|
+
import { theme } from '../theme.js';
|
|
15
|
+
const PROBES = [
|
|
16
|
+
{ name: 'Supabase (rest)', url: 'https://mbutedmgtkmoigfbrthr.supabase.co/rest/v1/', method: 'GET', expect: [200, 401] },
|
|
17
|
+
{ name: 'Supabase (edge fn)', url: 'https://mbutedmgtkmoigfbrthr.supabase.co/functions/v1/extract-mcp-content', method: 'POST', expect: [401] },
|
|
18
|
+
{ name: 'Voyage AI', url: 'https://api.voyageai.com/v1/embeddings', method: 'POST', expect: [401] },
|
|
19
|
+
{ name: 'Anthropic', url: 'https://api.anthropic.com/v1/messages', method: 'POST', expect: [401, 400] },
|
|
20
|
+
{ name: 'becki.io', url: 'https://www.becki.io/', method: 'GET', expect: [200] },
|
|
21
|
+
];
|
|
22
|
+
async function runProbe(p) {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
const ctrl = new AbortController();
|
|
25
|
+
const timeout = setTimeout(() => ctrl.abort(), 5_000);
|
|
26
|
+
try {
|
|
27
|
+
const r = await fetch(p.url, {
|
|
28
|
+
method: p.method ?? 'HEAD',
|
|
29
|
+
signal: ctrl.signal,
|
|
30
|
+
body: p.method === 'POST' ? '{}' : undefined,
|
|
31
|
+
headers: p.method === 'POST' ? { 'Content-Type': 'application/json' } : undefined,
|
|
32
|
+
});
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
const latencyMs = Date.now() - start;
|
|
35
|
+
const up = (p.expect ?? [200]).includes(r.status);
|
|
36
|
+
return { state: up ? 'up' : 'unknown', status: r.status, latencyMs };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
return { state: 'down', error: err.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function DiagnosticsScreen({ onBack }) {
|
|
44
|
+
const [results, setResults] = useState(new Map());
|
|
45
|
+
const runAll = React.useCallback(() => {
|
|
46
|
+
const next = new Map();
|
|
47
|
+
for (const p of PROBES)
|
|
48
|
+
next.set(p.name, { state: 'running' });
|
|
49
|
+
setResults(new Map(next));
|
|
50
|
+
Promise.all(PROBES.map(async (p) => {
|
|
51
|
+
const r = await runProbe(p);
|
|
52
|
+
next.set(p.name, r);
|
|
53
|
+
setResults(new Map(next));
|
|
54
|
+
}));
|
|
55
|
+
}, []);
|
|
56
|
+
React.useEffect(() => { runAll(); }, [runAll]);
|
|
57
|
+
useInput((input, key) => {
|
|
58
|
+
if (key.escape || key.leftArrow) {
|
|
59
|
+
onBack();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (input === 'r')
|
|
63
|
+
runAll();
|
|
64
|
+
});
|
|
65
|
+
const stateColor = (s) => {
|
|
66
|
+
switch (s) {
|
|
67
|
+
case 'up': return 'green';
|
|
68
|
+
case 'down': return 'red';
|
|
69
|
+
case 'unknown': return 'yellow';
|
|
70
|
+
case 'running': return theme.gray;
|
|
71
|
+
default: return theme.gray;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const stateGlyph = (s) => {
|
|
75
|
+
switch (s) {
|
|
76
|
+
case 'up': return '●';
|
|
77
|
+
case 'down': return '○';
|
|
78
|
+
case 'unknown': return '◐';
|
|
79
|
+
case 'running': return '…';
|
|
80
|
+
default: return '?';
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(ScreenHeader, { title: "Diagnostics", subtitle: "reachability checks for Becki's upstream dependencies" }), PROBES.map((p) => {
|
|
84
|
+
const r = results.get(p.name) ?? { state: 'idle' };
|
|
85
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(Text, { color: stateColor(r.state), children: stateGlyph(r.state) }) }), _jsx(Box, { width: 22, children: _jsx(Text, { color: theme.text, children: p.name }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: theme.gray, children: r.status ? `${r.status}` : r.state === 'down' ? 'down' : r.state === 'running' ? 'probing' : '—' }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: theme.gray, children: r.latencyMs ? `${r.latencyMs}ms` : '' }) }), _jsx(Text, { color: theme.gray, dimColor: true, children: r.error ?? '' })] }, p.name));
|
|
86
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.gray, dimColor: true, children: "\u25CF up \u00B7 \u25D0 unexpected status (still reachable) \u00B7 \u25CB unreachable \u00B7 \u2026 probing" }) }), _jsx(BackFooter, { extra: "r re-run all" })] }));
|
|
87
|
+
}
|