@tianmucreations/jeeves 0.2.0 → 0.3.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 +79 -18
- package/bin/jeeves +8 -1
- package/dist/agent/auto-ids.js +66 -0
- package/dist/agent/auto.js +178 -0
- package/dist/agent/context.js +55 -13
- package/dist/agent/errors.js +83 -22
- package/dist/agent/expert-chat.js +33 -0
- package/dist/agent/housekeeping.js +55 -0
- package/dist/agent/loop.js +168 -12
- package/dist/agent/permissions.js +167 -0
- package/dist/agent/research-gate.js +267 -0
- package/dist/agent/review.js +135 -0
- package/dist/agent/spending.js +73 -0
- package/dist/agent/systemPrompt.js +112 -0
- package/dist/app.js +25 -10
- package/dist/checkpoints/index.js +103 -0
- package/dist/checkpoints/store.js +239 -0
- package/dist/commands/address.js +5 -0
- package/dist/commands/clear.js +2 -0
- package/dist/commands/help.js +7 -4
- package/dist/commands/keys.js +1 -1
- package/dist/commands/verbose.js +1 -1
- package/dist/components/AddressPrompt.js +31 -0
- package/dist/components/Footer.js +74 -102
- package/dist/components/Input.js +76 -25
- package/dist/components/KeysManager.js +65 -20
- package/dist/components/ModelPicker.js +348 -75
- package/dist/components/ProjectPicker.js +4 -1
- package/dist/components/Transcript.js +29 -14
- package/dist/components/input-layout.js +34 -0
- package/dist/components/transcript-layout.js +13 -17
- package/dist/index.js +25 -7
- package/dist/ink/AlternateScreen.js +33 -16
- package/dist/ink/cursor.js +18 -0
- package/dist/ink/mouse.js +48 -0
- package/dist/keys/store.js +2 -1
- package/dist/models/registry.js +18 -2
- package/dist/platform/config.js +63 -7
- package/dist/providers/catalogue.js +293 -0
- package/dist/providers/direct-services.js +65 -0
- package/dist/providers/direct.js +145 -0
- package/dist/providers/index.js +123 -13
- package/dist/providers/models-snapshot.js +1037 -0
- package/dist/providers/ollama.js +21 -4
- package/dist/providers/openrouter.js +39 -4
- package/dist/providers/step-control.js +28 -0
- package/dist/providers/zai.js +31 -11
- package/dist/state/session.js +90 -36
- package/dist/state/today-spend.js +26 -0
- package/dist/tools/index.js +118 -10
- package/dist/tools/runBash.js +58 -11
- package/dist/tools/web/htmlToText.js +32 -0
- package/dist/tools/web/openrouterChat.js +31 -0
- package/dist/tools/web/research.js +191 -0
- package/package.json +34 -8
- package/dist/components/AlternateScreen.js +0 -74
|
@@ -1,114 +1,86 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { useSession
|
|
5
|
-
import {
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
|
+
import { useSession } from '../state/session.js';
|
|
5
|
+
import { allowanceToday } from '../agent/spending.js';
|
|
6
|
+
import { isAuto, workerModel } from '../agent/auto.js';
|
|
7
|
+
import { isDirectService, CUSTOM_SERVICE_ID } from '../providers/direct-services.js';
|
|
6
8
|
export function shortModelName(model) {
|
|
7
9
|
const short = model.split('/').pop();
|
|
8
10
|
return short && short.length > 0 ? short : model;
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (minutes <= 0 || minutes > 600)
|
|
25
|
-
return '';
|
|
26
|
-
return `~${Math.max(1, Math.round(minutes))}m left`;
|
|
27
|
-
}
|
|
28
|
-
function forecastRateReset(resetEpochSeconds) {
|
|
29
|
-
if (!resetEpochSeconds)
|
|
30
|
-
return '';
|
|
31
|
-
const seconds = resetEpochSeconds - Date.now() / 1000;
|
|
32
|
-
if (seconds <= 0 || seconds > 3600)
|
|
33
|
-
return '';
|
|
34
|
-
return `resets in ${Math.max(1, Math.round(seconds / 60))}m`;
|
|
35
|
-
}
|
|
36
|
-
export function Footer() {
|
|
37
|
-
const s = useSession();
|
|
38
|
-
const contextTokens = s.estimateContextTokens();
|
|
39
|
-
const tpm = s.tokensPerMinute();
|
|
40
|
-
const cacheRate = s.cacheHitRate();
|
|
41
|
-
if (s.footerExpanded !== null) {
|
|
42
|
-
let bar = {
|
|
43
|
-
label: 'context',
|
|
44
|
-
value: contextTokens,
|
|
45
|
-
max: DEFAULT_CONTEXT_TOKENS,
|
|
46
|
-
suffix: forecastContext(contextTokens, tpm),
|
|
47
|
-
};
|
|
48
|
-
if (s.footerExpanded === 'session') {
|
|
49
|
-
bar = { label: 'session', value: s.tokensIn + s.tokensOut, max: DEFAULT_CONTEXT_TOKENS, suffix: '' };
|
|
50
|
-
}
|
|
51
|
-
else if (s.footerExpanded === 'cache' && cacheRate !== null) {
|
|
52
|
-
bar = {
|
|
53
|
-
label: 'cache',
|
|
54
|
-
value: s.tokensCached,
|
|
55
|
-
max: s.tokensIn,
|
|
56
|
-
unit: 'of input read from cache',
|
|
57
|
-
suffix: '',
|
|
58
|
-
goodWhenFull: true,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
else if (s.footerExpanded === 'today' && s.spend > 0 && s.creditLimit) {
|
|
62
|
-
bar = { label: 'today', value: s.spend, max: s.creditLimit, suffix: '' };
|
|
63
|
-
}
|
|
64
|
-
else if (s.footerExpanded === 'credit' && s.creditRemaining !== null && s.creditLimit) {
|
|
65
|
-
bar = {
|
|
66
|
-
label: 'credit',
|
|
67
|
-
value: s.creditLimit - s.creditRemaining,
|
|
68
|
-
max: s.creditLimit,
|
|
69
|
-
unit: 'used',
|
|
70
|
-
suffix: `· $${s.creditRemaining.toFixed(2)} ${s.creditIsAccount ? 'left' : 'key cap'}`,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
else if (s.footerExpanded === 'speed' && s.rateLimit && s.rateLimit.limit > 0) {
|
|
74
|
-
bar = {
|
|
75
|
-
label: 'speed',
|
|
76
|
-
value: s.rateLimit.limit - s.rateLimit.remaining,
|
|
77
|
-
max: s.rateLimit.limit,
|
|
78
|
-
suffix: forecastRateReset(s.rateLimit.reset),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: shortModelName(s.model) }), _jsxs(Text, { dimColor: true, children: [_jsx(UsageBar, { label: bar.label, value: bar.value, max: bar.max, unit: bar.unit, width: 20, goodWhenFull: bar.goodWhenFull }), bar.suffix ? _jsxs(Text, { dimColor: true, children: [" ", bar.suffix] }) : null] })] }));
|
|
12
|
+
const money = (value) => `$${value.toFixed(2)}`;
|
|
13
|
+
// The info bar says only what is worth a glance: which model is working (left),
|
|
14
|
+
// and on the right what today has cost and what is left - or, for a flat-rate
|
|
15
|
+
// plan, whether it has allowance. Warnings appear only when they matter.
|
|
16
|
+
export function footerSegments(info) {
|
|
17
|
+
const busy = info.busyNote ?? (info.tidying ? 'tidying up…' : null);
|
|
18
|
+
const direct = isDirectService(info.providerId);
|
|
19
|
+
if (busy && info.providerId !== 'openrouter' && !direct)
|
|
20
|
+
return [{ text: busy }];
|
|
21
|
+
if (info.providerId === 'zai') {
|
|
22
|
+
if (info.planResetAt === null)
|
|
23
|
+
return [{ text: 'flat-rate plan' }];
|
|
24
|
+
const when = info.planResetAt ? ` · resets @ ${info.planResetAt}` : '';
|
|
25
|
+
return [{ text: `plan used up${when}`, color: 'red' }];
|
|
82
26
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
27
|
+
if (info.providerId === 'ollama') {
|
|
28
|
+
return [{ text: 'on this computer · free' }];
|
|
29
|
+
}
|
|
30
|
+
// A compatible service lists no prices, so its spending can't be worked out.
|
|
31
|
+
if (info.providerId === CUSTOM_SERVICE_ID) {
|
|
32
|
+
return [{ text: 'cost not tracked' }];
|
|
33
|
+
}
|
|
34
|
+
const spent = info.todaySpend ?? 0;
|
|
35
|
+
// Compared in whole cents, so $2.40 of $3.00 is exactly 80%.
|
|
36
|
+
const cents = Math.round(spent * 100);
|
|
37
|
+
const limitCents = Math.round(info.allowance * 100);
|
|
86
38
|
const segments = [
|
|
87
39
|
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
key: 'context',
|
|
93
|
-
render: _jsxs(Text, { color: usageColor(contextFraction), children: ["ctx ", pct(contextFraction)] }),
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
key: 'cache',
|
|
97
|
-
render: cacheRate === null ? (_jsx(Text, { children: "cache \u2014" })) : (_jsxs(Text, { color: benefitColor(cacheRate), children: ["cache ", pct(cacheRate)] })),
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
key: 'today',
|
|
101
|
-
render: _jsxs(Text, { children: ["today $", s.spend.toFixed(2)] }),
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
key: 'credit',
|
|
105
|
-
render: (_jsx(Text, { children: s.creditRemaining !== null
|
|
106
|
-
? s.creditIsAccount
|
|
107
|
-
? `$${s.creditRemaining.toFixed(2)} left`
|
|
108
|
-
: `$${s.creditRemaining.toFixed(2)} key cap`
|
|
109
|
-
: '$— left' })),
|
|
40
|
+
// "~": worked out from the company's price list, not reported by it.
|
|
41
|
+
text: `today ${direct ? '~' : ''}${info.todaySpend === null ? '$—' : money(spent)} of ${money(info.allowance)}`,
|
|
42
|
+
color: cents >= limitCents ? 'red' : cents * 10 >= limitCents * 8 ? 'yellow' : undefined,
|
|
110
43
|
},
|
|
111
44
|
];
|
|
112
|
-
|
|
113
|
-
|
|
45
|
+
if (busy)
|
|
46
|
+
segments.unshift({ text: busy });
|
|
47
|
+
// The companies don't tell a key what credit is left.
|
|
48
|
+
if (direct)
|
|
49
|
+
return segments;
|
|
50
|
+
if (info.creditRemaining === null) {
|
|
51
|
+
segments.push({ text: '$— left' });
|
|
52
|
+
return segments;
|
|
53
|
+
}
|
|
54
|
+
const remaining = info.creditRemaining;
|
|
55
|
+
const color = remaining < 1 ? 'red' : remaining < 5 ? 'yellow' : undefined;
|
|
56
|
+
segments.push({ text: `${money(remaining)} ${info.creditIsAccount ? 'left' : 'key limit'}`, color });
|
|
57
|
+
if (remaining < 1)
|
|
58
|
+
segments.push({ text: 'credit low - top up', color: 'red' });
|
|
59
|
+
return segments;
|
|
60
|
+
}
|
|
61
|
+
// The model name gives way first so the right side never wraps the bar.
|
|
62
|
+
export function fitModelName(name, available) {
|
|
63
|
+
if (available <= 1)
|
|
64
|
+
return '';
|
|
65
|
+
return name.length <= available ? name : name.slice(0, available - 1) + '…';
|
|
66
|
+
}
|
|
67
|
+
export function Footer() {
|
|
68
|
+
const s = useSession();
|
|
69
|
+
const { stdout } = useStdout();
|
|
70
|
+
const columns = Math.max(stdout.columns ?? 80, 40);
|
|
71
|
+
const segments = footerSegments({
|
|
72
|
+
providerId: s.providerId,
|
|
73
|
+
allowance: allowanceToday(),
|
|
74
|
+
tidying: s.tidying,
|
|
75
|
+
busyNote: s.busyNote,
|
|
76
|
+
todaySpend: s.todaySpend,
|
|
77
|
+
creditRemaining: s.creditRemaining,
|
|
78
|
+
creditIsAccount: s.creditIsAccount,
|
|
79
|
+
planResetAt: s.planResetAt,
|
|
80
|
+
});
|
|
81
|
+
const rightWidth = segments.reduce((sum, segment) => sum + segment.text.length, 0) + 3 * (segments.length - 1);
|
|
82
|
+
// In Auto mode the bar names the model actually working: "auto · deepseek-v4-flash-0731".
|
|
83
|
+
const name = isAuto(s.model) ? `auto · ${shortModelName(s.activeModel ?? workerModel())}` : shortModelName(s.model);
|
|
84
|
+
const model = fitModelName(name, columns - rightWidth - 2);
|
|
85
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { children: model }), _jsx(Text, { children: segments.map((segment, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(Text, { dimColor: true, children: " \u00B7 " }) : null, _jsx(Text, { color: segment.color, dimColor: !segment.color, children: segment.text })] }, segment.text))) })] }));
|
|
114
86
|
}
|
package/dist/components/Input.js
CHANGED
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Text, useInput } from 'ink';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Text, useCursor, useInput, useStdout } from 'ink';
|
|
4
4
|
import { runTurn } from '../agent/loop.js';
|
|
5
5
|
import { answerApproval } from '../agent/permissions.js';
|
|
6
6
|
import { useSession } from '../state/session.js';
|
|
7
|
-
|
|
7
|
+
import { BLOCK_CURSOR, inputFrameRow } from '../ink/cursor.js';
|
|
8
|
+
import { isMouseSequence, handleMouseInput } from '../ink/mouse.js';
|
|
9
|
+
import { inputView, dropLastChar } from './input-layout.js';
|
|
10
|
+
export function Input({ scrollPage = 10, width = 76 }) {
|
|
8
11
|
const [value, setValue] = useState('');
|
|
12
|
+
// The text lives in a ref as well as state: the keystroke handler reads and
|
|
13
|
+
// writes the ref, so two keys arriving before React re-renders never build on
|
|
14
|
+
// a stale value.
|
|
15
|
+
const valueRef = useRef('');
|
|
9
16
|
const s = useSession();
|
|
17
|
+
const { stdout } = useStdout();
|
|
18
|
+
const { setCursorPosition } = useCursor();
|
|
19
|
+
const view = inputView(valueRef.current, width);
|
|
20
|
+
const showingText = !s.approvalPending && s.transcriptScrollUp === 0;
|
|
21
|
+
// The block cursor sits at the text insertion point: two columns in (the
|
|
22
|
+
// border's │ and its padding space) plus the visible text's width, measured
|
|
23
|
+
// with stringWidth so wide characters count. y is the input row, third from
|
|
24
|
+
// the bottom (info bar, bottom border, input); inputFrameRow carries the +1
|
|
25
|
+
// Ink's fullscreen frames need. Set during render, as Ink documents: useCursor
|
|
26
|
+
// hands the position to Ink in its own useInsertionEffect, which runs before
|
|
27
|
+
// this commit's frame is written - a useLayoutEffect call runs after that and
|
|
28
|
+
// only lands a frame late (measured: the cursor stayed hidden when the window
|
|
29
|
+
// opened). Hidden while the row shows a hint instead of the text.
|
|
30
|
+
setCursorPosition(showingText ? { x: 2 + view.width, y: inputFrameRow(stdout.rows ?? 24) } : undefined);
|
|
31
|
+
// The terminal's real cursor becomes a steady block for the whole session; it is
|
|
32
|
+
// restored to the shell's default shape by the AlternateScreen exit paths.
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
try {
|
|
35
|
+
process.stdout.write(BLOCK_CURSOR);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// A closed stream must never crash the app.
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
10
41
|
useInput((input, key) => {
|
|
42
|
+
// SGR mouse events arrive as CSI chunks Ink cannot resolve; the wheel
|
|
43
|
+
// scrolls the transcript and every other mouse event is consumed here so
|
|
44
|
+
// none of it ever lands in the text.
|
|
45
|
+
if (isMouseSequence(input)) {
|
|
46
|
+
handleMouseInput(input);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
11
49
|
if (s.pickerOpen || s.keysOpen || s.wizardActive || s.helpOpen)
|
|
12
50
|
return;
|
|
13
51
|
if (s.approvalPending) {
|
|
@@ -26,25 +64,9 @@ export function Input({ scrollPage = 10 }) {
|
|
|
26
64
|
s.toggleShowLastReasoning();
|
|
27
65
|
return;
|
|
28
66
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (key.escape) {
|
|
34
|
-
s.escapeFooter();
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (key.return) {
|
|
38
|
-
const text = value.trim();
|
|
39
|
-
if (text && s.status !== 'working') {
|
|
40
|
-
setValue('');
|
|
41
|
-
s.followTranscript();
|
|
42
|
-
void runTurn(text);
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
// The terminal's own scrollback is off (alternate screen), so the arrow and
|
|
47
|
-
// page keys scroll the transcript region instead, three lines per press.
|
|
67
|
+
// The alternate screen has no native scrollback, so these keys scroll the
|
|
68
|
+
// transcript region itself (Claude Code's bindings): arrows move 3 rows,
|
|
69
|
+
// Page Up/Down a full page, End jumps back to the newest and re-follows.
|
|
48
70
|
if (key.upArrow) {
|
|
49
71
|
s.scrollTranscript(3);
|
|
50
72
|
return;
|
|
@@ -61,16 +83,45 @@ export function Input({ scrollPage = 10 }) {
|
|
|
61
83
|
s.scrollTranscript(-scrollPage);
|
|
62
84
|
return;
|
|
63
85
|
}
|
|
86
|
+
if (key.end) {
|
|
87
|
+
s.followTranscript();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Anything typed while reading history returns to the newest first.
|
|
91
|
+
if (key.return) {
|
|
92
|
+
const text = valueRef.current.trim();
|
|
93
|
+
// /exit is honoured even mid-turn so a wedged request can never trap the user.
|
|
94
|
+
if (text === '/exit' || (text && s.status !== 'working')) {
|
|
95
|
+
s.followTranscript();
|
|
96
|
+
valueRef.current = '';
|
|
97
|
+
setValue('');
|
|
98
|
+
void runTurn(text);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
64
102
|
if (key.backspace || key.delete) {
|
|
65
|
-
|
|
103
|
+
s.followTranscript();
|
|
104
|
+
valueRef.current = dropLastChar(valueRef.current);
|
|
105
|
+
setValue(valueRef.current);
|
|
66
106
|
return;
|
|
67
107
|
}
|
|
68
108
|
if (!input || key.ctrl || key.meta)
|
|
69
109
|
return;
|
|
70
|
-
|
|
110
|
+
s.followTranscript();
|
|
111
|
+
valueRef.current += input;
|
|
112
|
+
setValue(valueRef.current);
|
|
71
113
|
});
|
|
72
114
|
if (s.approvalPending) {
|
|
73
115
|
return _jsx(Text, { color: "yellow", children: "y = allow \u00B7 n = deny" });
|
|
74
116
|
}
|
|
75
|
-
|
|
117
|
+
// While the transcript is scrolled away from the newest, the prompt is replaced
|
|
118
|
+
// by Claude Code's reading-history hint; End (or any typing) returns to the live
|
|
119
|
+
// conversation.
|
|
120
|
+
if (s.transcriptScrollUp > 0) {
|
|
121
|
+
return _jsx(Text, { dimColor: true, children: "reading history \u2014 press End to return" });
|
|
122
|
+
}
|
|
123
|
+
// Trailing spaces are dimmed: invisible on screen, but it makes the frame's
|
|
124
|
+
// bytes differ from the same text without them, keeping Ink on its full-frame
|
|
125
|
+
// path (see input-layout.ts).
|
|
126
|
+
return (_jsx(Text, { children: value ? (_jsxs(Text, { children: [view.text, view.trailingSpaces ? _jsx(Text, { dimColor: true, children: view.trailingSpaces }) : null] })) : (_jsx(Text, { dimColor: true, children: "ask anything" })) }));
|
|
76
127
|
}
|
|
@@ -2,16 +2,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { session, useSession } from '../state/session.js';
|
|
5
|
-
import { PROVIDER_ROWS, storeOpenRouterKey, removeOpenRouterKey, storeZaiKey, removeZaiKey, storedKeyProviders, getKeySource, refreshCredit, } from '../providers/index.js';
|
|
5
|
+
import { PROVIDER_ROWS, storeOpenRouterKey, removeOpenRouterKey, storeZaiKey, removeZaiKey, storedKeyProviders, getKeySource, refreshCredit, storeDirectKey, removeServiceKey, customServiceName, } from '../providers/index.js';
|
|
6
|
+
import { directService, isDirectService, CUSTOM_SERVICE_ID } from '../providers/direct-services.js';
|
|
7
|
+
import { everydayModel } from '../providers/catalogue.js';
|
|
8
|
+
import { setDefaultModel, setDefaultProvider } from '../platform/config.js';
|
|
6
9
|
import { setKey, deleteKey } from '../keys/store.js';
|
|
7
10
|
import { keyLooksValid } from '../commands/keys.js';
|
|
8
|
-
|
|
9
|
-
// OpenRouter management key
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
];
|
|
11
|
+
import { isMouseSequence } from '../ink/mouse.js';
|
|
12
|
+
// Every service Jeeves connects to. The optional OpenRouter management key (it unlocks
|
|
13
|
+
// the real account balance) is for /keys only, and the compatible service needs its
|
|
14
|
+
// address too, so it is set up in /model - the first-run wizard keeps to the essentials.
|
|
15
|
+
const KEY_ROWS = [PROVIDER_ROWS[0], { id: 'openrouter-management', label: 'OpenRouter account' }, ...PROVIDER_ROWS.slice(1)];
|
|
16
|
+
const WIZARD_ROWS = KEY_ROWS.filter((row) => row.id !== 'openrouter-management' && row.id !== CUSTOM_SERVICE_ID);
|
|
15
17
|
function rowLabel(id) {
|
|
16
18
|
return KEY_ROWS.find((row) => row.id === id)?.label ?? id;
|
|
17
19
|
}
|
|
@@ -19,6 +21,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
19
21
|
const s = useSession();
|
|
20
22
|
const [phase, setPhase] = useState(mode === 'wizard' ? { kind: 'ask' } : { kind: 'list' });
|
|
21
23
|
const [cursor, setCursor] = useState(0);
|
|
24
|
+
const visibleRows = mode === 'wizard' ? WIZARD_ROWS : KEY_ROWS;
|
|
22
25
|
const [hidden, setHidden] = useState('');
|
|
23
26
|
const [stored, setStored] = useState([]);
|
|
24
27
|
const [note, setNote] = useState('');
|
|
@@ -33,7 +36,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
33
36
|
if (getKeySource() === 'keychain')
|
|
34
37
|
return 'key stored in your Mac keychain';
|
|
35
38
|
if (getKeySource() === 'env')
|
|
36
|
-
return 'key in
|
|
39
|
+
return 'key in a local file - add it with /keys to store it safely';
|
|
37
40
|
return stored.includes('openrouter') ? 'key stored in your Mac keychain' : 'no key';
|
|
38
41
|
}
|
|
39
42
|
if (rowId === 'openrouter-management') {
|
|
@@ -45,8 +48,11 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
45
48
|
return stored.includes('zai') ? 'key stored - GLM Coding Plan ready' : 'no key - add one to use the flat plan';
|
|
46
49
|
}
|
|
47
50
|
if (rowId === 'ollama')
|
|
48
|
-
return '
|
|
49
|
-
|
|
51
|
+
return 'runs on this computer - no key needed';
|
|
52
|
+
if (rowId === CUSTOM_SERVICE_ID) {
|
|
53
|
+
return stored.includes(rowId) ? `${customServiceName()} - address and key stored` : 'add it in /model, under Other service';
|
|
54
|
+
}
|
|
55
|
+
return stored.includes(rowId) ? 'key stored - direct connection ready' : 'no key';
|
|
50
56
|
}
|
|
51
57
|
function finishWizard(message, connected) {
|
|
52
58
|
if (connected) {
|
|
@@ -93,6 +99,37 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
93
99
|
}
|
|
94
100
|
return;
|
|
95
101
|
}
|
|
102
|
+
if (isDirectService(provider)) {
|
|
103
|
+
const label = directService(provider).label;
|
|
104
|
+
setNote(`Checking the key with ${label}…`);
|
|
105
|
+
const result = await storeDirectKey(provider, key);
|
|
106
|
+
setNote('');
|
|
107
|
+
if (result === 'rejected') {
|
|
108
|
+
setNote(`${label} didn't accept that key - press Enter to paste it again.`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (result === 'keychain') {
|
|
112
|
+
setNote('The Mac keychain was not reachable - press Enter and try again.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
refreshStored();
|
|
116
|
+
const unchecked = result === 'saved-unchecked' ? ` ${label} couldn't be reached to check it just now.` : '';
|
|
117
|
+
if (mode === 'wizard') {
|
|
118
|
+
// First launch: start straight away on the company's everyday model.
|
|
119
|
+
const model = await everydayModel(provider, key);
|
|
120
|
+
if (model) {
|
|
121
|
+
session.setProvider(provider);
|
|
122
|
+
session.setModel(model);
|
|
123
|
+
setDefaultProvider(provider);
|
|
124
|
+
setDefaultModel(model);
|
|
125
|
+
}
|
|
126
|
+
finishWizard(`Your ${label} key is saved.${unchecked} Jeeves will use ${model ?? 'its everyday model'} - type /model to change.`, true);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
setPhase({ kind: 'saved', message: `Your ${label} key is saved.${unchecked} Choose ${label} in /model to use it.` });
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
96
133
|
const saved = await setKey(provider, key);
|
|
97
134
|
if (!saved) {
|
|
98
135
|
setNote('The Mac keychain was not reachable - press Enter and try again.');
|
|
@@ -123,7 +160,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
123
160
|
const fallback = await removeOpenRouterKey();
|
|
124
161
|
refreshStored();
|
|
125
162
|
const message = fallback === 'env'
|
|
126
|
-
? 'The key was removed from your keychain -
|
|
163
|
+
? 'The key was removed from your keychain - a key in a local file is still being used.'
|
|
127
164
|
: 'The key was removed. You are signed out until a new key is added.';
|
|
128
165
|
if (fallback !== 'env')
|
|
129
166
|
session.setStatus('disconnected');
|
|
@@ -140,14 +177,18 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
140
177
|
await deleteKey(provider);
|
|
141
178
|
refreshStored();
|
|
142
179
|
void refreshCredit();
|
|
143
|
-
setPhase({ kind: 'saved', message:
|
|
180
|
+
setPhase({ kind: 'saved', message: "The management key was removed - the info bar shows the key's spending limit again." });
|
|
144
181
|
return;
|
|
145
182
|
}
|
|
146
|
-
await
|
|
183
|
+
await removeServiceKey(provider);
|
|
147
184
|
refreshStored();
|
|
185
|
+
if (session.providerId === provider)
|
|
186
|
+
session.setStatus('disconnected');
|
|
148
187
|
setPhase({ kind: 'saved', message: `The saved key for ${rowLabel(provider)} was removed.` });
|
|
149
188
|
}
|
|
150
189
|
useInput((input, key) => {
|
|
190
|
+
if (isMouseSequence(input))
|
|
191
|
+
return;
|
|
151
192
|
if (phase.kind === 'ask') {
|
|
152
193
|
const answer = input.toLowerCase();
|
|
153
194
|
if (answer === 'y') {
|
|
@@ -226,14 +267,18 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
226
267
|
return;
|
|
227
268
|
}
|
|
228
269
|
if (key.downArrow) {
|
|
229
|
-
setCursor((current) => Math.min(
|
|
270
|
+
setCursor((current) => Math.min(visibleRows.length - 1, current + 1));
|
|
230
271
|
return;
|
|
231
272
|
}
|
|
232
273
|
if (key.return) {
|
|
233
|
-
const row =
|
|
274
|
+
const row = visibleRows[cursor];
|
|
275
|
+
if (row?.id === CUSTOM_SERVICE_ID) {
|
|
276
|
+
setNote('Add it in /model, under Other service - it needs a web address as well as a key.');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
234
279
|
if (!row || row.id === 'ollama') {
|
|
235
280
|
if (mode === 'wizard' && row?.id === 'ollama') {
|
|
236
|
-
finishWizard('Ollama runs
|
|
281
|
+
finishWizard('Ollama runs on this computer - no key needed.', false);
|
|
237
282
|
}
|
|
238
283
|
return;
|
|
239
284
|
}
|
|
@@ -244,7 +289,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
244
289
|
}
|
|
245
290
|
const answer = input.toLowerCase();
|
|
246
291
|
if (answer === 'd') {
|
|
247
|
-
const row =
|
|
292
|
+
const row = visibleRows[cursor];
|
|
248
293
|
if (!row)
|
|
249
294
|
return;
|
|
250
295
|
const hasStored = stored.includes(row.id) || (row.id === 'openrouter' && getKeySource() !== null);
|
|
@@ -263,7 +308,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
263
308
|
: phase.kind === 'saved'
|
|
264
309
|
? 'Saved'
|
|
265
310
|
: mode === 'wizard'
|
|
266
|
-
? 'Which
|
|
311
|
+
? 'Which AI service should do the thinking?'
|
|
267
312
|
: 'Keys - stored in your Mac keychain';
|
|
268
313
|
const hint = phase.kind === 'ask'
|
|
269
314
|
? 'y = yes · n = not now'
|
|
@@ -277,5 +322,5 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
277
322
|
? '↑↓ move · Enter choose · Esc back'
|
|
278
323
|
: '↑↓ move · Enter add or replace · d remove · Esc close';
|
|
279
324
|
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: title }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [phase.kind === 'ask' && _jsx(Text, { children: ' Add an API key?' }), phase.kind === 'list' &&
|
|
280
|
-
|
|
325
|
+
visibleRows.map((row, index) => (_jsxs(Text, { inverse: index === cursor, children: [` ${row.label}`.padEnd(21), _jsx(Text, { dimColor: true, children: statusFor(row.id) })] }, row.id))), phase.kind === 'enter-key' && (_jsxs(Text, { children: [_jsxs(Text, { children: ["Paste the ", phase.label, " key (it stays hidden): "] }), _jsx(Text, { inverse: true, children: " " })] })), phase.kind === 'confirm-remove' && (_jsxs(Text, { children: ["Remove the ", phase.label, " key from your Mac keychain?"] })), phase.kind === 'saved' && _jsx(Text, { children: phase.message })] }), note ? (_jsx(Text, { color: "yellow", children: note })) : (_jsx(Text, { dimColor: true, children: hint }))] }));
|
|
281
326
|
}
|