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 not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions harnesses/cursor/extension/media/d3.v7.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions harnesses/cursor/extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion harnesses/cursor/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hivemind-cursor-extension",
"displayName": "Hivemind for Cursor",
"description": "Shared memory, health checks, dashboard, and codebase graph inside Cursor",
"version": "0.1.0",
"version": "0.1.4",
"publisher": "deeplake",
"engines": {
"vscode": "^1.85.0"
Expand Down
201 changes: 167 additions & 34 deletions harnesses/cursor/extension/scripts/load-dashboard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
* 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 { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import { loadCreds, deriveProjectKey, graphsHome } from "./lib/deeplake.mjs";
import { basename, dirname, join } from "node:path";
import { loadCreds, deriveProjectKey, graphsHome, query, sqlStr, sqlIdent, tableNames } from "./lib/deeplake.mjs";

const cwd = process.argv[2] || process.cwd();

Expand Down Expand Up @@ -66,17 +66,130 @@
return out;
}

function readOrgStatsCache(creds) {
if (!creds) return null;
const path = join(homedir(), ".deeplake", "hivemind-stats-cache.json");
if (!existsSync(path)) return null;
const STATS_CACHE_TTL_MS = 3600_000;

function statsCachePath() {
return join(homedir(), ".deeplake", "hivemind-stats-cache.json");
}

function statsScopeKey(creds) {
return JSON.stringify({
apiUrl: creds.apiUrl ?? "https://api.deeplake.ai",
orgId: creds.orgId ?? "",
userName: creds.userName ?? "",
});
}

function nonNegNumber(value) {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
}

function scopeFromServer(scope) {
return {
sessionsCount: nonNegNumber(scope?.sessions_count),
memoryRecallCount: nonNegNumber(scope?.memory_recall_count),
memorySearchBytes: nonNegNumber(scope?.memory_search_bytes),
};
}

function readStatsCache(scopeKey) {
try {
if (!existsSync(statsCachePath())) return {};
const parsed = JSON.parse(readFileSync(statsCachePath(), "utf-8"));
if (!parsed || parsed.scopeKey !== scopeKey || typeof parsed.fetchedAt !== "number") return {};
if (!parsed.data?.org) return {};
const age = Date.now() - parsed.fetchedAt;
if (age >= 0 && age < STATS_CACHE_TTL_MS) return { fresh: parsed.data, fetchedAt: parsed.fetchedAt };
return { stale: parsed.data, fetchedAt: parsed.fetchedAt };
} catch {
return {};
}
}

function writeStatsCache(scopeKey, data) {
try {
mkdirSync(dirname(statsCachePath()), { recursive: true });
writeFileSync(statsCachePath(), JSON.stringify({ fetchedAt: Date.now(), scopeKey, data }), "utf-8");
} catch {
/* best-effort cache */
}
}

/**
* Resolve org/user activity stats the same way the core CLI does: fresh
* cache first, then a live `GET /me/hivemind-stats`, then stale cache.
* Returns null when no creds or the endpoint is unreachable with no cache.
*/
async function fetchOrgStats(creds) {
if (!creds?.token) return null;
const apiUrl = creds.apiUrl ?? "https://api.deeplake.ai";
const scopeKey = statsScopeKey(creds);
const { fresh, stale, fetchedAt } = readStatsCache(scopeKey);
if (fresh) {
return { org: fresh.org, user: fresh.user ?? fresh.org, fetchedAt: new Date(fetchedAt).toISOString(), stale: false, offline: false };
}
try {
const resp = await fetch(`${apiUrl}/me/hivemind-stats`, {

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
headers: {
Authorization: `Bearer ${creds.token}`,
...(creds.orgId ? { "X-Activeloop-Org-Id": creds.orgId } : {}),
},

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Comment on lines +133 to +136
signal: AbortSignal.timeout(8000),
});
if (resp.ok) {
const body = await resp.json().catch(() => null);
if (body && typeof body === "object") {
const data = { org: scopeFromServer(body.org), user: scopeFromServer(body.user) };
writeStatsCache(scopeKey, data);
return { org: data.org, user: data.user, fetchedAt: new Date().toISOString(), stale: false, offline: false };
}
}
} catch {
/* fall through to stale */
}
if (stale) {
return { org: stale.org, user: stale.user ?? stale.org, fetchedAt: new Date(fetchedAt).toISOString(), stale: true, offline: true };
}
return null;
}

/** Sum the locally-tracked recall events (count + bytes delivered). */
function readRecallEvents() {
const path = join(homedir(), ".deeplake", "recall-events.jsonl");
if (!existsSync(path)) return { count: 0, bytes: 0 };
let count = 0;
let bytes = 0;
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 };
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.bytes === "number" && rec.bytes > 0) {
count += 1;
bytes += rec.bytes;
}
} catch {
/* skip bad line */
}
}
} catch {
return { count: 0, bytes: 0 };
}
return { count, bytes };
}

