@pentoshi/clai 4.11.21 → 4.11.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/evidence/task-selection.d.ts +0 -15
- package/dist/agent/evidence/task-selection.js +1 -33
- package/dist/agent/evidence/task-selection.js.map +1 -1
- package/dist/agent/plan/actions/plan-clear.js.map +1 -1
- package/dist/agent/plan/actions/plan-create.js.map +1 -1
- package/dist/agent/plan/actions/task-add.js.map +1 -1
- package/dist/agent/plan/actions/task-move.js.map +1 -1
- package/dist/agent/plan/handle-plan-tool.js +5 -154
- package/dist/agent/plan/handle-plan-tool.js.map +1 -1
- package/dist/agent/runner.js +1 -13
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-policy.d.ts +0 -6
- package/dist/agent/session-policy.js +0 -2
- package/dist/agent/session-policy.js.map +1 -1
- package/dist/agent/task-evidence.d.ts +2 -2
- package/dist/agent/task-evidence.js +1 -1
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/agent/turn/compaction-admission.d.ts +5 -0
- package/dist/agent/turn/compaction-admission.js +24 -0
- package/dist/agent/turn/compaction-admission.js.map +1 -1
- package/dist/agent/turn/compaction-coordinator.d.ts +1 -0
- package/dist/agent/turn/compaction-coordinator.js +2 -0
- package/dist/agent/turn/compaction-coordinator.js.map +1 -1
- package/dist/agent/turn/loop/run-rounds.js +17 -24
- package/dist/agent/turn/loop/run-rounds.js.map +1 -1
- package/dist/agent/turn/loop/state.d.ts +1 -2
- package/dist/agent/turn/loop/state.js +1 -2
- package/dist/agent/turn/loop/state.js.map +1 -1
- package/dist/agent/turn/setup/compaction-services.d.ts +1 -0
- package/dist/agent/turn/setup/compaction-services.js +1 -0
- package/dist/agent/turn/setup/compaction-services.js.map +1 -1
- package/dist/agent/turn/setup/task-gate-setup.d.ts +1 -11
- package/dist/agent/turn/setup/task-gate-setup.js +4 -19
- package/dist/agent/turn/setup/task-gate-setup.js.map +1 -1
- package/dist/agent/turn/tool-execution/deps.d.ts +1 -5
- package/dist/agent/turn/tool-execution/guards.d.ts +0 -4
- package/dist/agent/turn/tool-execution/guards.js +3 -12
- package/dist/agent/turn/tool-execution/guards.js.map +1 -1
- package/dist/agent/turn/tool-execution/meta-tools.d.ts +0 -2
- package/dist/agent/turn/tool-execution/meta-tools.js +0 -17
- package/dist/agent/turn/tool-execution/meta-tools.js.map +1 -1
- package/dist/agent/turn/tool-execution/single-tool.js +0 -14
- package/dist/agent/turn/tool-execution/single-tool.js.map +1 -1
- package/dist/app/commands/catalog.js +32 -1
- package/dist/app/commands/catalog.js.map +1 -1
- package/dist/classic/panels/keys-panel.js +8 -1
- package/dist/classic/panels/keys-panel.js.map +1 -1
- package/dist/classic/panels/pager-panel.d.ts +2 -2
- package/dist/classic/panels/pager-panel.js +10 -7
- package/dist/classic/panels/pager-panel.js.map +1 -1
- package/dist/classic/panels/panel-host.js +1 -1
- package/dist/classic/panels/panel-host.js.map +1 -1
- package/dist/commands/providers.d.ts +17 -0
- package/dist/commands/providers.js +317 -28
- package/dist/commands/providers.js.map +1 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/agentrouter.js +1 -1
- package/dist/llm/anthropic.d.ts +4 -2
- package/dist/llm/anthropic.js +186 -178
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/bynara.js +1 -1
- package/dist/llm/capability/state.d.ts +1 -1
- package/dist/llm/capability/tool-dialect.js +3 -0
- package/dist/llm/capability/tool-dialect.js.map +1 -1
- package/dist/llm/capability/vision-patterns.js +3 -0
- package/dist/llm/capability/vision-patterns.js.map +1 -1
- package/dist/llm/catalog-facts.js +50 -2
- package/dist/llm/catalog-facts.js.map +1 -1
- package/dist/llm/catalog-prefetch.js +1 -1
- package/dist/llm/cline-auth.d.ts +34 -0
- package/dist/llm/cline-auth.js +248 -0
- package/dist/llm/cline-auth.js.map +1 -0
- package/dist/llm/cline.d.ts +2 -0
- package/dist/llm/cline.js +147 -0
- package/dist/llm/cline.js.map +1 -0
- package/dist/llm/codex-auth.d.ts +53 -0
- package/dist/llm/codex-auth.js +512 -0
- package/dist/llm/codex-auth.js.map +1 -0
- package/dist/llm/codex.d.ts +4 -0
- package/dist/llm/codex.js +239 -0
- package/dist/llm/codex.js.map +1 -0
- package/dist/llm/copilot-auth.d.ts +38 -0
- package/dist/llm/copilot-auth.js +258 -0
- package/dist/llm/copilot-auth.js.map +1 -0
- package/dist/llm/copilot.d.ts +4 -0
- package/dist/llm/copilot.js +226 -0
- package/dist/llm/copilot.js.map +1 -0
- package/dist/llm/custom-providers.js +1 -1
- package/dist/llm/deepseek.js +1 -1
- package/dist/llm/explabs.js +1 -1
- package/dist/llm/fireworks.js +1 -1
- package/dist/llm/free-default-model.js +9 -4
- package/dist/llm/free-default-model.js.map +1 -1
- package/dist/llm/free.d.ts +1 -0
- package/dist/llm/free.js +44 -36
- package/dist/llm/free.js.map +1 -1
- package/dist/llm/glm.js +1 -1
- package/dist/llm/hetzner.js +1 -1
- package/dist/llm/key-rotation.js +1 -1
- package/dist/llm/key-rotation.js.map +1 -1
- package/dist/llm/kimi.js +1 -1
- package/dist/llm/lightning.js +1 -1
- package/dist/llm/merge-gateway.js +1 -1
- package/dist/llm/meta.js +1 -1
- package/dist/llm/minimax.js +1 -1
- package/dist/llm/model-families.js +1 -1
- package/dist/llm/model-families.js.map +1 -1
- package/dist/llm/model-layers/model-rules.js +6 -0
- package/dist/llm/model-layers/model-rules.js.map +1 -1
- package/dist/llm/nvidia.js +1 -1
- package/dist/llm/openrouter.js +1 -1
- package/dist/llm/orcarouter.js +1 -1
- package/dist/llm/provider-identity.d.ts +2 -0
- package/dist/llm/provider-identity.js +35 -0
- package/dist/llm/provider-identity.js.map +1 -1
- package/dist/llm/provider-info-text.js +115 -13
- package/dist/llm/provider-info-text.js.map +1 -1
- package/dist/llm/provider-profile-layers.js +101 -0
- package/dist/llm/provider-profile-layers.js.map +1 -1
- package/dist/llm/provider-profiles.js +2 -1
- package/dist/llm/provider-profiles.js.map +1 -1
- package/dist/llm/provider.d.ts +6 -4
- package/dist/llm/provider.js +2 -2
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/qwen-cloud.js +1 -1
- package/dist/llm/reasoning-capability.js +14 -0
- package/dist/llm/reasoning-capability.js.map +1 -1
- package/dist/llm/responses/item-events.js +10 -0
- package/dist/llm/responses/item-events.js.map +1 -1
- package/dist/llm/responses-config.d.ts +4 -0
- package/dist/llm/responses-config.js.map +1 -1
- package/dist/llm/responses-parse.js +23 -11
- package/dist/llm/responses-parse.js.map +1 -1
- package/dist/llm/responses-request.js +31 -8
- package/dist/llm/responses-request.js.map +1 -1
- package/dist/llm/responses-stream-events.js +21 -5
- package/dist/llm/responses-stream-events.js.map +1 -1
- package/dist/llm/routing/attempt-complete.js +1 -1
- package/dist/llm/routing/attempt-complete.js.map +1 -1
- package/dist/llm/routing/attempt-request.js +1 -1
- package/dist/llm/routing/attempt-request.js.map +1 -1
- package/dist/llm/routing/attempt-stream.js +2 -2
- package/dist/llm/routing/attempt-stream.js.map +1 -1
- package/dist/llm/routing/failure-report.js +1 -1
- package/dist/llm/routing/failure-report.js.map +1 -1
- package/dist/llm/routing/key-rotation.js +9 -4
- package/dist/llm/routing/key-rotation.js.map +1 -1
- package/dist/llm/routing/provider-selection.d.ts +2 -1
- package/dist/llm/routing/provider-selection.js +18 -4
- package/dist/llm/routing/provider-selection.js.map +1 -1
- package/dist/llm/tokenrouter.js +1 -1
- package/dist/llm/vercel.js +1 -1
- package/dist/llm/wire/chat-body.js +5 -3
- package/dist/llm/wire/chat-body.js.map +1 -1
- package/dist/llm/wire/openai-complete.d.ts +1 -0
- package/dist/llm/wire/openai-complete.js +9 -0
- package/dist/llm/wire/openai-complete.js.map +1 -1
- package/dist/store/config.js +3 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/keys/search-providers.d.ts +2 -0
- package/dist/store/keys/search-providers.js +6 -0
- package/dist/store/keys/search-providers.js.map +1 -1
- package/dist/store/keys.d.ts +6 -1
- package/dist/store/keys.js +46 -17
- package/dist/store/keys.js.map +1 -1
- package/dist/store/plan/mutation.d.ts +0 -1
- package/dist/store/plan/mutation.js +1 -22
- package/dist/store/plan/mutation.js.map +1 -1
- package/dist/store/plan.d.ts +1 -1
- package/dist/store/plan.js +1 -4
- package/dist/store/plan.js.map +1 -1
- package/dist/tui-v2/components/modal/keys-modal-pick.d.ts +5 -0
- package/dist/tui-v2/components/modal/keys-modal-pick.js +13 -0
- package/dist/tui-v2/components/modal/keys-modal-pick.js.map +1 -1
- package/dist/tui-v2/components/modal/keys-modal.js +15 -13
- package/dist/tui-v2/components/modal/keys-modal.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-core/bootstrap/capabilities.d.ts +1 -0
- package/dist/ui-core/bootstrap/capabilities.js +14 -0
- package/dist/ui-core/bootstrap/capabilities.js.map +1 -1
- package/dist/ui-core/commands/key-commands.d.ts +6 -3
- package/dist/ui-core/commands/key-commands.js +506 -15
- package/dist/ui-core/commands/key-commands.js.map +1 -1
- package/dist/ui-core/commands/keys/editors.js +10 -4
- package/dist/ui-core/commands/keys/editors.js.map +1 -1
- package/dist/ui-core/commands/picker-commands.js +86 -32
- package/dist/ui-core/commands/picker-commands.js.map +1 -1
- package/dist/ui-core/commands/subagent-models-editor.js +12 -4
- package/dist/ui-core/commands/subagent-models-editor.js.map +1 -1
- package/dist/ui-core/controllers/overlay-controller.d.ts +4 -0
- package/dist/ui-core/controllers/overlay-controller.js.map +1 -1
- package/dist/ui-core/rendering/format-keys.js +4 -2
- package/dist/ui-core/rendering/format-keys.js.map +1 -1
- package/dist/ui-core/rendering/format-usage.js +3 -1
- package/dist/ui-core/rendering/format-usage.js.map +1 -1
- package/dist/ui-core/rendering/intro-header.js +4 -1
- package/dist/ui-core/rendering/intro-header.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +1 -1
- package/dist/agent/task-sync.d.ts +0 -40
- package/dist/agent/task-sync.js +0 -105
- package/dist/agent/task-sync.js.map +0 -1
- package/dist/agent/turn/task-batch-guard.d.ts +0 -18
- package/dist/agent/turn/task-batch-guard.js +0 -83
- package/dist/agent/turn/task-batch-guard.js.map +0 -1
- package/dist/agent/turn/task-gate.d.ts +0 -13
- package/dist/agent/turn/task-gate.js +0 -34
- package/dist/agent/turn/task-gate.js.map +0 -1
- package/dist/agent/turn/task-update-gate.d.ts +0 -13
- package/dist/agent/turn/task-update-gate.js +0 -40
- package/dist/agent/turn/task-update-gate.js.map +0 -1
- package/dist/store/task-transitions.d.ts +0 -12
- package/dist/store/task-transitions.js +0 -40
- package/dist/store/task-transitions.js.map +0 -1
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credential management: /set, /unset, /keys, /info (multi-key per provider).
|
|
3
|
-
*/
|
|
4
1
|
import { getProvider } from "../../llm/router.js";
|
|
5
2
|
import { assertProvider, getProviderInfoText, maskSecret, normalizeEndpointUrl, } from "../../llm/provider.js";
|
|
6
3
|
import { appendProviderEndpoint, getConfig, getProviderEndpoints, providerUsesEndpoints, setProviderEndpoints, updateConfig } from "../../store/config.js";
|
|
@@ -8,6 +5,11 @@ import { appendSearchProviderKey, getProviderKeys, getSearchProviderKeys, listPr
|
|
|
8
5
|
import { searchProviderIds } from "../../tools/web/types.js";
|
|
9
6
|
import { formatKeyStatus } from "../rendering/format-keys.js";
|
|
10
7
|
import { notice, openEndpointsEditor, openSearchKeysEditor, resolveEditorRowsDetailed } from "./keys/editors.js";
|
|
8
|
+
import { pollClineDeviceAuth, startClineDeviceAuth, } from "../../llm/cline-auth.js";
|
|
9
|
+
import { codexKeyFromAccessToken, encodeCodexKey, pollCodexDeviceAuth, startCodexBrowserAuth, startCodexDeviceAuth, } from "../../llm/codex-auth.js";
|
|
10
|
+
import { openSystemBrowser } from "../../mcp/auth/loopback.js";
|
|
11
|
+
import { pollCopilotDeviceAuth, startCopilotDeviceAuth, } from "../../llm/copilot-auth.js";
|
|
12
|
+
import { appendProviderKey, replaceProviderKey } from "../../store/keys.js";
|
|
11
13
|
import { MAX_PROVIDER_KEYS } from "../../llm/key-rotation.js";
|
|
12
14
|
const SEARCH_IDS = new Set(["brave", "tavily", "duckduckgo", "exa"]);
|
|
13
15
|
async function getSearchKeyStatuses() {
|
|
@@ -114,7 +116,7 @@ async function openSetPicker(services) {
|
|
|
114
116
|
: `✓ ${count} keys`;
|
|
115
117
|
const row = {
|
|
116
118
|
value: `llm:${status.provider}`,
|
|
117
|
-
label: `${status.provider} ${keyLabel}${status.active ? " (active)" : ""}`,
|
|
119
|
+
label: `${getProvider(status.provider).displayName} ${keyLabel}${status.active ? " (active)" : ""}`,
|
|
118
120
|
};
|
|
119
121
|
if (!status.endpoints)
|
|
120
122
|
return [row];
|
|
@@ -123,7 +125,7 @@ async function openSetPicker(services) {
|
|
|
123
125
|
row,
|
|
124
126
|
{
|
|
125
127
|
value: `endpoint:${status.provider}`,
|
|
126
|
-
label: `${status.provider} endpoints ${urlCount === 0 ? "✗ none" : `✓ ${urlCount} URL${urlCount === 1 ? "" : "s"}`}`,
|
|
128
|
+
label: `${getProvider(status.provider).displayName} endpoints ${urlCount === 0 ? "✗ none" : `✓ ${urlCount} URL${urlCount === 1 ? "" : "s"}`}`,
|
|
127
129
|
description: status.note ?? "endpoint URLs",
|
|
128
130
|
},
|
|
129
131
|
];
|
|
@@ -175,7 +177,7 @@ async function openUnsetPicker(services) {
|
|
|
175
177
|
: `✓ ${count} keys — reset all`;
|
|
176
178
|
const row = {
|
|
177
179
|
value: `llm:${status.provider}`,
|
|
178
|
-
label: `${status.provider} ${keyLabel}${status.active ? " (active)" : ""}`,
|
|
180
|
+
label: `${getProvider(status.provider).displayName} ${keyLabel}${status.active ? " (active)" : ""}`,
|
|
179
181
|
};
|
|
180
182
|
if (!status.endpoints || status.endpoints.length === 0)
|
|
181
183
|
return [row];
|
|
@@ -183,7 +185,7 @@ async function openUnsetPicker(services) {
|
|
|
183
185
|
row,
|
|
184
186
|
{
|
|
185
187
|
value: `endpoint:${status.provider}`,
|
|
186
|
-
label: `${status.provider} endpoints ✓ ${status.endpoints.length} URL${status.endpoints.length === 1 ? "" : "s"} — clear all`,
|
|
188
|
+
label: `${getProvider(status.provider).displayName} endpoints ✓ ${status.endpoints.length} URL${status.endpoints.length === 1 ? "" : "s"} — clear all`,
|
|
187
189
|
description: status.note ?? "endpoint URLs",
|
|
188
190
|
},
|
|
189
191
|
];
|
|
@@ -215,7 +217,7 @@ async function openUnsetPicker(services) {
|
|
|
215
217
|
else if (kind === "endpoint") {
|
|
216
218
|
const count = getProviderEndpoints(id).urls.length;
|
|
217
219
|
setProviderEndpoints(id, []);
|
|
218
|
-
notice(services, "info", `unset ${count} endpoint URL(s) for ${id}`);
|
|
220
|
+
notice(services, "info", `unset ${count} endpoint URL(s) for ${getProvider(id).displayName}`);
|
|
219
221
|
}
|
|
220
222
|
else
|
|
221
223
|
await unsetLlmKey(services, id);
|
|
@@ -247,7 +249,7 @@ async function appendLlmKey(services, id, keyVal) {
|
|
|
247
249
|
const endpoint = normalizeEndpointUrl(keyVal.trim());
|
|
248
250
|
try {
|
|
249
251
|
const { endpoints, added } = appendProviderEndpoint(id, endpoint);
|
|
250
|
-
notice(services, "info", `${added ? "saved" : "activated"} ${id} endpoint #${endpoints.activeIndex + 1}/${endpoints.urls.length} → ${endpoint}`);
|
|
252
|
+
notice(services, "info", `${added ? "saved" : "activated"} ${getProvider(id).displayName} endpoint #${endpoints.activeIndex + 1}/${endpoints.urls.length} → ${endpoint}`);
|
|
251
253
|
}
|
|
252
254
|
catch (error) {
|
|
253
255
|
notice(services, "warn", error instanceof Error ? error.message : String(error));
|
|
@@ -255,10 +257,11 @@ async function appendLlmKey(services, id, keyVal) {
|
|
|
255
257
|
return;
|
|
256
258
|
}
|
|
257
259
|
const key = keyVal.trim();
|
|
260
|
+
const label = getProvider(id).displayName;
|
|
258
261
|
if (!getProvider(id).validateKey(key)) {
|
|
259
262
|
notice(services, "warn", providerUsesEndpoints(id)
|
|
260
|
-
? `invalid value for ${
|
|
261
|
-
: `invalid API key format for ${
|
|
263
|
+
? `invalid value for ${label} · expected a key/token, or an https:// URL to add an endpoint`
|
|
264
|
+
: `invalid API key format for ${label}`);
|
|
262
265
|
return;
|
|
263
266
|
}
|
|
264
267
|
const { appendProviderKey } = await import("../../store/keys.js");
|
|
@@ -266,8 +269,8 @@ async function appendLlmKey(services, id, keyVal) {
|
|
|
266
269
|
const multi = await getProviderKeys(id);
|
|
267
270
|
const count = multi.source === "env" ? 1 : multi.keys.length;
|
|
268
271
|
notice(services, "info", count > 1
|
|
269
|
-
? `added ${
|
|
270
|
-
: `saved ${
|
|
272
|
+
? `added ${label} ${maskSecret(key)} · ${count} keys total`
|
|
273
|
+
: `saved ${label} ${maskSecret(key)}`);
|
|
271
274
|
}
|
|
272
275
|
async function setSearchKey(services, id, keyVal) {
|
|
273
276
|
if (id === "duckduckgo") {
|
|
@@ -309,14 +312,488 @@ async function unsetLlmKey(services, id) {
|
|
|
309
312
|
notice(services, "info", "ollama does not store an API key");
|
|
310
313
|
return;
|
|
311
314
|
}
|
|
315
|
+
const label = getProvider(id).displayName;
|
|
312
316
|
const multi = await getProviderKeys(id);
|
|
313
317
|
const storedCount = multi.source === "env" ? 0 : multi.keys.length;
|
|
314
318
|
if (storedCount === 0) {
|
|
315
|
-
notice(services, "warn", `${
|
|
319
|
+
notice(services, "warn", `${label} has no key to unset`);
|
|
316
320
|
return;
|
|
317
321
|
}
|
|
318
322
|
await unsetProviderSecret(id);
|
|
319
|
-
notice(services, "info", storedCount > 1 ? `unset all ${storedCount} keys for ${
|
|
323
|
+
notice(services, "info", storedCount > 1 ? `unset all ${storedCount} keys for ${label}` : `unset ${label}`);
|
|
324
|
+
}
|
|
325
|
+
export async function runClineAuthForUI(services) {
|
|
326
|
+
let start;
|
|
327
|
+
try {
|
|
328
|
+
start = await startClineDeviceAuth();
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
notice(services, "warn", `could not start Cline sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
services.overlay.openPager("Cline sign-in", [
|
|
335
|
+
"Authenticate Cline on any device:",
|
|
336
|
+
"",
|
|
337
|
+
` ${start.verificationUrl}`,
|
|
338
|
+
"",
|
|
339
|
+
` Code: ${start.userCode}`,
|
|
340
|
+
"",
|
|
341
|
+
"Approve in your browser — clai will continue automatically.",
|
|
342
|
+
"(close this and press Ctrl-C to cancel)",
|
|
343
|
+
].join("\n"), undefined, undefined, "plain");
|
|
344
|
+
const waiting = services.toast.info("waiting for Cline approval…", {
|
|
345
|
+
sticky: true,
|
|
346
|
+
});
|
|
347
|
+
try {
|
|
348
|
+
const tokens = await pollClineDeviceAuth(start);
|
|
349
|
+
services.toast.dismiss(waiting);
|
|
350
|
+
notice(services, "info", "Cline authenticated");
|
|
351
|
+
return tokens;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
services.toast.dismiss(waiting);
|
|
355
|
+
notice(services, "warn", `Cline sign-in failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function clineAddAccount(services) {
|
|
360
|
+
const tokens = await runClineAuthForUI(services);
|
|
361
|
+
if (!tokens) {
|
|
362
|
+
notice(services, "info", "cancelled");
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
return tokens;
|
|
366
|
+
}
|
|
367
|
+
async function promptCodexApiKey(services) {
|
|
368
|
+
const answer = await services.overlay.openSecret({
|
|
369
|
+
title: "ChatGPT Subscription access token",
|
|
370
|
+
prompt: "Paste a ChatGPT access token (JWT from auth.openai.com) or an existing `codex:` key:",
|
|
371
|
+
});
|
|
372
|
+
const value = answer?.trim();
|
|
373
|
+
if (!value)
|
|
374
|
+
return undefined;
|
|
375
|
+
const key = codexKeyFromAccessToken(value) ?? (value.startsWith("codex:") ? value : undefined);
|
|
376
|
+
if (!key) {
|
|
377
|
+
notice(services, "warn", "invalid ChatGPT token — expected a JWT containing the chatgpt account id");
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
return key;
|
|
381
|
+
}
|
|
382
|
+
function pickCodexAuthMethod(services) {
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
const opened = services.overlay.openPicker({
|
|
385
|
+
title: "ChatGPT Subscription sign-in method",
|
|
386
|
+
options: [
|
|
387
|
+
{
|
|
388
|
+
value: "browser",
|
|
389
|
+
label: "Sign in with ChatGPT (browser)",
|
|
390
|
+
description: "opens auth.openai.com in your browser",
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
value: "headless",
|
|
394
|
+
label: "Sign in with ChatGPT (headless)",
|
|
395
|
+
description: "device code — works on remote/SSH machines",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
value: "apikey",
|
|
399
|
+
label: "Manually enter access token / API key",
|
|
400
|
+
description: "paste an existing Chatgpt Subscription token",
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
}, (value) => {
|
|
404
|
+
services.overlay.close();
|
|
405
|
+
resolve(value);
|
|
406
|
+
});
|
|
407
|
+
if (!opened)
|
|
408
|
+
resolve(undefined);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
export async function runCodexAuthForUI(services) {
|
|
412
|
+
const method = await pickCodexAuthMethod(services);
|
|
413
|
+
if (method === "headless")
|
|
414
|
+
return runCodexDeviceAuthForUI(services);
|
|
415
|
+
if (method === "apikey") {
|
|
416
|
+
const key = await promptCodexApiKey(services);
|
|
417
|
+
return key
|
|
418
|
+
? { manualKey: key, accessToken: "", accountId: "" }
|
|
419
|
+
: undefined;
|
|
420
|
+
}
|
|
421
|
+
if (method !== "browser")
|
|
422
|
+
return undefined;
|
|
423
|
+
let handle;
|
|
424
|
+
try {
|
|
425
|
+
handle = await startCodexBrowserAuth();
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return runCodexDeviceAuthForUI(services);
|
|
429
|
+
}
|
|
430
|
+
services.overlay.openPager("ChatGPT Subscription sign-in", [
|
|
431
|
+
"Opening ChatGPT sign-in in your browser…",
|
|
432
|
+
"",
|
|
433
|
+
` ${handle.url}`,
|
|
434
|
+
"",
|
|
435
|
+
"Log in with your ChatGPT account in the browser tab that opens.",
|
|
436
|
+
"clai will continue automatically once authorization completes.",
|
|
437
|
+
"(close this and press Ctrl-C to cancel)",
|
|
438
|
+
].join("\n"), undefined, undefined, "plain");
|
|
439
|
+
const waiting = services.toast.info("waiting for ChatGPT approval…", {
|
|
440
|
+
sticky: true,
|
|
441
|
+
});
|
|
442
|
+
try {
|
|
443
|
+
await openSystemBrowser(handle.url).catch(() => { });
|
|
444
|
+
const credential = await handle.waitForCredential();
|
|
445
|
+
services.toast.dismiss(waiting);
|
|
446
|
+
notice(services, "info", "ChatGPT Subscription authenticated");
|
|
447
|
+
return credential;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
services.toast.dismiss(waiting);
|
|
451
|
+
notice(services, "warn", `ChatGPT Subscription sign-in failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export async function runCodexDeviceAuthForUI(services) {
|
|
456
|
+
let start;
|
|
457
|
+
try {
|
|
458
|
+
start = await startCodexDeviceAuth();
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
notice(services, "warn", `could not start ChatGPT Subscription sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
services.overlay.openPager("ChatGPT Subscription sign-in", [
|
|
465
|
+
"Authenticate ChatGPT Subscription on any device:",
|
|
466
|
+
"",
|
|
467
|
+
` ${start.verificationUrl}`,
|
|
468
|
+
"",
|
|
469
|
+
` Code: ${start.userCode}`,
|
|
470
|
+
"",
|
|
471
|
+
"Approve in your browser — clai will continue automatically.",
|
|
472
|
+
"(close this and press Ctrl-C to cancel)",
|
|
473
|
+
].join("\n"), undefined, undefined, "plain");
|
|
474
|
+
const waiting = services.toast.info("waiting for ChatGPT approval…", {
|
|
475
|
+
sticky: true,
|
|
476
|
+
});
|
|
477
|
+
try {
|
|
478
|
+
const credential = await pollCodexDeviceAuth(start);
|
|
479
|
+
services.toast.dismiss(waiting);
|
|
480
|
+
notice(services, "info", "ChatGPT Subscription authenticated");
|
|
481
|
+
return credential;
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
services.toast.dismiss(waiting);
|
|
485
|
+
notice(services, "warn", `ChatGPT Subscription sign-in failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
export async function runCopilotAuthForUI(services) {
|
|
490
|
+
let start;
|
|
491
|
+
try {
|
|
492
|
+
start = await startCopilotDeviceAuth();
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
notice(services, "warn", `could not start Github Copilot sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
services.overlay.openPager("Github Copilot sign-in", [
|
|
499
|
+
"Authenticate Github Copilot on any device:",
|
|
500
|
+
"",
|
|
501
|
+
` ${start.verificationUrl}`,
|
|
502
|
+
"",
|
|
503
|
+
` Code: ${start.userCode}`,
|
|
504
|
+
"",
|
|
505
|
+
"Approve in your browser — clai will continue automatically.",
|
|
506
|
+
"(close this and press Ctrl-C to cancel)",
|
|
507
|
+
].join("\n"), undefined, undefined, "plain");
|
|
508
|
+
const waiting = services.toast.info("waiting for Github Copilot approval…", {
|
|
509
|
+
sticky: true,
|
|
510
|
+
});
|
|
511
|
+
try {
|
|
512
|
+
const token = await pollCopilotDeviceAuth(start);
|
|
513
|
+
services.toast.dismiss(waiting);
|
|
514
|
+
notice(services, "info", "Github Copilot authenticated");
|
|
515
|
+
return token;
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
services.toast.dismiss(waiting);
|
|
519
|
+
notice(services, "warn", `Github Copilot sign-in failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function loadClineKeys() {
|
|
524
|
+
const multi = await getProviderKeys("cline");
|
|
525
|
+
return {
|
|
526
|
+
keys: multi.source === "env" ? [] : multi.keys,
|
|
527
|
+
activeIndex: multi.source === "env" ? 0 : multi.activeIndex,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
async function storeClineAccount(services, tokens) {
|
|
531
|
+
if (tokens.refreshToken || tokens.expiresAt !== undefined) {
|
|
532
|
+
await appendProviderKey("cline", tokens.accessToken, {
|
|
533
|
+
...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}),
|
|
534
|
+
...(tokens.expiresAt !== undefined ? { expiresAt: tokens.expiresAt } : {}),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
await appendProviderKey("cline", tokens.accessToken);
|
|
539
|
+
}
|
|
540
|
+
notice(services, "info", `added Cline account ${maskSecret(tokens.accessToken)}`);
|
|
541
|
+
}
|
|
542
|
+
async function refreshClineAccount(services, oldValue) {
|
|
543
|
+
const tokens = await runClineAuthForUI(services);
|
|
544
|
+
if (!tokens) {
|
|
545
|
+
notice(services, "info", "cancelled");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const replaced = await replaceProviderKey("cline", oldValue, tokens.accessToken, {
|
|
549
|
+
...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}),
|
|
550
|
+
...(tokens.expiresAt !== undefined ? { expiresAt: tokens.expiresAt } : {}),
|
|
551
|
+
});
|
|
552
|
+
if (!replaced) {
|
|
553
|
+
notice(services, "warn", "Cline account was not found");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
notice(services, "info", `refreshed Cline account ${maskSecret(tokens.accessToken)}`);
|
|
557
|
+
}
|
|
558
|
+
async function saveClineKeys(services, answer, keys, activeIndex) {
|
|
559
|
+
const byId = new Map(keys.map((key) => [key.id, key.value]));
|
|
560
|
+
const detailed = resolveEditorRowsDetailed(answer.rows, byId);
|
|
561
|
+
const resolved = detailed.map((row) => row.value);
|
|
562
|
+
if (resolved.length === 0) {
|
|
563
|
+
await unsetProviderSecret("cline");
|
|
564
|
+
notice(services, "info", "unset all keys for cline");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
for (const key of resolved) {
|
|
568
|
+
if (!getProvider("cline").validateKey(key)) {
|
|
569
|
+
notice(services, "warn", "invalid Cline token");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (resolved.length > MAX_PROVIDER_KEYS) {
|
|
574
|
+
notice(services, "warn", `at most ${MAX_PROVIDER_KEYS} Cline accounts`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
await setProviderKeys("cline", resolved, answer.activeIndex ?? activeIndex, detailed.filter((row) => row.disabled).map((row) => row.value));
|
|
578
|
+
const index = answer.activeIndex ?? activeIndex;
|
|
579
|
+
notice(services, "info", resolved.length === 1
|
|
580
|
+
? `saved cline · ${maskSecret(resolved[0])}`
|
|
581
|
+
: `saved cline · ${resolved.length} accounts · active: #${index + 1}`);
|
|
582
|
+
}
|
|
583
|
+
async function openClineKeysFlow(services) {
|
|
584
|
+
let { keys, activeIndex } = await loadClineKeys();
|
|
585
|
+
for (;;) {
|
|
586
|
+
services.overlay.close();
|
|
587
|
+
const answer = await services.overlay.openKeysEditor({
|
|
588
|
+
provider: "cline",
|
|
589
|
+
heading: "CLINE ACCOUNTS",
|
|
590
|
+
itemLabel: "account",
|
|
591
|
+
addViaPicker: true,
|
|
592
|
+
refreshable: true,
|
|
593
|
+
initialKeys: keys.map((key) => ({
|
|
594
|
+
id: key.id,
|
|
595
|
+
masked: maskSecret(key.value),
|
|
596
|
+
disabled: key.disabled === true,
|
|
597
|
+
})),
|
|
598
|
+
activeIndex,
|
|
599
|
+
});
|
|
600
|
+
if (!answer) {
|
|
601
|
+
notice(services, "info", "cancelled");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (answer.action === "reset") {
|
|
605
|
+
await unsetProviderSecret("cline");
|
|
606
|
+
notice(services, "info", "unset all keys for cline");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (answer.action === "pick") {
|
|
610
|
+
const tokens = await clineAddAccount(services);
|
|
611
|
+
if (tokens)
|
|
612
|
+
await storeClineAccount(services, tokens);
|
|
613
|
+
({ keys, activeIndex } = await loadClineKeys());
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (answer.action === "refresh") {
|
|
617
|
+
const selected = keys.find((key) => key.id === answer.slotId);
|
|
618
|
+
if (selected) {
|
|
619
|
+
await refreshClineAccount(services, selected.value);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
notice(services, "warn", "Cline account was not found");
|
|
623
|
+
}
|
|
624
|
+
({ keys, activeIndex } = await loadClineKeys());
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
await saveClineKeys(services, answer, keys, activeIndex);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function loadOAuthKeys(provider) {
|
|
632
|
+
const multi = await getProviderKeys(provider);
|
|
633
|
+
return {
|
|
634
|
+
keys: multi.source === "env" ? [] : multi.keys,
|
|
635
|
+
activeIndex: multi.source === "env" ? 0 : multi.activeIndex,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
async function saveOAuthKeys(services, provider, answer, keys, activeIndex) {
|
|
639
|
+
const label = getProvider(provider).displayName;
|
|
640
|
+
const byId = new Map(keys.map((key) => [key.id, key.value]));
|
|
641
|
+
const detailed = resolveEditorRowsDetailed(answer.rows, byId);
|
|
642
|
+
const resolved = detailed.map((row) => row.value);
|
|
643
|
+
if (resolved.length === 0) {
|
|
644
|
+
await unsetProviderSecret(provider);
|
|
645
|
+
notice(services, "info", `unset all keys for ${label}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
for (const key of resolved) {
|
|
649
|
+
if (!getProvider(provider).validateKey(key)) {
|
|
650
|
+
notice(services, "warn", `invalid ${label} token`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (resolved.length > MAX_PROVIDER_KEYS) {
|
|
655
|
+
notice(services, "warn", `at most ${MAX_PROVIDER_KEYS} ${label} accounts`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
await setProviderKeys(provider, resolved, answer.activeIndex ?? activeIndex, detailed.filter((row) => row.disabled).map((row) => row.value));
|
|
659
|
+
const index = answer.activeIndex ?? activeIndex;
|
|
660
|
+
notice(services, "info", resolved.length === 1
|
|
661
|
+
? `saved ${label} · ${maskSecret(resolved[0])}`
|
|
662
|
+
: `saved ${label} · ${resolved.length} accounts · active: #${index + 1}`);
|
|
663
|
+
}
|
|
664
|
+
async function openCodexKeysFlow(services) {
|
|
665
|
+
let { keys, activeIndex } = await loadOAuthKeys("codex");
|
|
666
|
+
for (;;) {
|
|
667
|
+
services.overlay.close();
|
|
668
|
+
const answer = await services.overlay.openKeysEditor({
|
|
669
|
+
provider: "codex",
|
|
670
|
+
heading: "CHATGPT SUBSCRIPTION ACCOUNTS",
|
|
671
|
+
itemLabel: "account",
|
|
672
|
+
addViaPicker: true,
|
|
673
|
+
refreshable: true,
|
|
674
|
+
initialKeys: keys.map((key) => ({
|
|
675
|
+
id: key.id,
|
|
676
|
+
masked: maskSecret(key.value),
|
|
677
|
+
disabled: key.disabled === true,
|
|
678
|
+
})),
|
|
679
|
+
activeIndex,
|
|
680
|
+
});
|
|
681
|
+
if (!answer) {
|
|
682
|
+
notice(services, "info", "cancelled");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (answer.action === "reset") {
|
|
686
|
+
await unsetProviderSecret("codex");
|
|
687
|
+
notice(services, "info", "unset all keys for Chatgpt Subscription");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (answer.action === "pick") {
|
|
691
|
+
const credential = await runCodexAuthForUI(services);
|
|
692
|
+
const manualKey = credential?.manualKey;
|
|
693
|
+
if (credential) {
|
|
694
|
+
const key = manualKey ?? encodeCodexKey(credential);
|
|
695
|
+
await appendProviderKey("codex", key);
|
|
696
|
+
notice(services, "info", `added ChatGPT Subscription account ${maskSecret(key)}`);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
notice(services, "info", "cancelled");
|
|
700
|
+
}
|
|
701
|
+
({ keys, activeIndex } = await loadOAuthKeys("codex"));
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (answer.action === "refresh") {
|
|
705
|
+
const selected = keys.find((key) => key.id === answer.slotId);
|
|
706
|
+
if (selected) {
|
|
707
|
+
const credential = await runCodexAuthForUI(services);
|
|
708
|
+
const manualKey = credential?.manualKey;
|
|
709
|
+
if (credential) {
|
|
710
|
+
const key = manualKey ?? encodeCodexKey(credential);
|
|
711
|
+
const replaced = await replaceProviderKey("codex", selected.value, key);
|
|
712
|
+
if (!replaced) {
|
|
713
|
+
notice(services, "warn", "ChatGPT Subscription account was not found");
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
notice(services, "info", `refreshed ChatGPT Subscription account ${maskSecret(key)}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
notice(services, "info", "cancelled");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
notice(services, "warn", "ChatGPT Subscription account was not found");
|
|
725
|
+
}
|
|
726
|
+
({ keys, activeIndex } = await loadOAuthKeys("codex"));
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
await saveOAuthKeys(services, "codex", answer, keys, activeIndex);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function openCopilotKeysFlow(services) {
|
|
734
|
+
let { keys, activeIndex } = await loadOAuthKeys("copilot");
|
|
735
|
+
for (;;) {
|
|
736
|
+
services.overlay.close();
|
|
737
|
+
const answer = await services.overlay.openKeysEditor({
|
|
738
|
+
provider: "copilot",
|
|
739
|
+
heading: "GITHUB COPILOT ACCOUNTS",
|
|
740
|
+
itemLabel: "account",
|
|
741
|
+
addViaPicker: true,
|
|
742
|
+
refreshable: true,
|
|
743
|
+
initialKeys: keys.map((key) => ({
|
|
744
|
+
id: key.id,
|
|
745
|
+
masked: maskSecret(key.value),
|
|
746
|
+
disabled: key.disabled === true,
|
|
747
|
+
})),
|
|
748
|
+
activeIndex,
|
|
749
|
+
});
|
|
750
|
+
if (!answer) {
|
|
751
|
+
notice(services, "info", "cancelled");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (answer.action === "reset") {
|
|
755
|
+
await unsetProviderSecret("copilot");
|
|
756
|
+
notice(services, "info", "unset all keys for Github Copilot");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (answer.action === "pick") {
|
|
760
|
+
const token = await runCopilotAuthForUI(services);
|
|
761
|
+
if (token) {
|
|
762
|
+
await appendProviderKey("copilot", token);
|
|
763
|
+
notice(services, "info", `added Github Copilot account ${maskSecret(token)}`);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
notice(services, "info", "cancelled");
|
|
767
|
+
}
|
|
768
|
+
({ keys, activeIndex } = await loadOAuthKeys("copilot"));
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (answer.action === "refresh") {
|
|
772
|
+
const selected = keys.find((key) => key.id === answer.slotId);
|
|
773
|
+
if (selected) {
|
|
774
|
+
const token = await runCopilotAuthForUI(services);
|
|
775
|
+
if (token) {
|
|
776
|
+
const replaced = await replaceProviderKey("copilot", selected.value, token);
|
|
777
|
+
if (!replaced) {
|
|
778
|
+
notice(services, "warn", "Github Copilot account was not found");
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
notice(services, "info", `refreshed Github Copilot account ${maskSecret(token)}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
notice(services, "info", "cancelled");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
notice(services, "warn", "Github Copilot account was not found");
|
|
790
|
+
}
|
|
791
|
+
({ keys, activeIndex } = await loadOAuthKeys("copilot"));
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
await saveOAuthKeys(services, "copilot", answer, keys, activeIndex);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
320
797
|
}
|
|
321
798
|
export async function openLlmKeysEditor(services, id) {
|
|
322
799
|
if (id === "ollama") {
|
|
@@ -333,6 +810,18 @@ export async function openLlmKeysEditor(services, id) {
|
|
|
333
810
|
notice(services, "info", `saved ollama host → ${host.trim()}`);
|
|
334
811
|
return;
|
|
335
812
|
}
|
|
813
|
+
if (id === "cline") {
|
|
814
|
+
await openClineKeysFlow(services);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (id === "codex") {
|
|
818
|
+
await openCodexKeysFlow(services);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (id === "copilot") {
|
|
822
|
+
await openCopilotKeysFlow(services);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
336
825
|
if (providerUsesEndpoints(id)) {
|
|
337
826
|
await openEndpointsEditor(services, id);
|
|
338
827
|
}
|
|
@@ -359,6 +848,8 @@ export async function openLlmKeysEditor(services, id) {
|
|
|
359
848
|
notice(services, "info", `unset all keys for ${id}`);
|
|
360
849
|
return;
|
|
361
850
|
}
|
|
851
|
+
if (answer.action === "refresh")
|
|
852
|
+
return;
|
|
362
853
|
const byId = new Map(stored.map((key) => [key.id, key.value]));
|
|
363
854
|
const detailed = resolveEditorRowsDetailed(answer.rows, byId);
|
|
364
855
|
const resolved = detailed.map((row) => row.value);
|