@orbit-intelligence/orbit-agent 0.3.12

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 (80) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +23 -0
  3. package/bin/orbit +26 -0
  4. package/dist/prompts/system.js +80 -0
  5. package/dist/src/cli/args.js +145 -0
  6. package/dist/src/cli/orchestrate.js +100 -0
  7. package/dist/src/cli/run.js +393 -0
  8. package/dist/src/config/config-schema.js +151 -0
  9. package/dist/src/config/index.js +57 -0
  10. package/dist/src/core/agent/agent-loop.js +402 -0
  11. package/dist/src/core/agents/delegate.js +120 -0
  12. package/dist/src/core/agents/orchestrator.js +58 -0
  13. package/dist/src/core/agents/prompts.js +82 -0
  14. package/dist/src/core/agents/types.js +1 -0
  15. package/dist/src/core/context/context-manager.js +167 -0
  16. package/dist/src/core/events.js +23 -0
  17. package/dist/src/core/llm/http.js +207 -0
  18. package/dist/src/core/llm/index.js +93 -0
  19. package/dist/src/core/llm/models.js +228 -0
  20. package/dist/src/core/llm/providers/gemini.js +211 -0
  21. package/dist/src/core/llm/providers/openai-compat.js +31 -0
  22. package/dist/src/core/llm/router.js +125 -0
  23. package/dist/src/core/llm/secrets.js +121 -0
  24. package/dist/src/core/llm/types.js +10 -0
  25. package/dist/src/core/orchestration/dispatcher.js +74 -0
  26. package/dist/src/core/orchestration/messenger.js +139 -0
  27. package/dist/src/core/orchestration/roles.js +129 -0
  28. package/dist/src/core/orchestration/runtime.js +122 -0
  29. package/dist/src/core/orchestration/session.js +204 -0
  30. package/dist/src/core/orchestration/shared-context.js +88 -0
  31. package/dist/src/core/orchestration/tools.js +187 -0
  32. package/dist/src/core/orchestration/types.js +3 -0
  33. package/dist/src/core/permissions/index.js +58 -0
  34. package/dist/src/core/project-context.js +115 -0
  35. package/dist/src/core/skill-loader.js +31 -0
  36. package/dist/src/core/tools/edit.js +142 -0
  37. package/dist/src/core/tools/filesystem.js +203 -0
  38. package/dist/src/core/tools/git.js +138 -0
  39. package/dist/src/core/tools/registry.js +73 -0
  40. package/dist/src/core/tools/search.js +90 -0
  41. package/dist/src/core/tools/shell.js +65 -0
  42. package/dist/src/core/tools/types.js +6 -0
  43. package/dist/src/core/types.js +3 -0
  44. package/dist/src/index.js +11 -0
  45. package/dist/src/session/event-log.js +55 -0
  46. package/dist/src/session/store.js +76 -0
  47. package/dist/src/setup/wizard.js +401 -0
  48. package/dist/src/tui/InkApp.js +67 -0
  49. package/dist/src/tui/ansi.js +142 -0
  50. package/dist/src/tui/app.js +768 -0
  51. package/dist/src/tui/colors.js +13 -0
  52. package/dist/src/tui/components/AgentDock.js +46 -0
  53. package/dist/src/tui/components/Composer.js +35 -0
  54. package/dist/src/tui/components/Header.js +23 -0
  55. package/dist/src/tui/components/ModelPicker.js +23 -0
  56. package/dist/src/tui/components/PermissionModal.js +29 -0
  57. package/dist/src/tui/components/SlashMenu.js +15 -0
  58. package/dist/src/tui/components/StatusLine.js +27 -0
  59. package/dist/src/tui/components/Transcript.js +31 -0
  60. package/dist/src/tui/components/WorkingStatus.js +29 -0
  61. package/dist/src/tui/components/input.js +246 -0
  62. package/dist/src/tui/components/markdown.js +384 -0
  63. package/dist/src/tui/components/message.js +105 -0
  64. package/dist/src/tui/context.js +8 -0
  65. package/dist/src/tui/geometry.js +40 -0
  66. package/dist/src/tui/renderer.js +116 -0
  67. package/dist/src/tui/rows.js +247 -0
  68. package/dist/src/tui/scheduler.js +32 -0
  69. package/dist/src/tui/store.js +127 -0
  70. package/dist/src/tui/style.js +151 -0
  71. package/dist/src/tui/term.js +309 -0
  72. package/dist/src/tui/text.js +104 -0
  73. package/dist/src/tui/themes/index.js +15 -0
  74. package/dist/src/tui/themes/palettes.js +137 -0
  75. package/dist/src/tui/themes/types.js +1 -0
  76. package/dist/src/utils/diff.js +161 -0
  77. package/dist/src/utils/platform.js +71 -0
  78. package/dist/src/utils/signals.js +26 -0
  79. package/dist/src/version.js +4 -0
  80. package/package.json +71 -0
