@mars-sea/dsh-commandcode-provider 0.10.3 → 0.10.4
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/CHANGELOG.md +22 -0
- package/README.md +42 -0
- package/README.zh-CN.md +42 -0
- package/lib/client.js +115 -28
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +174 -3
- package/lib/index.js +421 -20
- package/lib/index.js.map +1 -1
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -220,16 +220,38 @@ var CommandCodeAccountPool = class {
|
|
|
220
220
|
until: 0
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* One account's key: literal → credential seam → auth file (default slot).
|
|
225
|
+
*
|
|
226
|
+
* Every source is normalized here, at the single point where a slot's key
|
|
227
|
+
* enters the pool. The adapter sends the key through the harness's
|
|
228
|
+
* `assertUsableApiKey()`, which trims it — a stored key from the credentials
|
|
229
|
+
* seam, a `.env` line, or a shell export all pick up surrounding whitespace
|
|
230
|
+
* — and reports that trimmed form back to `markRejected()`. Returning the
|
|
231
|
+
* raw value would file every 429/401 mark under a key no later lookup can
|
|
232
|
+
* find: rotation would re-offer the same account, the account card would show
|
|
233
|
+
* no mark, and the usage endpoints would 401 while chat kept working.
|
|
234
|
+
* Normalizing once makes resolution, probing, marking, and the request path
|
|
235
|
+
* agree on one string.
|
|
236
|
+
*/
|
|
224
237
|
async resolveSlotKey(slot) {
|
|
225
|
-
if (slot.literal !== void 0
|
|
238
|
+
if (slot.literal !== void 0) {
|
|
239
|
+
const literal = normalizeResolvedKey(slot.literal);
|
|
240
|
+
if (literal !== void 0) return literal;
|
|
241
|
+
}
|
|
226
242
|
if (slot.ref !== void 0) {
|
|
227
243
|
const hit = await this.deps.resolveRef(slot.ref);
|
|
228
|
-
if (hit !== void 0
|
|
244
|
+
if (hit !== void 0) {
|
|
245
|
+
const resolved = normalizeResolvedKey(hit);
|
|
246
|
+
if (resolved !== void 0) return resolved;
|
|
247
|
+
}
|
|
229
248
|
}
|
|
230
249
|
if (slot.allowAuthFile) {
|
|
231
250
|
const fromFile = this.deps.authFileKey();
|
|
232
|
-
if (fromFile !== void 0
|
|
251
|
+
if (fromFile !== void 0) {
|
|
252
|
+
const fileKey = normalizeResolvedKey(fromFile);
|
|
253
|
+
if (fileKey !== void 0) return fileKey;
|
|
254
|
+
}
|
|
233
255
|
}
|
|
234
256
|
}
|
|
235
257
|
/** Hand out the chosen account's key. */
|
|
@@ -240,6 +262,16 @@ var CommandCodeAccountPool = class {
|
|
|
240
262
|
};
|
|
241
263
|
}
|
|
242
264
|
};
|
|
265
|
+
/**
|
|
266
|
+
* Normalize one resolved credential to the form every consumer sees. Trim
|
|
267
|
+
* only: a blank-after-trim value means "no key" (the slot is omitted and the
|
|
268
|
+
* caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
|
|
269
|
+
* carry are left for `assertUsableApiKey()` to reject with its own message.
|
|
270
|
+
*/
|
|
271
|
+
function normalizeResolvedKey(value) {
|
|
272
|
+
const trimmed = value.trim();
|
|
273
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
274
|
+
}
|
|
243
275
|
//#endregion
|
|
244
276
|
//#region src/capabilities.ts
|
|
245
277
|
/**
|
|
@@ -1207,8 +1239,65 @@ function wireToolCallIds(paired) {
|
|
|
1207
1239
|
function blockText(block) {
|
|
1208
1240
|
return block.type === "text" || block.type === "reasoning" ? block.text : "";
|
|
1209
1241
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* Collect text and nested images of one tool result. The harness `read_image`
|
|
1244
|
+
* tool returns both, so dropping the image half is what made a tool-read image
|
|
1245
|
+
* invisible to a Vision model (issue #30). Deduplication keeps a result that
|
|
1246
|
+
* repeats one attachment from paying for the same pixels twice.
|
|
1247
|
+
*/
|
|
1248
|
+
function toolResultMedia(block) {
|
|
1249
|
+
const chunks = [];
|
|
1250
|
+
const images = [];
|
|
1251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1252
|
+
const walk = (blocks) => {
|
|
1253
|
+
for (const nested of blocks) {
|
|
1254
|
+
if (nested.type === "text" || nested.type === "reasoning") {
|
|
1255
|
+
if (nested.text) chunks.push(nested.text);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (nested.type === "image") {
|
|
1259
|
+
if (!seen.has(nested.attachment.attachmentId)) {
|
|
1260
|
+
seen.add(nested.attachment.attachmentId);
|
|
1261
|
+
images.push(nested.attachment);
|
|
1262
|
+
}
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (nested.type === "tool-result") walk(nested.content);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
walk(block.content);
|
|
1269
|
+
return {
|
|
1270
|
+
text: chunks.join("\n"),
|
|
1271
|
+
images
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Result text for the wire's `role: 'tool'` message. A tool that returned only
|
|
1276
|
+
* an image (and possibly a nested image-only tool result) has no text at all,
|
|
1277
|
+
* and neither transport accepts an empty tool content string, so it gets a
|
|
1278
|
+
* descriptor pointing at the user message that follows with the pixels.
|
|
1279
|
+
*/
|
|
1280
|
+
function toolResultTextForWire(media) {
|
|
1281
|
+
return media.text || (media.images.length > 0 ? "(image returned; see the attached image)" : "");
|
|
1282
|
+
}
|
|
1283
|
+
/** Leading line of the user message that carries a tool result's images. */
|
|
1284
|
+
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
|
|
1285
|
+
/**
|
|
1286
|
+
* The model-visible note introducing the images carried out of one tool
|
|
1287
|
+
* result. Neither transport merges them back into the tool result, so this
|
|
1288
|
+
* line is what tells the model the image it is about to see belongs to the
|
|
1289
|
+
* tool it just ran rather than to the user; it also leads the part list,
|
|
1290
|
+
* because an image-first content array is what some gateways reject. Count and
|
|
1291
|
+
* pixel dimensions are appended when the tool's own text does not already
|
|
1292
|
+
* state them (a result that returns the image and nothing else).
|
|
1293
|
+
*/
|
|
1294
|
+
function toolResultImageNote(media) {
|
|
1295
|
+
const first = media.images[0];
|
|
1296
|
+
if (first === void 0) return TOOL_RESULT_IMAGE_TEXT;
|
|
1297
|
+
const dimensions = `${first.width}x${first.height} px`;
|
|
1298
|
+
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return TOOL_RESULT_IMAGE_TEXT;
|
|
1299
|
+
const count = media.images.length > 1 ? `${media.images.length} images, ` : "";
|
|
1300
|
+
return `${TOOL_RESULT_IMAGE_TEXT} ${count}${dimensions}`;
|
|
1212
1301
|
}
|
|
1213
1302
|
function hasImageContent(message) {
|
|
1214
1303
|
const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
|
|
@@ -1276,6 +1365,7 @@ async function messagesToCC(messages, readImage) {
|
|
|
1276
1365
|
if (message.role === "user" && message.source.kind === "tool") {
|
|
1277
1366
|
const block = message.content[0];
|
|
1278
1367
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1368
|
+
const media = toolResultMedia(block);
|
|
1279
1369
|
out.push({
|
|
1280
1370
|
role: "tool",
|
|
1281
1371
|
content: [{
|
|
@@ -1284,13 +1374,25 @@ async function messagesToCC(messages, readImage) {
|
|
|
1284
1374
|
toolName: toolNames.get(block.toolCallId) || "unknown",
|
|
1285
1375
|
output: block.isError ? {
|
|
1286
1376
|
type: "error-text",
|
|
1287
|
-
value:
|
|
1377
|
+
value: toolResultTextForWire(media)
|
|
1288
1378
|
} : {
|
|
1289
1379
|
type: "text",
|
|
1290
|
-
value:
|
|
1380
|
+
value: toolResultTextForWire(media)
|
|
1291
1381
|
}
|
|
1292
1382
|
}]
|
|
1293
1383
|
});
|
|
1384
|
+
if (media.images.length > 0) {
|
|
1385
|
+
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1386
|
+
const carried = [{
|
|
1387
|
+
type: "text",
|
|
1388
|
+
text: toolResultImageNote(media)
|
|
1389
|
+
}];
|
|
1390
|
+
for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
|
|
1391
|
+
out.push({
|
|
1392
|
+
role: "user",
|
|
1393
|
+
content: carried
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1294
1396
|
}
|
|
1295
1397
|
}
|
|
1296
1398
|
return out;
|
|
@@ -1369,11 +1471,24 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1369
1471
|
if (message.role === "user" && message.source.kind === "tool") {
|
|
1370
1472
|
const block = message.content[0];
|
|
1371
1473
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1474
|
+
const media = toolResultMedia(block);
|
|
1372
1475
|
out.push({
|
|
1373
1476
|
role: "tool",
|
|
1374
1477
|
tool_call_id: wireIds.get(block.toolCallId) ?? block.toolCallId,
|
|
1375
|
-
content:
|
|
1478
|
+
content: toolResultTextForWire(media)
|
|
1376
1479
|
});
|
|
1480
|
+
if (media.images.length > 0) {
|
|
1481
|
+
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1482
|
+
const carried = [{
|
|
1483
|
+
type: "text",
|
|
1484
|
+
text: toolResultImageNote(media)
|
|
1485
|
+
}];
|
|
1486
|
+
for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
|
|
1487
|
+
out.push({
|
|
1488
|
+
role: "user",
|
|
1489
|
+
content: carried
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1377
1492
|
}
|
|
1378
1493
|
}
|
|
1379
1494
|
return out;
|
|
@@ -3122,6 +3237,14 @@ var CommandCodeLoginFlow = class {
|
|
|
3122
3237
|
timer;
|
|
3123
3238
|
/** Settle hooks of the live attempt's callback promise. */
|
|
3124
3239
|
settle;
|
|
3240
|
+
/**
|
|
3241
|
+
* Attempt generation. A delivered callback keeps validating the key
|
|
3242
|
+
* asynchronously (`complete()`), and that window is open to a cancel or a
|
|
3243
|
+
* fresh `begin()`; the generation lets a late completion recognize that it
|
|
3244
|
+
* no longer owns the status face and stop instead of storing a credential
|
|
3245
|
+
* the user cancelled and flipping the page back to success.
|
|
3246
|
+
*/
|
|
3247
|
+
attemptSeq = 0;
|
|
3125
3248
|
disposed = false;
|
|
3126
3249
|
constructor(deps) {
|
|
3127
3250
|
this.deps = deps;
|
|
@@ -3142,8 +3265,9 @@ var CommandCodeLoginFlow = class {
|
|
|
3142
3265
|
*/
|
|
3143
3266
|
async begin() {
|
|
3144
3267
|
if (this.disposed) throw new Error("login flow has been disposed");
|
|
3145
|
-
if (this.statusValue.state === "waiting") return this.statusValue;
|
|
3268
|
+
if (this.statusValue.state === "waiting" && this.server !== void 0) return this.statusValue;
|
|
3146
3269
|
this.teardown();
|
|
3270
|
+
const attempt = ++this.attemptSeq;
|
|
3147
3271
|
const port = await this.findPort();
|
|
3148
3272
|
const expectedState = this.deps.randomToken?.(32) ?? randomBytes(32).toString("base64url");
|
|
3149
3273
|
const settled = new Promise((resolve, reject) => {
|
|
@@ -3171,7 +3295,7 @@ var CommandCodeLoginFlow = class {
|
|
|
3171
3295
|
});
|
|
3172
3296
|
}, this.deps.timeoutMs ?? 12e4);
|
|
3173
3297
|
this.timer.unref?.();
|
|
3174
|
-
settled.then((credentials) => this.complete(credentials), (failure) => this.failFrom(failure));
|
|
3298
|
+
settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
|
|
3175
3299
|
return this.statusValue;
|
|
3176
3300
|
}
|
|
3177
3301
|
/** Cancel a waiting attempt; terminal states are untouched. */
|
|
@@ -3288,6 +3412,13 @@ var CommandCodeLoginFlow = class {
|
|
|
3288
3412
|
}
|
|
3289
3413
|
if (typeof payload === "object" && payload !== null && "error" in payload) {
|
|
3290
3414
|
const denial = payload;
|
|
3415
|
+
if (denial.state !== expectedState) {
|
|
3416
|
+
json(403, {
|
|
3417
|
+
success: false,
|
|
3418
|
+
error: "Invalid state token"
|
|
3419
|
+
});
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3291
3422
|
const description = denial.error_description ?? denial.error;
|
|
3292
3423
|
this.settleAttempt(json, 200, { success: true }, new LoginSettleError(denial.error === "access_denied" ? "denied" : "error", typeof description === "string" && description !== "" ? description : "Authorization failed"));
|
|
3293
3424
|
return;
|
|
@@ -3319,10 +3450,20 @@ var CommandCodeLoginFlow = class {
|
|
|
3319
3450
|
if (failure !== void 0) settle.reject(failure);
|
|
3320
3451
|
else if (credentials !== void 0) settle.resolve(credentials);
|
|
3321
3452
|
}
|
|
3322
|
-
/**
|
|
3323
|
-
|
|
3324
|
-
|
|
3453
|
+
/**
|
|
3454
|
+
* Post-validation completion: whoami check, then hand-off to storage.
|
|
3455
|
+
*
|
|
3456
|
+
* Every step re-checks {@link ownsAttempt} first: the whoami round-trip and
|
|
3457
|
+
* the credential write are awaits, and the user may cancel (or start another
|
|
3458
|
+
* attempt) while one is in flight. A completion that no longer owns the
|
|
3459
|
+
* attempt must not write the key or publish a status — otherwise cancel
|
|
3460
|
+
* would report "cancelled" while the credential landed anyway, and the page
|
|
3461
|
+
* would silently flip to success.
|
|
3462
|
+
*/
|
|
3463
|
+
async complete(attempt, credentials) {
|
|
3464
|
+
if (!this.ownsAttempt(attempt)) return;
|
|
3325
3465
|
const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey);
|
|
3466
|
+
if (!this.ownsAttempt(attempt)) return;
|
|
3326
3467
|
if (!validation.valid) {
|
|
3327
3468
|
const reason = validation.error === "invalid_key" ? "invalid-key" : validation.error === "network_error" ? "network" : "error";
|
|
3328
3469
|
this.setStatus({
|
|
@@ -3332,9 +3473,11 @@ var CommandCodeLoginFlow = class {
|
|
|
3332
3473
|
});
|
|
3333
3474
|
return;
|
|
3334
3475
|
}
|
|
3476
|
+
if (!this.ownsAttempt(attempt)) return;
|
|
3335
3477
|
try {
|
|
3336
3478
|
await this.deps.storeKey(credentials);
|
|
3337
3479
|
} catch (error) {
|
|
3480
|
+
if (!this.ownsAttempt(attempt)) return;
|
|
3338
3481
|
this.setStatus({
|
|
3339
3482
|
state: "failed",
|
|
3340
3483
|
reason: "unavailable",
|
|
@@ -3342,7 +3485,7 @@ var CommandCodeLoginFlow = class {
|
|
|
3342
3485
|
});
|
|
3343
3486
|
return;
|
|
3344
3487
|
}
|
|
3345
|
-
if (this.
|
|
3488
|
+
if (!this.ownsAttempt(attempt)) return;
|
|
3346
3489
|
this.clearTimer();
|
|
3347
3490
|
this.setStatus({
|
|
3348
3491
|
state: "success",
|
|
@@ -3350,10 +3493,14 @@ var CommandCodeLoginFlow = class {
|
|
|
3350
3493
|
keyName: credentials.keyName
|
|
3351
3494
|
});
|
|
3352
3495
|
}
|
|
3496
|
+
/** Whether one attempt still owns the status face (not cancelled, replaced, or disposed). */
|
|
3497
|
+
ownsAttempt(attempt) {
|
|
3498
|
+
return !this.disposed && this.attemptSeq === attempt && this.statusValue.state === "waiting";
|
|
3499
|
+
}
|
|
3353
3500
|
/** Map a tagged settle rejection onto the status face. */
|
|
3354
|
-
failFrom(failure) {
|
|
3501
|
+
failFrom(attempt, failure) {
|
|
3355
3502
|
if (!(failure instanceof LoginSettleError)) return;
|
|
3356
|
-
if (this.
|
|
3503
|
+
if (!this.ownsAttempt(attempt)) return;
|
|
3357
3504
|
this.setStatus({
|
|
3358
3505
|
state: "failed",
|
|
3359
3506
|
reason: failure.reason,
|
|
@@ -3634,6 +3781,244 @@ var CommandCodeSearchProvider = class {
|
|
|
3634
3781
|
}
|
|
3635
3782
|
};
|
|
3636
3783
|
//#endregion
|
|
3784
|
+
//#region src/tui-settings.ts
|
|
3785
|
+
/** The selector value meaning "no pinned account — follow rotation order". */
|
|
3786
|
+
const ACTIVE_ACCOUNT_AUTO = "auto";
|
|
3787
|
+
/** The selector value meaning "no language override — follow the shell locale". */
|
|
3788
|
+
const LANG_AUTO = "auto";
|
|
3789
|
+
/**
|
|
3790
|
+
* Build the section descriptor. Pure, so tests can pin the exact fields
|
|
3791
|
+
* without a dsh-TUI host.
|
|
3792
|
+
*
|
|
3793
|
+
* Field choices worth keeping: the two option-bearing fields (`activeAccount`,
|
|
3794
|
+
* `lang`) are `text` + `options` rather than `select`, because a `select`
|
|
3795
|
+
* cannot express "unset" — cycling only ever lands on a declared option, so a
|
|
3796
|
+
* `select` would strand the user on a pinned value with no way back to
|
|
3797
|
+
* automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
|
|
3798
|
+
* unset state reachable. The two booleans format their EFFECTIVE default
|
|
3799
|
+
* (`filterModelsByPlan` unset means true at the adapter), so a fresh install
|
|
3800
|
+
* reads true instead of the screen's "(empty)".
|
|
3801
|
+
*/
|
|
3802
|
+
function buildCommandCodeTuiSection(deps) {
|
|
3803
|
+
const ref = deps.apiKeyRef();
|
|
3804
|
+
const slots = deps.accountSlots();
|
|
3805
|
+
return {
|
|
3806
|
+
ns: deps.ns,
|
|
3807
|
+
title: deps.title ?? "Command Code",
|
|
3808
|
+
descriptions: { zh: "Command Code(非官方)" },
|
|
3809
|
+
groups: [
|
|
3810
|
+
{
|
|
3811
|
+
id: "connection",
|
|
3812
|
+
title: "Connection",
|
|
3813
|
+
descriptions: { zh: "连接" }
|
|
3814
|
+
},
|
|
3815
|
+
{
|
|
3816
|
+
id: "models",
|
|
3817
|
+
title: "Models",
|
|
3818
|
+
descriptions: { zh: "模型" }
|
|
3819
|
+
},
|
|
3820
|
+
{
|
|
3821
|
+
id: "advanced",
|
|
3822
|
+
title: "Advanced",
|
|
3823
|
+
descriptions: { zh: "高级" }
|
|
3824
|
+
}
|
|
3825
|
+
],
|
|
3826
|
+
fields: [
|
|
3827
|
+
{
|
|
3828
|
+
path: ["apiKey"],
|
|
3829
|
+
group: "connection",
|
|
3830
|
+
kind: "text",
|
|
3831
|
+
label: "API key",
|
|
3832
|
+
descriptions: { zh: "API 密钥" },
|
|
3833
|
+
secret: { ref },
|
|
3834
|
+
hint: `Stored in the credential store as ${ref}, never in settings.yaml.`,
|
|
3835
|
+
hintDescriptions: { zh: `保存在凭据库(${ref}),不会写入 settings.yaml。` }
|
|
3836
|
+
},
|
|
3837
|
+
{
|
|
3838
|
+
path: ["apiBase"],
|
|
3839
|
+
group: "connection",
|
|
3840
|
+
kind: "text",
|
|
3841
|
+
label: "API base",
|
|
3842
|
+
descriptions: { zh: "API 地址" },
|
|
3843
|
+
placeholder: "https://api.commandcode.ai",
|
|
3844
|
+
hint: "Leave empty for the public Command Code Provider API.",
|
|
3845
|
+
hintDescriptions: { zh: "留空即使用官方 Command Code Provider API。" },
|
|
3846
|
+
parse: (text) => {
|
|
3847
|
+
const trimmed = text.trim();
|
|
3848
|
+
return trimmed === "" ? { kind: "clear" } : {
|
|
3849
|
+
kind: "set",
|
|
3850
|
+
value: trimmed
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
},
|
|
3854
|
+
{
|
|
3855
|
+
path: ["filterModelsByPlan"],
|
|
3856
|
+
group: "models",
|
|
3857
|
+
kind: "boolean",
|
|
3858
|
+
label: "Hide out-of-plan models",
|
|
3859
|
+
descriptions: { zh: "隐藏套餐外的模型" },
|
|
3860
|
+
hint: "Keeps models above your subscription tier out of the picker. Fails open.",
|
|
3861
|
+
hintDescriptions: { zh: "在选择器里隐藏超出当前订阅档位的模型;判断不出来时全部显示。" },
|
|
3862
|
+
format: (value) => value === false ? "false" : "true",
|
|
3863
|
+
parse: (text) => ({
|
|
3864
|
+
kind: "set",
|
|
3865
|
+
value: text.trim() === "true"
|
|
3866
|
+
})
|
|
3867
|
+
},
|
|
3868
|
+
{
|
|
3869
|
+
path: ["visibleModels"],
|
|
3870
|
+
group: "models",
|
|
3871
|
+
kind: "text",
|
|
3872
|
+
label: "Visible models",
|
|
3873
|
+
descriptions: { zh: "显示的模型" },
|
|
3874
|
+
hint: "Comma-separated catalog ids; empty shows every model.",
|
|
3875
|
+
hintDescriptions: { zh: "以逗号分隔的模型 id;留空表示显示全部模型。" },
|
|
3876
|
+
format: (value) => (Array.isArray(value) ? value.filter((id) => typeof id === "string" && id !== "") : []).join(", "),
|
|
3877
|
+
parse: (text) => {
|
|
3878
|
+
const ids = text.split(",").map((id) => id.trim()).filter((id) => id !== "");
|
|
3879
|
+
return ids.length === 0 ? { kind: "clear" } : {
|
|
3880
|
+
kind: "set",
|
|
3881
|
+
value: ids
|
|
3882
|
+
};
|
|
3883
|
+
}
|
|
3884
|
+
},
|
|
3885
|
+
{
|
|
3886
|
+
path: ["activeAccount"],
|
|
3887
|
+
group: "advanced",
|
|
3888
|
+
kind: "text",
|
|
3889
|
+
label: "Active account",
|
|
3890
|
+
descriptions: { zh: "当前账号" },
|
|
3891
|
+
hint: "A pinned account id, or auto to follow the rotation order.",
|
|
3892
|
+
hintDescriptions: { zh: "固定使用某个账号的 id;auto 表示按轮换顺序自动选择。" },
|
|
3893
|
+
options: [{
|
|
3894
|
+
value: ACTIVE_ACCOUNT_AUTO,
|
|
3895
|
+
label: "Automatic (rotation order)",
|
|
3896
|
+
descriptions: { zh: "自动(按轮换顺序)" }
|
|
3897
|
+
}, ...slots.map((slot) => ({
|
|
3898
|
+
value: slot.id,
|
|
3899
|
+
label: slot.label,
|
|
3900
|
+
descriptions: { zh: slot.label }
|
|
3901
|
+
}))],
|
|
3902
|
+
format: (value) => typeof value === "string" && value.trim() !== "" ? value : ACTIVE_ACCOUNT_AUTO,
|
|
3903
|
+
parse: (text) => {
|
|
3904
|
+
const trimmed = text.trim();
|
|
3905
|
+
return trimmed === "" || trimmed === "auto" ? { kind: "clear" } : {
|
|
3906
|
+
kind: "set",
|
|
3907
|
+
value: trimmed
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
},
|
|
3911
|
+
{
|
|
3912
|
+
path: ["lang"],
|
|
3913
|
+
group: "advanced",
|
|
3914
|
+
kind: "text",
|
|
3915
|
+
label: "Command language",
|
|
3916
|
+
descriptions: { zh: "命令语言" },
|
|
3917
|
+
hint: "Language of the /commandcode dashboard; auto follows the shell locale.",
|
|
3918
|
+
hintDescriptions: { zh: "/commandcode 用量面板的语言;auto 跟随终端 locale。" },
|
|
3919
|
+
options: [
|
|
3920
|
+
{
|
|
3921
|
+
value: LANG_AUTO,
|
|
3922
|
+
label: "Automatic (shell locale)",
|
|
3923
|
+
descriptions: { zh: "自动(跟随终端 locale)" }
|
|
3924
|
+
},
|
|
3925
|
+
{
|
|
3926
|
+
value: "zh",
|
|
3927
|
+
label: "中文"
|
|
3928
|
+
},
|
|
3929
|
+
{
|
|
3930
|
+
value: "en",
|
|
3931
|
+
label: "English"
|
|
3932
|
+
}
|
|
3933
|
+
],
|
|
3934
|
+
format: (value) => value === "zh" || value === "en" ? value : LANG_AUTO,
|
|
3935
|
+
parse: (text) => {
|
|
3936
|
+
const trimmed = text.trim();
|
|
3937
|
+
return trimmed === "" || trimmed === "auto" ? { kind: "clear" } : {
|
|
3938
|
+
kind: "set",
|
|
3939
|
+
value: trimmed
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
]
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* The `tuiSettingsSections` service, read defensively.
|
|
3948
|
+
*
|
|
3949
|
+
* The service name is declared by dsh-TUI's own module augmentation, which
|
|
3950
|
+
* this package deliberately does not import, so the typed `Context` has no
|
|
3951
|
+
* such property. The read goes through the REFLECTIVE `ctx.get` rather than a
|
|
3952
|
+
* bare property access: cordis refuses a property read for a service the fiber
|
|
3953
|
+
* never declared in `inject` (`cannot get property … without inject`), and an
|
|
3954
|
+
* unmeet seam must degrade to "no section" instead of throwing out of the
|
|
3955
|
+
* plugin's boot.
|
|
3956
|
+
*/
|
|
3957
|
+
function tuiSettingsService(ctx) {
|
|
3958
|
+
let candidate;
|
|
3959
|
+
try {
|
|
3960
|
+
candidate = ctx.get("tuiSettingsSections");
|
|
3961
|
+
} catch {
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
3965
|
+
const register = candidate.register;
|
|
3966
|
+
if (typeof register !== "function") return void 0;
|
|
3967
|
+
return { register: register.bind(candidate) };
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Identity of everything in the section that can change at runtime: the
|
|
3971
|
+
* credential reference behind the API-key field and the account slots behind
|
|
3972
|
+
* the active-account selector. Re-registration is skipped while this matches,
|
|
3973
|
+
* so ordinary settings writes never churn the screen's section list.
|
|
3974
|
+
*/
|
|
3975
|
+
function sectionSignature(section) {
|
|
3976
|
+
const active = section.fields.find((field) => field.path.join(".") === "activeAccount");
|
|
3977
|
+
return JSON.stringify({
|
|
3978
|
+
secret: section.fields.find((field) => field.secret !== void 0)?.secret?.ref ?? "",
|
|
3979
|
+
options: active?.options?.map((option) => option.value) ?? []
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Register the Command Code section on a dsh-TUI host.
|
|
3984
|
+
*
|
|
3985
|
+
* @param ctx - the context of an activated `tuiSettingsSections` injection.
|
|
3986
|
+
* @param deps - plugin-owned facts the section reads.
|
|
3987
|
+
* @returns a refresh function that re-registers the section when a fact it
|
|
3988
|
+
* renders changed (the plugin entry calls it from its settings `onChange`
|
|
3989
|
+
* hook), or `undefined` when the seam is unusable. The returned function is
|
|
3990
|
+
* inert after the fiber is torn down.
|
|
3991
|
+
*/
|
|
3992
|
+
function applyCommandCodeTuiSettings(ctx, deps) {
|
|
3993
|
+
const service = tuiSettingsService(ctx);
|
|
3994
|
+
if (service === void 0) return void 0;
|
|
3995
|
+
let disposed = false;
|
|
3996
|
+
let current;
|
|
3997
|
+
const refresh = () => {
|
|
3998
|
+
if (disposed) return;
|
|
3999
|
+
const section = buildCommandCodeTuiSection(deps);
|
|
4000
|
+
const signature = sectionSignature(section);
|
|
4001
|
+
if (current?.signature === signature) return;
|
|
4002
|
+
current?.dispose();
|
|
4003
|
+
current = void 0;
|
|
4004
|
+
try {
|
|
4005
|
+
current = {
|
|
4006
|
+
signature,
|
|
4007
|
+
dispose: service.register(section)
|
|
4008
|
+
};
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${error instanceof Error ? error.message : String(error)}`);
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
refresh();
|
|
4014
|
+
ctx.effect(() => () => {
|
|
4015
|
+
disposed = true;
|
|
4016
|
+
current?.dispose();
|
|
4017
|
+
current = void 0;
|
|
4018
|
+
}, "dsh-commandcode-provider: tui settings section");
|
|
4019
|
+
return refresh;
|
|
4020
|
+
}
|
|
4021
|
+
//#endregion
|
|
3637
4022
|
//#region src/index.ts
|
|
3638
4023
|
/**
|
|
3639
4024
|
* dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
|
|
@@ -3672,7 +4057,7 @@ const PROVIDER = "commandcode";
|
|
|
3672
4057
|
const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
|
|
3673
4058
|
const Config = z.object({
|
|
3674
4059
|
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
3675
|
-
apiKey: z.string(),
|
|
4060
|
+
apiKey: z.string().role("secret"),
|
|
3676
4061
|
apiBase: z.string(),
|
|
3677
4062
|
workingDir: z.string(),
|
|
3678
4063
|
modelsCachePath: z.string(),
|
|
@@ -3684,7 +4069,8 @@ const Config = z.object({
|
|
|
3684
4069
|
accounts: z.array(z.object({
|
|
3685
4070
|
label: z.string(),
|
|
3686
4071
|
apiKeyEnv: z.string().role("credential-ref"),
|
|
3687
|
-
apiKey
|
|
4072
|
+
/** Literal key for one extra slot; see the top-level `apiKey` secret note. */
|
|
4073
|
+
apiKey: z.string().role("secret")
|
|
3688
4074
|
})),
|
|
3689
4075
|
activeAccount: z.string(),
|
|
3690
4076
|
modelAccountRules: z.array(z.object({
|
|
@@ -3874,6 +4260,20 @@ function apply(ctx, config) {
|
|
|
3874
4260
|
applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
|
|
3875
4261
|
}, "dsh-commandcode-provider: web search selection");
|
|
3876
4262
|
});
|
|
4263
|
+
let refreshTuiSettings;
|
|
4264
|
+
ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
|
|
4265
|
+
refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
|
|
4266
|
+
ns: NS,
|
|
4267
|
+
apiKeyRef: () => current().apiKeyEnv ?? DEFAULT_API_KEY_ENV,
|
|
4268
|
+
accountSlots: () => slots().map((slot) => ({
|
|
4269
|
+
id: slot.id,
|
|
4270
|
+
label: slot.label
|
|
4271
|
+
}))
|
|
4272
|
+
});
|
|
4273
|
+
tuiCtx.effect(() => () => {
|
|
4274
|
+
refreshTuiSettings = void 0;
|
|
4275
|
+
}, "dsh-commandcode-provider: tui settings handle");
|
|
4276
|
+
});
|
|
3877
4277
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
3878
4278
|
settingsCtx.settings.installSection(ctx, NS, Config, config, {
|
|
3879
4279
|
setSource: (source) => {
|
|
@@ -3881,11 +4281,12 @@ function apply(ctx, config) {
|
|
|
3881
4281
|
},
|
|
3882
4282
|
onChange: () => {
|
|
3883
4283
|
applySearchSelection(current().webSearch ?? true);
|
|
4284
|
+
refreshTuiSettings?.();
|
|
3884
4285
|
}
|
|
3885
4286
|
});
|
|
3886
4287
|
});
|
|
3887
4288
|
}
|
|
3888
4289
|
//#endregion
|
|
3889
|
-
export { BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommands, applyUsageRemote, buildCommandAuthUrl, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
|
|
4290
|
+
export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
|
|
3890
4291
|
|
|
3891
4292
|
//# sourceMappingURL=index.js.map
|