Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
accc213
feat(skillify): add local-source for non-auth session discovery
efenocchi May 13, 2026
26bd8d5
feat(skillify): add mine-local CLI orchestrator
efenocchi May 13, 2026
a716d8b
feat(skillify): wire `skillify mine-local` subcommand
efenocchi May 13, 2026
13aff2f
Merge remote-tracking branch 'origin/main' into feat/skillify-mine-local
efenocchi May 13, 2026
34291f4
feat(skillify): centralize command spec + advertise mine-local everyw…
efenocchi May 13, 2026
a38c2d6
test(skillify): unit tests for mine-local pure helpers
efenocchi May 13, 2026
7e92b3e
feat(skillify): fan out mined skills to all installed agent skill roots
efenocchi May 13, 2026
c06e728
refactor(skillify): extract local manifest read/write to shared module
efenocchi May 13, 2026
da512c4
feat(skillify): surface local skill count to not-signed-in users
efenocchi May 13, 2026
553e3bc
feat(skillify): auto-trigger mine-local on first SessionStart for non…
efenocchi May 13, 2026
a54c400
feat(skillify,notifications,codex): user-visible mined-skills CTA on …
efenocchi May 16, 2026
beaa0ac
Merge remote-tracking branch 'origin/main' into feat/skillify-mine-local
efenocchi May 16, 2026
5acc021
test(coverage): cover localMinedNote branches in CC session-start + l…
efenocchi May 16, 2026
4074452
test(coverage): bring PR-aggregate coverage above 90% across all four…
efenocchi May 16, 2026
60f9342
test(skillify-cli): avoid throwing inside .catch arrow for mine-local…
efenocchi May 16, 2026
cbf0a87
fix(mine-local): prefer claude_code gate when installed + fail-fast o…
efenocchi May 16, 2026
3f004a2
test(coverage): cover remaining branch gaps on session-start hooks
efenocchi May 16, 2026
0682603
refactor(skillify-spec): route hivemind --help and hivemind skillify …
efenocchi May 18, 2026
bc28573
fix(skillify-spec): preserve column gap for long commands + restore N…
efenocchi May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,131 changes: 1,041 additions & 90 deletions bundle/cli.js

Large diffs are not rendered by default.

