@saleso.innovations/bridge 0.1.24 → 0.1.26
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/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -0
- package/dist/hermesCommands.d.ts +3 -1
- package/dist/hermesCommands.d.ts.map +1 -1
- package/dist/hermesCommands.js +18 -1
- package/dist/hermesSessionDb.d.ts +11 -0
- package/dist/hermesSessionDb.d.ts.map +1 -1
- package/dist/hermesSessionDb.js +67 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/shellSession.d.ts +21 -0
- package/dist/shellSession.d.ts.map +1 -0
- package/dist/shellSession.js +210 -0
- package/dist/shellSession.test.d.ts +2 -0
- package/dist/shellSession.test.d.ts.map +1 -0
- package/dist/shellSession.test.js +27 -0
- package/package.json +2 -2
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAYhG,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrG,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,cAAc,GAAG,gBAAgB,CAAC;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,qBAAqB,EAAE,EACrC,wBAAwB,CAAC,EAAE,MAAM,KAC9B,IAAI,CAAC;IACV,MAAM,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -135,7 +135,20 @@ async function handleHermesCommandEnvelope(ws, envelope, fallbackAgentId) {
|
|
|
135
135
|
status: "accepted",
|
|
136
136
|
}));
|
|
137
137
|
try {
|
|
138
|
-
const
|
|
138
|
+
const onDelta = command === "shell.exec"
|
|
139
|
+
? (stream, delta, deltaSequence) => {
|
|
140
|
+
ws.send(JSON.stringify({
|
|
141
|
+
type: "hermes.command.delta",
|
|
142
|
+
requestId,
|
|
143
|
+
agentId,
|
|
144
|
+
command,
|
|
145
|
+
stream,
|
|
146
|
+
delta,
|
|
147
|
+
sequence: deltaSequence,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
: undefined;
|
|
151
|
+
const result = await executeHermesCommand(command, parseCommandArgs(envelope.args), { onDelta });
|
|
139
152
|
ws.send(JSON.stringify({
|
|
140
153
|
type: "hermes.command.result",
|
|
141
154
|
requestId,
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export declare const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642/v1/chat/com
|
|
|
6
6
|
export declare const HERMES_COMMANDS_CAPABILITY = "hermes.commands.v1";
|
|
7
7
|
/** Bridge can resolve auto-generated session titles from ~/.hermes/state.db. */
|
|
8
8
|
export declare const HERMES_SESSION_TITLES_CAPABILITY = "hermes.commands.sessions.titles.v1";
|
|
9
|
+
/** Bridge can list all Hermes sessions from ~/.hermes/state.db (CLI + Cleos + other clients). */
|
|
10
|
+
export declare const HERMES_SESSIONS_LIST_CAPABILITY = "hermes.commands.sessions.list.v1";
|
|
11
|
+
/** Remote shell command execution over relay. */
|
|
12
|
+
export declare const TERMINAL_SHELL_CAPABILITY = "terminal.shell.v1";
|
|
9
13
|
export declare const DEFAULT_BRIDGE_CAPABILITIES: string[];
|
|
10
14
|
/** VPS one-line installer (served from public Convex HTTP). */
|
|
11
15
|
export declare const DEFAULT_BRIDGE_INSTALL_URL = "https://amicable-elephant-407.convex.site/install-bridge.sh";
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,8CAA8C,CAAC;AAEzF,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,8CAA8C,CAAC;AAElF,4DAA4D;AAC5D,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,uCAAuC,CAAC;AAErF,eAAO,MAAM,2BAA2B,
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,8CAA8C,CAAC;AAEzF,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,8CAA8C,CAAC;AAElF,4DAA4D;AAC5D,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,uCAAuC,CAAC;AAErF,iGAAiG;AACjG,eAAO,MAAM,+BAA+B,qCAAqC,CAAC;AAElF,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,sBAAsB,CAAC;AAE7D,eAAO,MAAM,2BAA2B,UAMvC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,0BAA0B,gEACwB,CAAC;AAEhE,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,+DACwB,CAAC;AAE/D,uGAAuG;AACvG,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
package/dist/constants.js
CHANGED
|
@@ -6,10 +6,16 @@ export const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642/v1/chat/completions
|
|
|
6
6
|
export const HERMES_COMMANDS_CAPABILITY = "hermes.commands.v1";
|
|
7
7
|
/** Bridge can resolve auto-generated session titles from ~/.hermes/state.db. */
|
|
8
8
|
export const HERMES_SESSION_TITLES_CAPABILITY = "hermes.commands.sessions.titles.v1";
|
|
9
|
+
/** Bridge can list all Hermes sessions from ~/.hermes/state.db (CLI + Cleos + other clients). */
|
|
10
|
+
export const HERMES_SESSIONS_LIST_CAPABILITY = "hermes.commands.sessions.list.v1";
|
|
11
|
+
/** Remote shell command execution over relay. */
|
|
12
|
+
export const TERMINAL_SHELL_CAPABILITY = "terminal.shell.v1";
|
|
9
13
|
export const DEFAULT_BRIDGE_CAPABILITIES = [
|
|
10
14
|
"chat",
|
|
11
15
|
HERMES_COMMANDS_CAPABILITY,
|
|
12
16
|
HERMES_SESSION_TITLES_CAPABILITY,
|
|
17
|
+
HERMES_SESSIONS_LIST_CAPABILITY,
|
|
18
|
+
TERMINAL_SHELL_CAPABILITY,
|
|
13
19
|
];
|
|
14
20
|
/** VPS one-line installer (served from public Convex HTTP). */
|
|
15
21
|
export const DEFAULT_BRIDGE_INSTALL_URL = "https://amicable-elephant-407.convex.site/install-bridge.sh";
|
package/dist/hermesCommands.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ShellDeltaEmitter } from "./shellSession.js";
|
|
2
|
+
export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.list", "skills.list", "files.list", "files.read", "files.write", "memories.list", "shell.exec", "shell.session.reset"];
|
|
2
3
|
export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
|
|
3
4
|
export declare function isHermesCommandName(value: string): value is HermesCommandName;
|
|
4
5
|
export type HermesCommandErrorCode = "command_unsupported" | "hermes_unreachable" | "hermes_request_failed" | "invalid_command_args" | "unsupported_by_http";
|
|
@@ -10,6 +11,7 @@ export declare function executeHermesCommand(command: HermesCommandName, args: R
|
|
|
10
11
|
apiUrl?: string;
|
|
11
12
|
apiKey?: string;
|
|
12
13
|
model?: string;
|
|
14
|
+
onDelta?: ShellDeltaEmitter;
|
|
13
15
|
}): Promise<unknown>;
|
|
14
16
|
export declare function userSafeCommandError(error: unknown): {
|
|
15
17
|
code: HermesCommandErrorCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGhG,eAAO,MAAM,oBAAoB,6mBAoCvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AA6GD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9F,OAAO,CAAC,OAAO,CAAC,CAsNlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
|
package/dist/hermesCommands.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolveHermesApiConfig } from "./hermesForwarder.js";
|
|
2
2
|
import { restartHermesGateway, startHermesGateway, stopHermesGateway } from "./gatewayControl.js";
|
|
3
3
|
import { listHermesCronJobs } from "./cronList.js";
|
|
4
|
-
import { listSessionMessages, countUserMessagesSent, resolveSessionTitles } from "./hermesSessionDb.js";
|
|
4
|
+
import { listHermesSessions, listSessionMessages, countUserMessagesSent, resolveSessionTitles, } from "./hermesSessionDb.js";
|
|
5
5
|
import { listHermesSkills } from "./skillsList.js";
|
|
6
6
|
import { runHermesUpdate } from "./hermesUpdate.js";
|
|
7
7
|
import { executeFilesList, executeFilesRead, executeFilesWrite, executeMemoriesList, } from "./hermesFileCommands.js";
|
|
8
|
+
import { executeShellExec, resetShellSession } from "./shellSession.js";
|
|
8
9
|
import { fetchHermesRuntimeVersion } from "./runtimeVersion.js";
|
|
9
10
|
export const HERMES_COMMAND_NAMES = [
|
|
10
11
|
"runtime.health",
|
|
@@ -34,11 +35,14 @@ export const HERMES_COMMAND_NAMES = [
|
|
|
34
35
|
"sessions.messages.list",
|
|
35
36
|
"sessions.messages.countSent",
|
|
36
37
|
"sessions.titles.resolve",
|
|
38
|
+
"sessions.list",
|
|
37
39
|
"skills.list",
|
|
38
40
|
"files.list",
|
|
39
41
|
"files.read",
|
|
40
42
|
"files.write",
|
|
41
43
|
"memories.list",
|
|
44
|
+
"shell.exec",
|
|
45
|
+
"shell.session.reset",
|
|
42
46
|
];
|
|
43
47
|
export function isHermesCommandName(value) {
|
|
44
48
|
return HERMES_COMMAND_NAMES.includes(value);
|
|
@@ -139,6 +143,7 @@ async function hermesFetchVoid(config, path, init = {}) {
|
|
|
139
143
|
}
|
|
140
144
|
export async function executeHermesCommand(command, args, options = {}) {
|
|
141
145
|
const config = resolveHermesApiConfig(options);
|
|
146
|
+
const { onDelta } = options;
|
|
142
147
|
switch (command) {
|
|
143
148
|
case "runtime.health":
|
|
144
149
|
return await hermesFetchJson(config, "/health");
|
|
@@ -306,6 +311,10 @@ export async function executeHermesCommand(command, args, options = {}) {
|
|
|
306
311
|
const sessionIds = optionalStringArray(args, "sessionIds") ?? [];
|
|
307
312
|
return { titles: resolveSessionTitles(sessionIds) };
|
|
308
313
|
}
|
|
314
|
+
case "sessions.list": {
|
|
315
|
+
const limit = optionalNumber(args, "limit");
|
|
316
|
+
return { sessions: listHermesSessions({ limit: limit ?? 200 }) };
|
|
317
|
+
}
|
|
309
318
|
case "skills.list":
|
|
310
319
|
return await listHermesSkills();
|
|
311
320
|
case "files.list":
|
|
@@ -330,6 +339,14 @@ export async function executeHermesCommand(command, args, options = {}) {
|
|
|
330
339
|
}
|
|
331
340
|
case "memories.list":
|
|
332
341
|
return await executeMemoriesList();
|
|
342
|
+
case "shell.exec": {
|
|
343
|
+
if (!onDelta) {
|
|
344
|
+
throw new HermesCommandError("invalid_command_args", "shell.exec requires streaming support");
|
|
345
|
+
}
|
|
346
|
+
return await executeShellExec(args, onDelta);
|
|
347
|
+
}
|
|
348
|
+
case "shell.session.reset":
|
|
349
|
+
return resetShellSession(requireString(args, "sessionId"));
|
|
333
350
|
default: {
|
|
334
351
|
const _exhaustive = command;
|
|
335
352
|
throw new HermesCommandError("command_unsupported", `Unsupported command: ${String(_exhaustive)}`);
|
|
@@ -16,6 +16,17 @@ export declare function getLatestTurnMessageIds(sessionId: string): {
|
|
|
16
16
|
userMessageId?: number;
|
|
17
17
|
assistantMessageId?: number;
|
|
18
18
|
};
|
|
19
|
+
export type HermesSessionListEntry = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string | null;
|
|
22
|
+
startedAt: number;
|
|
23
|
+
lastMessageAt: number;
|
|
24
|
+
previewText: string;
|
|
25
|
+
messageCount: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function listHermesSessions(options?: {
|
|
28
|
+
limit?: number;
|
|
29
|
+
}): HermesSessionListEntry[];
|
|
19
30
|
export declare function hermesStateDbExists(): boolean;
|
|
20
31
|
export declare function countUserMessagesSent(): {
|
|
21
32
|
count: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hermesSessionDb.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;
|
|
1
|
+
{"version":3,"file":"hermesSessionDb.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAyIF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapE;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAuBxF;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACxE,oBAAoB,EAAE,CAwCxB;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAmCzD;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAMF,wBAAgB,kBAAkB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,sBAAsB,EAAE,CAyD7F;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAO7C;AAED,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAgBzD"}
|
package/dist/hermesSessionDb.js
CHANGED
|
@@ -93,13 +93,29 @@ function sessionTitleFromChain(db, sessionId) {
|
|
|
93
93
|
}
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
|
+
function sessionTitleFromFirstUserMessage(db, sessionId) {
|
|
97
|
+
const resolved = resolveResumeSessionId(db, sessionId);
|
|
98
|
+
const chain = sessionAncestorChain(db, resolved);
|
|
99
|
+
const placeholders = chain.map(() => "?").join(", ");
|
|
100
|
+
const row = db
|
|
101
|
+
.prepare(`SELECT content FROM messages
|
|
102
|
+
WHERE session_id IN (${placeholders}) AND role = 'user'
|
|
103
|
+
ORDER BY timestamp ASC, id ASC
|
|
104
|
+
LIMIT 1`)
|
|
105
|
+
.get(...chain);
|
|
106
|
+
const snippet = decodeMessageContent(row?.content ?? null).trim().slice(0, 120);
|
|
107
|
+
return snippet || null;
|
|
108
|
+
}
|
|
109
|
+
function sessionTitleForLookupId(db, sessionId) {
|
|
110
|
+
return sessionTitleFromChain(db, sessionId) ?? sessionTitleFromFirstUserMessage(db, sessionId);
|
|
111
|
+
}
|
|
96
112
|
export function resolveSessionTitle(sessionId) {
|
|
97
113
|
if (!sessionId.trim() || !hermesStateDbExists()) {
|
|
98
114
|
return null;
|
|
99
115
|
}
|
|
100
116
|
const db = openReadOnlyDb();
|
|
101
117
|
try {
|
|
102
|
-
return
|
|
118
|
+
return sessionTitleForLookupId(db, sessionId);
|
|
103
119
|
}
|
|
104
120
|
catch {
|
|
105
121
|
return null;
|
|
@@ -120,7 +136,7 @@ export function resolveSessionTitles(sessionIds) {
|
|
|
120
136
|
const db = openReadOnlyDb();
|
|
121
137
|
try {
|
|
122
138
|
for (const sessionId of uniqueIds) {
|
|
123
|
-
titles[sessionId] =
|
|
139
|
+
titles[sessionId] = sessionTitleForLookupId(db, sessionId);
|
|
124
140
|
}
|
|
125
141
|
}
|
|
126
142
|
catch {
|
|
@@ -197,6 +213,55 @@ export function getLatestTurnMessageIds(sessionId) {
|
|
|
197
213
|
db.close();
|
|
198
214
|
}
|
|
199
215
|
}
|
|
216
|
+
function normalizeTimestampMs(value) {
|
|
217
|
+
return value < 1_000_000_000_000 ? value * 1000 : value;
|
|
218
|
+
}
|
|
219
|
+
export function listHermesSessions(options = {}) {
|
|
220
|
+
if (!hermesStateDbExists()) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
const limit = Math.min(Math.max(options.limit ?? 200, 1), 500);
|
|
224
|
+
const db = openReadOnlyDb();
|
|
225
|
+
try {
|
|
226
|
+
const rows = db
|
|
227
|
+
.prepare(`SELECT
|
|
228
|
+
s.id AS id,
|
|
229
|
+
s.title AS title,
|
|
230
|
+
s.started_at AS started_at,
|
|
231
|
+
MAX(m.timestamp) AS last_message_at,
|
|
232
|
+
COUNT(m.id) AS message_count
|
|
233
|
+
FROM sessions s
|
|
234
|
+
INNER JOIN messages m ON m.session_id = s.id AND m.role IN ('user', 'assistant')
|
|
235
|
+
GROUP BY s.id
|
|
236
|
+
ORDER BY last_message_at DESC
|
|
237
|
+
LIMIT ?`)
|
|
238
|
+
.all(limit);
|
|
239
|
+
return rows.map((row) => {
|
|
240
|
+
const previewRow = db
|
|
241
|
+
.prepare(`SELECT content FROM messages
|
|
242
|
+
WHERE session_id = ? AND role IN ('user', 'assistant')
|
|
243
|
+
ORDER BY timestamp DESC, id DESC
|
|
244
|
+
LIMIT 1`)
|
|
245
|
+
.get(row.id);
|
|
246
|
+
const resolvedTitle = sessionTitleForLookupId(db, row.id);
|
|
247
|
+
const previewText = decodeMessageContent(previewRow?.content ?? null).trim().slice(0, 120);
|
|
248
|
+
return {
|
|
249
|
+
id: row.id,
|
|
250
|
+
title: resolvedTitle ?? row.title?.trim() ?? null,
|
|
251
|
+
startedAt: normalizeTimestampMs(row.started_at ?? row.last_message_at),
|
|
252
|
+
lastMessageAt: normalizeTimestampMs(row.last_message_at),
|
|
253
|
+
previewText,
|
|
254
|
+
messageCount: row.message_count,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
db.close();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
200
265
|
export function hermesStateDbExists() {
|
|
201
266
|
try {
|
|
202
267
|
readFileSync(hermesStateDbPath());
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export type { PairingInfo } from "./resolve.js";
|
|
|
7
7
|
export { pairWithCleos } from "./pairWithCleos.js";
|
|
8
8
|
export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
|
|
9
9
|
export type { HermesForwarderOptions, HermesForwardResult } from "./hermesForwarder.js";
|
|
10
|
-
export { listSessionMessages, resolveSessionTitle, resolveSessionTitles } from "./hermesSessionDb.js";
|
|
10
|
+
export { listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
|
|
11
|
+
export type { HermesSessionListEntry } from "./hermesSessionDb.js";
|
|
11
12
|
export type { HermesSessionMessage } from "./hermesSessionDb.js";
|
|
12
13
|
export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
|
|
13
14
|
export type { HermesCommandErrorCode, HermesCommandName } from "./hermesCommands.js";
|
|
@@ -16,5 +17,5 @@ export { backfillCronDeliveries, describeCronDeliveryState } from "./cronBackfil
|
|
|
16
17
|
export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
|
|
17
18
|
export { runBridgeDaemon } from "./daemon.js";
|
|
18
19
|
export type { RunBridgeOptions } from "./daemon.js";
|
|
19
|
-
export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
|
|
20
|
+
export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, HERMES_SESSIONS_LIST_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
|
|
20
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC3G,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC3G,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,6BAA6B,EAC7B,sBAAsB,EACtB,2BAA2B,EAC3B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,EAC/B,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,10 @@ export { loadCredentials, saveCredentials, credentialsPathForDisplay } from "./c
|
|
|
3
3
|
export { resolvePairingCode, convexSiteUrlFromEnv } from "./resolve.js";
|
|
4
4
|
export { pairWithCleos } from "./pairWithCleos.js";
|
|
5
5
|
export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
|
|
6
|
-
export { listSessionMessages, resolveSessionTitle, resolveSessionTitles } from "./hermesSessionDb.js";
|
|
6
|
+
export { listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
|
|
7
7
|
export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
|
|
8
8
|
export { startCronWatcher, listPendingCronFiles, clearDeliveredIndex } from "./cronWatcher.js";
|
|
9
9
|
export { backfillCronDeliveries, describeCronDeliveryState } from "./cronBackfill.js";
|
|
10
10
|
export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
|
|
11
11
|
export { runBridgeDaemon } from "./daemon.js";
|
|
12
|
-
export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
|
|
12
|
+
export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, HERMES_SESSIONS_LIST_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const EXIT_SENTINEL_PREFIX = "__CLEOS_EXIT_";
|
|
2
|
+
export declare const EXIT_SENTINEL_SUFFIX = "__";
|
|
3
|
+
export declare const MAX_OUTPUT_BYTES: number;
|
|
4
|
+
export declare const DEFAULT_COMMAND_TIMEOUT_MS = 120000;
|
|
5
|
+
export type ShellDeltaEmitter = (stream: "stdout" | "stderr", delta: string, sequence: number) => void;
|
|
6
|
+
export type ParsedShellFooter = {
|
|
7
|
+
output: string;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
cwd: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildWrappedCommand(userCommand: string): string;
|
|
12
|
+
export declare function parseShellFooter(raw: string): ParsedShellFooter | null;
|
|
13
|
+
export declare function executeShellExec(args: Record<string, unknown>, onDelta: ShellDeltaEmitter, timeoutMs?: number): Promise<{
|
|
14
|
+
exitCode: number;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function resetShellSession(sessionId: string): {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function clearShellSessionsForTests(): void;
|
|
21
|
+
//# sourceMappingURL=shellSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellSession.d.ts","sourceRoot":"","sources":["../src/shellSession.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AACpD,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAC3C,eAAO,MAAM,0BAA0B,SAAU,CAAC;AAGlD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAmBF,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAWtE;AAmJD,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,iBAAiB,EAC1B,SAAS,GAAE,MAAmC,GAC7C,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAwClD;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAU1E;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAOjD"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
export const EXIT_SENTINEL_PREFIX = "__CLEOS_EXIT_";
|
|
4
|
+
export const EXIT_SENTINEL_SUFFIX = "__";
|
|
5
|
+
export const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
6
|
+
export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
7
|
+
const SENTINEL_TAIL_RESERVE = 64;
|
|
8
|
+
const sessions = new Map();
|
|
9
|
+
export function buildWrappedCommand(userCommand) {
|
|
10
|
+
const encoded = Buffer.from(userCommand, "utf8").toString("base64");
|
|
11
|
+
return `{ eval "$(printf '%s' '${encoded}' | base64 -d)"; ec=$?; echo "${EXIT_SENTINEL_PREFIX}\${ec}${EXIT_SENTINEL_SUFFIX}"; pwd; } 2>&1\n`;
|
|
12
|
+
}
|
|
13
|
+
export function parseShellFooter(raw) {
|
|
14
|
+
const pattern = new RegExp(`\\n${EXIT_SENTINEL_PREFIX}(\\d+)${EXIT_SENTINEL_SUFFIX}\\n([^\\n]*)\\n?$`);
|
|
15
|
+
const match = raw.match(pattern);
|
|
16
|
+
if (!match || match.index === undefined)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
output: raw.slice(0, match.index),
|
|
20
|
+
exitCode: Number.parseInt(match[1] ?? "0", 10),
|
|
21
|
+
cwd: match[2] ?? "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function truncateNotice() {
|
|
25
|
+
return `\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]\n`;
|
|
26
|
+
}
|
|
27
|
+
function emitDelta(session, delta) {
|
|
28
|
+
if (!delta || !session.pendingOnDelta)
|
|
29
|
+
return;
|
|
30
|
+
session.pendingSequence += 1;
|
|
31
|
+
session.pendingOnDelta("stdout", delta, session.pendingSequence);
|
|
32
|
+
}
|
|
33
|
+
function flushStreamableOutput(session) {
|
|
34
|
+
const footer = parseShellFooter(session.pendingBuffer);
|
|
35
|
+
if (footer) {
|
|
36
|
+
const newOutput = footer.output.slice(session.streamedLength);
|
|
37
|
+
emitDelta(session, newOutput);
|
|
38
|
+
session.streamedLength = footer.output.length;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const reserveStart = Math.max(0, session.pendingBuffer.length - SENTINEL_TAIL_RESERVE);
|
|
42
|
+
const streamableEnd = reserveStart;
|
|
43
|
+
if (streamableEnd > session.streamedLength) {
|
|
44
|
+
emitDelta(session, session.pendingBuffer.slice(session.streamedLength, streamableEnd));
|
|
45
|
+
session.streamedLength = streamableEnd;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function finishPending(session, footer) {
|
|
49
|
+
if (session.pendingTimeout) {
|
|
50
|
+
clearTimeout(session.pendingTimeout);
|
|
51
|
+
session.pendingTimeout = null;
|
|
52
|
+
}
|
|
53
|
+
const resolve = session.pendingResolve;
|
|
54
|
+
session.pendingResolve = null;
|
|
55
|
+
session.pendingReject = null;
|
|
56
|
+
session.pendingOnDelta = null;
|
|
57
|
+
session.pendingBuffer = "";
|
|
58
|
+
session.streamedLength = 0;
|
|
59
|
+
session.pendingBytes = 0;
|
|
60
|
+
session.pendingTruncated = false;
|
|
61
|
+
session.busy = false;
|
|
62
|
+
if (footer.cwd)
|
|
63
|
+
session.cwd = footer.cwd;
|
|
64
|
+
resolve?.(footer);
|
|
65
|
+
}
|
|
66
|
+
function failPending(session, error) {
|
|
67
|
+
if (session.pendingTimeout) {
|
|
68
|
+
clearTimeout(session.pendingTimeout);
|
|
69
|
+
session.pendingTimeout = null;
|
|
70
|
+
}
|
|
71
|
+
const reject = session.pendingReject;
|
|
72
|
+
session.pendingResolve = null;
|
|
73
|
+
session.pendingReject = null;
|
|
74
|
+
session.pendingOnDelta = null;
|
|
75
|
+
session.pendingBuffer = "";
|
|
76
|
+
session.streamedLength = 0;
|
|
77
|
+
session.pendingBytes = 0;
|
|
78
|
+
session.pendingTruncated = false;
|
|
79
|
+
session.busy = false;
|
|
80
|
+
reject?.(error);
|
|
81
|
+
}
|
|
82
|
+
function appendPendingData(session, chunk) {
|
|
83
|
+
session.pendingBytes += Buffer.byteLength(chunk, "utf8");
|
|
84
|
+
if (session.pendingBytes > MAX_OUTPUT_BYTES) {
|
|
85
|
+
if (!session.pendingTruncated) {
|
|
86
|
+
session.pendingTruncated = true;
|
|
87
|
+
const allowed = MAX_OUTPUT_BYTES - Buffer.byteLength(truncateNotice(), "utf8");
|
|
88
|
+
const currentBytes = Buffer.byteLength(session.pendingBuffer, "utf8");
|
|
89
|
+
if (currentBytes > allowed) {
|
|
90
|
+
session.pendingBuffer = Buffer.from(session.pendingBuffer, "utf8")
|
|
91
|
+
.subarray(0, allowed)
|
|
92
|
+
.toString("utf8");
|
|
93
|
+
}
|
|
94
|
+
session.pendingBuffer += truncateNotice();
|
|
95
|
+
flushStreamableOutput(session);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
session.pendingBuffer += chunk;
|
|
100
|
+
flushStreamableOutput(session);
|
|
101
|
+
const footer = parseShellFooter(session.pendingBuffer);
|
|
102
|
+
if (footer) {
|
|
103
|
+
finishPending(session, footer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function attachSessionHandlers(sessionId, session) {
|
|
107
|
+
session.process.stdout.on("data", (data) => {
|
|
108
|
+
appendPendingData(session, data.toString("utf8"));
|
|
109
|
+
});
|
|
110
|
+
session.process.stderr.on("data", (data) => {
|
|
111
|
+
if (session.pendingOnDelta && session.busy) {
|
|
112
|
+
session.pendingSequence += 1;
|
|
113
|
+
session.pendingOnDelta("stderr", data.toString("utf8"), session.pendingSequence);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
session.process.on("exit", () => {
|
|
117
|
+
sessions.delete(sessionId);
|
|
118
|
+
if (session.busy) {
|
|
119
|
+
failPending(session, new Error("Shell session exited unexpectedly"));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function createSession(sessionId) {
|
|
124
|
+
const child = spawn("bash", ["--login"], {
|
|
125
|
+
cwd: homedir(),
|
|
126
|
+
env: process.env,
|
|
127
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
128
|
+
});
|
|
129
|
+
const session = {
|
|
130
|
+
process: child,
|
|
131
|
+
cwd: homedir(),
|
|
132
|
+
busy: false,
|
|
133
|
+
pendingBuffer: "",
|
|
134
|
+
pendingResolve: null,
|
|
135
|
+
pendingReject: null,
|
|
136
|
+
pendingTimeout: null,
|
|
137
|
+
pendingSequence: 0,
|
|
138
|
+
pendingBytes: 0,
|
|
139
|
+
pendingTruncated: false,
|
|
140
|
+
pendingOnDelta: null,
|
|
141
|
+
streamedLength: 0,
|
|
142
|
+
};
|
|
143
|
+
attachSessionHandlers(sessionId, session);
|
|
144
|
+
sessions.set(sessionId, session);
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
function getOrCreateSession(sessionId) {
|
|
148
|
+
const existing = sessions.get(sessionId);
|
|
149
|
+
if (existing && existing.process.exitCode === null && !existing.process.killed) {
|
|
150
|
+
return existing;
|
|
151
|
+
}
|
|
152
|
+
if (existing)
|
|
153
|
+
sessions.delete(sessionId);
|
|
154
|
+
return createSession(sessionId);
|
|
155
|
+
}
|
|
156
|
+
export async function executeShellExec(args, onDelta, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
|
|
157
|
+
const command = args.command;
|
|
158
|
+
const sessionIdRaw = args.sessionId;
|
|
159
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
160
|
+
throw new Error('Missing "command"');
|
|
161
|
+
}
|
|
162
|
+
if (typeof sessionIdRaw !== "string" || sessionIdRaw.trim().length === 0) {
|
|
163
|
+
throw new Error('Missing "sessionId"');
|
|
164
|
+
}
|
|
165
|
+
const sessionId = sessionIdRaw.trim();
|
|
166
|
+
const session = getOrCreateSession(sessionId);
|
|
167
|
+
if (session.busy) {
|
|
168
|
+
throw new Error("Shell session is busy");
|
|
169
|
+
}
|
|
170
|
+
session.busy = true;
|
|
171
|
+
session.pendingBuffer = "";
|
|
172
|
+
session.streamedLength = 0;
|
|
173
|
+
session.pendingBytes = 0;
|
|
174
|
+
session.pendingTruncated = false;
|
|
175
|
+
session.pendingSequence = 0;
|
|
176
|
+
session.pendingOnDelta = onDelta;
|
|
177
|
+
const wrapped = buildWrappedCommand(command);
|
|
178
|
+
return await new Promise((resolve, reject) => {
|
|
179
|
+
session.pendingResolve = (footer) => {
|
|
180
|
+
resolve({ exitCode: footer.exitCode, sessionId });
|
|
181
|
+
};
|
|
182
|
+
session.pendingReject = reject;
|
|
183
|
+
session.pendingTimeout = setTimeout(() => {
|
|
184
|
+
failPending(session, new Error(`Command timed out after ${timeoutMs}ms`));
|
|
185
|
+
}, timeoutMs);
|
|
186
|
+
const wrote = session.process.stdin.write(wrapped);
|
|
187
|
+
if (!wrote) {
|
|
188
|
+
session.process.stdin.once("drain", () => undefined);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export function resetShellSession(sessionId) {
|
|
193
|
+
const existing = sessions.get(sessionId);
|
|
194
|
+
if (existing) {
|
|
195
|
+
if (existing.busy) {
|
|
196
|
+
throw new Error("Shell session is busy");
|
|
197
|
+
}
|
|
198
|
+
existing.process.kill("SIGTERM");
|
|
199
|
+
sessions.delete(sessionId);
|
|
200
|
+
}
|
|
201
|
+
return { sessionId };
|
|
202
|
+
}
|
|
203
|
+
export function clearShellSessionsForTests() {
|
|
204
|
+
for (const [sessionId, session] of sessions.entries()) {
|
|
205
|
+
if (!session.busy) {
|
|
206
|
+
session.process.kill("SIGTERM");
|
|
207
|
+
}
|
|
208
|
+
sessions.delete(sessionId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellSession.test.d.ts","sourceRoot":"","sources":["../src/shellSession.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildWrappedCommand, parseShellFooter, EXIT_SENTINEL_PREFIX, EXIT_SENTINEL_SUFFIX, } from "./shellSession.js";
|
|
4
|
+
test("parseShellFooter extracts output, exit code, and cwd", () => {
|
|
5
|
+
const raw = "line one\nline two\n" + `${EXIT_SENTINEL_PREFIX}1${EXIT_SENTINEL_SUFFIX}\n/tmp\n`;
|
|
6
|
+
const parsed = parseShellFooter(raw);
|
|
7
|
+
assert.ok(parsed);
|
|
8
|
+
assert.equal(parsed.output, "line one\nline two");
|
|
9
|
+
assert.equal(parsed.exitCode, 1);
|
|
10
|
+
assert.equal(parsed.cwd, "/tmp");
|
|
11
|
+
});
|
|
12
|
+
test("parseShellFooter returns null when sentinel is missing", () => {
|
|
13
|
+
assert.equal(parseShellFooter("hello\nworld\n"), null);
|
|
14
|
+
});
|
|
15
|
+
test("parseShellFooter handles exit code zero", () => {
|
|
16
|
+
const raw = "ok\n" + `${EXIT_SENTINEL_PREFIX}0${EXIT_SENTINEL_SUFFIX}\n/home/user\n`;
|
|
17
|
+
const parsed = parseShellFooter(raw);
|
|
18
|
+
assert.ok(parsed);
|
|
19
|
+
assert.equal(parsed.exitCode, 0);
|
|
20
|
+
assert.equal(parsed.cwd, "/home/user");
|
|
21
|
+
});
|
|
22
|
+
test("buildWrappedCommand base64-encodes user input", () => {
|
|
23
|
+
const wrapped = buildWrappedCommand("echo hello");
|
|
24
|
+
assert.match(wrapped, /base64 -d/);
|
|
25
|
+
assert.match(wrapped, new RegExp(`${EXIT_SENTINEL_PREFIX}\\$\\{ec\\}${EXIT_SENTINEL_SUFFIX}`));
|
|
26
|
+
assert.match(wrapped, /pwd/);
|
|
27
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saleso.innovations/bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"lint": "eslint . --max-warnings 0",
|
|
37
37
|
"check-types": "tsc --noEmit",
|
|
38
38
|
"prepublishOnly": "npm run build",
|
|
39
|
-
"test": "node --import tsx --test src/hermesFiles.test.ts"
|
|
39
|
+
"test": "node --import tsx --test src/hermesFiles.test.ts src/shellSession.test.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"better-sqlite3": "^11.10.0",
|