@agent-native/dispatch 0.7.0 → 0.8.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 (235) hide show
  1. package/README.md +56 -3
  2. package/dist/actions/apply-dream-proposal.d.ts +3 -0
  3. package/dist/actions/apply-dream-proposal.d.ts.map +1 -0
  4. package/dist/actions/apply-dream-proposal.js +11 -0
  5. package/dist/actions/apply-dream-proposal.js.map +1 -0
  6. package/dist/actions/create-dream-report.d.ts +3 -0
  7. package/dist/actions/create-dream-report.d.ts.map +1 -0
  8. package/dist/actions/create-dream-report.js +67 -0
  9. package/dist/actions/create-dream-report.js.map +1 -0
  10. package/dist/actions/create-workspace-resource.js +3 -3
  11. package/dist/actions/create-workspace-resource.js.map +1 -1
  12. package/dist/actions/delete-workspace-resource.js +1 -1
  13. package/dist/actions/delete-workspace-resource.js.map +1 -1
  14. package/dist/actions/ensure-dream-job.d.ts +3 -0
  15. package/dist/actions/ensure-dream-job.d.ts.map +1 -0
  16. package/dist/actions/ensure-dream-job.js +73 -0
  17. package/dist/actions/ensure-dream-job.js.map +1 -0
  18. package/dist/actions/get-dream-settings.d.ts +3 -0
  19. package/dist/actions/get-dream-settings.d.ts.map +1 -0
  20. package/dist/actions/get-dream-settings.js +11 -0
  21. package/dist/actions/get-dream-settings.js.map +1 -0
  22. package/dist/actions/get-dream.d.ts +3 -0
  23. package/dist/actions/get-dream.d.ts.map +1 -0
  24. package/dist/actions/get-dream.js +13 -0
  25. package/dist/actions/get-dream.js.map +1 -0
  26. package/dist/actions/get-workspace-resource-effective-context.d.ts +3 -0
  27. package/dist/actions/get-workspace-resource-effective-context.d.ts.map +1 -0
  28. package/dist/actions/get-workspace-resource-effective-context.js +27 -0
  29. package/dist/actions/get-workspace-resource-effective-context.js.map +1 -0
  30. package/dist/actions/index.d.ts.map +1 -1
  31. package/dist/actions/index.js +30 -4
  32. package/dist/actions/index.js.map +1 -1
  33. package/dist/actions/list-dream-candidates.d.ts +3 -0
  34. package/dist/actions/list-dream-candidates.d.ts.map +1 -0
  35. package/dist/actions/list-dream-candidates.js +68 -0
  36. package/dist/actions/list-dream-candidates.js.map +1 -0
  37. package/dist/actions/list-dreams.d.ts +3 -0
  38. package/dist/actions/list-dreams.d.ts.map +1 -0
  39. package/dist/actions/list-dreams.js +17 -0
  40. package/dist/actions/list-dreams.js.map +1 -0
  41. package/dist/actions/list-workspace-resources-for-app.d.ts +3 -0
  42. package/dist/actions/list-workspace-resources-for-app.d.ts.map +1 -0
  43. package/dist/actions/list-workspace-resources-for-app.js +12 -0
  44. package/dist/actions/list-workspace-resources-for-app.js.map +1 -0
  45. package/dist/actions/list-workspace-resources.js +1 -1
  46. package/dist/actions/list-workspace-resources.js.map +1 -1
  47. package/dist/actions/navigate.d.ts +1 -0
  48. package/dist/actions/navigate.d.ts.map +1 -1
  49. package/dist/actions/navigate.js +2 -1
  50. package/dist/actions/navigate.js.map +1 -1
  51. package/dist/actions/preview-dream-proposal.d.ts +3 -0
  52. package/dist/actions/preview-dream-proposal.d.ts.map +1 -0
  53. package/dist/actions/preview-dream-proposal.js +13 -0
  54. package/dist/actions/preview-dream-proposal.js.map +1 -0
  55. package/dist/actions/preview-workspace-resource-change.d.ts +3 -0
  56. package/dist/actions/preview-workspace-resource-change.d.ts.map +1 -0
  57. package/dist/actions/preview-workspace-resource-change.js +24 -0
  58. package/dist/actions/preview-workspace-resource-change.js.map +1 -0
  59. package/dist/actions/reject-dream-proposal.d.ts +3 -0
  60. package/dist/actions/reject-dream-proposal.d.ts.map +1 -0
  61. package/dist/actions/reject-dream-proposal.js +12 -0
  62. package/dist/actions/reject-dream-proposal.js.map +1 -0
  63. package/dist/actions/restore-starter-workspace-resources.d.ts +3 -0
  64. package/dist/actions/restore-starter-workspace-resources.d.ts.map +1 -0
  65. package/dist/actions/restore-starter-workspace-resources.js +14 -0
  66. package/dist/actions/restore-starter-workspace-resources.js.map +1 -0
  67. package/dist/actions/send-code-agent-remote-command.d.ts +3 -0
  68. package/dist/actions/send-code-agent-remote-command.d.ts.map +1 -0
  69. package/dist/actions/send-code-agent-remote-command.js +53 -0
  70. package/dist/actions/send-code-agent-remote-command.js.map +1 -0
  71. package/dist/actions/set-dream-settings.d.ts +3 -0
  72. package/dist/actions/set-dream-settings.d.ts.map +1 -0
  73. package/dist/actions/set-dream-settings.js +41 -0
  74. package/dist/actions/set-dream-settings.js.map +1 -0
  75. package/dist/actions/start-workspace-app-creation.js +1 -1
  76. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  77. package/dist/actions/update-workspace-resource.js +1 -1
  78. package/dist/actions/update-workspace-resource.js.map +1 -1
  79. package/dist/actions/view-screen.d.ts.map +1 -1
  80. package/dist/actions/view-screen.js +73 -2
  81. package/dist/actions/view-screen.js.map +1 -1
  82. package/dist/components/approval-value-block.d.ts +7 -0
  83. package/dist/components/approval-value-block.d.ts.map +1 -0
  84. package/dist/components/approval-value-block.js +22 -0
  85. package/dist/components/approval-value-block.js.map +1 -0
  86. package/dist/components/create-app-popover.d.ts.map +1 -1
  87. package/dist/components/create-app-popover.js +6 -5
  88. package/dist/components/create-app-popover.js.map +1 -1
  89. package/dist/components/layout/Layout.d.ts.map +1 -1
  90. package/dist/components/layout/Layout.js +8 -1
  91. package/dist/components/layout/Layout.js.map +1 -1
  92. package/dist/components/ui/chart.d.ts +1 -1
  93. package/dist/components/workspace-app-card.d.ts.map +1 -1
  94. package/dist/components/workspace-app-card.js +25 -4
  95. package/dist/components/workspace-app-card.js.map +1 -1
  96. package/dist/components/workspace-resource-effective-stack.d.ts +11 -0
  97. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -0
  98. package/dist/components/workspace-resource-effective-stack.js +59 -0
  99. package/dist/components/workspace-resource-effective-stack.js.map +1 -0
  100. package/dist/components/workspace-resource-impact-preview.d.ts +9 -0
  101. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -0
  102. package/dist/components/workspace-resource-impact-preview.js +39 -0
  103. package/dist/components/workspace-resource-impact-preview.js.map +1 -0
  104. package/dist/db/migrations.d.ts.map +1 -1
  105. package/dist/db/migrations.js +59 -0
  106. package/dist/db/migrations.js.map +1 -1
  107. package/dist/db/schema.d.ts +714 -0
  108. package/dist/db/schema.d.ts.map +1 -1
  109. package/dist/db/schema.js +44 -2
  110. package/dist/db/schema.js.map +1 -1
  111. package/dist/hooks/use-navigation-state.d.ts +3 -0
  112. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  113. package/dist/hooks/use-navigation-state.js +23 -3
  114. package/dist/hooks/use-navigation-state.js.map +1 -1
  115. package/dist/lib/utils.d.ts +2 -1
  116. package/dist/lib/utils.d.ts.map +1 -1
  117. package/dist/lib/utils.js +5 -1
  118. package/dist/lib/utils.js.map +1 -1
  119. package/dist/routes/index.d.ts.map +1 -1
  120. package/dist/routes/index.js +1 -0
  121. package/dist/routes/index.js.map +1 -1
  122. package/dist/routes/pages/approval.d.ts.map +1 -1
  123. package/dist/routes/pages/approval.js +4 -1
  124. package/dist/routes/pages/approval.js.map +1 -1
  125. package/dist/routes/pages/approvals.js +1 -1
  126. package/dist/routes/pages/approvals.js.map +1 -1
  127. package/dist/routes/pages/dream-settings.d.ts +34 -0
  128. package/dist/routes/pages/dream-settings.d.ts.map +1 -0
  129. package/dist/routes/pages/dream-settings.js +68 -0
  130. package/dist/routes/pages/dream-settings.js.map +1 -0
  131. package/dist/routes/pages/dreams.d.ts +5 -0
  132. package/dist/routes/pages/dreams.d.ts.map +1 -0
  133. package/dist/routes/pages/dreams.js +435 -0
  134. package/dist/routes/pages/dreams.js.map +1 -0
  135. package/dist/routes/pages/workspace.d.ts.map +1 -1
  136. package/dist/routes/pages/workspace.js +187 -35
  137. package/dist/routes/pages/workspace.js.map +1 -1
  138. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  139. package/dist/server/lib/app-creation-store.js +3 -2
  140. package/dist/server/lib/app-creation-store.js.map +1 -1
  141. package/dist/server/lib/dispatch-integrations.d.ts +1 -1
  142. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  143. package/dist/server/lib/dispatch-integrations.js +9 -4
  144. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  145. package/dist/server/lib/dispatch-remote-commands.d.ts +83 -0
  146. package/dist/server/lib/dispatch-remote-commands.d.ts.map +1 -0
  147. package/dist/server/lib/dispatch-remote-commands.js +256 -0
  148. package/dist/server/lib/dispatch-remote-commands.js.map +1 -0
  149. package/dist/server/lib/dispatch-store.d.ts +26 -0
  150. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  151. package/dist/server/lib/dispatch-store.js +17 -1
  152. package/dist/server/lib/dispatch-store.js.map +1 -1
  153. package/dist/server/lib/dreams-store.d.ts +398 -0
  154. package/dist/server/lib/dreams-store.d.ts.map +1 -0
  155. package/dist/server/lib/dreams-store.js +2330 -0
  156. package/dist/server/lib/dreams-store.js.map +1 -0
  157. package/dist/server/lib/thread-debug-store.d.ts +2 -2
  158. package/dist/server/lib/vault-store.d.ts +1 -1
  159. package/dist/server/lib/workspace-resources-store.d.ts +181 -17
  160. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  161. package/dist/server/lib/workspace-resources-store.js +737 -108
  162. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  163. package/dist/server/plugins/agent-chat.js +1 -1
  164. package/dist/server/plugins/agent-chat.js.map +1 -1
  165. package/dist/server/plugins/integrations.js +2 -2
  166. package/dist/server/plugins/integrations.js.map +1 -1
  167. package/package.json +4 -2
  168. package/src/actions/apply-dream-proposal.ts +12 -0
  169. package/src/actions/create-dream-report.ts +76 -0
  170. package/src/actions/create-workspace-resource.ts +3 -3
  171. package/src/actions/delete-workspace-resource.ts +1 -1
  172. package/src/actions/ensure-dream-job.ts +76 -0
  173. package/src/actions/get-dream-settings.ts +12 -0
  174. package/src/actions/get-dream.ts +14 -0
  175. package/src/actions/get-workspace-resource-effective-context.ts +34 -0
  176. package/src/actions/index.spec.ts +26 -0
  177. package/src/actions/index.ts +31 -4
  178. package/src/actions/list-dream-candidates.ts +77 -0
  179. package/src/actions/list-dreams.ts +17 -0
  180. package/src/actions/list-workspace-resources-for-app.ts +13 -0
  181. package/src/actions/list-workspace-resources.ts +1 -1
  182. package/src/actions/navigate.ts +2 -1
  183. package/src/actions/preview-dream-proposal.ts +14 -0
  184. package/src/actions/preview-workspace-resource-change.ts +25 -0
  185. package/src/actions/reject-dream-proposal.ts +12 -0
  186. package/src/actions/restore-starter-workspace-resources.ts +17 -0
  187. package/src/actions/send-code-agent-remote-command.ts +59 -0
  188. package/src/actions/set-dream-settings.spec.ts +81 -0
  189. package/src/actions/set-dream-settings.ts +44 -0
  190. package/src/actions/start-workspace-app-creation.ts +1 -1
  191. package/src/actions/update-workspace-resource.ts +1 -1
  192. package/src/actions/view-screen.ts +90 -2
  193. package/src/components/approval-value-block.spec.tsx +59 -0
  194. package/src/components/approval-value-block.tsx +33 -0
  195. package/src/components/create-app-popover.tsx +6 -5
  196. package/src/components/layout/Layout.tsx +8 -0
  197. package/src/components/workspace-app-card.tsx +166 -1
  198. package/src/components/workspace-resource-effective-stack.spec.tsx +125 -0
  199. package/src/components/workspace-resource-effective-stack.tsx +141 -0
  200. package/src/components/workspace-resource-impact-preview.spec.tsx +147 -0
  201. package/src/components/workspace-resource-impact-preview.tsx +116 -0
  202. package/src/db/migrations.spec.ts +79 -0
  203. package/src/db/migrations.ts +59 -0
  204. package/src/db/schema.ts +46 -2
  205. package/src/hooks/use-navigation-state.ts +24 -5
  206. package/src/lib/utils.ts +6 -1
  207. package/src/routes/index.ts +1 -0
  208. package/src/routes/pages/approval.tsx +14 -1
  209. package/src/routes/pages/approvals.tsx +1 -1
  210. package/src/routes/pages/dream-settings.spec.ts +130 -0
  211. package/src/routes/pages/dream-settings.ts +103 -0
  212. package/src/routes/pages/dreams.tsx +1828 -0
  213. package/src/routes/pages/workspace.tsx +577 -97
  214. package/src/server/lib/app-creation-store.ts +3 -2
  215. package/src/server/lib/dispatch-integrations.ts +10 -3
  216. package/src/server/lib/dispatch-remote-commands.spec.ts +167 -0
  217. package/src/server/lib/dispatch-remote-commands.ts +375 -0
  218. package/src/server/lib/dispatch-store.ts +37 -1
  219. package/src/server/lib/dreams-store.spec.ts +1492 -0
  220. package/src/server/lib/dreams-store.ts +3168 -0
  221. package/src/server/lib/workspace-resource-approval-lifecycle.spec.ts +226 -0
  222. package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
  223. package/src/server/lib/workspace-resources-store.ts +1001 -134
  224. package/src/server/plugins/agent-chat.ts +1 -1
  225. package/src/server/plugins/integrations.ts +2 -2
  226. package/dist/actions/sync-workspace-resources-to-all.d.ts +0 -3
  227. package/dist/actions/sync-workspace-resources-to-all.d.ts.map +0 -1
  228. package/dist/actions/sync-workspace-resources-to-all.js +0 -9
  229. package/dist/actions/sync-workspace-resources-to-all.js.map +0 -1
  230. package/dist/actions/sync-workspace-resources-to-app.d.ts +0 -3
  231. package/dist/actions/sync-workspace-resources-to-app.d.ts.map +0 -1
  232. package/dist/actions/sync-workspace-resources-to-app.js +0 -11
  233. package/dist/actions/sync-workspace-resources-to-app.js.map +0 -1
  234. package/src/actions/sync-workspace-resources-to-all.ts +0 -10
  235. package/src/actions/sync-workspace-resources-to-app.ts +0 -12
