jeopi-cli 16.6.14 → 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.
@@ -1,10 +1,18 @@
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
  /**
10
18
  * Check is deliberately local-only: it never starts a transport. The gateway process itself never loads the
@@ -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;
@@ -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>;
@@ -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.14",
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.14",
61
- "jeopi-stats": "16.6.14",
62
- "jeopi-agent-core": "16.6.14",
63
- "jeopi-ai": "16.6.14",
64
- "jeopi-catalog": "16.6.14",
65
- "jeopi-mnemopi": "16.6.14",
66
- "jeopi-natives": "16.6.14",
67
- "jeopi-tui": "16.6.14",
68
- "jeopi-utils": "16.6.14",
69
- "jeopi-wire": "16.6.14",
70
- "jeopi-snapcompact": "16.6.14",
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",
@@ -1,21 +1,92 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { Writable } from "node:stream";
1
3
  import { loadMessengerConfig, validateMessengerEnvironment } from "../messenger/config";
2
4
  import { createDiscordAdapter } from "../messenger/discord";
5
+ import { type DiscordSetupIO, type DiscordSetupResult, setupDiscordMessenger } from "../messenger/discord-setup";
3
6
  import { createMessengerGateway } from "../messenger/gateway";
4
7
  import { createMessengerSessionPool, type MessengerSessionPool } from "../messenger/session-pool";
5
8
  import { createSlackAdapter } from "../messenger/slack";
6
9
  import { createTelegramAdapter } from "../messenger/telegram";
7
10
  import type { MessengerAdapter, MessengerConfig } from "../messenger/types";
8
11
 
9
- export const MESSENGER_ACTIONS = ["check", "run"] as const;
12
+ export const MESSENGER_ACTIONS = ["check", "run", "setup"] as const;
10
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
+ }
11
22
  export interface MessengerCommandArgs {
12
23
  action: MessengerAction;
13
- flags: { config: string };
24
+ flags: { config: string } & MessengerSetupFlags;
14
25
  }
15
26
 
16
27
  /** Conversations answered at once; each conversation runs in its own session process. */
17
28
  const MESSENGER_CONCURRENCY = 4;
18
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.");
88
+ }
89
+
19
90
  /**
20
91
  * Check is deliberately local-only: it never starts a transport. The gateway process itself never loads the
21
92
  * SDK; every conversation's AgentSession lives in its own session process.
@@ -23,6 +94,10 @@ const MESSENGER_CONCURRENCY = 4;
23
94
  export async function runMessengerCommand(cmd: MessengerCommandArgs): Promise<void> {
24
95
  if (!MESSENGER_ACTIONS.includes(cmd.action)) throw new Error("Unknown messenger action");
25
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
+ }
26
101
  // Do not echo parse errors, paths, configuration values, or environment secrets.
27
102
  let config: MessengerConfig;
28
103
  try {
@@ -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 = "Run authorized Discord, Telegram, and Slack text bots";
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({ action: args.action as MessengerAction, flags: { config: flags.config } });
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
  }