55 changes: 53 additions & 2 deletions claude-code/bundle/session-notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,12 @@ async function drainSessionStart(opts) {
try {
const state = readState();
const queue = readQueue();
const ctx = { agent: opts.agent, creds: opts.creds, state };
const ctx = {
agent: opts.agent,
creds: opts.creds,
state,
localSkillsCount: opts.localSkillsCount ?? null
};
const fromRules = evaluateRules("session_start", ctx);
const fromQueue = queue.queue;
const fromBackend = await fetchBackendNotifications(opts.creds);
Expand Down Expand Up @@ -485,17 +490,63 @@ var welcomeRule = {
}
};

// dist/src/notifications/rules/local-mined.js
var localMinedRule = {
id: "local-mined-surfaced",
trigger: "session_start",
evaluate({ creds, localSkillsCount }) {
if (creds?.token)
return null;
if (typeof localSkillsCount !== "number" || localSkillsCount <= 0)
return null;
const noun = localSkillsCount === 1 ? "skill" : "skills";
return {
id: "local-mined-surfaced",
severity: "info",
title: `\u{1F389} ${localSkillsCount} ${noun} mined from your local sessions`,
body: `Run 'hivemind login' to share new mining results with your team.`,
dedupKey: { count: localSkillsCount }
};
}
};

// dist/src/skillify/local-manifest.js
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
import { homedir as homedir6 } from "node:os";
import { dirname as dirname2, join as join6 } from "node:path";
var LOCAL_MANIFEST_PATH = join6(homedir6(), ".claude", "hivemind", "local-mined.json");
var LOCAL_MINE_LOCK_PATH = join6(homedir6(), ".claude", "hivemind", "local-mined.lock");
function readLocalManifest(path = LOCAL_MANIFEST_PATH) {
if (!existsSync2(path))
return null;
try {
return JSON.parse(readFileSync5(path, "utf-8"));
} catch {
return null;
}
}
function countLocalManifestEntries(path = LOCAL_MANIFEST_PATH) {
const m = readLocalManifest(path);
return Array.isArray(m?.entries) ? m.entries.length : 0;
}

// dist/src/hooks/session-notifications.js
var log8 = (msg) => log("session-notifications", msg);
registerRule(welcomeRule);
registerRule(localMinedRule);
async function main() {
if (process.env.HIVEMIND_WIKI_WORKER === "1")
return;
const input = await readStdin().catch(() => ({}));
const rawSessionId = typeof input?.session_id === "string" ? input.session_id.trim() : "";
const sessionId = rawSessionId.length > 0 ? rawSessionId : void 0;
const creds = loadCredentials();
await drainSessionStart({ agent: "claude-code", creds, sessionId });
let localSkillsCount = null;
try {
localSkillsCount = countLocalManifestEntries();
} catch {
}
await drainSessionStart({ agent: "claude-code", creds, sessionId, localSkillsCount });
}
main().catch((e) => {
log8(`fatal: ${e?.message ?? String(e)}`);
Expand Down
194 changes: 170 additions & 24 deletions claude-code/bundle/session-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ var init_index_marker_store = __esm({
});

// dist/src/hooks/session-start.js
import { fileURLToPath } from "node:url";
import { dirname as dirname4, join as join13 } from "node:path";
import { homedir as homedir9 } from "node:os";
import { fileURLToPath as fileURLToPath2 } from "node:url";
import { dirname as dirname6, join as join15 } from "node:path";
import { homedir as homedir11 } from "node:os";

// dist/src/commands/auth.js
import { execSync } from "node:child_process";
Expand Down Expand Up @@ -1354,9 +1354,165 @@ async function autoPullSkills(deps = {}) {
}
}

// dist/src/cli/skillify-spec.js
var SKILLIFY_COMMANDS = [
{ cmd: "hivemind skillify", desc: "show scope, team, install, per-project state" },
{ cmd: "hivemind skillify pull", desc: "sync project skills from the org table to local FS" },
{ cmd: "hivemind skillify pull --user <email>", desc: "only skills authored by that user" },
{ cmd: "hivemind skillify pull --users <a,b,c>", desc: "only skills from those authors" },
{ cmd: "hivemind skillify pull --all-users", desc: 'explicit "no author filter" (default)' },
{ cmd: "hivemind skillify pull --to <project|global>", desc: "install location (project=cwd/.claude/skills, global=~/.claude/skills)" },
{ cmd: "hivemind skillify pull --dry-run", desc: "preview without touching disk" },
{ cmd: "hivemind skillify pull --force", desc: "overwrite local files even if up-to-date (creates .bak)" },
{ cmd: "hivemind skillify pull <skill-name>", desc: "pull only that one skill (combines with --user)" },
{ cmd: "hivemind skillify unpull", desc: "remove every skill previously installed by pull" },
{ cmd: "hivemind skillify unpull --user <email>", desc: "remove only that author's pulls" },
{ cmd: "hivemind skillify unpull --not-mine", desc: "remove all pulls except your own" },
{ cmd: "hivemind skillify unpull --dry-run", desc: "preview without touching disk" },
{ cmd: "hivemind skillify scope <me|team|org>", desc: "sharing scope for newly mined skills" },
{ cmd: "hivemind skillify install <project|global>", desc: "default install location for new skills" },
{ cmd: "hivemind skillify promote <skill-name>", desc: "move a project skill to the global location" },
{ cmd: "hivemind skillify team add|remove|list <name>", desc: "manage team member list" },
{ cmd: "hivemind skillify mine-local", desc: "one-shot: mine skills from local sessions (no auth needed)" },
{ cmd: "hivemind skillify mine-local --n <num|all>", desc: "how many sessions to mine (default: 8)" },
{ cmd: "hivemind skillify mine-local --force", desc: "re-run even if the manifest sentinel exists" },
{ cmd: "hivemind skillify mine-local --dry-run", desc: "stop before calling the LLM gate" }
];
function renderSkillifyCommands() {
const maxLen = Math.max(...SKILLIFY_COMMANDS.map((c) => c.cmd.length));
return SKILLIFY_COMMANDS.map((c) => `- ${c.cmd.padEnd(maxLen + 2)} \u2014 ${c.desc}`).join("\n");
}

// dist/src/skillify/local-manifest.js
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
import { homedir as homedir9 } from "node:os";
import { dirname as dirname4, join as join13 } from "node:path";
var LOCAL_MANIFEST_PATH = join13(homedir9(), ".claude", "hivemind", "local-mined.json");
var LOCAL_MINE_LOCK_PATH = join13(homedir9(), ".claude", "hivemind", "local-mined.lock");
function readLocalManifest(path = LOCAL_MANIFEST_PATH) {
if (!existsSync9(path))
return null;
try {
return JSON.parse(readFileSync8(path, "utf-8"));
} catch {
return null;
}
}
function countLocalManifestEntries(path = LOCAL_MANIFEST_PATH) {
const m = readLocalManifest(path);
return Array.isArray(m?.entries) ? m.entries.length : 0;
}

// dist/src/skillify/spawn-mine-local-worker.js
import { execFileSync, spawn as spawn2 } from "node:child_process";
import { closeSync, existsSync as existsSync10, mkdirSync as mkdirSync8, openSync, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync4 } from "node:fs";
import { homedir as homedir10 } from "node:os";
import { dirname as dirname5, join as join14 } from "node:path";
import { fileURLToPath } from "node:url";
var HOME = homedir10();
var HIVEMIND_DIR = join14(HOME, ".claude", "hivemind");
var LOG_PATH = join14(HOME, ".claude", "hooks", "mine-local.log");
var CLAUDE_PROJECTS_DIR = join14(HOME, ".claude", "projects");
var LOCK_STALE_MS = 15 * 60 * 1e3;
function findBundledCliPath() {
try {
const thisDir = dirname5(fileURLToPath(import.meta.url));
const cliPath = join14(thisDir, "..", "..", "bundle", "cli.js");
return existsSync10(cliPath) ? cliPath : null;
} catch {
return null;
}
}
function findHivemindLauncher() {
const bundled = findBundledCliPath();
if (bundled)
return { kind: "node-script", path: bundled };
try {
const out = execFileSync("which", ["hivemind"], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"]
});
const bin = out.trim();
return bin ? { kind: "bin", path: bin } : null;
} catch {
return null;
}
}
function hasLocalClaudeSessions() {
if (!existsSync10(CLAUDE_PROJECTS_DIR))
return false;
let subdirs;
try {
subdirs = readdirSync2(CLAUDE_PROJECTS_DIR);
} catch {
return false;
}
for (const sub of subdirs) {
let files;
try {
files = readdirSync2(join14(CLAUDE_PROJECTS_DIR, sub));
} catch {
continue;
}
if (files.some((f) => f.endsWith(".jsonl")))
return true;
}
return false;
}
function maybeAutoMineLocal() {
if (existsSync10(LOCAL_MANIFEST_PATH))
return { triggered: false, reason: "manifest-exists" };
if (existsSync10(LOCAL_MINE_LOCK_PATH)) {
let stale = false;
try {
const stats = statSync2(LOCAL_MINE_LOCK_PATH);
stale = Date.now() - stats.mtimeMs > LOCK_STALE_MS;
} catch {
}
if (!stale)
return { triggered: false, reason: "lock-exists" };
try {
unlinkSync4(LOCAL_MINE_LOCK_PATH);
} catch {
return { triggered: false, reason: "lock-exists" };
}
}
if (!hasLocalClaudeSessions())
return { triggered: false, reason: "no-claude-sessions" };
const launcher = findHivemindLauncher();
if (!launcher)
return { triggered: false, reason: "no-hivemind-bin" };
try {
mkdirSync8(HIVEMIND_DIR, { recursive: true });
const fd = openSync(LOCAL_MINE_LOCK_PATH, "wx");
closeSync(fd);
} catch {
return { triggered: false, reason: "lock-acquire-failed" };
}
try {
mkdirSync8(join14(HOME, ".claude", "hooks"), { recursive: true });
const out = openSync(LOG_PATH, "a");
const [cmd, args] = launcher.kind === "node-script" ? [process.execPath, [launcher.path, "skillify", "mine-local"]] : [launcher.path, ["skillify", "mine-local"]];
const child = spawn2(cmd, args, {
detached: true,
stdio: ["ignore", out, out],
env: process.env
});
closeSync(out);
child.unref();
return { triggered: true };
} catch {
try {
unlinkSync4(LOCAL_MINE_LOCK_PATH);
} catch {
}
return { triggered: false, reason: "spawn-failed" };
}
}

// dist/src/hooks/session-start.js
var log5 = (msg) => log("session-start", msg);
var __bundleDir = dirname4(fileURLToPath(import.meta.url));
var __bundleDir = dirname6(fileURLToPath2(import.meta.url));
var context = `DEEPLAKE MEMORY: You have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:

1. Your built-in memory (~/.claude/) \u2014 personal per-project notes
Expand Down Expand Up @@ -1389,31 +1545,15 @@ Organization management \u2014 each argument is SEPARATE (do NOT quote subcomman
- hivemind remove <user-id> \u2014 remove member

Skill management (mine + share reusable Claude skills across the org):
- hivemind skillify \u2014 show scope, team, install, per-project state
- hivemind skillify pull \u2014 sync project skills from the org table to local FS
- hivemind skillify pull --user <email> \u2014 only skills authored by that user
- hivemind skillify pull --users <a,b,c> \u2014 only skills from those authors
- hivemind skillify pull --all-users \u2014 explicit "no author filter" (default)
- hivemind skillify pull --to <project|global> \u2014 install location (project=cwd/.claude/skills, global=~/.claude/skills)
- hivemind skillify pull --dry-run \u2014 preview without touching disk
- hivemind skillify pull --force \u2014 overwrite local files even if up-to-date (creates .bak)
- hivemind skillify pull <skill-name> \u2014 pull only that one skill (combines with --user)
- hivemind skillify unpull \u2014 remove every skill previously installed by pull
- hivemind skillify unpull --user <email> \u2014 remove only that author's pulls
- hivemind skillify unpull --not-mine \u2014 remove all pulls except your own
- hivemind skillify unpull --dry-run \u2014 preview without touching disk
- hivemind skillify scope <me|team> \u2014 sharing scope for newly mined skills
- hivemind skillify install <project|global> \u2014 default install location for new skills
- hivemind skillify promote <skill-name> \u2014 move a project skill to the global location
- hivemind skillify team add|remove|list <name> \u2014 manage team member list
${renderSkillifyCommands()}

IMPORTANT: Only use bash commands (cat, ls, grep, echo, jq, head, tail, etc.) to interact with ~/.deeplake/memory/. Do NOT use python, python3, node, curl, or other interpreters \u2014 they are not available in the memory filesystem. Avoid bash brace expansions like \`{1..10}\` (not fully supported); spell out paths explicitly. Bash output is capped at 10MB total \u2014 avoid \`for f in *.json; do cat $f\` style loops on the whole sessions dir.

LIMITS: Do NOT spawn subagents to read deeplake memory. If a file returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.

Debugging: Set HIVEMIND_DEBUG=1 to enable verbose logging to ~/.deeplake/hook-debug.log`;
var HOME = homedir9();
var { log: wikiLog } = makeWikiLogger(join13(HOME, ".claude", "hooks"));
var HOME2 = homedir11();
var { log: wikiLog } = makeWikiLogger(join15(HOME2, ".claude", "hooks"));
async function createPlaceholder(api, table, sessionId, cwd, userName, orgName, workspaceId, pluginVersion) {
const summaryPath = `/summaries/${userName}/${sessionId}.md`;
const existing = await api.query(`SELECT path FROM "${table}" WHERE path = '${sqlStr(summaryPath)}' LIMIT 1`);
Expand Down Expand Up @@ -1445,6 +1585,8 @@ async function main() {
let creds = loadCredentials();
if (!creds?.token) {
log5("no credentials found \u2014 run /hivemind:login to authenticate");
const auto = maybeAutoMineLocal();
log5(`auto-mine: ${auto.triggered ? "triggered (background)" : `skipped (${auto.reason})`}`);
} else {
log5(`credentials loaded: org=${creds.orgName ?? creds.orgId}`);
if (creds.token && !creds.userName) {
Expand Down Expand Up @@ -1488,11 +1630,15 @@ async function main() {

\u2705 Hivemind v${current}` : "";
const resolvedContext = context;
const localMined = countLocalManifestEntries();
const localMinedNote = localMined > 0 ? `

${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.` : "";
const additionalContext = creds?.token ? `${resolvedContext}

Logged in to Deeplake as org: ${creds.orgName ?? creds.orgId} (workspace: ${creds.workspaceId ?? "default"})${updateNotice}` : `${resolvedContext}

\u26A0\uFE0F Not logged in to Deeplake. Memory search will not work. Ask the user to run /hivemind:login to authenticate.${updateNotice}`;
\u26A0\uFE0F Not logged in to Deeplake. Memory search will not work. Ask the user to run /hivemind:login to authenticate.${localMinedNote}${updateNotice}`;
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
Expand Down
33 changes: 33 additions & 0 deletions claude-code/skills/hivemind-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,39 @@ The auth command path is injected at session start. Use the exact path from the
- `node "<AUTH_CMD>" remove <user-id>` — remove member
- `node "<AUTH_CMD>" --help` — show all commands

## Skill Management (skillify)

Hivemind can mine reusable skills from agent session logs and share them across your team. Each argument is separate — do NOT quote subcommands together.

- `hivemind skillify` — show current scope, team, install location, per-project state
- `hivemind skillify pull` — sync project skills from the org table to local FS
- `hivemind skillify pull --user <email>` — only skills authored by that user
- `hivemind skillify pull --users <a,b,c>` — multiple authors (CSV)
- `hivemind skillify pull --all-users` — explicit "no author filter" (default)
- `hivemind skillify pull --to <project|global>` — install location (project=cwd/.claude/skills, global=~/.claude/skills)
- `hivemind skillify pull --dry-run` — preview without touching disk
- `hivemind skillify pull --force` — overwrite local files even if up-to-date (creates .bak)
- `hivemind skillify pull <skill-name>` — pull only that one skill (combines with --user)
- `hivemind skillify unpull` — remove every skill previously installed by pull
- `hivemind skillify unpull --user <email>` — remove only that author's pulls
- `hivemind skillify unpull --not-mine` — remove all pulls except your own
- `hivemind skillify unpull --dry-run` — preview without touching disk
- `hivemind skillify scope <me|team>` — sharing scope for newly mined skills
- `hivemind skillify install <project|global>` — default install location for new skills
- `hivemind skillify promote <skill-name>` — move a project skill to the global location
- `hivemind skillify team add|remove|list <username>` — manage team member list
- `hivemind skillify mine-local` — one-shot: mine skills from local sessions, no auth needed

## Embeddings (semantic memory search)

Opt-in, persisted in `~/.deeplake/config.json`.

- `hivemind embeddings install` — download deps (~600MB), symlink agents, set enabled:true
- `hivemind embeddings enable` — flip enabled:true (run install first if deps missing)
- `hivemind embeddings disable` — flip enabled:false + SIGTERM daemon (deps stay on disk)
- `hivemind embeddings uninstall [--prune]` — remove agent symlinks + disable; --prune wipes deps too
- `hivemind embeddings status` — show config + deps + per-agent link state

## Important: Bash Only

Only use bash commands (cat, ls, grep, echo, jq, head, tail, sed, awk, etc.) to interact with `~/.deeplake/memory/`. Do NOT use python, python3, node, curl, or other interpreters — they are not available in the memory filesystem. If a task seems to require Python, rewrite it using bash tools (e.g., `cat file.json | jq 'keys | length'`).
Expand Down
Loading
Loading