@simplr-ai/connect 0.7.6 → 0.7.8

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 +292 -88
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  spawnSync
8
8
  } from "child_process";
9
9
  import {
10
- createHash,
10
+ createHash as createHash2,
11
11
  createPublicKey,
12
12
  randomBytes,
13
13
  randomUUID,
@@ -18,18 +18,52 @@ import {
18
18
  access,
19
19
  chmod,
20
20
  copyFile,
21
- mkdir,
21
+ mkdir as mkdir2,
22
22
  open,
23
- readFile,
24
- readdir,
23
+ readFile as readFile2,
24
+ readdir as readdir2,
25
25
  rename,
26
+ symlink,
26
27
  unlink,
27
28
  writeFile
28
29
  } from "fs/promises";
29
30
  import { arch, homedir, hostname, platform } from "os";
30
- import { basename, join as join2 } from "path";
31
+ import { basename, join as join3 } from "path";
31
32
  import { createServer } from "http";
32
33
 
34
+ // src/execution-tools.ts
35
+ import { createHash } from "crypto";
36
+ import { cp, mkdir, readFile, readdir, rm } from "fs/promises";
37
+ import { join, relative } from "path";
38
+ async function directoryFingerprint(directory) {
39
+ const hash = createHash("sha256");
40
+ const visit = async (current) => {
41
+ const entries = await readdir(current, { withFileTypes: true });
42
+ for (const entry of entries.sort(
43
+ (left, right) => left.name.localeCompare(right.name)
44
+ )) {
45
+ const path = join(current, entry.name);
46
+ const name = relative(directory, path);
47
+ hash.update(`${entry.isDirectory() ? "directory" : "file"}:${name}\0`);
48
+ if (entry.isDirectory()) await visit(path);
49
+ else if (entry.isFile()) hash.update(await readFile(path));
50
+ }
51
+ };
52
+ await visit(directory);
53
+ return hash.digest("hex");
54
+ }
55
+ async function syncDirectoryContents(source, destination) {
56
+ const sourceFingerprint = await directoryFingerprint(source);
57
+ const destinationFingerprint = await directoryFingerprint(destination).catch(
58
+ () => void 0
59
+ );
60
+ if (sourceFingerprint === destinationFingerprint) return false;
61
+ await rm(destination, { recursive: true, force: true });
62
+ await mkdir(destination, { recursive: true, mode: 448 });
63
+ await cp(source, destination, { recursive: true, force: true });
64
+ return true;
65
+ }
66
+
33
67
  // src/connections.ts
34
68
  function connectionKey(connection) {
35
69
  return `${connection.api_url}|${connection.organization_id}`;
@@ -86,7 +120,7 @@ function assertTenantEnvelope(connection, command) {
86
120
  }
87
121
 
88
122
  // src/hermes-config.ts
89
- import { join } from "path";
123
+ import { join as join2 } from "path";
90
124
  function organizationIdsNeedingMcpConfiguration(connections, configuredVersion, requiredVersion) {
91
125
  if (configuredVersion === requiredVersion) return [];
92
126
  return connections.filter((connection) => connection.setup_status === "ready").map((connection) => connection.organization_id);
@@ -102,10 +136,10 @@ function mcpServerNamesFromConfiguration(contents) {
102
136
  }
103
137
  function mcpConfigurationPaths(homeDirectory, workingDirectory, organizationId) {
104
138
  return [
105
- join(homeDirectory, ".codex", "config.toml"),
106
- join(homeDirectory, ".claude.json"),
107
- join(workingDirectory, ".mcp.json"),
108
- join(
139
+ join2(homeDirectory, ".codex", "config.toml"),
140
+ join2(homeDirectory, ".claude.json"),
141
+ join2(workingDirectory, ".mcp.json"),
142
+ join2(
109
143
  homeDirectory,
110
144
  ".hermes",
111
145
  "profiles",
@@ -126,6 +160,29 @@ function workOrderClassification(payload) {
126
160
  function awaitsFeaturePlanApproval(classification) {
127
161
  return classification.work_order_kind === "feature" && classification.source_type === "feedback" && classification.execution_mode === "plan";
128
162
  }
163
+ function isIncompleteCodeWorkResult(classification, output) {
164
+ if (!["fix", "fix_and_ship"].includes(String(classification.execution_mode)))
165
+ return false;
166
+ if (typeof output !== "string") return false;
167
+ const hasExecutorBlocker = /(?:executor|codex|claude code)[\s\S]{0,160}(?:blocked|unavailable|not installed|oauth)|(?:blocked|unavailable|not installed|oauth)[\s\S]{0,160}(?:executor|codex|claude code)/i.test(
168
+ output
169
+ ) || /stopped before code changes due to required executor blocker/i.test(output);
170
+ return /files changed:\s*(?:none|0)\b/i.test(output) && /pull request:\s*(?:none|not created)\b/i.test(output) && hasExecutorBlocker;
171
+ }
172
+ function isRecoverableIncompleteCompletion(entry) {
173
+ return entry.type === "run_hermes" && entry.status === "completed" && entry.source_type === "feedback" && isIncompleteCodeWorkResult(entry, entry.result);
174
+ }
175
+ function matchesCommandAcknowledgement(commandId, status, acknowledgement) {
176
+ return acknowledgement.command_id === commandId && acknowledgement.status === status;
177
+ }
178
+ function isHermesRunCancellation(message) {
179
+ return message === "Hermes run cancelled" || message === "Hermes run exceeded the 30 minute monitoring window and was stopped";
180
+ }
181
+ function shouldRestartHermesForExecutionTools(restartPending, journalStatuses) {
182
+ return restartPending && !journalStatuses.some(
183
+ (status) => ["preparing", "creating", "running", "waiting_approval"].includes(status)
184
+ );
185
+ }
129
186
  function workOrderPayload(classification) {
130
187
  return {
131
188
  ...classification.work_order_kind ? { work_order_kind: classification.work_order_kind } : {},
@@ -200,7 +257,14 @@ var KanbanReconciliationError = class extends Error {
200
257
  };
201
258
  var DeferredUpdateError = class extends Error {
202
259
  };
203
- var APP_VERSION = "0.7.6";
260
+ var HermesRequestError = class extends Error {
261
+ constructor(message, status) {
262
+ super(message);
263
+ this.status = status;
264
+ }
265
+ status;
266
+ };
267
+ var APP_VERSION = "0.7.8";
204
268
  var STANDALONE_EXECUTABLE = process.env.SIMPLR_CONNECT_STANDALONE === "true";
205
269
  var MANIFEST_PUBLIC_KEY = process.env.SIMPLR_CONNECT_MANIFEST_PUBLIC_KEY || "";
206
270
  var HERMES_INSTALL_COMMIT = "542e146b055f0d43767ac43cdb9e78054b240263";
@@ -216,45 +280,45 @@ var hermesTraceMutation = Promise.resolve();
216
280
  var serviceExecutableSnapshot;
217
281
  function stateDirectory() {
218
282
  if (platform() === "darwin")
219
- return join2(homedir(), "Library", "Application Support", "Simplr Connect");
283
+ return join3(homedir(), "Library", "Application Support", "Simplr Connect");
220
284
  if (platform() === "win32")
221
- return join2(
222
- process.env.APPDATA || join2(homedir(), "AppData", "Roaming"),
285
+ return join3(
286
+ process.env.APPDATA || join3(homedir(), "AppData", "Roaming"),
223
287
  "Simplr Connect"
224
288
  );
225
- return join2(
226
- process.env.XDG_CONFIG_HOME || join2(homedir(), ".config"),
289
+ return join3(
290
+ process.env.XDG_CONFIG_HOME || join3(homedir(), ".config"),
227
291
  "simplr-connect"
228
292
  );
229
293
  }
230
294
  function statePath() {
231
- return join2(stateDirectory(), "state.json");
295
+ return join3(stateDirectory(), "state.json");
232
296
  }
233
297
  function encryptedCredentialPath(workstationId) {
234
- return join2(
298
+ return join3(
235
299
  stateDirectory(),
236
300
  workstationId ? `credential-${workstationId}.bin` : "credential.bin"
237
301
  );
238
302
  }
239
303
  function encryptedAgentCredentialPath(workstationId) {
240
- return join2(stateDirectory(), `agent-credential-${workstationId}.bin`);
304
+ return join3(stateDirectory(), `agent-credential-${workstationId}.bin`);
241
305
  }
242
306
  function encryptedHermesCredentialPath() {
243
- return join2(stateDirectory(), "hermes-credential.bin");
307
+ return join3(stateDirectory(), "hermes-credential.bin");
244
308
  }
245
309
  function commandJournalPath() {
246
- return join2(stateDirectory(), "commands.json");
310
+ return join3(stateDirectory(), "commands.json");
247
311
  }
248
312
  function hermesTracePath() {
249
- return join2(stateDirectory(), "hermes-traces.json");
313
+ return join3(stateDirectory(), "hermes-traces.json");
250
314
  }
251
315
  function serviceLogPath() {
252
- return join2(stateDirectory(), "connect.log");
316
+ return join3(stateDirectory(), "connect.log");
253
317
  }
254
318
  function serviceExecutablePath() {
255
319
  if (!STANDALONE_EXECUTABLE)
256
- return join2(stateDirectory(), "simplr-connect.js");
257
- return join2(
320
+ return join3(stateDirectory(), "simplr-connect.js");
321
+ return join3(
258
322
  stateDirectory(),
259
323
  platform() === "win32" ? "simplr-connect.exe" : "simplr-connect"
260
324
  );
@@ -266,28 +330,28 @@ function updateRollbackPath() {
266
330
  return `${serviceExecutablePath()}.rollback`;
267
331
  }
268
332
  function supervisorLockPath() {
269
- return join2(stateDirectory(), "supervisor.lock");
333
+ return join3(stateDirectory(), "supervisor.lock");
270
334
  }
271
335
  function serviceLockPath() {
272
- return join2(stateDirectory(), "service.lock");
336
+ return join3(stateDirectory(), "service.lock");
273
337
  }
274
338
  function serviceDefinitionPath() {
275
339
  if (platform() === "darwin")
276
- return join2(
340
+ return join3(
277
341
  homedir(),
278
342
  "Library",
279
343
  "LaunchAgents",
280
344
  "ai.simplr.connect.plist"
281
345
  );
282
346
  if (platform() === "linux")
283
- return join2(
347
+ return join3(
284
348
  homedir(),
285
349
  ".config",
286
350
  "systemd",
287
351
  "user",
288
352
  "simplr-connect.service"
289
353
  );
290
- return join2(stateDirectory(), "simplr-connect-service.cmd");
354
+ return join3(stateDirectory(), "simplr-connect-service.cmd");
291
355
  }
292
356
  function apiUrl(override) {
293
357
  const value = override || process.env.SIMPLR_API_URL;
@@ -397,11 +461,11 @@ function hermesExecutableCandidates() {
397
461
  const localAppData = process.env.LOCALAPPDATA;
398
462
  if (localAppData)
399
463
  candidates.push(
400
- join2(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
464
+ join3(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
401
465
  );
402
466
  } else {
403
467
  candidates.push(
404
- join2(homedir(), ".local", "bin", "hermes"),
468
+ join3(homedir(), ".local", "bin", "hermes"),
405
469
  "/usr/local/bin/hermes"
406
470
  );
407
471
  if (platform() === "darwin") candidates.push("/opt/homebrew/bin/hermes");
@@ -722,15 +786,15 @@ async function existingDirectories(paths) {
722
786
  }
723
787
  async function detectSkills() {
724
788
  const paths = await existingDirectories([
725
- join2(homedir(), ".agents", "skills"),
726
- join2(homedir(), ".claude", "skills"),
727
- join2(process.cwd(), ".agents", "skills"),
728
- join2(process.cwd(), ".claude", "skills"),
729
- join2(homedir(), ".hermes", "skills")
789
+ join3(homedir(), ".agents", "skills"),
790
+ join3(homedir(), ".claude", "skills"),
791
+ join3(process.cwd(), ".agents", "skills"),
792
+ join3(process.cwd(), ".claude", "skills"),
793
+ join3(homedir(), ".hermes", "skills")
730
794
  ]);
731
795
  const skills = /* @__PURE__ */ new Map();
732
796
  for (const path of paths) {
733
- const entries = await readdir(path, { withFileTypes: true }).catch(
797
+ const entries = await readdir2(path, { withFileTypes: true }).catch(
734
798
  () => []
735
799
  );
736
800
  for (const entry of entries) {
@@ -751,7 +815,7 @@ async function detectMcpServers(state) {
751
815
  mcpConfigurationPaths(homedir(), process.cwd(), state.organization_id)
752
816
  );
753
817
  const contents = await Promise.all(
754
- configPaths.map((path) => readFile(path, "utf8").catch(() => ""))
818
+ configPaths.map((path) => readFile2(path, "utf8").catch(() => ""))
755
819
  );
756
820
  const names = new Set(mcpServerNamesFromConfiguration(contents));
757
821
  for (const content of contents) {
@@ -845,7 +909,7 @@ async function get(url, token) {
845
909
  return payload.content;
846
910
  }
847
911
  async function saveState(state) {
848
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
912
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
849
913
  const { device_token: legacyToken, ...safeState } = state;
850
914
  void legacyToken;
851
915
  await writeFile(statePath(), `${JSON.stringify(safeState, null, 2)}
@@ -856,7 +920,7 @@ async function saveState(state) {
856
920
  if (platform() !== "win32") await chmod(statePath(), 384);
857
921
  }
858
922
  async function loadState() {
859
- const stored = JSON.parse(await readFile(statePath(), "utf8"));
923
+ const stored = JSON.parse(await readFile2(statePath(), "utf8"));
860
924
  if (stored.device_token) {
861
925
  await storeCredential(stored.workstation_id, stored.device_token);
862
926
  delete stored.device_token;
@@ -867,13 +931,13 @@ async function loadState() {
867
931
  }
868
932
  async function loadCommandJournal() {
869
933
  try {
870
- return JSON.parse(await readFile(commandJournalPath(), "utf8"));
934
+ return JSON.parse(await readFile2(commandJournalPath(), "utf8"));
871
935
  } catch {
872
936
  return {};
873
937
  }
874
938
  }
875
939
  async function saveCommandJournal(journal) {
876
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
940
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
877
941
  const temporaryPath = `${commandJournalPath()}.${process.pid}.tmp`;
878
942
  await writeFile(temporaryPath, `${JSON.stringify(journal, null, 2)}
879
943
  `, {
@@ -903,13 +967,13 @@ async function removeCommandJournalEntry(commandId) {
903
967
  }
904
968
  async function loadHermesTraces() {
905
969
  try {
906
- return JSON.parse(await readFile(hermesTracePath(), "utf8"));
970
+ return JSON.parse(await readFile2(hermesTracePath(), "utf8"));
907
971
  } catch {
908
972
  return {};
909
973
  }
910
974
  }
911
975
  async function saveHermesTraces(traces) {
912
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
976
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
913
977
  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));
914
978
  const temporaryPath = `${hermesTracePath()}.${process.pid}.tmp`;
915
979
  await writeFile(temporaryPath, `${JSON.stringify(retained, null, 2)}
@@ -994,7 +1058,7 @@ async function storeMacKeychainCredential(account, service, token) {
994
1058
  throw new Error("Could not protect the credential in macOS Keychain");
995
1059
  }
996
1060
  async function storeCredential(workstationId, token) {
997
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
1061
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
998
1062
  if (platform() === "darwin") {
999
1063
  await storeMacKeychainCredential(workstationId, "Simplr Connect", token);
1000
1064
  return;
@@ -1072,10 +1136,10 @@ async function loadCredential(state) {
1072
1136
  );
1073
1137
  if (result.ok && result.output) return result.output;
1074
1138
  } else {
1075
- const encrypted = await readFile(
1139
+ const encrypted = await readFile2(
1076
1140
  encryptedCredentialPath(state.workstation_id),
1077
1141
  "utf8"
1078
- ).catch(() => readFile(encryptedCredentialPath(), "utf8"));
1142
+ ).catch(() => readFile2(encryptedCredentialPath(), "utf8"));
1079
1143
  const script = "$e=[Convert]::FromBase64String([Console]::In.ReadToEnd());$b=[Security.Cryptography.ProtectedData]::Unprotect($e,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($b)";
1080
1144
  const result = spawnSync(
1081
1145
  "powershell.exe",
@@ -1163,7 +1227,7 @@ async function loadAgentCredential(state) {
1163
1227
  );
1164
1228
  if (result.ok && result.output) return result.output;
1165
1229
  } else {
1166
- const encrypted = await readFile(
1230
+ const encrypted = await readFile2(
1167
1231
  encryptedAgentCredentialPath(state.workstation_id),
1168
1232
  "utf8"
1169
1233
  );
@@ -1295,7 +1359,7 @@ async function loadHermesCredential(state) {
1295
1359
  }
1296
1360
  }
1297
1361
  } else {
1298
- const encrypted = await readFile(encryptedHermesCredentialPath(), "utf8");
1362
+ const encrypted = await readFile2(encryptedHermesCredentialPath(), "utf8");
1299
1363
  const script = "$e=[Convert]::FromBase64String([Console]::In.ReadToEnd());$b=[Security.Cryptography.ProtectedData]::Unprotect($e,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($b)";
1300
1364
  const result = spawnSync(
1301
1365
  "powershell.exe",
@@ -1324,7 +1388,10 @@ async function hermesRequest(state, path, options = {}) {
1324
1388
  }
1325
1389
  );
1326
1390
  if (!response.ok)
1327
- throw new Error(`Hermes request failed (${response.status})`);
1391
+ throw new HermesRequestError(
1392
+ `Hermes request failed (${response.status})`,
1393
+ response.status
1394
+ );
1328
1395
  return await response.json();
1329
1396
  }
1330
1397
  async function streamHermesRunEvents(state, command, runId) {
@@ -1384,7 +1451,7 @@ async function streamHermesRunEvents(state, command, runId) {
1384
1451
  hermesRunApprovals.set(runId, {
1385
1452
  approval_command: approvalCommand,
1386
1453
  approval_tool: tool,
1387
- approval_fingerprint: createHash("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1454
+ approval_fingerprint: createHash2("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1388
1455
  approval_policy_decision: "approval_required"
1389
1456
  });
1390
1457
  }
@@ -1423,9 +1490,9 @@ async function waitForProcess(child) {
1423
1490
  });
1424
1491
  }
1425
1492
  async function installHermes() {
1426
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
1493
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
1427
1494
  const windows = platform() === "win32";
1428
- const scriptPath = join2(
1495
+ const scriptPath = join3(
1429
1496
  stateDirectory(),
1430
1497
  windows ? "hermes-install.ps1" : "hermes-install.sh"
1431
1498
  );
@@ -1482,14 +1549,14 @@ async function installHermes() {
1482
1549
  if (userPath.ok && userPath.output)
1483
1550
  process.env.PATH = `${userPath.output};${process.env.PATH || ""}`;
1484
1551
  } else {
1485
- process.env.PATH = `${join2(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
1552
+ process.env.PATH = `${join3(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
1486
1553
  }
1487
1554
  }
1488
1555
  async function configureHermesEnvironment(apiKey) {
1489
- const directory = join2(homedir(), ".hermes");
1490
- const path = join2(directory, ".env");
1491
- await mkdir(directory, { recursive: true, mode: 448 });
1492
- const existing = await readFile(path, "utf8").catch(() => "");
1556
+ const directory = join3(homedir(), ".hermes");
1557
+ const path = join3(directory, ".env");
1558
+ await mkdir2(directory, { recursive: true, mode: 448 });
1559
+ const existing = await readFile2(path, "utf8").catch(() => "");
1493
1560
  const settings = {
1494
1561
  API_SERVER_ENABLED: "true",
1495
1562
  API_SERVER_HOST: "127.0.0.1",
@@ -1519,15 +1586,15 @@ function hermesProfileArgs(state) {
1519
1586
  return ["--profile", hermesProfile(state.organization_id)];
1520
1587
  }
1521
1588
  async function configureSimplrMcp(state, executable) {
1522
- const profileDirectory = join2(
1589
+ const profileDirectory = join3(
1523
1590
  homedir(),
1524
1591
  ".hermes",
1525
1592
  "profiles",
1526
1593
  hermesProfile(state.organization_id)
1527
1594
  );
1528
- await mkdir(profileDirectory, { recursive: true, mode: 448 });
1529
- const environmentPath = join2(profileDirectory, ".env");
1530
- const existing = await readFile(environmentPath, "utf8").catch(() => "");
1595
+ await mkdir2(profileDirectory, { recursive: true, mode: 448 });
1596
+ const environmentPath = join3(profileDirectory, ".env");
1597
+ const existing = await readFile2(environmentPath, "utf8").catch(() => "");
1531
1598
  const settings = {
1532
1599
  SIMPLR_API_URL: state.api_url,
1533
1600
  SIMPLR_API_KEY: await ensureAgentCredential(state),
@@ -1549,7 +1616,7 @@ async function configureSimplrMcp(state, executable) {
1549
1616
  });
1550
1617
  if (platform() !== "win32") await chmod(environmentPath, 384);
1551
1618
  const profileArgs = hermesProfileArgs(state);
1552
- const wrapperPath = join2(profileDirectory, "simplr-mcp.mjs");
1619
+ const wrapperPath = join3(profileDirectory, "simplr-mcp.mjs");
1553
1620
  const wrapper = `#!/usr/bin/env node
1554
1621
  import { readFileSync } from "node:fs";
1555
1622
  import { spawn } from "node:child_process";
@@ -1608,8 +1675,8 @@ async function ensureSimplrMcpConfiguration(state) {
1608
1675
  state.mcp_configuration_version = SIMPLR_MCP_CONFIGURATION_VERSION;
1609
1676
  await saveState(state);
1610
1677
  }
1611
- async function installHermesSkill(state) {
1612
- const directory = join2(
1678
+ async function installHermesSkills(state) {
1679
+ const directory = join3(
1613
1680
  homedir(),
1614
1681
  ".hermes",
1615
1682
  "profiles",
@@ -1617,9 +1684,9 @@ async function installHermesSkill(state) {
1617
1684
  "skills",
1618
1685
  "simplr-remote-operations"
1619
1686
  );
1620
- await mkdir(directory, { recursive: true, mode: 448 });
1687
+ await mkdir2(directory, { recursive: true, mode: 448 });
1621
1688
  await writeFile(
1622
- join2(directory, "SKILL.md"),
1689
+ join3(directory, "SKILL.md"),
1623
1690
  `---
1624
1691
  name: simplr-remote-operations
1625
1692
  description: Execute a verified Simplr work-order envelope delivered by Simplr Connect. Do not use for direct local Hermes requests.
@@ -1653,6 +1720,79 @@ Never expose credentials, environment variables, tokens, keys, cookies, secret f
1653
1720
  `,
1654
1721
  { encoding: "utf8", mode: 384 }
1655
1722
  );
1723
+ const bundledCodexSources = [
1724
+ join3(homedir(), ".hermes", "skills", "autonomous-ai-agents", "codex"),
1725
+ join3(
1726
+ homedir(),
1727
+ ".hermes",
1728
+ "hermes-agent",
1729
+ "skills",
1730
+ "autonomous-ai-agents",
1731
+ "codex"
1732
+ )
1733
+ ];
1734
+ const codexSource = (await Promise.all(
1735
+ bundledCodexSources.map(async (candidate) => {
1736
+ try {
1737
+ await access(join3(candidate, "SKILL.md"), constants.R_OK);
1738
+ return candidate;
1739
+ } catch {
1740
+ return void 0;
1741
+ }
1742
+ })
1743
+ )).find(Boolean);
1744
+ if (!codexSource)
1745
+ throw new Error("Hermes bundled Codex skill is unavailable");
1746
+ const codexDestination = join3(
1747
+ homedir(),
1748
+ ".hermes",
1749
+ "profiles",
1750
+ hermesProfile(state.organization_id),
1751
+ "skills",
1752
+ "autonomous-ai-agents",
1753
+ "codex"
1754
+ );
1755
+ return syncDirectoryContents(codexSource, codexDestination);
1756
+ }
1757
+ async function ensureCodexExecutor() {
1758
+ if (platform() === "win32") return false;
1759
+ const localBin = join3(homedir(), ".local", "bin");
1760
+ const pathEntries = (process.env.PATH || "").split(":");
1761
+ const pathChanged = !pathEntries.includes(localBin);
1762
+ if (pathChanged)
1763
+ process.env.PATH = `${localBin}:${process.env.PATH || ""}`;
1764
+ if (commandVersion("codex")) return pathChanged;
1765
+ if (platform() !== "darwin") return pathChanged;
1766
+ const destination = join3(localBin, "codex");
1767
+ const candidates = [
1768
+ "/Applications/ChatGPT.app/Contents/Resources/codex",
1769
+ "/Applications/Codex.app/Contents/Resources/codex"
1770
+ ];
1771
+ for (const candidate of candidates) {
1772
+ try {
1773
+ await access(candidate, constants.X_OK);
1774
+ await mkdir2(localBin, { recursive: true, mode: 448 });
1775
+ await symlink(candidate, destination).catch((error) => {
1776
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
1777
+ throw error;
1778
+ });
1779
+ if (commandVersion("codex")) return true;
1780
+ } catch {
1781
+ }
1782
+ }
1783
+ return pathChanged;
1784
+ }
1785
+ async function ensureHermesExecutionTools(state) {
1786
+ let changed = await ensureCodexExecutor();
1787
+ for (const connection of state.connections.filter(
1788
+ (candidate) => candidate.setup_status === "ready"
1789
+ )) {
1790
+ const installed = await installHermesSkills(
1791
+ stateForConnection(state, connection)
1792
+ );
1793
+ changed = changed || installed;
1794
+ }
1795
+ return changed;
1656
1796
  }
1657
1797
  function isSimplrWorkOrder(command) {
1658
1798
  return ["incident", "bug", "feature"].includes(
@@ -1921,6 +2061,7 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
1921
2061
  );
1922
2062
  }
1923
2063
  async function setupHermes(state, interactiveOAuth) {
2064
+ await ensureCodexExecutor();
1924
2065
  let executable = detectHermesExecutable();
1925
2066
  if (!executable) {
1926
2067
  await installHermes();
@@ -1971,7 +2112,7 @@ async function setupHermes(state, interactiveOAuth) {
1971
2112
  );
1972
2113
  await configureHermesEnvironment(apiKey);
1973
2114
  await configureSimplrMcp(state, executable);
1974
- await installHermesSkill(state);
2115
+ await installHermesSkills(state);
1975
2116
  const multiplex = commandResult(
1976
2117
  executable,
1977
2118
  [
@@ -2232,11 +2373,15 @@ async function acknowledgeCommand(state, commandId, status, result) {
2232
2373
  let lastError;
2233
2374
  for (let attempt = 1; attempt <= 5; attempt += 1) {
2234
2375
  try {
2235
- await post(
2376
+ const acknowledged = await post(
2236
2377
  `${state.api_url}/v1/ai-workstations/commands/${commandId}/acknowledge`,
2237
2378
  { status, result: result.slice(0, 5e3) },
2238
2379
  token
2239
2380
  );
2381
+ if (!matchesCommandAcknowledgement(commandId, status, acknowledged))
2382
+ throw new Error(
2383
+ `Command acknowledgement remained ${acknowledged.status}`
2384
+ );
2240
2385
  await removeCommandJournalEntry(commandId);
2241
2386
  return;
2242
2387
  } catch (error) {
@@ -2304,7 +2449,11 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2304
2449
  `/v1/runs/${encodeURIComponent(runId)}`
2305
2450
  );
2306
2451
  consecutivePollFailures = 0;
2307
- } catch {
2452
+ } catch (error) {
2453
+ if (error instanceof HermesRequestError && error.status === 404)
2454
+ throw new Error(
2455
+ "Hermes run is no longer available after a gateway restart"
2456
+ );
2308
2457
  consecutivePollFailures += 1;
2309
2458
  if (consecutivePollFailures >= 3) {
2310
2459
  await repairConnector(state);
@@ -2315,6 +2464,23 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2315
2464
  }
2316
2465
  if (status.status === "completed") {
2317
2466
  const output = status.output || "Hermes completed the task";
2467
+ if (isIncompleteCodeWorkResult(
2468
+ workOrderClassification(command.payload),
2469
+ output
2470
+ )) {
2471
+ await updateCommandJournal({
2472
+ command_id: command.id,
2473
+ type: command.type,
2474
+ label: command.reason,
2475
+ ...workOrderClassification(command.payload),
2476
+ run_id: runId,
2477
+ status: "failed",
2478
+ result: output,
2479
+ started_at: startedAt,
2480
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2481
+ });
2482
+ throw new Error(output);
2483
+ }
2318
2484
  await updateHermesTrace(command, state, runId, {
2319
2485
  status: "completed",
2320
2486
  current_action: "Hermes run completed",
@@ -2408,7 +2574,7 @@ ${output}` : output
2408
2574
  );
2409
2575
  } catch (error) {
2410
2576
  const message = error instanceof Error ? error.message : "Hermes run failed";
2411
- const cancelled = /cancelled|stopped/i.test(message);
2577
+ const cancelled = isHermesRunCancellation(message);
2412
2578
  await updateHermesTrace(command, state, runId, {
2413
2579
  status: cancelled ? "cancelled" : "failed",
2414
2580
  current_action: cancelled ? "Hermes run stopped" : "Hermes run failed",
@@ -2426,6 +2592,8 @@ async function runHermesCommand(state, command) {
2426
2592
  const journal = await loadCommandJournal();
2427
2593
  const existing = journal[command.id];
2428
2594
  if (existing?.status === "completed") {
2595
+ if (isIncompleteCodeWorkResult(existing, existing.result))
2596
+ throw new Error(existing.result || "Hermes code work did not complete");
2429
2597
  const awaitingPlanApproval = awaitsFeaturePlanApproval(existing);
2430
2598
  updateSimplrKanbanTask(
2431
2599
  state,
@@ -2638,7 +2806,7 @@ async function stopManagedProcesses() {
2638
2806
  return `${processes.length} Simplr-managed AI process${processes.length === 1 ? "" : "es"} terminated`;
2639
2807
  }
2640
2808
  async function acquireProcessLock(lockPath, activeMessage) {
2641
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
2809
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
2642
2810
  const create = async () => {
2643
2811
  const handle = await open(lockPath, "wx", 384);
2644
2812
  await handle.writeFile(`${process.pid}
@@ -2651,7 +2819,7 @@ async function acquireProcessLock(lockPath, activeMessage) {
2651
2819
  if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
2652
2820
  throw error;
2653
2821
  const existingPid = Number.parseInt(
2654
- (await readFile(lockPath, "utf8").catch(() => "0")).trim(),
2822
+ (await readFile2(lockPath, "utf8").catch(() => "0")).trim(),
2655
2823
  10
2656
2824
  );
2657
2825
  let active = false;
@@ -2866,16 +3034,44 @@ async function resumeHermesRuns(state) {
2866
3034
  );
2867
3035
  }
2868
3036
  }
3037
+ async function reconcileIncompleteCompletedRuns(state) {
3038
+ const journal = await loadCommandJournal();
3039
+ for (const entry of Object.values(journal)) {
3040
+ if (!isRecoverableIncompleteCompletion(entry)) continue;
3041
+ const connection = entry.organization_id ? state.connections.find(
3042
+ (candidate) => candidate.organization_id === entry.organization_id && candidate.workstation_id === entry.workstation_id
3043
+ ) : state.connections.length === 1 ? state.connections[0] : void 0;
3044
+ if (!connection) continue;
3045
+ await acknowledgeCommand(
3046
+ stateForConnection(state, connection),
3047
+ entry.command_id,
3048
+ "failed",
3049
+ entry.result || "Hermes code work did not complete"
3050
+ );
3051
+ }
3052
+ }
3053
+ async function restartHermesForExecutionToolsWhenIdle(state) {
3054
+ const journal = await loadCommandJournal();
3055
+ if (!shouldRestartHermesForExecutionTools(
3056
+ Boolean(state.hermes_execution_tools_restart_pending),
3057
+ Object.values(journal).map((entry) => entry.status)
3058
+ ))
3059
+ return false;
3060
+ await restartHermesGateway(state);
3061
+ state.hermes_execution_tools_restart_pending = false;
3062
+ await saveState(state);
3063
+ return true;
3064
+ }
2869
3065
  async function serviceInstallationPresent() {
2870
3066
  try {
2871
3067
  await access(serviceExecutablePath(), constants.R_OK);
2872
3068
  if (serviceExecutableSnapshot) {
2873
- const deployed = await readFile(serviceExecutablePath());
2874
- const expectedHash = createHash("sha256").update(serviceExecutableSnapshot).digest("hex");
2875
- const deployedHash = createHash("sha256").update(deployed).digest("hex");
3069
+ const deployed = await readFile2(serviceExecutablePath());
3070
+ const expectedHash = createHash2("sha256").update(serviceExecutableSnapshot).digest("hex");
3071
+ const deployedHash = createHash2("sha256").update(deployed).digest("hex");
2876
3072
  if (expectedHash !== deployedHash) return false;
2877
3073
  }
2878
- const definition = await readFile(serviceDefinitionPath(), "utf8");
3074
+ const definition = await readFile2(serviceDefinitionPath(), "utf8");
2879
3075
  if (!definition.includes(serviceExecutablePath()) || !definition.includes("watch"))
2880
3076
  return false;
2881
3077
  if (platform() !== "win32") return true;
@@ -2917,10 +3113,17 @@ async function watch() {
2917
3113
  const state = await loadState();
2918
3114
  const executableSource = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
2919
3115
  if (executableSource)
2920
- serviceExecutableSnapshot = await readFile(executableSource).catch(
3116
+ serviceExecutableSnapshot = await readFile2(executableSource).catch(
2921
3117
  () => void 0
2922
3118
  );
3119
+ const executionToolsChanged = await ensureHermesExecutionTools(state);
2923
3120
  await ensureSimplrMcpConfiguration(state);
3121
+ if (executionToolsChanged) {
3122
+ state.hermes_execution_tools_restart_pending = true;
3123
+ await saveState(state);
3124
+ }
3125
+ await restartHermesForExecutionToolsWhenIdle(state);
3126
+ await reconcileIncompleteCompletedRuns(state);
2924
3127
  await resumeHermesRuns(state);
2925
3128
  let nextInventoryAt = 0;
2926
3129
  let nextServiceAuditAt = 0;
@@ -2933,6 +3136,7 @@ async function watch() {
2933
3136
  nextServiceAuditAt = Date.now() + 15 * 6e4;
2934
3137
  }
2935
3138
  if (Date.now() >= nextHermesAuditAt) {
3139
+ await restartHermesForExecutionToolsWhenIdle(state);
2936
3140
  for (const connection of state.connections.filter(
2937
3141
  (item) => item.setup_status !== "ready"
2938
3142
  )) {
@@ -3133,10 +3337,10 @@ function systemdValue(value) {
3133
3337
  }
3134
3338
  async function deployServiceExecutable() {
3135
3339
  const source = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
3136
- const executable = serviceExecutableSnapshot || (source ? await readFile(source) : void 0);
3340
+ const executable = serviceExecutableSnapshot || (source ? await readFile2(source) : void 0);
3137
3341
  if (!executable)
3138
3342
  throw new Error("Simplr Connect executable path is unavailable");
3139
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
3343
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
3140
3344
  await writeFile(serviceExecutablePath(), executable, {
3141
3345
  mode: 448
3142
3346
  });
@@ -3149,9 +3353,9 @@ async function installService(activate = true) {
3149
3353
  const nodePath = process.execPath;
3150
3354
  const connectPath = serviceExecutablePath();
3151
3355
  if (platform() === "darwin") {
3152
- const directory = join2(homedir(), "Library", "LaunchAgents");
3356
+ const directory = join3(homedir(), "Library", "LaunchAgents");
3153
3357
  const path = serviceDefinitionPath();
3154
- await mkdir(directory, { recursive: true, mode: 448 });
3358
+ await mkdir2(directory, { recursive: true, mode: 448 });
3155
3359
  const programArguments = STANDALONE_EXECUTABLE ? [connectPath, "watch"] : [nodePath, connectPath, "watch"];
3156
3360
  const plistArguments = programArguments.map((value) => `<string>${xmlValue(value)}</string>`).join("");
3157
3361
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -3182,9 +3386,9 @@ async function installService(activate = true) {
3182
3386
  return "Simplr Connect self-healing service installed";
3183
3387
  }
3184
3388
  if (platform() === "linux") {
3185
- const directory = join2(homedir(), ".config", "systemd", "user");
3389
+ const directory = join3(homedir(), ".config", "systemd", "user");
3186
3390
  const path = serviceDefinitionPath();
3187
- await mkdir(directory, { recursive: true, mode: 448 });
3391
+ await mkdir2(directory, { recursive: true, mode: 448 });
3188
3392
  const serviceCommand = STANDALONE_EXECUTABLE ? `${systemdValue(connectPath)} watch` : `${systemdValue(nodePath)} ${systemdValue(connectPath)} watch`;
3189
3393
  const unit = `[Unit]
3190
3394
  Description=Simplr Connect secure workstation proxy
@@ -3199,7 +3403,7 @@ RestartSec=10
3199
3403
  NoNewPrivileges=true
3200
3404
  PrivateTmp=true
3201
3405
  ProtectSystem=strict
3202
- ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join2(homedir(), ".hermes"))}
3406
+ ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join3(homedir(), ".hermes"))}
3203
3407
 
3204
3408
  [Install]
3205
3409
  WantedBy=default.target
@@ -3388,7 +3592,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
3388
3592
  status: "downloaded",
3389
3593
  artifact_sha256: artifact.sha256
3390
3594
  });
3391
- if (bytes.byteLength !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
3595
+ if (bytes.byteLength !== artifact.size || createHash2("sha256").update(bytes).digest("hex") !== artifact.sha256)
3392
3596
  throw new Error("Downloaded update failed integrity verification");
3393
3597
  await reportStage?.({
3394
3598
  status: "verified",
@@ -3435,7 +3639,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
3435
3639
  1e4
3436
3640
  );
3437
3641
  else {
3438
- const helperPath = join2(stateDirectory(), "complete-update.ps1");
3642
+ const helperPath = join3(stateDirectory(), "complete-update.ps1");
3439
3643
  const powershellValue = (value) => `'${value.replace(/'/g, "''")}'`;
3440
3644
  const helper = `$ErrorActionPreference = "Stop"\r
3441
3645
  & schtasks.exe /End /TN "Simplr Connect" | Out-Null\r
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplr-ai/connect",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Simplr Connect workstation enrollment and AI tool inventory companion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",