@saleso.innovations/bridge 0.1.25 → 0.1.27
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 +23 -1
- package/dist/hermesSessionDb.d.ts +26 -0
- package/dist/hermesSessionDb.d.ts.map +1 -1
- package/dist/hermesSessionDb.js +153 -0
- package/dist/hermesSessionDb.test.d.ts +2 -0
- package/dist/hermesSessionDb.test.d.ts.map +1 -0
- package/dist/hermesSessionDb.test.js +151 -0
- 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.usage.get", "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":"AAkBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGhG,eAAO,MAAM,oBAAoB,moBAqCvB,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,CA0NlB;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, getSessionUsage, 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,15 @@ export const HERMES_COMMAND_NAMES = [
|
|
|
34
35
|
"sessions.messages.list",
|
|
35
36
|
"sessions.messages.countSent",
|
|
36
37
|
"sessions.titles.resolve",
|
|
38
|
+
"sessions.usage.get",
|
|
39
|
+
"sessions.list",
|
|
37
40
|
"skills.list",
|
|
38
41
|
"files.list",
|
|
39
42
|
"files.read",
|
|
40
43
|
"files.write",
|
|
41
44
|
"memories.list",
|
|
45
|
+
"shell.exec",
|
|
46
|
+
"shell.session.reset",
|
|
42
47
|
];
|
|
43
48
|
export function isHermesCommandName(value) {
|
|
44
49
|
return HERMES_COMMAND_NAMES.includes(value);
|
|
@@ -139,6 +144,7 @@ async function hermesFetchVoid(config, path, init = {}) {
|
|
|
139
144
|
}
|
|
140
145
|
export async function executeHermesCommand(command, args, options = {}) {
|
|
141
146
|
const config = resolveHermesApiConfig(options);
|
|
147
|
+
const { onDelta } = options;
|
|
142
148
|
switch (command) {
|
|
143
149
|
case "runtime.health":
|
|
144
150
|
return await hermesFetchJson(config, "/health");
|
|
@@ -306,6 +312,14 @@ export async function executeHermesCommand(command, args, options = {}) {
|
|
|
306
312
|
const sessionIds = optionalStringArray(args, "sessionIds") ?? [];
|
|
307
313
|
return { titles: resolveSessionTitles(sessionIds) };
|
|
308
314
|
}
|
|
315
|
+
case "sessions.usage.get": {
|
|
316
|
+
const sessionId = requireString(args, "sessionId");
|
|
317
|
+
return getSessionUsage(sessionId);
|
|
318
|
+
}
|
|
319
|
+
case "sessions.list": {
|
|
320
|
+
const limit = optionalNumber(args, "limit");
|
|
321
|
+
return { sessions: listHermesSessions({ limit: limit ?? 200 }) };
|
|
322
|
+
}
|
|
309
323
|
case "skills.list":
|
|
310
324
|
return await listHermesSkills();
|
|
311
325
|
case "files.list":
|
|
@@ -330,6 +344,14 @@ export async function executeHermesCommand(command, args, options = {}) {
|
|
|
330
344
|
}
|
|
331
345
|
case "memories.list":
|
|
332
346
|
return await executeMemoriesList();
|
|
347
|
+
case "shell.exec": {
|
|
348
|
+
if (!onDelta) {
|
|
349
|
+
throw new HermesCommandError("invalid_command_args", "shell.exec requires streaming support");
|
|
350
|
+
}
|
|
351
|
+
return await executeShellExec(args, onDelta);
|
|
352
|
+
}
|
|
353
|
+
case "shell.session.reset":
|
|
354
|
+
return resetShellSession(requireString(args, "sessionId"));
|
|
333
355
|
default: {
|
|
334
356
|
const _exhaustive = command;
|
|
335
357
|
throw new HermesCommandError("command_unsupported", `Unsupported command: ${String(_exhaustive)}`);
|
|
@@ -16,6 +16,32 @@ 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
|
+
/** Hermes-injected prompts for cron runs and background skills — not user chat. */
|
|
28
|
+
export declare function isAutomatedHermesSessionPrompt(content: string): boolean;
|
|
29
|
+
export declare function listHermesSessions(options?: {
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): HermesSessionListEntry[];
|
|
32
|
+
export type HermesSessionUsage = {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
resolvedSessionId: string;
|
|
35
|
+
model: string | null;
|
|
36
|
+
inputTokens: number;
|
|
37
|
+
outputTokens: number;
|
|
38
|
+
cacheReadTokens: number;
|
|
39
|
+
cacheWriteTokens: number;
|
|
40
|
+
reasoningTokens: number;
|
|
41
|
+
estimatedCostUsd: number | null;
|
|
42
|
+
messageCount: number;
|
|
43
|
+
};
|
|
44
|
+
export declare function getSessionUsage(sessionId: string): HermesSessionUsage;
|
|
19
45
|
export declare function hermesStateDbExists(): boolean;
|
|
20
46
|
export declare function countUserMessagesSent(): {
|
|
21
47
|
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;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,wBAAgB,mBAAmB,IAAI,OAAO,CAO7C;AAED,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAgBzD"}
|
|
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,mFAAmF;AACnF,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAqBvE;AAcD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,sBAAsB,EAAE,CAsE7F;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAiBF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAkErE;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAO7C;AAED,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAgBzD"}
|
package/dist/hermesSessionDb.js
CHANGED
|
@@ -213,6 +213,159 @@ export function getLatestTurnMessageIds(sessionId) {
|
|
|
213
213
|
db.close();
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
+
function normalizeTimestampMs(value) {
|
|
217
|
+
return value < 1_000_000_000_000 ? value * 1000 : value;
|
|
218
|
+
}
|
|
219
|
+
/** Hermes-injected prompts for cron runs and background skills — not user chat. */
|
|
220
|
+
export function isAutomatedHermesSessionPrompt(content) {
|
|
221
|
+
const trimmed = content.trim();
|
|
222
|
+
if (!trimmed)
|
|
223
|
+
return false;
|
|
224
|
+
if (/\[IMPORTANT:[^\]]*scheduled cron job/i.test(trimmed)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (/you are running as .* background skill/i.test(trimmed)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (/^Cronjob Response:\s*.+\r?\n-+\r?\n/i.test(trimmed)) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (/^#\s*Cron Job:/im.test(trimmed) && /\bJob ID:\s*/im.test(trimmed)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function firstUserMessageContent(db, sessionId) {
|
|
239
|
+
const row = db
|
|
240
|
+
.prepare(`SELECT content FROM messages
|
|
241
|
+
WHERE session_id = ? AND role = 'user'
|
|
242
|
+
ORDER BY timestamp ASC, id ASC
|
|
243
|
+
LIMIT 1`)
|
|
244
|
+
.get(sessionId);
|
|
245
|
+
return decodeMessageContent(row?.content ?? null);
|
|
246
|
+
}
|
|
247
|
+
export function listHermesSessions(options = {}) {
|
|
248
|
+
if (!hermesStateDbExists()) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
const limit = Math.min(Math.max(options.limit ?? 200, 1), 500);
|
|
252
|
+
const fetchLimit = Math.min(limit * 4, 500);
|
|
253
|
+
const db = openReadOnlyDb();
|
|
254
|
+
try {
|
|
255
|
+
const rows = db
|
|
256
|
+
.prepare(`SELECT
|
|
257
|
+
s.id AS id,
|
|
258
|
+
s.title AS title,
|
|
259
|
+
s.started_at AS started_at,
|
|
260
|
+
MAX(m.timestamp) AS last_message_at,
|
|
261
|
+
COUNT(m.id) AS message_count
|
|
262
|
+
FROM sessions s
|
|
263
|
+
INNER JOIN messages m ON m.session_id = s.id AND m.role IN ('user', 'assistant')
|
|
264
|
+
GROUP BY s.id
|
|
265
|
+
ORDER BY last_message_at DESC
|
|
266
|
+
LIMIT ?`)
|
|
267
|
+
.all(fetchLimit);
|
|
268
|
+
const entries = [];
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
if (isAutomatedHermesSessionPrompt(firstUserMessageContent(db, row.id))) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const previewRow = db
|
|
274
|
+
.prepare(`SELECT content FROM messages
|
|
275
|
+
WHERE session_id = ? AND role IN ('user', 'assistant')
|
|
276
|
+
ORDER BY timestamp DESC, id DESC
|
|
277
|
+
LIMIT 1`)
|
|
278
|
+
.get(row.id);
|
|
279
|
+
const resolvedTitle = sessionTitleForLookupId(db, row.id);
|
|
280
|
+
const previewText = decodeMessageContent(previewRow?.content ?? null).trim().slice(0, 120);
|
|
281
|
+
entries.push({
|
|
282
|
+
id: row.id,
|
|
283
|
+
title: resolvedTitle ?? row.title?.trim() ?? null,
|
|
284
|
+
startedAt: normalizeTimestampMs(row.started_at ?? row.last_message_at),
|
|
285
|
+
lastMessageAt: normalizeTimestampMs(row.last_message_at),
|
|
286
|
+
previewText,
|
|
287
|
+
messageCount: row.message_count,
|
|
288
|
+
});
|
|
289
|
+
if (entries.length >= limit) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return entries;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
db.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function emptySessionUsage(sessionId, resolvedSessionId) {
|
|
303
|
+
return {
|
|
304
|
+
sessionId,
|
|
305
|
+
resolvedSessionId,
|
|
306
|
+
model: null,
|
|
307
|
+
inputTokens: 0,
|
|
308
|
+
outputTokens: 0,
|
|
309
|
+
cacheReadTokens: 0,
|
|
310
|
+
cacheWriteTokens: 0,
|
|
311
|
+
reasoningTokens: 0,
|
|
312
|
+
estimatedCostUsd: null,
|
|
313
|
+
messageCount: 0,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
export function getSessionUsage(sessionId) {
|
|
317
|
+
const trimmed = sessionId.trim();
|
|
318
|
+
if (!trimmed) {
|
|
319
|
+
return emptySessionUsage(sessionId, sessionId);
|
|
320
|
+
}
|
|
321
|
+
if (!hermesStateDbExists()) {
|
|
322
|
+
return emptySessionUsage(trimmed, trimmed);
|
|
323
|
+
}
|
|
324
|
+
const db = openReadOnlyDb();
|
|
325
|
+
try {
|
|
326
|
+
const resolved = resolveResumeSessionId(db, trimmed);
|
|
327
|
+
const chain = sessionAncestorChain(db, resolved);
|
|
328
|
+
const placeholders = chain.map(() => "?").join(", ");
|
|
329
|
+
const totals = db
|
|
330
|
+
.prepare(`SELECT
|
|
331
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
332
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
333
|
+
COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
|
|
334
|
+
COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
|
|
335
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens,
|
|
336
|
+
COALESCE(SUM(estimated_cost_usd), 0) AS estimated_cost_usd,
|
|
337
|
+
COALESCE(SUM(message_count), 0) AS message_count
|
|
338
|
+
FROM sessions
|
|
339
|
+
WHERE id IN (${placeholders})`)
|
|
340
|
+
.get(...chain);
|
|
341
|
+
const modelRow = db
|
|
342
|
+
.prepare("SELECT model FROM sessions WHERE id = ?")
|
|
343
|
+
.get(resolved);
|
|
344
|
+
const model = modelRow?.model?.trim() || null;
|
|
345
|
+
const estimatedCost = totals?.estimated_cost_usd;
|
|
346
|
+
const estimatedCostUsd = estimatedCost != null && Number.isFinite(estimatedCost) && estimatedCost > 0
|
|
347
|
+
? estimatedCost
|
|
348
|
+
: null;
|
|
349
|
+
return {
|
|
350
|
+
sessionId: trimmed,
|
|
351
|
+
resolvedSessionId: resolved,
|
|
352
|
+
model,
|
|
353
|
+
inputTokens: totals?.input_tokens ?? 0,
|
|
354
|
+
outputTokens: totals?.output_tokens ?? 0,
|
|
355
|
+
cacheReadTokens: totals?.cache_read_tokens ?? 0,
|
|
356
|
+
cacheWriteTokens: totals?.cache_write_tokens ?? 0,
|
|
357
|
+
reasoningTokens: totals?.reasoning_tokens ?? 0,
|
|
358
|
+
estimatedCostUsd,
|
|
359
|
+
messageCount: totals?.message_count ?? 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return emptySessionUsage(trimmed, trimmed);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
db.close();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
216
369
|
export function hermesStateDbExists() {
|
|
217
370
|
try {
|
|
218
371
|
readFileSync(hermesStateDbPath());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hermesSessionDb.test.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { getSessionUsage, isAutomatedHermesSessionPrompt } from "./hermesSessionDb.js";
|
|
8
|
+
function withHermesStateDb(setup, run) {
|
|
9
|
+
const home = mkdtempSync(join(tmpdir(), "hermes-session-db-test-"));
|
|
10
|
+
const previousHome = process.env.HERMES_HOME;
|
|
11
|
+
process.env.HERMES_HOME = home;
|
|
12
|
+
try {
|
|
13
|
+
mkdirSync(home, { recursive: true });
|
|
14
|
+
const db = new Database(join(home, "state.db"));
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE sessions (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
source TEXT NOT NULL,
|
|
19
|
+
user_id TEXT,
|
|
20
|
+
model TEXT,
|
|
21
|
+
model_config TEXT,
|
|
22
|
+
system_prompt TEXT,
|
|
23
|
+
parent_session_id TEXT,
|
|
24
|
+
started_at REAL NOT NULL,
|
|
25
|
+
ended_at REAL,
|
|
26
|
+
end_reason TEXT,
|
|
27
|
+
message_count INTEGER DEFAULT 0,
|
|
28
|
+
tool_call_count INTEGER DEFAULT 0,
|
|
29
|
+
input_tokens INTEGER DEFAULT 0,
|
|
30
|
+
output_tokens INTEGER DEFAULT 0,
|
|
31
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
32
|
+
cache_write_tokens INTEGER DEFAULT 0,
|
|
33
|
+
reasoning_tokens INTEGER DEFAULT 0,
|
|
34
|
+
estimated_cost_usd REAL,
|
|
35
|
+
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
|
|
36
|
+
);
|
|
37
|
+
CREATE TABLE messages (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
40
|
+
role TEXT NOT NULL,
|
|
41
|
+
content TEXT,
|
|
42
|
+
timestamp REAL NOT NULL
|
|
43
|
+
);
|
|
44
|
+
`);
|
|
45
|
+
setup(db);
|
|
46
|
+
db.close();
|
|
47
|
+
run();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (previousHome === undefined) {
|
|
51
|
+
delete process.env.HERMES_HOME;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
process.env.HERMES_HOME = previousHome;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
test("isAutomatedHermesSessionPrompt detects scheduled cron job instructions", () => {
|
|
59
|
+
const content = "[IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response is the delivery channel.]";
|
|
60
|
+
assert.equal(isAutomatedHermesSessionPrompt(content), true);
|
|
61
|
+
});
|
|
62
|
+
test("isAutomatedHermesSessionPrompt detects background skill runs", () => {
|
|
63
|
+
const content = "You are running as Hermes' background skill CURATOR. This is an UMBRELLA-BUILDING task.";
|
|
64
|
+
assert.equal(isAutomatedHermesSessionPrompt(content), true);
|
|
65
|
+
});
|
|
66
|
+
test("isAutomatedHermesSessionPrompt detects Hermes cron wrapper prompts", () => {
|
|
67
|
+
const content = `Cronjob Response: Morning feeds
|
|
68
|
+
-------------
|
|
69
|
+
Summarize today's AI news.
|
|
70
|
+
|
|
71
|
+
Note: The agent cannot see this message, and therefore cannot respond to it.`;
|
|
72
|
+
assert.equal(isAutomatedHermesSessionPrompt(content), true);
|
|
73
|
+
});
|
|
74
|
+
test("isAutomatedHermesSessionPrompt detects cron metadata headers", () => {
|
|
75
|
+
const content = `# Cron Job: Morning briefing
|
|
76
|
+
- Job ID: \`abc123\`
|
|
77
|
+
- Run Time: 2026-06-01 08:41:00`;
|
|
78
|
+
assert.equal(isAutomatedHermesSessionPrompt(content), true);
|
|
79
|
+
});
|
|
80
|
+
test("isAutomatedHermesSessionPrompt returns false for normal user chat", () => {
|
|
81
|
+
assert.equal(isAutomatedHermesSessionPrompt("Kan du lage en cron job som kjører hvert 5. minutt?"), false);
|
|
82
|
+
assert.equal(isAutomatedHermesSessionPrompt("Flott"), false);
|
|
83
|
+
assert.equal(isAutomatedHermesSessionPrompt("Sure, I can help with that."), false);
|
|
84
|
+
});
|
|
85
|
+
test("getSessionUsage returns zeros when state.db is missing", () => {
|
|
86
|
+
const previousHome = process.env.HERMES_HOME;
|
|
87
|
+
process.env.HERMES_HOME = mkdtempSync(join(tmpdir(), "hermes-session-db-missing-"));
|
|
88
|
+
try {
|
|
89
|
+
const usage = getSessionUsage("sess_missing");
|
|
90
|
+
assert.equal(usage.sessionId, "sess_missing");
|
|
91
|
+
assert.equal(usage.inputTokens, 0);
|
|
92
|
+
assert.equal(usage.outputTokens, 0);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (previousHome === undefined) {
|
|
96
|
+
delete process.env.HERMES_HOME;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
process.env.HERMES_HOME = previousHome;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
test("getSessionUsage sums token counters across parent and child sessions", () => {
|
|
104
|
+
withHermesStateDb((db) => {
|
|
105
|
+
const now = Date.now() / 1000;
|
|
106
|
+
db.prepare(`INSERT INTO sessions (
|
|
107
|
+
id, source, model, parent_session_id, started_at,
|
|
108
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
109
|
+
reasoning_tokens, estimated_cost_usd, message_count
|
|
110
|
+
) VALUES (?, 'cli', ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`).run("parent_sess", "anthropic/claude-sonnet-4", now, 10_000, 2_000, 500, 100, 50, 0.12, 8);
|
|
111
|
+
db.prepare(`INSERT INTO sessions (
|
|
112
|
+
id, source, model, parent_session_id, started_at,
|
|
113
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
114
|
+
reasoning_tokens, estimated_cost_usd, message_count
|
|
115
|
+
) VALUES (?, 'cli', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run("child_sess", "anthropic/claude-sonnet-4", "parent_sess", now + 1, 5_000, 1_000, 200, 40, 20, 0.05, 4);
|
|
116
|
+
db.prepare(`INSERT INTO messages (session_id, role, content, timestamp)
|
|
117
|
+
VALUES (?, 'user', ?, ?)`).run("child_sess", "hello", now + 2);
|
|
118
|
+
}, () => {
|
|
119
|
+
const usage = getSessionUsage("child_sess");
|
|
120
|
+
assert.equal(usage.resolvedSessionId, "child_sess");
|
|
121
|
+
assert.equal(usage.model, "anthropic/claude-sonnet-4");
|
|
122
|
+
assert.equal(usage.inputTokens, 15_000);
|
|
123
|
+
assert.equal(usage.outputTokens, 3_000);
|
|
124
|
+
assert.equal(usage.cacheReadTokens, 700);
|
|
125
|
+
assert.equal(usage.cacheWriteTokens, 140);
|
|
126
|
+
assert.equal(usage.reasoningTokens, 70);
|
|
127
|
+
assert.equal(usage.messageCount, 12);
|
|
128
|
+
assert.ok(usage.estimatedCostUsd != null && Math.abs(usage.estimatedCostUsd - 0.17) < 0.001);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
test("getSessionUsage resolves parent id to child session with messages", () => {
|
|
132
|
+
withHermesStateDb((db) => {
|
|
133
|
+
const now = Date.now() / 1000;
|
|
134
|
+
db.prepare(`INSERT INTO sessions (
|
|
135
|
+
id, source, model, parent_session_id, started_at,
|
|
136
|
+
input_tokens, output_tokens, message_count
|
|
137
|
+
) VALUES (?, 'cli', ?, NULL, ?, ?, ?, ?)`).run("parent_only", "hermes-agent", now, 100, 50, 2);
|
|
138
|
+
db.prepare(`INSERT INTO sessions (
|
|
139
|
+
id, source, model, parent_session_id, started_at,
|
|
140
|
+
input_tokens, output_tokens, message_count
|
|
141
|
+
) VALUES (?, 'cli', ?, ?, ?, ?, ?, ?)`).run("child_active", "hermes-agent", "parent_only", now + 1, 400, 200, 3);
|
|
142
|
+
db.prepare(`INSERT INTO messages (session_id, role, content, timestamp)
|
|
143
|
+
VALUES (?, 'user', ?, ?)`).run("child_active", "follow-up", now + 2);
|
|
144
|
+
}, () => {
|
|
145
|
+
const usage = getSessionUsage("parent_only");
|
|
146
|
+
assert.equal(usage.resolvedSessionId, "child_active");
|
|
147
|
+
assert.equal(usage.inputTokens, 500);
|
|
148
|
+
assert.equal(usage.outputTokens, 250);
|
|
149
|
+
assert.equal(usage.messageCount, 5);
|
|
150
|
+
});
|
|
151
|
+
});
|
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 { getSessionUsage, listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
|
|
11
|
+
export type { HermesSessionListEntry, HermesSessionUsage } 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,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACvF,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 { getSessionUsage, 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.27",
|
|
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/hermesSessionDb.test.ts src/shellSession.test.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"better-sqlite3": "^11.10.0",
|