nemoris 0.1.0 → 0.1.2

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 (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,80 +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 };
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 };
package/src/utils/fs.js CHANGED
@@ -1,101 +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
- }
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
+ }
package/src/utils/ids.js CHANGED
@@ -1,5 +1,5 @@
1
- import crypto from "node:crypto";
2
-
3
- export function createRuntimeId(prefix = "id") {
4
- return `${prefix}:${crypto.randomUUID()}`;
5
- }
1
+ import crypto from "node:crypto";
2
+
3
+ export function createRuntimeId(prefix = "id") {
4
+ return `${prefix}:${crypto.randomUUID()}`;
5
+ }
@@ -1,30 +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
- }
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
+ }
@@ -1,74 +1,74 @@
1
- /**
2
- * Token budget utilities — rough estimation and context pruning.
3
- * Uses character-count heuristic (length / 4) to avoid tiktoken dependency.
4
- */
5
-
6
- /**
7
- * Estimate token count for an array of messages.
8
- * @param {Array} messages
9
- * @returns {number}
10
- */
11
- export function estimateTokens(messages) {
12
- return Math.ceil(JSON.stringify(messages).length / 4);
13
- }
14
-
15
- /**
16
- * Prune stale tool_use + tool_result pairs from the start of a message list
17
- * when the estimated token count exceeds 80% of maxContextTokens.
18
- * Always preserves the last 4 turns.
19
- *
20
- * @param {Array<{role: string, content: any}>} messages
21
- * @param {number} maxContextTokens
22
- * @returns {{ pruned: Array, removed: number, estimatedTokens: number }}
23
- */
24
- export function pruneContext(messages, maxContextTokens) {
25
- const threshold = maxContextTokens * 0.8;
26
-
27
- if (estimateTokens(messages) <= threshold) {
28
- return { pruned: messages, removed: 0, estimatedTokens: estimateTokens(messages) };
29
- }
30
-
31
- // Identify tool_use + tool_result pair indices, from oldest to newest.
32
- // A pair is: assistant turn with tool_use content followed immediately by
33
- // user turn with tool_result content.
34
- const pairs = [];
35
- for (let i = 0; i < messages.length - 1; i++) {
36
- const msg = messages[i];
37
- const next = messages[i + 1];
38
- if (msg.role === "assistant" && isToolUse(msg.content) &&
39
- next.role === "user" && isToolResult(next.content)) {
40
- pairs.push([i, i + 1]);
41
- }
42
- }
43
-
44
- const protectedStart = Math.max(0, messages.length - 4);
45
- // Only consider pairs where both indices are before the protected zone
46
- const pruneable = pairs.filter(([a, b]) => a < protectedStart && b < protectedStart);
47
-
48
- let working = [...messages];
49
- let removed = 0;
50
-
51
- for (const [a, b] of pruneable) {
52
- if (estimateTokens(working) <= threshold) break;
53
- // Adjust indices for previously removed items
54
- const adj = a - removed * 2;
55
- working.splice(adj, 2);
56
- removed++;
57
- }
58
-
59
- return { pruned: working, removed, estimatedTokens: estimateTokens(working) };
60
- }
61
-
62
- function isToolUse(content) {
63
- if (Array.isArray(content)) {
64
- return content.some((c) => c.type === "tool_use");
65
- }
66
- return false;
67
- }
68
-
69
- function isToolResult(content) {
70
- if (Array.isArray(content)) {
71
- return content.some((c) => c.type === "tool_result");
72
- }
73
- return false;
74
- }
1
+ /**
2
+ * Token budget utilities — rough estimation and context pruning.
3
+ * Uses character-count heuristic (length / 4) to avoid tiktoken dependency.
4
+ */
5
+
6
+ /**
7
+ * Estimate token count for an array of messages.
8
+ * @param {Array} messages
9
+ * @returns {number}
10
+ */
11
+ export function estimateTokens(messages) {
12
+ return Math.ceil(JSON.stringify(messages).length / 4);
13
+ }
14
+
15
+ /**
16
+ * Prune stale tool_use + tool_result pairs from the start of a message list
17
+ * when the estimated token count exceeds 80% of maxContextTokens.
18
+ * Always preserves the last 4 turns.
19
+ *
20
+ * @param {Array<{role: string, content: any}>} messages
21
+ * @param {number} maxContextTokens
22
+ * @returns {{ pruned: Array, removed: number, estimatedTokens: number }}
23
+ */
24
+ export function pruneContext(messages, maxContextTokens) {
25
+ const threshold = maxContextTokens * 0.8;
26
+
27
+ if (estimateTokens(messages) <= threshold) {
28
+ return { pruned: messages, removed: 0, estimatedTokens: estimateTokens(messages) };
29
+ }
30
+
31
+ // Identify tool_use + tool_result pair indices, from oldest to newest.
32
+ // A pair is: assistant turn with tool_use content followed immediately by
33
+ // user turn with tool_result content.
34
+ const pairs = [];
35
+ for (let i = 0; i < messages.length - 1; i++) {
36
+ const msg = messages[i];
37
+ const next = messages[i + 1];
38
+ if (msg.role === "assistant" && isToolUse(msg.content) &&
39
+ next.role === "user" && isToolResult(next.content)) {
40
+ pairs.push([i, i + 1]);
41
+ }
42
+ }
43
+
44
+ const protectedStart = Math.max(0, messages.length - 4);
45
+ // Only consider pairs where both indices are before the protected zone
46
+ const pruneable = pairs.filter(([a, b]) => a < protectedStart && b < protectedStart);
47
+
48
+ let working = [...messages];
49
+ let removed = 0;
50
+
51
+ for (const [a, b] of pruneable) {
52
+ if (estimateTokens(working) <= threshold) break;
53
+ // Adjust indices for previously removed items
54
+ const adj = a - removed * 2;
55
+ working.splice(adj, 2);
56
+ removed++;
57
+ }
58
+
59
+ return { pruned: working, removed, estimatedTokens: estimateTokens(working) };
60
+ }
61
+
62
+ function isToolUse(content) {
63
+ if (Array.isArray(content)) {
64
+ return content.some((c) => c.type === "tool_use");
65
+ }
66
+ return false;
67
+ }
68
+
69
+ function isToolResult(content) {
70
+ if (Array.isArray(content)) {
71
+ return content.some((c) => c.type === "tool_result");
72
+ }
73
+ return false;
74
+ }
@@ -1,25 +1,25 @@
1
- // Rough cost per 1M tokens (USD) — update as pricing changes
2
- const COSTS = {
3
- "anthropic/claude-sonnet-4-6": { in: 3.00, out: 15.00 },
4
- "anthropic/claude-haiku-4-5": { in: 0.80, out: 4.00 },
5
- "anthropic/claude-opus-4-6": { in: 15.00, out: 75.00 },
6
- "openrouter/anthropic/claude-sonnet-4-6": { in: 3.00, out: 15.00 },
7
- "openrouter/anthropic/claude-haiku-4-5": { in: 0.80, out: 4.00 },
8
- "openrouter/anthropic/claude-opus-4-6": { in: 15.00, out: 75.00 },
9
- };
10
-
11
- /**
12
- * Estimate the cost of an LLM turn in USD.
13
- * @param {string} model The model ID (e.g. "anthropic/claude-sonnet-4-6")
14
- * @param {number} tokensIn Input tokens
15
- * @param {number} tokensOut Output tokens
16
- * @returns {number|null} Estimated cost in USD, or null if model unknown
17
- */
18
- export function estimateCost(model, tokensIn, tokensOut) {
19
- if (typeof model === "string" && model.startsWith("ollama/")) {
20
- return 0;
21
- }
22
- const rates = COSTS[model];
23
- if (!rates) return null; // Unknown or unpriced remote models are intentionally left unresolved.
24
- return (tokensIn / 1_000_000) * rates.in + (tokensOut / 1_000_000) * rates.out;
25
- }
1
+ // Rough cost per 1M tokens (USD) — update as pricing changes
2
+ const COSTS = {
3
+ "anthropic/claude-sonnet-4-6": { in: 3.00, out: 15.00 },
4
+ "anthropic/claude-haiku-4-5": { in: 0.80, out: 4.00 },
5
+ "anthropic/claude-opus-4-6": { in: 15.00, out: 75.00 },
6
+ "openrouter/anthropic/claude-sonnet-4-6": { in: 3.00, out: 15.00 },
7
+ "openrouter/anthropic/claude-haiku-4-5": { in: 0.80, out: 4.00 },
8
+ "openrouter/anthropic/claude-opus-4-6": { in: 15.00, out: 75.00 },
9
+ };
10
+
11
+ /**
12
+ * Estimate the cost of an LLM turn in USD.
13
+ * @param {string} model The model ID (e.g. "anthropic/claude-sonnet-4-6")
14
+ * @param {number} tokensIn Input tokens
15
+ * @param {number} tokensOut Output tokens
16
+ * @returns {number|null} Estimated cost in USD, or null if model unknown
17
+ */
18
+ export function estimateCost(model, tokensIn, tokensOut) {
19
+ if (typeof model === "string" && model.startsWith("ollama/")) {
20
+ return 0;
21
+ }
22
+ const rates = COSTS[model];
23
+ if (!rates) return null; // Unknown or unpriced remote models are intentionally left unresolved.
24
+ return (tokensIn / 1_000_000) * rates.in + (tokensOut / 1_000_000) * rates.out;
25
+ }
@@ -1,14 +1,14 @@
1
- export function extractUsageMetrics(rawResponse) {
2
- return {
3
- tokensIn: rawResponse?.usage?.input_tokens
4
- || rawResponse?.usage?.prompt_tokens
5
- || 0,
6
- tokensOut: rawResponse?.usage?.output_tokens
7
- || rawResponse?.usage?.completion_tokens
8
- || 0,
9
- cacheIn: rawResponse?.usage?.cache_read_input_tokens
10
- || rawResponse?.usage?.prompt_tokens_details?.cached_tokens
11
- || 0,
12
- cacheCreation: rawResponse?.usage?.cache_creation_input_tokens || 0,
13
- };
14
- }
1
+ export function extractUsageMetrics(rawResponse) {
2
+ return {
3
+ tokensIn: rawResponse?.usage?.input_tokens
4
+ || rawResponse?.usage?.prompt_tokens
5
+ || 0,
6
+ tokensOut: rawResponse?.usage?.output_tokens
7
+ || rawResponse?.usage?.completion_tokens
8
+ || 0,
9
+ cacheIn: rawResponse?.usage?.cache_read_input_tokens
10
+ || rawResponse?.usage?.prompt_tokens_details?.cached_tokens
11
+ || 0,
12
+ cacheCreation: rawResponse?.usage?.cache_creation_input_tokens || 0,
13
+ };
14
+ }