@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.
- package/dist/index.js +254 -86
- 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,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.
|
|
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
|
|
271
|
+
return join3(homedir(), "Library", "Application Support", "Simplr Connect");
|
|
220
272
|
if (platform() === "win32")
|
|
221
|
-
return
|
|
222
|
-
process.env.APPDATA ||
|
|
273
|
+
return join3(
|
|
274
|
+
process.env.APPDATA || join3(homedir(), "AppData", "Roaming"),
|
|
223
275
|
"Simplr Connect"
|
|
224
276
|
);
|
|
225
|
-
return
|
|
226
|
-
process.env.XDG_CONFIG_HOME ||
|
|
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
|
|
283
|
+
return join3(stateDirectory(), "state.json");
|
|
232
284
|
}
|
|
233
285
|
function encryptedCredentialPath(workstationId) {
|
|
234
|
-
return
|
|
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
|
|
292
|
+
return join3(stateDirectory(), `agent-credential-${workstationId}.bin`);
|
|
241
293
|
}
|
|
242
294
|
function encryptedHermesCredentialPath() {
|
|
243
|
-
return
|
|
295
|
+
return join3(stateDirectory(), "hermes-credential.bin");
|
|
244
296
|
}
|
|
245
297
|
function commandJournalPath() {
|
|
246
|
-
return
|
|
298
|
+
return join3(stateDirectory(), "commands.json");
|
|
247
299
|
}
|
|
248
300
|
function hermesTracePath() {
|
|
249
|
-
return
|
|
301
|
+
return join3(stateDirectory(), "hermes-traces.json");
|
|
250
302
|
}
|
|
251
303
|
function serviceLogPath() {
|
|
252
|
-
return
|
|
304
|
+
return join3(stateDirectory(), "connect.log");
|
|
253
305
|
}
|
|
254
306
|
function serviceExecutablePath() {
|
|
255
307
|
if (!STANDALONE_EXECUTABLE)
|
|
256
|
-
return
|
|
257
|
-
return
|
|
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
|
|
321
|
+
return join3(stateDirectory(), "supervisor.lock");
|
|
270
322
|
}
|
|
271
323
|
function serviceLockPath() {
|
|
272
|
-
return
|
|
324
|
+
return join3(stateDirectory(), "service.lock");
|
|
273
325
|
}
|
|
274
326
|
function serviceDefinitionPath() {
|
|
275
327
|
if (platform() === "darwin")
|
|
276
|
-
return
|
|
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
|
|
335
|
+
return join3(
|
|
284
336
|
homedir(),
|
|
285
337
|
".config",
|
|
286
338
|
"systemd",
|
|
287
339
|
"user",
|
|
288
340
|
"simplr-connect.service"
|
|
289
341
|
);
|
|
290
|
-
return
|
|
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
|
-
|
|
452
|
+
join3(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
|
|
401
453
|
);
|
|
402
454
|
} else {
|
|
403
455
|
candidates.push(
|
|
404
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1127
|
+
const encrypted = await readFile2(
|
|
1076
1128
|
encryptedCredentialPath(state.workstation_id),
|
|
1077
1129
|
"utf8"
|
|
1078
|
-
).catch(() =>
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
1478
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
1427
1479
|
const windows = platform() === "win32";
|
|
1428
|
-
const scriptPath =
|
|
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 = `${
|
|
1537
|
+
process.env.PATH = `${join3(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
|
|
1486
1538
|
}
|
|
1487
1539
|
}
|
|
1488
1540
|
async function configureHermesEnvironment(apiKey) {
|
|
1489
|
-
const directory =
|
|
1490
|
-
const path =
|
|
1491
|
-
await
|
|
1492
|
-
const existing = await
|
|
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 =
|
|
1574
|
+
const profileDirectory = join3(
|
|
1523
1575
|
homedir(),
|
|
1524
1576
|
".hermes",
|
|
1525
1577
|
"profiles",
|
|
1526
1578
|
hermesProfile(state.organization_id)
|
|
1527
1579
|
);
|
|
1528
|
-
await
|
|
1529
|
-
const environmentPath =
|
|
1530
|
-
const existing = await
|
|
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 =
|
|
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
|
|
1612
|
-
const directory =
|
|
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
|
|
1672
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
1621
1673
|
await writeFile(
|
|
1622
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
2874
|
-
const expectedHash =
|
|
2875
|
-
const deployedHash =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3320
|
+
const directory = join3(homedir(), "Library", "LaunchAgents");
|
|
3153
3321
|
const path = serviceDefinitionPath();
|
|
3154
|
-
await
|
|
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 =
|
|
3353
|
+
const directory = join3(homedir(), ".config", "systemd", "user");
|
|
3186
3354
|
const path = serviceDefinitionPath();
|
|
3187
|
-
await
|
|
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(
|
|
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 ||
|
|
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 =
|
|
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
|