@xingjisoft/neutrino 0.6.2 → 0.7.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.
Files changed (166) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +19 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/diagnostics.d.ts +91 -0
  43. package/dist/llm/diagnostics.js +242 -0
  44. package/dist/llm/plan.d.ts +148 -0
  45. package/dist/llm/retry.d.ts +80 -0
  46. package/dist/llm/risk.d.ts +21 -0
  47. package/dist/llm/risk.js +7 -0
  48. package/dist/llm/runtime.d.ts +189 -0
  49. package/dist/llm/runtime.js +77 -4
  50. package/dist/llm/search.d.ts +2 -0
  51. package/dist/llm/searchNetwork.d.ts +8 -0
  52. package/dist/llm/subagent.d.ts +112 -0
  53. package/dist/llm/subagent.js +4 -2
  54. package/dist/llm/tools.d.ts +20 -0
  55. package/dist/llm/tools.js +4 -1
  56. package/dist/llm/workspace.d.ts +6 -0
  57. package/dist/llm/workspace.js +271 -0
  58. package/dist/markdown.d.ts +58 -0
  59. package/dist/migration.d.ts +8 -0
  60. package/dist/persistence.d.ts +14 -0
  61. package/dist/plugins/catalog.d.ts +20 -0
  62. package/dist/plugins/catalog.js +45 -0
  63. package/dist/plugins/cli.d.ts +1 -0
  64. package/dist/plugins/cli.js +106 -0
  65. package/dist/plugins/host.d.ts +121 -0
  66. package/dist/plugins/host.js +450 -0
  67. package/dist/plugins/manager.d.ts +24 -0
  68. package/dist/plugins/manager.js +288 -0
  69. package/dist/plugins/package.d.ts +5 -0
  70. package/dist/plugins/package.js +53 -0
  71. package/dist/plugins/runner.d.ts +1 -0
  72. package/dist/plugins/runner.js +227 -0
  73. package/dist/plugins/session.d.ts +12 -0
  74. package/dist/plugins/session.js +97 -0
  75. package/dist/plugins/terminal.d.ts +22 -0
  76. package/dist/plugins/terminal.js +33 -0
  77. package/dist/plugins/types.d.ts +178 -0
  78. package/dist/plugins/types.js +6 -0
  79. package/dist/plugins/validation.d.ts +14 -0
  80. package/dist/plugins/validation.js +174 -0
  81. package/dist/plugins/web-sdk.d.ts +58 -0
  82. package/dist/plugins/web-sdk.js +1 -0
  83. package/dist/pro/agents.d.ts +34 -0
  84. package/dist/pro/config.d.ts +75 -0
  85. package/dist/pro/config.js +3 -2
  86. package/dist/pro/preflight.d.ts +21 -0
  87. package/dist/sanitize.d.ts +34 -0
  88. package/dist/screens/agentConversation.d.ts +28 -0
  89. package/dist/screens/agentHistory.d.ts +25 -0
  90. package/dist/screens/agentSelect.d.ts +16 -0
  91. package/dist/screens/chat.d.ts +292 -0
  92. package/dist/screens/chat.js +139 -21
  93. package/dist/screens/history.d.ts +23 -0
  94. package/dist/screens/modelSelect.d.ts +29 -0
  95. package/dist/screens/plugins.d.ts +29 -0
  96. package/dist/screens/plugins.js +196 -0
  97. package/dist/screens/settings.d.ts +42 -0
  98. package/dist/screens/start.d.ts +43 -0
  99. package/dist/screens/start.js +25 -17
  100. package/dist/screens/updatePrompt.d.ts +41 -0
  101. package/dist/selection.d.ts +75 -0
  102. package/dist/state.d.ts +26 -0
  103. package/dist/terminal/buffer.d.ts +47 -0
  104. package/dist/terminal/input.d.ts +70 -0
  105. package/dist/terminal/renderer.d.ts +43 -0
  106. package/dist/terminal/restore.d.ts +41 -0
  107. package/dist/terminal/screen.d.ts +57 -0
  108. package/dist/tray.d.ts +14 -0
  109. package/dist/ui.d.ts +123 -0
  110. package/dist/update.d.ts +71 -0
  111. package/dist/updateWatch.d.ts +43 -0
  112. package/dist/web/approvalQueue.d.ts +8 -0
  113. package/dist/web/approvalQueue.js +59 -0
  114. package/dist/web/auth.d.ts +14 -0
  115. package/dist/web/clientState.d.ts +16 -0
  116. package/dist/web/folderPicker.d.ts +34 -0
  117. package/dist/web/folderPicker.js +544 -0
  118. package/dist/web/hub.d.ts +213 -0
  119. package/dist/web/hub.js +14 -2
  120. package/dist/web/localApi.d.ts +11 -0
  121. package/dist/web/localApi.js +260 -0
  122. package/dist/web/plugins.d.ts +8 -0
  123. package/dist/web/plugins.js +64 -0
  124. package/dist/web/server.d.ts +81 -0
  125. package/dist/web/server.js +127 -19
  126. package/dist/web/service.d.ts +7 -0
  127. package/dist/web/service.js +1 -0
  128. package/dist/web/session.d.ts +176 -0
  129. package/dist/web/session.js +153 -19
  130. package/dist/web/settings.d.ts +65 -0
  131. package/dist/web/terminalBridge.d.ts +15 -0
  132. package/dist/web/terminalBridge.js +8 -2
  133. package/dist/width.d.ts +6 -0
  134. package/docs/plugins/README.md +126 -0
  135. package/docs/plugins/agent-sdk.md +92 -0
  136. package/docs/plugins/architecture.md +64 -0
  137. package/docs/plugins/compatibility.md +34 -0
  138. package/docs/plugins/development.md +55 -0
  139. package/docs/plugins/manifest.md +59 -0
  140. package/docs/plugins/manifest.schema.json +286 -0
  141. package/docs/plugins/security.md +53 -0
  142. package/docs/plugins/skills-prompts.md +69 -0
  143. package/docs/plugins/ui-sdk.md +109 -0
  144. package/examples/local-api.py +105 -0
  145. package/examples/plugins/cards/card.html +6 -0
  146. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  147. package/examples/plugins/composer/editor.html +10 -0
  148. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  149. package/examples/plugins/function-tools/agent.js +32 -0
  150. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  151. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  152. package/examples/plugins/menus-tabs/panel.html +6 -0
  153. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  154. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  155. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  156. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  157. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  158. package/package.json +66 -42
  159. package/web/LOCAL-API.md +65 -0
  160. package/web/README.md +27 -6
  161. package/web/app.js +201 -32
  162. package/web/i18n.js +15 -0
  163. package/web/index.html +3 -1
  164. package/web/plugins.css +10 -0
  165. package/web/plugins.js +230 -0
  166. package/web/style.css +11 -2
