@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.
- package/dist/index.js +292 -88
- 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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
283
|
+
return join3(homedir(), "Library", "Application Support", "Simplr Connect");
|
|
220
284
|
if (platform() === "win32")
|
|
221
|
-
return
|
|
222
|
-
process.env.APPDATA ||
|
|
285
|
+
return join3(
|
|
286
|
+
process.env.APPDATA || join3(homedir(), "AppData", "Roaming"),
|
|
223
287
|
"Simplr Connect"
|
|
224
288
|
);
|
|
225
|
-
return
|
|
226
|
-
process.env.XDG_CONFIG_HOME ||
|
|
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
|
|
295
|
+
return join3(stateDirectory(), "state.json");
|
|
232
296
|
}
|
|
233
297
|
function encryptedCredentialPath(workstationId) {
|
|
234
|
-
return
|
|
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
|
|
304
|
+
return join3(stateDirectory(), `agent-credential-${workstationId}.bin`);
|
|
241
305
|
}
|
|
242
306
|
function encryptedHermesCredentialPath() {
|
|
243
|
-
return
|
|
307
|
+
return join3(stateDirectory(), "hermes-credential.bin");
|
|
244
308
|
}
|
|
245
309
|
function commandJournalPath() {
|
|
246
|
-
return
|
|
310
|
+
return join3(stateDirectory(), "commands.json");
|
|
247
311
|
}
|
|
248
312
|
function hermesTracePath() {
|
|
249
|
-
return
|
|
313
|
+
return join3(stateDirectory(), "hermes-traces.json");
|
|
250
314
|
}
|
|
251
315
|
function serviceLogPath() {
|
|
252
|
-
return
|
|
316
|
+
return join3(stateDirectory(), "connect.log");
|
|
253
317
|
}
|
|
254
318
|
function serviceExecutablePath() {
|
|
255
319
|
if (!STANDALONE_EXECUTABLE)
|
|
256
|
-
return
|
|
257
|
-
return
|
|
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
|
|
333
|
+
return join3(stateDirectory(), "supervisor.lock");
|
|
270
334
|
}
|
|
271
335
|
function serviceLockPath() {
|
|
272
|
-
return
|
|
336
|
+
return join3(stateDirectory(), "service.lock");
|
|
273
337
|
}
|
|
274
338
|
function serviceDefinitionPath() {
|
|
275
339
|
if (platform() === "darwin")
|
|
276
|
-
return
|
|
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
|
|
347
|
+
return join3(
|
|
284
348
|
homedir(),
|
|
285
349
|
".config",
|
|
286
350
|
"systemd",
|
|
287
351
|
"user",
|
|
288
352
|
"simplr-connect.service"
|
|
289
353
|
);
|
|
290
|
-
return
|
|
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
|
-
|
|
464
|
+
join3(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
|
|
401
465
|
);
|
|
402
466
|
} else {
|
|
403
467
|
candidates.push(
|
|
404
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1139
|
+
const encrypted = await readFile2(
|
|
1076
1140
|
encryptedCredentialPath(state.workstation_id),
|
|
1077
1141
|
"utf8"
|
|
1078
|
-
).catch(() =>
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
1493
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
1427
1494
|
const windows = platform() === "win32";
|
|
1428
|
-
const scriptPath =
|
|
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 = `${
|
|
1552
|
+
process.env.PATH = `${join3(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
|
|
1486
1553
|
}
|
|
1487
1554
|
}
|
|
1488
1555
|
async function configureHermesEnvironment(apiKey) {
|
|
1489
|
-
const directory =
|
|
1490
|
-
const path =
|
|
1491
|
-
await
|
|
1492
|
-
const existing = await
|
|
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 =
|
|
1589
|
+
const profileDirectory = join3(
|
|
1523
1590
|
homedir(),
|
|
1524
1591
|
".hermes",
|
|
1525
1592
|
"profiles",
|
|
1526
1593
|
hermesProfile(state.organization_id)
|
|
1527
1594
|
);
|
|
1528
|
-
await
|
|
1529
|
-
const environmentPath =
|
|
1530
|
-
const existing = await
|
|
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 =
|
|
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
|
|
1612
|
-
const directory =
|
|
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
|
|
1687
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
1621
1688
|
await writeFile(
|
|
1622
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
2874
|
-
const expectedHash =
|
|
2875
|
-
const deployedHash =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3356
|
+
const directory = join3(homedir(), "Library", "LaunchAgents");
|
|
3153
3357
|
const path = serviceDefinitionPath();
|
|
3154
|
-
await
|
|
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 =
|
|
3389
|
+
const directory = join3(homedir(), ".config", "systemd", "user");
|
|
3186
3390
|
const path = serviceDefinitionPath();
|
|
3187
|
-
await
|
|
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(
|
|
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 ||
|
|
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 =
|
|
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
|