@xingjisoft/neutrino 0.3.0 → 0.4.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.
Files changed (56) hide show
  1. package/README.md +33 -549
  2. package/assets/neutrino.ico +0 -0
  3. package/assets/neutrino.svg +7 -0
  4. package/assets/pro/backend_output_model.txt +5 -0
  5. package/assets/pro/claude-module-prompt.txt +65 -0
  6. package/assets/pro/common.txt +9 -0
  7. package/assets/pro/confirm_model.txt +5 -0
  8. package/assets/pro/front_output_model.txt +9 -0
  9. package/assets/pro/genimage_model.txt +1 -0
  10. package/assets/pro/main_model.txt +5 -0
  11. package/assets/pro/model-template.json +376 -0
  12. package/assets/pro/modeling_model.txt +5 -0
  13. package/assets/pro/plan_model.txt +8 -0
  14. package/assets/pro/thinking_model.txt +3 -0
  15. package/dist/agents.js +22 -3
  16. package/dist/app.js +199 -26
  17. package/dist/background.js +358 -0
  18. package/dist/bin.js +184 -44
  19. package/dist/browser.js +57 -0
  20. package/dist/config.js +174 -26
  21. package/dist/header.js +2 -1
  22. package/dist/index.js +1 -1
  23. package/dist/llm/approval.js +6 -2
  24. package/dist/llm/client.js +197 -28
  25. package/dist/llm/subagent.js +37 -1
  26. package/dist/llm/tools.js +3 -1
  27. package/dist/migration.js +33 -0
  28. package/dist/pro/agents.js +104 -0
  29. package/dist/pro/config.js +88 -0
  30. package/dist/pro/preflight.js +172 -0
  31. package/dist/screens/agentConversation.js +48 -5
  32. package/dist/screens/agentHistory.js +4 -2
  33. package/dist/screens/chat.js +14 -8
  34. package/dist/screens/modelSelect.js +15 -4
  35. package/dist/screens/settings.js +52 -9
  36. package/dist/screens/start.js +48 -15
  37. package/dist/state.js +1 -0
  38. package/dist/terminal/restore.js +3 -12
  39. package/dist/tray.js +138 -0
  40. package/dist/ui.js +58 -0
  41. package/dist/web/hub.js +26 -4
  42. package/dist/web/server.js +495 -74
  43. package/dist/web/service.js +135 -0
  44. package/dist/web/session.js +278 -25
  45. package/dist/web/settings.js +97 -24
  46. package/dist/web/terminalBridge.js +250 -0
  47. package/package.json +37 -37
  48. package/web/README.md +150 -16
  49. package/web/app.js +490 -25
  50. package/web/i18n.js +396 -345
  51. package/web/index.html +120 -15
  52. package/web/neutrino-pro.gif +0 -0
  53. package/web/neutrino.png +0 -0
  54. package/web/pro.js +184 -0
  55. package/web/settings.js +373 -247
  56. package/web/style.css +398 -0
