@deeplake/hivemind 0.7.79 → 0.7.81
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 +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/bundle/cli.js +140 -94
- package/codex/bundle/capture.js +232 -72
- package/codex/bundle/commands/auth-login.js +14 -10
- package/codex/bundle/embeddings/embed-daemon.js +9 -5
- package/codex/bundle/graph-on-stop.js +32 -28
- package/codex/bundle/graph-pull-worker.js +27 -23
- package/codex/bundle/pre-tool-use.js +366 -123
- package/codex/bundle/session-start-setup.js +18 -14
- package/codex/bundle/session-start.js +26 -22
- package/codex/bundle/shell/deeplake-shell.js +30 -26
- package/codex/bundle/skillify-worker.js +14 -10
- package/codex/bundle/skillopt-worker.js +2061 -0
- package/codex/bundle/stop.js +50 -45
- package/codex/bundle/wiki-worker.js +18 -14
- package/cursor/bundle/capture.js +71 -44
- package/cursor/bundle/commands/auth-login.js +14 -10
- package/cursor/bundle/embeddings/embed-daemon.js +9 -5
- package/cursor/bundle/graph-on-stop.js +32 -28
- package/cursor/bundle/graph-pull-worker.js +27 -23
- package/cursor/bundle/pre-tool-use.js +28 -24
- package/cursor/bundle/session-end.js +40 -36
- package/cursor/bundle/session-start.js +39 -35
- package/cursor/bundle/shell/deeplake-shell.js +30 -26
- package/cursor/bundle/skillify-worker.js +14 -10
- package/cursor/bundle/wiki-worker.js +18 -14
- package/hermes/bundle/capture.js +235 -85
- package/hermes/bundle/commands/auth-login.js +14 -10
- package/hermes/bundle/embeddings/embed-daemon.js +9 -5
- package/hermes/bundle/graph-on-stop.js +32 -28
- package/hermes/bundle/graph-pull-worker.js +27 -23
- package/hermes/bundle/pre-tool-use.js +298 -74
- package/hermes/bundle/session-end.js +40 -36
- package/hermes/bundle/session-start.js +39 -35
- package/hermes/bundle/shell/deeplake-shell.js +30 -26
- package/hermes/bundle/skillify-worker.js +14 -10
- package/hermes/bundle/skillopt-worker.js +2061 -0
- package/hermes/bundle/wiki-worker.js +18 -14
- package/mcp/bundle/server.js +15 -11
- package/openclaw/dist/index.js +11 -7
- package/openclaw/dist/skillify-worker.js +14 -10
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +1 -1
- package/pi/extension-source/hivemind.ts +93 -0
|
@@ -0,0 +1,2061 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// dist/src/index-marker-store.js
|
|
13
|
+
var index_marker_store_exports = {};
|
|
14
|
+
__export(index_marker_store_exports, {
|
|
15
|
+
buildIndexMarkerPath: () => buildIndexMarkerPath,
|
|
16
|
+
getIndexMarkerDir: () => getIndexMarkerDir,
|
|
17
|
+
hasFreshIndexMarker: () => hasFreshIndexMarker,
|
|
18
|
+
writeIndexMarker: () => writeIndexMarker
|
|
19
|
+
});
|
|
20
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
21
|
+
import { join as join5 } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
function getIndexMarkerDir() {
|
|
24
|
+
return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join5(tmpdir(), "hivemind-deeplake-indexes");
|
|
25
|
+
}
|
|
26
|
+
function buildIndexMarkerPath(workspaceId, orgId, table, suffix) {
|
|
27
|
+
const markerKey = [workspaceId, orgId, table, suffix].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
28
|
+
return join5(getIndexMarkerDir(), `${markerKey}.json`);
|
|
29
|
+
}
|
|
30
|
+
function hasFreshIndexMarker(markerPath) {
|
|
31
|
+
if (!existsSync2(markerPath))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync4(markerPath, "utf-8"));
|
|
35
|
+
const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
|
|
36
|
+
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeIndexMarker(markerPath) {
|
|
44
|
+
mkdirSync4(getIndexMarkerDir(), { recursive: true });
|
|
45
|
+
writeFileSync3(markerPath, JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
46
|
+
}
|
|
47
|
+
var INDEX_MARKER_TTL_MS;
|
|
48
|
+
var init_index_marker_store = __esm({
|
|
49
|
+
"dist/src/index-marker-store.js"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// dist/src/skillify/skillopt-worker.js
|
|
56
|
+
import path2 from "node:path";
|
|
57
|
+
import { accessSync, constants as fsConstants } from "node:fs";
|
|
58
|
+
|
|
59
|
+
// dist/src/utils/debug.js
|
|
60
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
61
|
+
import { dirname, join } from "node:path";
|
|
62
|
+
import { homedir } from "node:os";
|
|
63
|
+
var LOG = join(homedir(), ".deeplake", "hook-debug.log");
|
|
64
|
+
function isDebug() {
|
|
65
|
+
return process.env.HIVEMIND_DEBUG === "1";
|
|
66
|
+
}
|
|
67
|
+
function log(tag, msg) {
|
|
68
|
+
if (!isDebug())
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(dirname(LOG), { recursive: true });
|
|
72
|
+
appendFileSync(LOG, `${(/* @__PURE__ */ new Date()).toISOString()} [${tag}] ${msg}
|
|
73
|
+
`);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// dist/src/config.js
|
|
79
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
80
|
+
import { join as join2 } from "node:path";
|
|
81
|
+
import { homedir as homedir2, userInfo } from "node:os";
|
|
82
|
+
function loadConfig() {
|
|
83
|
+
const home = homedir2();
|
|
84
|
+
const credPath = join2(home, ".deeplake", "credentials.json");
|
|
85
|
+
let creds = null;
|
|
86
|
+
if (existsSync(credPath)) {
|
|
87
|
+
try {
|
|
88
|
+
creds = JSON.parse(readFileSync(credPath, "utf-8"));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const token = process.env.HIVEMIND_TOKEN ?? creds?.token;
|
|
94
|
+
const orgId = process.env.HIVEMIND_ORG_ID ?? creds?.orgId;
|
|
95
|
+
if (!token || !orgId)
|
|
96
|
+
return null;
|
|
97
|
+
return {
|
|
98
|
+
token,
|
|
99
|
+
orgId,
|
|
100
|
+
orgName: creds?.orgName ?? orgId,
|
|
101
|
+
userName: creds?.userName || userInfo().username || "unknown",
|
|
102
|
+
workspaceId: process.env.HIVEMIND_WORKSPACE_ID ?? creds?.workspaceId ?? "default",
|
|
103
|
+
apiUrl: process.env.HIVEMIND_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai",
|
|
104
|
+
tableName: process.env.HIVEMIND_TABLE ?? "memory",
|
|
105
|
+
sessionsTableName: process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions",
|
|
106
|
+
skillsTableName: process.env.HIVEMIND_SKILLS_TABLE ?? "skills",
|
|
107
|
+
// Defaults match the table name written into the SQL — keep aligned
|
|
108
|
+
// with RULES_COLUMNS in deeplake-schema.ts and with the e2e test-org
|
|
109
|
+
// override convention (memory_test / sessions_test → goals_test, etc.)
|
|
110
|
+
// documented in CLAUDE.md.
|
|
111
|
+
rulesTableName: process.env.HIVEMIND_RULES_TABLE ?? "hivemind_rules",
|
|
112
|
+
// Goals + KPIs (refined design — VFS path classifier maps
|
|
113
|
+
// memory/goal/<user>/<status>/<uuid>.md → hivemind_goals row
|
|
114
|
+
// memory/kpi/<uuid>/<kpi_id>.md → hivemind_kpis row
|
|
115
|
+
// See src/shell/deeplake-fs.ts for the translation logic and
|
|
116
|
+
// GOALS_COLUMNS / KPIS_COLUMNS in deeplake-schema.ts for the
|
|
117
|
+
// table shape.
|
|
118
|
+
goalsTableName: process.env.HIVEMIND_GOALS_TABLE ?? "hivemind_goals",
|
|
119
|
+
kpisTableName: process.env.HIVEMIND_KPIS_TABLE ?? "hivemind_kpis",
|
|
120
|
+
codebaseTableName: process.env.HIVEMIND_CODEBASE_TABLE ?? "codebase",
|
|
121
|
+
memoryPath: process.env.HIVEMIND_MEMORY_PATH ?? join2(home, ".deeplake", "memory")
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// dist/src/deeplake-api.js
|
|
126
|
+
import { randomUUID } from "node:crypto";
|
|
127
|
+
|
|
128
|
+
// dist/src/utils/sql.js
|
|
129
|
+
function sqlStr(value) {
|
|
130
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
131
|
+
}
|
|
132
|
+
function sqlIdent(name) {
|
|
133
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
134
|
+
throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
|
|
135
|
+
}
|
|
136
|
+
return name;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// dist/src/embeddings/columns.js
|
|
140
|
+
var SUMMARY_EMBEDDING_COL = "summary_embedding";
|
|
141
|
+
|
|
142
|
+
// dist/src/utils/client-header.js
|
|
143
|
+
var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
|
|
144
|
+
function deeplakeClientValue() {
|
|
145
|
+
return "hivemind";
|
|
146
|
+
}
|
|
147
|
+
function deeplakeClientHeader() {
|
|
148
|
+
return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// dist/src/deeplake-schema.js
|
|
152
|
+
var MEMORY_COLUMNS = Object.freeze([
|
|
153
|
+
{ name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
154
|
+
{ name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
155
|
+
{ name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
156
|
+
{ name: "summary", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
157
|
+
{ name: "summary_embedding", sql: "FLOAT4[]" },
|
|
158
|
+
{ name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
159
|
+
{ name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'text/plain'" },
|
|
160
|
+
{ name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
|
|
161
|
+
{ name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
162
|
+
{ name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
163
|
+
{ name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
164
|
+
{ name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
165
|
+
{ name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
166
|
+
{ name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
|
|
167
|
+
]);
|
|
168
|
+
var SESSIONS_COLUMNS = Object.freeze([
|
|
169
|
+
{ name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
170
|
+
{ name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
171
|
+
{ name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
172
|
+
{ name: "message", sql: "JSONB" },
|
|
173
|
+
{ name: "message_embedding", sql: "FLOAT4[]" },
|
|
174
|
+
{ name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
175
|
+
{ name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'application/json'" },
|
|
176
|
+
{ name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
|
|
177
|
+
{ name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
178
|
+
{ name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
179
|
+
{ name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
180
|
+
{ name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
181
|
+
{ name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
182
|
+
{ name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
|
|
183
|
+
]);
|
|
184
|
+
var SKILLS_COLUMNS = Object.freeze([
|
|
185
|
+
{ name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
186
|
+
{ name: "name", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
187
|
+
{ name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
188
|
+
{ name: "project_key", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
189
|
+
{ name: "local_path", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
190
|
+
{ name: "install", sql: "TEXT NOT NULL DEFAULT 'project'" },
|
|
191
|
+
{ name: "source_sessions", sql: "TEXT NOT NULL DEFAULT '[]'" },
|
|
192
|
+
{ name: "source_agent", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
193
|
+
{ name: "scope", sql: "TEXT NOT NULL DEFAULT 'me'" },
|
|
194
|
+
{ name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
195
|
+
{ name: "contributors", sql: "TEXT NOT NULL DEFAULT '[]'" },
|
|
196
|
+
{ name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
197
|
+
{ name: "trigger_text", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
198
|
+
{ name: "body", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
199
|
+
{ name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
|
|
200
|
+
{ name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
201
|
+
{ name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }
|
|
202
|
+
]);
|
|
203
|
+
var RULES_COLUMNS = Object.freeze([
|
|
204
|
+
{ name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
205
|
+
{ name: "rule_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
206
|
+
{ name: "text", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
207
|
+
{ name: "scope", sql: "TEXT NOT NULL DEFAULT 'team'" },
|
|
208
|
+
{ name: "status", sql: "TEXT NOT NULL DEFAULT 'active'" },
|
|
209
|
+
{ name: "assigned_by", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
210
|
+
{ name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
|
|
211
|
+
{ name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
212
|
+
{ name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
|
|
213
|
+
{ name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
|
|
214
|
+
]);
|
|
215
|
+
var GOALS_COLUMNS = Object.freeze([
|
|
216
|
+
{ name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
217
|
+
{ name: "goal_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
218
|
+
{ name: "owner", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
219
|
+
{ name: "status", sql: "TEXT NOT NULL DEFAULT 'opened'" },
|
|
220
|
+
{ name: "content", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
221
|
+
{ name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
|
|
222
|
+
{ name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
223
|
+
{ name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
224
|
+
{ name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
|
|
225
|
+
{ name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
|
|
226
|
+
]);
|
|
227
|
+
var KPIS_COLUMNS = Object.freeze([
|
|
228
|
+
{ name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
229
|
+
{ name: "goal_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
230
|
+
{ name: "kpi_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
231
|
+
{ name: "content", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
232
|
+
{ name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
|
|
233
|
+
{ name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
234
|
+
{ name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
235
|
+
{ name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
|
|
236
|
+
{ name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
|
|
237
|
+
]);
|
|
238
|
+
function validateSchema(label, cols) {
|
|
239
|
+
const seen = /* @__PURE__ */ new Set();
|
|
240
|
+
for (const col of cols) {
|
|
241
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col.name)) {
|
|
242
|
+
throw new Error(`${label}: column name "${col.name}" is not a valid SQL identifier`);
|
|
243
|
+
}
|
|
244
|
+
if (seen.has(col.name)) {
|
|
245
|
+
throw new Error(`${label}: duplicate column "${col.name}"`);
|
|
246
|
+
}
|
|
247
|
+
seen.add(col.name);
|
|
248
|
+
const notNull = /\bNOT\s+NULL\b/i.test(col.sql);
|
|
249
|
+
const hasDefault = /\bDEFAULT\b/i.test(col.sql);
|
|
250
|
+
if (notNull && !hasDefault) {
|
|
251
|
+
throw new Error(`${label}: column "${col.name}" is NOT NULL but has no DEFAULT \u2014 ALTER TABLE ADD COLUMN on a populated table would fail.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
var CODEBASE_COLUMNS = Object.freeze([
|
|
256
|
+
// Identity key (matches the PK below)
|
|
257
|
+
{ name: "org_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
258
|
+
{ name: "workspace_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
259
|
+
{ name: "repo_slug", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
260
|
+
{ name: "user_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
261
|
+
{ name: "worktree_id", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
262
|
+
{ name: "commit_sha", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
263
|
+
// Observation metadata
|
|
264
|
+
{ name: "parent_sha", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
265
|
+
{ name: "branch", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
266
|
+
{ name: "ts", sql: "TIMESTAMP" },
|
|
267
|
+
{ name: "pushed_by", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
268
|
+
// Snapshot payload
|
|
269
|
+
{ name: "snapshot_sha256", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
270
|
+
{ name: "snapshot_jsonb", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
271
|
+
{ name: "node_count", sql: "BIGINT NOT NULL DEFAULT 0" },
|
|
272
|
+
{ name: "edge_count", sql: "BIGINT NOT NULL DEFAULT 0" },
|
|
273
|
+
// Generator metadata (for drift diagnostics — what hivemind version produced this?)
|
|
274
|
+
{ name: "generator", sql: "TEXT NOT NULL DEFAULT 'hivemind-graph'" },
|
|
275
|
+
{ name: "generator_version", sql: "TEXT NOT NULL DEFAULT ''" },
|
|
276
|
+
{ name: "schema_version", sql: "BIGINT NOT NULL DEFAULT 1" }
|
|
277
|
+
]);
|
|
278
|
+
validateSchema("MEMORY_COLUMNS", MEMORY_COLUMNS);
|
|
279
|
+
validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS);
|
|
280
|
+
validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS);
|
|
281
|
+
validateSchema("RULES_COLUMNS", RULES_COLUMNS);
|
|
282
|
+
validateSchema("GOALS_COLUMNS", GOALS_COLUMNS);
|
|
283
|
+
validateSchema("KPIS_COLUMNS", KPIS_COLUMNS);
|
|
284
|
+
validateSchema("CODEBASE_COLUMNS", CODEBASE_COLUMNS);
|
|
285
|
+
function buildCreateTableSql(tableName, cols) {
|
|
286
|
+
const safe = sqlIdent(tableName);
|
|
287
|
+
const colSql = cols.map((c) => `${c.name} ${c.sql}`).join(", ");
|
|
288
|
+
return `CREATE TABLE IF NOT EXISTS "${safe}" (${colSql}) USING deeplake`;
|
|
289
|
+
}
|
|
290
|
+
function buildIntrospectionSql(tableName, workspaceId) {
|
|
291
|
+
return `SELECT column_name FROM information_schema.columns WHERE table_name = '${sqlStr(tableName)}' AND table_schema = '${sqlStr(workspaceId)}'`;
|
|
292
|
+
}
|
|
293
|
+
async function healMissingColumns(args) {
|
|
294
|
+
const safeTable = sqlIdent(args.tableName);
|
|
295
|
+
const introspectSql = buildIntrospectionSql(args.tableName, args.workspaceId);
|
|
296
|
+
const rows = await args.query(introspectSql);
|
|
297
|
+
const existing = /* @__PURE__ */ new Set();
|
|
298
|
+
for (const row of rows) {
|
|
299
|
+
const v = row?.column_name;
|
|
300
|
+
if (typeof v === "string")
|
|
301
|
+
existing.add(v.toLowerCase());
|
|
302
|
+
}
|
|
303
|
+
const missingCols = args.columns.filter((c) => !existing.has(c.name.toLowerCase()));
|
|
304
|
+
const missing = missingCols.map((c) => c.name);
|
|
305
|
+
if (missingCols.length === 0)
|
|
306
|
+
return { missing, altered: [] };
|
|
307
|
+
const altered = [];
|
|
308
|
+
for (const col of missingCols) {
|
|
309
|
+
try {
|
|
310
|
+
await args.query(`ALTER TABLE "${safeTable}" ADD COLUMN ${col.name} ${col.sql}`);
|
|
311
|
+
altered.push(col.name);
|
|
312
|
+
args.log?.(`schema-heal: added "${args.tableName}"."${col.name}"`);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
315
|
+
if (!/already exists/i.test(msg))
|
|
316
|
+
throw e;
|
|
317
|
+
const recheck = await args.query(introspectSql);
|
|
318
|
+
const present = recheck.some((r) => {
|
|
319
|
+
const v = r?.column_name;
|
|
320
|
+
return typeof v === "string" && v.toLowerCase() === col.name.toLowerCase();
|
|
321
|
+
});
|
|
322
|
+
if (!present)
|
|
323
|
+
throw e;
|
|
324
|
+
args.log?.(`schema-heal: "${args.tableName}"."${col.name}" appeared via race, treating as success`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { missing, altered };
|
|
328
|
+
}
|
|
329
|
+
function isMissingTableError(message) {
|
|
330
|
+
if (!message)
|
|
331
|
+
return false;
|
|
332
|
+
if (/permission denied|must be owner/i.test(message))
|
|
333
|
+
return false;
|
|
334
|
+
if (/\bcolumn\b/i.test(message))
|
|
335
|
+
return false;
|
|
336
|
+
return /Table does not exist|relation .* does not exist|no such table/i.test(message);
|
|
337
|
+
}
|
|
338
|
+
function isMissingColumnError(message) {
|
|
339
|
+
if (!message)
|
|
340
|
+
return false;
|
|
341
|
+
if (/permission denied|must be owner/i.test(message))
|
|
342
|
+
return false;
|
|
343
|
+
return /column ["']?[A-Za-z_][A-Za-z0-9_]*["']? .*does not exist/i.test(message) || /unknown column/i.test(message) || /no such column/i.test(message);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// dist/src/notifications/queue.js
|
|
347
|
+
import { readFileSync as readFileSync2, writeFileSync, renameSync, mkdirSync as mkdirSync2, openSync, closeSync, unlinkSync, statSync } from "node:fs";
|
|
348
|
+
import { join as join3, resolve } from "node:path";
|
|
349
|
+
import { homedir as homedir3 } from "node:os";
|
|
350
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
351
|
+
var log2 = (msg) => log("notifications-queue", msg);
|
|
352
|
+
var LOCK_RETRY_MAX = 50;
|
|
353
|
+
var LOCK_RETRY_BASE_MS = 5;
|
|
354
|
+
var LOCK_STALE_MS = 5e3;
|
|
355
|
+
function queuePath() {
|
|
356
|
+
return join3(homedir3(), ".deeplake", "notifications-queue.json");
|
|
357
|
+
}
|
|
358
|
+
function lockPath() {
|
|
359
|
+
return `${queuePath()}.lock`;
|
|
360
|
+
}
|
|
361
|
+
function readQueue() {
|
|
362
|
+
try {
|
|
363
|
+
const raw = readFileSync2(queuePath(), "utf-8");
|
|
364
|
+
const parsed = JSON.parse(raw);
|
|
365
|
+
if (!parsed || !Array.isArray(parsed.queue)) {
|
|
366
|
+
log2(`queue malformed \u2192 treating as empty`);
|
|
367
|
+
return { queue: [] };
|
|
368
|
+
}
|
|
369
|
+
return { queue: parsed.queue };
|
|
370
|
+
} catch {
|
|
371
|
+
return { queue: [] };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function _isQueuePathInsideHome(path3, home) {
|
|
375
|
+
const r = resolve(path3);
|
|
376
|
+
const h = resolve(home);
|
|
377
|
+
return r.startsWith(h + "/") || r === h;
|
|
378
|
+
}
|
|
379
|
+
function writeQueue(q) {
|
|
380
|
+
const path3 = queuePath();
|
|
381
|
+
const home = resolve(homedir3());
|
|
382
|
+
if (!_isQueuePathInsideHome(path3, home)) {
|
|
383
|
+
throw new Error(`notifications-queue write blocked: ${path3} is outside ${home}`);
|
|
384
|
+
}
|
|
385
|
+
mkdirSync2(join3(home, ".deeplake"), { recursive: true, mode: 448 });
|
|
386
|
+
const tmp = `${path3}.${process.pid}.tmp`;
|
|
387
|
+
writeFileSync(tmp, JSON.stringify(q, null, 2), { mode: 384 });
|
|
388
|
+
renameSync(tmp, path3);
|
|
389
|
+
}
|
|
390
|
+
async function withQueueLock(fn) {
|
|
391
|
+
const path3 = lockPath();
|
|
392
|
+
mkdirSync2(join3(homedir3(), ".deeplake"), { recursive: true, mode: 448 });
|
|
393
|
+
let fd = null;
|
|
394
|
+
for (let attempt = 0; attempt < LOCK_RETRY_MAX; attempt++) {
|
|
395
|
+
try {
|
|
396
|
+
fd = openSync(path3, "wx", 384);
|
|
397
|
+
break;
|
|
398
|
+
} catch (e) {
|
|
399
|
+
const code = e.code;
|
|
400
|
+
if (code !== "EEXIST")
|
|
401
|
+
throw e;
|
|
402
|
+
try {
|
|
403
|
+
const age = Date.now() - statSync(path3).mtimeMs;
|
|
404
|
+
if (age > LOCK_STALE_MS) {
|
|
405
|
+
unlinkSync(path3);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
const delay = LOCK_RETRY_BASE_MS * (attempt + 1);
|
|
411
|
+
await sleep(delay);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (fd === null) {
|
|
415
|
+
log2(`lock acquisition gave up after ${LOCK_RETRY_MAX} attempts \u2014 proceeding unlocked (last-writer-wins)`);
|
|
416
|
+
return fn();
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
return fn();
|
|
420
|
+
} finally {
|
|
421
|
+
try {
|
|
422
|
+
closeSync(fd);
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
unlinkSync(path3);
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function sameDedupKey(a, b) {
|
|
432
|
+
if (a.id !== b.id)
|
|
433
|
+
return false;
|
|
434
|
+
return JSON.stringify(a.dedupKey) === JSON.stringify(b.dedupKey);
|
|
435
|
+
}
|
|
436
|
+
async function enqueueNotification(n) {
|
|
437
|
+
await withQueueLock(() => {
|
|
438
|
+
const q = readQueue();
|
|
439
|
+
if (q.queue.some((existing) => sameDedupKey(existing, n))) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
q.queue.push(n);
|
|
443
|
+
writeQueue(q);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// dist/src/commands/auth-creds.js
|
|
448
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
449
|
+
import { join as join4 } from "node:path";
|
|
450
|
+
import { homedir as homedir4 } from "node:os";
|
|
451
|
+
function configDir() {
|
|
452
|
+
return join4(homedir4(), ".deeplake");
|
|
453
|
+
}
|
|
454
|
+
function credsPath() {
|
|
455
|
+
return join4(configDir(), "credentials.json");
|
|
456
|
+
}
|
|
457
|
+
function loadCredentials() {
|
|
458
|
+
try {
|
|
459
|
+
return JSON.parse(readFileSync3(credsPath(), "utf-8"));
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// dist/src/deeplake-api.js
|
|
466
|
+
var indexMarkerStorePromise = null;
|
|
467
|
+
function getIndexMarkerStore() {
|
|
468
|
+
if (!indexMarkerStorePromise)
|
|
469
|
+
indexMarkerStorePromise = Promise.resolve().then(() => (init_index_marker_store(), index_marker_store_exports));
|
|
470
|
+
return indexMarkerStorePromise;
|
|
471
|
+
}
|
|
472
|
+
var log3 = (msg) => log("sdk", msg);
|
|
473
|
+
function summarizeSql(sql, maxLen = 220) {
|
|
474
|
+
const compact = sql.replace(/\s+/g, " ").trim();
|
|
475
|
+
return compact.length > maxLen ? `${compact.slice(0, maxLen)}...` : compact;
|
|
476
|
+
}
|
|
477
|
+
function traceSql(msg) {
|
|
478
|
+
const traceEnabled = process.env.HIVEMIND_TRACE_SQL === "1" || process.env.HIVEMIND_DEBUG === "1";
|
|
479
|
+
if (!traceEnabled)
|
|
480
|
+
return;
|
|
481
|
+
process.stderr.write(`[deeplake-sql] ${msg}
|
|
482
|
+
`);
|
|
483
|
+
if (process.env.HIVEMIND_DEBUG === "1")
|
|
484
|
+
log3(msg);
|
|
485
|
+
}
|
|
486
|
+
var _signalledBalanceExhausted = false;
|
|
487
|
+
function maybeSignalBalanceExhausted(status, bodyText) {
|
|
488
|
+
if (status !== 402)
|
|
489
|
+
return;
|
|
490
|
+
if (!bodyText.includes("balance_cents"))
|
|
491
|
+
return;
|
|
492
|
+
if (_signalledBalanceExhausted)
|
|
493
|
+
return;
|
|
494
|
+
_signalledBalanceExhausted = true;
|
|
495
|
+
log3(`balance exhausted \u2014 enqueuing session-start banner (body=${bodyText.slice(0, 120)})`);
|
|
496
|
+
enqueueNotification({
|
|
497
|
+
id: "balance-exhausted",
|
|
498
|
+
severity: "warn",
|
|
499
|
+
transient: true,
|
|
500
|
+
title: "Hivemind credits exhausted \u2014 top up to keep capturing",
|
|
501
|
+
body: `Sessions are not being saved and memory recall is returning empty. Top up at ${billingUrl()} to restore capture and recall.`,
|
|
502
|
+
dedupKey: { reason: "balance-zero" },
|
|
503
|
+
// User-facing billing notice → user channel only. Never the model's
|
|
504
|
+
// additionalContext: a "top up at <url>" instruction in the agent prompt
|
|
505
|
+
// is a prompt-injection pattern external agents flag.
|
|
506
|
+
userVisibleOnly: true
|
|
507
|
+
}).catch((e) => {
|
|
508
|
+
log3(`enqueue balance-exhausted failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function billingUrl() {
|
|
512
|
+
try {
|
|
513
|
+
const c = loadCredentials();
|
|
514
|
+
if (c?.orgName && c?.workspaceId) {
|
|
515
|
+
return `https://deeplake.ai/${encodeURIComponent(c.orgName)}/workspace/${encodeURIComponent(c.workspaceId)}/billing`;
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
return "https://deeplake.ai";
|
|
520
|
+
}
|
|
521
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
522
|
+
var MAX_RETRIES = 3;
|
|
523
|
+
var BASE_DELAY_MS = 500;
|
|
524
|
+
var MAX_CONCURRENCY = 5;
|
|
525
|
+
function getQueryTimeoutMs() {
|
|
526
|
+
return Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
527
|
+
}
|
|
528
|
+
function sleep2(ms) {
|
|
529
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
530
|
+
}
|
|
531
|
+
function isTimeoutError(error) {
|
|
532
|
+
const name = error instanceof Error ? error.name.toLowerCase() : "";
|
|
533
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
534
|
+
return name.includes("timeout") || name === "aborterror" || message.includes("timeout") || message.includes("timed out");
|
|
535
|
+
}
|
|
536
|
+
function isDuplicateIndexError(error) {
|
|
537
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
538
|
+
return message.includes("duplicate key value violates unique constraint") || message.includes("pg_class_relname_nsp_index") || message.includes("already exists");
|
|
539
|
+
}
|
|
540
|
+
function isSessionInsertQuery(sql) {
|
|
541
|
+
return /^\s*insert\s+into\s+"[^"]+"\s*\(\s*id\s*,\s*path\s*,\s*filename\s*,\s*message\s*,/i.test(sql);
|
|
542
|
+
}
|
|
543
|
+
function isTransientHtml403(text) {
|
|
544
|
+
const body = text.toLowerCase();
|
|
545
|
+
return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
|
|
546
|
+
}
|
|
547
|
+
var Semaphore = class {
|
|
548
|
+
max;
|
|
549
|
+
waiting = [];
|
|
550
|
+
active = 0;
|
|
551
|
+
constructor(max) {
|
|
552
|
+
this.max = max;
|
|
553
|
+
}
|
|
554
|
+
async acquire() {
|
|
555
|
+
if (this.active < this.max) {
|
|
556
|
+
this.active++;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
await new Promise((resolve3) => this.waiting.push(resolve3));
|
|
560
|
+
}
|
|
561
|
+
release() {
|
|
562
|
+
this.active--;
|
|
563
|
+
const next = this.waiting.shift();
|
|
564
|
+
if (next) {
|
|
565
|
+
this.active++;
|
|
566
|
+
next();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
var DeeplakeApi = class {
|
|
571
|
+
token;
|
|
572
|
+
apiUrl;
|
|
573
|
+
orgId;
|
|
574
|
+
workspaceId;
|
|
575
|
+
tableName;
|
|
576
|
+
_pendingRows = [];
|
|
577
|
+
_sem = new Semaphore(MAX_CONCURRENCY);
|
|
578
|
+
_tablesCache = null;
|
|
579
|
+
constructor(token, apiUrl, orgId, workspaceId, tableName) {
|
|
580
|
+
this.token = token;
|
|
581
|
+
this.apiUrl = apiUrl;
|
|
582
|
+
this.orgId = orgId;
|
|
583
|
+
this.workspaceId = workspaceId;
|
|
584
|
+
this.tableName = tableName;
|
|
585
|
+
}
|
|
586
|
+
/** Execute SQL with retry on transient errors and bounded concurrency. */
|
|
587
|
+
async query(sql) {
|
|
588
|
+
const startedAt = Date.now();
|
|
589
|
+
const summary = summarizeSql(sql);
|
|
590
|
+
traceSql(`query start: ${summary}`);
|
|
591
|
+
await this._sem.acquire();
|
|
592
|
+
try {
|
|
593
|
+
const rows = await this._queryWithRetry(sql);
|
|
594
|
+
traceSql(`query ok (${Date.now() - startedAt}ms, rows=${rows.length}): ${summary}`);
|
|
595
|
+
return rows;
|
|
596
|
+
} catch (e) {
|
|
597
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
598
|
+
traceSql(`query fail (${Date.now() - startedAt}ms): ${summary} :: ${message}`);
|
|
599
|
+
throw e;
|
|
600
|
+
} finally {
|
|
601
|
+
this._sem.release();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async _queryWithRetry(sql) {
|
|
605
|
+
let lastError;
|
|
606
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
607
|
+
let resp;
|
|
608
|
+
const timeoutMs = getQueryTimeoutMs();
|
|
609
|
+
try {
|
|
610
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
611
|
+
resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables/query`, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: {
|
|
614
|
+
Authorization: `Bearer ${this.token}`,
|
|
615
|
+
"Content-Type": "application/json",
|
|
616
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
617
|
+
...deeplakeClientHeader()
|
|
618
|
+
},
|
|
619
|
+
signal,
|
|
620
|
+
body: JSON.stringify({ query: sql })
|
|
621
|
+
});
|
|
622
|
+
} catch (e) {
|
|
623
|
+
if (isTimeoutError(e)) {
|
|
624
|
+
lastError = new Error(`Query timeout after ${timeoutMs}ms`);
|
|
625
|
+
throw lastError;
|
|
626
|
+
}
|
|
627
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
628
|
+
if (attempt < MAX_RETRIES) {
|
|
629
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
630
|
+
log3(`query retry ${attempt + 1}/${MAX_RETRIES} (fetch error: ${lastError.message}) in ${delay.toFixed(0)}ms`);
|
|
631
|
+
await sleep2(delay);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
throw lastError;
|
|
635
|
+
}
|
|
636
|
+
if (resp.ok) {
|
|
637
|
+
const raw = await resp.json();
|
|
638
|
+
if (!raw?.rows || !raw?.columns)
|
|
639
|
+
return [];
|
|
640
|
+
return raw.rows.map((row) => Object.fromEntries(raw.columns.map((col, i) => [col, row[i]])));
|
|
641
|
+
}
|
|
642
|
+
const text = await resp.text().catch(() => "");
|
|
643
|
+
const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
|
|
644
|
+
const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
|
|
645
|
+
if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
|
|
646
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
647
|
+
log3(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
|
|
648
|
+
await sleep2(delay);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
maybeSignalBalanceExhausted(resp.status, text);
|
|
652
|
+
throw new Error(`Query failed: ${resp.status}: ${text.slice(0, 200)}`);
|
|
653
|
+
}
|
|
654
|
+
throw lastError ?? new Error("Query failed: max retries exceeded");
|
|
655
|
+
}
|
|
656
|
+
// ── Writes ──────────────────────────────────────────────────────────────────
|
|
657
|
+
/** Queue rows for writing. Call commit() to flush. */
|
|
658
|
+
appendRows(rows) {
|
|
659
|
+
this._pendingRows.push(...rows);
|
|
660
|
+
}
|
|
661
|
+
/** Flush pending rows via SQL. */
|
|
662
|
+
async commit() {
|
|
663
|
+
if (this._pendingRows.length === 0)
|
|
664
|
+
return;
|
|
665
|
+
const rows = this._pendingRows;
|
|
666
|
+
this._pendingRows = [];
|
|
667
|
+
const CONCURRENCY = 10;
|
|
668
|
+
for (let i = 0; i < rows.length; i += CONCURRENCY) {
|
|
669
|
+
const chunk = rows.slice(i, i + CONCURRENCY);
|
|
670
|
+
await Promise.allSettled(chunk.map((r) => this.upsertRowSql(r)));
|
|
671
|
+
}
|
|
672
|
+
log3(`commit: ${rows.length} rows`);
|
|
673
|
+
}
|
|
674
|
+
async upsertRowSql(row) {
|
|
675
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
676
|
+
const cd = row.creationDate ?? ts;
|
|
677
|
+
const lud = row.lastUpdateDate ?? ts;
|
|
678
|
+
const exists = await this.query(`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`);
|
|
679
|
+
if (exists.length > 0) {
|
|
680
|
+
let setClauses = `summary = E'${sqlStr(row.contentText)}', ${SUMMARY_EMBEDDING_COL} = NULL, mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
681
|
+
if (row.project !== void 0)
|
|
682
|
+
setClauses += `, project = '${sqlStr(row.project)}'`;
|
|
683
|
+
if (row.description !== void 0)
|
|
684
|
+
setClauses += `, description = '${sqlStr(row.description)}'`;
|
|
685
|
+
await this.query(`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(row.path)}'`);
|
|
686
|
+
} else {
|
|
687
|
+
const id = randomUUID();
|
|
688
|
+
let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
|
|
689
|
+
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
690
|
+
if (row.project !== void 0) {
|
|
691
|
+
cols += ", project";
|
|
692
|
+
vals += `, '${sqlStr(row.project)}'`;
|
|
693
|
+
}
|
|
694
|
+
if (row.description !== void 0) {
|
|
695
|
+
cols += ", description";
|
|
696
|
+
vals += `, '${sqlStr(row.description)}'`;
|
|
697
|
+
}
|
|
698
|
+
await this.query(`INSERT INTO "${this.tableName}" (${cols}) VALUES (${vals})`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/** Update specific columns on a row by path. */
|
|
702
|
+
async updateColumns(path3, columns) {
|
|
703
|
+
const setClauses = Object.entries(columns).map(([col, val]) => typeof val === "number" ? `${col} = ${val}` : `${col} = '${sqlStr(String(val))}'`).join(", ");
|
|
704
|
+
await this.query(`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(path3)}'`);
|
|
705
|
+
}
|
|
706
|
+
// ── Convenience ─────────────────────────────────────────────────────────────
|
|
707
|
+
/** Create a BM25 search index on a column. */
|
|
708
|
+
async createIndex(column) {
|
|
709
|
+
await this.query(`CREATE INDEX IF NOT EXISTS idx_${sqlStr(column)}_bm25 ON "${this.tableName}" USING deeplake_index ("${column}")`);
|
|
710
|
+
}
|
|
711
|
+
buildLookupIndexName(table, suffix) {
|
|
712
|
+
return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
713
|
+
}
|
|
714
|
+
async ensureLookupIndex(table, suffix, columnsSql) {
|
|
715
|
+
const markers = await getIndexMarkerStore();
|
|
716
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
|
|
717
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
718
|
+
return;
|
|
719
|
+
const indexName = this.buildLookupIndexName(table, suffix);
|
|
720
|
+
try {
|
|
721
|
+
await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
|
|
722
|
+
markers.writeIndexMarker(markerPath);
|
|
723
|
+
} catch (e) {
|
|
724
|
+
if (isDuplicateIndexError(e)) {
|
|
725
|
+
markers.writeIndexMarker(markerPath);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
log3(`index "${indexName}" skipped: ${e.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Heal any missing columns on a table so it matches one of the schema
|
|
733
|
+
* definitions in `deeplake-schema.ts`. One SELECT against
|
|
734
|
+
* `information_schema.columns` per call, then `ALTER TABLE ADD COLUMN`
|
|
735
|
+
* only the genuinely missing ones — never blanket, never `IF NOT
|
|
736
|
+
* EXISTS`.
|
|
737
|
+
*
|
|
738
|
+
* History: an earlier path used a local marker file (`col_<name>` under
|
|
739
|
+
* the index-marker dir) to skip even the SELECT after the first
|
|
740
|
+
* confirmation, plus per-column ALTERs for `summary_embedding`,
|
|
741
|
+
* `message_embedding`, `agent`, `plugin_version`. The marker existed
|
|
742
|
+
* because Deeplake used to expose a ~30s post-ALTER bug where
|
|
743
|
+
* subsequent INSERTs failed, so we wanted to keep ALTER traffic to a
|
|
744
|
+
* minimum. The bug was re-verified on 2026-05-18 against
|
|
745
|
+
* `api.deeplake.ai` (`test_plugin` org) and no longer reproduces
|
|
746
|
+
* (71/71 INSERTs OK, first success 2ms after ALTER). The single SELECT
|
|
747
|
+
* + targeted ALTER pattern survives the marker removal because: each
|
|
748
|
+
* ALTER still costs ~800ms (so blanket sweeps are wasteful) and the
|
|
749
|
+
* diff produces clearer logs than "ALTER all with IF NOT EXISTS".
|
|
750
|
+
*/
|
|
751
|
+
async healSchema(table, columns) {
|
|
752
|
+
await healMissingColumns({
|
|
753
|
+
query: (sql) => this.query(sql),
|
|
754
|
+
tableName: table,
|
|
755
|
+
workspaceId: this.workspaceId,
|
|
756
|
+
columns,
|
|
757
|
+
log: log3
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
/** List all tables in the workspace (with retry). */
|
|
761
|
+
async listTables(forceRefresh = false) {
|
|
762
|
+
if (!forceRefresh && this._tablesCache)
|
|
763
|
+
return [...this._tablesCache];
|
|
764
|
+
const { tables, cacheable } = await this._fetchTables();
|
|
765
|
+
if (cacheable)
|
|
766
|
+
this._tablesCache = [...tables];
|
|
767
|
+
return tables;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Like listTables() but returns null when the list could NOT be trusted
|
|
771
|
+
* (the fetch failed / was non-cacheable). Callers gating a read on table
|
|
772
|
+
* existence use this to tell a genuinely-empty workspace ([]) apart from a
|
|
773
|
+
* failed lookup (null): on [] they can safely skip the read (no table → no
|
|
774
|
+
* 42P01), on null they must fall back to SELECT-then-catch so a transient
|
|
775
|
+
* lookup blip doesn't drop a read of a table that really exists.
|
|
776
|
+
*/
|
|
777
|
+
async knownTablesOrNull() {
|
|
778
|
+
if (this._tablesCache)
|
|
779
|
+
return [...this._tablesCache];
|
|
780
|
+
const { tables, cacheable } = await this._fetchTables();
|
|
781
|
+
if (!cacheable)
|
|
782
|
+
return null;
|
|
783
|
+
this._tablesCache = [...tables];
|
|
784
|
+
return [...tables];
|
|
785
|
+
}
|
|
786
|
+
async _fetchTables() {
|
|
787
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
788
|
+
try {
|
|
789
|
+
const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
|
|
790
|
+
headers: {
|
|
791
|
+
Authorization: `Bearer ${this.token}`,
|
|
792
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
793
|
+
...deeplakeClientHeader()
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (resp.ok) {
|
|
797
|
+
const data = await resp.json();
|
|
798
|
+
return {
|
|
799
|
+
tables: (data.tables ?? []).map((t) => t.table_name),
|
|
800
|
+
cacheable: true
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (attempt < MAX_RETRIES && RETRYABLE_CODES.has(resp.status)) {
|
|
804
|
+
await sleep2(BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
return { tables: [], cacheable: false };
|
|
808
|
+
} catch {
|
|
809
|
+
if (attempt < MAX_RETRIES) {
|
|
810
|
+
await sleep2(BASE_DELAY_MS * Math.pow(2, attempt));
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
return { tables: [], cacheable: false };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return { tables: [], cacheable: false };
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Run a `CREATE TABLE` with an extra outer retry budget. The base
|
|
820
|
+
* `query()` already retries 3 times on fetch errors (~3.5s total), but a
|
|
821
|
+
* failed CREATE is permanent corruption — every subsequent SELECT against
|
|
822
|
+
* the missing table fails. Wrapping in an outer loop with longer backoff
|
|
823
|
+
* (2s, 5s, then 10s) gives us ~17s of reach across transient network
|
|
824
|
+
* blips before giving up. Failures still propagate; getApi() resets its
|
|
825
|
+
* cache on init failure (openclaw plugin) so the next call retries the
|
|
826
|
+
* whole init flow.
|
|
827
|
+
*/
|
|
828
|
+
async createTableWithRetry(sql, label) {
|
|
829
|
+
const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
|
|
830
|
+
let lastErr = null;
|
|
831
|
+
for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
|
|
832
|
+
try {
|
|
833
|
+
await this.query(sql);
|
|
834
|
+
return;
|
|
835
|
+
} catch (err) {
|
|
836
|
+
lastErr = err;
|
|
837
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
838
|
+
log3(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
|
|
839
|
+
if (attempt < OUTER_BACKOFFS_MS.length) {
|
|
840
|
+
await sleep2(OUTER_BACKOFFS_MS[attempt]);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
throw lastErr;
|
|
845
|
+
}
|
|
846
|
+
/** Create the memory table if it doesn't already exist. Heal missing columns on existing tables. */
|
|
847
|
+
async ensureTable(name) {
|
|
848
|
+
if (!MEMORY_COLUMNS.some((c) => c.name === SUMMARY_EMBEDDING_COL)) {
|
|
849
|
+
throw new Error(`MEMORY_COLUMNS missing "${SUMMARY_EMBEDDING_COL}" (embeddings/columns.ts drift)`);
|
|
850
|
+
}
|
|
851
|
+
const tbl = sqlIdent(name ?? this.tableName);
|
|
852
|
+
const tables = await this.listTables();
|
|
853
|
+
if (!tables.includes(tbl)) {
|
|
854
|
+
log3(`table "${tbl}" not found, creating`);
|
|
855
|
+
await this.createTableWithRetry(buildCreateTableSql(tbl, MEMORY_COLUMNS), tbl);
|
|
856
|
+
log3(`table "${tbl}" created`);
|
|
857
|
+
if (!tables.includes(tbl))
|
|
858
|
+
this._tablesCache = [...tables, tbl];
|
|
859
|
+
}
|
|
860
|
+
await this.healSchema(tbl, MEMORY_COLUMNS);
|
|
861
|
+
}
|
|
862
|
+
/** Create the sessions table (uses JSONB for message since every row is a JSON event). */
|
|
863
|
+
async ensureSessionsTable(name) {
|
|
864
|
+
const safe = sqlIdent(name);
|
|
865
|
+
const tables = await this.listTables();
|
|
866
|
+
if (!tables.includes(safe)) {
|
|
867
|
+
log3(`table "${safe}" not found, creating`);
|
|
868
|
+
await this.createTableWithRetry(buildCreateTableSql(safe, SESSIONS_COLUMNS), safe);
|
|
869
|
+
log3(`table "${safe}" created`);
|
|
870
|
+
if (!tables.includes(safe))
|
|
871
|
+
this._tablesCache = [...tables, safe];
|
|
872
|
+
}
|
|
873
|
+
await this.healSchema(safe, SESSIONS_COLUMNS);
|
|
874
|
+
await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Create the skills table.
|
|
878
|
+
*
|
|
879
|
+
* One row per skill version. Workers INSERT a fresh row on every KEEP /
|
|
880
|
+
* MERGE rather than UPDATE-ing in place, so the full version history is
|
|
881
|
+
* recoverable. Uniqueness in the *current* state is by (project_key, name)
|
|
882
|
+
* — newer rows shadow older ones at read time (ORDER BY version DESC).
|
|
883
|
+
* This sidesteps the Deeplake UPDATE-coalescing quirk that bit the wiki
|
|
884
|
+
* worker.
|
|
885
|
+
*/
|
|
886
|
+
/**
|
|
887
|
+
* Create the codebase table. One row per (org, workspace, repo, user,
|
|
888
|
+
* worktree, commit) — see CODEBASE_COLUMNS for the schema. Healing
|
|
889
|
+
* + index follow the same pattern as ensureSessionsTable.
|
|
890
|
+
*/
|
|
891
|
+
async ensureCodebaseTable(name) {
|
|
892
|
+
const safe = sqlIdent(name);
|
|
893
|
+
const tables = await this.listTables();
|
|
894
|
+
if (!tables.includes(safe)) {
|
|
895
|
+
log3(`table "${safe}" not found, creating`);
|
|
896
|
+
await this.createTableWithRetry(buildCreateTableSql(safe, CODEBASE_COLUMNS), safe);
|
|
897
|
+
log3(`table "${safe}" created`);
|
|
898
|
+
if (!tables.includes(safe))
|
|
899
|
+
this._tablesCache = [...tables, safe];
|
|
900
|
+
}
|
|
901
|
+
await this.healSchema(safe, CODEBASE_COLUMNS);
|
|
902
|
+
await this.ensureLookupIndex(safe, "codebase_identity", `("org_id", "workspace_id", "repo_slug", "user_id", "worktree_id", "commit_sha")`);
|
|
903
|
+
}
|
|
904
|
+
async ensureSkillsTable(name) {
|
|
905
|
+
const safe = sqlIdent(name);
|
|
906
|
+
const tables = await this.listTables();
|
|
907
|
+
if (!tables.includes(safe)) {
|
|
908
|
+
log3(`table "${safe}" not found, creating`);
|
|
909
|
+
await this.createTableWithRetry(buildCreateTableSql(safe, SKILLS_COLUMNS), safe);
|
|
910
|
+
log3(`table "${safe}" created`);
|
|
911
|
+
if (!tables.includes(safe))
|
|
912
|
+
this._tablesCache = [...tables, safe];
|
|
913
|
+
}
|
|
914
|
+
await this.healSchema(safe, SKILLS_COLUMNS);
|
|
915
|
+
await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Create the rules table.
|
|
919
|
+
*
|
|
920
|
+
* One row per rule version (same write pattern as skills): edits INSERT
|
|
921
|
+
* a fresh row with version+1, reads pick latest per rule_id via
|
|
922
|
+
* `ORDER BY version DESC LIMIT 1`. Sidesteps the Deeplake
|
|
923
|
+
* UPDATE-coalescing quirk by never UPDATEing.
|
|
924
|
+
*/
|
|
925
|
+
async ensureRulesTable(name) {
|
|
926
|
+
const safe = sqlIdent(name);
|
|
927
|
+
const tables = await this.listTables();
|
|
928
|
+
if (!tables.includes(safe)) {
|
|
929
|
+
log3(`table "${safe}" not found, creating`);
|
|
930
|
+
await this.createTableWithRetry(buildCreateTableSql(safe, RULES_COLUMNS), safe);
|
|
931
|
+
log3(`table "${safe}" created`);
|
|
932
|
+
if (!tables.includes(safe))
|
|
933
|
+
this._tablesCache = [...tables, safe];
|
|
934
|
+
}
|
|
935
|
+
await this.healSchema(safe, RULES_COLUMNS);
|
|
936
|
+
await this.ensureLookupIndex(safe, "rule_id_version", `("rule_id", "version")`);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Create the goals table.
|
|
940
|
+
*
|
|
941
|
+
* Backed by the VFS path convention memory/goal/<owner>/<status>/<goal_id>.md.
|
|
942
|
+
* INSERT-only version-bumped: rm and mv operations translate to fresh
|
|
943
|
+
* v=N+1 rows (status flips for mv → closed; rm is the same soft-close).
|
|
944
|
+
* The (goal_id, version) index lets the VFS dispatch a cheap latest-row
|
|
945
|
+
* read on cat / Read of a single goal.
|
|
946
|
+
*/
|
|
947
|
+
async ensureGoalsTable(name) {
|
|
948
|
+
const safe = sqlIdent(name);
|
|
949
|
+
const tables = await this.listTables();
|
|
950
|
+
if (!tables.includes(safe)) {
|
|
951
|
+
log3(`table "${safe}" not found, creating`);
|
|
952
|
+
await this.createTableWithRetry(buildCreateTableSql(safe, GOALS_COLUMNS), safe);
|
|
953
|
+
log3(`table "${safe}" created`);
|
|
954
|
+
if (!tables.includes(safe))
|
|
955
|
+
this._tablesCache = [...tables, safe];
|
|
956
|
+
}
|
|
957
|
+
await this.healSchema(safe, GOALS_COLUMNS);
|
|
958
|
+
await this.ensureLookupIndex(safe, "goal_id_version", `("goal_id", "version")`);
|
|
959
|
+
await this.ensureLookupIndex(safe, "owner_status", `("owner", "status")`);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Create the kpis table.
|
|
963
|
+
*
|
|
964
|
+
* Backed by memory/kpi/<goal_id>/<kpi_id>.md. KPI rows do NOT carry
|
|
965
|
+
* owner — ownership derives from the parent goal via logical join on
|
|
966
|
+
* goal_id. INSERT-only version-bumped. (goal_id, kpi_id) index is the
|
|
967
|
+
* canonical lookup the VFS uses on Read and Write.
|
|
968
|
+
*/
|
|
969
|
+
async ensureKpisTable(name) {
|
|
970
|
+
const safe = sqlIdent(name);
|
|
971
|
+
const tables = await this.listTables();
|
|
972
|
+
if (!tables.includes(safe)) {
|
|
973
|
+
log3(`table "${safe}" not found, creating`);
|
|
974
|
+
await this.createTableWithRetry(buildCreateTableSql(safe, KPIS_COLUMNS), safe);
|
|
975
|
+
log3(`table "${safe}" created`);
|
|
976
|
+
if (!tables.includes(safe))
|
|
977
|
+
this._tablesCache = [...tables, safe];
|
|
978
|
+
}
|
|
979
|
+
await this.healSchema(safe, KPIS_COLUMNS);
|
|
980
|
+
await this.ensureLookupIndex(safe, "goal_id_kpi_id", `("goal_id", "kpi_id")`);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// dist/src/skillify/state-dir.js
|
|
985
|
+
import { homedir as homedir5 } from "node:os";
|
|
986
|
+
import { join as join6 } from "node:path";
|
|
987
|
+
function getStateDir() {
|
|
988
|
+
const override = process.env.HIVEMIND_STATE_DIR?.trim();
|
|
989
|
+
return override && override.length > 0 ? override : join6(homedir5(), ".deeplake", "state", "skillify");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// dist/src/skillify/agent-model.js
|
|
993
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
994
|
+
|
|
995
|
+
// dist/src/skillify/gate-runner.js
|
|
996
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
997
|
+
import { createRequire } from "node:module";
|
|
998
|
+
import { homedir as homedir6 } from "node:os";
|
|
999
|
+
import { join as join7 } from "node:path";
|
|
1000
|
+
var requireForCp = createRequire(import.meta.url);
|
|
1001
|
+
var { execFileSync: runChildProcess } = requireForCp("node:child_process");
|
|
1002
|
+
var inheritedEnv = process;
|
|
1003
|
+
function firstExistingPath(candidates) {
|
|
1004
|
+
for (const c of candidates) {
|
|
1005
|
+
if (existsSync3(c))
|
|
1006
|
+
return c;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
function findAgentBin(agent) {
|
|
1011
|
+
const home = homedir6();
|
|
1012
|
+
switch (agent) {
|
|
1013
|
+
// /usr/bin/<name> is included in every candidate list — that's the
|
|
1014
|
+
// common Linux package-manager install path (apt, dnf, pacman). Old
|
|
1015
|
+
// code used `which` which always checked it; the static-scan fix
|
|
1016
|
+
// dropped `which`, so /usr/bin needs to be explicit. CodeRabbit on
|
|
1017
|
+
// #170 caught the gap.
|
|
1018
|
+
case "claude_code":
|
|
1019
|
+
return firstExistingPath([
|
|
1020
|
+
join7(home, ".claude", "local", "claude"),
|
|
1021
|
+
"/usr/local/bin/claude",
|
|
1022
|
+
"/usr/bin/claude",
|
|
1023
|
+
join7(home, ".npm-global", "bin", "claude"),
|
|
1024
|
+
join7(home, ".local", "bin", "claude"),
|
|
1025
|
+
"/opt/homebrew/bin/claude"
|
|
1026
|
+
]) ?? join7(home, ".claude", "local", "claude");
|
|
1027
|
+
case "codex":
|
|
1028
|
+
return firstExistingPath([
|
|
1029
|
+
"/usr/local/bin/codex",
|
|
1030
|
+
"/usr/bin/codex",
|
|
1031
|
+
join7(home, ".npm-global", "bin", "codex"),
|
|
1032
|
+
join7(home, ".local", "bin", "codex"),
|
|
1033
|
+
"/opt/homebrew/bin/codex"
|
|
1034
|
+
]) ?? "/usr/local/bin/codex";
|
|
1035
|
+
case "cursor":
|
|
1036
|
+
return firstExistingPath([
|
|
1037
|
+
"/usr/local/bin/cursor-agent",
|
|
1038
|
+
"/usr/bin/cursor-agent",
|
|
1039
|
+
join7(home, ".npm-global", "bin", "cursor-agent"),
|
|
1040
|
+
join7(home, ".local", "bin", "cursor-agent"),
|
|
1041
|
+
"/opt/homebrew/bin/cursor-agent"
|
|
1042
|
+
]) ?? "/usr/local/bin/cursor-agent";
|
|
1043
|
+
case "hermes":
|
|
1044
|
+
return firstExistingPath([
|
|
1045
|
+
join7(home, ".local", "bin", "hermes"),
|
|
1046
|
+
"/usr/local/bin/hermes",
|
|
1047
|
+
"/usr/bin/hermes",
|
|
1048
|
+
join7(home, ".npm-global", "bin", "hermes"),
|
|
1049
|
+
"/opt/homebrew/bin/hermes"
|
|
1050
|
+
]) ?? join7(home, ".local", "bin", "hermes");
|
|
1051
|
+
case "pi":
|
|
1052
|
+
return firstExistingPath([
|
|
1053
|
+
join7(home, ".local", "bin", "pi"),
|
|
1054
|
+
"/usr/local/bin/pi",
|
|
1055
|
+
"/usr/bin/pi",
|
|
1056
|
+
join7(home, ".npm-global", "bin", "pi"),
|
|
1057
|
+
"/opt/homebrew/bin/pi"
|
|
1058
|
+
]) ?? join7(home, ".local", "bin", "pi");
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// dist/src/skillify/skillopt-env.js
|
|
1063
|
+
var SKILLOPT_ENV = {
|
|
1064
|
+
/** User-set kill switch: "1" disables the whole trigger. */
|
|
1065
|
+
DISABLED: "HIVEMIND_SKILLOPT_DISABLED",
|
|
1066
|
+
/** Recursion guard the trigger sets on the spawned worker so the worker can't re-arm. */
|
|
1067
|
+
WORKER: "HIVEMIND_SKILLOPT_WORKER",
|
|
1068
|
+
/** Worker inputs, handed trigger → worker via the child env. */
|
|
1069
|
+
SESSION: "HIVEMIND_SKILLOPT_SESSION",
|
|
1070
|
+
SKILL: "HIVEMIND_SKILLOPT_SKILL",
|
|
1071
|
+
REACTION: "HIVEMIND_SKILLOPT_REACTION",
|
|
1072
|
+
TOOL_USE_ID: "HIVEMIND_SKILLOPT_TOOL_USE_ID",
|
|
1073
|
+
/** Which agent's CLI runs the judge/proposer (claude_code/codex/hermes/cursor/pi). */
|
|
1074
|
+
AGENT: "HIVEMIND_SKILLOPT_AGENT",
|
|
1075
|
+
/** K-message judgment-window size override. */
|
|
1076
|
+
JUDGE_WINDOW: "HIVEMIND_SKILLOPT_JUDGE_WINDOW"
|
|
1077
|
+
};
|
|
1078
|
+
var SKILLOPT_ENV_PREFIX = "HIVEMIND_SKILLOPT_";
|
|
1079
|
+
function modelEnvNames(agent, role) {
|
|
1080
|
+
const A = agent.toUpperCase();
|
|
1081
|
+
return [`${SKILLOPT_ENV_PREFIX}${A}_${role.toUpperCase()}_MODEL`, `${SKILLOPT_ENV_PREFIX}${A}_MODEL`];
|
|
1082
|
+
}
|
|
1083
|
+
function providerEnvName(agent) {
|
|
1084
|
+
return `${SKILLOPT_ENV_PREFIX}${agent.toUpperCase()}_PROVIDER`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// dist/src/skillify/agent-model.js
|
|
1088
|
+
var fold = (system, user) => `${system}
|
|
1089
|
+
|
|
1090
|
+
${user}`;
|
|
1091
|
+
var DISPATCH = {
|
|
1092
|
+
claude_code: {
|
|
1093
|
+
// --tools "" = empty allow-list = NO tools (authoritative over built-ins AND MCP);
|
|
1094
|
+
// --strict-mcp-config ignores user MCP entirely. The verified-safe no-tools path.
|
|
1095
|
+
buildArgs: (model, _p, system, user) => [
|
|
1096
|
+
"-p",
|
|
1097
|
+
user,
|
|
1098
|
+
"--model",
|
|
1099
|
+
model ?? "sonnet",
|
|
1100
|
+
"--no-session-persistence",
|
|
1101
|
+
"--output-format",
|
|
1102
|
+
"json",
|
|
1103
|
+
"--system-prompt",
|
|
1104
|
+
system,
|
|
1105
|
+
"--tools",
|
|
1106
|
+
"",
|
|
1107
|
+
"--strict-mcp-config"
|
|
1108
|
+
],
|
|
1109
|
+
parse: (out) => {
|
|
1110
|
+
try {
|
|
1111
|
+
return String(JSON.parse(out).result ?? "");
|
|
1112
|
+
} catch {
|
|
1113
|
+
return out;
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
model: (role) => role === "judge" ? "haiku" : "sonnet"
|
|
1117
|
+
},
|
|
1118
|
+
codex: {
|
|
1119
|
+
// `-s read-only`: model-generated shell commands can't write/exec — the safest
|
|
1120
|
+
// codex-exec mode for untrusted prompt text. --skip-git-repo-check: the detached
|
|
1121
|
+
// worker isn't in a trusted git dir. No system-prompt flag → fold into the prompt.
|
|
1122
|
+
buildArgs: (model, _p, system, user) => [
|
|
1123
|
+
"exec",
|
|
1124
|
+
"--skip-git-repo-check",
|
|
1125
|
+
"-s",
|
|
1126
|
+
"read-only",
|
|
1127
|
+
...model ? ["-m", model] : [],
|
|
1128
|
+
fold(system, user)
|
|
1129
|
+
],
|
|
1130
|
+
parse: (out) => out,
|
|
1131
|
+
model: () => void 0
|
|
1132
|
+
// codex uses its configured default model
|
|
1133
|
+
},
|
|
1134
|
+
hermes: {
|
|
1135
|
+
// -z oneshot via the user's provider; --ignore-user-config drops user MCP/skills,
|
|
1136
|
+
// so an explicit -m/--provider is required (matches the wiki worker's defaults).
|
|
1137
|
+
// NOTE: the openrouter-style default model below is only valid for openrouter.
|
|
1138
|
+
// A user on another provider MUST set HIVEMIND_SKILLOPT_HERMES_PROVIDER + _MODEL
|
|
1139
|
+
// to a valid id — e.g. AWS Bedrock needs an INFERENCE-PROFILE id like
|
|
1140
|
+
// `us.anthropic.claude-haiku-4-5-20251001-v1:0` (a bare model id, or a legacy
|
|
1141
|
+
// one, is rejected by Bedrock and hermes swallows the error → empty output).
|
|
1142
|
+
buildArgs: (model, provider, system, user) => [
|
|
1143
|
+
"-z",
|
|
1144
|
+
fold(system, user),
|
|
1145
|
+
"--provider",
|
|
1146
|
+
provider ?? "openrouter",
|
|
1147
|
+
"-m",
|
|
1148
|
+
model ?? "anthropic/claude-haiku-4-5",
|
|
1149
|
+
"--yolo",
|
|
1150
|
+
"--ignore-user-config"
|
|
1151
|
+
],
|
|
1152
|
+
parse: (out) => out,
|
|
1153
|
+
model: () => void 0,
|
|
1154
|
+
// falls back to the buildArgs default
|
|
1155
|
+
provider: "openrouter"
|
|
1156
|
+
},
|
|
1157
|
+
cursor: {
|
|
1158
|
+
buildArgs: (model, _p, system, user) => [
|
|
1159
|
+
"--print",
|
|
1160
|
+
"--model",
|
|
1161
|
+
model ?? "auto",
|
|
1162
|
+
"--force",
|
|
1163
|
+
"--output-format",
|
|
1164
|
+
"text",
|
|
1165
|
+
fold(system, user)
|
|
1166
|
+
],
|
|
1167
|
+
parse: (out) => out,
|
|
1168
|
+
model: () => void 0
|
|
1169
|
+
},
|
|
1170
|
+
pi: {
|
|
1171
|
+
// The google/gemini default needs a Google API key. A user on another provider
|
|
1172
|
+
// MUST set HIVEMIND_SKILLOPT_PI_PROVIDER + _MODEL — e.g. AWS Bedrock uses provider
|
|
1173
|
+
// `amazon-bedrock` and an inference-profile model id like
|
|
1174
|
+
// `us.anthropic.claude-haiku-4-5-20251001-v1:0`. With a wrong default pi exits
|
|
1175
|
+
// non-zero ("No API key found") → surfaced loudly via the exit-code guard, not silent.
|
|
1176
|
+
buildArgs: (model, provider, system, user) => [
|
|
1177
|
+
"--print",
|
|
1178
|
+
"--provider",
|
|
1179
|
+
provider ?? "google",
|
|
1180
|
+
"--model",
|
|
1181
|
+
model ?? "gemini-2.5-flash",
|
|
1182
|
+
fold(system, user)
|
|
1183
|
+
],
|
|
1184
|
+
parse: (out) => out,
|
|
1185
|
+
model: () => void 0
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
function envModel(agent, role, env) {
|
|
1189
|
+
const [specific, fallback] = modelEnvNames(agent, role);
|
|
1190
|
+
return env[specific] ?? env[fallback];
|
|
1191
|
+
}
|
|
1192
|
+
function envProvider(agent, env) {
|
|
1193
|
+
return env[providerEnvName(agent)];
|
|
1194
|
+
}
|
|
1195
|
+
function agentModel(opts) {
|
|
1196
|
+
const env = opts.env ?? process.env;
|
|
1197
|
+
const d = DISPATCH[opts.agent];
|
|
1198
|
+
const modelOverride = opts.model ?? envModel(opts.agent, opts.role, env);
|
|
1199
|
+
const providerOverride = opts.provider ?? envProvider(opts.agent, env);
|
|
1200
|
+
const model = modelOverride ?? d.model(opts.role);
|
|
1201
|
+
const provider = providerOverride ?? d.provider;
|
|
1202
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1203
|
+
const spawnFn = opts.spawnImpl ?? nodeSpawn;
|
|
1204
|
+
const bin = opts.bin ?? findAgentBin(opts.agent);
|
|
1205
|
+
return (system, user) => new Promise((resolve3, reject) => {
|
|
1206
|
+
if (providerOverride && !modelOverride && (opts.agent === "hermes" || opts.agent === "pi")) {
|
|
1207
|
+
return reject(new Error(`${opts.agent}: provider overridden to '${provider}' without a model \u2014 set ${modelEnvNames(opts.agent, opts.role)[1]} to a valid id for that provider`));
|
|
1208
|
+
}
|
|
1209
|
+
const args = d.buildArgs(model, provider, system, user);
|
|
1210
|
+
const child = spawnFn(bin, args, {
|
|
1211
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1212
|
+
env: { ...env, HIVEMIND_CAPTURE: "false", HIVEMIND_WIKI_WORKER: "1" }
|
|
1213
|
+
});
|
|
1214
|
+
let out = "";
|
|
1215
|
+
let err = "";
|
|
1216
|
+
const timer = setTimeout(() => {
|
|
1217
|
+
child.kill("SIGKILL");
|
|
1218
|
+
reject(new Error(`${opts.agent} timed out`));
|
|
1219
|
+
}, timeoutMs);
|
|
1220
|
+
child.stdout?.on("data", (x) => {
|
|
1221
|
+
out += String(x);
|
|
1222
|
+
});
|
|
1223
|
+
child.stderr?.on("data", (x) => {
|
|
1224
|
+
err += String(x);
|
|
1225
|
+
});
|
|
1226
|
+
child.on("error", (e) => {
|
|
1227
|
+
clearTimeout(timer);
|
|
1228
|
+
reject(e);
|
|
1229
|
+
});
|
|
1230
|
+
child.on("close", (code) => {
|
|
1231
|
+
clearTimeout(timer);
|
|
1232
|
+
if (code !== 0)
|
|
1233
|
+
return reject(new Error(`${opts.agent} exit ${code}: ${err.slice(0, 200)}`));
|
|
1234
|
+
const text = d.parse(out);
|
|
1235
|
+
if (!text.trim())
|
|
1236
|
+
return reject(new Error(`${opts.agent} returned empty output (exit 0) \u2014 misconfigured provider/model?`));
|
|
1237
|
+
resolve3(text);
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function detectScorerAgent(env = process.env) {
|
|
1242
|
+
const explicit = env[SKILLOPT_ENV.AGENT];
|
|
1243
|
+
if (explicit && ["claude_code", "codex", "cursor", "hermes", "pi"].includes(explicit)) {
|
|
1244
|
+
return explicit;
|
|
1245
|
+
}
|
|
1246
|
+
if (env.CLAUDECODE === "1" || env.CLAUDE_CODE_ENTRYPOINT)
|
|
1247
|
+
return "claude_code";
|
|
1248
|
+
if (env.CODEX_HOME || env.CODEX_SESSION_ID)
|
|
1249
|
+
return "codex";
|
|
1250
|
+
return "claude_code";
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// dist/src/skillify/skill-invocations.js
|
|
1254
|
+
function parseMessage(m) {
|
|
1255
|
+
if (m == null)
|
|
1256
|
+
return null;
|
|
1257
|
+
if (typeof m === "string") {
|
|
1258
|
+
try {
|
|
1259
|
+
return JSON.parse(m);
|
|
1260
|
+
} catch {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (typeof m === "object")
|
|
1265
|
+
return m;
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
function pathToSkillRef(s) {
|
|
1269
|
+
if (typeof s !== "string")
|
|
1270
|
+
return null;
|
|
1271
|
+
const m = s.match(/\/skills\/(?:[^/\s"'`]+\/)*([^/\s"'`]+)\/SKILL\.md/);
|
|
1272
|
+
return m ? m[1] : null;
|
|
1273
|
+
}
|
|
1274
|
+
function invokedSkillRef(msg) {
|
|
1275
|
+
if (msg.type !== "tool_call")
|
|
1276
|
+
return null;
|
|
1277
|
+
let input = msg.tool_input;
|
|
1278
|
+
if (typeof input === "string") {
|
|
1279
|
+
try {
|
|
1280
|
+
input = JSON.parse(input);
|
|
1281
|
+
} catch {
|
|
1282
|
+
input = msg.tool_input;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (msg.tool_name === "Skill") {
|
|
1286
|
+
const skill = input?.skill;
|
|
1287
|
+
return typeof skill === "string" && skill.length > 0 ? skill : null;
|
|
1288
|
+
}
|
|
1289
|
+
const io = input;
|
|
1290
|
+
return pathToSkillRef(io?.path) ?? pathToSkillRef(io?.command);
|
|
1291
|
+
}
|
|
1292
|
+
function splitOrgSkill(skill) {
|
|
1293
|
+
if (skill.includes(":"))
|
|
1294
|
+
return null;
|
|
1295
|
+
if (skill.includes("/") || skill.includes("\\") || skill.includes(".."))
|
|
1296
|
+
return null;
|
|
1297
|
+
const i = skill.lastIndexOf("--");
|
|
1298
|
+
if (i <= 0 || i + 2 >= skill.length)
|
|
1299
|
+
return null;
|
|
1300
|
+
return { name: skill.slice(0, i), author: skill.slice(i + 2) };
|
|
1301
|
+
}
|
|
1302
|
+
function likeEscape(s) {
|
|
1303
|
+
return s.replace(/([\\%_])/g, "\\$1");
|
|
1304
|
+
}
|
|
1305
|
+
async function sessionTurns(query, sessionsTable, inv) {
|
|
1306
|
+
const sid = sqlStr(likeEscape(inv.sessionId));
|
|
1307
|
+
const rows = await query(`SELECT message FROM "${sessionsTable}" WHERE path LIKE '/sessions/%${sid}%' ESCAPE '\\' ORDER BY creation_date ASC`);
|
|
1308
|
+
const turns = [];
|
|
1309
|
+
let invIndex = -1;
|
|
1310
|
+
for (const r of rows) {
|
|
1311
|
+
const j = parseMessage(r.message);
|
|
1312
|
+
if (!j)
|
|
1313
|
+
continue;
|
|
1314
|
+
if (typeof j.session_id === "string" && j.session_id !== inv.sessionId)
|
|
1315
|
+
continue;
|
|
1316
|
+
const ref = invokedSkillRef(j);
|
|
1317
|
+
if (ref) {
|
|
1318
|
+
const p = splitOrgSkill(ref);
|
|
1319
|
+
if (invIndex < 0 && p && p.name === inv.name && p.author === inv.author && (typeof j.timestamp !== "string" || !inv.ts || j.timestamp === inv.ts)) {
|
|
1320
|
+
invIndex = turns.length;
|
|
1321
|
+
}
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const text = typeof j.content === "string" ? j.content.trim() : "";
|
|
1325
|
+
if (!text)
|
|
1326
|
+
continue;
|
|
1327
|
+
if (j.type === "user_message")
|
|
1328
|
+
turns.push({ role: "USER", text });
|
|
1329
|
+
else if (j.type === "assistant_message")
|
|
1330
|
+
turns.push({ role: "ASSISTANT", text });
|
|
1331
|
+
}
|
|
1332
|
+
if (invIndex < 0)
|
|
1333
|
+
invIndex = turns.length;
|
|
1334
|
+
return { turns, invIndex };
|
|
1335
|
+
}
|
|
1336
|
+
async function windowedTurns(query, sessionsTable, inv, opts = {}) {
|
|
1337
|
+
const before = opts.before ?? 3;
|
|
1338
|
+
const after = opts.after ?? 6;
|
|
1339
|
+
const { turns, invIndex } = await sessionTurns(query, sessionsTable, inv);
|
|
1340
|
+
const start = Math.max(0, invIndex - before);
|
|
1341
|
+
return { turns: turns.slice(start, invIndex + after), pivot: invIndex - start };
|
|
1342
|
+
}
|
|
1343
|
+
function elide(text, maxChars) {
|
|
1344
|
+
if (text.length <= maxChars)
|
|
1345
|
+
return text;
|
|
1346
|
+
const head = text.slice(0, Math.floor(maxChars * 0.55));
|
|
1347
|
+
const tail = text.slice(text.length - Math.floor(maxChars * 0.45));
|
|
1348
|
+
return `${head}
|
|
1349
|
+
|
|
1350
|
+
\u2026[${text.length - maxChars} chars elided]\u2026
|
|
1351
|
+
|
|
1352
|
+
${tail}`;
|
|
1353
|
+
}
|
|
1354
|
+
async function windowAroundInvocation(query, sessionsTable, inv, opts = {}) {
|
|
1355
|
+
const { turns } = await windowedTurns(query, sessionsTable, inv, opts);
|
|
1356
|
+
return elide(turns.map((t) => `${t.role}: ${t.text}`).join("\n\n"), opts.maxChars ?? 4e3);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// dist/src/skillify/claude-model.js
|
|
1360
|
+
import { spawn } from "node:child_process";
|
|
1361
|
+
function claudeModel(model, opts = {}) {
|
|
1362
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1363
|
+
return (system, user) => new Promise((resolve3, reject) => {
|
|
1364
|
+
const args = [
|
|
1365
|
+
"-p",
|
|
1366
|
+
user,
|
|
1367
|
+
"--model",
|
|
1368
|
+
model,
|
|
1369
|
+
"--no-session-persistence",
|
|
1370
|
+
"--output-format",
|
|
1371
|
+
"json",
|
|
1372
|
+
"--system-prompt",
|
|
1373
|
+
system,
|
|
1374
|
+
// Empty allow-list = NO tools available. Authoritative: it covers built-ins AND
|
|
1375
|
+
// any MCP/configured tools (a deny-list can't enumerate those), so prompt-injected
|
|
1376
|
+
// transcript text in the judge/proposer prompt can never trigger tool use.
|
|
1377
|
+
"--tools",
|
|
1378
|
+
"",
|
|
1379
|
+
// --strict-mcp-config ignores the user's MCP config entirely (--tools only denies
|
|
1380
|
+
// USE, not LOADING) — a broken/oversized user MCP schema would otherwise fail every
|
|
1381
|
+
// judge/proposer call before it returns JSON, silently stopping proposals.
|
|
1382
|
+
"--strict-mcp-config"
|
|
1383
|
+
];
|
|
1384
|
+
const child = spawn(findAgentBin("claude_code"), args, {
|
|
1385
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1386
|
+
env: { ...process.env, HIVEMIND_CAPTURE: "false", HIVEMIND_WIKI_WORKER: "1" }
|
|
1387
|
+
});
|
|
1388
|
+
let out = "";
|
|
1389
|
+
let err = "";
|
|
1390
|
+
const timer = setTimeout(() => {
|
|
1391
|
+
child.kill("SIGKILL");
|
|
1392
|
+
reject(new Error("claude timed out"));
|
|
1393
|
+
}, timeoutMs);
|
|
1394
|
+
child.stdout.on("data", (d) => {
|
|
1395
|
+
out += String(d);
|
|
1396
|
+
});
|
|
1397
|
+
child.stderr.on("data", (d) => {
|
|
1398
|
+
err += String(d);
|
|
1399
|
+
});
|
|
1400
|
+
child.on("error", (e) => {
|
|
1401
|
+
clearTimeout(timer);
|
|
1402
|
+
reject(e);
|
|
1403
|
+
});
|
|
1404
|
+
child.on("close", (code) => {
|
|
1405
|
+
clearTimeout(timer);
|
|
1406
|
+
if (code !== 0)
|
|
1407
|
+
return reject(new Error(`claude exit ${code}: ${err.slice(0, 200)}`));
|
|
1408
|
+
try {
|
|
1409
|
+
resolve3(String(JSON.parse(out).result ?? ""));
|
|
1410
|
+
} catch {
|
|
1411
|
+
resolve3(out);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// dist/src/skillify/success-judge.js
|
|
1418
|
+
var SYSTEM = `You are a strict engineering reviewer. Judge ONLY whether the user's task was actually accomplished CORRECTLY in this session slice. Ignore whether the user seemed happy or polite \u2014 a praised-but-wrong answer is a FAILURE. Reply with ONLY a JSON object: {"success": 0 or 1, "confidence": 0.0-1.0, "reason": "<=200 chars citing concrete evidence"}.`;
|
|
1419
|
+
function buildUserPrompt(window) {
|
|
1420
|
+
return `Session slice (USER/ASSISTANT turns around a skill invocation):
|
|
1421
|
+
|
|
1422
|
+
${window}
|
|
1423
|
+
|
|
1424
|
+
Did the user's task get accomplished correctly? JSON only.`;
|
|
1425
|
+
}
|
|
1426
|
+
function extractJson(raw) {
|
|
1427
|
+
let s = raw.trim();
|
|
1428
|
+
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1429
|
+
if (fence)
|
|
1430
|
+
s = fence[1].trim();
|
|
1431
|
+
const a = s.indexOf("{");
|
|
1432
|
+
const b = s.lastIndexOf("}");
|
|
1433
|
+
if (a === -1 || b <= a)
|
|
1434
|
+
return null;
|
|
1435
|
+
try {
|
|
1436
|
+
return JSON.parse(s.slice(a, b + 1));
|
|
1437
|
+
} catch {
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function parseVerdict(raw) {
|
|
1442
|
+
const j = extractJson(raw);
|
|
1443
|
+
if (!j)
|
|
1444
|
+
return { success: 1, confidence: 0, reason: "unparseable judge output" };
|
|
1445
|
+
const fail = j.success === 0 || j.success === "0" || j.success === false;
|
|
1446
|
+
const confidence = typeof j.confidence === "number" ? Math.max(0, Math.min(1, j.confidence)) : 0.5;
|
|
1447
|
+
const reason = typeof j.reason === "string" ? j.reason.slice(0, 240) : "";
|
|
1448
|
+
return { success: fail ? 0 : 1, confidence, reason };
|
|
1449
|
+
}
|
|
1450
|
+
async function judgeSuccess(window, opts = {}) {
|
|
1451
|
+
if (!window.trim())
|
|
1452
|
+
return { success: 1, confidence: 0, reason: "empty window" };
|
|
1453
|
+
const model = opts.model ?? claudeModel("haiku");
|
|
1454
|
+
try {
|
|
1455
|
+
return parseVerdict(await model(SYSTEM, buildUserPrompt(window)));
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
return { success: 1, confidence: 0, reason: `judge failed: ${e?.message ?? String(e)}` };
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// dist/src/skillify/skill-edits.js
|
|
1462
|
+
var SU_START = "<!-- SLOW_UPDATE_START -->";
|
|
1463
|
+
var SU_END = "<!-- SLOW_UPDATE_END -->";
|
|
1464
|
+
function protectedRange(skill) {
|
|
1465
|
+
const a = skill.indexOf(SU_START);
|
|
1466
|
+
const b = skill.indexOf(SU_END);
|
|
1467
|
+
if (a === -1 || b === -1 || b < a)
|
|
1468
|
+
return null;
|
|
1469
|
+
return [a, b + SU_END.length];
|
|
1470
|
+
}
|
|
1471
|
+
function targetsProtected(skill, target) {
|
|
1472
|
+
const r = protectedRange(skill);
|
|
1473
|
+
if (!r || !target)
|
|
1474
|
+
return false;
|
|
1475
|
+
const idx = skill.indexOf(target);
|
|
1476
|
+
if (idx === -1)
|
|
1477
|
+
return false;
|
|
1478
|
+
return idx < r[1] && idx + target.length > r[0];
|
|
1479
|
+
}
|
|
1480
|
+
function selectEdits(edits, budget) {
|
|
1481
|
+
return edits.slice(0, Math.max(0, budget));
|
|
1482
|
+
}
|
|
1483
|
+
function applyEdits(skill, edits) {
|
|
1484
|
+
let s = skill;
|
|
1485
|
+
const report = [];
|
|
1486
|
+
let applied = 0;
|
|
1487
|
+
const ok = (msg) => {
|
|
1488
|
+
applied++;
|
|
1489
|
+
report.push(`OK ${msg}`);
|
|
1490
|
+
};
|
|
1491
|
+
for (const e of edits) {
|
|
1492
|
+
if (e.target && targetsProtected(s, e.target)) {
|
|
1493
|
+
report.push(`SKIP ${e.op}: targets protected slow-update region`);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
switch (e.op) {
|
|
1497
|
+
case "append": {
|
|
1498
|
+
const content = (e.content ?? "").trim();
|
|
1499
|
+
if (!content) {
|
|
1500
|
+
report.push("SKIP append: empty content");
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
const r = protectedRange(s);
|
|
1504
|
+
if (r)
|
|
1505
|
+
s = s.slice(0, r[0]) + content + "\n\n" + s.slice(r[0]);
|
|
1506
|
+
else
|
|
1507
|
+
s = s.replace(/\s*$/, "") + "\n\n" + content + "\n";
|
|
1508
|
+
ok(`append (+${content.length} chars)`);
|
|
1509
|
+
break;
|
|
1510
|
+
}
|
|
1511
|
+
case "insert_after": {
|
|
1512
|
+
const target = e.target ?? "";
|
|
1513
|
+
const content = (e.content ?? "").trim();
|
|
1514
|
+
if (!target || !content) {
|
|
1515
|
+
report.push("SKIP insert_after: missing target/content");
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
const idx = s.indexOf(target);
|
|
1519
|
+
if (idx === -1) {
|
|
1520
|
+
report.push("SKIP insert_after: target not found");
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
const lineEnd = s.indexOf("\n", idx + target.length);
|
|
1524
|
+
const at = lineEnd === -1 ? s.length : lineEnd;
|
|
1525
|
+
s = s.slice(0, at) + "\n" + content + s.slice(at);
|
|
1526
|
+
ok("insert_after");
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
case "replace": {
|
|
1530
|
+
const target = e.target ?? "";
|
|
1531
|
+
const content = e.content ?? "";
|
|
1532
|
+
if (!target) {
|
|
1533
|
+
report.push("SKIP replace: missing target");
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
const idx = s.indexOf(target);
|
|
1537
|
+
if (idx === -1) {
|
|
1538
|
+
report.push("SKIP replace: target not found");
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
s = s.slice(0, idx) + content + s.slice(idx + target.length);
|
|
1542
|
+
ok("replace");
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
case "delete": {
|
|
1546
|
+
const target = e.target ?? "";
|
|
1547
|
+
if (!target) {
|
|
1548
|
+
report.push("SKIP delete: missing target");
|
|
1549
|
+
break;
|
|
1550
|
+
}
|
|
1551
|
+
const idx = s.indexOf(target);
|
|
1552
|
+
if (idx === -1) {
|
|
1553
|
+
report.push("SKIP delete: target not found");
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
s = s.slice(0, idx) + s.slice(idx + target.length);
|
|
1557
|
+
ok("delete");
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
default:
|
|
1561
|
+
report.push(`SKIP unknown op: ${e.op}`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return { skill: s, report, applied };
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// dist/src/skillify/skill-proposer.js
|
|
1568
|
+
var SYSTEM2 = `You improve an engineering SKILL document that has been producing repeated, confirmed failures. Diagnose the SINGLE recurring weakness behind the failures and propose a SMALL set of structured edits that fix it. Do NOT rewrite the whole doc, and do NOT touch anything between ${SU_START} and ${SU_END}. Reply with ONLY a JSON array of edits, each: {"op":"append|insert_after|replace|delete","target":"<exact existing text to anchor on; required for insert_after/replace/delete>","content":"<new text; required for append/insert_after/replace>"}. Prefer the smallest change that fixes the weakness.`;
|
|
1569
|
+
function buildUserPrompt2(body, failures, priorEdits) {
|
|
1570
|
+
const cases = failures.slice(0, 8).map((f, i) => `${i + 1}. ${f}`).join("\n");
|
|
1571
|
+
const prior = priorEdits.length ? `
|
|
1572
|
+
|
|
1573
|
+
ALREADY TRIED for this skill on earlier runs (do NOT repeat these \u2014 propose something different, or nothing):
|
|
1574
|
+
${priorEdits.slice(0, 12).map((p) => `- ${p}`).join("\n")}` : "";
|
|
1575
|
+
return `CURRENT SKILL:
|
|
1576
|
+
${body}
|
|
1577
|
+
|
|
1578
|
+
CONFIRMED FAILURES it produced (user pushed back AND a judge confirmed the task was not accomplished):
|
|
1579
|
+
${cases}${prior}
|
|
1580
|
+
|
|
1581
|
+
Propose the bounded edits. JSON array only.`;
|
|
1582
|
+
}
|
|
1583
|
+
var OPS = /* @__PURE__ */ new Set(["append", "insert_after", "replace", "delete"]);
|
|
1584
|
+
function parseEdits(raw) {
|
|
1585
|
+
let s = raw.trim();
|
|
1586
|
+
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1587
|
+
if (fence)
|
|
1588
|
+
s = fence[1].trim();
|
|
1589
|
+
const a = s.indexOf("[");
|
|
1590
|
+
const b = s.lastIndexOf("]");
|
|
1591
|
+
if (a === -1 || b <= a)
|
|
1592
|
+
return [];
|
|
1593
|
+
let arr;
|
|
1594
|
+
try {
|
|
1595
|
+
arr = JSON.parse(s.slice(a, b + 1));
|
|
1596
|
+
} catch {
|
|
1597
|
+
return [];
|
|
1598
|
+
}
|
|
1599
|
+
if (!Array.isArray(arr))
|
|
1600
|
+
return [];
|
|
1601
|
+
const out = [];
|
|
1602
|
+
for (const e of arr) {
|
|
1603
|
+
if (!e || typeof e !== "object")
|
|
1604
|
+
continue;
|
|
1605
|
+
const op = e.op;
|
|
1606
|
+
if (typeof op !== "string" || !OPS.has(op))
|
|
1607
|
+
continue;
|
|
1608
|
+
const target = e.target;
|
|
1609
|
+
const content = e.content;
|
|
1610
|
+
out.push({
|
|
1611
|
+
op,
|
|
1612
|
+
...typeof target === "string" ? { target } : {},
|
|
1613
|
+
...typeof content === "string" ? { content } : {}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
return out;
|
|
1617
|
+
}
|
|
1618
|
+
async function proposeSkillEdit(skillBody, failures, cfg = {}) {
|
|
1619
|
+
const budget = cfg.editBudget ?? 3;
|
|
1620
|
+
const model = cfg.model ?? claudeModel("sonnet");
|
|
1621
|
+
let raw;
|
|
1622
|
+
try {
|
|
1623
|
+
raw = await model(SYSTEM2, buildUserPrompt2(skillBody, failures, cfg.priorEdits ?? []));
|
|
1624
|
+
} catch {
|
|
1625
|
+
return { edits: [], editedBody: skillBody, report: ["proposer model call failed"], changed: false };
|
|
1626
|
+
}
|
|
1627
|
+
const edits = selectEdits(parseEdits(raw), budget);
|
|
1628
|
+
const { skill, report, applied } = applyEdits(skillBody, edits);
|
|
1629
|
+
return { edits, editedBody: skill, report, changed: applied > 0 };
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// dist/src/skillify/skills-table.js
|
|
1633
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1634
|
+
function esc(s) {
|
|
1635
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
1636
|
+
}
|
|
1637
|
+
async function insertSkillRow(args) {
|
|
1638
|
+
const id = args.id ?? randomUUID2();
|
|
1639
|
+
const sourceSessionsJson = JSON.stringify(args.sourceSessions);
|
|
1640
|
+
const contributorsJson = JSON.stringify(args.contributors);
|
|
1641
|
+
const sql = `INSERT INTO "${sqlIdent(args.tableName)}" (id, name, project, project_key, local_path, install, source_sessions, source_agent, scope, author, contributors, description, trigger_text, body, version, created_at, updated_at) VALUES ('${esc(id)}', '${esc(args.name)}', '${esc(args.project)}', '${esc(args.projectKey)}', '${esc(args.localPath)}', '${esc(args.install)}', '${esc(sourceSessionsJson)}', '${esc(args.sourceAgent)}', '${esc(args.scope)}', '${esc(args.author)}', '${esc(contributorsJson)}', '${esc(args.description)}', '${esc(args.trigger ?? "")}', '${esc(args.body)}', ${args.version}, '${esc(args.createdAt)}', '${esc(args.updatedAt)}')`;
|
|
1642
|
+
try {
|
|
1643
|
+
await args.query(sql);
|
|
1644
|
+
return;
|
|
1645
|
+
} catch (e) {
|
|
1646
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1647
|
+
if (isMissingTableError(msg)) {
|
|
1648
|
+
await args.query(buildCreateTableSql(args.tableName, SKILLS_COLUMNS));
|
|
1649
|
+
await healMissingColumns({
|
|
1650
|
+
query: args.query,
|
|
1651
|
+
tableName: args.tableName,
|
|
1652
|
+
workspaceId: args.workspaceId,
|
|
1653
|
+
columns: SKILLS_COLUMNS
|
|
1654
|
+
});
|
|
1655
|
+
await args.query(sql);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
if (isMissingColumnError(msg)) {
|
|
1659
|
+
const result = await healMissingColumns({
|
|
1660
|
+
query: args.query,
|
|
1661
|
+
tableName: args.tableName,
|
|
1662
|
+
workspaceId: args.workspaceId,
|
|
1663
|
+
columns: SKILLS_COLUMNS
|
|
1664
|
+
});
|
|
1665
|
+
if (result.missing.length === 0)
|
|
1666
|
+
throw e;
|
|
1667
|
+
await args.query(sql);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
throw e;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// dist/src/skillify/skill-org-publish.js
|
|
1675
|
+
var SKILLOPT_CONTRIBUTOR = "skillopt";
|
|
1676
|
+
function asString(v) {
|
|
1677
|
+
return typeof v === "string" ? v : v == null ? "" : String(v);
|
|
1678
|
+
}
|
|
1679
|
+
function asStringArray(v) {
|
|
1680
|
+
if (Array.isArray(v))
|
|
1681
|
+
return v.map(asString);
|
|
1682
|
+
if (typeof v === "string" && v.trim()) {
|
|
1683
|
+
try {
|
|
1684
|
+
const p = JSON.parse(v);
|
|
1685
|
+
return Array.isArray(p) ? p.map(asString) : [];
|
|
1686
|
+
} catch {
|
|
1687
|
+
return [];
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return [];
|
|
1691
|
+
}
|
|
1692
|
+
async function readCurrentSkillRow(query, skillsTable, name, author) {
|
|
1693
|
+
const rows = await query(`SELECT name, author, project, project_key, local_path, install, source_sessions, source_agent, scope, contributors, description, trigger_text, body, version FROM "${sqlIdent(skillsTable)}" WHERE name = '${sqlStr(name)}' AND author = '${sqlStr(author)}' ORDER BY version DESC, created_at DESC LIMIT 1`);
|
|
1694
|
+
const r = rows?.[0];
|
|
1695
|
+
if (!r)
|
|
1696
|
+
return null;
|
|
1697
|
+
const version = Number(r.version);
|
|
1698
|
+
return {
|
|
1699
|
+
name: asString(r.name) || name,
|
|
1700
|
+
author: asString(r.author) || author,
|
|
1701
|
+
project: asString(r.project),
|
|
1702
|
+
projectKey: asString(r.project_key),
|
|
1703
|
+
localPath: asString(r.local_path),
|
|
1704
|
+
install: asString(r.install) === "global" ? "global" : "project",
|
|
1705
|
+
sourceSessions: asStringArray(r.source_sessions),
|
|
1706
|
+
sourceAgent: asString(r.source_agent),
|
|
1707
|
+
scope: asString(r.scope) === "team" ? "team" : "me",
|
|
1708
|
+
contributors: asStringArray(r.contributors),
|
|
1709
|
+
description: asString(r.description),
|
|
1710
|
+
trigger: asString(r.trigger_text),
|
|
1711
|
+
body: asString(r.body),
|
|
1712
|
+
version: Number.isFinite(version) && version > 0 ? version : 1
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function appendUnique(base, add) {
|
|
1716
|
+
const out = [...base];
|
|
1717
|
+
for (const a of add)
|
|
1718
|
+
if (a && !out.includes(a))
|
|
1719
|
+
out.push(a);
|
|
1720
|
+
return out;
|
|
1721
|
+
}
|
|
1722
|
+
async function publishImprovedSkill(opts) {
|
|
1723
|
+
const version = opts.current.version + 1;
|
|
1724
|
+
const base = opts.current.contributors.length ? opts.current.contributors : [opts.current.author];
|
|
1725
|
+
const contributors = appendUnique(base, [opts.collaborator, SKILLOPT_CONTRIBUTOR]);
|
|
1726
|
+
await insertSkillRow({
|
|
1727
|
+
query: opts.query,
|
|
1728
|
+
tableName: opts.tableName,
|
|
1729
|
+
workspaceId: opts.workspaceId,
|
|
1730
|
+
name: opts.current.name,
|
|
1731
|
+
author: opts.current.author,
|
|
1732
|
+
project: opts.current.project,
|
|
1733
|
+
projectKey: opts.current.projectKey,
|
|
1734
|
+
localPath: opts.current.localPath,
|
|
1735
|
+
install: opts.current.install,
|
|
1736
|
+
sourceSessions: opts.current.sourceSessions,
|
|
1737
|
+
sourceAgent: opts.current.sourceAgent,
|
|
1738
|
+
scope: "team",
|
|
1739
|
+
contributors,
|
|
1740
|
+
description: opts.current.description,
|
|
1741
|
+
trigger: opts.current.trigger,
|
|
1742
|
+
body: opts.newBody,
|
|
1743
|
+
version,
|
|
1744
|
+
createdAt: opts.now,
|
|
1745
|
+
updatedAt: opts.now
|
|
1746
|
+
});
|
|
1747
|
+
return { version };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// dist/src/skillify/skillopt-improve.js
|
|
1751
|
+
function likeEscape2(s) {
|
|
1752
|
+
return s.replace(/([\\%_])/g, "\\$1");
|
|
1753
|
+
}
|
|
1754
|
+
async function findInvocation(query, sessionsTable, sessionId, name, author, toolUseId) {
|
|
1755
|
+
const sid = sqlStr(likeEscape2(sessionId));
|
|
1756
|
+
const rows = await query(`SELECT message FROM "${sqlIdent(sessionsTable)}" WHERE path LIKE '/sessions/%${sid}%' ESCAPE '\\' ORDER BY creation_date ASC`);
|
|
1757
|
+
let latest = null;
|
|
1758
|
+
let pinned = null;
|
|
1759
|
+
for (const r of rows) {
|
|
1760
|
+
const m = parseMessage(r.message);
|
|
1761
|
+
if (!m)
|
|
1762
|
+
continue;
|
|
1763
|
+
if (typeof m.session_id === "string" && m.session_id !== sessionId)
|
|
1764
|
+
continue;
|
|
1765
|
+
const ref = invokedSkillRef(m);
|
|
1766
|
+
if (!ref)
|
|
1767
|
+
continue;
|
|
1768
|
+
const p = splitOrgSkill(ref);
|
|
1769
|
+
if (!p || p.name !== name || p.author !== author)
|
|
1770
|
+
continue;
|
|
1771
|
+
const inv = { sessionId, name, author, ts: typeof m.timestamp === "string" ? m.timestamp : "" };
|
|
1772
|
+
latest = inv;
|
|
1773
|
+
if (toolUseId && m.tool_use_id === toolUseId)
|
|
1774
|
+
pinned = inv;
|
|
1775
|
+
}
|
|
1776
|
+
return pinned ?? latest;
|
|
1777
|
+
}
|
|
1778
|
+
async function improveSkillIfFailed(opts) {
|
|
1779
|
+
const none = (reason) => ({ judged: false, failed: false, improved: false, reason });
|
|
1780
|
+
const parts = splitOrgSkill(opts.skillRef);
|
|
1781
|
+
if (!parts)
|
|
1782
|
+
return none("not an org skill");
|
|
1783
|
+
const inv = await findInvocation(opts.query, opts.sessionsTable, opts.sessionId, parts.name, parts.author, opts.toolUseId);
|
|
1784
|
+
if (!inv)
|
|
1785
|
+
return none("invocation not found in session");
|
|
1786
|
+
let window = await windowAroundInvocation(opts.query, opts.sessionsTable, inv);
|
|
1787
|
+
if (opts.reaction?.trim())
|
|
1788
|
+
window += `
|
|
1789
|
+
|
|
1790
|
+
USER: ${opts.reaction.trim()}`;
|
|
1791
|
+
const verdict = await judgeSuccess(window, { model: opts.judge });
|
|
1792
|
+
if (verdict.success !== 0)
|
|
1793
|
+
return { judged: true, failed: false, improved: false, reason: verdict.reason };
|
|
1794
|
+
const current = await readCurrentSkillRow(opts.query, opts.skillsTable, parts.name, parts.author);
|
|
1795
|
+
if (!current)
|
|
1796
|
+
return { judged: true, failed: true, improved: false, reason: "skill not in org table" };
|
|
1797
|
+
const priorEdits = opts.prior?.(parts.name, parts.author) ?? [];
|
|
1798
|
+
const p = await proposeSkillEdit(current.body, [verdict.reason], { model: opts.proposerModel, priorEdits });
|
|
1799
|
+
if (!p.changed)
|
|
1800
|
+
return { judged: true, failed: true, improved: false, reason: "proposer made no change" };
|
|
1801
|
+
if (opts.alreadyProposed?.(parts.name, parts.author, p.edits)) {
|
|
1802
|
+
return { judged: true, failed: true, improved: false, reason: "edit already proposed (dedup)" };
|
|
1803
|
+
}
|
|
1804
|
+
const { version } = await publishImprovedSkill({
|
|
1805
|
+
query: opts.query,
|
|
1806
|
+
tableName: opts.skillsTable,
|
|
1807
|
+
workspaceId: opts.workspaceId,
|
|
1808
|
+
current,
|
|
1809
|
+
newBody: p.editedBody,
|
|
1810
|
+
collaborator: opts.collaborator,
|
|
1811
|
+
now: opts.now
|
|
1812
|
+
});
|
|
1813
|
+
try {
|
|
1814
|
+
opts.recordEdit?.(parts.name, parts.author, p.edits);
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
return { judged: true, failed: true, improved: true, version, reason: verdict.reason };
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// dist/src/skillify/skillopt-meta.js
|
|
1821
|
+
import fs from "node:fs";
|
|
1822
|
+
import path from "node:path";
|
|
1823
|
+
var skillRef = (name, author) => `${name}--${author}`;
|
|
1824
|
+
function summarizeEdit(e) {
|
|
1825
|
+
const anchor = e.target ? ` @"${e.target.slice(0, 40)}"` : "";
|
|
1826
|
+
const preview = e.content ? `: ${e.content.slice(0, 60).replace(/\s+/g, " ")}` : "";
|
|
1827
|
+
return `${e.op}${anchor}${preview}`;
|
|
1828
|
+
}
|
|
1829
|
+
function fingerprintEdits(edits) {
|
|
1830
|
+
return edits.map((e) => `${e.op}|${e.target ?? ""}|${e.content ?? ""}`).sort().join("\n");
|
|
1831
|
+
}
|
|
1832
|
+
function loadMeta(file) {
|
|
1833
|
+
let raw;
|
|
1834
|
+
try {
|
|
1835
|
+
raw = fs.readFileSync(file, "utf8");
|
|
1836
|
+
} catch {
|
|
1837
|
+
return [];
|
|
1838
|
+
}
|
|
1839
|
+
const out = [];
|
|
1840
|
+
for (const line of raw.split("\n")) {
|
|
1841
|
+
const t = line.trim();
|
|
1842
|
+
if (!t)
|
|
1843
|
+
continue;
|
|
1844
|
+
try {
|
|
1845
|
+
const e = JSON.parse(t);
|
|
1846
|
+
if (e && typeof e.skill === "string" && typeof e.fingerprint === "string")
|
|
1847
|
+
out.push(e);
|
|
1848
|
+
} catch {
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return out;
|
|
1852
|
+
}
|
|
1853
|
+
function appendMeta(file, entry) {
|
|
1854
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1855
|
+
fs.appendFileSync(file, JSON.stringify(entry) + "\n");
|
|
1856
|
+
}
|
|
1857
|
+
function alreadyProposed(meta, name, author, edits) {
|
|
1858
|
+
const ref = skillRef(name, author);
|
|
1859
|
+
const fp = fingerprintEdits(edits);
|
|
1860
|
+
return meta.some((m) => m.skill === ref && m.fingerprint === fp);
|
|
1861
|
+
}
|
|
1862
|
+
function priorEditSummaries(meta, name, author) {
|
|
1863
|
+
const ref = skillRef(name, author);
|
|
1864
|
+
return meta.filter((m) => m.skill === ref).flatMap((m) => m.ops);
|
|
1865
|
+
}
|
|
1866
|
+
function metaEntryFor(name, author, edits, now) {
|
|
1867
|
+
return {
|
|
1868
|
+
skill: skillRef(name, author),
|
|
1869
|
+
ops: edits.map(summarizeEdit),
|
|
1870
|
+
fingerprint: fingerprintEdits(edits),
|
|
1871
|
+
proposedAt: now,
|
|
1872
|
+
status: "proposed"
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// dist/src/skillify/state.js
|
|
1877
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, writeSync, mkdirSync as mkdirSync5, renameSync as renameSync3, rmdirSync, existsSync as existsSync5, lstatSync, unlinkSync as unlinkSync3, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
1878
|
+
import { join as join9 } from "node:path";
|
|
1879
|
+
|
|
1880
|
+
// dist/src/utils/repo-identity.js
|
|
1881
|
+
import { execSync } from "node:child_process";
|
|
1882
|
+
import { createHash } from "node:crypto";
|
|
1883
|
+
import { basename, resolve as resolve2 } from "node:path";
|
|
1884
|
+
|
|
1885
|
+
// dist/src/skillify/legacy-migration.js
|
|
1886
|
+
import { existsSync as existsSync4, renameSync as renameSync2 } from "node:fs";
|
|
1887
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
1888
|
+
var dlog = (msg) => log("skillify-migrate", msg);
|
|
1889
|
+
var attempted = false;
|
|
1890
|
+
function migrateLegacyStateDir() {
|
|
1891
|
+
if (process.env.HIVEMIND_STATE_DIR?.trim())
|
|
1892
|
+
return;
|
|
1893
|
+
if (attempted)
|
|
1894
|
+
return;
|
|
1895
|
+
attempted = true;
|
|
1896
|
+
const current = getStateDir();
|
|
1897
|
+
const legacy = join8(dirname2(current), "skilify");
|
|
1898
|
+
if (!existsSync4(legacy))
|
|
1899
|
+
return;
|
|
1900
|
+
if (existsSync4(current))
|
|
1901
|
+
return;
|
|
1902
|
+
try {
|
|
1903
|
+
renameSync2(legacy, current);
|
|
1904
|
+
dlog(`migrated ${legacy} -> ${current}`);
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
const code = err.code;
|
|
1907
|
+
if (code === "EXDEV" || code === "EPERM" || code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") {
|
|
1908
|
+
dlog(`migration skipped (${code}); legacy dir left as-is or another process handled it`);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
throw err;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// dist/src/skillify/state.js
|
|
1916
|
+
var dlog2 = (msg) => log("skillify-state", msg);
|
|
1917
|
+
var YIELD_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
1918
|
+
var TRIGGER_THRESHOLD = (() => {
|
|
1919
|
+
const n = Number(process.env.HIVEMIND_SKILLIFY_EVERY_N_TURNS ?? "");
|
|
1920
|
+
return Number.isInteger(n) && n > 0 ? n : 20;
|
|
1921
|
+
})();
|
|
1922
|
+
function lockPath2(projectKey) {
|
|
1923
|
+
return join9(getStateDir(), `${projectKey}.lock`);
|
|
1924
|
+
}
|
|
1925
|
+
function tryAcquireWorkerLock(projectKey, maxAgeMs = 10 * 60 * 1e3) {
|
|
1926
|
+
migrateLegacyStateDir();
|
|
1927
|
+
mkdirSync5(getStateDir(), { recursive: true });
|
|
1928
|
+
const p = lockPath2(projectKey);
|
|
1929
|
+
if (existsSync5(p)) {
|
|
1930
|
+
try {
|
|
1931
|
+
const ageMs = Date.now() - parseInt(readFileSync5(p, "utf-8"), 10);
|
|
1932
|
+
if (Number.isFinite(ageMs) && ageMs < maxAgeMs)
|
|
1933
|
+
return false;
|
|
1934
|
+
} catch (readErr) {
|
|
1935
|
+
dlog2(`worker lock unreadable for ${projectKey}, treating as stale: ${readErr.message}`);
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
unlinkSync3(p);
|
|
1939
|
+
} catch (unlinkErr) {
|
|
1940
|
+
if (unlinkErr?.code !== "EISDIR" && unlinkErr?.code !== "EPERM" && unlinkErr?.code !== "ENOENT") {
|
|
1941
|
+
dlog2(`could not unlink stale worker lock for ${projectKey}: ${unlinkErr.message}`);
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
let isDir = false;
|
|
1945
|
+
try {
|
|
1946
|
+
isDir = lstatSync(p).isDirectory();
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
if (isDir) {
|
|
1950
|
+
try {
|
|
1951
|
+
rmdirSync(p);
|
|
1952
|
+
} catch (rmErr) {
|
|
1953
|
+
dlog2(`rmdir stale lock skipped for ${projectKey}: ${rmErr.message}`);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
try {
|
|
1959
|
+
const fd = openSync2(p, "wx");
|
|
1960
|
+
try {
|
|
1961
|
+
writeSync(fd, String(Date.now()));
|
|
1962
|
+
} finally {
|
|
1963
|
+
closeSync2(fd);
|
|
1964
|
+
}
|
|
1965
|
+
return true;
|
|
1966
|
+
} catch {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function releaseWorkerLock(projectKey) {
|
|
1971
|
+
const p = lockPath2(projectKey);
|
|
1972
|
+
try {
|
|
1973
|
+
unlinkSync3(p);
|
|
1974
|
+
} catch {
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// dist/src/skillify/skillopt-worker.js
|
|
1979
|
+
var log4 = (m) => log("skillopt-worker", m);
|
|
1980
|
+
var AGENT_CMD = { claude_code: "claude", codex: "codex", cursor: "cursor-agent", hermes: "hermes", pi: "pi" };
|
|
1981
|
+
function resolveAgentBin(agent) {
|
|
1982
|
+
const cmd = AGENT_CMD[agent];
|
|
1983
|
+
if (!cmd)
|
|
1984
|
+
return void 0;
|
|
1985
|
+
for (const dir of (process.env.PATH ?? "").split(path2.delimiter)) {
|
|
1986
|
+
if (!dir)
|
|
1987
|
+
continue;
|
|
1988
|
+
const full = path2.join(dir, cmd);
|
|
1989
|
+
try {
|
|
1990
|
+
accessSync(full, fsConstants.X_OK);
|
|
1991
|
+
return full;
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
async function main() {
|
|
1998
|
+
const sessionId = process.env[SKILLOPT_ENV.SESSION] ?? "";
|
|
1999
|
+
const skillRef2 = process.env[SKILLOPT_ENV.SKILL] ?? "";
|
|
2000
|
+
const reaction = process.env[SKILLOPT_ENV.REACTION] ?? "";
|
|
2001
|
+
const toolUseId = process.env[SKILLOPT_ENV.TOOL_USE_ID] || void 0;
|
|
2002
|
+
if (!sessionId || !skillRef2) {
|
|
2003
|
+
log4("no session/skill in env \u2014 nothing to do");
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
const config = loadConfig();
|
|
2007
|
+
if (!config?.token) {
|
|
2008
|
+
log4("no config/credentials \u2014 exiting");
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
|
|
2012
|
+
const query = (sql) => api.query(sql);
|
|
2013
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2014
|
+
const agent = detectScorerAgent();
|
|
2015
|
+
const agentBin = resolveAgentBin(agent);
|
|
2016
|
+
const metaFile = path2.join(getStateDir(), "skillopt", "meta.jsonl");
|
|
2017
|
+
const metaCache = loadMeta(metaFile);
|
|
2018
|
+
const lockKey = `skillopt-improve-${skillRef2.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
2019
|
+
if (!tryAcquireWorkerLock(lockKey)) {
|
|
2020
|
+
log4(`another worker is improving ${skillRef2} \u2014 skipping`);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
try {
|
|
2024
|
+
log4(`judging ${skillRef2} in ${sessionId} (agent=${agent})`);
|
|
2025
|
+
const r = await improveSkillIfFailed({
|
|
2026
|
+
query,
|
|
2027
|
+
sessionsTable: config.sessionsTableName,
|
|
2028
|
+
skillsTable: config.skillsTableName,
|
|
2029
|
+
workspaceId: config.workspaceId,
|
|
2030
|
+
sessionId,
|
|
2031
|
+
skillRef: skillRef2,
|
|
2032
|
+
toolUseId,
|
|
2033
|
+
reaction,
|
|
2034
|
+
judge: agentModel({ agent, role: "judge", bin: agentBin }),
|
|
2035
|
+
proposerModel: agentModel({ agent, role: "proposer", bin: agentBin }),
|
|
2036
|
+
collaborator: config.userName,
|
|
2037
|
+
now,
|
|
2038
|
+
prior: (n, a) => priorEditSummaries(metaCache, n, a),
|
|
2039
|
+
alreadyProposed: (n, a, edits) => alreadyProposed(metaCache, n, a, edits),
|
|
2040
|
+
recordEdit: (n, a, edits) => {
|
|
2041
|
+
const e = metaEntryFor(n, a, edits, now);
|
|
2042
|
+
appendMeta(metaFile, e);
|
|
2043
|
+
metaCache.push(e);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
if (r.improved)
|
|
2047
|
+
log4(`improved ${skillRef2} \u2192 v${r.version} (${r.reason})`);
|
|
2048
|
+
else if (r.failed)
|
|
2049
|
+
log4(`${skillRef2} failed but not improved: ${r.reason}`);
|
|
2050
|
+
else if (r.judged)
|
|
2051
|
+
log4(`${skillRef2} ok \u2014 no change (${r.reason})`);
|
|
2052
|
+
else
|
|
2053
|
+
log4(`${skillRef2} not judged: ${r.reason}`);
|
|
2054
|
+
} finally {
|
|
2055
|
+
releaseWorkerLock(lockKey);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
main().catch((e) => {
|
|
2059
|
+
log4(`fatal (swallowed): ${e?.message ?? e}`);
|
|
2060
|
+
process.exit(0);
|
|
2061
|
+
});
|