coding-agent-adapters 0.11.1 → 0.12.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
@@ -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
  }
@@ -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,96 @@ 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
+ const markerPrefix = options?.markerPrefix || GEMINI_HOOK_MARKER_PREFIX;
1255
+ const scriptPath = options?.scriptPath || ".gemini/hooks/parallax-hook-telemetry.sh";
1256
+ const scriptCommand = `"${"$"}GEMINI_PROJECT_ROOT"/${scriptPath}`;
1257
+ const hookEntry = [{ matcher: "", hooks: [{ type: "command", command: scriptCommand }] }];
1258
+ const settingsHooks = {
1259
+ Notification: hookEntry,
1260
+ BeforeTool: hookEntry,
1261
+ AfterAgent: hookEntry,
1262
+ SessionEnd: hookEntry
1263
+ };
1264
+ const scriptContent = `#!/usr/bin/env bash
1265
+ set -euo pipefail
1266
+
1267
+ INPUT="$(cat)"
1268
+ [ -z "${"$"}INPUT" ] && exit 0
1269
+
1270
+ if ! command -v jq >/dev/null 2>&1; then
1271
+ # Valid no-op response
1272
+ printf '%s
1273
+ ' '{"continue":true}'
1274
+ exit 0
1275
+ fi
1276
+
1277
+ EVENT="$(printf '%s' "${"$"}INPUT" | jq -r '.hookEventName // .hook_event_name // empty')"
1278
+ [ -z "${"$"}EVENT" ] && { printf '%s
1279
+ ' '{"continue":true}'; exit 0; }
1280
+
1281
+ NOTIFICATION_TYPE="$(printf '%s' "${"$"}INPUT" | jq -r '.notificationType // .notification_type // empty')"
1282
+ TOOL_NAME="$(printf '%s' "${"$"}INPUT" | jq -r '.toolName // .tool_name // empty')"
1283
+ MESSAGE="$(printf '%s' "${"$"}INPUT" | jq -r '.message // empty')"
1284
+
1285
+ PAYLOAD="$(jq -nc \\
1286
+ --arg event "${"$"}EVENT" \\
1287
+ --arg notification_type "${"$"}NOTIFICATION_TYPE" \\
1288
+ --arg tool_name "${"$"}TOOL_NAME" \\
1289
+ --arg message "${"$"}MESSAGE" \\
1290
+ '({event: $event}
1291
+ + (if $notification_type != "" then {notification_type: $notification_type} else {} end)
1292
+ + (if $tool_name != "" then {tool_name: $tool_name} else {} end)
1293
+ + (if $message != "" then {message: $message} else {} end))')"
1294
+
1295
+ MARKER="${markerPrefix} ${"$"}PAYLOAD"
1296
+ jq -nc --arg m "${"$"}MARKER" '{continue: true, suppressOutput: true, systemMessage: $m}'
1297
+ `;
1298
+ return {
1299
+ markerPrefix,
1300
+ scriptPath,
1301
+ scriptContent,
1302
+ settingsHooks
1303
+ };
1304
+ }
1305
+ getHookMarkers(output) {
1306
+ const markers = [];
1307
+ const markerRegex = /(?:^|\n)\s*([A-Z0-9_]+)\s+(\{[^\n\r]+\})/g;
1308
+ let match;
1309
+ while ((match = markerRegex.exec(output)) !== null) {
1310
+ const markerToken = match[1];
1311
+ if (!markerToken.includes("GEMINI_HOOK")) {
1312
+ continue;
1313
+ }
1314
+ const payload = match[2];
1315
+ try {
1316
+ const parsed = JSON.parse(payload);
1317
+ const event = typeof parsed.event === "string" ? parsed.event : void 0;
1318
+ if (!event) continue;
1319
+ markers.push({
1320
+ event,
1321
+ notification_type: typeof parsed.notification_type === "string" ? parsed.notification_type : void 0,
1322
+ tool_name: typeof parsed.tool_name === "string" ? parsed.tool_name : void 0,
1323
+ message: typeof parsed.message === "string" ? parsed.message : void 0
1324
+ });
1325
+ } catch {
1326
+ }
1327
+ }
1328
+ return markers;
1329
+ }
1330
+ getLatestHookMarker(output) {
1331
+ const markers = this.getHookMarkers(output);
1332
+ return markers.length > 0 ? markers[markers.length - 1] : null;
1333
+ }
1334
+ stripHookMarkers(output) {
1335
+ return output.replace(/(?:^|\n)\s*[A-Z0-9_]*GEMINI_HOOK[A-Z0-9_]*\s+\{[^\n\r]+\}\s*/g, "\n");
1336
+ }
1065
1337
  detectLogin(output) {
1066
1338
  const stripped = this.stripAnsi(output);
1067
1339
  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 +1384,17 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1112
1384
  }
