@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,371 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import TOML from "@iarna/toml";
|
|
4
|
+
import { writeFileAtomic } from "../../cli/coding-agent/file-io.mjs";
|
|
5
|
+
import { DEFAULT_PROVIDER_ID } from "./providers/registry.mjs";
|
|
6
|
+
|
|
7
|
+
/** Tables owned by install-coding-agent; the wizard carries them over when it rewrites the file. */
|
|
8
|
+
const INSTALLER_TABLES = ["agents", "context_proxy", "compaction", "context_store"];
|
|
9
|
+
const AGENT_INTEGRATIONS = ["openmem", "provider"];
|
|
10
|
+
const DEFAULT_CONTEXT_PROXY_PORT = 4100;
|
|
11
|
+
/** `[context_proxy.agents]` keys. opencode2 trims in its plugin; the others route through the proxy service. */
|
|
12
|
+
export const CONTEXT_AGENT_KEYS = ["claude-code", "opencode1", "codex", "opencode2"];
|
|
13
|
+
export const CONTEXT_PROXY_AGENT_KEYS = ["claude-code", "opencode1", "codex"];
|
|
14
|
+
// Same defaults as proxy/openmem_context/settings.py and cli/coding-agent/context-trim.mjs.
|
|
15
|
+
const DEFAULT_TRIM_AT_TOKENS = 120000;
|
|
16
|
+
const DEFAULT_COMPRESS_OVER_TOKENS = 4000;
|
|
17
|
+
const DEFAULT_COMPACTION_MAX_CHARS = 12000;
|
|
18
|
+
/** Config keys that reach the proxy container (stack .env) and the OpenCode 2 plugin as env. */
|
|
19
|
+
const CONTEXT_TRIM_ENV_KEYS = [
|
|
20
|
+
["context_proxy", "trim_at_tokens", "CONTEXT_TRIM_AT_TOKENS"],
|
|
21
|
+
["context_proxy", "compress_over_tokens", "CONTEXT_COMPRESS_OVER_TOKENS"],
|
|
22
|
+
["compaction", "max_chars", "CONTEXT_COMPACTION_MAX_CHARS"],
|
|
23
|
+
["context_store", "ttl_days", "CONTEXT_STORE_TTL_DAYS"],
|
|
24
|
+
["context_store", "embedding_max_chars", "CONTEXT_EMBEDDING_MAX_CHARS"],
|
|
25
|
+
];
|
|
26
|
+
/** String keys of `[context_store]` and their env names (session database vectors, docs/context-store.md). */
|
|
27
|
+
const CONTEXT_STORE_STRING_KEYS = [
|
|
28
|
+
["embedding_model", "CONTEXT_EMBEDDING_MODEL"],
|
|
29
|
+
["embedding_url", "CONTEXT_EMBEDDING_URL"],
|
|
30
|
+
];
|
|
31
|
+
const CONFIG_MODE = 0o600;
|
|
32
|
+
const CONFIG_HEADER = "# Written by openmem — mise-like config.\n# Docs: docs/openmem-config.md\n";
|
|
33
|
+
|
|
34
|
+
/** @param {string} home */
|
|
35
|
+
export function userConfigPath(home) {
|
|
36
|
+
return path.join(home, ".config/personal-memory/config.toml");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string} repoRoot */
|
|
40
|
+
export function projectConfigPaths(repoRoot) {
|
|
41
|
+
return [
|
|
42
|
+
path.join(repoRoot, "openmem.toml"),
|
|
43
|
+
path.join(repoRoot, "config/openmem.toml"),
|
|
44
|
+
// Pre-rebrand filenames still load if present.
|
|
45
|
+
path.join(repoRoot, "personal-memory.toml"),
|
|
46
|
+
path.join(repoRoot, "config/personal-memory.toml"),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} file
|
|
52
|
+
* @returns {Record<string, unknown>}
|
|
53
|
+
*/
|
|
54
|
+
function readTomlFile(file) {
|
|
55
|
+
if (!fs.existsSync(file)) return {};
|
|
56
|
+
try {
|
|
57
|
+
return TOML.parse(fs.readFileSync(file, "utf8"));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
throw new Error(`${file}: ${/** @type {Error} */ (e).message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Atomic rewrite; a new file is created mode 600, an existing one keeps its mode. */
|
|
64
|
+
function writeConfigFile(file, text) {
|
|
65
|
+
writeFileAtomic(file, text, fs.existsSync(file) ? {} : { mode: CONFIG_MODE });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {Record<string, unknown>} base @param {Record<string, unknown>} over */
|
|
69
|
+
function deepMerge(base, over) {
|
|
70
|
+
const out = { ...base };
|
|
71
|
+
for (const [k, v] of Object.entries(over)) {
|
|
72
|
+
if (v && typeof v === "object" && !Array.isArray(v) && typeof out[k] === "object") {
|
|
73
|
+
out[k] = deepMerge(/** @type {Record<string, unknown>} */ (out[k]), /** @type {Record<string, unknown>} */ (v));
|
|
74
|
+
} else out[k] = v;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} home
|
|
81
|
+
* @param {string} repoRoot
|
|
82
|
+
* @returns {{ merged: Record<string, unknown>, userPath: string, wroteFrom: string[] }}
|
|
83
|
+
*/
|
|
84
|
+
export function loadPmConfig(home, repoRoot) {
|
|
85
|
+
let merged = {};
|
|
86
|
+
const wroteFrom = [];
|
|
87
|
+
for (const p of projectConfigPaths(repoRoot)) {
|
|
88
|
+
if (fs.existsSync(p)) {
|
|
89
|
+
merged = deepMerge(merged, readTomlFile(p));
|
|
90
|
+
wroteFrom.push(p);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const userPath = userConfigPath(home);
|
|
94
|
+
if (fs.existsSync(userPath)) {
|
|
95
|
+
merged = deepMerge(merged, readTomlFile(userPath));
|
|
96
|
+
wroteFrom.push(userPath);
|
|
97
|
+
}
|
|
98
|
+
return { merged, userPath, wroteFrom };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Flat defaults for the wizard (--yes / Clack initialValue). Provider-specific tables
|
|
103
|
+
* ([model], provider ports) are read by the provider's own hooks from `cfg`.
|
|
104
|
+
* @param {Record<string, unknown>} cfg
|
|
105
|
+
*/
|
|
106
|
+
export function defaultsFromPmConfig(cfg) {
|
|
107
|
+
const memory = /** @type {Record<string, unknown>} */ (cfg.memory || {});
|
|
108
|
+
const stack = /** @type {Record<string, unknown>} */ (cfg.stack || {});
|
|
109
|
+
const wizard = /** @type {Record<string, unknown>} */ (cfg.wizard || {});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
memoryProvider: String(memory.provider || ""),
|
|
113
|
+
stackProject: String(stack.project || ""),
|
|
114
|
+
wizardAction: String(wizard.action || ""),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
120
|
+
* @param {Record<string, Record<string, unknown>>} [sections] provider-owned tables:
|
|
121
|
+
* `stack` keys are merged into [stack]; `memory` sub-tables (for example `memory.mem0`) are
|
|
122
|
+
* merged into [memory], and `provider` there is always the active provider id; every other
|
|
123
|
+
* key becomes its own top-level table
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
export function buildPmConfigToml(ctx, sections = {}) {
|
|
127
|
+
const { stack: stackExtra = {}, memory: memoryExtra = {}, ...rest } = sections;
|
|
128
|
+
const doc = {
|
|
129
|
+
memory: { ...memoryExtra, provider: ctx.memoryProvider?.id || DEFAULT_PROVIDER_ID },
|
|
130
|
+
stack: { project: ctx.project, ...stackExtra },
|
|
131
|
+
wizard: { action: ctx.action || "configure" },
|
|
132
|
+
...rest,
|
|
133
|
+
tasks: {
|
|
134
|
+
configure: { description: "Secrets, index sources, build images, compose up" },
|
|
135
|
+
up: { description: "Start stack using existing config" },
|
|
136
|
+
down: { description: "docker compose down for this project" },
|
|
137
|
+
doctor: { description: "Validate memory setup on this machine" },
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return `# Written by ./setup — mise-like config for openmem.\n# Docs: docs/openmem-config.md\n${TOML.stringify(doc)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
145
|
+
* @param {Record<string, Record<string, unknown>>} [sections] see buildPmConfigToml
|
|
146
|
+
*/
|
|
147
|
+
export function syncPmConfig(ctx, sections = {}) {
|
|
148
|
+
if (ctx.dry) return;
|
|
149
|
+
const dir = path.dirname(userConfigPath(ctx.home));
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
151
|
+
const file = userConfigPath(ctx.home);
|
|
152
|
+
let previous = {};
|
|
153
|
+
try {
|
|
154
|
+
previous = readTomlFile(file);
|
|
155
|
+
} catch {
|
|
156
|
+
// A malformed file is replaced, as before; nothing to carry over.
|
|
157
|
+
}
|
|
158
|
+
const kept = Object.fromEntries(
|
|
159
|
+
INSTALLER_TABLES.filter((t) => previous[t] && !(t in sections)).map((t) => [t, previous[t]]),
|
|
160
|
+
);
|
|
161
|
+
writeConfigFile(file, buildPmConfigToml(ctx, { ...sections, ...kept }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Minimal in-place update of `memory.provider` in the user config.toml when the file
|
|
166
|
+
* already exists. Does not create the file (unlike syncPmConfig).
|
|
167
|
+
*
|
|
168
|
+
* @param {string} home
|
|
169
|
+
* @param {string} providerId
|
|
170
|
+
* @param {{ dry?: boolean }} [opts]
|
|
171
|
+
* @returns {"updated"|"skipped"|"dry-run"}
|
|
172
|
+
*/
|
|
173
|
+
export function updatePmConfigProvider(home, providerId, opts = {}) {
|
|
174
|
+
const file = userConfigPath(home);
|
|
175
|
+
if (!fs.existsSync(file)) return "skipped";
|
|
176
|
+
if (opts.dry) return "dry-run";
|
|
177
|
+
const doc = readTomlFile(file);
|
|
178
|
+
const memory = {
|
|
179
|
+
...(doc.memory && typeof doc.memory === "object" && !Array.isArray(doc.memory)
|
|
180
|
+
? /** @type {Record<string, unknown>} */ (doc.memory)
|
|
181
|
+
: {}),
|
|
182
|
+
provider: providerId,
|
|
183
|
+
};
|
|
184
|
+
doc.memory = memory;
|
|
185
|
+
writeConfigFile(file, `${CONFIG_HEADER}${TOML.stringify(doc)}`);
|
|
186
|
+
return "updated";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Merge `values` into one top-level table of the user config.toml, creating the file
|
|
191
|
+
* (mode 600, directory 700) when it is missing. Other tables are kept.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} home
|
|
194
|
+
* @param {string} table
|
|
195
|
+
* @param {Record<string, unknown>} values
|
|
196
|
+
* @param {{ dry?: boolean }} [opts]
|
|
197
|
+
* @returns {"updated"|"dry-run"}
|
|
198
|
+
*/
|
|
199
|
+
export function updatePmConfigTable(home, table, values, opts = {}) {
|
|
200
|
+
if (opts.dry) return "dry-run";
|
|
201
|
+
const file = userConfigPath(home);
|
|
202
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
203
|
+
const doc = readTomlFile(file);
|
|
204
|
+
const current = doc[table] && typeof doc[table] === "object" && !Array.isArray(doc[table])
|
|
205
|
+
? /** @type {Record<string, unknown>} */ (doc[table])
|
|
206
|
+
: {};
|
|
207
|
+
doc[table] = { ...current, ...values };
|
|
208
|
+
writeConfigFile(file, `${CONFIG_HEADER}${TOML.stringify(doc)}`);
|
|
209
|
+
return "updated";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Write per-agent context management values into `[context_proxy.agents]` (merged with saved keys) and set
|
|
214
|
+
* `[context_proxy] enabled` to whether any agent resolves on. Other `[context_proxy]` keys are kept.
|
|
215
|
+
* @param {string} home
|
|
216
|
+
* @param {Record<string, boolean>} values keys from CONTEXT_AGENT_KEYS
|
|
217
|
+
* @param {{ dry?: boolean }} [opts]
|
|
218
|
+
* @returns {{ result: "updated"|"dry-run", agents: Record<string, boolean>, enabled: boolean }}
|
|
219
|
+
*/
|
|
220
|
+
export function updatePmConfigContextAgents(home, values, opts = {}) {
|
|
221
|
+
const file = userConfigPath(home);
|
|
222
|
+
const doc = fs.existsSync(file) ? readTomlFile(file) : {};
|
|
223
|
+
const current = contextAgentsFromPmConfig(doc).agents;
|
|
224
|
+
const agents = { ...current, ...values };
|
|
225
|
+
const enabled = CONTEXT_AGENT_KEYS.some((k) => agents[k]);
|
|
226
|
+
const result = updatePmConfigTable(home, "context_proxy", { enabled, agents }, opts);
|
|
227
|
+
return { result, agents, enabled };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `[agents] integration`: "openmem" (OpenMem hooks) or "provider" (the provider's own plugin).
|
|
232
|
+
* @param {Record<string, unknown>} cfg
|
|
233
|
+
*/
|
|
234
|
+
export function agentIntegrationFromPmConfig(cfg) {
|
|
235
|
+
const value = String(/** @type {Record<string, unknown>} */ (cfg.agents || {}).integration || "");
|
|
236
|
+
return AGENT_INTEGRATIONS.includes(value) ? value : "openmem";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** @param {Record<string, unknown>} cfg @param {string} name */
|
|
240
|
+
function tableOf(cfg, name) {
|
|
241
|
+
const t = cfg?.[name];
|
|
242
|
+
return t && typeof t === "object" && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** A non-negative integer config value, or undefined when unset or invalid. */
|
|
246
|
+
function countValue(value) {
|
|
247
|
+
if (value === null || value === undefined || typeof value === "boolean" || typeof value === "string") return undefined;
|
|
248
|
+
const n = Number(value);
|
|
249
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* `[context_proxy]` wiring with defaults: disabled, port 4100.
|
|
254
|
+
* @param {Record<string, unknown>} cfg
|
|
255
|
+
*/
|
|
256
|
+
export function contextProxyFromPmConfig(cfg) {
|
|
257
|
+
const table = tableOf(cfg, "context_proxy");
|
|
258
|
+
const port = Number(table.port);
|
|
259
|
+
return {
|
|
260
|
+
enabled: table.enabled === true,
|
|
261
|
+
port: Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_CONTEXT_PROXY_PORT,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Per-agent context management: `[context_proxy.agents] <key> = true|false`, a missing key following
|
|
267
|
+
* `[context_proxy] enabled`; with neither set, opencode2 is on (it needs no service) and the proxy-routed
|
|
268
|
+
* agents are off. Keys: CONTEXT_AGENT_KEYS. `saved` lists the keys that config.toml sets.
|
|
269
|
+
* @param {Record<string, unknown>} cfg
|
|
270
|
+
* @returns {{ agents: Record<string, boolean>, saved: string[], enabledSaved: boolean }}
|
|
271
|
+
*/
|
|
272
|
+
export function contextAgentsFromPmConfig(cfg) {
|
|
273
|
+
const table = tableOf(cfg, "context_proxy");
|
|
274
|
+
const overrides = tableOf(table, "agents");
|
|
275
|
+
const enabledSaved = typeof table.enabled === "boolean";
|
|
276
|
+
const agents = {};
|
|
277
|
+
const saved = [];
|
|
278
|
+
for (const key of CONTEXT_AGENT_KEYS) {
|
|
279
|
+
if (typeof overrides[key] === "boolean") {
|
|
280
|
+
agents[key] = overrides[key];
|
|
281
|
+
saved.push(key);
|
|
282
|
+
} else agents[key] = enabledSaved ? table.enabled : key === "opencode2";
|
|
283
|
+
}
|
|
284
|
+
return { agents, saved, enabledSaved };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Trim settings with defaults: `[context_proxy] trim_at_tokens` (120000; unset, the proxy and plugin trim Haiku at 95000), `compress_over_tokens`
|
|
289
|
+
* (4000), `standing_instruction` (true), `[compaction] max_chars` (12000), `native` (true). Env overrides are applied by
|
|
290
|
+
* `contextTrimEnvFromPmConfig` + `trimSettings()`, not here.
|
|
291
|
+
* @param {Record<string, unknown>} cfg
|
|
292
|
+
*/
|
|
293
|
+
export function contextTrimFromPmConfig(cfg) {
|
|
294
|
+
const proxy = tableOf(cfg, "context_proxy");
|
|
295
|
+
const compaction = tableOf(cfg, "compaction");
|
|
296
|
+
return {
|
|
297
|
+
trimAtTokens: countValue(proxy.trim_at_tokens) ?? DEFAULT_TRIM_AT_TOKENS,
|
|
298
|
+
compressOverTokens: countValue(proxy.compress_over_tokens) ?? DEFAULT_COMPRESS_OVER_TOKENS,
|
|
299
|
+
compactionMaxChars: countValue(compaction.max_chars) ?? DEFAULT_COMPACTION_MAX_CHARS,
|
|
300
|
+
compactionNative: compaction.native !== false,
|
|
301
|
+
standingInstruction: proxy.standing_instruction !== false,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Context-trim settings that config.toml sets, as the environment variables the proxy and
|
|
307
|
+
* `trimSettings()` read: `[context_proxy] trim_at_tokens` → CONTEXT_TRIM_AT_TOKENS,
|
|
308
|
+
* `compress_over_tokens` → CONTEXT_COMPRESS_OVER_TOKENS, `[compaction] max_chars` →
|
|
309
|
+
* CONTEXT_COMPACTION_MAX_CHARS, `[context_proxy] standing_instruction` (boolean) →
|
|
310
|
+
* CONTEXT_STANDING_INSTRUCTION ("1" / "0"), `[compaction] recall` (boolean) → CONTEXT_COMPACTION_RECALL
|
|
311
|
+
* ("1" / "0"), `[context_proxy] chars_per_token` (positive number) →
|
|
312
|
+
* CONTEXT_CHARS_PER_TOKEN, `trim_to_fraction` (0 < x < 1) → CONTEXT_TRIM_TO_FRACTION, `[context_store] ttl_days` → CONTEXT_STORE_TTL_DAYS, `embedding_model` →
|
|
313
|
+
* CONTEXT_EMBEDDING_MODEL, `embedding_url` → CONTEXT_EMBEDDING_URL, `embedding_timeout_s` (positive number) →
|
|
314
|
+
* CONTEXT_EMBEDDING_TIMEOUT_S, `embedding_max_chars` → CONTEXT_EMBEDDING_MAX_CHARS. Unset or invalid keys are left out, so the reader's defaults
|
|
315
|
+
* apply. Real env vars override: `trimSettings({ ...contextTrimEnvFromPmConfig(cfg), ...process.env })`.
|
|
316
|
+
* @param {Record<string, unknown>} cfg
|
|
317
|
+
* @returns {Record<string, string>}
|
|
318
|
+
*/
|
|
319
|
+
export function contextTrimEnvFromPmConfig(cfg) {
|
|
320
|
+
const out = {};
|
|
321
|
+
for (const [table, key, envName] of CONTEXT_TRIM_ENV_KEYS) {
|
|
322
|
+
const value = countValue(tableOf(cfg, table)[key]);
|
|
323
|
+
if (value !== undefined) out[envName] = String(value);
|
|
324
|
+
}
|
|
325
|
+
const standing = tableOf(cfg, "context_proxy").standing_instruction;
|
|
326
|
+
if (typeof standing === "boolean") out.CONTEXT_STANDING_INSTRUCTION = standing ? "1" : "0";
|
|
327
|
+
const recall = tableOf(cfg, "compaction").recall;
|
|
328
|
+
if (typeof recall === "boolean") out.CONTEXT_COMPACTION_RECALL = recall ? "1" : "0";
|
|
329
|
+
const fraction = tableOf(cfg, "context_proxy").trim_to_fraction;
|
|
330
|
+
if (typeof fraction === "number" && Number.isFinite(fraction) && fraction > 0 && fraction < 1) {
|
|
331
|
+
out.CONTEXT_TRIM_TO_FRACTION = String(fraction);
|
|
332
|
+
}
|
|
333
|
+
const ratio = tableOf(cfg, "context_proxy").chars_per_token;
|
|
334
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) out.CONTEXT_CHARS_PER_TOKEN = String(ratio);
|
|
335
|
+
const store = tableOf(cfg, "context_store");
|
|
336
|
+
const timeout = store.embedding_timeout_s;
|
|
337
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) out.CONTEXT_EMBEDDING_TIMEOUT_S = String(timeout);
|
|
338
|
+
for (const [key, envName] of CONTEXT_STORE_STRING_KEYS) {
|
|
339
|
+
if (typeof store[key] === "string") out[envName] = store[key].trim();
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Stack .env entries for the context-proxy compose service: the trim env above plus
|
|
346
|
+
* CONTEXT_PROXY_PORT when `[context_proxy] port` is set, so the container binds the port
|
|
347
|
+
* Claude Code is pointed at.
|
|
348
|
+
* @param {Record<string, unknown>} cfg
|
|
349
|
+
* @returns {Record<string, string>}
|
|
350
|
+
*/
|
|
351
|
+
export function contextProxyStackEnvFromPmConfig(cfg) {
|
|
352
|
+
const out = contextTrimEnvFromPmConfig(cfg);
|
|
353
|
+
if (tableOf(cfg, "context_proxy").port !== undefined) out.CONTEXT_PROXY_PORT = String(contextProxyFromPmConfig(cfg).port);
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Apply config defaults into process.env when env/CLI did not set them. Only the
|
|
359
|
+
* provider-neutral keys live here; a provider maps its own tables in its `init` hook.
|
|
360
|
+
* @param {Record<string, unknown>} cfg
|
|
361
|
+
*/
|
|
362
|
+
export function applyPmConfigEnv(cfg) {
|
|
363
|
+
const d = defaultsFromPmConfig(cfg);
|
|
364
|
+
if (d.memoryProvider && !process.env.MEMORY_PROVIDER) {
|
|
365
|
+
process.env.MEMORY_PROVIDER = d.memoryProvider;
|
|
366
|
+
}
|
|
367
|
+
if (d.stackProject && !process.env.PM_PROJECT) process.env.PM_PROJECT = d.stackProject;
|
|
368
|
+
if (d.wizardAction && !process.env.PM_WIZARD_ACTION) {
|
|
369
|
+
process.env.PM_WIZARD_ACTION = d.wizardAction;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { envValue } from "./paths.mjs";
|
|
5
|
+
import { ok, warn, die } from "./log.mjs";
|
|
6
|
+
import { confirm } from "./prompts.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {number} port
|
|
10
|
+
* @returns {Promise<boolean>}
|
|
11
|
+
*/
|
|
12
|
+
export function portFree(port) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const server = net.createServer();
|
|
15
|
+
server.unref();
|
|
16
|
+
server.on("error", () => resolve(false));
|
|
17
|
+
server.listen({ port, host: "127.0.0.1", exclusive: true }, () => {
|
|
18
|
+
server.close(() => resolve(true));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keep a host port when it is free (or held by this compose project); otherwise offer
|
|
25
|
+
* the next free one.
|
|
26
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
27
|
+
* @param {string} label
|
|
28
|
+
* @param {number} port
|
|
29
|
+
* @param {{ envVars?: string[] }} [opts] env var names that override the ports, for messages
|
|
30
|
+
* @returns {Promise<number>}
|
|
31
|
+
*/
|
|
32
|
+
export async function choosePort(ctx, label, port, opts = {}) {
|
|
33
|
+
const envVars = opts.envVars || [];
|
|
34
|
+
if (await portFree(port)) {
|
|
35
|
+
ok(`${label} port ${port} is free`);
|
|
36
|
+
return port;
|
|
37
|
+
}
|
|
38
|
+
let owner = "";
|
|
39
|
+
const ps = spawnSync(
|
|
40
|
+
"docker",
|
|
41
|
+
["ps", "--filter", `publish=${port}`, "--format", "{{.Label \"com.docker.compose.project\"}}"],
|
|
42
|
+
{ encoding: "utf8" },
|
|
43
|
+
);
|
|
44
|
+
if (ps.status === 0 && ps.stdout) {
|
|
45
|
+
owner = ps.stdout.split("\n").find((l) => l.trim())?.trim() || "";
|
|
46
|
+
}
|
|
47
|
+
if (owner === ctx.project) {
|
|
48
|
+
ok(`${label} port ${port} is held by this project (${ctx.project})`);
|
|
49
|
+
return port;
|
|
50
|
+
}
|
|
51
|
+
if (owner) warn(`${label} port ${port} is held by compose project '${owner}'`);
|
|
52
|
+
else warn(`${label} port ${port} is held by another process`);
|
|
53
|
+
|
|
54
|
+
let alt = 0;
|
|
55
|
+
for (let i = 1; i <= 50; i++) {
|
|
56
|
+
if (await portFree(port + i)) {
|
|
57
|
+
alt = port + i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!alt) {
|
|
62
|
+
die(
|
|
63
|
+
`no free port near ${port}${envVars.length ? `; set ${envVars.join(" or ")} and` : ";"} rerun`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (await confirm(ctx, `Use port ${alt} for the ${label} instead (saved to ${path.join(ctx.repoRoot, ".env")})?`, "y")) {
|
|
67
|
+
return alt;
|
|
68
|
+
}
|
|
69
|
+
die(`free port ${port}${envVars.length ? ` (or set ${envVars.join(" / ")})` : ""} and rerun ./setup`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve one host port from the active provider's manifest (`compose.hostPortEnv`,
|
|
74
|
+
* `compose.defaultHostPort`): process env, then the stack .env, then the default.
|
|
75
|
+
* Exports the chosen port in process.env under the manifest's variable name.
|
|
76
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
77
|
+
* @param {string} [label]
|
|
78
|
+
* @returns {Promise<number>}
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveHostPort(ctx, label) {
|
|
81
|
+
const compose = ctx.memoryProvider?.compose || {};
|
|
82
|
+
const envName = compose.hostPortEnv;
|
|
83
|
+
if (!envName) die(`provider ${ctx.memoryProvider?.id} declares no compose.hostPortEnv`);
|
|
84
|
+
const wanted =
|
|
85
|
+
process.env[envName] ||
|
|
86
|
+
envValue(path.join(ctx.repoRoot, ".env"), envName) ||
|
|
87
|
+
String(compose.defaultHostPort ?? 8888);
|
|
88
|
+
if (!/^\d+$/.test(wanted)) die(`${envName} must be a port number`);
|
|
89
|
+
const port = await choosePort(ctx, label || `${ctx.memoryProvider.label} API`, Number(wanted), {
|
|
90
|
+
envVars: [envName],
|
|
91
|
+
});
|
|
92
|
+
process.env[envName] = String(port);
|
|
93
|
+
return port;
|
|
94
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
6
|
+
* @param {string} prompt
|
|
7
|
+
* @param {string} defaultValue
|
|
8
|
+
*/
|
|
9
|
+
export async function ask(ctx, prompt, defaultValue) {
|
|
10
|
+
if (ctx.yes) return defaultValue;
|
|
11
|
+
if (!process.stdin.isTTY) return defaultValue;
|
|
12
|
+
try {
|
|
13
|
+
const clack = await import("@clack/prompts");
|
|
14
|
+
const v = await clack.text({
|
|
15
|
+
message: prompt,
|
|
16
|
+
placeholder: defaultValue,
|
|
17
|
+
defaultValue,
|
|
18
|
+
});
|
|
19
|
+
if (clack.isCancel(v)) process.exit(0);
|
|
20
|
+
const s = String(v ?? "").trim();
|
|
21
|
+
return s || defaultValue;
|
|
22
|
+
} catch {
|
|
23
|
+
const rl = readline.createInterface({ input, output });
|
|
24
|
+
const line = await rl.question(` ${prompt} [${defaultValue}]: `);
|
|
25
|
+
rl.close();
|
|
26
|
+
const s = line.trim();
|
|
27
|
+
return s || defaultValue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pick any of `options`; `--yes` and non-TTY runs return `initial`.
|
|
33
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
34
|
+
* @param {string} prompt
|
|
35
|
+
* @param {{ value: string, label: string }[]} options
|
|
36
|
+
* @param {string[]} initial
|
|
37
|
+
* @returns {Promise<string[]>}
|
|
38
|
+
*/
|
|
39
|
+
export async function multiselect(ctx, prompt, options, initial) {
|
|
40
|
+
if (ctx.yes) return initial;
|
|
41
|
+
if (!process.stdin.isTTY) return initial;
|
|
42
|
+
try {
|
|
43
|
+
const clack = await import("@clack/prompts");
|
|
44
|
+
const v = await clack.multiselect({ message: prompt, options, initialValues: initial, required: false });
|
|
45
|
+
if (clack.isCancel(v)) process.exit(0);
|
|
46
|
+
return /** @type {string[]} */ (v);
|
|
47
|
+
} catch {
|
|
48
|
+
const rl = readline.createInterface({ input, output });
|
|
49
|
+
const names = options.map((o) => o.value).join(", ");
|
|
50
|
+
const line = await rl.question(` ${prompt} (comma-separated: ${names}; "none" for none) [${initial.join(",") || "none"}]: `);
|
|
51
|
+
rl.close();
|
|
52
|
+
const s = line.trim();
|
|
53
|
+
if (!s) return initial;
|
|
54
|
+
if (s === "none") return [];
|
|
55
|
+
return s.split(",").map((x) => x.trim()).filter((x) => options.some((o) => o.value === x));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
61
|
+
* @param {string} prompt
|
|
62
|
+
* @param {"y"|"n"} defaultYN
|
|
63
|
+
*/
|
|
64
|
+
export async function confirm(ctx, prompt, defaultYN) {
|
|
65
|
+
if (ctx.yes) return defaultYN === "y";
|
|
66
|
+
if (!process.stdin.isTTY) return defaultYN === "y";
|
|
67
|
+
try {
|
|
68
|
+
const clack = await import("@clack/prompts");
|
|
69
|
+
const v = await clack.confirm({
|
|
70
|
+
message: prompt,
|
|
71
|
+
active: "yes",
|
|
72
|
+
inactive: "no",
|
|
73
|
+
initialValue: defaultYN === "y",
|
|
74
|
+
});
|
|
75
|
+
if (clack.isCancel(v)) process.exit(0);
|
|
76
|
+
return Boolean(v);
|
|
77
|
+
} catch {
|
|
78
|
+
const hint = defaultYN === "y" ? "Y/n" : "y/N";
|
|
79
|
+
const rl = readline.createInterface({ input, output });
|
|
80
|
+
const line = await rl.question(` ${prompt} [${hint}]: `);
|
|
81
|
+
rl.close();
|
|
82
|
+
const reply = (line.trim() || defaultYN).toLowerCase();
|
|
83
|
+
return reply === "y" || reply === "yes";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { step, info, ok, warn, die, problem } from "../log.mjs";
|
|
2
|
+
import { ask, confirm } from "../prompts.mjs";
|
|
3
|
+
import { act, actWithEnv, commandExists } from "../exec.mjs";
|
|
4
|
+
import { curlGet, curlJson, curlRequest } from "../curl.mjs";
|
|
5
|
+
import { loadMachineEnv } from "../load-machine-env.mjs";
|
|
6
|
+
import { envValue, expandHome, ensureHsDir } from "../paths.mjs";
|
|
7
|
+
import { portFree, choosePort, resolveHostPort } from "../ports.mjs";
|
|
8
|
+
import { composeArgs } from "../compose.mjs";
|
|
9
|
+
import { writeWizardConf } from "../wizard-conf.mjs";
|
|
10
|
+
import { providerApiBase } from "../providers/registry.mjs";
|
|
11
|
+
|
|
12
|
+
/** Bump when a helper is removed or changes meaning; adding helpers does not bump it. */
|
|
13
|
+
export const HELPERS_API_VERSION = 2;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* API passed as the second argument of every hook in providers/<id>/wizard.mjs.
|
|
17
|
+
* Providers must not import run-wizard.mjs (it imports them); everything the meta wizard
|
|
18
|
+
* owns reaches them through this object. Check `apiVersion` before relying on a helper.
|
|
19
|
+
*
|
|
20
|
+
* Every function that takes `ctx` receives the WizardContext the hook was called with
|
|
21
|
+
* (`ctx.repoRoot` = mutable stack root, `ctx.packageRoot` = read-only code root).
|
|
22
|
+
*
|
|
23
|
+
* @typedef {object} ProviderWizardHelpers
|
|
24
|
+
* @property {2} apiVersion
|
|
25
|
+
* @property {(title: string) => void} step print a numbered step heading
|
|
26
|
+
* @property {(msg: string) => void} info
|
|
27
|
+
* @property {(msg: string) => void} ok
|
|
28
|
+
* @property {(msg: string) => void} warn
|
|
29
|
+
* @property {(msg: string) => never} die print an error and exit 1
|
|
30
|
+
* @property {(ctx: object, msg: string) => void} problem die, or warn in a dry run
|
|
31
|
+
* @property {(ctx: object, prompt: string, defaultValue: string) => Promise<string>} ask
|
|
32
|
+
* @property {(ctx: object, prompt: string, defaultYN: "y"|"n") => Promise<boolean>} confirm
|
|
33
|
+
* @property {(ctx: object, description: string, cmd: string, args: string[], opts?: { env?: Record<string, string>, cwd?: string }) => Promise<void>} act
|
|
34
|
+
* run a command (printed, not run, in a dry run); exits the wizard on failure
|
|
35
|
+
* @property {(ctx: object, description: string, cmd: string, args: string[], extraEnv: Record<string, string>) => Promise<void>} actWithEnv
|
|
36
|
+
* @property {(name: string) => boolean} commandExists
|
|
37
|
+
* @property {(repoRoot: string) => Record<string, string>} loadMachineEnv deprecated; always {}
|
|
38
|
+
* @property {(file: string, key: string) => string} envValue read one KEY=value from an env file
|
|
39
|
+
* @property {(p: string, home?: string) => string} expandHome
|
|
40
|
+
* @property {(ctx: object) => void} ensureConfigDir create the per-machine config dir (no-op in a dry run)
|
|
41
|
+
* @property {(url: string, timeoutSec?: number) => string} curlGet
|
|
42
|
+
* @property {(url: string) => object|null} curlJson
|
|
43
|
+
* @property {(method: string, url: string, jsonBody?: string, timeoutSec?: number) => string} curlRequest
|
|
44
|
+
* @property {(port: number) => Promise<boolean>} portFree
|
|
45
|
+
* @property {(ctx: object, label: string, port: number, opts?: { envVars?: string[] }) => Promise<number>} choosePort
|
|
46
|
+
* @property {(ctx: object, label?: string) => Promise<number>} resolveHostPort
|
|
47
|
+
* pick the manifest's host port (compose.hostPortEnv / defaultHostPort) and export it in process.env
|
|
48
|
+
* @property {(ctx: object, ...rest: string[]) => string[]} composeArgs
|
|
49
|
+
* `docker` args for a compose subcommand, with the manifest's profile and files applied
|
|
50
|
+
* @property {(ctx: object, extra?: Record<string, string>, opts?: { pmSections?: Record<string, Record<string, unknown>> }) => void} saveWizardConf
|
|
51
|
+
* write wizard.conf (MEMORY_PROVIDER + extra keys) and config.toml; no-op in a dry run
|
|
52
|
+
* @property {(ctx: object, opts?: { description?: string, args?: string[], env?: Record<string, string> }) => Promise<void>} runStackSetup
|
|
53
|
+
* run scripts/setup.mjs (writes the stack .env and compose.override.yaml, then the provider's
|
|
54
|
+
* `prepareStackEnv`); `description` and `env` default to what the `stackEnv` hook returns
|
|
55
|
+
* @property {(ctx: object) => Promise<string>|string} apiBase the provider's API base URL (apiBase hook, else manifest)
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the helper object. With no argument it is fully functional from leaf modules;
|
|
60
|
+
* the meta wizard overrides the few helpers that need its own state.
|
|
61
|
+
* @param {Partial<ProviderWizardHelpers>} [impl]
|
|
62
|
+
* @returns {ProviderWizardHelpers}
|
|
63
|
+
*/
|
|
64
|
+
export function createHelpers(impl = {}) {
|
|
65
|
+
return Object.freeze(
|
|
66
|
+
/** @type {ProviderWizardHelpers} */ ({
|
|
67
|
+
apiVersion: HELPERS_API_VERSION,
|
|
68
|
+
step,
|
|
69
|
+
info,
|
|
70
|
+
ok,
|
|
71
|
+
warn,
|
|
72
|
+
die,
|
|
73
|
+
problem,
|
|
74
|
+
ask,
|
|
75
|
+
confirm,
|
|
76
|
+
act,
|
|
77
|
+
actWithEnv,
|
|
78
|
+
commandExists,
|
|
79
|
+
loadMachineEnv,
|
|
80
|
+
envValue,
|
|
81
|
+
expandHome,
|
|
82
|
+
ensureConfigDir: ensureHsDir,
|
|
83
|
+
curlGet,
|
|
84
|
+
curlJson,
|
|
85
|
+
curlRequest,
|
|
86
|
+
portFree,
|
|
87
|
+
choosePort,
|
|
88
|
+
resolveHostPort,
|
|
89
|
+
composeArgs,
|
|
90
|
+
saveWizardConf: writeWizardConf,
|
|
91
|
+
runStackSetup: async () => {
|
|
92
|
+
throw new Error("runStackSetup is only available inside the meta wizard");
|
|
93
|
+
},
|
|
94
|
+
apiBase: (ctx) => providerApiBase(ctx, ctx.memoryProvider),
|
|
95
|
+
...impl,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|