nemoris 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +1,835 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { PassThrough } from "node:stream";
6
+ import packageJson from "../../package.json" with { type: "json" };
7
+ import { parseToml } from "../config/toml-lite.js";
8
+ import {
9
+ ANSI,
10
+ renderAgentResponse,
11
+ renderApprovalPrompt,
12
+ renderStatusBar,
13
+ renderThinking,
14
+ renderToolCard,
15
+ renderWordmark,
16
+ stripAnsi,
17
+ } from "./renderer.js";
18
+ import { TuiSocketClient } from "./socket-client.js";
19
+
20
+ const BRACKETED_PASTE_START = "\x1b[200~";
21
+ const BRACKETED_PASTE_END = "\x1b[201~";
22
+ const CTRL_U = { ctrl: true, name: "u" };
23
+
24
+ export const CHAT_SLASH_COMMANDS = new Set([
25
+ "/",
26
+ "/status",
27
+ "/model",
28
+ "/cost",
29
+ "/clear",
30
+ "/help",
31
+ "/who",
32
+ "/think",
33
+ "/compact",
34
+ "/exec_approve",
35
+ "/exec_deny",
36
+ "/stop",
37
+ ]);
38
+
39
+ function colorsEnabled(options = {}) {
40
+ return !(options.noColor || process.env.NO_COLOR);
41
+ }
42
+
43
+ function promptText(options = {}) {
44
+ const label = "You › ";
45
+ if (!colorsEnabled(options)) {
46
+ return label;
47
+ }
48
+ return `${ANSI.primary}${label}${ANSI.reset}`;
49
+ }
50
+
51
+ function infoText(text, color, options = {}) {
52
+ if (!colorsEnabled(options)) {
53
+ return stripAnsi(text);
54
+ }
55
+ return `${color}${text}${ANSI.reset}`;
56
+ }
57
+
58
+ function normaliseWhitespace(text) {
59
+ return String(text || "").replace(/\s+/g, " ").trim();
60
+ }
61
+
62
+ function parseBooleanApproval(text) {
63
+ const value = String(text || "").trim().toLowerCase();
64
+ if (value === "" || value === "y" || value === "yes") {
65
+ return true;
66
+ }
67
+ if (value === "n" || value === "no") {
68
+ return false;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function keyForTool(jobId, name) {
74
+ return `${jobId || "no-job"}:${name || "tool"}`;
75
+ }
76
+
77
+ function safeJsonStringify(value) {
78
+ try {
79
+ return JSON.stringify(value);
80
+ } catch {
81
+ return JSON.stringify({ error: "Unable to serialize event." });
82
+ }
83
+ }
84
+
85
+ function loadAgentName(projectRoot, agentId) {
86
+ const filePath = path.join(projectRoot, "config", "agents", `${agentId}.toml`);
87
+ try {
88
+ const parsed = parseToml(fs.readFileSync(filePath, "utf8"));
89
+ return parsed.name || parsed.id || agentId;
90
+ } catch {
91
+ return agentId;
92
+ }
93
+ }
94
+
95
+ function clampHistory(entries, maxEntries) {
96
+ return entries.slice(0, maxEntries);
97
+ }
98
+
99
+ function trailingMarkerPrefixLength(input, marker) {
100
+ const maxLength = Math.min(input.length, marker.length - 1);
101
+ for (let length = maxLength; length > 0; length -= 1) {
102
+ if (marker.startsWith(input.slice(-length))) {
103
+ return length;
104
+ }
105
+ }
106
+ return 0;
107
+ }
108
+
109
+ export function parseChatFlags(args = []) {
110
+ const flags = {
111
+ agent: "main",
112
+ model: null,
113
+ noColor: false,
114
+ json: false,
115
+ };
116
+
117
+ for (let index = 0; index < args.length; index += 1) {
118
+ const arg = args[index];
119
+ if (arg === "--agent") {
120
+ flags.agent = args[index + 1] || flags.agent;
121
+ index += 1;
122
+ continue;
123
+ }
124
+ if (arg === "--model") {
125
+ flags.model = args[index + 1] || null;
126
+ index += 1;
127
+ continue;
128
+ }
129
+ if (arg === "--no-color") {
130
+ flags.noColor = true;
131
+ continue;
132
+ }
133
+ if (arg === "--json") {
134
+ flags.json = true;
135
+ }
136
+ }
137
+
138
+ return flags;
139
+ }
140
+
141
+ export function isSlashCommand(input) {
142
+ const trimmed = String(input || "").trim();
143
+ if (!trimmed.startsWith("/")) {
144
+ return false;
145
+ }
146
+ if (trimmed === "/") {
147
+ return true;
148
+ }
149
+ const [command] = trimmed.split(/\s+/, 1);
150
+ return CHAT_SLASH_COMMANDS.has(command) || command.startsWith("/");
151
+ }
152
+
153
+ export function buildOutboundPayload(input, { agent, model } = {}) {
154
+ const text = String(input || "").trim();
155
+ if (!text) {
156
+ return null;
157
+ }
158
+
159
+ if (isSlashCommand(text)) {
160
+ const payload = {
161
+ type: "slash_command",
162
+ command: text,
163
+ text,
164
+ agent,
165
+ };
166
+ if (model) {
167
+ payload.model = model;
168
+ }
169
+ return payload;
170
+ }
171
+
172
+ const payload = {
173
+ type: "user_message",
174
+ text,
175
+ agent,
176
+ };
177
+ if (model) {
178
+ payload.model = model;
179
+ }
180
+ return payload;
181
+ }
182
+
183
+ export class CommandHistory {
184
+ constructor(maxEntries = 100) {
185
+ this.maxEntries = maxEntries;
186
+ this.entries = [];
187
+ this.cursor = -1;
188
+ this.draft = "";
189
+ }
190
+
191
+ add(entry) {
192
+ const value = String(entry || "").trim();
193
+ if (!value) {
194
+ return;
195
+ }
196
+ if (this.entries[0] === value) {
197
+ this.cursor = -1;
198
+ this.draft = "";
199
+ return;
200
+ }
201
+ this.entries.unshift(value);
202
+ this.entries = clampHistory(this.entries, this.maxEntries);
203
+ this.cursor = -1;
204
+ this.draft = "";
205
+ }
206
+
207
+ sync(entries = []) {
208
+ this.entries = clampHistory(
209
+ entries.map((entry) => String(entry || "").trim()).filter(Boolean),
210
+ this.maxEntries,
211
+ );
212
+ this.cursor = -1;
213
+ this.draft = "";
214
+ }
215
+
216
+ previous(currentDraft = "") {
217
+ if (this.entries.length === 0) {
218
+ return currentDraft;
219
+ }
220
+ if (this.cursor === -1) {
221
+ this.draft = currentDraft;
222
+ }
223
+ this.cursor = Math.min(this.cursor + 1, this.entries.length - 1);
224
+ return this.entries[this.cursor];
225
+ }
226
+
227
+ next() {
228
+ if (this.entries.length === 0) {
229
+ return this.draft;
230
+ }
231
+ if (this.cursor <= 0) {
232
+ this.cursor = -1;
233
+ return this.draft;
234
+ }
235
+ this.cursor -= 1;
236
+ return this.entries[this.cursor];
237
+ }
238
+ }
239
+
240
+ class ReadlineInputProxy extends PassThrough {
241
+ constructor(stdin) {
242
+ super();
243
+ this.stdin = stdin;
244
+ this.isTTY = Boolean(stdin?.isTTY);
245
+ this.fd = stdin?.fd;
246
+ }
247
+
248
+ setRawMode(mode) {
249
+ return this.stdin?.setRawMode?.(mode);
250
+ }
251
+
252
+ resume() {
253
+ this.stdin?.resume?.();
254
+ return super.resume();
255
+ }
256
+
257
+ pause() {
258
+ this.stdin?.pause?.();
259
+ return super.pause();
260
+ }
261
+ }
262
+
263
+ class StatusBarController {
264
+ constructor({ stdout = process.stdout, noColor = false } = {}) {
265
+ this.stdout = stdout;
266
+ this.noColor = noColor;
267
+ this.state = null;
268
+ }
269
+
270
+ update(nextState) {
271
+ this.state = nextState;
272
+ this.render();
273
+ }
274
+
275
+ render() {
276
+ if (!this.stdout.isTTY || !this.state) {
277
+ return;
278
+ }
279
+ const line = renderStatusBar(this.state, { noColor: this.noColor });
280
+ const row = Math.max(1, this.stdout.rows || 1);
281
+ this.stdout.write("\x1b7");
282
+ this.stdout.write(`\x1b[${row};1H`);
283
+ this.stdout.write("\x1b[2K");
284
+ this.stdout.write(line);
285
+ this.stdout.write("\x1b8");
286
+ }
287
+
288
+ clear() {
289
+ if (!this.stdout.isTTY) {
290
+ return;
291
+ }
292
+ const row = Math.max(1, this.stdout.rows || 1);
293
+ this.stdout.write("\x1b7");
294
+ this.stdout.write(`\x1b[${row};1H`);
295
+ this.stdout.write("\x1b[2K");
296
+ this.stdout.write("\x1b8");
297
+ }
298
+ }
299
+
300
+ class PromptController {
301
+ constructor({
302
+ stdin = process.stdin,
303
+ stdout = process.stdout,
304
+ noColor = false,
305
+ onCtrlC = async () => {},
306
+ history = new CommandHistory(100),
307
+ } = {}) {
308
+ this.stdin = stdin;
309
+ this.stdout = stdout;
310
+ this.noColor = noColor;
311
+ this.onCtrlC = onCtrlC;
312
+ this.history = history;
313
+ this.proxy = new ReadlineInputProxy(stdin);
314
+ this.rl = createInterface({
315
+ input: this.proxy,
316
+ output: stdout,
317
+ terminal: true,
318
+ historySize: history.maxEntries,
319
+ removeHistoryDuplicates: false,
320
+ });
321
+ this.rl.setPrompt(promptText({ noColor }));
322
+ this.pendingRead = null;
323
+ this.closed = false;
324
+ this.rawBuffer = "";
325
+ this.pasteBuffer = "";
326
+ this.inPaste = false;
327
+
328
+ this._handleRawData = this._handleRawData.bind(this);
329
+ this._handleSigint = this._handleSigint.bind(this);
330
+ this._handleHistory = this._handleHistory.bind(this);
331
+
332
+ this.stdin.setEncoding?.("utf8");
333
+ this.stdin.on("data", this._handleRawData);
334
+ this.rl.on("SIGINT", this._handleSigint);
335
+ this.rl.on("history", this._handleHistory);
336
+
337
+ if (this.stdout.isTTY) {
338
+ this.stdout.write("\x1b[?2004h");
339
+ }
340
+ }
341
+
342
+ _handleHistory(entries) {
343
+ this.history.sync(entries);
344
+ }
345
+
346
+ _handleSigint() {
347
+ Promise.resolve(this.onCtrlC()).catch(() => {});
348
+ this.rl.write(null, CTRL_U);
349
+ this.stdout.write("\n");
350
+ this.refresh();
351
+ }
352
+
353
+ _flushPlainBuffer(preserve = 0) {
354
+ if (this.rawBuffer.length <= preserve) {
355
+ return;
356
+ }
357
+ const chunk = this.rawBuffer.slice(0, this.rawBuffer.length - preserve);
358
+ this.rawBuffer = this.rawBuffer.slice(this.rawBuffer.length - preserve);
359
+ if (chunk) {
360
+ this.proxy.write(chunk);
361
+ }
362
+ }
363
+
364
+ _handleRawData(chunk) {
365
+ if (this.closed) {
366
+ return;
367
+ }
368
+ this.rawBuffer += String(chunk || "");
369
+
370
+ while (this.rawBuffer) {
371
+ if (this.inPaste) {
372
+ const endIndex = this.rawBuffer.indexOf(BRACKETED_PASTE_END);
373
+ if (endIndex === -1) {
374
+ const keep = trailingMarkerPrefixLength(this.rawBuffer, BRACKETED_PASTE_END);
375
+ this.pasteBuffer += this.rawBuffer.slice(0, this.rawBuffer.length - keep);
376
+ this.rawBuffer = this.rawBuffer.slice(this.rawBuffer.length - keep);
377
+ break;
378
+ }
379
+ this.pasteBuffer += this.rawBuffer.slice(0, endIndex);
380
+ this.rawBuffer = this.rawBuffer.slice(endIndex + BRACKETED_PASTE_END.length);
381
+ this.inPaste = false;
382
+ this._completePaste();
383
+ continue;
384
+ }
385
+
386
+ const startIndex = this.rawBuffer.indexOf(BRACKETED_PASTE_START);
387
+ if (startIndex === -1) {
388
+ this._flushPlainBuffer(trailingMarkerPrefixLength(this.rawBuffer, BRACKETED_PASTE_START));
389
+ break;
390
+ }
391
+
392
+ if (startIndex > 0) {
393
+ this.proxy.write(this.rawBuffer.slice(0, startIndex));
394
+ }
395
+ this.rawBuffer = this.rawBuffer.slice(startIndex + BRACKETED_PASTE_START.length);
396
+ this.inPaste = true;
397
+ this.pasteBuffer = "";
398
+ }
399
+ }
400
+
401
+ _completePaste() {
402
+ if (!this.pasteBuffer.includes("\n")) {
403
+ this.proxy.write(this.pasteBuffer);
404
+ this.pasteBuffer = "";
405
+ return;
406
+ }
407
+
408
+ const combined = `${this.rl.line || ""}${this.pasteBuffer}`.trimEnd();
409
+ this.rl.write(null, CTRL_U);
410
+ this.stdout.write("\n");
411
+ this.pasteBuffer = "";
412
+ this._resolvePending(combined);
413
+ }
414
+
415
+ _resolvePending(value) {
416
+ const pending = this.pendingRead;
417
+ if (!pending) {
418
+ return;
419
+ }
420
+ this.pendingRead = null;
421
+ pending.cleanup();
422
+ pending.resolve(value);
423
+ }
424
+
425
+ async readMessage() {
426
+ if (this.closed) {
427
+ return null;
428
+ }
429
+
430
+ return new Promise((resolve) => {
431
+ const onLine = (line) => this._resolvePending(String(line || "").trimEnd());
432
+ const onClose = () => this._resolvePending(null);
433
+ const cleanup = () => {
434
+ this.rl.off("line", onLine);
435
+ this.rl.off("close", onClose);
436
+ };
437
+ this.pendingRead = { resolve, cleanup };
438
+ this.rl.once("line", onLine);
439
+ this.rl.once("close", onClose);
440
+ this.rl.prompt();
441
+ });
442
+ }
443
+
444
+ print(text = "") {
445
+ if (!text) {
446
+ return;
447
+ }
448
+ this.stdout.write("\r\x1b[2K");
449
+ this.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
450
+ this.refresh();
451
+ }
452
+
453
+ beginExternalLine(text = "") {
454
+ this.stdout.write("\r\x1b[2K");
455
+ if (text) {
456
+ this.stdout.write(text);
457
+ }
458
+ }
459
+
460
+ writeInline(text = "") {
461
+ if (!text) {
462
+ return;
463
+ }
464
+ this.stdout.write(text);
465
+ }
466
+
467
+ refresh() {
468
+ if (typeof this.rl._refreshLine === "function") {
469
+ this.rl._refreshLine();
470
+ return;
471
+ }
472
+ this.rl.prompt(true);
473
+ }
474
+
475
+ clearCurrentLine() {
476
+ this.rl.write(null, CTRL_U);
477
+ this.stdout.write("\r\x1b[2K");
478
+ }
479
+
480
+ close() {
481
+ this.closed = true;
482
+ this.stdin.off("data", this._handleRawData);
483
+ this.rl.off("SIGINT", this._handleSigint);
484
+ this.rl.off("history", this._handleHistory);
485
+ if (this.stdout.isTTY) {
486
+ this.stdout.write("\x1b[?2004l");
487
+ }
488
+ this.rl.close();
489
+ this.proxy.end();
490
+ }
491
+ }
492
+
493
+ export function createCtrlCHandler({ client, promptController, statusBar, state } = {}) {
494
+ return async function handleCtrlC() {
495
+ client?.send?.({ type: "abort" });
496
+ if (state) {
497
+ state.thinking = false;
498
+ statusBar?.update?.({
499
+ agent: state.agentId,
500
+ model: state.model,
501
+ tokens: state.tokens,
502
+ cost: state.cost,
503
+ connected: state.connected,
504
+ });
505
+ }
506
+ promptController?.clearCurrentLine?.();
507
+ };
508
+ }
509
+
510
+ function createBannerLine({ agentName, model }) {
511
+ return `nemoris v${packageJson.version} · agent: ${agentName} · model: ${model || "default"}`;
512
+ }
513
+
514
+ function renderEventError(message, options = {}) {
515
+ return infoText(`Error: ${message}`, ANSI.error, options);
516
+ }
517
+
518
+ function renderAgentLabel(agentName, options = {}) {
519
+ return infoText(`${agentName} › `, ANSI.accent, options);
520
+ }
521
+
522
+ export async function runChat({
523
+ projectRoot,
524
+ stateDir,
525
+ agent,
526
+ model,
527
+ noColor = false,
528
+ json = false,
529
+ stdin = process.stdin,
530
+ stdout = process.stdout,
531
+ stderr = process.stderr,
532
+ socketClient,
533
+ sessionId = randomUUID(),
534
+ } = {}) {
535
+ const requestedAgent = agent || "main";
536
+ const requestedModel = model || null;
537
+ const agentName = loadAgentName(projectRoot, requestedAgent);
538
+ const client = socketClient || new TuiSocketClient({ stateDir });
539
+ const history = new CommandHistory(100);
540
+ const statusBar = new StatusBarController({ stdout, noColor });
541
+ const state = {
542
+ agentId: requestedAgent,
543
+ agentName,
544
+ model: requestedModel || "default",
545
+ tokens: 0,
546
+ cost: 0,
547
+ connected: false,
548
+ thinking: false,
549
+ };
550
+ const prompt = new PromptController({
551
+ stdin,
552
+ stdout,
553
+ noColor,
554
+ history,
555
+ onCtrlC: createCtrlCHandler({ client, statusBar, state }),
556
+ });
557
+ const pendingTools = new Map();
558
+ let activeStream = null;
559
+ let pendingApproval = null;
560
+ let bannerShown = false;
561
+ let closed = false;
562
+ let initialStatusResolved = false;
563
+ let spinnerTimer = null;
564
+ let spinnerFrame = 0;
565
+ let resolveInitialStatus = () => {};
566
+ const initialStatus = new Promise((resolve) => {
567
+ resolveInitialStatus = resolve;
568
+ });
569
+
570
+ const refreshStatusBar = () => {
571
+ const payload = {
572
+ agent: state.agentId,
573
+ model: state.model,
574
+ tokens: state.tokens,
575
+ cost: state.cost,
576
+ connected: state.connected,
577
+ };
578
+ if (state.thinking) {
579
+ payload.model = `${state.model} ${stripAnsi(renderThinking(spinnerFrame, { noColor }))}`;
580
+ }
581
+ statusBar.update(payload);
582
+ };
583
+
584
+ const stopSpinner = () => {
585
+ state.thinking = false;
586
+ if (spinnerTimer) {
587
+ clearInterval(spinnerTimer);
588
+ spinnerTimer = null;
589
+ }
590
+ refreshStatusBar();
591
+ };
592
+
593
+ const startSpinner = () => {
594
+ if (spinnerTimer) {
595
+ return;
596
+ }
597
+ state.thinking = true;
598
+ spinnerFrame = 0;
599
+ refreshStatusBar();
600
+ spinnerTimer = setInterval(() => {
601
+ spinnerFrame += 1;
602
+ refreshStatusBar();
603
+ }, 100);
604
+ };
605
+
606
+ const showBanner = () => {
607
+ if (bannerShown || json) {
608
+ return;
609
+ }
610
+ stdout.write(`${renderWordmark({ noColor })}\n`);
611
+ stdout.write(`${infoText(createBannerLine({ agentName: state.agentName, model: state.model }), ANSI.secondary, { noColor })}\n\n`);
612
+ bannerShown = true;
613
+ if (stdout.isTTY) {
614
+ stdout.write("\n");
615
+ }
616
+ refreshStatusBar();
617
+ };
618
+
619
+ const printMessage = (text) => {
620
+ if (json) {
621
+ stdout.write(`${safeJsonStringify({ type: "display", text: stripAnsi(text) })}\n`);
622
+ return;
623
+ }
624
+ prompt.print(text);
625
+ refreshStatusBar();
626
+ };
627
+
628
+ const handleMessage = (message) => {
629
+ if (json) {
630
+ stdout.write(`${safeJsonStringify(message)}\n`);
631
+ return;
632
+ }
633
+
634
+ switch (message.type) {
635
+ case "status":
636
+ state.connected = Boolean(message.connected ?? true);
637
+ state.agentId = message.agent || state.agentId;
638
+ state.agentName = loadAgentName(projectRoot, state.agentId);
639
+ state.model = message.model || state.model;
640
+ state.tokens = Number.isFinite(message.tokens) ? message.tokens : state.tokens;
641
+ state.cost = Number.isFinite(Number(message.cost)) ? Number(message.cost) : state.cost;
642
+ if (!initialStatusResolved) {
643
+ initialStatusResolved = true;
644
+ resolveInitialStatus();
645
+ }
646
+ refreshStatusBar();
647
+ return;
648
+ case "pong":
649
+ refreshStatusBar();
650
+ return;
651
+ case "error":
652
+ stopSpinner();
653
+ printMessage(renderEventError(message.message || "Unknown error.", { noColor }));
654
+ return;
655
+ case "tool_call": {
656
+ stopSpinner();
657
+ pendingTools.set(keyForTool(message.jobId, message.name), {
658
+ name: message.name,
659
+ args: message.args,
660
+ });
661
+ printMessage(renderToolCard({
662
+ name: message.name,
663
+ args: message.args,
664
+ }, { noColor }));
665
+ return;
666
+ }
667
+ case "tool_result": {
668
+ stopSpinner();
669
+ const cached = pendingTools.get(keyForTool(message.jobId, message.name)) || {};
670
+ printMessage(renderToolCard({
671
+ name: message.name,
672
+ args: cached.args,
673
+ result: message.result,
674
+ isError: Boolean(message.is_error),
675
+ durationMs: message.duration_ms,
676
+ }, { noColor }));
677
+ return;
678
+ }
679
+ case "approval_request":
680
+ stopSpinner();
681
+ pendingApproval = message;
682
+ printMessage(renderApprovalPrompt({
683
+ tool: message.tool,
684
+ args: message.args,
685
+ jobId: message.jobId,
686
+ }, { noColor }));
687
+ return;
688
+ case "stream_token":
689
+ stopSpinner();
690
+ if (!activeStream || activeStream.jobId !== message.jobId) {
691
+ activeStream = {
692
+ jobId: message.jobId,
693
+ text: "",
694
+ };
695
+ prompt.beginExternalLine(renderAgentLabel(state.agentName, { noColor }));
696
+ }
697
+ activeStream.text += message.token || "";
698
+ prompt.writeInline(infoText(message.token || "", ANSI.primary, { noColor }));
699
+ refreshStatusBar();
700
+ return;
701
+ case "stream_end":
702
+ stopSpinner();
703
+ state.tokens += Number.isFinite(message.tokens) ? message.tokens : 0;
704
+ state.cost += Number.isFinite(Number(message.cost)) ? Number(message.cost) : 0;
705
+ activeStream = {
706
+ jobId: message.jobId,
707
+ text: activeStream?.text || "",
708
+ complete: true,
709
+ };
710
+ prompt.writeInline("\n");
711
+ prompt.refresh();
712
+ refreshStatusBar();
713
+ return;
714
+ case "agent_response":
715
+ stopSpinner();
716
+ if (
717
+ activeStream &&
718
+ activeStream.jobId === message.jobId &&
719
+ normaliseWhitespace(activeStream.text) === normaliseWhitespace(message.text)
720
+ ) {
721
+ activeStream = null;
722
+ refreshStatusBar();
723
+ return;
724
+ }
725
+ printMessage(`${renderAgentLabel(state.agentName, { noColor })}${renderAgentResponse(message.text || "", { noColor })}`);
726
+ activeStream = null;
727
+ return;
728
+ default:
729
+ printMessage(infoText(safeJsonStringify(message), ANSI.muted, { noColor }));
730
+ }
731
+ };
732
+
733
+ client.on("connected", () => {
734
+ state.connected = true;
735
+ client.send({
736
+ type: "hello",
737
+ sessionId,
738
+ agent: requestedAgent,
739
+ model: requestedModel,
740
+ });
741
+ refreshStatusBar();
742
+ });
743
+
744
+ client.on("disconnected", () => {
745
+ state.connected = false;
746
+ refreshStatusBar();
747
+ if (!closed && !json) {
748
+ printMessage(infoText("Connection lost. Reconnecting...", ANSI.warning, { noColor }));
749
+ }
750
+ });
751
+
752
+ client.on("message", handleMessage);
753
+ client.on("error", (error) => {
754
+ if (closed) {
755
+ return;
756
+ }
757
+ stopSpinner();
758
+ const message = error?.message || String(error);
759
+ if (json) {
760
+ stderr.write(`${safeJsonStringify({ type: "client_error", message })}\n`);
761
+ return;
762
+ }
763
+ printMessage(renderEventError(message, { noColor }));
764
+ });
765
+
766
+ try {
767
+ try {
768
+ await client.connect();
769
+ } catch (connectError) {
770
+ const code = connectError?.code || "";
771
+ if (code === "ENOENT" || code === "ECONNREFUSED") {
772
+ stderr.write("Daemon not running. Start with: nemoris start\n");
773
+ process.exitCode = 1;
774
+ return 1;
775
+ }
776
+ throw connectError;
777
+ }
778
+ await Promise.race([
779
+ initialStatus,
780
+ new Promise((resolve) => setTimeout(resolve, 200)),
781
+ ]);
782
+ showBanner();
783
+
784
+ while (!closed) {
785
+ const line = await prompt.readMessage();
786
+ if (line == null) {
787
+ break;
788
+ }
789
+
790
+ const text = String(line || "").trim();
791
+ if (!text) {
792
+ continue;
793
+ }
794
+
795
+ history.add(text);
796
+
797
+ if (pendingApproval) {
798
+ const approved = parseBooleanApproval(text);
799
+ if (approved != null) {
800
+ client.send({
801
+ type: "approval",
802
+ action: approved ? "approve" : "deny",
803
+ approvalId: pendingApproval.approvalId,
804
+ jobId: pendingApproval.jobId,
805
+ });
806
+ pendingApproval = null;
807
+ continue;
808
+ }
809
+ }
810
+
811
+ const payload = buildOutboundPayload(text, {
812
+ agent: requestedAgent,
813
+ model: requestedModel,
814
+ });
815
+ if (!payload) {
816
+ continue;
817
+ }
818
+
819
+ if (!client.send(payload)) {
820
+ printMessage(renderEventError("Socket is not connected.", { noColor }));
821
+ continue;
822
+ }
823
+
824
+ startSpinner();
825
+ }
826
+ } finally {
827
+ closed = true;
828
+ stopSpinner();
829
+ statusBar.clear();
830
+ prompt.close();
831
+ client.close();
832
+ }
833
+
834
+ return 0;
835
+ }