@runcore-sh/runcore 0.5.6 → 0.5.7
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/dictionary.json +2 -2
- package/dist/.extensions/ext-byok.json +1 -0
- package/dist/.extensions/ext-hosted.json +1 -0
- package/dist/.extensions/ext-spawn.json +1 -0
- package/dist/agents/autonomous.d.ts.map +1 -1
- package/dist/agents/runtime/bus.d.ts +1 -0
- package/dist/agents/runtime/bus.d.ts.map +1 -1
- package/dist/agents/spawn.d.ts.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +3 -2
- package/dist/auth/middleware.js.map +1 -1
- package/dist/calendar/routes.d.ts.map +1 -1
- package/dist/calendar/routes.js +8 -7
- package/dist/calendar/routes.js.map +1 -1
- package/dist/files/registry.d.ts +4 -5
- package/dist/files/registry.d.ts.map +1 -1
- package/dist/files/registry.js +4 -5
- package/dist/files/registry.js.map +1 -1
- package/dist/files/store.d.ts +2 -0
- package/dist/files/store.d.ts.map +1 -1
- package/dist/files/store.js +5 -1
- package/dist/files/store.js.map +1 -1
- package/dist/instance.d.ts +2 -0
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +2 -0
- package/dist/instance.js.map +1 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +15 -1
- package/dist/lib/paths.js.map +1 -1
- package/dist/library/brain-shadow.d.ts.map +1 -1
- package/dist/library/brain-shadow.js +5 -4
- package/dist/library/brain-shadow.js.map +1 -1
- package/dist/library/routes.d.ts.map +1 -1
- package/dist/library/routes.js +10 -9
- package/dist/library/routes.js.map +1 -1
- package/dist/llm/cache.d.ts.map +1 -1
- package/dist/llm/cache.js +7 -5
- package/dist/llm/cache.js.map +1 -1
- package/dist/llm/complete.js +2 -0
- package/dist/llm/complete.js.map +1 -1
- package/dist/llm/providers/ollama.d.ts.map +1 -1
- package/dist/llm/providers/ollama.js +32 -7
- package/dist/llm/providers/ollama.js.map +1 -1
- package/dist/llm/providers/openrouter.d.ts.map +1 -1
- package/dist/llm/providers/openrouter.js +105 -12
- package/dist/llm/providers/openrouter.js.map +1 -1
- package/dist/llm/providers/types.d.ts +18 -0
- package/dist/llm/providers/types.d.ts.map +1 -1
- package/dist/llm/retry.d.ts.map +1 -1
- package/dist/llm/retry.js +6 -0
- package/dist/llm/retry.js.map +1 -1
- package/dist/llm/tools/handlers.d.ts +27 -0
- package/dist/llm/tools/handlers.d.ts.map +1 -0
- package/dist/llm/tools/handlers.js +842 -0
- package/dist/llm/tools/handlers.js.map +1 -0
- package/dist/llm/tools/index.d.ts +12 -0
- package/dist/llm/tools/index.d.ts.map +1 -0
- package/dist/llm/tools/index.js +10 -0
- package/dist/llm/tools/index.js.map +1 -0
- package/dist/llm/tools/loop.d.ts +47 -0
- package/dist/llm/tools/loop.d.ts.map +1 -0
- package/dist/llm/tools/loop.js +126 -0
- package/dist/llm/tools/loop.js.map +1 -0
- package/dist/llm/tools/registry.d.ts +27 -0
- package/dist/llm/tools/registry.d.ts.map +1 -0
- package/dist/llm/tools/registry.js +60 -0
- package/dist/llm/tools/registry.js.map +1 -0
- package/dist/llm/tools/schemas.d.ts +92 -0
- package/dist/llm/tools/schemas.d.ts.map +1 -0
- package/dist/llm/tools/schemas.js +154 -0
- package/dist/llm/tools/schemas.js.map +1 -0
- package/dist/llm/tools/types.d.ts +44 -0
- package/dist/llm/tools/types.d.ts.map +1 -0
- package/dist/llm/tools/types.js +9 -0
- package/dist/llm/tools/types.js.map +1 -0
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +249 -5
- package/dist/mcp-server.js.map +1 -1
- package/dist/memory/visual.d.ts.map +1 -1
- package/dist/memory/visual.js +3 -6
- package/dist/memory/visual.js.map +1 -1
- package/dist/openloop/foldback.js +1 -1
- package/dist/openloop/foldback.js.map +1 -1
- package/dist/openloop/resolution-scanner.d.ts.map +1 -1
- package/dist/openloop/resolution-scanner.js +76 -63
- package/dist/openloop/resolution-scanner.js.map +1 -1
- package/dist/plugins/github/index.d.ts +49 -0
- package/dist/plugins/github/index.d.ts.map +1 -0
- package/dist/plugins/github/index.js +153 -0
- package/dist/plugins/github/index.js.map +1 -0
- package/dist/plugins/index.d.ts +1 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +79 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/slack/index.d.ts +43 -0
- package/dist/plugins/slack/index.d.ts.map +1 -0
- package/dist/plugins/slack/index.js +158 -0
- package/dist/plugins/slack/index.js.map +1 -0
- package/dist/plugins/twilio/index.d.ts +41 -0
- package/dist/plugins/twilio/index.d.ts.map +1 -0
- package/dist/plugins/twilio/index.js +102 -0
- package/dist/plugins/twilio/index.js.map +1 -0
- package/dist/pulse/tier.d.ts +1 -1
- package/dist/pulse/tier.js +2 -2
- package/dist/pulse/tier.js.map +1 -1
- package/dist/search/gemini.d.ts +27 -0
- package/dist/search/gemini.d.ts.map +1 -0
- package/dist/search/gemini.js +103 -0
- package/dist/search/gemini.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +850 -536
- package/dist/server.js.map +1 -1
- package/dist/services/routine-patterns.d.ts.map +1 -1
- package/dist/services/routine-patterns.js +6 -0
- package/dist/services/routine-patterns.js.map +1 -1
- package/dist/services/traceInsights.d.ts +5 -0
- package/dist/services/traceInsights.d.ts.map +1 -1
- package/dist/services/traceInsights.js +18 -1
- package/dist/services/traceInsights.js.map +1 -1
- package/dist/settings.d.ts +1 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/vault/store.d.ts +1 -1
- package/dist/vault/store.d.ts.map +1 -1
- package/dist/webhooks/mount.d.ts.map +1 -1
- package/dist/whiteboard/store.d.ts +40 -0
- package/dist/whiteboard/store.d.ts.map +1 -0
- package/dist/whiteboard/store.js +280 -0
- package/dist/whiteboard/store.js.map +1 -0
- package/dist/whiteboard/types.d.ts +55 -0
- package/dist/whiteboard/types.d.ts.map +1 -0
- package/dist/whiteboard/types.js +9 -0
- package/dist/whiteboard/types.js.map +1 -0
- package/dist/whiteboard/weight.d.ts +23 -0
- package/dist/whiteboard/weight.d.ts.map +1 -0
- package/dist/whiteboard/weight.js +126 -0
- package/dist/whiteboard/weight.js.map +1 -0
- package/package.json +2 -2
- package/public/avatar/cache/0cdaf9c41eff4347.mp4 +0 -0
- package/public/avatar/cache/3dacc4ea1082ae36.mp4 +0 -0
- package/public/avatar/cache/44f5db0bfdde93c6.mp4 +0 -0
- package/public/avatar/cache/5628fd10fe55e529.mp4 +0 -0
- package/public/avatar/cache/7ee2ab1577690c8a.mp4 +0 -0
- package/public/avatar/cache/8c470929e814b6b0.mp4 +0 -0
- package/public/avatar/cache/8c908421ce52bf91.mp4 +0 -0
- package/public/avatar/cache/9532f8782a42a89c.mp4 +0 -0
- package/public/avatar/cache/9ce0dddd0cc4d7a1.mp4 +0 -0
- package/public/avatar/cache/a6508e00b6711143.mp4 +0 -0
- package/public/avatar/cache/ba61810a8915e0c7.mp4 +0 -0
- package/public/avatar/cache/c07bee3a10c917cf.mp4 +0 -0
- package/public/avatar/cache/d69175900ea4ea2a.mp4 +0 -0
- package/public/avatar/cache/e61039bc8d39cb93.mp4 +0 -0
- package/public/avatar/cache/e61c6b7047e2cbdb.mp4 +0 -0
- package/public/avatar/cache/efd93c9b18930cf6.mp4 +0 -0
- package/public/avatar/cache/f052d74f5c4abab7.mp4 +0 -0
- package/public/avatar/cache/f7c0be3429a4ef97.mp4 +0 -0
- package/public/avatar/cache/fc8e480f63fe4e35.mp4 +0 -0
- package/public/index.html +225 -51
- package/public/search-flyout.js +324 -0
- package/public/whiteboard.html +915 -0
- package/public/avatar/cache/06fa55aececcc478.mp4 +0 -0
- package/public/avatar/cache/07a65738ba170827.mp4 +0 -0
- package/public/avatar/cache/1185fd491f413406.mp4 +0 -0
- package/public/avatar/cache/272c004a41087de5.mp4 +0 -0
- package/public/avatar/cache/332384e088ca214b.mp4 +0 -0
- package/public/avatar/cache/5d9a960bbf71732c.mp4 +0 -0
- package/public/avatar/cache/5e0954401e15af89.mp4 +0 -0
- package/public/avatar/cache/b35f7a3d558f22cb.mp4 +0 -0
- package/public/avatar/cache/be89f49970672374.mp4 +0 -0
- package/public/avatar/cache/c900811e3382ac6d.mp4 +0 -0
- package/public/avatar/cache/d42a73667acf5716.mp4 +0 -0
- package/public/avatar/cache/e539f247a8908603.mp4 +0 -0
- package/public/avatar/cache/ec95af57d33b3f07.mp4 +0 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool handler factory — creates ToolDefinition[] for all 18 Core tools.
|
|
3
|
+
*
|
|
4
|
+
* Uses the same underlying functions as src/mcp-server.ts (hallway scan,
|
|
5
|
+
* WhiteboardStore, Crystallizer, etc.) — no logic duplication.
|
|
6
|
+
*
|
|
7
|
+
* The ctx parameter provides per-session state so handlers can be created
|
|
8
|
+
* once per chat session with the right brain directory and dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import { resolve, normalize, join } from "node:path";
|
|
11
|
+
import { readdir, stat } from "node:fs/promises";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { memoryRetrieveSchema, memoryLearnSchema, memoryListSchema, readBrainFileSchema, filesSearchSchema, getSettingsSchema, listLockedSchema, listRoomsSchema, whiteboardPlantSchema, whiteboardStatusSchema, voucherIssueSchema, voucherCheckSchema, sendAlertSchema, loopOpenSchema, loopListSchema, loopResolveSchema, dashStatusSchema, webFetchSchema, } from "./schemas.js";
|
|
14
|
+
// ── Helpers (shared with mcp-server.ts) ───────────────────────────────────────
|
|
15
|
+
/** Resolve a relative path under brainDir, guarding against traversal. */
|
|
16
|
+
function resolveBrainPath(brainDir, relativePath) {
|
|
17
|
+
const cleaned = normalize(relativePath).replace(/^[/\\]+/, "");
|
|
18
|
+
const full = resolve(brainDir, cleaned);
|
|
19
|
+
if (!full.startsWith(brainDir)) {
|
|
20
|
+
throw new Error("Path traversal blocked");
|
|
21
|
+
}
|
|
22
|
+
return full;
|
|
23
|
+
}
|
|
24
|
+
/** Format memory entries for display. */
|
|
25
|
+
function formatEntries(entries) {
|
|
26
|
+
if (entries.length === 0)
|
|
27
|
+
return "No entries found.";
|
|
28
|
+
return entries
|
|
29
|
+
.filter((e) => e.content)
|
|
30
|
+
.map((e) => {
|
|
31
|
+
const date = e.createdAt ? formatDate(e.createdAt) : "unknown date";
|
|
32
|
+
const metaParts = [];
|
|
33
|
+
if (e.meta) {
|
|
34
|
+
if (e.meta.tags)
|
|
35
|
+
metaParts.push(`tags: ${e.meta.tags}`);
|
|
36
|
+
if (e.meta.emotional_weight)
|
|
37
|
+
metaParts.push(`weight: ${e.meta.emotional_weight}/10`);
|
|
38
|
+
for (const [k, v] of Object.entries(e.meta)) {
|
|
39
|
+
if (k === "tags" || k === "emotional_weight" || k === "status")
|
|
40
|
+
continue;
|
|
41
|
+
metaParts.push(`${k}: ${v}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const meta = metaParts.length > 0 ? ` (${metaParts.join(", ")})` : "";
|
|
45
|
+
return `**${e.type}** — ${date}${meta}\n${e.content}`;
|
|
46
|
+
})
|
|
47
|
+
.join("\n\n---\n\n") || "No readable entries found.";
|
|
48
|
+
}
|
|
49
|
+
/** Format ISO date to readable string. */
|
|
50
|
+
function formatDate(iso) {
|
|
51
|
+
try {
|
|
52
|
+
const d = new Date(iso);
|
|
53
|
+
return d.toLocaleDateString("en-US", {
|
|
54
|
+
weekday: "short",
|
|
55
|
+
month: "short",
|
|
56
|
+
day: "numeric",
|
|
57
|
+
year: "numeric",
|
|
58
|
+
hour: "numeric",
|
|
59
|
+
minute: "2-digit",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return iso;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Lightweight HTML → markdown. Strips tags, converts common elements. */
|
|
67
|
+
function htmlToMarkdown(html) {
|
|
68
|
+
let s = html;
|
|
69
|
+
s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
70
|
+
s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
71
|
+
s = s.replace(/<head[\s\S]*?<\/head>/gi, "");
|
|
72
|
+
s = s.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n");
|
|
73
|
+
s = s.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n");
|
|
74
|
+
s = s.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n");
|
|
75
|
+
s = s.replace(/<h[4-6][^>]*>([\s\S]*?)<\/h[4-6]>/gi, "\n#### $1\n");
|
|
76
|
+
s = s.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "\n$1\n");
|
|
77
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
78
|
+
s = s.replace(/<hr\s*\/?>/gi, "\n---\n");
|
|
79
|
+
s = s.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
|
|
80
|
+
s = s.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
81
|
+
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
82
|
+
s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, "\n```\n$1\n```\n");
|
|
83
|
+
s = s.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
|
|
84
|
+
s = s.replace(/<img[^>]+alt="([^"]*)"[^>]*>/gi, "![$1]");
|
|
85
|
+
s = s.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n");
|
|
86
|
+
s = s.replace(/<\/?(ul|ol|dl|dt|dd|nav|header|footer|main|article|section|aside|div|span|table|thead|tbody|tr|td|th|figure|figcaption|blockquote)[^>]*>/gi, "\n");
|
|
87
|
+
s = s.replace(/<[^>]+>/g, "");
|
|
88
|
+
s = s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
89
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
90
|
+
s = s.replace(/\n{3,}/g, "\n\n").trim();
|
|
91
|
+
return s;
|
|
92
|
+
}
|
|
93
|
+
/** Convert a Zod schema to JSON Schema via zod v4 built-in.
|
|
94
|
+
* Strips $schema key — LLM APIs (Anthropic via OpenRouter) reject it. */
|
|
95
|
+
function toJsonSchema(schema) {
|
|
96
|
+
const js = z.toJSONSchema(schema);
|
|
97
|
+
delete js.$schema;
|
|
98
|
+
return js;
|
|
99
|
+
}
|
|
100
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Create tool handlers for all 18 Core tools.
|
|
103
|
+
* Each handler matches the behavior of the corresponding mcp.tool() in
|
|
104
|
+
* src/mcp-server.ts, using the same underlying modules.
|
|
105
|
+
*/
|
|
106
|
+
export function createToolHandlers(ctx) {
|
|
107
|
+
const memoryDir = join(ctx.brainDir, "memory");
|
|
108
|
+
// ── Hallway scan (same as mcp-server.ts) ──────────────────────────────────
|
|
109
|
+
const FILE_TO_TYPE = {
|
|
110
|
+
"experiences.jsonl": "episodic",
|
|
111
|
+
"decisions.jsonl": "episodic",
|
|
112
|
+
"failures.jsonl": "episodic",
|
|
113
|
+
"semantic.jsonl": "semantic",
|
|
114
|
+
"procedural.jsonl": "procedural",
|
|
115
|
+
};
|
|
116
|
+
function normalizeEntry(obj, fileName) {
|
|
117
|
+
const type = (typeof obj.type === "string" ? obj.type : FILE_TO_TYPE[fileName] ?? "episodic");
|
|
118
|
+
const content = (typeof obj.content === "string" ? obj.content : null) ??
|
|
119
|
+
(typeof obj.summary === "string" ? obj.summary : null) ??
|
|
120
|
+
(typeof obj.context === "string" ? obj.context : null) ??
|
|
121
|
+
(typeof obj.reasoning === "string" ? obj.reasoning : null) ??
|
|
122
|
+
"";
|
|
123
|
+
const createdAt = (typeof obj.createdAt === "string" ? obj.createdAt : null) ??
|
|
124
|
+
(typeof obj.date === "string" ? obj.date : null) ??
|
|
125
|
+
"";
|
|
126
|
+
const id = (typeof obj.id === "string" ? obj.id : null) ?? `${fileName}:${createdAt || Math.random()}`;
|
|
127
|
+
const meta = obj.meta
|
|
128
|
+
? { ...obj.meta }
|
|
129
|
+
: {};
|
|
130
|
+
if (typeof obj.tags === "string")
|
|
131
|
+
meta.tags = obj.tags;
|
|
132
|
+
if (typeof obj.emotional_weight === "number")
|
|
133
|
+
meta.emotional_weight = obj.emotional_weight;
|
|
134
|
+
if (typeof obj.root_cause === "string")
|
|
135
|
+
meta.root_cause = obj.root_cause;
|
|
136
|
+
if (typeof obj.prevention === "string")
|
|
137
|
+
meta.prevention = obj.prevention;
|
|
138
|
+
if (typeof obj.outcome === "string")
|
|
139
|
+
meta.outcome = obj.outcome;
|
|
140
|
+
if (typeof obj.source === "string")
|
|
141
|
+
meta.source = obj.source;
|
|
142
|
+
return { id, type, content, createdAt, meta: Object.keys(meta).length > 0 ? meta : undefined };
|
|
143
|
+
}
|
|
144
|
+
async function hallwayScanMemory() {
|
|
145
|
+
const { readBrainLines } = await import("../../lib/brain-io.js");
|
|
146
|
+
const { isLocked } = await import("../../lib/locked.js");
|
|
147
|
+
let files;
|
|
148
|
+
try {
|
|
149
|
+
files = await readdir(memoryDir);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl") && f !== "embeddings.jsonl");
|
|
155
|
+
const all = [];
|
|
156
|
+
for (const file of jsonlFiles) {
|
|
157
|
+
const relPath = `memory/${file}`;
|
|
158
|
+
if (isLocked(relPath))
|
|
159
|
+
continue;
|
|
160
|
+
let lines;
|
|
161
|
+
try {
|
|
162
|
+
lines = await readBrainLines(join(memoryDir, file));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const archived = new Set();
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
try {
|
|
170
|
+
const obj = JSON.parse(line);
|
|
171
|
+
if (obj._schema)
|
|
172
|
+
continue;
|
|
173
|
+
if (obj.status === "archived" && typeof obj.id === "string") {
|
|
174
|
+
archived.add(obj.id);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
all.push(normalizeEntry(obj, file));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Remove archived entries
|
|
184
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
185
|
+
if (archived.has(all[i].id))
|
|
186
|
+
all.splice(i, 1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return all.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
190
|
+
}
|
|
191
|
+
// ── Tool definitions ──────────────────────────────────────────────────────
|
|
192
|
+
const tools = [
|
|
193
|
+
// ── memory_retrieve ───────────────────────────────────────────────────
|
|
194
|
+
{
|
|
195
|
+
name: "memory_retrieve",
|
|
196
|
+
description: "Search long-term memory by query, with optional type filter",
|
|
197
|
+
parameters: toJsonSchema(memoryRetrieveSchema),
|
|
198
|
+
tier: "local",
|
|
199
|
+
handler: async (args) => {
|
|
200
|
+
const { query, type, max } = args;
|
|
201
|
+
const maxResults = max ?? 10;
|
|
202
|
+
// Type-filtered: use Brain's LTM retrieval
|
|
203
|
+
if (type && ctx.getBrain) {
|
|
204
|
+
const brain = ctx.getBrain();
|
|
205
|
+
const entries = await brain.retrieve(query, { type, max: maxResults });
|
|
206
|
+
return { content: formatEntries(entries) };
|
|
207
|
+
}
|
|
208
|
+
// Otherwise: hallway scan with scored retrieval
|
|
209
|
+
const { scoreEntry } = await import("../../crystallizer.js");
|
|
210
|
+
const all = await hallwayScanMemory();
|
|
211
|
+
const queryLower = query.toLowerCase();
|
|
212
|
+
const terms = queryLower.split(/\s+/).filter((t) => t.length > 1);
|
|
213
|
+
if (terms.length === 0) {
|
|
214
|
+
return { content: formatEntries(all.slice(0, maxResults)) };
|
|
215
|
+
}
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const scored = all
|
|
218
|
+
.map((e) => {
|
|
219
|
+
const eAny = e;
|
|
220
|
+
const text = [
|
|
221
|
+
e.content ?? "",
|
|
222
|
+
e.meta ? JSON.stringify(e.meta) : "",
|
|
223
|
+
eAny.summary ?? "",
|
|
224
|
+
eAny.title ?? "",
|
|
225
|
+
eAny.description ?? "",
|
|
226
|
+
]
|
|
227
|
+
.join(" ")
|
|
228
|
+
.toLowerCase();
|
|
229
|
+
if (!text)
|
|
230
|
+
return { entry: e, score: 0 };
|
|
231
|
+
const baseScore = scoreEntry(terms, queryLower, text);
|
|
232
|
+
if (baseScore === 0)
|
|
233
|
+
return { entry: e, score: 0 };
|
|
234
|
+
// Recency bonus
|
|
235
|
+
const age = now - new Date(e.createdAt).getTime();
|
|
236
|
+
const dayAge = age / (1000 * 60 * 60 * 24);
|
|
237
|
+
const recencyScore = Math.max(0, 0.1 * (1 - dayAge / 365));
|
|
238
|
+
return { entry: e, score: baseScore + recencyScore };
|
|
239
|
+
})
|
|
240
|
+
.filter((s) => s.score > 0)
|
|
241
|
+
.sort((a, b) => b.score - a.score)
|
|
242
|
+
.slice(0, maxResults)
|
|
243
|
+
.map((s) => s.entry);
|
|
244
|
+
return { content: formatEntries(scored) };
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
// ── memory_learn ──────────────────────────────────────────────────────
|
|
248
|
+
{
|
|
249
|
+
name: "memory_learn",
|
|
250
|
+
description: "Append a new entry to long-term memory (episodic, semantic, or procedural)",
|
|
251
|
+
parameters: toJsonSchema(memoryLearnSchema),
|
|
252
|
+
tier: "local",
|
|
253
|
+
handler: async (args) => {
|
|
254
|
+
const { type, content, meta } = args;
|
|
255
|
+
if (!ctx.getBrain) {
|
|
256
|
+
return { content: "Brain not available", isError: true };
|
|
257
|
+
}
|
|
258
|
+
const brain = ctx.getBrain();
|
|
259
|
+
const entry = await brain.learn({ type, content, meta });
|
|
260
|
+
// Flow through the crystallizer
|
|
261
|
+
let crystalNote = "";
|
|
262
|
+
try {
|
|
263
|
+
const { Crystallizer } = await import("../../crystallizer.js");
|
|
264
|
+
const crystallizer = new Crystallizer(memoryDir, async () => { });
|
|
265
|
+
await crystallizer.init();
|
|
266
|
+
const precipitations = await crystallizer.test(entry);
|
|
267
|
+
if (precipitations.length > 0) {
|
|
268
|
+
crystalNote =
|
|
269
|
+
`\n\nCrystallization: ${precipitations.length} loop(s) precipitated:\n` +
|
|
270
|
+
precipitations
|
|
271
|
+
.map((p) => ` - "${p.query}" (${p.evidenceCount} evidence)`)
|
|
272
|
+
.join("\n");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
/* never block memory writes */
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
content: `Stored as ${entry.id} (${entry.type}) at ${entry.createdAt}${crystalNote}`,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
// ── memory_list ───────────────────────────────────────────────────────
|
|
284
|
+
{
|
|
285
|
+
name: "memory_list",
|
|
286
|
+
description: "List memory entries by type, most recent first",
|
|
287
|
+
parameters: toJsonSchema(memoryListSchema),
|
|
288
|
+
tier: "local",
|
|
289
|
+
handler: async (args) => {
|
|
290
|
+
const { type, limit } = args;
|
|
291
|
+
if (type && ctx.getLtm) {
|
|
292
|
+
const ltm = ctx.getLtm();
|
|
293
|
+
const entries = await ltm.list(type);
|
|
294
|
+
return { content: formatEntries(entries.slice(0, limit ?? 20)) };
|
|
295
|
+
}
|
|
296
|
+
const entries = await hallwayScanMemory();
|
|
297
|
+
return { content: formatEntries(entries.slice(0, limit ?? 20)) };
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
// ── read_brain_file ───────────────────────────────────────────────────
|
|
301
|
+
{
|
|
302
|
+
name: "read_brain_file",
|
|
303
|
+
description: "Read any file under brain/ (path-guarded, encrypted files auto-decrypted)",
|
|
304
|
+
parameters: toJsonSchema(readBrainFileSchema),
|
|
305
|
+
tier: "local",
|
|
306
|
+
handler: async (args) => {
|
|
307
|
+
const { path } = args;
|
|
308
|
+
try {
|
|
309
|
+
const { readBrainFile } = await import("../../lib/brain-io.js");
|
|
310
|
+
const fullPath = resolveBrainPath(ctx.brainDir, path);
|
|
311
|
+
const content = await readBrainFile(fullPath);
|
|
312
|
+
return { content };
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
316
|
+
return { content: `Error: ${msg}`, isError: true };
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
// ── files_search ──────────────────────────────────────────────────────
|
|
321
|
+
{
|
|
322
|
+
name: "files_search",
|
|
323
|
+
description: "Search brain files (notes, research, identity, templates, protocols) by keyword. Returns matching filenames with context snippets.",
|
|
324
|
+
parameters: toJsonSchema(filesSearchSchema),
|
|
325
|
+
tier: "local",
|
|
326
|
+
handler: async (args) => {
|
|
327
|
+
const { query, max } = args;
|
|
328
|
+
const maxResults = max ?? 10;
|
|
329
|
+
const terms = query
|
|
330
|
+
.toLowerCase()
|
|
331
|
+
.split(/\s+/)
|
|
332
|
+
.filter((t) => t.length > 1);
|
|
333
|
+
if (terms.length === 0) {
|
|
334
|
+
return { content: "No search terms provided." };
|
|
335
|
+
}
|
|
336
|
+
const { readBrainFile } = await import("../../lib/brain-io.js");
|
|
337
|
+
const { isLocked } = await import("../../lib/locked.js");
|
|
338
|
+
const searchExts = new Set([
|
|
339
|
+
".md",
|
|
340
|
+
".yaml",
|
|
341
|
+
".yml",
|
|
342
|
+
".json",
|
|
343
|
+
".txt",
|
|
344
|
+
".jsonl",
|
|
345
|
+
]);
|
|
346
|
+
const skipDirs = new Set([
|
|
347
|
+
"log",
|
|
348
|
+
".config",
|
|
349
|
+
"ops",
|
|
350
|
+
"metrics",
|
|
351
|
+
".obsidian",
|
|
352
|
+
".git",
|
|
353
|
+
"node_modules",
|
|
354
|
+
]);
|
|
355
|
+
const hits = [];
|
|
356
|
+
async function scanDir(dir, rel) {
|
|
357
|
+
let entries;
|
|
358
|
+
try {
|
|
359
|
+
entries = await readdir(dir);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const name of entries) {
|
|
365
|
+
if (name.startsWith(".") && rel === "") {
|
|
366
|
+
if (skipDirs.has(name))
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (skipDirs.has(name))
|
|
370
|
+
continue;
|
|
371
|
+
const full = join(dir, name);
|
|
372
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
373
|
+
try {
|
|
374
|
+
const s = await stat(full);
|
|
375
|
+
if (s.isDirectory()) {
|
|
376
|
+
if (name === "memory" ||
|
|
377
|
+
name === "logs" ||
|
|
378
|
+
name === "tasks" ||
|
|
379
|
+
name === "daily" ||
|
|
380
|
+
name === "hourly")
|
|
381
|
+
continue;
|
|
382
|
+
await scanDir(full, childRel);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const ext = name
|
|
386
|
+
.substring(name.lastIndexOf("."))
|
|
387
|
+
.toLowerCase();
|
|
388
|
+
if (!searchExts.has(ext))
|
|
389
|
+
continue;
|
|
390
|
+
if (isLocked(childRel))
|
|
391
|
+
continue;
|
|
392
|
+
if (ext === ".jsonl" && s.size > 100_000)
|
|
393
|
+
continue;
|
|
394
|
+
const content = await readBrainFile(full);
|
|
395
|
+
const lower = content.toLowerCase();
|
|
396
|
+
let score = 0;
|
|
397
|
+
for (const term of terms) {
|
|
398
|
+
const idx = lower.indexOf(term);
|
|
399
|
+
if (idx !== -1) {
|
|
400
|
+
score++;
|
|
401
|
+
if (name.toLowerCase().includes(term))
|
|
402
|
+
score += 2;
|
|
403
|
+
}
|
|
404
|
+
if (childRel.toLowerCase().includes(term))
|
|
405
|
+
score += 2;
|
|
406
|
+
}
|
|
407
|
+
if (score > 0) {
|
|
408
|
+
const firstTerm = terms.find((t) => lower.includes(t));
|
|
409
|
+
const matchIdx = lower.indexOf(firstTerm);
|
|
410
|
+
const start = Math.max(0, matchIdx - 80);
|
|
411
|
+
const end = Math.min(content.length, matchIdx + 120);
|
|
412
|
+
const snippet = (start > 0 ? "..." : "") +
|
|
413
|
+
content
|
|
414
|
+
.substring(start, end)
|
|
415
|
+
.replace(/\n/g, " ")
|
|
416
|
+
.trim() +
|
|
417
|
+
(end < content.length ? "..." : "");
|
|
418
|
+
hits.push({ relPath: childRel, score, snippet });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
await scanDir(ctx.brainDir, "");
|
|
428
|
+
if (hits.length === 0) {
|
|
429
|
+
return { content: `No files matched: "${query}"` };
|
|
430
|
+
}
|
|
431
|
+
hits.sort((a, b) => b.score - a.score);
|
|
432
|
+
const top = hits.slice(0, maxResults);
|
|
433
|
+
const result = top
|
|
434
|
+
.map((h) => `${h.relPath} (score: ${h.score})\n ${h.snippet}`)
|
|
435
|
+
.join("\n\n");
|
|
436
|
+
return {
|
|
437
|
+
content: `Found ${hits.length} file(s) matching "${query}":\n\n${result}`,
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
// ── get_settings ──────────────────────────────────────────────────────
|
|
442
|
+
{
|
|
443
|
+
name: "get_settings",
|
|
444
|
+
description: "Return safe subset of Core settings (no keys or secrets)",
|
|
445
|
+
parameters: toJsonSchema(getSettingsSchema),
|
|
446
|
+
tier: "local",
|
|
447
|
+
handler: async () => {
|
|
448
|
+
const { getSettings } = await import("../../settings.js");
|
|
449
|
+
const s = getSettings();
|
|
450
|
+
const safe = {
|
|
451
|
+
instanceName: s.instanceName,
|
|
452
|
+
airplaneMode: s.airplaneMode,
|
|
453
|
+
privateMode: s.privateMode,
|
|
454
|
+
encryptBrainFiles: s.encryptBrainFiles,
|
|
455
|
+
models: s.models,
|
|
456
|
+
pulse: s.pulse,
|
|
457
|
+
};
|
|
458
|
+
return { content: JSON.stringify(safe, null, 2) };
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
// ── list_locked ───────────────────────────────────────────────────────
|
|
462
|
+
{
|
|
463
|
+
name: "list_locked",
|
|
464
|
+
description: "Show all currently locked paths in brain/.locked",
|
|
465
|
+
parameters: toJsonSchema(listLockedSchema),
|
|
466
|
+
tier: "local",
|
|
467
|
+
handler: async () => {
|
|
468
|
+
const { reloadLockedPaths } = await import("../../lib/locked.js");
|
|
469
|
+
const paths = await reloadLockedPaths();
|
|
470
|
+
const lines = paths.length > 0
|
|
471
|
+
? paths.map((p) => `locked: ${p}`).join("\n")
|
|
472
|
+
: "No locked paths.";
|
|
473
|
+
return { content: lines };
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
// ── list_rooms ────────────────────────────────────────────────────────
|
|
477
|
+
{
|
|
478
|
+
name: "list_rooms",
|
|
479
|
+
description: "List all files/dirs under brain/ with their locked status",
|
|
480
|
+
parameters: toJsonSchema(listRoomsSchema),
|
|
481
|
+
tier: "local",
|
|
482
|
+
handler: async () => {
|
|
483
|
+
const { reloadLockedPaths, isLocked } = await import("../../lib/locked.js");
|
|
484
|
+
await reloadLockedPaths();
|
|
485
|
+
async function walk(dir, prefix) {
|
|
486
|
+
const items = [];
|
|
487
|
+
let entries;
|
|
488
|
+
try {
|
|
489
|
+
entries = await readdir(dir);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return items;
|
|
493
|
+
}
|
|
494
|
+
for (const name of entries.sort()) {
|
|
495
|
+
const relPath = prefix ? `${prefix}/${name}` : name;
|
|
496
|
+
const fullPath = join(dir, name);
|
|
497
|
+
const locked = isLocked(relPath);
|
|
498
|
+
try {
|
|
499
|
+
const s = await stat(fullPath);
|
|
500
|
+
if (s.isDirectory()) {
|
|
501
|
+
items.push(`${locked ? "[locked]" : "[dir]"} ${relPath}/`);
|
|
502
|
+
if (!locked) {
|
|
503
|
+
const children = await walk(fullPath, relPath);
|
|
504
|
+
items.push(...children);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
items.push(`${locked ? "[locked]" : "[file]"} ${relPath}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
items.push(`[error] ${relPath} (unreadable)`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return items;
|
|
516
|
+
}
|
|
517
|
+
const tree = await walk(ctx.brainDir, "");
|
|
518
|
+
return { content: tree.join("\n") || "Empty brain directory." };
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
// ── whiteboard_plant ──────────────────────────────────────────────────
|
|
522
|
+
{
|
|
523
|
+
name: "whiteboard_plant",
|
|
524
|
+
description: "Plant a work item or question on the whiteboard. Use this to track goals, tasks, decisions, and questions that need human input.",
|
|
525
|
+
parameters: toJsonSchema(whiteboardPlantSchema),
|
|
526
|
+
tier: "local",
|
|
527
|
+
handler: async (args) => {
|
|
528
|
+
const { title, type, parentId, tags, body, question } = args;
|
|
529
|
+
const { WhiteboardStore } = await import("../../whiteboard/store.js");
|
|
530
|
+
const store = new WhiteboardStore(ctx.brainDir);
|
|
531
|
+
const node = await store.create({
|
|
532
|
+
title,
|
|
533
|
+
type: type,
|
|
534
|
+
parentId: parentId ?? null,
|
|
535
|
+
tags: tags ?? [],
|
|
536
|
+
plantedBy: "agent",
|
|
537
|
+
body,
|
|
538
|
+
question,
|
|
539
|
+
});
|
|
540
|
+
const typeLabel = type === "question"
|
|
541
|
+
? `Question planted: "${question ?? title}"`
|
|
542
|
+
: `${type} planted: "${title}"`;
|
|
543
|
+
return { content: `${typeLabel}\nID: ${node.id}\nWeight: ${node.weight}` };
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
// ── whiteboard_status ─────────────────────────────────────────────────
|
|
547
|
+
{
|
|
548
|
+
name: "whiteboard_status",
|
|
549
|
+
description: "Get whiteboard summary — open items, unanswered questions, and top items by attention weight.",
|
|
550
|
+
parameters: toJsonSchema(whiteboardStatusSchema),
|
|
551
|
+
tier: "local",
|
|
552
|
+
handler: async () => {
|
|
553
|
+
const { WhiteboardStore } = await import("../../whiteboard/store.js");
|
|
554
|
+
const store = new WhiteboardStore(ctx.brainDir);
|
|
555
|
+
const summary = await store.getSummary();
|
|
556
|
+
const lines = [
|
|
557
|
+
`Whiteboard: ${summary.total} items (${summary.open} open, ${summary.done} done)`,
|
|
558
|
+
`Open questions: ${summary.openQuestions}`,
|
|
559
|
+
];
|
|
560
|
+
if (summary.topWeighted.length > 0) {
|
|
561
|
+
lines.push("", "Top items by weight:");
|
|
562
|
+
for (const node of summary.topWeighted) {
|
|
563
|
+
const icon = node.type === "question"
|
|
564
|
+
? "?"
|
|
565
|
+
: node.type === "decision"
|
|
566
|
+
? "!"
|
|
567
|
+
: "-";
|
|
568
|
+
lines.push(` ${icon} [${node.weight}] ${node.title}${node.question ? ` — "${node.question}"` : ""}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (Object.keys(summary.byTag).length > 0) {
|
|
572
|
+
lines.push("", "By tag: " +
|
|
573
|
+
Object.entries(summary.byTag)
|
|
574
|
+
.map(([k, v]) => `${k}(${v})`)
|
|
575
|
+
.join(", "));
|
|
576
|
+
}
|
|
577
|
+
// Show open questions from the human
|
|
578
|
+
const openQs = await store.getOpenQuestions();
|
|
579
|
+
const humanQs = openQs.filter((q) => q.plantedBy === "human");
|
|
580
|
+
if (humanQs.length > 0) {
|
|
581
|
+
lines.push("", "Questions from human (answer or act on these):");
|
|
582
|
+
for (const q of humanQs) {
|
|
583
|
+
lines.push(` -> [${q.weight}] "${q.question || q.title}" (id: ${q.id})`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Show answered questions
|
|
587
|
+
const allNodes = await store.list();
|
|
588
|
+
const answered = allNodes.filter((n) => n.type === "question" && n.answer);
|
|
589
|
+
if (answered.length > 0) {
|
|
590
|
+
lines.push("", "=== ANSWERED QUESTIONS (act on these) ===");
|
|
591
|
+
for (const a of answered) {
|
|
592
|
+
lines.push(` Q: "${a.question || a.title}"`);
|
|
593
|
+
lines.push(` A: ${a.answer}`);
|
|
594
|
+
lines.push(` (id: ${a.id}, status: ${a.status})`);
|
|
595
|
+
lines.push("");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return { content: lines.join("\n") };
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
// ── voucher_issue ─────────────────────────────────────────────────────
|
|
602
|
+
{
|
|
603
|
+
name: "voucher_issue",
|
|
604
|
+
description: "Issue a short-lived voucher token for brain-to-brain verification",
|
|
605
|
+
parameters: toJsonSchema(voucherIssueSchema),
|
|
606
|
+
tier: "spawn",
|
|
607
|
+
handler: async (args) => {
|
|
608
|
+
const { scope, ttlMinutes } = args;
|
|
609
|
+
const { issueVoucher } = await import("../../voucher.js");
|
|
610
|
+
if (!ctx.getLtm) {
|
|
611
|
+
return { content: "LTM not available for voucher storage", isError: true };
|
|
612
|
+
}
|
|
613
|
+
const ltm = ctx.getLtm();
|
|
614
|
+
const token = await issueVoucher(ltm, scope, ttlMinutes);
|
|
615
|
+
return {
|
|
616
|
+
content: `Voucher issued: ${token}${scope ? ` (scope: ${scope})` : ""}\nExpires in ${ttlMinutes ?? 30} minutes. Carry this token to the other brain.`,
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
// ── voucher_check ─────────────────────────────────────────────────────
|
|
621
|
+
{
|
|
622
|
+
name: "voucher_check",
|
|
623
|
+
description: "Verify a voucher token carried from another brain",
|
|
624
|
+
parameters: toJsonSchema(voucherCheckSchema),
|
|
625
|
+
tier: "spawn",
|
|
626
|
+
handler: async (args) => {
|
|
627
|
+
const { token } = args;
|
|
628
|
+
const { checkVoucherWithAlert } = await import("../../voucher.js");
|
|
629
|
+
if (!ctx.getLtm) {
|
|
630
|
+
return { content: "LTM not available for voucher verification", isError: true };
|
|
631
|
+
}
|
|
632
|
+
const ltm = ctx.getLtm();
|
|
633
|
+
const result = await checkVoucherWithAlert(ltm, token, "tool:voucher_check");
|
|
634
|
+
if (result.valid) {
|
|
635
|
+
return {
|
|
636
|
+
content: `Valid voucher.${result.scope ? ` Scope: ${result.scope}` : " No scope restriction."}`,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return { content: "Invalid or expired voucher. Request denied." };
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
// ── send_alert ────────────────────────────────────────────────────────
|
|
643
|
+
{
|
|
644
|
+
name: "send_alert",
|
|
645
|
+
description: "Send an alert to the human via email and/or SMS. Use when something needs attention.",
|
|
646
|
+
parameters: toJsonSchema(sendAlertSchema),
|
|
647
|
+
tier: "byok",
|
|
648
|
+
handler: async (args) => {
|
|
649
|
+
const { subject, body } = args;
|
|
650
|
+
const { sendAlert } = await import("../../alert.js");
|
|
651
|
+
const results = await sendAlert(subject, body);
|
|
652
|
+
const summary = results
|
|
653
|
+
.map((r) => `${r.channel}: ${r.sent ? "sent" : `failed (${r.error})`}`)
|
|
654
|
+
.join(", ");
|
|
655
|
+
return { content: summary };
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
// ── loop_open ─────────────────────────────────────────────────────────
|
|
659
|
+
{
|
|
660
|
+
name: "loop_open",
|
|
661
|
+
description: "Create an open loop — a standing query that filters the memory stream. Evidence accumulates as matching memories are added.",
|
|
662
|
+
parameters: toJsonSchema(loopOpenSchema),
|
|
663
|
+
tier: "local",
|
|
664
|
+
handler: async (args) => {
|
|
665
|
+
const { query, context, threshold, minScore } = args;
|
|
666
|
+
const { Crystallizer } = await import("../../crystallizer.js");
|
|
667
|
+
const crystallizer = new Crystallizer(memoryDir, async () => { });
|
|
668
|
+
await crystallizer.init();
|
|
669
|
+
const loop = await crystallizer.open(query, context, threshold ?? 3, minScore ?? 0.4);
|
|
670
|
+
return {
|
|
671
|
+
content: `Loop opened: ${loop.id}\nQuery: "${loop.query}"\nContext: ${loop.context}\nThreshold: ${loop.threshold} evidence hits\nMin score: ${loop.minScore}`,
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
// ── loop_list ─────────────────────────────────────────────────────────
|
|
676
|
+
{
|
|
677
|
+
name: "loop_list",
|
|
678
|
+
description: "List open loops and their evidence state. Shows what's filtering, what's accumulated, what's precipitated.",
|
|
679
|
+
parameters: toJsonSchema(loopListSchema),
|
|
680
|
+
tier: "local",
|
|
681
|
+
handler: async (args) => {
|
|
682
|
+
const { status } = args;
|
|
683
|
+
const { Crystallizer } = await import("../../crystallizer.js");
|
|
684
|
+
const crystallizer = new Crystallizer(memoryDir, async () => { });
|
|
685
|
+
await crystallizer.init();
|
|
686
|
+
const filterStatus = status === "all" ? undefined : status;
|
|
687
|
+
const loops = crystallizer.list(filterStatus);
|
|
688
|
+
if (loops.length === 0) {
|
|
689
|
+
return { content: "No loops found." };
|
|
690
|
+
}
|
|
691
|
+
const lines = loops.map((l) => {
|
|
692
|
+
const evidenceStr = l.evidence.length > 0
|
|
693
|
+
? `\n Evidence (${l.evidence.length}/${l.threshold}):\n` +
|
|
694
|
+
l.evidence
|
|
695
|
+
.map((e) => ` - [${e.score.toFixed(2)}] ${e.snippet.slice(0, 80)}...`)
|
|
696
|
+
.join("\n")
|
|
697
|
+
: `\n No evidence yet (0/${l.threshold})`;
|
|
698
|
+
return `[${l.status.toUpperCase()}] ${l.id}\n Query: "${l.query}"\n Context: ${l.context}\n Created: ${l.createdAt}${evidenceStr}`;
|
|
699
|
+
});
|
|
700
|
+
return { content: lines.join("\n\n") };
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
// ── loop_resolve ──────────────────────────────────────────────────────
|
|
704
|
+
{
|
|
705
|
+
name: "loop_resolve",
|
|
706
|
+
description: "Manually resolve (close) an open loop. Use when the question has been answered or is no longer relevant.",
|
|
707
|
+
parameters: toJsonSchema(loopResolveSchema),
|
|
708
|
+
tier: "local",
|
|
709
|
+
handler: async (args) => {
|
|
710
|
+
const { loopId } = args;
|
|
711
|
+
const { Crystallizer } = await import("../../crystallizer.js");
|
|
712
|
+
const crystallizer = new Crystallizer(memoryDir, async () => { });
|
|
713
|
+
await crystallizer.init();
|
|
714
|
+
const loop = await crystallizer.resolve(loopId);
|
|
715
|
+
if (!loop) {
|
|
716
|
+
return {
|
|
717
|
+
content: `Loop ${loopId} not found.`,
|
|
718
|
+
isError: true,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
content: `Loop resolved: ${loop.id} ("${loop.query}")\nFinal evidence count: ${loop.evidence.length}`,
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
// ── dash_status ───────────────────────────────────────────────────────
|
|
727
|
+
{
|
|
728
|
+
name: "dash_status",
|
|
729
|
+
description: "Check if the Core server is running and review pending handoffs.",
|
|
730
|
+
parameters: toJsonSchema(dashStatusSchema),
|
|
731
|
+
tier: "byok",
|
|
732
|
+
handler: async () => {
|
|
733
|
+
const { discoverRunning } = await import("../../runtime-lock.js");
|
|
734
|
+
const lock = discoverRunning();
|
|
735
|
+
let instanceHealth;
|
|
736
|
+
if (!lock) {
|
|
737
|
+
instanceHealth = "Server is DOWN — no runtime lock found";
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetch(`http://localhost:${lock.port}/healthz`, { signal: AbortSignal.timeout(5000) });
|
|
742
|
+
if (res.ok) {
|
|
743
|
+
const h = (await res.json());
|
|
744
|
+
instanceHealth = `${lock.name} is RUNNING on port ${lock.port}. Uptime: ${h.uptime}s. Status: ${h.status}. PID: ${lock.pid}`;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
instanceHealth = `${lock.name} responded but unhealthy: HTTP ${res.status} (port ${lock.port}, pid ${lock.pid})`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
instanceHealth = `${lock.name} has lock (pid=${lock.pid}, port=${lock.port}) but HTTP health check failed`;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// Read handoffs
|
|
755
|
+
let handoffSummary;
|
|
756
|
+
try {
|
|
757
|
+
const { readFile } = await import("node:fs/promises");
|
|
758
|
+
const handoffPath = join(ctx.brainDir, "operations", "handoffs.jsonl");
|
|
759
|
+
const raw = await readFile(handoffPath, "utf-8");
|
|
760
|
+
const lines = raw
|
|
761
|
+
.split("\n")
|
|
762
|
+
.filter((l) => l.trim() && !l.includes("_schema"));
|
|
763
|
+
const pending = lines.filter((l) => {
|
|
764
|
+
try {
|
|
765
|
+
return JSON.parse(l).status === "pending";
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
const items = pending.map((l) => {
|
|
772
|
+
const o = JSON.parse(l);
|
|
773
|
+
return `[${o.priority || "?"}] ${o.title}`;
|
|
774
|
+
});
|
|
775
|
+
handoffSummary =
|
|
776
|
+
pending.length === 0
|
|
777
|
+
? "No pending handoffs."
|
|
778
|
+
: `${pending.length} pending handoffs:\n${items.join("\n")}`;
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
handoffSummary = "No pending handoffs.";
|
|
782
|
+
}
|
|
783
|
+
return { content: `${instanceHealth}\n\n${handoffSummary}` };
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
// ── web_fetch ──────────────────────────────────────────────────────────
|
|
787
|
+
{
|
|
788
|
+
name: "web_fetch",
|
|
789
|
+
description: "Fetch a URL and return its content as markdown. Only use when the user provides a specific URL — never browse autonomously.",
|
|
790
|
+
parameters: toJsonSchema(webFetchSchema),
|
|
791
|
+
tier: "local",
|
|
792
|
+
handler: async (args) => {
|
|
793
|
+
const { url, prompt } = args;
|
|
794
|
+
try {
|
|
795
|
+
const res = await fetch(url, {
|
|
796
|
+
headers: {
|
|
797
|
+
"User-Agent": "Core/1.0 (runcore.sh)",
|
|
798
|
+
"Accept": "text/html,application/xhtml+xml,text/plain,application/json,*/*",
|
|
799
|
+
},
|
|
800
|
+
signal: AbortSignal.timeout(15_000),
|
|
801
|
+
redirect: "follow",
|
|
802
|
+
});
|
|
803
|
+
if (!res.ok) {
|
|
804
|
+
return { content: `Fetch failed: HTTP ${res.status} ${res.statusText}` };
|
|
805
|
+
}
|
|
806
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
807
|
+
const raw = await res.text();
|
|
808
|
+
let body;
|
|
809
|
+
if (contentType.includes("application/json")) {
|
|
810
|
+
try {
|
|
811
|
+
body = "```json\n" + JSON.stringify(JSON.parse(raw), null, 2) + "\n```";
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
body = raw;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else if (contentType.includes("text/html")) {
|
|
818
|
+
body = htmlToMarkdown(raw);
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
body = raw;
|
|
822
|
+
}
|
|
823
|
+
const MAX_CHARS = 50_000;
|
|
824
|
+
if (body.length > MAX_CHARS) {
|
|
825
|
+
body = body.slice(0, MAX_CHARS) + `\n\n... (truncated at ${MAX_CHARS} chars)`;
|
|
826
|
+
}
|
|
827
|
+
const header = `**Fetched:** ${url}\n**Content-Type:** ${contentType}\n**Size:** ${raw.length} bytes\n\n---\n\n`;
|
|
828
|
+
const result = prompt
|
|
829
|
+
? `${header}${body}\n\n---\n\n**Extraction prompt:** ${prompt}`
|
|
830
|
+
: `${header}${body}`;
|
|
831
|
+
return { content: result };
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
835
|
+
return { content: `Fetch error: ${msg}` };
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
];
|
|
840
|
+
return tools;
|
|
841
|
+
}
|
|
842
|
+
//# sourceMappingURL=handlers.js.map
|