@memtensor/memos-local-openclaw-plugin 1.0.0 → 1.0.2-beta.1

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.
@@ -210,6 +210,12 @@ class ViewerServer {
210
210
  this.serveConfig(res);
211
211
  else if (p === "/api/config" && req.method === "PUT")
212
212
  this.handleSaveConfig(req, res);
213
+ else if (p === "/api/test-model" && req.method === "POST")
214
+ this.handleTestModel(req, res);
215
+ else if (p === "/api/fallback-model" && req.method === "GET")
216
+ this.serveFallbackModel(res);
217
+ else if (p === "/api/update-check" && req.method === "GET")
218
+ this.handleUpdateCheck(res);
213
219
  else if (p === "/api/auth/logout" && req.method === "POST")
214
220
  this.handleLogout(req, res);
215
221
  else if (p === "/api/migrate/scan" && req.method === "GET")
@@ -553,7 +559,8 @@ class ViewerServer {
553
559
  try {
554
560
  ftsResults = db.prepare("SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100").all(q).filter(passesFilter);
555
561
  }
556
- catch {
562
+ catch { /* FTS syntax error, fall through */ }
563
+ if (ftsResults.length === 0) {
557
564
  ftsResults = db.prepare("SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100").all(`%${q}%`, `%${q}%`).filter(passesFilter);
558
565
  }
559
566
  const SEMANTIC_THRESHOLD = 0.64;
@@ -584,16 +591,12 @@ class ViewerServer {
584
591
  }
585
592
  }
586
593
  for (const r of ftsResults) {
587
- if (seenIds.has(r.id))
588
- continue;
589
- const vscore = scoreMap.get(r.id);
590
- if (vscore !== undefined && vscore < SEMANTIC_THRESHOLD)
591
- continue;
592
- seenIds.add(r.id);
593
- merged.push(r);
594
- }
595
- const fallback = merged.length === 0 && ftsResults.length > 0;
596
- const results = fallback ? ftsResults.slice(0, 20) : merged;
594
+ if (!seenIds.has(r.id)) {
595
+ seenIds.add(r.id);
596
+ merged.push(r);
597
+ }
598
+ }
599
+ const results = merged.length > 0 ? merged : ftsResults.slice(0, 20);
597
600
  this.store.recordViewerEvent("search");
598
601
  this.jsonResponse(res, {
599
602
  results,
@@ -601,7 +604,6 @@ class ViewerServer {
601
604
  vectorCount: vectorResults.length,
602
605
  ftsCount: ftsResults.length,
603
606
  total: results.length,
604
- fallbackFts: fallback,
605
607
  });
606
608
  }
607
609
  // ─── Skills API ───
@@ -754,9 +756,10 @@ class ViewerServer {
754
756
  this.jsonResponse(res, { ok: true, skillId, visibility });
755
757
  }
756
758
  catch (err) {
757
- this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${err}`);
758
- res.writeHead(400, { "Content-Type": "application/json" });
759
- res.end(JSON.stringify({ error: String(err) }));
759
+ const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
760
+ this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
761
+ res.writeHead(500, { "Content-Type": "application/json" });
762
+ res.end(JSON.stringify({ error: errMsg }));
760
763
  }
761
764
  });
762
765
  }
@@ -962,20 +965,27 @@ class ViewerServer {
962
965
  this.jsonResponse(res, { ok: true, deleted: count });
963
966
  }
964
967
  handleDeleteAll(res) {
965
- const result = this.store.deleteAll();
966
- // Clean up skills-store directory
967
- const skillsStoreDir = node_path_1.default.join(this.dataDir, "skills-store");
968
968
  try {
969
- if (node_fs_1.default.existsSync(skillsStoreDir)) {
970
- node_fs_1.default.rmSync(skillsStoreDir, { recursive: true });
971
- node_fs_1.default.mkdirSync(skillsStoreDir, { recursive: true });
972
- this.log.info("Cleared skills-store directory");
969
+ const result = this.store.deleteAll();
970
+ const skillsStoreDir = node_path_1.default.join(this.dataDir, "skills-store");
971
+ try {
972
+ if (node_fs_1.default.existsSync(skillsStoreDir)) {
973
+ node_fs_1.default.rmSync(skillsStoreDir, { recursive: true });
974
+ node_fs_1.default.mkdirSync(skillsStoreDir, { recursive: true });
975
+ this.log.info("Cleared skills-store directory");
976
+ }
973
977
  }
978
+ catch (err) {
979
+ this.log.warn(`Failed to clear skills-store: ${err}`);
980
+ }
981
+ this.jsonResponse(res, { ok: true, deleted: result });
974
982
  }
975
983
  catch (err) {
976
- this.log.warn(`Failed to clear skills-store: ${err}`);
984
+ const msg = err instanceof Error ? err.message : String(err);
985
+ this.log.error(`handleDeleteAll error: ${msg}`);
986
+ res.writeHead(500, { "Content-Type": "application/json" });
987
+ res.end(JSON.stringify({ ok: false, error: msg }));
977
988
  }
978
- this.jsonResponse(res, { ok: true, deleted: result });
979
989
  }
980
990
  // ─── Helpers ───
981
991
  // ─── Config API ───
@@ -1058,6 +1068,208 @@ class ViewerServer {
1058
1068
  }
1059
1069
  });
1060
1070
  }
1071
+ handleTestModel(req, res) {
1072
+ this.readBody(req, async (body) => {
1073
+ try {
1074
+ const { type, provider, model, endpoint, apiKey } = JSON.parse(body);
1075
+ if (!provider) {
1076
+ this.jsonResponse(res, { ok: false, error: "provider is required" });
1077
+ return;
1078
+ }
1079
+ if (type === "embedding") {
1080
+ await this.testEmbeddingModel(provider, model, endpoint, apiKey);
1081
+ this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
1082
+ }
1083
+ else {
1084
+ await this.testChatModel(provider, model, endpoint, apiKey);
1085
+ this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
1086
+ }
1087
+ }
1088
+ catch (e) {
1089
+ const msg = e instanceof Error ? e.message : String(e);
1090
+ this.log.warn(`test-model failed: ${msg}`);
1091
+ this.jsonResponse(res, { ok: false, error: msg });
1092
+ }
1093
+ });
1094
+ }
1095
+ serveFallbackModel(res) {
1096
+ try {
1097
+ const cfgPath = this.getOpenClawConfigPath();
1098
+ if (!node_fs_1.default.existsSync(cfgPath)) {
1099
+ this.jsonResponse(res, { available: false });
1100
+ return;
1101
+ }
1102
+ const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
1103
+ const agentModel = raw?.agents?.defaults?.model?.primary;
1104
+ if (!agentModel) {
1105
+ this.jsonResponse(res, { available: false });
1106
+ return;
1107
+ }
1108
+ const [providerKey, modelId] = agentModel.includes("/")
1109
+ ? agentModel.split("/", 2)
1110
+ : [undefined, agentModel];
1111
+ const providerCfg = providerKey
1112
+ ? raw?.models?.providers?.[providerKey]
1113
+ : Object.values(raw?.models?.providers ?? {})[0];
1114
+ if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
1115
+ this.jsonResponse(res, { available: false });
1116
+ return;
1117
+ }
1118
+ this.jsonResponse(res, { available: true, model: modelId || agentModel, baseUrl: providerCfg.baseUrl });
1119
+ }
1120
+ catch {
1121
+ this.jsonResponse(res, { available: false });
1122
+ }
1123
+ }
1124
+ findPluginPackageJson() {
1125
+ let dir = __dirname;
1126
+ for (let i = 0; i < 6; i++) {
1127
+ const candidate = node_path_1.default.join(dir, "package.json");
1128
+ if (node_fs_1.default.existsSync(candidate)) {
1129
+ try {
1130
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(candidate, "utf-8"));
1131
+ if (pkg.name && pkg.name.includes("memos-local"))
1132
+ return candidate;
1133
+ }
1134
+ catch { /* skip */ }
1135
+ }
1136
+ dir = node_path_1.default.dirname(dir);
1137
+ }
1138
+ return null;
1139
+ }
1140
+ async handleUpdateCheck(res) {
1141
+ try {
1142
+ const pkgPath = this.findPluginPackageJson();
1143
+ if (!pkgPath) {
1144
+ this.jsonResponse(res, { updateAvailable: false, error: "package.json not found" });
1145
+ return;
1146
+ }
1147
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8"));
1148
+ const current = pkg.version;
1149
+ const name = pkg.name;
1150
+ if (!current || !name) {
1151
+ this.jsonResponse(res, { updateAvailable: false, current });
1152
+ return;
1153
+ }
1154
+ const npmResp = await fetch(`https://registry.npmjs.org/${name}/latest`, {
1155
+ signal: AbortSignal.timeout(6_000),
1156
+ });
1157
+ if (!npmResp.ok) {
1158
+ this.jsonResponse(res, { updateAvailable: false, current });
1159
+ return;
1160
+ }
1161
+ const data = await npmResp.json();
1162
+ const latest = data.version ?? current;
1163
+ this.jsonResponse(res, {
1164
+ updateAvailable: latest !== current,
1165
+ current,
1166
+ latest,
1167
+ packageName: name,
1168
+ });
1169
+ }
1170
+ catch (e) {
1171
+ this.log.warn(`handleUpdateCheck error: ${e}`);
1172
+ this.jsonResponse(res, { updateAvailable: false, error: String(e) });
1173
+ }
1174
+ }
1175
+ async testEmbeddingModel(provider, model, endpoint, apiKey) {
1176
+ if (provider === "local") {
1177
+ return;
1178
+ }
1179
+ const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
1180
+ const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
1181
+ const headers = {
1182
+ "Content-Type": "application/json",
1183
+ "Authorization": `Bearer ${apiKey}`,
1184
+ };
1185
+ if (provider === "cohere") {
1186
+ headers["Authorization"] = `Bearer ${apiKey}`;
1187
+ const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
1188
+ method: "POST",
1189
+ headers,
1190
+ body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
1191
+ signal: AbortSignal.timeout(15_000),
1192
+ });
1193
+ if (!resp.ok) {
1194
+ const txt = await resp.text();
1195
+ throw new Error(`Cohere embed ${resp.status}: ${txt}`);
1196
+ }
1197
+ return;
1198
+ }
1199
+ if (provider === "gemini") {
1200
+ const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
1201
+ const resp = await fetch(url, {
1202
+ method: "POST",
1203
+ headers: { "Content-Type": "application/json" },
1204
+ body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
1205
+ signal: AbortSignal.timeout(15_000),
1206
+ });
1207
+ if (!resp.ok) {
1208
+ const txt = await resp.text();
1209
+ throw new Error(`Gemini embed ${resp.status}: ${txt}`);
1210
+ }
1211
+ return;
1212
+ }
1213
+ const resp = await fetch(embUrl, {
1214
+ method: "POST",
1215
+ headers,
1216
+ body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
1217
+ signal: AbortSignal.timeout(15_000),
1218
+ });
1219
+ if (!resp.ok) {
1220
+ const txt = await resp.text();
1221
+ throw new Error(`${resp.status}: ${txt}`);
1222
+ }
1223
+ }
1224
+ async testChatModel(provider, model, endpoint, apiKey) {
1225
+ const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
1226
+ if (provider === "anthropic") {
1227
+ const url = endpoint || "https://api.anthropic.com/v1/messages";
1228
+ const resp = await fetch(url, {
1229
+ method: "POST",
1230
+ headers: {
1231
+ "Content-Type": "application/json",
1232
+ "x-api-key": apiKey,
1233
+ "anthropic-version": "2023-06-01",
1234
+ },
1235
+ body: JSON.stringify({ model: model || "claude-3-haiku-20240307", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
1236
+ signal: AbortSignal.timeout(15_000),
1237
+ });
1238
+ if (!resp.ok) {
1239
+ const txt = await resp.text();
1240
+ throw new Error(`Anthropic ${resp.status}: ${txt}`);
1241
+ }
1242
+ return;
1243
+ }
1244
+ if (provider === "gemini") {
1245
+ const url = `https://generativelanguage.googleapis.com/v1/models/${model || "gemini-1.5-flash"}:generateContent?key=${apiKey}`;
1246
+ const resp = await fetch(url, {
1247
+ method: "POST",
1248
+ headers: { "Content-Type": "application/json" },
1249
+ body: JSON.stringify({ contents: [{ parts: [{ text: "hi" }] }], generationConfig: { maxOutputTokens: 5 } }),
1250
+ signal: AbortSignal.timeout(15_000),
1251
+ });
1252
+ if (!resp.ok) {
1253
+ const txt = await resp.text();
1254
+ throw new Error(`Gemini ${resp.status}: ${txt}`);
1255
+ }
1256
+ return;
1257
+ }
1258
+ const chatUrl = baseUrl.endsWith("/chat/completions") ? baseUrl : `${baseUrl}/chat/completions`;
1259
+ const resp = await fetch(chatUrl, {
1260
+ method: "POST",
1261
+ headers: {
1262
+ "Content-Type": "application/json",
1263
+ "Authorization": `Bearer ${apiKey}`,
1264
+ },
1265
+ body: JSON.stringify({ model: model || "gpt-4o-mini", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
1266
+ signal: AbortSignal.timeout(15_000),
1267
+ });
1268
+ if (!resp.ok) {
1269
+ const txt = await resp.text();
1270
+ throw new Error(`${resp.status}: ${txt}`);
1271
+ }
1272
+ }
1061
1273
  serveLogs(res, url) {
1062
1274
  const limit = Math.min(Number(url.searchParams.get("limit") ?? 20), 200);
1063
1275
  const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0));