@coreplane/switchboard 1.237.0 → 1.238.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/assets/deploy/cloudflare-resident/worker.ts +35 -12
  2. package/dist/assets/package-lock.json +3 -3
  3. package/dist/assets/package.json +1 -1
  4. package/dist/assets/source.json +3 -3
  5. package/dist/assets/src/execution/snapshotRetention.ts +62 -0
  6. package/dist/assets/src/mcp/registry.ts +9 -1
  7. package/dist/assets/web/dist/.vite/manifest.json +27 -27
  8. package/dist/assets/web/dist/assets/{ResidentDetailPage-Dc23v_8v.js → ResidentDetailPage-CrfCd8fx.js} +1 -1
  9. package/dist/assets/web/dist/assets/{ResidentsIndexPage-TR5D1TRR.js → ResidentsIndexPage-BdW0k2h3.js} +1 -1
  10. package/dist/assets/web/dist/assets/{RunFoldRow-mkUj7WEN.js → RunFoldRow-CbTd9SiS.js} +1 -1
  11. package/dist/assets/web/dist/assets/{RunRoutePage-CQI17__g.js → RunRoutePage-br0H-8Pz.js} +3 -3
  12. package/dist/assets/web/dist/assets/{RunsIndexPage-yIP4PlKP.js → RunsIndexPage-Bnzb15zH.js} +1 -1
  13. package/dist/assets/web/dist/assets/{ScheduledPage-CdCVyNbh.js → ScheduledPage-NLhjzHAz.js} +1 -1
  14. package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +1 -0
  15. package/dist/assets/web/dist/assets/{StatusDot-DmNHX6am.js → StatusDot-DkXALvKf.js} +1 -1
  16. package/dist/assets/web/dist/assets/{Tooltip-qVnJTgSt.js → Tooltip-DtLIyi5P.js} +1 -1
  17. package/dist/assets/web/dist/assets/{UnitRoutePage-BcTeH7m2.js → UnitRoutePage-DGXukEAH.js} +1 -1
  18. package/dist/assets/web/dist/assets/{dist-TAGawIfM.js → dist-DtA7JiH5.js} +1 -1
  19. package/dist/assets/web/dist/assets/main-CFHF2RvQ.css +1 -0
  20. package/dist/assets/web/dist/assets/{main-mUd_TJjH.js → main-Dht0in4v.js} +2 -2
  21. package/dist/cli.js +184 -23
  22. package/package.json +1 -1
  23. package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +0 -1
  24. package/dist/assets/web/dist/assets/main-csihqEj5.css +0 -1
