@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,393 @@
1
+ import { resolve } from 'node:path';
2
+ import { createInterface } from 'node:readline';
3
+ import { parseArgs, HELP } from './args.js';
4
+ import { VERSION } from '../version.js';
5
+ import { touchConfig, saveConfig } from '../config/index.js';
6
+ import { EventBus } from '../core/events.js';
7
+ import { buildProviders, resolveCandidates } from '../core/llm/index.js';
8
+ import { AutoRouter } from '../core/llm/router.js';
9
+ import { ContextManager } from '../core/context/context-manager.js';
10
+ import { ToolRegistry } from '../core/tools/registry.js';
11
+ import { PermissionManager } from '../core/permissions/index.js';
12
+ import { Orchestrator } from '../core/agents/orchestrator.js';
13
+ import { buildSystemPrompt } from '../../prompts/system.js';
14
+ import { TuiApp } from '../tui/app.js';
15
+ import { hasAnySecrets } from '../core/llm/secrets.js';
16
+ import { runWizard } from '../setup/wizard.js';
17
+ import { createSession, saveSession, pruneSessions } from '../session/store.js';
18
+ import { createEventLog, appendEvent, readEventLog } from '../session/event-log.js';
19
+ import { loadProjectContext } from '../core/project-context.js';
20
+ import { createSkillLoaderTool } from '../core/skill-loader.js';
21
+ const THEME_NAMES = ['tokyonight', 'catppuccin-mocha', 'catppuccin-latte', 'nord', 'gruvbox', 'monokai', 'clean-dark'];
22
+ const PROVIDER_IDS = ['orbitx', 'groq', 'gemini', 'openrouter', 'openai'];
23
+ export async function main(argv) {
24
+ let args;
25
+ try {
26
+ args = parseArgs(argv);
27
+ }
28
+ catch (err) {
29
+ console.error(err.message);
30
+ console.error(HELP);
31
+ return 1;
32
+ }
33
+ if (args.help) {
34
+ console.log(HELP);
35
+ return 0;
36
+ }
37
+ if (args.version) {
38
+ console.log(`orbit ${VERSION}`);
39
+ return 0;
40
+ }
41
+ // `orbit log <id>` — print a session's append-only event log.
42
+ if (args.log) {
43
+ const entries = readEventLog(args.log);
44
+ if (entries.length === 0) {
45
+ console.error(`No event log for session "${args.log}".`);
46
+ return 1;
47
+ }
48
+ for (const e of entries) {
49
+ const d = e.data;
50
+ const when = new Date(e.ts).toISOString();
51
+ switch (e.type) {
52
+ case 'user':
53
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[36muser\x1b[0m`);
54
+ console.log(` ${d?.text ?? ''}`);
55
+ break;
56
+ case 'tool':
57
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[33mtool\x1b[0m ${d?.name ?? '?'} → ${String(d?.status ?? '')} (${String(d?.durationMs ?? '')}ms)`);
58
+ break;
59
+ case 'diff': {
60
+ const path = d?.path;
61
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[32mdiff\x1b[0m ${path} +${d?.added} −${d?.removed}`);
62
+ break;
63
+ }
64
+ case 'plan_proposed':
65
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[35mplan\x1b[0m proposed for "${d?.task}"`);
66
+ break;
67
+ case 'plan_approved':
68
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[35mplan\x1b[0m approved for "${d?.task}"`);
69
+ break;
70
+ case 'plan_rejected':
71
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[35mplan\x1b[0m rejected for "${d?.task}"`);
72
+ break;
73
+ default:
74
+ console.log(`\x1b[2m${when}\x1b[0m \x1b[31m${e.type}\x1b[0m ${d?.message ?? JSON.stringify(d ?? {})}`);
75
+ }
76
+ }
77
+ return 0;
78
+ }
79
+ const cwd = resolve(args.cwd ?? process.cwd());
80
+ let cfg = touchConfig();
81
+ // CLI overrides
82
+ if (args.theme) {
83
+ const t = args.theme.toLowerCase().replace(/[ _]/g, '-');
84
+ if (THEME_NAMES.includes(t))
85
+ cfg.theme = t;
86
+ }
87
+ if (args.provider) {
88
+ const p = args.provider.toLowerCase();
89
+ if (PROVIDER_IDS.includes(p))
90
+ cfg.provider = p;
91
+ }
92
+ if (args.model) {
93
+ cfg.model.fallback = [cfg.model.primary, ...cfg.model.fallback].filter((m, i, arr) => m !== args.model && arr.indexOf(m) === i);
94
+ cfg.model.primary = args.model;
95
+ }
96
+ if (args.yolo)
97
+ cfg.permissions.mode = 'allow';
98
+ if (args.yes && !args.yolo)
99
+ cfg.permissions.mode = 'allow';
100
+ // Single-shot mode: no TTY required, prints the answer and exits.
101
+ const singleShot = Boolean(args.run || args.prompt);
102
+ if (singleShot) {
103
+ const prompt = args.prompt?.trim() || 'Summarize this project and recommend the next task.';
104
+ if (args.setup) {
105
+ const next = await runWizard(cfg, args);
106
+ if (!next)
107
+ return 130;
108
+ cfg = next;
109
+ saveConfig(cfg);
110
+ }
111
+ return await runSingleShot(cfg, cwd, prompt, args.yes || args.yolo);
112
+ }
113
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
114
+ console.error('orbit requires an interactive TTY for the TUI. Use `orbit run "<prompt>"` for a headless single-shot turn.');
115
+ return 1;
116
+ }
117
+ // First-run wizard
118
+ if (!cfg.firstRunComplete || args.setup) {
119
+ const next = await runWizard(cfg, args);
120
+ if (!next)
121
+ return 130; // aborted
122
+ cfg = next;
123
+ saveConfig(cfg);
124
+ }
125
+ // Build runtime
126
+ const bus = new EventBus();
127
+ let providers = buildProviders(cfg);
128
+ let router = await makeRouter(cfg, providers);
129
+ if (router.candidateList.length === 0) {
130
+ const hasKeys = hasAnySecrets();
131
+ const tip = hasKeys
132
+ ? 'Provider keys are present — run `orbit setup` to pick a provider/model.'
133
+ : 'No LLM provider is configured. Run `orbit setup`, set an Orbit X URL/token, or provider keys in environment variables.';
134
+ console.error(`⚠ No models available for routing.\n ${tip}\n`);
135
+ return 1;
136
+ }
137
+ // Project context: AGENTS.md conventions + .orbit/skills. Injected into the
138
+ // system prompt; skills exposed both to the model (load_project_skill) and
139
+ // the /skills command.
140
+ const project = loadProjectContext(cwd);
141
+ const systemPrompt = buildSystemPrompt(cfg, cwd, {
142
+ conventions: project.conventions,
143
+ skills: project.skills.map((s) => ({ name: s.name, summary: s.summary })),
144
+ });
145
+ if (args.orchestrate) {
146
+ const { runOrchestrateCmd } = await import('./orchestrate.js');
147
+ return runOrchestrateCmd({
148
+ config: cfg,
149
+ providers,
150
+ bus,
151
+ cwd,
152
+ maxTokensPerSecond: Math.max(0, cfg.ui.maxTokensPerSecond),
153
+ });
154
+ }
155
+ const context = new ContextManager(systemPrompt);
156
+ const registry = new ToolRegistry({ cwd, canWrite: true, shell: cfg.tools.shell });
157
+ let session = createSession(new Date().toLocaleString(), cwd);
158
+ if (args.session) {
159
+ const { loadSession } = await import('../session/store.js');
160
+ const prior = loadSession(args.session);
161
+ if (prior) {
162
+ // Resume: reuse the prior session id + history so the story continues.
163
+ for (const m of prior.messages)
164
+ context.add(m);
165
+ session = { ...prior, updatedAt: Date.now() };
166
+ console.log(`\x1b[2mResumed session ${prior.id} (${prior.messages.length} messages).\x1b[0m`);
167
+ }
168
+ else {
169
+ console.error(`No session "${args.session}" found. Run orbit log to find one.`);
170
+ return 1;
171
+ }
172
+ }
173
+ createEventLog(session.id);
174
+ registry.register(createSkillLoaderTool(project));
175
+ const permissions = new PermissionManager(cfg.permissions, {
176
+ ask: (prompt) => app.ask(prompt),
177
+ });
178
+ let agent = makeAgent(cfg, router, bus, context, registry, permissions, systemPrompt, cwd);
179
+ function rebuild() {
180
+ return rebuildPipeline(cfg, bus).then((next) => {
181
+ providers = next.providers;
182
+ router = next.router;
183
+ agent = makeAgent(cfg, router, bus, context, registry, permissions, systemPrompt, cwd);
184
+ emitRoute(bus, router, cfg);
185
+ saveConfig(cfg);
186
+ });
187
+ }
188
+ const onCommand = async (cmd) => {
189
+ switch (cmd.type) {
190
+ case 'model': {
191
+ if (!cmd.value)
192
+ return null;
193
+ cfg.model.fallback = [cfg.model.primary, ...cfg.model.fallback].filter((m, i, arr) => m !== cmd.value && arr.indexOf(m) === i);
194
+ cfg.model.primary = cmd.value;
195
+ await rebuild();
196
+ return `Model → ${cfg.model.primary}`;
197
+ }
198
+ case 'provider': {
199
+ cfg.provider = cmd.value;
200
+ await rebuild();
201
+ return `Provider → ${cfg.provider}`;
202
+ }
203
+ case 'theme':
204
+ saveConfig(cfg);
205
+ return null;
206
+ case 'yolo':
207
+ cfg.permissions.mode = 'allow';
208
+ saveConfig(cfg);
209
+ return null;
210
+ default:
211
+ return null;
212
+ }
213
+ };
214
+ // Abort the in-flight run when the user hits Escape / Ctrl-C while streaming.
215
+ bus.on('onAbortRequest', () => agent.abort());
216
+ // Audit trail: record the interesting stream into the session event log.
217
+ bus.on('onToolEnd', (call) => {
218
+ appendEvent(session.id, {
219
+ level: 'tool',
220
+ type: 'tool',
221
+ data: { name: call.name, status: call.status, durationMs: call.durationMs, argsPreview: call.args.slice(0, 200) },
222
+ });
223
+ });
224
+ bus.on('onFileDiff', (d) => {
225
+ appendEvent(session.id, { level: 'tool', type: 'diff', data: { path: d.path, added: d.added, removed: d.removed } });
226
+ });
227
+ bus.on('onPlanProposed', (plan) => {
228
+ appendEvent(session.id, { level: 'plan', type: 'plan_proposed', data: { task: plan.task, text: plan.text } });
229
+ });
230
+ bus.on('onPlanApproved', (task) => {
231
+ appendEvent(session.id, { level: 'plan', type: 'plan_approved', data: { task } });
232
+ });
233
+ bus.on('onPlanRejected', (task) => {
234
+ appendEvent(session.id, { level: 'plan', type: 'plan_rejected', data: { task } });
235
+ });
236
+ bus.on('onError', (err) => {
237
+ appendEvent(session.id, { level: 'error', type: 'error', data: { message: err.message } });
238
+ });
239
+ const app = new TuiApp({
240
+ config: cfg,
241
+ bus,
242
+ onSubmit: (text) => submitQueued(text),
243
+ onCommand,
244
+ version: VERSION,
245
+ models: router.order(),
246
+ });
247
+ app.store.skills = project.skills.map((s) => ({ name: s.name, summary: s.summary }));
248
+ const resumeMessages = session.messages.filter((m) => m.role !== 'tool');
249
+ if (resumeMessages.length > 0)
250
+ app.store.messages = resumeMessages;
251
+ function makeAgent(config, r, evBus, ctx, reg, perms, sys, wd) {
252
+ return new Orchestrator({
253
+ bus: evBus,
254
+ router: r,
255
+ permissions: perms,
256
+ systemPrompt: sys,
257
+ context: ctx,
258
+ registry: reg,
259
+ cwd: wd,
260
+ config,
261
+ maxTokensPerSecond: Math.max(0, config.ui.maxTokensPerSecond),
262
+ });
263
+ }
264
+ // Serialized submission queue
265
+ let processing = false;
266
+ const queue = [];
267
+ async function submitQueued(text) {
268
+ queue.push(text);
269
+ await drain();
270
+ }
271
+ async function drain() {
272
+ if (processing)
273
+ return;
274
+ processing = true;
275
+ while (queue.length > 0) {
276
+ const text = queue.shift();
277
+ appendEvent(session.id, { level: 'info', type: 'user', data: { text } });
278
+ try {
279
+ await agent.run(text);
280
+ }
281
+ catch (err) {
282
+ appendEvent(session.id, { level: 'error', type: 'error', data: { message: err.message } });
283
+ bus.emit('onError', err);
284
+ }
285
+ session.messages = context.history();
286
+ saveSession(session);
287
+ pruneSessions(50);
288
+ }
289
+ processing = false;
290
+ }
291
+ // Greet with route info once
292
+ emitRoute(bus, router, cfg);
293
+ app.run();
294
+ // Keep process alive; clean exit signals
295
+ return await new Promise((r) => {
296
+ process.once('SIGINT', () => {
297
+ app.destroy();
298
+ r(130);
299
+ });
300
+ process.once('SIGTERM', () => {
301
+ app.destroy();
302
+ r(143);
303
+ });
304
+ });
305
+ }
306
+ // ---------------------------------------------------------------------------
307
+ // Single-shot mode
308
+ // ---------------------------------------------------------------------------
309
+ async function runSingleShot(cfg, cwd, prompt, autoAllow) {
310
+ const bus = new EventBus();
311
+ // `orbit run` is pinned to Orbit X — the gateway's own service env holds the
312
+ // upstream keys (Groq/Gemini/OpenRouter), so no local token is needed unless
313
+ // the gateway enforces one (ORBITX_TOKEN set server-side).
314
+ const runCfg = { ...cfg, provider: 'orbitx' };
315
+ const providers = buildProviders(runCfg);
316
+ if (!providers.orbitx) {
317
+ console.error('⚠ Could not initialise the Orbit X client.');
318
+ return 1;
319
+ }
320
+ const router = await makeRouter(runCfg, providers);
321
+ const project = loadProjectContext(cwd);
322
+ const systemPrompt = buildSystemPrompt(cfg, cwd, {
323
+ conventions: project.conventions,
324
+ skills: project.skills.map((s) => ({ name: s.name, summary: s.summary })),
325
+ });
326
+ const context = new ContextManager(systemPrompt);
327
+ const registry = new ToolRegistry({ cwd, canWrite: true, shell: cfg.tools.shell });
328
+ registry.register(createSkillLoaderTool(project));
329
+ const permissionMode = autoAllow ? 'allow' : cfg.permissions.mode;
330
+ const permissions = new PermissionManager({ ...cfg.permissions, mode: permissionMode }, { ask: (q) => askConsole(q) });
331
+ bus.on('onAbortRequest', () => agent.abort());
332
+ const agent = new Orchestrator({
333
+ bus,
334
+ router,
335
+ permissions,
336
+ systemPrompt,
337
+ context,
338
+ registry,
339
+ cwd,
340
+ config: cfg,
341
+ maxTokensPerSecond: 0,
342
+ });
343
+ let output = '';
344
+ let errored = false;
345
+ bus.on('onAssistantStart', (msg) => { output = msg.content; });
346
+ bus.on('onToken', (text) => { output += text; });
347
+ bus.on('onAssistantEnd', (msg) => { if (msg.content)
348
+ output = msg.content; });
349
+ bus.on('onThinking', () => { });
350
+ bus.on('onError', (err) => { errored = true; console.error(`\n⚠ ${err.message}`); });
351
+ bus.on('onRoute', (s) => { console.error(`${s.provider}/${s.model} (${s.strategy})`); });
352
+ const res = await agent.run(prompt);
353
+ const trimmed = output.trim();
354
+ if (trimmed)
355
+ process.stdout.write(trimmed.endsWith('\n') ? trimmed : `${trimmed}\n`);
356
+ else if (!errored) {
357
+ console.error('⚠ The model returned no content. Try again, or switch models with `/model <id>`.');
358
+ }
359
+ if (res.interrupted || res.error)
360
+ return 1;
361
+ if (!trimmed)
362
+ return 1;
363
+ return res.turns > 0 ? 0 : 1;
364
+ }
365
+ function askConsole(prompt) {
366
+ return new Promise((resolve) => {
367
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
368
+ process.stderr.write(`⚠ ${prompt} (y/N) `);
369
+ rl.once('line', (line) => {
370
+ rl.close();
371
+ const ok = /^(y|yes|true|allow|sure)$/i.test(line.trim());
372
+ resolve(ok);
373
+ });
374
+ });
375
+ }
376
+ // ---------------------------------------------------------------------------
377
+ // Pipeline builders
378
+ // ---------------------------------------------------------------------------
379
+ async function makeRouter(cfg, providers) {
380
+ const candidates = await resolveCandidates(cfg, providers);
381
+ return new AutoRouter(providers, candidates, cfg.routing.strategy);
382
+ }
383
+ async function rebuildPipeline(cfg, bus) {
384
+ const providers = buildProviders(cfg);
385
+ const router = await makeRouter(cfg, providers);
386
+ return { providers, router };
387
+ }
388
+ function emitRoute(bus, router, cfg) {
389
+ const firstRoute = router.peek();
390
+ if (firstRoute) {
391
+ bus.emit('onRoute', { ...firstRoute, strategy: cfg.routing.strategy });
392
+ }
393
+ }
@@ -0,0 +1,151 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Config schema for orbit-agent.
4
+ * Provider keys are NEVER stored here — they live in env vars or the
5
+ * git-ignored keys.json (0600 perms). Read via core/llm/secrets.ts.
6
+ */
7
+ export const providerNames = ['orbitx', 'groq', 'gemini', 'openrouter', 'openai'];
8
+ export const effortLevels = ['none', 'minimal', 'low', 'medium', 'high'];
9
+ export const reasoningDefault = { enabled: true, effort: 'medium' };
10
+ export const routerStrategies = ['auto', 'failover', 'round-robin', 'pinned'];
11
+ export const permissionModes = ['ask', 'allow', 'deny'];
12
+ export const themeNames = [
13
+ 'tokyonight',
14
+ 'catppuccin-mocha',
15
+ 'catppuccin-latte',
16
+ 'nord',
17
+ 'gruvbox',
18
+ 'monokai',
19
+ 'clean-dark',
20
+ ];
21
+ export const orbitXDefaults = {
22
+ // Default public Orbit X backend (custom domain overrides at wizard time).
23
+ url: 'https://orbit-x-rfj6.onrender.com',
24
+ };
25
+ export const openAIDefaults = {
26
+ url: 'https://api.openai.com/v1',
27
+ };
28
+ export const modelDefault = {
29
+ primary: 'gemini/gemini-2.5-flash',
30
+ fallback: [
31
+ 'openrouter/qwen/qwen-2.5-coder-32b-instruct',
32
+ 'groq/meta-llama/llama-3.3-70b-versatile',
33
+ 'gemini/gemini-3.1-pro-preview',
34
+ 'openrouter/deepseek/deepseek-v4-flash',
35
+ ],
36
+ };
37
+ export const routingDefault = {
38
+ strategy: 'auto',
39
+ maxRetries: 2,
40
+ retryDelayMs: 800,
41
+ };
42
+ export const keysDefault = { envOnly: true, file: '' };
43
+ export const permissionsDefault = {
44
+ mode: 'ask',
45
+ allowCommands: [],
46
+ allowPaths: [],
47
+ denyCommands: [],
48
+ denyPaths: [],
49
+ };
50
+ export const toolsDefault = { shell: true, filesystem: true, search: true, timeoutMs: 30_000 };
51
+ export const agentDefault = {
52
+ maxIterations: 16,
53
+ maxDelegations: 6,
54
+ contextBudgetTokens: 48_000,
55
+ };
56
+ export const orchestrationDefault = {
57
+ enabled: false,
58
+ maxLeadAgents: 2,
59
+ leads: [],
60
+ };
61
+ export const runtimeDefault = {
62
+ streamTimeoutMs: 120_000,
63
+ retryBackoffMs: 800,
64
+ retryBackoffMaxMs: 8_000,
65
+ };
66
+ export const uiDefault = {
67
+ showThinking: true,
68
+ autoCollapseTools: false,
69
+ maxTokensPerSecond: 0,
70
+ spinner: 'dots',
71
+ };
72
+ export const configSchema = z.object({
73
+ version: z.number().int().default(1),
74
+ firstRunComplete: z.boolean().default(false),
75
+ theme: z.enum(themeNames).default('tokyonight'),
76
+ provider: z.enum(providerNames).default('orbitx'),
77
+ model: z.object({
78
+ primary: z.string().default(modelDefault.primary),
79
+ fallback: z.array(z.string()).default([...modelDefault.fallback]),
80
+ }).default({ ...modelDefault }),
81
+ reasoning: z.object({
82
+ enabled: z.boolean().default(true),
83
+ effort: z.enum(effortLevels).default('medium'),
84
+ }).default({ ...reasoningDefault }),
85
+ routing: z.object({
86
+ strategy: z.enum(routerStrategies).default('auto'),
87
+ maxRetries: z.number().int().min(0).max(5).default(2),
88
+ retryDelayMs: z.number().int().min(0).max(5000).default(800),
89
+ }).default({ ...routingDefault }),
90
+ orbitx: z.object({
91
+ url: z.string().url().default(orbitXDefaults.url),
92
+ token: z.string().default(''),
93
+ }).default({ url: orbitXDefaults.url, token: '' }),
94
+ keys: z.object({
95
+ envOnly: z.boolean().default(true),
96
+ file: z.string().default(''),
97
+ }).default({ ...keysDefault }),
98
+ permissions: z.object({
99
+ mode: z.enum(permissionModes).default('ask'),
100
+ allowCommands: z.array(z.string()).default([]),
101
+ allowPaths: z.array(z.string()).default([]),
102
+ denyCommands: z.array(z.string()).default([]),
103
+ denyPaths: z.array(z.string()).default([]),
104
+ }).default({ ...permissionsDefault }),
105
+ tools: z.object({
106
+ shell: z.boolean().default(true),
107
+ filesystem: z.boolean().default(true),
108
+ search: z.boolean().default(true),
109
+ timeoutMs: z.number().int().min(1000).max(300_000).default(30_000),
110
+ }).default({ ...toolsDefault }),
111
+ agent: z.object({
112
+ maxIterations: z.number().int().min(1).max(64).default(16),
113
+ maxDelegations: z.number().int().min(0).max(20).default(6),
114
+ contextBudgetTokens: z.number().int().min(4000).max(200_000).default(48_000),
115
+ }).default({ ...agentDefault }),
116
+ orchestration: z
117
+ .object({
118
+ enabled: z.boolean().default(false),
119
+ maxLeadAgents: z.number().int().min(1).max(4).default(2),
120
+ leads: z
121
+ .array(z.object({
122
+ name: z.string().min(1).max(40),
123
+ systemPrompt: z.string().default(''),
124
+ model: z
125
+ .object({
126
+ primary: z.string(),
127
+ fallback: z.array(z.string()).default([]),
128
+ })
129
+ .default({ primary: modelDefault.primary, fallback: [...modelDefault.fallback] }),
130
+ }))
131
+ .default([]),
132
+ })
133
+ .default({ ...orchestrationDefault }),
134
+ runtime: z.object({
135
+ streamTimeoutMs: z.number().int().min(5000).max(600_000).default(120_000),
136
+ retryBackoffMs: z.number().int().min(100).max(5000).default(800),
137
+ retryBackoffMaxMs: z.number().int().min(1000).max(30_000).default(8000),
138
+ }).default({ ...runtimeDefault }),
139
+ ui: z.object({
140
+ showThinking: z.boolean().default(true),
141
+ autoCollapseTools: z.boolean().default(false),
142
+ maxTokensPerSecond: z.number().int().min(0).max(1000).default(0),
143
+ spinner: z.string().default('dots'),
144
+ }).default({ ...uiDefault }),
145
+ });
146
+ export function defaultConfig() {
147
+ return configSchema.parse({});
148
+ }
149
+ export function validateConfig(raw) {
150
+ return configSchema.parse(raw ?? {});
151
+ }
@@ -0,0 +1,57 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, statSync, chmodSync, renameSync } from 'node:fs';
2
+ import { configPath, configDir } from '../utils/platform.js';
3
+ import { defaultConfig, validateConfig } from './config-schema.js';
4
+ export { defaultConfig, validateConfig } from './config-schema.js';
5
+ /**
6
+ * Config persistence. Reads/writes JSON at the platform config path.
7
+ * Writes are atomic (tmp + rename) and never chmod-writable by group.
8
+ */
9
+ function ensureConfigDir() {
10
+ mkdirSync(configDir(), { recursive: true });
11
+ }
12
+ function atomicWrite(path, data) {
13
+ ensureConfigDir();
14
+ const tmp = `${path}.tmp`;
15
+ writeFileSync(tmp, data, 'utf8');
16
+ chmodSync(tmp, 0o600);
17
+ renameSync(tmp, path);
18
+ chmodSync(path, 0o600);
19
+ }
20
+ export function loadConfig() {
21
+ try {
22
+ const raw = readFileSync(configPath(), 'utf8');
23
+ return validateConfig(JSON.parse(raw));
24
+ }
25
+ catch {
26
+ return defaultConfig();
27
+ }
28
+ }
29
+ export function saveConfig(config) {
30
+ atomicWrite(configPath(), JSON.stringify(config, null, 2));
31
+ }
32
+ export function configFileIsValid() {
33
+ try {
34
+ const raw = readFileSync(configPath(), 'utf8');
35
+ validateConfig(JSON.parse(raw));
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ export function touchConfig() {
43
+ ensureConfigDir();
44
+ const cfg = loadConfig();
45
+ if (!configFileIsValid())
46
+ saveConfig(cfg);
47
+ return cfg;
48
+ }
49
+ export function configExists() {
50
+ try {
51
+ statSync(configPath());
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }