@memtensor/memos-local-openclaw-plugin 1.0.4-beta.6 → 1.0.4-beta.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.
Files changed (153) hide show
  1. package/README.md +39 -22
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +27 -7
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +29 -0
  7. package/dist/client/connector.d.ts.map +1 -0
  8. package/dist/client/connector.js +218 -0
  9. package/dist/client/connector.js.map +1 -0
  10. package/dist/client/hub.d.ts +61 -0
  11. package/dist/client/hub.d.ts.map +1 -0
  12. package/dist/client/hub.js +170 -0
  13. package/dist/client/hub.js.map +1 -0
  14. package/dist/client/skill-sync.d.ts +36 -0
  15. package/dist/client/skill-sync.d.ts.map +1 -0
  16. package/dist/client/skill-sync.js +226 -0
  17. package/dist/client/skill-sync.js.map +1 -0
  18. package/dist/config.d.ts +2 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +72 -3
  21. package/dist/config.js.map +1 -1
  22. package/dist/embedding/index.d.ts +4 -2
  23. package/dist/embedding/index.d.ts.map +1 -1
  24. package/dist/embedding/index.js +17 -1
  25. package/dist/embedding/index.js.map +1 -1
  26. package/dist/hub/auth.d.ts +19 -0
  27. package/dist/hub/auth.d.ts.map +1 -0
  28. package/dist/hub/auth.js +70 -0
  29. package/dist/hub/auth.js.map +1 -0
  30. package/dist/hub/server.d.ts +41 -0
  31. package/dist/hub/server.d.ts.map +1 -0
  32. package/dist/hub/server.js +767 -0
  33. package/dist/hub/server.js.map +1 -0
  34. package/dist/hub/user-manager.d.ts +31 -0
  35. package/dist/hub/user-manager.d.ts.map +1 -0
  36. package/dist/hub/user-manager.js +129 -0
  37. package/dist/hub/user-manager.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +8 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/ingest/providers/index.d.ts +10 -2
  43. package/dist/ingest/providers/index.d.ts.map +1 -1
  44. package/dist/ingest/providers/index.js +209 -43
  45. package/dist/ingest/providers/index.js.map +1 -1
  46. package/dist/ingest/providers/openai.d.ts +1 -0
  47. package/dist/ingest/providers/openai.d.ts.map +1 -1
  48. package/dist/ingest/providers/openai.js +1 -0
  49. package/dist/ingest/providers/openai.js.map +1 -1
  50. package/dist/ingest/task-processor.js +1 -1
  51. package/dist/ingest/task-processor.js.map +1 -1
  52. package/dist/openclaw-api.d.ts +53 -0
  53. package/dist/openclaw-api.d.ts.map +1 -0
  54. package/dist/openclaw-api.js +189 -0
  55. package/dist/openclaw-api.js.map +1 -0
  56. package/dist/recall/engine.js +1 -1
  57. package/dist/recall/engine.js.map +1 -1
  58. package/dist/shared/llm-call.d.ts +4 -2
  59. package/dist/shared/llm-call.d.ts.map +1 -1
  60. package/dist/shared/llm-call.js +20 -81
  61. package/dist/shared/llm-call.js.map +1 -1
  62. package/dist/sharing/types.contract.d.ts +2 -0
  63. package/dist/sharing/types.contract.d.ts.map +1 -0
  64. package/dist/sharing/types.contract.js +3 -0
  65. package/dist/sharing/types.contract.js.map +1 -0
  66. package/dist/sharing/types.d.ts +80 -0
  67. package/dist/sharing/types.d.ts.map +1 -0
  68. package/dist/sharing/types.js +3 -0
  69. package/dist/sharing/types.js.map +1 -0
  70. package/dist/skill/evaluator.d.ts.map +1 -1
  71. package/dist/skill/evaluator.js +2 -2
  72. package/dist/skill/evaluator.js.map +1 -1
  73. package/dist/skill/evolver.d.ts +0 -2
  74. package/dist/skill/evolver.d.ts.map +1 -1
  75. package/dist/skill/evolver.js +0 -3
  76. package/dist/skill/evolver.js.map +1 -1
  77. package/dist/skill/generator.d.ts.map +1 -1
  78. package/dist/skill/generator.js +4 -4
  79. package/dist/skill/generator.js.map +1 -1
  80. package/dist/skill/upgrader.js +1 -1
  81. package/dist/skill/upgrader.js.map +1 -1
  82. package/dist/skill/validator.js +1 -1
  83. package/dist/skill/validator.js.map +1 -1
  84. package/dist/storage/ensure-binding.d.ts.map +1 -1
  85. package/dist/storage/ensure-binding.js +3 -1
  86. package/dist/storage/ensure-binding.js.map +1 -1
  87. package/dist/storage/sqlite.d.ts +329 -1
  88. package/dist/storage/sqlite.d.ts.map +1 -1
  89. package/dist/storage/sqlite.js +909 -4
  90. package/dist/storage/sqlite.js.map +1 -1
  91. package/dist/telemetry.d.ts +5 -12
  92. package/dist/telemetry.d.ts.map +1 -1
  93. package/dist/telemetry.js +38 -135
  94. package/dist/telemetry.js.map +1 -1
  95. package/dist/tools/index.d.ts +1 -0
  96. package/dist/tools/index.d.ts.map +1 -1
  97. package/dist/tools/index.js +3 -1
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/memory-search.d.ts +5 -2
  100. package/dist/tools/memory-search.d.ts.map +1 -1
  101. package/dist/tools/memory-search.js +50 -7
  102. package/dist/tools/memory-search.js.map +1 -1
  103. package/dist/tools/network-memory-detail.d.ts +4 -0
  104. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  105. package/dist/tools/network-memory-detail.js +34 -0
  106. package/dist/tools/network-memory-detail.js.map +1 -0
  107. package/dist/types.d.ts +49 -2
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/types.js.map +1 -1
  110. package/dist/viewer/html.d.ts.map +1 -1
  111. package/dist/viewer/html.js +3965 -459
  112. package/dist/viewer/html.js.map +1 -1
  113. package/dist/viewer/server.d.ts +51 -0
  114. package/dist/viewer/server.d.ts.map +1 -1
  115. package/dist/viewer/server.js +1564 -23
  116. package/dist/viewer/server.js.map +1 -1
  117. package/index.ts +769 -67
  118. package/openclaw.plugin.json +2 -1
  119. package/package.json +4 -3
  120. package/scripts/postinstall.cjs +283 -46
  121. package/skill/memos-memory-guide/SKILL.md +82 -20
  122. package/src/capture/index.ts +27 -7
  123. package/src/client/connector.ts +212 -0
  124. package/src/client/hub.ts +207 -0
  125. package/src/client/skill-sync.ts +216 -0
  126. package/src/config.ts +94 -3
  127. package/src/embedding/index.ts +21 -1
  128. package/src/hub/auth.ts +78 -0
  129. package/src/hub/server.ts +754 -0
  130. package/src/hub/user-manager.ts +143 -0
  131. package/src/index.ts +13 -5
  132. package/src/ingest/providers/index.ts +246 -46
  133. package/src/ingest/providers/openai.ts +1 -1
  134. package/src/ingest/task-processor.ts +1 -1
  135. package/src/openclaw-api.ts +287 -0
  136. package/src/recall/engine.ts +1 -1
  137. package/src/shared/llm-call.ts +23 -95
  138. package/src/sharing/types.contract.ts +40 -0
  139. package/src/sharing/types.ts +102 -0
  140. package/src/skill/evaluator.ts +3 -2
  141. package/src/skill/evolver.ts +0 -5
  142. package/src/skill/generator.ts +6 -4
  143. package/src/skill/upgrader.ts +1 -1
  144. package/src/skill/validator.ts +1 -1
  145. package/src/storage/ensure-binding.ts +3 -1
  146. package/src/storage/sqlite.ts +1159 -4
  147. package/src/telemetry.ts +39 -152
  148. package/src/tools/index.ts +1 -0
  149. package/src/tools/memory-search.ts +58 -8
  150. package/src/tools/network-memory-detail.ts +34 -0
  151. package/src/types.ts +44 -2
  152. package/src/viewer/html.ts +3965 -459
  153. package/src/viewer/server.ts +1452 -25
@@ -0,0 +1,143 @@
1
+ import { randomUUID, createHash } from "crypto";
2
+ import { issueUserToken, verifyUserToken } from "./auth";
3
+ import type { Logger } from "../types";
4
+ import type { UserInfo } from "../sharing/types";
5
+ import type { SqliteStore } from "../storage/sqlite";
6
+
7
+ type ManagedHubUser = UserInfo & { tokenHash: string; createdAt: number; approvedAt: number | null; lastIp: string; lastActiveAt: number | null };
8
+
9
+ export class HubUserManager {
10
+ constructor(private store: SqliteStore, private log: Logger) {}
11
+
12
+ createPendingUser(input: { username: string; deviceName?: string }): ManagedHubUser {
13
+ const user = {
14
+ id: randomUUID(),
15
+ username: input.username,
16
+ deviceName: input.deviceName,
17
+ role: "member" as const,
18
+ status: "pending" as const,
19
+ groups: [],
20
+ tokenHash: "",
21
+ createdAt: Date.now(),
22
+ approvedAt: null,
23
+ lastIp: "",
24
+ lastActiveAt: null,
25
+ };
26
+ this.store.upsertHubUser(user);
27
+ return user;
28
+ }
29
+
30
+ listPendingUsers(): ManagedHubUser[] {
31
+ return this.store.listHubUsers("pending");
32
+ }
33
+
34
+ approveUser(userId: string, token: string): ManagedHubUser | null {
35
+ const user = this.store.getHubUser(userId);
36
+ if (!user) return null;
37
+ const updated = {
38
+ ...user,
39
+ status: "active" as const,
40
+ tokenHash: createHash("sha256").update(token).digest("hex"),
41
+ approvedAt: Date.now(),
42
+ };
43
+ this.store.upsertHubUser(updated);
44
+ return updated;
45
+ }
46
+
47
+ ensureBootstrapAdmin(secret: string, username = "admin", bootstrapUserId?: string, bootstrapToken?: string): { user: ManagedHubUser; token: string } {
48
+ if (bootstrapUserId) {
49
+ const bootstrapUser = this.store.getHubUser(bootstrapUserId);
50
+ if (bootstrapUser && bootstrapUser.role === "admin" && bootstrapUser.status === "active") {
51
+ if (bootstrapToken && bootstrapUser.tokenHash === createHash("sha256").update(bootstrapToken).digest("hex") && verifyUserToken(bootstrapToken, secret)) {
52
+ return { user: bootstrapUser, token: bootstrapToken };
53
+ }
54
+ const refreshedToken = issueUserToken(
55
+ { userId: bootstrapUser.id, username: bootstrapUser.username, role: bootstrapUser.role, status: bootstrapUser.status },
56
+ secret,
57
+ 3650 * 24 * 60 * 60 * 1000,
58
+ );
59
+ const refreshedUser: ManagedHubUser = {
60
+ ...bootstrapUser,
61
+ tokenHash: createHash("sha256").update(refreshedToken).digest("hex"),
62
+ };
63
+ this.store.upsertHubUser(refreshedUser);
64
+ return { user: refreshedUser, token: refreshedToken };
65
+ }
66
+ }
67
+
68
+ const existing = this.store.listHubUsers().find((user) => user.role === "admin" && user.status === "active");
69
+ if (existing) {
70
+ const refreshedToken = issueUserToken(
71
+ { userId: existing.id, username: existing.username, role: existing.role, status: existing.status },
72
+ secret,
73
+ 3650 * 24 * 60 * 60 * 1000,
74
+ );
75
+ const refreshedUser: ManagedHubUser = {
76
+ ...existing,
77
+ tokenHash: createHash("sha256").update(refreshedToken).digest("hex"),
78
+ };
79
+ this.store.upsertHubUser(refreshedUser);
80
+ return { user: refreshedUser, token: refreshedToken };
81
+ }
82
+
83
+ const user: ManagedHubUser = {
84
+ id: randomUUID(),
85
+ username,
86
+ deviceName: "hub",
87
+ role: "admin",
88
+ status: "active",
89
+ groups: [],
90
+ tokenHash: "",
91
+ createdAt: Date.now(),
92
+ approvedAt: Date.now(),
93
+ lastIp: "",
94
+ lastActiveAt: null,
95
+ };
96
+ const token = issueUserToken(
97
+ { userId: user.id, username: user.username, role: user.role, status: user.status },
98
+ secret,
99
+ 3650 * 24 * 60 * 60 * 1000,
100
+ );
101
+ user.tokenHash = createHash("sha256").update(token).digest("hex");
102
+ this.store.upsertHubUser(user);
103
+ return { user, token };
104
+ }
105
+
106
+ isUsernameTaken(username: string, excludeUserId?: string): boolean {
107
+ const users = this.store.listHubUsers();
108
+ return users.some(u => u.username === username && u.id !== excludeUserId);
109
+ }
110
+
111
+ updateUsername(userId: string, newUsername: string): ManagedHubUser | null {
112
+ const user = this.store.getHubUser(userId);
113
+ if (!user) return null;
114
+ const updated = { ...user, username: newUsername };
115
+ this.store.upsertHubUser(updated);
116
+ return updated;
117
+ }
118
+
119
+ rejectUser(userId: string): ManagedHubUser | null {
120
+ const user = this.store.getHubUser(userId);
121
+ if (!user) return null;
122
+ const updated = {
123
+ ...user,
124
+ status: "rejected" as const,
125
+ approvedAt: Date.now(),
126
+ };
127
+ this.store.upsertHubUser(updated);
128
+ return updated;
129
+ }
130
+
131
+ resetToPending(userId: string): ManagedHubUser | null {
132
+ const user = this.store.getHubUser(userId);
133
+ if (!user) return null;
134
+ const updated = {
135
+ ...user,
136
+ status: "pending" as const,
137
+ tokenHash: "",
138
+ approvedAt: null,
139
+ };
140
+ this.store.upsertHubUser(updated);
141
+ return updated;
142
+ }
143
+ }
package/src/index.ts CHANGED
@@ -6,8 +6,9 @@ import { Embedder } from "./embedding";
6
6
  import { IngestWorker } from "./ingest/worker";
7
7
  import { RecallEngine } from "./recall/engine";
8
8
  import { captureMessages } from "./capture";
9
- import { createMemorySearchTool, createMemoryTimelineTool, createMemoryGetTool } from "./tools";
9
+ import { createMemorySearchTool, createMemoryTimelineTool, createMemoryGetTool, createNetworkMemoryDetailTool } from "./tools";
10
10
  import type { MemosLocalConfig, ToolDefinition, Logger } from "./types";
11
+ import type { HostModelsConfig } from "./openclaw-api";
11
12
 
12
13
  export interface MemosLocalPlugin {
13
14
  id: string;
@@ -23,6 +24,7 @@ export interface PluginInitOptions {
23
24
  workspaceDir?: string;
24
25
  config?: Partial<MemosLocalConfig>;
25
26
  log?: Logger;
27
+ hostModels?: HostModelsConfig;
26
28
  }
27
29
 
28
30
  /**
@@ -51,21 +53,24 @@ export interface PluginInitOptions {
51
53
  export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
52
54
  const stateDir = opts.stateDir ?? defaultStateDir();
53
55
  const workspaceDir = opts.workspaceDir ?? process.cwd();
54
- const ctx = buildContext(stateDir, workspaceDir, opts.config, opts.log);
56
+ const ctx = buildContext(stateDir, workspaceDir, opts.config, opts.log, opts.hostModels);
55
57
 
56
58
  ctx.log.info("Initializing memos-local plugin...");
57
59
 
58
60
  ensureSqliteBinding(ctx.log);
59
61
 
60
62
  const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
61
- const embedder = new Embedder(ctx.config.embedding, ctx.log);
63
+ const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
62
64
  const worker = new IngestWorker(store, embedder, ctx);
63
65
  const engine = new RecallEngine(store, embedder, ctx);
64
66
 
67
+ const sharedState = { lastSearchTime: 0 };
68
+
65
69
  const tools: ToolDefinition[] = [
66
- createMemorySearchTool(engine),
70
+ createMemorySearchTool(engine, store, ctx, sharedState),
67
71
  createMemoryTimelineTool(store),
68
72
  createMemoryGetTool(store),
73
+ createNetworkMemoryDetailTool(store, ctx),
69
74
  ];
70
75
 
71
76
  ctx.log.info(`Plugin ready. DB: ${ctx.config.storage!.dbPath}, Embedding: ${embedder.provider}`);
@@ -84,7 +89,10 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
84
89
  const turnId = uuid();
85
90
  const tag = ctx.config.capture?.evidenceWrapperTag ?? "STORED_MEMORY";
86
91
 
87
- const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner);
92
+ const userSearchTime = sharedState.lastSearchTime || 0;
93
+ sharedState.lastSearchTime = 0;
94
+
95
+ const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner, userSearchTime);
88
96
  if (captured.length > 0) {
89
97
  worker.enqueue(captured);
90
98
  }
@@ -1,47 +1,13 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import type { SummarizerConfig, SummaryProvider, Logger } from "../../types";
4
- import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
3
+ import type { SummarizerConfig, Logger, OpenClawAPI } from "../../types";
4
+ import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai";
5
5
  import type { FilterResult, DedupResult } from "./openai";
6
6
  export type { FilterResult, DedupResult } from "./openai";
7
7
  import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
8
8
  import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
9
9
  import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
10
10
 
11
- /**
12
- * Detect provider type from provider key name or base URL.
13
- */
14
- function detectProvider(
15
- providerKey: string | undefined,
16
- baseUrl: string,
17
- ): SummaryProvider {
18
- const key = providerKey?.toLowerCase() ?? "";
19
- const url = baseUrl.toLowerCase();
20
- if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
21
- if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
22
- return "gemini";
23
- }
24
- if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
25
- return "openai_compatible";
26
- }
27
-
28
- /**
29
- * Return the correct endpoint for a given provider and base URL.
30
- */
31
- function normalizeEndpointForProvider(
32
- provider: SummaryProvider,
33
- baseUrl: string,
34
- ): string {
35
- const stripped = baseUrl.replace(/\/+$/, "");
36
- if (provider === "anthropic") {
37
- if (stripped.endsWith("/v1/messages")) return stripped;
38
- return `${stripped}/v1/messages`;
39
- }
40
- if (stripped.endsWith("/chat/completions")) return stripped;
41
- if (stripped.endsWith("/completions")) return stripped;
42
- return `${stripped}/chat/completions`;
43
- }
44
-
45
11
  /**
46
12
  * Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
47
13
  * This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
@@ -49,8 +15,7 @@ function normalizeEndpointForProvider(
49
15
  function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
50
16
  try {
51
17
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
52
- const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
53
- const cfgPath = path.join(ocHome, "openclaw.json");
18
+ const cfgPath = path.join(home, ".openclaw", "openclaw.json");
54
19
  if (!fs.existsSync(cfgPath)) return undefined;
55
20
 
56
21
  const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
@@ -71,12 +36,13 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
71
36
  const apiKey: string | undefined = providerCfg.apiKey;
72
37
  if (!baseUrl || !apiKey) return undefined;
73
38
 
74
- const provider = detectProvider(providerKey, baseUrl);
75
- const endpoint = normalizeEndpointForProvider(provider, baseUrl);
39
+ const endpoint = baseUrl.endsWith("/chat/completions")
40
+ ? baseUrl
41
+ : baseUrl.replace(/\/+$/, "") + "/chat/completions";
76
42
 
77
- log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
43
+ log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
78
44
  return {
79
- provider,
45
+ provider: "openai_compatible",
80
46
  endpoint,
81
47
  apiKey,
82
48
  model: modelId,
@@ -154,6 +120,7 @@ export class Summarizer {
154
120
  constructor(
155
121
  private cfg: SummarizerConfig | undefined,
156
122
  private log: Logger,
123
+ private openclawAPI?: OpenClawAPI,
157
124
  strongCfg?: SummarizerConfig,
158
125
  ) {
159
126
  this.strongCfg = strongCfg;
@@ -163,11 +130,20 @@ export class Summarizer {
163
130
  /**
164
131
  * Ordered config chain: strongCfg → cfg → fallbackCfg (OpenClaw native model).
165
132
  * Returns configs that are defined, in priority order.
133
+ * Openclaw configs without hostCompletion capability or without openclawAPI are excluded.
166
134
  */
167
135
  private getConfigChain(): SummarizerConfig[] {
168
136
  const chain: SummarizerConfig[] = [];
169
137
  if (this.strongCfg) chain.push(this.strongCfg);
170
- if (this.cfg) chain.push(this.cfg);
138
+ if (this.cfg) {
139
+ if (this.cfg.provider === "openclaw") {
140
+ if (this.cfg.capabilities?.hostCompletion === true && this.openclawAPI) {
141
+ chain.push(this.cfg);
142
+ }
143
+ } else {
144
+ chain.push(this.cfg);
145
+ }
146
+ }
171
147
  if (this.fallbackCfg) chain.push(this.fallbackCfg);
172
148
  return chain;
173
149
  }
@@ -251,7 +227,9 @@ export class Summarizer {
251
227
  return taskFallback(text);
252
228
  }
253
229
 
254
- const result = await this.tryChain("summarizeTask", (cfg) => callSummarizeTask(cfg, text, this.log));
230
+ const result = await this.tryChain("summarizeTask", (cfg) =>
231
+ cfg.provider === "openclaw" ? this.summarizeTaskOpenClaw(text) : callSummarizeTask(cfg, text, this.log),
232
+ );
255
233
  return result ?? taskFallback(text);
256
234
  }
257
235
 
@@ -290,7 +268,11 @@ export class Summarizer {
290
268
  if (!this.cfg && !this.fallbackCfg) return null;
291
269
  if (candidates.length === 0) return { relevant: [], sufficient: true };
292
270
 
293
- const result = await this.tryChain("filterRelevant", (cfg) => callFilterRelevant(cfg, query, candidates, this.log));
271
+ const result = await this.tryChain("filterRelevant", (cfg) =>
272
+ cfg.provider === "openclaw"
273
+ ? this.filterRelevantOpenClaw(query, candidates)
274
+ : callFilterRelevant(cfg, query, candidates, this.log),
275
+ );
294
276
  return result ?? null;
295
277
  }
296
278
 
@@ -301,13 +283,144 @@ export class Summarizer {
301
283
  if (!this.cfg && !this.fallbackCfg) return null;
302
284
  if (candidates.length === 0) return null;
303
285
 
304
- const result = await this.tryChain("judgeDedup", (cfg) => callJudgeDedup(cfg, newSummary, candidates, this.log));
286
+ const result = await this.tryChain("judgeDedup", (cfg) =>
287
+ cfg.provider === "openclaw"
288
+ ? this.judgeDedupOpenClaw(newSummary, candidates)
289
+ : callJudgeDedup(cfg, newSummary, candidates, this.log),
290
+ );
305
291
  return result ?? { action: "NEW", reason: "all_models_failed" };
306
292
  }
307
293
 
308
294
  getStrongConfig(): SummarizerConfig | undefined {
309
295
  return this.strongCfg;
310
296
  }
297
+
298
+ // ─── OpenClaw API Implementation ───
299
+
300
+ private requireOpenClawAPI(): void {
301
+ if (!this.openclawAPI) {
302
+ throw new Error(
303
+ "OpenClaw API not available. Ensure sharing.capabilities.hostCompletion is enabled in config."
304
+ );
305
+ }
306
+ }
307
+
308
+ private async summarizeOpenClaw(text: string): Promise<string> {
309
+ this.requireOpenClawAPI();
310
+ const prompt = [
311
+ `Summarize the text in ONE concise sentence (max 120 characters). IMPORTANT: Use the SAME language as the input text — if the input is Chinese, write Chinese; if English, write English. Preserve exact names, commands, error codes. No bullet points, no preamble — output only the sentence.`,
312
+ ``,
313
+ text.slice(0, 2000),
314
+ ].join("\n");
315
+
316
+ const response = await this.openclawAPI!.complete({
317
+ prompt,
318
+ maxTokens: 100,
319
+ temperature: 0,
320
+ model: this.cfg?.model,
321
+ });
322
+
323
+ return response.text.trim().slice(0, 200);
324
+ }
325
+
326
+ private async summarizeTaskOpenClaw(text: string): Promise<string> {
327
+ this.requireOpenClawAPI();
328
+ const prompt = [
329
+ OPENCLAW_TASK_SUMMARY_PROMPT,
330
+ ``,
331
+ text,
332
+ ].join("\n");
333
+
334
+ const response = await this.openclawAPI!.complete({
335
+ prompt,
336
+ maxTokens: 4096,
337
+ temperature: 0.1,
338
+ model: this.cfg?.model,
339
+ });
340
+
341
+ return response.text.trim();
342
+ }
343
+
344
+ private async judgeNewTopicOpenClaw(currentContext: string, newMessage: string): Promise<boolean> {
345
+ this.requireOpenClawAPI();
346
+ const prompt = [
347
+ OPENCLAW_TOPIC_JUDGE_PROMPT,
348
+ ``,
349
+ `CURRENT CONVERSATION SUMMARY:`,
350
+ currentContext,
351
+ ``,
352
+ `NEW USER MESSAGE:`,
353
+ newMessage,
354
+ ].join("\n");
355
+
356
+ const response = await this.openclawAPI!.complete({
357
+ prompt,
358
+ maxTokens: 10,
359
+ temperature: 0,
360
+ model: this.cfg?.model,
361
+ });
362
+
363
+ const answer = response.text.trim().toUpperCase();
364
+ this.log.debug(`Topic judge result: "${answer}"`);
365
+ return answer.startsWith("NEW");
366
+ }
367
+
368
+ private async filterRelevantOpenClaw(
369
+ query: string,
370
+ candidates: Array<{ index: number; role: string; content: string; time?: string }>,
371
+ ): Promise<FilterResult> {
372
+ this.requireOpenClawAPI();
373
+ const candidateText = candidates
374
+ .map((c) => `${c.index}. [${c.role}] ${c.content}`)
375
+ .join("\n");
376
+
377
+ const prompt = [
378
+ OPENCLAW_FILTER_RELEVANT_PROMPT,
379
+ ``,
380
+ `QUERY: ${query}`,
381
+ ``,
382
+ `CANDIDATES:`,
383
+ candidateText,
384
+ ].join("\n");
385
+
386
+ const response = await this.openclawAPI!.complete({
387
+ prompt,
388
+ maxTokens: 200,
389
+ temperature: 0,
390
+ model: this.cfg?.model,
391
+ });
392
+
393
+ return parseFilterResult(response.text.trim(), this.log);
394
+ }
395
+
396
+ private async judgeDedupOpenClaw(
397
+ newSummary: string,
398
+ candidates: Array<{ index: number; summary: string; chunkId: string }>,
399
+ ): Promise<DedupResult> {
400
+ this.requireOpenClawAPI();
401
+ const candidateText = candidates
402
+ .map((c) => `${c.index}. ${c.summary}`)
403
+ .join("\n");
404
+
405
+ const prompt = [
406
+ OPENCLAW_DEDUP_JUDGE_PROMPT,
407
+ ``,
408
+ `NEW MEMORY:`,
409
+ newSummary,
410
+ ``,
411
+ `EXISTING MEMORIES:`,
412
+ candidateText,
413
+ ].join("\n");
414
+
415
+ const response = await this.openclawAPI!.complete({
416
+ prompt,
417
+ maxTokens: 300,
418
+ temperature: 0,
419
+ model: this.cfg?.model,
420
+ });
421
+
422
+ return parseDedupResult(response.text.trim(), this.log);
423
+ }
311
424
  }
