@vellumai/assistant 0.12.0-staging.1 → 0.12.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.
Files changed (92) hide show
  1. package/Dockerfile +5 -0
  2. package/docs/architecture/turn-actor.md +9 -0
  3. package/knip.json +1 -0
  4. package/node_modules/@vellumai/app-icons/package.json +18 -0
  5. package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
  6. package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
  7. package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
  15. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
  16. package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
  17. package/openapi.yaml +628 -0
  18. package/package.json +3 -1
  19. package/scripts/smoke-container-workspace-dependencies.ts +19 -0
  20. package/src/__tests__/app-builder-icon-names.test.ts +29 -0
  21. package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  25. package/src/__tests__/conversation-attachments.test.ts +142 -0
  26. package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
  27. package/src/__tests__/conversation-event-sink.test.ts +50 -0
  28. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
  29. package/src/__tests__/conversation-queue.test.ts +297 -0
  30. package/src/__tests__/credential-routes.test.ts +185 -6
  31. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  32. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  33. package/src/__tests__/messaging-send-tool.test.ts +42 -0
  34. package/src/__tests__/oauth-commands-routes.test.ts +47 -0
  35. package/src/__tests__/subagent-manager-notify.test.ts +25 -4
  36. package/src/activation/progress-store.test.ts +1564 -0
  37. package/src/activation/progress-store.ts +1296 -0
  38. package/src/activation/turn-hooks.test.ts +246 -0
  39. package/src/activation/turn-hooks.ts +126 -0
  40. package/src/api/events/subagent-status-changed.ts +7 -5
  41. package/src/api/responses/activation.ts +136 -0
  42. package/src/apps/app-store.ts +8 -0
  43. package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
  44. package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
  45. package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
  46. package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
  47. package/src/cli/commands/channels/index.help.ts +70 -18
  48. package/src/cli/commands/channels/index.ts +17 -6
  49. package/src/cli/commands/channels/request.ts +66 -0
  50. package/src/cli/commands/oauth/request.test.ts +2 -0
  51. package/src/cli/commands/oauth/request.ts +227 -186
  52. package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
  54. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
  55. package/src/config/feature-flag-registry.json +35 -5
  56. package/src/daemon/assistant-attachments.ts +11 -0
  57. package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
  58. package/src/daemon/conversation-agent-loop.ts +71 -3
  59. package/src/daemon/conversation-attachments.ts +42 -1
  60. package/src/daemon/conversation-event-sink.ts +25 -0
  61. package/src/daemon/conversation-process.ts +69 -21
  62. package/src/daemon/conversation-store.ts +4 -3
  63. package/src/daemon/conversation-surfaces.ts +19 -4
  64. package/src/daemon/message-types/sync.ts +2 -0
  65. package/src/ipc/assistant-server.ts +2 -0
  66. package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
  67. package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
  68. package/src/notifications/AGENTS.md +1 -1
  69. package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
  70. package/src/notifications/conversation-pairing.ts +47 -5
  71. package/src/notifications/delivered-post-record.ts +3 -0
  72. package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
  73. package/src/persistence/conversation-crud.ts +39 -0
  74. package/src/persistence/delivery-crud.ts +13 -0
  75. package/src/plugins/AGENTS.md +1 -0
  76. package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
  77. package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
  78. package/src/runtime/routes/activation-routes.test.ts +415 -0
  79. package/src/runtime/routes/activation-routes.ts +172 -0
  80. package/src/runtime/routes/credential-routes.ts +46 -3
  81. package/src/runtime/routes/index.ts +2 -0
  82. package/src/runtime/routes/oauth-commands-routes.ts +21 -8
  83. package/src/runtime/routes/platform-managed-credentials.ts +48 -0
  84. package/src/runtime/routes/secret-routes.ts +3 -12
  85. package/src/runtime/routes/user-route-resolution.ts +21 -0
  86. package/src/runtime/routes/user-routes.ts +112 -9
  87. package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
  88. package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
  89. package/src/runtime/sync/resource-sync-events.ts +23 -0
  90. package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
  91. package/src/runtime/sync/worker-daemon-notify.ts +39 -1
  92. package/src/tools/apps/executors.ts +8 -8
