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,175 @@
1
+ const ANSI_PATTERN = /\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
2
+
3
+ export const ANSI = {
4
+ reset: "\x1b[0m",
5
+ bold: "\x1b[1m",
6
+ dim: "\x1b[2m",
7
+ accent: "\x1b[38;2;45;212;191m",
8
+ primary: "\x1b[38;2;232;232;236m",
9
+ secondary: "\x1b[38;2;161;161;170m",
10
+ muted: "\x1b[38;2;82;82;91m",
11
+ error: "\x1b[38;2;255;95;87m",
12
+ warning: "\x1b[38;2;255;189;46m",
13
+ success: "\x1b[38;2;40;200;64m",
14
+ codeBg: "\x1b[48;2;34;34;39m",
15
+ barBg: "\x1b[48;2;24;24;27m",
16
+ };
17
+
18
+ export const THINKING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
19
+
20
+ function colorsEnabled(options = {}) {
21
+ return !(options.noColor || process.env.NO_COLOR);
22
+ }
23
+
24
+ function stylize(text, codes = [], options = {}) {
25
+ const value = String(text ?? "");
26
+ if (!colorsEnabled(options)) {
27
+ return stripAnsi(value);
28
+ }
29
+ return `${codes.join("")}${value}${ANSI.reset}`;
30
+ }
31
+
32
+ function visibleLength(text) {
33
+ return stripAnsi(text).length;
34
+ }
35
+
36
+ function padVisible(text, width) {
37
+ const pad = Math.max(0, width - visibleLength(text));
38
+ return `${text}${" ".repeat(pad)}`;
39
+ }
40
+
41
+ function stringifyValue(value) {
42
+ if (value == null) {
43
+ return "None";
44
+ }
45
+ if (typeof value === "string") {
46
+ return value;
47
+ }
48
+ try {
49
+ return JSON.stringify(value, null, 2);
50
+ } catch {
51
+ return String(value);
52
+ }
53
+ }
54
+
55
+ function pushField(lines, label, value, labelColor, valueColor, options = {}) {
56
+ const rendered = stringifyValue(value).split("\n");
57
+ if (rendered.length === 0) {
58
+ lines.push(`${stylize(label, [labelColor], options)} ${stylize("None", [valueColor], options)}`);
59
+ return;
60
+ }
61
+ lines.push(`${stylize(label, [labelColor], options)} ${stylize(rendered[0], [valueColor], options)}`);
62
+ for (const line of rendered.slice(1)) {
63
+ lines.push(stylize(` ${line}`, [valueColor], options));
64
+ }
65
+ }
66
+
67
+ function boxify(lines, { borderColor = ANSI.accent, bodyColor = ANSI.primary, options = {} } = {}) {
68
+ const width = Math.max(...lines.map((line) => visibleLength(line)), 0);
69
+ const top = stylize(`┌${"─".repeat(width + 2)}┐`, [borderColor], options);
70
+ const bottom = stylize(`└${"─".repeat(width + 2)}┘`, [borderColor], options);
71
+ const body = lines.map((line) => {
72
+ const padded = padVisible(line, width);
73
+ const content = stylize(` ${padded} `, [bodyColor], options);
74
+ const left = stylize("│", [borderColor], options);
75
+ const right = stylize("│", [borderColor], options);
76
+ return `${left}${content}${right}`;
77
+ });
78
+ return [top, ...body, bottom].join("\n");
79
+ }
80
+
81
+ function renderCodeBlock(content, options = {}) {
82
+ const lines = String(content || "")
83
+ .replace(/\n$/, "")
84
+ .split("\n")
85
+ .map((line) => stylize(` ${line}`, [ANSI.codeBg, ANSI.secondary], options));
86
+ return lines.join("\n");
87
+ }
88
+
89
+ function renderInlineMarkdown(text, options = {}) {
90
+ return String(text || "")
91
+ .replace(/\*\*(.+?)\*\*/g, (_, content) => stylize(content, [ANSI.bold, ANSI.primary], options))
92
+ .replace(/`([^`\n]+)`/g, (_, content) => stylize(` ${content} `, [ANSI.codeBg, ANSI.secondary], options));
93
+ }
94
+
95
+ export function stripAnsi(text) {
96
+ return String(text ?? "").replace(ANSI_PATTERN, "");
97
+ }
98
+
99
+ export function renderAgentResponse(text, options = {}) {
100
+ const blocks = [];
101
+ const withPlaceholders = String(text || "").replace(/```(?:[\w-]+)?\n?([\s\S]*?)```/g, (_, content) => {
102
+ const marker = `__CODE_BLOCK_${blocks.length}__`;
103
+ blocks.push(renderCodeBlock(content, options));
104
+ return marker;
105
+ });
106
+
107
+ let rendered = renderInlineMarkdown(withPlaceholders, options);
108
+ rendered = blocks.reduce(
109
+ (value, block, index) => value.replace(`__CODE_BLOCK_${index}__`, block),
110
+ rendered,
111
+ );
112
+ rendered = rendered
113
+ .split("\n")
114
+ .map((line) => stylize(line, [ANSI.primary], options))
115
+ .join("\n");
116
+
117
+ return colorsEnabled(options) ? rendered : stripAnsi(rendered);
118
+ }
119
+
120
+ export function renderToolCard({ name, args, result, isError, durationMs } = {}, options = {}) {
121
+ const titleColor = isError ? ANSI.error : ANSI.success;
122
+ const lines = [];
123
+ pushField(lines, "Tool", stylize(name || "tool", [ANSI.bold, titleColor], options), ANSI.secondary, ANSI.primary, options);
124
+ pushField(lines, "Args", args, ANSI.secondary, ANSI.primary, options);
125
+
126
+ if (result !== undefined) {
127
+ pushField(lines, "Result", result, ANSI.secondary, isError ? ANSI.error : ANSI.primary, options);
128
+ }
129
+ if (durationMs != null) {
130
+ pushField(lines, "Duration", `${durationMs}ms`, ANSI.secondary, ANSI.primary, options);
131
+ }
132
+
133
+ return boxify(lines, { borderColor: ANSI.accent, bodyColor: ANSI.primary, options });
134
+ }
135
+
136
+ export function renderApprovalPrompt({ tool, args, jobId } = {}, options = {}) {
137
+ const lines = [stylize("Exec approval required", [ANSI.bold, ANSI.warning], options)];
138
+ pushField(lines, "Tool", tool || "tool", ANSI.secondary, ANSI.primary, options);
139
+ pushField(lines, "Args", args, ANSI.secondary, ANSI.primary, options);
140
+ pushField(lines, "Job", jobId || "unknown", ANSI.secondary, ANSI.primary, options);
141
+ lines.push(stylize("[Y/n]", [ANSI.bold, ANSI.warning], options));
142
+ return boxify(lines, { borderColor: ANSI.warning, bodyColor: ANSI.primary, options });
143
+ }
144
+
145
+ export function renderStatusBar({ agent, model, tokens, cost, connected } = {}, options = {}) {
146
+ const tokenCount = Number.isFinite(tokens) ? tokens : 0;
147
+ const numericCost = Number.isFinite(Number(cost)) ? Number(cost) : 0;
148
+ const parts = [
149
+ `agent ${agent || "main"}`,
150
+ `model ${model || "default"}`,
151
+ `tokens ${tokenCount}`,
152
+ `cost $${numericCost.toFixed(4)}`,
153
+ connected ? "online" : "offline",
154
+ ];
155
+ return stylize(` ${parts.join(" · ")} `, [ANSI.barBg, ANSI.muted], options);
156
+ }
157
+
158
+ export function renderThinking(frameIndex = 0, options = {}) {
159
+ const frame = THINKING_FRAMES[Math.abs(frameIndex) % THINKING_FRAMES.length];
160
+ return stylize(frame, [ANSI.accent], options);
161
+ }
162
+
163
+ export function renderWordmark(options = {}) {
164
+ const lines = [
165
+ "███╗ ██╗███████╗███╗ ███╗ ██████╗ ██████╗ ██╗███████╗",
166
+ "████╗ ██║██╔════╝████╗ ████║██╔═══██╗██╔══██╗██║██╔════╝",
167
+ "██╔██╗ ██║█████╗ ██╔████╔██║██║ ██║██████╔╝██║███████╗",
168
+ "██║╚██╗██║██╔══╝ ██║╚██╔╝██║██║ ██║██╔══██╗██║╚════██║",
169
+ "██║ ╚████║███████╗██║ ╚═╝ ██║╚██████╔╝██║ ██║██║███████║",
170
+ "╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝",
171
+ "NEMORIS",
172
+ ];
173
+ const rendered = lines.map((line) => stylize(line, [ANSI.accent], options)).join("\n");
174
+ return colorsEnabled(options) ? rendered : stripAnsi(rendered);
175
+ }
@@ -0,0 +1,217 @@
1
+ import net from "node:net";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { EventEmitter } from "node:events";
5
+
6
+ export function resolveSocketPath({ platform = process.platform, stateDir, homedir = os.homedir } = {}) {
7
+ if (platform === "win32") {
8
+ return "\\\\.\\pipe\\nemoris-daemon";
9
+ }
10
+ const root = stateDir || path.join(homedir(), ".nemoris", "state");
11
+ return path.join(root, "daemon.sock");
12
+ }
13
+
14
+ export function getReconnectDelay(attempt, { baseMs = 1000, maxMs = 30000 } = {}) {
15
+ return Math.min(maxMs, baseMs * (2 ** Math.max(0, attempt)));
16
+ }
17
+
18
+ export function parseNdjsonChunk(buffer = "", chunk = "") {
19
+ const input = `${buffer}${chunk}`;
20
+ const lines = input.split("\n");
21
+ const nextBuffer = lines.pop() || "";
22
+ const messages = [];
23
+ const errors = [];
24
+
25
+ for (const rawLine of lines) {
26
+ const line = rawLine.trim();
27
+ if (!line) {
28
+ continue;
29
+ }
30
+ try {
31
+ messages.push(JSON.parse(line));
32
+ } catch (error) {
33
+ errors.push(error);
34
+ }
35
+ }
36
+
37
+ return { buffer: nextBuffer, messages, errors };
38
+ }
39
+
40
+ function emitSafeError(target, error) {
41
+ if (target.listenerCount("error") > 0) {
42
+ target.emit("error", error);
43
+ }
44
+ }
45
+
46
+ export class TuiSocketClient extends EventEmitter {
47
+ constructor({
48
+ socketPath,
49
+ stateDir,
50
+ platform = process.platform,
51
+ homedir = os.homedir,
52
+ createConnection = net.createConnection,
53
+ reconnectBaseMs = 1000,
54
+ reconnectMaxMs = 30000,
55
+ keepaliveMs = 30000,
56
+ autoReconnect = true,
57
+ setTimeoutFn = setTimeout,
58
+ clearTimeoutFn = clearTimeout,
59
+ setIntervalFn = setInterval,
60
+ clearIntervalFn = clearInterval,
61
+ } = {}) {
62
+ super();
63
+ this.socketPath = socketPath || resolveSocketPath({ platform, stateDir, homedir });
64
+ this.createConnection = createConnection;
65
+ this.reconnectBaseMs = reconnectBaseMs;
66
+ this.reconnectMaxMs = reconnectMaxMs;
67
+ this.keepaliveMs = keepaliveMs;
68
+ this.autoReconnect = autoReconnect;
69
+ this.setTimeoutFn = setTimeoutFn;
70
+ this.clearTimeoutFn = clearTimeoutFn;
71
+ this.setIntervalFn = setIntervalFn;
72
+ this.clearIntervalFn = clearIntervalFn;
73
+
74
+ this.socket = null;
75
+ this.buffer = "";
76
+ this.connected = false;
77
+ this.closed = false;
78
+ this.reconnectAttempt = 0;
79
+ this.reconnectTimer = null;
80
+ this.keepaliveTimer = null;
81
+ }
82
+
83
+ async connect() {
84
+ this.closed = false;
85
+ return new Promise((resolve, reject) => {
86
+ let settled = false;
87
+ const cleanup = () => {
88
+ this.off("connected", onConnected);
89
+ this.off("error", onError);
90
+ };
91
+ const onConnected = () => {
92
+ if (settled) {
93
+ return;
94
+ }
95
+ settled = true;
96
+ cleanup();
97
+ resolve(this);
98
+ };
99
+ const onError = (error) => {
100
+ if (settled) {
101
+ return;
102
+ }
103
+ settled = true;
104
+ cleanup();
105
+ reject(error);
106
+ };
107
+ this.once("connected", onConnected);
108
+ this.once("error", onError);
109
+ this._openSocket();
110
+ });
111
+ }
112
+
113
+ _openSocket() {
114
+ if (this.closed) {
115
+ return;
116
+ }
117
+ this.buffer = "";
118
+ const socket = this.createConnection(this.socketPath);
119
+ this.socket = socket;
120
+ socket.setEncoding?.("utf8");
121
+
122
+ socket.on("connect", () => {
123
+ this.connected = true;
124
+ this.reconnectAttempt = 0;
125
+ this._clearReconnectTimer();
126
+ this._startKeepalive();
127
+ this.emit("connected");
128
+ });
129
+
130
+ socket.on("data", (chunk) => {
131
+ const parsed = parseNdjsonChunk(this.buffer, chunk);
132
+ this.buffer = parsed.buffer;
133
+ for (const message of parsed.messages) {
134
+ this.emit("message", message);
135
+ }
136
+ for (const error of parsed.errors) {
137
+ emitSafeError(this, error);
138
+ }
139
+ });
140
+
141
+ socket.on("error", (error) => {
142
+ emitSafeError(this, error);
143
+ });
144
+
145
+ socket.on("close", () => {
146
+ const wasConnected = this.connected;
147
+ this.connected = false;
148
+ this._stopKeepalive();
149
+ this.emit("disconnected");
150
+ if (!this.closed && this.autoReconnect) {
151
+ this._scheduleReconnect();
152
+ }
153
+ if (!wasConnected && this.closed) {
154
+ this.buffer = "";
155
+ }
156
+ });
157
+ }
158
+
159
+ _scheduleReconnect() {
160
+ if (this.reconnectTimer || this.closed) {
161
+ return null;
162
+ }
163
+ const delay = getReconnectDelay(this.reconnectAttempt, {
164
+ baseMs: this.reconnectBaseMs,
165
+ maxMs: this.reconnectMaxMs,
166
+ });
167
+ this.reconnectAttempt += 1;
168
+ this.reconnectTimer = this.setTimeoutFn(() => {
169
+ this.reconnectTimer = null;
170
+ this._openSocket();
171
+ }, delay);
172
+ return delay;
173
+ }
174
+
175
+ _clearReconnectTimer() {
176
+ if (!this.reconnectTimer) {
177
+ return;
178
+ }
179
+ this.clearTimeoutFn(this.reconnectTimer);
180
+ this.reconnectTimer = null;
181
+ }
182
+
183
+ _startKeepalive() {
184
+ this._stopKeepalive();
185
+ this.keepaliveTimer = this.setIntervalFn(() => {
186
+ this.send({ type: "ping" });
187
+ }, this.keepaliveMs);
188
+ }
189
+
190
+ _stopKeepalive() {
191
+ if (!this.keepaliveTimer) {
192
+ return;
193
+ }
194
+ this.clearIntervalFn(this.keepaliveTimer);
195
+ this.keepaliveTimer = null;
196
+ }
197
+
198
+ send(payload) {
199
+ if (!this.socket || this.socket.destroyed || !this.socket.writable) {
200
+ return false;
201
+ }
202
+ this.socket.write(`${JSON.stringify(payload)}\n`);
203
+ return true;
204
+ }
205
+
206
+ close() {
207
+ this.closed = true;
208
+ this.connected = false;
209
+ this._clearReconnectTimer();
210
+ this._stopKeepalive();
211
+ if (this.socket && !this.socket.destroyed) {
212
+ this.socket.end();
213
+ this.socket.destroy();
214
+ }
215
+ this.socket = null;
216
+ }
217
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Canonical JSON serializer — deterministic, stable output.
3
+ *
4
+ * Rules:
5
+ * - Object keys sorted lexicographically (recursive)
6
+ * - No insignificant whitespace
7
+ * - Stable number formatting (JSON.stringify default)
8
+ * - null, boolean, string, number pass through normally
9
+ * - UTF-8 (Node default)
10
+ */
11
+
12
+ export function canonicalJson(value) {
13
+ if (value === null || value === undefined) return "null";
14
+ if (typeof value === "boolean" || typeof value === "number") return JSON.stringify(value);
15
+ if (typeof value === "string") return JSON.stringify(value);
16
+
17
+ if (Array.isArray(value)) {
18
+ const items = value.map((item) => canonicalJson(item));
19
+ return "[" + items.join(",") + "]";
20
+ }
21
+
22
+ if (typeof value === "object") {
23
+ const keys = Object.keys(value).sort();
24
+ const pairs = keys.map((key) => JSON.stringify(key) + ":" + canonicalJson(value[key]));
25
+ return "{" + pairs.join(",") + "}";
26
+ }
27
+
28
+ return JSON.stringify(value);
29
+ }
@@ -0,0 +1,30 @@
1
+ export const COMPACTION_NOTICE_TEXT = "Compressing context to stay within limits — resuming in a moment…";
2
+
3
+ export function resolveCompactionThreshold(agentConfig) {
4
+ return agentConfig?.limits?.compaction_threshold_turns ?? 20;
5
+ }
6
+
7
+ export function resolveCompactionCondensedFanout(agentConfig) {
8
+ return agentConfig?.limits?.compaction_condensed_fanout ?? 4;
9
+ }
10
+
11
+ export function parseConversationTurns(contextJson) {
12
+ if (!contextJson) return [];
13
+
14
+ try {
15
+ const turns = JSON.parse(contextJson);
16
+ return Array.isArray(turns) ? turns : [];
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ export function shouldCompactConversation(contextJson, agentConfig) {
23
+ return parseConversationTurns(contextJson).length >= resolveCompactionThreshold(agentConfig);
24
+ }
25
+
26
+ export async function notifyCompaction(sendFn, chatId) {
27
+ if (!sendFn || !chatId) return false;
28
+ await sendFn(chatId, COMPACTION_NOTICE_TEXT);
29
+ return true;
30
+ }
@@ -0,0 +1,5 @@
1
+ import path from "node:path";
2
+
3
+ export function resolveEnvPath(srcDir) {
4
+ return path.resolve(srcDir, "..", ".env");
5
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared error formatting utility for Nemoris V2.
3
+ * Provides consistent, structured error output across all modules.
4
+ */
5
+
6
+ const VALID_CATEGORIES = new Set([
7
+ "config",
8
+ "provider",
9
+ "memory",
10
+ "delivery",
11
+ "security",
12
+ "execution"
13
+ ]);
14
+
15
+ export class RuntimeError extends Error {
16
+ /**
17
+ * @param {string} message
18
+ * @param {object} options
19
+ * @param {string} options.category - one of: config, provider, memory, delivery, security, execution
20
+ * @param {object} [options.context] - key-value context for diagnostics
21
+ * @param {boolean} [options.recoverable] - whether the caller can retry/recover
22
+ */
23
+ constructor(message, { category, context = {}, recoverable = false } = {}) {
24
+ super(message);
25
+ this.name = "RuntimeError";
26
+ this.category = VALID_CATEGORIES.has(category) ? category : "execution";
27
+ this.context = context;
28
+ this.recoverable = recoverable;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Format any error into a consistent Nemoris error string.
34
+ *
35
+ * Output format:
36
+ * [Nemoris] <category>: <message>
37
+ * context: <key=value pairs>
38
+ * source: <file:line if available>
39
+ *
40
+ * @param {Error|RuntimeError} error
41
+ * @param {object} [context] - additional context merged with error.context
42
+ * @returns {string}
43
+ */
44
+ export function formatRuntimeError(error, context = {}) {
45
+ const category = error?.category || "execution";
46
+ const message = error?.message || String(error);
47
+
48
+ const mergedContext = { ...(error?.context || {}), ...context };
49
+ const contextEntries = Object.entries(mergedContext);
50
+ const contextLine = contextEntries.length
51
+ ? contextEntries.map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`).join(", ")
52
+ : "none";
53
+
54
+ const source = extractSource(error);
55
+
56
+ const lines = [`[Nemoris] ${category}: ${message}`];
57
+ lines.push(` context: ${contextLine}`);
58
+ lines.push(` source: ${source}`);
59
+
60
+ return lines.join("\n");
61
+ }
62
+
63
+ function extractSource(error) {
64
+ if (!error?.stack) return "unknown";
65
+ const lines = error.stack.split("\n");
66
+ // Find first stack frame that is not this file
67
+ for (const line of lines) {
68
+ const match = line.match(/at\s+.+?\((.+:\d+:\d+)\)/);
69
+ if (match && !match[1].includes("utils/errors.js")) {
70
+ return match[1];
71
+ }
72
+ const matchNoParens = line.match(/at\s+(.+:\d+:\d+)$/);
73
+ if (matchNoParens && !matchNoParens[1].includes("utils/errors.js")) {
74
+ return matchNoParens[1];
75
+ }
76
+ }
77
+ return "unknown";
78
+ }
79
+
80
+ export { VALID_CATEGORIES };
@@ -0,0 +1,101 @@
1
+ import { mkdir, readFile, writeFile, appendFile, readdir, rm, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function ensureDir(dirPath) {
5
+ await mkdir(dirPath, { recursive: true });
6
+ }
7
+
8
+ export async function readJson(filePath, fallback = null) {
9
+ try {
10
+ const raw = await readFile(filePath, "utf8");
11
+ return JSON.parse(raw);
12
+ } catch (error) {
13
+ if (error.code === "ENOENT") return fallback;
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ export async function readText(filePath, fallback = null) {
19
+ try {
20
+ return await readFile(filePath, "utf8");
21
+ } catch (error) {
22
+ if (error.code === "ENOENT") return fallback;
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ export async function writeJson(filePath, value) {
28
+ await ensureDir(path.dirname(filePath));
29
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
30
+ }
31
+
32
+ export async function appendJsonLine(filePath, value) {
33
+ await ensureDir(path.dirname(filePath));
34
+ await appendFile(filePath, `${JSON.stringify(value)}\n`, "utf8");
35
+ }
36
+
37
+ export async function readJsonLines(filePath) {
38
+ try {
39
+ const raw = await readFile(filePath, "utf8");
40
+ return raw
41
+ .split("\n")
42
+ .map((line) => line.trim())
43
+ .filter(Boolean)
44
+ .map((line) => JSON.parse(line));
45
+ } catch (error) {
46
+ if (error.code === "ENOENT") return [];
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ export async function listFiles(dirPath) {
52
+ try {
53
+ return await readdir(dirPath);
54
+ } catch (error) {
55
+ if (error.code === "ENOENT") return [];
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ export async function listFilesRecursive(dirPath) {
61
+ const results = [];
62
+
63
+ async function walk(currentPath) {
64
+ let entries;
65
+ try {
66
+ entries = await readdir(currentPath, { withFileTypes: true });
67
+ } catch (error) {
68
+ if (error.code === "ENOENT") return;
69
+ throw error;
70
+ }
71
+
72
+ for (const entry of entries) {
73
+ const fullPath = path.join(currentPath, entry.name);
74
+ if (entry.isDirectory()) {
75
+ await walk(fullPath);
76
+ } else {
77
+ results.push(fullPath);
78
+ }
79
+ }
80
+ }
81
+
82
+ await walk(dirPath);
83
+ return results;
84
+ }
85
+
86
+ export async function resetDir(dirPath) {
87
+ await rm(dirPath, { recursive: true, force: true });
88
+ }
89
+
90
+ export async function removePath(targetPath) {
91
+ await rm(targetPath, { recursive: true, force: true });
92
+ }
93
+
94
+ export async function statPath(targetPath) {
95
+ try {
96
+ return await stat(targetPath);
97
+ } catch (error) {
98
+ if (error.code === "ENOENT") return null;
99
+ throw error;
100
+ }
101
+ }
@@ -0,0 +1,5 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export function createRuntimeId(prefix = "id") {
4
+ return `${prefix}:${crypto.randomUUID()}`;
5
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Model context window and output token limits by model ID.
3
+ */
4
+
5
+ /**
6
+ * Return the context window size for a given model ID.
7
+ * @param {string} modelId
8
+ * @returns {number}
9
+ */
10
+ export function getContextLimit(modelId) {
11
+ const id = String(modelId || "").toLowerCase();
12
+ if (id.includes("opus") || id.includes("sonnet") || id.includes("haiku")) return 200000;
13
+ if (id.includes("gpt-4")) return 128000;
14
+ if (id.includes("gpt-3.5")) return 16000;
15
+ return 100000;
16
+ }
17
+
18
+ /**
19
+ * Return the max output tokens for a given model ID.
20
+ * @param {string} modelId
21
+ * @returns {number}
22
+ */
23
+ export function getMaxOutputTokens(modelId) {
24
+ const id = String(modelId || "").toLowerCase();
25
+ if (id.includes("anthropic") || id.includes("claude") || id.includes("opus") ||
26
+ id.includes("sonnet") || id.includes("haiku")) {
27
+ return 8192;
28
+ }
29
+ return 4096;
30
+ }