@@ -1,28 +1,58 @@
1
- import { DEFAULT_CONFIG, INTENSITY_LEVELS, WEB_LANGUAGES, contextWindow, isContextWindow, loadConfig, loadModels, normalizeApprovalMode, normalizeWebLanguage, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
1
+ import { DEFAULT_CONFIG, INTENSITY_LEVELS, WEB_LANGUAGES, contextWindow, intensityForModel, isContextWindow, loadConfig, loadModels, modelContextWindow, modelContextWindowKey, modelIntensities, normalizeApprovalMode, normalizeIntensity, normalizeWebLanguage, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
2
2
  import { hashWebPassword, validateWebPassword } from "./auth.js";
3
- /** Public settings use a whitelist: API keys and cached credentials stay on disk. */
4
- export function getWebSettings() {
5
- const config = { ...DEFAULT_CONFIG, ...loadConfig() };
6
- const providers = loadModels().providers.map((provider) => ({
3
+ import { defaultProPreset, loadProModels, proMaxRecursionDepth, requireProPreset } from "../pro/config.js";
4
+ /**
5
+ * Public settings use a whitelist: API keys and cached credentials stay on disk.
6
+ *
7
+ * `mode` selects the workspace. The Pro workspace has its own defaults,
8
+ * language, plan and reviewer settings; the login password and the approval
9
+ * policy are layered in from the shared config by loadConfig(), so the two
10
+ * pages cannot drift apart on those.
11
+ */
12
+ export function getWebSettings(mode = "standard") {
13
+ const config = { ...DEFAULT_CONFIG, ...loadConfig(mode) };
14
+ const catalog = loadModels();
15
+ const providers = catalog.providers.map((provider) => ({
7
16
  name: provider.name,
8
- models: provider.models.map((model) => ({ name: model.name, configured: Boolean(model.api_key?.trim()), context_window: contextWindow(model.context_window) })),
17
+ models: provider.models.map((model) => ({
18
+ name: model.name,
19
+ configured: Boolean(model.api_key?.trim()),
20
+ context_window: modelContextWindow(provider.name, model, mode),
21
+ // The browser only offers these, so it cannot submit a level the model
22
+ // does not declare. Missing field = every intensity.
23
+ intensities: modelIntensities(model),
24
+ })),
9
25
  }));
10
26
  const preferred = providers.find((p) => p.name === config.selected_provider);
11
27
  const provider = preferred ?? providers.find((p) => p.models.some((m) => m.configured)) ?? providers[0];
12
28
  const model = provider?.models.find((m) => m.name === config.selected_model) ??
13
29
  provider?.models.find((m) => m.configured) ?? provider?.models[0];
14
- const intensity = String(config.selected_intensity ?? config.default_intensity);
30
+ const configuredModel = provider && model
31
+ ? catalog.providers.find((p) => p.name === provider.name)?.models.find((m) => m.name === model.name) ?? null
32
+ : null;
33
+ const intensity = intensityForModel(configuredModel, config.selected_intensity ?? config.default_intensity, config.default_intensity);
15
34
  return {
35
+ ...(mode === "pro" ? { pro: {
36
+ presets: loadProModels().tier.map((preset) => ({
37
+ name: preset.name, chinese_name: preset.chinese_name,
38
+ "model-setting": preset["model-setting"].map((setting) => ({
39
+ type: setting.type, provider: setting.provider, model: setting.model,
40
+ intensity: setting.intensity, context_window: setting.context_window,
41
+ })),
42
+ })),
43
+ preset: defaultProPreset(),
44
+ max_recursion_depth: proMaxRecursionDepth(),
45
+ } } : {}),
16
46
  providers,
17
47
  intensities: INTENSITY_LEVELS,
18
48
  languages: WEB_LANGUAGES.map((value) => ({ value, label: value === "zh" ? "简体中文" : "English" })),
19
49
  selection: {
20
50
  provider: provider?.name ?? "",
21
51
  model: model?.name ?? "",
22
- intensity: INTENSITY_LEVELS.includes(intensity) ? intensity : "High",
52
+ intensity,
23
53
  },
24
54
  settings: {
25
- default_intensity: String(config.default_intensity),
55
+ default_intensity: intensityForModel(configuredModel, config.default_intensity, "High"),
26
56
  model_context_window: model?.context_window ?? contextWindow(undefined),
27
57
  approval_model: String(config.approval_model ?? ""),
28
58
  auto_approve_extreme_risk: config.auto_approve_extreme_risk === true,
@@ -31,27 +61,50 @@ export function getWebSettings() {
31
61
  plan_first: config.plan_first !== false,
32
62
  approval_mode: normalizeApprovalMode(config.approval_mode),
33
63
  web_console: config.web_console !== false,
64
+ web_lan: config.web_lan === true,
65
+ background: config.background === true,
34
66
  },
35
67
  };
36
68
  }
37
69
  /** Validate the complete patch before writing anything, preserving unrelated settings. */
38
- export function updateWebSettings(value) {
70
+ export function updateWebSettings(value, mode = "standard") {
39
71
  if (!value || typeof value !== "object" || Array.isArray(value))
40
72
  throw new Error("invalid settings body");
41
73
  const body = value;
42
74
  if (Object.keys(body).some((key) => key !== "selection" && key !== "settings"))
43
75
  throw new Error("unknown settings field");
44
- const config = loadConfig();
76
+ const config = loadConfig(mode);
77
+ // Resolve the requested model first: its declared intensities decide which
78
+ // default level can be saved with this patch. Settings that do not touch a
79
+ // model stay editable even when the catalogue is empty.
80
+ const chosen = body.selection !== undefined ? resolveModelSelection(body.selection, false) : undefined;
81
+ let target = chosen ?? null;
82
+ const settingsTarget = () => {
83
+ target ??= resolveModelSelection(getWebSettings(mode).selection, false);
84
+ return target;
85
+ };
45
86
  if (body.settings !== undefined) {
46
87
  if (!body.settings || typeof body.settings !== "object" || Array.isArray(body.settings))
47
88
  throw new Error("invalid settings");
48
89
  const patch = body.settings;
49
90
  for (const [key, val] of Object.entries(patch)) {
50
- if (key === "default_intensity") {
51
- if (typeof val !== "string" || !INTENSITY_LEVELS.includes(val))
91
+ if (key === "pro_preset" && mode === "pro") {
92
+ config.pro_preset = requireProPreset(val);
93
+ }
94
+ else if (key === "pro_max_recursion_depth" && mode === "pro") {
95
+ if (typeof val !== "number" || !Number.isSafeInteger(val) || val < 1 || val > 100) {
96
+ throw new Error("Pro recursion depth must be a whole number from 1 to 100");
97
+ }
98
+ config.pro_max_recursion_depth = val;
99
+ }
100
+ else if (key === "default_intensity") {
101
+ const level = normalizeIntensity(val);
102
+ if (!level)
52
103
  throw new Error("invalid default intensity");
53
- config.default_intensity = val;
54
- config.selected_intensity = val;
104
+ if (!modelIntensities(settingsTarget().model).includes(level))
105
+ throw new Error("this model does not support that thinking intensity");
106
+ config.default_intensity = level;
107
+ config.selected_intensity = level;
55
108
  }
56
109
  else if (key === "model_context_window") {
57
110
  if (!isContextWindow(val))
@@ -93,6 +146,16 @@ export function updateWebSettings(value) {
93
146
  throw new Error("web console must be a boolean");
94
147
  config.web_console = val;
95
148
  }
149
+ else if (key === "web_lan") {
150
+ if (typeof val !== "boolean")
151
+ throw new Error("LAN access must be a boolean");
152
+ config.web_lan = val;
153
+ }
154
+ else if (key === "background") {
155
+ if (typeof val !== "boolean")
156
+ throw new Error("background residency must be a boolean");
157
+ config.background = val;
158
+ }
96
159
  else if (key === "web_password") {
97
160
  validateWebPassword(val);
98
161
  }
@@ -101,8 +164,7 @@ export function updateWebSettings(value) {
101
164
  }
102
165
  }
103
166
  }
104
- if (body.selection !== undefined) {
105
- const chosen = resolveModelSelection(body.selection, false);
167
+ if (chosen) {
106
168
  config.selected_provider = chosen.provider;
107
169
  config.selected_model = chosen.model.name;
108
170
  config.selected_model_url = chosen.model.url;
@@ -116,12 +178,23 @@ export function updateWebSettings(value) {
116
178
  config.web_password_hash = hashWebPassword(password);
117
179
  const window = body.settings?.model_context_window;
118
180
  if (window !== undefined) {
119
- const selection = body.selection ?? getWebSettings().selection;
120
- const chosen = resolveModelSelection(selection, false);
121
- const models = loadModels();
122
- const model = models.providers.find((p) => p.name === chosen.provider).models.find((m) => m.name === chosen.model.name);
123
- model.context_window = window;
124
- saveModels(models);
181
+ const selected = settingsTarget();
182
+ if (mode === "pro") {
183
+ // Context capacity is part of a workspace's compression settings. Pro
184
+ // stores its own copy instead of editing the shared model catalog.
185
+ const current = config.model_context_windows;
186
+ const overrides = current && typeof current === "object" && !Array.isArray(current)
187
+ ? { ...current }
188
+ : {};
189
+ overrides[modelContextWindowKey(selected.provider, selected.model.name)] = window;
190
+ config.model_context_windows = overrides;
191
+ }
192
+ else {
193
+ const models = loadModels();
194
+ const model = models.providers.find((p) => p.name === selected.provider).models.find((m) => m.name === selected.model.name);
195
+ model.context_window = window;
196
+ saveModels(models);
197
+ }
125
198
  }
126
- saveConfig(config);
199
+ saveConfig(config, mode);
127
200
  }
@@ -0,0 +1,250 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import http from "node:http";
3
+ import { resolveModelSelection } from "../config.js";
4
+ import { hub } from "./hub.js";
5
+ function sendJson(res, status, data) {
6
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
7
+ res.end(JSON.stringify(data));
8
+ }
9
+ function authorized(req, token) {
10
+ const provided = req.headers.authorization?.replace(/^Bearer /, "") ?? "";
11
+ const expected = Buffer.from(token);
12
+ const actual = Buffer.from(provided);
13
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
14
+ }
15
+ async function readBody(req) {
16
+ const chunks = [];
17
+ let size = 0;
18
+ for await (const chunk of req) {
19
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
20
+ size += bytes.length;
21
+ if (size > 1_000_000)
22
+ throw new Error("request too large");
23
+ chunks.push(bytes);
24
+ }
25
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
26
+ if (!value || typeof value !== "object" || Array.isArray(value))
27
+ throw new Error("invalid request");
28
+ return value;
29
+ }
30
+ /** A private loopback bridge exposes only the terminal's live hub. */
31
+ export async function startTerminalBridge() {
32
+ const token = randomBytes(32).toString("hex");
33
+ const server = http.createServer((req, res) => {
34
+ void (async () => {
35
+ if (!authorized(req, token)) {
36
+ sendJson(res, 401, { error: "terminal token required" });
37
+ return;
38
+ }
39
+ if (req.method === "GET" && req.url === "/snapshots") {
40
+ const snapshots = [];
41
+ for (const entry of hub.list()) {
42
+ const snapshot = hub.snapshot(entry.key);
43
+ if (snapshot)
44
+ snapshots.push({ mode: hub.has(entry.key, "pro") ? "pro" : "standard", snapshot });
45
+ }
46
+ sendJson(res, 200, snapshots);
47
+ return;
48
+ }
49
+ if (req.method !== "POST" || req.url !== "/action") {
50
+ sendJson(res, 404, { error: "not found" });
51
+ return;
52
+ }
53
+ const input = await readBody(req);
54
+ const key = typeof input.key === "string" ? input.key : "";
55
+ const action = input.action;
56
+ const body = input.body && typeof input.body === "object" && !Array.isArray(input.body)
57
+ ? input.body : {};
58
+ if (!hub.has(key)) {
59
+ sendJson(res, 404, { error: "unknown live session" });
60
+ return;
61
+ }
62
+ if (action === "pause")
63
+ hub.pause(key);
64
+ else if (action === "resume")
65
+ hub.resume(key);
66
+ else if (action === "message") {
67
+ if (typeof body.text !== "string" || !body.text.trim())
68
+ throw new Error("empty message");
69
+ hub.send(key, body.text);
70
+ }
71
+ else if (action === "approve") {
72
+ if (!hub.approve(key, body.approved === true)) {
73
+ sendJson(res, 409, { error: "no approval is pending" });
74
+ return;
75
+ }
76
+ }
77
+ else if (action === "approval-mode") {
78
+ if (body.mode !== "request" && body.mode !== "auto")
79
+ throw new Error("mode must be request or auto");
80
+ if (!hub.setApprovalMode(key, body.mode)) {
81
+ sendJson(res, 409, { error: "this session cannot change approval mode" });
82
+ return;
83
+ }
84
+ }
85
+ else if (action === "model") {
86
+ const result = hub.configure(key, resolveModelSelection(body));
87
+ if (!result.ok) {
88
+ sendJson(res, 409, { error: result.error });
89
+ return;
90
+ }
91
+ }
92
+ else if (action === "pro-preset") {
93
+ if (typeof body.preset !== "string")
94
+ throw new Error("invalid Pro preset");
95
+ const result = hub.setProPreset(key, body.preset);
96
+ if (!result.ok) {
97
+ sendJson(res, 409, { error: result.error });
98
+ return;
99
+ }
100
+ }
101
+ else if (action === "clear") {
102
+ if (!hub.clear(key)) {
103
+ sendJson(res, 409, { error: "this session cannot be cleared" });
104
+ return;
105
+ }
106
+ }
107
+ else {
108
+ sendJson(res, 400, { error: "this session is owned by the terminal UI" });
109
+ return;
110
+ }
111
+ sendJson(res, 200, {
112
+ ok: true, snapshot: hub.snapshot(key),
113
+ ...(action === "pause" ? { paused: true } : action === "resume" ? { paused: false } : {}),
114
+ ...(action === "approve" ? { approved: body.approved === true } : {}),
115
+ });
116
+ })().catch(error => {
117
+ if (!res.headersSent)
118
+ sendJson(res, 400, { error: error instanceof Error ? error.message : "invalid request" });
119
+ else
120
+ res.end();
121
+ });
122
+ });
123
+ await new Promise((resolve, reject) => {
124
+ server.once("error", reject);
125
+ server.listen(0, "127.0.0.1", () => { server.removeListener("error", reject); resolve(); });
126
+ });
127
+ server.unref();
128
+ const address = server.address();
129
+ if (!address || typeof address === "string")
130
+ throw new Error("could not open the terminal bridge");
131
+ return { url: `http://127.0.0.1:${address.port}`, token, close: () => { server.close(); } };
132
+ }
133
+ class RemoteTerminal {
134
+ bridge;
135
+ remoteKey;
136
+ mode;
137
+ current;
138
+ key = "";
139
+ constructor(bridge, remoteKey, mode, initial) {
140
+ this.bridge = bridge;
141
+ this.remoteKey = remoteKey;
142
+ this.mode = mode;
143
+ this.current = initial;
144
+ this.key = hub.register(this);
145
+ }
146
+ liveCwd() { return this.current.cwd; }
147
+ liveMeta() {
148
+ const { key: _key, cwd: _cwd, messages: _messages, streamingContent: _streaming, context: _context, plan: _plan, ...meta } = this.current;
149
+ return meta;
150
+ }
151
+ liveSnapshot(key) { return { ...this.current, key }; }
152
+ // The public server forwards actions through remoteAction, where failures can
153
+ // be returned to the browser rather than silently lost.
154
+ livePause() { }
155
+ liveResume() { }
156
+ liveSend(_text) { }
157
+ update(snapshot) {
158
+ this.current = snapshot;
159
+ hub.notify(this.key);
160
+ }
161
+ async remoteAction(action, body) {
162
+ try {
163
+ const response = await fetch(`${this.bridge.url}/action`, {
164
+ method: "POST", headers: { Authorization: `Bearer ${this.bridge.token}`, "Content-Type": "application/json" },
165
+ body: JSON.stringify({ key: this.remoteKey, action, body }), signal: AbortSignal.timeout(5000),
166
+ });
167
+ const data = await response.json();
168
+ if (data.snapshot) {
169
+ this.update(data.snapshot);
170
+ data.snapshot = this.liveSnapshot(this.key);
171
+ }
172
+ return { status: response.status, data };
173
+ }
174
+ catch {
175
+ return { status: 503, data: { error: "the terminal session is unavailable" } };
176
+ }
177
+ }
178
+ close() { hub.unregister(this.key); }
179
+ }
180
+ /** Mirror each attached terminal's live sessions into the Web service. */
181
+ export class TerminalBridges {
182
+ entries = new Map();
183
+ timer = setInterval(() => { for (const owner of this.entries.keys())
184
+ void this.poll(owner); }, 250);
185
+ constructor() { this.timer.unref(); }
186
+ add(owner, bridge) {
187
+ this.remove(owner);
188
+ this.entries.set(owner, { bridge, sessions: new Map(), polling: false, failures: 0 });
189
+ void this.poll(owner);
190
+ }
191
+ remove(owner) {
192
+ const entry = this.entries.get(owner);
193
+ if (!entry)
194
+ return;
195
+ this.entries.delete(owner);
196
+ for (const session of entry.sessions.values())
197
+ session.close();
198
+ }
199
+ close() {
200
+ clearInterval(this.timer);
201
+ for (const owner of this.entries.keys())
202
+ this.remove(owner);
203
+ }
204
+ async poll(owner) {
205
+ const entry = this.entries.get(owner);
206
+ if (!entry || entry.polling)
207
+ return;
208
+ entry.polling = true;
209
+ try {
210
+ const response = await fetch(`${entry.bridge.url}/snapshots`, {
211
+ headers: { Authorization: `Bearer ${entry.bridge.token}` }, signal: AbortSignal.timeout(1500),
212
+ });
213
+ if (!response.ok)
214
+ throw new Error("terminal bridge failed");
215
+ const snapshots = await response.json();
216
+ if (!Array.isArray(snapshots) || this.entries.get(owner) !== entry)
217
+ return;
218
+ entry.failures = 0;
219
+ const live = new Set();
220
+ for (const { mode, snapshot } of snapshots) {
221
+ if (!snapshot || typeof snapshot.key !== "string" || (mode !== "standard" && mode !== "pro"))
222
+ continue;
223
+ live.add(snapshot.key);
224
+ const session = entry.sessions.get(snapshot.key);
225
+ if (session)
226
+ session.update(snapshot);
227
+ else
228
+ entry.sessions.set(snapshot.key, new RemoteTerminal(entry.bridge, snapshot.key, mode, snapshot));
229
+ }
230
+ for (const [key, session] of entry.sessions) {
231
+ if (!live.has(key)) {
232
+ session.close();
233
+ entry.sessions.delete(key);
234
+ }
235
+ }
236
+ }
237
+ catch {
238
+ // Keep snapshots over brief disconnects, but never expose a dead terminal
239
+ // indefinitely if its process stays alive after the bridge fails.
240
+ if (++entry.failures >= 4) {
241
+ for (const session of entry.sessions.values())
242
+ session.close();
243
+ entry.sessions.clear();
244
+ }
245
+ }
246
+ finally {
247
+ entry.polling = false;
248
+ }
249
+ }
250
+ }
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
- {
2
- "name": "@xingjisoft/neutrino",
3
- "version": "0.3.0",
4
- "description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
5
- "type": "module",
6
- "license": "Apache-2.0",
7
- "bin": {
8
- "neutrino": "dist/bin.js"
9
- },
10
- "main": "dist/app.js",
11
- "files": [
12
- "dist",
13
- "assets",
14
- "web"
15
- ],
16
- "engines": {
17
- "node": ">=18.0.0"
18
- },
19
- "scripts": {
20
- "build": "tsc -p tsconfig.json",
21
- "test": "npm run build && node scripts/test-paste.mjs && node scripts/test-mouse.mjs && node scripts/test-cursor.mjs && node scripts/test-selection.mjs && node scripts/test-drag-scroll.mjs && node scripts/test-selection-performance.mjs && node scripts/test-copy.mjs && node scripts/test-scrollbar.mjs && node scripts/test-render.mjs && node scripts/test-drift.mjs && node scripts/test-sanitize.mjs && node scripts/test-markdown.mjs && node scripts/test-wrap.mjs && node scripts/test-fetch.mjs && node scripts/test-compact.mjs && node scripts/test-history-fold.mjs && node scripts/test-web-history.mjs && node scripts/test-web-default.mjs && node scripts/test-turn-fold.mjs && node scripts/test-plan.mjs && node scripts/test-subagent.mjs && node scripts/test-web-settings.mjs && node scripts/test-web-markdown.mjs && node scripts/test-web-i18n.mjs && node scripts/test-web-workspaces.mjs && node scripts/test-tools.mjs && node scripts/test-approval.mjs && node scripts/test-approval-review.mjs && node scripts/test-approval-ui.mjs && node scripts/test-web-approval-details.mjs && node scripts/test-approval-mode.mjs && node scripts/test-approval-context.mjs && node scripts/test-settings-render.mjs && node scripts/test-approval-flow.mjs && node scripts/test-web.mjs && node scripts/test-web-settings-http.mjs && node scripts/test-web-auth.mjs && node scripts/test-web-auth-ui.mjs && node scripts/test-interrupt.mjs && node scripts/test-terminal-lifecycle.mjs && node scripts/test-persistence.mjs && node scripts/test-resilience.mjs && node scripts/test-approval-timeout.mjs && node scripts/test-approval-hardening.mjs && node scripts/test-approval-extreme.mjs && node scripts/test-approval-reason.mjs",
22
- "prepare": "npm run build",
23
- "web": "node dist/bin.js web"
24
- },
25
- "keywords": [
26
- "cli",
27
- "agent",
28
- "ai",
29
- "terminal",
30
- "llm",
31
- "coding"
32
- ],
33
- "devDependencies": {
34
- "@types/node": "^22.0.0",
35
- "typescript": "^5.5.0"
36
- }
37
- }
1
+ {
2
+ "name": "@xingjisoft/neutrino",
3
+ "version": "0.4.1",
4
+ "description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "neutrino": "dist/bin.js"
9
+ },
10
+ "main": "dist/app.js",
11
+ "files": [
12
+ "dist",
13
+ "assets",
14
+ "web"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "test": "npm run build && node scripts/test-paste.mjs && node scripts/test-mouse.mjs && node scripts/test-cursor.mjs && node scripts/test-selection.mjs && node scripts/test-drag-scroll.mjs && node scripts/test-selection-performance.mjs && node scripts/test-copy.mjs && node scripts/test-scrollbar.mjs && node scripts/test-render.mjs && node scripts/test-drift.mjs && node scripts/test-sanitize.mjs && node scripts/test-markdown.mjs && node scripts/test-wrap.mjs && node scripts/test-fetch.mjs && node scripts/test-compact.mjs && node scripts/test-history-fold.mjs && node scripts/test-web-history.mjs && node scripts/test-session-migration.mjs && node scripts/test-web-session-migration.mjs && node scripts/test-web-default.mjs && node scripts/test-turn-fold.mjs && node scripts/test-long-turn.mjs && node scripts/test-plan.mjs && node scripts/test-subagent.mjs && node scripts/test-web-settings.mjs && node scripts/test-model-intensity.mjs && node scripts/test-background.mjs && node scripts/test-background-lifetime.mjs && node scripts/test-web-markdown.mjs && node scripts/test-web-i18n.mjs && node scripts/test-web-workspaces.mjs && node scripts/test-tools.mjs && node scripts/test-approval.mjs && node scripts/test-approval-review.mjs && node scripts/test-approval-ui.mjs && node scripts/test-web-approval-details.mjs && node scripts/test-approval-mode.mjs && node scripts/test-approval-context.mjs && node scripts/test-settings-render.mjs && node scripts/test-approval-flow.mjs && node scripts/test-web.mjs && node scripts/test-web-settings-http.mjs && node scripts/test-web-lan.mjs && node scripts/test-web-auth.mjs && node scripts/test-web-auth-ui.mjs && node scripts/test-web-pro.mjs && node scripts/test-pro-preflight.mjs && node scripts/test-pro-runtime.mjs && node scripts/test-pro-team.mjs && node scripts/test-pro-ui.mjs && node scripts/test-interrupt.mjs && node scripts/test-terminal-lifecycle.mjs && node scripts/test-persistence.mjs && node scripts/test-resilience.mjs && node scripts/test-approval-timeout.mjs && node scripts/test-approval-hardening.mjs && node scripts/test-approval-extreme.mjs && node scripts/test-approval-reason.mjs",
22
+ "prepare": "npm run build",
23
+ "web": "node dist/bin.js web"
24
+ },
25
+ "keywords": [
26
+ "cli",
27
+ "agent",
28
+ "ai",
29
+ "terminal",
30
+ "llm",
31
+ "coding"
32
+ ],
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "typescript": "^5.5.0"
36
+ }
37
+ }