switchroom 0.17.6 → 0.18.3
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.
- package/bin/workspace-dynamic-hook.sh +12 -13
- package/dist/agent-scheduler/index.js +65 -5
- package/dist/auth-broker/index.js +6623 -514
- package/dist/cli/notion-write-pretool.mjs +64 -4
- package/dist/cli/switchroom.js +1888 -1162
- package/dist/host-control/main.js +6306 -162
- package/dist/vault/approvals/kernel-server.js +6014 -202
- package/dist/vault/broker/server.js +6741 -940
- package/package.json +1 -1
- package/profiles/_base/settings.json.hbs +2 -2
- package/profiles/_base/start.sh.hbs +218 -25
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md +116 -0
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/mental-model-curator/SKILL.md +162 -0
- package/telegram-plugin/auth-snapshot-format.ts +22 -24
- package/telegram-plugin/bridge/bridge.ts +80 -1
- package/telegram-plugin/bridge/ipc-client.ts +19 -0
- package/telegram-plugin/bridge/permission-ledger.ts +61 -0
- package/telegram-plugin/consolidation-legibility.ts +279 -0
- package/telegram-plugin/context-exhaustion.ts +124 -0
- package/telegram-plugin/dist/bridge/bridge.js +85 -1
- package/telegram-plugin/dist/gateway/gateway.js +25802 -8488
- package/telegram-plugin/dist/server.js +86 -2
- package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
- package/telegram-plugin/gateway/activity-card-store.ts +369 -0
- package/telegram-plugin/gateway/gateway.ts +1861 -172
- package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
- package/telegram-plugin/gateway/inbound-spool.ts +22 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
- package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
- package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
- package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
- package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
- package/telegram-plugin/gateway/model-command.ts +70 -10
- package/telegram-plugin/gateway/permission-rearm.ts +115 -0
- package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
- package/telegram-plugin/memory-legibility.ts +217 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +6 -0
- package/telegram-plugin/quota-watch.ts +4 -6
- package/telegram-plugin/registry/turns-schema.test.ts +97 -0
- package/telegram-plugin/registry/turns-schema.ts +78 -0
- package/telegram-plugin/render/ir.ts +209 -0
- package/telegram-plugin/render/parse.ts +363 -0
- package/telegram-plugin/render/render.ts +440 -0
- package/telegram-plugin/render/rich-render.ts +72 -0
- package/telegram-plugin/scoped-approval.ts +59 -0
- package/telegram-plugin/silent-end.ts +78 -0
- package/telegram-plugin/stream-controller.ts +14 -3
- package/telegram-plugin/subagent-watcher.ts +60 -6
- package/telegram-plugin/tests/activity-card-store.test.ts +530 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
- package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
- package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
- package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +44 -9
- package/telegram-plugin/tests/feed-survival.test.ts +39 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
- package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
- package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
- package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
- package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
- package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
- package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
- package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
- package/telegram-plugin/tests/model-command.test.ts +193 -16
- package/telegram-plugin/tests/narrative-render.test.ts +125 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
- package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
- package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
- package/telegram-plugin/tests/quota-watch.test.ts +1 -4
- package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
- package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
- package/telegram-plugin/tests/render/parse.test.ts +393 -0
- package/telegram-plugin/tests/render/render.test.ts +436 -0
- package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
- package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
- package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
- package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +198 -0
- package/telegram-plugin/turn-liveness-floor.ts +35 -1
- package/telegram-plugin/uat/assertions.ts +88 -4
- package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
- package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
- package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
- package/telegram-plugin/worktree-watch-cwds.ts +135 -0
- package/vendor/hindsight-memory/hooks/hooks.json +9 -0
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
- package/vendor/hindsight-memory/scripts/lib/config.py +46 -2
- package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
- package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
- package/vendor/hindsight-memory/scripts/recall.py +217 -10
- package/vendor/hindsight-memory/scripts/retain.py +17 -0
- package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
- package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/test_client.py +43 -0
- package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
- package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -13542,7 +13542,7 @@ var init_zod = __esm(() => {
|
|
|
13542
13542
|
function isValidTimezone(value) {
|
|
13543
13543
|
return TIMEZONE_REGEX.test(value);
|
|
13544
13544
|
}
|
|
13545
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
13545
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
13546
13546
|
var init_schema = __esm(() => {
|
|
13547
13547
|
init_zod();
|
|
13548
13548
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -13656,8 +13656,35 @@ var init_schema = __esm(() => {
|
|
|
13656
13656
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
13657
13657
|
file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
|
|
13658
13658
|
isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
|
|
13659
|
-
bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
|
|
13660
|
-
|
|
13659
|
+
bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize " + "results. NOTE: this is an alias for the Hindsight engine's " + "`reflect_mission` field (verified live: switchroom's bank_mission " + "lands in `config.reflect_mission`). Prefer `reflect_mission` going " + "forward; `bank_mission` is retained for back-compat. If both are " + "set, `reflect_mission` wins. Cascade: override."),
|
|
13660
|
+
reflect_mission: exports_external.string().optional().describe("Mission/context steering Hindsight Reflect operations (the bank's " + "'who am I / what matters' framing applied during recall). The " + "engine-accurate name for what `bank_mission` sets. Cascade: override."),
|
|
13661
|
+
retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain. Cascade: override."),
|
|
13662
|
+
mental_models: exports_external.array(exports_external.object({
|
|
13663
|
+
name: exports_external.string().min(1).describe("Stable model name (identity key for idempotent ensure). Two " + "declarations with the same name in one agent are rejected."),
|
|
13664
|
+
source_query: exports_external.string().min(1).max(2000).describe("The reflection query the model answers, semantically " + "refreshed from the bank's content. Capped at 2000 chars \u2014 a " + "standing reflection query, not a document; the ceiling also " + "bounds what an agent-proposed model can smuggle past the " + "operator approval card."),
|
|
13665
|
+
refresh_after_consolidation: exports_external.boolean().optional().describe("Refresh this model after each consolidation. Defaults OFF \u2014 " + "refresh adds bounded background model-spend + timeout risk " + "(RFC Phase 5), so it is opt-in per model."),
|
|
13666
|
+
max_tokens: exports_external.number().int().positive().max(8192).optional().describe("Cap on the synthesized model's token size. Upper-bounded at " + "8192 \u2014 a mental model is a standing summary, not a corpus; the " + "ceiling also caps what an agent-proposed model can request.")
|
|
13667
|
+
})).superRefine((models, ctx) => {
|
|
13668
|
+
const seen = new Set;
|
|
13669
|
+
for (let i = 0;i < models.length; i++) {
|
|
13670
|
+
const key = models[i].name;
|
|
13671
|
+
if (seen.has(key)) {
|
|
13672
|
+
ctx.addIssue({
|
|
13673
|
+
code: exports_external.ZodIssueCode.custom,
|
|
13674
|
+
path: [i, "name"],
|
|
13675
|
+
message: `duplicate mental_models name "${key}" \u2014 model names must be ` + `unique within an agent (they are the idempotent-ensure key)`
|
|
13676
|
+
});
|
|
13677
|
+
}
|
|
13678
|
+
seen.add(key);
|
|
13679
|
+
}
|
|
13680
|
+
}).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry \u2014 e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced \u2014 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded \u2014 each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
|
|
13681
|
+
observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
|
|
13682
|
+
disposition: exports_external.object({
|
|
13683
|
+
skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
|
|
13684
|
+
literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
|
|
13685
|
+
empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
|
|
13686
|
+
}).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis \u2014 a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
|
|
13687
|
+
directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true \u2014 Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never \u2026", "from now on \u2026", ' + `"stop doing \u2026", a stated preference, "that's wrong, it's \u2026") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex \u2014 the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
13661
13688
|
recall: exports_external.object({
|
|
13662
13689
|
max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
|
|
13663
13690
|
cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
|
|
@@ -13667,7 +13694,11 @@ var init_schema = __esm(() => {
|
|
|
13667
13694
|
sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender \u2192 extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username \u2014 a leading @ is " + "optional \u2014 or numeric user_id), merged " + "into its own results \u2014 so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
|
|
13668
13695
|
skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true \u2014 saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
|
|
13669
13696
|
topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
|
|
13670
|
-
}).optional().describe("Auto-recall tuning knobs")
|
|
13697
|
+
}).optional().describe("Auto-recall tuning knobs"),
|
|
13698
|
+
retain: exports_external.object({
|
|
13699
|
+
every_n_turns: exports_external.number().int().min(1).optional().describe("How often the Stop hook fires auto-retention, in turns. The " + "vendor plugin default is 10 (a short session can end before " + "retention ever fires); switchroom's scaffold default is 3. " + "Lower = more frequent, smaller retains + tighter crash " + "durability (at 1, every turn is retained before a restart can " + "lose it); higher = fewer, larger retains + less LLM churn. " + "Raised from the historical 1 to 3 because the local reasoning " + "consolidation model (Ollama gpt-oss-20b) ran away on the large " + "overlapping every-turn payloads. Set to 1 for the old " + "every-turn crash-durability guarantee. Min 1. Cascade: " + "per-field merge (agent wins over default)."),
|
|
13700
|
+
overlap_turns: exports_external.number().int().min(0).optional().describe("Extra recent turns included in each chunked retain window on " + "top of `every_n_turns`, so window = overlap_turns + " + "every_n_turns recent turns. Vendor default is 2; switchroom's " + "scaffold default is 1 (smaller payloads for the local " + "reasoning consolidation model). Higher = more redundant " + "context re-sent per fire. Min 0. Cascade: per-field merge.")
|
|
13701
|
+
}).optional().describe("Auto-retain (Stop-hook consolidation) cadence knobs")
|
|
13671
13702
|
}).optional();
|
|
13672
13703
|
HookEntrySchema = exports_external.object({
|
|
13673
13704
|
command: exports_external.string().describe("Shell command to run. Supports ${CLAUDE_CONFIG_DIR} and ${CLAUDE_PLUGIN_ROOT} substitution."),
|
|
@@ -13845,6 +13876,21 @@ var init_schema = __esm(() => {
|
|
|
13845
13876
|
small_fast_model: exports_external.string().optional().describe("Model id exported as ANTHROPIC_SMALL_FAST_MODEL for the claude CLI's " + "background/fast lane, e.g. 'claude-haiku-4-5-20251001'."),
|
|
13846
13877
|
tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins).")
|
|
13847
13878
|
}).optional().describe("LiteLLM routing config \u2014 opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
|
|
13879
|
+
HindsightPerOpLlmSchema = exports_external.object({
|
|
13880
|
+
model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent \u2192 " + "inherit the global `hindsight.llm.model`."),
|
|
13881
|
+
provider: exports_external.string().min(1).optional().describe("Per-op provider (upstream `HINDSIGHT_API_<OP>_LLM_PROVIDER`). " + "Absent \u2192 inherit the global `hindsight.llm.provider`."),
|
|
13882
|
+
base_url: exports_external.string().min(1).optional().describe("Per-op base URL (upstream `HINDSIGHT_API_<OP>_LLM_BASE_URL`). " + "Optional passthrough; absent \u2192 inherit the global."),
|
|
13883
|
+
api_key: exports_external.string().min(1).optional().describe("Per-op API key (upstream `HINDSIGHT_API_<OP>_LLM_API_KEY`). Literal " + "or `vault:` reference. Optional passthrough; absent \u2192 inherit global.")
|
|
13884
|
+
}).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback \u2014 switchroom emits only the vars set.");
|
|
13885
|
+
HindsightConfigSchema = exports_external.object({
|
|
13886
|
+
llm: exports_external.object({
|
|
13887
|
+
provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
|
|
13888
|
+
model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
|
|
13889
|
+
retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent \u2192 uses the global model/provider."),
|
|
13890
|
+
reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
|
|
13891
|
+
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
|
|
13892
|
+
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults.")
|
|
13893
|
+
});
|
|
13848
13894
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
13849
13895
|
microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
|
|
13850
13896
|
microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
|
|
@@ -13922,6 +13968,7 @@ var init_schema = __esm(() => {
|
|
|
13922
13968
|
auto_recall: exports_external.boolean().optional(),
|
|
13923
13969
|
file: exports_external.boolean().optional(),
|
|
13924
13970
|
isolation: exports_external.enum(["default", "strict"]).optional(),
|
|
13971
|
+
directive_capture_nudge: exports_external.boolean().optional(),
|
|
13925
13972
|
recall: exports_external.object({
|
|
13926
13973
|
max_memories: exports_external.number().int().min(0).optional(),
|
|
13927
13974
|
cache_ttl_secs: exports_external.number().int().min(0).optional(),
|
|
@@ -14092,6 +14139,7 @@ var init_schema = __esm(() => {
|
|
|
14092
14139
|
config: exports_external.object({
|
|
14093
14140
|
provider: exports_external.string().default("ollama").describe("Embedding provider (ollama, openai, anthropic)"),
|
|
14094
14141
|
model: exports_external.string().optional().describe("Embedding model (e.g., 'nomic-embed-text')"),
|
|
14142
|
+
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
14095
14143
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
14096
14144
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
14097
14145
|
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
@@ -14169,6 +14217,7 @@ var init_schema = __esm(() => {
|
|
|
14169
14217
|
telegram: TelegramConfigSchema,
|
|
14170
14218
|
release: ReleaseBlock.optional().describe("Fleet-wide default release-channel pin / pointer for the update " + "flow. Either `channel` (dev|rc|latest) or `pin` (sha-<hex>|v<semver>) " + "\u2014 mutually exclusive. Per-agent `release` REPLACES this entirely."),
|
|
14171
14219
|
memory: MemoryBackendConfigSchema.optional(),
|
|
14220
|
+
hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) \u2014 takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
|
|
14172
14221
|
vault: VaultConfigSchema.optional(),
|
|
14173
14222
|
auth: exports_external.object({
|
|
14174
14223
|
active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
|
|
@@ -14617,6 +14666,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
14617
14666
|
continue;
|
|
14618
14667
|
if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
14619
14668
|
combined[k] = { ...base.recall, ...v };
|
|
14669
|
+
} else if (k === "retain" && base.retain && typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
14670
|
+
combined[k] = {
|
|
14671
|
+
...base.retain,
|
|
14672
|
+
...v
|
|
14673
|
+
};
|
|
14674
|
+
} else if (k === "disposition" && base.disposition && typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
14675
|
+
combined[k] = {
|
|
14676
|
+
...base.disposition,
|
|
14677
|
+
...v
|
|
14678
|
+
};
|
|
14620
14679
|
} else {
|
|
14621
14680
|
combined[k] = v;
|
|
14622
14681
|
}
|
|
@@ -15065,7 +15124,7 @@ function resolveAgentsDir(config) {
|
|
|
15065
15124
|
if (override && override.length > 0 && override.startsWith("/")) {
|
|
15066
15125
|
return override;
|
|
15067
15126
|
}
|
|
15068
|
-
return resolveDualPath(config.switchroom.agents_dir);
|
|
15127
|
+
return resolveDualPath(config.switchroom.agents_dir || "~/.switchroom/agents");
|
|
15069
15128
|
}
|
|
15070
15129
|
function resolvePath(pathStr) {
|
|
15071
15130
|
return resolveDualPath(pathStr);
|
|
@@ -15122,9 +15181,839 @@ var init_users = __esm(() => {
|
|
|
15122
15181
|
init_merge();
|
|
15123
15182
|
});
|
|
15124
15183
|
|
|
15184
|
+
// src/agents/pane-lock.ts
|
|
15185
|
+
function withPaneLock(key, fn) {
|
|
15186
|
+
const tail = tails.get(key) ?? Promise.resolve();
|
|
15187
|
+
const run = tail.then(() => fn());
|
|
15188
|
+
const next = run.then(() => {}, () => {});
|
|
15189
|
+
tails.set(key, next);
|
|
15190
|
+
next.then(() => {
|
|
15191
|
+
if (tails.get(key) === next)
|
|
15192
|
+
tails.delete(key);
|
|
15193
|
+
});
|
|
15194
|
+
return run;
|
|
15195
|
+
}
|
|
15196
|
+
var tails;
|
|
15197
|
+
var init_pane_lock = __esm(() => {
|
|
15198
|
+
tails = new Map;
|
|
15199
|
+
});
|
|
15200
|
+
|
|
15201
|
+
// src/agents/inject.ts
|
|
15202
|
+
var exports_inject = {};
|
|
15203
|
+
__export(exports_inject, {
|
|
15204
|
+
validateInjectCommand: () => validateInjectCommand,
|
|
15205
|
+
normalizeInjectCommand: () => normalizeInjectCommand,
|
|
15206
|
+
makeTmuxRunner: () => makeTmuxRunner,
|
|
15207
|
+
isTuiChromeLine: () => isTuiChromeLine,
|
|
15208
|
+
injectSlashCommandWith: () => injectSlashCommandWith,
|
|
15209
|
+
injectSlashCommand: () => injectSlashCommand,
|
|
15210
|
+
diffPane: () => diffPane,
|
|
15211
|
+
dedupeInjectQueue: () => dedupeInjectQueue,
|
|
15212
|
+
InjectError: () => InjectError,
|
|
15213
|
+
INJECT_COMMANDS: () => INJECT_COMMANDS,
|
|
15214
|
+
INJECT_BLOCKLIST: () => INJECT_BLOCKLIST,
|
|
15215
|
+
INJECT_BLOCKED: () => INJECT_BLOCKED,
|
|
15216
|
+
INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
|
|
15217
|
+
});
|
|
15218
|
+
import { execFileSync } from "node:child_process";
|
|
15219
|
+
function validateInjectCommand(command) {
|
|
15220
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
15221
|
+
throw new InjectError("invalid", "command is empty");
|
|
15222
|
+
}
|
|
15223
|
+
const trimmed = command.trim();
|
|
15224
|
+
if (!trimmed.startsWith("/")) {
|
|
15225
|
+
throw new InjectError("invalid", `command must start with "/": got ${trimmed}`);
|
|
15226
|
+
}
|
|
15227
|
+
const bare = trimmed.split(/\s+/, 1)[0].toLowerCase();
|
|
15228
|
+
const blockedMeta = INJECT_BLOCKED.get(bare);
|
|
15229
|
+
if (blockedMeta) {
|
|
15230
|
+
throw new InjectError("blocked", `${bare} is explicitly blocked from inject (${blockedMeta.reason}).`);
|
|
15231
|
+
}
|
|
15232
|
+
if (!INJECT_COMMANDS.has(bare)) {
|
|
15233
|
+
const allowed = [...INJECT_COMMANDS.keys()].sort().join(", ");
|
|
15234
|
+
throw new InjectError("not_allowed", `${bare} is not in the inject allowlist. Allowed: ${allowed}`);
|
|
15235
|
+
}
|
|
15236
|
+
return bare;
|
|
15237
|
+
}
|
|
15238
|
+
function normalizeInjectCommand(command) {
|
|
15239
|
+
return command.trim().replace(/\s+/g, " ").toLowerCase();
|
|
15240
|
+
}
|
|
15241
|
+
function dedupeInjectQueue(commands) {
|
|
15242
|
+
const seen = new Set;
|
|
15243
|
+
const out = [];
|
|
15244
|
+
for (const raw of commands) {
|
|
15245
|
+
if (typeof raw !== "string")
|
|
15246
|
+
continue;
|
|
15247
|
+
const key = normalizeInjectCommand(raw);
|
|
15248
|
+
if (key.length === 0)
|
|
15249
|
+
continue;
|
|
15250
|
+
if (seen.has(key))
|
|
15251
|
+
continue;
|
|
15252
|
+
seen.add(key);
|
|
15253
|
+
out.push(raw.trim());
|
|
15254
|
+
}
|
|
15255
|
+
return out;
|
|
15256
|
+
}
|
|
15257
|
+
function defaultSocketName(agentName) {
|
|
15258
|
+
return `switchroom-${agentName}`;
|
|
15259
|
+
}
|
|
15260
|
+
function makeTmuxRunner(tmuxBin) {
|
|
15261
|
+
return {
|
|
15262
|
+
capture(socket, session) {
|
|
15263
|
+
try {
|
|
15264
|
+
return execFileSync(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
15265
|
+
} catch {
|
|
15266
|
+
return null;
|
|
15267
|
+
}
|
|
15268
|
+
},
|
|
15269
|
+
send(socket, session, args) {
|
|
15270
|
+
const [subcmd, ...rest] = args;
|
|
15271
|
+
const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
|
|
15272
|
+
const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
|
|
15273
|
+
const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
|
|
15274
|
+
execFileSync(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
|
|
15275
|
+
},
|
|
15276
|
+
hasSession(socket, session) {
|
|
15277
|
+
try {
|
|
15278
|
+
execFileSync(tmuxBin, ["-L", socket, "has-session", "-t", session], {
|
|
15279
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
15280
|
+
});
|
|
15281
|
+
return true;
|
|
15282
|
+
} catch {
|
|
15283
|
+
return false;
|
|
15284
|
+
}
|
|
15285
|
+
}
|
|
15286
|
+
};
|
|
15287
|
+
}
|
|
15288
|
+
function sleep(ms) {
|
|
15289
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
15290
|
+
}
|
|
15291
|
+
function isTuiChromeLine(line) {
|
|
15292
|
+
const t = line.trim();
|
|
15293
|
+
if (t.length === 0)
|
|
15294
|
+
return false;
|
|
15295
|
+
if (/^[\s\-_\u2502\u2500\u256d\u256e\u2570\u256f\u250c\u2510\u2514\u2518|]+$/.test(t))
|
|
15296
|
+
return true;
|
|
15297
|
+
if (/^[\u256d\u256e\u2570\u256f\u2502\s]*[>)\u276f][\u256d\u256e\u2570\u256f\u2502\s]*$/.test(t))
|
|
15298
|
+
return true;
|
|
15299
|
+
if (/accept edits on/i.test(t))
|
|
15300
|
+
return true;
|
|
15301
|
+
if (/shift\+tab to cycle/i.test(t))
|
|
15302
|
+
return true;
|
|
15303
|
+
if (/for agents/i.test(t))
|
|
15304
|
+
return true;
|
|
15305
|
+
if (/\? for shortcuts/i.test(t))
|
|
15306
|
+
return true;
|
|
15307
|
+
if (/bypassing permissions/i.test(t))
|
|
15308
|
+
return true;
|
|
15309
|
+
if (t === "copy")
|
|
15310
|
+
return true;
|
|
15311
|
+
return false;
|
|
15312
|
+
}
|
|
15313
|
+
function diffPane(before, after, command) {
|
|
15314
|
+
if (command) {
|
|
15315
|
+
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15316
|
+
const lines = after.split(`
|
|
15317
|
+
`);
|
|
15318
|
+
let anchorIdx = -1;
|
|
15319
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
15320
|
+
if (/[\u276f>]\s+/.test(lines[i]) && lines[i].includes(escaped)) {
|
|
15321
|
+
anchorIdx = i;
|
|
15322
|
+
break;
|
|
15323
|
+
}
|
|
15324
|
+
}
|
|
15325
|
+
if (anchorIdx >= 0) {
|
|
15326
|
+
const tail = lines.slice(anchorIdx + 1);
|
|
15327
|
+
const trimmed = [];
|
|
15328
|
+
for (const raw of tail) {
|
|
15329
|
+
const line = raw.trimEnd();
|
|
15330
|
+
if (line.length === 0 && trimmed.length === 0)
|
|
15331
|
+
continue;
|
|
15332
|
+
if (isTuiChromeLine(line))
|
|
15333
|
+
continue;
|
|
15334
|
+
trimmed.push(line);
|
|
15335
|
+
}
|
|
15336
|
+
while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
|
|
15337
|
+
trimmed.pop();
|
|
15338
|
+
}
|
|
15339
|
+
const affordances = /^(esc to cancel|press any key|\u21b5 select)/i;
|
|
15340
|
+
while (trimmed.length > 0 && affordances.test(trimmed[trimmed.length - 1].trim())) {
|
|
15341
|
+
trimmed.pop();
|
|
15342
|
+
}
|
|
15343
|
+
if (trimmed.length > 0) {
|
|
15344
|
+
return { output: trimmed.join(`
|
|
15345
|
+
`), anchored: true };
|
|
15346
|
+
}
|
|
15347
|
+
}
|
|
15348
|
+
}
|
|
15349
|
+
const beforeSet = new Set(before.split(`
|
|
15350
|
+
`).map((l) => l.trimEnd()));
|
|
15351
|
+
const newLines = [];
|
|
15352
|
+
for (const raw of after.split(`
|
|
15353
|
+
`)) {
|
|
15354
|
+
const line = raw.trimEnd();
|
|
15355
|
+
if (line.length === 0)
|
|
15356
|
+
continue;
|
|
15357
|
+
if (beforeSet.has(line))
|
|
15358
|
+
continue;
|
|
15359
|
+
if (isTuiChromeLine(line))
|
|
15360
|
+
continue;
|
|
15361
|
+
newLines.push(line);
|
|
15362
|
+
}
|
|
15363
|
+
return { output: newLines.join(`
|
|
15364
|
+
`), anchored: false };
|
|
15365
|
+
}
|
|
15366
|
+
async function injectSlashCommand(agentName, command, opts = {}) {
|
|
15367
|
+
validateInjectCommand(command);
|
|
15368
|
+
const tmuxBin = opts.tmuxBin ?? "tmux";
|
|
15369
|
+
const socket = opts.socketName ?? defaultSocketName(agentName);
|
|
15370
|
+
const session = opts.sessionName ?? agentName;
|
|
15371
|
+
const settleMs = opts.settleMs ?? 2000;
|
|
15372
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
15373
|
+
return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
|
|
15374
|
+
socket,
|
|
15375
|
+
session,
|
|
15376
|
+
command: command.trim(),
|
|
15377
|
+
settleMs,
|
|
15378
|
+
timeoutMs
|
|
15379
|
+
}));
|
|
15380
|
+
}
|
|
15381
|
+
async function injectSlashCommandWith(runner, args) {
|
|
15382
|
+
const { socket, session, command, settleMs, timeoutMs } = args;
|
|
15383
|
+
let bareVerb;
|
|
15384
|
+
try {
|
|
15385
|
+
bareVerb = validateInjectCommand(command);
|
|
15386
|
+
} catch (err) {
|
|
15387
|
+
if (err instanceof InjectError) {
|
|
15388
|
+
return {
|
|
15389
|
+
outcome: "failed",
|
|
15390
|
+
output: "",
|
|
15391
|
+
truncated: false,
|
|
15392
|
+
command: command.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "",
|
|
15393
|
+
meta: null,
|
|
15394
|
+
errorCode: err.code,
|
|
15395
|
+
errorMessage: err.message
|
|
15396
|
+
};
|
|
15397
|
+
}
|
|
15398
|
+
throw err;
|
|
15399
|
+
}
|
|
15400
|
+
const meta = INJECT_COMMANDS.get(bareVerb) ?? null;
|
|
15401
|
+
if (!runner.hasSession(socket, session)) {
|
|
15402
|
+
return {
|
|
15403
|
+
outcome: "failed",
|
|
15404
|
+
output: "",
|
|
15405
|
+
truncated: false,
|
|
15406
|
+
command: bareVerb,
|
|
15407
|
+
meta,
|
|
15408
|
+
errorCode: "session_missing",
|
|
15409
|
+
errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
|
|
15410
|
+
};
|
|
15411
|
+
}
|
|
15412
|
+
const before = runner.capture(socket, session) ?? "";
|
|
15413
|
+
try {
|
|
15414
|
+
runner.send(socket, session, ["send-keys", "-l", command]);
|
|
15415
|
+
runner.send(socket, session, ["send-keys", "Enter"]);
|
|
15416
|
+
} catch (err) {
|
|
15417
|
+
return {
|
|
15418
|
+
outcome: "failed",
|
|
15419
|
+
output: "",
|
|
15420
|
+
truncated: false,
|
|
15421
|
+
command: bareVerb,
|
|
15422
|
+
meta,
|
|
15423
|
+
errorCode: "tmux_failed",
|
|
15424
|
+
errorMessage: `tmux send-keys failed: ${err instanceof Error ? err.message : String(err)}`
|
|
15425
|
+
};
|
|
15426
|
+
}
|
|
15427
|
+
const start = Date.now();
|
|
15428
|
+
let last = before;
|
|
15429
|
+
let stableSince = null;
|
|
15430
|
+
while (Date.now() - start < timeoutMs) {
|
|
15431
|
+
await sleep(POLL_INTERVAL_MS);
|
|
15432
|
+
const cur = runner.capture(socket, session) ?? "";
|
|
15433
|
+
if (cur === last && cur !== before) {
|
|
15434
|
+
if (stableSince === null) {
|
|
15435
|
+
stableSince = Date.now();
|
|
15436
|
+
} else if (Date.now() - stableSince >= POLL_INTERVAL_MS) {
|
|
15437
|
+
last = cur;
|
|
15438
|
+
break;
|
|
15439
|
+
}
|
|
15440
|
+
} else {
|
|
15441
|
+
stableSince = null;
|
|
15442
|
+
}
|
|
15443
|
+
last = cur;
|
|
15444
|
+
if (Date.now() - start >= settleMs && cur !== before) {
|
|
15445
|
+
break;
|
|
15446
|
+
}
|
|
15447
|
+
}
|
|
15448
|
+
const { output: rawOutput, anchored } = diffPane(before, last, command);
|
|
15449
|
+
let output = rawOutput;
|
|
15450
|
+
let truncated = false;
|
|
15451
|
+
const bytes = Buffer.byteLength(output, "utf-8");
|
|
15452
|
+
if (bytes > OUTPUT_BYTE_CAP) {
|
|
15453
|
+
while (Buffer.byteLength(output, "utf-8") > OUTPUT_BYTE_CAP) {
|
|
15454
|
+
output = output.slice(Math.floor(output.length * 0.1) + 1);
|
|
15455
|
+
}
|
|
15456
|
+
truncated = true;
|
|
15457
|
+
}
|
|
15458
|
+
if (meta?.dialog) {
|
|
15459
|
+
try {
|
|
15460
|
+
runner.send(socket, session, ["send-keys", "Escape"]);
|
|
15461
|
+
} catch {}
|
|
15462
|
+
}
|
|
15463
|
+
if (output.trim().length === 0) {
|
|
15464
|
+
return {
|
|
15465
|
+
outcome: "ok_no_output",
|
|
15466
|
+
output: "",
|
|
15467
|
+
truncated: false,
|
|
15468
|
+
command: bareVerb,
|
|
15469
|
+
meta,
|
|
15470
|
+
...anchored ? {} : { diagnostic: "anchor_missing" }
|
|
15471
|
+
};
|
|
15472
|
+
}
|
|
15473
|
+
return {
|
|
15474
|
+
outcome: "ok",
|
|
15475
|
+
output,
|
|
15476
|
+
truncated,
|
|
15477
|
+
command: bareVerb,
|
|
15478
|
+
meta,
|
|
15479
|
+
...truncated ? { diagnostic: "truncated_output" } : {}
|
|
15480
|
+
};
|
|
15481
|
+
}
|
|
15482
|
+
var INJECT_COMMANDS, INJECT_ALLOWLIST, INJECT_BLOCKED, INJECT_BLOCKLIST, InjectError, POLL_INTERVAL_MS = 150, OUTPUT_BYTE_CAP = 3000;
|
|
15483
|
+
var init_inject = __esm(() => {
|
|
15484
|
+
init_pane_lock();
|
|
15485
|
+
INJECT_COMMANDS = new Map([
|
|
15486
|
+
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
|
|
15487
|
+
["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
|
|
15488
|
+
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
|
|
15489
|
+
["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
|
|
15490
|
+
["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
|
|
15491
|
+
["/model", { description: "Open model picker", expectsOutput: true }],
|
|
15492
|
+
[
|
|
15493
|
+
"/clear",
|
|
15494
|
+
{
|
|
15495
|
+
description: "Clear session screen",
|
|
15496
|
+
expectsOutput: false,
|
|
15497
|
+
silentNote: "context cleared \u2014 fresh slate"
|
|
15498
|
+
}
|
|
15499
|
+
],
|
|
15500
|
+
[
|
|
15501
|
+
"/compact",
|
|
15502
|
+
{
|
|
15503
|
+
description: "Compact conversation history",
|
|
15504
|
+
expectsOutput: false,
|
|
15505
|
+
silentNote: "compaction runs silently"
|
|
15506
|
+
}
|
|
15507
|
+
]
|
|
15508
|
+
]);
|
|
15509
|
+
INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
|
|
15510
|
+
INJECT_BLOCKED = new Map([
|
|
15511
|
+
["/login", { reason: "would mutate auth state" }],
|
|
15512
|
+
["/logout", { reason: "would terminate the agent's auth session" }],
|
|
15513
|
+
["/exit", { reason: "would kill the agent process" }],
|
|
15514
|
+
["/quit", { reason: "would kill the agent process" }],
|
|
15515
|
+
[
|
|
15516
|
+
"/effort",
|
|
15517
|
+
{
|
|
15518
|
+
reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
|
|
15519
|
+
}
|
|
15520
|
+
]
|
|
15521
|
+
]);
|
|
15522
|
+
INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
|
|
15523
|
+
InjectError = class InjectError extends Error {
|
|
15524
|
+
code;
|
|
15525
|
+
constructor(code, message) {
|
|
15526
|
+
super(message);
|
|
15527
|
+
this.name = "InjectError";
|
|
15528
|
+
this.code = code;
|
|
15529
|
+
}
|
|
15530
|
+
};
|
|
15531
|
+
});
|
|
15532
|
+
|
|
15533
|
+
// src/agents/model-picker.ts
|
|
15534
|
+
var init_model_picker = __esm(() => {
|
|
15535
|
+
init_inject();
|
|
15536
|
+
init_pane_lock();
|
|
15537
|
+
});
|
|
15538
|
+
|
|
15539
|
+
// telegram-plugin/gateway/model-command.ts
|
|
15540
|
+
function isClaudeModel(name) {
|
|
15541
|
+
const lower = name.toLowerCase();
|
|
15542
|
+
if (MODEL_ALIASES.includes(lower))
|
|
15543
|
+
return true;
|
|
15544
|
+
return lower.startsWith("claude-");
|
|
15545
|
+
}
|
|
15546
|
+
var MODEL_ALIASES;
|
|
15547
|
+
var init_model_command = __esm(() => {
|
|
15548
|
+
init_model_picker();
|
|
15549
|
+
MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
|
|
15550
|
+
});
|
|
15551
|
+
|
|
15552
|
+
// src/util/atomic.ts
|
|
15553
|
+
var exports_atomic = {};
|
|
15554
|
+
__export(exports_atomic, {
|
|
15555
|
+
writeConfigFileSync: () => writeConfigFileSync,
|
|
15556
|
+
atomicWriteJsonSync: () => atomicWriteJsonSync,
|
|
15557
|
+
atomicWriteFileSync: () => atomicWriteFileSync
|
|
15558
|
+
});
|
|
15559
|
+
import { randomBytes } from "node:crypto";
|
|
15560
|
+
import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
|
|
15561
|
+
function atomicWriteFileSync(destPath, contents, mode = 384) {
|
|
15562
|
+
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
15563
|
+
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
15564
|
+
let fd = null;
|
|
15565
|
+
try {
|
|
15566
|
+
fd = openSync2(tmp, TMP_OPEN_FLAGS, mode);
|
|
15567
|
+
writeSync(fd, buf, 0, buf.length, 0);
|
|
15568
|
+
fsyncSync(fd);
|
|
15569
|
+
closeSync2(fd);
|
|
15570
|
+
fd = null;
|
|
15571
|
+
renameSync(tmp, destPath);
|
|
15572
|
+
} catch (err) {
|
|
15573
|
+
if (fd !== null) {
|
|
15574
|
+
try {
|
|
15575
|
+
closeSync2(fd);
|
|
15576
|
+
} catch {}
|
|
15577
|
+
}
|
|
15578
|
+
try {
|
|
15579
|
+
rmSync(tmp, { force: true });
|
|
15580
|
+
} catch {}
|
|
15581
|
+
throw err;
|
|
15582
|
+
}
|
|
15583
|
+
}
|
|
15584
|
+
function atomicWriteJsonSync(destPath, value, mode = 384) {
|
|
15585
|
+
atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
|
|
15586
|
+
`, mode);
|
|
15587
|
+
}
|
|
15588
|
+
function writeConfigFileSync(destPath, contents, mode = 420) {
|
|
15589
|
+
try {
|
|
15590
|
+
atomicWriteFileSync(destPath, contents, mode);
|
|
15591
|
+
} catch (err) {
|
|
15592
|
+
const code = err.code;
|
|
15593
|
+
if (code !== "EBUSY" && code !== "EXDEV" && code !== "EINVAL") {
|
|
15594
|
+
throw err;
|
|
15595
|
+
}
|
|
15596
|
+
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
15597
|
+
let fd = null;
|
|
15598
|
+
try {
|
|
15599
|
+
fd = openSync2(destPath, constants.O_WRONLY | constants.O_TRUNC, mode);
|
|
15600
|
+
writeSync(fd, buf, 0, buf.length, 0);
|
|
15601
|
+
fsyncSync(fd);
|
|
15602
|
+
closeSync2(fd);
|
|
15603
|
+
fd = null;
|
|
15604
|
+
} catch (fallbackErr) {
|
|
15605
|
+
if (fd !== null) {
|
|
15606
|
+
try {
|
|
15607
|
+
closeSync2(fd);
|
|
15608
|
+
} catch {}
|
|
15609
|
+
}
|
|
15610
|
+
throw fallbackErr;
|
|
15611
|
+
}
|
|
15612
|
+
}
|
|
15613
|
+
}
|
|
15614
|
+
var TMP_OPEN_FLAGS;
|
|
15615
|
+
var init_atomic = __esm(() => {
|
|
15616
|
+
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
15617
|
+
});
|
|
15618
|
+
|
|
15619
|
+
// src/setup/hindsight.ts
|
|
15620
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
15621
|
+
import { createServer } from "node:net";
|
|
15622
|
+
function hindsightImageRef(tag) {
|
|
15623
|
+
const t = tag?.trim();
|
|
15624
|
+
if (!t)
|
|
15625
|
+
return HINDSIGHT_IMAGE;
|
|
15626
|
+
const v = t.replace(/^v/, "");
|
|
15627
|
+
return `${HINDSIGHT_IMAGE_REPO}:v${v}`;
|
|
15628
|
+
}
|
|
15629
|
+
function isPortFree(port) {
|
|
15630
|
+
return new Promise((resolve4) => {
|
|
15631
|
+
const server = createServer();
|
|
15632
|
+
server.once("error", () => resolve4(false));
|
|
15633
|
+
server.once("listening", () => {
|
|
15634
|
+
server.close(() => resolve4(true));
|
|
15635
|
+
});
|
|
15636
|
+
server.listen(port, "127.0.0.1");
|
|
15637
|
+
});
|
|
15638
|
+
}
|
|
15639
|
+
function describePortHolder(port) {
|
|
15640
|
+
const probes = [
|
|
15641
|
+
["ss", ["-ltnpH", `sport = :${port}`]],
|
|
15642
|
+
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"]]
|
|
15643
|
+
];
|
|
15644
|
+
for (const [cmd, args] of probes) {
|
|
15645
|
+
try {
|
|
15646
|
+
const out = execFileSync2(cmd, args, {
|
|
15647
|
+
stdio: "pipe",
|
|
15648
|
+
encoding: "utf-8"
|
|
15649
|
+
}).trim();
|
|
15650
|
+
if (!out)
|
|
15651
|
+
continue;
|
|
15652
|
+
const ssMatch = out.match(/users:\(\("([^"]+)",pid=(\d+)/);
|
|
15653
|
+
if (ssMatch)
|
|
15654
|
+
return `pid ${ssMatch[2]} (${ssMatch[1]})`;
|
|
15655
|
+
const firstDataLine = out.split(`
|
|
15656
|
+
`).map((l) => l.trim()).find((l) => l && !/^COMMAND\b/.test(l));
|
|
15657
|
+
if (firstDataLine) {
|
|
15658
|
+
const parts = firstDataLine.split(/\s+/);
|
|
15659
|
+
if (cmd === "lsof" && parts.length >= 2) {
|
|
15660
|
+
return `pid ${parts[1]} (${parts[0]})`;
|
|
15661
|
+
}
|
|
15662
|
+
return firstDataLine.slice(0, 80);
|
|
15663
|
+
}
|
|
15664
|
+
} catch {}
|
|
15665
|
+
}
|
|
15666
|
+
return null;
|
|
15667
|
+
}
|
|
15668
|
+
async function preflightHindsightPorts(ports) {
|
|
15669
|
+
for (const port of [ports.apiPort, ports.uiPort]) {
|
|
15670
|
+
if (!await isPortFree(port)) {
|
|
15671
|
+
return { port, holder: describePortHolder(port) };
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
return null;
|
|
15675
|
+
}
|
|
15676
|
+
async function findFreePort(start, maxAttempts = 50) {
|
|
15677
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
15678
|
+
const port = start + i;
|
|
15679
|
+
if (port < 1024)
|
|
15680
|
+
continue;
|
|
15681
|
+
if (await isPortFree(port)) {
|
|
15682
|
+
return port;
|
|
15683
|
+
}
|
|
15684
|
+
}
|
|
15685
|
+
return null;
|
|
15686
|
+
}
|
|
15687
|
+
async function pickHindsightPorts() {
|
|
15688
|
+
if (await isPortFree(HINDSIGHT_DEFAULT_API_PORT) && await isPortFree(HINDSIGHT_DEFAULT_UI_PORT)) {
|
|
15689
|
+
return {
|
|
15690
|
+
apiPort: HINDSIGHT_DEFAULT_API_PORT,
|
|
15691
|
+
uiPort: HINDSIGHT_DEFAULT_UI_PORT
|
|
15692
|
+
};
|
|
15693
|
+
}
|
|
15694
|
+
const apiPort = await findFreePort(18888);
|
|
15695
|
+
const uiPort = await findFreePort(19999);
|
|
15696
|
+
if (apiPort === null || uiPort === null) {
|
|
15697
|
+
throw new Error("Could not find a free port for Hindsight. " + "Stop whatever is using 18888 / 19999 and retry.");
|
|
15698
|
+
}
|
|
15699
|
+
return { apiPort, uiPort };
|
|
15700
|
+
}
|
|
15701
|
+
function isDockerAvailable() {
|
|
15702
|
+
try {
|
|
15703
|
+
execFileSync2("docker", ["--version"], { stdio: "pipe" });
|
|
15704
|
+
return true;
|
|
15705
|
+
} catch {
|
|
15706
|
+
return false;
|
|
15707
|
+
}
|
|
15708
|
+
}
|
|
15709
|
+
function isHindsightRunning() {
|
|
15710
|
+
try {
|
|
15711
|
+
const output = execFileSync2("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
|
|
15712
|
+
return output.trim().length > 0;
|
|
15713
|
+
} catch {
|
|
15714
|
+
return false;
|
|
15715
|
+
}
|
|
15716
|
+
}
|
|
15717
|
+
function isHindsightContainerExists() {
|
|
15718
|
+
try {
|
|
15719
|
+
const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
|
|
15720
|
+
return output.trim().length > 0;
|
|
15721
|
+
} catch {
|
|
15722
|
+
return false;
|
|
15723
|
+
}
|
|
15724
|
+
}
|
|
15725
|
+
function resolveHindsightPerOpLlm(llm) {
|
|
15726
|
+
const out = [];
|
|
15727
|
+
if (!llm)
|
|
15728
|
+
return out;
|
|
15729
|
+
for (const op of HINDSIGHT_LLM_OPS) {
|
|
15730
|
+
const cfg = llm[op];
|
|
15731
|
+
if (!cfg)
|
|
15732
|
+
continue;
|
|
15733
|
+
const prefix = `HINDSIGHT_API_${op.toUpperCase()}_LLM`;
|
|
15734
|
+
const model = cfg.model?.trim();
|
|
15735
|
+
const provider = cfg.provider?.trim();
|
|
15736
|
+
const baseUrl = cfg.base_url?.trim();
|
|
15737
|
+
const apiKey = cfg.api_key?.trim();
|
|
15738
|
+
if (model)
|
|
15739
|
+
out.push([`${prefix}_MODEL`, model]);
|
|
15740
|
+
if (provider)
|
|
15741
|
+
out.push([`${prefix}_PROVIDER`, provider]);
|
|
15742
|
+
if (baseUrl)
|
|
15743
|
+
out.push([`${prefix}_BASE_URL`, baseUrl]);
|
|
15744
|
+
if (apiKey)
|
|
15745
|
+
out.push([`${prefix}_API_KEY`, apiKey]);
|
|
15746
|
+
}
|
|
15747
|
+
return out;
|
|
15748
|
+
}
|
|
15749
|
+
function resolveHindsightLlm(llm, litellm) {
|
|
15750
|
+
const provider = llm?.provider?.trim() || "claude-code";
|
|
15751
|
+
const model = llm?.model?.trim() || (litellm ? litellm.model ?? HINDSIGHT_DEFAULT_LITELLM_MODEL : HINDSIGHT_DEFAULT_MODEL);
|
|
15752
|
+
return { provider, model };
|
|
15753
|
+
}
|
|
15754
|
+
function startHindsight(ports, litellm, imageTag, llm) {
|
|
15755
|
+
const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
|
|
15756
|
+
const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
|
|
15757
|
+
const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm, litellm);
|
|
15758
|
+
const perOpLlm = resolveHindsightPerOpLlm(llm);
|
|
15759
|
+
const envArgs = [
|
|
15760
|
+
"-e",
|
|
15761
|
+
`HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
|
|
15762
|
+
"-e",
|
|
15763
|
+
`HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
|
|
15764
|
+
"-e",
|
|
15765
|
+
`HINDSIGHT_API_LLM_MODEL=${llmModel}`,
|
|
15766
|
+
...perOpLlm.flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
15767
|
+
"-e",
|
|
15768
|
+
`HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
|
|
15769
|
+
"-e",
|
|
15770
|
+
`HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
|
|
15771
|
+
"-e",
|
|
15772
|
+
`HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
|
|
15773
|
+
"-e",
|
|
15774
|
+
`HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
|
|
15775
|
+
"-e",
|
|
15776
|
+
`HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
|
|
15777
|
+
"-e",
|
|
15778
|
+
`HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
|
|
15779
|
+
"-e",
|
|
15780
|
+
`HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
|
|
15781
|
+
"-e",
|
|
15782
|
+
`HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
|
|
15783
|
+
"-e",
|
|
15784
|
+
`HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
|
|
15785
|
+
"-e",
|
|
15786
|
+
`HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
|
|
15787
|
+
];
|
|
15788
|
+
if (llmProvider === "claude-code") {
|
|
15789
|
+
envArgs.push("-e", `ANTHROPIC_MODEL=${llmModel}`);
|
|
15790
|
+
}
|
|
15791
|
+
if (litellm) {
|
|
15792
|
+
const litellmRoot = litellm.baseUrl.replace(/\/+$/, "");
|
|
15793
|
+
const anthropicBaseUrl = isClaudeModel(llmModel) ? `${litellmRoot}/anthropic` : litellmRoot;
|
|
15794
|
+
envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:${apiPort}`, "-e", `ANTHROPIC_BASE_URL=${anthropicBaseUrl}`, "-e", `ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}
|
|
15795
|
+
x-litellm-customer-id: hindsight
|
|
15796
|
+
x-litellm-tags: service:hindsight`);
|
|
15797
|
+
}
|
|
15798
|
+
const args = [
|
|
15799
|
+
"run",
|
|
15800
|
+
"-d",
|
|
15801
|
+
"--name",
|
|
15802
|
+
"switchroom-hindsight",
|
|
15803
|
+
"--restart",
|
|
15804
|
+
"always",
|
|
15805
|
+
`--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
|
|
15806
|
+
`--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
|
|
15807
|
+
`--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
|
|
15808
|
+
`--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
|
|
15809
|
+
"--health-cmd",
|
|
15810
|
+
litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
|
|
15811
|
+
"--health-interval",
|
|
15812
|
+
"30s",
|
|
15813
|
+
"--health-timeout",
|
|
15814
|
+
"5s",
|
|
15815
|
+
"--health-retries",
|
|
15816
|
+
"3",
|
|
15817
|
+
"--health-start-period",
|
|
15818
|
+
"60s"
|
|
15819
|
+
];
|
|
15820
|
+
if (litellm) {
|
|
15821
|
+
args.push("--network", "host");
|
|
15822
|
+
} else {
|
|
15823
|
+
args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
|
|
15824
|
+
}
|
|
15825
|
+
args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, hindsightImageRef(imageTag));
|
|
15826
|
+
execFileSync2("docker", args, { stdio: "pipe" });
|
|
15827
|
+
}
|
|
15828
|
+
function stopHindsight() {
|
|
15829
|
+
try {
|
|
15830
|
+
execFileSync2("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
|
|
15831
|
+
} catch {}
|
|
15832
|
+
try {
|
|
15833
|
+
execFileSync2("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
|
|
15834
|
+
} catch {}
|
|
15835
|
+
}
|
|
15836
|
+
function pullHindsightImage(imageTag) {
|
|
15837
|
+
execFileSync2("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
|
|
15838
|
+
}
|
|
15839
|
+
function getRunningHindsightPorts() {
|
|
15840
|
+
const CONTAINER_API_PORT = 8888;
|
|
15841
|
+
const CONTAINER_UI_PORT = 9999;
|
|
15842
|
+
const readPort = (containerPort) => {
|
|
15843
|
+
try {
|
|
15844
|
+
const out = execFileSync2("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
|
|
15845
|
+
const m = out.split(`
|
|
15846
|
+
`).map((l) => l.trim()).find((l) => l.length > 0);
|
|
15847
|
+
if (!m)
|
|
15848
|
+
return null;
|
|
15849
|
+
const port = Number(m.slice(m.lastIndexOf(":") + 1));
|
|
15850
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
15851
|
+
} catch {
|
|
15852
|
+
return null;
|
|
15853
|
+
}
|
|
15854
|
+
};
|
|
15855
|
+
const apiPort = readPort(CONTAINER_API_PORT);
|
|
15856
|
+
if (apiPort !== null) {
|
|
15857
|
+
const uiPort = readPort(CONTAINER_UI_PORT) ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
|
|
15858
|
+
return { apiPort, uiPort };
|
|
15859
|
+
}
|
|
15860
|
+
return getHindsightPortsFromEnv();
|
|
15861
|
+
}
|
|
15862
|
+
function getHindsightPortsFromEnv() {
|
|
15863
|
+
let env2;
|
|
15864
|
+
try {
|
|
15865
|
+
env2 = execFileSync2("docker", ["inspect", "--format", "{{.Config.Env}}", "switchroom-hindsight"], { stdio: "pipe", encoding: "utf-8" });
|
|
15866
|
+
} catch {
|
|
15867
|
+
return null;
|
|
15868
|
+
}
|
|
15869
|
+
const readVar = (name) => {
|
|
15870
|
+
const re = new RegExp(`(?:^|[\\s\\[])${name}=([^\\s\\]]+)`);
|
|
15871
|
+
const m = env2.match(re);
|
|
15872
|
+
if (!m)
|
|
15873
|
+
return null;
|
|
15874
|
+
const n = Number(m[1]);
|
|
15875
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
15876
|
+
};
|
|
15877
|
+
const apiPort = readVar("HINDSIGHT_API_PORT");
|
|
15878
|
+
if (apiPort === null)
|
|
15879
|
+
return null;
|
|
15880
|
+
const uiPort = readVar("HINDSIGHT_UI_PORT") ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
|
|
15881
|
+
return { apiPort, uiPort };
|
|
15882
|
+
}
|
|
15883
|
+
function getHindsightStatus() {
|
|
15884
|
+
try {
|
|
15885
|
+
const output = execFileSync2("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
|
|
15886
|
+
const status = output.trim();
|
|
15887
|
+
return status.length > 0 ? status : null;
|
|
15888
|
+
} catch {
|
|
15889
|
+
return null;
|
|
15890
|
+
}
|
|
15891
|
+
}
|
|
15892
|
+
function generateHindsightComposeSnippet(llm) {
|
|
15893
|
+
const { provider: llmProvider, model: llmModel } = resolveHindsightLlm(llm);
|
|
15894
|
+
const perOpLlm = resolveHindsightPerOpLlm(llm);
|
|
15895
|
+
const environment = [
|
|
15896
|
+
` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
|
|
15897
|
+
` - HINDSIGHT_API_LLM_PROVIDER=${llmProvider}`,
|
|
15898
|
+
` - HINDSIGHT_API_LLM_MODEL=${llmModel}`,
|
|
15899
|
+
...perOpLlm.map(([k, v]) => ` - ${k}=${v}`),
|
|
15900
|
+
...llmProvider === "claude-code" ? [` - ANTHROPIC_MODEL=${llmModel}`] : [],
|
|
15901
|
+
` - HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888`
|
|
15902
|
+
];
|
|
15903
|
+
return [
|
|
15904
|
+
"services:",
|
|
15905
|
+
" switchroom-hindsight:",
|
|
15906
|
+
` image: ${HINDSIGHT_IMAGE}`,
|
|
15907
|
+
" container_name: switchroom-hindsight",
|
|
15908
|
+
" ports:",
|
|
15909
|
+
` - "127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}:8888"`,
|
|
15910
|
+
' - "127.0.0.1:19999:9999"',
|
|
15911
|
+
" environment:",
|
|
15912
|
+
...environment,
|
|
15913
|
+
` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
|
|
15914
|
+
` - HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
|
|
15915
|
+
` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
|
|
15916
|
+
` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
|
|
15917
|
+
` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
|
|
15918
|
+
` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
|
|
15919
|
+
` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
|
|
15920
|
+
` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
|
|
15921
|
+
` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
|
|
15922
|
+
` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
|
|
15923
|
+
` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
|
|
15924
|
+
` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
|
|
15925
|
+
` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
|
|
15926
|
+
` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
|
|
15927
|
+
" healthcheck:",
|
|
15928
|
+
` test: ${JSON.stringify(["CMD", "python3", "-c", HINDSIGHT_HEALTHCHECK_PY])}`,
|
|
15929
|
+
" interval: 30s",
|
|
15930
|
+
" timeout: 5s",
|
|
15931
|
+
" retries: 3",
|
|
15932
|
+
" start_period: 60s",
|
|
15933
|
+
" volumes:",
|
|
15934
|
+
" - switchroom-hindsight-data:/home/hindsight/.pg0",
|
|
15935
|
+
" - switchroom-hindsight-backups:/backups",
|
|
15936
|
+
` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
|
|
15937
|
+
" tmpfs:",
|
|
15938
|
+
` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
|
|
15939
|
+
" restart: always",
|
|
15940
|
+
"",
|
|
15941
|
+
"volumes:",
|
|
15942
|
+
" switchroom-hindsight-data:",
|
|
15943
|
+
" switchroom-hindsight-backups:",
|
|
15944
|
+
` ${HINDSIGHT_BROKER_SOCK_VOLUME}:`,
|
|
15945
|
+
" external: true",
|
|
15946
|
+
" # Bound by the switchroom-auth-broker singleton in the main",
|
|
15947
|
+
" # switchroom compose project. Declare an `auth.consumers[]`",
|
|
15948
|
+
` # entry named "${HINDSIGHT_CONSUMER_NAME}" in switchroom.yaml.`
|
|
15949
|
+
].join(`
|
|
15950
|
+
`);
|
|
15951
|
+
}
|
|
15952
|
+
async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFAULT_UID) {
|
|
15953
|
+
const fs = await import("node:fs");
|
|
15954
|
+
const { parseDocument, isMap, isSeq, YAMLMap, YAMLSeq } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
|
|
15955
|
+
const { atomicWriteFileSync: atomicWriteFileSync2 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
|
|
15956
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
15957
|
+
const doc = parseDocument(raw);
|
|
15958
|
+
const root = doc.contents;
|
|
15959
|
+
if (!isMap(root)) {
|
|
15960
|
+
return { added: false, reason: "switchroom.yaml root is not a map" };
|
|
15961
|
+
}
|
|
15962
|
+
const authRaw = root.get("auth", true);
|
|
15963
|
+
let authNode;
|
|
15964
|
+
if (isMap(authRaw)) {
|
|
15965
|
+
authNode = authRaw;
|
|
15966
|
+
} else {
|
|
15967
|
+
authNode = new YAMLMap;
|
|
15968
|
+
doc.setIn(["auth"], authNode);
|
|
15969
|
+
}
|
|
15970
|
+
const consumersRaw = authNode.get("consumers", true);
|
|
15971
|
+
let consumersNode;
|
|
15972
|
+
if (isSeq(consumersRaw)) {
|
|
15973
|
+
consumersNode = consumersRaw;
|
|
15974
|
+
} else {
|
|
15975
|
+
consumersNode = new YAMLSeq;
|
|
15976
|
+
doc.setIn(["auth", "consumers"], consumersNode);
|
|
15977
|
+
}
|
|
15978
|
+
for (const item of consumersNode.items) {
|
|
15979
|
+
if (isMap(item)) {
|
|
15980
|
+
const name = item.get("name");
|
|
15981
|
+
if (name === HINDSIGHT_CONSUMER_NAME) {
|
|
15982
|
+
return { added: false, reason: "already present" };
|
|
15983
|
+
}
|
|
15984
|
+
}
|
|
15985
|
+
}
|
|
15986
|
+
const entry = new YAMLMap;
|
|
15987
|
+
entry.set("name", HINDSIGHT_CONSUMER_NAME);
|
|
15988
|
+
if (account !== undefined)
|
|
15989
|
+
entry.set("account", account);
|
|
15990
|
+
entry.set("uid", uid);
|
|
15991
|
+
consumersNode.add(entry);
|
|
15992
|
+
const out = String(doc);
|
|
15993
|
+
const tail = out.endsWith(`
|
|
15994
|
+
`) ? out : out + `
|
|
15995
|
+
`;
|
|
15996
|
+
let mode = 420;
|
|
15997
|
+
try {
|
|
15998
|
+
mode = fs.statSync(configPath).mode & 511;
|
|
15999
|
+
} catch {}
|
|
16000
|
+
atomicWriteFileSync2(configPath, tail, mode);
|
|
16001
|
+
return { added: true, reason: "added" };
|
|
16002
|
+
}
|
|
16003
|
+
var HINDSIGHT_DEFAULT_API_PORT = 18888, HINDSIGHT_DEFAULT_UI_PORT = 9999, HINDSIGHT_DEFAULT_MCP_URL, HINDSIGHT_DEFAULT_API_BASE_URL, HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000, HINDSIGHT_CONSUMER_NAME = "hindsight", HINDSIGHT_DEFAULT_UID = 11000, HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight", HINDSIGHT_IMAGE, HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5", HINDSIGHT_DEFAULT_LITELLM_MODEL = "openrouter/google/gemini-3.1-flash-lite", HINDSIGHT_DEFAULT_MCP_STATELESS = true, HINDSIGHT_BROKER_SOCK_VOLUME, HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true", HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150, HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4, HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8, HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1, HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2, HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 100, HINDSIGHT_DEFAULT_MEM_LIMIT = "8g", HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g", HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000, HINDSIGHT_DEFAULT_SHM_SIZE = "2g", HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)', HINDSIGHT_HEALTHCHECK_CMD, HINDSIGHT_LLM_OPS;
|
|
16004
|
+
var init_hindsight = __esm(() => {
|
|
16005
|
+
init_model_command();
|
|
16006
|
+
HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
|
|
16007
|
+
HINDSIGHT_DEFAULT_API_BASE_URL = HINDSIGHT_DEFAULT_MCP_URL.replace(/\/mcp\/?$/, "");
|
|
16008
|
+
HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
|
|
16009
|
+
HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
|
|
16010
|
+
HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
16011
|
+
HINDSIGHT_LLM_OPS = ["retain", "reflect", "consolidation"];
|
|
16012
|
+
});
|
|
16013
|
+
|
|
15125
16014
|
// src/memory/hindsight.ts
|
|
15126
16015
|
function generateHindsightMcpConfig(collection, memoryConfig) {
|
|
15127
|
-
const url = memoryConfig.config?.url ??
|
|
16016
|
+
const url = memoryConfig.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
15128
16017
|
return {
|
|
15129
16018
|
type: "http",
|
|
15130
16019
|
url,
|
|
@@ -15167,6 +16056,26 @@ function isHindsightEnabled(config) {
|
|
|
15167
16056
|
return false;
|
|
15168
16057
|
return config?.memory?.backend === "hindsight";
|
|
15169
16058
|
}
|
|
16059
|
+
function mergeDisposition(base, over) {
|
|
16060
|
+
if (!base && !over)
|
|
16061
|
+
return;
|
|
16062
|
+
const merged = { ...base ?? {}, ...over ?? {} };
|
|
16063
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
16064
|
+
}
|
|
16065
|
+
function resolveBankMissionExtras(memory, profileName) {
|
|
16066
|
+
const pd = PROFILE_MEMORY_DEFAULTS[profileName] ?? {};
|
|
16067
|
+
const out = {};
|
|
16068
|
+
const reflect = memory?.reflect_mission ?? pd.reflect_mission;
|
|
16069
|
+
if (reflect != null)
|
|
16070
|
+
out.reflect_mission = reflect;
|
|
16071
|
+
const observations = memory?.observations_mission ?? pd.observations_mission;
|
|
16072
|
+
if (observations != null)
|
|
16073
|
+
out.observations_mission = observations;
|
|
16074
|
+
const disposition = mergeDisposition(pd.disposition, memory?.disposition);
|
|
16075
|
+
if (disposition)
|
|
16076
|
+
out.disposition = disposition;
|
|
16077
|
+
return out;
|
|
16078
|
+
}
|
|
15170
16079
|
async function parseSseOrJson(resp) {
|
|
15171
16080
|
const text = await resp.text();
|
|
15172
16081
|
const dataLine = text.split(`
|
|
@@ -15253,9 +16162,19 @@ async function probeHindsight(apiUrl, opts) {
|
|
|
15253
16162
|
clearTimeout(timeout);
|
|
15254
16163
|
}
|
|
15255
16164
|
}
|
|
15256
|
-
async function
|
|
16165
|
+
async function ensureMentalModel(apiUrl, bankId, spec, opts) {
|
|
15257
16166
|
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
15258
16167
|
const timeoutMs = opts?.timeoutMs ?? 5000;
|
|
16168
|
+
const createArguments = {
|
|
16169
|
+
name: spec.name,
|
|
16170
|
+
source_query: spec.sourceQuery
|
|
16171
|
+
};
|
|
16172
|
+
if (spec.refreshAfterConsolidation !== undefined) {
|
|
16173
|
+
createArguments.trigger_refresh_after_consolidation = spec.refreshAfterConsolidation;
|
|
16174
|
+
}
|
|
16175
|
+
if (spec.maxTokens !== undefined) {
|
|
16176
|
+
createArguments.max_tokens = spec.maxTokens;
|
|
16177
|
+
}
|
|
15259
16178
|
try {
|
|
15260
16179
|
const controller = new AbortController;
|
|
15261
16180
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -15312,7 +16231,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
|
|
|
15312
16231
|
if (models && typeof models === "string") {
|
|
15313
16232
|
const parsed = JSON.parse(models);
|
|
15314
16233
|
const items = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
15315
|
-
if (items.some((m) => m?.name ===
|
|
16234
|
+
if (items.some((m) => m?.name === spec.name)) {
|
|
15316
16235
|
return { ok: true };
|
|
15317
16236
|
}
|
|
15318
16237
|
}
|
|
@@ -15333,10 +16252,7 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
|
|
|
15333
16252
|
method: "tools/call",
|
|
15334
16253
|
params: {
|
|
15335
16254
|
name: "create_mental_model",
|
|
15336
|
-
arguments:
|
|
15337
|
-
name: "user-profile",
|
|
15338
|
-
source_query: "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them."
|
|
15339
|
-
}
|
|
16255
|
+
arguments: createArguments
|
|
15340
16256
|
}
|
|
15341
16257
|
}),
|
|
15342
16258
|
signal: controller.signal
|
|
@@ -15360,6 +16276,33 @@ async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
|
|
|
15360
16276
|
return { ok: false, reason: String(err) };
|
|
15361
16277
|
}
|
|
15362
16278
|
}
|
|
16279
|
+
async function ensureUserProfileMentalModel(apiUrl, bankId, opts) {
|
|
16280
|
+
return ensureMentalModel(apiUrl, bankId, { name: "user-profile", sourceQuery: USER_PROFILE_SOURCE_QUERY }, opts);
|
|
16281
|
+
}
|
|
16282
|
+
async function ensureDeclaredMentalModels(apiUrl, bankId, models, opts) {
|
|
16283
|
+
if (!models || models.length === 0)
|
|
16284
|
+
return [];
|
|
16285
|
+
const outcomes = [];
|
|
16286
|
+
const seen = new Set;
|
|
16287
|
+
for (const m of models) {
|
|
16288
|
+
if (seen.has(m.name))
|
|
16289
|
+
continue;
|
|
16290
|
+
seen.add(m.name);
|
|
16291
|
+
const spec = {
|
|
16292
|
+
name: m.name,
|
|
16293
|
+
sourceQuery: m.source_query,
|
|
16294
|
+
refreshAfterConsolidation: m.refresh_after_consolidation,
|
|
16295
|
+
maxTokens: m.max_tokens
|
|
16296
|
+
};
|
|
16297
|
+
try {
|
|
16298
|
+
const result = await ensureMentalModel(apiUrl, bankId, spec, opts);
|
|
16299
|
+
outcomes.push(result.ok ? { name: m.name, ok: true } : { name: m.name, ok: false, reason: result.reason });
|
|
16300
|
+
} catch (err) {
|
|
16301
|
+
outcomes.push({ name: m.name, ok: false, reason: String(err) });
|
|
16302
|
+
}
|
|
16303
|
+
}
|
|
16304
|
+
return outcomes;
|
|
16305
|
+
}
|
|
15363
16306
|
async function createBank(apiUrl, bankId, opts) {
|
|
15364
16307
|
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
15365
16308
|
const timeoutMs = opts?.timeoutMs ?? 5000;
|
|
@@ -15487,8 +16430,24 @@ async function updateBankMissions(apiUrl, bankId, missions, opts) {
|
|
|
15487
16430
|
name: "update_bank",
|
|
15488
16431
|
arguments: {
|
|
15489
16432
|
bank_id: bankId,
|
|
15490
|
-
...missions.bank_mission != null ? { mission: missions.bank_mission } : {},
|
|
15491
|
-
...
|
|
16433
|
+
...missions.reflect_mission == null && missions.bank_mission != null ? { mission: missions.bank_mission } : {},
|
|
16434
|
+
...(() => {
|
|
16435
|
+
const configUpdates = {};
|
|
16436
|
+
if (missions.retain_mission != null)
|
|
16437
|
+
configUpdates.retain_mission = missions.retain_mission;
|
|
16438
|
+
if (missions.reflect_mission != null)
|
|
16439
|
+
configUpdates.reflect_mission = missions.reflect_mission;
|
|
16440
|
+
if (missions.observations_mission != null)
|
|
16441
|
+
configUpdates.observations_mission = missions.observations_mission;
|
|
16442
|
+
const d = missions.disposition;
|
|
16443
|
+
if (d?.skepticism != null)
|
|
16444
|
+
configUpdates.disposition_skepticism = d.skepticism;
|
|
16445
|
+
if (d?.literalism != null)
|
|
16446
|
+
configUpdates.disposition_literalism = d.literalism;
|
|
16447
|
+
if (d?.empathy != null)
|
|
16448
|
+
configUpdates.disposition_empathy = d.empathy;
|
|
16449
|
+
return Object.keys(configUpdates).length > 0 ? { config_updates: configUpdates } : {};
|
|
16450
|
+
})()
|
|
15492
16451
|
}
|
|
15493
16452
|
}
|
|
15494
16453
|
}),
|
|
@@ -15586,10 +16545,23 @@ async function addMemoryTag(apiUrl, bankId, memoryId, tag, opts) {
|
|
|
15586
16545
|
return { ok: false, reason: String(err) };
|
|
15587
16546
|
}
|
|
15588
16547
|
}
|
|
15589
|
-
var DEFAULT_RETAIN_MISSION, DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
|
|
15590
|
-
var
|
|
16548
|
+
var DEFAULT_RETAIN_MISSION, PROFILE_MEMORY_DEFAULTS, USER_PROFILE_SOURCE_QUERY = "What are the key facts, preferences, context, and communication style about the user I talk to? Summarize what matters for making the agent feel like it knows them.", DEMOTE_FROM_RECALL_TAG = "[demote-from-recall]";
|
|
16549
|
+
var init_hindsight2 = __esm(() => {
|
|
15591
16550
|
init_users();
|
|
16551
|
+
init_hindsight();
|
|
15592
16552
|
DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
|
|
16553
|
+
PROFILE_MEMORY_DEFAULTS = {
|
|
16554
|
+
"health-coach": {
|
|
16555
|
+
disposition: { skepticism: 2, literalism: 2, empathy: 5 },
|
|
16556
|
+
observations_mission: "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context \u2014 how habits, setbacks, and encouragement connect over time."
|
|
16557
|
+
},
|
|
16558
|
+
"executive-assistant": {
|
|
16559
|
+
disposition: { skepticism: 4, literalism: 4, empathy: 3 }
|
|
16560
|
+
},
|
|
16561
|
+
coding: {
|
|
16562
|
+
disposition: { skepticism: 4, literalism: 5, empathy: 2 }
|
|
16563
|
+
}
|
|
16564
|
+
};
|
|
15593
16565
|
});
|
|
15594
16566
|
|
|
15595
16567
|
// src/cli/helpers.ts
|
|
@@ -22398,7 +23370,8 @@ function getBuiltinDefaultSkillEntries() {
|
|
|
22398
23370
|
"switchroom-cli",
|
|
22399
23371
|
"switchroom-status",
|
|
22400
23372
|
"switchroom-health",
|
|
22401
|
-
"switchroom-runtime"
|
|
23373
|
+
"switchroom-runtime",
|
|
23374
|
+
"mental-model-curator"
|
|
22402
23375
|
];
|
|
22403
23376
|
return [
|
|
22404
23377
|
...anthropic.map((key) => ({ key, optOutKey: key, source: "anthropic" })),
|
|
@@ -22407,7 +23380,7 @@ function getBuiltinDefaultSkillEntries() {
|
|
|
22407
23380
|
}
|
|
22408
23381
|
var GOOGLE_WORKSPACE_MCP_PINNED_SHA = "9d69115b63e6bc2ef0d4b5d7a3b962396382b44c", MICROSOFT_WORKSPACE_MCP_PINNED_VERSION = "0.113.0", MICROSOFT_WORKSPACE_MCP_PACKAGE = "@softeria/ms-365-mcp-server", NOTION_MCP_PINNED_VERSION = "1.8.1", NOTION_MCP_PACKAGE = "@notionhq/notion-mcp-server", NOTION_TOKEN_ENV = "NOTION_TOKEN";
|
|
22409
23382
|
var init_scaffold_integration = __esm(() => {
|
|
22410
|
-
|
|
23383
|
+
init_hindsight2();
|
|
22411
23384
|
});
|
|
22412
23385
|
|
|
22413
23386
|
// src/config/microsoft-workspace-acl.ts
|
|
@@ -22425,7 +23398,7 @@ function shouldEmitMs365Mcp(agentName, agentMicrosoftAccount, microsoftAccounts)
|
|
|
22425
23398
|
}
|
|
22426
23399
|
|
|
22427
23400
|
// src/agents/reconcile-default-skills.ts
|
|
22428
|
-
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync as readlinkSync2, rmSync, symlinkSync } from "node:fs";
|
|
23401
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync as readlinkSync2, rmSync as rmSync2, symlinkSync } from "node:fs";
|
|
22429
23402
|
import { homedir as homedir3 } from "node:os";
|
|
22430
23403
|
import { join as join5, resolve as resolve5 } from "node:path";
|
|
22431
23404
|
function warnMissingPoolDir(poolDir) {
|
|
@@ -22495,7 +23468,7 @@ function reconcileAgentDefaultSkills(agentDir, optOuts = {}, defaults = getBuilt
|
|
|
22495
23468
|
}
|
|
22496
23469
|
if (currentTarget && isOwnedStaleLink(currentTarget, poolDir)) {
|
|
22497
23470
|
try {
|
|
22498
|
-
|
|
23471
|
+
rmSync2(dest, { force: true });
|
|
22499
23472
|
} catch {}
|
|
22500
23473
|
} else {
|
|
22501
23474
|
result.conflicts.push(entry.key);
|
|
@@ -22560,7 +23533,7 @@ import {
|
|
|
22560
23533
|
writeFileSync as writeFileSync2,
|
|
22561
23534
|
mkdirSync as mkdirSync4,
|
|
22562
23535
|
existsSync as existsSync8,
|
|
22563
|
-
renameSync,
|
|
23536
|
+
renameSync as renameSync2,
|
|
22564
23537
|
copyFileSync as copyFileSync2,
|
|
22565
23538
|
unlinkSync
|
|
22566
23539
|
} from "node:fs";
|
|
@@ -22600,7 +23573,7 @@ function saveTopicState(state, statePath) {
|
|
|
22600
23573
|
try {
|
|
22601
23574
|
writeFileSync2(tmp, JSON.stringify(state, null, 2) + `
|
|
22602
23575
|
`, "utf-8");
|
|
22603
|
-
|
|
23576
|
+
renameSync2(tmp, path);
|
|
22604
23577
|
} catch (err) {
|
|
22605
23578
|
try {
|
|
22606
23579
|
if (existsSync8(tmp))
|
|
@@ -22616,10 +23589,10 @@ var init_state = __esm(() => {
|
|
|
22616
23589
|
// src/vault/flock.ts
|
|
22617
23590
|
import {
|
|
22618
23591
|
existsSync as existsSync9,
|
|
22619
|
-
openSync as
|
|
22620
|
-
closeSync as
|
|
22621
|
-
writeSync,
|
|
22622
|
-
fsyncSync,
|
|
23592
|
+
openSync as openSync3,
|
|
23593
|
+
closeSync as closeSync3,
|
|
23594
|
+
writeSync as writeSync2,
|
|
23595
|
+
fsyncSync as fsyncSync2,
|
|
22623
23596
|
unlinkSync as unlinkSync2,
|
|
22624
23597
|
readFileSync as readFileSync8,
|
|
22625
23598
|
readdirSync as readdirSync4,
|
|
@@ -22741,7 +23714,7 @@ function acquireLock(vaultPath, options = {}) {
|
|
|
22741
23714
|
while (true) {
|
|
22742
23715
|
let fd = null;
|
|
22743
23716
|
try {
|
|
22744
|
-
fd =
|
|
23717
|
+
fd = openSync3(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
|
|
22745
23718
|
} catch (err) {
|
|
22746
23719
|
const code = err?.code ?? "";
|
|
22747
23720
|
if (code === "EEXIST") {
|
|
@@ -22777,11 +23750,11 @@ function acquireLock(vaultPath, options = {}) {
|
|
|
22777
23750
|
${Date.now()}
|
|
22778
23751
|
${process.argv[1] ?? ""}
|
|
22779
23752
|
`;
|
|
22780
|
-
|
|
22781
|
-
|
|
23753
|
+
writeSync2(fd, meta);
|
|
23754
|
+
fsyncSync2(fd);
|
|
22782
23755
|
} catch {
|
|
22783
23756
|
try {
|
|
22784
|
-
|
|
23757
|
+
closeSync3(fd);
|
|
22785
23758
|
} catch {}
|
|
22786
23759
|
try {
|
|
22787
23760
|
unlinkSync2(lockPath);
|
|
@@ -22792,7 +23765,7 @@ ${process.argv[1] ?? ""}
|
|
|
22792
23765
|
return {
|
|
22793
23766
|
release: () => {
|
|
22794
23767
|
try {
|
|
22795
|
-
|
|
23768
|
+
closeSync3(ownedFd);
|
|
22796
23769
|
} catch {}
|
|
22797
23770
|
try {
|
|
22798
23771
|
unlinkSync2(lockPath);
|
|
@@ -22849,22 +23822,22 @@ __export(exports_vault, {
|
|
|
22849
23822
|
KNOWN_VAULT_ARTIFACT_PATTERNS: () => KNOWN_VAULT_ARTIFACT_PATTERNS,
|
|
22850
23823
|
KNOWN_VAULT_ARTIFACT_NAMES: () => KNOWN_VAULT_ARTIFACT_NAMES
|
|
22851
23824
|
});
|
|
22852
|
-
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
23825
|
+
import { randomBytes as randomBytes2, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
22853
23826
|
import {
|
|
22854
23827
|
readFileSync as readFileSync9,
|
|
22855
|
-
writeSync as
|
|
23828
|
+
writeSync as writeSync3,
|
|
22856
23829
|
existsSync as existsSync10,
|
|
22857
|
-
renameSync as
|
|
23830
|
+
renameSync as renameSync3,
|
|
22858
23831
|
mkdirSync as mkdirSync5,
|
|
22859
23832
|
unlinkSync as unlinkSync3,
|
|
22860
|
-
fsyncSync as
|
|
22861
|
-
openSync as
|
|
22862
|
-
closeSync as
|
|
23833
|
+
fsyncSync as fsyncSync3,
|
|
23834
|
+
openSync as openSync4,
|
|
23835
|
+
closeSync as closeSync4,
|
|
22863
23836
|
lstatSync as lstatSync2,
|
|
22864
23837
|
realpathSync as realpathSync2
|
|
22865
23838
|
} from "node:fs";
|
|
22866
23839
|
import { dirname as dirname3, basename as basename3, resolve as resolve7 } from "node:path";
|
|
22867
|
-
function
|
|
23840
|
+
function atomicWriteFileSync2(path, data, mode) {
|
|
22868
23841
|
let effectivePath = path;
|
|
22869
23842
|
try {
|
|
22870
23843
|
if (existsSync10(path) && lstatSync2(path).isSymbolicLink()) {
|
|
@@ -22874,20 +23847,20 @@ function atomicWriteFileSync(path, data, mode) {
|
|
|
22874
23847
|
const dir = dirname3(resolve7(effectivePath));
|
|
22875
23848
|
const tmp = resolve7(dir, `.${basename3(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
22876
23849
|
try {
|
|
22877
|
-
const fd =
|
|
23850
|
+
const fd = openSync4(tmp, "wx", mode);
|
|
22878
23851
|
try {
|
|
22879
|
-
|
|
22880
|
-
|
|
23852
|
+
writeSync3(fd, data);
|
|
23853
|
+
fsyncSync3(fd);
|
|
22881
23854
|
} finally {
|
|
22882
|
-
|
|
23855
|
+
closeSync4(fd);
|
|
22883
23856
|
}
|
|
22884
|
-
|
|
23857
|
+
renameSync3(tmp, effectivePath);
|
|
22885
23858
|
try {
|
|
22886
|
-
const dirFd =
|
|
23859
|
+
const dirFd = openSync4(dir, "r");
|
|
22887
23860
|
try {
|
|
22888
|
-
|
|
23861
|
+
fsyncSync3(dirFd);
|
|
22889
23862
|
} finally {
|
|
22890
|
-
|
|
23863
|
+
closeSync4(dirFd);
|
|
22891
23864
|
}
|
|
22892
23865
|
} catch {}
|
|
22893
23866
|
} catch (err) {
|
|
@@ -22907,7 +23880,7 @@ function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCR
|
|
|
22907
23880
|
});
|
|
22908
23881
|
}
|
|
22909
23882
|
function encrypt(key, plaintext) {
|
|
22910
|
-
const iv =
|
|
23883
|
+
const iv = randomBytes2(12);
|
|
22911
23884
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
22912
23885
|
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
22913
23886
|
const tag = cipher.getAuthTag();
|
|
@@ -23012,7 +23985,7 @@ function createVault(passphrase, vaultPath) {
|
|
|
23012
23985
|
if (!existsSync10(dir)) {
|
|
23013
23986
|
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
23014
23987
|
}
|
|
23015
|
-
const salt =
|
|
23988
|
+
const salt = randomBytes2(16);
|
|
23016
23989
|
const key = deriveKey(passphrase, salt);
|
|
23017
23990
|
const vaultData = { secrets: {} };
|
|
23018
23991
|
const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
|
|
@@ -23023,7 +23996,7 @@ function createVault(passphrase, vaultPath) {
|
|
|
23023
23996
|
tag,
|
|
23024
23997
|
kdf: { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }
|
|
23025
23998
|
};
|
|
23026
|
-
|
|
23999
|
+
atomicWriteFileSync2(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
|
|
23027
24000
|
}
|
|
23028
24001
|
function openVault(passphrase, vaultPath) {
|
|
23029
24002
|
if (!existsSync10(vaultPath)) {
|
|
@@ -23080,7 +24053,7 @@ function saveVault(passphrase, vaultPath, secrets) {
|
|
|
23080
24053
|
vaultFile.iv = iv;
|
|
23081
24054
|
vaultFile.data = data;
|
|
23082
24055
|
vaultFile.tag = tag;
|
|
23083
|
-
|
|
24056
|
+
atomicWriteFileSync2(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
|
|
23084
24057
|
} finally {
|
|
23085
24058
|
if (releaseLock !== null) {
|
|
23086
24059
|
try {
|
|
@@ -23262,7 +24235,7 @@ function parseSocketIdentity(socketPath, spec) {
|
|
|
23262
24235
|
}
|
|
23263
24236
|
|
|
23264
24237
|
// src/vault/broker/peercred.ts
|
|
23265
|
-
import { execFileSync } from "node:child_process";
|
|
24238
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
23266
24239
|
import { readFileSync as readFileSync10, readlinkSync as readlinkSync3, fstatSync } from "node:fs";
|
|
23267
24240
|
function socketPathToIdentity(socketPath) {
|
|
23268
24241
|
return parseSocketIdentity(socketPath, {
|
|
@@ -23435,7 +24408,7 @@ function identify(socketPath, socket, execFileSyncOverride) {
|
|
|
23435
24408
|
if (process.platform !== "linux") {
|
|
23436
24409
|
return null;
|
|
23437
24410
|
}
|
|
23438
|
-
const runner = execFileSyncOverride ??
|
|
24411
|
+
const runner = execFileSyncOverride ?? execFileSync3;
|
|
23439
24412
|
let pid = null;
|
|
23440
24413
|
if (socket !== undefined) {
|
|
23441
24414
|
const fd = fdFromSocket(socket);
|
|
@@ -23919,7 +24892,8 @@ function brokerIsComposeManaged() {
|
|
|
23919
24892
|
return fs.existsSync(OPERATOR_SOCKET_PATH);
|
|
23920
24893
|
}
|
|
23921
24894
|
function vaultTokenFilePath(agentSlug) {
|
|
23922
|
-
|
|
24895
|
+
const base = process.env.SWITCHROOM_AGENTS_DIR || join6(homedir4(), ".switchroom", "agents");
|
|
24896
|
+
return join6(base, agentSlug, ".vault-token");
|
|
23923
24897
|
}
|
|
23924
24898
|
function readVaultTokenFile(agentSlug) {
|
|
23925
24899
|
const filePath = vaultTokenFilePath(agentSlug);
|
|
@@ -24295,13 +25269,13 @@ __export(exports_resolver, {
|
|
|
24295
25269
|
});
|
|
24296
25270
|
import {
|
|
24297
25271
|
chmodSync,
|
|
24298
|
-
closeSync as
|
|
25272
|
+
closeSync as closeSync5,
|
|
24299
25273
|
mkdirSync as mkdirSync6,
|
|
24300
25274
|
mkdtempSync,
|
|
24301
|
-
openSync as
|
|
24302
|
-
rmSync as
|
|
25275
|
+
openSync as openSync5,
|
|
25276
|
+
rmSync as rmSync3,
|
|
24303
25277
|
statSync as statSync6,
|
|
24304
|
-
writeSync as
|
|
25278
|
+
writeSync as writeSync4
|
|
24305
25279
|
} from "node:fs";
|
|
24306
25280
|
import { join as join7 } from "node:path";
|
|
24307
25281
|
import { tmpdir } from "node:os";
|
|
@@ -24329,13 +25303,13 @@ function registerCleanupHook() {
|
|
|
24329
25303
|
const cleanup = () => {
|
|
24330
25304
|
for (const dir of materializedDirs) {
|
|
24331
25305
|
try {
|
|
24332
|
-
|
|
25306
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
24333
25307
|
} catch {}
|
|
24334
25308
|
}
|
|
24335
25309
|
materializedDirs.clear();
|
|
24336
25310
|
if (cachedRoot) {
|
|
24337
25311
|
try {
|
|
24338
|
-
|
|
25312
|
+
rmSync3(cachedRoot, { recursive: true, force: true });
|
|
24339
25313
|
} catch {}
|
|
24340
25314
|
cachedRoot = null;
|
|
24341
25315
|
}
|
|
@@ -24366,18 +25340,18 @@ function materializationRoot() {
|
|
|
24366
25340
|
}
|
|
24367
25341
|
function writeFileExclusive(filePath, content) {
|
|
24368
25342
|
const buf = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
24369
|
-
const fd =
|
|
25343
|
+
const fd = openSync5(filePath, fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL, 384);
|
|
24370
25344
|
try {
|
|
24371
|
-
|
|
25345
|
+
writeSync4(fd, buf);
|
|
24372
25346
|
} finally {
|
|
24373
|
-
|
|
25347
|
+
closeSync5(fd);
|
|
24374
25348
|
}
|
|
24375
25349
|
}
|
|
24376
25350
|
function materializeFilesEntry(key, files) {
|
|
24377
25351
|
const dir = join7(materializationRoot(), key);
|
|
24378
25352
|
if (materializedDirs.has(dir)) {
|
|
24379
25353
|
try {
|
|
24380
|
-
|
|
25354
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
24381
25355
|
} catch {}
|
|
24382
25356
|
}
|
|
24383
25357
|
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
@@ -24401,13 +25375,13 @@ function materializeFilesEntry(key, files) {
|
|
|
24401
25375
|
function cleanupMaterializedSecrets() {
|
|
24402
25376
|
for (const dir of materializedDirs) {
|
|
24403
25377
|
try {
|
|
24404
|
-
|
|
25378
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
24405
25379
|
} catch {}
|
|
24406
25380
|
}
|
|
24407
25381
|
materializedDirs.clear();
|
|
24408
25382
|
if (cachedRoot) {
|
|
24409
25383
|
try {
|
|
24410
|
-
|
|
25384
|
+
rmSync3(cachedRoot, { recursive: true, force: true });
|
|
24411
25385
|
} catch {}
|
|
24412
25386
|
cachedRoot = null;
|
|
24413
25387
|
}
|
|
@@ -24728,7 +25702,7 @@ var init_onboarding = __esm(() => {
|
|
|
24728
25702
|
});
|
|
24729
25703
|
|
|
24730
25704
|
// src/repos/bare-clone.ts
|
|
24731
|
-
import { execFileSync as
|
|
25705
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
24732
25706
|
import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "node:fs";
|
|
24733
25707
|
import { resolve as resolve9 } from "node:path";
|
|
24734
25708
|
function bareClonePath(slug) {
|
|
@@ -24741,7 +25715,7 @@ function ensureBareClone(slug, url) {
|
|
|
24741
25715
|
if (!existsSync13(clonePath)) {
|
|
24742
25716
|
process.stderr.write(`[switchroom] repo "${slug}": cloning ${url} \u2026
|
|
24743
25717
|
`);
|
|
24744
|
-
|
|
25718
|
+
execFileSync4("git", ["clone", "--bare", url, clonePath], {
|
|
24745
25719
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24746
25720
|
});
|
|
24747
25721
|
process.stderr.write(`[switchroom] repo "${slug}": bare clone ready at ${clonePath}
|
|
@@ -24750,7 +25724,7 @@ function ensureBareClone(slug, url) {
|
|
|
24750
25724
|
process.stderr.write(`[switchroom] repo "${slug}": fetching ${clonePath} \u2026
|
|
24751
25725
|
`);
|
|
24752
25726
|
try {
|
|
24753
|
-
|
|
25727
|
+
execFileSync4("git", ["fetch", "--all"], {
|
|
24754
25728
|
cwd: clonePath,
|
|
24755
25729
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24756
25730
|
});
|
|
@@ -24767,7 +25741,7 @@ var init_bare_clone = __esm(() => {
|
|
|
24767
25741
|
});
|
|
24768
25742
|
|
|
24769
25743
|
// src/repos/agent-worktree.ts
|
|
24770
|
-
import { execFileSync as
|
|
25744
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
24771
25745
|
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync5 } from "node:fs";
|
|
24772
25746
|
import { join as join9, resolve as resolve10 } from "node:path";
|
|
24773
25747
|
function agentBranchName(agentName) {
|
|
@@ -24778,7 +25752,7 @@ function agentWorktreePath(agentDir, slug) {
|
|
|
24778
25752
|
}
|
|
24779
25753
|
function isWorktreeDirty(worktreePath) {
|
|
24780
25754
|
try {
|
|
24781
|
-
const out =
|
|
25755
|
+
const out = execFileSync5("git", ["status", "--porcelain"], {
|
|
24782
25756
|
cwd: worktreePath,
|
|
24783
25757
|
stdio: ["ignore", "pipe", "pipe"],
|
|
24784
25758
|
encoding: "utf-8"
|
|
@@ -24790,7 +25764,7 @@ function isWorktreeDirty(worktreePath) {
|
|
|
24790
25764
|
}
|
|
24791
25765
|
function headShortSha(worktreePath) {
|
|
24792
25766
|
try {
|
|
24793
|
-
return
|
|
25767
|
+
return execFileSync5("git", ["rev-parse", "--short", "HEAD"], {
|
|
24794
25768
|
cwd: worktreePath,
|
|
24795
25769
|
stdio: ["ignore", "pipe", "pipe"],
|
|
24796
25770
|
encoding: "utf-8"
|
|
@@ -24801,7 +25775,7 @@ function headShortSha(worktreePath) {
|
|
|
24801
25775
|
}
|
|
24802
25776
|
function branchExistsInBare(bareClonePath2, branchName) {
|
|
24803
25777
|
try {
|
|
24804
|
-
|
|
25778
|
+
execFileSync5("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
|
|
24805
25779
|
cwd: bareClonePath2,
|
|
24806
25780
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24807
25781
|
});
|
|
@@ -24812,7 +25786,7 @@ function branchExistsInBare(bareClonePath2, branchName) {
|
|
|
24812
25786
|
}
|
|
24813
25787
|
function resolveDefaultBranch(bareClonePath2) {
|
|
24814
25788
|
try {
|
|
24815
|
-
const out =
|
|
25789
|
+
const out = execFileSync5("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
24816
25790
|
cwd: bareClonePath2,
|
|
24817
25791
|
stdio: ["ignore", "pipe", "pipe"],
|
|
24818
25792
|
encoding: "utf-8"
|
|
@@ -24830,12 +25804,12 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
|
|
|
24830
25804
|
if (!existsSync14(worktreePath)) {
|
|
24831
25805
|
mkdirSync9(join9(agentDir, "work"), { recursive: true });
|
|
24832
25806
|
if (branchExistsInBare(bareClonePath2, branch)) {
|
|
24833
|
-
|
|
25807
|
+
execFileSync5("git", ["worktree", "add", worktreePath, branch], {
|
|
24834
25808
|
cwd: bareClonePath2,
|
|
24835
25809
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24836
25810
|
});
|
|
24837
25811
|
} else {
|
|
24838
|
-
|
|
25812
|
+
execFileSync5("git", [
|
|
24839
25813
|
"worktree",
|
|
24840
25814
|
"add",
|
|
24841
25815
|
worktreePath,
|
|
@@ -24863,11 +25837,11 @@ function ensureAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
|
|
|
24863
25837
|
};
|
|
24864
25838
|
}
|
|
24865
25839
|
try {
|
|
24866
|
-
|
|
25840
|
+
execFileSync5("git", ["fetch", "origin"], {
|
|
24867
25841
|
cwd: worktreePath,
|
|
24868
25842
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24869
25843
|
});
|
|
24870
|
-
|
|
25844
|
+
execFileSync5("git", ["merge", "--ff-only", `origin/${defaultBranch}`], {
|
|
24871
25845
|
cwd: worktreePath,
|
|
24872
25846
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24873
25847
|
});
|
|
@@ -24885,7 +25859,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
|
|
|
24885
25859
|
const branch = agentBranchName(agentName);
|
|
24886
25860
|
if (existsSync14(worktreePath)) {
|
|
24887
25861
|
try {
|
|
24888
|
-
|
|
25862
|
+
execFileSync5("git", ["worktree", "remove", "--force", worktreePath], {
|
|
24889
25863
|
cwd: bareClonePath2,
|
|
24890
25864
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24891
25865
|
});
|
|
@@ -24897,7 +25871,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
|
|
|
24897
25871
|
}
|
|
24898
25872
|
if (branchExistsInBare(bareClonePath2, branch)) {
|
|
24899
25873
|
try {
|
|
24900
|
-
|
|
25874
|
+
execFileSync5("git", ["branch", "-D", branch], {
|
|
24901
25875
|
cwd: bareClonePath2,
|
|
24902
25876
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24903
25877
|
});
|
|
@@ -24908,7 +25882,7 @@ function removeAgentWorktree(agentName, slug, bareClonePath2, agentDir) {
|
|
|
24908
25882
|
}
|
|
24909
25883
|
}
|
|
24910
25884
|
try {
|
|
24911
|
-
|
|
25885
|
+
execFileSync5("git", ["worktree", "prune"], {
|
|
24912
25886
|
cwd: bareClonePath2,
|
|
24913
25887
|
stdio: ["ignore", "pipe", "pipe"]
|
|
24914
25888
|
});
|
|
@@ -24925,19 +25899,19 @@ import {
|
|
|
24925
25899
|
writeFileSync as writeFileSync4,
|
|
24926
25900
|
appendFileSync as appendFileSync2,
|
|
24927
25901
|
readFileSync as readFileSync13,
|
|
24928
|
-
renameSync as
|
|
25902
|
+
renameSync as renameSync4,
|
|
24929
25903
|
chmodSync as chmodSync2,
|
|
24930
25904
|
symlinkSync as symlinkSync2,
|
|
24931
25905
|
copyFileSync as copyFileSync4,
|
|
24932
25906
|
readdirSync as readdirSync6,
|
|
24933
|
-
rmSync as
|
|
25907
|
+
rmSync as rmSync4,
|
|
24934
25908
|
statSync as statSync7,
|
|
24935
25909
|
lstatSync as lstatSync3,
|
|
24936
25910
|
readlinkSync as readlinkSync4,
|
|
24937
25911
|
unlinkSync as unlinkSync4
|
|
24938
25912
|
} from "node:fs";
|
|
24939
25913
|
import { homedir as homedir5 } from "node:os";
|
|
24940
|
-
import { execSync, execFileSync as
|
|
25914
|
+
import { execSync, execFileSync as execFileSync6 } from "node:child_process";
|
|
24941
25915
|
import { join as join10, resolve as resolve11 } from "node:path";
|
|
24942
25916
|
import { createHash as createHash2 } from "node:crypto";
|
|
24943
25917
|
function prependReplyDiscipline(rendered, context) {
|
|
@@ -25071,13 +26045,13 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
|
|
|
25071
26045
|
`));
|
|
25072
26046
|
}
|
|
25073
26047
|
try {
|
|
25074
|
-
|
|
26048
|
+
execFileSync6("chown", ["-R", `${uid}:${uid}`, ...paths], {
|
|
25075
26049
|
stdio: ["ignore", "ignore", "pipe"]
|
|
25076
26050
|
});
|
|
25077
26051
|
return { chowned: true, paths };
|
|
25078
26052
|
} catch {}
|
|
25079
26053
|
try {
|
|
25080
|
-
|
|
26054
|
+
execFileSync6("sudo", ["chown", "-R", `${uid}:${uid}`, ...paths], {
|
|
25081
26055
|
stdio: "inherit"
|
|
25082
26056
|
});
|
|
25083
26057
|
return { chowned: true, paths };
|
|
@@ -25271,7 +26245,7 @@ function migrateLegacySkillsDir(agentDir, skillsPool) {
|
|
|
25271
26245
|
}
|
|
25272
26246
|
if (target && target.startsWith(skillsPool)) {
|
|
25273
26247
|
try {
|
|
25274
|
-
|
|
26248
|
+
rmSync4(entryPath, { force: true });
|
|
25275
26249
|
} catch {}
|
|
25276
26250
|
}
|
|
25277
26251
|
}
|
|
@@ -25306,7 +26280,7 @@ function syncGlobalSkills(agentDir, declared, skillsDirOverride) {
|
|
|
25306
26280
|
} catch {}
|
|
25307
26281
|
if (target && target.startsWith(skillsPool)) {
|
|
25308
26282
|
try {
|
|
25309
|
-
|
|
26283
|
+
rmSync4(dest, { force: true });
|
|
25310
26284
|
} catch {}
|
|
25311
26285
|
} else {
|
|
25312
26286
|
continue;
|
|
@@ -25336,7 +26310,7 @@ function syncGlobalSkills(agentDir, declared, skillsDirOverride) {
|
|
|
25336
26310
|
continue;
|
|
25337
26311
|
}
|
|
25338
26312
|
if (linkTarget && linkTarget.startsWith(skillsPool)) {
|
|
25339
|
-
|
|
26313
|
+
rmSync4(entryPath, { force: true });
|
|
25340
26314
|
}
|
|
25341
26315
|
}
|
|
25342
26316
|
}
|
|
@@ -25391,7 +26365,7 @@ function installSwitchroomSkills(agentDir, opts = {}) {
|
|
|
25391
26365
|
if (currentTarget !== join10(builtinSkillsDir, name))
|
|
25392
26366
|
continue;
|
|
25393
26367
|
try {
|
|
25394
|
-
|
|
26368
|
+
rmSync4(dest, { force: true });
|
|
25395
26369
|
} catch {}
|
|
25396
26370
|
}
|
|
25397
26371
|
return;
|
|
@@ -25414,7 +26388,7 @@ function installSwitchroomSkills(agentDir, opts = {}) {
|
|
|
25414
26388
|
if (currentTarget === src)
|
|
25415
26389
|
continue;
|
|
25416
26390
|
try {
|
|
25417
|
-
|
|
26391
|
+
rmSync4(dest, { force: true });
|
|
25418
26392
|
} catch {}
|
|
25419
26393
|
} else {
|
|
25420
26394
|
continue;
|
|
@@ -25666,7 +26640,7 @@ function migrateLegacyAgentsMdIfPresent(agentWorkspaceDir, created) {
|
|
|
25666
26640
|
}
|
|
25667
26641
|
const content = readFileSync13(agentsMd, "utf-8");
|
|
25668
26642
|
writeFileSync4(claudeMd, content, "utf-8");
|
|
25669
|
-
|
|
26643
|
+
rmSync4(agentsMd);
|
|
25670
26644
|
created.push(claudeMd);
|
|
25671
26645
|
console.log(source_default.dim(` migrated legacy workspace/AGENTS.md \u2192 workspace/CLAUDE.md (content preserved)`));
|
|
25672
26646
|
}
|
|
@@ -25682,9 +26656,9 @@ function ensureClaudeMdSymlinks(agentWorkspaceDir, changes) {
|
|
|
25682
26656
|
const target = readlinkSync4(linkPath);
|
|
25683
26657
|
if (target === "CLAUDE.md")
|
|
25684
26658
|
continue;
|
|
25685
|
-
|
|
26659
|
+
rmSync4(linkPath);
|
|
25686
26660
|
} else {
|
|
25687
|
-
|
|
26661
|
+
rmSync4(linkPath);
|
|
25688
26662
|
}
|
|
25689
26663
|
}
|
|
25690
26664
|
symlinkSync2("CLAUDE.md", linkPath);
|
|
@@ -25725,7 +26699,7 @@ Thumbs.db
|
|
|
25725
26699
|
execSync("git add -A", { cwd: workspaceDir, stdio: "pipe" });
|
|
25726
26700
|
const userEmail = process.env.GIT_AUTHOR_EMAIL || "switchroom@localhost";
|
|
25727
26701
|
const userName = process.env.GIT_AUTHOR_NAME || "Switchroom Agent";
|
|
25728
|
-
|
|
26702
|
+
execFileSync6("git", [
|
|
25729
26703
|
"-c",
|
|
25730
26704
|
`user.email=${userEmail}`,
|
|
25731
26705
|
"-c",
|
|
@@ -25763,27 +26737,36 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
|
|
|
25763
26737
|
}
|
|
25764
26738
|
const destPath = join10(agentDir, ".claude", "plugins", "hindsight-memory");
|
|
25765
26739
|
const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
|
|
26740
|
+
const retainConfig = resolveHindsightRetainConfig(switchroomConfig, agentName);
|
|
25766
26741
|
let expectedSettings = null;
|
|
25767
26742
|
try {
|
|
25768
26743
|
const vendorSettingsPath = join10(sourcePath, "settings.json");
|
|
25769
26744
|
if (existsSync15(vendorSettingsPath)) {
|
|
25770
|
-
expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks);
|
|
26745
|
+
expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks, retainConfig);
|
|
25771
26746
|
}
|
|
25772
26747
|
} catch {}
|
|
25773
26748
|
const upToDate = dirContentEquals(sourcePath, destPath, expectedSettings != null ? { "settings.json": expectedSettings } : undefined);
|
|
25774
26749
|
if (!upToDate) {
|
|
25775
26750
|
if (existsSync15(destPath)) {
|
|
25776
|
-
|
|
26751
|
+
rmSync4(destPath, { recursive: true, force: true });
|
|
25777
26752
|
}
|
|
25778
26753
|
copyDirRecursive2(sourcePath, destPath);
|
|
25779
26754
|
}
|
|
25780
|
-
applyHindsightSettingsOverrides(destPath, additionalBanks);
|
|
26755
|
+
applyHindsightSettingsOverrides(destPath, additionalBanks, retainConfig);
|
|
25781
26756
|
const bankId = agentMemory?.collection ?? agentName;
|
|
25782
|
-
const mcpUrl = memory.config?.url ??
|
|
26757
|
+
const mcpUrl = memory.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
25783
26758
|
const apiBaseUrl = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
25784
26759
|
return { pluginDir: destPath, apiBaseUrl, bankId };
|
|
25785
26760
|
}
|
|
25786
|
-
function
|
|
26761
|
+
function resolveHindsightRetainConfig(switchroomConfig, agentName) {
|
|
26762
|
+
const resolved = switchroomConfig ? resolveAgentConfig(switchroomConfig.defaults, switchroomConfig.profiles, switchroomConfig.agents[agentName] ?? {}) : undefined;
|
|
26763
|
+
const retain = resolved?.memory?.retain;
|
|
26764
|
+
return {
|
|
26765
|
+
everyNTurns: retain?.every_n_turns ?? HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS,
|
|
26766
|
+
overlapTurns: retain?.overlap_turns ?? HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS
|
|
26767
|
+
};
|
|
26768
|
+
}
|
|
26769
|
+
function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks, retainConfig) {
|
|
25787
26770
|
const settingsPath = join10(pluginDestPath, "settings.json");
|
|
25788
26771
|
if (!existsSync15(settingsPath))
|
|
25789
26772
|
return;
|
|
@@ -25793,41 +26776,51 @@ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
|
|
|
25793
26776
|
} catch {
|
|
25794
26777
|
return;
|
|
25795
26778
|
}
|
|
25796
|
-
const next = renderHindsightSettingsOverrides(raw, additionalBanks);
|
|
26779
|
+
const next = renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig);
|
|
25797
26780
|
if (next == null)
|
|
25798
26781
|
return;
|
|
25799
26782
|
writeFileSyncIfChanged(settingsPath, next);
|
|
25800
26783
|
}
|
|
25801
|
-
function renderHindsightSettingsOverrides(raw, additionalBanks) {
|
|
26784
|
+
function renderHindsightSettingsOverrides(raw, additionalBanks, retainConfig) {
|
|
25802
26785
|
let settings;
|
|
25803
26786
|
try {
|
|
25804
26787
|
settings = JSON.parse(raw);
|
|
25805
26788
|
} catch {
|
|
25806
26789
|
return null;
|
|
25807
26790
|
}
|
|
25808
|
-
settings.retainEveryNTurns =
|
|
26791
|
+
settings.retainEveryNTurns = retainConfig.everyNTurns;
|
|
25809
26792
|
settings.retainMode = "chunked";
|
|
26793
|
+
settings.retainOverlapTurns = retainConfig.overlapTurns;
|
|
25810
26794
|
settings.recallMaxMemories = 8;
|
|
25811
26795
|
settings.recallMinOverlap = 0.1;
|
|
25812
26796
|
settings.recallTypes = ["world", "experience", "observation"];
|
|
25813
26797
|
settings.recallSkipTrivial = true;
|
|
26798
|
+
settings.directiveCaptureNudge = true;
|
|
25814
26799
|
if (additionalBanks.length > 0) {
|
|
25815
26800
|
settings.recallAdditionalBanks = [...additionalBanks];
|
|
25816
26801
|
}
|
|
25817
26802
|
return JSON.stringify(settings, null, 2) + `
|
|
25818
26803
|
`;
|
|
25819
26804
|
}
|
|
26805
|
+
function resolvePermissionDefaultMode(agentConfig, switchroomConfig) {
|
|
26806
|
+
const raw = agentConfig.permission_mode ?? switchroomConfig?.defaults?.permission_mode ?? SWITCHROOM_DEFAULT_PERMISSION_MODE;
|
|
26807
|
+
if (VALID_SETTINGS_DEFAULT_MODES.includes(raw)) {
|
|
26808
|
+
return raw;
|
|
26809
|
+
}
|
|
26810
|
+
console.warn(source_default.yellow(`[switchroom] permission_mode "${raw}" has no settings.json defaultMode ` + `equivalent \u2014 using "${SWITCHROOM_DEFAULT_PERMISSION_MODE}" for ` + `permissions.defaultMode (the --permission-mode CLI flag still carries "${raw}").`));
|
|
26811
|
+
return SWITCHROOM_DEFAULT_PERMISSION_MODE;
|
|
26812
|
+
}
|
|
25820
26813
|
function buildWorkspaceContext(args) {
|
|
25821
26814
|
const {
|
|
25822
26815
|
name,
|
|
25823
26816
|
agentDir,
|
|
25824
26817
|
agentConfig,
|
|
25825
26818
|
telegramConfig,
|
|
26819
|
+
switchroomConfig,
|
|
25826
26820
|
switchroomConfigPath,
|
|
25827
26821
|
topicId,
|
|
25828
26822
|
tools,
|
|
25829
26823
|
permissionAllow,
|
|
25830
|
-
hasAllWildcard,
|
|
25831
26824
|
resolvedBotToken,
|
|
25832
26825
|
rawBotToken,
|
|
25833
26826
|
hindsightAutoRecallEnabled,
|
|
@@ -25838,6 +26831,7 @@ function buildWorkspaceContext(args) {
|
|
|
25838
26831
|
hindsightRecallMinOverlap,
|
|
25839
26832
|
hindsightRecallTypes,
|
|
25840
26833
|
hindsightRecallSkipTrivial,
|
|
26834
|
+
hindsightDirectiveCaptureNudge,
|
|
25841
26835
|
hindsightTopicAliasesJson,
|
|
25842
26836
|
hindsightSenderBanksJson,
|
|
25843
26837
|
hindsightTopicFilterMode
|
|
@@ -25859,7 +26853,7 @@ function buildWorkspaceContext(args) {
|
|
|
25859
26853
|
...INTERACTIVE_TUI_FLEET_DENY_TOOLS
|
|
25860
26854
|
]),
|
|
25861
26855
|
permissionAllow,
|
|
25862
|
-
|
|
26856
|
+
defaultMode: resolvePermissionDefaultMode(agentConfig, switchroomConfig),
|
|
25863
26857
|
memory: agentConfig.memory,
|
|
25864
26858
|
model: agentConfig.model,
|
|
25865
26859
|
mcpServers: agentConfig.mcp_servers ? filterMcpServers(agentConfig.mcp_servers) : agentConfig.mcp_servers,
|
|
@@ -25882,6 +26876,7 @@ function buildWorkspaceContext(args) {
|
|
|
25882
26876
|
hindsightRecallMinOverlap,
|
|
25883
26877
|
hindsightRecallTypes,
|
|
25884
26878
|
hindsightRecallSkipTrivial,
|
|
26879
|
+
hindsightDirectiveCaptureNudge,
|
|
25885
26880
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
25886
26881
|
hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
|
|
25887
26882
|
hindsightTopicFilterMode,
|
|
@@ -26052,12 +27047,13 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
26052
27047
|
]);
|
|
26053
27048
|
const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
|
|
26054
27049
|
const hindsightBankId = agentConfig.memory?.collection ?? name;
|
|
26055
|
-
const hindsightApiBaseUrl = switchroomConfig?.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") :
|
|
27050
|
+
const hindsightApiBaseUrl = switchroomConfig?.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
|
|
26056
27051
|
const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
|
|
26057
27052
|
const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
|
|
26058
27053
|
const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
|
|
26059
27054
|
const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
|
|
26060
27055
|
const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
|
|
27056
|
+
const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
|
|
26061
27057
|
const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
|
|
26062
27058
|
const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
|
|
26063
27059
|
const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
|
|
@@ -26084,6 +27080,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
26084
27080
|
hindsightRecallMinOverlap,
|
|
26085
27081
|
hindsightRecallTypes,
|
|
26086
27082
|
hindsightRecallSkipTrivial,
|
|
27083
|
+
hindsightDirectiveCaptureNudge,
|
|
26087
27084
|
hindsightTopicAliasesJson,
|
|
26088
27085
|
hindsightSenderBanksJson,
|
|
26089
27086
|
hindsightTopicFilterMode
|
|
@@ -26452,12 +27449,12 @@ ${body}
|
|
|
26452
27449
|
if (target === "workspace/SOUL.md") {
|
|
26453
27450
|
skipped.push(agentSoulPath);
|
|
26454
27451
|
} else {
|
|
26455
|
-
|
|
27452
|
+
rmSync4(agentSoulPath);
|
|
26456
27453
|
symlinkSync2("workspace/SOUL.md", agentSoulPath);
|
|
26457
27454
|
created.push(agentSoulPath);
|
|
26458
27455
|
}
|
|
26459
27456
|
} else {
|
|
26460
|
-
|
|
27457
|
+
rmSync4(agentSoulPath);
|
|
26461
27458
|
symlinkSync2("workspace/SOUL.md", agentSoulPath);
|
|
26462
27459
|
created.push(agentSoulPath);
|
|
26463
27460
|
}
|
|
@@ -26490,8 +27487,10 @@ ${body}
|
|
|
26490
27487
|
const userBankMission = agentConfig.memory?.bank_mission;
|
|
26491
27488
|
const userRetainMission = agentConfig.memory?.retain_mission;
|
|
26492
27489
|
const seededRetainMission = userRetainMission ?? DEFAULT_RETAIN_MISSION;
|
|
27490
|
+
const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
|
|
26493
27491
|
const missions = {
|
|
26494
|
-
retain_mission: seededRetainMission
|
|
27492
|
+
retain_mission: seededRetainMission,
|
|
27493
|
+
...extras
|
|
26495
27494
|
};
|
|
26496
27495
|
if (userBankMission) {
|
|
26497
27496
|
missions.bank_mission = userBankMission;
|
|
@@ -26506,6 +27505,17 @@ ${body}
|
|
|
26506
27505
|
}).catch((err) => {
|
|
26507
27506
|
console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
|
|
26508
27507
|
});
|
|
27508
|
+
ensureDeclaredMentalModels(apiUrl, hindsightBankId, agentConfig.memory?.mental_models, { timeoutMs: 5000 }).then((outcomes) => {
|
|
27509
|
+
for (const o of outcomes) {
|
|
27510
|
+
if (o.ok) {
|
|
27511
|
+
console.log(` ${source_default.green("\u2713")} Mental model "${o.name}" ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
|
|
27512
|
+
} else {
|
|
27513
|
+
console.warn(` ${source_default.yellow("\u26a0")} Failed to ensure mental model "${o.name}" for ${formatAgentBankLabel(name, hindsightBankId)}: ${o.reason}`);
|
|
27514
|
+
}
|
|
27515
|
+
}
|
|
27516
|
+
}).catch((err) => {
|
|
27517
|
+
console.warn(` ${source_default.yellow("\u26a0")} Mental model ensure error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
|
|
27518
|
+
});
|
|
26509
27519
|
});
|
|
26510
27520
|
}
|
|
26511
27521
|
for (const f of rewrittenWithBackup) {
|
|
@@ -26759,7 +27769,7 @@ function buildSettingsHooksBlock(p) {
|
|
|
26759
27769
|
hooks: [
|
|
26760
27770
|
{
|
|
26761
27771
|
type: "command",
|
|
26762
|
-
command: wrap("hook:workspace-dynamic", `${useInjectOnChange ? `env SWITCHROOM_INJECT_ON_CHANGE=
|
|
27772
|
+
command: wrap("hook:workspace-dynamic", `${useInjectOnChange ? "" : `env SWITCHROOM_INJECT_ON_CHANGE=0 `}bash "${join10(DOCKER_BIN_PATH, "workspace-dynamic-hook.sh")}"`),
|
|
26763
27773
|
timeout: 5
|
|
26764
27774
|
}
|
|
26765
27775
|
]
|
|
@@ -26884,7 +27894,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
26884
27894
|
mkdirSync10(workspaceDir, { recursive: true });
|
|
26885
27895
|
const legacyContent = readFileSync13(legacyCustomPath, "utf-8");
|
|
26886
27896
|
writeFileSync4(newCustomPath, legacyContent, "utf-8");
|
|
26887
|
-
|
|
27897
|
+
rmSync4(legacyCustomPath);
|
|
26888
27898
|
console.log(source_default.green(` moved CLAUDE.custom.md \u2192 workspace/CLAUDE.custom.md`));
|
|
26889
27899
|
}
|
|
26890
27900
|
const tools = agentConfig.tools ?? { allow: [], deny: [] };
|
|
@@ -26923,12 +27933,13 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
26923
27933
|
const resolvedBotToken = resolveBotToken(rawBotToken);
|
|
26924
27934
|
const hindsightAutoRecallEnabled = hindsightEnabled && agentConfig.memory?.auto_recall !== false;
|
|
26925
27935
|
const hindsightBankId = agentConfig.memory?.collection ?? name;
|
|
26926
|
-
const hindsightApiBaseUrl = switchroomConfig.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") :
|
|
27936
|
+
const hindsightApiBaseUrl = switchroomConfig.memory?.config?.url ? switchroomConfig.memory.config.url.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
|
|
26927
27937
|
const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
|
|
26928
27938
|
const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
|
|
26929
27939
|
const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
|
|
26930
27940
|
const hindsightRecallTypes = agentConfig.memory?.recall?.types?.length ? agentConfig.memory.recall.types.join(",") : undefined;
|
|
26931
27941
|
const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
|
|
27942
|
+
const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
|
|
26932
27943
|
const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
|
|
26933
27944
|
const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
|
|
26934
27945
|
const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
|
|
@@ -26968,6 +27979,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
26968
27979
|
hindsightRecallMinOverlap,
|
|
26969
27980
|
hindsightRecallTypes,
|
|
26970
27981
|
hindsightRecallSkipTrivial,
|
|
27982
|
+
hindsightDirectiveCaptureNudge,
|
|
26971
27983
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
26972
27984
|
hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
|
|
26973
27985
|
hindsightTopicFilterMode,
|
|
@@ -27107,11 +28119,7 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
|
|
|
27107
28119
|
settings.permissions = settings.permissions ?? {};
|
|
27108
28120
|
settings.permissions.allow = desiredAllow;
|
|
27109
28121
|
settings.permissions.deny = desiredDeny;
|
|
27110
|
-
|
|
27111
|
-
settings.permissions.defaultMode = "acceptEdits";
|
|
27112
|
-
} else {
|
|
27113
|
-
delete settings.permissions.defaultMode;
|
|
27114
|
-
}
|
|
28122
|
+
settings.permissions.defaultMode = resolvePermissionDefaultMode(agentConfig, switchroomConfig);
|
|
27115
28123
|
const mcpServers = {};
|
|
27116
28124
|
const hindsightEntry = getHindsightSettingsEntry(name, switchroomConfig);
|
|
27117
28125
|
if (hindsightEntry) {
|
|
@@ -27369,6 +28377,7 @@ ${body}
|
|
|
27369
28377
|
hindsightRecallMinOverlap,
|
|
27370
28378
|
hindsightRecallTypes,
|
|
27371
28379
|
hindsightRecallSkipTrivial,
|
|
28380
|
+
hindsightDirectiveCaptureNudge,
|
|
27372
28381
|
hindsightTopicAliasesJson,
|
|
27373
28382
|
hindsightSenderBanksJson,
|
|
27374
28383
|
hindsightTopicFilterMode
|
|
@@ -27397,12 +28406,12 @@ ${body}
|
|
|
27397
28406
|
if (stat.isSymbolicLink()) {
|
|
27398
28407
|
const target = readlinkSync4(agentSoulPath);
|
|
27399
28408
|
if (target !== "workspace/SOUL.md") {
|
|
27400
|
-
|
|
28409
|
+
rmSync4(agentSoulPath);
|
|
27401
28410
|
symlinkSync2("workspace/SOUL.md", agentSoulPath);
|
|
27402
28411
|
changes.push(agentSoulPath);
|
|
27403
28412
|
}
|
|
27404
28413
|
} else {
|
|
27405
|
-
|
|
28414
|
+
rmSync4(agentSoulPath);
|
|
27406
28415
|
symlinkSync2("workspace/SOUL.md", agentSoulPath);
|
|
27407
28416
|
changes.push(agentSoulPath);
|
|
27408
28417
|
}
|
|
@@ -27446,14 +28455,15 @@ ${body}
|
|
|
27446
28455
|
bankOpsChain.then((bankReady) => {
|
|
27447
28456
|
if (!bankReady)
|
|
27448
28457
|
return;
|
|
27449
|
-
|
|
27450
|
-
|
|
27451
|
-
|
|
27452
|
-
|
|
27453
|
-
|
|
27454
|
-
|
|
27455
|
-
|
|
27456
|
-
|
|
28458
|
+
const extras = resolveBankMissionExtras(agentConfig.memory, agentConfig.extends ?? DEFAULT_PROFILE);
|
|
28459
|
+
const missions = { ...extras };
|
|
28460
|
+
if (agentConfig.memory?.bank_mission) {
|
|
28461
|
+
missions.bank_mission = agentConfig.memory.bank_mission;
|
|
28462
|
+
}
|
|
28463
|
+
if (agentConfig.memory?.retain_mission) {
|
|
28464
|
+
missions.retain_mission = agentConfig.memory.retain_mission;
|
|
28465
|
+
}
|
|
28466
|
+
if (Object.keys(missions).length > 0) {
|
|
27457
28467
|
updateBankMissions(apiUrl, hindsightBankId, missions, { timeoutMs: 5000 }).then((result) => {
|
|
27458
28468
|
if (result.ok) {
|
|
27459
28469
|
console.log(` ${source_default.green("\u2713")} Bank missions updated for ${formatAgentBankLabel(name, hindsightBankId)}`);
|
|
@@ -27464,6 +28474,17 @@ ${body}
|
|
|
27464
28474
|
console.warn(` ${source_default.yellow("\u26a0")} Bank mission update error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
|
|
27465
28475
|
});
|
|
27466
28476
|
}
|
|
28477
|
+
ensureDeclaredMentalModels(apiUrl, hindsightBankId, agentConfig.memory?.mental_models, { timeoutMs: 5000 }).then((outcomes) => {
|
|
28478
|
+
for (const o of outcomes) {
|
|
28479
|
+
if (o.ok) {
|
|
28480
|
+
console.log(` ${source_default.green("\u2713")} Mental model "${o.name}" ready for ${formatAgentBankLabel(name, hindsightBankId)}`);
|
|
28481
|
+
} else {
|
|
28482
|
+
console.warn(` ${source_default.yellow("\u26a0")} Failed to ensure mental model "${o.name}" for ${formatAgentBankLabel(name, hindsightBankId)}: ${o.reason}`);
|
|
28483
|
+
}
|
|
28484
|
+
}
|
|
28485
|
+
}).catch((err) => {
|
|
28486
|
+
console.warn(` ${source_default.yellow("\u26a0")} Mental model ensure error for ${formatAgentBankLabel(name, hindsightBankId)}: ${err}`);
|
|
28487
|
+
});
|
|
27467
28488
|
});
|
|
27468
28489
|
}
|
|
27469
28490
|
return {
|
|
@@ -27558,7 +28579,7 @@ function reconcileConfiguredGroup(accessPath, agentConfig, telegramConfig) {
|
|
|
27558
28579
|
const tmp = accessPath + ".tmp";
|
|
27559
28580
|
writeFileSync4(tmp, JSON.stringify(access, null, 2) + `
|
|
27560
28581
|
`, { mode: 384 });
|
|
27561
|
-
|
|
28582
|
+
renameSync4(tmp, accessPath);
|
|
27562
28583
|
console.log(source_default.green(` registered supergroup ${forumChatId} in access.json ` + `(responds to all topics; requireMention=false)`));
|
|
27563
28584
|
}
|
|
27564
28585
|
function buildAccessJson2(agentConfig, telegramConfig, resolvedTopicId, userId) {
|
|
@@ -27902,7 +28923,7 @@ in doubt, shorter and plainer wins.
|
|
|
27902
28923
|
|
|
27903
28924
|
Every turn that answers a user message ends with a user-visible \`reply\`
|
|
27904
28925
|
\u2014 Telegram is all the user sees; your terminal output
|
|
27905
|
-
never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
|
|
28926
|
+
never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, HINDSIGHT_DEFAULT_RETAIN_EVERY_N_TURNS = 3, HINDSIGHT_DEFAULT_RETAIN_OVERLAP_TURNS = 1, VALID_SETTINGS_DEFAULT_MODES, SWITCHROOM_DEFAULT_PERMISSION_MODE = "acceptEdits", DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
|
|
27906
28927
|
var init_scaffold = __esm(() => {
|
|
27907
28928
|
init_source();
|
|
27908
28929
|
init_cron_unit_name();
|
|
@@ -27916,13 +28937,14 @@ var init_scaffold = __esm(() => {
|
|
|
27916
28937
|
init_profiles();
|
|
27917
28938
|
init_scaffold_integration();
|
|
27918
28939
|
init_reconcile_default_skills();
|
|
27919
|
-
|
|
28940
|
+
init_hindsight2();
|
|
27920
28941
|
init_state();
|
|
27921
28942
|
init_paths();
|
|
27922
28943
|
init_loader();
|
|
27923
28944
|
init_resolver();
|
|
27924
28945
|
init_vault();
|
|
27925
28946
|
init_onboarding();
|
|
28947
|
+
init_hindsight();
|
|
27926
28948
|
init_bare_clone();
|
|
27927
28949
|
init_agent_worktree();
|
|
27928
28950
|
REPO_ROOT = resolve11(import.meta.dirname, "../..");
|
|
@@ -27940,8 +28962,31 @@ var init_scaffold = __esm(() => {
|
|
|
27940
28962
|
"mcp__switchroom-telegram__progress_update"
|
|
27941
28963
|
];
|
|
27942
28964
|
HINDSIGHT_MCP_TOOLS = [
|
|
27943
|
-
"
|
|
27944
|
-
"
|
|
28965
|
+
"mcp__hindsight__recall",
|
|
28966
|
+
"mcp__hindsight__retain",
|
|
28967
|
+
"mcp__hindsight__sync_retain",
|
|
28968
|
+
"mcp__hindsight__reflect",
|
|
28969
|
+
"mcp__hindsight__create_directive",
|
|
28970
|
+
"mcp__hindsight__update_bank",
|
|
28971
|
+
"mcp__hindsight__delete_directive",
|
|
28972
|
+
"mcp__hindsight__list_directives",
|
|
28973
|
+
"mcp__hindsight__create_bank",
|
|
28974
|
+
"mcp__hindsight__get_bank",
|
|
28975
|
+
"mcp__hindsight__get_bank_stats",
|
|
28976
|
+
"mcp__hindsight__list_banks",
|
|
28977
|
+
"mcp__hindsight__delete_bank",
|
|
28978
|
+
"mcp__hindsight__get_memory",
|
|
28979
|
+
"mcp__hindsight__list_memories",
|
|
28980
|
+
"mcp__hindsight__clear_memories",
|
|
28981
|
+
"mcp__hindsight__get_document",
|
|
28982
|
+
"mcp__hindsight__list_documents",
|
|
28983
|
+
"mcp__hindsight__delete_document",
|
|
28984
|
+
"mcp__hindsight__get_mental_model",
|
|
28985
|
+
"mcp__hindsight__list_mental_models",
|
|
28986
|
+
"mcp__hindsight__list_tags",
|
|
28987
|
+
"mcp__hindsight__get_operation",
|
|
28988
|
+
"mcp__hindsight__list_operations",
|
|
28989
|
+
"mcp__hindsight__cancel_operation"
|
|
27945
28990
|
];
|
|
27946
28991
|
AGENT_CONFIG_MCP_TOOLS = [
|
|
27947
28992
|
"mcp__agent-config",
|
|
@@ -27993,6 +29038,12 @@ var init_scaffold = __esm(() => {
|
|
|
27993
29038
|
"Agent",
|
|
27994
29039
|
"ExitPlanMode"
|
|
27995
29040
|
];
|
|
29041
|
+
VALID_SETTINGS_DEFAULT_MODES = [
|
|
29042
|
+
"default",
|
|
29043
|
+
"acceptEdits",
|
|
29044
|
+
"plan",
|
|
29045
|
+
"bypassPermissions"
|
|
29046
|
+
];
|
|
27996
29047
|
DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
|
|
27997
29048
|
INTEGRATION_MCP_RESOLVERS = [
|
|
27998
29049
|
{
|
|
@@ -28237,6 +29288,7 @@ function describeAgents(config, litellmConfirmedAgents) {
|
|
|
28237
29288
|
baseUrl: resolved.litellm?.base_url ?? config.litellm?.base_url,
|
|
28238
29289
|
smallFastModel: resolved.litellm?.small_fast_model ?? config.litellm?.small_fast_model ?? "claude-haiku-4-5-20251001"
|
|
28239
29290
|
},
|
|
29291
|
+
model: resolveMainModel(resolved.model),
|
|
28240
29292
|
timezone: resolveTimezone(config, resolved, {
|
|
28241
29293
|
onUtcFallback: () => {
|
|
28242
29294
|
console.warn(` \u26a0 timezone: no explicit timezone set and server detection resolved to UTC ` + `for agent "${name}" \u2014 cron schedules and the per-turn time hint will ` + `run in UTC. Add \`switchroom.timezone: "Region/City"\` (e.g. ` + `"Australia/Melbourne") to switchroom.yaml to silence this warning.`);
|
|
@@ -28669,7 +29721,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
28669
29721
|
const root = a.litellm.baseUrl.replace(/\/+$/, "");
|
|
28670
29722
|
env2.SWITCHROOM_LITELLM = "1";
|
|
28671
29723
|
env2.SWITCHROOM_LITELLM_BASE = root;
|
|
28672
|
-
env2.ANTHROPIC_BASE_URL = `${root}/anthropic
|
|
29724
|
+
env2.ANTHROPIC_BASE_URL = isClaudeModel(a.model) ? `${root}/anthropic` : root;
|
|
28673
29725
|
env2.ANTHROPIC_SMALL_FAST_MODEL = a.litellm.smallFastModel;
|
|
28674
29726
|
}
|
|
28675
29727
|
env2.SWITCHROOM_VOICE_ENGINE = voiceEngine;
|
|
@@ -28767,6 +29819,8 @@ var init_compose = __esm(() => {
|
|
|
28767
29819
|
init_schema();
|
|
28768
29820
|
init_cron_routing();
|
|
28769
29821
|
init_tier_selector();
|
|
29822
|
+
init_scaffold();
|
|
29823
|
+
init_model_command();
|
|
28770
29824
|
init_merge();
|
|
28771
29825
|
init_timezone();
|
|
28772
29826
|
init_peercred();
|
|
@@ -29033,7 +30087,7 @@ function resolveHostHomeForCompose() {
|
|
|
29033
30087
|
}
|
|
29034
30088
|
return homedir6();
|
|
29035
30089
|
}
|
|
29036
|
-
async function
|
|
30090
|
+
async function computeComposeContent(opts) {
|
|
29037
30091
|
const release = resolveRelease({ override: opts.releaseOverride, root: opts.config.release });
|
|
29038
30092
|
const imageTag = resolveImageTag(release);
|
|
29039
30093
|
const operatorUid = resolveOperatorUid();
|
|
@@ -29057,6 +30111,11 @@ async function writeComposeFile(opts) {
|
|
|
29057
30111
|
previous = null;
|
|
29058
30112
|
}
|
|
29059
30113
|
const previousImageTag = previous ? AGENT_IMAGE_TAG_RE.exec(previous)?.[1] ?? null : null;
|
|
30114
|
+
return { content, imageTag, previous, previousImageTag };
|
|
30115
|
+
}
|
|
30116
|
+
async function writeComposeFile(opts) {
|
|
30117
|
+
const { content, imageTag, previous, previousImageTag } = await computeComposeContent(opts);
|
|
30118
|
+
const operatorUid = resolveOperatorUid();
|
|
29060
30119
|
await mkdir(dirname6(opts.composePath), { recursive: true });
|
|
29061
30120
|
if (previous !== null) {
|
|
29062
30121
|
try {
|
|
@@ -29089,7 +30148,7 @@ var init_write_compose = __esm(() => {
|
|
|
29089
30148
|
});
|
|
29090
30149
|
|
|
29091
30150
|
// src/agents/tmux.ts
|
|
29092
|
-
import { execFileSync as
|
|
30151
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
29093
30152
|
function sendAgentInterrupt(opts) {
|
|
29094
30153
|
const { agentName } = opts;
|
|
29095
30154
|
const attempts = typeof opts.attempts === "number" && opts.attempts > 0 ? opts.attempts : 1;
|
|
@@ -29099,7 +30158,7 @@ function sendAgentInterrupt(opts) {
|
|
|
29099
30158
|
let lastError = null;
|
|
29100
30159
|
for (let i = 0;i < attempts; i++) {
|
|
29101
30160
|
try {
|
|
29102
|
-
|
|
30161
|
+
execFileSync7("tmux", args, {
|
|
29103
30162
|
timeout: 3000,
|
|
29104
30163
|
stdio: ["ignore", "pipe", "pipe"]
|
|
29105
30164
|
});
|
|
@@ -29140,7 +30199,7 @@ var init_compose_env = () => {};
|
|
|
29140
30199
|
import { resolve as resolve13 } from "node:path";
|
|
29141
30200
|
import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync6 } from "node:fs";
|
|
29142
30201
|
import { homedir as homedir7 } from "node:os";
|
|
29143
|
-
import { execFileSync as
|
|
30202
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
29144
30203
|
function resolveSwitchroomHome(explicit) {
|
|
29145
30204
|
if (explicit && explicit.length > 0)
|
|
29146
30205
|
return explicit;
|
|
@@ -29171,7 +30230,7 @@ function bringUpAgentService(opts) {
|
|
|
29171
30230
|
const dockerBin = opts.dockerBin ?? "docker";
|
|
29172
30231
|
const stdio = opts.stdio ?? "inherit";
|
|
29173
30232
|
for (const svc of ["vault-broker", "approval-kernel", "switchroom-auth-broker"]) {
|
|
29174
|
-
|
|
30233
|
+
execFileSync8(dockerBin, [
|
|
29175
30234
|
"compose",
|
|
29176
30235
|
"-f",
|
|
29177
30236
|
composePath,
|
|
@@ -29183,7 +30242,7 @@ function bringUpAgentService(opts) {
|
|
|
29183
30242
|
svc
|
|
29184
30243
|
], { stdio });
|
|
29185
30244
|
}
|
|
29186
|
-
|
|
30245
|
+
execFileSync8(dockerBin, [
|
|
29187
30246
|
"compose",
|
|
29188
30247
|
"-f",
|
|
29189
30248
|
composePath,
|
|
@@ -29202,7 +30261,7 @@ var init_docker_fleet = __esm(() => {
|
|
|
29202
30261
|
});
|
|
29203
30262
|
|
|
29204
30263
|
// src/agents/singleton-reconcile.ts
|
|
29205
|
-
import { execFileSync as
|
|
30264
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
29206
30265
|
import { readFileSync as readFileSync16 } from "node:fs";
|
|
29207
30266
|
function resolveSingletonServices(voiceEngine) {
|
|
29208
30267
|
const engine = voiceEngine ?? loadHostCapabilities()?.voice.engine ?? "cloud";
|
|
@@ -29228,7 +30287,7 @@ function readPinnedSingletonImages(composeText, services = SINGLETON_SERVICES) {
|
|
|
29228
30287
|
}
|
|
29229
30288
|
function defaultInspectImage(dockerBin, container) {
|
|
29230
30289
|
try {
|
|
29231
|
-
const out =
|
|
30290
|
+
const out = execFileSync9(dockerBin, ["inspect", "-f", "{{.Config.Image}}", container], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
29232
30291
|
return out.length > 0 ? out : null;
|
|
29233
30292
|
} catch {
|
|
29234
30293
|
return null;
|
|
@@ -29254,7 +30313,7 @@ function detectSingletonDrift(deps) {
|
|
|
29254
30313
|
});
|
|
29255
30314
|
}
|
|
29256
30315
|
function defaultRecreate(dockerBin, project, composeFile, service) {
|
|
29257
|
-
|
|
30316
|
+
execFileSync9(dockerBin, ["compose", "-p", project, "-f", composeFile, ...composeEnvFileArgs(composeFile), "up", "-d", "--no-deps", service], { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
|
|
29258
30317
|
}
|
|
29259
30318
|
function reconcileSingletons(deps) {
|
|
29260
30319
|
const dockerBin = deps.dockerBin ?? "docker";
|
|
@@ -29297,8 +30356,8 @@ var init_singleton_reconcile = __esm(() => {
|
|
|
29297
30356
|
});
|
|
29298
30357
|
|
|
29299
30358
|
// src/agents/lifecycle.ts
|
|
29300
|
-
import { execFileSync as
|
|
29301
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync14, writeFileSync as writeFileSync7, renameSync as
|
|
30359
|
+
import { execFileSync as execFileSync10, spawn, spawnSync } from "node:child_process";
|
|
30360
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync14, writeFileSync as writeFileSync7, renameSync as renameSync5, readFileSync as readFileSync17 } from "node:fs";
|
|
29302
30361
|
import { resolve as resolve14, join as join14 } from "node:path";
|
|
29303
30362
|
import { connect } from "node:net";
|
|
29304
30363
|
function cleanShutdownMarkerPathForAgent(name) {
|
|
@@ -29320,7 +30379,7 @@ function writeRestartReasonMarker(name, reason, opts = {}) {
|
|
|
29320
30379
|
const marker = { ts: Date.now(), signal: "SIGTERM", reason };
|
|
29321
30380
|
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
29322
30381
|
writeFileSync7(tmp, JSON.stringify(marker), "utf-8");
|
|
29323
|
-
|
|
30382
|
+
renameSync5(tmp, path);
|
|
29324
30383
|
} catch {}
|
|
29325
30384
|
}
|
|
29326
30385
|
function buildCliRestartReason(opts) {
|
|
@@ -29328,7 +30387,7 @@ function buildCliRestartReason(opts) {
|
|
|
29328
30387
|
if (!buildCommit)
|
|
29329
30388
|
return "cli: restart";
|
|
29330
30389
|
try {
|
|
29331
|
-
const head =
|
|
30390
|
+
const head = execFileSync10("git", ["rev-parse", "HEAD"], {
|
|
29332
30391
|
cwd: cwd ?? process.cwd(),
|
|
29333
30392
|
encoding: "utf-8",
|
|
29334
30393
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -29339,7 +30398,7 @@ function buildCliRestartReason(opts) {
|
|
|
29339
30398
|
return "cli: restart";
|
|
29340
30399
|
let subject = "";
|
|
29341
30400
|
try {
|
|
29342
|
-
subject =
|
|
30401
|
+
subject = execFileSync10("git", ["log", "-1", "--pretty=%s", head], {
|
|
29343
30402
|
cwd: cwd ?? process.cwd(),
|
|
29344
30403
|
encoding: "utf-8",
|
|
29345
30404
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -29369,7 +30428,7 @@ function reconcileSingletonImages(log) {
|
|
|
29369
30428
|
}
|
|
29370
30429
|
function dockerSync(args) {
|
|
29371
30430
|
try {
|
|
29372
|
-
return
|
|
30431
|
+
return execFileSync10("docker", args, {
|
|
29373
30432
|
encoding: "utf-8",
|
|
29374
30433
|
stdio: ["ignore", "pipe", "pipe"]
|
|
29375
30434
|
}).trim();
|
|
@@ -29758,12 +30817,12 @@ var init_lifecycle = __esm(() => {
|
|
|
29758
30817
|
});
|
|
29759
30818
|
|
|
29760
30819
|
// src/auth/pane-ready-probe.ts
|
|
29761
|
-
import { execFileSync as
|
|
30820
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
29762
30821
|
function defaultPaneReadyDeps() {
|
|
29763
30822
|
return {
|
|
29764
30823
|
capturePane(sessionName) {
|
|
29765
30824
|
try {
|
|
29766
|
-
return
|
|
30825
|
+
return execFileSync12("tmux", ["capture-pane", "-p", "-t", sessionName, "-S", "-200"], {
|
|
29767
30826
|
encoding: "utf-8",
|
|
29768
30827
|
stdio: ["pipe", "pipe", "pipe"]
|
|
29769
30828
|
}).trim();
|
|
@@ -29805,12 +30864,12 @@ import {
|
|
|
29805
30864
|
mkdirSync as mkdirSync16,
|
|
29806
30865
|
readFileSync as readFileSync21,
|
|
29807
30866
|
readdirSync as readdirSync10,
|
|
29808
|
-
renameSync as
|
|
29809
|
-
rmSync as
|
|
30867
|
+
renameSync as renameSync7,
|
|
30868
|
+
rmSync as rmSync5,
|
|
29810
30869
|
statSync as statSync12,
|
|
29811
30870
|
writeFileSync as writeFileSync9
|
|
29812
30871
|
} from "node:fs";
|
|
29813
|
-
import { randomBytes as
|
|
30872
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
29814
30873
|
import { join as join17 } from "node:path";
|
|
29815
30874
|
function claudeDir(agentDir) {
|
|
29816
30875
|
return join17(agentDir, ".claude");
|
|
@@ -29944,13 +31003,13 @@ function syncLegacyFromActive(agentDir) {
|
|
|
29944
31003
|
}
|
|
29945
31004
|
function atomicCopy(src, dest, mode) {
|
|
29946
31005
|
const contents = readFileSync21(src);
|
|
29947
|
-
const tmp = `${dest}.tmp-${process.pid}-${
|
|
31006
|
+
const tmp = `${dest}.tmp-${process.pid}-${randomBytes3(4).toString("hex")}`;
|
|
29948
31007
|
try {
|
|
29949
31008
|
writeFileSync9(tmp, contents, { mode });
|
|
29950
|
-
|
|
31009
|
+
renameSync7(tmp, dest);
|
|
29951
31010
|
} catch (err) {
|
|
29952
31011
|
try {
|
|
29953
|
-
|
|
31012
|
+
rmSync5(tmp);
|
|
29954
31013
|
} catch {}
|
|
29955
31014
|
throw err;
|
|
29956
31015
|
}
|
|
@@ -30044,7 +31103,7 @@ var init_accounts = __esm(() => {
|
|
|
30044
31103
|
});
|
|
30045
31104
|
|
|
30046
31105
|
// src/auth/manager.ts
|
|
30047
|
-
import { execFileSync as
|
|
31106
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
30048
31107
|
import {
|
|
30049
31108
|
readFileSync as readFileSync22,
|
|
30050
31109
|
readdirSync as readdirSync11,
|
|
@@ -30052,7 +31111,7 @@ import {
|
|
|
30052
31111
|
writeFileSync as writeFileSync10,
|
|
30053
31112
|
mkdirSync as mkdirSync17,
|
|
30054
31113
|
mkdtempSync as mkdtempSync2,
|
|
30055
|
-
rmSync as
|
|
31114
|
+
rmSync as rmSync6,
|
|
30056
31115
|
chmodSync as chmodSync3,
|
|
30057
31116
|
statSync as statSync13
|
|
30058
31117
|
} from "node:fs";
|
|
@@ -30095,14 +31154,14 @@ function authSessionName(name, slot) {
|
|
|
30095
31154
|
return `${base}-${slot.replace(/[^a-zA-Z0-9_.-]/g, "-")}`;
|
|
30096
31155
|
}
|
|
30097
31156
|
function tmux(args) {
|
|
30098
|
-
return
|
|
31157
|
+
return execFileSync13("tmux", args, {
|
|
30099
31158
|
encoding: "utf-8",
|
|
30100
31159
|
stdio: ["pipe", "pipe", "pipe"]
|
|
30101
31160
|
}).trim();
|
|
30102
31161
|
}
|
|
30103
31162
|
function tmuxSessionExists(sessionName) {
|
|
30104
31163
|
try {
|
|
30105
|
-
|
|
31164
|
+
execFileSync13("tmux", ["has-session", "-t", sessionName], {
|
|
30106
31165
|
stdio: ["pipe", "pipe", "pipe"]
|
|
30107
31166
|
});
|
|
30108
31167
|
return true;
|
|
@@ -30180,7 +31239,7 @@ function writeAuthSessionMeta(agentDir, meta) {
|
|
|
30180
31239
|
});
|
|
30181
31240
|
}
|
|
30182
31241
|
function clearAuthSessionMeta(agentDir) {
|
|
30183
|
-
|
|
31242
|
+
rmSync6(authSessionMetaPath(agentDir), { force: true });
|
|
30184
31243
|
}
|
|
30185
31244
|
function isSessionStale(agentDir, sessionName) {
|
|
30186
31245
|
const meta = readJsonFile(authSessionMetaPath(agentDir));
|
|
@@ -30206,7 +31265,7 @@ function cleanupAuthTempDirs(agentDir) {
|
|
|
30206
31265
|
try {
|
|
30207
31266
|
for (const entry of readdirSync11(dir)) {
|
|
30208
31267
|
if (entry.startsWith(".setup-token-tmp-")) {
|
|
30209
|
-
|
|
31268
|
+
rmSync6(join18(dir, entry), { recursive: true, force: true });
|
|
30210
31269
|
}
|
|
30211
31270
|
}
|
|
30212
31271
|
} catch {}
|
|
@@ -30364,7 +31423,7 @@ function startAuthSession(name, agentDir, opts = {}) {
|
|
|
30364
31423
|
mkdirSync17(claudeDir2(agentDir), { recursive: true });
|
|
30365
31424
|
cleanupAuthTempDirs(agentDir);
|
|
30366
31425
|
const logPath = authLogPath(agentDir);
|
|
30367
|
-
|
|
31426
|
+
rmSync6(logPath, { force: true });
|
|
30368
31427
|
let configDir;
|
|
30369
31428
|
if (opts.force) {
|
|
30370
31429
|
configDir = mkdtempSync2(join18(claudeDir2(agentDir), ".setup-token-tmp-"));
|
|
@@ -30384,589 +31443,240 @@ function startAuthSession(name, agentDir, opts = {}) {
|
|
|
30384
31443
|
}
|
|
30385
31444
|
commandParts.push(`export CLAUDE_CONFIG_DIR BROWSER`);
|
|
30386
31445
|
commandParts.push(`claude setup-token | tee -- "$LOG_PATH"`);
|
|
30387
|
-
const command = commandParts.join(" && ");
|
|
30388
|
-
tmux(["new-session", "-d", "-s", sessionName, "-c", agentDir, `bash -lc ${shellQuote(command)}`]);
|
|
30389
|
-
sleepMs(8000);
|
|
30390
|
-
const output = captureTmuxPane(sessionName);
|
|
30391
|
-
const loginUrl = parseSetupTokenUrl(output) ?? undefined;
|
|
30392
|
-
const initialCodeChallenge = loginUrl ? extractCodeChallenge(loginUrl) ?? undefined : undefined;
|
|
30393
|
-
writeAuthSessionMeta(agentDir, {
|
|
30394
|
-
sessionName,
|
|
30395
|
-
logPath,
|
|
30396
|
-
startedAt: Date.now(),
|
|
30397
|
-
slot: slotArg,
|
|
30398
|
-
initialCodeChallenge,
|
|
30399
|
-
configDir,
|
|
30400
|
-
credentialsMtimeAtStart
|
|
30401
|
-
});
|
|
30402
|
-
return {
|
|
30403
|
-
sessionName,
|
|
30404
|
-
loginUrl,
|
|
30405
|
-
instructions: [
|
|
30406
|
-
`Started Claude auth for agent "${name}" in tmux session ${sessionName}.`,
|
|
30407
|
-
...loginUrl ? [`Open this URL in your browser:`, loginUrl, ""] : [
|
|
30408
|
-
`Use 'tmux attach -t ${sessionName}' if you need to inspect the auth prompt.`,
|
|
30409
|
-
""
|
|
30410
|
-
],
|
|
30411
|
-
`After Claude shows you a browser code, finish with:`,
|
|
30412
|
-
` switchroom auth code ${name} <browser-code>`,
|
|
30413
|
-
`Cancel with: switchroom auth cancel ${name}`
|
|
30414
|
-
]
|
|
30415
|
-
};
|
|
30416
|
-
}
|
|
30417
|
-
function readTokenFromLogFile(logPath) {
|
|
30418
|
-
if (!existsSync25(logPath))
|
|
30419
|
-
return null;
|
|
30420
|
-
try {
|
|
30421
|
-
const content = readFileSync22(logPath, "utf-8");
|
|
30422
|
-
return parseSetupTokenValue(content);
|
|
30423
|
-
} catch {
|
|
30424
|
-
return null;
|
|
30425
|
-
}
|
|
30426
|
-
}
|
|
30427
|
-
function submitAuthCode(name, agentDir, code, slot, _opts = {}) {
|
|
30428
|
-
const pollIntervalMs = _opts.pollIntervalMs ?? 500;
|
|
30429
|
-
const envTimeout = process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS ? parseInt(process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS, 10) : NaN;
|
|
30430
|
-
const pollTimeoutMs = _opts.pollTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120000);
|
|
30431
|
-
let targetSlot = slot;
|
|
30432
|
-
if (!targetSlot) {
|
|
30433
|
-
const pending = readJsonFile(authSessionMetaPath(agentDir));
|
|
30434
|
-
if (pending?.slot)
|
|
30435
|
-
targetSlot = pending.slot;
|
|
30436
|
-
}
|
|
30437
|
-
const sessionName = authSessionName(name, targetSlot);
|
|
30438
|
-
if (!tmuxSessionExists(sessionName)) {
|
|
30439
|
-
return {
|
|
30440
|
-
completed: false,
|
|
30441
|
-
tokenSaved: false,
|
|
30442
|
-
instructions: [
|
|
30443
|
-
`No pending auth session for agent "${name}".`,
|
|
30444
|
-
`Start one with: switchroom auth add ${name}`
|
|
30445
|
-
]
|
|
30446
|
-
};
|
|
30447
|
-
}
|
|
30448
|
-
const paneReadyResult = probeForCodePrompt(sessionName);
|
|
30449
|
-
if (!paneReadyResult.ready) {
|
|
30450
|
-
clearAuthSessionMeta(agentDir);
|
|
30451
|
-
const outcome2 = { kind: "pane-not-ready" };
|
|
30452
|
-
return {
|
|
30453
|
-
completed: false,
|
|
30454
|
-
tokenSaved: false,
|
|
30455
|
-
outcome: outcome2,
|
|
30456
|
-
instructions: [
|
|
30457
|
-
`Auth pane for agent "${name}" is not ready (prompt not visible yet).`,
|
|
30458
|
-
paneReadyResult.reason === "session-gone" ? `The tmux session disappeared \u2014 start a new auth with: switchroom auth reauth ${name}` : `Retry in a moment with: switchroom auth code ${name} <browser-code>`
|
|
30459
|
-
]
|
|
30460
|
-
};
|
|
30461
|
-
}
|
|
30462
|
-
tmux(["send-keys", "-l", "-t", sessionName, code.trim()]);
|
|
30463
|
-
tmux(["send-keys", "-t", sessionName, "Enter"]);
|
|
30464
|
-
const meta = readJsonFile(authSessionMetaPath(agentDir));
|
|
30465
|
-
const credFileToWatch = meta?.configDir ? join18(meta.configDir, ".credentials.json") : credentialsPath(agentDir);
|
|
30466
|
-
const credsMtimeSnapshot = meta?.credentialsMtimeAtStart ?? 0;
|
|
30467
|
-
const logPath = authLogPath(agentDir);
|
|
30468
|
-
let token = null;
|
|
30469
|
-
const deadline = Date.now() + pollTimeoutMs;
|
|
30470
|
-
while (Date.now() < deadline) {
|
|
30471
|
-
sleepMs(pollIntervalMs);
|
|
30472
|
-
const credsMtime = fileMtimeMs(credFileToWatch);
|
|
30473
|
-
if (credsMtime > credsMtimeSnapshot) {
|
|
30474
|
-
token = readTokenFromCredentialsFile(credFileToWatch);
|
|
30475
|
-
if (token)
|
|
30476
|
-
break;
|
|
30477
|
-
}
|
|
30478
|
-
token = readTokenFromLogFile(logPath);
|
|
30479
|
-
if (token)
|
|
30480
|
-
break;
|
|
30481
|
-
}
|
|
30482
|
-
if (!token) {
|
|
30483
|
-
const paneOutput = captureTmuxPane(sessionName);
|
|
30484
|
-
token = parseSetupTokenValue(paneOutput);
|
|
30485
|
-
}
|
|
30486
|
-
if (!token) {
|
|
30487
|
-
let paneTailText;
|
|
30488
|
-
try {
|
|
30489
|
-
const paneText = captureTmuxPane(sessionName);
|
|
30490
|
-
const lastNonEmpty = paneText.split(`
|
|
30491
|
-
`).map((l) => l.trim()).filter(Boolean).at(-1);
|
|
30492
|
-
if (lastNonEmpty)
|
|
30493
|
-
paneTailText = lastNonEmpty;
|
|
30494
|
-
} catch {}
|
|
30495
|
-
const tailLower = paneTailText?.toLowerCase() ?? "";
|
|
30496
|
-
let outcomeKind = "timeout";
|
|
30497
|
-
if (/invalid.*code|code.*invalid/i.test(tailLower)) {
|
|
30498
|
-
outcomeKind = "invalid-code";
|
|
30499
|
-
} else if (/expired.*code|code.*expired/i.test(tailLower)) {
|
|
30500
|
-
outcomeKind = "expired-code";
|
|
30501
|
-
}
|
|
30502
|
-
clearAuthSessionMeta(agentDir);
|
|
30503
|
-
const outcome2 = { kind: outcomeKind, paneTailText };
|
|
30504
|
-
return {
|
|
30505
|
-
completed: false,
|
|
30506
|
-
tokenSaved: false,
|
|
30507
|
-
outcome: outcome2,
|
|
30508
|
-
instructions: [
|
|
30509
|
-
`Submitted code to Claude for agent "${name}", but no token was found after ${Math.round(pollTimeoutMs / 1000)}s.`,
|
|
30510
|
-
...paneTailText ? [`Pane output: ${paneTailText}`] : [],
|
|
30511
|
-
`If the code was invalid, Claude will say so in the auth session.`,
|
|
30512
|
-
`Inspect with: tmux attach -t ${sessionName}`,
|
|
30513
|
-
`Retry with: switchroom auth code ${name} <browser-code>`
|
|
30514
|
-
]
|
|
30515
|
-
};
|
|
30516
|
-
}
|
|
30517
|
-
const tokenPath = writeOAuthToken(agentDir, token, targetSlot);
|
|
30518
|
-
try {
|
|
30519
|
-
tmux(["kill-session", "-t", sessionName]);
|
|
30520
|
-
} catch {}
|
|
30521
|
-
clearAuthSessionMeta(agentDir);
|
|
30522
|
-
cleanupAuthTempDirs(agentDir);
|
|
30523
|
-
rmSync5(authLogPath(agentDir), { force: true });
|
|
30524
|
-
rmSync5(credentialsPath(agentDir), { force: true });
|
|
30525
|
-
const outcome = { kind: "success" };
|
|
30526
|
-
return {
|
|
30527
|
-
completed: true,
|
|
30528
|
-
tokenSaved: true,
|
|
30529
|
-
tokenPath,
|
|
30530
|
-
outcome,
|
|
30531
|
-
instructions: [
|
|
30532
|
-
`Saved Claude OAuth token for agent "${name}".`,
|
|
30533
|
-
` Token file: ${tokenPath}`,
|
|
30534
|
-
`Restart the agent to pick up the new account, or let switchroom do it now.`
|
|
30535
|
-
]
|
|
30536
|
-
};
|
|
30537
|
-
}
|
|
30538
|
-
function cancelAuthSession(name, agentDir, slot) {
|
|
30539
|
-
let targetSlot = slot;
|
|
30540
|
-
if (!targetSlot) {
|
|
30541
|
-
const pending = readJsonFile(authSessionMetaPath(agentDir));
|
|
30542
|
-
if (pending?.slot)
|
|
30543
|
-
targetSlot = pending.slot;
|
|
30544
|
-
}
|
|
30545
|
-
const sessionName = authSessionName(name, targetSlot);
|
|
30546
|
-
if (tmuxSessionExists(sessionName)) {
|
|
30547
|
-
tmux(["kill-session", "-t", sessionName]);
|
|
30548
|
-
}
|
|
30549
|
-
clearAuthSessionMeta(agentDir);
|
|
30550
|
-
cleanupAuthTempDirs(agentDir);
|
|
30551
|
-
return {
|
|
30552
|
-
instructions: [`Cancelled pending auth session for agent "${name}".`]
|
|
30553
|
-
};
|
|
30554
|
-
}
|
|
30555
|
-
var TOKEN_VALIDITY_MS;
|
|
30556
|
-
var init_manager = __esm(() => {
|
|
30557
|
-
init_loader();
|
|
30558
|
-
init_pane_ready_probe();
|
|
30559
|
-
init_accounts();
|
|
30560
|
-
TOKEN_VALIDITY_MS = 365 * 24 * 60 * 60000;
|
|
30561
|
-
});
|
|
30562
|
-
|
|
30563
|
-
// src/agents/quarantine.ts
|
|
30564
|
-
import {
|
|
30565
|
-
existsSync as existsSync30,
|
|
30566
|
-
mkdirSync as mkdirSync18,
|
|
30567
|
-
readFileSync as readFileSync24,
|
|
30568
|
-
unlinkSync as unlinkSync6,
|
|
30569
|
-
writeFileSync as writeFileSync12
|
|
30570
|
-
} from "node:fs";
|
|
30571
|
-
import { join as join20 } from "node:path";
|
|
30572
|
-
function quarantineMarkerPath(telegramStateDir) {
|
|
30573
|
-
return join20(telegramStateDir, QUARANTINE_FILENAME);
|
|
30574
|
-
}
|
|
30575
|
-
function readQuarantineMarker(telegramStateDir) {
|
|
30576
|
-
const path = quarantineMarkerPath(telegramStateDir);
|
|
30577
|
-
if (!existsSync30(path))
|
|
30578
|
-
return null;
|
|
30579
|
-
try {
|
|
30580
|
-
const raw = readFileSync24(path, "utf-8");
|
|
30581
|
-
const parsed = JSON.parse(raw);
|
|
30582
|
-
if (!parsed || typeof parsed !== "object")
|
|
30583
|
-
return null;
|
|
30584
|
-
const m = parsed;
|
|
30585
|
-
if (m.v !== 1)
|
|
30586
|
-
return null;
|
|
30587
|
-
if (typeof m.reason !== "string")
|
|
30588
|
-
return null;
|
|
30589
|
-
if (typeof m.ts !== "number")
|
|
30590
|
-
return null;
|
|
30591
|
-
return {
|
|
30592
|
-
v: 1,
|
|
30593
|
-
reason: m.reason,
|
|
30594
|
-
ts: m.ts,
|
|
30595
|
-
detail: typeof m.detail === "string" ? m.detail : undefined
|
|
30596
|
-
};
|
|
30597
|
-
} catch {
|
|
30598
|
-
return null;
|
|
30599
|
-
}
|
|
30600
|
-
}
|
|
30601
|
-
function clearQuarantineMarker(telegramStateDir) {
|
|
30602
|
-
const path = quarantineMarkerPath(telegramStateDir);
|
|
30603
|
-
if (!existsSync30(path))
|
|
30604
|
-
return false;
|
|
30605
|
-
try {
|
|
30606
|
-
unlinkSync6(path);
|
|
30607
|
-
return true;
|
|
30608
|
-
} catch {
|
|
30609
|
-
return false;
|
|
30610
|
-
}
|
|
30611
|
-
}
|
|
30612
|
-
function hostTelegramStateDir(agentsDir, name) {
|
|
30613
|
-
return join20(agentsDir, name, "telegram");
|
|
30614
|
-
}
|
|
30615
|
-
function readQuarantineMarkerForAgent(agentsDir, name) {
|
|
30616
|
-
return readQuarantineMarker(hostTelegramStateDir(agentsDir, name));
|
|
30617
|
-
}
|
|
30618
|
-
var QUARANTINE_FILENAME = "quarantine.json";
|
|
30619
|
-
var init_quarantine = () => {};
|
|
30620
|
-
|
|
30621
|
-
// src/agents/pane-lock.ts
|
|
30622
|
-
function withPaneLock(key, fn) {
|
|
30623
|
-
const tail = tails.get(key) ?? Promise.resolve();
|
|
30624
|
-
const run = tail.then(() => fn());
|
|
30625
|
-
const next = run.then(() => {}, () => {});
|
|
30626
|
-
tails.set(key, next);
|
|
30627
|
-
next.then(() => {
|
|
30628
|
-
if (tails.get(key) === next)
|
|
30629
|
-
tails.delete(key);
|
|
30630
|
-
});
|
|
30631
|
-
return run;
|
|
30632
|
-
}
|
|
30633
|
-
var tails;
|
|
30634
|
-
var init_pane_lock = __esm(() => {
|
|
30635
|
-
tails = new Map;
|
|
30636
|
-
});
|
|
30637
|
-
|
|
30638
|
-
// src/agents/inject.ts
|
|
30639
|
-
var exports_inject = {};
|
|
30640
|
-
__export(exports_inject, {
|
|
30641
|
-
validateInjectCommand: () => validateInjectCommand,
|
|
30642
|
-
normalizeInjectCommand: () => normalizeInjectCommand,
|
|
30643
|
-
makeTmuxRunner: () => makeTmuxRunner,
|
|
30644
|
-
isTuiChromeLine: () => isTuiChromeLine,
|
|
30645
|
-
injectSlashCommandWith: () => injectSlashCommandWith,
|
|
30646
|
-
injectSlashCommand: () => injectSlashCommand,
|
|
30647
|
-
diffPane: () => diffPane,
|
|
30648
|
-
dedupeInjectQueue: () => dedupeInjectQueue,
|
|
30649
|
-
InjectError: () => InjectError,
|
|
30650
|
-
INJECT_COMMANDS: () => INJECT_COMMANDS,
|
|
30651
|
-
INJECT_BLOCKLIST: () => INJECT_BLOCKLIST,
|
|
30652
|
-
INJECT_BLOCKED: () => INJECT_BLOCKED,
|
|
30653
|
-
INJECT_ALLOWLIST: () => INJECT_ALLOWLIST
|
|
30654
|
-
});
|
|
30655
|
-
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
30656
|
-
function validateInjectCommand(command) {
|
|
30657
|
-
if (typeof command !== "string" || command.trim().length === 0) {
|
|
30658
|
-
throw new InjectError("invalid", "command is empty");
|
|
30659
|
-
}
|
|
30660
|
-
const trimmed = command.trim();
|
|
30661
|
-
if (!trimmed.startsWith("/")) {
|
|
30662
|
-
throw new InjectError("invalid", `command must start with "/": got ${trimmed}`);
|
|
30663
|
-
}
|
|
30664
|
-
const bare = trimmed.split(/\s+/, 1)[0].toLowerCase();
|
|
30665
|
-
const blockedMeta = INJECT_BLOCKED.get(bare);
|
|
30666
|
-
if (blockedMeta) {
|
|
30667
|
-
throw new InjectError("blocked", `${bare} is explicitly blocked from inject (${blockedMeta.reason}).`);
|
|
30668
|
-
}
|
|
30669
|
-
if (!INJECT_COMMANDS.has(bare)) {
|
|
30670
|
-
const allowed = [...INJECT_COMMANDS.keys()].sort().join(", ");
|
|
30671
|
-
throw new InjectError("not_allowed", `${bare} is not in the inject allowlist. Allowed: ${allowed}`);
|
|
30672
|
-
}
|
|
30673
|
-
return bare;
|
|
30674
|
-
}
|
|
30675
|
-
function normalizeInjectCommand(command) {
|
|
30676
|
-
return command.trim().replace(/\s+/g, " ").toLowerCase();
|
|
30677
|
-
}
|
|
30678
|
-
function dedupeInjectQueue(commands) {
|
|
30679
|
-
const seen = new Set;
|
|
30680
|
-
const out = [];
|
|
30681
|
-
for (const raw of commands) {
|
|
30682
|
-
if (typeof raw !== "string")
|
|
30683
|
-
continue;
|
|
30684
|
-
const key = normalizeInjectCommand(raw);
|
|
30685
|
-
if (key.length === 0)
|
|
30686
|
-
continue;
|
|
30687
|
-
if (seen.has(key))
|
|
30688
|
-
continue;
|
|
30689
|
-
seen.add(key);
|
|
30690
|
-
out.push(raw.trim());
|
|
30691
|
-
}
|
|
30692
|
-
return out;
|
|
30693
|
-
}
|
|
30694
|
-
function defaultSocketName(agentName) {
|
|
30695
|
-
return `switchroom-${agentName}`;
|
|
30696
|
-
}
|
|
30697
|
-
function makeTmuxRunner(tmuxBin) {
|
|
30698
|
-
return {
|
|
30699
|
-
capture(socket, session) {
|
|
30700
|
-
try {
|
|
30701
|
-
return execFileSync13(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
30702
|
-
} catch {
|
|
30703
|
-
return null;
|
|
30704
|
-
}
|
|
30705
|
-
},
|
|
30706
|
-
send(socket, session, args) {
|
|
30707
|
-
const [subcmd, ...rest] = args;
|
|
30708
|
-
const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
|
|
30709
|
-
const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
|
|
30710
|
-
const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
|
|
30711
|
-
execFileSync13(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
|
|
30712
|
-
},
|
|
30713
|
-
hasSession(socket, session) {
|
|
30714
|
-
try {
|
|
30715
|
-
execFileSync13(tmuxBin, ["-L", socket, "has-session", "-t", session], {
|
|
30716
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
30717
|
-
});
|
|
30718
|
-
return true;
|
|
30719
|
-
} catch {
|
|
30720
|
-
return false;
|
|
30721
|
-
}
|
|
30722
|
-
}
|
|
30723
|
-
};
|
|
30724
|
-
}
|
|
30725
|
-
function sleep2(ms) {
|
|
30726
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
30727
|
-
}
|
|
30728
|
-
function isTuiChromeLine(line) {
|
|
30729
|
-
const t = line.trim();
|
|
30730
|
-
if (t.length === 0)
|
|
30731
|
-
return false;
|
|
30732
|
-
if (/^[\s\-_\u2502\u2500\u256d\u256e\u2570\u256f\u250c\u2510\u2514\u2518|]+$/.test(t))
|
|
30733
|
-
return true;
|
|
30734
|
-
if (/^[\u256d\u256e\u2570\u256f\u2502\s]*[>)\u276f][\u256d\u256e\u2570\u256f\u2502\s]*$/.test(t))
|
|
30735
|
-
return true;
|
|
30736
|
-
if (/accept edits on/i.test(t))
|
|
30737
|
-
return true;
|
|
30738
|
-
if (/shift\+tab to cycle/i.test(t))
|
|
30739
|
-
return true;
|
|
30740
|
-
if (/for agents/i.test(t))
|
|
30741
|
-
return true;
|
|
30742
|
-
if (/\? for shortcuts/i.test(t))
|
|
30743
|
-
return true;
|
|
30744
|
-
if (/bypassing permissions/i.test(t))
|
|
30745
|
-
return true;
|
|
30746
|
-
if (t === "copy")
|
|
30747
|
-
return true;
|
|
30748
|
-
return false;
|
|
30749
|
-
}
|
|
30750
|
-
function diffPane(before, after, command) {
|
|
30751
|
-
if (command) {
|
|
30752
|
-
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30753
|
-
const lines = after.split(`
|
|
30754
|
-
`);
|
|
30755
|
-
let anchorIdx = -1;
|
|
30756
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
30757
|
-
if (/[\u276f>]\s+/.test(lines[i]) && lines[i].includes(escaped)) {
|
|
30758
|
-
anchorIdx = i;
|
|
30759
|
-
break;
|
|
30760
|
-
}
|
|
30761
|
-
}
|
|
30762
|
-
if (anchorIdx >= 0) {
|
|
30763
|
-
const tail = lines.slice(anchorIdx + 1);
|
|
30764
|
-
const trimmed = [];
|
|
30765
|
-
for (const raw of tail) {
|
|
30766
|
-
const line = raw.trimEnd();
|
|
30767
|
-
if (line.length === 0 && trimmed.length === 0)
|
|
30768
|
-
continue;
|
|
30769
|
-
if (isTuiChromeLine(line))
|
|
30770
|
-
continue;
|
|
30771
|
-
trimmed.push(line);
|
|
30772
|
-
}
|
|
30773
|
-
while (trimmed.length > 0 && trimmed[trimmed.length - 1].length === 0) {
|
|
30774
|
-
trimmed.pop();
|
|
30775
|
-
}
|
|
30776
|
-
const affordances = /^(esc to cancel|press any key|\u21b5 select)/i;
|
|
30777
|
-
while (trimmed.length > 0 && affordances.test(trimmed[trimmed.length - 1].trim())) {
|
|
30778
|
-
trimmed.pop();
|
|
30779
|
-
}
|
|
30780
|
-
if (trimmed.length > 0) {
|
|
30781
|
-
return { output: trimmed.join(`
|
|
30782
|
-
`), anchored: true };
|
|
30783
|
-
}
|
|
30784
|
-
}
|
|
30785
|
-
}
|
|
30786
|
-
const beforeSet = new Set(before.split(`
|
|
30787
|
-
`).map((l) => l.trimEnd()));
|
|
30788
|
-
const newLines = [];
|
|
30789
|
-
for (const raw of after.split(`
|
|
30790
|
-
`)) {
|
|
30791
|
-
const line = raw.trimEnd();
|
|
30792
|
-
if (line.length === 0)
|
|
30793
|
-
continue;
|
|
30794
|
-
if (beforeSet.has(line))
|
|
30795
|
-
continue;
|
|
30796
|
-
if (isTuiChromeLine(line))
|
|
30797
|
-
continue;
|
|
30798
|
-
newLines.push(line);
|
|
30799
|
-
}
|
|
30800
|
-
return { output: newLines.join(`
|
|
30801
|
-
`), anchored: false };
|
|
30802
|
-
}
|
|
30803
|
-
async function injectSlashCommand(agentName, command, opts = {}) {
|
|
30804
|
-
validateInjectCommand(command);
|
|
30805
|
-
const tmuxBin = opts.tmuxBin ?? "tmux";
|
|
30806
|
-
const socket = opts.socketName ?? defaultSocketName(agentName);
|
|
30807
|
-
const session = opts.sessionName ?? agentName;
|
|
30808
|
-
const settleMs = opts.settleMs ?? 2000;
|
|
30809
|
-
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
30810
|
-
return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
|
|
30811
|
-
socket,
|
|
30812
|
-
session,
|
|
30813
|
-
command: command.trim(),
|
|
30814
|
-
settleMs,
|
|
30815
|
-
timeoutMs
|
|
30816
|
-
}));
|
|
31446
|
+
const command = commandParts.join(" && ");
|
|
31447
|
+
tmux(["new-session", "-d", "-s", sessionName, "-c", agentDir, `bash -lc ${shellQuote(command)}`]);
|
|
31448
|
+
sleepMs(8000);
|
|
31449
|
+
const output = captureTmuxPane(sessionName);
|
|
31450
|
+
const loginUrl = parseSetupTokenUrl(output) ?? undefined;
|
|
31451
|
+
const initialCodeChallenge = loginUrl ? extractCodeChallenge(loginUrl) ?? undefined : undefined;
|
|
31452
|
+
writeAuthSessionMeta(agentDir, {
|
|
31453
|
+
sessionName,
|
|
31454
|
+
logPath,
|
|
31455
|
+
startedAt: Date.now(),
|
|
31456
|
+
slot: slotArg,
|
|
31457
|
+
initialCodeChallenge,
|
|
31458
|
+
configDir,
|
|
31459
|
+
credentialsMtimeAtStart
|
|
31460
|
+
});
|
|
31461
|
+
return {
|
|
31462
|
+
sessionName,
|
|
31463
|
+
loginUrl,
|
|
31464
|
+
instructions: [
|
|
31465
|
+
`Started Claude auth for agent "${name}" in tmux session ${sessionName}.`,
|
|
31466
|
+
...loginUrl ? [`Open this URL in your browser:`, loginUrl, ""] : [
|
|
31467
|
+
`Use 'tmux attach -t ${sessionName}' if you need to inspect the auth prompt.`,
|
|
31468
|
+
""
|
|
31469
|
+
],
|
|
31470
|
+
`After Claude shows you a browser code, finish with:`,
|
|
31471
|
+
` switchroom auth code ${name} <browser-code>`,
|
|
31472
|
+
`Cancel with: switchroom auth cancel ${name}`
|
|
31473
|
+
]
|
|
31474
|
+
};
|
|
30817
31475
|
}
|
|
30818
|
-
|
|
30819
|
-
|
|
30820
|
-
|
|
31476
|
+
function readTokenFromLogFile(logPath) {
|
|
31477
|
+
if (!existsSync25(logPath))
|
|
31478
|
+
return null;
|
|
30821
31479
|
try {
|
|
30822
|
-
|
|
30823
|
-
|
|
30824
|
-
|
|
30825
|
-
|
|
30826
|
-
outcome: "failed",
|
|
30827
|
-
output: "",
|
|
30828
|
-
truncated: false,
|
|
30829
|
-
command: command.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "",
|
|
30830
|
-
meta: null,
|
|
30831
|
-
errorCode: err.code,
|
|
30832
|
-
errorMessage: err.message
|
|
30833
|
-
};
|
|
30834
|
-
}
|
|
30835
|
-
throw err;
|
|
31480
|
+
const content = readFileSync22(logPath, "utf-8");
|
|
31481
|
+
return parseSetupTokenValue(content);
|
|
31482
|
+
} catch {
|
|
31483
|
+
return null;
|
|
30836
31484
|
}
|
|
30837
|
-
|
|
30838
|
-
|
|
31485
|
+
}
|
|
31486
|
+
function submitAuthCode(name, agentDir, code, slot, _opts = {}) {
|
|
31487
|
+
const pollIntervalMs = _opts.pollIntervalMs ?? 500;
|
|
31488
|
+
const envTimeout = process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS ? parseInt(process.env.SWITCHROOM_AUTH_CODE_TIMEOUT_MS, 10) : NaN;
|
|
31489
|
+
const pollTimeoutMs = _opts.pollTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120000);
|
|
31490
|
+
let targetSlot = slot;
|
|
31491
|
+
if (!targetSlot) {
|
|
31492
|
+
const pending = readJsonFile(authSessionMetaPath(agentDir));
|
|
31493
|
+
if (pending?.slot)
|
|
31494
|
+
targetSlot = pending.slot;
|
|
31495
|
+
}
|
|
31496
|
+
const sessionName = authSessionName(name, targetSlot);
|
|
31497
|
+
if (!tmuxSessionExists(sessionName)) {
|
|
30839
31498
|
return {
|
|
30840
|
-
|
|
30841
|
-
|
|
30842
|
-
|
|
30843
|
-
|
|
30844
|
-
|
|
30845
|
-
|
|
30846
|
-
errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
|
|
31499
|
+
completed: false,
|
|
31500
|
+
tokenSaved: false,
|
|
31501
|
+
instructions: [
|
|
31502
|
+
`No pending auth session for agent "${name}".`,
|
|
31503
|
+
`Start one with: switchroom auth add ${name}`
|
|
31504
|
+
]
|
|
30847
31505
|
};
|
|
30848
31506
|
}
|
|
30849
|
-
const
|
|
30850
|
-
|
|
30851
|
-
|
|
30852
|
-
|
|
30853
|
-
} catch (err) {
|
|
31507
|
+
const paneReadyResult = probeForCodePrompt(sessionName);
|
|
31508
|
+
if (!paneReadyResult.ready) {
|
|
31509
|
+
clearAuthSessionMeta(agentDir);
|
|
31510
|
+
const outcome2 = { kind: "pane-not-ready" };
|
|
30854
31511
|
return {
|
|
30855
|
-
|
|
30856
|
-
|
|
30857
|
-
|
|
30858
|
-
|
|
30859
|
-
|
|
30860
|
-
|
|
30861
|
-
|
|
31512
|
+
completed: false,
|
|
31513
|
+
tokenSaved: false,
|
|
31514
|
+
outcome: outcome2,
|
|
31515
|
+
instructions: [
|
|
31516
|
+
`Auth pane for agent "${name}" is not ready (prompt not visible yet).`,
|
|
31517
|
+
paneReadyResult.reason === "session-gone" ? `The tmux session disappeared \u2014 start a new auth with: switchroom auth reauth ${name}` : `Retry in a moment with: switchroom auth code ${name} <browser-code>`
|
|
31518
|
+
]
|
|
30862
31519
|
};
|
|
30863
31520
|
}
|
|
30864
|
-
|
|
30865
|
-
|
|
30866
|
-
|
|
30867
|
-
|
|
30868
|
-
|
|
30869
|
-
|
|
30870
|
-
|
|
30871
|
-
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
31521
|
+
tmux(["send-keys", "-l", "-t", sessionName, code.trim()]);
|
|
31522
|
+
tmux(["send-keys", "-t", sessionName, "Enter"]);
|
|
31523
|
+
const meta = readJsonFile(authSessionMetaPath(agentDir));
|
|
31524
|
+
const credFileToWatch = meta?.configDir ? join18(meta.configDir, ".credentials.json") : credentialsPath(agentDir);
|
|
31525
|
+
const credsMtimeSnapshot = meta?.credentialsMtimeAtStart ?? 0;
|
|
31526
|
+
const logPath = authLogPath(agentDir);
|
|
31527
|
+
let token = null;
|
|
31528
|
+
const deadline = Date.now() + pollTimeoutMs;
|
|
31529
|
+
while (Date.now() < deadline) {
|
|
31530
|
+
sleepMs(pollIntervalMs);
|
|
31531
|
+
const credsMtime = fileMtimeMs(credFileToWatch);
|
|
31532
|
+
if (credsMtime > credsMtimeSnapshot) {
|
|
31533
|
+
token = readTokenFromCredentialsFile(credFileToWatch);
|
|
31534
|
+
if (token)
|
|
30875
31535
|
break;
|
|
30876
|
-
}
|
|
30877
|
-
} else {
|
|
30878
|
-
stableSince = null;
|
|
30879
31536
|
}
|
|
30880
|
-
|
|
30881
|
-
if (
|
|
31537
|
+
token = readTokenFromLogFile(logPath);
|
|
31538
|
+
if (token)
|
|
30882
31539
|
break;
|
|
30883
|
-
}
|
|
30884
31540
|
}
|
|
30885
|
-
|
|
30886
|
-
|
|
30887
|
-
|
|
30888
|
-
const bytes = Buffer.byteLength(output, "utf-8");
|
|
30889
|
-
if (bytes > OUTPUT_BYTE_CAP) {
|
|
30890
|
-
while (Buffer.byteLength(output, "utf-8") > OUTPUT_BYTE_CAP) {
|
|
30891
|
-
output = output.slice(Math.floor(output.length * 0.1) + 1);
|
|
30892
|
-
}
|
|
30893
|
-
truncated = true;
|
|
31541
|
+
if (!token) {
|
|
31542
|
+
const paneOutput = captureTmuxPane(sessionName);
|
|
31543
|
+
token = parseSetupTokenValue(paneOutput);
|
|
30894
31544
|
}
|
|
30895
|
-
if (
|
|
31545
|
+
if (!token) {
|
|
31546
|
+
let paneTailText;
|
|
30896
31547
|
try {
|
|
30897
|
-
|
|
31548
|
+
const paneText = captureTmuxPane(sessionName);
|
|
31549
|
+
const lastNonEmpty = paneText.split(`
|
|
31550
|
+
`).map((l) => l.trim()).filter(Boolean).at(-1);
|
|
31551
|
+
if (lastNonEmpty)
|
|
31552
|
+
paneTailText = lastNonEmpty;
|
|
30898
31553
|
} catch {}
|
|
30899
|
-
|
|
30900
|
-
|
|
31554
|
+
const tailLower = paneTailText?.toLowerCase() ?? "";
|
|
31555
|
+
let outcomeKind = "timeout";
|
|
31556
|
+
if (/invalid.*code|code.*invalid/i.test(tailLower)) {
|
|
31557
|
+
outcomeKind = "invalid-code";
|
|
31558
|
+
} else if (/expired.*code|code.*expired/i.test(tailLower)) {
|
|
31559
|
+
outcomeKind = "expired-code";
|
|
31560
|
+
}
|
|
31561
|
+
clearAuthSessionMeta(agentDir);
|
|
31562
|
+
const outcome2 = { kind: outcomeKind, paneTailText };
|
|
30901
31563
|
return {
|
|
30902
|
-
|
|
30903
|
-
|
|
30904
|
-
|
|
30905
|
-
|
|
30906
|
-
|
|
30907
|
-
|
|
31564
|
+
completed: false,
|
|
31565
|
+
tokenSaved: false,
|
|
31566
|
+
outcome: outcome2,
|
|
31567
|
+
instructions: [
|
|
31568
|
+
`Submitted code to Claude for agent "${name}", but no token was found after ${Math.round(pollTimeoutMs / 1000)}s.`,
|
|
31569
|
+
...paneTailText ? [`Pane output: ${paneTailText}`] : [],
|
|
31570
|
+
`If the code was invalid, Claude will say so in the auth session.`,
|
|
31571
|
+
`Inspect with: tmux attach -t ${sessionName}`,
|
|
31572
|
+
`Retry with: switchroom auth code ${name} <browser-code>`
|
|
31573
|
+
]
|
|
30908
31574
|
};
|
|
30909
31575
|
}
|
|
31576
|
+
const tokenPath = writeOAuthToken(agentDir, token, targetSlot);
|
|
31577
|
+
try {
|
|
31578
|
+
tmux(["kill-session", "-t", sessionName]);
|
|
31579
|
+
} catch {}
|
|
31580
|
+
clearAuthSessionMeta(agentDir);
|
|
31581
|
+
cleanupAuthTempDirs(agentDir);
|
|
31582
|
+
rmSync6(authLogPath(agentDir), { force: true });
|
|
31583
|
+
rmSync6(credentialsPath(agentDir), { force: true });
|
|
31584
|
+
const outcome = { kind: "success" };
|
|
30910
31585
|
return {
|
|
30911
|
-
|
|
30912
|
-
|
|
30913
|
-
|
|
30914
|
-
|
|
30915
|
-
|
|
30916
|
-
|
|
31586
|
+
completed: true,
|
|
31587
|
+
tokenSaved: true,
|
|
31588
|
+
tokenPath,
|
|
31589
|
+
outcome,
|
|
31590
|
+
instructions: [
|
|
31591
|
+
`Saved Claude OAuth token for agent "${name}".`,
|
|
31592
|
+
` Token file: ${tokenPath}`,
|
|
31593
|
+
`Restart the agent to pick up the new account, or let switchroom do it now.`
|
|
31594
|
+
]
|
|
30917
31595
|
};
|
|
30918
31596
|
}
|
|
30919
|
-
|
|
30920
|
-
|
|
30921
|
-
|
|
30922
|
-
|
|
30923
|
-
|
|
30924
|
-
|
|
30925
|
-
|
|
30926
|
-
|
|
30927
|
-
|
|
30928
|
-
["
|
|
30929
|
-
|
|
30930
|
-
|
|
30931
|
-
|
|
30932
|
-
|
|
30933
|
-
|
|
30934
|
-
silentNote: "context cleared \u2014 fresh slate"
|
|
30935
|
-
}
|
|
30936
|
-
],
|
|
30937
|
-
[
|
|
30938
|
-
"/compact",
|
|
30939
|
-
{
|
|
30940
|
-
description: "Compact conversation history",
|
|
30941
|
-
expectsOutput: false,
|
|
30942
|
-
silentNote: "compaction runs silently"
|
|
30943
|
-
}
|
|
30944
|
-
]
|
|
30945
|
-
]);
|
|
30946
|
-
INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
|
|
30947
|
-
INJECT_BLOCKED = new Map([
|
|
30948
|
-
["/login", { reason: "would mutate auth state" }],
|
|
30949
|
-
["/logout", { reason: "would terminate the agent's auth session" }],
|
|
30950
|
-
["/exit", { reason: "would kill the agent process" }],
|
|
30951
|
-
["/quit", { reason: "would kill the agent process" }],
|
|
30952
|
-
[
|
|
30953
|
-
"/effort",
|
|
30954
|
-
{
|
|
30955
|
-
reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
|
|
30956
|
-
}
|
|
30957
|
-
]
|
|
30958
|
-
]);
|
|
30959
|
-
INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
|
|
30960
|
-
InjectError = class InjectError extends Error {
|
|
30961
|
-
code;
|
|
30962
|
-
constructor(code, message) {
|
|
30963
|
-
super(message);
|
|
30964
|
-
this.name = "InjectError";
|
|
30965
|
-
this.code = code;
|
|
30966
|
-
}
|
|
31597
|
+
function cancelAuthSession(name, agentDir, slot) {
|
|
31598
|
+
let targetSlot = slot;
|
|
31599
|
+
if (!targetSlot) {
|
|
31600
|
+
const pending = readJsonFile(authSessionMetaPath(agentDir));
|
|
31601
|
+
if (pending?.slot)
|
|
31602
|
+
targetSlot = pending.slot;
|
|
31603
|
+
}
|
|
31604
|
+
const sessionName = authSessionName(name, targetSlot);
|
|
31605
|
+
if (tmuxSessionExists(sessionName)) {
|
|
31606
|
+
tmux(["kill-session", "-t", sessionName]);
|
|
31607
|
+
}
|
|
31608
|
+
clearAuthSessionMeta(agentDir);
|
|
31609
|
+
cleanupAuthTempDirs(agentDir);
|
|
31610
|
+
return {
|
|
31611
|
+
instructions: [`Cancelled pending auth session for agent "${name}".`]
|
|
30967
31612
|
};
|
|
31613
|
+
}
|
|
31614
|
+
var TOKEN_VALIDITY_MS;
|
|
31615
|
+
var init_manager = __esm(() => {
|
|
31616
|
+
init_loader();
|
|
31617
|
+
init_pane_ready_probe();
|
|
31618
|
+
init_accounts();
|
|
31619
|
+
TOKEN_VALIDITY_MS = 365 * 24 * 60 * 60000;
|
|
30968
31620
|
});
|
|
30969
31621
|
|
|
31622
|
+
// src/agents/quarantine.ts
|
|
31623
|
+
import {
|
|
31624
|
+
existsSync as existsSync30,
|
|
31625
|
+
mkdirSync as mkdirSync18,
|
|
31626
|
+
readFileSync as readFileSync24,
|
|
31627
|
+
unlinkSync as unlinkSync6,
|
|
31628
|
+
writeFileSync as writeFileSync12
|
|
31629
|
+
} from "node:fs";
|
|
31630
|
+
import { join as join20 } from "node:path";
|
|
31631
|
+
function quarantineMarkerPath(telegramStateDir) {
|
|
31632
|
+
return join20(telegramStateDir, QUARANTINE_FILENAME);
|
|
31633
|
+
}
|
|
31634
|
+
function readQuarantineMarker(telegramStateDir) {
|
|
31635
|
+
const path = quarantineMarkerPath(telegramStateDir);
|
|
31636
|
+
if (!existsSync30(path))
|
|
31637
|
+
return null;
|
|
31638
|
+
try {
|
|
31639
|
+
const raw = readFileSync24(path, "utf-8");
|
|
31640
|
+
const parsed = JSON.parse(raw);
|
|
31641
|
+
if (!parsed || typeof parsed !== "object")
|
|
31642
|
+
return null;
|
|
31643
|
+
const m = parsed;
|
|
31644
|
+
if (m.v !== 1)
|
|
31645
|
+
return null;
|
|
31646
|
+
if (typeof m.reason !== "string")
|
|
31647
|
+
return null;
|
|
31648
|
+
if (typeof m.ts !== "number")
|
|
31649
|
+
return null;
|
|
31650
|
+
return {
|
|
31651
|
+
v: 1,
|
|
31652
|
+
reason: m.reason,
|
|
31653
|
+
ts: m.ts,
|
|
31654
|
+
detail: typeof m.detail === "string" ? m.detail : undefined
|
|
31655
|
+
};
|
|
31656
|
+
} catch {
|
|
31657
|
+
return null;
|
|
31658
|
+
}
|
|
31659
|
+
}
|
|
31660
|
+
function clearQuarantineMarker(telegramStateDir) {
|
|
31661
|
+
const path = quarantineMarkerPath(telegramStateDir);
|
|
31662
|
+
if (!existsSync30(path))
|
|
31663
|
+
return false;
|
|
31664
|
+
try {
|
|
31665
|
+
unlinkSync6(path);
|
|
31666
|
+
return true;
|
|
31667
|
+
} catch {
|
|
31668
|
+
return false;
|
|
31669
|
+
}
|
|
31670
|
+
}
|
|
31671
|
+
function hostTelegramStateDir(agentsDir, name) {
|
|
31672
|
+
return join20(agentsDir, name, "telegram");
|
|
31673
|
+
}
|
|
31674
|
+
function readQuarantineMarkerForAgent(agentsDir, name) {
|
|
31675
|
+
return readQuarantineMarker(hostTelegramStateDir(agentsDir, name));
|
|
31676
|
+
}
|
|
31677
|
+
var QUARANTINE_FILENAME = "quarantine.json";
|
|
31678
|
+
var init_quarantine = () => {};
|
|
31679
|
+
|
|
30970
31680
|
// src/auth/broker/protocol.ts
|
|
30971
31681
|
function encodeRequest2(req) {
|
|
30972
31682
|
const line = JSON.stringify(RequestSchema2.parse(req)) + `
|
|
@@ -31607,8 +32317,8 @@ import {
|
|
|
31607
32317
|
mkdirSync as mkdirSync19,
|
|
31608
32318
|
readFileSync as readFileSync26,
|
|
31609
32319
|
readdirSync as readdirSync15,
|
|
31610
|
-
renameSync as
|
|
31611
|
-
rmSync as
|
|
32320
|
+
renameSync as renameSync8,
|
|
32321
|
+
rmSync as rmSync11,
|
|
31612
32322
|
statSync as statSync16,
|
|
31613
32323
|
writeFileSync as writeFileSync14
|
|
31614
32324
|
} from "node:fs";
|
|
@@ -31737,73 +32447,6 @@ var init_google_workspace_yaml = __esm(() => {
|
|
|
31737
32447
|
import_yaml7 = __toESM(require_dist(), 1);
|
|
31738
32448
|
});
|
|
31739
32449
|
|
|
31740
|
-
// src/util/atomic.ts
|
|
31741
|
-
var exports_atomic = {};
|
|
31742
|
-
__export(exports_atomic, {
|
|
31743
|
-
writeConfigFileSync: () => writeConfigFileSync,
|
|
31744
|
-
atomicWriteJsonSync: () => atomicWriteJsonSync,
|
|
31745
|
-
atomicWriteFileSync: () => atomicWriteFileSync2
|
|
31746
|
-
});
|
|
31747
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
31748
|
-
import { closeSync as closeSync5, constants, fsyncSync as fsyncSync3, openSync as openSync5, renameSync as renameSync8, rmSync as rmSync11, writeSync as writeSync4 } from "node:fs";
|
|
31749
|
-
function atomicWriteFileSync2(destPath, contents, mode = 384) {
|
|
31750
|
-
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes3(4).toString("hex")}`;
|
|
31751
|
-
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
31752
|
-
let fd = null;
|
|
31753
|
-
try {
|
|
31754
|
-
fd = openSync5(tmp, TMP_OPEN_FLAGS, mode);
|
|
31755
|
-
writeSync4(fd, buf, 0, buf.length, 0);
|
|
31756
|
-
fsyncSync3(fd);
|
|
31757
|
-
closeSync5(fd);
|
|
31758
|
-
fd = null;
|
|
31759
|
-
renameSync8(tmp, destPath);
|
|
31760
|
-
} catch (err) {
|
|
31761
|
-
if (fd !== null) {
|
|
31762
|
-
try {
|
|
31763
|
-
closeSync5(fd);
|
|
31764
|
-
} catch {}
|
|
31765
|
-
}
|
|
31766
|
-
try {
|
|
31767
|
-
rmSync11(tmp, { force: true });
|
|
31768
|
-
} catch {}
|
|
31769
|
-
throw err;
|
|
31770
|
-
}
|
|
31771
|
-
}
|
|
31772
|
-
function atomicWriteJsonSync(destPath, value, mode = 384) {
|
|
31773
|
-
atomicWriteFileSync2(destPath, JSON.stringify(value, null, 2) + `
|
|
31774
|
-
`, mode);
|
|
31775
|
-
}
|
|
31776
|
-
function writeConfigFileSync(destPath, contents, mode = 420) {
|
|
31777
|
-
try {
|
|
31778
|
-
atomicWriteFileSync2(destPath, contents, mode);
|
|
31779
|
-
} catch (err) {
|
|
31780
|
-
const code = err.code;
|
|
31781
|
-
if (code !== "EBUSY" && code !== "EXDEV" && code !== "EINVAL") {
|
|
31782
|
-
throw err;
|
|
31783
|
-
}
|
|
31784
|
-
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
31785
|
-
let fd = null;
|
|
31786
|
-
try {
|
|
31787
|
-
fd = openSync5(destPath, constants.O_WRONLY | constants.O_TRUNC, mode);
|
|
31788
|
-
writeSync4(fd, buf, 0, buf.length, 0);
|
|
31789
|
-
fsyncSync3(fd);
|
|
31790
|
-
closeSync5(fd);
|
|
31791
|
-
fd = null;
|
|
31792
|
-
} catch (fallbackErr) {
|
|
31793
|
-
if (fd !== null) {
|
|
31794
|
-
try {
|
|
31795
|
-
closeSync5(fd);
|
|
31796
|
-
} catch {}
|
|
31797
|
-
}
|
|
31798
|
-
throw fallbackErr;
|
|
31799
|
-
}
|
|
31800
|
-
}
|
|
31801
|
-
}
|
|
31802
|
-
var TMP_OPEN_FLAGS;
|
|
31803
|
-
var init_atomic = __esm(() => {
|
|
31804
|
-
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
31805
|
-
});
|
|
31806
|
-
|
|
31807
32450
|
// src/drive/oauth.ts
|
|
31808
32451
|
var exports_oauth = {};
|
|
31809
32452
|
__export(exports_oauth, {
|
|
@@ -33536,7 +34179,7 @@ __export(exports_via_claude, {
|
|
|
33536
34179
|
PRE_PASTE_RULES: () => PRE_PASTE_RULES,
|
|
33537
34180
|
POST_PASTE_RULES: () => POST_PASTE_RULES
|
|
33538
34181
|
});
|
|
33539
|
-
import { execFileSync as
|
|
34182
|
+
import { execFileSync as execFileSync15, spawnSync as spawnSync3 } from "node:child_process";
|
|
33540
34183
|
import { existsSync as existsSync34, mkdirSync as mkdirSync20, readFileSync as readFileSync29 } from "node:fs";
|
|
33541
34184
|
import { join as join26, resolve as resolve24 } from "node:path";
|
|
33542
34185
|
function tmuxHasSession(session) {
|
|
@@ -33547,7 +34190,7 @@ function tmuxHasSession(session) {
|
|
|
33547
34190
|
}
|
|
33548
34191
|
function tmuxKillSession(session) {
|
|
33549
34192
|
try {
|
|
33550
|
-
|
|
34193
|
+
execFileSync15("tmux", ["kill-session", "-t", session], {
|
|
33551
34194
|
stdio: "ignore",
|
|
33552
34195
|
timeout: 3000
|
|
33553
34196
|
});
|
|
@@ -33555,7 +34198,7 @@ function tmuxKillSession(session) {
|
|
|
33555
34198
|
}
|
|
33556
34199
|
function tmuxCapturePane(session) {
|
|
33557
34200
|
try {
|
|
33558
|
-
const out =
|
|
34201
|
+
const out = execFileSync15("tmux", ["capture-pane", "-p", "-t", session, "-S", "-200"], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 4 * 1024 * 1024 });
|
|
33559
34202
|
return out.toString("utf8");
|
|
33560
34203
|
} catch {
|
|
33561
34204
|
return "";
|
|
@@ -33563,7 +34206,7 @@ function tmuxCapturePane(session) {
|
|
|
33563
34206
|
}
|
|
33564
34207
|
function tmuxSendKeys(session, keys, literal = false) {
|
|
33565
34208
|
try {
|
|
33566
|
-
|
|
34209
|
+
execFileSync15("tmux", ["send-keys", "-t", session, ...literal ? ["-l"] : [], ...keys], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"] });
|
|
33567
34210
|
} catch {}
|
|
33568
34211
|
}
|
|
33569
34212
|
function extractAuthorizeUrl(pane) {
|
|
@@ -33592,7 +34235,7 @@ async function runViaClaude(opts) {
|
|
|
33592
34235
|
tmuxKillSession(SESSION);
|
|
33593
34236
|
}
|
|
33594
34237
|
const spawn4 = opts.spawnClaude ?? (() => {
|
|
33595
|
-
|
|
34238
|
+
execFileSync15("tmux", [
|
|
33596
34239
|
"new-session",
|
|
33597
34240
|
"-d",
|
|
33598
34241
|
"-s",
|
|
@@ -39970,6 +40613,30 @@ function classifyShmSize(bytes) {
|
|
|
39970
40613
|
const gib = (bytes / 1024 / 1024 / 1024).toFixed(bytes % 1024 ** 3 === 0 ? 0 : 1);
|
|
39971
40614
|
return { name: "hindsight shm-size", status: "ok", detail: `${gib}g` };
|
|
39972
40615
|
}
|
|
40616
|
+
function classifyConsolidationBacklog(queueDepth, oldestAgeSecs = null) {
|
|
40617
|
+
const ageStr = oldestAgeSecs !== null && oldestAgeSecs > 0 ? `, oldest ${Math.round(oldestAgeSecs / 60)}m old` : "";
|
|
40618
|
+
const base = `${queueDepth} pending consolidation op(s)${ageStr}`;
|
|
40619
|
+
if (queueDepth >= CONSOLIDATION_BACKLOG_FAIL) {
|
|
40620
|
+
return {
|
|
40621
|
+
name: "hindsight consolidation backlog",
|
|
40622
|
+
status: "fail",
|
|
40623
|
+
detail: `${base} \u2014 queue is deep enough to be wedged; retains are landing but ` + `not consolidating into recall (drains ~7 ops/hr under the #2894 throttle)`,
|
|
40624
|
+
fix: "Check `docker logs switchroom-hindsight` for the queue-lag WARNING and " + "consolidation errors (quota/429/shm); restart with `switchroom memory " + "--restart` if the worker is stuck."
|
|
40625
|
+
};
|
|
40626
|
+
}
|
|
40627
|
+
if (queueDepth >= CONSOLIDATION_BACKLOG_WARN) {
|
|
40628
|
+
return {
|
|
40629
|
+
name: "hindsight consolidation backlog",
|
|
40630
|
+
status: "warn",
|
|
40631
|
+
detail: `${base} \u2014 building faster than it drains (~7 ops/hr under the #2894 ` + `throttle); recall may lag recent turns until it catches up`
|
|
40632
|
+
};
|
|
40633
|
+
}
|
|
40634
|
+
return {
|
|
40635
|
+
name: "hindsight consolidation backlog",
|
|
40636
|
+
status: "ok",
|
|
40637
|
+
detail: queueDepth > 0 ? base : "queue drained (0 pending)"
|
|
40638
|
+
};
|
|
40639
|
+
}
|
|
39973
40640
|
function classifyExtractionLogs(logs) {
|
|
39974
40641
|
const noSpace = /No space left on device|could not resize shared memory/i.test(logs);
|
|
39975
40642
|
const llmError = /Claude Code returned an error result|claude_code_llm[\s\S]{0,80}error|Fact extraction failed|Content extraction failed/i.test(logs);
|
|
@@ -40026,6 +40693,49 @@ function checkHindsightContainerHealth(opts) {
|
|
|
40026
40693
|
} catch {}
|
|
40027
40694
|
return results;
|
|
40028
40695
|
}
|
|
40696
|
+
function classifyHindsightHealthProbe(status, port) {
|
|
40697
|
+
if (status === 200) {
|
|
40698
|
+
return {
|
|
40699
|
+
name: "hindsight /health",
|
|
40700
|
+
status: "ok",
|
|
40701
|
+
detail: `200 at :${port} \u2014 memory backend live`
|
|
40702
|
+
};
|
|
40703
|
+
}
|
|
40704
|
+
if (status === null) {
|
|
40705
|
+
return {
|
|
40706
|
+
name: "hindsight /health",
|
|
40707
|
+
status: "fail",
|
|
40708
|
+
detail: `no response on :${port} \u2014 the container is down or crash-looping ` + `(fleet memory is silently OFF: auto-recall/retain fail with no user error)`,
|
|
40709
|
+
fix: "Check `docker logs switchroom-hindsight` for `[Errno 98] address " + "already in use` (a foreign process on the port) and run " + "`switchroom memory --start` \u2014 the launcher now preflights the port " + "and reassigns instead of crash-looping."
|
|
40710
|
+
};
|
|
40711
|
+
}
|
|
40712
|
+
return {
|
|
40713
|
+
name: "hindsight /health",
|
|
40714
|
+
status: "fail",
|
|
40715
|
+
detail: `HTTP ${status} at :${port} \u2014 memory backend unhealthy`,
|
|
40716
|
+
fix: "Inspect `docker logs switchroom-hindsight`; restart with `switchroom memory --restart`."
|
|
40717
|
+
};
|
|
40718
|
+
}
|
|
40719
|
+
async function checkHindsightHealthEndpoint(mcpUrl, opts = {}) {
|
|
40720
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
40721
|
+
const timeoutMs = opts.timeoutMs ?? 4000;
|
|
40722
|
+
const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
40723
|
+
const healthUrl = `${origin}/health`;
|
|
40724
|
+
const port = (() => {
|
|
40725
|
+
const m = origin.match(/:(\d+)(?:$|\/)/);
|
|
40726
|
+
return m ? parseInt(m[1], 10) : 80;
|
|
40727
|
+
})();
|
|
40728
|
+
const controller = new AbortController;
|
|
40729
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
40730
|
+
try {
|
|
40731
|
+
const res = await doFetch(healthUrl, { signal: controller.signal });
|
|
40732
|
+
return classifyHindsightHealthProbe(res.status, port);
|
|
40733
|
+
} catch {
|
|
40734
|
+
return classifyHindsightHealthProbe(null, port);
|
|
40735
|
+
} finally {
|
|
40736
|
+
clearTimeout(timer);
|
|
40737
|
+
}
|
|
40738
|
+
}
|
|
40029
40739
|
function classifyToolContract(advertised) {
|
|
40030
40740
|
const byName = new Map(advertised.map((t) => [t.name, t]));
|
|
40031
40741
|
const results = [];
|
|
@@ -40068,7 +40778,7 @@ function classifyToolContract(advertised) {
|
|
|
40068
40778
|
}
|
|
40069
40779
|
return results;
|
|
40070
40780
|
}
|
|
40071
|
-
var MIN_HINDSIGHT_SHM_BYTES;
|
|
40781
|
+
var MIN_HINDSIGHT_SHM_BYTES, CONSOLIDATION_BACKLOG_WARN = 25, CONSOLIDATION_BACKLOG_FAIL = 200;
|
|
40072
40782
|
var init_doctor_memory = __esm(() => {
|
|
40073
40783
|
init_hindsight_tools();
|
|
40074
40784
|
MIN_HINDSIGHT_SHM_BYTES = 1024 * 1024 * 1024;
|
|
@@ -43474,6 +44184,19 @@ async function checkBankIngestHealth(config, url, opts) {
|
|
|
43474
44184
|
banks.set(bank, []);
|
|
43475
44185
|
}
|
|
43476
44186
|
const inspected = await Promise.all([...banks].map(async ([bankId, agents]) => [bankId, agents, await inspectBankHealth(url, bankId, { fetchImpl: opts?.fetchImpl })]));
|
|
44187
|
+
let backlogRow;
|
|
44188
|
+
if (opts?.includeConsolidationBacklog) {
|
|
44189
|
+
let totalPending = 0;
|
|
44190
|
+
let anyBankReachable = false;
|
|
44191
|
+
for (const [, , h] of inspected) {
|
|
44192
|
+
if (h.ok) {
|
|
44193
|
+
anyBankReachable = true;
|
|
44194
|
+
totalPending += h.pendingOperations;
|
|
44195
|
+
}
|
|
44196
|
+
}
|
|
44197
|
+
if (anyBankReachable)
|
|
44198
|
+
backlogRow = classifyConsolidationBacklog(totalPending);
|
|
44199
|
+
}
|
|
43477
44200
|
for (const [bankId, agents, h] of inspected) {
|
|
43478
44201
|
const label = `bank ${bankId}` + (profileBanks.has(bankId) ? " (profile)" : agents[0] !== bankId ? ` (${agents.join(", ")})` : "");
|
|
43479
44202
|
if (!h.ok) {
|
|
@@ -43535,6 +44258,8 @@ async function checkBankIngestHealth(config, url, opts) {
|
|
|
43535
44258
|
}
|
|
43536
44259
|
results.push({ name: label, status: "ok", detail: summary });
|
|
43537
44260
|
}
|
|
44261
|
+
if (backlogRow)
|
|
44262
|
+
results.push(backlogRow);
|
|
43538
44263
|
return results;
|
|
43539
44264
|
}
|
|
43540
44265
|
function hindsightDocReprocessUrl(mcpUrl, bankId, docId) {
|
|
@@ -43545,7 +44270,7 @@ async function checkHindsight(config) {
|
|
|
43545
44270
|
if (!isHindsightEnabled(config)) {
|
|
43546
44271
|
return [];
|
|
43547
44272
|
}
|
|
43548
|
-
const url = config.memory?.config?.url ??
|
|
44273
|
+
const url = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
43549
44274
|
const results = [];
|
|
43550
44275
|
const match = url.match(/^https?:\/\/([^:/]+):?(\d+)?/);
|
|
43551
44276
|
if (!match) {
|
|
@@ -43591,7 +44316,8 @@ async function checkHindsight(config) {
|
|
|
43591
44316
|
}
|
|
43592
44317
|
results.push(checkHindsightConsumer(config));
|
|
43593
44318
|
results.push(...checkHindsightContainerHealth());
|
|
43594
|
-
results.push(
|
|
44319
|
+
results.push(await checkHindsightHealthEndpoint(url));
|
|
44320
|
+
results.push(...await checkBankIngestHealth(config, url, { includeConsolidationBacklog: true }));
|
|
43595
44321
|
for (const [agentName, agentConfig] of Object.entries(config.agents)) {
|
|
43596
44322
|
const bankId = agentConfig.memory?.collection ?? agentName;
|
|
43597
44323
|
const hasBankMission = !!agentConfig.memory?.bank_mission;
|
|
@@ -44745,6 +45471,7 @@ var init_doctor = __esm(() => {
|
|
|
44745
45471
|
init_manager();
|
|
44746
45472
|
init_accounts();
|
|
44747
45473
|
init_manifest();
|
|
45474
|
+
init_hindsight2();
|
|
44748
45475
|
init_hindsight();
|
|
44749
45476
|
init_doctor_memory();
|
|
44750
45477
|
init_doctor_docker();
|
|
@@ -48352,12 +49079,15 @@ var exports_apply = {};
|
|
|
48352
49079
|
__export(exports_apply, {
|
|
48353
49080
|
writeInstallTypeCache: () => writeInstallTypeCache,
|
|
48354
49081
|
runApplyPreflight: () => runApplyPreflight,
|
|
49082
|
+
runApplyDryRun: () => runApplyDryRun,
|
|
48355
49083
|
runApply: () => runApply,
|
|
48356
49084
|
resolveVaultBindMountDir: () => resolveVaultBindMountDir,
|
|
48357
49085
|
resolveOperatorVaultPassphrase: () => resolveOperatorVaultPassphrase,
|
|
48358
49086
|
registerApplyCommand: () => registerApplyCommand,
|
|
48359
49087
|
reexecUnderSudo: () => reexecUnderSudo,
|
|
48360
49088
|
provisionLiteLLMKeys: () => provisionLiteLLMKeys,
|
|
49089
|
+
probeVaultProvisioning: () => probeVaultProvisioning,
|
|
49090
|
+
parseComposeServiceNames: () => parseComposeServiceNames,
|
|
48361
49091
|
isInAgentContainer: () => isInAgentContainer,
|
|
48362
49092
|
inspectVaultBindMountDir: () => inspectVaultBindMountDir,
|
|
48363
49093
|
formatScaffoldFailureResolution: () => formatScaffoldFailureResolution,
|
|
@@ -49040,6 +49770,186 @@ Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
|
|
|
49040
49770
|
failures
|
|
49041
49771
|
};
|
|
49042
49772
|
}
|
|
49773
|
+
async function probeVaultProvisioning(config, agentNames, home2) {
|
|
49774
|
+
const optedIn = [];
|
|
49775
|
+
for (const name of agentNames) {
|
|
49776
|
+
const agentConfig = config.agents[name];
|
|
49777
|
+
if (!agentConfig)
|
|
49778
|
+
continue;
|
|
49779
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
49780
|
+
if (effectiveLiteLLMEnabled(config, resolved.litellm))
|
|
49781
|
+
optedIn.push(name);
|
|
49782
|
+
}
|
|
49783
|
+
const needsHindsight = config.litellm?.enabled === true && config.memory?.backend === "hindsight";
|
|
49784
|
+
const passphraseAvailable = await resolveOperatorVaultPassphrase(home2) !== null;
|
|
49785
|
+
const alreadyProvisioned = [];
|
|
49786
|
+
const unprovisionedNew = [];
|
|
49787
|
+
const brokerUnreachable = [];
|
|
49788
|
+
if (optedIn.length > 0) {
|
|
49789
|
+
const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
49790
|
+
for (const name of optedIn) {
|
|
49791
|
+
const existing = await getViaBrokerStructured2(`litellm/${name}/api-key`);
|
|
49792
|
+
if (existing.kind === "ok")
|
|
49793
|
+
alreadyProvisioned.push(name);
|
|
49794
|
+
else if (existing.kind === "unreachable")
|
|
49795
|
+
brokerUnreachable.push(name);
|
|
49796
|
+
else
|
|
49797
|
+
unprovisionedNew.push(name);
|
|
49798
|
+
}
|
|
49799
|
+
}
|
|
49800
|
+
return {
|
|
49801
|
+
optedInCount: optedIn.length,
|
|
49802
|
+
passphraseAvailable,
|
|
49803
|
+
alreadyProvisioned,
|
|
49804
|
+
unprovisionedNew,
|
|
49805
|
+
brokerUnreachable,
|
|
49806
|
+
needsHindsight
|
|
49807
|
+
};
|
|
49808
|
+
}
|
|
49809
|
+
function parseComposeServiceNames(compose) {
|
|
49810
|
+
const lines = compose.split(`
|
|
49811
|
+
`);
|
|
49812
|
+
const names = [];
|
|
49813
|
+
let inServices = false;
|
|
49814
|
+
for (const line of lines) {
|
|
49815
|
+
if (/^services:\s*$/.test(line)) {
|
|
49816
|
+
inServices = true;
|
|
49817
|
+
continue;
|
|
49818
|
+
}
|
|
49819
|
+
if (inServices && /^\S/.test(line))
|
|
49820
|
+
break;
|
|
49821
|
+
if (!inServices)
|
|
49822
|
+
continue;
|
|
49823
|
+
const m = /^ {2}([A-Za-z0-9_.-]+):\s*$/.exec(line);
|
|
49824
|
+
if (m)
|
|
49825
|
+
names.push(m[1]);
|
|
49826
|
+
}
|
|
49827
|
+
return names;
|
|
49828
|
+
}
|
|
49829
|
+
async function runApplyDryRun(config, options, deps = {}, switchroomConfigPath) {
|
|
49830
|
+
const writeOut = deps.writeOut ?? ((s) => process.stdout.write(s));
|
|
49831
|
+
const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
|
|
49832
|
+
const blocking = [];
|
|
49833
|
+
const warnings = [];
|
|
49834
|
+
const info = [];
|
|
49835
|
+
writeOut(source_default.bold(`
|
|
49836
|
+
Dry-run: validating switchroom config (no changes will be written)...
|
|
49837
|
+
`));
|
|
49838
|
+
try {
|
|
49839
|
+
runApplyPreflight(config, { detectComposeV2: deps.detectComposeV2 });
|
|
49840
|
+
} catch (err) {
|
|
49841
|
+
blocking.push(`preflight: ${err.message}`);
|
|
49842
|
+
}
|
|
49843
|
+
const allAgentNames = Object.keys(config.agents ?? {});
|
|
49844
|
+
const agentNames = options.only !== undefined ? [options.only] : allAgentNames;
|
|
49845
|
+
if (options.only !== undefined && !allAgentNames.includes(options.only)) {
|
|
49846
|
+
blocking.push(`apply --only=${options.only}: no such agent in switchroom.yaml (defined: ${allAgentNames.join(", ") || "none"}).`);
|
|
49847
|
+
}
|
|
49848
|
+
for (const name of agentNames) {
|
|
49849
|
+
const agentConfig = config.agents[name];
|
|
49850
|
+
if (!agentConfig)
|
|
49851
|
+
continue;
|
|
49852
|
+
const profileName = agentConfig.extends ?? "default";
|
|
49853
|
+
try {
|
|
49854
|
+
getProfilePath(profileName);
|
|
49855
|
+
resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
49856
|
+
} catch (err) {
|
|
49857
|
+
blocking.push(`profile '${profileName}' for agent '${name}': ${err.message}`);
|
|
49858
|
+
}
|
|
49859
|
+
}
|
|
49860
|
+
try {
|
|
49861
|
+
const probe2 = await probeVaultProvisioning(config, agentNames, homedir47());
|
|
49862
|
+
if (probe2.alreadyProvisioned.length > 0) {
|
|
49863
|
+
info.push(`litellm: ${probe2.alreadyProvisioned.length} agent(s) already provisioned (${probe2.alreadyProvisioned.join(", ")}).`);
|
|
49864
|
+
}
|
|
49865
|
+
if (probe2.brokerUnreachable.length > 0) {
|
|
49866
|
+
warnings.push(`litellm: vault-broker unreachable \u2014 could not verify/provision ` + `${probe2.brokerUnreachable.length} agent(s) (${probe2.brokerUnreachable.join(", ")}); they keep their previous routing env.`);
|
|
49867
|
+
}
|
|
49868
|
+
const pending = [
|
|
49869
|
+
...probe2.unprovisionedNew,
|
|
49870
|
+
...probe2.needsHindsight ? ["hindsight (service)"] : []
|
|
49871
|
+
];
|
|
49872
|
+
if (pending.length > 0) {
|
|
49873
|
+
if (probe2.passphraseAvailable) {
|
|
49874
|
+
info.push(`litellm: would provision ${pending.length} new virtual key(s) (${pending.join(", ")}) \u2014 vault passphrase is available.`);
|
|
49875
|
+
} else {
|
|
49876
|
+
blocking.push(`litellm: ${pending.length} agent(s)/service(s) need a NEW vault key (${pending.join(", ")}) but the vault passphrase is unavailable (no SWITCHROOM_VAULT_PASSPHRASE env and machine-id auto-unlock blob ` + `missing/undecryptable) \u2014 a real apply could not provision them.`);
|
|
49877
|
+
}
|
|
49878
|
+
}
|
|
49879
|
+
} catch (err) {
|
|
49880
|
+
warnings.push(`litellm: provisioning probe failed \u2014 ${err.message}`);
|
|
49881
|
+
}
|
|
49882
|
+
let imageTag = null;
|
|
49883
|
+
let previousImageTag = null;
|
|
49884
|
+
let composeChanged = false;
|
|
49885
|
+
let addedServices = [];
|
|
49886
|
+
let removedServices = [];
|
|
49887
|
+
try {
|
|
49888
|
+
const computed = await computeComposeContent({
|
|
49889
|
+
config,
|
|
49890
|
+
composePath: options.outPath ?? DEFAULT_COMPOSE_PATH2,
|
|
49891
|
+
switchroomConfigPath,
|
|
49892
|
+
releaseOverride: options.releaseOverride,
|
|
49893
|
+
buildMode: options.buildLocal ? "local" : "pull",
|
|
49894
|
+
buildContext: options.buildContext
|
|
49895
|
+
});
|
|
49896
|
+
imageTag = computed.imageTag;
|
|
49897
|
+
previousImageTag = computed.previousImageTag;
|
|
49898
|
+
composeChanged = computed.previous !== computed.content;
|
|
49899
|
+
const newServices = parseComposeServiceNames(computed.content);
|
|
49900
|
+
const oldServices = computed.previous ? parseComposeServiceNames(computed.previous) : [];
|
|
49901
|
+
addedServices = newServices.filter((s) => !oldServices.includes(s));
|
|
49902
|
+
removedServices = oldServices.filter((s) => !newServices.includes(s));
|
|
49903
|
+
if (previousImageTag === null) {
|
|
49904
|
+
info.push(`compose: no existing compose on disk \u2014 a real apply would create it.`);
|
|
49905
|
+
} else if (previousImageTag !== imageTag) {
|
|
49906
|
+
info.push(`compose: agent image would change ${previousImageTag} \u2192 ${imageTag}.`);
|
|
49907
|
+
} else {
|
|
49908
|
+
info.push(`compose: agent image unchanged (${imageTag}).`);
|
|
49909
|
+
}
|
|
49910
|
+
if (addedServices.length > 0) {
|
|
49911
|
+
info.push(`compose: services added: ${addedServices.join(", ")}.`);
|
|
49912
|
+
}
|
|
49913
|
+
if (removedServices.length > 0) {
|
|
49914
|
+
info.push(`compose: services removed: ${removedServices.join(", ")}.`);
|
|
49915
|
+
}
|
|
49916
|
+
if (previousImageTag !== null && !composeChanged) {
|
|
49917
|
+
info.push(`compose: no change (would be byte-identical to the current file).`);
|
|
49918
|
+
}
|
|
49919
|
+
} catch (err) {
|
|
49920
|
+
blocking.push(`compose generation: ${err.message}`);
|
|
49921
|
+
}
|
|
49922
|
+
for (const line of info)
|
|
49923
|
+
writeOut(source_default.gray(` ~ ${line}
|
|
49924
|
+
`));
|
|
49925
|
+
for (const line of warnings)
|
|
49926
|
+
writeErr(source_default.yellow(` ! ${line}
|
|
49927
|
+
`));
|
|
49928
|
+
for (const line of blocking)
|
|
49929
|
+
writeErr(source_default.red(` x ${line}
|
|
49930
|
+
`));
|
|
49931
|
+
const wouldSucceed = blocking.length === 0;
|
|
49932
|
+
writeOut(`
|
|
49933
|
+
`);
|
|
49934
|
+
if (wouldSucceed) {
|
|
49935
|
+
writeOut(source_default.green.bold(`Dry-run: a real apply would SUCCEED. ${allAgentNames.length} agent(s) validated; nothing was written.
|
|
49936
|
+
`));
|
|
49937
|
+
} else {
|
|
49938
|
+
writeErr(source_default.red.bold(`Dry-run: a real apply would FAIL \u2014 ${blocking.length} blocking condition(s) above. Nothing was written.
|
|
49939
|
+
`));
|
|
49940
|
+
}
|
|
49941
|
+
return {
|
|
49942
|
+
blocking,
|
|
49943
|
+
warnings,
|
|
49944
|
+
info,
|
|
49945
|
+
imageTag,
|
|
49946
|
+
previousImageTag,
|
|
49947
|
+
composeChanged,
|
|
49948
|
+
addedServices,
|
|
49949
|
+
removedServices,
|
|
49950
|
+
wouldSucceed
|
|
49951
|
+
};
|
|
49952
|
+
}
|
|
49043
49953
|
function formatScaffoldFailureResolution(failures, scaffolded, agentsTotal) {
|
|
49044
49954
|
const fail3 = failures.length;
|
|
49045
49955
|
const lines = [];
|
|
@@ -49157,7 +50067,7 @@ ERROR: failed to spawn sudo: ${result.error.message}
|
|
|
49157
50067
|
process.exit(result.status ?? 1);
|
|
49158
50068
|
}
|
|
49159
50069
|
function registerApplyCommand(program3) {
|
|
49160
|
-
program3.command("apply").description("Apply switchroom.yaml: scaffold every agent and (re)generate the compose file. Run `docker compose -f <path> up -d` afterwards to bring the fleet up.").option("--build-local [context]", "Dev-only: emit `build:` blocks instead of GHCR `image:` refs so `docker compose up --build` rebuilds from in-tree Dockerfiles. Optional context path (defaults to cwd).").option("-o, --out <path>", `Override compose output path (default: ${DEFAULT_COMPOSE_PATH2}).`).option("--example <name>", "Copy an example config into cwd before applying (e.g., 'switchroom' or 'minimal').").option("--non-interactive", "Skip prompts (e.g. sudo-chown explainer for UID alignment). Use in CI / scripts.").option("--allow-unaligned", "Treat UID-alignment chown failures as warnings instead of hard errors. Unsafe: an unaligned state dir will break the agent on first write. Use only if you know you'll fix ownership out-of-band.").option("--only <agent>", "Restrict scaffold + UID-alignment to a single agent (compose still covers the full fleet). Use during a v0.6 \u2192 v0.7 cutover to migrate agents one at a time without breaking the systemd-managed siblings.").option("--compose-only", "Skip the per-agent scaffold loop entirely; only (re)generate the compose file. Use in CI / scripts that can't chown into per-agent state dirs (mode 0700, owned by per-agent UIDs in v0.7+ docker mode). The full apply still runs preflight + emits compose; only the start.sh / .mcp.json / settings.json refresh is skipped.").option("--no-doctor", "Skip the post-apply doctor sweep that surfaces stale start.sh / unhealthy agents (#929). Default: doctor runs after a successful scaffold so the operator sees whether the v0.7+ post-Phase-4 supervisor block is now in place. `switchroom update` passes this internally to avoid running doctor twice (it has its own doctor step).").addOption(new Option("--channel <c>", "Override the resolved `release` block for this apply run: follow the named channel pointer (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved `release` block for this apply run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--print-sudo-cmd", "Print the sudo invocation that `apply` would re-exec itself with when escalation is needed, then exit. Operators who want to script the escalation themselves (CI, custom orchestration) can capture this. Note: tokens are space-separated and not shell-quoted; re-quote arguments if pasting into a shell.").addOption(new Option("--skip-self-elevate").default(false).hideHelp()).action(async (opts) => {
|
|
50070
|
+
program3.command("apply").description("Apply switchroom.yaml: scaffold every agent and (re)generate the compose file. Run `docker compose -f <path> up -d` afterwards to bring the fleet up.").option("--build-local [context]", "Dev-only: emit `build:` blocks instead of GHCR `image:` refs so `docker compose up --build` rebuilds from in-tree Dockerfiles. Optional context path (defaults to cwd).").option("-o, --out <path>", `Override compose output path (default: ${DEFAULT_COMPOSE_PATH2}).`).option("--example <name>", "Copy an example config into cwd before applying (e.g., 'switchroom' or 'minimal').").option("--non-interactive", "Skip prompts (e.g. sudo-chown explainer for UID alignment). Use in CI / scripts.").option("--allow-unaligned", "Treat UID-alignment chown failures as warnings instead of hard errors. Unsafe: an unaligned state dir will break the agent on first write. Use only if you know you'll fix ownership out-of-band.").option("--only <agent>", "Restrict scaffold + UID-alignment to a single agent (compose still covers the full fleet). Use during a v0.6 \u2192 v0.7 cutover to migrate agents one at a time without breaking the systemd-managed siblings.").option("--compose-only", "Skip the per-agent scaffold loop entirely; only (re)generate the compose file. Use in CI / scripts that can't chown into per-agent state dirs (mode 0700, owned by per-agent UIDs in v0.7+ docker mode). The full apply still runs preflight + emits compose; only the start.sh / .mcp.json / settings.json refresh is skipped.").option("--dry-run", "Validate only: run the full compose-regeneration + validation pipeline IN MEMORY (resolve every agent's profile, probe the vault-broker for key-provisioning gaps, compute the would-be compose) and print a report \u2014 without writing the compose file, persisting anything, or touching a container. Exits non-zero if a real apply would fail (e.g. a profile can't resolve). Use as a pre-flight before a `rollout`/`update` (which only print the step plan, not the config-regeneration that actually fails).").option("--no-doctor", "Skip the post-apply doctor sweep that surfaces stale start.sh / unhealthy agents (#929). Default: doctor runs after a successful scaffold so the operator sees whether the v0.7+ post-Phase-4 supervisor block is now in place. `switchroom update` passes this internally to avoid running doctor twice (it has its own doctor step).").addOption(new Option("--channel <c>", "Override the resolved `release` block for this apply run: follow the named channel pointer (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved `release` block for this apply run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--print-sudo-cmd", "Print the sudo invocation that `apply` would re-exec itself with when escalation is needed, then exit. Operators who want to script the escalation themselves (CI, custom orchestration) can capture this. Note: tokens are space-separated and not shell-quoted; re-quote arguments if pasting into a shell.").addOption(new Option("--skip-self-elevate").default(false).hideHelp()).action(async (opts) => {
|
|
49161
50071
|
try {
|
|
49162
50072
|
if (opts.example) {
|
|
49163
50073
|
copyExampleConfig2(opts.example);
|
|
@@ -49171,6 +50081,23 @@ function registerApplyCommand(program3) {
|
|
|
49171
50081
|
`);
|
|
49172
50082
|
process.exit(0);
|
|
49173
50083
|
}
|
|
50084
|
+
if (opts.dryRun) {
|
|
50085
|
+
const dry = await runApplyDryRun(config, {
|
|
50086
|
+
buildLocal: !!opts.buildLocal,
|
|
50087
|
+
buildContext: typeof opts.buildLocal === "string" ? opts.buildLocal : process.cwd(),
|
|
50088
|
+
outPath: opts.out,
|
|
50089
|
+
only: opts.only,
|
|
50090
|
+
releaseOverride: opts.channel ? { channel: opts.channel } : opts.pin ? { pin: opts.pin } : undefined,
|
|
50091
|
+
dryRun: true
|
|
50092
|
+
}, {}, switchroomConfigPath);
|
|
50093
|
+
await captureEvent("apply_dry_run", {
|
|
50094
|
+
agents_total: Object.keys(config.agents ?? {}).length,
|
|
50095
|
+
would_succeed: dry.wouldSucceed,
|
|
50096
|
+
blocking: dry.blocking.length,
|
|
50097
|
+
warnings: dry.warnings.length
|
|
50098
|
+
});
|
|
50099
|
+
process.exit(dry.wouldSucceed ? 0 : 1);
|
|
50100
|
+
}
|
|
49174
50101
|
if (!opts.skipSelfElevate && !opts.composeOnly && process.geteuid?.() !== 0) {
|
|
49175
50102
|
const unwritable = findUnwritableAgentDirs(config, {
|
|
49176
50103
|
only: opts.only
|
|
@@ -49278,6 +50205,7 @@ var init_apply = __esm(() => {
|
|
|
49278
50205
|
init_update_prompt_hook();
|
|
49279
50206
|
init_compose();
|
|
49280
50207
|
init_write_compose();
|
|
50208
|
+
init_profiles();
|
|
49281
50209
|
init_install_detect();
|
|
49282
50210
|
init_operator_uid();
|
|
49283
50211
|
init_posthog();
|
|
@@ -63737,8 +64665,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
63737
64665
|
import { dirname, join } from "node:path";
|
|
63738
64666
|
|
|
63739
64667
|
// src/build-info.ts
|
|
63740
|
-
var VERSION = "0.
|
|
63741
|
-
var COMMIT_SHA = "
|
|
64668
|
+
var VERSION = "0.18.3";
|
|
64669
|
+
var COMMIT_SHA = "dbd8b112";
|
|
63742
64670
|
|
|
63743
64671
|
// src/cli/resolve-version.ts
|
|
63744
64672
|
function readPackageVersion() {
|
|
@@ -63769,7 +64697,7 @@ var SWITCHROOM_VERSION = (() => {
|
|
|
63769
64697
|
init_esm();
|
|
63770
64698
|
init_source();
|
|
63771
64699
|
import { join as join21, resolve as resolve22 } from "node:path";
|
|
63772
|
-
import { rmSync as
|
|
64700
|
+
import { rmSync as rmSync10, existsSync as existsSync31, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
|
|
63773
64701
|
import { homedir as homedir8 } from "node:os";
|
|
63774
64702
|
|
|
63775
64703
|
// src/agents/scheduler-state.ts
|
|
@@ -63833,6 +64761,7 @@ function formatSchedulerState(state) {
|
|
|
63833
64761
|
|
|
63834
64762
|
// src/cli/agent.ts
|
|
63835
64763
|
init_loader();
|
|
64764
|
+
init_hindsight2();
|
|
63836
64765
|
init_hindsight();
|
|
63837
64766
|
init_helpers();
|
|
63838
64767
|
init_scaffold();
|
|
@@ -63951,11 +64880,11 @@ function detectInFlight(opts) {
|
|
|
63951
64880
|
async function waitUntilIdle(opts) {
|
|
63952
64881
|
const pollMs = opts.pollMs ?? 2000;
|
|
63953
64882
|
const clock = opts.now ?? Date.now;
|
|
63954
|
-
const
|
|
64883
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
63955
64884
|
const deadline = clock() + opts.timeoutMs;
|
|
63956
64885
|
let last = detectInFlight(opts);
|
|
63957
64886
|
while (last.busy && clock() < deadline) {
|
|
63958
|
-
await
|
|
64887
|
+
await sleep2(pollMs);
|
|
63959
64888
|
last = detectInFlight(opts);
|
|
63960
64889
|
}
|
|
63961
64890
|
return last;
|
|
@@ -64067,10 +64996,10 @@ function spinner(message) {
|
|
|
64067
64996
|
// src/agents/status.ts
|
|
64068
64997
|
import { existsSync as existsSync23, readFileSync as readFileSync20, statSync as statSync11 } from "node:fs";
|
|
64069
64998
|
import { join as join16 } from "node:path";
|
|
64070
|
-
import { execFileSync as
|
|
64999
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
64071
65000
|
|
|
64072
65001
|
// src/agents/handoff-summarizer.ts
|
|
64073
|
-
import { readFileSync as readFileSync18, writeFileSync as writeFileSync8, renameSync as
|
|
65002
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync8, renameSync as renameSync6, mkdirSync as mkdirSync15, existsSync as existsSync21, statSync as statSync10, readdirSync as readdirSync9 } from "node:fs";
|
|
64074
65003
|
import { join as join15 } from "node:path";
|
|
64075
65004
|
var DEFAULT_MAX_TURNS = 50;
|
|
64076
65005
|
var TOPIC_MAX_CHARS = 117;
|
|
@@ -64212,8 +65141,8 @@ function writeSidecarsAtomic(agentDir, briefing, topic) {
|
|
|
64212
65141
|
const topicTmp = topicPath + ".tmp";
|
|
64213
65142
|
writeFileSync8(handoffTmp, briefing, "utf-8");
|
|
64214
65143
|
writeFileSync8(topicTmp, topic, "utf-8");
|
|
64215
|
-
|
|
64216
|
-
|
|
65144
|
+
renameSync6(handoffTmp, handoffPath);
|
|
65145
|
+
renameSync6(topicTmp, topicPath);
|
|
64217
65146
|
}
|
|
64218
65147
|
async function buildHandoff(opts) {
|
|
64219
65148
|
const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
@@ -64652,7 +65581,7 @@ async function buildAgentStatusReport(inputs) {
|
|
|
64652
65581
|
async function waitForAgentReady(inputs, options = {}) {
|
|
64653
65582
|
const timeoutMs = options.timeoutMs ?? 30000;
|
|
64654
65583
|
const pollIntervalMs = options.pollIntervalMs ?? 750;
|
|
64655
|
-
const
|
|
65584
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
64656
65585
|
const now = options.now ?? Date.now;
|
|
64657
65586
|
const startedAt = now();
|
|
64658
65587
|
const deadline = startedAt + timeoutMs;
|
|
@@ -64667,7 +65596,7 @@ async function waitForAgentReady(inputs, options = {}) {
|
|
|
64667
65596
|
notReady
|
|
64668
65597
|
};
|
|
64669
65598
|
}
|
|
64670
|
-
await
|
|
65599
|
+
await sleep2(pollIntervalMs);
|
|
64671
65600
|
report = await buildAgentStatusReport(inputs);
|
|
64672
65601
|
notReady = readinessGaps(report);
|
|
64673
65602
|
}
|
|
@@ -64744,14 +65673,14 @@ function readLastMessages(historyDbPath) {
|
|
|
64744
65673
|
};
|
|
64745
65674
|
}
|
|
64746
65675
|
try {
|
|
64747
|
-
const out =
|
|
65676
|
+
const out = execFileSync11("bun", [
|
|
64748
65677
|
"-e",
|
|
64749
65678
|
`const { Database } = require("bun:sqlite"); ` + `const db = new Database(${JSON.stringify(historyDbPath)}, { readonly: true }); ` + `const rows = db.prepare("SELECT role, MAX(ts) as ts FROM messages GROUP BY role").all(); ` + `for (const r of rows) console.log(r.role + "|" + r.ts); ` + `db.close();`
|
|
64750
65679
|
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
64751
65680
|
return parseSqliteRoleTsOutput(out);
|
|
64752
65681
|
} catch {}
|
|
64753
65682
|
try {
|
|
64754
|
-
const out =
|
|
65683
|
+
const out = execFileSync11("sqlite3", [
|
|
64755
65684
|
historyDbPath,
|
|
64756
65685
|
"SELECT role, MAX(ts) FROM messages GROUP BY role;"
|
|
64757
65686
|
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -64863,7 +65792,7 @@ function escapeRegex(s) {
|
|
|
64863
65792
|
}
|
|
64864
65793
|
function readDockerContainer(containerName2) {
|
|
64865
65794
|
try {
|
|
64866
|
-
const out =
|
|
65795
|
+
const out = execFileSync11("docker", ["inspect", "--format", "{{json .State}}", containerName2], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
64867
65796
|
const state = JSON.parse(out.trim());
|
|
64868
65797
|
const active = state.Status === "running" ? "active" : state.Status === "restarting" ? "restarting" : "inactive";
|
|
64869
65798
|
const pid = typeof state.Pid === "number" && state.Pid > 0 ? state.Pid : null;
|
|
@@ -64925,7 +65854,7 @@ init_profiles();
|
|
|
64925
65854
|
init_lifecycle();
|
|
64926
65855
|
init_manager();
|
|
64927
65856
|
import { resolve as resolve17 } from "node:path";
|
|
64928
|
-
import { existsSync as existsSync26, rmSync as
|
|
65857
|
+
import { existsSync as existsSync26, rmSync as rmSync7 } from "node:fs";
|
|
64929
65858
|
|
|
64930
65859
|
// src/telegram/redact-token.ts
|
|
64931
65860
|
var REDACTED_PLACEHOLDER = "<redacted-bot-token>";
|
|
@@ -64978,7 +65907,7 @@ async function pollForDmStart(token, timeoutMs = 120000) {
|
|
|
64978
65907
|
const response = await fetch(url);
|
|
64979
65908
|
data = await response.json();
|
|
64980
65909
|
} catch {
|
|
64981
|
-
await
|
|
65910
|
+
await sleep2(2000);
|
|
64982
65911
|
continue;
|
|
64983
65912
|
}
|
|
64984
65913
|
if (data.ok && Array.isArray(data.result)) {
|
|
@@ -64994,11 +65923,11 @@ async function pollForDmStart(token, timeoutMs = 120000) {
|
|
|
64994
65923
|
}
|
|
64995
65924
|
}
|
|
64996
65925
|
}
|
|
64997
|
-
await
|
|
65926
|
+
await sleep2(2000);
|
|
64998
65927
|
}
|
|
64999
65928
|
throw new Error("Timed out waiting for /start DM");
|
|
65000
65929
|
}
|
|
65001
|
-
function
|
|
65930
|
+
function sleep2(ms) {
|
|
65002
65931
|
return new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
65003
65932
|
}
|
|
65004
65933
|
|
|
@@ -65074,7 +66003,7 @@ async function createAgent(opts) {
|
|
|
65074
66003
|
const agentDir = resolve17(agentsDir, name);
|
|
65075
66004
|
await withRollback(() => {
|
|
65076
66005
|
scaffoldAgent(name, agentConfig, agentsDir, config.telegram, config, undefined, configPath);
|
|
65077
|
-
rollbackStack.push(() =>
|
|
66006
|
+
rollbackStack.push(() => rmSync7(agentDir, { recursive: true, force: true }));
|
|
65078
66007
|
});
|
|
65079
66008
|
await withRollback(() => {
|
|
65080
66009
|
writeAgentEnv(agentDir, telegramBotToken);
|
|
@@ -65141,8 +66070,8 @@ async function completeCreation(name, code, opts = {}) {
|
|
|
65141
66070
|
|
|
65142
66071
|
// src/agents/add-orchestrator.ts
|
|
65143
66072
|
import { resolve as resolve19, join as join19 } from "node:path";
|
|
65144
|
-
import { existsSync as existsSync28, rmSync as
|
|
65145
|
-
import { execFileSync as
|
|
66073
|
+
import { existsSync as existsSync28, rmSync as rmSync8, statSync as statSync15 } from "node:fs";
|
|
66074
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
65146
66075
|
init_onboarding();
|
|
65147
66076
|
|
|
65148
66077
|
// src/setup/botfather-walkthrough.ts
|
|
@@ -65708,7 +66637,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
|
|
|
65708
66637
|
if (!isDir)
|
|
65709
66638
|
continue;
|
|
65710
66639
|
try {
|
|
65711
|
-
|
|
66640
|
+
rmSync8(entryPath, { recursive: true, force: true });
|
|
65712
66641
|
removed.push(entry);
|
|
65713
66642
|
} catch {}
|
|
65714
66643
|
}
|
|
@@ -65716,7 +66645,7 @@ function pruneBundledSkills(agentDir, keep, scope) {
|
|
|
65716
66645
|
}
|
|
65717
66646
|
function defaultIsUnitActive(unitName) {
|
|
65718
66647
|
try {
|
|
65719
|
-
const out =
|
|
66648
|
+
const out = execFileSync14("systemctl", ["--user", "is-active", unitName], {
|
|
65720
66649
|
encoding: "utf-8",
|
|
65721
66650
|
stdio: ["ignore", "pipe", "pipe"]
|
|
65722
66651
|
}).trim();
|
|
@@ -65743,7 +66672,7 @@ import { resolve as resolve20 } from "node:path";
|
|
|
65743
66672
|
import {
|
|
65744
66673
|
existsSync as existsSync29,
|
|
65745
66674
|
mkdtempSync as mkdtempSync3,
|
|
65746
|
-
rmSync as
|
|
66675
|
+
rmSync as rmSync9,
|
|
65747
66676
|
cpSync
|
|
65748
66677
|
} from "node:fs";
|
|
65749
66678
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
@@ -65768,7 +66697,7 @@ function defaultDeps() {
|
|
|
65768
66697
|
return tmp;
|
|
65769
66698
|
},
|
|
65770
66699
|
copyDir: (src, dst) => cpSync(src, dst, { recursive: true }),
|
|
65771
|
-
removeDir: (p) =>
|
|
66700
|
+
removeDir: (p) => rmSync9(p, { recursive: true, force: true }),
|
|
65772
66701
|
existsSync: existsSync29,
|
|
65773
66702
|
readFileSync: (p, enc) => readFileSync23(p, enc)
|
|
65774
66703
|
};
|
|
@@ -66137,16 +67066,16 @@ function parsePorcelainDirty(porcelain) {
|
|
|
66137
67066
|
}
|
|
66138
67067
|
function checkSwitchroomBranch() {
|
|
66139
67068
|
try {
|
|
66140
|
-
const { execFileSync:
|
|
67069
|
+
const { execFileSync: execFileSync15 } = __require("node:child_process");
|
|
66141
67070
|
const cwd = process.cwd();
|
|
66142
|
-
const branch =
|
|
67071
|
+
const branch = execFileSync15("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
66143
67072
|
encoding: "utf-8",
|
|
66144
67073
|
stdio: ["ignore", "pipe", "ignore"]
|
|
66145
67074
|
}).trim();
|
|
66146
67075
|
if (branch === "" || branch === "HEAD")
|
|
66147
67076
|
return null;
|
|
66148
67077
|
if (branch === "main" || branch === "master") {
|
|
66149
|
-
const porcelain =
|
|
67078
|
+
const porcelain = execFileSync15("git", ["-C", cwd, "status", "--porcelain"], {
|
|
66150
67079
|
encoding: "utf-8",
|
|
66151
67080
|
stdio: ["ignore", "pipe", "ignore"]
|
|
66152
67081
|
});
|
|
@@ -66209,7 +67138,7 @@ function buildStatusInputs(name, config, agentsDir) {
|
|
|
66209
67138
|
let hindsightApiUrl = null;
|
|
66210
67139
|
let hindsightBankId = name;
|
|
66211
67140
|
if (isHindsightEnabled(config)) {
|
|
66212
|
-
const baseUrl = config.memory?.config?.url ??
|
|
67141
|
+
const baseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
66213
67142
|
hindsightApiUrl = baseUrl.endsWith("/mcp/") ? baseUrl : baseUrl.replace(/\/$/, "") + "/mcp/";
|
|
66214
67143
|
hindsightBankId = agentConfig?.memory?.collection ?? name;
|
|
66215
67144
|
}
|
|
@@ -66477,7 +67406,7 @@ function registerAgentCommand(program3) {
|
|
|
66477
67406
|
let hindsightApiUrl = null;
|
|
66478
67407
|
let hindsightBankId = name;
|
|
66479
67408
|
if (isHindsightEnabled(config)) {
|
|
66480
|
-
const baseUrl = config.memory?.config?.url ??
|
|
67409
|
+
const baseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
66481
67410
|
hindsightApiUrl = baseUrl.endsWith("/mcp/") ? baseUrl : baseUrl.replace(/\/$/, "") + "/mcp/";
|
|
66482
67411
|
hindsightBankId = agentConfig.memory?.collection ?? name;
|
|
66483
67412
|
}
|
|
@@ -67161,7 +68090,7 @@ Permissions for ${name}
|
|
|
67161
68090
|
}
|
|
67162
68091
|
}
|
|
67163
68092
|
if (existsSync31(agentDir)) {
|
|
67164
|
-
|
|
68093
|
+
rmSync10(agentDir, { recursive: true, force: true });
|
|
67165
68094
|
console.log(source_default.green(` Removed directory: ${agentDir}`));
|
|
67166
68095
|
} else {
|
|
67167
68096
|
console.log(source_default.gray(` Directory not found: ${agentDir}`));
|
|
@@ -67365,7 +68294,7 @@ switchroom agent add: ${name}
|
|
|
67365
68294
|
const agentsDirRollback = resolveAgentsDir(cfg);
|
|
67366
68295
|
const agentDirRollback = resolve22(agentsDirRollback, name);
|
|
67367
68296
|
if (existsSync31(agentDirRollback)) {
|
|
67368
|
-
|
|
68297
|
+
rmSync10(agentDirRollback, { recursive: true, force: true });
|
|
67369
68298
|
console.log(source_default.gray(` Rolled back: removed ${agentDirRollback}`));
|
|
67370
68299
|
}
|
|
67371
68300
|
process.exit(1);
|
|
@@ -69996,7 +70925,7 @@ function registerAuthCommand(program3) {
|
|
|
69996
70925
|
try {
|
|
69997
70926
|
mode = statSync17(yamlPath).mode & 511;
|
|
69998
70927
|
} catch {}
|
|
69999
|
-
|
|
70928
|
+
atomicWriteFileSync(yamlPath, after, mode);
|
|
70000
70929
|
if (!quiet) {
|
|
70001
70930
|
console.log(source_default.gray(` Pinned auth.active: ${label} in ${yamlPath}`));
|
|
70002
70931
|
}
|
|
@@ -70143,9 +71072,9 @@ import { readFileSync as readFileSync39 } from "node:fs";
|
|
|
70143
71072
|
init_source();
|
|
70144
71073
|
init_loader();
|
|
70145
71074
|
init_vault();
|
|
70146
|
-
|
|
71075
|
+
init_hindsight2();
|
|
70147
71076
|
import { readFileSync as readFileSync31, writeFileSync as writeFileSync17, copyFileSync as copyFileSync6, existsSync as existsSync36, readdirSync as readdirSync16, statSync as statSync18 } from "node:fs";
|
|
70148
|
-
import { execFileSync as
|
|
71077
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
70149
71078
|
import { join as join28 } from "node:path";
|
|
70150
71079
|
import { homedir as homedir13 } from "node:os";
|
|
70151
71080
|
function getVaultPath2(configPath) {
|
|
@@ -70492,7 +71421,7 @@ function registerVaultSweep(vault, program3) {
|
|
|
70492
71421
|
function scrubSqlite(path, values, dryRun) {
|
|
70493
71422
|
let hasSqlite = true;
|
|
70494
71423
|
try {
|
|
70495
|
-
|
|
71424
|
+
execFileSync16("sqlite3", ["-version"], { timeout: 2000, stdio: "ignore" });
|
|
70496
71425
|
} catch {
|
|
70497
71426
|
hasSqlite = false;
|
|
70498
71427
|
}
|
|
@@ -70504,7 +71433,7 @@ function scrubSqlite(path, values, dryRun) {
|
|
|
70504
71433
|
for (const { value } of values) {
|
|
70505
71434
|
const esc = value.replace(/'/g, "''");
|
|
70506
71435
|
try {
|
|
70507
|
-
const out =
|
|
71436
|
+
const out = execFileSync16("sqlite3", [path, `SELECT COUNT(*) FROM messages WHERE text LIKE '%${esc}%'`], { encoding: "utf8", timeout: 1e4 });
|
|
70508
71437
|
const n = parseInt(out.trim(), 10);
|
|
70509
71438
|
if (!Number.isNaN(n))
|
|
70510
71439
|
matches += n;
|
|
@@ -70518,7 +71447,7 @@ function scrubSqlite(path, values, dryRun) {
|
|
|
70518
71447
|
for (const { key, value } of values) {
|
|
70519
71448
|
const esc = value.replace(/'/g, "''");
|
|
70520
71449
|
const replacement = `vault:${key}`.replace(/'/g, "''");
|
|
70521
|
-
|
|
71450
|
+
execFileSync16("sqlite3", [path, `UPDATE messages SET text = replace(text, '${esc}', '${replacement}')`], { timeout: 15000 });
|
|
70522
71451
|
}
|
|
70523
71452
|
return { target: path, matches, modified: true };
|
|
70524
71453
|
} catch (err) {
|
|
@@ -74170,7 +75099,8 @@ class VaultBroker {
|
|
|
74170
75099
|
return;
|
|
74171
75100
|
}
|
|
74172
75101
|
try {
|
|
74173
|
-
const
|
|
75102
|
+
const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os4.homedir(), ".switchroom", "agents");
|
|
75103
|
+
const tokenDir = path3.join(agentsDir, agent);
|
|
74174
75104
|
mkdirSync24(tokenDir, { recursive: true });
|
|
74175
75105
|
const tokenPath = path3.join(tokenDir, ".vault-token");
|
|
74176
75106
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
@@ -74234,7 +75164,8 @@ class VaultBroker {
|
|
|
74234
75164
|
try {
|
|
74235
75165
|
const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
|
|
74236
75166
|
if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
|
|
74237
|
-
const
|
|
75167
|
+
const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os4.homedir(), ".switchroom", "agents");
|
|
75168
|
+
const tokenPath = path3.join(agentsDir, row.agent_slug, ".vault-token");
|
|
74238
75169
|
if (existsSync41(tokenPath)) {
|
|
74239
75170
|
try {
|
|
74240
75171
|
unlinkSync10(tokenPath);
|
|
@@ -77429,10 +78360,10 @@ function fail2(msg) {
|
|
|
77429
78360
|
|
|
77430
78361
|
// src/cli/memory.ts
|
|
77431
78362
|
init_source();
|
|
77432
|
-
|
|
78363
|
+
init_hindsight2();
|
|
77433
78364
|
|
|
77434
78365
|
// src/memory/search.ts
|
|
77435
|
-
|
|
78366
|
+
init_hindsight2();
|
|
77436
78367
|
function shellQuote2(s) {
|
|
77437
78368
|
return "'" + s.replace(/'/g, `'"'"'`) + "'";
|
|
77438
78369
|
}
|
|
@@ -77459,312 +78390,7 @@ function reflectAcrossAgents(config) {
|
|
|
77459
78390
|
|
|
77460
78391
|
// src/cli/memory.ts
|
|
77461
78392
|
init_helpers();
|
|
77462
|
-
|
|
77463
|
-
// src/setup/hindsight.ts
|
|
77464
|
-
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
77465
|
-
import { createServer as createServer4 } from "node:net";
|
|
77466
|
-
var HINDSIGHT_DEFAULT_API_PORT = 8888;
|
|
77467
|
-
var HINDSIGHT_DEFAULT_UI_PORT = 9999;
|
|
77468
|
-
var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
|
|
77469
|
-
var HINDSIGHT_CONSUMER_NAME = "hindsight";
|
|
77470
|
-
var HINDSIGHT_DEFAULT_UID = 11000;
|
|
77471
|
-
var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
|
|
77472
|
-
var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
|
|
77473
|
-
function hindsightImageRef(tag) {
|
|
77474
|
-
const t = tag?.trim();
|
|
77475
|
-
if (!t)
|
|
77476
|
-
return HINDSIGHT_IMAGE;
|
|
77477
|
-
const v = t.replace(/^v/, "");
|
|
77478
|
-
return `${HINDSIGHT_IMAGE_REPO}:v${v}`;
|
|
77479
|
-
}
|
|
77480
|
-
var HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5";
|
|
77481
|
-
var HINDSIGHT_DEFAULT_MCP_STATELESS = true;
|
|
77482
|
-
var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
|
|
77483
|
-
var HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true";
|
|
77484
|
-
var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
|
|
77485
|
-
var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
|
|
77486
|
-
var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
|
|
77487
|
-
var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
|
|
77488
|
-
var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 12;
|
|
77489
|
-
var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 3;
|
|
77490
|
-
var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 6;
|
|
77491
|
-
var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 300;
|
|
77492
|
-
var HINDSIGHT_DEFAULT_MEM_LIMIT = "8g";
|
|
77493
|
-
var HINDSIGHT_DEFAULT_MEM_RESERVATION = "4g";
|
|
77494
|
-
var HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000;
|
|
77495
|
-
var HINDSIGHT_DEFAULT_SHM_SIZE = "2g";
|
|
77496
|
-
var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
|
|
77497
|
-
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
77498
|
-
function isPortFree(port) {
|
|
77499
|
-
return new Promise((resolve29) => {
|
|
77500
|
-
const server = createServer4();
|
|
77501
|
-
server.once("error", () => resolve29(false));
|
|
77502
|
-
server.once("listening", () => {
|
|
77503
|
-
server.close(() => resolve29(true));
|
|
77504
|
-
});
|
|
77505
|
-
server.listen(port, "127.0.0.1");
|
|
77506
|
-
});
|
|
77507
|
-
}
|
|
77508
|
-
async function findFreePort(start, maxAttempts = 50) {
|
|
77509
|
-
for (let i = 0;i < maxAttempts; i++) {
|
|
77510
|
-
const port = start + i;
|
|
77511
|
-
if (port < 1024)
|
|
77512
|
-
continue;
|
|
77513
|
-
if (await isPortFree(port)) {
|
|
77514
|
-
return port;
|
|
77515
|
-
}
|
|
77516
|
-
}
|
|
77517
|
-
return null;
|
|
77518
|
-
}
|
|
77519
|
-
async function pickHindsightPorts() {
|
|
77520
|
-
if (await isPortFree(HINDSIGHT_DEFAULT_API_PORT) && await isPortFree(HINDSIGHT_DEFAULT_UI_PORT)) {
|
|
77521
|
-
return {
|
|
77522
|
-
apiPort: HINDSIGHT_DEFAULT_API_PORT,
|
|
77523
|
-
uiPort: HINDSIGHT_DEFAULT_UI_PORT
|
|
77524
|
-
};
|
|
77525
|
-
}
|
|
77526
|
-
const apiPort = await findFreePort(18888);
|
|
77527
|
-
const uiPort = await findFreePort(19999);
|
|
77528
|
-
if (apiPort === null || uiPort === null) {
|
|
77529
|
-
throw new Error("Could not find a free port for Hindsight. " + "Stop whatever is using 8888 / 9999 / 18888 / 19999 and retry.");
|
|
77530
|
-
}
|
|
77531
|
-
return { apiPort, uiPort };
|
|
77532
|
-
}
|
|
77533
|
-
function isDockerAvailable() {
|
|
77534
|
-
try {
|
|
77535
|
-
execFileSync16("docker", ["--version"], { stdio: "pipe" });
|
|
77536
|
-
return true;
|
|
77537
|
-
} catch {
|
|
77538
|
-
return false;
|
|
77539
|
-
}
|
|
77540
|
-
}
|
|
77541
|
-
function isHindsightRunning() {
|
|
77542
|
-
try {
|
|
77543
|
-
const output = execFileSync16("docker", ["ps", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
|
|
77544
|
-
return output.trim().length > 0;
|
|
77545
|
-
} catch {
|
|
77546
|
-
return false;
|
|
77547
|
-
}
|
|
77548
|
-
}
|
|
77549
|
-
function isHindsightContainerExists() {
|
|
77550
|
-
try {
|
|
77551
|
-
const output = execFileSync16("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf-8" });
|
|
77552
|
-
return output.trim().length > 0;
|
|
77553
|
-
} catch {
|
|
77554
|
-
return false;
|
|
77555
|
-
}
|
|
77556
|
-
}
|
|
77557
|
-
function startHindsight(ports, litellm, imageTag) {
|
|
77558
|
-
const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
|
|
77559
|
-
const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
|
|
77560
|
-
const envArgs = [
|
|
77561
|
-
"-e",
|
|
77562
|
-
`HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
|
|
77563
|
-
"-e",
|
|
77564
|
-
"HINDSIGHT_API_LLM_PROVIDER=claude-code",
|
|
77565
|
-
"-e",
|
|
77566
|
-
`HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
|
|
77567
|
-
"-e",
|
|
77568
|
-
`HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
|
|
77569
|
-
"-e",
|
|
77570
|
-
`HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
|
|
77571
|
-
"-e",
|
|
77572
|
-
`HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
|
|
77573
|
-
"-e",
|
|
77574
|
-
`HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
|
|
77575
|
-
"-e",
|
|
77576
|
-
`HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
|
|
77577
|
-
"-e",
|
|
77578
|
-
`HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
|
|
77579
|
-
"-e",
|
|
77580
|
-
`HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
|
|
77581
|
-
"-e",
|
|
77582
|
-
`HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
|
|
77583
|
-
"-e",
|
|
77584
|
-
`HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
|
|
77585
|
-
"-e",
|
|
77586
|
-
`HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`
|
|
77587
|
-
];
|
|
77588
|
-
if (litellm) {
|
|
77589
|
-
envArgs.push("-e", `HINDSIGHT_API_PORT=${apiPort}`, "-e", `ANTHROPIC_BASE_URL=${litellm.baseUrl.replace(/\/+$/, "")}/anthropic`, "-e", `ANTHROPIC_CUSTOM_HEADERS=x-litellm-api-key: Bearer ${litellm.apiKey}
|
|
77590
|
-
x-litellm-customer-id: hindsight
|
|
77591
|
-
x-litellm-tags: service:hindsight`);
|
|
77592
|
-
}
|
|
77593
|
-
const args = [
|
|
77594
|
-
"run",
|
|
77595
|
-
"-d",
|
|
77596
|
-
"--name",
|
|
77597
|
-
"switchroom-hindsight",
|
|
77598
|
-
"--restart",
|
|
77599
|
-
"always",
|
|
77600
|
-
`--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
|
|
77601
|
-
`--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
|
|
77602
|
-
`--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
|
|
77603
|
-
`--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
|
|
77604
|
-
"--health-cmd",
|
|
77605
|
-
litellm ? `python3 -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:${apiPort}/health",timeout=4).getcode()==200 else 1)'` : HINDSIGHT_HEALTHCHECK_CMD,
|
|
77606
|
-
"--health-interval",
|
|
77607
|
-
"30s",
|
|
77608
|
-
"--health-timeout",
|
|
77609
|
-
"5s",
|
|
77610
|
-
"--health-retries",
|
|
77611
|
-
"3",
|
|
77612
|
-
"--health-start-period",
|
|
77613
|
-
"60s"
|
|
77614
|
-
];
|
|
77615
|
-
if (litellm) {
|
|
77616
|
-
args.push("--network", "host");
|
|
77617
|
-
} else {
|
|
77618
|
-
args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
|
|
77619
|
-
}
|
|
77620
|
-
args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, hindsightImageRef(imageTag));
|
|
77621
|
-
execFileSync16("docker", args, { stdio: "pipe" });
|
|
77622
|
-
}
|
|
77623
|
-
function stopHindsight() {
|
|
77624
|
-
try {
|
|
77625
|
-
execFileSync16("docker", ["stop", "switchroom-hindsight"], { stdio: "pipe" });
|
|
77626
|
-
} catch {}
|
|
77627
|
-
try {
|
|
77628
|
-
execFileSync16("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
|
|
77629
|
-
} catch {}
|
|
77630
|
-
}
|
|
77631
|
-
function pullHindsightImage(imageTag) {
|
|
77632
|
-
execFileSync16("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
|
|
77633
|
-
}
|
|
77634
|
-
function getRunningHindsightPorts() {
|
|
77635
|
-
const readPort = (containerPort) => {
|
|
77636
|
-
try {
|
|
77637
|
-
const out = execFileSync16("docker", ["port", "switchroom-hindsight", `${containerPort}/tcp`], { stdio: "pipe", encoding: "utf-8" });
|
|
77638
|
-
const m = out.split(`
|
|
77639
|
-
`).map((l) => l.trim()).find((l) => l.length > 0);
|
|
77640
|
-
if (!m)
|
|
77641
|
-
return null;
|
|
77642
|
-
const port = Number(m.slice(m.lastIndexOf(":") + 1));
|
|
77643
|
-
return Number.isInteger(port) && port > 0 ? port : null;
|
|
77644
|
-
} catch {
|
|
77645
|
-
return null;
|
|
77646
|
-
}
|
|
77647
|
-
};
|
|
77648
|
-
const apiPort = readPort(HINDSIGHT_DEFAULT_API_PORT);
|
|
77649
|
-
if (apiPort === null)
|
|
77650
|
-
return null;
|
|
77651
|
-
const uiPort = readPort(HINDSIGHT_DEFAULT_UI_PORT) ?? (apiPort === HINDSIGHT_DEFAULT_API_PORT ? HINDSIGHT_DEFAULT_UI_PORT : 19999);
|
|
77652
|
-
return { apiPort, uiPort };
|
|
77653
|
-
}
|
|
77654
|
-
function getHindsightStatus() {
|
|
77655
|
-
try {
|
|
77656
|
-
const output = execFileSync16("docker", ["ps", "-a", "--filter", "name=switchroom-hindsight", "--format", "{{.Status}}"], { stdio: "pipe", encoding: "utf-8" });
|
|
77657
|
-
const status = output.trim();
|
|
77658
|
-
return status.length > 0 ? status : null;
|
|
77659
|
-
} catch {
|
|
77660
|
-
return null;
|
|
77661
|
-
}
|
|
77662
|
-
}
|
|
77663
|
-
function generateHindsightComposeSnippet() {
|
|
77664
|
-
return [
|
|
77665
|
-
"services:",
|
|
77666
|
-
" switchroom-hindsight:",
|
|
77667
|
-
` image: ${HINDSIGHT_IMAGE}`,
|
|
77668
|
-
" container_name: switchroom-hindsight",
|
|
77669
|
-
" ports:",
|
|
77670
|
-
' - "8888:8888"',
|
|
77671
|
-
' - "9999:9999"',
|
|
77672
|
-
" environment:",
|
|
77673
|
-
` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
|
|
77674
|
-
" - HINDSIGHT_API_LLM_PROVIDER=claude-code",
|
|
77675
|
-
` - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
|
|
77676
|
-
` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
|
|
77677
|
-
` - HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING=${HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING}`,
|
|
77678
|
-
` - HINDSIGHT_API_RERANKER_MAX_CANDIDATES=${HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES}`,
|
|
77679
|
-
` - HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT}`,
|
|
77680
|
-
` - HINDSIGHT_API_RECALL_MAX_CONCURRENT=${HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT}`,
|
|
77681
|
-
` - HINDSIGHT_API_REFLECT_WALL_TIMEOUT=${HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S}`,
|
|
77682
|
-
` - HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE}`,
|
|
77683
|
-
` - HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS}`,
|
|
77684
|
-
` - HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM=${HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM}`,
|
|
77685
|
-
` - HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND=${HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND}`,
|
|
77686
|
-
` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
|
|
77687
|
-
` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
|
|
77688
|
-
` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
|
|
77689
|
-
` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
|
|
77690
|
-
" healthcheck:",
|
|
77691
|
-
` test: ${JSON.stringify(["CMD", "python3", "-c", HINDSIGHT_HEALTHCHECK_PY])}`,
|
|
77692
|
-
" interval: 30s",
|
|
77693
|
-
" timeout: 5s",
|
|
77694
|
-
" retries: 3",
|
|
77695
|
-
" start_period: 60s",
|
|
77696
|
-
" volumes:",
|
|
77697
|
-
" - switchroom-hindsight-data:/home/hindsight/.pg0",
|
|
77698
|
-
" - switchroom-hindsight-backups:/backups",
|
|
77699
|
-
` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
|
|
77700
|
-
" tmpfs:",
|
|
77701
|
-
` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
|
|
77702
|
-
" restart: always",
|
|
77703
|
-
"",
|
|
77704
|
-
"volumes:",
|
|
77705
|
-
" switchroom-hindsight-data:",
|
|
77706
|
-
" switchroom-hindsight-backups:",
|
|
77707
|
-
` ${HINDSIGHT_BROKER_SOCK_VOLUME}:`,
|
|
77708
|
-
" external: true",
|
|
77709
|
-
" # Bound by the switchroom-auth-broker singleton in the main",
|
|
77710
|
-
" # switchroom compose project. Declare an `auth.consumers[]`",
|
|
77711
|
-
` # entry named "${HINDSIGHT_CONSUMER_NAME}" in switchroom.yaml.`
|
|
77712
|
-
].join(`
|
|
77713
|
-
`);
|
|
77714
|
-
}
|
|
77715
|
-
async function ensureHindsightConsumer(configPath, account, uid = HINDSIGHT_DEFAULT_UID) {
|
|
77716
|
-
const fs4 = await import("node:fs");
|
|
77717
|
-
const { parseDocument: parseDocument7, isMap: isMap7, isSeq: isSeq4, YAMLMap, YAMLSeq } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
|
|
77718
|
-
const { atomicWriteFileSync: atomicWriteFileSync3 } = await Promise.resolve().then(() => (init_atomic(), exports_atomic));
|
|
77719
|
-
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
77720
|
-
const doc = parseDocument7(raw);
|
|
77721
|
-
const root = doc.contents;
|
|
77722
|
-
if (!isMap7(root)) {
|
|
77723
|
-
return { added: false, reason: "switchroom.yaml root is not a map" };
|
|
77724
|
-
}
|
|
77725
|
-
const authRaw = root.get("auth", true);
|
|
77726
|
-
let authNode;
|
|
77727
|
-
if (isMap7(authRaw)) {
|
|
77728
|
-
authNode = authRaw;
|
|
77729
|
-
} else {
|
|
77730
|
-
authNode = new YAMLMap;
|
|
77731
|
-
doc.setIn(["auth"], authNode);
|
|
77732
|
-
}
|
|
77733
|
-
const consumersRaw = authNode.get("consumers", true);
|
|
77734
|
-
let consumersNode;
|
|
77735
|
-
if (isSeq4(consumersRaw)) {
|
|
77736
|
-
consumersNode = consumersRaw;
|
|
77737
|
-
} else {
|
|
77738
|
-
consumersNode = new YAMLSeq;
|
|
77739
|
-
doc.setIn(["auth", "consumers"], consumersNode);
|
|
77740
|
-
}
|
|
77741
|
-
for (const item of consumersNode.items) {
|
|
77742
|
-
if (isMap7(item)) {
|
|
77743
|
-
const name = item.get("name");
|
|
77744
|
-
if (name === HINDSIGHT_CONSUMER_NAME) {
|
|
77745
|
-
return { added: false, reason: "already present" };
|
|
77746
|
-
}
|
|
77747
|
-
}
|
|
77748
|
-
}
|
|
77749
|
-
const entry = new YAMLMap;
|
|
77750
|
-
entry.set("name", HINDSIGHT_CONSUMER_NAME);
|
|
77751
|
-
if (account !== undefined)
|
|
77752
|
-
entry.set("account", account);
|
|
77753
|
-
entry.set("uid", uid);
|
|
77754
|
-
consumersNode.add(entry);
|
|
77755
|
-
const out = String(doc);
|
|
77756
|
-
const tail = out.endsWith(`
|
|
77757
|
-
`) ? out : out + `
|
|
77758
|
-
`;
|
|
77759
|
-
let mode = 420;
|
|
77760
|
-
try {
|
|
77761
|
-
mode = fs4.statSync(configPath).mode & 511;
|
|
77762
|
-
} catch {}
|
|
77763
|
-
atomicWriteFileSync3(configPath, tail, mode);
|
|
77764
|
-
return { added: true, reason: "added" };
|
|
77765
|
-
}
|
|
77766
|
-
|
|
77767
|
-
// src/cli/memory.ts
|
|
78393
|
+
init_hindsight();
|
|
77768
78394
|
init_client();
|
|
77769
78395
|
init_loader();
|
|
77770
78396
|
var import_yaml13 = __toESM(require_dist(), 1);
|
|
@@ -77777,7 +78403,11 @@ async function resolveLiteLLMForHindsight(config) {
|
|
|
77777
78403
|
const result = await getViaBrokerStructured("litellm/hindsight/api-key").catch(() => null);
|
|
77778
78404
|
if (!result || result.kind !== "ok" || result.entry.kind !== "string")
|
|
77779
78405
|
return;
|
|
77780
|
-
return {
|
|
78406
|
+
return {
|
|
78407
|
+
baseUrl: topLiteLLM.base_url,
|
|
78408
|
+
apiKey: result.entry.value,
|
|
78409
|
+
model: config.memory?.config?.llm_model
|
|
78410
|
+
};
|
|
77781
78411
|
}
|
|
77782
78412
|
function readRecallLog(agentDir, limit) {
|
|
77783
78413
|
const path4 = join40(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
|
|
@@ -77984,6 +78614,21 @@ Cross-agent reflection plan
|
|
|
77984
78614
|
let ports;
|
|
77985
78615
|
if (reusePorts) {
|
|
77986
78616
|
ports = reusePorts;
|
|
78617
|
+
if (reusePorts.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
|
|
78618
|
+
let explicitUrl;
|
|
78619
|
+
try {
|
|
78620
|
+
explicitUrl = getConfig(program3).memory?.config?.url;
|
|
78621
|
+
} catch {
|
|
78622
|
+
explicitUrl = undefined;
|
|
78623
|
+
}
|
|
78624
|
+
if (!explicitUrl) {
|
|
78625
|
+
console.log(source_default.yellow(`
|
|
78626
|
+
\u26a0 MIGRATION HAZARD: reusing stale host port ${reusePorts.apiPort} ` + `for switchroom-hindsight, but scaffolding now defaults to ` + `${HINDSIGHT_DEFAULT_API_PORT} and no explicit memory.config.url is set.`));
|
|
78627
|
+
console.log(source_default.yellow(` Agents may be repointed to a dead URL (http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/) ` + `while the container listens on ${reusePorts.apiPort}.`));
|
|
78628
|
+
console.log(source_default.yellow(` Fix: set memory.config.url: http://127.0.0.1:${reusePorts.apiPort}/mcp/ explicitly, ` + `or migrate the container to port ${HINDSIGHT_DEFAULT_API_PORT}.
|
|
78629
|
+
`));
|
|
78630
|
+
}
|
|
78631
|
+
}
|
|
77987
78632
|
} else {
|
|
77988
78633
|
try {
|
|
77989
78634
|
ports = await pickHindsightPorts();
|
|
@@ -77997,14 +78642,38 @@ Cross-agent reflection plan
|
|
|
77997
78642
|
if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
|
|
77998
78643
|
console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; ` + `using ${ports.apiPort}/${ports.uiPort} instead.`));
|
|
77999
78644
|
}
|
|
78645
|
+
let conflict = await preflightHindsightPorts(ports);
|
|
78646
|
+
if (conflict) {
|
|
78647
|
+
const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
|
|
78648
|
+
console.log(source_default.yellow(`
|
|
78649
|
+
Chosen Hindsight port ${conflict.port} is occupied${heldBy}; ` + `selecting a free port instead of crash-looping.`));
|
|
78650
|
+
try {
|
|
78651
|
+
ports = await pickHindsightPorts();
|
|
78652
|
+
} catch (err) {
|
|
78653
|
+
console.error(source_default.red(`
|
|
78654
|
+
${err.message}
|
|
78655
|
+
`));
|
|
78656
|
+
process.exit(1);
|
|
78657
|
+
}
|
|
78658
|
+
conflict = await preflightHindsightPorts(ports);
|
|
78659
|
+
if (conflict) {
|
|
78660
|
+
const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
|
|
78661
|
+
console.error(source_default.red(`
|
|
78662
|
+
Refusing to start Hindsight: port ${conflict.port} is still ` + `occupied${stillHeldBy} after reassignment. Free it and retry ` + `\`switchroom memory --start\`.
|
|
78663
|
+
`));
|
|
78664
|
+
process.exit(1);
|
|
78665
|
+
}
|
|
78666
|
+
console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
|
|
78667
|
+
}
|
|
78000
78668
|
if (opts.provider && opts.provider !== "claude-code") {
|
|
78001
78669
|
console.log(source_default.gray(` Note: --provider=${opts.provider} ignored. switchroom-hindsight is pinned to the ` + "`claude-code` provider (subscription-honest). The flag is kept for back-compat."));
|
|
78002
78670
|
}
|
|
78003
78671
|
const provider = "claude-code";
|
|
78004
78672
|
console.log(source_default.gray(" Starting Hindsight Docker container..."));
|
|
78005
78673
|
try {
|
|
78006
|
-
const
|
|
78007
|
-
|
|
78674
|
+
const hindsightConfig = getConfig(program3);
|
|
78675
|
+
const litellmCfg = await resolveLiteLLMForHindsight(hindsightConfig);
|
|
78676
|
+
startHindsight(ports, litellmCfg, opts.tag, hindsightConfig.hindsight?.llm);
|
|
78008
78677
|
if (litellmCfg) {
|
|
78009
78678
|
console.log(source_default.gray(" LiteLLM routing enabled for hindsight (--network host)."));
|
|
78010
78679
|
}
|
|
@@ -78050,7 +78719,7 @@ Cross-agent reflection plan
|
|
|
78050
78719
|
console.log(source_default.bold(`
|
|
78051
78720
|
# Add this to your docker-compose.yml:
|
|
78052
78721
|
`));
|
|
78053
|
-
console.log(generateHindsightComposeSnippet());
|
|
78722
|
+
console.log(generateHindsightComposeSnippet(getConfig(program3).hindsight?.llm));
|
|
78054
78723
|
console.log();
|
|
78055
78724
|
});
|
|
78056
78725
|
memory.command("recall-log [agent]").description("Show recent auto-recall events (per-turn JSONL log) \u2014 see what was injected, when the cap fired, hit rate hints").option("-n, --limit <n>", "Tail the last N events per agent (default 20)", "20").option("--json", "Emit raw JSONL (one entry per line)").action(withConfigError(async (agent, opts) => {
|
|
@@ -78103,7 +78772,7 @@ ${name}:`));
|
|
|
78103
78772
|
process.exit(1);
|
|
78104
78773
|
}
|
|
78105
78774
|
const collection = getCollectionForAgent(agent, config);
|
|
78106
|
-
const apiUrl = config.memory?.config?.url ??
|
|
78775
|
+
const apiUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
78107
78776
|
const timeoutMs = Math.max(500, parseInt(opts.timeout, 10) || 5000);
|
|
78108
78777
|
console.log(source_default.bold(`
|
|
78109
78778
|
Demoting memory ${source_default.cyan(memoryId)}`));
|
|
@@ -78119,7 +78788,7 @@ Demoting memory ${source_default.cyan(memoryId)}`));
|
|
|
78119
78788
|
console.error(source_default.gray(" The Hindsight MCP `update_memory` tool may not be exposed by your deployment, or the memory ID may be wrong. Try `switchroom memory recall-log " + agent + " --json` to confirm the ID surfaced recently."));
|
|
78120
78789
|
process.exit(1);
|
|
78121
78790
|
}));
|
|
78122
|
-
const restBase = (url) => (url ??
|
|
78791
|
+
const restBase = (url) => (url ?? HINDSIGHT_DEFAULT_MCP_URL).replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
78123
78792
|
const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
|
|
78124
78793
|
const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
|
|
78125
78794
|
profile.command("add <bank> <fact...>").description("Add an operator-authored fact to a profile bank (creates the bank on first write)").option("--timeout <ms>", "HTTP timeout in milliseconds (retain runs synchronously; default: 60000)", "60000").action(withConfigError(async (bank, factWords, opts) => {
|
|
@@ -78227,13 +78896,13 @@ import { timingSafeEqual as timingSafeEqual3, randomBytes as randomBytes11 } fro
|
|
|
78227
78896
|
// src/web/api.ts
|
|
78228
78897
|
init_lifecycle();
|
|
78229
78898
|
init_manager();
|
|
78230
|
-
|
|
78899
|
+
init_hindsight2();
|
|
78231
78900
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
78232
78901
|
import { existsSync as existsSync51, readFileSync as readFileSync45, statSync as statSync23 } from "node:fs";
|
|
78233
78902
|
import { resolve as resolve29, join as join45 } from "node:path";
|
|
78234
78903
|
|
|
78235
78904
|
// src/web/memory-remediation.ts
|
|
78236
|
-
|
|
78905
|
+
init_hindsight2();
|
|
78237
78906
|
function restBaseFromMcpUrl(mcpUrl) {
|
|
78238
78907
|
return hindsightRestBase(mcpUrl);
|
|
78239
78908
|
}
|
|
@@ -78295,6 +78964,7 @@ async function buildUserProfile(mcpUrl, bank, opts) {
|
|
|
78295
78964
|
}
|
|
78296
78965
|
|
|
78297
78966
|
// src/web/api.ts
|
|
78967
|
+
init_hindsight();
|
|
78298
78968
|
init_audit_reader();
|
|
78299
78969
|
|
|
78300
78970
|
// src/scheduler/dispatch.ts
|
|
@@ -82307,6 +82977,7 @@ var NVIDIA_TOOLKIT_INSTALL_HINT = "https://docs.nvidia.com/datacenter/cloud-nati
|
|
|
82307
82977
|
|
|
82308
82978
|
// src/cli/setup.ts
|
|
82309
82979
|
init_host_capabilities();
|
|
82980
|
+
init_hindsight();
|
|
82310
82981
|
init_client();
|
|
82311
82982
|
init_posthog();
|
|
82312
82983
|
|
|
@@ -82852,7 +83523,11 @@ async function resolveLiteLLMForHindsight2(config) {
|
|
|
82852
83523
|
const result = await getViaBrokerStructured("litellm/hindsight/api-key").catch(() => null);
|
|
82853
83524
|
if (!result || result.kind !== "ok" || result.entry.kind !== "string")
|
|
82854
83525
|
return;
|
|
82855
|
-
return {
|
|
83526
|
+
return {
|
|
83527
|
+
baseUrl: topLiteLLM.base_url,
|
|
83528
|
+
apiKey: result.entry.value,
|
|
83529
|
+
model: config.memory?.config?.llm_model
|
|
83530
|
+
};
|
|
82856
83531
|
}
|
|
82857
83532
|
async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
|
|
82858
83533
|
stepHeader(6, "Memory backend", STEP_ACTIVE);
|
|
@@ -82913,17 +83588,45 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath) {
|
|
|
82913
83588
|
console.log(source_default.gray(" Found stopped switchroom-hindsight container, removing..."));
|
|
82914
83589
|
stopHindsight();
|
|
82915
83590
|
}
|
|
83591
|
+
let ports;
|
|
83592
|
+
try {
|
|
83593
|
+
ports = await pickHindsightPorts();
|
|
83594
|
+
} catch (err) {
|
|
83595
|
+
console.log(source_default.red(` ${err.message}`));
|
|
83596
|
+
return;
|
|
83597
|
+
}
|
|
83598
|
+
if (ports.apiPort !== HINDSIGHT_DEFAULT_API_PORT) {
|
|
83599
|
+
console.log(source_default.yellow(` Port ${HINDSIGHT_DEFAULT_API_PORT} is already in use; using ${ports.apiPort}/${ports.uiPort} instead.`));
|
|
83600
|
+
}
|
|
83601
|
+
let conflict = await preflightHindsightPorts(ports);
|
|
83602
|
+
if (conflict) {
|
|
83603
|
+
const heldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
|
|
83604
|
+
console.log(source_default.yellow(` Chosen Hindsight port ${conflict.port} is occupied${heldBy}; selecting a free port instead of crash-looping.`));
|
|
83605
|
+
try {
|
|
83606
|
+
ports = await pickHindsightPorts();
|
|
83607
|
+
} catch (err) {
|
|
83608
|
+
console.log(source_default.red(` ${err.message}`));
|
|
83609
|
+
return;
|
|
83610
|
+
}
|
|
83611
|
+
conflict = await preflightHindsightPorts(ports);
|
|
83612
|
+
if (conflict) {
|
|
83613
|
+
const stillHeldBy = conflict.holder ? ` (held by ${conflict.holder})` : "";
|
|
83614
|
+
console.log(source_default.red(` Refusing to start Hindsight: port ${conflict.port} is still occupied${stillHeldBy} after reassignment. Free it and re-run \`switchroom setup\`.`));
|
|
83615
|
+
return;
|
|
83616
|
+
}
|
|
83617
|
+
console.log(source_default.green(` Reassigned Hindsight to port ${ports.apiPort}/${ports.uiPort}.`));
|
|
83618
|
+
}
|
|
82916
83619
|
const spin = spinner("Starting Hindsight Docker container...");
|
|
82917
83620
|
try {
|
|
82918
83621
|
const litellmCfg = await resolveLiteLLMForHindsight2(config);
|
|
82919
|
-
startHindsight(undefined,
|
|
83622
|
+
startHindsight(ports, litellmCfg, undefined, config.hindsight?.llm);
|
|
82920
83623
|
if (litellmCfg) {
|
|
82921
83624
|
console.log(source_default.gray(" LiteLLM routing enabled (--network host, ANTHROPIC_BASE_URL set)."));
|
|
82922
83625
|
}
|
|
82923
83626
|
if (isHindsightRunning()) {
|
|
82924
83627
|
spin.stop(source_default.green(`${STEP_DONE} Hindsight container started (switchroom-hindsight)`));
|
|
82925
|
-
console.log(source_default.gray(
|
|
82926
|
-
console.log(source_default.gray(
|
|
83628
|
+
console.log(source_default.gray(` API: http://localhost:${ports.apiPort}/mcp`));
|
|
83629
|
+
console.log(source_default.gray(` UI: http://localhost:${ports.uiPort}`));
|
|
82927
83630
|
} else {
|
|
82928
83631
|
spin.stop(source_default.yellow("Container started but may still be initializing"));
|
|
82929
83632
|
}
|
|
@@ -83450,6 +84153,22 @@ var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
|
|
|
83450
84153
|
function encodeUpdateResultLine(payload) {
|
|
83451
84154
|
return `${UPDATE_RESULT_SENTINEL}${JSON.stringify(payload)}`;
|
|
83452
84155
|
}
|
|
84156
|
+
function resolveHindsightPinTag(opts) {
|
|
84157
|
+
if (typeof opts.hindsightPinTag === "string") {
|
|
84158
|
+
return opts.hindsightPinTag || undefined;
|
|
84159
|
+
}
|
|
84160
|
+
if (opts.channel)
|
|
84161
|
+
return;
|
|
84162
|
+
let candidate = opts.pin;
|
|
84163
|
+
if (!candidate) {
|
|
84164
|
+
try {
|
|
84165
|
+
candidate = loadConfig().release?.pin ?? undefined;
|
|
84166
|
+
} catch {
|
|
84167
|
+
candidate = undefined;
|
|
84168
|
+
}
|
|
84169
|
+
}
|
|
84170
|
+
return candidate && /^v\d+\.\d+\.\d+$/.test(candidate) ? candidate : undefined;
|
|
84171
|
+
}
|
|
83453
84172
|
function planUpdate(opts) {
|
|
83454
84173
|
const composePath = opts.composePath ?? DEFAULT_COMPOSE_PATH;
|
|
83455
84174
|
const runner = opts.runner ?? defaultRunner2;
|
|
@@ -83596,10 +84315,14 @@ function planUpdate(opts) {
|
|
|
83596
84315
|
}
|
|
83597
84316
|
steps.push({
|
|
83598
84317
|
name: "refresh-hindsight",
|
|
83599
|
-
description: "switchroom memory setup --recreate \u2014 pull
|
|
84318
|
+
description: "switchroom memory setup --recreate \u2014 pull the hindsight image (pinned to the release target when set, else :latest) + recreate the memory singleton (reusing its current port)",
|
|
83600
84319
|
skipReason: !memoryBackendHindsight ? "memory.backend is not hindsight \u2014 no hindsight singleton to refresh" : opts.skipImages ? "--skip-images flag set" : undefined,
|
|
83601
84320
|
run: () => {
|
|
83602
|
-
const
|
|
84321
|
+
const pinTag = resolveHindsightPinTag(opts);
|
|
84322
|
+
const setupArgs = ["memory", "setup", "--recreate"];
|
|
84323
|
+
if (pinTag)
|
|
84324
|
+
setupArgs.push("--tag", pinTag);
|
|
84325
|
+
const r = runner(process.execPath, [scriptPath, ...setupArgs]);
|
|
83603
84326
|
if (r.status !== 0)
|
|
83604
84327
|
throw new Error("switchroom memory setup --recreate failed");
|
|
83605
84328
|
}
|
|
@@ -83963,6 +84686,7 @@ import { homedir as homedir39 } from "node:os";
|
|
|
83963
84686
|
init_operator_uid();
|
|
83964
84687
|
init_atomic();
|
|
83965
84688
|
init_audit_reader();
|
|
84689
|
+
init_hindsight();
|
|
83966
84690
|
function normalizeVersion(v) {
|
|
83967
84691
|
return v.trim().replace(/^v/, "");
|
|
83968
84692
|
}
|
|
@@ -86902,7 +87626,7 @@ import { existsSync as existsSync72, readFileSync as readFileSync63, readdirSync
|
|
|
86902
87626
|
import { resolve as resolve45, join as join71 } from "node:path";
|
|
86903
87627
|
import { createHash as createHash14 } from "node:crypto";
|
|
86904
87628
|
init_merge();
|
|
86905
|
-
|
|
87629
|
+
init_hindsight2();
|
|
86906
87630
|
function formatBytes(bytes) {
|
|
86907
87631
|
return `${bytes.toLocaleString()} bytes`;
|
|
86908
87632
|
}
|
|
@@ -87323,6 +88047,8 @@ async function claimWorktree(input, codeRepos) {
|
|
|
87323
88047
|
const baseDir = worktreesBaseDir();
|
|
87324
88048
|
mkdirSync38(baseDir, { recursive: true });
|
|
87325
88049
|
worktreePath = join73(baseDir, `${id}-${taskSuffix}`);
|
|
88050
|
+
const ambientOwner = process.env.SWITCHROOM_AGENT_NAME;
|
|
88051
|
+
const ownerAgent = input.ownerAgent ?? (ambientOwner != null && ambientOwner !== "" ? ambientOwner : undefined);
|
|
87326
88052
|
const now = new Date().toISOString();
|
|
87327
88053
|
const record2 = {
|
|
87328
88054
|
id,
|
|
@@ -87332,7 +88058,7 @@ async function claimWorktree(input, codeRepos) {
|
|
|
87332
88058
|
path: worktreePath,
|
|
87333
88059
|
createdAt: now,
|
|
87334
88060
|
heartbeatAt: now,
|
|
87335
|
-
ownerAgent
|
|
88061
|
+
ownerAgent
|
|
87336
88062
|
};
|
|
87337
88063
|
writeRecord(record2);
|
|
87338
88064
|
} finally {
|