Skip to content
Closed
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
88 changes: 68 additions & 20 deletions claude-code/bundle/session-start-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

// dist/src/hooks/session-start-setup.js
import { fileURLToPath } from "node:url";
import { dirname as dirname2, join as join7 } from "node:path";
import { dirname as dirname3, join as join8 } from "node:path";
import { execSync as execSync2 } from "node:child_process";
import { homedir as homedir4 } from "node:os";
import { homedir as homedir5 } from "node:os";

// dist/src/commands/auth.js
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
Expand Down Expand Up @@ -451,34 +451,82 @@ function getInstalledVersion(bundleDir, pluginManifestDir) {
}
return null;
}
async function getLatestVersion(timeoutMs = 3e3) {
try {
const res = await fetch(GITHUB_RAW_PKG, { signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok)
return null;
const pkg = await res.json();
return pkg.version ?? null;
} catch {
return null;
}
}
function isNewer(latest, current) {
const parse = (v) => v.split(".").map(Number);
const [la, lb, lc] = parse(latest);
const [ca, cb, cc] = parse(current);
return la > ca || la === ca && lb > cb || la === ca && lb === cb && lc > cc;
}

// dist/src/hooks/version-check.js
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
import { dirname as dirname2, join as join6 } from "node:path";
import { homedir as homedir4 } from "node:os";
var DEFAULT_VERSION_CACHE_PATH = join6(homedir4(), ".deeplake", ".version-check.json");
var DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1e3;
function readVersionCache(cachePath = DEFAULT_VERSION_CACHE_PATH) {
if (!existsSync4(cachePath))
return null;
try {
const parsed = JSON.parse(readFileSync5(cachePath, "utf-8"));
if (parsed && typeof parsed.checkedAt === "number" && typeof parsed.url === "string" && (typeof parsed.latest === "string" || parsed.latest === null)) {
return parsed;
}
} catch {
}
return null;
}
function writeVersionCache(entry, cachePath = DEFAULT_VERSION_CACHE_PATH) {
mkdirSync3(dirname2(cachePath), { recursive: true });
writeFileSync3(cachePath, JSON.stringify(entry));
}
function readFreshCachedLatestVersion(url, ttlMs = DEFAULT_VERSION_CACHE_TTL_MS, cachePath = DEFAULT_VERSION_CACHE_PATH, nowMs = Date.now()) {
const cached = readVersionCache(cachePath);
if (!cached || cached.url !== url)
return void 0;
if (nowMs - cached.checkedAt > ttlMs)
return void 0;
return cached.latest;
}
async function getLatestVersionCached(opts) {
const ttlMs = opts.ttlMs ?? DEFAULT_VERSION_CACHE_TTL_MS;
const cachePath = opts.cachePath ?? DEFAULT_VERSION_CACHE_PATH;
const nowMs = opts.nowMs ?? Date.now();
const fetchImpl = opts.fetchImpl ?? fetch;
const fresh = readFreshCachedLatestVersion(opts.url, ttlMs, cachePath, nowMs);
if (fresh !== void 0)
return fresh;
const stale = readVersionCache(cachePath);
try {
const res = await fetchImpl(opts.url, { signal: AbortSignal.timeout(opts.timeoutMs) });
const latest = res.ok ? (await res.json()).version ?? null : stale?.latest ?? null;
writeVersionCache({
checkedAt: nowMs,
latest,
url: opts.url
}, cachePath);
return latest;
} catch {
const latest = stale?.latest ?? null;
writeVersionCache({
checkedAt: nowMs,
latest,
url: opts.url
}, cachePath);
return latest;
}
}

// dist/src/utils/wiki-log.js
import { mkdirSync as mkdirSync3, appendFileSync as appendFileSync2 } from "node:fs";
import { join as join6 } from "node:path";
import { mkdirSync as mkdirSync4, appendFileSync as appendFileSync2 } from "node:fs";
import { join as join7 } from "node:path";
function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {
const path = join6(hooksDir, filename);
const path = join7(hooksDir, filename);
return {
path,
log(msg) {
try {
mkdirSync3(hooksDir, { recursive: true });
mkdirSync4(hooksDir, { recursive: true });
appendFileSync2(path, `[${utcTimestamp()}] ${msg}
`);
} catch {
Expand All @@ -489,8 +537,8 @@ function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {

// dist/src/hooks/session-start-setup.js
var log3 = (msg) => log("session-setup", msg);
var __bundleDir = dirname2(fileURLToPath(import.meta.url));
var { log: wikiLog } = makeWikiLogger(join7(homedir4(), ".claude", "hooks"));
var __bundleDir = dirname3(fileURLToPath(import.meta.url));
var { log: wikiLog } = makeWikiLogger(join8(homedir5(), ".claude", "hooks"));
async function main() {
if (process.env.HIVEMIND_WIKI_WORKER === "1")
return;
Expand Down Expand Up @@ -527,7 +575,7 @@ async function main() {
try {
const current = getInstalledVersion(__bundleDir, ".claude-plugin");
if (current) {
const latest = await getLatestVersion();
const latest = await getLatestVersionCached({ url: GITHUB_RAW_PKG, timeoutMs: 3e3 });
if (latest && isNewer(latest, current)) {
if (autoupdate) {
log3(`autoupdate: updating ${current} \u2192 ${latest}`);
Expand Down
96 changes: 72 additions & 24 deletions claude-code/bundle/session-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

// dist/src/hooks/session-start.js
import { fileURLToPath } from "node:url";
import { dirname as dirname2, join as join7 } from "node:path";
import { dirname as dirname3, join as join8 } from "node:path";
import { readdirSync, rmSync } from "node:fs";
import { execSync as execSync2 } from "node:child_process";
import { homedir as homedir4 } from "node:os";
import { homedir as homedir5 } from "node:os";

// dist/src/commands/auth.js
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
Expand Down Expand Up @@ -452,34 +452,82 @@ function getInstalledVersion(bundleDir, pluginManifestDir) {
}
return null;
}
async function getLatestVersion(timeoutMs = 3e3) {
try {
const res = await fetch(GITHUB_RAW_PKG, { signal: AbortSignal.timeout(timeoutMs) });
if (!res.ok)
return null;
const pkg = await res.json();
return pkg.version ?? null;
} catch {
return null;
}
}
function isNewer(latest, current) {
const parse = (v) => v.split(".").map(Number);
const [la, lb, lc] = parse(latest);
const [ca, cb, cc] = parse(current);
return la > ca || la === ca && lb > cb || la === ca && lb === cb && lc > cc;
}

// dist/src/hooks/version-check.js
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
import { dirname as dirname2, join as join6 } from "node:path";
import { homedir as homedir4 } from "node:os";
var DEFAULT_VERSION_CACHE_PATH = join6(homedir4(), ".deeplake", ".version-check.json");
var DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1e3;
function readVersionCache(cachePath = DEFAULT_VERSION_CACHE_PATH) {
if (!existsSync4(cachePath))
return null;
try {
const parsed = JSON.parse(readFileSync5(cachePath, "utf-8"));
if (parsed && typeof parsed.checkedAt === "number" && typeof parsed.url === "string" && (typeof parsed.latest === "string" || parsed.latest === null)) {
return parsed;
}
} catch {
}
return null;
}
function writeVersionCache(entry, cachePath = DEFAULT_VERSION_CACHE_PATH) {
mkdirSync3(dirname2(cachePath), { recursive: true });
writeFileSync3(cachePath, JSON.stringify(entry));
}
function readFreshCachedLatestVersion(url, ttlMs = DEFAULT_VERSION_CACHE_TTL_MS, cachePath = DEFAULT_VERSION_CACHE_PATH, nowMs = Date.now()) {
const cached = readVersionCache(cachePath);
if (!cached || cached.url !== url)
return void 0;
if (nowMs - cached.checkedAt > ttlMs)
return void 0;
return cached.latest;
}
async function getLatestVersionCached(opts) {
const ttlMs = opts.ttlMs ?? DEFAULT_VERSION_CACHE_TTL_MS;
const cachePath = opts.cachePath ?? DEFAULT_VERSION_CACHE_PATH;
const nowMs = opts.nowMs ?? Date.now();
const fetchImpl = opts.fetchImpl ?? fetch;
const fresh = readFreshCachedLatestVersion(opts.url, ttlMs, cachePath, nowMs);
if (fresh !== void 0)
return fresh;
const stale = readVersionCache(cachePath);
try {
const res = await fetchImpl(opts.url, { signal: AbortSignal.timeout(opts.timeoutMs) });
const latest = res.ok ? (await res.json()).version ?? null : stale?.latest ?? null;
writeVersionCache({
checkedAt: nowMs,
latest,
url: opts.url
}, cachePath);
return latest;
} catch {
const latest = stale?.latest ?? null;
writeVersionCache({
checkedAt: nowMs,
latest,
url: opts.url
}, cachePath);
return latest;
}
Comment on lines +510 to +518
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing checkedAt: nowMs in the catch block resets the full 1h TTL on any transient fetch failure (timeout, DNS hiccup). A brand-new install with a brief network blip will cache null and skip version checks for an entire hour. The same issue exists in the !res.ok branch (line 503) — a non-200 response also silently extends the TTL.

Skip the cache write on error and just return the stale value; retrying next session is preferable to a guaranteed 1h blackout. Fix should be applied in src/hooks/version-check.ts (the bundle is generated):

Suggested change
} catch {
const latest = stale?.latest ?? null;
writeVersionCache({
checkedAt: nowMs,
latest,
url: opts.url
}, cachePath);
return latest;
}
} catch {
return stale?.latest ?? null;
}

}

// dist/src/utils/wiki-log.js
import { mkdirSync as mkdirSync3, appendFileSync as appendFileSync2 } from "node:fs";
import { join as join6 } from "node:path";
import { mkdirSync as mkdirSync4, appendFileSync as appendFileSync2 } from "node:fs";
import { join as join7 } from "node:path";
function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {
const path = join6(hooksDir, filename);
const path = join7(hooksDir, filename);
return {
path,
log(msg) {
try {
mkdirSync3(hooksDir, { recursive: true });
mkdirSync4(hooksDir, { recursive: true });
appendFileSync2(path, `[${utcTimestamp()}] ${msg}
`);
} catch {
Expand All @@ -490,8 +538,8 @@ function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {

// dist/src/hooks/session-start.js
var log3 = (msg) => log("session-start", msg);
var __bundleDir = dirname2(fileURLToPath(import.meta.url));
var AUTH_CMD = join7(__bundleDir, "commands", "auth-login.js");
var __bundleDir = dirname3(fileURLToPath(import.meta.url));
var AUTH_CMD = join8(__bundleDir, "commands", "auth-login.js");
var context = `DEEPLAKE MEMORY: You have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:

1. Your built-in memory (~/.claude/) \u2014 personal per-project notes
Expand Down Expand Up @@ -522,8 +570,8 @@ IMPORTANT: Only use bash commands (cat, ls, grep, echo, jq, head, tail, etc.) to
LIMITS: Do NOT spawn subagents to read deeplake memory. If a file returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.

Debugging: Set HIVEMIND_DEBUG=1 to enable verbose logging to ~/.deeplake/hook-debug.log`;
var HOME = homedir4();
var { log: wikiLog } = makeWikiLogger(join7(HOME, ".claude", "hooks"));
var HOME = homedir5();
var { log: wikiLog } = makeWikiLogger(join8(HOME, ".claude", "hooks"));
async function createPlaceholder(api, table, sessionId, cwd, userName, orgName, workspaceId) {
const summaryPath = `/summaries/${userName}/${sessionId}.md`;
const existing = await api.query(`SELECT path FROM "${table}" WHERE path = '${sqlStr(summaryPath)}' LIMIT 1`);
Expand Down Expand Up @@ -592,7 +640,7 @@ async function main() {
try {
const current = getInstalledVersion(__bundleDir, ".claude-plugin");
if (current) {
const latest = await getLatestVersion();
const latest = await getLatestVersionCached({ url: GITHUB_RAW_PKG, timeoutMs: 3e3 });
if (latest && isNewer(latest, current)) {
if (autoupdate) {
log3(`autoupdate: updating ${current} \u2192 ${latest}`);
Expand All @@ -601,11 +649,11 @@ async function main() {
const cmd = scopes.map((s) => `claude plugin update hivemind@hivemind --scope ${s} 2>/dev/null || true`).join("; ");
execSync2(cmd, { stdio: "ignore", timeout: 6e4 });
try {
const cacheParent = join7(homedir4(), ".claude", "plugins", "cache", "hivemind", "hivemind");
const cacheParent = join8(homedir5(), ".claude", "plugins", "cache", "hivemind", "hivemind");
const entries = readdirSync(cacheParent, { withFileTypes: true });
for (const e of entries) {
if (e.isDirectory() && e.name !== latest) {
rmSync(join7(cacheParent, e.name), { recursive: true, force: true });
rmSync(join8(cacheParent, e.name), { recursive: true, force: true });
log3(`cache cleanup: removed old version ${e.name}`);
}
}
Expand Down
9 changes: 9 additions & 0 deletions claude-code/tests/codex-session-start-setup-hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

/**
* Source-level tests for src/hooks/codex/session-start-setup.ts. The
Expand Down Expand Up @@ -67,7 +70,11 @@ const validConfig = {
sessionsTableName: "sessions",
};

let cacheTmp: string;

beforeEach(() => {
cacheTmp = mkdtempSync(join(tmpdir(), "codex-session-start-setup-test-"));
process.env.HIVEMIND_VERSION_CACHE_PATH = join(cacheTmp, "cache.json");
stdinMock.mockReset().mockResolvedValue({
session_id: "sid-1", cwd: "/workspaces/proj",
hook_event_name: "SessionStart", model: "gpt-5",
Expand All @@ -92,6 +99,8 @@ afterEach(() => {
vi.restoreAllMocks();
// @ts-expect-error
global.fetch = originalFetch;
delete process.env.HIVEMIND_VERSION_CACHE_PATH;
try { rmSync(cacheTmp, { recursive: true, force: true }); } catch { /* ignore */ }
});

describe("codex session-start-setup hook — guards", () => {
Expand Down
2 changes: 2 additions & 0 deletions claude-code/tests/session-start-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ let cacheTmp: string;

beforeEach(() => {
cacheTmp = mkdtempSync(join(tmpdir(), "session-start-test-"));
process.env.HIVEMIND_VERSION_CACHE_PATH = join(cacheTmp, "cache.json");
stdinMock.mockReset().mockResolvedValue({ session_id: "sid-1", cwd: "/workspaces/proj" });
loadCredsMock.mockReset().mockReturnValue({
token: "tok", orgId: "o", orgName: "acme", userName: "alice", workspaceId: "default",
Expand All @@ -122,6 +123,7 @@ afterEach(() => {
vi.restoreAllMocks();
// @ts-expect-error
global.fetch = originalFetch;
delete process.env.HIVEMIND_VERSION_CACHE_PATH;
try { rmSync(cacheTmp, { recursive: true, force: true }); } catch { /* ignore */ }
});

Expand Down
9 changes: 9 additions & 0 deletions claude-code/tests/session-start-setup-hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

/**
* Source-level tests for src/hooks/session-start-setup.ts. This hook
Expand Down Expand Up @@ -63,7 +66,11 @@ const validConfig = {
sessionsTableName: "sessions",
};

let cacheTmp: string;

beforeEach(() => {
cacheTmp = mkdtempSync(join(tmpdir(), "session-start-setup-test-"));
process.env.HIVEMIND_VERSION_CACHE_PATH = join(cacheTmp, "cache.json");
stdinMock.mockReset().mockResolvedValue({ session_id: "sid-1", cwd: "/x" });
loadCredsMock.mockReset().mockReturnValue({
token: "tok", orgId: "o", orgName: "acme", userName: "alice",
Expand All @@ -84,6 +91,8 @@ afterEach(() => {
vi.restoreAllMocks();
// @ts-expect-error
global.fetch = originalFetch;
delete process.env.HIVEMIND_VERSION_CACHE_PATH;
try { rmSync(cacheTmp, { recursive: true, force: true }); } catch { /* ignore */ }
});

describe("session-start-setup hook — guards", () => {
Expand Down
Loading
Loading