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
4 changes: 2 additions & 2 deletions src/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* maxRules / maxGoals defaults of 10 are the v1 contract).
*/

import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { renderContextBlock } from "../hooks/shared/context-renderer.js";

Expand All @@ -46,7 +46,7 @@ export async function runContextCommand(args: string[]): Promise<void> {
return;
}

const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) {
console.error("Not logged in. Run `hivemind login` first.");
process.exit(2);
Expand Down
7 changes: 5 additions & 2 deletions src/commands/flush-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

import { existsSync, readFileSync } from "node:fs";

import { loadConfig, type Config } from "../config.js";
import { type Config } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { uploadSummary, type QueryFn, type UploadParams, type UploadResult } from "../hooks/upload-summary.js";
import { EmbedClient } from "../embeddings/client.js";
Expand Down Expand Up @@ -78,7 +79,9 @@ async function defaultEmbed(text: string): Promise<number[] | null> {

export function defaultDeps(pluginVersion?: string): FlushDeps {
return {
loadConfig,
// Route by cwd so a flush targets the directory's `.hivemind` workspace,
// consistent with capture (see src/dir-config.ts).
loadConfig: loadRoutedConfig,
makeQuery: (config) =>
(sql: string) =>
new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName).query(sql),
Expand Down
18 changes: 9 additions & 9 deletions src/commands/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
*/

import { randomUUID } from "node:crypto";
import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { sqlIdent, sqlStr } from "../utils/sql.js";

Expand Down Expand Up @@ -78,7 +78,7 @@ function parseAgentFlag(args: string[]): { agent: string; rest: string[] } {
}

function loadApiOrDie(table: string): { api: DeeplakeApi; query: QueryFn; userName: string } {
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) {
process.stderr.write("hivemind: not logged in. Run `hivemind login` first.\n");
process.exit(1);
Expand All @@ -97,7 +97,7 @@ function loadApiOrDie(table: string): { api: DeeplakeApi; query: QueryFn; userNa
// ── goal subcommands ────────────────────────────────────────────────────────

async function goalAdd(text: string, agent: string = "manual"): Promise<void> {
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) {
process.stderr.write("hivemind: not logged in.\n");
process.exit(1);
Expand Down Expand Up @@ -126,7 +126,7 @@ async function goalAdd(text: string, agent: string = "manual"): Promise<void> {
}

async function goalList(filter: "all" | "mine"): Promise<void> {
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
const { query } = loadApiOrDie(cfg.goalsTableName);
const safe = sqlIdent(cfg.goalsTableName);
Expand All @@ -152,7 +152,7 @@ async function goalList(filter: "all" | "mine"): Promise<void> {

async function goalGet(goalId: string): Promise<void> {
if (!goalId) { process.stderr.write("usage: hivemind goal get <goal_id>\n"); process.exit(1); }
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
const { query } = loadApiOrDie(cfg.goalsTableName);
const safe = sqlIdent(cfg.goalsTableName);
Expand Down Expand Up @@ -183,7 +183,7 @@ async function goalProgress(goalId: string, status: string): Promise<void> {
process.stderr.write(`invalid status: ${status} (expected opened|in_progress|closed)\n`);
process.exit(1);
}
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
const { api, query } = loadApiOrDie(cfg.goalsTableName);
// Heal the schema before the UPDATE: an upgraded workspace's preexisting
Expand Down Expand Up @@ -212,7 +212,7 @@ async function kpiAdd(args: string[]): Promise<void> {
process.exit(1);
}
const name = nameParts.length > 0 ? nameParts.join(" ") : kpiId;
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
const { api, query } = loadApiOrDie(cfg.kpisTableName);
await api.ensureKpisTable(cfg.kpisTableName);
Expand All @@ -237,7 +237,7 @@ async function kpiAdd(args: string[]): Promise<void> {

async function kpiList(goalId: string): Promise<void> {
if (!goalId) { process.stderr.write("usage: hivemind kpi list <goal_id>\n"); process.exit(1); }
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
const { query } = loadApiOrDie(cfg.kpisTableName);
const safe = sqlIdent(cfg.kpisTableName);
Expand Down Expand Up @@ -266,7 +266,7 @@ async function kpiBump(goalId: string, kpiId: string, deltaStr: string): Promise
process.stderr.write(`invalid delta: ${deltaStr}\n`);
process.exit(1);
}
const cfg = loadConfig();
const cfg = loadRoutedConfig();
if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
const { api, query } = loadApiOrDie(cfg.kpisTableName);
// Heal the schema before the UPDATE — same reason as goalProgress: a
Expand Down
8 changes: 4 additions & 4 deletions src/commands/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* (see ./rules-module commit).
*/

import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { getVersion } from "../cli/version.js";
import {
Expand Down Expand Up @@ -52,8 +52,8 @@ function logUsageAndExit(code = 1): never {
throw new Error("unreachable");
}

function requireConfig(): ReturnType<typeof loadConfig> & object {
const cfg = loadConfig();
function requireConfig(): ReturnType<typeof loadRoutedConfig> & object {
const cfg = loadRoutedConfig();
if (!cfg) {
console.error("Not logged in. Run `hivemind login` first.");
process.exit(2);
Expand All @@ -62,7 +62,7 @@ function requireConfig(): ReturnType<typeof loadConfig> & object {
return cfg;
}

function makeApi(cfg: NonNullable<ReturnType<typeof loadConfig>>): DeeplakeApi {
function makeApi(cfg: NonNullable<ReturnType<typeof loadRoutedConfig>>): DeeplakeApi {
return new DeeplakeApi(
cfg.token,
cfg.apiUrl,
Expand Down
5 changes: 3 additions & 2 deletions src/commands/skillify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { runPull, type PullSummary } from "../skillify/pull.js";
import { runPush } from "../skillify/push.js";
import { runUnpull } from "../skillify/unpull.js";
import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { runMineLocal } from "./mine-local.js";
import { renderSubcommandUsageBlock } from "../cli/skillify-spec.js";
Expand Down Expand Up @@ -221,7 +222,7 @@ async function pullSkills(args: string[]): Promise<void> {
else if (userOne) users = [userOne];
else if (usersMany) users = usersMany.split(",").map(s => s.trim()).filter(Boolean);

const config = loadConfig();
const config = loadRoutedConfig();
if (!config) {
console.error("Not logged in. Run: hivemind login");
process.exit(1);
Expand Down Expand Up @@ -285,7 +286,7 @@ async function pushSkills(args: string[]): Promise<void> {
throw new Error("Usage: hivemind skillify push <skill-name> [--from project|global] [--dry-run]");
}

const config = loadConfig();
const config = loadRoutedConfig();
if (!config) {
throw new Error("Not logged in. Run: hivemind login");
}
Expand Down
26 changes: 25 additions & 1 deletion src/dir-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import type { Config } from "./config.js";
import { loadConfig, type Config } from "./config.js";

/** Committed (shared) and local (personal, gitignored) filenames, local first. */
export const DIR_CONFIG_FILENAMES = [".hivemind.local", ".hivemind"] as const;
Expand Down Expand Up @@ -145,3 +145,27 @@ export function resolveDirConfig(
};
return { config, collect: found.raw.collect !== false, found };
}

/**
* THE single entry point for a workspace-scoped Config.
*
* Any code path that builds a `DeeplakeApi` against per-directory workspace data
* — CLI commands (goals, rules, skills), memory read/write hooks, recall — MUST
* get its config from here, never from a bare `loadConfig()`. It folds the
* nearest `.hivemind` (and the `HIVEMIND_*` env locks, via resolveDirConfig)
* into one place, so routing can never again be half-wired across call sites.
*
* Drop-in for `loadConfig()`: same `Config | null` shape, `cwd` defaults to the
* process cwd (correct for every CLI command). Hooks that carry an explicit
* session cwd pass it in. `collect` is intentionally NOT surfaced here — it
* gates capture only; callers on the capture path use `resolveDirConfig`
* directly so they can honor the opt-out.
*
* The guard in tests/shared/dir-config-single-source.test.ts enforces that
* workspace-touching modules call this and not `loadConfig()`.
*/
export function loadRoutedConfig(cwd: string = process.cwd()): Config | null {
const base = loadConfig();
if (!base) return null;
return resolveDirConfig(base, cwd).config;
}
7 changes: 5 additions & 2 deletions src/graph/deeplake-pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function workTreeIdFor(cwd: string): string {
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
}

import { loadConfig, type Config } from "../config.js";
import { type Config } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { sqlIdent, sqlStr } from "../utils/sql.js";
import { deriveProjectKey } from "../utils/repo-identity.js";
Expand Down Expand Up @@ -95,7 +96,9 @@ export async function pullSnapshot(
if (process.env.HIVEMIND_GRAPH_PULL === "0") {
return { kind: "skipped-disabled" };
}
const config = (deps.loadConfig ?? loadConfig)();
// Route by the caller's cwd so a pull lands from the same `.hivemind`
// workspace the graph was pushed to (deeplake-push routes identically).
const config = (deps.loadConfig ?? (() => loadRoutedConfig(cwd)))();
if (config === null) {
return { kind: "skipped-no-auth" };
}
Expand Down
9 changes: 8 additions & 1 deletion src/hooks/codex/pre-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import { readStdin } from "../../utils/stdin.js";
import { loadConfig } from "../../config.js";
import { resolveDirConfig } from "../../dir-config.js";
import { DeeplakeApi } from "../../deeplake-api.js";
import { sqlLike } from "../../utils/sql.js";
import { parseBashGrep, handleGrepDirect } from "../grep-direct.js";
Expand Down Expand Up @@ -120,7 +121,7 @@ export async function processCodexPreToolUse(
deps: CodexPreToolDeps = {},
): Promise<CodexPreToolDecision> {
const {
config = loadConfig(),
config: baseConfig = loadConfig(),
createApi = (table, activeConfig) => new DeeplakeApi(
activeConfig.token,
activeConfig.apiUrl,
Expand Down Expand Up @@ -148,6 +149,12 @@ export async function processCodexPreToolUse(
logFn = log,
} = deps;

// Route memory reads/writes through the nearest `.hivemind`, like capture —
// a directory pinned to another workspace must read/write THAT workspace, not
// the global one. Applied to the injected/default base alike (a no-op when no
// `.hivemind` applies). See src/dir-config.ts.
const config = baseConfig ? resolveDirConfig(baseConfig, input.cwd ?? process.cwd()).config : baseConfig;

const cmd = input.tool_input?.command ?? "";
logFn(`hook fired: cmd=${cmd}`);

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/cursor/pre-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

import { readStdin } from "../../utils/stdin.js";
import { deriveProjectKey } from "../../utils/repo-identity.js";
import { loadConfig } from "../../config.js";
import { loadRoutedConfig } from "../../dir-config.js";
import { DeeplakeApi } from "../../deeplake-api.js";
import { log as _log } from "../../utils/debug.js";
import { parseBashGrep, handleGrepDirect } from "../grep-direct.js";
Expand Down Expand Up @@ -82,7 +82,7 @@ async function main(): Promise<void> {
return;
}

const config = loadConfig();
const config = loadRoutedConfig(input.cwd ?? process.cwd());
if (!config) {
log("no config — falling through to Cursor's bash");
return;
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/hermes/pre-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

import { readStdin } from "../../utils/stdin.js";
import { loadConfig } from "../../config.js";
import { loadRoutedConfig } from "../../dir-config.js";
import { DeeplakeApi } from "../../deeplake-api.js";
import { log as _log } from "../../utils/debug.js";
import { parseBashGrep, handleGrepDirect } from "../grep-direct.js";
Expand Down Expand Up @@ -67,7 +67,7 @@ async function main(): Promise<void> {
const grepParams = parseBashGrep(rewritten);
if (!grepParams) return; // not grep/rg/egrep/fgrep — leave it alone

const config = loadConfig();
const config = loadRoutedConfig(input.cwd ?? process.cwd());
if (!config) {
log("no config — falling through to Hermes");
return;
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/session-start-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
import { loadCredentials, saveCredentials } from "../commands/auth.js";
import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { readStdin } from "../utils/stdin.js";
import { log as _log } from "../utils/debug.js";
Expand Down Expand Up @@ -54,7 +54,7 @@ async function main(): Promise<void> {

if (input.session_id) {
try {
const config = loadConfig();
const config = loadRoutedConfig(input.cwd ?? process.cwd());
if (config) {
const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
await api.ensureTable();
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import * as z from "zod/v3";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { loadCredentials } from "../commands/auth.js";
import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { isMissingTableError } from "../deeplake-schema.js";
import { sqlStr, sqlLike } from "../utils/sql.js";
Expand All @@ -40,7 +40,7 @@ function getContext(): ServerContext | { error: string } {
if (!creds?.token) {
return { error: "Not authenticated. Run `hivemind login` to sign in to Deeplake." };
}
const config = loadConfig();
const config = loadRoutedConfig();
if (!config) {
return { error: "Hivemind config could not be loaded — credentials present but invalid." };
}
Expand Down
11 changes: 8 additions & 3 deletions src/skillify/auto-pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
* exactly equivalent to `hivemind skillify pull --all-users --to global`.
*/

import { loadConfig, type Config } from "../config.js";
import { type Config } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { runPull, type QueryFn } from "./pull.js";
import { log as _log } from "../utils/debug.js";
Expand Down Expand Up @@ -80,8 +81,12 @@ export async function autoPullSkills(deps: AutoPullDeps = {}): Promise<AutoPullR
}

// Not logged in → silent skip (no nag).
const loadFn = deps.loadConfigFn ?? loadConfig;
const config = loadFn();
// Real callers get the routed config (nearest `.hivemind` for the session
// cwd); tests inject their own loadConfigFn. No inline default fn so the
// real path stays a plain call, not an extra uncovered closure.
const config = deps.loadConfigFn
? deps.loadConfigFn()
: loadRoutedConfig(deps.cwd ?? process.cwd());
if (!config) {
log("skipped: not logged in");
return { pulled: 0, skipped: true, reason: "not-logged-in" };
Expand Down
4 changes: 2 additions & 2 deletions src/skillify/skillopt-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import path from "node:path";
import { accessSync, constants as fsConstants } from "node:fs";
import { log as _log } from "../utils/debug.js";
import { loadConfig } from "../config.js";
import { loadRoutedConfig } from "../dir-config.js";
import { DeeplakeApi } from "../deeplake-api.js";
import { getStateDir } from "./state-dir.js";
import { agentModel, detectScorerAgent } from "./agent-model.js";
Expand Down Expand Up @@ -54,7 +54,7 @@ async function main(): Promise<void> {
const toolUseId = process.env[SKILLOPT_ENV.TOOL_USE_ID] || undefined;
if (!sessionId || !skillRef) { log("no session/skill in env — nothing to do"); return; }

const config = loadConfig();
const config = loadRoutedConfig();
if (!config?.token) { log("no config/credentials — exiting"); return; }

const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
Expand Down
Loading