@simplr-ai/connect 0.7.6 → 0.7.7

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 +254 -86
  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,24 @@ 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
+ }
129
181
  function workOrderPayload(classification) {
130
182
  return {
131
183
  ...classification.work_order_kind ? { work_order_kind: classification.work_order_kind } : {},
@@ -200,7 +252,7 @@ var KanbanReconciliationError = class extends Error {
200
252
  };
201
253
  var DeferredUpdateError = class extends Error {
202
254
  };
203
- var APP_VERSION = "0.7.6";
255
+ var APP_VERSION = "0.7.7";
204
256
  var STANDALONE_EXECUTABLE = process.env.SIMPLR_CONNECT_STANDALONE === "true";
205
257
  var MANIFEST_PUBLIC_KEY = process.env.SIMPLR_CONNECT_MANIFEST_PUBLIC_KEY || "";
206
258
  var HERMES_INSTALL_COMMIT = "542e146b055f0d43767ac43cdb9e78054b240263";
@@ -216,45 +268,45 @@ var hermesTraceMutation = Promise.resolve();
216
268
  var serviceExecutableSnapshot;
217
269
  function stateDirectory() {
218
270
  if (platform() === "darwin")
219
- return join2(homedir(), "Library", "Application Support", "Simplr Connect");
271
+ return join3(homedir(), "Library", "Application Support", "Simplr Connect");
220
272
  if (platform() === "win32")
221
- return join2(
222
- process.env.APPDATA || join2(homedir(), "AppData", "Roaming"),
273
+ return join3(
274
+ process.env.APPDATA || join3(homedir(), "AppData", "Roaming"),
223
275
  "Simplr Connect"
224
276
  );
225
- return join2(
226
- process.env.XDG_CONFIG_HOME || join2(homedir(), ".config"),
277
+ return join3(
278
+ process.env.XDG_CONFIG_HOME || join3(homedir(), ".config"),
227
279
  "simplr-connect"
228
280
  );
229
281
  }
230
282
  function statePath() {
231
- return join2(stateDirectory(), "state.json");
283
+ return join3(stateDirectory(), "state.json");
232
284
  }
233
285
  function encryptedCredentialPath(workstationId) {
234
- return join2(
286
+ return join3(
235
287
  stateDirectory(),
236
288
  workstationId ? `credential-${workstationId}.bin` : "credential.bin"
237
289
  );
238
290
  }
239
291
  function encryptedAgentCredentialPath(workstationId) {
240
- return join2(stateDirectory(), `agent-credential-${workstationId}.bin`);
292
+ return join3(stateDirectory(), `agent-credential-${workstationId}.bin`);
241
293
  }
242
294
  function encryptedHermesCredentialPath() {
243
- return join2(stateDirectory(), "hermes-credential.bin");
295
+ return join3(stateDirectory(), "hermes-credential.bin");
244
296
  }
245
297
  function commandJournalPath() {
246
- return join2(stateDirectory(), "commands.json");
298
+ return join3(stateDirectory(), "commands.json");
247
299
  }
248
300
  function hermesTracePath() {
249
- return join2(stateDirectory(), "hermes-traces.json");
301
+ return join3(stateDirectory(), "hermes-traces.json");
250
302
  }
251
303
  function serviceLogPath() {
252
- return join2(stateDirectory(), "connect.log");
304
+ return join3(stateDirectory(), "connect.log");
253
305
  }
254
306
  function serviceExecutablePath() {
255
307
  if (!STANDALONE_EXECUTABLE)
256
- return join2(stateDirectory(), "simplr-connect.js");
257
- return join2(
308
+ return join3(stateDirectory(), "simplr-connect.js");
309
+ return join3(
258
310
  stateDirectory(),
259
311
  platform() === "win32" ? "simplr-connect.exe" : "simplr-connect"
260
312
  );
@@ -266,28 +318,28 @@ function updateRollbackPath() {
266
318
  return `${serviceExecutablePath()}.rollback`;
267
319
  }
268
320
  function supervisorLockPath() {
269
- return join2(stateDirectory(), "supervisor.lock");
321
+ return join3(stateDirectory(), "supervisor.lock");
270
322
  }
271
323
  function serviceLockPath() {
272
- return join2(stateDirectory(), "service.lock");
324
+ return join3(stateDirectory(), "service.lock");
273
325
  }
274
326
  function serviceDefinitionPath() {
275
327
  if (platform() === "darwin")
276
- return join2(
328
+ return join3(
277
329
  homedir(),
278
330
  "Library",
279
331
  "LaunchAgents",
280
332
  "ai.simplr.connect.plist"
281
333
  );
282
334
  if (platform() === "linux")
283
- return join2(
335
+ return join3(
284
336
  homedir(),
285
337
  ".config",
286
338
  "systemd",
287
339
  "user",
288
340
  "simplr-connect.service"
289
341
  );
290
- return join2(stateDirectory(), "simplr-connect-service.cmd");
342
+ return join3(stateDirectory(), "simplr-connect-service.cmd");
291
343
  }
292
344
  function apiUrl(override) {
293
345
  const value = override || process.env.SIMPLR_API_URL;
@@ -397,11 +449,11 @@ function hermesExecutableCandidates() {
397
449
  const localAppData = process.env.LOCALAPPDATA;
398
450
  if (localAppData)
399
451
  candidates.push(
400
- join2(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
452
+ join3(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
401
453
  );
402
454
  } else {
403
455
  candidates.push(
404
- join2(homedir(), ".local", "bin", "hermes"),
456
+ join3(homedir(), ".local", "bin", "hermes"),
405
457
  "/usr/local/bin/hermes"
406
458
  );
407
459
  if (platform() === "darwin") candidates.push("/opt/homebrew/bin/hermes");
@@ -722,15 +774,15 @@ async function existingDirectories(paths) {
722
774
  }
723
775
  async function detectSkills() {
724
776
  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")
777
+ join3(homedir(), ".agents", "skills"),
778
+ join3(homedir(), ".claude", "skills"),
779
+ join3(process.cwd(), ".agents", "skills"),
780
+ join3(process.cwd(), ".claude", "skills"),
781
+ join3(homedir(), ".hermes", "skills")
730
782
  ]);
731
783
  const skills = /* @__PURE__ */ new Map();
732
784
  for (const path of paths) {
733
- const entries = await readdir(path, { withFileTypes: true }).catch(
785
+ const entries = await readdir2(path, { withFileTypes: true }).catch(
734
786
  () => []
735
787
  );
736
788
  for (const entry of entries) {
@@ -751,7 +803,7 @@ async function detectMcpServers(state) {
751
803
  mcpConfigurationPaths(homedir(), process.cwd(), state.organization_id)
752
804
  );
753
805
  const contents = await Promise.all(
754
- configPaths.map((path) => readFile(path, "utf8").catch(() => ""))
806
+ configPaths.map((path) => readFile2(path, "utf8").catch(() => ""))
755
807
  );
756
808
  const names = new Set(mcpServerNamesFromConfiguration(contents));
757
809
  for (const content of contents) {
@@ -845,7 +897,7 @@ async function get(url, token) {
845
897
  return payload.content;
846
898
  }
847
899
  async function saveState(state) {
848
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
900
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
849
901
  const { device_token: legacyToken, ...safeState } = state;
850
902
  void legacyToken;
851
903
  await writeFile(statePath(), `${JSON.stringify(safeState, null, 2)}
@@ -856,7 +908,7 @@ async function saveState(state) {
856
908
  if (platform() !== "win32") await chmod(statePath(), 384);
857
909
  }
858
910
  async function loadState() {
859
- const stored = JSON.parse(await readFile(statePath(), "utf8"));
911
+ const stored = JSON.parse(await readFile2(statePath(), "utf8"));
860
912
  if (stored.device_token) {
861
913
  await storeCredential(stored.workstation_id, stored.device_token);
862
914
  delete stored.device_token;
@@ -867,13 +919,13 @@ async function loadState() {
867
919
  }
868
920
  async function loadCommandJournal() {
869
921
  try {
870
- return JSON.parse(await readFile(commandJournalPath(), "utf8"));
922
+ return JSON.parse(await readFile2(commandJournalPath(), "utf8"));
871
923
  } catch {
872
924
  return {};
873
925
  }
874
926
  }
875
927
  async function saveCommandJournal(journal) {
876
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
928
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
877
929
  const temporaryPath = `${commandJournalPath()}.${process.pid}.tmp`;
878
930
  await writeFile(temporaryPath, `${JSON.stringify(journal, null, 2)}
879
931
  `, {
@@ -903,13 +955,13 @@ async function removeCommandJournalEntry(commandId) {
903
955
  }
904
956
  async function loadHermesTraces() {
905
957
  try {
906
- return JSON.parse(await readFile(hermesTracePath(), "utf8"));
958
+ return JSON.parse(await readFile2(hermesTracePath(), "utf8"));
907
959
  } catch {
908
960
  return {};
909
961
  }
910
962
  }
911
963
  async function saveHermesTraces(traces) {
912
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
964
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
913
965
  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
966
  const temporaryPath = `${hermesTracePath()}.${process.pid}.tmp`;
915
967
  await writeFile(temporaryPath, `${JSON.stringify(retained, null, 2)}
@@ -994,7 +1046,7 @@ async function storeMacKeychainCredential(account, service, token) {
994
1046
  throw new Error("Could not protect the credential in macOS Keychain");
995
1047
  }
996
1048
  async function storeCredential(workstationId, token) {
997
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
1049
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
998
1050
  if (platform() === "darwin") {
999
1051
  await storeMacKeychainCredential(workstationId, "Simplr Connect", token);
1000
1052
  return;
@@ -1072,10 +1124,10 @@ async function loadCredential(state) {
1072
1124
  );
1073
1125
  if (result.ok && result.output) return result.output;
1074
1126
  } else {
1075
- const encrypted = await readFile(
1127
+ const encrypted = await readFile2(
1076
1128
  encryptedCredentialPath(state.workstation_id),
1077
1129
  "utf8"
1078
- ).catch(() => readFile(encryptedCredentialPath(), "utf8"));
1130
+ ).catch(() => readFile2(encryptedCredentialPath(), "utf8"));
1079
1131
  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
1132
  const result = spawnSync(
1081
1133
  "powershell.exe",
@@ -1163,7 +1215,7 @@ async function loadAgentCredential(state) {
1163
1215
  );
1164
1216
  if (result.ok && result.output) return result.output;
1165
1217
  } else {
1166
- const encrypted = await readFile(
1218
+ const encrypted = await readFile2(
1167
1219
  encryptedAgentCredentialPath(state.workstation_id),
1168
1220
  "utf8"
1169
1221
  );
@@ -1295,7 +1347,7 @@ async function loadHermesCredential(state) {
1295
1347
  }
1296
1348
  }
1297
1349
  } else {
1298
- const encrypted = await readFile(encryptedHermesCredentialPath(), "utf8");
1350
+ const encrypted = await readFile2(encryptedHermesCredentialPath(), "utf8");
1299
1351
  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
1352
  const result = spawnSync(
1301
1353
  "powershell.exe",
@@ -1384,7 +1436,7 @@ async function streamHermesRunEvents(state, command, runId) {
1384
1436
  hermesRunApprovals.set(runId, {
1385
1437
  approval_command: approvalCommand,
1386
1438
  approval_tool: tool,
1387
- approval_fingerprint: createHash("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1439
+ approval_fingerprint: createHash2("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
1388
1440
  approval_policy_decision: "approval_required"
1389
1441
  });
1390
1442
  }
@@ -1423,9 +1475,9 @@ async function waitForProcess(child) {
1423
1475
  });
1424
1476
  }
1425
1477
  async function installHermes() {
1426
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
1478
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
1427
1479
  const windows = platform() === "win32";
1428
- const scriptPath = join2(
1480
+ const scriptPath = join3(
1429
1481
  stateDirectory(),
1430
1482
  windows ? "hermes-install.ps1" : "hermes-install.sh"
1431
1483
  );
@@ -1482,14 +1534,14 @@ async function installHermes() {
1482
1534
  if (userPath.ok && userPath.output)
1483
1535
  process.env.PATH = `${userPath.output};${process.env.PATH || ""}`;
1484
1536
  } else {
1485
- process.env.PATH = `${join2(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
1537
+ process.env.PATH = `${join3(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
1486
1538
  }
1487
1539
  }
1488
1540
  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(() => "");
1541
+ const directory = join3(homedir(), ".hermes");
1542
+ const path = join3(directory, ".env");
1543
+ await mkdir2(directory, { recursive: true, mode: 448 });
1544
+ const existing = await readFile2(path, "utf8").catch(() => "");
1493
1545
  const settings = {
1494
1546
  API_SERVER_ENABLED: "true",
1495
1547
  API_SERVER_HOST: "127.0.0.1",
@@ -1519,15 +1571,15 @@ function hermesProfileArgs(state) {
1519
1571
  return ["--profile", hermesProfile(state.organization_id)];
1520
1572
  }
1521
1573
  async function configureSimplrMcp(state, executable) {
1522
- const profileDirectory = join2(
1574
+ const profileDirectory = join3(
1523
1575
  homedir(),
1524
1576
  ".hermes",
1525
1577
  "profiles",
1526
1578
  hermesProfile(state.organization_id)
1527
1579
  );
1528
- await mkdir(profileDirectory, { recursive: true, mode: 448 });
1529
- const environmentPath = join2(profileDirectory, ".env");
1530
- const existing = await readFile(environmentPath, "utf8").catch(() => "");
1580
+ await mkdir2(profileDirectory, { recursive: true, mode: 448 });
1581
+ const environmentPath = join3(profileDirectory, ".env");
1582
+ const existing = await readFile2(environmentPath, "utf8").catch(() => "");
1531
1583
  const settings = {
1532
1584
  SIMPLR_API_URL: state.api_url,
1533
1585
  SIMPLR_API_KEY: await ensureAgentCredential(state),
@@ -1549,7 +1601,7 @@ async function configureSimplrMcp(state, executable) {
1549
1601
  });
1550
1602
  if (platform() !== "win32") await chmod(environmentPath, 384);
1551
1603
  const profileArgs = hermesProfileArgs(state);
1552
- const wrapperPath = join2(profileDirectory, "simplr-mcp.mjs");
1604
+ const wrapperPath = join3(profileDirectory, "simplr-mcp.mjs");
1553
1605
  const wrapper = `#!/usr/bin/env node
1554
1606
  import { readFileSync } from "node:fs";
1555
1607
  import { spawn } from "node:child_process";
@@ -1608,8 +1660,8 @@ async function ensureSimplrMcpConfiguration(state) {
1608
1660
  state.mcp_configuration_version = SIMPLR_MCP_CONFIGURATION_VERSION;
1609
1661
  await saveState(state);
1610
1662
  }
1611
- async function installHermesSkill(state) {
1612
- const directory = join2(
1663
+ async function installHermesSkills(state) {
1664
+ const directory = join3(
1613
1665
  homedir(),
1614
1666
  ".hermes",
1615
1667
  "profiles",
@@ -1617,9 +1669,9 @@ async function installHermesSkill(state) {
1617
1669
  "skills",
1618
1670
  "simplr-remote-operations"
1619
1671
  );
1620
- await mkdir(directory, { recursive: true, mode: 448 });
1672
+ await mkdir2(directory, { recursive: true, mode: 448 });
1621
1673
  await writeFile(
1622
- join2(directory, "SKILL.md"),
1674
+ join3(directory, "SKILL.md"),
1623
1675
  `---
1624
1676
  name: simplr-remote-operations
1625
1677
  description: Execute a verified Simplr work-order envelope delivered by Simplr Connect. Do not use for direct local Hermes requests.
@@ -1653,6 +1705,79 @@ Never expose credentials, environment variables, tokens, keys, cookies, secret f
1653
1705
  `,
1654
1706
  { encoding: "utf8", mode: 384 }
1655
1707
  );
1708
+ const bundledCodexSources = [
1709
+ join3(homedir(), ".hermes", "skills", "autonomous-ai-agents", "codex"),
1710
+ join3(
1711
+ homedir(),
1712
+ ".hermes",
1713
+ "hermes-agent",
1714
+ "skills",
1715
+ "autonomous-ai-agents",
1716
+ "codex"
1717
+ )
1718
+ ];
1719
+ const codexSource = (await Promise.all(
1720
+ bundledCodexSources.map(async (candidate) => {
1721
+ try {
1722
+ await access(join3(candidate, "SKILL.md"), constants.R_OK);
1723
+ return candidate;
1724
+ } catch {
1725
+ return void 0;
1726
+ }
1727
+ })
1728
+ )).find(Boolean);
1729
+ if (!codexSource)
1730
+ throw new Error("Hermes bundled Codex skill is unavailable");
1731
+ const codexDestination = join3(
1732
+ homedir(),
1733
+ ".hermes",
1734
+ "profiles",
1735
+ hermesProfile(state.organization_id),
1736
+ "skills",
1737
+ "autonomous-ai-agents",
1738
+ "codex"
1739
+ );
1740
+ return syncDirectoryContents(codexSource, codexDestination);
1741
+ }
1742
+ async function ensureCodexExecutor() {
1743
+ if (platform() === "win32") return false;
1744
+ const localBin = join3(homedir(), ".local", "bin");
1745
+ const pathEntries = (process.env.PATH || "").split(":");
1746
+ const pathChanged = !pathEntries.includes(localBin);
1747
+ if (pathChanged)
1748
+ process.env.PATH = `${localBin}:${process.env.PATH || ""}`;
1749
+ if (commandVersion("codex")) return pathChanged;
1750
+ if (platform() !== "darwin") return pathChanged;
1751
+ const destination = join3(localBin, "codex");
1752
+ const candidates = [
1753
+ "/Applications/ChatGPT.app/Contents/Resources/codex",
1754
+ "/Applications/Codex.app/Contents/Resources/codex"
1755
+ ];
1756
+ for (const candidate of candidates) {
1757
+ try {
1758
+ await access(candidate, constants.X_OK);
1759
+ await mkdir2(localBin, { recursive: true, mode: 448 });
1760
+ await symlink(candidate, destination).catch((error) => {
1761
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
1762
+ throw error;
1763
+ });
1764
+ if (commandVersion("codex")) return true;
1765
+ } catch {
1766
+ }
1767
+ }
1768
+ return pathChanged;
1769
+ }
1770
+ async function ensureHermesExecutionTools(state) {
1771
+ let changed = await ensureCodexExecutor();
1772
+ for (const connection of state.connections.filter(
1773
+ (candidate) => candidate.setup_status === "ready"
1774
+ )) {
1775
+ const installed = await installHermesSkills(
1776
+ stateForConnection(state, connection)
1777
+ );
1778
+ changed = changed || installed;
1779
+ }
1780
+ return changed;
1656
1781
  }
1657
1782
  function isSimplrWorkOrder(command) {
1658
1783
  return ["incident", "bug", "feature"].includes(
@@ -1921,6 +2046,7 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
1921
2046
  );
1922
2047
  }
1923
2048
  async function setupHermes(state, interactiveOAuth) {
2049
+ await ensureCodexExecutor();
1924
2050
  let executable = detectHermesExecutable();
1925
2051
  if (!executable) {
1926
2052
  await installHermes();
@@ -1971,7 +2097,7 @@ async function setupHermes(state, interactiveOAuth) {
1971
2097
  );
1972
2098
  await configureHermesEnvironment(apiKey);
1973
2099
  await configureSimplrMcp(state, executable);
1974
- await installHermesSkill(state);
2100
+ await installHermesSkills(state);
1975
2101
  const multiplex = commandResult(
1976
2102
  executable,
1977
2103
  [
@@ -2232,11 +2358,15 @@ async function acknowledgeCommand(state, commandId, status, result) {
2232
2358
  let lastError;
2233
2359
  for (let attempt = 1; attempt <= 5; attempt += 1) {
2234
2360
  try {
2235
- await post(
2361
+ const acknowledged = await post(
2236
2362
  `${state.api_url}/v1/ai-workstations/commands/${commandId}/acknowledge`,
2237
2363
  { status, result: result.slice(0, 5e3) },
2238
2364
  token
2239
2365
  );
2366
+ if (!matchesCommandAcknowledgement(commandId, status, acknowledged))
2367
+ throw new Error(
2368
+ `Command acknowledgement remained ${acknowledged.status}`
2369
+ );
2240
2370
  await removeCommandJournalEntry(commandId);
2241
2371
  return;
2242
2372
  } catch (error) {
@@ -2315,6 +2445,23 @@ async function monitorHermesRun(state, command, runId, startedAt) {
2315
2445
  }
2316
2446
  if (status.status === "completed") {
2317
2447
  const output = status.output || "Hermes completed the task";
2448
+ if (isIncompleteCodeWorkResult(
2449
+ workOrderClassification(command.payload),
2450
+ output
2451
+ )) {
2452
+ await updateCommandJournal({
2453
+ command_id: command.id,
2454
+ type: command.type,
2455
+ label: command.reason,
2456
+ ...workOrderClassification(command.payload),
2457
+ run_id: runId,
2458
+ status: "failed",
2459
+ result: output,
2460
+ started_at: startedAt,
2461
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2462
+ });
2463
+ throw new Error(output);
2464
+ }
2318
2465
  await updateHermesTrace(command, state, runId, {
2319
2466
  status: "completed",
2320
2467
  current_action: "Hermes run completed",
@@ -2408,7 +2555,7 @@ ${output}` : output
2408
2555
  );
2409
2556
  } catch (error) {
2410
2557
  const message = error instanceof Error ? error.message : "Hermes run failed";
2411
- const cancelled = /cancelled|stopped/i.test(message);
2558
+ const cancelled = isHermesRunCancellation(message);
2412
2559
  await updateHermesTrace(command, state, runId, {
2413
2560
  status: cancelled ? "cancelled" : "failed",
2414
2561
  current_action: cancelled ? "Hermes run stopped" : "Hermes run failed",
@@ -2426,6 +2573,8 @@ async function runHermesCommand(state, command) {
2426
2573
  const journal = await loadCommandJournal();
2427
2574
  const existing = journal[command.id];
2428
2575
  if (existing?.status === "completed") {
2576
+ if (isIncompleteCodeWorkResult(existing, existing.result))
2577
+ throw new Error(existing.result || "Hermes code work did not complete");
2429
2578
  const awaitingPlanApproval = awaitsFeaturePlanApproval(existing);
2430
2579
  updateSimplrKanbanTask(
2431
2580
  state,
@@ -2638,7 +2787,7 @@ async function stopManagedProcesses() {
2638
2787
  return `${processes.length} Simplr-managed AI process${processes.length === 1 ? "" : "es"} terminated`;
2639
2788
  }
2640
2789
  async function acquireProcessLock(lockPath, activeMessage) {
2641
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
2790
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
2642
2791
  const create = async () => {
2643
2792
  const handle = await open(lockPath, "wx", 384);
2644
2793
  await handle.writeFile(`${process.pid}
@@ -2651,7 +2800,7 @@ async function acquireProcessLock(lockPath, activeMessage) {
2651
2800
  if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
2652
2801
  throw error;
2653
2802
  const existingPid = Number.parseInt(
2654
- (await readFile(lockPath, "utf8").catch(() => "0")).trim(),
2803
+ (await readFile2(lockPath, "utf8").catch(() => "0")).trim(),
2655
2804
  10
2656
2805
  );
2657
2806
  let active = false;
@@ -2866,16 +3015,32 @@ async function resumeHermesRuns(state) {
2866
3015
  );
2867
3016
  }
2868
3017
  }
3018
+ async function reconcileIncompleteCompletedRuns(state) {
3019
+ const journal = await loadCommandJournal();
3020
+ for (const entry of Object.values(journal)) {
3021
+ if (!isRecoverableIncompleteCompletion(entry)) continue;
3022
+ const connection = entry.organization_id ? state.connections.find(
3023
+ (candidate) => candidate.organization_id === entry.organization_id && candidate.workstation_id === entry.workstation_id
3024
+ ) : state.connections.length === 1 ? state.connections[0] : void 0;
3025
+ if (!connection) continue;
3026
+ await acknowledgeCommand(
3027
+ stateForConnection(state, connection),
3028
+ entry.command_id,
3029
+ "failed",
3030
+ entry.result || "Hermes code work did not complete"
3031
+ );
3032
+ }
3033
+ }
2869
3034
  async function serviceInstallationPresent() {
2870
3035
  try {
2871
3036
  await access(serviceExecutablePath(), constants.R_OK);
2872
3037
  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");
3038
+ const deployed = await readFile2(serviceExecutablePath());
3039
+ const expectedHash = createHash2("sha256").update(serviceExecutableSnapshot).digest("hex");
3040
+ const deployedHash = createHash2("sha256").update(deployed).digest("hex");
2876
3041
  if (expectedHash !== deployedHash) return false;
2877
3042
  }
2878
- const definition = await readFile(serviceDefinitionPath(), "utf8");
3043
+ const definition = await readFile2(serviceDefinitionPath(), "utf8");
2879
3044
  if (!definition.includes(serviceExecutablePath()) || !definition.includes("watch"))
2880
3045
  return false;
2881
3046
  if (platform() !== "win32") return true;
@@ -2917,10 +3082,13 @@ async function watch() {
2917
3082
  const state = await loadState();
2918
3083
  const executableSource = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
2919
3084
  if (executableSource)
2920
- serviceExecutableSnapshot = await readFile(executableSource).catch(
3085
+ serviceExecutableSnapshot = await readFile2(executableSource).catch(
2921
3086
  () => void 0
2922
3087
  );
3088
+ const executionToolsChanged = await ensureHermesExecutionTools(state);
2923
3089
  await ensureSimplrMcpConfiguration(state);
3090
+ if (executionToolsChanged) await restartHermesGateway(state);
3091
+ await reconcileIncompleteCompletedRuns(state);
2924
3092
  await resumeHermesRuns(state);
2925
3093
  let nextInventoryAt = 0;
2926
3094
  let nextServiceAuditAt = 0;
@@ -3133,10 +3301,10 @@ function systemdValue(value) {
3133
3301
  }
3134
3302
  async function deployServiceExecutable() {
3135
3303
  const source = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
3136
- const executable = serviceExecutableSnapshot || (source ? await readFile(source) : void 0);
3304
+ const executable = serviceExecutableSnapshot || (source ? await readFile2(source) : void 0);
3137
3305
  if (!executable)
3138
3306
  throw new Error("Simplr Connect executable path is unavailable");
3139
- await mkdir(stateDirectory(), { recursive: true, mode: 448 });
3307
+ await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
3140
3308
  await writeFile(serviceExecutablePath(), executable, {
3141
3309
  mode: 448
3142
3310
  });
@@ -3149,9 +3317,9 @@ async function installService(activate = true) {
3149
3317
  const nodePath = process.execPath;
3150
3318
  const connectPath = serviceExecutablePath();
3151
3319
  if (platform() === "darwin") {
3152
- const directory = join2(homedir(), "Library", "LaunchAgents");
3320
+ const directory = join3(homedir(), "Library", "LaunchAgents");
3153
3321
  const path = serviceDefinitionPath();
3154
- await mkdir(directory, { recursive: true, mode: 448 });
3322
+ await mkdir2(directory, { recursive: true, mode: 448 });
3155
3323
  const programArguments = STANDALONE_EXECUTABLE ? [connectPath, "watch"] : [nodePath, connectPath, "watch"];
3156
3324
  const plistArguments = programArguments.map((value) => `<string>${xmlValue(value)}</string>`).join("");
3157
3325
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -3182,9 +3350,9 @@ async function installService(activate = true) {
3182
3350
  return "Simplr Connect self-healing service installed";
3183
3351
  }
3184
3352
  if (platform() === "linux") {
3185
- const directory = join2(homedir(), ".config", "systemd", "user");
3353
+ const directory = join3(homedir(), ".config", "systemd", "user");
3186
3354
  const path = serviceDefinitionPath();
3187
- await mkdir(directory, { recursive: true, mode: 448 });
3355
+ await mkdir2(directory, { recursive: true, mode: 448 });
3188
3356
  const serviceCommand = STANDALONE_EXECUTABLE ? `${systemdValue(connectPath)} watch` : `${systemdValue(nodePath)} ${systemdValue(connectPath)} watch`;
3189
3357
  const unit = `[Unit]
3190
3358
  Description=Simplr Connect secure workstation proxy
@@ -3199,7 +3367,7 @@ RestartSec=10
3199
3367
  NoNewPrivileges=true
3200
3368
  PrivateTmp=true
3201
3369
  ProtectSystem=strict
3202
- ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join2(homedir(), ".hermes"))}
3370
+ ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join3(homedir(), ".hermes"))}
3203
3371
 
3204
3372
  [Install]
3205
3373
  WantedBy=default.target
@@ -3388,7 +3556,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
3388
3556
  status: "downloaded",
3389
3557
  artifact_sha256: artifact.sha256
3390
3558
  });
3391
- if (bytes.byteLength !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
3559
+ if (bytes.byteLength !== artifact.size || createHash2("sha256").update(bytes).digest("hex") !== artifact.sha256)
3392
3560
  throw new Error("Downloaded update failed integrity verification");
3393
3561
  await reportStage?.({
3394
3562
  status: "verified",
@@ -3435,7 +3603,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
3435
3603
  1e4
3436
3604
  );
3437
3605
  else {
3438
- const helperPath = join2(stateDirectory(), "complete-update.ps1");
3606
+ const helperPath = join3(stateDirectory(), "complete-update.ps1");
3439
3607
  const powershellValue = (value) => `'${value.replace(/'/g, "''")}'`;
3440
3608
  const helper = `$ErrorActionPreference = "Stop"\r
3441
3609
  & 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.7",
4
4
  "description": "Simplr Connect workstation enrollment and AI tool inventory companion",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",