@khalilgharbaoui/opencode-claude-code-plugin 0.2.6 → 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/README.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +194 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -311,7 +311,7 @@ Set `permissionMode: "plan"` to forward `--permission-mode plan` to Claude. The
|
|
|
311
311
|
|
|
312
312
|
- **Empty text blocks are dropped.** Claude sometimes opens a `content_block_start` for text but never sends a delta. The plugin no longer emits the empty block (which was triggering Anthropic 400s like `cache_control cannot be set for empty text blocks`).
|
|
313
313
|
- **`AskUserQuestion`** from the CLI is converted into plain text content rather than forwarded as a tool call.
|
|
314
|
-
- **
|
|
314
|
+
- **Wire-inactivity watchdog.** Once the CLI has produced any content, the stream closes gracefully if stdout goes silent for 60 seconds without a `result` message arriving. Resets on every line received, so long mid-turn pauses (Sonnet between text-end and the next tool_use, for example) are tolerated. On a user-initiated abort, the watchdog shortens to 5 seconds.
|
|
315
315
|
- **Per-iteration usage.** When the CLI internally retries with tools, the plugin only counts the last iteration's usage so opencode's context accounting stays accurate.
|
|
316
316
|
- **Lazy `cwd`.** The working directory is re-resolved at every request, so opencode's project-aware behavior works without restarting the plugin.
|
|
317
317
|
- **Variants survive merge.** opencode recalculates variant lists after the plugin loads; the plugin re-injects defaults into runtime config so your variants don't disappear.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var log = {
|
|
|
19
19
|
console.error(fmt("NOTICE", msg, data));
|
|
20
20
|
},
|
|
21
21
|
warn(msg, data) {
|
|
22
|
-
|
|
22
|
+
console.error(fmt("WARN", msg, data));
|
|
23
23
|
},
|
|
24
24
|
error(msg, data) {
|
|
25
25
|
console.error(fmt("ERROR", msg, data));
|
|
@@ -145,7 +145,7 @@ function mapTool(name, input, opts) {
|
|
|
145
145
|
const toolName = parts.slice(1).join("_");
|
|
146
146
|
const openCodeName = `${serverName}_${toolName}`;
|
|
147
147
|
log.debug("mapping MCP tool", { original: name, mapped: openCodeName });
|
|
148
|
-
return { name: openCodeName, input, executed:
|
|
148
|
+
return { name: openCodeName, input, executed: true };
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
if (OPENCODE_HANDLED_TOOLS.has(name)) {
|
|
@@ -381,22 +381,49 @@ Now continuing with the current message:
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// src/mcp-bridge.ts
|
|
384
|
+
import * as fs2 from "fs";
|
|
385
|
+
import * as path2 from "path";
|
|
386
|
+
import * as os2 from "os";
|
|
387
|
+
import * as crypto from "crypto";
|
|
388
|
+
|
|
389
|
+
// src/tmp.ts
|
|
384
390
|
import * as fs from "fs";
|
|
385
|
-
import * as path from "path";
|
|
386
391
|
import * as os from "os";
|
|
387
|
-
import * as
|
|
392
|
+
import * as path from "path";
|
|
393
|
+
var PLUGIN_TMP_DIR = path.join(
|
|
394
|
+
os.tmpdir(),
|
|
395
|
+
`opencode-claude-code-${process.pid}`
|
|
396
|
+
);
|
|
397
|
+
var registered = false;
|
|
398
|
+
function pluginTmpDir() {
|
|
399
|
+
if (!fs.existsSync(PLUGIN_TMP_DIR)) {
|
|
400
|
+
fs.mkdirSync(PLUGIN_TMP_DIR, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
if (!registered) {
|
|
403
|
+
registered = true;
|
|
404
|
+
process.on("exit", () => {
|
|
405
|
+
try {
|
|
406
|
+
fs.rmSync(PLUGIN_TMP_DIR, { recursive: true, force: true });
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return PLUGIN_TMP_DIR;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/mcp-bridge.ts
|
|
388
415
|
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
389
416
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
390
417
|
function fileExists(p) {
|
|
391
418
|
try {
|
|
392
|
-
return
|
|
419
|
+
return fs2.statSync(p).isFile();
|
|
393
420
|
} catch {
|
|
394
421
|
return false;
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
function dirExists(p) {
|
|
398
425
|
try {
|
|
399
|
-
return
|
|
426
|
+
return fs2.statSync(p).isDirectory();
|
|
400
427
|
} catch {
|
|
401
428
|
return false;
|
|
402
429
|
}
|
|
@@ -442,7 +469,7 @@ function stripJsonComments(text) {
|
|
|
442
469
|
}
|
|
443
470
|
function readAndParse(file) {
|
|
444
471
|
try {
|
|
445
|
-
const raw =
|
|
472
|
+
const raw = fs2.readFileSync(file, "utf8");
|
|
446
473
|
return JSON.parse(stripJsonComments(raw));
|
|
447
474
|
} catch (e) {
|
|
448
475
|
log.warn("failed to parse opencode config", {
|
|
@@ -470,14 +497,14 @@ function deepMerge(target, source) {
|
|
|
470
497
|
}
|
|
471
498
|
function walkUp(opts) {
|
|
472
499
|
const out = [];
|
|
473
|
-
let current =
|
|
500
|
+
let current = path2.resolve(opts.start);
|
|
474
501
|
while (true) {
|
|
475
502
|
for (const target of opts.targets) {
|
|
476
|
-
const candidate =
|
|
503
|
+
const candidate = path2.join(current, target);
|
|
477
504
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
478
505
|
}
|
|
479
|
-
if (opts.stop && current ===
|
|
480
|
-
const parent =
|
|
506
|
+
if (opts.stop && current === path2.resolve(opts.stop)) break;
|
|
507
|
+
const parent = path2.dirname(current);
|
|
481
508
|
if (parent === current) break;
|
|
482
509
|
current = parent;
|
|
483
510
|
}
|
|
@@ -485,28 +512,28 @@ function walkUp(opts) {
|
|
|
485
512
|
}
|
|
486
513
|
function detectWorktree(cwd) {
|
|
487
514
|
const override = process.env.OPENCODE_WORKTREE;
|
|
488
|
-
if (override) return
|
|
489
|
-
let current =
|
|
515
|
+
if (override) return path2.resolve(override);
|
|
516
|
+
let current = path2.resolve(cwd);
|
|
490
517
|
while (true) {
|
|
491
|
-
const gitPath =
|
|
518
|
+
const gitPath = path2.join(current, ".git");
|
|
492
519
|
try {
|
|
493
|
-
if (
|
|
520
|
+
if (fs2.existsSync(gitPath)) return current;
|
|
494
521
|
} catch {
|
|
495
522
|
}
|
|
496
|
-
const parent =
|
|
523
|
+
const parent = path2.dirname(current);
|
|
497
524
|
if (parent === current) return void 0;
|
|
498
525
|
current = parent;
|
|
499
526
|
}
|
|
500
527
|
}
|
|
501
528
|
function globalConfigDir() {
|
|
502
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
503
|
-
return
|
|
529
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
530
|
+
return path2.join(xdg, "opencode");
|
|
504
531
|
}
|
|
505
532
|
function loadGlobalConfig() {
|
|
506
533
|
const dir = globalConfigDir();
|
|
507
534
|
let merged = {};
|
|
508
535
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
509
|
-
const file =
|
|
536
|
+
const file = path2.join(dir, name);
|
|
510
537
|
if (!fileExists(file)) continue;
|
|
511
538
|
const parsed = readAndParse(file);
|
|
512
539
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -516,7 +543,7 @@ function loadGlobalConfig() {
|
|
|
516
543
|
function loadProjectFilesInDir(dir) {
|
|
517
544
|
let merged = {};
|
|
518
545
|
for (const name of PROJECT_FILE_NAMES) {
|
|
519
|
-
const file =
|
|
546
|
+
const file = path2.join(dir, name);
|
|
520
547
|
if (!fileExists(file)) continue;
|
|
521
548
|
const parsed = readAndParse(file);
|
|
522
549
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -527,7 +554,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
527
554
|
const dirs = [];
|
|
528
555
|
const seen = /* @__PURE__ */ new Set();
|
|
529
556
|
const push = (p) => {
|
|
530
|
-
const abs =
|
|
557
|
+
const abs = path2.resolve(p);
|
|
531
558
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
532
559
|
seen.add(abs);
|
|
533
560
|
dirs.push(abs);
|
|
@@ -541,9 +568,9 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
541
568
|
})) {
|
|
542
569
|
push(dir);
|
|
543
570
|
}
|
|
544
|
-
const home =
|
|
571
|
+
const home = os2.homedir();
|
|
545
572
|
if (home) {
|
|
546
|
-
const homeDot =
|
|
573
|
+
const homeDot = path2.join(home, ".opencode");
|
|
547
574
|
if (dirExists(homeDot)) push(homeDot);
|
|
548
575
|
}
|
|
549
576
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -628,7 +655,7 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
628
655
|
const projectDirs = [];
|
|
629
656
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
630
657
|
for (const f of projectFiles) {
|
|
631
|
-
const d =
|
|
658
|
+
const d = path2.dirname(f);
|
|
632
659
|
if (!seenProjectDirs.has(d)) {
|
|
633
660
|
seenProjectDirs.add(d);
|
|
634
661
|
projectDirs.push(d);
|
|
@@ -658,13 +685,13 @@ function bridgeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
658
685
|
if (Object.keys(servers).length === 0) return null;
|
|
659
686
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
660
687
|
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
661
|
-
const outPath =
|
|
662
|
-
|
|
663
|
-
`
|
|
688
|
+
const outPath = path2.join(
|
|
689
|
+
pluginTmpDir(),
|
|
690
|
+
`mcp-${hash}.json`
|
|
664
691
|
);
|
|
665
692
|
try {
|
|
666
693
|
if (!fileExists(outPath)) {
|
|
667
|
-
|
|
694
|
+
fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
668
695
|
}
|
|
669
696
|
} catch (e) {
|
|
670
697
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -825,10 +852,12 @@ function buildCliArgs(opts) {
|
|
|
825
852
|
appendSystemPromptFile
|
|
826
853
|
} = opts;
|
|
827
854
|
const args = [
|
|
855
|
+
"--print",
|
|
828
856
|
"--output-format",
|
|
829
857
|
"stream-json",
|
|
830
858
|
"--input-format",
|
|
831
859
|
"stream-json",
|
|
860
|
+
"--include-partial-messages",
|
|
832
861
|
"--verbose"
|
|
833
862
|
];
|
|
834
863
|
if (model) {
|
|
@@ -870,14 +899,14 @@ function sessionKey(cwd, modelId) {
|
|
|
870
899
|
|
|
871
900
|
// src/proxy-mcp.ts
|
|
872
901
|
import { createServer } from "http";
|
|
873
|
-
import * as
|
|
874
|
-
import * as
|
|
875
|
-
import * as os2 from "os";
|
|
902
|
+
import * as fs3 from "fs";
|
|
903
|
+
import * as path3 from "path";
|
|
876
904
|
import * as crypto2 from "crypto";
|
|
877
905
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
878
906
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
879
907
|
var SERVER_NAME = "opencode_proxy";
|
|
880
908
|
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
909
|
+
var PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
881
910
|
var DEFAULT_PROXY_TOOLS = [
|
|
882
911
|
{
|
|
883
912
|
name: "bash",
|
|
@@ -1048,6 +1077,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1048
1077
|
toolName,
|
|
1049
1078
|
hasInput: input != null
|
|
1050
1079
|
});
|
|
1080
|
+
let timer = null;
|
|
1051
1081
|
const result = await new Promise(
|
|
1052
1082
|
(resolve3, reject) => {
|
|
1053
1083
|
const entry = {
|
|
@@ -1058,9 +1088,24 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1058
1088
|
reject
|
|
1059
1089
|
};
|
|
1060
1090
|
pending.set(callId, entry);
|
|
1091
|
+
timer = setTimeout(() => {
|
|
1092
|
+
if (!pending.has(callId)) return;
|
|
1093
|
+
pending.delete(callId);
|
|
1094
|
+
log.warn("proxy-mcp tool call timed out", {
|
|
1095
|
+
callId,
|
|
1096
|
+
toolName,
|
|
1097
|
+
timeoutMs: PROXY_CALL_TIMEOUT_MS
|
|
1098
|
+
});
|
|
1099
|
+
reject(
|
|
1100
|
+
new Error(
|
|
1101
|
+
`Proxy tool '${toolName}' timed out after ${PROXY_CALL_TIMEOUT_MS}ms waiting for opencode to resolve the call`
|
|
1102
|
+
)
|
|
1103
|
+
);
|
|
1104
|
+
}, PROXY_CALL_TIMEOUT_MS);
|
|
1061
1105
|
calls.emit("call", entry);
|
|
1062
1106
|
}
|
|
1063
1107
|
).finally(() => {
|
|
1108
|
+
if (timer) clearTimeout(timer);
|
|
1064
1109
|
pending.delete(callId);
|
|
1065
1110
|
});
|
|
1066
1111
|
if (result.kind === "error") {
|
|
@@ -1149,11 +1194,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1149
1194
|
2
|
|
1150
1195
|
);
|
|
1151
1196
|
const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
1152
|
-
const outPath =
|
|
1153
|
-
|
|
1154
|
-
`
|
|
1197
|
+
const outPath = path3.join(
|
|
1198
|
+
pluginTmpDir(),
|
|
1199
|
+
`proxy-${hash}.json`
|
|
1155
1200
|
);
|
|
1156
|
-
|
|
1201
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
1157
1202
|
configFilePath = outPath;
|
|
1158
1203
|
return outPath;
|
|
1159
1204
|
},
|
|
@@ -1165,6 +1210,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
|
1165
1210
|
await new Promise((resolve3) => {
|
|
1166
1211
|
server2.close(() => resolve3());
|
|
1167
1212
|
});
|
|
1213
|
+
if (configFilePath) {
|
|
1214
|
+
try {
|
|
1215
|
+
fs3.unlinkSync(configFilePath);
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
configFilePath = null;
|
|
1219
|
+
}
|
|
1168
1220
|
}
|
|
1169
1221
|
};
|
|
1170
1222
|
return api;
|
|
@@ -1264,12 +1316,32 @@ function resolvePendingProxyCall(sessionKey2, result) {
|
|
|
1264
1316
|
// src/claude-code-language-model.ts
|
|
1265
1317
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1266
1318
|
import { unlink as unlink2 } from "fs/promises";
|
|
1267
|
-
import { homedir as homedir2, tmpdir as
|
|
1319
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
1268
1320
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1269
|
-
import { dirname as dirname2, join as
|
|
1270
|
-
function
|
|
1321
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1322
|
+
function hasNewUserContent(prompt) {
|
|
1323
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1324
|
+
const msg = prompt[i];
|
|
1325
|
+
if (msg.role === "assistant") return false;
|
|
1326
|
+
if (msg.role !== "user") continue;
|
|
1327
|
+
const content = msg.content;
|
|
1328
|
+
if (typeof content === "string") {
|
|
1329
|
+
if (content.trim()) return true;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
if (Array.isArray(content)) {
|
|
1333
|
+
for (const part of content) {
|
|
1334
|
+
if (part.type === "text" && part.text && part.text.trim()) return true;
|
|
1335
|
+
if (part.type === "tool-result") return true;
|
|
1336
|
+
if (part.type === "image" || part.type === "file") return true;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
function readPromptFileIfPresent(path5) {
|
|
1271
1343
|
try {
|
|
1272
|
-
const content = readFileSync2(
|
|
1344
|
+
const content = readFileSync2(path5, "utf8").trim();
|
|
1273
1345
|
return content || void 0;
|
|
1274
1346
|
} catch {
|
|
1275
1347
|
return void 0;
|
|
@@ -1278,7 +1350,7 @@ function readPromptFileIfPresent(path4) {
|
|
|
1278
1350
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
1279
1351
|
let dir = cwd;
|
|
1280
1352
|
while (true) {
|
|
1281
|
-
const content = readPromptFileIfPresent(
|
|
1353
|
+
const content = readPromptFileIfPresent(join4(dir, "AGENTS.md"));
|
|
1282
1354
|
if (content) return content;
|
|
1283
1355
|
const parent = dirname2(dir);
|
|
1284
1356
|
if (parent === dir) return void 0;
|
|
@@ -1287,17 +1359,17 @@ function nearestWorkspaceAgentsPrompt(cwd) {
|
|
|
1287
1359
|
}
|
|
1288
1360
|
function buildAppendedSystemPrompt(cwd) {
|
|
1289
1361
|
const parts = [];
|
|
1290
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
1291
|
-
const globalAgents = readPromptFileIfPresent(
|
|
1362
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config");
|
|
1363
|
+
const globalAgents = readPromptFileIfPresent(join4(configRoot, "opencode", "AGENTS.md"));
|
|
1292
1364
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
1293
1365
|
if (globalAgents) parts.push(globalAgents);
|
|
1294
1366
|
if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
|
|
1295
1367
|
const content = parts.join("\n\n");
|
|
1296
1368
|
if (!content) return void 0;
|
|
1297
|
-
const
|
|
1369
|
+
const path5 = join4(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
|
|
1298
1370
|
try {
|
|
1299
|
-
writeFileSync3(
|
|
1300
|
-
return
|
|
1371
|
+
writeFileSync3(path5, content, "utf8");
|
|
1372
|
+
return path5;
|
|
1301
1373
|
} catch (err) {
|
|
1302
1374
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
1303
1375
|
return void 0;
|
|
@@ -1692,6 +1764,24 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1692
1764
|
warnings
|
|
1693
1765
|
};
|
|
1694
1766
|
}
|
|
1767
|
+
if (!hasNewUserContent(options.prompt)) {
|
|
1768
|
+
log.info("doGenerate short-circuit: no new user content");
|
|
1769
|
+
return {
|
|
1770
|
+
content: [],
|
|
1771
|
+
finishReason: this.toFinishReason("stop"),
|
|
1772
|
+
usage: this.toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
1773
|
+
request: { body: { text: "" } },
|
|
1774
|
+
response: {
|
|
1775
|
+
id: generateId(),
|
|
1776
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1777
|
+
modelId: this.modelId
|
|
1778
|
+
},
|
|
1779
|
+
providerMetadata: {
|
|
1780
|
+
"claude-code": { synthetic: true, path: "no-new-user-content" }
|
|
1781
|
+
},
|
|
1782
|
+
warnings
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1695
1785
|
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
1696
1786
|
if (!hasPriorConversation) {
|
|
1697
1787
|
deleteClaudeSessionId(sk);
|
|
@@ -1743,11 +1833,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1743
1833
|
let thinkingText = "";
|
|
1744
1834
|
let resultMeta = {};
|
|
1745
1835
|
const toolCalls = [];
|
|
1836
|
+
let gotPartialEvents = false;
|
|
1746
1837
|
const result = await new Promise((resolve3, reject) => {
|
|
1747
1838
|
rl.on("line", (line) => {
|
|
1748
1839
|
if (!line.trim()) return;
|
|
1749
1840
|
try {
|
|
1750
|
-
const
|
|
1841
|
+
const outer = JSON.parse(line);
|
|
1842
|
+
const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
|
|
1843
|
+
if (outer.type === "stream_event") {
|
|
1844
|
+
gotPartialEvents = true;
|
|
1845
|
+
}
|
|
1751
1846
|
if (this.handleControlRequest(msg, proc)) {
|
|
1752
1847
|
return;
|
|
1753
1848
|
}
|
|
@@ -1756,7 +1851,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
1756
1851
|
setClaudeSessionId(sk, msg.session_id);
|
|
1757
1852
|
}
|
|
1758
1853
|
}
|
|
1759
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
1854
|
+
if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
|
|
1760
1855
|
for (const block of msg.message.content) {
|
|
1761
1856
|
if (block.type === "text" && block.text) {
|
|
1762
1857
|
responseText += block.text;
|
|
@@ -1975,6 +2070,24 @@ ${plan}
|
|
|
1975
2070
|
request: { body: { text: "" } }
|
|
1976
2071
|
};
|
|
1977
2072
|
}
|
|
2073
|
+
if (!hasNewUserContent(options.prompt)) {
|
|
2074
|
+
log.info("doStream short-circuit: no new user content");
|
|
2075
|
+
const stream2 = new ReadableStream({
|
|
2076
|
+
start(controller) {
|
|
2077
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
2078
|
+
controller.enqueue({
|
|
2079
|
+
type: "finish",
|
|
2080
|
+
finishReason: toFinishReason("stop"),
|
|
2081
|
+
usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
2082
|
+
providerMetadata: {
|
|
2083
|
+
"claude-code": { synthetic: true, path: "no-new-user-content" }
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
controller.close();
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
return { stream: stream2, request: { body: { text: "" } } };
|
|
2090
|
+
}
|
|
1978
2091
|
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
1979
2092
|
if (!hasPriorConversation) {
|
|
1980
2093
|
deleteClaudeSessionId(sk);
|
|
@@ -2141,12 +2254,17 @@ ${plan}
|
|
|
2141
2254
|
} catch {
|
|
2142
2255
|
}
|
|
2143
2256
|
};
|
|
2257
|
+
let gotPartialEvents = false;
|
|
2144
2258
|
const lineHandler = (line) => {
|
|
2145
2259
|
if (!line.trim()) return;
|
|
2146
2260
|
if (controllerClosed) return;
|
|
2147
2261
|
startResultFallback();
|
|
2148
2262
|
try {
|
|
2149
|
-
const
|
|
2263
|
+
const outer = JSON.parse(line);
|
|
2264
|
+
const msg = outer.type === "stream_event" && outer.event ? { ...outer.event, session_id: outer.session_id } : outer;
|
|
2265
|
+
if (outer.type === "stream_event") {
|
|
2266
|
+
gotPartialEvents = true;
|
|
2267
|
+
}
|
|
2150
2268
|
if (handleControlRequest(msg, proc)) {
|
|
2151
2269
|
return;
|
|
2152
2270
|
}
|
|
@@ -2338,7 +2456,7 @@ ${plan}
|
|
|
2338
2456
|
}
|
|
2339
2457
|
}
|
|
2340
2458
|
}
|
|
2341
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
2459
|
+
if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
|
|
2342
2460
|
const hasText = msg.message.content.some(
|
|
2343
2461
|
(b) => b.type === "text" && b.text
|
|
2344
2462
|
);
|
|
@@ -2822,7 +2940,7 @@ var defaultModels = {
|
|
|
2822
2940
|
|
|
2823
2941
|
// src/accounts.ts
|
|
2824
2942
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
2825
|
-
import
|
|
2943
|
+
import path4 from "path";
|
|
2826
2944
|
var BASE_PROVIDER_ID = "claude-code";
|
|
2827
2945
|
var DEFAULT_ACCOUNT = "default";
|
|
2828
2946
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -2860,7 +2978,7 @@ function expandHome(value) {
|
|
|
2860
2978
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
2861
2979
|
if (value === "~") return home ?? value;
|
|
2862
2980
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
2863
|
-
return home ?
|
|
2981
|
+
return home ? path4.join(home, value.slice(2)) : value;
|
|
2864
2982
|
}
|
|
2865
2983
|
return value;
|
|
2866
2984
|
}
|
|
@@ -2892,8 +3010,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
2892
3010
|
}
|
|
2893
3011
|
}
|
|
2894
3012
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
2895
|
-
const source =
|
|
2896
|
-
const target =
|
|
3013
|
+
const source = path4.join(sourceRoot, item);
|
|
3014
|
+
const target = path4.join(targetRoot, item);
|
|
2897
3015
|
let sourceStat;
|
|
2898
3016
|
try {
|
|
2899
3017
|
sourceStat = await lstat(source);
|
|
@@ -2904,8 +3022,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
2904
3022
|
const targetStat = await lstat(target);
|
|
2905
3023
|
if (targetStat.isSymbolicLink()) {
|
|
2906
3024
|
const current = await readlink(target);
|
|
2907
|
-
const resolvedCurrent =
|
|
2908
|
-
const resolvedSource =
|
|
3025
|
+
const resolvedCurrent = path4.resolve(path4.dirname(target), current);
|
|
3026
|
+
const resolvedSource = path4.resolve(source);
|
|
2909
3027
|
if (resolvedCurrent === resolvedSource) return;
|
|
2910
3028
|
}
|
|
2911
3029
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -2920,11 +3038,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
2920
3038
|
await symlink(source, target, type);
|
|
2921
3039
|
}
|
|
2922
3040
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
2923
|
-
const cacheRoot =
|
|
3041
|
+
const cacheRoot = path4.join(
|
|
2924
3042
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
2925
3043
|
"opencode-claude-code-plugin"
|
|
2926
3044
|
);
|
|
2927
|
-
const wrapperPath =
|
|
3045
|
+
const wrapperPath = path4.join(cacheRoot, `claude-${account}`);
|
|
2928
3046
|
const suffix = `@${account}`;
|
|
2929
3047
|
await mkdir(cacheRoot, { recursive: true });
|
|
2930
3048
|
const script = `#!/usr/bin/env bash
|
|
@@ -2964,14 +3082,14 @@ function titleizeAccount(account) {
|
|
|
2964
3082
|
|
|
2965
3083
|
// src/cleanup-stale.ts
|
|
2966
3084
|
import {
|
|
2967
|
-
existsSync as
|
|
3085
|
+
existsSync as existsSync3,
|
|
2968
3086
|
readFileSync as readFileSync3,
|
|
2969
3087
|
realpathSync,
|
|
2970
|
-
rmSync,
|
|
3088
|
+
rmSync as rmSync2,
|
|
2971
3089
|
writeFileSync as writeFileSync4
|
|
2972
3090
|
} from "fs";
|
|
2973
3091
|
import { homedir as homedir3 } from "os";
|
|
2974
|
-
import { join as
|
|
3092
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
2975
3093
|
import { fileURLToPath } from "url";
|
|
2976
3094
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
2977
3095
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -2979,18 +3097,18 @@ var alreadyRan = false;
|
|
|
2979
3097
|
function candidateCacheRoots() {
|
|
2980
3098
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
2981
3099
|
return [
|
|
2982
|
-
xdg ?
|
|
2983
|
-
|
|
2984
|
-
|
|
3100
|
+
xdg ? join5(xdg, "opencode") : null,
|
|
3101
|
+
join5(homedir3(), ".cache", "opencode"),
|
|
3102
|
+
join5(homedir3(), "Library", "Caches", "opencode")
|
|
2985
3103
|
].filter((p) => Boolean(p));
|
|
2986
3104
|
}
|
|
2987
3105
|
function userOpencodeJsonPath() {
|
|
2988
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
2989
|
-
return
|
|
3106
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
|
|
3107
|
+
return join5(xdgConfig, "opencode", "opencode.json");
|
|
2990
3108
|
}
|
|
2991
3109
|
function userIntendsToUseUnscoped() {
|
|
2992
3110
|
const cfg = userOpencodeJsonPath();
|
|
2993
|
-
if (!
|
|
3111
|
+
if (!existsSync3(cfg)) return false;
|
|
2994
3112
|
try {
|
|
2995
3113
|
const json = JSON.parse(readFileSync3(cfg, "utf8"));
|
|
2996
3114
|
const plugins = json.plugin;
|
|
@@ -3028,17 +3146,17 @@ function cleanupStaleUnscopedInstall() {
|
|
|
3028
3146
|
}
|
|
3029
3147
|
}
|
|
3030
3148
|
function cleanupOne(cacheRoot, ourDir) {
|
|
3031
|
-
if (!
|
|
3032
|
-
const stalePath =
|
|
3033
|
-
if (!
|
|
3149
|
+
if (!existsSync3(cacheRoot)) return;
|
|
3150
|
+
const stalePath = join5(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
3151
|
+
if (!existsSync3(stalePath)) return;
|
|
3034
3152
|
let realStalePath = stalePath;
|
|
3035
3153
|
try {
|
|
3036
3154
|
realStalePath = realpathSync(stalePath);
|
|
3037
3155
|
} catch {
|
|
3038
3156
|
}
|
|
3039
3157
|
if (ourDir && realStalePath === ourDir) return;
|
|
3040
|
-
const pkgJsonPath =
|
|
3041
|
-
if (!
|
|
3158
|
+
const pkgJsonPath = join5(stalePath, "package.json");
|
|
3159
|
+
if (!existsSync3(pkgJsonPath)) return;
|
|
3042
3160
|
let pkg = {};
|
|
3043
3161
|
try {
|
|
3044
3162
|
pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
@@ -3049,7 +3167,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
3049
3167
|
if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
|
|
3050
3168
|
log.info("cleanup-stale: removing unscoped install", { stalePath });
|
|
3051
3169
|
try {
|
|
3052
|
-
|
|
3170
|
+
rmSync2(stalePath, { recursive: true, force: true });
|
|
3053
3171
|
} catch (err) {
|
|
3054
3172
|
log.warn("cleanup-stale: rmSync failed", {
|
|
3055
3173
|
stalePath,
|
|
@@ -3057,8 +3175,8 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
3057
3175
|
});
|
|
3058
3176
|
return;
|
|
3059
3177
|
}
|
|
3060
|
-
const cachePkgJson =
|
|
3061
|
-
if (!
|
|
3178
|
+
const cachePkgJson = join5(cacheRoot, "package.json");
|
|
3179
|
+
if (!existsSync3(cachePkgJson)) return;
|
|
3062
3180
|
try {
|
|
3063
3181
|
const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
|
|
3064
3182
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
@@ -3275,12 +3393,12 @@ var server = async (input) => {
|
|
|
3275
3393
|
config.provider ??= {};
|
|
3276
3394
|
const expanded = await expandAccountProviders(config);
|
|
3277
3395
|
if (expanded) {
|
|
3278
|
-
const
|
|
3396
|
+
const registered2 = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
|
|
3279
3397
|
id,
|
|
3280
3398
|
name: p?.name ?? id,
|
|
3281
3399
|
cwd: p?.options?.cwd
|
|
3282
3400
|
}));
|
|
3283
|
-
log.notice("registered claude-code providers", { providers:
|
|
3401
|
+
log.notice("registered claude-code providers", { providers: registered2 });
|
|
3284
3402
|
return;
|
|
3285
3403
|
}
|
|
3286
3404
|
const existing = config.provider[PROVIDER_ID2];
|