@xingjisoft/neutrino 0.2.1 → 0.4.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/README.md +312 -436
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/app.js +43 -7
- package/dist/bin.js +5 -2
- package/dist/browser.js +57 -0
- package/dist/config.js +179 -26
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/chat.js +54 -38
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +9 -7
- package/dist/screens/start.js +48 -15
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +31 -4
- package/dist/web/server.js +611 -50
- package/dist/web/session.js +320 -20
- package/dist/web/settings.js +117 -24
- package/package.json +37 -37
- package/web/README.md +158 -16
- package/web/app.js +1603 -128
- package/web/i18n.js +382 -0
- package/web/index.html +289 -37
- package/web/markdown.js +710 -0
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +322 -193
- package/web/style.css +1378 -42
package/dist/browser.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
/**
|
|
4
|
+
* Where the browser console's Pro workspace lives, given the console's base
|
|
5
|
+
* URL.
|
|
6
|
+
*
|
|
7
|
+
* The base comes straight from the running server, but it is normalised here
|
|
8
|
+
* anyway: a trailing slash must not produce `//pro`, and a URL that already
|
|
9
|
+
* points at `/pro` must not become `/pro/pro`. A missing/blank base yields the
|
|
10
|
+
* path alone instead of throwing, so a half-configured console can still be
|
|
11
|
+
* reported instead of crashing the click that asked for it.
|
|
12
|
+
*/
|
|
13
|
+
export function proModeUrl(baseUrl) {
|
|
14
|
+
const base = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
15
|
+
if (/\/pro$/i.test(base))
|
|
16
|
+
return base;
|
|
17
|
+
return `${base}/pro`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Open `url` in the user's default browser. Fire-and-forget: the child is
|
|
21
|
+
* detached, its stdio is ignored and it is unref'd so the terminal is never
|
|
22
|
+
* held open (or blocked) by the browser. Every failure is swallowed — opening
|
|
23
|
+
* a browser is best-effort and must never take the TUI down.
|
|
24
|
+
*/
|
|
25
|
+
export function openInBrowser(url) {
|
|
26
|
+
try {
|
|
27
|
+
let command;
|
|
28
|
+
let args;
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
// `start` is a cmd built-in. The empty "" is the (required) window title,
|
|
31
|
+
// so a quoted URL is never mistaken for one.
|
|
32
|
+
command = "cmd";
|
|
33
|
+
args = ["/c", "start", "", url];
|
|
34
|
+
}
|
|
35
|
+
else if (process.platform === "darwin") {
|
|
36
|
+
command = "open";
|
|
37
|
+
args = [url];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
command = "xdg-open";
|
|
41
|
+
args = [url];
|
|
42
|
+
}
|
|
43
|
+
const child = spawn(command, args, {
|
|
44
|
+
detached: true,
|
|
45
|
+
stdio: "ignore",
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
// A missing opener (xdg-open is not installed everywhere) is reported
|
|
49
|
+
// asynchronously; without a listener that 'error' event would crash the
|
|
50
|
+
// whole process.
|
|
51
|
+
child.on("error", () => { });
|
|
52
|
+
child.unref();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Best-effort only.
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -11,6 +11,36 @@ export const NEUTRINO_DIR = path.join(os.homedir(), ".neutrino");
|
|
|
11
11
|
export const MODEL_FILE = path.join(NEUTRINO_DIR, "model.json");
|
|
12
12
|
export const SESSION_DIR = path.join(NEUTRINO_DIR, "session");
|
|
13
13
|
export const CONFIG_FILE = path.join(NEUTRINO_DIR, "config.json");
|
|
14
|
+
/**
|
|
15
|
+
* The browser console has two workspaces: the regular one and the isolated
|
|
16
|
+
* "Pro" workspace served at `/pro`. Everything a workspace owns — transcripts,
|
|
17
|
+
* model defaults, plan/compaction settings, reviewer model and interface
|
|
18
|
+
* language — is stored per mode; only the credentials and the approval policy
|
|
19
|
+
* below are shared, because they protect or gate the whole process.
|
|
20
|
+
*/
|
|
21
|
+
export const MODES = ["standard", "pro"];
|
|
22
|
+
export const PRO_DIR = path.join(NEUTRINO_DIR, "pro");
|
|
23
|
+
export const PRO_CONFIG_FILE = path.join(PRO_DIR, "config.json");
|
|
24
|
+
export const PRO_SESSION_DIR = path.join(PRO_DIR, "session");
|
|
25
|
+
/**
|
|
26
|
+
* Settings that intentionally apply to both workspaces. The Web password, the
|
|
27
|
+
* approval policy and the console's interface are properties of this process,
|
|
28
|
+
* not of a conversation workspace.
|
|
29
|
+
*/
|
|
30
|
+
export const SHARED_CONFIG_KEYS = ["web_password_hash", "approval_mode", "auto_approve_extreme_risk", "web_lan"];
|
|
31
|
+
export function normalizeMode(value) {
|
|
32
|
+
return typeof value === "string" && value.trim().toLowerCase() === "pro" ? "pro" : "standard";
|
|
33
|
+
}
|
|
34
|
+
export function configFile(mode = "standard") {
|
|
35
|
+
return mode === "pro" ? PRO_CONFIG_FILE : CONFIG_FILE;
|
|
36
|
+
}
|
|
37
|
+
export function sessionDir(mode = "standard") {
|
|
38
|
+
return mode === "pro" ? PRO_SESSION_DIR : SESSION_DIR;
|
|
39
|
+
}
|
|
40
|
+
/** Key used to remember a model's context capacity per workspace. */
|
|
41
|
+
export function modelContextWindowKey(provider, modelName) {
|
|
42
|
+
return `${provider}/${modelName}`;
|
|
43
|
+
}
|
|
14
44
|
/**
|
|
15
45
|
* Whether assistant messages must be sent back with a `reasoning_content`
|
|
16
46
|
* field. DeepSeek's thinking mode requires one on *every* assistant message of
|
|
@@ -32,12 +62,26 @@ export function contextWindow(value) {
|
|
|
32
62
|
return isContextWindow(value) ? value : DEFAULT_CONTEXT_WINDOW;
|
|
33
63
|
}
|
|
34
64
|
/** Reload the exact provider/model so settings also apply to active chats. */
|
|
35
|
-
export function modelContextWindow(provider, model) {
|
|
65
|
+
export function modelContextWindow(provider, model, mode = "standard") {
|
|
66
|
+
// The Pro workspace keeps its own context capacities: changing one there must
|
|
67
|
+
// not move the compression threshold of the regular workspace (and vice
|
|
68
|
+
// versa). The capacity is a per-workspace setting, not shared model metadata,
|
|
69
|
+
// so an unset Pro value starts from the documented default instead of
|
|
70
|
+
// inheriting whatever the regular workspace saved in model.json.
|
|
71
|
+
if (mode === "pro") {
|
|
72
|
+
const configured = loadConfig("pro").model_context_windows;
|
|
73
|
+
if (configured && typeof configured === "object" && !Array.isArray(configured)) {
|
|
74
|
+
const override = configured[modelContextWindowKey(provider, model.name)];
|
|
75
|
+
if (isContextWindow(override))
|
|
76
|
+
return override;
|
|
77
|
+
}
|
|
78
|
+
return contextWindow(undefined);
|
|
79
|
+
}
|
|
36
80
|
const saved = loadModels().providers.find((p) => p.name === provider)?.models.find((m) => m.name === model.name);
|
|
37
81
|
return contextWindow((saved ?? model).context_window);
|
|
38
82
|
}
|
|
39
|
-
export function contextCompressionThreshold(provider, model) {
|
|
40
|
-
return Math.max(1, Math.floor(modelContextWindow(provider, model) * CONTEXT_COMPRESSION_RATIO));
|
|
83
|
+
export function contextCompressionThreshold(provider, model, mode = "standard") {
|
|
84
|
+
return Math.max(1, Math.floor(modelContextWindow(provider, model, mode) * CONTEXT_COMPRESSION_RATIO));
|
|
41
85
|
}
|
|
42
86
|
export const DEFAULT_MODELS = {
|
|
43
87
|
providers: [
|
|
@@ -63,6 +107,12 @@ export const DEFAULT_MODELS = {
|
|
|
63
107
|
},
|
|
64
108
|
],
|
|
65
109
|
};
|
|
110
|
+
/** Languages the browser console can render its own interface in. */
|
|
111
|
+
export const WEB_LANGUAGES = ["en", "zh"];
|
|
112
|
+
/** Unknown or missing values fall back to English rather than breaking the UI. */
|
|
113
|
+
export function normalizeWebLanguage(value) {
|
|
114
|
+
return typeof value === "string" && value.trim().toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
115
|
+
}
|
|
66
116
|
export const DEFAULT_CONFIG = {
|
|
67
117
|
language: "en",
|
|
68
118
|
theme: "default",
|
|
@@ -83,6 +133,12 @@ export const DEFAULT_CONFIG = {
|
|
|
83
133
|
// Mount the browser UI alongside the terminal UI unless it is turned off
|
|
84
134
|
// here or on the command line.
|
|
85
135
|
web_console: true,
|
|
136
|
+
// Listen on every interface (the equivalent of --lan) instead of loopback.
|
|
137
|
+
// The listener belongs to the process, so both workspaces share this value.
|
|
138
|
+
web_lan: false,
|
|
139
|
+
// Working directories opened from the browser, newest first. They remain
|
|
140
|
+
// listed even when they hold no conversations yet.
|
|
141
|
+
web_workspaces: [],
|
|
86
142
|
// Initialized as a salted hash of the default Web password on first use.
|
|
87
143
|
web_password_hash: "",
|
|
88
144
|
};
|
|
@@ -94,6 +150,54 @@ export const INTENSITY_PARAMS = {
|
|
|
94
150
|
Max: { temperature: 1.0, top_p: 1.0 },
|
|
95
151
|
Ultra: { temperature: 1.5, top_p: 1.0 },
|
|
96
152
|
};
|
|
153
|
+
/** Canonical spelling of a configured intensity name, or null when unknown. */
|
|
154
|
+
export function normalizeIntensity(value) {
|
|
155
|
+
if (typeof value !== "string")
|
|
156
|
+
return null;
|
|
157
|
+
const wanted = value.trim().toLowerCase();
|
|
158
|
+
return INTENSITY_LEVELS.find((level) => level.toLowerCase() === wanted) ?? null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The intensities a model accepts, in the canonical Low → Ultra order.
|
|
162
|
+
*
|
|
163
|
+
* An absent or empty list keeps the historical behaviour: every level is
|
|
164
|
+
* allowed. Unknown names are ignored rather than making the model unusable, so
|
|
165
|
+
* a typo in `model.json` falls back to the remaining valid entries (or to all
|
|
166
|
+
* levels when none is valid).
|
|
167
|
+
*/
|
|
168
|
+
export function modelIntensities(model) {
|
|
169
|
+
const raw = model?.intensity;
|
|
170
|
+
const entries = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
171
|
+
const allowed = new Set();
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const level = normalizeIntensity(entry);
|
|
174
|
+
if (level)
|
|
175
|
+
allowed.add(level);
|
|
176
|
+
}
|
|
177
|
+
if (allowed.size === 0)
|
|
178
|
+
return [...INTENSITY_LEVELS];
|
|
179
|
+
return INTENSITY_LEVELS.filter((level) => allowed.has(level));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pick a usable intensity for a model. The preferred value wins when the model
|
|
183
|
+
* accepts it; otherwise the configured fallback, then High, then the first
|
|
184
|
+
* supported level. This is the one place stale UI state, a saved session or a
|
|
185
|
+
* hand-edited `config.json` is reconciled with the model's limits.
|
|
186
|
+
*/
|
|
187
|
+
export function intensityForModel(model, preferred, fallback) {
|
|
188
|
+
const allowed = modelIntensities(model);
|
|
189
|
+
for (const candidate of [preferred, fallback, "High"]) {
|
|
190
|
+
const level = normalizeIntensity(candidate);
|
|
191
|
+
if (level && allowed.includes(level))
|
|
192
|
+
return level;
|
|
193
|
+
}
|
|
194
|
+
return allowed[0] ?? "High";
|
|
195
|
+
}
|
|
196
|
+
/** Exact model lookup within a provider (names may repeat across providers). */
|
|
197
|
+
export function findConfiguredModel(providerName, modelName) {
|
|
198
|
+
return loadModels().providers.find((provider) => provider.name === providerName)
|
|
199
|
+
?.models.find((model) => model.name === modelName) ?? null;
|
|
200
|
+
}
|
|
97
201
|
export function normalizeApprovalMode(value) {
|
|
98
202
|
// Config files are hand-edited: "Auto" or " autO " must not silently
|
|
99
203
|
// downgrade the conversation to a prompt on every tool call.
|
|
@@ -101,9 +205,11 @@ export function normalizeApprovalMode(value) {
|
|
|
101
205
|
? "auto"
|
|
102
206
|
: "request";
|
|
103
207
|
}
|
|
104
|
-
export function ensureDirs() {
|
|
208
|
+
export function ensureDirs(mode = "standard") {
|
|
105
209
|
fs.mkdirSync(NEUTRINO_DIR, { recursive: true });
|
|
106
|
-
fs.mkdirSync(
|
|
210
|
+
fs.mkdirSync(sessionDir(mode), { recursive: true });
|
|
211
|
+
if (mode === "pro")
|
|
212
|
+
fs.mkdirSync(PRO_DIR, { recursive: true });
|
|
107
213
|
}
|
|
108
214
|
export function loadModels() {
|
|
109
215
|
try {
|
|
@@ -123,7 +229,26 @@ export function saveModels(data) {
|
|
|
123
229
|
ensureDirs();
|
|
124
230
|
fs.writeFileSync(MODEL_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
125
231
|
}
|
|
126
|
-
|
|
232
|
+
/** Read a config file without creating it; null when missing or unreadable. */
|
|
233
|
+
function readConfigFile(file) {
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export function loadConfig(mode = "standard") {
|
|
242
|
+
if (mode === "pro") {
|
|
243
|
+
// Pro keeps its own file, with credentials/approval policy layered on top
|
|
244
|
+
// from the main config so those stay shared by construction.
|
|
245
|
+
const own = readConfigFile(PRO_CONFIG_FILE) ?? {};
|
|
246
|
+
const shared = readConfigFile(CONFIG_FILE) ?? DEFAULT_CONFIG;
|
|
247
|
+
const merged = { ...DEFAULT_CONFIG, ...own };
|
|
248
|
+
for (const key of SHARED_CONFIG_KEYS)
|
|
249
|
+
merged[key] = shared[key] ?? DEFAULT_CONFIG[key];
|
|
250
|
+
return merged;
|
|
251
|
+
}
|
|
127
252
|
try {
|
|
128
253
|
ensureDirs();
|
|
129
254
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
@@ -138,8 +263,30 @@ export function loadConfig() {
|
|
|
138
263
|
return { ...DEFAULT_CONFIG };
|
|
139
264
|
}
|
|
140
265
|
}
|
|
141
|
-
export function saveConfig(data) {
|
|
142
|
-
ensureDirs();
|
|
266
|
+
export function saveConfig(data, mode = "standard") {
|
|
267
|
+
ensureDirs(mode);
|
|
268
|
+
if (mode === "pro") {
|
|
269
|
+
fs.mkdirSync(PRO_DIR, { recursive: true });
|
|
270
|
+
const own = { ...data };
|
|
271
|
+
for (const key of SHARED_CONFIG_KEYS)
|
|
272
|
+
delete own[key];
|
|
273
|
+
fs.writeFileSync(PRO_CONFIG_FILE, JSON.stringify(own, null, 2), "utf-8");
|
|
274
|
+
// A Pro settings save that touched a shared key (password, approval policy)
|
|
275
|
+
// writes it back to the main config, where the terminal and both pages read it.
|
|
276
|
+
const shared = readConfigFile(CONFIG_FILE) ?? { ...DEFAULT_CONFIG };
|
|
277
|
+
let changed = false;
|
|
278
|
+
for (const key of SHARED_CONFIG_KEYS) {
|
|
279
|
+
if (data[key] === undefined)
|
|
280
|
+
continue;
|
|
281
|
+
if (JSON.stringify(data[key]) === JSON.stringify(shared[key]))
|
|
282
|
+
continue;
|
|
283
|
+
shared[key] = data[key];
|
|
284
|
+
changed = true;
|
|
285
|
+
}
|
|
286
|
+
if (changed)
|
|
287
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(shared, null, 2), "utf-8");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
143
290
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
144
291
|
}
|
|
145
292
|
/** Look a model up by name across every configured provider. */
|
|
@@ -170,7 +317,8 @@ export function resolveModelSelection(value, requireKey = true) {
|
|
|
170
317
|
if (typeof input.provider !== "string" || typeof input.model !== "string" ||
|
|
171
318
|
typeof input.intensity !== "string")
|
|
172
319
|
throw new Error("provider, model and intensity are required");
|
|
173
|
-
|
|
320
|
+
const intensity = normalizeIntensity(input.intensity);
|
|
321
|
+
if (!intensity)
|
|
174
322
|
throw new Error("invalid thinking intensity");
|
|
175
323
|
const provider = loadModels().providers.find((p) => p.name === input.provider);
|
|
176
324
|
if (!provider)
|
|
@@ -178,15 +326,17 @@ export function resolveModelSelection(value, requireKey = true) {
|
|
|
178
326
|
const model = provider.models.find((m) => m.name === input.model);
|
|
179
327
|
if (!model)
|
|
180
328
|
throw new Error("unknown model for this provider");
|
|
329
|
+
if (!modelIntensities(model).includes(intensity))
|
|
330
|
+
throw new Error("this model does not support that thinking intensity");
|
|
181
331
|
if (requireKey && !model.api_key?.trim())
|
|
182
332
|
throw new Error("this model has no API key configured");
|
|
183
|
-
return { provider: provider.name, model: { ...model }, intensity
|
|
333
|
+
return { provider: provider.name, model: { ...model }, intensity };
|
|
184
334
|
}
|
|
185
|
-
export function saveSession(messages, modelName, provider, intensity, cwd, sessionId, tokens, context, approvalMode, plan) {
|
|
186
|
-
ensureDirs();
|
|
335
|
+
export function saveSession(messages, modelName, provider, intensity, cwd, sessionId, tokens, context, approvalMode, plan, mode = "standard", proPreset) {
|
|
336
|
+
ensureDirs(mode);
|
|
187
337
|
const now = new Date();
|
|
188
338
|
const id = sessionId ?? `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
189
|
-
const existing = sessionId ? loadSession(sessionId) : null;
|
|
339
|
+
const existing = sessionId ? loadSession(sessionId, mode) : null;
|
|
190
340
|
const created_at = existing ? existing.created_at : now.toISOString();
|
|
191
341
|
let title = "";
|
|
192
342
|
for (const msg of messages) {
|
|
@@ -207,6 +357,8 @@ export function saveSession(messages, modelName, provider, intensity, cwd, sessi
|
|
|
207
357
|
messages,
|
|
208
358
|
tokens,
|
|
209
359
|
approval_mode: normalizeApprovalMode(approvalMode ?? existing?.approval_mode),
|
|
360
|
+
...(mode === "pro" && (proPreset ?? existing?.pro_preset)
|
|
361
|
+
? { pro_preset: proPreset ?? existing?.pro_preset } : {}),
|
|
210
362
|
};
|
|
211
363
|
const validContext = normalizeContext(context, messages.filter((m) => m.role !== "system").length);
|
|
212
364
|
if (validContext)
|
|
@@ -215,7 +367,7 @@ export function saveSession(messages, modelName, provider, intensity, cwd, sessi
|
|
|
215
367
|
// how far it got, and a stopped plan is still the record of what was agreed.
|
|
216
368
|
if (plan && plan.steps.length)
|
|
217
369
|
session.plan = plan;
|
|
218
|
-
const filePath = path.join(
|
|
370
|
+
const filePath = path.join(sessionDir(mode), `${id}.json`);
|
|
219
371
|
// Keep the last complete transcript intact if writing or replacing fails.
|
|
220
372
|
// The temporary file shares the destination directory so rename is atomic.
|
|
221
373
|
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -243,8 +395,8 @@ export function saveSession(messages, modelName, provider, intensity, cwd, sessi
|
|
|
243
395
|
}
|
|
244
396
|
return id;
|
|
245
397
|
}
|
|
246
|
-
export function loadSession(sessionId) {
|
|
247
|
-
const filePath = path.join(
|
|
398
|
+
export function loadSession(sessionId, mode = "standard") {
|
|
399
|
+
const filePath = path.join(sessionDir(mode), `${sessionId}.json`);
|
|
248
400
|
if (!fs.existsSync(filePath))
|
|
249
401
|
return null;
|
|
250
402
|
try {
|
|
@@ -254,27 +406,28 @@ export function loadSession(sessionId) {
|
|
|
254
406
|
return null;
|
|
255
407
|
}
|
|
256
408
|
}
|
|
257
|
-
export function deleteSession(sessionId) {
|
|
258
|
-
const filePath = path.join(
|
|
409
|
+
export function deleteSession(sessionId, mode = "standard") {
|
|
410
|
+
const filePath = path.join(sessionDir(mode), `${sessionId}.json`);
|
|
259
411
|
if (fs.existsSync(filePath)) {
|
|
260
412
|
fs.unlinkSync(filePath);
|
|
261
413
|
return true;
|
|
262
414
|
}
|
|
263
415
|
return false;
|
|
264
416
|
}
|
|
265
|
-
export function listSessions() {
|
|
266
|
-
ensureDirs();
|
|
417
|
+
export function listSessions(mode = "standard") {
|
|
418
|
+
ensureDirs(mode);
|
|
267
419
|
const sessions = [];
|
|
420
|
+
const dir = sessionDir(mode);
|
|
268
421
|
let files = [];
|
|
269
422
|
try {
|
|
270
|
-
files = fs.readdirSync(
|
|
423
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
271
424
|
}
|
|
272
425
|
catch {
|
|
273
426
|
return sessions;
|
|
274
427
|
}
|
|
275
428
|
for (const f of files) {
|
|
276
429
|
try {
|
|
277
|
-
const data = JSON.parse(fs.readFileSync(path.join(
|
|
430
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
278
431
|
sessions.push({
|
|
279
432
|
id: data.id ?? f.replace(/\.json$/, ""),
|
|
280
433
|
created_at: data.created_at ?? "",
|
|
@@ -299,8 +452,8 @@ export function listSessions() {
|
|
|
299
452
|
* The history menu is scoped to the current project so sessions started in
|
|
300
453
|
* other directories do not clutter it.
|
|
301
454
|
*/
|
|
302
|
-
export function listSessionsForCwd(cwd = process.cwd()) {
|
|
303
|
-
return listSessions().filter((s) => sameCwd(s.cwd, cwd));
|
|
455
|
+
export function listSessionsForCwd(cwd = process.cwd(), mode = "standard") {
|
|
456
|
+
return listSessions(mode).filter((s) => sameCwd(s.cwd, cwd));
|
|
304
457
|
}
|
|
305
458
|
function readAsset(...candidates) {
|
|
306
459
|
for (const candidate of candidates) {
|
|
@@ -351,13 +504,13 @@ export function loadSubAgentPrompt() {
|
|
|
351
504
|
* `NEUTRINO_PLAN=off` overrides both ways for a single run, the way the other
|
|
352
505
|
* environment variables override the saved configuration.
|
|
353
506
|
*/
|
|
354
|
-
export function planFirstEnabled() {
|
|
507
|
+
export function planFirstEnabled(mode = "standard") {
|
|
355
508
|
const override = String(process.env.NEUTRINO_PLAN ?? "").trim().toLowerCase();
|
|
356
509
|
if (override === "off" || override === "0" || override === "false" || override === "no")
|
|
357
510
|
return false;
|
|
358
511
|
if (override === "on" || override === "1" || override === "true" || override === "yes")
|
|
359
512
|
return true;
|
|
360
|
-
return loadConfig().plan_first !== false;
|
|
513
|
+
return loadConfig(mode).plan_first !== false;
|
|
361
514
|
}
|
|
362
515
|
let cachedVersion = null;
|
|
363
516
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { App } from "./app.js";
|
|
2
2
|
export { defaultState } from "./state.js";
|
|
3
|
-
export { INTENSITY_LEVELS, INTENSITY_PARAMS, DEFAULT_MODELS, loadModels, saveModels, loadConfig, saveConfig, listSessions, loadSession, deleteSession, saveSession, } from "./config.js";
|
|
3
|
+
export { INTENSITY_LEVELS, INTENSITY_PARAMS, DEFAULT_MODELS, MODES, normalizeMode, normalizeIntensity, modelIntensities, intensityForModel, findConfiguredModel, loadModels, saveModels, loadConfig, saveConfig, listSessions, loadSession, deleteSession, saveSession, } from "./config.js";
|
|
4
4
|
export { LLMClient } from "./llm/client.js";
|
|
5
5
|
export { executeTool, getToolDefinitions } from "./llm/tools.js";
|
package/dist/llm/approval.js
CHANGED
|
@@ -511,9 +511,13 @@ export class ApprovalGate {
|
|
|
511
511
|
// Resolved inside the try: an unresolvable reviewer model is a failed
|
|
512
512
|
// vote (and therefore a denial), not an exception that aborts the call.
|
|
513
513
|
const { model, provider, intensity } = this.options.reviewer();
|
|
514
|
-
const client = new LLMClient(model, provider, intensity);
|
|
514
|
+
const client = this.options.createReviewerClient?.() ?? new LLMClient(model, provider, intensity);
|
|
515
515
|
const timeout = this.options.reviewTimeoutMs?.() ?? REVIEW_TIMEOUT_MS;
|
|
516
|
-
const
|
|
516
|
+
const rolePrompt = this.options.reviewerSystemPrompt?.();
|
|
517
|
+
const system = rolePrompt ? `${rolePrompt}\n\n${REVIEW_SYSTEM_PROMPT}` : REVIEW_SYSTEM_PROMPT;
|
|
518
|
+
const controller = new AbortController();
|
|
519
|
+
const raw = await withTimeout(client.complete(system, prompt, { signal: controller.signal, cancelled: this.options.cancelled }), timeout)
|
|
520
|
+
.finally(() => controller.abort());
|
|
517
521
|
if (raw === null) {
|
|
518
522
|
// Fail closed: a reviewer that never answers must not stall the turn.
|
|
519
523
|
return {
|