@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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotebookLM opt-in during setup: install the CLI if needed, run `notebooklm login`
|
|
3
|
+
* (browser) when there is no valid session, then inject NOTEBOOKLM_HOME so host
|
|
4
|
+
* commands and wizard.conf agree with the compose bind mount (~/.config/notebooklm).
|
|
5
|
+
* After sync is enabled, optionally opt in coding-agent MCP access (notebooklm_list_banks /
|
|
6
|
+
* notebooklm_search).
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { step, info, ok, warn } from "./log.mjs";
|
|
12
|
+
import { act, commandExists } from "./exec.mjs";
|
|
13
|
+
import { confirm } from "./prompts.mjs";
|
|
14
|
+
import { upsertWizardConf } from "./wizard-conf.mjs";
|
|
15
|
+
import { setNotebooklmAccessEnabled } from "./notebooklm-agent-access.mjs";
|
|
16
|
+
import { installNotebooklmMcp } from "../../providers/hindsight/lib/notebooklm-mcp-install.mjs";
|
|
17
|
+
|
|
18
|
+
/** @typedef {import("./run-wizard.mjs").WizardContext} WizardContext */
|
|
19
|
+
|
|
20
|
+
const MCP_CONFIRM =
|
|
21
|
+
"Allow coding agents to query NotebookLM banks via MCP (notebooklm_list_banks / notebooklm_search)?";
|
|
22
|
+
|
|
23
|
+
/** @param {WizardContext} ctx */
|
|
24
|
+
function codingAgentPaths(ctx) {
|
|
25
|
+
const agentPath = ctx.agentJson || path.join(ctx.home, ".hindsight", "coding-agent.json");
|
|
26
|
+
const templatePath = path.join(ctx.repoRoot, "config/coding-agent.json");
|
|
27
|
+
return { agentPath, templatePath };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* After NotebookLM sync was enabled this run: opt-in MCP access (default yes).
|
|
32
|
+
* @param {WizardContext} ctx
|
|
33
|
+
*/
|
|
34
|
+
export async function offerNotebooklmMcpAccess(ctx) {
|
|
35
|
+
const allow = await confirm(ctx, MCP_CONFIRM, "y");
|
|
36
|
+
const { agentPath, templatePath } = codingAgentPaths(ctx);
|
|
37
|
+
const h = { info, ok, warn };
|
|
38
|
+
|
|
39
|
+
if (!allow) {
|
|
40
|
+
if (ctx.dry) {
|
|
41
|
+
info(`[dry-run] leave notebooklmAccess.enabled false; skip MCP merge (${agentPath})`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (fs.existsSync(agentPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const st = fs.lstatSync(agentPath);
|
|
47
|
+
if (st.isFile() || st.isSymbolicLink()) {
|
|
48
|
+
setNotebooklmAccessEnabled(agentPath, templatePath, false);
|
|
49
|
+
ok(`notebooklmAccess.enabled=false in ${agentPath}`);
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
warn(`could not clear notebooklmAccess.enabled: ${err instanceof Error ? err.message : err}`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
info("notebooklmAccess left disabled (no coding-agent.json yet)");
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (ctx.dry) {
|
|
61
|
+
const plan = setNotebooklmAccessEnabled(agentPath, templatePath, true, { dry: true });
|
|
62
|
+
info(
|
|
63
|
+
`[dry-run] ${plan.action} ${agentPath} with notebooklmAccess.enabled=true; merge notebooklm MCP into ~/.cursor/mcp.json`,
|
|
64
|
+
);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = setNotebooklmAccessEnabled(agentPath, templatePath, true);
|
|
69
|
+
ok(
|
|
70
|
+
`notebooklmAccess.enabled=true in ${agentPath}` +
|
|
71
|
+
(result.action === "materialized" ? " (copied from symlink → regular file)" : ""),
|
|
72
|
+
);
|
|
73
|
+
await installNotebooklmMcp(
|
|
74
|
+
{ home: ctx.home, repoRoot: ctx.repoRoot, agentJson: agentPath, dry: false },
|
|
75
|
+
h,
|
|
76
|
+
);
|
|
77
|
+
ok("Restart Cursor Agent so it picks up the notebooklm MCP");
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Host path the compose service bind-mounts as /data/notebooklm. */
|
|
82
|
+
export function notebooklmHome(ctx) {
|
|
83
|
+
return path.join(ctx.home, ".config/notebooklm");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Env for every host `notebooklm` child: NOTEBOOKLM_HOME points at the shared cookie dir. */
|
|
87
|
+
export function notebooklmEnv(ctx) {
|
|
88
|
+
return { ...process.env, NOTEBOOKLM_HOME: notebooklmHome(ctx) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Persist NOTEBOOKLM_HOME into the process env and wizard.conf (compose already mounts the path).
|
|
93
|
+
* @param {WizardContext} ctx
|
|
94
|
+
*/
|
|
95
|
+
export function injectNotebooklmHome(ctx) {
|
|
96
|
+
const home = notebooklmHome(ctx);
|
|
97
|
+
process.env.NOTEBOOKLM_HOME = home;
|
|
98
|
+
upsertWizardConf(ctx, { NOTEBOOKLM_HOME: home });
|
|
99
|
+
ok(`NOTEBOOKLM_HOME=${home} (process env + wizard.conf)`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {WizardContext} ctx
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
export function notebooklmAuthOk(ctx) {
|
|
107
|
+
if (!commandExists("notebooklm")) return false;
|
|
108
|
+
const check = spawnSync("notebooklm", ["auth", "check", "--test", "--json"], {
|
|
109
|
+
env: notebooklmEnv(ctx),
|
|
110
|
+
stdio: "ignore",
|
|
111
|
+
});
|
|
112
|
+
return check.status === 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Install notebooklm-py + Playwright Chromium via uv when the CLI is missing.
|
|
117
|
+
* @param {WizardContext} ctx
|
|
118
|
+
* @returns {Promise<boolean>} true when `notebooklm` is on PATH afterwards
|
|
119
|
+
*/
|
|
120
|
+
export async function ensureNotebooklmCli(ctx) {
|
|
121
|
+
if (commandExists("notebooklm")) return true;
|
|
122
|
+
if (!commandExists("uv")) {
|
|
123
|
+
warn("uv is not on PATH; install uv, then rerun setup to enable NotebookLM");
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (ctx.dry) {
|
|
127
|
+
info(`[dry-run] uv tool install ${ctx.notebooklmPkg} && playwright install chromium`);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
info(`installing ${ctx.notebooklmPkg} (includes Playwright Chromium)`);
|
|
131
|
+
await act(ctx, `uv tool install ${ctx.notebooklmPkg}`, "uv", ["tool", "install", ctx.notebooklmPkg]);
|
|
132
|
+
await act(
|
|
133
|
+
ctx,
|
|
134
|
+
"playwright install chromium for notebooklm-py",
|
|
135
|
+
"uv",
|
|
136
|
+
["tool", "run", "--from", ctx.notebooklmPkg, "python", "-m", "playwright", "install", "chromium"],
|
|
137
|
+
);
|
|
138
|
+
if (!commandExists("notebooklm")) {
|
|
139
|
+
warn("notebooklm still not on PATH after install (check ~/.local/bin)");
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Open the Google sign-in browser via notebooklm-py. Stdio is inherited so the
|
|
147
|
+
* operator can complete login; no separate "are you done?" prompt.
|
|
148
|
+
* @param {WizardContext} ctx
|
|
149
|
+
* @returns {Promise<boolean>}
|
|
150
|
+
*/
|
|
151
|
+
export async function runNotebooklmLogin(ctx) {
|
|
152
|
+
if (ctx.dry) {
|
|
153
|
+
info("[dry-run] notebooklm login (opens a browser; NOTEBOOKLM_HOME set)");
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if (ctx.yes) {
|
|
157
|
+
warn("skipping notebooklm login under --yes (needs a browser); log in interactively or rerun without --yes");
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (!process.stdin.isTTY) {
|
|
161
|
+
warn("skipping notebooklm login (no TTY); run setup interactively once");
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
fs.mkdirSync(notebooklmHome(ctx), { recursive: true, mode: 0o700 });
|
|
165
|
+
info("opening Google sign-in for NotebookLM (complete it in the browser window)…");
|
|
166
|
+
const r = spawnSync("notebooklm", ["login"], {
|
|
167
|
+
env: notebooklmEnv(ctx),
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
});
|
|
170
|
+
return r.status === 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {WizardContext} ctx
|
|
175
|
+
* @param {(c: WizardContext) => void} enableNotebooklm
|
|
176
|
+
*/
|
|
177
|
+
export async function setupNotebooklm(ctx, enableNotebooklm) {
|
|
178
|
+
step("6. NotebookLM sync (optional, default no)");
|
|
179
|
+
if (!(await confirm(ctx, "Enable the NotebookLM sync? It stores a Google session cookie on this machine", "n"))) {
|
|
180
|
+
info("skipped; the service stays off");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (ctx.dry) {
|
|
184
|
+
info("[dry-run] install notebooklm-py if needed, login if auth fails, inject NOTEBOOKLM_HOME, enable notebooklm.yml");
|
|
185
|
+
await offerNotebooklmMcpAccess(ctx);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fs.mkdirSync(notebooklmHome(ctx), { recursive: true, mode: 0o700 });
|
|
190
|
+
injectNotebooklmHome(ctx);
|
|
191
|
+
|
|
192
|
+
if (!(await ensureNotebooklmCli(ctx))) return;
|
|
193
|
+
|
|
194
|
+
if (notebooklmAuthOk(ctx)) {
|
|
195
|
+
ok("NotebookLM session already valid");
|
|
196
|
+
enableNotebooklm(ctx);
|
|
197
|
+
await offerNotebooklmMcpAccess(ctx);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
info("no valid NotebookLM session yet; starting login…");
|
|
202
|
+
const loggedIn = await runNotebooklmLogin(ctx);
|
|
203
|
+
if (!loggedIn) {
|
|
204
|
+
warn(`login did not finish; ${ctx.notebookFile} is unchanged. Rerun setup when ready`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!notebooklmAuthOk(ctx)) {
|
|
208
|
+
warn(`auth check failed after login; ${ctx.notebookFile} is unchanged`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
ok("NotebookLM login ok");
|
|
212
|
+
enableNotebooklm(ctx);
|
|
213
|
+
await offerNotebooklmMcpAccess(ctx);
|
|
214
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { die } from "./log.mjs";
|
|
4
|
+
import { parseActionFromArgv } from "./wizard-action.mjs";
|
|
5
|
+
|
|
6
|
+
const HELP = `Setup wizard: takes a fresh clone to a running memory stack for the selected provider. Symlinked as ./setup at the repo root.
|
|
7
|
+
Re-runnable: shows current values as defaults and asks before it replaces config or secrets.
|
|
8
|
+
|
|
9
|
+
./setup interactive (@clack/prompts UI, then the rest)
|
|
10
|
+
./setup --yes accept every default, no prompts
|
|
11
|
+
./setup --dry-run [--yes] print the actions; read-only checks run, nothing is written or started
|
|
12
|
+
./setup --yes --no-smoke skip the smoke retain/recall (for scripted re-runs)
|
|
13
|
+
./setup --rebuild rebuild the images even when their sources are unchanged
|
|
14
|
+
./setup --up [--yes] start the stack using existing wizard.conf
|
|
15
|
+
./setup --down [--yes] stop the stack (docker compose down)
|
|
16
|
+
|
|
17
|
+
PM_PROJECT sets the compose project name (default: openmem). MEMORY_PROVIDER selects the
|
|
18
|
+
memory plugin (default: hindsight). PM_WIZARD_ACTION=configure|up|down. Each provider documents its
|
|
19
|
+
own environment variables (model backend, ports) in providers/<id>/. PM_STACK_ROOT and PM_PACKAGE_ROOT
|
|
20
|
+
split the mutable stack directory from the read-only code directory. The last line is
|
|
21
|
+
"wizard: changed=N": files and containers this run altered.
|
|
22
|
+
Test against a scratch HOME:
|
|
23
|
+
HOME=/tmp/wiz-home PM_PROJECT=pm-wizard-test ./setup --yes`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} repoRoot
|
|
27
|
+
* @param {string[]} argv
|
|
28
|
+
* @returns {import("./run-wizard.mjs").WizardContext}
|
|
29
|
+
*/
|
|
30
|
+
export function parseWizardFlags(repoRoot, argv) {
|
|
31
|
+
const home = process.env.HOME || os.homedir();
|
|
32
|
+
|
|
33
|
+
const ctx = {
|
|
34
|
+
repoRoot,
|
|
35
|
+
// Read-only code root (wizard/, providers/, scripts/); the stack root is repoRoot.
|
|
36
|
+
packageRoot: process.env.PM_PACKAGE_ROOT ? path.resolve(process.env.PM_PACKAGE_ROOT) : repoRoot,
|
|
37
|
+
home,
|
|
38
|
+
project: process.env.PM_PROJECT || "openmem",
|
|
39
|
+
// Shared per-machine config dir: wizard.conf, index.yml, notebooklm.yml. The name predates
|
|
40
|
+
// provider plugins and stays fixed so existing machines keep their config.
|
|
41
|
+
hsDir: path.join(home, ".config/hindsight"),
|
|
42
|
+
indexFile: path.join(home, ".config/hindsight/index.yml"),
|
|
43
|
+
notebookFile: path.join(home, ".config/hindsight/notebooklm.yml"),
|
|
44
|
+
wizardConf: path.join(home, ".config/hindsight/wizard.conf"),
|
|
45
|
+
smokeBank: "wizard-smoke",
|
|
46
|
+
buildHashFile: path.join(repoRoot, ".build-hash"),
|
|
47
|
+
notebooklmPkg: "notebooklm-py[browser]==0.8.5",
|
|
48
|
+
yes: false,
|
|
49
|
+
dry: false,
|
|
50
|
+
smoke: true,
|
|
51
|
+
rebuild: false,
|
|
52
|
+
// True when the Clack UI already asked the provider's questions (WIZARD_SKIP_BACKEND_STEP=1).
|
|
53
|
+
skipBackendStep: process.env.WIZARD_SKIP_BACKEND_STEP === "1",
|
|
54
|
+
action: parseActionFromArgv(argv),
|
|
55
|
+
providerId: "",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (const arg of argv) {
|
|
59
|
+
switch (arg) {
|
|
60
|
+
case "--up":
|
|
61
|
+
case "--down":
|
|
62
|
+
break;
|
|
63
|
+
case "--yes":
|
|
64
|
+
case "-y":
|
|
65
|
+
ctx.yes = true;
|
|
66
|
+
break;
|
|
67
|
+
case "--dry-run":
|
|
68
|
+
ctx.dry = true;
|
|
69
|
+
break;
|
|
70
|
+
case "--no-smoke":
|
|
71
|
+
ctx.smoke = false;
|
|
72
|
+
break;
|
|
73
|
+
case "--rebuild":
|
|
74
|
+
ctx.rebuild = true;
|
|
75
|
+
break;
|
|
76
|
+
case "-h":
|
|
77
|
+
case "--help":
|
|
78
|
+
console.log(HELP);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
die(`unknown option: ${arg} (see --help)`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ctx;
|
|
87
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function expandHome(p, home = process.env.HOME || os.homedir()) {
|
|
6
|
+
if (p === "~") return home;
|
|
7
|
+
if (p.startsWith("~/")) return path.join(home, p.slice(2));
|
|
8
|
+
return p;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function envValue(filePath, key) {
|
|
12
|
+
if (!fs.existsSync(filePath)) return "";
|
|
13
|
+
const prefix = `${key}=`;
|
|
14
|
+
for (const line of fs.readFileSync(filePath, "utf8").split("\n")) {
|
|
15
|
+
if (line.startsWith(prefix)) return line.slice(prefix.length);
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ensureHsDir(ctx) {
|
|
21
|
+
if (ctx.dry) return;
|
|
22
|
+
fs.mkdirSync(ctx.hsDir, { recursive: true, mode: 0o700 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function realpathSafe(p) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.realpathSync(p);
|
|
28
|
+
} catch {
|
|
29
|
+
return path.resolve(p);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mount point for the shared indexer's state. The directory name predates provider
|
|
35
|
+
* plugins and stays fixed so existing state keeps working.
|
|
36
|
+
*/
|
|
37
|
+
export function indexerStateDir(ctx) {
|
|
38
|
+
return path.join(ctx.home, ".cache", "hindsight-project-index");
|
|
39
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import TOML from "@iarna/toml";
|
|
4
|
+
import { DEFAULT_PROVIDER_ID } from "./providers/registry.mjs";
|
|
5
|
+
|
|
6
|
+
/** @param {string} home */
|
|
7
|
+
export function userConfigPath(home) {
|
|
8
|
+
return path.join(home, ".config/personal-memory/config.toml");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @param {string} repoRoot */
|
|
12
|
+
export function projectConfigPaths(repoRoot) {
|
|
13
|
+
return [
|
|
14
|
+
path.join(repoRoot, "openmem.toml"),
|
|
15
|
+
path.join(repoRoot, "config/openmem.toml"),
|
|
16
|
+
// Pre-rebrand filenames still load if present.
|
|
17
|
+
path.join(repoRoot, "personal-memory.toml"),
|
|
18
|
+
path.join(repoRoot, "config/personal-memory.toml"),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} file
|
|
24
|
+
* @returns {Record<string, unknown>}
|
|
25
|
+
*/
|
|
26
|
+
function readTomlFile(file) {
|
|
27
|
+
if (!fs.existsSync(file)) return {};
|
|
28
|
+
try {
|
|
29
|
+
return TOML.parse(fs.readFileSync(file, "utf8"));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error(`${file}: ${/** @type {Error} */ (e).message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {Record<string, unknown>} base @param {Record<string, unknown>} over */
|
|
36
|
+
function deepMerge(base, over) {
|
|
37
|
+
const out = { ...base };
|
|
38
|
+
for (const [k, v] of Object.entries(over)) {
|
|
39
|
+
if (v && typeof v === "object" && !Array.isArray(v) && typeof out[k] === "object") {
|
|
40
|
+
out[k] = deepMerge(/** @type {Record<string, unknown>} */ (out[k]), /** @type {Record<string, unknown>} */ (v));
|
|
41
|
+
} else out[k] = v;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} home
|
|
48
|
+
* @param {string} repoRoot
|
|
49
|
+
* @returns {{ merged: Record<string, unknown>, userPath: string, wroteFrom: string[] }}
|
|
50
|
+
*/
|
|
51
|
+
export function loadPmConfig(home, repoRoot) {
|
|
52
|
+
let merged = {};
|
|
53
|
+
const wroteFrom = [];
|
|
54
|
+
for (const p of projectConfigPaths(repoRoot)) {
|
|
55
|
+
if (fs.existsSync(p)) {
|
|
56
|
+
merged = deepMerge(merged, readTomlFile(p));
|
|
57
|
+
wroteFrom.push(p);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const userPath = userConfigPath(home);
|
|
61
|
+
if (fs.existsSync(userPath)) {
|
|
62
|
+
merged = deepMerge(merged, readTomlFile(userPath));
|
|
63
|
+
wroteFrom.push(userPath);
|
|
64
|
+
}
|
|
65
|
+
return { merged, userPath, wroteFrom };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Flat defaults for the wizard (--yes / Clack initialValue). Provider-specific tables
|
|
70
|
+
* ([model], provider ports) are read by the provider's own hooks from `cfg`.
|
|
71
|
+
* @param {Record<string, unknown>} cfg
|
|
72
|
+
*/
|
|
73
|
+
export function defaultsFromPmConfig(cfg) {
|
|
74
|
+
const memory = /** @type {Record<string, unknown>} */ (cfg.memory || {});
|
|
75
|
+
const stack = /** @type {Record<string, unknown>} */ (cfg.stack || {});
|
|
76
|
+
const wizard = /** @type {Record<string, unknown>} */ (cfg.wizard || {});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
memoryProvider: String(memory.provider || ""),
|
|
80
|
+
stackProject: String(stack.project || ""),
|
|
81
|
+
wizardAction: String(wizard.action || ""),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
87
|
+
* @param {Record<string, Record<string, unknown>>} [sections] provider-owned tables:
|
|
88
|
+
* `stack` keys are merged into [stack]; `memory` sub-tables (for example `memory.mem0`) are
|
|
89
|
+
* merged into [memory], and `provider` there is always the active provider id; every other
|
|
90
|
+
* key becomes its own top-level table
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function buildPmConfigToml(ctx, sections = {}) {
|
|
94
|
+
const { stack: stackExtra = {}, memory: memoryExtra = {}, ...rest } = sections;
|
|
95
|
+
const doc = {
|
|
96
|
+
memory: { ...memoryExtra, provider: ctx.memoryProvider?.id || DEFAULT_PROVIDER_ID },
|
|
97
|
+
stack: { project: ctx.project, ...stackExtra },
|
|
98
|
+
wizard: { action: ctx.action || "configure" },
|
|
99
|
+
...rest,
|
|
100
|
+
tasks: {
|
|
101
|
+
configure: { description: "Secrets, index sources, build images, compose up" },
|
|
102
|
+
up: { description: "Start stack using existing config" },
|
|
103
|
+
down: { description: "docker compose down for this project" },
|
|
104
|
+
doctor: { description: "Validate memory setup on this machine" },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
return `# Written by ./setup — mise-like config for openmem.\n# Docs: docs/openmem-config.md\n${TOML.stringify(doc)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
112
|
+
* @param {Record<string, Record<string, unknown>>} [sections] see buildPmConfigToml
|
|
113
|
+
*/
|
|
114
|
+
export function syncPmConfig(ctx, sections = {}) {
|
|
115
|
+
if (ctx.dry) return;
|
|
116
|
+
const dir = path.dirname(userConfigPath(ctx.home));
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
118
|
+
const file = userConfigPath(ctx.home);
|
|
119
|
+
fs.writeFileSync(file, buildPmConfigToml(ctx, sections), { mode: 0o600 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Minimal in-place update of `memory.provider` in the user config.toml when the file
|
|
124
|
+
* already exists. Does not create the file (unlike syncPmConfig).
|
|
125
|
+
*
|
|
126
|
+
* @param {string} home
|
|
127
|
+
* @param {string} providerId
|
|
128
|
+
* @param {{ dry?: boolean }} [opts]
|
|
129
|
+
* @returns {"updated"|"skipped"|"dry-run"}
|
|
130
|
+
*/
|
|
131
|
+
export function updatePmConfigProvider(home, providerId, opts = {}) {
|
|
132
|
+
const file = userConfigPath(home);
|
|
133
|
+
if (!fs.existsSync(file)) return "skipped";
|
|
134
|
+
if (opts.dry) return "dry-run";
|
|
135
|
+
const doc = readTomlFile(file);
|
|
136
|
+
const memory = {
|
|
137
|
+
...(doc.memory && typeof doc.memory === "object" && !Array.isArray(doc.memory)
|
|
138
|
+
? /** @type {Record<string, unknown>} */ (doc.memory)
|
|
139
|
+
: {}),
|
|
140
|
+
provider: providerId,
|
|
141
|
+
};
|
|
142
|
+
doc.memory = memory;
|
|
143
|
+
const header =
|
|
144
|
+
"# Written by openmem — mise-like config.\n# Docs: docs/openmem-config.md\n";
|
|
145
|
+
fs.writeFileSync(file, `${header}${TOML.stringify(doc)}`, { mode: 0o600 });
|
|
146
|
+
return "updated";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Apply config defaults into process.env when env/CLI did not set them. Only the
|
|
151
|
+
* provider-neutral keys live here; a provider maps its own tables in its `init` hook.
|
|
152
|
+
* @param {Record<string, unknown>} cfg
|
|
153
|
+
*/
|
|
154
|
+
export function applyPmConfigEnv(cfg) {
|
|
155
|
+
const d = defaultsFromPmConfig(cfg);
|
|
156
|
+
if (d.memoryProvider && !process.env.MEMORY_PROVIDER) {
|
|
157
|
+
process.env.MEMORY_PROVIDER = d.memoryProvider;
|
|
158
|
+
}
|
|
159
|
+
if (d.stackProject && !process.env.PM_PROJECT) process.env.PM_PROJECT = d.stackProject;
|
|
160
|
+
if (d.wizardAction && !process.env.PM_WIZARD_ACTION) {
|
|
161
|
+
process.env.PM_WIZARD_ACTION = d.wizardAction;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -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,57 @@
|
|
|
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
|
+
* @param {import("./run-wizard.mjs").WizardContext} ctx
|
|
33
|
+
* @param {string} prompt
|
|
34
|
+
* @param {"y"|"n"} defaultYN
|
|
35
|
+
*/
|
|
36
|
+
export async function confirm(ctx, prompt, defaultYN) {
|
|
37
|
+
if (ctx.yes) return defaultYN === "y";
|
|
38
|
+
if (!process.stdin.isTTY) return defaultYN === "y";
|
|
39
|
+
try {
|
|
40
|
+
const clack = await import("@clack/prompts");
|
|
41
|
+
const v = await clack.confirm({
|
|
42
|
+
message: prompt,
|
|
43
|
+
active: "yes",
|
|
44
|
+
inactive: "no",
|
|
45
|
+
initialValue: defaultYN === "y",
|
|
46
|
+
});
|
|
47
|
+
if (clack.isCancel(v)) process.exit(0);
|
|
48
|
+
return Boolean(v);
|
|
49
|
+
} catch {
|
|
50
|
+
const hint = defaultYN === "y" ? "Y/n" : "y/N";
|
|
51
|
+
const rl = readline.createInterface({ input, output });
|
|
52
|
+
const line = await rl.question(` ${prompt} [${hint}]: `);
|
|
53
|
+
rl.close();
|
|
54
|
+
const reply = (line.trim() || defaultYN).toLowerCase();
|
|
55
|
+
return reply === "y" || reply === "yes";
|
|
56
|
+
}
|
|
57
|
+
}
|