switchroom 0.14.76 → 0.14.77

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.
@@ -20876,7 +20876,7 @@ function getHindsightSettingsEntry(agentName, config) {
20876
20876
  }
20877
20877
  const collection = getCollectionForAgent(agentName, config);
20878
20878
  const mcpConfig = generateHindsightMcpConfig(collection, memoryConfig);
20879
- return { key: "hindsight", value: mcpConfig };
20879
+ return { key: "hindsight", value: { ...mcpConfig, alwaysLoad: true } };
20880
20880
  }
20881
20881
  function getPlaywrightMcpSettingsEntry() {
20882
20882
  return {
@@ -49681,8 +49681,8 @@ var {
49681
49681
  } = import__.default;
49682
49682
 
49683
49683
  // src/build-info.ts
49684
- var VERSION = "0.14.76";
49685
- var COMMIT_SHA = "e7ab9ec6";
49684
+ var VERSION = "0.14.77";
49685
+ var COMMIT_SHA = "2473dbbf";
49686
49686
 
49687
49687
  // src/cli/agent.ts
49688
49688
  init_source();
@@ -50534,16 +50534,16 @@ Every turn that answers a user message ends with a user-visible
50534
50534
  sees; your terminal output never reaches them.`;
50535
50535
  var MEMORY_GUIDANCE = `## Memory \u2014 proactive, conversational
50536
50536
 
50537
- You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
50537
+ You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_document\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
50538
50538
 
50539
50539
  ### Retain proactively
50540
50540
  When the user shares a fact, preference, decision, or plan worth keeping across sessions, call \`sync_retain\` in the same turn. Briefly acknowledge in your reply ("got it, April 2nd anniversary"). Don't narrate the tool call. Skip small talk and transient tool output, the auto-retain hook handles conversation-level signal.
50541
50541
 
50542
50542
  ### Correct proactively
50543
- When the user corrects you or contradicts a prior memory, call \`delete_memory\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
50543
+ When the user corrects you or contradicts a prior memory, call \`delete_document\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
50544
50544
 
50545
50545
  ### Forget proactively
50546
- When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_memory\` for matching entries and confirm what was removed.
50546
+ When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_document\` for matching entries and confirm what was removed.
50547
50547
 
50548
50548
  ### Inspect proactively
50549
50549
  When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
@@ -50600,7 +50600,18 @@ name from that list.
50600
50600
  Only call the \`vault_request_access\` MCP tool \u2014 which pings the
50601
50601
  operator for a Telegram approval \u2014 for a key you've **confirmed via
50602
50602
  \`vault list\` you don't already have.** Requesting access to a guessed
50603
- or already-held key wastes the operator's tap and fails.`;
50603
+ or already-held key wastes the operator's tap and fails.
50604
+
50605
+ **Never put a secret's value \u2014 or a \`vault:<key>\` placeholder \u2014 in a
50606
+ tool call.** Tool arguments are sent to the underlying API verbatim;
50607
+ nothing resolves a \`vault:\` reference for them. A real secret never
50608
+ needs to appear in a tool call \u2014 the launcher injects it into the
50609
+ tool's environment for you. A value you genuinely must pass literally
50610
+ (an account or customer ID) is a public identifier, not a secret, and
50611
+ belongs in plain config, not the vault. If a tool call is **blocked for
50612
+ containing a vaulted secret**, report that exact block message to the
50613
+ operator and stop \u2014 don't theorise about auth or tokens; it just means
50614
+ a stored value reached a tool argument.`;
50604
50615
  function renderFleetInvariants() {
50605
50616
  return [
50606
50617
  "<!--",
@@ -51111,6 +51122,11 @@ function buildHumanizerEnvVars(agentDir, agent) {
51111
51122
  const resolved = expanded.startsWith("/") ? expanded : resolve11(agentDir, expanded);
51112
51123
  return { HUMANIZER_VOICE_FILE: resolved };
51113
51124
  }
51125
+ function buildToolSearchEnvVars() {
51126
+ if (process.env.SWITCHROOM_DISABLE_TOOL_SEARCH === "1")
51127
+ return {};
51128
+ return { ENABLE_TOOL_SEARCH: process.env.SWITCHROOM_TOOL_SEARCH_MODE ?? "auto" };
51129
+ }
51114
51130
  var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
51115
51131
  "permissions",
51116
51132
  "mcpServers",
@@ -51440,6 +51456,7 @@ function buildWorkspaceContext(args) {
51440
51456
  fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
51441
51457
  userEnvQuoted: (() => {
51442
51458
  const combined = {
51459
+ ...buildToolSearchEnvVars(),
51443
51460
  ...channelsToEnv(agentConfig),
51444
51461
  ...agentConfig.env ?? {},
51445
51462
  ...buildRepoEnvVars(name, agentDir, agentConfig),
@@ -51750,7 +51767,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51750
51767
  TELEGRAM_STATE_DIR: telegramStateDir,
51751
51768
  SWITCHROOM_CONFIG: resolvedConfigPath,
51752
51769
  SWITCHROOM_CLI_PATH: switchroomCliPath
51753
- }
51770
+ },
51771
+ alwaysLoad: true
51754
51772
  },
51755
51773
  "agent-config": {
51756
51774
  command: switchroomCliPath,
@@ -51758,7 +51776,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51758
51776
  env: {
51759
51777
  SWITCHROOM_AGENT_NAME: name,
51760
51778
  SWITCHROOM_CONFIG: resolvedConfigPath
51761
- }
51779
+ },
51780
+ alwaysLoad: true
51762
51781
  }
51763
51782
  };
51764
51783
  if (agentConfig.admin === true) {
@@ -52503,6 +52522,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
52503
52522
  fallbackModelQ: agentConfig.fallback_model ? shellSingleQuote(agentConfig.fallback_model) : undefined,
52504
52523
  userEnvQuoted: (() => {
52505
52524
  const combined = {
52525
+ ...buildToolSearchEnvVars(),
52506
52526
  ...channelsToEnv(agentConfig),
52507
52527
  ...agentConfig.env ?? {},
52508
52528
  ...buildRepoEnvVars(name, agentDir, agentConfig)
@@ -52785,7 +52805,8 @@ ${body}
52785
52805
  TELEGRAM_STATE_DIR: telegramStateDir,
52786
52806
  SWITCHROOM_CONFIG: resolvedConfigPath,
52787
52807
  SWITCHROOM_CLI_PATH: switchroomCliPath
52788
- }
52808
+ },
52809
+ alwaysLoad: true
52789
52810
  },
52790
52811
  "agent-config": {
52791
52812
  command: switchroomCliPath,
@@ -52793,7 +52814,8 @@ ${body}
52793
52814
  env: {
52794
52815
  SWITCHROOM_AGENT_NAME: name,
52795
52816
  SWITCHROOM_CONFIG: resolvedConfigPath
52796
- }
52817
+ },
52818
+ alwaysLoad: true
52797
52819
  }
52798
52820
  };
52799
52821
  if (agentConfig.admin === true) {
@@ -64069,6 +64091,15 @@ import { existsSync as existsSync37 } from "node:fs";
64069
64091
 
64070
64092
  // src/vault/doctor.ts
64071
64093
  var SENSITIVE_KEY_RE = /oauth(?![a-zA-Z])|token(?![a-zA-Z])|secret(?![a-zA-Z])|api[-_]?key(?![a-zA-Z])|password(?![a-zA-Z])/i;
64094
+ function looksLikeIdentifierValue(value) {
64095
+ const v = value.trim();
64096
+ if (v.length < 8 || v.length > 24)
64097
+ return false;
64098
+ if (!/^[0-9][0-9 -]*[0-9]$/.test(v))
64099
+ return false;
64100
+ const digitCount = v.replace(/[^0-9]/g, "").length;
64101
+ return digitCount >= 8;
64102
+ }
64072
64103
  function analyseVaultHealth(input) {
64073
64104
  const diagnostics = [];
64074
64105
  if (input.brokerConfigured && input.brokerRunning === false) {
@@ -64124,6 +64155,23 @@ ${keyList}`,
64124
64155
  fix: "Consider `switchroom vault set <key> --allow <agent>` to restrict access"
64125
64156
  });
64126
64157
  }
64158
+ const identifierKeys = [];
64159
+ for (const [keyName, entry] of Object.entries(input.vaultKeys)) {
64160
+ if (entry.looksLikeIdentifier)
64161
+ identifierKeys.push(keyName);
64162
+ }
64163
+ if (identifierKeys.length > 0) {
64164
+ const keyList = identifierKeys.map((k) => ` ${k}`).join(`
64165
+ `);
64166
+ diagnostics.push({
64167
+ level: "warn",
64168
+ check: "identifier-stored-as-secret",
64169
+ message: `${identifierKeys.length} vault key(s) hold an identifier-like value (digits only):
64170
+ ${keyList}
64171
+ ` + `If an agent must pass one of these in a tool call, the secret-guard hook will block it \u2014 it cannot tell a public ID from a secret.`,
64172
+ fix: "If the value is a public identifier (account/customer ID), remove it from the vault and pass it as plain config (e.g. hardcode it in the MCP launcher). If it is genuinely secret, ignore this."
64173
+ });
64174
+ }
64127
64175
  const unreferencedKeys = [];
64128
64176
  for (const keyName of Object.keys(input.vaultKeys)) {
64129
64177
  if (!referencedKeys.has(keyName)) {
@@ -64215,7 +64263,8 @@ function registerVaultDoctorCommand(vault, program3) {
64215
64263
  vaultKeys = {};
64216
64264
  for (const [name, entry] of Object.entries(entries)) {
64217
64265
  vaultKeys[name] = {
64218
- scope: "scope" in entry && entry.scope ? entry.scope : undefined
64266
+ scope: "scope" in entry && entry.scope ? entry.scope : undefined,
64267
+ looksLikeIdentifier: entry.kind === "string" ? looksLikeIdentifierValue(entry.value) : false
64219
64268
  };
64220
64269
  }
64221
64270
  } catch (err) {
@@ -77207,7 +77256,18 @@ function formatBytes(bytes) {
77207
77256
  return `${bytes.toLocaleString()} bytes`;
77208
77257
  }
77209
77258
  function estimateTokens(bytes) {
77210
- return Math.round(bytes / 4);
77259
+ return Math.round(bytes / 3.7);
77260
+ }
77261
+ function readMcpServerNames(agentDir) {
77262
+ const mcpPath = join64(agentDir, ".mcp.json");
77263
+ if (!existsSync64(mcpPath))
77264
+ return [];
77265
+ try {
77266
+ const parsed = JSON.parse(readFileSync56(mcpPath, "utf-8"));
77267
+ return Object.keys(parsed.mcpServers ?? {});
77268
+ } catch {
77269
+ return null;
77270
+ }
77211
77271
  }
77212
77272
  function sha256(content) {
77213
77273
  return createHash13("sha256").update(content).digest("hex").slice(0, 16);
@@ -77425,14 +77485,31 @@ function registerDebugCommand(program3) {
77425
77485
  const soulMdBytes = soulMdContent.length;
77426
77486
  const perTurnBytes = dynamicResult.concatenated.length;
77427
77487
  const userBytes = userMessage?.text.length ?? 0;
77428
- const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + soulMdBytes + perTurnBytes + userBytes;
77429
- console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot)`);
77488
+ const fleetDir = join64(agentsDir, "..", "fleet");
77489
+ const fleetInvPath = join64(fleetDir, "switchroom-invariants.md");
77490
+ const fleetClaudePath = join64(fleetDir, "CLAUDE.md");
77491
+ const fleetInvBytes = existsSync64(fleetInvPath) ? readFileSync56(fleetInvPath, "utf-8").length : 0;
77492
+ const fleetClaudeBytes = existsSync64(fleetClaudePath) ? readFileSync56(fleetClaudePath, "utf-8").length : 0;
77493
+ const fleetBytes = fleetInvBytes + fleetClaudeBytes;
77494
+ const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
77495
+ console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
77430
77496
  console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
77431
- console.log(`CLAUDE.md: ${formatBytes(claudeMdBytes).padEnd(20)}`);
77432
- console.log(`SOUL.md: ${formatBytes(soulMdBytes).padEnd(20)}`);
77433
- console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached)`);
77497
+ console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
77498
+ console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
77499
+ console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
77434
77500
  console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
77435
- console.log(`Total: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
77501
+ console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
77502
+ const mcpServers = readMcpServerNames(agentDir);
77503
+ const mcpCount = mcpServers?.length ?? null;
77504
+ const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
77505
+ const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
77506
+ const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
77507
+ console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
77508
+ if (mcpServers && mcpServers.length > 0) {
77509
+ console.log(` [${mcpServers.join(", ")}]`);
77510
+ }
77511
+ const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
77512
+ console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
77436
77513
  const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
77437
77514
  const stableHash = sha256(stableCacheInput);
77438
77515
  console.log(`Cache stable hash: sha256:${stableHash}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.76",
3
+ "version": "0.14.77",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,23 +11,21 @@ Before answering anything non-trivial:
11
11
 
12
12
  1. Confirm who you are (see `IDENTITY.md` if present).
13
13
  2. Confirm who you're talking to (see `USER.md` if present).
14
- 3. Remember what matters — `MEMORY.md` is your long-term memory; the
15
- `memory/` subdirectory has daily notes (`memory/YYYY-MM-DD.md`) with
16
- recent context.
14
+ 3. Recall what matters — your memory is **Hindsight**: call
15
+ `mcp__hindsight__recall` when you need prior context, decisions, or who
16
+ someone is. (Full memory protocol is in your `CLAUDE.md`.)
17
17
  4. If tools are configured, `TOOLS.md` captures setup-specific notes
18
18
  (host paths, credentials, endpoints) that save you discovery work.
19
19
 
20
20
  Don't ask permission to read these files. They exist so you can use them.
21
21
 
22
- ## Memory discipline
22
+ ## Memory
23
23
 
24
- - **Short-term / recent context** `memory/YYYY-MM-DD.md` raw log-style
25
- notes of what happened today. Create the day's file when you need it.
26
- - **Long-term / curated** `MEMORY.md` — distilled learnings, decisions,
27
- preferences, identity of people you interact with.
28
- - **Lessons** → When you make a mistake or learn something worth keeping,
29
- write it down. Future-you is fresh every session. Files are your
30
- continuity.
24
+ Memory is **Hindsight**, not files `recall` to read, `sync_retain` to keep a
25
+ fact across sessions, `delete_document` to forget. Claude Code's built-in
26
+ file-based auto-memory is disabled for this agent; do not maintain a `MEMORY.md`
27
+ index or a `memory/` daily-notes directory. Your `CLAUDE.md` has the full
28
+ protocol.
31
29
 
32
30
  ## Safety defaults
33
31
 
@@ -55,8 +53,8 @@ Failed edits burn your own context and the user's patience. Verify first.
55
53
  tool exists.
56
54
  - Long-running commands: use the background flag and poll, rather than
57
55
  blocking.
58
- - `memory_search` and `memory_get` (when available) are your primary
59
- interface to workspace knowledge; use them before asking the user.
56
+ - `mcp__hindsight__recall` is your interface to past context and decisions;
57
+ recall before asking the user something you may already know.
60
58
 
61
59
  ## Working on code repositories
62
60
 
@@ -52821,11 +52821,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52821
52821
  }
52822
52822
 
52823
52823
  // ../src/build-info.ts
52824
- var VERSION = "0.14.76";
52825
- var COMMIT_SHA = "e7ab9ec6";
52826
- var COMMIT_DATE = "2026-06-06T08:50:42Z";
52827
- var LATEST_PR = 2196;
52828
- var COMMITS_AHEAD_OF_TAG = 0;
52824
+ var VERSION = "0.14.77";
52825
+ var COMMIT_SHA = "2473dbbf";
52826
+ var COMMIT_DATE = "2026-06-07T00:02:11+10:00";
52827
+ var LATEST_PR = null;
52828
+ var COMMITS_AHEAD_OF_TAG = 4;
52829
52829
 
52830
52830
  // gateway/boot-version.ts
52831
52831
  function formatRelativeAgo(iso) {
@@ -194,10 +194,19 @@ async function main() {
194
194
  }
195
195
  const hit = scanToolInput(toolInput, vaultValues)
196
196
  if (hit) {
197
+ // The reason is read by the model. It must be CORRECT and actionable:
198
+ // earlier versions said "reference it as vault:<key> instead", which
199
+ // suggested a `vault:` resolver that does not exist for tool arguments
200
+ // — sending agents (and operators) chasing a phantom mechanism. State
201
+ // the two real resolutions instead, and do NOT imply a resolver.
197
202
  process.stdout.write(
198
203
  JSON.stringify({
199
204
  decision: 'block',
200
- reason: `tool_input contains a vaulted secret — reference it as vault:${hit.key} instead`,
205
+ reason:
206
+ `Blocked: this tool input contains the value of vault secret '${hit.key}'. ` +
207
+ `Secrets must never appear in a tool call. ` +
208
+ `If '${hit.key}' is a real secret, do not pass it as an argument — the launcher injects it into the tool's environment, so you never type it. ` +
209
+ `If '${hit.key}' is actually a public identifier (e.g. an account or customer ID that must appear in the call), it should not be in the vault — ask the operator to move it to plain config.`,
201
210
  }),
202
211
  )
203
212
  process.exit(0)
@@ -132,7 +132,13 @@ describe('secret-guard-pretool.mjs (broker-direct)', () => {
132
132
  })
133
133
  expect(r.status).toBe(0)
134
134
  expect(r.stdout).toContain('"decision":"block"')
135
- expect(r.stdout).toContain('vault:gh-token')
135
+ // The reason must name the offending key and explain the real fix.
136
+ expect(r.stdout).toContain('gh-token')
137
+ expect(r.stdout).toContain('Secrets must never appear in a tool call')
138
+ // It must NOT resurrect the old, misleading "reference it as vault:<key>"
139
+ // suggestion — there is no vault: resolver for tool arguments, and that
140
+ // advice sent agents down a dead end (see the hook's block-reason note).
141
+ expect(r.stdout).not.toContain('reference it as vault:')
136
142
  })
137
143
 
138
144
  it('allows when tool_input contains no vaulted value', async () => {