@deeplake/hivemind 0.7.21 → 0.7.23

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 (37) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/bundle/cli.js +66 -15
  5. package/codex/bundle/capture.js +60 -12
  6. package/codex/bundle/commands/auth-login.js +4 -2
  7. package/codex/bundle/pre-tool-use.js +4 -2
  8. package/codex/bundle/session-start-setup.js +52 -5
  9. package/codex/bundle/session-start.js +62 -11
  10. package/codex/bundle/shell/deeplake-shell.js +4 -2
  11. package/codex/bundle/skillify-worker.js +118 -29
  12. package/codex/bundle/stop.js +100 -52
  13. package/codex/bundle/wiki-worker.js +7 -3
  14. package/cursor/bundle/capture.js +87 -39
  15. package/cursor/bundle/commands/auth-login.js +4 -2
  16. package/cursor/bundle/pre-tool-use.js +4 -2
  17. package/cursor/bundle/session-end.js +78 -34
  18. package/cursor/bundle/session-start.js +67 -15
  19. package/cursor/bundle/shell/deeplake-shell.js +4 -2
  20. package/cursor/bundle/skillify-worker.js +118 -29
  21. package/cursor/bundle/wiki-worker.js +7 -3
  22. package/hermes/bundle/capture.js +87 -39
  23. package/hermes/bundle/commands/auth-login.js +4 -2
  24. package/hermes/bundle/pre-tool-use.js +4 -2
  25. package/hermes/bundle/session-end.js +78 -34
  26. package/hermes/bundle/session-start.js +67 -15
  27. package/hermes/bundle/shell/deeplake-shell.js +4 -2
  28. package/hermes/bundle/skillify-worker.js +118 -29
  29. package/hermes/bundle/wiki-worker.js +7 -3
  30. package/mcp/bundle/server.js +4 -2
  31. package/openclaw/dist/index.js +8 -6
  32. package/openclaw/dist/skillify-worker.js +118 -29
  33. package/openclaw/openclaw.plugin.json +1 -1
  34. package/openclaw/package.json +1 -1
  35. package/openclaw/skills/SKILL.md +1 -1
  36. package/package.json +1 -1
  37. package/pi/extension-source/hivemind.ts +18 -3
