@khalilgharbaoui/opencode-claude-code-plugin 0.49.0 → 0.51.0
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 +74 -1
- package/dist/index.js +666 -219
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +38 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/claude-code-language-model.ts
|
|
2
|
-
import { join as
|
|
2
|
+
import { join as join13 } from "path";
|
|
3
3
|
|
|
4
4
|
// src/ids.ts
|
|
5
5
|
import { randomInt } from "crypto";
|
|
@@ -572,6 +572,139 @@ function titleizeAccount(account) {
|
|
|
572
572
|
return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
// src/account-groups.ts
|
|
576
|
+
var DEFAULT_ACCOUNT_GROUP = "default";
|
|
577
|
+
function normalizeGroupName(value) {
|
|
578
|
+
return value.trim().toLowerCase();
|
|
579
|
+
}
|
|
580
|
+
var warnedUnknownAccounts = /* @__PURE__ */ new Set();
|
|
581
|
+
var warnedShape = false;
|
|
582
|
+
function resolveAccountGroups(value, knownAccounts) {
|
|
583
|
+
if (value === void 0 || value === null) return null;
|
|
584
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
585
|
+
if (!warnedShape) {
|
|
586
|
+
warnedShape = true;
|
|
587
|
+
log.warn(
|
|
588
|
+
"accountGroups must be an object mapping an account name to a group name; ignoring it",
|
|
589
|
+
{ type: Array.isArray(value) ? "array" : typeof value }
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const known = knownAccounts ? new Set(knownAccounts.map((account) => normalizeAccountName(String(account)))) : void 0;
|
|
595
|
+
const out = {};
|
|
596
|
+
for (const [rawAccount, rawGroup] of Object.entries(value)) {
|
|
597
|
+
const account = normalizeAccountName(rawAccount);
|
|
598
|
+
if (!account) continue;
|
|
599
|
+
if (typeof rawGroup !== "string") {
|
|
600
|
+
if (!warnedShape) {
|
|
601
|
+
warnedShape = true;
|
|
602
|
+
log.warn(
|
|
603
|
+
"accountGroups must be an object mapping an account name to a group name; ignoring it",
|
|
604
|
+
{ account, type: typeof rawGroup }
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const group = normalizeGroupName(rawGroup);
|
|
610
|
+
if (!group) continue;
|
|
611
|
+
if (known && !known.has(account)) {
|
|
612
|
+
if (!warnedUnknownAccounts.has(account)) {
|
|
613
|
+
warnedUnknownAccounts.add(account);
|
|
614
|
+
log.warn("accountGroups names an account that is not configured; ignoring it", {
|
|
615
|
+
account,
|
|
616
|
+
group
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
out[account] = group;
|
|
622
|
+
}
|
|
623
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
624
|
+
}
|
|
625
|
+
function accountGroup(account, groups) {
|
|
626
|
+
const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
627
|
+
return groups?.[name] ?? DEFAULT_ACCOUNT_GROUP;
|
|
628
|
+
}
|
|
629
|
+
function accountsShareGroup(a, b, groups) {
|
|
630
|
+
if (!groups) return true;
|
|
631
|
+
return accountGroup(a, groups) === accountGroup(b, groups);
|
|
632
|
+
}
|
|
633
|
+
function accountsInGroupOf(account, accounts, groups) {
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const raw of accounts ?? []) {
|
|
636
|
+
const name = normalizeAccountName(String(raw));
|
|
637
|
+
if (!name || out.includes(name)) continue;
|
|
638
|
+
if (!accountsShareGroup(account, name, groups)) continue;
|
|
639
|
+
out.push(name);
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
function describeAccountGroups(groups) {
|
|
644
|
+
if (!groups) return [];
|
|
645
|
+
return Object.entries(groups).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([account, group]) => `${account}=${group}`);
|
|
646
|
+
}
|
|
647
|
+
function accountProviderMap(accounts) {
|
|
648
|
+
const out = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const raw of accounts ?? []) {
|
|
650
|
+
const account = normalizeAccountName(String(raw));
|
|
651
|
+
if (!account) continue;
|
|
652
|
+
out.set(`${BASE_PROVIDER_ID}-${account}`, account);
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
var CONTEXT_PREFIX = "context=";
|
|
657
|
+
function sessionKeyContextBlob(sessionKey2) {
|
|
658
|
+
const parts = sessionKey2.split("::");
|
|
659
|
+
if (parts.length < 5) return void 0;
|
|
660
|
+
if (parts[2] === "compaction") return void 0;
|
|
661
|
+
const withoutEffort = parts[parts.length - 1].startsWith("effort=") ? parts.slice(0, -1) : parts;
|
|
662
|
+
if (withoutEffort.length < 5) return void 0;
|
|
663
|
+
const tail = withoutEffort.slice(4).join("::");
|
|
664
|
+
return tail.startsWith(CONTEXT_PREFIX) ? tail.slice(CONTEXT_PREFIX.length) : void 0;
|
|
665
|
+
}
|
|
666
|
+
function sessionKeyProvider(sessionKey2) {
|
|
667
|
+
const blob = sessionKeyContextBlob(sessionKey2);
|
|
668
|
+
if (blob === void 0) return void 0;
|
|
669
|
+
try {
|
|
670
|
+
const parsed = JSON.parse(blob);
|
|
671
|
+
const first = Array.isArray(parsed) ? parsed[0] : void 0;
|
|
672
|
+
return typeof first === "string" ? first : void 0;
|
|
673
|
+
} catch {
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function sessionKeyAccount(sessionKey2, providers) {
|
|
678
|
+
const provider = sessionKeyProvider(sessionKey2);
|
|
679
|
+
return provider === void 0 ? void 0 : providers.get(provider);
|
|
680
|
+
}
|
|
681
|
+
function blankAccountProvider(sessionKey2, providers) {
|
|
682
|
+
if (providers.size === 0) return sessionKey2;
|
|
683
|
+
const blob = sessionKeyContextBlob(sessionKey2);
|
|
684
|
+
if (blob === void 0) return sessionKey2;
|
|
685
|
+
let parsed;
|
|
686
|
+
try {
|
|
687
|
+
parsed = JSON.parse(blob);
|
|
688
|
+
} catch {
|
|
689
|
+
return sessionKey2;
|
|
690
|
+
}
|
|
691
|
+
if (!Array.isArray(parsed)) return sessionKey2;
|
|
692
|
+
const provider = parsed[0];
|
|
693
|
+
if (typeof provider !== "string" || !providers.has(provider)) return sessionKey2;
|
|
694
|
+
const blanked = [...parsed];
|
|
695
|
+
blanked[0] = "*";
|
|
696
|
+
const prefix = sessionKey2.slice(0, sessionKey2.length - blob.length);
|
|
697
|
+
return `${prefix}${JSON.stringify(blanked)}`;
|
|
698
|
+
}
|
|
699
|
+
var ACCOUNT_GROUP_MARKER = "\u258C **account group:**";
|
|
700
|
+
function formatAccountGroupNote(input) {
|
|
701
|
+
const source = normalizeAccountName(input.sourceAccount || DEFAULT_ACCOUNT);
|
|
702
|
+
const target = normalizeAccountName(input.targetAccount || DEFAULT_ACCOUNT);
|
|
703
|
+
return `
|
|
704
|
+
${ACCOUNT_GROUP_MARKER} this conversation was running on the Claude account "${source}" (group "${input.sourceGroup}") and is now on "${target}" (group "${input.targetGroup}"), so none of it was carried over: accountGroups keeps a conversation inside one group. This account is starting fresh and sees only your latest message. The conversation is still on "${source}"; switch back to it to continue where you stopped.
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
|
|
575
708
|
// src/types.ts
|
|
576
709
|
var READ_ONLY_PERMISSION_MODE = "read-only";
|
|
577
710
|
var DEFAULT_PROXY_TOOL_NAMES = [
|
|
@@ -1489,12 +1622,13 @@ function isAccountFailoverQuestionActive(input) {
|
|
|
1489
1622
|
if (input.candidates.length === 0) return false;
|
|
1490
1623
|
return input.opencodeHasQuestion;
|
|
1491
1624
|
}
|
|
1492
|
-
function failoverCandidates(accounts, source) {
|
|
1625
|
+
function failoverCandidates(accounts, source, groups) {
|
|
1493
1626
|
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
1494
1627
|
const out = [];
|
|
1495
1628
|
for (const raw of accounts ?? []) {
|
|
1496
1629
|
const name = normalizeAccountName(String(raw));
|
|
1497
1630
|
if (!name || name === from || out.includes(name)) continue;
|
|
1631
|
+
if (!accountsShareGroup(from, name, groups)) continue;
|
|
1498
1632
|
out.push(name);
|
|
1499
1633
|
}
|
|
1500
1634
|
return out;
|
|
@@ -3514,33 +3648,97 @@ function clearCompression(sessionKey2) {
|
|
|
3514
3648
|
}
|
|
3515
3649
|
|
|
3516
3650
|
// src/mcp-bridge.ts
|
|
3517
|
-
import * as
|
|
3518
|
-
import * as
|
|
3519
|
-
import * as
|
|
3651
|
+
import * as fs5 from "fs";
|
|
3652
|
+
import * as path6 from "path";
|
|
3653
|
+
import * as os3 from "os";
|
|
3520
3654
|
import * as crypto2 from "crypto";
|
|
3521
3655
|
import {
|
|
3522
3656
|
parse as parseJsonc,
|
|
3523
3657
|
printParseErrorCode
|
|
3524
3658
|
} from "jsonc-parser";
|
|
3659
|
+
|
|
3660
|
+
// src/mcp-auth.ts
|
|
3661
|
+
import * as fs4 from "fs";
|
|
3662
|
+
import * as os2 from "os";
|
|
3663
|
+
import * as path5 from "path";
|
|
3664
|
+
var EXPIRY_SKEW_SECONDS = 30;
|
|
3665
|
+
function opencodeStateDir() {
|
|
3666
|
+
const dataHome = process.env.XDG_DATA_HOME || path5.join(os2.homedir(), ".local", "share");
|
|
3667
|
+
return path5.join(dataHome, "opencode");
|
|
3668
|
+
}
|
|
3669
|
+
function readMcpAuthStore(stateDir) {
|
|
3670
|
+
const file = path5.join(stateDir, "mcp-auth.json");
|
|
3671
|
+
try {
|
|
3672
|
+
const stat = fs4.lstatSync(file);
|
|
3673
|
+
if (!stat.isFile()) {
|
|
3674
|
+
log.debug("ignoring opencode MCP auth store that is not a regular file", { path: file });
|
|
3675
|
+
return {};
|
|
3676
|
+
}
|
|
3677
|
+
if (typeof stat.uid === "number" && typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
3678
|
+
log.debug("ignoring opencode MCP auth store owned by another user", { path: file });
|
|
3679
|
+
return {};
|
|
3680
|
+
}
|
|
3681
|
+
const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
|
|
3682
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3683
|
+
log.debug("opencode MCP auth store has an unexpected shape", { path: file });
|
|
3684
|
+
return {};
|
|
3685
|
+
}
|
|
3686
|
+
return parsed;
|
|
3687
|
+
} catch {
|
|
3688
|
+
return {};
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
function selectBearerToken(store, serverName, serverUrl, now = Date.now()) {
|
|
3692
|
+
const entry = Object.prototype.hasOwnProperty.call(store, serverName) ? store[serverName] : void 0;
|
|
3693
|
+
if (!entry || typeof entry !== "object") return { token: null, refusal: "no-entry" };
|
|
3694
|
+
if (!entry.serverUrl || entry.serverUrl !== serverUrl) {
|
|
3695
|
+
return { token: null, refusal: "url-mismatch" };
|
|
3696
|
+
}
|
|
3697
|
+
const accessToken = entry.tokens?.accessToken;
|
|
3698
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
3699
|
+
return { token: null, refusal: "no-token" };
|
|
3700
|
+
}
|
|
3701
|
+
const expiresAt = entry.tokens?.expiresAt;
|
|
3702
|
+
if (typeof expiresAt === "number" && expiresAt - now / 1e3 < EXPIRY_SKEW_SECONDS) {
|
|
3703
|
+
return { token: null, refusal: "expired" };
|
|
3704
|
+
}
|
|
3705
|
+
return { token: accessToken, expiresAt: typeof expiresAt === "number" ? expiresAt : void 0 };
|
|
3706
|
+
}
|
|
3707
|
+
function buildFreshnessKey(expiresAt) {
|
|
3708
|
+
return expiresAt === void 0 ? "no-expiry" : String(expiresAt);
|
|
3709
|
+
}
|
|
3710
|
+
function hasAuthorizationHeader(headers) {
|
|
3711
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === "authorization");
|
|
3712
|
+
}
|
|
3713
|
+
function injectBearerHeaders(server2, accessToken) {
|
|
3714
|
+
const existing = server2.headers && typeof server2.headers === "object" && !Array.isArray(server2.headers) ? server2.headers : {};
|
|
3715
|
+
if (hasAuthorizationHeader(existing)) return { ...server2 };
|
|
3716
|
+
return {
|
|
3717
|
+
...server2,
|
|
3718
|
+
headers: { ...existing, Authorization: `Bearer ${accessToken}` }
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// src/mcp-bridge.ts
|
|
3525
3723
|
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
3526
3724
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3527
3725
|
function fileExists(p) {
|
|
3528
3726
|
try {
|
|
3529
|
-
return
|
|
3727
|
+
return fs5.statSync(p).isFile();
|
|
3530
3728
|
} catch {
|
|
3531
3729
|
return false;
|
|
3532
3730
|
}
|
|
3533
3731
|
}
|
|
3534
3732
|
function dirExists(p) {
|
|
3535
3733
|
try {
|
|
3536
|
-
return
|
|
3734
|
+
return fs5.statSync(p).isDirectory();
|
|
3537
3735
|
} catch {
|
|
3538
3736
|
return false;
|
|
3539
3737
|
}
|
|
3540
3738
|
}
|
|
3541
3739
|
function readAndParse(file) {
|
|
3542
3740
|
try {
|
|
3543
|
-
const raw =
|
|
3741
|
+
const raw = fs5.readFileSync(file, "utf8");
|
|
3544
3742
|
const errors = [];
|
|
3545
3743
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3546
3744
|
if (errors.length > 0) {
|
|
@@ -3576,14 +3774,14 @@ function deepMerge(target, source) {
|
|
|
3576
3774
|
}
|
|
3577
3775
|
function walkUp(opts) {
|
|
3578
3776
|
const out = [];
|
|
3579
|
-
let current =
|
|
3777
|
+
let current = path6.resolve(opts.start);
|
|
3580
3778
|
while (true) {
|
|
3581
3779
|
for (const target of opts.targets) {
|
|
3582
|
-
const candidate =
|
|
3780
|
+
const candidate = path6.join(current, target);
|
|
3583
3781
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3584
3782
|
}
|
|
3585
|
-
if (opts.stop && current ===
|
|
3586
|
-
const parent =
|
|
3783
|
+
if (opts.stop && current === path6.resolve(opts.stop)) break;
|
|
3784
|
+
const parent = path6.dirname(current);
|
|
3587
3785
|
if (parent === current) break;
|
|
3588
3786
|
current = parent;
|
|
3589
3787
|
}
|
|
@@ -3591,28 +3789,28 @@ function walkUp(opts) {
|
|
|
3591
3789
|
}
|
|
3592
3790
|
function detectWorktree(cwd) {
|
|
3593
3791
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3594
|
-
if (override) return
|
|
3595
|
-
let current =
|
|
3792
|
+
if (override) return path6.resolve(override);
|
|
3793
|
+
let current = path6.resolve(cwd);
|
|
3596
3794
|
while (true) {
|
|
3597
|
-
const gitPath =
|
|
3795
|
+
const gitPath = path6.join(current, ".git");
|
|
3598
3796
|
try {
|
|
3599
|
-
if (
|
|
3797
|
+
if (fs5.existsSync(gitPath)) return current;
|
|
3600
3798
|
} catch {
|
|
3601
3799
|
}
|
|
3602
|
-
const parent =
|
|
3800
|
+
const parent = path6.dirname(current);
|
|
3603
3801
|
if (parent === current) return void 0;
|
|
3604
3802
|
current = parent;
|
|
3605
3803
|
}
|
|
3606
3804
|
}
|
|
3607
3805
|
function globalConfigDir() {
|
|
3608
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3609
|
-
return
|
|
3806
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path6.join(os3.homedir(), ".config");
|
|
3807
|
+
return path6.join(xdg, "opencode");
|
|
3610
3808
|
}
|
|
3611
3809
|
function loadGlobalConfig(hostApi = "v1") {
|
|
3612
3810
|
const dir = globalConfigDir();
|
|
3613
3811
|
let merged = {};
|
|
3614
3812
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3615
|
-
const file =
|
|
3813
|
+
const file = path6.join(dir, name);
|
|
3616
3814
|
if (!fileExists(file)) continue;
|
|
3617
3815
|
const parsed = readAndParse(file);
|
|
3618
3816
|
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
@@ -3622,7 +3820,7 @@ function loadGlobalConfig(hostApi = "v1") {
|
|
|
3622
3820
|
function loadProjectFilesInDir(dir, hostApi = "v1") {
|
|
3623
3821
|
let merged = {};
|
|
3624
3822
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3625
|
-
const file =
|
|
3823
|
+
const file = path6.join(dir, name);
|
|
3626
3824
|
if (!fileExists(file)) continue;
|
|
3627
3825
|
const parsed = readAndParse(file);
|
|
3628
3826
|
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
@@ -3633,7 +3831,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3633
3831
|
const dirs = [];
|
|
3634
3832
|
const seen = /* @__PURE__ */ new Set();
|
|
3635
3833
|
const push = (p) => {
|
|
3636
|
-
const abs =
|
|
3834
|
+
const abs = path6.resolve(p);
|
|
3637
3835
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
3638
3836
|
seen.add(abs);
|
|
3639
3837
|
dirs.push(abs);
|
|
@@ -3647,9 +3845,9 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3647
3845
|
})) {
|
|
3648
3846
|
push(dir);
|
|
3649
3847
|
}
|
|
3650
|
-
const home =
|
|
3848
|
+
const home = os3.homedir();
|
|
3651
3849
|
if (home) {
|
|
3652
|
-
const homeDot =
|
|
3850
|
+
const homeDot = path6.join(home, ".opencode");
|
|
3653
3851
|
if (dirExists(homeDot)) push(homeDot);
|
|
3654
3852
|
}
|
|
3655
3853
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -3667,7 +3865,33 @@ function substituteEnvPlaceholders(source) {
|
|
|
3667
3865
|
}
|
|
3668
3866
|
return out;
|
|
3669
3867
|
}
|
|
3670
|
-
|
|
3868
|
+
var warnedExpiredBearer = /* @__PURE__ */ new Set();
|
|
3869
|
+
function withBearerToken(name, spec, url, auth) {
|
|
3870
|
+
if (!auth) return spec;
|
|
3871
|
+
const lookup = selectBearerToken(auth.store, name, url);
|
|
3872
|
+
if (lookup.token === null) {
|
|
3873
|
+
if (lookup.refusal === "expired" && !warnedExpiredBearer.has(name)) {
|
|
3874
|
+
warnedExpiredBearer.add(name);
|
|
3875
|
+
log.warn(
|
|
3876
|
+
"opencode's stored OAuth token for this MCP server has expired, so the bridged server carries no credential; re-authenticate it in opencode",
|
|
3877
|
+
{ server: name }
|
|
3878
|
+
);
|
|
3879
|
+
} else {
|
|
3880
|
+
log.debug("no opencode OAuth token for bridged MCP server", {
|
|
3881
|
+
server: name,
|
|
3882
|
+
reason: lookup.refusal
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
return spec;
|
|
3886
|
+
}
|
|
3887
|
+
log.info("bridged MCP server carries opencode's stored OAuth token", { server: name });
|
|
3888
|
+
return injectBearerHeaders(spec, lookup.token);
|
|
3889
|
+
}
|
|
3890
|
+
function bearerAuthContext(options) {
|
|
3891
|
+
if (!options?.oauthTokens) return void 0;
|
|
3892
|
+
return { store: readMcpAuthStore(options.stateDir ?? opencodeStateDir()) };
|
|
3893
|
+
}
|
|
3894
|
+
function translateServer(name, spec, auth) {
|
|
3671
3895
|
if (spec.enabled === false) return null;
|
|
3672
3896
|
const type = spec.type;
|
|
3673
3897
|
if (type === "local") {
|
|
@@ -3693,7 +3917,7 @@ function translateServer(name, spec) {
|
|
|
3693
3917
|
log.warn("skipping remote MCP server with no url", { name });
|
|
3694
3918
|
return null;
|
|
3695
3919
|
}
|
|
3696
|
-
|
|
3920
|
+
let out = {
|
|
3697
3921
|
type: "http",
|
|
3698
3922
|
url: spec.url
|
|
3699
3923
|
};
|
|
@@ -3702,7 +3926,7 @@ function translateServer(name, spec) {
|
|
|
3702
3926
|
spec.headers
|
|
3703
3927
|
);
|
|
3704
3928
|
}
|
|
3705
|
-
return out;
|
|
3929
|
+
return withBearerToken(name, out, spec.url, auth);
|
|
3706
3930
|
}
|
|
3707
3931
|
log.warn("skipping MCP server with unknown type", {
|
|
3708
3932
|
name,
|
|
@@ -3753,18 +3977,19 @@ function mergeMcp(target, source) {
|
|
|
3753
3977
|
return out;
|
|
3754
3978
|
}
|
|
3755
3979
|
var MCP_PENDING_STATUS = "pending";
|
|
3756
|
-
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, hostApi = "v1") {
|
|
3980
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, hostApi = "v1", auth) {
|
|
3757
3981
|
const {
|
|
3758
3982
|
servers: merged,
|
|
3759
3983
|
enabledServerNames: allEnabledServerNames,
|
|
3760
3984
|
hash
|
|
3761
|
-
} = mergeOpencodeMcp(cwd, runtimeStatus, hostApi);
|
|
3985
|
+
} = mergeOpencodeMcp(cwd, runtimeStatus, hostApi, auth);
|
|
3986
|
+
const authContext = bearerAuthContext(auth);
|
|
3762
3987
|
const servers = {};
|
|
3763
3988
|
const bridgedServerNames = [];
|
|
3764
3989
|
for (const [name, spec] of Object.entries(merged)) {
|
|
3765
3990
|
if (!spec || typeof spec !== "object") continue;
|
|
3766
3991
|
if (excludeServers?.has(name)) continue;
|
|
3767
|
-
const translated = translateServer(name, spec);
|
|
3992
|
+
const translated = translateServer(name, spec, authContext);
|
|
3768
3993
|
if (translated) {
|
|
3769
3994
|
servers[name] = translated;
|
|
3770
3995
|
bridgedServerNames.push(name);
|
|
@@ -3796,7 +4021,7 @@ function opencodeConfigLayers(cwd, hostApi = "v1") {
|
|
|
3796
4021
|
const projectDirs = [];
|
|
3797
4022
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3798
4023
|
for (const f of projectFiles) {
|
|
3799
|
-
const d =
|
|
4024
|
+
const d = path6.dirname(f);
|
|
3800
4025
|
if (!seenProjectDirs.has(d)) {
|
|
3801
4026
|
seenProjectDirs.add(d);
|
|
3802
4027
|
projectDirs.push(d);
|
|
@@ -3822,7 +4047,7 @@ function loadMergedOpencodeConfig(cwd, hostApi = "v1") {
|
|
|
3822
4047
|
for (const layer of opencodeConfigLayers(cwd, hostApi)) merged = mergeConfigLayer(merged, layer, hostApi);
|
|
3823
4048
|
return merged;
|
|
3824
4049
|
}
|
|
3825
|
-
function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1") {
|
|
4050
|
+
function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1", auth) {
|
|
3826
4051
|
let merged = {};
|
|
3827
4052
|
for (const layer of opencodeConfigLayers(cwd, hostApi)) {
|
|
3828
4053
|
const servers = extractMcpBlock(layer);
|
|
@@ -3846,7 +4071,19 @@ function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1") {
|
|
|
3846
4071
|
enabledServerNames.push(name);
|
|
3847
4072
|
}
|
|
3848
4073
|
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3849
|
-
const
|
|
4074
|
+
const hasher = crypto2.createHash("sha256").update(mergedBody);
|
|
4075
|
+
const authContext = bearerAuthContext(auth);
|
|
4076
|
+
if (authContext) {
|
|
4077
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
4078
|
+
if (!spec || typeof spec !== "object") continue;
|
|
4079
|
+
const server2 = spec;
|
|
4080
|
+
if (server2.type !== "remote" || typeof server2.url !== "string") continue;
|
|
4081
|
+
const lookup = selectBearerToken(authContext.store, name, server2.url);
|
|
4082
|
+
if (lookup.token === null) continue;
|
|
4083
|
+
hasher.update(`\0bearer:${name}:${buildFreshnessKey(lookup.expiresAt)}`);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
const hash = hasher.digest("hex").slice(0, 12);
|
|
3850
4087
|
return { servers: merged, enabledServerNames, hash };
|
|
3851
4088
|
}
|
|
3852
4089
|
function finishBridge(input) {
|
|
@@ -3863,13 +4100,13 @@ function finishBridge(input) {
|
|
|
3863
4100
|
}
|
|
3864
4101
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3865
4102
|
const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3866
|
-
const outPath =
|
|
4103
|
+
const outPath = path6.join(
|
|
3867
4104
|
pluginTmpDir(),
|
|
3868
4105
|
`mcp-${bodyDigest}.json`
|
|
3869
4106
|
);
|
|
3870
4107
|
try {
|
|
3871
4108
|
if (!fileExists(outPath)) {
|
|
3872
|
-
|
|
4109
|
+
fs5.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3873
4110
|
}
|
|
3874
4111
|
} catch (e) {
|
|
3875
4112
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -4441,7 +4678,7 @@ function forkSiblingSignature(sessionKey2) {
|
|
|
4441
4678
|
blanked[3] = "*";
|
|
4442
4679
|
return blanked.join("::");
|
|
4443
4680
|
}
|
|
4444
|
-
function modelSiblingSignature(sessionKey2) {
|
|
4681
|
+
function modelSiblingSignature(sessionKey2, accountProviders) {
|
|
4445
4682
|
const parts = sessionKey2.split("::");
|
|
4446
4683
|
if (parts.length < 5) return null;
|
|
4447
4684
|
if (parts[2] === "compaction") return null;
|
|
@@ -4449,7 +4686,8 @@ function modelSiblingSignature(sessionKey2) {
|
|
|
4449
4686
|
if (withoutEffort.length < 5) return null;
|
|
4450
4687
|
const blanked = [...withoutEffort];
|
|
4451
4688
|
blanked[1] = "*";
|
|
4452
|
-
|
|
4689
|
+
const signature = blanked.join("::");
|
|
4690
|
+
return accountProviders && accountProviders.size > 0 ? blankAccountProvider(signature, accountProviders) : signature;
|
|
4453
4691
|
}
|
|
4454
4692
|
function moveForkFingerprint(fromKey, toKey) {
|
|
4455
4693
|
const record = fingerprints.get(fromKey);
|
|
@@ -4536,17 +4774,17 @@ function tailIsReplyOnly(history, from) {
|
|
|
4536
4774
|
}
|
|
4537
4775
|
|
|
4538
4776
|
// src/session-resume-store.ts
|
|
4539
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as
|
|
4540
|
-
import { homedir as
|
|
4541
|
-
import { join as
|
|
4777
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
4778
|
+
import { homedir as homedir5 } from "os";
|
|
4779
|
+
import { join as join6 } from "path";
|
|
4542
4780
|
var MAX_RESUME_RECORDS = 256;
|
|
4543
4781
|
var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4544
4782
|
var records = null;
|
|
4545
4783
|
var pathOverride = null;
|
|
4546
4784
|
function resumeStorePath() {
|
|
4547
4785
|
if (pathOverride) return pathOverride;
|
|
4548
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
4549
|
-
return
|
|
4786
|
+
const stateRoot = process.env.XDG_STATE_HOME || join6(homedir5(), ".local", "state");
|
|
4787
|
+
return join6(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
|
|
4550
4788
|
}
|
|
4551
4789
|
function isDigest(value) {
|
|
4552
4790
|
const entry = value;
|
|
@@ -4554,13 +4792,13 @@ function isDigest(value) {
|
|
|
4554
4792
|
}
|
|
4555
4793
|
function isRecord2(value) {
|
|
4556
4794
|
const record = value;
|
|
4557
|
-
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest));
|
|
4795
|
+
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest)) && (record.configDir === void 0 || typeof record.configDir === "string") && (record.account === void 0 || typeof record.account === "string");
|
|
4558
4796
|
}
|
|
4559
4797
|
function load() {
|
|
4560
4798
|
if (records) return records;
|
|
4561
4799
|
records = /* @__PURE__ */ new Map();
|
|
4562
4800
|
try {
|
|
4563
|
-
const parsed = JSON.parse(
|
|
4801
|
+
const parsed = JSON.parse(readFileSync3(resumeStorePath(), "utf8"));
|
|
4564
4802
|
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
4565
4803
|
if (entries && typeof entries === "object") {
|
|
4566
4804
|
const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
|
|
@@ -4575,7 +4813,7 @@ function save(update) {
|
|
|
4575
4813
|
const file = resumeStorePath();
|
|
4576
4814
|
const merged = /* @__PURE__ */ new Map();
|
|
4577
4815
|
try {
|
|
4578
|
-
const onDisk = JSON.parse(
|
|
4816
|
+
const onDisk = JSON.parse(readFileSync3(file, "utf8"))?.sessions;
|
|
4579
4817
|
if (onDisk && typeof onDisk === "object") {
|
|
4580
4818
|
for (const [key, record] of Object.entries(onDisk)) {
|
|
4581
4819
|
if (isRecord2(record)) merged.set(key, record);
|
|
@@ -4588,7 +4826,7 @@ function save(update) {
|
|
|
4588
4826
|
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
|
|
4589
4827
|
records = new Map(kept);
|
|
4590
4828
|
try {
|
|
4591
|
-
mkdirSync3(
|
|
4829
|
+
mkdirSync3(join6(file, ".."), { recursive: true, mode: 448 });
|
|
4592
4830
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
4593
4831
|
writeFileSync3(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
4594
4832
|
mode: 384
|
|
@@ -4600,17 +4838,27 @@ function save(update) {
|
|
|
4600
4838
|
});
|
|
4601
4839
|
}
|
|
4602
4840
|
}
|
|
4603
|
-
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
|
|
4841
|
+
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where) {
|
|
4604
4842
|
const chain = conversationDigests(prompt);
|
|
4605
4843
|
if (chain.length === 0) return;
|
|
4606
4844
|
const shape2 = conversationDigests(prompt, { assistantContent: false });
|
|
4845
|
+
const configDir = where?.configDir;
|
|
4846
|
+
const account = where?.account;
|
|
4607
4847
|
const current = load().get(sessionKey2);
|
|
4608
|
-
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4848
|
+
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.configDir === configDir && current.account === account && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4609
4849
|
return;
|
|
4610
4850
|
}
|
|
4611
4851
|
save((merged) => {
|
|
4612
4852
|
merged.delete(sessionKey2);
|
|
4613
|
-
merged.set(sessionKey2, {
|
|
4853
|
+
merged.set(sessionKey2, {
|
|
4854
|
+
claudeSessionId,
|
|
4855
|
+
cliPath,
|
|
4856
|
+
chain,
|
|
4857
|
+
shape: shape2,
|
|
4858
|
+
...configDir === void 0 ? {} : { configDir },
|
|
4859
|
+
...account === void 0 ? {} : { account },
|
|
4860
|
+
updatedAt: Date.now()
|
|
4861
|
+
});
|
|
4614
4862
|
});
|
|
4615
4863
|
}
|
|
4616
4864
|
function forgetResumePoint(sessionKey2) {
|
|
@@ -4652,18 +4900,31 @@ function findResumePoint(opts) {
|
|
|
4652
4900
|
return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
|
|
4653
4901
|
}
|
|
4654
4902
|
function findSiblingResumePoint(opts) {
|
|
4655
|
-
const
|
|
4903
|
+
const providers = opts.accountProviders;
|
|
4904
|
+
const signature = modelSiblingSignature(opts.sessionKey, providers);
|
|
4656
4905
|
if (!signature) {
|
|
4657
4906
|
opts.onRefused?.("no-sibling-possible");
|
|
4658
4907
|
return void 0;
|
|
4659
4908
|
}
|
|
4909
|
+
const ownAccount = providers ? sessionKeyAccount(opts.sessionKey, providers) : void 0;
|
|
4660
4910
|
let best;
|
|
4661
4911
|
let bestUpdatedAt = -1;
|
|
4662
4912
|
let refusal;
|
|
4663
4913
|
for (const [key, record] of load()) {
|
|
4664
4914
|
if (key === opts.sessionKey) continue;
|
|
4665
|
-
if (modelSiblingSignature(key) !== signature) continue;
|
|
4666
|
-
|
|
4915
|
+
if (modelSiblingSignature(key, providers) !== signature) continue;
|
|
4916
|
+
const siblingAccount = providers ? sessionKeyAccount(key, providers) : void 0;
|
|
4917
|
+
const crossAccount = ownAccount !== void 0 && siblingAccount !== void 0 && siblingAccount !== ownAccount;
|
|
4918
|
+
if (crossAccount) {
|
|
4919
|
+
if (opts.allowAccount && !opts.allowAccount(siblingAccount)) {
|
|
4920
|
+
refusal = "sibling-another-account-group";
|
|
4921
|
+
continue;
|
|
4922
|
+
}
|
|
4923
|
+
if (!record.configDir) {
|
|
4924
|
+
refusal = "sibling-account-dir-unknown";
|
|
4925
|
+
continue;
|
|
4926
|
+
}
|
|
4927
|
+
} else if (record.cliPath !== opts.cliPath) {
|
|
4667
4928
|
refusal = "sibling-another-claude-binary";
|
|
4668
4929
|
continue;
|
|
4669
4930
|
}
|
|
@@ -4677,7 +4938,7 @@ function findSiblingResumePoint(opts) {
|
|
|
4677
4938
|
refusal = "sibling-still-writing";
|
|
4678
4939
|
continue;
|
|
4679
4940
|
}
|
|
4680
|
-
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4941
|
+
if (!existsSync2(opts.transcriptPath(record.claudeSessionId, record.configDir))) {
|
|
4681
4942
|
refusal = "sibling-transcript-gone";
|
|
4682
4943
|
continue;
|
|
4683
4944
|
}
|
|
@@ -4686,7 +4947,8 @@ function findSiblingResumePoint(opts) {
|
|
|
4686
4947
|
best = {
|
|
4687
4948
|
claudeSessionId: record.claudeSessionId,
|
|
4688
4949
|
siblingKey: key,
|
|
4689
|
-
matched: record.chain.length
|
|
4950
|
+
matched: record.chain.length,
|
|
4951
|
+
...crossAccount ? { siblingAccount, siblingConfigDir: record.configDir } : {}
|
|
4690
4952
|
};
|
|
4691
4953
|
}
|
|
4692
4954
|
}
|
|
@@ -4697,6 +4959,35 @@ function findSiblingResumePoint(opts) {
|
|
|
4697
4959
|
}
|
|
4698
4960
|
return best;
|
|
4699
4961
|
}
|
|
4962
|
+
function findForeignAccountSibling(opts) {
|
|
4963
|
+
const providers = opts.accountProviders;
|
|
4964
|
+
if (providers.size === 0) return void 0;
|
|
4965
|
+
const signature = modelSiblingSignature(opts.sessionKey, providers);
|
|
4966
|
+
if (!signature) return void 0;
|
|
4967
|
+
const ownAccount = sessionKeyAccount(opts.sessionKey, providers);
|
|
4968
|
+
if (ownAccount === void 0) return void 0;
|
|
4969
|
+
const consider = (key) => {
|
|
4970
|
+
if (key === opts.sessionKey) return void 0;
|
|
4971
|
+
if (modelSiblingSignature(key, providers) !== signature) return void 0;
|
|
4972
|
+
const account = sessionKeyAccount(key, providers);
|
|
4973
|
+
if (account === void 0 || account === ownAccount) return void 0;
|
|
4974
|
+
return { siblingKey: key, account };
|
|
4975
|
+
};
|
|
4976
|
+
for (const key of opts.extraKeys ?? []) {
|
|
4977
|
+
const hit = consider(key);
|
|
4978
|
+
if (hit) return hit;
|
|
4979
|
+
}
|
|
4980
|
+
let found;
|
|
4981
|
+
let foundUpdatedAt = -1;
|
|
4982
|
+
for (const [key, record] of load()) {
|
|
4983
|
+
const hit = consider(key);
|
|
4984
|
+
if (hit && record.updatedAt >= foundUpdatedAt) {
|
|
4985
|
+
found = hit;
|
|
4986
|
+
foundUpdatedAt = record.updatedAt;
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
return found;
|
|
4990
|
+
}
|
|
4700
4991
|
|
|
4701
4992
|
// src/permission-presets.ts
|
|
4702
4993
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
@@ -5369,6 +5660,9 @@ async function deleteActiveProcessAndWait(key, options = {}) {
|
|
|
5369
5660
|
function getClaudeSessionId(key) {
|
|
5370
5661
|
return claudeSessions.get(key);
|
|
5371
5662
|
}
|
|
5663
|
+
function listClaudeSessionKeys() {
|
|
5664
|
+
return claudeSessions.keys();
|
|
5665
|
+
}
|
|
5372
5666
|
var MAX_CLAUDE_SESSION_ENTRIES = 64;
|
|
5373
5667
|
function claudeSessionIsBusy(key) {
|
|
5374
5668
|
if (activeProcesses.has(key)) return true;
|
|
@@ -6037,7 +6331,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
6037
6331
|
// src/diagnostic-bundle.ts
|
|
6038
6332
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
6039
6333
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
6040
|
-
import { homedir as
|
|
6334
|
+
import { homedir as homedir6 } from "os";
|
|
6041
6335
|
|
|
6042
6336
|
// src/log-messages.ts
|
|
6043
6337
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -6048,6 +6342,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6048
6342
|
"MCP server asked for operator input and was declined: a headless Claude Code session cannot prompt, so its elicitation can only be refused. Run that server's flow in Claude Code directly, or configure it not to elicit.",
|
|
6049
6343
|
"MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
|
|
6050
6344
|
"account failover declined; ending the turn",
|
|
6345
|
+
"accountGroups must be an object mapping an account name to a group name; ignoring it",
|
|
6346
|
+
"accountGroups names an account that is not configured; ignoring it",
|
|
6051
6347
|
"agent effort override refused: unknown level",
|
|
6052
6348
|
"agent model override refused: unknown model",
|
|
6053
6349
|
"agent prompt cache ttl refused: unknown value",
|
|
@@ -6133,6 +6429,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6133
6429
|
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
6134
6430
|
"no stdout after envelope write; respawning claude process to resume conversation",
|
|
6135
6431
|
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
|
|
6432
|
+
"opencode's stored OAuth token for this MCP server has expired, so the bridged server carries no credential; re-authenticate it in opencode",
|
|
6136
6433
|
"pending proxy call arrived after stream close; rejecting",
|
|
6137
6434
|
'permissionMode "plan" is enforced: claude cannot edit files or run commands, and --dangerously-skip-permissions is deliberately not passed so it stays that way. Headless Claude Code is not offered an ExitPlanMode tool, so nothing releases plan mode mid-session; approving a plan in chat does not unlock writes. Leaving plan mode means changing the config and restarting opencode.',
|
|
6138
6435
|
`permissionPreset "read-only": this claude CLI has no --permission-prompts (2.1.263+), so permission requests are denied by the plugin's own can_use_tool handler instead of by the CLI. Same outcome, one layer fewer.`,
|
|
@@ -6157,6 +6454,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6157
6454
|
"replaying stdout the child emitted between turns",
|
|
6158
6455
|
"replaying the conversation as text: no claude session was available to continue",
|
|
6159
6456
|
"replaying this conversation as text on the other account",
|
|
6457
|
+
"replaying this conversation rather than carrying it across accounts",
|
|
6160
6458
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
6161
6459
|
"resuming the claude session from before the restart instead of replaying it",
|
|
6162
6460
|
"shared Claude capability already exists; leaving untouched",
|
|
@@ -6164,6 +6462,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6164
6462
|
"skipping local MCP server with no command",
|
|
6165
6463
|
"skipping remote MCP server with no url",
|
|
6166
6464
|
"stale scratch directory sweep failed",
|
|
6465
|
+
"starting fresh on this account: it is in another account group",
|
|
6167
6466
|
"task_batch resolving with child results missing",
|
|
6168
6467
|
"the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
|
|
6169
6468
|
"the interactive turn did not take a forwarded user message",
|
|
@@ -6299,7 +6598,13 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
6299
6598
|
// doctor table already prints the same class of value (providers, accounts,
|
|
6300
6599
|
// proxyTools, MCP servers), so a bundle is no more revealing than the report
|
|
6301
6600
|
// it is attached to.
|
|
6601
|
+
// An account name and a group name are both operator-chosen labels of the
|
|
6602
|
+
// same class as `accounts`, which the doctor table already prints a column
|
|
6603
|
+
// of; `from` is already here as the account a conversation came from.
|
|
6604
|
+
account: "name",
|
|
6302
6605
|
accounts: "name",
|
|
6606
|
+
accountGroups: "name",
|
|
6607
|
+
group: "name",
|
|
6303
6608
|
agent: "name",
|
|
6304
6609
|
bundled: "name",
|
|
6305
6610
|
excluded: "name",
|
|
@@ -6398,7 +6703,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
6398
6703
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
6399
6704
|
}
|
|
6400
6705
|
function createRedactionContext(options = {}) {
|
|
6401
|
-
const home = options.home ??
|
|
6706
|
+
const home = options.home ?? homedir6();
|
|
6402
6707
|
const homes = /* @__PURE__ */ new Set();
|
|
6403
6708
|
if (home) homes.add(home);
|
|
6404
6709
|
if (home) {
|
|
@@ -6583,8 +6888,8 @@ function redactLogLine(raw, context) {
|
|
|
6583
6888
|
if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
|
|
6584
6889
|
return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
|
|
6585
6890
|
}
|
|
6586
|
-
function readLogTail(
|
|
6587
|
-
const fd = openSync(
|
|
6891
|
+
function readLogTail(path15, maxBytes = BUNDLE_TAIL_BYTES) {
|
|
6892
|
+
const fd = openSync(path15, "r");
|
|
6588
6893
|
try {
|
|
6589
6894
|
const size = fstatSync(fd).size;
|
|
6590
6895
|
const length = Math.min(size, maxBytes);
|
|
@@ -6789,14 +7094,14 @@ async function fetchPlanUsage(cliPath, options = {}) {
|
|
|
6789
7094
|
}
|
|
6790
7095
|
|
|
6791
7096
|
// src/stale-build.ts
|
|
6792
|
-
import * as
|
|
6793
|
-
import * as
|
|
7097
|
+
import * as fs7 from "fs";
|
|
7098
|
+
import * as path8 from "path";
|
|
6794
7099
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6795
7100
|
|
|
6796
7101
|
// src/startup-diagnostics.ts
|
|
6797
7102
|
import { execFile as execFile3 } from "child_process";
|
|
6798
|
-
import * as
|
|
6799
|
-
import * as
|
|
7103
|
+
import * as fs6 from "fs";
|
|
7104
|
+
import * as path7 from "path";
|
|
6800
7105
|
import { promisify as promisify2 } from "util";
|
|
6801
7106
|
import { fileURLToPath } from "url";
|
|
6802
7107
|
|
|
@@ -6831,8 +7136,8 @@ var cachedPluginVersion;
|
|
|
6831
7136
|
function pluginVersion() {
|
|
6832
7137
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
6833
7138
|
try {
|
|
6834
|
-
const here =
|
|
6835
|
-
const raw =
|
|
7139
|
+
const here = path7.dirname(fileURLToPath(import.meta.url));
|
|
7140
|
+
const raw = fs6.readFileSync(path7.join(here, "..", "package.json"), "utf8");
|
|
6836
7141
|
const version = JSON.parse(raw).version;
|
|
6837
7142
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
6838
7143
|
} catch {
|
|
@@ -6856,7 +7161,7 @@ var opencodeVersionProbe;
|
|
|
6856
7161
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
6857
7162
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
6858
7163
|
opencodeVersionProbe = (async () => {
|
|
6859
|
-
if (!
|
|
7164
|
+
if (!path7.basename(execPath).toLowerCase().includes("opencode")) {
|
|
6860
7165
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
6861
7166
|
return void 0;
|
|
6862
7167
|
}
|
|
@@ -6925,6 +7230,15 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
6925
7230
|
cwd,
|
|
6926
7231
|
providers: Object.keys(providers),
|
|
6927
7232
|
accounts,
|
|
7233
|
+
// One guard per install, so the first provider that declares one answers
|
|
7234
|
+
// for all of them; it is validated against the accounts the expansion
|
|
7235
|
+
// produced, which is the same list every provider carries.
|
|
7236
|
+
accountGroups: describeAccountGroups(
|
|
7237
|
+
resolveAccountGroups(
|
|
7238
|
+
firstOption(providers, "accountGroups"),
|
|
7239
|
+
stringList(firstOption(providers, "failoverAccounts"))
|
|
7240
|
+
)
|
|
7241
|
+
),
|
|
6928
7242
|
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
6929
7243
|
mcpServers,
|
|
6930
7244
|
permissionPresets: Object.entries(providers).map(
|
|
@@ -7085,10 +7399,10 @@ function defaultEntryPath() {
|
|
|
7085
7399
|
}
|
|
7086
7400
|
function readOnDiskBuild(entryPath) {
|
|
7087
7401
|
try {
|
|
7088
|
-
const raw =
|
|
7402
|
+
const raw = fs7.readFileSync(path8.join(path8.dirname(entryPath), "..", "package.json"), "utf8");
|
|
7089
7403
|
const version = JSON.parse(raw).version;
|
|
7090
7404
|
if (typeof version !== "string" || version.length === 0) return void 0;
|
|
7091
|
-
const stat =
|
|
7405
|
+
const stat = fs7.statSync(entryPath);
|
|
7092
7406
|
return { version, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
7093
7407
|
} catch {
|
|
7094
7408
|
return void 0;
|
|
@@ -7099,7 +7413,7 @@ function recordLoadedBuild() {
|
|
|
7099
7413
|
let mtimeMs;
|
|
7100
7414
|
let size;
|
|
7101
7415
|
try {
|
|
7102
|
-
const stat =
|
|
7416
|
+
const stat = fs7.statSync(entryPath);
|
|
7103
7417
|
mtimeMs = stat.mtimeMs;
|
|
7104
7418
|
size = stat.size;
|
|
7105
7419
|
} catch {
|
|
@@ -7197,6 +7511,9 @@ function formatDoctorReport(report) {
|
|
|
7197
7511
|
lines.push(`| cwd | \`${report.cwd.resolved}\` (${report.cwd.source}) |`);
|
|
7198
7512
|
lines.push(`| providers | ${list(report.providers)} |`);
|
|
7199
7513
|
lines.push(`| accounts | ${list(report.accounts)} |`);
|
|
7514
|
+
lines.push(
|
|
7515
|
+
`| accountGroups | ${report.accountGroups.length > 0 ? list(report.accountGroups) : "none (every account is one group)"} |`
|
|
7516
|
+
);
|
|
7200
7517
|
lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
|
|
7201
7518
|
lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
|
|
7202
7519
|
lines.push(`| permissionPreset | ${describePermissionPresets(report.permissionPresets)} |`);
|
|
@@ -7450,6 +7767,7 @@ async function gatherDoctorReport(options) {
|
|
|
7450
7767
|
cwd: base.cwd,
|
|
7451
7768
|
providers: base.providers,
|
|
7452
7769
|
accounts: base.accounts,
|
|
7770
|
+
accountGroups: base.accountGroups,
|
|
7453
7771
|
proxyTools: base.proxyTools,
|
|
7454
7772
|
mcpServers: base.mcpServers,
|
|
7455
7773
|
permissionPresets: base.permissionPresets,
|
|
@@ -7537,7 +7855,7 @@ Could not build the report: ${message}`;
|
|
|
7537
7855
|
|
|
7538
7856
|
// src/agent-models.ts
|
|
7539
7857
|
import { readFile, readdir } from "fs/promises";
|
|
7540
|
-
import
|
|
7858
|
+
import path9 from "path";
|
|
7541
7859
|
|
|
7542
7860
|
// src/models.ts
|
|
7543
7861
|
var PROVIDER_ID = "claude-code";
|
|
@@ -8103,13 +8421,13 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
8103
8421
|
const name = entry.slice(0, -3);
|
|
8104
8422
|
if (records2[name]) continue;
|
|
8105
8423
|
try {
|
|
8106
|
-
const text = await readFile(
|
|
8424
|
+
const text = await readFile(path9.join(directory, entry), "utf8");
|
|
8107
8425
|
records2[name] = parseAgentFrontmatter(text);
|
|
8108
8426
|
const prompt = agentMarkdownBody(text);
|
|
8109
8427
|
if (prompt) records2[name].prompt = prompt;
|
|
8110
8428
|
} catch (err) {
|
|
8111
8429
|
log.debug("failed to read agent markdown", {
|
|
8112
|
-
file:
|
|
8430
|
+
file: path9.join(directory, entry),
|
|
8113
8431
|
error: String(err)
|
|
8114
8432
|
});
|
|
8115
8433
|
}
|
|
@@ -8121,12 +8439,12 @@ function agentDirectories(home, projectDirectory) {
|
|
|
8121
8439
|
const directories = [];
|
|
8122
8440
|
if (projectDirectory) {
|
|
8123
8441
|
for (const name of AGENT_DIR_NAMES) {
|
|
8124
|
-
directories.push(
|
|
8442
|
+
directories.push(path9.join(projectDirectory, ".opencode", name));
|
|
8125
8443
|
}
|
|
8126
8444
|
}
|
|
8127
8445
|
if (home) {
|
|
8128
8446
|
for (const name of AGENT_DIR_NAMES) {
|
|
8129
|
-
directories.push(
|
|
8447
|
+
directories.push(path9.join(home, ".config", "opencode", name));
|
|
8130
8448
|
}
|
|
8131
8449
|
}
|
|
8132
8450
|
return directories;
|
|
@@ -8294,6 +8612,7 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
8294
8612
|
SILENT_TURN_MARKER,
|
|
8295
8613
|
FAILOVER_MARKER,
|
|
8296
8614
|
ACCOUNT_BLOCK_MARKER,
|
|
8615
|
+
ACCOUNT_GROUP_MARKER,
|
|
8297
8616
|
USAGE_LIMIT_MARKER,
|
|
8298
8617
|
MODEL_FALLBACK_MARKER,
|
|
8299
8618
|
UNATTENDED_REPLAY_MARKER,
|
|
@@ -8442,6 +8761,17 @@ function clipWithMarker(text, max) {
|
|
|
8442
8761
|
return `${text.slice(0, max)}
|
|
8443
8762
|
\u2026[truncated ${text.length - max} chars]`;
|
|
8444
8763
|
}
|
|
8764
|
+
var MAX_REPLAY_MESSAGE_CHARS = 2e3;
|
|
8765
|
+
var MAX_REPLAY_LAST_REPLY_CHARS = 12e3;
|
|
8766
|
+
var MAX_REPLAY_HEAD_CHARS = 1e3;
|
|
8767
|
+
function clipMiddle(text, max) {
|
|
8768
|
+
if (text.length <= max) return text;
|
|
8769
|
+
const head = Math.min(Math.floor(max / 4), MAX_REPLAY_HEAD_CHARS);
|
|
8770
|
+
const tail = max - head;
|
|
8771
|
+
return `${text.slice(0, head)}
|
|
8772
|
+
\u2026[${text.length - max} chars omitted]\u2026
|
|
8773
|
+
${text.slice(-tail)}`;
|
|
8774
|
+
}
|
|
8445
8775
|
function renderToolInput(input) {
|
|
8446
8776
|
let raw;
|
|
8447
8777
|
try {
|
|
@@ -8510,13 +8840,20 @@ function compactConversationHistory(prompt, opts = {}) {
|
|
|
8510
8840
|
return null;
|
|
8511
8841
|
}
|
|
8512
8842
|
const historyParts = [];
|
|
8843
|
+
let lastAssistant = -1;
|
|
8844
|
+
for (let i = conversationMessages.length - 2; i >= 0; i--) {
|
|
8845
|
+
if (conversationMessages[i].role === "assistant") {
|
|
8846
|
+
lastAssistant = i;
|
|
8847
|
+
break;
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8513
8850
|
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
8514
8851
|
const msg = conversationMessages[i];
|
|
8515
8852
|
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
8516
8853
|
const { text } = renderMessageContentForCompaction(msg);
|
|
8517
8854
|
if (text.trim()) {
|
|
8518
|
-
const
|
|
8519
|
-
historyParts.push(`${role}: ${
|
|
8855
|
+
const max = i === lastAssistant ? MAX_REPLAY_LAST_REPLY_CHARS : MAX_REPLAY_MESSAGE_CHARS;
|
|
8856
|
+
historyParts.push(`${role}: ${clipMiddle(text, max)}`);
|
|
8520
8857
|
}
|
|
8521
8858
|
}
|
|
8522
8859
|
if (historyParts.length === 0) {
|
|
@@ -8726,14 +9063,14 @@ function getTrailingUserMessages(prompt, opts = {}) {
|
|
|
8726
9063
|
|
|
8727
9064
|
// src/skill-bridge.ts
|
|
8728
9065
|
import * as crypto3 from "crypto";
|
|
8729
|
-
import * as
|
|
8730
|
-
import * as
|
|
8731
|
-
import * as
|
|
9066
|
+
import * as fs8 from "fs";
|
|
9067
|
+
import * as os4 from "os";
|
|
9068
|
+
import * as path10 from "path";
|
|
8732
9069
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8733
9070
|
function bundledSkillsDir() {
|
|
8734
9071
|
try {
|
|
8735
9072
|
const here = fileURLToPath3(import.meta.url);
|
|
8736
|
-
const dir =
|
|
9073
|
+
const dir = path10.resolve(path10.dirname(here), "..", "skills");
|
|
8737
9074
|
return dirExists2(dir) ? dir : null;
|
|
8738
9075
|
} catch {
|
|
8739
9076
|
return null;
|
|
@@ -8741,16 +9078,16 @@ function bundledSkillsDir() {
|
|
|
8741
9078
|
}
|
|
8742
9079
|
function dirExists2(p) {
|
|
8743
9080
|
try {
|
|
8744
|
-
return
|
|
9081
|
+
return fs8.statSync(p).isDirectory();
|
|
8745
9082
|
} catch {
|
|
8746
9083
|
return false;
|
|
8747
9084
|
}
|
|
8748
9085
|
}
|
|
8749
9086
|
function realPath(p) {
|
|
8750
9087
|
try {
|
|
8751
|
-
return
|
|
9088
|
+
return fs8.realpathSync(p);
|
|
8752
9089
|
} catch {
|
|
8753
|
-
return
|
|
9090
|
+
return path10.resolve(p);
|
|
8754
9091
|
}
|
|
8755
9092
|
}
|
|
8756
9093
|
function envEnabled(name) {
|
|
@@ -8771,10 +9108,10 @@ function declaredSkillName(source) {
|
|
|
8771
9108
|
return SAFE_SKILL_NAME.test(name) ? name : null;
|
|
8772
9109
|
}
|
|
8773
9110
|
function readSkill(root, entry) {
|
|
8774
|
-
const dir =
|
|
9111
|
+
const dir = path10.join(root, entry);
|
|
8775
9112
|
let source;
|
|
8776
9113
|
try {
|
|
8777
|
-
source =
|
|
9114
|
+
source = fs8.readFileSync(path10.join(dir, "SKILL.md"), "utf8");
|
|
8778
9115
|
} catch {
|
|
8779
9116
|
return null;
|
|
8780
9117
|
}
|
|
@@ -8792,47 +9129,47 @@ function skillRoots(cwd) {
|
|
|
8792
9129
|
const roots = [];
|
|
8793
9130
|
const seen = /* @__PURE__ */ new Set();
|
|
8794
9131
|
const push = (p) => {
|
|
8795
|
-
const abs =
|
|
9132
|
+
const abs = path10.resolve(p);
|
|
8796
9133
|
if (seen.has(abs)) return;
|
|
8797
9134
|
seen.add(abs);
|
|
8798
9135
|
if (dirExists2(abs)) roots.push(abs);
|
|
8799
9136
|
};
|
|
8800
|
-
const home =
|
|
8801
|
-
const homeDir = home ?
|
|
8802
|
-
let current =
|
|
9137
|
+
const home = os4.homedir();
|
|
9138
|
+
const homeDir = home ? path10.resolve(home) : null;
|
|
9139
|
+
let current = path10.resolve(cwd);
|
|
8803
9140
|
while (true) {
|
|
8804
|
-
push(
|
|
9141
|
+
push(path10.join(current, ".opencode", "skills"));
|
|
8805
9142
|
if (current !== homeDir) {
|
|
8806
|
-
push(
|
|
8807
|
-
push(
|
|
9143
|
+
push(path10.join(current, ".claude", "skills"));
|
|
9144
|
+
push(path10.join(current, ".agents", "skills"));
|
|
8808
9145
|
}
|
|
8809
|
-
const parent =
|
|
9146
|
+
const parent = path10.dirname(current);
|
|
8810
9147
|
if (parent === current) break;
|
|
8811
9148
|
current = parent;
|
|
8812
9149
|
}
|
|
8813
|
-
if (homeDir) push(
|
|
9150
|
+
if (homeDir) push(path10.join(homeDir, ".opencode", "skills"));
|
|
8814
9151
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
8815
9152
|
if (envDir) {
|
|
8816
|
-
push(
|
|
8817
|
-
push(
|
|
9153
|
+
push(path10.join(envDir, "skills"));
|
|
9154
|
+
push(path10.join(envDir, "skill"));
|
|
8818
9155
|
}
|
|
8819
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ?
|
|
9156
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path10.join(homeDir, ".config") : null);
|
|
8820
9157
|
if (xdg) {
|
|
8821
|
-
push(
|
|
8822
|
-
push(
|
|
9158
|
+
push(path10.join(xdg, "opencode", "skills"));
|
|
9159
|
+
push(path10.join(xdg, "opencode", "skill"));
|
|
8823
9160
|
}
|
|
8824
9161
|
if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
|
|
8825
9162
|
if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
|
|
8826
|
-
push(
|
|
9163
|
+
push(path10.join(homeDir, ".claude", "skills"));
|
|
8827
9164
|
}
|
|
8828
|
-
push(
|
|
9165
|
+
push(path10.join(homeDir, ".agents", "skills"));
|
|
8829
9166
|
}
|
|
8830
9167
|
return roots;
|
|
8831
9168
|
}
|
|
8832
9169
|
function collectSkills(root, claimed, found) {
|
|
8833
9170
|
let entries;
|
|
8834
9171
|
try {
|
|
8835
|
-
entries =
|
|
9172
|
+
entries = fs8.readdirSync(root, { withFileTypes: true });
|
|
8836
9173
|
} catch {
|
|
8837
9174
|
return;
|
|
8838
9175
|
}
|
|
@@ -8872,23 +9209,23 @@ function discoverBundledSkills() {
|
|
|
8872
9209
|
}
|
|
8873
9210
|
function claudeConfigDir(configDir) {
|
|
8874
9211
|
const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8875
|
-
if (explicit) return
|
|
8876
|
-
const home =
|
|
8877
|
-
return home ?
|
|
9212
|
+
if (explicit) return path10.resolve(expandHome(explicit));
|
|
9213
|
+
const home = os4.homedir();
|
|
9214
|
+
return home ? path10.resolve(home, ".claude") : null;
|
|
8878
9215
|
}
|
|
8879
9216
|
function installedPluginSkillRoots(configDir, cwd) {
|
|
8880
9217
|
if (!configDir) return [];
|
|
8881
9218
|
let parsed;
|
|
8882
9219
|
try {
|
|
8883
9220
|
parsed = JSON.parse(
|
|
8884
|
-
|
|
9221
|
+
fs8.readFileSync(path10.join(configDir, "plugins", "installed_plugins.json"), "utf8")
|
|
8885
9222
|
);
|
|
8886
9223
|
} catch {
|
|
8887
9224
|
return [];
|
|
8888
9225
|
}
|
|
8889
9226
|
const plugins = parsed?.plugins;
|
|
8890
9227
|
if (!plugins || typeof plugins !== "object") return [];
|
|
8891
|
-
const workspace =
|
|
9228
|
+
const workspace = path10.resolve(cwd);
|
|
8892
9229
|
const roots = [];
|
|
8893
9230
|
for (const installs of Object.values(plugins)) {
|
|
8894
9231
|
if (!Array.isArray(installs)) continue;
|
|
@@ -8896,28 +9233,28 @@ function installedPluginSkillRoots(configDir, cwd) {
|
|
|
8896
9233
|
const entry = install;
|
|
8897
9234
|
if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
|
|
8898
9235
|
if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
|
|
8899
|
-
roots.push(
|
|
9236
|
+
roots.push(path10.join(entry.installPath, "skills"));
|
|
8900
9237
|
}
|
|
8901
9238
|
}
|
|
8902
9239
|
return roots;
|
|
8903
9240
|
}
|
|
8904
9241
|
function coversWorkspace(projectPath, workspace) {
|
|
8905
9242
|
if (typeof projectPath !== "string" || !projectPath) return false;
|
|
8906
|
-
const root =
|
|
9243
|
+
const root = path10.resolve(projectPath);
|
|
8907
9244
|
if (root === workspace) return true;
|
|
8908
|
-
const rel =
|
|
8909
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
9245
|
+
const rel = path10.relative(root, workspace);
|
|
9246
|
+
return rel !== "" && !rel.startsWith("..") && !path10.isAbsolute(rel);
|
|
8910
9247
|
}
|
|
8911
9248
|
function discoverNativeClaudeSkills(opts) {
|
|
8912
9249
|
const found = [];
|
|
8913
9250
|
const scanned = /* @__PURE__ */ new Set();
|
|
8914
9251
|
const scan = (root, scope) => {
|
|
8915
|
-
const abs =
|
|
9252
|
+
const abs = path10.resolve(root);
|
|
8916
9253
|
if (scanned.has(abs)) return;
|
|
8917
9254
|
scanned.add(abs);
|
|
8918
9255
|
let entries;
|
|
8919
9256
|
try {
|
|
8920
|
-
entries =
|
|
9257
|
+
entries = fs8.readdirSync(abs, { withFileTypes: true });
|
|
8921
9258
|
} catch {
|
|
8922
9259
|
return;
|
|
8923
9260
|
}
|
|
@@ -8929,13 +9266,13 @@ function discoverNativeClaudeSkills(opts) {
|
|
|
8929
9266
|
}
|
|
8930
9267
|
};
|
|
8931
9268
|
const configDir = claudeConfigDir(opts.configDir);
|
|
8932
|
-
if (configDir) scan(
|
|
8933
|
-
const home =
|
|
8934
|
-
const homeDir = home ?
|
|
8935
|
-
let current =
|
|
9269
|
+
if (configDir) scan(path10.join(configDir, "skills"), "user");
|
|
9270
|
+
const home = os4.homedir();
|
|
9271
|
+
const homeDir = home ? path10.resolve(home) : null;
|
|
9272
|
+
let current = path10.resolve(opts.cwd);
|
|
8936
9273
|
while (true) {
|
|
8937
|
-
if (current !== homeDir) scan(
|
|
8938
|
-
const parent =
|
|
9274
|
+
if (current !== homeDir) scan(path10.join(current, ".claude", "skills"), "project");
|
|
9275
|
+
const parent = path10.dirname(current);
|
|
8939
9276
|
if (parent === current) break;
|
|
8940
9277
|
current = parent;
|
|
8941
9278
|
}
|
|
@@ -8969,10 +9306,10 @@ function dropNativelyLoadedSkills(skills, native) {
|
|
|
8969
9306
|
}
|
|
8970
9307
|
function readBundledSkillInfos() {
|
|
8971
9308
|
return discoverBundledSkills().flatMap((skill) => {
|
|
8972
|
-
const file =
|
|
9309
|
+
const file = path10.join(skill.dir, "SKILL.md");
|
|
8973
9310
|
let text;
|
|
8974
9311
|
try {
|
|
8975
|
-
text =
|
|
9312
|
+
text = fs8.readFileSync(file, "utf8");
|
|
8976
9313
|
} catch {
|
|
8977
9314
|
return [];
|
|
8978
9315
|
}
|
|
@@ -8996,15 +9333,15 @@ function readBundledSkillInfos() {
|
|
|
8996
9333
|
}
|
|
8997
9334
|
function linkSkill(source, target) {
|
|
8998
9335
|
try {
|
|
8999
|
-
|
|
9336
|
+
fs8.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
9000
9337
|
return;
|
|
9001
9338
|
} catch {
|
|
9002
|
-
|
|
9339
|
+
fs8.cpSync(source, target, { recursive: true, dereference: true });
|
|
9003
9340
|
}
|
|
9004
9341
|
}
|
|
9005
9342
|
function fileExists2(p) {
|
|
9006
9343
|
try {
|
|
9007
|
-
return
|
|
9344
|
+
return fs8.statSync(p).isFile();
|
|
9008
9345
|
} catch {
|
|
9009
9346
|
return false;
|
|
9010
9347
|
}
|
|
@@ -9013,14 +9350,14 @@ function buildSkillPluginDir(skills) {
|
|
|
9013
9350
|
if (skills.length === 0) return null;
|
|
9014
9351
|
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
9015
9352
|
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
9016
|
-
const root =
|
|
9017
|
-
const manifest =
|
|
9353
|
+
const root = path10.join(pluginTmpDir(), `skills-${hash}`);
|
|
9354
|
+
const manifest = path10.join(root, ".claude-plugin", "plugin.json");
|
|
9018
9355
|
if (fileExists2(manifest)) return root;
|
|
9019
9356
|
try {
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9357
|
+
fs8.rmSync(root, { recursive: true, force: true });
|
|
9358
|
+
fs8.mkdirSync(path10.join(root, ".claude-plugin"), { recursive: true });
|
|
9359
|
+
fs8.mkdirSync(path10.join(root, "skills"), { recursive: true });
|
|
9360
|
+
fs8.writeFileSync(
|
|
9024
9361
|
manifest,
|
|
9025
9362
|
JSON.stringify(
|
|
9026
9363
|
{
|
|
@@ -9033,7 +9370,7 @@ function buildSkillPluginDir(skills) {
|
|
|
9033
9370
|
{ encoding: "utf8", mode: 384 }
|
|
9034
9371
|
);
|
|
9035
9372
|
for (const skill of skills) {
|
|
9036
|
-
linkSkill(skill.dir,
|
|
9373
|
+
linkSkill(skill.dir, path10.join(root, "skills", skill.name));
|
|
9037
9374
|
}
|
|
9038
9375
|
} catch (err) {
|
|
9039
9376
|
log.warn("failed to stage opencode skill plugin dir", {
|
|
@@ -9049,7 +9386,7 @@ function registerBundledSkillPath(config) {
|
|
|
9049
9386
|
if (!dir) return false;
|
|
9050
9387
|
config.skills ??= {};
|
|
9051
9388
|
const paths = config.skills.paths ??= [];
|
|
9052
|
-
if (paths.some((p) =>
|
|
9389
|
+
if (paths.some((p) => path10.resolve(p) === dir)) return false;
|
|
9053
9390
|
paths.push(dir);
|
|
9054
9391
|
return true;
|
|
9055
9392
|
}
|
|
@@ -9099,36 +9436,36 @@ import { EventEmitter as EventEmitter4 } from "events";
|
|
|
9099
9436
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
9100
9437
|
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
9101
9438
|
import { unlink as unlink2 } from "fs/promises";
|
|
9102
|
-
import { join as
|
|
9439
|
+
import { join as join11 } from "path";
|
|
9103
9440
|
|
|
9104
9441
|
// src/claude-session-bun.ts
|
|
9105
|
-
import * as
|
|
9106
|
-
import * as
|
|
9107
|
-
import * as
|
|
9442
|
+
import * as os5 from "os";
|
|
9443
|
+
import * as fs9 from "fs";
|
|
9444
|
+
import * as path11 from "path";
|
|
9108
9445
|
import { execFileSync } from "child_process";
|
|
9109
9446
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
9110
9447
|
import { StringDecoder } from "string_decoder";
|
|
9111
9448
|
function resolveClaude(cmd = "claude") {
|
|
9112
|
-
if (
|
|
9449
|
+
if (path11.isAbsolute(cmd) && fs9.existsSync(cmd)) return cmd;
|
|
9113
9450
|
const viaBun = Bun.which(cmd);
|
|
9114
9451
|
if (viaBun) return viaBun;
|
|
9115
|
-
const isWin =
|
|
9452
|
+
const isWin = os5.platform() === "win32";
|
|
9116
9453
|
try {
|
|
9117
9454
|
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
9118
9455
|
encoding: "utf8",
|
|
9119
9456
|
stdio: ["ignore", "pipe", "ignore"]
|
|
9120
9457
|
});
|
|
9121
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
9458
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs9.existsSync(p));
|
|
9122
9459
|
if (first) return first;
|
|
9123
9460
|
} catch {
|
|
9124
9461
|
}
|
|
9125
9462
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
9126
9463
|
}
|
|
9127
9464
|
function encodeCwd(cwd) {
|
|
9128
|
-
const resolved =
|
|
9465
|
+
const resolved = path11.resolve(cwd);
|
|
9129
9466
|
let real = resolved;
|
|
9130
9467
|
try {
|
|
9131
|
-
real =
|
|
9468
|
+
real = fs9.realpathSync.native(resolved);
|
|
9132
9469
|
} catch {
|
|
9133
9470
|
}
|
|
9134
9471
|
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
@@ -9136,7 +9473,7 @@ function encodeCwd(cwd) {
|
|
|
9136
9473
|
var bunPtySpawner = (argv, opts) => {
|
|
9137
9474
|
const [command, ...args] = argv;
|
|
9138
9475
|
const resolved = resolveClaude(command ?? "claude");
|
|
9139
|
-
if (
|
|
9476
|
+
if (os5.platform() === "win32" && isBatchFile(resolved)) {
|
|
9140
9477
|
throw new Error(
|
|
9141
9478
|
`The interactive transport cannot run a Windows batch shim (${resolved}). Point \`cliPath\` at claude.exe, or use the default headless transport.`
|
|
9142
9479
|
);
|
|
@@ -9244,10 +9581,10 @@ function classifyScreen(text) {
|
|
|
9244
9581
|
return null;
|
|
9245
9582
|
}
|
|
9246
9583
|
function interactiveTranscriptPath(opts) {
|
|
9247
|
-
return
|
|
9584
|
+
return path11.join(
|
|
9248
9585
|
resolveConfigDir(opts.configDir),
|
|
9249
9586
|
"projects",
|
|
9250
|
-
encodeCwd(
|
|
9587
|
+
encodeCwd(path11.resolve(opts.cwd)),
|
|
9251
9588
|
`${opts.sessionId}.jsonl`
|
|
9252
9589
|
);
|
|
9253
9590
|
}
|
|
@@ -9425,7 +9762,7 @@ var TranscriptCursor = class {
|
|
|
9425
9762
|
const filePath = this.resolvePath();
|
|
9426
9763
|
let stat;
|
|
9427
9764
|
try {
|
|
9428
|
-
stat =
|
|
9765
|
+
stat = fs9.statSync(filePath);
|
|
9429
9766
|
} catch {
|
|
9430
9767
|
return;
|
|
9431
9768
|
}
|
|
@@ -9441,13 +9778,13 @@ var TranscriptCursor = class {
|
|
|
9441
9778
|
if (stat.size <= this.offset) return;
|
|
9442
9779
|
let fd = null;
|
|
9443
9780
|
try {
|
|
9444
|
-
fd =
|
|
9781
|
+
fd = fs9.openSync(filePath, "r");
|
|
9445
9782
|
const buffer = Buffer.allocUnsafe(
|
|
9446
9783
|
Math.min(TRANSCRIPT_CHUNK_BYTES, stat.size - this.offset)
|
|
9447
9784
|
);
|
|
9448
9785
|
while (this.offset < stat.size) {
|
|
9449
9786
|
const want = Math.min(buffer.length, stat.size - this.offset);
|
|
9450
|
-
const read =
|
|
9787
|
+
const read = fs9.readSync(fd, buffer, 0, want, this.offset);
|
|
9451
9788
|
this.stats.reads++;
|
|
9452
9789
|
if (read <= 0) break;
|
|
9453
9790
|
this.offset += read;
|
|
@@ -9458,7 +9795,7 @@ var TranscriptCursor = class {
|
|
|
9458
9795
|
} finally {
|
|
9459
9796
|
if (fd !== null) {
|
|
9460
9797
|
try {
|
|
9461
|
-
|
|
9798
|
+
fs9.closeSync(fd);
|
|
9462
9799
|
} catch {
|
|
9463
9800
|
}
|
|
9464
9801
|
}
|
|
@@ -9488,12 +9825,12 @@ function isOwnPromptRecord(rec, normalized) {
|
|
|
9488
9825
|
}
|
|
9489
9826
|
function resolveConfigDir(configDir) {
|
|
9490
9827
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
9491
|
-
if (!value) return
|
|
9492
|
-
if (value === "~") return
|
|
9828
|
+
if (!value) return path11.join(os5.homedir(), ".claude");
|
|
9829
|
+
if (value === "~") return os5.homedir();
|
|
9493
9830
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
9494
|
-
return
|
|
9831
|
+
return path11.join(os5.homedir(), value.slice(2));
|
|
9495
9832
|
}
|
|
9496
|
-
return
|
|
9833
|
+
return path11.resolve(value);
|
|
9497
9834
|
}
|
|
9498
9835
|
function fatalScreenMessage(kind, configDir, cwd) {
|
|
9499
9836
|
if (kind === "trust") {
|
|
@@ -9557,7 +9894,7 @@ var ClaudeSession = class {
|
|
|
9557
9894
|
abandonTurn = false;
|
|
9558
9895
|
o;
|
|
9559
9896
|
constructor(opts = {}) {
|
|
9560
|
-
this.cwd =
|
|
9897
|
+
this.cwd = path11.resolve(opts.cwd ?? process.cwd());
|
|
9561
9898
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
9562
9899
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
9563
9900
|
this.signal = opts.signal;
|
|
@@ -9569,7 +9906,7 @@ var ClaudeSession = class {
|
|
|
9569
9906
|
this.onExit = opts.onExit;
|
|
9570
9907
|
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
9571
9908
|
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
9572
|
-
this.jsonlPath =
|
|
9909
|
+
this.jsonlPath = path11.join(
|
|
9573
9910
|
this.configDir,
|
|
9574
9911
|
"projects",
|
|
9575
9912
|
encodeCwd(this.cwd),
|
|
@@ -10280,7 +10617,7 @@ ${text2}`
|
|
|
10280
10617
|
}
|
|
10281
10618
|
function stageImage(data, extension) {
|
|
10282
10619
|
try {
|
|
10283
|
-
const file =
|
|
10620
|
+
const file = join11(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
|
|
10284
10621
|
writeFileSync5(file, data, { mode: 384, flag: "wx" });
|
|
10285
10622
|
return file;
|
|
10286
10623
|
} catch (err) {
|
|
@@ -10765,7 +11102,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
10765
11102
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
10766
11103
|
import { constants as fsConstants } from "fs";
|
|
10767
11104
|
import { chmod as chmod2, copyFile as copyFile2, lstat as lstat2, mkdir as mkdir2 } from "fs/promises";
|
|
10768
|
-
import
|
|
11105
|
+
import path12 from "path";
|
|
10769
11106
|
var transcriptPath = interactiveTranscriptPath;
|
|
10770
11107
|
function configDirForAccount(account) {
|
|
10771
11108
|
const normalized = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
@@ -10793,7 +11130,7 @@ async function carryTranscriptToAccount(input) {
|
|
|
10793
11130
|
const carriedId = renamed ? (input.newSessionId ?? randomUUID6)() : sessionId;
|
|
10794
11131
|
const target = renamed ? transcriptPath({ configDir: targetConfigDir, cwd, sessionId: carriedId }) : targetOwn;
|
|
10795
11132
|
try {
|
|
10796
|
-
await mkdir2(
|
|
11133
|
+
await mkdir2(path12.dirname(target), { recursive: true, mode: 448 });
|
|
10797
11134
|
await copyFile2(live.file, target, fsConstants.COPYFILE_EXCL);
|
|
10798
11135
|
await chmod2(target, 384);
|
|
10799
11136
|
} catch (err) {
|
|
@@ -10813,7 +11150,7 @@ function dedupe2(dirs) {
|
|
|
10813
11150
|
const seen = /* @__PURE__ */ new Set();
|
|
10814
11151
|
const out = [];
|
|
10815
11152
|
for (const dir of dirs) {
|
|
10816
|
-
const resolved =
|
|
11153
|
+
const resolved = path12.resolve(dir);
|
|
10817
11154
|
if (seen.has(resolved)) continue;
|
|
10818
11155
|
seen.add(resolved);
|
|
10819
11156
|
out.push(resolved);
|
|
@@ -10822,13 +11159,13 @@ function dedupe2(dirs) {
|
|
|
10822
11159
|
}
|
|
10823
11160
|
|
|
10824
11161
|
// src/prompts.ts
|
|
10825
|
-
import { readFileSync as
|
|
10826
|
-
import { homedir as
|
|
11162
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
11163
|
+
import { homedir as homedir9 } from "os";
|
|
10827
11164
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
10828
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
10829
|
-
function readPromptFileIfPresent(
|
|
11165
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join12 } from "path";
|
|
11166
|
+
function readPromptFileIfPresent(path15) {
|
|
10830
11167
|
try {
|
|
10831
|
-
const content =
|
|
11168
|
+
const content = readFileSync7(path15, "utf8").trim();
|
|
10832
11169
|
return content || void 0;
|
|
10833
11170
|
} catch {
|
|
10834
11171
|
return void 0;
|
|
@@ -10837,7 +11174,7 @@ function readPromptFileIfPresent(path14) {
|
|
|
10837
11174
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
10838
11175
|
let dir = cwd;
|
|
10839
11176
|
while (true) {
|
|
10840
|
-
const content = readPromptFileIfPresent(
|
|
11177
|
+
const content = readPromptFileIfPresent(join12(dir, "AGENTS.md"));
|
|
10841
11178
|
if (content) return content;
|
|
10842
11179
|
const parent = dirname6(dir);
|
|
10843
11180
|
if (parent === dir) return void 0;
|
|
@@ -10933,9 +11270,9 @@ function extractSystemMessages(prompt) {
|
|
|
10933
11270
|
}
|
|
10934
11271
|
var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
|
|
10935
11272
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
10936
|
-
const readFile2 = opts.readFile ?? ((
|
|
11273
|
+
const readFile2 = opts.readFile ?? ((path15) => {
|
|
10937
11274
|
try {
|
|
10938
|
-
return
|
|
11275
|
+
return readFileSync7(path15, "utf8");
|
|
10939
11276
|
} catch {
|
|
10940
11277
|
return null;
|
|
10941
11278
|
}
|
|
@@ -10947,16 +11284,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
|
|
|
10947
11284
|
}
|
|
10948
11285
|
const seen = /* @__PURE__ */ new Set();
|
|
10949
11286
|
for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
|
|
10950
|
-
const
|
|
10951
|
-
if (seen.has(
|
|
10952
|
-
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(
|
|
10953
|
-
seen.add(
|
|
10954
|
-
const file = readFile2(
|
|
11287
|
+
const path15 = match[1].trim();
|
|
11288
|
+
if (seen.has(path15) || !isAbsolute3(path15)) continue;
|
|
11289
|
+
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path15))) continue;
|
|
11290
|
+
seen.add(path15);
|
|
11291
|
+
const file = readFile2(path15);
|
|
10955
11292
|
if (!file?.trim()) continue;
|
|
10956
11293
|
const start = match.index + match[0].length + 1;
|
|
10957
11294
|
const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
|
|
10958
11295
|
if (content === null) continue;
|
|
10959
|
-
out.push({ text: `Instructions from: ${
|
|
11296
|
+
out.push({ text: `Instructions from: ${path15}
|
|
10960
11297
|
${content}`, content });
|
|
10961
11298
|
}
|
|
10962
11299
|
return out;
|
|
@@ -10976,8 +11313,8 @@ ${options.compressionSummary.trim()}`
|
|
|
10976
11313
|
for (const s of extraSystemContent) {
|
|
10977
11314
|
if (s.trim()) parts.push(s.trim());
|
|
10978
11315
|
}
|
|
10979
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
10980
|
-
const globalAgents = readPromptFileIfPresent(
|
|
11316
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
|
|
11317
|
+
const globalAgents = readPromptFileIfPresent(join12(configRoot, "opencode", "AGENTS.md"));
|
|
10981
11318
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
10982
11319
|
const forwarded = extraSystemContent.join("\n\n");
|
|
10983
11320
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -10993,10 +11330,10 @@ ${options.compressionSummary.trim()}`
|
|
|
10993
11330
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
10994
11331
|
const content = parts.join("\n\n");
|
|
10995
11332
|
if (!content) return void 0;
|
|
10996
|
-
const
|
|
11333
|
+
const path15 = join12(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
|
|
10997
11334
|
try {
|
|
10998
|
-
writeFileSync6(
|
|
10999
|
-
return
|
|
11335
|
+
writeFileSync6(path15, content, { encoding: "utf8", mode: 384 });
|
|
11336
|
+
return path15;
|
|
11000
11337
|
} catch (err) {
|
|
11001
11338
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
11002
11339
|
return void 0;
|
|
@@ -11423,7 +11760,9 @@ function effectiveMcpConfig(config, cwd, proxyConfigPath, runtimeStatus, exclude
|
|
|
11423
11760
|
let bridgedHash = null;
|
|
11424
11761
|
let allEnabledServerNames = [];
|
|
11425
11762
|
if (config.bridgeOpencodeMcp !== false) {
|
|
11426
|
-
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, config.hostApi
|
|
11763
|
+
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, config.hostApi, {
|
|
11764
|
+
oauthTokens: config.bridgeMcpOauthTokens === true
|
|
11765
|
+
});
|
|
11427
11766
|
if (bridged) {
|
|
11428
11767
|
if (bridged.path) paths.push(bridged.path);
|
|
11429
11768
|
bridgedHash = bridged.hash;
|
|
@@ -13806,6 +14145,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13806
14145
|
const sourceAccount = normalizeAccountName(
|
|
13807
14146
|
this.config.account ?? DEFAULT_ACCOUNT
|
|
13808
14147
|
);
|
|
14148
|
+
const accountProviders = accountProviderMap(this.config.failoverAccounts);
|
|
14149
|
+
const groups = this.config.accountGroups ?? null;
|
|
13809
14150
|
const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
|
|
13810
14151
|
let failover = compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
|
|
13811
14152
|
account: sourceAccount,
|
|
@@ -13967,7 +14308,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13967
14308
|
}
|
|
13968
14309
|
const failoverAnswer = compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
|
|
13969
14310
|
sourceAccount,
|
|
13970
|
-
candidates: failoverCandidates(
|
|
14311
|
+
candidates: failoverCandidates(
|
|
14312
|
+
this.config.failoverAccounts,
|
|
14313
|
+
sourceAccount,
|
|
14314
|
+
groups
|
|
14315
|
+
)
|
|
13971
14316
|
});
|
|
13972
14317
|
if (failoverAnswer?.kind === "stop") {
|
|
13973
14318
|
log.warn("account failover declined; ending the turn", {
|
|
@@ -14049,8 +14394,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14049
14394
|
// Every account's own directory, plus this model's configured one:
|
|
14050
14395
|
// `configDir` is an option, so an account's directory is not always
|
|
14051
14396
|
// the `~/.claude-<name>` the name alone would build.
|
|
14397
|
+
// Same-group accounts only (h #g226): a transcript sitting in an
|
|
14398
|
+
// account from another group is not a source this may read from, and
|
|
14399
|
+
// with no groups configured this is the full list exactly as before.
|
|
14052
14400
|
accountConfigDirs: [
|
|
14053
|
-
...(this.config.failoverAccounts
|
|
14401
|
+
...accountsInGroupOf(sourceAccount, this.config.failoverAccounts, groups).map(
|
|
14402
|
+
configDirForAccount
|
|
14403
|
+
),
|
|
14054
14404
|
resolveConfigDir(this.config.configDir)
|
|
14055
14405
|
]
|
|
14056
14406
|
});
|
|
@@ -14084,6 +14434,32 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14084
14434
|
if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
|
|
14085
14435
|
includeHistoryContext = true;
|
|
14086
14436
|
}
|
|
14437
|
+
let accountGroupNote = null;
|
|
14438
|
+
if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
|
|
14439
|
+
const foreign = findForeignAccountSibling({
|
|
14440
|
+
sessionKey: sk,
|
|
14441
|
+
accountProviders,
|
|
14442
|
+
// The live process's own view, so the guard holds with
|
|
14443
|
+
// `resumeAfterRestart: false`, where the store is never written.
|
|
14444
|
+
extraKeys: listClaudeSessionKeys()
|
|
14445
|
+
});
|
|
14446
|
+
if (foreign && !accountsShareGroup(sourceAccount, foreign.account, groups)) {
|
|
14447
|
+
includeHistoryContext = false;
|
|
14448
|
+
accountGroupNote = formatAccountGroupNote({
|
|
14449
|
+
sourceAccount: foreign.account,
|
|
14450
|
+
sourceGroup: accountGroup(foreign.account, groups),
|
|
14451
|
+
targetAccount: sourceAccount,
|
|
14452
|
+
targetGroup: accountGroup(sourceAccount, groups)
|
|
14453
|
+
});
|
|
14454
|
+
log.notice("starting fresh on this account: it is in another account group", {
|
|
14455
|
+
sessionKey: sk,
|
|
14456
|
+
reason: "another-account-group",
|
|
14457
|
+
account: sourceAccount,
|
|
14458
|
+
from: foreign.account,
|
|
14459
|
+
group: accountGroup(sourceAccount, groups)
|
|
14460
|
+
});
|
|
14461
|
+
}
|
|
14462
|
+
}
|
|
14087
14463
|
let forkFromClaudeSessionId;
|
|
14088
14464
|
if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && failoverAnswer?.kind !== "switch") {
|
|
14089
14465
|
const parent = findForkParent({
|
|
@@ -14127,7 +14503,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14127
14503
|
let replayReason = "no-session-for-key";
|
|
14128
14504
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
14129
14505
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
14130
|
-
const transcriptPath2 = (id) =>
|
|
14506
|
+
const transcriptPath2 = (id, dir) => join13(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
14131
14507
|
const resumePoint = findResumePoint({
|
|
14132
14508
|
sessionKey: sk,
|
|
14133
14509
|
prompt: options.prompt,
|
|
@@ -14155,25 +14531,70 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14155
14531
|
// timers on everything it touches (the same reason the fork lookup
|
|
14156
14532
|
// above uses it).
|
|
14157
14533
|
isBusy: claudeSessionIsWriting,
|
|
14534
|
+
// The ACCOUNT is the third thing that may differ (h #g226), and the
|
|
14535
|
+
// guard is where a cross-group sibling is refused by name.
|
|
14536
|
+
accountProviders,
|
|
14537
|
+
allowAccount: (account) => this.config.crossAccountResume !== false && accountsShareGroup(sourceAccount, account, groups),
|
|
14158
14538
|
onRefused: (reason) => {
|
|
14159
14539
|
replayReason = reason;
|
|
14160
14540
|
}
|
|
14161
14541
|
});
|
|
14162
14542
|
if (sibling) {
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
if (
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14543
|
+
let carriedSessionId;
|
|
14544
|
+
let carryRefused;
|
|
14545
|
+
if (sibling.siblingAccount && sibling.siblingConfigDir) {
|
|
14546
|
+
const carry = await carryTranscriptToAccount({
|
|
14547
|
+
sessionId: sibling.claudeSessionId,
|
|
14548
|
+
cwd,
|
|
14549
|
+
targetConfigDir: resolveConfigDir(this.skillBridgeSpawn(failover).configDir),
|
|
14550
|
+
// The sibling's recorded directory first, then every same-group
|
|
14551
|
+
// account's. The recorded one can be stale by a turn (it is
|
|
14552
|
+
// rewritten only when a turn succeeds), so the conventional
|
|
14553
|
+
// directories are what make a stale record recoverable rather
|
|
14554
|
+
// than a refusal.
|
|
14555
|
+
accountConfigDirs: [
|
|
14556
|
+
sibling.siblingConfigDir,
|
|
14557
|
+
...accountsInGroupOf(sourceAccount, this.config.failoverAccounts, groups).map(
|
|
14558
|
+
configDirForAccount
|
|
14559
|
+
),
|
|
14560
|
+
configDir
|
|
14561
|
+
]
|
|
14562
|
+
});
|
|
14563
|
+
if (carry.kind === "carried" || carry.kind === "already-there") {
|
|
14564
|
+
carriedSessionId = carry.sessionId;
|
|
14565
|
+
} else {
|
|
14566
|
+
carryRefused = carry.reason;
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14569
|
+
if (carryRefused) {
|
|
14570
|
+
replayReason = `sibling-${carryRefused}`;
|
|
14571
|
+
log.notice("replaying this conversation rather than carrying it across accounts", {
|
|
14572
|
+
sessionKey: sk,
|
|
14573
|
+
siblingKey: sibling.siblingKey,
|
|
14574
|
+
reason: carryRefused,
|
|
14575
|
+
account: sourceAccount,
|
|
14576
|
+
from: sibling.siblingAccount
|
|
14577
|
+
});
|
|
14175
14578
|
} else {
|
|
14176
|
-
|
|
14579
|
+
deleteActiveProcess(sibling.siblingKey);
|
|
14580
|
+
const carried = transferClaudeSession(sibling.siblingKey, sk);
|
|
14581
|
+
if (carried) {
|
|
14582
|
+
if (carriedSessionId && carriedSessionId !== carried) {
|
|
14583
|
+
setClaudeSessionId(sk, carriedSessionId);
|
|
14584
|
+
}
|
|
14585
|
+
includeHistoryContext = false;
|
|
14586
|
+
log.notice(
|
|
14587
|
+
"continuing this conversation's claude session under the new model or effort instead of replaying it",
|
|
14588
|
+
{
|
|
14589
|
+
sessionKey: sk,
|
|
14590
|
+
siblingKey: sibling.siblingKey,
|
|
14591
|
+
matchedMessages: sibling.matched,
|
|
14592
|
+
...sibling.siblingAccount ? { from: sibling.siblingAccount, account: sourceAccount } : {}
|
|
14593
|
+
}
|
|
14594
|
+
);
|
|
14595
|
+
} else {
|
|
14596
|
+
replayReason = "sibling-carry-over-failed";
|
|
14597
|
+
}
|
|
14177
14598
|
}
|
|
14178
14599
|
}
|
|
14179
14600
|
}
|
|
@@ -14255,7 +14676,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14255
14676
|
}
|
|
14256
14677
|
const failoverAccounts = failoverCandidates(
|
|
14257
14678
|
this.config.failoverAccounts,
|
|
14258
|
-
sourceAccount
|
|
14679
|
+
sourceAccount,
|
|
14680
|
+
groups
|
|
14259
14681
|
);
|
|
14260
14682
|
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
|
|
14261
14683
|
// rate-limit error, never the form. See `TurnMode`.
|
|
@@ -14847,6 +15269,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14847
15269
|
});
|
|
14848
15270
|
state.endTextBlock();
|
|
14849
15271
|
}
|
|
15272
|
+
if (accountGroupNote) {
|
|
15273
|
+
controller.enqueue({
|
|
15274
|
+
type: "text-delta",
|
|
15275
|
+
id: state.startTextBlock(),
|
|
15276
|
+
delta: accountGroupNote
|
|
15277
|
+
});
|
|
15278
|
+
state.endTextBlock();
|
|
15279
|
+
}
|
|
14850
15280
|
if (failoverNote) {
|
|
14851
15281
|
controller.enqueue({
|
|
14852
15282
|
type: "text-delta",
|
|
@@ -14859,7 +15289,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14859
15289
|
const completeResult = (msg) => {
|
|
14860
15290
|
if (state.controllerClosed) return;
|
|
14861
15291
|
if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
|
|
14862
|
-
recordResumePoint(sk, msg.session_id, options.prompt, cliPath
|
|
15292
|
+
recordResumePoint(sk, msg.session_id, options.prompt, cliPath, {
|
|
15293
|
+
configDir: resolveConfigDir(self.skillBridgeSpawn(failover).configDir),
|
|
15294
|
+
account: failover.target ?? sourceAccount
|
|
15295
|
+
});
|
|
14863
15296
|
}
|
|
14864
15297
|
if (deliverPendingCompletions(state)) {
|
|
14865
15298
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
@@ -15383,8 +15816,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15383
15816
|
};
|
|
15384
15817
|
|
|
15385
15818
|
// src/account-tier.ts
|
|
15386
|
-
import { readFileSync as
|
|
15387
|
-
import
|
|
15819
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
15820
|
+
import path13 from "path";
|
|
15388
15821
|
var ORGANIZATION_TYPE_LABELS = {
|
|
15389
15822
|
claude_max: "Max",
|
|
15390
15823
|
claude_pro: "Pro",
|
|
@@ -15413,7 +15846,7 @@ function readAccountTier(account) {
|
|
|
15413
15846
|
return label;
|
|
15414
15847
|
}
|
|
15415
15848
|
function readTierFromConfigDir(configDir, allowLegacyFallback) {
|
|
15416
|
-
const candidates = [
|
|
15849
|
+
const candidates = [path13.join(configDir, ".claude.json")];
|
|
15417
15850
|
if (allowLegacyFallback) candidates.push(expandHome("~/.claude.json"));
|
|
15418
15851
|
for (const candidate of candidates) {
|
|
15419
15852
|
const oauth = readOauthAccount(candidate);
|
|
@@ -15426,7 +15859,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
|
|
|
15426
15859
|
function readOauthAccount(file) {
|
|
15427
15860
|
let raw;
|
|
15428
15861
|
try {
|
|
15429
|
-
raw =
|
|
15862
|
+
raw = readFileSync8(file, "utf8");
|
|
15430
15863
|
} catch {
|
|
15431
15864
|
return void 0;
|
|
15432
15865
|
}
|
|
@@ -15455,13 +15888,13 @@ function tierLabel(oauthAccount) {
|
|
|
15455
15888
|
import {
|
|
15456
15889
|
existsSync as existsSync5,
|
|
15457
15890
|
mkdirSync as mkdirSync5,
|
|
15458
|
-
readFileSync as
|
|
15891
|
+
readFileSync as readFileSync9,
|
|
15459
15892
|
realpathSync as realpathSync4,
|
|
15460
15893
|
rmSync as rmSync3,
|
|
15461
15894
|
writeFileSync as writeFileSync7
|
|
15462
15895
|
} from "fs";
|
|
15463
|
-
import { homedir as
|
|
15464
|
-
import { dirname as dirname7, join as
|
|
15896
|
+
import { homedir as homedir10 } from "os";
|
|
15897
|
+
import { dirname as dirname7, join as join14, resolve as resolve5 } from "path";
|
|
15465
15898
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
15466
15899
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
15467
15900
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -15469,20 +15902,20 @@ var alreadyRan = false;
|
|
|
15469
15902
|
function candidateCacheRoots() {
|
|
15470
15903
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
15471
15904
|
return [
|
|
15472
|
-
xdg ?
|
|
15473
|
-
|
|
15474
|
-
|
|
15905
|
+
xdg ? join14(xdg, "opencode") : null,
|
|
15906
|
+
join14(homedir10(), ".cache", "opencode"),
|
|
15907
|
+
join14(homedir10(), "Library", "Caches", "opencode")
|
|
15475
15908
|
].filter((p) => Boolean(p));
|
|
15476
15909
|
}
|
|
15477
15910
|
function userOpencodeJsonPath() {
|
|
15478
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
15479
|
-
return
|
|
15911
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
|
|
15912
|
+
return join14(xdgConfig, "opencode", "opencode.json");
|
|
15480
15913
|
}
|
|
15481
15914
|
function userIntendsToUseUnscoped() {
|
|
15482
15915
|
const cfg = userOpencodeJsonPath();
|
|
15483
15916
|
if (!existsSync5(cfg)) return false;
|
|
15484
15917
|
try {
|
|
15485
|
-
const json = JSON.parse(
|
|
15918
|
+
const json = JSON.parse(readFileSync9(cfg, "utf8"));
|
|
15486
15919
|
const plugins = json.plugin;
|
|
15487
15920
|
if (!Array.isArray(plugins)) return false;
|
|
15488
15921
|
return plugins.some(
|
|
@@ -15503,19 +15936,19 @@ function ourLoadedDir() {
|
|
|
15503
15936
|
function ourVersion(dir) {
|
|
15504
15937
|
if (!dir) return null;
|
|
15505
15938
|
try {
|
|
15506
|
-
const pkg = JSON.parse(
|
|
15939
|
+
const pkg = JSON.parse(readFileSync9(join14(dir, "package.json"), "utf8"));
|
|
15507
15940
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
15508
15941
|
} catch {
|
|
15509
15942
|
return null;
|
|
15510
15943
|
}
|
|
15511
15944
|
}
|
|
15512
15945
|
function markerPath() {
|
|
15513
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
15514
|
-
return
|
|
15946
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join14(homedir10(), ".local", "state");
|
|
15947
|
+
return join14(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
15515
15948
|
}
|
|
15516
15949
|
function markerVersion() {
|
|
15517
15950
|
try {
|
|
15518
|
-
const marker = JSON.parse(
|
|
15951
|
+
const marker = JSON.parse(readFileSync9(markerPath(), "utf8"));
|
|
15519
15952
|
return typeof marker.version === "string" ? marker.version : null;
|
|
15520
15953
|
} catch {
|
|
15521
15954
|
return null;
|
|
@@ -15560,7 +15993,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
15560
15993
|
}
|
|
15561
15994
|
function cleanupOne(cacheRoot, ourDir) {
|
|
15562
15995
|
if (!existsSync5(cacheRoot)) return;
|
|
15563
|
-
const stalePath =
|
|
15996
|
+
const stalePath = join14(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
15564
15997
|
if (!existsSync5(stalePath)) return;
|
|
15565
15998
|
let realStalePath = stalePath;
|
|
15566
15999
|
try {
|
|
@@ -15568,11 +16001,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
15568
16001
|
} catch {
|
|
15569
16002
|
}
|
|
15570
16003
|
if (ourDir && realStalePath === ourDir) return;
|
|
15571
|
-
const pkgJsonPath =
|
|
16004
|
+
const pkgJsonPath = join14(stalePath, "package.json");
|
|
15572
16005
|
if (!existsSync5(pkgJsonPath)) return;
|
|
15573
16006
|
let pkg = {};
|
|
15574
16007
|
try {
|
|
15575
|
-
pkg = JSON.parse(
|
|
16008
|
+
pkg = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
|
|
15576
16009
|
} catch {
|
|
15577
16010
|
return;
|
|
15578
16011
|
}
|
|
@@ -15588,10 +16021,10 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
15588
16021
|
});
|
|
15589
16022
|
return;
|
|
15590
16023
|
}
|
|
15591
|
-
const cachePkgJson =
|
|
16024
|
+
const cachePkgJson = join14(cacheRoot, "package.json");
|
|
15592
16025
|
if (!existsSync5(cachePkgJson)) return;
|
|
15593
16026
|
try {
|
|
15594
|
-
const cfg = JSON.parse(
|
|
16027
|
+
const cfg = JSON.parse(readFileSync9(cachePkgJson, "utf8"));
|
|
15595
16028
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
15596
16029
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
15597
16030
|
writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -15671,7 +16104,7 @@ function registerOpencodeLogSink(client) {
|
|
|
15671
16104
|
}
|
|
15672
16105
|
|
|
15673
16106
|
// src/v2-client.ts
|
|
15674
|
-
import
|
|
16107
|
+
import path14 from "path";
|
|
15675
16108
|
function formatAgentTypeList(agents) {
|
|
15676
16109
|
const lines = agents.flatMap((agent) => {
|
|
15677
16110
|
if (agent.hidden === true || agent.mode === "primary") return [];
|
|
@@ -15700,7 +16133,7 @@ function toV1Session(info) {
|
|
|
15700
16133
|
if (!info || typeof info !== "object") return void 0;
|
|
15701
16134
|
const session = info;
|
|
15702
16135
|
const root = session.location?.directory;
|
|
15703
|
-
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ?
|
|
16136
|
+
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path14.resolve(root, session.subpath) : root : void 0;
|
|
15704
16137
|
return { ...info, directory, parentID: session.parentID };
|
|
15705
16138
|
}
|
|
15706
16139
|
function toV1Messages(messages) {
|
|
@@ -16198,6 +16631,12 @@ function applyPermissionPreset(settings, defaultProxyTools) {
|
|
|
16198
16631
|
}
|
|
16199
16632
|
return resolved;
|
|
16200
16633
|
}
|
|
16634
|
+
function accountGroups(settings) {
|
|
16635
|
+
return resolveAccountGroups(
|
|
16636
|
+
settings.accountGroups,
|
|
16637
|
+
settings.failoverAccounts ?? settings.accounts
|
|
16638
|
+
);
|
|
16639
|
+
}
|
|
16201
16640
|
function createClaudeCode(settings = {}) {
|
|
16202
16641
|
if (settings.logging) {
|
|
16203
16642
|
configureLogger({
|
|
@@ -16228,6 +16667,11 @@ function createClaudeCode(settings = {}) {
|
|
|
16228
16667
|
// unset option must stay unset rather than being written to one mode or
|
|
16229
16668
|
// the other in only one of the two entry points (h #g194).
|
|
16230
16669
|
accountFailover: settings.accountFailover,
|
|
16670
|
+
// Resolved once here rather than per turn, so the WARNs an invalid entry
|
|
16671
|
+
// raises are tied to provider creation and a turn reads a value that is
|
|
16672
|
+
// already validated. `failoverAccounts` is what the expansion configured,
|
|
16673
|
+
// which is exactly the set a group may name (h #g226).
|
|
16674
|
+
accountGroups: accountGroups(settings),
|
|
16231
16675
|
providerID: settings.providerID,
|
|
16232
16676
|
skipPermissions: preset?.skipPermissions ?? settings.skipPermissions ?? true,
|
|
16233
16677
|
permissionMode: preset?.permissionMode ?? settings.permissionMode,
|
|
@@ -16235,6 +16679,9 @@ function createClaudeCode(settings = {}) {
|
|
|
16235
16679
|
mcpConfig: settings.mcpConfig,
|
|
16236
16680
|
strictMcpConfig: settings.strictMcpConfig,
|
|
16237
16681
|
bridgeOpencodeMcp: settings.bridgeOpencodeMcp ?? true,
|
|
16682
|
+
// No `?? true` twin: reading opencode's credential store is opt-in, so
|
|
16683
|
+
// the absent value has to stay falsy.
|
|
16684
|
+
bridgeMcpOauthTokens: settings.bridgeMcpOauthTokens,
|
|
16238
16685
|
controlRequestBehavior: preset?.controlRequestBehavior ?? settings.controlRequestBehavior ?? "allow",
|
|
16239
16686
|
controlRequestToolBehaviors: preset ? void 0 : settings.controlRequestToolBehaviors,
|
|
16240
16687
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|