@khalilgharbaoui/opencode-claude-code-plugin 0.52.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +519 -292
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +21 -11
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 join15 } from "path";
|
|
3
3
|
|
|
4
4
|
// src/ids.ts
|
|
5
5
|
import { randomInt } from "crypto";
|
|
@@ -1844,6 +1844,9 @@ function failoverUntil(resetsAt) {
|
|
|
1844
1844
|
return resetsAtToMs(resetsAt);
|
|
1845
1845
|
}
|
|
1846
1846
|
|
|
1847
|
+
// src/subagent-dispatch.ts
|
|
1848
|
+
import { join as join4 } from "path";
|
|
1849
|
+
|
|
1847
1850
|
// src/agent-models.ts
|
|
1848
1851
|
import { readFile, readdir } from "fs/promises";
|
|
1849
1852
|
import path2 from "path";
|
|
@@ -2441,6 +2444,252 @@ function agentDirectories(home, projectDirectory) {
|
|
|
2441
2444
|
return directories;
|
|
2442
2445
|
}
|
|
2443
2446
|
|
|
2447
|
+
// src/dispatch-choice-store.ts
|
|
2448
|
+
import { mkdirSync as mkdirSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
|
|
2449
|
+
import { homedir as homedir3 } from "os";
|
|
2450
|
+
import { join as join2 } from "path";
|
|
2451
|
+
var MAX_DISPATCH_CHOICE_SESSIONS = 128;
|
|
2452
|
+
var MAX_DISPATCH_CHOICES_PER_SESSION = 32;
|
|
2453
|
+
var DISPATCH_CHOICE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2454
|
+
var sessions = null;
|
|
2455
|
+
var pathOverride = null;
|
|
2456
|
+
function dispatchChoiceStorePath() {
|
|
2457
|
+
if (pathOverride) return pathOverride;
|
|
2458
|
+
const stateRoot = process.env.XDG_STATE_HOME || join2(homedir3(), ".local", "state");
|
|
2459
|
+
return join2(stateRoot, "opencode-claude-code-plugin", "subagent-dispatch.json");
|
|
2460
|
+
}
|
|
2461
|
+
function isChoice(value) {
|
|
2462
|
+
const choice = value;
|
|
2463
|
+
if (!choice || typeof choice !== "object" || Array.isArray(choice)) return false;
|
|
2464
|
+
const ok = (field) => field === void 0 || typeof field === "string";
|
|
2465
|
+
if (!ok(choice.model) || !ok(choice.effort) || !ok(choice.account)) return false;
|
|
2466
|
+
return !!(choice.model || choice.effort || choice.account);
|
|
2467
|
+
}
|
|
2468
|
+
function isSessionRecord(value) {
|
|
2469
|
+
const record = value;
|
|
2470
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) return false;
|
|
2471
|
+
if (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt)) {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
const choices = record.choices;
|
|
2475
|
+
if (!choices || typeof choices !== "object" || Array.isArray(choices)) return false;
|
|
2476
|
+
return Object.values(choices).every(isChoice);
|
|
2477
|
+
}
|
|
2478
|
+
function trimChoices(choices) {
|
|
2479
|
+
const entries = Object.entries(choices).filter(([, choice]) => isChoice(choice));
|
|
2480
|
+
return Object.fromEntries(entries.slice(-MAX_DISPATCH_CHOICES_PER_SESSION));
|
|
2481
|
+
}
|
|
2482
|
+
function readFile2(file) {
|
|
2483
|
+
const out = /* @__PURE__ */ new Map();
|
|
2484
|
+
try {
|
|
2485
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
2486
|
+
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
2487
|
+
if (!entries || typeof entries !== "object") return out;
|
|
2488
|
+
const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
|
|
2489
|
+
const valid = Object.entries(entries).filter((entry) => isSessionRecord(entry[1])).filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
|
2490
|
+
for (const [key, record] of valid) out.set(key, record);
|
|
2491
|
+
} catch {
|
|
2492
|
+
}
|
|
2493
|
+
return out;
|
|
2494
|
+
}
|
|
2495
|
+
function load() {
|
|
2496
|
+
if (!sessions) sessions = readFile2(dispatchChoiceStorePath());
|
|
2497
|
+
return sessions;
|
|
2498
|
+
}
|
|
2499
|
+
function save(update) {
|
|
2500
|
+
const file = dispatchChoiceStorePath();
|
|
2501
|
+
const merged = readFile2(file);
|
|
2502
|
+
update(merged);
|
|
2503
|
+
const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
|
|
2504
|
+
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).filter(([, record]) => Object.keys(record.choices).length > 0).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_DISPATCH_CHOICE_SESSIONS);
|
|
2505
|
+
sessions = new Map(kept);
|
|
2506
|
+
try {
|
|
2507
|
+
mkdirSync2(join2(file, ".."), { recursive: true, mode: 448 });
|
|
2508
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2509
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
2510
|
+
mode: 384
|
|
2511
|
+
});
|
|
2512
|
+
renameSync2(tmp, file);
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
log.debug("could not write the subagent dispatch choice store", {
|
|
2515
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
function readDispatchChoice(opencodeSessionId, agent) {
|
|
2520
|
+
if (!opencodeSessionId || opencodeSessionId === "default") return void 0;
|
|
2521
|
+
const record = load().get(opencodeSessionId);
|
|
2522
|
+
const choice = record?.choices[agent];
|
|
2523
|
+
return choice ? { ...choice } : void 0;
|
|
2524
|
+
}
|
|
2525
|
+
function writeDispatchChoice(opencodeSessionId, agent, choice) {
|
|
2526
|
+
if (!opencodeSessionId || opencodeSessionId === "default" || !agent) return;
|
|
2527
|
+
const stored = {
|
|
2528
|
+
...choice.model ? { model: choice.model } : {},
|
|
2529
|
+
...choice.effort ? { effort: choice.effort } : {},
|
|
2530
|
+
...choice.account ? { account: choice.account } : {}
|
|
2531
|
+
};
|
|
2532
|
+
if (!isChoice(stored)) return;
|
|
2533
|
+
const current = load().get(opencodeSessionId)?.choices[agent];
|
|
2534
|
+
if (current && current.model === stored.model && current.effort === stored.effort && current.account === stored.account) {
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
save((merged) => {
|
|
2538
|
+
const record = merged.get(opencodeSessionId) ?? { choices: {}, updatedAt: 0 };
|
|
2539
|
+
const choices = { ...record.choices };
|
|
2540
|
+
delete choices[agent];
|
|
2541
|
+
choices[agent] = stored;
|
|
2542
|
+
merged.delete(opencodeSessionId);
|
|
2543
|
+
merged.set(opencodeSessionId, {
|
|
2544
|
+
choices: trimChoices(choices),
|
|
2545
|
+
updatedAt: Date.now()
|
|
2546
|
+
});
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
function forgetDispatchChoices(opencodeSessionId) {
|
|
2550
|
+
if (!load().has(opencodeSessionId)) return;
|
|
2551
|
+
save((merged) => {
|
|
2552
|
+
merged.delete(opencodeSessionId);
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// src/tmp.ts
|
|
2557
|
+
import * as fs from "fs";
|
|
2558
|
+
import * as os from "os";
|
|
2559
|
+
import * as path3 from "path";
|
|
2560
|
+
var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
|
|
2561
|
+
var PID_TMP_DIR = path3.join(
|
|
2562
|
+
os.tmpdir(),
|
|
2563
|
+
`${PLUGIN_TMP_PREFIX}${process.pid}`
|
|
2564
|
+
);
|
|
2565
|
+
var currentDir = null;
|
|
2566
|
+
var createdDirs = /* @__PURE__ */ new Set();
|
|
2567
|
+
var registered = false;
|
|
2568
|
+
function currentUid() {
|
|
2569
|
+
return typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
2570
|
+
}
|
|
2571
|
+
function untrustworthyReason(dir) {
|
|
2572
|
+
let stat;
|
|
2573
|
+
try {
|
|
2574
|
+
stat = fs.lstatSync(dir);
|
|
2575
|
+
} catch {
|
|
2576
|
+
return null;
|
|
2577
|
+
}
|
|
2578
|
+
if (stat.isSymbolicLink()) return "path is a symlink";
|
|
2579
|
+
if (!stat.isDirectory()) return "path is not a directory";
|
|
2580
|
+
const uid = currentUid();
|
|
2581
|
+
if (uid !== void 0 && stat.uid !== uid) {
|
|
2582
|
+
return `directory is owned by uid ${stat.uid}, not ${uid}`;
|
|
2583
|
+
}
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2586
|
+
function ensureDir(dir) {
|
|
2587
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2588
|
+
try {
|
|
2589
|
+
fs.chmodSync(dir, 448);
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function chooseDir() {
|
|
2594
|
+
const reason = untrustworthyReason(PID_TMP_DIR);
|
|
2595
|
+
if (!reason) return PID_TMP_DIR;
|
|
2596
|
+
const fallback = fs.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
|
|
2597
|
+
log.warn("refusing the pid-named scratch directory", {
|
|
2598
|
+
path: PID_TMP_DIR,
|
|
2599
|
+
reason,
|
|
2600
|
+
fallback
|
|
2601
|
+
});
|
|
2602
|
+
return fallback;
|
|
2603
|
+
}
|
|
2604
|
+
function registerExitCleanup() {
|
|
2605
|
+
if (registered) return;
|
|
2606
|
+
registered = true;
|
|
2607
|
+
process.on("exit", () => {
|
|
2608
|
+
for (const dir of createdDirs) {
|
|
2609
|
+
try {
|
|
2610
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
|
|
2617
|
+
function isProcessAlive(pid) {
|
|
2618
|
+
try {
|
|
2619
|
+
process.kill(pid, 0);
|
|
2620
|
+
return true;
|
|
2621
|
+
} catch (err) {
|
|
2622
|
+
return err.code === "EPERM";
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
function sweepStalePluginTmpDirs(keep) {
|
|
2626
|
+
const removed = [];
|
|
2627
|
+
if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
|
|
2628
|
+
const root = os.tmpdir();
|
|
2629
|
+
let entries;
|
|
2630
|
+
try {
|
|
2631
|
+
entries = fs.readdirSync(root);
|
|
2632
|
+
} catch {
|
|
2633
|
+
return removed;
|
|
2634
|
+
}
|
|
2635
|
+
const uid = currentUid();
|
|
2636
|
+
const keepPath = keep ? path3.resolve(keep) : null;
|
|
2637
|
+
for (const entry of entries) {
|
|
2638
|
+
const match = PID_DIR_NAME.exec(entry);
|
|
2639
|
+
if (!match) continue;
|
|
2640
|
+
const pid = Number(match[1]);
|
|
2641
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) continue;
|
|
2642
|
+
if (pid === process.pid) continue;
|
|
2643
|
+
const dir = path3.join(root, entry);
|
|
2644
|
+
if (keepPath && path3.resolve(dir) === keepPath) continue;
|
|
2645
|
+
let stat;
|
|
2646
|
+
try {
|
|
2647
|
+
stat = fs.lstatSync(dir);
|
|
2648
|
+
} catch {
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
|
|
2652
|
+
if (uid !== void 0 && stat.uid !== uid) continue;
|
|
2653
|
+
if (isProcessAlive(pid)) continue;
|
|
2654
|
+
try {
|
|
2655
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
2656
|
+
removed.push(dir);
|
|
2657
|
+
} catch (err) {
|
|
2658
|
+
log.debug("could not remove a stale scratch directory", {
|
|
2659
|
+
dir,
|
|
2660
|
+
error: String(err)
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (removed.length > 0) {
|
|
2665
|
+
log.info("removed stale plugin scratch directories", {
|
|
2666
|
+
count: removed.length,
|
|
2667
|
+
dirs: removed
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
return removed;
|
|
2671
|
+
}
|
|
2672
|
+
var swept = false;
|
|
2673
|
+
function sweepOnce(keep) {
|
|
2674
|
+
if (swept) return;
|
|
2675
|
+
swept = true;
|
|
2676
|
+
try {
|
|
2677
|
+
sweepStalePluginTmpDirs(keep);
|
|
2678
|
+
} catch (err) {
|
|
2679
|
+
log.warn("stale scratch directory sweep failed", { error: String(err) });
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
function pluginTmpDir() {
|
|
2683
|
+
if (currentDir === null || untrustworthyReason(currentDir) !== null) {
|
|
2684
|
+
currentDir = chooseDir();
|
|
2685
|
+
}
|
|
2686
|
+
ensureDir(currentDir);
|
|
2687
|
+
createdDirs.add(currentDir);
|
|
2688
|
+
registerExitCleanup();
|
|
2689
|
+
sweepOnce(currentDir);
|
|
2690
|
+
return currentDir;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2444
2693
|
// src/subagent-dispatch.ts
|
|
2445
2694
|
var SUBAGENT_DISPATCH_MARKER = "\u258C **subagent dispatch:**";
|
|
2446
2695
|
var SUBAGENT_DISPATCH_TOOL_CALL_PREFIX = "subagent_dispatch_";
|
|
@@ -2449,6 +2698,7 @@ var DEFAULT_ANSWER = "Default";
|
|
|
2449
2698
|
var LAST_ANSWER_PREFIX = "Same as last time";
|
|
2450
2699
|
var CUSTOMISE_ANSWER = "Customise\u2026";
|
|
2451
2700
|
var PER_TASK_ANSWER = "Per task\u2026";
|
|
2701
|
+
var PER_TYPE_ANSWER = "Per type\u2026";
|
|
2452
2702
|
var DISPATCH_COMBOS = [
|
|
2453
2703
|
{
|
|
2454
2704
|
model: "claude-haiku-5-5",
|
|
@@ -2487,30 +2737,49 @@ function isSubagentDispatchActive(input) {
|
|
|
2487
2737
|
if (input.configured !== "ask") return false;
|
|
2488
2738
|
return input.opencodeHasQuestion;
|
|
2489
2739
|
}
|
|
2490
|
-
var lastChoices = /* @__PURE__ */ new Map();
|
|
2491
|
-
var MAX_REMEMBERED_CHOICES = 256;
|
|
2492
|
-
function lastChoiceKey(parentSessionId, agent) {
|
|
2493
|
-
return `${parentSessionId}${KEY_SEPARATOR3}${agent}`;
|
|
2494
|
-
}
|
|
2495
2740
|
function rememberDispatchChoice(parentSessionId, agent, choice) {
|
|
2496
|
-
|
|
2497
|
-
const key = lastChoiceKey(parentSessionId, agent);
|
|
2498
|
-
lastChoices.delete(key);
|
|
2499
|
-
lastChoices.set(key, { ...choice });
|
|
2500
|
-
while (lastChoices.size > MAX_REMEMBERED_CHOICES) {
|
|
2501
|
-
const oldest = lastChoices.keys().next().value;
|
|
2502
|
-
if (oldest === void 0) break;
|
|
2503
|
-
lastChoices.delete(oldest);
|
|
2504
|
-
}
|
|
2741
|
+
writeDispatchChoice(parentSessionId, agent, choice);
|
|
2505
2742
|
}
|
|
2506
2743
|
function recallDispatchChoice(parentSessionId, agent) {
|
|
2507
|
-
return
|
|
2744
|
+
return readDispatchChoice(parentSessionId, agent);
|
|
2745
|
+
}
|
|
2746
|
+
function forgetDispatchChoicesForSession(opencodeSessionId) {
|
|
2747
|
+
forgetDispatchChoices(opencodeSessionId);
|
|
2748
|
+
}
|
|
2749
|
+
function usableRemembered(context, agent) {
|
|
2750
|
+
const remembered = recallDispatchChoice(context.parentSessionId, agent);
|
|
2751
|
+
if (!remembered) return void 0;
|
|
2752
|
+
const choice = {};
|
|
2753
|
+
if (remembered.model && Object.hasOwn(defaultModels, remembered.model)) {
|
|
2754
|
+
choice.model = remembered.model;
|
|
2755
|
+
}
|
|
2756
|
+
if (remembered.effort && REASONING_EFFORTS.includes(remembered.effort)) {
|
|
2757
|
+
choice.effort = remembered.effort;
|
|
2758
|
+
}
|
|
2759
|
+
let droppedAccount;
|
|
2760
|
+
if (remembered.account) {
|
|
2761
|
+
if (dispatchAccountCandidates(context).includes(remembered.account)) {
|
|
2762
|
+
choice.account = remembered.account;
|
|
2763
|
+
} else {
|
|
2764
|
+
droppedAccount = remembered.account;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (!choice.model && !choice.effort && !choice.account) {
|
|
2768
|
+
return droppedAccount ? { choice, droppedAccount } : void 0;
|
|
2769
|
+
}
|
|
2770
|
+
return droppedAccount ? { choice, droppedAccount } : { choice };
|
|
2508
2771
|
}
|
|
2509
2772
|
var CLAIM_TTL_MS = 15 * 6e4;
|
|
2510
2773
|
var MAX_CLAIMS = 512;
|
|
2511
2774
|
var claims = [];
|
|
2512
2775
|
var sessionChoices = /* @__PURE__ */ new Map();
|
|
2513
2776
|
var MAX_SESSION_CHOICES = 512;
|
|
2777
|
+
function promptMatchesClaim(claimPrompt, childPrompt) {
|
|
2778
|
+
if (childPrompt === claimPrompt) return true;
|
|
2779
|
+
if (claimPrompt.length === 0) return false;
|
|
2780
|
+
return childPrompt.endsWith(`
|
|
2781
|
+
${claimPrompt}`);
|
|
2782
|
+
}
|
|
2514
2783
|
function pruneClaims(now) {
|
|
2515
2784
|
for (let i = claims.length - 1; i >= 0; i--) {
|
|
2516
2785
|
if (now - claims[i].createdAt > CLAIM_TTL_MS) claims.splice(i, 1);
|
|
@@ -2541,7 +2810,9 @@ function lookupSessionChoice(sessionId) {
|
|
|
2541
2810
|
function hasCandidateClaim(agent, prompt, now = Date.now()) {
|
|
2542
2811
|
if (!agent || prompt === void 0) return false;
|
|
2543
2812
|
pruneClaims(now);
|
|
2544
|
-
return claims.some(
|
|
2813
|
+
return claims.some(
|
|
2814
|
+
(claim) => claim.agent === agent && promptMatchesClaim(claim.prompt, prompt)
|
|
2815
|
+
);
|
|
2545
2816
|
}
|
|
2546
2817
|
function claimDispatchChoice(input) {
|
|
2547
2818
|
const existing = sessionChoices.get(input.sessionId);
|
|
@@ -2550,9 +2821,14 @@ function claimDispatchChoice(input) {
|
|
|
2550
2821
|
return void 0;
|
|
2551
2822
|
}
|
|
2552
2823
|
pruneClaims(input.now ?? Date.now());
|
|
2553
|
-
const
|
|
2554
|
-
|
|
2555
|
-
);
|
|
2824
|
+
const belongsHere = (claim2) => claim2.parentSessionId === input.parentSessionId && claim2.agent === input.agent;
|
|
2825
|
+
const childPrompt = input.prompt;
|
|
2826
|
+
let index = claims.findIndex((claim2) => belongsHere(claim2) && claim2.prompt === childPrompt);
|
|
2827
|
+
if (index === -1) {
|
|
2828
|
+
index = claims.findIndex(
|
|
2829
|
+
(claim2) => belongsHere(claim2) && promptMatchesClaim(claim2.prompt, childPrompt)
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2556
2832
|
if (index === -1) return void 0;
|
|
2557
2833
|
const [claim] = claims.splice(index, 1);
|
|
2558
2834
|
sessionChoices.set(input.sessionId, claim.choice);
|
|
@@ -2636,7 +2912,11 @@ function comboLabel(model, effort) {
|
|
|
2636
2912
|
return effort ? `${model} / ${effort}` : model;
|
|
2637
2913
|
}
|
|
2638
2914
|
function describeChoice(choice, fallback) {
|
|
2639
|
-
|
|
2915
|
+
const combo = comboLabel(choice.model ?? fallback.model, choice.effort ?? fallback.effort);
|
|
2916
|
+
return choice.account ? `${combo} @${choice.account}` : combo;
|
|
2917
|
+
}
|
|
2918
|
+
function droppedAccountPhrase(account) {
|
|
2919
|
+
return ` The account "${account}" you picked last time is no longer available to this conversation, so this stays on the account you are on.`;
|
|
2640
2920
|
}
|
|
2641
2921
|
function availableCombos() {
|
|
2642
2922
|
return DISPATCH_COMBOS.filter((combo) => Object.hasOwn(defaultModels, combo.model));
|
|
@@ -2654,16 +2934,25 @@ function dispatchAccountCandidates(context) {
|
|
|
2654
2934
|
}
|
|
2655
2935
|
return out;
|
|
2656
2936
|
}
|
|
2657
|
-
function accountQuestion(context) {
|
|
2937
|
+
function accountQuestion(context, scope) {
|
|
2658
2938
|
const candidates = dispatchAccountCandidates(context);
|
|
2659
2939
|
if (candidates.length < 2) return null;
|
|
2660
2940
|
const source = candidates[0];
|
|
2941
|
+
const perType = "agent" in scope;
|
|
2942
|
+
const subject = perType ? `the "${scope.agent}" subagents` : "these subagents";
|
|
2943
|
+
const options = candidates.map((account) => ({
|
|
2944
|
+
label: account,
|
|
2945
|
+
description: account === source ? `Stay on this conversation's own account. This is what happens today.` : `Spawn ${subject} with "${account}"'s Claude config dir, so the work draws on that account's usage window.`
|
|
2946
|
+
}));
|
|
2947
|
+
if (!perType && scope.types > 1) {
|
|
2948
|
+
options.push({
|
|
2949
|
+
label: PER_TYPE_ANSWER,
|
|
2950
|
+
description: `Choose the account separately for each of these ${scope.types} agent types on the next screen.`
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2661
2953
|
return {
|
|
2662
|
-
question: "Which Claude account should these subagents run on?",
|
|
2663
|
-
options
|
|
2664
|
-
label: account,
|
|
2665
|
-
description: account === source ? `Stay on this conversation's own account. This is what happens today.` : `Spawn each subagent with "${account}"'s Claude config dir, so its work draws on that account's usage window.`
|
|
2666
|
-
}))
|
|
2954
|
+
question: perType ? `Which Claude account should the "${scope.agent}" subagents run on?` : "Which Claude account should these subagents run on?",
|
|
2955
|
+
options
|
|
2667
2956
|
};
|
|
2668
2957
|
}
|
|
2669
2958
|
function summaryQuestion(tasks, context) {
|
|
@@ -2674,13 +2963,14 @@ function summaryQuestion(tasks, context) {
|
|
|
2674
2963
|
});
|
|
2675
2964
|
const question = tasks.length === 1 ? `About to dispatch the "${tasks[0].agent}" subagent (${tasks[0].description || "no description"}). How should it run?` : `About to dispatch ${tasks.length} subagents (${counts.join(", ")}). How should they run?`;
|
|
2676
2965
|
const options = [];
|
|
2677
|
-
const remembered = types.map((agent) =>
|
|
2678
|
-
if (remembered.every((
|
|
2966
|
+
const remembered = types.map((agent) => usableRemembered(context, agent));
|
|
2967
|
+
if (remembered.every((entry) => entry !== void 0)) {
|
|
2968
|
+
const dropped = remembered.map((entry) => entry.droppedAccount).find((account) => !!account);
|
|
2679
2969
|
options.push({
|
|
2680
2970
|
label: LAST_ANSWER_PREFIX,
|
|
2681
2971
|
description: types.map(
|
|
2682
|
-
(agent, index) => `${agent}: ${describeChoice(remembered[index], context.describeDefault(agent))}`
|
|
2683
|
-
).join(". ")
|
|
2972
|
+
(agent, index) => `${agent}: ${describeChoice(remembered[index].choice, context.describeDefault(agent))}`
|
|
2973
|
+
).join(". ") + (dropped ? `.${droppedAccountPhrase(dropped)}` : "")
|
|
2684
2974
|
});
|
|
2685
2975
|
}
|
|
2686
2976
|
options.push({
|
|
@@ -2698,20 +2988,20 @@ function summaryQuestion(tasks, context) {
|
|
|
2698
2988
|
}
|
|
2699
2989
|
options.push({
|
|
2700
2990
|
label: CUSTOMISE_ANSWER,
|
|
2701
|
-
description: tasks.length === 1 ? "Choose the account, or type a model and effort of your own." : "Choose per agent type on the next screen, and per task after that."
|
|
2991
|
+
description: tasks.length === 1 ? "Choose the account, or type a model and effort of your own." : "Choose per agent type on the next screen, and per account or per task after that."
|
|
2702
2992
|
});
|
|
2703
2993
|
return { question, options };
|
|
2704
2994
|
}
|
|
2705
2995
|
function typeQuestion(agent, tasks, context) {
|
|
2706
2996
|
const mine = tasks.filter((task) => task.agent === agent);
|
|
2707
2997
|
const fallback = context.describeDefault(agent);
|
|
2708
|
-
const question = mine.length === 1 ? `"${agent}" (${mine[0].description || "no description"}): how should it run?` : `"${agent}" (${mine.length} subagents): how should they run
|
|
2998
|
+
const question = (mine.length === 1 ? `"${agent}" (${mine[0].description || "no description"}): how should it run?` : `"${agent}" (${mine.length} subagents): how should they run?`) + accountHint(context);
|
|
2709
2999
|
const options = [];
|
|
2710
|
-
const remembered =
|
|
3000
|
+
const remembered = usableRemembered(context, agent);
|
|
2711
3001
|
if (remembered) {
|
|
2712
3002
|
options.push({
|
|
2713
3003
|
label: LAST_ANSWER_PREFIX,
|
|
2714
|
-
description: `${describeChoice(remembered, fallback)}, the last thing you picked for "${agent}" in this conversation.`
|
|
3004
|
+
description: `${describeChoice(remembered.choice, fallback)}, the last thing you picked for "${agent}" in this conversation.` + (remembered.droppedAccount ? droppedAccountPhrase(remembered.droppedAccount) : "")
|
|
2715
3005
|
});
|
|
2716
3006
|
}
|
|
2717
3007
|
options.push({
|
|
@@ -2735,7 +3025,7 @@ function typeQuestion(agent, tasks, context) {
|
|
|
2735
3025
|
function taskQuestion(task, typeChoice, context) {
|
|
2736
3026
|
const fallback = context.describeDefault(task.agent);
|
|
2737
3027
|
const options = [];
|
|
2738
|
-
if (typeChoice && (typeChoice.model || typeChoice.effort)) {
|
|
3028
|
+
if (typeChoice && (typeChoice.model || typeChoice.effort || typeChoice.account)) {
|
|
2739
3029
|
options.push({
|
|
2740
3030
|
label: `Same as the rest (${describeChoice(typeChoice, fallback)})`,
|
|
2741
3031
|
description: `What you picked for every "${task.agent}" subagent in this dispatch.`
|
|
@@ -2749,10 +3039,15 @@ function taskQuestion(task, typeChoice, context) {
|
|
|
2749
3039
|
options.push({ label: comboLabel(combo.model, combo.effort), description: combo.description });
|
|
2750
3040
|
}
|
|
2751
3041
|
return {
|
|
2752
|
-
question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}`,
|
|
3042
|
+
question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}${accountHint(context)}`,
|
|
2753
3043
|
options
|
|
2754
3044
|
};
|
|
2755
3045
|
}
|
|
3046
|
+
function accountHint(context) {
|
|
3047
|
+
const candidates = dispatchAccountCandidates(context);
|
|
3048
|
+
if (candidates.length < 2) return "";
|
|
3049
|
+
return ` Add "@${candidates[1]}" to a typed answer to send it to that account.`;
|
|
3050
|
+
}
|
|
2756
3051
|
function header(text) {
|
|
2757
3052
|
return text.length <= 30 ? text : `${text.slice(0, 29)}\u2026`;
|
|
2758
3053
|
}
|
|
@@ -2787,17 +3082,26 @@ function createSubagentDispatchQuestion(sessionKey2, heldCallIds, tasks, context
|
|
|
2787
3082
|
fields: [{ kind: "all" }],
|
|
2788
3083
|
choices: /* @__PURE__ */ new Map(),
|
|
2789
3084
|
account: void 0,
|
|
3085
|
+
typeAccounts: /* @__PURE__ */ new Map(),
|
|
3086
|
+
typeChoices: /* @__PURE__ */ new Map(),
|
|
3087
|
+
droppedAccounts: [],
|
|
2790
3088
|
context
|
|
2791
3089
|
};
|
|
2792
3090
|
const summary = summaryQuestion(tasks, context);
|
|
2793
3091
|
return buildCall(entry, [{ header: "Subagents", ...summary }], toolCallId);
|
|
2794
3092
|
}
|
|
2795
|
-
function parseCustomChoice(answer) {
|
|
3093
|
+
function parseCustomChoice(answer, candidates = []) {
|
|
2796
3094
|
const words2 = answer.trim().split(/[\s,/]+/).map((word) => word.trim()).filter(Boolean);
|
|
2797
3095
|
if (words2.length === 0) return null;
|
|
2798
3096
|
const choice = {};
|
|
2799
3097
|
for (const word of words2) {
|
|
2800
3098
|
const lower = word.toLowerCase();
|
|
3099
|
+
if (lower.startsWith("@")) {
|
|
3100
|
+
const account = normalizeAccountName(lower.slice(1));
|
|
3101
|
+
if (!account || !candidates.includes(account)) return null;
|
|
3102
|
+
choice.account = account;
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
2801
3105
|
if (REASONING_EFFORTS.includes(lower)) {
|
|
2802
3106
|
choice.effort = lower;
|
|
2803
3107
|
continue;
|
|
@@ -2808,7 +3112,7 @@ function parseCustomChoice(answer) {
|
|
|
2808
3112
|
}
|
|
2809
3113
|
return null;
|
|
2810
3114
|
}
|
|
2811
|
-
return choice.model || choice.effort ? choice : null;
|
|
3115
|
+
return choice.model || choice.effort || choice.account ? choice : null;
|
|
2812
3116
|
}
|
|
2813
3117
|
function parseComboLabel(answer) {
|
|
2814
3118
|
for (const combo of availableCombos()) {
|
|
@@ -2818,7 +3122,7 @@ function parseComboLabel(answer) {
|
|
|
2818
3122
|
}
|
|
2819
3123
|
return null;
|
|
2820
3124
|
}
|
|
2821
|
-
function classifyAnswer(answer) {
|
|
3125
|
+
function classifyAnswer(answer, candidates = []) {
|
|
2822
3126
|
const trimmed = answer.trim();
|
|
2823
3127
|
if (!trimmed) return { kind: "default" };
|
|
2824
3128
|
if (trimmed === DEFAULT_ANSWER) return { kind: "default" };
|
|
@@ -2828,7 +3132,7 @@ function classifyAnswer(answer) {
|
|
|
2828
3132
|
if (trimmed.startsWith("Same as the rest")) return { kind: "sameAsRest" };
|
|
2829
3133
|
const combo = parseComboLabel(trimmed);
|
|
2830
3134
|
if (combo) return { kind: "choice", choice: combo };
|
|
2831
|
-
const custom = parseCustomChoice(trimmed);
|
|
3135
|
+
const custom = parseCustomChoice(trimmed, candidates);
|
|
2832
3136
|
if (custom) return { kind: "choice", choice: custom };
|
|
2833
3137
|
return { kind: "unknown", answer: trimmed };
|
|
2834
3138
|
}
|
|
@@ -2869,19 +3173,28 @@ function advance(entry, part) {
|
|
|
2869
3173
|
case "types":
|
|
2870
3174
|
return advanceTypes(entry, answers);
|
|
2871
3175
|
default:
|
|
2872
|
-
return
|
|
3176
|
+
return advanceDetail(entry, answers);
|
|
2873
3177
|
}
|
|
2874
3178
|
}
|
|
2875
3179
|
function applyToAll(entry, choice) {
|
|
2876
3180
|
for (const index of entry.tasks.keys()) entry.choices.set(index, { ...choice });
|
|
2877
3181
|
}
|
|
3182
|
+
function noteDroppedAccount(entry, agent, account) {
|
|
3183
|
+
if (entry.droppedAccounts.some((dropped) => dropped.agent === agent)) return;
|
|
3184
|
+
entry.droppedAccounts.push({ agent, account });
|
|
3185
|
+
}
|
|
2878
3186
|
function advanceSummary(entry, answers) {
|
|
2879
|
-
const
|
|
3187
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
3188
|
+
const answer = classifyAnswer(answers.get(entry.questions[0]) ?? "", candidates);
|
|
2880
3189
|
if (answer.kind === "customise") return typesForm(entry);
|
|
2881
3190
|
if (answer.kind === "last") {
|
|
2882
3191
|
for (const [index, task] of entry.tasks.entries()) {
|
|
2883
|
-
const remembered =
|
|
2884
|
-
if (remembered)
|
|
3192
|
+
const remembered = usableRemembered(entry.context, task.agent);
|
|
3193
|
+
if (!remembered) continue;
|
|
3194
|
+
if (remembered.droppedAccount) {
|
|
3195
|
+
noteDroppedAccount(entry, task.agent, remembered.droppedAccount);
|
|
3196
|
+
}
|
|
3197
|
+
entry.choices.set(index, { ...remembered.choice });
|
|
2885
3198
|
}
|
|
2886
3199
|
return release(entry, null);
|
|
2887
3200
|
}
|
|
@@ -2908,7 +3221,7 @@ function typesForm(entry) {
|
|
|
2908
3221
|
...typeQuestion(agent, entry.tasks, entry.context)
|
|
2909
3222
|
}));
|
|
2910
3223
|
const fields = types.map((agent) => ({ kind: "type", agent }));
|
|
2911
|
-
const account = accountQuestion(entry.context);
|
|
3224
|
+
const account = accountQuestion(entry.context, { types: types.length });
|
|
2912
3225
|
if (account) {
|
|
2913
3226
|
prompts.push({ header: "Account", ...account });
|
|
2914
3227
|
fields.push({ kind: "account" });
|
|
@@ -2920,25 +3233,30 @@ function typesForm(entry) {
|
|
|
2920
3233
|
call: buildCall(entry, prompts, questionCallId("types"))
|
|
2921
3234
|
};
|
|
2922
3235
|
}
|
|
3236
|
+
function readAccountAnswer(entry, raw, agent) {
|
|
3237
|
+
const trimmed = raw.trim();
|
|
3238
|
+
if (!trimmed || trimmed === DEFAULT_ANSWER) return void 0;
|
|
3239
|
+
const picked = normalizeAccountName(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
|
|
3240
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
3241
|
+
if (picked && candidates.includes(picked)) return picked;
|
|
3242
|
+
log.notice("subagent dispatch account answer refused", {
|
|
3243
|
+
sessionKey: entry.sessionKey,
|
|
3244
|
+
agent,
|
|
3245
|
+
answer: trimmed,
|
|
3246
|
+
candidates
|
|
3247
|
+
});
|
|
3248
|
+
return void 0;
|
|
3249
|
+
}
|
|
2923
3250
|
function advanceTypes(entry, answers) {
|
|
3251
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
2924
3252
|
const perTask = [];
|
|
2925
|
-
|
|
3253
|
+
let perTypeAccounts = false;
|
|
2926
3254
|
for (const [position, field] of entry.fields.entries()) {
|
|
2927
3255
|
const raw = answers.get(entry.questions[position]) ?? "";
|
|
2928
|
-
const answer = classifyAnswer(raw);
|
|
3256
|
+
const answer = classifyAnswer(raw, candidates);
|
|
2929
3257
|
if (field.kind === "account") {
|
|
2930
|
-
if (
|
|
2931
|
-
|
|
2932
|
-
const candidates = dispatchAccountCandidates(entry.context);
|
|
2933
|
-
if (candidates.includes(picked)) entry.account = picked;
|
|
2934
|
-
else {
|
|
2935
|
-
log.notice("subagent dispatch account answer refused", {
|
|
2936
|
-
sessionKey: entry.sessionKey,
|
|
2937
|
-
answer: answer.answer,
|
|
2938
|
-
candidates
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
3258
|
+
if (raw.trim() === PER_TYPE_ANSWER) perTypeAccounts = true;
|
|
3259
|
+
else entry.account = readAccountAnswer(entry, raw, null) ?? entry.account;
|
|
2942
3260
|
continue;
|
|
2943
3261
|
}
|
|
2944
3262
|
if (field.kind !== "type") continue;
|
|
@@ -2947,12 +3265,15 @@ function advanceTypes(entry, answers) {
|
|
|
2947
3265
|
continue;
|
|
2948
3266
|
}
|
|
2949
3267
|
if (answer.kind === "last") {
|
|
2950
|
-
const remembered =
|
|
2951
|
-
if (remembered)
|
|
3268
|
+
const remembered = usableRemembered(entry.context, field.agent);
|
|
3269
|
+
if (remembered?.droppedAccount) {
|
|
3270
|
+
noteDroppedAccount(entry, field.agent, remembered.droppedAccount);
|
|
3271
|
+
}
|
|
3272
|
+
if (remembered) applyTypeChoice(entry, field.agent, remembered.choice);
|
|
2952
3273
|
continue;
|
|
2953
3274
|
}
|
|
2954
3275
|
if (answer.kind === "choice") {
|
|
2955
|
-
|
|
3276
|
+
applyTypeChoice(entry, field.agent, answer.choice);
|
|
2956
3277
|
continue;
|
|
2957
3278
|
}
|
|
2958
3279
|
if (answer.kind === "unknown") {
|
|
@@ -2964,31 +3285,60 @@ function advanceTypes(entry, answers) {
|
|
|
2964
3285
|
}
|
|
2965
3286
|
}
|
|
2966
3287
|
for (const [index, task] of entry.tasks.entries()) {
|
|
2967
|
-
const choice = typeChoices.get(task.agent);
|
|
3288
|
+
const choice = entry.typeChoices.get(task.agent);
|
|
2968
3289
|
if (choice) entry.choices.set(index, { ...choice });
|
|
2969
3290
|
}
|
|
2970
|
-
if (perTask.length > 0
|
|
3291
|
+
if (perTask.length > 0 || perTypeAccounts) {
|
|
3292
|
+
return detailForm(entry, perTask, perTypeAccounts);
|
|
3293
|
+
}
|
|
2971
3294
|
return release(entry, null);
|
|
2972
3295
|
}
|
|
2973
|
-
function
|
|
3296
|
+
function applyTypeChoice(entry, agent, choice) {
|
|
3297
|
+
const { account, ...rest } = choice;
|
|
3298
|
+
if (account) entry.typeAccounts.set(agent, account);
|
|
3299
|
+
entry.typeChoices.set(agent, rest);
|
|
3300
|
+
}
|
|
3301
|
+
function detailForm(entry, perTaskAgents, perTypeAccounts) {
|
|
2974
3302
|
const prompts = [];
|
|
2975
3303
|
const fields = [];
|
|
3304
|
+
if (perTypeAccounts) {
|
|
3305
|
+
for (const agent of dispatchAgentTypes(entry.tasks)) {
|
|
3306
|
+
const account = accountQuestion(entry.context, { agent });
|
|
3307
|
+
if (!account) continue;
|
|
3308
|
+
prompts.push({ header: `${agent} account`, ...account });
|
|
3309
|
+
fields.push({ kind: "typeAccount", agent });
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
2976
3312
|
for (const [index, task] of entry.tasks.entries()) {
|
|
2977
|
-
if (!
|
|
3313
|
+
if (!perTaskAgents.includes(task.agent)) continue;
|
|
3314
|
+
const typeChoice = entry.typeChoices.get(task.agent);
|
|
3315
|
+
const typeAccount = entry.typeAccounts.get(task.agent);
|
|
2978
3316
|
prompts.push({
|
|
2979
3317
|
header: task.description || task.agent,
|
|
2980
|
-
...taskQuestion(
|
|
3318
|
+
...taskQuestion(
|
|
3319
|
+
task,
|
|
3320
|
+
typeChoice || typeAccount ? { ...typeChoice, ...typeAccount ? { account: typeAccount } : {} } : void 0,
|
|
3321
|
+
entry.context
|
|
3322
|
+
)
|
|
2981
3323
|
});
|
|
2982
3324
|
fields.push({ kind: "task", taskIndex: index });
|
|
2983
3325
|
}
|
|
2984
|
-
|
|
3326
|
+
if (prompts.length === 0) return release(entry, null);
|
|
3327
|
+
entry.stage = "detail";
|
|
2985
3328
|
entry.fields = fields;
|
|
2986
|
-
return { kind: "form", call: buildCall(entry, prompts, questionCallId("
|
|
3329
|
+
return { kind: "form", call: buildCall(entry, prompts, questionCallId("detail")) };
|
|
2987
3330
|
}
|
|
2988
|
-
function
|
|
3331
|
+
function advanceDetail(entry, answers) {
|
|
3332
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
2989
3333
|
for (const [position, field] of entry.fields.entries()) {
|
|
3334
|
+
const raw = answers.get(entry.questions[position]) ?? "";
|
|
3335
|
+
if (field.kind === "typeAccount") {
|
|
3336
|
+
const picked = readAccountAnswer(entry, raw, field.agent);
|
|
3337
|
+
if (picked) entry.typeAccounts.set(field.agent, picked);
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
2990
3340
|
if (field.kind !== "task") continue;
|
|
2991
|
-
const answer = classifyAnswer(
|
|
3341
|
+
const answer = classifyAnswer(raw, candidates);
|
|
2992
3342
|
if (answer.kind === "choice") {
|
|
2993
3343
|
entry.choices.set(field.taskIndex, answer.choice);
|
|
2994
3344
|
continue;
|
|
@@ -3008,13 +3358,18 @@ function advanceTasks(entry, answers) {
|
|
|
3008
3358
|
}
|
|
3009
3359
|
return release(entry, null);
|
|
3010
3360
|
}
|
|
3361
|
+
function droppedAccountNote(dropped) {
|
|
3362
|
+
const list2 = dropped.map((entry) => `"${entry.agent}" (was "${entry.account}")`).join(", ");
|
|
3363
|
+
return `
|
|
3364
|
+
${SUBAGENT_DISPATCH_MARKER} The account you last picked for ${list2} is no longer configured for this conversation, or no longer in its account group, so that work stays on the account you are on. Everything else you picked still applies.
|
|
3365
|
+
`;
|
|
3366
|
+
}
|
|
3011
3367
|
function release(entry, note) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
}
|
|
3368
|
+
for (const [index, task] of entry.tasks.entries()) {
|
|
3369
|
+
const current = entry.choices.get(index);
|
|
3370
|
+
const account = current?.account ?? entry.typeAccounts.get(task.agent) ?? entry.account;
|
|
3371
|
+
if (!account) continue;
|
|
3372
|
+
entry.choices.set(index, { ...current ?? {}, account });
|
|
3018
3373
|
}
|
|
3019
3374
|
const seen = /* @__PURE__ */ new Set();
|
|
3020
3375
|
for (const [index, task] of entry.tasks.entries()) {
|
|
@@ -3024,6 +3379,15 @@ function release(entry, note) {
|
|
|
3024
3379
|
seen.add(task.agent);
|
|
3025
3380
|
rememberDispatchChoice(entry.context.parentSessionId, task.agent, choice);
|
|
3026
3381
|
}
|
|
3382
|
+
if (!note && entry.droppedAccounts.length > 0) {
|
|
3383
|
+
log.notice("a remembered subagent dispatch account is no longer available", {
|
|
3384
|
+
sessionKey: entry.sessionKey,
|
|
3385
|
+
// `agent` rather than a key of its own: `dropped` already means a count
|
|
3386
|
+
// in the bundle allowlist, and these are agent names like every other.
|
|
3387
|
+
agent: entry.droppedAccounts.map((dropped) => dropped.agent)
|
|
3388
|
+
});
|
|
3389
|
+
note = droppedAccountNote(entry.droppedAccounts);
|
|
3390
|
+
}
|
|
3027
3391
|
return {
|
|
3028
3392
|
kind: "release",
|
|
3029
3393
|
heldCallIds: entry.heldCallIds,
|
|
@@ -3060,8 +3424,8 @@ import { execFile } from "child_process";
|
|
|
3060
3424
|
import { promisify } from "util";
|
|
3061
3425
|
|
|
3062
3426
|
// src/windows-spawn.ts
|
|
3063
|
-
import * as
|
|
3064
|
-
import * as
|
|
3427
|
+
import * as fs2 from "fs";
|
|
3428
|
+
import * as path4 from "path";
|
|
3065
3429
|
var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
3066
3430
|
function envValue(env, name) {
|
|
3067
3431
|
const direct = env[name];
|
|
@@ -3071,7 +3435,7 @@ function envValue(env, name) {
|
|
|
3071
3435
|
}
|
|
3072
3436
|
function defaultIsFile(candidate) {
|
|
3073
3437
|
try {
|
|
3074
|
-
return
|
|
3438
|
+
return fs2.statSync(candidate).isFile();
|
|
3075
3439
|
} catch {
|
|
3076
3440
|
return false;
|
|
3077
3441
|
}
|
|
@@ -3081,7 +3445,7 @@ function executableExtensions(env = process.env) {
|
|
|
3081
3445
|
return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
|
|
3082
3446
|
}
|
|
3083
3447
|
function isBatchFile(file) {
|
|
3084
|
-
const ext =
|
|
3448
|
+
const ext = path4.win32.extname(file).toLowerCase();
|
|
3085
3449
|
return ext === ".cmd" || ext === ".bat";
|
|
3086
3450
|
}
|
|
3087
3451
|
function resolveWindowsCommand(command, deps = {}) {
|
|
@@ -3089,7 +3453,7 @@ function resolveWindowsCommand(command, deps = {}) {
|
|
|
3089
3453
|
const isFile = deps.isFile ?? defaultIsFile;
|
|
3090
3454
|
const extensions = executableExtensions(env);
|
|
3091
3455
|
const candidatesFor = (base) => {
|
|
3092
|
-
const ext =
|
|
3456
|
+
const ext = path4.win32.extname(base).toLowerCase();
|
|
3093
3457
|
if (ext && extensions.includes(ext)) return [base];
|
|
3094
3458
|
return [...extensions.map((candidate) => base + candidate), base];
|
|
3095
3459
|
};
|
|
@@ -3100,10 +3464,10 @@ function resolveWindowsCommand(command, deps = {}) {
|
|
|
3100
3464
|
return null;
|
|
3101
3465
|
}
|
|
3102
3466
|
const pathValue = envValue(env, "PATH") ?? "";
|
|
3103
|
-
for (const entry of pathValue.split(
|
|
3467
|
+
for (const entry of pathValue.split(path4.win32.delimiter)) {
|
|
3104
3468
|
const directory = entry.trim().replace(/^"(.*)"$/, "$1");
|
|
3105
3469
|
if (!directory) continue;
|
|
3106
|
-
for (const candidate of candidatesFor(
|
|
3470
|
+
for (const candidate of candidatesFor(path4.win32.join(directory, command))) {
|
|
3107
3471
|
if (isFile(candidate)) return candidate;
|
|
3108
3472
|
}
|
|
3109
3473
|
}
|
|
@@ -3132,7 +3496,7 @@ function quoteBatchArgument(arg) {
|
|
|
3132
3496
|
}
|
|
3133
3497
|
function planCmdInvocation(file, args, env = process.env) {
|
|
3134
3498
|
const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
|
|
3135
|
-
const line = [`"${
|
|
3499
|
+
const line = [`"${path4.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
|
|
3136
3500
|
return {
|
|
3137
3501
|
file: comspec,
|
|
3138
3502
|
args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
|
|
@@ -3376,145 +3740,6 @@ import * as fs3 from "fs";
|
|
|
3376
3740
|
import * as path5 from "path";
|
|
3377
3741
|
import * as crypto from "crypto";
|
|
3378
3742
|
import { EventEmitter } from "events";
|
|
3379
|
-
|
|
3380
|
-
// src/tmp.ts
|
|
3381
|
-
import * as fs2 from "fs";
|
|
3382
|
-
import * as os from "os";
|
|
3383
|
-
import * as path4 from "path";
|
|
3384
|
-
var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
|
|
3385
|
-
var PID_TMP_DIR = path4.join(
|
|
3386
|
-
os.tmpdir(),
|
|
3387
|
-
`${PLUGIN_TMP_PREFIX}${process.pid}`
|
|
3388
|
-
);
|
|
3389
|
-
var currentDir = null;
|
|
3390
|
-
var createdDirs = /* @__PURE__ */ new Set();
|
|
3391
|
-
var registered = false;
|
|
3392
|
-
function currentUid() {
|
|
3393
|
-
return typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
3394
|
-
}
|
|
3395
|
-
function untrustworthyReason(dir) {
|
|
3396
|
-
let stat;
|
|
3397
|
-
try {
|
|
3398
|
-
stat = fs2.lstatSync(dir);
|
|
3399
|
-
} catch {
|
|
3400
|
-
return null;
|
|
3401
|
-
}
|
|
3402
|
-
if (stat.isSymbolicLink()) return "path is a symlink";
|
|
3403
|
-
if (!stat.isDirectory()) return "path is not a directory";
|
|
3404
|
-
const uid = currentUid();
|
|
3405
|
-
if (uid !== void 0 && stat.uid !== uid) {
|
|
3406
|
-
return `directory is owned by uid ${stat.uid}, not ${uid}`;
|
|
3407
|
-
}
|
|
3408
|
-
return null;
|
|
3409
|
-
}
|
|
3410
|
-
function ensureDir(dir) {
|
|
3411
|
-
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3412
|
-
try {
|
|
3413
|
-
fs2.chmodSync(dir, 448);
|
|
3414
|
-
} catch {
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
function chooseDir() {
|
|
3418
|
-
const reason = untrustworthyReason(PID_TMP_DIR);
|
|
3419
|
-
if (!reason) return PID_TMP_DIR;
|
|
3420
|
-
const fallback = fs2.mkdtempSync(path4.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
|
|
3421
|
-
log.warn("refusing the pid-named scratch directory", {
|
|
3422
|
-
path: PID_TMP_DIR,
|
|
3423
|
-
reason,
|
|
3424
|
-
fallback
|
|
3425
|
-
});
|
|
3426
|
-
return fallback;
|
|
3427
|
-
}
|
|
3428
|
-
function registerExitCleanup() {
|
|
3429
|
-
if (registered) return;
|
|
3430
|
-
registered = true;
|
|
3431
|
-
process.on("exit", () => {
|
|
3432
|
-
for (const dir of createdDirs) {
|
|
3433
|
-
try {
|
|
3434
|
-
fs2.rmSync(dir, { recursive: true, force: true });
|
|
3435
|
-
} catch {
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
});
|
|
3439
|
-
}
|
|
3440
|
-
var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
|
|
3441
|
-
function isProcessAlive(pid) {
|
|
3442
|
-
try {
|
|
3443
|
-
process.kill(pid, 0);
|
|
3444
|
-
return true;
|
|
3445
|
-
} catch (err) {
|
|
3446
|
-
return err.code === "EPERM";
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
function sweepStalePluginTmpDirs(keep) {
|
|
3450
|
-
const removed = [];
|
|
3451
|
-
if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
|
|
3452
|
-
const root = os.tmpdir();
|
|
3453
|
-
let entries;
|
|
3454
|
-
try {
|
|
3455
|
-
entries = fs2.readdirSync(root);
|
|
3456
|
-
} catch {
|
|
3457
|
-
return removed;
|
|
3458
|
-
}
|
|
3459
|
-
const uid = currentUid();
|
|
3460
|
-
const keepPath = keep ? path4.resolve(keep) : null;
|
|
3461
|
-
for (const entry of entries) {
|
|
3462
|
-
const match = PID_DIR_NAME.exec(entry);
|
|
3463
|
-
if (!match) continue;
|
|
3464
|
-
const pid = Number(match[1]);
|
|
3465
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) continue;
|
|
3466
|
-
if (pid === process.pid) continue;
|
|
3467
|
-
const dir = path4.join(root, entry);
|
|
3468
|
-
if (keepPath && path4.resolve(dir) === keepPath) continue;
|
|
3469
|
-
let stat;
|
|
3470
|
-
try {
|
|
3471
|
-
stat = fs2.lstatSync(dir);
|
|
3472
|
-
} catch {
|
|
3473
|
-
continue;
|
|
3474
|
-
}
|
|
3475
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
|
|
3476
|
-
if (uid !== void 0 && stat.uid !== uid) continue;
|
|
3477
|
-
if (isProcessAlive(pid)) continue;
|
|
3478
|
-
try {
|
|
3479
|
-
fs2.rmSync(dir, { recursive: true, force: true });
|
|
3480
|
-
removed.push(dir);
|
|
3481
|
-
} catch (err) {
|
|
3482
|
-
log.debug("could not remove a stale scratch directory", {
|
|
3483
|
-
dir,
|
|
3484
|
-
error: String(err)
|
|
3485
|
-
});
|
|
3486
|
-
}
|
|
3487
|
-
}
|
|
3488
|
-
if (removed.length > 0) {
|
|
3489
|
-
log.info("removed stale plugin scratch directories", {
|
|
3490
|
-
count: removed.length,
|
|
3491
|
-
dirs: removed
|
|
3492
|
-
});
|
|
3493
|
-
}
|
|
3494
|
-
return removed;
|
|
3495
|
-
}
|
|
3496
|
-
var swept = false;
|
|
3497
|
-
function sweepOnce(keep) {
|
|
3498
|
-
if (swept) return;
|
|
3499
|
-
swept = true;
|
|
3500
|
-
try {
|
|
3501
|
-
sweepStalePluginTmpDirs(keep);
|
|
3502
|
-
} catch (err) {
|
|
3503
|
-
log.warn("stale scratch directory sweep failed", { error: String(err) });
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
function pluginTmpDir() {
|
|
3507
|
-
if (currentDir === null || untrustworthyReason(currentDir) !== null) {
|
|
3508
|
-
currentDir = chooseDir();
|
|
3509
|
-
}
|
|
3510
|
-
ensureDir(currentDir);
|
|
3511
|
-
createdDirs.add(currentDir);
|
|
3512
|
-
registerExitCleanup();
|
|
3513
|
-
sweepOnce(currentDir);
|
|
3514
|
-
return currentDir;
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
// src/proxy-mcp.ts
|
|
3518
3743
|
var PROXY_KEEPALIVE_MS = 15e3;
|
|
3519
3744
|
function acceptsEventStream(acceptHeader) {
|
|
3520
3745
|
return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
|
|
@@ -6023,17 +6248,17 @@ function tailIsReplyOnly(history, from) {
|
|
|
6023
6248
|
}
|
|
6024
6249
|
|
|
6025
6250
|
// src/session-resume-store.ts
|
|
6026
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
6027
|
-
import { homedir as
|
|
6028
|
-
import { join as
|
|
6251
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6252
|
+
import { homedir as homedir6 } from "os";
|
|
6253
|
+
import { join as join8 } from "path";
|
|
6029
6254
|
var MAX_RESUME_RECORDS = 256;
|
|
6030
6255
|
var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6031
6256
|
var records = null;
|
|
6032
|
-
var
|
|
6257
|
+
var pathOverride2 = null;
|
|
6033
6258
|
function resumeStorePath() {
|
|
6034
|
-
if (
|
|
6035
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
6036
|
-
return
|
|
6259
|
+
if (pathOverride2) return pathOverride2;
|
|
6260
|
+
const stateRoot = process.env.XDG_STATE_HOME || join8(homedir6(), ".local", "state");
|
|
6261
|
+
return join8(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
|
|
6037
6262
|
}
|
|
6038
6263
|
function isDigest(value) {
|
|
6039
6264
|
const entry = value;
|
|
@@ -6043,11 +6268,11 @@ function isRecord2(value) {
|
|
|
6043
6268
|
const record = value;
|
|
6044
6269
|
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");
|
|
6045
6270
|
}
|
|
6046
|
-
function
|
|
6271
|
+
function load2() {
|
|
6047
6272
|
if (records) return records;
|
|
6048
6273
|
records = /* @__PURE__ */ new Map();
|
|
6049
6274
|
try {
|
|
6050
|
-
const parsed = JSON.parse(
|
|
6275
|
+
const parsed = JSON.parse(readFileSync4(resumeStorePath(), "utf8"));
|
|
6051
6276
|
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
6052
6277
|
if (entries && typeof entries === "object") {
|
|
6053
6278
|
const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
|
|
@@ -6058,11 +6283,11 @@ function load() {
|
|
|
6058
6283
|
}
|
|
6059
6284
|
return records;
|
|
6060
6285
|
}
|
|
6061
|
-
function
|
|
6286
|
+
function save2(update) {
|
|
6062
6287
|
const file = resumeStorePath();
|
|
6063
6288
|
const merged = /* @__PURE__ */ new Map();
|
|
6064
6289
|
try {
|
|
6065
|
-
const onDisk = JSON.parse(
|
|
6290
|
+
const onDisk = JSON.parse(readFileSync4(file, "utf8"))?.sessions;
|
|
6066
6291
|
if (onDisk && typeof onDisk === "object") {
|
|
6067
6292
|
for (const [key, record] of Object.entries(onDisk)) {
|
|
6068
6293
|
if (isRecord2(record)) merged.set(key, record);
|
|
@@ -6075,12 +6300,12 @@ function save(update) {
|
|
|
6075
6300
|
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
|
|
6076
6301
|
records = new Map(kept);
|
|
6077
6302
|
try {
|
|
6078
|
-
|
|
6303
|
+
mkdirSync4(join8(file, ".."), { recursive: true, mode: 448 });
|
|
6079
6304
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
6080
|
-
|
|
6305
|
+
writeFileSync4(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
6081
6306
|
mode: 384
|
|
6082
6307
|
});
|
|
6083
|
-
|
|
6308
|
+
renameSync3(tmp, file);
|
|
6084
6309
|
} catch (err) {
|
|
6085
6310
|
log.debug("could not write the claude session resume store", {
|
|
6086
6311
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -6093,11 +6318,11 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
|
|
|
6093
6318
|
const shape2 = conversationDigests(prompt, { assistantContent: false });
|
|
6094
6319
|
const configDir = where?.configDir;
|
|
6095
6320
|
const account = where?.account;
|
|
6096
|
-
const current =
|
|
6321
|
+
const current = load2().get(sessionKey2);
|
|
6097
6322
|
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)) {
|
|
6098
6323
|
return;
|
|
6099
6324
|
}
|
|
6100
|
-
|
|
6325
|
+
save2((merged) => {
|
|
6101
6326
|
merged.delete(sessionKey2);
|
|
6102
6327
|
merged.set(sessionKey2, {
|
|
6103
6328
|
claudeSessionId,
|
|
@@ -6111,22 +6336,22 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
|
|
|
6111
6336
|
});
|
|
6112
6337
|
}
|
|
6113
6338
|
function forgetResumePoint(sessionKey2) {
|
|
6114
|
-
if (!
|
|
6115
|
-
|
|
6339
|
+
if (!load2().has(sessionKey2)) return;
|
|
6340
|
+
save2((merged) => {
|
|
6116
6341
|
merged.delete(sessionKey2);
|
|
6117
6342
|
});
|
|
6118
6343
|
}
|
|
6119
6344
|
function moveResumePoint(fromKey, toKey) {
|
|
6120
|
-
const record =
|
|
6345
|
+
const record = load2().get(fromKey);
|
|
6121
6346
|
if (!record) return;
|
|
6122
|
-
|
|
6347
|
+
save2((merged) => {
|
|
6123
6348
|
merged.delete(fromKey);
|
|
6124
6349
|
if (merged.has(toKey)) return;
|
|
6125
6350
|
merged.set(toKey, { ...record, updatedAt: Date.now() });
|
|
6126
6351
|
});
|
|
6127
6352
|
}
|
|
6128
6353
|
function findResumePoint(opts) {
|
|
6129
|
-
const record =
|
|
6354
|
+
const record = load2().get(opts.sessionKey);
|
|
6130
6355
|
if (!record) {
|
|
6131
6356
|
opts.onRefused?.("no-session-recorded");
|
|
6132
6357
|
return void 0;
|
|
@@ -6159,7 +6384,7 @@ function findSiblingResumePoint(opts) {
|
|
|
6159
6384
|
let best;
|
|
6160
6385
|
let bestUpdatedAt = -1;
|
|
6161
6386
|
let refusal;
|
|
6162
|
-
for (const [key, record] of
|
|
6387
|
+
for (const [key, record] of load2()) {
|
|
6163
6388
|
if (key === opts.sessionKey) continue;
|
|
6164
6389
|
if (modelSiblingSignature(key, providers) !== signature) continue;
|
|
6165
6390
|
const siblingAccount = providers ? sessionKeyAccount(key, providers) : void 0;
|
|
@@ -6228,7 +6453,7 @@ function findForeignAccountSibling(opts) {
|
|
|
6228
6453
|
}
|
|
6229
6454
|
let found;
|
|
6230
6455
|
let foundUpdatedAt = -1;
|
|
6231
|
-
for (const [key, record] of
|
|
6456
|
+
for (const [key, record] of load2()) {
|
|
6232
6457
|
const hit = consider(key);
|
|
6233
6458
|
if (hit && record.updatedAt >= foundUpdatedAt) {
|
|
6234
6459
|
found = hit;
|
|
@@ -6848,6 +7073,7 @@ function deleteActiveProcessesForSession(sessionID) {
|
|
|
6848
7073
|
}
|
|
6849
7074
|
for (const key of released) clearBackgroundTasks(key);
|
|
6850
7075
|
forgetSessionChoice(sessionID);
|
|
7076
|
+
forgetDispatchChoicesForSession(sessionID);
|
|
6851
7077
|
return released;
|
|
6852
7078
|
}
|
|
6853
7079
|
function killAllActiveProcesses() {
|
|
@@ -7583,7 +7809,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
7583
7809
|
// src/diagnostic-bundle.ts
|
|
7584
7810
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
7585
7811
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
7586
|
-
import { homedir as
|
|
7812
|
+
import { homedir as homedir7 } from "os";
|
|
7587
7813
|
|
|
7588
7814
|
// src/log-messages.ts
|
|
7589
7815
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -7593,6 +7819,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
7593
7819
|
"Code Mode execute not forwarded: read-only permission preset",
|
|
7594
7820
|
"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.",
|
|
7595
7821
|
"MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
|
|
7822
|
+
"a remembered subagent dispatch account is no longer available",
|
|
7596
7823
|
"account failover declined; ending the turn",
|
|
7597
7824
|
"accountGroups must be an object mapping an account name to a group name; ignoring it",
|
|
7598
7825
|
"accountGroups names an account that is not configured; ignoring it",
|
|
@@ -7974,7 +8201,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
7974
8201
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
7975
8202
|
}
|
|
7976
8203
|
function createRedactionContext(options = {}) {
|
|
7977
|
-
const home = options.home ??
|
|
8204
|
+
const home = options.home ?? homedir7();
|
|
7978
8205
|
const homes = /* @__PURE__ */ new Set();
|
|
7979
8206
|
if (home) homes.add(home);
|
|
7980
8207
|
if (home) {
|
|
@@ -10116,9 +10343,9 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
10116
10343
|
// src/claude-session-wrapper.ts
|
|
10117
10344
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
10118
10345
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
10119
|
-
import { existsSync as existsSync4, writeFileSync as
|
|
10346
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
10120
10347
|
import { unlink as unlink2 } from "fs/promises";
|
|
10121
|
-
import { join as
|
|
10348
|
+
import { join as join13 } from "path";
|
|
10122
10349
|
|
|
10123
10350
|
// src/claude-session-bun.ts
|
|
10124
10351
|
import * as os5 from "os";
|
|
@@ -11299,8 +11526,8 @@ ${text2}`
|
|
|
11299
11526
|
}
|
|
11300
11527
|
function stageImage(data, extension) {
|
|
11301
11528
|
try {
|
|
11302
|
-
const file =
|
|
11303
|
-
|
|
11529
|
+
const file = join13(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
|
|
11530
|
+
writeFileSync6(file, data, { mode: 384, flag: "wx" });
|
|
11304
11531
|
return file;
|
|
11305
11532
|
} catch (err) {
|
|
11306
11533
|
log.warn("interactive transport could not stage an image", {
|
|
@@ -11841,13 +12068,13 @@ function dedupe2(dirs) {
|
|
|
11841
12068
|
}
|
|
11842
12069
|
|
|
11843
12070
|
// src/prompts.ts
|
|
11844
|
-
import { readFileSync as
|
|
11845
|
-
import { homedir as
|
|
12071
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
12072
|
+
import { homedir as homedir10 } from "os";
|
|
11846
12073
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
11847
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
12074
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join14 } from "path";
|
|
11848
12075
|
function readPromptFileIfPresent(path15) {
|
|
11849
12076
|
try {
|
|
11850
|
-
const content =
|
|
12077
|
+
const content = readFileSync8(path15, "utf8").trim();
|
|
11851
12078
|
return content || void 0;
|
|
11852
12079
|
} catch {
|
|
11853
12080
|
return void 0;
|
|
@@ -11856,7 +12083,7 @@ function readPromptFileIfPresent(path15) {
|
|
|
11856
12083
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
11857
12084
|
let dir = cwd;
|
|
11858
12085
|
while (true) {
|
|
11859
|
-
const content = readPromptFileIfPresent(
|
|
12086
|
+
const content = readPromptFileIfPresent(join14(dir, "AGENTS.md"));
|
|
11860
12087
|
if (content) return content;
|
|
11861
12088
|
const parent = dirname6(dir);
|
|
11862
12089
|
if (parent === dir) return void 0;
|
|
@@ -11952,9 +12179,9 @@ function extractSystemMessages(prompt) {
|
|
|
11952
12179
|
}
|
|
11953
12180
|
var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
|
|
11954
12181
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
11955
|
-
const
|
|
12182
|
+
const readFile3 = opts.readFile ?? ((path15) => {
|
|
11956
12183
|
try {
|
|
11957
|
-
return
|
|
12184
|
+
return readFileSync8(path15, "utf8");
|
|
11958
12185
|
} catch {
|
|
11959
12186
|
return null;
|
|
11960
12187
|
}
|
|
@@ -11970,7 +12197,7 @@ function userAuthoredInstructions(forwarded, opts = {}) {
|
|
|
11970
12197
|
if (seen.has(path15) || !isAbsolute3(path15)) continue;
|
|
11971
12198
|
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path15))) continue;
|
|
11972
12199
|
seen.add(path15);
|
|
11973
|
-
const file =
|
|
12200
|
+
const file = readFile3(path15);
|
|
11974
12201
|
if (!file?.trim()) continue;
|
|
11975
12202
|
const start = match.index + match[0].length + 1;
|
|
11976
12203
|
const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
|
|
@@ -11995,8 +12222,8 @@ ${options.compressionSummary.trim()}`
|
|
|
11995
12222
|
for (const s of extraSystemContent) {
|
|
11996
12223
|
if (s.trim()) parts.push(s.trim());
|
|
11997
12224
|
}
|
|
11998
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
11999
|
-
const globalAgents = readPromptFileIfPresent(
|
|
12225
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
|
|
12226
|
+
const globalAgents = readPromptFileIfPresent(join14(configRoot, "opencode", "AGENTS.md"));
|
|
12000
12227
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
12001
12228
|
const forwarded = extraSystemContent.join("\n\n");
|
|
12002
12229
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -12012,9 +12239,9 @@ ${options.compressionSummary.trim()}`
|
|
|
12012
12239
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
12013
12240
|
const content = parts.join("\n\n");
|
|
12014
12241
|
if (!content) return void 0;
|
|
12015
|
-
const path15 =
|
|
12242
|
+
const path15 = join14(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
|
|
12016
12243
|
try {
|
|
12017
|
-
|
|
12244
|
+
writeFileSync7(path15, content, { encoding: "utf8", mode: 384 });
|
|
12018
12245
|
return path15;
|
|
12019
12246
|
} catch (err) {
|
|
12020
12247
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -15346,7 +15573,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15346
15573
|
let replayReason = "no-session-for-key";
|
|
15347
15574
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
15348
15575
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
15349
|
-
const transcriptPath2 = (id, dir) =>
|
|
15576
|
+
const transcriptPath2 = (id, dir) => join15(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
15350
15577
|
const resumePoint = findResumePoint({
|
|
15351
15578
|
sessionKey: sk,
|
|
15352
15579
|
prompt: options.prompt,
|
|
@@ -16693,7 +16920,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16693
16920
|
};
|
|
16694
16921
|
|
|
16695
16922
|
// src/account-tier.ts
|
|
16696
|
-
import { readFileSync as
|
|
16923
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
16697
16924
|
import path13 from "path";
|
|
16698
16925
|
var ORGANIZATION_TYPE_LABELS = {
|
|
16699
16926
|
claude_max: "Max",
|
|
@@ -16736,7 +16963,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
|
|
|
16736
16963
|
function readOauthAccount(file) {
|
|
16737
16964
|
let raw;
|
|
16738
16965
|
try {
|
|
16739
|
-
raw =
|
|
16966
|
+
raw = readFileSync9(file, "utf8");
|
|
16740
16967
|
} catch {
|
|
16741
16968
|
return void 0;
|
|
16742
16969
|
}
|
|
@@ -16764,14 +16991,14 @@ function tierLabel(oauthAccount) {
|
|
|
16764
16991
|
// src/cleanup-stale.ts
|
|
16765
16992
|
import {
|
|
16766
16993
|
existsSync as existsSync5,
|
|
16767
|
-
mkdirSync as
|
|
16768
|
-
readFileSync as
|
|
16994
|
+
mkdirSync as mkdirSync6,
|
|
16995
|
+
readFileSync as readFileSync10,
|
|
16769
16996
|
realpathSync as realpathSync4,
|
|
16770
16997
|
rmSync as rmSync3,
|
|
16771
|
-
writeFileSync as
|
|
16998
|
+
writeFileSync as writeFileSync8
|
|
16772
16999
|
} from "fs";
|
|
16773
|
-
import { homedir as
|
|
16774
|
-
import { dirname as dirname7, join as
|
|
17000
|
+
import { homedir as homedir11 } from "os";
|
|
17001
|
+
import { dirname as dirname7, join as join16, resolve as resolve5 } from "path";
|
|
16775
17002
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
16776
17003
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
16777
17004
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -16779,20 +17006,20 @@ var alreadyRan = false;
|
|
|
16779
17006
|
function candidateCacheRoots() {
|
|
16780
17007
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
16781
17008
|
return [
|
|
16782
|
-
xdg ?
|
|
16783
|
-
|
|
16784
|
-
|
|
17009
|
+
xdg ? join16(xdg, "opencode") : null,
|
|
17010
|
+
join16(homedir11(), ".cache", "opencode"),
|
|
17011
|
+
join16(homedir11(), "Library", "Caches", "opencode")
|
|
16785
17012
|
].filter((p) => Boolean(p));
|
|
16786
17013
|
}
|
|
16787
17014
|
function userOpencodeJsonPath() {
|
|
16788
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
16789
|
-
return
|
|
17015
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join16(homedir11(), ".config");
|
|
17016
|
+
return join16(xdgConfig, "opencode", "opencode.json");
|
|
16790
17017
|
}
|
|
16791
17018
|
function userIntendsToUseUnscoped() {
|
|
16792
17019
|
const cfg = userOpencodeJsonPath();
|
|
16793
17020
|
if (!existsSync5(cfg)) return false;
|
|
16794
17021
|
try {
|
|
16795
|
-
const json = JSON.parse(
|
|
17022
|
+
const json = JSON.parse(readFileSync10(cfg, "utf8"));
|
|
16796
17023
|
const plugins = json.plugin;
|
|
16797
17024
|
if (!Array.isArray(plugins)) return false;
|
|
16798
17025
|
return plugins.some(
|
|
@@ -16813,19 +17040,19 @@ function ourLoadedDir() {
|
|
|
16813
17040
|
function ourVersion(dir) {
|
|
16814
17041
|
if (!dir) return null;
|
|
16815
17042
|
try {
|
|
16816
|
-
const pkg = JSON.parse(
|
|
17043
|
+
const pkg = JSON.parse(readFileSync10(join16(dir, "package.json"), "utf8"));
|
|
16817
17044
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
16818
17045
|
} catch {
|
|
16819
17046
|
return null;
|
|
16820
17047
|
}
|
|
16821
17048
|
}
|
|
16822
17049
|
function markerPath() {
|
|
16823
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
16824
|
-
return
|
|
17050
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join16(homedir11(), ".local", "state");
|
|
17051
|
+
return join16(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
16825
17052
|
}
|
|
16826
17053
|
function markerVersion() {
|
|
16827
17054
|
try {
|
|
16828
|
-
const marker = JSON.parse(
|
|
17055
|
+
const marker = JSON.parse(readFileSync10(markerPath(), "utf8"));
|
|
16829
17056
|
return typeof marker.version === "string" ? marker.version : null;
|
|
16830
17057
|
} catch {
|
|
16831
17058
|
return null;
|
|
@@ -16834,8 +17061,8 @@ function markerVersion() {
|
|
|
16834
17061
|
function writeMarker(version) {
|
|
16835
17062
|
const file = markerPath();
|
|
16836
17063
|
try {
|
|
16837
|
-
|
|
16838
|
-
|
|
17064
|
+
mkdirSync6(dirname7(file), { recursive: true });
|
|
17065
|
+
writeFileSync8(
|
|
16839
17066
|
file,
|
|
16840
17067
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
16841
17068
|
"utf8"
|
|
@@ -16870,7 +17097,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
16870
17097
|
}
|
|
16871
17098
|
function cleanupOne(cacheRoot, ourDir) {
|
|
16872
17099
|
if (!existsSync5(cacheRoot)) return;
|
|
16873
|
-
const stalePath =
|
|
17100
|
+
const stalePath = join16(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
16874
17101
|
if (!existsSync5(stalePath)) return;
|
|
16875
17102
|
let realStalePath = stalePath;
|
|
16876
17103
|
try {
|
|
@@ -16878,11 +17105,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
16878
17105
|
} catch {
|
|
16879
17106
|
}
|
|
16880
17107
|
if (ourDir && realStalePath === ourDir) return;
|
|
16881
|
-
const pkgJsonPath =
|
|
17108
|
+
const pkgJsonPath = join16(stalePath, "package.json");
|
|
16882
17109
|
if (!existsSync5(pkgJsonPath)) return;
|
|
16883
17110
|
let pkg = {};
|
|
16884
17111
|
try {
|
|
16885
|
-
pkg = JSON.parse(
|
|
17112
|
+
pkg = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
|
|
16886
17113
|
} catch {
|
|
16887
17114
|
return;
|
|
16888
17115
|
}
|
|
@@ -16898,13 +17125,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
16898
17125
|
});
|
|
16899
17126
|
return;
|
|
16900
17127
|
}
|
|
16901
|
-
const cachePkgJson =
|
|
17128
|
+
const cachePkgJson = join16(cacheRoot, "package.json");
|
|
16902
17129
|
if (!existsSync5(cachePkgJson)) return;
|
|
16903
17130
|
try {
|
|
16904
|
-
const cfg = JSON.parse(
|
|
17131
|
+
const cfg = JSON.parse(readFileSync10(cachePkgJson, "utf8"));
|
|
16905
17132
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
16906
17133
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
16907
|
-
|
|
17134
|
+
writeFileSync8(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
16908
17135
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
16909
17136
|
}
|
|
16910
17137
|
} catch (err) {
|