@zackbart/connecta 0.35.0 → 0.36.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/CHANGELOG.md +55 -0
- package/README.md +8 -4
- package/bin/version.mjs +1 -1
- package/dist/auth/downstream-oauth.js +71 -79
- package/dist/auth/static-oauth.js +104 -26
- package/dist/connectors/api-connector.d.ts +17 -0
- package/dist/connectors/api-connector.js +3 -1
- package/dist/http/client.js +1 -1
- package/dist/http/paging.js +1 -2
- package/dist/http/services.js +1 -1
- package/dist/http/tools.js +0 -4
- package/dist/operator-ui/contract.d.ts +1 -0
- package/dist/operator-ui/generated.js +4 -4
- package/dist/providers/_shared/google/kit.js +29 -139
- package/dist/providers/_shared/rest/kit.js +4 -404
- package/dist/providers/docs/tools.js +1 -2
- package/dist/providers/drive/api.js +1 -1
- package/dist/providers/forms/tools.js +1 -2
- package/dist/providers/gmail/tools.js +2 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/tools.js +1 -2
- package/dist/providers/slides/tools.js +2 -2
- package/dist/providers/vercel/index.js +1 -2
- package/dist/routes/oauth.js +3 -0
- package/dist/runtime/call.js +0 -4
- package/dist/skills.js +0 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +134 -301
- package/documentation/auth.md +47 -17
- package/documentation/integrating.md +1 -1
- package/documentation/meta-tools.md +1 -1
- package/documentation/operator-ui.md +16 -3
- package/documentation/providers.md +382 -0
- package/package.json +7 -8
- package/templates/node/package.json +1 -1
- package/dist/operator-ui/agents.d.ts +0 -75
- package/dist/operator-ui/agents.js +0 -114
- package/dist/operator-ui/config-summary.js +0 -566
- package/dist/operator-ui/connection-map.js +0 -378
- package/dist/operator-ui/endpoints.js +0 -212
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -79
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -126
- package/dist/operator-ui/setup-commands.js +0 -47
- package/dist/operator-ui/view.d.ts +0 -378
- package/dist/operator-ui/view.js +0 -631
- package/dist/providers/_shared/rest/dispatch.js +0 -21
- package/dist/providers/_shared/rest/tools.js +0 -592
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { renderFixPrompt } from "../fix-prompt.js";
|
|
2
|
-
const CATALOGUE = {
|
|
3
|
-
connector_unavailable: {
|
|
4
|
-
problem: "A connector is unavailable: its status check or catalog load failed, or did not finish before the operator page's deadline.",
|
|
5
|
-
steps: [
|
|
6
|
-
"Confirm the connector's downstream URL or API base in the deployment config, and that the deployment can reach it from where it runs.",
|
|
7
|
-
"Check the credentials or headers the connector sends; a rejected credential often surfaces as a failed status rather than as an authorization prompt.",
|
|
8
|
-
"If the downstream is slow rather than down, review the connector's timeouts and the deployment's discovery.probeTimeoutMs.",
|
|
9
|
-
],
|
|
10
|
-
},
|
|
11
|
-
oauth_required: {
|
|
12
|
-
problem: "A downstream OAuth connector needs authorization: no grant is stored, or the stored grant expired or was revoked and could not be refreshed.",
|
|
13
|
-
steps: [
|
|
14
|
-
"Reauthorize from the connector's Connection tab (Connect or Reconnect) or with authorize_connector. A lapsed grant needs no code change.",
|
|
15
|
-
"If it needs reauthorizing again soon after, check that the OAuth client requests offline access or a refresh token, and that the storage holding OAuth tokens persists across restarts and is shared by every instance.",
|
|
16
|
-
"If authorization cannot start at all, check the connector's OAuth client configuration and the deployment's publicUrl, which forms the /oauth/callback/<connector id> redirect URI.",
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
|
-
credential_required: {
|
|
20
|
-
problem: "A connector with an operator-managed credential slot has no usable credential stored.",
|
|
21
|
-
steps: [
|
|
22
|
-
"With a credential vault configured, a maintainer adds a shared credential on the operator page, and a pool member saves their own personal one from their view of it; neither needs a code change. Without a vault, configure one first.",
|
|
23
|
-
"If nobody can, list the person in the deployment's maintainers for shared auth, or in the members of a pool containing the connector for personal auth.",
|
|
24
|
-
"Check that the connector's credential declaration (label and fields) matches what the downstream actually needs.",
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
auth_required: {
|
|
28
|
-
problem: "A connector reports that it needs authorization, and its secret lives in deployment configuration rather than in an operator-managed slot.",
|
|
29
|
-
steps: [
|
|
30
|
-
"Find where the connector's secret is read (usually an environment variable or Worker secret passed into the connector config) and confirm it is set in the running environment.",
|
|
31
|
-
"Rotate the secret at the provider if it expired or was revoked, then update the deployment's environment, not its source.",
|
|
32
|
-
"If operators should manage this secret from the page instead, declare a credential slot on the connector and configure a credential vault.",
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
credential_mismatch: {
|
|
36
|
-
problem: "The credential stored for a connector does not match the fields the connector currently declares, so it cannot be used.",
|
|
37
|
-
steps: [
|
|
38
|
-
"If the connector's credential fields changed on purpose, re-enter the credential on the operator page so the stored fields match.",
|
|
39
|
-
"If they changed by accident, restore the previous field names in the connector's credential declaration.",
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
credential_unreadable: {
|
|
43
|
-
problem: "A stored credential exists but could not be read or decrypted.",
|
|
44
|
-
steps: [
|
|
45
|
-
"Check that the credential vault's encryption key in the deployment environment is the one the credential was stored with; a rotated or missing key makes every stored value unreadable.",
|
|
46
|
-
"Check the storage adapter backing the vault is reachable from the deployment.",
|
|
47
|
-
"If the key was lost, remove and re-add the credential on the operator page; the old value cannot be recovered.",
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
credential_test_failed: {
|
|
51
|
-
problem: "The test for a stored connector credential failed: the downstream rejected it, or the test could not reach the downstream.",
|
|
52
|
-
steps: [
|
|
53
|
-
"Confirm the stored value is current at the provider (not rotated, revoked, or scoped too narrowly), and replace it on the operator page if not.",
|
|
54
|
-
"Check that the connector's credential test targets the same API base and auth scheme its tool calls use.",
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
oauth_action_failed: {
|
|
58
|
-
problem: "Restarting or disconnecting a connector's downstream OAuth from the operator page failed.",
|
|
59
|
-
steps: [
|
|
60
|
-
"Check that the connector implements the hooks the action needs (startAuth, finishAuth, and verifyState to connect; disconnectAuth to disconnect), and that the storage holding its OAuth state is writable.",
|
|
61
|
-
"Check the connector's OAuth client configuration, including the authorization server it discovers or is given.",
|
|
62
|
-
"Confirm the person is a maintainer for shared auth, or a member of a pool containing the connector for personal auth.",
|
|
63
|
-
],
|
|
64
|
-
},
|
|
65
|
-
catalog_failed: {
|
|
66
|
-
problem: "A connector reports itself connected, but loading its tool catalog failed, so none of its tools are being served.",
|
|
67
|
-
steps: [
|
|
68
|
-
"Check that the downstream still serves a complete tools list; connecta refuses a partial catalog rather than serving part of it.",
|
|
69
|
-
"Check for tools the connector cannot accept: invalid schemas, missing descriptions, or names that collide.",
|
|
70
|
-
"Pin or upgrade the @zackbart/connecta version if a maintained provider's catalog changed shape underneath it.",
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
/** The clipboard text for one failure. Fixed by kind; see `CATALOGUE`. */
|
|
75
|
-
export function fixPrompt(kind, connectorId) {
|
|
76
|
-
return renderFixPrompt(CATALOGUE[kind], connectorId);
|
|
77
|
-
}
|
|
78
|
-
/** Every kind, so a test can walk the whole catalogue. */
|
|
79
|
-
export const FIX_PROMPT_KINDS = Object.keys(CATALOGUE);
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The next steady evidence. A settled connector takes its current row; one
|
|
3
|
-
* being read again keeps its previous evidence when this identity has any.
|
|
4
|
-
* Returns `previous` itself when nothing changed, so it can live in React
|
|
5
|
-
* state. A new identity starts empty.
|
|
6
|
-
*/
|
|
7
|
-
export function steadyHealth(state, previous) {
|
|
8
|
-
const same = previous?.generation === state.generation ? previous : null;
|
|
9
|
-
const contract = state.contract?.role === "maintainer" && state.data ? state.contract : null;
|
|
10
|
-
// No contract on screen: keep what was shown until the next one arrives.
|
|
11
|
-
if (!contract || !state.data)
|
|
12
|
-
return same ?? { generation: state.generation, rows: Object.create(null) };
|
|
13
|
-
const reading = new Set(state.data.connectors.filter((c) => c.status === "loading").map((c) => c.id));
|
|
14
|
-
const held = same?.rows ?? Object.create(null);
|
|
15
|
-
const rows = Object.create(null);
|
|
16
|
-
let changed = !same || Object.keys(held).length !== contract.live.connectors.length;
|
|
17
|
-
for (const live of contract.live.connectors) {
|
|
18
|
-
const before = Object.hasOwn(held, live.id) ? held[live.id] : undefined;
|
|
19
|
-
if (before && reading.has(live.id)) {
|
|
20
|
-
rows[live.id] = before;
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const failed = Object.hasOwn(state.connectorFailures, live.id) && Boolean(state.connectorFailures[live.id]);
|
|
24
|
-
if (before?.live === live && before.failed === failed)
|
|
25
|
-
rows[live.id] = before;
|
|
26
|
-
else {
|
|
27
|
-
rows[live.id] = { live, failed };
|
|
28
|
-
changed = true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return changed ? { generation: state.generation, rows } : same;
|
|
32
|
-
}
|
|
33
|
-
/** The contract each view renders health from, with every live row replaced by its steady evidence. */
|
|
34
|
-
export function shownHealth(contract, health) {
|
|
35
|
-
const signals = Object.create(null);
|
|
36
|
-
if (!contract)
|
|
37
|
-
return { contract, signals };
|
|
38
|
-
let replaced = false;
|
|
39
|
-
const connectors = contract.live.connectors.map((live) => {
|
|
40
|
-
const row = Object.hasOwn(health.rows, live.id) ? health.rows[live.id] : undefined;
|
|
41
|
-
if (row?.failed)
|
|
42
|
-
signals[live.id] = { failed: true };
|
|
43
|
-
if (!row || row.live === live)
|
|
44
|
-
return live;
|
|
45
|
-
replaced = true;
|
|
46
|
-
return row.live;
|
|
47
|
-
});
|
|
48
|
-
return {
|
|
49
|
-
contract: replaced ? { ...contract, live: { ...contract.live, connectors } } : contract,
|
|
50
|
-
signals,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { connectorStatusLabel, connectorStatusTone } from "./view.js";
|
|
2
|
-
/**
|
|
3
|
-
* Whether the member's own partition holds a credential, read from `actions`
|
|
4
|
-
* alone (P6b): `clear` is listed only when a value is stored. Connection
|
|
5
|
-
* state says whether it works, never whether it exists.
|
|
6
|
-
*/
|
|
7
|
-
function credentialStored(manage) {
|
|
8
|
-
return manage.actions.includes("clear");
|
|
9
|
-
}
|
|
10
|
-
function controlsFor(manage, connection) {
|
|
11
|
-
const can = (action) => (manage.kind === "none" ? false : manage.actions.includes(action));
|
|
12
|
-
if (manage.kind === "oauth") {
|
|
13
|
-
const connected = connection.state === "ok";
|
|
14
|
-
const controls = [];
|
|
15
|
-
if (!connected && can("connect")) {
|
|
16
|
-
controls.push({ kind: "connect", label: connection.state === "auth_required" ? "Connect" : "Reconnect" });
|
|
17
|
-
}
|
|
18
|
-
if (connected && can("restart"))
|
|
19
|
-
controls.push({ kind: "restart", label: "Reconnect" });
|
|
20
|
-
if (connection.state !== "auth_required" && can("disconnect")) {
|
|
21
|
-
controls.push({ kind: "disconnect", label: "Disconnect" });
|
|
22
|
-
}
|
|
23
|
-
return controls;
|
|
24
|
-
}
|
|
25
|
-
if (manage.kind === "credential") {
|
|
26
|
-
// `save` without `clear`: nothing stored yet. `test` is listed only for a stored value.
|
|
27
|
-
const stored = credentialStored(manage);
|
|
28
|
-
return [
|
|
29
|
-
...(can("save")
|
|
30
|
-
? [{ kind: "save", label: stored ? "Replace" : "Add credential" }]
|
|
31
|
-
: []),
|
|
32
|
-
...(can("test") ? [{ kind: "test", label: "Test" }] : []),
|
|
33
|
-
...(can("clear") ? [{ kind: "clear", label: "Clear" }] : []),
|
|
34
|
-
];
|
|
35
|
-
}
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* A few words under the account's name, only where the badge alone would
|
|
40
|
-
* leave something out: a saved credential that no longer fits, or one saved
|
|
41
|
-
* but not working.
|
|
42
|
-
*/
|
|
43
|
-
function summaryFor(manage, connection) {
|
|
44
|
-
if (manage.kind !== "credential" || !credentialStored(manage))
|
|
45
|
-
return null;
|
|
46
|
-
if (connection.problem === "credential_mismatch")
|
|
47
|
-
return "Saved credential no longer fits";
|
|
48
|
-
return connection.state === "ok" ? null : "Saved, but not working";
|
|
49
|
-
}
|
|
50
|
-
const toolCount = (connector) => connector.tools.length;
|
|
51
|
-
export function memberPage(contract) {
|
|
52
|
-
if (contract?.role !== "member")
|
|
53
|
-
return { endpoints: [], accounts: [], shared: [] };
|
|
54
|
-
return {
|
|
55
|
-
endpoints: contract.pools.map((pool) => ({ pool: pool.name, path: pool.humanPath })),
|
|
56
|
-
accounts: contract.connectors
|
|
57
|
-
.filter((connector) => connector.authScope === "personal")
|
|
58
|
-
.map((connector) => {
|
|
59
|
-
const connection = connector.connection ?? { state: "auth_required" };
|
|
60
|
-
return {
|
|
61
|
-
id: connector.id,
|
|
62
|
-
title: connector.title || connector.id,
|
|
63
|
-
toolCount: toolCount(connector),
|
|
64
|
-
badge: connector.connection
|
|
65
|
-
? {
|
|
66
|
-
label: connectorStatusLabel(connection.state, connection.problem),
|
|
67
|
-
tone: connectorStatusTone(connection.state),
|
|
68
|
-
}
|
|
69
|
-
: null,
|
|
70
|
-
summary: summaryFor(connector.manage, connection),
|
|
71
|
-
manage: connector.manage,
|
|
72
|
-
controls: controlsFor(connector.manage, connection),
|
|
73
|
-
};
|
|
74
|
-
}),
|
|
75
|
-
// Names and counts only: a shared connector's state is not the member's to see.
|
|
76
|
-
shared: contract.connectors
|
|
77
|
-
.filter((connector) => connector.authScope === "shared")
|
|
78
|
-
.map((connector) => ({
|
|
79
|
-
id: connector.id,
|
|
80
|
-
title: connector.title || connector.id,
|
|
81
|
-
toolCount: toolCount(connector),
|
|
82
|
-
})),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
export function toolCountLabel(count) {
|
|
86
|
-
return `${count} ${count === 1 ? "tool" : "tools"}`;
|
|
87
|
-
}
|
|
88
|
-
/** The in-page question before a member's action that takes something away. */
|
|
89
|
-
export function memberConfirmCopy(action, name) {
|
|
90
|
-
if (action === "oauth_restart") {
|
|
91
|
-
return { question: `Reconnect ${name}? It stops working until you finish in the new tab.`, confirm: "Reconnect" };
|
|
92
|
-
}
|
|
93
|
-
if (action === "oauth_disconnect") {
|
|
94
|
-
return { question: `Disconnect your ${name} account? Its tools stop working for you.`, confirm: "Disconnect" };
|
|
95
|
-
}
|
|
96
|
-
return { question: `Clear your ${name} credential? Its tools stop working for you.`, confirm: "Clear" };
|
|
97
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { endpointRows } from "./endpoints.js";
|
|
2
|
-
import { joinList } from "./connection-map.js";
|
|
3
|
-
import { connectorVerdict } from "./view.js";
|
|
4
|
-
const PROBLEM_TITLE = {
|
|
5
|
-
connector_unavailable: "is unavailable",
|
|
6
|
-
oauth_required: "needs authorization",
|
|
7
|
-
credential_required: "needs a credential",
|
|
8
|
-
auth_required: "needs a secret in the deployment",
|
|
9
|
-
credential_mismatch: "can't use its saved credential",
|
|
10
|
-
catalog_failed: "couldn't load its tools",
|
|
11
|
-
};
|
|
12
|
-
function affects(paths) {
|
|
13
|
-
return paths.length ? `Affects ${joinList(paths)}` : "No endpoint serves it";
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* One row per connector that is not healthy, in config order, then one per
|
|
17
|
-
* agent that sits in an endpoint without a working token. Each row carries
|
|
18
|
-
* the fix the rest of the UI already offers for it.
|
|
19
|
-
*/
|
|
20
|
-
export function attentionItems(contract, options = {}) {
|
|
21
|
-
if (contract.role !== "maintainer")
|
|
22
|
-
return [];
|
|
23
|
-
const rows = endpointRows(contract);
|
|
24
|
-
const connectors = [];
|
|
25
|
-
for (const config of contract.config?.connectors ?? []) {
|
|
26
|
-
const live = contract.live.connectors.find((c) => c.id === config.id);
|
|
27
|
-
if (!live)
|
|
28
|
-
continue;
|
|
29
|
-
const signals = options.signals && Object.hasOwn(options.signals, config.id) ? options.signals[config.id] : {};
|
|
30
|
-
const verdict = connectorVerdict(live, signals);
|
|
31
|
-
if ((verdict.tone !== "warn" && verdict.tone !== "danger") || !verdict.action)
|
|
32
|
-
continue;
|
|
33
|
-
const title = config.title ?? config.id;
|
|
34
|
-
const id = encodeURIComponent(config.id);
|
|
35
|
-
const paths = rows.filter((row) => row.groups.some((group) => group.config.id === config.id)).map((r) => r.path);
|
|
36
|
-
const connection = `/connectors/${id}#connection`;
|
|
37
|
-
const base = {
|
|
38
|
-
key: `connector:${config.id}`,
|
|
39
|
-
kind: "connector",
|
|
40
|
-
tone: verdict.tone,
|
|
41
|
-
subject: title,
|
|
42
|
-
action: { label: verdict.action, href: connection, ariaLabel: `${verdict.action} ${title}` },
|
|
43
|
-
href: connection,
|
|
44
|
-
};
|
|
45
|
-
// The server's problem code outranks a failed read on this page: the row
|
|
46
|
-
// keeps its fix prompt and says the details didn't load. "Unavailable"
|
|
47
|
-
// beside a failed read is the page's own (see connectorVerdict).
|
|
48
|
-
const reported = live.problem ?? (live.status === "error" ? "connector_unavailable" : undefined);
|
|
49
|
-
const problem = signals.failed && reported === "connector_unavailable" ? undefined : reported;
|
|
50
|
-
if (problem) {
|
|
51
|
-
connectors.push({
|
|
52
|
-
...base,
|
|
53
|
-
problem: PROBLEM_TITLE[problem],
|
|
54
|
-
body: signals.failed ? `Details didn't load · ${affects(paths)}` : affects(paths),
|
|
55
|
-
fix: { kind: problem, connectorId: config.id },
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
else if (signals.failed) {
|
|
59
|
-
connectors.push({
|
|
60
|
-
...base,
|
|
61
|
-
problem: "didn't load on this page",
|
|
62
|
-
body: affects(paths),
|
|
63
|
-
fix: null,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
else if (live.status !== "ok") {
|
|
67
|
-
connectors.push({
|
|
68
|
-
...base,
|
|
69
|
-
problem: verdict.label === "Authorization needed" ? "needs authorization" : "isn't connected",
|
|
70
|
-
body: affects(paths),
|
|
71
|
-
fix: null,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const agents = contract.agents
|
|
76
|
-
.filter((agent) => agent.pools.length &&
|
|
77
|
-
!agent.credentialTypes.includes("vercel_oidc") &&
|
|
78
|
-
agent.token &&
|
|
79
|
-
agent.token.state !== "issued")
|
|
80
|
-
.map((agent) => {
|
|
81
|
-
// An agent connects only at a pool's agent URL.
|
|
82
|
-
const paths = agent.pools.map((name) => contract.pools.find((pool) => pool.name === name)?.agentPath ?? `/agent/${name}`);
|
|
83
|
-
const revoked = agent.token?.state === "revoked";
|
|
84
|
-
return {
|
|
85
|
-
key: `agent:${agent.name}`,
|
|
86
|
-
kind: "agent",
|
|
87
|
-
tone: "warn",
|
|
88
|
-
subject: agent.name,
|
|
89
|
-
problem: revoked ? "has a revoked token" : "has no token",
|
|
90
|
-
body: `Can't connect at ${joinList(paths)}`,
|
|
91
|
-
action: options.tokensPage
|
|
92
|
-
? { label: "Issue token", href: "/tokens", ariaLabel: `Issue a token for ${agent.name}` }
|
|
93
|
-
: null,
|
|
94
|
-
href: options.tokensPage ? "/tokens" : null,
|
|
95
|
-
fix: null,
|
|
96
|
-
};
|
|
97
|
-
});
|
|
98
|
-
// Severity changes as individual details settle. Config order keeps surviving
|
|
99
|
-
// rows in place without waiting for the slowest connector's read.
|
|
100
|
-
return [...connectors, ...agents];
|
|
101
|
-
}
|
|
102
|
-
/** The numbers above the list, counted from the same map the page draws. */
|
|
103
|
-
export function overviewHeadline(contract, map, items, signals = {}) {
|
|
104
|
-
const bar = (contract.config?.connectors ?? []).flatMap((config) => {
|
|
105
|
-
const live = contract.live.connectors.find((c) => c.id === config.id);
|
|
106
|
-
if (!live)
|
|
107
|
-
return [];
|
|
108
|
-
const health = connectorVerdict(live, Object.hasOwn(signals, config.id) ? signals[config.id] : undefined);
|
|
109
|
-
return [{ id: config.id, title: config.title ?? config.id, tone: health.tone, label: health.label }];
|
|
110
|
-
});
|
|
111
|
-
const healthy = bar.filter((entry) => entry.tone === "ok").length;
|
|
112
|
-
const warn = bar.filter((entry) => entry.tone === "warn").length;
|
|
113
|
-
const danger = bar.filter((entry) => entry.tone === "danger").length;
|
|
114
|
-
const count = (kind) => map.nodes.filter((node) => node.kind === kind).length;
|
|
115
|
-
return {
|
|
116
|
-
connectors: bar.length,
|
|
117
|
-
healthy,
|
|
118
|
-
warn,
|
|
119
|
-
danger,
|
|
120
|
-
bar,
|
|
121
|
-
endpoints: count("endpoint"),
|
|
122
|
-
readOnlyEndpoints: map.nodes.filter((node) => node.kind === "endpoint" && node.readOnly).length,
|
|
123
|
-
people: count("person"),
|
|
124
|
-
agents: contract.agents.length,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client setup snippets for one MCP endpoint: the deployment's `/mcp` or a
|
|
3
|
-
* `/mcp/<pool>`. Three formats and no more — Claude Code's CLI, Codex's CLI,
|
|
4
|
-
* and the `mcpServers` JSON most other clients read — because a longer list
|
|
5
|
-
* is a list to keep current.
|
|
6
|
-
*
|
|
7
|
-
* None of them carries a credential. A client authenticates through the
|
|
8
|
-
* deployment's inbound auth — OAuth discovery where that is configured — and
|
|
9
|
-
* a token pasted into a snippet on this page would be one rendered by it.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* The name a client files this server under. Derived from the deployment's
|
|
13
|
-
* configured server name, reduced to the characters every client CLI accepts
|
|
14
|
-
* unquoted, with the pool appended so two endpoints never collide.
|
|
15
|
-
*/
|
|
16
|
-
export function clientServerName(serverName, pool) {
|
|
17
|
-
const base = (serverName ?? "")
|
|
18
|
-
.toLowerCase()
|
|
19
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
20
|
-
.replace(/-{2,}/g, "-")
|
|
21
|
-
.replace(/^-+|-+$/g, "")
|
|
22
|
-
.slice(0, 48) || "connecta";
|
|
23
|
-
return pool ? `${base}-${pool}` : base;
|
|
24
|
-
}
|
|
25
|
-
/** POSIX single-quoting, only when the value needs it. */
|
|
26
|
-
function shellWord(value) {
|
|
27
|
-
return /^[A-Za-z0-9_./:@%+=,~-]+$/.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
28
|
-
}
|
|
29
|
-
export function clientSetupCommands(name, url) {
|
|
30
|
-
return [
|
|
31
|
-
{
|
|
32
|
-
id: "claude",
|
|
33
|
-
label: "Claude Code",
|
|
34
|
-
text: `claude mcp add --transport http ${shellWord(name)} ${shellWord(url)}`,
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "codex",
|
|
38
|
-
label: "Codex",
|
|
39
|
-
text: `codex mcp add ${shellWord(name)} --url ${shellWord(url)}`,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "json",
|
|
43
|
-
label: "JSON config",
|
|
44
|
-
text: JSON.stringify({ mcpServers: { [name]: { type: "http", url } } }, null, 2),
|
|
45
|
-
},
|
|
46
|
-
];
|
|
47
|
-
}
|