pi-roundtable 0.15.4 → 0.17.0
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 +24 -0
- package/README.md +5 -0
- package/docs/plugins.md +68 -0
- package/package.json +1 -1
- package/src/cli/checks/configuration.ts +21 -1
- package/src/cli/project.ts +59 -8
- package/src/cli/runtime.ts +3 -0
- package/src/core/agents/agent-store.ts +4 -1
- package/src/core/builtin/discord.ts +5 -0
- package/src/core/config/adapters.ts +12 -0
- package/src/core/contract/runtime.ts +7 -0
- package/src/core/contract/surface.ts +7 -0
- package/src/core/db/check-database.ts +39 -0
- package/src/core/define-roundtable.ts +3 -0
- package/src/core/discord/discord-surface.ts +5 -0
- package/src/core/discord/formatting.ts +20 -0
- package/src/core/host-registration.ts +292 -0
- package/src/core/host.ts +45 -227
- package/src/core/identity/config-owner.ts +34 -0
- package/src/core/identity/identity-plugin.ts +7 -2
- package/src/core/identity/identity-service.ts +8 -1
- package/src/core/identity/plugin-identities.ts +19 -4
- package/src/core/modules/skills/skill-store.ts +3 -1
- package/src/core/runtime/extensions/surface-formatting.ts +22 -0
- package/src/core/runtime/pi-agent-runtime.ts +6 -8
- package/src/core/runtime/running-turn.ts +19 -0
- package/src/core/runtime/runtime-plugin.ts +4 -0
- package/src/core/runtime/runtime-types.ts +12 -1
- package/src/core/runtime/session-factory.ts +7 -0
- package/src/core/testing/database.ts +20 -2
- package/src/core/testing/postgres.ts +160 -0
- package/src/core/testing/run-tool.ts +110 -0
- package/src/core/testing/test-host.ts +78 -30
- package/src/index.ts +1 -0
- package/src/testing.ts +25 -56
- package/templates/official/codex-images/plugin.test.ts.tmpl +29 -1
- package/templates/official/codex-images/plugin.ts +19 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { mkdtempSync } from "node:fs";
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
type ExtensionAPI,
|
|
6
5
|
type ExtensionFactory,
|
|
7
6
|
SessionManager,
|
|
8
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
|
|
9
9
|
import { DISCORD, type DiscordServices } from "../builtin/discord.ts";
|
|
10
10
|
import { type RoundtableConfig, resolveConfig } from "../config/config.ts";
|
|
11
11
|
import type { ConversationPort } from "../contract/channels.ts";
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
InteractionContribution,
|
|
22
22
|
} from "../discord/interaction-module.ts";
|
|
23
23
|
import { commandGuard, ownerRootCommand } from "../discord/owner-command.ts";
|
|
24
|
+
import type { HoldDescription } from "../holds.ts";
|
|
24
25
|
import { Roundtable } from "../host.ts";
|
|
25
26
|
import { silentLogger } from "../log.ts";
|
|
26
27
|
import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
|
|
@@ -28,9 +29,15 @@ import { CompactionTiers } from "../runtime/compaction-tiers.ts";
|
|
|
28
29
|
import { privateCompaction } from "../runtime/extensions/private-memory.ts";
|
|
29
30
|
import { sessionExtensions } from "../runtime/runtime-types.ts";
|
|
30
31
|
import { IDENTITY } from "../services.ts";
|
|
31
|
-
import type {
|
|
32
|
-
|
|
32
|
+
import type {
|
|
33
|
+
AgentTurnScope,
|
|
34
|
+
ChannelKey,
|
|
35
|
+
SessionContext,
|
|
36
|
+
} from "../sessions.ts";
|
|
37
|
+
import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
38
|
+
import { toolTurn } from "../tool-turn.ts";
|
|
33
39
|
import { testDatabaseUrl } from "./database.ts";
|
|
40
|
+
import { callRegisteredTool, registeredTools, toolNamed } from "./run-tool.ts";
|
|
34
41
|
|
|
35
42
|
export interface TestHostOptions {
|
|
36
43
|
/**
|
|
@@ -74,6 +81,26 @@ export interface TestHost {
|
|
|
74
81
|
): Promise<{ extension: string; tools: string[] }[]>;
|
|
75
82
|
/** A session context as the runtime builds it, with the real compaction wrapper. */
|
|
76
83
|
sessionContext(scope?: AgentTurnScope): SessionContext;
|
|
84
|
+
/**
|
|
85
|
+
* Runs a contributed tool in the owner's session as `speaker` on `channel` (the owner's channel
|
|
86
|
+
* by default), as `testPlugin`'s `runTool` does, and gives the text the model would read. A tool
|
|
87
|
+
* that throws throws here, a refusal (`ToolRefusal`) comes back as the text the model reads, and a
|
|
88
|
+
* tool no plugin added throws a PluginError.
|
|
89
|
+
*/
|
|
90
|
+
runTool(
|
|
91
|
+
name: string,
|
|
92
|
+
args: Record<string, unknown>,
|
|
93
|
+
options?: { speaker?: Speaker; channel?: ChannelKey; origin?: string },
|
|
94
|
+
): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* What the tool's hold would show for the call, asked as a turn of `speaker` on `channel` asks
|
|
97
|
+
* it, or undefined when the call runs without an approval.
|
|
98
|
+
*/
|
|
99
|
+
hold(
|
|
100
|
+
name: string,
|
|
101
|
+
args: Record<string, unknown>,
|
|
102
|
+
options?: { speaker?: Speaker; channel?: ChannelKey },
|
|
103
|
+
): Promise<HoldDescription | undefined>;
|
|
77
104
|
/** Stops the host, services first-started last. */
|
|
78
105
|
stop(): Promise<void>;
|
|
79
106
|
}
|
|
@@ -189,31 +216,6 @@ export function standInDiscord(
|
|
|
189
216
|
};
|
|
190
217
|
}
|
|
191
218
|
|
|
192
|
-
/** Anything an extension may call or read on the Pi API besides registering a tool: accepted, does nothing. */
|
|
193
|
-
type Ignored = (...args: readonly never[]) => Ignored;
|
|
194
|
-
|
|
195
|
-
function ignored(): Ignored {
|
|
196
|
-
return new Proxy((() => ignored()) as Ignored, {
|
|
197
|
-
get: (target, key) => (key === "then" ? undefined : target),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** The tools a factory registers, by name; every other registration (handlers, commands, events) is accepted and ignored. */
|
|
202
|
-
async function registeredBy(factory: ExtensionFactory): Promise<string[]> {
|
|
203
|
-
const names: string[] = [];
|
|
204
|
-
const registerTool = (tool: { name: string }) => names.push(tool.name);
|
|
205
|
-
// SAFETY: a session factory only registers tools, handlers and commands; the fake records the tool names and ignores the rest.
|
|
206
|
-
const api = new Proxy(
|
|
207
|
-
{},
|
|
208
|
-
{
|
|
209
|
-
get: (_target, key) =>
|
|
210
|
-
key === "registerTool" ? registerTool : ignored(),
|
|
211
|
-
},
|
|
212
|
-
) as unknown as ExtensionAPI;
|
|
213
|
-
await factory(api);
|
|
214
|
-
return names;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
219
|
const noop: ExtensionFactory = () => undefined;
|
|
218
220
|
/** The core's own extensions, which a session here does not load: only the plugins' registrations are listed. */
|
|
219
221
|
const CORE_EXTENSIONS = {
|
|
@@ -222,6 +224,7 @@ const CORE_EXTENSIONS = {
|
|
|
222
224
|
askUser: noop,
|
|
223
225
|
selfCompactGuard: noop,
|
|
224
226
|
privateMemory: noop,
|
|
227
|
+
surfaceFormatting: noop,
|
|
225
228
|
activeTools: noop,
|
|
226
229
|
};
|
|
227
230
|
|
|
@@ -296,6 +299,26 @@ export async function testHost(
|
|
|
296
299
|
const ownerChannel = withDiscord
|
|
297
300
|
? await (given?.ownerChannel ?? (async () => "discord:owner-dm" as const))()
|
|
298
301
|
: ("test:owner" as const);
|
|
302
|
+
let tools: string | undefined;
|
|
303
|
+
/** The directory the tool runs of this host keep their attachments in; made on the first run. */
|
|
304
|
+
const attachmentDir = () =>
|
|
305
|
+
(tools ??= mkdtempSync(join(tmpdir(), "roundtable-test-host-tools-")));
|
|
306
|
+
/** The owner's session as a tool call of `call` finds it: that speaker on that channel. */
|
|
307
|
+
const toolSession = (call?: {
|
|
308
|
+
speaker?: Speaker;
|
|
309
|
+
channel?: ChannelKey;
|
|
310
|
+
origin?: string;
|
|
311
|
+
}): SessionContext => {
|
|
312
|
+
const channel = call?.channel ?? ownerChannel;
|
|
313
|
+
return {
|
|
314
|
+
...sessionContext(),
|
|
315
|
+
homeChannel: channel,
|
|
316
|
+
turnChannel: channel,
|
|
317
|
+
attachmentDir: ownerAttachmentDir(attachmentDir(), channel),
|
|
318
|
+
speaker: () => call?.speaker,
|
|
319
|
+
origin: () => call?.origin,
|
|
320
|
+
};
|
|
321
|
+
};
|
|
299
322
|
/** A group seat's turns run in the group's channel; an agent's in its own. */
|
|
300
323
|
const turnChannelOf = (scope?: AgentTurnScope) =>
|
|
301
324
|
scope?.group ? scope.session : scope?.home;
|
|
@@ -351,13 +374,38 @@ export async function testHost(
|
|
|
351
374
|
))
|
|
352
375
|
tools.push({
|
|
353
376
|
extension: extension.name,
|
|
354
|
-
tools: await
|
|
377
|
+
tools: (await registeredTools(extension.factory)).map(
|
|
378
|
+
(tool) => tool.name,
|
|
379
|
+
),
|
|
355
380
|
});
|
|
356
381
|
return tools;
|
|
357
382
|
},
|
|
358
383
|
sessionContext,
|
|
384
|
+
runTool: async (name, args, call) => {
|
|
385
|
+
const channel = call?.channel ?? ownerChannel;
|
|
386
|
+
const session = toolSession(call);
|
|
387
|
+
const tools = [];
|
|
388
|
+
for (const extension of sessionExtensions(
|
|
389
|
+
context.sessions().plan,
|
|
390
|
+
session,
|
|
391
|
+
CORE_EXTENSIONS,
|
|
392
|
+
))
|
|
393
|
+
tools.push(...(await registeredTools(extension.factory)));
|
|
394
|
+
const result = await callRegisteredTool(
|
|
395
|
+
toolNamed(tools, name),
|
|
396
|
+
args,
|
|
397
|
+
context.surfaces.of(channel)?.supportsFiles === true,
|
|
398
|
+
);
|
|
399
|
+
return result.text;
|
|
400
|
+
},
|
|
401
|
+
hold: async (name, args, call) =>
|
|
402
|
+
context
|
|
403
|
+
.sessions()
|
|
404
|
+
.holds.inTurn?.(name, args, {}, toolTurn(toolSession(call), undefined)),
|
|
359
405
|
stop: async () => {
|
|
360
406
|
await roundtable.shutdown("test");
|
|
407
|
+
if (tools) rmSync(tools, { recursive: true, force: true });
|
|
408
|
+
tools = undefined;
|
|
361
409
|
},
|
|
362
410
|
};
|
|
363
411
|
}
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,7 @@ export type {
|
|
|
155
155
|
} from "./core/holds.ts";
|
|
156
156
|
export type { HostEnvironment, RoundtableOptions } from "./core/host.ts";
|
|
157
157
|
export { Roundtable } from "./core/host.ts";
|
|
158
|
+
export type { RegistrationReport } from "./core/host-registration.ts";
|
|
158
159
|
export type {
|
|
159
160
|
HttpRoute,
|
|
160
161
|
ListenerAddress,
|
package/src/testing.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdtempSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import type { SQL } from "bun";
|
|
6
5
|
import { agentClaim } from "./core/agents/agent-claim.ts";
|
|
7
6
|
import { ownerAttachmentDir } from "./core/attachments/attachment-dir.ts";
|
|
@@ -60,11 +59,6 @@ import {
|
|
|
60
59
|
} from "./core/registry/contributions.ts";
|
|
61
60
|
import { resolveProviders } from "./core/registry/providers.ts";
|
|
62
61
|
import { ServiceRegistry } from "./core/registry/services.ts";
|
|
63
|
-
import {
|
|
64
|
-
attachReplyFile,
|
|
65
|
-
hasReplyFileScope,
|
|
66
|
-
withReplyFiles,
|
|
67
|
-
} from "./core/reply-files.ts";
|
|
68
62
|
import { ChannelQueue } from "./core/routing/channel-queue.ts";
|
|
69
63
|
import { ChannelRouter } from "./core/routing/channel-router.ts";
|
|
70
64
|
import {
|
|
@@ -83,6 +77,10 @@ import {
|
|
|
83
77
|
import type { ChannelKey, SessionContext } from "./core/sessions.ts";
|
|
84
78
|
import { type Speaker, THE_SPEAKER } from "./core/speakers.ts";
|
|
85
79
|
import { mapIdentity } from "./core/testing/map-identity.ts";
|
|
80
|
+
import {
|
|
81
|
+
callRegisteredTool,
|
|
82
|
+
registeredTools,
|
|
83
|
+
} from "./core/testing/run-tool.ts";
|
|
86
84
|
import { type ToolTierTable, toolTiers } from "./core/tool-tiers.ts";
|
|
87
85
|
|
|
88
86
|
// Fixtures for plugin tests.
|
|
@@ -92,6 +90,7 @@ export type { TestStore } from "./core/testing/database.ts";
|
|
|
92
90
|
export {
|
|
93
91
|
describeDb,
|
|
94
92
|
openTestStore,
|
|
93
|
+
requireDatabase,
|
|
95
94
|
TEST_GUILD,
|
|
96
95
|
testDatabaseUrl,
|
|
97
96
|
} from "./core/testing/database.ts";
|
|
@@ -100,6 +99,8 @@ export type { TestLocale } from "./core/testing/locale.ts";
|
|
|
100
99
|
export { useTestLocale } from "./core/testing/locale.ts";
|
|
101
100
|
export { OWNER_SPEAKER } from "./core/testing/owner.ts";
|
|
102
101
|
export { partial } from "./core/testing/partial.ts";
|
|
102
|
+
export type { UsePostgresOptions } from "./core/testing/postgres.ts";
|
|
103
|
+
export { usePostgresForTests } from "./core/testing/postgres.ts";
|
|
103
104
|
export type {
|
|
104
105
|
FakePrecheck,
|
|
105
106
|
FakePrecheckAnswer,
|
|
@@ -447,6 +448,7 @@ export async function testPlugin(
|
|
|
447
448
|
sessions: () => linked ?? unlinked("sessions"),
|
|
448
449
|
toolTiers: tiers,
|
|
449
450
|
prompts: (channel, speaker) => surfaces.prompts(channel, speaker),
|
|
451
|
+
formatting: (channel) => surfaces.of(channel)?.formatting,
|
|
450
452
|
agents: {
|
|
451
453
|
workDir: tmpdir(),
|
|
452
454
|
skills: () => [],
|
|
@@ -606,6 +608,14 @@ export async function testPlugin(
|
|
|
606
608
|
// The plugin's own surfaces start as services; the injected ones start beside them, after.
|
|
607
609
|
const deliver = (message: InboundMessage) =>
|
|
608
610
|
void conversations.handle(message);
|
|
611
|
+
// As the host does, the plugin's preflight runs once everything is set up and before any service starts.
|
|
612
|
+
try {
|
|
613
|
+
await plugin.preflight?.();
|
|
614
|
+
} catch (error) {
|
|
615
|
+
// The runtime the provider built is not left running when the plugin refuses.
|
|
616
|
+
await runtime?.dispose?.();
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
609
619
|
for (const service of registry.services) await service.start?.();
|
|
610
620
|
for (const surface of options.surfaces ?? []) await surface.start(deliver);
|
|
611
621
|
await runtime?.preflight?.();
|
|
@@ -627,21 +637,6 @@ export async function testPlugin(
|
|
|
627
637
|
async runTool(name, args, runOptions) {
|
|
628
638
|
const tool = registry.tools.find((item) => item.name === name);
|
|
629
639
|
if (!tool) throw new PluginError(`tool ${name} is not registered`);
|
|
630
|
-
const registered: {
|
|
631
|
-
execute(
|
|
632
|
-
id: string,
|
|
633
|
-
params: Record<string, unknown>,
|
|
634
|
-
signal?: AbortSignal,
|
|
635
|
-
): Promise<{
|
|
636
|
-
content: readonly {
|
|
637
|
-
type: string;
|
|
638
|
-
text?: string;
|
|
639
|
-
data?: string;
|
|
640
|
-
mimeType?: string;
|
|
641
|
-
}[];
|
|
642
|
-
isError?: boolean;
|
|
643
|
-
}>;
|
|
644
|
-
}[] = [];
|
|
645
640
|
const channel = runOptions?.channel ?? "test:1";
|
|
646
641
|
const session = {
|
|
647
642
|
kind: "agent",
|
|
@@ -660,44 +655,18 @@ export async function testPlugin(
|
|
|
660
655
|
throw new Error("test session cannot run tasks");
|
|
661
656
|
},
|
|
662
657
|
} satisfies SessionContext;
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
registered.push(definition);
|
|
668
|
-
},
|
|
669
|
-
} as unknown as ExtensionAPI);
|
|
670
|
-
const execute = registered[0]?.execute;
|
|
671
|
-
if (!execute)
|
|
658
|
+
const [registered] = await registeredTools(
|
|
659
|
+
tool.session.snapshot().factory(session),
|
|
660
|
+
);
|
|
661
|
+
if (!registered)
|
|
672
662
|
throw new PluginError(`tool ${name} did not register in the session`);
|
|
673
|
-
const result = await
|
|
663
|
+
const result = await callRegisteredTool(
|
|
664
|
+
registered,
|
|
665
|
+
args,
|
|
674
666
|
surfaces.of(channel)?.supportsFiles === true,
|
|
675
|
-
async () => {
|
|
676
|
-
const output = await execute("test-call", args);
|
|
677
|
-
for (const item of output.content)
|
|
678
|
-
if (item.type === "image" && item.data && item.mimeType)
|
|
679
|
-
images.push({
|
|
680
|
-
channel,
|
|
681
|
-
image: {
|
|
682
|
-
mimeType: item.mimeType,
|
|
683
|
-
data: new Uint8Array(Buffer.from(item.data, "base64")),
|
|
684
|
-
},
|
|
685
|
-
});
|
|
686
|
-
return {
|
|
687
|
-
ok: true,
|
|
688
|
-
text: output.content
|
|
689
|
-
.filter((item) => item.type === "text")
|
|
690
|
-
.map((item) => item.text ?? "")
|
|
691
|
-
.join("\n"),
|
|
692
|
-
};
|
|
693
|
-
},
|
|
694
667
|
);
|
|
695
|
-
|
|
696
|
-
for (const file of result.files
|
|
697
|
-
files.push({ channel, file });
|
|
698
|
-
// When runTool is used by a fake runtime, forward into the enclosing real turn.
|
|
699
|
-
if (hasReplyFileScope()) attachReplyFile(file);
|
|
700
|
-
}
|
|
668
|
+
for (const image of result.images) images.push({ channel, image });
|
|
669
|
+
for (const file of result.files) files.push({ channel, file });
|
|
701
670
|
return result.text;
|
|
702
671
|
},
|
|
703
672
|
async stop() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import { PluginError } from "pi-roundtable";
|
|
3
|
-
import { testPlugin } from "pi-roundtable/testing";
|
|
3
|
+
import { recordingLogger, testPlugin } from "pi-roundtable/testing";
|
|
4
4
|
import {
|
|
5
5
|
type CodexFetch,
|
|
6
6
|
codexImages,
|
|
@@ -116,3 +116,31 @@ test("codex-images refuses to start without an openai-codex login", async () =>
|
|
|
116
116
|
expect((failure as PluginError).message).toContain("openai-codex");
|
|
117
117
|
expect((failure as PluginError).message).toContain("Log in");
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
test("codex-images with onMissingLogin warn starts without a login and fails the drawing", async () => {
|
|
121
|
+
const plugin = createCodexImages({
|
|
122
|
+
onMissingLogin: "warn",
|
|
123
|
+
fetch: async () => stream(completed),
|
|
124
|
+
});
|
|
125
|
+
const harness = await testPlugin(plugin);
|
|
126
|
+
const failure = await plugin.providers?.images?.("a fox", []).then(
|
|
127
|
+
() => undefined,
|
|
128
|
+
(error: unknown) => error,
|
|
129
|
+
);
|
|
130
|
+
expect(failure).toBeInstanceOf(PluginError);
|
|
131
|
+
expect((failure as PluginError).message).toContain("openai-codex");
|
|
132
|
+
await harness.stop();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("codex-images with onMissingLogin warn logs one warning at setup", async () => {
|
|
136
|
+
const { logger, lines } = recordingLogger();
|
|
137
|
+
const plugin = createCodexImages({ onMissingLogin: "warn" });
|
|
138
|
+
// SAFETY: setup reads only apiKey and logger.
|
|
139
|
+
await plugin.setup?.({ apiKey: async () => undefined, logger } as never);
|
|
140
|
+
expect(lines.map((line) => line.level)).toEqual(["warn"]);
|
|
141
|
+
expect(lines[0]?.message).toContain("openai-codex");
|
|
142
|
+
expect(lines[0]?.message).toContain(
|
|
143
|
+
"Drawing images fails until the login exists.",
|
|
144
|
+
);
|
|
145
|
+
expect(lines[0]?.message).not.toContain("plugin codex-images:");
|
|
146
|
+
});
|
|
@@ -31,6 +31,11 @@ export interface CodexImagesOptions {
|
|
|
31
31
|
fetch?: CodexFetch;
|
|
32
32
|
/** The model that routes the request; default `gpt-6-sol`. */
|
|
33
33
|
model?: string;
|
|
34
|
+
/**
|
|
35
|
+
* What setup does when the openai-codex login is missing or unusable: "throw" (the default)
|
|
36
|
+
* stops the host; "warn" logs a warning, and a drawing then fails with the same PluginError.
|
|
37
|
+
*/
|
|
38
|
+
onMissingLogin?: "throw" | "warn";
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** The ChatGPT account the login token belongs to, from the token's claims. */
|
|
@@ -197,7 +202,11 @@ const NO_LOGIN = `the host has no login for ${CODEX_PROVIDER}, the provider that
|
|
|
197
202
|
|
|
198
203
|
/** The plugin, with its request function and model replaceable for a test. */
|
|
199
204
|
export function createCodexImages(options: CodexImagesOptions = {}) {
|
|
200
|
-
const {
|
|
205
|
+
const {
|
|
206
|
+
fetch: fetchImpl = fetch,
|
|
207
|
+
model = CODEX_MODEL,
|
|
208
|
+
onMissingLogin = "throw",
|
|
209
|
+
} = options;
|
|
201
210
|
let apiKey: ((provider: string) => Promise<string | undefined>) | undefined;
|
|
202
211
|
// The login refreshes its token, so each image reads it again instead of keeping the first.
|
|
203
212
|
const token = async (): Promise<string> => {
|
|
@@ -214,7 +223,15 @@ export function createCodexImages(options: CodexImagesOptions = {}) {
|
|
|
214
223
|
},
|
|
215
224
|
async setup(context) {
|
|
216
225
|
apiKey = context.apiKey;
|
|
217
|
-
|
|
226
|
+
try {
|
|
227
|
+
await token();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (onMissingLogin !== "warn" || !(error instanceof PluginError))
|
|
230
|
+
throw error;
|
|
231
|
+
context.logger.warn(
|
|
232
|
+
`${NO_LOGIN} Drawing images fails until the login exists.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
218
235
|
return {};
|
|
219
236
|
},
|
|
220
237
|
});
|