@vellumai/assistant 0.6.0 → 0.6.1

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 (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -7,7 +7,6 @@ import {
7
7
  getMemorySystemStatus,
8
8
  queryMemory,
9
9
  requestMemoryBackfill,
10
- requestMemoryCleanup,
11
10
  requestMemoryRebuildIndex,
12
11
  requestReextract,
13
12
  } from "../../memory/admin.js";
@@ -106,38 +105,6 @@ Examples:
106
105
  log.info(`Queued backfill job: ${jobId}`);
107
106
  });
108
107
 
109
- memory
110
- .command("cleanup")
111
- .description("Queue cleanup jobs for stale superseded items")
112
- .option(
113
- "--retention-ms <ms>",
114
- "Optional retention threshold in milliseconds"
115
- )
116
- .addHelpText(
117
- "after",
118
- `
119
- Queues a background cleanup job to remove memory items that have been
120
- superseded by newer, corrected facts past the retention threshold.
121
-
122
- The optional --retention-ms flag sets the minimum age (in milliseconds) a
123
- record must have before it is eligible for cleanup. If omitted, the system
124
- default retention period is used.
125
-
126
- Examples:
127
- $ assistant memory cleanup
128
- $ assistant memory cleanup --retention-ms 86400000`
129
- )
130
- .action((opts: { retentionMs?: string }) => {
131
- initializeDb();
132
- const retentionMs = opts.retentionMs
133
- ? Number.parseInt(opts.retentionMs, 10)
134
- : undefined;
135
- requestMemoryCleanup(
136
- Number.isFinite(retentionMs) ? retentionMs : undefined
137
- );
138
- log.info("Memory cleanup requested (legacy items table dropped)");
139
- });
140
-
141
108
  memory
142
109
  .command("cleanup-segments")
143
110
  .description("Remove short segments that waste retrieval budget")
@@ -270,7 +237,7 @@ extraction prompt. This is useful after updating the extraction prompt
270
237
  (e.g. importance scoring rework) to re-score and re-extract memories
271
238
  from historically important conversations.
272
239
 
273
- The command resets extraction checkpoints so the batch extraction handler
240
+ The command resets extraction checkpoints so the graph extraction handler
274
241
  re-processes all messages. Existing memories are provided as supersession
275
242
  context — the new extraction can supersede old flat-fact memories with
276
243
  richer, properly-scored replacements.
@@ -10,6 +10,10 @@ import {
10
10
  NOTIFICATION_SOURCE_EVENT_NAMES,
11
11
  } from "../../notifications/signal.js";
12
12
  import type { NotificationChannel } from "../../notifications/types.js";
13
+ import {
14
+ initAuthSigningKey,
15
+ resolveSigningKey,
16
+ } from "../../runtime/auth/token-service.js";
13
17
  import { initializeDb } from "../db.js";
14
18
  import { log } from "../logger.js";
15
19
  import { shouldOutputJson, writeOutput } from "../output.js";
@@ -159,7 +163,7 @@ Examples:
159
163
  visibleInSourceNow: boolean;
160
164
  deadlineAt?: string;
161
165
  preferredChannels?: string;
162
- conversationId?: string;
166
+ sessionId?: string;
163
167
  dedupeKey?: string;
164
168
  },
165
169
  cmd: Command,
@@ -251,8 +255,9 @@ Examples:
251
255
  }
252
256
 
253
257
  initializeDb();
258
+ initAuthSigningKey(resolveSigningKey());
254
259
 
255
- const sourceContextId = opts.conversationId ?? `cli-${Date.now()}`;
260
+ const sourceContextId = opts.sessionId ?? `cli-${Date.now()}`;
256
261
 
