@deeplake/hivemind 0.7.16 → 0.7.18
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bundle/cli.js +294 -105
- package/codex/bundle/session-start.js +1154 -25
- package/codex/bundle/{skilify-worker.js → skillify-worker.js} +65 -30
- package/codex/bundle/stop.js +105 -68
- package/cursor/bundle/capture.js +84 -47
- package/cursor/bundle/session-end.js +82 -45
- package/cursor/bundle/session-start.js +633 -25
- package/cursor/bundle/{skilify-worker.js → skillify-worker.js} +65 -30
- package/hermes/bundle/capture.js +84 -47
- package/hermes/bundle/session-end.js +82 -45
- package/hermes/bundle/session-start.js +631 -23
- package/hermes/bundle/{skilify-worker.js → skillify-worker.js} +65 -30
- package/openclaw/dist/index.js +47 -30
- package/openclaw/dist/{skilify-worker.js → skillify-worker.js} +65 -30
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/openclaw/skills/SKILL.md +19 -19
- package/package.json +1 -1
- package/pi/extension-source/hivemind.ts +94 -45
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// dist/src/
|
|
4
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as
|
|
5
|
-
import { join as
|
|
3
|
+
// dist/src/skillify/skillify-worker.js
|
|
4
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, appendFileSync as appendFileSync2, rmSync } from "node:fs";
|
|
5
|
+
import { join as join6 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// dist/src/utils/debug.js
|
|
8
8
|
import { appendFileSync } from "node:fs";
|
|
@@ -29,7 +29,7 @@ function deeplakeClientHeader() {
|
|
|
29
29
|
return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// dist/src/
|
|
32
|
+
// dist/src/skillify/extractors/index.js
|
|
33
33
|
function extractPairs(rows) {
|
|
34
34
|
const pairs = [];
|
|
35
35
|
let pendingPrompt = null;
|
|
@@ -60,7 +60,7 @@ function extractPairs(rows) {
|
|
|
60
60
|
return pairs;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// dist/src/
|
|
63
|
+
// dist/src/skillify/skill-writer.js
|
|
64
64
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
65
65
|
import { homedir as homedir2 } from "node:os";
|
|
66
66
|
import { join as join2 } from "node:path";
|
|
@@ -216,7 +216,7 @@ function resolveSkillsRoot(install, cwd) {
|
|
|
216
216
|
return join2(cwd, ".claude", "skills");
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
// dist/src/
|
|
219
|
+
// dist/src/skillify/skills-table.js
|
|
220
220
|
import { randomUUID } from "node:crypto";
|
|
221
221
|
|
|
222
222
|
// dist/src/utils/sql.js
|
|
@@ -227,7 +227,7 @@ function sqlIdent(name) {
|
|
|
227
227
|
return name;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
// dist/src/
|
|
230
|
+
// dist/src/skillify/skills-table.js
|
|
231
231
|
function createSkillsTableSql(tableName) {
|
|
232
232
|
const safe = sqlIdent(tableName);
|
|
233
233
|
return `CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`;
|
|
@@ -256,7 +256,7 @@ async function insertSkillRow(args) {
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
// dist/src/
|
|
259
|
+
// dist/src/skillify/gate-parser.js
|
|
260
260
|
function extractJsonBlock(s) {
|
|
261
261
|
const trimmed = s.trim();
|
|
262
262
|
if (!trimmed)
|
|
@@ -294,7 +294,7 @@ function parseVerdict(raw) {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
// dist/src/
|
|
297
|
+
// dist/src/skillify/gate-runner.js
|
|
298
298
|
import { execFileSync } from "node:child_process";
|
|
299
299
|
import { existsSync as existsSync2 } from "node:fs";
|
|
300
300
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -403,28 +403,61 @@ function runGate(opts) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
// dist/src/
|
|
407
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, writeSync, mkdirSync as mkdirSync2, renameSync, existsSync as
|
|
406
|
+
// dist/src/skillify/state.js
|
|
407
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, writeSync, mkdirSync as mkdirSync2, renameSync as renameSync2, existsSync as existsSync4, unlinkSync, openSync, closeSync } from "node:fs";
|
|
408
408
|
import { execSync } from "node:child_process";
|
|
409
|
-
import { homedir as
|
|
409
|
+
import { homedir as homedir5 } from "node:os";
|
|
410
410
|
import { createHash } from "node:crypto";
|
|
411
|
-
import { join as
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
import { join as join5, basename } from "node:path";
|
|
412
|
+
|
|
413
|
+
// dist/src/skillify/legacy-migration.js
|
|
414
|
+
import { existsSync as existsSync3, renameSync } from "node:fs";
|
|
415
|
+
import { homedir as homedir4 } from "node:os";
|
|
416
|
+
import { join as join4 } from "node:path";
|
|
417
|
+
var dlog = (msg) => log("skillify-migrate", msg);
|
|
418
|
+
var attempted = false;
|
|
419
|
+
function migrateLegacyStateDir() {
|
|
420
|
+
if (attempted)
|
|
421
|
+
return;
|
|
422
|
+
attempted = true;
|
|
423
|
+
const root = join4(homedir4(), ".deeplake", "state");
|
|
424
|
+
const legacy = join4(root, "skilify");
|
|
425
|
+
const current = join4(root, "skillify");
|
|
426
|
+
if (!existsSync3(legacy))
|
|
427
|
+
return;
|
|
428
|
+
if (existsSync3(current))
|
|
429
|
+
return;
|
|
430
|
+
try {
|
|
431
|
+
renameSync(legacy, current);
|
|
432
|
+
dlog(`migrated ${legacy} -> ${current}`);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
const code = err.code;
|
|
435
|
+
if (code === "EXDEV" || code === "EPERM") {
|
|
436
|
+
dlog(`migration failed (${code}); leaving legacy dir in place`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// dist/src/skillify/state.js
|
|
444
|
+
var dlog2 = (msg) => log("skillify-state", msg);
|
|
445
|
+
var STATE_DIR = join5(homedir5(), ".deeplake", "state", "skillify");
|
|
414
446
|
var YIELD_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
415
447
|
var TRIGGER_THRESHOLD = (() => {
|
|
416
|
-
const n = Number(process.env.
|
|
448
|
+
const n = Number(process.env.HIVEMIND_SKILLIFY_EVERY_N_TURNS ?? "");
|
|
417
449
|
return Number.isInteger(n) && n > 0 ? n : 20;
|
|
418
450
|
})();
|
|
419
451
|
function statePath(projectKey) {
|
|
420
|
-
return
|
|
452
|
+
return join5(STATE_DIR, `${projectKey}.json`);
|
|
421
453
|
}
|
|
422
454
|
function lockPath(projectKey) {
|
|
423
|
-
return
|
|
455
|
+
return join5(STATE_DIR, `${projectKey}.lock`);
|
|
424
456
|
}
|
|
425
457
|
function readState(projectKey) {
|
|
458
|
+
migrateLegacyStateDir();
|
|
426
459
|
const p = statePath(projectKey);
|
|
427
|
-
if (!
|
|
460
|
+
if (!existsSync4(p))
|
|
428
461
|
return null;
|
|
429
462
|
try {
|
|
430
463
|
return JSON.parse(readFileSync2(p, "utf-8"));
|
|
@@ -433,13 +466,15 @@ function readState(projectKey) {
|
|
|
433
466
|
}
|
|
434
467
|
}
|
|
435
468
|
function writeState(projectKey, state) {
|
|
469
|
+
migrateLegacyStateDir();
|
|
436
470
|
mkdirSync2(STATE_DIR, { recursive: true });
|
|
437
471
|
const p = statePath(projectKey);
|
|
438
472
|
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
439
473
|
writeFileSync2(tmp, JSON.stringify(state, null, 2));
|
|
440
|
-
|
|
474
|
+
renameSync2(tmp, p);
|
|
441
475
|
}
|
|
442
476
|
function withRmwLock(projectKey, fn) {
|
|
477
|
+
migrateLegacyStateDir();
|
|
443
478
|
mkdirSync2(STATE_DIR, { recursive: true });
|
|
444
479
|
const rmw = lockPath(projectKey) + ".rmw";
|
|
445
480
|
const deadline = Date.now() + 2e3;
|
|
@@ -451,11 +486,11 @@ function withRmwLock(projectKey, fn) {
|
|
|
451
486
|
if (e.code !== "EEXIST")
|
|
452
487
|
throw e;
|
|
453
488
|
if (Date.now() > deadline) {
|
|
454
|
-
|
|
489
|
+
dlog2(`rmw lock deadline exceeded for ${projectKey}, reclaiming stale lock`);
|
|
455
490
|
try {
|
|
456
491
|
unlinkSync(rmw);
|
|
457
492
|
} catch (unlinkErr) {
|
|
458
|
-
|
|
493
|
+
dlog2(`stale rmw lock unlink failed for ${projectKey}: ${unlinkErr.message}`);
|
|
459
494
|
}
|
|
460
495
|
continue;
|
|
461
496
|
}
|
|
@@ -469,7 +504,7 @@ function withRmwLock(projectKey, fn) {
|
|
|
469
504
|
try {
|
|
470
505
|
unlinkSync(rmw);
|
|
471
506
|
} catch (unlinkErr) {
|
|
472
|
-
|
|
507
|
+
dlog2(`rmw lock cleanup failed for ${projectKey}: ${unlinkErr.message}`);
|
|
473
508
|
}
|
|
474
509
|
}
|
|
475
510
|
}
|
|
@@ -509,18 +544,18 @@ function releaseWorkerLock(projectKey) {
|
|
|
509
544
|
}
|
|
510
545
|
}
|
|
511
546
|
|
|
512
|
-
// dist/src/
|
|
547
|
+
// dist/src/skillify/skillify-worker.js
|
|
513
548
|
var cfg = JSON.parse(readFileSync3(process.argv[2], "utf-8"));
|
|
514
549
|
var tmpDir = cfg.tmpDir;
|
|
515
|
-
var verdictPath =
|
|
516
|
-
var promptPath =
|
|
550
|
+
var verdictPath = join6(tmpDir, "verdict.json");
|
|
551
|
+
var promptPath = join6(tmpDir, "prompt.txt");
|
|
517
552
|
var SESSIONS_TO_MINE = 10;
|
|
518
553
|
var PAIR_CHAR_CAP = 2e3;
|
|
519
554
|
var TOTAL_PAIRS_CHAR_CAP = 4e4;
|
|
520
555
|
var EXISTING_SKILLS_CHAR_CAP = 3e4;
|
|
521
556
|
function wlog(msg) {
|
|
522
557
|
try {
|
|
523
|
-
appendFileSync2(cfg.
|
|
558
|
+
appendFileSync2(cfg.skillifyLog, `[${utcTimestamp()}] skillify-worker(${cfg.projectKey}): ${msg}
|
|
524
559
|
`);
|
|
525
560
|
} catch {
|
|
526
561
|
}
|
|
@@ -715,7 +750,7 @@ function buildPrompt(pairs) {
|
|
|
715
750
|
].join("\n");
|
|
716
751
|
}
|
|
717
752
|
function readVerdict(stdout) {
|
|
718
|
-
if (
|
|
753
|
+
if (existsSync5(verdictPath)) {
|
|
719
754
|
try {
|
|
720
755
|
const text = readFileSync3(verdictPath, "utf-8");
|
|
721
756
|
const v2 = parseVerdict(text);
|
|
@@ -784,9 +819,9 @@ async function main() {
|
|
|
784
819
|
timeoutMs: 12e4
|
|
785
820
|
});
|
|
786
821
|
try {
|
|
787
|
-
writeFileSync3(
|
|
822
|
+
writeFileSync3(join6(tmpDir, "gate-stdout.txt"), gate.stdout);
|
|
788
823
|
if (gate.stderr)
|
|
789
|
-
writeFileSync3(
|
|
824
|
+
writeFileSync3(join6(tmpDir, "gate-stderr.txt"), gate.stderr);
|
|
790
825
|
} catch {
|
|
791
826
|
}
|
|
792
827
|
if (gate.errored) {
|
package/openclaw/package.json
CHANGED
package/openclaw/skills/SKILL.md
CHANGED
|
@@ -45,28 +45,28 @@ Do NOT jump straight to reading raw JSONL files. Always start with `hivemind_ind
|
|
|
45
45
|
- `/hivemind_update` — shows how to install (ask the agent, or run `hivemind update` in your terminal)
|
|
46
46
|
- `/hivemind_autoupdate [on|off]` — toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)
|
|
47
47
|
|
|
48
|
-
## Skill Management (
|
|
48
|
+
## Skill Management (skillify)
|
|
49
49
|
|
|
50
50
|
Hivemind also mines reusable Claude skills from agent sessions and stores them in a per-org Deeplake table. Openclaw itself doesn't run sessions to mine, but you can pull skills others have already mined for the user. These run in the user's terminal (the openclaw plugin does not register them as `/hivemind_*` commands):
|
|
51
51
|
|
|
52
|
-
- `hivemind
|
|
53
|
-
- `hivemind
|
|
54
|
-
- `hivemind
|
|
55
|
-
- `hivemind
|
|
56
|
-
- `hivemind
|
|
57
|
-
- `hivemind
|
|
58
|
-
- `hivemind
|
|
59
|
-
- `hivemind
|
|
60
|
-
- `hivemind
|
|
61
|
-
- `hivemind
|
|
62
|
-
- `hivemind
|
|
63
|
-
- `hivemind
|
|
64
|
-
- `hivemind
|
|
65
|
-
- `hivemind
|
|
66
|
-
- `hivemind
|
|
67
|
-
- `hivemind
|
|
68
|
-
|
|
69
|
-
If the user asks to "pull skills from X", "share skills with the team", or similar, suggest the matching `hivemind
|
|
52
|
+
- `hivemind skillify` — show scope/team/install + per-project state
|
|
53
|
+
- `hivemind skillify pull` — sync skills for the current project from the org table
|
|
54
|
+
- `hivemind skillify pull --user <email>` — only that author's skills
|
|
55
|
+
- `hivemind skillify pull --users a,b,c` — multiple authors (CSV)
|
|
56
|
+
- `hivemind skillify pull --all-users` — explicit "no author filter"
|
|
57
|
+
- `hivemind skillify pull --to project|global` — install location (`<cwd>/.claude/skills/` vs `~/.claude/skills/`)
|
|
58
|
+
- `hivemind skillify pull --dry-run` — preview without touching disk
|
|
59
|
+
- `hivemind skillify pull --force` — overwrite local (creates `.bak`)
|
|
60
|
+
- `hivemind skillify pull <skill-name>` — pull only that one skill (combines with `--user`)
|
|
61
|
+
- `hivemind skillify unpull` — remove every skill previously installed by pull
|
|
62
|
+
- `hivemind skillify unpull --user <email>` — remove only that author's pulls
|
|
63
|
+
- `hivemind skillify unpull --not-mine` — remove all pulls except your own
|
|
64
|
+
- `hivemind skillify unpull --dry-run` — preview without touching disk
|
|
65
|
+
- `hivemind skillify scope <me|team|org>` — set sharing scope for new skills
|
|
66
|
+
- `hivemind skillify install <project|global>` — default install location
|
|
67
|
+
- `hivemind skillify team add|remove|list <name>` — manage team list
|
|
68
|
+
|
|
69
|
+
If the user asks to "pull skills from X", "share skills with the team", or similar, suggest the matching `hivemind skillify` command. Run `hivemind skillify --help` for the full reference.
|
|
70
70
|
|
|
71
71
|
## Limits
|
|
72
72
|
|
package/package.json
CHANGED
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
27
27
|
import {
|
|
28
28
|
readFileSync, existsSync, appendFileSync, mkdirSync, writeFileSync,
|
|
29
|
-
openSync, closeSync, constants as fsConstants,
|
|
29
|
+
openSync, closeSync, renameSync, constants as fsConstants,
|
|
30
30
|
} from "node:fs";
|
|
31
31
|
import { homedir, tmpdir } from "node:os";
|
|
32
32
|
import { join, dirname } from "node:path";
|
|
33
33
|
import { connect } from "node:net";
|
|
34
|
-
import { spawn, execSync } from "node:child_process";
|
|
34
|
+
import { spawn, spawnSync, execSync } from "node:child_process";
|
|
35
35
|
import { createHash } from "node:crypto";
|
|
36
36
|
|
|
37
37
|
// ---------- diagnostic logging --------------------------------------------------
|
|
@@ -205,11 +205,51 @@ function tryEmbedOverSocket(text: string, kind: "document" | "query"): Promise<n
|
|
|
205
205
|
|
|
206
206
|
const SUMMARY_STATE_DIR = join(homedir(), ".claude", "hooks", "summary-state");
|
|
207
207
|
const PI_WIKI_WORKER_PATH = join(homedir(), ".pi", "agent", "hivemind", "wiki-worker.js");
|
|
208
|
-
//
|
|
208
|
+
// Skillify worker installed alongside wiki-worker by `hivemind pi install`.
|
|
209
209
|
// Spawned on session_shutdown to mine reusable Claude skills from the just-
|
|
210
210
|
// finished session. Same shared bundle used by CC/Codex/Cursor/Hermes.
|
|
211
|
-
const
|
|
212
|
-
|
|
211
|
+
const PI_SKILLIFY_WORKER_PATH = join(homedir(), ".pi", "agent", "hivemind", "skillify-worker.js");
|
|
212
|
+
// Auto-pull worker installed alongside wiki-worker / skillify-worker by
|
|
213
|
+
// `hivemind pi install`. Spawned synchronously on session_start to fetch
|
|
214
|
+
// all-author skills from the org table. The worker is a thin wrapper
|
|
215
|
+
// around the shared autoPullSkills() that codex / cursor / hermes call
|
|
216
|
+
// directly — pi can't import the TS module (raw .ts, zero deps), so it
|
|
217
|
+
// routes through this child process. Keeps pi's pulled skills layout +
|
|
218
|
+
// symlink fan-out in lockstep with the other agents automatically.
|
|
219
|
+
const PI_AUTOPULL_WORKER_PATH = join(homedir(), ".pi", "agent", "hivemind", "autopull-worker.js");
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Synchronously run the bundled auto-pull worker. Bounded by a 6s
|
|
223
|
+
* wall-clock cap (the worker's internal timeout is 5s; the extra second
|
|
224
|
+
* is defence-in-depth for spawn overhead). Returns when the worker
|
|
225
|
+
* exits, regardless of exit code — autoPullSkills is documented as
|
|
226
|
+
* never-rejecting and the worker swallows all failures, so a non-zero
|
|
227
|
+
* exit code can only mean an unrecoverable runtime error that we want
|
|
228
|
+
* to ignore here too. Pi's session_start blocks on this, mirroring the
|
|
229
|
+
* `await autoPullSkills()` in the other agents.
|
|
230
|
+
*/
|
|
231
|
+
function runAutopullWorker(): void {
|
|
232
|
+
if (!existsSync(PI_AUTOPULL_WORKER_PATH)) {
|
|
233
|
+
logHm(`autopull: worker bundle missing at ${PI_AUTOPULL_WORKER_PATH} — skipping`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const result = spawnSync(process.execPath, [PI_AUTOPULL_WORKER_PATH], {
|
|
238
|
+
stdio: "ignore",
|
|
239
|
+
timeout: 6_000,
|
|
240
|
+
env: process.env,
|
|
241
|
+
});
|
|
242
|
+
if (result.error) {
|
|
243
|
+
logHm(`autopull: spawn failed (swallowed): ${result.error.message}`);
|
|
244
|
+
} else if (result.signal) {
|
|
245
|
+
logHm(`autopull: worker killed by signal ${result.signal} (likely the 6s cap)`);
|
|
246
|
+
} else {
|
|
247
|
+
logHm(`autopull: worker exited code=${result.status}`);
|
|
248
|
+
}
|
|
249
|
+
} catch (e: any) {
|
|
250
|
+
logHm(`autopull: spawn threw (swallowed): ${e?.message ?? e}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
213
253
|
|
|
214
254
|
interface SummaryState {
|
|
215
255
|
lastSummaryAt: number;
|
|
@@ -404,20 +444,20 @@ function spawnWikiWorker(
|
|
|
404
444
|
}
|
|
405
445
|
}
|
|
406
446
|
|
|
407
|
-
// ----------
|
|
447
|
+
// ---------- skillify worker spawn ---------------------------------------------
|
|
408
448
|
//
|
|
409
|
-
// Mirror of src/
|
|
449
|
+
// Mirror of src/skillify/spawn-skillify-worker.ts and src/skillify/triggers.ts —
|
|
410
450
|
// inlined here because pi/extension-source/hivemind.ts is shipped as raw .ts
|
|
411
451
|
// with zero non-builtin runtime dependencies (pi compiles + loads it at
|
|
412
|
-
// extension-load time). The shared TypeScript modules under src/
|
|
452
|
+
// extension-load time). The shared TypeScript modules under src/skillify/
|
|
413
453
|
// can't be imported from this file.
|
|
414
454
|
//
|
|
415
|
-
// The
|
|
455
|
+
// The skillify worker mines the just-finished session for reusable Claude
|
|
416
456
|
// skills, gates each cluster via a model call, and writes SKILL.md files +
|
|
417
457
|
// rows in the org's skills Deeplake table.
|
|
418
458
|
|
|
419
|
-
/** Stable project key — sha1(cwd) truncated, mirrors src/
|
|
420
|
-
function
|
|
459
|
+
/** Stable project key — sha1(cwd) truncated, mirrors src/skillify/state.ts deriveProjectKey. */
|
|
460
|
+
function deriveSkillifyProjectKey(cwd: string): { key: string; project: string } {
|
|
421
461
|
const project = (cwd ?? "").split("/").pop() || "unknown";
|
|
422
462
|
// Pi's extension can't easily run `git config` synchronously here; use cwd
|
|
423
463
|
// as the signature. Two checkouts of the same repo at different paths get
|
|
@@ -427,15 +467,15 @@ function deriveSkilifyProjectKey(cwd: string): { key: string; project: string }
|
|
|
427
467
|
return { key, project };
|
|
428
468
|
}
|
|
429
469
|
|
|
430
|
-
function
|
|
431
|
-
if (!existsSync(
|
|
432
|
-
logHm(`
|
|
470
|
+
function spawnPiSkillifyWorker(creds: Creds, sessionId: string, cwd: string): void {
|
|
471
|
+
if (!existsSync(PI_SKILLIFY_WORKER_PATH)) {
|
|
472
|
+
logHm(`spawnPiSkillifyWorker: no worker at ${PI_SKILLIFY_WORKER_PATH} — install via 'hivemind pi install' or rebuild`);
|
|
433
473
|
return;
|
|
434
474
|
}
|
|
435
|
-
const { key: projectKey, project } =
|
|
475
|
+
const { key: projectKey, project } = deriveSkillifyProjectKey(cwd);
|
|
436
476
|
|
|
437
477
|
// No spawn-side lock: the worker itself acquires `<projectKey>.lock` via
|
|
438
|
-
// src/
|
|
478
|
+
// src/skillify/state.ts:tryAcquireWorkerLock and releases it on exit (with
|
|
439
479
|
// a 10-min stale-lock fallback). A spawn-side lock here would create a
|
|
440
480
|
// SECOND lockfile (`<projectKey>.worker.lock`) that nobody releases,
|
|
441
481
|
// permanently blocking subsequent spawns from the same Pi runtime
|
|
@@ -443,12 +483,12 @@ function spawnPiSkilifyWorker(creds: Creds, sessionId: string, cwd: string): voi
|
|
|
443
483
|
// back-to-back spawns where a worker is in flight cost only one extra
|
|
444
484
|
// node cold-start (~50ms) before the worker self-skips on the lock.
|
|
445
485
|
|
|
446
|
-
const tmpDir = join(tmpdir(), `deeplake-
|
|
486
|
+
const tmpDir = join(tmpdir(), `deeplake-skillify-${projectKey}-${Date.now()}`);
|
|
447
487
|
try { mkdirSync(tmpDir, { recursive: true, mode: 0o700 }); }
|
|
448
|
-
catch (e: any) { logHm(`
|
|
488
|
+
catch (e: any) { logHm(`spawnPiSkillifyWorker: mkdir failed: ${e?.message ?? e}`); return; }
|
|
449
489
|
const configPath = join(tmpDir, "config.json");
|
|
450
490
|
|
|
451
|
-
// Same shape the spawn-
|
|
491
|
+
// Same shape the spawn-skillify-worker.ts module writes for the other agents.
|
|
452
492
|
// Defaults match scope-config.ts: scope=me, install=project, no team list.
|
|
453
493
|
// Pi-specific: no per-agent gate binary (`gateBin: null`) — the worker's
|
|
454
494
|
// gate-runner falls back to its agent dispatch which for `agent: "pi"`
|
|
@@ -476,21 +516,21 @@ function spawnPiSkilifyWorker(creds: Creds, sessionId: string, cwd: string): voi
|
|
|
476
516
|
// pi-specific gate args — match wikiWorker config defaults (google + gemini-2.5-flash)
|
|
477
517
|
piProvider: process.env.HIVEMIND_PI_PROVIDER ?? "google",
|
|
478
518
|
piModel: process.env.HIVEMIND_PI_MODEL ?? "gemini-2.5-flash",
|
|
479
|
-
|
|
519
|
+
skillifyLog: join(homedir(), ".deeplake", "hivemind-pi-skillify.log"),
|
|
480
520
|
currentSessionId: sessionId,
|
|
481
521
|
};
|
|
482
522
|
try { writeFileSync(configPath, JSON.stringify(config), { mode: 0o600 }); }
|
|
483
|
-
catch (e: any) { logHm(`
|
|
523
|
+
catch (e: any) { logHm(`spawnPiSkillifyWorker: config write failed: ${e?.message ?? e}`); return; }
|
|
484
524
|
|
|
485
|
-
logHm(`
|
|
525
|
+
logHm(`spawnPiSkillifyWorker: spawning ${PI_SKILLIFY_WORKER_PATH} project=${project} key=${projectKey} session=${sessionId}`);
|
|
486
526
|
try {
|
|
487
|
-
spawn(process.execPath, [
|
|
527
|
+
spawn(process.execPath, [PI_SKILLIFY_WORKER_PATH, configPath], {
|
|
488
528
|
detached: true,
|
|
489
529
|
stdio: "ignore",
|
|
490
|
-
env: { ...process.env,
|
|
530
|
+
env: { ...process.env, HIVEMIND_SKILLIFY_WORKER: "1", HIVEMIND_CAPTURE: "false" },
|
|
491
531
|
}).unref();
|
|
492
532
|
} catch (e: any) {
|
|
493
|
-
logHm(`
|
|
533
|
+
logHm(`spawnPiSkillifyWorker: spawn failed: ${e?.message ?? e}`);
|
|
494
534
|
}
|
|
495
535
|
}
|
|
496
536
|
|
|
@@ -656,23 +696,23 @@ Organization management — each argument is SEPARATE (do NOT quote subcommands
|
|
|
656
696
|
- hivemind members — list members
|
|
657
697
|
- hivemind remove <user-id> — remove member
|
|
658
698
|
|
|
659
|
-
SKILLS (
|
|
660
|
-
- hivemind
|
|
661
|
-
- hivemind
|
|
662
|
-
- hivemind
|
|
663
|
-
- hivemind
|
|
664
|
-
- hivemind
|
|
665
|
-
- hivemind
|
|
666
|
-
- hivemind
|
|
667
|
-
- hivemind
|
|
668
|
-
- hivemind
|
|
669
|
-
- hivemind
|
|
670
|
-
- hivemind
|
|
671
|
-
- hivemind
|
|
672
|
-
- hivemind
|
|
673
|
-
- hivemind
|
|
674
|
-
- hivemind
|
|
675
|
-
- hivemind
|
|
699
|
+
SKILLS (skillify) — mine + share reusable skills across the org. Run these in a terminal (or via shell if available):
|
|
700
|
+
- hivemind skillify — show scope/team/install + per-project state
|
|
701
|
+
- hivemind skillify pull — sync project skills from the org table
|
|
702
|
+
- hivemind skillify pull --user <email> — only that author's skills
|
|
703
|
+
- hivemind skillify pull --users a,b,c — multiple authors (CSV)
|
|
704
|
+
- hivemind skillify pull --all-users — explicit "no author filter"
|
|
705
|
+
- hivemind skillify pull --to project|global — install location
|
|
706
|
+
- hivemind skillify pull --dry-run — preview only
|
|
707
|
+
- hivemind skillify pull --force — overwrite local (creates .bak)
|
|
708
|
+
- hivemind skillify pull <skill-name> — pull only that skill (combines with --user)
|
|
709
|
+
- hivemind skillify unpull — remove every skill previously installed by pull
|
|
710
|
+
- hivemind skillify unpull --user <email> — remove only that author's pulls
|
|
711
|
+
- hivemind skillify unpull --not-mine — remove all pulls except your own
|
|
712
|
+
- hivemind skillify unpull --dry-run — preview without touching disk
|
|
713
|
+
- hivemind skillify scope <me|team|org> — sharing scope for new skills
|
|
714
|
+
- hivemind skillify install <project|global> — default install location
|
|
715
|
+
- hivemind skillify team add|remove|list <name> — manage team list`;
|
|
676
716
|
|
|
677
717
|
export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
678
718
|
const captureEnabled = process.env.HIVEMIND_CAPTURE !== "false";
|
|
@@ -850,6 +890,15 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
850
890
|
}
|
|
851
891
|
logHm(`session_start: sessions table visible=${visible} (probe took ${Date.now() - start}ms)`);
|
|
852
892
|
}
|
|
893
|
+
|
|
894
|
+
// Auto-pull all-author skills via the bundled worker (same shared
|
|
895
|
+
// autoPullSkills as codex / cursor / hermes — see runAutopullWorker
|
|
896
|
+
// above). Synchronous so freshly pulled skills are visible to pi
|
|
897
|
+
// before the first prompt; 6s upper bound. Throttling, layout, and
|
|
898
|
+
// per-agent symlink fan-out all live in the worker — no inline
|
|
899
|
+
// duplicate maintained here.
|
|
900
|
+
if (creds) runAutopullWorker();
|
|
901
|
+
|
|
853
902
|
const additional = creds
|
|
854
903
|
? `${CONTEXT_PREAMBLE}\nLogged in to Deeplake as org: ${creds.orgName ?? creds.orgId} (workspace: ${creds.workspaceId}).`
|
|
855
904
|
: `${CONTEXT_PREAMBLE}\nNot logged in to Deeplake. Run \`hivemind login\` to authenticate.`;
|
|
@@ -958,13 +1007,13 @@ export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
|
958
1007
|
// skips if a periodic worker is mid-flight. Non-fatal either way.
|
|
959
1008
|
spawnWikiWorker(creds, sessionId, cwd, "final");
|
|
960
1009
|
|
|
961
|
-
// Also kick off the
|
|
1010
|
+
// Also kick off the skillify worker so this session's prompt+answer
|
|
962
1011
|
// pairs become candidates for reusable skills. Lock keyed on
|
|
963
1012
|
// projectKey, not sessionId — multiple sessions in the same project
|
|
964
1013
|
// shouldn't race the gate. Non-fatal: failure here only loses the
|
|
965
1014
|
// mining for this one session, never breaks the wiki summary above.
|
|
966
|
-
try {
|
|
967
|
-
catch (e: any) { logHm(`session_shutdown:
|
|
1015
|
+
try { spawnPiSkillifyWorker(creds, sessionId, cwd); }
|
|
1016
|
+
catch (e: any) { logHm(`session_shutdown: skillify spawn threw: ${e?.message ?? e}`); }
|
|
968
1017
|
});
|
|
969
1018
|
|
|
970
1019
|
// Module-load breadcrumb so we know the extension's default export ran at all.
|