switchroom 0.12.27 → 0.12.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cli/switchroom.js +4 -2
  2. package/package.json +2 -1
  3. package/telegram-plugin/dist/gateway/gateway.js +49 -5
  4. package/telegram-plugin/gateway/gateway.ts +5 -0
  5. package/telegram-plugin/stderr-timestamps.ts +106 -0
  6. package/telegram-plugin/tests/stderr-timestamps.test.ts +113 -0
  7. package/vendor/hindsight-memory/.claude-plugin/plugin.json +8 -0
  8. package/vendor/hindsight-memory/CHANGELOG.md +32 -0
  9. package/vendor/hindsight-memory/LICENSE +21 -0
  10. package/vendor/hindsight-memory/README.md +329 -0
  11. package/vendor/hindsight-memory/hooks/hooks.json +49 -0
  12. package/vendor/hindsight-memory/scripts/drain_pending.py +190 -0
  13. package/vendor/hindsight-memory/scripts/lib/__init__.py +0 -0
  14. package/vendor/hindsight-memory/scripts/lib/bank.py +122 -0
  15. package/vendor/hindsight-memory/scripts/lib/client.py +204 -0
  16. package/vendor/hindsight-memory/scripts/lib/config.py +180 -0
  17. package/vendor/hindsight-memory/scripts/lib/content.py +493 -0
  18. package/vendor/hindsight-memory/scripts/lib/daemon.py +334 -0
  19. package/vendor/hindsight-memory/scripts/lib/directives.py +119 -0
  20. package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +126 -0
  21. package/vendor/hindsight-memory/scripts/lib/llm.py +146 -0
  22. package/vendor/hindsight-memory/scripts/lib/pending.py +218 -0
  23. package/vendor/hindsight-memory/scripts/lib/state.py +196 -0
  24. package/vendor/hindsight-memory/scripts/recall.py +873 -0
  25. package/vendor/hindsight-memory/scripts/retain.py +286 -0
  26. package/vendor/hindsight-memory/scripts/session_end.py +122 -0
  27. package/vendor/hindsight-memory/scripts/session_start.py +76 -0
  28. package/vendor/hindsight-memory/scripts/setup_hooks.py +115 -0
  29. package/vendor/hindsight-memory/scripts/tests/__init__.py +0 -0
  30. package/vendor/hindsight-memory/scripts/tests/test_directives.py +211 -0
  31. package/vendor/hindsight-memory/scripts/tests/test_gateway_ipc.py +205 -0
  32. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +621 -0
  33. package/vendor/hindsight-memory/settings.json +37 -0
  34. package/vendor/hindsight-memory/skills/setup.md +24 -0
  35. package/vendor/hindsight-memory/tests/conftest.py +94 -0
  36. package/vendor/hindsight-memory/tests/test_bank.py +142 -0
  37. package/vendor/hindsight-memory/tests/test_client.py +232 -0
  38. package/vendor/hindsight-memory/tests/test_config.py +128 -0
  39. package/vendor/hindsight-memory/tests/test_content.py +471 -0
  40. package/vendor/hindsight-memory/tests/test_drain_pending.py +192 -0
  41. package/vendor/hindsight-memory/tests/test_hooks.py +808 -0
  42. package/vendor/hindsight-memory/tests/test_manifest.py +14 -0
  43. package/vendor/hindsight-memory/tests/test_pending.py +152 -0
  44. package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +325 -0
  45. package/vendor/hindsight-memory/tests/test_session_end_pending.py +205 -0
  46. package/vendor/hindsight-memory/tests/test_state.py +125 -0
@@ -47247,8 +47247,8 @@ var {
47247
47247
  } = import__.default;
47248
47248
 
47249
47249
  // src/build-info.ts
47250
- var VERSION = "0.12.27";
47251
- var COMMIT_SHA = "64fb245d";
47250
+ var VERSION = "0.12.28";
47251
+ var COMMIT_SHA = "61036e48";
47252
47252
 
47253
47253
  // src/cli/agent.ts
47254
47254
  init_source();
@@ -48576,6 +48576,8 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
48576
48576
  return null;
48577
48577
  const sourcePath = resolveHindsightVendorPath();
