@hydra-acp/cli 0.1.29 → 0.1.31
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/README.md +14 -0
- package/dist/cli.js +425 -73
- package/dist/index.d.ts +5 -0
- package/dist/index.js +125 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,20 @@ hydra-acp --name "$BUFFER_NAME" launch claude-acp
|
|
|
259
259
|
|
|
260
260
|
After the first user prompt lands, hydra automatically replaces the label with the first line of that prompt (truncated, ≤80 chars) and emits a `session_info_update` so every attached client (TUI, slack, browser) refreshes its header. Agents that emit their own `session_info_update` override that — last write wins.
|
|
261
261
|
|
|
262
|
+
### Read-only viewer (`--readonly`)
|
|
263
|
+
|
|
264
|
+
Sometimes you want to scroll through a session's transcript — usually one imported from another machine — without spawning the underlying agent. Pass `--readonly` to `tui` to attach in view-only mode:
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
hydra-acp tui --resume <id> --readonly
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The daemon enforces the contract: a `readonly: true` attach to a *cold* session takes a viewer path that streams history straight from disk — no `manager.resurrect`, no agent process. Any mutating JSON-RPC method (`session/prompt`, `session/cancel`, `session/set_model`, and the `hydra-acp/*` prompt-mutation methods) sent from a read-only connection is refused with `-32011 PermissionDenied`. History replay and `session/update` deliveries are unchanged, so the existing scrollback search (`^R` when scrolled back) works over the full transcript.
|
|
271
|
+
|
|
272
|
+
The TUI suppresses the composer entirely — those rows go to scrollback so you see more of the conversation. The window title is suffixed `[VIEW ONLY]` so the mode is unambiguous. Prompt-shaped keys (Enter, Shift+Enter, Shift+Tab) are inert; `^P`, `^G`, `^L`, `^R`, `PgUp/PgDn`, `^C`, `^D` work as usual.
|
|
273
|
+
|
|
274
|
+
From inside the TUI's session picker, **`v`** on a selected row enters view-only mode for that session. Enter still attaches normally. The mode is per-session: `^P` → pick another with Enter drops out of read-only; `v` re-enters it.
|
|
275
|
+
|
|
262
276
|
### Slash commands (typed in any composer)
|
|
263
277
|
|
|
264
278
|
Slash commands of the form `/hydra <verb> [args]` are intercepted by hydra before the prompt reaches the agent. They never appear in the conversation log; the only client-visible signal is the notification(s) the verb implies.
|
package/dist/cli.js
CHANGED
|
@@ -584,6 +584,12 @@ var init_types = __esm({
|
|
|
584
584
|
name: z3.string(),
|
|
585
585
|
version: z3.string().optional()
|
|
586
586
|
}).optional(),
|
|
587
|
+
// When true, the connection observes the session but cannot mutate
|
|
588
|
+
// it: state-changing methods (session/prompt, session/cancel,
|
|
589
|
+
// session/set_model, etc.) are rejected with -32011, and attaching
|
|
590
|
+
// to a cold session does not resurrect or spawn an agent — just
|
|
591
|
+
// streams history from disk. Used by the TUI's view-only mode.
|
|
592
|
+
readonly: z3.boolean().optional(),
|
|
587
593
|
_meta: z3.record(z3.unknown()).optional()
|
|
588
594
|
});
|
|
589
595
|
HYDRA_META_KEY = "hydra-acp";
|
|
@@ -5413,6 +5419,15 @@ var init_discovery = __esm({
|
|
|
5413
5419
|
|
|
5414
5420
|
// src/tui/picker.ts
|
|
5415
5421
|
async function pickSession(term, opts) {
|
|
5422
|
+
process.stdout.write("\x1B[<u");
|
|
5423
|
+
process.stdout.write("\x1B[?2004l");
|
|
5424
|
+
process.stdout.write("\x1B[>4;0m");
|
|
5425
|
+
process.stdout.write("\x1B[>5;0m");
|
|
5426
|
+
process.stdout.write("\x1B[?1000l");
|
|
5427
|
+
process.stdout.write("\x1B[?1002l");
|
|
5428
|
+
process.stdout.write("\x1B[?1006l");
|
|
5429
|
+
process.stdout.write("\x1B[?1l");
|
|
5430
|
+
process.stdout.write("\x1B>");
|
|
5416
5431
|
if (opts.sessions.length === 0) {
|
|
5417
5432
|
return { kind: "new" };
|
|
5418
5433
|
}
|
|
@@ -5947,6 +5962,23 @@ async function pickSession(term, opts) {
|
|
|
5947
5962
|
void refresh(currentId);
|
|
5948
5963
|
return;
|
|
5949
5964
|
}
|
|
5965
|
+
if ((name === "v" || name === "V") && selectedIdx > 0) {
|
|
5966
|
+
const session = visible[selectedIdx - 1];
|
|
5967
|
+
if (!session) {
|
|
5968
|
+
return;
|
|
5969
|
+
}
|
|
5970
|
+
cleanup();
|
|
5971
|
+
const result = {
|
|
5972
|
+
kind: "attach",
|
|
5973
|
+
sessionId: session.sessionId,
|
|
5974
|
+
readonly: true
|
|
5975
|
+
};
|
|
5976
|
+
if (session.agentId !== void 0) {
|
|
5977
|
+
result.agentId = session.agentId;
|
|
5978
|
+
}
|
|
5979
|
+
resolve5(result);
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5950
5982
|
if ((name === "k" || name === "K") && selectedIdx > 0) {
|
|
5951
5983
|
const session = visible[selectedIdx - 1];
|
|
5952
5984
|
if (!session) {
|
|
@@ -6134,6 +6166,7 @@ var init_picker = __esm({
|
|
|
6134
6166
|
["PgUp / PgDn", "page up / page down"],
|
|
6135
6167
|
["Home / End", "first / last"],
|
|
6136
6168
|
["Enter", "open selected session (or create new)"],
|
|
6169
|
+
["v", "view-only (open transcript without spawning the agent)"],
|
|
6137
6170
|
null,
|
|
6138
6171
|
["/", "search sessions"],
|
|
6139
6172
|
["o", "toggle cwd-only filter"],
|
|
@@ -6807,6 +6840,110 @@ function mapKeyName(name) {
|
|
|
6807
6840
|
return null;
|
|
6808
6841
|
}
|
|
6809
6842
|
}
|
|
6843
|
+
function emergencyTerminalReset() {
|
|
6844
|
+
const seq = [
|
|
6845
|
+
"\x1B[?1000l",
|
|
6846
|
+
// mouse button reporting off
|
|
6847
|
+
"\x1B[?1002l",
|
|
6848
|
+
// mouse drag reporting off
|
|
6849
|
+
"\x1B[?1003l",
|
|
6850
|
+
// mouse any-motion reporting off
|
|
6851
|
+
"\x1B[?1006l",
|
|
6852
|
+
// SGR mouse mode off
|
|
6853
|
+
"\x1B[?1015l",
|
|
6854
|
+
// urxvt mouse mode off
|
|
6855
|
+
"\x1B[?2004l",
|
|
6856
|
+
// bracketed paste off
|
|
6857
|
+
"\x1B[>4;0m",
|
|
6858
|
+
// xterm modifyOtherKeys off
|
|
6859
|
+
"\x1B[>5;0m",
|
|
6860
|
+
// xterm formatOtherKeys off
|
|
6861
|
+
"\x1B[<u",
|
|
6862
|
+
// pop kitty keyboard stack
|
|
6863
|
+
"\x1B[?1l",
|
|
6864
|
+
// DECCKM off: arrows send CSI A/B/C/D not SS3 O A/B/C/D
|
|
6865
|
+
"\x1B>",
|
|
6866
|
+
// DECPAM off: numeric keypad mode
|
|
6867
|
+
"\x1B[?7h",
|
|
6868
|
+
// auto-wrap on
|
|
6869
|
+
"\x1B[?25h",
|
|
6870
|
+
// show cursor
|
|
6871
|
+
"\x1B]9;4;0\x07",
|
|
6872
|
+
// clear OSC 9;4 progress indicator
|
|
6873
|
+
"\x1B[?1049l"
|
|
6874
|
+
// leave alternate screen
|
|
6875
|
+
].join("");
|
|
6876
|
+
try {
|
|
6877
|
+
process.stdout.write(seq);
|
|
6878
|
+
} catch {
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
function mapCsiUToKeyName(code, mod) {
|
|
6882
|
+
const CTRL_LETTERS = {
|
|
6883
|
+
97: "ctrl-a",
|
|
6884
|
+
98: "ctrl-b",
|
|
6885
|
+
99: "ctrl-c",
|
|
6886
|
+
100: "ctrl-d",
|
|
6887
|
+
101: "ctrl-e",
|
|
6888
|
+
102: "ctrl-f",
|
|
6889
|
+
103: "ctrl-g",
|
|
6890
|
+
107: "ctrl-k",
|
|
6891
|
+
108: "ctrl-l",
|
|
6892
|
+
110: "ctrl-n",
|
|
6893
|
+
111: "ctrl-o",
|
|
6894
|
+
112: "ctrl-p",
|
|
6895
|
+
114: "ctrl-r",
|
|
6896
|
+
115: "ctrl-s",
|
|
6897
|
+
116: "ctrl-t",
|
|
6898
|
+
117: "ctrl-u",
|
|
6899
|
+
118: "ctrl-v",
|
|
6900
|
+
119: "ctrl-w",
|
|
6901
|
+
121: "ctrl-y"
|
|
6902
|
+
};
|
|
6903
|
+
if (mod === 5) {
|
|
6904
|
+
return CTRL_LETTERS[code] ?? null;
|
|
6905
|
+
}
|
|
6906
|
+
if (code === 27) {
|
|
6907
|
+
return "escape";
|
|
6908
|
+
}
|
|
6909
|
+
if (code === 9) {
|
|
6910
|
+
if (mod === 2) {
|
|
6911
|
+
return "shift-tab";
|
|
6912
|
+
}
|
|
6913
|
+
if (mod === 1) {
|
|
6914
|
+
return "tab";
|
|
6915
|
+
}
|
|
6916
|
+
return null;
|
|
6917
|
+
}
|
|
6918
|
+
if (code === 13) {
|
|
6919
|
+
if (mod === 2) {
|
|
6920
|
+
return "shift-enter";
|
|
6921
|
+
}
|
|
6922
|
+
if (mod === 3) {
|
|
6923
|
+
return "alt-enter";
|
|
6924
|
+
}
|
|
6925
|
+
if (mod === 5) {
|
|
6926
|
+
return "ctrl-enter";
|
|
6927
|
+
}
|
|
6928
|
+
if (mod === 1) {
|
|
6929
|
+
return "enter";
|
|
6930
|
+
}
|
|
6931
|
+
return null;
|
|
6932
|
+
}
|
|
6933
|
+
if (code === 127 && mod === 1) {
|
|
6934
|
+
return "backspace";
|
|
6935
|
+
}
|
|
6936
|
+
if (mod === 3) {
|
|
6937
|
+
if (code === 98 || code === 66) {
|
|
6938
|
+
return "alt-b";
|
|
6939
|
+
}
|
|
6940
|
+
if (code === 102 || code === 70) {
|
|
6941
|
+
return "alt-f";
|
|
6942
|
+
}
|
|
6943
|
+
return null;
|
|
6944
|
+
}
|
|
6945
|
+
return null;
|
|
6946
|
+
}
|
|
6810
6947
|
var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_HELP_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
|
|
6811
6948
|
var init_screen = __esm({
|
|
6812
6949
|
"src/tui/screen.ts"() {
|
|
@@ -6935,11 +7072,25 @@ var init_screen = __esm({
|
|
|
6935
7072
|
rawStdinHandler;
|
|
6936
7073
|
mouseEnabled;
|
|
6937
7074
|
progressIndicatorEnabled;
|
|
7075
|
+
// Listeners registered on process via installEmergencyCleanup so an
|
|
7076
|
+
// ungraceful exit (SIGTERM, SIGHUP, uncaughtException) still restores
|
|
7077
|
+
// mouse capture / alt-screen / kitty stack / cursor visibility — the
|
|
7078
|
+
// graceful stop() path isn't guaranteed to run in those cases and
|
|
7079
|
+
// would otherwise leave the host terminal wedged.
|
|
7080
|
+
emergencyCleanupInstalled = false;
|
|
7081
|
+
onProcessExit = null;
|
|
7082
|
+
onProcessSignal = null;
|
|
7083
|
+
onProcessUncaught = null;
|
|
6938
7084
|
// Last OSC 9;4 state we wrote (3 = indeterminate, 0 = remove). Used to
|
|
6939
7085
|
// suppress redundant writes when setBanner runs but `status` didn't
|
|
6940
7086
|
// actually change, and to re-emit on start() if a picker round-trip
|
|
6941
7087
|
// cleared the host terminal's indicator.
|
|
6942
7088
|
lastProgressState = 0;
|
|
7089
|
+
// View-only mode. Set once at construction. When true, promptRows()
|
|
7090
|
+
// returns 0 (composer collapses, scrollback expands), drawPrompt()
|
|
7091
|
+
// bails before computing layout, and syncWindowTitle() appends
|
|
7092
|
+
// "[VIEW ONLY]" so the chrome makes the mode obvious.
|
|
7093
|
+
readonly;
|
|
6943
7094
|
constructor(opts) {
|
|
6944
7095
|
this.term = opts.term;
|
|
6945
7096
|
this.dispatcher = opts.dispatcher;
|
|
@@ -6948,6 +7099,7 @@ var init_screen = __esm({
|
|
|
6948
7099
|
this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
|
|
6949
7100
|
this.mouseEnabled = opts.mouse ?? true;
|
|
6950
7101
|
this.progressIndicatorEnabled = opts.progressIndicator ?? true;
|
|
7102
|
+
this.readonly = opts.readonly ?? false;
|
|
6951
7103
|
this.resizeHandler = () => this.repaint();
|
|
6952
7104
|
this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
|
|
6953
7105
|
this.mouseHandler = (name) => this.handleMouse(name);
|
|
@@ -6976,6 +7128,7 @@ var init_screen = __esm({
|
|
|
6976
7128
|
}
|
|
6977
7129
|
this.term.on("resize", this.resizeHandler);
|
|
6978
7130
|
this.installBracketedPaste();
|
|
7131
|
+
this.installEmergencyCleanup();
|
|
6979
7132
|
this.lastProgressState = 0;
|
|
6980
7133
|
this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
|
|
6981
7134
|
this.repaint();
|
|
@@ -6993,6 +7146,7 @@ var init_screen = __esm({
|
|
|
6993
7146
|
this.throttledRepaintTimer = null;
|
|
6994
7147
|
}
|
|
6995
7148
|
this.uninstallBracketedPaste();
|
|
7149
|
+
this.uninstallEmergencyCleanup();
|
|
6996
7150
|
this.term.off("key", this.keyHandler);
|
|
6997
7151
|
if (this.mouseEnabled) {
|
|
6998
7152
|
this.term.off("mouse", this.mouseHandler);
|
|
@@ -7028,6 +7182,8 @@ var init_screen = __esm({
|
|
|
7028
7182
|
process.stdout.write("\x1B[>4;0m");
|
|
7029
7183
|
process.stdout.write("\x1B[>5;0m");
|
|
7030
7184
|
process.stdout.write("\x1B[<u");
|
|
7185
|
+
process.stdout.write("\x1B[?1l");
|
|
7186
|
+
process.stdout.write("\x1B>");
|
|
7031
7187
|
const t = this.term;
|
|
7032
7188
|
if (!t.stdin || this.terminalKitStdinHandler === null) {
|
|
7033
7189
|
return;
|
|
@@ -7038,72 +7194,109 @@ var init_screen = __esm({
|
|
|
7038
7194
|
this.pasteActive = false;
|
|
7039
7195
|
this.pasteBuffer = "";
|
|
7040
7196
|
}
|
|
7197
|
+
installEmergencyCleanup() {
|
|
7198
|
+
if (this.emergencyCleanupInstalled) {
|
|
7199
|
+
return;
|
|
7200
|
+
}
|
|
7201
|
+
this.emergencyCleanupInstalled = true;
|
|
7202
|
+
this.onProcessExit = () => emergencyTerminalReset();
|
|
7203
|
+
this.onProcessSignal = (sig) => {
|
|
7204
|
+
emergencyTerminalReset();
|
|
7205
|
+
process.off(sig, this.onProcessSignal);
|
|
7206
|
+
process.kill(process.pid, sig);
|
|
7207
|
+
};
|
|
7208
|
+
this.onProcessUncaught = (err) => {
|
|
7209
|
+
emergencyTerminalReset();
|
|
7210
|
+
process.stderr.write(`
|
|
7211
|
+
uncaught: ${err.stack ?? err.message}
|
|
7212
|
+
`);
|
|
7213
|
+
process.exit(1);
|
|
7214
|
+
};
|
|
7215
|
+
process.on("exit", this.onProcessExit);
|
|
7216
|
+
process.on("SIGTERM", this.onProcessSignal);
|
|
7217
|
+
process.on("SIGHUP", this.onProcessSignal);
|
|
7218
|
+
process.on("uncaughtException", this.onProcessUncaught);
|
|
7219
|
+
}
|
|
7220
|
+
uninstallEmergencyCleanup() {
|
|
7221
|
+
if (!this.emergencyCleanupInstalled) {
|
|
7222
|
+
return;
|
|
7223
|
+
}
|
|
7224
|
+
this.emergencyCleanupInstalled = false;
|
|
7225
|
+
if (this.onProcessExit) {
|
|
7226
|
+
process.off("exit", this.onProcessExit);
|
|
7227
|
+
this.onProcessExit = null;
|
|
7228
|
+
}
|
|
7229
|
+
if (this.onProcessSignal) {
|
|
7230
|
+
process.off("SIGTERM", this.onProcessSignal);
|
|
7231
|
+
process.off("SIGHUP", this.onProcessSignal);
|
|
7232
|
+
this.onProcessSignal = null;
|
|
7233
|
+
}
|
|
7234
|
+
if (this.onProcessUncaught) {
|
|
7235
|
+
process.off("uncaughtException", this.onProcessUncaught);
|
|
7236
|
+
this.onProcessUncaught = null;
|
|
7237
|
+
}
|
|
7238
|
+
}
|
|
7041
7239
|
handleRawStdin(chunk) {
|
|
7042
|
-
|
|
7043
|
-
if (
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
if (i < parts.length - 1) {
|
|
7062
|
-
this.onKey([{ type: "key", name }]);
|
|
7063
|
-
}
|
|
7240
|
+
const text = chunk.toString("binary");
|
|
7241
|
+
if (this.pasteActive) {
|
|
7242
|
+
this.handleRawStdinSegment(text);
|
|
7243
|
+
return;
|
|
7244
|
+
}
|
|
7245
|
+
const legacyMarkers = [
|
|
7246
|
+
{ seq: "\x1B[27;2;13~", name: "shift-enter" },
|
|
7247
|
+
{ seq: "\x1B[27;5;13~", name: "ctrl-enter" },
|
|
7248
|
+
// Bare LF — universal fallback for terminals without
|
|
7249
|
+
// modifyOtherKeys / kitty protocol. Last so the longer escape
|
|
7250
|
+
// sequences match first and we don't double-fire.
|
|
7251
|
+
{ seq: "\n", name: "ctrl-enter" }
|
|
7252
|
+
];
|
|
7253
|
+
for (const { seq, name } of legacyMarkers) {
|
|
7254
|
+
if (text.includes(seq)) {
|
|
7255
|
+
const parts = text.split(seq);
|
|
7256
|
+
for (let i = 0; i < parts.length; i++) {
|
|
7257
|
+
if (parts[i].length > 0) {
|
|
7258
|
+
this.handleRawStdin(Buffer.from(parts[i], "binary"));
|
|
7064
7259
|
}
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
}
|
|
7068
|
-
const csiUCtrlMap = {
|
|
7069
|
-
97: "ctrl-a",
|
|
7070
|
-
98: "ctrl-b",
|
|
7071
|
-
99: "ctrl-c",
|
|
7072
|
-
100: "ctrl-d",
|
|
7073
|
-
101: "ctrl-e",
|
|
7074
|
-
102: "ctrl-f",
|
|
7075
|
-
103: "ctrl-g",
|
|
7076
|
-
107: "ctrl-k",
|
|
7077
|
-
108: "ctrl-l",
|
|
7078
|
-
110: "ctrl-n",
|
|
7079
|
-
111: "ctrl-o",
|
|
7080
|
-
112: "ctrl-p",
|
|
7081
|
-
114: "ctrl-r",
|
|
7082
|
-
115: "ctrl-s",
|
|
7083
|
-
116: "ctrl-t",
|
|
7084
|
-
117: "ctrl-u",
|
|
7085
|
-
118: "ctrl-v",
|
|
7086
|
-
119: "ctrl-w",
|
|
7087
|
-
121: "ctrl-y"
|
|
7088
|
-
};
|
|
7089
|
-
const csiUCtrlRe = /\x1b\[(\d+);5u/;
|
|
7090
|
-
const m = csiUCtrlRe.exec(text);
|
|
7091
|
-
if (m !== null) {
|
|
7092
|
-
const keyName = csiUCtrlMap[parseInt(m[1], 10)];
|
|
7093
|
-
if (keyName !== void 0) {
|
|
7094
|
-
const parts = text.split(m[0]);
|
|
7095
|
-
for (let i = 0; i < parts.length; i++) {
|
|
7096
|
-
if (parts[i].length > 0)
|
|
7097
|
-
this.handleRawStdin(Buffer.from(parts[i], "binary"));
|
|
7098
|
-
if (i < parts.length - 1)
|
|
7099
|
-
this.onKey([{ type: "key", name: keyName }]);
|
|
7260
|
+
if (i < parts.length - 1) {
|
|
7261
|
+
this.onKey([{ type: "key", name }]);
|
|
7100
7262
|
}
|
|
7101
|
-
return;
|
|
7102
7263
|
}
|
|
7264
|
+
return;
|
|
7103
7265
|
}
|
|
7104
7266
|
}
|
|
7267
|
+
if (text.includes("\x1B[") && /\x1b\[\d+(?:;\d+)?u/.test(text)) {
|
|
7268
|
+
this.handleCsiUStdin(text);
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
7105
7271
|
this.handleRawStdinSegment(text);
|
|
7106
7272
|
}
|
|
7273
|
+
// Walk `text` extracting every kitty CSI-u sequence. Each non-CSI-u
|
|
7274
|
+
// span is recursed back into handleRawStdin so paste markers and
|
|
7275
|
+
// legacy-modifyOtherKeys sequences in the same chunk still get
|
|
7276
|
+
// handled; each matched CSI-u is mapped to a KeyEvent (or dropped if
|
|
7277
|
+
// unmapped). Caller has already verified at least one match exists.
|
|
7278
|
+
handleCsiUStdin(text) {
|
|
7279
|
+
const csiU = /\x1b\[(\d+)(?:;(\d+))?u/g;
|
|
7280
|
+
let lastEnd = 0;
|
|
7281
|
+
let m;
|
|
7282
|
+
while ((m = csiU.exec(text)) !== null) {
|
|
7283
|
+
if (m.index > lastEnd) {
|
|
7284
|
+
this.handleRawStdin(
|
|
7285
|
+
Buffer.from(text.slice(lastEnd, m.index), "binary")
|
|
7286
|
+
);
|
|
7287
|
+
}
|
|
7288
|
+
const code = parseInt(m[1], 10);
|
|
7289
|
+
const mod = m[2] !== void 0 ? parseInt(m[2], 10) : 1;
|
|
7290
|
+
const name = mapCsiUToKeyName(code, mod);
|
|
7291
|
+
if (name !== null) {
|
|
7292
|
+
this.onKey([{ type: "key", name }]);
|
|
7293
|
+
}
|
|
7294
|
+
lastEnd = m.index + m[0].length;
|
|
7295
|
+
}
|
|
7296
|
+
if (lastEnd < text.length) {
|
|
7297
|
+
this.handleRawStdin(Buffer.from(text.slice(lastEnd), "binary"));
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7107
7300
|
// Inner stdin-segment handler — paste-marker detection and forwarding
|
|
7108
7301
|
// to terminal-kit. Split out so shift-enter interception can call it
|
|
7109
7302
|
// for the non-shift-enter portions of a mixed chunk.
|
|
@@ -7360,7 +7553,8 @@ var init_screen = __esm({
|
|
|
7360
7553
|
const title = this.sessionbar.title?.trim();
|
|
7361
7554
|
const fallback = shortId(this.sessionbar.sessionId) || "hydra";
|
|
7362
7555
|
const raw = title && title.length > 0 ? title : fallback;
|
|
7363
|
-
const
|
|
7556
|
+
const tagged = this.readonly ? `${raw} [VIEW ONLY]` : raw;
|
|
7557
|
+
const clean = tagged.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
7364
7558
|
if (clean === this.lastWindowTitle) {
|
|
7365
7559
|
return;
|
|
7366
7560
|
}
|
|
@@ -8243,6 +8437,9 @@ var init_screen = __esm({
|
|
|
8243
8437
|
this.drawHelpPrompt();
|
|
8244
8438
|
return;
|
|
8245
8439
|
}
|
|
8440
|
+
if (this.readonly) {
|
|
8441
|
+
return;
|
|
8442
|
+
}
|
|
8246
8443
|
const w = this.term.width;
|
|
8247
8444
|
const room = Math.max(1, w - 2);
|
|
8248
8445
|
const state = this.dispatcher.state();
|
|
@@ -8493,6 +8690,9 @@ var init_screen = __esm({
|
|
|
8493
8690
|
if (this.helpPrompt) {
|
|
8494
8691
|
return this.helpRows();
|
|
8495
8692
|
}
|
|
8693
|
+
if (this.readonly) {
|
|
8694
|
+
return 0;
|
|
8695
|
+
}
|
|
8496
8696
|
const w = this.term.width;
|
|
8497
8697
|
const room = Math.max(1, w - 2);
|
|
8498
8698
|
const state = this.dispatcher.state();
|
|
@@ -10248,6 +10448,19 @@ import { nanoid as nanoid3 } from "nanoid";
|
|
|
10248
10448
|
import termkit from "terminal-kit";
|
|
10249
10449
|
import fs19 from "fs/promises";
|
|
10250
10450
|
import path14 from "path";
|
|
10451
|
+
function isReadonlyForbiddenEffect(effect) {
|
|
10452
|
+
switch (effect.type) {
|
|
10453
|
+
case "send":
|
|
10454
|
+
case "amend":
|
|
10455
|
+
case "queue-edit":
|
|
10456
|
+
case "queue-remove":
|
|
10457
|
+
case "plan-toggle":
|
|
10458
|
+
case "attachment-request":
|
|
10459
|
+
return true;
|
|
10460
|
+
default:
|
|
10461
|
+
return false;
|
|
10462
|
+
}
|
|
10463
|
+
}
|
|
10251
10464
|
async function runTuiApp(opts) {
|
|
10252
10465
|
const config = await loadConfig();
|
|
10253
10466
|
const serviceToken = await ensureServiceToken();
|
|
@@ -10266,8 +10479,11 @@ async function runTuiApp(opts) {
|
|
|
10266
10479
|
}
|
|
10267
10480
|
if (exitHint.sessionId) {
|
|
10268
10481
|
const short = stripHydraSessionPrefix(exitHint.sessionId);
|
|
10269
|
-
|
|
10270
|
-
|
|
10482
|
+
const flags = exitHint.readonly ? " --readonly" : "";
|
|
10483
|
+
process.stdout.write(
|
|
10484
|
+
`To resume: hydra-acp tui --resume ${short}${flags}
|
|
10485
|
+
`
|
|
10486
|
+
);
|
|
10271
10487
|
}
|
|
10272
10488
|
}
|
|
10273
10489
|
async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
@@ -10695,6 +10911,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10695
10911
|
ownClientId = created.clientId;
|
|
10696
10912
|
}
|
|
10697
10913
|
exitHint.sessionId = resolvedSessionId;
|
|
10914
|
+
exitHint.readonly = false;
|
|
10698
10915
|
const hydraMeta = extractHydraMeta(created._meta ?? void 0);
|
|
10699
10916
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
10700
10917
|
if (hydraMeta.agentId) {
|
|
@@ -10721,13 +10938,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10721
10938
|
const attached = await conn.request("session/attach", {
|
|
10722
10939
|
sessionId: ctx.sessionId,
|
|
10723
10940
|
historyPolicy: "full",
|
|
10724
|
-
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
|
|
10941
|
+
clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
|
|
10942
|
+
...opts.readonly === true ? { readonly: true } : {}
|
|
10725
10943
|
});
|
|
10726
10944
|
resolvedSessionId = attached.sessionId;
|
|
10727
10945
|
if (attached.clientId) {
|
|
10728
10946
|
ownClientId = attached.clientId;
|
|
10729
10947
|
}
|
|
10730
10948
|
exitHint.sessionId = resolvedSessionId;
|
|
10949
|
+
exitHint.readonly = opts.readonly === true;
|
|
10731
10950
|
const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
|
|
10732
10951
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
10733
10952
|
if (hydraMeta.agentId) {
|
|
@@ -10767,6 +10986,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10767
10986
|
maxScrollbackLines: config.tui.maxScrollbackLines,
|
|
10768
10987
|
mouse: config.tui.mouse,
|
|
10769
10988
|
progressIndicator: config.tui.progressIndicator,
|
|
10989
|
+
readonly: opts.readonly === true,
|
|
10770
10990
|
onKey: (events) => {
|
|
10771
10991
|
for (const ev of events) {
|
|
10772
10992
|
if (pendingPermission && tryHandlePermissionKey(ev)) {
|
|
@@ -10790,6 +11010,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10790
11010
|
}
|
|
10791
11011
|
const effects = dispatcher.feed(ev);
|
|
10792
11012
|
for (const effect of effects) {
|
|
11013
|
+
if (opts.readonly === true && isReadonlyForbiddenEffect(effect)) {
|
|
11014
|
+
continue;
|
|
11015
|
+
}
|
|
10793
11016
|
handleEffect(effect);
|
|
10794
11017
|
}
|
|
10795
11018
|
}
|
|
@@ -11152,13 +11375,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11152
11375
|
if (choice.kind === "new") {
|
|
11153
11376
|
const { sessionId: _drop, ...rest } = opts;
|
|
11154
11377
|
void _drop;
|
|
11155
|
-
resume({ ...rest, cwd: resolvedCwd, forceNew: true });
|
|
11378
|
+
resume({ ...rest, cwd: resolvedCwd, forceNew: true, readonly: false });
|
|
11156
11379
|
return;
|
|
11157
11380
|
}
|
|
11158
11381
|
const nextOpts = {
|
|
11159
11382
|
...opts,
|
|
11160
11383
|
sessionId: choice.sessionId,
|
|
11161
|
-
cwd: resolvedCwd
|
|
11384
|
+
cwd: resolvedCwd,
|
|
11385
|
+
readonly: choice.readonly === true
|
|
11162
11386
|
};
|
|
11163
11387
|
if (choice.agentId !== void 0) {
|
|
11164
11388
|
nextOpts.agentId = choice.agentId;
|
|
@@ -11178,7 +11402,12 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11178
11402
|
finishSession = null;
|
|
11179
11403
|
process.off("SIGINT", sigintHandler);
|
|
11180
11404
|
void stream.close().catch(() => void 0);
|
|
11181
|
-
const nextOpts = {
|
|
11405
|
+
const nextOpts = {
|
|
11406
|
+
...opts,
|
|
11407
|
+
sessionId: next.sessionId,
|
|
11408
|
+
cwd: resolvedCwd,
|
|
11409
|
+
readonly: false
|
|
11410
|
+
};
|
|
11182
11411
|
if (next.agentId !== void 0)
|
|
11183
11412
|
nextOpts.agentId = next.agentId;
|
|
11184
11413
|
resume(nextOpts);
|
|
@@ -12235,6 +12464,7 @@ async function resolveSession(term, config, serviceToken, opts) {
|
|
|
12235
12464
|
if (choice.kind === "new") {
|
|
12236
12465
|
return newCtx(opts, cwd, config);
|
|
12237
12466
|
}
|
|
12467
|
+
opts.readonly = choice.readonly === true;
|
|
12238
12468
|
return {
|
|
12239
12469
|
sessionId: choice.sessionId,
|
|
12240
12470
|
agentId: choice.agentId ?? "",
|
|
@@ -12454,6 +12684,7 @@ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
12454
12684
|
"json",
|
|
12455
12685
|
"new",
|
|
12456
12686
|
"reattach",
|
|
12687
|
+
"readonly",
|
|
12457
12688
|
"replace",
|
|
12458
12689
|
"rotate-token",
|
|
12459
12690
|
"version"
|
|
@@ -13998,6 +14229,13 @@ var SessionManager = class {
|
|
|
13998
14229
|
}
|
|
13999
14230
|
return this.histories.load(sessionId).catch(() => []);
|
|
14000
14231
|
}
|
|
14232
|
+
// Read the on-disk history.jsonl for a session without constructing a
|
|
14233
|
+
// Session instance. Used by the daemon's read-only viewer attach path
|
|
14234
|
+
// (cli/src/daemon/acp-ws.ts) to stream replay events to a client for
|
|
14235
|
+
// a cold session without spawning an agent.
|
|
14236
|
+
async loadHistory(sessionId) {
|
|
14237
|
+
return this.histories.load(sessionId);
|
|
14238
|
+
}
|
|
14001
14239
|
async loadFromDisk(sessionId) {
|
|
14002
14240
|
const record = await this.store.read(sessionId);
|
|
14003
14241
|
if (!record) {
|
|
@@ -14224,6 +14462,10 @@ var SessionManager = class {
|
|
|
14224
14462
|
args.sessionId,
|
|
14225
14463
|
args.bundle.history
|
|
14226
14464
|
);
|
|
14465
|
+
const sourceMtime = new Date(args.bundle.session.updatedAt);
|
|
14466
|
+
if (!Number.isNaN(sourceMtime.getTime())) {
|
|
14467
|
+
await fs11.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
|
|
14468
|
+
}
|
|
14227
14469
|
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
14228
14470
|
await saveHistory(
|
|
14229
14471
|
paths.tuiHistoryFile(args.sessionId),
|
|
@@ -14247,7 +14489,9 @@ var SessionManager = class {
|
|
|
14247
14489
|
currentUsage: args.bundle.session.currentUsage,
|
|
14248
14490
|
agentCommands: args.bundle.session.agentCommands,
|
|
14249
14491
|
createdAt: args.preservedCreatedAt ?? now,
|
|
14250
|
-
|
|
14492
|
+
// Fallback path for historyMtimeIso (used when the history file
|
|
14493
|
+
// is missing). Keep this consistent with the utimes stamp above.
|
|
14494
|
+
updatedAt: args.bundle.session.updatedAt
|
|
14251
14495
|
});
|
|
14252
14496
|
});
|
|
14253
14497
|
}
|
|
@@ -15170,6 +15414,9 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
15170
15414
|
if (activeVersions.has(version)) {
|
|
15171
15415
|
continue;
|
|
15172
15416
|
}
|
|
15417
|
+
if (version.includes(".partial-")) {
|
|
15418
|
+
continue;
|
|
15419
|
+
}
|
|
15173
15420
|
const versionDir = path8.join(agentDir, version);
|
|
15174
15421
|
try {
|
|
15175
15422
|
await fsp4.rm(versionDir, { recursive: true, force: true });
|
|
@@ -16094,6 +16341,16 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16094
16341
|
}
|
|
16095
16342
|
state.attached.clear();
|
|
16096
16343
|
});
|
|
16344
|
+
const denyIfReadonly = (sessionId, method) => {
|
|
16345
|
+
const att = state.attached.get(sessionId);
|
|
16346
|
+
if (att?.readonly) {
|
|
16347
|
+
const err = new Error(
|
|
16348
|
+
`${method} not permitted on a read-only attachment`
|
|
16349
|
+
);
|
|
16350
|
+
err.code = JsonRpcErrorCodes.PermissionDenied;
|
|
16351
|
+
throw err;
|
|
16352
|
+
}
|
|
16353
|
+
};
|
|
16097
16354
|
connection.onRequest("initialize", async (raw) => {
|
|
16098
16355
|
InitializeParams.parse(raw ?? {});
|
|
16099
16356
|
return buildInitializeResult();
|
|
@@ -16120,7 +16377,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16120
16377
|
const { entries: replay } = await session.attach(client, "full");
|
|
16121
16378
|
state.attached.set(session.sessionId, {
|
|
16122
16379
|
sessionId: session.sessionId,
|
|
16123
|
-
clientId: client.clientId
|
|
16380
|
+
clientId: client.clientId,
|
|
16381
|
+
readonly: false
|
|
16124
16382
|
});
|
|
16125
16383
|
setImmediate(() => {
|
|
16126
16384
|
void (async () => {
|
|
@@ -16146,11 +16404,46 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16146
16404
|
connection.onRequest("session/attach", async (raw) => {
|
|
16147
16405
|
const params = SessionAttachParams.parse(raw);
|
|
16148
16406
|
const hydraHints = extractHydraMeta(params._meta).resume;
|
|
16407
|
+
const readonly = params.readonly === true;
|
|
16149
16408
|
app.log.info(
|
|
16150
|
-
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints}`
|
|
16409
|
+
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints} readonly=${readonly}`
|
|
16151
16410
|
);
|
|
16152
16411
|
const lookupId = hydraHints ? params.sessionId : await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
16153
16412
|
let session = deps.manager.get(lookupId);
|
|
16413
|
+
if (!session && readonly) {
|
|
16414
|
+
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
16415
|
+
if (!fromDisk) {
|
|
16416
|
+
const err = new Error(
|
|
16417
|
+
`session ${params.sessionId} not found`
|
|
16418
|
+
);
|
|
16419
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
16420
|
+
throw err;
|
|
16421
|
+
}
|
|
16422
|
+
const history = await deps.manager.loadHistory(lookupId);
|
|
16423
|
+
const viewerClientId = params.clientId ?? `cli_${nanoid2(8)}`;
|
|
16424
|
+
state.attached.set(fromDisk.hydraSessionId, {
|
|
16425
|
+
sessionId: fromDisk.hydraSessionId,
|
|
16426
|
+
clientId: viewerClientId,
|
|
16427
|
+
readonly: true
|
|
16428
|
+
});
|
|
16429
|
+
app.log.info(
|
|
16430
|
+
`session/attach OK (viewer) sessionId=${fromDisk.hydraSessionId} clientId=${viewerClientId} attachedCount=${state.attached.size} replayed=${history.length}`
|
|
16431
|
+
);
|
|
16432
|
+
for (const entry of history) {
|
|
16433
|
+
await connection.notify(entry.method, entry.params).catch(() => void 0);
|
|
16434
|
+
}
|
|
16435
|
+
return {
|
|
16436
|
+
sessionId: fromDisk.hydraSessionId,
|
|
16437
|
+
clientId: viewerClientId,
|
|
16438
|
+
connectedClients: [viewerClientId],
|
|
16439
|
+
// No Session.attach() ran, so no history policy was applied —
|
|
16440
|
+
// the viewer always gets full history. Report "full" so the
|
|
16441
|
+
// wire shape matches the normal attach response.
|
|
16442
|
+
historyPolicy: "full",
|
|
16443
|
+
replayed: history.length,
|
|
16444
|
+
_meta: buildViewerResponseMeta(fromDisk)
|
|
16445
|
+
};
|
|
16446
|
+
}
|
|
16154
16447
|
if (!session) {
|
|
16155
16448
|
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
16156
16449
|
let resurrectParams = fromDisk;
|
|
@@ -16194,10 +16487,11 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16194
16487
|
);
|
|
16195
16488
|
state.attached.set(session.sessionId, {
|
|
16196
16489
|
sessionId: session.sessionId,
|
|
16197
|
-
clientId: client.clientId
|
|
16490
|
+
clientId: client.clientId,
|
|
16491
|
+
readonly
|
|
16198
16492
|
});
|
|
16199
16493
|
app.log.info(
|
|
16200
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length}`
|
|
16494
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}`
|
|
16201
16495
|
);
|
|
16202
16496
|
for (const note of replay) {
|
|
16203
16497
|
await connection.notify(note.method, note.params);
|
|
@@ -16241,6 +16535,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16241
16535
|
});
|
|
16242
16536
|
connection.onRequest("session/prompt", async (raw) => {
|
|
16243
16537
|
const params = SessionPromptParams.parse(raw);
|
|
16538
|
+
denyIfReadonly(params.sessionId, "session/prompt");
|
|
16244
16539
|
const att = state.attached.get(params.sessionId);
|
|
16245
16540
|
if (!att) {
|
|
16246
16541
|
app.log.warn(
|
|
@@ -16289,6 +16584,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16289
16584
|
if (!att) {
|
|
16290
16585
|
return;
|
|
16291
16586
|
}
|
|
16587
|
+
if (att.readonly) {
|
|
16588
|
+
app.log.warn(
|
|
16589
|
+
`session/cancel dropped (readonly attachment) sessionId=${params.sessionId}`
|
|
16590
|
+
);
|
|
16591
|
+
return;
|
|
16592
|
+
}
|
|
16292
16593
|
const session = deps.manager.get(params.sessionId);
|
|
16293
16594
|
if (!session) {
|
|
16294
16595
|
return;
|
|
@@ -16301,11 +16602,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16301
16602
|
};
|
|
16302
16603
|
connection.onNotification("session/cancel", handleCancelParams);
|
|
16303
16604
|
connection.onRequest("session/cancel", async (raw) => {
|
|
16605
|
+
const params = SessionCancelParams.parse(raw);
|
|
16606
|
+
denyIfReadonly(params.sessionId, "session/cancel");
|
|
16304
16607
|
handleCancelParams(raw);
|
|
16305
16608
|
return null;
|
|
16306
16609
|
});
|
|
16307
16610
|
connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
|
|
16308
16611
|
const params = CancelPromptParams.parse(raw);
|
|
16612
|
+
denyIfReadonly(params.sessionId, "hydra-acp/cancel_prompt");
|
|
16309
16613
|
const session = deps.manager.get(params.sessionId);
|
|
16310
16614
|
if (!session) {
|
|
16311
16615
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -16316,6 +16620,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16316
16620
|
});
|
|
16317
16621
|
connection.onRequest("hydra-acp/update_prompt", async (raw) => {
|
|
16318
16622
|
const params = UpdatePromptParams.parse(raw);
|
|
16623
|
+
denyIfReadonly(params.sessionId, "hydra-acp/update_prompt");
|
|
16319
16624
|
const session = deps.manager.get(params.sessionId);
|
|
16320
16625
|
if (!session) {
|
|
16321
16626
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -16326,6 +16631,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16326
16631
|
});
|
|
16327
16632
|
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
16328
16633
|
const params = AmendPromptParams.parse(raw);
|
|
16634
|
+
denyIfReadonly(params.sessionId, "hydra-acp/amend_prompt");
|
|
16329
16635
|
const att = state.attached.get(params.sessionId);
|
|
16330
16636
|
if (!att) {
|
|
16331
16637
|
const err = new Error("not attached to session");
|
|
@@ -16365,7 +16671,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16365
16671
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
16366
16672
|
state.attached.set(session.sessionId, {
|
|
16367
16673
|
sessionId: session.sessionId,
|
|
16368
|
-
clientId: client.clientId
|
|
16674
|
+
clientId: client.clientId,
|
|
16675
|
+
readonly: false
|
|
16369
16676
|
});
|
|
16370
16677
|
for (const note of replay) {
|
|
16371
16678
|
await connection.notify(note.method, note.params);
|
|
@@ -16384,6 +16691,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16384
16691
|
};
|
|
16385
16692
|
});
|
|
16386
16693
|
connection.onRequest("session/set_model", async (rawParams) => {
|
|
16694
|
+
const sessionIdField = rawParams?.sessionId;
|
|
16695
|
+
if (typeof sessionIdField === "string") {
|
|
16696
|
+
denyIfReadonly(sessionIdField, "session/set_model");
|
|
16697
|
+
}
|
|
16387
16698
|
const decision = decideSetModel(rawParams, deps.manager);
|
|
16388
16699
|
if (decision.kind === "error") {
|
|
16389
16700
|
app.log.warn(decision.logMessage);
|
|
@@ -16417,6 +16728,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
16417
16728
|
err.code = JsonRpcErrorCodes.MethodNotFound;
|
|
16418
16729
|
throw err;
|
|
16419
16730
|
}
|
|
16731
|
+
denyIfReadonly(sessionId, method);
|
|
16420
16732
|
const session = deps.manager.get(sessionId);
|
|
16421
16733
|
if (!session) {
|
|
16422
16734
|
const err = new Error(`session ${sessionId} not found`);
|
|
@@ -16555,6 +16867,38 @@ function decideSetModel(rawParams, manager) {
|
|
|
16555
16867
|
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16556
16868
|
};
|
|
16557
16869
|
}
|
|
16870
|
+
function buildViewerResponseMeta(fromDisk) {
|
|
16871
|
+
const ours = {
|
|
16872
|
+
upstreamSessionId: fromDisk.upstreamSessionId,
|
|
16873
|
+
agentId: fromDisk.agentId,
|
|
16874
|
+
cwd: fromDisk.cwd
|
|
16875
|
+
};
|
|
16876
|
+
if (fromDisk.title !== void 0) {
|
|
16877
|
+
ours.name = fromDisk.title;
|
|
16878
|
+
}
|
|
16879
|
+
if (fromDisk.agentArgs && fromDisk.agentArgs.length > 0) {
|
|
16880
|
+
ours.agentArgs = fromDisk.agentArgs;
|
|
16881
|
+
}
|
|
16882
|
+
if (fromDisk.currentModel !== void 0) {
|
|
16883
|
+
ours.currentModel = fromDisk.currentModel;
|
|
16884
|
+
}
|
|
16885
|
+
if (fromDisk.currentMode !== void 0) {
|
|
16886
|
+
ours.currentMode = fromDisk.currentMode;
|
|
16887
|
+
}
|
|
16888
|
+
if (fromDisk.currentUsage !== void 0) {
|
|
16889
|
+
ours.currentUsage = fromDisk.currentUsage;
|
|
16890
|
+
}
|
|
16891
|
+
if (fromDisk.agentCommands && fromDisk.agentCommands.length > 0) {
|
|
16892
|
+
ours.availableCommands = fromDisk.agentCommands;
|
|
16893
|
+
}
|
|
16894
|
+
if (fromDisk.agentModes && fromDisk.agentModes.length > 0) {
|
|
16895
|
+
ours.availableModes = fromDisk.agentModes;
|
|
16896
|
+
}
|
|
16897
|
+
if (fromDisk.agentModels && fromDisk.agentModels.length > 0) {
|
|
16898
|
+
ours.availableModels = fromDisk.agentModels;
|
|
16899
|
+
}
|
|
16900
|
+
return { [HYDRA_META_KEY]: ours };
|
|
16901
|
+
}
|
|
16558
16902
|
function buildResponseMeta(session) {
|
|
16559
16903
|
const ours = {
|
|
16560
16904
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -18536,8 +18880,15 @@ async function dispatchTui(flags, base) {
|
|
|
18536
18880
|
const cwd = resolveOption(flags, "cwd");
|
|
18537
18881
|
const resume = flags.reattach === true;
|
|
18538
18882
|
const forceNew = flags.new === true;
|
|
18883
|
+
const readonly = flags.readonly === true;
|
|
18884
|
+
if (readonly && base.sessionId === void 0) {
|
|
18885
|
+
process.stderr.write(
|
|
18886
|
+
"hydra-acp: --readonly requires a session id. Pass --resume <id> --readonly, or open the picker and press `v` on a session.\n"
|
|
18887
|
+
);
|
|
18888
|
+
process.exit(2);
|
|
18889
|
+
}
|
|
18539
18890
|
const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
|
|
18540
|
-
const tuiOpts = { resume, forceNew };
|
|
18891
|
+
const tuiOpts = { resume, forceNew, readonly };
|
|
18541
18892
|
if (base.sessionId !== void 0) {
|
|
18542
18893
|
tuiOpts.sessionId = base.sessionId;
|
|
18543
18894
|
}
|
|
@@ -18612,8 +18963,9 @@ function printHelp() {
|
|
|
18612
18963
|
" hydra-acp auth password [--force] Set the daemon's master password",
|
|
18613
18964
|
" hydra-acp auth [list] List active session tokens",
|
|
18614
18965
|
" hydra-acp auth revoke <id> Revoke a session token",
|
|
18615
|
-
" hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
18966
|
+
" hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--readonly] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
|
|
18616
18967
|
" --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
|
|
18968
|
+
" --readonly opens a session as a transcript viewer (no agent spawn, no prompting). Requires --resume.",
|
|
18617
18969
|
" Smart default (no flags): shows a picker when sessions exist, else new.",
|
|
18618
18970
|
" hydra-acp --version Print version",
|
|
18619
18971
|
" hydra-acp --help Show this help",
|
package/dist/index.d.ts
CHANGED
|
@@ -1320,10 +1320,12 @@ declare const SessionAttachParams: z.ZodObject<{
|
|
|
1320
1320
|
name: string;
|
|
1321
1321
|
version?: string | undefined;
|
|
1322
1322
|
}>>;
|
|
1323
|
+
readonly: z.ZodOptional<z.ZodBoolean>;
|
|
1323
1324
|
_meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1324
1325
|
}, "strip", z.ZodTypeAny, {
|
|
1325
1326
|
sessionId: string;
|
|
1326
1327
|
historyPolicy: "full" | "pending_only" | "none" | "after_message";
|
|
1328
|
+
readonly?: boolean | undefined;
|
|
1327
1329
|
clientInfo?: {
|
|
1328
1330
|
name: string;
|
|
1329
1331
|
version?: string | undefined;
|
|
@@ -1333,6 +1335,7 @@ declare const SessionAttachParams: z.ZodObject<{
|
|
|
1333
1335
|
_meta?: Record<string, unknown> | undefined;
|
|
1334
1336
|
}, {
|
|
1335
1337
|
sessionId: string;
|
|
1338
|
+
readonly?: boolean | undefined;
|
|
1336
1339
|
clientInfo?: {
|
|
1337
1340
|
name: string;
|
|
1338
1341
|
version?: string | undefined;
|
|
@@ -1779,6 +1782,7 @@ interface AttachedClient {
|
|
|
1779
1782
|
name: string;
|
|
1780
1783
|
version?: string;
|
|
1781
1784
|
};
|
|
1785
|
+
readonly?: boolean;
|
|
1782
1786
|
}
|
|
1783
1787
|
type CachedNotification = HistoryEntry;
|
|
1784
1788
|
interface SpawnReplacementAgentParams {
|
|
@@ -2400,6 +2404,7 @@ declare class SessionManager {
|
|
|
2400
2404
|
private bootstrapAgent;
|
|
2401
2405
|
private attachManagerHooks;
|
|
2402
2406
|
getHistory(sessionId: string): Promise<HistoryEntry[] | undefined>;
|
|
2407
|
+
loadHistory(sessionId: string): Promise<HistoryEntry[]>;
|
|
2403
2408
|
loadFromDisk(sessionId: string): Promise<ResurrectParams | undefined>;
|
|
2404
2409
|
private deriveTitleFromHistory;
|
|
2405
2410
|
get(sessionId: string): Session | undefined;
|
package/dist/index.js
CHANGED
|
@@ -1075,6 +1075,12 @@ var SessionAttachParams = z3.object({
|
|
|
1075
1075
|
name: z3.string(),
|
|
1076
1076
|
version: z3.string().optional()
|
|
1077
1077
|
}).optional(),
|
|
1078
|
+
// When true, the connection observes the session but cannot mutate
|
|
1079
|
+
// it: state-changing methods (session/prompt, session/cancel,
|
|
1080
|
+
// session/set_model, etc.) are rejected with -32011, and attaching
|
|
1081
|
+
// to a cold session does not resurrect or spawn an agent — just
|
|
1082
|
+
// streams history from disk. Used by the TUI's view-only mode.
|
|
1083
|
+
readonly: z3.boolean().optional(),
|
|
1078
1084
|
_meta: z3.record(z3.unknown()).optional()
|
|
1079
1085
|
});
|
|
1080
1086
|
var HYDRA_META_KEY = "hydra-acp";
|
|
@@ -4893,6 +4899,13 @@ var SessionManager = class {
|
|
|
4893
4899
|
}
|
|
4894
4900
|
return this.histories.load(sessionId).catch(() => []);
|
|
4895
4901
|
}
|
|
4902
|
+
// Read the on-disk history.jsonl for a session without constructing a
|
|
4903
|
+
// Session instance. Used by the daemon's read-only viewer attach path
|
|
4904
|
+
// (cli/src/daemon/acp-ws.ts) to stream replay events to a client for
|
|
4905
|
+
// a cold session without spawning an agent.
|
|
4906
|
+
async loadHistory(sessionId) {
|
|
4907
|
+
return this.histories.load(sessionId);
|
|
4908
|
+
}
|
|
4896
4909
|
async loadFromDisk(sessionId) {
|
|
4897
4910
|
const record = await this.store.read(sessionId);
|
|
4898
4911
|
if (!record) {
|
|
@@ -5119,6 +5132,10 @@ var SessionManager = class {
|
|
|
5119
5132
|
args.sessionId,
|
|
5120
5133
|
args.bundle.history
|
|
5121
5134
|
);
|
|
5135
|
+
const sourceMtime = new Date(args.bundle.session.updatedAt);
|
|
5136
|
+
if (!Number.isNaN(sourceMtime.getTime())) {
|
|
5137
|
+
await fs10.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
|
|
5138
|
+
}
|
|
5122
5139
|
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
5123
5140
|
await saveHistory(
|
|
5124
5141
|
paths.tuiHistoryFile(args.sessionId),
|
|
@@ -5142,7 +5159,9 @@ var SessionManager = class {
|
|
|
5142
5159
|
currentUsage: args.bundle.session.currentUsage,
|
|
5143
5160
|
agentCommands: args.bundle.session.agentCommands,
|
|
5144
5161
|
createdAt: args.preservedCreatedAt ?? now,
|
|
5145
|
-
|
|
5162
|
+
// Fallback path for historyMtimeIso (used when the history file
|
|
5163
|
+
// is missing). Keep this consistent with the utimes stamp above.
|
|
5164
|
+
updatedAt: args.bundle.session.updatedAt
|
|
5146
5165
|
});
|
|
5147
5166
|
});
|
|
5148
5167
|
}
|
|
@@ -6060,6 +6079,9 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6060
6079
|
if (activeVersions.has(version)) {
|
|
6061
6080
|
continue;
|
|
6062
6081
|
}
|
|
6082
|
+
if (version.includes(".partial-")) {
|
|
6083
|
+
continue;
|
|
6084
|
+
}
|
|
6063
6085
|
const versionDir = path8.join(agentDir, version);
|
|
6064
6086
|
try {
|
|
6065
6087
|
await fsp4.rm(versionDir, { recursive: true, force: true });
|
|
@@ -7708,6 +7730,16 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7708
7730
|
}
|
|
7709
7731
|
state.attached.clear();
|
|
7710
7732
|
});
|
|
7733
|
+
const denyIfReadonly = (sessionId, method) => {
|
|
7734
|
+
const att = state.attached.get(sessionId);
|
|
7735
|
+
if (att?.readonly) {
|
|
7736
|
+
const err = new Error(
|
|
7737
|
+
`${method} not permitted on a read-only attachment`
|
|
7738
|
+
);
|
|
7739
|
+
err.code = JsonRpcErrorCodes.PermissionDenied;
|
|
7740
|
+
throw err;
|
|
7741
|
+
}
|
|
7742
|
+
};
|
|
7711
7743
|
connection.onRequest("initialize", async (raw) => {
|
|
7712
7744
|
InitializeParams.parse(raw ?? {});
|
|
7713
7745
|
return buildInitializeResult();
|
|
@@ -7734,7 +7766,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7734
7766
|
const { entries: replay } = await session.attach(client, "full");
|
|
7735
7767
|
state.attached.set(session.sessionId, {
|
|
7736
7768
|
sessionId: session.sessionId,
|
|
7737
|
-
clientId: client.clientId
|
|
7769
|
+
clientId: client.clientId,
|
|
7770
|
+
readonly: false
|
|
7738
7771
|
});
|
|
7739
7772
|
setImmediate(() => {
|
|
7740
7773
|
void (async () => {
|
|
@@ -7760,11 +7793,46 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7760
7793
|
connection.onRequest("session/attach", async (raw) => {
|
|
7761
7794
|
const params = SessionAttachParams.parse(raw);
|
|
7762
7795
|
const hydraHints = extractHydraMeta(params._meta).resume;
|
|
7796
|
+
const readonly = params.readonly === true;
|
|
7763
7797
|
app.log.info(
|
|
7764
|
-
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints}`
|
|
7798
|
+
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints} readonly=${readonly}`
|
|
7765
7799
|
);
|
|
7766
7800
|
const lookupId = hydraHints ? params.sessionId : await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
7767
7801
|
let session = deps.manager.get(lookupId);
|
|
7802
|
+
if (!session && readonly) {
|
|
7803
|
+
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
7804
|
+
if (!fromDisk) {
|
|
7805
|
+
const err = new Error(
|
|
7806
|
+
`session ${params.sessionId} not found`
|
|
7807
|
+
);
|
|
7808
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
7809
|
+
throw err;
|
|
7810
|
+
}
|
|
7811
|
+
const history = await deps.manager.loadHistory(lookupId);
|
|
7812
|
+
const viewerClientId = params.clientId ?? `cli_${nanoid2(8)}`;
|
|
7813
|
+
state.attached.set(fromDisk.hydraSessionId, {
|
|
7814
|
+
sessionId: fromDisk.hydraSessionId,
|
|
7815
|
+
clientId: viewerClientId,
|
|
7816
|
+
readonly: true
|
|
7817
|
+
});
|
|
7818
|
+
app.log.info(
|
|
7819
|
+
`session/attach OK (viewer) sessionId=${fromDisk.hydraSessionId} clientId=${viewerClientId} attachedCount=${state.attached.size} replayed=${history.length}`
|
|
7820
|
+
);
|
|
7821
|
+
for (const entry of history) {
|
|
7822
|
+
await connection.notify(entry.method, entry.params).catch(() => void 0);
|
|
7823
|
+
}
|
|
7824
|
+
return {
|
|
7825
|
+
sessionId: fromDisk.hydraSessionId,
|
|
7826
|
+
clientId: viewerClientId,
|
|
7827
|
+
connectedClients: [viewerClientId],
|
|
7828
|
+
// No Session.attach() ran, so no history policy was applied —
|
|
7829
|
+
// the viewer always gets full history. Report "full" so the
|
|
7830
|
+
// wire shape matches the normal attach response.
|
|
7831
|
+
historyPolicy: "full",
|
|
7832
|
+
replayed: history.length,
|
|
7833
|
+
_meta: buildViewerResponseMeta(fromDisk)
|
|
7834
|
+
};
|
|
7835
|
+
}
|
|
7768
7836
|
if (!session) {
|
|
7769
7837
|
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
7770
7838
|
let resurrectParams = fromDisk;
|
|
@@ -7808,10 +7876,11 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7808
7876
|
);
|
|
7809
7877
|
state.attached.set(session.sessionId, {
|
|
7810
7878
|
sessionId: session.sessionId,
|
|
7811
|
-
clientId: client.clientId
|
|
7879
|
+
clientId: client.clientId,
|
|
7880
|
+
readonly
|
|
7812
7881
|
});
|
|
7813
7882
|
app.log.info(
|
|
7814
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length}`
|
|
7883
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}`
|
|
7815
7884
|
);
|
|
7816
7885
|
for (const note of replay) {
|
|
7817
7886
|
await connection.notify(note.method, note.params);
|
|
@@ -7855,6 +7924,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7855
7924
|
});
|
|
7856
7925
|
connection.onRequest("session/prompt", async (raw) => {
|
|
7857
7926
|
const params = SessionPromptParams.parse(raw);
|
|
7927
|
+
denyIfReadonly(params.sessionId, "session/prompt");
|
|
7858
7928
|
const att = state.attached.get(params.sessionId);
|
|
7859
7929
|
if (!att) {
|
|
7860
7930
|
app.log.warn(
|
|
@@ -7903,6 +7973,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7903
7973
|
if (!att) {
|
|
7904
7974
|
return;
|
|
7905
7975
|
}
|
|
7976
|
+
if (att.readonly) {
|
|
7977
|
+
app.log.warn(
|
|
7978
|
+
`session/cancel dropped (readonly attachment) sessionId=${params.sessionId}`
|
|
7979
|
+
);
|
|
7980
|
+
return;
|
|
7981
|
+
}
|
|
7906
7982
|
const session = deps.manager.get(params.sessionId);
|
|
7907
7983
|
if (!session) {
|
|
7908
7984
|
return;
|
|
@@ -7915,11 +7991,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7915
7991
|
};
|
|
7916
7992
|
connection.onNotification("session/cancel", handleCancelParams);
|
|
7917
7993
|
connection.onRequest("session/cancel", async (raw) => {
|
|
7994
|
+
const params = SessionCancelParams.parse(raw);
|
|
7995
|
+
denyIfReadonly(params.sessionId, "session/cancel");
|
|
7918
7996
|
handleCancelParams(raw);
|
|
7919
7997
|
return null;
|
|
7920
7998
|
});
|
|
7921
7999
|
connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
|
|
7922
8000
|
const params = CancelPromptParams.parse(raw);
|
|
8001
|
+
denyIfReadonly(params.sessionId, "hydra-acp/cancel_prompt");
|
|
7923
8002
|
const session = deps.manager.get(params.sessionId);
|
|
7924
8003
|
if (!session) {
|
|
7925
8004
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -7930,6 +8009,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7930
8009
|
});
|
|
7931
8010
|
connection.onRequest("hydra-acp/update_prompt", async (raw) => {
|
|
7932
8011
|
const params = UpdatePromptParams.parse(raw);
|
|
8012
|
+
denyIfReadonly(params.sessionId, "hydra-acp/update_prompt");
|
|
7933
8013
|
const session = deps.manager.get(params.sessionId);
|
|
7934
8014
|
if (!session) {
|
|
7935
8015
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -7940,6 +8020,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7940
8020
|
});
|
|
7941
8021
|
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
7942
8022
|
const params = AmendPromptParams.parse(raw);
|
|
8023
|
+
denyIfReadonly(params.sessionId, "hydra-acp/amend_prompt");
|
|
7943
8024
|
const att = state.attached.get(params.sessionId);
|
|
7944
8025
|
if (!att) {
|
|
7945
8026
|
const err = new Error("not attached to session");
|
|
@@ -7979,7 +8060,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7979
8060
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
7980
8061
|
state.attached.set(session.sessionId, {
|
|
7981
8062
|
sessionId: session.sessionId,
|
|
7982
|
-
clientId: client.clientId
|
|
8063
|
+
clientId: client.clientId,
|
|
8064
|
+
readonly: false
|
|
7983
8065
|
});
|
|
7984
8066
|
for (const note of replay) {
|
|
7985
8067
|
await connection.notify(note.method, note.params);
|
|
@@ -7998,6 +8080,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7998
8080
|
};
|
|
7999
8081
|
});
|
|
8000
8082
|
connection.onRequest("session/set_model", async (rawParams) => {
|
|
8083
|
+
const sessionIdField = rawParams?.sessionId;
|
|
8084
|
+
if (typeof sessionIdField === "string") {
|
|
8085
|
+
denyIfReadonly(sessionIdField, "session/set_model");
|
|
8086
|
+
}
|
|
8001
8087
|
const decision = decideSetModel(rawParams, deps.manager);
|
|
8002
8088
|
if (decision.kind === "error") {
|
|
8003
8089
|
app.log.warn(decision.logMessage);
|
|
@@ -8031,6 +8117,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8031
8117
|
err.code = JsonRpcErrorCodes.MethodNotFound;
|
|
8032
8118
|
throw err;
|
|
8033
8119
|
}
|
|
8120
|
+
denyIfReadonly(sessionId, method);
|
|
8034
8121
|
const session = deps.manager.get(sessionId);
|
|
8035
8122
|
if (!session) {
|
|
8036
8123
|
const err = new Error(`session ${sessionId} not found`);
|
|
@@ -8169,6 +8256,38 @@ function decideSetModel(rawParams, manager) {
|
|
|
8169
8256
|
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
8170
8257
|
};
|
|
8171
8258
|
}
|
|
8259
|
+
function buildViewerResponseMeta(fromDisk) {
|
|
8260
|
+
const ours = {
|
|
8261
|
+
upstreamSessionId: fromDisk.upstreamSessionId,
|
|
8262
|
+
agentId: fromDisk.agentId,
|
|
8263
|
+
cwd: fromDisk.cwd
|
|
8264
|
+
};
|
|
8265
|
+
if (fromDisk.title !== void 0) {
|
|
8266
|
+
ours.name = fromDisk.title;
|
|
8267
|
+
}
|
|
8268
|
+
if (fromDisk.agentArgs && fromDisk.agentArgs.length > 0) {
|
|
8269
|
+
ours.agentArgs = fromDisk.agentArgs;
|
|
8270
|
+
}
|
|
8271
|
+
if (fromDisk.currentModel !== void 0) {
|
|
8272
|
+
ours.currentModel = fromDisk.currentModel;
|
|
8273
|
+
}
|
|
8274
|
+
if (fromDisk.currentMode !== void 0) {
|
|
8275
|
+
ours.currentMode = fromDisk.currentMode;
|
|
8276
|
+
}
|
|
8277
|
+
if (fromDisk.currentUsage !== void 0) {
|
|
8278
|
+
ours.currentUsage = fromDisk.currentUsage;
|
|
8279
|
+
}
|
|
8280
|
+
if (fromDisk.agentCommands && fromDisk.agentCommands.length > 0) {
|
|
8281
|
+
ours.availableCommands = fromDisk.agentCommands;
|
|
8282
|
+
}
|
|
8283
|
+
if (fromDisk.agentModes && fromDisk.agentModes.length > 0) {
|
|
8284
|
+
ours.availableModes = fromDisk.agentModes;
|
|
8285
|
+
}
|
|
8286
|
+
if (fromDisk.agentModels && fromDisk.agentModels.length > 0) {
|
|
8287
|
+
ours.availableModels = fromDisk.agentModels;
|
|
8288
|
+
}
|
|
8289
|
+
return { [HYDRA_META_KEY]: ours };
|
|
8290
|
+
}
|
|
8172
8291
|
function buildResponseMeta(session) {
|
|
8173
8292
|
const ours = {
|
|
8174
8293
|
upstreamSessionId: session.upstreamSessionId,
|