jeopi-cli 16.6.9 → 16.6.12
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 +28 -0
- package/README.md +34 -9
- package/dist/cli.js +3492 -3483
- package/dist/types/main.d.ts +1 -1
- package/dist/types/messenger/artifacts.d.ts +12 -0
- package/dist/types/messenger/live-session.d.ts +32 -0
- package/dist/types/messenger/types.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/session-teardown.d.ts +13 -8
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/package.json +12 -12
- package/src/config/model-registry.ts +50 -17
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +12 -2
- package/src/messenger/artifacts.ts +167 -0
- package/src/messenger/config.ts +14 -2
- package/src/messenger/discord.ts +272 -22
- package/src/messenger/gateway.ts +53 -7
- package/src/messenger/http.ts +1 -0
- package/src/messenger/live-session.ts +502 -0
- package/src/messenger/session.ts +8 -1
- package/src/messenger/types.ts +10 -0
- package/src/modes/controllers/input-controller.ts +26 -4
- package/src/modes/interactive-mode.ts +31 -7
- package/src/modes/session-teardown.test.ts +61 -0
- package/src/modes/session-teardown.ts +20 -8
- package/src/modes/types.ts +2 -0
- package/src/prompts/system/messenger-artifacts.md +4 -0
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +14 -10
- package/src/slash-commands/builtin-registry.ts +127 -0
|
@@ -903,7 +903,19 @@ export class InputController {
|
|
|
903
903
|
try {
|
|
904
904
|
await this.ctx.withLocalSubmission(
|
|
905
905
|
text,
|
|
906
|
-
() =>
|
|
906
|
+
() => {
|
|
907
|
+
const submit = () => this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
|
|
908
|
+
const bridge = this.ctx.liveDiscordBridge;
|
|
909
|
+
return bridge?.active
|
|
910
|
+
? bridge.withLocalTurn(text, onPreparedUserMessage =>
|
|
911
|
+
this.ctx.session.prompt(text, {
|
|
912
|
+
streamingBehavior: "steer",
|
|
913
|
+
images,
|
|
914
|
+
onPreparedUserMessage,
|
|
915
|
+
}),
|
|
916
|
+
)
|
|
917
|
+
: submit();
|
|
918
|
+
},
|
|
907
919
|
{
|
|
908
920
|
imageCount: images?.length ?? 0,
|
|
909
921
|
},
|
|
@@ -1229,9 +1241,19 @@ export class InputController {
|
|
|
1229
1241
|
// Not streaming — just submit normally
|
|
1230
1242
|
this.ctx.editor.clearDraft(text);
|
|
1231
1243
|
try {
|
|
1232
|
-
await this.ctx.withLocalSubmission(
|
|
1233
|
-
|
|
1234
|
-
|
|
1244
|
+
await this.ctx.withLocalSubmission(
|
|
1245
|
+
text,
|
|
1246
|
+
() => {
|
|
1247
|
+
const submit = () => this.ctx.session.prompt(text, { images });
|
|
1248
|
+
const bridge = this.ctx.liveDiscordBridge;
|
|
1249
|
+
return bridge?.active
|
|
1250
|
+
? bridge.withLocalTurn(text, onPreparedUserMessage =>
|
|
1251
|
+
this.ctx.session.prompt(text, { images, onPreparedUserMessage }),
|
|
1252
|
+
)
|
|
1253
|
+
: submit();
|
|
1254
|
+
},
|
|
1255
|
+
{ imageCount: images?.length ?? 0 },
|
|
1256
|
+
);
|
|
1235
1257
|
} catch (error) {
|
|
1236
1258
|
restoreOnError(error);
|
|
1237
1259
|
}
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
MCP_CONNECTION_STATUS_EVENT_CHANNEL,
|
|
81
81
|
type McpConnectionStatusEvent,
|
|
82
82
|
} from "../mcp/startup-events";
|
|
83
|
+
import type { LiveDiscordBridge } from "../messenger/live-session";
|
|
83
84
|
import {
|
|
84
85
|
humanizePlanTitle,
|
|
85
86
|
type PlanApprovalDetails,
|
|
@@ -491,6 +492,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
491
492
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
492
493
|
collabHost?: CollabHost;
|
|
493
494
|
collabGuest?: CollabGuestLink;
|
|
495
|
+
liveDiscordBridge?: LiveDiscordBridge;
|
|
494
496
|
|
|
495
497
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
496
498
|
#cleanupUnsubscribe?: () => void;
|
|
@@ -796,10 +798,27 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
796
798
|
// signal arriving mid-Ctrl+C no-ops instead of racing a second dispose.
|
|
797
799
|
this.#signalTeardown = createSessionTeardown({
|
|
798
800
|
getDraftText: () => this.editor.getText(),
|
|
801
|
+
beforeDispose: async () => {
|
|
802
|
+
const liveDiscordBridge = this.liveDiscordBridge;
|
|
803
|
+
this.liveDiscordBridge = undefined;
|
|
804
|
+
try {
|
|
805
|
+
await liveDiscordBridge?.close();
|
|
806
|
+
} catch (error) {
|
|
807
|
+
logger.warn("Failed to close live Discord bridge before teardown", { error: String(error) });
|
|
808
|
+
}
|
|
809
|
+
},
|
|
799
810
|
beginDispose: () => this.session.beginDispose(),
|
|
800
811
|
saveDraft: text => this.sessionManager.saveDraft(text),
|
|
801
|
-
disposeSession: reason =>
|
|
802
|
-
|
|
812
|
+
disposeSession: async reason => {
|
|
813
|
+
const liveDiscordBridge = this.liveDiscordBridge;
|
|
814
|
+
this.liveDiscordBridge = undefined;
|
|
815
|
+
try {
|
|
816
|
+
await liveDiscordBridge?.close();
|
|
817
|
+
} catch (error) {
|
|
818
|
+
logger.warn("Failed to close live Discord bridge during teardown", { error: String(error) });
|
|
819
|
+
}
|
|
820
|
+
await this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS, reason });
|
|
821
|
+
},
|
|
803
822
|
});
|
|
804
823
|
// Forward the postmortem reason (SIGTERM/SIGHUP/uncaughtException/…) so the
|
|
805
824
|
// persisted `session_exit` diagnostic carries the real trigger. Postmortem
|
|
@@ -3349,14 +3368,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3349
3368
|
this.#isShuttingDown = true;
|
|
3350
3369
|
|
|
3351
3370
|
this.#btwController.dispose();
|
|
3352
|
-
this.#omfgController.dispose();
|
|
3353
3371
|
this.#focusController.dispose();
|
|
3354
|
-
|
|
3372
|
+
this.#omfgController.dispose();
|
|
3355
3373
|
// Surface an explicit "Closing session…" line so the user sees a reason
|
|
3356
3374
|
// for the pause while `session.dispose()` flushes memory consolidate and
|
|
3357
|
-
// other cleanups (issue #3641). The
|
|
3358
|
-
//
|
|
3359
|
-
|
|
3375
|
+
// other cleanups (issue #3641). The bridge is closed before disposal so
|
|
3376
|
+
// revoked Discord work cannot race session teardown.
|
|
3377
|
+
const liveDiscordBridge = this.liveDiscordBridge;
|
|
3378
|
+
this.liveDiscordBridge = undefined;
|
|
3379
|
+
try {
|
|
3380
|
+
await liveDiscordBridge?.close();
|
|
3381
|
+
} catch (error) {
|
|
3382
|
+
logger.warn("Failed to close live Discord bridge during shutdown", { error: String(error) });
|
|
3383
|
+
}
|
|
3360
3384
|
this.showStatus("Closing session…");
|
|
3361
3385
|
|
|
3362
3386
|
// Persist the draft and dispose the session through the shared teardown
|
|
@@ -66,6 +66,67 @@ describe("createSessionTeardown", () => {
|
|
|
66
66
|
expect(order).toEqual(["snapshot", "beginDispose", "saveDraft:start", "saveDraft:done", "disposeSession"]);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it("awaits beforeDispose before marking the session disposing", async () => {
|
|
70
|
+
const order: string[] = [];
|
|
71
|
+
const release = Promise.withResolvers<void>();
|
|
72
|
+
|
|
73
|
+
const teardown = createSessionTeardown({
|
|
74
|
+
getDraftText: () => "draft",
|
|
75
|
+
beforeDispose: async () => {
|
|
76
|
+
order.push("beforeDispose:start");
|
|
77
|
+
await release.promise;
|
|
78
|
+
order.push("beforeDispose:done");
|
|
79
|
+
},
|
|
80
|
+
beginDispose: () => {
|
|
81
|
+
order.push("beginDispose");
|
|
82
|
+
},
|
|
83
|
+
saveDraft: async () => {
|
|
84
|
+
order.push("saveDraft");
|
|
85
|
+
},
|
|
86
|
+
disposeSession: async () => {
|
|
87
|
+
order.push("disposeSession");
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const running = teardown();
|
|
92
|
+
expect(order).toEqual(["beforeDispose:start"]);
|
|
93
|
+
release.resolve();
|
|
94
|
+
await running;
|
|
95
|
+
|
|
96
|
+
expect(order).toEqual([
|
|
97
|
+
"beforeDispose:start",
|
|
98
|
+
"beforeDispose:done",
|
|
99
|
+
"beginDispose",
|
|
100
|
+
"saveDraft",
|
|
101
|
+
"disposeSession",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("continues teardown when beforeDispose rejects", async () => {
|
|
106
|
+
const order: string[] = [];
|
|
107
|
+
|
|
108
|
+
const teardown = createSessionTeardown({
|
|
109
|
+
getDraftText: () => "draft",
|
|
110
|
+
beforeDispose: async () => {
|
|
111
|
+
order.push("beforeDispose");
|
|
112
|
+
throw new Error("revocation failed");
|
|
113
|
+
},
|
|
114
|
+
beginDispose: () => {
|
|
115
|
+
order.push("beginDispose");
|
|
116
|
+
},
|
|
117
|
+
saveDraft: async () => {
|
|
118
|
+
order.push("saveDraft");
|
|
119
|
+
},
|
|
120
|
+
disposeSession: async () => {
|
|
121
|
+
order.push("disposeSession");
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await teardown();
|
|
126
|
+
|
|
127
|
+
expect(order).toEqual(["beforeDispose", "beginDispose", "saveDraft", "disposeSession"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
69
130
|
it("still disposes when saveDraft rejects — never leaves session_shutdown unemitted", async () => {
|
|
70
131
|
let disposed = false;
|
|
71
132
|
|
|
@@ -16,11 +16,16 @@ export interface SessionTeardownDeps {
|
|
|
16
16
|
/** Snapshot the current editor text; called once, before disposal touches session state. */
|
|
17
17
|
getDraftText: () => string;
|
|
18
18
|
/**
|
|
19
|
-
* Synchronously mark the session as disposing
|
|
20
|
-
*
|
|
21
|
-
* after a signal requested shutdown
|
|
19
|
+
* Synchronously mark the session as disposing after asynchronous revocation
|
|
20
|
+
* completes and before any further awaited teardown work. This closes the async
|
|
21
|
+
* gap where deferred jobs could otherwise start after a signal requested shutdown.
|
|
22
22
|
*/
|
|
23
23
|
beginDispose: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Complete asynchronous revocation before the session is marked disposed.
|
|
26
|
+
* Failures are logged and never skip the disposal chain.
|
|
27
|
+
*/
|
|
28
|
+
beforeDispose?: () => Promise<void>;
|
|
24
29
|
/**
|
|
25
30
|
* Persist the snapshotted draft. Called even for an empty string so a
|
|
26
31
|
* previously-persisted draft sidecar is cleared on a clean exit.
|
|
@@ -44,11 +49,11 @@ export interface SessionTeardownDeps {
|
|
|
44
49
|
export type SessionTeardown = (reason?: postmortem.Reason) => Promise<void>;
|
|
45
50
|
|
|
46
51
|
/**
|
|
47
|
-
* Build a promise-memoized teardown
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
+
* Build a promise-memoized teardown: snapshot the draft, complete asynchronous
|
|
53
|
+
* revocation, mark the session disposing synchronously, run `saveDraft` (draft-loss
|
|
54
|
+
* protection for `--resume`), then `disposeSession`; subsequent calls await the same
|
|
55
|
+
* settled promise so the keypress `InteractiveMode.shutdown()` path and the postmortem
|
|
56
|
+
* signal callback cannot
|
|
52
57
|
* double-emit `session_shutdown`, double-dispose the session's async-job
|
|
53
58
|
* manager, or race each other.
|
|
54
59
|
*
|
|
@@ -67,6 +72,13 @@ export function createSessionTeardown(deps: SessionTeardownDeps): SessionTeardow
|
|
|
67
72
|
let pending: Promise<void> | undefined;
|
|
68
73
|
const run = async (reason?: postmortem.Reason): Promise<void> => {
|
|
69
74
|
const draftText = deps.getDraftText();
|
|
75
|
+
if (deps.beforeDispose) {
|
|
76
|
+
try {
|
|
77
|
+
await deps.beforeDispose();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.warn("Failed pre-dispose cleanup during teardown", { error: String(err) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
70
82
|
deps.beginDispose();
|
|
71
83
|
try {
|
|
72
84
|
await deps.saveDraft(draftText);
|
package/src/modes/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
17
17
|
import type { Skill } from "../extensibility/skills";
|
|
18
18
|
import type { MCPManager } from "../mcp";
|
|
19
|
+
import type { LiveDiscordBridge } from "../messenger/live-session";
|
|
19
20
|
import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
|
|
20
21
|
import type { AgentSession } from "../session/agent-session";
|
|
21
22
|
import type { CompactMode } from "../session/compact-modes";
|
|
@@ -134,6 +135,7 @@ export interface InteractiveModeContext {
|
|
|
134
135
|
collabGuest?: CollabGuestLink;
|
|
135
136
|
eventController: EventController;
|
|
136
137
|
eventBus?: EventBus;
|
|
138
|
+
liveDiscordBridge?: LiveDiscordBridge;
|
|
137
139
|
|
|
138
140
|
// State
|
|
139
141
|
isInitialized: boolean;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
To deliver a requested file, write it inside {{directory}} and put [artifact:relative/path] on its own line in your final response. Only files in that directory can be uploaded; each must be at most 8 MiB. Never include credentials or secrets in output files.
|
|
2
|
+
{{#if inputs}}
|
|
3
|
+
Incoming files: {{inputs}}
|
|
4
|
+
{{/if}}
|
package/src/sdk.ts
CHANGED
|
@@ -1921,11 +1921,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1921
1921
|
const refreshedProviders = new Set<string>();
|
|
1922
1922
|
for (const selector of discoverySelectors) {
|
|
1923
1923
|
if (!selector) continue;
|
|
1924
|
-
const parsed =
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1924
|
+
const parsed =
|
|
1925
|
+
parseModelString(selector, {
|
|
1926
|
+
allowMaxAlias: true,
|
|
1927
|
+
allowAutoAlias: true,
|
|
1928
|
+
isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
|
|
1929
|
+
}) ??
|
|
1930
|
+
(!selector.includes("/") &&
|
|
1931
|
+
!/[?*[\]]/.test(selector) &&
|
|
1932
|
+
/argon|gemini-(?:3\.8|4)/i.test(selector) &&
|
|
1933
|
+
!parseModelPattern(selector, modelRegistry.getAll(), modelMatchPreferences).model
|
|
1934
|
+
? { provider: "google-antigravity", id: selector.split(":")[0] }
|
|
1935
|
+
: undefined);
|
|
1929
1936
|
if (!parsed || /[?*[\]]/.test(parsed.id)) continue;
|
|
1930
1937
|
const existing = modelRegistry.find(parsed.provider, parsed.id);
|
|
1931
1938
|
if (existing && hasModelAuth(existing)) break;
|
|
@@ -97,6 +97,7 @@ import type {
|
|
|
97
97
|
ToolChoice,
|
|
98
98
|
Usage,
|
|
99
99
|
UsageReport,
|
|
100
|
+
UserMessage,
|
|
100
101
|
} from "jeopi-ai";
|
|
101
102
|
import {
|
|
102
103
|
calculateRateLimitBackoffMs,
|
|
@@ -944,6 +945,8 @@ export interface PromptOptions {
|
|
|
944
945
|
userInitiated?: boolean;
|
|
945
946
|
/** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
|
|
946
947
|
attribution?: MessageAttribution;
|
|
948
|
+
/** Observe the exact user message prepared for this submission, including queued messages. */
|
|
949
|
+
onPreparedUserMessage?: (message: UserMessage) => void;
|
|
947
950
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
948
951
|
skipCompactionCheck?: boolean;
|
|
949
952
|
}
|
|
@@ -7539,9 +7542,9 @@ export class AgentSession {
|
|
|
7539
7542
|
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
7540
7543
|
}
|
|
7541
7544
|
if (options.streamingBehavior === "followUp") {
|
|
7542
|
-
await this.#queueUserMessage(expandedText, options?.images, "followUp");
|
|
7545
|
+
await this.#queueUserMessage(expandedText, options?.images, "followUp", options?.onPreparedUserMessage);
|
|
7543
7546
|
} else {
|
|
7544
|
-
await this.#queueUserMessage(expandedText, options?.images, "steer");
|
|
7547
|
+
await this.#queueUserMessage(expandedText, options?.images, "steer", options?.onPreparedUserMessage);
|
|
7545
7548
|
}
|
|
7546
7549
|
return true;
|
|
7547
7550
|
}
|
|
@@ -7568,6 +7571,7 @@ export class AgentSession {
|
|
|
7568
7571
|
const message = options?.synthetic
|
|
7569
7572
|
? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
|
|
7570
7573
|
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
7574
|
+
if (message.role === "user") options?.onPreparedUserMessage?.(message);
|
|
7571
7575
|
|
|
7572
7576
|
const preludeMessages: AgentMessage[] = [];
|
|
7573
7577
|
if (eagerTodoPrelude) {
|
|
@@ -8021,6 +8025,7 @@ export class AgentSession {
|
|
|
8021
8025
|
text: string,
|
|
8022
8026
|
images: ImageContent[] | undefined,
|
|
8023
8027
|
mode: "steer" | "followUp",
|
|
8028
|
+
onPreparedUserMessage?: (message: UserMessage) => void,
|
|
8024
8029
|
): Promise<void> {
|
|
8025
8030
|
// A queued user message (RPC/SDK/collab steer or follow-up, or a typed message
|
|
8026
8031
|
// while streaming) is a deliberate resume; re-enable advisor auto-resume that
|
|
@@ -8038,21 +8043,20 @@ export class AgentSession {
|
|
|
8038
8043
|
: undefined;
|
|
8039
8044
|
if (mode === "followUp") {
|
|
8040
8045
|
if (imageDescriptionNotice) this.agent.followUp(imageDescriptionNotice);
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
attribution: "user",
|
|
8045
|
-
timestamp: Date.now(),
|
|
8046
|
-
});
|
|
8046
|
+
const message: UserMessage = { role: "user", content, attribution: "user", timestamp: Date.now() };
|
|
8047
|
+
onPreparedUserMessage?.(message);
|
|
8048
|
+
this.agent.followUp(message);
|
|
8047
8049
|
} else {
|
|
8048
8050
|
if (imageDescriptionNotice) this.agent.steer(imageDescriptionNotice);
|
|
8049
|
-
|
|
8051
|
+
const message: UserMessage = {
|
|
8050
8052
|
role: "user",
|
|
8051
8053
|
content,
|
|
8052
8054
|
steering: true,
|
|
8053
8055
|
attribution: "user",
|
|
8054
8056
|
timestamp: Date.now(),
|
|
8055
|
-
}
|
|
8057
|
+
};
|
|
8058
|
+
onPreparedUserMessage?.(message);
|
|
8059
|
+
this.agent.steer(message);
|
|
8056
8060
|
}
|
|
8057
8061
|
this.#scheduleIdleQueueDrain();
|
|
8058
8062
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
MarketplaceManager,
|
|
25
25
|
} from "../extensibility/plugins/marketplace";
|
|
26
26
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
27
|
+
import { loadAndCreateLiveDiscordBridge } from "../messenger/live-session";
|
|
27
28
|
import { describeLoopLimitRuntime } from "../modes/loop-limit";
|
|
28
29
|
import { theme } from "../modes/theme/theme";
|
|
29
30
|
import type { InteractiveModeContext } from "../modes/types";
|
|
@@ -66,6 +67,13 @@ import type {
|
|
|
66
67
|
|
|
67
68
|
export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
68
69
|
|
|
70
|
+
interface LiveDiscordStart {
|
|
71
|
+
cancelled: boolean;
|
|
72
|
+
promise: Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const liveDiscordStarts = new WeakMap<InteractiveModeContext, LiveDiscordStart>();
|
|
76
|
+
|
|
69
77
|
/** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
|
|
70
78
|
export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
|
|
71
79
|
|
|
@@ -650,6 +658,125 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
650
658
|
runtime.ctx.editor.setText("");
|
|
651
659
|
},
|
|
652
660
|
},
|
|
661
|
+
{
|
|
662
|
+
name: "discord",
|
|
663
|
+
description: "Opt in to this session from an authorized Discord channel",
|
|
664
|
+
inlineHint: "[live|relay|stop|status] [configPath|channelId|off] [--allow-full-control]",
|
|
665
|
+
subcommands: [
|
|
666
|
+
{ name: "live", description: "Start the live Discord bridge with a private config file" },
|
|
667
|
+
{ name: "relay", description: "Relay local responses to an allowed channel, or turn relay off" },
|
|
668
|
+
{ name: "status", description: "Show live Discord bridge status" },
|
|
669
|
+
{ name: "stop", description: "Stop the live Discord bridge" },
|
|
670
|
+
],
|
|
671
|
+
allowArgs: true,
|
|
672
|
+
getTuiAutocompleteDescription: runtime =>
|
|
673
|
+
runtime.ctx.liveDiscordBridge?.active ? "Discord live bridge: active" : "Discord live bridge: off",
|
|
674
|
+
handleTui: async (command, runtime) => {
|
|
675
|
+
const ctx = runtime.ctx;
|
|
676
|
+
ctx.editor.setText("");
|
|
677
|
+
const { verb, rest } = parseSubcommand(command.args.trim());
|
|
678
|
+
if (verb === "status") {
|
|
679
|
+
if (!ctx.liveDiscordBridge?.active) {
|
|
680
|
+
ctx.showStatus("Discord live bridge is off");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const approvalMode = String(ctx.session.settings.get("tools.approvalMode"));
|
|
684
|
+
const activeTools = ctx.session.getActiveToolNames();
|
|
685
|
+
ctx.showStatus(
|
|
686
|
+
`Discord live bridge is active with full host control acknowledged. Approval mode: ${approvalMode}; active tools: ${activeTools.length > 0 ? activeTools.join(", ") : "none"}. Local response relay: ${ctx.liveDiscordBridge.relayDestination ?? "off"}.`,
|
|
687
|
+
);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (verb === "relay") {
|
|
691
|
+
const bridge = ctx.liveDiscordBridge;
|
|
692
|
+
if (!bridge?.active) {
|
|
693
|
+
ctx.showError("Start the Discord live bridge before enabling relay");
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (!rest || /\s/.test(rest)) {
|
|
697
|
+
ctx.showError("Usage: /discord relay <allowed-channel-id|off>");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
bridge.setRelayDestination(rest === "off" ? undefined : rest);
|
|
702
|
+
ctx.showStatus(
|
|
703
|
+
rest === "off"
|
|
704
|
+
? "Discord local response relay is off"
|
|
705
|
+
: `Discord local responses will be sent to channel ${rest}; /discord relay off revokes relay.`,
|
|
706
|
+
);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
ctx.showError(`Failed to configure Discord relay: ${errorMessage(error)}`);
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (verb === "stop") {
|
|
713
|
+
const bridge = ctx.liveDiscordBridge;
|
|
714
|
+
const start = liveDiscordStarts.get(ctx);
|
|
715
|
+
if (!bridge && start) {
|
|
716
|
+
start.cancelled = true;
|
|
717
|
+
ctx.showStatus("Discord live bridge startup cancellation requested");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (!bridge) {
|
|
721
|
+
ctx.showStatus("Discord live bridge is not active");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
ctx.liveDiscordBridge = undefined;
|
|
725
|
+
try {
|
|
726
|
+
await bridge.close();
|
|
727
|
+
ctx.showStatus("Discord live bridge stopped; access revoked");
|
|
728
|
+
} catch (error) {
|
|
729
|
+
ctx.showError(`Failed to stop Discord live bridge: ${errorMessage(error)}`);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const acknowledgment = /^(.+?)\s+--allow-full-control$/.exec(rest);
|
|
734
|
+
if (verb !== "live" || !acknowledgment) {
|
|
735
|
+
ctx.showError("Usage: /discord live <private messenger.json> --allow-full-control");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (ctx.liveDiscordBridge?.active || liveDiscordStarts.has(ctx)) {
|
|
739
|
+
ctx.showError("Discord live bridge is already active or starting (/discord stop first)");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
let resolveStart!: () => void;
|
|
743
|
+
const startPromise = new Promise<void>(resolve => {
|
|
744
|
+
resolveStart = resolve;
|
|
745
|
+
});
|
|
746
|
+
const startState: LiveDiscordStart = { cancelled: false, promise: startPromise };
|
|
747
|
+
liveDiscordStarts.set(ctx, startState);
|
|
748
|
+
try {
|
|
749
|
+
const configPath = resolveToCwd(acknowledgment[1]!, ctx.sessionManager.getCwd());
|
|
750
|
+
const bridge = await loadAndCreateLiveDiscordBridge(ctx, configPath, {}, { acknowledgeFullControl: true });
|
|
751
|
+
if (startState.cancelled || ctx.shutdownRequested || ctx.isShuttingDown) {
|
|
752
|
+
await bridge.close().catch(() => {});
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (ctx.liveDiscordBridge?.active) {
|
|
756
|
+
await bridge.close().catch(() => {});
|
|
757
|
+
ctx.showError("Discord live bridge is already active (/discord stop first)");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
ctx.liveDiscordBridge = bridge;
|
|
761
|
+
void bridge.run().catch(error => {
|
|
762
|
+
if (ctx.liveDiscordBridge !== bridge || ctx.isShuttingDown) return;
|
|
763
|
+
ctx.liveDiscordBridge = undefined;
|
|
764
|
+
ctx.showError(`Discord live bridge stopped: ${errorMessage(error)}`);
|
|
765
|
+
});
|
|
766
|
+
const approvalMode = String(ctx.session.settings.get("tools.approvalMode"));
|
|
767
|
+
const activeTools = ctx.session.getActiveToolNames();
|
|
768
|
+
ctx.showStatus(
|
|
769
|
+
`Discord live bridge active with full host control acknowledged. Approval mode: ${approvalMode}; active tools: ${activeTools.length > 0 ? activeTools.join(", ") : "none"}. /discord stop revokes access.`,
|
|
770
|
+
);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
ctx.showError(`Failed to start Discord live bridge: ${errorMessage(error)}`);
|
|
773
|
+
} finally {
|
|
774
|
+
resolveStart();
|
|
775
|
+
if (liveDiscordStarts.get(ctx)?.promise === startPromise) liveDiscordStarts.delete(ctx);
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
|
|
653
780
|
{
|
|
654
781
|
name: "collab",
|
|
655
782
|
description: "Share this session live via a relay",
|