@@ -0,0 +1,191 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // The command runs the authenticated-request route in-process, as `oauth
4
+ // request` does. The route is stubbed so the tests see exactly what provider
5
+ // key and request shape the command hands it; IPC is stubbed to prove the
6
+ // command never takes that path. The exit-code helpers stay real, so the
7
+ // tests assert the mapping that ships.
8
+ const ipcCalls: string[] = [];
9
+ const handleRequestCalls: Array<{ body: Record<string, unknown> }> = [];
10
+
11
+ const actualCliClient = await import("../../../../ipc/cli-client.js");
12
+ mock.module("../../../../ipc/cli-client.js", () => ({
13
+ ...actualCliClient,
14
+ cliIpcCall: async (method: string) => {
15
+ ipcCalls.push(method);
16
+ return { ok: false, error: `Unexpected IPC method ${method}` };
17
+ },
18
+ }));
19
+
20
+ mock.module("../../../../runtime/routes/oauth-commands-routes.js", () => ({
21
+ handleRequest: async (args: { body: Record<string, unknown> }) => {
22
+ handleRequestCalls.push(args);
23
+ return {
24
+ ok: true,
25
+ status: 200,
26
+ headers: { "content-type": "application/json" },
27
+ body: { ok: true, user_id: "U0BOT" },
28
+ };
29
+ },
30
+ }));
31
+
32
+ import { botProviderForChannel } from "@vellumai/service-contracts/channels";
33
+ import type { Command } from "commander";
34
+
35
+ import { runCliCommand } from "../../__tests__/cli-test-harness.js";
36
+ import { registerChannelsCommand } from "../index.js";
37
+ import { REQUESTABLE_CHANNELS } from "../request.js";
38
+
39
+ const { exitCodeFromIpcResult } = actualCliClient;
40
+
41
+ beforeEach(() => {
42
+ ipcCalls.length = 0;
43
+ handleRequestCalls.length = 0;
44
+ });
45
+
46
+ /**
47
+ * The shipped registration, plus the `--json` leaf option the program
48
+ * normally attaches after every command is registered.
49
+ */
50
+ function registerChannels(program: Command): void {
51
+ registerChannelsCommand(program);
52
+ program.commands
53
+ .find((c) => c.name() === "channels")
54
+ ?.commands.find((c) => c.name() === "request")
55
+ ?.option("--json");
56
+ }
57
+
58
+ function runChannelsRequest(args: string[]) {
59
+ return runCliCommand(registerChannels, ["channels", "request", ...args]);
60
+ }
61
+
62
+ describe("channel to bot provider", () => {
63
+ test("names the bot credential's provider for every built-in bot channel", () => {
64
+ // The map is the contract's, not a copy: the command answers exactly what
65
+ // CHANNEL_BOT_PROVIDER says, including telegram, whose bot key carries no
66
+ // suffix because Telegram has no user-identity integration.
67
+ expect(botProviderForChannel("slack")).toBe("slack_channel");
68
+ expect(botProviderForChannel("discord")).toBe("discord_channel");
69
+ expect(botProviderForChannel("telegram")).toBe("telegram");
70
+ expect([...REQUESTABLE_CHANNELS].sort()).toEqual([
71
+ "discord",
72
+ "slack",
73
+ "telegram",
74
+ ]);
75
+ });
76
+
77
+ test("a channel without a bot credential resolves to nothing", () => {
78
+ expect(botProviderForChannel("phone")).toBeUndefined();
79
+ expect(botProviderForChannel("vellum")).toBeUndefined();
80
+ expect(botProviderForChannel("not-a-channel")).toBeUndefined();
81
+ });
82
+
83
+ test("an inherited object property is not a channel", () => {
84
+ expect(botProviderForChannel("constructor")).toBeUndefined();
85
+ expect(botProviderForChannel("toString")).toBeUndefined();
86
+ expect(botProviderForChannel("__proto__")).toBeUndefined();
87
+ });
88
+ });
89
+
90
+ describe("assistant channels request", () => {
91
+ test("requests as the channel's bot, in-process, never over IPC", async () => {
92
+ const { stdout, exitCode } = await runChannelsRequest([
93
+ "slack",
94
+ "-s",
95
+ "/auth.test",
96
+ ]);
97
+
98
+ expect(exitCode).toBe(0);
99
+ expect(ipcCalls).toEqual([]);
100
+ expect(handleRequestCalls).toEqual([
101
+ { body: { provider: "slack_channel", url: "/auth.test" } },
102
+ ]);
103
+ expect(stdout).toContain("U0BOT");
104
+ });
105
+
106
+ test("forwards method and JSON body the way oauth request does", async () => {
107
+ await runChannelsRequest([
108
+ "slack",
109
+ "-s",
110
+ "-X",
111
+ "POST",
112
+ "-d",
113
+ '{"channel":"D0123456789","limit":20}',
114
+ "/conversations.history",
115
+ ]);
116
+
117
+ expect(handleRequestCalls).toEqual([
118
+ {
119
+ body: {
120
+ provider: "slack_channel",
121
+ url: "/conversations.history",
122
+ method: "POST",
123
+ parsed_data: { channel: "D0123456789", limit: 20 },
124
+ },
125
+ },
126
+ ]);
127
+ });
128
+
129
+ test("never offers the person's OAuth integration for a bot channel", async () => {
130
+ await runChannelsRequest(["discord", "-s", "/users/@me"]);
131
+
132
+ expect(handleRequestCalls[0]?.body.provider).toBe("discord_channel");
133
+ expect(handleRequestCalls[0]?.body).not.toHaveProperty("account");
134
+ expect(handleRequestCalls[0]?.body).not.toHaveProperty("client_id");
135
+ });
136
+
137
+ test("a channel with no bot credential is refused before any request", async () => {
138
+ const { stderr, exitCode } = await runChannelsRequest([
139
+ "phone",
140
+ "/anything",
141
+ ]);
142
+
143
+ expect(exitCode).toBe(1);
144
+ expect(handleRequestCalls).toEqual([]);
145
+ expect(stderr).toContain('Channel "phone" has no bot credential');
146
+ expect(stderr).toContain("plugins search");
147
+ });
148
+
149
+ test("names the channel command, not the OAuth one, in its diagnostics hint", async () => {
150
+ mock.module("../../../../runtime/routes/oauth-commands-routes.js", () => ({
151
+ handleRequest: async () => {
152
+ throw new Error("boom");
153
+ },
154
+ }));
155
+
156
+ const { stderr, exitCode } = await runChannelsRequest([
157
+ "slack",
158
+ "/auth.test",
159
+ ]);
160
+
161
+ expect(exitCode).toBe(1);
162
+ expect(stderr).toContain("assistant channels get slack");
163
+ expect(stderr).not.toContain("oauth providers get");
164
+ });
165
+
166
+ test("a structured route failure keeps the JSON envelope and the hint", async () => {
167
+ const { RouteError } = await import("../../../../runtime/routes/errors.js");
168
+ mock.module("../../../../runtime/routes/oauth-commands-routes.js", () => ({
169
+ handleRequest: async () => {
170
+ throw new RouteError(
171
+ "Provider not configured: slack_channel",
172
+ "NOT_FOUND",
173
+ 404,
174
+ );
175
+ },
176
+ }));
177
+
178
+ const { stdout, exitCode } = await runChannelsRequest([
179
+ "slack",
180
+ "--json",
181
+ "/auth.test",
182
+ ]);
183
+
184
+ // The exit code is the one the route's status maps to for every command.
185
+ expect(exitCode).toBe(exitCodeFromIpcResult({ statusCode: 404 }));
186
+ const envelope = JSON.parse(stdout.trim());
187
+ expect(envelope.ok).toBe(false);
188
+ expect(envelope.error).toContain("Provider not configured");
189
+ expect(envelope.error).toContain("assistant channels get slack");
190
+ });
191
+ });
@@ -1,40 +1,92 @@
1
1
  /** Declarative help for the `assistant channels` command. */
