coding-agent-adapters 0.11.1 → 0.13.0

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.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  var promises = require('fs/promises');
4
4
  var path = require('path');
5
- var ptyManager = require('pty-manager');
5
+ var adapterTypes = require('adapter-types');
6
6
 
7
7
  // src/base-coding-adapter.ts
8
8
 
@@ -337,6 +337,16 @@ function generateAiderApprovalConfig(preset) {
337
337
  summary: `Aider: ${def.description}`
338
338
  };
339
339
  }
340
+ function generateHermesApprovalConfig(preset) {
341
+ const def = getPresetDefinition(preset);
342
+ return {
343
+ preset,
344
+ cliFlags: [],
345
+ workspaceFiles: [],
346
+ envVars: {},
347
+ summary: `Hermes Agent: ${def.description}`
348
+ };
349
+ }
340
350
  function generateApprovalConfig(adapterType, preset) {
341
351
  switch (adapterType) {
342
352
  case "claude":
@@ -347,6 +357,8 @@ function generateApprovalConfig(adapterType, preset) {
347
357
  return generateCodexApprovalConfig(preset);
348
358
  case "aider":
349
359
  return generateAiderApprovalConfig(preset);
360
+ case "hermes":
361
+ return generateHermesApprovalConfig(preset);
350
362
  default:
351
363
  throw new Error(`Unknown adapter type: ${adapterType}`);
352
364
  }
@@ -363,7 +375,7 @@ function getPresetDefinition(preset) {
363
375
  }
364
376
 
365
377
  // src/base-coding-adapter.ts
366
- var BaseCodingAdapter = class extends ptyManager.BaseCLIAdapter {
378
+ var BaseCodingAdapter = class extends adapterTypes.BaseCLIAdapter {
367
379
  /**
368
380
  * Coding agent CLIs use TUI menus that require arrow-key navigation.
369
381
  */
@@ -424,6 +436,13 @@ var BaseCodingAdapter = class extends ptyManager.BaseCLIAdapter {
424
436
  result = result.replace(/ {2,}/g, " ");
425
437
  return result;
426
438
  }
439
+ /**
440
+ * Generate hook telemetry protocol configuration.
441
+ * Returns null by default — only Claude adapter supports hooks.
442
+ */
443
+ getHookTelemetryProtocol(_options) {
444
+ return null;
445
+ }
427
446
  /**
428
447
  * Override detectExit to include installation instructions
429
448
  */
@@ -549,6 +568,7 @@ Docs: ${this.installation.docsUrl}`
549
568
  };
550
569
 
551
570
  // src/claude-adapter.ts
571
+ var CLAUDE_HOOK_MARKER_PREFIX = "PARALLAX_CLAUDE_HOOK";
552
572
  var ClaudeAdapter = class extends BaseCodingAdapter {
553
573
  adapterType = "claude";
554
574
  displayName = "Claude Code";
@@ -586,7 +606,8 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
586
606
  responseType: "keys",
587
607
  keys: ["enter"],
588
608
  description: "Auto-approve tool permission prompts (file access, MCP tools, etc.)",
589
- safe: true
609
+ safe: true,
610
+ once: true
590
611
  },
591
612
  {
592
613
  pattern: /update available.*\[y\/n\]/i,
@@ -695,6 +716,7 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
695
716
  getEnv(config) {
696
717
  const env = {};
697
718
  const credentials = this.getCredentials(config);
719
+ const adapterConfig = config.adapterConfig;
698
720
  if (credentials.anthropicKey) {
699
721
  env.ANTHROPIC_API_KEY = credentials.anthropicKey;
700
722
  }
@@ -704,8 +726,110 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
704
726
  if (!this.isInteractive(config)) {
705
727
  env.CLAUDE_CODE_DISABLE_INTERACTIVE = "true";
706
728
  }
729
+ if (adapterConfig?.claudeHookTelemetry) {
730
+ env.PARALLAX_CLAUDE_HOOK_TELEMETRY = "1";
731
+ env.PARALLAX_CLAUDE_HOOK_MARKER_PREFIX = adapterConfig.claudeHookMarkerPrefix || CLAUDE_HOOK_MARKER_PREFIX;
732
+ }
707
733
  return env;
708
734
  }
735
+ getHookTelemetryProtocol(options) {
736
+ if (options?.httpUrl) {
737
+ const httpHookBase = {
738
+ type: "http",
739
+ url: options.httpUrl,
740
+ timeout: 5
741
+ };
742
+ if (options.sessionId) {
743
+ httpHookBase.headers = { "X-Parallax-Session-Id": options.sessionId };
744
+ }
745
+ const hookEntry2 = [{ matcher: "", hooks: [{ ...httpHookBase }] }];
746
+ const hookEntryNoMatcher = [{ hooks: [{ ...httpHookBase }] }];
747
+ const settingsHooks2 = {
748
+ PermissionRequest: hookEntryNoMatcher,
749
+ PreToolUse: hookEntry2,
750
+ Stop: hookEntryNoMatcher,
751
+ Notification: hookEntry2,
752
+ TaskCompleted: hookEntryNoMatcher
753
+ };
754
+ return {
755
+ markerPrefix: "",
756
+ scriptPath: "",
757
+ scriptContent: "",
758
+ settingsHooks: settingsHooks2
759
+ };
760
+ }
761
+ const markerPrefix = options?.markerPrefix || CLAUDE_HOOK_MARKER_PREFIX;
762
+ const scriptPath = options?.scriptPath || ".claude/hooks/parallax-hook-telemetry.sh";
763
+ const scriptCommand = `"${"$"}CLAUDE_PROJECT_DIR"/${scriptPath}`;
764
+ const hookEntry = [{ matcher: "", hooks: [{ type: "command", command: scriptCommand }] }];
765
+ const settingsHooks = {
766
+ Notification: hookEntry,
767
+ PreToolUse: hookEntry,
768
+ TaskCompleted: hookEntry,
769
+ SessionEnd: hookEntry
770
+ };
771
+ const scriptContent = `#!/usr/bin/env bash
772
+ set -euo pipefail
773
+
774
+ INPUT="$(cat)"
775
+ [ -z "${"$"}INPUT" ] && exit 0
776
+
777
+ if ! command -v jq >/dev/null 2>&1; then
778
+ exit 0
779
+ fi
780
+
781
+ EVENT="$(printf '%s' "${"$"}INPUT" | jq -r '.hook_event_name // empty')"
782
+ [ -z "${"$"}EVENT" ] && exit 0
783
+
784
+ NOTIFICATION_TYPE="$(printf '%s' "${"$"}INPUT" | jq -r '.notification_type // empty')"
785
+ TOOL_NAME="$(printf '%s' "${"$"}INPUT" | jq -r '.tool_name // empty')"
786
+ MESSAGE="$(printf '%s' "${"$"}INPUT" | jq -r '.message // empty')"
787
+
788
+ printf '%s ' '${markerPrefix}'
789
+ jq -nc --arg event "${"$"}EVENT" --arg notification_type "${"$"}NOTIFICATION_TYPE" --arg tool_name "${"$"}TOOL_NAME" --arg message "${"$"}MESSAGE" '({event: $event}
790
+ + (if $notification_type != "" then {notification_type: $notification_type} else {} end)
791
+ + (if $tool_name != "" then {tool_name: $tool_name} else {} end)
792
+ + (if $message != "" then {message: $message} else {} end))'
793
+ `;
794
+ return {
795
+ markerPrefix,
796
+ scriptPath,
797
+ scriptContent,
798
+ settingsHooks
799
+ };
800
+ }
801
+ getHookMarkers(output) {
802
+ const markers = [];
803
+ const markerRegex = /(?:^|\n)\s*([A-Z0-9_]+)\s+(\{[^\n\r]+\})/g;
804
+ let match;
805
+ while ((match = markerRegex.exec(output)) !== null) {
806
+ const markerToken = match[1];
807
+ if (!markerToken.includes("CLAUDE_HOOK")) {
808
+ continue;
809
+ }
810
+ const payload = match[2];
811
+ try {
812
+ const parsed = JSON.parse(payload);
813
+ const event = typeof parsed.event === "string" ? parsed.event : void 0;
814
+ if (!event) continue;
815
+ markers.push({
816
+ event,
817
+ notification_type: typeof parsed.notification_type === "string" ? parsed.notification_type : void 0,
818
+ tool_name: typeof parsed.tool_name === "string" ? parsed.tool_name : void 0,
819
+ message: typeof parsed.message === "string" ? parsed.message : void 0
820
+ });
821
+ } catch {
822
+ }
823
+ }
824
+ return markers;
825
+ }
826
+ getLatestHookMarker(output) {
827
+ const markers = this.getHookMarkers(output);
828
+ return markers.length > 0 ? markers[markers.length - 1] : null;
829
+ }
830
+ stripHookMarkers(output) {
831
+ return output.replace(/(?:^|\n)\s*[A-Z0-9_]*CLAUDE_HOOK[A-Z0-9_]*\s+\{[^\n\r]+\}\s*/g, "\n");
832
+ }
709
833
  detectLogin(output) {
710
834
  const stripped = this.stripAnsi(output);
711
835
  if (stripped.includes("Not logged in") || stripped.includes("Please run /login") || stripped.includes("please log in") || stripped.includes("run /login")) {
@@ -738,6 +862,7 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
738
862
  */
739
863
  detectBlockingPrompt(output) {
740
864
  const stripped = this.stripAnsi(output);
865
+ const marker = this.getLatestHookMarker(stripped);
741
866
  const loginDetection = this.detectLogin(output);
742
867
  if (loginDetection.required) {
743
868
  return {
@@ -749,6 +874,27 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
749
874
  instructions: loginDetection.instructions
750
875
  };
751
876
  }
877
+ if (marker?.event === "Notification") {
878
+ if (marker.notification_type === "permission_prompt") {
879
+ return {
880
+ detected: true,
881
+ type: "permission",
882
+ prompt: marker.message || "Claude permission prompt",
883
+ suggestedResponse: "keys:enter",
884
+ canAutoRespond: true,
885
+ instructions: "Claude is waiting for permission approval"
886
+ };
887
+ }
888
+ if (marker.notification_type === "elicitation_dialog") {
889
+ return {
890
+ detected: true,
891
+ type: "tool_wait",
892
+ prompt: marker.message || "Claude elicitation dialog",
893
+ canAutoRespond: false,
894
+ instructions: "Claude is waiting for required user input"
895
+ };
896
+ }
897
+ }
752
898
  if (/how is claude doing this session\?\s*\(optional\)|1:\s*bad\s+2:\s*fine\s+3:\s*good\s+0:\s*dismiss/i.test(stripped)) {
753
899
  return {
754
900
  detected: true,
@@ -847,7 +993,11 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
847
993
  */
848
994
  detectLoading(output) {
849
995
  const stripped = this.stripAnsi(output);
996
+ const marker = this.getLatestHookMarker(stripped);
850
997
  const tail = stripped.slice(-500);
998
+ if (marker?.event === "PreToolUse") {
999
+ return true;
1000
+ }
851
1001
  if (/esc\s+to\s+interrupt/i.test(tail)) {
852
1002
  return true;
853
1003
  }
@@ -868,7 +1018,14 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
868
1018
  */
869
1019
  detectToolRunning(output) {
870
1020
  const stripped = this.stripAnsi(output);
1021
+ const marker = this.getLatestHookMarker(stripped);
871
1022
  const tail = stripped.slice(-500);
1023
+ if (marker?.event === "PreToolUse" && marker.tool_name) {
1024
+ return {
1025
+ toolName: marker.tool_name.toLowerCase(),
1026
+ description: `${marker.tool_name} (hook)`
1027
+ };
1028
+ }
872
1029
  const contextualMatch = tail.match(/Claude\s+in\s+([A-Za-z0-9._-]+)\s*\[(\w+_tool)\]/i);
873
1030
  if (contextualMatch) {
874
1031
  const appName = contextualMatch[1];
@@ -897,7 +1054,14 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
897
1054
  */
898
1055
  detectTaskComplete(output) {
899
1056
  const stripped = this.stripAnsi(output);
1057
+ const marker = this.getLatestHookMarker(stripped);
900
1058
  if (!stripped.trim()) return false;
1059
+ if (marker?.event === "TaskCompleted") {
1060
+ return true;
1061
+ }
1062
+ if (marker?.event === "Notification" && marker.notification_type === "idle_prompt") {
1063
+ return true;
1064
+ }
901
1065
  if (/trust.*directory|do you want to|needs? your permission/i.test(stripped)) {
902
1066
  return false;
903
1067
  }
@@ -914,7 +1078,16 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
914
1078
  }
915
1079
  detectReady(output) {
916
1080
  const stripped = this.stripAnsi(output);
1081
+ const marker = this.getLatestHookMarker(stripped);
917
1082
  if (!stripped.trim()) return false;
1083
+ if (marker?.event === "Notification") {
1084
+ if (marker.notification_type === "permission_prompt" || marker.notification_type === "elicitation_dialog") {
1085
+ return false;
1086
+ }
1087
+ if (marker.notification_type === "idle_prompt") {
1088
+ return true;
1089
+ }
1090
+ }
918
1091
  if (/trust.*directory|do you want to|needs? your permission/i.test(stripped)) {
919
1092
  return false;
920
1093
  }
@@ -925,7 +1098,8 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
925
1098
  return hasConversationalReadyText || hasLegacyPrompt || hasShortcutsHint;
926
1099
  }
927
1100
  parseOutput(output) {
928
- const stripped = this.stripAnsi(output);
1101
+ const withoutHookMarkers = this.stripHookMarkers(output);
1102
+ const stripped = this.stripAnsi(withoutHookMarkers);
929
1103
  const isComplete = this.isResponseComplete(stripped);
930
1104
  if (!isComplete) {
931
1105
  return null;
@@ -948,9 +1122,18 @@ var ClaudeAdapter = class extends BaseCodingAdapter {
948
1122
  getHealthCheckCommand() {
949
1123
  return "claude --version";
950
1124
  }
1125
+ detectExit(output) {
1126
+ const stripped = this.stripAnsi(output);
1127
+ const marker = this.getLatestHookMarker(stripped);
1128
+ if (marker?.event === "SessionEnd") {
1129
+ return { exited: true, code: 0 };
1130
+ }
1131
+ return super.detectExit(output);
1132
+ }
951
1133
  };
952
1134
 
953
1135
  // src/gemini-adapter.ts
1136
+ var GEMINI_HOOK_MARKER_PREFIX = "PARALLAX_GEMINI_HOOK";
954
1137
  var GeminiAdapter = class extends BaseCodingAdapter {
955
1138
  adapterType = "gemini";
956
1139
  displayName = "Google Gemini";
@@ -1050,6 +1233,7 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1050
1233
  getEnv(config) {
1051
1234
  const env = {};
1052
1235
  const credentials = this.getCredentials(config);
1236
+ const adapterConfig = config.adapterConfig;
1053
1237
  if (credentials.googleKey) {
1054
1238
  env.GOOGLE_API_KEY = credentials.googleKey;
1055
1239
  env.GEMINI_API_KEY = credentials.googleKey;
@@ -1060,8 +1244,115 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1060
1244
  if (!this.isInteractive(config)) {
1061
1245
  env.NO_COLOR = "1";
1062
1246
  }
1247
+ if (adapterConfig?.geminiHookTelemetry) {
1248
+ env.PARALLAX_GEMINI_HOOK_TELEMETRY = "1";
1249
+ env.PARALLAX_GEMINI_HOOK_MARKER_PREFIX = adapterConfig.geminiHookMarkerPrefix || GEMINI_HOOK_MARKER_PREFIX;
1250
+ }
1063
1251
  return env;
1064
1252
  }
1253
+ getHookTelemetryProtocol(options) {
1254
+ if (options?.httpUrl) {
1255
+ const sessionHeader = options.sessionId ? ` -H 'X-Parallax-Session-Id: ${options.sessionId}'` : "";
1256
+ const curlCommand = `bash -c 'curl -sf -X POST "${options.httpUrl}" -H "Content-Type: application/json"${sessionHeader} -d @- --max-time 4 2>/dev/null || echo "{\\"continue\\":true}"'`;
1257
+ const hookEntry2 = [{ matcher: "", hooks: [{ type: "command", command: curlCommand, timeout: 5e3 }] }];
1258
+ const hookEntryNoMatcher = [{ hooks: [{ type: "command", command: curlCommand, timeout: 5e3 }] }];
1259
+ const settingsHooks2 = {
1260
+ BeforeTool: hookEntry2,
1261
+ AfterTool: hookEntry2,
1262
+ AfterAgent: hookEntryNoMatcher,
1263
+ SessionEnd: hookEntryNoMatcher,
1264
+ Notification: hookEntry2
1265
+ };
1266
+ return {
1267
+ markerPrefix: "",
1268
+ scriptPath: "",
1269
+ scriptContent: "",
1270
+ settingsHooks: settingsHooks2
1271
+ };
1272
+ }
1273
+ const markerPrefix = options?.markerPrefix || GEMINI_HOOK_MARKER_PREFIX;
1274
+ const scriptPath = options?.scriptPath || ".gemini/hooks/parallax-hook-telemetry.sh";
1275
+ const scriptCommand = `"${"$"}GEMINI_PROJECT_ROOT"/${scriptPath}`;
1276
+ const hookEntry = [{ matcher: "", hooks: [{ type: "command", command: scriptCommand }] }];
1277
+ const settingsHooks = {
1278
+ Notification: hookEntry,
1279
+ BeforeTool: hookEntry,
1280
+ AfterAgent: hookEntry,
1281
+ SessionEnd: hookEntry
1282
+ };
1283
+ const scriptContent = `#!/usr/bin/env bash
1284
+ set -euo pipefail
1285
+
1286
+ INPUT="$(cat)"
1287
+ [ -z "${"$"}INPUT" ] && exit 0
1288
+
1289
+ if ! command -v jq >/dev/null 2>&1; then
1290
+ # Valid no-op response
1291
+ printf '%s
1292
+ ' '{"continue":true}'
1293
+ exit 0
1294
+ fi
1295
+
1296
+ EVENT="$(printf '%s' "${"$"}INPUT" | jq -r '.hookEventName // .hook_event_name // empty')"
1297
+ [ -z "${"$"}EVENT" ] && { printf '%s
1298
+ ' '{"continue":true}'; exit 0; }
1299
+
1300
+ NOTIFICATION_TYPE="$(printf '%s' "${"$"}INPUT" | jq -r '.notificationType // .notification_type // empty')"
1301
+ TOOL_NAME="$(printf '%s' "${"$"}INPUT" | jq -r '.toolName // .tool_name // empty')"
1302
+ MESSAGE="$(printf '%s' "${"$"}INPUT" | jq -r '.message // empty')"
1303
+
1304
+ PAYLOAD="$(jq -nc \\
1305
+ --arg event "${"$"}EVENT" \\
1306
+ --arg notification_type "${"$"}NOTIFICATION_TYPE" \\
1307
+ --arg tool_name "${"$"}TOOL_NAME" \\
1308
+ --arg message "${"$"}MESSAGE" \\
1309
+ '({event: $event}
1310
+ + (if $notification_type != "" then {notification_type: $notification_type} else {} end)
1311
+ + (if $tool_name != "" then {tool_name: $tool_name} else {} end)
1312
+ + (if $message != "" then {message: $message} else {} end))')"
1313
+
1314
+ MARKER="${markerPrefix} ${"$"}PAYLOAD"
1315
+ jq -nc --arg m "${"$"}MARKER" '{continue: true, suppressOutput: true, systemMessage: $m}'
1316
+ `;
1317
+ return {
1318
+ markerPrefix,
1319
+ scriptPath,
1320
+ scriptContent,
1321
+ settingsHooks
1322
+ };
1323
+ }
1324
+ getHookMarkers(output) {
1325
+ const markers = [];
1326
+ const markerRegex = /(?:^|\n)\s*([A-Z0-9_]+)\s+(\{[^\n\r]+\})/g;
1327
+ let match;
1328
+ while ((match = markerRegex.exec(output)) !== null) {
1329
+ const markerToken = match[1];
1330
+ if (!markerToken.includes("GEMINI_HOOK")) {
1331
+ continue;
1332
+ }
1333
+ const payload = match[2];
1334
+ try {
1335
+ const parsed = JSON.parse(payload);
1336
+ const event = typeof parsed.event === "string" ? parsed.event : void 0;
1337
+ if (!event) continue;
1338
+ markers.push({
1339
+ event,
1340
+ notification_type: typeof parsed.notification_type === "string" ? parsed.notification_type : void 0,
1341
+ tool_name: typeof parsed.tool_name === "string" ? parsed.tool_name : void 0,
1342
+ message: typeof parsed.message === "string" ? parsed.message : void 0
1343
+ });
1344
+ } catch {
1345
+ }
1346
+ }
1347
+ return markers;
1348
+ }
1349
+ getLatestHookMarker(output) {
1350
+ const markers = this.getHookMarkers(output);
1351
+ return markers.length > 0 ? markers[markers.length - 1] : null;
1352
+ }
1353
+ stripHookMarkers(output) {
1354
+ return output.replace(/(?:^|\n)\s*[A-Z0-9_]*GEMINI_HOOK[A-Z0-9_]*\s+\{[^\n\r]+\}\s*/g, "\n");
1355
+ }
1065
1356
  detectLogin(output) {
1066
1357
  const stripped = this.stripAnsi(output);
1067
1358
  if (stripped.includes("API key not found") || /set (?:GOOGLE_API_KEY|GEMINI_API_KEY)/i.test(stripped) || stripped.includes("authentication required") || stripped.includes("Invalid API key") || stripped.includes("API key is not valid")) {
@@ -1112,6 +1403,17 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1112
1403
  }
1113
1404
  detectBlockingPrompt(output) {
1114
1405
  const stripped = this.stripAnsi(output);
1406
+ const marker = this.getLatestHookMarker(stripped);
1407
+ if (marker?.event === "Notification" && marker.notification_type === "ToolPermission") {
1408
+ return {
1409
+ detected: true,
1410
+ type: "permission",
1411
+ prompt: marker.message || "Gemini tool permission",
1412
+ suggestedResponse: "keys:enter",
1413
+ canAutoRespond: true,
1414
+ instructions: "Gemini is asking to allow a tool action"
1415
+ };
1416
+ }
1115
1417
  if (/apply.?this.?change\??/i.test(stripped) || /allow.?execution.?of/i.test(stripped) || /do.?you.?want.?to.?proceed\??/i.test(stripped) || /waiting.?for.?user.?confirmation/i.test(stripped)) {
1116
1418
  return {
1117
1419
  detected: true,
@@ -1207,7 +1509,11 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1207
1509
  */
1208
1510
  detectLoading(output) {
1209
1511
  const stripped = this.stripAnsi(output);
1512
+ const marker = this.getLatestHookMarker(stripped);
1210
1513
  const tail = stripped.slice(-500);
1514
+ if (marker?.event === "BeforeTool") {
1515
+ return true;
1516
+ }
1211
1517
  if (/esc\s+to\s+cancel/i.test(tail)) {
1212
1518
  return true;
1213
1519
  }
@@ -1216,6 +1522,17 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1216
1522
  }
1217
1523
  return false;
1218
1524
  }
1525
+ detectToolRunning(output) {
1526
+ const stripped = this.stripAnsi(output);
1527
+ const marker = this.getLatestHookMarker(stripped);
1528
+ if (marker?.event === "BeforeTool" && marker.tool_name) {
1529
+ return {
1530
+ toolName: marker.tool_name.toLowerCase(),
1531
+ description: `${marker.tool_name} (hook)`
1532
+ };
1533
+ }
1534
+ return null;
1535
+ }
1219
1536
  /**
1220
1537
  * Detect task completion for Gemini CLI.
1221
1538
  *
@@ -1228,6 +1545,10 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1228
1545
  */
1229
1546
  detectTaskComplete(output) {
1230
1547
  const stripped = this.stripAnsi(output);
1548
+ const marker = this.getLatestHookMarker(stripped);
1549
+ if (marker?.event === "AfterAgent") {
1550
+ return true;
1551
+ }
1231
1552
  if (/◇\s+Ready/.test(stripped)) {
1232
1553
  return true;
1233
1554
  }
@@ -1238,6 +1559,13 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1238
1559
  }
1239
1560
  detectReady(output) {
1240
1561
  const stripped = this.stripAnsi(output);
1562
+ const marker = this.getLatestHookMarker(stripped);
1563
+ if (marker?.event === "Notification" && marker.notification_type === "ToolPermission") {
1564
+ return false;
1565
+ }
1566
+ if (marker?.event === "AfterAgent") {
1567
+ return true;
1568
+ }
1241
1569
  const hasActiveOverlay = /interactive\s+shell\s+awaiting\s+input|press\s+tab\s+to\s+focus\s+shell/i.test(stripped) || /waiting\s+for\s+user\s+confirmation|apply.?this.?change|allow.?execution|do.?you.?want.?to.?proceed/i.test(stripped) || /do.?you.?want.?to.?continue\s*\([yY]\/[nN]\)\??|are.?you.?sure\??\s*\([yY]\/[nN]\)\??/i.test(stripped) || /enable.?checkpointing.?to.?recover.?your.?session.?after.?a.?crash/i.test(stripped) || /esc\s+to\s+cancel|esc\s+to\s+interrupt/i.test(stripped);
1242
1570
  if (hasActiveOverlay) {
1243
1571
  return false;
@@ -1255,7 +1583,8 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1255
1583
  /gemini>\s*$/i.test(stripped);
1256
1584
  }
1257
1585
  parseOutput(output) {
1258
- const stripped = this.stripAnsi(output);
1586
+ const withoutHookMarkers = this.stripHookMarkers(output);
1587
+ const stripped = this.stripAnsi(withoutHookMarkers);
1259
1588
  const isComplete = this.isResponseComplete(stripped);
1260
1589
  if (!isComplete) {
1261
1590
  return null;
@@ -1279,6 +1608,13 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1279
1608
  */
1280
1609
  detectExit(output) {
1281
1610
  const stripped = this.stripAnsi(output);
1611
+ const marker = this.getLatestHookMarker(stripped);
1612
+ if (marker?.event === "SessionEnd") {
1613
+ return {
1614
+ exited: true,
1615
+ code: 0
1616
+ };
1617
+ }
1282
1618
  if (/folder.?trust.?level.?must.?be.?selected.*exiting/i.test(stripped)) {
1283
1619
  return {
1284
1620
  exited: true,
@@ -2128,6 +2464,213 @@ var AiderAdapter = class extends BaseCodingAdapter {
2128
2464
  }
2129
2465
  };
2130
2466
 
2467
+ // src/hermes-adapter.ts
2468
+ var HermesAdapter = class extends BaseCodingAdapter {
2469
+ adapterType = "hermes";
2470
+ displayName = "Hermes Agent";
2471
+ /** Prompt-toolkit TUI + spinner rendering needs a slightly longer settle. */
2472
+ readySettleMs = 400;
2473
+ installation = {
2474
+ command: 'pip install "hermes-agent[cli]"',
2475
+ alternatives: [
2476
+ 'pipx install "hermes-agent[cli]"',
2477
+ 'uv tool install "hermes-agent[cli]"'
2478
+ ],
2479
+ docsUrl: "https://github.com/NousResearch/hermes-agent"
2480
+ };
2481
+ getWorkspaceFiles() {
2482
+ return [
2483
+ {
2484
+ relativePath: "AGENTS.md",
2485
+ description: "Project instructions and architecture notes loaded by Hermes context files",
2486
+ autoLoaded: true,
2487
+ type: "memory",
2488
+ format: "markdown"
2489
+ },
2490
+ {
2491
+ relativePath: "SOUL.md",
2492
+ description: "Optional persona/context file auto-injected into Hermes system prompt",
2493
+ autoLoaded: true,
2494
+ type: "memory",
2495
+ format: "markdown"
2496
+ },
2497
+ {
2498
+ relativePath: "cli-config.yaml",
2499
+ description: "Legacy/local Hermes CLI configuration file",
2500
+ autoLoaded: true,
2501
+ type: "config",
2502
+ format: "yaml"
2503
+ }
2504
+ ];
2505
+ }
2506
+ getRecommendedModels(_credentials) {
2507
+ return {
2508
+ powerful: "anthropic/claude-opus-4.6",
2509
+ fast: "google/gemini-3-flash-preview"
2510
+ };
2511
+ }
2512
+ getCommand() {
2513
+ return "hermes";
2514
+ }
2515
+ getArgs(_config) {
2516
+ return ["chat"];
2517
+ }
2518
+ getEnv(config) {
2519
+ const env = {};
2520
+ const credentials = this.getCredentials(config);
2521
+ if (credentials.openaiKey) {
2522
+ env.OPENROUTER_API_KEY = credentials.openaiKey;
2523
+ env.OPENAI_API_KEY = credentials.openaiKey;
2524
+ }
2525
+ if (credentials.anthropicKey) {
2526
+ env.ANTHROPIC_API_KEY = credentials.anthropicKey;
2527
+ }
2528
+ if (credentials.googleKey) {
2529
+ env.GOOGLE_API_KEY = credentials.googleKey;
2530
+ env.GEMINI_API_KEY = credentials.googleKey;
2531
+ }
2532
+ if (!this.isInteractive(config)) {
2533
+ env.HERMES_QUIET = "1";
2534
+ }
2535
+ return env;
2536
+ }
2537
+ detectLogin(output) {
2538
+ const stripped = this.stripAnsi(output);
2539
+ if (/isn.?t configured yet/i.test(stripped) || /no api keys or providers found/i.test(stripped) || /run setup now\?\s*\[y\/n\]/i.test(stripped)) {
2540
+ return {
2541
+ required: true,
2542
+ type: "api_key",
2543
+ instructions: "Hermes requires provider credentials. Run: hermes setup"
2544
+ };
2545
+ }
2546
+ return { required: false };
2547
+ }
2548
+ detectBlockingPrompt(output) {
2549
+ const stripped = this.stripAnsi(output);
2550
+ const loginDetection = this.detectLogin(output);
2551
+ if (loginDetection.required) {
2552
+ return {
2553
+ detected: true,
2554
+ type: "login",
2555
+ prompt: loginDetection.instructions,
2556
+ canAutoRespond: false,
2557
+ instructions: loginDetection.instructions
2558
+ };
2559
+ }
2560
+ if (/Hermes needs your input|Other \(type your answer\)/i.test(stripped)) {
2561
+ return {
2562
+ detected: true,
2563
+ type: "tool_wait",
2564
+ prompt: "Hermes clarify prompt",
2565
+ canAutoRespond: false,
2566
+ instructions: "Hermes is waiting for clarify input (arrow keys + Enter or free text)."
2567
+ };
2568
+ }
2569
+ if (/Sudo Password Required|password hidden|Password \(hidden\):/i.test(stripped)) {
2570
+ return {
2571
+ detected: true,
2572
+ type: "tool_wait",
2573
+ prompt: "Hermes sudo password prompt",
2574
+ canAutoRespond: false,
2575
+ instructions: "Hermes terminal tool is waiting for a sudo password or skip."
2576
+ };
2577
+ }
2578
+ if (/Dangerous Command|Allow once|Allow for this session|permanent allowlist|\bDeny\b/i.test(stripped)) {
2579
+ return {
2580
+ detected: true,
2581
+ type: "permission",
2582
+ prompt: "Hermes dangerous command approval",
2583
+ canAutoRespond: false,
2584
+ instructions: "Choose approval policy (once/session/always/deny)."
2585
+ };
2586
+ }
2587
+ return super.detectBlockingPrompt(output);
2588
+ }
2589
+ detectLoading(output) {
2590
+ const stripped = this.stripAnsi(output);
2591
+ const tail = stripped.slice(-1200);
2592
+ if (/(?:pondering|contemplating|musing|cogitating|ruminating|deliberating|mulling|reflecting|processing|reasoning|analyzing|computing|synthesizing|formulating|brainstorming)\.\.\.\s*\(\d+\.\d+s\)/i.test(tail)) {
2593
+ return true;
2594
+ }
2595
+ if (/\(\d+\.\d+s\)\s*$/.test(tail) && /(?:🔍|📄|💻|⚙️|📖|✍️|🔧|🌐|👆|⌨️|📋|🧠|📚|🎨|🐍|🔀|⚡|💬)/.test(tail)) {
2596
+ return true;
2597
+ }
2598
+ if (/⚕\s*❯\s*$/.test(tail)) {
2599
+ return true;
2600
+ }
2601
+ return false;
2602
+ }
2603
+ detectTaskComplete(output) {
2604
+ if (/╭─\s*⚕\s*Hermes/i.test(output)) {
2605
+ return true;
2606
+ }
2607
+ const stripped = this.stripAnsi(output);
2608
+ if (!stripped.trim()) return false;
2609
+ if (this.detectLoading(stripped)) {
2610
+ return false;
2611
+ }
2612
+ const hasIdlePrompt = /(?:^|\n)\s*❯\s*$/m.test(stripped);
2613
+ const hasToolFeed = /(?:^|\n)\s*┊\s+\S+/m.test(stripped);
2614
+ if (hasIdlePrompt && hasToolFeed) {
2615
+ return true;
2616
+ }
2617
+ return false;
2618
+ }
2619
+ detectReady(output) {
2620
+ const stripped = this.stripAnsi(output);
2621
+ const tail = stripped.slice(-800);
2622
+ if (!tail.trim()) return false;
2623
+ if (this.detectLoading(tail)) {
2624
+ return false;
2625
+ }
2626
+ if (/Hermes needs your input|Sudo Password Required|Dangerous Command/i.test(tail)) {
2627
+ return false;
2628
+ }
2629
+ if (/(?:⚕|⚠|🔐|\?|✎)\s*❯\s*$/.test(tail)) {
2630
+ return false;
2631
+ }
2632
+ return /(?:^|\n)\s*❯\s*$/m.test(tail);
2633
+ }
2634
+ parseOutput(output) {
2635
+ const raw = output;
2636
+ const stripped = this.stripAnsi(output);
2637
+ const complete = this.detectTaskComplete(raw) || this.detectReady(stripped);
2638
+ if (!complete) {
2639
+ return null;
2640
+ }
2641
+ let content = "";
2642
+ const boxMatch = raw.match(/╭─\s*⚕\s*Hermes[^\n]*\n([\s\S]*?)\n\s*╰/i);
2643
+ if (boxMatch?.[1]) {
2644
+ content = boxMatch[1].trim();
2645
+ }
2646
+ if (!content) {
2647
+ content = this.extractContent(stripped, /(?:^|\n)\s*(?:⚕|⚠|🔐|\?|✎)?\s*❯\s*$/gim);
2648
+ }
2649
+ return {
2650
+ type: this.containsQuestion(content) ? "question" : "response",
2651
+ content,
2652
+ isComplete: true,
2653
+ isQuestion: this.containsQuestion(content),
2654
+ metadata: {
2655
+ raw: output
2656
+ }
2657
+ };
2658
+ }
2659
+ getPromptPattern() {
2660
+ return /(?:^|\n)\s*(?:⚕|⚠|🔐|\?|✎)?\s*❯\s*$/i;
2661
+ }
2662
+ detectExit(output) {
2663
+ const stripped = this.stripAnsi(output);
2664
+ if (/Goodbye!\s*⚕/i.test(stripped)) {
2665
+ return { exited: true, code: 0 };
2666
+ }
2667
+ return super.detectExit(output);
2668
+ }
2669
+ getHealthCheckCommand() {
2670
+ return "hermes version";
2671
+ }
2672
+ };
2673
+
2131
2674
  // src/pattern-loader.ts
2132
2675
  var BASELINE_PATTERNS = {
2133
2676
  claude: {
@@ -2251,6 +2794,41 @@ var BASELINE_PATTERNS = {
2251
2794
  toolWait: [],
2252
2795
  exit: [],
2253
2796
  source: "baseline"
2797
+ },
2798
+ hermes: {
2799
+ ready: [
2800
+ "\u276F",
2801
+ "\u2695 Hermes",
2802
+ "Welcome to Hermes Agent"
2803
+ ],
2804
+ auth: [
2805
+ "isn't configured yet",
2806
+ "no API keys or providers found",
2807
+ "Run setup now? [Y/n]"
2808
+ ],
2809
+ blocking: [
2810
+ "Hermes needs your input",
2811
+ "Sudo Password Required",
2812
+ "Dangerous Command"
2813
+ ],
2814
+ loading: [
2815
+ "deliberating...",
2816
+ "(0.0s)",
2817
+ "\u2695 \u276F"
2818
+ ],
2819
+ turnComplete: [
2820
+ "\u256D\u2500 \u2695 Hermes",
2821
+ "\u276F"
2822
+ ],
2823
+ toolWait: [
2824
+ "Hermes needs your input",
2825
+ "Sudo Password Required",
2826
+ "Dangerous Command"
2827
+ ],
2828
+ exit: [
2829
+ "Goodbye! \u2695"
2830
+ ],
2831
+ source: "baseline"
2254
2832
  }
2255
2833
  };
2256
2834
  var patternCache = /* @__PURE__ */ new Map();
@@ -2321,14 +2899,16 @@ function createAllAdapters() {
2321
2899
  new ClaudeAdapter(),
2322
2900
  new GeminiAdapter(),
2323
2901
  new CodexAdapter(),
2324
- new AiderAdapter()
2902
+ new AiderAdapter(),
2903
+ new HermesAdapter()
2325
2904
  ];
2326
2905
  }
2327
2906
  var ADAPTER_TYPES = {
2328
2907
  claude: ClaudeAdapter,
2329
2908
  gemini: GeminiAdapter,
2330
2909
  codex: CodexAdapter,
2331
- aider: AiderAdapter
2910
+ aider: AiderAdapter,
2911
+ hermes: HermesAdapter
2332
2912
  };
2333
2913
  function createAdapter(type) {
2334
2914
  const AdapterClass = ADAPTER_TYPES[type];
@@ -2385,6 +2965,7 @@ exports.ClaudeAdapter = ClaudeAdapter;
2385
2965
  exports.CodexAdapter = CodexAdapter;
2386
2966
  exports.GEMINI_TOOL_CATEGORIES = GEMINI_TOOL_CATEGORIES;
2387
2967
  exports.GeminiAdapter = GeminiAdapter;
2968
+ exports.HermesAdapter = HermesAdapter;
2388
2969
  exports.PRESET_DEFINITIONS = PRESET_DEFINITIONS;
2389
2970
  exports.TOOL_CATEGORIES = TOOL_CATEGORIES;
2390
2971
  exports.checkAdapters = checkAdapters;
@@ -2397,6 +2978,7 @@ exports.generateApprovalConfig = generateApprovalConfig;
2397
2978
  exports.generateClaudeApprovalConfig = generateClaudeApprovalConfig;
2398
2979
  exports.generateCodexApprovalConfig = generateCodexApprovalConfig;
2399
2980
  exports.generateGeminiApprovalConfig = generateGeminiApprovalConfig;
2981
+ exports.generateHermesApprovalConfig = generateHermesApprovalConfig;
2400
2982
  exports.getBaselinePatterns = getBaselinePatterns;
2401
2983
  exports.getPresetDefinition = getPresetDefinition;
2402
2984
  exports.hasDynamicPatterns = hasDynamicPatterns;