@simplr-ai/connect 0.7.2 → 0.7.3

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.
Files changed (2) hide show
  1. package/dist/index.js +194 -68
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -130,9 +130,11 @@ var SIMPLR_MCP_VERSION = "2.3.0";
130
130
  var SIMPLR_MCP_CONFIGURATION_VERSION = `operations@${SIMPLR_MCP_VERSION}`;
131
131
  var managedProcesses = /* @__PURE__ */ new Map();
132
132
  var hermesRuns = /* @__PURE__ */ new Map();
133
- var hermesApprovalStreams = /* @__PURE__ */ new Map();
133
+ var hermesRunStreams = /* @__PURE__ */ new Map();
134
+ var hermesRunApprovals = /* @__PURE__ */ new Map();
134
135
  var activeCommandIds = /* @__PURE__ */ new Set();
135
136
  var commandJournalMutation = Promise.resolve();
137
+ var hermesTraceMutation = Promise.resolve();
136
138
  var serviceExecutableSnapshot;
137
139
  function stateDirectory() {
138
140
  if (platform() === "darwin")
@@ -165,6 +167,9 @@ function encryptedHermesCredentialPath() {
165
167
  function commandJournalPath() {
166
168
  return join2(stateDirectory(), "commands.json");
167
169
  }
170
+ function hermesTracePath() {
171
+ return join2(stateDirectory(), "hermes-traces.json");
172
+ }
168
173
  function serviceLogPath() {
169
174
  return join2(stateDirectory(), "connect.log");
170
175
  }
@@ -818,6 +823,62 @@ async function removeCommandJournalEntry(commandId) {
818
823
  commandJournalMutation = mutation.catch(() => void 0);
819
824
  await mutation;
820
825
  }
826
+ async function loadHermesTraces() {
827
+ try {
828
+ return JSON.parse(await readFile(hermesTracePath(), "utf8"));
829
+ } catch {
830
+ return {};
831
+ }
832
+ }
833
+ async function saveHermesTraces(traces) {
834
+ await mkdir(stateDirectory(), { recursive: true, mode: 448 });
835
+ const retained = Object.fromEntries(Object.entries(traces).sort(([, left], [, right]) => Date.parse(right.last_event_at) - Date.parse(left.last_event_at)).slice(0, 25));
836
+ const temporaryPath = `${hermesTracePath()}.${process.pid}.tmp`;
837
+ await writeFile(temporaryPath, `${JSON.stringify(retained, null, 2)}
838
+ `, { encoding: "utf8", mode: 384 });
839
+ if (platform() !== "win32") await chmod(temporaryPath, 384);
840
+ await rename(temporaryPath, hermesTracePath());
841
+ }
842
+ async function updateHermesTrace(command, state, runId, update, event) {
843
+ const mutation = hermesTraceMutation.then(async () => {
844
+ const traces = await loadHermesTraces();
845
+ const now = (/* @__PURE__ */ new Date()).toISOString();
846
+ const existing = traces[command.id];
847
+ const events = event ? [...existing?.events || [], event].slice(-40) : existing?.events || [];
848
+ traces[command.id] = {
849
+ ...existing,
850
+ ...update,
851
+ command_id: command.id,
852
+ organization_id: state.organization_id,
853
+ workstation_id: state.workstation_id,
854
+ run_id: runId,
855
+ status: update.status || existing?.status || "running",
856
+ current_action: update.current_action || existing?.current_action || "Starting Hermes",
857
+ next_step: update.next_step || existing?.next_step || "Waiting for the first local activity",
858
+ last_event_at: event?.created_at || update.last_event_at || existing?.last_event_at || now,
859
+ supported: update.supported ?? existing?.supported ?? true,
860
+ events
861
+ };
862
+ await saveHermesTraces(traces);
863
+ });
864
+ hermesTraceMutation = mutation.catch(() => void 0);
865
+ await mutation;
866
+ }
867
+ function localTraceText(value, maximumLength = 500) {
868
+ if (typeof value !== "string") return "";
869
+ return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, maximumLength);
870
+ }
871
+ function hermesTraceEvent(runId, sequence, type, title, detail, timestamp, durationMs) {
872
+ const seconds = typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0 && timestamp < 864e10 ? timestamp : Date.now() / 1e3;
873
+ return {
874
+ id: `${runId}:${sequence}`,
875
+ type,
876
+ title: localTraceText(title, 160),
877
+ ...detail ? { detail: localTraceText(detail) } : {},
878
+ created_at: new Date(seconds * 1e3).toISOString(),
879
+ ...durationMs === void 0 ? {} : { duration_ms: durationMs }
880
+ };
881
+ }
821
882
  function credentialService(workstationId) {
822
883
  return `simplr-connect:${workstationId}`;
823
884
  }
@@ -1188,76 +1249,94 @@ async function hermesRequest(state, path, options = {}) {
1188
1249
  throw new Error(`Hermes request failed (${response.status})`);
1189
1250
  return await response.json();
1190
1251
  }
1191
- async function readHermesApproval(state, runId) {
1192
- let stream = hermesApprovalStreams.get(runId);
1193
- if (!stream) {
1194
- const token = await loadHermesCredential(state);
1195
- const controller = new AbortController();
1196
- const startupTimeout = setTimeout(() => controller.abort(), 5e3);
1197
- const response = await fetch(
1198
- `http://127.0.0.1:8642/p/${hermesProfile(state.organization_id)}/v1/runs/${encodeURIComponent(runId)}/events`,
1199
- {
1200
- headers: {
1201
- Authorization: `Bearer ${token}`,
1202
- Accept: "text/event-stream"
1203
- },
1204
- signal: controller.signal
1205
- }
1206
- );
1207
- if (!response.ok || !response.body) {
1208
- clearTimeout(startupTimeout);
1209
- controller.abort();
1210
- return {};
1211
- }
1212
- stream = {
1213
- controller,
1214
- reader: response.body.getReader(),
1215
- decoder: new TextDecoder(),
1216
- buffered: "",
1217
- startup_timeout: startupTimeout
1218
- };
1219
- hermesApprovalStreams.set(runId, stream);
1252
+ async function streamHermesRunEvents(state, command, runId) {
1253
+ const controller = new AbortController();
1254
+ hermesRunStreams.set(runId, controller);
1255
+ const token = await loadHermesCredential(state);
1256
+ const response = await fetch(
1257
+ `http://127.0.0.1:8642/p/${hermesProfile(state.organization_id)}/v1/runs/${encodeURIComponent(runId)}/events`,
1258
+ { headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" }, signal: controller.signal }
1259
+ );
1260
+ if (!response.ok || !response.body) {
1261
+ await updateHermesTrace(command, state, runId, { supported: false, current_action: "Hermes is running", next_step: "Live detail is unavailable; local status polling remains active" });
1262
+ return;
1220
1263
  }
1264
+ const reader = response.body.getReader();
1265
+ const decoder = new TextDecoder();
1266
+ let buffered = "";
1267
+ let sequence = 0;
1221
1268
  try {
1222
1269
  while (true) {
1223
- const boundary = stream.buffered.indexOf("\n\n");
1224
- if (boundary < 0) {
1225
- const chunk = await stream.reader.read();
1226
- if (chunk.done) return {};
1227
- stream.buffered += stream.decoder.decode(chunk.value, { stream: true });
1228
- continue;
1270
+ const chunk = await reader.read();
1271
+ if (chunk.done) break;
1272
+ buffered += decoder.decode(chunk.value, { stream: true }).replace(/\r\n/g, "\n");
1273
+ let boundary = buffered.indexOf("\n\n");
1274
+ while (boundary >= 0) {
1275
+ const record = buffered.slice(0, boundary);
1276
+ buffered = buffered.slice(boundary + 2);
1277
+ boundary = buffered.indexOf("\n\n");
1278
+ const data = record.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
1279
+ if (!data) continue;
1280
+ let raw;
1281
+ try {
1282
+ const parsed = JSON.parse(data);
1283
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
1284
+ raw = parsed;
1285
+ } catch {
1286
+ continue;
1287
+ }
1288
+ const eventType = localTraceText(raw.event, 80);
1289
+ const rawTool = localTraceText(raw.tool, 100);
1290
+ const tool = /^[a-zA-Z0-9_.:-]{1,100}$/.test(rawTool) ? rawTool : "Hermes tool";
1291
+ sequence += 1;
1292
+ if (eventType === "reasoning.available") {
1293
+ const detail = localTraceText(raw.text);
1294
+ if (!detail) continue;
1295
+ await updateHermesTrace(command, state, runId, { current_action: "Reviewing the plan", next_step: detail, blocker: void 0 }, hermesTraceEvent(runId, sequence, "reasoning", "Reasoning summary", detail, raw.timestamp));
1296
+ } else if (eventType === "tool.started") {
1297
+ const preview = localTraceText(raw.preview);
1298
+ await updateHermesTrace(command, state, runId, { current_action: `Running ${tool}`, next_step: `Waiting for ${tool} to finish`, blocker: void 0 }, hermesTraceEvent(runId, sequence, "tool", `Started ${tool}`, preview || void 0, raw.timestamp));
1299
+ } else if (eventType === "tool.completed") {
1300
+ const duration = typeof raw.duration === "number" && Number.isFinite(raw.duration) ? Math.max(0, Math.round(raw.duration * 1e3)) : void 0;
1301
+ const failed = raw.error === true;
1302
+ await updateHermesTrace(command, state, runId, { current_action: failed ? `${tool} reported an error` : `Finished ${tool}`, next_step: "Hermes is deciding the next action", ...failed ? { blocker: `${tool} reported an error` } : { blocker: void 0 } }, hermesTraceEvent(runId, sequence, failed ? "error" : "tool", failed ? `${tool} failed` : `Finished ${tool}`, void 0, raw.timestamp, duration));
1303
+ } else if (eventType === "approval.request") {
1304
+ const approvalCommand = localTraceText(raw.command, 1e3);
1305
+ if (approvalCommand) {
1306
+ hermesRunApprovals.set(runId, {
1307
+ approval_command: approvalCommand,
1308
+ approval_tool: tool,
1309
+ approval_fingerprint: createHash("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1310
+ approval_policy_decision: "approval_required"
1311
+ });
1312
+ }
1313
+ await updateHermesTrace(command, state, runId, { status: "waiting_approval", current_action: "Waiting for your approval", next_step: `Approve or reject ${tool}`, blocker: "Human approval is required" }, hermesTraceEvent(runId, sequence, "approval", "Approval required", approvalCommand || tool, raw.timestamp));
1314
+ } else if (["run.completed", "run.failed", "run.cancelled"].includes(eventType)) {
1315
+ const failed = eventType === "run.failed";
1316
+ const cancelled = eventType === "run.cancelled";
1317
+ const status = failed ? "failed" : cancelled ? "cancelled" : "completed";
1318
+ const title = failed ? "Hermes run failed" : cancelled ? "Hermes run stopped" : "Hermes run completed";
1319
+ await updateHermesTrace(command, state, runId, { status, current_action: title, next_step: failed ? "Review the failure and decide whether to retry" : cancelled ? "Continue in Chat or start a new run" : "Review the result and pull request", ...failed ? { blocker: localTraceText(raw.error, 300) || "Hermes reported a failure" } : { blocker: void 0 } }, hermesTraceEvent(runId, sequence, failed ? "error" : "success", title, failed ? localTraceText(raw.error, 300) : void 0, raw.timestamp));
1320
+ }
1229
1321
  }
1230
- const record = stream.buffered.slice(0, boundary);
1231
- stream.buffered = stream.buffered.slice(boundary + 2);
1232
- const data = record.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
1233
- if (!data) continue;
1234
- const event = JSON.parse(data);
1235
- if (event.event !== "approval.request") continue;
1236
- clearTimeout(stream.startup_timeout);
1237
- const tool = typeof event.tool === "string" && /^[a-zA-Z0-9_.:-]{1,100}$/.test(event.tool) ? event.tool : "protected tool";
1238
- const command = typeof event.command === "string" ? event.command.trim().slice(0, 1e3) : "";
1239
- if (!command) return {};
1240
- const fingerprint = createHash("sha256").update(JSON.stringify({ run_id: runId, tool, command })).digest("hex");
1241
- return {
1242
- approval_command: command,
1243
- approval_tool: tool,
1244
- approval_fingerprint: fingerprint,
1245
- approval_policy_decision: "approval_required"
1246
- };
1247
1322
  }
1248
- } catch {
1249
- clearTimeout(stream.startup_timeout);
1250
- hermesApprovalStreams.delete(runId);
1251
- return {};
1323
+ } finally {
1324
+ await reader.cancel().catch(() => void 0);
1325
+ if (hermesRunStreams.get(runId) === controller) hermesRunStreams.delete(runId);
1252
1326
  }
1253
1327
  }
1254
- function closeHermesApprovalStream(runId) {
1255
- const stream = hermesApprovalStreams.get(runId);
1256
- if (!stream) return;
1257
- clearTimeout(stream.startup_timeout);
1258
- stream.controller.abort();
1259
- void stream.reader.cancel().catch(() => void 0);
1260
- hermesApprovalStreams.delete(runId);
1328
+ async function readHermesApproval(runId) {
1329
+ for (let attempt = 0; attempt < 20; attempt += 1) {
1330
+ const approval = hermesRunApprovals.get(runId);
1331
+ if (approval) return approval;
1332
+ await new Promise((resolve) => setTimeout(resolve, 250));
1333
+ }
1334
+ return {};
1335
+ }
1336
+ function closeHermesRunStream(runId) {
1337
+ hermesRunStreams.get(runId)?.abort();
1338
+ hermesRunStreams.delete(runId);
1339
+ hermesRunApprovals.delete(runId);
1261
1340
  }
1262
1341
  async function waitForProcess(child) {
1263
1342
  return new Promise((resolve, reject) => {
@@ -2038,6 +2117,22 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2038
2117
  started_at: startedAt
2039
2118
  };
2040
2119
  hermesRuns.set(runId, run);
2120
+ await updateHermesTrace(command, state, runId, {
2121
+ status: "running",
2122
+ current_action: "Starting Hermes",
2123
+ next_step: "Waiting for the first local activity",
2124
+ blocker: void 0,
2125
+ supported: true
2126
+ }, hermesTraceEvent(runId, 0, "status", "Hermes run started", void 0, Date.now() / 1e3));
2127
+ const traceStream = streamHermesRunEvents(state, command, runId).catch(async () => {
2128
+ if (hermesRunStreams.has(runId)) {
2129
+ await updateHermesTrace(command, state, runId, {
2130
+ supported: false,
2131
+ current_action: "Hermes is running",
2132
+ next_step: "Live detail is unavailable; local status polling remains active"
2133
+ });
2134
+ }
2135
+ });
2041
2136
  let lastReportedStatus;
2042
2137
  let consecutivePollFailures = 0;
2043
2138
  try {
@@ -2061,6 +2156,13 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2061
2156
  }
2062
2157
  if (status.status === "completed") {
2063
2158
  const output = status.output || "Hermes completed the task";
2159
+ await updateHermesTrace(command, state, runId, {
2160
+ status: "completed",
2161
+ current_action: "Hermes run completed",
2162
+ next_step: "Review the result and pull request",
2163
+ blocker: void 0,
2164
+ last_event_at: (/* @__PURE__ */ new Date()).toISOString()
2165
+ });
2064
2166
  await updateCommandJournal({
2065
2167
  command_id: command.id,
2066
2168
  type: command.type,
@@ -2089,7 +2191,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2089
2191
  started_at: startedAt,
2090
2192
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
2091
2193
  });
2092
- const approval = run.status === "waiting_approval" ? await readHermesApproval(state, runId).catch(() => ({})) : {};
2194
+ const approval = run.status === "waiting_approval" ? await readHermesApproval(runId).catch(() => ({})) : {};
2093
2195
  if (run.status === "waiting_approval" && approval.approval_fingerprint) {
2094
2196
  await updateCommandJournal({
2095
2197
  command_id: command.id,
@@ -2132,8 +2234,19 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2132
2234
  throw new Error(
2133
2235
  "Hermes run exceeded the 30 minute monitoring window and was stopped"
2134
2236
  );
2237
+ } catch (error) {
2238
+ const message = error instanceof Error ? error.message : "Hermes run failed";
2239
+ const cancelled = /cancelled|stopped/i.test(message);
2240
+ await updateHermesTrace(command, state, runId, {
2241
+ status: cancelled ? "cancelled" : "failed",
2242
+ current_action: cancelled ? "Hermes run stopped" : "Hermes run failed",
2243
+ next_step: cancelled ? "Continue in Chat or start a new run" : "Review the failure and decide whether to retry",
2244
+ ...cancelled ? { blocker: void 0 } : { blocker: localTraceText(message, 300) }
2245
+ }, hermesTraceEvent(runId, Date.now(), cancelled ? "status" : "error", cancelled ? "Hermes run stopped" : "Hermes run failed", cancelled ? void 0 : message, Date.now() / 1e3));
2246
+ throw error;
2135
2247
  } finally {
2136
- closeHermesApprovalStream(runId);
2248
+ closeHermesRunStream(runId);
2249
+ await traceStream;
2137
2250
  hermesRuns.delete(runId);
2138
2251
  }
2139
2252
  }
@@ -2517,7 +2630,7 @@ function dispatchCommand(state, command, inventoryAlreadySynced) {
2517
2630
  );
2518
2631
  if (run) {
2519
2632
  void (async () => {
2520
- const approval = run.status === "waiting_approval" ? await readHermesApproval(state, run.id).catch(() => ({})) : {};
2633
+ const approval = run.status === "waiting_approval" ? await readHermesApproval(run.id).catch(() => ({})) : {};
2521
2634
  await reportCommandProgress(
2522
2635
  state,
2523
2636
  command.id,
@@ -3361,6 +3474,19 @@ async function main() {
3361
3474
  token
3362
3475
  );
3363
3476
  process.stdout.write(`${JSON.stringify(overview)}
3477
+ `);
3478
+ return;
3479
+ }
3480
+ if (command === "work-order-traces") {
3481
+ const organizationId = argument("--organization-id");
3482
+ if (!isUuid(organizationId))
3483
+ throw new Error("Use: simplr-connect work-order-traces --organization-id <uuid>");
3484
+ const state = await loadState();
3485
+ const connection = state.connections.find((item) => item.organization_id === organizationId);
3486
+ if (!connection) throw new Error("Organization is not connected");
3487
+ await hermesTraceMutation;
3488
+ const traces = Object.values(await loadHermesTraces()).filter((trace) => trace.organization_id === connection.organization_id && trace.workstation_id === connection.workstation_id).sort((left, right) => Date.parse(right.last_event_at) - Date.parse(left.last_event_at));
3489
+ process.stdout.write(`${JSON.stringify({ traces, updated_at: (/* @__PURE__ */ new Date()).toISOString() })}
3364
3490
  `);
3365
3491
  return;
3366
3492
  }
@@ -3466,7 +3592,7 @@ async function main() {
3466
3592
  return;
3467
3593
  }
3468
3594
  process.stdout.write(
3469
- "Simplr Connect\n\nCommands:\n wizard\n enroll --api-url <url> --code <code> [--setup-hermes] [--install-service]\n hermes-setup\n status\n service-install\n repair\n update-check\n update\n sync\n watch\n run -- <ai-command> [arguments]\n"
3595
+ "Simplr Connect\n\nCommands:\n wizard\n enroll --api-url <url> --code <code> [--setup-hermes] [--install-service]\n hermes-setup\n status\n overview --organization-id <uuid>\n work-order-traces --organization-id <uuid>\n service-install\n repair\n update-check\n update\n sync\n watch\n run -- <ai-command> [arguments]\n"
3470
3596
  );
3471
3597
  }
3472
3598
  main().catch(async (error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplr-ai/connect",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Simplr Connect workstation enrollment and AI tool inventory companion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",