switchroom 0.15.36 → 0.15.38
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/dist/agent-scheduler/index.js +10 -9
- package/dist/auth-broker/index.js +9 -9
- package/dist/cli/autoaccept-poll.js +13 -7
- package/dist/cli/notion-write-pretool.mjs +9 -9
- package/dist/cli/switchroom.js +480 -217
- package/dist/cli/ui/index.html +87 -17
- package/dist/host-control/main.js +10 -10
- package/dist/vault/approvals/kernel-server.js +9 -9
- package/dist/vault/broker/server.js +9 -9
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +1 -1
- package/profiles/_base/start.sh.hbs +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +25 -0
- package/skills/switchroom-manage/SKILL.md +1 -1
- package/skills/switchroom-runtime/SKILL.md +1 -1
- package/telegram-plugin/answer-stream.ts +1 -1
- package/telegram-plugin/bridge/bridge.ts +50 -1
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/bridge/tool-filter.ts +77 -0
- package/telegram-plugin/chat-lock.ts +1 -1
- package/telegram-plugin/credits-watch.ts +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +60 -3
- package/telegram-plugin/dist/gateway/gateway.js +753 -207
- package/telegram-plugin/dist/server.js +64 -4
- package/telegram-plugin/gateway/auto-classify-mid-turn.ts +1 -1
- package/telegram-plugin/gateway/boot-card.ts +5 -1
- package/telegram-plugin/gateway/boot-probes.ts +62 -0
- package/telegram-plugin/gateway/cron-session.ts +1 -1
- package/telegram-plugin/gateway/gateway.ts +254 -15
- package/telegram-plugin/gateway/grant-restart.ts +1 -1
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +1 -1
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +1 -1
- package/telegram-plugin/gateway/inbound-delivery-machine.ts +1 -1
- package/telegram-plugin/gateway/interrupt-defer.ts +1 -1
- package/telegram-plugin/gateway/ipc-protocol.ts +12 -0
- package/telegram-plugin/gateway/linear-activity.ts +56 -0
- package/telegram-plugin/gateway/linear-auth-watch.ts +102 -0
- package/telegram-plugin/gateway/linear-setup.ts +196 -0
- package/telegram-plugin/gateway/permission-card-origin.ts +62 -0
- package/telegram-plugin/gateway/permission-timeout.ts +70 -0
- package/telegram-plugin/gateway/prefix-warmup.ts +1 -1
- package/telegram-plugin/gateway/webhook-ingest-server.test.ts +1 -1
- package/telegram-plugin/gateway/webhook-ingest-server.ts +1 -1
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +1 -1
- package/telegram-plugin/interrupt-marker.ts +1 -1
- package/telegram-plugin/over-ping-safety-net.ts +1 -1
- package/telegram-plugin/scoped-approval.ts +1 -1
- package/telegram-plugin/secret-detect/vault-error.ts +1 -1
- package/telegram-plugin/silence-poke.ts +2 -2
- package/telegram-plugin/silent-reply-anchor.ts +1 -1
- package/telegram-plugin/slot-banner-driver.ts +1 -1
- package/telegram-plugin/startup-reset.ts +1 -1
- package/telegram-plugin/tests/boot-probes-connections.test.ts +66 -0
- package/telegram-plugin/tests/gateway-startup-reset.test.ts +1 -1
- package/telegram-plugin/tests/inbound-delivery-machine.test.ts +1 -1
- package/telegram-plugin/tests/linear-agent-activity.test.ts +77 -0
- package/telegram-plugin/tests/linear-agent-setup.test.ts +132 -0
- package/telegram-plugin/tests/linear-auth-watch.test.ts +79 -0
- package/telegram-plugin/tests/linear-create-issue.test.ts +3 -1
- package/telegram-plugin/tests/permission-card-origin.test.ts +97 -0
- package/telegram-plugin/tests/permission-card-routing.test.ts +23 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +76 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +87 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +1 -1
- package/telegram-plugin/tests/silence-poke.test.ts +1 -1
- package/telegram-plugin/tests/tool-filter.test.ts +87 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +1 -1
- package/telegram-plugin/turn-flush-safety.ts +1 -1
- package/telegram-plugin/uat/assertions.ts +1 -1
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-fast-ack-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-fast-trivial-dm.test.ts +2 -2
- package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-memory-survives-restart-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-wake-audit-content-dm.test.ts +1 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -13565,7 +13565,7 @@ var init_schema = __esm(() => {
|
|
|
13565
13565
|
ScheduleEntrySchema = exports_external.object({
|
|
13566
13566
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
13567
13567
|
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
13568
|
-
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (
|
|
13568
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
|
|
13569
13569
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
13570
13570
|
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
13571
13571
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
@@ -13574,7 +13574,7 @@ var init_schema = __esm(() => {
|
|
|
13574
13574
|
topic: exports_external.union([
|
|
13575
13575
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
13576
13576
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
13577
|
-
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See
|
|
13577
|
+
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
|
|
13578
13578
|
}).superRefine((entry, ctx) => {
|
|
13579
13579
|
const kind = entry.kind ?? "prompt";
|
|
13580
13580
|
if (kind === "poll" && !entry.poll) {
|
|
@@ -13747,15 +13747,15 @@ var init_schema = __esm(() => {
|
|
|
13747
13747
|
webhook_rate_limit: exports_external.object({
|
|
13748
13748
|
rpm: exports_external.number().int().positive()
|
|
13749
13749
|
}).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
|
|
13750
|
-
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See
|
|
13751
|
-
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See
|
|
13750
|
+
webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See reference/rfcs/webhook-via-gateway-socket.md."),
|
|
13751
|
+
webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See reference/rfcs/webhook-cloudflare-edge-lock.md."),
|
|
13752
13752
|
linear_agent: exports_external.object({
|
|
13753
13753
|
enabled: exports_external.boolean(),
|
|
13754
13754
|
token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
|
|
13755
13755
|
workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
|
|
13756
13756
|
default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
|
|
13757
13757
|
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
13758
|
-
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See
|
|
13758
|
+
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See reference/rfcs/supergroup-mode.md."),
|
|
13759
13759
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
13760
13760
|
topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
|
|
13761
13761
|
}).optional().superRefine((tg, ctx) => {
|
|
@@ -13981,7 +13981,7 @@ var init_schema = __esm(() => {
|
|
|
13981
13981
|
drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent \u2014 they live at the top level."),
|
|
13982
13982
|
google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides \u2014 currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent \u2014 they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
|
|
13983
13983
|
microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override \u2014 pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
|
|
13984
|
-
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC
|
|
13984
|
+
notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
|
|
13985
13985
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
13986
13986
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
13987
13987
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
@@ -14116,9 +14116,9 @@ var init_schema = __esm(() => {
|
|
|
14116
14116
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
14117
14117
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
14118
14118
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration \u2014 OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
14119
|
-
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC
|
|
14119
|
+
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
14120
14120
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
14121
|
-
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (
|
|
14121
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
14122
14122
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
14123
14123
|
web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
|
|
14124
14124
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
@@ -14140,7 +14140,7 @@ var init_schema = __esm(() => {
|
|
|
14140
14140
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
14141
14141
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
14142
14142
|
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
14143
|
-
cron: CronConfigSchema.optional().describe("Cheap-cron settings (
|
|
14143
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
|
|
14144
14144
|
});
|
|
14145
14145
|
});
|
|
14146
14146
|
|
|
@@ -31570,6 +31570,106 @@ var init_doctor_notion = __esm(() => {
|
|
|
31570
31570
|
HEARTBEAT_STALE_MS = 60 * 1000;
|
|
31571
31571
|
});
|
|
31572
31572
|
|
|
31573
|
+
// src/cli/doctor-mcp-secrets.ts
|
|
31574
|
+
function computeMcpSecretRequirements(config) {
|
|
31575
|
+
const cfg = config;
|
|
31576
|
+
const reqs = [];
|
|
31577
|
+
for (const [agent, agentConfig] of Object.entries(cfg.agents ?? {})) {
|
|
31578
|
+
if (!agentConfig)
|
|
31579
|
+
continue;
|
|
31580
|
+
const profileName = agentConfig.extends;
|
|
31581
|
+
const profileMcp = profileName != null && profileName.length > 0 ? cfg.profiles?.[profileName]?.mcp_servers ?? {} : {};
|
|
31582
|
+
const effectiveMcp = {
|
|
31583
|
+
...cfg.defaults?.mcp_servers ?? {},
|
|
31584
|
+
...profileMcp,
|
|
31585
|
+
...agentConfig.mcp_servers ?? {}
|
|
31586
|
+
};
|
|
31587
|
+
for (const [server, entry] of Object.entries(effectiveMcp)) {
|
|
31588
|
+
if (!entry || typeof entry !== "object")
|
|
31589
|
+
continue;
|
|
31590
|
+
const declared = entry.secrets;
|
|
31591
|
+
if (!Array.isArray(declared))
|
|
31592
|
+
continue;
|
|
31593
|
+
const keys = declared.filter((k) => typeof k === "string" && k.length > 0);
|
|
31594
|
+
if (keys.length === 0)
|
|
31595
|
+
continue;
|
|
31596
|
+
reqs.push({ agent, server, keys });
|
|
31597
|
+
}
|
|
31598
|
+
}
|
|
31599
|
+
return reqs;
|
|
31600
|
+
}
|
|
31601
|
+
async function runMcpSecretChecks(config, deps = {}) {
|
|
31602
|
+
const reqs = computeMcpSecretRequirements(config);
|
|
31603
|
+
if (reqs.length === 0)
|
|
31604
|
+
return [];
|
|
31605
|
+
const read = deps.vaultAclReader ?? (async () => ({
|
|
31606
|
+
kind: "unreachable",
|
|
31607
|
+
msg: "no vault reader wired"
|
|
31608
|
+
}));
|
|
31609
|
+
const aclCache = new Map;
|
|
31610
|
+
const readKey = async (key) => {
|
|
31611
|
+
const cached = aclCache.get(key);
|
|
31612
|
+
if (cached)
|
|
31613
|
+
return cached;
|
|
31614
|
+
const r = await read(key);
|
|
31615
|
+
aclCache.set(key, r);
|
|
31616
|
+
return r;
|
|
31617
|
+
};
|
|
31618
|
+
const agentsNeedingKey = new Map;
|
|
31619
|
+
for (const r of reqs) {
|
|
31620
|
+
for (const key of r.keys) {
|
|
31621
|
+
let set = agentsNeedingKey.get(key);
|
|
31622
|
+
if (!set) {
|
|
31623
|
+
set = new Set;
|
|
31624
|
+
agentsNeedingKey.set(key, set);
|
|
31625
|
+
}
|
|
31626
|
+
set.add(r.agent);
|
|
31627
|
+
}
|
|
31628
|
+
}
|
|
31629
|
+
const results = [];
|
|
31630
|
+
for (const r of reqs) {
|
|
31631
|
+
for (const key of r.keys) {
|
|
31632
|
+
const acl = await readKey(key);
|
|
31633
|
+
const name = `mcp-conn:${r.agent}:${r.server}:${key}`;
|
|
31634
|
+
const want = [...agentsNeedingKey.get(key) ?? new Set].sort();
|
|
31635
|
+
if (acl.kind === "unreachable") {
|
|
31636
|
+
results.push({
|
|
31637
|
+
name,
|
|
31638
|
+
status: "warn",
|
|
31639
|
+
detail: `vault-broker unreachable, can't verify '${key}' for MCP '${r.server}': ${acl.msg}`,
|
|
31640
|
+
fix: "Ensure the vault-broker is running and the operator socket is reachable, then re-run doctor."
|
|
31641
|
+
});
|
|
31642
|
+
continue;
|
|
31643
|
+
}
|
|
31644
|
+
if (acl.kind === "not_found") {
|
|
31645
|
+
results.push({
|
|
31646
|
+
name,
|
|
31647
|
+
status: "fail",
|
|
31648
|
+
detail: `agent '${r.agent}' loads MCP '${r.server}' which needs vault key '${key}', but the key is missing \u2014 '${r.server}' is configured but NOT authed and will fail at runtime`,
|
|
31649
|
+
fix: `switchroom vault set ${key} --allow ${want.join(",")} (then provide the value), or remove '${r.server}' from this agent's mcp_servers.`
|
|
31650
|
+
});
|
|
31651
|
+
continue;
|
|
31652
|
+
}
|
|
31653
|
+
if (!acl.allow.includes(r.agent)) {
|
|
31654
|
+
const updated = [...new Set([...acl.allow, r.agent])].sort().join(",");
|
|
31655
|
+
results.push({
|
|
31656
|
+
name,
|
|
31657
|
+
status: "fail",
|
|
31658
|
+
detail: `agent '${r.agent}' loads MCP '${r.server}' but is NOT on the vault ACL for '${key}' \u2014 the broker will deny it at runtime`,
|
|
31659
|
+
fix: `switchroom vault set ${key} --allow ${updated} (vault set overwrites the scope \u2014 re-state the full list including '${r.agent}').`
|
|
31660
|
+
});
|
|
31661
|
+
continue;
|
|
31662
|
+
}
|
|
31663
|
+
results.push({
|
|
31664
|
+
name,
|
|
31665
|
+
status: "ok",
|
|
31666
|
+
detail: `MCP '${r.server}' \u2192 '${key}' present + ACL-allowed`
|
|
31667
|
+
});
|
|
31668
|
+
}
|
|
31669
|
+
}
|
|
31670
|
+
return results;
|
|
31671
|
+
}
|
|
31672
|
+
|
|
31573
31673
|
// src/cli/doctor-credentials-migration.ts
|
|
31574
31674
|
import {
|
|
31575
31675
|
existsSync as realExistsSync5,
|
|
@@ -33909,6 +34009,20 @@ function registerDoctorCommand(program3) {
|
|
|
33909
34009
|
console.error(`Unknown skill: ${opts.skill}. Supported: mff`);
|
|
33910
34010
|
process.exit(1);
|
|
33911
34011
|
}
|
|
34012
|
+
const vaultAclReader = async (key) => {
|
|
34013
|
+
try {
|
|
34014
|
+
const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
34015
|
+
const result = await getViaBrokerStructured2(key);
|
|
34016
|
+
if (result.kind === "ok") {
|
|
34017
|
+
return { kind: "ok", allow: result.entry.scope?.allow ?? [] };
|
|
34018
|
+
}
|
|
34019
|
+
if (result.kind === "not_found")
|
|
34020
|
+
return { kind: "not_found" };
|
|
34021
|
+
return { kind: "unreachable", msg: result.msg };
|
|
34022
|
+
} catch (err) {
|
|
34023
|
+
return { kind: "unreachable", msg: err.message };
|
|
34024
|
+
}
|
|
34025
|
+
};
|
|
33912
34026
|
const sections = [
|
|
33913
34027
|
{ title: "Dependencies", results: checkDependencies() },
|
|
33914
34028
|
{ title: "Skills Prerequisites", results: checkSkillsPrerequisites() },
|
|
@@ -33950,25 +34064,11 @@ function registerDoctorCommand(program3) {
|
|
|
33950
34064
|
},
|
|
33951
34065
|
{
|
|
33952
34066
|
title: "Notion (RFC notion-integration)",
|
|
33953
|
-
results: await runNotionChecks(config, {
|
|
33954
|
-
|
|
33955
|
-
|
|
33956
|
-
|
|
33957
|
-
|
|
33958
|
-
if (result.kind === "ok") {
|
|
33959
|
-
return {
|
|
33960
|
-
kind: "ok",
|
|
33961
|
-
allow: result.entry.scope?.allow ?? []
|
|
33962
|
-
};
|
|
33963
|
-
}
|
|
33964
|
-
if (result.kind === "not_found")
|
|
33965
|
-
return { kind: "not_found" };
|
|
33966
|
-
return { kind: "unreachable", msg: result.msg };
|
|
33967
|
-
} catch (err) {
|
|
33968
|
-
return { kind: "unreachable", msg: err.message };
|
|
33969
|
-
}
|
|
33970
|
-
}
|
|
33971
|
-
})
|
|
34067
|
+
results: await runNotionChecks(config, { vaultAclReader })
|
|
34068
|
+
},
|
|
34069
|
+
{
|
|
34070
|
+
title: "MCP Connections (auth)",
|
|
34071
|
+
results: await runMcpSecretChecks(config, { vaultAclReader })
|
|
33972
34072
|
},
|
|
33973
34073
|
{ title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) },
|
|
33974
34074
|
{ title: "Webkite", results: runWebkiteChecks(config) },
|
|
@@ -50592,8 +50692,8 @@ var {
|
|
|
50592
50692
|
} = import__.default;
|
|
50593
50693
|
|
|
50594
50694
|
// src/build-info.ts
|
|
50595
|
-
var VERSION = "0.15.
|
|
50596
|
-
var COMMIT_SHA = "
|
|
50695
|
+
var VERSION = "0.15.38";
|
|
50696
|
+
var COMMIT_SHA = "d28a331f";
|
|
50597
50697
|
|
|
50598
50698
|
// src/cli/agent.ts
|
|
50599
50699
|
init_source();
|
|
@@ -53134,6 +53234,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
53134
53234
|
const switchroomCliPath = "/usr/local/bin/switchroom";
|
|
53135
53235
|
const resolvedConfigPath = DOCKER_CONFIG_PATH;
|
|
53136
53236
|
const telegramStateDir = `${DOCKER_AGENT_HOME}/.switchroom/agents/${name}/telegram`;
|
|
53237
|
+
const linearAgentEnabled = agentConfig.channels?.telegram?.linear_agent?.enabled === true;
|
|
53137
53238
|
const mcpServers = {
|
|
53138
53239
|
"switchroom-telegram": {
|
|
53139
53240
|
command: "bun",
|
|
@@ -53141,9 +53242,10 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
53141
53242
|
env: {
|
|
53142
53243
|
TELEGRAM_STATE_DIR: telegramStateDir,
|
|
53143
53244
|
SWITCHROOM_CONFIG: resolvedConfigPath,
|
|
53144
|
-
SWITCHROOM_CLI_PATH: switchroomCliPath
|
|
53245
|
+
SWITCHROOM_CLI_PATH: switchroomCliPath,
|
|
53246
|
+
...linearAgentEnabled ? { SWITCHROOM_TELEGRAM_LINEAR: "1" } : {}
|
|
53145
53247
|
},
|
|
53146
|
-
alwaysLoad:
|
|
53248
|
+
alwaysLoad: false
|
|
53147
53249
|
},
|
|
53148
53250
|
"agent-config": {
|
|
53149
53251
|
command: switchroomCliPath,
|
|
@@ -54203,6 +54305,7 @@ ${body}
|
|
|
54203
54305
|
const switchroomCliPath = "/usr/local/bin/switchroom";
|
|
54204
54306
|
const resolvedConfigPath = DOCKER_CONFIG_PATH;
|
|
54205
54307
|
const telegramStateDir = `${DOCKER_AGENT_HOME}/.switchroom/agents/${name}/telegram`;
|
|
54308
|
+
const linearAgentEnabled = agentConfig.channels?.telegram?.linear_agent?.enabled === true;
|
|
54206
54309
|
const mcpServers = {
|
|
54207
54310
|
"switchroom-telegram": {
|
|
54208
54311
|
command: "bun",
|
|
@@ -54210,9 +54313,10 @@ ${body}
|
|
|
54210
54313
|
env: {
|
|
54211
54314
|
TELEGRAM_STATE_DIR: telegramStateDir,
|
|
54212
54315
|
SWITCHROOM_CONFIG: resolvedConfigPath,
|
|
54213
|
-
SWITCHROOM_CLI_PATH: switchroomCliPath
|
|
54316
|
+
SWITCHROOM_CLI_PATH: switchroomCliPath,
|
|
54317
|
+
...linearAgentEnabled ? { SWITCHROOM_TELEGRAM_LINEAR: "1" } : {}
|
|
54214
54318
|
},
|
|
54215
|
-
alwaysLoad:
|
|
54319
|
+
alwaysLoad: false
|
|
54216
54320
|
},
|
|
54217
54321
|
"agent-config": {
|
|
54218
54322
|
command: switchroomCliPath,
|
|
@@ -58912,7 +59016,7 @@ function hasGoogleAccountEntry(doc, account) {
|
|
|
58912
59016
|
// src/cli/auth-google.ts
|
|
58913
59017
|
init_helpers();
|
|
58914
59018
|
function registerAuthGoogleSubcommands(program3, authParent) {
|
|
58915
|
-
const google = authParent.command("google").description("Manage Google Workspace accounts shared across agents (RFC G \u2014 see
|
|
59019
|
+
const google = authParent.command("google").description("Manage Google Workspace accounts shared across agents (RFC G \u2014 see reference/rfcs/google-workspace-generalization.md)");
|
|
58916
59020
|
registerConnect(google, program3);
|
|
58917
59021
|
registerEnable(google, program3);
|
|
58918
59022
|
registerDisable(google, program3);
|
|
@@ -59696,7 +59800,7 @@ function buildMicrosoftCredentials(opts) {
|
|
|
59696
59800
|
|
|
59697
59801
|
// src/cli/auth-microsoft.ts
|
|
59698
59802
|
function registerAuthMicrosoftSubcommands(program3, authParent) {
|
|
59699
|
-
const microsoft = authParent.command("microsoft").description("Manage Microsoft 365 accounts shared across agents (RFC #1873 \u2014 see
|
|
59803
|
+
const microsoft = authParent.command("microsoft").description("Manage Microsoft 365 accounts shared across agents (RFC #1873 \u2014 see reference/rfcs/microsoft-workspace.md)");
|
|
59700
59804
|
registerEnable2(microsoft, program3);
|
|
59701
59805
|
registerDisable2(microsoft, program3);
|
|
59702
59806
|
registerList2(microsoft, program3);
|
|
@@ -68263,9 +68367,22 @@ async function performLinearRefresh(io) {
|
|
|
68263
68367
|
function bundleKeyFor(agent) {
|
|
68264
68368
|
return `linear/${agent}/oauth`;
|
|
68265
68369
|
}
|
|
68370
|
+
function linearSandboxRefusal(verb, runtime = process.env.SWITCHROOM_RUNTIME) {
|
|
68371
|
+
if (runtime !== "docker")
|
|
68372
|
+
return null;
|
|
68373
|
+
return `'linear-agent ${verb}' is a HOST command \u2014 it writes the vault file directly, which ` + `doesn't work inside an agent container (no mounted vault, no passphrase) and would ` + `silently no-op.
|
|
68374
|
+
\u2022 In-container: use the 'linear_agent_setup' MCP tool (operator-approved).
|
|
68375
|
+
` + ` \u2022 On the host shell: run this same command there.`;
|
|
68376
|
+
}
|
|
68377
|
+
function refuseInSandbox(verb) {
|
|
68378
|
+
const msg = linearSandboxRefusal(verb);
|
|
68379
|
+
if (msg)
|
|
68380
|
+
fail2(msg);
|
|
68381
|
+
}
|
|
68266
68382
|
function registerLinearAgentCommand(program3) {
|
|
68267
68383
|
const linear = program3.command("linear-agent").description("Install an agent into a Linear workspace as a first-class app actor (#2298) \u2014 @-mentionable, delegate-assignable, agent sessions wake it instantly.");
|
|
68268
68384
|
linear.command("setup").description("Provision <agent> as a Linear agent. Vault-stores the Linear OAuth app token (actor=app) under 'linear/<agent>/token' and enables the linear_agent block in switchroom.yaml. The OAuth browser authorize step is printed as instructions (it can't run headless); pass the already-obtained --token.").requiredOption("--agent <name>", "Agent name (must exist in switchroom.yaml)").requiredOption("--token <token>", "The Linear OAuth app token (actor=app), obtained out-of-band via the browser authorize step. Stored in the vault, never in switchroom.yaml.").option("--client-id <id>", "Linear OAuth app client id. Stored (with --client-secret + --refresh-token) to enable unattended token refresh.").option("--client-secret <secret>", "Linear OAuth app client secret. Stored in the vault (with --client-id + --refresh-token) so the token can be refreshed without a browser re-auth.").option("--refresh-token <token>", "The refresh_token from the OAuth exchange. Stored so an expired access token self-heals (see 'linear-agent refresh').").option("--token-expires-in <seconds>", "expires_in from the OAuth token response (seconds). Records when the access token expires so refresh runs proactively. Defaults to 86400.").option("--redirect-uri <uri>", "OAuth redirect URI registered on the Linear app (for the authorize-URL hint).").option("--workspace-id <id>", "Optional Linear workspace (organization) id to record in config.").option("--webhook-base <url>", "Base URL of the switchroom web server (e.g. https://hooks.switchroom.ai). Used to print the webhook URL to register in Linear. Defaults to a placeholder.").option("--dry-run", "Print the YAML diff + instructions without writing or vaulting anything").action(withConfigError(async (opts) => {
|
|
68385
|
+
refuseInSandbox("setup");
|
|
68269
68386
|
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(opts.agent)) {
|
|
68270
68387
|
fail2(`--agent must be a lowercase agent slug (got '${opts.agent}').`);
|
|
68271
68388
|
}
|
|
@@ -68328,6 +68445,7 @@ function registerLinearAgentCommand(program3) {
|
|
|
68328
68445
|
printLinearInstructions(opts, vaultKey);
|
|
68329
68446
|
}));
|
|
68330
68447
|
linear.command("refresh").description("Refresh <agent>'s Linear app token using the stored refresh bundle (linear/<agent>/oauth). Exchanges the refresh_token for a fresh access token, writes it to linear/<agent>/token, and rotates the stored refresh_token + expiry. Use to recover an expired token or seed automation. Host-side write \u2014 the running agent picks it up on its next broker re-mirror / restart.").requiredOption("--agent <name>", "Agent name (must have a linear_agent block)").action(withConfigError(async (opts) => {
|
|
68448
|
+
refuseInSandbox("refresh");
|
|
68331
68449
|
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(opts.agent)) {
|
|
68332
68450
|
fail2(`--agent must be a lowercase agent slug (got '${opts.agent}').`);
|
|
68333
68451
|
}
|
|
@@ -74792,6 +74910,28 @@ function handleGetNotionWorkspace(config) {
|
|
|
74792
74910
|
fullAccessAgents
|
|
74793
74911
|
};
|
|
74794
74912
|
}
|
|
74913
|
+
function handleGetLinearAgents(config) {
|
|
74914
|
+
const agentNames = Object.keys(config.agents ?? {});
|
|
74915
|
+
const agents = [];
|
|
74916
|
+
for (const name of agentNames) {
|
|
74917
|
+
const rawAgent = config.agents?.[name];
|
|
74918
|
+
if (!rawAgent)
|
|
74919
|
+
continue;
|
|
74920
|
+
const linear = resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.linear_agent;
|
|
74921
|
+
if (!linear?.enabled)
|
|
74922
|
+
continue;
|
|
74923
|
+
const tok = linear.token;
|
|
74924
|
+
const tokenVaultKey = typeof tok === "string" && tok.startsWith("vault:") ? tok : null;
|
|
74925
|
+
agents.push({
|
|
74926
|
+
agent: name,
|
|
74927
|
+
workspaceId: linear.workspace_id ?? null,
|
|
74928
|
+
defaultTeamId: linear.default_team_id ?? null,
|
|
74929
|
+
tokenVaultKey
|
|
74930
|
+
});
|
|
74931
|
+
}
|
|
74932
|
+
agents.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
74933
|
+
return { configured: agents.length > 0, agents };
|
|
74934
|
+
}
|
|
74795
74935
|
var connectionAccessStatuses = new Map;
|
|
74796
74936
|
function reapConnectionAccessStatuses(now = Date.now()) {
|
|
74797
74937
|
for (const [id, s] of connectionAccessStatuses) {
|
|
@@ -75863,6 +76003,23 @@ var MIME_TYPES = {
|
|
|
75863
76003
|
".svg": "image/svg+xml",
|
|
75864
76004
|
".ico": "image/x-icon"
|
|
75865
76005
|
};
|
|
76006
|
+
var SPA_TAB_ROUTES = new Set([
|
|
76007
|
+
"summary",
|
|
76008
|
+
"agents",
|
|
76009
|
+
"accounts",
|
|
76010
|
+
"system",
|
|
76011
|
+
"memory",
|
|
76012
|
+
"connections",
|
|
76013
|
+
"schedule",
|
|
76014
|
+
"approvals"
|
|
76015
|
+
]);
|
|
76016
|
+
function resolveDashboardFilePath(pathname) {
|
|
76017
|
+
const routeName = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
76018
|
+
return pathname === "/" || SPA_TAB_ROUTES.has(routeName) ? "/index.html" : pathname;
|
|
76019
|
+
}
|
|
76020
|
+
function dashboardCacheControl(ext) {
|
|
76021
|
+
return ext === ".html" ? "no-cache, must-revalidate" : undefined;
|
|
76022
|
+
}
|
|
75866
76023
|
function jsonResponse(data, status = 200) {
|
|
75867
76024
|
return new Response(JSON.stringify(data), {
|
|
75868
76025
|
status,
|
|
@@ -76100,6 +76257,9 @@ function parseRoute(pathname, method) {
|
|
|
76100
76257
|
if (method === "GET" && pathname === "/api/notion-workspace") {
|
|
76101
76258
|
return { handler: "getNotionWorkspace", params: {} };
|
|
76102
76259
|
}
|
|
76260
|
+
if (method === "GET" && pathname === "/api/linear-agents") {
|
|
76261
|
+
return { handler: "getLinearAgents", params: {} };
|
|
76262
|
+
}
|
|
76103
76263
|
if (method === "GET" && pathname === "/api/schedule") {
|
|
76104
76264
|
return { handler: "getSchedule", params: {} };
|
|
76105
76265
|
}
|
|
@@ -76191,8 +76351,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
76191
76351
|
return new Response("Unauthorized", { status: 401 });
|
|
76192
76352
|
}
|
|
76193
76353
|
const wsProto = req.headers.get("Sec-WebSocket-Protocol");
|
|
76194
|
-
const
|
|
76195
|
-
const upgraded = server2.upgrade(req,
|
|
76354
|
+
const headers2 = wsProto && wsProto.split(",").map((s) => s.trim()).includes("bearer") ? { "Sec-WebSocket-Protocol": "bearer" } : undefined;
|
|
76355
|
+
const upgraded = server2.upgrade(req, headers2 ? { headers: headers2 } : undefined);
|
|
76196
76356
|
if (!upgraded) {
|
|
76197
76357
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
76198
76358
|
}
|
|
@@ -76280,6 +76440,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
76280
76440
|
return (async () => jsonResponse(await handleGetMicrosoftAccounts(freshConfig())))();
|
|
76281
76441
|
case "getNotionWorkspace":
|
|
76282
76442
|
return jsonResponse(handleGetNotionWorkspace(freshConfig()));
|
|
76443
|
+
case "getLinearAgents":
|
|
76444
|
+
return jsonResponse(handleGetLinearAgents(freshConfig()));
|
|
76283
76445
|
case "getSchedule":
|
|
76284
76446
|
return (async () => jsonResponse(withStamp(await cachedSchedule())))();
|
|
76285
76447
|
case "getApprovals":
|
|
@@ -76414,7 +76576,7 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
76414
76576
|
}
|
|
76415
76577
|
}
|
|
76416
76578
|
}
|
|
76417
|
-
|
|
76579
|
+
const filePath = resolveDashboardFilePath(pathname);
|
|
76418
76580
|
const fullPath = join47(uiDir, filePath);
|
|
76419
76581
|
if (!existsSync50(fullPath)) {
|
|
76420
76582
|
return new Response("Not Found", { status: 404 });
|
|
@@ -76432,9 +76594,11 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
|
|
|
76432
76594
|
const ext = extname(realFullPath);
|
|
76433
76595
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
76434
76596
|
const content = readFileSync46(realFullPath);
|
|
76435
|
-
|
|
76436
|
-
|
|
76437
|
-
|
|
76597
|
+
const headers = { "Content-Type": contentType };
|
|
76598
|
+
const cacheControl = dashboardCacheControl(ext);
|
|
76599
|
+
if (cacheControl)
|
|
76600
|
+
headers["Cache-Control"] = cacheControl;
|
|
76601
|
+
return new Response(content, { headers });
|
|
76438
76602
|
},
|
|
76439
76603
|
websocket: {
|
|
76440
76604
|
open(_ws) {},
|
|
@@ -82005,7 +82169,7 @@ async function runNotionMcpLauncher(opts, runtime) {
|
|
|
82005
82169
|
return exitCode;
|
|
82006
82170
|
}
|
|
82007
82171
|
function registerNotionMcpLauncherCommand(program3) {
|
|
82008
|
-
program3.command("notion-mcp-launcher", { hidden: true }).option("--vault-key <key>", "Override the vault key holding the Notion integration token. " + "Defaults to `notion/integration-token`.", DEFAULT_VAULT_KEY).option("--mcp-version <semver>", "Override the @notionhq/notion-mcp-server version to spawn.").option("--heartbeat-path <path>", "Override the heartbeat file path. Default: /state/agent/notion-launcher.heartbeat.json").description("Internal \u2014 Notion MCP launcher. Fetches the integration token from the vault-broker and execs @notionhq/notion-mcp-server in stdio mode. RFC
|
|
82172
|
+
program3.command("notion-mcp-launcher", { hidden: true }).option("--vault-key <key>", "Override the vault key holding the Notion integration token. " + "Defaults to `notion/integration-token`.", DEFAULT_VAULT_KEY).option("--mcp-version <semver>", "Override the @notionhq/notion-mcp-server version to spawn.").option("--heartbeat-path <path>", "Override the heartbeat file path. Default: /state/agent/notion-launcher.heartbeat.json").description("Internal \u2014 Notion MCP launcher. Fetches the integration token from the vault-broker and execs @notionhq/notion-mcp-server in stdio mode. RFC reference/rfcs/notion-integration.md PR 2.").action(async (opts) => {
|
|
82009
82173
|
const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
82010
82174
|
const code = await runNotionMcpLauncher(opts, {
|
|
82011
82175
|
fetchToken: async () => {
|
|
@@ -82478,7 +82642,7 @@ function createNotionApiClient(opts) {
|
|
|
82478
82642
|
// src/cli/notion.ts
|
|
82479
82643
|
init_client();
|
|
82480
82644
|
function registerNotionCommand(program3) {
|
|
82481
|
-
const cmd = program3.command("notion").description("Notion integration operator helpers (list-dbs, test). See
|
|
82645
|
+
const cmd = program3.command("notion").description("Notion integration operator helpers (list-dbs, test). See reference/rfcs/notion-integration.md.");
|
|
82482
82646
|
cmd.command("list-dbs").description("List the databases the Notion integration can access. Output is a ready-to-paste YAML block for `notion_workspace.databases`.").option("--vault-key <key>", "Override the vault key holding the integration token.", "notion/integration-token").action(async (opts) => {
|
|
82483
82647
|
const code = await runListDbs(opts);
|
|
82484
82648
|
process.exit(code);
|
|
@@ -82668,7 +82832,7 @@ async function fetchToken(vaultKey) {
|
|
|
82668
82832
|
|
|
82669
82833
|
// src/cli/apply.ts
|
|
82670
82834
|
init_source();
|
|
82671
|
-
import { accessSync as accessSync3, chownSync as chownSync7, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync74, mkdirSync as
|
|
82835
|
+
import { accessSync as accessSync3, chownSync as chownSync7, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync74, mkdirSync as mkdirSync43, readFileSync as readFileSync63, readdirSync as readdirSync26, renameSync as renameSync14, writeFileSync as writeFileSync38 } from "node:fs";
|
|
82672
82836
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
82673
82837
|
import { spawnSync as childSpawnSync } from "node:child_process";
|
|
82674
82838
|
import readline from "node:readline";
|
|
@@ -83057,16 +83221,97 @@ agents:
|
|
|
83057
83221
|
|
|
83058
83222
|
// src/cli/apply.ts
|
|
83059
83223
|
init_resolver();
|
|
83060
|
-
import { dirname as dirname23, join as
|
|
83224
|
+
import { dirname as dirname23, join as join76, resolve as resolve46 } from "node:path";
|
|
83061
83225
|
import { homedir as homedir44 } from "node:os";
|
|
83062
83226
|
import { execFileSync as execFileSync24 } from "node:child_process";
|
|
83063
83227
|
init_vault();
|
|
83064
83228
|
init_loader();
|
|
83065
83229
|
init_loader();
|
|
83066
83230
|
|
|
83067
|
-
// src/
|
|
83068
|
-
import {
|
|
83231
|
+
// src/agents/connection-health.ts
|
|
83232
|
+
import { mkdirSync as mkdirSync41, writeFileSync as writeFileSync36 } from "node:fs";
|
|
83069
83233
|
import { join as join73 } from "node:path";
|
|
83234
|
+
var CONNECTION_HEALTH_FILENAME = "connection-health.json";
|
|
83235
|
+
async function computeAgentConnectionIssues(config, agentName, vaultAclReader) {
|
|
83236
|
+
const reqs = computeMcpSecretRequirements(config).filter((r) => r.agent === agentName);
|
|
83237
|
+
if (reqs.length === 0)
|
|
83238
|
+
return [];
|
|
83239
|
+
const agentsNeedingKey = new Map;
|
|
83240
|
+
for (const r of computeMcpSecretRequirements(config)) {
|
|
83241
|
+
for (const key of r.keys) {
|
|
83242
|
+
let set = agentsNeedingKey.get(key);
|
|
83243
|
+
if (!set) {
|
|
83244
|
+
set = new Set;
|
|
83245
|
+
agentsNeedingKey.set(key, set);
|
|
83246
|
+
}
|
|
83247
|
+
set.add(r.agent);
|
|
83248
|
+
}
|
|
83249
|
+
}
|
|
83250
|
+
const aclCache = new Map;
|
|
83251
|
+
const readKey = async (key) => {
|
|
83252
|
+
const cached = aclCache.get(key);
|
|
83253
|
+
if (cached)
|
|
83254
|
+
return cached;
|
|
83255
|
+
const r = await vaultAclReader(key);
|
|
83256
|
+
aclCache.set(key, r);
|
|
83257
|
+
return r;
|
|
83258
|
+
};
|
|
83259
|
+
const issues = [];
|
|
83260
|
+
for (const r of reqs) {
|
|
83261
|
+
for (const key of r.keys) {
|
|
83262
|
+
const acl = await readKey(key);
|
|
83263
|
+
const want = [...agentsNeedingKey.get(key) ?? new Set].sort();
|
|
83264
|
+
if (acl.kind === "unreachable")
|
|
83265
|
+
continue;
|
|
83266
|
+
if (acl.kind === "not_found") {
|
|
83267
|
+
issues.push({
|
|
83268
|
+
server: r.server,
|
|
83269
|
+
key,
|
|
83270
|
+
kind: "missing",
|
|
83271
|
+
detail: `MCP '${r.server}' needs vault key '${key}', but it is missing \u2014 configured but not authed`,
|
|
83272
|
+
fix: `switchroom vault set ${key} --allow ${want.join(",")} (then provide the value)`
|
|
83273
|
+
});
|
|
83274
|
+
continue;
|
|
83275
|
+
}
|
|
83276
|
+
if (!acl.allow.includes(agentName)) {
|
|
83277
|
+
const updated = [...new Set([...acl.allow, agentName])].sort().join(",");
|
|
83278
|
+
issues.push({
|
|
83279
|
+
server: r.server,
|
|
83280
|
+
key,
|
|
83281
|
+
kind: "acl",
|
|
83282
|
+
detail: `MCP '${r.server}' not on the vault ACL for '${key}' \u2014 broker will deny at runtime`,
|
|
83283
|
+
fix: `switchroom vault set ${key} --allow ${updated} (re-state the full list)`
|
|
83284
|
+
});
|
|
83285
|
+
}
|
|
83286
|
+
}
|
|
83287
|
+
}
|
|
83288
|
+
return issues;
|
|
83289
|
+
}
|
|
83290
|
+
function writeConnectionHealthFile(agentDir, health, deps) {
|
|
83291
|
+
const dir = join73(agentDir, ".claude");
|
|
83292
|
+
const path7 = join73(dir, CONNECTION_HEALTH_FILENAME);
|
|
83293
|
+
(deps?.mkdir ?? ((p, o) => mkdirSync41(p, o)))(dir, { recursive: true });
|
|
83294
|
+
(deps?.writeFile ?? ((p, d) => writeFileSync36(p, d)))(path7, JSON.stringify(health, null, 2) + `
|
|
83295
|
+
`);
|
|
83296
|
+
}
|
|
83297
|
+
async function refreshAgentConnectionHealth(config, agentName, agentDir, deps) {
|
|
83298
|
+
const now = deps.now ?? Date.now;
|
|
83299
|
+
let issues = [];
|
|
83300
|
+
try {
|
|
83301
|
+
issues = await computeAgentConnectionIssues(config, agentName, deps.vaultAclReader);
|
|
83302
|
+
} catch {
|
|
83303
|
+
issues = [];
|
|
83304
|
+
}
|
|
83305
|
+
const health = { computedAt: now(), issues };
|
|
83306
|
+
try {
|
|
83307
|
+
writeConnectionHealthFile(agentDir, health, deps);
|
|
83308
|
+
} catch {}
|
|
83309
|
+
return health;
|
|
83310
|
+
}
|
|
83311
|
+
|
|
83312
|
+
// src/cli/update-prompt-hook.ts
|
|
83313
|
+
import { existsSync as existsSync72, readFileSync as readFileSync62, writeFileSync as writeFileSync37, chmodSync as chmodSync10, mkdirSync as mkdirSync42 } from "node:fs";
|
|
83314
|
+
import { join as join74 } from "node:path";
|
|
83070
83315
|
var HOOK_FILENAME = "update-card-on-prompt.sh";
|
|
83071
83316
|
function updatePromptHookScript() {
|
|
83072
83317
|
return `#!/bin/bash
|
|
@@ -83132,14 +83377,14 @@ exit 0
|
|
|
83132
83377
|
`;
|
|
83133
83378
|
}
|
|
83134
83379
|
function installUpdatePromptHook(agentDir) {
|
|
83135
|
-
const hooksDir =
|
|
83136
|
-
|
|
83137
|
-
const scriptPath =
|
|
83380
|
+
const hooksDir = join74(agentDir, ".claude", "hooks");
|
|
83381
|
+
mkdirSync42(hooksDir, { recursive: true });
|
|
83382
|
+
const scriptPath = join74(hooksDir, HOOK_FILENAME);
|
|
83138
83383
|
const desired = updatePromptHookScript();
|
|
83139
83384
|
let installed = false;
|
|
83140
83385
|
const existing = existsSync72(scriptPath) ? readFileSync62(scriptPath, "utf-8") : "";
|
|
83141
83386
|
if (existing !== desired) {
|
|
83142
|
-
|
|
83387
|
+
writeFileSync37(scriptPath, desired, { mode: 493 });
|
|
83143
83388
|
chmodSync10(scriptPath, 493);
|
|
83144
83389
|
installed = true;
|
|
83145
83390
|
} else {
|
|
@@ -83147,7 +83392,7 @@ function installUpdatePromptHook(agentDir) {
|
|
|
83147
83392
|
chmodSync10(scriptPath, 493);
|
|
83148
83393
|
} catch {}
|
|
83149
83394
|
}
|
|
83150
|
-
const settingsPath =
|
|
83395
|
+
const settingsPath = join74(agentDir, ".claude", "settings.json");
|
|
83151
83396
|
if (!existsSync72(settingsPath)) {
|
|
83152
83397
|
return { scriptPath, settingsPath, installed };
|
|
83153
83398
|
}
|
|
@@ -83184,7 +83429,7 @@ function installUpdatePromptHook(agentDir) {
|
|
|
83184
83429
|
});
|
|
83185
83430
|
hooks.UserPromptSubmit = list2;
|
|
83186
83431
|
parsed.hooks = hooks;
|
|
83187
|
-
|
|
83432
|
+
writeFileSync37(settingsPath, JSON.stringify(parsed, null, 2) + `
|
|
83188
83433
|
`, { mode: 384 });
|
|
83189
83434
|
installed = true;
|
|
83190
83435
|
}
|
|
@@ -83249,14 +83494,14 @@ var EMBEDDED_EXAMPLES = {
|
|
|
83249
83494
|
switchroom: switchroom_default,
|
|
83250
83495
|
minimal: minimal_default
|
|
83251
83496
|
};
|
|
83252
|
-
var DEFAULT_COMPOSE_PATH2 =
|
|
83497
|
+
var DEFAULT_COMPOSE_PATH2 = join76(homedir44(), ".switchroom", "compose", "docker-compose.yml");
|
|
83253
83498
|
var COMPOSE_PROJECT2 = "switchroom";
|
|
83254
83499
|
function resolveVaultBindMountDir(homeDir, ctx) {
|
|
83255
83500
|
const isCustomPath = ctx.migrationKind === "custom-path-skipped";
|
|
83256
83501
|
if (isCustomPath && ctx.customVaultPath) {
|
|
83257
83502
|
return dirname23(ctx.customVaultPath);
|
|
83258
83503
|
}
|
|
83259
|
-
return
|
|
83504
|
+
return join76(homeDir, ".switchroom", "vault");
|
|
83260
83505
|
}
|
|
83261
83506
|
function inspectVaultBindMountDir(vaultDir) {
|
|
83262
83507
|
if (!existsSync74(vaultDir))
|
|
@@ -83287,61 +83532,61 @@ function hasVaultRefs(value) {
|
|
|
83287
83532
|
async function ensureHostMountSources(config) {
|
|
83288
83533
|
const home2 = homedir44();
|
|
83289
83534
|
const dirs = [
|
|
83290
|
-
|
|
83291
|
-
|
|
83292
|
-
|
|
83293
|
-
|
|
83294
|
-
|
|
83535
|
+
join76(home2, ".switchroom", "approvals"),
|
|
83536
|
+
join76(home2, ".switchroom", "scheduler"),
|
|
83537
|
+
join76(home2, ".switchroom", "logs"),
|
|
83538
|
+
join76(home2, ".switchroom", "compose"),
|
|
83539
|
+
join76(home2, ".switchroom", "broker-operator")
|
|
83295
83540
|
];
|
|
83296
83541
|
for (const name of Object.keys(config.agents)) {
|
|
83297
|
-
dirs.push(
|
|
83298
|
-
dirs.push(
|
|
83299
|
-
dirs.push(
|
|
83300
|
-
dirs.push(
|
|
83301
|
-
if (existsSync74(
|
|
83302
|
-
dirs.push(
|
|
83542
|
+
dirs.push(join76(home2, ".switchroom", "agents", name));
|
|
83543
|
+
dirs.push(join76(home2, ".switchroom", "logs", name));
|
|
83544
|
+
dirs.push(join76(home2, ".claude", "projects", name));
|
|
83545
|
+
dirs.push(join76(home2, ".switchroom", "audit", name));
|
|
83546
|
+
if (existsSync74(join76(home2, ".switchroom-config"))) {
|
|
83547
|
+
dirs.push(join76(home2, ".switchroom-config", "agents", name, "personal-skills"));
|
|
83303
83548
|
}
|
|
83304
83549
|
}
|
|
83305
83550
|
for (const dir of dirs) {
|
|
83306
83551
|
await mkdir2(dir, { recursive: true });
|
|
83307
83552
|
}
|
|
83308
|
-
const autoUnlockPath =
|
|
83553
|
+
const autoUnlockPath = join76(home2, ".switchroom", "vault-auto-unlock");
|
|
83309
83554
|
if (!existsSync74(autoUnlockPath)) {
|
|
83310
|
-
|
|
83555
|
+
writeFileSync38(autoUnlockPath, "", { mode: 384 });
|
|
83311
83556
|
}
|
|
83312
|
-
const auditLogPath =
|
|
83557
|
+
const auditLogPath = join76(home2, ".switchroom", "vault-audit.log");
|
|
83313
83558
|
if (!existsSync74(auditLogPath)) {
|
|
83314
|
-
|
|
83559
|
+
writeFileSync38(auditLogPath, "", { mode: 420 });
|
|
83315
83560
|
}
|
|
83316
|
-
const grantsDbPath =
|
|
83561
|
+
const grantsDbPath = join76(home2, ".switchroom", "vault-grants.db");
|
|
83317
83562
|
if (!existsSync74(grantsDbPath)) {
|
|
83318
|
-
|
|
83563
|
+
writeFileSync38(grantsDbPath, "", { mode: 384 });
|
|
83319
83564
|
}
|
|
83320
|
-
const hostdAuditLogPath =
|
|
83565
|
+
const hostdAuditLogPath = join76(home2, ".switchroom", "host-control-audit.log");
|
|
83321
83566
|
if (!existsSync74(hostdAuditLogPath)) {
|
|
83322
|
-
|
|
83567
|
+
writeFileSync38(hostdAuditLogPath, "", { mode: 420 });
|
|
83323
83568
|
}
|
|
83324
83569
|
for (const name of Object.keys(config.agents)) {
|
|
83325
|
-
const tokenPath =
|
|
83570
|
+
const tokenPath = join76(home2, ".switchroom", "agents", name, ".vault-token");
|
|
83326
83571
|
if (!existsSync74(tokenPath)) {
|
|
83327
|
-
|
|
83572
|
+
writeFileSync38(tokenPath, "", { mode: 384 });
|
|
83328
83573
|
}
|
|
83329
83574
|
try {
|
|
83330
83575
|
const uid = allocateAgentUid(name);
|
|
83331
83576
|
chownSync7(tokenPath, uid, uid);
|
|
83332
83577
|
} catch {}
|
|
83333
83578
|
}
|
|
83334
|
-
const fleetDir =
|
|
83579
|
+
const fleetDir = join76(home2, ".switchroom", "fleet");
|
|
83335
83580
|
await mkdir2(fleetDir, { recursive: true });
|
|
83336
|
-
const invariantsPath =
|
|
83581
|
+
const invariantsPath = join76(fleetDir, "switchroom-invariants.md");
|
|
83337
83582
|
const invariantsCanonical = renderFleetInvariants();
|
|
83338
83583
|
const invariantsCurrent = existsSync74(invariantsPath) ? readFileSync63(invariantsPath, "utf-8") : null;
|
|
83339
83584
|
if (invariantsCurrent !== invariantsCanonical) {
|
|
83340
|
-
|
|
83585
|
+
writeFileSync38(invariantsPath, invariantsCanonical, { mode: 420 });
|
|
83341
83586
|
}
|
|
83342
|
-
const fleetClaudePath =
|
|
83587
|
+
const fleetClaudePath = join76(fleetDir, "CLAUDE.md");
|
|
83343
83588
|
if (!existsSync74(fleetClaudePath)) {
|
|
83344
|
-
|
|
83589
|
+
writeFileSync38(fleetClaudePath, [
|
|
83345
83590
|
"# Switchroom fleet defaults",
|
|
83346
83591
|
"",
|
|
83347
83592
|
"Operator-owned fleet brain. Every agent reads this via",
|
|
@@ -83424,16 +83669,16 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
|
|
|
83424
83669
|
}
|
|
83425
83670
|
function writeInstallTypeCache(homeDir = homedir44()) {
|
|
83426
83671
|
const ctx = detectInstallType();
|
|
83427
|
-
const dir =
|
|
83428
|
-
const out =
|
|
83672
|
+
const dir = join76(homeDir, ".switchroom");
|
|
83673
|
+
const out = join76(dir, "install-type.json");
|
|
83429
83674
|
const tmp = `${out}.tmp`;
|
|
83430
|
-
|
|
83675
|
+
mkdirSync43(dir, { recursive: true });
|
|
83431
83676
|
const payload = {
|
|
83432
83677
|
install_type: ctx.install_type,
|
|
83433
83678
|
detected_at: new Date().toISOString(),
|
|
83434
83679
|
source_paths: ctx.source_paths
|
|
83435
83680
|
};
|
|
83436
|
-
|
|
83681
|
+
writeFileSync38(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
|
|
83437
83682
|
renameSync14(tmp, out);
|
|
83438
83683
|
return out;
|
|
83439
83684
|
}
|
|
@@ -83460,6 +83705,20 @@ Applying switchroom config...
|
|
|
83460
83705
|
writeOut(source_default.gray(` (--only=${options.only}: scaffolding/aligning this agent only; ` + `compose still covers all ${allAgentNames.length})
|
|
83461
83706
|
`));
|
|
83462
83707
|
}
|
|
83708
|
+
const connHealthVaultAclReader = async (key) => {
|
|
83709
|
+
try {
|
|
83710
|
+
const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
83711
|
+
const result = await getViaBrokerStructured2(key);
|
|
83712
|
+
if (result.kind === "ok") {
|
|
83713
|
+
return { kind: "ok", allow: result.entry.scope?.allow ?? [] };
|
|
83714
|
+
}
|
|
83715
|
+
if (result.kind === "not_found")
|
|
83716
|
+
return { kind: "not_found" };
|
|
83717
|
+
return { kind: "unreachable", msg: result.msg };
|
|
83718
|
+
} catch (err) {
|
|
83719
|
+
return { kind: "unreachable", msg: err.message };
|
|
83720
|
+
}
|
|
83721
|
+
};
|
|
83463
83722
|
let scaffolded = 0;
|
|
83464
83723
|
const failures = [];
|
|
83465
83724
|
|
|
@@ -83474,14 +83733,17 @@ Applying switchroom config...
|
|
|
83474
83733
|
writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
|
|
83475
83734
|
`));
|
|
83476
83735
|
try {
|
|
83477
|
-
installUpdatePromptHook(
|
|
83736
|
+
installUpdatePromptHook(join76(agentsDir, name));
|
|
83478
83737
|
} catch (hookErr) {
|
|
83479
83738
|
writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
|
|
83480
83739
|
`));
|
|
83481
83740
|
}
|
|
83741
|
+
await refreshAgentConnectionHealth(config, name, join76(agentsDir, name), {
|
|
83742
|
+
vaultAclReader: connHealthVaultAclReader
|
|
83743
|
+
});
|
|
83482
83744
|
try {
|
|
83483
83745
|
const uid = allocateAgentUid(name);
|
|
83484
|
-
alignAgentUid(name,
|
|
83746
|
+
alignAgentUid(name, join76(agentsDir, name), uid, {
|
|
83485
83747
|
confirm: !options.nonInteractive,
|
|
83486
83748
|
writeOut
|
|
83487
83749
|
});
|
|
@@ -83489,12 +83751,12 @@ Applying switchroom config...
|
|
|
83489
83751
|
const msg = alignErr.message;
|
|
83490
83752
|
if (options.allowUnaligned) {
|
|
83491
83753
|
writeOut(source_default.yellow(` ! could not chown ${name} state dir: ${msg}
|
|
83492
|
-
|
|
83754
|
+
continuing because --allow-unaligned was passed; agent may fail on first write.
|
|
83493
83755
|
`));
|
|
83494
83756
|
} else {
|
|
83495
83757
|
writeOut(source_default.red(` x could not chown ${name} state dir: ${msg}
|
|
83496
|
-
|
|
83497
|
-
|
|
83758
|
+
The bind-mounted state dir must be owned by the container's UID or the agent will fail on first write.
|
|
83759
|
+
Fix: run \`switchroom apply\` from a TTY so it can prompt for sudo, OR run the suggested chown manually, OR re-run with --allow-unaligned to skip this check.
|
|
83498
83760
|
`));
|
|
83499
83761
|
throw new UidAlignmentAbort(`UID alignment failed for agent ${name}; aborting apply (pass --allow-unaligned to override).`);
|
|
83500
83762
|
}
|
|
@@ -83518,14 +83780,14 @@ Applying switchroom config...
|
|
|
83518
83780
|
for (const name of agentNames) {
|
|
83519
83781
|
try {
|
|
83520
83782
|
const uid = allocateAgentUid(name);
|
|
83521
|
-
alignAgentUid(name,
|
|
83783
|
+
alignAgentUid(name, join76(agentsDir, name), uid, {
|
|
83522
83784
|
confirm: !options.nonInteractive,
|
|
83523
83785
|
writeOut
|
|
83524
83786
|
});
|
|
83525
83787
|
} catch (alignErr) {
|
|
83526
83788
|
const msg = alignErr.message;
|
|
83527
83789
|
writeOut(source_default.yellow(` ! post-mount-source UID re-align failed for ${name}: ${msg}
|
|
83528
|
-
|
|
83790
|
+
Agent may fail to write supervisor logs on first boot.
|
|
83529
83791
|
`));
|
|
83530
83792
|
}
|
|
83531
83793
|
}
|
|
@@ -83568,7 +83830,7 @@ Applying switchroom config...
|
|
|
83568
83830
|
];
|
|
83569
83831
|
if (!acceptable.includes(postMigrationInspect.kind)) {
|
|
83570
83832
|
writeErr(source_default.red(`Post-migration verification failed: state is ${postMigrationInspect.kind}
|
|
83571
|
-
|
|
83833
|
+
Expected one of: ${acceptable.join(", ")}
|
|
83572
83834
|
` + `This is a switchroom bug \u2014 please file an issue with the apply log.
|
|
83573
83835
|
`));
|
|
83574
83836
|
process.exit(5);
|
|
@@ -83583,11 +83845,11 @@ Applying switchroom config...
|
|
|
83583
83845
|
writeErr(source_default.red(`Vault directory ${vaultDir} contains unexpected files:
|
|
83584
83846
|
` + unknown.map((n) => ` - ${n}
|
|
83585
83847
|
`).join("") + `Refusing to bind-mount: a docker bind-mount source is the
|
|
83586
|
-
|
|
83587
|
-
|
|
83588
|
-
|
|
83589
|
-
|
|
83590
|
-
|
|
83848
|
+
entire directory, so unexpected files would be visible inside
|
|
83849
|
+
the broker container. Move them out, then re-run apply.
|
|
83850
|
+
Known artifacts: vault.enc, vault.enc.bak, vault.enc.tmp,
|
|
83851
|
+
vault.enc.lock (PID-file flock from saveVault), and
|
|
83852
|
+
.vault.enc.<pid>.<ms>.tmp (atomicWriteFileSync sibling-tmp).
|
|
83591
83853
|
`));
|
|
83592
83854
|
process.exit(6);
|
|
83593
83855
|
}
|
|
@@ -83606,8 +83868,8 @@ Applying switchroom config...
|
|
|
83606
83868
|
Wrote `) + displayComposePath + source_default.gray(` (${composeBytes} bytes)
|
|
83607
83869
|
`));
|
|
83608
83870
|
writeOut(`Bring the fleet up with:
|
|
83609
|
-
|
|
83610
|
-
|
|
83871
|
+
docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} pull && \\
|
|
83872
|
+
docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} up -d --remove-orphans
|
|
83611
83873
|
`);
|
|
83612
83874
|
writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
|
|
83613
83875
|
`));
|
|
@@ -83666,7 +83928,7 @@ function copyExampleConfig2(name) {
|
|
|
83666
83928
|
}
|
|
83667
83929
|
const embedded = EMBEDDED_EXAMPLES[name];
|
|
83668
83930
|
if (embedded !== undefined) {
|
|
83669
|
-
|
|
83931
|
+
writeFileSync38(dest, embedded, { encoding: "utf8" });
|
|
83670
83932
|
console.log(source_default.green(`Copied ${name}.yaml -> switchroom.yaml`));
|
|
83671
83933
|
return;
|
|
83672
83934
|
}
|
|
@@ -83682,7 +83944,7 @@ function findUnwritableAgentDirs(config, opts) {
|
|
|
83682
83944
|
const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
|
|
83683
83945
|
const unwritable = [];
|
|
83684
83946
|
for (const name of targets) {
|
|
83685
|
-
const startSh =
|
|
83947
|
+
const startSh = join76(agentsDir, name, "start.sh");
|
|
83686
83948
|
if (!existsSync74(startSh))
|
|
83687
83949
|
continue;
|
|
83688
83950
|
try {
|
|
@@ -83729,7 +83991,8 @@ function reexecUnderSudo() {
|
|
|
83729
83991
|
if (errCode === "ENOENT") {
|
|
83730
83992
|
process.stderr.write(source_default.red(`
|
|
83731
83993
|
ERROR: sudo not found on PATH. Re-run as root, or use
|
|
83732
|
-
|
|
83994
|
+
\`switchroom apply --compose-only\` to skip the per-agent
|
|
83995
|
+
scaffold refresh entirely (compose file still regenerates).
|
|
83733
83996
|
`));
|
|
83734
83997
|
process.exit(1);
|
|
83735
83998
|
}
|
|
@@ -83742,7 +84005,7 @@ ERROR: failed to spawn sudo: ${result.error.message}
|
|
|
83742
84005
|
process.exit(result.status ?? 1);
|
|
83743
84006
|
}
|
|
83744
84007
|
function registerApplyCommand(program3) {
|
|
83745
|
-
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:
|
|
84008
|
+
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) => {
|
|
83746
84009
|
try {
|
|
83747
84010
|
if (opts.example) {
|
|
83748
84011
|
copyExampleConfig2(opts.example);
|
|
@@ -83768,7 +84031,7 @@ function registerApplyCommand(program3) {
|
|
|
83768
84031
|
const canPrompt = !opts.nonInteractive && process.stdin.isTTY === true;
|
|
83769
84032
|
const proceed = canPrompt ? await confirmYesNo(`Re-exec under sudo to refresh them? [Y/n] `) : true;
|
|
83770
84033
|
if (!proceed) {
|
|
83771
|
-
process.stderr.write(source_default.gray(
|
|
84034
|
+
process.stderr.write(source_default.gray(`Skipping. Re-run with --compose-only to regenerate compose without touching per-agent files.
|
|
83772
84035
|
`));
|
|
83773
84036
|
process.exit(0);
|
|
83774
84037
|
}
|
|
@@ -83862,7 +84125,7 @@ function runRedactStdin() {
|
|
|
83862
84125
|
|
|
83863
84126
|
// src/cli/status-ask.ts
|
|
83864
84127
|
import { readFileSync as readFileSync64, existsSync as existsSync75, readdirSync as readdirSync27 } from "node:fs";
|
|
83865
|
-
import { join as
|
|
84128
|
+
import { join as join77 } from "node:path";
|
|
83866
84129
|
import { homedir as homedir45 } from "node:os";
|
|
83867
84130
|
|
|
83868
84131
|
// src/status-ask/report.ts
|
|
@@ -84198,7 +84461,7 @@ function resolveSources(explicitPath) {
|
|
|
84198
84461
|
const config = loadConfig();
|
|
84199
84462
|
agentsDir = resolveAgentsDir(config);
|
|
84200
84463
|
} catch {
|
|
84201
|
-
agentsDir =
|
|
84464
|
+
agentsDir = join77(homedir45(), ".switchroom", "agents");
|
|
84202
84465
|
}
|
|
84203
84466
|
if (!existsSync75(agentsDir))
|
|
84204
84467
|
return [];
|
|
@@ -84210,7 +84473,7 @@ function resolveSources(explicitPath) {
|
|
|
84210
84473
|
return [];
|
|
84211
84474
|
}
|
|
84212
84475
|
for (const name of entries) {
|
|
84213
|
-
const path8 =
|
|
84476
|
+
const path8 = join77(agentsDir, name, "runtime-metrics.jsonl");
|
|
84214
84477
|
if (existsSync75(path8)) {
|
|
84215
84478
|
sources.push({ path: path8, agent: name });
|
|
84216
84479
|
}
|
|
@@ -84239,7 +84502,7 @@ import {
|
|
|
84239
84502
|
closeSync as closeSync13,
|
|
84240
84503
|
existsSync as existsSync76,
|
|
84241
84504
|
fsyncSync as fsyncSync6,
|
|
84242
|
-
mkdirSync as
|
|
84505
|
+
mkdirSync as mkdirSync44,
|
|
84243
84506
|
openSync as openSync13,
|
|
84244
84507
|
readdirSync as readdirSync28,
|
|
84245
84508
|
readFileSync as readFileSync65,
|
|
@@ -84248,34 +84511,34 @@ import {
|
|
|
84248
84511
|
unlinkSync as unlinkSync14,
|
|
84249
84512
|
writeSync as writeSync8
|
|
84250
84513
|
} from "node:fs";
|
|
84251
|
-
import { join as
|
|
84514
|
+
import { join as join78, resolve as resolve47 } from "node:path";
|
|
84252
84515
|
var STAGING_SUBDIR = ".staging";
|
|
84253
84516
|
function overlayPathsFor(agent, opts = {}) {
|
|
84254
84517
|
const base = opts.root ? resolve47(opts.root, agent) : resolve47(resolveDualPath(`~/.switchroom/agents/${agent}`));
|
|
84255
|
-
const scheduleDir =
|
|
84256
|
-
const scheduleStagingDir =
|
|
84257
|
-
const skillsDir =
|
|
84258
|
-
const skillsStagingDir =
|
|
84518
|
+
const scheduleDir = join78(base, "schedule.d");
|
|
84519
|
+
const scheduleStagingDir = join78(scheduleDir, STAGING_SUBDIR);
|
|
84520
|
+
const skillsDir = join78(base, "skills.d");
|
|
84521
|
+
const skillsStagingDir = join78(skillsDir, STAGING_SUBDIR);
|
|
84259
84522
|
return {
|
|
84260
84523
|
agentRoot: base,
|
|
84261
84524
|
scheduleDir,
|
|
84262
84525
|
scheduleStagingDir,
|
|
84263
84526
|
skillsDir,
|
|
84264
84527
|
skillsStagingDir,
|
|
84265
|
-
lockPath:
|
|
84528
|
+
lockPath: join78(base, ".lock"),
|
|
84266
84529
|
stagingDir: scheduleStagingDir
|
|
84267
84530
|
};
|
|
84268
84531
|
}
|
|
84269
84532
|
function ensureDirs(paths) {
|
|
84270
|
-
|
|
84271
|
-
|
|
84533
|
+
mkdirSync44(paths.scheduleDir, { recursive: true });
|
|
84534
|
+
mkdirSync44(paths.scheduleStagingDir, { recursive: true });
|
|
84272
84535
|
}
|
|
84273
84536
|
function ensureSkillsDirs(paths) {
|
|
84274
|
-
|
|
84275
|
-
|
|
84537
|
+
mkdirSync44(paths.skillsDir, { recursive: true });
|
|
84538
|
+
mkdirSync44(paths.skillsStagingDir, { recursive: true });
|
|
84276
84539
|
}
|
|
84277
84540
|
function withAgentLock(paths, fn) {
|
|
84278
|
-
|
|
84541
|
+
mkdirSync44(paths.agentRoot, { recursive: true });
|
|
84279
84542
|
const start = Date.now();
|
|
84280
84543
|
const TIMEOUT_MS = 5000;
|
|
84281
84544
|
let fd = null;
|
|
@@ -84316,8 +84579,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
84316
84579
|
const paths = overlayPathsFor(agent, opts);
|
|
84317
84580
|
return withAgentLock(paths, () => {
|
|
84318
84581
|
ensureDirs(paths);
|
|
84319
|
-
const stagingPath =
|
|
84320
|
-
const finalPath =
|
|
84582
|
+
const stagingPath = join78(paths.scheduleStagingDir, `${slug}.yaml`);
|
|
84583
|
+
const finalPath = join78(paths.scheduleDir, `${slug}.yaml`);
|
|
84321
84584
|
const fd = openSync13(stagingPath, "w", 384);
|
|
84322
84585
|
try {
|
|
84323
84586
|
writeSync8(fd, yamlText);
|
|
@@ -84333,8 +84596,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
84333
84596
|
const paths = overlayPathsFor(agent, opts);
|
|
84334
84597
|
return withAgentLock(paths, () => {
|
|
84335
84598
|
ensureSkillsDirs(paths);
|
|
84336
|
-
const stagingPath =
|
|
84337
|
-
const finalPath =
|
|
84599
|
+
const stagingPath = join78(paths.skillsStagingDir, `${slug}.yaml`);
|
|
84600
|
+
const finalPath = join78(paths.skillsDir, `${slug}.yaml`);
|
|
84338
84601
|
const fd = openSync13(stagingPath, "w", 384);
|
|
84339
84602
|
try {
|
|
84340
84603
|
writeSync8(fd, yamlText);
|
|
@@ -84349,7 +84612,7 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
84349
84612
|
function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
|
|
84350
84613
|
const paths = overlayPathsFor(agent, opts);
|
|
84351
84614
|
return withAgentLock(paths, () => {
|
|
84352
|
-
const finalPath =
|
|
84615
|
+
const finalPath = join78(paths.skillsDir, `${slug}.yaml`);
|
|
84353
84616
|
if (!existsSync76(finalPath))
|
|
84354
84617
|
return false;
|
|
84355
84618
|
unlinkSync14(finalPath);
|
|
@@ -84364,7 +84627,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
|
|
|
84364
84627
|
for (const name of readdirSync28(paths.skillsDir)) {
|
|
84365
84628
|
if (!/\.ya?ml$/i.test(name))
|
|
84366
84629
|
continue;
|
|
84367
|
-
const full =
|
|
84630
|
+
const full = join78(paths.skillsDir, name);
|
|
84368
84631
|
try {
|
|
84369
84632
|
const raw = readFileSync65(full, "utf-8");
|
|
84370
84633
|
const slug = name.replace(/\.ya?ml$/i, "");
|
|
@@ -84376,7 +84639,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
|
|
|
84376
84639
|
function deleteOverlayEntry(agent, slug, opts = {}) {
|
|
84377
84640
|
const paths = overlayPathsFor(agent, opts);
|
|
84378
84641
|
return withAgentLock(paths, () => {
|
|
84379
|
-
const finalPath =
|
|
84642
|
+
const finalPath = join78(paths.scheduleDir, `${slug}.yaml`);
|
|
84380
84643
|
if (!existsSync76(finalPath))
|
|
84381
84644
|
return false;
|
|
84382
84645
|
unlinkSync14(finalPath);
|
|
@@ -84391,7 +84654,7 @@ function listOverlayEntries(agent, opts = {}) {
|
|
|
84391
84654
|
for (const name of readdirSync28(paths.scheduleDir)) {
|
|
84392
84655
|
if (!/\.ya?ml$/i.test(name))
|
|
84393
84656
|
continue;
|
|
84394
|
-
const full =
|
|
84657
|
+
const full = join78(paths.scheduleDir, name);
|
|
84395
84658
|
try {
|
|
84396
84659
|
const raw = readFileSync65(full, "utf-8");
|
|
84397
84660
|
const slug = name.replace(/\.ya?ml$/i, "");
|
|
@@ -84538,25 +84801,25 @@ import {
|
|
|
84538
84801
|
closeSync as closeSync14,
|
|
84539
84802
|
existsSync as existsSync77,
|
|
84540
84803
|
fsyncSync as fsyncSync7,
|
|
84541
|
-
mkdirSync as
|
|
84804
|
+
mkdirSync as mkdirSync45,
|
|
84542
84805
|
openSync as openSync14,
|
|
84543
84806
|
readdirSync as readdirSync29,
|
|
84544
84807
|
readFileSync as readFileSync66,
|
|
84545
84808
|
renameSync as renameSync16,
|
|
84546
84809
|
unlinkSync as unlinkSync15,
|
|
84547
|
-
writeFileSync as
|
|
84810
|
+
writeFileSync as writeFileSync39,
|
|
84548
84811
|
writeSync as writeSync9
|
|
84549
84812
|
} from "node:fs";
|
|
84550
|
-
import { join as
|
|
84813
|
+
import { join as join79 } from "node:path";
|
|
84551
84814
|
import { randomBytes as randomBytes14 } from "node:crypto";
|
|
84552
84815
|
var STAGE_ID_PREFIX = "cap_";
|
|
84553
84816
|
function pendingDir(agent, opts = {}) {
|
|
84554
84817
|
const paths = overlayPathsFor(agent, opts);
|
|
84555
|
-
return
|
|
84818
|
+
return join79(paths.scheduleDir, ".pending");
|
|
84556
84819
|
}
|
|
84557
84820
|
function ensurePendingDir(agent, opts = {}) {
|
|
84558
84821
|
const dir = pendingDir(agent, opts);
|
|
84559
|
-
|
|
84822
|
+
mkdirSync45(dir, { recursive: true });
|
|
84560
84823
|
return dir;
|
|
84561
84824
|
}
|
|
84562
84825
|
function newStageId() {
|
|
@@ -84565,8 +84828,8 @@ function newStageId() {
|
|
|
84565
84828
|
function stagePendingScheduleEntry(opts) {
|
|
84566
84829
|
const dir = ensurePendingDir(opts.agent, { root: opts.root });
|
|
84567
84830
|
const stageId = opts.stageId ?? newStageId();
|
|
84568
|
-
const yamlPath =
|
|
84569
|
-
const metaPath =
|
|
84831
|
+
const yamlPath = join79(dir, `${stageId}.yaml`);
|
|
84832
|
+
const metaPath = join79(dir, `${stageId}.meta.json`);
|
|
84570
84833
|
const meta = {
|
|
84571
84834
|
v: 1,
|
|
84572
84835
|
stage_id: stageId,
|
|
@@ -84587,7 +84850,7 @@ function stagePendingScheduleEntry(opts) {
|
|
|
84587
84850
|
}
|
|
84588
84851
|
renameSync16(yamlTmp, yamlPath);
|
|
84589
84852
|
}
|
|
84590
|
-
|
|
84853
|
+
writeFileSync39(metaPath, JSON.stringify(meta, null, 2) + `
|
|
84591
84854
|
`, { mode: 384 });
|
|
84592
84855
|
return { stageId, yamlPath, metaPath };
|
|
84593
84856
|
}
|
|
@@ -84600,8 +84863,8 @@ function listPendingScheduleEntries(agent, opts = {}) {
|
|
|
84600
84863
|
if (!name.endsWith(".meta.json"))
|
|
84601
84864
|
continue;
|
|
84602
84865
|
const stageId = name.slice(0, -".meta.json".length);
|
|
84603
|
-
const metaPath =
|
|
84604
|
-
const yamlPath =
|
|
84866
|
+
const metaPath = join79(dir, name);
|
|
84867
|
+
const yamlPath = join79(dir, `${stageId}.yaml`);
|
|
84605
84868
|
if (!existsSync77(yamlPath))
|
|
84606
84869
|
continue;
|
|
84607
84870
|
try {
|
|
@@ -84620,7 +84883,7 @@ function commitPendingScheduleEntry(opts) {
|
|
|
84620
84883
|
return { committed: false, reason: "not_found" };
|
|
84621
84884
|
const slug = match.meta.entry.name ?? match.stageId;
|
|
84622
84885
|
const paths = overlayPathsFor(opts.agent, { root: opts.root });
|
|
84623
|
-
const finalPath =
|
|
84886
|
+
const finalPath = join79(paths.scheduleDir, `${slug}.yaml`);
|
|
84624
84887
|
if (existsSync77(finalPath)) {
|
|
84625
84888
|
return { committed: false, reason: "slug_collision" };
|
|
84626
84889
|
}
|
|
@@ -85253,7 +85516,7 @@ var import_yaml21 = __toESM(require_dist(), 1);
|
|
|
85253
85516
|
import { existsSync as existsSync79 } from "node:fs";
|
|
85254
85517
|
init_reconcile_default_skills();
|
|
85255
85518
|
var import_yaml22 = __toESM(require_dist(), 1);
|
|
85256
|
-
import { join as
|
|
85519
|
+
import { join as join80 } from "node:path";
|
|
85257
85520
|
var MAX_SKILLS_PER_AGENT = 20;
|
|
85258
85521
|
var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
|
|
85259
85522
|
function exitCodeFor2(code) {
|
|
@@ -85328,7 +85591,7 @@ function skillInstall(opts) {
|
|
|
85328
85591
|
return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
|
|
85329
85592
|
}
|
|
85330
85593
|
const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
|
|
85331
|
-
const skillPath =
|
|
85594
|
+
const skillPath = join80(poolDir, skillName);
|
|
85332
85595
|
if (!existsSync79(skillPath)) {
|
|
85333
85596
|
return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
|
|
85334
85597
|
}
|
|
@@ -85495,7 +85758,7 @@ import {
|
|
|
85495
85758
|
closeSync as closeSync15,
|
|
85496
85759
|
existsSync as existsSync80,
|
|
85497
85760
|
lstatSync as lstatSync9,
|
|
85498
|
-
mkdirSync as
|
|
85761
|
+
mkdirSync as mkdirSync46,
|
|
85499
85762
|
mkdtempSync as mkdtempSync5,
|
|
85500
85763
|
openSync as openSync15,
|
|
85501
85764
|
readFileSync as readFileSync68,
|
|
@@ -85504,10 +85767,10 @@ import {
|
|
|
85504
85767
|
renameSync as renameSync17,
|
|
85505
85768
|
rmSync as rmSync16,
|
|
85506
85769
|
statSync as statSync32,
|
|
85507
|
-
writeFileSync as
|
|
85770
|
+
writeFileSync as writeFileSync40
|
|
85508
85771
|
} from "node:fs";
|
|
85509
85772
|
import { tmpdir as tmpdir5, homedir as homedir46 } from "node:os";
|
|
85510
|
-
import { dirname as dirname24, join as
|
|
85773
|
+
import { dirname as dirname24, join as join81, relative as relative2, resolve as resolve48 } from "node:path";
|
|
85511
85774
|
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
85512
85775
|
|
|
85513
85776
|
// src/cli/skill-common.ts
|
|
@@ -85701,7 +85964,7 @@ function scanForClaudeP2(content) {
|
|
|
85701
85964
|
function resolveSkillsPoolDir2(override) {
|
|
85702
85965
|
const raw = override ?? "~/.switchroom/skills";
|
|
85703
85966
|
if (raw.startsWith("~/")) {
|
|
85704
|
-
return
|
|
85967
|
+
return join81(homedir46(), raw.slice(2));
|
|
85705
85968
|
}
|
|
85706
85969
|
if (raw === "~")
|
|
85707
85970
|
return homedir46();
|
|
@@ -85740,7 +86003,7 @@ function loadFromDir(dir) {
|
|
|
85740
86003
|
const walk2 = (sub) => {
|
|
85741
86004
|
const entries = readdirSync30(sub, { withFileTypes: true });
|
|
85742
86005
|
for (const ent of entries) {
|
|
85743
|
-
const full =
|
|
86006
|
+
const full = join81(sub, ent.name);
|
|
85744
86007
|
const rel = relative2(abs, full);
|
|
85745
86008
|
if (ent.isSymbolicLink()) {
|
|
85746
86009
|
fail3(`refusing to read symlink inside --from dir: ${rel}`);
|
|
@@ -85775,7 +86038,7 @@ function loadFromTarball(tarPath) {
|
|
|
85775
86038
|
fail3(`tarball contains disallowed path: ${JSON.stringify(entry)} \u2014 ` + `refusing to extract before any file is written`);
|
|
85776
86039
|
}
|
|
85777
86040
|
}
|
|
85778
|
-
const staging = mkdtempSync5(
|
|
86041
|
+
const staging = mkdtempSync5(join81(tmpdir5(), "skill-apply-extract-"));
|
|
85779
86042
|
try {
|
|
85780
86043
|
const flags = isGz ? ["-xzf"] : ["-xf"];
|
|
85781
86044
|
const r = spawnSync12("tar", [
|
|
@@ -85861,10 +86124,10 @@ function validatePayload(name, files) {
|
|
|
85861
86124
|
errors2.push(`${path8} fails \`bash -n\` syntax check: ${(r.stderr ?? "").trim()}`);
|
|
85862
86125
|
}
|
|
85863
86126
|
} else if (PY_SCRIPT_RE2.test(path8)) {
|
|
85864
|
-
const tmp = mkdtempSync5(
|
|
85865
|
-
const tmpPy =
|
|
86127
|
+
const tmp = mkdtempSync5(join81(tmpdir5(), "skill-apply-py-"));
|
|
86128
|
+
const tmpPy = join81(tmp, "check.py");
|
|
85866
86129
|
try {
|
|
85867
|
-
|
|
86130
|
+
writeFileSync40(tmpPy, content);
|
|
85868
86131
|
const r = spawnSync12("python3", ["-m", "py_compile", tmpPy], {
|
|
85869
86132
|
encoding: "utf-8"
|
|
85870
86133
|
});
|
|
@@ -85885,7 +86148,7 @@ function diffSummary(currentDir, files) {
|
|
|
85885
86148
|
if (existsSync80(currentDir)) {
|
|
85886
86149
|
const walk2 = (sub) => {
|
|
85887
86150
|
for (const ent of readdirSync30(sub, { withFileTypes: true })) {
|
|
85888
|
-
const full =
|
|
86151
|
+
const full = join81(sub, ent.name);
|
|
85889
86152
|
const rel = relative2(currentDir, full);
|
|
85890
86153
|
if (ent.isDirectory()) {
|
|
85891
86154
|
walk2(full);
|
|
@@ -85919,9 +86182,9 @@ function diffSummary(currentDir, files) {
|
|
|
85919
86182
|
}
|
|
85920
86183
|
function writePayload(poolDir, name, files) {
|
|
85921
86184
|
if (!existsSync80(poolDir)) {
|
|
85922
|
-
|
|
86185
|
+
mkdirSync46(poolDir, { recursive: true, mode: 493 });
|
|
85923
86186
|
}
|
|
85924
|
-
const target =
|
|
86187
|
+
const target = join81(poolDir, name);
|
|
85925
86188
|
let targetIsSymlink = false;
|
|
85926
86189
|
try {
|
|
85927
86190
|
const st = lstatSync9(target);
|
|
@@ -85932,15 +86195,15 @@ function writePayload(poolDir, name, files) {
|
|
|
85932
86195
|
if (targetIsSymlink) {
|
|
85933
86196
|
fail3(`refusing to overwrite symlink at ${target}; investigate manually`);
|
|
85934
86197
|
}
|
|
85935
|
-
const staging = mkdtempSync5(
|
|
86198
|
+
const staging = mkdtempSync5(join81(poolDir, `.skill-apply-stage-${name}-`));
|
|
85936
86199
|
let oldRename = null;
|
|
85937
86200
|
try {
|
|
85938
86201
|
for (const [path8, content] of Object.entries(files)) {
|
|
85939
|
-
const full =
|
|
85940
|
-
|
|
86202
|
+
const full = join81(staging, path8);
|
|
86203
|
+
mkdirSync46(dirname24(full), { recursive: true, mode: 493 });
|
|
85941
86204
|
const fd = openSync15(full, "wx");
|
|
85942
86205
|
try {
|
|
85943
|
-
|
|
86206
|
+
writeFileSync40(fd, content);
|
|
85944
86207
|
} finally {
|
|
85945
86208
|
closeSync15(fd);
|
|
85946
86209
|
}
|
|
@@ -86014,7 +86277,7 @@ function registerSkillCommand(program3) {
|
|
|
86014
86277
|
}
|
|
86015
86278
|
const config = loadConfig();
|
|
86016
86279
|
const poolDir = resolveSkillsPoolDir2(config.switchroom?.skills_dir);
|
|
86017
|
-
const currentDir =
|
|
86280
|
+
const currentDir = join81(poolDir, name);
|
|
86018
86281
|
console.log(source_default.bold(`Skill: ${name}`) + source_default.gray(` (${Object.keys(files).length} files, ${sumBytes(files)} bytes)`));
|
|
86019
86282
|
console.log(source_default.bold("Diff vs current pool content:"));
|
|
86020
86283
|
console.log(diffSummary(currentDir, files));
|
|
@@ -86047,7 +86310,7 @@ import {
|
|
|
86047
86310
|
closeSync as closeSync16,
|
|
86048
86311
|
existsSync as existsSync81,
|
|
86049
86312
|
lstatSync as lstatSync10,
|
|
86050
|
-
mkdirSync as
|
|
86313
|
+
mkdirSync as mkdirSync47,
|
|
86051
86314
|
mkdtempSync as mkdtempSync6,
|
|
86052
86315
|
openSync as openSync16,
|
|
86053
86316
|
readFileSync as readFileSync69,
|
|
@@ -86056,9 +86319,9 @@ import {
|
|
|
86056
86319
|
rmSync as rmSync17,
|
|
86057
86320
|
statSync as statSync33,
|
|
86058
86321
|
utimesSync,
|
|
86059
|
-
writeFileSync as
|
|
86322
|
+
writeFileSync as writeFileSync41
|
|
86060
86323
|
} from "node:fs";
|
|
86061
|
-
import { dirname as dirname25, join as
|
|
86324
|
+
import { dirname as dirname25, join as join82, relative as relative3, resolve as resolve49 } from "node:path";
|
|
86062
86325
|
import { homedir as homedir47, tmpdir as tmpdir6 } from "node:os";
|
|
86063
86326
|
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
86064
86327
|
init_helpers();
|
|
@@ -86069,10 +86332,10 @@ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
86069
86332
|
var PERSONAL_SKILLS_SUBPATH = "personal-skills";
|
|
86070
86333
|
function resolveConfigSkillsDir(agent) {
|
|
86071
86334
|
const override = process.env.SWITCHROOM_CONFIG_DIR;
|
|
86072
|
-
const candidate = override ? resolve49(override) :
|
|
86335
|
+
const candidate = override ? resolve49(override) : join82(homedir47(), ".switchroom-config");
|
|
86073
86336
|
if (!existsSync81(candidate))
|
|
86074
86337
|
return null;
|
|
86075
|
-
return
|
|
86338
|
+
return join82(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
|
|
86076
86339
|
}
|
|
86077
86340
|
var MIRROR_PRIOR_TTL_MS = 24 * 60 * 60 * 1000;
|
|
86078
86341
|
function sweepMirrorPriors(configSkillsRoot) {
|
|
@@ -86090,7 +86353,7 @@ function sweepMirrorPriors(configSkillsRoot) {
|
|
|
86090
86353
|
if (now - ts < MIRROR_PRIOR_TTL_MS)
|
|
86091
86354
|
continue;
|
|
86092
86355
|
try {
|
|
86093
|
-
rmSync17(
|
|
86356
|
+
rmSync17(join82(configSkillsRoot, ent), { recursive: true, force: true });
|
|
86094
86357
|
} catch {}
|
|
86095
86358
|
}
|
|
86096
86359
|
} catch {}
|
|
@@ -86099,7 +86362,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
|
|
|
86099
86362
|
const configSkillsRoot = resolveConfigSkillsDir(agent);
|
|
86100
86363
|
if (!configSkillsRoot)
|
|
86101
86364
|
return;
|
|
86102
|
-
const dest =
|
|
86365
|
+
const dest = join82(configSkillsRoot, name);
|
|
86103
86366
|
try {
|
|
86104
86367
|
if (liveSkillDir !== null) {
|
|
86105
86368
|
try {
|
|
@@ -86114,31 +86377,31 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
|
|
|
86114
86377
|
if (liveSkillDir === null) {
|
|
86115
86378
|
sweepMirrorPriors(configSkillsRoot);
|
|
86116
86379
|
if (existsSync81(dest)) {
|
|
86117
|
-
const trash =
|
|
86380
|
+
const trash = join82(configSkillsRoot, `.${name}-trash-${Date.now()}`);
|
|
86118
86381
|
renameSync18(dest, trash);
|
|
86119
86382
|
}
|
|
86120
86383
|
return;
|
|
86121
86384
|
}
|
|
86122
|
-
|
|
86385
|
+
mkdirSync47(configSkillsRoot, { recursive: true, mode: 493 });
|
|
86123
86386
|
sweepMirrorPriors(configSkillsRoot);
|
|
86124
|
-
const staging = mkdtempSync6(
|
|
86387
|
+
const staging = mkdtempSync6(join82(configSkillsRoot, `.${name}-staging-`));
|
|
86125
86388
|
const walk2 = (src, dst) => {
|
|
86126
|
-
|
|
86389
|
+
mkdirSync47(dst, { recursive: true, mode: 493 });
|
|
86127
86390
|
for (const ent of readdirSync31(src, { withFileTypes: true })) {
|
|
86128
|
-
const s =
|
|
86129
|
-
const d =
|
|
86391
|
+
const s = join82(src, ent.name);
|
|
86392
|
+
const d = join82(dst, ent.name);
|
|
86130
86393
|
if (ent.isSymbolicLink())
|
|
86131
86394
|
continue;
|
|
86132
86395
|
if (ent.isDirectory())
|
|
86133
86396
|
walk2(s, d);
|
|
86134
86397
|
else if (ent.isFile()) {
|
|
86135
|
-
|
|
86398
|
+
writeFileSync41(d, readFileSync69(s));
|
|
86136
86399
|
}
|
|
86137
86400
|
}
|
|
86138
86401
|
};
|
|
86139
86402
|
walk2(liveSkillDir, staging);
|
|
86140
86403
|
if (existsSync81(dest)) {
|
|
86141
|
-
const prior =
|
|
86404
|
+
const prior = join82(configSkillsRoot, `.${name}-prior-${Date.now()}`);
|
|
86142
86405
|
renameSync18(dest, prior);
|
|
86143
86406
|
}
|
|
86144
86407
|
renameSync18(staging, dest);
|
|
@@ -86165,13 +86428,13 @@ function resolveAgent(opts) {
|
|
|
86165
86428
|
function resolveAgentsRoot(opts) {
|
|
86166
86429
|
if (opts.root)
|
|
86167
86430
|
return resolve49(opts.root);
|
|
86168
|
-
return
|
|
86431
|
+
return join82(homedir47(), ".switchroom", "agents");
|
|
86169
86432
|
}
|
|
86170
86433
|
function personalSkillDir(agentsRoot, agent, name) {
|
|
86171
|
-
return
|
|
86434
|
+
return join82(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
|
|
86172
86435
|
}
|
|
86173
86436
|
function trashDir(agentsRoot, agent) {
|
|
86174
|
-
return
|
|
86437
|
+
return join82(agentsRoot, agent, ".claude", TRASH_DIRNAME);
|
|
86175
86438
|
}
|
|
86176
86439
|
function readStdinSync2() {
|
|
86177
86440
|
const chunks = [];
|
|
@@ -86201,7 +86464,7 @@ function loadFromDir2(dir) {
|
|
|
86201
86464
|
const files = {};
|
|
86202
86465
|
const walk2 = (sub) => {
|
|
86203
86466
|
for (const ent of readdirSync31(sub, { withFileTypes: true })) {
|
|
86204
|
-
const full =
|
|
86467
|
+
const full = join82(sub, ent.name);
|
|
86205
86468
|
if (ent.isSymbolicLink()) {
|
|
86206
86469
|
fail4(`refusing to read symlink in --from dir: ${relative3(abs, full)}`);
|
|
86207
86470
|
}
|
|
@@ -86254,10 +86517,10 @@ function behavioralValidate(files) {
|
|
|
86254
86517
|
errors2.push(`${path8} fails \`bash -n\`: ${(r.stderr ?? "").trim()}`);
|
|
86255
86518
|
}
|
|
86256
86519
|
} else if (PY_SCRIPT_RE.test(path8)) {
|
|
86257
|
-
const tmp = mkdtempSync6(
|
|
86258
|
-
const tmpPy =
|
|
86520
|
+
const tmp = mkdtempSync6(join82(tmpdir6(), "skill-personal-py-"));
|
|
86521
|
+
const tmpPy = join82(tmp, "check.py");
|
|
86259
86522
|
try {
|
|
86260
|
-
|
|
86523
|
+
writeFileSync41(tmpPy, content);
|
|
86261
86524
|
const r = spawnSync13("python3", ["-m", "py_compile", tmpPy], {
|
|
86262
86525
|
encoding: "utf-8"
|
|
86263
86526
|
});
|
|
@@ -86279,7 +86542,7 @@ function sweepTrash(agentsRoot, agent) {
|
|
|
86279
86542
|
for (const ent of readdirSync31(trash, { withFileTypes: true })) {
|
|
86280
86543
|
if (!ent.isDirectory())
|
|
86281
86544
|
continue;
|
|
86282
|
-
const entPath =
|
|
86545
|
+
const entPath = join82(trash, ent.name);
|
|
86283
86546
|
try {
|
|
86284
86547
|
const st = statSync33(entPath);
|
|
86285
86548
|
if (now - st.mtimeMs > TRASH_TTL_MS) {
|
|
@@ -86299,16 +86562,16 @@ function writePersonalSkill(targetDir, files) {
|
|
|
86299
86562
|
if (targetIsSymlink) {
|
|
86300
86563
|
fail4(`refusing to overwrite symlink at ${targetDir}; investigate manually`);
|
|
86301
86564
|
}
|
|
86302
|
-
|
|
86303
|
-
const staging = mkdtempSync6(
|
|
86565
|
+
mkdirSync47(dirname25(targetDir), { recursive: true, mode: 493 });
|
|
86566
|
+
const staging = mkdtempSync6(join82(dirname25(targetDir), `.skill-personal-stage-`));
|
|
86304
86567
|
let oldRename = null;
|
|
86305
86568
|
try {
|
|
86306
86569
|
for (const [path8, content] of Object.entries(files)) {
|
|
86307
|
-
const full =
|
|
86308
|
-
|
|
86570
|
+
const full = join82(staging, path8);
|
|
86571
|
+
mkdirSync47(dirname25(full), { recursive: true, mode: 493 });
|
|
86309
86572
|
const fd = openSync16(full, "wx");
|
|
86310
86573
|
try {
|
|
86311
|
-
|
|
86574
|
+
writeFileSync41(fd, content);
|
|
86312
86575
|
} finally {
|
|
86313
86576
|
closeSync16(fd);
|
|
86314
86577
|
}
|
|
@@ -86437,10 +86700,10 @@ function editPersonalAction(name, opts) {
|
|
|
86437
86700
|
}
|
|
86438
86701
|
var CLONE_SOURCE_RE = /^(shared|bundled):([a-z0-9][a-z0-9_-]{0,62})$/;
|
|
86439
86702
|
function defaultSharedRoot() {
|
|
86440
|
-
return
|
|
86703
|
+
return join82(homedir47(), ".switchroom", "skills");
|
|
86441
86704
|
}
|
|
86442
86705
|
function defaultBundledRoot() {
|
|
86443
|
-
return
|
|
86706
|
+
return join82(homedir47(), ".switchroom", "skills", "_bundled");
|
|
86444
86707
|
}
|
|
86445
86708
|
function resolveCloneSource(source, opts) {
|
|
86446
86709
|
const m = CLONE_SOURCE_RE.exec(source);
|
|
@@ -86450,7 +86713,7 @@ function resolveCloneSource(source, opts) {
|
|
|
86450
86713
|
const tier = m[1];
|
|
86451
86714
|
const slug = m[2];
|
|
86452
86715
|
const root = tier === "bundled" ? opts.bundledRoot ?? defaultBundledRoot() : opts.sharedRoot ?? defaultSharedRoot();
|
|
86453
|
-
const dir =
|
|
86716
|
+
const dir = join82(root, slug);
|
|
86454
86717
|
if (!existsSync81(dir)) {
|
|
86455
86718
|
fail4(`clone source ${JSON.stringify(source)} not found at ${dir}; ` + `check \`switchroom skill search --tier ${tier}\``, 1);
|
|
86456
86719
|
}
|
|
@@ -86466,7 +86729,7 @@ function readSourceFiles(dir) {
|
|
|
86466
86729
|
const skipped = [];
|
|
86467
86730
|
const walk2 = (sub) => {
|
|
86468
86731
|
for (const ent of readdirSync31(sub, { withFileTypes: true })) {
|
|
86469
|
-
const full =
|
|
86732
|
+
const full = join82(sub, ent.name);
|
|
86470
86733
|
if (ent.isSymbolicLink()) {
|
|
86471
86734
|
continue;
|
|
86472
86735
|
}
|
|
@@ -86575,9 +86838,9 @@ function removePersonalAction(name, opts) {
|
|
|
86575
86838
|
throw err2;
|
|
86576
86839
|
}
|
|
86577
86840
|
const trashRoot = trashDir(agentsRoot, agent);
|
|
86578
|
-
|
|
86841
|
+
mkdirSync47(trashRoot, { recursive: true, mode: 493 });
|
|
86579
86842
|
const ts = Date.now();
|
|
86580
|
-
const trashTarget =
|
|
86843
|
+
const trashTarget = join82(trashRoot, `${name}-${ts}`);
|
|
86581
86844
|
renameSync18(target, trashTarget);
|
|
86582
86845
|
const now = new Date(ts);
|
|
86583
86846
|
utimesSync(trashTarget, now, now);
|
|
@@ -86596,7 +86859,7 @@ function listPersonalAction(opts) {
|
|
|
86596
86859
|
const agent = resolveAgent(opts);
|
|
86597
86860
|
const agentsRoot = resolveAgentsRoot(opts);
|
|
86598
86861
|
sweepTrash(agentsRoot, agent);
|
|
86599
|
-
const skillsDir =
|
|
86862
|
+
const skillsDir = join82(agentsRoot, agent, ".claude", "skills");
|
|
86600
86863
|
const personal = [];
|
|
86601
86864
|
if (existsSync81(skillsDir)) {
|
|
86602
86865
|
for (const ent of readdirSync31(skillsDir, { withFileTypes: true })) {
|
|
@@ -86605,7 +86868,7 @@ function listPersonalAction(opts) {
|
|
|
86605
86868
|
if (!ent.name.startsWith(PERSONAL_PREFIX))
|
|
86606
86869
|
continue;
|
|
86607
86870
|
const skillName = ent.name.slice(PERSONAL_PREFIX.length);
|
|
86608
|
-
const skillPath =
|
|
86871
|
+
const skillPath = join82(skillsDir, ent.name);
|
|
86609
86872
|
let fileCount = 0;
|
|
86610
86873
|
let totalBytes = 0;
|
|
86611
86874
|
const walk2 = (sub) => {
|
|
@@ -86613,10 +86876,10 @@ function listPersonalAction(opts) {
|
|
|
86613
86876
|
if (e.isFile()) {
|
|
86614
86877
|
fileCount += 1;
|
|
86615
86878
|
try {
|
|
86616
|
-
totalBytes += statSync33(
|
|
86879
|
+
totalBytes += statSync33(join82(sub, e.name)).size;
|
|
86617
86880
|
} catch {}
|
|
86618
86881
|
} else if (e.isDirectory()) {
|
|
86619
|
-
walk2(
|
|
86882
|
+
walk2(join82(sub, e.name));
|
|
86620
86883
|
}
|
|
86621
86884
|
}
|
|
86622
86885
|
};
|
|
@@ -86657,7 +86920,7 @@ init_helpers();
|
|
|
86657
86920
|
var import_yaml24 = __toESM(require_dist(), 1);
|
|
86658
86921
|
import { existsSync as existsSync82, readdirSync as readdirSync32, readFileSync as readFileSync70, statSync as statSync34 } from "node:fs";
|
|
86659
86922
|
import { homedir as homedir48 } from "node:os";
|
|
86660
|
-
import { join as
|
|
86923
|
+
import { join as join83, resolve as resolve50 } from "node:path";
|
|
86661
86924
|
var PERSONAL_PREFIX2 = "personal-";
|
|
86662
86925
|
var BUNDLED_SUBDIR = "_bundled";
|
|
86663
86926
|
var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
@@ -86671,7 +86934,7 @@ function defaultBundledRoot2() {
|
|
|
86671
86934
|
return resolve50(homedir48(), ".switchroom/skills/_bundled");
|
|
86672
86935
|
}
|
|
86673
86936
|
function readSkillFrontmatter(skillDir) {
|
|
86674
|
-
const mdPath =
|
|
86937
|
+
const mdPath = join83(skillDir, "SKILL.md");
|
|
86675
86938
|
if (!existsSync82(mdPath))
|
|
86676
86939
|
return null;
|
|
86677
86940
|
let content;
|
|
@@ -86704,7 +86967,7 @@ function readSkillFrontmatter(skillDir) {
|
|
|
86704
86967
|
return { fm: parsed };
|
|
86705
86968
|
}
|
|
86706
86969
|
function statSkillMd(skillDir) {
|
|
86707
|
-
const mdPath =
|
|
86970
|
+
const mdPath = join83(skillDir, "SKILL.md");
|
|
86708
86971
|
try {
|
|
86709
86972
|
const st = statSync34(mdPath);
|
|
86710
86973
|
return { size: st.size, mtime: st.mtime.toISOString() };
|
|
@@ -86715,7 +86978,7 @@ function statSkillMd(skillDir) {
|
|
|
86715
86978
|
function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
|
|
86716
86979
|
if (!AGENT_NAME_RE3.test(agent))
|
|
86717
86980
|
return [];
|
|
86718
|
-
const skillsDir =
|
|
86981
|
+
const skillsDir = join83(agentsRoot, agent, ".claude/skills");
|
|
86719
86982
|
if (!existsSync82(skillsDir))
|
|
86720
86983
|
return [];
|
|
86721
86984
|
const out = [];
|
|
@@ -86728,7 +86991,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
|
|
|
86728
86991
|
for (const ent of entries) {
|
|
86729
86992
|
if (!ent.startsWith(PERSONAL_PREFIX2))
|
|
86730
86993
|
continue;
|
|
86731
|
-
const dirPath =
|
|
86994
|
+
const dirPath = join83(skillsDir, ent);
|
|
86732
86995
|
try {
|
|
86733
86996
|
if (!statSync34(dirPath).isDirectory())
|
|
86734
86997
|
continue;
|
|
@@ -86768,7 +87031,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
|
|
|
86768
87031
|
continue;
|
|
86769
87032
|
if (ent.startsWith("."))
|
|
86770
87033
|
continue;
|
|
86771
|
-
const dirPath =
|
|
87034
|
+
const dirPath = join83(sharedRoot, ent);
|
|
86772
87035
|
try {
|
|
86773
87036
|
if (!statSync34(dirPath).isDirectory())
|
|
86774
87037
|
continue;
|
|
@@ -86804,7 +87067,7 @@ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
|
|
|
86804
87067
|
for (const ent of entries) {
|
|
86805
87068
|
if (ent.startsWith("."))
|
|
86806
87069
|
continue;
|
|
86807
|
-
const dirPath =
|
|
87070
|
+
const dirPath = join83(bundledRoot, ent);
|
|
86808
87071
|
try {
|
|
86809
87072
|
if (!statSync34(dirPath).isDirectory())
|
|
86810
87073
|
continue;
|
|
@@ -86948,9 +87211,9 @@ function registerHostdMcpCommand(program3) {
|
|
|
86948
87211
|
// src/cli/hostd.ts
|
|
86949
87212
|
init_source();
|
|
86950
87213
|
init_helpers();
|
|
86951
|
-
import { existsSync as existsSync84, mkdirSync as
|
|
87214
|
+
import { existsSync as existsSync84, mkdirSync as mkdirSync48, readdirSync as readdirSync33, readFileSync as readFileSync72, writeFileSync as writeFileSync42, statSync as statSync35, copyFileSync as copyFileSync12 } from "node:fs";
|
|
86952
87215
|
import { homedir as homedir49 } from "node:os";
|
|
86953
|
-
import { join as
|
|
87216
|
+
import { join as join84 } from "node:path";
|
|
86954
87217
|
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
86955
87218
|
|
|
86956
87219
|
// src/cli/deploy-version-guard.ts
|
|
@@ -87113,10 +87376,10 @@ function resolveHostdHostHome(env2 = process.env, home2 = homedir49()) {
|
|
|
87113
87376
|
return resolved;
|
|
87114
87377
|
}
|
|
87115
87378
|
function hostdDir() {
|
|
87116
|
-
return
|
|
87379
|
+
return join84(homedir49(), ".switchroom", "hostd");
|
|
87117
87380
|
}
|
|
87118
87381
|
function hostdComposePath() {
|
|
87119
|
-
return
|
|
87382
|
+
return join84(hostdDir(), "docker-compose.yml");
|
|
87120
87383
|
}
|
|
87121
87384
|
function backupExistingCompose() {
|
|
87122
87385
|
const p = hostdComposePath();
|
|
@@ -87153,7 +87416,7 @@ async function doInstall(opts, program3) {
|
|
|
87153
87416
|
}
|
|
87154
87417
|
const dir = hostdDir();
|
|
87155
87418
|
const composePath = hostdComposePath();
|
|
87156
|
-
|
|
87419
|
+
mkdirSync48(dir, { recursive: true });
|
|
87157
87420
|
const imageTag = resolveHostdImageTag(opts.tag, cfg.release);
|
|
87158
87421
|
const guard = checkDowngrade({
|
|
87159
87422
|
container: "switchroom-hostd",
|
|
@@ -87178,7 +87441,7 @@ async function doInstall(opts, program3) {
|
|
|
87178
87441
|
const bak = backupExistingCompose();
|
|
87179
87442
|
if (bak)
|
|
87180
87443
|
console.log(source_default.dim(` Backed up existing compose to ${bak}`));
|
|
87181
|
-
|
|
87444
|
+
writeFileSync42(composePath, yaml, "utf8");
|
|
87182
87445
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
87183
87446
|
const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
|
|
87184
87447
|
console.log(source_default.dim(` agents served (one socket each): ${allAgents.length === 0 ? "(none)" : allAgents.join(", ")}`));
|
|
@@ -87236,7 +87499,7 @@ function doStatus() {
|
|
|
87236
87499
|
for (const name of readdirSync33(dir)) {
|
|
87237
87500
|
if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
|
|
87238
87501
|
continue;
|
|
87239
|
-
const sockPath =
|
|
87502
|
+
const sockPath = join84(dir, name, "sock");
|
|
87240
87503
|
if (existsSync84(sockPath)) {
|
|
87241
87504
|
const st = statSync35(sockPath);
|
|
87242
87505
|
if ((st.mode & 61440) === 49152) {
|
|
@@ -87327,9 +87590,9 @@ The log is created when hostd handles its first privileged-verb request.`));
|
|
|
87327
87590
|
// src/cli/webd.ts
|
|
87328
87591
|
init_source();
|
|
87329
87592
|
init_helpers();
|
|
87330
|
-
import { existsSync as existsSync85, mkdirSync as
|
|
87593
|
+
import { existsSync as existsSync85, mkdirSync as mkdirSync49, writeFileSync as writeFileSync43, copyFileSync as copyFileSync13 } from "node:fs";
|
|
87331
87594
|
import { homedir as homedir50 } from "node:os";
|
|
87332
|
-
import { join as
|
|
87595
|
+
import { join as join85 } from "node:path";
|
|
87333
87596
|
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
87334
87597
|
function resolveWebImageTag(explicitTag, release) {
|
|
87335
87598
|
if (explicitTag)
|
|
@@ -87414,10 +87677,10 @@ services:
|
|
|
87414
87677
|
`;
|
|
87415
87678
|
}
|
|
87416
87679
|
function webdDir() {
|
|
87417
|
-
return
|
|
87680
|
+
return join85(homedir50(), ".switchroom", "web");
|
|
87418
87681
|
}
|
|
87419
87682
|
function webdComposePath() {
|
|
87420
|
-
return
|
|
87683
|
+
return join85(webdDir(), "docker-compose.yml");
|
|
87421
87684
|
}
|
|
87422
87685
|
function backupExistingCompose2() {
|
|
87423
87686
|
const p = webdComposePath();
|
|
@@ -87446,7 +87709,7 @@ async function doInstall2(opts, program3) {
|
|
|
87446
87709
|
}
|
|
87447
87710
|
const dir = webdDir();
|
|
87448
87711
|
const composePath = webdComposePath();
|
|
87449
|
-
|
|
87712
|
+
mkdirSync49(dir, { recursive: true });
|
|
87450
87713
|
const cfg = getConfig(program3);
|
|
87451
87714
|
const imageTag = resolveWebImageTag(opts.tag, cfg.release);
|
|
87452
87715
|
const guard = checkDowngrade({
|
|
@@ -87472,7 +87735,7 @@ async function doInstall2(opts, program3) {
|
|
|
87472
87735
|
const bak = backupExistingCompose2();
|
|
87473
87736
|
if (bak)
|
|
87474
87737
|
console.log(source_default.dim(` Backed up existing compose to ${bak}`));
|
|
87475
|
-
|
|
87738
|
+
writeFileSync43(composePath, yaml, "utf8");
|
|
87476
87739
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
87477
87740
|
console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
|
|
87478
87741
|
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${imageTag}\u2026`));
|