@simplr-ai/connect 0.7.5 → 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 +489 -128
- 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",
|
|
@@ -115,6 +149,102 @@ function mcpConfigurationPaths(homeDirectory, workingDirectory, organizationId)
|
|
|
115
149
|
];
|
|
116
150
|
}
|
|
117
151
|
|
|
152
|
+
// src/work-order-support.ts
|
|
153
|
+
function workOrderClassification(payload) {
|
|
154
|
+
return {
|
|
155
|
+
...typeof payload.work_order_kind === "string" ? { work_order_kind: payload.work_order_kind } : {},
|
|
156
|
+
...typeof payload.source_type === "string" ? { source_type: payload.source_type } : {},
|
|
157
|
+
...typeof payload.execution_mode === "string" ? { execution_mode: payload.execution_mode } : {}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function awaitsFeaturePlanApproval(classification) {
|
|
161
|
+
return classification.work_order_kind === "feature" && classification.source_type === "feedback" && classification.execution_mode === "plan";
|
|
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 workOrderPayload(classification) {
|
|
182
|
+
return {
|
|
183
|
+
...classification.work_order_kind ? { work_order_kind: classification.work_order_kind } : {},
|
|
184
|
+
...classification.source_type ? { source_type: classification.source_type } : {},
|
|
185
|
+
...classification.execution_mode ? { execution_mode: classification.execution_mode } : {}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function postWithApiKey(url, body, apiKey) {
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
"X-API-Key": apiKey
|
|
194
|
+
},
|
|
195
|
+
body: JSON.stringify(body),
|
|
196
|
+
signal: AbortSignal.timeout(1e4)
|
|
197
|
+
});
|
|
198
|
+
const payload = await response.json().catch(() => ({}));
|
|
199
|
+
if (!response.ok || !payload.content)
|
|
200
|
+
throw new Error(
|
|
201
|
+
payload.message || `Simplr request failed (${response.status})`
|
|
202
|
+
);
|
|
203
|
+
return payload.content;
|
|
204
|
+
}
|
|
205
|
+
function reservedSimplrConnectLane(organizationNamespace2) {
|
|
206
|
+
return `simplr-connect-reserved:${organizationNamespace2}`;
|
|
207
|
+
}
|
|
208
|
+
function isRunningTaskOwnedBy(task, expectedAssignee) {
|
|
209
|
+
return (task.task?.status || task.status) === "running" && (task.task?.assignee || task.assignee) === expectedAssignee;
|
|
210
|
+
}
|
|
211
|
+
var terminalCommentMarker = (action) => `[simplr-connect-terminal:${action}]`;
|
|
212
|
+
function taskStatus(task) {
|
|
213
|
+
return task.task?.status || task.status;
|
|
214
|
+
}
|
|
215
|
+
function taskBlockKind(task) {
|
|
216
|
+
const blockedEvent = [...task.events || []].reverse().find((event) => event.kind === "blocked");
|
|
217
|
+
return task.task?.block_kind || task.block_kind || blockedEvent?.payload?.kind;
|
|
218
|
+
}
|
|
219
|
+
function hasTerminalComment(task, action) {
|
|
220
|
+
const marker = terminalCommentMarker(action);
|
|
221
|
+
return (task.comments || []).some((comment) => comment.body?.includes(marker));
|
|
222
|
+
}
|
|
223
|
+
function reconcileTerminalBlock(action, detail, operations) {
|
|
224
|
+
const kind = action === "needs_input" ? "needs_input" : "transient";
|
|
225
|
+
const matches = (task) => taskStatus(task) === "blocked" && taskBlockKind(task) === kind && hasTerminalComment(task, action);
|
|
226
|
+
let current = operations.read();
|
|
227
|
+
if (!current)
|
|
228
|
+
throw new Error(`Hermes work-board ${action} preflight failed`);
|
|
229
|
+
if (matches(current)) return true;
|
|
230
|
+
if (!hasTerminalComment(current, action)) {
|
|
231
|
+
const body = `${detail}
|
|
232
|
+
|
|
233
|
+
${terminalCommentMarker(action)}`;
|
|
234
|
+
const commented = operations.comment(body);
|
|
235
|
+
current = operations.read();
|
|
236
|
+
if (!current || !commented && !hasTerminalComment(current, action))
|
|
237
|
+
throw new Error(`Hermes work-board ${action} comment reconciliation failed`);
|
|
238
|
+
}
|
|
239
|
+
if (matches(current)) return true;
|
|
240
|
+
const blocked = operations.block(kind);
|
|
241
|
+
current = operations.read();
|
|
242
|
+
if (current && matches(current)) return true;
|
|
243
|
+
if (!blocked)
|
|
244
|
+
throw new Error(`Hermes work-board ${action} reconciliation failed`);
|
|
245
|
+
throw new Error(`Hermes work-board ${action} verification failed`);
|
|
246
|
+
}
|
|
247
|
+
|
|
118
248
|
// src/index.ts
|
|
119
249
|
var AmbiguousHermesRunError = class extends Error {
|
|
120
250
|
};
|
|
@@ -122,7 +252,7 @@ var KanbanReconciliationError = class extends Error {
|
|
|
122
252
|
};
|
|
123
253
|
var DeferredUpdateError = class extends Error {
|
|
124
254
|
};
|
|
125
|
-
var APP_VERSION = "0.7.
|
|
255
|
+
var APP_VERSION = "0.7.7";
|
|
126
256
|
var STANDALONE_EXECUTABLE = process.env.SIMPLR_CONNECT_STANDALONE === "true";
|
|
127
257
|
var MANIFEST_PUBLIC_KEY = process.env.SIMPLR_CONNECT_MANIFEST_PUBLIC_KEY || "";
|
|
128
258
|
var HERMES_INSTALL_COMMIT = "542e146b055f0d43767ac43cdb9e78054b240263";
|
|
@@ -138,45 +268,45 @@ var hermesTraceMutation = Promise.resolve();
|
|
|
138
268
|
var serviceExecutableSnapshot;
|
|
139
269
|
function stateDirectory() {
|
|
140
270
|
if (platform() === "darwin")
|
|
141
|
-
return
|
|
271
|
+
return join3(homedir(), "Library", "Application Support", "Simplr Connect");
|
|
142
272
|
if (platform() === "win32")
|
|
143
|
-
return
|
|
144
|
-
process.env.APPDATA ||
|
|
273
|
+
return join3(
|
|
274
|
+
process.env.APPDATA || join3(homedir(), "AppData", "Roaming"),
|
|
145
275
|
"Simplr Connect"
|
|
146
276
|
);
|
|
147
|
-
return
|
|
148
|
-
process.env.XDG_CONFIG_HOME ||
|
|
277
|
+
return join3(
|
|
278
|
+
process.env.XDG_CONFIG_HOME || join3(homedir(), ".config"),
|
|
149
279
|
"simplr-connect"
|
|
150
280
|
);
|
|
151
281
|
}
|
|
152
282
|
function statePath() {
|
|
153
|
-
return
|
|
283
|
+
return join3(stateDirectory(), "state.json");
|
|
154
284
|
}
|
|
155
285
|
function encryptedCredentialPath(workstationId) {
|
|
156
|
-
return
|
|
286
|
+
return join3(
|
|
157
287
|
stateDirectory(),
|
|
158
288
|
workstationId ? `credential-${workstationId}.bin` : "credential.bin"
|
|
159
289
|
);
|
|
160
290
|
}
|
|
161
291
|
function encryptedAgentCredentialPath(workstationId) {
|
|
162
|
-
return
|
|
292
|
+
return join3(stateDirectory(), `agent-credential-${workstationId}.bin`);
|
|
163
293
|
}
|
|
164
294
|
function encryptedHermesCredentialPath() {
|
|
165
|
-
return
|
|
295
|
+
return join3(stateDirectory(), "hermes-credential.bin");
|
|
166
296
|
}
|
|
167
297
|
function commandJournalPath() {
|
|
168
|
-
return
|
|
298
|
+
return join3(stateDirectory(), "commands.json");
|
|
169
299
|
}
|
|
170
300
|
function hermesTracePath() {
|
|
171
|
-
return
|
|
301
|
+
return join3(stateDirectory(), "hermes-traces.json");
|
|
172
302
|
}
|
|
173
303
|
function serviceLogPath() {
|
|
174
|
-
return
|
|
304
|
+
return join3(stateDirectory(), "connect.log");
|
|
175
305
|
}
|
|
176
306
|
function serviceExecutablePath() {
|
|
177
307
|
if (!STANDALONE_EXECUTABLE)
|
|
178
|
-
return
|
|
179
|
-
return
|
|
308
|
+
return join3(stateDirectory(), "simplr-connect.js");
|
|
309
|
+
return join3(
|
|
180
310
|
stateDirectory(),
|
|
181
311
|
platform() === "win32" ? "simplr-connect.exe" : "simplr-connect"
|
|
182
312
|
);
|
|
@@ -188,28 +318,28 @@ function updateRollbackPath() {
|
|
|
188
318
|
return `${serviceExecutablePath()}.rollback`;
|
|
189
319
|
}
|
|
190
320
|
function supervisorLockPath() {
|
|
191
|
-
return
|
|
321
|
+
return join3(stateDirectory(), "supervisor.lock");
|
|
192
322
|
}
|
|
193
323
|
function serviceLockPath() {
|
|
194
|
-
return
|
|
324
|
+
return join3(stateDirectory(), "service.lock");
|
|
195
325
|
}
|
|
196
326
|
function serviceDefinitionPath() {
|
|
197
327
|
if (platform() === "darwin")
|
|
198
|
-
return
|
|
328
|
+
return join3(
|
|
199
329
|
homedir(),
|
|
200
330
|
"Library",
|
|
201
331
|
"LaunchAgents",
|
|
202
332
|
"ai.simplr.connect.plist"
|
|
203
333
|
);
|
|
204
334
|
if (platform() === "linux")
|
|
205
|
-
return
|
|
335
|
+
return join3(
|
|
206
336
|
homedir(),
|
|
207
337
|
".config",
|
|
208
338
|
"systemd",
|
|
209
339
|
"user",
|
|
210
340
|
"simplr-connect.service"
|
|
211
341
|
);
|
|
212
|
-
return
|
|
342
|
+
return join3(stateDirectory(), "simplr-connect-service.cmd");
|
|
213
343
|
}
|
|
214
344
|
function apiUrl(override) {
|
|
215
345
|
const value = override || process.env.SIMPLR_API_URL;
|
|
@@ -319,11 +449,11 @@ function hermesExecutableCandidates() {
|
|
|
319
449
|
const localAppData = process.env.LOCALAPPDATA;
|
|
320
450
|
if (localAppData)
|
|
321
451
|
candidates.push(
|
|
322
|
-
|
|
452
|
+
join3(localAppData, "hermes", "hermes-agent", "bin", "hermes.exe")
|
|
323
453
|
);
|
|
324
454
|
} else {
|
|
325
455
|
candidates.push(
|
|
326
|
-
|
|
456
|
+
join3(homedir(), ".local", "bin", "hermes"),
|
|
327
457
|
"/usr/local/bin/hermes"
|
|
328
458
|
);
|
|
329
459
|
if (platform() === "darwin") candidates.push("/opt/homebrew/bin/hermes");
|
|
@@ -644,15 +774,15 @@ async function existingDirectories(paths) {
|
|
|
644
774
|
}
|
|
645
775
|
async function detectSkills() {
|
|
646
776
|
const paths = await existingDirectories([
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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")
|
|
652
782
|
]);
|
|
653
783
|
const skills = /* @__PURE__ */ new Map();
|
|
654
784
|
for (const path of paths) {
|
|
655
|
-
const entries = await
|
|
785
|
+
const entries = await readdir2(path, { withFileTypes: true }).catch(
|
|
656
786
|
() => []
|
|
657
787
|
);
|
|
658
788
|
for (const entry of entries) {
|
|
@@ -673,7 +803,7 @@ async function detectMcpServers(state) {
|
|
|
673
803
|
mcpConfigurationPaths(homedir(), process.cwd(), state.organization_id)
|
|
674
804
|
);
|
|
675
805
|
const contents = await Promise.all(
|
|
676
|
-
configPaths.map((path) =>
|
|
806
|
+
configPaths.map((path) => readFile2(path, "utf8").catch(() => ""))
|
|
677
807
|
);
|
|
678
808
|
const names = new Set(mcpServerNamesFromConfiguration(contents));
|
|
679
809
|
for (const content of contents) {
|
|
@@ -767,7 +897,7 @@ async function get(url, token) {
|
|
|
767
897
|
return payload.content;
|
|
768
898
|
}
|
|
769
899
|
async function saveState(state) {
|
|
770
|
-
await
|
|
900
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
771
901
|
const { device_token: legacyToken, ...safeState } = state;
|
|
772
902
|
void legacyToken;
|
|
773
903
|
await writeFile(statePath(), `${JSON.stringify(safeState, null, 2)}
|
|
@@ -778,7 +908,7 @@ async function saveState(state) {
|
|
|
778
908
|
if (platform() !== "win32") await chmod(statePath(), 384);
|
|
779
909
|
}
|
|
780
910
|
async function loadState() {
|
|
781
|
-
const stored = JSON.parse(await
|
|
911
|
+
const stored = JSON.parse(await readFile2(statePath(), "utf8"));
|
|
782
912
|
if (stored.device_token) {
|
|
783
913
|
await storeCredential(stored.workstation_id, stored.device_token);
|
|
784
914
|
delete stored.device_token;
|
|
@@ -789,13 +919,13 @@ async function loadState() {
|
|
|
789
919
|
}
|
|
790
920
|
async function loadCommandJournal() {
|
|
791
921
|
try {
|
|
792
|
-
return JSON.parse(await
|
|
922
|
+
return JSON.parse(await readFile2(commandJournalPath(), "utf8"));
|
|
793
923
|
} catch {
|
|
794
924
|
return {};
|
|
795
925
|
}
|
|
796
926
|
}
|
|
797
927
|
async function saveCommandJournal(journal) {
|
|
798
|
-
await
|
|
928
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
799
929
|
const temporaryPath = `${commandJournalPath()}.${process.pid}.tmp`;
|
|
800
930
|
await writeFile(temporaryPath, `${JSON.stringify(journal, null, 2)}
|
|
801
931
|
`, {
|
|
@@ -825,13 +955,13 @@ async function removeCommandJournalEntry(commandId) {
|
|
|
825
955
|
}
|
|
826
956
|
async function loadHermesTraces() {
|
|
827
957
|
try {
|
|
828
|
-
return JSON.parse(await
|
|
958
|
+
return JSON.parse(await readFile2(hermesTracePath(), "utf8"));
|
|
829
959
|
} catch {
|
|
830
960
|
return {};
|
|
831
961
|
}
|
|
832
962
|
}
|
|
833
963
|
async function saveHermesTraces(traces) {
|
|
834
|
-
await
|
|
964
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
835
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));
|
|
836
966
|
const temporaryPath = `${hermesTracePath()}.${process.pid}.tmp`;
|
|
837
967
|
await writeFile(temporaryPath, `${JSON.stringify(retained, null, 2)}
|
|
@@ -916,7 +1046,7 @@ async function storeMacKeychainCredential(account, service, token) {
|
|
|
916
1046
|
throw new Error("Could not protect the credential in macOS Keychain");
|
|
917
1047
|
}
|
|
918
1048
|
async function storeCredential(workstationId, token) {
|
|
919
|
-
await
|
|
1049
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
920
1050
|
if (platform() === "darwin") {
|
|
921
1051
|
await storeMacKeychainCredential(workstationId, "Simplr Connect", token);
|
|
922
1052
|
return;
|
|
@@ -994,10 +1124,10 @@ async function loadCredential(state) {
|
|
|
994
1124
|
);
|
|
995
1125
|
if (result.ok && result.output) return result.output;
|
|
996
1126
|
} else {
|
|
997
|
-
const encrypted = await
|
|
1127
|
+
const encrypted = await readFile2(
|
|
998
1128
|
encryptedCredentialPath(state.workstation_id),
|
|
999
1129
|
"utf8"
|
|
1000
|
-
).catch(() =>
|
|
1130
|
+
).catch(() => readFile2(encryptedCredentialPath(), "utf8"));
|
|
1001
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)";
|
|
1002
1132
|
const result = spawnSync(
|
|
1003
1133
|
"powershell.exe",
|
|
@@ -1085,7 +1215,7 @@ async function loadAgentCredential(state) {
|
|
|
1085
1215
|
);
|
|
1086
1216
|
if (result.ok && result.output) return result.output;
|
|
1087
1217
|
} else {
|
|
1088
|
-
const encrypted = await
|
|
1218
|
+
const encrypted = await readFile2(
|
|
1089
1219
|
encryptedAgentCredentialPath(state.workstation_id),
|
|
1090
1220
|
"utf8"
|
|
1091
1221
|
);
|
|
@@ -1217,7 +1347,7 @@ async function loadHermesCredential(state) {
|
|
|
1217
1347
|
}
|
|
1218
1348
|
}
|
|
1219
1349
|
} else {
|
|
1220
|
-
const encrypted = await
|
|
1350
|
+
const encrypted = await readFile2(encryptedHermesCredentialPath(), "utf8");
|
|
1221
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)";
|
|
1222
1352
|
const result = spawnSync(
|
|
1223
1353
|
"powershell.exe",
|
|
@@ -1306,7 +1436,7 @@ async function streamHermesRunEvents(state, command, runId) {
|
|
|
1306
1436
|
hermesRunApprovals.set(runId, {
|
|
1307
1437
|
approval_command: approvalCommand,
|
|
1308
1438
|
approval_tool: tool,
|
|
1309
|
-
approval_fingerprint:
|
|
1439
|
+
approval_fingerprint: createHash2("sha256").update(JSON.stringify({ run_id: runId, tool, command: approvalCommand })).digest("hex"),
|
|
1310
1440
|
approval_policy_decision: "approval_required"
|
|
1311
1441
|
});
|
|
1312
1442
|
}
|
|
@@ -1345,9 +1475,9 @@ async function waitForProcess(child) {
|
|
|
1345
1475
|
});
|
|
1346
1476
|
}
|
|
1347
1477
|
async function installHermes() {
|
|
1348
|
-
await
|
|
1478
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
1349
1479
|
const windows = platform() === "win32";
|
|
1350
|
-
const scriptPath =
|
|
1480
|
+
const scriptPath = join3(
|
|
1351
1481
|
stateDirectory(),
|
|
1352
1482
|
windows ? "hermes-install.ps1" : "hermes-install.sh"
|
|
1353
1483
|
);
|
|
@@ -1404,14 +1534,14 @@ async function installHermes() {
|
|
|
1404
1534
|
if (userPath.ok && userPath.output)
|
|
1405
1535
|
process.env.PATH = `${userPath.output};${process.env.PATH || ""}`;
|
|
1406
1536
|
} else {
|
|
1407
|
-
process.env.PATH = `${
|
|
1537
|
+
process.env.PATH = `${join3(homedir(), ".local", "bin")}:${process.env.PATH || ""}`;
|
|
1408
1538
|
}
|
|
1409
1539
|
}
|
|
1410
1540
|
async function configureHermesEnvironment(apiKey) {
|
|
1411
|
-
const directory =
|
|
1412
|
-
const path =
|
|
1413
|
-
await
|
|
1414
|
-
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(() => "");
|
|
1415
1545
|
const settings = {
|
|
1416
1546
|
API_SERVER_ENABLED: "true",
|
|
1417
1547
|
API_SERVER_HOST: "127.0.0.1",
|
|
@@ -1441,15 +1571,15 @@ function hermesProfileArgs(state) {
|
|
|
1441
1571
|
return ["--profile", hermesProfile(state.organization_id)];
|
|
1442
1572
|
}
|
|
1443
1573
|
async function configureSimplrMcp(state, executable) {
|
|
1444
|
-
const profileDirectory =
|
|
1574
|
+
const profileDirectory = join3(
|
|
1445
1575
|
homedir(),
|
|
1446
1576
|
".hermes",
|
|
1447
1577
|
"profiles",
|
|
1448
1578
|
hermesProfile(state.organization_id)
|
|
1449
1579
|
);
|
|
1450
|
-
await
|
|
1451
|
-
const environmentPath =
|
|
1452
|
-
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(() => "");
|
|
1453
1583
|
const settings = {
|
|
1454
1584
|
SIMPLR_API_URL: state.api_url,
|
|
1455
1585
|
SIMPLR_API_KEY: await ensureAgentCredential(state),
|
|
@@ -1471,7 +1601,7 @@ async function configureSimplrMcp(state, executable) {
|
|
|
1471
1601
|
});
|
|
1472
1602
|
if (platform() !== "win32") await chmod(environmentPath, 384);
|
|
1473
1603
|
const profileArgs = hermesProfileArgs(state);
|
|
1474
|
-
const wrapperPath =
|
|
1604
|
+
const wrapperPath = join3(profileDirectory, "simplr-mcp.mjs");
|
|
1475
1605
|
const wrapper = `#!/usr/bin/env node
|
|
1476
1606
|
import { readFileSync } from "node:fs";
|
|
1477
1607
|
import { spawn } from "node:child_process";
|
|
@@ -1530,8 +1660,8 @@ async function ensureSimplrMcpConfiguration(state) {
|
|
|
1530
1660
|
state.mcp_configuration_version = SIMPLR_MCP_CONFIGURATION_VERSION;
|
|
1531
1661
|
await saveState(state);
|
|
1532
1662
|
}
|
|
1533
|
-
async function
|
|
1534
|
-
const directory =
|
|
1663
|
+
async function installHermesSkills(state) {
|
|
1664
|
+
const directory = join3(
|
|
1535
1665
|
homedir(),
|
|
1536
1666
|
".hermes",
|
|
1537
1667
|
"profiles",
|
|
@@ -1539,9 +1669,9 @@ async function installHermesSkill(state) {
|
|
|
1539
1669
|
"skills",
|
|
1540
1670
|
"simplr-remote-operations"
|
|
1541
1671
|
);
|
|
1542
|
-
await
|
|
1672
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
1543
1673
|
await writeFile(
|
|
1544
|
-
|
|
1674
|
+
join3(directory, "SKILL.md"),
|
|
1545
1675
|
`---
|
|
1546
1676
|
name: simplr-remote-operations
|
|
1547
1677
|
description: Execute a verified Simplr work-order envelope delivered by Simplr Connect. Do not use for direct local Hermes requests.
|
|
@@ -1575,6 +1705,79 @@ Never expose credentials, environment variables, tokens, keys, cookies, secret f
|
|
|
1575
1705
|
`,
|
|
1576
1706
|
{ encoding: "utf8", mode: 384 }
|
|
1577
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;
|
|
1578
1781
|
}
|
|
1579
1782
|
function isSimplrWorkOrder(command) {
|
|
1580
1783
|
return ["incident", "bug", "feature"].includes(
|
|
@@ -1583,11 +1786,47 @@ function isSimplrWorkOrder(command) {
|
|
|
1583
1786
|
String(command.payload.execution_mode)
|
|
1584
1787
|
);
|
|
1585
1788
|
}
|
|
1789
|
+
async function claimSimplrFeedbackInCloud(state, command) {
|
|
1790
|
+
if (String(command.payload.source_type) !== "feedback") return;
|
|
1791
|
+
const sourceId = String(command.payload.source_id || "");
|
|
1792
|
+
if (!sourceId) throw new Error("Simplr feedback source is missing");
|
|
1793
|
+
const agentToken = await ensureAgentCredential(state);
|
|
1794
|
+
await postWithApiKey(
|
|
1795
|
+
`${state.api_url}/v1/feedback/agent/items/${encodeURIComponent(sourceId)}/claim`,
|
|
1796
|
+
{},
|
|
1797
|
+
agentToken
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
function readSimplrKanbanTask(executable, state, taskId) {
|
|
1801
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1802
|
+
const shown = commandResult(
|
|
1803
|
+
executable,
|
|
1804
|
+
[
|
|
1805
|
+
...hermesProfileArgs(state),
|
|
1806
|
+
"kanban",
|
|
1807
|
+
"--board",
|
|
1808
|
+
`simplr-${organizationNamespace(state.organization_id)}`,
|
|
1809
|
+
"show",
|
|
1810
|
+
taskId,
|
|
1811
|
+
"--json"
|
|
1812
|
+
],
|
|
1813
|
+
1e4
|
|
1814
|
+
);
|
|
1815
|
+
if (shown.ok) return JSON.parse(shown.output);
|
|
1816
|
+
}
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
function simplrKanbanTaskStatus(task) {
|
|
1820
|
+
return task.task?.status || task.status;
|
|
1821
|
+
}
|
|
1586
1822
|
function ensureSimplrKanbanTask(state, command, prompt) {
|
|
1587
1823
|
if (!isSimplrWorkOrder(command)) return void 0;
|
|
1588
1824
|
const executable = detectHermesExecutable();
|
|
1589
1825
|
if (!executable) throw new Error("Hermes executable is unavailable");
|
|
1590
1826
|
const boardSlug = `simplr-${organizationNamespace(state.organization_id)}`;
|
|
1827
|
+
const reservedAssignee = reservedSimplrConnectLane(
|
|
1828
|
+
organizationNamespace(state.organization_id)
|
|
1829
|
+
);
|
|
1591
1830
|
const boards = commandResult(
|
|
1592
1831
|
executable,
|
|
1593
1832
|
[...hermesProfileArgs(state), "kanban", "boards", "list", "--json"],
|
|
@@ -1664,10 +1903,12 @@ ${prompt}`;
|
|
|
1664
1903
|
`simplr-connect:${state.organization_id}:${command.id}`,
|
|
1665
1904
|
"--created-by",
|
|
1666
1905
|
"Simplr Connect",
|
|
1906
|
+
"--assignee",
|
|
1907
|
+
reservedAssignee,
|
|
1667
1908
|
"--skill",
|
|
1668
1909
|
"simplr-remote-operations",
|
|
1669
1910
|
"--initial-status",
|
|
1670
|
-
"
|
|
1911
|
+
"running",
|
|
1671
1912
|
"--json"
|
|
1672
1913
|
],
|
|
1673
1914
|
15e3
|
|
@@ -1676,12 +1917,33 @@ ${prompt}`;
|
|
|
1676
1917
|
throw new Error("Hermes could not persist the Simplr work order");
|
|
1677
1918
|
const task = JSON.parse(createdTask.output);
|
|
1678
1919
|
if (!task.id) throw new Error("Hermes did not return a work-order task id");
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1920
|
+
let current = readSimplrKanbanTask(executable, state, task.id);
|
|
1921
|
+
if (!current) throw new Error("Hermes could not verify the Simplr work-order task");
|
|
1922
|
+
if (simplrKanbanTaskStatus(current) === "ready") {
|
|
1923
|
+
const claimed = commandResult(
|
|
1924
|
+
executable,
|
|
1925
|
+
[
|
|
1926
|
+
...hermesProfileArgs(state),
|
|
1927
|
+
"kanban",
|
|
1928
|
+
"--board",
|
|
1929
|
+
boardSlug,
|
|
1930
|
+
"claim",
|
|
1931
|
+
task.id,
|
|
1932
|
+
"--ttl",
|
|
1933
|
+
"3600"
|
|
1934
|
+
],
|
|
1935
|
+
1e4
|
|
1936
|
+
);
|
|
1937
|
+
current = readSimplrKanbanTask(executable, state, task.id);
|
|
1938
|
+
if (!claimed.ok && simplrKanbanTaskStatus(current || {}) !== "running") {
|
|
1939
|
+
throw new Error("Hermes could not claim the Simplr work-order task");
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (!current || !isRunningTaskOwnedBy(current, reservedAssignee)) {
|
|
1943
|
+
throw new Error(
|
|
1944
|
+
"Hermes Simplr work-order task is not running in its reserved ownership lane"
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1685
1947
|
return task.id;
|
|
1686
1948
|
}
|
|
1687
1949
|
function updateSimplrKanbanTask(state, taskId, action, detail) {
|
|
@@ -1710,7 +1972,47 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
|
|
|
1710
1972
|
1e4
|
|
1711
1973
|
).ok;
|
|
1712
1974
|
}
|
|
1713
|
-
|
|
1975
|
+
if (action === "failed" || action === "needs_input") {
|
|
1976
|
+
try {
|
|
1977
|
+
return reconcileTerminalBlock(action, detail.slice(0, 2e3), {
|
|
1978
|
+
read: () => readSimplrKanbanTask(executable, state, taskId),
|
|
1979
|
+
comment: (body) => commandResult(
|
|
1980
|
+
executable,
|
|
1981
|
+
[
|
|
1982
|
+
...hermesProfileArgs(state),
|
|
1983
|
+
"kanban",
|
|
1984
|
+
"--board",
|
|
1985
|
+
`simplr-${organizationNamespace(state.organization_id)}`,
|
|
1986
|
+
"comment",
|
|
1987
|
+
"--author",
|
|
1988
|
+
"Simplr Connect",
|
|
1989
|
+
taskId,
|
|
1990
|
+
body
|
|
1991
|
+
],
|
|
1992
|
+
1e4
|
|
1993
|
+
).ok,
|
|
1994
|
+
block: (kind) => commandResult(
|
|
1995
|
+
executable,
|
|
1996
|
+
[
|
|
1997
|
+
...hermesProfileArgs(state),
|
|
1998
|
+
"kanban",
|
|
1999
|
+
"--board",
|
|
2000
|
+
`simplr-${organizationNamespace(state.organization_id)}`,
|
|
2001
|
+
"block",
|
|
2002
|
+
"--kind",
|
|
2003
|
+
kind,
|
|
2004
|
+
taskId
|
|
2005
|
+
],
|
|
2006
|
+
1e4
|
|
2007
|
+
).ok
|
|
2008
|
+
});
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
throw new KanbanReconciliationError(
|
|
2011
|
+
error instanceof Error ? error.message : `Hermes work-board ${action} reconciliation failed`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
const args = [
|
|
1714
2016
|
...hermesProfileArgs(state),
|
|
1715
2017
|
"kanban",
|
|
1716
2018
|
"--board",
|
|
@@ -1724,44 +2026,27 @@ function updateSimplrKanbanTask(state, taskId, action, detail) {
|
|
|
1724
2026
|
source: "simplr-connect",
|
|
1725
2027
|
rules_version: "simplr-work-order-v1"
|
|
1726
2028
|
})
|
|
1727
|
-
] : [
|
|
1728
|
-
...hermesProfileArgs(state),
|
|
1729
|
-
"kanban",
|
|
1730
|
-
"--board",
|
|
1731
|
-
`simplr-${organizationNamespace(state.organization_id)}`,
|
|
1732
|
-
"block",
|
|
1733
|
-
"--kind",
|
|
1734
|
-
"transient",
|
|
1735
|
-
taskId,
|
|
1736
|
-
detail.slice(0, 2e3)
|
|
1737
2029
|
];
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
`simplr-${organizationNamespace(state.organization_id)}`,
|
|
1747
|
-
"show",
|
|
1748
|
-
taskId,
|
|
1749
|
-
"--json"
|
|
1750
|
-
],
|
|
1751
|
-
1e4
|
|
2030
|
+
const matchesAction = (current) => {
|
|
2031
|
+
const currentStatus = simplrKanbanTaskStatus(current);
|
|
2032
|
+
return action === "complete" && currentStatus === "done";
|
|
2033
|
+
};
|
|
2034
|
+
const initial = readSimplrKanbanTask(executable, state, taskId);
|
|
2035
|
+
if (!initial) {
|
|
2036
|
+
throw new KanbanReconciliationError(
|
|
2037
|
+
`Hermes work-board ${action} preflight failed`
|
|
1752
2038
|
);
|
|
1753
|
-
if (shown.ok) {
|
|
1754
|
-
const current = JSON.parse(shown.output);
|
|
1755
|
-
const currentStatus = current.task?.status || current.status;
|
|
1756
|
-
if (action === "complete" && currentStatus === "done" || action === "failed" && currentStatus === "blocked")
|
|
1757
|
-
return true;
|
|
1758
|
-
}
|
|
1759
2039
|
}
|
|
2040
|
+
if (matchesAction(initial)) return true;
|
|
2041
|
+
if (commandResult(executable, args, 1e4).ok) return true;
|
|
2042
|
+
const reconciled = readSimplrKanbanTask(executable, state, taskId);
|
|
2043
|
+
if (reconciled && matchesAction(reconciled)) return true;
|
|
1760
2044
|
throw new KanbanReconciliationError(
|
|
1761
2045
|
`Hermes work-board ${action} reconciliation failed`
|
|
1762
2046
|
);
|
|
1763
2047
|
}
|
|
1764
2048
|
async function setupHermes(state, interactiveOAuth) {
|
|
2049
|
+
await ensureCodexExecutor();
|
|
1765
2050
|
let executable = detectHermesExecutable();
|
|
1766
2051
|
if (!executable) {
|
|
1767
2052
|
await installHermes();
|
|
@@ -1812,7 +2097,7 @@ async function setupHermes(state, interactiveOAuth) {
|
|
|
1812
2097
|
);
|
|
1813
2098
|
await configureHermesEnvironment(apiKey);
|
|
1814
2099
|
await configureSimplrMcp(state, executable);
|
|
1815
|
-
await
|
|
2100
|
+
await installHermesSkills(state);
|
|
1816
2101
|
const multiplex = commandResult(
|
|
1817
2102
|
executable,
|
|
1818
2103
|
[
|
|
@@ -2073,11 +2358,15 @@ async function acknowledgeCommand(state, commandId, status, result) {
|
|
|
2073
2358
|
let lastError;
|
|
2074
2359
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
2075
2360
|
try {
|
|
2076
|
-
await post(
|
|
2361
|
+
const acknowledged = await post(
|
|
2077
2362
|
`${state.api_url}/v1/ai-workstations/commands/${commandId}/acknowledge`,
|
|
2078
2363
|
{ status, result: result.slice(0, 5e3) },
|
|
2079
2364
|
token
|
|
2080
2365
|
);
|
|
2366
|
+
if (!matchesCommandAcknowledgement(commandId, status, acknowledged))
|
|
2367
|
+
throw new Error(
|
|
2368
|
+
`Command acknowledgement remained ${acknowledged.status}`
|
|
2369
|
+
);
|
|
2081
2370
|
await removeCommandJournalEntry(commandId);
|
|
2082
2371
|
return;
|
|
2083
2372
|
} catch (error) {
|
|
@@ -2156,6 +2445,23 @@ async function monitorHermesRun(state, command, runId, startedAt) {
|
|
|
2156
2445
|
}
|
|
2157
2446
|
if (status.status === "completed") {
|
|
2158
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
|
+
}
|
|
2159
2465
|
await updateHermesTrace(command, state, runId, {
|
|
2160
2466
|
status: "completed",
|
|
2161
2467
|
current_action: "Hermes run completed",
|
|
@@ -2167,13 +2473,24 @@ async function monitorHermesRun(state, command, runId, startedAt) {
|
|
|
2167
2473
|
command_id: command.id,
|
|
2168
2474
|
type: command.type,
|
|
2169
2475
|
label: command.reason,
|
|
2476
|
+
...workOrderClassification(command.payload),
|
|
2170
2477
|
run_id: runId,
|
|
2171
2478
|
status: "completed",
|
|
2172
2479
|
result: output,
|
|
2173
2480
|
started_at: startedAt,
|
|
2174
2481
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2175
2482
|
});
|
|
2176
|
-
|
|
2483
|
+
const awaitingPlanApproval = awaitsFeaturePlanApproval(
|
|
2484
|
+
workOrderClassification(command.payload)
|
|
2485
|
+
);
|
|
2486
|
+
updateSimplrKanbanTask(
|
|
2487
|
+
state,
|
|
2488
|
+
taskId,
|
|
2489
|
+
awaitingPlanApproval ? "needs_input" : "complete",
|
|
2490
|
+
awaitingPlanApproval ? `Feature plan ready for comment and approval. Review the plan below, add guidance, then explicitly approve a new implementation work order.
|
|
2491
|
+
|
|
2492
|
+
${output}` : output
|
|
2493
|
+
);
|
|
2177
2494
|
return output;
|
|
2178
2495
|
}
|
|
2179
2496
|
if (status.status === "cancelled")
|
|
@@ -2186,6 +2503,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
|
|
|
2186
2503
|
command_id: command.id,
|
|
2187
2504
|
type: command.type,
|
|
2188
2505
|
label: command.reason,
|
|
2506
|
+
...workOrderClassification(command.payload),
|
|
2189
2507
|
run_id: runId,
|
|
2190
2508
|
status: run.status,
|
|
2191
2509
|
started_at: startedAt,
|
|
@@ -2197,6 +2515,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
|
|
|
2197
2515
|
command_id: command.id,
|
|
2198
2516
|
type: command.type,
|
|
2199
2517
|
label: command.reason,
|
|
2518
|
+
...workOrderClassification(command.payload),
|
|
2200
2519
|
run_id: runId,
|
|
2201
2520
|
status: run.status,
|
|
2202
2521
|
approval_command: approval.approval_command,
|
|
@@ -2236,7 +2555,7 @@ async function monitorHermesRun(state, command, runId, startedAt) {
|
|
|
2236
2555
|
);
|
|
2237
2556
|
} catch (error) {
|
|
2238
2557
|
const message = error instanceof Error ? error.message : "Hermes run failed";
|
|
2239
|
-
const cancelled =
|
|
2558
|
+
const cancelled = isHermesRunCancellation(message);
|
|
2240
2559
|
await updateHermesTrace(command, state, runId, {
|
|
2241
2560
|
status: cancelled ? "cancelled" : "failed",
|
|
2242
2561
|
current_action: cancelled ? "Hermes run stopped" : "Hermes run failed",
|
|
@@ -2254,11 +2573,16 @@ async function runHermesCommand(state, command) {
|
|
|
2254
2573
|
const journal = await loadCommandJournal();
|
|
2255
2574
|
const existing = journal[command.id];
|
|
2256
2575
|
if (existing?.status === "completed") {
|
|
2576
|
+
if (isIncompleteCodeWorkResult(existing, existing.result))
|
|
2577
|
+
throw new Error(existing.result || "Hermes code work did not complete");
|
|
2578
|
+
const awaitingPlanApproval = awaitsFeaturePlanApproval(existing);
|
|
2257
2579
|
updateSimplrKanbanTask(
|
|
2258
2580
|
state,
|
|
2259
2581
|
existing.kanban_task_id,
|
|
2260
|
-
"complete",
|
|
2261
|
-
|
|
2582
|
+
awaitingPlanApproval ? "needs_input" : "complete",
|
|
2583
|
+
awaitingPlanApproval ? `Feature plan ready for comment and approval. Review the plan below, add guidance, then explicitly approve a new implementation work order.
|
|
2584
|
+
|
|
2585
|
+
${existing.result || "Hermes completed the plan"}` : existing.result || "Hermes completed the task"
|
|
2262
2586
|
);
|
|
2263
2587
|
return "Hermes completed this task before Simplr reconnected";
|
|
2264
2588
|
}
|
|
@@ -2273,6 +2597,21 @@ async function runHermesCommand(state, command) {
|
|
|
2273
2597
|
if (!runId) {
|
|
2274
2598
|
const prompt = typeof command.payload.prompt === "string" ? command.payload.prompt : "";
|
|
2275
2599
|
if (!prompt) throw new Error("Hermes task prompt is missing");
|
|
2600
|
+
if (existing?.status !== "preparing") {
|
|
2601
|
+
await updateCommandJournal({
|
|
2602
|
+
command_id: command.id,
|
|
2603
|
+
organization_id: state.organization_id,
|
|
2604
|
+
organization_name: state.organization_name,
|
|
2605
|
+
workstation_id: state.workstation_id,
|
|
2606
|
+
type: command.type,
|
|
2607
|
+
label: command.reason,
|
|
2608
|
+
...workOrderClassification(command.payload),
|
|
2609
|
+
status: "preparing",
|
|
2610
|
+
started_at: startedAt,
|
|
2611
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
await claimSimplrFeedbackInCloud(state, command);
|
|
2276
2615
|
const kanbanTaskId = ensureSimplrKanbanTask(state, command, prompt);
|
|
2277
2616
|
await updateCommandJournal({
|
|
2278
2617
|
command_id: command.id,
|
|
@@ -2281,6 +2620,7 @@ async function runHermesCommand(state, command) {
|
|
|
2281
2620
|
workstation_id: state.workstation_id,
|
|
2282
2621
|
type: command.type,
|
|
2283
2622
|
label: command.reason,
|
|
2623
|
+
...workOrderClassification(command.payload),
|
|
2284
2624
|
status: "creating",
|
|
2285
2625
|
kanban_task_id: kanbanTaskId,
|
|
2286
2626
|
started_at: startedAt,
|
|
@@ -2303,6 +2643,7 @@ async function runHermesCommand(state, command) {
|
|
|
2303
2643
|
command_id: command.id,
|
|
2304
2644
|
type: command.type,
|
|
2305
2645
|
label: command.reason,
|
|
2646
|
+
...workOrderClassification(command.payload),
|
|
2306
2647
|
run_id: runId,
|
|
2307
2648
|
status: "running",
|
|
2308
2649
|
started_at: startedAt,
|
|
@@ -2446,7 +2787,7 @@ async function stopManagedProcesses() {
|
|
|
2446
2787
|
return `${processes.length} Simplr-managed AI process${processes.length === 1 ? "" : "es"} terminated`;
|
|
2447
2788
|
}
|
|
2448
2789
|
async function acquireProcessLock(lockPath, activeMessage) {
|
|
2449
|
-
await
|
|
2790
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
2450
2791
|
const create = async () => {
|
|
2451
2792
|
const handle = await open(lockPath, "wx", 384);
|
|
2452
2793
|
await handle.writeFile(`${process.pid}
|
|
@@ -2459,7 +2800,7 @@ async function acquireProcessLock(lockPath, activeMessage) {
|
|
|
2459
2800
|
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
|
|
2460
2801
|
throw error;
|
|
2461
2802
|
const existingPid = Number.parseInt(
|
|
2462
|
-
(await
|
|
2803
|
+
(await readFile2(lockPath, "utf8").catch(() => "0")).trim(),
|
|
2463
2804
|
10
|
|
2464
2805
|
);
|
|
2465
2806
|
let active = false;
|
|
@@ -2612,6 +2953,7 @@ async function applyCommand(state, command, inventoryAlreadySynced) {
|
|
|
2612
2953
|
command_id: command.id,
|
|
2613
2954
|
type: command.type,
|
|
2614
2955
|
label: command.reason,
|
|
2956
|
+
...workOrderClassification(command.payload),
|
|
2615
2957
|
run_id: existing?.run_id,
|
|
2616
2958
|
status: "failed",
|
|
2617
2959
|
started_at: existing?.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2667,22 +3009,38 @@ async function resumeHermesRuns(state) {
|
|
|
2667
3009
|
workstation_id: connection.workstation_id,
|
|
2668
3010
|
type: "run_hermes",
|
|
2669
3011
|
reason: entry.label,
|
|
2670
|
-
payload:
|
|
3012
|
+
payload: workOrderPayload(entry)
|
|
2671
3013
|
},
|
|
2672
3014
|
false
|
|
2673
3015
|
);
|
|
2674
3016
|
}
|
|
2675
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
|
+
}
|
|
2676
3034
|
async function serviceInstallationPresent() {
|
|
2677
3035
|
try {
|
|
2678
3036
|
await access(serviceExecutablePath(), constants.R_OK);
|
|
2679
3037
|
if (serviceExecutableSnapshot) {
|
|
2680
|
-
const deployed = await
|
|
2681
|
-
const expectedHash =
|
|
2682
|
-
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");
|
|
2683
3041
|
if (expectedHash !== deployedHash) return false;
|
|
2684
3042
|
}
|
|
2685
|
-
const definition = await
|
|
3043
|
+
const definition = await readFile2(serviceDefinitionPath(), "utf8");
|
|
2686
3044
|
if (!definition.includes(serviceExecutablePath()) || !definition.includes("watch"))
|
|
2687
3045
|
return false;
|
|
2688
3046
|
if (platform() !== "win32") return true;
|
|
@@ -2724,10 +3082,13 @@ async function watch() {
|
|
|
2724
3082
|
const state = await loadState();
|
|
2725
3083
|
const executableSource = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
|
|
2726
3084
|
if (executableSource)
|
|
2727
|
-
serviceExecutableSnapshot = await
|
|
3085
|
+
serviceExecutableSnapshot = await readFile2(executableSource).catch(
|
|
2728
3086
|
() => void 0
|
|
2729
3087
|
);
|
|
3088
|
+
const executionToolsChanged = await ensureHermesExecutionTools(state);
|
|
2730
3089
|
await ensureSimplrMcpConfiguration(state);
|
|
3090
|
+
if (executionToolsChanged) await restartHermesGateway(state);
|
|
3091
|
+
await reconcileIncompleteCompletedRuns(state);
|
|
2731
3092
|
await resumeHermesRuns(state);
|
|
2732
3093
|
let nextInventoryAt = 0;
|
|
2733
3094
|
let nextServiceAuditAt = 0;
|
|
@@ -2940,10 +3301,10 @@ function systemdValue(value) {
|
|
|
2940
3301
|
}
|
|
2941
3302
|
async function deployServiceExecutable() {
|
|
2942
3303
|
const source = STANDALONE_EXECUTABLE ? process.execPath : process.argv[1];
|
|
2943
|
-
const executable = serviceExecutableSnapshot || (source ? await
|
|
3304
|
+
const executable = serviceExecutableSnapshot || (source ? await readFile2(source) : void 0);
|
|
2944
3305
|
if (!executable)
|
|
2945
3306
|
throw new Error("Simplr Connect executable path is unavailable");
|
|
2946
|
-
await
|
|
3307
|
+
await mkdir2(stateDirectory(), { recursive: true, mode: 448 });
|
|
2947
3308
|
await writeFile(serviceExecutablePath(), executable, {
|
|
2948
3309
|
mode: 448
|
|
2949
3310
|
});
|
|
@@ -2956,9 +3317,9 @@ async function installService(activate = true) {
|
|
|
2956
3317
|
const nodePath = process.execPath;
|
|
2957
3318
|
const connectPath = serviceExecutablePath();
|
|
2958
3319
|
if (platform() === "darwin") {
|
|
2959
|
-
const directory =
|
|
3320
|
+
const directory = join3(homedir(), "Library", "LaunchAgents");
|
|
2960
3321
|
const path = serviceDefinitionPath();
|
|
2961
|
-
await
|
|
3322
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
2962
3323
|
const programArguments = STANDALONE_EXECUTABLE ? [connectPath, "watch"] : [nodePath, connectPath, "watch"];
|
|
2963
3324
|
const plistArguments = programArguments.map((value) => `<string>${xmlValue(value)}</string>`).join("");
|
|
2964
3325
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -2989,9 +3350,9 @@ async function installService(activate = true) {
|
|
|
2989
3350
|
return "Simplr Connect self-healing service installed";
|
|
2990
3351
|
}
|
|
2991
3352
|
if (platform() === "linux") {
|
|
2992
|
-
const directory =
|
|
3353
|
+
const directory = join3(homedir(), ".config", "systemd", "user");
|
|
2993
3354
|
const path = serviceDefinitionPath();
|
|
2994
|
-
await
|
|
3355
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
2995
3356
|
const serviceCommand = STANDALONE_EXECUTABLE ? `${systemdValue(connectPath)} watch` : `${systemdValue(nodePath)} ${systemdValue(connectPath)} watch`;
|
|
2996
3357
|
const unit = `[Unit]
|
|
2997
3358
|
Description=Simplr Connect secure workstation proxy
|
|
@@ -3006,7 +3367,7 @@ RestartSec=10
|
|
|
3006
3367
|
NoNewPrivileges=true
|
|
3007
3368
|
PrivateTmp=true
|
|
3008
3369
|
ProtectSystem=strict
|
|
3009
|
-
ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(
|
|
3370
|
+
ReadWritePaths=${systemdValue(stateDirectory())} ${systemdValue(join3(homedir(), ".hermes"))}
|
|
3010
3371
|
|
|
3011
3372
|
[Install]
|
|
3012
3373
|
WantedBy=default.target
|
|
@@ -3195,7 +3556,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
|
|
|
3195
3556
|
status: "downloaded",
|
|
3196
3557
|
artifact_sha256: artifact.sha256
|
|
3197
3558
|
});
|
|
3198
|
-
if (bytes.byteLength !== artifact.size ||
|
|
3559
|
+
if (bytes.byteLength !== artifact.size || createHash2("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
|
3199
3560
|
throw new Error("Downloaded update failed integrity verification");
|
|
3200
3561
|
await reportStage?.({
|
|
3201
3562
|
status: "verified",
|
|
@@ -3242,7 +3603,7 @@ async function updateConnector(apply, expectedVersion, reportStage, commandId, o
|
|
|
3242
3603
|
1e4
|
|
3243
3604
|
);
|
|
3244
3605
|
else {
|
|
3245
|
-
const helperPath =
|
|
3606
|
+
const helperPath = join3(stateDirectory(), "complete-update.ps1");
|
|
3246
3607
|
const powershellValue = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
3247
3608
|
const helper = `$ErrorActionPreference = "Stop"\r
|
|
3248
3609
|
& schtasks.exe /End /TN "Simplr Connect" | Out-Null\r
|