@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.
- package/LICENSE +16 -0
- package/README.md +23 -0
- package/bin/orbit +26 -0
- package/dist/prompts/system.js +80 -0
- package/dist/src/cli/args.js +145 -0
- package/dist/src/cli/orchestrate.js +100 -0
- package/dist/src/cli/run.js +393 -0
- package/dist/src/config/config-schema.js +151 -0
- package/dist/src/config/index.js +57 -0
- package/dist/src/core/agent/agent-loop.js +402 -0
- package/dist/src/core/agents/delegate.js +120 -0
- package/dist/src/core/agents/orchestrator.js +58 -0
- package/dist/src/core/agents/prompts.js +82 -0
- package/dist/src/core/agents/types.js +1 -0
- package/dist/src/core/context/context-manager.js +167 -0
- package/dist/src/core/events.js +23 -0
- package/dist/src/core/llm/http.js +207 -0
- package/dist/src/core/llm/index.js +93 -0
- package/dist/src/core/llm/models.js +228 -0
- package/dist/src/core/llm/providers/gemini.js +211 -0
- package/dist/src/core/llm/providers/openai-compat.js +31 -0
- package/dist/src/core/llm/router.js +125 -0
- package/dist/src/core/llm/secrets.js +121 -0
- package/dist/src/core/llm/types.js +10 -0
- package/dist/src/core/orchestration/dispatcher.js +74 -0
- package/dist/src/core/orchestration/messenger.js +139 -0
- package/dist/src/core/orchestration/roles.js +129 -0
- package/dist/src/core/orchestration/runtime.js +122 -0
- package/dist/src/core/orchestration/session.js +204 -0
- package/dist/src/core/orchestration/shared-context.js +88 -0
- package/dist/src/core/orchestration/tools.js +187 -0
- package/dist/src/core/orchestration/types.js +3 -0
- package/dist/src/core/permissions/index.js +58 -0
- package/dist/src/core/project-context.js +115 -0
- package/dist/src/core/skill-loader.js +31 -0
- package/dist/src/core/tools/edit.js +142 -0
- package/dist/src/core/tools/filesystem.js +203 -0
- package/dist/src/core/tools/git.js +138 -0
- package/dist/src/core/tools/registry.js +73 -0
- package/dist/src/core/tools/search.js +90 -0
- package/dist/src/core/tools/shell.js +65 -0
- package/dist/src/core/tools/types.js +6 -0
- package/dist/src/core/types.js +3 -0
- package/dist/src/index.js +11 -0
- package/dist/src/session/event-log.js +55 -0
- package/dist/src/session/store.js +76 -0
- package/dist/src/setup/wizard.js +401 -0
- package/dist/src/tui/InkApp.js +67 -0
- package/dist/src/tui/ansi.js +142 -0
- package/dist/src/tui/app.js +768 -0
- package/dist/src/tui/colors.js +13 -0
- package/dist/src/tui/components/AgentDock.js +46 -0
- package/dist/src/tui/components/Composer.js +35 -0
- package/dist/src/tui/components/Header.js +23 -0
- package/dist/src/tui/components/ModelPicker.js +23 -0
- package/dist/src/tui/components/PermissionModal.js +29 -0
- package/dist/src/tui/components/SlashMenu.js +15 -0
- package/dist/src/tui/components/StatusLine.js +27 -0
- package/dist/src/tui/components/Transcript.js +31 -0
- package/dist/src/tui/components/WorkingStatus.js +29 -0
- package/dist/src/tui/components/input.js +246 -0
- package/dist/src/tui/components/markdown.js +384 -0
- package/dist/src/tui/components/message.js +105 -0
- package/dist/src/tui/context.js +8 -0
- package/dist/src/tui/geometry.js +40 -0
- package/dist/src/tui/renderer.js +116 -0
- package/dist/src/tui/rows.js +247 -0
- package/dist/src/tui/scheduler.js +32 -0
- package/dist/src/tui/store.js +127 -0
- package/dist/src/tui/style.js +151 -0
- package/dist/src/tui/term.js +309 -0
- package/dist/src/tui/text.js +104 -0
- package/dist/src/tui/themes/index.js +15 -0
- package/dist/src/tui/themes/palettes.js +137 -0
- package/dist/src/tui/themes/types.js +1 -0
- package/dist/src/utils/diff.js +161 -0
- package/dist/src/utils/platform.js +71 -0
- package/dist/src/utils/signals.js +26 -0
- package/dist/src/version.js +4 -0
- 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,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
|
+
}
|