Publish the TypeScript SDK and integration guides (#3141)
* feat(client): publish the TypeScript SDK surface Make the high-level client API usable for external integrations and document complete agent, workspace, provider, event, and recipe flows. * fix(client): correlate provider snapshots by daemon cwd Return the daemon's canonical snapshot identity so waitForReady can match updates for relative, tilde, and otherwise non-canonical paths without duplicating filesystem rules in the SDK. * test(server): make snapshot cwd assertion portable * fix(client): gate canonical provider snapshot waits Older daemons do not expose the canonical snapshot identity needed to correlate workspace updates safely. Require the advertised capability and fail with an update-host message instead of guessing across workspaces.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { createPaseoClient } from "./index.js";
|
||||
import type { PaseoAgent, PaseoClient, PaseoProviderConfig, PaseoWorkspace } from "./index.js";
|
||||
import type { PaseoAgent, PaseoClient, PaseoWorkspace } from "./index.js";
|
||||
|
||||
type FakeWebSocketHandler = (...args: unknown[]) => void;
|
||||
|
||||
@@ -80,7 +80,9 @@ function parseSentFrame(
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
async function connectClient(): Promise<{ client: PaseoClient; ws: FakeWebSocket }> {
|
||||
async function connectClient(
|
||||
features: Record<string, boolean> = { providersSnapshotCwd: true },
|
||||
): Promise<{ client: PaseoClient; ws: FakeWebSocket }> {
|
||||
vi.stubGlobal("WebSocket", FakeWebSocket);
|
||||
const client = createPaseoClient({
|
||||
url: "ws://daemon.test",
|
||||
@@ -105,6 +107,7 @@ async function connectClient(): Promise<{ client: PaseoClient; ws: FakeWebSocket
|
||||
serverId: "srv_sdk_test",
|
||||
hostname: null,
|
||||
version: null,
|
||||
features,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -211,6 +214,59 @@ test("createPaseoClient exposes workspace list through the daemon client", async
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent actions list the daemon directory without exposing the low-level client", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const listedAgent = createAgent({ title: "Planner" });
|
||||
|
||||
const listPromise = client.agents.list({
|
||||
filter: { includeArchived: false },
|
||||
page: { limit: 10 },
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
type: "fetch_agents_request",
|
||||
filter: { includeArchived: false },
|
||||
page: { limit: 10 },
|
||||
});
|
||||
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "fetch_agents_response",
|
||||
payload: {
|
||||
requestId: request.requestId,
|
||||
entries: [
|
||||
{
|
||||
agent: listedAgent,
|
||||
project: {
|
||||
projectKey: "project_sdk",
|
||||
projectName: "SDK",
|
||||
workspaceName: "main",
|
||||
checkout: {
|
||||
cwd: "/repo/sdk",
|
||||
isGit: false,
|
||||
currentBranch: null,
|
||||
remoteUrl: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(listPromise).resolves.toMatchObject({
|
||||
entries: [{ agent: { id: "agent_sdk", title: "Planner" } }],
|
||||
});
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("workspace handles keep identity and refresh snapshots through existing driver calls", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const openedWorkspace = createWorkspace();
|
||||
@@ -232,17 +288,18 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
}),
|
||||
);
|
||||
|
||||
const opened = await openPromise;
|
||||
const workspace = opened.workspace;
|
||||
expect(workspace?.id).toBe("workspace_sdk");
|
||||
expect(workspace?.latest()).toEqual(openedWorkspace);
|
||||
const workspace = await openPromise;
|
||||
expect(workspace.id).toBe("workspace_sdk");
|
||||
expect(workspace.projectId).toBe("project_sdk");
|
||||
expect(workspace.directory).toBe("/repo/sdk");
|
||||
expect(workspace.current()).toEqual(openedWorkspace);
|
||||
|
||||
const refreshedWorkspace = createWorkspace({ name: "sdk refreshed" });
|
||||
const refetchPromise = workspace?.refetch({ requestId: "workspace-refetch-request" });
|
||||
const refetchPromise = workspace.refresh({ requestId: "workspace-refetch-request" });
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
||||
type: "fetch_workspaces_request",
|
||||
requestId: "workspace-refetch-request",
|
||||
page: { limit: 25 },
|
||||
page: { limit: 200 },
|
||||
});
|
||||
|
||||
ws.message(
|
||||
@@ -250,6 +307,27 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
requestId: "workspace-refetch-request",
|
||||
entries: [],
|
||||
pageInfo: {
|
||||
nextCursor: "workspace-page-2",
|
||||
prevCursor: null,
|
||||
hasMore: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const secondPageRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(secondPageRequest).toMatchObject({
|
||||
type: "fetch_workspaces_request",
|
||||
page: { limit: 200, cursor: "workspace-page-2" },
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
requestId: secondPageRequest.requestId,
|
||||
entries: [refreshedWorkspace],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
@@ -261,10 +339,10 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
);
|
||||
|
||||
await expect(refetchPromise).resolves.toEqual(refreshedWorkspace);
|
||||
expect(workspace?.latest()).toEqual(refreshedWorkspace);
|
||||
expect(workspace.current()).toEqual(refreshedWorkspace);
|
||||
|
||||
const updates: string[] = [];
|
||||
const unsubscribe = workspace?.subscribe((update) => {
|
||||
const unsubscribe = workspace.subscribe((update) => {
|
||||
if (update.kind === "upsert") {
|
||||
updates.push(update.workspace.name);
|
||||
}
|
||||
@@ -280,9 +358,9 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
}),
|
||||
);
|
||||
expect(updates).toEqual(["sdk pushed"]);
|
||||
expect(workspace?.latest()).toEqual(pushedWorkspace);
|
||||
expect(workspace.current()).toEqual(pushedWorkspace);
|
||||
|
||||
unsubscribe?.();
|
||||
unsubscribe();
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "workspace_update",
|
||||
@@ -297,20 +375,79 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("workspace create is fresh and workspace-scoped agent create owns placement", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const createdWorkspace = createWorkspace({ id: "workspace_fresh", name: "Issue 42" });
|
||||
|
||||
const workspacePromise = client.workspaces.create({
|
||||
source: { kind: "directory", path: "/repo/sdk", projectId: "project_sdk" },
|
||||
title: "Issue 42",
|
||||
});
|
||||
const workspaceRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(workspaceRequest).toMatchObject({
|
||||
type: "workspace.create.request",
|
||||
source: { kind: "directory", path: "/repo/sdk", projectId: "project_sdk" },
|
||||
title: "Issue 42",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "workspace.create.response",
|
||||
payload: {
|
||||
requestId: workspaceRequest.requestId,
|
||||
workspace: createdWorkspace,
|
||||
setupTerminalId: null,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const workspace = await workspacePromise;
|
||||
const agentPromise = workspace.agents.create({
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
parent: client.agents.ref("parent_sdk"),
|
||||
prompt: "Implement issue 42.",
|
||||
});
|
||||
const agentRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(agentRequest).toMatchObject({
|
||||
type: "create_agent_request",
|
||||
config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk" },
|
||||
workspaceId: "workspace_fresh",
|
||||
callerAgentId: "parent_sdk",
|
||||
initialPrompt: "Implement issue 42.",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "agent_created",
|
||||
requestId: agentRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
agent: createAgent({ workspaceId: "workspace_fresh" }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const agent = await agentPromise;
|
||||
expect(agent.workspaceId).toBe("workspace_fresh");
|
||||
expect(agent.cwd).toBe("/repo/sdk");
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent handles delegate create, send, timeline refetch, archive, and local updates", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const createdAgent = createAgent();
|
||||
|
||||
const createPromise = client.agents.create({
|
||||
provider: "codex",
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
cwd: "/repo/sdk",
|
||||
initialPrompt: "ship it",
|
||||
prompt: "ship it",
|
||||
});
|
||||
const createRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(createRequest).toMatchObject({
|
||||
type: "create_agent_request",
|
||||
config: {
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
cwd: "/repo/sdk",
|
||||
},
|
||||
initialPrompt: "ship it",
|
||||
@@ -330,7 +467,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
|
||||
const agent = await createPromise;
|
||||
expect(agent.id).toBe("agent_sdk");
|
||||
expect(agent.latest()).toEqual(createdAgent);
|
||||
expect(agent.current()).toEqual(createdAgent);
|
||||
|
||||
const updatedAgents: string[] = [];
|
||||
const unsubscribe = agent.subscribe((update) => {
|
||||
@@ -350,7 +487,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
}),
|
||||
);
|
||||
expect(updatedAgents).toEqual(["Updated"]);
|
||||
expect(agent.latest()).toEqual(updatedAgent);
|
||||
expect(agent.current()).toEqual(updatedAgent);
|
||||
|
||||
const sendPromise = agent.send("hello", { messageId: "message-sdk" });
|
||||
const sendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
@@ -374,6 +511,74 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
);
|
||||
await sendPromise;
|
||||
|
||||
const runPromise = agent.run("finish the task", {
|
||||
messageId: "run-message-sdk",
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
const runSendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(runSendRequest).toMatchObject({
|
||||
type: "send_agent_message_request",
|
||||
agentId: "agent_sdk",
|
||||
text: "finish the task",
|
||||
messageId: "run-message-sdk",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "send_agent_message_response",
|
||||
payload: {
|
||||
requestId: runSendRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
accepted: true,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const waitRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(waitRequest).toMatchObject({
|
||||
type: "wait_for_finish_request",
|
||||
agentId: "agent_sdk",
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
const finishedAgent = createAgent({ title: "Finished" });
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "wait_for_finish_response",
|
||||
payload: {
|
||||
requestId: waitRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
status: "idle",
|
||||
final: finishedAgent,
|
||||
error: null,
|
||||
lastMessage: "DONE",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(runPromise).resolves.toMatchObject({ status: "idle", lastMessage: "DONE" });
|
||||
expect(agent.current()).toEqual(finishedAgent);
|
||||
|
||||
const defaultWaitPromise = agent.waitForFinish();
|
||||
const defaultWaitRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(defaultWaitRequest).toMatchObject({
|
||||
type: "wait_for_finish_request",
|
||||
agentId: "agent_sdk",
|
||||
timeoutMs: 10 * 60_000,
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "wait_for_finish_response",
|
||||
payload: {
|
||||
requestId: defaultWaitRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
status: "idle",
|
||||
final: finishedAgent,
|
||||
error: null,
|
||||
lastMessage: "DONE",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(defaultWaitPromise).resolves.toMatchObject({ status: "idle" });
|
||||
|
||||
const timelineAgent = createAgent({ title: "Timeline" });
|
||||
const timelinePromise = agent.timeline.refetch({ limit: 5 });
|
||||
const timelineRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
@@ -410,7 +615,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
}),
|
||||
);
|
||||
await timelinePromise;
|
||||
expect(agent.latest()).toEqual(timelineAgent);
|
||||
expect(agent.current()).toEqual(timelineAgent);
|
||||
|
||||
const archivePromise = agent.archive();
|
||||
const archiveRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
@@ -431,7 +636,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
await expect(archivePromise).resolves.toEqual({
|
||||
archivedAt: "2026-05-16T01:00:00.000Z",
|
||||
});
|
||||
expect(agent.latest()?.archivedAt).toBe("2026-05-16T01:00:00.000Z");
|
||||
expect(agent.current()?.archivedAt).toBe("2026-05-16T01:00:00.000Z");
|
||||
|
||||
unsubscribe();
|
||||
await client.close();
|
||||
@@ -496,10 +701,9 @@ test("provider actions delegate to existing provider RPCs and local snapshot upd
|
||||
|
||||
const featuresPromise = client.providers.listFeatures(
|
||||
{
|
||||
provider: "codex",
|
||||
provider: "codex/gpt-5.4",
|
||||
cwd: "/repo/sdk",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.4",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
},
|
||||
@@ -579,6 +783,81 @@ test("provider actions delegate to existing provider RPCs and local snapshot upd
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
});
|
||||
|
||||
const readyPromise = client.providers.waitForReady({ cwd: "/repo/sdk", timeoutMs: 5_000 });
|
||||
const readyRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(readyRequest).toMatchObject({
|
||||
type: "get_providers_snapshot_request",
|
||||
cwd: "/repo/sdk",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
requestId: readyRequest.requestId,
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "loading", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:00.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd: "/repo/other",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:30.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:01.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(readyPromise).resolves.toMatchObject({
|
||||
requestId: readyRequest.requestId,
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:01.000Z",
|
||||
});
|
||||
|
||||
const canonicalReadyPromise = client.providers.waitForReady({
|
||||
cwd: "/repo/./sdk",
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
const canonicalReadyRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:02.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
requestId: canonicalReadyRequest.requestId,
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "loading", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:01.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(canonicalReadyPromise).resolves.toMatchObject({
|
||||
requestId: canonicalReadyRequest.requestId,
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
});
|
||||
|
||||
const refreshPromise = client.providers.refresh({
|
||||
cwd: "/repo/sdk",
|
||||
providers: ["codex"],
|
||||
@@ -665,6 +944,18 @@ test("provider actions delegate to existing provider RPCs and local snapshot upd
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("waitForReady requires canonical provider snapshot identity from the host", async () => {
|
||||
const { client, ws } = await connectClient({});
|
||||
const sentBeforeWait = ws.sent.length;
|
||||
|
||||
await expect(client.providers.waitForReady({ cwd: "/repo/./sdk" })).rejects.toThrow(
|
||||
"Update the host to wait for provider discovery.",
|
||||
);
|
||||
expect(ws.sent).toHaveLength(sentBeforeWait);
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("config actions delegate to existing daemon config RPCs", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
|
||||
@@ -757,34 +1048,29 @@ test("config actions delegate to existing daemon config RPCs", async () => {
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("provider config builders shape existing create-agent config fields", async () => {
|
||||
test("agent config maps provider/model and provider-native options to the daemon", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const provider = client.providers.codex({
|
||||
model: "gpt-5.4",
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
});
|
||||
const expectedProviderConfig = {
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
} satisfies PaseoProviderConfig;
|
||||
|
||||
expect(provider).toEqual(expectedProviderConfig);
|
||||
|
||||
const createdAgent = createAgent({
|
||||
model: "gpt-5.4",
|
||||
currentModeId: "full-access",
|
||||
});
|
||||
const createPromise = client.agents.create({
|
||||
config: {
|
||||
...provider,
|
||||
cwd: "/repo/sdk",
|
||||
provider: "codex/gpt-5.4",
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
options: {
|
||||
approval_policy: "never",
|
||||
sandbox_mode: "workspace-write",
|
||||
sandbox_workspace_write: {
|
||||
writable_roots: ["/repo/sdk"],
|
||||
network_access: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialPrompt: "use configured provider",
|
||||
cwd: "/repo/sdk",
|
||||
prompt: "use configured provider",
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
@@ -796,6 +1082,14 @@ test("provider config builders shape existing create-agent config fields", async
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
providerOptions: {
|
||||
approval_policy: "never",
|
||||
sandbox_mode: "workspace-write",
|
||||
sandbox_workspace_write: {
|
||||
writable_roots: ["/repo/sdk"],
|
||||
network_access: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialPrompt: "use configured provider",
|
||||
});
|
||||
@@ -813,7 +1107,20 @@ test("provider config builders shape existing create-agent config fields", async
|
||||
);
|
||||
|
||||
const agent = await createPromise;
|
||||
expect(agent.latest()).toEqual(createdAgent);
|
||||
expect(agent.current()).toEqual(createdAgent);
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent config requires provider/model syntax", async () => {
|
||||
const { client } = await connectClient();
|
||||
|
||||
await expect(
|
||||
client.agents.create({
|
||||
config: { provider: "codex" },
|
||||
cwd: "/repo/sdk",
|
||||
}),
|
||||
).rejects.toThrow('Expected config.provider in "provider/model" format');
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user