Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified harnesses/cursor/extension/hivemind-cursor-extension-0.1.0.vsix
Binary file not shown.
140 changes: 140 additions & 0 deletions harnesses/cursor/extension/scripts/lib/deeplake.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Self-contained Deeplake data helper for the Cursor extension loaders.
*
* The loader scripts run via plain `node` spawned from the packaged
* extension. The core Hivemind CLI source (repo-root `src/`) is neither
* shipped in the vsix nor compiled to `.js`, so the old loaders that did
* `import ... from "../../../../src/*.js"` always failed at runtime. This
* module reimplements just the small slice of behaviour the loaders need,
* with no dependency outside the scripts directory: reading credentials,
* running a SQL query against the Deeplake HTTP endpoint, escaping SQL
* literals, and deriving the per-repo graph key the core uses.
*/
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { createHash } from "node:crypto";

const DEFAULT_API_URL = "https://api.deeplake.ai";

export function loadCreds() {
try {
const raw = readFileSync(join(homedir(), ".deeplake", "credentials.json"), "utf-8");
const creds = JSON.parse(raw);
if (!creds || !creds.token || !creds.orgId) return null;
return {
token: creds.token,
orgId: creds.orgId,
orgName: creds.orgName ?? creds.orgId,
userName: creds.userName ?? "",
workspaceId: creds.workspaceId ?? "default",
apiUrl: creds.apiUrl ?? DEFAULT_API_URL,
};
} catch {
return null;
}
}

/** Table names, matching core `src/config.ts` defaults plus env overrides. */
export function tableNames() {
return {
memory: process.env.HIVEMIND_TABLE ?? "memory",
sessions: process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions",
rules: process.env.HIVEMIND_RULES_TABLE ?? "hivemind_rules",
goals: process.env.HIVEMIND_GOALS_TABLE ?? "hivemind_goals",
};
}

/** Escape a string for a single-quoted SQL literal. Mirrors src/utils/sql.ts. */
export function sqlStr(value) {
return String(value)
.replace(/\\/g, "\\\\")
.replace(/'/g, "''")
.replace(/\0/g, "")
// eslint-disable-next-line no-control-regex
.replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
}

/** Validate a SQL identifier (table/column). Mirrors src/utils/sql.ts. */
export function sqlIdent(name) {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
}
return name;
}

/** True when the API error means the table has not been created yet. */
export function isMissingTableError(message) {
return /does not exist|no such table|not found/i.test(String(message ?? ""));
}

/**
* Run a SQL query against the Deeplake query endpoint and return rows as
* plain objects. Mirrors the request shape in src/deeplake-api.ts.
*/
export async function query(creds, sql, timeoutMs = 8000) {
const resp = await fetch(`${creds.apiUrl}/workspaces/${creds.workspaceId}/tables/query`, {

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
method: "POST",
headers: {
Authorization: `Bearer ${creds.token}`,
"Content-Type": "application/json",
"X-Activeloop-Org-Id": creds.orgId,
"X-Deeplake-Client": "hivemind",
},

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Comment on lines +79 to +84
signal: AbortSignal.timeout(timeoutMs),
body: JSON.stringify({ query: sql }),

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
});
if (!resp.ok) {
const text = await resp.text().catch(() => "");
throw new Error(`Query failed: ${resp.status}: ${text.slice(0, 200)}`);
}
const raw = await resp.json().catch(() => null);
if (!raw || !Array.isArray(raw.rows) || !Array.isArray(raw.columns)) return [];
return raw.rows.map((row) => Object.fromEntries(raw.columns.map((col, i) => [col, row[i]])));
}

/** Collapse the surface forms of a git remote URL. Mirrors src/utils/repo-identity.ts. */
export function normalizeGitRemoteUrl(url) {
let s = String(url).trim();
const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
if (schemeMatch) s = s.slice(schemeMatch[0].length);
if (!scheme) {
const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
if (scp) s = `${scp[1]}/${scp[2]}`;
}
s = s.replace(/^[^@/]+@/, "");
const defaultPorts = { http: "80", https: "443", ssh: "22", git: "9418" };
if (scheme && defaultPorts[scheme]) {
s = s.replace(new RegExp(`^([^/]+):${defaultPorts[scheme]}(/|$)`), "$1$2");
}
s = s.replace(/\.git\/?$/i, "");
s = s.replace(/\/+$/, "");
return s.toLowerCase();
}