/** Real distinct-session count for this user from the sessions table. */
async function fetchDistinctSessions(creds) {
if (!creds || !creds.userName) return null;
try {
const table = sqlIdent(tableNames().sessions);
const rows = await query(
creds,
`SELECT COUNT(DISTINCT path) AS c FROM "${table}" WHERE author = '${sqlStr(creds.userName)}'`,
);
const c = rows && rows[0] ? Number(rows[0].c) : NaN;
return Number.isFinite(c) ? c : null;
} catch {
return null;
}
Expand Down Expand Up @@ -129,35 +242,55 @@
const graph = resolveSnapshot(join(graphsHome(), repoKey));
const skillsCreated = countUserGeneratedSkills(creds?.userName);

const cache = readOrgStatsCache(creds);
// Sessions: real distinct-session count for this user from the sessions
// table (consistent with the Sessions tab), with org rollup as a fallback.
const distinctSessions = await fetchDistinctSessions(creds);
// Memory recall: tracked locally by the pre-tool-use hook into
// recall-events.jsonl. The server rollup does not yet count Cursor recalls,
// so this local store is the source of truth for memory-search / tokens-saved.
const recall = readRecallEvents();
const org = await fetchOrgStats(creds);

let sessionsCount = distinctSessions;
if (sessionsCount == null && org) sessionsCount = org.org.sessionsCount ?? null;

let kpis;
if (cache) {
if (recall.count > 0) {
kpis = {
tokensSaved: bytesToSavedTokens(cache.org.memorySearchBytes ?? 0),
tokensSource: "org",
tokensSaved: bytesToSavedTokens(recall.bytes),
tokensSource: "local",
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,
memorySearches: recall.count,
sessionsCount,
userTokensSaved: bytesToSavedTokens(recall.bytes),
orgStatsFetchedAt: null,
orgStatsStale: false,
orgStatsOffline: false,
};
} else if (org && ((org.org.memoryRecallCount ?? 0) > 0 || (org.org.memorySearchBytes ?? 0) > 0)) {
kpis = {
tokensSaved: bytesToSavedTokens(org.org.memorySearchBytes ?? 0),
tokensSource: "org",
skillsCreated,
memorySearches: org.org.memoryRecallCount ?? 0,
sessionsCount,
userTokensSaved: bytesToSavedTokens((org.user ?? org.org).memorySearchBytes ?? 0),
orgStatsFetchedAt: org.fetchedAt,
orgStatsStale: org.stale,
orgStatsOffline: org.offline,
};
} 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;
// Sessions are real; memory-recall metrics have not accumulated yet.
kpis = {
tokensSaved: has ? bytesToSavedTokens(localBytes) : null,
tokensSource: has ? "local" : "none",
tokensSaved: null,
tokensSource: "none",
skillsCreated,
memorySearches: localCount,
sessionsCount: has ? records.length : null,
userTokensSaved: has ? bytesToSavedTokens(localBytes) : null,
orgStatsFetchedAt: null,
orgStatsStale: false,
orgStatsOffline: false,
memorySearches: 0,
sessionsCount,
userTokensSaved: null,
orgStatsFetchedAt: org ? org.fetchedAt : null,
orgStatsStale: org ? org.stale : false,
orgStatsOffline: org ? org.offline : false,
};
}

Expand Down
6 changes: 5 additions & 1 deletion harnesses/cursor/extension/src/webview/DashboardPanel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from "vscode";
import { join } from "node:path";
import { backfillCursorLinks, listLocalSkillsForPromoter, skillDirLabel, syncSkillsToCursor } from "../bridge/skill-sync";
import { detectAuthState, formatIdentity } from "../auth";
import { runHealthCheck } from "../health/checker";
Expand Down Expand Up @@ -82,7 +83,10 @@ class DashboardController {
) {
const stored = memento.get<string[]>(PROMOTED_STEPS_KEY, []);
this.promotedSteps = new Set(stored);
webview.options = { enableScripts: true };
webview.options = {
enableScripts: true,
localResourceRoots: [vscode.Uri.file(join(__dirname, ".."))],
};
webview.html = getDashboardHtml(webview, vscode.Uri.file(__dirname));

disposables.push(
Expand Down
10 changes: 5 additions & 5 deletions harnesses/cursor/extension/src/webview/data-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function repoRootFromExtension(): string {
}

function loadDashboardScriptPath(): string {
return join(__dirname, "..", "..", "scripts", "load-dashboard.mjs");
return join(__dirname, "..", "scripts", "load-dashboard.mjs");
}

function statsFilePath(): string {
Expand Down Expand Up @@ -250,7 +250,7 @@ export function invalidateOrgStatsCache(): void {
}

export async function loadRecentSessions(_cwd: string): Promise<RecentSession[]> {
const scriptPath = join(__dirname, "..", "..", "scripts", "load-sessions.mjs");
const scriptPath = join(__dirname, "..", "scripts", "load-sessions.mjs");
if (existsSync(scriptPath)) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath, _cwd], {
Expand Down Expand Up @@ -295,7 +295,7 @@ export interface RulesListResult {
}

export async function loadRulesList(status: string, limit = 10): Promise<RulesListResult> {
const scriptPath = join(__dirname, "..", "..", "scripts", "load-rules.mjs");
const scriptPath = join(__dirname, "..", "scripts", "load-rules.mjs");
if (!existsSync(scriptPath)) {
return { loggedOut: true, rules: [], message: "Rules loader unavailable." };
}
Expand All @@ -321,7 +321,7 @@ export async function loadRulesList(status: string, limit = 10): Promise<RulesLi
}

export async function loadGoalsList(filter: "mine" | "all" = "mine"): Promise<GoalsListResult> {
const scriptPath = join(__dirname, "..", "..", "scripts", "load-goals.mjs");
const scriptPath = join(__dirname, "..", "scripts", "load-goals.mjs");
if (!existsSync(scriptPath)) {
return { loggedOut: true, goals: [], message: "Goals loader unavailable." };
}
Expand Down Expand Up @@ -350,7 +350,7 @@ export async function loadGoalsList(filter: "mine" | "all" = "mine"): Promise<Go
export async function loadSessionSummary(sessionId: string, cwd: string): Promise<SessionSummaryResult> {
const creds = readJson<{ userName?: string }>(credentialsPath());
const userName = creds?.userName ?? "";
const scriptPath = join(__dirname, "..", "..", "scripts", "load-session-summary.mjs");
const scriptPath = join(__dirname, "..", "scripts", "load-session-summary.mjs");
if (!existsSync(scriptPath)) {
const path = join(homedir(), ".deeplake", "memory", "summaries", userName, `${sessionId}.md`);
if (userName && existsSync(path)) {
Expand Down
Loading
Loading