@tianmucreations/jeeves 0.2.1 → 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.
Files changed (55) hide show
  1. package/README.md +79 -18
  2. package/bin/jeeves +8 -1
  3. package/dist/agent/auto-ids.js +66 -0
  4. package/dist/agent/auto.js +178 -0
  5. package/dist/agent/context.js +55 -13
  6. package/dist/agent/errors.js +83 -22
  7. package/dist/agent/expert-chat.js +33 -0
  8. package/dist/agent/housekeeping.js +55 -0
  9. package/dist/agent/loop.js +168 -12
  10. package/dist/agent/permissions.js +167 -0
  11. package/dist/agent/research-gate.js +267 -0
  12. package/dist/agent/review.js +135 -0
  13. package/dist/agent/spending.js +73 -0
  14. package/dist/agent/systemPrompt.js +112 -0
  15. package/dist/app.js +25 -10
  16. package/dist/checkpoints/index.js +103 -0
  17. package/dist/checkpoints/store.js +239 -0
  18. package/dist/commands/address.js +5 -0
  19. package/dist/commands/clear.js +2 -0
  20. package/dist/commands/help.js +7 -4
  21. package/dist/commands/keys.js +1 -1
  22. package/dist/commands/verbose.js +1 -1
  23. package/dist/components/AddressPrompt.js +31 -0
  24. package/dist/components/Footer.js +74 -102
  25. package/dist/components/Input.js +76 -25
  26. package/dist/components/KeysManager.js +65 -20
  27. package/dist/components/ModelPicker.js +348 -75
  28. package/dist/components/ProjectPicker.js +4 -1
  29. package/dist/components/Transcript.js +29 -14
  30. package/dist/components/input-layout.js +34 -0
  31. package/dist/components/transcript-layout.js +13 -17
  32. package/dist/index.js +25 -7
  33. package/dist/ink/AlternateScreen.js +33 -16
  34. package/dist/ink/cursor.js +18 -0
  35. package/dist/ink/mouse.js +48 -0
  36. package/dist/keys/store.js +2 -1
  37. package/dist/models/registry.js +18 -2
  38. package/dist/platform/config.js +63 -7
  39. package/dist/providers/catalogue.js +293 -0
  40. package/dist/providers/direct-services.js +65 -0
  41. package/dist/providers/direct.js +145 -0
  42. package/dist/providers/index.js +123 -13
  43. package/dist/providers/models-snapshot.js +1037 -0
  44. package/dist/providers/ollama.js +21 -4
  45. package/dist/providers/openrouter.js +39 -4
  46. package/dist/providers/step-control.js +28 -0
  47. package/dist/providers/zai.js +31 -11
  48. package/dist/state/session.js +90 -36
  49. package/dist/state/today-spend.js +26 -0
  50. package/dist/tools/index.js +118 -10
  51. package/dist/tools/runBash.js +58 -11
  52. package/dist/tools/web/htmlToText.js +32 -0
  53. package/dist/tools/web/openrouterChat.js +31 -0
  54. package/dist/tools/web/research.js +191 -0
  55. package/package.json +32 -6
@@ -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: 'direct connection' },
18
- { id: 'openai', label: 'OpenAI', description: 'direct connection' },
19
- { id: 'google', label: 'Google', description: 'direct connection' },
20
- { id: 'xai', label: 'xAI', description: 'direct connection' },
21
- { id: 'groq', label: 'Groq', description: 'direct connection' },
22
- { id: 'mistral', label: 'Mistral', description: 'direct connection' },
23
- { id: 'ollama', label: 'Ollama', description: 'local models, no key needed' },
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 API key was moved from the .env file into your Mac keychain. The .env file has been removed.');
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 .env development file is being used. Run /keys to store the key securely.');
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 found. Add one with /keys.');
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 found. Add one with /keys.');
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();