switchroom 0.19.44 → 0.19.45

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.
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.19.44", COMMIT_SHA = "82d3b0b2";
2123
+ var VERSION = "0.19.45", COMMIT_SHA = "2ad33d5d";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -13572,28 +13572,7 @@ var init_zod = __esm(() => {
13572
13572
  });
13573
13573
 
13574
13574
  // src/memory/observation-scopes.ts
13575
- function isObservationScope(value) {
13576
- return typeof value === "string" && OBSERVATION_SCOPES.includes(value);
13577
- }
13578
- function observationScopesHint() {
13579
- return OBSERVATION_SCOPES.join(", ");
13580
- }
13581
- function classifyObservationScope(raw) {
13582
- if (raw === undefined)
13583
- return { kind: "unset" };
13584
- const value = raw.trim();
13585
- if (!value)
13586
- return { kind: "unset" };
13587
- if (!isObservationScope(value))
13588
- return { kind: "invalid", raw };
13589
- return { kind: "set", scope: value };
13590
- }
13591
- function resolveObservationScope(configured, env2 = process.env) {
13592
- if (configured !== undefined)
13593
- return classifyObservationScope(configured);
13594
- return classifyObservationScope(env2[OBSERVATION_SCOPES_ENV]);
13595
- }
13596
- var OBSERVATION_SCOPES, OBSERVATION_SCOPES_ENV = "HINDSIGHT_OBSERVATION_SCOPES", OBSERVATION_SCOPE_STRATEGIES;
13575
+ var OBSERVATION_SCOPES, OBSERVATION_SCOPE_STRATEGIES;
13597
13576
  var init_observation_scopes = __esm(() => {
13598
13577
  OBSERVATION_SCOPES = [
13599
13578
  "per_tag",
@@ -36321,7 +36300,7 @@ class AuthBrokerClient {
36321
36300
  sock.once("error", onError);
36322
36301
  sock.once("connect", () => {
36323
36302
  sock.removeListener("error", onError);
36324
- sock.on("data", (chunk2) => this.onData(chunk2));
36303
+ sock.on("data", (chunk) => this.onData(chunk));
36325
36304
  sock.on("error", (err) => this.onSocketError(err));
36326
36305
  sock.on("close", () => this.onSocketClose());
36327
36306
  this.socket = sock;
@@ -36335,8 +36314,8 @@ class AuthBrokerClient {
36335
36314
  this.connecting = null;
36336
36315
  }
36337
36316
  }
36338
- onData(chunk2) {
36339
- this.buffer += chunk2.toString("utf8");
36317
+ onData(chunk) {
36318
+ this.buffer += chunk.toString("utf8");
36340
36319
  let idx;
36341
36320
  while ((idx = this.buffer.indexOf(`
36342
36321
  `)) !== -1) {
@@ -73049,730 +73028,137 @@ import { execFileSync as execFileSync11 } from "node:child_process";
73049
73028
  init_atomic();
73050
73029
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync9, renameSync as renameSync7, mkdirSync as mkdirSync16, existsSync as existsSync24, statSync as statSync11, readdirSync as readdirSync10 } from "node:fs";
73051
73030
  import { join as join18 } from "node:path";
73052
-
73053
- // telegram-plugin/secret-detect/patterns.ts
73054
- var ANCHORED_PATTERNS = [
73055
- { rule_id: "anthropic_api_key", regex: /\b(sk-ant-[A-Za-z0-9_-]{8,})\b/g, captureIndex: 1, slugHint: "anthropic_api_key" },
73056
- { rule_id: "anthropic_oauth_code", regex: /(?:^|\s)([A-Za-z0-9_-]{20,}#[A-Za-z0-9_-]{20,})(?=\s|$)/gm, captureIndex: 1, slugHint: "anthropic_oauth_code" },
73057
- { rule_id: "openai_api_key", regex: /\b(sk-[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "openai_api_key" },
73058
- { rule_id: "github_pat_classic", regex: /\b(ghp_[A-Za-z0-9]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
73059
- { rule_id: "github_pat_fine_grained", regex: /\b(github_pat_[A-Za-z0-9_]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
73060
- { rule_id: "slack_token", regex: /\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_token" },
73061
- { rule_id: "slack_app_token", regex: /\b(xapp-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_app_token" },
73062
- { rule_id: "groq_api_key", regex: /\b(gsk_[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "groq_api_key" },
73063
- { rule_id: "google_api_key", regex: /\b(AIza[0-9A-Za-z\-_]{20,})\b/g, captureIndex: 1, slugHint: "google_api_key" },
73064
- { rule_id: "perplexity_api_key", regex: /\b(pplx-[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "perplexity_api_key" },
73065
- { rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
73066
- { rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
73067
- { rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
73068
- { rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
73069
- { rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
73070
- { rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
73071
- ];
73072
- var STRUCTURED_PATTERNS = [
73073
- {
73074
- rule_id: "env_key_value",
73075
- regex: /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\b\s*[=:]\s*(["']?)([^\s"'\\]+)\2/g,
73076
- captureIndex: 3,
73077
- slugHint: "env"
73078
- },
73079
- {
73080
- rule_id: "json_secret_field",
73081
- regex: /"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"/g,
73082
- captureIndex: 1,
73083
- slugHint: "json_secret"
73084
- },
73085
- {
73086
- rule_id: "cli_flag",
73087
- regex: /--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1/g,
73088
- captureIndex: 2,
73089
- slugHint: "cli_flag"
73090
- },
73091
- {
73092
- rule_id: "bearer_auth_header",
73093
- regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
73094
- captureIndex: 1,
73095
- slugHint: "bearer_token"
73096
- },
73097
- {
73098
- rule_id: "bearer_loose",
73099
- regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
73100
- captureIndex: 1,
73101
- slugHint: "bearer_token"
73102
- },
73103
- {
73104
- rule_id: "basic_auth_header",
73105
- regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
73106
- captureIndex: 1,
73107
- slugHint: "basic_auth"
73108
- },
73109
- {
73110
- rule_id: "pem_private_key",
73111
- regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
73112
- captureIndex: 0,
73113
- slugHint: "pem_private_key"
73114
- }
73115
- ];
73116
- var PROVIDER_PATTERNS = [
73117
- { rule_id: "slack_webhook", regex: /(https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_/]+)/g, captureIndex: 1, slugHint: "slack_webhook" },
73118
- { rule_id: "stripe_live_secret", regex: /\b(sk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
73119
- { rule_id: "stripe_restricted", regex: /\b(rk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
73120
- { rule_id: "sendgrid_api_key", regex: /\b(SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "sendgrid_key" },
73121
- { rule_id: "gitlab_pat", regex: /\b(glpat-[A-Za-z0-9_-]{20})\b/g, captureIndex: 1, slugHint: "gitlab_pat" },
73122
- { rule_id: "huggingface_token", regex: /\b(hf_[A-Za-z0-9]{34,})\b/g, captureIndex: 1, slugHint: "huggingface_token" },
73123
- { rule_id: "twilio_api_key", regex: /\b(SK[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "twilio_api_key" },
73124
- { rule_id: "mailgun_key", regex: /\b(key-[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "mailgun_key" },
73125
- { rule_id: "digitalocean_pat", regex: /\b(dop_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
73126
- { rule_id: "digitalocean_oauth", regex: /\b(doo_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
73127
- { rule_id: "digitalocean_refresh", regex: /\b(dor_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
73128
- { rule_id: "doppler_token", regex: /\b(dp\.(?:pt|st|ct|sa|scim|audit)\.[A-Za-z0-9]{40,44})\b/g, captureIndex: 1, slugHint: "doppler_token" },
73129
- { rule_id: "linear_api_key", regex: /\b(lin_api_[A-Za-z0-9]{40})\b/g, captureIndex: 1, slugHint: "linear_api_key" },
73130
- { rule_id: "shopify_access_token", regex: /\b(shpat_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
73131
- { rule_id: "shopify_shared_secret", regex: /\b(shpss_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
73132
- { rule_id: "shopify_private_app", regex: /\b(shppa_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
73133
- { rule_id: "square_access_token", regex: /\b(sq0atp-[A-Za-z0-9_-]{22})\b/g, captureIndex: 1, slugHint: "square_token" },
73134
- { rule_id: "square_oauth_secret", regex: /\b(sq0csp-[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "square_token" },
73135
- { rule_id: "newrelic_key", regex: /\b(NRAK-[A-Z0-9]{27})\b/g, captureIndex: 1, slugHint: "newrelic_key" },
73136
- { rule_id: "notion_token", regex: /\b(ntn_[A-Za-z0-9]{46})\b/g, captureIndex: 1, slugHint: "notion_token" },
73137
- { rule_id: "planetscale_password", regex: /\b(pscale_pw_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
73138
- { rule_id: "planetscale_token", regex: /\b(pscale_tkn_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
73139
- { rule_id: "supabase_service_key", regex: /\b(sbp_[a-f0-9]{40})\b/g, captureIndex: 1, slugHint: "supabase_key" },
73140
- { rule_id: "atlassian_token", regex: /\b(ATATT[A-Za-z0-9_\-=]{20,})\b/g, captureIndex: 1, slugHint: "atlassian_token" },
73141
- { rule_id: "dropbox_token", regex: /\b(sl\.[A-Za-z0-9_-]{130,})/g, captureIndex: 1, slugHint: "dropbox_token" },
73142
- { rule_id: "databricks_token", regex: /\b(dapi[a-f0-9]{32})\b/g, captureIndex: 1, slugHint: "databricks_token" },
73143
- { rule_id: "grafana_service_account", regex: /\b(glsa_[A-Za-z0-9]{32}_[a-fA-F0-9]{8})\b/g, captureIndex: 1, slugHint: "grafana_token" },
73144
- { rule_id: "pypi_token", regex: /\b(pypi-AgEIcHlwaS[A-Za-z0-9_-]{50,})/g, captureIndex: 1, slugHint: "pypi_token" },
73145
- { rule_id: "aws_temp_access_key", regex: /\b(ASIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
73146
- { rule_id: "gcp_oauth_token", regex: /\b(ya29\.[A-Za-z0-9_-]{30,})/g, captureIndex: 1, slugHint: "gcp_oauth_token" }
73147
- ];
73148
- var ALL_PATTERNS = [...ANCHORED_PATTERNS, ...PROVIDER_PATTERNS, ...STRUCTURED_PATTERNS];
73149
-
73150
- // telegram-plugin/secret-detect/entropy.ts
73151
- function shannonEntropy(s) {
73152
- if (s.length === 0)
73153
- return 0;
73154
- const counts = new Map;
73155
- for (const ch of s) {
73156
- counts.set(ch, (counts.get(ch) ?? 0) + 1);
73157
- }
73158
- let h = 0;
73159
- const len = s.length;
73160
- for (const c of counts.values()) {
73161
- const p = c / len;
73162
- h -= p * Math.log2(p);
73163
- }
73164
- return h;
73165
- }
73166
-
73167
- // telegram-plugin/secret-detect/inert-values.ts
73168
- var INERT_VALUE_RE = [
73169
- /^\[REDACTED(?::[A-Za-z0-9_]+)?\]$/i,
73170
- /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/,
73171
- /^\{\{[^\n\r\u2028\u2029]*\}\}$/,
73172
- /^<[^<>\n\r\u2028\u2029]*>$/,
73173
- /^%[A-Za-z_][A-Za-z0-9_]*%$/,
73174
- /^vault:[A-Za-z0-9_./-]*$/i,
73175
- /^[*x\u2022.]+$/i,
73176
- /^(?:process\.env|import\.meta\.env|os\.environ)(?:\.[A-Za-z_$][A-Za-z0-9_$]*|\[["']?[A-Za-z_$][A-Za-z0-9_$]*["']?\])*$/,
73177
- /^(?:changeme|change-me|replaceme|replace-me|placeholder|todo|tbd|yourkey|your-key|yourpassword|your-password|yoursecret|your-secret|yourtoken|your-token)(?:[-_][A-Za-z0-9-]+)?$/i,
73178
- /^[a-z]+(?: [a-z]+){2,}$/
73179
- ];
73180
- var CREDENTIAL_RUN_RE = /[A-Za-z0-9]{12,}/g;
73181
- var MIXED_CLASS_RUN_MIN = 12;
73182
- var SINGLE_CLASS_RUN_MIN = 16;
73183
- function runIsCredentialShaped(run) {
73184
- let classes = 0;
73185
- if (/[a-z]/.test(run))
73186
- classes++;
73187
- if (/[A-Z]/.test(run))
73188
- classes++;
73189
- if (/[0-9]/.test(run))
73190
- classes++;
73191
- return run.length >= (classes >= 2 ? MIXED_CLASS_RUN_MIN : SINGLE_CLASS_RUN_MIN);
73192
- }
73193
- function hasCredentialShapedRun(value) {
73194
- for (const run of value.match(CREDENTIAL_RUN_RE) ?? []) {
73195
- if (runIsCredentialShaped(run))
73196
- return true;
73031
+ var DEFAULT_MAX_TURNS = 50;
73032
+ var TOPIC_MAX_CHARS = 117;
73033
+ var TURN_TEXT_MAX_CHARS = 1200;
73034
+ function extractTurnsFromJsonl(path2, maxTurns) {
73035
+ let raw;
73036
+ try {
73037
+ raw = readFileSync20(path2, "utf-8");
73038
+ } catch {
73039
+ return [];
73197
73040
  }
73198
- return false;
73199
- }
73200
- function isInertValue(value) {
73201
- if (!INERT_VALUE_RE.some((re) => re.test(value)))
73202
- return false;
73203
- return !hasCredentialShapedRun(value);
73204
- }
73205
- var INERT_GATED_RULES = new Set([
73206
- "env_key_value",
73207
- "json_secret_field",
73208
- "cli_flag"
73209
- ]);
73210
- var TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
73211
- function stripTrailingPunctuation(value) {
73212
- return value.replace(TRAILING_PUNCT_RE, "");
73213
- }
73214
-
73215
- // telegram-plugin/secret-detect/kv-scanner.ts
73216
- var KV_RE = /\b([A-Za-z_][A-Za-z0-9_-]*(?:password|passwd|token|secret|key|api[_-]?key))\s*[:=]\s*["']?([^\s"'\\]{8,})["']?/gi;
73217
- var KV_ENTROPY_THRESHOLD = 4;
73218
- var MEMORABLE_PW_RE = /\b([A-Za-z0-9_-]*(?:password|passwd|passphrase|pwd))\b\s*(?:[:=]\s*|\s+is\s+)(["']?)([^\s"']{8,64})\2/gi;
73219
- var MEMORABLE_PW_RULE_ID = "memorable_password";
73220
- var MEMORABLE_PW_MIN_CLASSES = 2;
73221
- function charClassCount(value) {
73222
- let n = 0;
73223
- if (/[a-z]/.test(value))
73224
- n++;
73225
- if (/[A-Z]/.test(value))
73226
- n++;
73227
- if (/[0-9]/.test(value))
73228
- n++;
73229
- if (/[^A-Za-z0-9]/.test(value))
73230
- n++;
73231
- return n;
73232
- }
73233
- function looksLikeMemorablePassword(value) {
73234
- if (isInertValue(value))
73235
- return false;
73236
- const core = stripTrailingPunctuation(value);
73237
- if (core.length < 8 || core.length > 64)
73238
- return false;
73239
- if (isInertValue(core))
73240
- return false;
73241
- if (new Set(core).size < 4)
73242
- return false;
73243
- return charClassCount(core) >= MEMORABLE_PW_MIN_CLASSES;
73244
- }
73245
- function scanMemorablePasswords(text) {
73246
- const hits = [];
73247
- MEMORABLE_PW_RE.lastIndex = 0;
73248
- let m;
73249
- while ((m = MEMORABLE_PW_RE.exec(text)) !== null) {
73250
- const keyName = m[1];
73251
- const value = m[3];
73252
- if (!value || !looksLikeMemorablePassword(value))
73253
- continue;
73254
- const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
73255
- if (valueOffsetInMatch < 0)
73041
+ const turns = [];
73042
+ for (const line of raw.split(`
73043
+ `)) {
73044
+ if (!line.trim())
73256
73045
  continue;
73257
- const start = m.index + valueOffsetInMatch;
73258
- hits.push({
73259
- rule_id: MEMORABLE_PW_RULE_ID,
73260
- start,
73261
- end: start + value.length,
73262
- matched_text: value,
73263
- key_name: keyName,
73264
- confidence: "ambiguous"
73265
- });
73266
- }
73267
- return hits;
73268
- }
73269
- function scanKeyValue(text) {
73270
- const hits = [];
73271
- KV_RE.lastIndex = 0;
73272
- let m;
73273
- while ((m = KV_RE.exec(text)) !== null) {
73274
- const [, keyName, value] = m;
73275
- if (!value)
73046
+ let obj;
73047
+ try {
73048
+ obj = JSON.parse(line);
73049
+ } catch {
73276
73050
  continue;
73277
- if (isInertValue(value))
73051
+ }
73052
+ if (obj.type === "queue-operation") {
73053
+ const op = obj.operation;
73054
+ if (op !== "enqueue")
73055
+ continue;
73056
+ const content = obj.content;
73057
+ if (typeof content !== "string")
73058
+ continue;
73059
+ const text = extractChannelBody(content);
73060
+ if (text)
73061
+ turns.push({ role: "user", text });
73278
73062
  continue;
73279
- const h = shannonEntropy(value);
73280
- if (h < KV_ENTROPY_THRESHOLD)
73063
+ }
73064
+ if (obj.type === "user" && obj.message && typeof obj.message === "object") {
73065
+ const content = obj.message.content;
73066
+ const raw2 = extractTextBlocks(content);
73067
+ const text = raw2 ? extractChannelBody(raw2) : null;
73068
+ if (text)
73069
+ turns.push({ role: "user", text });
73281
73070
  continue;
73282
- const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
73283
- if (valueOffsetInMatch < 0)
73071
+ }
73072
+ if (obj.type === "assistant" && obj.message && typeof obj.message === "object") {
73073
+ const content = obj.message.content;
73074
+ const text = extractTextBlocks(content);
73075
+ if (text)
73076
+ turns.push({ role: "assistant", text });
73284
73077
  continue;
73285
- const start = m.index + valueOffsetInMatch;
73286
- const end = start + value.length;
73287
- hits.push({
73288
- rule_id: "kv_entropy",
73289
- start,
73290
- end,
73291
- matched_text: value,
73292
- key_name: keyName,
73293
- confidence: "ambiguous"
73294
- });
73078
+ }
73295
73079
  }
73296
- return hits;
73297
- }
73298
-
73299
- // telegram-plugin/secret-detect/db-uri.ts
73300
- var DB_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]+):\/\/([^\s:/@]+):([^\s/?#]+)@([^\s/@?#]+)/g;
73301
- var DB_URI_RULE_ID = "db_uri_password";
73302
- function isInertPassword(value) {
73303
- if (value.startsWith("[REDACTED"))
73304
- return true;
73305
- return /^[*x]+$/i.test(value);
73306
- }
73307
- function scanDbUris(text) {
73308
- const hits = [];
73309
- DB_URI_RE.lastIndex = 0;
73310
- let m;
73311
- while ((m = DB_URI_RE.exec(text)) !== null) {
73312
- const scheme = m[1];
73313
- const user = m[2];
73314
- const password = m[3];
73315
- if (isInertPassword(password))
73080
+ const deduped = [];
73081
+ for (const t of turns) {
73082
+ const prev = deduped[deduped.length - 1];
73083
+ if (prev && prev.role === t.role && prev.text === t.text)
73316
73084
  continue;
73317
- const start = m.index + scheme.length + 3 + user.length + 1;
73318
- hits.push({
73319
- rule_id: DB_URI_RULE_ID,
73320
- start,
73321
- end: start + password.length,
73322
- matched_text: password,
73323
- key_name: `${scheme}_password`,
73324
- confidence: "ambiguous"
73325
- });
73085
+ deduped.push(t);
73326
73086
  }
73327
- return hits;
73087
+ if (deduped.length <= maxTurns)
73088
+ return deduped;
73089
+ return deduped.slice(deduped.length - maxTurns);
73328
73090
  }
73329
-
73330
- // telegram-plugin/secret-detect/generic-entropy.ts
73331
- var CANDIDATE_RE = /[A-Za-z0-9]{28,}/g;
73332
- var GENERIC_MIN_DISTINCT = 18;
73333
- var MAX_GENERIC_HITS = 20;
73334
- function hasDistinctChars(tok, n) {
73335
- const seen = new Uint8Array(128);
73336
- let distinct = 0;
73337
- for (let i = 0;i < tok.length; i++) {
73338
- const c = tok.charCodeAt(i);
73339
- if (seen[c] === 0) {
73340
- seen[c] = 1;
73341
- if (++distinct >= n)
73342
- return true;
73343
- }
73344
- }
73345
- return false;
73091
+ function extractChannelBody(raw) {
73092
+ const m = raw.match(/<channel[^>]*>([\s\S]*?)<\/channel>/);
73093
+ if (m)
73094
+ return m[1].trim();
73095
+ const trimmed = raw.trim();
73096
+ return trimmed.length > 0 ? trimmed : null;
73346
73097
  }
73347
- function hasDigit(tok) {
73348
- for (let i = 0;i < tok.length; i++) {
73349
- const c = tok.charCodeAt(i);
73350
- if (c >= 48 && c <= 57)
73351
- return true;
73098
+ function extractTextBlocks(content) {
73099
+ if (typeof content === "string") {
73100
+ const t = content.trim();
73101
+ return t.length > 0 ? t : null;
73352
73102
  }
73353
- return false;
73354
- }
73355
- function scanGenericSecrets(text) {
73356
- const hits = [];
73357
- CANDIDATE_RE.lastIndex = 0;
73358
- let m;
73359
- while ((m = CANDIDATE_RE.exec(text)) !== null) {
73360
- if (hits.length >= MAX_GENERIC_HITS)
73361
- break;
73362
- const tok = m[0];
73363
- if (!hasDigit(tok))
73364
- continue;
73365
- if (!hasDistinctChars(tok, GENERIC_MIN_DISTINCT))
73103
+ if (!Array.isArray(content))
73104
+ return null;
73105
+ const parts = [];
73106
+ for (const block of content) {
73107
+ if (!block || typeof block !== "object")
73366
73108
  continue;
73367
- hits.push({
73368
- rule_id: "generic_high_entropy",
73369
- start: m.index,
73370
- end: m.index + tok.length,
73371
- matched_text: tok,
73372
- confidence: "ambiguous"
73373
- });
73109
+ const b = block;
73110
+ if (b.type === "text" && typeof b.text === "string") {
73111
+ parts.push(b.text);
73112
+ } else if (b.type === "tool_result" && typeof b.content === "string") {
73113
+ parts.push(`[tool result] ${b.content.slice(0, 400)}`);
73114
+ }
73374
73115
  }
73375
- return hits;
73116
+ const joined = parts.join(`
73117
+ `).trim();
73118
+ return joined.length > 0 ? joined : null;
73376
73119
  }
73120
+ function formatTranscriptTail(turns) {
73121
+ const header = `# Handoff \u2014 previous session
73377
73122
 
73378
- // telegram-plugin/secret-detect/chunker.ts
73379
- var CHUNK_THRESHOLD = 32 * 1024;
73380
- var WINDOW_SIZE = 16 * 1024;
73381
- var OVERLAP = 8 * 1024;
73382
- function chunk(text) {
73383
- if (text.length <= CHUNK_THRESHOLD) {
73384
- return [{ offset: 0, text }];
73385
- }
73386
- const out = [];
73387
- let offset = 0;
73388
- while (offset < text.length) {
73389
- const end = Math.min(offset + WINDOW_SIZE, text.length);
73390
- out.push({ offset, text: text.slice(offset, end) });
73391
- if (end >= text.length)
73392
- break;
73393
- offset = end - OVERLAP;
73123
+ ` + "You are resuming this agent's work. There is no generated summary " + "\u2014 below is the **raw tail of the previous session's transcript** " + "(oldest first, most recent last). Read it to reorient, then carry " + "on. Anything important worth keeping long-term should already be " + `in your memory files \u2014 check those too.
73124
+ `;
73125
+ if (turns.length === 0) {
73126
+ return header + `
73127
+ _(No recent turns were recoverable from the previous session.)_
73128
+ `;
73394
73129
  }
73395
- return out;
73396
- }
73130
+ const body = turns.map((t) => {
73131
+ let text = t.text;
73132
+ if (text.length > TURN_TEXT_MAX_CHARS) {
73133
+ text = text.slice(0, TURN_TEXT_MAX_CHARS) + `
73134
+ \u2026[truncated]`;
73135
+ }
73136
+ return `### ${t.role === "user" ? "User" : "Assistant"}
73137
+ ${text}`;
73138
+ }).join(`
73397
73139
 
73398
- // telegram-plugin/secret-detect/suppressor.ts
73399
- var MARKERS = ["test", "mock", "example", "fixture", "dummy"];
73400
- var WINDOW = 40;
73401
- var MARKER_RE = new RegExp(`\\b(?:${MARKERS.join("|")})\\b`, "i");
73402
- function isSuppressed(text, start, end) {
73403
- const left = Math.max(0, start - WINDOW);
73404
- const right = Math.min(text.length, end + WINDOW);
73405
- const context = text.slice(left, start) + text.slice(end, right);
73406
- return MARKER_RE.test(context);
73407
- }
73140
+ `);
73141
+ return `${header}
73142
+ ## Recent turns
73408
73143
 
73409
- // telegram-plugin/secret-detect/slug.ts
73410
- function sanitizeKeyName(raw) {
73411
- const up = raw.toUpperCase();
73412
- const cleaned = up.replace(/[^A-Z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
73413
- return cleaned.length > 0 ? cleaned : "SECRET";
73414
- }
73415
- function datePart(now = new Date) {
73416
- const y = now.getUTCFullYear();
73417
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
73418
- const d = String(now.getUTCDate()).padStart(2, "0");
73419
- return `${y}${m}${d}`;
73144
+ ${body}
73145
+ `;
73420
73146
  }
73421
- function deriveSlug(inputs, existing) {
73422
- let base;
73423
- if (inputs.key_name && inputs.key_name.trim().length > 0) {
73424
- base = sanitizeKeyName(inputs.key_name);
73425
- } else {
73426
- base = `${inputs.rule_id}_${datePart(inputs.now)}`;
73147
+ function deriveTopic(turns) {
73148
+ for (let i = turns.length - 1;i >= 0; i--) {
73149
+ if (turns[i].role === "user")
73150
+ return clampTopic(firstLine(turns[i].text));
73427
73151
  }
73428
- if (!existing.has(base))
73429
- return base;
73430
- let n = 2;
73431
- while (existing.has(`${base}_${n}`))
73432
- n++;
73433
- return `${base}_${n}`;
73152
+ if (turns.length > 0)
73153
+ return clampTopic(firstLine(turns[turns.length - 1].text));
73154
+ return "previous session";
73434
73155
  }
73435
- // telegram-plugin/secret-detect/url-redact.ts
73436
- var SENSITIVE_PARAMS = new Set([
73437
- "token",
73438
- "key",
73439
- "api_key",
73440
- "apikey",
73441
- "secret",
73442
- "access_token",
73443
- "password",
73444
- "pass",
73445
- "auth",
73446
- "client_secret",
73447
- "refresh_token",
73448
- "signature"
73449
- ]);
73450
- var URL_RE = /\b(?:https?|wss?|ftp):\/\/[^\s<>"']+/gi;
73451
- function redactUrls(text) {
73452
- return text.replace(URL_RE, (m) => {
73453
- const redacted = redactOne(m);
73454
- return redacted ?? m;
73455
- });
73156
+ function firstLine(s) {
73157
+ const line = s.split(/\r?\n/).find((l) => l.trim().length > 0) ?? s;
73158
+ return line.trim();
73456
73159
  }
73457
- function redactOne(raw) {
73458
- let u;
73459
- try {
73460
- u = new URL(raw);
73461
- } catch {
73462
- return null;
73463
- }
73464
- let changed = false;
73465
- if (u.username || u.password) {
73466
- u.username = "***";
73467
- u.password = "";
73468
- changed = true;
73469
- }
73470
- for (const [key] of u.searchParams) {
73471
- if (SENSITIVE_PARAMS.has(key.toLowerCase())) {
73472
- u.searchParams.set(key, "***");
73473
- changed = true;
73474
- }
73475
- }
73476
- return changed ? u.toString() : null;
73477
- }
73478
-
73479
- // telegram-plugin/secret-detect/index.ts
73480
- function detectSecrets(text) {
73481
- if (!text || text.length === 0)
73482
- return [];
73483
- const windows = chunk(text);
73484
- const raw = [];
73485
- for (const win of windows) {
73486
- for (const p of ALL_PATTERNS) {
73487
- const re = new RegExp(p.regex.source, p.regex.flags.includes("g") ? p.regex.flags : p.regex.flags + "g");
73488
- let m;
73489
- while ((m = re.exec(win.text)) !== null) {
73490
- if (m[0].length === 0) {
73491
- re.lastIndex++;
73492
- continue;
73493
- }
73494
- const cap = p.captureIndex === 0 ? m[0] : m[p.captureIndex];
73495
- if (!cap)
73496
- continue;
73497
- const matchStart = p.captureIndex === 0 ? m.index : m.index + m[0].indexOf(cap);
73498
- if (matchStart < 0)
73499
- continue;
73500
- const globalStart = win.offset + matchStart;
73501
- const globalEnd = globalStart + cap.length;
73502
- const keyName = p.rule_id === "env_key_value" ? m[1] : undefined;
73503
- if (INERT_GATED_RULES.has(p.rule_id) && isInertValue(cap))
73504
- continue;
73505
- if (p.rule_id === "env_key_value") {
73506
- const ENV_KV_MIN_LEN = 12;
73507
- const ENV_KV_MIN_ENTROPY = 3.5;
73508
- if (cap.length < ENV_KV_MIN_LEN)
73509
- continue;
73510
- if (shannonEntropy(cap) < ENV_KV_MIN_ENTROPY)
73511
- continue;
73512
- }
73513
- raw.push({
73514
- rule_id: p.rule_id,
73515
- start: globalStart,
73516
- end: globalEnd,
73517
- matched_text: cap,
73518
- key_name: keyName,
73519
- confidence: "high"
73520
- });
73521
- }
73522
- }
73523
- const kvHits = scanKeyValue(win.text);
73524
- for (const h of kvHits) {
73525
- raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
73526
- }
73527
- for (const h of scanDbUris(win.text)) {
73528
- raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
73529
- }
73530
- for (const h of scanMemorablePasswords(win.text)) {
73531
- raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
73532
- }
73533
- const genHits = scanGenericSecrets(win.text);
73534
- for (const h of genHits) {
73535
- raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
73536
- }
73537
- }
73538
- const deduped = dedupeRaw(raw);
73539
- const final = dropOverlaps(deduped);
73540
- const existing = new Set;
73541
- const out = [];
73542
- for (const h of final) {
73543
- const suggested_slug = deriveSlug({ key_name: h.key_name, rule_id: h.rule_id }, existing);
73544
- existing.add(suggested_slug);
73545
- out.push({
73546
- rule_id: h.rule_id,
73547
- matched_text: h.matched_text,
73548
- start: h.start,
73549
- end: h.end,
73550
- confidence: h.confidence,
73551
- suppressed: isSuppressed(text, h.start, h.end),
73552
- suggested_slug,
73553
- key_name: h.key_name
73554
- });
73555
- }
73556
- out.sort((a, b) => a.start - b.start);
73557
- return out;
73558
- }
73559
- function dedupeRaw(raw) {
73560
- const seen = new Map;
73561
- for (const h of raw) {
73562
- const key = `${h.start}:${h.end}`;
73563
- const existing = seen.get(key);
73564
- if (!existing) {
73565
- seen.set(key, h);
73566
- continue;
73567
- }
73568
- if (existing.confidence === "ambiguous" && h.confidence === "high") {
73569
- seen.set(key, h);
73570
- }
73571
- }
73572
- return Array.from(seen.values());
73573
- }
73574
- function dropOverlaps(hits) {
73575
- const out = hits.filter((h) => !(h.confidence === "ambiguous" && hits.some((o) => o !== h && o.start <= h.start && o.end >= h.end && !(o.start === h.start && o.end === h.end))));
73576
- out.sort((a, b) => a.start - b.start || a.end - b.end);
73577
- return out;
73578
- }
73579
-
73580
- // telegram-plugin/secret-detect/redact.ts
73581
- var REDACTED_MARKER = "[REDACTED]";
73582
- function redact(text) {
73583
- if (!text || text.length === 0)
73584
- return text;
73585
- const urlScrubbed = redactUrls(text);
73586
- const hits = detectSecrets(urlScrubbed).filter((h) => h.rule_id !== "generic_high_entropy");
73587
- if (hits.length === 0)
73588
- return urlScrubbed;
73589
- const sorted = [...hits].sort((a, b) => b.start - a.start);
73590
- let out = urlScrubbed;
73591
- for (const h of sorted) {
73592
- out = out.slice(0, h.start) + redactedMarker(h.rule_id) + out.slice(h.end);
73593
- }
73594
- return out;
73595
- }
73596
- function redactedMarker(ruleId) {
73597
- const trimmed = ruleId.replace(/^(kv|env)_/, "");
73598
- if (!trimmed || trimmed === "key_value" || trimmed === "kv_entropy") {
73599
- return REDACTED_MARKER;
73600
- }
73601
- return `[REDACTED:${trimmed}]`;
73602
- }
73603
- // src/memory/hindsight-write-redaction.ts
73604
- function redactJsonStrings(value) {
73605
- if (typeof value === "string")
73606
- return redact(value);
73607
- if (Array.isArray(value))
73608
- return value.map(redactJsonStrings);
73609
- if (value && typeof value === "object") {
73610
- const out = {};
73611
- for (const [k, v] of Object.entries(value)) {
73612
- out[k] = redactJsonStrings(v);
73613
- }
73614
- return out;
73615
- }
73616
- return value;
73617
- }
73618
- var REDACTED_ITEM_FIELDS = ["content", "context"];
73619
- function redactMemoryWriteBody(body) {
73620
- if (!body || typeof body !== "object" || Array.isArray(body))
73621
- return body;
73622
- const src = body;
73623
- if (!Array.isArray(src.items))
73624
- return body;
73625
- const items = src.items.map((item) => {
73626
- if (!item || typeof item !== "object" || Array.isArray(item))
73627
- return item;
73628
- const row = { ...item };
73629
- for (const field of REDACTED_ITEM_FIELDS) {
73630
- if (typeof row[field] === "string") {
73631
- row[field] = redact(row[field]);
73632
- }
73633
- }
73634
- if (row.metadata && typeof row.metadata === "object") {
73635
- row.metadata = redactJsonStrings(row.metadata);
73636
- }
73637
- return row;
73638
- });
73639
- return { ...src, items };
73640
- }
73641
-
73642
- // src/agents/handoff-summarizer.ts
73643
- init_observation_scopes();
73644
- var HANDOFF_STATUS_MIRROR_SKIPPED = "mirror-skipped-invalid-scope";
73645
- var DEFAULT_MAX_TURNS = 50;
73646
- var TOPIC_MAX_CHARS = 117;
73647
- var TURN_TEXT_MAX_CHARS = 1200;
73648
- function extractTurnsFromJsonl(path2, maxTurns) {
73649
- let raw;
73650
- try {
73651
- raw = readFileSync20(path2, "utf-8");
73652
- } catch {
73653
- return [];
73654
- }
73655
- const turns = [];
73656
- for (const line of raw.split(`
73657
- `)) {
73658
- if (!line.trim())
73659
- continue;
73660
- let obj;
73661
- try {
73662
- obj = JSON.parse(line);
73663
- } catch {
73664
- continue;
73665
- }
73666
- if (obj.type === "queue-operation") {
73667
- const op = obj.operation;
73668
- if (op !== "enqueue")
73669
- continue;
73670
- const content = obj.content;
73671
- if (typeof content !== "string")
73672
- continue;
73673
- const text = extractChannelBody(content);
73674
- if (text)
73675
- turns.push({ role: "user", text });
73676
- continue;
73677
- }
73678
- if (obj.type === "user" && obj.message && typeof obj.message === "object") {
73679
- const content = obj.message.content;
73680
- const raw2 = extractTextBlocks(content);
73681
- const text = raw2 ? extractChannelBody(raw2) : null;
73682
- if (text)
73683
- turns.push({ role: "user", text });
73684
- continue;
73685
- }
73686
- if (obj.type === "assistant" && obj.message && typeof obj.message === "object") {
73687
- const content = obj.message.content;
73688
- const text = extractTextBlocks(content);
73689
- if (text)
73690
- turns.push({ role: "assistant", text });
73691
- continue;
73692
- }
73693
- }
73694
- const deduped = [];
73695
- for (const t of turns) {
73696
- const prev = deduped[deduped.length - 1];
73697
- if (prev && prev.role === t.role && prev.text === t.text)
73698
- continue;
73699
- deduped.push(t);
73700
- }
73701
- if (deduped.length <= maxTurns)
73702
- return deduped;
73703
- return deduped.slice(deduped.length - maxTurns);
73704
- }
73705
- function extractChannelBody(raw) {
73706
- const m = raw.match(/<channel[^>]*>([\s\S]*?)<\/channel>/);
73707
- if (m)
73708
- return m[1].trim();
73709
- const trimmed = raw.trim();
73710
- return trimmed.length > 0 ? trimmed : null;
73711
- }
73712
- function extractTextBlocks(content) {
73713
- if (typeof content === "string") {
73714
- const t = content.trim();
73715
- return t.length > 0 ? t : null;
73716
- }
73717
- if (!Array.isArray(content))
73718
- return null;
73719
- const parts = [];
73720
- for (const block of content) {
73721
- if (!block || typeof block !== "object")
73722
- continue;
73723
- const b = block;
73724
- if (b.type === "text" && typeof b.text === "string") {
73725
- parts.push(b.text);
73726
- } else if (b.type === "tool_result" && typeof b.content === "string") {
73727
- parts.push(`[tool result] ${b.content.slice(0, 400)}`);
73728
- }
73729
- }
73730
- const joined = parts.join(`
73731
- `).trim();
73732
- return joined.length > 0 ? joined : null;
73733
- }
73734
- function formatTranscriptTail(turns) {
73735
- const header = `# Handoff \u2014 previous session
73736
-
73737
- ` + "You are resuming this agent's work. There is no generated summary " + "\u2014 below is the **raw tail of the previous session's transcript** " + "(oldest first, most recent last). Read it to reorient, then carry " + "on. Anything important worth keeping long-term should already be " + `in your memory files \u2014 check those too.
73738
- `;
73739
- if (turns.length === 0) {
73740
- return header + `
73741
- _(No recent turns were recoverable from the previous session.)_
73742
- `;
73743
- }
73744
- const body = turns.map((t) => {
73745
- let text = t.text;
73746
- if (text.length > TURN_TEXT_MAX_CHARS) {
73747
- text = text.slice(0, TURN_TEXT_MAX_CHARS) + `
73748
- \u2026[truncated]`;
73749
- }
73750
- return `### ${t.role === "user" ? "User" : "Assistant"}
73751
- ${text}`;
73752
- }).join(`
73753
-
73754
- `);
73755
- return `${header}
73756
- ## Recent turns
73757
-
73758
- ${body}
73759
- `;
73760
- }
73761
- function deriveTopic(turns) {
73762
- for (let i = turns.length - 1;i >= 0; i--) {
73763
- if (turns[i].role === "user")
73764
- return clampTopic(firstLine(turns[i].text));
73765
- }
73766
- if (turns.length > 0)
73767
- return clampTopic(firstLine(turns[turns.length - 1].text));
73768
- return "previous session";
73769
- }
73770
- function firstLine(s) {
73771
- const line = s.split(/\r?\n/).find((l) => l.trim().length > 0) ?? s;
73772
- return line.trim();
73773
- }
73774
- function clampTopic(s) {
73775
- return s.length > TOPIC_MAX_CHARS ? s.slice(0, TOPIC_MAX_CHARS) + "\u2026" : s;
73160
+ function clampTopic(s) {
73161
+ return s.length > TOPIC_MAX_CHARS ? s.slice(0, TOPIC_MAX_CHARS) + "\u2026" : s;
73776
73162
  }
73777
73163
  function writeSidecarsAtomic(agentDir, briefing, topic) {
73778
73164
  mkdirSync16(agentDir, { recursive: true });
@@ -73805,8 +73191,7 @@ async function buildHandoff(opts) {
73805
73191
  `);
73806
73192
  return "write-error";
73807
73193
  }
73808
- const mirrored = await mirrorToHindsight(briefing, opts).catch(() => true);
73809
- return mirrored ? "ok" : HANDOFF_STATUS_MIRROR_SKIPPED;
73194
+ return "ok";
73810
73195
  }
73811
73196
  function errMsg(err) {
73812
73197
  if (err && typeof err === "object" && "message" in err) {
@@ -73814,42 +73199,6 @@ function errMsg(err) {
73814
73199
  }
73815
73200
  return String(err);
73816
73201
  }
73817
- async function mirrorToHindsight(briefing, opts) {
73818
- const url = opts.hindsightUrl ?? process.env.HINDSIGHT_API_URL;
73819
- const bankId = opts.hindsightBankId ?? process.env.HINDSIGHT_BANK_ID ?? "default";
73820
- if (!url)
73821
- return true;
73822
- const fetchFn = opts.fetch ?? fetch;
73823
- const endpoint = `${url.replace(/\/$/, "")}/v1/default/banks/${encodeURIComponent(bankId)}/memories`;
73824
- const scope = resolveObservationScope(opts.observationScopes, opts.env ?? process.env);
73825
- if (scope.kind === "invalid") {
73826
- process.stderr.write(`handoff: hindsight mirror SKIPPED \u2014 observation scope ` + `${JSON.stringify(scope.raw)} is not valid ` + `(accepted: ${observationScopesHint()}). Fix ` + `memory.observation_scopes in switchroom.yaml (or the ` + `${OBSERVATION_SCOPES_ENV} export), then \`switchroom apply\` and ` + `restart the agent. The on-disk handoff sidecars were written ` + `normally; only the recallable Hindsight copy was skipped.
73827
- `);
73828
- return false;
73829
- }
73830
- const body = redactMemoryWriteBody({
73831
- items: [
73832
- {
73833
- content: briefing,
73834
- document_id: "session_handoff",
73835
- tags: ["session_handoff", opts.agentName],
73836
- ...scope.kind === "set" ? { observation_scopes: scope.scope } : {}
73837
- }
73838
- ],
73839
- async: true
73840
- });
73841
- try {
73842
- await fetchFn(endpoint, {
73843
- method: "POST",
73844
- headers: { "content-type": "application/json" },
73845
- body: JSON.stringify(body)
73846
- });
73847
- } catch (err) {
73848
- process.stderr.write(`handoff: hindsight mirror failed \u2014 ${errMsg(err)}
73849
- `);
73850
- }
73851
- return true;
73852
- }
73853
73202
  function findLatestSessionJsonl(claudeConfigDir) {
73854
73203
  const projects = join18(claudeConfigDir, "projects");
73855
73204
  if (!existsSync24(projects))
@@ -76856,8 +76205,8 @@ Bootstrapping agent: ${name}
76856
76205
  const code = await new Promise((resolve25) => {
76857
76206
  process.stdin.setEncoding("utf-8");
76858
76207
  let buf = "";
76859
- process.stdin.on("data", (chunk2) => {
76860
- buf += chunk2.toString();
76208
+ process.stdin.on("data", (chunk) => {
76209
+ buf += chunk.toString();
76861
76210
  const newlineIdx = buf.indexOf(`
76862
76211
  `);
76863
76212
  if (newlineIdx !== -1) {
@@ -76939,8 +76288,8 @@ switchroom agent add: ${name}
76939
76288
  return new Promise((resolveLine) => {
76940
76289
  process.stdin.setEncoding("utf-8");
76941
76290
  let buf = "";
76942
- const onData = (chunk2) => {
76943
- buf += chunk2.toString();
76291
+ const onData = (chunk) => {
76292
+ buf += chunk.toString();
76944
76293
  const newlineIdx = buf.indexOf(`
76945
76294
  `);
76946
76295
  if (newlineIdx !== -1) {
@@ -78971,8 +78320,8 @@ async function readHiddenLine2(prompt) {
78971
78320
  stdin.setRawMode(true);
78972
78321
  }
78973
78322
  let line = "";
78974
- const onData = (chunk2) => {
78975
- const s = chunk2.toString("utf8");
78323
+ const onData = (chunk) => {
78324
+ const s = chunk.toString("utf8");
78976
78325
  for (const ch of s) {
78977
78326
  if (ch === `
78978
78327
  ` || ch === "\r") {
@@ -79901,7 +79250,7 @@ function getVaultPath2(configPath) {
79901
79250
  return resolvePath("~/.switchroom/vault.enc");
79902
79251
  }
79903
79252
  }
79904
- function maskToken2(s) {
79253
+ function maskToken(s) {
79905
79254
  if (s.length >= 18)
79906
79255
  return `${s.slice(0, 6)}...${s.slice(-4)}`;
79907
79256
  return "***";
@@ -80189,7 +79538,7 @@ function registerVaultSweep(vault, program3) {
80189
79538
  console.log("");
80190
79539
  console.log(source_default.dim(`masked values for display only:`));
80191
79540
  for (const v of values) {
80192
- console.log(` vault:${v.key} \u2192 ${maskToken2(v.value)}`);
79541
+ console.log(` vault:${v.key} \u2192 ${maskToken(v.value)}`);
80193
79542
  }
80194
79543
  if (opts.dryRun) {
80195
79544
  console.log(source_default.yellow("dry-run \u2014 no files modified. Rerun without --dry-run to apply."));
@@ -83162,8 +82511,8 @@ class VaultBroker {
83162
82511
  peer = this.testOpts._testIdentify ? this.testOpts._testIdentify(listenerSocketPath, socket) : identify(listenerSocketPath, socket);
83163
82512
  }
83164
82513
  let buffer = "";
83165
- socket.on("data", (chunk2) => {
83166
- buffer += chunk2.toString("utf8");
82514
+ socket.on("data", (chunk) => {
82515
+ buffer += chunk.toString("utf8");
83167
82516
  if (Buffer.byteLength(buffer, "utf8") > MAX_FRAME_BYTES) {
83168
82517
  const resp = encodeResponse(errorResponse("BAD_REQUEST", "Frame exceeds 64 KiB limit"));
83169
82518
  socket.end(resp);
@@ -84272,8 +83621,8 @@ class VaultBroker {
84272
83621
  const auditCaller = isOperator ? "operator" : unlockPeer !== null ? callerFromPeer(unlockPeer) : `pid:${process.pid}`;
84273
83622
  const auditCgroup = isOperator ? undefined : unlockPeer?.systemdUnit ?? undefined;
84274
83623
  let buffer = "";
84275
- socket.on("data", (chunk2) => {
84276
- buffer += chunk2.toString("utf8");
83624
+ socket.on("data", (chunk) => {
83625
+ buffer += chunk.toString("utf8");
84277
83626
  const newlineIdx = buffer.indexOf(`
84278
83627
  `);
84279
83628
  if (newlineIdx === -1) {
@@ -85840,7 +85189,7 @@ function promptLine2(prompt, hidden = false) {
85840
85189
  function readStdinToEnd() {
85841
85190
  return new Promise((resolve31, reject) => {
85842
85191
  const chunks = [];
85843
- process.stdin.on("data", (chunk2) => chunks.push(chunk2));
85192
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
85844
85193
  process.stdin.on("end", () => {
85845
85194
  resolve31(Buffer.concat(chunks).toString("utf8"));
85846
85195
  });
@@ -87265,257 +86614,846 @@ function registerLinearAgentCommand(program3) {
87265
86614
  } else {
87266
86615
  console.log(source_default.green(`\u2713 Default Linear team for '${opts.agent}' set to ${opts.team.trim()}.`));
87267
86616
  }
87268
- console.log(source_default.gray(` Run 'switchroom agent restart ${opts.agent}' to pick up the change.`));
87269
- }));
86617
+ console.log(source_default.gray(` Run 'switchroom agent restart ${opts.agent}' to pick up the change.`));
86618
+ }));
86619
+ }
86620
+ function printLinearInstructions(opts, vaultKey) {
86621
+ const base = opts.webhookBase ?? "https://<your-switchroom-web-host>";
86622
+ const webhookUrl = `${base.replace(/\/$/, "")}/webhook/${opts.agent}/linear`;
86623
+ console.log("");
86624
+ console.log(source_default.bold("Next steps in Linear (browser, one-time per agent):"));
86625
+ console.log(source_default.gray(" 1. Create / open your Linear OAuth app with actor=app and scopes"));
86626
+ console.log(source_default.gray(" app:mentionable + app:assignable (https://linear.app/developers/agents)."));
86627
+ if (opts.clientId) {
86628
+ const redirect = opts.redirectUri ?? `${base.replace(/\/$/, "")}/oauth/callback`;
86629
+ const authorizeUrl = `https://linear.app/oauth/authorize?` + `client_id=${encodeURIComponent(opts.clientId)}` + `&redirect_uri=${encodeURIComponent(redirect)}` + `&response_type=code` + `&scope=${encodeURIComponent("read,write,app:assignable,app:mentionable")}` + `&actor=app`;
86630
+ console.log(source_default.gray(" 2. Authorize the app as an actor (open in a browser):"));
86631
+ console.log(source_default.cyan(` ${authorizeUrl}`));
86632
+ console.log(source_default.gray(" The redirect delivers the app token you pass to this verb via --token."));
86633
+ } else {
86634
+ console.log(source_default.gray(" 2. Authorize the app (actor=app) in a browser; capture the app token and"));
86635
+ console.log(source_default.gray(" re-run this verb with --token (and optionally --client-id to print the"));
86636
+ console.log(source_default.gray(" authorize URL)."));
86637
+ }
86638
+ console.log(source_default.gray(" 3. Register this webhook URL on the app (AgentSessionEvent + Issue/Comment):"));
86639
+ console.log(source_default.cyan(` ${webhookUrl}`));
86640
+ console.log(source_default.gray(" Use Linear's signing secret as the webhook secret \u2014 store it in the vault " + `under webhook/${opts.agent}/linear (e.g. 'switchroom telegram enable webhook ` + `--agent ${opts.agent} --source linear --secret <signing-secret>').`));
86641
+ console.log("");
86642
+ console.log(source_default.gray(` Token is read at runtime from vault:${vaultKey} (actor=app).`));
86643
+ }
86644
+ function makeUnifiedDiff2(before, after) {
86645
+ const a = before.split(`
86646
+ `);
86647
+ const b = after.split(`
86648
+ `);
86649
+ const out = [];
86650
+ let i = 0, j = 0;
86651
+ while (i < a.length || j < b.length) {
86652
+ if (i < a.length && j < b.length && a[i] === b[j]) {
86653
+ out.push(` ${a[i]}`);
86654
+ i++;
86655
+ j++;
86656
+ } else if (j < b.length && (i >= a.length || a[i] !== b[j])) {
86657
+ out.push(source_default.green(`+ ${b[j]}`));
86658
+ j++;
86659
+ } else {
86660
+ out.push(source_default.red(`- ${a[i]}`));
86661
+ i++;
86662
+ }
86663
+ }
86664
+ return out.join(`
86665
+ `);
86666
+ }
86667
+ function fail3(msg) {
86668
+ console.error(source_default.red(`Error: ${msg}`));
86669
+ process.exit(1);
86670
+ }
86671
+
86672
+ // src/cli/memory.ts
86673
+ init_source();
86674
+ init_hindsight2();
86675
+
86676
+ // src/memory/search.ts
86677
+ init_hindsight2();
86678
+ function shellQuote2(s) {
86679
+ return "'" + s.replace(/'/g, `'"'"'`) + "'";
86680
+ }
86681
+ function searchMemory(query, collection) {
86682
+ return `hindsight recall --collection ${shellQuote2(collection)} --query ${shellQuote2(query)}`;
86683
+ }
86684
+ function getMemoryStats(collection) {
86685
+ return `hindsight stats --collection ${shellQuote2(collection)}`;
86686
+ }
86687
+ function reflectAcrossAgents(config) {
86688
+ const eligible = [];
86689
+ const excluded = [];
86690
+ for (const agentName of Object.keys(config.agents)) {
86691
+ const collection = getCollectionForAgent(agentName, config);
86692
+ if (isStrictIsolation(agentName, config)) {
86693
+ excluded.push({ agent: agentName, collection });
86694
+ } else {
86695
+ eligible.push({ agent: agentName, collection });
86696
+ }
86697
+ }
86698
+ const commands = eligible.map(({ collection }) => `hindsight reflect --collection ${shellQuote2(collection)}`);
86699
+ return { eligible, excluded, commands };
86700
+ }
86701
+
86702
+ // src/memory/observation-scope-cleanup.ts
86703
+ var CLEANUP_VOLATILE_PATTERNS = [
86704
+ /^parent_session:/,
86705
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(-sub-.+)?$/
86706
+ ];
86707
+ var CLEANUP_STALE_PATTERNS = [/^agent-[0-9a-f]{8,}$/];
86708
+ function matchesAny(tag, patterns) {
86709
+ return patterns.some((p) => p.test(tag));
86710
+ }
86711
+ function scopeKey(tags) {
86712
+ return tags.length === 0 ? "shared" : [...tags].sort().join(",");
86713
+ }
86714
+ function planScopeCleanup(bankId, scopes, options = {}) {
86715
+ const volatilePatterns = options.volatilePatterns ?? CLEANUP_VOLATILE_PATTERNS;
86716
+ const stalePatterns = options.stalePatterns ?? CLEANUP_STALE_PATTERNS;
86717
+ const retireStale = options.retireStale ?? false;
86718
+ const totalScopes = scopes.length;
86719
+ const totalObservations = scopes.reduce((n, s) => n + s.count, 0);
86720
+ const reclassified = [];
86721
+ const breakdown = {
86722
+ allVolatileScopes: 0,
86723
+ allVolatileObservations: 0,
86724
+ mixedScopes: 0,
86725
+ mixedObservations: 0,
86726
+ staleScopes: 0,
86727
+ staleObservations: 0
86728
+ };
86729
+ const targets = new Map;
86730
+ for (const s of scopes) {
86731
+ const volatileTags = s.tags.filter((t) => matchesAny(t, volatilePatterns));
86732
+ const staleTags = s.tags.filter((t) => matchesAny(t, stalePatterns));
86733
+ const stripped = s.tags.filter((t) => {
86734
+ if (matchesAny(t, volatilePatterns))
86735
+ return false;
86736
+ if (retireStale && matchesAny(t, stalePatterns))
86737
+ return false;
86738
+ return true;
86739
+ });
86740
+ const toKey = scopeKey(stripped);
86741
+ const changed = scopeKey(s.tags) !== toKey;
86742
+ const hasStable = s.tags.some((t) => !matchesAny(t, volatilePatterns) && !matchesAny(t, stalePatterns));
86743
+ if (staleTags.length > 0) {
86744
+ breakdown.staleScopes += 1;
86745
+ breakdown.staleObservations += s.count;
86746
+ }
86747
+ if (volatileTags.length > 0 && !hasStable && staleTags.length === 0) {
86748
+ breakdown.allVolatileScopes += 1;
86749
+ breakdown.allVolatileObservations += s.count;
86750
+ } else if (volatileTags.length > 0 && hasStable) {
86751
+ breakdown.mixedScopes += 1;
86752
+ breakdown.mixedObservations += s.count;
86753
+ }
86754
+ const entry = targets.get(toKey) ?? { sources: 0, observations: 0, changed: false };
86755
+ entry.sources += 1;
86756
+ entry.observations += s.count;
86757
+ entry.changed = entry.changed || changed;
86758
+ targets.set(toKey, entry);
86759
+ if (changed) {
86760
+ reclassified.push({
86761
+ from: s.tags,
86762
+ to: toKey,
86763
+ count: s.count,
86764
+ strippedVolatile: volatileTags,
86765
+ strippedStale: retireStale ? staleTags : []
86766
+ });
86767
+ }
86768
+ }
86769
+ reclassified.sort((a, b) => b.count - a.count);
86770
+ const merges = [...targets.entries()].filter(([, v]) => v.sources > 1 && v.changed).map(([target, v]) => ({ target, sourceScopes: v.sources, observations: v.observations })).sort((a, b) => b.observations - a.observations);
86771
+ const affectedObservations = reclassified.reduce((n, r) => n + r.count, 0);
86772
+ const resultingScopeCount = targets.size;
86773
+ return {
86774
+ bankId,
86775
+ ok: true,
86776
+ totalScopes,
86777
+ totalObservations,
86778
+ reclassified,
86779
+ merges,
86780
+ affectedScopes: reclassified.length,
86781
+ affectedObservations,
86782
+ resultingScopeCount,
86783
+ scopeReduction: totalScopes - resultingScopeCount,
86784
+ breakdown
86785
+ };
86786
+ }
86787
+ async function getJson2(url, opts) {
86788
+ const fetchImpl = opts?.fetchImpl ?? fetch;
86789
+ const timeoutMs = opts?.timeoutMs ?? 15000;
86790
+ const controller = new AbortController;
86791
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
86792
+ try {
86793
+ const resp = await fetchImpl(url, { signal: controller.signal });
86794
+ clearTimeout(timeout);
86795
+ if (!resp.ok)
86796
+ return { ok: false, reason: `HTTP ${resp.status}` };
86797
+ return { ok: true, data: await resp.json() };
86798
+ } catch (err) {
86799
+ clearTimeout(timeout);
86800
+ if (err.name === "AbortError")
86801
+ return { ok: false, reason: "Timeout" };
86802
+ return { ok: false, reason: String(err.message ?? err) };
86803
+ }
86804
+ }
86805
+ async function planBankCleanup(apiUrl, bankId, options = {}, fetchOpts) {
86806
+ const base = hindsightRestBase(apiUrl);
86807
+ const bank = encodeURIComponent(bankId);
86808
+ const resp = await getJson2(`${base}/v1/default/banks/${bank}/observations/scopes`, fetchOpts);
86809
+ if (!resp.ok) {
86810
+ return {
86811
+ bankId,
86812
+ ok: false,
86813
+ reason: resp.reason,
86814
+ totalScopes: 0,
86815
+ totalObservations: 0,
86816
+ reclassified: [],
86817
+ merges: [],
86818
+ affectedScopes: 0,
86819
+ affectedObservations: 0,
86820
+ resultingScopeCount: 0,
86821
+ scopeReduction: 0,
86822
+ breakdown: {
86823
+ allVolatileScopes: 0,
86824
+ allVolatileObservations: 0,
86825
+ mixedScopes: 0,
86826
+ mixedObservations: 0,
86827
+ staleScopes: 0,
86828
+ staleObservations: 0
86829
+ }
86830
+ };
86831
+ }
86832
+ const raw = resp.data?.scopes;
86833
+ const scopes = [];
86834
+ if (Array.isArray(raw)) {
86835
+ for (const entry of raw) {
86836
+ const tags = entry?.tags;
86837
+ const count = entry?.count;
86838
+ if (!Array.isArray(tags) || typeof count !== "number")
86839
+ continue;
86840
+ if (!tags.every((t) => typeof t === "string"))
86841
+ continue;
86842
+ scopes.push({ tags, count });
86843
+ }
86844
+ }
86845
+ return planScopeCleanup(bankId, scopes, options);
86846
+ }
86847
+ function formatCleanupPlan(plan, maxMerges = 10) {
86848
+ if (!plan.ok) {
86849
+ return `bank ${plan.bankId}: UNREADABLE (${plan.reason ?? "unknown"})`;
86850
+ }
86851
+ const b = plan.breakdown;
86852
+ const lines = [];
86853
+ lines.push(`bank ${plan.bankId}`);
86854
+ lines.push(` today: ${plan.totalScopes} scopes, ${plan.totalObservations.toLocaleString()} observations`);
86855
+ lines.push(` legacy fragmentation:` + ` all-volatile ${b.allVolatileScopes} scopes / ${b.allVolatileObservations.toLocaleString()} obs;` + ` mixed ${b.mixedScopes} scopes / ${b.mixedObservations.toLocaleString()} obs;` + ` stale agent-<hex> ${b.staleScopes} scopes / ${b.staleObservations.toLocaleString()} obs`);
86856
+ lines.push(` WOULD change: ${plan.affectedScopes} scopes` + ` (${plan.affectedObservations.toLocaleString()} obs re-homed),` + ` ${plan.totalScopes} \u2192 ${plan.resultingScopeCount} scopes` + ` (\u2212${plan.scopeReduction})`);
86857
+ if (plan.merges.length > 0) {
86858
+ lines.push(` top merges:`);
86859
+ for (const m of plan.merges.slice(0, maxMerges)) {
86860
+ lines.push(` ${m.sourceScopes} scopes \u2192 ${m.target} (${m.observations.toLocaleString()} obs)`);
86861
+ }
86862
+ if (plan.merges.length > maxMerges) {
86863
+ lines.push(` +${plan.merges.length - maxMerges} more merge targets`);
86864
+ }
86865
+ }
86866
+ return lines.join(`
86867
+ `);
86868
+ }
86869
+
86870
+ // telegram-plugin/secret-detect/patterns.ts
86871
+ var ANCHORED_PATTERNS = [
86872
+ { rule_id: "anthropic_api_key", regex: /\b(sk-ant-[A-Za-z0-9_-]{8,})\b/g, captureIndex: 1, slugHint: "anthropic_api_key" },
86873
+ { rule_id: "anthropic_oauth_code", regex: /(?:^|\s)([A-Za-z0-9_-]{20,}#[A-Za-z0-9_-]{20,})(?=\s|$)/gm, captureIndex: 1, slugHint: "anthropic_oauth_code" },
86874
+ { rule_id: "openai_api_key", regex: /\b(sk-[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "openai_api_key" },
86875
+ { rule_id: "github_pat_classic", regex: /\b(ghp_[A-Za-z0-9]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
86876
+ { rule_id: "github_pat_fine_grained", regex: /\b(github_pat_[A-Za-z0-9_]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
86877
+ { rule_id: "slack_token", regex: /\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_token" },
86878
+ { rule_id: "slack_app_token", regex: /\b(xapp-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_app_token" },
86879
+ { rule_id: "groq_api_key", regex: /\b(gsk_[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "groq_api_key" },
86880
+ { rule_id: "google_api_key", regex: /\b(AIza[0-9A-Za-z\-_]{20,})\b/g, captureIndex: 1, slugHint: "google_api_key" },
86881
+ { rule_id: "perplexity_api_key", regex: /\b(pplx-[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "perplexity_api_key" },
86882
+ { rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
86883
+ { rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
86884
+ { rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
86885
+ { rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
86886
+ { rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
86887
+ { rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
86888
+ ];
86889
+ var STRUCTURED_PATTERNS = [
86890
+ {
86891
+ rule_id: "env_key_value",
86892
+ regex: /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\b\s*[=:]\s*(["']?)([^\s"'\\]+)\2/g,
86893
+ captureIndex: 3,
86894
+ slugHint: "env"
86895
+ },
86896
+ {
86897
+ rule_id: "json_secret_field",
86898
+ regex: /"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"/g,
86899
+ captureIndex: 1,
86900
+ slugHint: "json_secret"
86901
+ },
86902
+ {
86903
+ rule_id: "cli_flag",
86904
+ regex: /--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1/g,
86905
+ captureIndex: 2,
86906
+ slugHint: "cli_flag"
86907
+ },
86908
+ {
86909
+ rule_id: "bearer_auth_header",
86910
+ regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
86911
+ captureIndex: 1,
86912
+ slugHint: "bearer_token"
86913
+ },
86914
+ {
86915
+ rule_id: "bearer_loose",
86916
+ regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
86917
+ captureIndex: 1,
86918
+ slugHint: "bearer_token"
86919
+ },
86920
+ {
86921
+ rule_id: "basic_auth_header",
86922
+ regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
86923
+ captureIndex: 1,
86924
+ slugHint: "basic_auth"
86925
+ },
86926
+ {
86927
+ rule_id: "pem_private_key",
86928
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
86929
+ captureIndex: 0,
86930
+ slugHint: "pem_private_key"
86931
+ }
86932
+ ];
86933
+ var PROVIDER_PATTERNS = [
86934
+ { rule_id: "slack_webhook", regex: /(https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_/]+)/g, captureIndex: 1, slugHint: "slack_webhook" },
86935
+ { rule_id: "stripe_live_secret", regex: /\b(sk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
86936
+ { rule_id: "stripe_restricted", regex: /\b(rk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
86937
+ { rule_id: "sendgrid_api_key", regex: /\b(SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "sendgrid_key" },
86938
+ { rule_id: "gitlab_pat", regex: /\b(glpat-[A-Za-z0-9_-]{20})\b/g, captureIndex: 1, slugHint: "gitlab_pat" },
86939
+ { rule_id: "huggingface_token", regex: /\b(hf_[A-Za-z0-9]{34,})\b/g, captureIndex: 1, slugHint: "huggingface_token" },
86940
+ { rule_id: "twilio_api_key", regex: /\b(SK[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "twilio_api_key" },
86941
+ { rule_id: "mailgun_key", regex: /\b(key-[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "mailgun_key" },
86942
+ { rule_id: "digitalocean_pat", regex: /\b(dop_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
86943
+ { rule_id: "digitalocean_oauth", regex: /\b(doo_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
86944
+ { rule_id: "digitalocean_refresh", regex: /\b(dor_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
86945
+ { rule_id: "doppler_token", regex: /\b(dp\.(?:pt|st|ct|sa|scim|audit)\.[A-Za-z0-9]{40,44})\b/g, captureIndex: 1, slugHint: "doppler_token" },
86946
+ { rule_id: "linear_api_key", regex: /\b(lin_api_[A-Za-z0-9]{40})\b/g, captureIndex: 1, slugHint: "linear_api_key" },
86947
+ { rule_id: "shopify_access_token", regex: /\b(shpat_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
86948
+ { rule_id: "shopify_shared_secret", regex: /\b(shpss_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
86949
+ { rule_id: "shopify_private_app", regex: /\b(shppa_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
86950
+ { rule_id: "square_access_token", regex: /\b(sq0atp-[A-Za-z0-9_-]{22})\b/g, captureIndex: 1, slugHint: "square_token" },
86951
+ { rule_id: "square_oauth_secret", regex: /\b(sq0csp-[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "square_token" },
86952
+ { rule_id: "newrelic_key", regex: /\b(NRAK-[A-Z0-9]{27})\b/g, captureIndex: 1, slugHint: "newrelic_key" },
86953
+ { rule_id: "notion_token", regex: /\b(ntn_[A-Za-z0-9]{46})\b/g, captureIndex: 1, slugHint: "notion_token" },
86954
+ { rule_id: "planetscale_password", regex: /\b(pscale_pw_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
86955
+ { rule_id: "planetscale_token", regex: /\b(pscale_tkn_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
86956
+ { rule_id: "supabase_service_key", regex: /\b(sbp_[a-f0-9]{40})\b/g, captureIndex: 1, slugHint: "supabase_key" },
86957
+ { rule_id: "atlassian_token", regex: /\b(ATATT[A-Za-z0-9_\-=]{20,})\b/g, captureIndex: 1, slugHint: "atlassian_token" },
86958
+ { rule_id: "dropbox_token", regex: /\b(sl\.[A-Za-z0-9_-]{130,})/g, captureIndex: 1, slugHint: "dropbox_token" },
86959
+ { rule_id: "databricks_token", regex: /\b(dapi[a-f0-9]{32})\b/g, captureIndex: 1, slugHint: "databricks_token" },
86960
+ { rule_id: "grafana_service_account", regex: /\b(glsa_[A-Za-z0-9]{32}_[a-fA-F0-9]{8})\b/g, captureIndex: 1, slugHint: "grafana_token" },
86961
+ { rule_id: "pypi_token", regex: /\b(pypi-AgEIcHlwaS[A-Za-z0-9_-]{50,})/g, captureIndex: 1, slugHint: "pypi_token" },
86962
+ { rule_id: "aws_temp_access_key", regex: /\b(ASIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
86963
+ { rule_id: "gcp_oauth_token", regex: /\b(ya29\.[A-Za-z0-9_-]{30,})/g, captureIndex: 1, slugHint: "gcp_oauth_token" }
86964
+ ];
86965
+ var ALL_PATTERNS = [...ANCHORED_PATTERNS, ...PROVIDER_PATTERNS, ...STRUCTURED_PATTERNS];
86966
+
86967
+ // telegram-plugin/secret-detect/entropy.ts
86968
+ function shannonEntropy(s) {
86969
+ if (s.length === 0)
86970
+ return 0;
86971
+ const counts = new Map;
86972
+ for (const ch of s) {
86973
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
86974
+ }
86975
+ let h = 0;
86976
+ const len = s.length;
86977
+ for (const c of counts.values()) {
86978
+ const p = c / len;
86979
+ h -= p * Math.log2(p);
86980
+ }
86981
+ return h;
86982
+ }
86983
+
86984
+ // telegram-plugin/secret-detect/inert-values.ts
86985
+ var INERT_VALUE_RE = [
86986
+ /^\[REDACTED(?::[A-Za-z0-9_]+)?\]$/i,
86987
+ /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/,
86988
+ /^\{\{[^\n\r\u2028\u2029]*\}\}$/,
86989
+ /^<[^<>\n\r\u2028\u2029]*>$/,
86990
+ /^%[A-Za-z_][A-Za-z0-9_]*%$/,
86991
+ /^vault:[A-Za-z0-9_./-]*$/i,
86992
+ /^[*x\u2022.]+$/i,
86993
+ /^(?:process\.env|import\.meta\.env|os\.environ)(?:\.[A-Za-z_$][A-Za-z0-9_$]*|\[["']?[A-Za-z_$][A-Za-z0-9_$]*["']?\])*$/,
86994
+ /^(?:changeme|change-me|replaceme|replace-me|placeholder|todo|tbd|yourkey|your-key|yourpassword|your-password|yoursecret|your-secret|yourtoken|your-token)(?:[-_][A-Za-z0-9-]+)?$/i,
86995
+ /^[a-z]+(?: [a-z]+){2,}$/
86996
+ ];
86997
+ var CREDENTIAL_RUN_RE = /[A-Za-z0-9]{12,}/g;
86998
+ var MIXED_CLASS_RUN_MIN = 12;
86999
+ var SINGLE_CLASS_RUN_MIN = 16;
87000
+ function runIsCredentialShaped(run) {
87001
+ let classes = 0;
87002
+ if (/[a-z]/.test(run))
87003
+ classes++;
87004
+ if (/[A-Z]/.test(run))
87005
+ classes++;
87006
+ if (/[0-9]/.test(run))
87007
+ classes++;
87008
+ return run.length >= (classes >= 2 ? MIXED_CLASS_RUN_MIN : SINGLE_CLASS_RUN_MIN);
87009
+ }
87010
+ function hasCredentialShapedRun(value) {
87011
+ for (const run of value.match(CREDENTIAL_RUN_RE) ?? []) {
87012
+ if (runIsCredentialShaped(run))
87013
+ return true;
87014
+ }
87015
+ return false;
87016
+ }
87017
+ function isInertValue(value) {
87018
+ if (!INERT_VALUE_RE.some((re) => re.test(value)))
87019
+ return false;
87020
+ return !hasCredentialShapedRun(value);
87021
+ }
87022
+ var INERT_GATED_RULES = new Set([
87023
+ "env_key_value",
87024
+ "json_secret_field",
87025
+ "cli_flag"
87026
+ ]);
87027
+ var TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
87028
+ function stripTrailingPunctuation(value) {
87029
+ return value.replace(TRAILING_PUNCT_RE, "");
87030
+ }
87031
+
87032
+ // telegram-plugin/secret-detect/kv-scanner.ts
87033
+ var KV_RE = /\b([A-Za-z_][A-Za-z0-9_-]*(?:password|passwd|token|secret|key|api[_-]?key))\s*[:=]\s*["']?([^\s"'\\]{8,})["']?/gi;
87034
+ var KV_ENTROPY_THRESHOLD = 4;
87035
+ var MEMORABLE_PW_RE = /\b([A-Za-z0-9_-]*(?:password|passwd|passphrase|pwd))\b\s*(?:[:=]\s*|\s+is\s+)(["']?)([^\s"']{8,64})\2/gi;
87036
+ var MEMORABLE_PW_RULE_ID = "memorable_password";
87037
+ var MEMORABLE_PW_MIN_CLASSES = 2;
87038
+ function charClassCount(value) {
87039
+ let n = 0;
87040
+ if (/[a-z]/.test(value))
87041
+ n++;
87042
+ if (/[A-Z]/.test(value))
87043
+ n++;
87044
+ if (/[0-9]/.test(value))
87045
+ n++;
87046
+ if (/[^A-Za-z0-9]/.test(value))
87047
+ n++;
87048
+ return n;
87049
+ }
87050
+ function looksLikeMemorablePassword(value) {
87051
+ if (isInertValue(value))
87052
+ return false;
87053
+ const core = stripTrailingPunctuation(value);
87054
+ if (core.length < 8 || core.length > 64)
87055
+ return false;
87056
+ if (isInertValue(core))
87057
+ return false;
87058
+ if (new Set(core).size < 4)
87059
+ return false;
87060
+ return charClassCount(core) >= MEMORABLE_PW_MIN_CLASSES;
87061
+ }
87062
+ function scanMemorablePasswords(text) {
87063
+ const hits = [];
87064
+ MEMORABLE_PW_RE.lastIndex = 0;
87065
+ let m;
87066
+ while ((m = MEMORABLE_PW_RE.exec(text)) !== null) {
87067
+ const keyName = m[1];
87068
+ const value = m[3];
87069
+ if (!value || !looksLikeMemorablePassword(value))
87070
+ continue;
87071
+ const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
87072
+ if (valueOffsetInMatch < 0)
87073
+ continue;
87074
+ const start = m.index + valueOffsetInMatch;
87075
+ hits.push({
87076
+ rule_id: MEMORABLE_PW_RULE_ID,
87077
+ start,
87078
+ end: start + value.length,
87079
+ matched_text: value,
87080
+ key_name: keyName,
87081
+ confidence: "ambiguous"
87082
+ });
87083
+ }
87084
+ return hits;
87085
+ }
87086
+ function scanKeyValue(text) {
87087
+ const hits = [];
87088
+ KV_RE.lastIndex = 0;
87089
+ let m;
87090
+ while ((m = KV_RE.exec(text)) !== null) {
87091
+ const [, keyName, value] = m;
87092
+ if (!value)
87093
+ continue;
87094
+ if (isInertValue(value))
87095
+ continue;
87096
+ const h = shannonEntropy(value);
87097
+ if (h < KV_ENTROPY_THRESHOLD)
87098
+ continue;
87099
+ const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
87100
+ if (valueOffsetInMatch < 0)
87101
+ continue;
87102
+ const start = m.index + valueOffsetInMatch;
87103
+ const end = start + value.length;
87104
+ hits.push({
87105
+ rule_id: "kv_entropy",
87106
+ start,
87107
+ end,
87108
+ matched_text: value,
87109
+ key_name: keyName,
87110
+ confidence: "ambiguous"
87111
+ });
87112
+ }
87113
+ return hits;
87114
+ }
87115
+
87116
+ // telegram-plugin/secret-detect/db-uri.ts
87117
+ var DB_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]+):\/\/([^\s:/@]+):([^\s/?#]+)@([^\s/@?#]+)/g;
87118
+ var DB_URI_RULE_ID = "db_uri_password";
87119
+ function isInertPassword(value) {
87120
+ if (value.startsWith("[REDACTED"))
87121
+ return true;
87122
+ return /^[*x]+$/i.test(value);
87123
+ }
87124
+ function scanDbUris(text) {
87125
+ const hits = [];
87126
+ DB_URI_RE.lastIndex = 0;
87127
+ let m;
87128
+ while ((m = DB_URI_RE.exec(text)) !== null) {
87129
+ const scheme = m[1];
87130
+ const user = m[2];
87131
+ const password = m[3];
87132
+ if (isInertPassword(password))
87133
+ continue;
87134
+ const start = m.index + scheme.length + 3 + user.length + 1;
87135
+ hits.push({
87136
+ rule_id: DB_URI_RULE_ID,
87137
+ start,
87138
+ end: start + password.length,
87139
+ matched_text: password,
87140
+ key_name: `${scheme}_password`,
87141
+ confidence: "ambiguous"
87142
+ });
87143
+ }
87144
+ return hits;
87145
+ }
87146
+
87147
+ // telegram-plugin/secret-detect/generic-entropy.ts
87148
+ var CANDIDATE_RE = /[A-Za-z0-9]{28,}/g;
87149
+ var GENERIC_MIN_DISTINCT = 18;
87150
+ var MAX_GENERIC_HITS = 20;
87151
+ function hasDistinctChars(tok, n) {
87152
+ const seen = new Uint8Array(128);
87153
+ let distinct = 0;
87154
+ for (let i = 0;i < tok.length; i++) {
87155
+ const c = tok.charCodeAt(i);
87156
+ if (seen[c] === 0) {
87157
+ seen[c] = 1;
87158
+ if (++distinct >= n)
87159
+ return true;
87160
+ }
87161
+ }
87162
+ return false;
87270
87163
  }
87271
- function printLinearInstructions(opts, vaultKey) {
87272
- const base = opts.webhookBase ?? "https://<your-switchroom-web-host>";
87273
- const webhookUrl = `${base.replace(/\/$/, "")}/webhook/${opts.agent}/linear`;
87274
- console.log("");
87275
- console.log(source_default.bold("Next steps in Linear (browser, one-time per agent):"));
87276
- console.log(source_default.gray(" 1. Create / open your Linear OAuth app with actor=app and scopes"));
87277
- console.log(source_default.gray(" app:mentionable + app:assignable (https://linear.app/developers/agents)."));
87278
- if (opts.clientId) {
87279
- const redirect = opts.redirectUri ?? `${base.replace(/\/$/, "")}/oauth/callback`;
87280
- const authorizeUrl = `https://linear.app/oauth/authorize?` + `client_id=${encodeURIComponent(opts.clientId)}` + `&redirect_uri=${encodeURIComponent(redirect)}` + `&response_type=code` + `&scope=${encodeURIComponent("read,write,app:assignable,app:mentionable")}` + `&actor=app`;
87281
- console.log(source_default.gray(" 2. Authorize the app as an actor (open in a browser):"));
87282
- console.log(source_default.cyan(` ${authorizeUrl}`));
87283
- console.log(source_default.gray(" The redirect delivers the app token you pass to this verb via --token."));
87284
- } else {
87285
- console.log(source_default.gray(" 2. Authorize the app (actor=app) in a browser; capture the app token and"));
87286
- console.log(source_default.gray(" re-run this verb with --token (and optionally --client-id to print the"));
87287
- console.log(source_default.gray(" authorize URL)."));
87164
+ function hasDigit(tok) {
87165
+ for (let i = 0;i < tok.length; i++) {
87166
+ const c = tok.charCodeAt(i);
87167
+ if (c >= 48 && c <= 57)
87168
+ return true;
87288
87169
  }
87289
- console.log(source_default.gray(" 3. Register this webhook URL on the app (AgentSessionEvent + Issue/Comment):"));
87290
- console.log(source_default.cyan(` ${webhookUrl}`));
87291
- console.log(source_default.gray(" Use Linear's signing secret as the webhook secret \u2014 store it in the vault " + `under webhook/${opts.agent}/linear (e.g. 'switchroom telegram enable webhook ` + `--agent ${opts.agent} --source linear --secret <signing-secret>').`));
87292
- console.log("");
87293
- console.log(source_default.gray(` Token is read at runtime from vault:${vaultKey} (actor=app).`));
87170
+ return false;
87294
87171
  }
87295
- function makeUnifiedDiff2(before, after) {
87296
- const a = before.split(`
87297
- `);
87298
- const b = after.split(`
87299
- `);
87300
- const out = [];
87301
- let i = 0, j = 0;
87302
- while (i < a.length || j < b.length) {
87303
- if (i < a.length && j < b.length && a[i] === b[j]) {
87304
- out.push(` ${a[i]}`);
87305
- i++;
87306
- j++;
87307
- } else if (j < b.length && (i >= a.length || a[i] !== b[j])) {
87308
- out.push(source_default.green(`+ ${b[j]}`));
87309
- j++;
87310
- } else {
87311
- out.push(source_default.red(`- ${a[i]}`));
87312
- i++;
87313
- }
87172
+ function scanGenericSecrets(text) {
87173
+ const hits = [];
87174
+ CANDIDATE_RE.lastIndex = 0;
87175
+ let m;
87176
+ while ((m = CANDIDATE_RE.exec(text)) !== null) {
87177
+ if (hits.length >= MAX_GENERIC_HITS)
87178
+ break;
87179
+ const tok = m[0];
87180
+ if (!hasDigit(tok))
87181
+ continue;
87182
+ if (!hasDistinctChars(tok, GENERIC_MIN_DISTINCT))
87183
+ continue;
87184
+ hits.push({
87185
+ rule_id: "generic_high_entropy",
87186
+ start: m.index,
87187
+ end: m.index + tok.length,
87188
+ matched_text: tok,
87189
+ confidence: "ambiguous"
87190
+ });
87314
87191
  }
87315
- return out.join(`
87316
- `);
87192
+ return hits;
87317
87193
  }
87318
- function fail3(msg) {
87319
- console.error(source_default.red(`Error: ${msg}`));
87320
- process.exit(1);
87194
+
87195
+ // telegram-plugin/secret-detect/chunker.ts
87196
+ var CHUNK_THRESHOLD = 32 * 1024;
87197
+ var WINDOW_SIZE = 16 * 1024;
87198
+ var OVERLAP = 8 * 1024;
87199
+ function chunk(text) {
87200
+ if (text.length <= CHUNK_THRESHOLD) {
87201
+ return [{ offset: 0, text }];
87202
+ }
87203
+ const out = [];
87204
+ let offset = 0;
87205
+ while (offset < text.length) {
87206
+ const end = Math.min(offset + WINDOW_SIZE, text.length);
87207
+ out.push({ offset, text: text.slice(offset, end) });
87208
+ if (end >= text.length)
87209
+ break;
87210
+ offset = end - OVERLAP;
87211
+ }
87212
+ return out;
87321
87213
  }
87322
87214
 
87323
- // src/cli/memory.ts
87324
- init_source();
87325
- init_hindsight2();
87215
+ // telegram-plugin/secret-detect/suppressor.ts
87216
+ var MARKERS = ["test", "mock", "example", "fixture", "dummy"];
87217
+ var WINDOW = 40;
87218
+ var MARKER_RE = new RegExp(`\\b(?:${MARKERS.join("|")})\\b`, "i");
87219
+ function isSuppressed(text, start, end) {
87220
+ const left = Math.max(0, start - WINDOW);
87221
+ const right = Math.min(text.length, end + WINDOW);
87222
+ const context = text.slice(left, start) + text.slice(end, right);
87223
+ return MARKER_RE.test(context);
87224
+ }
87326
87225
 
87327
- // src/memory/search.ts
87328
- init_hindsight2();
87329
- function shellQuote2(s) {
87330
- return "'" + s.replace(/'/g, `'"'"'`) + "'";
87226
+ // telegram-plugin/secret-detect/slug.ts
87227
+ function sanitizeKeyName(raw) {
87228
+ const up = raw.toUpperCase();
87229
+ const cleaned = up.replace(/[^A-Z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
87230
+ return cleaned.length > 0 ? cleaned : "SECRET";
87331
87231
  }
87332
- function searchMemory(query, collection) {
87333
- return `hindsight recall --collection ${shellQuote2(collection)} --query ${shellQuote2(query)}`;
87232
+ function datePart(now = new Date) {
87233
+ const y = now.getUTCFullYear();
87234
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
87235
+ const d = String(now.getUTCDate()).padStart(2, "0");
87236
+ return `${y}${m}${d}`;
87334
87237
  }
87335
- function getMemoryStats(collection) {
87336
- return `hindsight stats --collection ${shellQuote2(collection)}`;
87238
+ function deriveSlug(inputs, existing) {
87239
+ let base;
87240
+ if (inputs.key_name && inputs.key_name.trim().length > 0) {
87241
+ base = sanitizeKeyName(inputs.key_name);
87242
+ } else {
87243
+ base = `${inputs.rule_id}_${datePart(inputs.now)}`;
87244
+ }
87245
+ if (!existing.has(base))
87246
+ return base;
87247
+ let n = 2;
87248
+ while (existing.has(`${base}_${n}`))
87249
+ n++;
87250
+ return `${base}_${n}`;
87337
87251
  }
87338
- function reflectAcrossAgents(config) {
87339
- const eligible = [];
87340
- const excluded = [];
87341
- for (const agentName of Object.keys(config.agents)) {
87342
- const collection = getCollectionForAgent(agentName, config);
87343
- if (isStrictIsolation(agentName, config)) {
87344
- excluded.push({ agent: agentName, collection });
87345
- } else {
87346
- eligible.push({ agent: agentName, collection });
87252
+ // telegram-plugin/secret-detect/url-redact.ts
87253
+ var SENSITIVE_PARAMS = new Set([
87254
+ "token",
87255
+ "key",
87256
+ "api_key",
87257
+ "apikey",
87258
+ "secret",
87259
+ "access_token",
87260
+ "password",
87261
+ "pass",
87262
+ "auth",
87263
+ "client_secret",
87264
+ "refresh_token",
87265
+ "signature"
87266
+ ]);
87267
+ var URL_RE = /\b(?:https?|wss?|ftp):\/\/[^\s<>"']+/gi;
87268
+ function redactUrls(text) {
87269
+ return text.replace(URL_RE, (m) => {
87270
+ const redacted = redactOne(m);
87271
+ return redacted ?? m;
87272
+ });
87273
+ }
87274
+ function redactOne(raw) {
87275
+ let u;
87276
+ try {
87277
+ u = new URL(raw);
87278
+ } catch {
87279
+ return null;
87280
+ }
87281
+ let changed = false;
87282
+ if (u.username || u.password) {
87283
+ u.username = "***";
87284
+ u.password = "";
87285
+ changed = true;
87286
+ }
87287
+ for (const [key] of u.searchParams) {
87288
+ if (SENSITIVE_PARAMS.has(key.toLowerCase())) {
87289
+ u.searchParams.set(key, "***");
87290
+ changed = true;
87347
87291
  }
87348
87292
  }
87349
- const commands = eligible.map(({ collection }) => `hindsight reflect --collection ${shellQuote2(collection)}`);
87350
- return { eligible, excluded, commands };
87293
+ return changed ? u.toString() : null;
87351
87294
  }
87352
87295
 
87353
- // src/memory/observation-scope-cleanup.ts
87354
- var CLEANUP_VOLATILE_PATTERNS = [
87355
- /^parent_session:/,
87356
- /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(-sub-.+)?$/
87357
- ];
87358
- var CLEANUP_STALE_PATTERNS = [/^agent-[0-9a-f]{8,}$/];
87359
- function matchesAny(tag, patterns) {
87360
- return patterns.some((p) => p.test(tag));
87361
- }
87362
- function scopeKey(tags) {
87363
- return tags.length === 0 ? "shared" : [...tags].sort().join(",");
87364
- }
87365
- function planScopeCleanup(bankId, scopes, options = {}) {
87366
- const volatilePatterns = options.volatilePatterns ?? CLEANUP_VOLATILE_PATTERNS;
87367
- const stalePatterns = options.stalePatterns ?? CLEANUP_STALE_PATTERNS;
87368
- const retireStale = options.retireStale ?? false;
87369
- const totalScopes = scopes.length;
87370
- const totalObservations = scopes.reduce((n, s) => n + s.count, 0);
87371
- const reclassified = [];
87372
- const breakdown = {
87373
- allVolatileScopes: 0,
87374
- allVolatileObservations: 0,
87375
- mixedScopes: 0,
87376
- mixedObservations: 0,
87377
- staleScopes: 0,
87378
- staleObservations: 0
87379
- };
87380
- const targets = new Map;
87381
- for (const s of scopes) {
87382
- const volatileTags = s.tags.filter((t) => matchesAny(t, volatilePatterns));
87383
- const staleTags = s.tags.filter((t) => matchesAny(t, stalePatterns));
87384
- const stripped = s.tags.filter((t) => {
87385
- if (matchesAny(t, volatilePatterns))
87386
- return false;
87387
- if (retireStale && matchesAny(t, stalePatterns))
87388
- return false;
87389
- return true;
87390
- });
87391
- const toKey = scopeKey(stripped);
87392
- const changed = scopeKey(s.tags) !== toKey;
87393
- const hasStable = s.tags.some((t) => !matchesAny(t, volatilePatterns) && !matchesAny(t, stalePatterns));
87394
- if (staleTags.length > 0) {
87395
- breakdown.staleScopes += 1;
87396
- breakdown.staleObservations += s.count;
87296
+ // telegram-plugin/secret-detect/index.ts
87297
+ function detectSecrets(text) {
87298
+ if (!text || text.length === 0)
87299
+ return [];
87300
+ const windows = chunk(text);
87301
+ const raw = [];
87302
+ for (const win of windows) {
87303
+ for (const p of ALL_PATTERNS) {
87304
+ const re = new RegExp(p.regex.source, p.regex.flags.includes("g") ? p.regex.flags : p.regex.flags + "g");
87305
+ let m;
87306
+ while ((m = re.exec(win.text)) !== null) {
87307
+ if (m[0].length === 0) {
87308
+ re.lastIndex++;
87309
+ continue;
87310
+ }
87311
+ const cap = p.captureIndex === 0 ? m[0] : m[p.captureIndex];
87312
+ if (!cap)
87313
+ continue;
87314
+ const matchStart = p.captureIndex === 0 ? m.index : m.index + m[0].indexOf(cap);
87315
+ if (matchStart < 0)
87316
+ continue;
87317
+ const globalStart = win.offset + matchStart;
87318
+ const globalEnd = globalStart + cap.length;
87319
+ const keyName = p.rule_id === "env_key_value" ? m[1] : undefined;
87320
+ if (INERT_GATED_RULES.has(p.rule_id) && isInertValue(cap))
87321
+ continue;
87322
+ if (p.rule_id === "env_key_value") {
87323
+ const ENV_KV_MIN_LEN = 12;
87324
+ const ENV_KV_MIN_ENTROPY = 3.5;
87325
+ if (cap.length < ENV_KV_MIN_LEN)
87326
+ continue;
87327
+ if (shannonEntropy(cap) < ENV_KV_MIN_ENTROPY)
87328
+ continue;
87329
+ }
87330
+ raw.push({
87331
+ rule_id: p.rule_id,
87332
+ start: globalStart,
87333
+ end: globalEnd,
87334
+ matched_text: cap,
87335
+ key_name: keyName,
87336
+ confidence: "high"
87337
+ });
87338
+ }
87397
87339
  }
87398
- if (volatileTags.length > 0 && !hasStable && staleTags.length === 0) {
87399
- breakdown.allVolatileScopes += 1;
87400
- breakdown.allVolatileObservations += s.count;
87401
- } else if (volatileTags.length > 0 && hasStable) {
87402
- breakdown.mixedScopes += 1;
87403
- breakdown.mixedObservations += s.count;
87340
+ const kvHits = scanKeyValue(win.text);
87341
+ for (const h of kvHits) {
87342
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
87404
87343
  }
87405
- const entry = targets.get(toKey) ?? { sources: 0, observations: 0, changed: false };
87406
- entry.sources += 1;
87407
- entry.observations += s.count;
87408
- entry.changed = entry.changed || changed;
87409
- targets.set(toKey, entry);
87410
- if (changed) {
87411
- reclassified.push({
87412
- from: s.tags,
87413
- to: toKey,
87414
- count: s.count,
87415
- strippedVolatile: volatileTags,
87416
- strippedStale: retireStale ? staleTags : []
87417
- });
87344
+ for (const h of scanDbUris(win.text)) {
87345
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
87346
+ }
87347
+ for (const h of scanMemorablePasswords(win.text)) {
87348
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
87349
+ }
87350
+ const genHits = scanGenericSecrets(win.text);
87351
+ for (const h of genHits) {
87352
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
87418
87353
  }
87419
87354
  }
87420
- reclassified.sort((a, b) => b.count - a.count);
87421
- const merges = [...targets.entries()].filter(([, v]) => v.sources > 1 && v.changed).map(([target, v]) => ({ target, sourceScopes: v.sources, observations: v.observations })).sort((a, b) => b.observations - a.observations);
87422
- const affectedObservations = reclassified.reduce((n, r) => n + r.count, 0);
87423
- const resultingScopeCount = targets.size;
87424
- return {
87425
- bankId,
87426
- ok: true,
87427
- totalScopes,
87428
- totalObservations,
87429
- reclassified,
87430
- merges,
87431
- affectedScopes: reclassified.length,
87432
- affectedObservations,
87433
- resultingScopeCount,
87434
- scopeReduction: totalScopes - resultingScopeCount,
87435
- breakdown
87436
- };
87355
+ const deduped = dedupeRaw(raw);
87356
+ const final = dropOverlaps(deduped);
87357
+ const existing = new Set;
87358
+ const out = [];
87359
+ for (const h of final) {
87360
+ const suggested_slug = deriveSlug({ key_name: h.key_name, rule_id: h.rule_id }, existing);
87361
+ existing.add(suggested_slug);
87362
+ out.push({
87363
+ rule_id: h.rule_id,
87364
+ matched_text: h.matched_text,
87365
+ start: h.start,
87366
+ end: h.end,
87367
+ confidence: h.confidence,
87368
+ suppressed: isSuppressed(text, h.start, h.end),
87369
+ suggested_slug,
87370
+ key_name: h.key_name
87371
+ });
87372
+ }
87373
+ out.sort((a, b) => a.start - b.start);
87374
+ return out;
87437
87375
  }
87438
- async function getJson2(url, opts) {
87439
- const fetchImpl = opts?.fetchImpl ?? fetch;
87440
- const timeoutMs = opts?.timeoutMs ?? 15000;
87441
- const controller = new AbortController;
87442
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
87443
- try {
87444
- const resp = await fetchImpl(url, { signal: controller.signal });
87445
- clearTimeout(timeout);
87446
- if (!resp.ok)
87447
- return { ok: false, reason: `HTTP ${resp.status}` };
87448
- return { ok: true, data: await resp.json() };
87449
- } catch (err) {
87450
- clearTimeout(timeout);
87451
- if (err.name === "AbortError")
87452
- return { ok: false, reason: "Timeout" };
87453
- return { ok: false, reason: String(err.message ?? err) };
87376
+ function dedupeRaw(raw) {
87377
+ const seen = new Map;
87378
+ for (const h of raw) {
87379
+ const key = `${h.start}:${h.end}`;
87380
+ const existing = seen.get(key);
87381
+ if (!existing) {
87382
+ seen.set(key, h);
87383
+ continue;
87384
+ }
87385
+ if (existing.confidence === "ambiguous" && h.confidence === "high") {
87386
+ seen.set(key, h);
87387
+ }
87454
87388
  }
87389
+ return Array.from(seen.values());
87455
87390
  }
87456
- async function planBankCleanup(apiUrl, bankId, options = {}, fetchOpts) {
87457
- const base = hindsightRestBase(apiUrl);
87458
- const bank = encodeURIComponent(bankId);
87459
- const resp = await getJson2(`${base}/v1/default/banks/${bank}/observations/scopes`, fetchOpts);
87460
- if (!resp.ok) {
87461
- return {
87462
- bankId,
87463
- ok: false,
87464
- reason: resp.reason,
87465
- totalScopes: 0,
87466
- totalObservations: 0,
87467
- reclassified: [],
87468
- merges: [],
87469
- affectedScopes: 0,
87470
- affectedObservations: 0,
87471
- resultingScopeCount: 0,
87472
- scopeReduction: 0,
87473
- breakdown: {
87474
- allVolatileScopes: 0,
87475
- allVolatileObservations: 0,
87476
- mixedScopes: 0,
87477
- mixedObservations: 0,
87478
- staleScopes: 0,
87479
- staleObservations: 0
87480
- }
87481
- };
87391
+ function dropOverlaps(hits) {
87392
+ const out = hits.filter((h) => !(h.confidence === "ambiguous" && hits.some((o) => o !== h && o.start <= h.start && o.end >= h.end && !(o.start === h.start && o.end === h.end))));
87393
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
87394
+ return out;
87395
+ }
87396
+
87397
+ // telegram-plugin/secret-detect/redact.ts
87398
+ var REDACTED_MARKER = "[REDACTED]";
87399
+ function redact(text) {
87400
+ if (!text || text.length === 0)
87401
+ return text;
87402
+ const urlScrubbed = redactUrls(text);
87403
+ const hits = detectSecrets(urlScrubbed).filter((h) => h.rule_id !== "generic_high_entropy");
87404
+ if (hits.length === 0)
87405
+ return urlScrubbed;
87406
+ const sorted = [...hits].sort((a, b) => b.start - a.start);
87407
+ let out = urlScrubbed;
87408
+ for (const h of sorted) {
87409
+ out = out.slice(0, h.start) + redactedMarker(h.rule_id) + out.slice(h.end);
87482
87410
  }
87483
- const raw = resp.data?.scopes;
87484
- const scopes = [];
87485
- if (Array.isArray(raw)) {
87486
- for (const entry of raw) {
87487
- const tags = entry?.tags;
87488
- const count = entry?.count;
87489
- if (!Array.isArray(tags) || typeof count !== "number")
87490
- continue;
87491
- if (!tags.every((t) => typeof t === "string"))
87492
- continue;
87493
- scopes.push({ tags, count });
87494
- }
87411
+ return out;
87412
+ }
87413
+ function redactedMarker(ruleId) {
87414
+ const trimmed = ruleId.replace(/^(kv|env)_/, "");
87415
+ if (!trimmed || trimmed === "key_value" || trimmed === "kv_entropy") {
87416
+ return REDACTED_MARKER;
87495
87417
  }
87496
- return planScopeCleanup(bankId, scopes, options);
87418
+ return `[REDACTED:${trimmed}]`;
87497
87419
  }
87498
- function formatCleanupPlan(plan, maxMerges = 10) {
87499
- if (!plan.ok) {
87500
- return `bank ${plan.bankId}: UNREADABLE (${plan.reason ?? "unknown"})`;
87420
+ // src/memory/hindsight-write-redaction.ts
87421
+ function redactJsonStrings(value) {
87422
+ if (typeof value === "string")
87423
+ return redact(value);
87424
+ if (Array.isArray(value))
87425
+ return value.map(redactJsonStrings);
87426
+ if (value && typeof value === "object") {
87427
+ const out = {};
87428
+ for (const [k, v] of Object.entries(value)) {
87429
+ out[k] = redactJsonStrings(v);
87430
+ }
87431
+ return out;
87501
87432
  }
87502
- const b = plan.breakdown;
87503
- const lines = [];
87504
- lines.push(`bank ${plan.bankId}`);
87505
- lines.push(` today: ${plan.totalScopes} scopes, ${plan.totalObservations.toLocaleString()} observations`);
87506
- lines.push(` legacy fragmentation:` + ` all-volatile ${b.allVolatileScopes} scopes / ${b.allVolatileObservations.toLocaleString()} obs;` + ` mixed ${b.mixedScopes} scopes / ${b.mixedObservations.toLocaleString()} obs;` + ` stale agent-<hex> ${b.staleScopes} scopes / ${b.staleObservations.toLocaleString()} obs`);
87507
- lines.push(` WOULD change: ${plan.affectedScopes} scopes` + ` (${plan.affectedObservations.toLocaleString()} obs re-homed),` + ` ${plan.totalScopes} \u2192 ${plan.resultingScopeCount} scopes` + ` (\u2212${plan.scopeReduction})`);
87508
- if (plan.merges.length > 0) {
87509
- lines.push(` top merges:`);
87510
- for (const m of plan.merges.slice(0, maxMerges)) {
87511
- lines.push(` ${m.sourceScopes} scopes \u2192 ${m.target} (${m.observations.toLocaleString()} obs)`);
87433
+ return value;
87434
+ }
87435
+ var REDACTED_ITEM_FIELDS = ["content", "context"];
87436
+ function redactMemoryWriteBody(body) {
87437
+ if (!body || typeof body !== "object" || Array.isArray(body))
87438
+ return body;
87439
+ const src = body;
87440
+ if (!Array.isArray(src.items))
87441
+ return body;
87442
+ const items = src.items.map((item) => {
87443
+ if (!item || typeof item !== "object" || Array.isArray(item))
87444
+ return item;
87445
+ const row = { ...item };
87446
+ for (const field of REDACTED_ITEM_FIELDS) {
87447
+ if (typeof row[field] === "string") {
87448
+ row[field] = redact(row[field]);
87449
+ }
87512
87450
  }
87513
- if (plan.merges.length > maxMerges) {
87514
- lines.push(` +${plan.merges.length - maxMerges} more merge targets`);
87451
+ if (row.metadata && typeof row.metadata === "object") {
87452
+ row.metadata = redactJsonStrings(row.metadata);
87515
87453
  }
87516
- }
87517
- return lines.join(`
87518
- `);
87454
+ return row;
87455
+ });
87456
+ return { ...src, items };
87519
87457
  }
87520
87458
 
87521
87459
  // src/cli/memory.ts
@@ -97162,14 +97100,10 @@ function registerHandoffCommand(program3) {
97162
97100
  jsonlPath: jsonl,
97163
97101
  agentDir,
97164
97102
  agentName,
97165
- maxTurns: cappedMaxTurns,
97166
- observationScopes: agentConfig?.memory?.observation_scopes
97103
+ maxTurns: cappedMaxTurns
97167
97104
  });
97168
97105
  process.stderr.write(`handoff: ${status}
97169
97106
  `);
97170
- if (status === HANDOFF_STATUS_MIRROR_SKIPPED) {
97171
- process.exitCode = 1;
97172
- }
97173
97107
  const retention = pruneSessionJsonl(claudeConfigDir, {
97174
97108
  maxCount: continuity?.session_retention_max_count ?? DEFAULT_SESSION_RETENTION_MAX_COUNT,
97175
97109
  maxAgeDays: continuity?.session_retention_max_age_days ?? DEFAULT_SESSION_RETENTION_MAX_AGE_DAYS,