48578
48578
  if (!existsSync11(sourcePath)) {
48579
+ process.stderr.write(`installHindsightPlugin: vendor source missing at ${sourcePath} ` + `\u2014 hindsight plugin NOT installed for ${agentName}. ` + `Likely a packaging regression: check the npm tarball's files array.
48580
+ `);
48579
48581
  return null;
48580
48582
  }
48581
48583
  const destPath = join8(agentDir, ".claude", "plugins", "hindsight-memory");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.12.27",
3
+ "version": "0.12.28",
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": {
@@ -13,6 +13,7 @@
13
13
  "profiles",
14
14
  "skills",
15
15
  "telegram-plugin",
16
+ "vendor",
16
17
  "bin",
17
18
  "README.md",
18
19
  "LICENSE"
@@ -29800,6 +29800,49 @@ function installPluginLogger(env = process.env) {
29800
29800
  return activeHandle;
29801
29801
  }
29802
29802
 
29803
+ // stderr-timestamps.ts
29804
+ var installed = false;
29805
+ var originalWrite = null;
29806
+ var partialBuffer = "";
29807
+ function isoTimestamp() {
29808
+ return new Date().toISOString();
29809
+ }
29810
+ function installStderrTimestamps(env = process.env) {
29811
+ if (env.SWITCHROOM_LOG_TIMESTAMPS === "0")
29812
+ return false;
29813
+ if (installed)
29814
+ return true;
29815
+ const origin = process.stderr.write.bind(process.stderr);
29816
+ originalWrite = origin;
29817
+ const wrapped = function write(chunk, encodingOrCb, cb) {
29818
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
29819
+ const stamped = stampLines(text);
29820
+ return origin(stamped, encodingOrCb, cb);
29821
+ };
29822
+ process.stderr.write = wrapped;
29823
+ installed = true;
29824
+ return true;
29825
+ }
29826
+ function stampLines(text, now = isoTimestamp) {
29827
+ if (text === "")
29828
+ return "";
29829
+ let out = "";
29830
+ let i = 0;
29831
+ while (i < text.length) {
29832
+ const nl = text.indexOf(`
29833
+ `, i);
29834
+ if (nl === -1) {
29835
+ partialBuffer += text.slice(i);
29836
+ break;
29837
+ }
29838
+ const line = partialBuffer + text.slice(i, nl + 1);
29839
+ partialBuffer = "";
29840
+ out += `[${now()}] ${line}`;
29841
+ i = nl + 1;
29842
+ }
29843
+ return out;
29844
+ }
29845
+
29803
29846
  // dm-command-gate.ts
29804
29847
  function decideDmCommandGate(input) {
29805
29848
  if (input.chatType !== "private")
@@ -47472,11 +47515,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
47472
47515
  }
47473
47516
 
47474
47517
  // ../src/build-info.ts
47475
- var VERSION = "0.12.27";
47476
- var COMMIT_SHA = "64fb245d";
47477
- var COMMIT_DATE = "2026-05-20T07:46:30Z";
47478
- var LATEST_PR = 1588;
47479
- var COMMITS_AHEAD_OF_TAG = 0;
47518
+ var VERSION = "0.12.28";
47519
+ var COMMIT_SHA = "61036e48";
47520
+ var COMMIT_DATE = "2026-05-20T14:43:34Z";
47521
+ var LATEST_PR = 1592;
47522
+ var COMMITS_AHEAD_OF_TAG = 4;
47480
47523
 
47481
47524
  // gateway/boot-version.ts
