@deeplake/hivemind 0.7.19 → 0.7.21

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.
@@ -6,13 +6,13 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
9
- "version": "0.7.19"
9
+ "version": "0.7.21"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "hivemind",
14
14
  "description": "Persistent shared memory powered by Deeplake — captures all session activity and provides cross-session, cross-agent memory search",
15
- "version": "0.7.19",
15
+ "version": "0.7.21",
16
16
  "source": "./claude-code",
17
17
  "homepage": "https://github.com/activeloopai/hivemind"
18
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hivemind",
3
3
  "description": "Cloud-backed persistent memory powered by Deeplake — read, write, and share memory across Claude Code sessions and agents",
4
- "version": "0.7.19",
4
+ "version": "0.7.21",
5
5
  "author": {
6
6
  "name": "Activeloop",
7
7
  "url": "https://deeplake.ai"
@@ -166,7 +166,7 @@ function writeNewSkill(args) {
166
166
  ${args.body.trim()}
167
167
  `;
168
168
  writeFileSync(path, text);
169
- return { path, action: "created", version: 1 };
169
+ return { path, action: "created", version: 1, createdAt: now, updatedAt: now };
170
170
  }
171
171
  function mergeSkill(args) {
172
172
  assertValidSkillName(args.name);
@@ -195,7 +195,7 @@ function mergeSkill(args) {
195
195
  ${args.body.trim()}
196
196
  `;
197
197
  writeFileSync(path, text);
198
- return { path, action: "merged", version: fm.version };
198
+ return { path, action: "merged", version: fm.version, createdAt: fm.created_at, updatedAt: fm.updated_at };
199
199
  }
200
200
  function listSkills(skillsRoot) {
201
201
  if (!existsSync(skillsRoot))
@@ -216,6 +216,50 @@ function resolveSkillsRoot(install, cwd) {
216
216
  return join2(cwd, ".claude", "skills");
217
217
  }
218
218
 
219
+ // dist/src/skillify/existing-skills.js
220
+ function listAllExistingSkills(cwd) {
221
+ const projectRoot = resolveSkillsRoot("project", cwd);
222
+ const globalRoot = resolveSkillsRoot("global", cwd);
223
+ const tagged = [
224
+ ...listSkills(projectRoot).map((s) => ({ ...s, source: "project" })),
225
+ ...listSkills(globalRoot).map((s) => ({ ...s, source: "global" }))
226
+ ];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ const out = [];
229
+ for (const s of tagged) {
230
+ if (seen.has(s.name))
231
+ continue;
232
+ seen.add(s.name);
233
+ out.push(s);
234
+ }
235
+ return out;
236
+ }
237
+ function renderExistingSkillsBlock(cwd, charCap) {
238
+ const skills = listAllExistingSkills(cwd);
239
+ if (skills.length === 0) {
240
+ return {
241
+ mergeTargetNames: [],
242
+ block: "(no existing skills \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
243
+ };
244
+ }
245
+ const mergeTargetNames = skills.filter((s) => s.source === "project").map((s) => s.name);
246
+ let total = 0;
247
+ const out = [];
248
+ for (const s of skills) {
249
+ const tag = s.source === "project" ? "[project]" : "[global, read-only]";
250
+ const block = `--- existing skill ${tag}: ${s.name} ---
251
+ ${s.body}
252
+ `;
253
+ if (total + block.length > charCap) {
254
+ out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
255
+ break;
256
+ }
257
+ out.push(block);
258
+ total += block.length;
259
+ }
260
+ return { mergeTargetNames, block: out.join("\n") };
261
+ }
262
+
219
263
  // dist/src/skillify/skills-table.js
220
264
  import { randomUUID } from "node:crypto";
221
265
 
@@ -675,34 +719,9 @@ ${answer}
675
719
  }
676
720
  return out.join("\n");
677
721
  }
