@tianmucreations/jeeves 0.3.1 → 0.3.2
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/dist/agent/errors.js +1 -1
- package/dist/agent/loop.js +34 -1
- package/dist/agent/systemPrompt.js +19 -5
- package/dist/app.js +23 -1
- package/dist/checkpoints/index.js +34 -4
- package/dist/commands/clear.js +2 -0
- package/dist/commands/help.js +16 -14
- package/dist/commands/keys.js +2 -1
- package/dist/components/AddressPrompt.js +21 -6
- package/dist/components/Footer.js +6 -1
- package/dist/components/Header.js +5 -1
- package/dist/components/HelpView.js +1 -1
- package/dist/components/Input.js +135 -13
- package/dist/components/KeysManager.js +33 -18
- package/dist/components/ModelPicker.js +27 -8
- package/dist/components/OpenRouterConnect.js +114 -0
- package/dist/components/ProjectPicker.js +55 -16
- package/dist/components/Transcript.js +39 -2
- package/dist/components/input-layout.js +129 -26
- package/dist/components/markdown.js +185 -0
- package/dist/components/transcript-layout.js +50 -2
- package/dist/index.js +2 -1
- package/dist/ink/mouse.js +39 -6
- package/dist/ink/quit.js +21 -0
- package/dist/ink/selection.js +128 -0
- package/dist/keys/store.js +72 -2
- package/dist/platform/address.js +16 -0
- package/dist/platform/chat-folder.js +24 -0
- package/dist/platform/config.js +3 -0
- package/dist/platform/wording.js +10 -0
- package/dist/providers/direct.js +8 -2
- package/dist/providers/index.js +21 -3
- package/dist/providers/ollama.js +8 -2
- package/dist/providers/openrouter-signin.js +102 -0
- package/dist/providers/openrouter.js +17 -2
- package/dist/providers/silence.js +39 -0
- package/dist/providers/zai.js +17 -13
- package/dist/state/session.js +41 -2
- package/dist/tools/index.js +22 -5
- package/dist/tools/runBash.js +17 -5
- package/dist/tools/web/research.js +119 -21
- package/dist/tools/web/zai-search.js +79 -0
- package/package.json +2 -1
package/dist/components/Input.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import stringWidth from 'string-width';
|
|
3
4
|
import { Box, Text, useCursor, useInput, usePaste, useStdout } from 'ink';
|
|
4
|
-
import { runTurn } from '../agent/loop.js';
|
|
5
|
+
import { runTurn, stopTurn } from '../agent/loop.js';
|
|
6
|
+
import { copySelection } from '../ink/selection.js';
|
|
7
|
+
import { pressCtrlCToQuit } from '../ink/quit.js';
|
|
5
8
|
import { answerApproval, currentApprovalTrustable } from '../agent/permissions.js';
|
|
6
9
|
import { session, useSession } from '../state/session.js';
|
|
7
10
|
import { BLOCK_CURSOR, inputFrameRow } from '../ink/cursor.js';
|
|
8
11
|
import { isMouseSequence, handleMouseInput } from '../ink/mouse.js';
|
|
9
|
-
import { inputLayout,
|
|
12
|
+
import { inputLayout, splitTypedBurst, cleanPaste, scrollToShowCursor, previousWordStart, nextWordEnd } from './input-layout.js';
|
|
10
13
|
// The rows the input box needs for the text being typed (the window makes room).
|
|
11
14
|
export const MAX_INPUT_ROWS = 6;
|
|
12
15
|
export function inputRowsFor(text, width) {
|
|
@@ -27,11 +30,63 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
27
30
|
const s = useSession();
|
|
28
31
|
const { stdout } = useStdout();
|
|
29
32
|
const { setCursorPosition } = useCursor();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
+
// How far the box is scrolled back through a message taller than it (0 = the
|
|
34
|
+
// end, where the typing is). A ref for the same reason as the text.
|
|
35
|
+
const draftUpRef = useRef(0);
|
|
36
|
+
const [, setDraftUp] = useState(0);
|
|
37
|
+
// Where the cursor is in the message, in characters; null means at the end (as
|
|
38
|
+
// Claude Code: arrows, Option+arrows for words, Ctrl+A / Ctrl+E, and a click
|
|
39
|
+
// move it, and typing goes in where it is - owner, 19 Sept).
|
|
40
|
+
const cursorRef = useRef(null);
|
|
41
|
+
const [, setCursorTick] = useState(0);
|
|
42
|
+
const layout = inputLayout(valueRef.current, width, MAX_INPUT_ROWS, draftUpRef.current, cursorRef.current);
|
|
43
|
+
// The real cursor only at the end of the message; inside it, the highlighted
|
|
44
|
+
// character is the cursor.
|
|
45
|
+
const showingText = !s.approvalPending && s.transcriptScrollUp === 0 && layout.scrollUp === 0 && cursorRef.current === null;
|
|
46
|
+
const scrollDraft = (up) => {
|
|
47
|
+
draftUpRef.current = up;
|
|
48
|
+
setDraftUp(up);
|
|
49
|
+
};
|
|
50
|
+
const chars = () => Array.from(valueRef.current);
|
|
51
|
+
const moveCursor = (to) => {
|
|
52
|
+
const length = chars().length;
|
|
53
|
+
const next = to === null || to >= length ? null : Math.max(0, to);
|
|
54
|
+
cursorRef.current = next;
|
|
55
|
+
scrollDraft(scrollToShowCursor(valueRef.current, width, MAX_INPUT_ROWS, next === null ? 0 : draftUpRef.current, next));
|
|
56
|
+
setCursorTick((tick) => tick + 1);
|
|
57
|
+
};
|
|
58
|
+
const setValue = (text, cursor = null) => {
|
|
33
59
|
valueRef.current = text;
|
|
34
60
|
session.setInputText(text);
|
|
61
|
+
cursorRef.current = cursor !== null && cursor < Array.from(text).length ? cursor : null;
|
|
62
|
+
// A change shows where it was made: at the end, or at the cursor inside the text.
|
|
63
|
+
const up = cursorRef.current === null ? 0 : scrollToShowCursor(text, width, MAX_INPUT_ROWS, draftUpRef.current, cursorRef.current);
|
|
64
|
+
if (up !== draftUpRef.current)
|
|
65
|
+
scrollDraft(up);
|
|
66
|
+
};
|
|
67
|
+
// Typed or pasted text goes in at the cursor.
|
|
68
|
+
const insert = (text) => {
|
|
69
|
+
const all = chars();
|
|
70
|
+
const at = cursorRef.current ?? all.length;
|
|
71
|
+
const added = Array.from(text);
|
|
72
|
+
setValue([...all.slice(0, at), ...added, ...all.slice(at)].join(''), cursorRef.current === null ? null : at + added.length);
|
|
73
|
+
};
|
|
74
|
+
// Clicking in the typing box puts the cursor there.
|
|
75
|
+
session.inputClick = (col, row) => {
|
|
76
|
+
const rows = stdout.rows ?? 24;
|
|
77
|
+
const first = rows - 2 - (layout.rows.length - 1);
|
|
78
|
+
const target = layout.rows[row - first];
|
|
79
|
+
if (!valueRef.current || !target || target.hint || target.start === undefined)
|
|
80
|
+
return;
|
|
81
|
+
let width = 0;
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (const ch of Array.from(target.text)) {
|
|
84
|
+
if (width >= col - 3)
|
|
85
|
+
break;
|
|
86
|
+
width += stringWidth(ch);
|
|
87
|
+
offset += 1;
|
|
88
|
+
}
|
|
89
|
+
moveCursor(target.start + offset);
|
|
35
90
|
};
|
|
36
91
|
// The block cursor sits at the text insertion point: two columns in (the
|
|
37
92
|
// border's │ and its padding space) plus the visible text's width, measured
|
|
@@ -41,7 +96,8 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
41
96
|
// hands the position to Ink in its own useInsertionEffect, which runs before
|
|
42
97
|
// this commit's frame is written - a useLayoutEffect call runs after that and
|
|
43
98
|
// only lands a frame late (measured: the cursor stayed hidden when the window
|
|
44
|
-
// opened). Hidden while the row shows a hint instead of the text
|
|
99
|
+
// opened). Hidden while the row shows a hint instead of the text, and while the
|
|
100
|
+
// box is scrolled back through a long message (the typing point is out of view).
|
|
45
101
|
// The last row of the box stays on the same terminal row however tall the box
|
|
46
102
|
// grows (it grows upwards), so the cursor's row is unchanged.
|
|
47
103
|
setCursorPosition(showingText ? { x: 2 + layout.cursorX, y: inputFrameRow(stdout.rows ?? 24) } : undefined);
|
|
@@ -65,6 +121,33 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
65
121
|
}
|
|
66
122
|
if (s.pickerOpen || s.keysOpen || s.wizardActive || s.helpOpen)
|
|
67
123
|
return;
|
|
124
|
+
// Ctrl+C, as Claude Code: copy a selection, else clear the typing, else stop the
|
|
125
|
+
// job, else quit only when pressed twice. It used to quit at once, losing the
|
|
126
|
+
// conversation - and Windows and Linux people press Ctrl+C to copy (audit, 19 Sept).
|
|
127
|
+
if (key.ctrl && input === 'c') {
|
|
128
|
+
if (session.selection) {
|
|
129
|
+
void copySelection();
|
|
130
|
+
session.setSelection(null);
|
|
131
|
+
}
|
|
132
|
+
else if (valueRef.current) {
|
|
133
|
+
setValue('');
|
|
134
|
+
}
|
|
135
|
+
else if (s.status === 'working' || s.approvalPending) {
|
|
136
|
+
stopTurn();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
pressCtrlCToQuit();
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Esc stops the job, as in Claude Code and the window's Stop button.
|
|
144
|
+
if (key.escape && (s.status === 'working' || s.approvalPending)) {
|
|
145
|
+
stopTurn();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Any key clears a selection, as in Claude Code.
|
|
149
|
+
if (session.selection)
|
|
150
|
+
session.setSelection(null);
|
|
68
151
|
if (s.approvalPending) {
|
|
69
152
|
const answer = input.toLowerCase();
|
|
70
153
|
if (answer === 'y')
|
|
@@ -83,6 +166,31 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
83
166
|
s.toggleShowLastReasoning();
|
|
84
167
|
return;
|
|
85
168
|
}
|
|
169
|
+
// A message taller than the box: the arrows move through the message itself
|
|
170
|
+
// (as Claude Code's do when the input spans more than one line). Page Up/Down
|
|
171
|
+
// and the mouse wheel still scroll the conversation.
|
|
172
|
+
if ((key.upArrow || key.downArrow) && layout.maxScrollUp > 0 && s.transcriptScrollUp === 0) {
|
|
173
|
+
const next = Math.min(draftUpRef.current, layout.maxScrollUp) + (key.upArrow ? 3 : -3);
|
|
174
|
+
scrollDraft(Math.max(0, Math.min(next, layout.maxScrollUp)));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Moving the cursor within the message, as in Claude Code.
|
|
178
|
+
if (valueRef.current && s.transcriptScrollUp === 0) {
|
|
179
|
+
const here = cursorRef.current ?? chars().length;
|
|
180
|
+
if (key.leftArrow && !key.meta && !key.ctrl)
|
|
181
|
+
return moveCursor(Math.max(0, here - 1));
|
|
182
|
+
if (key.rightArrow && !key.meta && !key.ctrl)
|
|
183
|
+
return moveCursor(here + 1);
|
|
184
|
+
// Option+Left / Option+Right arrive from Terminal.app as Esc-b / Esc-f.
|
|
185
|
+
if ((key.leftArrow && (key.meta || key.ctrl)) || (key.meta && input === 'b'))
|
|
186
|
+
return moveCursor(previousWordStart(valueRef.current, here));
|
|
187
|
+
if ((key.rightArrow && (key.meta || key.ctrl)) || (key.meta && input === 'f'))
|
|
188
|
+
return moveCursor(nextWordEnd(valueRef.current, here));
|
|
189
|
+
if (key.ctrl && input === 'a')
|
|
190
|
+
return moveCursor(0);
|
|
191
|
+
if (key.ctrl && input === 'e')
|
|
192
|
+
return moveCursor(null);
|
|
193
|
+
}
|
|
86
194
|
// The alternate screen has no native scrollback, so these keys scroll the
|
|
87
195
|
// transcript region itself (Claude Code's bindings): arrows move 3 rows,
|
|
88
196
|
// Page Up/Down a full page, End jumps back to the newest and re-follows.
|
|
@@ -109,7 +217,9 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
109
217
|
// A burst of typing that ends with Enter is typing plus Enter (see splitTypedBurst).
|
|
110
218
|
const burst = key.return ? { typed: '', enter: true, rest: '' } : splitTypedBurst(input);
|
|
111
219
|
if (burst.enter) {
|
|
112
|
-
|
|
220
|
+
if (burst.typed)
|
|
221
|
+
insert(burst.typed);
|
|
222
|
+
const text = valueRef.current.trim();
|
|
113
223
|
setValue(burst.rest);
|
|
114
224
|
if (!text)
|
|
115
225
|
return;
|
|
@@ -127,13 +237,18 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
127
237
|
}
|
|
128
238
|
if (key.backspace || key.delete) {
|
|
129
239
|
s.followTranscript();
|
|
130
|
-
|
|
240
|
+
// The character before the cursor goes (one whole character, never half of one).
|
|
241
|
+
const all = chars();
|
|
242
|
+
const at = cursorRef.current ?? all.length;
|
|
243
|
+
if (at === 0)
|
|
244
|
+
return;
|
|
245
|
+
setValue([...all.slice(0, at - 1), ...all.slice(at)].join(''), cursorRef.current === null ? null : at - 1);
|
|
131
246
|
return;
|
|
132
247
|
}
|
|
133
248
|
if (!input || key.ctrl || key.meta)
|
|
134
249
|
return;
|
|
135
250
|
s.followTranscript();
|
|
136
|
-
|
|
251
|
+
insert(input);
|
|
137
252
|
});
|
|
138
253
|
// Pasted text arrives whole (bracketed paste), keeps its line breaks, and never
|
|
139
254
|
// sends by itself - only Enter does.
|
|
@@ -141,7 +256,7 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
141
256
|
if (s.pickerOpen || s.keysOpen || s.wizardActive || s.helpOpen || s.approvalPending)
|
|
142
257
|
return;
|
|
143
258
|
s.followTranscript();
|
|
144
|
-
|
|
259
|
+
insert(cleanPaste(text));
|
|
145
260
|
});
|
|
146
261
|
if (s.approvalPending) {
|
|
147
262
|
return (_jsx(Text, { color: "yellow", children: currentApprovalTrustable() ? 'y = allow · a = always allow in this project · n = deny' : 'y = allow · n = deny' }));
|
|
@@ -156,7 +271,14 @@ export function Input({ scrollPage = 10, width = 76 }) {
|
|
|
156
271
|
// bytes differ from the same text without them, keeping Ink on its full-frame
|
|
157
272
|
// path (see input-layout.ts).
|
|
158
273
|
if (!valueRef.current) {
|
|
159
|
-
return _jsx(Text, { dimColor: true, children: s.status === 'working' ? 'type your next message - it will be sent when I finish' : 'ask anything' });
|
|
274
|
+
return _jsx(Text, { dimColor: true, children: s.status === 'working' ? 'type your next message - it will be sent when I finish · Esc stops' : 'ask anything' });
|
|
160
275
|
}
|
|
161
|
-
return (_jsx(Box, { flexDirection: "column", children: layout.rows.map((row, index) =>
|
|
276
|
+
return (_jsx(Box, { flexDirection: "column", children: layout.rows.map((row, index) => {
|
|
277
|
+
if (row.cursorAt !== undefined) {
|
|
278
|
+
// The cursor inside the text: the character under it drawn reversed.
|
|
279
|
+
const rowChars = Array.from(row.text);
|
|
280
|
+
return (_jsxs(Text, { children: [rowChars.slice(0, row.cursorAt).join(''), _jsx(Text, { inverse: true, children: rowChars[row.cursorAt] ?? ' ' }), rowChars.slice(row.cursorAt + 1).join('')] }, index));
|
|
281
|
+
}
|
|
282
|
+
return (_jsxs(Text, { dimColor: row.hint, children: [row.text, row.trailingSpaces ? _jsx(Text, { dimColor: true, children: row.trailingSpaces }) : null] }, index));
|
|
283
|
+
}) }));
|
|
162
284
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, 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';
|
|
@@ -9,6 +9,8 @@ import { setDefaultModel, setDefaultProvider } from '../platform/config.js';
|
|
|
9
9
|
import { setKey, deleteKey } from '../keys/store.js';
|
|
10
10
|
import { keyLooksValid } from '../commands/keys.js';
|
|
11
11
|
import { isMouseSequence } from '../ink/mouse.js';
|
|
12
|
+
import { OpenRouterConnect } from './OpenRouterConnect.js';
|
|
13
|
+
import { COPY_KEYS, KEY_STORE, KEY_STORE_SUBJECT } from '../platform/wording.js';
|
|
12
14
|
// Every service Jeeves connects to. The optional OpenRouter management key (it unlocks
|
|
13
15
|
// the real account balance) is for /keys only, and the compatible service needs its
|
|
14
16
|
// address too, so it is set up in /model - the first-run wizard keeps to the essentials.
|
|
@@ -34,10 +36,10 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
34
36
|
function statusFor(rowId) {
|
|
35
37
|
if (rowId === 'openrouter') {
|
|
36
38
|
if (getKeySource() === 'keychain')
|
|
37
|
-
return
|
|
39
|
+
return `key stored in ${KEY_STORE}`;
|
|
38
40
|
if (getKeySource() === 'env')
|
|
39
41
|
return 'key in a local file - add it with /keys to store it safely';
|
|
40
|
-
return stored.includes('openrouter') ?
|
|
42
|
+
return stored.includes('openrouter') ? `key stored in ${KEY_STORE}` : 'no key';
|
|
41
43
|
}
|
|
42
44
|
if (rowId === 'openrouter-management') {
|
|
43
45
|
return stored.includes('openrouter-management')
|
|
@@ -54,7 +56,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
54
56
|
}
|
|
55
57
|
return stored.includes(rowId) ? 'key stored - direct connection ready' : 'no key';
|
|
56
58
|
}
|
|
57
|
-
function finishWizard(message, connected) {
|
|
59
|
+
function finishWizard(message, connected, skipped = false) {
|
|
58
60
|
if (connected) {
|
|
59
61
|
session.addNotice(message);
|
|
60
62
|
if (session.status === 'disconnected')
|
|
@@ -64,17 +66,17 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
64
66
|
else {
|
|
65
67
|
session.addNotice(message);
|
|
66
68
|
}
|
|
67
|
-
session.endWizard();
|
|
69
|
+
session.endWizard(skipped);
|
|
68
70
|
}
|
|
69
71
|
async function saveKey(provider, key) {
|
|
70
72
|
if (provider === 'openrouter') {
|
|
71
73
|
const saved = await storeOpenRouterKey(key);
|
|
72
74
|
if (!saved) {
|
|
73
|
-
setNote(
|
|
75
|
+
setNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
78
|
refreshStored();
|
|
77
|
-
const message =
|
|
79
|
+
const message = `Your key is saved securely in ${KEY_STORE}. You will not be asked for it again.`;
|
|
78
80
|
if (mode === 'wizard') {
|
|
79
81
|
finishWizard(message, true);
|
|
80
82
|
}
|
|
@@ -86,7 +88,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
86
88
|
if (provider === 'zai') {
|
|
87
89
|
const saved = await storeZaiKey(key);
|
|
88
90
|
if (!saved) {
|
|
89
|
-
setNote(
|
|
91
|
+
setNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
refreshStored();
|
|
@@ -109,7 +111,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
if (result === 'keychain') {
|
|
112
|
-
setNote(
|
|
114
|
+
setNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
refreshStored();
|
|
@@ -132,7 +134,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
132
134
|
}
|
|
133
135
|
const saved = await setKey(provider, key);
|
|
134
136
|
if (!saved) {
|
|
135
|
-
setNote(
|
|
137
|
+
setNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
136
138
|
return;
|
|
137
139
|
}
|
|
138
140
|
refreshStored();
|
|
@@ -189,6 +191,9 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
189
191
|
useInput((input, key) => {
|
|
190
192
|
if (isMouseSequence(input))
|
|
191
193
|
return;
|
|
194
|
+
// The OpenRouter screen handles its own keys.
|
|
195
|
+
if (phase.kind === 'openrouter')
|
|
196
|
+
return;
|
|
192
197
|
if (phase.kind === 'ask') {
|
|
193
198
|
const answer = input.toLowerCase();
|
|
194
199
|
if (answer === 'y') {
|
|
@@ -196,7 +201,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
196
201
|
setPhase({ kind: 'list' });
|
|
197
202
|
}
|
|
198
203
|
else if (answer === 'n') {
|
|
199
|
-
finishWizard('
|
|
204
|
+
finishWizard('Not connected yet - type /keys any time to connect an AI service. It takes about a minute.', false, true);
|
|
200
205
|
}
|
|
201
206
|
return;
|
|
202
207
|
}
|
|
@@ -255,7 +260,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
255
260
|
// phase: list
|
|
256
261
|
if (key.escape) {
|
|
257
262
|
if (mode === 'wizard') {
|
|
258
|
-
finishWizard('
|
|
263
|
+
finishWizard('Not connected yet - type /keys any time to connect an AI service. It takes about a minute.', false, true);
|
|
259
264
|
}
|
|
260
265
|
else {
|
|
261
266
|
session.closeKeys();
|
|
@@ -282,9 +287,10 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
282
287
|
}
|
|
283
288
|
return;
|
|
284
289
|
}
|
|
285
|
-
setPhase({ kind: 'enter-key', provider: row.id, label: row.label });
|
|
286
290
|
setNote('');
|
|
287
291
|
setHidden('');
|
|
292
|
+
// OpenRouter gets the explained choice: sign in through the browser, or paste a key.
|
|
293
|
+
setPhase(row.id === 'openrouter' ? { kind: 'openrouter' } : { kind: 'enter-key', provider: row.id, label: row.label });
|
|
288
294
|
return;
|
|
289
295
|
}
|
|
290
296
|
const answer = input.toLowerCase();
|
|
@@ -300,7 +306,7 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
300
306
|
}
|
|
301
307
|
});
|
|
302
308
|
const title = phase.kind === 'ask'
|
|
303
|
-
? 'Welcome - one quick setup
|
|
309
|
+
? 'Welcome - one quick setup step'
|
|
304
310
|
: phase.kind === 'enter-key'
|
|
305
311
|
? `Paste the ${phase.label} key`
|
|
306
312
|
: phase.kind === 'confirm-remove'
|
|
@@ -309,9 +315,9 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
309
315
|
? 'Saved'
|
|
310
316
|
: mode === 'wizard'
|
|
311
317
|
? 'Which AI service should do the thinking?'
|
|
312
|
-
:
|
|
318
|
+
: `Keys - stored in ${KEY_STORE}`;
|
|
313
319
|
const hint = phase.kind === 'ask'
|
|
314
|
-
? 'y = yes · n = not now'
|
|
320
|
+
? 'y = yes, set it up · n = not now'
|
|
315
321
|
: phase.kind === 'enter-key'
|
|
316
322
|
? 'paste the key · Enter save · Esc back'
|
|
317
323
|
: phase.kind === 'confirm-remove'
|
|
@@ -321,6 +327,15 @@ export function KeysManager({ mode, rows, columns }) {
|
|
|
321
327
|
: mode === 'wizard'
|
|
322
328
|
? '↑↓ move · Enter choose · Esc back'
|
|
323
329
|
: '↑↓ move · Enter add or replace · d remove · Esc close';
|
|
324
|
-
|
|
325
|
-
|
|
330
|
+
if (phase.kind === 'openrouter') {
|
|
331
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(OpenRouterConnect, { hasKey: stored.includes('openrouter') || getKeySource() !== null, onBack: () => setPhase({ kind: 'list' }), onDone: (message) => {
|
|
332
|
+
refreshStored();
|
|
333
|
+
if (mode === 'wizard')
|
|
334
|
+
finishWizard(message, true);
|
|
335
|
+
else
|
|
336
|
+
setPhase({ kind: 'saved', message });
|
|
337
|
+
} }) }));
|
|
338
|
+
}
|
|
339
|
+
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' && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Jeeves needs an AI service to think with - the company that runs the AI models." }), _jsx(Text, { children: "You connect one account once, and pay that service only for what you use." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Set one up now? (y/n)" })] })), phase.kind === 'list' &&
|
|
340
|
+
visibleRows.map((row, index) => (_jsxs(Text, { inverse: index === cursor, children: [` ${row.label}`.padEnd(21), _jsx(Text, { dimColor: true, children: mode === 'wizard' && 'description' in row && !stored.includes(row.id) ? row.description : statusFor(row.id) })] }, row.id))), phase.kind === 'enter-key' && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsxs(Text, { children: ["Paste the ", phase.label, " key (it stays hidden): "] }), _jsx(Text, { dimColor: true, children: hidden ? `${Array.from(hidden).length} characters ` : '' }), _jsx(Text, { inverse: true, children: " " })] }), isDirectService(phase.provider) ? (_jsx(Text, { dimColor: true, children: `Get one at ${directService(phase.provider).keyPage} - select the address with the mouse and press ${COPY_KEYS} to copy it.` })) : phase.provider === 'zai' ? (_jsx(Text, { dimColor: true, children: `Get one at z.ai/manage-apikey/apikey-list - select the address with the mouse and press ${COPY_KEYS} to copy it.` })) : null] })), phase.kind === 'confirm-remove' && (_jsxs(Text, { children: ["Remove the ", phase.label, " key from ", KEY_STORE, "?"] })), phase.kind === 'saved' && _jsx(Text, { children: phase.message })] }), note ? (_jsx(Text, { color: "yellow", children: note })) : (_jsx(Text, { dimColor: true, children: hint }))] }));
|
|
326
341
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
5
|
import Fuse from 'fuse.js';
|
|
@@ -12,10 +12,12 @@ import { directService, isDirectService, CUSTOM_SERVICE_ID } from '../providers/
|
|
|
12
12
|
import { loadDirectModels, checkCustomService } from '../providers/catalogue.js';
|
|
13
13
|
import { autoRowFor, noAutoNote } from '../agent/auto.js';
|
|
14
14
|
import { getCustomService } from '../platform/config.js';
|
|
15
|
+
import { OpenRouterConnect } from './OpenRouterConnect.js';
|
|
15
16
|
import { keyLooksValid } from '../commands/keys.js';
|
|
16
17
|
import { listLocalOllamaModels, isOllamaOnline } from '../providers/ollama.js';
|
|
17
18
|
import { ZAI_MODELS } from '../providers/zai.js';
|
|
18
19
|
import { isMouseSequence } from '../ink/mouse.js';
|
|
20
|
+
import { COPY_KEYS, KEY_STORE, KEY_STORE_SUBJECT } from '../platform/wording.js';
|
|
19
21
|
const TABS = ['favorites', 'recent', 'all', 'tools', 'free'];
|
|
20
22
|
const TAB_LABELS = {
|
|
21
23
|
favorites: 'Favorites',
|
|
@@ -134,6 +136,8 @@ export function ModelPicker({ rows, columns }) {
|
|
|
134
136
|
const [addressValue, setAddressValue] = useState('');
|
|
135
137
|
// True while a key is being checked with its company.
|
|
136
138
|
const [checking, setChecking] = useState(false);
|
|
139
|
+
// The browser sign-in in progress, so Esc can cancel it.
|
|
140
|
+
const signInAbort = useRef(null);
|
|
137
141
|
const [limitValue, setLimitValue] = useState('');
|
|
138
142
|
const [limitNote, setLimitNote] = useState('');
|
|
139
143
|
const [limitReturn, setLimitReturn] = useState('providers');
|
|
@@ -431,9 +435,24 @@ export function ModelPicker({ rows, columns }) {
|
|
|
431
435
|
setAddressValue((v) => v + input);
|
|
432
436
|
return;
|
|
433
437
|
}
|
|
438
|
+
// OpenRouter's key step is its own explained screen, which handles its own keys.
|
|
439
|
+
if (step === 'key' && keyFor === 'openrouter')
|
|
440
|
+
return;
|
|
441
|
+
if (step === 'key' && keyFor === 'openrouter') {
|
|
442
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(OpenRouterConnect, { hasKey: false, onBack: () => setStep('providers'), onDone: (message) => {
|
|
443
|
+
// Said in the conversation too - including "no credit yet" for a new account.
|
|
444
|
+
s.addNotice(message);
|
|
445
|
+
if (s.status === 'disconnected')
|
|
446
|
+
s.setStatus('idle');
|
|
447
|
+
enterModelStep('openrouter');
|
|
448
|
+
} }) }));
|
|
449
|
+
}
|
|
434
450
|
if (step === 'key') {
|
|
435
|
-
if (checking)
|
|
451
|
+
if (checking) {
|
|
452
|
+
if (key.escape && signInAbort.current)
|
|
453
|
+
signInAbort.current.abort();
|
|
436
454
|
return;
|
|
455
|
+
}
|
|
437
456
|
if (key.escape) {
|
|
438
457
|
goBack();
|
|
439
458
|
return;
|
|
@@ -456,7 +475,7 @@ export function ModelPicker({ rows, columns }) {
|
|
|
456
475
|
setKeyNote("The service didn't accept that key - paste it again, or Esc");
|
|
457
476
|
}
|
|
458
477
|
else if (result === 'keychain') {
|
|
459
|
-
setKeyNote(
|
|
478
|
+
setKeyNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
460
479
|
}
|
|
461
480
|
else {
|
|
462
481
|
setStep('address');
|
|
@@ -483,7 +502,7 @@ export function ModelPicker({ rows, columns }) {
|
|
|
483
502
|
return;
|
|
484
503
|
}
|
|
485
504
|
if (result === 'keychain') {
|
|
486
|
-
setKeyNote(
|
|
505
|
+
setKeyNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
487
506
|
return;
|
|
488
507
|
}
|
|
489
508
|
if (result === 'saved-unchecked') {
|
|
@@ -508,7 +527,7 @@ export function ModelPicker({ rows, columns }) {
|
|
|
508
527
|
const store = keyFor === 'openrouter' ? storeOpenRouterKey : storeZaiKey;
|
|
509
528
|
void store(trimmed).then((saved) => {
|
|
510
529
|
if (!saved) {
|
|
511
|
-
setKeyNote(
|
|
530
|
+
setKeyNote(`${KEY_STORE_SUBJECT} was not reachable - press Enter and try again.`);
|
|
512
531
|
return;
|
|
513
532
|
}
|
|
514
533
|
if (keyFor === 'zai') {
|
|
@@ -648,13 +667,13 @@ export function ModelPicker({ rows, columns }) {
|
|
|
648
667
|
? `Other service — ${addressValue.trim()}`
|
|
649
668
|
: `${service} — a direct connection with your own key`;
|
|
650
669
|
const where = keyFor === 'openrouter'
|
|
651
|
-
? '
|
|
670
|
+
? ''
|
|
652
671
|
: direct
|
|
653
|
-
? `Get one at ${direct.keyPage}. `
|
|
672
|
+
? `Get one at ${direct.keyPage} (select it with the mouse, ${COPY_KEYS} to copy). `
|
|
654
673
|
: keyFor === CUSTOM_SERVICE_ID
|
|
655
674
|
? 'No key needed for a service on this computer - just press Enter. '
|
|
656
675
|
: '';
|
|
657
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: title }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsxs(Text, { children: [_jsxs(Text, { children: ["Paste your ", service, " API key (it stays hidden): "] }), _jsx(Text, { dimColor: true, children: keyValue ? `${keyValue.length} characters ` : '' }), _jsx(Text, { inverse: true, children: " " })] }), _jsxs(Text, { dimColor: true, children: [where, "It is stored in
|
|
676
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Text, { dimColor: true, children: title }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [_jsxs(Text, { children: [_jsxs(Text, { children: ["Paste your ", service, " API key (it stays hidden): "] }), _jsx(Text, { dimColor: true, children: keyValue ? `${keyValue.length} characters ` : '' }), _jsx(Text, { inverse: true, children: " " })] }), _jsxs(Text, { dimColor: true, children: [where, "It is stored in ", KEY_STORE, " and never shown again."] })] }), keyNote ? (_jsx(Text, { color: "yellow", children: keyNote })) : (_jsx(Text, { dimColor: true, children: "paste the key \u00B7 Enter save \u00B7 Esc back" }))] }));
|
|
658
677
|
}
|
|
659
678
|
if (step === 'curated') {
|
|
660
679
|
const picks = resolveCurated(catalog);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { signInWithOpenRouter, WAITING_STEPS } from '../providers/openrouter-signin.js';
|
|
5
|
+
import { noCreditNote } from '../providers/openrouter.js';
|
|
6
|
+
import { storeOpenRouterKey, refreshCredit } from '../providers/index.js';
|
|
7
|
+
import { keyLooksValid } from '../commands/keys.js';
|
|
8
|
+
import { isMouseSequence } from '../ink/mouse.js';
|
|
9
|
+
import { KEY_STORE, KEY_STORE_SUBJECT } from '../platform/wording.js';
|
|
10
|
+
const CHOICES = [
|
|
11
|
+
{ title: 'Sign in with OpenRouter (recommended)', detail: 'Your browser opens. Log in, or make a free account, then click Authorize and come back. No key to copy.' },
|
|
12
|
+
{ title: 'Paste a key I already have', detail: 'For people who already made a key at openrouter.ai/keys.' },
|
|
13
|
+
];
|
|
14
|
+
export function OpenRouterConnect({ hasKey, onDone, onBack }) {
|
|
15
|
+
const [step, setStep] = useState('choose');
|
|
16
|
+
const [cursor, setCursor] = useState(0);
|
|
17
|
+
const [pasted, setPasted] = useState('');
|
|
18
|
+
const [note, setNote] = useState('');
|
|
19
|
+
const [address, setAddress] = useState('');
|
|
20
|
+
const signingIn = useRef(null);
|
|
21
|
+
useEffect(() => () => signingIn.current?.abort(), []);
|
|
22
|
+
async function saveKey(key, how) {
|
|
23
|
+
if (!(await storeOpenRouterKey(key))) {
|
|
24
|
+
setStep('choose');
|
|
25
|
+
setNote(`${KEY_STORE_SUBJECT} was not reachable - please try again.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
void refreshCredit();
|
|
29
|
+
const credit = await noCreditNote(key);
|
|
30
|
+
const saved = how === 'signed-in'
|
|
31
|
+
? `Connected - Jeeves is signed in to OpenRouter, and the key is saved securely in ${KEY_STORE}.`
|
|
32
|
+
: `Your OpenRouter key is saved securely in ${KEY_STORE}. You will not be asked for it again.`;
|
|
33
|
+
onDone(credit ? `${saved} ${credit}` : saved);
|
|
34
|
+
}
|
|
35
|
+
function signIn() {
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
signingIn.current = controller;
|
|
38
|
+
setStep('waiting');
|
|
39
|
+
setNote('');
|
|
40
|
+
setAddress('');
|
|
41
|
+
void signInWithOpenRouter({ signal: controller.signal, onUrl: setAddress }).then((result) => {
|
|
42
|
+
signingIn.current = null;
|
|
43
|
+
if (result.ok) {
|
|
44
|
+
void saveKey(result.key, 'signed-in');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setStep('choose');
|
|
48
|
+
if (result.reason === 'cancelled')
|
|
49
|
+
return;
|
|
50
|
+
setNote(result.reason === 'timeout'
|
|
51
|
+
? 'No approval arrived after fifteen minutes, so I stopped waiting. Choose Sign in to try again.'
|
|
52
|
+
: "OpenRouter didn't complete the sign-in. Choose Sign in to try again, or paste a key.");
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
useInput((input, key) => {
|
|
56
|
+
if (isMouseSequence(input))
|
|
57
|
+
return;
|
|
58
|
+
if (step === 'waiting') {
|
|
59
|
+
if (key.escape)
|
|
60
|
+
signingIn.current?.abort();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (step === 'paste') {
|
|
64
|
+
if (key.escape) {
|
|
65
|
+
setStep('choose');
|
|
66
|
+
setPasted('');
|
|
67
|
+
setNote('');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (key.return) {
|
|
71
|
+
const value = pasted.trim();
|
|
72
|
+
if (!value) {
|
|
73
|
+
setNote('Paste your key first - or press Esc to go back and choose Sign in instead.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!keyLooksValid(value, 'openrouter')) {
|
|
77
|
+
setPasted('');
|
|
78
|
+
setNote('That does not look like an OpenRouter key (they start with sk-or-) - paste it again.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
void saveKey(value, 'pasted');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.backspace || key.delete) {
|
|
85
|
+
setPasted((current) => current.slice(0, -1));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!input || key.ctrl || key.meta)
|
|
89
|
+
return;
|
|
90
|
+
setNote('');
|
|
91
|
+
setPasted((current) => current + input.replace(/[\r\n]/g, ''));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// step: choose
|
|
95
|
+
if (key.escape)
|
|
96
|
+
return onBack();
|
|
97
|
+
if (key.upArrow || key.downArrow) {
|
|
98
|
+
setCursor((current) => (current === 0 ? 1 : 0));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (input === '1' || input === '2') {
|
|
102
|
+
setCursor(Number(input) - 1);
|
|
103
|
+
}
|
|
104
|
+
if (key.return || input === '1' || input === '2') {
|
|
105
|
+
const choice = input === '1' ? 0 : input === '2' ? 1 : cursor;
|
|
106
|
+
setNote('');
|
|
107
|
+
if (choice === 0)
|
|
108
|
+
signIn();
|
|
109
|
+
else
|
|
110
|
+
setStep('paste');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { dimColor: true, children: "Connect Jeeves to OpenRouter" }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [step === 'choose' && (_jsxs(_Fragment, { children: [_jsx(Text, { children: "OpenRouter runs the AI models Jeeves thinks with. One account gives you hundreds of models, and you pay only for what you use." }), hasKey ? _jsx(Text, { color: "yellow", children: "You already have an OpenRouter key saved. A new one replaces it - same account, same credit." }) : null, _jsx(Text, { children: " " }), CHOICES.map((choice, index) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { inverse: index === cursor, children: ` ${index + 1}. ${choice.title} ` }), _jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: choice.detail }) })] }, choice.title)))] })), step === 'waiting' && (_jsxs(_Fragment, { children: [WAITING_STEPS.map((line) => (_jsx(Text, { children: line }, line))), _jsx(Text, { children: " " }), address ? _jsx(Text, { dimColor: true, children: `Browser didn't open? Go to: ${address}` }) : null] })), step === 'paste' && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { children: "Paste your OpenRouter key (it stays hidden): " }), _jsx(Text, { dimColor: true, children: pasted ? `${pasted.length} characters ` : '' }), _jsx(Text, { inverse: true, children: " " })] }), _jsx(Text, { dimColor: true, children: "Keys start with sk-or- and are made at openrouter.ai/keys." })] }))] }), note ? (_jsx(Text, { color: "yellow", children: note })) : (_jsx(Text, { dimColor: true, children: step === 'choose' ? '↑↓ or 1 / 2 choose · Enter continue · Esc back' : step === 'waiting' ? 'Esc stop waiting' : 'paste the key · Enter save · Esc back' }))] }));
|
|
114
|
+
}
|