@@ -1564,6 +1564,7 @@ function buildWorkspaceAppPrompt(input: {
1564
1564
  ? `Dispatch vault keys selected for this app: ${selectedKeys.join(", ")}`
1565
1565
  : "Dispatch vault keys selected for this app: none",
1566
1566
  `Dispatch workspace resources selected for this app:\n${resourceList}`,
1567
+ `Dispatch workspace resources with scope=all are global. After the app exists, sync workspace resources to appId "${appId}" so global skills, guardrail instructions, and reference resources reach the new app even when no per-app resources were selected.`,
1567
1568
  "",
1568
1569
  `Use the workspace app layout: create it under apps/${appId}, mount it at /${appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
1569
1570
  `Important routing rule: from outside the app, link to /${appId}; inside apps/${appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${appId}/${appId}/review.`,
@@ -1574,8 +1575,8 @@ function buildWorkspaceAppPrompt(input: {
1574
1575
  ? `Dispatch will create pending vault requests for the selected keys for appId "${appId}" after this app creation request is accepted. Do not grant or sync vault keys directly from the app-creation branch.`
1575
1576
  : "Do not grant or request any Dispatch vault keys unless the user asks later.",
1576
1577
  selectedResources.length
1577
- ? `Dispatch will create workspace resource grants for the selected resources for appId "${appId}". After the app exists, sync workspace resources so the app receives those shared resources. Add a short note to apps/${appId}/AGENTS.md telling the app agent to read relevant shared resources under context/ or the selected resource paths before doing GTM/domain work.`
1578
- : "Do not grant any Dispatch workspace resources unless the user asks later.",
1578
+ ? `Dispatch will create workspace resource grants for the selected resources for appId "${appId}". After the app exists, sync workspace resources so the app receives both global and selected shared resources.`
1579
+ : "Do not grant any selected-only Dispatch workspace resources unless the user asks later.",
1579
1580
  "",
1580
1581
  "Agent-native rules (these are the framework's contract — not optional):",
1581
1582
  `- Persist ALL data in SQL via Drizzle. Add tables to apps/${appId}/server/db/schema.ts and migrations to apps/${appId}/server/plugins/db.ts. NEVER use localStorage, sessionStorage, IndexedDB, or in-memory state for anything the user expects to persist — agent and UI must read the same source of truth.`,
@@ -5,6 +5,7 @@ import type {
5
5
  import { resolveOrgIdForEmail } from "@agent-native/core/org";
6
6
  import crypto from "node:crypto";
7
7
  import { consumeLinkToken, resolveLinkedOwner } from "./dispatch-store.js";
8
+ import { handleRemoteCodeCommand } from "./dispatch-remote-commands.js";
8
9
 
9
10
  type SlackSenderProfile = {
10
11
  email: string | null;
@@ -195,11 +196,17 @@ export async function resolveDispatchOwner(
195
196
 
196
197
  export async function beforeDispatchProcess(
197
198
  incoming: IncomingMessage,
198
- _adapter: PlatformAdapter,
199
+ adapter: PlatformAdapter,
199
200
  ): Promise<{ handled: true; responseText?: string } | { handled: false }> {
200
201
  const trimmed = incoming.text.trim();
201
- const match = trimmed.match(/^\/link\s+([a-zA-Z0-9_-]+)$/);
202
- if (!match) return { handled: false };
202
+ const commandText =
203
+ contextString(incoming.platformContext.rawText) || trimmed;
204
+ const match = commandText.match(/^\/link(?:@\w+)?\s+([a-zA-Z0-9_-]+)$/);
205
+ if (!match) {
206
+ return handleRemoteCodeCommand(incoming, adapter, {
207
+ resolveOwner: () => resolveDispatchOwner(incoming),
208
+ });
209
+ }
203
210
 
204
211
  try {
205
212
  const owner = await consumeLinkToken({
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type {
3
+ IncomingMessage,
4
+ PlatformAdapter,
5
+ } from "@agent-native/core/server";
6
+ import {
7
+ handleRemoteCodeCommand,
8
+ parseTelegramCodeCommand,
9
+ type RemoteCodeCommandEnvelope,
10
+ } from "./dispatch-remote-commands.js";
11
+
12
+ function telegramIncoming(text: string, rawText = text): IncomingMessage {
13
+ return {
14
+ platform: "telegram",
15
+ externalThreadId: "chat-123",
16
+ text,
17
+ senderId: "user-1",
18
+ senderName: "User One",
19
+ platformContext: { chatId: 123, messageId: 456, rawText },
20
+ timestamp: 1710000000000,
21
+ };
22
+ }
23
+
24
+ const adapter = {
25
+ platform: "telegram",
26
+ label: "Telegram",
27
+ } as PlatformAdapter;
28
+
29
+ describe("parseTelegramCodeCommand", () => {
30
+ it("parses a prompt as a create command", () => {
31
+ expect(
32
+ parseTelegramCodeCommand(
33
+ telegramIncoming(
34
+ "fix the failing tests",
35
+ "/code fix the failing tests",
36
+ ),
37
+ ),
38
+ ).toEqual({ type: "create", prompt: "fix the failing tests" });
39
+ });
40
+
41
+ it("parses run management commands", () => {
42
+ expect(
43
+ parseTelegramCodeCommand(telegramIncoming("list", "/code list")),
44
+ ).toEqual({
45
+ type: "list",
46
+ });
47
+ expect(
48
+ parseTelegramCodeCommand(telegramIncoming("status 2", "/code status 2")),
49
+ ).toEqual({ type: "status", runRef: "2" });
50
+ expect(
51
+ parseTelegramCodeCommand(
52
+ telegramIncoming(
53
+ "continue run_123 add docs",
54
+ "/code continue run_123 add docs",
55
+ ),
56
+ ),
57
+ ).toEqual({ type: "continue", runRef: "run_123", text: "add docs" });
58
+ expect(
59
+ parseTelegramCodeCommand(
60
+ telegramIncoming("approve req_1", "/code approve req_1"),
61
+ ),
62
+ ).toEqual({ type: "approve", approvalId: "req_1" });
63
+ expect(
64
+ parseTelegramCodeCommand(
65
+ telegramIncoming("deny req_1", "/code deny req_1"),
66
+ ),
67
+ ).toEqual({ type: "deny", approvalId: "req_1" });
68
+ expect(
69
+ parseTelegramCodeCommand(telegramIncoming("stop 1", "/code stop 1")),
70
+ ).toEqual({
71
+ type: "stop",
72
+ runRef: "1",
73
+ });
74
+ });
75
+
76
+ it("does not hijack Telegram messages after the adapter strips other commands", () => {
77
+ expect(
78
+ parseTelegramCodeCommand({
79
+ platform: "telegram",
80
+ text: "list",
81
+ platformContext: {},
82
+ }),
83
+ ).toBeNull();
84
+ });
85
+ });
86
+
87
+ describe("handleRemoteCodeCommand", () => {
88
+ it("routes code commands to the remote relay with owner and source context", async () => {
89
+ const relay = vi.fn(async () => ({
90
+ ok: true,
91
+ runId: "run_123",
92
+ hostOnline: true,
93
+ }));
94
+
95
+ const result = await handleRemoteCodeCommand(
96
+ telegramIncoming("ship it", "/code ship it"),
97
+ adapter,
98
+ {
99
+ resolveOwner: () => "owner@example.test",
100
+ relay,
101
+ },
102
+ );
103
+
104
+ expect(result).toEqual({
105
+ handled: true,
106
+ responseText: "Queued code run (run_123).",
107
+ });
108
+ expect(relay).toHaveBeenCalledWith({
109
+ kind: "code-agent",
110
+ ownerEmail: "owner@example.test",
111
+ command: { type: "create", prompt: "ship it" },
112
+ source: {
113
+ platform: "telegram",
114
+ externalThreadId: "chat-123",
115
+ senderId: "user-1",
116
+ senderName: "User One",
117
+ messageId: "456",
118
+ timestamp: 1710000000000,
119
+ },
120
+ } satisfies RemoteCodeCommandEnvelope);
121
+ });
122
+
123
+ it("keeps offline hosts pending without pretending the run is active", async () => {
124
+ const result = await handleRemoteCodeCommand(
125
+ telegramIncoming("fix it", "/code fix it"),
126
+ adapter,
127
+ {
128
+ resolveOwner: () => "owner@example.test",
129
+ relay: async () => ({
130
+ ok: true,
131
+ commandId: "cmd_123",
132
+ hostOnline: false,
133
+ hostStatus: "asleep",
134
+ }),
135
+ },
136
+ );
137
+
138
+ expect(result).toEqual({
139
+ handled: true,
140
+ responseText:
141
+ "Queued code run (cmd_123). Your computer looks offline or asleep, so it will pick this up when it wakes.",
142
+ });
143
+ });
144
+
145
+ it("formats recent run lists compactly", async () => {
146
+ const result = await handleRemoteCodeCommand(
147
+ telegramIncoming("list", "/code list"),
148
+ adapter,
149
+ {
150
+ resolveOwner: () => "owner@example.test",
151
+ relay: async () => ({
152
+ ok: true,
153
+ runs: [
154
+ { id: "run_a", title: "Fix auth", status: "running" },
155
+ { id: "run_b", title: "Add docs", status: "completed" },
156
+ ],
157
+ }),
158
+ },
159
+ );
160
+
161
+ expect(result).toEqual({
162
+ handled: true,
163
+ responseText:
164
+ "Recent code-agent runs:\n1. Fix auth — running (run_a)\n2. Add docs — completed (run_b)",
165
+ });
166
+ });
167
+ });
@@ -0,0 +1,375 @@
1
+ import type {
2
+ IncomingMessage,
3
+ PlatformAdapter,
4
+ } from "@agent-native/core/server";
5
+
6
+ export type RemoteCodeCommand =
7
+ | { type: "create"; prompt: string }
8
+ | { type: "list" }
9
+ | { type: "status"; runRef?: string }
10
+ | { type: "continue"; runRef: string; text: string }
11
+ | { type: "approve"; approvalId: string }
12
+ | { type: "deny"; approvalId: string }
13
+ | { type: "stop"; runRef: string }
14
+ | { type: "help"; reason?: string };
15
+
16
+ export interface RemoteCodeCommandEnvelope {
17
+ kind: "code-agent";
18
+ ownerEmail: string;
19
+ command: Exclude<RemoteCodeCommand, { type: "help" }>;
20
+ source: {
21
+ platform: string;
22
+ externalThreadId: string;
23
+ senderId?: string;
24
+ senderName?: string;
25
+ messageId?: string;
26
+ timestamp: number;
27
+ };
28
+ }
29
+
30
+ export interface RemoteCodeRunSummary {
31
+ id?: string;
32
+ runId?: string;
33
+ title?: string;
34
+ prompt?: string;
35
+ status?: string;
36
+ updatedAt?: string | number | Date;
37
+ createdAt?: string | number | Date;
38
+ }
39
+
40
+ export interface RemoteCodeCommandResult {
41
+ ok?: boolean;
42
+ status?: string;
43
+ hostOnline?: boolean;
44
+ hostStatus?: string;
45
+ commandId?: string;
46
+ requestId?: string;
47
+ runId?: string;
48
+ run?: RemoteCodeRunSummary;
49
+ runs?: RemoteCodeRunSummary[];
50
+ message?: string;
51
+ error?: string;
52
+ }
53
+
54
+ export type RemoteCodeCommandRelay = (
55
+ envelope: RemoteCodeCommandEnvelope,
56
+ ) => Promise<RemoteCodeCommandResult>;
57
+
58
+ export interface HandleRemoteCodeCommandOptions {
59
+ resolveOwner: () => Promise<string> | string;
60
+ relay?: RemoteCodeCommandRelay;
61
+ }
62
+
63
+ const CODE_COMMAND_RE = /^\/code(?:@[a-zA-Z0-9_]+)?(?:\s+|$)/i;
64
+
65
+ export function parseTelegramCodeCommand(
66
+ incoming: Pick<IncomingMessage, "platform" | "text" | "platformContext">,
67
+ ): RemoteCodeCommand | null {
68
+ if (incoming.platform !== "telegram") return null;
69
+
70
+ const rawText = rawTelegramText(incoming);
71
+ if (!rawText || !CODE_COMMAND_RE.test(rawText)) return null;
72
+
73
+ return parseCodeCommandBody(rawText.replace(CODE_COMMAND_RE, "").trim());
74
+ }
75
+
76
+ export async function handleRemoteCodeCommand(
77
+ incoming: IncomingMessage,
78
+ _adapter: PlatformAdapter,
79
+ options: HandleRemoteCodeCommandOptions,
80
+ ): Promise<{ handled: true; responseText?: string } | { handled: false }> {
81
+ const command = parseTelegramCodeCommand(incoming);
82
+ if (!command) return { handled: false };
83
+
84
+ if (command.type === "help") {
85
+ return { handled: true, responseText: formatCodeCommandHelp(command) };
86
+ }
87
+
88
+ try {
89
+ const ownerEmail = await options.resolveOwner();
90
+ const envelope = createRemoteCodeCommandEnvelope(
91
+ incoming,
92
+ ownerEmail,
93
+ command,
94
+ );
95
+ const relay = options.relay ?? enqueueRemoteCodeCommand;
96
+ const result = await relay(envelope);
97
+ return {
98
+ handled: true,
99
+ responseText: formatRemoteCodeCommandResult(command, result),
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ handled: true,
104
+ responseText:
105
+ error instanceof Error
106
+ ? `I couldn't route that code command: ${error.message}`
107
+ : "I couldn't route that code command.",
108
+ };
109
+ }
110
+ }
111
+
112
+ export function createRemoteCodeCommandEnvelope(
113
+ incoming: IncomingMessage,
114
+ ownerEmail: string,
115
+ command: Exclude<RemoteCodeCommand, { type: "help" }>,
116
+ ): RemoteCodeCommandEnvelope {
117
+ return {
118
+ kind: "code-agent",
119
+ ownerEmail,
120
+ command,
121
+ source: {
122
+ platform: incoming.platform,
123
+ externalThreadId: incoming.externalThreadId,
124
+ senderId: incoming.senderId,
125
+ senderName: incoming.senderName,
126
+ messageId: contextString(incoming.platformContext.messageId),
127
+ timestamp: incoming.timestamp,
128
+ },
129
+ };
130
+ }
131
+
132
+ export async function enqueueRemoteCodeCommand(
133
+ envelope: RemoteCodeCommandEnvelope,
134
+ ): Promise<RemoteCodeCommandResult> {
135
+ const helperResult = await tryCoreRemoteCommandHelper(envelope);
136
+ if (helperResult) return helperResult;
137
+
138
+ const endpoint = `${resolveRemoteRelayBaseUrl()}/_agent-native/integrations/remote/enqueue`;
139
+ const response = await fetch(endpoint, {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json" },
142
+ body: JSON.stringify(envelope),
143
+ });
144
+
145
+ let body: unknown = null;
146
+ try {
147
+ body = await response.json();
148
+ } catch {
149
+ body = null;
150
+ }
151
+
152
+ if (!response.ok) {
153
+ const message =
154
+ typeof body === "object" && body && "error" in body
155
+ ? String((body as { error?: unknown }).error)
156
+ : `remote relay returned ${response.status}`;
157
+ throw new Error(message);
158
+ }
159
+
160
+ return normalizeRemoteCodeCommandResult(body);
161
+ }
162
+
163
+ export function formatRemoteCodeCommandResult(
164
+ command: Exclude<RemoteCodeCommand, { type: "help" }>,
165
+ result: RemoteCodeCommandResult,
166
+ ): string {
167
+ if (result.message?.trim()) return result.message.trim();
168
+ if (result.error?.trim())
169
+ return `Code command failed: ${result.error.trim()}`;
170
+
171
+ if (command.type === "list") return formatRunList(result.runs ?? []);
172
+ if (command.type === "status") return formatStatus(command, result);
173
+
174
+ const id =
175
+ result.runId ||
176
+ result.run?.runId ||
177
+ result.run?.id ||
178
+ result.commandId ||
179
+ result.requestId;
180
+ const suffix = id ? ` (${id})` : "";
181
+ const offline = isOfflineOrSleeping(result);
182
+
183
+ if (command.type === "create") {
184
+ return offline
185
+ ? `Queued code run${suffix}. Your computer looks offline or asleep, so it will pick this up when it wakes.`
186
+ : `Queued code run${suffix}.`;
187
+ }
188
+
189
+ if (command.type === "continue") {
190
+ return offline
191
+ ? `Queued follow-up for ${command.runRef}. Your computer looks offline or asleep, so it will pick this up when it wakes.`
192
+ : `Queued follow-up for ${command.runRef}.`;
193
+ }
194
+
195
+ if (command.type === "approve") {
196
+ return `Approved code-agent request ${command.approvalId}.`;
197
+ }
198
+
199
+ if (command.type === "deny") {
200
+ return `Denied code-agent request ${command.approvalId}.`;
201
+ }
202
+
203
+ if (command.type === "stop") {
204
+ return offline
205
+ ? `Queued stop request for ${command.runRef}. Your computer looks offline or asleep, so it will receive the stop request when it wakes.`
206
+ : `Stop requested for ${command.runRef}.`;
207
+ }
208
+
209
+ return "Code command routed.";
210
+ }
211
+
212
+ function parseCodeCommandBody(body: string): RemoteCodeCommand {
213
+ if (!body) return { type: "help" };
214
+
215
+ const [verbRaw = "", ...restParts] = body.split(/\s+/);
216
+ const verb = verbRaw.toLowerCase();
217
+ const rest = restParts.join(" ").trim();
218
+
219
+ if (verb === "help") return { type: "help" };
220
+ if (verb === "list") return { type: "list" };
221
+ if (verb === "status") {
222
+ return rest ? { type: "status", runRef: rest } : { type: "status" };
223
+ }
224
+ if (verb === "continue") {
225
+ const { first, rest: text } = splitFirst(rest);
226
+ if (!first || !text) {
227
+ return { type: "help", reason: "continue needs a run id and text" };
228
+ }
229
+ return { type: "continue", runRef: first, text };
230
+ }
231
+ if (verb === "approve") {
232
+ return rest
233
+ ? { type: "approve", approvalId: rest }
234
+ : { type: "help", reason: "approve needs a request id" };
235
+ }
236
+ if (verb === "deny") {
237
+ return rest
238
+ ? { type: "deny", approvalId: rest }
239
+ : { type: "help", reason: "deny needs a request id" };
240
+ }
241
+ if (verb === "stop") {
242
+ return rest
243
+ ? { type: "stop", runRef: rest }
244
+ : { type: "help", reason: "stop needs a run id or list index" };
245
+ }
246
+
247
+ return { type: "create", prompt: body };
248
+ }
249
+
250
+ function rawTelegramText(
251
+ incoming: Pick<IncomingMessage, "text" | "platformContext">,
252
+ ): string | null {
253
+ const context = incoming.platformContext;
254
+ return (
255
+ contextString(context.rawText) ||
256
+ contextString(context.originalText) ||
257
+ contextString(context.messageText) ||
258
+ (CODE_COMMAND_RE.test(incoming.text) ? incoming.text : null)
259
+ );
260
+ }
261
+
262
+ function formatCodeCommandHelp(
263
+ command?: Extract<RemoteCodeCommand, { type: "help" }>,
264
+ ): string {
265
+ const prefix = command?.reason ? `${command.reason}.\n\n` : "";
266
+ return `${prefix}Use /code <prompt>, /code list, /code status [run], /code continue <run> <text>, /code approve <id>, /code deny <id>, or /code stop <run>.`;
267
+ }
268
+
269
+ function formatRunList(runs: RemoteCodeRunSummary[]): string {
270
+ if (!runs.length) return "No recent code-agent runs found.";
271
+ const lines = runs.slice(0, 8).map((run, index) => {
272
+ const id = run.runId || run.id || "unknown";
273
+ const title = run.title || run.prompt || "Untitled run";
274
+ const status = run.status || "unknown";
275
+ return `${index + 1}. ${title} — ${status} (${id})`;
276
+ });
277
+ return `Recent code-agent runs:\n${lines.join("\n")}`;
278
+ }
279
+
280
+ function formatStatus(
281
+ command: Extract<RemoteCodeCommand, { type: "status" }>,
282
+ result: RemoteCodeCommandResult,
283
+ ): string {
284
+ const run = result.run;
285
+ const hostStatus =
286
+ result.hostStatus || (result.hostOnline ? "online" : "offline");
287
+ if (!run) {
288
+ const target = command.runRef ? ` for ${command.runRef}` : "";
289
+ return `Code-agent host is ${hostStatus}${target}.`;
290
+ }
291
+
292
+ const id = run.runId || run.id || command.runRef || "unknown";
293
+ const title = run.title || run.prompt || "Untitled run";
294
+ const status = run.status || result.status || "unknown";
295
+ const updated = formatDate(run.updatedAt || run.createdAt);
296
+ return [
297
+ `Code run ${id}: ${status}`,
298
+ `Task: ${title}`,
299
+ `Host: ${hostStatus}`,
300
+ updated ? `Updated: ${updated}` : "",
301
+ ]
302
+ .filter(Boolean)
303
+ .join("\n");
304
+ }
305
+
306
+ function isOfflineOrSleeping(result: RemoteCodeCommandResult): boolean {
307
+ if (result.hostOnline === false) return true;
308
+ const status = result.hostStatus?.toLowerCase();
309
+ return status === "offline" || status === "asleep" || status === "sleeping";
310
+ }
311
+
312
+ function splitFirst(value: string): { first: string; rest: string } {
313
+ const trimmed = value.trim();
314
+ const match = trimmed.match(/^(\S+)(?:\s+([\s\S]+))?$/);
315
+ return { first: match?.[1] || "", rest: match?.[2]?.trim() || "" };
316
+ }
317
+
318
+ function contextString(value: unknown): string | undefined {
319
+ if (typeof value === "string" && value.trim()) return value.trim();
320
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
321
+ return undefined;
322
+ }
323
+
324
+ function formatDate(value: RemoteCodeRunSummary["updatedAt"]): string | null {
325
+ if (!value) return null;
326
+ const date = value instanceof Date ? value : new Date(value);
327
+ if (Number.isNaN(date.getTime())) return null;
328
+ return date.toISOString();
329
+ }
330
+
331
+ async function tryCoreRemoteCommandHelper(
332
+ envelope: RemoteCodeCommandEnvelope,
333
+ ): Promise<RemoteCodeCommandResult | null> {
334
+ const core = (await import("@agent-native/core/server")) as Record<
335
+ string,
336
+ unknown
337
+ >;
338
+ const helper =
339
+ core.enqueueRemoteCommand ||
340
+ core.enqueueIntegrationRemoteCommand ||
341
+ core.enqueueRemoteIntegrationCommand;
342
+ if (typeof helper !== "function") return null;
343
+ return normalizeRemoteCodeCommandResult(await helper(envelope));
344
+ }
345
+
346
+ function normalizeRemoteCodeCommandResult(
347
+ value: unknown,
348
+ ): RemoteCodeCommandResult {
349
+ if (!value || typeof value !== "object") return { ok: true };
350
+ const result = value as RemoteCodeCommandResult;
351
+ return {
352
+ ok: result.ok,
353
+ status: contextString(result.status),
354
+ hostOnline:
355
+ typeof result.hostOnline === "boolean" ? result.hostOnline : undefined,
356
+ hostStatus: contextString(result.hostStatus),
357
+ commandId: contextString(result.commandId),
358
+ requestId: contextString(result.requestId),
359
+ runId: contextString(result.runId),
360
+ run: result.run,
361
+ runs: Array.isArray(result.runs) ? result.runs : undefined,
362
+ message: contextString(result.message),
363
+ error: contextString(result.error),
364
+ };
365
+ }
366
+
367
+ function resolveRemoteRelayBaseUrl(): string {
368
+ const raw =
369
+ process.env.WEBHOOK_BASE_URL ||
370
+ process.env.APP_URL ||
371
+ process.env.URL ||
372
+ "http://localhost:3000";
373
+ if (/^https?:\/\//i.test(raw)) return raw.replace(/\/$/, "");
374
+ return `https://${raw.replace(/\/$/, "")}`;
375
+ }
@@ -423,7 +423,7 @@ async function notifyApprovers(requestId: string, summary: string) {
423
423
  }).catch(() => {});