package/dist/cli.js CHANGED
@@ -3269,7 +3269,7 @@ function isOutcome(v) {
3269
3269
  function isMcpServerEntry(v) {
3270
3270
  if (!v || typeof v !== "object" || Array.isArray(v)) return false;
3271
3271
  const e = v;
3272
- return isStr(e.url, MCP_URL_MAX) && (e.agents === void 0 || Array.isArray(e.agents) && e.agents.length > 0 && e.agents.length <= MCP_AGENTS_MAX && e.agents.every((a) => isStr(a, 32))) && (e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") && (e.tokenEnv === void 0 || isStr(e.tokenEnv, 128)) && (e.headersEnv === void 0 || isHeadersEnv(e.headersEnv)) && (e.addedBy === void 0 || isStr(e.addedBy, 260)) && (e.addedAt === void 0 || isNum(e.addedAt));
3272
+ return isStr(e.url, MCP_URL_MAX) && (e.agents === void 0 || Array.isArray(e.agents) && e.agents.length > 0 && e.agents.length <= MCP_AGENTS_MAX && e.agents.every((a) => isStr(a, 32))) && (e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") && (e.tokenEnv === void 0 || isStr(e.tokenEnv, 128)) && (e.headersEnv === void 0 || isHeadersEnv(e.headersEnv)) && (e.addedBy === void 0 || isStr(e.addedBy, 260)) && (e.addedAt === void 0 || isNum(e.addedAt)) && (e.promotedFrom === void 0 || isStr(e.promotedFrom, 260));
3273
3273
  }
3274
3274
  function isSealedCredential(v) {
3275
3275
  if (!v || typeof v !== "object") return false;
@@ -3295,7 +3295,8 @@ function serverView(scopeKey, name, entry, opts) {
3295
3295
  state,
3296
3296
  source: opts.source,
3297
3297
  ...entry.addedBy ? { addedBy: entry.addedBy } : {},
3298
- ...entry.addedAt !== void 0 ? { addedAt: entry.addedAt } : {}
3298
+ ...entry.addedAt !== void 0 ? { addedAt: entry.addedAt } : {},
3299
+ ...entry.promotedFrom ? { promotedFrom: entry.promotedFrom } : {}
3299
3300
  };
3300
3301
  }
3301
3302
  function safeUrl(raw) {
@@ -4156,6 +4157,15 @@ var init_config2 = __esm({
4156
4157
  * caller's right to read each channel is decided; `config show --channel`
4157
4158
  * is the read that carries the values.
4158
4159
  */
4160
+ /** Every channel and every user with a tier of its own — a static block or a
4161
+ * runtime override — sorted: what `mcp list --all` walks (record 0042). */
4162
+ mcpTierIds() {
4163
+ const ids = (stat, runtime) => [.../* @__PURE__ */ new Set([...Object.keys(stat ?? {}), ...Object.keys(runtime)])].sort();
4164
+ return {
4165
+ channels: ids(this.config.channels, this.overrides.channels),
4166
+ users: ids(this.config.users, this.overrides.users)
4167
+ };
4168
+ }
4159
4169
  channelsWithScope() {
4160
4170
  const ids = /* @__PURE__ */ new Set([...Object.keys(this.config.channels ?? {}), ...Object.keys(this.overrides.channels)]);
4161
4171
  const rows = [];
@@ -18981,6 +18991,25 @@ var init_service = __esm({
18981
18991
  for (const r of resolved) out.push(await this.view(r));
18982
18992
  return out;
18983
18993
  }
18994
+ /** Every tier there is — the org, every channel, every user (static and runtime) — for an
18995
+ * actor with org rights; anyone else is refused (record 0042: a non-admin never sees another
18996
+ * person's tier). Each row carries `addedByName` when the lookup answers. */
18997
+ async listAll(actor) {
18998
+ if (!actor.orgAdmin)
18999
+ throw new McpServiceError(
19000
+ "unauthorized",
19001
+ "listing every tier's MCP servers takes admin rights (repo-management rights). `mcp list` shows the org's, this channel's and your own."
19002
+ );
19003
+ const { channels, users } = this.opts.config.mcpTierIds();
19004
+ const targets = [
19005
+ { kind: "org" },
19006
+ ...channels.map((id) => ({ kind: "channel", id })),
19007
+ ...users.map((id) => ({ kind: "user", id }))
19008
+ ];
19009
+ const out = [];
19010
+ for (const target of targets) for (const r of this.tierEntries(target)) out.push(await this.view(r));
19011
+ return out;
19012
+ }
18984
19013
  async add(actor, target, input) {
18985
19014
  const scopeKey = mcpScopeKey(target.kind, target.id);
18986
19015
  try {
@@ -19095,6 +19124,55 @@ var init_service = __esm({
19095
19124
  return { ...view, probe: { ok: false, error: (err2 instanceof Error ? err2.message : String(err2)).slice(0, 300) } };
19096
19125
  }
19097
19126
  }
19127
+ /** `mcp promote <name> --from <user>` (record 0042): copy a person's runtime entry — `url`,
19128
+ * `auth`, `agents` (widened only when the admin passes some) — into the org tier under the
19129
+ * same name, `addedBy` the admin and `promotedFrom` the person, and for bearer/oauth mint a
19130
+ * fresh ORG connect ticket exactly as `mcp add --scope org` would. The person's sealed
19131
+ * credential is never read, unsealed or written: it is theirs, and the org's calls must not
19132
+ * run as them. The personal entry stays (shadowed by the org tier at run time). */
19133
+ async promote(actor, name, from, opts = {}) {
19134
+ const org = this.target(actor, "org", void 0);
19135
+ const source2 = this.tierEntries({ kind: "user", id: from }).find((r) => r.name === name);
19136
+ if (!source2)
19137
+ throw new McpServiceError(
19138
+ "not_found",
19139
+ `no MCP server named "${name}" in the user scope of ${from} (see \`mcp list --all\`)`
19140
+ );
19141
+ if (source2.source === "config")
19142
+ throw new McpServiceError(
19143
+ "conflict",
19144
+ `"${name}" is pinned in config.yaml under users.${from} \u2014 move it to defaults.mcpServers there`
19145
+ );
19146
+ const runtime = this.opts.config.runtimeScope("org");
19147
+ if (runtime.mcpServers?.[name] || this.opts.config.isStaticMcpServer("org", void 0, name))
19148
+ throw new McpServiceError(
19149
+ "conflict",
19150
+ `an org MCP server named "${name}" already exists \u2014 remove it first or pick another name`
19151
+ );
19152
+ if (Object.keys(runtime.mcpServers ?? {}).length >= MCP_SERVERS_PER_SCOPE_MAX)
19153
+ throw new McpServiceError("invalid_input", `the org scope already has ${MCP_SERVERS_PER_SCOPE_MAX} servers`);
19154
+ const agents = this.checkAgents("org", opts.agents ?? source2.entry.agents);
19155
+ if (source2.entry.auth !== "none") this.requireCredentialSupport(source2.entry.auth);
19156
+ const entry = {
19157
+ url: source2.entry.url,
19158
+ agents,
19159
+ auth: source2.entry.auth,
19160
+ addedBy: actor.id,
19161
+ addedAt: this.now(),
19162
+ promotedFrom: from
19163
+ };
19164
+ await this.writeServers(org, { ...runtime.mcpServers, [name]: entry });
19165
+ const view = serverView("org", name, entry, { hasCredential: false, source: "runtime" });
19166
+ if (entry.auth === "none") return { server: view, promotedFrom: from };
19167
+ const ticket = await this.mintTicket(mcpCredentialKey("org", name), actor);
19168
+ return {
19169
+ server: view,
19170
+ promotedFrom: from,
19171
+ connectUrl: this.connectUrl(ticket),
19172
+ expiresAt: ticket.expiresAt,
19173
+ expiresInMinutes: TICKET_MINUTES
19174
+ };
19175
+ }
19098
19176
  // ---- the connect page ----------------------------------------------------------
19099
19177
  /** GET: binding an unbound ticket is a compare-and-swap on its state; when
19100
19178
  * another opener won the race the decision is re-planned against the
@@ -19464,7 +19542,7 @@ var init_service = __esm({
19464
19542
  return `${(this.opts.publicBaseUrl ?? "").replace(/\/+$/, "")}/mcp/connect/${ticket.nonce}`;
19465
19543
  }
19466
19544
  async mintTicket(serverId, actor) {
19467
- const email = this.opts.resolveEmail ? await this.opts.resolveEmail(actor.id).catch(() => void 0) : void 0;
19545
+ const email = actor.email ?? (this.opts.resolveEmail ? await this.opts.resolveEmail(actor.id).catch(() => void 0) : void 0);
19468
19546
  const ticket = newTicket({
19469
19547
  nonce: this.nonce(),
19470
19548
  serverId,
@@ -19521,7 +19599,9 @@ var init_service = __esm({
19521
19599
  }
19522
19600
  async view(r) {
19523
19601
  const hasCredential = r.entry.auth !== "none" && !r.entry.tokenEnv ? await this.viaSecrets(() => this.opts.secrets.getCredential(mcpCredentialKey(r.scopeKey, r.name))) !== null : false;
19524
- return serverView(r.scopeKey, r.name, r.entry, { hasCredential, source: r.source });
19602
+ const view = serverView(r.scopeKey, r.name, r.entry, { hasCredential, source: r.source });
19603
+ const name = view.addedBy && this.opts.resolveName ? await this.opts.resolveName(view.addedBy).catch(() => void 0) : void 0;
19604
+ return name ? { ...view, addedByName: name } : view;
19525
19605
  }
19526
19606
  async specFor(r) {
19527
19607
  const id = mcpCredentialKey(r.scopeKey, r.name);
@@ -19622,6 +19702,8 @@ function actorOf(caller, channel) {
19622
19702
  // The person, for a dashboard session linked to one (record 0042): its
19623
19703
  // own tier, `addedBy` and the connect ticket's email are the person's.
19624
19704
  id: meIdOf(caller) ?? caller.id,
19705
+ // A dashboard session's own email binds the tickets it mints, linked or not.
19706
+ ...caller.email ? { email: caller.email } : {},
19625
19707
  orgAdmin: authorize(caller.actor, "mcp:write", { type: "config-scope", kind: "org" }).allow,
19626
19708
  channelAdmin: authorize(caller.actor, "mcp:write", { type: "config-scope", kind: "channel", id: channel ?? "" }).allow
19627
19709
  };
@@ -19672,7 +19754,10 @@ function renderServerLine(r) {
19672
19754
  const state = r.state === "connected" ? "\u2705 connected" : r.state === "static" ? "\u2705 static (bot env)" : "\u23F3 awaiting credential";
19673
19755
  const agents = Array.isArray(r.agents) ? r.agents.join(", ") : "";
19674
19756
  const pinned = r.source === "config" ? " \xB7 pinned in config.yaml" : "";
19675
- return `\u2022 \`${String(r.name)}\` (${String(r.scope)}) ${state} \u2014 ${String(r.url)} \xB7 agents: ${agents} \xB7 auth: ${String(r.auth)}${pinned}`;
19757
+ const by = typeof r.addedBy === "string" ? ` \xB7 added by ${typeof r.addedByName === "string" ? r.addedByName : r.addedBy}` : "";
19758
+ const from = typeof r.promotedFrom === "string" ? ` \xB7 promoted from ${r.promotedFrom}` : "";
19759
+ const owner = r.scope === "user" && typeof r.scopeKey === "string" ? ` \xB7 tier of ${r.scopeKey.slice("user:".length)}` : "";
19760
+ return `\u2022 \`${String(r.name)}\` (${String(r.scope)}) ${state} \u2014 ${String(r.url)} \xB7 agents: ${agents} \xB7 auth: ${String(r.auth)}${pinned}${by}${from}${owner}`;
19676
19761
  }
19677
19762
  function renderList(output) {
19678
19763
  const o = output;
@@ -19712,7 +19797,7 @@ function renderShow(output) {
19712
19797
  function registerMcpCommands(registry) {
19713
19798
  for (const cmd of MCP_COMMANDS) registry.register(cmd);
19714
19799
  }
19715
- var defineCommand10, serverName, serverUrl, scopeOption, channelOption2, nameArg, MCP_ME_ON_ACCESS_MESSAGE, mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove, MCP_COMMANDS;
19800
+ var defineCommand10, serverName, serverUrl, scopeOption, channelOption2, nameArg, USER_ID_PATTERN, MCP_ME_ON_ACCESS_MESSAGE, mcpList, mcpPromote, mcpAdd, mcpConnect, mcpShow, mcpRemove, MCP_COMMANDS;
19716
19801
  var init_mcp = __esm({
19717
19802
  "../../src/core/commands/mcp.ts"() {
19718
19803
  "use strict";
@@ -19729,22 +19814,53 @@ var init_mcp = __esm({
19729
19814
  );
19730
19815
  channelOption2 = z13.string().optional().describe("target channel for `--scope channel` and for `list` (default: the channel you are speaking in)");
19731
19816
  nameArg = { name: "name", schema: serverName, describe: "the server's name (see `mcp list`)" };
19817
+ USER_ID_PATTERN = /^[a-z]+:[A-Za-z0-9_.@-]{1,200}$/;
19732
19818
  MCP_ME_ON_ACCESS_MESSAGE = "Personal MCP servers are added in chat (`mcp add <name> --url \u2026`): your runs are requested as your chat user, not as this browser session, so a server added here for yourself would reach no run. Pass --scope org or --scope channel.";
19733
19819
  mcpList = defineCommand10({
19734
19820
  id: "mcp.list",
19735
19821
  enabledWhen: (caps) => caps.mcp,
19736
- options: z13.object({ channel: channelOption2 }),
19822
+ options: z13.object({
19823
+ channel: channelOption2,
19824
+ all: flag.optional().describe("every tier there is \u2014 the org's, every channel's, every user's (admins)")
19825
+ }),
19737
19826
  action: "mcp:read",
19738
19827
  effect: "read",
19739
- describe: "External MCP servers your runs in this channel can use \u2014 org-wide, this channel's, and your own \u2014 with state and agents; never a credential.",
19828
+ describe: "External MCP servers your runs in this channel can use \u2014 org-wide, this channel's, and your own \u2014 with state and agents; never a credential. `--all` (admins): every tier.",
19740
19829
  render: renderList,
19741
19830
  handler: async ({ options, caller, deps }) => {
19742
19831
  const svc = await serviceOf(deps);
19743
19832
  const channel = channelOf2(caller, options.channel);
19744
- const servers = await via(() => svc.list(actorOf(caller, channel), channel));
19833
+ const actor = actorOf(caller, channel);
19834
+ const servers = await via(() => options.all ? svc.listAll(actor) : svc.list(actor, channel));
19745
19835
  return { servers };
19746
19836
  }
19747
19837
  });
19838
+ mcpPromote = defineCommand10({
19839
+ id: "mcp.promote",
19840
+ enabledWhen: (caps) => caps.mcp,
19841
+ args: [nameArg],
19842
+ options: z13.object({
19843
+ from: z13.string().refine((s) => USER_ID_PATTERN.test(s), "expected a platform-namespaced user id (slack:U\u2026)").describe("whose personal server to promote (`slack:U\u2026`, as `mcp list --all` shows it)"),
19844
+ agents: z13.string().transform(
19845
+ (s) => s.split(",").map((a) => a.trim()).filter(Boolean)
19846
+ ).optional().describe("widen the agents (default: the personal entry's); an org server may name coding/review/ship")
19847
+ }),
19848
+ action: "mcp:write",
19849
+ // Org-only by definition (there is no `me` alternative to point at), so the gate is the
19850
+ // table's, at the door: the same `mcp:write` on `config-scope { org }` that `mcp add --scope org` asks.
19851
+ resource: () => ({ type: "config-scope", kind: "org" }),
19852
+ effect: "write",
19853
+ describe: "Re-issue a person's MCP server in the org tier (admins): the same name, URL and auth, added by you; a bearer/oauth server gets a fresh org connect link for you to complete \u2014 the person's credential is never copied.",
19854
+ render: renderAdd,
19855
+ settle: (output, { deps }) => settleConnect(deps, output),
19856
+ handler: async ({ args, options, caller, deps }) => {
19857
+ const svc = await serviceOf(deps);
19858
+ const actor = actorOf(caller, channelOf2(caller, void 0));
19859
+ return await via(
19860
+ () => svc.promote(actor, args.name, options.from, options.agents ? { agents: options.agents } : {})
19861
+ );
19862
+ }
19863
+ });
19748
19864
  mcpAdd = defineCommand10({
19749
19865
  id: "mcp.add",
19750
19866
  enabledWhen: (caps) => caps.mcp,
@@ -19842,7 +19958,7 @@ var init_mcp = __esm({
19842
19958
  return await via(() => svc.remove(actor, target, args.name));
19843
19959
  }
19844
19960
  });
19845
- MCP_COMMANDS = [mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove];
19961
+ MCP_COMMANDS = [mcpList, mcpAdd, mcpConnect, mcpShow, mcpRemove, mcpPromote];
19846
19962
  }
19847
19963
  });
19848
19964
 
@@ -26175,7 +26291,12 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs, recor
26175
26291
  } else {
26176
26292
  out.prUnpostable = { number: inherited.number, reason: head.reason };
26177
26293
  if (inherited.source === "record" && head.reason === "closed" && head.sha !== void 0) {
26178
- out.closedRecordPr = { number: inherited.number, headSha: head.sha, merged: head.merged };
26294
+ out.closedRecordPr = {
26295
+ number: inherited.number,
26296
+ headSha: head.sha,
26297
+ ...head.ref !== void 0 ? { headRef: head.ref } : {},
26298
+ merged: head.merged
26299
+ };
26179
26300
  }
26180
26301
  }
26181
26302
  }
@@ -26188,11 +26309,21 @@ function ownPrOf(ctx) {
26188
26309
  }
26189
26310
  function recordPrOf(ctx) {
26190
26311
  if (ctx.prFromRecord && ctx.pr !== void 0 && ctx.headSha !== void 0) {
26191
- return { number: ctx.pr, headSha: ctx.headSha, state: "open" };
26312
+ return {
26313
+ number: ctx.pr,
26314
+ headSha: ctx.headSha,
26315
+ ...ctx.refFromPr && ctx.ref !== void 0 ? { headBranch: ctx.ref } : {},
26316
+ state: "open"
26317
+ };
26192
26318
  }
26193
26319
  if (ctx.closedRecordPr !== void 0) {
26194
- const { number, headSha, merged } = ctx.closedRecordPr;
26195
- return { number, headSha, state: merged ? "merged" : "closed" };
26320
+ const { number, headSha, headRef, merged } = ctx.closedRecordPr;
26321
+ return {
26322
+ number,
26323
+ headSha,
26324
+ ...headRef !== void 0 ? { headBranch: headRef } : {},
26325
+ state: merged ? "merged" : "closed"
26326
+ };
26196
26327
  }
26197
26328
  return void 0;
26198
26329
  }
@@ -26204,8 +26335,14 @@ function inheritedPr(thread, records, repo) {
26204
26335
  }
26205
26336
  async function openPrHeadSha(pr) {
26206
26337
  const head = await prHead(pr).catch(() => void 0);
26207
- if (head?.state === "closed")
26208
- return { reason: "closed", ...head.sha ? { sha: head.sha } : {}, merged: head.merged };
26338
+ if (head?.state === "closed") {
26339
+ return {
26340
+ reason: "closed",
26341
+ ...head.sha ? { sha: head.sha } : {},
26342
+ ...head.ref ? { ref: head.ref } : {},
26343
+ merged: head.merged
26344
+ };
26345
+ }
26209
26346
  if (head?.state === "open" && head.sha) {
26210
26347
  return {
26211
26348
  sha: head.sha,
@@ -27053,7 +27190,7 @@ var init_types5 = __esm({
27053
27190
 
27054
27191
  // ../../src/core/dispatch/commandRun.ts
27055
27192
  function isInlineRunCommand(id) {
27056
- return id.startsWith("friction.") || id === "memory.forget" || /^repo\.(onboard|offboard|rebuild|reconfigure|test|build)$/.test(id) || /^mcp\.(add|connect|remove)$/.test(id);
27193
+ return id.startsWith("friction.") || id === "memory.forget" || /^repo\.(onboard|offboard|rebuild|reconfigure|test|build)$/.test(id) || /^mcp\.(add|connect|remove|promote)$/.test(id);
27057
27194
  }
27058
27195
  async function runChatCommand(deps, msg, io, parsed, ending, trace, opts = {}) {
27059
27196
  const commands = deps.commands;
@@ -36526,7 +36663,8 @@ async function runCodingPrPostStep(input) {
36526
36663
  const base = prDescription ? await resolveBaseRefLazy(candidates, repo, input.fetchRepoInfo) : resolveBaseRef(candidates, void 0);
36527
36664
  const pushedBranch = branch2 !== void 0 && branch2 !== base && pushed;
36528
36665
  const ownPr = target.ownPr;
36529
- const pushedPastOwnPr = ownPr !== void 0 && pushed && headSha !== void 0 && !sameCommit(headSha, ownPr.headSha) ? headSha : void 0;
36666
+ const onOwnPrBranch = ownPr?.headBranch !== void 0 && branch2 === ownPr.headBranch;
36667
+ const pushedPastOwnPr = ownPr !== void 0 && onOwnPrBranch && pushed && headSha !== void 0 && !sameCommit(headSha, ownPr.headSha) ? headSha : void 0;
36530
36668
  if (prDescription && ownPr !== void 0 && ownPr.state !== "open" && pushedPastOwnPr !== void 0 && branch2 !== void 0) {
36531
36669
  const url = `https://github.com/${repo}/pull/${ownPr.number}`;
36532
36670
  console.log(
@@ -36540,7 +36678,7 @@ async function runCodingPrPostStep(input) {
36540
36678
  });
36541
36679
  return `\u26A0\uFE0F A PR description was submitted and \`${branch2}\`${branchNote} was pushed to \`${pushedPastOwnPr.slice(0, 7)}\`, but this thread's pull request ${url} is ${ownPr.state} and that branch is its head \u2014 the new commits need a new pull request, so nothing was edited: compare & open manually: ${compareUrl}`;
36542
36680
  }
36543
- if (prDescription && ownPr !== void 0 && (branch2 === base || ownPr.state !== "open")) {
36681
+ if (prDescription && ownPr !== void 0 && (branch2 === base || ownPr.state !== "open" && onOwnPrBranch)) {
36544
36682
  const { number, headSha: prHead2, state } = ownPr;
36545
36683
  const url = `https://github.com/${repo}/pull/${number}`;
36546
36684
  const standing = state === "open" ? "" : ` (${state})`;
@@ -41837,7 +41975,8 @@ function buildMcp(config, secrets, opts = {}) {
41837
41975
  fetch: webFetch,
41838
41976
  publicBaseUrl: opts.publicBaseUrl,
41839
41977
  bearers: secrets,
41840
- resolveEmail: opts.resolveEmail
41978
+ resolveEmail: opts.resolveEmail,
41979
+ resolveName: opts.resolveName
41841
41980
  });
41842
41981
  return { source: service.source, service };
41843
41982
  }
@@ -45351,15 +45490,28 @@ function createSettingsViewHandler(deps, shell) {
45351
45490
  action,
45352
45491
  kind === "org" ? { type: "config-scope", kind: "org" } : { type: "config-scope", kind: "channel", id: id ?? "" }
45353
45492
  ).allow;
45493
+ const serversOf = (answer) => answer.ok ? answer.value.servers ?? [] : [];
45354
45494
  async function mcpsSeed(caller, channel) {
45355
- const listed = await deps.commands.invoke("mcp.list", channel ? { options: { channel } } : {}, caller);
45356
45495
  const write = {
45357
45496
  org: canWrite(caller, "mcp:write", "org"),
45358
45497
  channel: channel !== void 0 && canWrite(caller, "mcp:write", "channel", channel)
45359
45498
  };
45499
+ const listed = await deps.commands.invoke(
45500
+ "mcp.list",
45501
+ write.org ? { options: { all: true } } : channel ? { options: { channel } } : {},
45502
+ caller
45503
+ );
45360
45504
  if (!listed.ok)
45361
45505
  return { ...channel ? { channel } : {}, servers: [], unavailable: failureText(listed), canWrite: write };
45362
- const servers = listed.value.servers ?? [];
45506
+ if (write.org)
45507
+ return { ...channel ? { channel } : {}, allTiers: true, servers: serversOf(listed), canWrite: write };
45508
+ const servers = serversOf(listed);
45509
+ const readable = await deps.commands.invoke("config.overrides", {}, caller);
45510
+ const others = (readable.ok ? readable.value.channels ?? [] : []).filter((r) => r.channelId !== channel && r.settings.includes("mcpServers")).map((r) => r.channelId);
45511
+ const tiers = await Promise.all(
45512
+ others.map((id) => deps.commands.invoke("mcp.list", { options: { channel: id } }, caller))
45513
+ );
45514
+ for (const answer of tiers) for (const s of serversOf(answer)) if (s.scope === "channel") servers.push(s);
45363
45515
  return { ...channel ? { channel } : {}, servers, canWrite: write };
45364
45516
  }
45365
45517
  async function channelsSeed(caller, channel) {
@@ -48291,7 +48443,12 @@ async function resolveAccessActor(identity2, opts) {
48291
48443
  };
48292
48444
  }
48293
48445
  async function callerFor(identity2, opts) {
48294
- return { kind: "access", id: callerIdFor(identity2), actor: await resolveAccessActor(identity2, opts) };
48446
+ return {
48447
+ kind: "access",
48448
+ id: callerIdFor(identity2),
48449
+ actor: await resolveAccessActor(identity2, opts),
48450
+ ...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
48451
+ };
48295
48452
  }
48296
48453
  function hostOf2(url) {
48297
48454
  if (!url) return void 0;
@@ -48446,11 +48603,14 @@ async function runBot() {
48446
48603
  const completions = new PiAiProviders(config.config.providers);
48447
48604
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
48448
48605
  let slackEmailLookup;
48606
+ let slackNameLookup;
48449
48607
  let slackPersonByEmail;
48450
48608
  const personByEmail = (email) => slackPersonByEmail ? slackPersonByEmail(email) : Promise.resolve(void 0);
48451
48609
  const mcpWiring = buildMcp(config, processSecrets, {
48452
48610
  publicBaseUrl: process.env.PUBLIC_BASE_URL,
48453
48611
  resolveEmail: (userId) => slackEmailLookup ? slackEmailLookup(userId) : Promise.resolve(void 0),
48612
+ // `addedBy` as a name on the MCP lists (record 0042): the cached lookup the runs index uses.
48613
+ resolveName: (userId) => slackNameLookup ? slackNameLookup(userId) : Promise.resolve(void 0),
48454
48614
  warn: (m) => console.warn(`[mcp] ${m}`)
48455
48615
  });
48456
48616
  const mcp = mcpWiring.source ?? new NullMcpToolSource();
@@ -48646,6 +48806,7 @@ async function runBot() {
48646
48806
  const conversationReader = new SlackConversationReader(app.client);
48647
48807
  deps.conversationReaders = [conversationReader];
48648
48808
  slackEmailLookup = (userId) => userId.startsWith("slack:") ? resolveUserEmail(app.client, userId.slice("slack:".length)) : Promise.resolve(void 0);
48809
+ slackNameLookup = (userId) => userId.startsWith("slack:") ? resolveUserName(app.client, userId.slice("slack:".length)) : Promise.resolve(void 0);
48649
48810
  slackPersonByEmail = (email) => resolvePersonByEmail(app.client, email);
48650
48811
  const pendingHistoryWrites = () => runHistoryWriter.pending();
48651
48812
  const inFlight = () => activeRunCount() + pendingReflectionCount() + pendingHistoryWrites();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.237.0",
3
+ "version": "1.238.0",
4
4
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://openswitchboard.dev",
@@ -1 +0,0 @@
1
- import{i as e,n as t,t as n}from"./AppShell-MYHNxJks.js";import{Bt as r,Lr as i,Vt as a,Wt as o,Xt as s,_n as c,a as l,ci as u,cn as d,gn as f,ir as p,ln as m,oi as h,on as g,r as _,sn as v,un as y,ur as b,vi as x,wi as S,xi as C,yn as w,zr as T}from"./seed-DeKSHrt2.js";async function E(e,t,n){try{let r=await e(`/api/${t}`,{method:`POST`,credentials:`same-origin`,headers:{"content-type":`application/json`},body:JSON.stringify(n)}),i=await r.json().catch(()=>null);return r.ok?{ok:!0,value:i}:{ok:!1,failure:{error:typeof i?.error==`string`?i.error:`HTTP ${r.status}`,message:typeof i?.message==`string`?i.message:`HTTP ${r.status}`}}}catch(e){return{ok:!1,failure:{error:`network`,message:e instanceof Error?e.message:String(e)}}}}async function D(e,t,n){let r=new URLSearchParams;for(let[e,t]of Object.entries(n))t!==void 0&&t!==``&&r.set(e,t);let i=r.size>0?`?${r.toString()}`:``;try{let n=await e(`/api/${t}${i}`,{credentials:`same-origin`}),r=await n.json().catch(()=>null);return n.ok?{ok:!0,value:r}:{ok:!1,failure:{error:typeof r?.error==`string`?r.error:`HTTP ${n.status}`,message:typeof r?.message==`string`?r.message:`HTTP ${n.status}`}}}catch(e){return{ok:!1,failure:{error:`network`,message:e instanceof Error?e.message:String(e)}}}}var O=`min-w-0 rounded border border-accented bg-default px-2 py-1 text-sm text-highlighted placeholder:text-dimmed disabled:opacity-60`,k=`rounded border border-accented bg-default px-2 py-1 font-mono text-xs text-toned disabled:opacity-60`,A={class:`grid gap-4`},j={class:`grid gap-3 lg:grid-cols-[minmax(16rem,22rem)_1fr]`},M={class:`grid content-start gap-3`},N={class:`overflow-hidden rounded-lg border border-default bg-elevated`},P={key:0,class:`unavailable px-4 py-3 text-sm text-warn`},F={key:1,class:`empty px-4 py-3 text-sm text-muted`},I={key:2,class:`index text-sm`},L=[`data-channel`],R=[`href`,`aria-current`],z={class:`font-mono text-xs font-medium text-highlighted`},B={class:`text-xs text-muted`},V={key:0,class:`placeholder rounded-lg border border-dashed border-default px-5 py-8 text-center text-sm text-muted`},H={key:1,class:`refused rounded-lg border border-warn/30 bg-warn/10 px-5 py-4 text-sm`},U={class:`font-mono text-xs`},W={key:2,class:`scope grid gap-4`},G={class:`flex flex-wrap items-baseline justify-between gap-2`},K={class:`font-mono text-sm font-medium text-highlighted`},q={class:`effective text-xs text-muted`},ee={key:0,class:`restricted text-xs text-muted`},te={class:`grid gap-3 sm:grid-cols-[9rem_1fr]`},ne=[`disabled`],re={value:``},ie=[`value`,`disabled`],ae=[`disabled`],oe={class:`grid gap-1.5`},se={class:`text-muted`},ce=[`onUpdate:modelValue`,`name`,`placeholder`,`disabled`],le=[`disabled`],ue=[`value`],de={class:`grid gap-1.5`},fe={class:`text-muted`},pe=[`onUpdate:modelValue`,`name`,`disabled`],me=[`value`],he={class:`grid gap-3 sm:grid-cols-[9rem_1fr]`},ge=[`disabled`],_e=[`disabled`],ve=[`value`],ye={class:`flex flex-wrap gap-x-4 gap-y-1`},be=[`name`,`checked`,`disabled`,`onChange`],xe={class:`flex flex-wrap items-center gap-3`},Se=[`disabled`],Ce={class:`flex flex-wrap items-center gap-3`},we={class:`text-xs text-dimmed tabular-nums`},J=w({__name:`ChannelsPanel`,props:{channels:{},vocabulary:{},fetch:{type:Function}},setup(e){let n=e,d=(e,t)=>(n.fetch??((e,t)=>globalThis.fetch(e,t)))(e,t),_=g(()=>n.channels.selected??null),w=g(()=>_.value?.scope?.channel??null),D=u(``);function J(){let e=D.value.trim();e&&t.navigate(`/settings/channels/${encodeURIComponent(e)}`)}let Y=h({agent:w.value?.agent??``,model:w.value?.model??``,models:Object.fromEntries(n.vocabulary.agents.map(e=>[e,w.value?.models?.[e]??``])),effort:w.value?.effort??``,efforts:Object.fromEntries(n.vocabulary.agents.map(e=>[e,w.value?.efforts?.[e]??``])),maxMinutes:w.value?.boundary?.maxMinutes?.toString()??``,maxIdentity:w.value?.boundary?.maxIdentity??``,machines:new Set(w.value?.boundary?.machines??[]),instructions:w.value?.instructions??``}),X=u(!1),Z=u(null),Q=g(()=>_.value?.canWrite===!0);function Te(e,t){t?Y.machines.add(e):Y.machines.delete(e)}function Ee(){let e={};Y.agent&&(e.agent=Y.agent),Y.model.trim()&&(e.model=Y.model.trim());let t=Object.fromEntries(Object.entries(Y.models).filter(([,e])=>e.trim()));Object.keys(t).length&&(e.models=t),Y.effort&&(e.effort=Y.effort);let n=Object.fromEntries(Object.entries(Y.efforts).filter(([,e])=>e));Object.keys(n).length&&(e.efforts=n);let r={};return Y.maxMinutes.trim()&&(r.maxMinutes=Number(Y.maxMinutes)),Y.maxIdentity&&(r.maxIdentity=Y.maxIdentity),Y.machines.size&&(r.machines=[...Y.machines].join(`,`)),Object.keys(r).length&&(e.boundary=r),e}async function $(e,n,r){if(!_.value)return;X.value=!0,Z.value=null;let i=await E(d,e,{scope:`channel`,channel:_.value.channelId,...n});if(X.value=!1,!i.ok){Z.value={kind:`error`,text:i.failure.message};return}Z.value={kind:`ok`,text:r},t.reload()}let De=()=>$(`config.set`,Ee(),`Saved.`),Oe=()=>$(`config.instructions`,{text:Y.instructions.trim()},`Instructions saved.`),ke=()=>$(`config.instructions`,{text:``},`Instructions cleared.`);async function Ae(){_.value&&t.confirm(`Drop every runtime override of ${_.value.channelId}? Static config.yaml values show through again.`)&&await $(`config.clear`,{},`Cleared.`)}let je={config:`config.yaml`,runtime:`runtime`,both:`config.yaml + runtime`};return(t,n)=>{let u=l;return p(),y(`section`,A,[n[31]||=v(`p`,{class:`text-sm text-muted`},[f(` A channel's scope sets which agent, model and effort its runs get, caps every run there, and adds advisory instructions. It layers over the installation defaults; a person's own settings (`),v(`code`,null,`config set me`),f(` in chat) layer over it. `)],-1),v(`div`,j,[v(`aside`,M,[v(`div`,N,[n[7]||=v(`h2`,{class:`border-b border-muted px-4 py-2 font-mono text-[0.6875rem] font-medium uppercase tracking-widest text-dimmed`},` Configured channels `,-1),e.channels.unavailable?(p(),y(`p`,P,S(e.channels.unavailable),1)):e.channels.index.length===0?(p(),y(`p`,F,` No channel carries a scope yet. `)):(p(),y(`ul`,I,[(p(!0),y(s,null,b(e.channels.index,e=>(p(),y(`li`,{key:e.channelId,class:`border-b border-muted last:border-0`,"data-channel":e.channelId},[v(`a`,{class:`grid gap-0.5 px-4 py-2 no-underline hover:bg-(--ui-bg-muted)`,href:`/settings/channels/${encodeURIComponent(e.channelId)}`,"aria-current":_.value?.channelId===e.channelId?`page`:void 0},[v(`span`,z,S(e.channelId),1),v(`span`,B,S(e.settings.join(`, `))+` · `+S(je[e.source]),1)],8,R)],8,L))),128))]))]),v(`form`,{class:`flex items-center gap-2`,onSubmit:o(J,[`prevent`])},[T(v(`input`,{id:`channel-open`,"onUpdate:modelValue":n[0]||=e=>D.value=e,class:C([x(O),`flex-1 font-mono text-xs`]),placeholder:`slack:C0123… — a channel with no scope yet`,"aria-label":`channel id to open`},null,2),[[a,D.value]]),c(u,{type:`submit`,size:`xs`,color:`neutral`,variant:`outline`},{default:i(()=>[...n[8]||=[f(`Open`,-1)]]),_:1})],32)]),_.value?_.value.refused?(p(),y(`div`,H,[v(`span`,U,S(_.value.channelId),1),f(` · `+S(_.value.refused),1)])):w.value&&_.value.scope?(p(),y(`div`,W,[v(`div`,G,[v(`h2`,K,S(_.value.channelId),1),v(`p`,q,[n[10]||=f(` Runs here get agent `,-1),v(`code`,null,S(_.value.scope.effective.agent),1),n[11]||=f(`, model `,-1),v(`code`,null,S(_.value.scope.effective.model),1),_.value.scope.effective.effort?(p(),y(s,{key:0},[n[9]||=f(`, effort `,-1),v(`code`,null,S(_.value.scope.effective.effort),1)],64)):m(``,!0)])]),Q.value?m(``,!0):(p(),y(`p`,ee,` Read-only: channel config changes need the channel-config right. `+S(_.value.scope.adminsHint),1)),v(`form`,{class:`agent-form grid gap-3 rounded-lg border border-default bg-elevated px-5 py-4`,onSubmit:o(De,[`prevent`])},[n[26]||=v(`h3`,{class:`font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Agent, model, effort`,-1),v(`div`,te,[n[14]||=v(`label`,{class:`text-sm text-muted`,for:`ch-agent`},`Agent`,-1),T(v(`select`,{id:`ch-agent`,"onUpdate:modelValue":n[1]||=e=>Y.agent=e,class:C(x(k)),disabled:!Q.value},[v(`option`,re,`— the default (`+S(_.value.scope.defaults.agent)+`), or the router —`,1),(p(!0),y(s,null,b(e.vocabulary.agents,e=>(p(),y(`option`,{key:e,value:e,disabled:_.value.scope.restrictedAgents.includes(e)},S(e)+S(_.value.scope.restrictedAgents.includes(e)?` (restricted)`:``),9,ie))),128))],10,ne),[[r,Y.agent]]),n[15]||=v(`label`,{class:`text-sm text-muted`,for:`ch-model`},`Model, any agent`,-1),T(v(`input`,{id:`ch-model`,"onUpdate:modelValue":n[2]||=e=>Y.model=e,class:C([x(O),`font-mono text-xs`]),placeholder:`provider/model — forces every run's model`,disabled:!Q.value},null,10,ae),[[a,Y.model]]),n[16]||=v(`span`,{class:`text-sm text-muted`},`Model per agent`,-1),v(`div`,oe,[(p(!0),y(s,null,b(e.vocabulary.agents,e=>(p(),y(`label`,{key:e,class:`grid grid-cols-[6rem_1fr] items-center gap-2 font-mono text-xs`},[v(`span`,se,S(e),1),T(v(`input`,{"onUpdate:modelValue":t=>Y.models[e]=t,name:`models.${e}`,class:C([x(O),`font-mono text-xs`]),placeholder:_.value.scope.defaults.models[e]??`default`,disabled:!Q.value},null,10,ce),[[a,Y.models[e]]])]))),128))]),n[17]||=v(`label`,{class:`text-sm text-muted`,for:`ch-effort`},`Effort, any agent`,-1),T(v(`select`,{id:`ch-effort`,"onUpdate:modelValue":n[3]||=e=>Y.effort=e,class:C(x(k)),disabled:!Q.value},[n[12]||=v(`option`,{value:``},`— unset —`,-1),(p(!0),y(s,null,b(e.vocabulary.efforts,e=>(p(),y(`option`,{key:e,value:e},S(e),9,ue))),128))],10,le),[[r,Y.effort]]),n[18]||=v(`span`,{class:`text-sm text-muted`},`Effort per agent`,-1),v(`div`,de,[(p(!0),y(s,null,b(e.vocabulary.agents,t=>(p(),y(`label`,{key:t,class:`grid grid-cols-[6rem_1fr] items-center gap-2 font-mono text-xs`},[v(`span`,fe,S(t),1),T(v(`select`,{"onUpdate:modelValue":e=>Y.efforts[t]=e,name:`efforts.${t}`,class:C(x(k)),disabled:!Q.value},[n[13]||=v(`option`,{value:``},`— unset —`,-1),(p(!0),y(s,null,b(e.vocabulary.efforts,e=>(p(),y(`option`,{key:e,value:e},S(e),9,me))),128))],10,pe),[[r,Y.efforts[t]]])]))),128))])]),n[27]||=v(`h3`,{class:`mt-2 font-mono text-xs font-medium uppercase tracking-wider text-muted`},` Boundary · caps every run here, never grants `,-1),v(`div`,he,[n[20]||=v(`label`,{class:`text-sm text-muted`,for:`ch-minutes`},`Max minutes`,-1),T(v(`input`,{id:`ch-minutes`,"onUpdate:modelValue":n[4]||=e=>Y.maxMinutes=e,class:C([x(O),`w-28 font-mono text-xs tabular-nums`]),type:`number`,min:`2`,placeholder:`uncapped`,disabled:!Q.value},null,10,ge),[[a,Y.maxMinutes]]),n[21]||=v(`label`,{class:`text-sm text-muted`,for:`ch-identity`},`Max identity`,-1),T(v(`select`,{id:`ch-identity`,"onUpdate:modelValue":n[5]||=e=>Y.maxIdentity=e,class:C(x(k)),disabled:!Q.value},[n[19]||=v(`option`,{value:``},`— uncapped —`,-1),(p(!0),y(s,null,b(e.vocabulary.identities,e=>(p(),y(`option`,{key:e,value:e},S(e),9,ve))),128))],10,_e),[[r,Y.maxIdentity]]),n[22]||=v(`span`,{class:`text-sm text-muted`},`Machines`,-1),v(`div`,ye,[(p(!0),y(s,null,b(e.vocabulary.machines,e=>(p(),y(`label`,{key:e,class:`inline-flex items-center gap-1.5 font-mono text-xs`},[v(`input`,{type:`checkbox`,name:`machine-${e}`,checked:Y.machines.has(e),disabled:!Q.value,onChange:t=>Te(e,t.target.checked)},null,40,be),f(` `+S(e),1)]))),128))])]),v(`div`,xe,[c(u,{type:`submit`,size:`sm`,color:`neutral`,disabled:!Q.value||X.value},{default:i(()=>[...n[23]||=[f(`Save`,-1)]]),_:1},8,[`disabled`]),c(u,{size:`sm`,color:`error`,variant:`ghost`,disabled:!Q.value||X.value,onClick:Ae},{default:i(()=>[...n[24]||=[f(`Clear every override`,-1)]]),_:1},8,[`disabled`]),n[25]||=v(`span`,{class:`text-xs text-dimmed`},`Save sets the filled fields and leaves the rest as they are.`,-1)])],32),v(`form`,{class:`instructions-form grid gap-3 rounded-lg border border-default bg-elevated px-5 py-4`,onSubmit:o(Oe,[`prevent`])},[n[30]||=v(`h3`,{class:`font-mono text-xs font-medium uppercase tracking-wider text-muted`},` Custom instructions · advisory, on every run here `,-1),T(v(`textarea`,{id:`ch-instructions`,"onUpdate:modelValue":n[6]||=e=>Y.instructions=e,class:C([x(O),`min-h-28 w-full`]),maxlength:`2000`,placeholder:`Always reply in bullet points.`,disabled:!Q.value},null,10,Se),[[a,Y.instructions]]),v(`div`,Ce,[c(u,{type:`submit`,size:`sm`,color:`neutral`,disabled:!Q.value||X.value},{default:i(()=>[...n[28]||=[f(`Save instructions`,-1)]]),_:1},8,[`disabled`]),c(u,{size:`sm`,color:`neutral`,variant:`ghost`,disabled:!Q.value||X.value,onClick:ke},{default:i(()=>[...n[29]||=[f(`Clear instructions`,-1)]]),_:1},8,[`disabled`]),v(`span`,we,S(Y.instructions.length)+` / 2000`,1)])],32),Z.value?(p(),y(`p`,{key:1,class:C([`notice rounded-md px-3 py-2 text-sm`,Z.value.kind===`error`?`border border-err/30 bg-err/10`:`border border-ok/30 bg-ok/10`])},S(Z.value.text),3)):m(``,!0)])):m(``,!0):(p(),y(`div`,V,` Pick a channel to see and change its scope. `))])])}}}),Y={class:`grid gap-4`},X={class:`overflow-x-auto rounded-lg border border-default bg-elevated`},Z={class:`settings w-full min-w-[42rem] border-collapse text-[0.8125rem]`},Q={class:`border-t border-muted bg-muted/40`},Te={colspan:`4`,class:`px-4 py-1.5 text-left font-mono text-xs font-medium text-toned`},Ee=[`data-key`],$={class:`px-4 py-2 font-mono text-xs text-highlighted`},De={class:`px-4 py-2 font-mono text-xs tabular-nums`},Oe={class:`value`},ke={key:0,class:`ml-1.5 rounded bg-accented px-1.5 py-0.5 text-[0.625rem] text-muted`},Ae={class:`px-4 py-2 text-muted`},je={class:`px-4 py-2 text-xs text-dimmed`},Me={class:`capabilities grid gap-1.5 rounded-lg border border-default bg-elevated px-4 py-3 text-sm`},Ne=[`data-capability`,`data-on`],Pe={class:`w-40 font-mono text-xs text-highlighted`},Fe={class:`text-xs text-muted`},Ie=w({__name:`InstallationPanel`,props:{installation:{}},setup(e){let t=e,n=g(()=>{let e=[];for(let n of t.installation.settings){let t=n.key.split(`.`)[0],r=e[e.length-1];r&&r.block===t?r.rows.push(n):e.push({block:t,rows:[n]})}return e}),r={runtime:"config.yaml default · a channel overrides it at run time (Channels tab, `config set channel`)",config:"config.yaml · `deploy config`, then a restart"},i=e=>typeof e==`string`?e:e?`on`:`off`;return(t,a)=>(p(),y(`section`,Y,[a[1]||=v(`p`,{class:`text-sm text-muted`},[f(` The behaviour knobs of this installation's `),v(`code`,null,`config.yaml`),f(`, with the value in force. A running process keeps the config it started with: a change here is `),v(`code`,null,`deploy config`),f(` and a restart. The `),v(`code`,null,`defaults.*`),f(` rows are what a channel overrides on the Channels tab. `)],-1),v(`div`,X,[v(`table`,Z,[a[0]||=v(`thead`,null,[v(`tr`,{class:`text-left font-mono text-[0.6875rem] uppercase tracking-widest text-dimmed`},[v(`th`,{class:`px-4 py-2.5 font-medium`},`Key`),v(`th`,{class:`px-4 py-2.5 font-medium`},`Value`),v(`th`,{class:`px-4 py-2.5 font-medium`},`What it does`),v(`th`,{class:`px-4 py-2.5 font-medium`},`Changes in`)])],-1),(p(!0),y(s,null,b(n.value,e=>(p(),y(`tbody`,{key:e.block},[v(`tr`,Q,[v(`th`,Te,S(e.block),1)]),(p(!0),y(s,null,b(e.rows,e=>(p(),y(`tr`,{key:e.key,class:`setting border-t border-muted align-top`,"data-key":e.key},[v(`td`,$,S(e.key),1),v(`td`,De,[v(`span`,Oe,S(e.value),1),e.isDefault?(p(),y(`span`,ke,`default`)):m(``,!0)]),v(`td`,Ae,S(e.note),1),v(`td`,je,S(r[e.how]),1)],8,Ee))),128))]))),128))])]),a[2]||=v(`h2`,{class:`mt-2 font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Capabilities`,-1),a[3]||=v(`p`,{class:`-mt-3 text-sm text-muted`},[f(` What is on in this process. Each one is a `),v(`code`,null,`config.yaml`),f(` block plus what its environment needs; the commands, dashboard section and Worker of a capability appear together when it is on. `)],-1),v(`ul`,Me,[(p(!0),y(s,null,b(e.installation.capabilities,e=>(p(),y(`li`,{key:e.key,class:`capability flex flex-wrap items-baseline gap-x-3 gap-y-0.5 border-b border-muted py-1.5 last:border-0`,"data-capability":e.key,"data-on":String(e.on)},[v(`span`,Pe,S(e.key),1),v(`span`,{class:C([`state rounded px-1.5 py-0.5 font-mono text-[0.6875rem]`,e.on===!1?`bg-accented text-dimmed`:`bg-ok/15 text-ok`])},S(i(e.on)),3),v(`span`,Fe,S(e.how),1)],8,Ne))),128))])]))}}),Le={class:`grid gap-4`},Re={class:`text-sm text-muted`},ze={key:0,class:`unavailable rounded-md border border-warn/30 bg-warn/10 px-3 py-2 text-sm`},Be={key:1,class:`overflow-x-auto rounded-lg border border-default bg-elevated`},Ve={class:`servers w-full min-w-[46rem] border-collapse text-[0.8125rem]`},He={key:0},Ue=[`data-name`,`data-scope`],We={class:`px-4 py-2 font-mono text-xs font-medium text-highlighted`},Ge={key:0,class:`ml-1 text-[0.625rem] font-normal text-dimmed`},Ke={class:`px-4 py-2 font-mono text-xs`},qe=[`title`],Je={class:`px-4 py-2 font-mono text-xs`},Ye={class:`px-4 py-2 font-mono text-xs`},Xe={class:`px-4 py-2 text-xs`},Ze={key:0,class:`probe mt-1 text-dimmed`},Qe={class:`px-4 py-1.5 text-right whitespace-nowrap`},$e={class:`grid gap-3 sm:grid-cols-[8rem_1fr]`},et=[`disabled`],tt=[`disabled`],nt=[`disabled`],rt=[`disabled`],it=[`disabled`],at={class:`flex flex-wrap gap-x-4 gap-y-1 text-sm`},ot=[`name`,`checked`,`disabled`,`onChange`],st=[`disabled`],ct={class:`flex flex-wrap items-center gap-3`},lt={key:0,class:`restricted text-xs text-muted`},ut=[`href`],dt=w({__name:`McpServersPanel`,props:{mcps:{},vocabulary:{},viewer:{},asUser:{},fetch:{type:Function}},setup(e){let n=e,_=(e,t)=>(n.fetch??((e,t)=>globalThis.fetch(e,t)))(e,t),w=[`general`,`research`],A=g(()=>n.asUser!==void 0),j=h({name:``,url:``,scope:n.mcps.channel?`channel`:n.asUser?`me`:`org`,agents:new Set(w),auth:``}),M=u(n.mcps.channel??``),N=u(!1),P=u(null),F=h({}),I=g(()=>j.scope===`org`?n.mcps.canWrite.org:j.scope===`channel`?n.mcps.canWrite.channel:A.value),L=e=>e.scope===`org`?n.mcps.canWrite.org:e.scope===`channel`?n.mcps.canWrite.channel:n.asUser!==void 0&&e.scopeKey===`user:${n.asUser.id}`,R=e=>e.addedBy===n.viewer||n.asUser!==void 0&&e.addedBy===n.asUser.id,z={connected:`connected`,awaiting_credential:`awaiting credential`,static:`pinned in config.yaml`};function B(){let e=M.value.trim();t.navigate(e?`/settings/mcps?channel=${encodeURIComponent(e)}`:`/settings/mcps`)}function V(e,t){t?j.agents.add(e):j.agents.delete(e)}function H(e){return e===`channel`?{scope:`channel`,channel:n.mcps.channel}:{scope:e}}let U=e=>e.scope===`user`?`me`:e.scope;async function W(){N.value=!0,P.value=null;let e=await E(_,`mcp.add`,{name:j.name.trim(),url:j.url.trim(),...H(j.scope),agents:[...j.agents].join(`,`),...j.auth?{auth:j.auth}:{}});if(N.value=!1,!e.ok){P.value={kind:`error`,text:e.failure.message};return}let n=e.value;P.value=n.connectUrl?{kind:`ok`,text:`Added ${n.server?.name??j.name}. Open the one-time link to connect it (it expires in 10 minutes; only you can complete it).`,connectUrl:n.connectUrl}:{kind:`ok`,text:`Added ${n.server?.name??j.name}.`},n.connectUrl||t.reload()}async function G(e){N.value=!0,P.value=null;let t=await E(_,`mcp.connect`,{name:e.name,...H(U(e))});if(N.value=!1,!t.ok){P.value={kind:`error`,text:t.failure.message};return}let n=t.value;P.value={kind:`ok`,text:`A fresh one-time link for ${e.name} (10 minutes; only you can complete it).`,...n.connectUrl?{connectUrl:n.connectUrl}:{}}}async function K(e){if(!t.confirm(`Remove ${e.name} from the ${e.scope} tier and forget its stored credential?`))return;N.value=!0,P.value=null;let n=await E(_,`mcp.remove`,{name:e.name,...H(U(e))});if(N.value=!1,!n.ok){P.value={kind:`error`,text:n.failure.message};return}t.reload()}async function q(e){F[e.name]=`probing…`;let t=await D(_,`mcp.show`,{name:e.name,scope:e.scope===`user`?`me`:e.scope,channel:e.scope===`channel`?n.mcps.channel:void 0});if(!t.ok){F[e.name]=t.failure.message;return}let{probe:r}=t.value;F[e.name]=r?.tools?`${r.tools.length} tool${r.tools.length===1?``:`s`}: ${r.tools.map(e=>e.name).join(`, `)}`:r?.error??`no tools listed`}return(t,n)=>{let u=l;return p(),y(`section`,Le,[v(`p`,Re,[n[9]||=f(` External MCP servers a run's tools may call, by tier: `,-1),n[10]||=v(`strong`,null,`org`,-1),n[11]||=f(` reaches every run and is the only tier that may name the coding, review or ship agents; `,-1),n[12]||=v(`strong`,null,`channel`,-1),n[13]||=f(` reaches that channel's runs; `,-1),e.asUser?(p(),y(s,{key:0},[n[5]||=v(`strong`,null,`me`,-1),f(` reaches the runs you ask for as `+S(e.asUser.name??e.asUser.id)+`.`,1)],64)):(p(),y(s,{key:1},[n[6]||=f(`personal servers are added in chat (`,-1),n[7]||=v(`code`,null,`mcp add`,-1),n[8]||=f(`), where your runs are requested as you.`,-1)],64))]),v(`form`,{class:`flex flex-wrap items-center gap-2 text-sm`,onSubmit:o(B,[`prevent`])},[n[15]||=v(`label`,{class:`text-muted`,for:`mcp-channel`},`Channel tier`,-1),T(v(`input`,{id:`mcp-channel`,"onUpdate:modelValue":n[0]||=e=>M.value=e,class:C([x(O),`font-mono text-xs`]),placeholder:`slack:C0123… (blank: org and your own)`,"aria-label":`channel whose MCP servers to list`},null,2),[[a,M.value]]),c(u,{type:`submit`,size:`xs`,color:`neutral`,variant:`outline`},{default:i(()=>[...n[14]||=[f(`Open`,-1)]]),_:1})],32),e.mcps.unavailable?(p(),y(`p`,ze,S(e.mcps.unavailable),1)):(p(),y(`div`,Be,[v(`table`,Ve,[n[20]||=v(`thead`,null,[v(`tr`,{class:`text-left font-mono text-[0.6875rem] uppercase tracking-widest text-dimmed`},[v(`th`,{class:`px-4 py-2.5 font-medium`},`Name`),v(`th`,{class:`px-4 py-2.5 font-medium`},`Tier`),v(`th`,{class:`px-4 py-2.5 font-medium`},`URL`),v(`th`,{class:`px-4 py-2.5 font-medium`},`Agents`),v(`th`,{class:`px-4 py-2.5 font-medium`},`Auth`),v(`th`,{class:`px-4 py-2.5 font-medium`},`State`),v(`th`,{class:`px-4 py-2.5 font-medium`},[v(`span`,{class:`sr-only`},`Actions`)])])],-1),v(`tbody`,null,[e.mcps.servers.length===0?(p(),y(`tr`,He,[...n[16]||=[v(`td`,{colspan:`7`,class:`empty px-4 py-6 text-center text-muted`},` No MCP server reaches runs here yet. Add one below. `,-1)]])):m(``,!0),(p(!0),y(s,null,b(e.mcps.servers,e=>(p(),y(`tr`,{key:`${e.scopeKey}/${e.name}`,class:`server border-t border-muted align-top`,"data-name":e.name,"data-scope":e.scope},[v(`td`,We,[f(S(e.name)+` `,1),R(e)?(p(),y(`span`,Ge,`· added by you`)):m(``,!0)]),v(`td`,Ke,S(e.scope),1),v(`td`,{class:`max-w-[18rem] truncate px-4 py-2 font-mono text-xs text-muted`,title:e.url},S(e.url),9,qe),v(`td`,Je,S(e.agents.join(`, `)),1),v(`td`,Ye,S(e.auth),1),v(`td`,Xe,[v(`span`,{class:C([`state rounded px-1.5 py-0.5 font-mono text-[0.6875rem]`,e.state===`connected`?`bg-ok/15 text-ok`:e.state===`awaiting_credential`?`bg-warn/15 text-warn`:`bg-accented text-muted`])},S(z[e.state]),3),F[e.name]?(p(),y(`div`,Ze,S(F[e.name]),1)):m(``,!0)]),v(`td`,Qe,[c(u,{size:`xs`,color:`neutral`,variant:`ghost`,disabled:N.value,onClick:t=>q(e)},{default:i(()=>[...n[17]||=[f(`Probe`,-1)]]),_:1},8,[`disabled`,`onClick`]),e.source===`runtime`&&e.auth!==`none`?(p(),d(u,{key:0,size:`xs`,color:`neutral`,variant:`ghost`,disabled:N.value||!L(e),onClick:t=>G(e)},{default:i(()=>[...n[18]||=[f(`Connect`,-1)]]),_:1},8,[`disabled`,`onClick`])):m(``,!0),e.source===`runtime`?(p(),d(u,{key:1,size:`xs`,color:`error`,variant:`ghost`,disabled:N.value||!L(e),onClick:t=>K(e)},{default:i(()=>[...n[19]||=[f(`Remove`,-1)]]),_:1},8,[`disabled`,`onClick`])):m(``,!0)])],8,Ue))),128))])])])),e.mcps.unavailable?m(``,!0):(p(),y(`form`,{key:2,class:`add grid gap-3 rounded-lg border border-default bg-elevated px-5 py-4`,onSubmit:o(W,[`prevent`])},[n[29]||=v(`h2`,{class:`font-mono text-xs font-medium uppercase tracking-wider text-muted`},`Add a server`,-1),v(`div`,$e,[n[23]||=v(`label`,{class:`text-sm text-muted`,for:`mcp-name`},`Name`,-1),T(v(`input`,{id:`mcp-name`,"onUpdate:modelValue":n[1]||=e=>j.name=e,class:C([x(O),`font-mono text-xs`]),required:``,pattern:`[a-z0-9][a-z0-9-]{0,31}`,placeholder:`notion — becomes the tool prefix mcp__notion__`,disabled:!I.value},null,10,et),[[a,j.name]]),n[24]||=v(`label`,{class:`text-sm text-muted`,for:`mcp-url`},`URL`,-1),T(v(`input`,{id:`mcp-url`,"onUpdate:modelValue":n[2]||=e=>j.url=e,class:C([x(O),`font-mono text-xs`]),type:`url`,required:``,placeholder:`https://mcp.example.com/mcp — Streamable HTTP`,disabled:!I.value},null,10,tt),[[a,j.url]]),n[25]||=v(`label`,{class:`text-sm text-muted`,for:`mcp-scope`},`Tier`,-1),T(v(`select`,{id:`mcp-scope`,"onUpdate:modelValue":n[3]||=e=>j.scope=e,class:C(x(k)),disabled:!I.value&&!e.mcps.canWrite.org&&!e.mcps.canWrite.channel&&!A.value},[v(`option`,{value:`me`,disabled:!A.value},` me — `+S(e.asUser?`your own runs, as ${e.asUser.name??e.asUser.id}`:`your session is not linked to a Slack user`),9,rt),n[21]||=v(`option`,{value:`org`},`org — every run; may name coding, review, ship`,-1),v(`option`,{value:`channel`,disabled:!e.mcps.channel},` channel — `+S(e.mcps.channel??`open a channel above first`),9,it)],10,nt),[[r,j.scope]]),n[26]||=v(`span`,{class:`text-sm text-muted`},`Agents`,-1),v(`div`,at,[(p(!0),y(s,null,b(e.vocabulary.agents,e=>(p(),y(`label`,{key:e,class:`inline-flex items-center gap-1.5 font-mono text-xs`},[v(`input`,{type:`checkbox`,name:`agent-${e}`,checked:j.agents.has(e),disabled:!I.value||j.scope===`channel`&&!w.includes(e),onChange:t=>V(e,t.target.checked)},null,40,ot),f(` `+S(e),1)]))),128))]),n[27]||=v(`label`,{class:`text-sm text-muted`,for:`mcp-auth`},`Auth`,-1),T(v(`select`,{id:`mcp-auth`,"onUpdate:modelValue":n[4]||=e=>j.auth=e,class:C(x(k)),disabled:!I.value},[...n[22]||=[v(`option`,{value:``},`detect from the server`,-1),v(`option`,{value:`oauth`},`oauth — sign in on a one-time link`,-1),v(`option`,{value:`bearer`},`bearer — paste a token on a one-time link`,-1),v(`option`,{value:`none`},`none`,-1)]],10,st),[[r,j.auth]])]),v(`div`,ct,[c(u,{type:`submit`,size:`sm`,color:`neutral`,disabled:!I.value||N.value},{default:i(()=>[...n[28]||=[f(`Add`,-1)]]),_:1},8,[`disabled`]),I.value?m(``,!0):(p(),y(`span`,lt,S(j.scope===`org`?`Org-wide servers are managed by admins (repo-management rights).`:j.scope===`channel`?`This channel's servers need channel-config rights.`:"Personal servers need a session linked to your Slack user; add yours in chat with `mcp add`."),1))]),P.value?(p(),y(`p`,{key:0,class:C([`notice rounded-md px-3 py-2 text-sm`,P.value.kind===`error`?`border border-err/30 bg-err/10`:`border border-ok/30 bg-ok/10`])},[f(S(P.value.text)+` `,1),P.value.connectUrl?(p(),y(`a`,{key:0,class:`connect ml-1 font-mono text-xs underline`,href:P.value.connectUrl},S(P.value.connectUrl),9,ut)):m(``,!0)],2)):m(``,!0)],32))])}}}),ft=[{id:`mcps`,href:`/settings/mcps`,label:`MCPs`,on:e=>e.mcp},{id:`channels`,href:`/settings/channels`,label:`Channels`},{id:`installation`,href:`/settings/installation`,label:`Installation`}];function pt(e,t){return ft.filter(n=>n.id===t||n.on===void 0||e!==null&&n.on(e))}var mt={class:`tabs mb-3.5 flex gap-5 border-b border-muted px-2 text-[0.8rem]`,"aria-label":`Settings sections`},ht=[`href`,`aria-current`],gt=w({__name:`SettingsTabs`,props:{current:{}},setup(t){let n=t,r=e(),i=g(()=>pt(r,n.current));return(e,n)=>(p(),y(`nav`,mt,[(p(!0),y(s,null,b(i.value,e=>(p(),y(`a`,{key:e.id,href:e.href,"aria-current":e.id===t.current?`page`:void 0,class:`-mb-px border-b border-transparent pb-2 pt-1.5 text-muted no-underline hover:text-highlighted aria-[current=page]:border-(--ui-text-highlighted) aria-[current=page]:font-medium aria-[current=page]:text-highlighted`},S(e.label),9,ht))),128))]))}}),_t=w({__name:`SettingsPage`,setup(e){let t=_(`settings`),r=g(()=>t?.tab??`channels`);return(e,a)=>x(t)?(p(),d(n,{key:0,title:`Settings`,nav:`settings`},{default:i(()=>[c(gt,{current:r.value},null,8,[`current`]),r.value===`mcps`&&x(t).mcps?(p(),d(dt,{key:0,mcps:x(t).mcps,vocabulary:x(t).vocabulary,viewer:x(t).viewer,"as-user":x(t).asUser},null,8,[`mcps`,`vocabulary`,`viewer`,`as-user`])):r.value===`channels`&&x(t).channels?(p(),d(J,{key:1,channels:x(t).channels,vocabulary:x(t).vocabulary},null,8,[`channels`,`vocabulary`])):r.value===`installation`&&x(t).installation?(p(),d(Ie,{key:2,installation:x(t).installation},null,8,[`installation`])):m(``,!0)]),_:1})):(p(),d(n,{key:1,title:`Settings`,nav:`settings`},{default:i(()=>[...a[0]||=[v(`p`,{class:`empty py-8 text-center text-sm text-muted`},` Settings are unavailable: the page was served without its data. `,-1)]]),_:1}))}});export{_t as default};