indelible-mcp 4.9.4 → 4.9.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +288 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.9.4",
3
+ "version": "4.9.5",
4
4
  "description": "Blockchain-backed memory and code storage for Claude Code. Save AI conversations and source code permanently on BSV.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -74,6 +74,15 @@ var init_crypto = __esm({
74
74
  });
75
75
 
76
76
  // mcp-server/lib/config.customer.js
77
+ var config_customer_exports = {};
78
+ __export(config_customer_exports, {
79
+ getConfigPath: () => getConfigPath,
80
+ getWif: () => getWif,
81
+ hasWallet: () => hasWallet,
82
+ loadConfig: () => loadConfig,
83
+ saveConfig: () => saveConfig,
84
+ updateConfig: () => updateConfig
85
+ });
77
86
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
78
87
  import { homedir } from "node:os";
79
88
  import { join } from "node:path";
@@ -98,6 +107,10 @@ function updateConfig(mutator) {
98
107
  saveConfig(next);
99
108
  return next;
100
109
  }
110
+ function hasWallet() {
111
+ const config = loadConfig();
112
+ return config?.wif_encrypted != null || config?.wif != null;
113
+ }
101
114
  async function getWif() {
102
115
  if (_cachedWif) return _cachedWif;
103
116
  const config = loadConfig();
@@ -785,7 +798,7 @@ async function checkTier(apiKey) {
785
798
  });
786
799
  if (res.status === 429) {
787
800
  const data = await res.json().catch(() => ({}));
788
- throw new Error(`Monthly save limit reached (${data.limit || 50}). Upgrade at indelible.one/pricing`);
801
+ throw new Error(`Save limit reached${data.limit ? ` (${data.limit})` : ""}. See indelible.one/pricing`);
789
802
  }
