@khalilgharbaoui/opencode-claude-code-plugin 0.50.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 +38 -0
- package/dist/index.js +350 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +35 -1
package/dist/index.d.ts
CHANGED
|
@@ -335,6 +335,9 @@ type OpenCodeHooks = {
|
|
|
335
335
|
};
|
|
336
336
|
type OpenCodePlugin = (input: unknown, options?: Record<string, unknown>) => Promise<OpenCodeHooks>;
|
|
337
337
|
|
|
338
|
+
/** Account name -> group name. Empty is never stored; `null` means no guard. */
|
|
339
|
+
type AccountGroups = Readonly<Record<string, string>>;
|
|
340
|
+
|
|
338
341
|
type LogLevel = "debug" | "info" | "notice" | "warn" | "error";
|
|
339
342
|
type LogMode = "silent" | "debug";
|
|
340
343
|
|
|
@@ -390,6 +393,12 @@ interface ClaudeCodeConfig {
|
|
|
390
393
|
*/
|
|
391
394
|
accountInProcess?: boolean;
|
|
392
395
|
accountFailover?: AccountFailoverMode;
|
|
396
|
+
/**
|
|
397
|
+
* The resolved `accountGroups` guard, or null for "no guard". Resolved once
|
|
398
|
+
* by `createClaudeCode` from the user's option, so every turn reads a value
|
|
399
|
+
* that has already been validated and warned about (h #g226).
|
|
400
|
+
*/
|
|
401
|
+
accountGroups?: AccountGroups | null;
|
|
393
402
|
providerID?: string;
|
|
394
403
|
skipPermissions?: boolean;
|
|
395
404
|
/**
|
|
@@ -531,6 +540,35 @@ interface ClaudeCodeProviderSettings {
|
|
|
531
540
|
* error. See README "Account failover".
|
|
532
541
|
*/
|
|
533
542
|
accountFailover?: AccountFailoverMode;
|
|
543
|
+
/**
|
|
544
|
+
* Which accounts may see each other's conversations. A map from an account
|
|
545
|
+
* name in `accounts` to a group name, e.g. `{ "appical": "work" }`.
|
|
546
|
+
*
|
|
547
|
+
* **Off unless set**, and unset is the default: with no map, or an empty one,
|
|
548
|
+
* every account behaves as one group and nothing about today's behaviour
|
|
549
|
+
* changes. Every account not named here, `default` included, is in one
|
|
550
|
+
* implicit group, so naming the one account that must stay apart is the whole
|
|
551
|
+
* configuration.
|
|
552
|
+
*
|
|
553
|
+
* When it is set, the plugin only ever moves a conversation between accounts
|
|
554
|
+
* in the SAME group: the by-hand switch (picking a model under another
|
|
555
|
+
* account's provider), the account-switch form, the override that form sets,
|
|
556
|
+
* and `crossAccountResume`. A switch ACROSS groups sends the other account
|
|
557
|
+
* nothing at all, not the Claude transcript and not a text replay of the
|
|
558
|
+
* thread either, because the replay is the same history by another route. The
|
|
559
|
+
* turn there starts fresh with only the current message and says so once, in
|
|
560
|
+
* one `▌ **account group:**` note naming both accounts. The conversation is
|
|
561
|
+
* left intact on the account it was on, so switching back continues it.
|
|
562
|
+
*
|
|
563
|
+
* The switch form offers only accounts in the limited account's own group,
|
|
564
|
+
* and with none left it behaves as a single-account install does.
|
|
565
|
+
*
|
|
566
|
+
* An account name that is not in `accounts` is a typo, so it is ignored with
|
|
567
|
+
* one WARN rather than silently creating a group that guards nothing. The
|
|
568
|
+
* resolved map is printed in the startup block and in
|
|
569
|
+
* `/claude-code-doctor`, names only.
|
|
570
|
+
*/
|
|
571
|
+
accountGroups?: Record<string, string>;
|
|
534
572
|
/**
|
|
535
573
|
* Model that subagents run on when their own definition pins nothing.
|
|
536
574
|
* Unset means no implicit override at all, so an agent keeps inheriting the
|
package/dist/index.js
CHANGED
|
@@ -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;
|
|
@@ -4544,7 +4678,7 @@ function forkSiblingSignature(sessionKey2) {
|
|
|
4544
4678
|
blanked[3] = "*";
|
|
4545
4679
|
return blanked.join("::");
|
|
4546
4680
|
}
|
|
4547
|
-
function modelSiblingSignature(sessionKey2) {
|
|
4681
|
+
function modelSiblingSignature(sessionKey2, accountProviders) {
|
|
4548
4682
|
const parts = sessionKey2.split("::");
|
|
4549
4683
|
if (parts.length < 5) return null;
|
|
4550
4684
|
if (parts[2] === "compaction") return null;
|
|
@@ -4552,7 +4686,8 @@ function modelSiblingSignature(sessionKey2) {
|
|
|
4552
4686
|
if (withoutEffort.length < 5) return null;
|
|
4553
4687
|
const blanked = [...withoutEffort];
|
|
4554
4688
|
blanked[1] = "*";
|
|
4555
|
-
|
|
4689
|
+
const signature = blanked.join("::");
|
|
4690
|
+
return accountProviders && accountProviders.size > 0 ? blankAccountProvider(signature, accountProviders) : signature;
|
|
4556
4691
|
}
|
|
4557
4692
|
function moveForkFingerprint(fromKey, toKey) {
|
|
4558
4693
|
const record = fingerprints.get(fromKey);
|
|
@@ -4657,7 +4792,7 @@ function isDigest(value) {
|
|
|
4657
4792
|
}
|
|
4658
4793
|
function isRecord2(value) {
|
|
4659
4794
|
const record = value;
|
|
4660
|
-
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");
|
|
4661
4796
|
}
|
|
4662
4797
|
function load() {
|
|
4663
4798
|
if (records) return records;
|
|
@@ -4703,17 +4838,27 @@ function save(update) {
|
|
|
4703
4838
|
});
|
|
4704
4839
|
}
|
|
4705
4840
|
}
|
|
4706
|
-
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
|
|
4841
|
+
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where) {
|
|
4707
4842
|
const chain = conversationDigests(prompt);
|
|
4708
4843
|
if (chain.length === 0) return;
|
|
4709
4844
|
const shape2 = conversationDigests(prompt, { assistantContent: false });
|
|
4845
|
+
const configDir = where?.configDir;
|
|
4846
|
+
const account = where?.account;
|
|
4710
4847
|
const current = load().get(sessionKey2);
|
|
4711
|
-
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)) {
|
|
4712
4849
|
return;
|
|
4713
4850
|
}
|
|
4714
4851
|
save((merged) => {
|
|
4715
4852
|
merged.delete(sessionKey2);
|
|
4716
|
-
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
|
+
});
|
|
4717
4862
|
});
|
|
4718
4863
|
}
|
|
4719
4864
|
function forgetResumePoint(sessionKey2) {
|
|
@@ -4755,18 +4900,31 @@ function findResumePoint(opts) {
|
|
|
4755
4900
|
return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
|
|
4756
4901
|
}
|
|
4757
4902
|
function findSiblingResumePoint(opts) {
|
|
4758
|
-
const
|
|
4903
|
+
const providers = opts.accountProviders;
|
|
4904
|
+
const signature = modelSiblingSignature(opts.sessionKey, providers);
|
|
4759
4905
|
if (!signature) {
|
|
4760
4906
|
opts.onRefused?.("no-sibling-possible");
|
|
4761
4907
|
return void 0;
|
|
4762
4908
|
}
|
|
4909
|
+
const ownAccount = providers ? sessionKeyAccount(opts.sessionKey, providers) : void 0;
|
|
4763
4910
|
let best;
|
|
4764
4911
|
let bestUpdatedAt = -1;
|
|
4765
4912
|
let refusal;
|
|
4766
4913
|
for (const [key, record] of load()) {
|
|
4767
4914
|
if (key === opts.sessionKey) continue;
|
|
4768
|
-
if (modelSiblingSignature(key) !== signature) continue;
|
|
4769
|
-
|
|
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) {
|
|
4770
4928
|
refusal = "sibling-another-claude-binary";
|
|
4771
4929
|
continue;
|
|
4772
4930
|
}
|
|
@@ -4780,7 +4938,7 @@ function findSiblingResumePoint(opts) {
|
|
|
4780
4938
|
refusal = "sibling-still-writing";
|
|
4781
4939
|
continue;
|
|
4782
4940
|
}
|
|
4783
|
-
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4941
|
+
if (!existsSync2(opts.transcriptPath(record.claudeSessionId, record.configDir))) {
|
|
4784
4942
|
refusal = "sibling-transcript-gone";
|
|
4785
4943
|
continue;
|
|
4786
4944
|
}
|
|
@@ -4789,7 +4947,8 @@ function findSiblingResumePoint(opts) {
|
|
|
4789
4947
|
best = {
|
|
4790
4948
|
claudeSessionId: record.claudeSessionId,
|
|
4791
4949
|
siblingKey: key,
|
|
4792
|
-
matched: record.chain.length
|
|
4950
|
+
matched: record.chain.length,
|
|
4951
|
+
...crossAccount ? { siblingAccount, siblingConfigDir: record.configDir } : {}
|
|
4793
4952
|
};
|
|
4794
4953
|
}
|
|
4795
4954
|
}
|
|
@@ -4800,6 +4959,35 @@ function findSiblingResumePoint(opts) {
|
|
|
4800
4959
|
}
|
|
4801
4960
|
return best;
|
|
4802
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
|
+
}
|
|
4803
4991
|
|
|
4804
4992
|
// src/permission-presets.ts
|
|
4805
4993
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
@@ -5472,6 +5660,9 @@ async function deleteActiveProcessAndWait(key, options = {}) {
|
|
|
5472
5660
|
function getClaudeSessionId(key) {
|
|
5473
5661
|
return claudeSessions.get(key);
|
|
5474
5662
|
}
|
|
5663
|
+
function listClaudeSessionKeys() {
|
|
5664
|
+
return claudeSessions.keys();
|
|
5665
|
+
}
|
|
5475
5666
|
var MAX_CLAUDE_SESSION_ENTRIES = 64;
|
|
5476
5667
|
function claudeSessionIsBusy(key) {
|
|
5477
5668
|
if (activeProcesses.has(key)) return true;
|
|
@@ -6151,6 +6342,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6151
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.",
|
|
6152
6343
|
"MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
|
|
6153
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",
|
|
6154
6347
|
"agent effort override refused: unknown level",
|
|
6155
6348
|
"agent model override refused: unknown model",
|
|
6156
6349
|
"agent prompt cache ttl refused: unknown value",
|
|
@@ -6261,6 +6454,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6261
6454
|
"replaying stdout the child emitted between turns",
|
|
6262
6455
|
"replaying the conversation as text: no claude session was available to continue",
|
|
6263
6456
|
"replaying this conversation as text on the other account",
|
|
6457
|
+
"replaying this conversation rather than carrying it across accounts",
|
|
6264
6458
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
6265
6459
|
"resuming the claude session from before the restart instead of replaying it",
|
|
6266
6460
|
"shared Claude capability already exists; leaving untouched",
|
|
@@ -6268,6 +6462,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
6268
6462
|
"skipping local MCP server with no command",
|
|
6269
6463
|
"skipping remote MCP server with no url",
|
|
6270
6464
|
"stale scratch directory sweep failed",
|
|
6465
|
+
"starting fresh on this account: it is in another account group",
|
|
6271
6466
|
"task_batch resolving with child results missing",
|
|
6272
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",
|
|
6273
6468
|
"the interactive turn did not take a forwarded user message",
|
|
@@ -6403,7 +6598,13 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
6403
6598
|
// doctor table already prints the same class of value (providers, accounts,
|
|
6404
6599
|
// proxyTools, MCP servers), so a bundle is no more revealing than the report
|
|
6405
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",
|
|
6406
6605
|
accounts: "name",
|
|
6606
|
+
accountGroups: "name",
|
|
6607
|
+
group: "name",
|
|
6407
6608
|
agent: "name",
|
|
6408
6609
|
bundled: "name",
|
|
6409
6610
|
excluded: "name",
|
|
@@ -7029,6 +7230,15 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
7029
7230
|
cwd,
|
|
7030
7231
|
providers: Object.keys(providers),
|
|
7031
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
|
+
),
|
|
7032
7242
|
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
7033
7243
|
mcpServers,
|
|
7034
7244
|
permissionPresets: Object.entries(providers).map(
|
|
@@ -7301,6 +7511,9 @@ function formatDoctorReport(report) {
|
|
|
7301
7511
|
lines.push(`| cwd | \`${report.cwd.resolved}\` (${report.cwd.source}) |`);
|
|
7302
7512
|
lines.push(`| providers | ${list(report.providers)} |`);
|
|
7303
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
|
+
);
|
|
7304
7517
|
lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
|
|
7305
7518
|
lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
|
|
7306
7519
|
lines.push(`| permissionPreset | ${describePermissionPresets(report.permissionPresets)} |`);
|
|
@@ -7554,6 +7767,7 @@ async function gatherDoctorReport(options) {
|
|
|
7554
7767
|
cwd: base.cwd,
|
|
7555
7768
|
providers: base.providers,
|
|
7556
7769
|
accounts: base.accounts,
|
|
7770
|
+
accountGroups: base.accountGroups,
|
|
7557
7771
|
proxyTools: base.proxyTools,
|
|
7558
7772
|
mcpServers: base.mcpServers,
|
|
7559
7773
|
permissionPresets: base.permissionPresets,
|
|
@@ -8398,6 +8612,7 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
8398
8612
|
SILENT_TURN_MARKER,
|
|
8399
8613
|
FAILOVER_MARKER,
|
|
8400
8614
|
ACCOUNT_BLOCK_MARKER,
|
|
8615
|
+
ACCOUNT_GROUP_MARKER,
|
|
8401
8616
|
USAGE_LIMIT_MARKER,
|
|
8402
8617
|
MODEL_FALLBACK_MARKER,
|
|
8403
8618
|
UNATTENDED_REPLAY_MARKER,
|
|
@@ -13930,6 +14145,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13930
14145
|
const sourceAccount = normalizeAccountName(
|
|
13931
14146
|
this.config.account ?? DEFAULT_ACCOUNT
|
|
13932
14147
|
);
|
|
14148
|
+
const accountProviders = accountProviderMap(this.config.failoverAccounts);
|
|
14149
|
+
const groups = this.config.accountGroups ?? null;
|
|
13933
14150
|
const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
|
|
13934
14151
|
let failover = compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
|
|
13935
14152
|
account: sourceAccount,
|
|
@@ -14091,7 +14308,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14091
14308
|
}
|
|
14092
14309
|
const failoverAnswer = compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
|
|
14093
14310
|
sourceAccount,
|
|
14094
|
-
candidates: failoverCandidates(
|
|
14311
|
+
candidates: failoverCandidates(
|
|
14312
|
+
this.config.failoverAccounts,
|
|
14313
|
+
sourceAccount,
|
|
14314
|
+
groups
|
|
14315
|
+
)
|
|
14095
14316
|
});
|
|
14096
14317
|
if (failoverAnswer?.kind === "stop") {
|
|
14097
14318
|
log.warn("account failover declined; ending the turn", {
|
|
@@ -14173,8 +14394,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14173
14394
|
// Every account's own directory, plus this model's configured one:
|
|
14174
14395
|
// `configDir` is an option, so an account's directory is not always
|
|
14175
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.
|
|
14176
14400
|
accountConfigDirs: [
|
|
14177
|
-
...(this.config.failoverAccounts
|
|
14401
|
+
...accountsInGroupOf(sourceAccount, this.config.failoverAccounts, groups).map(
|
|
14402
|
+
configDirForAccount
|
|
14403
|
+
),
|
|
14178
14404
|
resolveConfigDir(this.config.configDir)
|
|
14179
14405
|
]
|
|
14180
14406
|
});
|
|
@@ -14208,6 +14434,32 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14208
14434
|
if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
|
|
14209
14435
|
includeHistoryContext = true;
|
|
14210
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
|
+
}
|
|
14211
14463
|
let forkFromClaudeSessionId;
|
|
14212
14464
|
if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && failoverAnswer?.kind !== "switch") {
|
|
14213
14465
|
const parent = findForkParent({
|
|
@@ -14251,7 +14503,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14251
14503
|
let replayReason = "no-session-for-key";
|
|
14252
14504
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
14253
14505
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
14254
|
-
const transcriptPath2 = (id) => join13(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
14506
|
+
const transcriptPath2 = (id, dir) => join13(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
14255
14507
|
const resumePoint = findResumePoint({
|
|
14256
14508
|
sessionKey: sk,
|
|
14257
14509
|
prompt: options.prompt,
|
|
@@ -14279,25 +14531,70 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14279
14531
|
// timers on everything it touches (the same reason the fork lookup
|
|
14280
14532
|
// above uses it).
|
|
14281
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),
|
|
14282
14538
|
onRefused: (reason) => {
|
|
14283
14539
|
replayReason = reason;
|
|
14284
14540
|
}
|
|
14285
14541
|
});
|
|
14286
14542
|
if (sibling) {
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
if (
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
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
|
+
});
|
|
14299
14578
|
} else {
|
|
14300
|
-
|
|
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
|
+
}
|
|
14301
14598
|
}
|
|
14302
14599
|
}
|
|
14303
14600
|
}
|
|
@@ -14379,7 +14676,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14379
14676
|
}
|
|
14380
14677
|
const failoverAccounts = failoverCandidates(
|
|
14381
14678
|
this.config.failoverAccounts,
|
|
14382
|
-
sourceAccount
|
|
14679
|
+
sourceAccount,
|
|
14680
|
+
groups
|
|
14383
14681
|
);
|
|
14384
14682
|
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
|
|
14385
14683
|
// rate-limit error, never the form. See `TurnMode`.
|
|
@@ -14971,6 +15269,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14971
15269
|
});
|
|
14972
15270
|
state.endTextBlock();
|
|
14973
15271
|
}
|
|
15272
|
+
if (accountGroupNote) {
|
|
15273
|
+
controller.enqueue({
|
|
15274
|
+
type: "text-delta",
|
|
15275
|
+
id: state.startTextBlock(),
|
|
15276
|
+
delta: accountGroupNote
|
|
15277
|
+
});
|
|
15278
|
+
state.endTextBlock();
|
|
15279
|
+
}
|
|
14974
15280
|
if (failoverNote) {
|
|
14975
15281
|
controller.enqueue({
|
|
14976
15282
|
type: "text-delta",
|
|
@@ -14983,7 +15289,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14983
15289
|
const completeResult = (msg) => {
|
|
14984
15290
|
if (state.controllerClosed) return;
|
|
14985
15291
|
if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
|
|
14986
|
-
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
|
+
});
|
|
14987
15296
|
}
|
|
14988
15297
|
if (deliverPendingCompletions(state)) {
|
|
14989
15298
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
@@ -16322,6 +16631,12 @@ function applyPermissionPreset(settings, defaultProxyTools) {
|
|
|
16322
16631
|
}
|
|
16323
16632
|
return resolved;
|
|
16324
16633
|
}
|
|
16634
|
+
function accountGroups(settings) {
|
|
16635
|
+
return resolveAccountGroups(
|
|
16636
|
+
settings.accountGroups,
|
|
16637
|
+
settings.failoverAccounts ?? settings.accounts
|
|
16638
|
+
);
|
|
16639
|
+
}
|
|
16325
16640
|
function createClaudeCode(settings = {}) {
|
|
16326
16641
|
if (settings.logging) {
|
|
16327
16642
|
configureLogger({
|
|
@@ -16352,6 +16667,11 @@ function createClaudeCode(settings = {}) {
|
|
|
16352
16667
|
// unset option must stay unset rather than being written to one mode or
|
|
16353
16668
|
// the other in only one of the two entry points (h #g194).
|
|
16354
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),
|
|
16355
16675
|
providerID: settings.providerID,
|
|
16356
16676
|
skipPermissions: preset?.skipPermissions ?? settings.skipPermissions ?? true,
|
|
16357
16677
|
permissionMode: preset?.permissionMode ?? settings.permissionMode,
|