1113
1385
  detectBlockingPrompt(output) {
1114
1386
  const stripped = this.stripAnsi(output);
1387
+ const marker = this.getLatestHookMarker(stripped);
1388
+ if (marker?.event === "Notification" && marker.notification_type === "ToolPermission") {
1389
+ return {
1390
+ detected: true,
1391
+ type: "permission",
1392
+ prompt: marker.message || "Gemini tool permission",
1393
+ suggestedResponse: "keys:enter",
1394
+ canAutoRespond: true,
1395
+ instructions: "Gemini is asking to allow a tool action"
1396
+ };
1397
+ }
1115
1398
  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
1399
  return {
1117
1400
  detected: true,
@@ -1207,7 +1490,11 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1207
1490
  */
1208
1491
  detectLoading(output) {
1209
1492
  const stripped = this.stripAnsi(output);
1493
+ const marker = this.getLatestHookMarker(stripped);
1210
1494
  const tail = stripped.slice(-500);
1495
+ if (marker?.event === "BeforeTool") {
1496
+ return true;
1497
+ }
1211
1498
  if (/esc\s+to\s+cancel/i.test(tail)) {
1212
1499
  return true;
1213
1500
  }
@@ -1216,6 +1503,17 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1216
1503
  }
1217
1504
  return false;
1218
1505
  }
1506
+ detectToolRunning(output) {
1507
+ const stripped = this.stripAnsi(output);
1508
+ const marker = this.getLatestHookMarker(stripped);
1509
+ if (marker?.event === "BeforeTool" && marker.tool_name) {
1510
+ return {
1511
+ toolName: marker.tool_name.toLowerCase(),
1512
+ description: `${marker.tool_name} (hook)`
1513
+ };
1514
+ }
1515
+ return null;
1516
+ }
1219
1517
  /**
1220
1518
  * Detect task completion for Gemini CLI.
1221
1519
  *
@@ -1228,6 +1526,10 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1228
1526
  */
1229
1527
  detectTaskComplete(output) {
1230
1528
  const stripped = this.stripAnsi(output);
1529
+ const marker = this.getLatestHookMarker(stripped);
1530
+ if (marker?.event === "AfterAgent") {
1531
+ return true;
1532
+ }
1231
1533
  if (/◇\s+Ready/.test(stripped)) {
1232
1534
  return true;
1233
1535
  }
@@ -1238,6 +1540,13 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1238
1540
  }
1239
1541
  detectReady(output) {
1240
1542
  const stripped = this.stripAnsi(output);
1543
+ const marker = this.getLatestHookMarker(stripped);
1544
+ if (marker?.event === "Notification" && marker.notification_type === "ToolPermission") {
1545
+ return false;
1546
+ }
1547
+ if (marker?.event === "AfterAgent") {
1548
+ return true;
1549
+ }
1241
1550
  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
1551
  if (hasActiveOverlay) {
1243
1552
  return false;
@@ -1255,7 +1564,8 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1255
1564
  /gemini>\s*$/i.test(stripped);
1256
1565
  }
1257
1566
  parseOutput(output) {
1258
- const stripped = this.stripAnsi(output);
1567
+ const withoutHookMarkers = this.stripHookMarkers(output);
1568
+ const stripped = this.stripAnsi(withoutHookMarkers);
1259
1569
  const isComplete = this.isResponseComplete(stripped);
1260
1570
  if (!isComplete) {
1261
1571
  return null;
@@ -1279,6 +1589,13 @@ var GeminiAdapter = class extends BaseCodingAdapter {
1279
1589
  */
1280
1590
  detectExit(output) {
1281
1591
  const stripped = this.stripAnsi(output);
1592
+ const marker = this.getLatestHookMarker(stripped);
1593
+ if (marker?.event === "SessionEnd") {
1594
+ return {
1595
+ exited: true,
1596
+ code: 0
1597
+ };
1598
+ }
1282
1599
  if (/folder.?trust.?level.?must.?be.?selected.*exiting/i.test(stripped)) {
1283
1600
  return {
1284
1601
  exited: true,
@@ -2128,6 +2445,213 @@ var AiderAdapter = class extends BaseCodingAdapter {
2128
2445
  }
2129
2446
  };
2130
2447
 
2448
+ // src/hermes-adapter.ts
2449
+ var HermesAdapter = class extends BaseCodingAdapter {
2450
+ adapterType = "hermes";
2451
+ displayName = "Hermes Agent";
2452
+ /** Prompt-toolkit TUI + spinner rendering needs a slightly longer settle. */
2453
+ readySettleMs = 400;
2454
+ installation = {
2455
+ command: 'pip install "hermes-agent[cli]"',
2456
+ alternatives: [
2457
+ 'pipx install "hermes-agent[cli]"',
2458
+ 'uv tool install "hermes-agent[cli]"'
2459
+ ],
2460
+ docsUrl: "https://github.com/NousResearch/hermes-agent"
2461
+ };
2462
+ getWorkspaceFiles() {
2463
+ return [
2464
+ {
2465
+ relativePath: "AGENTS.md",
2466
+ description: "Project instructions and architecture notes loaded by Hermes context files",
2467
+ autoLoaded: true,
2468
+ type: "memory",
2469
+ format: "markdown"
2470
+ },
2471
+ {
2472
+ relativePath: "SOUL.md",
2473
+ description: "Optional persona/context file auto-injected into Hermes system prompt",
2474
+ autoLoaded: true,
2475
+ type: "memory",
2476
+ format: "markdown"
2477
+ },
2478
+ {
2479
+ relativePath: "cli-config.yaml",
2480
+ description: "Legacy/local Hermes CLI configuration file",
2481
+ autoLoaded: true,
2482
+ type: "config",
2483
+ format: "yaml"
2484
+ }
2485
+ ];
2486
+ }
2487
+ getRecommendedModels(_credentials) {
2488
+ return {
2489
+ powerful: "anthropic/claude-opus-4.6",
2490
+ fast: "google/gemini-3-flash-preview"
2491
+ };
2492
+ }
2493
+ getCommand() {
2494
+ return "hermes";
2495
+ }
2496
+ getArgs(_config) {
2497
+ return ["chat"];
2498
+ }
2499
+ getEnv(config) {
2500
+ const env = {};
2501
+ const credentials = this.getCredentials(config);
2502
+ if (credentials.openaiKey) {
2503
+ env.OPENROUTER_API_KEY = credentials.openaiKey;
2504
+ env.OPENAI_API_KEY = credentials.openaiKey;
2505
+ }
2506
+ if (credentials.anthropicKey) {
2507
+ env.ANTHROPIC_API_KEY = credentials.anthropicKey;
2508
+ }
2509
+ if (credentials.googleKey) {
2510
+ env.GOOGLE_API_KEY = credentials.googleKey;
2511
+ env.GEMINI_API_KEY = credentials.googleKey;
2512
+ }
2513
+ if (!this.isInteractive(config)) {
2514
+ env.HERMES_QUIET = "1";
2515
+ }
2516
+ return env;
2517
+ }
2518
+ detectLogin(output) {
2519
+ const stripped = this.stripAnsi(output);
2520
+ 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)) {
2521
+ return {
2522
+ required: true,
2523
+ type: "api_key",
2524
+ instructions: "Hermes requires provider credentials. Run: hermes setup"
2525
+ };
2526
+ }
2527
+ return { required: false };
2528
+ }
2529
+ detectBlockingPrompt(output) {
2530
+ const stripped = this.stripAnsi(output);
2531
+ const loginDetection = this.detectLogin(output);
2532
+ if (loginDetection.required) {
2533
+ return {
2534
+ detected: true,
2535
+ type: "login",
2536
+ prompt: loginDetection.instructions,
2537
+ canAutoRespond: false,
2538
+ instructions: loginDetection.instructions
2539
+ };
2540
+ }
2541
+ if (/Hermes needs your input|Other \(type your answer\)/i.test(stripped)) {
2542
+ return {
2543
+ detected: true,
2544
+ type: "tool_wait",
2545
+ prompt: "Hermes clarify prompt",
2546
+ canAutoRespond: false,
2547
+ instructions: "Hermes is waiting for clarify input (arrow keys + Enter or free text)."
2548
+ };
2549
+ }
2550
+ if (/Sudo Password Required|password hidden|Password \(hidden\):/i.test(stripped)) {
2551
+ return {
2552
+ detected: true,
2553
+ type: "tool_wait",
2554
+ prompt: "Hermes sudo password prompt",
2555
+ canAutoRespond: false,
2556
+ instructions: "Hermes terminal tool is waiting for a sudo password or skip."
2557
+ };
2558
+ }
2559
+ if (/Dangerous Command|Allow once|Allow for this session|permanent allowlist|\bDeny\b/i.test(stripped)) {
2560
+ return {
2561
+ detected: true,
2562
+ type: "permission",
2563
+ prompt: "Hermes dangerous command approval",
2564
+ canAutoRespond: false,
2565
+ instructions: "Choose approval policy (once/session/always/deny)."
2566
+ };
2567
+ }
2568
+ return super.detectBlockingPrompt(output);
2569
+ }
2570
+ detectLoading(output) {
2571
+ const stripped = this.stripAnsi(output);
2572
+ const tail = stripped.slice(-1200);
2573
+ if (/(?:pondering|contemplating|musing|cogitating|ruminating|deliberating|mulling|reflecting|processing|reasoning|analyzing|computing|synthesizing|formulating|brainstorming)\.\.\.\s*\(\d+\.\d+s\)/i.test(tail)) {
2574
+ return true;
2575
+ }
2576
+ if (/\(\d+\.\d+s\)\s*$/.test(tail) && /(?:🔍|📄|💻|⚙️|📖|✍️|🔧|🌐|👆|⌨️|📋|🧠|📚|🎨|🐍|🔀|⚡|💬)/.test(tail)) {
2577
+ return true;
2578
+ }
2579
+ if (/⚕\s*❯\s*$/.test(tail)) {
2580
+ return true;
2581
+ }
2582
+ return false;
2583
+ }
2584
+ detectTaskComplete(output) {
2585
+ if (/╭─\s*⚕\s*Hermes/i.test(output)) {
2586
+ return true;
2587
+ }
2588
+ const stripped = this.stripAnsi(output);
2589
+ if (!stripped.trim()) return false;
2590
+ if (this.detectLoading(stripped)) {
2591
+ return false;
2592
+ }
2593
+ const hasIdlePrompt = /(?:^|\n)\s*❯\s*$/m.test(stripped);
2594
+ const hasToolFeed = /(?:^|\n)\s*┊\s+\S+/m.test(stripped);
2595
+ if (hasIdlePrompt && hasToolFeed) {
2596
+ return true;
2597
+ }
2598
+ return false;
2599
+ }
2600
+ detectReady(output) {
2601
+ const stripped = this.stripAnsi(output);
2602
+ const tail = stripped.slice(-800);
2603
+ if (!tail.trim()) return false;
2604
+ if (this.detectLoading(tail)) {
2605
+ return false;
2606
+ }
2607
+ if (/Hermes needs your input|Sudo Password Required|Dangerous Command/i.test(tail)) {
2608
+ return false;
2609
+ }
2610
+ if (/(?:⚕|⚠|🔐|\?|✎)\s*❯\s*$/.test(tail)) {
2611
+ return false;
2612
+ }
2613
+ return /(?:^|\n)\s*❯\s*$/m.test(tail);
2614
+ }
2615
+ parseOutput(output) {
2616
+ const raw = output;
2617
+ const stripped = this.stripAnsi(output);
2618
+ const complete = this.detectTaskComplete(raw) || this.detectReady(stripped);
2619
+ if (!complete) {
2620
+ return null;
2621
+ }
2622
+ let content = "";
2623
+ const boxMatch = raw.match(/╭─\s*⚕\s*Hermes[^\n]*\n([\s\S]*?)\n\s*╰/i);
2624
+ if (boxMatch?.[1]) {
2625
+ content = boxMatch[1].trim();
2626
+ }
2627
+ if (!content) {
2628
+ content = this.extractContent(stripped, /(?:^|\n)\s*(?:⚕|⚠|🔐|\?|✎)?\s*❯\s*$/gim);
2629
+ }
2630
+ return {
2631
+ type: this.containsQuestion(content) ? "question" : "response",
2632
+ content,
2633
+ isComplete: true,
2634
+ isQuestion: this.containsQuestion(content),
2635
+ metadata: {
2636
+ raw: output
2637
+ }
2638
+ };
2639
+ }
2640
+ getPromptPattern() {
2641
+ return /(?:^|\n)\s*(?:⚕|⚠|🔐|\?|✎)?\s*❯\s*$/i;
2642
+ }
2643
+ detectExit(output) {
2644
+ const stripped = this.stripAnsi(output);
2645
+ if (/Goodbye!\s*⚕/i.test(stripped)) {
2646
+ return { exited: true, code: 0 };
2647
+ }
2648
+ return super.detectExit(output);
2649
+ }
2650
+ getHealthCheckCommand() {
2651
+ return "hermes version";
2652
+ }
2653
+ };
2654
+
2131
2655
  // src/pattern-loader.ts
2132
2656
  var BASELINE_PATTERNS = {
2133
2657
  claude: {
@@ -2251,6 +2775,41 @@ var BASELINE_PATTERNS = {
2251
2775
  toolWait: [],
2252
2776
  exit: [],
2253
2777
  source: "baseline"
2778
+ },
2779
+ hermes: {
2780
+ ready: [
2781
+ "\u276F",
2782
+ "\u2695 Hermes",
2783
+ "Welcome to Hermes Agent"
2784
+ ],
2785
+ auth: [
2786
+ "isn't configured yet",
2787
+ "no API keys or providers found",
2788
+ "Run setup now? [Y/n]"
2789
+ ],
2790
+ blocking: [
2791
+ "Hermes needs your input",
2792
+ "Sudo Password Required",
2793
+ "Dangerous Command"
2794
+ ],
2795
+ loading: [
2796
+ "deliberating...",
2797
+ "(0.0s)",
2798
+ "\u2695 \u276F"
2799
+ ],
2800
+ turnComplete: [
2801
+ "\u256D\u2500 \u2695 Hermes",
2802
+ "\u276F"
2803
+ ],
2804
+ toolWait: [
2805
+ "Hermes needs your input",
2806
+ "Sudo Password Required",
2807
+ "Dangerous Command"
2808
+ ],
2809
+ exit: [
2810
+ "Goodbye! \u2695"
2811
+ ],
2812
+ source: "baseline"
2254
2813
  }
2255
2814
  };
2256
2815
  var patternCache = /* @__PURE__ */ new Map();
@@ -2321,14 +2880,16 @@ function createAllAdapters() {
2321
2880
  new ClaudeAdapter(),
2322
2881
  new GeminiAdapter(),
2323
2882
  new CodexAdapter(),
2324
- new AiderAdapter()
2883
+ new AiderAdapter(),
2884
+ new HermesAdapter()
2325
2885
  ];
2326
2886
  }
2327
2887
  var ADAPTER_TYPES = {
2328
2888
  claude: ClaudeAdapter,
2329
2889
  gemini: GeminiAdapter,
2330
2890
  codex: CodexAdapter,
2331
- aider: AiderAdapter
2891
+ aider: AiderAdapter,
2892
+ hermes: HermesAdapter
2332
2893
  };
2333
2894
  function createAdapter(type) {
2334
2895
  const AdapterClass = ADAPTER_TYPES[type];
@@ -2385,6 +2946,7 @@ exports.ClaudeAdapter = ClaudeAdapter;
2385
2946
  exports.CodexAdapter = CodexAdapter;
2386
2947
  exports.GEMINI_TOOL_CATEGORIES = GEMINI_TOOL_CATEGORIES;
2387
2948
  exports.GeminiAdapter = GeminiAdapter;
2949
+ exports.HermesAdapter = HermesAdapter;
2388
2950
  exports.PRESET_DEFINITIONS = PRESET_DEFINITIONS;
2389
2951
  exports.TOOL_CATEGORIES = TOOL_CATEGORIES;
2390
2952
  exports.checkAdapters = checkAdapters;
@@ -2397,6 +2959,7 @@ exports.generateApprovalConfig = generateApprovalConfig;
2397
2959
  exports.generateClaudeApprovalConfig = generateClaudeApprovalConfig;
2398
2960
  exports.generateCodexApprovalConfig = generateCodexApprovalConfig;
2399
2961
  exports.generateGeminiApprovalConfig = generateGeminiApprovalConfig;
2962
+ exports.generateHermesApprovalConfig = generateHermesApprovalConfig;
2400
2963
  exports.getBaselinePatterns = getBaselinePatterns;
2401
2964
  exports.getPresetDefinition = getPresetDefinition;
2402
2965
  exports.hasDynamicPatterns = hasDynamicPatterns;