@novaqore/atom 0.0.1

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,371 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import NovaQoreAI from "@novaqore/ai";
4
+ import { SERVICE_FILE } from "../utils/paths.js";
5
+ import { tools, executeTool } from "../tools/index.js";
6
+ import { buildSystemPrompt } from "../utils/system-prompt.js";
7
+ import { ChatInput } from "../components/Chat/ChatInput.js";
8
+ import { ConfirmDangerousCommand } from "../components/Chat/ConfirmDangerousCommand.js";
9
+ import { Header } from "../components/Header.js";
10
+ import { renderMarkdown } from "../utils/markdown.js";
11
+
12
+ export function ChatScreen({ version, unhinged }) {
13
+ const [nq] = useState(() => new NovaQoreAI(SERVICE_FILE));
14
+ const [messages, setMessages] = useState([]);
15
+ const [input, setInput] = useState("");
16
+ const [streaming, setStreaming] = useState("");
17
+ const [streamingTools, setStreamingTools] = useState([]);
18
+ const [sending, setSending] = useState(false);
19
+ const [error, setError] = useState(null);
20
+ const [confirm, setConfirm] = useState(null);
21
+ const [usage, setUsage] = useState({ prompt: 0, completion: 0, total: 0 });
22
+ const [cwd, setCwd] = useState(process.cwd());
23
+ const [running, setRunning] = useState(null);
24
+ const [durations, setDurations] = useState({});
25
+ const stopRef = useRef(null);
26
+
27
+ const toApiMessage = (m) => {
28
+ if (m.role === "tool") {
29
+ return {
30
+ role: "tool",
31
+ tool_call_id: m.tool_call_id,
32
+ content: m.content,
33
+ };
34
+ }
35
+ return m;
36
+ };
37
+
38
+ useEffect(() => {
39
+ if (!running) return;
40
+ const id = setInterval(() => {
41
+ setRunning((r) => (r ? { ...r, elapsed: Date.now() - r.start } : null));
42
+ }, 100);
43
+ return () => clearInterval(id);
44
+ }, [running?.id]);
45
+
46
+ useInput((_input, key) => {
47
+ if (key.escape && sending && stopRef.current) {
48
+ const stop = stopRef.current;
49
+ stopRef.current = null;
50
+ stop();
51
+ }
52
+ });
53
+
54
+ const askConfirm = (command) =>
55
+ new Promise((resolve) => setConfirm({ command, resolve }));
56
+
57
+ const handleSubmit = async (text) => {
58
+ if (!text.trim() || sending) return;
59
+
60
+ let convo = [...messages, { role: "user", content: text }];
61
+ setMessages(convo);
62
+ setInput("");
63
+ setSending(true);
64
+ setError(null);
65
+
66
+ let acc = "";
67
+ let toolCalls = [];
68
+
69
+ try {
70
+ while (true) {
71
+ setStreaming("");
72
+ setStreamingTools([]);
73
+
74
+ const { stream, stop } = await nq.chat(
75
+ [
76
+ { role: "system", content: buildSystemPrompt() },
77
+ ...convo.map(toApiMessage),
78
+ ],
79
+ { stream: true, tools }
80
+ );
81
+ stopRef.current = stop;
82
+
83
+ acc = "";
84
+ toolCalls = [];
85
+ for await (const chunk of stream) {
86
+ if (chunk.timings) {
87
+ const t = chunk.timings;
88
+ const promptTokens = (t.cache_n || 0) + (t.prompt_n || 0);
89
+ const completionTokens = t.predicted_n || 0;
90
+ setUsage((prev) => ({
91
+ prompt: prev.prompt + promptTokens,
92
+ completion: prev.completion + completionTokens,
93
+ total: prev.total + promptTokens + completionTokens,
94
+ }));
95
+ }
96
+ const delta = chunk.choices[0]?.delta;
97
+ if (delta?.content) {
98
+ acc += delta.content;
99
+ setStreaming(acc);
100
+ }
101
+ if (delta?.tool_calls) {
102
+ for (const tc of delta.tool_calls) {
103
+ const i = tc.index ?? 0;
104
+ if (!toolCalls[i]) {
105
+ toolCalls[i] = {
106
+ id: tc.id || "",
107
+ type: "function",
108
+ function: {
109
+ name: tc.function?.name || "",
110
+ arguments: tc.function?.arguments || "",
111
+ },
112
+ };
113
+ } else {
114
+ if (tc.id) toolCalls[i].id = tc.id;
115
+ if (tc.function?.name)
116
+ toolCalls[i].function.name += tc.function.name;
117
+ if (tc.function?.arguments)
118
+ toolCalls[i].function.arguments += tc.function.arguments;
119
+ }
120
+ }
121
+ setStreamingTools([...toolCalls]);
122
+ }
123
+ }
124
+
125
+ const assistantMsg = { role: "assistant", content: acc };
126
+ if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
127
+ convo = [...convo, assistantMsg];
128
+ setMessages(convo);
129
+ setStreaming("");
130
+ setStreamingTools([]);
131
+
132
+ if (toolCalls.length === 0) break;
133
+
134
+ for (const tc of toolCalls) {
135
+ const start = Date.now();
136
+ setRunning({ id: tc.id, start, elapsed: 0 });
137
+ const result = await executeTool(
138
+ tc.function.name,
139
+ tc.function.arguments,
140
+ { askConfirm, unhinged, cwd, setCwd }
141
+ );
142
+ const duration = Date.now() - start;
143
+ setRunning(null);
144
+ setDurations((prev) => ({ ...prev, [tc.id]: duration }));
145
+ convo = [
146
+ ...convo,
147
+ {
148
+ role: "tool",
149
+ tool_call_id: tc.id,
150
+ content: result,
151
+ },
152
+ ];
153
+ setMessages(convo);
154
+ }
155
+ }
156
+ setSending(false);
157
+ } catch (err) {
158
+ const aborted =
159
+ err.name === "AbortError" ||
160
+ err.code === "ABORT_ERR" ||
161
+ /aborted/i.test(err.message || "");
162
+ if (aborted) {
163
+ if (acc.trim() || toolCalls.length > 0) {
164
+ const partial = { role: "assistant", content: acc };
165
+ if (toolCalls.length > 0) partial.tool_calls = toolCalls;
166
+ setMessages((prev) => [...prev, partial]);
167
+ }
168
+ setStreaming("");
169
+ setStreamingTools([]);
170
+ } else {
171
+ setError(
172
+ `${err.message} | ${err.cause?.code || ""} ${err.cause?.message || ""}`
173
+ );
174
+ }
175
+ setSending(false);
176
+ } finally {
177
+ stopRef.current = null;
178
+ }
179
+ };
180
+
181
+ const formatToolName = (name) =>
182
+ name.charAt(0).toUpperCase() + name.slice(1);
183
+
184
+ const formatToolArgs = (rawArgs) => {
185
+ try {
186
+ const parsed = JSON.parse(rawArgs || "{}");
187
+ const keys = Object.keys(parsed);
188
+ if (keys.length === 1) return String(parsed[keys[0]]);
189
+ return keys.map((k) => `${k}=${JSON.stringify(parsed[k])}`).join(", ");
190
+ } catch {
191
+ return rawArgs || "";
192
+ }
193
+ };
194
+
195
+ const renderToolCall = (tc, key) =>
196
+ React.createElement(
197
+ Box,
198
+ { key },
199
+ React.createElement(
200
+ Text,
201
+ { bold: true, color: "yellow" },
202
+ formatToolName(tc.function.name)
203
+ ),
204
+ React.createElement(Text, { dimColor: true }, "("),
205
+ React.createElement(Text, null, formatToolArgs(tc.function.arguments)),
206
+ React.createElement(Text, { dimColor: true }, ")")
207
+ );
208
+
209
+ const MAX_OUTPUT_LINES = 2;
210
+
211
+ const formatMs = (ms) =>
212
+ ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
213
+
214
+ const renderToolResult = (msg, key) => {
215
+ const content = (msg.content || "").trimEnd();
216
+ const ms = durations[msg.tool_call_id];
217
+ const dur = ms ? ` · ${formatMs(ms)}` : "";
218
+ if (!content) {
219
+ return React.createElement(
220
+ Box,
221
+ { key, paddingLeft: 2, marginBottom: 1 },
222
+ React.createElement(
223
+ Text,
224
+ { dimColor: true },
225
+ `⎿ (No output)${dur}`
226
+ )
227
+ );
228
+ }
229
+ const lines = content.split("\n");
230
+ const visible = lines.slice(0, MAX_OUTPUT_LINES);
231
+ const overflow = lines.length - MAX_OUTPUT_LINES;
232
+
233
+ const children = visible.map((line, idx) => {
234
+ const isLastShown = idx === visible.length - 1 && overflow === 0;
235
+ const suffix = isLastShown ? dur : "";
236
+ return React.createElement(
237
+ Text,
238
+ { key: `line-${idx}`, dimColor: true },
239
+ idx === 0 ? `⎿ ${line}${suffix}` : ` ${line}${suffix}`
240
+ );
241
+ });
242
+ if (overflow > 0) {
243
+ children.push(
244
+ React.createElement(
245
+ Text,
246
+ { key: "overflow", dimColor: true },
247
+ ` … +${overflow} lines${dur}`
248
+ )
249
+ );
250
+ }
251
+
252
+ return React.createElement(
253
+ Box,
254
+ { key, flexDirection: "column", paddingLeft: 2, marginBottom: 1 },
255
+ children
256
+ );
257
+ };
258
+
259
+ return React.createElement(
260
+ Box,
261
+ { flexDirection: "column" },
262
+ React.createElement(Header, { key: "header", version, unhinged }),
263
+ ...messages.flatMap((msg, i) => {
264
+ if (msg.role === "user") {
265
+ return [
266
+ React.createElement(
267
+ Box,
268
+ { key: `m-${i}`, marginBottom: 1 },
269
+ React.createElement(Text, { bold: true, color: "white" }, "You: "),
270
+ React.createElement(Text, { dimColor: true }, msg.content)
271
+ ),
272
+ ];
273
+ }
274
+ if (msg.role === "assistant") {
275
+ const nodes = [];
276
+ if (msg.content) {
277
+ const rendered = renderMarkdown(msg.content);
278
+ const nlIdx = rendered.indexOf("\n");
279
+ const firstLine = nlIdx === -1 ? rendered : rendered.slice(0, nlIdx);
280
+ const rest = nlIdx === -1 ? "" : rendered.slice(nlIdx + 1);
281
+ nodes.push(
282
+ React.createElement(
283
+ Box,
284
+ {
285
+ key: `m-${i}`,
286
+ marginBottom: 1,
287
+ flexDirection: "column",
288
+ },
289
+ React.createElement(
290
+ Box,
291
+ null,
292
+ React.createElement(
293
+ Text,
294
+ { bold: true, color: "cyan" },
295
+ "Atom: "
296
+ ),
297
+ React.createElement(Text, null, firstLine)
298
+ ),
299
+ rest ? React.createElement(Text, null, rest) : null
300
+ )
301
+ );
302
+ }
303
+ if (msg.tool_calls) {
304
+ for (let j = 0; j < msg.tool_calls.length; j++) {
305
+ nodes.push(renderToolCall(msg.tool_calls[j], `m-${i}-tc-${j}`));
306
+ }
307
+ }
308
+ return nodes;
309
+ }
310
+ if (msg.role === "tool") {
311
+ return [renderToolResult(msg, `m-${i}`)];
312
+ }
313
+ return [];
314
+ }),
315
+ streaming &&
316
+ (() => {
317
+ const rendered = renderMarkdown(streaming, { streaming: true });
318
+ const nlIdx = rendered.indexOf("\n");
319
+ const firstLine = nlIdx === -1 ? rendered : rendered.slice(0, nlIdx);
320
+ const rest = nlIdx === -1 ? "" : rendered.slice(nlIdx + 1);
321
+ return React.createElement(
322
+ Box,
323
+ { key: "streaming", flexDirection: "column" },
324
+ React.createElement(
325
+ Box,
326
+ null,
327
+ React.createElement(
328
+ Text,
329
+ { color: "cyan", bold: true },
330
+ "Atom: "
331
+ ),
332
+ React.createElement(Text, null, firstLine)
333
+ ),
334
+ rest ? React.createElement(Text, null, rest) : null
335
+ );
336
+ })(),
337
+ ...streamingTools.map((tc, j) => renderToolCall(tc, `streaming-tc-${j}`)),
338
+ running &&
339
+ React.createElement(
340
+ Box,
341
+ { key: "running", paddingLeft: 2, marginBottom: 1 },
342
+ React.createElement(
343
+ Text,
344
+ { dimColor: true },
345
+ `⎿ running ${formatMs(running.elapsed)}`
346
+ )
347
+ ),
348
+ error &&
349
+ React.createElement(Text, { key: "error", color: "red" }, `Error: ${error}`),
350
+ confirm
351
+ ? React.createElement(ConfirmDangerousCommand, {
352
+ key: "confirm",
353
+ command: confirm.command,
354
+ onAnswer: (value) => {
355
+ const { resolve } = confirm;
356
+ setConfirm(null);
357
+ resolve(value);
358
+ },
359
+ })
360
+ : React.createElement(ChatInput, {
361
+ key: "input",
362
+ value: input,
363
+ onChange: setInput,
364
+ onSubmit: handleSubmit,
365
+ cwd,
366
+ totalTokens: usage.total,
367
+ loading: sending && !streaming && streamingTools.length === 0,
368
+ disabled: sending,
369
+ })
370
+ );
371
+ }
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+ import { Box, Text, useApp } from "ink";
3
+ import SelectInput from "ink-select-input";
4
+ import { Header } from "../components/Header.js";
5
+ import { HOSTNAME, USERNAME, IS_ROOT } from "../utils/system.js";
6
+
7
+ function YellowIndicator({ isSelected }) {
8
+ return React.createElement(
9
+ Box,
10
+ { marginRight: 1 },
11
+ isSelected
12
+ ? React.createElement(Text, { color: "yellow" }, "❯")
13
+ : React.createElement(Text, null, " ")
14
+ );
15
+ }
16
+
17
+ function YellowItem({ isSelected, label }) {
18
+ const text = React.createElement(
19
+ Text,
20
+ isSelected ? { color: "yellow" } : null,
21
+ label
22
+ );
23
+ if (label === "Exit") {
24
+ return React.createElement(
25
+ Box,
26
+ { flexDirection: "column" },
27
+ React.createElement(Text, { dimColor: true }, "──────"),
28
+ text
29
+ );
30
+ }
31
+ return text;
32
+ }
33
+
34
+ export function MainMenuScreen({
35
+ version,
36
+ unhinged,
37
+ onboarded,
38
+ onChat,
39
+ onGetStarted,
40
+ onSystem,
41
+ onReset,
42
+ }) {
43
+ const { exit } = useApp();
44
+
45
+ const items = onboarded
46
+ ? [
47
+ { label: "Chat", value: "chat" },
48
+ { label: "System", value: "system" },
49
+ { label: "Reset", value: "reset" },
50
+ { label: "Exit", value: "exit" },
51
+ ]
52
+ : [
53
+ { label: "Get Started", value: "start" },
54
+ { label: "System", value: "system" },
55
+ { label: "Exit", value: "exit" },
56
+ ];
57
+
58
+ const handleSelect = (item) => {
59
+ if (item.value === "chat") onChat();
60
+ if (item.value === "start") onGetStarted();
61
+ if (item.value === "system") onSystem();
62
+ if (item.value === "reset") onReset();
63
+ if (item.value === "exit") exit();
64
+ };
65
+
66
+ return React.createElement(
67
+ Box,
68
+ { flexDirection: "column", paddingBottom: 1 },
69
+ React.createElement(Header, { version, unhinged }),
70
+ React.createElement(
71
+ Box,
72
+ { paddingX: 1, marginTop: 1, flexDirection: "column" },
73
+ React.createElement(
74
+ Text,
75
+ null,
76
+ "Welcome. Atom will run as ",
77
+ React.createElement(
78
+ Text,
79
+ { color: IS_ROOT ? "red" : "cyan", bold: true },
80
+ USERNAME
81
+ ),
82
+ " on the ",
83
+ React.createElement(
84
+ Text,
85
+ { color: "cyan", bold: true },
86
+ HOSTNAME
87
+ ),
88
+ " system, with full access to read, write, and execute anything here."
89
+ ),
90
+ React.createElement(
91
+ Text,
92
+ null,
93
+ "This is experimental, so use it at your own risk and have fun."
94
+ )
95
+ ),
96
+ React.createElement(
97
+ Box,
98
+ { marginTop: 1, paddingX: 1 },
99
+ React.createElement(SelectInput, {
100
+ items,
101
+ onSelect: handleSelect,
102
+ indicatorComponent: YellowIndicator,
103
+ itemComponent: YellowItem,
104
+ })
105
+ )
106
+ );
107
+ }
@@ -0,0 +1,166 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { Box, Text, useApp, useStdin } from "ink";
3
+ import { writeFile, mkdir } from "node:fs/promises";
4
+ import { ATOM_DIR, SERVICE_FILE } from "../utils/paths.js";
5
+ import { Header } from "../components/Header.js";
6
+
7
+ const STEPS = [
8
+ { key: "uid", name: "UID", label: "Enter your UID" },
9
+ { key: "keyId", name: "Key ID", label: "Enter your Key ID" },
10
+ { key: "quantumKey", name: "Quantum Key", label: "Enter your Quantum Key" },
11
+ ];
12
+
13
+ function PasteInput({ onSubmit, onCancel, onExit }) {
14
+ const { stdin, setRawMode } = useStdin();
15
+ const [value, setValue] = useState("");
16
+ const valueRef = useRef("");
17
+
18
+ useEffect(() => {
19
+ setRawMode(true);
20
+ const handler = (data) => {
21
+ const str = data.toString();
22
+
23
+ if (str === "\x03") {
24
+ onExit();
25
+ return;
26
+ }
27
+ if (str === "\x1b") {
28
+ onCancel();
29
+ return;
30
+ }
31
+
32
+ const newlineIdx = str.search(/\r|\n/);
33
+ if (newlineIdx >= 0) {
34
+ const before = str.slice(0, newlineIdx).replace(/[\x00-\x1f\x7f]/g, "");
35
+ valueRef.current += before;
36
+ onSubmit(valueRef.current);
37
+ return;
38
+ }
39
+ if (str === "\x7f" || str === "\b") {
40
+ valueRef.current = valueRef.current.slice(0, -1);
41
+ setValue(valueRef.current);
42
+ return;
43
+ }
44
+ const cleaned = str.replace(/[\x00-\x1f\x7f]/g, "");
45
+ if (cleaned) {
46
+ valueRef.current += cleaned;
47
+ setValue(valueRef.current);
48
+ }
49
+ };
50
+ stdin.on("data", handler);
51
+ return () => {
52
+ stdin.off("data", handler);
53
+ setRawMode(false);
54
+ };
55
+ }, [onSubmit, onCancel, onExit, stdin, setRawMode]);
56
+
57
+ return React.createElement(Text, null, "•".repeat(value.length));
58
+ }
59
+
60
+ export function OnboardingScreen({ version, unhinged, onComplete, onBack }) {
61
+ const { exit } = useApp();
62
+ const [step, setStep] = useState(0);
63
+ const [values, setValues] = useState({});
64
+ const [saved, setSaved] = useState(false);
65
+
66
+ useEffect(() => {
67
+ if (step < STEPS.length || saved) return;
68
+ setSaved(true);
69
+ mkdir(ATOM_DIR, { recursive: true, mode: 0o700 })
70
+ .then(() =>
71
+ writeFile(SERVICE_FILE, JSON.stringify(values, null, 2), { mode: 0o600 })
72
+ )
73
+ .then(() => onComplete());
74
+ }, [step, saved, values, onComplete]);
75
+
76
+ const isComplete = step >= STEPS.length;
77
+
78
+ return React.createElement(
79
+ Box,
80
+ { flexDirection: "column", paddingBottom: 1 },
81
+ React.createElement(Header, { version, unhinged }),
82
+ React.createElement(
83
+ Box,
84
+ { marginTop: 1, paddingX: 1, flexDirection: "column" },
85
+ React.createElement(
86
+ Box,
87
+ null,
88
+ React.createElement(Text, null, "Sign up at "),
89
+ React.createElement(Text, { color: "cyan" }, "https://novaqore.ai"),
90
+ React.createElement(Text, null, " and generate your keys at "),
91
+ React.createElement(
92
+ Text,
93
+ { color: "cyan" },
94
+ "https://chat.novaqore.ai/keys"
95
+ ),
96
+ React.createElement(Text, null, ", then paste each value below.")
97
+ ),
98
+ React.createElement(
99
+ Text,
100
+ { dimColor: true },
101
+ "Keys are stored locally in ~/.atom/."
102
+ ),
103
+ React.createElement(
104
+ Box,
105
+ { marginTop: 1, flexDirection: "column" },
106
+ STEPS.map((s) => {
107
+ const filled = values[s.key] !== undefined;
108
+ return React.createElement(
109
+ Box,
110
+ { key: s.key },
111
+ filled
112
+ ? React.createElement(
113
+ Text,
114
+ { color: "green", bold: true },
115
+ " ✓ "
116
+ )
117
+ : React.createElement(Text, { dimColor: true }, " · "),
118
+ React.createElement(
119
+ Text,
120
+ filled ? null : { dimColor: true },
121
+ s.name
122
+ )
123
+ );
124
+ })
125
+ ),
126
+ isComplete
127
+ ? React.createElement(
128
+ Box,
129
+ { marginTop: 1 },
130
+ React.createElement(Text, { color: "green" }, "Saving...")
131
+ )
132
+ : React.createElement(
133
+ Box,
134
+ {
135
+ marginTop: 1,
136
+ borderStyle: "single",
137
+ borderTop: true,
138
+ borderBottom: true,
139
+ borderLeft: false,
140
+ borderRight: false,
141
+ borderColor: "gray",
142
+ },
143
+ React.createElement(
144
+ Text,
145
+ { color: "white" },
146
+ `${STEPS[step].label}: `
147
+ ),
148
+ React.createElement(PasteInput, {
149
+ key: step,
150
+ onSubmit: (val) => {
151
+ setValues({ ...values, [STEPS[step].key]: val });
152
+ setStep(step + 1);
153
+ },
154
+ onCancel: onBack,
155
+ onExit: exit,
156
+ })
157
+ ),
158
+ React.createElement(
159
+ Box,
160
+ { marginTop: 1 },
161
+ React.createElement(Text, { color: "yellow", bold: true }, "[Esc]"),
162
+ React.createElement(Text, { dimColor: true }, " Back")
163
+ )
164
+ )
165
+ );
166
+ }