clawmem 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/AGENTS.md +660 -0
- package/CLAUDE.md +660 -0
- package/LICENSE +21 -0
- package/README.md +993 -0
- package/SKILL.md +717 -0
- package/bin/clawmem +75 -0
- package/package.json +72 -0
- package/src/amem.ts +797 -0
- package/src/beads.ts +263 -0
- package/src/clawmem.ts +1849 -0
- package/src/collections.ts +405 -0
- package/src/config.ts +178 -0
- package/src/consolidation.ts +123 -0
- package/src/directory-context.ts +248 -0
- package/src/errors.ts +41 -0
- package/src/formatter.ts +427 -0
- package/src/graph-traversal.ts +247 -0
- package/src/hooks/context-surfacing.ts +317 -0
- package/src/hooks/curator-nudge.ts +89 -0
- package/src/hooks/decision-extractor.ts +639 -0
- package/src/hooks/feedback-loop.ts +214 -0
- package/src/hooks/handoff-generator.ts +345 -0
- package/src/hooks/postcompact-inject.ts +226 -0
- package/src/hooks/precompact-extract.ts +314 -0
- package/src/hooks/pretool-inject.ts +79 -0
- package/src/hooks/session-bootstrap.ts +324 -0
- package/src/hooks/staleness-check.ts +130 -0
- package/src/hooks.ts +367 -0
- package/src/indexer.ts +327 -0
- package/src/intent.ts +294 -0
- package/src/limits.ts +26 -0
- package/src/llm.ts +1175 -0
- package/src/mcp.ts +2138 -0
- package/src/memory.ts +336 -0
- package/src/mmr.ts +93 -0
- package/src/observer.ts +269 -0
- package/src/openclaw/engine.ts +283 -0
- package/src/openclaw/index.ts +221 -0
- package/src/openclaw/plugin.json +83 -0
- package/src/openclaw/shell.ts +207 -0
- package/src/openclaw/tools.ts +304 -0
- package/src/profile.ts +346 -0
- package/src/promptguard.ts +218 -0
- package/src/retrieval-gate.ts +106 -0
- package/src/search-utils.ts +127 -0
- package/src/server.ts +783 -0
- package/src/splitter.ts +325 -0
- package/src/store.ts +4062 -0
- package/src/validation.ts +67 -0
- package/src/watcher.ts +58 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMem OpenClaw Plugin — Shell-out utilities
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 transport: spawn `clawmem hook <name>` as a Bun subprocess.
|
|
5
|
+
* All hook handlers accept JSON on stdin and return JSON on stdout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile, spawn, type ChildProcess } from "node:child_process";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export type ClawMemConfig = {
|
|
17
|
+
clawmemBin: string;
|
|
18
|
+
tokenBudget: number;
|
|
19
|
+
profile: string;
|
|
20
|
+
enableTools: boolean;
|
|
21
|
+
servePort: number;
|
|
22
|
+
env: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ShellResult = {
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
exitCode: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Binary Resolution
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
const SEARCH_PATHS = [
|
|
36
|
+
// Relative to this plugin (ClawMem repo layout)
|
|
37
|
+
resolve(__dirname, "../../bin/clawmem"),
|
|
38
|
+
// Common install locations
|
|
39
|
+
"/usr/local/bin/clawmem",
|
|
40
|
+
resolve(process.env.HOME || "/tmp", "Projects/forge-stack/skill-forge/clawmem/bin/clawmem"),
|
|
41
|
+
resolve(process.env.HOME || "/tmp", "clawmem/bin/clawmem"),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function resolveClawMemBin(configured?: string): string {
|
|
45
|
+
if (configured && existsSync(configured)) return configured;
|
|
46
|
+
|
|
47
|
+
for (const p of SEARCH_PATHS) {
|
|
48
|
+
if (existsSync(p)) return p;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fallback: assume it's on PATH
|
|
52
|
+
return "clawmem";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Shell Execution
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
const DEFAULT_TIMEOUT = 10_000; // 10s for most hooks
|
|
60
|
+
const EXTRACTION_TIMEOUT = 30_000; // 30s for LLM-based extraction
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute a clawmem hook with JSON on stdin, capture JSON stdout.
|
|
64
|
+
* Fail-open: returns empty result on timeout or error.
|
|
65
|
+
*/
|
|
66
|
+
export function execHook(
|
|
67
|
+
cfg: ClawMemConfig,
|
|
68
|
+
hookName: string,
|
|
69
|
+
input: Record<string, unknown>,
|
|
70
|
+
timeout?: number
|
|
71
|
+
): Promise<ShellResult> {
|
|
72
|
+
const hookTimeout = timeout ?? (
|
|
73
|
+
hookName === "decision-extractor" || hookName === "handoff-generator"
|
|
74
|
+
? EXTRACTION_TIMEOUT
|
|
75
|
+
: DEFAULT_TIMEOUT
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const child = execFile(
|
|
80
|
+
cfg.clawmemBin,
|
|
81
|
+
["hook", hookName],
|
|
82
|
+
{
|
|
83
|
+
timeout: hookTimeout,
|
|
84
|
+
env: { ...process.env, ...cfg.env },
|
|
85
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
86
|
+
},
|
|
87
|
+
(error, stdout, stderr) => {
|
|
88
|
+
if (error) {
|
|
89
|
+
// Fail-open: log but don't throw
|
|
90
|
+
const msg = (error as any).killed
|
|
91
|
+
? `timeout after ${hookTimeout}ms`
|
|
92
|
+
: String(error.message || error);
|
|
93
|
+
resolve({
|
|
94
|
+
stdout: "",
|
|
95
|
+
stderr: `[clawmem-plugin] hook ${hookName} failed: ${msg}\n${stderr}`,
|
|
96
|
+
exitCode: (error as any).code ?? 1,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
resolve({ stdout: stdout || "", stderr: stderr || "", exitCode: 0 });
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Send hook input on stdin
|
|
105
|
+
if (child.stdin) {
|
|
106
|
+
child.stdin.write(JSON.stringify(input));
|
|
107
|
+
child.stdin.end();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Execute a clawmem CLI command (non-hook).
|
|
114
|
+
*/
|
|
115
|
+
export function execCommand(
|
|
116
|
+
cfg: ClawMemConfig,
|
|
117
|
+
args: string[],
|
|
118
|
+
timeout: number = DEFAULT_TIMEOUT
|
|
119
|
+
): Promise<ShellResult> {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
execFile(
|
|
122
|
+
cfg.clawmemBin,
|
|
123
|
+
args,
|
|
124
|
+
{
|
|
125
|
+
timeout,
|
|
126
|
+
env: { ...process.env, ...cfg.env },
|
|
127
|
+
maxBuffer: 1024 * 1024,
|
|
128
|
+
},
|
|
129
|
+
(error, stdout, stderr) => {
|
|
130
|
+
if (error) {
|
|
131
|
+
resolve({
|
|
132
|
+
stdout: "",
|
|
133
|
+
stderr: `[clawmem-plugin] command failed: ${String(error.message || error)}\n${stderr}`,
|
|
134
|
+
exitCode: (error as any).code ?? 1,
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
resolve({ stdout: stdout || "", stderr: stderr || "", exitCode: 0 });
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Spawn a long-lived background process (e.g., `clawmem serve`).
|
|
146
|
+
* Returns the child process handle for lifecycle management.
|
|
147
|
+
* The child is detached from the parent's event loop via unref().
|
|
148
|
+
*/
|
|
149
|
+
export function spawnBackground(
|
|
150
|
+
cfg: ClawMemConfig,
|
|
151
|
+
args: string[],
|
|
152
|
+
logger?: { info: (...args: any[]) => void; warn: (...args: any[]) => void }
|
|
153
|
+
): ChildProcess {
|
|
154
|
+
const child = spawn(cfg.clawmemBin, args, {
|
|
155
|
+
env: { ...process.env, ...cfg.env },
|
|
156
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
157
|
+
detached: false,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
child.stdout?.on("data", (data: Buffer) => {
|
|
161
|
+
logger?.info(`[clawmem-serve] ${data.toString().trim()}`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
child.stderr?.on("data", (data: Buffer) => {
|
|
165
|
+
logger?.warn(`[clawmem-serve] ${data.toString().trim()}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.on("exit", (code, signal) => {
|
|
169
|
+
logger?.warn(`[clawmem-serve] exited (code=${code}, signal=${signal})`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
child.unref();
|
|
173
|
+
return child;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse hook output JSON. Returns null on parse failure.
|
|
178
|
+
*/
|
|
179
|
+
export function parseHookOutput(stdout: string): Record<string, unknown> | null {
|
|
180
|
+
if (!stdout.trim()) return null;
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(stdout.trim());
|
|
183
|
+
} catch {
|
|
184
|
+
// Hook output may have non-JSON preamble (stderr leak)
|
|
185
|
+
// Try to find the last JSON object
|
|
186
|
+
const lastBrace = stdout.lastIndexOf("}");
|
|
187
|
+
const firstBrace = stdout.indexOf("{");
|
|
188
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(stdout.slice(firstBrace, lastBrace + 1));
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract additionalContext from hook output.
|
|
201
|
+
* Hooks return: { hookSpecificOutput: { additionalContext: "..." } }
|
|
202
|
+
*/
|
|
203
|
+
export function extractContext(hookOutput: Record<string, unknown> | null): string {
|
|
204
|
+
if (!hookOutput) return "";
|
|
205
|
+
const hso = hookOutput.hookSpecificOutput as Record<string, unknown> | undefined;
|
|
206
|
+
return (hso?.additionalContext as string) || "";
|
|
207
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMem OpenClaw Plugin — Tool registrations
|
|
3
|
+
*
|
|
4
|
+
* Registers a subset of ClawMem's retrieval tools as OpenClaw agent tools.
|
|
5
|
+
* Tools call the ClawMem REST API (clawmem serve) for efficient access.
|
|
6
|
+
*
|
|
7
|
+
* Registered tools (retrieval subset per GPT 5.4 recommendation):
|
|
8
|
+
* - clawmem_search: Unified search (keyword/semantic/hybrid)
|
|
9
|
+
* - clawmem_get: Get document by docid
|
|
10
|
+
* - clawmem_session_log: Recent session summaries
|
|
11
|
+
* - clawmem_timeline: Temporal context around a document
|
|
12
|
+
* - clawmem_similar: Find similar documents
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ClawMemConfig } from "./shell.js";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types (matching OpenClaw's tool interface without importing it)
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
type ToolResult = {
|
|
22
|
+
content: Array<{ type: string; text: string }>;
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Logger = {
|
|
27
|
+
debug?: (msg: string) => void;
|
|
28
|
+
info: (msg: string) => void;
|
|
29
|
+
warn: (msg: string) => void;
|
|
30
|
+
error: (msg: string) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// REST API Client
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
async function apiCall(
|
|
38
|
+
cfg: ClawMemConfig,
|
|
39
|
+
method: string,
|
|
40
|
+
path: string,
|
|
41
|
+
body?: Record<string, unknown>
|
|
42
|
+
): Promise<{ ok: boolean; status: number; data: any }> {
|
|
43
|
+
const url = `http://127.0.0.1:${cfg.servePort}${path}`;
|
|
44
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
45
|
+
|
|
46
|
+
// Add auth token if configured
|
|
47
|
+
const token = cfg.env.CLAWMEM_API_TOKEN || process.env.CLAWMEM_API_TOKEN;
|
|
48
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const resp = await fetch(url, {
|
|
52
|
+
method,
|
|
53
|
+
headers,
|
|
54
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
55
|
+
signal: AbortSignal.timeout(5000),
|
|
56
|
+
});
|
|
57
|
+
const data = await resp.json();
|
|
58
|
+
return { ok: resp.ok, status: resp.status, data };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 0,
|
|
63
|
+
data: { error: `ClawMem API unreachable at ${url}: ${String(err)}` },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Tool Definitions
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
type ToolDef = {
|
|
73
|
+
name: string;
|
|
74
|
+
label: string;
|
|
75
|
+
description: string;
|
|
76
|
+
parameters: Record<string, unknown>;
|
|
77
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function createTools(cfg: ClawMemConfig, logger: Logger): ToolDef[] {
|
|
81
|
+
return [
|
|
82
|
+
// --- Unified Search ---
|
|
83
|
+
{
|
|
84
|
+
name: "clawmem_search",
|
|
85
|
+
label: "ClawMem Search",
|
|
86
|
+
description:
|
|
87
|
+
"Search long-term memory for relevant context. Supports keyword, semantic, and hybrid modes. " +
|
|
88
|
+
"Use for recalling past decisions, preferences, session history, and learned patterns.",
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
query: { type: "string", description: "Search query" },
|
|
93
|
+
mode: {
|
|
94
|
+
type: "string",
|
|
95
|
+
enum: ["auto", "keyword", "semantic", "hybrid"],
|
|
96
|
+
description: "Search mode (default: auto)",
|
|
97
|
+
},
|
|
98
|
+
collection: { type: "string", description: "Limit to specific collection" },
|
|
99
|
+
limit: { type: "number", description: "Max results (default: 10, max: 50)" },
|
|
100
|
+
compact: { type: "boolean", description: "Return compact results (default: true)" },
|
|
101
|
+
},
|
|
102
|
+
required: ["query"],
|
|
103
|
+
},
|
|
104
|
+
async execute(_id, params) {
|
|
105
|
+
const result = await apiCall(cfg, "POST", "/search", {
|
|
106
|
+
query: params.query as string,
|
|
107
|
+
mode: params.mode ?? "auto",
|
|
108
|
+
collection: params.collection,
|
|
109
|
+
limit: params.limit ?? 10,
|
|
110
|
+
compact: params.compact ?? true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!result.ok) {
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: `Search failed: ${result.data?.error || "unknown error"}` }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = result.data;
|
|
120
|
+
if (!data.results || data.results.length === 0) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: "No relevant memories found." }],
|
|
123
|
+
details: { count: 0 },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const text = data.results
|
|
128
|
+
.map((r: any, i: number) =>
|
|
129
|
+
`${i + 1}. [${r.contentType || "note"}] ${r.title || r.path} (score: ${r.score})${r.snippet ? `\n ${r.snippet}` : ""}`
|
|
130
|
+
)
|
|
131
|
+
.join("\n");
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: `Found ${data.count} results:\n\n${text}` }],
|
|
135
|
+
details: { count: data.count, query: data.query, mode: data.mode },
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// --- Get Document ---
|
|
141
|
+
{
|
|
142
|
+
name: "clawmem_get",
|
|
143
|
+
label: "ClawMem Get",
|
|
144
|
+
description:
|
|
145
|
+
"Retrieve full content of a specific memory document by its docid (6-char hex prefix).",
|
|
146
|
+
parameters: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
docid: { type: "string", description: "Document ID (6-char hex prefix from search results)" },
|
|
150
|
+
},
|
|
151
|
+
required: ["docid"],
|
|
152
|
+
},
|
|
153
|
+
async execute(_id, params) {
|
|
154
|
+
const docid = params.docid as string;
|
|
155
|
+
const result = await apiCall(cfg, "GET", `/documents/${docid}`);
|
|
156
|
+
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: "text", text: `Document not found: ${docid}` }],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const d = result.data;
|
|
164
|
+
return {
|
|
165
|
+
content: [{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: `# ${d.title || d.path}\n\nCollection: ${d.collection}\nModified: ${d.modifiedAt}\n\n${d.body}`,
|
|
168
|
+
}],
|
|
169
|
+
details: { docid: d.docid, path: d.path },
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// --- Session Log ---
|
|
175
|
+
{
|
|
176
|
+
name: "clawmem_session_log",
|
|
177
|
+
label: "ClawMem Sessions",
|
|
178
|
+
description:
|
|
179
|
+
"List recent session summaries. Use when asked about past work, previous conversations, or session history.",
|
|
180
|
+
parameters: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
limit: { type: "number", description: "Number of sessions to return (default: 5)" },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
async execute(_id, params) {
|
|
187
|
+
const limit = (params.limit as number) || 5;
|
|
188
|
+
const result = await apiCall(cfg, "GET", `/sessions?limit=${limit}`);
|
|
189
|
+
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text", text: `Failed to retrieve sessions: ${result.data?.error}` }],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sessions = result.data.sessions;
|
|
197
|
+
if (!sessions || sessions.length === 0) {
|
|
198
|
+
return { content: [{ type: "text", text: "No session history found." }] };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const text = sessions
|
|
202
|
+
.map((s: any, i: number) =>
|
|
203
|
+
`${i + 1}. [${s.started_at}] ${s.session_id?.slice(0, 8)}... — ${s.prompt_count || 0} prompts`
|
|
204
|
+
)
|
|
205
|
+
.join("\n");
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: `Recent sessions:\n\n${text}` }],
|
|
209
|
+
details: { count: sessions.length },
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// --- Timeline ---
|
|
215
|
+
{
|
|
216
|
+
name: "clawmem_timeline",
|
|
217
|
+
label: "ClawMem Timeline",
|
|
218
|
+
description:
|
|
219
|
+
"Show temporal context around a document — what was created before and after it.",
|
|
220
|
+
parameters: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
docid: { type: "string", description: "Document ID (6-char hex prefix)" },
|
|
224
|
+
before: { type: "number", description: "Documents before (default: 5)" },
|
|
225
|
+
after: { type: "number", description: "Documents after (default: 5)" },
|
|
226
|
+
same_collection: { type: "boolean", description: "Limit to same collection (default: false)" },
|
|
227
|
+
},
|
|
228
|
+
required: ["docid"],
|
|
229
|
+
},
|
|
230
|
+
async execute(_id, params) {
|
|
231
|
+
const docid = params.docid as string;
|
|
232
|
+
const before = params.before ?? 5;
|
|
233
|
+
const after = params.after ?? 5;
|
|
234
|
+
const sameCol = params.same_collection ?? false;
|
|
235
|
+
const qs = `before=${before}&after=${after}&same_collection=${sameCol}`;
|
|
236
|
+
const result = await apiCall(cfg, "GET", `/timeline/${docid}?${qs}`);
|
|
237
|
+
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text", text: `Timeline failed: ${result.data?.error}` }],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const d = result.data;
|
|
245
|
+
const lines: string[] = [];
|
|
246
|
+
if (d.before?.length) {
|
|
247
|
+
lines.push("**Before:**");
|
|
248
|
+
for (const e of d.before) lines.push(` - [${e.modifiedAt}] ${e.title} (${e.collection})`);
|
|
249
|
+
}
|
|
250
|
+
lines.push(`**→ ${d.anchor?.title || docid}** [${d.anchor?.modifiedAt}]`);
|
|
251
|
+
if (d.after?.length) {
|
|
252
|
+
lines.push("**After:**");
|
|
253
|
+
for (const e of d.after) lines.push(` - [${e.modifiedAt}] ${e.title} (${e.collection})`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
258
|
+
details: { docid, before: d.before?.length, after: d.after?.length },
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// --- Similar ---
|
|
264
|
+
{
|
|
265
|
+
name: "clawmem_similar",
|
|
266
|
+
label: "ClawMem Similar",
|
|
267
|
+
description:
|
|
268
|
+
"Find documents semantically similar to a given document. Use for discovery and context expansion.",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
docid: { type: "string", description: "Document ID (6-char hex prefix)" },
|
|
273
|
+
limit: { type: "number", description: "Max results (default: 5)" },
|
|
274
|
+
},
|
|
275
|
+
required: ["docid"],
|
|
276
|
+
},
|
|
277
|
+
async execute(_id, params) {
|
|
278
|
+
const docid = params.docid as string;
|
|
279
|
+
const limit = params.limit ?? 5;
|
|
280
|
+
const result = await apiCall(cfg, "GET", `/graph/similar/${docid}?limit=${limit}`);
|
|
281
|
+
|
|
282
|
+
if (!result.ok) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: `Similar search failed: ${result.data?.error}` }],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const similar = result.data.similar;
|
|
289
|
+
if (!similar || similar.length === 0) {
|
|
290
|
+
return { content: [{ type: "text", text: "No similar documents found." }] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const text = similar
|
|
294
|
+
.map((s: any, i: number) => `${i + 1}. ${s.title || s.path} (similarity: ${s.score})`)
|
|
295
|
+
.join("\n");
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: `Similar documents:\n\n${text}` }],
|
|
299
|
+
details: { count: similar.length },
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
}
|