openaxies 0.1.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.
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import App from '../src/App.js';
7
+
8
+ const h = React.createElement;
9
+
10
+ const debugBuffer = [];
11
+
12
+ function sandboxWrite(chunk) {
13
+ const str = typeof chunk === 'string' ? chunk : String(chunk);
14
+ const trimmed = str.trim();
15
+ if (trimmed.length > 0) {
16
+ debugBuffer.push(trimmed);
17
+ }
18
+ return true;
19
+ }
20
+
21
+ const consoleKeys = ['log', 'error', 'warn'];
22
+ for (let i = 0; i < consoleKeys.length; i++) {
23
+ const key = consoleKeys[i];
24
+ const original = console[key];
25
+ console[key] = function sandboxedConsole() {
26
+ const args = [];
27
+ for (let j = 0; j < arguments.length; j++) {
28
+ args.push(arguments[j]);
29
+ }
30
+ sandboxWrite(args.join(' '));
31
+ return original.apply(console, args);
32
+ };
33
+ }
34
+
35
+ const { waitUntilExit } = render(h(App, {}));
36
+
37
+ function cleanup() {
38
+ if (debugBuffer.length > 0) {
39
+ const logPath = path.join(process.cwd(), 'openaxies-debug.log');
40
+ try {
41
+ fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
42
+ } catch (_) {
43
+ }
44
+ }
45
+ process.exit(0);
46
+ }
47
+
48
+ process.on('SIGINT', cleanup);
49
+ process.on('SIGTERM', cleanup);
50
+ process.on('exit', function () {
51
+ if (debugBuffer.length > 0) {
52
+ const logPath = path.join(process.cwd(), 'openaxies-debug.log');
53
+ try {
54
+ fs.writeFileSync(logPath, debugBuffer.join('\n') + '\n');
55
+ } catch (_) {
56
+ }
57
+ }
58
+ });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "openaxies",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "openaxies": "./bin/openaxis.js",
8
+ "openaxis": "./bin/openaxis.js"
9
+ },
10
+ "dependencies": {
11
+ "@langchain/core": "^1.0.0",
12
+ "@langchain/langgraph": "^1.0.0",
13
+ "ink": "^5.0.0",
14
+ "ink-big-text": "^2.0.0",
15
+ "ink-select-input": "^6.2.0",
16
+ "ink-text-input": "^6.0.0",
17
+ "react": "^18.3.1"
18
+ }
19
+ }
package/src/App.js ADDED
@@ -0,0 +1,391 @@
1
+ import React from 'react';
2
+ import { Box, useInput, useStdout } from 'ink';
3
+ import { hex } from './config/theme.js';
4
+ import { getCommands } from './config/commands.js';
5
+ import { getModelById, DEFAULT_MODEL_ID } from './config/models.js';
6
+ import { callModel } from './providers/index.js';
7
+ import { createBrandHeader, BRAND_HEIGHT } from './components/BrandHeader.js';
8
+ import { createRouterBar, ROUTER_HEIGHT } from './components/RouterBar.js';
9
+ import { createChatViewport } from './components/ChatViewport.js';
10
+ import { createSlashOverlay, SLASH_OVERLAY_HEIGHT } from './components/SlashOverlay.js';
11
+ import { createComposerDock, DOCK_HEIGHT } from './components/ComposerDock.js';
12
+
13
+ const h = React.createElement;
14
+
15
+ export default AppRoot;
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function buildHistory(messages, newText) {
22
+ const history = [];
23
+ for (let i = 0; i < messages.length; i++) {
24
+ const m = messages[i];
25
+ if (m === null || m === undefined || typeof m !== 'object') {
26
+ continue;
27
+ }
28
+ if (m.role === 'user' || m.role === 'assistant') {
29
+ const content = typeof m.content === 'string' ? m.content : '';
30
+ history.push({ role: m.role, content: content });
31
+ }
32
+ }
33
+ if (typeof newText === 'string' && newText.length > 0) {
34
+ history.push({ role: 'user', content: newText });
35
+ }
36
+ return history;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Root component
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function AppRoot() {
44
+ const { stdout } = useStdout();
45
+ const rows = stdout.rows || 24;
46
+ const cols = stdout.columns || 80;
47
+
48
+ // --- UI state ---
49
+ const [inputBuffer, setInputBuffer] = React.useState('');
50
+ const [showOverlay, setShowOverlay] = React.useState(false);
51
+ const [overlayIndex, setOverlayIndex] = React.useState(0); // cursor inside overlay
52
+
53
+ // --- Chat state ---
54
+ const [messages, setMessages] = React.useState([]);
55
+ const [activeModel, setActiveModel] = React.useState(DEFAULT_MODEL_ID);
56
+ const [streamText, setStreamText] = React.useState('');
57
+ const [streamingActive, setStreamingActive] = React.useState(false);
58
+ const [scrollOffset, setScrollOffset] = React.useState(0);
59
+
60
+ const commands = getCommands();
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Strict row budget
64
+ // BrandHeader : BRAND_HEIGHT (7)
65
+ // RouterBar : ROUTER_HEIGHT (2)
66
+ // SlashOverlay : SLASH_OVERLAY_HEIGHT (6) — conditional
67
+ // ComposerDock : DOCK_HEIGHT (3)
68
+ // ChatViewport : rows - all fixed rows (flexGrow fills remainder)
69
+ // ---------------------------------------------------------------------------
70
+ const overlayH = showOverlay ? SLASH_OVERLAY_HEIGHT : 0;
71
+ const fixedH = BRAND_HEIGHT + ROUTER_HEIGHT + overlayH + DOCK_HEIGHT;
72
+ const availLines = Math.max(1, rows - fixedH);
73
+
74
+ const abortRef = React.useRef(null);
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Input handlers
78
+ // ---------------------------------------------------------------------------
79
+
80
+ function handleInputChange(value) {
81
+ const safeValue = typeof value === 'string' ? value : '';
82
+ setInputBuffer(safeValue);
83
+ // Open overlay on first '/' character
84
+ if (safeValue.length === 1 && safeValue[0] === '/' && showOverlay === false) {
85
+ setShowOverlay(true);
86
+ setOverlayIndex(0);
87
+ }
88
+ // Close overlay if buffer no longer starts with '/'
89
+ if (showOverlay === true && (safeValue.length === 0 || safeValue[0] !== '/')) {
90
+ setShowOverlay(false);
91
+ }
92
+ }
93
+
94
+ // onSubmit for TextInput — actual submit goes through useInput Enter handler.
95
+ function handleInputSubmit() {
96
+ return;
97
+ }
98
+
99
+ function handleSlashSelect(item) {
100
+ if (item === null || item === undefined) {
101
+ return;
102
+ }
103
+ const trigger = item.value;
104
+ if (typeof trigger !== 'string') {
105
+ return;
106
+ }
107
+
108
+ if (trigger === '/exit') {
109
+ process.exit(0);
110
+ }
111
+
112
+ if (trigger === '/clear') {
113
+ setMessages([]);
114
+ setScrollOffset(0);
115
+ }
116
+
117
+ if (trigger === '/model') {
118
+ // Toggle between the two local models
119
+ setActiveModel(function (prev) {
120
+ if (prev === 'openaxis/openaxis-flash') {
121
+ return 'openaxis/openaxis-heavy';
122
+ }
123
+ return 'openaxis/openaxis-flash';
124
+ });
125
+ }
126
+
127
+ if (trigger === '/help') {
128
+ setScrollOffset(0);
129
+ setMessages(function (prev) {
130
+ return prev.concat([{
131
+ role: 'assistant',
132
+ content:
133
+ '**Available commands:**\n' +
134
+ '- `/help` — Show this help\n' +
135
+ '- `/clear` — Clear the conversation\n' +
136
+ '- `/model` — Toggle between OpenAxies Flash and Heavy\n' +
137
+ '- `/exit` — Quit the application\n\n' +
138
+ 'Type your message and press **Enter** to chat.',
139
+ id: 'help-' + Date.now(),
140
+ }]);
141
+ });
142
+ }
143
+
144
+ // Always close & clear after any selection
145
+ setInputBuffer('');
146
+ setShowOverlay(false);
147
+ setOverlayIndex(0);
148
+ }
149
+
150
+ async function handleSubmit(text) {
151
+ const safeText = typeof text === 'string' ? text : '';
152
+ if (safeText.length === 0 || streamingActive === true) {
153
+ return;
154
+ }
155
+
156
+ const msgId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
157
+
158
+ setStreamingActive(true);
159
+ setScrollOffset(0);
160
+ setMessages(function (prev) {
161
+ return prev.concat([{ role: 'user', content: safeText, id: msgId }]);
162
+ });
163
+ setInputBuffer('');
164
+ setStreamText('\u2022\u2022\u2022'); // animated placeholder
165
+
166
+ const modelInfo = getModelById(activeModel);
167
+ if (modelInfo === null || modelInfo === undefined) {
168
+ setStreamingActive(false);
169
+ setStreamText('');
170
+ setScrollOffset(0);
171
+ setMessages(function (prev) {
172
+ return prev.concat([{
173
+ role: 'assistant',
174
+ content: 'No model selected. Use `/model` to switch.',
175
+ id: 'err-' + Date.now(),
176
+ }]);
177
+ });
178
+ return;
179
+ }
180
+
181
+ const history = buildHistory(messages, safeText);
182
+ const abortController = new AbortController();
183
+ abortRef.current = abortController;
184
+
185
+ try {
186
+ const gen = callModel(modelInfo, history, abortController.signal);
187
+ let accumulated = '';
188
+ let thinkingId = null;
189
+ const thinkingLines = [];
190
+
191
+ for await (const event of gen) {
192
+ if (abortController.signal.aborted === true) {
193
+ break;
194
+ }
195
+ if (event.type === 'thinking') {
196
+ if (thinkingId === null) {
197
+ thinkingId = 'think-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
198
+ }
199
+ thinkingLines.push(typeof event.content === 'string' ? event.content : '');
200
+ const content = thinkingLines.join('\n');
201
+ setMessages(function (prev) {
202
+ let replaced = false;
203
+ const next = [];
204
+ for (let i = 0; i < prev.length; i++) {
205
+ const item = prev[i];
206
+ if (item !== null && item !== undefined && item.id === thinkingId) {
207
+ next.push({ role: 'thinking', content: content, id: thinkingId });
208
+ replaced = true;
209
+ } else {
210
+ next.push(item);
211
+ }
212
+ }
213
+ if (replaced === false) {
214
+ next.push({ role: 'thinking', content: content, id: thinkingId });
215
+ }
216
+ return next;
217
+ });
218
+ setScrollOffset(0);
219
+ }
220
+ if (event.type === 'token') {
221
+ const content = typeof event.content === 'string' ? event.content : '';
222
+ for (let i = 0; i < content.length; i++) {
223
+ accumulated = accumulated + content[i];
224
+ setScrollOffset(0);
225
+ setStreamText(accumulated);
226
+ await new Promise(function (resolve) {
227
+ setTimeout(resolve, 0);
228
+ });
229
+ }
230
+ }
231
+ if (event.type === 'done') {
232
+ break;
233
+ }
234
+ if (event.type === 'timeout') {
235
+ if (accumulated.length > 0) {
236
+ accumulated = accumulated + '\n\n*[Response incomplete \u2014 stream timed out]*';
237
+ }
238
+ setStreamText(accumulated);
239
+ break;
240
+ }
241
+ }
242
+
243
+ setStreamingActive(false);
244
+ setStreamText('');
245
+
246
+ if (accumulated.length > 0) {
247
+ setMessages(function (prev) {
248
+ return prev.concat([{
249
+ role: 'assistant',
250
+ content: accumulated,
251
+ id: 'resp-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
252
+ }]);
253
+ });
254
+ }
255
+ } catch (err) {
256
+ setStreamingActive(false);
257
+ setStreamText('');
258
+ setScrollOffset(0);
259
+ const errMsg = (err !== null && err !== undefined)
260
+ ? (err.message || String(err))
261
+ : 'Unknown error';
262
+ setMessages(function (prev) {
263
+ return prev.concat([{
264
+ role: 'assistant',
265
+ content:
266
+ '**Connection Error:** ' + errMsg +
267
+ '\n\nThe Space may be cold-starting (model download can take 30\u2013120 s). Please try again.',
268
+ id: 'err-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
269
+ }]);
270
+ });
271
+ }
272
+ }
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Keyboard router
276
+ //
277
+ // Overlay mode: ↑↓ move cursor · Enter selects · Esc dismisses
278
+ // All other keys are absorbed (TextInput has focus=false)
279
+ //
280
+ // Normal mode: Enter submits · Ctrl+C abort/quit
281
+ // ---------------------------------------------------------------------------
282
+ useInput(function handleInput(input, key) {
283
+
284
+ // ── Overlay branch ──────────────────────────────────────────────────────
285
+ if (showOverlay === true) {
286
+ if (key.escape === true) {
287
+ setShowOverlay(false);
288
+ setInputBuffer('');
289
+ setOverlayIndex(0);
290
+ return;
291
+ }
292
+
293
+ if (key.upArrow === true) {
294
+ setOverlayIndex(function (prev) {
295
+ return prev > 0 ? prev - 1 : commands.length - 1; // wrap around top
296
+ });
297
+ return;
298
+ }
299
+
300
+ if (key.downArrow === true) {
301
+ setOverlayIndex(function (prev) {
302
+ return prev < commands.length - 1 ? prev + 1 : 0; // wrap around bottom
303
+ });
304
+ return;
305
+ }
306
+
307
+ if (key.return === true) {
308
+ const selectedCmd = commands[overlayIndex];
309
+ if (selectedCmd !== null && selectedCmd !== undefined) {
310
+ handleSlashSelect({ value: selectedCmd.trigger });
311
+ }
312
+ return;
313
+ }
314
+
315
+ // Absorb everything else while overlay is open
316
+ return;
317
+ }
318
+
319
+ // ── Normal branch ────────────────────────────────────────────────────────
320
+ if (key.return === true) {
321
+ const safeValue = typeof inputBuffer === 'string' ? inputBuffer : '';
322
+ if (safeValue.length > 0 && streamingActive === false) {
323
+ handleSubmit(safeValue);
324
+ }
325
+ return;
326
+ }
327
+
328
+ if (key.pageUp === true || (key.ctrl === true && key.upArrow === true)) {
329
+ const pageSize = Math.max(1, availLines - 2);
330
+ setScrollOffset(function (prev) {
331
+ return prev + pageSize;
332
+ });
333
+ return;
334
+ }
335
+
336
+ if (key.pageDown === true || (key.ctrl === true && key.downArrow === true)) {
337
+ const pageSize = Math.max(1, availLines - 2);
338
+ setScrollOffset(function (prev) {
339
+ return Math.max(0, prev - pageSize);
340
+ });
341
+ return;
342
+ }
343
+
344
+ if (key.ctrl === true && (input === 'c' || input === 'C')) {
345
+ if (abortRef.current !== null) {
346
+ try {
347
+ abortRef.current.abort();
348
+ } catch (_) { }
349
+ abortRef.current = null;
350
+ setStreamingActive(false);
351
+ setStreamText('');
352
+ }
353
+ process.exit(0);
354
+ }
355
+ });
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Render
359
+ // ---------------------------------------------------------------------------
360
+
361
+ const header = createBrandHeader(cols);
362
+ const routerBar = createRouterBar(activeModel, cols);
363
+ const viewport = createChatViewport(messages, streamText, availLines, cols, scrollOffset);
364
+ const slashOverlay = showOverlay === true
365
+ ? createSlashOverlay(commands, overlayIndex)
366
+ : null;
367
+ const dock = createComposerDock({
368
+ value: inputBuffer,
369
+ onChange: handleInputChange,
370
+ onSubmit: handleInputSubmit,
371
+ inputActive: showOverlay === false, // yield focus to overlay navigation
372
+ isStreaming: streamingActive,
373
+ terminalWidth: cols,
374
+ });
375
+
376
+ // Strict column layout — total height === rows (no scroll, no overflow).
377
+ // ComposerDock has flexShrink:0 so it is always pinned at bottom.
378
+ return h(Box, {
379
+ flexDirection: 'column',
380
+ width: '100%',
381
+ height: rows,
382
+ backgroundColor: hex.black,
383
+ overflow: 'hidden',
384
+ },
385
+ header, // 7 rows
386
+ routerBar, // 2 rows
387
+ viewport, // dynamic — flexGrow fills all remaining rows
388
+ slashOverlay, // 6 rows (null when closed)
389
+ dock // 3 rows — flexShrink:0, always at bottom
390
+ );
391
+ }
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { hex } from '../config/theme.js';
4
+
5
+ const h = React.createElement;
6
+
7
+ export const BRAND_HEIGHT = 5;
8
+
9
+ function checkColumns(cols) {
10
+ if (typeof cols !== 'number' || cols < 20) {
11
+ return 80;
12
+ }
13
+ return Math.max(20, Math.floor(cols));
14
+ }
15
+
16
+ function padLine(content, innerWidth) {
17
+ const safeContent = typeof content === 'string' ? content : '';
18
+ const clipped = safeContent.length > innerWidth
19
+ ? safeContent.slice(0, innerWidth)
20
+ : safeContent;
21
+ return clipped + ' '.repeat(Math.max(0, innerWidth - clipped.length));
22
+ }
23
+
24
+ export function createBrandHeader(cols) {
25
+ const width = checkColumns(cols);
26
+ const innerWidth = Math.max(1, width - 2);
27
+ const rule = '\u2500'.repeat(innerWidth);
28
+ const statusLine = ' [STATUS: ACTIVE] [ENGINE: 32B-OLLAMA] [WEBSEARCH: READY]';
29
+
30
+ return h(Box, {
31
+ flexDirection: 'column',
32
+ width: '100%',
33
+ height: BRAND_HEIGHT,
34
+ backgroundColor: hex.black,
35
+ flexShrink: 0,
36
+ overflow: 'hidden',
37
+ },
38
+ h(Box, { height: 1 },
39
+ h(Text, { color: hex.primary, bold: true }, '\u250C' + rule + '\u2510')
40
+ ),
41
+ h(Box, { height: 1 },
42
+ h(Text, { color: hex.primary, bold: true },
43
+ '\u2502' + padLine(' OPEN', innerWidth) + '\u2502'
44
+ )
45
+ ),
46
+ h(Box, { height: 1 },
47
+ h(Text, { color: hex.primary, bold: true },
48
+ '\u2502' + padLine(' AXIES', innerWidth) + '\u2502'
49
+ )
50
+ ),
51
+ h(Box, { height: 1 },
52
+ h(Text, { color: hex.greenOnline, bold: true },
53
+ '\u2502' + padLine(statusLine, innerWidth) + '\u2502'
54
+ )
55
+ ),
56
+ h(Box, { height: 1 },
57
+ h(Text, { color: hex.primary, bold: true }, '\u2514' + rule + '\u2518')
58
+ ),
59
+ );
60
+ }