@platless/axom-cli 0.6.36 → 0.6.52
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 +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationPreparation.js +1 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate after the other pinned ZCode recipes: python3 <script> <pristine-package>."""
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
root = Path(__file__).resolve().parents[1]
|
|
9
|
+
package = Path(sys.argv[1])
|
|
10
|
+
assert json.loads((package / 'package.json').read_text())['version'] == '0.80.0'
|
|
11
|
+
files = {}
|
|
12
|
+
runtime = json.loads((root / 'runtime.json').read_text())
|
|
13
|
+
for name in runtime['package_patches']:
|
|
14
|
+
recipe = json.loads((root / name).read_text())
|
|
15
|
+
if recipe['package'] != 'zcode-acp-server' or recipe['id'] == 'zcode-env-account':
|
|
16
|
+
continue
|
|
17
|
+
for edit in recipe['edits']:
|
|
18
|
+
source = files.get(edit['file'], (package / edit['file']).read_text())
|
|
19
|
+
assert hashlib.sha256(source.encode()).hexdigest() == edit['before_sha256']
|
|
20
|
+
files[edit['file']] = source.replace(edit['before'], edit['after'], 1)
|
|
21
|
+
|
|
22
|
+
edits = []
|
|
23
|
+
def edit(file, before, after):
|
|
24
|
+
source = files.get(file, (package / file).read_text())
|
|
25
|
+
assert source.count(before) == 1, (file, before[:80], source.count(before))
|
|
26
|
+
result = source.replace(before, after, 1)
|
|
27
|
+
edits.append(dict(file=file, before_sha256=hashlib.sha256(source.encode()).hexdigest(),
|
|
28
|
+
after_sha256=hashlib.sha256(result.encode()).hexdigest(), before=before, after=after))
|
|
29
|
+
files[file] = result
|
|
30
|
+
|
|
31
|
+
edit('dist/config/bridge-accounts.js', 'import { randomUUID } from "node:crypto";',
|
|
32
|
+
'import { readEnvAccount } from "./axom-env-account.mjs";\nimport { randomUUID } from "node:crypto";')
|
|
33
|
+
edit('dist/config/bridge-accounts.js',
|
|
34
|
+
'function overlayAccounts() {\n try {\n return readBridgeAccounts();\n }\n catch (error) {\n warnOnce(`bridge-accounts: ignoring saved accounts. ${error.message}`);\n return {};\n }\n}',
|
|
35
|
+
'function overlayAccounts() {\n const env = readEnvAccount(codingPlanCatalog);\n try {\n return { ...readBridgeAccounts(), ...env };\n }\n catch (error) {\n warnOnce(`bridge-accounts: ignoring saved accounts. ${error.message}`);\n return env;\n }\n}')
|
|
36
|
+
|
|
37
|
+
edit('dist/index.js', 'const result = await server.initialize(ctx.params, ctx.client);',
|
|
38
|
+
'if (process.env.ZCODE_API_KEY) (await import("./config/bridge-accounts.js")).bridgeOverlays(); '
|
|
39
|
+
'const result = await server.initialize(ctx.params, ctx.client);')
|
|
40
|
+
edit('dist/index.js', 'const { buildAccountProviderConfig, codingPlanRequestAuthFor } = await import("./config/account-provider.js");',
|
|
41
|
+
'const { buildAccountProviderConfig, codingPlanRequestAuthFor, configProviderIdFor } = await import("./config/account-provider.js");')
|
|
42
|
+
edit('dist/index.js', 'if (!provider?.access?.entitled) continue;',
|
|
43
|
+
'if (!provider?.access?.entitled || (process.env.ZCODE_API_KEY && configProviderIdFor(providerId) !== process.env.ZCODE_PROVIDER)) continue;')
|
|
44
|
+
edit('dist/handlers/session.js',
|
|
45
|
+
'const pinnedModel = process.env.ZCODE_MODEL;\n if (pinnedProvider && pinnedModel)',
|
|
46
|
+
'const pinnedModel = process.env.ZCODE_MODEL ?? (process.env.ZCODE_API_KEY ? loadAllModels()[0]?.modelId : undefined);\n if (pinnedProvider && pinnedModel)')
|
|
47
|
+
|
|
48
|
+
asset = root / 'zcode/env-account.mjs'
|
|
49
|
+
recipe = dict(id='zcode-env-account', package='zcode-acp-server', versions=['0.80.0'],
|
|
50
|
+
assets=[dict(source='zcode/env-account.mjs', target='dist/config/axom-env-account.mjs',
|
|
51
|
+
sha256=hashlib.sha256(asset.read_bytes()).hexdigest())], edits=edits)
|
|
52
|
+
(root / 'zcode-env-account.json').write_text(json.dumps(recipe, ensure_ascii=False, indent=2) + '\n')
|
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
"id": "zcode-account-limits",
|
|
3
3
|
"package": "zcode-acp-server",
|
|
4
4
|
"versions": [
|
|
5
|
-
"0.
|
|
5
|
+
"0.80.0"
|
|
6
6
|
],
|
|
7
7
|
"edits": [
|
|
8
8
|
{
|
|
9
9
|
"file": "dist/index.js",
|
|
10
|
-
"before_sha256": "
|
|
11
|
-
"after_sha256": "
|
|
10
|
+
"before_sha256": "982001fdae54eb211ed3ad0af459035ce42696df2303ee0a50e94aff07b4f1b8",
|
|
11
|
+
"after_sha256": "82fc48b741090d8a178e5dca26f226960f9c2f222badc9c334347cc4b6300d22",
|
|
12
12
|
"before": " .onRequest(\"account/usage_stats\", z.object({}).passthrough(), () => accountUsageStatsViaHub())",
|
|
13
13
|
"after": " .onRequest(axomQuota.READ, z.object({}).passthrough(), async () => {\n const { buildAccountProviderConfig, codingPlanRequestAuthFor } = await import(\"./config/account-provider.js\");\n const { resolveQuotaHost } = await import(\"./quota/client.js\");\n let apiKey = null, family = \"\";\n for (const [providerId, provider] of Object.entries(buildAccountProviderConfig()?.providers ?? {})) {\n if (!provider?.access?.entitled) continue;\n const auth = codingPlanRequestAuthFor(providerId);\n if (auth?.apiKey) { apiKey = auth.apiKey; family = providerId; break; }\n }\n if (!apiKey) throw new Error(\"No entitled GLM coding plan: sign in once in the ZCode desktop app\");\n const resp = await fetch(resolveQuotaHost(family.startsWith(\"account:zai\") ? \"https://api.z.ai\" : \"\") + \"/api/monitor/usage/quota/limit\", { headers: { Accept: \"application/json, text/plain, */*\", Authorization: \"Bearer \" + apiKey }, signal: AbortSignal.timeout(8000) });\n const text = await resp.text();\n let json = null; try { json = text ? JSON.parse(text) : null; } catch {}\n if (resp.status === 429 || !json || json.success !== true) throw new Error(\"GLM quota read failed\" + (json?.msg ? \": \" + json.msg : \" (HTTP \" + resp.status + \")\"));\n const snapshot = axomQuota.zcode({ provider: family, level: json.data?.level, limits: json.data?.limits });\n axomQuotaNotify(snapshot);\n return snapshot;\n })\n .onRequest(\"account/usage_stats\", z.object({}).passthrough(), () => accountUsageStatsViaHub())"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"file": "dist/index.js",
|
|
17
|
-
"before_sha256": "
|
|
18
|
-
"after_sha256": "
|
|
17
|
+
"before_sha256": "82fc48b741090d8a178e5dca26f226960f9c2f222badc9c334347cc4b6300d22",
|
|
18
|
+
"after_sha256": "f0cff75cf1da73f39f72d6ac7f32b6e9811ecef5743a987f3fb043dbe7275d96",
|
|
19
19
|
"before": " .onRequest(\"initialize\", (ctx) => server.initialize(ctx.params, ctx.client))",
|
|
20
20
|
"after": " .onRequest(\"initialize\", async (ctx) => { const result = await server.initialize(ctx.params, ctx.client); const caps = result.agentCapabilities ?? {}; result.agentCapabilities = { ...caps, _meta: { ...caps._meta, \"axom/accountRateLimits\": { ...axomQuota.CAPABILITY, provider: \"zcode\" } } }; return result; })"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"file": "dist/index.js",
|
|
24
|
-
"before_sha256": "
|
|
25
|
-
"after_sha256": "
|
|
24
|
+
"before_sha256": "f0cff75cf1da73f39f72d6ac7f32b6e9811ecef5743a987f3fb043dbe7275d96",
|
|
25
|
+
"after_sha256": "00e43e449e3e428778a8b4ee2f1ab4535d28113bf7e1504e8215cba76d6629b4",
|
|
26
26
|
"before": "#!/usr/bin/env node\n/**\n * Entry point: wire the ZcodeAcpServer to a stdio ACP stream.\n *\n * The ACP",
|
|
27
27
|
"after": "#!/usr/bin/env node\n\nconst axomQuota = (() => {\n/** Account quota normalization. No I/O: passive events must never trigger a query. */\nconst READ = '_axom/account/rateLimits/read';\nconst UPDATED = '_axom/account/rateLimits/updated';\nconst CAPABILITY = { version: 2, read: true, updated: true };\nconst percent = v => typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : null;\nconst finite = v => { if (v == null || typeof v === 'boolean') return null; const n = typeof v === 'number' ? v : Number(v); return Number.isFinite(n) ? n : null; };\nconst time = (v, milliseconds = false) => {\n if (v == null || v === 0) return null;\n const d = new Date(typeof v === 'number' ? v * (milliseconds ? 1 : 1000) : v);\n return Number.isFinite(d.getTime()) ? d.toISOString() : null;\n};\nconst status = v => ({ allowed: 'allowed', allowed_warning: 'warning', rejected: 'blocked', normal: 'allowed' }[v] ?? 'unknown');\n/** Canonical window key rides the snapshot; the UI owns the key → display-label translation.\n * Windows without a canonical key keep their vendor content untouched. */\nconst windowKey = (seconds, period) => period === 'month' ? 'monthly' : seconds === 18000 ? 'five_hour' : seconds === 604800 ? 'weekly' : null;\nconst base = (provider, mode, raw, observedAt) => ({ schemaVersion: 1, updateMode: 'snapshot', provider, billingMode: mode, availability: mode === 'subscription' ? 'available' : mode === 'unknown' ? 'unknown' : 'not_applicable', observedAt: observedAt ?? new Date().toISOString(), plan: null, limits: [], balances: [], _meta: { ['_' + provider]: structuredClone(raw) } });\nfunction codex(raw, authType = 'chatgpt', observedAt) {\n const out = base('codex', ['chatgpt', 'chatgptAuthTokens'].includes(authType) ? 'subscription' : ['apiKey','apikey','amazonBedrock'].includes(authType) ? 'non_subscription' : 'unknown', raw, observedAt);\n if (out.billingMode !== 'subscription') return out;\n const buckets = Object.entries(raw.rateLimitsByLimitId ?? {});\n if (!buckets.length && raw.rateLimits) buckets.push([raw.rateLimits.limitId ?? 'codex', raw.rateLimits]);\n for (const [id, bucket] of buckets) {\n if (!bucket) continue;\n if (bucket.planType) out.plan = { id: bucket.planType, label: bucket.planType };\n for (const name of ['primary', 'secondary']) {\n const w = bucket[name]; if (!w) continue;\n const seconds = w.windowDurationMins == null ? null : w.windowDurationMins * 60;\n out.limits.push({ id: `${id}.${name}`, label: bucket.limitName ?? id, kind: 'subscription', windowSeconds: seconds, key: windowKey(seconds), usedPercent: percent(w.usedPercent), resetsAt: time(w.resetsAt), status: bucket.rateLimitReachedType ? 'blocked' : 'unknown', scope: { bucket: id } });\n }\n if (bucket.credits) out.balances.push({ id: `${id}.credits`, label: 'Credits', remaining: bucket.credits.balance ?? null, unit: 'credits', unlimited: bucket.credits.unlimited === true });\n }\n // Account IDs must not escape via provider extensions.\n delete out._meta._codex.accountId;\n return out;\n}\n/** App-server notifications describe changed buckets, not the complete account read. */\nfunction codexEvent(raw, authType, observedAt) {\n return { ...codex(raw, authType, observedAt), updateMode: 'delta' };\n}\nfunction claude(raw, observedAt) {\n const mode = raw.billing_mode ?? (raw.subscription_type || raw.rate_limits_available === true ? 'subscription' : 'unknown');\n const out = base('claude', mode, raw, observedAt);\n out.plan = raw.subscription_type ? { id: raw.subscription_type, label: raw.subscription_type } : null;\n if (mode === 'subscription' && !raw.rate_limits_available) out.availability = 'unavailable';\n const limits = raw.rate_limits ?? {};\n const rows = Array.isArray(limits.limits) ? limits.limits : [];\n if (rows.length) {\n rows.forEach((w, i) => {\n const seconds = w.group === 'session' ? 18000 : w.group === 'weekly' ? 604800 : null;\n out.limits.push({ id: `claude.${w.kind}.${i}`, label: w.scope?.model?.display_name ?? w.kind, kind: 'subscription', windowSeconds: seconds, key: windowKey(seconds), usedPercent: percent(w.percent), resetsAt: time(w.resets_at), status: status(w.severity), active: w.is_active ?? null, scope: w.scope ?? null });\n });\n } else for (const [id, seconds] of [['five_hour', 18000], ['seven_day', 604800], ['seven_day_opus',604800], ['seven_day_sonnet',604800]]) {\n const w = limits[id]; if (!w) continue;\n out.limits.push({ id: `claude.${id}`, label: id, kind: 'subscription', windowSeconds: seconds, key: windowKey(seconds), usedPercent: percent(w.utilization), resetsAt: time(w.resets_at), status: 'unknown', scope: id.endsWith('opus') || id.endsWith('sonnet') ? { model: id.split('_').at(-1) } : null });\n }\n const spend=limits.spend;\n if(spend?.enabled && spend.used && spend.limit) out.limits.push({id:'claude.extra_spend',label:'Extra usage',kind:'spend',windowSeconds:null,usedPercent:percent(spend.percent),resetsAt:null,status:status(spend.severity),used:String(spend.used.amount_minor),limit:String(spend.limit.amount_minor),unit:'money',currency:spend.used.currency,exponent:spend.used.exponent});\n if(spend?.balance?.amount_minor != null) out.balances.push({id:'claude.extra_balance',label:'Extra usage balance',remaining:String(spend.balance.amount_minor),unit:'money',currency:spend.balance.currency,exponent:spend.balance.exponent});\n return out;\n}\nfunction claudeEvent(raw, previous = null, observedAt) {\n const out = previous ? structuredClone(previous) : base('claude',raw.rateLimitType?'subscription':'unknown',{},observedAt);\n out.updateMode = 'delta';\n out.balances = [];\n out.observedAt = observedAt ?? new Date().toISOString();\n // Other sessions may have refreshed the account since this process's cached read.\n // Send only facts from this event; the server owns the merged account observation.\n out.plan = null;\n out._meta = { _claude: { rateLimitEvent: raw } };\n const seconds = raw.rateLimitType === 'five_hour' ? 18000 : raw.rateLimitType?.startsWith('seven_day') ? 604800 : null;\n if (seconds !== null) {\n const id = `claude.${raw.rateLimitType}`;\n const scoped = !['five_hour','seven_day'].includes(raw.rateLimitType);\n const prior=out.limits.find(w => w.windowSeconds===seconds && JSON.stringify(w.scope??null)===JSON.stringify(scoped ? {model:raw.rateLimitType.split('_').at(-1)}:null));\n const row = { id: prior?.id ?? id, label: raw.rateLimitType, kind: 'subscription', windowSeconds: seconds, key: windowKey(seconds), usedPercent: raw.utilization == null ? null : percent(raw.utilization * 100), resetsAt: time(raw.resetsAt), status: status(raw.status), scope: scoped ? { model: raw.rateLimitType.split('_').at(-1) } : null };\n // Event utilization is a ratio, unlike get_usage's percentage. Match the existing window without duplicating it.\n out.limits = [row];\n } else out.limits = [];\n return out;\n}\nfunction kimi(raw, observedAt) {\n const quota = raw.quota ?? raw;\n const out = base('kimi','subscription',quota,observedAt);\n for (const [id, seconds] of [['limit5h',18000],['limit7d',604800],['monthTotal',null]]) {\n const w = quota.usages?.[id]; if (!w) continue;\n out.limits.push({ id: `kimi.${id}`, label: id, kind: 'subscription', windowSeconds: seconds, period: id === 'monthTotal' ? 'month' : null, key: windowKey(seconds, id === 'monthTotal' ? 'month' : null), usedPercent: percent(w.usedRatio * 100), resetsAt: time(w.resetAt), status: 'unknown' });\n }\n const wallet = quota.extraUsage;\n if (wallet) out.balances.push({ id:'kimi.extra',label:'Extra usage',remaining:String(wallet.balanceCents),unit:'money',currency:wallet.currency,exponent:2 });\n return out;\n}\n/** GLM quota `percentage` is the USED share (measured: 5% for 58/1000); counters win when present.\n * Counter-derived shares are rounded to 2 decimals — 100-94.2 keeps fp noise otherwise. */\nconst glmPercent = limit => {\n const used = finite(limit.currentValue), remaining = finite(limit.remaining), total = finite(limit.usage);\n const cap = remaining !== null && used !== null ? remaining + used : total;\n const round2 = v => v == null ? null : Math.round(v * 100) / 100;\n if (cap !== null && cap > 0 && remaining !== null && remaining >= 0 && remaining <= cap) return round2(100 - percent(remaining / cap * 100));\n if (cap !== null && cap > 0 && used !== null && used >= 0 && used <= cap) return round2(percent(used / cap * 100));\n return percent(limit.percentage);\n};\nfunction zcode(raw, observedAt) {\n // Raw: the GLM quota endpoint's data block — { level, limits: [{ type, number, usage,\n // currentValue, remaining, percentage, nextResetTime(epoch ms), usageDetails }] }. Which windows\n // exist is plan-dependent (measured pro plan: 5h prompts + a tool/MCP allowance; no weekly row —\n // a `number: 7` window maps when a plan returns one, and nothing is invented when it doesn't).\n const limits = Array.isArray(raw?.limits) ? raw.limits : [];\n const level = typeof raw?.level === 'string' && raw.level ? raw.level : null;\n const out = base('zcode', limits.length ? 'subscription' : 'unknown', raw, observedAt);\n out.plan = level ? { id: level, label: level } : null;\n let tools = 0;\n limits.forEach((limit, i) => {\n if (!limit || typeof limit !== 'object') return;\n const resetsAt = time(limit.nextResetTime, true);\n if (limit.type === 'TOKENS_LIMIT') {\n // GLM window convention: number 5 → the 5-hour prompt window, 7 → weekly.\n // Ids mirror the adapter's own /quota keys (token_5h / token_week).\n const seconds = limit.number === 5 ? 18000 : limit.number === 7 ? 604800 : null;\n const label = limit.number === 5 ? '5h' : limit.number === 7 ? 'Week' : `token ${limit.number ?? i}`;\n const id = limit.number === 5 ? 'zcode.token_5h' : limit.number === 7 ? 'zcode.token_week' : `zcode.token_${limit.number ?? i}`;\n out.limits.push({ id, label, kind: 'subscription', windowSeconds: seconds, key: windowKey(seconds), usedPercent: glmPercent(limit), resetsAt, status: 'unknown' });\n } else if (limit.type === 'TIME_LIMIT' || limit.type === 'MCP_LIMIT') {\n // Built-in tool/MCP allowance: absolute counters (unit 'calls'); per-model detail stays in _meta.\n const used = finite(limit.currentValue), total = finite(limit.usage), remaining = finite(limit.remaining);\n out.limits.push({ id: tools ? `zcode.mcp.${i}` : 'zcode.mcp', label: 'MCP', kind: 'subscription', windowSeconds: null, usedPercent: glmPercent(limit), ...(used !== null && total !== null ? { used: String(used), limit: String(total), unit: 'calls' } : {}), resetsAt, status: remaining === 0 ? 'blocked' : used !== null || limit.percentage != null ? 'allowed' : 'unknown' });\n tools++;\n }\n });\n return out;\n}\nfunction commandcode(raw, observedAt) {\n const subscription = raw.subscription?.data ?? raw.subscription;\n const windows = raw.credits?.windowLimits ?? raw.windowLimits;\n const subscribed = ['active','trialing','past_due'].includes(subscription?.status) || windows?.limited === true;\n const mode = subscribed ? 'subscription' : subscription || windows ? 'non_subscription' : 'unknown';\n const out = base('commandcode',mode,raw,observedAt);\n out.plan = subscription?.planId ? { id:subscription.planId,label:subscription.planId } : null;\n for (const [id, seconds] of [['fiveHour',18000],['weekly',604800]]) {\n const w=windows?.[id]; if (!w) continue;\n out.limits.push({ id:`commandcode.${id}`,label:id,kind:'subscription',windowSeconds:seconds,key:windowKey(seconds),usedPercent:w.cap > 0 ? percent(w.used / w.cap * 100) : null,used:String(w.used),limit:String(w.cap),unit:'credits',resetsAt:time(w.resetAt,true),status:w.exceeded === true ? 'blocked' : 'allowed' });\n }\n const credits=raw.credits?.credits ?? raw.credits;\n if (subscribed && credits?.monthlyCredits != null) {\n const cap=raw.monthly?.limit;\n const known=typeof cap==='number' && Number.isFinite(cap) && cap>0;\n const used=known ? Math.max(0,cap-credits.monthlyCredits) : null;\n out.limits.push({id:'commandcode.monthly',label:'Monthly',kind:'subscription',windowSeconds:null,period:'month',key:windowKey(null,'month'),usedPercent:known?percent(used/cap*100):null,resetsAt:time(subscription?.currentPeriodEnd,true),status:'unknown',...(known?{used:String(used),limit:String(cap),unit:'credits'}:{})});\n }\n for (const [id,label] of [['monthlyCredits','Monthly credits'],['purchasedCredits','Purchased credits'],['freeCredits','Free credits']]) if (credits?.[id] != null) out.balances.push({ id:`commandcode.${id}`,label,remaining:String(credits[id]),unit:'credits' });\n return out;\n}\n\nreturn {READ,UPDATED,CAPABILITY,codex,codexEvent,claude,claudeEvent,kimi,commandcode,zcode};\n})();\nconst axomQuotaNotify = snapshot => process.stdout.write(JSON.stringify({jsonrpc:\"2.0\",method:axomQuota.UPDATED,params:snapshot})+\"\\n\");\n/**\n * Entry point: wire the ZcodeAcpServer to a stdio ACP stream.\n *\n * The ACP"
|
|
28
28
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "zcode-env-account",
|
|
3
|
+
"package": "zcode-acp-server",
|
|
4
|
+
"versions": [
|
|
5
|
+
"0.80.0"
|
|
6
|
+
],
|
|
7
|
+
"assets": [
|
|
8
|
+
{
|
|
9
|
+
"source": "zcode/env-account.mjs",
|
|
10
|
+
"target": "dist/config/axom-env-account.mjs",
|
|
11
|
+
"sha256": "b936067b9ee2ee42dfd9b331e5c642022c38a8739dc6e66d7bcfeb2e293ff046"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"edits": [
|
|
15
|
+
{
|
|
16
|
+
"file": "dist/config/bridge-accounts.js",
|
|
17
|
+
"before_sha256": "aefcb48f6cf64085e3cf3fca76ec16a0e1edf65e34033a7be41a89ed1521492f",
|
|
18
|
+
"after_sha256": "935683330d9c23e50ded074a5218b7651a6344a5026dd83f7a3a3b0d9aadec97",
|
|
19
|
+
"before": "import { randomUUID } from \"node:crypto\";",
|
|
20
|
+
"after": "import { readEnvAccount } from \"./axom-env-account.mjs\";\nimport { randomUUID } from \"node:crypto\";"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"file": "dist/config/bridge-accounts.js",
|
|
24
|
+
"before_sha256": "935683330d9c23e50ded074a5218b7651a6344a5026dd83f7a3a3b0d9aadec97",
|
|
25
|
+
"after_sha256": "7000e117941758917c00f6564e1333385541e79777ef712c8a7870d454fbd7d1",
|
|
26
|
+
"before": "function overlayAccounts() {\n try {\n return readBridgeAccounts();\n }\n catch (error) {\n warnOnce(`bridge-accounts: ignoring saved accounts. ${error.message}`);\n return {};\n }\n}",
|
|
27
|
+
"after": "function overlayAccounts() {\n const env = readEnvAccount(codingPlanCatalog);\n try {\n return { ...readBridgeAccounts(), ...env };\n }\n catch (error) {\n warnOnce(`bridge-accounts: ignoring saved accounts. ${error.message}`);\n return env;\n }\n}"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"file": "dist/index.js",
|
|
31
|
+
"before_sha256": "00e43e449e3e428778a8b4ee2f1ab4535d28113bf7e1504e8215cba76d6629b4",
|
|
32
|
+
"after_sha256": "d07d3900a6d88e29d4e18d562b4298cddf362e792e5a861ad839ba79cc2c92e7",
|
|
33
|
+
"before": "const result = await server.initialize(ctx.params, ctx.client);",
|
|
34
|
+
"after": "if (process.env.ZCODE_API_KEY) (await import(\"./config/bridge-accounts.js\")).bridgeOverlays(); const result = await server.initialize(ctx.params, ctx.client);"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"file": "dist/index.js",
|
|
38
|
+
"before_sha256": "d07d3900a6d88e29d4e18d562b4298cddf362e792e5a861ad839ba79cc2c92e7",
|
|
39
|
+
"after_sha256": "f5945f9fa858c03ea183176c57b0c1794925779f0c8b26d9acd970316351c548",
|
|
40
|
+
"before": "const { buildAccountProviderConfig, codingPlanRequestAuthFor } = await import(\"./config/account-provider.js\");",
|
|
41
|
+
"after": "const { buildAccountProviderConfig, codingPlanRequestAuthFor, configProviderIdFor } = await import(\"./config/account-provider.js\");"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"file": "dist/index.js",
|
|
45
|
+
"before_sha256": "f5945f9fa858c03ea183176c57b0c1794925779f0c8b26d9acd970316351c548",
|
|
46
|
+
"after_sha256": "28fcf1c87700d0da9b7820e778d507208ae66c22b340d02f1c3719cba2888a62",
|
|
47
|
+
"before": "if (!provider?.access?.entitled) continue;",
|
|
48
|
+
"after": "if (!provider?.access?.entitled || (process.env.ZCODE_API_KEY && configProviderIdFor(providerId) !== process.env.ZCODE_PROVIDER)) continue;"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"file": "dist/handlers/session.js",
|
|
52
|
+
"before_sha256": "ba9c2d4c131356661d5c79a89c840cb2d7a5dca65005c1d65bb5dd72871a7dfe",
|
|
53
|
+
"after_sha256": "bdf68cef3906a597f5f568a3f53cca880391bd7f6ab5700ba8ed3a2572509d34",
|
|
54
|
+
"before": "const pinnedModel = process.env.ZCODE_MODEL;\n if (pinnedProvider && pinnedModel)",
|
|
55
|
+
"after": "const pinnedModel = process.env.ZCODE_MODEL ?? (process.env.ZCODE_API_KEY ? loadAllModels()[0]?.modelId : undefined);\n if (pinnedProvider && pinnedModel)"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
@@ -2,34 +2,34 @@
|
|
|
2
2
|
"id": "zcode-models",
|
|
3
3
|
"package": "zcode-acp-server",
|
|
4
4
|
"versions": [
|
|
5
|
-
"0.
|
|
5
|
+
"0.80.0"
|
|
6
6
|
],
|
|
7
7
|
"edits": [
|
|
8
8
|
{
|
|
9
9
|
"file": "dist/config/options.js",
|
|
10
|
-
"before_sha256": "
|
|
11
|
-
"after_sha256": "
|
|
10
|
+
"before_sha256": "40605e6a76bfe9df5af4101b3b09c66c63247e245645603a51c42a85044f7826",
|
|
11
|
+
"after_sha256": "508042a42e89da936df13f0330cb79669a4e876781bf178cc529ec2d8d8684e0",
|
|
12
12
|
"before": "import { configProviderIdFor } from \"./account-provider.js\";",
|
|
13
13
|
"after": "import { buildAccountProviderConfig as axomAccountProviders, codingPlanRequestAuthFor as axomPlanAuth, configProviderIdFor } from \"./account-provider.js\";"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"file": "dist/config/options.js",
|
|
17
|
-
"before_sha256": "
|
|
18
|
-
"after_sha256": "
|
|
17
|
+
"before_sha256": "508042a42e89da936df13f0330cb79669a4e876781bf178cc529ec2d8d8684e0",
|
|
18
|
+
"after_sha256": "e46ef7e6ff4418b54eacb333e8f55968abbbf9488427939925e126e95a555f61",
|
|
19
19
|
"before": "/** Read the config.json contents (UTF-8). Throws on read/parse failure. */",
|
|
20
20
|
"after": "function axomFreshInstallModels() {\n const fallback = { providerId: DEFAULT_PROVIDER_ID, providerName: DEFAULT_PROVIDER_NAME, modelId: DEFAULT_MODEL_ID };\n try {\n const out = [];\n for (const [pid, p] of Object.entries(axomAccountProviders()?.providers ?? {})) {\n if (!p?.access?.entitled || !axomPlanAuth(pid)?.apiKey) continue;\n const providerId = configProviderIdFor(pid);\n for (const modelId of p.builtinModelIds ?? []) {\n if (out.some((m) => m.providerId === providerId && m.modelId === modelId)) continue;\n out.push({ providerId, providerName: providerId, modelId });\n }\n }\n if (out.length === 0) return [fallback];\n return out.some((m) => m.providerId === fallback.providerId && m.modelId === fallback.modelId)\n ? [fallback, ...out.filter((m) => !(m.providerId === fallback.providerId && m.modelId === fallback.modelId))]\n : [fallback, ...out];\n }\n catch {\n return [fallback];\n }\n}\n\n/** Read the config.json contents (UTF-8). Throws on read/parse failure. */"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"file": "dist/config/options.js",
|
|
24
|
-
"before_sha256": "
|
|
25
|
-
"after_sha256": "
|
|
24
|
+
"before_sha256": "e46ef7e6ff4418b54eacb333e8f55968abbbf9488427939925e126e95a555f61",
|
|
25
|
+
"after_sha256": "e15b0b9977c6be120a53fba1fa282b4fd00194f9f49ebc94b221a8aa3bd04101",
|
|
26
26
|
"before": " if (out.length === 0 && Object.keys(cfg.provider ?? {}).length === 0) {\n return [\n {\n providerId: DEFAULT_PROVIDER_ID,\n providerName: DEFAULT_PROVIDER_NAME,\n modelId: DEFAULT_MODEL_ID,\n },\n ];\n }",
|
|
27
27
|
"after": " if (out.length === 0 && Object.keys(cfg.provider ?? {}).length === 0) {\n return axomFreshInstallModels();\n }"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"file": "dist/config/options.js",
|
|
31
|
-
"before_sha256": "
|
|
32
|
-
"after_sha256": "
|
|
31
|
+
"before_sha256": "e15b0b9977c6be120a53fba1fa282b4fd00194f9f49ebc94b221a8aa3bd04101",
|
|
32
|
+
"after_sha256": "e4624ca2590a3e577eeb73a7e7f9a961623dd486a5d78d238ed1db7a09a16a16",
|
|
33
33
|
"before": " if (out.length > 0)\n return out;\n return [\n {\n providerId: DEFAULT_PROVIDER_ID,\n providerName: DEFAULT_PROVIDER_NAME,\n modelId: DEFAULT_MODEL_ID,\n },\n ];\n }\n}",
|
|
34
34
|
"after": " if (out.length > 0)\n return out;\n return axomFreshInstallModels();\n }\n}"
|
|
35
35
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "zcode-subagent-subwork",
|
|
3
3
|
"package": "zcode-acp-server",
|
|
4
4
|
"versions": [
|
|
5
|
-
"0.
|
|
5
|
+
"0.80.0"
|
|
6
6
|
],
|
|
7
7
|
"edits": [
|
|
8
8
|
{
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"file": "dist/server.js",
|
|
31
|
-
"before_sha256": "
|
|
32
|
-
"after_sha256": "
|
|
31
|
+
"before_sha256": "42db5e33c8abdceaede811ae6ca318a08c23d9681052299c7226ddc41fe561b9",
|
|
32
|
+
"after_sha256": "e29a42ff87e89ab8141425b47c70bb12d97f7beba6cf2cc99a410fd779b04596",
|
|
33
33
|
"before": "export class ZcodeAcpServer {",
|
|
34
|
-
"after": "/** Axom patch `zcode-subagent-subwork`: normalize ZCode sub-agents and background\n * tasks into the draft ACP subagent variants (RFD #1992) and the AIR async-task\n * extension — the same contracts claude-agent-acp ships. The backend already emits\n * first-class lifecycle events on `session.updated` (agentId/childSessionId for\n * sub-agents, taskId for background tasks); this listener forwards them so hosts\n * can project subworks, and dispatch.js re-scopes a foreground sub-agent's internal\n * `tool_subagent_*` tool calls onto its child session. */\nclass AxomZcodeSubagents {\n constructor(server, zcodeSid) {\n this.server = server;\n this.sid = zcodeSid;\n /** agentId → { agentId, child, background, terminal, pendingState, pendingP, resultSeen, preview } */\n this.agents = new Map();\n /** parentToolCallId → agent record (foreground result flush). */\n this.parents = new Map();\n /** taskId → { terminal } (bash/exec background tasks only). */\n this.tasks = new Map();\n }\n /** Draft RFD #1992 negotiated — the axom daemon always declares this. */\n nativeOk() {\n const caps = this.server.clientCapabilities;\n return !!(caps && caps.subagents);\n }\n /** AIR async-task capability declared (`_meta.jetbrains.air.capabilities`). */\n asyncOk() {\n const air = this.server.clientCapabilities?._meta?.jetbrains?.air;\n return !!(air && Array.isArray(air.capabilities) && air.capabilities.includes(\"asyncTasks\"));\n }\n handleEvent(ev) {\n try {\n void this.onEvent(ev);\n }\n catch {\n // Never crash the bridge from a normalization observer.\n }\n }\n async onEvent(ev) {\n if (!ev || ev.type !== \"session.updated\") {\n this.onToolEvent(ev);\n return;\n }\n const p = ev.payload ?? {};\n if (typeof p.agentId === \"string\" && p.agentId && typeof p.childSessionId === \"string\" && p.childSessionId) {\n this.onAgent(p);\n return;\n }\n // Sub-agent background tasks also carry taskId with taskKind \"subagent\" —\n // the agentId path above owns those. Workflow runs have their own poller.\n if (typeof p.taskId === \"string\" && p.taskId && p.taskKind !== \"subagent\" && p.taskKind !== \"workflow\") {\n this.onTask(p);\n }\n }\n onAgent(p) {\n if (!this.nativeOk())\n return;\n const agentId = p.agentId;\n const child = p.childSessionId;\n const status = typeof p.status === \"string\" ? p.status : \"running\";\n let rec = this.agents.get(agentId);\n if (!rec) {\n rec = { agentId, child, background: p.background === true, terminal: false, pendingState: null, pendingP: null, resultSeen: false, preview: null };\n this.agents.set(agentId, rec);\n if (typeof p.parentToolCallId === \"string\" && p.parentToolCallId)\n this.parents.set(p.parentToolCallId, rec);\n // First sighting already terminal (adapter restart): adopt — emit the\n // spawn so the host can still link the child, then the state below.\n if (status !== \"running\")\n rec.resultSeen = true;\n this.emit({\n sessionUpdate: \"subagent_spawned\",\n subagentSessionId: child,\n name: (typeof p.description === \"string\" && p.description) || (typeof p.agentType === \"string\" && p.agentType) || \"Subagent\",\n task: typeof p.prompt === \"string\" ? p.prompt : (p.description ?? \"\"),\n capabilities: {},\n _meta: { zcode: { agentId, parentToolCallId: p.parentToolCallId, background: rec.background, adopted: status !== \"running\" } },\n });\n }\n if (rec.child !== child)\n return;\n const mapped = status === \"completed\" ? \"completed\"\n : status === \"failed\" || status === \"error\" ? \"failed\"\n : status === \"cancelled\" || status === \"stopped\" ? \"cancelled\"\n : status === \"disconnected\" ? \"disconnected\" : null;\n if (!mapped || rec.terminal)\n return;\n if (rec.background || rec.resultSeen || mapped !== \"completed\") {\n this.finishAgent(rec, mapped, p);\n return;\n }\n // Foreground completion: hold the state until the parent Agent tool's own\n // result event arrives (one event later, measured) so the child-scoped\n // preview precedes the terminal state on the wire.\n rec.pendingState = mapped;\n rec.pendingP = p;\n }\n finishAgent(rec, state, p) {\n rec.terminal = true;\n if (rec.preview) {\n this.emitChild(rec.child, { sessionUpdate: \"agent_message_chunk\", content: { type: \"text\", text: rec.preview } });\n rec.preview = null;\n }\n const meta = { zcode: { agentId: rec.agentId } };\n if (p) {\n for (const key of [\"parentToolCallId\", \"outputFile\"])\n if (typeof p[key] === \"string\")\n meta.zcode[key] = p[key];\n if (typeof p.totalTokens === \"number\")\n meta.zcode.totalTokens = p.totalTokens;\n if (typeof p.totalDurationMs === \"number\")\n meta.zcode.totalDurationMs = p.totalDurationMs;\n }\n this.emit({ sessionUpdate: \"subagent_state_update\", subagentSessionId: rec.child, state, _meta: meta });\n }\n onToolEvent(ev) {\n if (!ev || ev.type !== \"tool.updated\")\n return;\n const p = ev.payload ?? {};\n if (p.kind !== \"result\" || typeof p.toolCallId !== \"string\")\n return;\n const rec = this.parents.get(p.toolCallId);\n if (!rec || rec.terminal || rec.background)\n return;\n rec.resultSeen = true;\n const text = axomSubagentResultPreview(p.result);\n if (text)\n rec.preview = text;\n if (rec.pendingState)\n this.finishAgent(rec, rec.pendingState, rec.pendingP);\n }\n onTask(p) {\n if (!this.asyncOk())\n return;\n const taskId = p.taskId;\n let task = this.tasks.get(taskId);\n const status = typeof p.status === \"string\" ? p.status : \"running\";\n if (!task) {\n task = { terminal: false };\n this.tasks.set(taskId, task);\n this.emit({\n sessionUpdate: \"async_task_spawned\",\n asyncTaskId: taskId,\n name: (typeof p.description === \"string\" && p.description) || (typeof p.command === \"string\" && p.command) || \"background task\",\n taskType: typeof p.taskKind === \"string\" ? p.taskKind : \"shell\",\n description: typeof p.description === \"string\" ? p.description : \"\",\n showInTranscript: false,\n canStop: p.cancellable === true,\n ...(typeof p.outputPath === \"string\" ? { outputFilePath: p.outputPath } : {}),\n ...(typeof p.toolCallId === \"string\" ? { toolCallId: p.toolCallId } : {}),\n _meta: { zcode: { taskKind: p.taskKind, command: p.command } },\n });\n }\n const mapped = status === \"completed\" ? \"completed\"\n : status === \"failed\" || status === \"error\" ? \"failed\"\n : status === \"cancelled\" ? \"stopped\" : null;\n if (!mapped || task.terminal)\n return;\n task.terminal = true;\n this.emit({\n sessionUpdate: \"async_task_state_update\",\n asyncTaskId: taskId,\n state: mapped,\n ...(typeof p.toolCallId === \"string\" ? { toolCallId: p.toolCallId } : {}),\n });\n }\n emit(update) {\n void this.server.notifyByZcodeSid(this.sid, update);\n }\n emitChild(childId, update) {\n const server = this.server;\n if (!server.clients || server.clients.size === 0)\n return;\n void enqueueSessionSend(childId, () => server.clients.broadcast().notify(\"session/update\", { sessionId: childId, update }));\n }\n}\n/** The parent Agent tool's result carries the sub-agent's final message plus an\n * `agentId:` line and a `<usage>` block that belong to the envelope, not the\n * answer. The backend delivers the payload as a JSON string or as an already\n * parsed object (both measured) — accept either. */\nfunction axomSubagentResultPreview(result) {\n let content = null;\n if (typeof result === \"string\") {\n try {\n const parsed = JSON.parse(result);\n if (parsed && typeof parsed.content === \"string\")\n content = parsed.content;\n }\n catch {\n content = result;\n }\n }\n else if (result && typeof result === \"object\" && typeof result.content === \"string\") {\n content = result.content;\n }\n if (typeof content !== \"string\" || !content.trim())\n return null;\n const stripped = content\n .replace(/<usage>[\\s\\S]*?<\\/usage>/g, \"\")\n .split(\"\\n\")\n .filter((line) => !/^agentId:\\s*\\S+/.test(line))\n .join(\"\\n\")\n .trim();\n return stripped ? stripped.slice(0, 2000) : null;\n}\nexport { AxomZcodeSubagents };\nexport class ZcodeAcpServer {"
|
|
34
|
+
"after": "/** Axom patch `zcode-subagent-subwork`: normalize ZCode sub-agents and background\n * tasks into the draft ACP subagent variants (RFD #1992) and the AIR async-task\n * extension — the same contracts claude-agent-acp ships. The backend already emits\n * first-class lifecycle events on `session.updated` (agentId/childSessionId for\n * sub-agents, taskId for background tasks); this listener forwards them so hosts\n * can project subworks, and dispatch.js re-scopes a foreground sub-agent's internal\n * `tool_subagent_*` tool calls onto its child session. */\nclass AxomZcodeSubagents {\n constructor(server, zcodeSid) {\n this.server = server;\n this.sid = zcodeSid;\n /** agentId → { agentId, child, background, terminal, pendingState, pendingP, resultSeen, preview } */\n this.agents = new Map();\n /** parentToolCallId → agent record (foreground result flush). */\n this.parents = new Map();\n /** taskId → { terminal } (bash/exec background tasks only). */\n this.tasks = new Map();\n }\n /** Draft RFD #1992 negotiated — the axom daemon always declares this. */\n nativeOk() {\n const caps = this.server.clientCapabilities;\n return !!(caps && caps.subagents);\n }\n /** AIR async-task capability declared (`_meta.jetbrains.air.capabilities`). */\n asyncOk() {\n const air = this.server.clientCapabilities?._meta?.jetbrains?.air;\n return !!(air && Array.isArray(air.capabilities) && air.capabilities.includes(\"asyncTasks\"));\n }\n handleEvent(ev) {\n try {\n void this.onEvent(ev);\n }\n catch {\n // Never crash the bridge from a normalization observer.\n }\n }\n async onEvent(ev) {\n if (!ev || ev.type !== \"session.updated\") {\n this.onToolEvent(ev);\n return;\n }\n const p = ev.payload ?? {};\n if (typeof p.agentId === \"string\" && p.agentId && typeof p.childSessionId === \"string\" && p.childSessionId) {\n this.onAgent(p);\n return;\n }\n // Sub-agent background tasks also carry taskId with taskKind \"subagent\" —\n // the agentId path above owns those. Workflow runs have their own poller.\n if (typeof p.taskId === \"string\" && p.taskId && p.taskKind !== \"subagent\" && p.taskKind !== \"workflow\") {\n this.onTask(p);\n }\n }\n onAgent(p) {\n if (!this.nativeOk())\n return;\n const agentId = p.agentId;\n const child = p.childSessionId;\n const status = typeof p.status === \"string\" ? p.status : \"running\";\n let rec = this.agents.get(agentId);\n if (!rec) {\n rec = { agentId, child, background: p.background === true, terminal: false, pendingState: null, pendingP: null, resultSeen: false, preview: null };\n this.agents.set(agentId, rec);\n if (typeof p.parentToolCallId === \"string\" && p.parentToolCallId)\n this.parents.set(p.parentToolCallId, rec);\n // First sighting already terminal (adapter restart): adopt — emit the\n // spawn so the host can still link the child, then the state below.\n if (status !== \"running\")\n rec.resultSeen = true;\n this.emit({\n sessionUpdate: \"subagent_spawned\",\n subagentSessionId: child,\n name: (typeof p.description === \"string\" && p.description) || (typeof p.agentType === \"string\" && p.agentType) || \"Subagent\",\n task: typeof p.prompt === \"string\" ? p.prompt : (p.description ?? \"\"),\n capabilities: {},\n _meta: { zcode: { agentId, parentToolCallId: p.parentToolCallId, background: rec.background, adopted: status !== \"running\" } },\n });\n }\n if (rec.child !== child)\n return;\n const mapped = status === \"completed\" ? \"completed\"\n : status === \"failed\" || status === \"error\" ? \"failed\"\n : status === \"cancelled\" || status === \"stopped\" ? \"cancelled\"\n : status === \"disconnected\" ? \"disconnected\" : null;\n if (!mapped || rec.terminal)\n return;\n if (rec.background || rec.resultSeen || mapped !== \"completed\") {\n this.finishAgent(rec, mapped, p);\n return;\n }\n // Foreground completion: hold the state until the parent Agent tool's own\n // result event arrives (one event later, measured) so the child-scoped\n // preview precedes the terminal state on the wire.\n rec.pendingState = mapped;\n rec.pendingP = p;\n }\n finishAgent(rec, state, p) {\n rec.terminal = true;\n if (rec.preview) {\n this.emitChild(rec.child, { sessionUpdate: \"agent_message_chunk\", content: { type: \"text\", text: rec.preview } });\n rec.preview = null;\n }\n const meta = { zcode: { agentId: rec.agentId } };\n if (p) {\n for (const key of [\"parentToolCallId\", \"outputFile\"])\n if (typeof p[key] === \"string\")\n meta.zcode[key] = p[key];\n if (typeof p.totalTokens === \"number\")\n meta.zcode.totalTokens = p.totalTokens;\n if (typeof p.totalDurationMs === \"number\")\n meta.zcode.totalDurationMs = p.totalDurationMs;\n }\n this.emit({ sessionUpdate: \"subagent_state_update\", subagentSessionId: rec.child, state, _meta: meta });\n }\n onToolEvent(ev) {\n if (!ev || ev.type !== \"tool.updated\")\n return;\n const p = ev.payload ?? {};\n if (p.kind !== \"result\" || typeof p.toolCallId !== \"string\")\n return;\n const rec = this.parents.get(p.toolCallId);\n if (!rec || rec.terminal || rec.background)\n return;\n rec.resultSeen = true;\n const text = axomSubagentResultPreview(p.result);\n if (text)\n rec.preview = text;\n if (rec.pendingState)\n this.finishAgent(rec, rec.pendingState, rec.pendingP);\n }\n onTask(p) {\n if (!this.asyncOk())\n return;\n const taskId = p.taskId;\n let task = this.tasks.get(taskId);\n const status = typeof p.status === \"string\" ? p.status : \"running\";\n if (!task) {\n task = { terminal: false };\n this.tasks.set(taskId, task);\n this.emit({\n sessionUpdate: \"async_task_spawned\",\n asyncTaskId: taskId,\n name: (typeof p.description === \"string\" && p.description) || (typeof p.command === \"string\" && p.command) || \"background task\",\n taskType: typeof p.taskKind === \"string\" ? p.taskKind : \"shell\",\n description: typeof p.description === \"string\" ? p.description : \"\",\n showInTranscript: false,\n canStop: p.cancellable === true,\n ...(typeof p.outputPath === \"string\" ? { outputFilePath: p.outputPath } : {}),\n ...(typeof p.toolCallId === \"string\" ? { toolCallId: p.toolCallId } : {}),\n _meta: { zcode: { taskKind: p.taskKind, command: p.command } },\n });\n }\n // Preserve cumulative backend output without reviving already settled tasks.\n const output = p.outputTail ?? p.stdoutTail;\n if (!task.terminal && typeof output === \"string\" && output && output !== task.outputTail) {\n task.outputTail = output;\n this.emit({\n sessionUpdate: \"async_task_progress\", asyncTaskId: taskId,\n ...(typeof p.toolCallId === \"string\" ? { toolCallId: p.toolCallId } : {}),\n ...(typeof p.outputPath === \"string\" ? { outputFilePath: p.outputPath } : {}),\n _meta: { zcode: { outputTail: output } },\n });\n }\n const mapped = status === \"completed\" ? \"completed\"\n : status === \"failed\" || status === \"error\" ? \"failed\"\n : status === \"cancelled\" ? \"stopped\" : null;\n if (!mapped || task.terminal)\n return;\n task.terminal = true;\n this.emit({\n sessionUpdate: \"async_task_state_update\",\n asyncTaskId: taskId,\n state: mapped,\n ...(typeof p.toolCallId === \"string\" ? { toolCallId: p.toolCallId } : {}),\n });\n }\n emit(update) {\n void this.server.notifyByZcodeSid(this.sid, update);\n }\n emitChild(childId, update) {\n const server = this.server;\n if (!server.clients || server.clients.size === 0)\n return;\n void enqueueSessionSend(childId, () => server.clients.broadcast().notify(\"session/update\", { sessionId: childId, update }));\n }\n}\n/** The parent Agent tool's result carries the sub-agent's final message plus an\n * `agentId:` line and a `<usage>` block that belong to the envelope, not the\n * answer. The backend delivers the payload as a JSON string or as an already\n * parsed object (both measured) — accept either. */\nfunction axomSubagentResultPreview(result) {\n let content = null;\n if (typeof result === \"string\") {\n try {\n const parsed = JSON.parse(result);\n if (parsed && typeof parsed.content === \"string\")\n content = parsed.content;\n }\n catch {\n content = result;\n }\n }\n else if (result && typeof result === \"object\" && typeof result.content === \"string\") {\n content = result.content;\n }\n if (typeof content !== \"string\" || !content.trim())\n return null;\n const stripped = content\n .replace(/<usage>[\\s\\S]*?<\\/usage>/g, \"\")\n .split(\"\\n\")\n .filter((line) => !/^agentId:\\s*\\S+/.test(line))\n .join(\"\\n\")\n .trim();\n return stripped ? stripped.slice(0, 2000) : null;\n}\nexport { AxomZcodeSubagents };\nexport class ZcodeAcpServer {"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"file": "dist/server.js",
|
|
38
|
-
"before_sha256": "
|
|
39
|
-
"after_sha256": "
|
|
38
|
+
"before_sha256": "e29a42ff87e89ab8141425b47c70bb12d97f7beba6cf2cc99a410fd779b04596",
|
|
39
|
+
"after_sha256": "00aa402eebd8537507c324371b3baaad851f3e409c97b3394b8c3e7fefc0e498",
|
|
40
40
|
"before": " backend.registerEventListener(zcodeSid, new SessionTitleListener(this, zcodeSid));\n this.backgroundListenerBackend.set(zcodeSid, backend);",
|
|
41
41
|
"after": " backend.registerEventListener(zcodeSid, new SessionTitleListener(this, zcodeSid));\n backend.registerEventListener(zcodeSid, new AxomZcodeSubagents(this, zcodeSid));\n this.backgroundListenerBackend.set(zcodeSid, backend);"
|
|
42
42
|
},
|
|
@@ -56,15 +56,15 @@
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"file": "dist/handlers/session.js",
|
|
59
|
-
"before_sha256": "
|
|
60
|
-
"after_sha256": "
|
|
59
|
+
"before_sha256": "d78d4a702ff01fc539de62c84cdb04c5a5c1486dc6de6554edf2b8ac5c3b788d",
|
|
60
|
+
"after_sha256": "cf260853e2940ce841f8ac0f8788574a30a047c4da38006099ecca4bf1bced72",
|
|
61
61
|
"before": "async function adoptStoredTitle(server, acpSid, zcodeSid) {\n if (server.sessionTitles.has(acpSid))\n return;",
|
|
62
62
|
"after": "async function adoptStoredTitle(server, acpSid, zcodeSid) {\n // Axom patch `zcode-subagent-subwork`: the in-memory title is usually the first\n // prompt's echo; the backend's stored generated title is the real answer, and the\n // live titleUpdated push is routinely lost at a backend respawn boundary — this\n // load-time read is then the only surviving copy. A user-pinned rename still wins.\n if (server.sessionTitles.has(acpSid) && server.titleUserSetBy.has(acpSid))\n return;"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"file": "dist/handlers/session.js",
|
|
66
|
-
"before_sha256": "
|
|
67
|
-
"after_sha256": "
|
|
66
|
+
"before_sha256": "cf260853e2940ce841f8ac0f8788574a30a047c4da38006099ecca4bf1bced72",
|
|
67
|
+
"after_sha256": "ba9c2d4c131356661d5c79a89c840cb2d7a5dca65005c1d65bb5dd72871a7dfe",
|
|
68
68
|
"before": " if (hit?.title) {\n server.sessionTitles.set(acpSid, hit.title);\n server.touchSessionSummary(acpSid, hit.title);\n refreshTerminalTabTitle(server, acpSid);\n log(`adopted stored title for ${acpSid.slice(0, 8)}: ${hit.title}`);\n }",
|
|
69
69
|
"after": " if (hit?.title && server.sessionTitles.get(acpSid) !== hit.title) {\n server.sessionTitles.set(acpSid, hit.title);\n server.touchSessionSummary(acpSid, hit.title);\n refreshTerminalTabTitle(server, acpSid);\n // Axom patch `zcode-subagent-subwork`: broadcast so hosts sync the stored\n // generated title on (re-)attach — the live push was lost at a respawn.\n void sendSessionUpdate(server.clients.broadcast(), acpSid, {\n sessionUpdate: \"session_info_update\",\n title: hit.title,\n updatedAt: new Date().toISOString(),\n }).catch(() => { });\n log(`adopted stored title for ${acpSid.slice(0, 8)}: ${hit.title}`);\n }"
|
|
70
70
|
}
|
package/dist/cli.js
CHANGED
|
@@ -13,9 +13,9 @@ import { registerWorkstationCommands } from './commands/workstation.js';
|
|
|
13
13
|
import { whoamiAction } from './commands/whoami.js';
|
|
14
14
|
import { runCredentialBroker } from './credentials.js';
|
|
15
15
|
import { runCommand } from './output.js';
|
|
16
|
-
import {
|
|
16
|
+
import { clientDir } from './workstation/config.js';
|
|
17
|
+
import { executeDeviceLifecycle, inspectLocalDevice } from './workstation/control/deviceCommands.js';
|
|
17
18
|
import { runDaemon } from './workstation/daemon.js';
|
|
18
|
-
import { registerWorkstation } from './workstation/register.js';
|
|
19
19
|
import { detectSupervision } from './workstation/supervision.js';
|
|
20
20
|
import { CLIENT_VERSION } from './workstation/version.js';
|
|
21
21
|
export function buildProgram() {
|
|
@@ -46,19 +46,17 @@ export function buildProgram() {
|
|
|
46
46
|
registerSkillsCommands(program);
|
|
47
47
|
registerUpdateCommand(program);
|
|
48
48
|
registerWorkstationCommands(program, {
|
|
49
|
-
start
|
|
49
|
+
start: () => executeDeviceLifecycle('start', clientDir()),
|
|
50
|
+
stop: () => executeDeviceLifecycle('stop', clientDir()),
|
|
51
|
+
restart: () => executeDeviceLifecycle('restart', clientDir()),
|
|
52
|
+
install: () => executeDeviceLifecycle('install', clientDir()),
|
|
53
|
+
uninstall: () => executeDeviceLifecycle('uninstall', clientDir()), runDaemon,
|
|
50
54
|
status: async () => {
|
|
51
|
-
const current = await
|
|
52
|
-
|
|
53
|
-
return 'workstation state is unknown; run `axom workstation inspect --json` to verify it';
|
|
54
|
-
return current.running
|
|
55
|
-
? `running connected to ${current.server ?? 'unknown server'} as ${current.workstationId ?? 'unknown workstation'}`
|
|
56
|
-
: 'stopped';
|
|
55
|
+
const current = await inspectLocalDevice(clientDir());
|
|
56
|
+
return `${current.runtime.state}; ${current.registrations.length} workstation registrations`;
|
|
57
57
|
},
|
|
58
58
|
log: console.log,
|
|
59
|
-
authenticate: (server) => authAction({ server }),
|
|
60
59
|
detectSupervision,
|
|
61
|
-
isInteractive: () => Boolean(process.stdin.isTTY),
|
|
62
60
|
});
|
|
63
61
|
return program;
|
|
64
62
|
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -5,9 +5,12 @@ import { authorizeDevice } from '../workstation/oauth/deviceAuthorization.js';
|
|
|
5
5
|
import { revokeAndClearTokens, revokeRefreshToken, saveTokens } from '../credentials.js';
|
|
6
6
|
import { cliRequestedScopes } from '../authScopes.js';
|
|
7
7
|
export const DEFAULT_SERVER_URL = 'http://localhost:8080';
|
|
8
|
+
export function resolveAuthServer(config, explicit) {
|
|
9
|
+
return resolveServer(config, explicit ?? config.defaultServer ?? DEFAULT_SERVER_URL);
|
|
10
|
+
}
|
|
8
11
|
export async function authAction(options, deps = {}) {
|
|
9
12
|
const config = await loadConfig();
|
|
10
|
-
const server =
|
|
13
|
+
const server = resolveAuthServer(config, options.server);
|
|
11
14
|
const host = hostname();
|
|
12
15
|
const { accessToken, refreshToken, expiresInSeconds, scopes } = await authorizeDevice(server, `Axomwork CLI (${host})`, host, deps, cliRequestedScopes(options.cowork));
|
|
13
16
|
const updated = await loadConfig();
|
package/dist/commands/update.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { inspectWorkstation } from '../workstation/control/inspect.js';
|
|
3
|
-
import { requestRuntimeUpdate } from '../workstation/control/runtimeClient.js';
|
|
3
|
+
import { requestDeviceRuntimeUpdate, requestRuntimeUpdate } from '../workstation/control/runtimeClient.js';
|
|
4
|
+
import { inspectLocalDevice } from '../workstation/control/deviceCommands.js';
|
|
5
|
+
import { assertDeviceGuard, deviceRuntimeUpdateRequestSchema } from '../workstation/control/deviceProtocol.js';
|
|
4
6
|
import { withWorkstationLock } from '../workstation/control/locks.js';
|
|
5
7
|
import { bindingGuardSchema, LocalControlError } from '../workstation/control/protocol.js';
|
|
6
8
|
import { enrollmentFailure } from '../workstation/control/enrollment.js';
|
|
7
9
|
import { readLocalRequest } from '../workstation/control/commands.js';
|
|
8
10
|
import { deploymentDownloadURL, normalizeWorkstationServer, workstationEndpoint } from '../workstation/control/serverIdentity.js';
|
|
9
11
|
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
10
14
|
import { UsageError, loadConfig, resolveServer } from '../config.js';
|
|
11
15
|
import { runCommand } from '../output.js';
|
|
12
16
|
import { clientDir } from '../workstation/config.js';
|
|
@@ -64,6 +68,26 @@ export async function updateAction(options, deps = {}) {
|
|
|
64
68
|
};
|
|
65
69
|
const dir = deps.dir ?? clientDir();
|
|
66
70
|
if (!deps.createCoordinator) {
|
|
71
|
+
let deviceRoot = false;
|
|
72
|
+
for (const name of ['device.json', 'legacy-migration.json']) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.lstat(path.join(dir, name));
|
|
75
|
+
deviceRoot = true;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (deviceRoot) {
|
|
83
|
+
const current = await inspectLocalDevice(dir);
|
|
84
|
+
const result = await updateLocalCli({ schemaVersion: 2, action: 'update', operationId: `manual-${randomUUID()}`, server, guard: { deviceRevision: current.deviceRevision, runtimeGeneration: current.runtime.generation }, expectedRelease: { version: release.version, sha512: release.sha512 } }, dir, fetchImpl);
|
|
85
|
+
if (result.status === 'succeeded' || result.status === 'pending' && !result.error) {
|
|
86
|
+
(deps.log ?? console.log)(result.status === 'pending' ? 'Update requested. Check workstation status for progress.' : `Installed ${release.version}.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(result.error?.message ?? 'The workstation is busy; try again when its work finishes.');
|
|
90
|
+
}
|
|
67
91
|
const current = await inspectWorkstation(dir);
|
|
68
92
|
const result = await updateLocalCli({ operationId: `manual-${randomUUID()}`, server, guard: { revision: current.revision, runtimeGeneration: current.runtime.generation }, expectedRelease: { version: release.version, sha512: release.sha512 } }, dir, fetchImpl);
|
|
69
93
|
if (result.status === 'succeeded' || result.status === 'pending') {
|
|
@@ -96,7 +120,34 @@ export async function updateAction(options, deps = {}) {
|
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
const updateRequestSchema = z.object({ operationId: z.string().min(1).max(128), server: z.string(), guard: bindingGuardSchema, expectedRelease: z.object({ version: z.string(), sha512: z.string().regex(/^[a-f0-9]{128}$/i) }).strict() }).strict();
|
|
123
|
+
const deviceUpdateCliSchema = deviceRuntimeUpdateRequestSchema.omit({ release: true }).extend({ expectedRelease: updateRequestSchema.shape.expectedRelease }).strict();
|
|
124
|
+
async function updateDeviceCli(raw, directory, fetchImpl) {
|
|
125
|
+
let operationId = randomUUID();
|
|
126
|
+
try {
|
|
127
|
+
const request = deviceUpdateCliSchema.parse(raw);
|
|
128
|
+
operationId = request.operationId;
|
|
129
|
+
const server = normalizeWorkstationServer(request.server);
|
|
130
|
+
const current = await inspectLocalDevice(directory);
|
|
131
|
+
if (current.runtime.state !== 'running')
|
|
132
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Start and verify the original device owner before updating');
|
|
133
|
+
assertDeviceGuard(request.guard, current);
|
|
134
|
+
const release = await fetchManifest(server, fetchImpl);
|
|
135
|
+
if (release.version !== request.expectedRelease.version || release.sha512 !== request.expectedRelease.sha512)
|
|
136
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The published release changed; review it again');
|
|
137
|
+
deploymentDownloadURL(server, release.url);
|
|
138
|
+
const status = await requestDeviceRuntimeUpdate(directory, { schemaVersion: 2, action: 'update', operationId, server, guard: request.guard, release });
|
|
139
|
+
const result = ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : ['FAILED', 'ROLLED_BACK'].includes(status.phase) ? 'failed' : 'pending';
|
|
140
|
+
const inspection = await inspectLocalDevice(directory).catch(() => null);
|
|
141
|
+
return { operationId, registrationId: null, status: result, inspection, ...(status.error ? { error: { code: 'SERVICE_FAILED', message: status.error } } : {}) };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const code = error instanceof LocalControlError ? error.code : 'UNSUPPORTED_PROTOCOL';
|
|
145
|
+
return { operationId, registrationId: null, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : 'failed', inspection: null, error: { code, message: 'Device update requires the original verified owner and unchanged source/release' } };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
99
148
|
export async function updateLocalCli(raw, directory, fetchImpl = globalThis.fetch) {
|
|
149
|
+
if (raw && typeof raw === 'object' && 'schemaVersion' in raw)
|
|
150
|
+
return updateDeviceCli(raw, directory, fetchImpl);
|
|
100
151
|
try {
|
|
101
152
|
const request = updateRequestSchema.parse(raw);
|
|
102
153
|
const server = normalizeWorkstationServer(request.server);
|
|
@@ -104,17 +155,26 @@ export async function updateLocalCli(raw, directory, fetchImpl = globalThis.fetc
|
|
|
104
155
|
if (manifest.version !== request.expectedRelease.version || manifest.sha512 !== request.expectedRelease.sha512)
|
|
105
156
|
throw new LocalControlError('BINDING_CONFLICT', 'The published release changed; review it again');
|
|
106
157
|
manifest.url = deploymentDownloadURL(server, manifest.url);
|
|
158
|
+
const before = await inspectWorkstation(directory);
|
|
159
|
+
if (before.revision !== request.guard.revision || before.runtime.generation !== request.guard.runtimeGeneration)
|
|
160
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
161
|
+
if (before.runtime.state === 'unknown')
|
|
162
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing daemon before updating');
|
|
163
|
+
if (before.runtime.state === 'running') {
|
|
164
|
+
if (before.runtime.binding?.server !== server)
|
|
165
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Another server owns the running daemon');
|
|
166
|
+
const status = await requestRuntimeUpdate(directory, manifest, request.operationId);
|
|
167
|
+
return { status: ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' ? 'failed' : 'pending', inspection: await inspectWorkstation(directory) };
|
|
168
|
+
}
|
|
107
169
|
return await withWorkstationLock(directory, 'installation', () => withWorkstationLock(directory, 'operation', async () => {
|
|
108
170
|
const current = await inspectWorkstation(directory);
|
|
109
171
|
if (current.revision !== request.guard.revision || current.runtime.generation !== request.guard.runtimeGeneration)
|
|
110
172
|
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
111
173
|
if (current.runtime.state === 'unknown')
|
|
112
174
|
throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing daemon before updating');
|
|
113
|
-
if (current.runtime.state
|
|
114
|
-
throw new LocalControlError('
|
|
115
|
-
const status =
|
|
116
|
-
? await requestRuntimeUpdate(directory, manifest, request.operationId)
|
|
117
|
-
: await createUpdateCoordinator({ dir: directory, server, cliPath: CLI_PATH, serviceLifecycle: 'none', installationLockHeld: true }).handle({ commandId: request.operationId, release: manifest, force: false });
|
|
175
|
+
if (current.runtime.state !== 'stopped')
|
|
176
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The original stopped owner changed before offline update admission');
|
|
177
|
+
const status = await createUpdateCoordinator({ dir: directory, server, cliPath: CLI_PATH, serviceLifecycle: 'none', installationLockHeld: true }).handle({ commandId: request.operationId, release: manifest, force: false });
|
|
118
178
|
return { status: ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' ? 'failed' : 'pending', inspection: await inspectWorkstation(directory) };
|
|
119
179
|
}));
|
|
120
180
|
}
|
|
@@ -144,7 +204,8 @@ export function registerUpdateCommand(program) {
|
|
|
144
204
|
catch (error) {
|
|
145
205
|
result = await enrollmentFailure(error, clientDir());
|
|
146
206
|
}
|
|
207
|
+
const device = deviceUpdateCliSchema.safeParse(request);
|
|
147
208
|
const id = updateRequestSchema.safeParse(request);
|
|
148
|
-
process.stdout.write(JSON.stringify({ schemaVersion: 1, kind: 'result', operationId: id.success ? id.data.operationId : randomUUID(), result }) + '\n');
|
|
209
|
+
process.stdout.write(JSON.stringify({ schemaVersion: device.success ? 2 : 1, kind: 'result', operationId: device.success ? device.data.operationId : id.success ? id.data.operationId : randomUUID(), result }) + '\n');
|
|
149
210
|
}));
|
|
150
211
|
}
|