257
262
  const result = await emitNotificationSignal({
258
263
  sourceEventName: opts.sourceEventName,
@@ -2,6 +2,7 @@ import { createServer, type Server } from "node:http";
2
2
 
3
3
  import type { Command } from "commander";
4
4
 
5
+ import { getIsContainerized } from "../../../config/env-registry.js";
5
6
  import { orchestrateOAuthConnect } from "../../../oauth/connect-orchestrator.js";
6
7
  import {
7
8
  getAppByProviderAndClientId,
@@ -178,15 +179,23 @@ Examples:
178
179
 
179
180
  // When opening the browser, start a local server to show a nice
180
181
  // completion page instead of redirecting to the platform website.
182
+ //
183
+ // In containerized mode the loopback server is unreachable from
184
+ // the host browser, so redirect to the platform's own completion
185
+ // page instead.
181
186
  let redirectServer:
182
187
  | { redirectUrl: string; cleanup: () => void }
183
188
  | undefined;
184
189
  if (opts.browser !== false) {
185
- try {
186
- redirectServer = await startManagedRedirectServer(provider);
187
- body.redirect_after_connect = redirectServer.redirectUrl;
188
- } catch {
189
- // Non-fatal fall back to platform default redirect
190
+ if (getIsContainerized()) {
191
+ body.redirect_after_connect = "/account/oauth/desktop-complete";
192
+ } else {
193
+ try {
194
+ redirectServer = await startManagedRedirectServer(provider);
195
+ body.redirect_after_connect = redirectServer.redirectUrl;
196
+ } catch {
197
+ // Non-fatal — fall back to platform default redirect
198
+ }
190
199
  }
191
200
  }
192
201
 
@@ -0,0 +1,396 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+
4
+ import type { Command } from "commander";
5
+
6
+ import { getConfig } from "../../config/loader.js";
7
+ import { getPublicBaseUrl } from "../../inbound/public-ingress-urls.js";
8
+ import { getWorkspaceRoutesDir } from "../../util/platform.js";
9
+ import { log } from "../logger.js";
10
+
11
+ /** HTTP methods that can be exported from a handler module. */
12
+ const HTTP_METHODS = [
13
+ "GET",
14
+ "POST",
15
+ "PUT",
16
+ "PATCH",
17
+ "DELETE",
18
+ "HEAD",
19
+ "OPTIONS",
20
+ ] as const;
21
+
22
+ type HttpMethod = (typeof HTTP_METHODS)[number];
23
+
24
+ /** Supported file extensions for handler modules. */
25
+ const HANDLER_EXTENSIONS = [".ts", ".js"] as const;
26
+
27
+ type HandlerExtension = (typeof HANDLER_EXTENSIONS)[number];
28
+
29
+ interface DiscoveredRoute {
30
+ /** Route path relative to /x/ prefix (e.g. "my-app/status"). */
31
+ routePath: string;
32
+ /** Absolute path to the handler file. */
33
+ filePath: string;
34
+ /** HTTP methods exported by the handler module. */
35
+ methods: HttpMethod[];
36
+ /** Optional description exported by the handler module. */
37
+ description?: string;
38
+ /** File size in bytes. */
39
+ fileSize: number;
40
+ /** Last modified time as ISO string. */
41
+ modifiedAt: string;
42
+ }
43
+
44
+ /**
45
+ * Load a handler module and extract its exported HTTP methods and description.
46
+ */
47
+ async function inspectModule(
48
+ filePath: string,
49
+ ): Promise<{ methods: HttpMethod[]; description?: string }> {
50
+ const stat = statSync(filePath);
51
+ const mod = (await import(`${filePath}?t=${stat.mtimeMs}`)) as Record<
52
+ string,
53
+ unknown
54
+ >;
55
+
56
+ const methods: HttpMethod[] = [];
57
+ for (const method of HTTP_METHODS) {
58
+ if (typeof mod[method] === "function") {
59
+ methods.push(method);
60
+ }
61
+ }
62
+
63
+ const description =
64
+ typeof mod.description === "string" ? mod.description : undefined;
65
+
66
+ return { methods, description };
67
+ }
68
+
69
+ /**
70
+ * Recursively scan the routes directory for handler files (.ts, .js).
71
+ * Returns discovered routes sorted alphabetically by route path.
72
+ */
73
+ async function discoverRoutes(routesDir: string): Promise<DiscoveredRoute[]> {
74
+ if (!existsSync(routesDir)) {
75
+ return [];
76
+ }
77
+
78
+ const routes: DiscoveredRoute[] = [];
79
+
80
+ function scanDir(dir: string): void {
81
+ const entries = readdirSync(dir, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ const fullPath = join(dir, entry.name);
84
+ if (entry.isDirectory()) {
85
+ scanDir(fullPath);
86
+ } else if (entry.isFile()) {
87
+ const ext = HANDLER_EXTENSIONS.find((e) => entry.name.endsWith(e)) as
88
+ | HandlerExtension
89
+ | undefined;
90
+ if (!ext) continue;
91
+
92
+ const relativePath = relative(routesDir, fullPath);
93
+ const withoutExt = relativePath.slice(0, -ext.length);
94
+
95
+ // Convert filesystem path to route path:
96
+ // - Strip /index suffix for index file convention
97
+ // - Replace backslashes with forward slashes (Windows compat)
98
+ let routePath = withoutExt.replace(/\\/g, "/");
99
+ if (routePath.endsWith("/index")) {
100
+ routePath = routePath.slice(0, -"/index".length);
101
+ } else if (routePath === "index") {
102
+ routePath = "";
103
+ }
104
+
105
+ routes.push({
106
+ routePath,
107
+ filePath: fullPath,
108
+ methods: [],
109
+ description: undefined,
110
+ fileSize: 0,
111
+ modifiedAt: "",
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ scanDir(routesDir);
118
+
119
+ // Load each module to detect exported methods and description
120
+ for (const route of routes) {
121
+ try {
122
+ const stat = statSync(route.filePath);
123
+ route.fileSize = stat.size;
124
+ route.modifiedAt = stat.mtime.toISOString();
125
+
126
+ const { methods, description } = await inspectModule(route.filePath);
127
+ route.methods = methods;
128
+ route.description = description;
129
+ } catch {
130
+ // If a module fails to load, keep it in the list with empty methods
131
+ }
132
+ }
133
+
134
+ return routes.sort((a, b) => a.routePath.localeCompare(b.routePath));
135
+ }
136
+
137
+ /**
138
+ * Try to resolve the public base URL for building full endpoint URLs.
139
+ * Returns null if no public URL is configured (non-fatal for CLI display).
140
+ */
141
+ function tryGetPublicBaseUrl(): string | null {
142
+ try {
143
+ const config = getConfig();
144
+ return getPublicBaseUrl(config);
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Format a list of HTTP methods for display, abbreviating DELETE to DEL.
152
+ */
153
+ function formatMethods(methods: HttpMethod[]): string {
154
+ return methods.map((m) => (m === "DELETE" ? "DEL" : m)).join(",");
155
+ }
156
+
157
+ export function registerRoutesCommand(program: Command): void {
158
+ const routes = program
159
+ .command("routes")
160
+ .description("Manage user-defined HTTP route handlers under /x/*");
161
+
162
+ routes.addHelpText(
163
+ "after",
164
+ `
165
+ User-defined routes let you expose custom HTTP endpoints by dropping handler
166
+ files into /workspace/routes/. Each file exports named HTTP method functions
167
+ (GET, POST, etc.) and becomes reachable at /x/<path>.
168
+
169
+ Routes are managed by creating and deleting files — no add/remove commands
170
+ needed.
171
+
172
+ Examples:
173
+ $ assistant routes list
174
+ $ assistant routes list --json
175
+ $ assistant routes inspect my-dashboard-api/submit`,
176
+ );
177
+
178
+ routes
179
+ .command("list")
180
+ .description("List all user-defined route handlers and their public URLs")
181
+ .option("--json", "Machine-readable JSON output")
182
+ .addHelpText(
183
+ "after",
184
+ `
185
+ Scans /workspace/routes/ for handler files (.ts, .js) and displays the route
186
+ path, exported HTTP methods, optional description, and file location.
187
+
188
+ Examples:
189
+ $ assistant routes list
190
+ $ assistant routes list --json`,
191
+ )
192
+ .action(async (opts: { json?: boolean }) => {
193
+ try {
194
+ const routesDir = getWorkspaceRoutesDir();
195
+ const discovered = await discoverRoutes(routesDir);
196
+
197
+ if (opts.json) {
198
+ const publicBase = tryGetPublicBaseUrl();
199
+ const items = discovered.map((r) => ({
200
+ routePath: `/x/${r.routePath}`,
201
+ methods: r.methods,
202
+ description: r.description ?? null,
203
+ filePath: relative(routesDir, r.filePath),
204
+ publicUrl: publicBase ? `${publicBase}/x/${r.routePath}` : null,
205
+ }));
206
+ console.log(JSON.stringify({ ok: true, routes: items }));
207
+ return;
208
+ }
209
+
210
+ if (discovered.length === 0) {
211
+ log.info("No route handlers found in /workspace/routes/.");
212
+ log.info(
213
+ "Create a .ts or .js file exporting named HTTP method functions (GET, POST, etc.).",
214
+ );
215
+ return;
216
+ }
217
+
218
+ const publicBase = tryGetPublicBaseUrl();
219
+
220
+ log.info("");
221
+ // Table header
222
+ const routeCol = "ROUTE PATH";
223
+ const methodsCol = "METHODS";
224
+ const descCol = "DESCRIPTION";
225
+ const fileCol = "FILE";
226
+
227
+ // Calculate column widths
228
+ const routeWidth = Math.max(
229
+ routeCol.length,
230
+ ...discovered.map((r) => `/x/${r.routePath}`.length),
231
+ );
232
+ const methodsWidth = Math.max(
233
+ methodsCol.length,
234
+ ...discovered.map((r) => formatMethods(r.methods).length),
235
+ );
236
+ const descWidth = Math.max(
237
+ descCol.length,
238
+ ...discovered.map((r) => (r.description ?? "").length),
239
+ );
240
+
241
+ const header = [
242
+ routeCol.padEnd(routeWidth),
243
+ methodsCol.padEnd(methodsWidth),
244
+ descCol.padEnd(descWidth),
245
+ fileCol,
246
+ ].join(" ");
247
+
248
+ log.info(` ${header}`);
249
+
250
+ for (const route of discovered) {
251
+ const cols = [
252
+ `/x/${route.routePath}`.padEnd(routeWidth),
253
+ formatMethods(route.methods).padEnd(methodsWidth),
254
+ (route.description ?? "").padEnd(descWidth),
255
+ `routes/${relative(routesDir, route.filePath)}`,
256
+ ].join(" ");
257
+ log.info(` ${cols}`);
258
+ }
259
+
260
+ log.info("");
261
+ const countLabel = discovered.length === 1 ? "route" : "routes";
262
+ const summary = `${discovered.length} ${countLabel}`;
263
+ if (publicBase) {
264
+ log.info(` ${summary} • Public base: ${publicBase}`);
265
+ } else {
266
+ log.info(` ${summary}`);
267
+ }
268
+ log.info("");
269
+ } catch (err) {
270
+ const msg = err instanceof Error ? err.message : String(err);
271
+ if (opts.json) {
272
+ console.log(JSON.stringify({ ok: false, error: msg }));
273
+ } else {
274
+ log.error(`Error: ${msg}`);
275
+ }
276
+ process.exitCode = 1;
277
+ }
278
+ });
279
+
280
+ routes
281
+ .command("inspect <path>")
282
+ .description("Show details of a specific user-defined route handler")
283
+ .option("--json", "Machine-readable JSON output")
284
+ .addHelpText(
285
+ "after",
286
+ `
287
+ Arguments:
288
+ path Route path relative to /x/ (e.g. "my-dashboard-api/submit").
289
+ Do not include the /x/ prefix.
290
+
291
+ Loads the handler file and displays exported methods, description, file path,
292
+ public URL, file size, and last modified time.
293
+
294
+ Examples:
295
+ $ assistant routes inspect my-dashboard-api/submit
296
+ $ assistant routes inspect items --json`,
297
+ )
298
+ .action(async (routePath: string, opts: { json?: boolean }) => {
299
+ try {
300
+ const routesDir = getWorkspaceRoutesDir();
301
+ const filePath = resolveHandlerFile(routesDir, routePath);
302
+
303
+ if (!filePath) {
304
+ const msg = `No handler file found for route path "${routePath}"`;
305
+ if (opts.json) {
306
+ console.log(JSON.stringify({ ok: false, error: msg }));
307
+ } else {
308
+ log.error(msg);
309
+ log.info("Expected file at one of:");
310
+ for (const ext of HANDLER_EXTENSIONS) {
311
+ log.info(` ${join(routesDir, `${routePath}${ext}`)}`);
312
+ log.info(` ${join(routesDir, routePath, `index${ext}`)}`);
313
+ }
314
+ }
315
+ process.exitCode = 1;
316
+ return;
317
+ }
318
+
319
+ const stat = statSync(filePath);
320
+ const { methods, description } = await inspectModule(filePath);
321
+ const publicBase = tryGetPublicBaseUrl();
322
+ const publicUrl = publicBase ? `${publicBase}/x/${routePath}` : null;
323
+
324
+ if (opts.json) {
325
+ console.log(
326
+ JSON.stringify({
327
+ ok: true,
328
+ route: {
329
+ routePath: `/x/${routePath}`,
330
+ methods,
331
+ description: description ?? null,
332
+ filePath,
333
+ publicUrl,
334
+ fileSize: stat.size,
335
+ modifiedAt: stat.mtime.toISOString(),
336
+ },
337
+ }),
338
+ );
339
+ return;
340
+ }
341
+
342
+ log.info("");
343
+ log.info(` Route: /x/${routePath}`);
344
+ log.info(
345
+ ` Methods: ${methods.join(", ") || "(none)"} (detected from named exports)`,
346
+ );
347
+ if (description) {
348
+ log.info(` Description: ${description}`);
349
+ }
350
+ log.info(` File: ${filePath}`);
351
+ if (publicUrl) {
352
+ log.info(` Public URL: ${publicUrl}`);
353
+ }
354
+ log.info(` File Size: ${stat.size} bytes`);
355
+ log.info(` Modified: ${stat.mtime.toISOString()}`);
356
+ log.info("");
357
+ } catch (err) {
358
+ const msg = err instanceof Error ? err.message : String(err);
359
+ if (opts.json) {
360
+ console.log(JSON.stringify({ ok: false, error: msg }));
361
+ } else {
362
+ log.error(`Error: ${msg}`);
363
+ }
364
+ process.exitCode = 1;
365
+ }
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Resolve a route path to a handler file on disk.
371
+ * Mirrors the resolution logic from UserRouteDispatcher.
372
+ */
373
+ function resolveHandlerFile(
374
+ routesDir: string,
375
+ routePath: string,
376
+ ): string | null {
377
+ const basePath = join(routesDir, routePath);
378
+
379
+ // Direct file match
380
+ for (const ext of HANDLER_EXTENSIONS) {
381
+ const candidate = `${basePath}${ext}`;
382
+ if (existsSync(candidate)) {
383
+ return candidate;
384
+ }
385
+ }
386
+
387
+ // Index file convention
388
+ for (const ext of HANDLER_EXTENSIONS) {
389
+ const candidate = join(basePath, `index${ext}`);
390
+ if (existsSync(candidate)) {
391
+ return candidate;
392
+ }
393
+ }
394
+
395
+ return null;
396
+ }