2
2
 
3
+ import {
4
+ CHANNEL_BOT_PROVIDER,
5
+ CHANNEL_IDS,
6
+ } from "@vellumai/service-contracts/channels";
7
+
3
8
  import type { CliCommandHelp } from "../../lib/cli-command-help.js";
4
9
 
5
- /** All channel IDs the readiness service knows about. Mirrors channels/types.ts. */
6
- export const KNOWN_CHANNELS = [
7
- "telegram",
8
- "phone",
9
- "vellum",
10
- "whatsapp",
11
- "slack",
12
- "email",
13
- "platform",
14
- "a2a",
15
- ] as const;
10
+ /**
11
+ * The channel ids the readiness route accepts: the canonical vocabulary. A
12
+ * channel with no built-in probe reports as unsupported rather than being
13
+ * refused, so the list is the contract's, not a copy kept here.
14
+ */
15
+ export const KNOWN_CHANNELS: readonly string[] = CHANNEL_IDS;
16
+
17
+ /** The channels whose bot the assistant can act as. */
18
+ const BOT_CHANNELS = Object.keys(CHANNEL_BOT_PROVIDER).join(", ");
16
19
 
17
20
  export const CHANNELS_PLUGIN_SEARCH_HINT =
18
21
  "If the channel you are looking for is not listed, search the plugin marketplace with 'assistant plugins search <name>'.";
