@xingjisoft/neutrino 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.
package/dist/agents.js ADDED
@@ -0,0 +1,396 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+ const HOME = os.homedir();
6
+ function parseJsonlLines(filePath) {
7
+ let raw;
8
+ try {
9
+ raw = fs.readFileSync(filePath, "utf-8");
10
+ }
11
+ catch {
12
+ return [];
13
+ }
14
+ const out = [];
15
+ for (const line of raw.split("\n")) {
16
+ const t = line.trim();
17
+ if (!t)
18
+ continue;
19
+ try {
20
+ const v = JSON.parse(t);
21
+ if (v && typeof v === "object")
22
+ out.push(v);
23
+ }
24
+ catch {
25
+ // skip malformed lines
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+ function contentText(content) {
31
+ if (typeof content === "string")
32
+ return content;
33
+ if (Array.isArray(content)) {
34
+ return content
35
+ .map((b) => {
36
+ if (typeof b === "string")
37
+ return b;
38
+ if (b && typeof b === "object" && typeof b.text === "string") {
39
+ return b.text;
40
+ }
41
+ return "";
42
+ })
43
+ .join("");
44
+ }
45
+ return "";
46
+ }
47
+ function iso(ms) {
48
+ const n = typeof ms === "number" ? ms : typeof ms === "string" ? Number(ms) : 0;
49
+ if (!Number.isFinite(n) || n <= 0)
50
+ return "";
51
+ return new Date(n).toISOString();
52
+ }
53
+ function cleanTitle(s) {
54
+ return s
55
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
56
+ .replace(/ {2,}/g, " ")
57
+ .trim();
58
+ }
59
+ // ---------------- Claude Code ----------------
60
+ function claudeProjectDirs() {
61
+ const base = path.join(HOME, ".claude", "projects");
62
+ let entries;
63
+ try {
64
+ entries = fs.readdirSync(base, { withFileTypes: true });
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ return entries.filter((e) => e.isDirectory()).map((e) => path.join(base, e.name));
70
+ }
71
+ function claudeTitle(lines) {
72
+ for (const o of lines) {
73
+ if (o.type === "user") {
74
+ const t = contentText(o.message?.content).trim();
75
+ if (t)
76
+ return t;
77
+ }
78
+ }
79
+ return "";
80
+ }
81
+ function listClaude() {
82
+ const metas = [];
83
+ for (const dir of claudeProjectDirs()) {
84
+ let files;
85
+ try {
86
+ files = fs.readdirSync(dir);
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ for (const f of files) {
92
+ if (!f.endsWith(".jsonl"))
93
+ continue;
94
+ const fp = path.join(dir, f);
95
+ let st;
96
+ try {
97
+ st = fs.statSync(fp);
98
+ }
99
+ catch {
100
+ continue;
101
+ }
102
+ const id = f.slice(0, -".jsonl".length);
103
+ const title = cleanTitle(claudeTitle(parseJsonlLines(fp)) || id.slice(0, 8));
104
+ metas.push({
105
+ id,
106
+ title: title.slice(0, 80),
107
+ created_at: iso(st.birthtimeMs ?? st.mtimeMs),
108
+ updated_at: iso(st.mtimeMs),
109
+ model: "",
110
+ });
111
+ }
112
+ }
113
+ metas.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
114
+ return metas;
115
+ }
116
+ function loadClaude(id) {
117
+ for (const dir of claudeProjectDirs()) {
118
+ const fp = path.join(dir, `${id}.jsonl`);
119
+ if (!fs.existsSync(fp))
120
+ continue;
121
+ const out = [];
122
+ for (const o of parseJsonlLines(fp)) {
123
+ if (o.type === "user") {
124
+ const t = contentText(o.message?.content).trim();
125
+ if (t)
126
+ out.push({ role: "user", content: t });
127
+ }
128
+ else if (o.type === "assistant") {
129
+ const t = contentText(o.message?.content).trim();
130
+ if (t)
131
+ out.push({ role: "assistant", content: t });
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+ return [];
137
+ }
138
+ // ---------------- Codex ----------------
139
+ function codexIndex() {
140
+ const map = new Map();
141
+ const idx = path.join(HOME, ".codex", "session_index.jsonl");
142
+ for (const o of parseJsonlLines(idx)) {
143
+ if (typeof o.id === "string" && o.id) {
144
+ map.set(o.id, {
145
+ title: typeof o.thread_name === "string" ? o.thread_name : "",
146
+ updated_at: typeof o.updated_at === "string" ? o.updated_at : "",
147
+ });
148
+ }
149
+ }
150
+ return map;
151
+ }
152
+ function codexRolloutId(filename) {
153
+ const m = filename.match(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/);
154
+ return m ? m[1] : null;
155
+ }
156
+ function codexRolloutFiles() {
157
+ const map = new Map();
158
+ const roots = [
159
+ path.join(HOME, ".codex", "sessions"),
160
+ path.join(HOME, ".codex", "archived_sessions"),
161
+ ];
162
+ const walk = (dir) => {
163
+ let entries;
164
+ try {
165
+ entries = fs.readdirSync(dir, { withFileTypes: true });
166
+ }
167
+ catch {
168
+ return;
169
+ }
170
+ for (const e of entries) {
171
+ const p = path.join(dir, e.name);
172
+ if (e.isDirectory()) {
173
+ walk(p);
174
+ }
175
+ else if (e.name.endsWith(".jsonl")) {
176
+ const id = codexRolloutId(e.name);
177
+ if (!id)
178
+ continue;
179
+ const prev = map.get(id);
180
+ if (!prev) {
181
+ map.set(id, p);
182
+ continue;
183
+ }
184
+ let pm = 0;
185
+ let cm = 0;
186
+ try {
187
+ pm = fs.statSync(prev).mtimeMs;
188
+ }
189
+ catch { }
190
+ try {
191
+ cm = fs.statSync(p).mtimeMs;
192
+ }
193
+ catch { }
194
+ if (cm > pm)
195
+ map.set(id, p);
196
+ }
197
+ }
198
+ };
199
+ for (const r of roots)
200
+ walk(r);
201
+ return map;
202
+ }
203
+ function codexMessageText(payload) {
204
+ if (payload.role !== "user" && payload.role !== "assistant")
205
+ return "";
206
+ const content = payload.content;
207
+ if (!Array.isArray(content))
208
+ return "";
209
+ return content
210
+ .map((c) => {
211
+ if (c && typeof c === "object" && typeof c.text === "string") {
212
+ return c.text;
213
+ }
214
+ return "";
215
+ })
216
+ .join("")
217
+ .trim();
218
+ }
219
+ function listCodex() {
220
+ const index = codexIndex();
221
+ const files = codexRolloutFiles();
222
+ const metas = [];
223
+ for (const [id, fp] of files) {
224
+ const info = index.get(id);
225
+ let title = info?.title ?? "";
226
+ let updated = info?.updated_at ?? "";
227
+ if (!updated) {
228
+ try {
229
+ updated = iso(fs.statSync(fp).mtimeMs);
230
+ }
231
+ catch { }
232
+ }
233
+ if (!title) {
234
+ for (const o of parseJsonlLines(fp)) {
235
+ if (o.type === "response_item") {
236
+ const t = codexMessageText(o.payload ?? {});
237
+ if (t) {
238
+ title = t;
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ if (!title)
244
+ title = id.slice(0, 8);
245
+ }
246
+ metas.push({
247
+ id,
248
+ title: cleanTitle(title).slice(0, 80),
249
+ created_at: updated,
250
+ updated_at: updated,
251
+ model: "",
252
+ });
253
+ }
254
+ metas.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
255
+ return metas;
256
+ }
257
+ function loadCodex(id) {
258
+ const files = codexRolloutFiles();
259
+ const fp = files.get(id);
260
+ if (!fp)
261
+ return [];
262
+ const out = [];
263
+ for (const o of parseJsonlLines(fp)) {
264
+ if (o.type !== "response_item")
265
+ continue;
266
+ const payload = o.payload ?? {};
267
+ if (payload.type !== "message")
268
+ continue;
269
+ const role = payload.role;
270
+ if (role !== "user" && role !== "assistant")
271
+ continue;
272
+ const text = codexMessageText(payload);
273
+ if (text)
274
+ out.push({ role, content: text });
275
+ }
276
+ return out;
277
+ }
278
+ // ---------------- OpenCode ----------------
279
+ function opencodeDataDir() {
280
+ const candidates = [];
281
+ if (process.env.XDG_DATA_HOME) {
282
+ candidates.push(path.join(process.env.XDG_DATA_HOME, "opencode"));
283
+ }
284
+ candidates.push(path.join(HOME, ".local", "share", "opencode"));
285
+ candidates.push(path.join(HOME, "Library", "Application Support", "opencode"));
286
+ for (const c of candidates) {
287
+ if (fs.existsSync(path.join(c, "opencode.db")))
288
+ return c;
289
+ }
290
+ return null;
291
+ }
292
+ let opencodeDb = null;
293
+ function opencodeDbHandle() {
294
+ if (opencodeDb)
295
+ return opencodeDb;
296
+ const dir = opencodeDataDir();
297
+ if (!dir)
298
+ return null;
299
+ try {
300
+ process.env.NODE_NO_WARNINGS = "1";
301
+ const req = createRequire(import.meta.url);
302
+ const { DatabaseSync } = req("node:sqlite");
303
+ opencodeDb = new DatabaseSync(path.join(dir, "opencode.db"), { readOnly: true });
304
+ return opencodeDb;
305
+ }
306
+ catch {
307
+ return null;
308
+ }
309
+ }
310
+ function opencodeModelName(modelJson) {
311
+ if (typeof modelJson !== "string" || !modelJson)
312
+ return "";
313
+ try {
314
+ const m = JSON.parse(modelJson);
315
+ const id = m.id ?? "";
316
+ const pid = m.providerID ?? m.provider ?? "";
317
+ return pid && id ? `${pid}/${id}` : id;
318
+ }
319
+ catch {
320
+ return "";
321
+ }
322
+ }
323
+ function listOpencode() {
324
+ const db = opencodeDbHandle();
325
+ if (!db)
326
+ return [];
327
+ try {
328
+ const rows = db
329
+ .prepare("SELECT id, title, agent, model, time_created, time_updated FROM session ORDER BY time_updated DESC")
330
+ .all();
331
+ return rows.map((r) => ({
332
+ id: String(r.id ?? ""),
333
+ title: cleanTitle(typeof r.title === "string" ? r.title : "").slice(0, 80),
334
+ created_at: iso(r.time_created),
335
+ updated_at: iso(r.time_updated),
336
+ model: opencodeModelName(r.model) || (typeof r.agent === "string" ? r.agent : ""),
337
+ }));
338
+ }
339
+ catch {
340
+ return [];
341
+ }
342
+ }
343
+ function loadOpencode(id) {
344
+ const db = opencodeDbHandle();
345
+ if (!db)
346
+ return [];
347
+ try {
348
+ const msgs = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created, id").all(id);
349
+ const out = [];
350
+ for (const m of msgs) {
351
+ let data;
352
+ try {
353
+ data = JSON.parse(m.data);
354
+ }
355
+ catch {
356
+ continue;
357
+ }
358
+ if (data.role !== "user" && data.role !== "assistant")
359
+ continue;
360
+ const parts = db.prepare("SELECT data FROM part WHERE message_id = ? ORDER BY time_created, id").all(m.id);
361
+ let text = "";
362
+ for (const pr of parts) {
363
+ let pd;
364
+ try {
365
+ pd = JSON.parse(pr.data);
366
+ }
367
+ catch {
368
+ continue;
369
+ }
370
+ if (pd.type === "text" && typeof pd.text === "string")
371
+ text += pd.text;
372
+ }
373
+ if (text.trim())
374
+ out.push({ role: data.role, content: text.trim() });
375
+ }
376
+ return out;
377
+ }
378
+ catch {
379
+ return [];
380
+ }
381
+ }
382
+ // ---------------- Public API ----------------
383
+ export function listAgentSessions(agent) {
384
+ if (agent === "claude")
385
+ return listClaude();
386
+ if (agent === "codex")
387
+ return listCodex();
388
+ return listOpencode();
389
+ }
390
+ export function loadAgentMessages(agent, id) {
391
+ if (agent === "claude")
392
+ return loadClaude(id);
393
+ if (agent === "codex")
394
+ return loadCodex(id);
395
+ return loadOpencode(id);
396
+ }
package/dist/app.js ADDED
@@ -0,0 +1,169 @@
1
+ import process from "node:process";
2
+ import { Buffer } from "./terminal/buffer.js";
3
+ import { InputReader } from "./terminal/input.js";
4
+ import { Renderer } from "./terminal/renderer.js";
5
+ import { defaultState } from "./state.js";
6
+ import { loadConfig, saveConfig } from "./config.js";
7
+ import { ChatScreen } from "./screens/chat.js";
8
+ import { HistoryScreen } from "./screens/history.js";
9
+ import { AgentSelectScreen } from "./screens/agentSelect.js";
10
+ import { ModelSelectScreen } from "./screens/modelSelect.js";
11
+ import { SettingsScreen } from "./screens/settings.js";
12
+ import { StartScreen } from "./screens/start.js";
13
+ export class App {
14
+ state = defaultState();
15
+ width;
16
+ height;
17
+ screens = [];
18
+ renderer = new Renderer();
19
+ input = new InputReader();
20
+ running = false;
21
+ sizeTimer = null;
22
+ resizeHandler = () => this.onResize();
23
+ constructor() {
24
+ this.width = process.stdout.columns || 80;
25
+ this.height = process.stdout.rows || 24;
26
+ this.restoreSelection();
27
+ }
28
+ readSize() {
29
+ return {
30
+ width: process.stdout.columns || 80,
31
+ height: process.stdout.rows || 24,
32
+ };
33
+ }
34
+ syncSize() {
35
+ const size = this.readSize();
36
+ if (size.width !== this.width || size.height !== this.height) {
37
+ this.width = size.width;
38
+ this.height = size.height;
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ restoreSelection() {
44
+ const config = loadConfig();
45
+ this.state.selectedProvider = String(config.selected_provider ?? this.state.selectedProvider);
46
+ this.state.selectedModelName = String(config.selected_model ?? this.state.selectedModelName);
47
+ this.state.selectedIntensity = String(config.selected_intensity ?? this.state.selectedIntensity);
48
+ this.state.selectedModelUrl = String(config.selected_model_url ?? this.state.selectedModelUrl);
49
+ this.state.selectedModelApiKey = String(config.selected_model_api_key ?? this.state.selectedModelApiKey);
50
+ this.state.selectedModelStreaming = Boolean(config.selected_model_streaming ?? true);
51
+ }
52
+ async start() {
53
+ this.renderer.init();
54
+ this.input.start((ev) => this.handleEvent(ev));
55
+ process.stdout.on("resize", this.resizeHandler);
56
+ process.on("SIGINT", () => this.quit());
57
+ process.on("SIGTERM", () => this.quit());
58
+ this.sizeTimer = setInterval(() => {
59
+ if (this.syncSize())
60
+ this.render();
61
+ }, 200);
62
+ this.pushScreen(new StartScreen());
63
+ }
64
+ onResize() {
65
+ if (this.syncSize())
66
+ this.render();
67
+ }
68
+ get top() {
69
+ return this.screens[this.screens.length - 1];
70
+ }
71
+ pushScreen(screen) {
72
+ screen.app = this;
73
+ this.screens.push(screen);
74
+ screen.onActivate();
75
+ this.render();
76
+ }
77
+ popScreen() {
78
+ const screen = this.screens.pop();
79
+ screen?.onDeactivate();
80
+ this.render();
81
+ }
82
+ render() {
83
+ const buf = new Buffer(this.width, this.height);
84
+ this.top?.render(buf);
85
+ this.renderer.render(buf);
86
+ }
87
+ handleEvent(ev) {
88
+ if (ev.type === "key") {
89
+ const k = ev.key;
90
+ if (k.ctrl && (k.name === "c" || k.name === "q")) {
91
+ this.quit();
92
+ return;
93
+ }
94
+ if (k.ctrl && (k.name === "m" || k.name === "e")) {
95
+ this.openModelConfig();
96
+ return;
97
+ }
98
+ this.top?.onKey(k);
99
+ }
100
+ else {
101
+ this.top?.onMouse(ev.event);
102
+ }
103
+ }
104
+ setModelConfig(providerName, model, intensity) {
105
+ this.state.selectedProvider = providerName;
106
+ this.state.selectedModelName = model.name;
107
+ this.state.selectedModelUrl = model.url;
108
+ this.state.selectedModelApiKey = model.api_key ?? "";
109
+ this.state.selectedModelStreaming = model.supports_streaming ?? true;
110
+ this.state.selectedIntensity = intensity;
111
+ const config = loadConfig();
112
+ config.selected_provider = providerName;
113
+ config.selected_model = model.name;
114
+ config.selected_intensity = intensity;
115
+ config.selected_model_url = model.url;
116
+ config.selected_model_api_key = model.api_key ?? "";
117
+ config.selected_model_streaming = model.supports_streaming ?? true;
118
+ saveConfig(config);
119
+ }
120
+ startNewChat(text) {
121
+ this.pushScreen(new ChatScreen(text));
122
+ }
123
+ loadSession(data) {
124
+ const screen = new ChatScreen("");
125
+ screen.sessionId = data.id;
126
+ screen.loadedMessages = data.messages ?? [];
127
+ screen.loadedProvider = data.provider || null;
128
+ screen.loadedModel = data.model || null;
129
+ screen.loadedIntensity = data.intensity || null;
130
+ screen.loadedTokens = data.tokens ?? null;
131
+ if (data.provider)
132
+ this.state.selectedProvider = data.provider;
133
+ if (data.model)
134
+ this.state.selectedModelName = data.model;
135
+ if (data.intensity)
136
+ this.state.selectedIntensity = data.intensity;
137
+ this.pushScreen(screen);
138
+ }
139
+ openModelConfig() {
140
+ if (this.screens.some((s) => s instanceof ModelSelectScreen))
141
+ return;
142
+ this.pushScreen(new ModelSelectScreen());
143
+ }
144
+ openHistory() {
145
+ if (this.screens.some((s) => s instanceof HistoryScreen))
146
+ return;
147
+ this.pushScreen(new HistoryScreen());
148
+ }
149
+ openAgentSelect() {
150
+ if (this.screens.some((s) => s instanceof AgentSelectScreen))
151
+ return;
152
+ this.pushScreen(new AgentSelectScreen());
153
+ }
154
+ openSettings() {
155
+ if (this.screens.some((s) => s instanceof SettingsScreen))
156
+ return;
157
+ this.pushScreen(new SettingsScreen());
158
+ }
159
+ quit() {
160
+ if (this.running)
161
+ return;
162
+ this.running = true;
163
+ if (this.sizeTimer)
164
+ clearInterval(this.sizeTimer);
165
+ this.input.stop();
166
+ this.renderer.dispose();
167
+ process.exit(0);
168
+ }
169
+ }
package/dist/bin.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { App } from "./app.js";
4
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
5
+ process.stderr.write("neutrino requires an interactive terminal.\n");
6
+ process.exit(1);
7
+ }
8
+ const app = new App();
9
+ await app.start();
package/dist/colors.js ADDED
@@ -0,0 +1,14 @@
1
+ export const C = {
2
+ accent: [153, 217, 234], // #99D9EA
3
+ highlightBg: [40, 57, 61], // #28393D
4
+ black: [0, 0, 0],
5
+ dim: [86, 95, 137], // #565F89
6
+ user: [169, 177, 214], // #A9B1D6
7
+ assistant: [192, 202, 245], // #C0CAF5
8
+ toolHeader: [153, 217, 234], // cyan
9
+ amber: [224, 175, 104], // #E0AF68
10
+ white: [226, 232, 240],
11
+ green: [115, 218, 202],
12
+ red: [247, 118, 142],
13
+ };
14
+ export const EMPTY_STYLE = {};