@orbit-intelligence/orbit-agent 0.3.14 → 0.3.16

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.
@@ -1,59 +1,147 @@
1
1
  import { PROVIDER_SPECS, ORBITX_SERVE } from '../core/llm/index.js';
2
- import { describeProvidersAvailable } from '../core/llm/secrets.js';
2
+ import { describeProviderKey } from '../core/llm/secrets.js';
3
3
  import { createOllamaProvider } from '../core/llm/providers/ollama.js';
4
+ import { loadEndpoints, listEndpointModels } from '../core/llm/endpoints.js';
4
5
  export function isPickerHeader(row) {
5
6
  return row.kind === 'header';
6
7
  }
7
- export async function resolvePickerRows() {
8
+ function specLabel(id) {
9
+ return PROVIDER_SPECS.find((p) => p.id === id)?.label ?? id;
10
+ }
11
+ /** Stage 1 — provider rows (sync; no probing here). */
12
+ export function resolveProviderRows() {
8
13
  const rows = [];
9
- const seen = new Set();
10
- const avail = describeProvidersAvailable();
11
14
  for (const spec of PROVIDER_SPECS) {
12
15
  if (spec.id === 'ollama') {
13
- const ollama = createOllamaProvider();
14
- const local = await ollama.listModels();
15
- if (local.length === 0) {
16
- rows.push({ kind: 'header', label: 'Ollama (local) — server offline, run ollama serve' });
17
- continue;
18
- }
19
- rows.push({ kind: 'header', label: `Ollama (local) — ${local.length} installed` });
20
- for (const full of local) {
21
- const name = full.slice('ollama/'.length);
22
- if (seen.has(full))
23
- continue;
24
- seen.add(full);
25
- rows.push({ kind: 'item', label: name, id: full, provider: 'ollama' });
26
- }
16
+ const keyInfo = describeProviderKey('ollama');
17
+ rows.push({
18
+ kind: 'item',
19
+ providerId: 'ollama',
20
+ label: 'Ollama (local)',
21
+ hint: 'auto-detects a local server · no key required',
22
+ keyInfo: keyInfo === null ? { provider: 'ollama', available: false } : keyInfo,
23
+ });
27
24
  continue;
28
25
  }
29
26
  if (spec.id === 'orbitx') {
30
- rows.push({ kind: 'header', label: 'Orbit X (auto-routes Groq · Gemini · OpenRouter)' });
31
- for (const bare of ORBITX_SERVE) {
32
- const full = `orbitx/${bare}`;
33
- if (seen.has(full))
34
- continue;
35
- seen.add(full);
36
- rows.push({
37
- kind: 'item',
38
- label: bare === 'auto' ? 'auto (backend routes across providers)' : bare,
39
- id: full,
40
- provider: 'orbitx',
41
- });
42
- }
27
+ const keyInfo = describeProviderKey('orbitx');
28
+ rows.push({
29
+ kind: 'item',
30
+ providerId: 'orbitx',
31
+ label: 'Orbit X (auto-routes Groq · Gemini · OpenRouter)',
32
+ hint: keyInfo?.available ? `token ${keyInfo.masked} · ${keyInfo.source}` : 'token optional — gateway auto-routes',
33
+ keyInfo: keyInfo === null ? { provider: 'orbitx', available: false } : keyInfo,
34
+ });
43
35
  continue;
44
36
  }
45
- // Other providers: only when a key is configured.
46
- const available = avail.some((a) => a.provider === spec.id);
47
- if (!available)
48
- continue;
49
- rows.push({ kind: 'header', label: spec.label });
50
- for (const m of spec.models) {
51
- const full = `${spec.id}/${m.id}`;
37
+ const keyInfo = describeProviderKey(spec.id);
38
+ const needsKey = spec.requiresSecret && !keyInfo?.available;
39
+ rows.push({
40
+ kind: 'item',
41
+ providerId: spec.id,
42
+ label: spec.label,
43
+ hint: needsKey
44
+ ? 'no key — press Enter to add'
45
+ : keyInfo
46
+ ? `key ${keyInfo.masked} · ${keyInfo.source}${keyInfo.label ? ` · ${keyInfo.label}` : ''}`
47
+ : 'no key required',
48
+ needsKey,
49
+ keyInfo: keyInfo ?? null,
50
+ });
51
+ }
52
+ // Named custom OpenAI-compatible endpoints.
53
+ const endpoints = loadEndpoints();
54
+ rows.push({ kind: 'header', label: 'Custom endpoints (OpenAI-compatible)' });
55
+ for (const ep of endpoints) {
56
+ rows.push({
57
+ kind: 'item',
58
+ providerId: `custom:${ep.name}`,
59
+ label: ep.name,
60
+ hint: `${ep.baseUrl}${ep.key ? ' · key set' : ' · no key'}`,
61
+ keyInfo: { provider: ep.name, available: true, source: 'file' },
62
+ });
63
+ }
64
+ rows.push({
65
+ kind: 'item',
66
+ providerId: '__add_custom__',
67
+ label: '+ add a custom endpoint',
68
+ hint: 'LM Studio · vLLM · any OpenAI-compatible server',
69
+ keyInfo: null,
70
+ });
71
+ return rows;
72
+ }
73
+ /** Stage 2 — model rows for a provider. */
74
+ export async function resolveModelRows(providerId, liveModels) {
75
+ const rows = [];
76
+ if (providerId.startsWith('custom:')) {
77
+ const name = providerId.slice('custom:'.length);
78
+ const ep = loadEndpoints().find((e) => e.name === name);
79
+ if (!ep) {
80
+ rows.push({ kind: 'header', label: 'unknown custom endpoint' });
81
+ return rows;
82
+ }
83
+ rows.push({ kind: 'header', label: `${name} — ${ep.baseUrl}` });
84
+ const live = await listEndpointModels(name);
85
+ const saved = (ep.models ?? []).filter(Boolean);
86
+ const merged = [...new Set([...live, ...saved])];
87
+ if (merged.length === 0) {
88
+ rows.push({
89
+ kind: 'header',
90
+ label: 'no models found — type a model id below (or start the server)',
91
+ });
92
+ }
93
+ else {
94
+ for (const m of merged) {
95
+ rows.push({ kind: 'item', label: m, id: `${name}/${m}`, provider: `custom:${name}` });
96
+ }
97
+ }
98
+ return rows;
99
+ }
100
+ if (providerId === 'ollama') {
101
+ const local = await createOllamaProvider().listModels();
102
+ if (local.length === 0) {
103
+ rows.push({ kind: 'header', label: 'Ollama server offline — run `ollama serve`' });
104
+ return rows;
105
+ }
106
+ rows.push({ kind: 'header', label: `Ollama (local) — ${local.length} installed` });
107
+ for (const full of local) {
108
+ rows.push({ kind: 'item', label: full.slice('ollama/'.length), id: full, provider: 'ollama' });
109
+ }
110
+ return rows;
111
+ }
112
+ if (providerId === 'orbitx') {
113
+ rows.push({ kind: 'header', label: 'Orbit X (auto-routes Groq · Gemini · OpenRouter)' });
114
+ // Prefer the LIVE routable list so offered == routable (kills "No model
115
+ // matches"); fall back to the serve table only when no orbitx candidates.
116
+ const live = liveModels.filter((m) => m.startsWith('orbitx/'));
117
+ const serve = live.length > 0 ? live : ORBITX_SERVE.map((b) => `orbitx/${b}`);
118
+ const seen = new Set();
119
+ for (const full of serve) {
52
120
  if (seen.has(full))
53
121
  continue;
54
122
  seen.add(full);
55
- rows.push({ kind: 'item', label: m.label ?? m.id, id: full, provider: spec.id });
123
+ const bare = full.slice('orbitx/'.length);
124
+ rows.push({
125
+ kind: 'item',
126
+ label: bare === 'auto' ? 'auto (backend routes across providers)' : bare,
127
+ id: full,
128
+ provider: 'orbitx',
129
+ });
56
130
  }
131
+ return rows;
132
+ }
133
+ // Catalog provider.
134
+ const spec = PROVIDER_SPECS.find((p) => p.id === providerId);
135
+ if (!spec) {
136
+ rows.push({ kind: 'header', label: `unknown provider ${providerId}` });
137
+ return rows;
138
+ }
139
+ rows.push({ kind: 'header', label: spec.label });
140
+ for (const m of spec.models) {
141
+ rows.push({ kind: 'item', label: m.label ?? m.id, id: `${spec.id}/${m.id}`, provider: spec.id });
142
+ }
143
+ if (spec.supportsCustom) {
144
+ rows.push({ kind: 'header', label: '— or type a custom model id below' });
57
145
  }
58
146
  return rows;
59
147
  }
@@ -10,6 +10,8 @@ const SLASH_COMMANDS = [
10
10
  { cmd: '/model', desc: '[id] select model' },
11
11
  { cmd: '/provider', desc: '<id> switch provider' },
12
12
  { cmd: '/theme', desc: '<name> switch theme' },
13
+ { cmd: '/compact', desc: 'summarize old context to free budget' },
14
+ { cmd: '/remember', desc: '<note> save a fact to MEMORY.md' },
13
15
  { cmd: '/plan', desc: '<task> propose a plan (approve with /run)' },
14
16
  { cmd: '/run', desc: 'execute the proposed plan' },
15
17
  { cmd: '/skills', desc: 'list project skills' },
@@ -32,6 +34,8 @@ export class AppStore {
32
34
  pendingAsk = null;
33
35
  inputTokens = 0;
34
36
  outputTokens = 0;
37
+ /** Estimated context size after the last compaction (tokens). */
38
+ contextTokens = null;
35
39
  scrollOffset = 0;
36
40
  cancelArmed = false;
37
41
  greeted = false;
@@ -39,6 +43,12 @@ export class AppStore {
39
43
  cwd = '';
40
44
  models = [];
41
45
  modelPicker = { open: false, index: 0 };
46
+ /** Which stage of the /model overlay is showing rows: providers or models. */
47
+ pickStage = 'providers';
48
+ /** Provider whose model list is showing in the model stage. */
49
+ pickProviderProviderId = null;
50
+ /** Inline key/custom-endpoint entry overlay (two-stage picker). */
51
+ keyEntry = null;
42
52
  /** Grouped rows for the /model overlay (see picker.ts). */
43
53
  pickRows = [];
44
54
  agents = new Map();
@@ -42,6 +42,9 @@ export function configPath() {
42
42
  export function keysPath() {
43
43
  return join(configDir(), 'keys.json');
44
44
  }
45
+ export function endpointsPath() {
46
+ return join(configDir(), 'endpoints.json');
47
+ }
45
48
  export function historyPath() {
46
49
  return join(dataDir(), 'history.json');
47
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbit-intelligence/orbit-agent",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "orbit — a premium pure-TypeScript coding agent for Termux and desktop terminals, powered by Orbit X.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",