@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
package/dist/providers/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { createOpenRouterProvider, fetchCreditInfo } from './openrouter.js';
|
|
1
|
+
import { createOpenRouterProvider, fetchCreditInfo, fetchKeyUsage } from './openrouter.js';
|
|
2
|
+
import { getSpendReading, setSpendReading } from '../platform/config.js';
|
|
3
|
+
import { nextSpendReading, spentToday } from '../state/today-spend.js';
|
|
2
4
|
import { createOllamaProvider } from './ollama.js';
|
|
3
5
|
import { createZaiProvider } from './zai.js';
|
|
6
|
+
import { createDirectProvider, createCustomProvider } from './direct.js';
|
|
7
|
+
import { DIRECT_SERVICES, CUSTOM_SERVICE_ID, directService, isDirectService, serviceNameFor } from './direct-services.js';
|
|
8
|
+
import { checkKey, checkCustomService, forgetLiveList, loadCatalogue } from './catalogue.js';
|
|
9
|
+
import { getCustomService, setCustomService, getEstimatedSpend } from '../platform/config.js';
|
|
10
|
+
import { localDate } from '../state/today-spend.js';
|
|
4
11
|
import { session } from '../state/session.js';
|
|
5
12
|
import { getKey, setKey, deleteKey, listProviders } from '../keys/store.js';
|
|
6
13
|
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
@@ -10,17 +17,24 @@ let active = null;
|
|
|
10
17
|
let resolvedKey = null;
|
|
11
18
|
let zaiKey = null;
|
|
12
19
|
let keySource = null;
|
|
20
|
+
// Keys for the direct connections and the "any compatible service", by service id.
|
|
21
|
+
const serviceKeys = new Map();
|
|
22
|
+
// The OpenRouter key, for features that call OpenRouter directly (web research).
|
|
23
|
+
export function getOpenRouterKey() {
|
|
24
|
+
return resolvedKey;
|
|
25
|
+
}
|
|
13
26
|
// The calm provider list shared by the model picker and the key screens.
|
|
14
27
|
export const PROVIDER_ROWS = [
|
|
15
28
|
{ id: 'openrouter', label: 'OpenRouter', description: 'one key unlocks 400+ models - recommended' },
|
|
16
29
|
{ id: 'zai', label: 'Z.ai', description: 'GLM Coding Plan - $18/month flat - best for heavy daily use' },
|
|
17
|
-
{ id: 'anthropic', label: 'Anthropic', description: '
|
|
18
|
-
{ id: 'openai', label: 'OpenAI', description: '
|
|
19
|
-
{ id: 'google', label: 'Google', description: '
|
|
20
|
-
{ id: 'xai', label: 'xAI', description: '
|
|
21
|
-
{ id: 'groq', label: 'Groq', description: '
|
|
22
|
-
{ id: 'mistral', label: 'Mistral', description: '
|
|
23
|
-
{ id: '
|
|
30
|
+
{ id: 'anthropic', label: 'Anthropic', description: 'Claude, with your own Anthropic key' },
|
|
31
|
+
{ id: 'openai', label: 'OpenAI', description: 'GPT, with your own OpenAI key' },
|
|
32
|
+
{ id: 'google', label: 'Google', description: 'Gemini, with your own Google AI Studio key' },
|
|
33
|
+
{ id: 'xai', label: 'xAI (Grok)', description: 'Grok, with your own xAI key' },
|
|
34
|
+
{ id: 'groq', label: 'Groq', description: 'fast open models, with your own Groq key' },
|
|
35
|
+
{ id: 'mistral', label: 'Mistral', description: 'Mistral, with your own Mistral key' },
|
|
36
|
+
{ id: 'custom', label: 'Other service', description: 'any compatible service - paste its address and key' },
|
|
37
|
+
{ id: 'ollama', label: 'Ollama', description: 'models on this computer, no key needed' },
|
|
24
38
|
];
|
|
25
39
|
export function getKeySource() {
|
|
26
40
|
return keySource;
|
|
@@ -33,8 +47,60 @@ export function hasCredentialsFor(providerId) {
|
|
|
33
47
|
return zaiKey !== null;
|
|
34
48
|
if (providerId === 'ollama')
|
|
35
49
|
return true;
|
|
50
|
+
if (providerId === CUSTOM_SERVICE_ID)
|
|
51
|
+
return serviceKeys.has(CUSTOM_SERVICE_ID) && getCustomService() !== null;
|
|
52
|
+
if (isDirectService(providerId))
|
|
53
|
+
return serviceKeys.has(providerId);
|
|
36
54
|
return false;
|
|
37
55
|
}
|
|
56
|
+
// Uses a key for this session only, without saving it (the bench and tests; never the keychain).
|
|
57
|
+
export function useKeyForThisSession(providerId, key) {
|
|
58
|
+
serviceKeys.set(providerId, key);
|
|
59
|
+
}
|
|
60
|
+
// A compatible service may need no key (one running on this computer); this stands in.
|
|
61
|
+
export const NO_KEY = 'no-key';
|
|
62
|
+
// The key for a direct connection or the compatible service, if one is stored.
|
|
63
|
+
export function serviceKey(providerId) {
|
|
64
|
+
return serviceKeys.get(providerId) ?? null;
|
|
65
|
+
}
|
|
66
|
+
// The name of the compatible service, from its address ("Other service" before one is added).
|
|
67
|
+
export function customServiceName() {
|
|
68
|
+
const saved = getCustomService();
|
|
69
|
+
return saved ? serviceNameFor(saved.baseURL) : 'Other service';
|
|
70
|
+
}
|
|
71
|
+
// Checks a key with the company (a free request for its model list), then stores it.
|
|
72
|
+
// A key the company refuses is not stored; when the company can't be reached the key
|
|
73
|
+
// is stored anyway and the person is told it could not be checked.
|
|
74
|
+
export async function storeDirectKey(serviceId, key) {
|
|
75
|
+
const check = await checkKey(serviceId, key);
|
|
76
|
+
if (!check.ok && check.reason === 'rejected')
|
|
77
|
+
return 'rejected';
|
|
78
|
+
if (!(await setKey(serviceId, key)))
|
|
79
|
+
return 'keychain';
|
|
80
|
+
serviceKeys.set(serviceId, key);
|
|
81
|
+
forgetLiveList(serviceId);
|
|
82
|
+
return check.ok ? 'saved' : 'saved-unchecked';
|
|
83
|
+
}
|
|
84
|
+
export async function removeServiceKey(serviceId) {
|
|
85
|
+
await deleteKey(serviceId);
|
|
86
|
+
serviceKeys.delete(serviceId);
|
|
87
|
+
forgetLiveList(serviceId);
|
|
88
|
+
if (serviceId === CUSTOM_SERVICE_ID)
|
|
89
|
+
setCustomService(null);
|
|
90
|
+
}
|
|
91
|
+
// Checks the compatible service's address and key together (it must list its models),
|
|
92
|
+
// then stores both. A blank key is allowed for a service on this computer.
|
|
93
|
+
export async function storeCustomService(address, key) {
|
|
94
|
+
const secret = key.trim() || NO_KEY;
|
|
95
|
+
const check = await checkCustomService(address, secret);
|
|
96
|
+
if (!check.ok)
|
|
97
|
+
return check.reason;
|
|
98
|
+
if (!(await setKey(CUSTOM_SERVICE_ID, secret)))
|
|
99
|
+
return 'keychain';
|
|
100
|
+
setCustomService({ baseURL: check.baseURL });
|
|
101
|
+
serviceKeys.set(CUSTOM_SERVICE_ID, secret);
|
|
102
|
+
return check.keyChecked ? 'saved' : 'saved-unchecked';
|
|
103
|
+
}
|
|
38
104
|
export function hasCredentials() {
|
|
39
105
|
return hasCredentialsFor(session.providerId);
|
|
40
106
|
}
|
|
@@ -42,6 +108,14 @@ export function hasCredentials() {
|
|
|
42
108
|
// that gets migrated into the Keychain on first launch. The first access may pop a
|
|
43
109
|
// macOS permission dialog - that is expected and allowed once.
|
|
44
110
|
export async function initKeys() {
|
|
111
|
+
// Read together, so startup is not held up by one keychain read after another.
|
|
112
|
+
const ids = [...DIRECT_SERVICES.map((service) => service.id), CUSTOM_SERVICE_ID];
|
|
113
|
+
const serviceStored = await Promise.all(ids.map((id) => getKey(id)));
|
|
114
|
+
ids.forEach((id, index) => {
|
|
115
|
+
const key = serviceStored[index];
|
|
116
|
+
if (key && key.length > 0)
|
|
117
|
+
serviceKeys.set(id, key);
|
|
118
|
+
});
|
|
45
119
|
const storedZai = await getKey('zai');
|
|
46
120
|
if (storedZai && storedZai.length > 0) {
|
|
47
121
|
zaiKey = storedZai;
|
|
@@ -57,10 +131,10 @@ export async function initKeys() {
|
|
|
57
131
|
const moved = await setKey('openrouter', envKey);
|
|
58
132
|
if (moved) {
|
|
59
133
|
removeEnvFile();
|
|
60
|
-
session.addNotice('Your
|
|
134
|
+
session.addNotice('Your key was moved from a local file into your Mac keychain, and the file was removed.');
|
|
61
135
|
}
|
|
62
136
|
else {
|
|
63
|
-
session.addNotice('The Mac keychain was not reachable, so the
|
|
137
|
+
session.addNotice('The Mac keychain was not reachable, so the key is being read from a local file for now. Type /keys to store it securely.');
|
|
64
138
|
}
|
|
65
139
|
resolvedKey = envKey;
|
|
66
140
|
keySource = moved ? 'keychain' : 'env';
|
|
@@ -126,12 +200,27 @@ export function getActiveProvider() {
|
|
|
126
200
|
}
|
|
127
201
|
if (session.providerId === 'zai') {
|
|
128
202
|
if (!zaiKey) {
|
|
129
|
-
throw new Error('No Z.ai key
|
|
203
|
+
throw new Error('No Z.ai key yet. Add one with /keys.');
|
|
130
204
|
}
|
|
131
205
|
return createZaiProvider(zaiKey);
|
|
132
206
|
}
|
|
207
|
+
if (session.providerId === CUSTOM_SERVICE_ID) {
|
|
208
|
+
const saved = getCustomService();
|
|
209
|
+
const key = serviceKeys.get(CUSTOM_SERVICE_ID);
|
|
210
|
+
if (!saved || !key)
|
|
211
|
+
throw new Error('No key yet for the other service. Add one with /model.');
|
|
212
|
+
return createCustomProvider(saved.baseURL, key);
|
|
213
|
+
}
|
|
214
|
+
if (isDirectService(session.providerId)) {
|
|
215
|
+
const key = serviceKeys.get(session.providerId);
|
|
216
|
+
if (!key)
|
|
217
|
+
throw new Error(`No ${directService(session.providerId).label} key yet. Add one with /keys.`);
|
|
218
|
+
// Prices for the cost estimate; loads once, and never fails.
|
|
219
|
+
void loadCatalogue();
|
|
220
|
+
return createDirectProvider(session.providerId, key);
|
|
221
|
+
}
|
|
133
222
|
if (!resolvedKey) {
|
|
134
|
-
throw new Error('No OpenRouter API key
|
|
223
|
+
throw new Error('No OpenRouter API key yet. Add one with /keys.');
|
|
135
224
|
}
|
|
136
225
|
if (!active) {
|
|
137
226
|
active = createOpenRouterProvider(resolvedKey);
|
|
@@ -142,8 +231,12 @@ export function getActiveProvider() {
|
|
|
142
231
|
// the inference key only sees its own spending cap, which is labelled as such.
|
|
143
232
|
// Local Ollama has no credit balance, and failures are silent.
|
|
144
233
|
export async function refreshCredit() {
|
|
145
|
-
if (session.providerId !== 'openrouter')
|
|
234
|
+
if (session.providerId !== 'openrouter') {
|
|
235
|
+
// Direct connections: today's spending as worked out from price lists.
|
|
236
|
+
session.setTodaySpend(Math.max(session.todaySpend ?? 0, estimatedToday()));
|
|
146
237
|
return;
|
|
238
|
+
}
|
|
239
|
+
void refreshTodaySpend();
|
|
147
240
|
const managementKey = await getKey('openrouter-management');
|
|
148
241
|
if (managementKey) {
|
|
149
242
|
const info = await fetchCreditInfo(managementKey);
|
|
@@ -159,6 +252,23 @@ export async function refreshCredit() {
|
|
|
159
252
|
session.setCredit(info.used, info.limit, info.remaining, false);
|
|
160
253
|
}
|
|
161
254
|
}
|
|
255
|
+
// Today's spend on the key Jeeves uses, in the user's local calendar day.
|
|
256
|
+
async function refreshTodaySpend() {
|
|
257
|
+
if (!resolvedKey)
|
|
258
|
+
return;
|
|
259
|
+
const usage = await fetchKeyUsage(resolvedKey);
|
|
260
|
+
if (usage === null)
|
|
261
|
+
return;
|
|
262
|
+
const reading = nextSpendReading(getSpendReading(), usage, new Date());
|
|
263
|
+
setSpendReading(reading);
|
|
264
|
+
// Never lower than the live figure: the key's total can lag a request or two behind.
|
|
265
|
+
session.setTodaySpend(Math.max(session.todaySpend ?? 0, spentToday(reading) + estimatedToday()));
|
|
266
|
+
}
|
|
267
|
+
// Today's spending on direct connections, worked out from price lists.
|
|
268
|
+
export function estimatedToday(now = new Date()) {
|
|
269
|
+
const saved = getEstimatedSpend();
|
|
270
|
+
return saved && saved.date === localDate(now) ? saved.amount : 0;
|
|
271
|
+
}
|
|
162
272
|
// Which providers have a key in the OS credential store.
|
|
163
273
|
export async function storedKeyProviders() {
|
|
164
274
|
return listProviders();
|