@deeplake/hivemind 0.7.25 → 0.7.27

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.
@@ -26,12 +26,14 @@
26
26
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
27
27
  import {
28
28
  readFileSync, existsSync, appendFileSync, mkdirSync, writeFileSync,
29
- openSync, closeSync, renameSync, constants as fsConstants,
29
+ openSync, closeSync, renameSync, readdirSync, statSync, unlinkSync,
30
+ constants as fsConstants,
30
31
  } from "node:fs";
31
32
  import { homedir, tmpdir } from "node:os";
32
33
  import { join, dirname } from "node:path";
34
+ import { fileURLToPath } from "node:url";
33
35
  import { connect } from "node:net";
34
- import { spawn, spawnSync, execSync } from "node:child_process";
36
+ import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
35
37
  import { createHash } from "node:crypto";
36
38
 
37
39
  // ---------- diagnostic logging --------------------------------------------------
@@ -691,6 +693,145 @@ function textResult(text: string) {
691
693
 
692
694
  // ---------- main extension -----------------------------------------------------
693
695
 
696
+ // MIRROR of src/skillify/local-manifest.ts countLocalManifestEntries.
697
+ //
698
+ // pi's extension cannot import from src/. Read the manifest inline so the
699
+ // SessionStart hook can surface "you have N local skills" when the user
700
+ // isn't signed in. Returns 0 on any error (missing file, parse failure)
701
+ // so the message is silently omitted in those cases.
702
+ const PI_LOCAL_MANIFEST_PATH = join(homedir(), ".claude", "hivemind", "local-mined.json");
703
+
704
+ function piCountLocalManifestEntries(): number {
705
+ try {
706
+ if (!existsSync(PI_LOCAL_MANIFEST_PATH)) return 0;
707
+ const data = JSON.parse(readFileSync(PI_LOCAL_MANIFEST_PATH, "utf-8"));
708
+ return Array.isArray(data?.entries) ? data.entries.length : 0;
709
+ } catch {
710
+ return 0;
711
+ }
712
+ }
713
+
714
+ // MIRROR of src/skillify/spawn-mine-local-worker.ts maybeAutoMineLocal().
715
+ // First-impression bootstrap: when an unauthenticated pi session sees
716
+ // past Claude Code transcripts but no local mining manifest, spawn the
717
+ // `hivemind` CLI in the background. THIS session sees the standard
718
+ // "not logged in" message; the NEXT pi session sees the mined-count
719
+ // CTA from piCountLocalManifestEntries above.
720
+ const PI_LOCAL_MINE_LOCK_PATH = join(homedir(), ".claude", "hivemind", "local-mined.lock");
721
+ const PI_AUTO_MINE_LOG_PATH = join(homedir(), ".claude", "hooks", "mine-local.log");
722
+ const PI_CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
723
+ const PI_LOCK_STALE_MS = 15 * 60 * 1000;
724
+
725
+ function piMaybeAutoMineLocal(): boolean {
726
+ try {
727
+ if (existsSync(PI_LOCAL_MANIFEST_PATH)) return false;
728
+ if (existsSync(PI_LOCAL_MINE_LOCK_PATH)) {
729
+ let stale = false;
730
+ try {
731
+ const stats = statSync(PI_LOCAL_MINE_LOCK_PATH);
732
+ stale = Date.now() - stats.mtimeMs > PI_LOCK_STALE_MS;
733
+ } catch { /* not stale */ }
734
+ if (!stale) return false;
735
+ try { unlinkSync(PI_LOCAL_MINE_LOCK_PATH); } catch { return false; }
736
+ }
737
+ if (!existsSync(PI_CLAUDE_PROJECTS_DIR)) return false;
738
+ // cheap existence-of-jsonl check (1-level walk)
739
+ let hasJsonl = false;
740
+ try {
741
+ for (const sub of readdirSync(PI_CLAUDE_PROJECTS_DIR)) {
742
+ let files: string[] = [];
743
+ try { files = readdirSync(join(PI_CLAUDE_PROJECTS_DIR, sub)); } catch { continue; }
744
+ if (files.some((f: string) => f.endsWith(".jsonl"))) { hasJsonl = true; break; }
745
+ }
746
+ } catch { return false; }
747
+ if (!hasJsonl) return false;
748
+
749
+ // Prefer the sibling bundled CLI (same plugin install as this hook
750
+ // extension → guaranteed to know `mine-local`). Fall back to PATH for
751
+ // unusual install layouts. Mirrors findHivemindLauncher() in
752
+ // src/skillify/spawn-mine-local-worker.ts.
753
+ let launcher: { kind: "node-script" | "bin"; path: string } | null = null;
754
+ try {
755
+ const thisDir = dirname(fileURLToPath(import.meta.url));
756
+ const cliPath = join(thisDir, "..", "..", "bundle", "cli.js");
757
+ if (existsSync(cliPath)) launcher = { kind: "node-script", path: cliPath };
758
+ } catch { /* fall through to which */ }
759
+ if (!launcher) {
760
+ try {
761
+ const out = execFileSync("which", ["hivemind"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
762
+ const bin = String(out).trim();
763
+ if (bin) launcher = { kind: "bin", path: bin };
764
+ } catch { return false; }
765
+ }
766
+ if (!launcher) return false;
767
+
768
+ // Acquire the lock (exclusive create); if another pi session got
769
+ // here first, skip.
770
+ try {
771
+ mkdirSync(dirname(PI_LOCAL_MINE_LOCK_PATH), { recursive: true });
772
+ const fd = openSync(PI_LOCAL_MINE_LOCK_PATH, "wx");
773
+ closeSync(fd);
774
+ } catch { return false; }
775
+
776
+ try {
777
+ mkdirSync(dirname(PI_AUTO_MINE_LOG_PATH), { recursive: true });
778
+ const out = openSync(PI_AUTO_MINE_LOG_PATH, "a");
779
+ const [cmd, args]: [string, string[]] = launcher.kind === "node-script"
780
+ ? [process.execPath, [launcher.path, "skillify", "mine-local"]]
781
+ : [launcher.path, ["skillify", "mine-local"]];
782
+ const child = spawn(cmd, args, {
783
+ detached: true,
784
+ stdio: ["ignore", out, out],
785
+ env: process.env,
786
+ });
787
+ closeSync(out);
788
+ child.unref();
789
+ return true;
790
+ } catch {
791
+ try { unlinkSync(PI_LOCAL_MINE_LOCK_PATH); } catch { /* best-effort */ }
792
+ return false;
793
+ }
794
+ } catch { return false; }
795
+ }
796
+
797
+ // MIRROR of src/cli/skillify-spec.ts SKILLIFY_COMMANDS.
798
+ //
799
+ // pi extensions are shipped as a single self-contained .ts file loaded by
800
+ // pi's runtime, so they cannot import from src/. This array is hand-kept
801
+ // in sync with the canonical spec; the agents-deployment-session-start-injection
802
+ // skill documents the rule and there is a vitest drift-scan that fails the
803
+ // build if the two lists diverge.
804
+ const PI_SKILLIFY_COMMANDS: { cmd: string; desc: string }[] = [
805
+ { cmd: "hivemind skillify", desc: "show scope, team, install, per-project state" },
806
+ { cmd: "hivemind skillify pull", desc: "sync project skills from the org table to local FS" },
807
+ { cmd: "hivemind skillify pull --user <email>", desc: "only skills authored by that user" },
808
+ { cmd: "hivemind skillify pull --users <a,b,c>", desc: "only skills from those authors" },
809
+ { cmd: "hivemind skillify pull --all-users", desc: 'explicit "no author filter" (default)' },
810
+ { cmd: "hivemind skillify pull --to <project|global>", desc: "install location (project=cwd/.claude/skills, global=~/.claude/skills)" },
811
+ { cmd: "hivemind skillify pull --dry-run", desc: "preview without touching disk" },
812
+ { cmd: "hivemind skillify pull --force", desc: "overwrite local files even if up-to-date (creates .bak)" },
813
+ { cmd: "hivemind skillify pull <skill-name>", desc: "pull only that one skill (combines with --user)" },
814
+ { cmd: "hivemind skillify unpull", desc: "remove every skill previously installed by pull" },
815
+ { cmd: "hivemind skillify unpull --user <email>", desc: "remove only that author's pulls" },
816
+ { cmd: "hivemind skillify unpull --not-mine", desc: "remove all pulls except your own" },
817
+ { cmd: "hivemind skillify unpull --dry-run", desc: "preview without touching disk" },
818
+ { cmd: "hivemind skillify scope <me|team|org>", desc: "sharing scope for newly mined skills" },
819
+ { cmd: "hivemind skillify install <project|global>", desc: "default install location for new skills" },
820
+ { cmd: "hivemind skillify promote <skill-name>", desc: "move a project skill to the global location" },
821
+ { cmd: "hivemind skillify team add|remove|list <name>", desc: "manage team member list" },
822
+ { cmd: "hivemind skillify mine-local", desc: "one-shot: mine skills from local sessions (no auth needed)" },
823
+ { cmd: "hivemind skillify mine-local --n <num|all>", desc: "how many sessions to mine (default: 8)" },
824
+ { cmd: "hivemind skillify mine-local --force", desc: "re-run even if the manifest sentinel exists" },
825
+ { cmd: "hivemind skillify mine-local --dry-run", desc: "stop before calling the LLM gate" },
826
+ ];
827
+
828
+ function piRenderSkillifyCommands(): string {
829
+ const maxLen = Math.max(...PI_SKILLIFY_COMMANDS.map(c => c.cmd.length));
830
+ return PI_SKILLIFY_COMMANDS
831
+ .map(c => `- ${c.cmd.padEnd(maxLen + 2)} — ${c.desc}`)
832
+ .join("\n");
833
+ }
834
+
694
835
  const CONTEXT_PREAMBLE = `DEEPLAKE MEMORY: Persistent memory at ~/.deeplake/memory/ shared across sessions, users, and agents in your org.
695
836
 
696
837
  Three hivemind tools are registered:
@@ -712,22 +853,7 @@ Organization management — each argument is SEPARATE (do NOT quote subcommands
712
853
  - hivemind remove <user-id> — remove member
713
854
 
714
855
  SKILLS (skillify) — mine + share reusable skills across the org. Run these in a terminal (or via shell if available):
715
- - hivemind skillify — show scope/team/install + per-project state
716
- - hivemind skillify pull — sync project skills from the org table
717
- - hivemind skillify pull --user <email> — only that author's skills
718
- - hivemind skillify pull --users a,b,c — multiple authors (CSV)
719
- - hivemind skillify pull --all-users — explicit "no author filter"
720
- - hivemind skillify pull --to project|global — install location
721
- - hivemind skillify pull --dry-run — preview only
722
- - hivemind skillify pull --force — overwrite local (creates .bak)
723
- - hivemind skillify pull <skill-name> — pull only that skill (combines with --user)
724
- - hivemind skillify unpull — remove every skill previously installed by pull
725
- - hivemind skillify unpull --user <email> — remove only that author's pulls
726
- - hivemind skillify unpull --not-mine — remove all pulls except your own
727
- - hivemind skillify unpull --dry-run — preview without touching disk
728
- - hivemind skillify scope <me|team> — sharing scope for new skills
729
- - hivemind skillify install <project|global> — default install location
730
- - hivemind skillify team add|remove|list <name> — manage team list`;
856
+ ${piRenderSkillifyCommands()}`;
731
857
 
732
858
  export default function hivemindExtension(pi: ExtensionAPI): void {
733
859
  const captureEnabled = process.env.HIVEMIND_CAPTURE !== "false";
@@ -913,10 +1039,22 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
913
1039
  // per-agent symlink fan-out all live in the worker — no inline
914
1040
  // duplicate maintained here.
915
1041
  if (creds) runAutopullWorker();
1042
+ else {
1043
+ // First-impression bootstrap: auto-run `hivemind skillify mine-local`
1044
+ // when the user isn't signed in and has Claude Code transcripts on
1045
+ // disk. THIS session sees nothing different; the NEXT pi session
1046
+ // surfaces the mined count + sign-in CTA below.
1047
+ const triggered = piMaybeAutoMineLocal();
1048
+ logHm(`auto-mine: ${triggered ? "triggered" : "skipped"}`);
1049
+ }
916
1050
 
1051
+ const localMined = piCountLocalManifestEntries();
1052
+ const localMinedNote = localMined > 0
1053
+ ? `\n${localMined} local skill${localMined === 1 ? "" : "s"} from past 'hivemind skillify mine-local' run(s) live in ~/.claude/skills/. Run 'hivemind login' to start sharing new mining results with your team.`
1054
+ : "";
917
1055
  const additional = creds
918
1056
  ? `${CONTEXT_PREAMBLE}\nLogged in to Deeplake as org: ${creds.orgName ?? creds.orgId} (workspace: ${creds.workspaceId}).`
919
- : `${CONTEXT_PREAMBLE}\nNot logged in to Deeplake. Run \`hivemind login\` to authenticate.`;
1057
+ : `${CONTEXT_PREAMBLE}\nNot logged in to Deeplake. Run \`hivemind login\` to authenticate.${localMinedNote}`;
920
1058
  return { additionalContext: additional };
921
1059
  });
922
1060