@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -19,6 +19,10 @@ import {
19
19
  } from "../lib/install-from-github.js";
20
20
  import { listInstalledPlugins } from "../lib/list-installed-plugins.js";
21
21
  import { registerCommand } from "../lib/register-command.js";
22
+ import {
23
+ InvalidSearchPatternError,
24
+ searchPlugins,
25
+ } from "../lib/search-plugins.js";
22
26
  import {
23
27
  PluginNotInstalledError,
24
28
  uninstallPlugin,
@@ -42,6 +46,9 @@ Examples:
42
46
  $ assistant plugins install simple-memory --ref my-feature-branch
43
47
  $ assistant plugins list
44
48
  $ assistant plugins list --json
49
+ $ assistant plugins search memory
50
+ $ assistant plugins search "^simple"
51
+ $ assistant plugins search memory --json
45
52
  $ assistant plugins uninstall simple-memory`,
46
53
  );
47
54
 
@@ -134,6 +141,66 @@ Examples:
134
141
  );
135
142
  });
136
143
 
144
+ plugins
145
+ .command("search <query>")
146
+ .description(
147
+ "Search vellum-ai/vellum-assistant/experimental/plugins for plugin names matching <query> (case-insensitive regex)",
148
+ )
149
+ .option("--json", "Emit machine-readable JSON instead of a table")
150
+ .action(async (query: string, opts: { json?: boolean }) => {
151
+ try {
152
+ const result = await searchPlugins(
153
+ { query },
154
+ { fetch: globalThis.fetch.bind(globalThis) },
155
+ );
156
+
157
+ // Log on every success path — JSON output, empty results, and
158
+ // populated tables alike — so observability doesn't depend on
159
+ // which formatting branch the caller landed in.
160
+ log.info(
161
+ {
162
+ query: result.query,
163
+ ref: result.ref,
164
+ matchCount: result.matches.length,
165
+ },
166
+ "external plugin search",
167
+ );
168
+
169
+ if (opts.json) {
170
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
171
+ return;
172
+ }
173
+
174
+ if (result.matches.length === 0) {
175
+ console.log(`No plugins matched "${result.query}".`);
176
+ return;
177
+ }
178
+
179
+ const nameW = Math.max(
180
+ 4,
181
+ ...result.matches.map((m) => m.name.length),
182
+ );
183
+ const pad = (s: string, w: number) => s + " ".repeat(w - s.length);
184
+ console.log(`${pad("NAME", nameW)} PATH`);
185
+ for (const m of result.matches) {
186
+ console.log(`${pad(m.name, nameW)} ${m.path}`);
187
+ }
188
+ console.log("");
189
+ console.log(
190
+ `${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${result.query}".`,
191
+ );
192
+ } catch (err) {
193
+ if (err instanceof InvalidSearchPatternError) {
194
+ console.error(err.message);
195
+ process.exitCode = 1;
196
+ return;
197
+ }
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ console.error(`Plugin search failed: ${message}`);
200
+ process.exitCode = 1;
201
+ }
202
+ });
203
+
137
204
  plugins
138
205
  .command("uninstall <name>")
139
206
  .description("Remove a plugin from <workspaceDir>/plugins/<name>/")
@@ -1,6 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
 
3
3
  import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
4
+ import { confirmPrompt } from "../lib/confirm-prompt.js";
4
5
  import { registerCommand } from "../lib/register-command.js";
5
6
  import { log } from "../logger.js";
6
7
  import { writeOutput } from "../output.js";