312
425
 
313
426
  // ─── Dispatch helpers ───
@@ -483,3 +596,90 @@ function wordCount(text: string): number {
483
596
  return count;
484
597
  }
485
598
 
599
+ // ─── OpenClaw Prompt Templates ───
600
+
601
+ const OPENCLAW_TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
602
+
603
+ CRITICAL LANGUAGE RULE: You MUST write in the SAME language as the user's messages. Chinese input → Chinese output. English input → English output. NEVER mix languages.
604
+
605
+ Output EXACTLY this structure:
606
+
607
+ 📌 Title
608
+ A short, descriptive title (10-30 characters). Like a chat group name.
609
+
610
+ 🎯 Goal
611
+ One sentence: what the user wanted to accomplish.
612
+
613
+ 📋 Key Steps
614
+ - Describe each meaningful step in detail
615
+ - Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs
616
+ - For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks
617
+ - For configs: include the actual config values and structure
618
+ - For lists/instructions: include the actual items, not just "provided a list"
619
+ - Merge only truly trivial back-and-forth (like "ok" / "sure")
620
+ - Do NOT over-summarize: "provided a function" is BAD; show the actual function
621
+
622
+ ✅ Result
623
+ What was the final outcome? Include the final version of any code/config/content produced.
624
+
625
+ 💡 Key Details
626
+ - Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned
627
+ - Specific values: numbers, versions, thresholds, URLs, file paths, model names
628
+ - Omit this section only if there truly are no noteworthy details
629
+
630
+ RULES:
631
+ - This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough.
632
+ - PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts
633
+ - DISCARD only: greetings, filler, the assistant explaining what it will do before doing it
634
+ - Replace secrets (API keys, tokens, passwords) with [REDACTED]
635
+ - Target length: 30-50% of the original conversation length. Longer conversations need longer summaries.
636
+ - Output summary only, no preamble.`;
637
+
638
+ const OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given a summary of the CURRENT conversation and a NEW user message, determine if the new message starts a DIFFERENT topic/task.
639
+
640
+ Answer ONLY "NEW" or "SAME".
641
+
642
+ Rules:
643
+ - "NEW" = the new message is about a completely different subject, project, or task
644
+ - "SAME" = the new message continues, follows up on, or is closely related to the current topic
645
+ - Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME
646
+ - Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME
647
+ - A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW
648
+
649
+ Output exactly one word: NEW or SAME`;
650
+
651
+ const OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge. Given a user's QUERY and a list of CANDIDATE memory summaries, do two things:
652
+
653
+ 1. Select ALL candidates that could be useful for answering the query. When in doubt, INCLUDE the candidate.
654
+ - For questions about lists, history, or "what/where/who" across multiple items, include ALL matching items.
655
+ - For factual lookups, a single direct answer is enough.
656
+ 2. Judge whether the selected memories are SUFFICIENT to fully answer the query WITHOUT fetching additional context.
657
+
658
+ IMPORTANT for "sufficient" judgment:
659
+ - sufficient=true ONLY when the memories contain a concrete ANSWER, fact, decision, or actionable information that directly addresses the query.
660
+ - sufficient=false when the memories only repeat the question, show related topics but lack the specific detail, or contain partial information.
661
+
662
+ Output a JSON object with exactly two fields:
663
+ {"relevant":[1,3,5],"sufficient":true}
664
+
665
+ - "relevant": array of candidate numbers that are useful. Empty array [] if none are relevant.
666
+ - "sufficient": true ONLY if the memories contain a direct answer; false otherwise.
667
+
668
+ Output ONLY the JSON object, nothing else.`;
669
+
670
+ const OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system. Given a NEW memory summary and several EXISTING memory summaries, determine the relationship.
671
+
672
+ For each EXISTING memory, the NEW memory is either:
673
+ - "DUPLICATE": NEW is fully covered by an EXISTING memory — no new information at all
674
+ - "UPDATE": NEW contains information that supplements or updates an EXISTING memory (new data, status change, additional detail)
675
+ - "NEW": NEW is a different topic/event despite surface similarity
676
+
677
+ Pick the BEST match among all candidates. If none match well, choose "NEW".
678
+
679
+ Output a single JSON object:
680
+ - If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"..."}
681
+ - If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"...","mergedSummary":"a combined summary preserving all info from both old and new, same language as input"}
682
+ - If NEW: {"action":"NEW","reason":"..."}
683
+
684
+ CRITICAL: mergedSummary must use the SAME language as the input. Output ONLY the JSON object.`;
685
+
@@ -316,7 +316,7 @@ export async function filterRelevantOpenAI(
316
316
  return parseFilterResult(raw, log);
317
317
  }
318
318
 
319
- function parseFilterResult(raw: string, log: Logger): FilterResult {
319
+ export function parseFilterResult(raw: string, log: Logger): FilterResult {
320
320
  try {
321
321
  const match = raw.match(/\{[\s\S]*\}/);
322
322
  if (match) {
@@ -39,7 +39,7 @@ export class TaskProcessor {
39
39
  private ctx: PluginContext,
40
40
  ) {
41
41
  const strongCfg = ctx.config.skillEvolution?.summarizer;
42
- this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, strongCfg);
42
+ this.summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI, strongCfg);
43
43
  }
44
44
 
45
45
  onTaskCompleted(cb: (task: Task) => void): void {