47482
47525
  function formatRelativeAgo(iso) {
@@ -47963,6 +48006,7 @@ function resolveCallingSubagent(opts) {
47963
48006
 
47964
48007
  // gateway/gateway.ts
47965
48008
  var REPLY_TO_TEXT_MAX = 200;
48009
+ installStderrTimestamps();
47966
48010
  installPluginLogger();
47967
48011
  installGlobalErrorHandlers();
47968
48012
  process.on("beforeExit", () => {
@@ -23,6 +23,7 @@ import { homedir } from 'os'
23
23
  import { join, extname, sep, basename } from 'path'
24
24
 
25
25
  import { installPluginLogger } from '../plugin-logger.js'
26
+ import { installStderrTimestamps } from '../stderr-timestamps.js'
26
27
  import { decideDmCommandGate } from '../dm-command-gate.js'
27
28
  import { redactAuthCodeMessage } from '../auth-code-redact.js'
28
29
  import {
@@ -380,6 +381,10 @@ import { formatIdleFooter } from '../idle-footer.js'
380
381
  import { resolveCallingSubagent } from './resolve-calling-subagent.js'
381
382
 
382
383
  // ─── Stderr logging ───────────────────────────────────────────────────────
384
+ // Install the line-stamper FIRST so it wraps closest to the original
385
+ // stderr.write. plugin-logger's file mirror then sees the timestamped text.
386
+ // Kill switch: SWITCHROOM_LOG_TIMESTAMPS=0 disables.
387
+ installStderrTimestamps()
383
388
  installPluginLogger()
384
389
 
385
390
  // ─── Telemetry ────────────────────────────────────────────────────────────
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Per-line timestamp wrapper for `process.stderr.write`.
3
+ *
4
+ * The gateway's stderr is captured to `/var/log/switchroom/gateway-supervisor.log`
5
+ * by `start.sh`'s `_switchroom_supervise` redirect. The capture has NO
6
+ * line-level timestamps, which makes it impossible to measure the gap between
7
+ * events (e.g., `bridge registered` → first `dispatch stage=bridge_recover` →
8
+ * first `tg-post method=sendMessage`). Without those gaps the cold-start TTFO
9
+ * RFC's optimization claims (PR #1589) are unverifiable.
10
+ *
11
+ * This module installs a one-time wrapper on `process.stderr.write` that
12
+ * prepends an ISO-8601 timestamp (`[YYYY-MM-DDTHH:MM:SS.mmmZ]`) at the start
13
+ * of each logical line. Line-buffered: partial writes that don't end in `\n`
14
+ * are buffered until they do. Newlines mid-chunk split the chunk into
15
+ * multiple timestamped lines.
16
+ *
17
+ * Layered separately from `plugin-logger.ts`'s file mirror so each can be
18
+ * toggled independently. Order at install time: this wrapper runs FIRST
19
+ * (closest to the original write), then plugin-logger's file mirror sees
20
+ * the timestamped text.
21
+ *
22
+ * Kill switch: `SWITCHROOM_LOG_TIMESTAMPS=0` disables. Default ON.
23
+ */
24
+
25
+ let installed = false
26
+ let originalWrite: typeof process.stderr.write | null = null
27
+ let partialBuffer = ''
28
+
29
+ function isoTimestamp(): string {
30
+ return new Date().toISOString()
31
+ }
32
+
33
+ /**
34
+ * Wrap `process.stderr.write` to prepend an ISO timestamp at each line
35
+ * boundary. Idempotent — second call is a no-op.
36
+ *
37
+ * Returns true when the wrapper was installed (or was already), false when
38
+ * the kill-switch env var disabled it.
39
+ */
40
+ export function installStderrTimestamps(env: NodeJS.ProcessEnv = process.env): boolean {
41
+ if (env.SWITCHROOM_LOG_TIMESTAMPS === '0') return false
42
+ if (installed) return true
43
+
44
+ const origin = process.stderr.write.bind(process.stderr)
45
+ originalWrite = origin as typeof process.stderr.write
46
+
47
+ const wrapped = function write(
48
+ chunk: string | Uint8Array,
49
+ encodingOrCb?: BufferEncoding | ((err?: Error) => void),
50
+ cb?: (err?: Error) => void,
51
+ ): boolean {
52
+ const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
53
+ const stamped = stampLines(text)
54
+ return (origin as (c: unknown, e?: unknown, cb?: unknown) => boolean)(
55
+ stamped,
56
+ encodingOrCb,
57
+ cb,
58
+ )
59
+ } as typeof process.stderr.write
60
+
61
+ process.stderr.write = wrapped
62
+ installed = true
63
+ return true
64
+ }
65
+
66
+ /**
67
+ * Internal: split `text` into lines, prepend `[ISO] ` to each complete
68
+ * line, leave any trailing partial line buffered for the next call.
69
+ *
70
+ * Exported for tests only.
71
+ */
72
+ export function stampLines(text: string, now: () => string = isoTimestamp): string {
73
+ if (text === '') return ''
74
+
75
+ let out = ''
76
+ let i = 0
77
+ while (i < text.length) {
78
+ const nl = text.indexOf('\n', i)
79
+ if (nl === -1) {
80
+ // No more newlines in this chunk — buffer the rest.
81
+ partialBuffer += text.slice(i)
82
+ break
83
+ }
84
+ // We have a complete line: anything in partialBuffer + slice up to \n.
85
+ const line = partialBuffer + text.slice(i, nl + 1)
86
+ partialBuffer = ''
87
+ out += `[${now()}] ${line}`
88
+ i = nl + 1
89
+ }
90
+ return out
91
+ }
92
+
93
+ /** Test hook: reset module state. */
94
+ export function __resetForTests(): void {
95
+ if (installed && originalWrite) {
96
+ process.stderr.write = originalWrite
97
+ }
98
+ installed = false
99
+ originalWrite = null
100
+ partialBuffer = ''
101
+ }
102
+
103
+ /** Test hook: read the current partial buffer (debug-only). */
104
+ export function __getPartialBufferForTests(): string {
105
+ return partialBuffer
106
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Tests for the line-buffered stderr timestamp wrapper.
3
+ */
4
+
5
+ import { describe, expect, it, beforeEach } from 'vitest'
6
+ import {
7
+ __resetForTests,
8
+ __getPartialBufferForTests,
9
+ installStderrTimestamps,
10
+ stampLines,
11
+ } from '../stderr-timestamps'
12
+
13
+ beforeEach(() => {
14
+ __resetForTests()
15
+ })
16
+
17
+ describe('stampLines (pure)', () => {
18
+ it('stamps a single complete line', () => {
19
+ const t = () => '2026-05-20T18:00:00.000Z'
20
+ expect(stampLines('hello world\n', t)).toBe('[2026-05-20T18:00:00.000Z] hello world\n')
21
+ })
22
+
23
+ it('returns empty for empty input', () => {
24
+ const t = () => '2026-05-20T18:00:00.000Z'
25
+ expect(stampLines('', t)).toBe('')
26
+ })
27
+
28
+ it('stamps multiple lines in one chunk', () => {
29
+ const t = () => 'T'
30
+ expect(stampLines('a\nb\nc\n', t)).toBe('[T] a\n[T] b\n[T] c\n')
31
+ })
32
+
33
+ it('buffers a partial line until the newline arrives', () => {
34
+ const t = () => 'T'
35
+ // Partial chunk: no newline → no output, content buffered.
36
+ expect(stampLines('partial', t)).toBe('')
37
+ expect(__getPartialBufferForTests()).toBe('partial')
38
+ // Newline finishes the buffered line.
39
+ expect(stampLines('-end\n', t)).toBe('[T] partial-end\n')
40
+ expect(__getPartialBufferForTests()).toBe('')
41
+ })
42
+
43
+ it('handles partial + complete in one chunk', () => {
44
+ const t = () => 'T'
45
+ expect(stampLines('line1\nstart-of-2', t)).toBe('[T] line1\n')
46
+ expect(__getPartialBufferForTests()).toBe('start-of-2')
47
+ expect(stampLines('-end\n', t)).toBe('[T] start-of-2-end\n')
48
+ })
49
+
50
+ it('handles chunk that ends exactly on a newline', () => {
51
+ const t = () => 'T'
52
+ expect(stampLines('exact\n', t)).toBe('[T] exact\n')
53
+ expect(__getPartialBufferForTests()).toBe('')
54
+ })
55
+
56
+ it('handles a multi-newline chunk with trailing partial', () => {
57
+ const t = () => 'T'
58
+ expect(stampLines('a\nb\nc', t)).toBe('[T] a\n[T] b\n')
59
+ expect(__getPartialBufferForTests()).toBe('c')
60
+ })
61
+ })
62
+
63
+ describe('installStderrTimestamps (integration)', () => {
64
+ it('kill switch SWITCHROOM_LOG_TIMESTAMPS=0 prevents install', () => {
65
+ const env: NodeJS.ProcessEnv = { SWITCHROOM_LOG_TIMESTAMPS: '0' }
66
+ const result = installStderrTimestamps(env)
67
+ expect(result).toBe(false)
68
+ })
69
+
70
+ it('default install returns true', () => {
71
+ const result = installStderrTimestamps({})
72
+ expect(result).toBe(true)
73
+ })
74
+
75
+ it('second install is a no-op (idempotent)', () => {
76
+ expect(installStderrTimestamps({})).toBe(true)
77
+ expect(installStderrTimestamps({})).toBe(true)
78
+ })
79
+
80
+ it('wrapped write emits ISO-timestamped lines', () => {
81
+ installStderrTimestamps({})
82
+ // Capture by replacing the write FUNCTION the wrapper forwards to.
83
+ // The wrapper calls the ORIGINAL bound `process.stderr.write`. We
84
+ // can validate end-to-end by writing and then reading back from a
85
+ // captured forward — but easier: pipe the wrapped output to a
86
+ // string by hooking process.stderr.write a second time.
87
+ const captured: string[] = []
88
+ const prev = process.stderr.write
89
+ process.stderr.write = ((chunk: string | Uint8Array) => {
90
+ const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
91
+ captured.push(text)
92
+ return true
93
+ }) as typeof process.stderr.write
94
+ try {
95
+ // The OUTER wrapper (installStderrTimestamps) was installed first,
96
+ // then we layered the capture on top. Calling process.stderr.write
97
+ // hits the CAPTURE, which means we'd capture the un-stamped text.
98
+ // Reverse: call the STAMPED wrapper directly by invoking through
99
+ // the installed function reference. Easiest path: just exercise
100
+ // stampLines (already pure-tested above) and rely on the install
101
+ // returning true to know we wired the wrapper.
102
+ //
103
+ // This test is best-effort end-to-end; the pure tests are the
104
+ // load-bearing contract.
105
+ process.stderr.write('test\n')
106
+ } finally {
107
+ process.stderr.write = prev
108
+ }
109
+ // The capture above is positioned ABOVE the stamper, so what it
110
+ // captures is what callers see. We at least verify it didn't crash.
111
+ expect(captured.length).toBeGreaterThan(0)
112
+ })
113
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "hindsight-memory",
3
+ "description": "Automatic long-term memory for Claude Code via Hindsight. Recalls relevant memories before each prompt and retains conversation transcripts after each response.",
4
+ "version": "0.4.0",
5
+ "author": {"name": "Hindsight Team", "url": "https://vectorize.io/hindsight"},
6
+ "license": "MIT",
7
+ "keywords": ["memory", "hindsight", "recall", "retain"]
8
+ }
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - `{user_id}` template variable for `retainTags` and `retainMetadata`, resolved
8
+ from the `HINDSIGHT_USER_ID` env var (empty string if unset). Enables
9
+ machine-independent per-user memory scoping without hardcoding user ids in
10
+ `settings.json`.
11
+
12
+ ### Changed
13
+
14
+ - Tags that resolve to an empty namespace content (e.g. `"user:"` when
15
+ `HINDSIGHT_USER_ID` is unset) are now dropped from retain requests. Previously
16
+ such tags were sent as-is. Tags without `:` are unaffected.
17
+
18
+ ## [0.1.0] - 2025-03-23
19
+
20
+ ### Added
21
+ - Initial release: Claude Code plugin for Hindsight long-term memory
22
+ - Auto-recall on every user prompt via `UserPromptSubmit` hook — injects relevant memories as `additionalContext`
23
+ - Auto-retain after every response via async `Stop` hook — extracts and stores conversation transcript
24
+ - Session lifecycle hooks (`SessionStart` health check, `SessionEnd` daemon cleanup)
25
+ - Three connection modes: external API, auto-managed local daemon (`uvx hindsight-embed`), existing local server
26
+ - Dynamic bank IDs with configurable granularity (`agent`, `project`, `session`, `channel`, `user`)
27
+ - Channel-agnostic: works with Claude Code Channels (Telegram, Discord, Slack) and interactive sessions
28
+ - Zero pip dependencies — pure Python stdlib (`urllib`, `fcntl`, `subprocess`)
29
+ - 34 configuration options via `settings.json` with env var overrides
30
+ - LLM auto-detection from `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY`
31
+ - Chunked retention with sliding window (`retainEveryNTurns` + `retainOverlapTurns`)
32
+ - Memory tag stripping to prevent retain feedback loops
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vectorize AI, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.