@hydra-acp/cli 0.1.30 → 0.1.32
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 +17 -1
- package/dist/cli.js +797 -181
- package/dist/index.d.ts +5 -0
- package/dist/index.js +133 -20
- package/package.json +1 -1
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
|
@@ -168,12 +168,12 @@ var TuiConfig = z.object({
|
|
|
168
168
|
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
169
169
|
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
170
170
|
maxScrollbackLines: z.number().int().positive().default(1e4),
|
|
171
|
-
// When true
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
// false
|
|
175
|
-
//
|
|
176
|
-
mouse: z.boolean().default(
|
|
171
|
+
// When true, the TUI captures mouse events so the wheel can drive
|
|
172
|
+
// scrollback. The cost: terminals route clicks to the app, so text
|
|
173
|
+
// selection requires shift+drag to bypass mouse reporting. Default
|
|
174
|
+
// false — wheel scrollback stops working, but plain click-drag
|
|
175
|
+
// selects text via the terminal emulator. Set true to opt back in.
|
|
176
|
+
mouse: z.boolean().default(false),
|
|
177
177
|
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
178
178
|
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
179
179
|
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
@@ -188,13 +188,13 @@ var TuiConfig = z.object({
|
|
|
188
188
|
// just don't want it.
|
|
189
189
|
progressIndicator: z.boolean().default(true),
|
|
190
190
|
// What the unmodified Enter key does in the prompt composer.
|
|
191
|
-
// "
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// "
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
defaultEnterAction: z.enum(["enqueue", "amend"]).default("
|
|
191
|
+
// "amend" (default) — Enter amends the in-flight turn; Shift+Enter
|
|
192
|
+
// enqueues. With no turn in flight either key just enqueues,
|
|
193
|
+
// since there's nothing to amend.
|
|
194
|
+
// "enqueue" — flips the two: Enter enqueues the prompt (sends
|
|
195
|
+
// immediately when idle, queues behind an in-flight turn);
|
|
196
|
+
// Shift+Enter amends the in-flight turn.
|
|
197
|
+
defaultEnterAction: z.enum(["enqueue", "amend"]).default("amend")
|
|
198
198
|
});
|
|
199
199
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
200
200
|
var ExtensionBody = z.object({
|
|
@@ -235,11 +235,11 @@ var HydraConfig = z.object({
|
|
|
235
235
|
tui: TuiConfig.default({
|
|
236
236
|
repaintThrottleMs: 1e3,
|
|
237
237
|
maxScrollbackLines: 1e4,
|
|
238
|
-
mouse:
|
|
238
|
+
mouse: false,
|
|
239
239
|
logMaxBytes: 5 * 1024 * 1024,
|
|
240
240
|
cwdColumnMaxWidth: 24,
|
|
241
241
|
progressIndicator: true,
|
|
242
|
-
defaultEnterAction: "
|
|
242
|
+
defaultEnterAction: "amend"
|
|
243
243
|
})
|
|
244
244
|
});
|
|
245
245
|
function extensionList(config) {
|
|
@@ -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) {
|
|
@@ -6066,6 +6079,9 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6066
6079
|
if (activeVersions.has(version)) {
|
|
6067
6080
|
continue;
|
|
6068
6081
|
}
|
|
6082
|
+
if (version.includes(".partial-")) {
|
|
6083
|
+
continue;
|
|
6084
|
+
}
|
|
6069
6085
|
const versionDir = path8.join(agentDir, version);
|
|
6070
6086
|
try {
|
|
6071
6087
|
await fsp4.rm(versionDir, { recursive: true, force: true });
|
|
@@ -7714,6 +7730,16 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7714
7730
|
}
|
|
7715
7731
|
state.attached.clear();
|
|
7716
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
|
+
};
|
|
7717
7743
|
connection.onRequest("initialize", async (raw) => {
|
|
7718
7744
|
InitializeParams.parse(raw ?? {});
|
|
7719
7745
|
return buildInitializeResult();
|
|
@@ -7740,7 +7766,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7740
7766
|
const { entries: replay } = await session.attach(client, "full");
|
|
7741
7767
|
state.attached.set(session.sessionId, {
|
|
7742
7768
|
sessionId: session.sessionId,
|
|
7743
|
-
clientId: client.clientId
|
|
7769
|
+
clientId: client.clientId,
|
|
7770
|
+
readonly: false
|
|
7744
7771
|
});
|
|
7745
7772
|
setImmediate(() => {
|
|
7746
7773
|
void (async () => {
|
|
@@ -7766,11 +7793,46 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7766
7793
|
connection.onRequest("session/attach", async (raw) => {
|
|
7767
7794
|
const params = SessionAttachParams.parse(raw);
|
|
7768
7795
|
const hydraHints = extractHydraMeta(params._meta).resume;
|
|
7796
|
+
const readonly = params.readonly === true;
|
|
7769
7797
|
app.log.info(
|
|
7770
|
-
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints}`
|
|
7798
|
+
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints} readonly=${readonly}`
|
|
7771
7799
|
);
|
|
7772
7800
|
const lookupId = hydraHints ? params.sessionId : await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
7773
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
|
+
}
|
|
7774
7836
|
if (!session) {
|
|
7775
7837
|
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
7776
7838
|
let resurrectParams = fromDisk;
|
|
@@ -7814,10 +7876,11 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7814
7876
|
);
|
|
7815
7877
|
state.attached.set(session.sessionId, {
|
|
7816
7878
|
sessionId: session.sessionId,
|
|
7817
|
-
clientId: client.clientId
|
|
7879
|
+
clientId: client.clientId,
|
|
7880
|
+
readonly
|
|
7818
7881
|
});
|
|
7819
7882
|
app.log.info(
|
|
7820
|
-
`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}`
|
|
7821
7884
|
);
|
|
7822
7885
|
for (const note of replay) {
|
|
7823
7886
|
await connection.notify(note.method, note.params);
|
|
@@ -7861,6 +7924,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7861
7924
|
});
|
|
7862
7925
|
connection.onRequest("session/prompt", async (raw) => {
|
|
7863
7926
|
const params = SessionPromptParams.parse(raw);
|
|
7927
|
+
denyIfReadonly(params.sessionId, "session/prompt");
|
|
7864
7928
|
const att = state.attached.get(params.sessionId);
|
|
7865
7929
|
if (!att) {
|
|
7866
7930
|
app.log.warn(
|
|
@@ -7909,6 +7973,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7909
7973
|
if (!att) {
|
|
7910
7974
|
return;
|
|
7911
7975
|
}
|
|
7976
|
+
if (att.readonly) {
|
|
7977
|
+
app.log.warn(
|
|
7978
|
+
`session/cancel dropped (readonly attachment) sessionId=${params.sessionId}`
|
|
7979
|
+
);
|
|
7980
|
+
return;
|
|
7981
|
+
}
|
|
7912
7982
|
const session = deps.manager.get(params.sessionId);
|
|
7913
7983
|
if (!session) {
|
|
7914
7984
|
return;
|
|
@@ -7921,11 +7991,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7921
7991
|
};
|
|
7922
7992
|
connection.onNotification("session/cancel", handleCancelParams);
|
|
7923
7993
|
connection.onRequest("session/cancel", async (raw) => {
|
|
7994
|
+
const params = SessionCancelParams.parse(raw);
|
|
7995
|
+
denyIfReadonly(params.sessionId, "session/cancel");
|
|
7924
7996
|
handleCancelParams(raw);
|
|
7925
7997
|
return null;
|
|
7926
7998
|
});
|
|
7927
7999
|
connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
|
|
7928
8000
|
const params = CancelPromptParams.parse(raw);
|
|
8001
|
+
denyIfReadonly(params.sessionId, "hydra-acp/cancel_prompt");
|
|
7929
8002
|
const session = deps.manager.get(params.sessionId);
|
|
7930
8003
|
if (!session) {
|
|
7931
8004
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -7936,6 +8009,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7936
8009
|
});
|
|
7937
8010
|
connection.onRequest("hydra-acp/update_prompt", async (raw) => {
|
|
7938
8011
|
const params = UpdatePromptParams.parse(raw);
|
|
8012
|
+
denyIfReadonly(params.sessionId, "hydra-acp/update_prompt");
|
|
7939
8013
|
const session = deps.manager.get(params.sessionId);
|
|
7940
8014
|
if (!session) {
|
|
7941
8015
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -7946,6 +8020,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7946
8020
|
});
|
|
7947
8021
|
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
7948
8022
|
const params = AmendPromptParams.parse(raw);
|
|
8023
|
+
denyIfReadonly(params.sessionId, "hydra-acp/amend_prompt");
|
|
7949
8024
|
const att = state.attached.get(params.sessionId);
|
|
7950
8025
|
if (!att) {
|
|
7951
8026
|
const err = new Error("not attached to session");
|
|
@@ -7985,7 +8060,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7985
8060
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
7986
8061
|
state.attached.set(session.sessionId, {
|
|
7987
8062
|
sessionId: session.sessionId,
|
|
7988
|
-
clientId: client.clientId
|
|
8063
|
+
clientId: client.clientId,
|
|
8064
|
+
readonly: false
|
|
7989
8065
|
});
|
|
7990
8066
|
for (const note of replay) {
|
|
7991
8067
|
await connection.notify(note.method, note.params);
|
|
@@ -8004,6 +8080,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8004
8080
|
};
|
|
8005
8081
|
});
|
|
8006
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
|
+
}
|
|
8007
8087
|
const decision = decideSetModel(rawParams, deps.manager);
|
|
8008
8088
|
if (decision.kind === "error") {
|
|
8009
8089
|
app.log.warn(decision.logMessage);
|
|
@@ -8037,6 +8117,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8037
8117
|
err.code = JsonRpcErrorCodes.MethodNotFound;
|
|
8038
8118
|
throw err;
|
|
8039
8119
|
}
|
|
8120
|
+
denyIfReadonly(sessionId, method);
|
|
8040
8121
|
const session = deps.manager.get(sessionId);
|
|
8041
8122
|
if (!session) {
|
|
8042
8123
|
const err = new Error(`session ${sessionId} not found`);
|
|
@@ -8175,6 +8256,38 @@ function decideSetModel(rawParams, manager) {
|
|
|
8175
8256
|
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
8176
8257
|
};
|
|
8177
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
|
+
}
|
|
8178
8291
|
function buildResponseMeta(session) {
|
|
8179
8292
|
const ours = {
|
|
8180
8293
|
upstreamSessionId: session.upstreamSessionId,
|