@chibitek/chibi 0.1.157 → 0.1.158
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/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
- package/dist/assets/{arc-QwTuMsxp.js → arc-CSHQ9Dx3.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
- package/dist/assets/channel-bPN03jIt.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
- package/dist/assets/{index-BEJ8EKQt.js → index-CG4AFPb4.js} +1 -1
- package/dist/assets/{index-ziyH_uer.js → index-CMcr5k3Z.js} +199 -196
- package/dist/assets/index-CZjRcUdn.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
- package/dist/assets/{linear-Ch19KVyt.js → linear-hX6nFOQr.js} +1 -1
- package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DZObGLN7.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/connector-proxy.js +205 -0
- package/dist-server/container-mcp.js +149 -95
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +4459 -4013
- package/dist-server/openmausbot.js +596 -570
- package/dist-server/pair-cli.js +596 -570
- package/dist-server/server/avatar-subscription.js +51 -0
- package/dist-server/server/cli.js +5 -4
- package/dist-server/server/colima-docker.js +136 -0
- package/dist-server/server/composio.js +1051 -0
- package/dist-server/server/connector-proxy.js +249 -0
- package/dist-server/server/container-computer.js +31 -63
- package/dist-server/server/container-mcp.js +5 -0
- package/dist-server/server/engine-failover.js +22 -12
- package/dist-server/server/index.js +40 -16
- package/dist-server/server/mcp-public-registry.js +325 -0
- package/dist-server/server/port-in-use.js +21 -0
- package/dist-server/server/session-fallback.js +76 -0
- package/dist-server/server/vault/routes.js +43 -0
- package/dist-server/server/vault/runtime.js +48 -0
- package/dist-server/server/vault/store.js +177 -0
- package/dist-server/shared/connector-availability.js +12 -0
- package/dist-server/shared/desktop-private-port.js +1 -0
- package/dist-server/shared/subscription-bots.js +3 -0
- package/dist-server/shared/vault.js +59 -0
- package/dist-server/vps-container-mcp.js +85 -83
- package/package.json +1 -1
- package/dist/assets/channel-D2y0i2ps.js +0 -1
- package/dist/assets/index-DGWeJcck.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +0 -8
|
@@ -68,7 +68,7 @@ import { turnTimingFinish, turnTimingMark, turnTimingStart } from "./turn-timing
|
|
|
68
68
|
import { ATOMIC_TEAM_PACKAGE_ID, BUILTIN_CHIBI_REFUSALS, builtinChibiPatchRefusal } from "./builtin-chibi.js";
|
|
69
69
|
import { canAccessTeam, canContactTeamOf, canManagePeer, canReachPeer, canReachTeam, coordinatorSupervises, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, reachesEveryTeam, roomBoundaryHolds, teamStatusPrompt, PEER_ROSTER_MAX, resolveTeammate, roomPeerRosterSystemPrompt, rosterAbout, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
70
70
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
71
|
-
import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
|
|
71
|
+
import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, containerRuntimeError, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
|
|
72
72
|
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmAutoStartRuntime, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, claudeUserMcpEnabled, userMcpEnabledFor, addonEnabled, skillAuthoringEnabled, sharedComputersEnabled, teamRoomsEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, onDeviceHelpersEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, accountBotGrantPolicy, } from "./config.js";
|
|
73
73
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
74
74
|
import { GodModeSwitch } from "./god-mode.js";
|
|
@@ -263,6 +263,7 @@ import { createProviderAccountRoutes } from "./routes/provider-accounts.js";
|
|
|
263
263
|
import { accountStops, injectedModel, restScope } from "./injected-model-rest.js";
|
|
264
264
|
import { accountProvider } from "../shared/provider-accounts.js";
|
|
265
265
|
import { createAccountRoutes } from "./routes/accounts.js";
|
|
266
|
+
import { installNativeVault } from "./vault/runtime.js";
|
|
266
267
|
import { OAuthAccounts, takeOAuthClients } from "./oauth-accounts.js";
|
|
267
268
|
import { DesktopAccountVault, FileAccountVault, UnavailableAccountVault, takeAccountsFromEnvironment } from "./oauth-account-vault.js";
|
|
268
269
|
import { accountConnectorNames } from "./claude-account-connectors.js";
|
|
@@ -304,6 +305,7 @@ import { createBoardRoutes } from "./routes/board.js";
|
|
|
304
305
|
import { readBoardFromNexus } from "./board/nexus-board.js";
|
|
305
306
|
import { LocalModelInventory, localModePlacement, machineMemory } from "./local-models-mode.js";
|
|
306
307
|
import { FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
|
|
308
|
+
import { enterSessionFallback, onSessionFallback, ownOrFallback, sessionFallbackSpentLine, withoutSubscription } from "./session-fallback.js";
|
|
307
309
|
import { TURN_ERROR_CATEGORIES } from "../shared/runtime-events.js";
|
|
308
310
|
import { EngineProbes, PROBE_PROMPT, isProbeThread, probeThreadId } from "./engine-probe.js";
|
|
309
311
|
// [jev-verifier] opt-in Jev answer checks after a model switch (server/jev).
|
|
@@ -1154,9 +1156,14 @@ if (!hostedModels) {
|
|
|
1154
1156
|
// its add-on's capability, so it loads only while the add-on applies.
|
|
1155
1157
|
const bundledSkills = [...loadBundledSkills(), ...loadAddonSkillLibraries()];
|
|
1156
1158
|
const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
|
|
1159
|
+
// Electron's utility-process parent port is private to the desktop main
|
|
1160
|
+
// process. It carries desktop-only messages (approval and browser control,
|
|
1161
|
+
// phone secrets, the account vault) without putting them in the renderer.
|
|
1162
|
+
// Plain Node/dev launches have no parentPort.
|
|
1157
1163
|
// SAFETY: Electron's utility-process runtime is the only environment that
|
|
1158
1164
|
// supplies parentPort; plain Node intentionally leaves it absent.
|
|
1159
1165
|
const utilityParentPort = process.parentPort;
|
|
1166
|
+
installNativeVault(DATA_DIR, utilityParentPort, () => desktopMutationToken, req => authorizedInternalCapability(req.headers.authorization) !== null);
|
|
1160
1167
|
function postDesktopPrivateMessage(message) {
|
|
1161
1168
|
if (!utilityParentPort)
|
|
1162
1169
|
return false;
|
|
@@ -3045,12 +3052,19 @@ function turnEngineChoice(botId, threadId, needs, runOn) {
|
|
|
3045
3052
|
if (hostedModels || !profile || !task)
|
|
3046
3053
|
return undefined;
|
|
3047
3054
|
if (isSubscriptionBot(profile))
|
|
3048
|
-
return subscriptionTurnEngineChoice(profile, threadId);
|
|
3055
|
+
return ownOrFallback(threadId, () => subscriptionTurnEngineChoice(profile, threadId), () => sessionFallbackChoice(profile, threadId, needs, runOn), (stop) => stop instanceof SubscriptionStop, (stop) => new SubscriptionStop(sessionFallbackSpentLine(stop.message)));
|
|
3049
3056
|
if (isPersonalBot(profile))
|
|
3050
3057
|
return personalTurnEngineChoice(profile, threadId, needs, runOn);
|
|
3051
3058
|
const projected = store.projectBotForTask(botId, threadId);
|
|
3052
3059
|
return projected ? ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) : undefined;
|
|
3053
3060
|
}
|
|
3061
|
+
/** A company subscription bot's conversation on another provider's subscription, same class (server/session-fallback.ts). */
|
|
3062
|
+
function sessionFallbackChoice(profile, threadId, needs, runOn) {
|
|
3063
|
+
const projected = store.projectBotForTask(profile.id, threadId);
|
|
3064
|
+
const choice = isPersonalBot(profile) || !projected ? undefined
|
|
3065
|
+
: ladderTurnEngineChoice(withoutSubscription(profile), withoutSubscription(projected), threadId, needs, runOn, true);
|
|
3066
|
+
return choice && choice.selection.instanceId !== projected.modelSelection.instanceId ? { selection: choice.selection, persist: false } : undefined;
|
|
3067
|
+
}
|
|
3054
3068
|
/** A room member's engine at turn start, by turnEngineChoice's rules for
|
|
3055
3069
|
* the bot's own default (a room turn has no task of its own, so the bot's
|
|
3056
3070
|
* engine mode decides): resting and signed-out engines passed over, a
|
|
@@ -3066,7 +3080,7 @@ function roomTurnEngineChoice(bot, threadId) {
|
|
|
3066
3080
|
: ladderTurnEngineChoice(bot, projected, threadId);
|
|
3067
3081
|
return choice && { selection: choice.selection, persist: false };
|
|
3068
3082
|
}
|
|
3069
|
-
function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
|
|
3083
|
+
function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn, subscriptionsOnly = false) {
|
|
3070
3084
|
const candidates = engineSnapshots.current();
|
|
3071
3085
|
if (!candidates)
|
|
3072
3086
|
return undefined;
|
|
@@ -3075,7 +3089,7 @@ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
|
|
|
3075
3089
|
const { ladder } = plan;
|
|
3076
3090
|
const computer = turnComputer(projected, threadId, runOn);
|
|
3077
3091
|
const context = { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
|
|
3078
|
-
keyBandPaused: payPerUse.pausedUntil() !== undefined };
|
|
3092
|
+
keyBandPaused: subscriptionsOnly || payPerUse.pausedUntil() !== undefined };
|
|
3079
3093
|
const local = (resolution) => localModeTurn(resolution, { tier, context, needs, prefer: [projected.modelSelection.instanceId] });
|
|
3080
3094
|
const wider = (tried, modelClass) => resolveFailover({
|
|
3081
3095
|
ladder, classLadder: (cls) => classLadder(classedModels(), cls), failed: projected.modelSelection, context, tried,
|
|
@@ -3812,14 +3826,19 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
|
|
|
3812
3826
|
const computer = projected ? turnComputer(projected, turn.threadId, attempt.runOn) : undefined;
|
|
3813
3827
|
// A subscription bot fails over only to another account of its own
|
|
3814
3828
|
// provider on its side of the personal line; with none left it stops with
|
|
3815
|
-
// its one line
|
|
3829
|
+
// its one line, or a company bot's own conversation takes its session fallback (server/session-fallback.ts).
|
|
3816
3830
|
const subscriptionMove = isSubscriptionBot(profile)
|
|
3817
3831
|
? tried.size >= MAX_ENGINES_PER_TURN || !projected
|
|
3818
3832
|
? { line: subscriptionLine(profile, candidates) }
|
|
3819
3833
|
: subscriptionPlacement({ profile, projected, candidates, tried: new Set([...tried, turn.instanceId]), auto: attempt.persist, modelClass: neededClass })
|
|
3820
3834
|
: undefined;
|
|
3821
|
-
|
|
3835
|
+
const fallback = subscriptionMove && !subscriptionMove.choice && projected && !isPersonalBot(profile) && tried.size < MAX_ENGINES_PER_TURN && store.taskByThread(profile.id, turn.threadId);
|
|
3836
|
+
if (fallback && !enterSessionFallback(turn.threadId, false))
|
|
3837
|
+
return { line: sessionFallbackSpentLine(subscriptionMove.line) };
|
|
3838
|
+
if (subscriptionMove && !subscriptionMove.choice && !fallback)
|
|
3822
3839
|
return { line: subscriptionMove.line };
|
|
3840
|
+
if (fallback)
|
|
3841
|
+
[profile, projected] = [withoutSubscription(profile), withoutSubscription(projected)];
|
|
3823
3842
|
const personalTurn = isPersonalBot(profile) && !subscriptionMove;
|
|
3824
3843
|
// The Private Bot prefers personal engines and opted-in local compute,
|
|
3825
3844
|
// then company compute. startTurn rechecks company cost on every attempt.
|
|
@@ -3839,7 +3858,7 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
|
|
|
3839
3858
|
classLadder: (modelClass) => classLadder(classedModels(), modelClass),
|
|
3840
3859
|
failed: attempt.selection,
|
|
3841
3860
|
context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
|
|
3842
|
-
keyBandPaused: keyBandPausedFor(turn.threadId) },
|
|
3861
|
+
keyBandPaused: Boolean(fallback) || keyBandPausedFor(turn.threadId) },
|
|
3843
3862
|
tried,
|
|
3844
3863
|
modelClass: neededClass,
|
|
3845
3864
|
mayDrop: true,
|
|
@@ -3855,6 +3874,8 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
|
|
|
3855
3874
|
context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting } })
|
|
3856
3875
|
: { ...resolution, local: false };
|
|
3857
3876
|
const choice = placed.choice && placed.local ? { ...placed.choice, modelClass: "Añejo" } : placed.choice;
|
|
3877
|
+
if (!choice && fallback)
|
|
3878
|
+
return { line: subscriptionMove.line };
|
|
3858
3879
|
if (!choice && turn.failure?.kind === "provider_error")
|
|
3859
3880
|
return { passthrough: true };
|
|
3860
3881
|
if (!choice) {
|
|
@@ -3864,11 +3885,11 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
|
|
|
3864
3885
|
const resting = resolution.skipped.filter((skip) => skip.reason === "resting" || skip.reason === "tried").map((skip) => skip.instanceId);
|
|
3865
3886
|
const named = restingNamed([turn.instanceId, ...attempt.tried, ...resting]);
|
|
3866
3887
|
return {
|
|
3867
|
-
line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer)),
|
|
3888
|
+
line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer), { capped: tried.size >= MAX_ENGINES_PER_TURN }),
|
|
3868
3889
|
category: named.some((engine) => engine.needsSignIn || engine.keyExpired) ? "account" : "failover_exhausted",
|
|
3869
3890
|
};
|
|
3870
3891
|
}
|
|
3871
|
-
if (subscriptionMove) { /* subscriptions only: no pay-per-use episode */ }
|
|
3892
|
+
if (subscriptionMove || fallback) { /* subscriptions only: no pay-per-use episode */ }
|
|
3872
3893
|
else if (personalMove)
|
|
3873
3894
|
notePersonalPayPerUse(personalMove);
|
|
3874
3895
|
else
|
|
@@ -6973,15 +6994,15 @@ function localVmSetUp() {
|
|
|
6973
6994
|
}
|
|
6974
6995
|
/** The one gate for starting the container engine (App Settings →
|
|
6975
6996
|
* Computers): the person's switch, the organisation's policy, and a Local VM
|
|
6976
|
-
* actually set up here. Returns what it started (
|
|
6977
|
-
* Desktop
|
|
6997
|
+
* actually set up here. Returns what it started (Colima, the only engine
|
|
6998
|
+
* Atomic starts; never Docker Desktop) once it answers, or null. */
|
|
6978
6999
|
async function startLocalVmRuntime() {
|
|
6979
7000
|
if (!localVmAutoStartRuntime(cfg) || !managedPolicy.computerAllowed("localVm") || !localVmSetUp())
|
|
6980
7001
|
return null;
|
|
6981
7002
|
try {
|
|
6982
|
-
const { status, started } = await ensureContainerRuntime();
|
|
6983
|
-
if (started)
|
|
6984
|
-
console
|
|
7003
|
+
const { status, started, error } = await ensureContainerRuntime();
|
|
7004
|
+
if (started || error)
|
|
7005
|
+
console[error ? "error" : "log"](`[local-vm] ${started ? `started ${started}: ${status.daemonUp ? "ready" : "still not answering"}` : "not started"}${error ? `. ${error}` : ""}`);
|
|
6985
7006
|
return started && status.daemonUp ? started : null;
|
|
6986
7007
|
}
|
|
6987
7008
|
catch (error) {
|
|
@@ -7493,7 +7514,7 @@ async function unusableAutoPin(bot, threadId, plan, runOn) {
|
|
|
7493
7514
|
if (pinned === "vm" && !choice.available && !choice.starting && await startLocalVmRuntime()) {
|
|
7494
7515
|
choice = await computerChoice(bot, pinned);
|
|
7495
7516
|
}
|
|
7496
|
-
return choice.available || choice.starting ? null : choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
|
|
7517
|
+
return choice.available || choice.starting ? null : (pinned === "vm" ? containerRuntimeError() : null) ?? choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
|
|
7497
7518
|
}
|
|
7498
7519
|
function continueComputerSelection(threadId, generation, succeeded) {
|
|
7499
7520
|
const selection = computerSelectionTurns.get(threadId);
|
|
@@ -9632,7 +9653,8 @@ async function startTurn(botId, text, opts) {
|
|
|
9632
9653
|
: threadBusy(botId, threadId) ? undefined : turnEngineChoice(botId, threadId, { tools: true, vision: turnHasImages(opts?.userMessage) }, opts?.runOn);
|
|
9633
9654
|
const tierSelection = engineChoice?.persist ? engineChoice.selection : undefined;
|
|
9634
9655
|
const projectedBot = store.projectBotForTask(botId, threadId);
|
|
9635
|
-
const
|
|
9656
|
+
const fallbackBot = projectedBot && !opts?.engine && onSessionFallback(threadId) ? withoutSubscription(projectedBot) : projectedBot;
|
|
9657
|
+
const bot = fallbackBot && engineChoice ? { ...fallbackBot, modelSelection: engineChoice.selection } : fallbackBot;
|
|
9636
9658
|
if (!bot)
|
|
9637
9659
|
throw Object.assign(new Error("no such task"), { status: 404 });
|
|
9638
9660
|
// Routines and legacy peer delivery already have their own completion
|
|
@@ -14311,6 +14333,8 @@ async function readyLocalVmForTurn(botId, target, isCurrent = () => true, onNote
|
|
|
14311
14333
|
onNote?.(`Started ${started} for the Local VM`);
|
|
14312
14334
|
status = await containerComputerStatus(undefined, undefined, target);
|
|
14313
14335
|
}
|
|
14336
|
+
else if (containerRuntimeError() && isCurrent())
|
|
14337
|
+
onNote?.(containerRuntimeError());
|
|
14314
14338
|
}
|
|
14315
14339
|
noteLocalVmSeen(target, status);
|
|
14316
14340
|
if (!isCurrent())
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// The Plugins window's MCP Marketplace, backed by the official MCP Registry
|
|
2
|
+
// (https://registry.modelcontextprotocol.io, public, no key). This module
|
|
3
|
+
// fetches and normalizes registry pages and turns one entry, or a featured
|
|
4
|
+
// service from shared/mcp-featured.ts, into a config.json `mcpServers` entry.
|
|
5
|
+
// Adding goes through the same rules as every other MCP server: the entry is
|
|
6
|
+
// saved switched off, secrets are kept in its env or headers like any other
|
|
7
|
+
// server's, and nothing here logs a value.
|
|
8
|
+
import { featuredMcpIcon, featuredMcpServer, MCP_FEATURED } from "../shared/mcp-featured.js";
|
|
9
|
+
import { slugMcpName } from "./mcp-registry.js";
|
|
10
|
+
export const MCP_REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
|
|
11
|
+
/** The OAuth bridge Atomic already runs remote servers through (see
|
|
12
|
+
* electron/chibitek-defaults.mjs MCP_REMOTE_PACKAGE; a test keeps them equal).
|
|
13
|
+
* Pinned so its saved sign-ins survive a new mcp-remote release. */
|
|
14
|
+
export const MCP_REMOTE_PACKAGE = "mcp-remote@0.14.3";
|
|
15
|
+
const CACHE_MS = 60 * 60 * 1000;
|
|
16
|
+
const CACHE_MAX = 200;
|
|
17
|
+
const TIMEOUT_MS = 8_000;
|
|
18
|
+
const PAGE_SIZE = 30;
|
|
19
|
+
const OFFICIAL = "io.modelcontextprotocol.registry/official";
|
|
20
|
+
export const REGISTRY_OFFLINE_MESSAGE = "The MCP Registry could not be reached. Check your internet connection and try again.";
|
|
21
|
+
export class RegistryUnavailableError extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(REGISTRY_OFFLINE_MESSAGE);
|
|
24
|
+
this.name = "RegistryUnavailableError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function record(value) {
|
|
28
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
29
|
+
}
|
|
30
|
+
function text(value) {
|
|
31
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
32
|
+
}
|
|
33
|
+
function httpsUrl(value) {
|
|
34
|
+
const raw = text(value);
|
|
35
|
+
if (!raw)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(raw);
|
|
39
|
+
return url.protocol === "https:" ? url.toString() : undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const featuredByRegistryName = new Map(MCP_FEATURED.filter((server) => server.registryName).map((server) => [server.registryName, server]));
|
|
46
|
+
/** "io.github.acme/thing" -> "acme", "com.notion/mcp" -> "notion.com". */
|
|
47
|
+
export function publisherFor(name) {
|
|
48
|
+
const namespace = name.split("/")[0] ?? name;
|
|
49
|
+
if (namespace.startsWith("io.github."))
|
|
50
|
+
return namespace.slice("io.github.".length);
|
|
51
|
+
return namespace.split(".").reverse().join(".");
|
|
52
|
+
}
|
|
53
|
+
/** "notion-mcp-server" -> "Notion Mcp Server" when the entry has no title. */
|
|
54
|
+
function titleFor(name) {
|
|
55
|
+
const leaf = name.split("/").pop() ?? name;
|
|
56
|
+
return leaf.split(/[-_.]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ") || name;
|
|
57
|
+
}
|
|
58
|
+
function githubOwner(repositoryUrl) {
|
|
59
|
+
if (!repositoryUrl)
|
|
60
|
+
return undefined;
|
|
61
|
+
const match = /^https:\/\/github\.com\/([A-Za-z0-9-]{1,39})(?:\/|$)/.exec(repositoryUrl);
|
|
62
|
+
return match?.[1];
|
|
63
|
+
}
|
|
64
|
+
function input(kind, raw) {
|
|
65
|
+
const item = record(raw);
|
|
66
|
+
const name = text(item.name);
|
|
67
|
+
if (!name)
|
|
68
|
+
return null;
|
|
69
|
+
const description = text(item.description);
|
|
70
|
+
return {
|
|
71
|
+
kind,
|
|
72
|
+
name,
|
|
73
|
+
...(description ? { description: description.slice(0, 300) } : {}),
|
|
74
|
+
required: item.isRequired === true,
|
|
75
|
+
secret: item.isSecret === true,
|
|
76
|
+
...(kind === "header" && text(item.value) ? { template: text(item.value) } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function remotes(raw) {
|
|
80
|
+
if (!Array.isArray(raw))
|
|
81
|
+
return [];
|
|
82
|
+
return raw.flatMap((entry) => {
|
|
83
|
+
const item = record(entry);
|
|
84
|
+
const url = httpsUrl(item.url);
|
|
85
|
+
// A URL with {placeholders} needs per-person values Atomic cannot guess.
|
|
86
|
+
if (!url || /[{}]|%7B|%7D/i.test(url))
|
|
87
|
+
return [];
|
|
88
|
+
const type = item.type === "sse" ? "sse" : item.type === "streamable-http" ? "http" : null;
|
|
89
|
+
if (!type)
|
|
90
|
+
return [];
|
|
91
|
+
const headers = Array.isArray(item.headers) ? item.headers.map((header) => input("header", header)).filter((header) => header !== null) : [];
|
|
92
|
+
return [{ type, url, headers }];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function packages(raw) {
|
|
96
|
+
if (!Array.isArray(raw))
|
|
97
|
+
return [];
|
|
98
|
+
return raw.flatMap((entry) => {
|
|
99
|
+
const item = record(entry);
|
|
100
|
+
const registryType = text(item.registryType);
|
|
101
|
+
const identifier = text(item.identifier);
|
|
102
|
+
if (!registryType || !identifier)
|
|
103
|
+
return [];
|
|
104
|
+
const env = Array.isArray(item.environmentVariables)
|
|
105
|
+
? item.environmentVariables.map((variable) => input("env", variable)).filter((variable) => variable !== null)
|
|
106
|
+
: [];
|
|
107
|
+
return [{
|
|
108
|
+
registryType,
|
|
109
|
+
identifier,
|
|
110
|
+
...(text(item.version) ? { version: text(item.version) } : {}),
|
|
111
|
+
...(text(record(item.transport).type) ? { transport: text(record(item.transport).type) } : {}),
|
|
112
|
+
...(text(item.runtimeHint) ? { runtimeHint: text(item.runtimeHint) } : {}),
|
|
113
|
+
env,
|
|
114
|
+
}];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const SAFE_PACKAGE = /^(@[a-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*$/;
|
|
118
|
+
const SAFE_VERSION = /^[\w.+-]{1,64}$/;
|
|
119
|
+
/** Remote first (nothing to install), then npm, then PyPI. A remote whose
|
|
120
|
+
* headers are all optional is assumed to sign in with OAuth, so it runs
|
|
121
|
+
* through the mcp-remote bridge; one that requires a header (an API key) is
|
|
122
|
+
* kept as a plain URL entry with that header. */
|
|
123
|
+
function installPlan(server) {
|
|
124
|
+
const remote = server.remotes.find((item) => item.type === "http") ?? server.remotes.find((item) => item.type === "sse");
|
|
125
|
+
if (remote)
|
|
126
|
+
return { kind: "remote", remote, oauth: !remote.headers.some((header) => header.required) };
|
|
127
|
+
const stdio = (item) => !item.transport || item.transport === "stdio";
|
|
128
|
+
const npm = server.packages.find((item) => item.registryType === "npm" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
|
|
129
|
+
if (npm)
|
|
130
|
+
return { kind: "npm", pkg: npm };
|
|
131
|
+
const pypi = server.packages.find((item) => item.registryType === "pypi" && stdio(item) && SAFE_PACKAGE.test(item.identifier));
|
|
132
|
+
if (pypi)
|
|
133
|
+
return { kind: "pypi", pkg: pypi };
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function planInputs(plan) {
|
|
137
|
+
if (!plan)
|
|
138
|
+
return [];
|
|
139
|
+
const list = plan.kind === "remote"
|
|
140
|
+
? plan.oauth ? [] : plan.remote.headers.map(({ template: _template, ...header }) => header)
|
|
141
|
+
: plan.pkg.env;
|
|
142
|
+
return [...list].sort((a, b) => Number(b.required) - Number(a.required));
|
|
143
|
+
}
|
|
144
|
+
/** One registry list entry to the shape the renderer shows, or null when it
|
|
145
|
+
* is not the latest version or not usable at all. */
|
|
146
|
+
export function normalizeRegistryEntry(raw) {
|
|
147
|
+
const entry = record(raw);
|
|
148
|
+
const server = record(entry.server);
|
|
149
|
+
const meta = record(record(entry._meta)[OFFICIAL]);
|
|
150
|
+
if (meta.isLatest === false)
|
|
151
|
+
return null;
|
|
152
|
+
if (typeof meta.status === "string" && meta.status !== "active")
|
|
153
|
+
return null;
|
|
154
|
+
const name = text(server.name);
|
|
155
|
+
if (!name)
|
|
156
|
+
return null;
|
|
157
|
+
const repositoryUrl = httpsUrl(record(server.repository).url);
|
|
158
|
+
const websiteUrl = httpsUrl(server.websiteUrl);
|
|
159
|
+
const featured = featuredByRegistryName.get(name);
|
|
160
|
+
const icons = Array.isArray(server.icons) ? server.icons : [];
|
|
161
|
+
const registryIcon = icons.map((icon) => httpsUrl(record(icon).src)).find(Boolean);
|
|
162
|
+
const owner = githubOwner(repositoryUrl);
|
|
163
|
+
const iconUrl = featured
|
|
164
|
+
? featuredMcpIcon(featured.id)
|
|
165
|
+
: registryIcon ?? (owner ? `https://github.com/${owner}.png?size=64` : undefined);
|
|
166
|
+
const normalized = {
|
|
167
|
+
id: name,
|
|
168
|
+
name,
|
|
169
|
+
title: text(server.title) ?? featured?.name ?? titleFor(name),
|
|
170
|
+
description: (text(server.description) ?? "").slice(0, 400),
|
|
171
|
+
publisher: publisherFor(name),
|
|
172
|
+
version: text(server.version) ?? "",
|
|
173
|
+
...(repositoryUrl ? { repositoryUrl } : {}),
|
|
174
|
+
...(websiteUrl ? { websiteUrl } : {}),
|
|
175
|
+
remotes: remotes(server.remotes),
|
|
176
|
+
packages: packages(server.packages),
|
|
177
|
+
...(iconUrl ? { iconUrl } : {}),
|
|
178
|
+
...(featured ? { featured: featured.id } : {}),
|
|
179
|
+
};
|
|
180
|
+
const plan = installPlan(normalized);
|
|
181
|
+
return { ...normalized, install: plan?.kind ?? null, inputs: planInputs(plan) };
|
|
182
|
+
}
|
|
183
|
+
/** Normalize a page: latest versions only, one card per server name. */
|
|
184
|
+
export function normalizeRegistryPage(body) {
|
|
185
|
+
const page = record(body);
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const servers = [];
|
|
188
|
+
for (const raw of Array.isArray(page.servers) ? page.servers : []) {
|
|
189
|
+
const server = normalizeRegistryEntry(raw);
|
|
190
|
+
if (!server || seen.has(server.id))
|
|
191
|
+
continue;
|
|
192
|
+
seen.add(server.id);
|
|
193
|
+
servers.push(server);
|
|
194
|
+
}
|
|
195
|
+
const nextCursor = text(record(page.metadata).nextCursor);
|
|
196
|
+
return { servers, ...(nextCursor ? { nextCursor } : {}) };
|
|
197
|
+
}
|
|
198
|
+
/** A registry client with a one-hour in-memory cache. Every failure (offline,
|
|
199
|
+
* timeout, a registry error) becomes RegistryUnavailableError. */
|
|
200
|
+
export function createMcpPublicRegistry(options = {}) {
|
|
201
|
+
const doFetch = options.fetch ?? fetch;
|
|
202
|
+
const base = options.base ?? MCP_REGISTRY_BASE;
|
|
203
|
+
const now = options.now ?? Date.now;
|
|
204
|
+
const cache = new Map();
|
|
205
|
+
async function getJson(url) {
|
|
206
|
+
const hit = cache.get(url);
|
|
207
|
+
if (hit && now() - hit.at < CACHE_MS)
|
|
208
|
+
return hit.value;
|
|
209
|
+
let response;
|
|
210
|
+
try {
|
|
211
|
+
response = await doFetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new RegistryUnavailableError();
|
|
215
|
+
}
|
|
216
|
+
if (response.status === 404)
|
|
217
|
+
return null;
|
|
218
|
+
if (!response.ok)
|
|
219
|
+
throw new RegistryUnavailableError();
|
|
220
|
+
let value;
|
|
221
|
+
try {
|
|
222
|
+
value = await response.json();
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
throw new RegistryUnavailableError();
|
|
226
|
+
}
|
|
227
|
+
if (cache.size >= CACHE_MAX)
|
|
228
|
+
cache.delete(cache.keys().next().value);
|
|
229
|
+
cache.set(url, { at: now(), value });
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
async search({ search, cursor }) {
|
|
234
|
+
const params = new URLSearchParams({ limit: String(PAGE_SIZE), version: "latest" });
|
|
235
|
+
const query = search?.trim().slice(0, 100);
|
|
236
|
+
if (query)
|
|
237
|
+
params.set("search", query);
|
|
238
|
+
if (cursor)
|
|
239
|
+
params.set("cursor", cursor.slice(0, 300));
|
|
240
|
+
return normalizeRegistryPage(await getJson(`${base}/v0/servers?${params}`));
|
|
241
|
+
},
|
|
242
|
+
async get(id) {
|
|
243
|
+
const body = await getJson(`${base}/v0/servers/${encodeURIComponent(id)}/versions/latest`);
|
|
244
|
+
return body === null ? null : normalizeRegistryEntry(body);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** A free config name from the server's own name: "notion", then "notion-2". */
|
|
249
|
+
export function freeMcpName(preferred, taken) {
|
|
250
|
+
const base = slugMcpName(preferred) || "mcp-server";
|
|
251
|
+
if (!taken(base))
|
|
252
|
+
return base;
|
|
253
|
+
for (let n = 2; n < 100; n += 1) {
|
|
254
|
+
const candidate = `${base.slice(0, 32 - String(n).length - 1)}-${n}`;
|
|
255
|
+
if (!taken(candidate))
|
|
256
|
+
return candidate;
|
|
257
|
+
}
|
|
258
|
+
return `${base.slice(0, 23)}-${Date.now().toString(36).slice(-8)}`;
|
|
259
|
+
}
|
|
260
|
+
function bridgeEntry(url) {
|
|
261
|
+
return { command: "npx", args: ["-y", MCP_REMOTE_PACKAGE, url], env: {} };
|
|
262
|
+
}
|
|
263
|
+
function cleanValues(values) {
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const [key, value] of Object.entries(record(values))) {
|
|
266
|
+
if (typeof value === "string" && value.trim())
|
|
267
|
+
out[key] = value.trim();
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
/** The config entry a featured service becomes: its official URL through the
|
|
272
|
+
* OAuth bridge. */
|
|
273
|
+
export function planFeaturedAdd(featured, taken) {
|
|
274
|
+
return { ok: true, name: freeMcpName(featured.id, taken), entry: bridgeEntry(featured.url), kind: "remote", oauth: true };
|
|
275
|
+
}
|
|
276
|
+
/** The config entry a registry server becomes, with the person's values for
|
|
277
|
+
* its inputs. Required inputs that are missing are refused with the list, so
|
|
278
|
+
* the renderer can ask for them. */
|
|
279
|
+
export function planRegistryAdd(server, rawValues, taken) {
|
|
280
|
+
const values = cleanValues(rawValues);
|
|
281
|
+
const plan = installPlan(server);
|
|
282
|
+
if (!plan) {
|
|
283
|
+
const docker = server.packages.some((item) => item.registryType === "oci" || item.registryType === "docker");
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
status: 400,
|
|
287
|
+
code: "unsupported",
|
|
288
|
+
error: docker
|
|
289
|
+
? `${server.title} only ships as a Docker image, which Atomic can't add for you yet. Add it by hand under MCP servers.`
|
|
290
|
+
: `${server.title} doesn't offer a remote address or an npm or PyPI package, so Atomic can't add it for you. Add it by hand under MCP servers.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const inputs = planInputs(plan);
|
|
294
|
+
const missing = inputs.filter((item) => item.required && !values[item.name]);
|
|
295
|
+
if (missing.length) {
|
|
296
|
+
return { ok: false, status: 400, code: "needs_input", inputs, error: `${server.title} needs ${missing.map((item) => item.name).join(", ")} before it can be added.` };
|
|
297
|
+
}
|
|
298
|
+
const name = freeMcpName(server.featured ?? server.name.split("/").pop() ?? server.title, taken);
|
|
299
|
+
if (plan.kind === "remote") {
|
|
300
|
+
if (plan.oauth)
|
|
301
|
+
return { ok: true, name, entry: bridgeEntry(plan.remote.url), kind: "remote", oauth: true };
|
|
302
|
+
const headers = {};
|
|
303
|
+
for (const header of plan.remote.headers) {
|
|
304
|
+
const value = values[header.name];
|
|
305
|
+
if (!value)
|
|
306
|
+
continue;
|
|
307
|
+
// "Bearer {api_key}" -> "Bearer <value>" unless the value already is the whole header.
|
|
308
|
+
headers[header.name] = header.template && /\{[^}]+\}/.test(header.template) && !/\s/.test(value)
|
|
309
|
+
? header.template.replace(/\{[^}]+\}/, value)
|
|
310
|
+
: value;
|
|
311
|
+
}
|
|
312
|
+
return { ok: true, name, entry: { type: plan.remote.type, url: plan.remote.url, headers }, kind: "remote", oauth: false };
|
|
313
|
+
}
|
|
314
|
+
const env = {};
|
|
315
|
+
for (const variable of plan.pkg.env) {
|
|
316
|
+
if (values[variable.name])
|
|
317
|
+
env[variable.name] = values[variable.name];
|
|
318
|
+
}
|
|
319
|
+
const version = plan.pkg.version && SAFE_VERSION.test(plan.pkg.version) ? plan.pkg.version : undefined;
|
|
320
|
+
if (plan.kind === "npm") {
|
|
321
|
+
return { ok: true, name, entry: { command: "npx", args: ["-y", version ? `${plan.pkg.identifier}@${version}` : plan.pkg.identifier], env }, kind: "npm", oauth: false };
|
|
322
|
+
}
|
|
323
|
+
return { ok: true, name, entry: { command: "uvx", args: [plan.pkg.identifier], env }, kind: "pypi", oauth: false };
|
|
324
|
+
}
|
|
325
|
+
export { featuredMcpServer };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
/** True when anything accepts TCP on 127.0.0.1:port, not just an Atomic server. */
|
|
3
|
+
export function portInUse(port) {
|
|
4
|
+
return new Promise((done) => {
|
|
5
|
+
const socket = connect({ host: "127.0.0.1", port });
|
|
6
|
+
socket.setTimeout(1000);
|
|
7
|
+
socket.once("connect", () => { socket.destroy(); done(true); });
|
|
8
|
+
socket.once("timeout", () => { socket.destroy(); done(false); });
|
|
9
|
+
socket.once("error", () => done(false));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** The server binds port and port + 1 (webhooks); report the first busy one. */
|
|
13
|
+
export async function reportBusyPort(port, dataDirFlag) {
|
|
14
|
+
for (const candidate of [port, Number(process.env.OMB_WEBHOOK_PORT || port + 1)]) {
|
|
15
|
+
if (!(await portInUse(candidate)))
|
|
16
|
+
continue;
|
|
17
|
+
console.error(`Port ${candidate} is already in use by another app. Choose a free port, for example: chibi start --port ${port + 20}${dataDirFlag}`);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Session fallback for company subscription bots (docs/chibitek/engine-tiers.md,
|
|
2
|
+
// "Subscription bots: the one failover exception"). When no account of the
|
|
3
|
+
// bot's own provider can take a turn, one conversation may run on the same
|
|
4
|
+
// class from another provider's subscription for up to SESSION_FALLBACK_TURNS
|
|
5
|
+
// turns. The bot's own default never changes; the next turn tries its own
|
|
6
|
+
// provider first again.
|
|
7
|
+
import { SESSION_FALLBACK_TURNS } from "../shared/subscription-bots.js";
|
|
8
|
+
export { SESSION_FALLBACK_TURNS };
|
|
9
|
+
// ponytail: in memory, so a restart gives a conversation its 50 turns again;
|
|
10
|
+
// persist on the task record if that matters.
|
|
11
|
+
const fallbacks = new Map();
|
|
12
|
+
/** The thread's own provider took this turn: it leaves the fallback (its count stays). */
|
|
13
|
+
export function leaveSessionFallback(threadId) {
|
|
14
|
+
const entry = fallbacks.get(threadId);
|
|
15
|
+
if (entry)
|
|
16
|
+
entry.active = false;
|
|
17
|
+
}
|
|
18
|
+
/** Enter (or stay on) the fallback for this thread. A new turn counts one;
|
|
19
|
+
* a failover within a turn already on it counts nothing. False once the
|
|
20
|
+
* conversation has used its turns. */
|
|
21
|
+
export function enterSessionFallback(threadId, newTurn) {
|
|
22
|
+
const entry = fallbacks.get(threadId) ?? { turns: 0, active: false };
|
|
23
|
+
if (entry.active && !newTurn)
|
|
24
|
+
return true;
|
|
25
|
+
if (entry.turns >= SESSION_FALLBACK_TURNS) {
|
|
26
|
+
entry.active = false;
|
|
27
|
+
fallbacks.set(threadId, entry);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
entry.turns += 1;
|
|
31
|
+
entry.active = true;
|
|
32
|
+
fallbacks.set(threadId, entry);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** Turn start for a subscription bot: its own provider's engine (which
|
|
36
|
+
* leaves the fallback), else, on its stop, the fallback's, counted as a
|
|
37
|
+
* turn. No fallback keeps the stop; past the limit, `spent` of it. */
|
|
38
|
+
export function ownOrFallback(threadId, own, fallback, isStop, spent) {
|
|
39
|
+
try {
|
|
40
|
+
const choice = own();
|
|
41
|
+
leaveSessionFallback(threadId);
|
|
42
|
+
return choice;
|
|
43
|
+
}
|
|
44
|
+
catch (stop) {
|
|
45
|
+
if (!isStop(stop))
|
|
46
|
+
throw stop;
|
|
47
|
+
let choice;
|
|
48
|
+
try {
|
|
49
|
+
choice = fallback();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
choice = undefined;
|
|
53
|
+
}
|
|
54
|
+
if (choice === undefined)
|
|
55
|
+
throw stop;
|
|
56
|
+
if (!enterSessionFallback(threadId, true))
|
|
57
|
+
throw spent(stop);
|
|
58
|
+
return choice;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function onSessionFallback(threadId) {
|
|
62
|
+
return fallbacks.get(threadId)?.active === true;
|
|
63
|
+
}
|
|
64
|
+
export function sessionFallbackTurns(threadId) {
|
|
65
|
+
return fallbacks.get(threadId)?.turns ?? 0;
|
|
66
|
+
}
|
|
67
|
+
/** The bot as a fallback turn runs it: an ordinary company bot, so the
|
|
68
|
+
* ladder and every gate read it without the provider lock. */
|
|
69
|
+
export function withoutSubscription(bot) {
|
|
70
|
+
const { subscription: _subscription, ...rest } = bot;
|
|
71
|
+
return rest;
|
|
72
|
+
}
|
|
73
|
+
/** The line a company subscription bot stops with once its fallback turns are used. */
|
|
74
|
+
export function sessionFallbackSpentLine(line) {
|
|
75
|
+
return `${line} This conversation already used its ${SESSION_FALLBACK_TURNS} turns on another provider; download a handoff or hand it to another bot.`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PASS } from "../routes/table.js";
|
|
2
|
+
import { VaultError } from "./store.js";
|
|
3
|
+
/** The owner is supplied by the authenticated host, never from request JSON.
|
|
4
|
+
* Authorization runs on reads too, and is checked again after reading a body. */
|
|
5
|
+
export function createVaultRoutes(deps) {
|
|
6
|
+
return async ({ req, res, path, method, auth, json, readBody }) => {
|
|
7
|
+
if (path !== "/api/vault" && !path.startsWith("/api/vault/"))
|
|
8
|
+
return PASS;
|
|
9
|
+
res.setHeader("cache-control", "no-store");
|
|
10
|
+
const ownerId = deps.owner(req, auth);
|
|
11
|
+
if (!ownerId)
|
|
12
|
+
return json(res, 403, { error: "Only the Vault owner can manage saved items." });
|
|
13
|
+
const store = deps.store();
|
|
14
|
+
if (!store)
|
|
15
|
+
return json(res, 503, { error: "Vault encryption is unavailable. Unlock the secure store and try again." });
|
|
16
|
+
const item = /^\/api\/vault\/([0-9a-f-]{36})$/.exec(path);
|
|
17
|
+
if (path !== "/api/vault" && !item)
|
|
18
|
+
return json(res, 404, { error: "Vault item is unavailable." });
|
|
19
|
+
if (!(path === "/api/vault" ? ["GET", "POST"] : ["PUT", "DELETE"]).includes(method))
|
|
20
|
+
return json(res, 405, { error: "Method not allowed." });
|
|
21
|
+
try {
|
|
22
|
+
if (method === "GET")
|
|
23
|
+
return json(res, 200, { items: store.list(ownerId) });
|
|
24
|
+
const body = method === "DELETE" ? undefined : await readBody(req).catch(() => null);
|
|
25
|
+
if (deps.owner(req, auth) !== ownerId || deps.store() !== store)
|
|
26
|
+
return json(res, 403, { error: "Vault access changed. Try again." });
|
|
27
|
+
if (method === "POST")
|
|
28
|
+
return json(res, 201, { item: store.create(ownerId, body) });
|
|
29
|
+
if (method === "PUT" && item)
|
|
30
|
+
return json(res, 200, { item: store.update(ownerId, item[1], body) });
|
|
31
|
+
if (method === "DELETE" && item) {
|
|
32
|
+
store.remove(ownerId, item[1]);
|
|
33
|
+
return json(res, 200, { removed: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// No raw parser, filesystem, crypto or browser error crosses this route.
|
|
38
|
+
if (error instanceof VaultError)
|
|
39
|
+
return json(res, 400, { error: error.message });
|
|
40
|
+
return json(res, 503, { error: "Vault could not complete the request." });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|