@jeffreyjyz/opencode-command-code 0.2.11 → 0.3.1

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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Plugin as PluginV1 } from "@opencode-ai/plugin";
2
2
  export declare const CommandCodePlugin: PluginV1;
3
+ export { createCommandCode } from "./provider";
3
4
  declare const _default: {
4
5
  /** v1 entrypoint (opencode >=1.18.29 object entrypoints). */
5
6
  server: PluginV1;
package/dist/index.js CHANGED
@@ -3054,6 +3054,654 @@ async function loadModels(key) {
3054
3054
  return { claude, open };
3055
3055
  }
3056
3056
 
3057
+ // src/provider/stream.ts
3058
+ function sseDecoder(onEvent) {
3059
+ let buffer = "";
3060
+ return (text) => {
3061
+ buffer += text;
3062
+ for (;; ) {
3063
+ const nl = buffer.indexOf(`
3064
+ `);
3065
+ if (nl < 0)
3066
+ return;
3067
+ const line = buffer.slice(0, nl).replace(/\r$/, "");
3068
+ buffer = buffer.slice(nl + 1);
3069
+ const trimmed = line.trim();
3070
+ if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:"))
3071
+ continue;
3072
+ const payload = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
3073
+ if (!payload || payload === "[DONE]")
3074
+ continue;
3075
+ try {
3076
+ const parsed = JSON.parse(payload);
3077
+ if (parsed && typeof parsed === "object")
3078
+ onEvent(parsed);
3079
+ } catch {}
3080
+ }
3081
+ };
3082
+ }
3083
+ var text = (v) => typeof v === "string" ? v : "";
3084
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
3085
+ var rec = (v) => v && typeof v === "object" ? v : {};
3086
+ var TOOL_FINISH = new Set(["tool_use", "tool_calls", "tool-call", "function_call"]);
3087
+ var LENGTH_FINISH = new Set(["length", "max_tokens", "max_output_tokens", "model_context_window_exceeded"]);
3088
+ function finishReason(raw) {
3089
+ const value = (raw ?? "unknown").toLowerCase();
3090
+ if (TOOL_FINISH.has(value))
3091
+ return { unified: "tool-calls", raw };
3092
+ if (LENGTH_FINISH.has(value))
3093
+ return { unified: "length", raw };
3094
+ if (value === "error")
3095
+ return { unified: "error", raw };
3096
+ return { unified: "stop", raw };
3097
+ }
3098
+ function usageFrom(total, noCache, cacheRead, cacheWrite, output, reasoning) {
3099
+ return {
3100
+ inputTokens: { total, noCache, cacheRead, cacheWrite },
3101
+ outputTokens: { total: output, text: Math.max(0, output - reasoning), reasoning }
3102
+ };
3103
+ }
3104
+ function anthropicUsage(raw) {
3105
+ const total = num(raw.input_tokens);
3106
+ const cacheRead = num(raw.cache_read_input_tokens);
3107
+ const cacheWrite = num(raw.cache_creation_input_tokens);
3108
+ const output = num(raw.output_tokens);
3109
+ return usageFrom(total, Math.max(0, total - cacheRead - cacheWrite), cacheRead, cacheWrite, output, 0);
3110
+ }
3111
+ function openaiUsage(raw) {
3112
+ const total = num(raw.prompt_tokens);
3113
+ const cached = num(rec(raw.prompt_tokens_details).cached_tokens);
3114
+ const output = num(raw.completion_tokens);
3115
+ const reasoning = num(rec(raw.completion_tokens_details).reasoning_tokens);
3116
+ return usageFrom(total, Math.max(0, total - cached), cached, 0, output, reasoning);
3117
+ }
3118
+ function anthropicReducer() {
3119
+ const blocks = new Map;
3120
+ let finished = false;
3121
+ let warningsEmitted = false;
3122
+ let last = { unified: "stop", raw: "unknown" };
3123
+ const open = (index, kind) => {
3124
+ const id = `${kind === "text" ? "text" : "reasoning"}-${index}`;
3125
+ blocks.set(index, { type: kind, id });
3126
+ return [{ type: kind === "text" ? "text-start" : "reasoning-start", id }];
3127
+ };
3128
+ return {
3129
+ feed(event) {
3130
+ const type = text(event.type);
3131
+ if (type === "message_start") {
3132
+ if (warningsEmitted)
3133
+ return [];
3134
+ warningsEmitted = true;
3135
+ return [{ type: "stream-start", warnings: [] }];
3136
+ }
3137
+ if (type === "content_block_start") {
3138
+ const index = num(event.index);
3139
+ const block = rec(event.content_block);
3140
+ const blockType = text(block.type);
3141
+ if (blockType === "text")
3142
+ return open(index, "text");
3143
+ if (blockType === "thinking")
3144
+ return open(index, "thinking");
3145
+ if (blockType === "tool_use") {
3146
+ const id = text(block.id) || `tool-${index}`;
3147
+ blocks.set(index, { type: "tool_use", id });
3148
+ return [{ type: "tool-input-start", id, toolName: text(block.name) }];
3149
+ }
3150
+ blocks.set(index, { type: "other", id: `block-${index}` });
3151
+ return [];
3152
+ }
3153
+ if (type === "content_block_delta") {
3154
+ const index = num(event.index);
3155
+ const delta = rec(event.delta);
3156
+ const block = blocks.get(index);
3157
+ const textDelta = text(delta.text);
3158
+ if (textDelta) {
3159
+ const parts = block?.type === "text" ? [] : open(index, "text");
3160
+ const id = blocks.get(index)?.id ?? `text-${index}`;
3161
+ return [...parts, { type: "text-delta", id, delta: textDelta }];
3162
+ }
3163
+ const thinking = text(delta.thinking);
3164
+ if (thinking) {
3165
+ const parts = block?.type === "thinking" ? [] : open(index, "thinking");
3166
+ const id = blocks.get(index)?.id ?? `reasoning-${index}`;
3167
+ return [...parts, { type: "reasoning-delta", id, delta: thinking }];
3168
+ }
3169
+ const partial = text(delta.partial_json);
3170
+ if (partial && block?.type === "tool_use") {
3171
+ return [{ type: "tool-input-delta", id: block.id, delta: partial }];
3172
+ }
3173
+ return [];
3174
+ }
3175
+ if (type === "content_block_stop") {
3176
+ const index = num(event.index);
3177
+ const block = blocks.get(index);
3178
+ if (!block)
3179
+ return [];
3180
+ blocks.delete(index);
3181
+ if (block.type === "text")
3182
+ return [{ type: "text-end", id: block.id }];
3183
+ if (block.type === "thinking")
3184
+ return [{ type: "reasoning-end", id: block.id }];
3185
+ if (block.type === "tool_use")
3186
+ return [{ type: "tool-input-end", id: block.id }];
3187
+ return [];
3188
+ }
3189
+ if (type === "message_delta") {
3190
+ const delta = rec(event.delta);
3191
+ last = finishReason(text(delta.stop_reason) || undefined);
3192
+ if (text(delta.stop_reason) === "pause_turn")
3193
+ last = { unified: "stop", raw: "pause_turn" };
3194
+ if (finished)
3195
+ return [];
3196
+ finished = true;
3197
+ return [{ type: "finish", finishReason: last, usage: anthropicUsage(rec(event.usage)) }];
3198
+ }
3199
+ if (type === "message_stop" && !finished) {
3200
+ finished = true;
3201
+ return [{ type: "finish", finishReason: last, usage: usageFrom(0, 0, 0, 0, 0, 0) }];
3202
+ }
3203
+ return [];
3204
+ },
3205
+ close() {
3206
+ const parts = [];
3207
+ for (const block of blocks.values()) {
3208
+ if (block.type === "text")
3209
+ parts.push({ type: "text-end", id: block.id });
3210
+ if (block.type === "thinking")
3211
+ parts.push({ type: "reasoning-end", id: block.id });
3212
+ if (block.type === "tool_use")
3213
+ parts.push({ type: "tool-input-end", id: block.id });
3214
+ }
3215
+ blocks.clear();
3216
+ return parts;
3217
+ }
3218
+ };
3219
+ }
3220
+ function openaiReducer() {
3221
+ const tools = new Map;
3222
+ let textId;
3223
+ let reasoningId;
3224
+ let pending;
3225
+ let usage;
3226
+ let finished = false;
3227
+ const closeText = () => {
3228
+ if (!textId)
3229
+ return [];
3230
+ const id = textId;
3231
+ textId = undefined;
3232
+ return [{ type: "text-end", id }];
3233
+ };
3234
+ const closeReasoning = () => {
3235
+ if (!reasoningId)
3236
+ return [];
3237
+ const id = reasoningId;
3238
+ reasoningId = undefined;
3239
+ return [{ type: "reasoning-end", id }];
3240
+ };
3241
+ const completeIfParsed = (buffer) => {
3242
+ if (!buffer.opened || buffer.closed)
3243
+ return [];
3244
+ try {
3245
+ JSON.parse(buffer.input);
3246
+ } catch {
3247
+ return [];
3248
+ }
3249
+ buffer.closed = true;
3250
+ return [
3251
+ { type: "tool-input-end", id: buffer.id },
3252
+ { type: "tool-call", toolCallId: buffer.id, toolName: buffer.name, input: buffer.input }
3253
+ ];
3254
+ };
3255
+ const flushTools = () => {
3256
+ const parts = [];
3257
+ for (const [index, buffer] of tools) {
3258
+ if (buffer.opened && !buffer.closed) {
3259
+ parts.push({ type: "tool-input-end", id: buffer.id });
3260
+ parts.push({ type: "tool-call", toolCallId: buffer.id, toolName: buffer.name, input: buffer.input });
3261
+ buffer.closed = true;
3262
+ }
3263
+ tools.delete(index);
3264
+ }
3265
+ return parts;
3266
+ };
3267
+ return {
3268
+ feed(event) {
3269
+ if (text(event.type) === "error" || event.error !== undefined) {
3270
+ const message = text(rec(event.error).message) || text(event.message) || "provider stream error";
3271
+ return [{ type: "error", error: new Error(message) }];
3272
+ }
3273
+ const parts = [];
3274
+ const choices = Array.isArray(event.choices) ? event.choices : [];
3275
+ const choice = rec(choices[0]);
3276
+ const delta = rec(choice.delta);
3277
+ const reasoning = text(delta.reasoning) || text(delta.reasoning_content);
3278
+ if (reasoning) {
3279
+ if (!reasoningId) {
3280
+ parts.push(...closeText());
3281
+ reasoningId = "reasoning-0";
3282
+ parts.push({ type: "reasoning-start", id: reasoningId });
3283
+ }
3284
+ parts.push({ type: "reasoning-delta", id: reasoningId, delta: reasoning });
3285
+ }
3286
+ const content = text(delta.content);
3287
+ if (content) {
3288
+ if (reasoningId)
3289
+ parts.push(...closeReasoning());
3290
+ if (!textId) {
3291
+ textId = "text-0";
3292
+ parts.push({ type: "text-start", id: textId });
3293
+ }
3294
+ parts.push({ type: "text-delta", id: textId, delta: content });
3295
+ }
3296
+ const calls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
3297
+ if (calls.length > 0) {
3298
+ parts.push(...closeReasoning(), ...closeText());
3299
+ for (const raw of calls) {
3300
+ const call = rec(raw);
3301
+ const fn = rec(call.function);
3302
+ const index = typeof call.index === "number" ? call.index : tools.size;
3303
+ let buffer = tools.get(index);
3304
+ if (!buffer) {
3305
+ buffer = { id: text(call.id) || `tool-${index}`, name: text(fn.name), input: "", opened: false, closed: false };
3306
+ tools.set(index, buffer);
3307
+ }
3308
+ if (text(call.id))
3309
+ buffer.id = text(call.id);
3310
+ if (text(fn.name))
3311
+ buffer.name = text(fn.name);
3312
+ const args = text(fn.arguments);
3313
+ if (!buffer.opened && buffer.name) {
3314
+ buffer.opened = true;
3315
+ parts.push({ type: "tool-input-start", id: buffer.id, toolName: buffer.name });
3316
+ if (buffer.input)
3317
+ parts.push({ type: "tool-input-delta", id: buffer.id, delta: buffer.input });
3318
+ } else if (args && !buffer.closed) {
3319
+ parts.push({ type: "tool-input-delta", id: buffer.id, delta: args });
3320
+ }
3321
+ if (!buffer.closed) {
3322
+ buffer.input += args;
3323
+ parts.push(...completeIfParsed(buffer));
3324
+ }
3325
+ }
3326
+ }
3327
+ const rawFinish = text(choice.finish_reason) || text(choice.finishReason) || text(event.finish_reason) || text(event.finishReason);
3328
+ if (rawFinish)
3329
+ pending = finishReason(rawFinish);
3330
+ if (event.usage !== undefined && event.usage !== null)
3331
+ usage = openaiUsage(rec(event.usage));
3332
+ if (pending && !finished && (usage !== undefined || choices.length === 0)) {
3333
+ finished = true;
3334
+ parts.push(...closeReasoning(), ...closeText(), ...flushTools());
3335
+ parts.push({ type: "finish", finishReason: pending, usage: usage ?? usageFrom(0, 0, 0, 0, 0, 0) });
3336
+ }
3337
+ return parts;
3338
+ },
3339
+ close() {
3340
+ const parts = [...closeReasoning(), ...closeText(), ...flushTools()];
3341
+ if (pending && !finished) {
3342
+ finished = true;
3343
+ parts.push({ type: "finish", finishReason: pending, usage: usage ?? usageFrom(0, 0, 0, 0, 0, 0) });
3344
+ }
3345
+ return parts;
3346
+ }
3347
+ };
3348
+ }
3349
+
3350
+ // src/provider/wire.ts
3351
+ var DEFAULT_MAX_TOKENS = 64000;
3352
+ function capTokens(value) {
3353
+ const n = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_MAX_TOKENS;
3354
+ return Math.min(n, DEFAULT_MAX_TOKENS);
3355
+ }
3356
+ function textOf(part) {
3357
+ const p = part;
3358
+ return typeof p.text === "string" ? p.text : "";
3359
+ }
3360
+ function resultText(part) {
3361
+ const output = part.output ?? part.result;
3362
+ if (typeof output === "string")
3363
+ return output;
3364
+ if (output && typeof output === "object") {
3365
+ const o = output;
3366
+ if (typeof o.value === "string")
3367
+ return o.value;
3368
+ if (o.value !== undefined)
3369
+ return JSON.stringify(o.value);
3370
+ if (typeof o.reason === "string")
3371
+ return o.reason;
3372
+ }
3373
+ return JSON.stringify(output ?? "");
3374
+ }
3375
+ function imageUrl(part) {
3376
+ const p = part;
3377
+ const mime = p.mediaType ?? p.mimeType ?? "application/octet-stream";
3378
+ const data = p.data;
3379
+ if (typeof data === "string")
3380
+ return data.startsWith("data:") ? data : `data:${mime};base64,${data}`;
3381
+ if (data instanceof Uint8Array)
3382
+ return `data:${mime};base64,${Buffer.from(data).toString("base64")}`;
3383
+ if (data instanceof URL)
3384
+ return data.href;
3385
+ return;
3386
+ }
3387
+ function pairedToolCallIds(prompt) {
3388
+ const calls = new Set;
3389
+ const results = new Set;
3390
+ for (const message of prompt) {
3391
+ for (const part of message.content) {
3392
+ if (part.type === "tool-call" && typeof part.toolCallId === "string")
3393
+ calls.add(part.toolCallId);
3394
+ if (part.type === "tool-result" && typeof part.toolCallId === "string")
3395
+ results.add(part.toolCallId);
3396
+ }
3397
+ }
3398
+ return new Set([...calls].filter((id) => results.has(id)));
3399
+ }
3400
+ function systemText(prompt) {
3401
+ return prompt.filter((m) => m.role === "system").map((m) => m.content).join(`
3402
+ `);
3403
+ }
3404
+ function anthropicUserContent(content, images) {
3405
+ if (typeof content === "string")
3406
+ return content;
3407
+ const parts = content ?? [];
3408
+ const blocks = [];
3409
+ for (const part of parts) {
3410
+ if (part.type === "text")
3411
+ blocks.push({ type: "text", text: textOf(part) });
3412
+ else if (part.type === "file" && images) {
3413
+ const url = imageUrl(part);
3414
+ if (url?.startsWith("data:")) {
3415
+ const [head, b64] = url.slice(5).split(";base64,");
3416
+ blocks.push({ type: "image", source: { type: "base64", media_type: head, data: b64 } });
3417
+ }
3418
+ }
3419
+ }
3420
+ if (blocks.length === 1 && blocks[0].type === "text") {
3421
+ return blocks[0].text;
3422
+ }
3423
+ return blocks;
3424
+ }
3425
+ function openaiUserContent(content, images) {
3426
+ if (typeof content === "string")
3427
+ return content;
3428
+ const parts = content ?? [];
3429
+ const blocks = [];
3430
+ for (const part of parts) {
3431
+ if (part.type === "text")
3432
+ blocks.push({ type: "text", text: textOf(part) });
3433
+ else if (part.type === "file" && images) {
3434
+ const url = imageUrl(part);
3435
+ if (url)
3436
+ blocks.push({ type: "image_url", image_url: { url } });
3437
+ }
3438
+ }
3439
+ if (blocks.every((b) => b.type === "text")) {
3440
+ return blocks.map((b) => b.text).join(`
3441
+ `);
3442
+ }
3443
+ return blocks;
3444
+ }
3445
+ function anthropicBody(model, prompt, options) {
3446
+ const paired = pairedToolCallIds(prompt);
3447
+ const messages = [];
3448
+ for (const message of prompt) {
3449
+ if (message.role === "system")
3450
+ continue;
3451
+ if (message.role === "user") {
3452
+ messages.push({ role: "user", content: anthropicUserContent(message.content, options.images) });
3453
+ continue;
3454
+ }
3455
+ if (message.role === "assistant") {
3456
+ const content = [];
3457
+ for (const part of message.content) {
3458
+ if (part.type === "text") {
3459
+ const text = textOf(part);
3460
+ if (text)
3461
+ content.push({ type: "text", text });
3462
+ } else if (part.type === "tool-call" && paired.has(String(part.toolCallId))) {
3463
+ content.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input: part.input ?? {} });
3464
+ }
3465
+ }
3466
+ if (content.length > 0)
3467
+ messages.push({ role: "assistant", content });
3468
+ continue;
3469
+ }
3470
+ for (const part of message.content) {
3471
+ if (part.type !== "tool-result" || !paired.has(String(part.toolCallId)))
3472
+ continue;
3473
+ messages.push({
3474
+ role: "user",
3475
+ content: [
3476
+ {
3477
+ type: "tool_result",
3478
+ tool_use_id: part.toolCallId,
3479
+ content: resultText(part)
3480
+ }
3481
+ ]
3482
+ });
3483
+ }
3484
+ }
3485
+ const system = systemText(prompt);
3486
+ const body = { model, stream: true, messages, max_tokens: capTokens(options.maxOutputTokens) };
3487
+ if (system)
3488
+ body.system = [{ type: "text", text: system, cache_control: { type: "ephemeral" } }];
3489
+ if (options.tools?.length) {
3490
+ body.tools = options.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema ?? {} }));
3491
+ }
3492
+ if (options.temperature !== undefined)
3493
+ body.temperature = options.temperature;
3494
+ if (options.reasoningEffort)
3495
+ body.reasoning_effort = options.reasoningEffort;
3496
+ return body;
3497
+ }
3498
+ function openaiBody(model, prompt, options) {
3499
+ const paired = pairedToolCallIds(prompt);
3500
+ const messages = [];
3501
+ const system = systemText(prompt);
3502
+ if (system)
3503
+ messages.push({ role: "system", content: system });
3504
+ for (const message of prompt) {
3505
+ if (message.role === "system")
3506
+ continue;
3507
+ if (message.role === "user") {
3508
+ messages.push({ role: "user", content: openaiUserContent(message.content, options.images) });
3509
+ continue;
3510
+ }
3511
+ if (message.role === "assistant") {
3512
+ const calls = message.content.filter((p) => p.type === "tool-call" && paired.has(String(p.toolCallId)));
3513
+ const texts = message.content.filter((p) => p.type === "text").map(textOf).filter(Boolean);
3514
+ const reasoning = message.content.filter((p) => p.type === "reasoning").map(textOf).join("");
3515
+ if (calls.length > 0) {
3516
+ messages.push({
3517
+ role: "assistant",
3518
+ ...reasoning ? { reasoning_content: reasoning } : {},
3519
+ content: texts.length ? texts.join(`
3520
+ `) : null,
3521
+ tool_calls: calls.map((c) => ({
3522
+ id: c.toolCallId,
3523
+ type: "function",
3524
+ function: { name: c.toolName, arguments: JSON.stringify(c.input ?? {}) }
3525
+ }))
3526
+ });
3527
+ } else if (texts.length > 0) {
3528
+ messages.push({ role: "assistant", ...reasoning ? { reasoning_content: reasoning } : {}, content: texts.join(`
3529
+ `) });
3530
+ }
3531
+ continue;
3532
+ }
3533
+ for (const part of message.content) {
3534
+ if (part.type !== "tool-result" || !paired.has(String(part.toolCallId)))
3535
+ continue;
3536
+ messages.push({
3537
+ role: "tool",
3538
+ tool_call_id: part.toolCallId,
3539
+ content: resultText(part)
3540
+ });
3541
+ }
3542
+ }
3543
+ const body = { model, stream: true, messages, max_tokens: capTokens(options.maxOutputTokens) };
3544
+ if (options.tools?.length) {
3545
+ body.tools = options.tools.map((t) => ({
3546
+ type: "function",
3547
+ function: { name: t.name, description: t.description, parameters: t.inputSchema ?? {} }
3548
+ }));
3549
+ }
3550
+ if (options.temperature !== undefined)
3551
+ body.temperature = options.temperature;
3552
+ if (options.reasoningEffort)
3553
+ body.reasoning_effort = options.reasoningEffort;
3554
+ body.stream_options = { include_usage: true };
3555
+ return body;
3556
+ }
3557
+ function requestBody(lane, model, prompt, options) {
3558
+ return lane === "anthropic" ? anthropicBody(model, prompt, options) : openaiBody(model, prompt, options);
3559
+ }
3560
+
3561
+ // src/provider/index.ts
3562
+ var DEFAULT_BASE_URL = "https://api.commandcode.ai";
3563
+ function laneOf(modelId) {
3564
+ const bare = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1) : modelId;
3565
+ return bare.toLowerCase().startsWith("claude") ? "anthropic" : "openai";
3566
+ }
3567
+ function endpoint(lane) {
3568
+ return lane === "anthropic" ? "/provider/v1/messages" : "/provider/v1/chat/completions";
3569
+ }
3570
+ function effortFrom(options) {
3571
+ const providerOptions = options.providerOptions ?? {};
3572
+ const top = providerOptions.reasoning ?? providerOptions.reasoningEffort;
3573
+ if (typeof top === "string")
3574
+ return top;
3575
+ const namespaced = providerOptions.commandcode;
3576
+ if (namespaced && typeof namespaced === "object") {
3577
+ const value = namespaced.reasoning ?? namespaced.reasoningEffort;
3578
+ if (typeof value === "string")
3579
+ return value;
3580
+ }
3581
+ return;
3582
+ }
3583
+ function toolsOf(options) {
3584
+ const tools = options.tools ?? [];
3585
+ if (tools.length === 0)
3586
+ return;
3587
+ return tools.map((tool) => ({
3588
+ name: tool.name,
3589
+ description: "description" in tool ? tool.description : undefined,
3590
+ inputSchema: tool.inputSchema ?? {}
3591
+ }));
3592
+ }
3593
+ function createCommandCode(options = {}, images = () => false) {
3594
+ const baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
3595
+ const doFetch = options.fetch ?? fetch;
3596
+ function model(modelId) {
3597
+ const lane = laneOf(modelId);
3598
+ return {
3599
+ specificationVersion: "v3",
3600
+ provider: "command-code",
3601
+ modelId,
3602
+ supportedUrls: {},
3603
+ async doStream(callOptions) {
3604
+ const body = requestBody(lane, modelId, callOptions.prompt, {
3605
+ maxOutputTokens: callOptions.maxOutputTokens,
3606
+ temperature: callOptions.temperature,
3607
+ tools: toolsOf(callOptions),
3608
+ reasoningEffort: effortFrom(callOptions) ?? options.reasoningEffort,
3609
+ images: images(modelId)
3610
+ });
3611
+ const response = await doFetch(`${baseURL}${endpoint(lane)}`, {
3612
+ method: "POST",
3613
+ headers: {
3614
+ "Content-Type": "application/json",
3615
+ Authorization: `Bearer ${options.apiKey ?? ""}`,
3616
+ ...options.headers ?? {},
3617
+ ...callOptions.headers ?? {}
3618
+ },
3619
+ body: JSON.stringify(body),
3620
+ signal: callOptions.abortSignal
3621
+ });
3622
+ if (!response.ok || !response.body) {
3623
+ const detail = await response.text().catch(() => "");
3624
+ throw new Error(`command-code ${lane} ${response.status}: ${detail.slice(0, 300)}`);
3625
+ }
3626
+ const reducer = lane === "anthropic" ? anthropicReducer() : openaiReducer();
3627
+ const stream = new ReadableStream({
3628
+ async start(controller) {
3629
+ const reader = response.body.getReader();
3630
+ const decoder = new TextDecoder;
3631
+ const emit = (parts) => {
3632
+ for (const part of parts)
3633
+ controller.enqueue(part);
3634
+ };
3635
+ const decode = sseDecoder((event) => emit(reducer.feed(event)));
3636
+ try {
3637
+ for (;; ) {
3638
+ const { done, value } = await reader.read();
3639
+ if (done)
3640
+ break;
3641
+ decode(decoder.decode(value, { stream: true }));
3642
+ }
3643
+ emit(reducer.close());
3644
+ controller.close();
3645
+ } catch (error) {
3646
+ controller.enqueue({ type: "error", error });
3647
+ controller.close();
3648
+ }
3649
+ }
3650
+ });
3651
+ return { stream, response: { headers: Object.fromEntries(response.headers) } };
3652
+ },
3653
+ async doGenerate(callOptions) {
3654
+ const { stream } = await this.doStream(callOptions);
3655
+ const reader = stream.getReader();
3656
+ const content = [];
3657
+ let finish = { unified: "stop", raw: "unknown" };
3658
+ let usage = {
3659
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
3660
+ outputTokens: { total: 0, text: 0, reasoning: 0 }
3661
+ };
3662
+ const textOf = {};
3663
+ const toolsById = new Map;
3664
+ for (;; ) {
3665
+ const { done, value } = await reader.read();
3666
+ if (done)
3667
+ break;
3668
+ const part = value;
3669
+ if (part.type === "text-delta")
3670
+ textOf[String(part.id)] = (textOf[String(part.id)] ?? "") + String(part.delta);
3671
+ else if (part.type === "reasoning-delta") {
3672
+ content.push({ type: "reasoning", text: String(part.delta) });
3673
+ } else if (part.type === "tool-input-start") {
3674
+ toolsById.set(String(part.id), { name: String(part.toolName), input: "" });
3675
+ } else if (part.type === "tool-input-delta") {
3676
+ const buffer = toolsById.get(String(part.id));
3677
+ if (buffer)
3678
+ buffer.input += String(part.delta);
3679
+ } else if (part.type === "tool-call") {
3680
+ content.push({
3681
+ type: "tool-call",
3682
+ toolCallId: part.toolCallId,
3683
+ toolName: part.toolName,
3684
+ input: part.input
3685
+ });
3686
+ } else if (part.type === "finish") {
3687
+ finish = part;
3688
+ usage = part.usage;
3689
+ }
3690
+ }
3691
+ for (const [id, value] of Object.entries(textOf))
3692
+ content.push({ type: "text", text: value, id });
3693
+ return {
3694
+ content,
3695
+ finishReason: finish.finishReason,
3696
+ usage,
3697
+ warnings: []
3698
+ };
3699
+ }
3700
+ };
3701
+ }
3702
+ return { languageModel: model };
3703
+ }
3704
+
3057
3705
  // src/v2.ts