/**
* Stable per-repo key: sha1 of the normalized git remote (fallback to the
* absolute cwd), first 16 hex chars. Mirrors core deriveProjectKey so the
* extension resolves the SAME `~/.hivemind/graphs/<key>` dir the CLI writes.
*/
export function deriveProjectKey(cwd) {
let signature = null;
try {
const raw = execSync("git config --get remote.origin.url", {
cwd,
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
signature = raw ? normalizeGitRemoteUrl(raw) : null;
} catch {
signature = null;
}
const input = signature ?? cwd;
return createHash("sha1").update(input).digest("hex").slice(0, 16);
}

export function graphsHome() {
return process.env.HIVEMIND_GRAPHS_HOME ?? join(homedir(), ".hivemind", "graphs");
}
173 changes: 170 additions & 3 deletions harnesses/cursor/extension/scripts/load-dashboard.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,172 @@
import { loadDashboardData } from "../../../../src/dashboard/data.js";
/**
* Build the dashboard data envelope (KPIs + codebase graph snapshot).
*
* Self-contained: resolves the per-repo graph key the same way the core CLI
* does (see lib/deeplake.mjs deriveProjectKey) so it finds the snapshot the
* `hivemind graph build` command actually wrote under
* ~/.hivemind/graphs/<key>/snapshots. KPIs come from the org-stats cache the
* CLI maintains, falling back to local usage records, then to an empty state.
* Prints a DashboardDataEnvelope JSON to stdout.
*/
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import { loadCreds, deriveProjectKey, graphsHome } from "./lib/deeplake.mjs";

const cwd = process.argv[2] || process.cwd();
const data = await loadDashboardData({ cwd });
console.log(JSON.stringify(data));

const BYTES_PER_TOKEN = 4;
const SAVINGS_MULTIPLIER = 1.7;

function bytesToSavedTokens(bytes) {
if (!Number.isFinite(bytes) || bytes <= 0) return 0;
return (SAVINGS_MULTIPLIER - 1) * (bytes / BYTES_PER_TOKEN);
}

function countUserGeneratedSkills(userName) {
if (!userName) return 0;
const dir = join(homedir(), ".claude", "skills");
if (!existsSync(dir)) return 0;
const suffix = `--${userName}`;
try {
let count = 0;
for (const name of readdirSync(dir)) {
const idx = name.lastIndexOf(suffix);
if (idx > 0 && idx + suffix.length === name.length) count += 1;
}
return count;
} catch {
return 0;
}
}

function readUsageRecords() {
const path = join(homedir(), ".deeplake", "usage-stats.jsonl");
if (!existsSync(path)) return [];
const out = [];
try {
for (const line of readFileSync(path, "utf-8").split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const rec = JSON.parse(trimmed);
if (typeof rec.endedAt === "string" && typeof rec.sessionId === "string") {
out.push({
memorySearchBytes: typeof rec.memorySearchBytes === "number" ? rec.memorySearchBytes : 0,
memorySearchCount: typeof rec.memorySearchCount === "number" ? rec.memorySearchCount : 0,
});
}
} catch {
/* skip bad line */
}
}
} catch {
return [];
}
return out;
}

function readOrgStatsCache(creds) {
if (!creds) return null;
const path = join(homedir(), ".deeplake", "hivemind-stats-cache.json");
if (!existsSync(path)) return null;
try {
const cache = JSON.parse(readFileSync(path, "utf-8"));
const data = cache?.data;
if (!data?.org) return null;
const fetchedAt = typeof cache.fetchedAt === "number" ? new Date(cache.fetchedAt).toISOString() : null;
const stale = typeof cache.fetchedAt === "number" ? Date.now() - cache.fetchedAt > 3600_000 : false;
return { org: data.org, user: data.user ?? data.org, fetchedAt, stale };
} catch {
return null;
}
}

function resolveSnapshot(repoDir) {
const snapshotsDir = join(repoDir, "snapshots");
if (!existsSync(snapshotsDir)) return null;
let snapshotPath = null;
const pointer = join(repoDir, "latest-commit.txt");
if (existsSync(pointer)) {
try {
const sha = readFileSync(pointer, "utf-8").trim();
const candidate = join(snapshotsDir, `${sha}.json`);
if (sha && existsSync(candidate)) snapshotPath = candidate;
} catch {
/* fall through to newest-file scan */
}
}
if (!snapshotPath) {
try {
const candidates = readdirSync(snapshotsDir)
.filter((n) => n.endsWith(".json"))
.map((n) => ({ full: join(snapshotsDir, n), mtime: statSync(join(snapshotsDir, n)).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime);
if (candidates[0]) snapshotPath = candidates[0].full;
} catch {
return null;
}
}
if (!snapshotPath) return null;
try {
const parsed = JSON.parse(readFileSync(snapshotPath, "utf-8"));
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.links)) return null;
return {
commitSha: parsed.graph?.commit_sha ?? null,
snapshotPath,
nodeCount: parsed.nodes.length,
edgeCount: parsed.links.length,
snapshot: parsed,
};
} catch {
return null;
}
}

const creds = loadCreds();
const repoKey = deriveProjectKey(cwd);
const repoProject = basename(cwd);
const graph = resolveSnapshot(join(graphsHome(), repoKey));
const skillsCreated = countUserGeneratedSkills(creds?.userName);

const cache = readOrgStatsCache(creds);
let kpis;
if (cache) {
kpis = {
tokensSaved: bytesToSavedTokens(cache.org.memorySearchBytes ?? 0),
tokensSource: "org",
skillsCreated,
memorySearches: cache.org.memoryRecallCount ?? 0,
sessionsCount: cache.org.sessionsCount ?? null,
userTokensSaved: bytesToSavedTokens((cache.user ?? cache.org).memorySearchBytes ?? 0),
orgStatsFetchedAt: cache.fetchedAt,
orgStatsStale: cache.stale,
orgStatsOffline: false,
};
} else {
const records = readUsageRecords();
const localBytes = records.reduce((s, r) => s + r.memorySearchBytes, 0);
const localCount = records.reduce((s, r) => s + r.memorySearchCount, 0);
const has = records.length > 0;
kpis = {
tokensSaved: has ? bytesToSavedTokens(localBytes) : null,
tokensSource: has ? "local" : "none",
skillsCreated,
memorySearches: localCount,
sessionsCount: has ? records.length : null,
userTokensSaved: has ? bytesToSavedTokens(localBytes) : null,
orgStatsFetchedAt: null,
orgStatsStale: false,
orgStatsOffline: false,
};
}

process.stdout.write(
JSON.stringify({
repoKey,
repoProject,
generatedAt: new Date().toISOString(),
kpis,
graph,
}),
);
62 changes: 62 additions & 0 deletions harnesses/cursor/extension/scripts/load-goals.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* List goals from the Deeplake `hivemind_goals` table.
*
* Self-contained: reads credentials and queries the Deeplake HTTP endpoint
* directly. Mirrors core src/commands/goal.ts goalList, with latest-version
* dedup per goal_id (the VFS write path appends a fresh row per overwrite).
* Prints a GoalsListResult JSON to stdout.
*
* argv[2]: filter — "mine" (default) or "all".
*/
import { loadCreds, query, sqlIdent, sqlStr, tableNames, isMissingTableError } from "./lib/deeplake.mjs";

const filter = process.argv[2] === "all" ? "all" : "mine";

function emit(obj) {
process.stdout.write(JSON.stringify(obj));
}

const creds = loadCreds();
if (!creds) {
emit({ loggedOut: true, goals: [], message: "Log in with `hivemind login` to track team goals." });
process.exit(0);
}

const table = sqlIdent(tableNames().goals);
const where = filter === "mine" ? `WHERE owner = '${sqlStr(creds.userName)}'` : "";

let rows;
try {
rows = await query(
creds,
`SELECT goal_id, owner, status, content, version, created_at FROM "${table}" ${where} ORDER BY version DESC, created_at DESC LIMIT 200`,
);
} catch (e) {
if (isMissingTableError(e?.message)) {
emit({ loggedOut: false, goals: [] });
process.exit(0);
}
emit({ loggedOut: false, goals: [], message: "Could not load goals." });
process.exit(0);
}

const latest = new Map();
for (const r of rows) {
const goalId = String(r.goal_id ?? "");
if (!goalId || latest.has(goalId)) continue;
const text = String(r.content ?? "").split(/\r?\n/)[0].trim();
latest.set(goalId, {
goalId,
owner: String(r.owner ?? ""),
status: String(r.status ?? ""),
text,
createdAt: String(r.created_at ?? ""),
});
}

const goals = [...latest.values()]
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.slice(0, 50)
.map(({ goalId, owner, status, text }) => ({ goalId, owner, status, text }));

emit({ loggedOut: false, goals });
Loading
Loading