@@ -60,18 +61,21 @@ export function registerSchedulesCommand(program: Command): void {
60
61
  schedules.addHelpText(
61
62
  "after",
62
63
  `
63
- Schedules are recurring or one-shot jobs run by the assistant daemon.
64
+ Schedules are recurring or one-shot jobs run by the assistant.
64
65
 
65
66
  This CLI namespace is intentionally landing incrementally. Today it supports
66
- listing schedules, viewing recent run history, and manually executing a schedule
67
- one time; create, delete, enable/disable, and run inspection will follow as
68
- separate slices.
67
+ listing schedules, viewing recent run history, enabling/disabling schedules,
68
+ manually executing a schedule one time, and cancelling pending one-shot schedules;
69
+ create, delete, and run inspection will follow as separate slices.
69
70
 
70
71
  Examples:
71
72
  $ assistant schedules list
72
73
  $ assistant schedules list --all
73
74
  $ assistant schedules runs <schedule-id>
74
75
  $ assistant schedules runs <schedule-id> --limit 25 --json
76
+ $ assistant schedules disable <schedule-id>
77
+ $ assistant schedules enable <schedule-id>
78
+ $ assistant schedules cancel <schedule-id>
75
79
  $ assistant schedules execute <schedule-id>`,
76
80
  );
77
81
 
@@ -203,7 +207,7 @@ Examples:
203
207
  "after",
204
208
  `
205
209
  Options:
206
- --limit <count> Max runs to return. The daemon clamps values to 1-100.
210
+ --limit <count> Max runs to return. The assistant clamps values to 1-100.
207
211
  --json Output the raw run list as compact JSON.
208
212
 
209
213
  Arguments:
@@ -307,6 +311,271 @@ Examples:
307
311
  },
308
312
  );
309
313
 
314
+ schedules
315
+ .command("create <name>")
316
+ .description("Create a new recurring schedule")
317
+ .requiredOption(
318
+ "-e, --expression <expr>",
319
+ "Cron or RRULE expression that schedules the fire times",
320
+ )
321
+ .requiredOption(
322
+ "-m, --message <text>",
323
+ "Message body sent to the assistant on each fire",
324
+ )
325
+ .option(
326
+ "-t, --timezone <tz>",
327
+ "IANA timezone for the expression (e.g. America/New_York)",
328
+ )
329
+ .option("--no-enabled", "Create the schedule in a disabled state")
330
+ .option("--json", "Machine-readable compact JSON output")
331
+ .addHelpText(
332
+ "after",
333
+ `
334
+ Options:
335
+ -e, --expression <expr> Cron (e.g. '*/30 * * * *') or RRULE expression.
336
+ -m, --message <text> Message body sent on each fire.
337
+ -t, --timezone <tz> IANA timezone applied to the expression.
338
+ --no-enabled Create the schedule disabled. Defaults to enabled.
339
+ --json Output the updated schedule list as compact JSON.
340
+
341
+ Arguments:
342
+ <name> Display name for the schedule.
343
+
344
+ Behavior:
345
+ Creates a recurring schedule in 'execute' mode. The IPC endpoint is
346
+ currently locked to execute mode; notify/script/wake schedules remain
347
+ reachable only through the in-assistant schedule_create LLM tool.
348
+
349
+ Examples:
350
+ $ assistant schedules create "Heartbeat" \\
351
+ --expression '*/30 * * * *' \\
352
+ --message 'run heartbeat'
353
+ $ assistant schedules create "Morning summary" \\
354
+ --expression '0 9 * * MON-FRI' \\
355
+ --timezone America/New_York \\
356
+ --message 'write the morning summary'
357
+ $ assistant schedules create "Drafted" \\
358
+ --expression '0 0 * * *' \\
359
+ --message 'placeholder' \\
360
+ --no-enabled --json`,
361
+ )
362
+ .action(
363
+ async (
364
+ name: string,
365
+ opts: {
366
+ expression: string;
367
+ message: string;
368
+ timezone?: string;
369
+ enabled: boolean;
370
+ json?: boolean;
371
+ },
372
+ cmd: Command,
373
+ ) => {
374
+ const scheduleName = name.trim();
375
+ if (!scheduleName) {
376
+ const error = "name is required";
377
+ if (opts.json) {
378
+ writeOutput(cmd, { ok: false, error });
379
+ } else {
380
+ log.error(error);
381
+ }
382
+ process.exitCode = 1;
383
+ return;
384
+ }
385
+
386
+ const body: Record<string, unknown> = {
387
+ name: scheduleName,
388
+ expression: opts.expression,
389
+ message: opts.message,
390
+ enabled: opts.enabled,
391
+ };
392
+ if (opts.timezone != null) body.timezone = opts.timezone;
393
+
394
+ const result = await cliIpcCall<ListSchedulesResponse>(
395
+ "createSchedule",
396
+ { body },
397
+ );
398
+
399
+ if (!result.ok) return exitFromIpcResult(result, cmd);
400
+
401
+ const response = result.result ?? { schedules: [] };
402
+ if (opts.json) {
403
+ writeOutput(cmd, response);
404
+ return;
405
+ }
406
+
407
+ log.info(`Created schedule: ${scheduleName}`);
408
+ },
409
+ );
410
+
411
+ schedules
412
+ .command("enable <id>")
413
+ .description("Enable a schedule")
414
+ .option("--json", "Machine-readable compact JSON output")
415
+ .addHelpText(
416
+ "after",
417
+ `
418
+ Options:
419
+ --json Output the updated schedule list as compact JSON.
420
+
421
+ Arguments:
422
+ <id> Schedule ID (UUID) — run 'assistant schedules list --all' to find it.
423
+
424
+ Behavior:
425
+ Enables the schedule so it can run on future matching times. This does not
426
+ execute the schedule immediately; use 'assistant schedules execute <id>' for
427
+ manual run-now behavior.
428
+
429
+ Examples:
430
+ $ assistant schedules enable 9f2c4f3a-3f1a-41e4-88e7-abc123
431
+ $ assistant schedules enable 9f2c4f3a-3f1a-41e4-88e7-abc123 --json`,
432
+ )
433
+ .action(async (id: string, opts: { json?: boolean }, cmd: Command) => {
434
+ await toggleScheduleEnabled(id, true, opts, cmd);
435
+ });
436
+
437
+ schedules
438
+ .command("disable <id>")
439
+ .description("Disable a schedule")
440
+ .option("--json", "Machine-readable compact JSON output")
441
+ .addHelpText(
442
+ "after",
443
+ `
444
+ Options:
445
+ --json Output the updated schedule list as compact JSON.
446
+
447
+ Arguments:
448
+ <id> Schedule ID (UUID) — run 'assistant schedules list --all' to find it.
449
+
450
+ Behavior:
451
+ Disables the schedule so future scheduled fires are skipped until it is
452
+ enabled again. Existing run history is preserved.
453
+
454
+ Examples:
455
+ $ assistant schedules disable 9f2c4f3a-3f1a-41e4-88e7-abc123
456
+ $ assistant schedules disable 9f2c4f3a-3f1a-41e4-88e7-abc123 --json`,
457
+ )
458
+ .action(async (id: string, opts: { json?: boolean }, cmd: Command) => {
459
+ await toggleScheduleEnabled(id, false, opts, cmd);
460
+ });
461
+
462
+ schedules
463
+ .command("cancel <id>")
464
+ .description("Cancel a pending one-shot schedule")
465
+ .option("--json", "Machine-readable compact JSON output")
466
+ .addHelpText(
467
+ "after",
468
+ `
469
+ Options:
470
+ --json Output the updated schedule list as compact JSON.
471
+
472
+ Arguments:
473
+ <id> Schedule ID (UUID) — run 'assistant schedules list --all' to find
474
+ pending one-shot/deferred schedules.
475
+
476
+ Behavior:
477
+ Cancels a pending one-shot schedule. Recurring schedules are not cancellable;
478
+ use 'assistant schedules disable <id>' to pause recurring schedules.
479
+
480
+ Examples:
481
+ $ assistant schedules cancel 9f2c4f3a-3f1a-41e4-88e7-abc123
482
+ $ assistant schedules cancel 9f2c4f3a-3f1a-41e4-88e7-abc123 --json`,
483
+ )
484
+ .action(async (id: string, opts: { json?: boolean }, cmd: Command) => {
485
+ const scheduleId = id.trim();
486
+ const result = await cliIpcCall<ListSchedulesResponse>(
487
+ "cancelSchedule",
488
+ { pathParams: { id: scheduleId } },
489
+ );
490
+
491
+ if (!result.ok) return exitFromIpcResult(result, cmd);
492
+
493
+ const response = result.result ?? { schedules: [] };
494
+ if (opts.json) {
495
+ writeOutput(cmd, response);
496
+ return;
497
+ }
498
+
499
+ log.info(`Cancelled schedule: ${scheduleId}`);
500
+ });
501
+
502
+ schedules
503
+ .command("delete <id>")
504
+ .description("Permanently delete a schedule and its run history")
505
+ .option("--force", "Skip the confirmation prompt")
506
+ .option("--json", "Machine-readable compact JSON output")
507
+ .addHelpText(
508
+ "after",
509
+ `
510
+ Options:
511
+ --force Skip the destructive y/N confirmation prompt. Required when stdin
512
+ is not a TTY (e.g. in scripts and CI).
513
+ --json Output the updated schedule list as compact JSON.
514
+
515
+ Arguments:
516
+ <id> Schedule ID (UUID) — run 'assistant schedules list --all' to find it.
517
+
518
+ Behavior:
519
+ Permanently removes the schedule and its run history. This cannot be undone.
520
+ To temporarily pause a recurring schedule, use
521
+ 'assistant schedules disable <id>' instead.
522
+
523
+ Examples:
524
+ $ assistant schedules delete 9f2c4f3a-3f1a-41e4-88e7-abc123
525
+ $ assistant schedules delete 9f2c4f3a-3f1a-41e4-88e7-abc123 --force
526
+ $ assistant schedules delete 9f2c4f3a-3f1a-41e4-88e7-abc123 --force --json`,
527
+ )
528
+ .action(
529
+ async (
530
+ id: string,
531
+ opts: { force?: boolean; json?: boolean },
532
+ cmd: Command,
533
+ ) => {
534
+ const scheduleId = id.trim();
535
+ if (!scheduleId) {
536
+ const error = "Schedule ID is required";
537
+ if (opts.json) {
538
+ writeOutput(cmd, { ok: false, error });
539
+ } else {
540
+ log.error(error);
541
+ }
542
+ process.exitCode = 1;
543
+ return;
544
+ }
545
+
546
+ if (!opts.force) {
547
+ const decision = await confirmPrompt({
548
+ question: `Delete schedule "${scheduleId}"? [y/N] `,
549
+ isTTY: Boolean(process.stdin.isTTY),
550
+ refuseNonInteractiveMessage: `Refusing to delete schedule "${scheduleId}" non-interactively. Pass --force to confirm.`,
551
+ });
552
+ if (decision === "non-interactive") {
553
+ process.exitCode = 1;
554
+ return;
555
+ }
556
+ if (decision === "denied") {
557
+ log.info("Delete cancelled.");
558
+ return;
559
+ }
560
+ }
561
+
562
+ const result = await cliIpcCall<ListSchedulesResponse>(
563
+ "deleteSchedule",
564
+ { pathParams: { id: scheduleId } },
565
+ );
566
+
567
+ if (!result.ok) return exitFromIpcResult(result, cmd);
568
+
569
+ const response = result.result ?? { schedules: [] };
570
+ if (opts.json) {
571
+ writeOutput(cmd, response);
572
+ return;
573
+ }
574
+
575
+ log.info(`Deleted schedule: ${scheduleId}`);
576
+ },
577
+ );
578
+
310
579
  schedules
311
580
  .command("execute <id>")
312
581
  .description("Execute a schedule one time immediately")
@@ -368,6 +637,29 @@ Examples:
368
637
  });
369
638
  }
370
639
 
640
+ async function toggleScheduleEnabled(
641
+ id: string,
642
+ enabled: boolean,
643
+ opts: { json?: boolean },
644
+ cmd: Command,
645
+ ): Promise<void> {
646
+ const scheduleId = id.trim();
647
+ const result = await cliIpcCall<ListSchedulesResponse>("toggleSchedule", {
648
+ pathParams: { id: scheduleId },
649
+ body: { enabled },
650
+ });
651
+
652
+ if (!result.ok) return exitFromIpcResult(result, cmd);
653
+
654
+ const response = result.result ?? { schedules: [] };
655
+ if (opts.json) {
656
+ writeOutput(cmd, response);
657
+ return;
658
+ }
659
+
660
+ log.info(`${enabled ? "Enabled" : "Disabled"} schedule: ${scheduleId}`);
661
+ }
662
+
371
663
  function describeSchedule(schedule: ScheduleRecord): string {
372
664
  if (schedule.isOneShot) return "one-shot";
373
665
  const expression = schedule.description ?? schedule.expression ?? "—";
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Tests for {@link searchPlugins}.
3
+ *
4
+ * Network is replaced with an in-memory fixture passed via the `fetch`
5
+ * dependency — no globals are monkey-patched and no `--test-hook` exports
6
+ * leak into production code.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import {
12
+ type FetchLike,
13
+ InvalidSearchPatternError,
14
+ searchPlugins,
15
+ } from "../search-plugins.js";
16
+
17
+ /**
18
+ * Build a GitHub Contents API fixture from an in-memory directory listing.
19
+ *
20
+ * `entries` maps each name under `experimental/plugins/` to its `type`. The
21
+ * fixture answers GET requests against
22
+ * - `https://api.github.com/repos/vellum-ai/vellum-assistant/contents/experimental/plugins...`
23
+ * and returns 500 for anything else (forces test bugs to surface loudly).
24
+ */
25
+ function fixtureFetch(
26
+ entries: Record<string, "dir" | "file" | "symlink" | "submodule">,
27
+ ): FetchLike {
28
+ const PREFIX_API =
29
+ "https://api.github.com/repos/vellum-ai/vellum-assistant/contents/experimental/plugins";
30
+
31
+ return (async (input: RequestInfo | URL) => {
32
+ const url = typeof input === "string" ? input : input.toString();
33
+ if (!url.startsWith(PREFIX_API)) {
34
+ return new Response("unexpected url: " + url, { status: 500 });
35
+ }
36
+ const body = Object.entries(entries).map(([name, type]) => ({
37
+ name,
38
+ path: `experimental/plugins/${name}`,
39
+ type,
40
+ size: type === "file" ? 1 : 0,
41
+ download_url:
42
+ type === "file"
43
+ ? `https://raw.githubusercontent.com/vellum-ai/vellum-assistant/main/experimental/plugins/${name}`
44
+ : null,
45
+ }));
46
+ return new Response(JSON.stringify(body), {
47
+ status: 200,
48
+ headers: { "content-type": "application/json" },
49
+ });
50
+ }) as FetchLike;
51
+ }
52
+
53
+ describe("searchPlugins", () => {
54
+ test("matches the query as a case-insensitive regex against directory names", async () => {
55
+ const result = await searchPlugins(
56
+ { query: "memory" },
57
+ {
58
+ fetch: fixtureFetch({
59
+ "simple-memory": "dir",
60
+ "memory-graph": "dir",
61
+ "git-tools": "dir",
62
+ }),
63
+ },
64
+ );
65
+
66
+ expect(result.matches.map((m) => m.name)).toEqual([
67
+ "memory-graph",
68
+ "simple-memory",
69
+ ]);
70
+ expect(result.matches[0]!.path).toBe("experimental/plugins/memory-graph");
71
+ expect(result.query).toBe("memory");
72
+ expect(result.ref).toBe("main");
73
+ });
74
+
75
+ test("matches regardless of query casing (case-insensitive)", async () => {
76
+ const result = await searchPlugins(
77
+ { query: "MEMORY" },
78
+ { fetch: fixtureFetch({ "simple-memory": "dir" }) },
79
+ );
80
+ expect(result.matches.map((m) => m.name)).toEqual(["simple-memory"]);
81
+ });
82
+
83
+ test("anchored patterns work without escaping", async () => {
84
+ const result = await searchPlugins(
85
+ { query: "^memory-" },
86
+ {
87
+ fetch: fixtureFetch({
88
+ "memory-graph": "dir",
89
+ "simple-memory": "dir",
90
+ }),
91
+ },
92
+ );
93
+ expect(result.matches.map((m) => m.name)).toEqual(["memory-graph"]);
94
+ });
95
+
96
+ test("empty query matches all directories", async () => {
97
+ const result = await searchPlugins(
98
+ { query: "" },
99
+ {
100
+ fetch: fixtureFetch({
101
+ "simple-memory": "dir",
102
+ "memory-graph": "dir",
103
+ "git-tools": "dir",
104
+ }),
105
+ },
106
+ );
107
+ expect(result.matches.map((m) => m.name)).toEqual([
108
+ "git-tools",
109
+ "memory-graph",
110
+ "simple-memory",
111
+ ]);
112
+ });
113
+
114
+ test("skips entries that are not directories", async () => {
115
+ const result = await searchPlugins(
116
+ { query: "" },
117
+ {
118
+ fetch: fixtureFetch({
119
+ "simple-memory": "dir",
120
+ "README.md": "file",
121
+ "broken-symlink": "symlink",
122
+ "old-plugin": "submodule",
123
+ }),
124
+ },
125
+ );
126
+ expect(result.matches.map((m) => m.name)).toEqual(["simple-memory"]);
127
+ });
128
+
129
+ test("rejects invalid regex patterns up front (no network call)", async () => {
130
+ let fetchCalled = false;
131
+ const fetch: FetchLike = (async () => {
132
+ fetchCalled = true;
133
+ return new Response("", { status: 200 });
134
+ }) as FetchLike;
135
+
136
+ await expect(
137
+ searchPlugins({ query: "(unterminated" }, { fetch }),
138
+ ).rejects.toBeInstanceOf(InvalidSearchPatternError);
139
+ expect(fetchCalled).toBe(false);
140
+ });
141
+
142
+ test("empty result set on no matches", async () => {
143
+ const result = await searchPlugins(
144
+ { query: "nothing-matches" },
145
+ { fetch: fixtureFetch({ "simple-memory": "dir" }) },
146
+ );
147
+ expect(result.matches).toEqual([]);
148
+ });
149
+
150
+ test("respects `ref` option by forwarding to GitHub", async () => {
151
+ // The CLI does not surface a `--ref` flag (the source-path convention
152
+ // may change), but the underlying function keeps `ref` for test
153
+ // injection and future internal callers.
154
+ let seenRef: string | undefined;
155
+ const result = await searchPlugins(
156
+ { query: "memory", ref: "feat-branch" },
157
+ {
158
+ fetch: (async (input: RequestInfo | URL) => {
159
+ const url = typeof input === "string" ? input : input.toString();
160
+ const m = /[?&]ref=([^&]+)/.exec(url);
161
+ seenRef = m ? decodeURIComponent(m[1]!) : undefined;
162
+ return new Response(
163
+ JSON.stringify([
164
+ {
165
+ name: "simple-memory",
166
+ path: "experimental/plugins/simple-memory",
167
+ type: "dir",
168
+ size: 0,
169
+ download_url: null,
170
+ },
171
+ ]),
172
+ { status: 200, headers: { "content-type": "application/json" } },
173
+ );
174
+ }) as FetchLike,
175
+ },
176
+ );
177
+
178
+ expect(seenRef).toBe("feat-branch");
179
+ expect(result.ref).toBe("feat-branch");
180
+ expect(result.matches.map((m) => m.name)).toEqual(["simple-memory"]);
181
+ });
182
+
183
+ test("HTTP 5xx from GitHub propagates with the status code", async () => {
184
+ await expect(
185
+ searchPlugins(
186
+ { query: "memory" },
187
+ {
188
+ fetch: (async () =>
189
+ new Response("upstream broken", { status: 503 })) as FetchLike,
190
+ },
191
+ ),
192
+ ).rejects.toThrow(/HTTP 503/);
193
+ });
194
+
195
+ test("HTTP 403 (rate-limited / forbidden) surfaces as an error", async () => {
196
+ await expect(
197
+ searchPlugins(
198
+ { query: "memory" },
199
+ {
200
+ fetch: (async () =>
201
+ new Response("rate limit exceeded", { status: 403 })) as FetchLike,
202
+ },
203
+ ),
204
+ ).rejects.toThrow(/HTTP 403/);
205
+ });
206
+
207
+ test("404 on the plugins prefix surfaces as an error (not silently empty)", async () => {
208
+ // Distinct from `installPlugin`, where 404 on a specific plugin name is
209
+ // normal "not found". For the search, 404 on the prefix means the
210
+ // canonical source path itself is gone — that's an upstream problem
211
+ // worth surfacing, not a clean empty result.
212
+ await expect(
213
+ searchPlugins(
214
+ { query: "memory" },
215
+ {
216
+ fetch: (async () =>
217
+ new Response("not found", { status: 404 })) as FetchLike,
218
+ },
219
+ ),
220
+ ).rejects.toThrow(/HTTP 404/);
221
+ });
222
+
223
+ test("returns matches sorted by name", async () => {
224
+ const result = await searchPlugins(
225
+ { query: "" },
226
+ {
227
+ fetch: fixtureFetch({
228
+ "zeta-plugin": "dir",
229
+ "alpha-plugin": "dir",
230
+ "mu-plugin": "dir",
231
+ }),
232
+ },
233
+ );
234
+ expect(result.matches.map((m) => m.name)).toEqual([
235
+ "alpha-plugin",
236
+ "mu-plugin",
237
+ "zeta-plugin",
238
+ ]);
239
+ });
240
+
241
+ test("sends no Authorization header (canonical source is a public repo)", async () => {
242
+ let seenAuth: string | undefined;
243
+ let seenUserAgent: string | undefined;
244
+ await searchPlugins(
245
+ { query: "memory" },
246
+ {
247
+ fetch: (async (_input: RequestInfo | URL, init?: RequestInit) => {
248
+ const headers = init?.headers as Record<string, string> | undefined;
249
+ seenAuth = headers?.Authorization;
250
+ seenUserAgent = headers?.["User-Agent"];
251
+ return new Response("[]", {
252
+ status: 200,
253
+ headers: { "content-type": "application/json" },
254
+ });
255
+ }) as FetchLike,
256
+ },
257
+ );
258
+ expect(seenAuth).toBeUndefined();
259
+ expect(seenUserAgent).toBe("vellum-assistant-cli");
260
+ });
261
+ });