@platless/axom-cli 0.1.0 → 0.5.9
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/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node gate for npm-built adapters (spec 2026-10-11 §6.2 "Node 门改读已装包的 engines.node").
|
|
3
|
+
*
|
|
4
|
+
* A JavaScript adapter declares the Node it needs in its own `package.json#engines.node`; npm/npx never
|
|
5
|
+
* enforced it, and the old gate matched a hard-coded package name against the spawn args, so any launch
|
|
6
|
+
* whose args were a path (a patched build, a direct bin launch) went straight past it. The gate is now
|
|
7
|
+
* a property of the **build** and of **the Node that will run it** (`Build.launch.runtime === 'node'`
|
|
8
|
+
* runs the entry with the daemon's fixed absolute Node, `AcpAgents`' `nodePath`):
|
|
9
|
+
*
|
|
10
|
+
* - at install / migration / certified reconcile ({@link assertBuildRunsOn} from `npmBuild`): an
|
|
11
|
+
* unsatisfied range fails the install and the build never becomes current;
|
|
12
|
+
* - at every launch (`registry/launch.ts` `launchForBuild`): the same check again, because the Node
|
|
13
|
+
* that runs a build can change after it was installed (a CLI upgrade bringing its own Node).
|
|
14
|
+
*
|
|
15
|
+
* Native entries do not run on Node, so `engines.node` says nothing about them. A range that does not
|
|
16
|
+
* parse is reported and not enforced — npm itself only warns about one.
|
|
17
|
+
*/
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { readFile } from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import semver from 'semver';
|
|
22
|
+
import { inside } from './model.js';
|
|
23
|
+
/** The adapter needs a Node the daemon would not run it with. Carries what the install result reports as `nodeTooOld`. */
|
|
24
|
+
export class AdapterNodeVersionError extends Error {
|
|
25
|
+
agentId;
|
|
26
|
+
required;
|
|
27
|
+
found;
|
|
28
|
+
constructor(agentId, packageName, version, required, found, nodePath) {
|
|
29
|
+
super(`Agent '${agentId}' (${packageName} ${version}) needs Node ${required}, but the Node this workstation ` +
|
|
30
|
+
`runs adapters with (${nodePath}) is ${found}. Upgrade that Node, or install a version of the adapter that supports it.`);
|
|
31
|
+
this.name = 'AdapterNodeVersionError';
|
|
32
|
+
this.agentId = agentId;
|
|
33
|
+
this.required = required;
|
|
34
|
+
this.found = found;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const versions = new Map();
|
|
38
|
+
/**
|
|
39
|
+
* The version (`v24.1.0`) of the Node at `nodePath`. The daemon's own Node answers from memory; any
|
|
40
|
+
* other is asked once per process (`<node> --version`) and remembered.
|
|
41
|
+
*/
|
|
42
|
+
export function nodeVersionOf(nodePath) {
|
|
43
|
+
if (path.resolve(nodePath) === path.resolve(process.execPath))
|
|
44
|
+
return Promise.resolve(process.version);
|
|
45
|
+
let known = versions.get(nodePath);
|
|
46
|
+
if (!known) {
|
|
47
|
+
known = new Promise((resolve, reject) => {
|
|
48
|
+
execFile(nodePath, ['--version'], { timeout: 10_000, windowsHide: true }, (error, stdout) => {
|
|
49
|
+
const version = stdout?.toString().trim() ?? '';
|
|
50
|
+
if (error || !semver.valid(version)) {
|
|
51
|
+
reject(new Error(`Cannot tell the version of the Node adapters run with (${nodePath}): ${error?.message ?? (version || 'no output')}`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
resolve(version);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// A failed probe is not remembered: the next launch asks again.
|
|
58
|
+
known.catch(() => versions.delete(nodePath));
|
|
59
|
+
versions.set(nodePath, known);
|
|
60
|
+
}
|
|
61
|
+
return known;
|
|
62
|
+
}
|
|
63
|
+
/** `engines.node` of the package installed at `packageDir`, when it declares one. */
|
|
64
|
+
export async function enginesNodeOf(packageDir) {
|
|
65
|
+
let manifest;
|
|
66
|
+
try {
|
|
67
|
+
manifest = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const range = manifest.engines?.node;
|
|
73
|
+
return typeof range === 'string' && range.trim() ? range.trim() : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Whether `nodeVersion` satisfies `range`; `undefined` when the range does not parse (not enforced). */
|
|
76
|
+
export function nodeSatisfies(nodeVersion, range) {
|
|
77
|
+
if (semver.validRange(range) === null)
|
|
78
|
+
return undefined;
|
|
79
|
+
return semver.satisfies(nodeVersion, range, { includePrerelease: true });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Throws {@link AdapterNodeVersionError} when `build` is a JavaScript npm build whose package's
|
|
83
|
+
* `engines.node` the Node at `nodePath` does not satisfy. Anything else passes.
|
|
84
|
+
*/
|
|
85
|
+
export async function assertBuildRunsOn(build, root, nodePath) {
|
|
86
|
+
if (build.launch.runtime !== 'node' || !build.package || (build.kind !== 'npm' && build.kind !== 'npm-patched'))
|
|
87
|
+
return;
|
|
88
|
+
const packageDir = inside(inside(root, build.dir), `node_modules/${build.package}`);
|
|
89
|
+
const range = await enginesNodeOf(packageDir);
|
|
90
|
+
if (range === undefined)
|
|
91
|
+
return;
|
|
92
|
+
const found = await nodeVersionOf(nodePath);
|
|
93
|
+
const satisfied = nodeSatisfies(found, range);
|
|
94
|
+
if (satisfied === undefined) {
|
|
95
|
+
console.warn(`[acp-builds] ${build.package} ${build.version} declares engines.node '${range}', which is not a valid range; not enforced.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!satisfied)
|
|
99
|
+
throw new AdapterNodeVersionError(build.agentId, build.package, build.version, range, found, nodePath);
|
|
100
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `builds.json` and the per-agent serialisation of install / switch / GC (spec §6.1–§6.3).
|
|
3
|
+
*
|
|
4
|
+
* ## Single writer
|
|
5
|
+
*
|
|
6
|
+
* Exactly one daemon owns a config directory, and this store is its only writer of `builds.json`.
|
|
7
|
+
* Every write is a read-modify-write of the whole document under one in-process mutex, then a
|
|
8
|
+
* temp-file + `rename(2)`: a reader (or a crash) sees the old document or the new one, never a torn
|
|
9
|
+
* one.
|
|
10
|
+
*
|
|
11
|
+
* ## Per-agent lock
|
|
12
|
+
*
|
|
13
|
+
* {@link BuildStore.withAgentLock} serialises everything that creates, selects or (later) collects an
|
|
14
|
+
* agent's builds. Two concurrent Updates of the same agent therefore run one after the other, and the
|
|
15
|
+
* second finds the first one's complete tree and downloads nothing. Different agents never wait on
|
|
16
|
+
* each other — only on the few milliseconds of the document write.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { mkdir, open, readdir, readFile, rename, rm, stat } from 'node:fs/promises';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { BUILDS_FILE, STAGING_DIR, TRASH_DIR, inside } from './model.js';
|
|
23
|
+
/** A FIFO async mutex per key. */
|
|
24
|
+
export class KeyedMutex {
|
|
25
|
+
tails = new Map();
|
|
26
|
+
async run(key, work) {
|
|
27
|
+
const previous = this.tails.get(key) ?? Promise.resolve();
|
|
28
|
+
let release;
|
|
29
|
+
const mine = new Promise((resolve) => { release = resolve; });
|
|
30
|
+
const tail = previous.then(() => mine);
|
|
31
|
+
this.tails.set(key, tail);
|
|
32
|
+
await previous;
|
|
33
|
+
try {
|
|
34
|
+
return await work();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
release();
|
|
38
|
+
if (this.tails.get(key) === tail)
|
|
39
|
+
this.tails.delete(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Staging trees older than this are leftovers of a killed install and may be swept. */
|
|
44
|
+
const STALE_STAGING_MS = 60 * 60_000;
|
|
45
|
+
function emptyDocument() {
|
|
46
|
+
return { schema: 1, agents: {} };
|
|
47
|
+
}
|
|
48
|
+
function isRecord(value) {
|
|
49
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
export class BuildStore {
|
|
52
|
+
/** The `acp-adapters` root. */
|
|
53
|
+
root;
|
|
54
|
+
agentLocks = new KeyedMutex();
|
|
55
|
+
documentLock = new KeyedMutex();
|
|
56
|
+
treeLocks = new KeyedMutex();
|
|
57
|
+
swept = false;
|
|
58
|
+
constructor(root) {
|
|
59
|
+
this.root = root;
|
|
60
|
+
}
|
|
61
|
+
get file() {
|
|
62
|
+
return path.join(this.root, BUILDS_FILE);
|
|
63
|
+
}
|
|
64
|
+
/** Serialise install / switch / GC for one agent. Not re-entrant. */
|
|
65
|
+
withAgentLock(agentId, work) {
|
|
66
|
+
return this.agentLocks.run(agentId, work);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Serialise everything that creates or deletes one tree that is not published by `rename(2)` (a uv
|
|
70
|
+
* build, see `model.ts`), keyed by its relative directory. Two agents could share a package; the
|
|
71
|
+
* agent lock alone would not keep them apart. Taken inside an agent lock, never the other way round.
|
|
72
|
+
*/
|
|
73
|
+
withTreeLock(dir, work) {
|
|
74
|
+
return this.treeLocks.run(dir, work);
|
|
75
|
+
}
|
|
76
|
+
/** The whole document. A missing file is an empty document; an unreadable one is reported and treated as empty. */
|
|
77
|
+
async read() {
|
|
78
|
+
return (await this.readChecked()).document;
|
|
79
|
+
}
|
|
80
|
+
/** {@link read}, also saying whether a file was there but unreadable (`commit` must not overwrite it blindly). */
|
|
81
|
+
async readChecked() {
|
|
82
|
+
let text;
|
|
83
|
+
try {
|
|
84
|
+
text = await readFile(this.file, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code === 'ENOENT')
|
|
88
|
+
return { document: emptyDocument(), unreadable: false };
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(text);
|
|
93
|
+
if (!isRecord(parsed) || parsed.schema !== 1 || !isRecord(parsed.agents))
|
|
94
|
+
throw new Error('unexpected shape');
|
|
95
|
+
return { document: parsed, unreadable: false };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.warn(`[acp-builds] ${this.file} is unreadable (${String(error)}); treating every adapter as not installed.`);
|
|
99
|
+
return { document: emptyDocument(), unreadable: true };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async agent(agentId) {
|
|
103
|
+
return (await this.read()).agents[agentId];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The agent's current build, when one is recorded **and** its directory is still on disk. A record
|
|
107
|
+
* whose tree was removed behind our back is not an installed adapter.
|
|
108
|
+
*/
|
|
109
|
+
async current(agentId) {
|
|
110
|
+
return this.currentIn(await this.read(), agentId);
|
|
111
|
+
}
|
|
112
|
+
currentIn(document, agentId) {
|
|
113
|
+
const entry = document.agents[agentId];
|
|
114
|
+
const build = entry?.current ? entry.builds?.[entry.current] : undefined;
|
|
115
|
+
if (!build)
|
|
116
|
+
return undefined;
|
|
117
|
+
try {
|
|
118
|
+
if (!existsSync(inside(this.root, build.dir)))
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return build;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Record `build` for its agent and, with `makeCurrent`, point `current` at it. The caller holds the
|
|
128
|
+
* agent's lock and has already renamed the build's tree into place.
|
|
129
|
+
*/
|
|
130
|
+
async commit(build, options) {
|
|
131
|
+
await this.documentLock.run('document', async () => {
|
|
132
|
+
const { document, unreadable } = await this.readChecked();
|
|
133
|
+
if (unreadable) {
|
|
134
|
+
// The next write starts from an empty document and would drop every other agent's records for
|
|
135
|
+
// good. Keep the unreadable file beside it, for a person (or a later repair) to recover them from.
|
|
136
|
+
const backup = `${this.file}.corrupt-${(options.now ?? new Date()).toISOString().replace(/[:.]/g, '-')}-${randomUUID().slice(0, 8)}`;
|
|
137
|
+
await rename(this.file, backup);
|
|
138
|
+
console.warn(`[acp-builds] moved the unreadable ${this.file} aside to ${backup} before rewriting it.`);
|
|
139
|
+
}
|
|
140
|
+
const previous = document.agents[build.agentId];
|
|
141
|
+
const entry = { ...(previous ?? {}), builds: { ...(previous?.builds ?? {}), [build.buildId]: build } };
|
|
142
|
+
if (options.makeCurrent) {
|
|
143
|
+
// The build the pointer leaves is the agent's rollback build (GC keeps one, Task 24).
|
|
144
|
+
if (previous?.current !== undefined && previous.current !== build.buildId)
|
|
145
|
+
entry.previous = previous.current;
|
|
146
|
+
entry.current = build.buildId;
|
|
147
|
+
entry.currentSetBy = options.setBy;
|
|
148
|
+
entry.currentSetAt = (options.now ?? new Date()).toISOString();
|
|
149
|
+
}
|
|
150
|
+
document.agents = { ...document.agents, [build.agentId]: entry };
|
|
151
|
+
await this.write(document);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A read-modify-write of the whole document under the document lock (Task 24: switch intents, GC). `change` mutates the document
|
|
156
|
+
* it is given and returns `false` when nothing changed (no write). An unreadable file is never overwritten from here: the change
|
|
157
|
+
* is refused (`false`) — only {@link commit} (an install) may set an unreadable document aside.
|
|
158
|
+
*/
|
|
159
|
+
async update(change) {
|
|
160
|
+
return this.documentLock.run('document', async () => {
|
|
161
|
+
const { document, unreadable } = await this.readChecked();
|
|
162
|
+
if (unreadable)
|
|
163
|
+
return false;
|
|
164
|
+
if (!change(document))
|
|
165
|
+
return false;
|
|
166
|
+
await this.write(document);
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** Every persistent switch intent (spec §6.3 step 7), by agent and session ref. */
|
|
171
|
+
async switchIntents() {
|
|
172
|
+
const document = await this.read();
|
|
173
|
+
return Object.entries(document.agents).flatMap(([agentId, entry]) => Object.entries(isRecord(entry?.switchPending) ? entry.switchPending : {}).map(([sessionRef, intent]) => ({ agentId, sessionRef, intent })));
|
|
174
|
+
}
|
|
175
|
+
/** Records (or replaces) the switch intent of `sessionRef` under `agentId`; one intent per ref (any other agent's is dropped). */
|
|
176
|
+
setSwitchIntent(agentId, sessionRef, intent) {
|
|
177
|
+
return this.update((document) => {
|
|
178
|
+
for (const [other, entry] of Object.entries(document.agents)) {
|
|
179
|
+
if (other !== agentId && entry.switchPending && Object.hasOwn(entry.switchPending, sessionRef)) {
|
|
180
|
+
const { [sessionRef]: _gone, ...rest } = entry.switchPending;
|
|
181
|
+
entry.switchPending = rest;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const entry = document.agents[agentId] ?? { builds: {} };
|
|
185
|
+
entry.switchPending = { ...(entry.switchPending ?? {}), [sessionRef]: intent };
|
|
186
|
+
document.agents = { ...document.agents, [agentId]: entry };
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** Drops the switch intents of `sessionRef` (every agent), or of every ref of `agentId`; `true` when one existed. */
|
|
191
|
+
cancelSwitchIntents(target) {
|
|
192
|
+
return this.update((document) => {
|
|
193
|
+
let changed = false;
|
|
194
|
+
for (const [agentId, entry] of Object.entries(document.agents)) {
|
|
195
|
+
if (!entry.switchPending)
|
|
196
|
+
continue;
|
|
197
|
+
if ('agentId' in target) {
|
|
198
|
+
if (agentId !== target.agentId || Object.keys(entry.switchPending).length === 0)
|
|
199
|
+
continue;
|
|
200
|
+
entry.switchPending = {};
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
else if (Object.hasOwn(entry.switchPending, target.sessionRef)) {
|
|
204
|
+
const { [target.sessionRef]: _gone, ...rest } = entry.switchPending;
|
|
205
|
+
entry.switchPending = rest;
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return changed;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Removes build records of `agentId` (GC, Task 24). Never the current one. Returns the records removed. */
|
|
213
|
+
async removeBuilds(agentId, buildIds) {
|
|
214
|
+
const removed = [];
|
|
215
|
+
await this.update((document) => {
|
|
216
|
+
const entry = document.agents[agentId];
|
|
217
|
+
if (!entry)
|
|
218
|
+
return false;
|
|
219
|
+
const builds = { ...entry.builds };
|
|
220
|
+
for (const buildId of buildIds) {
|
|
221
|
+
if (buildId === entry.current || !Object.hasOwn(builds, buildId))
|
|
222
|
+
continue;
|
|
223
|
+
removed.push(builds[buildId]);
|
|
224
|
+
delete builds[buildId];
|
|
225
|
+
}
|
|
226
|
+
if (removed.length === 0)
|
|
227
|
+
return false;
|
|
228
|
+
entry.builds = builds;
|
|
229
|
+
if (entry.previous !== undefined && !Object.hasOwn(builds, entry.previous))
|
|
230
|
+
delete entry.previous;
|
|
231
|
+
return true;
|
|
232
|
+
});
|
|
233
|
+
return removed;
|
|
234
|
+
}
|
|
235
|
+
async write(document) {
|
|
236
|
+
await mkdir(this.root, { recursive: true });
|
|
237
|
+
await writeFileDurably(this.file, `${JSON.stringify(document, null, 2)}\n`);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* A fresh, empty staging directory on the same filesystem as every final location, so the publishing
|
|
241
|
+
* `rename(2)` is atomic. The first call in a process also sweeps staging trees an earlier, killed
|
|
242
|
+
* install left behind (only ones older than an hour, so a concurrent install is never touched).
|
|
243
|
+
*/
|
|
244
|
+
async stagingDir() {
|
|
245
|
+
const staging = path.join(this.root, STAGING_DIR);
|
|
246
|
+
await mkdir(staging, { recursive: true });
|
|
247
|
+
if (!this.swept) {
|
|
248
|
+
this.swept = true;
|
|
249
|
+
await sweepOlderThan(staging, STALE_STAGING_MS);
|
|
250
|
+
// `.trash/` only ever holds trees already renamed out of use; a discard killed before its `rm`
|
|
251
|
+
// leaves one behind. Nothing is in flight there at first use, so all of it goes.
|
|
252
|
+
await sweepOlderThan(path.join(this.root, TRASH_DIR), -Infinity);
|
|
253
|
+
}
|
|
254
|
+
const directory = path.join(staging, randomUUID());
|
|
255
|
+
await mkdir(directory);
|
|
256
|
+
return directory;
|
|
257
|
+
}
|
|
258
|
+
/** Move `directory` to `.trash/` and delete it there, so nothing ever observes a half-deleted tree in place. */
|
|
259
|
+
async discard(directory) {
|
|
260
|
+
const trash = path.join(this.root, TRASH_DIR);
|
|
261
|
+
await mkdir(trash, { recursive: true });
|
|
262
|
+
const target = path.join(trash, randomUUID());
|
|
263
|
+
try {
|
|
264
|
+
await rename(directory, target);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (error.code === 'ENOENT')
|
|
268
|
+
return;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
await rm(target, { recursive: true, force: true }).catch(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Replace `file` with `content` so that a crash at any point leaves the old file or the new one, and a
|
|
276
|
+
* power loss after return keeps the new one: write a temp file, fsync it, rename it over `file`, then
|
|
277
|
+
* fsync the directory (where the platform lets a directory be opened — not Windows).
|
|
278
|
+
*/
|
|
279
|
+
export async function writeFileDurably(file, content) {
|
|
280
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
281
|
+
try {
|
|
282
|
+
const handle = await open(temporary, 'wx');
|
|
283
|
+
try {
|
|
284
|
+
await handle.writeFile(content, 'utf8');
|
|
285
|
+
await handle.sync();
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
await handle.close();
|
|
289
|
+
}
|
|
290
|
+
await syncDirectory(path.dirname(file));
|
|
291
|
+
await rename(temporary, file);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
await syncDirectory(path.dirname(file));
|
|
298
|
+
}
|
|
299
|
+
async function syncDirectory(directory) {
|
|
300
|
+
if (process.platform === 'win32')
|
|
301
|
+
return;
|
|
302
|
+
let handle;
|
|
303
|
+
try {
|
|
304
|
+
handle = await open(directory, 'r');
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await handle.sync();
|
|
311
|
+
}
|
|
312
|
+
catch { /* not supported on this filesystem */ }
|
|
313
|
+
finally {
|
|
314
|
+
await handle.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function sweepOlderThan(directory, ageMs) {
|
|
318
|
+
let names;
|
|
319
|
+
try {
|
|
320
|
+
names = await readdir(directory);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const cutoff = Date.now() - ageMs;
|
|
326
|
+
await Promise.all(names.map(async (name) => {
|
|
327
|
+
const target = path.join(directory, name);
|
|
328
|
+
try {
|
|
329
|
+
if ((await stat(target)).mtimeMs < cutoff)
|
|
330
|
+
await rm(target, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
catch { /* already gone */ }
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Publish a complete `staged` tree at `final` with one `rename(2)`. When another install already
|
|
337
|
+
* published the same immutable tree (`EEXIST`/`ENOTEMPTY`), the staged copy is dropped and the
|
|
338
|
+
* existing one is used: `published: false`.
|
|
339
|
+
*/
|
|
340
|
+
export async function publishStaged(staged, final) {
|
|
341
|
+
await mkdir(path.dirname(final), { recursive: true });
|
|
342
|
+
try {
|
|
343
|
+
await rename(staged, final);
|
|
344
|
+
return { published: true };
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
|
|
348
|
+
throw error;
|
|
349
|
+
await rm(staged, { recursive: true, force: true }).catch(() => undefined);
|
|
350
|
+
return { published: false };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { methods } from '@agentclientprotocol/sdk';
|
|
2
|
+
import { newSessionMetaFor } from './activityReporting.js';
|
|
3
|
+
import { normalizeConfigOptions } from './configOptions.js';
|
|
4
|
+
/** Updates kept for a session id this client has not yet learned (the pre-bind window). */
|
|
5
|
+
const EARLY_UPDATES_MAX = 32;
|
|
6
|
+
export function createCommandTableProbe() {
|
|
7
|
+
const early = [];
|
|
8
|
+
let target;
|
|
9
|
+
let onTable;
|
|
10
|
+
return {
|
|
11
|
+
configure(app) {
|
|
12
|
+
app.onNotification(methods.client.session.update, (ctx) => {
|
|
13
|
+
const update = ctx.params.update;
|
|
14
|
+
if (update.sessionUpdate !== 'available_commands_update')
|
|
15
|
+
return;
|
|
16
|
+
if (target === undefined) {
|
|
17
|
+
if (early.length >= EARLY_UPDATES_MAX)
|
|
18
|
+
early.shift();
|
|
19
|
+
early.push({ sessionId: ctx.params.sessionId, commands: update.availableCommands });
|
|
20
|
+
}
|
|
21
|
+
else if (ctx.params.sessionId === target) {
|
|
22
|
+
onTable?.(update.availableCommands);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
async observe(connection, cwd, timing) {
|
|
27
|
+
const meta = newSessionMetaFor(connection.agentInfo?.name);
|
|
28
|
+
const response = await connection.connection.agent.request(methods.agent.session.new, {
|
|
29
|
+
cwd, mcpServers: [], ...(meta ? { _meta: meta } : {}),
|
|
30
|
+
});
|
|
31
|
+
const configOptions = normalizeConfigOptions(response.configOptions, response.modes, response.models);
|
|
32
|
+
let raw;
|
|
33
|
+
let finish;
|
|
34
|
+
const settled = new Promise((resolve) => { finish = resolve; });
|
|
35
|
+
let quietTimer;
|
|
36
|
+
let ceilingTimer;
|
|
37
|
+
let firstTimer = setTimeout(() => finish?.(), timing.firstWindowMs);
|
|
38
|
+
const table = (commands) => {
|
|
39
|
+
if (Array.isArray(commands))
|
|
40
|
+
raw = commands;
|
|
41
|
+
// The first one: the first-table window is over; settle on quiet, under the ceiling.
|
|
42
|
+
if (firstTimer !== undefined) {
|
|
43
|
+
clearTimeout(firstTimer);
|
|
44
|
+
firstTimer = undefined;
|
|
45
|
+
ceilingTimer = setTimeout(() => finish?.(), timing.ceilingMs);
|
|
46
|
+
}
|
|
47
|
+
// Every further one restarts the quiet period (Claude's second table is the usable one).
|
|
48
|
+
if (quietTimer !== undefined)
|
|
49
|
+
clearTimeout(quietTimer);
|
|
50
|
+
quietTimer = setTimeout(() => finish?.(), timing.quietMs);
|
|
51
|
+
};
|
|
52
|
+
target = response.sessionId;
|
|
53
|
+
onTable = table;
|
|
54
|
+
for (const update of early.splice(0))
|
|
55
|
+
if (update.sessionId === target)
|
|
56
|
+
table(update.commands);
|
|
57
|
+
await settled;
|
|
58
|
+
if (firstTimer !== undefined)
|
|
59
|
+
clearTimeout(firstTimer);
|
|
60
|
+
if (quietTimer !== undefined)
|
|
61
|
+
clearTimeout(quietTimer);
|
|
62
|
+
if (ceilingTimer !== undefined)
|
|
63
|
+
clearTimeout(ceilingTimer);
|
|
64
|
+
onTable = undefined;
|
|
65
|
+
return { raw, configOptions };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* 就用这个 id 的情形(agent 的赢,什么都不合成)。
|
|
5
5
|
*/
|
|
6
6
|
export const SYNTHESIZED_MODES_CONFIG_ID = 'acp:modes';
|
|
7
|
+
/**
|
|
8
|
+
* daemon 保留 id:agent 只给 unstable 的 `models`(`SessionModelState`——hermes,以及不经自己的
|
|
9
|
+
* configOption 暴露模型的 agent)而没有自己的模型控件时合成。与 {@link SYNTHESIZED_MODES_CONFIG_ID}
|
|
10
|
+
* 同一条规则:agent 自己的控件赢(`id: "model"` 或 `category: "model"`,pi-acp 的
|
|
11
|
+
* `buildConfigOptions` 两者都用),合成只在缺席时补位。合成的是**分组** select(组 = provider,
|
|
12
|
+
* {@link modelGroupsOf}),页脚选择器据此按 provider 分层展示;设置它的值经 `processor/control.ts`
|
|
13
|
+
* 路由到 ACP 的 `session/set_model`,不走 `session/set_config_option`。
|
|
14
|
+
*/
|
|
15
|
+
export const SYNTHESIZED_MODEL_CONFIG_ID = 'acp:model';
|
|
7
16
|
function toSelectOption(o) {
|
|
8
17
|
return { value: o.value, name: o.name, ...(o.description != null ? { description: o.description } : {}) };
|
|
9
18
|
}
|
|
@@ -122,6 +131,88 @@ function toWire(option) {
|
|
|
122
131
|
}
|
|
123
132
|
return null;
|
|
124
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* One raw `SessionModelState` (`models` in a `session/new|load|fork|resume` result; hermes sends it
|
|
136
|
+
* unconditionally), sanitized — `null` when it isn't a record, `currentModelId` isn't a string,
|
|
137
|
+
* `availableModels` isn't an array, or nothing usable is left after entry-by-entry sanitizing.
|
|
138
|
+
* The same read-nothing-off-null boundary as {@link sanitizeModes}; the TS SDK's v1 schema does not
|
|
139
|
+
* model this unstable field at all, so this is the only validation it ever gets.
|
|
140
|
+
*/
|
|
141
|
+
function sanitizeModels(models) {
|
|
142
|
+
if (!isRecord(models) || typeof models.currentModelId !== 'string' || !Array.isArray(models.availableModels))
|
|
143
|
+
return null;
|
|
144
|
+
const availableModels = [];
|
|
145
|
+
for (const entry of models.availableModels) {
|
|
146
|
+
if (!isRecord(entry))
|
|
147
|
+
continue;
|
|
148
|
+
if (typeof entry.modelId !== 'string' || entry.modelId === '' || typeof entry.name !== 'string')
|
|
149
|
+
continue;
|
|
150
|
+
if (entry.description != null && typeof entry.description !== 'string')
|
|
151
|
+
continue;
|
|
152
|
+
availableModels.push({ modelId: entry.modelId, name: entry.name, description: entry.description });
|
|
153
|
+
}
|
|
154
|
+
if (availableModels.length === 0)
|
|
155
|
+
return null;
|
|
156
|
+
return { currentModelId: models.currentModelId, availableModels };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* `modelId` 的 provider 段:`anthropic:claude-opus-4.6` → `anthropic`;没有 `:` 的 id 没有可提取的
|
|
160
|
+
* provider,返回 `''` —— 它们全部落进同一个桶,而不是各成一组(每个组里只有一个没有 provider 的模型)。
|
|
161
|
+
*/
|
|
162
|
+
function providerKeyOf(modelId) {
|
|
163
|
+
const colon = modelId.indexOf(':');
|
|
164
|
+
return colon > 0 ? modelId.slice(0, colon) : '';
|
|
165
|
+
}
|
|
166
|
+
/** hermes 的描述字段写着 `Provider: Anthropic`——组名优先从那里取;取不到时,无 provider 桶叫
|
|
167
|
+
* `Models`,其余退回 provider key 原样。 */
|
|
168
|
+
function providerNameOf(key, group) {
|
|
169
|
+
for (const model of group) {
|
|
170
|
+
const match = /^Provider:\s*(.+)$/.exec((model.description ?? '').trim());
|
|
171
|
+
if (match)
|
|
172
|
+
return match[1];
|
|
173
|
+
}
|
|
174
|
+
return key === '' ? 'Models' : key;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `availableModels` → 分组 options(`SessionConfigSelectGroup`)。组 = `modelId` 的 provider 段,
|
|
178
|
+
* 遇到序保持(agent 自己的目录顺序就是它的推荐顺序);组内每一条的名字如果都以
|
|
179
|
+
* `<组名> · ` 开头(hermes:`Anthropic · claude-opus-4.6`),把这段前缀去掉——组标题已经说了它是谁。
|
|
180
|
+
*/
|
|
181
|
+
export function modelGroupsOf(availableModels) {
|
|
182
|
+
const byKey = new Map();
|
|
183
|
+
for (const model of availableModels) {
|
|
184
|
+
const key = providerKeyOf(model.modelId);
|
|
185
|
+
const bucket = byKey.get(key);
|
|
186
|
+
if (bucket)
|
|
187
|
+
bucket.push(model);
|
|
188
|
+
else
|
|
189
|
+
byKey.set(key, [model]);
|
|
190
|
+
}
|
|
191
|
+
return [...byKey.entries()].map(([key, group]) => {
|
|
192
|
+
const groupName = providerNameOf(key, group);
|
|
193
|
+
const prefix = `${groupName} · `;
|
|
194
|
+
const strip = group.length > 1 && group.every((model) => model.name.startsWith(prefix));
|
|
195
|
+
return {
|
|
196
|
+
group: key,
|
|
197
|
+
name: groupName,
|
|
198
|
+
options: group.map((model) => ({
|
|
199
|
+
value: model.modelId,
|
|
200
|
+
name: strip ? model.name.slice(prefix.length) : model.name,
|
|
201
|
+
...(model.description != null ? { description: model.description } : {}),
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* "This list already gives the reader a control for models" — `id: "model"`(pi-acp 的
|
|
208
|
+
* `MODEL_CONFIG_ID`)、`category: "model"`(ACP 给模型控件的归类,pi-acp 两者都用),或 agent 自己
|
|
209
|
+
* 占了保留 id。与 {@link modesAlreadyRepresented} 同一条规则:agent 自己的赢,合成的退位。不比较
|
|
210
|
+
* 选项列表:modes 那边的逐条比对是因为 pi 真的把同一份清单发了两遍,而模型控件的 `category`
|
|
211
|
+
* 归类是 ACP 明文写的、pi-acp 也遭守,靠它就够。
|
|
212
|
+
*/
|
|
213
|
+
export function modelsAlreadyRepresented(options) {
|
|
214
|
+
return options.some((o) => o.id === 'model' || o.id === SYNTHESIZED_MODEL_CONFIG_ID || o.category === 'model');
|
|
215
|
+
}
|
|
125
216
|
/**
|
|
126
217
|
* One raw `SessionMode` entry (`types.gen.d.ts:2590-2613`), sanitized — `null` when it isn't a
|
|
127
218
|
* record, or `id`/`name` are missing or not strings, or a `description` is present but not a
|
|
@@ -236,7 +327,7 @@ export function modesAlreadyRepresented(options, choices) {
|
|
|
236
327
|
* Nothing below this function re-derives either guarantee — that is the point of establishing it
|
|
237
328
|
* once, here.
|
|
238
329
|
*/
|
|
239
|
-
export function normalizeConfigOptions(configOptions, modes) {
|
|
330
|
+
export function normalizeConfigOptions(configOptions, modes, models) {
|
|
240
331
|
const rawOptions = Array.isArray(configOptions) ? configOptions.filter(isRecord) : [];
|
|
241
332
|
const out = rawOptions.map(toWire).filter((o) => o !== null);
|
|
242
333
|
const cleanModes = sanitizeModes(modes);
|
|
@@ -253,5 +344,16 @@ export function normalizeConfigOptions(configOptions, modes) {
|
|
|
253
344
|
});
|
|
254
345
|
}
|
|
255
346
|
}
|
|
347
|
+
const cleanModels = sanitizeModels(models);
|
|
348
|
+
if (cleanModels && !modelsAlreadyRepresented(out)) {
|
|
349
|
+
out.push({
|
|
350
|
+
id: SYNTHESIZED_MODEL_CONFIG_ID,
|
|
351
|
+
name: 'Model',
|
|
352
|
+
category: 'model',
|
|
353
|
+
type: 'select',
|
|
354
|
+
currentValue: cleanModels.currentModelId,
|
|
355
|
+
options: modelGroupsOf(cleanModels.availableModels),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
256
358
|
return out;
|
|
257
359
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function buildSessionMcpServers(userServers = [], lease) {
|
|
2
|
+
if (!lease)
|
|
3
|
+
return [...userServers];
|
|
4
|
+
const fail = () => { throw new Error('Delegation MCP configuration is unavailable'); };
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
url = new URL(lease.url);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return fail();
|
|
11
|
+
}
|
|
12
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash ||
|
|
13
|
+
url.pathname !== '/mcp/delegation' || !Number.isSafeInteger(lease.expiresAtEpochMs) ||
|
|
14
|
+
lease.expiresAtEpochMs <= Date.now() || userServers.some((server) => server.name === 'axom-delegation'))
|
|
15
|
+
return fail();
|
|
16
|
+
const headers = Object.entries(lease.headers);
|
|
17
|
+
if (headers.length !== 1 || headers[0][0].toLowerCase() !== 'authorization' ||
|
|
18
|
+
!/^Bearer [A-Za-z0-9_-]+$/.test(headers[0][1]))
|
|
19
|
+
return fail();
|
|
20
|
+
return [...userServers, {
|
|
21
|
+
type: 'http', name: 'axom-delegation', url: url.toString(),
|
|
22
|
+
headers: headers.map(([name, value]) => ({ name, value })),
|
|
23
|
+
}];
|
|
24
|
+
}
|