memorylake-openclaw 1.1.2 → 1.1.4
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/dist/index.js +1794 -0
- package/dist/index.js.map +1 -0
- package/package.json +18 -1
- package/.github/workflows/release.yml +0 -23
- package/CHANGELOG.md +0 -55
- package/docs/openclaw.mdx +0 -110
- package/index.ts +0 -65
- package/lib/cli/register-cli.ts +0 -134
- package/lib/config.ts +0 -105
- package/lib/core-bridge.ts +0 -155
- package/lib/helpers/parse-content-disposition.ts +0 -21
- package/lib/helpers/rewrite-query.ts +0 -122
- package/lib/helpers/upload-record.ts +0 -47
- package/lib/hooks/auto-capture.ts +0 -97
- package/lib/hooks/auto-recall.ts +0 -89
- package/lib/hooks/auto-upload.ts +0 -72
- package/lib/plugin-context.ts +0 -77
- package/lib/prompt/register-prompt.ts +0 -66
- package/lib/provider.ts +0 -227
- package/lib/tools/document-tools.ts +0 -100
- package/lib/tools/memory-tools.ts +0 -298
- package/lib/tools/search-tools.ts +0 -288
- package/lib/types.ts +0 -273
- package/lib/utils/builders.ts +0 -127
- package/lib/utils/config-parser.ts +0 -14
- package/lib/utils/normalizers.ts +0 -76
- package/test/json5_config_smoke.test.mjs +0 -104
- package/test/path_reg.test.mjs +0 -197
package/lib/hooks/auto-recall.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { PluginContext } from "../plugin-context";
|
|
2
|
-
import type { OpenDataIndustry } from "../types";
|
|
3
|
-
import { getProvider } from "../provider";
|
|
4
|
-
|
|
5
|
-
const sessionIndustriesCache = new Map<string, OpenDataIndustry[]>();
|
|
6
|
-
|
|
7
|
-
export const MEMORYLAKE_REMINDER = [
|
|
8
|
-
`[MEMORYLAKE REMINDER] Before responding to this message, call \`retrieve_context\` first`,
|
|
9
|
-
`to fetch relevant memories and documents. Do not skip this step.`,
|
|
10
|
-
].join(" ");
|
|
11
|
-
|
|
12
|
-
export function registerAutoRecall(pctx: PluginContext): void {
|
|
13
|
-
const { api, resolveConfig } = pctx;
|
|
14
|
-
|
|
15
|
-
api.on("before_prompt_build", async (event, ctx) => {
|
|
16
|
-
if ((ctx as any)?.trigger !== "user") {
|
|
17
|
-
api.logger.info(`memorylake-openclaw: auto-recall skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (!event.prompt) return;
|
|
21
|
-
|
|
22
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
23
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
24
|
-
|
|
25
|
-
const sessionId = (ctx as any)?.sessionId ?? undefined;
|
|
26
|
-
|
|
27
|
-
// Fetch industries once per session, then cache
|
|
28
|
-
let industries: OpenDataIndustry[] | undefined;
|
|
29
|
-
if (sessionId && sessionIndustriesCache.has(sessionId)) {
|
|
30
|
-
industries = sessionIndustriesCache.get(sessionId);
|
|
31
|
-
} else {
|
|
32
|
-
try {
|
|
33
|
-
const projectInfo = await effectiveProvider.getProject();
|
|
34
|
-
industries = projectInfo.industries;
|
|
35
|
-
if (sessionId) {
|
|
36
|
-
sessionIndustriesCache.set(sessionId, industries);
|
|
37
|
-
}
|
|
38
|
-
} catch (err) {
|
|
39
|
-
api.logger.warn(`memorylake-openclaw: project info fetch failed: ${String(err)}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const result: { prependContext?: string; prependSystemContext?: string; appendSystemContext?: string } = {};
|
|
44
|
-
|
|
45
|
-
// Per-turn reminder injected right before the user's message.
|
|
46
|
-
// Survives long conversations because it is not buried in the system prompt.
|
|
47
|
-
result.prependContext = MEMORYLAKE_REMINDER;
|
|
48
|
-
|
|
49
|
-
const memoryDirectiveParts: string[] = [
|
|
50
|
-
`<memory-instructions>`,
|
|
51
|
-
`You have access to the user's long-term memory AND documents via MemoryLake (retrieve_context tool).`,
|
|
52
|
-
`The retrieve_context tool searches both memories and uploaded documents in a single call.`,
|
|
53
|
-
``,
|
|
54
|
-
`MANDATORY PROCEDURE — applies to EVERY user message, including follow-ups in multi-turn conversations:`,
|
|
55
|
-
`1. BEFORE generating any text, call retrieve_context with a query derived from the current message.`,
|
|
56
|
-
`2. Use the search results (memories + documents) to personalize your response.`,
|
|
57
|
-
`3. Respond to the user.`,
|
|
58
|
-
``,
|
|
59
|
-
`COMMON MISTAKES TO AVOID:`,
|
|
60
|
-
`- Searching only at the start of a conversation and skipping subsequent messages — WRONG.`,
|
|
61
|
-
`- Seeing a task-oriented message (e.g. "check weather", "translate this") and skipping retrieve_context — WRONG.`,
|
|
62
|
-
`- Deciding retrieve_context is "not useful" for this particular message — WRONG. Always search.`,
|
|
63
|
-
`- Generating any text response before calling retrieve_context — WRONG.`,
|
|
64
|
-
``,
|
|
65
|
-
`The rule is absolute: retrieve_context FIRST, then respond. Every message. No exceptions.`,
|
|
66
|
-
`</memory-instructions>`,
|
|
67
|
-
];
|
|
68
|
-
result.prependSystemContext = memoryDirectiveParts.join("\n");
|
|
69
|
-
|
|
70
|
-
const appendParts: string[] = [];
|
|
71
|
-
if (industries && industries.length > 0) {
|
|
72
|
-
const categoryList = industries
|
|
73
|
-
.map((ind) => `- ${ind.id}: ${ind.name}${ind.description ? ` — ${ind.description}` : ""}`)
|
|
74
|
-
.join("\n");
|
|
75
|
-
appendParts.push(
|
|
76
|
-
`<open-data-categories>\nThis project has access to the following open data categories via the open_data_search tool:\n${categoryList}\nWhen the user's question relates to any of these categories, use the open_data_search tool to retrieve relevant data.\n</open-data-categories>`,
|
|
77
|
-
);
|
|
78
|
-
api.logger.info(
|
|
79
|
-
`memorylake-openclaw: injecting ${industries.length} open data categories into system context`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (appendParts.length > 0) {
|
|
84
|
-
result.appendSystemContext = appendParts.join("\n\n");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result;
|
|
88
|
-
});
|
|
89
|
-
}
|
package/lib/hooks/auto-upload.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { PluginContext } from "../plugin-context";
|
|
4
|
-
import type { UploadFn } from "../types";
|
|
5
|
-
import { getUploadedRecord, saveUploadedRecord, needsUpload, extractInboundPaths } from "../helpers/upload-record";
|
|
6
|
-
|
|
7
|
-
export function registerAutoUpload(pctx: PluginContext): void {
|
|
8
|
-
const { api, resolveConfig } = pctx;
|
|
9
|
-
|
|
10
|
-
// Lazy-load upload function from upload.mjs
|
|
11
|
-
let uploadAutoFn: UploadFn | undefined;
|
|
12
|
-
|
|
13
|
-
api.on("before_prompt_build", (event, ctx) => {
|
|
14
|
-
if ((ctx as any)?.trigger !== "user") {
|
|
15
|
-
api.logger.info(`memorylake-openclaw: auto-upload skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
const workspaceDir = (ctx as any)?.workspaceDir;
|
|
19
|
-
if (!workspaceDir || !event.prompt) return;
|
|
20
|
-
|
|
21
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
22
|
-
const paths = extractInboundPaths(event.prompt);
|
|
23
|
-
if (paths.length === 0) return;
|
|
24
|
-
|
|
25
|
-
const record = getUploadedRecord(workspaceDir);
|
|
26
|
-
const filesToUpload: { filePath: string; stat: fs.Stats }[] = [];
|
|
27
|
-
for (const p of paths) {
|
|
28
|
-
const stat = needsUpload(record, p);
|
|
29
|
-
if (stat) filesToUpload.push({ filePath: p, stat });
|
|
30
|
-
}
|
|
31
|
-
if (filesToUpload.length === 0) return;
|
|
32
|
-
|
|
33
|
-
// Fire-and-forget: upload asynchronously without blocking
|
|
34
|
-
(async () => {
|
|
35
|
-
// Lazy import upload.mjs
|
|
36
|
-
if (!uploadAutoFn) {
|
|
37
|
-
const uploadModule = await import(
|
|
38
|
-
/* webpackIgnore: true */
|
|
39
|
-
new URL("../../skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
|
|
40
|
-
);
|
|
41
|
-
uploadAutoFn = uploadModule.uploadAuto;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
for (const { filePath, stat } of filesToUpload) {
|
|
45
|
-
try {
|
|
46
|
-
await uploadAutoFn!({
|
|
47
|
-
host: effectiveCfg.host,
|
|
48
|
-
apiKey: effectiveCfg.apiKey,
|
|
49
|
-
projectId: effectiveCfg.projectId,
|
|
50
|
-
filePath,
|
|
51
|
-
fileName: path.basename(filePath),
|
|
52
|
-
});
|
|
53
|
-
// Save record only after successful upload to avoid race on crash
|
|
54
|
-
const current = getUploadedRecord(workspaceDir);
|
|
55
|
-
current[filePath] = { mtimeMs: stat.mtimeMs };
|
|
56
|
-
saveUploadedRecord(workspaceDir, current);
|
|
57
|
-
api.logger.info(
|
|
58
|
-
`memorylake-openclaw: auto-uploaded ${path.basename(filePath)}`,
|
|
59
|
-
);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
api.logger.warn(
|
|
62
|
-
`memorylake-openclaw: auto-upload failed for ${filePath}: ${String(err)}`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
})().catch((err) => {
|
|
67
|
-
api.logger.warn(
|
|
68
|
-
`memorylake-openclaw: auto-upload unexpected error: ${String(err)}`,
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
package/lib/plugin-context.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
-
import type { MemoryLakeConfig } from "./types";
|
|
6
|
-
import { ALLOWED_KEYS, memoryLakeConfigSchema } from "./config";
|
|
7
|
-
import { readJson5ConfigFile } from "./utils/config-parser";
|
|
8
|
-
|
|
9
|
-
const PLUGIN_ID = "memorylake-openclaw";
|
|
10
|
-
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Read and parse the plugin config from the global openclaw.json.
|
|
14
|
-
* Returns a fully parsed MemoryLakeConfig, or null if the file is
|
|
15
|
-
* unreadable, missing, or the plugin section is absent/invalid.
|
|
16
|
-
*/
|
|
17
|
-
function readGlobalConfig(logger: OpenClawPluginApi["logger"]): MemoryLakeConfig | null {
|
|
18
|
-
try {
|
|
19
|
-
const raw = readJson5ConfigFile(GLOBAL_CONFIG_PATH) as any;
|
|
20
|
-
const pluginCfg = raw?.plugins?.entries?.[PLUGIN_ID]?.config;
|
|
21
|
-
if (!pluginCfg) {
|
|
22
|
-
logger.info(`memorylake-openclaw: no plugin config found in global config (path: ${GLOBAL_CONFIG_PATH}, pluginId: ${PLUGIN_ID})`);
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
return memoryLakeConfigSchema.parse(pluginCfg);
|
|
26
|
-
} catch (err) {
|
|
27
|
-
logger.warn(`memorylake-openclaw: failed to read or parse global config (path: ${GLOBAL_CONFIG_PATH}): ${String(err)}`);
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface PluginContext {
|
|
33
|
-
api: OpenClawPluginApi;
|
|
34
|
-
resolveConfig: (ctx: any) => MemoryLakeConfig;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function createPluginContext(api: OpenClawPluginApi, initialCfg: MemoryLakeConfig): PluginContext {
|
|
38
|
-
|
|
39
|
-
function resolveConfig(ctx: any): MemoryLakeConfig {
|
|
40
|
-
// Re-read global config on every call so changes take effect without restart
|
|
41
|
-
const globalCfg = readGlobalConfig(api.logger) ?? initialCfg;
|
|
42
|
-
|
|
43
|
-
const workspaceDir = ctx?.workspaceDir;
|
|
44
|
-
if (!workspaceDir) return globalCfg;
|
|
45
|
-
|
|
46
|
-
const localPath = path.join(workspaceDir, ".memorylake", "config.json");
|
|
47
|
-
if (!fs.existsSync(localPath)) return globalCfg;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const raw = JSON.parse(fs.readFileSync(localPath, "utf-8"));
|
|
51
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
52
|
-
api.logger.warn(
|
|
53
|
-
`memorylake-openclaw: workspace config exists but is not a JSON object; falling back to global config (path: ${localPath})`,
|
|
54
|
-
);
|
|
55
|
-
return globalCfg;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const allowed = new Set(ALLOWED_KEYS);
|
|
59
|
-
const overrides: Record<string, unknown> = {};
|
|
60
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
61
|
-
if (allowed.has(key)) overrides[key] = value;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { ...globalCfg, ...overrides } as MemoryLakeConfig;
|
|
65
|
-
} catch (err) {
|
|
66
|
-
api.logger.warn(
|
|
67
|
-
`memorylake-openclaw: failed to parse workspace config JSON; falling back to global config (path: ${localPath}): ${String(err)}`,
|
|
68
|
-
);
|
|
69
|
-
return globalCfg;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
api,
|
|
75
|
-
resolveConfig,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { PluginContext } from "../plugin-context";
|
|
2
|
-
import type { MemoryLakeConfig } from "../types";
|
|
3
|
-
|
|
4
|
-
export function registerMemoryPromptSection(pctx: PluginContext, cfg: MemoryLakeConfig): void {
|
|
5
|
-
pctx.api.registerMemoryPromptSection((params) => {
|
|
6
|
-
const lines: string[] = [
|
|
7
|
-
cfg.autoRecall ? "## Memory (MANDATORY — MemoryLake)" : "## Memory (MemoryLake)",
|
|
8
|
-
"",
|
|
9
|
-
"You have access to MemoryLake, a long-term memory system that stores the user's preferences, history, personal context, and uploaded documents across sessions.",
|
|
10
|
-
"",
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
if (params.availableTools.has("retrieve_context")) {
|
|
14
|
-
if (cfg.autoRecall) {
|
|
15
|
-
lines.push(
|
|
16
|
-
"### retrieve_context — MUST be your FIRST action for EVERY message",
|
|
17
|
-
"",
|
|
18
|
-
"**RULE: Before generating ANY text, call `retrieve_context` first.** This is mandatory for EVERY user message in the conversation — the 1st, 2nd, 5th, 20th, every single one.",
|
|
19
|
-
"",
|
|
20
|
-
"This tool searches BOTH memories AND documents in one call.",
|
|
21
|
-
"",
|
|
22
|
-
"This applies to ALL types of messages:",
|
|
23
|
-
"- Greetings -> search for who the user is",
|
|
24
|
-
"- Recommendations (books, activities, food) -> search for preferences",
|
|
25
|
-
"- Tasks (check weather, translate, write email) -> search for user context",
|
|
26
|
-
"- Follow-up questions in the same conversation -> search again with new query",
|
|
27
|
-
"- Document questions -> search for uploaded document content",
|
|
28
|
-
"- ANY other message -> search for relevant context",
|
|
29
|
-
"",
|
|
30
|
-
"**WRONG behavior (do NOT do this):**",
|
|
31
|
-
"- Searching only on the first message, then skipping for the rest of the conversation",
|
|
32
|
-
"- Deciding a message does not need memory context and skipping the search",
|
|
33
|
-
"- Responding first, then searching (or not searching at all)",
|
|
34
|
-
"",
|
|
35
|
-
"**CORRECT behavior:**",
|
|
36
|
-
"- Every message -> retrieve_context -> then respond. Always. No thinking about whether to skip.",
|
|
37
|
-
"",
|
|
38
|
-
);
|
|
39
|
-
} else {
|
|
40
|
-
lines.push(
|
|
41
|
-
"### retrieve_context",
|
|
42
|
-
"- Use `retrieve_context` to search the user's long-term memories and uploaded documents when relevant context may help your response.",
|
|
43
|
-
"",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (params.availableTools.has("memory_list")) {
|
|
49
|
-
lines.push(
|
|
50
|
-
"### memory_list",
|
|
51
|
-
"- When the user asks what you remember or wants to see all stored memories, call `memory_list`.",
|
|
52
|
-
"",
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (params.availableTools.has("memory_forget")) {
|
|
57
|
-
lines.push(
|
|
58
|
-
"### memory_forget",
|
|
59
|
-
"- When the user explicitly asks to delete or forget a specific memory, call `memory_forget` with the memory ID.",
|
|
60
|
-
"",
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return lines;
|
|
65
|
-
});
|
|
66
|
-
}
|
package/lib/provider.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import got from "got";
|
|
2
|
-
import type { MemoryLakeConfig, AddOptions, SearchOptions, ListOptions, MemoryItem, AddResult, DocumentSearchResponse, WebSearchOptions, WebSearchResponse, OpenDataSearchOptions, OpenDataSearchResponse, ProjectInfo, ConflictItem, ConflictListResponse, MemoryLakeProvider } from "./types";
|
|
3
|
-
import { normalizeMemoryItem, normalizeSearchResults, normalizeAddResult, normalizeWebSearchResponse, normalizeOpenDataSearchResponse, normalizeWebSearchDomain, normalizeOpenDataCategory } from "./utils/normalizers";
|
|
4
|
-
|
|
5
|
-
interface ApiResponse<T = unknown> {
|
|
6
|
-
success: boolean;
|
|
7
|
-
message?: string;
|
|
8
|
-
data?: T;
|
|
9
|
-
error_code?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class PlatformProvider implements MemoryLakeProvider {
|
|
13
|
-
private readonly http: ReturnType<typeof got.extend>;
|
|
14
|
-
private readonly basePath: string;
|
|
15
|
-
private readonly docSearchPath: string;
|
|
16
|
-
private readonly webSearchPath: string;
|
|
17
|
-
private readonly openDataSearchPath: string;
|
|
18
|
-
private readonly projectPath: string;
|
|
19
|
-
private readonly conflictsPath: string;
|
|
20
|
-
private readonly projectId: string;
|
|
21
|
-
|
|
22
|
-
constructor(host: string, apiKey: string, projectId: string) {
|
|
23
|
-
this.projectId = projectId;
|
|
24
|
-
this.basePath = `openapi/memorylake/api/v2/projects/${projectId}/memories`;
|
|
25
|
-
this.docSearchPath = `openapi/memorylake/api/v1/projects/${projectId}/documents/search`;
|
|
26
|
-
this.webSearchPath = "openapi/memorylake/api/v1/search";
|
|
27
|
-
this.openDataSearchPath = "openapi/memorylake/api/v1/search/opendata";
|
|
28
|
-
this.projectPath = `openapi/memorylake/api/v1/projects/${projectId}`;
|
|
29
|
-
this.conflictsPath = `openapi/memorylake/api/v2/projects/${projectId}/memories/conflicts`;
|
|
30
|
-
this.http = got.extend({
|
|
31
|
-
prefixUrl: host,
|
|
32
|
-
headers: {
|
|
33
|
-
Authorization: `Bearer ${apiKey}`,
|
|
34
|
-
},
|
|
35
|
-
responseType: "json",
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async add(
|
|
40
|
-
messages: Array<{ role: string; content: string }>,
|
|
41
|
-
options: AddOptions,
|
|
42
|
-
): Promise<AddResult> {
|
|
43
|
-
const body: Record<string, unknown> = {
|
|
44
|
-
messages,
|
|
45
|
-
user_id: options.user_id,
|
|
46
|
-
infer: options.infer ?? true,
|
|
47
|
-
};
|
|
48
|
-
if (options.chat_session_id) body.chat_session_id = options.chat_session_id;
|
|
49
|
-
if (options.metadata) body.metadata = options.metadata;
|
|
50
|
-
|
|
51
|
-
const resp = await this.http
|
|
52
|
-
.post(this.basePath, { json: body })
|
|
53
|
-
.json<ApiResponse>();
|
|
54
|
-
if (!resp.success) throw new Error(resp.message ?? "add failed");
|
|
55
|
-
return normalizeAddResult(resp.data);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
|
59
|
-
const body: Record<string, unknown> = {
|
|
60
|
-
query,
|
|
61
|
-
user_id: options.user_id,
|
|
62
|
-
with_conflicts: true,
|
|
63
|
-
};
|
|
64
|
-
if (options.top_k != null) body.top_k = options.top_k;
|
|
65
|
-
if (options.threshold != null) body.threshold = options.threshold;
|
|
66
|
-
if (options.rerank != null) body.rerank = options.rerank;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const resp = await this.http
|
|
70
|
-
.post(`${this.basePath}/search`, { json: body })
|
|
71
|
-
.json<ApiResponse>();
|
|
72
|
-
if (!resp.success) throw new Error(resp.message ?? "search failed");
|
|
73
|
-
|
|
74
|
-
return normalizeSearchResults(resp.data);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async get(memoryId: string): Promise<MemoryItem> {
|
|
78
|
-
const resp = await this.http
|
|
79
|
-
.get(`${this.basePath}/${memoryId}`)
|
|
80
|
-
.json<ApiResponse>();
|
|
81
|
-
if (!resp.success) throw new Error(resp.message ?? "get failed");
|
|
82
|
-
return normalizeMemoryItem(resp.data);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
|
86
|
-
const searchParams: Record<string, string | number> = {};
|
|
87
|
-
if (options.user_id) searchParams.user_id = options.user_id;
|
|
88
|
-
if (options.page != null) searchParams.page = options.page;
|
|
89
|
-
if (options.size != null) searchParams.size = options.size;
|
|
90
|
-
|
|
91
|
-
const resp = await this.http
|
|
92
|
-
.get(this.basePath, { searchParams })
|
|
93
|
-
.json<ApiResponse>();
|
|
94
|
-
if (!resp.success) throw new Error(resp.message ?? "getAll failed");
|
|
95
|
-
const data = resp.data as any;
|
|
96
|
-
if (data?.items && Array.isArray(data.items))
|
|
97
|
-
return data.items.map(normalizeMemoryItem);
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async delete(memoryId: string): Promise<void> {
|
|
102
|
-
const resp = await this.http
|
|
103
|
-
.delete(`${this.basePath}/${memoryId}`)
|
|
104
|
-
.json<ApiResponse>();
|
|
105
|
-
if (!resp.success) throw new Error(resp.message ?? "delete failed");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse> {
|
|
109
|
-
const resp = await this.http
|
|
110
|
-
.post(this.docSearchPath, { json: { query, top_N: topN } })
|
|
111
|
-
.json<ApiResponse>();
|
|
112
|
-
if (!resp.success) throw new Error(resp.message ?? "document search failed");
|
|
113
|
-
const data = resp.data as any;
|
|
114
|
-
return {
|
|
115
|
-
count: data?.count ?? 0,
|
|
116
|
-
results: Array.isArray(data?.results) ? data.results : [],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async getDocumentDownloadUrl(documentId: string): Promise<string> {
|
|
121
|
-
const downloadPath = `openapi/memorylake/api/v1/projects/${this.projectId}/documents/${documentId}/download`;
|
|
122
|
-
const resp = await this.http.get(downloadPath, {
|
|
123
|
-
followRedirect: false,
|
|
124
|
-
responseType: "text" as any,
|
|
125
|
-
throwHttpErrors: false,
|
|
126
|
-
});
|
|
127
|
-
if (resp.statusCode === 303 || resp.statusCode === 302) {
|
|
128
|
-
const location = resp.headers.location;
|
|
129
|
-
if (!location) throw new Error("Download redirect missing Location header");
|
|
130
|
-
return location;
|
|
131
|
-
}
|
|
132
|
-
if (resp.statusCode === 404) {
|
|
133
|
-
throw new Error(`Document not found: ${documentId}`);
|
|
134
|
-
}
|
|
135
|
-
throw new Error(`Unexpected download response status: ${resp.statusCode}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse> {
|
|
139
|
-
const domain = options.domain != null ? normalizeWebSearchDomain(options.domain) : "web";
|
|
140
|
-
const body: Record<string, unknown> = {
|
|
141
|
-
query,
|
|
142
|
-
domain,
|
|
143
|
-
};
|
|
144
|
-
if (options.max_results != null) body.max_results = options.max_results;
|
|
145
|
-
if (options.start_date) body.start_date = options.start_date;
|
|
146
|
-
if (options.end_date) body.end_date = options.end_date;
|
|
147
|
-
if (options.include_domains?.length) body.include_domains = options.include_domains;
|
|
148
|
-
if (options.exclude_domains?.length) body.exclude_domains = options.exclude_domains;
|
|
149
|
-
if (options.user_location) body.user_location = options.user_location;
|
|
150
|
-
|
|
151
|
-
const resp = await this.http
|
|
152
|
-
.post(this.webSearchPath, { json: body })
|
|
153
|
-
.json<WebSearchResponse>();
|
|
154
|
-
return normalizeWebSearchResponse(resp);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async searchOpenData(query: string, options: OpenDataSearchOptions): Promise<OpenDataSearchResponse> {
|
|
158
|
-
const body: Record<string, unknown> = { query };
|
|
159
|
-
if (options.dataset != null) {
|
|
160
|
-
const ds = normalizeOpenDataCategory(options.dataset);
|
|
161
|
-
if (!ds) throw new Error(`Invalid open data dataset: "${options.dataset}"`);
|
|
162
|
-
body.dataset = ds;
|
|
163
|
-
}
|
|
164
|
-
if (options.max_results != null) body.max_results = options.max_results;
|
|
165
|
-
if (options.start_date) body.start_date = options.start_date;
|
|
166
|
-
if (options.end_date) body.end_date = options.end_date;
|
|
167
|
-
|
|
168
|
-
const resp = await this.http
|
|
169
|
-
.post(this.openDataSearchPath, { json: body })
|
|
170
|
-
.json<OpenDataSearchResponse>();
|
|
171
|
-
return normalizeOpenDataSearchResponse(resp);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async getProject(): Promise<ProjectInfo> {
|
|
175
|
-
const resp = await this.http
|
|
176
|
-
.get(this.projectPath)
|
|
177
|
-
.json<ApiResponse<{ id?: string; name?: string; description?: string; industries?: Array<{ id?: string; name?: string; description?: string }> }>>();
|
|
178
|
-
if (!resp.success) throw new Error(resp.message ?? "get project failed");
|
|
179
|
-
const data = resp.data;
|
|
180
|
-
const info: ProjectInfo = {
|
|
181
|
-
id: data?.id ?? "",
|
|
182
|
-
name: data?.name ?? "",
|
|
183
|
-
description: data?.description,
|
|
184
|
-
industries: Array.isArray(data?.industries)
|
|
185
|
-
? data.industries.map((ind) => ({
|
|
186
|
-
id: ind.id ?? "",
|
|
187
|
-
name: ind.name ?? "",
|
|
188
|
-
description: ind.description,
|
|
189
|
-
}))
|
|
190
|
-
: [],
|
|
191
|
-
};
|
|
192
|
-
return info;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async listConflicts(memoryIds: string[], userId: string): Promise<ConflictItem[]> {
|
|
196
|
-
if (memoryIds.length === 0) return [];
|
|
197
|
-
const searchParams: Record<string, string> = {
|
|
198
|
-
resolved: "false",
|
|
199
|
-
memory_ids: memoryIds.join(","),
|
|
200
|
-
};
|
|
201
|
-
const resp = await this.http
|
|
202
|
-
.get(this.conflictsPath, {
|
|
203
|
-
searchParams
|
|
204
|
-
})
|
|
205
|
-
.json<ApiResponse<ConflictListResponse>>();
|
|
206
|
-
if (!resp.success) throw new Error(resp.message ?? "list conflicts failed");
|
|
207
|
-
const data = resp.data;
|
|
208
|
-
return Array.isArray(data?.items) ? data.items : [];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// ============================================================================
|
|
214
|
-
// Provider Cache
|
|
215
|
-
// ============================================================================
|
|
216
|
-
|
|
217
|
-
const providerCache = new Map<string, MemoryLakeProvider>();
|
|
218
|
-
|
|
219
|
-
export function getProvider(effectiveCfg: MemoryLakeConfig): MemoryLakeProvider {
|
|
220
|
-
const key = `${effectiveCfg.host}|${effectiveCfg.apiKey}|${effectiveCfg.projectId}`;
|
|
221
|
-
let p = providerCache.get(key);
|
|
222
|
-
if (!p) {
|
|
223
|
-
p = new PlatformProvider(effectiveCfg.host, effectiveCfg.apiKey, effectiveCfg.projectId);
|
|
224
|
-
providerCache.set(key, p);
|
|
225
|
-
}
|
|
226
|
-
return p;
|
|
227
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import got from "got";
|
|
5
|
-
import { pipeline } from "node:stream/promises";
|
|
6
|
-
import { Type } from "@sinclair/typebox";
|
|
7
|
-
import type { PluginContext } from "../plugin-context";
|
|
8
|
-
import { getProvider } from "../provider";
|
|
9
|
-
import { parseContentDispositionFilename } from "../helpers/parse-content-disposition";
|
|
10
|
-
|
|
11
|
-
export function registerDocumentTools(pctx: PluginContext): void {
|
|
12
|
-
const { api, resolveConfig } = pctx;
|
|
13
|
-
|
|
14
|
-
api.registerTool(
|
|
15
|
-
(ctx) => ({
|
|
16
|
-
name: "document_download",
|
|
17
|
-
label: "Document Download",
|
|
18
|
-
description:
|
|
19
|
-
"Download a document (image, PDF, etc.) from MemoryLake to local disk. After calling this tool, you MUST call the `message` tool with action='send' and media=<the returned local file path> to deliver the file to the user.",
|
|
20
|
-
parameters: Type.Object({
|
|
21
|
-
documentId: Type.String({
|
|
22
|
-
description:
|
|
23
|
-
"The document ID to download (from retrieve_context results or document listing)",
|
|
24
|
-
}),
|
|
25
|
-
}),
|
|
26
|
-
async execute(_toolCallId, params) {
|
|
27
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
28
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
29
|
-
const { documentId } = params as { documentId: string };
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
// 1. Get pre-signed download URL (API returns 303 redirect to object storage)
|
|
33
|
-
const downloadUrl =
|
|
34
|
-
await effectiveProvider.getDocumentDownloadUrl(documentId);
|
|
35
|
-
|
|
36
|
-
// 2. Determine local save directory (cross-platform)
|
|
37
|
-
const workspaceDir = (ctx as any)?.workspaceDir;
|
|
38
|
-
const downloadDir = workspaceDir
|
|
39
|
-
? path.join(workspaceDir, ".memorylake", "downloads")
|
|
40
|
-
: path.join(os.tmpdir(), "memorylake-downloads");
|
|
41
|
-
fs.mkdirSync(downloadDir, { recursive: true });
|
|
42
|
-
|
|
43
|
-
// 3. Download file to a temp path and extract filename from the
|
|
44
|
-
// object storage response's Content-Disposition header.
|
|
45
|
-
// The API redirect URL includes a `response-content-disposition` query
|
|
46
|
-
// param which instructs object storage to return the header with the
|
|
47
|
-
// original filename — this is the sole reliable source of the filename.
|
|
48
|
-
const tempPath = path.join(downloadDir, `.dl-${documentId}.tmp`);
|
|
49
|
-
let cdFileName: string | null = null;
|
|
50
|
-
const stream = got.stream(downloadUrl);
|
|
51
|
-
stream.on("response", (resp: any) => {
|
|
52
|
-
cdFileName = parseContentDispositionFilename(
|
|
53
|
-
resp.headers?.["content-disposition"],
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
await pipeline(stream, fs.createWriteStream(tempPath));
|
|
57
|
-
|
|
58
|
-
// 4. Use Content-Disposition filename; fall back to documentId if absent
|
|
59
|
-
const finalName = cdFileName || documentId;
|
|
60
|
-
|
|
61
|
-
// 5. Resolve filename collisions by appending a numeric suffix
|
|
62
|
-
let localPath = path.join(downloadDir, finalName);
|
|
63
|
-
if (fs.existsSync(localPath)) {
|
|
64
|
-
const ext = path.extname(finalName);
|
|
65
|
-
const base = finalName.slice(0, finalName.length - ext.length);
|
|
66
|
-
let counter = 1;
|
|
67
|
-
while (fs.existsSync(localPath)) {
|
|
68
|
-
localPath = path.join(downloadDir, `${base}-${counter}${ext}`);
|
|
69
|
-
counter++;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 6. Rename temp to final name
|
|
74
|
-
fs.renameSync(tempPath, localPath);
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
content: [
|
|
78
|
-
{
|
|
79
|
-
type: "text",
|
|
80
|
-
text: `Document ${documentId} downloaded to:\n${localPath}\n\nYou MUST now call the message tool with action="send" and media set to this local path to deliver the file to the user.`,
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
details: { documentId, localPath },
|
|
84
|
-
};
|
|
85
|
-
} catch (err) {
|
|
86
|
-
return {
|
|
87
|
-
content: [
|
|
88
|
-
{
|
|
89
|
-
type: "text",
|
|
90
|
-
text: `Document download failed: ${String(err)}`,
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
details: { error: String(err) },
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
}),
|
|
98
|
-
{ name: "document_download" },
|
|
99
|
-
);
|
|
100
|
-
}
|