@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.
- package/dist/assets/deploy/cloudflare-resident/worker.ts +35 -12
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/execution/snapshotRetention.ts +62 -0
- package/dist/assets/src/mcp/registry.ts +9 -1
- package/dist/assets/web/dist/.vite/manifest.json +27 -27
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Dc23v_8v.js → ResidentDetailPage-CrfCd8fx.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-TR5D1TRR.js → ResidentsIndexPage-BdW0k2h3.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-mkUj7WEN.js → RunFoldRow-CbTd9SiS.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-CQI17__g.js → RunRoutePage-br0H-8Pz.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-yIP4PlKP.js → RunsIndexPage-Bnzb15zH.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-CdCVyNbh.js → ScheduledPage-NLhjzHAz.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-D-i0uVw-.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DmNHX6am.js → StatusDot-DkXALvKf.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-qVnJTgSt.js → Tooltip-DtLIyi5P.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BcTeH7m2.js → UnitRoutePage-DGXukEAH.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-TAGawIfM.js → dist-DtA7JiH5.js} +1 -1
- package/dist/assets/web/dist/assets/main-CFHF2RvQ.css +1 -0
- package/dist/assets/web/dist/assets/{main-mUd_TJjH.js → main-Dht0in4v.js} +2 -2
- package/dist/cli.js +184 -23
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
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 = {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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};
|