@memtensor/memos-local-openclaw-plugin 0.3.18 → 0.3.19

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 (69) hide show
  1. package/README.md +21 -11
  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 +7 -2
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/ingest/dedup.d.ts +2 -2
  11. package/dist/ingest/dedup.d.ts.map +1 -1
  12. package/dist/ingest/dedup.js +4 -4
  13. package/dist/ingest/dedup.js.map +1 -1
  14. package/dist/ingest/task-processor.d.ts +1 -1
  15. package/dist/ingest/task-processor.d.ts.map +1 -1
  16. package/dist/ingest/task-processor.js +14 -13
  17. package/dist/ingest/task-processor.js.map +1 -1
  18. package/dist/ingest/worker.d.ts.map +1 -1
  19. package/dist/ingest/worker.js +7 -3
  20. package/dist/ingest/worker.js.map +1 -1
  21. package/dist/recall/engine.d.ts +5 -1
  22. package/dist/recall/engine.d.ts.map +1 -1
  23. package/dist/recall/engine.js +77 -2
  24. package/dist/recall/engine.js.map +1 -1
  25. package/dist/skill/evolver.d.ts +2 -1
  26. package/dist/skill/evolver.d.ts.map +1 -1
  27. package/dist/skill/evolver.js +2 -2
  28. package/dist/skill/evolver.js.map +1 -1
  29. package/dist/skill/generator.d.ts +3 -1
  30. package/dist/skill/generator.d.ts.map +1 -1
  31. package/dist/skill/generator.js +15 -1
  32. package/dist/skill/generator.js.map +1 -1
  33. package/dist/storage/sqlite.d.ts +24 -8
  34. package/dist/storage/sqlite.d.ts.map +1 -1
  35. package/dist/storage/sqlite.js +233 -28
  36. package/dist/storage/sqlite.js.map +1 -1
  37. package/dist/storage/vector.d.ts +1 -1
  38. package/dist/storage/vector.d.ts.map +1 -1
  39. package/dist/storage/vector.js +3 -3
  40. package/dist/storage/vector.js.map +1 -1
  41. package/dist/types.d.ts +16 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/dist/viewer/html.d.ts +1 -1
  45. package/dist/viewer/html.d.ts.map +1 -1
  46. package/dist/viewer/html.js +107 -1
  47. package/dist/viewer/html.js.map +1 -1
  48. package/dist/viewer/server.d.ts +1 -0
  49. package/dist/viewer/server.d.ts.map +1 -1
  50. package/dist/viewer/server.js +52 -3
  51. package/dist/viewer/server.js.map +1 -1
  52. package/index.ts +171 -7
  53. package/package.json +1 -1
  54. package/skill/browserwing-admin/SKILL.md +521 -0
  55. package/skill/browserwing-executor/SKILL.md +510 -0
  56. package/skill/memos-memory-guide/SKILL.md +62 -36
  57. package/src/capture/index.ts +7 -1
  58. package/src/index.ts +3 -2
  59. package/src/ingest/dedup.ts +4 -2
  60. package/src/ingest/task-processor.ts +14 -13
  61. package/src/ingest/worker.ts +7 -3
  62. package/src/recall/engine.ts +94 -4
  63. package/src/skill/evolver.ts +3 -1
  64. package/src/skill/generator.ts +15 -0
  65. package/src/storage/sqlite.ts +262 -34
  66. package/src/storage/vector.ts +3 -2
  67. package/src/types.ts +18 -0
  68. package/src/viewer/html.ts +107 -1
  69. package/src/viewer/server.ts +48 -3
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
2
2
  import { createHash } from "crypto";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
- import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
5
+ import type { Chunk, ChunkRef, DedupStatus, Task, TaskStatus, Skill, SkillStatus, SkillVisibility, SkillVersion, TaskSkillLink, TaskSkillRelation, Logger } from "../types";
6
6
 