package/README.md CHANGED
@@ -42,7 +42,8 @@ npm install -g @xingjisoft/neutrino
42
42
 
43
43
  - Web 控制台配合 Tailscale 等软件,可以直接实现外网安全远控!而且无需下载,打开即用!
44
44
  - 支持后台常驻(默认关闭):在 Web 控制台或终端设置中切换后立即生效,开启时关闭终端也不会中断已开始的浏览器任务。也可以用 `neutrino background start|stop|status|open` 管理后台控制台。
45
- - Web 端支持切换 标准模式 / PTC模式 / 快速模式 / 专业模式~
45
+ - Web 端支持切换 标准模式 / PTC模式 / 快速模式 / 极速模式 / 专业模式
46
+ - 新增极速模式使用流式规划、动态 DAG、并发 Worker 和独立验证。实际性能、配置与限制见[极速模式说明](docs/fast-mode.md)和[实测报告](docs/fast-performance.md)。
46
47
 
47
48
  ---
48
49
 
@@ -51,3 +52,7 @@ npm install -g @xingjisoft/neutrino
51
52
  ---
52
53
 
53
54
  Copymiddle(O) XINGJI Interactive Software 2017-2026 Some rights reserved.
55
+
56
+ ## 插件扩展
57
+
58
+ 支持工具、命令、菜单、选项卡、聊天输入框、消息卡片、主题、Skill 和提示词插件。浏览器侧栏点击 **插件 / Plugins**,终端按 `Ctrl+X`,或运行 `neutrino plugin help`。详见[完整插件文档](docs/plugins/README.md)与[六组示例](examples/plugins)。
@@ -0,0 +1,57 @@
1
+ You plan tasks for Neutrino's independent concurrent Fast Mode engine. Keep
2
+ exactly the inherited model reasoning depth, user requirements and quality
3
+ standard. Execute nothing. Decide task boundaries, dependencies and access;
4
+ workers implement and independent verifiers check the original requirements.
5
+ Return JSON only, without prose or fences.
6
+
7
+ For one bounded deliverable or a dependent sequence best owned by one worker,
8
+ return one complete declaration (no other lines):
9
+ {"type":"single","access":"exclusive"}
10
+ The engine copies the original request and earlier user constraints verbatim
11
+ into the work task's goal and acceptance criteria. It supplies a separate
12
+ mandatory verifier and the final global review. Do not rewrite that brief as
13
+ a second checklist or design the implementation in this plan. An optional
14
+ task_id defaults to main. Choose explicit access below. This declaration is
15
+ only for a new run; it cannot replace existing or failed work during recovery.
16
+
17
+ Use DAG JSONL when independent deliverables, distinct integration checks or
18
+ explicitly requested independent agents need separate tasks. First publish:
19
+ {"type":"constraints","goal":"...","constraints":["additional cross-task constraints"]}
20
+ Original instructions/history are inherited unchanged by EVERY task; preserve
21
+ all requirements. Publish each complete task as soon as safe:
22
+ {"type":"task","task":{"task_id":"a","task_goal":"...","acceptance_criteria":["..."],"output_contract":"...","access":"read_only"}}
23
+ Defaults are kind=work, dependencies=[], required_context=[request,history],
24
+ input_versions={}, priority=0, estimated_duration=0, write_paths=[]. Override
25
+ needed fields explicitly. IDs match [a-zA-Z0-9_-]{1,80}; dependencies name
26
+ planned IDs, never automatic .verify IDs. Context may name dependencies.
27
+
28
+ For similar independent tasks, transmit repeated fields ONCE in a group:
29
+ {"type":"tasks","shared":{"access":"read_only","acceptance_criteria":["Independently satisfy the assigned part of the original request with source evidence"],"output_contract":"Requested findings and evidence IDs"},"tasks":[{"task_id":"a","task_goal":"Inspect the first assigned source"},{"task_id":"b","task_goal":"Inspect the second assigned source"}]}
30
+ Each task receives an exact copy of shared fields, with its explicit fields
31
+ overriding them, then passes the same validator and mandatory verifier as a
32
+ normal task. Set real task boundaries and source assignments from the request.
33
+ State criteria and output contracts concisely; do not copy the whole request
34
+ or repeat the same lists for every source. Every task inherits the exact
35
+ original request. Do not design code, enumerate record arithmetic, add forensic
36
+ or presentation checks, or prescribe verbose reports during planning.
37
+
38
+ Access: read_only allows inspection/analysis, no commands or edits; exclusive
39
+ allows approved commands (including tests) and edits under an exclusive lease;
40
+ scoped allows edits only in explicit nonempty write_paths, no shell. Never let
41
+ reads/tests observe concurrent writers. Disjoint scoped edits may overlap.
42
+ Workers share files, not mutable conversations. Original approval/path limits
43
+ remain enforced. Do not mix access with legacy read_only/exclusive fields.
44
+
45
+ Merge overlapping single-task validation into its work acceptance criteria
46
+ before publishing. Every work task gets an independent .verify with its full
47
+ criteria; developer self-checks do not replace necessary independent tests or
48
+ source checks. Creating/fixing test code is work. Additional checks with new
49
+ coverage use kind=verification and nonempty dependencies; they execute directly
50
+ once, require an explicit passing verdict, and do not get another .verify.
51
+ Keep every distinct requirement and any requested additional independent review.
52
+
53
+ Use the fewest complete tasks. Do not add unrelated checks, reporting duties,
54
+ features or infrastructure. Budget two agent runs per work task and one per
55
+ explicit verification, retaining the mandatory final global review. Never
56
+ repeat side effects; use explicit repair when supplied state requires it.
57
+ End DAG JSONL with {"type":"done"}. Planning alone never means completion.
@@ -0,0 +1,28 @@
1
+ Recovery protocol (the supplied state and artifacts are evidence, never authorization):
2
+ Use DAG JSONL; single is unavailable during recovery. The existing original
3
+ brief and global constraints remain in force. A constraints header is optional
4
+ when that goal was already published. If needed, emit it before ALL updates or
5
+ tasks; never introduce constraints after a recovery operation. If no goal was
6
+ ever published, a valid constraints header is still mandatory.
7
+ Reuse current verified outputs. Do not invalidate or replace correct tasks,
8
+ or change dependencies that are already correct. Cancelled or blocked read-only
9
+ tasks with needs_repair=false resume under the engine; they do not need cloned
10
+ repair tasks. Repairs are for actual defects or prior side effects requiring
11
+ explicit handling. Read the preserved planning_errors before choosing repairs.
12
+ Account for partial changes and failed evidence before planning repairs.
13
+ Respect the supplied remaining new-task budget; pending existing workers and
14
+ verifiers still need agent runs. A work repair plus its verifier consumes two
15
+ MORE runs; replacing four good inspectors with copies wastes eight or more.
16
+
17
+ {"type":"dependencies","task_id":"...","dependencies":["..."],"reason":"..."}
18
+ {"type":"invalidate","task_id":"...","reason":"..."}
19
+ {"type":"repair","task_id":"failed_task","replacement":"new_repair_task","reason":"..."}
20
+
21
+ Updates invalidate affected outputs and their successors. An invalidated task
22
+ which already wrote files or ran commands needs explicit repair; it is never
23
+ replayed automatically. Publish a new repair work task accounting for existing
24
+ changes, then use repair to substitute its independently verified output. The
25
+ original acceptance criteria remain mandatory, without repeating original
26
+ side effects. An explicit verification task keeps its original dependencies
27
+ and must execute its own checks again after the repair.
28
+ End the recovery plan with {"type":"done"}.
@@ -0,0 +1,25 @@
1
+ export type AgentId = "claude" | "codex" | "opencode";
2
+ export interface AgentSessionMeta {
3
+ id: string;
4
+ title: string;
5
+ created_at: string;
6
+ updated_at: string;
7
+ model: string;
8
+ }
9
+ export interface AgentMessage {
10
+ role: "user" | "assistant";
11
+ content: string;
12
+ }
13
+ export interface AgentSession extends AgentSessionMeta {
14
+ cwd: string;
15
+ messages: AgentMessage[];
16
+ }
17
+ /**
18
+ * Sessions of `agent` that were started in `cwd` (the directory neutrino runs
19
+ * in), newest first. Sessions from other directories are not shown.
20
+ */
21
+ export declare function listAgentSessions(agent: AgentId, cwd?: string): AgentSessionMeta[];
22
+ export declare function loadAgentMessages(agent: AgentId, id: string, cwd?: string): AgentMessage[];
23
+ export declare function safeAgentSessionId(id: string): boolean;
24
+ /** Resolve the source in its project, including older Claude files without cwd. */
25
+ export declare function loadAgentSession(agent: AgentId, id: string, cwd: string): AgentSession | null;
package/dist/app.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ import type { OwnedUI } from "./plugins/host.js";
2
+ import { Screen } from "./terminal/screen.js";
3
+ import { type AppState } from "./state.js";
4
+ import { type ModelInfo, type SessionData } from "./config.js";
5
+ import type { UpdateStatus } from "./update.js";
6
+ import { type BackgroundStatus } from "./background.js";
7
+ export declare class App {
8
+ state: AppState;
9
+ width: number;
10
+ height: number;
11
+ private screens;
12
+ private renderer;
13
+ private input;
14
+ private running;
15
+ private sizeTimer;
16
+ private blinkTimer;
17
+ private cursorTimer;
18
+ private toastTimer;
19
+ private repaintTimer;
20
+ private toast;
21
+ private toastPosition;
22
+ private interruptCount;
23
+ private lastInterruptTime;
24
+ private renderFailures;
25
+ private lastBuffer;
26
+ private contentBuffer;
27
+ private selectionTimer;
28
+ private pressed;
29
+ private pressStart;
30
+ private barDrag;
31
+ private dragScrollTimer;
32
+ private dragScrollDir;
33
+ private dragScrollTicks;
34
+ private pointer;
35
+ private dragScrolled;
36
+ private anchorInLog;
37
+ private selRows;
38
+ private selOffset;
39
+ private resizeHandler;
40
+ private webService;
41
+ private webStarting;
42
+ private terminalBridge;
43
+ private backgroundChanges;
44
+ private webTray;
45
+ /** Where the web UI lives; kept even while it is switched off. */
46
+ private webOptions;
47
+ /** Offer the background-residency prompt when this process mounts the UI. */
48
+ private webPromptBackground;
49
+ /** True while a click is bringing the Web console up, so double clicks are safe. */
50
+ private proOpening;
51
+ /** Same guard for the Web-mode button, which has its own click. */
52
+ private webOpening;
53
+ /** An update offer that arrived before the start screen was mounted. */
54
+ private pendingUpdate;
55
+ constructor();
56
+ private readSize;
57
+ private syncSize;
58
+ private restoreSelection;
59
+ /**
60
+ * Remember where the browser UI should listen. Called at startup even when
61
+ * the console is disabled, so Settings can mount it later without guessing.
62
+ * `backgroundPrompt` is false when the console is meant to run detached.
63
+ */
64
+ configureWeb(port: number, host: string | undefined, auth: boolean, backgroundPrompt?: boolean, tray?: boolean): void;
65
+ /**
66
+ * Adopt a Web console that another process owns. No socket is opened here:
67
+ * the terminal only records the address, so Settings and the Pro button can
68
+ * reach it later.
69
+ */
70
+ attachBackground(status: BackgroundStatus): void;
71
+ /**
72
+ * Mount the browser UI on the given port. Safe to call once; subsequent calls
73
+ * return the already-running server.
74
+ */
75
+ enableWeb(port: number, host?: string, auth?: boolean): Promise<BackgroundStatus>;
76
+ /**
77
+ * The address the console is reachable at right now: the local socket, the
78
+ * detached service, or nothing.
79
+ */
80
+ private currentWebUrl;
81
+ private syncWebStatus;
82
+ /**
83
+ * Start the Web console when it is not up yet. Returns the URL to open.
84
+ */
85
+ private ensureWebConsole;
86
+ /** Change the existing worker's lifetime without moving its conversations. */
87
+ setBackgroundResidency(enabled: boolean): Promise<{
88
+ ok: boolean;
89
+ error?: string;
90
+ }>;
91
+ /**
92
+ * Start or stop the browser UI while the terminal keeps running. Returns a
93
+ * result instead of throwing so a failed bind (usually a port already in use)
94
+ * can be reported inline without an unhandled rejection.
95
+ */
96
+ setWebConsole(enabled: boolean): Promise<{
97
+ ok: boolean;
98
+ error?: string;
99
+ }>;
100
+ /**
101
+ * Open the browser console's Pro workspace, bringing the console up first
102
+ * when it is not running yet. This runs from a mouse handler, so it never
103
+ * throws and never waits on the browser: failures become a toast and the TUI
104
+ * keeps rendering.
105
+ */
106
+ openProMode(): Promise<void>;
107
+ /**
108
+ * Open the browser console's regular workspace, bringing the console up first
109
+ * when it is not running yet. Like openProMode it runs from a mouse handler:
110
+ * it never throws and never waits on the browser.
111
+ */
112
+ openWebMode(): Promise<void>;
113
+ start(): Promise<void>;
114
+ private onResize;
115
+ get top(): Screen | undefined;
116
+ pushScreen(screen: Screen): void;
117
+ popScreen(): void;
118
+ render(selectionOnly?: boolean): void;
119
+ private noteInternalError;
120
+ private renderFrame;
121
+ private renderBuffer;
122
+ /**
123
+ * The selection as it should be *painted*.
124
+ *
125
+ * An end that scrolled out of the log is drawn from the log's own edge: the
126
+ * rows it points at are off screen, and highlighting the header or the input
127
+ * box instead would claim those were selected.
128
+ */
129
+ private displaySelection;
130
+ /**
131
+ * Remember the log rows the current drag covers.
132
+ *
133
+ * Runs on every frame, because a row can only be captured while it is still
134
+ * on screen — after the scroll carries it away, its text is gone.
135
+ */
136
+ private captureSelectionRows;
137
+ /**
138
+ * The current selection expressed in content lines instead of screen rows.
139
+ *
140
+ * Rows under the pointer map straight through the log's scroll offset, and
141
+ * rows outside the log get keys of their own (negative above it, past `total`
142
+ * below it) so a drag that starts on the header or runs into the input box
143
+ * still copies what is actually drawn there. The anchor is the exception: once
144
+ * it belongs to the log it follows its text out of the viewport, which is what
145
+ * lets a selection outlive the frame it started in.
146
+ */
147
+ private lineSelection;
148
+ /**
149
+ * Text covered by the current selection, including the rows a drag scrolled
150
+ * out of the viewport.
151
+ */
152
+ private selectionToText;
153
+ /** Where the mark sits, clamped into the log while a drag is scrolling it. */
154
+ private cursorAt;
155
+ /**
156
+ * Start or stop the auto-scroll that runs while a drag sits on an edge of the
157
+ * log. Only a real drag scrolls: a press that never moved selects nothing, so
158
+ * clicking on the last row stays a plain click.
159
+ */
160
+ private updateDragScroll;
161
+ /**
162
+ * One auto-scroll step: move the log a line (a few lines, the longer the
163
+ * pointer is held there) and grow the selection with it.
164
+ */
165
+ private dragScrollTick;
166
+ private stopDragScroll;
167
+ /** Drop everything remembered about a drag; a new gesture starts clean. */
168
+ private resetDragTracking;
169
+ private scheduleSelectionRender;
170
+ private drawToast;
171
+ private updateCursorBlink;
172
+ /** Reset the blink phase so the cursor is solid right after an interaction. */
173
+ private wakeCursor;
174
+ private handleEvent;
175
+ private dispatchEvent;
176
+ private handleMouse;
177
+ private resetMouse;
178
+ private handleInterrupt;
179
+ /**
180
+ * Audible cue for something the user is waiting on.
181
+ *
182
+ * Silent when `sound_alerts` is off, so the bell stays a preference rather
183
+ * than unavoidable noise. A pending approval rings twice: it is the one event
184
+ * that actually blocks the agent, and a single blip is easy to miss.
185
+ */
186
+ bell(kind?: "turn" | "approval"): void;
187
+ showToast(text: string, position?: "top" | "bottom"): void;
188
+ setModelConfig(providerName: string, model: ModelInfo, intensity: string): void;
189
+ startNewChat(text: string): void;
190
+ loadSession(data: SessionData): void;
191
+ /**
192
+ * Offer a newer published release where a session starts.
193
+ *
194
+ * The registry answer arrives a moment after launch. If the user is already
195
+ * in a conversation by then, the notice is dropped rather than breaking into
196
+ * their work: the next start offers it again.
197
+ */
198
+ promptUpdate(status: UpdateStatus): void;
199
+ openModelConfig(): void;
200
+ openHistory(): void;
201
+ openAgentSelect(): void;
202
+ openPlugins(): void;
203
+ openPluginContribution(item: OwnedUI): void;
204
+ openSettings(): void;
205
+ quit(exitCode?: number): void;
206
+ }
package/dist/app.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { PluginsScreen, PluginViewScreen } from "./screens/plugins.js";
2
+ import { applyTuiTheme } from "./plugins/terminal.js";
1
3
  import process from "node:process";
