@xingjisoft/neutrino 0.1.4 → 0.1.6
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/README.md +413 -163
- package/assets/prompt.txt +4 -0
- package/dist/agents.js +114 -15
- package/dist/app.js +442 -30
- package/dist/bin.js +177 -5
- package/dist/colors.js +2 -0
- package/dist/compact.js +19 -0
- package/dist/config.js +121 -28
- package/dist/cwd.js +54 -0
- package/dist/header.js +8 -0
- package/dist/llm/approval.js +543 -0
- package/dist/llm/client.js +14 -0
- package/dist/llm/risk.js +377 -0
- package/dist/llm/tools.js +37 -21
- package/dist/persistence.js +51 -0
- package/dist/screens/agentHistory.js +7 -4
- package/dist/screens/chat.js +709 -49
- package/dist/screens/history.js +9 -7
- package/dist/screens/settings.js +131 -14
- package/dist/screens/start.js +50 -16
- package/dist/selection.js +60 -0
- package/dist/state.js +1 -0
- package/dist/terminal/input.js +34 -5
- package/dist/terminal/renderer.js +58 -12
- package/dist/terminal/restore.js +148 -0
- package/dist/ui.js +16 -0
- package/dist/web/auth.js +82 -0
- package/dist/web/hub.js +180 -0
- package/dist/web/server.js +688 -0
- package/dist/web/session.js +572 -0
- package/dist/web/settings.js +101 -0
- package/dist/width.js +20 -0
- package/package.json +6 -4
- package/web/README.md +210 -0
- package/web/app.js +1100 -0
- package/web/index.html +90 -0
- package/web/settings.js +193 -0
- package/web/style.css +1088 -0
- package/web/turns.js +52 -0
package/dist/agents.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { claudeProjectKey, sameCwd } from "./cwd.js";
|
|
5
6
|
const HOME = os.homedir();
|
|
6
7
|
function parseJsonlLines(filePath) {
|
|
7
8
|
let raw;
|
|
@@ -27,6 +28,37 @@ function parseJsonlLines(filePath) {
|
|
|
27
28
|
}
|
|
28
29
|
return out;
|
|
29
30
|
}
|
|
31
|
+
const HEAD_BYTES = 64 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Read at most `maxBytes` from the start of a file.
|
|
34
|
+
*
|
|
35
|
+
* Session transcripts can be several megabytes, but the working directory is
|
|
36
|
+
* always recorded up front, so scanning just the head keeps the history list
|
|
37
|
+
* fast when a project holds many sessions.
|
|
38
|
+
*/
|
|
39
|
+
function readHead(filePath, maxBytes = HEAD_BYTES) {
|
|
40
|
+
let fd = null;
|
|
41
|
+
try {
|
|
42
|
+
fd = fs.openSync(filePath, "r");
|
|
43
|
+
const len = Math.min(fs.fstatSync(fd).size, maxBytes);
|
|
44
|
+
if (len <= 0)
|
|
45
|
+
return "";
|
|
46
|
+
const buf = Buffer.alloc(len);
|
|
47
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
48
|
+
return buf.toString("utf-8");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (fd !== null) {
|
|
55
|
+
try {
|
|
56
|
+
fs.closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
30
62
|
function contentText(content) {
|
|
31
63
|
if (typeof content === "string")
|
|
32
64
|
return content;
|
|
@@ -78,9 +110,27 @@ function claudeTitle(lines) {
|
|
|
78
110
|
}
|
|
79
111
|
return "";
|
|
80
112
|
}
|
|
81
|
-
|
|
113
|
+
const CWD_FIELD_RE = /"cwd"\s*:\s*("(?:[^"\\]|\\.)*")/;
|
|
114
|
+
/** Working directory recorded in a Claude Code transcript ("" when unknown). */
|
|
115
|
+
function claudeSessionCwd(filePath) {
|
|
116
|
+
const m = CWD_FIELD_RE.exec(readHead(filePath));
|
|
117
|
+
if (!m)
|
|
118
|
+
return "";
|
|
119
|
+
try {
|
|
120
|
+
const v = JSON.parse(m[1]);
|
|
121
|
+
return typeof v === "string" ? v : "";
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function listClaude(cwd) {
|
|
82
128
|
const metas = [];
|
|
129
|
+
const wantKey = claudeProjectKey(cwd);
|
|
83
130
|
for (const dir of claudeProjectDirs()) {
|
|
131
|
+
// Claude names each project dir after its cwd, so the dir name still tells
|
|
132
|
+
// us the directory when a transcript predates the `cwd` field.
|
|
133
|
+
const dirKey = path.basename(dir).toLowerCase();
|
|
84
134
|
let files;
|
|
85
135
|
try {
|
|
86
136
|
files = fs.readdirSync(dir);
|
|
@@ -99,6 +149,10 @@ function listClaude() {
|
|
|
99
149
|
catch {
|
|
100
150
|
continue;
|
|
101
151
|
}
|
|
152
|
+
// History is scoped to the current project: skip other directories.
|
|
153
|
+
const sessionCwd = claudeSessionCwd(fp);
|
|
154
|
+
if (sessionCwd ? !sameCwd(sessionCwd, cwd) : dirKey !== wantKey)
|
|
155
|
+
continue;
|
|
102
156
|
const id = f.slice(0, -".jsonl".length);
|
|
103
157
|
const title = cleanTitle(claudeTitle(parseJsonlLines(fp)) || id.slice(0, 8));
|
|
104
158
|
metas.push({
|
|
@@ -216,11 +270,41 @@ function codexMessageText(payload) {
|
|
|
216
270
|
.join("")
|
|
217
271
|
.trim();
|
|
218
272
|
}
|
|
219
|
-
|
|
273
|
+
/** Working directory recorded in a Codex rollout file ("" when unknown). */
|
|
274
|
+
function codexSessionCwd(filePath) {
|
|
275
|
+
let fallback = "";
|
|
276
|
+
for (const line of readHead(filePath).split("\n")) {
|
|
277
|
+
const t = line.trim();
|
|
278
|
+
if (!t)
|
|
279
|
+
continue;
|
|
280
|
+
let o;
|
|
281
|
+
try {
|
|
282
|
+
o = JSON.parse(t);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const payload = o.payload;
|
|
288
|
+
const c = typeof payload?.cwd === "string" ? payload.cwd : typeof o.cwd === "string" ? o.cwd : "";
|
|
289
|
+
if (!c)
|
|
290
|
+
continue;
|
|
291
|
+
// session_meta is authoritative; other records (turn_context) only serve
|
|
292
|
+
// as a fallback for older rollouts.
|
|
293
|
+
if (o.type === "session_meta")
|
|
294
|
+
return c;
|
|
295
|
+
if (!fallback)
|
|
296
|
+
fallback = c;
|
|
297
|
+
}
|
|
298
|
+
return fallback;
|
|
299
|
+
}
|
|
300
|
+
function listCodex(cwd) {
|
|
220
301
|
const index = codexIndex();
|
|
221
302
|
const files = codexRolloutFiles();
|
|
222
303
|
const metas = [];
|
|
223
304
|
for (const [id, fp] of files) {
|
|
305
|
+
// History is scoped to the current project: skip other directories.
|
|
306
|
+
if (!sameCwd(codexSessionCwd(fp), cwd))
|
|
307
|
+
continue;
|
|
224
308
|
const info = index.get(id);
|
|
225
309
|
let title = info?.title ?? "";
|
|
226
310
|
let updated = info?.updated_at ?? "";
|
|
@@ -320,15 +404,28 @@ function opencodeModelName(modelJson) {
|
|
|
320
404
|
return "";
|
|
321
405
|
}
|
|
322
406
|
}
|
|
323
|
-
function listOpencode() {
|
|
407
|
+
function listOpencode(cwd) {
|
|
324
408
|
const db = opencodeDbHandle();
|
|
325
409
|
if (!db)
|
|
326
410
|
return [];
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
411
|
+
// Current schemas store the project directory on the session row; older ones
|
|
412
|
+
// keep it on the related project row. Try both, newest schema first.
|
|
413
|
+
const queries = [
|
|
414
|
+
"SELECT id, title, agent, model, time_created, time_updated, directory AS dir FROM session ORDER BY time_updated DESC",
|
|
415
|
+
"SELECT s.id AS id, s.title AS title, s.agent AS agent, s.model AS model, s.time_created AS time_created, s.time_updated AS time_updated, p.worktree AS dir FROM session s LEFT JOIN project p ON p.id = s.project_id ORDER BY s.time_updated DESC",
|
|
416
|
+
];
|
|
417
|
+
for (const sql of queries) {
|
|
418
|
+
let rows;
|
|
419
|
+
try {
|
|
420
|
+
rows = db.prepare(sql).all();
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
continue; // this schema has no project path column, try the next query
|
|
424
|
+
}
|
|
425
|
+
// History is scoped to the current project: skip other directories.
|
|
426
|
+
return rows
|
|
427
|
+
.filter((r) => sameCwd(typeof r.dir === "string" ? r.dir : "", cwd))
|
|
428
|
+
.map((r) => ({
|
|
332
429
|
id: String(r.id ?? ""),
|
|
333
430
|
title: cleanTitle(typeof r.title === "string" ? r.title : "").slice(0, 80),
|
|
334
431
|
created_at: iso(r.time_created),
|
|
@@ -336,9 +433,7 @@ function listOpencode() {
|
|
|
336
433
|
model: opencodeModelName(r.model) || (typeof r.agent === "string" ? r.agent : ""),
|
|
337
434
|
}));
|
|
338
435
|
}
|
|
339
|
-
|
|
340
|
-
return [];
|
|
341
|
-
}
|
|
436
|
+
return [];
|
|
342
437
|
}
|
|
343
438
|
function loadOpencode(id) {
|
|
344
439
|
const db = opencodeDbHandle();
|
|
@@ -380,12 +475,16 @@ function loadOpencode(id) {
|
|
|
380
475
|
}
|
|
381
476
|
}
|
|
382
477
|
// ---------------- Public API ----------------
|
|
383
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Sessions of `agent` that were started in `cwd` (the directory neutrino runs
|
|
480
|
+
* in), newest first. Sessions from other directories are not shown.
|
|
481
|
+
*/
|
|
482
|
+
export function listAgentSessions(agent, cwd = process.cwd()) {
|
|
384
483
|
if (agent === "claude")
|
|
385
|
-
return listClaude();
|
|
484
|
+
return listClaude(cwd);
|
|
386
485
|
if (agent === "codex")
|
|
387
|
-
return listCodex();
|
|
388
|
-
return listOpencode();
|
|
486
|
+
return listCodex(cwd);
|
|
487
|
+
return listOpencode(cwd);
|
|
389
488
|
}
|
|
390
489
|
export function loadAgentMessages(agent, id) {
|
|
391
490
|
if (agent === "claude")
|