@khalilgharbaoui/opencode-claude-code-plugin 0.52.0 → 0.53.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.js +502 -288
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +20 -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,24 +2737,37 @@ 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;
|
|
@@ -2636,7 +2899,11 @@ function comboLabel(model, effort) {
|
|
|
2636
2899
|
return effort ? `${model} / ${effort}` : model;
|
|
2637
2900
|
}
|
|
2638
2901
|
function describeChoice(choice, fallback) {
|
|
2639
|
-
|
|
2902
|
+
const combo = comboLabel(choice.model ?? fallback.model, choice.effort ?? fallback.effort);
|
|
2903
|
+
return choice.account ? `${combo} @${choice.account}` : combo;
|
|
2904
|
+
}
|
|
2905
|
+
function droppedAccountPhrase(account) {
|
|
2906
|
+
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
2907
|
}
|
|
2641
2908
|
function availableCombos() {
|
|
2642
2909
|
return DISPATCH_COMBOS.filter((combo) => Object.hasOwn(defaultModels, combo.model));
|
|
@@ -2654,16 +2921,25 @@ function dispatchAccountCandidates(context) {
|
|
|
2654
2921
|
}
|
|
2655
2922
|
return out;
|
|
2656
2923
|
}
|
|
2657
|
-
function accountQuestion(context) {
|
|
2924
|
+
function accountQuestion(context, scope) {
|
|
2658
2925
|
const candidates = dispatchAccountCandidates(context);
|
|
2659
2926
|
if (candidates.length < 2) return null;
|
|
2660
2927
|
const source = candidates[0];
|
|
2928
|
+
const perType = "agent" in scope;
|
|
2929
|
+
const subject = perType ? `the "${scope.agent}" subagents` : "these subagents";
|
|
2930
|
+
const options = candidates.map((account) => ({
|
|
2931
|
+
label: account,
|
|
2932
|
+
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.`
|
|
2933
|
+
}));
|
|
2934
|
+
if (!perType && scope.types > 1) {
|
|
2935
|
+
options.push({
|
|
2936
|
+
label: PER_TYPE_ANSWER,
|
|
2937
|
+
description: `Choose the account separately for each of these ${scope.types} agent types on the next screen.`
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2661
2940
|
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
|
-
}))
|
|
2941
|
+
question: perType ? `Which Claude account should the "${scope.agent}" subagents run on?` : "Which Claude account should these subagents run on?",
|
|
2942
|
+
options
|
|
2667
2943
|
};
|
|
2668
2944
|
}
|
|
2669
2945
|
function summaryQuestion(tasks, context) {
|
|
@@ -2674,13 +2950,14 @@ function summaryQuestion(tasks, context) {
|
|
|
2674
2950
|
});
|
|
2675
2951
|
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
2952
|
const options = [];
|
|
2677
|
-
const remembered = types.map((agent) =>
|
|
2678
|
-
if (remembered.every((
|
|
2953
|
+
const remembered = types.map((agent) => usableRemembered(context, agent));
|
|
2954
|
+
if (remembered.every((entry) => entry !== void 0)) {
|
|
2955
|
+
const dropped = remembered.map((entry) => entry.droppedAccount).find((account) => !!account);
|
|
2679
2956
|
options.push({
|
|
2680
2957
|
label: LAST_ANSWER_PREFIX,
|
|
2681
2958
|
description: types.map(
|
|
2682
|
-
(agent, index) => `${agent}: ${describeChoice(remembered[index], context.describeDefault(agent))}`
|
|
2683
|
-
).join(". ")
|
|
2959
|
+
(agent, index) => `${agent}: ${describeChoice(remembered[index].choice, context.describeDefault(agent))}`
|
|
2960
|
+
).join(". ") + (dropped ? `.${droppedAccountPhrase(dropped)}` : "")
|
|
2684
2961
|
});
|
|
2685
2962
|
}
|
|
2686
2963
|
options.push({
|
|
@@ -2698,20 +2975,20 @@ function summaryQuestion(tasks, context) {
|
|
|
2698
2975
|
}
|
|
2699
2976
|
options.push({
|
|
2700
2977
|
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."
|
|
2978
|
+
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
2979
|
});
|
|
2703
2980
|
return { question, options };
|
|
2704
2981
|
}
|
|
2705
2982
|
function typeQuestion(agent, tasks, context) {
|
|
2706
2983
|
const mine = tasks.filter((task) => task.agent === agent);
|
|
2707
2984
|
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
|
|
2985
|
+
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
2986
|
const options = [];
|
|
2710
|
-
const remembered =
|
|
2987
|
+
const remembered = usableRemembered(context, agent);
|
|
2711
2988
|
if (remembered) {
|
|
2712
2989
|
options.push({
|
|
2713
2990
|
label: LAST_ANSWER_PREFIX,
|
|
2714
|
-
description: `${describeChoice(remembered, fallback)}, the last thing you picked for "${agent}" in this conversation.`
|
|
2991
|
+
description: `${describeChoice(remembered.choice, fallback)}, the last thing you picked for "${agent}" in this conversation.` + (remembered.droppedAccount ? droppedAccountPhrase(remembered.droppedAccount) : "")
|
|
2715
2992
|
});
|
|
2716
2993
|
}
|
|
2717
2994
|
options.push({
|
|
@@ -2735,7 +3012,7 @@ function typeQuestion(agent, tasks, context) {
|
|
|
2735
3012
|
function taskQuestion(task, typeChoice, context) {
|
|
2736
3013
|
const fallback = context.describeDefault(task.agent);
|
|
2737
3014
|
const options = [];
|
|
2738
|
-
if (typeChoice && (typeChoice.model || typeChoice.effort)) {
|
|
3015
|
+
if (typeChoice && (typeChoice.model || typeChoice.effort || typeChoice.account)) {
|
|
2739
3016
|
options.push({
|
|
2740
3017
|
label: `Same as the rest (${describeChoice(typeChoice, fallback)})`,
|
|
2741
3018
|
description: `What you picked for every "${task.agent}" subagent in this dispatch.`
|
|
@@ -2749,10 +3026,15 @@ function taskQuestion(task, typeChoice, context) {
|
|
|
2749
3026
|
options.push({ label: comboLabel(combo.model, combo.effort), description: combo.description });
|
|
2750
3027
|
}
|
|
2751
3028
|
return {
|
|
2752
|
-
question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}`,
|
|
3029
|
+
question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}${accountHint(context)}`,
|
|
2753
3030
|
options
|
|
2754
3031
|
};
|
|
2755
3032
|
}
|
|
3033
|
+
function accountHint(context) {
|
|
3034
|
+
const candidates = dispatchAccountCandidates(context);
|
|
3035
|
+
if (candidates.length < 2) return "";
|
|
3036
|
+
return ` Add "@${candidates[1]}" to a typed answer to send it to that account.`;
|
|
3037
|
+
}
|
|
2756
3038
|
function header(text) {
|
|
2757
3039
|
return text.length <= 30 ? text : `${text.slice(0, 29)}\u2026`;
|
|
2758
3040
|
}
|
|
@@ -2787,17 +3069,26 @@ function createSubagentDispatchQuestion(sessionKey2, heldCallIds, tasks, context
|
|
|
2787
3069
|
fields: [{ kind: "all" }],
|
|
2788
3070
|
choices: /* @__PURE__ */ new Map(),
|
|
2789
3071
|
account: void 0,
|
|
3072
|
+
typeAccounts: /* @__PURE__ */ new Map(),
|
|
3073
|
+
typeChoices: /* @__PURE__ */ new Map(),
|
|
3074
|
+
droppedAccounts: [],
|
|
2790
3075
|
context
|
|
2791
3076
|
};
|
|
2792
3077
|
const summary = summaryQuestion(tasks, context);
|
|
2793
3078
|
return buildCall(entry, [{ header: "Subagents", ...summary }], toolCallId);
|
|
2794
3079
|
}
|
|
2795
|
-
function parseCustomChoice(answer) {
|
|
3080
|
+
function parseCustomChoice(answer, candidates = []) {
|
|
2796
3081
|
const words2 = answer.trim().split(/[\s,/]+/).map((word) => word.trim()).filter(Boolean);
|
|
2797
3082
|
if (words2.length === 0) return null;
|
|
2798
3083
|
const choice = {};
|
|
2799
3084
|
for (const word of words2) {
|
|
2800
3085
|
const lower = word.toLowerCase();
|
|
3086
|
+
if (lower.startsWith("@")) {
|
|
3087
|
+
const account = normalizeAccountName(lower.slice(1));
|
|
3088
|
+
if (!account || !candidates.includes(account)) return null;
|
|
3089
|
+
choice.account = account;
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
2801
3092
|
if (REASONING_EFFORTS.includes(lower)) {
|
|
2802
3093
|
choice.effort = lower;
|
|
2803
3094
|
continue;
|
|
@@ -2808,7 +3099,7 @@ function parseCustomChoice(answer) {
|
|
|
2808
3099
|
}
|
|
2809
3100
|
return null;
|
|
2810
3101
|
}
|
|
2811
|
-
return choice.model || choice.effort ? choice : null;
|
|
3102
|
+
return choice.model || choice.effort || choice.account ? choice : null;
|
|
2812
3103
|
}
|
|
2813
3104
|
function parseComboLabel(answer) {
|
|
2814
3105
|
for (const combo of availableCombos()) {
|
|
@@ -2818,7 +3109,7 @@ function parseComboLabel(answer) {
|
|
|
2818
3109
|
}
|
|
2819
3110
|
return null;
|
|
2820
3111
|
}
|
|
2821
|
-
function classifyAnswer(answer) {
|
|
3112
|
+
function classifyAnswer(answer, candidates = []) {
|
|
2822
3113
|
const trimmed = answer.trim();
|
|
2823
3114
|
if (!trimmed) return { kind: "default" };
|
|
2824
3115
|
if (trimmed === DEFAULT_ANSWER) return { kind: "default" };
|
|
@@ -2828,7 +3119,7 @@ function classifyAnswer(answer) {
|
|
|
2828
3119
|
if (trimmed.startsWith("Same as the rest")) return { kind: "sameAsRest" };
|
|
2829
3120
|
const combo = parseComboLabel(trimmed);
|
|
2830
3121
|
if (combo) return { kind: "choice", choice: combo };
|
|
2831
|
-
const custom = parseCustomChoice(trimmed);
|
|
3122
|
+
const custom = parseCustomChoice(trimmed, candidates);
|
|
2832
3123
|
if (custom) return { kind: "choice", choice: custom };
|
|
2833
3124
|
return { kind: "unknown", answer: trimmed };
|
|
2834
3125
|
}
|
|
@@ -2869,19 +3160,28 @@ function advance(entry, part) {
|
|
|
2869
3160
|
case "types":
|
|
2870
3161
|
return advanceTypes(entry, answers);
|
|
2871
3162
|
default:
|
|
2872
|
-
return
|
|
3163
|
+
return advanceDetail(entry, answers);
|
|
2873
3164
|
}
|
|
2874
3165
|
}
|
|
2875
3166
|
function applyToAll(entry, choice) {
|
|
2876
3167
|
for (const index of entry.tasks.keys()) entry.choices.set(index, { ...choice });
|
|
2877
3168
|
}
|
|
3169
|
+
function noteDroppedAccount(entry, agent, account) {
|
|
3170
|
+
if (entry.droppedAccounts.some((dropped) => dropped.agent === agent)) return;
|
|
3171
|
+
entry.droppedAccounts.push({ agent, account });
|
|
3172
|
+
}
|
|
2878
3173
|
function advanceSummary(entry, answers) {
|
|
2879
|
-
const
|
|
3174
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
3175
|
+
const answer = classifyAnswer(answers.get(entry.questions[0]) ?? "", candidates);
|
|
2880
3176
|
if (answer.kind === "customise") return typesForm(entry);
|
|
2881
3177
|
if (answer.kind === "last") {
|
|
2882
3178
|
for (const [index, task] of entry.tasks.entries()) {
|
|
2883
|
-
const remembered =
|
|
2884
|
-
if (remembered)
|
|
3179
|
+
const remembered = usableRemembered(entry.context, task.agent);
|
|
3180
|
+
if (!remembered) continue;
|
|
3181
|
+
if (remembered.droppedAccount) {
|
|
3182
|
+
noteDroppedAccount(entry, task.agent, remembered.droppedAccount);
|
|
3183
|
+
}
|
|
3184
|
+
entry.choices.set(index, { ...remembered.choice });
|
|
2885
3185
|
}
|
|
2886
3186
|
return release(entry, null);
|
|
2887
3187
|
}
|
|
@@ -2908,7 +3208,7 @@ function typesForm(entry) {
|
|
|
2908
3208
|
...typeQuestion(agent, entry.tasks, entry.context)
|
|
2909
3209
|
}));
|
|
2910
3210
|
const fields = types.map((agent) => ({ kind: "type", agent }));
|
|
2911
|
-
const account = accountQuestion(entry.context);
|
|
3211
|
+
const account = accountQuestion(entry.context, { types: types.length });
|
|
2912
3212
|
if (account) {
|
|
2913
3213
|
prompts.push({ header: "Account", ...account });
|
|
2914
3214
|
fields.push({ kind: "account" });
|
|
@@ -2920,25 +3220,30 @@ function typesForm(entry) {
|
|
|
2920
3220
|
call: buildCall(entry, prompts, questionCallId("types"))
|
|
2921
3221
|
};
|
|
2922
3222
|
}
|
|
3223
|
+
function readAccountAnswer(entry, raw, agent) {
|
|
3224
|
+
const trimmed = raw.trim();
|
|
3225
|
+
if (!trimmed || trimmed === DEFAULT_ANSWER) return void 0;
|
|
3226
|
+
const picked = normalizeAccountName(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
|
|
3227
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
3228
|
+
if (picked && candidates.includes(picked)) return picked;
|
|
3229
|
+
log.notice("subagent dispatch account answer refused", {
|
|
3230
|
+
sessionKey: entry.sessionKey,
|
|
3231
|
+
agent,
|
|
3232
|
+
answer: trimmed,
|
|
3233
|
+
candidates
|
|
3234
|
+
});
|
|
3235
|
+
return void 0;
|
|
3236
|
+
}
|
|
2923
3237
|
function advanceTypes(entry, answers) {
|
|
3238
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
2924
3239
|
const perTask = [];
|
|
2925
|
-
|
|
3240
|
+
let perTypeAccounts = false;
|
|
2926
3241
|
for (const [position, field] of entry.fields.entries()) {
|
|
2927
3242
|
const raw = answers.get(entry.questions[position]) ?? "";
|
|
2928
|
-
const answer = classifyAnswer(raw);
|
|
3243
|
+
const answer = classifyAnswer(raw, candidates);
|
|
2929
3244
|
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
|
-
}
|
|
3245
|
+
if (raw.trim() === PER_TYPE_ANSWER) perTypeAccounts = true;
|
|
3246
|
+
else entry.account = readAccountAnswer(entry, raw, null) ?? entry.account;
|
|
2942
3247
|
continue;
|
|
2943
3248
|
}
|
|
2944
3249
|
if (field.kind !== "type") continue;
|
|
@@ -2947,12 +3252,15 @@ function advanceTypes(entry, answers) {
|
|
|
2947
3252
|
continue;
|
|
2948
3253
|
}
|
|
2949
3254
|
if (answer.kind === "last") {
|
|
2950
|
-
const remembered =
|
|
2951
|
-
if (remembered)
|
|
3255
|
+
const remembered = usableRemembered(entry.context, field.agent);
|
|
3256
|
+
if (remembered?.droppedAccount) {
|
|
3257
|
+
noteDroppedAccount(entry, field.agent, remembered.droppedAccount);
|
|
3258
|
+
}
|
|
3259
|
+
if (remembered) applyTypeChoice(entry, field.agent, remembered.choice);
|
|
2952
3260
|
continue;
|
|
2953
3261
|
}
|
|
2954
3262
|
if (answer.kind === "choice") {
|
|
2955
|
-
|
|
3263
|
+
applyTypeChoice(entry, field.agent, answer.choice);
|
|
2956
3264
|
continue;
|
|
2957
3265
|
}
|
|
2958
3266
|
if (answer.kind === "unknown") {
|
|
@@ -2964,31 +3272,60 @@ function advanceTypes(entry, answers) {
|
|
|
2964
3272
|
}
|
|
2965
3273
|
}
|
|
2966
3274
|
for (const [index, task] of entry.tasks.entries()) {
|
|
2967
|
-
const choice = typeChoices.get(task.agent);
|
|
3275
|
+
const choice = entry.typeChoices.get(task.agent);
|
|
2968
3276
|
if (choice) entry.choices.set(index, { ...choice });
|
|
2969
3277
|
}
|
|
2970
|
-
if (perTask.length > 0
|
|
3278
|
+
if (perTask.length > 0 || perTypeAccounts) {
|
|
3279
|
+
return detailForm(entry, perTask, perTypeAccounts);
|
|
3280
|
+
}
|
|
2971
3281
|
return release(entry, null);
|
|
2972
3282
|
}
|
|
2973
|
-
function
|
|
3283
|
+
function applyTypeChoice(entry, agent, choice) {
|
|
3284
|
+
const { account, ...rest } = choice;
|
|
3285
|
+
if (account) entry.typeAccounts.set(agent, account);
|
|
3286
|
+
entry.typeChoices.set(agent, rest);
|
|
3287
|
+
}
|
|
3288
|
+
function detailForm(entry, perTaskAgents, perTypeAccounts) {
|
|
2974
3289
|
const prompts = [];
|
|
2975
3290
|
const fields = [];
|
|
3291
|
+
if (perTypeAccounts) {
|
|
3292
|
+
for (const agent of dispatchAgentTypes(entry.tasks)) {
|
|
3293
|
+
const account = accountQuestion(entry.context, { agent });
|
|
3294
|
+
if (!account) continue;
|
|
3295
|
+
prompts.push({ header: `${agent} account`, ...account });
|
|
3296
|
+
fields.push({ kind: "typeAccount", agent });
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
2976
3299
|
for (const [index, task] of entry.tasks.entries()) {
|
|
2977
|
-
if (!
|
|
3300
|
+
if (!perTaskAgents.includes(task.agent)) continue;
|
|
3301
|
+
const typeChoice = entry.typeChoices.get(task.agent);
|
|
3302
|
+
const typeAccount = entry.typeAccounts.get(task.agent);
|
|
2978
3303
|
prompts.push({
|
|
2979
3304
|
header: task.description || task.agent,
|
|
2980
|
-
...taskQuestion(
|
|
3305
|
+
...taskQuestion(
|
|
3306
|
+
task,
|
|
3307
|
+
typeChoice || typeAccount ? { ...typeChoice, ...typeAccount ? { account: typeAccount } : {} } : void 0,
|
|
3308
|
+
entry.context
|
|
3309
|
+
)
|
|
2981
3310
|
});
|
|
2982
3311
|
fields.push({ kind: "task", taskIndex: index });
|
|
2983
3312
|
}
|
|
2984
|
-
|
|
3313
|
+
if (prompts.length === 0) return release(entry, null);
|
|
3314
|
+
entry.stage = "detail";
|
|
2985
3315
|
entry.fields = fields;
|
|
2986
|
-
return { kind: "form", call: buildCall(entry, prompts, questionCallId("
|
|
3316
|
+
return { kind: "form", call: buildCall(entry, prompts, questionCallId("detail")) };
|
|
2987
3317
|
}
|
|
2988
|
-
function
|
|
3318
|
+
function advanceDetail(entry, answers) {
|
|
3319
|
+
const candidates = dispatchAccountCandidates(entry.context);
|
|
2989
3320
|
for (const [position, field] of entry.fields.entries()) {
|
|
3321
|
+
const raw = answers.get(entry.questions[position]) ?? "";
|
|
3322
|
+
if (field.kind === "typeAccount") {
|
|
3323
|
+
const picked = readAccountAnswer(entry, raw, field.agent);
|
|
3324
|
+
if (picked) entry.typeAccounts.set(field.agent, picked);
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
2990
3327
|
if (field.kind !== "task") continue;
|
|
2991
|
-
const answer = classifyAnswer(
|
|
3328
|
+
const answer = classifyAnswer(raw, candidates);
|
|
2992
3329
|
if (answer.kind === "choice") {
|
|
2993
3330
|
entry.choices.set(field.taskIndex, answer.choice);
|
|
2994
3331
|
continue;
|
|
@@ -3008,13 +3345,18 @@ function advanceTasks(entry, answers) {
|
|
|
3008
3345
|
}
|
|
3009
3346
|
return release(entry, null);
|
|
3010
3347
|
}
|
|
3348
|
+
function droppedAccountNote(dropped) {
|
|
3349
|
+
const list2 = dropped.map((entry) => `"${entry.agent}" (was "${entry.account}")`).join(", ");
|
|
3350
|
+
return `
|
|
3351
|
+
${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.
|
|
3352
|
+
`;
|
|
3353
|
+
}
|
|
3011
3354
|
function release(entry, note) {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
}
|
|
3355
|
+
for (const [index, task] of entry.tasks.entries()) {
|
|
3356
|
+
const current = entry.choices.get(index);
|
|
3357
|
+
const account = current?.account ?? entry.typeAccounts.get(task.agent) ?? entry.account;
|
|
3358
|
+
if (!account) continue;
|
|
3359
|
+
entry.choices.set(index, { ...current ?? {}, account });
|
|
3018
3360
|
}
|
|
3019
3361
|
const seen = /* @__PURE__ */ new Set();
|
|
3020
3362
|
for (const [index, task] of entry.tasks.entries()) {
|
|
@@ -3024,6 +3366,15 @@ function release(entry, note) {
|
|
|
3024
3366
|
seen.add(task.agent);
|
|
3025
3367
|
rememberDispatchChoice(entry.context.parentSessionId, task.agent, choice);
|
|
3026
3368
|
}
|
|
3369
|
+
if (!note && entry.droppedAccounts.length > 0) {
|
|
3370
|
+
log.notice("a remembered subagent dispatch account is no longer available", {
|
|
3371
|
+
sessionKey: entry.sessionKey,
|
|
3372
|
+
// `agent` rather than a key of its own: `dropped` already means a count
|
|
3373
|
+
// in the bundle allowlist, and these are agent names like every other.
|
|
3374
|
+
agent: entry.droppedAccounts.map((dropped) => dropped.agent)
|
|
3375
|
+
});
|
|
3376
|
+
note = droppedAccountNote(entry.droppedAccounts);
|
|
3377
|
+
}
|
|
3027
3378
|
return {
|
|
3028
3379
|
kind: "release",
|
|
3029
3380
|
heldCallIds: entry.heldCallIds,
|
|
@@ -3060,8 +3411,8 @@ import { execFile } from "child_process";
|
|
|
3060
3411
|
import { promisify } from "util";
|
|
3061
3412
|
|
|
3062
3413
|
// src/windows-spawn.ts
|
|
3063
|
-
import * as
|
|
3064
|
-
import * as
|
|
3414
|
+
import * as fs2 from "fs";
|
|
3415
|
+
import * as path4 from "path";
|
|
3065
3416
|
var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
3066
3417
|
function envValue(env, name) {
|
|
3067
3418
|
const direct = env[name];
|
|
@@ -3071,7 +3422,7 @@ function envValue(env, name) {
|
|
|
3071
3422
|
}
|
|
3072
3423
|
function defaultIsFile(candidate) {
|
|
3073
3424
|
try {
|
|
3074
|
-
return
|
|
3425
|
+
return fs2.statSync(candidate).isFile();
|
|
3075
3426
|
} catch {
|
|
3076
3427
|
return false;
|
|
3077
3428
|
}
|
|
@@ -3081,7 +3432,7 @@ function executableExtensions(env = process.env) {
|
|
|
3081
3432
|
return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
|
|
3082
3433
|
}
|
|
3083
3434
|
function isBatchFile(file) {
|
|
3084
|
-
const ext =
|
|
3435
|
+
const ext = path4.win32.extname(file).toLowerCase();
|
|
3085
3436
|
return ext === ".cmd" || ext === ".bat";
|
|
3086
3437
|
}
|
|
3087
3438
|
function resolveWindowsCommand(command, deps = {}) {
|
|
@@ -3089,7 +3440,7 @@ function resolveWindowsCommand(command, deps = {}) {
|
|
|
3089
3440
|
const isFile = deps.isFile ?? defaultIsFile;
|
|
3090
3441
|
const extensions = executableExtensions(env);
|
|
3091
3442
|
const candidatesFor = (base) => {
|
|
3092
|
-
const ext =
|
|
3443
|
+
const ext = path4.win32.extname(base).toLowerCase();
|
|
3093
3444
|
if (ext && extensions.includes(ext)) return [base];
|
|
3094
3445
|
return [...extensions.map((candidate) => base + candidate), base];
|
|
3095
3446
|
};
|
|
@@ -3100,10 +3451,10 @@ function resolveWindowsCommand(command, deps = {}) {
|
|
|
3100
3451
|
return null;
|
|
3101
3452
|
}
|
|
3102
3453
|
const pathValue = envValue(env, "PATH") ?? "";
|
|
3103
|
-
for (const entry of pathValue.split(
|
|
3454
|
+
for (const entry of pathValue.split(path4.win32.delimiter)) {
|
|
3104
3455
|
const directory = entry.trim().replace(/^"(.*)"$/, "$1");
|
|
3105
3456
|
if (!directory) continue;
|
|
3106
|
-
for (const candidate of candidatesFor(
|
|
3457
|
+
for (const candidate of candidatesFor(path4.win32.join(directory, command))) {
|
|
3107
3458
|
if (isFile(candidate)) return candidate;
|
|
3108
3459
|
}
|
|
3109
3460
|
}
|
|
@@ -3132,7 +3483,7 @@ function quoteBatchArgument(arg) {
|
|
|
3132
3483
|
}
|
|
3133
3484
|
function planCmdInvocation(file, args, env = process.env) {
|
|
3134
3485
|
const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
|
|
3135
|
-
const line = [`"${
|
|
3486
|
+
const line = [`"${path4.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
|
|
3136
3487
|
return {
|
|
3137
3488
|
file: comspec,
|
|
3138
3489
|
args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
|
|
@@ -3376,145 +3727,6 @@ import * as fs3 from "fs";
|
|
|
3376
3727
|
import * as path5 from "path";
|
|
3377
3728
|
import * as crypto from "crypto";
|
|
3378
3729
|
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
3730
|
var PROXY_KEEPALIVE_MS = 15e3;
|
|
3519
3731
|
function acceptsEventStream(acceptHeader) {
|
|
3520
3732
|
return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
|
|
@@ -6023,17 +6235,17 @@ function tailIsReplyOnly(history, from) {
|
|
|
6023
6235
|
}
|
|
6024
6236
|
|
|
6025
6237
|
// src/session-resume-store.ts
|
|
6026
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
6027
|
-
import { homedir as
|
|
6028
|
-
import { join as
|
|
6238
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6239
|
+
import { homedir as homedir6 } from "os";
|
|
6240
|
+
import { join as join8 } from "path";
|
|
6029
6241
|
var MAX_RESUME_RECORDS = 256;
|
|
6030
6242
|
var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6031
6243
|
var records = null;
|
|
6032
|
-
var
|
|
6244
|
+
var pathOverride2 = null;
|
|
6033
6245
|
function resumeStorePath() {
|
|
6034
|
-
if (
|
|
6035
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
6036
|
-
return
|
|
6246
|
+
if (pathOverride2) return pathOverride2;
|
|
6247
|
+
const stateRoot = process.env.XDG_STATE_HOME || join8(homedir6(), ".local", "state");
|
|
6248
|
+
return join8(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
|
|
6037
6249
|
}
|
|
6038
6250
|
function isDigest(value) {
|
|
6039
6251
|
const entry = value;
|
|
@@ -6043,11 +6255,11 @@ function isRecord2(value) {
|
|
|
6043
6255
|
const record = value;
|
|
6044
6256
|
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
6257
|
}
|
|
6046
|
-
function
|
|
6258
|
+
function load2() {
|
|
6047
6259
|
if (records) return records;
|
|
6048
6260
|
records = /* @__PURE__ */ new Map();
|
|
6049
6261
|
try {
|
|
6050
|
-
const parsed = JSON.parse(
|
|
6262
|
+
const parsed = JSON.parse(readFileSync4(resumeStorePath(), "utf8"));
|
|
6051
6263
|
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
6052
6264
|
if (entries && typeof entries === "object") {
|
|
6053
6265
|
const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
|
|
@@ -6058,11 +6270,11 @@ function load() {
|
|
|
6058
6270
|
}
|
|
6059
6271
|
return records;
|
|
6060
6272
|
}
|
|
6061
|
-
function
|
|
6273
|
+
function save2(update) {
|
|
6062
6274
|
const file = resumeStorePath();
|
|
6063
6275
|
const merged = /* @__PURE__ */ new Map();
|
|
6064
6276
|
try {
|
|
6065
|
-
const onDisk = JSON.parse(
|
|
6277
|
+
const onDisk = JSON.parse(readFileSync4(file, "utf8"))?.sessions;
|
|
6066
6278
|
if (onDisk && typeof onDisk === "object") {
|
|
6067
6279
|
for (const [key, record] of Object.entries(onDisk)) {
|
|
6068
6280
|
if (isRecord2(record)) merged.set(key, record);
|
|
@@ -6075,12 +6287,12 @@ function save(update) {
|
|
|
6075
6287
|
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
|
|
6076
6288
|
records = new Map(kept);
|
|
6077
6289
|
try {
|
|
6078
|
-
|
|
6290
|
+
mkdirSync4(join8(file, ".."), { recursive: true, mode: 448 });
|
|
6079
6291
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
6080
|
-
|
|
6292
|
+
writeFileSync4(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
6081
6293
|
mode: 384
|
|
6082
6294
|
});
|
|
6083
|
-
|
|
6295
|
+
renameSync3(tmp, file);
|
|
6084
6296
|
} catch (err) {
|
|
6085
6297
|
log.debug("could not write the claude session resume store", {
|
|
6086
6298
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -6093,11 +6305,11 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
|
|
|
6093
6305
|
const shape2 = conversationDigests(prompt, { assistantContent: false });
|
|
6094
6306
|
const configDir = where?.configDir;
|
|
6095
6307
|
const account = where?.account;
|
|
6096
|
-
const current =
|
|
6308
|
+
const current = load2().get(sessionKey2);
|
|
6097
6309
|
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
6310
|
return;
|
|
6099
6311
|
}
|
|
6100
|
-
|
|
6312
|
+
save2((merged) => {
|
|
6101
6313
|
merged.delete(sessionKey2);
|
|
6102
6314
|
merged.set(sessionKey2, {
|
|
6103
6315
|
claudeSessionId,
|
|
@@ -6111,22 +6323,22 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
|
|
|
6111
6323
|
});
|
|
6112
6324
|
}
|
|
6113
6325
|
function forgetResumePoint(sessionKey2) {
|
|
6114
|
-
if (!
|
|
6115
|
-
|
|
6326
|
+
if (!load2().has(sessionKey2)) return;
|
|
6327
|
+
save2((merged) => {
|
|
6116
6328
|
merged.delete(sessionKey2);
|
|
6117
6329
|
});
|
|
6118
6330
|
}
|
|
6119
6331
|
function moveResumePoint(fromKey, toKey) {
|
|
6120
|
-
const record =
|
|
6332
|
+
const record = load2().get(fromKey);
|
|
6121
6333
|
if (!record) return;
|
|
6122
|
-
|
|
6334
|
+
save2((merged) => {
|
|
6123
6335
|
merged.delete(fromKey);
|
|
6124
6336
|
if (merged.has(toKey)) return;
|
|
6125
6337
|
merged.set(toKey, { ...record, updatedAt: Date.now() });
|
|
6126
6338
|
});
|
|
6127
6339
|
}
|
|
6128
6340
|
function findResumePoint(opts) {
|
|
6129
|
-
const record =
|
|
6341
|
+
const record = load2().get(opts.sessionKey);
|
|
6130
6342
|
if (!record) {
|
|
6131
6343
|
opts.onRefused?.("no-session-recorded");
|
|
6132
6344
|
return void 0;
|
|
@@ -6159,7 +6371,7 @@ function findSiblingResumePoint(opts) {
|
|
|
6159
6371
|
let best;
|
|
6160
6372
|
let bestUpdatedAt = -1;
|
|
6161
6373
|
let refusal;
|
|
6162
|
-
for (const [key, record] of
|
|
6374
|
+
for (const [key, record] of load2()) {
|
|
6163
6375
|
if (key === opts.sessionKey) continue;
|
|
6164
6376
|
if (modelSiblingSignature(key, providers) !== signature) continue;
|
|
6165
6377
|
const siblingAccount = providers ? sessionKeyAccount(key, providers) : void 0;
|
|
@@ -6228,7 +6440,7 @@ function findForeignAccountSibling(opts) {
|
|
|
6228
6440
|
}
|
|
6229
6441
|
let found;
|
|
6230
6442
|
let foundUpdatedAt = -1;
|
|
6231
|
-
for (const [key, record] of
|
|
6443
|
+
for (const [key, record] of load2()) {
|
|
6232
6444
|
const hit = consider(key);
|
|
6233
6445
|
if (hit && record.updatedAt >= foundUpdatedAt) {
|
|
6234
6446
|
found = hit;
|
|
@@ -6848,6 +7060,7 @@ function deleteActiveProcessesForSession(sessionID) {
|
|
|
6848
7060
|
}
|
|
6849
7061
|
for (const key of released) clearBackgroundTasks(key);
|
|
6850
7062
|
forgetSessionChoice(sessionID);
|
|
7063
|
+
forgetDispatchChoicesForSession(sessionID);
|
|
6851
7064
|
return released;
|
|
6852
7065
|
}
|
|
6853
7066
|
function killAllActiveProcesses() {
|
|
@@ -7583,7 +7796,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
7583
7796
|
// src/diagnostic-bundle.ts
|
|
7584
7797
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
7585
7798
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
7586
|
-
import { homedir as
|
|
7799
|
+
import { homedir as homedir7 } from "os";
|
|
7587
7800
|
|
|
7588
7801
|
// src/log-messages.ts
|
|
7589
7802
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -7593,6 +7806,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
7593
7806
|
"Code Mode execute not forwarded: read-only permission preset",
|
|
7594
7807
|
"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
7808
|
"MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
|
|
7809
|
+
"a remembered subagent dispatch account is no longer available",
|
|
7596
7810
|
"account failover declined; ending the turn",
|
|
7597
7811
|
"accountGroups must be an object mapping an account name to a group name; ignoring it",
|
|
7598
7812
|
"accountGroups names an account that is not configured; ignoring it",
|
|
@@ -7974,7 +8188,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
7974
8188
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
7975
8189
|
}
|
|
7976
8190
|
function createRedactionContext(options = {}) {
|
|
7977
|
-
const home = options.home ??
|
|
8191
|
+
const home = options.home ?? homedir7();
|
|
7978
8192
|
const homes = /* @__PURE__ */ new Set();
|
|
7979
8193
|
if (home) homes.add(home);
|
|
7980
8194
|
if (home) {
|
|
@@ -10116,9 +10330,9 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
10116
10330
|
// src/claude-session-wrapper.ts
|
|
10117
10331
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
10118
10332
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
10119
|
-
import { existsSync as existsSync4, writeFileSync as
|
|
10333
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
10120
10334
|
import { unlink as unlink2 } from "fs/promises";
|
|
10121
|
-
import { join as
|
|
10335
|
+
import { join as join13 } from "path";
|
|
10122
10336
|
|
|
10123
10337
|
// src/claude-session-bun.ts
|
|
10124
10338
|
import * as os5 from "os";
|
|
@@ -11299,8 +11513,8 @@ ${text2}`
|
|
|
11299
11513
|
}
|
|
11300
11514
|
function stageImage(data, extension) {
|
|
11301
11515
|
try {
|
|
11302
|
-
const file =
|
|
11303
|
-
|
|
11516
|
+
const file = join13(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
|
|
11517
|
+
writeFileSync6(file, data, { mode: 384, flag: "wx" });
|
|
11304
11518
|
return file;
|
|
11305
11519
|
} catch (err) {
|
|
11306
11520
|
log.warn("interactive transport could not stage an image", {
|
|
@@ -11841,13 +12055,13 @@ function dedupe2(dirs) {
|
|
|
11841
12055
|
}
|
|
11842
12056
|
|
|
11843
12057
|
// src/prompts.ts
|
|
11844
|
-
import { readFileSync as
|
|
11845
|
-
import { homedir as
|
|
12058
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
12059
|
+
import { homedir as homedir10 } from "os";
|
|
11846
12060
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
11847
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
12061
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join14 } from "path";
|
|
11848
12062
|
function readPromptFileIfPresent(path15) {
|
|
11849
12063
|
try {
|
|
11850
|
-
const content =
|
|
12064
|
+
const content = readFileSync8(path15, "utf8").trim();
|
|
11851
12065
|
return content || void 0;
|
|
11852
12066
|
} catch {
|
|
11853
12067
|
return void 0;
|
|
@@ -11856,7 +12070,7 @@ function readPromptFileIfPresent(path15) {
|
|
|
11856
12070
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
11857
12071
|
let dir = cwd;
|
|
11858
12072
|
while (true) {
|
|
11859
|
-
const content = readPromptFileIfPresent(
|
|
12073
|
+
const content = readPromptFileIfPresent(join14(dir, "AGENTS.md"));
|
|
11860
12074
|
if (content) return content;
|
|
11861
12075
|
const parent = dirname6(dir);
|
|
11862
12076
|
if (parent === dir) return void 0;
|
|
@@ -11952,9 +12166,9 @@ function extractSystemMessages(prompt) {
|
|
|
11952
12166
|
}
|
|
11953
12167
|
var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
|
|
11954
12168
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
11955
|
-
const
|
|
12169
|
+
const readFile3 = opts.readFile ?? ((path15) => {
|
|
11956
12170
|
try {
|
|
11957
|
-
return
|
|
12171
|
+
return readFileSync8(path15, "utf8");
|
|
11958
12172
|
} catch {
|
|
11959
12173
|
return null;
|
|
11960
12174
|
}
|
|
@@ -11970,7 +12184,7 @@ function userAuthoredInstructions(forwarded, opts = {}) {
|
|
|
11970
12184
|
if (seen.has(path15) || !isAbsolute3(path15)) continue;
|
|
11971
12185
|
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path15))) continue;
|
|
11972
12186
|
seen.add(path15);
|
|
11973
|
-
const file =
|
|
12187
|
+
const file = readFile3(path15);
|
|
11974
12188
|
if (!file?.trim()) continue;
|
|
11975
12189
|
const start = match.index + match[0].length + 1;
|
|
11976
12190
|
const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
|
|
@@ -11995,8 +12209,8 @@ ${options.compressionSummary.trim()}`
|
|
|
11995
12209
|
for (const s of extraSystemContent) {
|
|
11996
12210
|
if (s.trim()) parts.push(s.trim());
|
|
11997
12211
|
}
|
|
11998
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
11999
|
-
const globalAgents = readPromptFileIfPresent(
|
|
12212
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
|
|
12213
|
+
const globalAgents = readPromptFileIfPresent(join14(configRoot, "opencode", "AGENTS.md"));
|
|
12000
12214
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
12001
12215
|
const forwarded = extraSystemContent.join("\n\n");
|
|
12002
12216
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -12012,9 +12226,9 @@ ${options.compressionSummary.trim()}`
|
|
|
12012
12226
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
12013
12227
|
const content = parts.join("\n\n");
|
|
12014
12228
|
if (!content) return void 0;
|
|
12015
|
-
const path15 =
|
|
12229
|
+
const path15 = join14(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
|
|
12016
12230
|
try {
|
|
12017
|
-
|
|
12231
|
+
writeFileSync7(path15, content, { encoding: "utf8", mode: 384 });
|
|
12018
12232
|
return path15;
|
|
12019
12233
|
} catch (err) {
|
|
12020
12234
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -15346,7 +15560,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15346
15560
|
let replayReason = "no-session-for-key";
|
|
15347
15561
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
15348
15562
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
15349
|
-
const transcriptPath2 = (id, dir) =>
|
|
15563
|
+
const transcriptPath2 = (id, dir) => join15(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
15350
15564
|
const resumePoint = findResumePoint({
|
|
15351
15565
|
sessionKey: sk,
|
|
15352
15566
|
prompt: options.prompt,
|
|
@@ -16693,7 +16907,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16693
16907
|
};
|
|
16694
16908
|
|
|
16695
16909
|
// src/account-tier.ts
|
|
16696
|
-
import { readFileSync as
|
|
16910
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
16697
16911
|
import path13 from "path";
|
|
16698
16912
|
var ORGANIZATION_TYPE_LABELS = {
|
|
16699
16913
|
claude_max: "Max",
|
|
@@ -16736,7 +16950,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
|
|
|
16736
16950
|
function readOauthAccount(file) {
|
|
16737
16951
|
let raw;
|
|
16738
16952
|
try {
|
|
16739
|
-
raw =
|
|
16953
|
+
raw = readFileSync9(file, "utf8");
|
|
16740
16954
|
} catch {
|
|
16741
16955
|
return void 0;
|
|
16742
16956
|
}
|
|
@@ -16764,14 +16978,14 @@ function tierLabel(oauthAccount) {
|
|
|
16764
16978
|
// src/cleanup-stale.ts
|
|
16765
16979
|
import {
|
|
16766
16980
|
existsSync as existsSync5,
|
|
16767
|
-
mkdirSync as
|
|
16768
|
-
readFileSync as
|
|
16981
|
+
mkdirSync as mkdirSync6,
|
|
16982
|
+
readFileSync as readFileSync10,
|
|
16769
16983
|
realpathSync as realpathSync4,
|
|
16770
16984
|
rmSync as rmSync3,
|
|
16771
|
-
writeFileSync as
|
|
16985
|
+
writeFileSync as writeFileSync8
|
|
16772
16986
|
} from "fs";
|
|
16773
|
-
import { homedir as
|
|
16774
|
-
import { dirname as dirname7, join as
|
|
16987
|
+
import { homedir as homedir11 } from "os";
|
|
16988
|
+
import { dirname as dirname7, join as join16, resolve as resolve5 } from "path";
|
|
16775
16989
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
16776
16990
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
16777
16991
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -16779,20 +16993,20 @@ var alreadyRan = false;
|
|
|
16779
16993
|
function candidateCacheRoots() {
|
|
16780
16994
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
16781
16995
|
return [
|
|
16782
|
-
xdg ?
|
|
16783
|
-
|
|
16784
|
-
|
|
16996
|
+
xdg ? join16(xdg, "opencode") : null,
|
|
16997
|
+
join16(homedir11(), ".cache", "opencode"),
|
|
16998
|
+
join16(homedir11(), "Library", "Caches", "opencode")
|
|
16785
16999
|
].filter((p) => Boolean(p));
|
|
16786
17000
|
}
|
|
16787
17001
|
function userOpencodeJsonPath() {
|
|
16788
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
16789
|
-
return
|
|
17002
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join16(homedir11(), ".config");
|
|
17003
|
+
return join16(xdgConfig, "opencode", "opencode.json");
|
|
16790
17004
|
}
|
|
16791
17005
|
function userIntendsToUseUnscoped() {
|
|
16792
17006
|
const cfg = userOpencodeJsonPath();
|
|
16793
17007
|
if (!existsSync5(cfg)) return false;
|
|
16794
17008
|
try {
|
|
16795
|
-
const json = JSON.parse(
|
|
17009
|
+
const json = JSON.parse(readFileSync10(cfg, "utf8"));
|
|
16796
17010
|
const plugins = json.plugin;
|
|
16797
17011
|
if (!Array.isArray(plugins)) return false;
|
|
16798
17012
|
return plugins.some(
|
|
@@ -16813,19 +17027,19 @@ function ourLoadedDir() {
|
|
|
16813
17027
|
function ourVersion(dir) {
|
|
16814
17028
|
if (!dir) return null;
|
|
16815
17029
|
try {
|
|
16816
|
-
const pkg = JSON.parse(
|
|
17030
|
+
const pkg = JSON.parse(readFileSync10(join16(dir, "package.json"), "utf8"));
|
|
16817
17031
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
16818
17032
|
} catch {
|
|
16819
17033
|
return null;
|
|
16820
17034
|
}
|
|
16821
17035
|
}
|
|
16822
17036
|
function markerPath() {
|
|
16823
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
16824
|
-
return
|
|
17037
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join16(homedir11(), ".local", "state");
|
|
17038
|
+
return join16(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
16825
17039
|
}
|
|
16826
17040
|
function markerVersion() {
|
|
16827
17041
|
try {
|
|
16828
|
-
const marker = JSON.parse(
|
|
17042
|
+
const marker = JSON.parse(readFileSync10(markerPath(), "utf8"));
|
|
16829
17043
|
return typeof marker.version === "string" ? marker.version : null;
|
|
16830
17044
|
} catch {
|
|
16831
17045
|
return null;
|
|
@@ -16834,8 +17048,8 @@ function markerVersion() {
|
|
|
16834
17048
|
function writeMarker(version) {
|
|
16835
17049
|
const file = markerPath();
|
|
16836
17050
|
try {
|
|
16837
|
-
|
|
16838
|
-
|
|
17051
|
+
mkdirSync6(dirname7(file), { recursive: true });
|
|
17052
|
+
writeFileSync8(
|
|
16839
17053
|
file,
|
|
16840
17054
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
16841
17055
|
"utf8"
|
|
@@ -16870,7 +17084,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
16870
17084
|
}
|
|
16871
17085
|
function cleanupOne(cacheRoot, ourDir) {
|
|
16872
17086
|
if (!existsSync5(cacheRoot)) return;
|
|
16873
|
-
const stalePath =
|
|
17087
|
+
const stalePath = join16(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
16874
17088
|
if (!existsSync5(stalePath)) return;
|
|
16875
17089
|
let realStalePath = stalePath;
|
|
16876
17090
|
try {
|
|
@@ -16878,11 +17092,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
16878
17092
|
} catch {
|
|
16879
17093
|
}
|
|
16880
17094
|
if (ourDir && realStalePath === ourDir) return;
|
|
16881
|
-
const pkgJsonPath =
|
|
17095
|
+
const pkgJsonPath = join16(stalePath, "package.json");
|
|
16882
17096
|
if (!existsSync5(pkgJsonPath)) return;
|
|
16883
17097
|
let pkg = {};
|
|
16884
17098
|
try {
|
|
16885
|
-
pkg = JSON.parse(
|
|
17099
|
+
pkg = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
|
|
16886
17100
|
} catch {
|
|
16887
17101
|
return;
|
|
16888
17102
|
}
|
|
@@ -16898,13 +17112,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
16898
17112
|
});
|
|
16899
17113
|
return;
|
|
16900
17114
|
}
|
|
16901
|
-
const cachePkgJson =
|
|
17115
|
+
const cachePkgJson = join16(cacheRoot, "package.json");
|
|
16902
17116
|
if (!existsSync5(cachePkgJson)) return;
|
|
16903
17117
|
try {
|
|
16904
|
-
const cfg = JSON.parse(
|
|
17118
|
+
const cfg = JSON.parse(readFileSync10(cachePkgJson, "utf8"));
|
|
16905
17119
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
16906
17120
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
16907
|
-
|
|
17121
|
+
writeFileSync8(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
16908
17122
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
16909
17123
|
}
|
|
16910
17124
|
} catch (err) {
|