wikimemory 0.2.3 → 0.2.5

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.
@@ -5,6 +5,7 @@ import { WIKIMEMORY_VERSION } from "../src/version.js";
5
5
  import { deploymentArguments, installArguments } from "./cli-options.js";
6
6
  import { deploymentPaths } from "./deployment-record.js";
7
7
  import { packageRoot } from "./package-root.js";
8
+ import { conciseError } from "./subprocess.js";
8
9
  const [command, ...args] = process.argv.slice(2);
9
10
  function usage() {
10
11
  return `Wikimemory ${WIKIMEMORY_VERSION}\nPersonal, passkey-protected memory for Claude, Codex, and other MCP clients.\n\nUsage: wikimemory COMMAND [OPTIONS]\n\nStart a personal Cloudflare-hosted instance:\n npx --yes wikimemory install\n\nTry it locally without a Cloudflare deployment:\n npx --yes wikimemory dev\n\nCommands:\n wikimemory install [--deployment NAME] [installer options]\n wikimemory recover [--deployment NAME]\n wikimemory dev [wrangler dev options]\n wikimemory status [--deployment NAME]\n wikimemory upgrade [--deployment NAME] [--yes]\n wikimemory passkeys [--deployment NAME] list|add|revoke\n wikimemory connect [--deployment NAME] codex|claude\n wikimemory skills install codex|claude\n wikimemory uninstall [--deployment NAME] [--apply]\n wikimemory --version\n\nUse \`wikimemory COMMAND --help\` for command-specific options.`;
@@ -74,6 +75,6 @@ async function main() {
74
75
  throw new Error(`Unknown command: ${command}`);
75
76
  }
76
77
  main().catch((error) => {
77
- console.error(`Wikimemory failed: ${error instanceof Error ? error.message : "Unknown error"}`);
78
+ console.error(`Wikimemory failed: ${conciseError(error)}`);
78
79
  process.exitCode = 1;
79
80
  });
@@ -1,24 +1,19 @@
1
- import { spawn } from "node:child_process";
2
1
  import { cp, mkdir } from "node:fs/promises";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
5
4
  import { deploymentRecordPath, readDeploymentRecord } from "./deployment-record.js";
5
+ import { commandFailureMessage, runCommand } from "./subprocess.js";
6
6
  function client(value) {
7
7
  if (value === "codex" || value === "claude")
8
8
  return value;
9
9
  throw new Error("Client must be codex or claude");
10
10
  }
11
- async function run(command, args) {
12
- await new Promise((resolve, reject) => {
13
- const child = spawn(command, args, { stdio: "inherit" });
14
- child.on("error", reject);
15
- child.on("close", (code) => {
16
- if (code === 0)
17
- resolve();
18
- else
19
- reject(new Error(`${command} exited with status ${code ?? "unknown"}`));
20
- });
11
+ async function run(command, args, interactive = false) {
12
+ const result = await runCommand(command, args, {
13
+ ...(interactive ? { forwardLimitBytes: 4000, inheritStdin: true } : {})
21
14
  });
15
+ if (result.exitCode !== 0)
16
+ throw new Error(commandFailureMessage(`${command} client configuration`, result));
22
17
  }
23
18
  export async function connectClient(deployment, selected) {
24
19
  const target = client(selected);
@@ -26,7 +21,8 @@ export async function connectClient(deployment, selected) {
26
21
  const endpoint = `${record.origin}/mcp`;
27
22
  if (target === "codex") {
28
23
  await run("codex", ["mcp", "add", deployment, "--url", endpoint]);
29
- await run("codex", ["mcp", "login", deployment, "--scopes", "memory:read,memory:write"]);
24
+ console.log("Complete authorization in your browser…");
25
+ await run("codex", ["mcp", "login", deployment, "--scopes", "memory:read,memory:write"], true);
30
26
  }
31
27
  else {
32
28
  await run("claude", [
@@ -39,8 +35,10 @@ export async function connectClient(deployment, selected) {
39
35
  deployment,
40
36
  endpoint
41
37
  ]);
42
- await run("claude", ["mcp", "login", deployment]);
38
+ console.log("Complete authorization in your browser…");
39
+ await run("claude", ["mcp", "login", deployment], true);
43
40
  }
41
+ console.log(`Connected ${target} to ${deployment}.`);
44
42
  }
45
43
  export async function installSkills(packageRoot, selected) {
46
44
  const target = client(selected);
@@ -1,6 +1,6 @@
1
- import { spawn } from "node:child_process";
2
1
  import { mkdir, writeFile } from "node:fs/promises";
3
2
  import { join, resolve } from "node:path";
3
+ import { commandFailureMessage, runCommand } from "./subprocess.js";
4
4
  export function localConfig(packageRoot) {
5
5
  return `${JSON.stringify({
6
6
  $schema: join(packageRoot, "node_modules", "wrangler", "config-schema.json"),
@@ -27,24 +27,19 @@ export function localConfig(packageRoot) {
27
27
  }, null, 2)}\n`;
28
28
  }
29
29
  async function runWrangler(args, inherited = false) {
30
- await new Promise((resolvePromise, reject) => {
31
- const child = spawn("npx", ["wrangler", ...args], {
32
- stdio: inherited ? "inherit" : ["ignore", "inherit", "inherit"]
33
- });
34
- child.on("error", reject);
35
- child.on("close", (code) => {
36
- if (code === 0)
37
- resolvePromise();
38
- else
39
- reject(new Error(`Wrangler exited with status ${code ?? "unknown"}`));
40
- });
30
+ const result = await runCommand("npx", ["wrangler", ...args], {
31
+ ...(inherited ? { forwardLimitBytes: 8000, inheritStdin: true } : {})
41
32
  });
33
+ if (result.exitCode !== 0)
34
+ throw new Error(commandFailureMessage("Wrangler", result));
42
35
  }
43
36
  export async function runDev(packageRoot, args) {
44
37
  const stateDirectory = resolve(".wikimemory", "dev");
45
38
  await mkdir(stateDirectory, { recursive: true });
46
39
  const configPath = join(stateDirectory, "wrangler.jsonc");
47
40
  await writeFile(configPath, localConfig(packageRoot), "utf8");
41
+ console.log("Preparing local database…");
48
42
  await runWrangler(["d1", "migrations", "apply", "wikimemory", "--local", "--config", configPath]);
43
+ console.log("Starting local Wikimemory…");
49
44
  await runWrangler(["dev", "--config", configPath, ...args], true);
50
45
  }
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
7
7
  import { z } from "zod";
8
8
  import { passkeyRuntime } from "./lifecycle-runtime.js";
9
9
  import { configuredOrigin } from "./setup.js";
10
+ import { conciseError } from "./subprocess.js";
10
11
  const PRODUCTION_CONFIG = passkeyRuntime.config;
11
12
  const CLI_STATE = passkeyRuntime.client;
12
13
  const CALLBACK = "http://127.0.0.1:45831/callback";
@@ -191,7 +192,7 @@ export async function runPasskeys(args) {
191
192
  const entrypoint = process.argv[1];
192
193
  if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
193
194
  runPasskeys(process.argv.slice(2)).catch((error) => {
194
- console.error(`Passkey command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
195
+ console.error(`Passkey command failed: ${conciseError(error)}`);
195
196
  process.exitCode = 1;
196
197
  });
197
198
  }
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createHash, randomBytes } from "node:crypto";
3
2
  import { constants } from "node:fs";
4
3
  import { access, mkdtemp, readdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
@@ -11,6 +10,8 @@ import { z } from "zod";
11
10
  import { WIKIMEMORY_VERSION } from "../src/version.js";
12
11
  import { deploymentRecordFromConfig, writeDeploymentRecord } from "./deployment-record.js";
13
12
  import { installedRecordPath, packageRoot, setupRuntime } from "./lifecycle-runtime.js";
13
+ import { commandFailureMessage, conciseError, runCommand } from "./subprocess.js";
14
+ export { commandFailureMessage } from "./subprocess.js";
14
15
  const PACKAGE_ROOT = packageRoot;
15
16
  const CONFIG_PATH = setupRuntime.config;
16
17
  const STATE_PATH = setupRuntime.progress;
@@ -55,16 +56,6 @@ function cloudflareOperation(args) {
55
56
  return "KV namespace creation";
56
57
  return `Cloudflare ${command}`;
57
58
  }
58
- function conciseDiagnostic(result) {
59
- const raw = result.stderr.trim() || result.stdout.trim();
60
- const plain = raw.replaceAll("\u001b", "").replaceAll(/\s+/gu, " ").trim();
61
- if (plain === "")
62
- return `process exited with status ${result.exitCode}`;
63
- return plain.length > 600 ? `${plain.slice(0, 597)}...` : plain;
64
- }
65
- export function commandFailureMessage(operation, result) {
66
- return `${operation} failed: ${conciseDiagnostic(result)}`;
67
- }
68
59
  async function sleep(milliseconds) {
69
60
  await new Promise((resolve) => {
70
61
  setTimeout(resolve, milliseconds);
@@ -146,44 +137,18 @@ async function exists(path) {
146
137
  }
147
138
  }
148
139
  async function run(command, args, input, allowFailure = false) {
149
- const result = await new Promise((resolve, reject) => {
150
- const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
151
- let stdout = "";
152
- let stderr = "";
153
- child.stdout.setEncoding("utf8");
154
- child.stderr.setEncoding("utf8");
155
- child.stdout.on("data", (chunk) => {
156
- stdout += chunk;
157
- });
158
- child.stderr.on("data", (chunk) => {
159
- stderr += chunk;
160
- });
161
- child.on("error", (error) => {
162
- reject(error);
163
- });
164
- child.on("close", (code) => {
165
- resolve({ stdout, stderr, exitCode: code ?? -1 });
166
- });
167
- child.stdin.end(input === undefined ? undefined : `${input}\n`);
168
- });
140
+ const result = await runCommand(command, args, { ...(input === undefined ? {} : { input }) });
169
141
  if (result.exitCode !== 0 && !allowFailure)
170
142
  throw new Error(commandFailureMessage(cloudflareOperation(args), result));
171
143
  return result;
172
144
  }
173
145
  async function runInteractive(command, args) {
174
- console.log(`\n> ${command} ${args.join(" ")}`);
175
- await new Promise((resolve, reject) => {
176
- const child = spawn(command, args, { stdio: "inherit" });
177
- child.on("error", (error) => {
178
- reject(error);
179
- });
180
- child.on("close", (code) => {
181
- if (code === 0)
182
- resolve();
183
- else
184
- reject(new Error(`${command} exited with status ${code ?? "unknown"}`));
185
- });
146
+ const result = await runCommand(command, args, {
147
+ forwardLimitBytes: 4000,
148
+ inheritStdin: true
186
149
  });
150
+ if (result.exitCode !== 0)
151
+ throw new Error(commandFailureMessage(cloudflareOperation(args), result));
187
152
  }
188
153
  export function initialConfig(options, account) {
189
154
  return `${JSON.stringify({
@@ -504,7 +469,6 @@ async function finalizeDeployment(options) {
504
469
  const deploymentRecord = deploymentRecordFromConfig(finalConfig, WIKIMEMORY_VERSION, options.kvName);
505
470
  const recordPath = installedRecordPath(options.workerName);
506
471
  await writeDeploymentRecord(deploymentRecord, recordPath);
507
- console.log(`\nSaved deployment record: ${recordPath}`);
508
472
  console.log(`\n${handoff(origin, secret.raw, options.workerName)}`);
509
473
  }
510
474
  async function freshDeployment(options) {
@@ -602,7 +566,7 @@ export async function runSetup(args) {
602
566
  const entrypoint = process.argv[1];
603
567
  if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
604
568
  runSetup(process.argv.slice(2)).catch((error) => {
605
- console.error(`\nSetup failed: ${error instanceof Error ? error.message : "Unknown error"}`);
569
+ console.error(`\nSetup failed: ${conciseError(error)}`);
606
570
  process.exitCode = 1;
607
571
  });
608
572
  }
@@ -45,5 +45,11 @@ export async function deploymentStatus(deployment) {
45
45
  }
46
46
  export async function runStatus(deployment) {
47
47
  const status = await deploymentStatus(deployment);
48
- console.log(`Wikimemory deployment: ${status.deployment}\n Account: ${status.accountId}\n Worker: ${status.workerName}\n D1: ${status.database}\n KV: ${status.kvNamespace}\n Origin: ${status.origin}\n Recorded version: ${status.recordedVersion}\n Running version: ${status.runningVersion}\n Schema: ${status.schemaVersion}`);
48
+ console.log(statusSummary(status));
49
+ }
50
+ export function statusSummary(status) {
51
+ const mismatch = status.recordedVersion === status.runningVersion
52
+ ? ""
53
+ : `\nLocal record: ${status.recordedVersion} (run wikimemory upgrade to reconcile)`;
54
+ return `Wikimemory ${status.deployment}: ready\nURL: ${status.origin}\nVersion: ${status.runningVersion}\nDatabase: up to date.${mismatch}`;
49
55
  }
@@ -0,0 +1,74 @@
1
+ import { spawn } from "node:child_process";
2
+ import process from "node:process";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ export function boundedOutputChunk(chunk, remaining) {
5
+ const visible = chunk.slice(0, remaining);
6
+ return {
7
+ visible,
8
+ remaining: Math.max(0, remaining - visible.length),
9
+ truncated: visible.length < chunk.length
10
+ };
11
+ }
12
+ function forwardBounded(destination, chunk, state) {
13
+ const limited = boundedOutputChunk(chunk, state.remaining);
14
+ if (limited.visible !== "")
15
+ destination.write(limited.visible);
16
+ state.remaining = limited.remaining;
17
+ if (limited.truncated && !state.suppressed) {
18
+ destination.write("\n… additional command output suppressed.\n");
19
+ state.suppressed = true;
20
+ }
21
+ }
22
+ export async function runCommand(command, args, options = {}) {
23
+ return await new Promise((resolve, reject) => {
24
+ const child = spawn(command, args, {
25
+ stdio: [options.inheritStdin === true ? "inherit" : "pipe", "pipe", "pipe"]
26
+ });
27
+ if (child.stdout === null || child.stderr === null) {
28
+ child.kill();
29
+ reject(new Error("Subprocess output pipes were not created"));
30
+ return;
31
+ }
32
+ let stdout = "";
33
+ let stderr = "";
34
+ const stdoutForward = { remaining: options.forwardLimitBytes ?? 0, suppressed: false };
35
+ const stderrForward = { remaining: options.forwardLimitBytes ?? 0, suppressed: false };
36
+ child.stdout.setEncoding("utf8");
37
+ child.stderr.setEncoding("utf8");
38
+ child.stdout.on("data", (chunk) => {
39
+ stdout += chunk;
40
+ if (options.forwardLimitBytes !== undefined)
41
+ forwardBounded(process.stdout, chunk, stdoutForward);
42
+ });
43
+ child.stderr.on("data", (chunk) => {
44
+ stderr += chunk;
45
+ if (options.forwardLimitBytes !== undefined)
46
+ forwardBounded(process.stderr, chunk, stderrForward);
47
+ });
48
+ child.on("error", reject);
49
+ child.on("close", (code) => {
50
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
51
+ });
52
+ if (child.stdin !== null)
53
+ child.stdin.end(options.input === undefined ? undefined : `${options.input}\n`);
54
+ });
55
+ }
56
+ export function conciseDiagnostic(result, maximumLength = 600) {
57
+ const raw = result.stderr.trim() || result.stdout.trim();
58
+ const plain = stripVTControlCharacters(raw).replaceAll(/\s+/gu, " ").trim();
59
+ if (plain === "")
60
+ return `process exited with status ${result.exitCode}`;
61
+ if (plain.length <= maximumLength)
62
+ return plain;
63
+ return `${plain.slice(0, Math.max(0, maximumLength - 3))}...`;
64
+ }
65
+ export function commandFailureMessage(operation, result) {
66
+ return `${operation} failed: ${conciseDiagnostic(result)}`;
67
+ }
68
+ export function conciseError(error, maximumLength = 600) {
69
+ const raw = error instanceof Error ? error.message : "Unknown error";
70
+ const singleLine = stripVTControlCharacters(raw).replaceAll(/\s+/gu, " ").trim();
71
+ if (singleLine.length <= maximumLength)
72
+ return singleLine;
73
+ return `${singleLine.slice(0, Math.max(0, maximumLength - 3))}...`;
74
+ }
@@ -1,10 +1,10 @@
1
- import { spawn } from "node:child_process";
2
1
  import { readFile, unlink, writeFile } from "node:fs/promises";
3
2
  import process from "node:process";
4
3
  import { createInterface } from "node:readline/promises";
5
4
  import { pathToFileURL } from "node:url";
6
5
  import { removePackagedDeploymentRecord, uninstallRuntime } from "./lifecycle-runtime.js";
7
6
  import { bindingProperty, configValue, deploymentListIndicatesExisting } from "./setup.js";
7
+ import { commandFailureMessage, conciseError, runCommand as executeCommand } from "./subprocess.js";
8
8
  const CONFIG_PATH = uninstallRuntime.config;
9
9
  const STATE_PATH = uninstallRuntime.installProgress;
10
10
  const UNINSTALL_STATE_PATH = uninstallRuntime.uninstallProgress;
@@ -54,30 +54,9 @@ export function clientRemovalInstructions(connectorName = "wikimemory") {
54
54
  return `Client registrations are not removed automatically. Disconnect each configured client separately:\n\n codex mcp logout ${connectorName}\n codex mcp remove ${connectorName}\n\n claude mcp logout ${connectorName}\n claude mcp remove --scope user ${connectorName}\n\nFor Claude web or mobile, remove the custom connector from Settings > Connectors.`;
55
55
  }
56
56
  async function run(args, allowFailure = false) {
57
- console.log(`\n> npx ${args.join(" ")}`);
58
- const result = await new Promise((resolve, reject) => {
59
- const child = spawn("npx", args, { stdio: ["ignore", "pipe", "pipe"] });
60
- let stdout = "";
61
- let stderr = "";
62
- child.stdout.setEncoding("utf8");
63
- child.stderr.setEncoding("utf8");
64
- child.stdout.on("data", (chunk) => {
65
- stdout += chunk;
66
- process.stdout.write(chunk);
67
- });
68
- child.stderr.on("data", (chunk) => {
69
- stderr += chunk;
70
- process.stderr.write(chunk);
71
- });
72
- child.on("error", (error) => {
73
- reject(error);
74
- });
75
- child.on("close", (code) => {
76
- resolve({ stdout, stderr, exitCode: code ?? -1 });
77
- });
78
- });
57
+ const result = await executeCommand("npx", args);
79
58
  if (result.exitCode !== 0 && !allowFailure)
80
- throw new Error(`npx exited with status ${result.exitCode}`);
59
+ throw new Error(commandFailureMessage("Cloudflare resource deletion", result));
81
60
  return result;
82
61
  }
83
62
  async function requireExactName(workerName, supplied) {
@@ -145,6 +124,7 @@ export async function runUninstall(args, runCommand = run) {
145
124
  const progress = await loadProgress(targets);
146
125
  await saveProgress(progress);
147
126
  if (!progress.workerDeleted) {
127
+ console.log(" Removing Worker…");
148
128
  const probe = await runCommand([
149
129
  "wrangler",
150
130
  "deployments",
@@ -172,6 +152,7 @@ export async function runUninstall(args, runCommand = run) {
172
152
  await saveProgress(progress);
173
153
  }
174
154
  if (!progress.kvDeleted) {
155
+ console.log(" Removing OAuth state…");
175
156
  await runCommand([
176
157
  "wrangler",
177
158
  "kv",
@@ -187,6 +168,7 @@ export async function runUninstall(args, runCommand = run) {
187
168
  await saveProgress(progress);
188
169
  }
189
170
  if (!progress.databaseDeleted) {
171
+ console.log(" Removing database…");
190
172
  await runCommand([
191
173
  "wrangler",
192
174
  "d1",
@@ -214,7 +196,7 @@ export async function runUninstall(args, runCommand = run) {
214
196
  const entrypoint = process.argv[1];
215
197
  if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
216
198
  runUninstall(process.argv.slice(2)).catch((error) => {
217
- console.error(`\nUninstall failed: ${error instanceof Error ? error.message : "Unknown error"}`);
199
+ console.error(`\nUninstall failed: ${conciseError(error)}`);
218
200
  process.exitCode = 1;
219
201
  });
220
202
  }
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises";
7
7
  import { z } from "zod";
8
8
  import { deploymentRecordPath, readDeploymentRecord, writeDeploymentRecord } from "./deployment-record.js";
9
9
  import { packageRoot } from "./package-root.js";
10
+ import { commandFailureMessage, runCommand } from "./subprocess.js";
10
11
  const PACKAGE_ROOT = packageRoot();
11
12
  const DEPLOYMENT_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/u;
12
13
  const MIGRATION_NAME = /^\d{4}_[a-z0-9_]+\.sql$/u;
@@ -79,6 +80,10 @@ export function compareSemanticVersions(left, right) {
79
80
  }
80
81
  return 0;
81
82
  }
83
+ export function deploymentIsCurrent(currentVersion, manifest, pendingMigrations) {
84
+ return (compareSemanticVersions(currentVersion, manifest.version) === 0 &&
85
+ pendingMigrations.length === 0);
86
+ }
82
87
  export function productionUpgradeConfig(record, packageRoot) {
83
88
  return `${JSON.stringify({
84
89
  $schema: join(packageRoot, "node_modules", "wrangler", "config-schema.json"),
@@ -130,28 +135,14 @@ function migrationBundle(sql, migration) {
130
135
  return `${sql.trimEnd()}\n\nINSERT INTO d1_migrations(name) VALUES ('${name}');\n`;
131
136
  }
132
137
  async function command(commandName, args) {
133
- const { spawn } = await import("node:child_process");
134
- return await new Promise((resolve, reject) => {
135
- const child = spawn(commandName, args, { stdio: ["ignore", "pipe", "pipe"] });
136
- let stdout = "";
137
- let stderr = "";
138
- child.stdout.setEncoding("utf8");
139
- child.stderr.setEncoding("utf8");
140
- child.stdout.on("data", (chunk) => {
141
- stdout += chunk;
142
- process.stdout.write(chunk);
143
- });
144
- child.stderr.on("data", (chunk) => {
145
- stderr += chunk;
146
- process.stderr.write(chunk);
147
- });
148
- child.on("error", (error) => {
149
- reject(error);
150
- });
151
- child.on("close", (code) => {
152
- resolve({ stdout, stderr, exitCode: code ?? -1 });
153
- });
154
- });
138
+ return await runCommand(commandName, args);
139
+ }
140
+ export function upgradeSummary(record, currentVersion, targetVersion, databaseUpdates) {
141
+ const updates = databaseUpdates === 0 ? "none" : `${databaseUpdates} required`;
142
+ return `Upgrade Wikimemory “${record.workerName}” at ${record.origin}\n Version: ${currentVersion} -> ${targetVersion}\n Database updates: ${updates}`;
143
+ }
144
+ export function readySummary(version, alreadyCurrent = false) {
145
+ return `Wikimemory ${version} is ${alreadyCurrent ? "already " : ""}ready.\nDatabase: up to date.`;
155
146
  }
156
147
  function parsedJson(stdout) {
157
148
  return JSON.parse(stdout);
@@ -166,34 +157,64 @@ async function confirmUpgrade(summary, automatic) {
166
157
  if (answer !== "y" && answer !== "yes")
167
158
  throw new Error("Cancelled");
168
159
  }
169
- async function verifyRelease(origin, manifest) {
170
- const health = await fetch(`${origin}/health`, { redirect: "error" });
171
- const healthBody = z
172
- .object({ status: z.literal("ok"), service: z.literal("wikimemory"), version: z.string() })
173
- .parse(await health.json());
174
- if (!health.ok || healthBody.version !== manifest.version)
175
- throw new Error("Deployed Worker version verification failed");
176
- const ready = await fetch(`${origin}/ready`, { redirect: "error" });
177
- const readyBody = z
178
- .object({
179
- status: z.literal("ready"),
180
- service: z.literal("wikimemory"),
181
- version: z.string(),
182
- schemaVersion: z.string()
183
- })
184
- .parse(await ready.json());
185
- if (!ready.ok ||
186
- readyBody.version !== manifest.version ||
187
- readyBody.schemaVersion !== manifest.schemaVersion)
188
- throw new Error("Deployed schema version verification failed");
189
- const discovery = await fetch(`${origin}/.well-known/oauth-protected-resource/mcp`, {
190
- redirect: "error"
160
+ function sleep(milliseconds) {
161
+ return new Promise((resolve) => {
162
+ setTimeout(resolve, milliseconds);
191
163
  });
192
- if (!discovery.ok)
193
- throw new Error("OAuth protected-resource discovery verification failed");
194
- const app = await fetch(`${origin}/app`, { redirect: "error" });
195
- if (!app.ok || !(await app.text()).includes('<div id="root"></div>'))
196
- throw new Error("React application verification failed");
164
+ }
165
+ async function responseJson(response, label) {
166
+ const text = await response.text();
167
+ if (!response.ok)
168
+ throw new Error(`${label} returned HTTP ${response.status}: ${text.slice(0, 300)}`);
169
+ try {
170
+ return JSON.parse(text);
171
+ }
172
+ catch {
173
+ throw new Error(`${label} returned non-JSON content`);
174
+ }
175
+ }
176
+ export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper = sleep, attempts = 6) {
177
+ let finalError;
178
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
179
+ try {
180
+ const health = await fetcher(`${origin}/health`, { redirect: "error" });
181
+ const healthBody = z
182
+ .object({ status: z.literal("ok"), service: z.literal("wikimemory"), version: z.string() })
183
+ .parse(await responseJson(health, "Health endpoint"));
184
+ if (healthBody.version !== manifest.version) {
185
+ throw new Error(`Health endpoint reports version ${healthBody.version}; expected ${manifest.version}`);
186
+ }
187
+ const ready = await fetcher(`${origin}/ready`, { redirect: "error" });
188
+ const readyBody = z
189
+ .object({
190
+ status: z.literal("ready"),
191
+ service: z.literal("wikimemory"),
192
+ version: z.string(),
193
+ schemaVersion: z.string()
194
+ })
195
+ .parse(await responseJson(ready, "Readiness endpoint"));
196
+ if (readyBody.version !== manifest.version ||
197
+ readyBody.schemaVersion !== manifest.schemaVersion) {
198
+ throw new Error(`Readiness endpoint reports version ${readyBody.version} and schema ${readyBody.schemaVersion}; expected ${manifest.version} and ${manifest.schemaVersion}`);
199
+ }
200
+ const discovery = await fetcher(`${origin}/.well-known/oauth-protected-resource/mcp`, {
201
+ redirect: "error"
202
+ });
203
+ if (!discovery.ok)
204
+ throw new Error(`OAuth protected-resource discovery returned HTTP ${discovery.status}`);
205
+ const app = await fetcher(`${origin}/app`, { redirect: "error" });
206
+ if (!app.ok || !(await app.text()).includes('<div id="root"></div>'))
207
+ throw new Error("React application verification failed");
208
+ return;
209
+ }
210
+ catch (error) {
211
+ finalError = error;
212
+ if (attempt + 1 < attempts)
213
+ await sleeper(250 * 2 ** attempt);
214
+ }
215
+ }
216
+ const detail = finalError instanceof Error ? finalError.message : "Unknown verification failure";
217
+ throw new Error(`Deployed release verification failed after ${attempts} attempts: ${detail}`);
197
218
  }
198
219
  export async function runUpgrade(args) {
199
220
  const options = parseUpgradeOptions(args);
@@ -230,7 +251,7 @@ export async function runUpgrade(args) {
230
251
  command("npx", ["wrangler", "kv", "namespace", "list", ...common])
231
252
  ]);
232
253
  if ([whoami, deployments, d1, kv].some((result) => result.exitCode !== 0))
233
- throw new Error("Cloudflare target preflight failed");
254
+ throw new Error(commandFailureMessage("Cloudflare target preflight", [whoami, deployments, d1, kv].find((result) => result.exitCode !== 0) ?? whoami));
234
255
  const whoamiBody = z
235
256
  .object({ accounts: z.array(z.object({ id: z.string() })) })
236
257
  .parse(parsedJson(whoami.stdout));
@@ -258,7 +279,7 @@ export async function runUpgrade(args) {
258
279
  "SELECT name FROM d1_migrations ORDER BY id"
259
280
  ]);
260
281
  if (listed.exitCode !== 0)
261
- throw new Error("Could not read installed migrations");
282
+ throw new Error(commandFailureMessage("Database version check", listed));
262
283
  const query = z
263
284
  .array(z.object({ results: z.array(z.object({ name: z.string() })) }))
264
285
  .parse(parsedJson(listed.stdout));
@@ -271,7 +292,15 @@ export async function runUpgrade(args) {
271
292
  throw new Error("Current deployment health check failed");
272
293
  if (compareSemanticVersions(current, manifest.version) > 0)
273
294
  throw new Error(`Refusing to downgrade Wikimemory from ${current} to ${manifest.version}`);
274
- await confirmUpgrade(`\nWikimemory upgrade\n Account: ${record.accountId}\n Worker: ${record.workerName}\n D1: ${record.databaseName} (${record.databaseId})\n KV: ${record.kvName} (${record.kvId})\n Origin: ${record.origin}\n Version: ${current} -> ${manifest.version}\n Migrations: ${pending.map((item) => item.name).join(", ") || "none"}`, options.yes);
295
+ if (deploymentIsCurrent(current, manifest, pending)) {
296
+ await verifyRelease(record.origin, manifest);
297
+ await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
298
+ console.log(`\n${readySummary(manifest.version, true)}`);
299
+ return;
300
+ }
301
+ await confirmUpgrade(`\n${upgradeSummary(record, current, manifest.version, pending.length)}`, options.yes);
302
+ if (pending.length > 0)
303
+ console.log(" Updating database…");
275
304
  for (const migration of pending) {
276
305
  const bundlePath = join(temporary, migration.name);
277
306
  const sql = await readFile(join(PACKAGE_ROOT, "migrations", migration.name), "utf8");
@@ -287,14 +316,16 @@ export async function runUpgrade(args) {
287
316
  bundlePath
288
317
  ]);
289
318
  if (result.exitCode !== 0)
290
- throw new Error(`Migration failed: ${migration.name}`);
319
+ throw new Error(commandFailureMessage("Database update", result));
291
320
  }
321
+ console.log(" Deploying Wikimemory…");
292
322
  const deployed = await command("npx", ["wrangler", "deploy", "--strict", ...common]);
293
323
  if (deployed.exitCode !== 0)
294
- throw new Error("Worker deployment failed");
324
+ throw new Error(commandFailureMessage("Worker deployment", deployed));
325
+ console.log(" Verifying deployment…");
295
326
  await verifyRelease(record.origin, manifest);
296
327
  await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
297
- console.log(`\nWikimemory ${manifest.version} is ready. Schema: ${manifest.schemaVersion}`);
328
+ console.log(`\n${readySummary(manifest.version)}`);
298
329
  }
299
330
  finally {
300
331
  await rm(temporary, { recursive: true, force: true });
@@ -1,2 +1,2 @@
1
- export const WIKIMEMORY_VERSION = "0.2.3";
1
+ export const WIKIMEMORY_VERSION = "0.2.5";
2
2
  export const LATEST_SCHEMA_VERSION = "0004_credential_bound_registration_tokens.sql";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikimemory",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Personal, passkey-protected memory for Claude, Codex, and other MCP clients",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.3",
2
+ "version": "0.2.5",
3
3
  "schemaVersion": "0004_credential_bound_registration_tokens.sql",
4
4
  "migrations": [
5
5
  {
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const WIKIMEMORY_VERSION = "0.2.3";
1
+ export const WIKIMEMORY_VERSION = "0.2.5";
2
2
  export const LATEST_SCHEMA_VERSION = "0004_credential_bound_registration_tokens.sql";