@@ -90,9 +90,16 @@ function renderFrontmatter(fm) {
90
90
  lines.push(`description: ${JSON.stringify(fm.description)}`);
91
91
  if (fm.trigger)
92
92
  lines.push(`trigger: ${JSON.stringify(fm.trigger)}`);
93
+ if (fm.author)
94
+ lines.push(`author: ${fm.author}`);
93
95
  lines.push(`source_sessions:`);
94
96
  for (const s of fm.source_sessions)
95
97
  lines.push(` - ${s}`);
98
+ if (fm.contributors && fm.contributors.length > 0) {
99
+ lines.push(`contributors:`);
100
+ for (const c of fm.contributors)
101
+ lines.push(` - ${c}`);
102
+ }
96
103
  lines.push(`version: ${fm.version}`);
97
104
  lines.push(`created_by_agent: ${fm.created_by_agent}`);
98
105
  lines.push(`created_at: ${fm.created_at}`);
@@ -109,18 +116,25 @@ function parseFrontmatter(text) {
109
116
  const head = text.slice(4, end).trim();
110
117
  const body = text.slice(end + 4).replace(/^\r?\n/, "");
111
118
  const fm = { source_sessions: [] };
112
- let mode = "kv";
119
+ let arrayKey = null;
113
120
  for (const raw of head.split(/\r?\n/)) {
114
- if (mode === "sources") {
121
+ if (arrayKey) {
115
122
  const m2 = raw.match(/^\s+-\s+(.+)$/);
116
123
  if (m2) {
117
- fm.source_sessions.push(m2[1].trim());
124
+ const arr = fm[arrayKey] ?? [];
125
+ arr.push(m2[1].trim());
126
+ fm[arrayKey] = arr;
118
127
  continue;
119
128
  }
120
- mode = "kv";
129
+ arrayKey = null;
121
130
  }
122
131
  if (raw.startsWith("source_sessions:")) {
123
- mode = "sources";
132
+ arrayKey = "source_sessions";
133
+ continue;
134
+ }
135
+ if (raw.startsWith("contributors:")) {
136
+ arrayKey = "contributors";
137
+ fm.contributors = [];
124
138
  continue;
125
139
  }
126
140
  const m = raw.match(/^([a-zA-Z_]+):\s*(.*)$/);
@@ -151,11 +165,15 @@ function writeNewSkill(args) {
151
165
  }
152
166
  mkdirSync(dir, { recursive: true });
153
167
  const now = (/* @__PURE__ */ new Date()).toISOString();
168
+ const author = args.author && args.author.length > 0 ? args.author : void 0;
169
+ const contributors = author ? [author] : [];
154
170
  const fm = {
155
171
  name: args.name,
156
172
  description: args.description,
157
173
  trigger: args.trigger,
174
+ author,
158
175
  source_sessions: args.sourceSessions,
176
+ contributors,
159
177
  version: 1,
160
178
  created_by_agent: args.agent,
161
179
  created_at: now,
@@ -166,7 +184,15 @@ function writeNewSkill(args) {
166
184
  ${args.body.trim()}
167
185
  `;
168
186
  writeFileSync(path, text);
169
- return { path, action: "created", version: 1, createdAt: now, updatedAt: now };
187
+ return {
188
+ path,
189
+ action: "created",
190
+ version: 1,
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ author,
194
+ contributors
195
+ };
170
196
  }
171
197
  function mergeSkill(args) {
172
198
  assertValidSkillName(args.name);
@@ -179,12 +205,20 @@ function mergeSkill(args) {
179
205
  const prevVersion = parsed?.fm.version ?? 1;
180
206
  const prevSources = parsed?.fm.source_sessions ?? [];
181
207
  const merged = Array.from(/* @__PURE__ */ new Set([...prevSources, ...args.newSourceSessions]));
208
+ const author = parsed?.fm.author;
209
+ const prevContribs = parsed?.fm.contributors && parsed.fm.contributors.length > 0 ? parsed.fm.contributors : author ? [author] : [];
210
+ const contributors = [...prevContribs];
211
+ if (args.editor && args.editor.length > 0 && !contributors.includes(args.editor)) {
212
+ contributors.push(args.editor);
213
+ }
182
214
  const now = (/* @__PURE__ */ new Date()).toISOString();
183
215
  const fm = {
184
216
  name: args.name,
185
217
  description: args.description ?? parsed?.fm.description ?? "",
186
218
  trigger: parsed?.fm.trigger,
219
+ author,
187
220
  source_sessions: merged,
221
+ contributors,
188
222
  version: prevVersion + 1,
189
223
  created_by_agent: parsed?.fm.created_by_agent ?? args.agent,
190
224
  created_at: parsed?.fm.created_at ?? now,
@@ -195,7 +229,15 @@ function mergeSkill(args) {
195
229
  ${args.body.trim()}
196
230
  `;
197
231
  writeFileSync(path, text);
198
- return { path, action: "merged", version: fm.version, createdAt: fm.created_at, updatedAt: fm.updated_at };
232
+ return {
233
+ path,
234
+ action: "merged",
235
+ version: fm.version,
236
+ createdAt: fm.created_at,
237
+ updatedAt: fm.updated_at,
238
+ author,
239
+ contributors
240
+ };
199
241
  }
200
242
  function listSkills(skillsRoot) {
201
243
  if (!existsSync(skillsRoot))
@@ -220,9 +262,14 @@ function resolveSkillsRoot(install, cwd) {
220
262
  function listAllExistingSkills(cwd) {
221
263
  const projectRoot = resolveSkillsRoot("project", cwd);
222
264
  const globalRoot = resolveSkillsRoot("global", cwd);
265
+ const tag = (source) => (s) => {
266
+ const parsed = parseFrontmatter(s.body);
267
+ const author = typeof parsed?.fm.author === "string" && parsed.fm.author.length > 0 ? parsed.fm.author : void 0;
268
+ return { name: s.name, body: s.body, source, author };
269
+ };
223
270
  const tagged = [
224
- ...listSkills(projectRoot).map((s) => ({ ...s, source: "project" })),
225
- ...listSkills(globalRoot).map((s) => ({ ...s, source: "global" }))
271
+ ...listSkills(projectRoot).map(tag("project")),
272
+ ...listSkills(globalRoot).map(tag("global"))
226
273
  ];
227
274
  const seen = /* @__PURE__ */ new Set();
228
275
  const out = [];
@@ -242,12 +289,13 @@ function renderExistingSkillsBlock(cwd, charCap) {
242
289
  block: "(no existing skills \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
243
290
  };
244
291
  }
245
- const mergeTargetNames = skills.filter((s) => s.source === "project").map((s) => s.name);
246
292
  let total = 0;
247
293
  const out = [];
294
+ const mergeTargetNames = [];
248
295
  for (const s of skills) {
249
- const tag = s.source === "project" ? "[project]" : "[global, read-only]";
250
- const block = `--- existing skill ${tag}: ${s.name} ---
296
+ const sourceTag = s.source === "project" ? "project" : "global, read-only";
297
+ const authorTag = s.author ? `, author=${s.author}` : "";
298
+ const block = `--- existing skill [${sourceTag}${authorTag}]: ${s.name} ---
251
299
  ${s.body}
252
300
  `;
253
301
  if (total + block.length > charCap) {
@@ -256,6 +304,8 @@ ${s.body}
256
304
  }
257
305
  out.push(block);
258
306
  total += block.length;
307
+ if (s.source === "project")
308
+ mergeTargetNames.push(s.name);
259
309
  }
260
310
  return { mergeTargetNames, block: out.join("\n") };
261
311
  }
@@ -274,7 +324,11 @@ function sqlIdent(name) {
274
324
  // dist/src/skillify/skills-table.js
275
325
  function createSkillsTableSql(tableName) {
276
326
  const safe = sqlIdent(tableName);
277
- return `CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`;
327
+ return `CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', contributors TEXT NOT NULL DEFAULT '[]', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`;
328
+ }
329
+ function addContributorsColumnSql(tableName) {
330
+ const safe = sqlIdent(tableName);
331
+ return `ALTER TABLE "${safe}" ADD COLUMN IF NOT EXISTS contributors TEXT NOT NULL DEFAULT '[]'`;
278
332
  }
279
333
  function esc(s) {
280
334
  return s.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
@@ -282,12 +336,20 @@ function esc(s) {
282
336
  function isMissingTableError(message) {
283
337
  if (!message)
284
338
  return false;
339
+ if (/\bcolumn\b/i.test(message))
340
+ return false;
285
341
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
286
342
  }
343
+ function isMissingContributorsColumnError(message) {
344
+ if (!message)
345
+ return false;
346
+ return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
347
+ }
287
348
  async function insertSkillRow(args) {
288
349
  const id = args.id ?? randomUUID();
289
350
  const sourceSessionsJson = JSON.stringify(args.sourceSessions);
290
- const sql = `INSERT INTO "${sqlIdent(args.tableName)}" (id, name, project, project_key, local_path, install, source_sessions, source_agent, scope, author, description, trigger_text, body, version, created_at, updated_at) VALUES ('${esc(id)}', '${esc(args.name)}', '${esc(args.project)}', '${esc(args.projectKey)}', '${esc(args.localPath)}', '${esc(args.install)}', '${esc(sourceSessionsJson)}', '${esc(args.sourceAgent)}', '${esc(args.scope)}', '${esc(args.author)}', '${esc(args.description)}', '${esc(args.trigger ?? "")}', '${esc(args.body)}', ${args.version}, '${esc(args.createdAt)}', '${esc(args.updatedAt)}')`;
351
+ const contributorsJson = JSON.stringify(args.contributors);
352
+ const sql = `INSERT INTO "${sqlIdent(args.tableName)}" (id, name, project, project_key, local_path, install, source_sessions, source_agent, scope, author, contributors, description, trigger_text, body, version, created_at, updated_at) VALUES ('${esc(id)}', '${esc(args.name)}', '${esc(args.project)}', '${esc(args.projectKey)}', '${esc(args.localPath)}', '${esc(args.install)}', '${esc(sourceSessionsJson)}', '${esc(args.sourceAgent)}', '${esc(args.scope)}', '${esc(args.author)}', '${esc(contributorsJson)}', '${esc(args.description)}', '${esc(args.trigger ?? "")}', '${esc(args.body)}', ${args.version}, '${esc(args.createdAt)}', '${esc(args.updatedAt)}')`;
291
353
  try {
292
354
  await args.query(sql);
293
355
  } catch (e) {
@@ -296,6 +358,11 @@ async function insertSkillRow(args) {
296
358
  await args.query(sql);
297
359
  return;
298
360
  }
361
+ if (isMissingContributorsColumnError(e?.message)) {
362
+ await args.query(addContributorsColumnSql(args.tableName));
363
+ await args.query(sql);
364
+ return;
365
+ }
299
366
  throw e;
300
367
  }
301
368
  }
@@ -447,6 +514,14 @@ function runGate(opts) {
447
514
  }
448
515
  }
449
516
 
517
+ // dist/src/skillify/scope-promotion.js
518
+ function isCrossAuthorMergeVerdict(args) {
519
+ return args.verdict === "MERGE" && args.resultAuthor !== void 0 && args.resultAuthor !== args.userName;
520
+ }
521
+ function resolveRecordScope(args) {
522
+ return args.isCrossAuthorMerge && args.configScope === "me" ? "team" : args.configScope;
523
+ }
524
+
450
525
  // dist/src/skillify/state.js
451
526
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, writeSync, mkdirSync as mkdirSync2, renameSync as renameSync2, existsSync as existsSync4, unlinkSync, openSync, closeSync } from "node:fs";
452
527
  import { execSync } from "node:child_process";
@@ -652,8 +727,6 @@ async function query(sql, retries = 4) {
652
727
  return [];
653
728
  }
654
729
  function authorClause() {
655
- if (cfg.scope === "org")
656
- return "";
657
730
  if (cfg.scope === "team" && cfg.team.length > 0) {
658
731
  const list = cfg.team.map((n) => `'${esc2(n)}'`).join(", ");
659
732
  return ` AND author IN (${list})`;
@@ -721,7 +794,7 @@ ${answer}
721
794
  }
722
795
  function buildPrompt(pairs) {
723
796
  const existing = renderExistingSkillsBlock(cfg.cwd, EXISTING_SKILLS_CHAR_CAP);
724
- const mergeTargetsClause = existing.mergeTargetNames.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.mergeTargetNames.join(", ")}]. Any other name MUST use KEEP, not MERGE.` : `MERGE is FORBIDDEN \u2014 there are no project skills to merge into. Use KEEP or SKIP only.`;
797
+ const mergeTargetsClause = existing.mergeTargetNames.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.mergeTargetNames.join(", ")}]. Any other name MUST use KEEP, not MERGE.` : `MERGE is FORBIDDEN \u2014 there are no existing skills to merge into. Use KEEP or SKIP only.`;
725
798
  return [
726
799
  `You are a skill curator for the "${cfg.project}" project. You decide whether the recent`,
727
800
  `agent activity below contains a recurring, non-trivial pattern worth crystallizing as a`,
@@ -731,18 +804,19 @@ function buildPrompt(pairs) {
731
804
  `- KEEP only if the pattern recurs across at least 3 of the exchanges, is non-obvious to a`,
732
805
  ` competent engineer, and is not already covered by an existing skill below.`,
733
806
  `- SKIP if the activity is one-off, generic engineering work, or already covered.`,
734
- `- MERGE if the pattern is a meaningful extension of an existing PROJECT skill \u2014 produce a`,
807
+ `- MERGE if the pattern is a meaningful extension of an existing skill \u2014 produce a`,
735
808
  ` merged body that incorporates the new evidence without exceeding ~3000 characters or`,
736
809
  ` covering unrelated domains.`,
737
810
  `- ${mergeTargetsClause}`,
738
- `- Skills tagged [global, read-only] are autopulled from the team's shared skills`,
739
- ` table. They exist so you can recognise patterns already covered globally and pick`,
740
- ` SKIP (or a more specific KEEP) instead of duplicating them. They are NOT valid`,
741
- ` MERGE targets \u2014 only [project] skills can be merged into.`,
811
+ `- Cross-author MERGE has a real cost: editing a skill authored by someone else is`,
812
+ ` recorded as a team-level edit (scope=team, contributors+="${cfg.userName}"). Use it only`,
813
+ ` when the new evidence genuinely extends the existing skill; otherwise pick KEEP or SKIP.`,
814
+ ` Tags like [project, author=alice] / [global, author=bob] tell you whose skill it is.`,
742
815
  `- Skill bodies should follow the existing style: short sections (When to use, Workflow,`,
743
816
  ` Anti-patterns), concrete commands and file paths drawn from the exchanges, no marketing.`,
744
817
  ``,
745
- `=== EXISTING SKILLS ([project] are MERGE-eligible, [global] are reference only) ===`,
818
+ `=== EXISTING SKILLS (all MERGE-eligible; [global, author=X] entries from teammate X mean`,
819
+ `cross-author MERGE auto-promotes scope to team) ===`,
746
820
  existing.block,
747
821
  ``,
748
822
  `=== RECENT EXCHANGES (prompt + answer pairs, tool calls already stripped) ===`,
@@ -865,6 +939,17 @@ async function main() {
865
939
  const watermarkDate = oldest.lastMsg;
866
940
  const sourceSessions = usable.map((c) => (c.path.split("/").pop() ?? "").replace(/\.[^.]+$/, ""));
867
941
  async function recordToDeeplake(result, verdict2) {
942
+ const author = result.author ?? cfg.userName;
943
+ const isCrossAuthorMerge = isCrossAuthorMergeVerdict({
944
+ verdict: verdict2.verdict,
945
+ resultAuthor: result.author,
946
+ userName: cfg.userName
947
+ });
948
+ const scope = resolveRecordScope({
949
+ configScope: cfg.scope,
950
+ isCrossAuthorMerge
951
+ });
952
+ const contributors = result.contributors;
868
953
  try {
869
954
  await insertSkillRow({
870
955
  query,
@@ -876,8 +961,9 @@ async function main() {
876
961
  install: cfg.install,
877
962
  sourceSessions,
878
963
  sourceAgent: cfg.agent,
879
- scope: cfg.scope,
880
- author: cfg.userName,
964
+ scope,
965
+ author,
966
+ contributors,
881
967
  description: verdict2.description ?? "",
882
968
  trigger: verdict2.trigger,
883
969
  body: verdict2.body,
@@ -885,7 +971,7 @@ async function main() {
885
971
  createdAt: result.createdAt,
886
972
  updatedAt: result.updatedAt
887
973
  });
888
- wlog(`recorded to skills table: name=${verdict2.name} v${result.version}`);
974
+ wlog(`recorded to skills table: name=${verdict2.name} v${result.version} author=${author} scope=${scope} contributors=${contributors.length}` + (isCrossAuthorMerge ? " [auto-promoted me->team]" : ""));
889
975
  } catch (e) {
890
976
  wlog(`skills table insert failed (non-fatal): ${e.message}`);
891
977
  }
@@ -899,7 +985,8 @@ async function main() {
899
985
  trigger: verdict.trigger,
900
986
  body: verdict.body,
901
987
  sourceSessions,
902
- agent: cfg.agent
988
+ agent: cfg.agent,
989
+ author: cfg.userName
903
990
  });
904
991
  wlog(`wrote new skill: ${result.path}`);
905
992
  recordSkill(cfg.projectKey, verdict.name, watermarkUuid, watermarkDate);
@@ -916,7 +1003,8 @@ async function main() {
916
1003
  description: verdict.description,
917
1004
  body: verdict.body,
918
1005
  newSourceSessions: sourceSessions,
919
- agent: cfg.agent
1006
+ agent: cfg.agent,
1007
+ editor: cfg.userName
920
1008
  });
921
1009
  wlog(`merged into skill: ${result.path} (v${result.version})`);
922
1010
  recordSkill(cfg.projectKey, verdict.name, watermarkUuid, watermarkDate);
@@ -932,7 +1020,8 @@ async function main() {
932
1020
  trigger: verdict.trigger,
933
1021
  body: verdict.body,
934
1022
  sourceSessions,
935
- agent: cfg.agent
1023
+ agent: cfg.agent,
1024
+ author: cfg.userName
936
1025
  });
937
1026
  wlog(`wrote new skill (merge fallback): ${result.path}`);
938
1027
  recordSkill(cfg.projectKey, verdict.name, watermarkUuid, watermarkDate);
@@ -135,13 +135,16 @@ async function uploadSummary(query2, params) {
135
135
  const desc = extractDescription(text);
136
136
  const sizeBytes = Buffer.byteLength(text);
137
137
  const embSql = embeddingSqlLiteral(params.embedding ?? null);
138
+ const pluginVersion = params.pluginVersion;
138
139
  const existing = await query2(`SELECT path FROM "${tableName}" WHERE path = '${esc(vpath)}' LIMIT 1`);
139
140
  if (existing.length > 0) {
140
- const sql2 = `UPDATE "${tableName}" SET summary = E'${esc(text)}', summary_embedding = ${embSql}, size_bytes = ${sizeBytes}, description = E'${esc(desc)}', last_update_date = '${ts}' WHERE path = '${esc(vpath)}'`;
141
+ const pluginVersionSet = pluginVersion === void 0 ? "" : `plugin_version = '${esc(pluginVersion)}', `;
142
+ const sql2 = `UPDATE "${tableName}" SET summary = E'${esc(text)}', summary_embedding = ${embSql}, size_bytes = ${sizeBytes}, description = E'${esc(desc)}', ` + pluginVersionSet + `last_update_date = '${ts}' WHERE path = '${esc(vpath)}'`;
141
143
  await query2(sql2);
142
144
  return { path: "update", sql: sql2, descLength: desc.length, summaryLength: text.length };
143
145
  }
144
- const sql = `INSERT INTO "${tableName}" (id, path, filename, summary, summary_embedding, author, mime_type, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${randomUUID()}', '${esc(vpath)}', '${esc(fname)}', E'${esc(text)}', ${embSql}, '${esc(userName)}', 'text/markdown', ${sizeBytes}, '${esc(project)}', E'${esc(desc)}', '${esc(agent)}', '${ts}', '${ts}')`;
146
+ const pluginVersionForInsert = pluginVersion ?? "";
147
+ const sql = `INSERT INTO "${tableName}" (id, path, filename, summary, summary_embedding, author, mime_type, size_bytes, project, description, agent, plugin_version, creation_date, last_update_date) VALUES ('${randomUUID()}', '${esc(vpath)}', '${esc(fname)}', E'${esc(text)}', ${embSql}, '${esc(userName)}', 'text/markdown', ${sizeBytes}, '${esc(project)}', E'${esc(desc)}', '${esc(agent)}', '${esc(pluginVersionForInsert)}', '${ts}', '${ts}')`;
145
148
  await query2(sql);
146
149
  return { path: "insert", sql, descLength: desc.length, summaryLength: text.length };
147
150
  }
@@ -543,7 +546,8 @@ async function main() {
543
546
  agent: "cursor",
544
547
  sessionId: cfg.sessionId,
545
548
  text,
546
- embedding
549
+ embedding,
550
+ pluginVersion: cfg.pluginVersion ?? ""
547
551
  });
548
552
  wlog(`uploaded ${vpath} (summary=${result.summaryLength}, desc=${result.descLength})`);
549
553
  try {