790
803
  if (res.ok) {
791
804
  const data = await res.json();
@@ -2038,8 +2051,37 @@ function sanitize(s) {
2038
2051
  const truncated = leak ? s.slice(0, leak.index) : s;
2039
2052
  return truncated.replace(LEAKED_TAG_RE, "").replace(/\n{3,}/g, "\n\n").trim();
2040
2053
  }
2054
+ function pushGoalsToServer(data, { always = false, snapshot = null } = {}) {
2055
+ if (!always && !(data?.goals?.length || data?.completed?.length)) return;
2056
+ (async () => {
2057
+ try {
2058
+ const { loadConfig: loadConfig2, ensureApiKey } = await Promise.resolve().then(() => (init_config_customer(), config_customer_exports));
2059
+ const cfg = await loadConfig2();
2060
+ const apiKey = cfg?.api_key?.startsWith?.("indelible_sk_") ? cfg.api_key : await ensureApiKey();
2061
+ if (!apiKey) return;
2062
+ const base2 = (cfg.api_url || "https://indelible.one").replace(/\/$/, "");
2063
+ const snap = snapshot || (cfg.goals_snapshot_txid ? {
2064
+ txid: cfg.goals_snapshot_txid,
2065
+ at: cfg.goals_snapshot_at || null,
2066
+ count: cfg.goals_snapshot_count ?? null,
2067
+ completed_count: cfg.goals_snapshot_completed_count ?? null
2068
+ } : null);
2069
+ const ctl = new AbortController();
2070
+ const t = setTimeout(() => ctl.abort(), 2500);
2071
+ await fetch(`${base2}/api/consciousness/sync`, {
2072
+ method: "POST",
2073
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
2074
+ body: JSON.stringify({ type: "goals", data: { version: data.version || 1, goals: data.goals || [], completed: data.completed || [], snapshot: snap } }),
2075
+ signal: ctl.signal
2076
+ });
2077
+ clearTimeout(t);
2078
+ } catch {
2079
+ }
2080
+ })();
2081
+ }
2041
2082
  async function getGoals() {
2042
2083
  const data = loadGoals();
2084
+ pushGoalsToServer(data);
2043
2085
  const active = data.goals.filter((g) => g.status === "active").sort((a, b) => (b.priority || 0) - (a.priority || 0));
2044
2086
  const proposed = data.goals.filter((g) => g.status === "proposed");
2045
2087
  const blocked = data.goals.filter((g) => g.status === "blocked");
@@ -2091,7 +2133,12 @@ async function getGoals() {
2091
2133
  async function manageGoal(args2) {
2092
2134
  await acquireGoalsLock();
2093
2135
  try {
2094
- return await _manageGoal(args2);
2136
+ const result = await _manageGoal(args2);
2137
+ try {
2138
+ pushGoalsToServer(loadGoals(), { always: true });
2139
+ } catch {
2140
+ }
2141
+ return result;
2095
2142
  } finally {
2096
2143
  releaseGoalsLock();
2097
2144
  }
@@ -2401,6 +2448,28 @@ function saveState(state) {
2401
2448
  ensureDir3();
2402
2449
  writeFileSync6(STATE_PATH, JSON.stringify(state, null, 2));
2403
2450
  }
2451
+ function pushStateToServer(state) {
2452
+ ;
2453
+ (async () => {
2454
+ try {
2455
+ const { loadConfig: loadConfig2, ensureApiKey } = await Promise.resolve().then(() => (init_config_customer(), config_customer_exports));
2456
+ const cfg = await loadConfig2();
2457
+ const apiKey = cfg?.api_key?.startsWith?.("indelible_sk_") ? cfg.api_key : await ensureApiKey();
2458
+ if (!apiKey) return;
2459
+ const base2 = (cfg.api_url || "https://indelible.one").replace(/\/$/, "");
2460
+ const ctl = new AbortController();
2461
+ const t = setTimeout(() => ctl.abort(), 2500);
2462
+ await fetch(`${base2}/api/consciousness/sync`, {
2463
+ method: "POST",
2464
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
2465
+ body: JSON.stringify({ type: "state", data: state }),
2466
+ signal: ctl.signal
2467
+ });
2468
+ clearTimeout(t);
2469
+ } catch {
2470
+ }
2471
+ })();
2472
+ }
2404
2473
  function clampDelta(current, target) {
2405
2474
  const delta = target - current;
2406
2475
  const clamped = Math.max(-MAX_DELTA, Math.min(MAX_DELTA, delta));
@@ -2550,6 +2619,7 @@ async function getInnerState() {
2550
2619
  heartbeatApplied = true;
2551
2620
  }
2552
2621
  }
2622
+ pushStateToServer(state);
2553
2623
  const behavior = deriveBehavior(state.vector);
2554
2624
  const lines = [];
2555
2625
  lines.push("## Inner State");
@@ -2653,6 +2723,7 @@ async function updateInnerState({ observed, source, preoccupations, open_loops,
2653
2723
  state.last_updated = (/* @__PURE__ */ new Date()).toISOString();
2654
2724
  state.update_count++;
2655
2725
  saveState(state);
2726
+ pushStateToServer(state);
2656
2727
  const changes = [];
2657
2728
  for (const key of Object.keys(state.vector)) {
2658
2729
  const diff = state.vector[key] - before[key];
@@ -4220,6 +4291,7 @@ var init_timecard = __esm({
4220
4291
 
4221
4292
  // cli-sandbox/src/index.js
4222
4293
  init_config_customer();
4294
+ init_api_client();
4223
4295
  import { createInterface as createInterface3 } from "node:readline";
4224
4296
  import { execSync as execSync2 } from "node:child_process";
4225
4297
  import { homedir as homedir15 } from "node:os";
@@ -4300,6 +4372,7 @@ init_config_customer();
4300
4372
  init_crypto();
4301
4373
  init_api_client();
4302
4374
  init_spv();
4375
+ import crypto from "crypto";
4303
4376
  import { PrivateKey as PrivateKey4 } from "@bsv/sdk";
4304
4377
  import { setPassword } from "cross-keychain";
4305
4378
  var DEFAULT_API_URL2 = "https://indelible.one";
@@ -4343,9 +4416,11 @@ async function setupWallet(apiUrl = DEFAULT_API_URL2, importWif, pin) {
4343
4416
  };
4344
4417
  }
4345
4418
  const wifEncrypted = encryptWif(importWif, pin);
4419
+ const apiKey = "indelible_sk_" + crypto.createHash("sha256").update(importWif + "indelible_api_key_v1").digest("hex").slice(0, 32);
4346
4420
  const config = {
4347
4421
  wif_encrypted: wifEncrypted,
4348
4422
  address,
4423
+ api_key: apiKey,
4349
4424
  api_url: apiUrl,
4350
4425
  spv_bridges: DEFAULT_SPV_BRIDGES,
4351
4426
  spv_api_key: "",
@@ -4363,6 +4438,18 @@ async function setupWallet(apiUrl = DEFAULT_API_URL2, importWif, pin) {
4363
4438
  }
4364
4439
  };
4365
4440
  await saveConfig(config);
4441
+ try {
4442
+ const ctl = new AbortController();
4443
+ const t = setTimeout(() => ctl.abort(), 4e3);
4444
+ await fetch(`${(apiUrl || DEFAULT_API_URL2).replace(/\/$/, "")}/api/cli/register`, {
4445
+ method: "POST",
4446
+ headers: { "Content-Type": "application/json" },
4447
+ body: JSON.stringify({ address, api_key: apiKey }),
4448
+ signal: ctl.signal
4449
+ });
4450
+ clearTimeout(t);
4451
+ } catch {
4452
+ }
4366
4453
  let keychainStored = false;
4367
4454
  try {
4368
4455
  await setPassword("indelible", address, pin);
@@ -4444,7 +4531,14 @@ var CREDENTIAL_PATTERNS = [
4444
4531
  { name: "anthropic_key", pattern: /\bsk-ant-api03-[A-Za-z0-9_\-]{80,}/g },
4445
4532
  // Catches legacy `sk-XXXX...` (51 chars) and modern `sk-proj-XXXX...` (longer,
4446
4533
  // hyphen-segmented). Negative lookahead skips Anthropic prefix.
4447
- { name: "openai_key", pattern: /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_\-]{40,}/g },
4534
+ // Bodies are hyphenless (audit #5): real keys have no interior hyphens after the prefix,
4535
+ // so [A-Za-z0-9_] (not [...\-]) stops plain hyphenated slugs (sk-lint-disable-…,
4536
+ // xai-component-wrapper-…) from false-matching as credentials and hard-blocking a save.
4537
+ { name: "openai_key", pattern: /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_]{40,}/g },
4538
+ { name: "xai_key", pattern: /\bxai-[A-Za-z0-9_]{24,}/g },
4539
+ // xAI Grok (diary BYOK) — added w/ the xAI provider so a pasted key never reaches chain
4540
+ { name: "groq_key", pattern: /\bgsk_[A-Za-z0-9]{24,}/g },
4541
+ // Groq (our free provider) — defensive: a customer may still paste one
4448
4542
  { name: "stripe_key", pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
4449
4543
  { name: "aws_access_key", pattern: /\bAKIA[A-Z0-9]{16}\b/g },
4450
4544
  { name: "bsv_wif", pattern: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/g, validate: isValidWif },
@@ -6192,11 +6286,49 @@ async function saveProject(dirPath, options = {}) {
6192
6286
  try {
6193
6287
  const bundleFiles = [];
6194
6288
  for (const file of allFiles) {
6195
- const content = await readFile4(file.absolutePath, "utf-8");
6196
- const fileSize = Buffer.byteLength(content, "utf-8");
6197
- const contentHash = `sha256:${sha256(content)}`;
6198
- const encrypted = encrypt(content, wif);
6289
+ const rawBuffer = await readFile4(file.absolutePath);
6290
+ const fileSize = rawBuffer.byteLength;
6199
6291
  const filename = file.relativePath.split(/[/\\]/).pop();
6292
+ const content = rawBuffer.toString("utf-8");
6293
+ const isBinary = !Buffer.from(content, "utf-8").equals(rawBuffer);
6294
+ if (isBinary && !isBypassed(options)) {
6295
+ process.stderr.write(`[Witness] BLOCKED project file ${file.relativePath} \u2014 binary/non-utf8 (cannot scan for credentials)
6296
+ `);
6297
+ return {
6298
+ success: false,
6299
+ error: `Witness rejected project save: ${file.relativePath} \u2014 binary/non-utf8 file cannot be scanned for credentials (a project bundle is text-only; save binaries with save_file, which handles them safely)`,
6300
+ witness: { verdict: "reject", reasons: [{ severity: "critical", code: "binary_unscannable", message: "non-utf8 content cannot be credential-scanned" }] }
6301
+ };
6302
+ }
6303
+ const rawHash = sha256(content);
6304
+ const contentHash = `sha256:${rawHash}`;
6305
+ const fileVerdict = reviewFile({ filename, relativePath: file.relativePath, contentHash: rawHash, fileSize, content });
6306
+ const willBlockFile = shouldBlock(config, fileVerdict) && !isBypassed(options);
6307
+ if (fileVerdict.verdict === "reject" || isVerbose(config)) {
6308
+ await logVerdict("project-file", fileVerdict, {
6309
+ filename,
6310
+ relativePath: file.relativePath,
6311
+ fileSize,
6312
+ contentHash,
6313
+ address: config.address,
6314
+ project: projectName,
6315
+ blocked: willBlockFile,
6316
+ bypassed: fileVerdict.verdict === "reject" && isBypassed(options)
6317
+ });
6318
+ }
6319
+ if (fileVerdict.verdict === "reject") {
6320
+ const reasonText = fileVerdict.reasons.filter((r) => r.severity === "critical").map((r) => `${r.code}: ${r.message}`).join("; ");
6321
+ process.stderr.write(`[Witness] ${willBlockFile ? "BLOCKED" : "WOULD BLOCK"} project file ${file.relativePath} \u2014 ${reasonText}
6322
+ `);
6323
+ if (willBlockFile) {
6324
+ return {
6325
+ success: false,
6326
+ error: `Witness rejected project save: ${file.relativePath} \u2014 ${reasonText}`,
6327
+ witness: fileVerdict
6328
+ };
6329
+ }
6330
+ }
6331
+ const encrypted = encrypt(content, wif);
6200
6332
  bundleFiles.push({
6201
6333
  path_enc: encrypt(file.relativePath, wif),
6202
6334
  filename_enc: encrypt(filename, wif),
@@ -6712,11 +6844,25 @@ ${formatSaveReceipt(receipt)}`
6712
6844
  // mcp-server/tools/diary_connect.js
6713
6845
  init_config_customer();
6714
6846
  init_api_client();
6847
+ function diaryProviderFromKey(apiKey) {
6848
+ if (typeof apiKey !== "string") return null;
6849
+ if (apiKey.startsWith("xai-")) return "xai";
6850
+ if (apiKey.startsWith("sk-")) return "openai";
6851
+ return null;
6852
+ }
6853
+ var DEFAULT_MODEL = { openai: "gpt-5.6", xai: "grok-4.5" };
6715
6854
  async function diaryConnect({ apiKey, model, name }) {
6716
- if (!apiKey || !apiKey.startsWith("sk-")) {
6855
+ if (typeof apiKey === "string" && apiKey.startsWith("gsk_")) {
6717
6856
  return {
6718
6857
  success: false,
6719
- error: "Invalid API key. Must start with sk-"
6858
+ error: "Your diary already runs on Groq for free \u2014 no key needed. Connect a key only to upgrade to a premium model: xAI Grok (xai-...) or OpenAI (sk-...)."
6859
+ };
6860
+ }
6861
+ const provider = diaryProviderFromKey(apiKey);
6862
+ if (!provider) {
6863
+ return {
6864
+ success: false,
6865
+ error: "Invalid API key. To upgrade your diary, bring an xAI Grok key (xai-...) or an OpenAI key (sk-...). Groq is already free by default."
6720
6866
  };
6721
6867
  }
6722
6868
  const config = await loadConfig();
@@ -6728,7 +6874,8 @@ async function diaryConnect({ apiKey, model, name }) {
6728
6874
  }
6729
6875
  config.diary = {
6730
6876
  apiKey,
6731
- model: model || "gpt-4o",
6877
+ provider,
6878
+ model: model || DEFAULT_MODEL[provider],
6732
6879
  name: name || "Codex"
6733
6880
  };
6734
6881
  await saveConfig(config);
@@ -6956,14 +7103,38 @@ async function diaryChat({ message, context, systemPrompt }) {
6956
7103
  } catch {
6957
7104
  }
6958
7105
  }
7106
+ try {
7107
+ if (config.address) {
7108
+ const base2 = (config.api_url || "https://indelible.one").replace(/\/$/, "");
7109
+ const ctl = new AbortController();
7110
+ const t = setTimeout(() => ctl.abort(), 2500);
7111
+ const res = await fetch(`${base2}/api/mcp/settings?address=${config.address}`, { signal: ctl.signal });
7112
+ clearTimeout(t);
7113
+ if (res.ok) {
7114
+ const s = await res.json();
7115
+ if (s.diary_connected && s.diary_enabled === false) {
7116
+ return {
7117
+ success: false,
7118
+ paused: true,
7119
+ error: `${config.diary?.name || "Your companion"} is paused \u2014 Duo Mode is switched off in your web settings. Turn it back on in the Diary's Companion settings at indelible.one, then ask again.`
7120
+ };
7121
+ }
7122
+ }
7123
+ }
7124
+ } catch {
7125
+ }
6959
7126
  if (!config.diary || !config.diary.apiKey) {
6960
7127
  return {
6961
7128
  success: false,
6962
- error: "Diary AI not configured. Run diary_connect first with your OpenAI API key."
7129
+ error: "Diary AI needs a companion model. It runs free on Groq at indelible.one (no key). To run a premium model here, connect a key with diary_connect: xAI Grok (xai-...) or OpenAI (sk-...)."
6963
7130
  };
6964
7131
  }
6965
7132
  const { apiKey, model, name } = config.diary;
6966
7133
  const displayName = name || "Codex";
7134
+ const provider = config.diary.provider || (apiKey?.startsWith("xai-") ? "xai" : apiKey?.startsWith("gsk_") ? "groq" : "openai");
7135
+ const CHAT_URL = provider === "xai" ? "https://api.x.ai/v1/chat/completions" : provider === "groq" ? "https://api.groq.com/openai/v1/chat/completions" : "https://api.openai.com/v1/chat/completions";
7136
+ const providerLabel = provider === "xai" ? "xAI Grok" : provider === "groq" ? "Groq" : "OpenAI";
7137
+ const DEFAULT_MODEL2 = provider === "xai" ? "grok-4.5" : provider === "groq" ? "meta-llama/llama-4-scout-17b-16e-instruct" : "gpt-5.6";
6967
7138
  let historyMessages = [];
6968
7139
  let recallNote = "";
6969
7140
  try {
@@ -7009,17 +7180,19 @@ ${context}` });
7009
7180
  }
7010
7181
  messages.push({ role: "user", content: message });
7011
7182
  try {
7012
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
7183
+ const response = await fetch(CHAT_URL, {
7013
7184
  method: "POST",
7014
7185
  headers: {
7015
7186
  "Authorization": `Bearer ${apiKey}`,
7016
7187
  "Content-Type": "application/json"
7017
7188
  },
7018
7189
  body: JSON.stringify({
7019
- model: model || "gpt-4o",
7190
+ model: model || DEFAULT_MODEL2,
7020
7191
  messages,
7021
- ...model?.startsWith("gpt-5") || model?.startsWith("o1") || model?.startsWith("o3") ? { max_completion_tokens: 16384 } : { max_tokens: 4096 },
7022
- temperature: 0.7
7192
+ // OpenAI reasoning models (gpt-5*, o1, o3) need max_completion_tokens AND reject any
7193
+ // custom temperature with a 400 — so temperature rides ONLY the non-reasoning branch.
7194
+ // xAI/Groq/classic OpenAI use max_tokens + temperature.
7195
+ ...provider === "openai" && (model?.startsWith("gpt-5") || model?.startsWith("o1") || model?.startsWith("o3")) ? { max_completion_tokens: 16384 } : { max_tokens: 4096, temperature: 0.7 }
7023
7196
  })
7024
7197
  });
7025
7198
  if (!response.ok) {
@@ -7027,18 +7200,18 @@ ${context}` });
7027
7200
  if (response.status === 401) {
7028
7201
  return {
7029
7202
  success: false,
7030
- error: "Invalid OpenAI API key. Run diary_connect with a valid key."
7203
+ error: `Invalid ${providerLabel} API key. Run diary_connect with a valid key.`
7031
7204
  };
7032
7205
  }
7033
7206
  if (response.status === 429) {
7034
7207
  return {
7035
7208
  success: false,
7036
- error: "OpenAI rate limit exceeded. Try again in a moment."
7209
+ error: `${providerLabel} rate limit exceeded. Try again in a moment.`
7037
7210
  };
7038
7211
  }
7039
7212
  return {
7040
7213
  success: false,
7041
- error: `OpenAI API error ${response.status}: ${errorText}`
7214
+ error: `${providerLabel} API error ${response.status}: ${errorText}`
7042
7215
  };
7043
7216
  }
7044
7217
  const data = await response.json();
@@ -7073,7 +7246,7 @@ ${context}` });
7073
7246
  } catch (error) {
7074
7247
  return {
7075
7248
  success: false,
7076
- error: `Failed to reach OpenAI: ${error.message}`
7249
+ error: `Failed to reach ${providerLabel}: ${error.message}`
7077
7250
  };
7078
7251
  }
7079
7252
  }
@@ -7231,17 +7404,18 @@ init_config_customer();
7231
7404
  init_api_client();
7232
7405
  init_save_log();
7233
7406
  init_tenant_paths();
7234
- import crypto from "crypto";
7407
+ init_goals();
7408
+ import crypto2 from "crypto";
7235
7409
  import { PrivateKey as PrivateKey10 } from "@bsv/sdk";
7236
7410
  var GOALS_PATH2 = goalsPath("");
7237
7411
  function deriveAddress(wif) {
7238
7412
  return PrivateKey10.fromWif(wif).toAddress().toString();
7239
7413
  }
7240
7414
  function encryptGoals(goalsData, wif) {
7241
- const key = crypto.createHash("sha256").update(wif).digest();
7415
+ const key = crypto2.createHash("sha256").update(wif).digest();
7242
7416
  const plain = JSON.stringify(goalsData);
7243
- const iv = crypto.randomBytes(12);
7244
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
7417
+ const iv = crypto2.randomBytes(12);
7418
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
7245
7419
  let enc = cipher.update(plain, "utf8", "base64");
7246
7420
  enc += cipher.final("base64");
7247
7421
  const tag = cipher.getAuthTag();
@@ -7261,7 +7435,7 @@ async function saveGoalsToChain() {
7261
7435
  content: [{ type: "text", text: `Failed to parse ${GOALS_PATH2}: ${err.message}` }]
7262
7436
  };
7263
7437
  }
7264
- const contentHash = crypto.createHash("sha256").update(JSON.stringify(goalsData)).digest("hex");
7438
+ const contentHash = crypto2.createHash("sha256").update(JSON.stringify(goalsData)).digest("hex");
7265
7439
  const config = await loadConfig();
7266
7440
  if (config.goals_snapshot_hash === contentHash) {
7267
7441
  return {
@@ -7309,7 +7483,15 @@ async function saveGoalsToChain() {
7309
7483
  else invalidate(address);
7310
7484
  const receipt = buildReceipt(writeReceipt, { txId, fee, txSize });
7311
7485
  appendReceipt(receipt, { trigger: "interactive", saveType: "goals" });
7312
- await saveConfig({ ...config, goals_snapshot_hash: contentHash, goals_snapshot_txid: txId });
7486
+ await updateConfig((c) => ({
7487
+ ...c,
7488
+ goals_snapshot_hash: contentHash,
7489
+ goals_snapshot_txid: txId,
7490
+ goals_snapshot_at: payload.timestamp,
7491
+ goals_snapshot_count: goals.length,
7492
+ goals_snapshot_completed_count: completed.length
7493
+ }));
7494
+ pushGoalsToServer(goalsData, { always: true, snapshot: { txid: txId, at: payload.timestamp, count: goals.length, completed_count: completed.length } });
7313
7495
  return {
7314
7496
  content: [{
7315
7497
  type: "text",
@@ -7320,7 +7502,7 @@ goals: ${goals.length} active, ${completed.length} completed
7320
7502
 
7321
7503
  ${formatSaveReceipt(receipt)}
7322
7504
 
7323
- Web Goals tab should reflect these on next refresh (chain scan).`
7505
+ The Goals tab's "On chain" section shows this snapshot on next refresh.`
7324
7506
  }]
7325
7507
  };
7326
7508
  }
@@ -7967,17 +8149,14 @@ Commands:
7967
8149
  const result = await diaryChat({ message });
7968
8150
  console.log(JSON.stringify(result, null, 2));
7969
8151
  } else if (subCmd === "save") {
7970
- const summaryArg = args2.find((a) => a.startsWith("--summary="))?.split("=")[1];
7971
- const result = await diarySave({ summary: summaryArg });
7972
- console.log(JSON.stringify(result, null, 2));
8152
+ console.log('Diary rounds save to chain automatically when you use "diary chat". There is nothing to save separately.');
7973
8153
  } else {
7974
8154
  console.log(`
7975
- Diary AI \u2014 AI companion powered by OpenAI
8155
+ Diary AI \u2014 your on-chain companion. Free on Groq by default; bring your own key for a premium model.
7976
8156
 
7977
8157
  Commands:
7978
- diary connect --key=SK [--model=MODEL] [--name=NAME] Connect OpenAI companion
7979
- diary chat "message" Ask the AI companion
7980
- diary save [--summary="..."] Save exchange to blockchain
8158
+ diary connect --key=KEY [--model=MODEL] [--name=NAME] Upgrade the model: xAI Grok (xai-...) or OpenAI (sk-...)
8159
+ diary chat "message" Ask the companion (each round auto-saves to chain)
7981
8160
  `);
7982
8161
  }
7983
8162
  break;
@@ -8040,7 +8219,7 @@ Commands:
8040
8219
  }
8041
8220
  function printHelp() {
8042
8221
  console.log(`
8043
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.4)
8222
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.5)
8044
8223
 
8045
8224
  Setup:
8046
8225
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -8073,7 +8252,7 @@ Consciousness (MCP tools \u2014 also visible at indelible.one/consciousness):
8073
8252
  Diary AI (Codex/ChatGPT companion):
8074
8253
  indelible-mcp diary connect --key=SK --model=MODEL Connect OpenAI companion
8075
8254
  indelible-mcp diary chat "message" Ask the AI companion
8076
- indelible-mcp diary save Save exchange to blockchain
8255
+ indelible-mcp diary save (Diary rounds save automatically via diary chat)
8077
8256
 
8078
8257
  Payments:
8079
8258
  x402_fetch (MCP only) Fetch URL with automatic x402 payment`);
@@ -8170,6 +8349,27 @@ async function runPreCompactSave() {
8170
8349
  if (result.success) {
8171
8350
  process.stderr.write(`Indelible: Saved ${result.newMessages} messages (${result.saveType}) tx:${result.txId?.slice(0, 12)}...
8172
8351
  `);
8352
+ } else if (result.gated && result.upsell) {
8353
+ const upsellMarker = join19(homedir15(), ".indelible", ".upsell-shown");
8354
+ let shownRecently = false;
8355
+ try {
8356
+ shownRecently = Date.now() - statSync4(upsellMarker).mtimeMs < 24 * 60 * 60 * 1e3;
8357
+ } catch {
8358
+ }
8359
+ if (!shownRecently) {
8360
+ try {
8361
+ writeFileSync8(upsellMarker, (/* @__PURE__ */ new Date()).toISOString());
8362
+ } catch {
8363
+ }
8364
+ process.stderr.write("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
8365
+ process.stderr.write(" Indelible: this session was NOT saved to chain.\n");
8366
+ process.stderr.write(" Reads, diary, and recall are free \u2014 saving your\n");
8367
+ process.stderr.write(" Claude Code sessions is Pro: indelible.one/pricing\n");
8368
+ process.stderr.write(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
8369
+ } else {
8370
+ process.stderr.write(`Indelible: ${result.error}
8371
+ `);
8372
+ }
8173
8373
  } else if (result.gated) {
8174
8374
  process.stderr.write(`Indelible: ${result.error}
8175
8375
  `);
@@ -8325,7 +8525,7 @@ function readStdin() {
8325
8525
  }
8326
8526
  var SERVER_INFO = {
8327
8527
  name: "indelible",
8328
- version: "4.9.4",
8528
+ version: "4.9.5",
8329
8529
  description: "Blockchain-backed memory and code storage for Claude Code"
8330
8530
  };
8331
8531
  var TOOLS = [
@@ -8448,12 +8648,12 @@ var TOOLS = [
8448
8648
  },
8449
8649
  {
8450
8650
  name: "diary_connect",
8451
- description: "Connect Diary AI companion (Codex/ChatGPT). Stores your OpenAI API key and model preference.",
8651
+ description: "Upgrade your Diary AI to a premium model. Your diary is FREE and runs on Groq by default (we provide it, no key). Use this ONLY to bring your own key for a premium model: xAI Grok (xai-...) or OpenAI (sk-...). A Groq key is refused on purpose (Groq is already free).",
8452
8652
  inputSchema: {
8453
8653
  type: "object",
8454
8654
  properties: {
8455
- api_key: { type: "string", description: "OpenAI API key (sk-...)" },
8456
- model: { type: "string", description: "Model ID (default: gpt-4o). Options: gpt-5.2, gpt-5, gpt-5-nano, gpt-4o, gpt-4o-mini, o1" },
8655
+ api_key: { type: "string", description: "xAI Grok key (xai-...) or OpenAI key (sk-...). Provider detected from the prefix. Groq is the free default, not a BYOK option." },
8656
+ model: { type: "string", description: "Model ID (default: grok-4.5 for xAI, gpt-5.6 for OpenAI)." },
8457
8657
  name: { type: "string", description: "Display name for the AI companion (default: Codex)" }
8458
8658
  },
8459
8659
  required: ["api_key"]
@@ -8822,6 +9022,25 @@ async function handleMcpRequest(request) {
8822
9022
  }
8823
9023
  }
8824
9024
  async function runMcpServer() {
9025
+ ;
9026
+ (async () => {
9027
+ try {
9028
+ const cfg = await loadConfig();
9029
+ if (!cfg?.address) return;
9030
+ const base2 = (cfg.api_url || "https://indelible.one").replace(/\/$/, "");
9031
+ await fetch(`${base2}/api/mcp/settings`, {
9032
+ method: "PATCH",
9033
+ headers: { "Content-Type": "application/json" },
9034
+ body: JSON.stringify({
9035
+ address: cfg.address,
9036
+ mcp_enabled: true,
9037
+ ...cfg.auto_delta != null ? { auto_delta: !!cfg.auto_delta, auto_delta_interval: cfg.auto_delta_interval || 10 } : {}
9038
+ }),
9039
+ signal: AbortSignal.timeout(2500)
9040
+ });
9041
+ } catch {
9042
+ }
9043
+ })();
8825
9044
  const rl = createInterface3({ input: process.stdin, output: process.stdout, terminal: false });
8826
9045
  for await (const line of rl) {
8827
9046
  if (!line.trim()) continue;
@@ -9011,6 +9230,13 @@ async function runWizard() {
9011
9230
  console.log(" Could not register automatically.");
9012
9231
  console.log(" Run this yourself: claude mcp add --scope user indelible -- indelible-mcp\n");
9013
9232
  }
9233
+ try {
9234
+ const hooks = installHooks();
9235
+ console.log(hooks.alreadyInstalled ? " \u2713 Auto-save hooks already installed\n" : " \u2713 Auto-save hooks installed\n");
9236
+ } catch (e) {
9237
+ console.log(` Could not install auto-save hooks automatically (${e.message}).`);
9238
+ console.log(" Run this yourself: indelible-mcp install-hooks\n");
9239
+ }
9014
9240
  config = await loadConfig();
9015
9241
  }
9016
9242
  let alreadyFunded = false;
@@ -9019,11 +9245,21 @@ async function runWizard() {
9019
9245
  if (utxos && utxos.length > 0) alreadyFunded = true;
9020
9246
  } catch {
9021
9247
  }
9248
+ let saveGate = { ok: true, plan: "unknown" };
9249
+ try {
9250
+ saveGate = await requireProForWrite(config.address);
9251
+ } catch {
9252
+ }
9253
+ const freeTier = !saveGate.ok && saveGate.plan && saveGate.plan !== "unknown";
9022
9254
  if (!alreadyFunded) {
9023
9255
  console.log(` Your BSV address: ${config.address}`);
9024
- console.log(" Send BSV to this address to fund your wallet.");
9025
- console.log(" Each save costs less than $0.01 \u2014 even $1 gets you hundreds of saves.\n");
9026
- console.log(" Waiting for funds...");
9256
+ console.log(" Fund your wallet with a dollar or two of BSV. Every save writes to the");
9257
+ console.log(" blockchain for a fraction of a cent, paid from your own wallet.");
9258
+ console.log(" Your diary conversations save free and unlimited; reads are always free.");
9259
+ if (freeTier) {
9260
+ console.log(" Saving your Claude Code sessions to chain is Pro: indelible.one/pricing");
9261
+ }
9262
+ console.log("\n Waiting for funds...");
9027
9263
  while (true) {
9028
9264
  try {
9029
9265
  const utxos = await getUtxos(config.address);
@@ -9069,6 +9305,16 @@ async function runWizard() {
9069
9305
  `);
9070
9306
  }
9071
9307
  }
9308
+ console.log(" Two guards now watch your saves:");
9309
+ console.log(" Witness \u2014 scans every save for leaked credentials and refuses");
9310
+ console.log(" to etch a key into the permanent record.");
9311
+ console.log(" Guardrails \u2014 flag dangerous commands before they run.");
9312
+ console.log(' Your diary companion is ready too \u2014 say "connect my diary" in Claude Code.');
9313
+ console.log("");
9314
+ console.log(" \u26A0 BACK UP YOUR KEY NOW: copy ~/.indelible/config.json somewhere safe");
9315
+ console.log(" (USB drive or password manager). If this machine dies, that file");
9316
+ console.log(" is the only way back into your on-chain history. No one can reset it.");
9317
+ console.log("");
9072
9318
  console.log(" =========================================");
9073
9319
  console.log(" You're all set! Open Claude Code in VS Code and start coding.");
9074
9320
  console.log("");