424
424
  }
425
425
 
426
- async function createApprovalRequest(input: {
426
+ export async function createApprovalRequest(input: {
427
427
  changeType: string;
428
428
  targetType: string;
429
429
  targetId?: string | null;
@@ -564,6 +564,42 @@ async function applyApprovedRequest(request: DispatchApprovalRequest) {
564
564
  request.reviewedBy || currentOwnerEmail(),
565
565
  );
566
566
  }
567
+ if (request.changeType === "dream-proposal.apply") {
568
+ const { applyApprovedDreamProposal } = await import("./dreams-store.js");
569
+ return applyApprovedDreamProposal(
570
+ payload.proposalId,
571
+ request.reviewedBy || currentOwnerEmail(),
572
+ requestCtx,
573
+ );
574
+ }
575
+ if (request.changeType === "workspace-resource.create") {
576
+ const { applyWorkspaceResourceCreate } =
577
+ await import("./workspace-resources-store.js");
578
+ return applyWorkspaceResourceCreate(
579
+ payload.input,
580
+ request.reviewedBy || currentOwnerEmail(),
581
+ requestCtx,
582
+ );
583
+ }
584
+ if (request.changeType === "workspace-resource.update") {
585
+ const { applyWorkspaceResourceUpdate } =
586
+ await import("./workspace-resources-store.js");
587
+ return applyWorkspaceResourceUpdate(
588
+ payload.id,
589
+ payload.input,
590
+ request.reviewedBy || currentOwnerEmail(),
591
+ requestCtx,
592
+ );
593
+ }
594
+ if (request.changeType === "workspace-resource.delete") {
595
+ const { applyWorkspaceResourceDelete } =
596
+ await import("./workspace-resources-store.js");
597
+ return applyWorkspaceResourceDelete(
598
+ payload.id,
599
+ request.reviewedBy || currentOwnerEmail(),
600
+ requestCtx,
601
+ );
602
+ }
567
603
  throw new Error(`Unsupported approval request type: ${request.changeType}`);
568
604
  }
569
605