@bcmyguest/openmem 0.0.0-stage → 0.6.3
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/LICENSE +190 -0
- package/README.md +136 -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 +23 -0
- package/cli/coding-agent/hook-harnesses.mjs +87 -0
- package/cli/coding-agent/hook-runner.mjs +260 -0
- package/cli/coding-agent/install-access.mjs +93 -0
- package/cli/coding-agent/install-shim.mjs +97 -0
- package/cli/coding-agent/opencode-config.mjs +91 -0
- package/cli/coding-agent/opencode-plugin.mjs +96 -0
- package/cli/coding-agent/runtime.mjs +39 -0
- package/cli/coding-agent/session-facade.mjs +62 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/feed-sync.mjs +11 -0
- package/cli/install-coding-agent.mjs +165 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -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/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +455 -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 +35 -0
- package/cli/openmem.mjs +205 -0
- package/cli/roots.mjs +85 -0
- package/cli/stack.mjs +436 -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 +164 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +24 -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 +62 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +219 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +51 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +51 -0
- package/docs/cognee.md +37 -0
- package/docs/concepts.md +112 -0
- package/docs/hindsight.md +74 -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 +101 -0
- package/docs/mem0.md +103 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/model-configuration.md +84 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +170 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +101 -0
- package/docs/setup-wizard.md +116 -0
- package/docs/sources-feeds.md +52 -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 +95 -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 +245 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +73 -0
- package/providers/hindsight/lib/env-setup.mjs +71 -0
- package/providers/hindsight/lib/litellm-routes.mjs +187 -0
- package/providers/hindsight/lib/model-backend.mjs +214 -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 +33 -0
- package/providers/hindsight/lib/stack-env.mjs +132 -0
- package/providers/hindsight/lib/state.mjs +75 -0
- package/providers/hindsight/lib/summary.mjs +43 -0
- package/providers/hindsight/lib/validate.mjs +75 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +149 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +124 -0
- package/providers/mem0/compose.yaml +57 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +135 -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/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 +216 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +60 -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 +133 -0
- package/providers/zep/manifest.json +29 -0
- package/scripts/check-pack.mjs +218 -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 +94 -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/skills/openmem/SKILL.md +29 -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/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/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 +163 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +57 -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 +755 -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,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "cognee",
|
|
3
|
+
"label": "Cognee",
|
|
4
|
+
"status": "supported",
|
|
5
|
+
"notes": "Self-hosted graph/RAG memory run as a pinned cognee/cognee image (single container, SQLite + LanceDB + Ladybug) on a loopback port in single-user mode (no authentication). Ingestion is asynchronous: the indexer adds documents, then requests cognify once per changed dataset, so a document is searchable only after the graph is built. Default cognify path is local GLiNER + FastEmbed (no API key); alternatives are LLM_PROVIDER=ollama or an OpenAI-compatible LLM_API_KEY. No coding-agent plugin — agents use the REST API on the loopback port (/docs).",
|
|
6
|
+
"settings": {
|
|
7
|
+
"nativeDocs": "https://docs.cognee.ai/",
|
|
8
|
+
"nativeConfigPaths": [
|
|
9
|
+
"~/.config/personal-memory/cognee.env",
|
|
10
|
+
"~/.config/personal-memory/cognee-indexer.env"
|
|
11
|
+
],
|
|
12
|
+
"wizardOwns": [
|
|
13
|
+
"compose profile 'cognee', the loopback host port (COGNEE_PORT, default 8766) and the stack .env entries",
|
|
14
|
+
"GRAPH_EXTRACTOR / EMBEDDING_* (default GLiNER + FastEmbed) and optional LLM_* settings in cognee.env (mode 600, outside the repo)",
|
|
15
|
+
"the smoke test (health, dataset, multipart add, list, delete; recall is skipped because ingestion is asynchronous)",
|
|
16
|
+
"index.yml sources (shared indexer)",
|
|
17
|
+
"Cognee dataset mapping from indexer banks (the indexer sink maps each bank to a dataset)"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"compose": {
|
|
21
|
+
"services": ["cognee", "indexer"],
|
|
22
|
+
"healthService": "cognee",
|
|
23
|
+
"logServices": ["cognee"],
|
|
24
|
+
"healthPath": "/health",
|
|
25
|
+
"hostPortEnv": "COGNEE_PORT",
|
|
26
|
+
"defaultHostPort": 8766,
|
|
27
|
+
"profile": "cognee",
|
|
28
|
+
"files": ["providers/cognee/compose.yaml"]
|
|
29
|
+
},
|
|
30
|
+
"indexer": {
|
|
31
|
+
"apiUrlEnv": "COGNEE_API_URL",
|
|
32
|
+
"sink": "cognee",
|
|
33
|
+
"sinkDriver": "cognee-rest"
|
|
34
|
+
},
|
|
35
|
+
"codingAgent": {
|
|
36
|
+
"pluginPackage": null,
|
|
37
|
+
"configTemplate": null,
|
|
38
|
+
"notes": "No plugin package. setupCodingAgent prints REST paths and registers the read-only Cognee MCP (cognee_health / cognee_search) in ~/.cursor/mcp.json."
|
|
39
|
+
},
|
|
40
|
+
"wizardHooks": {
|
|
41
|
+
"module": "wizard.mjs",
|
|
42
|
+
"delegates": {}
|
|
43
|
+
},
|
|
44
|
+
"wizard": {
|
|
45
|
+
"smokeBank": "wizard-smoke",
|
|
46
|
+
"retainMethod": "POST",
|
|
47
|
+
"retainPath": "/api/v1/add",
|
|
48
|
+
"retainBody": "multipart/form-data: datasetName=wizard_smoke, data=wizard-smoke.md (done by the smoke hook)",
|
|
49
|
+
"recallMethod": "POST",
|
|
50
|
+
"recallPath": "/api/v1/search",
|
|
51
|
+
"recallBody": "{\"searchType\":\"GRAPH_COMPLETION\",\"query\":\"What is the project codename?\",\"datasets\":[\"wizard_smoke\"]}",
|
|
52
|
+
"recallHint": "check {compose} logs cognee and cognify settings (GLiNER/FastEmbed, Ollama, or LLM_API_KEY)",
|
|
53
|
+
"deleteMethod": "DELETE",
|
|
54
|
+
"deletePath": "/api/v1/datasets/{bank}"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognee sub-wizard (hook API v2). Cognee runs as a pinned docker service on a loopback
|
|
3
|
+
* port in single-user mode; its secrets (LLM_API_KEY) live in an operator env file outside
|
|
4
|
+
* the repo (default ~/.config/personal-memory/cognee.env), never in the stack .env,
|
|
5
|
+
* wizard.conf, config.toml, the summary or an error message. See lib/ for the
|
|
6
|
+
* implementations and docs/provider-wizard-hooks.md for the hook contract.
|
|
7
|
+
*
|
|
8
|
+
* This module must not import wizard/lib/run-wizard.mjs; it receives `helpers`.
|
|
9
|
+
*/
|
|
10
|
+
import { applyConfig, configToml as buildConfigToml } from "./lib/config.mjs";
|
|
11
|
+
import { clackSetup as askInClack } from "./lib/clack.mjs";
|
|
12
|
+
import { setupCodingAgent as printRestAgentHelp } from "./lib/coding-agent.mjs";
|
|
13
|
+
import { beforeStack as prepareEnvDirs, stackEnv as cogneeStackEnv } from "./lib/env-setup.mjs";
|
|
14
|
+
import { captureSecrets, configureLlm } from "./lib/llm-env.mjs";
|
|
15
|
+
import { resolvePorts as resolveCogneePorts } from "./lib/ports.mjs";
|
|
16
|
+
import { runSmoke } from "./lib/smoke.mjs";
|
|
17
|
+
import { ensureState } from "./lib/state.mjs";
|
|
18
|
+
import { summary as cogneeSummary } from "./lib/summary.mjs";
|
|
19
|
+
import { validateCognee } from "./lib/validate.mjs";
|
|
20
|
+
|
|
21
|
+
// Called by scripts/setup.mjs (the stack .env writer), not by the wizard.
|
|
22
|
+
export { prepareStackEnv, setupNotices } from "./lib/stack-env.mjs";
|
|
23
|
+
|
|
24
|
+
/** @typedef {import('../../wizard/lib/provider-wizard/helpers.mjs').ProviderWizardHelpers} Helpers */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Quiet: fill Cognee's context fields, map the `[memory.cognee]` TOML table (port,
|
|
28
|
+
* env_file, indexer_env_file) into the environment when the environment did not set it, and
|
|
29
|
+
* take the secret answers (PM_COGNEE_LLM_API_KEY and friends) out of `process.env`. This
|
|
30
|
+
* runs before the wizard's first docker call, so no child process inherits a key.
|
|
31
|
+
* @param {Record<string, any>} ctx
|
|
32
|
+
* @param {Helpers} h
|
|
33
|
+
*/
|
|
34
|
+
export async function init(ctx, h) {
|
|
35
|
+
applyConfig(ctx, h);
|
|
36
|
+
captureSecrets(ctx);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Quiet: nothing to restore. The saved port is picked up in `resolvePorts`, which runs
|
|
41
|
+
* before this hook; nothing secret is saved anywhere.
|
|
42
|
+
* @param {Record<string, any>} ctx
|
|
43
|
+
*/
|
|
44
|
+
export async function hydrate(ctx) {
|
|
45
|
+
ensureState(ctx);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The single host port (COGNEE_PORT, default 8766). */
|
|
49
|
+
export const resolvePorts = resolveCogneePorts;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Step "LLM / embeddings": write cognify settings (default GLiNER + FastEmbed, or Ollama /
|
|
53
|
+
* OpenAI answers given as PM_COGNEE_<NAME>) into the operator env file. Never fails for a
|
|
54
|
+
* missing cloud key: the default local path is cognify-ready without one.
|
|
55
|
+
* @param {Record<string, any>} ctx
|
|
56
|
+
* @param {Helpers} h
|
|
57
|
+
*/
|
|
58
|
+
export async function configureModelRouting(ctx, h) {
|
|
59
|
+
await configureLlm(ctx, h);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Quiet: description and environment for the stack setup script (scripts/setup.mjs).
|
|
64
|
+
* @param {Record<string, any>} ctx
|
|
65
|
+
*/
|
|
66
|
+
export function stackEnv(ctx) {
|
|
67
|
+
return cogneeStackEnv(ctx);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create the env directories (mode 700) before the stack setup masks them; warn about
|
|
72
|
+
* loose modes.
|
|
73
|
+
* @param {Record<string, any>} ctx
|
|
74
|
+
* @param {Helpers} h
|
|
75
|
+
*/
|
|
76
|
+
export async function beforeStack(ctx, h) {
|
|
77
|
+
await prepareEnvDirs(ctx, h);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Replaces the manifest smoke test: health, create dataset, multipart add, list, delete.
|
|
82
|
+
* Recall is not checked (ingestion is asynchronous).
|
|
83
|
+
* @param {Record<string, any>} ctx
|
|
84
|
+
* @param {Helpers} h
|
|
85
|
+
*/
|
|
86
|
+
export async function smoke(ctx, h) {
|
|
87
|
+
await runSmoke(ctx, h);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Appends to ctx.validationErrors / ctx.validationChecks after the shared checks.
|
|
92
|
+
* @param {Record<string, any>} ctx
|
|
93
|
+
* @param {Helpers} h
|
|
94
|
+
*/
|
|
95
|
+
export async function extendValidation(ctx, h) {
|
|
96
|
+
validateCognee(ctx, h);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Quiet: tables merged into config.toml (`[memory.cognee]`: port, env_file; no secrets).
|
|
101
|
+
* @param {Record<string, any>} ctx
|
|
102
|
+
*/
|
|
103
|
+
export function configToml(ctx) {
|
|
104
|
+
return buildConfigToml(ctx);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Quiet: lines for the closing summary.
|
|
109
|
+
* @param {Record<string, any>} ctx
|
|
110
|
+
*/
|
|
111
|
+
export function summary(ctx) {
|
|
112
|
+
return cogneeSummary(ctx);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clack-only: choose OpenAI key vs Ollama; for OpenAI, ask with a hidden prompt. Answers
|
|
117
|
+
* reach the wizard as PM_COGNEE_* in its own process environment.
|
|
118
|
+
* @param {Record<string, any>} ctx
|
|
119
|
+
* @param {typeof import("@clack/prompts")} ui
|
|
120
|
+
* @param {Helpers} h
|
|
121
|
+
*/
|
|
122
|
+
export async function clackSetup(ctx, ui, h) {
|
|
123
|
+
return askInClack(ctx, ui, h);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* After the generic coding-agent step (skipped: no pluginPackage): print how agents talk
|
|
128
|
+
* to Cognee over REST on the loopback port.
|
|
129
|
+
* @param {Record<string, any>} ctx
|
|
130
|
+
* @param {Helpers} h
|
|
131
|
+
*/
|
|
132
|
+
export async function setupCodingAgent(ctx, h) {
|
|
133
|
+
await printRestAgentHelp(ctx, h);
|
|
134
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "graphiti",
|
|
3
|
+
"label": "Graphiti",
|
|
4
|
+
"status": "planned",
|
|
5
|
+
"notes": "Placeholder for Zep Graphiti temporal knowledge graph backend.",
|
|
6
|
+
"settings": {
|
|
7
|
+
"nativeDocs": "https://help.getzep.com/graphiti",
|
|
8
|
+
"nativeConfigPaths": [],
|
|
9
|
+
"wizardOwns": []
|
|
10
|
+
},
|
|
11
|
+
"compose": { "profile": "graphiti", "files": [], "services": [] },
|
|
12
|
+
"indexer": { "apiUrlEnv": "GRAPHITI_API_URL", "sink": "graphiti" },
|
|
13
|
+
"codingAgent": { "pluginPackage": null, "configTemplate": null },
|
|
14
|
+
"wizard": {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hindsight's questions for the Clack setup UI: model backend, models, reflect model.
|
|
3
|
+
* The meta UI asks the provider-neutral questions (action, provider) and then calls
|
|
4
|
+
* clackSetup; the returned environment is handed to the wizard so it skips its own
|
|
5
|
+
* model prompts (WIZARD_SKIP_BACKEND_STEP).
|
|
6
|
+
*/
|
|
7
|
+
import { applyConfigEnv } from "./config.mjs";
|
|
8
|
+
import { fetchJson, normalizeOpenAiBase, ollamaModelNames, openAiModelIds } from "./probe.mjs";
|
|
9
|
+
import {
|
|
10
|
+
LEMOND_DEFAULT_MODEL,
|
|
11
|
+
OLLAMA_COMPOSE_API_BASE,
|
|
12
|
+
OLLAMA_DEFAULT_MODEL,
|
|
13
|
+
OLLAMA_HOST_API_BASE,
|
|
14
|
+
} from "./state.mjs";
|
|
15
|
+
|
|
16
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ repoRoot: string, home: string, wizardConf: string, pmMerged?: Record<string, unknown> }} ctx
|
|
20
|
+
* @param {typeof import("@clack/prompts")} ui the @clack/prompts module
|
|
21
|
+
* @param {Helpers} h
|
|
22
|
+
* @returns {Promise<Record<string, string>>} environment for the wizard process
|
|
23
|
+
*/
|
|
24
|
+
export async function clackSetup(ctx, ui, h) {
|
|
25
|
+
const wizardConf = ctx.wizardConf;
|
|
26
|
+
if (ctx.pmMerged) applyConfigEnv(ctx.pmMerged);
|
|
27
|
+
/** @type {Record<string, string>} */
|
|
28
|
+
const handoff = {};
|
|
29
|
+
|
|
30
|
+
const lemondBase = process.env.LEMOND_BASE || "http://127.0.0.1:13305";
|
|
31
|
+
handoff.LEMOND_BASE = lemondBase;
|
|
32
|
+
|
|
33
|
+
const backendDefault =
|
|
34
|
+
process.env.PM_BACKEND ||
|
|
35
|
+
h.envValue(wizardConf, "MEMORY_BACKEND") ||
|
|
36
|
+
"lemond";
|
|
37
|
+
|
|
38
|
+
const backend = await ui.select({
|
|
39
|
+
message: "Model backend",
|
|
40
|
+
options: [
|
|
41
|
+
{
|
|
42
|
+
value: "lemond",
|
|
43
|
+
label: "Lemonade (lemond)",
|
|
44
|
+
hint: "local GGUFs on this host",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: "ollama",
|
|
48
|
+
label: "Ollama",
|
|
49
|
+
hint: "local models on this host",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: "custom",
|
|
53
|
+
label: "OpenAI-compatible URL",
|
|
54
|
+
hint: "Meridian, hosted API, other",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
initialValue: ["lemond", "ollama", "custom"].includes(backendDefault)
|
|
58
|
+
? backendDefault
|
|
59
|
+
: "lemond",
|
|
60
|
+
});
|
|
61
|
+
if (ui.isCancel(backend)) {
|
|
62
|
+
ui.cancel("Setup cancelled.");
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let model = "";
|
|
67
|
+
let apiBase = "";
|
|
68
|
+
let reflectModel =
|
|
69
|
+
process.env.PM_REFLECT_MODEL ||
|
|
70
|
+
machine.PM_REFLECT_MODEL ||
|
|
71
|
+
h.envValue(wizardConf, "MEMORY_REFLECT_MODEL") ||
|
|
72
|
+
"";
|
|
73
|
+
|
|
74
|
+
if (backend === "lemond") {
|
|
75
|
+
const spin = ui.spinner();
|
|
76
|
+
spin.start(`Checking lemond at ${lemondBase}`);
|
|
77
|
+
const body = await fetchJson(`${lemondBase}/v1/models`);
|
|
78
|
+
const ids = openAiModelIds(body);
|
|
79
|
+
spin.stop(
|
|
80
|
+
body ? `lemond answered (${ids.length} models)` : "lemond not reachable",
|
|
81
|
+
);
|
|
82
|
+
if (!body) {
|
|
83
|
+
ui.log.warn(
|
|
84
|
+
"Start lemonade (lemond) on this host, or pick custom / Ollama instead.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const defaultModel =
|
|
88
|
+
machine.PM_DEFAULT_MODEL ||
|
|
89
|
+
machine.MEMORY_MODEL ||
|
|
90
|
+
h.envValue(wizardConf, "MEMORY_MODEL") ||
|
|
91
|
+
LEMOND_DEFAULT_MODEL;
|
|
92
|
+
if (ids.length > 0) {
|
|
93
|
+
const picked = await ui.select({
|
|
94
|
+
message: "Retain / consolidate model",
|
|
95
|
+
options: ids.map((id) => ({ value: id, label: id })),
|
|
96
|
+
initialValue: ids.includes(defaultModel) ? defaultModel : ids[0],
|
|
97
|
+
});
|
|
98
|
+
if (ui.isCancel(picked)) process.exit(0);
|
|
99
|
+
model = /** @type {string} */ (picked);
|
|
100
|
+
} else {
|
|
101
|
+
const typed = await ui.text({
|
|
102
|
+
message: "Retain / consolidate model id",
|
|
103
|
+
placeholder: defaultModel,
|
|
104
|
+
defaultValue: defaultModel,
|
|
105
|
+
});
|
|
106
|
+
if (ui.isCancel(typed)) process.exit(0);
|
|
107
|
+
model = /** @type {string} */ (typed);
|
|
108
|
+
}
|
|
109
|
+
apiBase = "http://host.docker.internal:13305/v1";
|
|
110
|
+
} else if (backend === "ollama") {
|
|
111
|
+
const ollamaBase = machine.OLLAMA_BASE || "http://127.0.0.1:11434";
|
|
112
|
+
const spin = ui.spinner();
|
|
113
|
+
spin.start(`Checking Ollama at ${ollamaBase}`);
|
|
114
|
+
const body = await fetchJson(`${ollamaBase}/api/tags`);
|
|
115
|
+
const names = ollamaModelNames(body);
|
|
116
|
+
spin.stop(
|
|
117
|
+
body ? `Ollama answered (${names.length} models)` : "Ollama not reachable",
|
|
118
|
+
);
|
|
119
|
+
let ollamaInCompose = false;
|
|
120
|
+
if (!body) {
|
|
121
|
+
const useDocker = await ui.confirm({
|
|
122
|
+
message: "Ollama is not on this host. Run it in Docker (compose profile ollama)?",
|
|
123
|
+
active: "yes",
|
|
124
|
+
inactive: "no",
|
|
125
|
+
initialValue: true,
|
|
126
|
+
});
|
|
127
|
+
if (ui.isCancel(useDocker)) process.exit(0);
|
|
128
|
+
ollamaInCompose = Boolean(useDocker);
|
|
129
|
+
if (ollamaInCompose) {
|
|
130
|
+
ui.log.info(
|
|
131
|
+
"LiteLLM will call http://ollama:11434; the wizard pulls the model after the stack starts.",
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
ui.log.warn(
|
|
135
|
+
"Start Ollama on this host (OLLAMA_HOST on the Docker bridge), or rerun and pick Docker.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const defaultModel =
|
|
140
|
+
machine.PM_DEFAULT_MODEL ||
|
|
141
|
+
machine.MEMORY_MODEL ||
|
|
142
|
+
h.envValue(wizardConf, "MEMORY_MODEL") ||
|
|
143
|
+
OLLAMA_DEFAULT_MODEL;
|
|
144
|
+
if (names.length > 0) {
|
|
145
|
+
const picked = await ui.select({
|
|
146
|
+
message: "Retain / consolidate model",
|
|
147
|
+
options: names.map((n) => ({ value: n, label: n })),
|
|
148
|
+
initialValue: names.includes(defaultModel) ? defaultModel : names[0],
|
|
149
|
+
});
|
|
150
|
+
if (ui.isCancel(picked)) process.exit(0);
|
|
151
|
+
model = /** @type {string} */ (picked);
|
|
152
|
+
} else {
|
|
153
|
+
const typed = await ui.text({
|
|
154
|
+
message: "Retain / consolidate model",
|
|
155
|
+
placeholder: defaultModel,
|
|
156
|
+
defaultValue: defaultModel,
|
|
157
|
+
});
|
|
158
|
+
if (ui.isCancel(typed)) process.exit(0);
|
|
159
|
+
model = /** @type {string} */ (typed);
|
|
160
|
+
}
|
|
161
|
+
apiBase = ollamaInCompose ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
|
|
162
|
+
if (ollamaInCompose) handoff.MEMORY_OLLAMA_IN_COMPOSE = "1";
|
|
163
|
+
} else {
|
|
164
|
+
const defaultBase =
|
|
165
|
+
machine.CUSTOM_DEFAULT_BASE || "http://host.docker.internal:3456/v1";
|
|
166
|
+
const defaultModel =
|
|
167
|
+
machine.CUSTOM_DEFAULT_MODEL ||
|
|
168
|
+
machine.PM_DEFAULT_MODEL ||
|
|
169
|
+
h.envValue(wizardConf, "MEMORY_MODEL") ||
|
|
170
|
+
LEMOND_DEFAULT_MODEL;
|
|
171
|
+
const baseInput = await ui.text({
|
|
172
|
+
message: "OpenAI-compatible base URL (host.docker.internal from containers)",
|
|
173
|
+
placeholder: defaultBase,
|
|
174
|
+
defaultValue: defaultBase,
|
|
175
|
+
});
|
|
176
|
+
if (ui.isCancel(baseInput)) process.exit(0);
|
|
177
|
+
apiBase = normalizeOpenAiBase(/** @type {string} */ (baseInput));
|
|
178
|
+
|
|
179
|
+
const spin = ui.spinner();
|
|
180
|
+
spin.start("Listing models");
|
|
181
|
+
const body = await fetchJson(`${apiBase}/models`);
|
|
182
|
+
const ids = openAiModelIds(body);
|
|
183
|
+
spin.stop(
|
|
184
|
+
ids.length
|
|
185
|
+
? `Found ${ids.length} models`
|
|
186
|
+
: "Could not list models (enter id manually)",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (ids.length > 0) {
|
|
190
|
+
const picked = await ui.select({
|
|
191
|
+
message: "Retain / consolidate model",
|
|
192
|
+
options: ids.map((id) => ({ value: id, label: id })),
|
|
193
|
+
initialValue: ids.includes(defaultModel) ? defaultModel : ids[0],
|
|
194
|
+
});
|
|
195
|
+
if (ui.isCancel(picked)) process.exit(0);
|
|
196
|
+
model = /** @type {string} */ (picked);
|
|
197
|
+
} else {
|
|
198
|
+
const typed = await ui.text({
|
|
199
|
+
message: "Retain / consolidate model id",
|
|
200
|
+
placeholder: defaultModel,
|
|
201
|
+
defaultValue: defaultModel,
|
|
202
|
+
});
|
|
203
|
+
if (ui.isCancel(typed)) process.exit(0);
|
|
204
|
+
model = /** @type {string} */ (typed);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const reflectChoice = await ui.select({
|
|
209
|
+
message: "Reflect model (needs tool calls; used for memory-reflect)",
|
|
210
|
+
options: [
|
|
211
|
+
{
|
|
212
|
+
value: "",
|
|
213
|
+
label: "Shipped default for this backend",
|
|
214
|
+
hint: "E4B route on lemond, etc.",
|
|
215
|
+
},
|
|
216
|
+
{ value: "same", label: "Same as retain model" },
|
|
217
|
+
{ value: "custom", label: "Pick another model id" },
|
|
218
|
+
],
|
|
219
|
+
initialValue: reflectModel ? "custom" : "",
|
|
220
|
+
});
|
|
221
|
+
if (ui.isCancel(reflectChoice)) process.exit(0);
|
|
222
|
+
|
|
223
|
+
if (reflectChoice === "same") {
|
|
224
|
+
reflectModel = model;
|
|
225
|
+
} else if (reflectChoice === "custom") {
|
|
226
|
+
const typed = await ui.text({
|
|
227
|
+
message: "Reflect model id",
|
|
228
|
+
placeholder: model,
|
|
229
|
+
defaultValue: reflectModel || model,
|
|
230
|
+
});
|
|
231
|
+
if (ui.isCancel(typed)) process.exit(0);
|
|
232
|
+
reflectModel = /** @type {string} */ (typed);
|
|
233
|
+
} else {
|
|
234
|
+
reflectModel = "";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ui.note("Next: Docker, secrets, indexing, and starting the stack.", "Backend configured");
|
|
238
|
+
|
|
239
|
+
handoff.WIZARD_SKIP_BACKEND_STEP = "1";
|
|
240
|
+
handoff.PM_BACKEND = /** @type {string} */ (backend);
|
|
241
|
+
handoff.PM_DEFAULT_MODEL = model;
|
|
242
|
+
handoff.MEMORY_API_BASE = apiBase;
|
|
243
|
+
handoff.PM_REFLECT_MODEL = reflectModel;
|
|
244
|
+
return handoff;
|
|
245
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step "8. Coding-agent plugin", Hindsight half: ensure ~/.hindsight/coding-agent.json
|
|
3
|
+
* exists (seeded from the tracked template when missing) and check that its optInPaths
|
|
4
|
+
* cover the indexed roots. Machine-local keys such as notebooklmAccess must live in a
|
|
5
|
+
* real file — never in the repo template — so we do not replace a materialised config
|
|
6
|
+
* with a symlink (that used to wipe notebooklmAccess.enabled after step 6).
|
|
7
|
+
* The meta wizard installs the plugin package itself from the manifest.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
12
|
+
import { installNotebooklmMcp } from "./notebooklm-mcp-install.mjs";
|
|
13
|
+
import { setNotebooklmAccessEnabled } from "../../../wizard/lib/notebooklm-agent-access.mjs";
|
|
14
|
+
import { ensureState } from "./state.mjs";
|
|
15
|
+
|
|
16
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
17
|
+
|
|
18
|
+
function realpathSafe(p) {
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(p);
|
|
21
|
+
} catch {
|
|
22
|
+
return path.resolve(p);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {string} filePath */
|
|
27
|
+
function readJsonSafe(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when notebooklm.yml opts the sync service in. */
|
|
36
|
+
function notebooklmSyncEnabled(ctx) {
|
|
37
|
+
const yml = ctx.notebookFile || path.join(ctx.home, ".config/hindsight/notebooklm.yml");
|
|
38
|
+
if (!fs.existsSync(yml)) return false;
|
|
39
|
+
return /^enabled:\s*true\b/m.test(fs.readFileSync(yml, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Seed or keep a real coding-agent.json. Never leave (or create) a symlink to the shipped
|
|
44
|
+
* template — machine keys such as notebooklmAccess must not write into the repo.
|
|
45
|
+
*
|
|
46
|
+
* @param {Record<string, any>} ctx
|
|
47
|
+
* @param {Helpers} h
|
|
48
|
+
*/
|
|
49
|
+
async function linkAgentConfig(ctx, h) {
|
|
50
|
+
const rel = ctx.memoryProvider.codingAgent?.configTemplate;
|
|
51
|
+
if (!rel) return;
|
|
52
|
+
const target = path.join(ctx.repoRoot, rel);
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.lstatSync(ctx.agentJson);
|
|
56
|
+
} catch {
|
|
57
|
+
stat = null;
|
|
58
|
+
}
|
|
59
|
+
if (stat?.isSymbolicLink()) {
|
|
60
|
+
// Copy current contents into a regular file so later writes never hit the template.
|
|
61
|
+
if (ctx.dry) {
|
|
62
|
+
h.info(`[dry-run] materialize ${ctx.agentJson} (symlink → regular file)`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const raw = fs.readFileSync(ctx.agentJson, "utf8");
|
|
66
|
+
fs.unlinkSync(ctx.agentJson);
|
|
67
|
+
fs.mkdirSync(path.dirname(ctx.agentJson), { recursive: true });
|
|
68
|
+
fs.writeFileSync(ctx.agentJson, raw.endsWith("\n") ? raw : `${raw}\n`);
|
|
69
|
+
h.ok(`materialized ${ctx.agentJson} (was symlink → regular file)`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (stat?.isFile()) {
|
|
73
|
+
// Keep the real file. Replacing it with a symlink to the shipped template drops
|
|
74
|
+
// notebooklmAccess.enabled and breaks the NotebookLM MCP on the next Cursor start.
|
|
75
|
+
h.ok(`${ctx.agentJson} is a machine-local file (not replaced with a symlink)`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Missing: copy the template into a real file so later notebooklmAccess writes never
|
|
79
|
+
// land in the repo.
|
|
80
|
+
if (ctx.dry) {
|
|
81
|
+
h.info(`[dry-run] create ${ctx.agentJson} from ${rel} (regular file, not a symlink)`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
fs.mkdirSync(path.dirname(ctx.agentJson), { recursive: true });
|
|
85
|
+
const seeded = readJsonSafe(target) || {
|
|
86
|
+
serverMode: "self-hosted",
|
|
87
|
+
apiUrl: "http://127.0.0.1:8888",
|
|
88
|
+
optInOnly: true,
|
|
89
|
+
optInPaths: [],
|
|
90
|
+
mapPathToBank: {},
|
|
91
|
+
notebooklmAccess: { enabled: false, banks: ["*"] },
|
|
92
|
+
};
|
|
93
|
+
fs.writeFileSync(ctx.agentJson, `${JSON.stringify(seeded, null, 2)}\n`);
|
|
94
|
+
h.ok(`created ${ctx.agentJson} from ${rel}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* If NotebookLM sync is on, keep notebooklmAccess.enabled and the Cursor MCP entry.
|
|
99
|
+
* Materialises a symlink into a real file first so the flag never writes into the template.
|
|
100
|
+
*
|
|
101
|
+
* @param {Record<string, any>} ctx
|
|
102
|
+
* @param {Helpers} h
|
|
103
|
+
*/
|
|
104
|
+
async function ensureNotebooklmAccess(ctx, h) {
|
|
105
|
+
if (!notebooklmSyncEnabled(ctx)) return;
|
|
106
|
+
const templatePath = path.join(ctx.repoRoot, "config/coding-agent.json");
|
|
107
|
+
if (ctx.dry) {
|
|
108
|
+
h.info(`[dry-run] keep notebooklmAccess.enabled=true (NotebookLM sync is on)`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const result = setNotebooklmAccessEnabled(ctx.agentJson, templatePath, true);
|
|
112
|
+
h.ok(
|
|
113
|
+
`notebooklmAccess.enabled=true in ${ctx.agentJson}` +
|
|
114
|
+
(result.action === "materialized" ? " (materialized symlink → regular file)" : ""),
|
|
115
|
+
);
|
|
116
|
+
await installNotebooklmMcp(
|
|
117
|
+
{ home: ctx.home, repoRoot: ctx.repoRoot, agentJson: ctx.agentJson, dry: false },
|
|
118
|
+
h,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {Record<string, any>} ctx
|
|
124
|
+
* @param {Helpers} h
|
|
125
|
+
* @returns {string[]}
|
|
126
|
+
*/
|
|
127
|
+
function indexRoots(ctx, h) {
|
|
128
|
+
const source = fs.existsSync(ctx.indexFile)
|
|
129
|
+
? ctx.indexFile
|
|
130
|
+
: path.join(ctx.repoRoot, "config/index.yml");
|
|
131
|
+
const roots = [];
|
|
132
|
+
for (const line of fs.readFileSync(source, "utf8").split("\n")) {
|
|
133
|
+
const m = line.match(/^\s*- path:\s*"?([^"]+)"?/);
|
|
134
|
+
if (m) roots.push(h.expandHome(m[1].trim(), ctx.home));
|
|
135
|
+
}
|
|
136
|
+
return roots;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {Record<string, any>} ctx
|
|
141
|
+
* @param {Helpers} h
|
|
142
|
+
*/
|
|
143
|
+
async function checkOptIn(ctx, h) {
|
|
144
|
+
// Prefer the live agent config (real file or symlink target). Never write
|
|
145
|
+
// machine paths into the shipped template unless that is what agentJson resolves to.
|
|
146
|
+
let target = ctx.agentJson;
|
|
147
|
+
try {
|
|
148
|
+
if (fs.lstatSync(ctx.agentJson).isSymbolicLink()) {
|
|
149
|
+
target = realpathSafe(fs.readlinkSync(ctx.agentJson));
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
target = path.join(ctx.repoRoot, "config/coding-agent.json");
|
|
153
|
+
}
|
|
154
|
+
if (!fs.existsSync(target)) {
|
|
155
|
+
target = path.join(ctx.repoRoot, "config/coding-agent.json");
|
|
156
|
+
}
|
|
157
|
+
const cfg = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
158
|
+
if (!Array.isArray(cfg.optInPaths)) cfg.optInPaths = [];
|
|
159
|
+
const opt = cfg.optInPaths.map((p) => path.resolve(h.expandHome(p, ctx.home)));
|
|
160
|
+
const missing = [];
|
|
161
|
+
for (const root of indexRoots(ctx, h)) {
|
|
162
|
+
const abs = path.resolve(root);
|
|
163
|
+
const covered = opt.some((p) => abs === p || abs.startsWith(`${p.replace(/\/$/, "")}/`));
|
|
164
|
+
if (!covered) missing.push(root);
|
|
165
|
+
}
|
|
166
|
+
if (!missing.length) {
|
|
167
|
+
h.ok(`optInPaths in ${target} covers the indexed roots`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
h.warn("optInPaths does not cover:");
|
|
171
|
+
for (const m of missing) h.info(` ${m}`);
|
|
172
|
+
if (ctx.dry) {
|
|
173
|
+
h.info(`[dry-run] offer to add these to optInPaths in ${target}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const tracked = path.join(ctx.repoRoot, "config/coding-agent.json");
|
|
177
|
+
const prompt =
|
|
178
|
+
realpathSafe(target) === realpathSafe(tracked)
|
|
179
|
+
? "Add them to optInPaths in the tracked file config/coding-agent.json (shows in git status)?"
|
|
180
|
+
: `Add them to optInPaths in ${target}?`;
|
|
181
|
+
if (await h.confirm(ctx, prompt, "n")) {
|
|
182
|
+
for (const root of missing) {
|
|
183
|
+
if (!cfg.optInPaths.includes(root)) cfg.optInPaths.push(root);
|
|
184
|
+
}
|
|
185
|
+
fs.writeFileSync(target, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
186
|
+
h.ok(`updated ${target}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {Record<string, any>} ctx
|
|
192
|
+
* @param {Helpers} h
|
|
193
|
+
*/
|
|
194
|
+
export async function setupCodingAgent(ctx, h) {
|
|
195
|
+
ensureState(ctx);
|
|
196
|
+
await linkAgentConfig(ctx, h);
|
|
197
|
+
await checkOptIn(ctx, h);
|
|
198
|
+
// Step 6 may have set notebooklmAccess on a real file; linkAgentConfig used to
|
|
199
|
+
// symlink over it. Re-assert after linking, then register the MCP.
|
|
200
|
+
await ensureNotebooklmAccess(ctx, h);
|
|
201
|
+
if (!notebooklmSyncEnabled(ctx)) {
|
|
202
|
+
await installNotebooklmMcp(ctx, h);
|
|
203
|
+
}
|
|
204
|
+
}
|