678
- function renderExistingSkillsBlock() {
679
- const skills = listSkills(resolveSkillsRoot(cfg.install, cfg.cwd));
680
- if (skills.length === 0) {
681
- return {
682
- names: [],
683
- block: "(no existing skills in this project \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
684
- };
685
- }
686
- let total = 0;
687
- const out = [];
688
- const names = [];
689
- for (const s of skills) {
690
- const block = `--- existing skill: ${s.name} ---
691
- ${s.body}
692
- `;
693
- if (total + block.length > EXISTING_SKILLS_CHAR_CAP) {
694
- out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
695
- break;
696
- }
697
- out.push(block);
698
- names.push(s.name);
699
- total += block.length;
700
- }
701
- return { names, block: out.join("\n") };
702
- }
703
722
  function buildPrompt(pairs) {
704
- const existing = renderExistingSkillsBlock();
705
- const mergeTargetsClause = existing.names.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.names.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.`;
723
+ 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.`;
706
725
  return [
707
726
  `You are a skill curator for the "${cfg.project}" project. You decide whether the recent`,
708
727
  `agent activity below contains a recurring, non-trivial pattern worth crystallizing as a`,
@@ -716,12 +735,14 @@ function buildPrompt(pairs) {
716
735
  ` merged body that incorporates the new evidence without exceeding ~3000 characters or`,
717
736
  ` covering unrelated domains.`,
718
737
  `- ${mergeTargetsClause}`,
719
- `- Do NOT reference skills outside this project (e.g. ones from ~/.claude/skills/). Only`,
720
- ` the project skills listed below count for MERGE.`,
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.`,
721
742
  `- Skill bodies should follow the existing style: short sections (When to use, Workflow,`,
722
743
  ` Anti-patterns), concrete commands and file paths drawn from the exchanges, no marketing.`,
723
744
  ``,
724
- `=== EXISTING PROJECT SKILLS ===`,
745
+ `=== EXISTING SKILLS ([project] are MERGE-eligible, [global] are reference only) ===`,
725
746
  existing.block,
726
747
  ``,
727
748
  `=== RECENT EXCHANGES (prompt + answer pairs, tool calls already stripped) ===`,
@@ -861,8 +882,8 @@ async function main() {
861
882
  trigger: verdict2.trigger,
862
883
  body: verdict2.body,
863
884
  version: result.version,
864
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
865
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
885
+ createdAt: result.createdAt,
886
+ updatedAt: result.updatedAt
866
887
  });
867
888
  wlog(`recorded to skills table: name=${verdict2.name} v${result.version}`);
868
889
  } catch (e) {
@@ -823,15 +823,41 @@ function statePath(projectKey) {
823
823
  function lockPath(projectKey) {
824
824
  return join9(STATE_DIR, `${projectKey}.lock`);
825
825
  }
826
+ var DEFAULT_PORTS = {
827
+ http: "80",
828
+ https: "443",
829
+ ssh: "22",
830
+ git: "9418"
831
+ };
832
+ function normalizeGitRemoteUrl(url) {
833
+ let s = url.trim();
834
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
835
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
836
+ if (schemeMatch)
837
+ s = s.slice(schemeMatch[0].length);
838
+ if (!scheme) {
839
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
840
+ if (scp)
841
+ s = `${scp[1]}/${scp[2]}`;
842
+ }
843
+ s = s.replace(/^[^@/]+@/, "");
844
+ if (scheme && DEFAULT_PORTS[scheme]) {
845
+ s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
846
+ }
847
+ s = s.replace(/\.git\/?$/i, "");
848
+ s = s.replace(/\/+$/, "");
849
+ return s.toLowerCase();
850
+ }
826
851
  function deriveProjectKey(cwd) {
827
852
  const project = basename(cwd) || "unknown";
828
853
  let signature = null;
829
854
  try {
830
- signature = execSync2("git config --get remote.origin.url", {
855
+ const raw = execSync2("git config --get remote.origin.url", {
831
856
  cwd,
832
857
  encoding: "utf-8",
833
858
  stdio: ["ignore", "pipe", "ignore"]
834
- }).trim() || null;
859
+ }).trim();
860
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
835
861
  } catch {
836
862
  }
837
863
  const input = signature ?? cwd;
@@ -1240,15 +1240,41 @@ function statePath2(projectKey) {
1240
1240
  function lockPath2(projectKey) {
1241
1241
  return join12(STATE_DIR2, `${projectKey}.lock`);
1242
1242
  }
1243
+ var DEFAULT_PORTS = {
1244
+ http: "80",
1245
+ https: "443",
1246
+ ssh: "22",
1247
+ git: "9418"
1248
+ };
1249
+ function normalizeGitRemoteUrl(url) {
1250
+ let s = url.trim();
1251
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
1252
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
1253
+ if (schemeMatch)
1254
+ s = s.slice(schemeMatch[0].length);
1255
+ if (!scheme) {
1256
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
1257
+ if (scp)
1258
+ s = `${scp[1]}/${scp[2]}`;
1259
+ }
1260
+ s = s.replace(/^[^@/]+@/, "");
1261
+ if (scheme && DEFAULT_PORTS[scheme]) {
1262
+ s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
1263
+ }
1264
+ s = s.replace(/\.git\/?$/i, "");
1265
+ s = s.replace(/\/+$/, "");
1266
+ return s.toLowerCase();
1267
+ }
1243
1268
  function deriveProjectKey(cwd) {
1244
1269
  const project = basename(cwd) || "unknown";
1245
1270
  let signature = null;
1246
1271
  try {
1247
- signature = execSync2("git config --get remote.origin.url", {
1272
+ const raw = execSync2("git config --get remote.origin.url", {
1248
1273
  cwd,
1249
1274
  encoding: "utf-8",
1250
1275
  stdio: ["ignore", "pipe", "ignore"]
1251
- }).trim() || null;
1276
+ }).trim();
1277
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
1252
1278
  } catch {
1253
1279
  }
1254
1280
  const input = signature ?? cwd;
@@ -374,15 +374,41 @@ function statePath(projectKey) {
374
374
  function lockPath2(projectKey) {
375
375
  return join9(STATE_DIR2, `${projectKey}.lock`);
376
376
  }
377
+ var DEFAULT_PORTS = {
378
+ http: "80",
379
+ https: "443",
380
+ ssh: "22",
381
+ git: "9418"
382
+ };
383
+ function normalizeGitRemoteUrl(url) {
384
+ let s = url.trim();
385
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
386
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
387
+ if (schemeMatch)
388
+ s = s.slice(schemeMatch[0].length);
389
+ if (!scheme) {
390
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
391
+ if (scp)
392
+ s = `${scp[1]}/${scp[2]}`;
393
+ }
394
+ s = s.replace(/^[^@/]+@/, "");
395
+ if (scheme && DEFAULT_PORTS[scheme]) {
396
+ s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
397
+ }
398
+ s = s.replace(/\.git\/?$/i, "");
399
+ s = s.replace(/\/+$/, "");
400
+ return s.toLowerCase();
401
+ }
377
402
  function deriveProjectKey(cwd) {
378
403
  const project = basename(cwd) || "unknown";
379
404
  let signature = null;
380
405
  try {
381
- signature = execSync2("git config --get remote.origin.url", {
406
+ const raw = execSync2("git config --get remote.origin.url", {
382
407
  cwd,
383
408
  encoding: "utf-8",
384
409
  stdio: ["ignore", "pipe", "ignore"]
385
- }).trim() || null;
410
+ }).trim();
411
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
386
412
  } catch {
387
413
  }
388
414
  const input = signature ?? cwd;
@@ -166,7 +166,7 @@ function writeNewSkill(args) {
166
166
  ${args.body.trim()}
167
167
  `;
168
168
  writeFileSync(path, text);
169
- return { path, action: "created", version: 1 };
169
+ return { path, action: "created", version: 1, createdAt: now, updatedAt: now };
170
170
  }
171
171
  function mergeSkill(args) {
172
172
  assertValidSkillName(args.name);
@@ -195,7 +195,7 @@ function mergeSkill(args) {
195
195
  ${args.body.trim()}
196
196
  `;
197
197
  writeFileSync(path, text);
198
- return { path, action: "merged", version: fm.version };
198
+ return { path, action: "merged", version: fm.version, createdAt: fm.created_at, updatedAt: fm.updated_at };
199
199
  }
200
200
  function listSkills(skillsRoot) {
201
201
  if (!existsSync(skillsRoot))
@@ -216,6 +216,50 @@ function resolveSkillsRoot(install, cwd) {
216
216
  return join2(cwd, ".claude", "skills");
217
217
  }
218
218
 
219
+ // dist/src/skillify/existing-skills.js
220
+ function listAllExistingSkills(cwd) {
221
+ const projectRoot = resolveSkillsRoot("project", cwd);
222
+ const globalRoot = resolveSkillsRoot("global", cwd);
223
+ const tagged = [
224
+ ...listSkills(projectRoot).map((s) => ({ ...s, source: "project" })),
225
+ ...listSkills(globalRoot).map((s) => ({ ...s, source: "global" }))
226
+ ];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ const out = [];
229
+ for (const s of tagged) {
230
+ if (seen.has(s.name))
231
+ continue;
232
+ seen.add(s.name);
233
+ out.push(s);
234
+ }
235
+ return out;
236
+ }
237
+ function renderExistingSkillsBlock(cwd, charCap) {
238
+ const skills = listAllExistingSkills(cwd);
239
+ if (skills.length === 0) {
240
+ return {
241
+ mergeTargetNames: [],
242
+ block: "(no existing skills \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
243
+ };
244
+ }
245
+ const mergeTargetNames = skills.filter((s) => s.source === "project").map((s) => s.name);
246
+ let total = 0;
247
+ const out = [];
248
+ for (const s of skills) {
249
+ const tag = s.source === "project" ? "[project]" : "[global, read-only]";
250
+ const block = `--- existing skill ${tag}: ${s.name} ---
251
+ ${s.body}
252
+ `;
253
+ if (total + block.length > charCap) {
254
+ out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
255
+ break;
256
+ }
257
+ out.push(block);
258
+ total += block.length;
259
+ }
260
+ return { mergeTargetNames, block: out.join("\n") };
261
+ }
262
+
219
263
  // dist/src/skillify/skills-table.js
220
264
  import { randomUUID } from "node:crypto";
221
265
 
@@ -675,34 +719,9 @@ ${answer}
675
719
  }
676
720
  return out.join("\n");
677
721
  }
678
- function renderExistingSkillsBlock() {
679
- const skills = listSkills(resolveSkillsRoot(cfg.install, cfg.cwd));
680
- if (skills.length === 0) {
681
- return {
682
- names: [],
683
- block: "(no existing skills in this project \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
684
- };
685
- }
686
- let total = 0;
687
- const out = [];
688
- const names = [];
689
- for (const s of skills) {
690
- const block = `--- existing skill: ${s.name} ---
691
- ${s.body}
692
- `;
693
- if (total + block.length > EXISTING_SKILLS_CHAR_CAP) {
694
- out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
695
- break;
696
- }
697
- out.push(block);
698
- names.push(s.name);
699
- total += block.length;
700
- }
701
- return { names, block: out.join("\n") };
702
- }
703
722
  function buildPrompt(pairs) {
704
- const existing = renderExistingSkillsBlock();
705
- const mergeTargetsClause = existing.names.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.names.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.`;
723
+ 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.`;
706
725
  return [
707
726
  `You are a skill curator for the "${cfg.project}" project. You decide whether the recent`,
708
727
  `agent activity below contains a recurring, non-trivial pattern worth crystallizing as a`,
@@ -716,12 +735,14 @@ function buildPrompt(pairs) {
716
735
  ` merged body that incorporates the new evidence without exceeding ~3000 characters or`,
717
736
  ` covering unrelated domains.`,
718
737
  `- ${mergeTargetsClause}`,
719
- `- Do NOT reference skills outside this project (e.g. ones from ~/.claude/skills/). Only`,
720
- ` the project skills listed below count for MERGE.`,
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.`,
721
742
  `- Skill bodies should follow the existing style: short sections (When to use, Workflow,`,
722
743
  ` Anti-patterns), concrete commands and file paths drawn from the exchanges, no marketing.`,
723
744
  ``,
724
- `=== EXISTING PROJECT SKILLS ===`,
745
+ `=== EXISTING SKILLS ([project] are MERGE-eligible, [global] are reference only) ===`,
725
746
  existing.block,
726
747
  ``,
727
748
  `=== RECENT EXCHANGES (prompt + answer pairs, tool calls already stripped) ===`,
@@ -861,8 +882,8 @@ async function main() {
861
882
  trigger: verdict2.trigger,
862
883
  body: verdict2.body,
863
884
  version: result.version,
864
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
865
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
885
+ createdAt: result.createdAt,
886
+ updatedAt: result.updatedAt
866
887
  });
867
888
  wlog(`recorded to skills table: name=${verdict2.name} v${result.version}`);
868
889
  } catch (e) {
@@ -1240,15 +1240,41 @@ function statePath2(projectKey) {
1240
1240
  function lockPath2(projectKey) {
1241
1241
  return join12(STATE_DIR2, `${projectKey}.lock`);
1242
1242
  }
1243
+ var DEFAULT_PORTS = {
1244
+ http: "80",
1245
+ https: "443",
1246
+ ssh: "22",
1247
+ git: "9418"
1248
+ };
1249
+ function normalizeGitRemoteUrl(url) {
1250
+ let s = url.trim();
1251
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
1252
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
1253
+ if (schemeMatch)
1254
+ s = s.slice(schemeMatch[0].length);
1255
+ if (!scheme) {
1256
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
1257
+ if (scp)
1258
+ s = `${scp[1]}/${scp[2]}`;
1259
+ }
1260
+ s = s.replace(/^[^@/]+@/, "");
1261
+ if (scheme && DEFAULT_PORTS[scheme]) {
1262
+ s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
1263
+ }
1264
+ s = s.replace(/\.git\/?$/i, "");
1265
+ s = s.replace(/\/+$/, "");
1266
+ return s.toLowerCase();
1267
+ }
1243
1268
  function deriveProjectKey(cwd) {
1244
1269
  const project = basename(cwd) || "unknown";
1245
1270
  let signature = null;
1246
1271
  try {
1247
- signature = execSync2("git config --get remote.origin.url", {
1272
+ const raw = execSync2("git config --get remote.origin.url", {
1248
1273
  cwd,
1249
1274
  encoding: "utf-8",
1250
1275
  stdio: ["ignore", "pipe", "ignore"]
1251
- }).trim() || null;
1276
+ }).trim();
1277
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
1252
1278
  } catch {
1253
1279
  }
1254
1280
  const input = signature ?? cwd;
@@ -373,15 +373,41 @@ function statePath(projectKey) {
373
373
  function lockPath2(projectKey) {
374
374
  return join9(STATE_DIR2, `${projectKey}.lock`);
375
375
  }
376
+ var DEFAULT_PORTS = {
377
+ http: "80",
378
+ https: "443",
379
+ ssh: "22",
380
+ git: "9418"
381
+ };
382
+ function normalizeGitRemoteUrl(url) {
383
+ let s = url.trim();
384
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
385
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
386
+ if (schemeMatch)
387
+ s = s.slice(schemeMatch[0].length);
388
+ if (!scheme) {
389
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
390
+ if (scp)
391
+ s = `${scp[1]}/${scp[2]}`;
392
+ }
393
+ s = s.replace(/^[^@/]+@/, "");
394
+ if (scheme && DEFAULT_PORTS[scheme]) {
395
+ s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
396
+ }
397
+ s = s.replace(/\.git\/?$/i, "");
398
+ s = s.replace(/\/+$/, "");
399
+ return s.toLowerCase();
400
+ }
376
401
  function deriveProjectKey(cwd) {
377
402
  const project = basename(cwd) || "unknown";
378
403
  let signature = null;
379
404
  try {
380
- signature = execSync2("git config --get remote.origin.url", {
405
+ const raw = execSync2("git config --get remote.origin.url", {
381
406
  cwd,
382
407
  encoding: "utf-8",
383
408
  stdio: ["ignore", "pipe", "ignore"]
384
- }).trim() || null;
409
+ }).trim();
410
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
385
411
  } catch {
386
412
  }
387
413
  const input = signature ?? cwd;
@@ -166,7 +166,7 @@ function writeNewSkill(args) {
166
166
  ${args.body.trim()}
167
167
  `;
168
168
  writeFileSync(path, text);
169
- return { path, action: "created", version: 1 };
169
+ return { path, action: "created", version: 1, createdAt: now, updatedAt: now };
170
170
  }
171
171
  function mergeSkill(args) {
172
172
  assertValidSkillName(args.name);
@@ -195,7 +195,7 @@ function mergeSkill(args) {
195
195
  ${args.body.trim()}
196
196
  `;
197
197
  writeFileSync(path, text);
198
- return { path, action: "merged", version: fm.version };
198
+ return { path, action: "merged", version: fm.version, createdAt: fm.created_at, updatedAt: fm.updated_at };
199
199
  }
200
200
  function listSkills(skillsRoot) {
201
201
  if (!existsSync(skillsRoot))
@@ -216,6 +216,50 @@ function resolveSkillsRoot(install, cwd) {
216
216
  return join2(cwd, ".claude", "skills");
217
217
  }
218
218
 
219
+ // dist/src/skillify/existing-skills.js
220
+ function listAllExistingSkills(cwd) {
221
+ const projectRoot = resolveSkillsRoot("project", cwd);
222
+ const globalRoot = resolveSkillsRoot("global", cwd);
223
+ const tagged = [
224
+ ...listSkills(projectRoot).map((s) => ({ ...s, source: "project" })),
225
+ ...listSkills(globalRoot).map((s) => ({ ...s, source: "global" }))
226
+ ];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ const out = [];
229
+ for (const s of tagged) {
230
+ if (seen.has(s.name))
231
+ continue;
232
+ seen.add(s.name);
233
+ out.push(s);
234
+ }
235
+ return out;
236
+ }
237
+ function renderExistingSkillsBlock(cwd, charCap) {
238
+ const skills = listAllExistingSkills(cwd);
239
+ if (skills.length === 0) {
240
+ return {
241
+ mergeTargetNames: [],
242
+ block: "(no existing skills \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
243
+ };
244
+ }
245
+ const mergeTargetNames = skills.filter((s) => s.source === "project").map((s) => s.name);
246
+ let total = 0;
247
+ const out = [];
248
+ for (const s of skills) {
249
+ const tag = s.source === "project" ? "[project]" : "[global, read-only]";
250
+ const block = `--- existing skill ${tag}: ${s.name} ---
251
+ ${s.body}
252
+ `;
253
+ if (total + block.length > charCap) {
254
+ out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
255
+ break;
256
+ }
257
+ out.push(block);
258
+ total += block.length;
259
+ }
260
+ return { mergeTargetNames, block: out.join("\n") };
261
+ }
262
+
219
263
  // dist/src/skillify/skills-table.js
220
264
  import { randomUUID } from "node:crypto";
221
265
 
@@ -675,34 +719,9 @@ ${answer}
675
719
  }
676
720
  return out.join("\n");
677
721
  }
678
- function renderExistingSkillsBlock() {
679
- const skills = listSkills(resolveSkillsRoot(cfg.install, cfg.cwd));
680
- if (skills.length === 0) {
681
- return {
682
- names: [],
683
- block: "(no existing skills in this project \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
684
- };
685
- }
686
- let total = 0;
687
- const out = [];
688
- const names = [];
689
- for (const s of skills) {
690
- const block = `--- existing skill: ${s.name} ---
691
- ${s.body}
692
- `;
693
- if (total + block.length > EXISTING_SKILLS_CHAR_CAP) {
694
- out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
695
- break;
696
- }
697
- out.push(block);
698
- names.push(s.name);
699
- total += block.length;
700
- }
701
- return { names, block: out.join("\n") };
702
- }
703
722
  function buildPrompt(pairs) {
704
- const existing = renderExistingSkillsBlock();
705
- const mergeTargetsClause = existing.names.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.names.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.`;
723
+ 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.`;
706
725
  return [
707
726
  `You are a skill curator for the "${cfg.project}" project. You decide whether the recent`,
708
727
  `agent activity below contains a recurring, non-trivial pattern worth crystallizing as a`,
@@ -716,12 +735,14 @@ function buildPrompt(pairs) {
716
735
  ` merged body that incorporates the new evidence without exceeding ~3000 characters or`,
717
736
  ` covering unrelated domains.`,
718
737
  `- ${mergeTargetsClause}`,
719
- `- Do NOT reference skills outside this project (e.g. ones from ~/.claude/skills/). Only`,
720
- ` the project skills listed below count for MERGE.`,
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.`,
721
742
  `- Skill bodies should follow the existing style: short sections (When to use, Workflow,`,
722
743
  ` Anti-patterns), concrete commands and file paths drawn from the exchanges, no marketing.`,
723
744
  ``,
724
- `=== EXISTING PROJECT SKILLS ===`,
745
+ `=== EXISTING SKILLS ([project] are MERGE-eligible, [global] are reference only) ===`,
725
746
  existing.block,
726
747
  ``,
727
748
  `=== RECENT EXCHANGES (prompt + answer pairs, tool calls already stripped) ===`,
@@ -861,8 +882,8 @@ async function main() {
861
882
  trigger: verdict2.trigger,
862
883
  body: verdict2.body,
863
884
  version: result.version,
864
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
865
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
885
+ createdAt: result.createdAt,
886
+ updatedAt: result.updatedAt
866
887
  });
867
888
  wlog(`recorded to skills table: name=${verdict2.name} v${result.version}`);
868
889
  } catch (e) {
@@ -1071,7 +1071,7 @@ function extractLatestVersion(body) {
1071
1071
  return typeof v === "string" && v.length > 0 ? v : null;
1072
1072
  }
1073
1073
  function getInstalledVersion() {
1074
- return "0.7.19".length > 0 ? "0.7.19" : null;
1074
+ return "0.7.21".length > 0 ? "0.7.21" : null;
1075
1075
  }
1076
1076
  function isNewer(latest, current) {
1077
1077
  const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
@@ -166,7 +166,7 @@ function writeNewSkill(args) {
166
166
  ${args.body.trim()}
167
167
  `;
168
168
  writeFileSync(path, text);
169
- return { path, action: "created", version: 1 };
169
+ return { path, action: "created", version: 1, createdAt: now, updatedAt: now };
170
170
  }
171
171
  function mergeSkill(args) {
172
172
  assertValidSkillName(args.name);
@@ -195,7 +195,7 @@ function mergeSkill(args) {
195
195
  ${args.body.trim()}
196
196
  `;
197
197
  writeFileSync(path, text);
198
- return { path, action: "merged", version: fm.version };
198
+ return { path, action: "merged", version: fm.version, createdAt: fm.created_at, updatedAt: fm.updated_at };
199
199
  }
200
200
  function listSkills(skillsRoot) {
201
201
  if (!existsSync(skillsRoot))
@@ -216,6 +216,50 @@ function resolveSkillsRoot(install, cwd) {
216
216
  return join2(cwd, ".claude", "skills");
217
217
  }
218
218
 
219
+ // dist/src/skillify/existing-skills.js
220
+ function listAllExistingSkills(cwd) {
221
+ const projectRoot = resolveSkillsRoot("project", cwd);
222
+ const globalRoot = resolveSkillsRoot("global", cwd);
223
+ const tagged = [
224
+ ...listSkills(projectRoot).map((s) => ({ ...s, source: "project" })),
225
+ ...listSkills(globalRoot).map((s) => ({ ...s, source: "global" }))
226
+ ];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ const out = [];
229
+ for (const s of tagged) {
230
+ if (seen.has(s.name))
231
+ continue;
232
+ seen.add(s.name);
233
+ out.push(s);
234
+ }
235
+ return out;
236
+ }
237
+ function renderExistingSkillsBlock(cwd, charCap) {
238
+ const skills = listAllExistingSkills(cwd);
239
+ if (skills.length === 0) {
240
+ return {
241
+ mergeTargetNames: [],
242
+ block: "(no existing skills \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
243
+ };
244
+ }
245
+ const mergeTargetNames = skills.filter((s) => s.source === "project").map((s) => s.name);
246
+ let total = 0;
247
+ const out = [];
248
+ for (const s of skills) {
249
+ const tag = s.source === "project" ? "[project]" : "[global, read-only]";
250
+ const block = `--- existing skill ${tag}: ${s.name} ---
251
+ ${s.body}
252
+ `;
253
+ if (total + block.length > charCap) {
254
+ out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
255
+ break;
256
+ }
257
+ out.push(block);
258
+ total += block.length;
259
+ }
260
+ return { mergeTargetNames, block: out.join("\n") };
261
+ }
262
+
219
263
  // dist/src/skillify/skills-table.js
220
264
  import { randomUUID } from "node:crypto";
221
265
 
@@ -675,34 +719,9 @@ ${answer}
675
719
  }
676
720
  return out.join("\n");
677
721
  }
678
- function renderExistingSkillsBlock() {
679
- const skills = listSkills(resolveSkillsRoot(cfg.install, cfg.cwd));
680
- if (skills.length === 0) {
681
- return {
682
- names: [],
683
- block: "(no existing skills in this project \u2014 MERGE is NOT a valid choice; pick KEEP or SKIP only)"
684
- };
685
- }
686
- let total = 0;
687
- const out = [];
688
- const names = [];
689
- for (const s of skills) {
690
- const block = `--- existing skill: ${s.name} ---
691
- ${s.body}
692
- `;
693
- if (total + block.length > EXISTING_SKILLS_CHAR_CAP) {
694
- out.push(`[\u2026${skills.length - out.length} more existing skills omitted]`);
695
- break;
696
- }
697
- out.push(block);
698
- names.push(s.name);
699
- total += block.length;
700
- }
701
- return { names, block: out.join("\n") };
702
- }
703
722
  function buildPrompt(pairs) {
704
- const existing = renderExistingSkillsBlock();
705
- const mergeTargetsClause = existing.names.length > 0 ? `MERGE is allowed only if your "name" is EXACTLY one of: [${existing.names.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.`;
723
+ 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.`;
706
725
  return [
707
726
  `You are a skill curator for the "${cfg.project}" project. You decide whether the recent`,
708
727
  `agent activity below contains a recurring, non-trivial pattern worth crystallizing as a`,
@@ -716,12 +735,14 @@ function buildPrompt(pairs) {
716
735
  ` merged body that incorporates the new evidence without exceeding ~3000 characters or`,
717
736
  ` covering unrelated domains.`,
718
737
  `- ${mergeTargetsClause}`,
719
- `- Do NOT reference skills outside this project (e.g. ones from ~/.claude/skills/). Only`,
720
- ` the project skills listed below count for MERGE.`,
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.`,
721
742
  `- Skill bodies should follow the existing style: short sections (When to use, Workflow,`,
722
743
  ` Anti-patterns), concrete commands and file paths drawn from the exchanges, no marketing.`,
723
744
  ``,
724
- `=== EXISTING PROJECT SKILLS ===`,
745
+ `=== EXISTING SKILLS ([project] are MERGE-eligible, [global] are reference only) ===`,
725
746
  existing.block,
726
747
  ``,
727
748
  `=== RECENT EXCHANGES (prompt + answer pairs, tool calls already stripped) ===`,
@@ -861,8 +882,8 @@ async function main() {
861
882
  trigger: verdict2.trigger,
862
883
  body: verdict2.body,
863
884
  version: result.version,
864
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
865
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
885
+ createdAt: result.createdAt,
886
+ updatedAt: result.updatedAt
866
887
  });
867
888
  wlog(`recorded to skills table: name=${verdict2.name} v${result.version}`);
868
889
  } catch (e) {
@@ -52,5 +52,5 @@
52
52
  }
53
53
  }
54
54
  },
55
- "version": "0.7.19"
55
+ "version": "0.7.21"
56
56
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hivemind",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "type": "module",
5
5
  "description": "Hivemind — cloud-backed persistent shared memory for AI agents, powered by DeepLake",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeplake/hivemind",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
5
5
  "type": "module",
6
6
  "repository": {