jeopi-cli 16.6.8 → 16.6.11

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.
@@ -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,102 @@ 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|stop|status] [configPath] [--allow-full-control]",
665
+ subcommands: [
666
+ { name: "live", description: "Start the live Discord bridge with a private config file" },
667
+ { name: "status", description: "Show live Discord bridge status" },
668
+ { name: "stop", description: "Stop the live Discord bridge" },
669
+ ],
670
+ allowArgs: true,
671
+ getTuiAutocompleteDescription: runtime =>
672
+ runtime.ctx.liveDiscordBridge?.active ? "Discord live bridge: active" : "Discord live bridge: off",
673
+ handleTui: async (command, runtime) => {
674
+ const ctx = runtime.ctx;
675
+ ctx.editor.setText("");
676
+ const { verb, rest } = parseSubcommand(command.args.trim());
677
+ if (verb === "status") {
678
+ if (!ctx.liveDiscordBridge?.active) {
679
+ ctx.showStatus("Discord live bridge is off");
680
+ return;
681
+ }
682
+ const approvalMode = String(ctx.session.settings.get("tools.approvalMode"));
683
+ const activeTools = ctx.session.getActiveToolNames();
684
+ ctx.showStatus(
685
+ `Discord live bridge is active with full host control acknowledged. Approval mode: ${approvalMode}; active tools: ${activeTools.length > 0 ? activeTools.join(", ") : "none"}.`,
686
+ );
687
+ return;
688
+ }
689
+ if (verb === "stop") {
690
+ const bridge = ctx.liveDiscordBridge;
691
+ const start = liveDiscordStarts.get(ctx);
692
+ if (!bridge && start) {
693
+ start.cancelled = true;
694
+ ctx.showStatus("Discord live bridge startup cancellation requested");
695
+ return;
696
+ }
697
+ if (!bridge) {
698
+ ctx.showStatus("Discord live bridge is not active");
699
+ return;
700
+ }
701
+ ctx.liveDiscordBridge = undefined;
702
+ try {
703
+ await bridge.close();
704
+ ctx.showStatus("Discord live bridge stopped; access revoked");
705
+ } catch (error) {
706
+ ctx.showError(`Failed to stop Discord live bridge: ${errorMessage(error)}`);
707
+ }
708
+ return;
709
+ }
710
+ const acknowledgment = /^(.+?)\s+--allow-full-control$/.exec(rest);
711
+ if (verb !== "live" || !acknowledgment) {
712
+ ctx.showError("Usage: /discord live <private messenger.json> --allow-full-control");
713
+ return;
714
+ }
715
+ if (ctx.liveDiscordBridge?.active || liveDiscordStarts.has(ctx)) {
716
+ ctx.showError("Discord live bridge is already active or starting (/discord stop first)");
717
+ return;
718
+ }
719
+ let resolveStart!: () => void;
720
+ const startPromise = new Promise<void>(resolve => {
721
+ resolveStart = resolve;
722
+ });
723
+ const startState: LiveDiscordStart = { cancelled: false, promise: startPromise };
724
+ liveDiscordStarts.set(ctx, startState);
725
+ try {
726
+ const configPath = resolveToCwd(acknowledgment[1]!, ctx.sessionManager.getCwd());
727
+ const bridge = await loadAndCreateLiveDiscordBridge(ctx, configPath, {}, { acknowledgeFullControl: true });
728
+ if (startState.cancelled || ctx.shutdownRequested || ctx.isShuttingDown) {
729
+ await bridge.close().catch(() => {});
730
+ return;
731
+ }
732
+ if (ctx.liveDiscordBridge?.active) {
733
+ await bridge.close().catch(() => {});
734
+ ctx.showError("Discord live bridge is already active (/discord stop first)");
735
+ return;
736
+ }
737
+ ctx.liveDiscordBridge = bridge;
738
+ void bridge.run().catch(error => {
739
+ if (ctx.liveDiscordBridge !== bridge || ctx.isShuttingDown) return;
740
+ ctx.liveDiscordBridge = undefined;
741
+ ctx.showError(`Discord live bridge stopped: ${errorMessage(error)}`);
742
+ });
743
+ const approvalMode = String(ctx.session.settings.get("tools.approvalMode"));
744
+ const activeTools = ctx.session.getActiveToolNames();
745
+ ctx.showStatus(
746
+ `Discord live bridge active with full host control acknowledged. Approval mode: ${approvalMode}; active tools: ${activeTools.length > 0 ? activeTools.join(", ") : "none"}. /discord stop revokes access.`,
747
+ );
748
+ } catch (error) {
749
+ ctx.showError(`Failed to start Discord live bridge: ${errorMessage(error)}`);
750
+ } finally {
751
+ resolveStart();
752
+ if (liveDiscordStarts.get(ctx)?.promise === startPromise) liveDiscordStarts.delete(ctx);
753
+ }
754
+ },
755
+ },
756
+
653
757
  {
654
758
  name: "collab",
655
759
  description: "Share this session live via a relay",
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { type } from "arktype";
9
9
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "jeopi-agent-core";
10
- import type { AuthStorage } from "jeopi-ai";
10
+ import type { Api, AuthStorage, Model } from "jeopi-ai";
11
11
  import { prompt } from "jeopi-utils";
12
12
  import { settings } from "../../config/settings";
13
13
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
@@ -117,6 +117,7 @@ function hasRenderableSearchContent(response: SearchResponse): boolean {
117
117
 
118
118
  interface ExecuteSearchOptions {
119
119
  authStorage: AuthStorage;
120
+ model?: Model<Api>;
120
121
  sessionId?: string;
121
122
  signal?: AbortSignal;
122
123
  }
@@ -127,7 +128,7 @@ async function executeSearch(
127
128
  params: SearchQueryParams,
128
129
  options: ExecuteSearchOptions,
129
130
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
130
- const { authStorage, sessionId, signal } = options;
131
+ const { authStorage, model, sessionId, signal } = options;
131
132
  const explicitProvider = params.provider;
132
133
  let providers: SearchProvider[];
133
134
  if (explicitProvider && explicitProvider !== "auto") {
@@ -174,6 +175,7 @@ async function executeSearch(
174
175
  numSearchResults: params.num_search_results,
175
176
  temperature: params.temperature,
176
177
  signal,
178
+ model,
177
179
  authStorage,
178
180
  sessionId,
179
181
  antigravityEndpointMode,
@@ -222,7 +224,7 @@ async function executeSearch(
222
224
  */
223
225
  export async function runSearchQuery(
224
226
  params: SearchQueryParams,
225
- options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal } = {},
227
+ options: { authStorage?: AuthStorage; model?: Model<Api>; sessionId?: string; signal?: AbortSignal } = {},
226
228
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
227
229
  const createdAuthStorage = options.authStorage ? undefined : await discoverAuthStorage();
228
230
  const authStorage = options.authStorage ?? createdAuthStorage;
@@ -232,6 +234,7 @@ export async function runSearchQuery(
232
234
  try {
233
235
  return await executeSearch("cli-web-search", params, {
234
236
  authStorage,
237
+ model: options.model,
235
238
  sessionId: options.sessionId,
236
239
  signal: options.signal,
237
240
  });
@@ -271,7 +274,12 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
271
274
  ): Promise<AgentToolResult<SearchRenderDetails>> {
272
275
  const authStorage = this.#session.authStorage ?? (await discoverAuthStorage());
273
276
  const sessionId = this.#session.getSessionId?.() ?? undefined;
274
- return executeSearch(_toolCallId, params, { authStorage, sessionId, signal });
277
+ return executeSearch(_toolCallId, params, {
278
+ authStorage,
279
+ model: this.#session.getActiveModel?.(),
280
+ sessionId,
281
+ signal,
282
+ });
275
283
  }
276
284
  }
277
285
 
@@ -292,7 +300,7 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
292
300
  ) {
293
301
  const authStorage = ctx.modelRegistry?.authStorage ?? (await discoverAuthStorage());
294
302
  const sessionId = ctx.sessionManager.getSessionId();
295
- return executeSearch(toolCallId, params, { authStorage, sessionId, signal });
303
+ return executeSearch(toolCallId, params, { authStorage, model: ctx.model, sessionId, signal });
296
304
  },
297
305
 
298
306
  renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
@@ -1,6 +1,17 @@
1
- import type { AuthStorage, FetchImpl } from "jeopi-ai";
1
+ import type { Api, AuthStorage, FetchImpl, Model } from "jeopi-ai";
2
2
  import type { SearchProviderId, SearchResponse } from "../types";
3
3
 
4
+ export type GeminiOAuthProvider = "google-gemini-cli" | "google-antigravity";
5
+
6
+ /** Return the OAuth provider owned by a matching Gemini model, if any. */
7
+ export function resolveGeminiOAuthProvider(model?: Model<Api>): GeminiOAuthProvider | undefined {
8
+ if (!model?.id.startsWith("gemini-")) return undefined;
9
+ if (model.provider === "google-antigravity" || model.provider === "google-gemini-cli") {
10
+ return model.provider;
11
+ }
12
+ return undefined;
13
+ }
14
+
4
15
  /**
5
16
  * Shared web search parameters passed to providers.
6
17
  *
@@ -51,6 +62,8 @@ export interface SearchParams {
51
62
  * caller's agent session when available; otherwise omit.
52
63
  */
53
64
  sessionId?: string;
65
+ /** Active session model, when the caller has one. Providers may use it only when it matches their protocol. */
66
+ model?: Model<Api>;
54
67
  antigravityEndpointMode?: "auto" | "production" | "sandbox";
55
68
  }
56
69
 
@@ -8,7 +8,7 @@
8
8
  * sibling SQLite store and never POSTs the broker sentinel to a Google token
9
9
  * endpoint.
10
10
  */
11
- import { type AuthStorage, type FetchImpl, type OAuthAccess, withOAuthAccess } from "jeopi-ai";
11
+ import { type Api, type AuthStorage, type FetchImpl, type Model, type OAuthAccess, withOAuthAccess } from "jeopi-ai";
12
12
  import {
13
13
  ANTIGRAVITY_SYSTEM_INSTRUCTION,
14
14
  getAntigravityUserAgent,
@@ -18,8 +18,8 @@ import { fetchWithRetry } from "jeopi-utils";
18
18
 
19
19
  import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
20
20
  import { SearchProviderError } from "../../../web/search/types";
21
- import type { SearchParams } from "./base";
22
- import { SearchProvider } from "./base";
21
+ import type { GeminiOAuthProvider, SearchParams } from "./base";
22
+ import { resolveGeminiOAuthProvider, SearchProvider } from "./base";
23
23
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
24
24
 
25
25
  const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
@@ -52,6 +52,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
52
52
  temperature?: number;
53
53
  signal?: AbortSignal;
54
54
  authStorage: AuthStorage;
55
+ model?: Model<Api>;
55
56
  sessionId?: string;
56
57
  fetch?: FetchImpl;
57
58
  antigravityEndpointMode?: "auto" | "production" | "sandbox";
@@ -102,8 +103,10 @@ export async function findGeminiAuth(
102
103
  authStorage: AuthStorage,
103
104
  sessionId: string | undefined,
104
105
  signal: AbortSignal | undefined,
106
+ preferredProvider?: GeminiOAuthProvider,
105
107
  ): Promise<GeminiAuthSeed | null> {
106
- for (const provider of GEMINI_PROVIDERS) {
108
+ const providers = preferredProvider ? [preferredProvider] : GEMINI_PROVIDERS;
109
+ for (const provider of providers) {
107
110
  const access = await authStorage.getOAuthAccess(provider, sessionId, { signal });
108
111
  if (!access?.accessToken || !access.projectId) continue;
109
112
  return { provider, access, projectId: access.projectId };
@@ -160,13 +163,16 @@ interface CloudCodeResponseChunk {
160
163
  response?: GeminiModelResponse;
161
164
  }
162
165
 
163
- async function parseGeminiSearchStream(body: ReadableStream<Uint8Array>): Promise<GeminiSearchResult> {
166
+ async function parseGeminiSearchStream(
167
+ body: ReadableStream<Uint8Array>,
168
+ fallbackModel: string = DEFAULT_MODEL,
169
+ ): Promise<GeminiSearchResult> {
164
170
  const answerParts: string[] = [];
165
171
  const sources: SearchSource[] = [];
166
172
  const citations: SearchCitation[] = [];
167
173
  const searchQueries: string[] = [];
168
174
  const seenUrls = new Set<string>();
169
- let model = DEFAULT_MODEL;
175
+ let model = fallbackModel;
170
176
  let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
171
177
 
172
178
  const reader = body.getReader();
@@ -287,6 +293,7 @@ async function parseGeminiSearchStream(body: ReadableStream<Uint8Array>): Promis
287
293
  */
288
294
  async function callGeminiSearch(
289
295
  auth: GeminiAuth,
296
+ modelId: string,
290
297
  query: string,
291
298
  systemPrompt: string | undefined,
292
299
  maxOutputTokens: number | undefined,
@@ -330,7 +337,7 @@ async function callGeminiSearch(
330
337
 
331
338
  const requestBody: Record<string, unknown> = {
332
339
  project: auth.projectId,
333
- model: DEFAULT_MODEL,
340
+ model: modelId,
334
341
  request: {
335
342
  contents: [
336
343
  {
@@ -414,7 +421,7 @@ async function callGeminiSearch(
414
421
  throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
415
422
  }
416
423
 
417
- return parseGeminiSearchStream(response.body);
424
+ return parseGeminiSearchStream(response.body, modelId);
418
425
  }
419
426
 
420
427
  async function callGeminiDeveloperSearch(
@@ -494,7 +501,10 @@ async function callGeminiDeveloperSearch(
494
501
  * Executes a web search using Google Gemini with Google Search grounding.
495
502
  */
496
503
  export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
497
- const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
504
+ const selectedProvider = resolveGeminiOAuthProvider(params.model);
505
+ const selectedModelId =
506
+ selectedProvider && params.model ? (params.model.requestModelId ?? params.model.id) : DEFAULT_MODEL;
507
+ const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal, selectedProvider);
498
508
  let result: GeminiSearchResult;
499
509
 
500
510
  if (seed) {
@@ -513,6 +523,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
513
523
  projectId: access.projectId ?? seed.projectId,
514
524
  isAntigravity,
515
525
  },
526
+ selectedModelId,
516
527
  params.query,
517
528
  params.system_prompt,
518
529
  params.max_output_tokens,
@@ -528,6 +539,12 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
528
539
  ),
529
540
  { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
530
541
  );
542
+ } else if (selectedProvider) {
543
+ throw new SearchProviderError(
544
+ "gemini",
545
+ `No OAuth credentials found for selected Gemini provider "${selectedProvider}".`,
546
+ 401,
547
+ );
531
548
  } else {
532
549
  const apiKey = await params.authStorage.getApiKey(DEVELOPER_API_PROVIDER, params.sessionId, {
533
550
  signal: params.signal,
@@ -594,8 +611,10 @@ export class GeminiProvider extends SearchProvider {
594
611
  url_context: params.urlContext,
595
612
  signal: params.signal,
596
613
  authStorage: params.authStorage,
614
+ model: params.model,
597
615
  sessionId: params.sessionId,
598
616
  fetch: params.fetch,
617
+ antigravityEndpointMode: params.antigravityEndpointMode,
599
618
  });
600
619
  }
601
620
  }