@oar-sh/oar 0.9.17 → 0.9.18
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/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +192 -0
- package/README.md +13 -12
- package/package.json +1 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +35 -9
- package/server/claude-worker/claude-session-process.mjs +166 -21
- package/server/claude-worker/claude-session-worker-main.mjs +3 -0
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-publisher.mjs +117 -5
- package/server/claude-worker/claude-usage-totals.mjs +53 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +33 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +52 -0
- package/server/public/app/bootstrap.js +153 -27
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +6 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +11 -0
- package/server/public/app/conversation-view.js +29 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +127 -10
- package/server/public/app/model-selector-options.mjs +17 -1
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/provider-readiness.mjs +5 -2
- package/server/public/app/relay-error-ctas.mjs +50 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +189 -3
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +1 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +1 -1
- package/server/routes/messages-routes.mjs +645 -27
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +327 -29
- package/server/server-runtime.mjs +363 -26
- package/server/services/about-service.mjs +1 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-service.mjs +5 -0
- package/server/services/provider-model-selection.mjs +3 -2
- package/server/services/provider-reasoning-effort.mjs +9 -0
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +83 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +61 -2
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +2 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -106,3 +106,56 @@ export function claudeToolRequests(serverToolUse, { modelKey = (value) => String
|
|
|
106
106
|
}
|
|
107
107
|
return [...requests.values()].filter((entry) => entry.searchRequests + entry.fetchRequests > 0);
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
function costOf(value) {
|
|
111
|
+
const number = Number(value ?? 0);
|
|
112
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The CLI's totals per model with Claude Code's own cost estimate, for the
|
|
117
|
+
* Claude API booking (claude-api-usage-service.mjs): `[{ model, hitTokens,
|
|
118
|
+
* missTokens, outputTokens, costUsd, costUnknown }]`. From `modelUsage` (the
|
|
119
|
+
* session's running totals, sub-agents and helper calls included; `costUSD`
|
|
120
|
+
* per model, `costBasis: 'unknown'` for a model the CLI has no price for), or
|
|
121
|
+
* from `usage` alone under the turn's model (pass `modelUsage` as null): the
|
|
122
|
+
* turn's main agent loop, which carries no cost. Model ids keep Claude's
|
|
123
|
+
* `[1m]` tier; the relay books them under the base id.
|
|
124
|
+
*/
|
|
125
|
+
export function claudeBillingTotals(modelUsage, { usage = null, model = '' } = {}) {
|
|
126
|
+
const totals = new Map();
|
|
127
|
+
const add = (name, hit, miss, output, costUsd, costUnknown) => {
|
|
128
|
+
const key = String(name ?? '').trim().toLowerCase();
|
|
129
|
+
if (!key) return;
|
|
130
|
+
const current = totals.get(key) || { model: key, hitTokens: 0, missTokens: 0, outputTokens: 0, costUsd: 0, costUnknown: false };
|
|
131
|
+
current.hitTokens += hit;
|
|
132
|
+
current.missTokens += miss;
|
|
133
|
+
current.outputTokens += output;
|
|
134
|
+
current.costUsd += costUsd;
|
|
135
|
+
current.costUnknown = current.costUnknown || costUnknown;
|
|
136
|
+
totals.set(key, current);
|
|
137
|
+
};
|
|
138
|
+
if (modelUsage && typeof modelUsage === 'object' && !Array.isArray(modelUsage)) {
|
|
139
|
+
for (const [name, entry] of Object.entries(modelUsage)) {
|
|
140
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
141
|
+
add(
|
|
142
|
+
name,
|
|
143
|
+
count(entry.cacheReadInputTokens),
|
|
144
|
+
count(entry.inputTokens) + count(entry.cacheCreationInputTokens),
|
|
145
|
+
count(entry.outputTokens),
|
|
146
|
+
costOf(entry.costUSD),
|
|
147
|
+
entry.costBasis === 'unknown',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
} else if (usage && typeof usage === 'object') {
|
|
151
|
+
add(
|
|
152
|
+
model,
|
|
153
|
+
count(usage.cache_read_input_tokens),
|
|
154
|
+
count(usage.input_tokens) + count(usage.cache_creation_input_tokens),
|
|
155
|
+
count(usage.output_tokens),
|
|
156
|
+
0,
|
|
157
|
+
false,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return [...totals.values()].filter((entry) => entry.hitTokens + entry.missTokens + entry.outputTokens > 0 || entry.costUsd > 0);
|
|
161
|
+
}
|
|
@@ -135,7 +135,7 @@ export function isBackgroundLaunch(block, toolUseResult = null) {
|
|
|
135
135
|
|
|
136
136
|
export function summarizeToolInput(toolName, input) {
|
|
137
137
|
if (!input || typeof input !== 'object') return '';
|
|
138
|
-
// `
|
|
138
|
+
// `mcp__relay__relay_tool` reads as what it does on which relay
|
|
139
139
|
// (`send → linux-test session 01234567: “…”`), not as its first argument.
|
|
140
140
|
if (isRemoteRelayToolName(toolName)) {
|
|
141
141
|
const summary = remoteRelayActivitySummary(input);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The relay's `
|
|
1
|
+
// The relay's `relay_tool` tool for Copilot SDK sessions: a plain SDK
|
|
2
2
|
// `Tool` (`{ name, description, parameters, skipPermission, handler }`) built
|
|
3
3
|
// from the shared contract and forwarded through the shared tool core. The
|
|
4
4
|
// module never imports the SDK; the session process hands the definition to
|
|
@@ -50,7 +50,7 @@ export function buildCopilotRemoteRelayTool({
|
|
|
50
50
|
}));
|
|
51
51
|
} catch (error) {
|
|
52
52
|
const message = error?.message || String(error);
|
|
53
|
-
dbg('
|
|
53
|
+
dbg('relay_tool failed', message);
|
|
54
54
|
result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
|
|
55
55
|
}
|
|
56
56
|
return formatRemoteRelayToolResult(result);
|
|
@@ -84,7 +84,7 @@ export function summarizeToolInput(input) {
|
|
|
84
84
|
|
|
85
85
|
export function formatToolActivityText(toolName, input) {
|
|
86
86
|
const rawName = String(toolName || '').trim() || 'tool';
|
|
87
|
-
// `
|
|
87
|
+
// `relay_tool` (bare, or namespaced by an MCP server) reads as what it
|
|
88
88
|
// does on which relay (`send → linux-test session 01234567: “…”`); its
|
|
89
89
|
// arguments may arrive as a JSON string.
|
|
90
90
|
const isRemoteRelay = isRemoteRelayToolName(rawName);
|
|
@@ -456,7 +456,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
456
456
|
// runtime's built-in one. A runtime that refuses the override falls back
|
|
457
457
|
// to the built-in for the rest of this worker's life.
|
|
458
458
|
relayAskUserTool = true,
|
|
459
|
-
// Whether sessions carry the relay's `
|
|
459
|
+
// Whether sessions carry the relay's `relay_tool` tool
|
|
460
460
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
461
461
|
remoteRelayToolGate = null,
|
|
462
462
|
taskRefreshMs = DEFAULT_TASK_REFRESH_MS,
|
|
@@ -696,12 +696,12 @@ export function createCopilotSdkSessionRunner({
|
|
|
696
696
|
// the human's eventual answer to a runtime whose row is already settled.
|
|
697
697
|
// Same guard the Cursor worker's `hasPendingClientWork` provides.
|
|
698
698
|
let pendingHumanRequests = 0;
|
|
699
|
-
// Running `
|
|
699
|
+
// Running `relay_tool` calls. The runtime is just as silent while one
|
|
700
700
|
// runs (the relay may wait up to 600 s for a remote turn, plus an approval
|
|
701
701
|
// card), so they hold the stall watchdog and the idle shutdown too — but
|
|
702
702
|
// not steering: nobody is answering a card here.
|
|
703
703
|
let pendingRelayToolCalls = 0;
|
|
704
|
-
// `
|
|
704
|
+
// `relay_tool` is registered only while this relay has paired remotes.
|
|
705
705
|
// Asked once when the worker starts; a session build waits for that first
|
|
706
706
|
// answer (it is async anyway), and every runtime stop asks again so the
|
|
707
707
|
// next session picks up a remote paired in the meantime.
|
|
@@ -2306,7 +2306,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
2306
2306
|
}
|
|
2307
2307
|
|
|
2308
2308
|
/**
|
|
2309
|
-
* The relay's `
|
|
2309
|
+
* The relay's `relay_tool` tool (copilot-remote-relay-tool.mjs). Every call
|
|
2310
2310
|
* is counted while it runs: the stall watchdog re-arms instead of failing the
|
|
2311
2311
|
* row, and the lifecycle keeps the runtime up. The clock restarts from the
|
|
2312
2312
|
* answer, like after a card. A call is cancelled by the runtime (its
|
|
@@ -2349,7 +2349,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
2349
2349
|
* runtime refuses the override (an older runtime). The refusal is
|
|
2350
2350
|
* remembered until the runtime is stopped — the next runtime (possibly
|
|
2351
2351
|
* auto-updated under the relay) gets another try. Only the override goes:
|
|
2352
|
-
* the relay's other tools (`
|
|
2352
|
+
* the relay's other tools (`relay_tool`) override nothing and stay.
|
|
2353
2353
|
*/
|
|
2354
2354
|
async function openSession(open, config) {
|
|
2355
2355
|
try {
|
|
@@ -3198,7 +3198,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3198
3198
|
// Cancels any relay question card this turn is blocked on, so an aborted
|
|
3199
3199
|
// turn does not leave a human answering into the void.
|
|
3200
3200
|
abortController: new AbortController(),
|
|
3201
|
-
// Cancels the `
|
|
3201
|
+
// Cancels the `relay_tool` calls that started under this turn — on the
|
|
3202
3202
|
// user's Stop only. Not `abortController`: that one also fires when the
|
|
3203
3203
|
// turn settles normally, and a background agent's call must outlive that.
|
|
3204
3204
|
remoteRelayStop: new AbortController(),
|
|
@@ -3369,7 +3369,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3369
3369
|
if (turn.settled) return;
|
|
3370
3370
|
// A human staring at a question card is not a stalled runtime: the card
|
|
3371
3371
|
// has its own (much longer) timeout, and failing the row here would
|
|
3372
|
-
// settle it while the answer is still coming. A
|
|
3372
|
+
// settle it while the answer is still coming. A relay_tool call waiting
|
|
3373
3373
|
// on another relay is the same kind of silence; the relay bounds that
|
|
3374
3374
|
// wait itself. So is a compaction, bounded by its stale cap, and the wait
|
|
3375
3375
|
// for the previous runtime to stop, bounded by the teardown's own limits.
|
|
@@ -3912,7 +3912,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
3912
3912
|
}
|
|
3913
3913
|
turn.aborted = true;
|
|
3914
3914
|
turn.abortedRowId = rowId;
|
|
3915
|
-
// A pending `
|
|
3915
|
+
// A pending `relay_tool` call ends now rather than when the runtime gets
|
|
3916
3916
|
// round to it: the relay drops the request, withdraws its approval card
|
|
3917
3917
|
// and stops waiting on the remote, and the watchdog hold is released.
|
|
3918
3918
|
turn.remoteRelayStop?.abort();
|
|
@@ -4564,7 +4564,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4564
4564
|
* is not waiting for its tools, so a steer interrupts nothing. A
|
|
4565
4565
|
* subagent the main agent runs in the foreground is covered by the main
|
|
4566
4566
|
* agent's own tool call that started it.
|
|
4567
|
-
* - `
|
|
4567
|
+
* - `relay_tool`, which holds the stall watchdog but never held steering
|
|
4568
4568
|
* (see `pendingRelayToolCalls`).
|
|
4569
4569
|
*/
|
|
4570
4570
|
function mainToolsInFlight(turn = activeTurn) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The relay's `
|
|
2
|
+
* The relay's `relay_tool` custom tool for the Cursor worker. SDK-free: the
|
|
3
3
|
* turn runner passes the returned `{ description, inputSchema, execute }` into
|
|
4
|
-
* `customTools.
|
|
4
|
+
* `customTools.relay_tool`, and every call goes through the shared tool core
|
|
5
5
|
* to the local relay endpoint.
|
|
6
6
|
*
|
|
7
7
|
* A call can legitimately run for many minutes (a remote turn plus an approval
|
|
@@ -46,7 +46,7 @@ export function createRemoteRelayTool({
|
|
|
46
46
|
} catch (error) {
|
|
47
47
|
// A tool exception must never kill the run.
|
|
48
48
|
const message = error?.message || String(error);
|
|
49
|
-
dbg('
|
|
49
|
+
dbg('relay_tool failed', message);
|
|
50
50
|
const result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
|
|
51
51
|
return {
|
|
52
52
|
structuredContent: result,
|
|
@@ -126,7 +126,7 @@ export function createCursorTurnRunner({
|
|
|
126
126
|
// The error an agent started in `cwd` fails with up front, or null
|
|
127
127
|
// (shared/worker-runtime/working-directory.mjs).
|
|
128
128
|
checkWorkingDirectoryImpl = missingWorkingDirectoryError,
|
|
129
|
-
// Whether agent handles carry the relay's `
|
|
129
|
+
// Whether agent handles carry the relay's `relay_tool` tool
|
|
130
130
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
131
131
|
remoteRelayToolGate = null,
|
|
132
132
|
dbg = () => {},
|
|
@@ -149,7 +149,7 @@ export function createCursorTurnRunner({
|
|
|
149
149
|
// Open ask_user round-trips: the merged-stream stall watchdog defers while
|
|
150
150
|
// a human is thinking about a question card.
|
|
151
151
|
let pendingAskUserCalls = 0;
|
|
152
|
-
// Running
|
|
152
|
+
// Running relay_tool calls, same family: the relay may be waiting up to
|
|
153
153
|
// 600 s for a remote turn (plus an approval card) with no SDK traffic.
|
|
154
154
|
let pendingRemoteRelayCalls = 0;
|
|
155
155
|
// Mirrors the Copilot extension's lastPromptedRelayMode: full mode
|
|
@@ -178,7 +178,7 @@ export function createCursorTurnRunner({
|
|
|
178
178
|
// plain refusal the model can relay instead of a failed turn.
|
|
179
179
|
const previewTool = createPreviewTool({
|
|
180
180
|
api,
|
|
181
|
-
// Between turns, the worker's own session (as
|
|
181
|
+
// Between turns, the worker's own session (as relay_tool below): list
|
|
182
182
|
// and close are scoped to a conversation and refused without one.
|
|
183
183
|
getConversationId: () => activeMessage?.conversationId || sdkSessionId || '',
|
|
184
184
|
dbg,
|
|
@@ -202,7 +202,7 @@ export function createCursorTurnRunner({
|
|
|
202
202
|
execute: previewTool.execute,
|
|
203
203
|
},
|
|
204
204
|
};
|
|
205
|
-
// `
|
|
205
|
+
// `relay_tool` joins the set only while this relay has paired remotes.
|
|
206
206
|
// Asked once when the worker starts; the first handle waits for the answer
|
|
207
207
|
// (it is created asynchronously anyway), and every handle creation refreshes
|
|
208
208
|
// it so a later rebuild picks up a remote paired in the meantime.
|
|
@@ -47,8 +47,8 @@ export function summarizeToolInput(toolName, input) {
|
|
|
47
47
|
/**
|
|
48
48
|
* The relay's custom tools reach the stream as the SDK's generic MCP call
|
|
49
49
|
* (`name: 'mcp'`, args `{ providerIdentifier: 'custom-user-tools', toolName,
|
|
50
|
-
* args }`). `
|
|
51
|
-
* summary, so the transcript reads `Tool (
|
|
50
|
+
* args }`). `relay_tool` is shown as itself with the contract's one-line
|
|
51
|
+
* summary, so the transcript reads `Tool (relay_tool): send → …` like every
|
|
52
52
|
* other provider's; null for any other tool.
|
|
53
53
|
*/
|
|
54
54
|
function remoteRelayCallArgs(toolName, input) {
|
package/server/db-schema.mjs
CHANGED
|
@@ -36,6 +36,10 @@ import { migrateDeepSeekUsageCheckpointRuns } from './migrations/0027-deepseek-u
|
|
|
36
36
|
import { migrateDeepSeekUsageCheckpointRequestRuns } from './migrations/0028-deepseek-usage-checkpoint-request-runs.mjs';
|
|
37
37
|
import { migrateDeepSeekBalanceRises } from './migrations/0029-deepseek-balance-rises.mjs';
|
|
38
38
|
import { migrateDeepSeekUsageActuals } from './migrations/0030-deepseek-usage-actuals.mjs';
|
|
39
|
+
import { migrateQueueProvider } from './migrations/0031-queue-provider.mjs';
|
|
40
|
+
import { migrateRuntimeSessionBillingSwitch } from './migrations/0032-runtime-session-billing-switch.mjs';
|
|
41
|
+
import { migrateClaudeApiUsageCheckpoints } from './migrations/0033-claude-api-usage-checkpoints.mjs';
|
|
42
|
+
import { migrateClaudeApiUsageTotals } from './migrations/0034-claude-api-usage-totals.mjs';
|
|
39
43
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
40
44
|
|
|
41
45
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -905,7 +909,7 @@ try {
|
|
|
905
909
|
console.warn(`[relay-schedules] migration 0022 failed; retrying on next boot: ${error?.message || error}`);
|
|
906
910
|
}
|
|
907
911
|
|
|
908
|
-
// Questions that don't block (
|
|
912
|
+
// Questions that don't block (relay_tool ask): one new table. The ask
|
|
909
913
|
// service checks for it and refuses the ask actions without it, so a failure
|
|
910
914
|
// is logged and retried next boot like 0022.
|
|
911
915
|
try {
|
|
@@ -1027,6 +1031,34 @@ if (runtimeSessionColumns.length) {
|
|
|
1027
1031
|
}
|
|
1028
1032
|
}
|
|
1029
1033
|
|
|
1034
|
+
// The Claude billing switch: the billing a queue row runs on, and the chat's
|
|
1035
|
+
// last switch. After runtime_sessions has provider_type: 0031's back-fill
|
|
1036
|
+
// reads it. Every reader and writer of these columns checks for them, so a
|
|
1037
|
+
// failure is logged and retried next boot like 0004.
|
|
1038
|
+
try {
|
|
1039
|
+
migrateQueueProvider(db);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
console.warn(`[claude-billing] migration 0031 failed; retrying on next boot: ${error?.message || error}`);
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
migrateRuntimeSessionBillingSwitch(db);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.warn(`[claude-billing] migration 0032 failed; retrying on next boot: ${error?.message || error}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Claude API usage: the checkpoints across both Claude billings and the
|
|
1050
|
+
// booked totals per day and model, handled like 0024.
|
|
1051
|
+
try {
|
|
1052
|
+
migrateClaudeApiUsageCheckpoints(db);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.warn(`[claude-api-usage] migration 0033 failed; retrying on next boot: ${error?.message || error}`);
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
migrateClaudeApiUsageTotals(db);
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
console.warn(`[claude-api-usage] migration 0034 failed; retrying on next boot: ${error?.message || error}`);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1030
1062
|
const conversationColumns = db.prepare(`PRAGMA table_info(conversations)`).all().map((c) => c.name);
|
|
1031
1063
|
if (!conversationColumns.includes('archived')) {
|
|
1032
1064
|
db.exec(`ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0`);
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
// directory of its own, no nonessential traffic, and nothing of the host's
|
|
8
8
|
// Claude login or of another Anthropic configuration. The endpoint, the
|
|
9
9
|
// key and everything else that routes the CLI's requests or decides whom
|
|
10
|
-
// it trusts (
|
|
10
|
+
// it trusts (CLAUDE_CLI_PINNED_ENV: provider switches, credentials,
|
|
11
11
|
// proxies, TLS) are pinned again in the CLI's flag settings
|
|
12
12
|
// (liveFlagSettings), over the settings of the chat's folder.
|
|
13
|
+
// - Hooks: none run, neither the user's nor the chat folder's, and no
|
|
14
|
+
// statusLine command (CLAUDE_CLI_KEY_CHAT_SETTINGS, on the command line).
|
|
13
15
|
// - The model: DeepSeek's ids, with Claude Code's `[1m]` suffix so the CLI
|
|
14
16
|
// plans with the 1M window; helper calls (titles, summaries) go to flash.
|
|
15
17
|
// Claude Code's model aliases are pinned to the session's model
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
|
|
35
37
|
import { deepseekAnthropicBaseUrl } from '../../shared/deepseek-api.mjs';
|
|
36
38
|
import { isHostClaudeSessionVariable } from '../../shared/host-claude-session-env.mjs';
|
|
39
|
+
import { CLAUDE_CLI_KEY_CHAT_SETTINGS, claudeCliPinnedEnv } from '../claude-worker/claude-cli-pinned-env.mjs';
|
|
37
40
|
import {
|
|
38
41
|
DEEPSEEK_PROVIDER_TYPE,
|
|
39
42
|
DEFAULT_DEEPSEEK_MODEL,
|
|
@@ -120,133 +123,6 @@ const FOREIGN_CLI_VARIABLES = Object.freeze([
|
|
|
120
123
|
// ANTHROPIC_AUTH_TOKEN) and the relay's own OAR_* switches.
|
|
121
124
|
const FOREIGN_CLI_PREFIXES = Object.freeze(['ANTHROPIC_', 'COPILOT_WEB_RELAY_', 'COPILOT_PROVIDER_', 'DEEPSEEK_', 'OAR_']);
|
|
122
125
|
|
|
123
|
-
/**
|
|
124
|
-
* Every variable through which the env of the chat folder's settings
|
|
125
|
-
* (.claude/settings.json, settings.local.json) could send the CLI's requests,
|
|
126
|
-
* and with them the key, somewhere else, or change whom the CLI trusts on the
|
|
127
|
-
* way: the endpoints, the provider switches, the credentials, the proxies and
|
|
128
|
-
* the TLS trust. liveFlagSettings pins each one in the flag settings layer,
|
|
129
|
-
* which the CLI applies over the folder's, to the value the CLI was spawned
|
|
130
|
-
* with: the worker's own, else the other case's for a proxy variable, else ''.
|
|
131
|
-
* '' is unset to the CLI for each of these (truthy or trimmed reads), except
|
|
132
|
-
* SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
|
|
133
|
-
* store, so those two are pinned only when the worker sets them.
|
|
134
|
-
*
|
|
135
|
-
* Source: the names Claude Code 2.1.304 (the bundled CLI) reads, by a grep
|
|
136
|
-
* of the binary; the CLI's own lists of endpoints (with their selection
|
|
137
|
-
* switches and companions) and credentials are in it as they are there. A
|
|
138
|
-
* deny-list, because a settings layer can only pin names known in advance.
|
|
139
|
-
* Left out, each for its reason: the model variables (same endpoint, same
|
|
140
|
-
* key), what the CLI already drops from a folder's settings or from all
|
|
141
|
-
* settings (CLAUDE_CONFIG_DIR, CLAUDE_CODE_CUSTOM_OAUTH_URL, the OTEL
|
|
142
|
-
* exporter endpoints, ANTHROPIC_UNIX_SOCKET, the host-managed provider
|
|
143
|
-
* switches, the file-descriptor credentials), a provider's own SDK settings
|
|
144
|
-
* (AWS_*, GOOGLE_*, AZURE_*, read only for the provider the switches here
|
|
145
|
-
* turn off), NODE_OPTIONS (at runtime the CLI reads only its system-CA
|
|
146
|
-
* flags, which add no CA), and what routes only other programs' traffic
|
|
147
|
-
* (NPM_CONFIG_*, GIT_*, the resolver's variables).
|
|
148
|
-
*/
|
|
149
|
-
export const DEEPSEEK_CLI_PINNED_ENV = Object.freeze([
|
|
150
|
-
// Endpoints: the CLI's list, its fallback and the headers sent with them.
|
|
151
|
-
'ANTHROPIC_BASE_URL',
|
|
152
|
-
'_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL',
|
|
153
|
-
'ANTHROPIC_CUSTOM_HEADERS',
|
|
154
|
-
'ANTHROPIC_BEDROCK_BASE_URL',
|
|
155
|
-
'ANTHROPIC_VERTEX_BASE_URL',
|
|
156
|
-
'ANTHROPIC_FOUNDRY_BASE_URL',
|
|
157
|
-
'ANTHROPIC_AWS_BASE_URL',
|
|
158
|
-
'ANTHROPIC_GOOGLE_CLOUD_BASE_URL',
|
|
159
|
-
'ANTHROPIC_BEDROCK_MANTLE_BASE_URL',
|
|
160
|
-
'CLAUDE_CODE_API_BASE_URL',
|
|
161
|
-
'CLAUDE_CODE_ARTIFACTS_API_BASE_URL',
|
|
162
|
-
'CLAUDE_CODE_ARTIFACTS_API_TOKEN',
|
|
163
|
-
'CLAUDE_CODE_ARTIFACT_ASSET_BASE_URL',
|
|
164
|
-
'CLAUDE_CODE_ARTIFACT_LIVE_BASE_URL',
|
|
165
|
-
'CLAUDE_CODE_ARTIFACT_SYNC_BASE_URL',
|
|
166
|
-
'CLAUDE_CODE_ARTIFACT_VIEWER_BASE_URL',
|
|
167
|
-
'CLAUDE_CODE_MEMORY_API_BASE_URL',
|
|
168
|
-
'CLAUDE_CODE_MEMORY_API_TOKEN',
|
|
169
|
-
// Provider switches.
|
|
170
|
-
'CLAUDE_CODE_USE_BEDROCK',
|
|
171
|
-
'CLAUDE_CODE_USE_VERTEX',
|
|
172
|
-
'CLAUDE_CODE_USE_FOUNDRY',
|
|
173
|
-
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
174
|
-
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
175
|
-
'CLAUDE_CODE_USE_MANTLE',
|
|
176
|
-
'CLAUDE_CODE_USE_GATEWAY',
|
|
177
|
-
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
178
|
-
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
179
|
-
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
180
|
-
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
181
|
-
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
182
|
-
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
183
|
-
// Credentials: the CLI's list, the OAuth login, the federation profile and
|
|
184
|
-
// the proxy credential helper.
|
|
185
|
-
'ANTHROPIC_API_KEY',
|
|
186
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
187
|
-
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
188
|
-
'AWS_BEARER_TOKEN_BEDROCK',
|
|
189
|
-
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
190
|
-
'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
|
|
191
|
-
'ANTHROPIC_AWS_API_KEY',
|
|
192
|
-
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
193
|
-
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
194
|
-
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
195
|
-
'ANTHROPIC_PROFILE',
|
|
196
|
-
'ANTHROPIC_IDENTITY_TOKEN',
|
|
197
|
-
'ANTHROPIC_IDENTITY_TOKEN_FILE',
|
|
198
|
-
'ANTHROPIC_FEDERATION_RULE_ID',
|
|
199
|
-
'ANTHROPIC_ORGANIZATION_ID',
|
|
200
|
-
'ANTHROPIC_SERVICE_ACCOUNT_ID',
|
|
201
|
-
'ANTHROPIC_SCOPE',
|
|
202
|
-
'ANTHROPIC_WORKSPACE_ID',
|
|
203
|
-
'CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER',
|
|
204
|
-
// Proxies: the CLI reads both cases, the lower one first.
|
|
205
|
-
'HTTPS_PROXY',
|
|
206
|
-
'https_proxy',
|
|
207
|
-
'HTTP_PROXY',
|
|
208
|
-
'http_proxy',
|
|
209
|
-
'ALL_PROXY',
|
|
210
|
-
'all_proxy',
|
|
211
|
-
'NO_PROXY',
|
|
212
|
-
'no_proxy',
|
|
213
|
-
'CLAUDE_CODE_HTTPS_PROXY',
|
|
214
|
-
'CLAUDE_CODE_HTTP_PROXY',
|
|
215
|
-
// TLS trust and the client certificate.
|
|
216
|
-
'NODE_TLS_REJECT_UNAUTHORIZED',
|
|
217
|
-
'NODE_EXTRA_CA_CERTS',
|
|
218
|
-
'CLAUDE_CODE_CERT_STORE',
|
|
219
|
-
'CLAUDE_CODE_CLIENT_CERT',
|
|
220
|
-
'CLAUDE_CODE_CLIENT_KEY',
|
|
221
|
-
'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
|
|
222
|
-
'SSL_CERT_FILE',
|
|
223
|
-
'SSL_CERT_DIR',
|
|
224
|
-
]);
|
|
225
|
-
// The two of DEEPSEEK_CLI_PINNED_ENV for which '' is not unset (see there).
|
|
226
|
-
const PINNED_ONLY_WHEN_SET = new Set(['SSL_CERT_FILE', 'SSL_CERT_DIR']);
|
|
227
|
-
// The proxy variables the CLI and the tools it runs read in either case.
|
|
228
|
-
const OTHER_CASE = new Map(
|
|
229
|
-
['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY'].flatMap((name) => [
|
|
230
|
-
[name, name.toLowerCase()],
|
|
231
|
-
[name.toLowerCase(), name],
|
|
232
|
-
]),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* The pins of DEEPSEEK_CLI_PINNED_ENV for a CLI spawned with `cliEnv`: the
|
|
237
|
-
* value it was spawned with, else the other case's (a proxy the worker sets
|
|
238
|
-
* as https_proxy holds for HTTPS_PROXY as well), else ''.
|
|
239
|
-
*/
|
|
240
|
-
export function deepseekPinnedEnv(cliEnv = {}) {
|
|
241
|
-
const pins = {};
|
|
242
|
-
for (const name of DEEPSEEK_CLI_PINNED_ENV) {
|
|
243
|
-
const own = cliEnv[name] ?? cliEnv[OTHER_CASE.get(name)];
|
|
244
|
-
if (own !== undefined) pins[name] = String(own);
|
|
245
|
-
else if (!PINNED_ONLY_WHEN_SET.has(name)) pins[name] = '';
|
|
246
|
-
}
|
|
247
|
-
return pins;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
126
|
/**
|
|
251
127
|
* Whether a variable of the worker's stays out of the CLI's environment:
|
|
252
128
|
* another Anthropic configuration or credential, the relay's settings for
|
|
@@ -410,13 +286,21 @@ export function createDeepSeekClaudeBackend({ env = process.env } = {}) {
|
|
|
410
286
|
|
|
411
287
|
spawnEnv,
|
|
412
288
|
|
|
289
|
+
/**
|
|
290
|
+
* The settings layer every CLI of this backend is spawned with, on its
|
|
291
|
+
* command line (no key in it): no hooks and no statusLine command, from
|
|
292
|
+
* the user's settings or the chat folder's. They would run with the key
|
|
293
|
+
* in their environment, a folder's SessionStart hook before the pins.
|
|
294
|
+
*/
|
|
295
|
+
flagSettings: () => ({ ...CLAUDE_CLI_KEY_CHAT_SETTINGS }),
|
|
296
|
+
|
|
413
297
|
/**
|
|
414
298
|
* The CLI's routing, trust and credentials once more, in its flag
|
|
415
299
|
* settings layer, which outranks the settings of the chat's folder: a
|
|
416
300
|
* .claude/settings.json or settings.local.json there whose
|
|
417
301
|
* env.ANTHROPIC_BASE_URL, proxy or CA points elsewhere would otherwise
|
|
418
302
|
* get the DeepSeek key as its bearer token (scripts/deepseek-cli-proof.mjs).
|
|
419
|
-
* Every variable of
|
|
303
|
+
* Every variable of CLAUDE_CLI_PINNED_ENV holds the value of `cliEnv`,
|
|
420
304
|
* the environment the CLI was spawned with (spawnEnv), or is blanked, and
|
|
421
305
|
* so is an apiKeyHelper of the folder's. The key may not go on the
|
|
422
306
|
* command line, so the adapter sends these over the control channel
|
|
@@ -425,7 +309,7 @@ export function createDeepSeekClaudeBackend({ env = process.env } = {}) {
|
|
|
425
309
|
liveFlagSettings(cliEnv = spawnEnv(env)) {
|
|
426
310
|
if (!apiKey) throw new Error('deepseek-api-key-not-configured');
|
|
427
311
|
return {
|
|
428
|
-
env: { ...
|
|
312
|
+
env: { ...claudeCliPinnedEnv(cliEnv), ANTHROPIC_BASE_URL: anthropicBaseUrl, ANTHROPIC_AUTH_TOKEN: apiKey },
|
|
429
313
|
apiKeyHelper: '',
|
|
430
314
|
};
|
|
431
315
|
},
|
|
@@ -358,7 +358,7 @@ export class AcpClient extends EventEmitter {
|
|
|
358
358
|
* `holdStall` (async, optional) is asked once the inactivity window has run
|
|
359
359
|
* out: `true` re-arms the full window instead of failing the prompt. It is
|
|
360
360
|
* for work the agent is blocked on that this client cannot see — a
|
|
361
|
-
*
|
|
361
|
+
* relay_tool call the agent runs through its MCP server, which the relay
|
|
362
362
|
* reports as in flight. A question that fails or takes longer than
|
|
363
363
|
* `holdCheckTimeoutMs` counts as `false`.
|
|
364
364
|
*
|
|
@@ -134,7 +134,7 @@ export function createGrokTurnRunner({
|
|
|
134
134
|
} = {}) {
|
|
135
135
|
const terminalTasks = taskPublisher;
|
|
136
136
|
// The OAR MCP server gives Grok the relay's real tools — `preview`, and
|
|
137
|
-
// `
|
|
137
|
+
// `relay_tool` when remotes are paired — bound to this conversation.
|
|
138
138
|
// `OAR_MCP_SERVER=0` is the operator's way back to the preview instruction
|
|
139
139
|
// block, should an agent build turn out to ignore ACP `mcpServers`.
|
|
140
140
|
const oarMcpLaunch = oarMcpServer !== undefined
|
|
@@ -238,7 +238,7 @@ export function createGrokTurnRunner({
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
|
-
* The inactivity hold. A
|
|
241
|
+
* The inactivity hold. A relay_tool call runs inside the MCP server, which
|
|
242
242
|
* this worker cannot see: Grok just goes quiet for as long as the relay
|
|
243
243
|
* waits on the remote turn. Before the ACP watchdog calls that a stall, the
|
|
244
244
|
* relay is asked whether a call of this conversation is still in flight.
|
|
@@ -247,7 +247,7 @@ export function createGrokTurnRunner({
|
|
|
247
247
|
*/
|
|
248
248
|
async function remoteRelayCallInFlight() {
|
|
249
249
|
const inflight = await fetchRemoteRelayInflight({ api, conversationId: sdkSessionId });
|
|
250
|
-
if (inflight > 0) dbg('
|
|
250
|
+
if (inflight > 0) dbg('relay_tool call in flight; holding the inactivity watchdog', String(inflight));
|
|
251
251
|
return inflight > 0;
|
|
252
252
|
}
|
|
253
253
|
|
|
@@ -447,7 +447,7 @@ export function createGrokTurnRunner({
|
|
|
447
447
|
// absolute cap: "No limit" (0) must not be silently trimmed to the
|
|
448
448
|
// 30-minute default. The inactivity watchdog stays — it detects a dead
|
|
449
449
|
// transport, not a long turn — but asks the relay before it fires while
|
|
450
|
-
// Grok may be blocked on a
|
|
450
|
+
// Grok may be blocked on a relay_tool call.
|
|
451
451
|
const watchdog = {
|
|
452
452
|
...(turnCeilingMs !== null ? { maxTurnMs: turnCeilingMs > 0 ? turnCeilingMs : 0 } : {}),
|
|
453
453
|
...(acpMcpServers.length ? { holdStall: remoteRelayCallInFlight } : {}),
|
|
@@ -64,7 +64,7 @@ export function summarizeToolInput(input) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function formatToolActivityText(toolName, input, title = '') {
|
|
67
|
-
// The OAR MCP server's `
|
|
67
|
+
// The OAR MCP server's `relay_tool`, however the agent labels it (bare,
|
|
68
68
|
// or namespaced by the server), reads as what it does on which relay.
|
|
69
69
|
if (isRemoteRelayToolName(toolName) || isRemoteRelayToolName(title)) {
|
|
70
70
|
const remoteSummary = remoteRelayActivitySummary(input);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// OAR's own stdio MCP server. It gives the agents OAR launches without an
|
|
3
3
|
// in-process tool surface — Grok (ACP `mcpServers`) and the Copilot CLI
|
|
4
4
|
// extension engine (`--additional-mcp-config`) — the relay's real tools:
|
|
5
|
-
// `
|
|
5
|
+
// `relay_tool` and `preview`, bound to the one conversation it was started
|
|
6
6
|
// for, so the relay's mention gate and approval apply exactly as they do for
|
|
7
7
|
// the Claude, Cursor and Copilot SDK workers.
|
|
8
8
|
//
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS,
|
|
35
35
|
REMOTE_RELAY_TOOL_DESCRIPTION,
|
|
36
36
|
REMOTE_RELAY_TOOL_NAME,
|
|
37
|
+
canonicalRemoteRelayToolCallName,
|
|
37
38
|
cloneRemoteRelayToolInputSchema,
|
|
38
39
|
executeRemoteRelayTool,
|
|
39
40
|
formatRemoteRelayToolResult,
|
|
@@ -216,10 +217,15 @@ export function createOarMcpServer({
|
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
async function callTool(id, params) {
|
|
219
|
-
|
|
220
|
+
// `remote_relay`, the tool's name up to 0.9.17, still dispatches while
|
|
221
|
+
// the tool is on, but is never listed: a second definition would only
|
|
222
|
+
// cost context. The hosts in use (Grok, the Copilot CLI) call only what
|
|
223
|
+
// tools/list gave them, so for them the old name is gone; a client that
|
|
224
|
+
// kept it still works.
|
|
225
|
+
const name = canonicalRemoteRelayToolCallName(params?.name);
|
|
220
226
|
const tools = await listTools();
|
|
221
227
|
if (!tools.some((tool) => tool.name === name)) {
|
|
222
|
-
return fail(id, JSON_RPC.invalidParams, `Unknown tool: ${name || '(none)'}`);
|
|
228
|
+
return fail(id, JSON_RPC.invalidParams, `Unknown tool: ${toText(params?.name) || '(none)'}`);
|
|
223
229
|
}
|
|
224
230
|
const args = params?.arguments && typeof params.arguments === 'object' && !Array.isArray(params.arguments)
|
|
225
231
|
? params.arguments
|
|
@@ -348,7 +354,7 @@ export function createOarMcpServer({
|
|
|
348
354
|
|
|
349
355
|
/**
|
|
350
356
|
* The stdio process: parse the arguments, reach the relay like a worker does,
|
|
351
|
-
* decide whether `
|
|
357
|
+
* decide whether `relay_tool` is listed (asked once, at startup), then serve
|
|
352
358
|
* until stdin closes. Returns the server, or null after a startup failure.
|
|
353
359
|
*/
|
|
354
360
|
export async function runOarMcpServer({
|
|
@@ -413,7 +419,7 @@ export async function runOarMcpServer({
|
|
|
413
419
|
remoteRelay: Promise.resolve()
|
|
414
420
|
.then(() => shouldRegisterRemoteRelayToolImpl({ api }))
|
|
415
421
|
.then((enabled) => {
|
|
416
|
-
log(`serving conversation ${conversationId.slice(0, 8)};
|
|
422
|
+
log(`serving conversation ${conversationId.slice(0, 8)}; relay_tool ${enabled ? 'on' : 'off'}`);
|
|
417
423
|
return enabled;
|
|
418
424
|
}),
|
|
419
425
|
version: readPackageVersion(),
|