3058
3706
  var PLUGIN_ID = "command-code";
3059
3707
  var PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
@@ -3103,6 +3751,19 @@ function staticSeedModels() {
3103
3751
  }
3104
3752
  return seed;
3105
3753
  }
3754
+ function mergeModels(snapshot, live) {
3755
+ const byId = new Map;
3756
+ const add = (model) => {
3757
+ const id = model?.id;
3758
+ if (typeof id === "string")
3759
+ byId.set(id, model);
3760
+ };
3761
+ for (const model of snapshot)
3762
+ add(model);
3763
+ for (const model of live)
3764
+ add(model);
3765
+ return [...byId.values()];
3766
+ }
3106
3767
  async function credentialKey(ctx) {
3107
3768
  try {
3108
3769
  const connection = await ctx.integration.connection.active(INTEGRATION_ID);
@@ -3123,17 +3784,14 @@ async function credentialKey(ctx) {
3123
3784
  var commandCodeV2 = {
3124
3785
  id: PLUGIN_ID,
3125
3786
  async setup(ctx) {
3787
+ const t0 = Date.now();
3126
3788
  let localKey;
3789
+ let keyMs = 0;
3127
3790
  try {
3791
+ const keyStart = Date.now();
3128
3792
  localKey = await resolveKey();
3793
+ keyMs = Date.now() - keyStart;
3129
3794
  } catch {}
3130
- let hasConnection = false;
3131
- try {
3132
- hasConnection = await ctx.integration.connection.active(INTEGRATION_ID) !== undefined;
3133
- } catch {}
3134
- if (!localKey && !hasConnection) {
3135
- console.warn(`[command-code] no API key found. Run /connect and choose "${INTEGRATION_NAME}", or \`cmd login\` (~/.commandcode/auth.json), or set CMD_API_KEY.`);
3136
- }
3137
3795
  await ctx.integration.transform((editor) => {
3138
3796
  editor.update(INTEGRATION_ID, (integration) => {
3139
3797
  if (integration.name === integration.id) {
@@ -3149,6 +3807,17 @@ var commandCodeV2 = {
3149
3807
  method: { type: "key", label: "Command Code API key" }
3150
3808
  });
3151
3809
  });
3810
+ const images = (modelId) => inputModalities(modelId).includes("image");
3811
+ for (const lane of LANES) {
3812
+ await ctx.aisdk.hook("sdk", (event) => {
3813
+ const options = event.options ?? {};
3814
+ event.sdk = createCommandCode({
3815
+ apiKey: typeof options.apiKey === "string" ? options.apiKey : undefined,
3816
+ baseURL: typeof options.baseURL === "string" ? options.baseURL : PROVIDER_BASE,
3817
+ headers: options.headers
3818
+ }, images);
3819
+ }, { providerID: lane.id });
3820
+ }
3152
3821
  const seed = staticSeedModels();
3153
3822
  await ctx.provider.transform((editor) => {
3154
3823
  for (const lane of LANES) {
@@ -3157,9 +3826,9 @@ var commandCodeV2 = {
3157
3826
  provider.name = lane.name;
3158
3827
  if (provider.package === "")
3159
3828
  provider.package = lane.pkg;
3160
- provider.activation = hasConnection || localKey ? "enabled" : "auto";
3829
+ provider.activation = localKey ? "enabled" : "auto";
3161
3830
  provider.settings = { ...provider.settings, baseURL: PROVIDER_BASE };
3162
- if (!hasConnection && localKey && provider.settings.apiKey === undefined) {
3831
+ if (localKey && provider.settings.apiKey === undefined) {
3163
3832
  provider.settings.apiKey = localKey;
3164
3833
  }
3165
3834
  provider.integrationID ??= INTEGRATION_ID;
@@ -3167,6 +3836,7 @@ var commandCodeV2 = {
3167
3836
  editor.models.set(lane.id, seed[lane.id]);
3168
3837
  }
3169
3838
  });
3839
+ const registerMs = Date.now() - t0;
3170
3840
  await ctx.tool.transform((editor) => {
3171
3841
  editor.add({
3172
3842
  name: "cmd_usage",
@@ -3190,12 +3860,32 @@ var commandCodeV2 = {
3190
3860
  }
3191
3861
  });
3192
3862
  });
3863
+ let hasConnection = false;
3864
+ let connectionMs = 0;
3865
+ try {
3866
+ const connectionStart = Date.now();
3867
+ hasConnection = await ctx.integration.connection.active(INTEGRATION_ID) !== undefined;
3868
+ connectionMs = Date.now() - connectionStart;
3869
+ } catch {}
3870
+ if (hasConnection) {
3871
+ await ctx.provider.transform((editor) => {
3872
+ for (const lane of LANES) {
3873
+ editor.update(lane.id, (provider) => {
3874
+ provider.activation = "enabled";
3875
+ });
3876
+ }
3877
+ });
3878
+ }
3879
+ if (!localKey && !hasConnection) {
3880
+ console.warn(`[command-code] no API key found. Run /connect and choose "${INTEGRATION_NAME}", or \`cmd login\` (~/.commandcode/auth.json), or set CMD_API_KEY.`);
3881
+ }
3193
3882
  const controller = new AbortController;
3194
3883
  let refreshing = false;
3195
3884
  const refresh = async () => {
3196
3885
  if (refreshing || controller.signal.aborted)
3197
3886
  return;
3198
3887
  refreshing = true;
3888
+ const refreshStart = Date.now();
3199
3889
  try {
3200
3890
  const key = await credentialKey(ctx);
3201
3891
  if (!key)
@@ -3204,11 +3894,12 @@ var commandCodeV2 = {
3204
3894
  if (controller.signal.aborted)
3205
3895
  return;
3206
3896
  await ctx.provider.transform((editor) => {
3207
- editor.models.set("command-code-anthropic", split.claude.map((m) => toV2Model(m, LANES[0])));
3208
- editor.models.set("command-code-openai", split.open.map((m) => toV2Model(m, LANES[1])));
3897
+ editor.models.set("command-code-anthropic", mergeModels(seed["command-code-anthropic"], split.claude.map((m) => toV2Model(m, LANES[0]))));
3898
+ editor.models.set("command-code-openai", mergeModels(seed["command-code-openai"], split.open.map((m) => toV2Model(m, LANES[1]))));
3209
3899
  });
3900
+ console.log(`[command-code] setup: key=${keyMs}ms register=${registerMs}ms connection=${connectionMs}ms refresh=${Date.now() - refreshStart}ms models=${split.claude.length + split.open.length}`);
3210
3901
  } catch (e) {
3211
- console.warn("[command-code] live model list unavailable, keeping previous list:", e);
3902
+ console.warn("[command-code] live model list unavailable, keeping the snapshot:", e);
3212
3903
  } finally {
3213
3904
  refreshing = false;
3214
3905
  }
@@ -3221,8 +3912,58 @@ var commandCodeV2 = {
3221
3912
  };
3222
3913
  }
3223
3914
  };
3915
+ // package.json
3916
+ var package_default = {
3917
+ name: "@jeffreyjyz/opencode-command-code",
3918
+ version: "0.3.1",
3919
+ description: "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
3920
+ type: "module",
3921
+ main: "dist/index.js",
3922
+ types: "dist/index.d.ts",
3923
+ exports: {
3924
+ ".": "./dist/index.js",
3925
+ "./tui": "./dist/tui.js"
3926
+ },
3927
+ scripts: {
3928
+ build: "rm -rf dist && bun build src/index.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin --external @opencode/plugin/tui && bun scripts/build-tui.ts && tsc -p tsconfig.build.json",
3929
+ test: "bun test",
3930
+ typecheck: "tsc --noEmit",
3931
+ "build:tui": "bun scripts/build-tui.ts",
3932
+ "extract:catalog": "bun scripts/extract-catalog.ts"
3933
+ },
3934
+ dependencies: {
3935
+ "@opentui/core": "^0.5.4",
3936
+ "@opentui/solid": "^0.5.4",
3937
+ "solid-js": "1.9.12",
3938
+ zod: "4.1.8"
3939
+ },
3940
+ devDependencies: {
3941
+ "@types/bun": "latest",
3942
+ typescript: "^5",
3943
+ "@opencode-ai/plugin": "*",
3944
+ "@opencode/plugin": "^2.0.11",
3945
+ "@ai-sdk/provider": "3.0.8"
3946
+ },
3947
+ files: [
3948
+ "dist",
3949
+ "tui.js",
3950
+ "LICENSE"
3951
+ ],
3952
+ keywords: [
3953
+ "opencode",
3954
+ "command-code",
3955
+ "plugin",
3956
+ "provider",
3957
+ "ai-sdk"
3958
+ ],
3959
+ license: "MIT",
3960
+ publishConfig: {
3961
+ access: "public"
3962
+ }
3963
+ };
3224
3964
 
3225
3965
  // src/index.ts
3966
+ var PROVIDER_NPM = `${package_default.name}@${package_default.version}`;
3226
3967
  function tool(input) {
3227
3968
  return input;
3228
3969
  }
@@ -3318,18 +4059,18 @@ var CommandCodePlugin = async (_input) => {
3318
4059
  console.warn("[command-code] model list unavailable:", e);
3319
4060
  }
3320
4061
  }
3321
- const claudeDefs = split ? toModelDefs(split.claude, "command-code-anthropic", "@ai-sdk/anthropic") : {};
3322
- const openDefs = split ? toModelDefs(split.open, "command-code-openai", "@ai-sdk/openai-compatible", { field: "reasoning_content" }) : {};
4062
+ const claudeDefs = split ? toModelDefs(split.claude, "command-code-anthropic", PROVIDER_NPM) : {};
4063
+ const openDefs = split ? toModelDefs(split.open, "command-code-openai", PROVIDER_NPM, { field: "reasoning_content" }) : {};
3323
4064
  const userAnthropic = existing("command-code-anthropic");
3324
4065
  cfg.provider["command-code-anthropic"] = {
3325
- npm: userAnthropic.npm ?? "@ai-sdk/anthropic",
4066
+ npm: userAnthropic.npm ?? PROVIDER_NPM,
3326
4067
  name: userAnthropic.name ?? "Command Code (Anthropic)",
3327
4068
  options: { baseURL: PROVIDER_BASE, ...userAnthropic.options },
3328
4069
  models: { ...claudeDefs, ...userAnthropic.models }
3329
4070
  };
3330
4071
  const userOpenai = existing("command-code-openai");
3331
4072
  cfg.provider["command-code-openai"] = {
3332
- npm: userOpenai.npm ?? "@ai-sdk/openai-compatible",
4073
+ npm: userOpenai.npm ?? PROVIDER_NPM,
3333
4074
  name: userOpenai.name ?? "Command Code (OpenAI)",
3334
4075
  options: {
3335
4076
  baseURL: PROVIDER_BASE,
@@ -3352,7 +4093,7 @@ var CommandCodePlugin = async (_input) => {
3352
4093
  return a?.type === "api" && a.key ? { key: a.key } : undefined;
3353
4094
  });
3354
4095
  const split = await loadModels(key);
3355
- return toModelDefs(split.claude, "command-code-anthropic", "@ai-sdk/anthropic");
4096
+ return toModelDefs(split.claude, "command-code-anthropic", PROVIDER_NPM);
3356
4097
  }
3357
4098
  },
3358
4099
  tool: {
@@ -3375,5 +4116,6 @@ var src_default = {
3375
4116
  };
3376
4117
  export {
3377
4118
  CommandCodePlugin,
4119
+ createCommandCode,
3378
4120
  src_default as default
3379
4121
  };
@@ -0,0 +1,22 @@
1
+ import type { LanguageModelV3 } from "@ai-sdk/provider";
2
+ import { type WireLane } from "./wire";
3
+ export declare const DEFAULT_BASE_URL = "https://api.commandcode.ai";
4
+ export interface CommandCodeProviderOptions {
5
+ /** Bearer key; the v2 host passes the resolved connection credential here. */
6
+ apiKey?: string;
7
+ baseURL?: string;
8
+ /** Extra headers (host-supplied), merged last. */
9
+ headers?: Record<string, string>;
10
+ /** Reasoning effort the host wants (`providerOptions.commandcode.reasoning`). */
11
+ reasoningEffort?: string;
12
+ /** Injected for tests. */
13
+ fetch?: typeof fetch;
14
+ }
15
+ export interface CommandCodeProvider {
16
+ languageModel(modelId: string): LanguageModelV3;
17
+ }
18
+ /** Claude models use the Anthropic wire; everything else chat completions. */
19
+ export declare function laneOf(modelId: string): WireLane;
20
+ /** True when the model advertises image input; the caller passes the table in. */
21
+ export type ImagesFor = (modelId: string) => boolean;
22
+ export declare function createCommandCode(options?: CommandCodeProviderOptions, images?: ImagesFor): CommandCodeProvider;
@@ -0,0 +1,21 @@
1
+ import type { LanguageModelV3FinishReason, LanguageModelV3StreamPart, LanguageModelV3Usage } from "@ai-sdk/provider";
2
+ type Part = LanguageModelV3StreamPart;
3
+ /** Split an SSE byte stream into JSON events, tolerating partial chunks. */
4
+ export declare function sseDecoder(onEvent: (event: Record<string, unknown>) => void): (text: string) => void;
5
+ /** Wire stop reason → unified v3 finish reason; unknown reasons completed. */
6
+ export declare function finishReason(raw: string | undefined): LanguageModelV3FinishReason;
7
+ /** Anthropic `message_start`/`message_delta` usage → v3 usage. */
8
+ export declare function anthropicUsage(raw: Record<string, unknown>): LanguageModelV3Usage;
9
+ /** OpenAI usage → v3 usage (`prompt_tokens_details.cached_tokens` is cache read). */
10
+ export declare function openaiUsage(raw: Record<string, unknown>): LanguageModelV3Usage;
11
+ export interface Reducer {
12
+ /** Feed one parsed SSE event; returns the parts it produced. */
13
+ feed(event: Record<string, unknown>): Part[];
14
+ /** Close open parts when the transport ends (abort/error). */
15
+ close(): Part[];
16
+ }
17
+ /** Anthropic Messages reducer. */
18
+ export declare function anthropicReducer(): Reducer;
19
+ /** OpenAI Chat Completions reducer. */
20
+ export declare function openaiReducer(): Reducer;
21
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { LanguageModelV3Prompt } from "@ai-sdk/provider";
2
+ export type WireLane = "anthropic" | "openai";
3
+ /** The subset of tools we serialize (V3 FunctionTool already matches). */
4
+ export interface WireTool {
5
+ name: string;
6
+ description?: string;
7
+ inputSchema: unknown;
8
+ }
9
+ export interface WireOptions {
10
+ maxOutputTokens?: number;
11
+ temperature?: number;
12
+ tools?: WireTool[];
13
+ /** Wire extension both gates honour (`reasoning_effort`). */
14
+ reasoningEffort?: string;
15
+ /** Whether the model advertises image input (catalog-derived). */
16
+ images: boolean;
17
+ }
18
+ /** Anthropic body: POST /provider/v1/messages. */
19
+ export declare function anthropicBody(model: string, prompt: LanguageModelV3Prompt, options: WireOptions): Record<string, unknown>;
20
+ /** OpenAI body: POST /provider/v1/chat/completions. */
21
+ export declare function openaiBody(model: string, prompt: LanguageModelV3Prompt, options: WireOptions): Record<string, unknown>;
22
+ export declare function requestBody(lane: WireLane, model: string, prompt: LanguageModelV3Prompt, options: WireOptions): Record<string, unknown>;
@@ -8,3 +8,9 @@ export declare const WEEKLY_SECS: number;
8
8
  * the same unit cmduse-core's vectors pin.
9
9
  */
10
10
  export declare function elapsedPct(resetAtMs: number | undefined, durSecs: number, nowSecs: number): number | undefined;
11
+ /**
12
+ * `elapsedPct` as text: a seven-day window forty minutes in is 0.4%, and "0%"
13
+ * reads as "not started". Non-zero fractions below a percent render "<1%".
14
+ * Exact zero stays "0%".
15
+ */
16
+ export declare function elapsedLabel(resetAtMs: number | undefined, durSecs: number, nowSecs: number): string | undefined;
package/dist/tui.js CHANGED
@@ -403,6 +403,14 @@ function elapsedPct(resetAtMs, durSecs, nowSecs) {
403
403
  const pct = (nowSecs - start) / durSecs * 100;
404
404
  return Math.round(Math.min(100, Math.max(0, pct)));
405
405
  }
406
+ function elapsedLabel(resetAtMs, durSecs, nowSecs) {
407
+ const pct = elapsedPct(resetAtMs, durSecs, nowSecs);
408
+ if (pct === undefined)
409
+ return;
410
+ if (pct === 0 && nowSecs > Math.floor(resetAtMs / 1000) - durSecs)
411
+ return "<1%";
412
+ return `${pct}%`;
413
+ }
406
414
 
407
415
  // src/sidebar/rows.ts
408
416
  var TIER_DISPLAY = {
@@ -445,9 +453,9 @@ function windowRow(label, w, now, durSecs) {
445
453
  return;
446
454
  const used = typeof w.used === "number" ? w.used : 0;
447
455
  const pct = Math.round(used / w.cap * 100);
448
- const elapsed = elapsedPct(w.resetAt, durSecs, Math.floor(now / 1000));
456
+ const elapsed = elapsedLabel(w.resetAt, durSecs, Math.floor(now / 1000));
449
457
  const reset = until(w.resetAt, now);
450
- const parts = [`${money(used)} / ${money(w.cap)} (${pct}%)`, ...elapsed === undefined ? [] : [`${elapsed}% elapsed`], ...reset ? [`resets ${reset}`] : []];
458
+ const parts = [`${money(used)} / ${money(w.cap)} (${pct}%)`, ...elapsed === undefined ? [] : [`${elapsed} elapsed`], ...reset ? [`resets ${reset}`] : []];
451
459
  return [label, parts.join(" \xB7 ")];
452
460
  }
453
461
  function usageRows(usage, now = Date.now()) {
package/dist/v2.d.ts CHANGED
@@ -21,9 +21,15 @@ export declare const TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: pla
21
21
  * only the open lane needs the reasoning round-trip field. */
22
22
  export declare function toV2Model(m: CmdModel, lane: Lane): unknown;
23
23
  /** Instant static seed from gating.json's known model ids, split by wire lane.
24
- * Replaced by the gated live list once the background fetch lands; also the
25
- * fallback shown when the live fetch fails. */
24
+ * Shows in /model immediately; the live fetch merges over it later. */
26
25
  export declare function staticSeedModels(): Record<Lane["id"], unknown[]>;
26
+ /**
27
+ * Union by model id: the snapshot keeps every id it had, so a gated or partial
28
+ * live response can never make a model vanish from the picker, while live
29
+ * entries win field-by-field so context, rates, variants and modalities stay
30
+ * fresh. Snapshot order is preserved; live-only models append in live order.
31
+ */
32
+ export declare function mergeModels(snapshot: readonly unknown[], live: readonly unknown[]): unknown[];
27
33
  /** The slice of the plugin context the credential resolver needs. */
28
34
  type CredentialContext = {
29
35
  integration: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.2.11",
3
+ "version": "0.3.1",
4
4
  "description": "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,8 @@
26
26
  "@types/bun": "latest",
27
27
  "typescript": "^5",
28
28
  "@opencode-ai/plugin": "*",
29
- "@opencode/plugin": "^2.0.11"
29
+ "@opencode/plugin": "^2.0.11",
30
+ "@ai-sdk/provider": "3.0.8"
30
31
  },
31
32
  "files": [
32
33
  "dist",