jeopi-cli 16.6.13 → 16.6.17
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 +30 -0
- package/README.md +3 -1
- package/dist/cli.js +4158 -4105
- package/dist/types/cli/messenger-cli.d.ts +14 -3
- package/dist/types/commands/messenger.d.ts +21 -1
- package/dist/types/messenger/discord-setup.d.ts +48 -0
- package/dist/types/messenger/discord.d.ts +3 -0
- package/dist/types/messenger/gateway.d.ts +7 -0
- package/dist/types/messenger/service.d.ts +19 -0
- package/dist/types/messenger/session-pool.d.ts +38 -0
- package/dist/types/messenger/session-worker.d.ts +49 -0
- package/dist/types/messenger/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/messenger-cli.ts +99 -26
- package/src/cli.ts +13 -0
- package/src/commands/messenger.ts +26 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/messenger/discord-setup.ts +563 -0
- package/src/messenger/discord.ts +213 -54
- package/src/messenger/gateway.ts +170 -91
- package/src/messenger/service.ts +122 -0
- package/src/messenger/session-pool.ts +366 -0
- package/src/messenger/session-worker.ts +79 -0
- package/src/messenger/session.ts +5 -4
- package/src/messenger/types.ts +4 -0
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
export declare const MESSENGER_ACTIONS: readonly ["check", "run"];
|
|
1
|
+
export declare const MESSENGER_ACTIONS: readonly ["check", "run", "setup"];
|
|
2
2
|
export type MessengerAction = (typeof MESSENGER_ACTIONS)[number];
|
|
3
|
+
export interface MessengerSetupFlags {
|
|
4
|
+
channel?: string[];
|
|
5
|
+
user?: string[];
|
|
6
|
+
tools?: string;
|
|
7
|
+
artifacts?: boolean;
|
|
8
|
+
service?: boolean;
|
|
9
|
+
"no-dm"?: boolean;
|
|
10
|
+
}
|
|
3
11
|
export interface MessengerCommandArgs {
|
|
4
12
|
action: MessengerAction;
|
|
5
13
|
flags: {
|
|
6
14
|
config: string;
|
|
7
|
-
};
|
|
15
|
+
} & MessengerSetupFlags;
|
|
8
16
|
}
|
|
9
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Check is deliberately local-only: it never starts a transport. The gateway process itself never loads the
|
|
19
|
+
* SDK; every conversation's AgentSession lives in its own session process.
|
|
20
|
+
*/
|
|
10
21
|
export declare function runMessengerCommand(cmd: MessengerCommandArgs): Promise<void>;
|
|
@@ -5,13 +5,33 @@ export default class Messenger extends Command {
|
|
|
5
5
|
action: import("jeopi-utils/cli").ArgDescriptor & {
|
|
6
6
|
description: string;
|
|
7
7
|
required: false;
|
|
8
|
-
options: ("check" | "run")[];
|
|
8
|
+
options: ("check" | "run" | "setup")[];
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
11
|
static flags: {
|
|
12
12
|
config: import("jeopi-utils/cli").FlagDescriptor<"string"> & {
|
|
13
13
|
description: string;
|
|
14
14
|
};
|
|
15
|
+
channel: import("jeopi-utils/cli").FlagDescriptor<"string"> & {
|
|
16
|
+
description: string;
|
|
17
|
+
multiple: true;
|
|
18
|
+
};
|
|
19
|
+
user: import("jeopi-utils/cli").FlagDescriptor<"string"> & {
|
|
20
|
+
description: string;
|
|
21
|
+
multiple: true;
|
|
22
|
+
};
|
|
23
|
+
tools: import("jeopi-utils/cli").FlagDescriptor<"string"> & {
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
26
|
+
artifacts: import("jeopi-utils/cli").FlagDescriptor<"boolean"> & {
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
service: import("jeopi-utils/cli").FlagDescriptor<"boolean"> & {
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
"no-dm": import("jeopi-utils/cli").FlagDescriptor<"boolean"> & {
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
15
35
|
};
|
|
16
36
|
static examples: string[];
|
|
17
37
|
run(): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { installMessengerService } from "./service";
|
|
2
|
+
export declare const DISCORD_TOKEN_ENV = "JEOPI_DISCORD_BOT_TOKEN";
|
|
3
|
+
/** What the gateway needs in a request channel: read, reply, react, attach, and open request threads. */
|
|
4
|
+
export declare const DISCORD_REQUIRED_PERMISSIONS: ReadonlyArray<readonly [name: string, bit: bigint]>;
|
|
5
|
+
export interface DiscordSetupIO {
|
|
6
|
+
readonly interactive: boolean;
|
|
7
|
+
say(line: string): void;
|
|
8
|
+
ask(question: string): Promise<string>;
|
|
9
|
+
askSecret(question: string): Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
export interface DiscordSetupOptions {
|
|
12
|
+
/** messenger.json to create or update; its directory becomes the private profile directory. */
|
|
13
|
+
configPath: string;
|
|
14
|
+
channelIds?: string[];
|
|
15
|
+
userIds?: string[];
|
|
16
|
+
toolNames?: string[];
|
|
17
|
+
artifacts?: boolean;
|
|
18
|
+
service?: boolean;
|
|
19
|
+
/** Open the owner's DM channel, allow it, and send one test message. Default true. */
|
|
20
|
+
dmTest?: boolean;
|
|
21
|
+
inviteWaitMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DiscordSetupDeps {
|
|
24
|
+
fetch?: typeof fetch;
|
|
25
|
+
WebSocket?: typeof WebSocket;
|
|
26
|
+
env?: Record<string, string | undefined>;
|
|
27
|
+
sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
/** argv that starts this jeopi build; defaults to the running runtime and entry. */
|
|
29
|
+
jeopiCommand?: string[];
|
|
30
|
+
installService?: typeof installMessengerService;
|
|
31
|
+
}
|
|
32
|
+
export interface DiscordSetupResult {
|
|
33
|
+
configPath: string;
|
|
34
|
+
botName: string;
|
|
35
|
+
applicationId: string;
|
|
36
|
+
inviteUrl: string;
|
|
37
|
+
allowedUserIds: string[];
|
|
38
|
+
allowedChannelIds: string[];
|
|
39
|
+
runScript: string;
|
|
40
|
+
service?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function discordInviteUrl(applicationId: string): string;
|
|
43
|
+
/** Discord's documented order: @everyone and role bits, Administrator, then @everyone/role/member overwrites. */
|
|
44
|
+
export declare function discordChannelPermissions(guild: Record<string, unknown>, memberRoleIds: string[], channel: Record<string, unknown>, botId: string): bigint;
|
|
45
|
+
/** The token is read from the 0600 file at launch, so it never lands in this script or a service definition. */
|
|
46
|
+
export declare function discordRunScript(command: string[], configName: string, tokenEnv: string, envPath: string): string;
|
|
47
|
+
/** Create or repair a Discord messenger profile with as few manual steps as Discord allows. */
|
|
48
|
+
export declare function setupDiscordMessenger(options: DiscordSetupOptions, io: DiscordSetupIO, deps?: DiscordSetupDeps): Promise<DiscordSetupResult>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { MessengerAdapter, MessengerPlatformConfig, MessengerTransportDeps } from "./types";
|
|
2
|
+
export declare const DISCORD_API = "https://discord.com/api/v10";
|
|
3
|
+
export declare const DISCORD_GATEWAY_INTENTS: number;
|
|
2
4
|
export interface DiscordClock {
|
|
3
5
|
/** Schedule once and return an idempotent cancellation function. */
|
|
4
6
|
schedule(callback: () => void, delayMs: number): () => void;
|
|
@@ -7,4 +9,5 @@ export interface DiscordClock {
|
|
|
7
9
|
export interface DiscordTransportDeps extends MessengerTransportDeps {
|
|
8
10
|
clock?: DiscordClock;
|
|
9
11
|
}
|
|
12
|
+
export declare function discordGatewayUrl(value: unknown): string;
|
|
10
13
|
export declare function createDiscordAdapter(config: MessengerPlatformConfig, env?: Record<string, string | undefined>, deps?: DiscordTransportDeps): MessengerAdapter;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { MessengerAdapter, MessengerConfig, MessengerMessage, MessengerReply } from "./types";
|
|
2
2
|
/** Include the sender even in shared threads: remote histories must not cross users. */
|
|
3
3
|
export declare function messengerConversationKey(message: MessengerMessage): string;
|
|
4
|
+
/** Stable per-conversation route shared by the persisted session file and its session process. */
|
|
5
|
+
export declare function messengerSessionRoute(cwd: string, message: MessengerMessage): string;
|
|
4
6
|
export declare function isMessengerMessageAllowed(message: MessengerMessage, config: MessengerConfig): boolean;
|
|
5
7
|
export interface MessengerGateway {
|
|
6
8
|
run(signal: AbortSignal): Promise<void>;
|
|
@@ -9,5 +11,10 @@ export interface MessengerGateway {
|
|
|
9
11
|
export interface MessengerGatewayOptions {
|
|
10
12
|
/** Minimum spacing between in-place activity edits for one request. */
|
|
11
13
|
activityIntervalMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Requests processed at once (default 1). Requests of one prepared conversation still answer and deliver in
|
|
16
|
+
* order; above 1 the reply implementation must accept concurrent turns for different conversations.
|
|
17
|
+
*/
|
|
18
|
+
concurrency?: number;
|
|
12
19
|
}
|
|
13
20
|
export declare function createMessengerGateway(config: MessengerConfig, adapters: MessengerAdapter[], reply: MessengerReply, options?: MessengerGatewayOptions): MessengerGateway;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A per-user background service that keeps one messenger profile's gateway running. */
|
|
2
|
+
export interface MessengerServicePlan {
|
|
3
|
+
/** Stable service name derived from the profile directory. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Profile directory: working directory and home of the run script and log. */
|
|
6
|
+
directory: string;
|
|
7
|
+
runScript: string;
|
|
8
|
+
logFile: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function messengerServicePlan(directory: string, runScript: string): MessengerServicePlan;
|
|
11
|
+
/** launchd job: starts at login, restarts after a crash (throttled), logs to the profile directory. */
|
|
12
|
+
export declare function launchdPlist(plan: MessengerServicePlan): string;
|
|
13
|
+
/** systemd --user unit with the same lifecycle as the launchd job. */
|
|
14
|
+
export declare function systemdUnit(plan: MessengerServicePlan): string;
|
|
15
|
+
/**
|
|
16
|
+
* Install (or replace) and start the service, then confirm the platform reports it running.
|
|
17
|
+
* Returns a one-line description of where it was installed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installMessengerService(plan: MessengerServicePlan, platform?: NodeJS.Platform, home?: string): Promise<string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MessengerSessionInbound, MessengerSessionOutbound } from "./session-worker";
|
|
2
|
+
import type { MessengerConfig, MessengerReply } from "./types";
|
|
3
|
+
/** Hidden CLI selector that re-enters jeopi as one conversation's session process (dispatched in `cli.ts`). */
|
|
4
|
+
export declare const MESSENGER_SESSION_WORKER_ARG = "__omp_worker_messenger_session";
|
|
5
|
+
/** One conversation's jeopi session process, as the pool sees it. */
|
|
6
|
+
export interface MessengerSessionProcess {
|
|
7
|
+
send(message: MessengerSessionInbound): void;
|
|
8
|
+
onMessage(handler: (message: MessengerSessionOutbound) => void): () => void;
|
|
9
|
+
/** Settles when the process has exited for any reason. */
|
|
10
|
+
exited: Promise<unknown>;
|
|
11
|
+
kill(): void;
|
|
12
|
+
}
|
|
13
|
+
/** Spawn a real session process (the pool's default). */
|
|
14
|
+
export declare function spawnMessengerSessionProcess(config: MessengerConfig): MessengerSessionProcess;
|
|
15
|
+
/** `jeopi --smoke-test` probe: the session process loads its module graph and answers over IPC. */
|
|
16
|
+
export declare function smokeTestMessengerSessionWorker({ timeoutMs, }?: {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export interface MessengerSessionPoolOptions {
|
|
20
|
+
/** Resident session processes; at the limit a new conversation retires the least recently used idle one. */
|
|
21
|
+
maxProcesses?: number;
|
|
22
|
+
/** Idle time after which a conversation's process exits; its session file keeps the history. */
|
|
23
|
+
idleMs?: number;
|
|
24
|
+
/** Test seam: start one conversation's session process. */
|
|
25
|
+
spawn?: (config: MessengerConfig) => MessengerSessionProcess;
|
|
26
|
+
}
|
|
27
|
+
export interface MessengerSessionPool {
|
|
28
|
+
reply: MessengerReply;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Give every conversation (a Discord thread or DM, a Telegram chat or topic, a Slack thread) its own jeopi
|
|
33
|
+
* session process: the first message starts it, later turns reuse it, and it exits after idling; the next
|
|
34
|
+
* message resumes the persisted session in a fresh process. Turns of one conversation run strictly in order
|
|
35
|
+
* (including retirement), while different conversations run concurrently. At most `maxProcesses` processes
|
|
36
|
+
* are resident: a new conversation waits for the least recently used idle one to retire, or for a slot.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createMessengerSessionPool(config: MessengerConfig, options?: MessengerSessionPoolOptions): MessengerSessionPool;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createMessengerSessionBridge } from "./session";
|
|
2
|
+
import type { MessengerConfig, MessengerMessage } from "./types";
|
|
3
|
+
/** Gateway pool → session process. */
|
|
4
|
+
export type MessengerSessionInbound = {
|
|
5
|
+
type: "ping";
|
|
6
|
+
id: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "init";
|
|
9
|
+
id: string;
|
|
10
|
+
config: MessengerConfig;
|
|
11
|
+
} | {
|
|
12
|
+
type: "turn";
|
|
13
|
+
id: string;
|
|
14
|
+
message: MessengerMessage;
|
|
15
|
+
} | {
|
|
16
|
+
type: "abort";
|
|
17
|
+
id: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: "close";
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
/** Session process → gateway pool. Failures carry no detail: tool output and provider errors stay here. */
|
|
23
|
+
export type MessengerSessionOutbound = {
|
|
24
|
+
type: "pong";
|
|
25
|
+
id: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "activity";
|
|
28
|
+
id: string;
|
|
29
|
+
text: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "reply";
|
|
32
|
+
id: string;
|
|
33
|
+
text: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "failed";
|
|
36
|
+
id: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "closed";
|
|
39
|
+
id: string;
|
|
40
|
+
};
|
|
41
|
+
export interface MessengerSessionTransport {
|
|
42
|
+
send(message: MessengerSessionOutbound): void;
|
|
43
|
+
onMessage(handler: (message: MessengerSessionInbound) => void): () => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Serve one conversation's isolated AgentSession inside its own session process. The pool serializes turns, so
|
|
47
|
+
* the bridge sees one turn at a time and keeps its tool isolation and persisted history unchanged.
|
|
48
|
+
*/
|
|
49
|
+
export declare function startMessengerSessionWorker(transport: MessengerSessionTransport, createBridge?: typeof createMessengerSessionBridge): void;
|
|
@@ -24,11 +24,15 @@ export interface MessengerAdapter {
|
|
|
24
24
|
send(message: MessengerMessage, text: string, signal?: AbortSignal): Promise<void>;
|
|
25
25
|
prepare?(message: MessengerMessage, signal?: AbortSignal): Promise<MessengerMessage>;
|
|
26
26
|
status?(message: MessengerMessage, status: "processing" | "success" | "failure", signal?: AbortSignal): Promise<void>;
|
|
27
|
+
/** Remove an in-progress status after cancellation without adding a terminal status. */
|
|
28
|
+
clearStatus?(message: MessengerMessage, signal?: AbortSignal): Promise<void>;
|
|
27
29
|
/**
|
|
28
30
|
* Show `text` (one sanitized line) as the request's single in-progress status, editing it in place on
|
|
29
31
|
* later calls; `undefined` removes it. The gateway serializes and throttles calls per request.
|
|
30
32
|
*/
|
|
31
33
|
activity?(message: MessengerMessage, text: string | undefined, signal?: AbortSignal): Promise<void>;
|
|
34
|
+
/** Forget request-local cosmetic activity state after the gateway finishes cleanup retries. */
|
|
35
|
+
releaseActivity?(message: MessengerMessage): void;
|
|
32
36
|
}
|
|
33
37
|
export interface MessengerPlatformConfig {
|
|
34
38
|
tokenEnv: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-cli",
|
|
4
|
-
"version": "16.6.
|
|
4
|
+
"version": "16.6.17",
|
|
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.6.
|
|
61
|
-
"jeopi-stats": "16.6.
|
|
62
|
-
"jeopi-agent-core": "16.6.
|
|
63
|
-
"jeopi-ai": "16.6.
|
|
64
|
-
"jeopi-catalog": "16.6.
|
|
65
|
-
"jeopi-mnemopi": "16.6.
|
|
66
|
-
"jeopi-natives": "16.6.
|
|
67
|
-
"jeopi-tui": "16.6.
|
|
68
|
-
"jeopi-utils": "16.6.
|
|
69
|
-
"jeopi-wire": "16.6.
|
|
70
|
-
"jeopi-snapcompact": "16.6.
|
|
60
|
+
"jeopi-hashline": "16.6.17",
|
|
61
|
+
"jeopi-stats": "16.6.17",
|
|
62
|
+
"jeopi-agent-core": "16.6.17",
|
|
63
|
+
"jeopi-ai": "16.6.17",
|
|
64
|
+
"jeopi-catalog": "16.6.17",
|
|
65
|
+
"jeopi-mnemopi": "16.6.17",
|
|
66
|
+
"jeopi-natives": "16.6.17",
|
|
67
|
+
"jeopi-tui": "16.6.17",
|
|
68
|
+
"jeopi-utils": "16.6.17",
|
|
69
|
+
"jeopi-wire": "16.6.17",
|
|
70
|
+
"jeopi-snapcompact": "16.6.17",
|
|
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",
|
package/src/cli/messenger-cli.ts
CHANGED
|
@@ -1,18 +1,103 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
1
3
|
import { loadMessengerConfig, validateMessengerEnvironment } from "../messenger/config";
|
|
2
|
-
import
|
|
3
|
-
import { type
|
|
4
|
+
import { createDiscordAdapter } from "../messenger/discord";
|
|
5
|
+
import { type DiscordSetupIO, type DiscordSetupResult, setupDiscordMessenger } from "../messenger/discord-setup";
|
|
6
|
+
import { createMessengerGateway } from "../messenger/gateway";
|
|
7
|
+
import { createMessengerSessionPool, type MessengerSessionPool } from "../messenger/session-pool";
|
|
8
|
+
import { createSlackAdapter } from "../messenger/slack";
|
|
9
|
+
import { createTelegramAdapter } from "../messenger/telegram";
|
|
10
|
+
import type { MessengerAdapter, MessengerConfig } from "../messenger/types";
|
|
4
11
|
|
|
5
|
-
export const MESSENGER_ACTIONS = ["check", "run"] as const;
|
|
12
|
+
export const MESSENGER_ACTIONS = ["check", "run", "setup"] as const;
|
|
6
13
|
export type MessengerAction = (typeof MESSENGER_ACTIONS)[number];
|
|
14
|
+
export interface MessengerSetupFlags {
|
|
15
|
+
channel?: string[];
|
|
16
|
+
user?: string[];
|
|
17
|
+
tools?: string;
|
|
18
|
+
artifacts?: boolean;
|
|
19
|
+
service?: boolean;
|
|
20
|
+
"no-dm"?: boolean;
|
|
21
|
+
}
|
|
7
22
|
export interface MessengerCommandArgs {
|
|
8
23
|
action: MessengerAction;
|
|
9
|
-
flags: { config: string };
|
|
24
|
+
flags: { config: string } & MessengerSetupFlags;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Conversations answered at once; each conversation runs in its own session process. */
|
|
28
|
+
const MESSENGER_CONCURRENCY = 4;
|
|
29
|
+
|
|
30
|
+
function terminalSetupIO(): DiscordSetupIO {
|
|
31
|
+
const question = async (prompt: string, hidden: boolean): Promise<string> => {
|
|
32
|
+
// Hidden input: readline still reads keys but echoes into a sink.
|
|
33
|
+
const output = hidden ? new Writable({ write: (_chunk, _encoding, done) => done() }) : process.stdout;
|
|
34
|
+
if (hidden) process.stdout.write(prompt);
|
|
35
|
+
const rl = createInterface({ input: process.stdin, output, terminal: true });
|
|
36
|
+
try {
|
|
37
|
+
return await rl.question(hidden ? "" : prompt);
|
|
38
|
+
} finally {
|
|
39
|
+
rl.close();
|
|
40
|
+
if (hidden) process.stdout.write("\n");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
45
|
+
say: line => process.stdout.write(`${line}\n`),
|
|
46
|
+
ask: prompt => question(prompt, false),
|
|
47
|
+
askSecret: prompt => question(prompt, true),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function splitList(values: string[] | undefined): string[] | undefined {
|
|
52
|
+
const items = values
|
|
53
|
+
?.flatMap(value => value.split(","))
|
|
54
|
+
.map(value => value.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
return items?.length ? items : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function runSetup(flags: MessengerCommandArgs["flags"]): Promise<void> {
|
|
60
|
+
const io = terminalSetupIO();
|
|
61
|
+
let result: DiscordSetupResult;
|
|
62
|
+
try {
|
|
63
|
+
result = await setupDiscordMessenger(
|
|
64
|
+
{
|
|
65
|
+
configPath: flags.config,
|
|
66
|
+
channelIds: splitList(flags.channel),
|
|
67
|
+
userIds: splitList(flags.user),
|
|
68
|
+
toolNames: flags.tools === undefined ? undefined : (splitList([flags.tools]) ?? []),
|
|
69
|
+
artifacts: flags.artifacts,
|
|
70
|
+
service: flags.service,
|
|
71
|
+
dmTest: !flags["no-dm"],
|
|
72
|
+
},
|
|
73
|
+
io,
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Setup errors are written for the user and never carry the token.
|
|
77
|
+
process.stderr.write(`Discord setup stopped: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
io.say("");
|
|
82
|
+
io.say(`Discord messenger ready: ${result.configPath}`);
|
|
83
|
+
io.say(` allowed users: ${result.allowedUserIds.join(", ")}`);
|
|
84
|
+
io.say(` allowed channels: ${result.allowedChannelIds.join(", ")}`);
|
|
85
|
+
io.say(` invite (other servers): ${result.inviteUrl}`);
|
|
86
|
+
io.say(result.service ? ` service: ${result.service}` : ` start: ${result.runScript} (or rerun with --service)`);
|
|
87
|
+
io.say("Test: DM the bot, and mention it in the request channel; it replies in a thread.");
|
|
10
88
|
}
|
|
11
89
|
|
|
12
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Check is deliberately local-only: it never starts a transport. The gateway process itself never loads the
|
|
92
|
+
* SDK; every conversation's AgentSession lives in its own session process.
|
|
93
|
+
*/
|
|
13
94
|
export async function runMessengerCommand(cmd: MessengerCommandArgs): Promise<void> {
|
|
14
95
|
if (!MESSENGER_ACTIONS.includes(cmd.action)) throw new Error("Unknown messenger action");
|
|
15
96
|
if (!cmd.flags.config) throw new Error("Messenger requires --config <file>");
|
|
97
|
+
if (cmd.action === "setup") {
|
|
98
|
+
await runSetup(cmd.flags);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
16
101
|
// Do not echo parse errors, paths, configuration values, or environment secrets.
|
|
17
102
|
let config: MessengerConfig;
|
|
18
103
|
try {
|
|
@@ -38,35 +123,23 @@ export async function runMessengerCommand(cmd: MessengerCommandArgs): Promise<vo
|
|
|
38
123
|
const stop = (): void => controller.abort();
|
|
39
124
|
process.once("SIGINT", stop);
|
|
40
125
|
process.once("SIGTERM", stop);
|
|
41
|
-
let
|
|
126
|
+
let pool: MessengerSessionPool | undefined;
|
|
42
127
|
let failure: Error | undefined;
|
|
43
128
|
try {
|
|
44
|
-
|
|
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);
|
|
129
|
+
pool = createMessengerSessionPool(config);
|
|
54
130
|
const adapters: MessengerAdapter[] = [];
|
|
55
|
-
if (config.platforms.telegram) adapters.push(
|
|
56
|
-
if (config.platforms.discord) adapters.push(
|
|
57
|
-
if (config.platforms.slack) adapters.push(
|
|
58
|
-
const gateway = createMessengerGateway(config, adapters,
|
|
131
|
+
if (config.platforms.telegram) adapters.push(createTelegramAdapter(config.platforms.telegram));
|
|
132
|
+
if (config.platforms.discord) adapters.push(createDiscordAdapter(config.platforms.discord));
|
|
133
|
+
if (config.platforms.slack) adapters.push(createSlackAdapter(config.platforms.slack));
|
|
134
|
+
const gateway = createMessengerGateway(config, adapters, pool.reply, { concurrency: MESSENGER_CONCURRENCY });
|
|
59
135
|
process.stdout.write(`Starting messenger: ${platforms.join(", ")}. Press Ctrl+C to stop.\n`);
|
|
60
136
|
await gateway.run(controller.signal);
|
|
61
|
-
} catch
|
|
62
|
-
failure =
|
|
63
|
-
error instanceof MessengerFatalError
|
|
64
|
-
? error
|
|
65
|
-
: new Error("Messenger gateway failed; check local configuration and provider access");
|
|
137
|
+
} catch {
|
|
138
|
+
failure = new Error("Messenger gateway failed; check local configuration and provider access");
|
|
66
139
|
} finally {
|
|
67
140
|
controller.abort();
|
|
68
141
|
try {
|
|
69
|
-
await
|
|
142
|
+
await pool?.close();
|
|
70
143
|
} catch {
|
|
71
144
|
failure ??= new Error("Messenger session shutdown failed");
|
|
72
145
|
} finally {
|
package/src/cli.ts
CHANGED
|
@@ -63,6 +63,9 @@ async function runSmokeTest(): Promise<void> {
|
|
|
63
63
|
const { smokeTestTtsWorker } = await import("./tts/tts-client");
|
|
64
64
|
const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client");
|
|
65
65
|
const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
|
|
66
|
+
// Dynamic like its siblings: `session-pool` imports `jeopi-utils`, whose env module loads `.env` at import
|
|
67
|
+
// time and must not run before profile bootstrap (see the note above `showHelp`).
|
|
68
|
+
const { smokeTestMessengerSessionWorker } = await import("./messenger/session-pool");
|
|
66
69
|
await smokeTestSyncWorker();
|
|
67
70
|
|
|
68
71
|
const statsServer = await startServer(0);
|
|
@@ -82,6 +85,7 @@ async function runSmokeTest(): Promise<void> {
|
|
|
82
85
|
await smokeTestJsEvalWorker();
|
|
83
86
|
await smokeTestTtsWorker();
|
|
84
87
|
await smokeTestMnemopiEmbedWorker();
|
|
88
|
+
await smokeTestMessengerSessionWorker();
|
|
85
89
|
process.stdout.write("smoke-test: ok\n");
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -92,6 +96,8 @@ const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
|
|
|
92
96
|
const STT_WORKER_ARG = "__omp_worker_stt";
|
|
93
97
|
const TTS_WORKER_ARG = "__omp_worker_tts";
|
|
94
98
|
const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
|
|
99
|
+
/** Must match `MESSENGER_SESSION_WORKER_ARG` in `messenger/session-pool.ts`, which this entry cannot import early. */
|
|
100
|
+
const MESSENGER_SESSION_WORKER_ARG = "__omp_worker_messenger_session";
|
|
95
101
|
|
|
96
102
|
async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
97
103
|
if (arg === TINY_WORKER_ARG) {
|
|
@@ -152,6 +158,13 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
152
158
|
await runIpcSubprocessWorker(startMnemopiEmbedWorker);
|
|
153
159
|
return true;
|
|
154
160
|
}
|
|
161
|
+
if (arg === MESSENGER_SESSION_WORKER_ARG) {
|
|
162
|
+
// Dynamic: the session process loads the agent SDK only after profile bootstrap and worker dispatch;
|
|
163
|
+
// a static import would load it into every CLI start and every other worker kind.
|
|
164
|
+
const { startMessengerSessionWorker } = await import("./messenger/session-worker");
|
|
165
|
+
await runIpcSubprocessWorker(startMessengerSessionWorker);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
155
168
|
return false;
|
|
156
169
|
}
|
|
157
170
|
|
|
@@ -3,7 +3,7 @@ import { Args, Command, Flags, renderCommandHelp } from "jeopi-utils/cli";
|
|
|
3
3
|
import { MESSENGER_ACTIONS, type MessengerAction, runMessengerCommand } from "../cli/messenger-cli";
|
|
4
4
|
|
|
5
5
|
export default class Messenger extends Command {
|
|
6
|
-
static description = "
|
|
6
|
+
static description = "Set up and run authorized Discord, Telegram, and Slack text bots";
|
|
7
7
|
|
|
8
8
|
static args = {
|
|
9
9
|
action: Args.string({ description: "Sub-command", required: false, options: [...MESSENGER_ACTIONS] }),
|
|
@@ -11,9 +11,22 @@ export default class Messenger extends Command {
|
|
|
11
11
|
|
|
12
12
|
static flags = {
|
|
13
13
|
config: Flags.string({ description: "Path to messenger JSON configuration" }),
|
|
14
|
+
channel: Flags.string({
|
|
15
|
+
description: "setup: Discord request channel or thread ID (repeatable)",
|
|
16
|
+
multiple: true,
|
|
17
|
+
}),
|
|
18
|
+
user: Flags.string({
|
|
19
|
+
description: "setup: allowed Discord user ID (default: the application owner)",
|
|
20
|
+
multiple: true,
|
|
21
|
+
}),
|
|
22
|
+
tools: Flags.string({ description: "setup: comma-separated tools for remote chats (default: none)" }),
|
|
23
|
+
artifacts: Flags.boolean({ description: "setup: enable Discord file exchange in ./artifacts" }),
|
|
24
|
+
service: Flags.boolean({ description: "setup: run the gateway as a login service (launchd or systemd --user)" }),
|
|
25
|
+
"no-dm": Flags.boolean({ description: "setup: skip opening and testing the owner's DM channel" }),
|
|
14
26
|
};
|
|
15
27
|
|
|
16
28
|
static examples = [
|
|
29
|
+
"jeopi messenger setup --config ~/.jeopi/discord/messenger.json --service",
|
|
17
30
|
"jeopi messenger check --config ./messenger.json",
|
|
18
31
|
"jeopi messenger run --config ./messenger.json",
|
|
19
32
|
];
|
|
@@ -25,6 +38,17 @@ export default class Messenger extends Command {
|
|
|
25
38
|
return;
|
|
26
39
|
}
|
|
27
40
|
if (!flags.config) throw new Error("Messenger requires --config <file>");
|
|
28
|
-
await runMessengerCommand({
|
|
41
|
+
await runMessengerCommand({
|
|
42
|
+
action: args.action as MessengerAction,
|
|
43
|
+
flags: {
|
|
44
|
+
config: flags.config,
|
|
45
|
+
channel: flags.channel,
|
|
46
|
+
user: flags.user,
|
|
47
|
+
tools: flags.tools,
|
|
48
|
+
artifacts: flags.artifacts,
|
|
49
|
+
service: flags.service,
|
|
50
|
+
"no-dm": flags["no-dm"],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
29
53
|
}
|
|
30
54
|
}
|