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.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/skills/loader.js
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseToml } from "../config/toml-lite.js";
|
|
5
|
+
|
|
6
|
+
export class SkillsLoader {
|
|
7
|
+
#skills = new Map();
|
|
8
|
+
|
|
9
|
+
async loadSkills(configDir) {
|
|
10
|
+
try {
|
|
11
|
+
const files = await readdir(configDir);
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
if (!file.endsWith(".toml")) continue;
|
|
14
|
+
const content = await readFile(join(configDir, file), "utf8");
|
|
15
|
+
const parsed = parseToml(content);
|
|
16
|
+
const skill = extractSkill(parsed);
|
|
17
|
+
if (!skill.id) continue;
|
|
18
|
+
this.#skills.set(skill.id, skill);
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code !== "ENOENT") throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resolve(skillId) {
|
|
26
|
+
return this.#skills.get(skillId) ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isAgentAllowed(skillId, agentId) {
|
|
30
|
+
const skill = this.#skills.get(skillId);
|
|
31
|
+
if (!skill) return false;
|
|
32
|
+
return skill.agent_scope.includes(agentId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
listAll() {
|
|
36
|
+
return [...this.#skills.entries()];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
compose(skillIds) {
|
|
40
|
+
const prompts = [];
|
|
41
|
+
const allTools = new Set();
|
|
42
|
+
let minMaxTokens = Infinity;
|
|
43
|
+
let minMaxToolCalls = Infinity;
|
|
44
|
+
|
|
45
|
+
for (const id of skillIds) {
|
|
46
|
+
const skill = this.#skills.get(id);
|
|
47
|
+
if (!skill) continue;
|
|
48
|
+
prompts.push(skill.context.prompt);
|
|
49
|
+
for (const t of skill.tools.required) allTools.add(t);
|
|
50
|
+
for (const t of skill.tools.optional) allTools.add(t);
|
|
51
|
+
if (skill.budget.max_tokens < minMaxTokens) minMaxTokens = skill.budget.max_tokens;
|
|
52
|
+
if (skill.budget.max_tool_calls < minMaxToolCalls) minMaxToolCalls = skill.budget.max_tool_calls;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
prompt: prompts.join("\n\n"),
|
|
57
|
+
tools: [...allTools],
|
|
58
|
+
budget: {
|
|
59
|
+
max_tokens: minMaxTokens === Infinity ? undefined : minMaxTokens,
|
|
60
|
+
max_tool_calls: minMaxToolCalls === Infinity ? undefined : minMaxToolCalls,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractSkill(parsed) {
|
|
67
|
+
const raw = parsed.skill ?? {};
|
|
68
|
+
return {
|
|
69
|
+
id: raw.id ?? null,
|
|
70
|
+
description: raw.description ?? "",
|
|
71
|
+
agent_scope: Array.isArray(raw.agent_scope) ? raw.agent_scope : (raw.agent_scope ? [raw.agent_scope] : []),
|
|
72
|
+
context: {
|
|
73
|
+
prompt: raw.context?.prompt ?? "",
|
|
74
|
+
},
|
|
75
|
+
tools: {
|
|
76
|
+
required: Array.isArray(raw.tools?.required) ? raw.tools.required : (raw.tools?.required ? [raw.tools.required] : []),
|
|
77
|
+
optional: Array.isArray(raw.tools?.optional) ? raw.tools.optional : (raw.tools?.optional ? [raw.tools.optional] : []),
|
|
78
|
+
},
|
|
79
|
+
budget: {
|
|
80
|
+
max_tokens: raw.budget?.max_tokens ?? 8192,
|
|
81
|
+
max_tool_calls: raw.budget?.max_tool_calls ?? 20,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"github": {
|
|
3
|
+
"tier": "mcp",
|
|
4
|
+
"description": "GitHub operations via MCP",
|
|
5
|
+
"command": "npx",
|
|
6
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
7
|
+
"env": { "GITHUB_TOKEN": "env:NEMORIS_GITHUB_TOKEN" },
|
|
8
|
+
"env_prompts": { "NEMORIS_GITHUB_TOKEN": "GitHub personal access token" }
|
|
9
|
+
},
|
|
10
|
+
"supabase": {
|
|
11
|
+
"tier": "mcp",
|
|
12
|
+
"description": "Supabase database and auth operations",
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@supabase/mcp-server"],
|
|
15
|
+
"env": { "SUPABASE_URL": "env:NEMORIS_SUPABASE_URL", "SUPABASE_KEY": "env:NEMORIS_SUPABASE_KEY" },
|
|
16
|
+
"env_prompts": { "NEMORIS_SUPABASE_URL": "Supabase project URL", "NEMORIS_SUPABASE_KEY": "Supabase service role key" }
|
|
17
|
+
},
|
|
18
|
+
"slack": {
|
|
19
|
+
"tier": "mcp",
|
|
20
|
+
"description": "Slack messaging operations",
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@anthropic/mcp-server-slack"],
|
|
23
|
+
"env": { "SLACK_TOKEN": "env:NEMORIS_SLACK_TOKEN" },
|
|
24
|
+
"env_prompts": { "NEMORIS_SLACK_TOKEN": "Slack bot OAuth token" }
|
|
25
|
+
},
|
|
26
|
+
"filesystem": {
|
|
27
|
+
"tier": "mcp",
|
|
28
|
+
"description": "Extended filesystem operations",
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
31
|
+
"env": {}
|
|
32
|
+
},
|
|
33
|
+
"claude_code": {
|
|
34
|
+
"tier": "cli",
|
|
35
|
+
"description": "Delegate complex coding tasks to Claude Code",
|
|
36
|
+
"command": "claude",
|
|
37
|
+
"args": ["-p", "--output-format", "json"],
|
|
38
|
+
"env": {},
|
|
39
|
+
"env_prompts": {}
|
|
40
|
+
},
|
|
41
|
+
"gemini_cli": {
|
|
42
|
+
"tier": "cli",
|
|
43
|
+
"description": "Delegate tasks to Gemini CLI",
|
|
44
|
+
"command": "gemini",
|
|
45
|
+
"args": [],
|
|
46
|
+
"env": {},
|
|
47
|
+
"env_prompts": {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/tools/cli-delegate.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
export function cliDelegate({ command, args = [], input = "", timeout = 120000, env = {} }) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
let stdout = "";
|
|
7
|
+
let stderr = "";
|
|
8
|
+
|
|
9
|
+
const child = spawn(command, args, {
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
env: { ...process.env, ...env },
|
|
12
|
+
// Note: spawn() does not support timeout option — handled manually below
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
16
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
17
|
+
|
|
18
|
+
if (input) {
|
|
19
|
+
child.stdin.write(input);
|
|
20
|
+
child.stdin.end();
|
|
21
|
+
} else {
|
|
22
|
+
child.stdin.end();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
child.kill();
|
|
27
|
+
resolve(`Error: command timed out after ${timeout}ms`);
|
|
28
|
+
}, timeout);
|
|
29
|
+
|
|
30
|
+
child.on("close", (code) => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
if (code === 0) {
|
|
33
|
+
resolve(stdout || "(no output)");
|
|
34
|
+
} else {
|
|
35
|
+
resolve(`Exit ${code}:\n${stderr || stdout || "(no output)"}`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on("error", (err) => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
resolve(`Error: ${err.message}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/tools/mcp-client.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
export class McpClient {
|
|
5
|
+
#process = null;
|
|
6
|
+
#config;
|
|
7
|
+
#requestId = 0;
|
|
8
|
+
#pending = new Map();
|
|
9
|
+
#buffer = "";
|
|
10
|
+
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.#config = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async spawn() {
|
|
16
|
+
if (this.#process) return;
|
|
17
|
+
const { command, args = [], env = {} } = this.#config;
|
|
18
|
+
this.#process = spawn(command, args, {
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
env: { ...process.env, ...env },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
this.#process.stdout.on("data", (data) => {
|
|
24
|
+
this.#buffer += data.toString();
|
|
25
|
+
this.#drainBuffer();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.#process.on("exit", () => {
|
|
29
|
+
this.#process = null;
|
|
30
|
+
for (const [, { reject }] of this.#pending) {
|
|
31
|
+
reject(new Error("MCP server exited unexpectedly"));
|
|
32
|
+
}
|
|
33
|
+
this.#pending.clear();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Send initialize request per MCP protocol
|
|
37
|
+
await this.#send("initialize", {
|
|
38
|
+
protocolVersion: "2024-11-05",
|
|
39
|
+
capabilities: {},
|
|
40
|
+
clientInfo: { name: "nemoris", version: "0.1.0" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Send initialized notification (required by MCP protocol)
|
|
44
|
+
this.#process.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async callTool(name, args) {
|
|
48
|
+
if (!this.#process) throw new Error("MCP server not running");
|
|
49
|
+
const result = await this.#send("tools/call", { name, arguments: args });
|
|
50
|
+
if (result.content && Array.isArray(result.content)) {
|
|
51
|
+
return result.content.map(c => c.text || JSON.stringify(c)).join("\n");
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(result);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async listTools() {
|
|
57
|
+
if (!this.#process) throw new Error("MCP server not running");
|
|
58
|
+
const result = await this.#send("tools/list", {});
|
|
59
|
+
return result.tools || [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isAlive() {
|
|
63
|
+
return this.#process !== null && !this.#process.killed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
kill() {
|
|
67
|
+
if (this.#process) {
|
|
68
|
+
this.#process.kill();
|
|
69
|
+
this.#process = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#send(method, params) {
|
|
74
|
+
const id = ++this.#requestId;
|
|
75
|
+
const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
76
|
+
this.#process.stdin.write(message + "\n");
|
|
77
|
+
|
|
78
|
+
const timeout = this.#config.timeout || 30000;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
this.#pending.delete(id);
|
|
82
|
+
reject(new Error(`MCP request timed out after ${timeout}ms`));
|
|
83
|
+
}, timeout);
|
|
84
|
+
this.#pending.set(id, { resolve: (v) => { clearTimeout(timer); resolve(v); }, reject: (e) => { clearTimeout(timer); reject(e); } });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#drainBuffer() {
|
|
89
|
+
const lines = this.#buffer.split("\n");
|
|
90
|
+
this.#buffer = lines.pop() || "";
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (!line.trim()) continue;
|
|
93
|
+
try {
|
|
94
|
+
const msg = JSON.parse(line);
|
|
95
|
+
if (msg.id && this.#pending.has(msg.id)) {
|
|
96
|
+
const { resolve, reject } = this.#pending.get(msg.id);
|
|
97
|
+
this.#pending.delete(msg.id);
|
|
98
|
+
if (msg.error) reject(new Error(msg.error.message || "MCP error"));
|
|
99
|
+
else resolve(msg.result);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Non-JSON line — ignore
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function executeCancelTask(input, context) {
|
|
2
|
+
const { taskId } = input;
|
|
3
|
+
if (!taskId) throw new Error("taskId is required");
|
|
4
|
+
const task = context.taskContract.transition(taskId, "cancelled", context.callerAgentId);
|
|
5
|
+
return { status: "cancelled", taskId: task.task_id };
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function executeCompleteTask(input, context) {
|
|
2
|
+
const { taskId, result } = input;
|
|
3
|
+
if (!taskId) throw new Error("taskId is required");
|
|
4
|
+
const task = context.taskContract.transition(taskId, "completed", context.callerAgentId, { result });
|
|
5
|
+
return { status: "completed", taskId: task.task_id };
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function executeFailTask(input, context) {
|
|
2
|
+
const { taskId, error } = input;
|
|
3
|
+
if (!taskId) throw new Error("taskId is required");
|
|
4
|
+
const task = context.taskContract.transition(taskId, "failed", context.callerAgentId, { error });
|
|
5
|
+
return { status: "failed", taskId: task.task_id };
|
|
6
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createToolResult } from "../tool-result-contract.js";
|
|
2
|
+
import { inspectOutboundUrl } from "../../security/ssrf-check.js";
|
|
3
|
+
|
|
4
|
+
const BRIEF_CAP = 3000; // chars in brief sent to LLM prompt
|
|
5
|
+
const RAW_CAP = 1048576; // 1MB raw storage cap
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a useful brief from HTTP response content.
|
|
9
|
+
* - JSON arrays → first 20 items, serialised cleanly
|
|
10
|
+
* - JSON objects → pretty-printed (capped)
|
|
11
|
+
* - HTML → basic tag strip to text (capped)
|
|
12
|
+
* - Plain text → capped
|
|
13
|
+
*/
|
|
14
|
+
function buildBrief(status, statusText, contentType, text) {
|
|
15
|
+
const header = `HTTP ${status} ${statusText}`;
|
|
16
|
+
const isJson = contentType?.includes("application/json") || text.trimStart().startsWith("{") || text.trimStart().startsWith("[");
|
|
17
|
+
const isHtml = contentType?.includes("text/html") || text.trimStart().startsWith("<!") || text.trimStart().startsWith("<html");
|
|
18
|
+
|
|
19
|
+
if (isJson) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(text);
|
|
22
|
+
let compact;
|
|
23
|
+
if (Array.isArray(parsed)) {
|
|
24
|
+
const slice = parsed.slice(0, 20);
|
|
25
|
+
compact = JSON.stringify(slice, null, 2);
|
|
26
|
+
const totalNote = parsed.length > 20 ? `\n[array length: ${parsed.length} — showing first 20]` : "";
|
|
27
|
+
return `${header}\n\n${compact.slice(0, BRIEF_CAP)}${totalNote}`;
|
|
28
|
+
} else {
|
|
29
|
+
compact = JSON.stringify(parsed, null, 2);
|
|
30
|
+
return `${header}\n\n${compact.slice(0, BRIEF_CAP)}${compact.length > BRIEF_CAP ? "\n[truncated]" : ""}`;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Fall through to plain text handling
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isHtml) {
|
|
38
|
+
// Strip tags, collapse whitespace
|
|
39
|
+
const stripped = text
|
|
40
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
41
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
42
|
+
.replace(/<[^>]+>/g, " ")
|
|
43
|
+
.replace(/\s+/g, " ")
|
|
44
|
+
.trim();
|
|
45
|
+
return `${header}\n\n${stripped.slice(0, BRIEF_CAP)}${stripped.length > BRIEF_CAP ? "\n[truncated]" : ""}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `${header}\n\n${text.slice(0, BRIEF_CAP)}${text.length > BRIEF_CAP ? "\n[truncated]" : ""}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function httpFetchTool({ url, method = "GET", body, headers = {} }, { fetchImpl = globalThis.fetch, lookupImpl } = {}) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = new URL(url);
|
|
54
|
+
const inspection = await inspectOutboundUrl(parsed, { lookupImpl });
|
|
55
|
+
if (!inspection.ok) {
|
|
56
|
+
return "Error: request blocked — target resolves to a private/reserved IP address.";
|
|
57
|
+
}
|
|
58
|
+
const opts = {
|
|
59
|
+
method,
|
|
60
|
+
headers: { "User-Agent": "Nemoris/0.1", ...headers },
|
|
61
|
+
signal: AbortSignal.timeout(30000),
|
|
62
|
+
};
|
|
63
|
+
if (body && method !== "GET") opts.body = body;
|
|
64
|
+
const resp = await fetchImpl(url, opts);
|
|
65
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
66
|
+
const text = await resp.text();
|
|
67
|
+
const raw = text.length > RAW_CAP ? text.slice(0, RAW_CAP) + "\n[truncated]" : text;
|
|
68
|
+
const brief = buildBrief(resp.status, resp.statusText, contentType, text);
|
|
69
|
+
|
|
70
|
+
return createToolResult(brief, raw, [url, `http-${resp.status}`]);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return `Error: ${err.message}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileTool } from "./read-file.js";
|
|
2
|
+
import { writeFileTool } from "./write-file.js";
|
|
3
|
+
import { listDirTool } from "./list-dir.js";
|
|
4
|
+
import { shellExecTool } from "./shell-exec.js";
|
|
5
|
+
import { httpFetchTool } from "./http-fetch.js";
|
|
6
|
+
import { executeTriggerJob } from "./trigger-job.js";
|
|
7
|
+
import { executeCompleteTask } from "./complete-task.js";
|
|
8
|
+
import { executeFailTask } from "./fail-task.js";
|
|
9
|
+
import { executeCancelTask } from "./cancel-task.js";
|
|
10
|
+
import { executeMemorySearch } from "./memory-search.js";
|
|
11
|
+
import { writeMemoryTool } from "./write-memory.js";
|
|
12
|
+
import { executeWebSearch } from "./web-search.js";
|
|
13
|
+
import { lcmRecallHandler } from "./lcm-recall.js";
|
|
14
|
+
import { loadSkillHandler } from "./load-skill.js";
|
|
15
|
+
import { listSkillsHandler } from "./list-skills.js";
|
|
16
|
+
|
|
17
|
+
export function registerMicroToolHandlers(registry, context = {}) {
|
|
18
|
+
registry.registerHandler("read_file", (input) => readFileTool(input, context));
|
|
19
|
+
registry.registerHandler("write_file", (input) => writeFileTool(input, context));
|
|
20
|
+
registry.registerHandler("list_dir", (input) => listDirTool(input, context));
|
|
21
|
+
registry.registerHandler("shell_exec", (input) => shellExecTool(input, context));
|
|
22
|
+
registry.registerHandler("http_fetch", (input) => httpFetchTool(input, context));
|
|
23
|
+
registry.registerHandler("trigger_job", (input) => executeTriggerJob(input, context));
|
|
24
|
+
registry.registerHandler("complete_task", (input) => executeCompleteTask(input, context));
|
|
25
|
+
registry.registerHandler("fail_task", (input) => executeFailTask(input, context));
|
|
26
|
+
registry.registerHandler("cancel_task", (input) => executeCancelTask(input, context));
|
|
27
|
+
registry.registerHandler("memory_search", (input) => executeMemorySearch(input, context));
|
|
28
|
+
registry.registerHandler("write_memory", (input) => writeMemoryTool(input, context));
|
|
29
|
+
registry.registerHandler("lcm_recall", (input) => lcmRecallHandler(input, context));
|
|
30
|
+
registry.registerHandler("load_skill", (input) => loadSkillHandler(input, context));
|
|
31
|
+
registry.registerHandler("list_skills", (input) => listSkillsHandler(input, context));
|
|
32
|
+
registry.registerHandler("web_search", (input) => executeWebSearch(input, {
|
|
33
|
+
fetchImpl: context.fetchImpl || globalThis.fetch,
|
|
34
|
+
apiKey: context.openRouterApiKey || process.env.OPENROUTER_API_KEY,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lcm_recall micro tool — search compacted conversation history.
|
|
3
|
+
*
|
|
4
|
+
* Returns a typed tool result contract: { brief, raw, index_terms }.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createToolResult } from "../tool-result-contract.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ query: string, limit?: number }} input
|
|
11
|
+
* @param {{ contextLedger: object, sessionId: string }} context
|
|
12
|
+
* @returns {Promise<import('../tool-result-contract.js').ToolResultContract>}
|
|
13
|
+
*/
|
|
14
|
+
export async function lcmRecallHandler(input, context) {
|
|
15
|
+
const { query, limit = 5 } = input;
|
|
16
|
+
const { contextLedger, sessionId } = context;
|
|
17
|
+
|
|
18
|
+
if (!query) throw new Error("lcm_recall requires a query parameter.");
|
|
19
|
+
if (!contextLedger) {
|
|
20
|
+
// Fallback if ledger is missing in context (e.g. some tests)
|
|
21
|
+
return createToolResult(
|
|
22
|
+
"ContextLedger not available. Cannot recall compacted context.",
|
|
23
|
+
JSON.stringify([]),
|
|
24
|
+
[query]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const summaries = contextLedger.getContextSummaries({ session_id: sessionId });
|
|
29
|
+
|
|
30
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
31
|
+
|
|
32
|
+
const matches = summaries.filter(s => {
|
|
33
|
+
const text = s.summary_text.toLowerCase();
|
|
34
|
+
return queryWords.every(word => text.includes(word));
|
|
35
|
+
}).slice(0, limit);
|
|
36
|
+
|
|
37
|
+
if (matches.length === 0) {
|
|
38
|
+
return createToolResult(
|
|
39
|
+
`No relevant compacted context found for "${query}".`,
|
|
40
|
+
JSON.stringify([]),
|
|
41
|
+
[query]
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const briefLines = matches.map((s, i) => {
|
|
46
|
+
const dateStr = s.created_at || "unknown date";
|
|
47
|
+
return `${i + 1}. [Depth ${s.depth}] (${dateStr}) — ${s.summary_text}`;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const brief = `Found ${matches.length} matching compacted summary(ies):\n${briefLines.join("\n")}`;
|
|
51
|
+
|
|
52
|
+
const indexTerms = new Set([query]);
|
|
53
|
+
matches.forEach(s => {
|
|
54
|
+
s.summary_text.split(/\W+/).forEach(word => {
|
|
55
|
+
if (word.length > 3) indexTerms.add(word.toLowerCase());
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return createToolResult(brief, JSON.stringify(matches), Array.from(indexTerms));
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function listDirTool({ path }, context = {}) {
|
|
5
|
+
try {
|
|
6
|
+
const targetPath = path || context.workspaceRoot;
|
|
7
|
+
const entries = await readdir(targetPath);
|
|
8
|
+
const lines = [];
|
|
9
|
+
for (const entry of entries.sort()) {
|
|
10
|
+
const s = await stat(join(targetPath, entry));
|
|
11
|
+
lines.push(`${s.isDirectory() ? "d" : "-"} ${entry}`);
|
|
12
|
+
}
|
|
13
|
+
return lines.join("\n");
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return `Error listing directory: ${err.message}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readdir, access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createToolResult } from "../tool-result-contract.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List all available skills by name.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} input - {}
|
|
9
|
+
* @param {Object} context - { workspaceRoot, installDir }
|
|
10
|
+
*/
|
|
11
|
+
export async function listSkillsHandler(_, { workspaceRoot, installDir }) {
|
|
12
|
+
const skillDirs = [
|
|
13
|
+
{ path: join(workspaceRoot, "skills"), label: "workspace" },
|
|
14
|
+
{ path: join(installDir, "skills"), label: "install" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const skillNames = new Set();
|
|
18
|
+
|
|
19
|
+
for (const { path } of skillDirs) {
|
|
20
|
+
try {
|
|
21
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
const skillName = entry.name;
|
|
25
|
+
const skillPath = join(path, skillName, "SKILL.md");
|
|
26
|
+
try {
|
|
27
|
+
await access(skillPath);
|
|
28
|
+
skillNames.add(skillName);
|
|
29
|
+
} catch {
|
|
30
|
+
// No SKILL.md here, skip
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.code !== "ENOENT") throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (skillNames.size === 0) {
|
|
40
|
+
return createToolResult("No skills found.", [], ["skills"]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sortedNames = Array.from(skillNames).sort();
|
|
44
|
+
const brief = "Available skills:\n" + sortedNames.join("\n");
|
|
45
|
+
return createToolResult(brief, sortedNames, ["skills", "list"]);
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createToolResult } from "../tool-result-contract.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load a skill by name to get instructions for a specific task.
|
|
7
|
+
* Skills provide step-by-step guidance for tools, APIs, and workflows.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} input - { name }
|
|
10
|
+
* @param {Object} context - { workspaceRoot, installDir }
|
|
11
|
+
*/
|
|
12
|
+
export async function loadSkillHandler({ name }, { workspaceRoot, installDir }) {
|
|
13
|
+
const skillPaths = [
|
|
14
|
+
join(workspaceRoot, "skills", name, "SKILL.md"),
|
|
15
|
+
join(installDir, "skills", name, "SKILL.md"),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
let content = null;
|
|
19
|
+
for (const skillPath of skillPaths) {
|
|
20
|
+
try {
|
|
21
|
+
content = await readFile(skillPath, "utf8");
|
|
22
|
+
break;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.code !== "ENOENT") throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (content === null) {
|
|
29
|
+
return createToolResult(
|
|
30
|
+
`Skill '${name}' not found. Available skills can be listed with list_skills.`,
|
|
31
|
+
null,
|
|
32
|
+
[name]
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cappedContent = content.length > 6000 ? content.slice(0, 6000) + "\n[truncated]" : content;
|
|
37
|
+
return createToolResult(cappedContent, content, [name, "skill"]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory_search micro tool — semantic + lexical search across agent memory.
|
|
3
|
+
*
|
|
4
|
+
* Returns a typed tool result contract: { brief, raw, index_terms }.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createToolResult } from "../tool-result-contract.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ query: string, agentId?: string, limit?: number }} input
|
|
11
|
+
* @param {{ memoryStore: object, callerAgentId: string }} context
|
|
12
|
+
* @returns {Promise<import('../tool-result-contract.js').ToolResultContract>}
|
|
13
|
+
*/
|
|
14
|
+
export async function executeMemorySearch(input, context) {
|
|
15
|
+
const { query, agentId, limit = 5 } = input;
|
|
16
|
+
const { memoryStore, callerAgentId } = context;
|
|
17
|
+
|
|
18
|
+
if (!query) throw new Error("memory_search requires a query parameter.");
|
|
19
|
+
|
|
20
|
+
const targetAgent = agentId || callerAgentId;
|
|
21
|
+
const result = await memoryStore.query(targetAgent, query, { limit });
|
|
22
|
+
|
|
23
|
+
if (!result.items || result.items.length === 0) {
|
|
24
|
+
return createToolResult(
|
|
25
|
+
`No results found for "${query}" in ${targetAgent}'s memory.`,
|
|
26
|
+
JSON.stringify(result),
|
|
27
|
+
[query]
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Brief: concise list of titles + snippet (≤200 tokens target)
|
|
32
|
+
const briefLines = result.items.map((item, i) => {
|
|
33
|
+
const snippet = (item.content || item.summary || "").slice(0, 80);
|
|
34
|
+
return `${i + 1}. **${item.title || "untitled"}** — ${snippet}${snippet.length >= 80 ? "..." : ""}`;
|
|
35
|
+
});
|
|
36
|
+
const brief = `Found ${result.items.length} result(s) for "${query}":\n${briefLines.join("\n")}`;
|
|
37
|
+
|
|
38
|
+
// Index terms: query words + titles
|
|
39
|
+
const indexTerms = [
|
|
40
|
+
query,
|
|
41
|
+
...result.items.map((item) => item.title).filter(Boolean),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return createToolResult(brief, JSON.stringify(result), indexTerms);
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { assertWorkspacePath, resolveToolPath } from "./workspace-paths.js";
|
|
3
|
+
|
|
4
|
+
export async function readFileTool({ path, encoding = "utf8" }, context = {}) {
|
|
5
|
+
try {
|
|
6
|
+
const targetPath = assertWorkspacePath(resolveToolPath(path, context.workspaceRoot), context, { mode: "read" });
|
|
7
|
+
return await readFile(targetPath, encoding);
|
|
8
|
+
} catch (err) {
|
|
9
|
+
return `Error reading file: ${err.message}`;
|
|
10
|
+
}
|
|
11
|
+
}
|