jeopi-cli 16.5.6 → 16.6.1
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/CHANGELOG.md +10 -0
- package/README.md +80 -0
- package/dist/cli.js +3866 -4031
- package/dist/types/cli/messenger-cli.d.ts +10 -0
- package/dist/types/commands/messenger.d.ts +18 -0
- package/dist/types/messenger/config.d.ts +4 -0
- package/dist/types/messenger/discord.d.ts +10 -0
- package/dist/types/messenger/gateway.d.ts +9 -0
- package/dist/types/messenger/http.d.ts +9 -0
- package/dist/types/messenger/session.d.ts +17 -0
- package/dist/types/messenger/slack.d.ts +2 -0
- package/dist/types/messenger/telegram.d.ts +2 -0
- package/dist/types/messenger/types.d.ts +38 -0
- package/package.json +12 -12
- package/src/cli/messenger-cli.ts +78 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/messenger.ts +30 -0
- package/src/config/model-registry.ts +33 -6
- package/src/messenger/config.ts +111 -0
- package/src/messenger/discord.ts +382 -0
- package/src/messenger/gateway.ts +153 -0
- package/src/messenger/http.ts +87 -0
- package/src/messenger/session.ts +219 -0
- package/src/messenger/slack.ts +333 -0
- package/src/messenger/telegram.ts +244 -0
- package/src/messenger/types.ts +47 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const MESSENGER_ACTIONS: readonly ["check", "run"];
|
|
2
|
+
export type MessengerAction = (typeof MESSENGER_ACTIONS)[number];
|
|
3
|
+
export interface MessengerCommandArgs {
|
|
4
|
+
action: MessengerAction;
|
|
5
|
+
flags: {
|
|
6
|
+
config: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** Check is deliberately local-only: it never imports the SDK or starts a transport. */
|
|
10
|
+
export declare function runMessengerCommand(cmd: MessengerCommandArgs): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "jeopi-utils/cli";
|
|
2
|
+
export default class Messenger extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static args: {
|
|
5
|
+
action: import("jeopi-utils/cli").ArgDescriptor & {
|
|
6
|
+
description: string;
|
|
7
|
+
required: false;
|
|
8
|
+
options: ("check" | "run")[];
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
static flags: {
|
|
12
|
+
config: import("jeopi-utils/cli").FlagDescriptor<"string"> & {
|
|
13
|
+
description: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
static examples: string[];
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MessengerConfig } from "./types";
|
|
2
|
+
export declare function parseMessengerConfig(value: unknown, baseDir: string): MessengerConfig;
|
|
3
|
+
export declare function loadMessengerConfig(file: string): Promise<MessengerConfig>;
|
|
4
|
+
export declare function validateMessengerEnvironment(config: MessengerConfig, env?: Record<string, string | undefined>): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MessengerAdapter, MessengerPlatformConfig, MessengerTransportDeps } from "./types";
|
|
2
|
+
export interface DiscordClock {
|
|
3
|
+
/** Schedule once and return an idempotent cancellation function. */
|
|
4
|
+
schedule(callback: () => void, delayMs: number): () => void;
|
|
5
|
+
random(): number;
|
|
6
|
+
}
|
|
7
|
+
export interface DiscordTransportDeps extends MessengerTransportDeps {
|
|
8
|
+
clock?: DiscordClock;
|
|
9
|
+
}
|
|
10
|
+
export declare function createDiscordAdapter(config: MessengerPlatformConfig, env?: Record<string, string | undefined>, deps?: DiscordTransportDeps): MessengerAdapter;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MessengerAdapter, MessengerConfig, MessengerMessage, MessengerReply } from "./types";
|
|
2
|
+
/** Include the sender even in shared threads: remote histories must not cross users. */
|
|
3
|
+
export declare function messengerConversationKey(message: MessengerMessage): string;
|
|
4
|
+
export declare function isMessengerMessageAllowed(message: MessengerMessage, config: MessengerConfig): boolean;
|
|
5
|
+
export interface MessengerGateway {
|
|
6
|
+
run(signal: AbortSignal): Promise<void>;
|
|
7
|
+
handle(message: MessengerMessage, signal?: AbortSignal): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function createMessengerGateway(config: MessengerConfig, adapters: MessengerAdapter[], reply: MessengerReply): MessengerGateway;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class MessengerHttpError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly retryAfterMs?: number | undefined;
|
|
4
|
+
constructor(status: number, retryAfterMs?: number | undefined);
|
|
5
|
+
}
|
|
6
|
+
/** Provider limits count UTF-16 units; do not bisect supplementary characters. */
|
|
7
|
+
export declare function splitMessengerText(text: string, limit: number): string[];
|
|
8
|
+
export declare function messengerFetchJson(url: string, init: RequestInit, signal?: AbortSignal, fetchImpl?: typeof fetch): Promise<unknown>;
|
|
9
|
+
export declare function waitForMessengerRetry(ms: number, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Model } from "jeopi-ai";
|
|
2
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
3
|
+
import type { AuthStorage } from "../session/auth-storage";
|
|
4
|
+
import { type MessengerConfig, type MessengerReply } from "./types";
|
|
5
|
+
export interface MessengerSessionBridge {
|
|
6
|
+
reply: MessengerReply;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/** Explicit SDK resources for embedded callers; security policy remains bridge-owned. */
|
|
10
|
+
export interface MessengerSessionDependencies {
|
|
11
|
+
agentDir: string;
|
|
12
|
+
authStorage: AuthStorage;
|
|
13
|
+
modelRegistry: ModelRegistry;
|
|
14
|
+
model: Model;
|
|
15
|
+
}
|
|
16
|
+
/** One Main session owns the SDK's process-global lifecycle until gateway shutdown. */
|
|
17
|
+
export declare function createMessengerSessionBridge(config: MessengerConfig, dependencies?: MessengerSessionDependencies): MessengerSessionBridge;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type MessengerPlatform = "discord" | "telegram" | "slack";
|
|
2
|
+
export interface MessengerMessage {
|
|
3
|
+
platform: MessengerPlatform;
|
|
4
|
+
accountId: string;
|
|
5
|
+
channelId: string;
|
|
6
|
+
senderId: string;
|
|
7
|
+
messageId: string;
|
|
8
|
+
text: string;
|
|
9
|
+
threadId?: string;
|
|
10
|
+
direct: boolean;
|
|
11
|
+
mentioned: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface MessengerAdapter {
|
|
14
|
+
platform: MessengerPlatform;
|
|
15
|
+
start(onMessage: (message: MessengerMessage) => Promise<void>, signal: AbortSignal): Promise<void>;
|
|
16
|
+
send(message: MessengerMessage, text: string, signal?: AbortSignal): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface MessengerPlatformConfig {
|
|
19
|
+
tokenEnv: string;
|
|
20
|
+
appTokenEnv?: string;
|
|
21
|
+
allowedUserIds: string[];
|
|
22
|
+
allowedChannelIds?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface MessengerConfig {
|
|
25
|
+
cwd: string;
|
|
26
|
+
sessionDir: string;
|
|
27
|
+
toolNames: string[];
|
|
28
|
+
platforms: Partial<Record<MessengerPlatform, MessengerPlatformConfig>>;
|
|
29
|
+
}
|
|
30
|
+
export interface MessengerTransportDeps {
|
|
31
|
+
fetch?: typeof fetch;
|
|
32
|
+
WebSocket?: typeof WebSocket;
|
|
33
|
+
}
|
|
34
|
+
export type MessengerReply = (message: MessengerMessage, signal: AbortSignal) => Promise<string>;
|
|
35
|
+
/** A partially initialized SDK must not be retried inside a live gateway. */
|
|
36
|
+
export declare class MessengerFatalError extends Error {
|
|
37
|
+
constructor();
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-cli",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.6.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/akillness/jeopi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -57,17 +57,17 @@
|
|
|
57
57
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
58
58
|
"@babel/parser": "^7.29.7",
|
|
59
59
|
"@mozilla/readability": "^0.6.0",
|
|
60
|
-
"jeopi-hashline": "16.
|
|
61
|
-
"jeopi-stats": "16.
|
|
62
|
-
"jeopi-agent-core": "16.
|
|
63
|
-
"jeopi-ai": "16.
|
|
64
|
-
"jeopi-catalog": "16.
|
|
65
|
-
"jeopi-mnemopi": "16.
|
|
66
|
-
"jeopi-natives": "16.
|
|
67
|
-
"jeopi-tui": "16.
|
|
68
|
-
"jeopi-utils": "16.
|
|
69
|
-
"jeopi-wire": "16.
|
|
70
|
-
"jeopi-snapcompact": "16.
|
|
60
|
+
"jeopi-hashline": "16.6.1",
|
|
61
|
+
"jeopi-stats": "16.6.1",
|
|
62
|
+
"jeopi-agent-core": "16.6.1",
|
|
63
|
+
"jeopi-ai": "16.6.1",
|
|
64
|
+
"jeopi-catalog": "16.6.1",
|
|
65
|
+
"jeopi-mnemopi": "16.6.1",
|
|
66
|
+
"jeopi-natives": "16.6.1",
|
|
67
|
+
"jeopi-tui": "16.6.1",
|
|
68
|
+
"jeopi-utils": "16.6.1",
|
|
69
|
+
"jeopi-wire": "16.6.1",
|
|
70
|
+
"jeopi-snapcompact": "16.6.1",
|
|
71
71
|
"@opentelemetry/api": "^1.9.1",
|
|
72
72
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
73
73
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { loadMessengerConfig, validateMessengerEnvironment } from "../messenger/config";
|
|
2
|
+
import type { MessengerSessionBridge } from "../messenger/session";
|
|
3
|
+
import { type MessengerAdapter, type MessengerConfig, MessengerFatalError } from "../messenger/types";
|
|
4
|
+
|
|
5
|
+
export const MESSENGER_ACTIONS = ["check", "run"] as const;
|
|
6
|
+
export type MessengerAction = (typeof MESSENGER_ACTIONS)[number];
|
|
7
|
+
export interface MessengerCommandArgs {
|
|
8
|
+
action: MessengerAction;
|
|
9
|
+
flags: { config: string };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Check is deliberately local-only: it never imports the SDK or starts a transport. */
|
|
13
|
+
export async function runMessengerCommand(cmd: MessengerCommandArgs): Promise<void> {
|
|
14
|
+
if (!MESSENGER_ACTIONS.includes(cmd.action)) throw new Error("Unknown messenger action");
|
|
15
|
+
if (!cmd.flags.config) throw new Error("Messenger requires --config <file>");
|
|
16
|
+
// Do not echo parse errors, paths, configuration values, or environment secrets.
|
|
17
|
+
let config: MessengerConfig;
|
|
18
|
+
try {
|
|
19
|
+
config = await loadMessengerConfig(cmd.flags.config);
|
|
20
|
+
validateMessengerEnvironment(config);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error("Messenger configuration or required environment is invalid");
|
|
23
|
+
}
|
|
24
|
+
const platforms = Object.keys(config.platforms);
|
|
25
|
+
if (cmd.action === "check") {
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
`Messenger configuration valid: ${platforms.join(", ")}; ${config.toolNames.length} tools enabled. No network checks performed.\n`,
|
|
28
|
+
);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (config.toolNames.length > 0) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
"Warning: enabled messenger tools grant authorized remote users local machine authority; this is not a sandbox.\n",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const stop = (): void => controller.abort();
|
|
39
|
+
process.once("SIGINT", stop);
|
|
40
|
+
process.once("SIGTERM", stop);
|
|
41
|
+
let bridge: MessengerSessionBridge | undefined;
|
|
42
|
+
let failure: Error | undefined;
|
|
43
|
+
try {
|
|
44
|
+
const [{ createMessengerSessionBridge }, { createMessengerGateway }, telegram, discord, slack] =
|
|
45
|
+
await Promise.all([
|
|
46
|
+
import("../messenger/session"),
|
|
47
|
+
import("../messenger/gateway"),
|
|
48
|
+
import("../messenger/telegram"),
|
|
49
|
+
import("../messenger/discord"),
|
|
50
|
+
import("../messenger/slack"),
|
|
51
|
+
]);
|
|
52
|
+
if (controller.signal.aborted) return;
|
|
53
|
+
bridge = createMessengerSessionBridge(config);
|
|
54
|
+
const adapters: MessengerAdapter[] = [];
|
|
55
|
+
if (config.platforms.telegram) adapters.push(telegram.createTelegramAdapter(config.platforms.telegram));
|
|
56
|
+
if (config.platforms.discord) adapters.push(discord.createDiscordAdapter(config.platforms.discord));
|
|
57
|
+
if (config.platforms.slack) adapters.push(slack.createSlackAdapter(config.platforms.slack));
|
|
58
|
+
const gateway = createMessengerGateway(config, adapters, bridge.reply);
|
|
59
|
+
process.stdout.write(`Starting messenger: ${platforms.join(", ")}. Press Ctrl+C to stop.\n`);
|
|
60
|
+
await gateway.run(controller.signal);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
failure =
|
|
63
|
+
error instanceof MessengerFatalError
|
|
64
|
+
? error
|
|
65
|
+
: new Error("Messenger gateway failed; check local configuration and provider access");
|
|
66
|
+
} finally {
|
|
67
|
+
controller.abort();
|
|
68
|
+
try {
|
|
69
|
+
await bridge?.close();
|
|
70
|
+
} catch {
|
|
71
|
+
failure ??= new Error("Messenger session shutdown failed");
|
|
72
|
+
} finally {
|
|
73
|
+
process.off("SIGINT", stop);
|
|
74
|
+
process.off("SIGTERM", stop);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (failure) throw failure;
|
|
78
|
+
}
|
package/src/cli-commands.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const commands: CommandEntry[] = [
|
|
|
16
16
|
{ name: "acp", load: () => import("./commands/acp").then(m => m.default) },
|
|
17
17
|
{ name: "auth-broker", load: () => import("./commands/auth-broker").then(m => m.default) },
|
|
18
18
|
{ name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) },
|
|
19
|
+
{ name: "messenger", load: () => import("./commands/messenger").then(m => m.default) },
|
|
19
20
|
{ name: "agents", load: () => import("./commands/agents").then(m => m.default) },
|
|
20
21
|
{ name: "bench", load: () => import("./commands/bench").then(m => m.default) },
|
|
21
22
|
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { APP_NAME } from "jeopi-utils";
|
|
2
|
+
import { Args, Command, Flags, renderCommandHelp } from "jeopi-utils/cli";
|
|
3
|
+
import { MESSENGER_ACTIONS, type MessengerAction, runMessengerCommand } from "../cli/messenger-cli";
|
|
4
|
+
|
|
5
|
+
export default class Messenger extends Command {
|
|
6
|
+
static description = "Run authorized Discord, Telegram, and Slack text bots";
|
|
7
|
+
|
|
8
|
+
static args = {
|
|
9
|
+
action: Args.string({ description: "Sub-command", required: false, options: [...MESSENGER_ACTIONS] }),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
static flags = {
|
|
13
|
+
config: Flags.string({ description: "Path to messenger JSON configuration" }),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
static examples = [
|
|
17
|
+
"jeopi messenger check --config ./messenger.json",
|
|
18
|
+
"jeopi messenger run --config ./messenger.json",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async run(): Promise<void> {
|
|
22
|
+
const { args, flags } = await this.parse(Messenger);
|
|
23
|
+
if (!args.action) {
|
|
24
|
+
renderCommandHelp(APP_NAME, "messenger", Messenger);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!flags.config) throw new Error("Messenger requires --config <file>");
|
|
28
|
+
await runMessengerCommand({ action: args.action as MessengerAction, flags: { config: flags.config } });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1643,13 +1643,40 @@ export class ModelRegistry {
|
|
|
1643
1643
|
}> = [
|
|
1644
1644
|
{
|
|
1645
1645
|
providerId: "google-antigravity",
|
|
1646
|
-
resolveKey:
|
|
1647
|
-
|
|
1648
|
-
|
|
1646
|
+
resolveKey: value =>
|
|
1647
|
+
extractGoogleOAuthToken(value) ?? this.authStorage.getOAuthCredential("google-antigravity")?.access,
|
|
1648
|
+
createOptions: oauthToken => {
|
|
1649
|
+
// Pin discovery to one stable account, including when its access token has expired.
|
|
1650
|
+
// Independent peek/getApiKey calls otherwise advance round-robin and poison the cache scope.
|
|
1651
|
+
const account = this.authStorage.listOAuthAccounts("google-antigravity")[0];
|
|
1652
|
+
const cacheScope = account
|
|
1653
|
+
? JSON.stringify([account.credentialId, account.accountId ?? account.email, account.projectId])
|
|
1654
|
+
: undefined;
|
|
1655
|
+
return googleAntigravityModelManagerOptions({
|
|
1649
1656
|
oauthToken,
|
|
1650
|
-
|
|
1657
|
+
cacheScope,
|
|
1658
|
+
endpoint:
|
|
1659
|
+
this.#runtimeProviderOverrides.get("google-antigravity")?.baseUrl ??
|
|
1660
|
+
this.#providerOverrides.get("google-antigravity")?.baseUrl ??
|
|
1661
|
+
this.getProviderBaseUrl("google-antigravity"),
|
|
1651
1662
|
fetch: this.#fetch,
|
|
1652
|
-
|
|
1663
|
+
resolveOAuthToken: async signal => {
|
|
1664
|
+
if (!account) {
|
|
1665
|
+
return extractGoogleOAuthToken(
|
|
1666
|
+
await this.getApiKeyForProvider("google-antigravity", undefined, { signal }),
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const current = this.authStorage
|
|
1670
|
+
.listOAuthAccounts("google-antigravity")
|
|
1671
|
+
.find(entry => entry.credentialId === account.credentialId);
|
|
1672
|
+
if (!current) return undefined;
|
|
1673
|
+
const access = await this.authStorage.getOAuthAccessAt("google-antigravity", current.position, {
|
|
1674
|
+
signal,
|
|
1675
|
+
});
|
|
1676
|
+
return access?.ok ? access.accessToken : undefined;
|
|
1677
|
+
},
|
|
1678
|
+
});
|
|
1679
|
+
},
|
|
1653
1680
|
},
|
|
1654
1681
|
{
|
|
1655
1682
|
providerId: "google-gemini-cli",
|
|
@@ -1747,7 +1774,7 @@ export class ModelRegistry {
|
|
|
1747
1774
|
model.provider === options.providerId ? model : { ...model, provider: options.providerId },
|
|
1748
1775
|
);
|
|
1749
1776
|
const authoritativeProviders = new Set<string>();
|
|
1750
|
-
if (options.dynamicModelsAuthoritative && !result.stale) {
|
|
1777
|
+
if (options.dynamicModelsAuthoritative && (!result.stale || result.preservedAuthoritativeCache)) {
|
|
1751
1778
|
authoritativeProviders.add(options.providerId);
|
|
1752
1779
|
}
|
|
1753
1780
|
return { models, authoritativeProviders };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { MessengerConfig, MessengerPlatform, MessengerPlatformConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
const PLATFORMS: MessengerPlatform[] = ["discord", "telegram", "slack"];
|
|
5
|
+
const TOOLS: Record<string, true> = { read: true, grep: true, glob: true, bash: true, edit: true, write: true };
|
|
6
|
+
|
|
7
|
+
function object(value: unknown, label: string): Record<string, unknown> {
|
|
8
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
9
|
+
throw new Error(`${label} must be an object`);
|
|
10
|
+
}
|
|
11
|
+
return value as Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function keys(value: Record<string, unknown>, allowed: string[], label: string): void {
|
|
15
|
+
if (Object.keys(value).some(key => !allowed.includes(key))) throw new Error(`${label} contains an unknown setting`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function string(value: unknown, label: string): string {
|
|
19
|
+
if (typeof value !== "string" || value.trim() !== value || !value || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
20
|
+
throw new Error(`${label} must be a nonempty string without surrounding whitespace or control characters`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ids(value: unknown, label: string): string[] {
|
|
26
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`${label} must be a nonempty list of exact IDs`);
|
|
27
|
+
const result = value.map(item => string(item, label));
|
|
28
|
+
if (result.some(id => /[\s*]/u.test(id)) || new Set(result).size !== result.length) {
|
|
29
|
+
throw new Error(`${label} must contain unique exact IDs, without whitespace or wildcards`);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function envName(value: unknown, fallback: string, label: string): string {
|
|
35
|
+
const result = value === undefined ? fallback : string(value, label);
|
|
36
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(result)) throw new Error(`${label} must name an environment variable`);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseMessengerConfig(value: unknown, baseDir: string): MessengerConfig {
|
|
41
|
+
const input = object(value, "Messenger config");
|
|
42
|
+
keys(input, ["cwd", "sessionDir", "toolNames", "platforms"], "Messenger config");
|
|
43
|
+
const platformsInput = object(input.platforms, "platforms");
|
|
44
|
+
keys(platformsInput, PLATFORMS, "platforms");
|
|
45
|
+
if (Object.keys(platformsInput).length === 0) throw new Error("At least one messenger platform is required");
|
|
46
|
+
const platforms: MessengerConfig["platforms"] = {};
|
|
47
|
+
for (const platform of PLATFORMS) {
|
|
48
|
+
if (!Object.hasOwn(platformsInput, platform)) continue;
|
|
49
|
+
const item = object(platformsInput[platform], `${platform} config`);
|
|
50
|
+
keys(
|
|
51
|
+
item,
|
|
52
|
+
platform === "slack"
|
|
53
|
+
? ["tokenEnv", "appTokenEnv", "allowedUserIds", "allowedChannelIds"]
|
|
54
|
+
: ["tokenEnv", "allowedUserIds", "allowedChannelIds"],
|
|
55
|
+
`${platform} config`,
|
|
56
|
+
);
|
|
57
|
+
const config: MessengerPlatformConfig = {
|
|
58
|
+
tokenEnv: envName(item.tokenEnv, `JEOPI_${platform.toUpperCase()}_BOT_TOKEN`, `${platform}.tokenEnv`),
|
|
59
|
+
allowedUserIds: ids(item.allowedUserIds, `${platform}.allowedUserIds`),
|
|
60
|
+
};
|
|
61
|
+
if (platform === "slack")
|
|
62
|
+
config.appTokenEnv = envName(item.appTokenEnv, "JEOPI_SLACK_APP_TOKEN", "slack.appTokenEnv");
|
|
63
|
+
if (item.allowedChannelIds !== undefined)
|
|
64
|
+
config.allowedChannelIds = ids(item.allowedChannelIds, `${platform}.allowedChannelIds`);
|
|
65
|
+
platforms[platform] = config;
|
|
66
|
+
}
|
|
67
|
+
const toolNames = input.toolNames === undefined ? [] : input.toolNames;
|
|
68
|
+
if (
|
|
69
|
+
!Array.isArray(toolNames) ||
|
|
70
|
+
toolNames.some(name => typeof name !== "string" || !Object.hasOwn(TOOLS, name)) ||
|
|
71
|
+
new Set(toolNames).size !== toolNames.length
|
|
72
|
+
) {
|
|
73
|
+
throw new Error("toolNames must be a unique list drawn from read, grep, glob, bash, edit, write");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
cwd: path.resolve(baseDir, input.cwd === undefined ? "." : string(input.cwd, "cwd")),
|
|
77
|
+
sessionDir: path.resolve(
|
|
78
|
+
baseDir,
|
|
79
|
+
input.sessionDir === undefined ? ".jeopi/messenger-sessions" : string(input.sessionDir, "sessionDir"),
|
|
80
|
+
),
|
|
81
|
+
toolNames: [...toolNames] as string[],
|
|
82
|
+
platforms,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function loadMessengerConfig(file: string): Promise<MessengerConfig> {
|
|
87
|
+
let value: unknown;
|
|
88
|
+
try {
|
|
89
|
+
value = await Bun.file(file).json();
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error("Cannot read messenger configuration as JSON");
|
|
92
|
+
}
|
|
93
|
+
return parseMessengerConfig(value, path.dirname(path.resolve(file)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function validateMessengerEnvironment(
|
|
97
|
+
config: MessengerConfig,
|
|
98
|
+
env: Record<string, string | undefined> = process.env,
|
|
99
|
+
): void {
|
|
100
|
+
for (const platform of PLATFORMS) {
|
|
101
|
+
const item = config.platforms[platform];
|
|
102
|
+
if (!item) continue;
|
|
103
|
+
for (const name of [item.tokenEnv, item.appTokenEnv]) {
|
|
104
|
+
if (name === undefined) continue;
|
|
105
|
+
const token = env[name];
|
|
106
|
+
if (!token?.trim() || token !== token.trim() || /[\s\u0000-\u001f\u007f]/u.test(token)) {
|
|
107
|
+
throw new Error(`${platform} requires a nonempty credential in ${name}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|