19
22
 
20
23
  export const channelsHelp: CliCommandHelp = {
21
24
  name: "channels",
22
- description: "Inspect and repair messaging channels",
25
+ description: "Inspect messaging channels and act as their bots",
23
26
  helpText: `
24
- Channels are the messaging surfaces the assistant talks over. Built-in
25
- readiness probes cover slack, telegram, whatsapp, email, phone, vellum,
26
- platform, and a2a.
27
+ Channels are the messaging surfaces the assistant talks over. Channel ids:
28
+ ${KNOWN_CHANNELS.join(", ")}. A channel without a built-in readiness probe
29
+ reports as unsupported.
27
30
 
28
31
  ${CHANNELS_PLUGIN_SEARCH_HINT} Plugins can bundle additional channels
29
32
  from other Vellum users.
30
33
 
31
- list Overview of every channel + ready state
32
- get <channel> Live snapshot of one channel (always re-probes)
34
+ Two identities can reach a channel's platform. 'request' acts as the
35
+ assistant's own bot on that channel (${BOT_CHANNELS}), resolving the bot
36
+ credential from the channel id so no token is handled. Acting as a person
37
+ through their OAuth integration is 'assistant oauth request'.
33
38
 
34
39
  Examples:
35
40
  $ assistant channels list
36
- $ assistant channels get slack`,
41
+ $ assistant channels get slack
42
+ $ assistant channels request slack /auth.test --json
43
+ $ assistant channels request slack -X POST \\
44
+ -d '{"channel":"D0123456789","limit":20}' /conversations.history --json`,
37
45
  subcommands: [
46
+ {
47
+ name: "request",
48
+ args: "<channel> <url>",
49
+ description:
50
+ "Call the channel's platform API as the assistant's own bot on that channel (curl-like interface)",
51
+ // The request-shaping options (-X, -H, -d, -G, -I, -o, -s, -v, -i) are
52
+ // registered imperatively in request.ts: the repeatable "-H, --header"
53
+ // flag needs a Commander collect parser the declarative contract cannot
54
+ // express, and the options must keep their registration order around
55
+ // it.
56
+ helpText: `
57
+ Makes one authenticated request to the channel's platform API as the
58
+ assistant's bot. The bot credential is the one the channel's setup wizard
59
+ stored; it is resolved from the channel and sent from inside the assistant,
60
+ never printed or passed on the command line. Acting as a person through
61
+ their OAuth integration is a different identity and stays on
62
+ 'assistant oauth request --provider <key>'.
63
+
64
+ This command can do anything the bot's API allows, including sending,
65
+ editing, deleting, uploading, and reacting, so it is classified high risk
66
+ and asks for approval like any action with irreversible effects. Reads are
67
+ not distinguished from writes: the effect is the endpoint's. To send a
68
+ message, use the messaging tool, which records what it sent; do not post
69
+ through this command.
70
+
71
+ Arguments:
72
+ <channel> One of: ${BOT_CHANNELS}. A channel with no bot credential of
73
+ its own (phone, vellum) is refused.
74
+ <url> The API method path, relative to the platform's API host
75
+ (for Slack, '/conversations.history'; for Discord, '/users/@me';
76
+ for Telegram, '/getMe'). The provider supplies the host.
77
+
78
+ Options:
79
+ -X <method> HTTP method (default: GET; POST when -d is given).
80
+ -H 'Key: Value' Request header, repeatable.
81
+ -d <data> Request body: inline JSON, @file, or @- for stdin.
82
+ --json Machine-readable envelope: ok, status, headers, body.
83
+
84
+ Examples:
85
+ $ assistant channels request slack /auth.test --json
86
+ $ assistant channels request slack -X POST \\
87
+ -d '{"channel":"D0123456789","limit":100}' /conversations.history --json
88
+ $ assistant channels request discord /users/@me --json`,
89
+ },
38
90
  {
39
91
  name: "list",
40
92
  description: "Show readiness state for every configured channel",
@@ -1,15 +1,19 @@
1
1
  /**
2
- * `assistant channels` — inspect messaging channels.
2
+ * `assistant channels`: inspect messaging channels, and act as their bots.
3
3
  *
4
- * list — overview of every channel + ready state
5
- * get <channel> — detailed live snapshot of a single channel
4
+ * list : overview of every channel + ready state
5
+ * get <channel> : detailed live snapshot of a single channel
6
+ * request <channel> <url> : call the channel's platform API as its bot
6
7
  *
7
8
  * `get` always re-runs remote probes (it invalidates the readiness cache
8
9
  * before reading), so the CLI answer matches the live source-of-truth.
9
10
  *
10
- * A mutating `refresh` verb (for reconnecting channels — e.g. supplying
11
- * fresh Slack tokens) is intentionally not shipped here; it will land in
12
- * its own PR.
11
+ * `request` is the bot-identity door (see ./request.ts): it resolves the
12
+ * channel's bot credential and makes the same authenticated request
13
+ * `oauth request` makes, without the caller naming a provider.
14
+ *
15
+ * A mutating `refresh` verb (for reconnecting channels, e.g. supplying
16
+ * fresh Slack tokens) is intentionally not part of this group.
13
17
  */
14
18
 
15
19
  import type { Command } from "commander";
@@ -21,6 +25,7 @@ import { registerCommand } from "../../lib/register-command.js";
21
25
  import { log } from "../../logger.js";
22
26
  import { shouldOutputJson, writeOutput } from "../../output.js";
23
27
  import { CHANNELS_PLUGIN_SEARCH_HINT, channelsHelp } from "./index.help.js";
28
+ import { registerChannelsRequestCommand } from "./request.js";
24
29
 
25
30
  // ---------------------------------------------------------------------------
26
31
  // Snapshot shape
@@ -200,6 +205,12 @@ export function registerChannelsCommand(program: Command): void {
200
205
  }
201
206
  },
202
207
  );
208
+
209
+ // -----------------------------------------------------------------------
210
+ // request: call the channel's platform API as its bot
211
+ // -----------------------------------------------------------------------
212
+
213
+ registerChannelsRequestCommand(channels);
203
214
  },
204
215
  });
205
216
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `assistant channels request`: call a channel's platform API as the
3
+ * assistant's own bot on that channel.
4
+ *
5
+ * This is the bot-identity door. A channel's bot credential (the token the
6
+ * setup wizard stored) and a person's OAuth integration for the same brand
7
+ * are different identities that reach different things, and the provider
8
+ * keys do not say which is which (`slack_channel` is the bot, `slack` is the
9
+ * person; `telegram` is the bot). Keying the request on the channel id
10
+ * resolves the bot credential through the one map that owns that fact,
11
+ * `CHANNEL_BOT_PROVIDER`, so a caller names the channel and never chooses a
12
+ * provider. Acting as the person is `oauth request`.
13
+ *
14
+ * The request itself is the same authenticated request `oauth request`
15
+ * makes: the route resolves the provider's connection and injects the
16
+ * credential, so no token is ever handled here. A channel this map does not
17
+ * name has no bot credential of its own (phone, vellum, platform) or is not
18
+ * built in, and the command says so rather than guessing a provider.
19
+ */
20
+
21
+ import {
22
+ botProviderForChannel,
23
+ CHANNEL_BOT_PROVIDER,
24
+ } from "@vellumai/service-contracts/channels";
25
+ import type { Command } from "commander";
26
+
27
+ import { subcommand } from "../../lib/cli-command-help.js";
28
+ import { writeError } from "../../output.js";
29
+ import {
30
+ attachRequestOptions,
31
+ type AuthenticatedRequestOptions,
32
+ runAuthenticatedRequest,
33
+ } from "../oauth/request.js";
34
+ import { CHANNELS_PLUGIN_SEARCH_HINT } from "./index.help.js";
35
+
36
+ /** The channels whose bot credential this command can act with. */
37
+ export const REQUESTABLE_CHANNELS: readonly string[] =
38
+ Object.keys(CHANNEL_BOT_PROVIDER);
39
+
40
+ export function registerChannelsRequestCommand(channels: Command): void {
41
+ attachRequestOptions(subcommand(channels, "request")).action(
42
+ async (
43
+ channel: string,
44
+ url: string,
45
+ opts: AuthenticatedRequestOptions,
46
+ cmd: Command,
47
+ ) => {
48
+ const providerKey = botProviderForChannel(channel);
49
+ if (!providerKey) {
50
+ writeError(
51
+ cmd,
52
+ `Channel "${channel}" has no bot credential to request with. Channels with one: ${REQUESTABLE_CHANNELS.join(", ")}. ${CHANNELS_PLUGIN_SEARCH_HINT}`,
53
+ );
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ await runAuthenticatedRequest({
58
+ providerKey,
59
+ url,
60
+ opts,
61
+ diagnosticsHint: `For channel diagnostics, run 'assistant channels get ${channel}'.`,
62
+ cmd,
63
+ });
64
+ },
65
+ );
66
+ }
@@ -33,6 +33,8 @@ mock.module("../../../ipc/cli-client.js", () => ({
33
33
  exitFromIpcResult: (r: { error?: string }) => {
34
34
  throw new Error(r.error ?? "IPC error");
35
35
  },
36
+ exitCodeFromIpcResult: (r: { statusCode?: number }) =>
37
+ r.statusCode === undefined ? 10 : r.statusCode >= 500 ? 3 : 1,
36
38
  }));
37
39
 
38
40
  mock.module("../../../runtime/routes/oauth-commands-routes.js", () => ({