pi-gang 0.1.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/LICENSE +21 -0
- package/NOTICE +15 -0
- package/README.md +159 -0
- package/assets/gang-logo.png +0 -0
- package/package.json +64 -0
- package/src/gang/events.ts +11 -0
- package/src/gang/feed-client.ts +88 -0
- package/src/gang/index.ts +618 -0
- package/src/gang/members.ts +124 -0
- package/src/gang/tmux.ts +151 -0
- package/src/gang/ui/mission-control.ts +234 -0
- package/src/intercom/broker/broker.ts +388 -0
- package/src/intercom/broker/client.ts +535 -0
- package/src/intercom/broker/framing.ts +73 -0
- package/src/intercom/broker/paths.ts +20 -0
- package/src/intercom/broker/spawn.ts +309 -0
- package/src/intercom/broker/tap.ts +31 -0
- package/src/intercom/config.ts +108 -0
- package/src/intercom/gui/dashboard.html +272 -0
- package/src/intercom/gui/server.ts +108 -0
- package/src/intercom/index.ts +1807 -0
- package/src/intercom/reply-tracker.ts +105 -0
- package/src/intercom/skills/pi-intercom/SKILL.md +513 -0
- package/src/intercom/types.ts +46 -0
- package/src/intercom/ui/compose.ts +139 -0
- package/src/intercom/ui/inline-message.ts +78 -0
- package/src/intercom/ui/session-list.ts +162 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import net from "net";
|
|
7
|
+
import { getBrokerSocketPath } from "./paths.js";
|
|
8
|
+
|
|
9
|
+
const INTERCOM_DIR = join(homedir(), ".pi/agent/intercom");
|
|
10
|
+
const EXTENSION_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const BROKER_SOCKET = getBrokerSocketPath();
|
|
12
|
+
const BROKER_PID = join(INTERCOM_DIR, "broker.pid");
|
|
13
|
+
const BROKER_SPAWN_LOCK = join(INTERCOM_DIR, "broker.spawn.lock");
|
|
14
|
+
|
|
15
|
+
type BrokerLaunchSpec =
|
|
16
|
+
| {
|
|
17
|
+
kind: "direct";
|
|
18
|
+
command: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
kind: "windows-launcher";
|
|
23
|
+
command: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
launcherPath: string;
|
|
26
|
+
launcherCommandLine: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function sleep(ms: number): Promise<void> {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getTsxCliPath(extensionDir: string = EXTENSION_DIR): string {
|
|
34
|
+
return join(extensionDir, "node_modules", "tsx", "dist", "cli.mjs");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function quoteWindowsArg(value: string): string {
|
|
38
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getWindowsHiddenLauncherPath(intercomDir: string = INTERCOM_DIR): string {
|
|
42
|
+
return join(intercomDir, "broker-launch.vbs");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function usesDefaultBrokerCommand(brokerCommand: string, brokerArgs: string[]): boolean {
|
|
46
|
+
return brokerCommand === "npx"
|
|
47
|
+
&& brokerArgs.length === 2
|
|
48
|
+
&& brokerArgs[0] === "--no-install"
|
|
49
|
+
&& brokerArgs[1] === "tsx";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getWindowsBrokerCommandLine(
|
|
53
|
+
brokerPath: string,
|
|
54
|
+
extensionDir: string = EXTENSION_DIR,
|
|
55
|
+
nodePath: string = process.execPath,
|
|
56
|
+
brokerCommand = "npx",
|
|
57
|
+
brokerArgs: string[] = ["--no-install", "tsx"],
|
|
58
|
+
): string {
|
|
59
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
60
|
+
return [quoteWindowsArg(nodePath), quoteWindowsArg(getTsxCliPath(extensionDir)), quoteWindowsArg(brokerPath)].join(" ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return [quoteWindowsArg(brokerCommand), ...brokerArgs.map(quoteWindowsArg), quoteWindowsArg(brokerPath)].join(" ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getWindowsHiddenLauncherScript(commandLine: string): string {
|
|
67
|
+
return [
|
|
68
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
69
|
+
`WshShell.Run "${commandLine.replace(/"/g, '""')}", 0, False`,
|
|
70
|
+
'Set WshShell = Nothing',
|
|
71
|
+
'',
|
|
72
|
+
].join("\r\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeWindowsHiddenLauncher(
|
|
76
|
+
commandLine: string,
|
|
77
|
+
launcherPath: string = getWindowsHiddenLauncherPath(),
|
|
78
|
+
): string {
|
|
79
|
+
mkdirSync(dirname(launcherPath), { recursive: true, mode: 0o700 });
|
|
80
|
+
writeFileSync(launcherPath, getWindowsHiddenLauncherScript(commandLine), { encoding: "utf-8", mode: 0o600 });
|
|
81
|
+
return launcherPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getBrokerLaunchSpec(
|
|
85
|
+
brokerPath: string,
|
|
86
|
+
brokerCommand: string,
|
|
87
|
+
brokerArgs: string[],
|
|
88
|
+
extensionDir: string = EXTENSION_DIR,
|
|
89
|
+
platform: NodeJS.Platform = process.platform,
|
|
90
|
+
intercomDir: string = INTERCOM_DIR,
|
|
91
|
+
nodePath: string = process.execPath,
|
|
92
|
+
): BrokerLaunchSpec {
|
|
93
|
+
if (platform === "win32") {
|
|
94
|
+
const launcherPath = getWindowsHiddenLauncherPath(intercomDir);
|
|
95
|
+
return {
|
|
96
|
+
kind: "windows-launcher",
|
|
97
|
+
command: "wscript.exe",
|
|
98
|
+
args: [launcherPath],
|
|
99
|
+
launcherPath,
|
|
100
|
+
launcherCommandLine: getWindowsBrokerCommandLine(brokerPath, extensionDir, nodePath, brokerCommand, brokerArgs),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
kind: "direct",
|
|
106
|
+
command: brokerCommand,
|
|
107
|
+
args: [...brokerArgs, brokerPath],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getBrokerSpawnOptions(extensionDir: string = EXTENSION_DIR): {
|
|
112
|
+
detached: true;
|
|
113
|
+
stdio: "ignore";
|
|
114
|
+
cwd: string;
|
|
115
|
+
env: NodeJS.ProcessEnv;
|
|
116
|
+
windowsHide: true;
|
|
117
|
+
} {
|
|
118
|
+
return {
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: "ignore",
|
|
121
|
+
cwd: extensionDir,
|
|
122
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toError(error: unknown): Error {
|
|
128
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function spawnBrokerIfNeeded(brokerCommand: string, brokerArgs: string[]): Promise<void> {
|
|
132
|
+
mkdirSync(INTERCOM_DIR, { recursive: true, mode: 0o700 });
|
|
133
|
+
if (process.platform !== "win32") chmodSync(INTERCOM_DIR, 0o700);
|
|
134
|
+
|
|
135
|
+
if (await isBrokerRunning()) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ownsLock = acquireSpawnLock();
|
|
140
|
+
if (!ownsLock) {
|
|
141
|
+
await waitForBroker();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
if (await isBrokerRunning()) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const brokerPath = join(dirname(fileURLToPath(import.meta.url)), "broker.ts");
|
|
151
|
+
const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
|
|
152
|
+
if (launch.kind === "windows-launcher") {
|
|
153
|
+
writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
|
|
154
|
+
}
|
|
155
|
+
const child = spawn(launch.command, launch.args, getBrokerSpawnOptions());
|
|
156
|
+
child.unref();
|
|
157
|
+
|
|
158
|
+
await new Promise<void>((resolve, reject) => {
|
|
159
|
+
const cleanup = () => {
|
|
160
|
+
child.off("error", onError);
|
|
161
|
+
child.off("exit", onExit);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const onError = (error: Error) => {
|
|
165
|
+
cleanup();
|
|
166
|
+
reject(new Error(`Failed to spawn intercom broker: ${error.message}`, { cause: error }));
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
170
|
+
if (launch.kind === "windows-launcher" && code === 0 && signal === null) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
cleanup();
|
|
174
|
+
if (signal) {
|
|
175
|
+
reject(new Error(`Intercom broker exited before startup with signal ${signal}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}`));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
child.once("error", onError);
|
|
182
|
+
child.once("exit", onExit);
|
|
183
|
+
waitForBroker().then(() => {
|
|
184
|
+
cleanup();
|
|
185
|
+
resolve();
|
|
186
|
+
}, (error) => {
|
|
187
|
+
cleanup();
|
|
188
|
+
reject(toError(error));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
releaseSpawnLock();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function isBrokerRunning(): Promise<boolean> {
|
|
197
|
+
if (await checkSocketConnectable()) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!existsSync(BROKER_PID)) return false;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const pid = parseInt(readFileSync(BROKER_PID, "utf-8").trim(), 10);
|
|
205
|
+
if (!Number.isFinite(pid)) return false;
|
|
206
|
+
process.kill(pid, 0);
|
|
207
|
+
return checkSocketConnectable();
|
|
208
|
+
} catch {
|
|
209
|
+
// Missing or unreadable PID state means there is no live broker to reuse.
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function checkSocketConnectable(): Promise<boolean> {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const socket = net.connect(BROKER_SOCKET);
|
|
217
|
+
const finish = (isConnected: boolean) => {
|
|
218
|
+
clearTimeout(timeout);
|
|
219
|
+
socket.off("connect", onConnect);
|
|
220
|
+
socket.off("error", onError);
|
|
221
|
+
resolve(isConnected);
|
|
222
|
+
};
|
|
223
|
+
const onConnect = () => {
|
|
224
|
+
socket.end();
|
|
225
|
+
finish(true);
|
|
226
|
+
};
|
|
227
|
+
const onError = () => {
|
|
228
|
+
socket.destroy();
|
|
229
|
+
finish(false);
|
|
230
|
+
};
|
|
231
|
+
socket.on("connect", onConnect);
|
|
232
|
+
socket.on("error", onError);
|
|
233
|
+
const timeout = setTimeout(() => {
|
|
234
|
+
socket.destroy();
|
|
235
|
+
finish(false);
|
|
236
|
+
}, 1000);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function acquireSpawnLock(): boolean {
|
|
241
|
+
const maxRetries = 5;
|
|
242
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
243
|
+
try {
|
|
244
|
+
writeFileSync(BROKER_SPAWN_LOCK, `${process.pid}\n${Date.now()}\n`, { flag: "wx", mode: 0o600 });
|
|
245
|
+
if (process.platform !== "win32") chmodSync(BROKER_SPAWN_LOCK, 0o600);
|
|
246
|
+
return true;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (isSpawnLockStale()) {
|
|
252
|
+
try {
|
|
253
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
254
|
+
} catch {
|
|
255
|
+
// If we can't delete the stale lock, retry a few times before giving up
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isSpawnLockStale(): boolean {
|
|
266
|
+
if (!existsSync(BROKER_SPAWN_LOCK)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const [pidLine = "", createdAtLine = "0"] = readFileSync(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
|
|
272
|
+
const pid = Number.parseInt(pidLine, 10);
|
|
273
|
+
const createdAt = Number.parseInt(createdAtLine, 10);
|
|
274
|
+
const ageMs = Date.now() - createdAt;
|
|
275
|
+
|
|
276
|
+
if (Number.isFinite(pid)) {
|
|
277
|
+
try {
|
|
278
|
+
process.kill(pid, 0);
|
|
279
|
+
} catch {
|
|
280
|
+
// The process that created the lock is gone.
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return !Number.isFinite(createdAt) || ageMs > 10_000;
|
|
286
|
+
} catch {
|
|
287
|
+
// Unreadable lock contents are treated as stale so a new broker can start.
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function releaseSpawnLock(): void {
|
|
293
|
+
try {
|
|
294
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
295
|
+
} catch {
|
|
296
|
+
// Another cleanup path may already have removed the lock.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function waitForBroker(timeoutMs = 5000): Promise<void> {
|
|
301
|
+
const start = Date.now();
|
|
302
|
+
while (Date.now() - start < timeoutMs) {
|
|
303
|
+
if (await checkSocketConnectable()) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await sleep(100);
|
|
307
|
+
}
|
|
308
|
+
throw new Error("Broker failed to start within timeout");
|
|
309
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { appendFileSync, chmodSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
/** One durable record per routed cross-agent message. */
|
|
6
|
+
export interface RoutedLogEntry {
|
|
7
|
+
ts: number;
|
|
8
|
+
id: string;
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
text: string;
|
|
12
|
+
replyTo?: string;
|
|
13
|
+
expectsReply?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getIntercomLogPath(homeDir: string = homedir()): string {
|
|
17
|
+
return join(homeDir, ".pi/agent/intercom/intercom.jsonl");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Append one greppable, replayable JSONL line. Best-effort: never break routing. */
|
|
21
|
+
export function appendIntercomLog(entry: RoutedLogEntry, logPath: string = getIntercomLogPath()): void {
|
|
22
|
+
try {
|
|
23
|
+
const dir = dirname(logPath);
|
|
24
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
if (process.platform !== "win32") chmodSync(dir, 0o700);
|
|
26
|
+
appendFileSync(logPath, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
27
|
+
if (process.platform !== "win32") chmodSync(logPath, 0o600);
|
|
28
|
+
} catch {
|
|
29
|
+
// ponytail: observability is best-effort; a failed log write must not drop a message.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
export interface IntercomConfig {
|
|
6
|
+
/** Broker command used to spawn the broker process (e.g. "npx" or "bun") */
|
|
7
|
+
brokerCommand: string;
|
|
8
|
+
|
|
9
|
+
/** Arguments passed to the broker command before the broker script path */
|
|
10
|
+
brokerArgs: string[];
|
|
11
|
+
|
|
12
|
+
/** Require confirmation before non-reply sends from interactive sessions */
|
|
13
|
+
confirmSend: boolean;
|
|
14
|
+
|
|
15
|
+
/** Optional custom status suffix shown after automatic lifecycle status */
|
|
16
|
+
status?: string;
|
|
17
|
+
|
|
18
|
+
/** Enable/disable intercom (default: true) */
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
|
|
21
|
+
/** Show reply hint in incoming messages (default: true) */
|
|
22
|
+
replyHint: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CONFIG_PATH = join(homedir(), ".pi/agent/intercom/config.json");
|
|
26
|
+
|
|
27
|
+
const defaults: IntercomConfig = {
|
|
28
|
+
brokerCommand: "npx",
|
|
29
|
+
brokerArgs: ["--no-install", "tsx"],
|
|
30
|
+
confirmSend: false,
|
|
31
|
+
enabled: true,
|
|
32
|
+
replyHint: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function loadConfig(): IntercomConfig {
|
|
36
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
37
|
+
return { ...defaults };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
42
|
+
const parsed: unknown = JSON.parse(raw);
|
|
43
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
44
|
+
throw new Error("Config must be a JSON object");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parsedConfig = parsed as Record<string, unknown>;
|
|
48
|
+
const config: IntercomConfig = { ...defaults };
|
|
49
|
+
|
|
50
|
+
if (Object.hasOwn(parsedConfig, "brokerCommand")) {
|
|
51
|
+
if (typeof parsedConfig.brokerCommand !== "string") {
|
|
52
|
+
throw new Error(`"brokerCommand" must be a string`);
|
|
53
|
+
}
|
|
54
|
+
const brokerCommand = parsedConfig.brokerCommand.trim();
|
|
55
|
+
if (!brokerCommand) {
|
|
56
|
+
throw new Error(`"brokerCommand" must not be empty`);
|
|
57
|
+
}
|
|
58
|
+
config.brokerCommand = brokerCommand;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Object.hasOwn(parsedConfig, "brokerArgs")) {
|
|
62
|
+
if (!Array.isArray(parsedConfig.brokerArgs)) {
|
|
63
|
+
throw new Error(`"brokerArgs" must be an array`);
|
|
64
|
+
}
|
|
65
|
+
const brokerArgs: string[] = [];
|
|
66
|
+
for (const arg of parsedConfig.brokerArgs) {
|
|
67
|
+
if (typeof arg !== "string") {
|
|
68
|
+
throw new Error(`"brokerArgs" items must be strings`);
|
|
69
|
+
}
|
|
70
|
+
brokerArgs.push(arg);
|
|
71
|
+
}
|
|
72
|
+
config.brokerArgs = brokerArgs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Object.hasOwn(parsedConfig, "confirmSend")) {
|
|
76
|
+
if (typeof parsedConfig.confirmSend !== "boolean") {
|
|
77
|
+
throw new Error(`"confirmSend" must be a boolean`);
|
|
78
|
+
}
|
|
79
|
+
config.confirmSend = parsedConfig.confirmSend;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.hasOwn(parsedConfig, "enabled")) {
|
|
83
|
+
if (typeof parsedConfig.enabled !== "boolean") {
|
|
84
|
+
throw new Error(`"enabled" must be a boolean`);
|
|
85
|
+
}
|
|
86
|
+
config.enabled = parsedConfig.enabled;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Object.hasOwn(parsedConfig, "replyHint")) {
|
|
90
|
+
if (typeof parsedConfig.replyHint !== "boolean") {
|
|
91
|
+
throw new Error(`"replyHint" must be a boolean`);
|
|
92
|
+
}
|
|
93
|
+
config.replyHint = parsedConfig.replyHint;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Object.hasOwn(parsedConfig, "status")) {
|
|
97
|
+
if (typeof parsedConfig.status !== "string") {
|
|
98
|
+
throw new Error(`"status" must be a string`);
|
|
99
|
+
}
|
|
100
|
+
config.status = parsedConfig.status;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return config;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(`Failed to load intercom config at ${CONFIG_PATH}:`, error);
|
|
106
|
+
return { ...defaults };
|
|
107
|
+
}
|
|
108
|
+
}
|