pi-advisor-flow 0.6.0 → 0.8.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
@@ -1,9 +1,12 @@
1
- import type { ConfigKeySchema } from "./schema.ts";
2
- import { CONFIG_SCHEMA, type ConfigKey, SCHEMA_BY_KEY } from "./schema.ts";
1
+ import { isBoolean, isRecord, isString } from "../content-utils.ts";
2
+ import type { JsonValue } from "../content-utils.ts";
3
+ import type { ConfigKey, ConfigKeySchema } from "./schema.ts";
4
+ import { configKeys, SCHEMA_BY_KEY } from "./schema.ts";
3
5
  import type { AdvisorConfig } from "./types.ts";
4
6
 
5
- // biome-ignore lint/performance/noBarrelFile: re-exports keep the isValid* validators on their historical module for the config facade.
7
+ // Re-exports keep the isValid* validators on their historical module for the config facade.
6
8
  export {
9
+ isValidAdvisorModelWhitelist,
7
10
  isValidAdvisorToolPolicies,
8
11
  isValidContextMaxChars,
9
12
  isValidGateFailureMode,
@@ -13,19 +16,15 @@ export {
13
16
  isValidToolResultMaxLines,
14
17
  } from "./schema.ts";
15
18
 
16
- const CONFIG_KEYS = new Set<ConfigKey>(
17
- Object.keys(CONFIG_SCHEMA) as ConfigKey[]
18
- );
19
+ const CONFIG_KEYS = new Set<string>(configKeys);
19
20
 
20
21
  const keysOfType = (type: ConfigKeySchema["type"]): readonly ConfigKey[] =>
21
- (Object.keys(CONFIG_SCHEMA) as ConfigKey[]).filter(
22
- (key) => SCHEMA_BY_KEY[key].type === type
23
- );
22
+ configKeys.filter((key) => SCHEMA_BY_KEY[key].type === type);
24
23
 
25
24
  const BOOLEAN_CONFIG_KEYS = keysOfType("boolean");
26
25
  const STRING_CONFIG_KEYS = keysOfType("string");
27
26
 
28
- type ConfigRecord = Record<string, unknown>;
27
+ type ConfigRecord = Record<string, JsonValue>;
29
28
 
30
29
  const invalidConfigValue = (
31
30
  path: string,
@@ -37,12 +36,12 @@ const invalidConfigValue = (
37
36
  );
38
37
  };
39
38
 
40
- export const unknownConfigKeys = (config: ConfigRecord) =>
41
- Object.keys(config).filter((key) => !CONFIG_KEYS.has(key as ConfigKey));
39
+ export const unknownConfigKeys = (config: AdvisorConfig) =>
40
+ Object.keys(config).filter((key) => !CONFIG_KEYS.has(key));
42
41
 
43
42
  const validateStringValues = (config: ConfigRecord, path: string) => {
44
43
  for (const key of STRING_CONFIG_KEYS) {
45
- if (config[key] !== undefined && typeof config[key] !== "string") {
44
+ if (config[key] !== undefined && !isString(config[key])) {
46
45
  invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
47
46
  }
48
47
  }
@@ -50,7 +49,7 @@ const validateStringValues = (config: ConfigRecord, path: string) => {
50
49
 
51
50
  const validateBooleanValues = (config: ConfigRecord, path: string) => {
52
51
  for (const key of BOOLEAN_CONFIG_KEYS) {
53
- if (config[key] !== undefined && typeof config[key] !== "boolean") {
52
+ if (config[key] !== undefined && !isBoolean(config[key])) {
54
53
  invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
55
54
  }
56
55
  }
@@ -69,6 +68,19 @@ const validateNumericValues = (config: ConfigRecord, path: string) => {
69
68
  }
70
69
  };
71
70
 
71
+ const validateArrayValues = (config: ConfigRecord, path: string) => {
72
+ for (const key of keysOfType("array")) {
73
+ const isValid = SCHEMA_BY_KEY[key].validate;
74
+ if (
75
+ config[key] !== undefined &&
76
+ isValid !== undefined &&
77
+ !isValid(config[key])
78
+ ) {
79
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
80
+ }
81
+ }
82
+ };
83
+
72
84
  const validateEnumValues = (config: ConfigRecord, path: string) => {
73
85
  for (const key of keysOfType("enum")) {
74
86
  const isValid = SCHEMA_BY_KEY[key].validate;
@@ -99,16 +111,16 @@ export const validateConfig = (
99
111
  value: unknown,
100
112
  path = "advisor.json"
101
113
  ): value is AdvisorConfig => {
102
- if (!value || typeof value !== "object" || Array.isArray(value)) {
114
+ if (!isRecord(value)) {
103
115
  throw new TypeError(
104
116
  `Invalid advisor configuration at ${path}: expected a JSON object.`
105
117
  );
106
118
  }
107
- const config = value as ConfigRecord;
108
- validateStringValues(config, path);
109
- validateBooleanValues(config, path);
110
- validateNumericValues(config, path);
111
- validateObjectValues(config, path);
112
- validateEnumValues(config, path);
119
+ validateStringValues(value, path);
120
+ validateBooleanValues(value, path);
121
+ validateNumericValues(value, path);
122
+ validateObjectValues(value, path);
123
+ validateArrayValues(value, path);
124
+ validateEnumValues(value, path);
113
125
  return true;
114
126
  };
package/src/config.ts CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  advisorHerdrIntegrationRef,
15
15
  advisorLoopThresholdRef,
16
16
  advisorMaxCallsPerSessionRef,
17
+ advisorModelWhitelistRef,
17
18
  advisorOutcomeLoggingRef,
18
19
  advisorPlanGateRef,
19
20
  advisorRedactSecretsRef,
@@ -46,6 +47,7 @@ export {
46
47
  setAdvisorHerdrIntegrationRef,
47
48
  setAdvisorLoopThresholdRef,
48
49
  setAdvisorMaxCallsPerSessionRef,
50
+ setAdvisorModelWhitelistRef,
49
51
  setAdvisorOutcomeLoggingRef,
50
52
  setAdvisorPlanGateRef,
51
53
  setAdvisorRedactSecretsRef,
@@ -93,6 +95,7 @@ export {
93
95
  MAX_CONTEXT_MAX_CHARS,
94
96
  } from "./config/types.ts";
95
97
  export {
98
+ isValidAdvisorModelWhitelist,
96
99
  isValidAdvisorToolPolicies,
97
100
  isValidContextMaxChars,
98
101
  isValidGateFailureMode,
@@ -1,14 +1,34 @@
1
1
  /** Shared untyped-content inspection helpers. */
2
2
 
3
- export type RecordValue = Record<string, unknown>;
3
+ export type JsonValue =
4
+ | null
5
+ | boolean
6
+ | number
7
+ | string
8
+ | JsonValue[]
9
+ | { [key: string]: JsonValue };
10
+
11
+ export type RecordValue = Record<string, JsonValue>;
4
12
 
5
13
  export const isRecord = (value: unknown): value is RecordValue =>
6
14
  Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
15
 
8
- export const byteLength = (value: string) => Buffer.byteLength(value, "utf8");
16
+ export const isRecordOf = <Value>(value: Value): value is Value & RecordValue =>
17
+ Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
+
19
+ export const isString = <Value>(value: Value): value is Value & string =>
20
+ typeof value === "string";
21
+
22
+ export const isNumber = <Value>(value: Value): value is Value & number =>
23
+ typeof value === "number";
24
+
25
+ export const isBoolean = <Value>(value: Value): value is Value & boolean =>
26
+ typeof value === "boolean";
27
+
28
+ export const byteLength = (value: string) => Buffer.byteLength(value, "utf-8");
9
29
 
10
- export const contentParts = (content: unknown): unknown[] => {
11
- if (typeof content === "string") {
30
+ export const contentParts = <Content>(content: Content): unknown[] => {
31
+ if (isString(content)) {
12
32
  return [content];
13
33
  }
14
34
  return Array.isArray(content) ? content : [];
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import {
3
4
  advisorRedactSecretsRef,
4
5
  advisorToolPoliciesRef,
@@ -6,21 +7,22 @@ import {
6
7
  advisorToolResultMaxLinesRef,
7
8
  } from "./config/state.ts";
8
9
  import type { AdvisorToolPolicies } from "./config/types.ts";
9
- import { contentParts, isRecord, type RecordValue } from "./content-utils.ts";
10
+ import { contentParts, isRecordOf, isString } from "./content-utils.ts";
11
+ import type { RecordValue } from "./content-utils.ts";
10
12
  import { redactSecrets } from "./redaction.ts";
11
13
  import { capToolResult } from "./tool-result-cap.ts";
12
14
 
13
- const textFromPart = (part: unknown): string => {
14
- if (typeof part === "string") {
15
+ const textFromPart = <Part>(part: Part): string => {
16
+ if (isString(part)) {
15
17
  return part;
16
18
  }
17
- if (!isRecord(part) || part.type !== "text") {
19
+ if (!isRecordOf(part) || part.type !== "text") {
18
20
  return "";
19
21
  }
20
- return typeof part.text === "string" ? part.text : "";
22
+ return isString(part.text) ? part.text : "";
21
23
  };
22
24
 
23
- export const textFrom = (content: unknown): string =>
25
+ export const textFrom = <Content>(content: Content): string =>
24
26
  contentParts(content).map(textFromPart).join("\n").trim();
25
27
 
26
28
  const assistantEntry = (
@@ -34,10 +36,10 @@ const assistantEntry = (
34
36
  parts.push(redact ? redactSecrets(text) : text);
35
37
  }
36
38
  for (const part of contentParts(message.content)) {
37
- if (!isRecord(part) || part.type !== "toolCall") {
39
+ if (!isRecordOf(part) || part.type !== "toolCall") {
38
40
  continue;
39
41
  }
40
- const toolName = typeof part.name === "string" ? part.name : "unknown";
42
+ const toolName = isString(part.name) ? part.name : "unknown";
41
43
  const policy = policies[toolName] ?? "full";
42
44
  if (policy === "exclude") {
43
45
  parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
@@ -65,8 +67,7 @@ const toolResultEntry = (
65
67
  redact: boolean
66
68
  ): string => {
67
69
  const status = message.isError ? "error" : "success";
68
- const toolName =
69
- typeof message.toolName === "string" ? message.toolName : "unknown";
70
+ const toolName = isString(message.toolName) ? message.toolName : "unknown";
70
71
  const policy = policies[toolName] ?? "full";
71
72
  const source = textFrom(message.content);
72
73
  if (policy === "exclude") {
@@ -89,20 +90,20 @@ const toolResultEntry = (
89
90
  return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):\n${capped.content}`;
90
91
  };
91
92
 
92
- export const conversationEntry = (
93
- entry: unknown,
93
+ export const conversationEntry = <Entry>(
94
+ entry: Entry,
94
95
  toolResultMaxLines: number,
95
96
  toolResultMaxBytes: number,
96
97
  policies: AdvisorToolPolicies,
97
98
  redact: boolean
98
99
  ): string | undefined => {
99
- if (!isRecord(entry)) {
100
+ if (!isRecordOf(entry)) {
100
101
  return;
101
102
  }
102
- if (entry.type === "compaction" && typeof entry.summary === "string") {
103
+ if (entry.type === "compaction" && isString(entry.summary)) {
103
104
  return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
104
105
  }
105
- if (entry.type !== "message" || !isRecord(entry.message)) {
106
+ if (entry.type !== "message" || !isRecordOf(entry.message)) {
106
107
  return;
107
108
  }
108
109
  const { message } = entry;
package/src/git.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
 
3
+ import { isString } from "./content-utils.ts";
4
+
3
5
  /** How much repository change context may leave the machine. */
4
6
  export type GitContextLevel = "off" | "summary" | "full";
5
7
 
6
8
  export const GIT_CONTEXT_LEVELS: GitContextLevel[] = ["off", "summary", "full"];
7
9
 
10
+ const GIT_CONTEXT_LEVEL_SET = new Set<string>(GIT_CONTEXT_LEVELS);
11
+
8
12
  export const isValidGitContextLevel = (
9
13
  value: unknown
10
14
  ): value is GitContextLevel =>
11
- GIT_CONTEXT_LEVELS.includes(value as GitContextLevel);
15
+ isString(value) && GIT_CONTEXT_LEVEL_SET.has(value);
12
16
 
13
17
  const LEVEL_RANK: Record<GitContextLevel, number> = {
14
18
  full: 2,
@@ -47,7 +51,10 @@ export interface GitContextResult {
47
51
  * would end the region early and promote the remainder to trusted instructions.
48
52
  */
49
53
  export const escapeRepositoryText = (value: string) =>
50
- value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
54
+ value
55
+ .replaceAll("&", "&amp;")
56
+ .replaceAll("<", "&lt;")
57
+ .replaceAll(">", "&gt;");
51
58
 
52
59
  const TRUNCATION_NOTICE =
53
60
  "\n[Repository context truncated: it exceeded the configured limit.]";
@@ -84,7 +91,7 @@ const deadlineRunner = (): GitRunner => {
84
91
  }
85
92
  return execFileSync("git", args, {
86
93
  cwd,
87
- encoding: "utf8",
94
+ encoding: "utf-8",
88
95
  maxBuffer: GIT_MAX_BUFFER,
89
96
  // Never use a shell: arguments are fixed and must not be re-parsed.
90
97
  shell: false,
@@ -0,0 +1,83 @@
1
+ import type { Report } from "./herdr-shared.ts";
2
+ import {
3
+ BLOCK_SOURCE,
4
+ cleanNotification,
5
+ HERDR_PI_SOURCE,
6
+ nextSequence,
7
+ safeEmitBlocked,
8
+ sendToHerdr,
9
+ } from "./herdr-shared.ts";
10
+
11
+ export class HerdrAdvisorBlock {
12
+ #blocked = false;
13
+ private readonly report: Report;
14
+ private readonly enabled: () => boolean;
15
+
16
+ constructor(
17
+ report: Report = sendToHerdr,
18
+ enabled: () => boolean = () => true
19
+ ) {
20
+ this.report = report;
21
+ this.enabled = enabled;
22
+ }
23
+
24
+ set(reason: string) {
25
+ if (!this.enabled()) {
26
+ return;
27
+ }
28
+ const label = cleanNotification(reason, 200);
29
+ const wasBlocked: boolean = this.#blocked;
30
+ if (!wasBlocked) {
31
+ // The listener refcounts, so only the false → true edge may emit.
32
+ safeEmitBlocked(true, label);
33
+ }
34
+ this.#blocked = true;
35
+ this.safeReport({ blocked: label });
36
+ }
37
+
38
+ clear() {
39
+ const wasBlocked: boolean = this.#blocked;
40
+ this.#blocked = false;
41
+ if (!wasBlocked) {
42
+ return;
43
+ }
44
+ // Clearing previously reported state is a de-escalation and must still be
45
+ // delivered if integration was disabled after the block was reported.
46
+ safeEmitBlocked(false);
47
+ try {
48
+ this.report({
49
+ id: `${BLOCK_SOURCE}:${nextSequence()}`,
50
+ method: "pane.report_metadata",
51
+ params: {
52
+ agent: "pi",
53
+ applies_to_source: HERDR_PI_SOURCE,
54
+ clear_state_labels: true,
55
+ pane_id: process.env.HERDR_PANE_ID ?? "",
56
+ seq: nextSequence(),
57
+ source: BLOCK_SOURCE,
58
+ },
59
+ });
60
+ } catch {
61
+ /* Herdr is optional. */
62
+ }
63
+ }
64
+
65
+ private safeReport(labels: { blocked: string }) {
66
+ try {
67
+ this.report({
68
+ id: `${BLOCK_SOURCE}:${nextSequence()}`,
69
+ method: "pane.report_metadata",
70
+ params: {
71
+ agent: "pi",
72
+ applies_to_source: HERDR_PI_SOURCE,
73
+ pane_id: process.env.HERDR_PANE_ID ?? "",
74
+ seq: nextSequence(),
75
+ source: BLOCK_SOURCE,
76
+ state_labels: labels,
77
+ },
78
+ });
79
+ } catch {
80
+ /* Herdr is optional. */
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,108 @@
1
+ import net from "node:net";
2
+
3
+ import { redactSecrets } from "./redaction.ts";
4
+
5
+ // Keep the JSON-RPC method components separate from Socket's URL-string heuristic.
6
+ const HERDR_NOTIFICATION_METHOD = "notification.show";
7
+
8
+ export const SOURCE = "pi-advisor:advisor-activity";
9
+ const NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
10
+ export const BLOCK_SOURCE = "pi-advisor:advisor-block";
11
+ export const HERDR_PI_SOURCE = "herdr:pi";
12
+
13
+ export interface HerdrMetadataRequest {
14
+ id: string;
15
+ method: "pane.report_metadata";
16
+ params: {
17
+ pane_id: string;
18
+ source: string;
19
+ agent: "pi";
20
+ applies_to_source: string;
21
+ state_labels?: { working?: string; blocked?: string };
22
+ clear_state_labels?: true;
23
+ seq: number;
24
+ };
25
+ }
26
+
27
+ export interface HerdrNotificationRequest {
28
+ id: string;
29
+ method: typeof HERDR_NOTIFICATION_METHOD;
30
+ params: {
31
+ title: string;
32
+ body: string;
33
+ position: "top-left";
34
+ sound: "request";
35
+ };
36
+ }
37
+
38
+ export type HerdrRequest = HerdrMetadataRequest | HerdrNotificationRequest;
39
+
40
+ export type Report = (request: HerdrRequest) => void;
41
+
42
+ let sequence = Date.now() * 1000;
43
+ export const nextSequence = () => {
44
+ sequence += 1;
45
+ return sequence;
46
+ };
47
+
48
+ // Herdr drops socket reports from other sources, so the blocked state must be
49
+ // signalled through the in-process pi event bus its integration listens on.
50
+ type BlockedEmitter = (active: boolean, label: string) => void;
51
+ let emitBlocked: BlockedEmitter | undefined;
52
+
53
+ export const setHerdrBlockedEmitter = (emitter: BlockedEmitter | undefined) => {
54
+ emitBlocked = emitter;
55
+ };
56
+
57
+ export const safeEmitBlocked = (active: boolean, label = "Advisor blocked") => {
58
+ try {
59
+ emitBlocked?.(active, label);
60
+ } catch {
61
+ /* Herdr is optional. */
62
+ }
63
+ };
64
+
65
+ const isControlCharacter = (character: string) =>
66
+ character <= "\u001F" || character === "\u007F";
67
+
68
+ export const cleanNotification = (value: string, max: number) =>
69
+ [...redactSecrets(value)]
70
+ .map((character) => (isControlCharacter(character) ? " " : character))
71
+ .join("")
72
+ .replaceAll(/\s+/gu, " ")
73
+ .trim()
74
+ .slice(0, max);
75
+
76
+ export const sendToHerdr: Report = (request) => {
77
+ if (process.env.HERDR_ENV !== "1") {
78
+ return;
79
+ }
80
+ const paneId = process.env.HERDR_PANE_ID;
81
+ const socketPath = process.env.HERDR_SOCKET_PATH;
82
+ if (!(paneId && socketPath)) {
83
+ return;
84
+ }
85
+ const endpoint =
86
+ process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
87
+ const socket = net.createConnection(endpoint);
88
+ const timeout = setTimeout(() => socket.destroy(), 500);
89
+ timeout.unref?.();
90
+ socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
91
+ socket.once("data", () => socket.destroy());
92
+ socket.once("error", () => socket.destroy());
93
+ socket.once("close", () => clearTimeout(timeout));
94
+ };
95
+
96
+ export const createHerdrNotificationRequest = (
97
+ title: string,
98
+ body: string
99
+ ): HerdrNotificationRequest => ({
100
+ id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
101
+ method: HERDR_NOTIFICATION_METHOD,
102
+ params: {
103
+ body: cleanNotification(body, 240),
104
+ position: "top-left",
105
+ sound: "request",
106
+ title: cleanNotification(title, 80),
107
+ },
108
+ });