@songsid/agend 2.2.0-beta.3 → 2.2.0-beta.5
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/agent-cli.js +6 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/backend/claude-code.d.ts +46 -2
- package/dist/backend/claude-code.js +125 -31
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +33 -3
- package/dist/backend/credential-profile.js +107 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +13 -0
- package/dist/channel/adapters/discord.js +42 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +6 -1
- package/dist/channel/adapters/telegram.js +27 -2
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +22 -1
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +7 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +96 -0
- package/dist/daemon.js +339 -16
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +18 -5
- package/dist/delivery-outbox.js +121 -14
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -0
- package/dist/fleet-manager.d.ts +89 -3
- package/dist/fleet-manager.js +541 -86
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +25 -6
- package/dist/instance-lifecycle.js +73 -26
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +48 -18
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/outbound-handlers.d.ts +61 -0
- package/dist/outbound-handlers.js +100 -35
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-preview.d.ts +100 -0
- package/dist/public-preview.js +233 -0
- package/dist/public-preview.js.map +1 -0
- package/dist/public-web-gateway.js +6 -4
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +20 -0
- package/dist/public-web-link.js +26 -1
- package/dist/public-web-link.js.map +1 -1
- package/dist/restart-progress.d.ts +29 -0
- package/dist/restart-progress.js +23 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/settings-api.js +10 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/topic-commands.js +9 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +5 -1
- package/dist/tunnel/purpose-lane.js +14 -7
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +25 -0
- package/dist/ui/chat-strings.js +14 -2
- package/dist/ui/chat-thread.js +28 -10
- package/dist/ui/panel-chat.js +26 -11
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +199 -57
- package/dist/ui/panel-needs.js +4 -3
- package/dist/ui/panel-settings.js +4 -2
- package/dist/ui/preview-panel.js +48 -14
- package/dist/ui/preview-status.js +28 -0
- package/dist/ui/preview.js +40 -3
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-i18n.js +4 -0
- package/dist/ui/shared/app-prefs.js +1 -0
- package/dist/ui/shared/app-shell.js +146 -2
- package/dist/ui/shared/app-store.js +3 -1
- package/dist/ui/shared/app.css +47 -20
- package/dist/ui/shared/instance-nav.js +79 -23
- package/dist/ui/shared/view-strings.js +10 -0
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-install.d.ts +8 -0
- package/dist/update-install.js +9 -3
- package/dist/update-install.js.map +1 -1
- package/dist/update-marker.d.ts +40 -0
- package/dist/update-marker.js +49 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +34 -5
- package/dist/usage/providers.js +160 -27
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.d.ts +6 -2
- package/dist/usage/statusline-usage.js +10 -4
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/web-api.d.ts +20 -0
- package/dist/web-api.js +216 -30
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +3 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.js +2 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -45,7 +45,9 @@ import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
|
45
45
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
46
46
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
47
47
|
import { codexLiveRowMatches } from "./backend/codex.js";
|
|
48
|
+
import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
|
|
48
49
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
50
|
+
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
49
51
|
const __filename = fileURLToPath(import.meta.url);
|
|
50
52
|
const __dirname = dirname(__filename);
|
|
51
53
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -522,8 +524,16 @@ const DIALOG_PROBE_UNKNOWN_MAX = 40;
|
|
|
522
524
|
/** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
|
|
523
525
|
const STARTUP_DIALOG_POLL_MS = 500;
|
|
524
526
|
const STARTUP_DIALOG_BUDGET_MS = 30_000;
|
|
527
|
+
/** #1690: after AgEnD's own `/model`, how long the pane is read for claude's "Switch model?" or its result. */
|
|
528
|
+
export const OWN_SWITCH_WAIT_MS = 6_000;
|
|
529
|
+
/** #1690: after an answer to that dialog, how long it gets to go away. */
|
|
530
|
+
const OWN_SWITCH_SETTLE_MS = 3_000;
|
|
525
531
|
/** A blocking dialog still on screen this long after first being seen is reported for a human. */
|
|
526
532
|
const DIALOG_PARKED_NOTIFY_MS = 60_000;
|
|
533
|
+
/** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
|
|
534
|
+
export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
|
|
535
|
+
/** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
|
|
536
|
+
const COMPOSER_TAIL_ROWS = 4;
|
|
527
537
|
/** After a runtime dialog's keys, how long the pane gets to repaint before it is read again to see whether they took. */
|
|
528
538
|
const DIALOG_ANSWER_SETTLE_MS = 300;
|
|
529
539
|
/** Consecutive answers one runtime dialog ignored (still on screen right after the keys) before it is reported for a human. */
|
|
@@ -1163,6 +1173,34 @@ export class Daemon extends EventEmitter {
|
|
|
1163
1173
|
dialogParkedKey = null;
|
|
1164
1174
|
dialogParkedInteractionEpisode = null;
|
|
1165
1175
|
dialogParkedReported = false;
|
|
1176
|
+
/**
|
|
1177
|
+
* #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
|
|
1178
|
+
* it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
|
|
1179
|
+
*/
|
|
1180
|
+
unrecognisedPrompt = null;
|
|
1181
|
+
/**
|
|
1182
|
+
* #1691: when (performance.now) the newest pane read that decided the evidence above was taken — a scan or a fresh
|
|
1183
|
+
* read, confirming, restarting or ending it. A read taken before it finishes late and says nothing about now: it may
|
|
1184
|
+
* neither clear, restart nor confirm (#1695 r4).
|
|
1185
|
+
*/
|
|
1186
|
+
unrecognisedPromptReadAt = -Infinity;
|
|
1187
|
+
/**
|
|
1188
|
+
* #1691: the catch-all for that prompt, as a hold-only runtime dialog — so the delivery hold, awaiting_input, the
|
|
1189
|
+
* Needs-you item and the single parked report are the existing ones. Never answered (hold-only, no keys).
|
|
1190
|
+
*/
|
|
1191
|
+
unrecognisedPromptDialog = {
|
|
1192
|
+
pattern: /(?!)unrecognised-prompt-1691/,
|
|
1193
|
+
keys: [],
|
|
1194
|
+
description: "a prompt AgEnD does not recognise",
|
|
1195
|
+
holdOnly: true,
|
|
1196
|
+
isActive: (pane) => this.unrecognisedPromptActive(pane),
|
|
1197
|
+
requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
|
|
1198
|
+
};
|
|
1199
|
+
/**
|
|
1200
|
+
* #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
|
|
1201
|
+
* (if any) is answered. Every delivery is held meanwhile: the dialog's default cursor is Yes, a stray Enter confirms it.
|
|
1202
|
+
*/
|
|
1203
|
+
ownSwitch = null;
|
|
1166
1204
|
/**
|
|
1167
1205
|
* The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
|
|
1168
1206
|
* still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
|
|
@@ -3216,7 +3254,6 @@ export class Daemon extends EventEmitter {
|
|
|
3216
3254
|
if (this.runtimeMonitorsFrozen || this.errorMonitorTimer)
|
|
3217
3255
|
return;
|
|
3218
3256
|
const patterns = this.backend?.getErrorPatterns?.() ?? [];
|
|
3219
|
-
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
3220
3257
|
if (!this.tmux)
|
|
3221
3258
|
return;
|
|
3222
3259
|
if (!this.backend)
|
|
@@ -3252,6 +3289,9 @@ export class Daemon extends EventEmitter {
|
|
|
3252
3289
|
scanStartedAt = performance.now();
|
|
3253
3290
|
if (stale())
|
|
3254
3291
|
return;
|
|
3292
|
+
this.trackUnrecognisedPrompt(pane, captureMono, this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt);
|
|
3293
|
+
// The backend's dialogs, and #1691's catch-all while an unrecognised prompt is active (decided just above).
|
|
3294
|
+
const dialogs = this.runtimeDialogs();
|
|
3255
3295
|
if (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt) {
|
|
3256
3296
|
this.unverifyInteraction(captureMono, captureOrder);
|
|
3257
3297
|
}
|
|
@@ -3731,14 +3771,21 @@ export class Daemon extends EventEmitter {
|
|
|
3731
3771
|
isDeliveryEpochCurrent(epoch) {
|
|
3732
3772
|
return epoch === this.deliveryEpoch;
|
|
3733
3773
|
}
|
|
3734
|
-
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
|
|
3774
|
+
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt, ownSwitch) {
|
|
3775
|
+
const reportSwitch = (result) => {
|
|
3776
|
+
if (!ownSwitch)
|
|
3777
|
+
return;
|
|
3778
|
+
this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
3779
|
+
};
|
|
3735
3780
|
if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3736
3781
|
if (durableAttempt)
|
|
3737
3782
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
|
|
3783
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3738
3784
|
return;
|
|
3739
3785
|
}
|
|
3740
3786
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
3741
3787
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3788
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3742
3789
|
this.logger.info("Pending raw delivery dropped by user cancel");
|
|
3743
3790
|
if (durableAttempt)
|
|
3744
3791
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
|
|
@@ -3750,6 +3797,7 @@ export class Daemon extends EventEmitter {
|
|
|
3750
3797
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3751
3798
|
if (durableAttempt)
|
|
3752
3799
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
|
|
3800
|
+
reportSwitch({ outcome: "not_sent" }); // #1690: cancelled before anything was typed — said at once, nothing saved
|
|
3753
3801
|
return;
|
|
3754
3802
|
}
|
|
3755
3803
|
const delivered = await this.deliverMessage(rawText, undefined, {
|
|
@@ -3757,6 +3805,9 @@ export class Daemon extends EventEmitter {
|
|
|
3757
3805
|
verdict,
|
|
3758
3806
|
durableAttempt,
|
|
3759
3807
|
preserveExactBytes: true,
|
|
3808
|
+
// #1690: our own /model holds every other delivery from its write on — before the lock is released, so a steer
|
|
3809
|
+
// or /btw waiting on it cannot type into the gap before claude paints "Switch model?" (default cursor: Yes).
|
|
3810
|
+
...(ownSwitch ? { onPaneWritten: () => { this.ownSwitch = ownSwitch; } } : {}),
|
|
3760
3811
|
});
|
|
3761
3812
|
if (delivered)
|
|
3762
3813
|
this.finishDurableSubmission(durableAttempt ?? null, verdict);
|
|
@@ -3789,10 +3840,31 @@ export class Daemon extends EventEmitter {
|
|
|
3789
3840
|
else if (durableAttempt)
|
|
3790
3841
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
3791
3842
|
this.logger.warn({ err: err.message }, "raw_paste delivery error");
|
|
3843
|
+
if (ownSwitch) {
|
|
3844
|
+
if (this.ownSwitch === ownSwitch)
|
|
3845
|
+
this.ownSwitch = null;
|
|
3846
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3847
|
+
}
|
|
3792
3848
|
return;
|
|
3793
3849
|
}
|
|
3794
3850
|
if (confirmClear)
|
|
3795
3851
|
await this.confirmBackendClearDialog();
|
|
3852
|
+
if (ownSwitch) {
|
|
3853
|
+
if (this.ownSwitch !== ownSwitch)
|
|
3854
|
+
reportSwitch({ outcome: "not_sent" }); // its write never began
|
|
3855
|
+
else {
|
|
3856
|
+
try {
|
|
3857
|
+
reportSwitch(await this.answerOwnSwitchDialog(ownSwitch));
|
|
3858
|
+
}
|
|
3859
|
+
catch (err) {
|
|
3860
|
+
this.logger.warn({ err: err.message }, "Answering our own model switch failed");
|
|
3861
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3862
|
+
}
|
|
3863
|
+
finally {
|
|
3864
|
+
this.ownSwitch = null;
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3796
3868
|
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
3797
3869
|
});
|
|
3798
3870
|
this.pasteLock = this.pasteLock.catch(err => {
|
|
@@ -3811,7 +3883,93 @@ export class Daemon extends EventEmitter {
|
|
|
3811
3883
|
this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
|
|
3812
3884
|
return;
|
|
3813
3885
|
}
|
|
3814
|
-
|
|
3886
|
+
const sw = msg.runtime_switch;
|
|
3887
|
+
const ownSwitch = sw && typeof sw.id === "string" && sw.kind === "model" && typeof sw.requested === "string"
|
|
3888
|
+
? { id: sw.id, kind: "model", requested: sw.requested } : undefined;
|
|
3889
|
+
this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined, ownSwitch);
|
|
3890
|
+
}
|
|
3891
|
+
/**
|
|
3892
|
+
* #1690 (the user's choice A): after AgEnD's own `/model <name>`, claude may ask "Switch model?" (a warm cache for
|
|
3893
|
+
* another model). When the model it names is the one asked for, answer Yes; otherwise No (Escape — `Kept model as …`),
|
|
3894
|
+
* and say so. Every key goes on a freshly read, verified dialog under the pane write lock: the same target, and for
|
|
3895
|
+
* Enter the cursor on "1. Yes"; the cursor is walked there with Up, one verified step at a time, never a blind key.
|
|
3896
|
+
* Without the dialog, claude's own "Set model to …" line after the command is the result.
|
|
3897
|
+
*/
|
|
3898
|
+
async answerOwnSwitchDialog(spec) {
|
|
3899
|
+
const tmux = this.tmux;
|
|
3900
|
+
if (!tmux)
|
|
3901
|
+
return { outcome: "not_confirmed" };
|
|
3902
|
+
const read = () => tmux.capturePane().catch(() => "");
|
|
3903
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
3904
|
+
/** One key on a dialog re-read under the lock: still the same target, and (when given) the cursor where expected. */
|
|
3905
|
+
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
3906
|
+
if (this.tmux !== tmux)
|
|
3907
|
+
return false;
|
|
3908
|
+
const now = claudeSwitchDialog(await read());
|
|
3909
|
+
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
3910
|
+
return false;
|
|
3911
|
+
return tmux.sendSpecialKey(key);
|
|
3912
|
+
});
|
|
3913
|
+
const gone = async () => {
|
|
3914
|
+
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
3915
|
+
while (performance.now() < until) {
|
|
3916
|
+
if (!claudeSwitchDialog(await read()))
|
|
3917
|
+
return true;
|
|
3918
|
+
await sleep(150);
|
|
3919
|
+
}
|
|
3920
|
+
return false;
|
|
3921
|
+
};
|
|
3922
|
+
const deadline = performance.now() + OWN_SWITCH_WAIT_MS;
|
|
3923
|
+
while (performance.now() < deadline && this.tmux === tmux) {
|
|
3924
|
+
const pane = await read();
|
|
3925
|
+
const dialog = claudeSwitchDialog(pane);
|
|
3926
|
+
if (dialog && dialog.kind === "model") {
|
|
3927
|
+
const shown = dialog.target;
|
|
3928
|
+
if (!claudeSwitchTargetMatches(spec.requested, shown)) {
|
|
3929
|
+
const answered = await press(shown, null, "Escape") && await gone();
|
|
3930
|
+
this.logger.warn({ requested: spec.requested, shown, answered }, "claude would switch to another model than asked — answered No");
|
|
3931
|
+
return { outcome: "mismatch", shown, answered };
|
|
3932
|
+
}
|
|
3933
|
+
// Walk the cursor to "1. Yes" (it starts there; a human may have moved it): Up from row 2, re-read each time.
|
|
3934
|
+
let current = dialog;
|
|
3935
|
+
for (let step = 0; step < 2 && current.cursor === 2; step++) {
|
|
3936
|
+
if (!await press(shown, 2, "Up"))
|
|
3937
|
+
break;
|
|
3938
|
+
await sleep(120);
|
|
3939
|
+
const next = claudeSwitchDialog(await read());
|
|
3940
|
+
if (!next || next.target !== shown)
|
|
3941
|
+
break;
|
|
3942
|
+
current = next;
|
|
3943
|
+
}
|
|
3944
|
+
if (current.cursor !== 1 || !await press(shown, 1, "Enter") || !await gone()) {
|
|
3945
|
+
this.logger.warn({ requested: spec.requested, shown, cursor: current.cursor }, "Our own model switch dialog could not be confirmed on a verified cursor — left for a human");
|
|
3946
|
+
return { outcome: "not_confirmed", shown };
|
|
3947
|
+
}
|
|
3948
|
+
this.logger.info({ requested: spec.requested, shown }, "Confirmed our own model switch (claude: the next reply re-reads the history)");
|
|
3949
|
+
return { outcome: "switched", confirmed: true, shown };
|
|
3950
|
+
}
|
|
3951
|
+
if (Daemon.switchResultAfterCommand(pane, spec.requested))
|
|
3952
|
+
return { outcome: "switched", confirmed: false };
|
|
3953
|
+
await sleep(200);
|
|
3954
|
+
}
|
|
3955
|
+
return { outcome: "not_confirmed" };
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* #1690: claude's "Set model to …" line under our `/model <requested>` (no dialog was needed). Only the newest command
|
|
3959
|
+
* on the pane counts: an older identical `/model` with its result further up (switched away and back) must not pass
|
|
3960
|
+
* for this one before it has even painted — that left its dialog unanswered (live, 2.1.296).
|
|
3961
|
+
*/
|
|
3962
|
+
static switchResultAfterCommand(pane, requested) {
|
|
3963
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
3964
|
+
let last = -1;
|
|
3965
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
3966
|
+
if (/^❯[ \t\u00a0]+\S/.test(rows[i])) {
|
|
3967
|
+
last = i;
|
|
3968
|
+
break;
|
|
3969
|
+
}
|
|
3970
|
+
if (last < 0 || rows[last].replace(/^❯[ \t\u00a0]+/, "").trimEnd() !== `/model ${requested}`)
|
|
3971
|
+
return false;
|
|
3972
|
+
return rows.slice(last + 1).some(row => /^\s*⎿?\s*Set model to /.test(row));
|
|
3815
3973
|
}
|
|
3816
3974
|
/**
|
|
3817
3975
|
* Confirm a backend's terminal-level /clear prompt after the channel-level
|
|
@@ -4275,7 +4433,9 @@ export class Daemon extends EventEmitter {
|
|
|
4275
4433
|
// Bare backend regexes are not proof of a live modal. The canonical
|
|
4276
4434
|
// isActive predicate replaces pattern.test(), preserving table order.
|
|
4277
4435
|
const dialog = dialogs.find(d => d.isActive && (d.inputBlocked || d.blocksDelivery || d.holdOnly)
|
|
4278
|
-
&& Daemon.dialogMatches(d, pane)
|
|
4436
|
+
&& Daemon.dialogMatches(d, pane)
|
|
4437
|
+
// #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
|
|
4438
|
+
&& !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
|
|
4279
4439
|
if (dialog) {
|
|
4280
4440
|
const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
|
|
4281
4441
|
.split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
|
|
@@ -4291,7 +4451,7 @@ export class Daemon extends EventEmitter {
|
|
|
4291
4451
|
// No captured text leaves this function or the private observer identity.
|
|
4292
4452
|
return evidence ? { ...evidence, identity: createHash("sha256").update(evidence.identity).digest("hex") } : null;
|
|
4293
4453
|
}
|
|
4294
|
-
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.
|
|
4454
|
+
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.runtimeDialogs(), order = ++this.interactionCaptureSerial, allowConfirmation = true) {
|
|
4295
4455
|
const owner = this.interactionOwner();
|
|
4296
4456
|
const evidence = this.interactionEvidence(pane, dialogs);
|
|
4297
4457
|
if (!this.interactionObservation.observe(evidence, owner, observedAt, monotonicAt, this.interactionOutputRevision !== this.interactionObservedOutputRevision, order))
|
|
@@ -4776,10 +4936,7 @@ export class Daemon extends EventEmitter {
|
|
|
4776
4936
|
backend: resumeBackend ?? "unknown",
|
|
4777
4937
|
cwd: resumeCwd,
|
|
4778
4938
|
armedAt: Date.now(),
|
|
4779
|
-
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend),
|
|
4780
|
-
instanceDir: this.instanceDir,
|
|
4781
|
-
workingDirectory: resumeCwd,
|
|
4782
|
-
}),
|
|
4939
|
+
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), this.resumeRoots(resumeCwd)),
|
|
4783
4940
|
});
|
|
4784
4941
|
}
|
|
4785
4942
|
}
|
|
@@ -4825,7 +4982,7 @@ export class Daemon extends EventEmitter {
|
|
|
4825
4982
|
&& this.spawnGeneration === gateSpawn
|
|
4826
4983
|
&& this.launchFenceEpoch === gateFence;
|
|
4827
4984
|
const backend = this.resolveResumeBackend() ?? normalizeResumeBackend(marker.backend);
|
|
4828
|
-
const roots =
|
|
4985
|
+
const roots = this.resumeRoots(this.config.working_directory ?? "");
|
|
4829
4986
|
// This boot's CLI spawn is what the daemon itself caused: pre-restart
|
|
4830
4987
|
// writes (including pre-crash progress) are excluded, and the flush
|
|
4831
4988
|
// grace holds the decision until post-spawn writes are observable.
|
|
@@ -5554,6 +5711,8 @@ export class Daemon extends EventEmitter {
|
|
|
5554
5711
|
return;
|
|
5555
5712
|
if (!this.observeInteractionPane(pane, captureStartedAt, interactionCaptureAt, undefined, interactionCaptureOrder, reason !== "interaction_confirmation"))
|
|
5556
5713
|
return;
|
|
5714
|
+
// #1691: an accepted capture of this launch is a fresh read for the unrecognised-prompt evidence too (#1695 r3).
|
|
5715
|
+
this.noteFreshPaneRead(pane, interactionCaptureAt);
|
|
5557
5716
|
this.updateInputBlockedState(pane);
|
|
5558
5717
|
// Advance the shared 10s timer only for a fresh capture of this launch.
|
|
5559
5718
|
const deliveryFallback = deliveryCandidate === true
|
|
@@ -6712,6 +6871,9 @@ export class Daemon extends EventEmitter {
|
|
|
6712
6871
|
if (residue !== "clear")
|
|
6713
6872
|
return this.failDelivery(verdict, status, "input-residue", residue);
|
|
6714
6873
|
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
|
|
6874
|
+
// #1690: still under the lock — whatever waits on it next must already see what this write started.
|
|
6875
|
+
if (verdict.paneWriteStarted)
|
|
6876
|
+
opts?.onPaneWritten?.();
|
|
6715
6877
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
6716
6878
|
if (written === "handoff-box-unread")
|
|
6717
6879
|
return "dialog";
|
|
@@ -6759,7 +6921,148 @@ export class Daemon extends EventEmitter {
|
|
|
6759
6921
|
* on purpose ("I trust", "Yes, continue") and would match ordinary prose.
|
|
6760
6922
|
*/
|
|
6761
6923
|
deliveryBlockingDialogs() {
|
|
6762
|
-
return
|
|
6924
|
+
return this.runtimeDialogs().filter(d => d.blocksDelivery || d.holdOnly);
|
|
6925
|
+
}
|
|
6926
|
+
/**
|
|
6927
|
+
* The backend's runtime dialogs, then #1691's catch-all for a prompt none of them knows (last: known ones win). The
|
|
6928
|
+
* catch-all is listed only while such a prompt is active: otherwise it could match nothing, and a backend with no
|
|
6929
|
+
* dialogs keeps its empty list (its delivery path skips the dialog probe, as before).
|
|
6930
|
+
*/
|
|
6931
|
+
runtimeDialogs() {
|
|
6932
|
+
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
6933
|
+
return this.unrecognisedPromptCurrent()?.active ? [...dialogs, this.unrecognisedPromptDialog] : dialogs;
|
|
6934
|
+
}
|
|
6935
|
+
/**
|
|
6936
|
+
* #1691: is the pane at the CLI's own idle composer? Then whatever looks like a prompt above it is transcript output
|
|
6937
|
+
* (an agent's numbered list, a quoted "[y/n]"), not a prompt. The backend's own reading, most specific first.
|
|
6938
|
+
*/
|
|
6939
|
+
cliComposerShowing(pane, shape) {
|
|
6940
|
+
const backend = this.backend;
|
|
6941
|
+
if (!backend)
|
|
6942
|
+
return false;
|
|
6943
|
+
if (backend.readInputRow)
|
|
6944
|
+
return backend.readInputRow(pane) !== null;
|
|
6945
|
+
// muse: its live input box — null when the bottom of the screen is not its layout (#829), e.g. a selection menu.
|
|
6946
|
+
if (backend.inputDraft)
|
|
6947
|
+
return backend.inputDraft(pane) !== null;
|
|
6948
|
+
if (backend.isDeliveryInputReadyPane)
|
|
6949
|
+
return backend.isDeliveryInputReadyPane(pane);
|
|
6950
|
+
const bottom = backend.getBottomReadyPattern?.();
|
|
6951
|
+
if (bottom) {
|
|
6952
|
+
bottom.lastIndex = 0;
|
|
6953
|
+
return bottom.test(pane);
|
|
6954
|
+
}
|
|
6955
|
+
// No reader of its own (opencode, grok, …): its ready chrome BELOW the prompt rows — a status bar or prompt hint drawn
|
|
6956
|
+
// under its input box (opencode's "ctrl+p commands"), which a prompt shown in place of the input has nothing of. Not
|
|
6957
|
+
// the whole pane (a reply quoting "ctrl+p commands" higher up), and never the prompt's own rows: a ready pattern that
|
|
6958
|
+
// is just a cursor glyph would otherwise read the menu's own "❯ 1." as the CLI's prompt (#1695 r2).
|
|
6959
|
+
const rows = pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd());
|
|
6960
|
+
const lastOwn = (shape.block.split("\n").at(-1) ?? "").trim();
|
|
6961
|
+
let end = -1;
|
|
6962
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
6963
|
+
if (rows[i].trim() === lastOwn) {
|
|
6964
|
+
end = i;
|
|
6965
|
+
break;
|
|
6966
|
+
}
|
|
6967
|
+
if (end < 0)
|
|
6968
|
+
return false;
|
|
6969
|
+
const below = rows.slice(end + 1).filter(r => r.trim() !== "").slice(0, COMPOSER_TAIL_ROWS).join("\n");
|
|
6970
|
+
const ready = backend.getReadyPattern();
|
|
6971
|
+
ready.lastIndex = 0;
|
|
6972
|
+
return below !== "" && ready.test(below);
|
|
6973
|
+
}
|
|
6974
|
+
/**
|
|
6975
|
+
* #1691: the prompt shape this pane shows, when nothing else explains it (no known dialog, login, idle composer, and
|
|
6976
|
+
* the CLI is not showing its busy indicator).
|
|
6977
|
+
*/
|
|
6978
|
+
unrecognisedPromptCandidate(pane) {
|
|
6979
|
+
const shape = unrecognisedPromptShape(pane);
|
|
6980
|
+
if (!shape)
|
|
6981
|
+
return null;
|
|
6982
|
+
const busy = this.backend?.getBusyPattern?.() ?? null;
|
|
6983
|
+
if (busy) {
|
|
6984
|
+
busy.lastIndex = 0;
|
|
6985
|
+
if (busy.test(pane))
|
|
6986
|
+
return null;
|
|
6987
|
+
}
|
|
6988
|
+
if ((this.backend?.getRuntimeDialogs?.() ?? []).some(dialog => Daemon.dialogMatches(dialog, pane)))
|
|
6989
|
+
return null;
|
|
6990
|
+
if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenActive?.(pane))
|
|
6991
|
+
return null;
|
|
6992
|
+
if (this.cliComposerShowing(pane, shape))
|
|
6993
|
+
return null;
|
|
6994
|
+
return shape;
|
|
6995
|
+
}
|
|
6996
|
+
/**
|
|
6997
|
+
* #1691, from each runtime scan: the same unexplained prompt block unchanged for UNRECOGNISED_PROMPT_STABLE_MS
|
|
6998
|
+
* becomes active (held, awaiting_input, reported once by trackDialogParked). Any other screen ends it. A turn keeps
|
|
6999
|
+
* changing the pane (spinner, timer, streamed text), so its clock never completes mid-turn.
|
|
7000
|
+
*/
|
|
7001
|
+
trackUnrecognisedPrompt(pane, mono, outputMoved) {
|
|
7002
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7003
|
+
return;
|
|
7004
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7005
|
+
const shape = this.unrecognisedPromptCandidate(pane);
|
|
7006
|
+
if (!shape) {
|
|
7007
|
+
this.unrecognisedPrompt = null;
|
|
7008
|
+
return;
|
|
7009
|
+
}
|
|
7010
|
+
const screen = Daemon.stableScreen(pane);
|
|
7011
|
+
const owner = this.interactionOwner();
|
|
7012
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7013
|
+
// Restart the minute on any change: another prompt, motion anywhere on the screen, output that arrived while this
|
|
7014
|
+
// capture was taken, or evidence from another launch (#1695 review).
|
|
7015
|
+
if (!current || current.block !== shape.block || current.screen !== screen || outputMoved) {
|
|
7016
|
+
this.unrecognisedPrompt = { block: shape.block, shape, since: mono, active: false, reportedElsewhere: false, screen, owner };
|
|
7017
|
+
return;
|
|
7018
|
+
}
|
|
7019
|
+
if (!current.active && mono - current.since >= UNRECOGNISED_PROMPT_STABLE_MS) {
|
|
7020
|
+
current.active = true;
|
|
7021
|
+
// A [y/n] the interactive-prompt detector knows was already told (interactive_prompt, after 10 s): held and
|
|
7022
|
+
// awaiting input from here on, but not reported a second time.
|
|
7023
|
+
current.reportedElsewhere = shape.kind === "yes-no" && this.interactivePromptDetector.match(pane) !== null;
|
|
7024
|
+
this.logger.warn({ shape: shape.kind, options: shape.options, question: shape.question, unchangedForMs: Math.round(mono - current.since) }, "CLI is waiting on a prompt AgEnD does not recognise — holding deliveries, reporting for a human (capture this pane for the per-version audit, ffb8104e)");
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
7027
|
+
/** #1691: the tracked evidence, if it still belongs to this launch (else it is dropped). */
|
|
7028
|
+
unrecognisedPromptCurrent() {
|
|
7029
|
+
const current = this.unrecognisedPrompt;
|
|
7030
|
+
if (current && (this.runtimeMonitorsFrozen || !sameInteractionOwner(current.owner, this.interactionOwner()))) {
|
|
7031
|
+
this.unrecognisedPrompt = null;
|
|
7032
|
+
return null;
|
|
7033
|
+
}
|
|
7034
|
+
return current;
|
|
7035
|
+
}
|
|
7036
|
+
/**
|
|
7037
|
+
* #1691: the catch-all's predicate — the tracked prompt is active and this pane still shows exactly it (any reader:
|
|
7038
|
+
* the scan, a probe, an interaction re-capture). Ending the evidence on a read without it is noteFreshPaneRead's.
|
|
7039
|
+
*/
|
|
7040
|
+
unrecognisedPromptActive(pane) {
|
|
7041
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7042
|
+
return !!current?.active && this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen;
|
|
7043
|
+
}
|
|
7044
|
+
/**
|
|
7045
|
+
* #1691: a fresh pane read outside the scan (a delivery probe, an accepted state re-capture), taken at `mono`. When it
|
|
7046
|
+
* no longer shows exactly the tracked prompt, the evidence ends — counting or active, so a return needs a fresh minute
|
|
7047
|
+
* (#1695 r2). When it does, it is the newest confirmation. Either way only a read newer than the newest read that
|
|
7048
|
+
* decided the evidence counts: an older one, finishing late, says nothing about now (#1695 r3, r4).
|
|
7049
|
+
*/
|
|
7050
|
+
noteFreshPaneRead(pane, mono) {
|
|
7051
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7052
|
+
return;
|
|
7053
|
+
// Advanced even with nothing tracked: a scan whose capture is older must not start evidence this read contradicts
|
|
7054
|
+
// (#1695 r5).
|
|
7055
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7056
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7057
|
+
if (!current)
|
|
7058
|
+
return;
|
|
7059
|
+
if (this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen)
|
|
7060
|
+
return;
|
|
7061
|
+
this.unrecognisedPrompt = null;
|
|
7062
|
+
}
|
|
7063
|
+
/** #1691: the screen as the stability clock compares it (trailing blanks are not motion). */
|
|
7064
|
+
static stableScreen(pane) {
|
|
7065
|
+
return pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd()).join("\n").trimEnd();
|
|
6763
7066
|
}
|
|
6764
7067
|
/** Passive input-unavailable phases relevant to the current spawn only. */
|
|
6765
7068
|
guardedInputTransients() {
|
|
@@ -6927,7 +7230,7 @@ export class Daemon extends EventEmitter {
|
|
|
6927
7230
|
return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
|
|
6928
7231
|
}
|
|
6929
7232
|
/** Refresh the separate stdin-blocked observation from one pane capture. */
|
|
6930
|
-
updateInputBlockedState(pane, dialogs = this.
|
|
7233
|
+
updateInputBlockedState(pane, dialogs = this.runtimeDialogs()) {
|
|
6931
7234
|
const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
|
|
6932
7235
|
const nextKey = active ? Daemon.dialogKey(active) : null;
|
|
6933
7236
|
if (nextKey !== this.inputBlockedDialogKey) {
|
|
@@ -7015,9 +7318,15 @@ export class Daemon extends EventEmitter {
|
|
|
7015
7318
|
async probeBlockingDialog() {
|
|
7016
7319
|
if (!this.tmux || !this.backend)
|
|
7017
7320
|
return { state: "clear" };
|
|
7321
|
+
// #1691: a probe is a fresh read — it may end unrecognised-prompt evidence even when no dialog is listed.
|
|
7322
|
+
const tracking = this.unrecognisedPrompt !== null;
|
|
7018
7323
|
const dialogs = this.deliveryBlockingDialogs();
|
|
7019
|
-
|
|
7324
|
+
// #1690: our own switch is being answered — nothing else types into the pane until it has settled.
|
|
7325
|
+
if (this.ownSwitch && dialogs.length > 0)
|
|
7326
|
+
return { state: "dialog", dialog: dialogs[0] };
|
|
7327
|
+
if (dialogs.length === 0 && !tracking)
|
|
7020
7328
|
return { state: "clear" };
|
|
7329
|
+
const probeMono = performance.now();
|
|
7021
7330
|
let pane;
|
|
7022
7331
|
try {
|
|
7023
7332
|
pane = await this.tmux.capturePane();
|
|
@@ -7026,6 +7335,7 @@ export class Daemon extends EventEmitter {
|
|
|
7026
7335
|
this.logger.debug({ err }, "capture-pane failed during the dialog probe — pane state unknown");
|
|
7027
7336
|
return { state: "unknown" };
|
|
7028
7337
|
}
|
|
7338
|
+
this.noteFreshPaneRead(pane, probeMono);
|
|
7029
7339
|
for (const dialog of dialogs) {
|
|
7030
7340
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
7031
7341
|
continue;
|
|
@@ -7235,11 +7545,16 @@ export class Daemon extends EventEmitter {
|
|
|
7235
7545
|
this.dialogParkedReported = false;
|
|
7236
7546
|
return;
|
|
7237
7547
|
}
|
|
7238
|
-
|
|
7548
|
+
// #1691: the catch-all waited its minute before it became active; it is reported on the next scan.
|
|
7549
|
+
const unrecognised = dialog === this.unrecognisedPromptDialog;
|
|
7550
|
+
if (unrecognised && this.unrecognisedPrompt?.reportedElsewhere)
|
|
7551
|
+
this.dialogParkedReported = true;
|
|
7552
|
+
if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
|
|
7239
7553
|
this.dialogParkedReported = true;
|
|
7240
7554
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
7241
7555
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
7242
|
-
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {})
|
|
7556
|
+
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
7557
|
+
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
7243
7558
|
}
|
|
7244
7559
|
}
|
|
7245
7560
|
/**
|
|
@@ -10704,7 +11019,7 @@ export class Daemon extends EventEmitter {
|
|
|
10704
11019
|
if (launchFence !== this.launchFenceEpoch)
|
|
10705
11020
|
throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
|
|
10706
11021
|
this.backend.writeConfig(backendConfig);
|
|
10707
|
-
this.backend.preTrust?.(this.config.working_directory);
|
|
11022
|
+
this.backend.preTrust?.(this.config.working_directory, backendConfig);
|
|
10708
11023
|
// Resolve working directory (e.g. symlink for hidden paths)
|
|
10709
11024
|
const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
|
|
10710
11025
|
// Generate a fresh per-instance agent token each spawn. agent-cli reads
|
|
@@ -11344,6 +11659,14 @@ export class Daemon extends EventEmitter {
|
|
|
11344
11659
|
}, "Snapshot written");
|
|
11345
11660
|
return snapshot;
|
|
11346
11661
|
}
|
|
11662
|
+
/** #1699: the claude config dir this instance's CLI uses — its credential profile's, else exactly as before. */
|
|
11663
|
+
claudeConfigDir() {
|
|
11664
|
+
return claudeConfigDirFor(claudeProfileConfigDir(this.config.backend_options?.["claude-code"]));
|
|
11665
|
+
}
|
|
11666
|
+
/** Where this instance's CLI keeps the conversation a turn resume is judged on (#1699: claude's follows the profile). */
|
|
11667
|
+
resumeRoots(workingDirectory) {
|
|
11668
|
+
return { instanceDir: this.instanceDir, workingDirectory, claudeConfigDir: this.claudeConfigDir() };
|
|
11669
|
+
}
|
|
11347
11670
|
/**
|
|
11348
11671
|
* The credential profile's store this instance's CLI writes to, if any.
|
|
11349
11672
|
*
|