@@ -0,0 +1,65 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { toolError } from './types.js';
3
+ import { defaultShell } from '../../utils/platform.js';
4
+ /** Run a shell command and capture output. Uses the platform shell. */
5
+ export function createShellTool(opts) {
6
+ const shell = opts.shell ?? defaultShell();
7
+ return {
8
+ name: 'run_shell',
9
+ description: 'Run a shell command in the working directory. Use for anything outside the filesystem tools: installs, test runs, git, builds, network.',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ command: { type: 'string', description: 'The shell command to run.' },
14
+ cwd: { type: 'string', description: 'Working directory (default: current).' },
15
+ timeoutMs: { type: 'number', description: 'Timeout in ms (default 30_000).', default: 30000 },
16
+ },
17
+ required: ['command'],
18
+ },
19
+ async run(args, ctx) {
20
+ const command = String(args.command ?? '');
21
+ const cwd = String(args.cwd ?? ctx.cwd);
22
+ const timeoutMs = Math.min(Number(args.timeoutMs ?? 30_000), 120_000);
23
+ if (!command.trim())
24
+ return toolError('empty command');
25
+ return new Promise((resolve) => {
26
+ const child = spawn(command, { shell, cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
27
+ let stdout = '';
28
+ let stderr = '';
29
+ let settled = false;
30
+ const settle = (result) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ clearTimeout(timer);
35
+ ctx.signal?.removeEventListener('abort', onAbort);
36
+ resolve(result);
37
+ };
38
+ const summarize = (code, signal, timedOut) => {
39
+ const out = stdout + (stderr ? `\n${stderr}` : '');
40
+ const maxLen = 40_000;
41
+ const truncated = out.length > maxLen ? `${out.slice(0, maxLen)}\n… [truncated]` : out;
42
+ const status = timedOut
43
+ ? `(timed out after ${timeoutMs}ms)`
44
+ : signal ? `(killed by ${signal})` : `(exit ${code ?? 0})`;
45
+ const content = `${truncated}\n${status}`.trim();
46
+ const failed = timedOut || (code !== null && code !== 0);
47
+ settle(failed ? { content, isError: true } : { content });
48
+ };
49
+ const timer = setTimeout(() => {
50
+ child.kill('SIGKILL');
51
+ summarize(null, null, true);
52
+ }, timeoutMs);
53
+ const onAbort = () => {
54
+ child.kill('SIGKILL');
55
+ settle({ content: `Error: command cancelled (${ctx.signal?.reason?.message ?? 'interrupted'})`, isError: true });
56
+ };
57
+ ctx.signal?.addEventListener('abort', onAbort, { once: true });
58
+ child.stdout?.on('data', (d) => { stdout += d.toString('utf8'); });
59
+ child.stderr?.on('data', (d) => { stderr += d.toString('utf8'); });
60
+ child.on('error', (err) => settle({ content: `Error: ${err.message}`, isError: true }));
61
+ child.on('close', (code, signal) => summarize(code, signal, false));
62
+ });
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,6 @@
1
+ export function okToolOk(content) {
2
+ return { content };
3
+ }
4
+ export function toolError(message) {
5
+ return { content: `Error: ${message}`, isError: true };
6
+ }
@@ -0,0 +1,3 @@
1
+ export function newMessage(id, role, content = '') {
2
+ return { id, role, content, createdAt: Date.now() };
3
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli/run.js';
3
+ // orbit-agent entrypoint. Kept tiny: all logic lives in cli/run.ts.
4
+ main(process.argv.slice(2))
5
+ .then((code) => {
6
+ process.exitCode = code;
7
+ })
8
+ .catch((err) => {
9
+ console.error('\n\x1b[31morbital failure:\x1b[0m', err instanceof Error ? err.message : String(err));
10
+ process.exitCode = 1;
11
+ });
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, appendFileSync, readFileSync, existsSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { sessionsDir } from '../utils/platform.js';
4
+ function logFileFor(sessionId) {
5
+ return join(sessionsDir(), `${sessionId}.log`);
6
+ }
7
+ export function createEventLog(sessionId) {
8
+ try {
9
+ mkdirSync(sessionsDir(), { recursive: true });
10
+ appendFileSync(logFileFor(sessionId), '');
11
+ }
12
+ catch {
13
+ /* best-effort */
14
+ }
15
+ }
16
+ export function appendEvent(sessionId, entry) {
17
+ try {
18
+ const line = JSON.stringify({ ts: Date.now(), ...entry }) + '\n';
19
+ appendFileSync(logFileFor(sessionId), line, 'utf8');
20
+ }
21
+ catch {
22
+ /* never let logging break the agent */
23
+ }
24
+ }
25
+ export function readEventLog(sessionId) {
26
+ const p = logFileFor(sessionId);
27
+ if (!existsSync(p))
28
+ return [];
29
+ try {
30
+ const raw = readFileSync(p, 'utf8');
31
+ const out = [];
32
+ for (const line of raw.split('\n')) {
33
+ if (!line.trim())
34
+ continue;
35
+ try {
36
+ out.push(JSON.parse(line));
37
+ }
38
+ catch {
39
+ /* skip corrupt line */
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export function eventLogSize(sessionId) {
49
+ try {
50
+ return statSync(logFileFor(sessionId)).size;
51
+ }
52
+ catch {
53
+ return 0;
54
+ }
55
+ }
@@ -0,0 +1,76 @@
1
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync, statSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { sessionsDir } from '../utils/platform.js';
4
+ function ensureDir() {
5
+ mkdirSync(sessionsDir(), { recursive: true });
6
+ }
7
+ function fileFor(id) {
8
+ return join(sessionsDir(), `${id}.json`);
9
+ }
10
+ export function createSession(name, cwd) {
11
+ ensureDir();
12
+ const id = `s_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
13
+ const session = {
14
+ id,
15
+ name,
16
+ createdAt: Date.now(),
17
+ updatedAt: Date.now(),
18
+ cwd,
19
+ messages: [],
20
+ };
21
+ writeFileSync(fileFor(id), JSON.stringify(session, null, 2), 'utf8');
22
+ return session;
23
+ }
24
+ export function saveSession(session) {
25
+ ensureDir();
26
+ session.updatedAt = Date.now();
27
+ writeFileSync(fileFor(session.id), JSON.stringify(session, null, 2), 'utf8');
28
+ }
29
+ export function loadSession(id) {
30
+ try {
31
+ const raw = readFileSync(fileFor(id), 'utf8');
32
+ return JSON.parse(raw);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ export function listSessions() {
39
+ if (!existsSync(sessionsDir()))
40
+ return [];
41
+ const out = [];
42
+ for (const f of readdirSync(sessionsDir())) {
43
+ if (!f.endsWith('.json'))
44
+ continue;
45
+ try {
46
+ const raw = readFileSync(join(sessionsDir(), f), 'utf8');
47
+ out.push(JSON.parse(raw));
48
+ }
49
+ catch {
50
+ /* skip corrupt */
51
+ }
52
+ }
53
+ return out.sort((a, b) => b.updatedAt - a.updatedAt);
54
+ }
55
+ export function pruneSessions(max = 50) {
56
+ const sessions = listSessions();
57
+ if (sessions.length <= max)
58
+ return;
59
+ const toRemove = sessions.slice(max);
60
+ for (const s of toRemove) {
61
+ try {
62
+ unlinkSync(fileFor(s.id));
63
+ }
64
+ catch {
65
+ /* ignore */
66
+ }
67
+ }
68
+ }
69
+ export function sessionFileModifiedMs(id) {
70
+ try {
71
+ return statSync(fileFor(id)).mtimeMs;
72
+ }
73
+ catch {
74
+ return 0;
75
+ }
76
+ }
@@ -0,0 +1,401 @@
1
+ import * as term from '../tui/term.js';
2
+ import { VTerm, composeRow } from '../tui/renderer.js';
3
+ import { setRenderCallback, flushNow } from '../tui/scheduler.js';
4
+ import { makeTheme, THEME_NAMES } from '../tui/themes/index.js';
5
+ import { padRight } from '../tui/text.js';
6
+ import { defaultConfig } from '../config/config-schema.js';
7
+ import { PROVIDER_SPECS } from '../core/llm/index.js';
8
+ import { modelSupportsReasoning } from '../core/llm/models.js';
9
+ import { describeProvidersAvailable } from '../core/llm/secrets.js';
10
+ const PERM_OPTIONS = ['ask', 'allow', 'deny'];
11
+ const FOOTER = '[↑↓] move · [Enter] confirm · [Ctrl-C] cancel';
12
+ export async function runWizard(cfg, _args) {
13
+ const state = {
14
+ themeIdx: Math.max(0, THEME_NAMES.indexOf(cfg.theme)),
15
+ providerSelected: Math.max(0, PROVIDER_SPECS.findIndex((p) => p.id === cfg.provider)),
16
+ orbitxUrl: cfg.orbitx.url,
17
+ orbitxToken: '',
18
+ modelSelected: 0,
19
+ reasoningIdx: 0,
20
+ permSelected: Math.max(0, PERM_OPTIONS.indexOf(cfg.permissions.mode)),
21
+ step: 0,
22
+ };
23
+ let theme = makeTheme(cfg.theme);
24
+ term.setup();
25
+ try {
26
+ const [cols, rows] = term.termSize();
27
+ const vterm = new VTerm(Math.max(cols, 20), Math.max(rows, 8));
28
+ let done = false;
29
+ let finalizedAt = Infinity;
30
+ let result = null;
31
+ setRenderCallback(render);
32
+ term.onResize(() => {
33
+ const [c, h] = term.termSize();
34
+ vterm.resize(Math.max(c, 20), Math.max(h, 8));
35
+ flushNow();
36
+ });
37
+ const keyUnsub = term.onKey((key) => {
38
+ if (key.name === 'ctrl-c') {
39
+ keyUnsub();
40
+ done = true;
41
+ finalizedAt = Date.now() + 250;
42
+ result = null;
43
+ flushNow();
44
+ return;
45
+ }
46
+ handleKey(state, key);
47
+ theme = makeTheme(THEME_NAMES[Math.max(0, Math.min(state.themeIdx, THEME_NAMES.length - 1))]);
48
+ if (state.step >= 7 && !done) {
49
+ keyUnsub();
50
+ done = true;
51
+ finalizedAt = Date.now() + 900;
52
+ result = buildConfig(state, cfg);
53
+ }
54
+ flushNow();
55
+ });
56
+ function render() {
57
+ if (done)
58
+ return;
59
+ const w = vterm.w;
60
+ const h = vterm.h;
61
+ vterm.fillClear(theme);
62
+ if (state.step >= 7) {
63
+ renderDone(w, h);
64
+ return;
65
+ }
66
+ const lines = contentLines(state, theme, w);
67
+ const boxW = Math.min(64, w - 4);
68
+ const boxX = Math.max(0, Math.floor((w - boxW) / 2));
69
+ const availH = h - 2;
70
+ const body = lines.slice(0, Math.max(1, availH - 1));
71
+ vterm.setRow(1, boxRow(theme, w, boxX, boxW, '', 'top'));
72
+ for (let i = 0; i < Math.max(1, availH - 2); i++) {
73
+ const line = body[i] ?? '';
74
+ vterm.setRow(2 + i, boxRow(theme, w, boxX, boxW, padRight(line, boxW - 2, ' '), 'body'));
75
+ }
76
+ vterm.setRow(Math.min(h - 1, 2 + Math.max(1, availH - 2)), boxRow(theme, w, boxX, boxW, '', 'bottom'));
77
+ term.hideCursor();
78
+ }
79
+ function renderDone(w, h) {
80
+ const lines = contentLines(state, theme, w);
81
+ const boxW = Math.min(64, w - 4);
82
+ const boxX = Math.max(0, Math.floor((w - boxW) / 2));
83
+ const body = lines.slice(0, Math.max(1, h - 4));
84
+ const topY = Math.max(0, Math.floor(h / 2) - 1);
85
+ vterm.setRow(topY, boxRow(theme, w, boxX, boxW, '', 'top'));
86
+ for (let i = 0; i < body.length; i++) {
87
+ const line = body[i] ?? '';
88
+ vterm.setRow(topY + 1 + i, boxRow(theme, w, boxX, boxW, padRight(line, boxW - 2, ' '), 'body'));
89
+ }
90
+ vterm.setRow(topY + 1 + body.length, boxRow(theme, w, boxX, boxW, '', 'bottom'));
91
+ term.hideCursor();
92
+ }
93
+ flushNow();
94
+ return await new Promise((resolve) => {
95
+ const poll = setInterval(() => {
96
+ if (done && Date.now() >= finalizedAt) {
97
+ clearInterval(poll);
98
+ resolve(result);
99
+ }
100
+ }, 30);
101
+ void keyUnsub;
102
+ });
103
+ }
104
+ finally {
105
+ term.teardown();
106
+ }
107
+ }
108
+ function handleKey(state, key) {
109
+ switch (state.step) {
110
+ case 0: // theme
111
+ if (key.name === 'up')
112
+ state.themeIdx = (state.themeIdx - 1 + THEME_NAMES.length) % THEME_NAMES.length;
113
+ else if (key.name === 'down')
114
+ state.themeIdx = (state.themeIdx + 1) % THEME_NAMES.length;
115
+ else if (key.name === 'enter')
116
+ state.step = 1;
117
+ break;
118
+ case 1: // provider
119
+ if (key.name === 'up')
120
+ state.providerSelected = Math.max(0, state.providerSelected - 1);
121
+ else if (key.name === 'down')
122
+ state.providerSelected = Math.min(PROVIDER_SPECS.length - 1, state.providerSelected + 1);
123
+ else if (key.name === 'enter') {
124
+ const sel = PROVIDER_SPECS[state.providerSelected];
125
+ state.modelSelected = 0;
126
+ state.reasoningIdx = 0;
127
+ if (sel.id === 'orbitx')
128
+ state.step = 2;
129
+ else {
130
+ state.orbitxUrl = '';
131
+ state.orbitxToken = '';
132
+ state.step = 4;
133
+ }
134
+ }
135
+ break;
136
+ case 2: // orbitx url
137
+ if (key.name === 'char' && key.value)
138
+ state.orbitxUrl += key.value;
139
+ else if (key.name === 'backspace')
140
+ state.orbitxUrl = state.orbitxUrl.slice(0, -1);
141
+ else if (key.name === 'enter')
142
+ state.step = 3;
143
+ break;
144
+ case 3: // orbitx token
145
+ if (key.name === 'char' && key.value)
146
+ state.orbitxToken += key.value;
147
+ else if (key.name === 'backspace')
148
+ state.orbitxToken = state.orbitxToken.slice(0, -1);
149
+ else if (key.name === 'enter')
150
+ state.step = 4;
151
+ break;
152
+ case 4: // model
153
+ if (key.name === 'up')
154
+ state.modelSelected = Math.max(0, state.modelSelected - 1);
155
+ else if (key.name === 'down')
156
+ state.modelSelected++;
157
+ else if (key.name === 'enter') {
158
+ state.reasoningIdx = defaultReasoningIdx(selectedModel(state));
159
+ state.step = 5;
160
+ }
161
+ break;
162
+ case 5: { // reasoning
163
+ const options = reasoningOptions(state);
164
+ if (key.name === 'up')
165
+ state.reasoningIdx = Math.max(0, state.reasoningIdx - 1);
166
+ else if (key.name === 'down')
167
+ state.reasoningIdx = Math.min(options.length - 1, state.reasoningIdx + 1);
168
+ else if (key.name === 'enter')
169
+ state.step = 6;
170
+ break;
171
+ }
172
+ case 6: // permissions
173
+ if (key.name === 'up')
174
+ state.permSelected = Math.max(0, state.permSelected - 1);
175
+ else if (key.name === 'down')
176
+ state.permSelected = Math.min(PERM_OPTIONS.length - 1, state.permSelected + 1);
177
+ else if (key.name === 'enter')
178
+ state.step = 7;
179
+ break;
180
+ default:
181
+ break;
182
+ }
183
+ }
184
+ /** The currently selected catalog model, if any. */
185
+ function selectedModel(state) {
186
+ const spec = PROVIDER_SPECS[state.providerSelected];
187
+ if (!spec || !spec.models || spec.models.length === 0)
188
+ return null;
189
+ const m = spec.models[Math.min(state.modelSelected, spec.models.length - 1)];
190
+ return m ?? null;
191
+ }
192
+ /** Reasoning picker options for the selected model, or [] when not applicable. */
193
+ function reasoningOptions(state) {
194
+ const m = selectedModel(state);
195
+ if (!m || !modelSupportsReasoning(m))
196
+ return [];
197
+ const efforts = m.reasoning?.efforts ?? ['low', 'medium', 'high'];
198
+ const leveled = efforts.filter((e) => e !== 'none');
199
+ if (m.reasoning?.switchable === false)
200
+ return leveled;
201
+ return ['off', ...leveled];
202
+ }
203
+ function defaultReasoningIdx(m) {
204
+ if (!m || !modelSupportsReasoning(m))
205
+ return 0;
206
+ const options = reasoningOptionsFor(m);
207
+ const def = m.reasoning?.default;
208
+ if (def && def !== 'none') {
209
+ const i = options.indexOf(String(def));
210
+ if (i >= 0)
211
+ return i;
212
+ }
213
+ return options.indexOf('off') >= 0 ? options.indexOf('off') + 1 : 0;
214
+ }
215
+ function reasoningOptionsFor(m) {
216
+ const efforts = m.reasoning?.efforts ?? ['low', 'medium', 'high'];
217
+ const leveled = efforts.filter((e) => e !== 'none');
218
+ if (m.reasoning?.switchable === false)
219
+ return leveled;
220
+ return ['off', ...leveled];
221
+ }
222
+ function currentReasoning(m, st) {
223
+ if (!m || !modelSupportsReasoning(m))
224
+ return null;
225
+ const options = reasoningOptionsFor(m);
226
+ const pick = options[st.reasoningIdx] ?? 'medium';
227
+ if (pick === 'off')
228
+ return { enabled: false, effort: 'medium' };
229
+ return { enabled: true, effort: pick };
230
+ }
231
+ function contentLines(state, theme, width) {
232
+ const lines = [];
233
+ const accentSgr = theme.table.sgr(theme.styles['accent']);
234
+ const dimSgr = theme.table.sgr(theme.styles['dim']);
235
+ const infoSgr = theme.table.sgr(theme.styles['info']);
236
+ const reset = '\x1b[0m';
237
+ switch (state.step) {
238
+ case 0: {
239
+ lines.push(`${accentSgr}◆ orbit-agent${reset} ${dimSgr}setup · theme${reset}`);
240
+ lines.push('');
241
+ THEME_NAMES.forEach((name, i) => {
242
+ if (i === state.themeIdx)
243
+ lines.push(`${accentSgr}▸ ${name}${reset}`);
244
+ else
245
+ lines.push(` ${dimSgr}${name}${reset}`);
246
+ });
247
+ lines.push('');
248
+ break;
249
+ }
250
+ case 1: {
251
+ lines.push(`${accentSgr}Pick an LLM provider${reset}`);
252
+ lines.push('');
253
+ PROVIDER_SPECS.forEach((p, i) => {
254
+ const av = describeProvidersAvailable().some((a) => a.provider === p.id);
255
+ const flag = p.id === 'orbitx' ? ' free · auto-routes Groq/Gemini/OpenRouter' : av ? ' ✓ available' : ` ${dimSgr}(keys not set)${reset}`;
256
+ if (i === state.providerSelected)
257
+ lines.push(`${accentSgr}▸ ${p.label}${reset}${dimSgr}${flag}${reset}`);
258
+ else
259
+ lines.push(` ${p.label}${dimSgr}${flag}${reset}`);
260
+ });
261
+ lines.push('');
262
+ break;
263
+ }
264
+ case 2: {
265
+ lines.push(`${accentSgr}Orbit X backend URL${reset}`);
266
+ lines.push('');
267
+ lines.push(`❯ ${state.orbitxUrl || `${dimSgr}(default https://orbit-x-rfj6.onrender.com)${reset}`}`);
268
+ lines.push('');
269
+ break;
270
+ }
271
+ case 3: {
272
+ lines.push(`${accentSgr}Orbit X token${reset}`);
273
+ lines.push('');
274
+ const masked = state.orbitxToken ? '•'.repeat(Math.min(state.orbitxToken.length, 24)) : '';
275
+ lines.push(`❯ ${masked || dimSgr + '(type or paste your token — kept in keys.json, never config)' + reset}`);
276
+ lines.push('');
277
+ break;
278
+ }
279
+ case 4: {
280
+ const provider = PROVIDER_SPECS[state.providerSelected];
281
+ lines.push(`${accentSgr}Pick a model · ${provider.label}${reset}`);
282
+ lines.push('');
283
+ const models = provider.models.length > 0 ? provider.models : [];
284
+ if (models.length === 0) {
285
+ lines.push(`${dimSgr}(no catalog — will be resolved by the backend)${reset}`);
286
+ }
287
+ else {
288
+ models.forEach((m, i) => {
289
+ const display = provider.id === 'orbitx' ? m.label ?? 'auto' : m.label ?? m.id;
290
+ if (i === state.modelSelected)
291
+ lines.push(`${accentSgr}▸ ${display}${reset}`);
292
+ else
293
+ lines.push(` ${display}`);
294
+ });
295
+ }
296
+ lines.push('');
297
+ break;
298
+ }
299
+ case 5: {
300
+ const m = selectedModel(state);
301
+ const options = reasoningOptions(state);
302
+ if (options.length === 0) {
303
+ lines.push(`${infoSgr}This model has no reasoning toggle.${reset}`);
304
+ lines.push('');
305
+ lines.push(`${dimSgr}Press Enter to continue.${reset}`);
306
+ }
307
+ else {
308
+ lines.push(`${accentSgr}Thinking / reasoning · ${m?.label ?? ''}${reset}`);
309
+ lines.push('');
310
+ options.forEach((opt, i) => {
311
+ const display = opt === 'off' ? 'off (no thinking)' : `effort: ${opt}`;
312
+ if (i === state.reasoningIdx)
313
+ lines.push(`${accentSgr}▸ ${display}${reset}`);
314
+ else
315
+ lines.push(` ${display}`);
316
+ });
317
+ }
318
+ lines.push('');
319
+ break;
320
+ }
321
+ case 6: {
322
+ lines.push(`${accentSgr}Permission mode${reset}`);
323
+ lines.push('');
324
+ PERM_OPTIONS.forEach((mode, i) => {
325
+ if (i === state.permSelected)
326
+ lines.push(`${accentSgr}▸ ${mode}${reset}`);
327
+ else
328
+ lines.push(` ${mode}`);
329
+ });
330
+ lines.push('');
331
+ lines.push(dimSgr + 'ask = confirm risky commands · allow = auto-approve · deny = block all' + reset);
332
+ break;
333
+ }
334
+ case 7: {
335
+ const provider = PROVIDER_SPECS[state.providerSelected];
336
+ lines.push(`${accentSgr}✓ Setup complete${reset}`);
337
+ lines.push('');
338
+ lines.push(` Theme: ${THEME_NAMES[state.themeIdx]}`);
339
+ lines.push(` Provider: ${provider.label}`);
340
+ if (provider.id === 'orbitx')
341
+ lines.push(` Backend: ${state.orbitxUrl || '(default)'}`);
342
+ const model = selectedModel(state);
343
+ if (model && provider.id !== 'orbitx') {
344
+ lines.push(` Model: ${model.label ?? model.id}`);
345
+ const r = currentReasoning(model, state);
346
+ if (r)
347
+ lines.push(` Thinking: ${r.enabled ? r.effort : 'off'}`);
348
+ }
349
+ else {
350
+ lines.push(` Model: auto (backend routes)`);
351
+ }
352
+ lines.push(` Permissions: ${PERM_OPTIONS[state.permSelected]}`);
353
+ lines.push('');
354
+ lines.push(dimSgr + 'Keys live in environment variables or ~/.config/orbit-agent/keys.json — never config.' + reset);
355
+ break;
356
+ }
357
+ default:
358
+ lines.push('');
359
+ }
360
+ lines.push('');
361
+ lines.push(`${dimSgr}${FOOTER}${reset}`);
362
+ void width;
363
+ return lines;
364
+ }
365
+ function boxRow(theme, width, boxX, boxW, inner, kind) {
366
+ const border = theme.table.sgr(theme.styles['border']);
367
+ const reset = '\x1b[0m';
368
+ let content;
369
+ if (kind === 'top')
370
+ content = `${border}╭${'─'.repeat(Math.max(0, boxW - 2))}╮${reset}`;
371
+ else if (kind === 'bottom')
372
+ content = `${border}╰${'─'.repeat(Math.max(0, boxW - 2))}╯${reset}`;
373
+ else
374
+ content = `${border}│${reset}${padRight(inner, boxW - 2, ' ')}${border}│${reset}`;
375
+ return composeRow(theme, width, `${' '.repeat(Math.max(0, boxX))}${content}`);
376
+ }
377
+ function buildConfig(state, base) {
378
+ const spec = PROVIDER_SPECS[state.providerSelected];
379
+ const provider = spec.id;
380
+ const cfg = { ...defaultConfig(), ...base };
381
+ cfg.theme = THEME_NAMES[state.themeIdx];
382
+ cfg.provider = provider;
383
+ cfg.orbitx.url = state.orbitxUrl.trim() || 'https://orbit-x-rfj6.onrender.com';
384
+ if (state.orbitxToken.trim())
385
+ cfg.orbitx.token = state.orbitxToken.trim();
386
+ cfg.permissions.mode = PERM_OPTIONS[state.permSelected];
387
+ cfg.firstRunComplete = true;
388
+ const model = selectedModel(state);
389
+ if (provider !== 'orbitx' && model) {
390
+ cfg.model.primary = `${provider}/${model.id}`;
391
+ cfg.model.fallback = [];
392
+ const r = currentReasoning(model, state);
393
+ if (r)
394
+ cfg.reasoning = r;
395
+ }
396
+ else {
397
+ cfg.model.primary = 'gemini/gemini-2.5-flash';
398
+ cfg.model.fallback = ['openrouter/qwen/qwen-2.5-coder-32b-instruct', 'groq/llama-3.3-70b-versatile'];
399
+ }
400
+ return cfg;
401
+ }