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.
- package/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
package/src/utils/errors.js
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/usage-cost.js
CHANGED
|
@@ -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
|
+
}
|