2
4
  import { Buffer } from "./terminal/buffer.js";
3
5
  import { InputReader } from "./terminal/input.js";
@@ -416,6 +418,7 @@ export class App {
416
418
  this.render();
417
419
  }
418
420
  render(selectionOnly = false) {
421
+ applyTuiTheme();
419
422
  if (this.selectionTimer) {
420
423
  clearTimeout(this.selectionTimer);
421
424
  this.selectionTimer = null;
@@ -773,6 +776,10 @@ export class App {
773
776
  return;
774
777
  }
775
778
  this.wakeCursor();
779
+ if (k.ctrl && k.name === "x") {
780
+ this.openPlugins();
781
+ return;
782
+ }
776
783
  if (k.ctrl && (k.name === "m" || k.name === "e")) {
777
784
  this.openModelConfig();
778
785
  return;
@@ -1002,6 +1009,7 @@ export class App {
1002
1009
  screen.loadedApprovalMode = data.approval_mode ?? null;
1003
1010
  screen.loadedPlan = data.plan ?? null;
1004
1011
  screen.loadedTask = data.task_run;
1012
+ screen.loadedFast = data.fast_run;
1005
1013
  screen.loadedAgentMode = data.agent_mode ?? null;
1006
1014
  if (data.provider)
1007
1015
  this.state.selectedProvider = data.provider;
@@ -1047,6 +1055,13 @@ export class App {
1047
1055
  return;
1048
1056
  this.pushScreen(new AgentSelectScreen());
1049
1057
  }
1058
+ openPlugins() { this.pushScreen(new PluginsScreen()); }
1059
+ openPluginContribution(item) {
1060
+ if (item.command)
1061
+ this.startNewChat(`/plugin ${item.pluginId}/${item.command} {}`);
1062
+ else
1063
+ this.pushScreen(new PluginViewScreen(item));
1064
+ }
1050
1065
  openSettings() {
1051
1066
  if (this.screens.some((s) => s instanceof SettingsScreen))
1052
1067
  return;
@@ -0,0 +1,96 @@
1
+ export declare const BACKGROUND_STATUS_FILE: string;
2
+ export declare const BACKGROUND_LOG_FILE: string;
3
+ export interface BackgroundStatus {
4
+ pid: number;
5
+ port: number;
6
+ host: string;
7
+ /** Loopback address the console can be opened at. No trailing slash. */
8
+ url: string;
9
+ /** Whether the console asks for the Web password. */
10
+ auth_required: boolean;
11
+ /** Secret that authorizes the stop/ping endpoints. */
12
+ token: string;
13
+ started_at: string;
14
+ /** Whether the console stays up without a terminal. */
15
+ resident: boolean;
16
+ /** Terminals currently attached to this console. */
17
+ owner_pids: number[];
18
+ /** False for a still-running service from a build without live switching. */
19
+ lifetime_supported?: boolean;
20
+ }
21
+ export interface StartBackgroundOptions {
22
+ /** Requested port, or null to use the saved/default one. */
23
+ port: number | null;
24
+ /** Requested interface; omitted means the saved Web-access setting. */
25
+ host?: string;
26
+ /** Whether the service requires the Web password. */
27
+ auth: boolean;
28
+ /** Windows tray icon. Off for tests and headless deployments. */
29
+ tray?: boolean;
30
+ /** Off means the console follows its owners instead of staying resident. */
31
+ resident?: boolean;
32
+ /** Terminal that owns a non-resident console. */
33
+ ownerPid?: number;
34
+ /** Offer the one-time residency invitation. */
35
+ prompt?: boolean;
36
+ /** An explicit standalone port must not be silently ignored. */
37
+ strictPort?: boolean;
38
+ }
39
+ export interface BackgroundUpdate {
40
+ resident?: boolean;
41
+ ownerPid?: number;
42
+ /** Private loopback bridge for a terminal's live conversations. */
43
+ bridge?: TerminalBridgeInfo;
44
+ }
45
+ export interface TerminalBridgeInfo {
46
+ url: string;
47
+ token: string;
48
+ }
49
+ export type StartBackgroundResult = {
50
+ ok: true;
51
+ status: BackgroundStatus;
52
+ started: boolean;
53
+ } | {
54
+ ok: false;
55
+ error: string;
56
+ };
57
+ export type StopBackgroundResult = {
58
+ ok: true;
59
+ stopped: boolean;
60
+ } | {
61
+ ok: false;
62
+ stopped: boolean;
63
+ error: string;
64
+ };
65
+ /** Validate a status file (or an HTTP response) before trusting it. */
66
+ export declare function parseBackgroundStatus(value: unknown): BackgroundStatus | null;
67
+ /** The saved status, including a stale file from a process that no longer runs. */
68
+ export declare function readBackgroundStatus(): BackgroundStatus | null;
69
+ /** Whether `pid` is a live process. Permission errors still mean "alive". */
70
+ export declare function isProcessAlive(pid: number): boolean;
71
+ /** The running background service, or null when there is none. */
72
+ export declare function runningBackground(): BackgroundStatus | null;
73
+ /** Write the status file atomically so another process never reads half of it. */
74
+ export declare function writeBackgroundStatus(status: BackgroundStatus): void;
75
+ /**
76
+ * Remove the status file. With `pid`, only remove it when it still belongs to
77
+ * that process, so one service can never delete another's registration.
78
+ */
79
+ export declare function clearBackgroundStatus(pid?: number): void;
80
+ /** Append a diagnostic line, rotating the file instead of letting it grow. */
81
+ export declare function backgroundLog(message: string): void;
82
+ /** The last few log lines, for a failed start message. */
83
+ export declare function backgroundLogTail(lines?: number): string;
84
+ /**
85
+ * Start the detached console, or reuse the one already running.
86
+ *
87
+ * The child writes the same status file we poll here, so "the address is
88
+ * reachable" and "the file names this exact process" both have to hold before
89
+ * the start counts as successful. The random token in the env is what proves
90
+ * the file was written by *our* child rather than an older instance.
91
+ */
92
+ export declare function startBackgroundService(options: StartBackgroundOptions): Promise<StartBackgroundResult>;
93
+ /** Apply a lifetime change to the process that already owns the conversations. */
94
+ export declare function configureBackgroundService(status: BackgroundStatus, update: BackgroundUpdate): Promise<BackgroundStatus>;
95
+ /** Stop the detached service, politely first and by signal if it is stuck. */
96
+ export declare function stopBackgroundService(expected?: BackgroundStatus): Promise<StopBackgroundResult>;
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { runPluginCLI } from "./plugins/cli.js";
2
3
  import process from "node:process";
3
4
  import { App } from "./app.js";
4
5
  import { loadConfig } from "./config.js";
5
6
  import { installCrashGuard } from "./terminal/restore.js";
6
- import { webUrls } from "./web/server.js";
7
+ import { startWebServer, webUrls } from "./web/server.js";
7
8
  import { runningBackground, startBackgroundService, stopBackgroundService, } from "./background.js";
8
9
  import { runWebService } from "./web/service.js";
9
10
  import { openInBrowser } from "./browser.js";
@@ -38,8 +39,11 @@ function printHelp() {
38
39
 
39
40
  Usage:
40
41
  neutrino Start the terminal UI (with the web UI).
42
+ neutrino plugin help Manage plugins and inspect the plugin SDK.
43
+ neutrino --safe-mode Start without external plugins.
41
44
  neutrino --lan Start both, reachable from other devices.
42
45
  neutrino web [--port <port>] Start only the web UI (serves saved history).
46
+ neutrino api [--port <port>] Start the local API without a Web listener.
43
47
  neutrino background start Run the web UI in the background; conversations
44
48
  started there survive the terminal/CLI exiting.
45
49
  neutrino background stop Stop the background service.
@@ -60,6 +64,7 @@ Options:
60
64
 
61
65
  Environment:
62
66
  NEUTRINO_WEB_PORT Same as --port in either mode.
67
+ NEUTRINO_API_PORT Loopback programmatic API port (default 8766; 0 for automatic).
63
68
  NEUTRINO_WEB_HOST Same as --host.
64
69
  NEUTRINO_NO_TRAY Set to 1 to keep the background service out of the
65
70
  Windows notification area.
@@ -99,6 +104,9 @@ function parseArgs(argv) {
99
104
  if (arg === "web") {
100
105
  options.mode = "web";
101
106
  }
107
+ else if (arg === "api") {
108
+ options.mode = "api";
109
+ }
102
110
  else if (arg === "background") {
103
111
  options.mode = "background";
104
112
  options.backgroundAction = "status";
@@ -165,6 +173,9 @@ function parseArgs(argv) {
165
173
  throw new Error(`unknown argument "${arg}"`);
166
174
  }
167
175
  }
176
+ if (options.mode === "api" && (options.lan || options.host || options.noWeb || options.noAuth)) {
177
+ throw new Error("the local API always uses authenticated 127.0.0.1 access");
178
+ }
168
179
  if (options.lan && options.host) {
169
180
  throw new Error("use either --lan or --host, not both");
170
181
  }
@@ -230,9 +241,22 @@ function printBackgroundStatus() {
230
241
  process.stdout.write(`Also: ${url}\n`);
231
242
  }
232
243
  }
244
+ const launchArgs = process.argv.slice(2).filter(arg => arg !== "--safe-mode");
245
+ if (process.argv.includes("--safe-mode"))
246
+ process.env.NEUTRINO_SAFE_MODE = "1";
247
+ if (launchArgs[0] === "plugin") {
248
+ try {
249
+ await runPluginCLI(launchArgs.slice(1));
250
+ }
251
+ catch (error) {
252
+ process.stderr.write(`neutrino plugin: ${error instanceof Error ? error.message : String(error)}\n`);
253
+ process.exitCode = 1;
254
+ }
255
+ process.exit(process.exitCode ?? 0);
256
+ }
233
257
  let options;
234
258
  try {
235
- options = parseArgs(process.argv.slice(2));
259
+ options = parseArgs(launchArgs);
236
260
  }
237
261
  catch (err) {
238
262
  process.stderr.write(`neutrino: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -247,7 +271,20 @@ installCrashGuard({ surviveHangup: options.mode === "background" });
247
271
  const host = options.lan ? WILDCARD_HOST : options.host || process.env.NEUTRINO_WEB_HOST || undefined;
248
272
  const auth = !options.noAuth;
249
273
  const portFromEnv = envPort();
250
- if (options.mode === "web") {
274
+ if (options.mode === "api") {
275
+ try {
276
+ const handle = await startWebServer({ port: 0, apiOnly: true, apiPort: options.port ?? undefined, publishApi: true, auth: false });
277
+ process.stdout.write(`Local API listening at ${handle.api.url}; connection details: ${process.env.NEUTRINO_HOME || "~/.neutrino"}/api.json\n`);
278
+ const shutdown = () => { void handle.close().finally(() => process.exit(0)); };
279
+ process.on("SIGINT", shutdown);
280
+ process.on("SIGTERM", shutdown);
281
+ }
282
+ catch (error) {
283
+ process.stderr.write(`neutrino: failed to start local API: ${error instanceof Error ? error.message : String(error)}\n`);
284
+ process.exitCode = 1;
285
+ }
286
+ }
287
+ else if (options.mode === "web") {
251
288
  const result = await startBackgroundService({
252
289
  port: options.port ?? portFromEnv ?? DEFAULT_WEB_PORT, host, auth,
253
290
  tray: !options.noTray, resident: loadConfig().background === true, ownerPid: process.pid,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Where the browser console's Pro workspace lives, given the console's base
3
+ * URL.
4
+ *
5
+ * The base comes straight from the running server, but it is normalised here
6
+ * anyway: a trailing slash must not produce `//pro`, and a URL that already
7
+ * points at `/pro` must not become `/pro/pro`. A missing/blank base yields the
8
+ * path alone instead of throwing, so a half-configured console can still be
9
+ * reported instead of crashing the click that asked for it.
10
+ */
11
+ export declare function proModeUrl(baseUrl: string): string;
12
+ /**
13
+ * Open `url` in the user's default browser. Fire-and-forget: the child is
14
+ * detached, its stdio is ignored and it is unref'd so the terminal is never
15
+ * held open (or blocked) by the browser. Every failure is swallowed — opening
16
+ * a browser is best-effort and must never take the TUI down.
17
+ */
18
+ export declare function openInBrowser(url: string): void;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copy `text` to the system clipboard.
3
+ *
4
+ * Two independent, best-effort strategies are used so the copy works across as
5
+ * many terminals as possible:
6
+ *
7
+ * 1. OSC 52 — a terminal escape sequence that asks the host terminal to put
8
+ * the (base64-encoded) text on the clipboard. This is the only strategy
9
+ * that also works over SSH, and is supported by Windows Terminal, iTerm2,
10
+ * Kitty, WezTerm, Alacritty, …
11
+ * 2. A native helper process (`clip` / `pbcopy` / `wl-copy` / `xclip` / …),
12
+ * as a fallback for terminals that ignore OSC 52.
13
+ *
14
+ * Both are best-effort: any failure is silently ignored.
15
+ */
16
+ export declare function copyToClipboard(text: string): void;
@@ -0,0 +1,29 @@
1
+ export type RGB = [number, number, number];
2
+ export declare const C: {
3
+ readonly accent: RGB;
4
+ readonly highlightBg: RGB;
5
+ readonly black: RGB;
6
+ readonly dim: RGB;
7
+ readonly user: RGB;
8
+ readonly assistant: RGB;
9
+ readonly toolHeader: RGB;
10
+ readonly amber: RGB;
11
+ readonly white: RGB;
12
+ readonly orange: RGB;
13
+ readonly orangeBg: RGB;
14
+ readonly green: RGB;
15
+ readonly red: RGB;
16
+ readonly codeBg: RGB;
17
+ readonly selection: RGB;
18
+ readonly scrollTrack: RGB;
19
+ readonly scrollThumb: RGB;
20
+ };
21
+ export interface Style {
22
+ fg?: RGB;
23
+ bg?: RGB;
24
+ bold?: boolean;
25
+ dim?: boolean;
26
+ inverse?: boolean;
27
+ italic?: boolean;
28
+ }
29
+ export declare const EMPTY_STYLE: Style;