7
7
  export class SqliteStore {
8
8
  private db: Database.Database;
@@ -106,6 +106,9 @@ export class SqliteStore {
106
106
  this.migrateApiLogs();
107
107
  this.migrateDedupStatus();
108
108
  this.migrateChunksIndexesForRecall();
109
+ this.migrateOwnerFields();
110
+ this.migrateSkillVisibility();
111
+ this.migrateSkillEmbeddingsAndFts();
109
112
  this.log.debug("Database schema initialized");
110
113
  }
111
114
 
@@ -113,6 +116,85 @@ export class SqliteStore {
113
116
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
114
117
  }
115
118
 
119
+ private migrateOwnerFields(): void {
120
+ const chunkCols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
121
+ if (!chunkCols.some((c) => c.name === "owner")) {
122
+ this.db.exec("ALTER TABLE chunks ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
123
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_owner ON chunks(owner)");
124
+ this.log.info("Migrated: added owner column to chunks");
125
+ }
126
+ const taskCols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
127
+ if (!taskCols.some((c) => c.name === "owner")) {
128
+ this.db.exec("ALTER TABLE tasks ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
129
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner)");
130
+ this.log.info("Migrated: added owner column to tasks");
131
+ }
132
+ }
133
+
134
+ private migrateSkillVisibility(): void {
135
+ const cols = this.db.prepare("PRAGMA table_info(skills)").all() as Array<{ name: string }>;
136
+ if (!cols.some((c) => c.name === "owner")) {
137
+ this.db.exec("ALTER TABLE skills ADD COLUMN owner TEXT NOT NULL DEFAULT 'agent:main'");
138
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(owner)");
139
+ this.log.info("Migrated: added owner column to skills");
140
+ }
141
+ if (!cols.some((c) => c.name === "visibility")) {
142
+ this.db.exec("ALTER TABLE skills ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private'");
143
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(visibility)");
144
+ this.log.info("Migrated: added visibility column to skills");
145
+ }
146
+ }
147
+
148
+ private migrateSkillEmbeddingsAndFts(): void {
149
+ this.db.exec(`
150
+ CREATE TABLE IF NOT EXISTS skill_embeddings (
151
+ skill_id TEXT PRIMARY KEY REFERENCES skills(id) ON DELETE CASCADE,
152
+ vector BLOB NOT NULL,
153
+ dimensions INTEGER NOT NULL,
154
+ updated_at INTEGER NOT NULL
155
+ );
156
+
157
+ CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
158
+ name,
159
+ description,
160
+ content='skills',
161
+ content_rowid='rowid',
162
+ tokenize='porter unicode61'
163
+ );
164
+ `);
165
+
166
+ try {
167
+ this.db.exec(`
168
+ CREATE TRIGGER IF NOT EXISTS skills_ai AFTER INSERT ON skills BEGIN
169
+ INSERT INTO skills_fts(rowid, name, description)
170
+ VALUES (new.rowid, new.name, new.description);
171
+ END;
172
+ CREATE TRIGGER IF NOT EXISTS skills_ad AFTER DELETE ON skills BEGIN
173
+ INSERT INTO skills_fts(skills_fts, rowid, name, description)
174
+ VALUES ('delete', old.rowid, old.name, old.description);
175
+ END;
176
+ CREATE TRIGGER IF NOT EXISTS skills_au AFTER UPDATE ON skills BEGIN
177
+ INSERT INTO skills_fts(skills_fts, rowid, name, description)
178
+ VALUES ('delete', old.rowid, old.name, old.description);
179
+ INSERT INTO skills_fts(rowid, name, description)
180
+ VALUES (new.rowid, new.name, new.description);
181
+ END;
182
+ `);
183
+ } catch {
184
+ // triggers may already exist
185
+ }
186
+
187
+ // Backfill FTS for existing skills
188
+ try {
189
+ const count = (this.db.prepare("SELECT COUNT(*) as c FROM skills_fts").get() as { c: number }).c;
190
+ const skillCount = (this.db.prepare("SELECT COUNT(*) as c FROM skills").get() as { c: number }).c;
191
+ if (count === 0 && skillCount > 0) {
192
+ this.db.exec("INSERT INTO skills_fts(rowid, name, description) SELECT rowid, name, description FROM skills");
193
+ this.log.info(`Migrated: backfilled skills_fts for ${skillCount} skills`);
194
+ }
195
+ } catch { /* best-effort */ }
196
+ }
197
+
116
198
  private migrateTaskId(): void {
117
199
  const cols = this.db.prepare("PRAGMA table_info(chunks)").all() as Array<{ name: string }>;
118
200
  if (!cols.some((c) => c.name === "task_id")) {
@@ -506,8 +588,8 @@ export class SqliteStore {
506
588
 
507
589
  insertChunk(chunk: Chunk): void {
508
590
  const stmt = this.db.prepare(`
509
- INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, dedup_status, dedup_target, dedup_reason, created_at, updated_at)
510
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
591
+ INSERT OR REPLACE INTO chunks (id, session_key, turn_id, seq, role, content, kind, summary, task_id, content_hash, owner, dedup_status, dedup_target, dedup_reason, created_at, updated_at)
592
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
511
593
  `);
512
594
  stmt.run(
513
595
  chunk.id,
@@ -520,6 +602,7 @@ export class SqliteStore {
520
602
  chunk.summary,
521
603
  chunk.taskId,
522
604
  contentHash(chunk.content),
605
+ chunk.owner ?? "agent:main",
523
606
  chunk.dedupStatus ?? "active",
524
607
  chunk.dedupTarget ?? null,
525
608
  chunk.dedupReason ?? null,
@@ -581,19 +664,28 @@ export class SqliteStore {
581
664
 
582
665
  // ─── FTS Search ───
583
666
 
584
- ftsSearch(query: string, limit: number): Array<{ chunkId: string; score: number }> {
667
+ ftsSearch(query: string, limit: number, ownerFilter?: string[]): Array<{ chunkId: string; score: number }> {
585
668
  const sanitized = sanitizeFtsQuery(query);
586
669
  if (!sanitized) return [];
587
670
 
588
671
  try {
589
- const rows = this.db.prepare(`
672
+ let sql = `
590
673
  SELECT c.id as chunk_id, rank
591
674
  FROM chunks_fts f
592
675
  JOIN chunks c ON c.rowid = f.rowid
593
- WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'
594
- ORDER BY rank
595
- LIMIT ?
596
- `).all(sanitized, limit) as Array<{ chunk_id: string; rank: number }>;
676
+ WHERE chunks_fts MATCH ? AND c.dedup_status = 'active'`;
677
+ const params: any[] = [sanitized];
678
+
679
+ if (ownerFilter && ownerFilter.length > 0) {
680
+ const placeholders = ownerFilter.map(() => "?").join(",");
681
+ sql += ` AND c.owner IN (${placeholders})`;
682
+ params.push(...ownerFilter);
683
+ }
684
+
685
+ sql += ` ORDER BY rank LIMIT ?`;
686
+ params.push(limit);
687
+
688
+ const rows = this.db.prepare(sql).all(...params) as Array<{ chunk_id: string; rank: number }>;
597
689
 
598
690
  if (rows.length === 0) return [];
599
691
  const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
@@ -642,12 +734,19 @@ export class SqliteStore {
642
734
 
643
735
  // ─── Vector Search ───
644
736
 
645
- getAllEmbeddings(): Array<{ chunkId: string; vector: number[] }> {
646
- const rows = this.db.prepare(
647
- `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
737
+ getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
738
+ let sql = `SELECT e.chunk_id, e.vector, e.dimensions FROM embeddings e
648
739
  JOIN chunks c ON c.id = e.chunk_id
649
- WHERE c.dedup_status = 'active'`,
650
- ).all() as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
740
+ WHERE c.dedup_status = 'active'`;
741
+ const params: any[] = [];
742
+
743
+ if (ownerFilter && ownerFilter.length > 0) {
744
+ const placeholders = ownerFilter.map(() => "?").join(",");
745
+ sql += ` AND c.owner IN (${placeholders})`;
746
+ params.push(...ownerFilter);
747
+ }
748
+
749
+ const rows = this.db.prepare(sql).all(...params) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
651
750
 
652
751
  return rows.map((r) => ({
653
752
  chunkId: r.chunk_id,
@@ -655,17 +754,25 @@ export class SqliteStore {
655
754
  }));
656
755
  }
657
756
 
658
- /** Like getAllEmbeddings but only for the most recent N chunks (uses idx_chunks_dedup_created). Use for vector search cap to avoid full scan. */
659
- getRecentEmbeddings(limit: number): Array<{ chunkId: string; vector: number[] }> {
660
- if (limit <= 0) return this.getAllEmbeddings();
661
- const rows = this.db.prepare(
662
- `SELECT e.chunk_id, e.vector, e.dimensions
757
+ getRecentEmbeddings(limit: number, ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
758
+ if (limit <= 0) return this.getAllEmbeddings(ownerFilter);
759
+
760
+ let sql = `SELECT e.chunk_id, e.vector, e.dimensions
663
761
  FROM chunks c
664
762
  JOIN embeddings e ON e.chunk_id = c.id
665
- WHERE c.dedup_status = 'active'
666
- ORDER BY c.created_at DESC
667
- LIMIT ?`,
668
- ).all(limit) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
763
+ WHERE c.dedup_status = 'active'`;
764
+ const params: any[] = [];
765
+
766
+ if (ownerFilter && ownerFilter.length > 0) {
767
+ const placeholders = ownerFilter.map(() => "?").join(",");
768
+ sql += ` AND c.owner IN (${placeholders})`;
769
+ params.push(...ownerFilter);
770
+ }
771
+
772
+ sql += ` ORDER BY c.created_at DESC LIMIT ?`;
773
+ params.push(limit);
774
+
775
+ const rows = this.db.prepare(sql).all(...params) as Array<{ chunk_id: string; vector: Buffer; dimensions: number }>;
669
776
 
670
777
  return rows.map((r) => ({
671
778
  chunkId: r.chunk_id,
@@ -683,7 +790,7 @@ export class SqliteStore {
683
790
 
684
791
  // ─── Update ───
685
792
 
686
- updateChunk(chunkId: string, fields: { summary?: string; content?: string; role?: string; kind?: string }): boolean {
793
+ updateChunk(chunkId: string, fields: { summary?: string; content?: string; role?: string; kind?: string; owner?: string }): boolean {
687
794
  const sets: string[] = [];
688
795
  const params: unknown[] = [];
689
796
 
@@ -703,6 +810,10 @@ export class SqliteStore {
703
810
  sets.push("kind = ?");
704
811
  params.push(fields.kind);
705
812
  }
813
+ if (fields.owner !== undefined) {
814
+ sets.push("owner = ?");
815
+ params.push(fields.owner);
816
+ }
706
817
  if (sets.length === 0) return false;
707
818
 
708
819
  sets.push("updated_at = ?");
@@ -747,9 +858,9 @@ export class SqliteStore {
747
858
 
748
859
  insertTask(task: Task): void {
749
860
  this.db.prepare(`
750
- INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, started_at, ended_at, updated_at)
751
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
752
- `).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.startedAt, task.endedAt, task.updatedAt);
861
+ INSERT OR REPLACE INTO tasks (id, session_key, title, summary, status, owner, started_at, ended_at, updated_at)
862
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
863
+ `).run(task.id, task.sessionKey, task.title, task.summary, task.status, task.owner ?? "agent:main", task.startedAt, task.endedAt, task.updatedAt);
753
864
  }
754
865
 
755
866
  getTask(taskId: string): Task | null {
@@ -757,7 +868,13 @@ export class SqliteStore {
757
868
  return row ? rowToTask(row) : null;
758
869
  }
759
870
 
760
- getActiveTask(sessionKey: string): Task | null {
871
+ getActiveTask(sessionKey: string, owner?: string): Task | null {
872
+ if (owner) {
873
+ const row = this.db.prepare(
874
+ "SELECT * FROM tasks WHERE session_key = ? AND status = 'active' AND owner = ? ORDER BY started_at DESC LIMIT 1",
875
+ ).get(sessionKey, owner) as TaskRow | undefined;
876
+ return row ? rowToTask(row) : null;
877
+ }
761
878
  const row = this.db.prepare(
762
879
  "SELECT * FROM tasks WHERE session_key = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1",
763
880
  ).get(sessionKey) as TaskRow | undefined;
@@ -785,7 +902,13 @@ export class SqliteStore {
785
902
  return rows.map(rowToTask);
786
903
  }
787
904
 
788
- getAllActiveTasks(): Task[] {
905
+ getAllActiveTasks(owner?: string): Task[] {
906
+ if (owner) {
907
+ const rows = this.db.prepare(
908
+ "SELECT * FROM tasks WHERE status = 'active' AND owner = ? ORDER BY started_at DESC",
909
+ ).all(owner) as TaskRow[];
910
+ return rows.map(rowToTask);
911
+ }
789
912
  const rows = this.db.prepare(
790
913
  "SELECT * FROM tasks WHERE status = 'active' ORDER BY started_at DESC",
791
914
  ).all() as TaskRow[];
@@ -812,10 +935,11 @@ export class SqliteStore {
812
935
  return rows.map(rowToChunk);
813
936
  }
814
937
 
815
- listTasks(opts: { status?: string; limit?: number; offset?: number } = {}): { tasks: Task[]; total: number } {
938
+ listTasks(opts: { status?: string; limit?: number; offset?: number; owner?: string } = {}): { tasks: Task[]; total: number } {
816
939
  const conditions: string[] = [];
817
940
  const params: unknown[] = [];
818
941
  if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
942
+ if (opts.owner) { conditions.push("owner = ?"); params.push(opts.owner); }
819
943
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
820
944
 
821
945
  const countRow = this.db.prepare(`SELECT COUNT(*) as c FROM tasks ${whereClause}`).get(...params) as { c: number };
@@ -839,7 +963,13 @@ export class SqliteStore {
839
963
  this.db.prepare("UPDATE chunks SET task_id = ?, updated_at = ? WHERE id = ?").run(taskId, Date.now(), chunkId);
840
964
  }
841
965
 
842
- getUnassignedChunks(sessionKey: string): Chunk[] {
966
+ getUnassignedChunks(sessionKey: string, owner?: string): Chunk[] {
967
+ if (owner) {
968
+ const rows = this.db.prepare(
969
+ "SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL AND owner = ? ORDER BY created_at, seq",
970
+ ).all(sessionKey, owner) as ChunkRow[];
971
+ return rows.map(rowToChunk);
972
+ }
843
973
  const rows = this.db.prepare(
844
974
  "SELECT * FROM chunks WHERE session_key = ? AND task_id IS NULL ORDER BY created_at, seq",
845
975
  ).all(sessionKey) as ChunkRow[];
@@ -877,9 +1007,9 @@ export class SqliteStore {
877
1007
 
878
1008
  insertSkill(skill: Skill): void {
879
1009
  this.db.prepare(`
880
- INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, quality_score, created_at, updated_at)
881
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
882
- `).run(skill.id, skill.name, skill.description, skill.version, skill.status, skill.tags, skill.sourceType, skill.dirPath, skill.installed, skill.qualityScore, skill.createdAt, skill.updatedAt);
1010
+ INSERT OR REPLACE INTO skills (id, name, description, version, status, tags, source_type, dir_path, installed, owner, visibility, quality_score, created_at, updated_at)
1011
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1012
+ `).run(skill.id, skill.name, skill.description, skill.version, skill.status, skill.tags, skill.sourceType, skill.dirPath, skill.installed, skill.owner ?? "agent:main", skill.visibility ?? "private", skill.qualityScore, skill.createdAt, skill.updatedAt);
883
1013
  }
884
1014
 
885
1015
  getSkill(skillId: string): Skill | null {
@@ -914,6 +1044,96 @@ export class SqliteStore {
914
1044
  return rows.map(rowToSkill);
915
1045
  }
916
1046
 
1047
+ // ─── Skill Visibility & Embeddings ───
1048
+
1049
+ setSkillVisibility(skillId: string, visibility: SkillVisibility): void {
1050
+ this.db.prepare("UPDATE skills SET visibility = ?, updated_at = ? WHERE id = ?")
1051
+ .run(visibility, Date.now(), skillId);
1052
+ }
1053
+
1054
+ upsertSkillEmbedding(skillId: string, vector: number[]): void {
1055
+ const buf = Buffer.from(new Float32Array(vector).buffer);
1056
+ this.db.prepare(`
1057
+ INSERT OR REPLACE INTO skill_embeddings (skill_id, vector, dimensions, updated_at)
1058
+ VALUES (?, ?, ?, ?)
1059
+ `).run(skillId, buf, vector.length, Date.now());
1060
+ }
1061
+
1062
+ getSkillEmbedding(skillId: string): number[] | null {
1063
+ const row = this.db.prepare(
1064
+ "SELECT vector, dimensions FROM skill_embeddings WHERE skill_id = ?",
1065
+ ).get(skillId) as { vector: Buffer; dimensions: number } | undefined;
1066
+ if (!row) return null;
1067
+ return Array.from(new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions));
1068
+ }
1069
+
1070
+ getSkillEmbeddings(scope: "self" | "public" | "mix", currentOwner: string): Array<{ skillId: string; vector: number[] }> {
1071
+ let sql = `SELECT se.skill_id, se.vector, se.dimensions
1072
+ FROM skill_embeddings se
1073
+ JOIN skills s ON s.id = se.skill_id
1074
+ WHERE s.status = 'active'`;
1075
+ const params: any[] = [];
1076
+
1077
+ if (scope === "self") {
1078
+ sql += ` AND s.owner = ?`;
1079
+ params.push(currentOwner);
1080
+ } else if (scope === "public") {
1081
+ sql += ` AND s.visibility = 'public'`;
1082
+ } else {
1083
+ sql += ` AND (s.owner = ? OR s.visibility = 'public')`;
1084
+ params.push(currentOwner);
1085
+ }
1086
+
1087
+ const rows = this.db.prepare(sql).all(...params) as Array<{ skill_id: string; vector: Buffer; dimensions: number }>;
1088
+ return rows.map((r) => ({
1089
+ skillId: r.skill_id,
1090
+ vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
1091
+ }));
1092
+ }
1093
+
1094
+ skillFtsSearch(query: string, limit: number, scope: "self" | "public" | "mix", currentOwner: string): Array<{ skillId: string; score: number }> {
1095
+ const sanitized = sanitizeFtsQuery(query);
1096
+ if (!sanitized) return [];
1097
+
1098
+ try {
1099
+ let sql = `
1100
+ SELECT s.id as skill_id, rank
1101
+ FROM skills_fts f
1102
+ JOIN skills s ON s.rowid = f.rowid
1103
+ WHERE skills_fts MATCH ? AND s.status = 'active'`;
1104
+ const params: any[] = [sanitized];
1105
+
1106
+ if (scope === "self") {
1107
+ sql += ` AND s.owner = ?`;
1108
+ params.push(currentOwner);
1109
+ } else if (scope === "public") {
1110
+ sql += ` AND s.visibility = 'public'`;
1111
+ } else {
1112
+ sql += ` AND (s.owner = ? OR s.visibility = 'public')`;
1113
+ params.push(currentOwner);
1114
+ }
1115
+
1116
+ sql += ` ORDER BY rank LIMIT ?`;
1117
+ params.push(limit);
1118
+
1119
+ const rows = this.db.prepare(sql).all(...params) as Array<{ skill_id: string; rank: number }>;
1120
+ if (rows.length === 0) return [];
1121
+ const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
1122
+ return rows.map((r) => ({
1123
+ skillId: r.skill_id,
1124
+ score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
1125
+ }));
1126
+ } catch {
1127
+ this.log.warn(`Skill FTS query failed for: "${sanitized}", returning empty`);
1128
+ return [];
1129
+ }
1130
+ }
1131
+
1132
+ listPublicSkills(): Skill[] {
1133
+ const rows = this.db.prepare("SELECT * FROM skills WHERE visibility = 'public' AND status = 'active' ORDER BY updated_at DESC").all() as SkillRow[];
1134
+ return rows.map(rowToSkill);
1135
+ }
1136
+
917
1137
  // ─── Skill Versions ───
918
1138
 
919
1139
  insertSkillVersion(sv: SkillVersion): void {
@@ -1029,6 +1249,7 @@ interface ChunkRow {
1029
1249
  summary: string;
1030
1250
  task_id: string | null;
1031
1251
  skill_id: string | null;
1252
+ owner: string;
1032
1253
  dedup_status: string;
1033
1254
  dedup_target: string | null;
1034
1255
  dedup_reason: string | null;
@@ -1052,6 +1273,7 @@ function rowToChunk(row: ChunkRow): Chunk {
1052
1273
  embedding: null,
1053
1274
  taskId: row.task_id,
1054
1275
  skillId: row.skill_id ?? null,
1276
+ owner: row.owner ?? "agent:main",
1055
1277
  dedupStatus: (row.dedup_status ?? "active") as DedupStatus,
1056
1278
  dedupTarget: row.dedup_target ?? null,
1057
1279
  dedupReason: row.dedup_reason ?? null,
@@ -1069,6 +1291,7 @@ interface TaskRow {
1069
1291
  title: string;
1070
1292
  summary: string;
1071
1293
  status: string;
1294
+ owner: string;
1072
1295
  started_at: number;
1073
1296
  ended_at: number | null;
1074
1297
  updated_at: number;
@@ -1081,6 +1304,7 @@ function rowToTask(row: TaskRow): Task {
1081
1304
  title: row.title,
1082
1305
  summary: row.summary,
1083
1306
  status: row.status as Task["status"],
1307
+ owner: row.owner ?? "agent:main",
1084
1308
  startedAt: row.started_at,
1085
1309
  endedAt: row.ended_at,
1086
1310
  updatedAt: row.updated_at,
@@ -1097,6 +1321,8 @@ interface SkillRow {
1097
1321
  source_type: string;
1098
1322
  dir_path: string;
1099
1323
  installed: number;
1324
+ owner: string;
1325
+ visibility: string;
1100
1326
  quality_score: number | null;
1101
1327
  created_at: number;
1102
1328
  updated_at: number;
@@ -1113,6 +1339,8 @@ function rowToSkill(row: SkillRow): Skill {
1113
1339
  sourceType: row.source_type as Skill["sourceType"],
1114
1340
  dirPath: row.dir_path,
1115
1341
  installed: row.installed,
1342
+ owner: row.owner ?? "agent:main",
1343
+ visibility: (row.visibility ?? "private") as Skill["visibility"],
1116
1344
  qualityScore: row.quality_score ?? null,
1117
1345
  createdAt: row.created_at,
1118
1346
  updatedAt: row.updated_at,
@@ -28,10 +28,11 @@ export function vectorSearch(
28
28
  queryVec: number[],
29
29
  topK: number,
30
30
  maxChunks?: number,
31
+ ownerFilter?: string[],
31
32
  ): VectorHit[] {
32
33
  const all = maxChunks != null && maxChunks > 0
33
- ? store.getRecentEmbeddings(maxChunks)
34
- : store.getAllEmbeddings();
34
+ ? store.getRecentEmbeddings(maxChunks, ownerFilter)
35
+ : store.getAllEmbeddings(ownerFilter);
35
36
  const scored: VectorHit[] = all.map((row) => ({
36
37
  chunkId: row.chunkId,
37
38
  score: cosineSimilarity(queryVec, row.vector),
package/src/types.ts CHANGED
@@ -9,6 +9,7 @@ export interface ConversationMessage {
9
9
  turnId: string;
10
10
  sessionKey: string;
11
11
  toolName?: string;
12
+ owner?: string;
12
13
  }
13
14
 
14
15
  // ─── Chunk & Storage ───
@@ -27,6 +28,7 @@ export interface Chunk {
27
28
  embedding: number[] | null;
28
29
  taskId: string | null;
29
30
  skillId: string | null;
31
+ owner: string;
30
32
  dedupStatus: DedupStatus;
31
33
  dedupTarget: string | null;
32
34
  dedupReason: string | null;
@@ -47,6 +49,7 @@ export interface Task {
47
49
  title: string;
48
50
  summary: string;
49
51
  status: TaskStatus;
52
+ owner: string;
50
53
  startedAt: number;
51
54
  endedAt: number | null;
52
55
  updatedAt: number;
@@ -77,6 +80,7 @@ export interface SearchHit {
77
80
  score: number;
78
81
  taskId: string | null;
79
82
  skillId: string | null;
83
+ owner?: string;
80
84
  source: {
81
85
  ts: number;
82
86
  role: Role;
@@ -84,6 +88,16 @@ export interface SearchHit {
84
88
  };
85
89
  }
86
90
 
91
+ export interface SkillSearchHit {
92
+ skillId: string;
93
+ name: string;
94
+ description: string;
95
+ owner: string;
96
+ visibility: SkillVisibility;
97
+ score: number;
98
+ reason: string;
99
+ }
100
+
87
101
  export interface SearchResult {
88
102
  hits: SearchHit[];
89
103
  meta: {
@@ -176,6 +190,8 @@ export type SkillStatus = "active" | "archived" | "draft";
176
190
  export type SkillUpgradeType = "create" | "refine" | "extend" | "fix";
177
191
  export type TaskSkillRelation = "generated_from" | "evolved_from" | "applied_to";
178
192
 
193
+ export type SkillVisibility = "private" | "public";
194
+
179
195
  export interface Skill {
180
196
  id: string;
181
197
  name: string;
@@ -186,6 +202,8 @@ export interface Skill {
186
202
  sourceType: "task" | "manual";
187
203
  dirPath: string;
188
204
  installed: number;
205
+ owner: string;
206
+ visibility: SkillVisibility;
189
207
  qualityScore: number | null;
190
208
  createdAt: number;
191
209
  updatedAt: number;