@bcmyguest/openmem 0.0.0-stage → 0.8.0
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/.agents/skills/openmem/SKILL.md +34 -0
- package/LICENSE +190 -0
- package/README.md +140 -2
- package/bin/hindsight-project-index +530 -0
- package/cli/coding-agent/access-policy.mjs +38 -0
- package/cli/coding-agent/agents.mjs +34 -0
- package/cli/coding-agent/bank-from-cwd.mjs +140 -0
- package/cli/coding-agent/capabilities.mjs +34 -0
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +897 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +88 -0
- package/cli/coding-agent/hook-runner.mjs +284 -0
- package/cli/coding-agent/install-access.mjs +119 -0
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +201 -0
- package/cli/coding-agent/opencode-config.mjs +107 -0
- package/cli/coding-agent/opencode-plugin.mjs +132 -0
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +40 -0
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +86 -0
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +27 -0
- package/cli/install-coding-agent.mjs +298 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -0
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/context.mjs +61 -0
- package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
- package/cli/mcp/export/cognee.mjs +119 -0
- package/cli/mcp/export/hindsight.mjs +48 -0
- package/cli/mcp/export/index.mjs +26 -0
- package/cli/mcp/export/mem0.mjs +31 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +521 -0
- package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
- package/cli/mcp/notebooklm-banks.mjs +21 -0
- package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
- package/cli/mcp/notebooklm-provider.mjs +225 -0
- package/cli/mcp/pmm-records.mjs +23 -0
- package/cli/mcp/portability.mjs +158 -0
- package/cli/mcp/stdio.mjs +65 -0
- package/cli/mcp/unified-mcp-install.mjs +65 -0
- package/cli/mem0-mcp.mjs +108 -0
- package/cli/notebooklm-mcp.mjs +236 -0
- package/cli/openmem-mcp.mjs +38 -0
- package/cli/openmem.mjs +222 -0
- package/cli/roots.mjs +85 -0
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +438 -0
- package/cli/switch-memory-provider.mjs +358 -0
- package/cli/wiring-report.mjs +125 -0
- package/compose.bench.yaml +12 -0
- package/compose.yaml +227 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +26 -0
- package/config/index.yml +54 -0
- package/config/litellm.lemond.yaml +40 -0
- package/config/litellm.ollama.yaml +33 -0
- package/config/notebooklm.yml +33 -0
- package/config/openmem.toml.example +124 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +245 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +75 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +71 -0
- package/docs/cognee.md +37 -0
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +135 -0
- package/docs/context-management.md +255 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +215 -0
- package/docs/context-store.md +277 -0
- package/docs/hindsight.md +77 -0
- package/docs/index-sources-roadmap.md +38 -0
- package/docs/index-sources.md +39 -0
- package/docs/indexer-sinks.md +122 -0
- package/docs/indexing.md +57 -0
- package/docs/manual-stack-setup.md +74 -0
- package/docs/mcp-install-guide.md +4 -0
- package/docs/mcp-setup.md +104 -0
- package/docs/mem0.md +141 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +106 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +172 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +107 -0
- package/docs/setup-wizard.md +117 -0
- package/docs/sources-feeds.md +39 -0
- package/docs/sources-readwise.md +23 -0
- package/docs/sources-zotero.md +22 -0
- package/glama.json +4 -0
- package/indexer/Dockerfile +38 -0
- package/indexer/code_outline.py +146 -0
- package/indexer/feed_sync.py +107 -0
- package/indexer/index_config.py +290 -0
- package/indexer/pytest.ini +5 -0
- package/indexer/sinks/__init__.py +148 -0
- package/indexer/sinks/_http.py +44 -0
- package/indexer/sinks/cognee.py +329 -0
- package/indexer/sinks/hindsight.py +107 -0
- package/indexer/sinks/mem0.py +247 -0
- package/indexer/watch.py +240 -0
- package/package.json +96 -4
- package/providers/_template/compose.yaml +52 -0
- package/providers/cognee/OPERATOR.md +82 -0
- package/providers/cognee/compose.yaml +89 -0
- package/providers/cognee/lib/clack.mjs +157 -0
- package/providers/cognee/lib/coding-agent.mjs +26 -0
- package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
- package/providers/cognee/lib/config.mjs +98 -0
- package/providers/cognee/lib/env-file.mjs +194 -0
- package/providers/cognee/lib/env-setup.mjs +83 -0
- package/providers/cognee/lib/llm-env.mjs +382 -0
- package/providers/cognee/lib/ports.mjs +25 -0
- package/providers/cognee/lib/smoke.mjs +246 -0
- package/providers/cognee/lib/stack-env.mjs +134 -0
- package/providers/cognee/lib/state.mjs +101 -0
- package/providers/cognee/lib/summary.mjs +56 -0
- package/providers/cognee/lib/validate.mjs +97 -0
- package/providers/cognee/manifest.json +56 -0
- package/providers/cognee/wizard.mjs +134 -0
- package/providers/graphiti/manifest.json +15 -0
- package/providers/hindsight/lib/clack.mjs +347 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +79 -0
- package/providers/hindsight/lib/env-setup.mjs +72 -0
- package/providers/hindsight/lib/litellm-routes.mjs +196 -0
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +243 -0
- package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
- package/providers/hindsight/lib/ollama-compose.mjs +51 -0
- package/providers/hindsight/lib/ports.mjs +30 -0
- package/providers/hindsight/lib/probe.mjs +54 -0
- package/providers/hindsight/lib/stack-env.mjs +125 -0
- package/providers/hindsight/lib/state.mjs +80 -0
- package/providers/hindsight/lib/summary.mjs +57 -0
- package/providers/hindsight/lib/validate.mjs +81 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +150 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +143 -0
- package/providers/mem0/compose.yaml +79 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +201 -0
- package/providers/mem0/lib/dotenv.mjs +71 -0
- package/providers/mem0/lib/env-file.mjs +110 -0
- package/providers/mem0/lib/http.mjs +69 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/mcp-install.mjs +165 -0
- package/providers/mem0/lib/secret.mjs +30 -0
- package/providers/mem0/lib/smoke.mjs +179 -0
- package/providers/mem0/lib/stack-env.mjs +255 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +88 -0
- package/providers/mem0/lib/url.mjs +106 -0
- package/providers/mem0/lib/validate.mjs +158 -0
- package/providers/mem0/manifest.json +47 -0
- package/providers/mem0/wizard.mjs +146 -0
- package/providers/zep/manifest.json +29 -0
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +606 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +219 -0
- package/scripts/gen-masks.mjs +83 -0
- package/scripts/gen-masks.sh +4 -0
- package/scripts/promotion-provider-smoke.mjs +34 -0
- package/scripts/run-setup.sh +30 -0
- package/scripts/setup.mjs +107 -0
- package/scripts/setup.sh +5 -0
- package/scripts/spin-up.sh +45 -0
- package/scripts/wizard.sh +5 -0
- package/server.json +34 -0
- package/sync/Dockerfile +27 -0
- package/sync/memory_backend.py +87 -0
- package/sync/notebooklm_sync.py +664 -0
- package/wizard/bin/pm.mjs +97 -0
- package/wizard/bin/setup.mjs +33 -0
- package/wizard/lib/compose-version.mjs +32 -0
- package/wizard/lib/compose.mjs +70 -0
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/curl.mjs +103 -0
- package/wizard/lib/docker-build.mjs +148 -0
- package/wizard/lib/exec.mjs +41 -0
- package/wizard/lib/indexer.mjs +6 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/load-machine-env.mjs +4 -0
- package/wizard/lib/log.mjs +32 -0
- package/wizard/lib/notebooklm-agent-access.mjs +92 -0
- package/wizard/lib/notebooklm.mjs +214 -0
- package/wizard/lib/parse-args.mjs +87 -0
- package/wizard/lib/paths.mjs +39 -0
- package/wizard/lib/pm-config.mjs +371 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +85 -0
- package/wizard/lib/provider-wizard/helpers.mjs +98 -0
- package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
- package/wizard/lib/providers/registry.mjs +262 -0
- package/wizard/lib/run-clack.mjs +108 -0
- package/wizard/lib/run-wizard.mjs +750 -0
- package/wizard/lib/validate-setup.mjs +62 -0
- package/wizard/lib/wizard-action.mjs +34 -0
- package/wizard/lib/wizard-conf.mjs +71 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect-only "Claude subscription (Meridian)" backend: probe a Meridian proxy the user
|
|
3
|
+
* already runs, write the LiteLLM route from docs/meridian-litellm.md, and keep
|
|
4
|
+
* MERIDIAN_API_KEY in litellm.env. The wizard never installs or starts Meridian.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fetchStatus, openAiModelIds } from "./probe.mjs";
|
|
10
|
+
import { ensureLitellmEnv } from "./stack-env.mjs";
|
|
11
|
+
import { storedOr } from "./state.mjs";
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
14
|
+
|
|
15
|
+
export const MERIDIAN_DEFAULT_PORT = "3456";
|
|
16
|
+
export const MERIDIAN_DEFAULT_BASE = `http://127.0.0.1:${MERIDIAN_DEFAULT_PORT}`;
|
|
17
|
+
/** Guide defaults (docs/meridian-litellm.md#route-file). */
|
|
18
|
+
export const MERIDIAN_RETAIN_DEFAULT = "claude-haiku-5-5";
|
|
19
|
+
export const MERIDIAN_REFLECT_DEFAULT = "claude-sonnet-5-5";
|
|
20
|
+
export const MERIDIAN_TERMS_URL = "https://www.anthropic.com/legal/consumer-terms";
|
|
21
|
+
export const MERIDIAN_GUIDE = "docs/meridian-litellm.md";
|
|
22
|
+
/** litellm.env value when Meridian runs without a key; LiteLLM still needs a non-empty one. */
|
|
23
|
+
export const MERIDIAN_NO_KEY = "placeholder";
|
|
24
|
+
|
|
25
|
+
const BRIDGE_TIMEOUT_MS = 2000;
|
|
26
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
27
|
+
|
|
28
|
+
/** IPv4 address of the default Docker bridge (docker0), which `host-gateway` resolves to. */
|
|
29
|
+
export function dockerBridgeIp() {
|
|
30
|
+
const nics = os.networkInterfaces().docker0 || [];
|
|
31
|
+
const v4 = nics.find((n) => n.family === "IPv4" || n.family === 4);
|
|
32
|
+
return v4 ? v4.address : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {string} base */
|
|
36
|
+
export function normalizeMeridianBase(base) {
|
|
37
|
+
return base.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {string} hostname */
|
|
41
|
+
function isLoopback(hostname) {
|
|
42
|
+
return hostname === "localhost" || hostname === "[::1]" || hostname.startsWith("127.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {URL} u */
|
|
46
|
+
function portOf(u) {
|
|
47
|
+
return u.port || (u.protocol === "https:" ? "443" : "80");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The api_base the LiteLLM container uses: host.docker.internal for a loopback URL,
|
|
52
|
+
* otherwise the URL itself. No /v1: LiteLLM appends /v1/messages.
|
|
53
|
+
* @param {string} hostBase
|
|
54
|
+
*/
|
|
55
|
+
export function containerApiBase(hostBase) {
|
|
56
|
+
const u = new URL(hostBase);
|
|
57
|
+
if (!isLoopback(u.hostname)) return `${u.protocol}//${u.host}`;
|
|
58
|
+
return `http://host.docker.internal:${portOf(u)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* First usable model: a valid candidate, else `preferred` when listed, else the first id
|
|
63
|
+
* matching `family`, else the first id, else `preferred`.
|
|
64
|
+
* @param {string[]} ids ids from Meridian's /v1/models (may be empty)
|
|
65
|
+
* @param {string} preferred
|
|
66
|
+
* @param {RegExp} family
|
|
67
|
+
* @param {string[]} candidates explicit or stored choices, in priority order
|
|
68
|
+
*/
|
|
69
|
+
export function pickModel(ids, preferred, family, candidates = []) {
|
|
70
|
+
const valid = (m) => Boolean(m) && (ids.length ? ids.includes(m) : m.startsWith("claude-"));
|
|
71
|
+
const hit = candidates.find(valid);
|
|
72
|
+
if (hit) return hit;
|
|
73
|
+
if (!ids.length || ids.includes(preferred)) return preferred;
|
|
74
|
+
return ids.find((id) => family.test(id)) || ids[0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where an existing Meridian key comes from: the environment, Meridian's own env file
|
|
79
|
+
* (the guide's ~/.config/meridian/meridian.env), or litellm.env.
|
|
80
|
+
* @param {Record<string, any>} ctx
|
|
81
|
+
* @param {Helpers} h
|
|
82
|
+
* @returns {{ key: string, source: string }}
|
|
83
|
+
*/
|
|
84
|
+
export function existingMeridianKey(ctx, h) {
|
|
85
|
+
if (process.env.MERIDIAN_API_KEY) {
|
|
86
|
+
return { key: process.env.MERIDIAN_API_KEY, source: "MERIDIAN_API_KEY from the environment" };
|
|
87
|
+
}
|
|
88
|
+
const fromMeridian = h.envValue(ctx.meridianEnvFile, "MERIDIAN_API_KEY");
|
|
89
|
+
if (fromMeridian) return { key: fromMeridian, source: ctx.meridianEnvFile };
|
|
90
|
+
const fromLitellm = h.envValue(ctx.litellmEnv, "MERIDIAN_API_KEY");
|
|
91
|
+
if (fromLitellm && fromLitellm !== MERIDIAN_NO_KEY) return { key: fromLitellm, source: ctx.litellmEnv };
|
|
92
|
+
return { key: "", source: "" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Probe Meridian from the host: /health (open), /v1/models (needs the key when set), and
|
|
97
|
+
* /health on the Docker bridge address to tell a loopback-only bind from 0.0.0.0.
|
|
98
|
+
* @param {string} base host URL without /v1
|
|
99
|
+
* @param {string} key MERIDIAN_API_KEY or ""
|
|
100
|
+
* @param {string} bridgeIp docker0 IPv4, or "" when unknown
|
|
101
|
+
*/
|
|
102
|
+
export async function probeMeridian(base, key, bridgeIp) {
|
|
103
|
+
const headers = key ? { "x-api-key": key } : {};
|
|
104
|
+
const health = await fetchStatus(`${base}/health`, { timeoutMs: PROBE_TIMEOUT_MS });
|
|
105
|
+
const models = await fetchStatus(`${base}/v1/models`, { headers, timeoutMs: PROBE_TIMEOUT_MS });
|
|
106
|
+
const u = new URL(base);
|
|
107
|
+
/** @type {"ok"|"unreachable"|"unknown"|"n/a"} */
|
|
108
|
+
let bridge = "n/a";
|
|
109
|
+
const answered = health.status > 0 || models.status > 0;
|
|
110
|
+
if (answered && isLoopback(u.hostname)) {
|
|
111
|
+
if (!bridgeIp) bridge = "unknown";
|
|
112
|
+
else {
|
|
113
|
+
const viaBridge = await fetchStatus(`http://${bridgeIp}:${portOf(u)}/health`, {
|
|
114
|
+
timeoutMs: BRIDGE_TIMEOUT_MS,
|
|
115
|
+
});
|
|
116
|
+
bridge = viaBridge.status > 0 ? "ok" : "unreachable";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
answered,
|
|
121
|
+
healthOk: health.status >= 200 && health.status < 300,
|
|
122
|
+
modelsStatus: models.status,
|
|
123
|
+
ids: models.status === 200 ? openAiModelIds(models.json) : [],
|
|
124
|
+
bridge,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {string[]} names @param {Helpers} h */
|
|
129
|
+
function showModels(names, h) {
|
|
130
|
+
for (const n of names) h.info(` ${n}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What the probe says about reachability from the LiteLLM container.
|
|
135
|
+
* @param {{ meridianBase: string, apiBase: string, dockerBridgeIp: string, meridianEnvFile: string }} ctx
|
|
136
|
+
* @param {(file: string, key: string) => string} envValue
|
|
137
|
+
* @param {Awaited<ReturnType<typeof probeMeridian>>} probe
|
|
138
|
+
* @returns {{ level: "ok"|"warn"|"info", text: string } | null}
|
|
139
|
+
*/
|
|
140
|
+
export function bridgeReport(ctx, envValue, probe) {
|
|
141
|
+
const u = new URL(ctx.meridianBase);
|
|
142
|
+
const port = portOf(u);
|
|
143
|
+
const bindHint = `Restart Meridian with MERIDIAN_HOST=0.0.0.0 (${MERIDIAN_GUIDE}#run-meridian).`;
|
|
144
|
+
const declaredHost = process.env.MERIDIAN_HOST || envValue(ctx.meridianEnvFile, "MERIDIAN_HOST");
|
|
145
|
+
if (probe.bridge === "ok") {
|
|
146
|
+
return {
|
|
147
|
+
level: "ok",
|
|
148
|
+
text: `Meridian also answers on the Docker bridge address ${ctx.dockerBridgeIp}:${port}; LiteLLM calls ${ctx.apiBase}. A firewall can still block containers; run the guide's smoke test.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (probe.bridge === "unreachable") {
|
|
152
|
+
return {
|
|
153
|
+
level: "warn",
|
|
154
|
+
text: `Meridian answers on ${u.host} but not on the Docker bridge address ${ctx.dockerBridgeIp}:${port}: it binds 127.0.0.1 only, so the LiteLLM container cannot reach ${ctx.apiBase}. ${bindHint}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (declaredHost && isLoopback(declaredHost)) {
|
|
158
|
+
return {
|
|
159
|
+
level: "warn",
|
|
160
|
+
text: `MERIDIAN_HOST is ${declaredHost}: the LiteLLM container cannot reach a loopback-only Meridian. ${bindHint}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (isLoopback(u.hostname)) {
|
|
164
|
+
return {
|
|
165
|
+
level: "info",
|
|
166
|
+
text: `LiteLLM calls Meridian at ${ctx.apiBase}. On Linux, a Meridian bound to 127.0.0.1 (its default) is unreachable from the container; run it with MERIDIAN_HOST=0.0.0.0.`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Interactive and --yes Meridian questions. Fills ctx.meridianBase, ctx.meridianKey,
|
|
174
|
+
* ctx.apiBase, ctx.model and ctx.reflectModel.
|
|
175
|
+
* @param {Record<string, any>} ctx
|
|
176
|
+
* @param {Helpers} h
|
|
177
|
+
*/
|
|
178
|
+
export async function chooseMeridian(ctx, h) {
|
|
179
|
+
h.info(`The wizard does not install or start Meridian; see ${MERIDIAN_GUIDE}.`);
|
|
180
|
+
if (ctx.dockerBridgeIp === undefined) ctx.dockerBridgeIp = dockerBridgeIp();
|
|
181
|
+
const port = h.envValue(ctx.meridianEnvFile, "MERIDIAN_PORT") || MERIDIAN_DEFAULT_PORT;
|
|
182
|
+
const defaultBase =
|
|
183
|
+
process.env.MERIDIAN_BASE ||
|
|
184
|
+
storedOr(ctx, h, "meridian", "MEMORY_MERIDIAN_URL", `http://127.0.0.1:${port}`);
|
|
185
|
+
ctx.meridianBase = normalizeMeridianBase(await h.ask(ctx, "Meridian URL from this host", defaultBase));
|
|
186
|
+
try {
|
|
187
|
+
ctx.apiBase = containerApiBase(ctx.meridianBase);
|
|
188
|
+
} catch {
|
|
189
|
+
h.die(`not a URL: ${ctx.meridianBase}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const existing = existingMeridianKey(ctx, h);
|
|
193
|
+
const typed = await h.ask(
|
|
194
|
+
ctx,
|
|
195
|
+
`MERIDIAN_API_KEY (typed input is visible; blank uses ${existing.source || "no key"})`,
|
|
196
|
+
"",
|
|
197
|
+
);
|
|
198
|
+
ctx.meridianKey = typed || existing.key;
|
|
199
|
+
|
|
200
|
+
const probe = await probeMeridian(ctx.meridianBase, ctx.meridianKey, ctx.dockerBridgeIp);
|
|
201
|
+
if (!probe.answered) {
|
|
202
|
+
h.warn(
|
|
203
|
+
`Meridian is not answering on ${ctx.meridianBase}. Start it first (npm install -g @rynfar/meridian; claude login; meridian), see ${MERIDIAN_GUIDE}. The route uses the guide's default models.`,
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
h.ok(`Meridian answers on ${ctx.meridianBase}${probe.healthOk ? "" : " (/health not OK)"}`);
|
|
207
|
+
if (probe.modelsStatus === 401 || probe.modelsStatus === 403) {
|
|
208
|
+
h.warn(
|
|
209
|
+
`Meridian rejected ${ctx.meridianKey ? "the key" : "a request without a key"} on /v1/models (HTTP ${probe.modelsStatus}): set MERIDIAN_API_KEY to the key Meridian runs with.`,
|
|
210
|
+
);
|
|
211
|
+
} else if (probe.ids.length) {
|
|
212
|
+
h.ok(`Meridian lists ${probe.ids.length} models`);
|
|
213
|
+
showModels(probe.ids, h);
|
|
214
|
+
} else h.warn(`Meridian returned no model list (HTTP ${probe.modelsStatus}); using the guide's defaults`);
|
|
215
|
+
}
|
|
216
|
+
const bridge = bridgeReport(ctx, h.envValue, probe);
|
|
217
|
+
if (bridge) h[bridge.level](bridge.text);
|
|
218
|
+
if (!ctx.meridianKey) {
|
|
219
|
+
h.warn("No MERIDIAN_API_KEY: with MERIDIAN_HOST=0.0.0.0 anyone on the network can use your subscription.");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const retainDefault = pickModel(probe.ids, MERIDIAN_RETAIN_DEFAULT, /haiku/, [
|
|
223
|
+
process.env.PM_DEFAULT_MODEL,
|
|
224
|
+
storedOr(ctx, h, "meridian", "MEMORY_MODEL", ""),
|
|
225
|
+
]);
|
|
226
|
+
const reflectDefault = pickModel(probe.ids, MERIDIAN_REFLECT_DEFAULT, /sonnet/, [
|
|
227
|
+
process.env.PM_REFLECT_MODEL,
|
|
228
|
+
storedOr(ctx, h, "meridian", "MEMORY_REFLECT_MODEL", ""),
|
|
229
|
+
]);
|
|
230
|
+
ctx.model = await h.ask(ctx, "Meridian retain / consolidate model", retainDefault);
|
|
231
|
+
ctx.reflectModel = await h.ask(ctx, "Meridian reflect model (needs tool calls)", reflectDefault);
|
|
232
|
+
for (const m of [ctx.model, ctx.reflectModel]) {
|
|
233
|
+
if (probe.ids.length && !probe.ids.includes(m)) h.warn(`${m} is not listed by Meridian's /v1/models`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* One alias of the guide's route file.
|
|
239
|
+
* @param {string} alias
|
|
240
|
+
* @param {string} model
|
|
241
|
+
* @param {string} apiBase
|
|
242
|
+
*/
|
|
243
|
+
function renderAlias(alias, model, apiBase) {
|
|
244
|
+
return (
|
|
245
|
+
` - model_name: ${alias}\n` +
|
|
246
|
+
" litellm_params:\n" +
|
|
247
|
+
` model: anthropic/${model}\n` +
|
|
248
|
+
` api_base: ${apiBase}\n` +
|
|
249
|
+
" api_key: os.environ/MERIDIAN_API_KEY\n" +
|
|
250
|
+
" max_tokens: 4096\n" +
|
|
251
|
+
" timeout: 300\n" +
|
|
252
|
+
' additional_drop_params: ["reasoning_effort"]\n'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The route file from docs/meridian-litellm.md#route-file. The key stays in litellm.env.
|
|
258
|
+
* @param {Record<string, any>} ctx
|
|
259
|
+
*/
|
|
260
|
+
export function renderMeridianRoutes(ctx) {
|
|
261
|
+
const reflect = ctx.reflectModel || ctx.model;
|
|
262
|
+
return (
|
|
263
|
+
"# Written by ./setup: LiteLLM routes for Hindsight, backend Meridian on the host (Claude subscription).\n" +
|
|
264
|
+
`# See ${MERIDIAN_GUIDE}. MERIDIAN_API_KEY lives in ~/.config/hindsight/litellm.env.\n` +
|
|
265
|
+
"# No thinking parameter: Meridian rejects thinking: disabled on Haiku 5.5, so reasoning_effort is dropped.\n" +
|
|
266
|
+
"model_list:\n" +
|
|
267
|
+
renderAlias("memory-retain", ctx.model, ctx.apiBase) +
|
|
268
|
+
renderAlias("memory-reflect", reflect, ctx.apiBase) +
|
|
269
|
+
renderAlias("memory-consolidate", ctx.model, ctx.apiBase) +
|
|
270
|
+
"\ngeneral_settings:\n master_key: os.environ/LITELLM_MASTER_KEY\n"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set MERIDIAN_API_KEY in litellm.env (mode 600), creating the file with a master key
|
|
276
|
+
* when it does not exist yet. Never prints the value.
|
|
277
|
+
* @param {Record<string, any>} ctx
|
|
278
|
+
* @param {Helpers} h
|
|
279
|
+
*/
|
|
280
|
+
export function writeMeridianKey(ctx, h) {
|
|
281
|
+
const value = ctx.meridianKey || h.envValue(ctx.litellmEnv, "MERIDIAN_API_KEY") || MERIDIAN_NO_KEY;
|
|
282
|
+
if (/[\r\n]/.test(value)) h.die("MERIDIAN_API_KEY must be one line");
|
|
283
|
+
if (ctx.dry) {
|
|
284
|
+
h.info(`[dry-run] set MERIDIAN_API_KEY in ${ctx.litellmEnv} (mode 600; value not printed)`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
h.ensureConfigDir(ctx);
|
|
288
|
+
ensureLitellmEnv(ctx.litellmEnv);
|
|
289
|
+
const lines = fs.readFileSync(ctx.litellmEnv, "utf8").replace(/\n$/, "").split("\n");
|
|
290
|
+
const line = `MERIDIAN_API_KEY=${value}`;
|
|
291
|
+
const at = lines.findIndex((l) => l.startsWith("MERIDIAN_API_KEY="));
|
|
292
|
+
if (at >= 0 && lines[at] === line) {
|
|
293
|
+
fs.chmodSync(ctx.litellmEnv, 0o600);
|
|
294
|
+
h.ok(`${ctx.litellmEnv} already has MERIDIAN_API_KEY`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (at >= 0) lines[at] = line;
|
|
298
|
+
else {
|
|
299
|
+
if (value === MERIDIAN_NO_KEY) lines.push("# Meridian runs without MERIDIAN_API_KEY; LiteLLM needs a non-empty value.");
|
|
300
|
+
lines.push(line);
|
|
301
|
+
}
|
|
302
|
+
const tmp = path.join(path.dirname(ctx.litellmEnv), `.litellm.env.${process.pid}`);
|
|
303
|
+
fs.writeFileSync(tmp, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
304
|
+
fs.chmodSync(tmp, 0o600);
|
|
305
|
+
fs.renameSync(tmp, ctx.litellmEnv);
|
|
306
|
+
h.ok(`set MERIDIAN_API_KEY in ${ctx.litellmEnv} (mode 600)`);
|
|
307
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hindsight model routing: how the LiteLLM proxy in front of Hindsight reaches an LLM
|
|
3
|
+
* (lemond, Ollama, a custom OpenAI-compatible URL or Meridian), plus the reflect-model question and
|
|
4
|
+
* `lemonade pull`. The meta wizard knows none of this; it calls configureModelRouting.
|
|
5
|
+
*/
|
|
6
|
+
import { applyRoute, litellmConfig, saveConf } from "./litellm-routes.mjs";
|
|
7
|
+
import {
|
|
8
|
+
chooseMeridian,
|
|
9
|
+
containerApiBase,
|
|
10
|
+
existingMeridianKey,
|
|
11
|
+
MERIDIAN_DEFAULT_BASE,
|
|
12
|
+
MERIDIAN_REFLECT_DEFAULT,
|
|
13
|
+
normalizeMeridianBase,
|
|
14
|
+
writeMeridianKey,
|
|
15
|
+
} from "./meridian.mjs";
|
|
16
|
+
import { ollamaModelNames, openAiModelIds } from "./probe.mjs";
|
|
17
|
+
import {
|
|
18
|
+
backendDefault,
|
|
19
|
+
ensureState,
|
|
20
|
+
OLLAMA_COMPOSE_API_BASE,
|
|
21
|
+
OLLAMA_HOST_API_BASE,
|
|
22
|
+
storedOr,
|
|
23
|
+
} from "./state.mjs";
|
|
24
|
+
|
|
25
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Record<string, any>} ctx
|
|
29
|
+
* @param {Helpers} h
|
|
30
|
+
*/
|
|
31
|
+
async function pickBackend(ctx, h) {
|
|
32
|
+
const def = backendDefault(ctx, h);
|
|
33
|
+
if (ctx.yes) {
|
|
34
|
+
ctx.backend = def;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
h.info("Pick a backend first; only that server is contacted.");
|
|
38
|
+
h.info("1) lemond 2) ollama 3) custom OpenAI-compatible URL 4) Claude subscription (Meridian)");
|
|
39
|
+
const defNum = { lemond: "1", ollama: "2", custom: "3", meridian: "4" }[def] || "1";
|
|
40
|
+
const choice = await h.ask(ctx, "Model backend (1-4)", defNum);
|
|
41
|
+
switch (choice) {
|
|
42
|
+
case "1":
|
|
43
|
+
ctx.backend = "lemond";
|
|
44
|
+
break;
|
|
45
|
+
case "2":
|
|
46
|
+
ctx.backend = "ollama";
|
|
47
|
+
break;
|
|
48
|
+
case "3":
|
|
49
|
+
ctx.backend = "custom";
|
|
50
|
+
break;
|
|
51
|
+
case "4":
|
|
52
|
+
ctx.backend = "meridian";
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
h.die("choose 1, 2, 3 or 4");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
60
|
+
function probeLemond(ctx, h) {
|
|
61
|
+
const body = h.curlJson(`${ctx.lemondBase}/v1/models`);
|
|
62
|
+
ctx.lemondJson = body;
|
|
63
|
+
ctx.lemondNames = openAiModelIds(typeof body === "object" ? body : null);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
67
|
+
function probeOllama(ctx, h) {
|
|
68
|
+
const body = h.curlJson(`${ctx.ollamaBase}/api/tags`);
|
|
69
|
+
ctx.ollamaJson = body;
|
|
70
|
+
ctx.ollamaNames = ollamaModelNames(body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @param {string[]} names @param {Helpers} h */
|
|
74
|
+
function showModels(names, h) {
|
|
75
|
+
for (const n of names) h.info(` ${n}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
79
|
+
async function askReflectModel(ctx, h) {
|
|
80
|
+
const defaultAnswer =
|
|
81
|
+
process.env.PM_REFLECT_MODEL ||
|
|
82
|
+
storedOr(ctx, h, ctx.backend, "MEMORY_REFLECT_MODEL", "");
|
|
83
|
+
const answer = await h.ask(
|
|
84
|
+
ctx,
|
|
85
|
+
"Separate reflect model (type shipped for the shipped reflect route)",
|
|
86
|
+
defaultAnswer,
|
|
87
|
+
);
|
|
88
|
+
if (answer === "" || answer === "shipped") ctx.reflectModel = "";
|
|
89
|
+
else ctx.reflectModel = answer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
93
|
+
async function pullLemondModels(ctx, h) {
|
|
94
|
+
if (!ctx.lemondJson) return;
|
|
95
|
+
const names = new Set();
|
|
96
|
+
for (const line of litellmConfig(ctx).split("\n")) {
|
|
97
|
+
const m = line.match(/^\s*model: openai\/(.+)/);
|
|
98
|
+
if (m) names.add(m[1]);
|
|
99
|
+
}
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
if (!name || ctx.lemondNames.includes(name)) continue;
|
|
102
|
+
h.warn(`${name} is not downloaded in lemond`);
|
|
103
|
+
if (await h.confirm(ctx, `Run lemonade pull ${name} now?`, "y")) {
|
|
104
|
+
if (h.commandExists("lemonade")) {
|
|
105
|
+
await h.act(ctx, `lemonade pull ${name}`, "lemonade", ["pull", name]);
|
|
106
|
+
} else h.warn(`lemonade is not on PATH: run 'lemonade pull ${name}' on this host`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
112
|
+
async function chooseLemond(ctx, h) {
|
|
113
|
+
ctx.apiBase = "http://host.docker.internal:13305/v1";
|
|
114
|
+
const defaultModel = storedOr(
|
|
115
|
+
ctx,
|
|
116
|
+
h,
|
|
117
|
+
"lemond",
|
|
118
|
+
"MEMORY_MODEL",
|
|
119
|
+
process.env.PM_DEFAULT_MODEL || ctx.lemondDefaultModel,
|
|
120
|
+
);
|
|
121
|
+
if (!ctx.lemondJson) {
|
|
122
|
+
h.warn(`lemond is not answering on ${ctx.lemondBase}; the model list is unavailable`);
|
|
123
|
+
} else if (!ctx.lemondNames.includes(ctx.lemondDefaultModel)) {
|
|
124
|
+
h.warn(`${ctx.lemondDefaultModel} is not listed by lemond`);
|
|
125
|
+
}
|
|
126
|
+
ctx.model = await h.ask(ctx, "lemond model id", defaultModel);
|
|
127
|
+
await askReflectModel(ctx, h);
|
|
128
|
+
await pullLemondModels(ctx, h);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
132
|
+
async function chooseOllama(ctx, h) {
|
|
133
|
+
const defaultModel = storedOr(
|
|
134
|
+
ctx,
|
|
135
|
+
h,
|
|
136
|
+
"ollama",
|
|
137
|
+
"MEMORY_MODEL",
|
|
138
|
+
process.env.PM_DEFAULT_MODEL || ctx.ollamaDefaultModel,
|
|
139
|
+
);
|
|
140
|
+
// Compose Ollama publishes 127.0.0.1:11434, so a saved compose choice must win over the host probe.
|
|
141
|
+
if (storedOr(ctx, h, "ollama", "MEMORY_OLLAMA_IN_COMPOSE", "0") === "1") {
|
|
142
|
+
ctx.ollamaInCompose = true;
|
|
143
|
+
ctx.apiBase = OLLAMA_COMPOSE_API_BASE;
|
|
144
|
+
h.info("Keeping Ollama in Docker (compose profile ollama); LiteLLM calls http://ollama:11434.");
|
|
145
|
+
} else if (ctx.ollamaJson) {
|
|
146
|
+
ctx.ollamaInCompose = false;
|
|
147
|
+
ctx.apiBase = OLLAMA_HOST_API_BASE;
|
|
148
|
+
} else {
|
|
149
|
+
h.warn(`Ollama is not answering on ${ctx.ollamaBase}`);
|
|
150
|
+
const useDocker = await h.confirm(
|
|
151
|
+
ctx,
|
|
152
|
+
"Ollama is not on this host. Run it in Docker (compose profile ollama)?",
|
|
153
|
+
"y",
|
|
154
|
+
);
|
|
155
|
+
ctx.ollamaInCompose = useDocker;
|
|
156
|
+
ctx.apiBase = useDocker ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
|
|
157
|
+
if (useDocker) {
|
|
158
|
+
h.info("LiteLLM will call http://ollama:11434; the wizard pulls the model after the stack starts.");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
ctx.model = await h.ask(ctx, "Ollama model", defaultModel);
|
|
162
|
+
await askReflectModel(ctx, h);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @param {Record<string, any>} ctx @param {Helpers} h */
|
|
166
|
+
async function chooseCustom(ctx, h) {
|
|
167
|
+
const defaultBase = storedOr(ctx, h, "custom", "MEMORY_API_BASE", ctx.customDefaultBase);
|
|
168
|
+
const defaultModel = storedOr(ctx, h, "custom", "MEMORY_MODEL", ctx.lemondDefaultModel);
|
|
169
|
+
h.info("The litellm container calls this URL. Host services use host.docker.internal.");
|
|
170
|
+
h.info("Example: lemond http://host.docker.internal:13305/v1. For a Claude subscription through Meridian, pick backend 4.");
|
|
171
|
+
ctx.apiBase = await h.ask(ctx, "OpenAI-compatible base URL", defaultBase);
|
|
172
|
+
ctx.model = await h.ask(ctx, "Model id the server serves", defaultModel);
|
|
173
|
+
await askReflectModel(ctx, h);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Step "3. Model backend": choose lemond, Ollama, a custom URL or Meridian, write the LiteLLM route
|
|
178
|
+
* override when the choice differs from the shipped routes, and record it in wizard.conf.
|
|
179
|
+
* With the Clack UI done (ctx.skipBackendStep) the answers come from PM_BACKEND,
|
|
180
|
+
* PM_DEFAULT_MODEL, MEMORY_API_BASE and PM_REFLECT_MODEL instead (Meridian adds
|
|
181
|
+
* MERIDIAN_BASE and MERIDIAN_API_KEY).
|
|
182
|
+
*
|
|
183
|
+
* @param {Record<string, any>} ctx
|
|
184
|
+
* @param {Helpers} h
|
|
185
|
+
*/
|
|
186
|
+
export async function chooseModelBackend(ctx, h) {
|
|
187
|
+
ensureState(ctx);
|
|
188
|
+
h.step("3. Model backend");
|
|
189
|
+
if (ctx.skipBackendStep) {
|
|
190
|
+
if (!process.env.PM_BACKEND) h.die("PM_BACKEND missing with WIZARD_SKIP_BACKEND_STEP");
|
|
191
|
+
if (!process.env.PM_DEFAULT_MODEL) h.die("PM_DEFAULT_MODEL missing with WIZARD_SKIP_BACKEND_STEP");
|
|
192
|
+
ctx.backend = process.env.PM_BACKEND;
|
|
193
|
+
ctx.model = process.env.PM_DEFAULT_MODEL;
|
|
194
|
+
ctx.apiBase = process.env.MEMORY_API_BASE || "";
|
|
195
|
+
ctx.reflectModel = process.env.PM_REFLECT_MODEL || "";
|
|
196
|
+
ctx.ollamaInCompose = process.env.MEMORY_OLLAMA_IN_COMPOSE === "1";
|
|
197
|
+
if (ctx.backend === "ollama" && !ctx.apiBase) {
|
|
198
|
+
ctx.apiBase = ctx.ollamaInCompose ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
|
|
199
|
+
}
|
|
200
|
+
if (ctx.backend === "meridian") {
|
|
201
|
+
ctx.meridianBase = normalizeMeridianBase(process.env.MERIDIAN_BASE || MERIDIAN_DEFAULT_BASE);
|
|
202
|
+
ctx.apiBase ||= containerApiBase(ctx.meridianBase);
|
|
203
|
+
ctx.reflectModel ||= MERIDIAN_REFLECT_DEFAULT;
|
|
204
|
+
ctx.meridianKey = existingMeridianKey(ctx, h).key;
|
|
205
|
+
}
|
|
206
|
+
h.ok(`backend ${ctx.backend}, model ${ctx.model} (setup UI)`);
|
|
207
|
+
await applyRoute(ctx, h);
|
|
208
|
+
if (ctx.backend === "meridian") writeMeridianKey(ctx, h);
|
|
209
|
+
saveConf(ctx, h);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await pickBackend(ctx, h);
|
|
213
|
+
switch (ctx.backend) {
|
|
214
|
+
case "lemond":
|
|
215
|
+
probeLemond(ctx, h);
|
|
216
|
+
if (ctx.lemondJson) {
|
|
217
|
+
h.ok(`lemond answers on ${ctx.lemondBase}`);
|
|
218
|
+
showModels(ctx.lemondNames, h);
|
|
219
|
+
} else h.info(`lemond: not answering on ${ctx.lemondBase}`);
|
|
220
|
+
await chooseLemond(ctx, h);
|
|
221
|
+
break;
|
|
222
|
+
case "ollama":
|
|
223
|
+
probeOllama(ctx, h);
|
|
224
|
+
if (ctx.ollamaJson) {
|
|
225
|
+
h.ok(`Ollama answers on ${ctx.ollamaBase}`);
|
|
226
|
+
showModels(ctx.ollamaNames, h);
|
|
227
|
+
} else h.info(`Ollama: not answering on ${ctx.ollamaBase}`);
|
|
228
|
+
await chooseOllama(ctx, h);
|
|
229
|
+
break;
|
|
230
|
+
case "custom":
|
|
231
|
+
await chooseCustom(ctx, h);
|
|
232
|
+
break;
|
|
233
|
+
case "meridian":
|
|
234
|
+
await chooseMeridian(ctx, h);
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
h.die(`unknown backend ${ctx.backend}`);
|
|
238
|
+
}
|
|
239
|
+
h.ok(`backend ${ctx.backend}, model ${ctx.model}`);
|
|
240
|
+
await applyRoute(ctx, h);
|
|
241
|
+
if (ctx.backend === "meridian") writeMeridianKey(ctx, h);
|
|
242
|
+
saveConf(ctx, h);
|
|
243
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When notebooklmAccess.enabled, merge a "notebooklm" stdio MCP into ~/.cursor/mcp.json.
|
|
3
|
+
* Never overwrites existing "hindsight" or "codegraph" entries. Dry-run safe.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { resolveAccess, loadCodingAgentConfig } from "../../../cli/notebooklm-mcp.mjs";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
10
|
+
|
|
11
|
+
/** @param {string} repoRoot */
|
|
12
|
+
export function notebooklmMcpScriptPath(repoRoot) {
|
|
13
|
+
return path.resolve(repoRoot, "cli/notebooklm-mcp.mjs");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {Record<string, any>} mcp
|
|
18
|
+
* @param {string} scriptPath absolute path to cli/notebooklm-mcp.mjs
|
|
19
|
+
* @param {{ agentJson?: string }} [opts]
|
|
20
|
+
* @returns {{ mcp: Record<string, any>, changed: boolean, skipped?: string }}
|
|
21
|
+
*/
|
|
22
|
+
export function mergeNotebooklmMcpServer(mcp, scriptPath, opts = {}) {
|
|
23
|
+
const out = { ...mcp };
|
|
24
|
+
const servers = { ...(mcp.mcpServers && typeof mcp.mcpServers === "object" ? mcp.mcpServers : {}) };
|
|
25
|
+
// Absolute node + explicit config path: Cursor Agent's PATH may resolve a different
|
|
26
|
+
// `node`, and a symlink ~/.hindsight/coding-agent.json → shipped template used to
|
|
27
|
+
// make the process exit before handshake ("Connection closed").
|
|
28
|
+
const nodeBin = process.execPath;
|
|
29
|
+
const agentJson =
|
|
30
|
+
opts.agentJson ||
|
|
31
|
+
process.env.HINDSIGHT_CODING_AGENT_JSON ||
|
|
32
|
+
path.join(process.env.HOME || "", ".hindsight", "coding-agent.json");
|
|
33
|
+
const next = {
|
|
34
|
+
type: "stdio",
|
|
35
|
+
command: nodeBin,
|
|
36
|
+
args: [scriptPath],
|
|
37
|
+
env: {
|
|
38
|
+
HINDSIGHT_CODING_AGENT_JSON: agentJson,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const prev = servers.notebooklm;
|
|
42
|
+
const same =
|
|
43
|
+
prev &&
|
|
44
|
+
prev.command === next.command &&
|
|
45
|
+
Array.isArray(prev.args) &&
|
|
46
|
+
prev.args.length === 1 &&
|
|
47
|
+
prev.args[0] === scriptPath &&
|
|
48
|
+
prev.type === "stdio" &&
|
|
49
|
+
prev.env?.HINDSIGHT_CODING_AGENT_JSON === next.env.HINDSIGHT_CODING_AGENT_JSON;
|
|
50
|
+
if (same) return { mcp: { ...out, mcpServers: servers }, changed: false, skipped: "already configured" };
|
|
51
|
+
servers.notebooklm = next;
|
|
52
|
+
// Preserve sibling servers by reference identity of keys we did not touch.
|
|
53
|
+
return { mcp: { ...out, mcpServers: servers }, changed: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Record<string, any>} ctx
|
|
58
|
+
* @param {Helpers} h
|
|
59
|
+
*/
|
|
60
|
+
export async function installNotebooklmMcp(ctx, h) {
|
|
61
|
+
const agentPath = ctx.agentJson || path.join(ctx.home, ".hindsight", "coding-agent.json");
|
|
62
|
+
const tracked = path.join(ctx.repoRoot, "config/coding-agent.json");
|
|
63
|
+
let cfg = null;
|
|
64
|
+
for (const p of [agentPath, tracked]) {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(p)) {
|
|
67
|
+
cfg = loadCodingAgentConfig(p);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* try next */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!cfg) {
|
|
75
|
+
h.info("notebooklm MCP: no coding-agent.json yet; skip Cursor mcp.json merge");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const access = resolveAccess(cfg);
|
|
79
|
+
if (!access.enabled) {
|
|
80
|
+
h.info("notebooklmAccess.enabled is false; skip registering notebooklm MCP in ~/.cursor/mcp.json");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const script = notebooklmMcpScriptPath(ctx.repoRoot);
|
|
85
|
+
if (!fs.existsSync(script)) {
|
|
86
|
+
h.warn(`notebooklm MCP script missing: ${script}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const mcpPath = path.join(ctx.home, ".cursor", "mcp.json");
|
|
91
|
+
let existing = {};
|
|
92
|
+
if (fs.existsSync(mcpPath)) {
|
|
93
|
+
try {
|
|
94
|
+
existing = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
h.warn(`cannot parse ${mcpPath}: ${err instanceof Error ? err.message : err}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Guard: never clobber unrelated servers (merge only adds/updates "notebooklm").
|
|
102
|
+
const beforeHindsight = existing.mcpServers?.hindsight;
|
|
103
|
+
const beforeCodegraph = existing.mcpServers?.codegraph;
|
|
104
|
+
const { mcp, changed, skipped } = mergeNotebooklmMcpServer(existing, script, {
|
|
105
|
+
agentJson: path.resolve(agentPath),
|
|
106
|
+
});
|
|
107
|
+
if (beforeHindsight && mcp.mcpServers?.hindsight !== beforeHindsight) {
|
|
108
|
+
h.warn("refusing to alter hindsight MCP entry");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (beforeCodegraph && mcp.mcpServers?.codegraph !== beforeCodegraph) {
|
|
112
|
+
h.warn("refusing to alter codegraph MCP entry");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!changed) {
|
|
117
|
+
h.ok(`notebooklm MCP already in ${mcpPath}${skipped ? ` (${skipped})` : ""}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (ctx.dry) {
|
|
121
|
+
h.info(`[dry-run] merge notebooklm MCP into ${mcpPath} → node ${script}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
|
|
125
|
+
fs.writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`);
|
|
126
|
+
h.ok(`registered notebooklm MCP in ${mcpPath}`);
|
|
127
|
+
}
|