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/cli/register-cli.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { PluginContext } from "../plugin-context";
|
|
4
|
-
import type { MemoryLakeConfig, UploadFn } from "../types";
|
|
5
|
-
import { getProvider } from "../provider";
|
|
6
|
-
import { buildSearchOptions } from "../utils/builders";
|
|
7
|
-
import { readJson5ConfigFile } from "../utils/config-parser";
|
|
8
|
-
|
|
9
|
-
export function registerCli(pctx: PluginContext, cfg: MemoryLakeConfig): void {
|
|
10
|
-
const { api, resolveConfig } = pctx;
|
|
11
|
-
const provider = getProvider(cfg);
|
|
12
|
-
|
|
13
|
-
api.registerCli(
|
|
14
|
-
({ program }) => {
|
|
15
|
-
const memorylake = program
|
|
16
|
-
.command("memorylake")
|
|
17
|
-
.description("MemoryLake memory plugin commands");
|
|
18
|
-
|
|
19
|
-
memorylake
|
|
20
|
-
.command("search")
|
|
21
|
-
.description("Search memories in MemoryLake")
|
|
22
|
-
.argument("<query>", "Search query")
|
|
23
|
-
.option("--limit <n>", "Max results", String(cfg.topK))
|
|
24
|
-
.action(async (query: string, opts: { limit: string }) => {
|
|
25
|
-
try {
|
|
26
|
-
const limit = parseInt(opts.limit, 10);
|
|
27
|
-
const results = await provider.search(
|
|
28
|
-
query,
|
|
29
|
-
buildSearchOptions(cfg, undefined, limit),
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
if (!results.length) {
|
|
33
|
-
console.log("No memories found.");
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const output = results.map((r) => ({
|
|
38
|
-
id: r.id,
|
|
39
|
-
content: r.content,
|
|
40
|
-
user_id: r.user_id,
|
|
41
|
-
created_at: r.created_at,
|
|
42
|
-
}));
|
|
43
|
-
console.log(JSON.stringify(output, null, 2));
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.error(`Search failed: ${String(err)}`);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
memorylake
|
|
50
|
-
.command("upload")
|
|
51
|
-
.description("Upload files or directories to MemoryLake")
|
|
52
|
-
.argument("<path>", "File or directory path to upload")
|
|
53
|
-
.option("--agent <id>", "Agent ID (resolves workspace and per-agent projectId)")
|
|
54
|
-
.option("--project-id <id>", "Override project ID (takes precedence over --agent)")
|
|
55
|
-
.action(async (targetPath: string, opts: { agent?: string; projectId?: string }) => {
|
|
56
|
-
// Resolve effective config: --project-id > agent workspace config > global config
|
|
57
|
-
let effectiveCfg = cfg;
|
|
58
|
-
if (opts.agent) {
|
|
59
|
-
try {
|
|
60
|
-
const openclawPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
61
|
-
const openclaw = readJson5ConfigFile(openclawPath) as any;
|
|
62
|
-
const agents = openclaw?.agents;
|
|
63
|
-
const agentEntry = agents?.list?.find((a: any) => a.id === opts.agent);
|
|
64
|
-
const workspace = agentEntry?.workspace || agents?.defaults?.workspace;
|
|
65
|
-
if (workspace) {
|
|
66
|
-
effectiveCfg = resolveConfig({ workspaceDir: workspace });
|
|
67
|
-
} else {
|
|
68
|
-
console.warn(`Warning: no workspace found for agent "${opts.agent}", using global config.`);
|
|
69
|
-
}
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.warn(`Warning: failed to resolve agent config: ${String(err)}, using global config.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const effectiveProjectId = opts.projectId || effectiveCfg.projectId;
|
|
75
|
-
if (!effectiveProjectId) {
|
|
76
|
-
console.error("No project ID configured. Use --project-id or set up agent/workspace config.");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (!effectiveCfg.host || !effectiveCfg.apiKey) {
|
|
80
|
-
console.error("Missing host or apiKey in config. Check your MemoryLake configuration.");
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Lazy import upload.mjs (use uploadAuto to support archives)
|
|
85
|
-
let uploadFn: UploadFn;
|
|
86
|
-
try {
|
|
87
|
-
const uploadModule = await import(
|
|
88
|
-
/* webpackIgnore: true */
|
|
89
|
-
new URL("../../skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
|
|
90
|
-
);
|
|
91
|
-
uploadFn = uploadModule.uploadAuto;
|
|
92
|
-
} catch (err) {
|
|
93
|
-
console.error(`Failed to load upload module: ${String(err)}`);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const absPath = path.resolve(targetPath);
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
await uploadFn({
|
|
101
|
-
host: effectiveCfg.host,
|
|
102
|
-
apiKey: effectiveCfg.apiKey,
|
|
103
|
-
projectId: effectiveProjectId,
|
|
104
|
-
filePath: absPath,
|
|
105
|
-
fileName: path.basename(absPath),
|
|
106
|
-
});
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.error(`Upload failed: ${String(err)}`);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
memorylake
|
|
113
|
-
.command("stats")
|
|
114
|
-
.description("Show memory statistics from MemoryLake")
|
|
115
|
-
.action(async () => {
|
|
116
|
-
try {
|
|
117
|
-
const memories = await provider.getAll({
|
|
118
|
-
user_id: cfg.userId,
|
|
119
|
-
});
|
|
120
|
-
console.log(`User: ${cfg.userId}`);
|
|
121
|
-
console.log(
|
|
122
|
-
`Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
|
|
123
|
-
);
|
|
124
|
-
console.log(
|
|
125
|
-
`Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`,
|
|
126
|
-
);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.error(`Stats failed: ${String(err)}`);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
{ commands: ["memorylake"] },
|
|
133
|
-
);
|
|
134
|
-
}
|
package/lib/config.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { MemoryLakeConfig } from "./types";
|
|
2
|
-
import { DEFAULT_USER_ID } from "./types";
|
|
3
|
-
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// Config Schema
|
|
6
|
-
// ============================================================================
|
|
7
|
-
|
|
8
|
-
export const ALLOWED_KEYS = [
|
|
9
|
-
"host",
|
|
10
|
-
"apiKey",
|
|
11
|
-
"projectId",
|
|
12
|
-
"userId",
|
|
13
|
-
"autoCapture",
|
|
14
|
-
"autoRecall",
|
|
15
|
-
"autoUpload",
|
|
16
|
-
"searchThreshold",
|
|
17
|
-
"topK",
|
|
18
|
-
"rerank",
|
|
19
|
-
"webSearchIncludeDomains",
|
|
20
|
-
"webSearchExcludeDomains",
|
|
21
|
-
"webSearchCountry",
|
|
22
|
-
"webSearchTimezone",
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
function assertAllowedKeys(
|
|
26
|
-
value: Record<string, unknown>,
|
|
27
|
-
allowed: string[],
|
|
28
|
-
label: string,
|
|
29
|
-
) {
|
|
30
|
-
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
31
|
-
if (unknown.length === 0) return;
|
|
32
|
-
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function parseOptionalStringArray(
|
|
36
|
-
value: unknown,
|
|
37
|
-
label: string,
|
|
38
|
-
): string[] | undefined {
|
|
39
|
-
if (value == null) return undefined;
|
|
40
|
-
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
41
|
-
throw new Error(`${label} must be an array of strings`);
|
|
42
|
-
}
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseOptionalString(
|
|
47
|
-
value: unknown,
|
|
48
|
-
label: string,
|
|
49
|
-
): string | undefined {
|
|
50
|
-
if (value == null) return undefined;
|
|
51
|
-
if (typeof value !== "string") {
|
|
52
|
-
throw new Error(`${label} must be a string`);
|
|
53
|
-
}
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const memoryLakeConfigSchema = {
|
|
58
|
-
parse(value: unknown): MemoryLakeConfig {
|
|
59
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
60
|
-
throw new Error("memorylake-openclaw config required");
|
|
61
|
-
}
|
|
62
|
-
const cfg = value as Record<string, unknown>;
|
|
63
|
-
assertAllowedKeys(cfg, ALLOWED_KEYS, "memorylake-openclaw config");
|
|
64
|
-
|
|
65
|
-
if (typeof cfg.apiKey !== "string" || !cfg.apiKey) {
|
|
66
|
-
throw new Error("apiKey is required");
|
|
67
|
-
}
|
|
68
|
-
if (typeof cfg.projectId !== "string" || !cfg.projectId) {
|
|
69
|
-
throw new Error("projectId is required");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
host:
|
|
74
|
-
typeof cfg.host === "string" && cfg.host
|
|
75
|
-
? cfg.host
|
|
76
|
-
: "https://app.memorylake.ai",
|
|
77
|
-
apiKey: cfg.apiKey as string,
|
|
78
|
-
projectId: cfg.projectId as string,
|
|
79
|
-
userId: DEFAULT_USER_ID,
|
|
80
|
-
autoCapture: cfg.autoCapture !== false,
|
|
81
|
-
autoRecall: cfg.autoRecall !== false,
|
|
82
|
-
autoUpload: cfg.autoUpload !== false,
|
|
83
|
-
searchThreshold:
|
|
84
|
-
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.3,
|
|
85
|
-
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
|
86
|
-
rerank: cfg.rerank !== false,
|
|
87
|
-
webSearchIncludeDomains: parseOptionalStringArray(
|
|
88
|
-
cfg.webSearchIncludeDomains,
|
|
89
|
-
"webSearchIncludeDomains",
|
|
90
|
-
),
|
|
91
|
-
webSearchExcludeDomains: parseOptionalStringArray(
|
|
92
|
-
cfg.webSearchExcludeDomains,
|
|
93
|
-
"webSearchExcludeDomains",
|
|
94
|
-
),
|
|
95
|
-
webSearchCountry: parseOptionalString(
|
|
96
|
-
cfg.webSearchCountry,
|
|
97
|
-
"webSearchCountry",
|
|
98
|
-
),
|
|
99
|
-
webSearchTimezone: parseOptionalString(
|
|
100
|
-
cfg.webSearchTimezone,
|
|
101
|
-
"webSearchTimezone",
|
|
102
|
-
),
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
};
|
package/lib/core-bridge.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
export type CoreConfig = {
|
|
6
|
-
session?: {
|
|
7
|
-
store?: string;
|
|
8
|
-
};
|
|
9
|
-
[key: string]: unknown;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type CoreAgentDeps = {
|
|
13
|
-
resolveAgentDir: (cfg: CoreConfig, agentId: string) => string;
|
|
14
|
-
resolveAgentWorkspaceDir: (cfg: CoreConfig, agentId: string) => string;
|
|
15
|
-
resolveAgentIdentity: (
|
|
16
|
-
cfg: CoreConfig,
|
|
17
|
-
agentId: string,
|
|
18
|
-
) => { name?: string | null } | null | undefined;
|
|
19
|
-
resolveThinkingDefault: (params: {
|
|
20
|
-
cfg: CoreConfig;
|
|
21
|
-
provider?: string;
|
|
22
|
-
model?: string;
|
|
23
|
-
}) => string;
|
|
24
|
-
runEmbeddedPiAgent: (params: {
|
|
25
|
-
sessionId: string;
|
|
26
|
-
sessionKey?: string;
|
|
27
|
-
messageProvider?: string;
|
|
28
|
-
sessionFile: string;
|
|
29
|
-
workspaceDir: string;
|
|
30
|
-
config?: CoreConfig;
|
|
31
|
-
prompt: string;
|
|
32
|
-
provider?: string;
|
|
33
|
-
model?: string;
|
|
34
|
-
thinkLevel?: string;
|
|
35
|
-
verboseLevel?: string;
|
|
36
|
-
timeoutMs: number;
|
|
37
|
-
runId: string;
|
|
38
|
-
lane?: string;
|
|
39
|
-
extraSystemPrompt?: string;
|
|
40
|
-
agentDir?: string;
|
|
41
|
-
}) => Promise<{
|
|
42
|
-
payloads?: Array<{ text?: string; isError?: boolean }>;
|
|
43
|
-
meta?: { aborted?: boolean };
|
|
44
|
-
}>;
|
|
45
|
-
resolveAgentTimeoutMs: (opts: { cfg: CoreConfig }) => number;
|
|
46
|
-
ensureAgentWorkspace: (params?: { dir: string }) => Promise<void>;
|
|
47
|
-
resolveStorePath: (store?: string, opts?: { agentId?: string }) => string;
|
|
48
|
-
loadSessionStore: (storePath: string) => Record<string, unknown>;
|
|
49
|
-
saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
|
|
50
|
-
resolveSessionFilePath: (
|
|
51
|
-
sessionId: string,
|
|
52
|
-
entry: unknown,
|
|
53
|
-
opts?: { agentId?: string },
|
|
54
|
-
) => string;
|
|
55
|
-
DEFAULT_MODEL: string;
|
|
56
|
-
DEFAULT_PROVIDER: string;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
let coreRootCache: string | null = null;
|
|
60
|
-
let coreDepsPromise: Promise<CoreAgentDeps> | null = null;
|
|
61
|
-
|
|
62
|
-
function findPackageRoot(startDir: string, name: string): string | null {
|
|
63
|
-
let dir = startDir;
|
|
64
|
-
for (;;) {
|
|
65
|
-
const pkgPath = path.join(dir, "package.json");
|
|
66
|
-
try {
|
|
67
|
-
if (fs.existsSync(pkgPath)) {
|
|
68
|
-
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
69
|
-
const pkg = JSON.parse(raw) as { name?: string };
|
|
70
|
-
if (pkg.name === name) {
|
|
71
|
-
return dir;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
// ignore parse errors and keep walking
|
|
76
|
-
}
|
|
77
|
-
const parent = path.dirname(dir);
|
|
78
|
-
if (parent === dir) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
dir = parent;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function resolveOpenClawRoot(): string {
|
|
86
|
-
if (coreRootCache) {
|
|
87
|
-
return coreRootCache;
|
|
88
|
-
}
|
|
89
|
-
const override = process.env.OPENCLAW_ROOT?.trim();
|
|
90
|
-
if (override) {
|
|
91
|
-
coreRootCache = override;
|
|
92
|
-
return override;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const candidates = new Set<string>();
|
|
96
|
-
if (process.argv[1]) {
|
|
97
|
-
candidates.add(path.dirname(process.argv[1]));
|
|
98
|
-
}
|
|
99
|
-
candidates.add(process.cwd());
|
|
100
|
-
try {
|
|
101
|
-
const urlPath = fileURLToPath(import.meta.url);
|
|
102
|
-
candidates.add(path.dirname(urlPath));
|
|
103
|
-
} catch {
|
|
104
|
-
// ignore
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const start of candidates) {
|
|
108
|
-
for (const name of ["openclaw"]) {
|
|
109
|
-
const found = findPackageRoot(start, name);
|
|
110
|
-
if (found) {
|
|
111
|
-
coreRootCache = found;
|
|
112
|
-
return found;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
throw new Error("Unable to resolve core root. Set OPENCLAW_ROOT to the package root.");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function importCoreExtensionAPI(): Promise<{
|
|
121
|
-
resolveAgentDir: CoreAgentDeps["resolveAgentDir"];
|
|
122
|
-
resolveAgentWorkspaceDir: CoreAgentDeps["resolveAgentWorkspaceDir"];
|
|
123
|
-
DEFAULT_MODEL: string;
|
|
124
|
-
DEFAULT_PROVIDER: string;
|
|
125
|
-
resolveAgentIdentity: CoreAgentDeps["resolveAgentIdentity"];
|
|
126
|
-
resolveThinkingDefault: CoreAgentDeps["resolveThinkingDefault"];
|
|
127
|
-
runEmbeddedPiAgent: CoreAgentDeps["runEmbeddedPiAgent"];
|
|
128
|
-
resolveAgentTimeoutMs: CoreAgentDeps["resolveAgentTimeoutMs"];
|
|
129
|
-
ensureAgentWorkspace: CoreAgentDeps["ensureAgentWorkspace"];
|
|
130
|
-
resolveStorePath: CoreAgentDeps["resolveStorePath"];
|
|
131
|
-
loadSessionStore: CoreAgentDeps["loadSessionStore"];
|
|
132
|
-
saveSessionStore: CoreAgentDeps["saveSessionStore"];
|
|
133
|
-
resolveSessionFilePath: CoreAgentDeps["resolveSessionFilePath"];
|
|
134
|
-
}> {
|
|
135
|
-
// Do not import any other module. You can't touch this or you will be fired.
|
|
136
|
-
const distPath = path.join(resolveOpenClawRoot(), "dist", "extensionAPI.js");
|
|
137
|
-
if (!fs.existsSync(distPath)) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Missing core module at ${distPath}. Run \`pnpm build\` or install the official package.`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
return await import(pathToFileURL(distPath).href);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function loadCoreAgentDeps(): Promise<CoreAgentDeps> {
|
|
146
|
-
if (coreDepsPromise) {
|
|
147
|
-
return coreDepsPromise;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
coreDepsPromise = (async () => {
|
|
151
|
-
return await importCoreExtensionAPI();
|
|
152
|
-
})();
|
|
153
|
-
|
|
154
|
-
return coreDepsPromise;
|
|
155
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse the filename from a Content-Disposition header.
|
|
3
|
-
* Handles both `filename="..."` and RFC 5987 `filename*=UTF-8''...` forms.
|
|
4
|
-
* Returns null if not found or unparseable.
|
|
5
|
-
*/
|
|
6
|
-
export function parseContentDispositionFilename(header?: string): string | null {
|
|
7
|
-
if (!header) return null;
|
|
8
|
-
// RFC 5987: filename*=UTF-8''encoded%20name.pdf (takes priority)
|
|
9
|
-
const star = header.match(/filename\*\s*=\s*(?:UTF-8|utf-8)?''(.+?)(?:;|$)/i);
|
|
10
|
-
if (star?.[1]) {
|
|
11
|
-
try {
|
|
12
|
-
return decodeURIComponent(star[1].trim());
|
|
13
|
-
} catch (err) {
|
|
14
|
-
console.warn(`memorylake-openclaw: failed to decode Content-Disposition filename* value "${star[1]}": ${String(err)}`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
// Standard: filename="name.pdf" or filename=name.pdf
|
|
18
|
-
const plain = header.match(/filename\s*=\s*"?([^";]+)"?/i);
|
|
19
|
-
if (plain?.[1]) return plain[1].trim();
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import fsPromises from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import type { PluginContext } from "../plugin-context";
|
|
5
|
-
import { loadCoreAgentDeps } from "../core-bridge";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Summarize recent session messages into a compact text block for the rewrite prompt.
|
|
9
|
-
* Messages are unknown[] from the hook event — we extract role+content from each.
|
|
10
|
-
*/
|
|
11
|
-
export function summarizeMessages(messages: unknown[], maxMessages = 10): string {
|
|
12
|
-
if (!messages || messages.length === 0) return "";
|
|
13
|
-
const recent = messages.slice(-maxMessages);
|
|
14
|
-
return recent
|
|
15
|
-
.map((m: any) => {
|
|
16
|
-
const role = m?.role ?? "user";
|
|
17
|
-
const content =
|
|
18
|
-
typeof m?.content === "string"
|
|
19
|
-
? m.content
|
|
20
|
-
: JSON.stringify(m?.content ?? "");
|
|
21
|
-
return `[${role}]: ${content}`;
|
|
22
|
-
})
|
|
23
|
-
.join("\n");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Resolve provider/model from config. Returns undefined for both if not found
|
|
28
|
-
* (openclaw will use its own defaults).
|
|
29
|
-
*/
|
|
30
|
-
export function resolveProviderModel(api: PluginContext["api"]): { provider: string | undefined; model: string | undefined } {
|
|
31
|
-
const modelPrimary = (api.config as any)?.agents?.defaults?.model?.primary as string | undefined;
|
|
32
|
-
if (modelPrimary) {
|
|
33
|
-
const slashIdx = modelPrimary.indexOf("/");
|
|
34
|
-
if (slashIdx >= 0) {
|
|
35
|
-
return { provider: modelPrimary.slice(0, slashIdx), model: modelPrimary.slice(slashIdx + 1) };
|
|
36
|
-
}
|
|
37
|
-
return { provider: undefined, model: modelPrimary };
|
|
38
|
-
}
|
|
39
|
-
return { provider: undefined, model: undefined };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Rewrite the user's prompt into a search-optimized query using
|
|
44
|
-
* openclaw's runEmbeddedPiAgent, considering conversation history.
|
|
45
|
-
*
|
|
46
|
-
* Priority: api.runtime.agent.runEmbeddedPiAgent → loadCoreAgentDeps()
|
|
47
|
-
*/
|
|
48
|
-
export async function rewriteQueryForSearch(
|
|
49
|
-
api: PluginContext["api"],
|
|
50
|
-
originalPrompt: string,
|
|
51
|
-
messages: unknown[],
|
|
52
|
-
ctx: { workspaceDir?: string },
|
|
53
|
-
): Promise<string> {
|
|
54
|
-
if (!ctx.workspaceDir) {
|
|
55
|
-
api.logger.warn("memorylake-openclaw: no workspaceDir, skipping query rewrite");
|
|
56
|
-
return originalPrompt;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const conversationHistory = summarizeMessages(messages);
|
|
60
|
-
const systemPrompt =
|
|
61
|
-
"You are a search query optimizer. Extract the key search intent and produce a concise, search-optimized query. Output ONLY the rewritten query, nothing else. Preserve important entities, names, dates, and technical terms.";
|
|
62
|
-
const userContent = conversationHistory
|
|
63
|
-
? `Conversation history:\n${conversationHistory}\n\nUser's latest message:\n${originalPrompt}`
|
|
64
|
-
: originalPrompt;
|
|
65
|
-
const fullPrompt = `${systemPrompt}\n\n${userContent}`;
|
|
66
|
-
|
|
67
|
-
const { provider, model } = resolveProviderModel(api);
|
|
68
|
-
api.logger.info(`memorylake-openclaw: rewriting query via runEmbeddedPiAgent (provider=${provider}, model=${model})`);
|
|
69
|
-
|
|
70
|
-
let tempSessionFile: string | null = null;
|
|
71
|
-
try {
|
|
72
|
-
const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "memorylake-rewrite-"));
|
|
73
|
-
tempSessionFile = path.join(tempDir, "session.jsonl");
|
|
74
|
-
|
|
75
|
-
const nowMs = Date.now();
|
|
76
|
-
const callParams = {
|
|
77
|
-
sessionId: `memorylake-rewrite-${nowMs}`,
|
|
78
|
-
sessionKey: `temp:memorylake-rewrite`,
|
|
79
|
-
sessionFile: tempSessionFile,
|
|
80
|
-
workspaceDir: ctx.workspaceDir,
|
|
81
|
-
config: api.config,
|
|
82
|
-
prompt: fullPrompt,
|
|
83
|
-
provider,
|
|
84
|
-
model,
|
|
85
|
-
disableTools: true,
|
|
86
|
-
timeoutMs: 15_000,
|
|
87
|
-
runId: `memorylake-rewrite-${nowMs}`,
|
|
88
|
-
lane: `memorylake-rewrite`,
|
|
89
|
-
trigger: "memory",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Priority 1: try api.runtime.agent.runEmbeddedPiAgent
|
|
93
|
-
let runEmbeddedPiAgent: ((p: typeof callParams) => Promise<any>) | undefined =
|
|
94
|
-
(api.runtime as any)?.agent?.runEmbeddedPiAgent;
|
|
95
|
-
|
|
96
|
-
if (typeof runEmbeddedPiAgent !== "function") {
|
|
97
|
-
api.logger.info("memorylake-openclaw: api.runtime.agent.runEmbeddedPiAgent not available, using loadCoreAgentDeps fallback");
|
|
98
|
-
const deps = await loadCoreAgentDeps();
|
|
99
|
-
runEmbeddedPiAgent = deps.runEmbeddedPiAgent;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const result = await runEmbeddedPiAgent(callParams);
|
|
103
|
-
|
|
104
|
-
const rewritten = result?.payloads?.[0]?.text?.trim();
|
|
105
|
-
if (rewritten && rewritten.length > 0) {
|
|
106
|
-
api.logger.info(`memorylake-openclaw: rewritten query: "${rewritten}"`);
|
|
107
|
-
return rewritten;
|
|
108
|
-
}
|
|
109
|
-
api.logger.warn("memorylake-openclaw: rewrite returned empty, using original");
|
|
110
|
-
} catch (err) {
|
|
111
|
-
api.logger.warn(`memorylake-openclaw: query rewrite failed, using original: ${String(err)}`);
|
|
112
|
-
} finally {
|
|
113
|
-
if (tempSessionFile) {
|
|
114
|
-
try {
|
|
115
|
-
await fsPromises.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
|
|
116
|
-
} catch (cleanupErr) {
|
|
117
|
-
api.logger.warn(`memorylake-openclaw: temp session cleanup failed: ${String(cleanupErr)}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return originalPrompt;
|
|
122
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const UPLOADED_RECORD_FILE = "uploaded.json";
|
|
5
|
-
|
|
6
|
-
export type UploadedRecord = Record<string, { mtimeMs: number }>;
|
|
7
|
-
|
|
8
|
-
export function getUploadedRecord(workspaceDir: string): UploadedRecord {
|
|
9
|
-
const filePath = path.join(workspaceDir, ".memorylake", UPLOADED_RECORD_FILE);
|
|
10
|
-
try {
|
|
11
|
-
if (fs.existsSync(filePath)) {
|
|
12
|
-
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
13
|
-
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
14
|
-
}
|
|
15
|
-
} catch {
|
|
16
|
-
// ignore read errors
|
|
17
|
-
}
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function saveUploadedRecord(workspaceDir: string, record: UploadedRecord): void {
|
|
22
|
-
const dirPath = path.join(workspaceDir, ".memorylake");
|
|
23
|
-
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
24
|
-
fs.writeFileSync(
|
|
25
|
-
path.join(dirPath, UPLOADED_RECORD_FILE),
|
|
26
|
-
JSON.stringify(record, null, 2),
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function needsUpload(record: UploadedRecord, filePath: string): fs.Stats | null {
|
|
31
|
-
if (!fs.existsSync(filePath)) return null;
|
|
32
|
-
const stat = fs.statSync(filePath);
|
|
33
|
-
const prev = record[filePath];
|
|
34
|
-
return (!prev || prev.mtimeMs !== stat.mtimeMs) ? stat : null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function extractInboundPaths(prompt: string): string[] {
|
|
38
|
-
// Path must contain /media/inbound/ (or \media\inbound\)
|
|
39
|
-
// Filename must end with .<ext>, ext = alphanumeric, 1-6 chars
|
|
40
|
-
const sep = '[/\\\\]';
|
|
41
|
-
const regex = new RegExp(
|
|
42
|
-
`(?:[A-Za-z]:${sep}|/)\\S*?media${sep}inbound${sep}.+?\\.[a-zA-Z0-9]{1,6}(?=[^a-zA-Z0-9]|$)`,
|
|
43
|
-
"g",
|
|
44
|
-
);
|
|
45
|
-
const matches = prompt.match(regex) || [];
|
|
46
|
-
return [...new Set(matches)];
|
|
47
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { PluginContext } from "../plugin-context";
|
|
2
|
-
import { getProvider } from "../provider";
|
|
3
|
-
import { buildAddOptions } from "../utils/builders";
|
|
4
|
-
import { MEMORYLAKE_REMINDER } from "./auto-recall";
|
|
5
|
-
|
|
6
|
-
function stripReminder(content: string): string {
|
|
7
|
-
if (content.includes(MEMORYLAKE_REMINDER)) {
|
|
8
|
-
return content.replace(MEMORYLAKE_REMINDER, "").trim();
|
|
9
|
-
}
|
|
10
|
-
return content;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function registerAutoCapture(pctx: PluginContext): void {
|
|
14
|
-
const { api, resolveConfig } = pctx;
|
|
15
|
-
|
|
16
|
-
api.on("agent_end", async (event, ctx) => {
|
|
17
|
-
if ((ctx as any)?.trigger !== "user") {
|
|
18
|
-
api.logger.info(`memorylake-openclaw: auto-capture skipped, trigger=${(ctx as any)?.trigger ?? "undefined"}`);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Resolve per-workspace config override
|
|
26
|
-
const effectiveCfg = resolveConfig(ctx);
|
|
27
|
-
const effectiveProvider = getProvider(effectiveCfg);
|
|
28
|
-
|
|
29
|
-
// Track session ID
|
|
30
|
-
const sessionId = (ctx as any)?.sessionId ?? undefined;
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Extract messages, limiting to last 10
|
|
34
|
-
const recentMessages = event.messages.slice(-10);
|
|
35
|
-
const formattedMessages: Array<{
|
|
36
|
-
role: string;
|
|
37
|
-
content: string;
|
|
38
|
-
}> = [];
|
|
39
|
-
|
|
40
|
-
for (const msg of recentMessages) {
|
|
41
|
-
if (!msg || typeof msg !== "object") continue;
|
|
42
|
-
const msgObj = msg as Record<string, unknown>;
|
|
43
|
-
|
|
44
|
-
const role = msgObj.role;
|
|
45
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
46
|
-
|
|
47
|
-
let textContent = "";
|
|
48
|
-
const content = msgObj.content;
|
|
49
|
-
|
|
50
|
-
if (typeof content === "string") {
|
|
51
|
-
textContent = content;
|
|
52
|
-
} else if (Array.isArray(content)) {
|
|
53
|
-
for (const block of content) {
|
|
54
|
-
if (
|
|
55
|
-
block &&
|
|
56
|
-
typeof block === "object" &&
|
|
57
|
-
"text" in block &&
|
|
58
|
-
typeof (block as Record<string, unknown>).text === "string"
|
|
59
|
-
) {
|
|
60
|
-
textContent +=
|
|
61
|
-
(textContent ? "\n" : "") +
|
|
62
|
-
((block as Record<string, unknown>).text as string);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (role === "user") {
|
|
68
|
-
textContent = stripReminder(textContent);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!textContent) continue;
|
|
72
|
-
|
|
73
|
-
formattedMessages.push({
|
|
74
|
-
role: role as string,
|
|
75
|
-
content: textContent,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (formattedMessages.length === 0) return;
|
|
80
|
-
|
|
81
|
-
const addOpts = buildAddOptions(effectiveCfg, undefined, sessionId);
|
|
82
|
-
const result = await effectiveProvider.add(
|
|
83
|
-
formattedMessages,
|
|
84
|
-
addOpts,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const capturedCount = result.results?.length ?? 0;
|
|
88
|
-
if (capturedCount > 0) {
|
|
89
|
-
api.logger.info(
|
|
90
|
-
`memorylake-openclaw: auto-captured ${capturedCount} memories`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
} catch (err) {
|
|
94
|
-
api.logger.warn(`memorylake-openclaw: capture failed: ${String(err)}`);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|