@agent-native/core 0.114.5 → 0.114.7

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 (210) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/client/resources/ResourceTree.tsx +11 -5
  11. package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
  12. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  13. package/corpus/core/src/mcp-client/index.ts +9 -0
  14. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  15. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  16. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  17. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  18. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  19. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  20. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  21. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  22. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  23. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  24. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  25. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  26. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  27. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  28. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  29. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +1 -2
  30. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  31. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  32. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  33. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  34. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  35. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  47. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  51. package/corpus/templates/clips/desktop/package.json +1 -0
  52. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  53. package/corpus/templates/clips/desktop/src/app.tsx +2478 -223
  54. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  55. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  56. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  57. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  58. package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
  59. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  60. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  61. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  62. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  63. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  64. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
  65. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  66. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  67. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  68. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  69. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  70. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  71. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  72. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  73. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  74. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  76. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  77. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  78. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  79. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  80. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  82. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  83. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  89. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  91. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  92. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  93. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  94. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  96. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  97. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  98. package/corpus/templates/clips/shared/meeting-join-url.ts +0 -5
  99. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
  100. package/corpus/templates/content/AGENTS.md +4 -1
  101. package/corpus/templates/content/actions/_content-spaces.ts +6 -1
  102. package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
  103. package/corpus/templates/content/actions/_database-utils.ts +83 -15
  104. package/corpus/templates/content/actions/add-database-item.ts +3 -0
  105. package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
  106. package/corpus/templates/content/actions/delete-content-database.ts +12 -9
  107. package/corpus/templates/content/actions/delete-database-items.ts +3 -2
  108. package/corpus/templates/content/actions/delete-document.ts +233 -6
  109. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
  110. package/corpus/templates/content/actions/get-document.ts +4 -1
  111. package/corpus/templates/content/actions/list-content-databases.ts +1 -0
  112. package/corpus/templates/content/actions/list-documents.ts +1 -0
  113. package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
  114. package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
  115. package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
  116. package/corpus/templates/content/actions/pull-document.ts +15 -2
  117. package/corpus/templates/content/actions/restore-content-database.ts +48 -11
  118. package/corpus/templates/content/actions/restore-document.ts +28 -0
  119. package/corpus/templates/content/actions/search-documents.ts +2 -1
  120. package/corpus/templates/content/actions/view-screen.ts +2 -1
  121. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
  122. package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
  123. package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
  124. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
  125. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
  126. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
  127. package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
  128. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
  129. package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
  130. package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
  131. package/corpus/templates/content/app/i18n-data.ts +83 -2
  132. package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
  133. package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
  134. package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
  135. package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
  136. package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
  137. package/corpus/templates/content/parity/matrix.md +1 -1
  138. package/corpus/templates/content/parity/matrix.ts +3 -0
  139. package/corpus/templates/content/server/db/schema.ts +2 -0
  140. package/corpus/templates/content/server/lib/public-documents.ts +8 -2
  141. package/corpus/templates/content/server/plugins/db.ts +52 -0
  142. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
  143. package/corpus/templates/content/shared/api.ts +10 -0
  144. package/corpus/toolkit/CHANGELOG.md +6 -0
  145. package/corpus/toolkit/package.json +1 -1
  146. package/corpus/toolkit/src/ui/progress.tsx +3 -2
  147. package/dist/cli/mcp-config-writers.d.ts +2 -2
  148. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  149. package/dist/cli/mcp-config-writers.js +6 -6
  150. package/dist/cli/mcp-config-writers.js.map +1 -1
  151. package/dist/cli/mcp.d.ts +14 -0
  152. package/dist/cli/mcp.d.ts.map +1 -1
  153. package/dist/cli/mcp.js +79 -0
  154. package/dist/cli/mcp.js.map +1 -1
  155. package/dist/cli/skills-content/help.d.ts +1 -1
  156. package/dist/cli/skills-content/help.d.ts.map +1 -1
  157. package/dist/cli/skills-content/help.js +4 -3
  158. package/dist/cli/skills-content/help.js.map +1 -1
  159. package/dist/cli/skills-content/index.d.ts +1 -0
  160. package/dist/cli/skills-content/index.d.ts.map +1 -1
  161. package/dist/cli/skills-content/index.js +1 -0
  162. package/dist/cli/skills-content/index.js.map +1 -1
  163. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  164. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  165. package/dist/cli/skills-content/rewind-skill.js +53 -0
  166. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  167. package/dist/cli/skills.d.ts +6 -0
  168. package/dist/cli/skills.d.ts.map +1 -1
  169. package/dist/cli/skills.js +90 -14
  170. package/dist/cli/skills.js.map +1 -1
  171. package/dist/client/resources/ResourceTree.d.ts +3 -1
  172. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  173. package/dist/client/resources/ResourceTree.js +4 -4
  174. package/dist/client/resources/ResourceTree.js.map +1 -1
  175. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  176. package/dist/client/resources/ResourcesPanel.js +55 -6
  177. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  178. package/dist/collab/routes.d.ts +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  181. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  182. package/dist/mcp/screen-memory-stdio.js +1014 -91
  183. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  184. package/dist/mcp-client/index.d.ts +1 -1
  185. package/dist/mcp-client/index.d.ts.map +1 -1
  186. package/dist/mcp-client/index.js +1 -1
  187. package/dist/mcp-client/index.js.map +1 -1
  188. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  189. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  190. package/dist/mcp-client/screen-memory-local.js +590 -12
  191. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  192. package/dist/notifications/routes.d.ts +2 -2
  193. package/dist/observability/routes.d.ts +5 -5
  194. package/dist/progress/routes.d.ts +1 -1
  195. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  196. package/dist/resources/handlers.d.ts +1 -1
  197. package/dist/secrets/routes.d.ts +9 -9
  198. package/dist/server/transcribe-voice.d.ts +1 -1
  199. package/package.json +2 -2
  200. package/src/cli/mcp-config-writers.ts +6 -4
  201. package/src/cli/mcp.ts +124 -0
  202. package/src/cli/skills-content/help.ts +4 -3
  203. package/src/cli/skills-content/index.ts +1 -0
  204. package/src/cli/skills-content/rewind-skill.ts +52 -0
  205. package/src/cli/skills.ts +115 -13
  206. package/src/client/resources/ResourceTree.tsx +11 -5
  207. package/src/client/resources/ResourcesPanel.tsx +141 -18
  208. package/src/mcp/screen-memory-stdio.ts +1360 -104
  209. package/src/mcp-client/index.ts +9 -0
  210. package/src/mcp-client/screen-memory-local.ts +847 -13
@@ -1,10 +1,11 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
- import { eq } from "drizzle-orm";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
4
  import { z } from "zod";
5
5
 
6
- import { getDb, schema } from "../server/db/index.js";
6
+ import { getDb } from "../server/db/index.js";
7
7
  import { assertContentDatabaseLifecycleAccess } from "./_content-database-lifecycle.js";
8
+ import { trashDocumentSubtree } from "./delete-document.js";
8
9
 
9
10
  export default defineAction({
10
11
  description:
@@ -17,15 +18,17 @@ export default defineAction({
17
18
  if (database.systemRole) {
18
19
  throw new Error("System Content databases cannot be deleted");
19
20
  }
21
+ await assertAccess("document", database.documentId, "admin");
20
22
  const db = getDb();
21
23
  const deletedAt = database.deletedAt ?? new Date().toISOString();
22
-
23
- if (!database.deletedAt) {
24
- await db
25
- .update(schema.contentDatabases)
26
- .set({ deletedAt, updatedAt: deletedAt })
27
- .where(eq(schema.contentDatabases.id, databaseId));
28
- }
24
+ await db.transaction((tx) =>
25
+ trashDocumentSubtree(
26
+ tx as unknown as ReturnType<typeof getDb>,
27
+ database.documentId,
28
+ database.ownerEmail,
29
+ deletedAt,
30
+ ),
31
+ );
29
32
 
30
33
  await writeAppState("refresh-signal", { ts: Date.now() });
31
34
 
@@ -11,7 +11,7 @@ import {
11
11
  resolveDatabaseRowsForBatch,
12
12
  } from "./_database-row-batch.js";
13
13
  import { getContentDatabaseResponse } from "./_database-utils.js";
14
- import { deleteDocumentRecursive } from "./delete-document.js";
14
+ import { trashDocumentSubtree } from "./delete-document.js";
15
15
 
16
16
  export default defineAction({
17
17
  description:
@@ -57,10 +57,11 @@ export default defineAction({
57
57
 
58
58
  await db.transaction(async (tx) => {
59
59
  for (const row of rows) {
60
- await deleteDocumentRecursive(
60
+ await trashDocumentSubtree(
61
61
  tx as unknown as ReturnType<typeof getDb>,
62
62
  row.document.id,
63
63
  row.document.ownerEmail,
64
+ now,
64
65
  );
65
66
  }
66
67
  await renumberDatabaseRows(
@@ -1,12 +1,13 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { assertAccess } from "@agent-native/core/sharing";
4
- import { and, eq, inArray } from "drizzle-orm";
4
+ import { and, eq, inArray, isNotNull, isNull, ne, or } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import { chunks } from "./_batch-utils.js";
9
9
  import { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
10
+ import { renumberDatabaseRows } from "./_database-row-batch.js";
10
11
 
11
12
  const DELETE_BATCH_SIZE = 90;
12
13
 
@@ -155,6 +156,152 @@ async function collectDocumentSubtreeForDelete(
155
156
  };
156
157
  }
157
158
 
159
+ export async function trashDocumentSubtree(
160
+ db: ReturnType<typeof getDb>,
161
+ id: string,
162
+ ownerEmail: string,
163
+ trashedAt = new Date().toISOString(),
164
+ ): Promise<string[]> {
165
+ const { documentIds } = await collectDocumentSubtreeForDelete(
166
+ db,
167
+ id,
168
+ ownerEmail,
169
+ );
170
+ await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
171
+
172
+ const independentlyTrashedDatabaseDocumentIds = new Set<string>();
173
+ for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
174
+ for (const database of await db
175
+ .select({ documentId: schema.contentDatabases.documentId })
176
+ .from(schema.contentDatabases)
177
+ .where(
178
+ and(
179
+ inArray(schema.contentDatabases.documentId, batch),
180
+ eq(schema.contentDatabases.ownerEmail, ownerEmail),
181
+ isNotNull(schema.contentDatabases.deletedAt),
182
+ ),
183
+ )) {
184
+ independentlyTrashedDatabaseDocumentIds.add(database.documentId);
185
+ }
186
+ }
187
+
188
+ const activeDocumentIds: string[] = [];
189
+ for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
190
+ activeDocumentIds.push(
191
+ ...(
192
+ await db
193
+ .select({ id: schema.documents.id })
194
+ .from(schema.documents)
195
+ .where(
196
+ and(
197
+ inArray(schema.documents.id, batch),
198
+ eq(schema.documents.ownerEmail, ownerEmail),
199
+ isNull(schema.documents.trashedAt),
200
+ ),
201
+ )
202
+ )
203
+ .map((document) => document.id)
204
+ .filter(
205
+ (documentId) =>
206
+ !independentlyTrashedDatabaseDocumentIds.has(documentId),
207
+ ),
208
+ );
209
+ }
210
+
211
+ for (const batch of chunks(activeDocumentIds, DELETE_BATCH_SIZE)) {
212
+ await db
213
+ .update(schema.documents)
214
+ .set({ trashedAt, trashRootId: id, updatedAt: trashedAt })
215
+ .where(
216
+ and(
217
+ inArray(schema.documents.id, batch),
218
+ eq(schema.documents.ownerEmail, ownerEmail),
219
+ isNull(schema.documents.trashedAt),
220
+ ),
221
+ );
222
+ await db
223
+ .update(schema.contentDatabases)
224
+ .set({ deletedAt: trashedAt, updatedAt: trashedAt })
225
+ .where(
226
+ and(
227
+ inArray(schema.contentDatabases.documentId, batch),
228
+ eq(schema.contentDatabases.ownerEmail, ownerEmail),
229
+ isNull(schema.contentDatabases.deletedAt),
230
+ ),
231
+ );
232
+ }
233
+
234
+ return activeDocumentIds;
235
+ }
236
+
237
+ export async function restoreDocumentSubtree(
238
+ db: ReturnType<typeof getDb>,
239
+ rootId: string,
240
+ ownerEmail: string,
241
+ ): Promise<string[]> {
242
+ const documentIds = (
243
+ await db
244
+ .select({ id: schema.documents.id })
245
+ .from(schema.documents)
246
+ .where(
247
+ and(
248
+ eq(schema.documents.trashRootId, rootId),
249
+ eq(schema.documents.ownerEmail, ownerEmail),
250
+ ),
251
+ )
252
+ ).map((document) => document.id);
253
+ if (documentIds.length === 0) return [];
254
+
255
+ const now = new Date().toISOString();
256
+ for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
257
+ await db
258
+ .update(schema.documents)
259
+ .set({ trashedAt: null, trashRootId: null, updatedAt: now })
260
+ .where(
261
+ and(
262
+ inArray(schema.documents.id, batch),
263
+ eq(schema.documents.ownerEmail, ownerEmail),
264
+ eq(schema.documents.trashRootId, rootId),
265
+ ),
266
+ );
267
+ await db
268
+ .update(schema.contentDatabases)
269
+ .set({ deletedAt: null, updatedAt: now })
270
+ .where(
271
+ and(
272
+ inArray(schema.contentDatabases.documentId, batch),
273
+ eq(schema.contentDatabases.ownerEmail, ownerEmail),
274
+ ),
275
+ );
276
+ }
277
+
278
+ const databaseIds = [
279
+ ...new Set(
280
+ (
281
+ await db
282
+ .select({ databaseId: schema.contentDatabaseItems.databaseId })
283
+ .from(schema.contentDatabaseItems)
284
+ .where(inArray(schema.contentDatabaseItems.documentId, documentIds))
285
+ ).map((item) => item.databaseId),
286
+ ),
287
+ ];
288
+ for (const databaseId of databaseIds) {
289
+ const [database] = await db
290
+ .select()
291
+ .from(schema.contentDatabases)
292
+ .where(
293
+ and(
294
+ eq(schema.contentDatabases.id, databaseId),
295
+ isNull(schema.contentDatabases.deletedAt),
296
+ ),
297
+ )
298
+ .limit(1);
299
+ if (database) await renumberDatabaseRows(db, database, now);
300
+ }
301
+
302
+ return documentIds;
303
+ }
304
+
158
305
  async function deleteWhereIn<T>(
159
306
  items: T[],
160
307
  run: (batch: T[]) => Promise<unknown>,
@@ -171,6 +318,20 @@ export async function deleteDocumentRecursive(
171
318
  ): Promise<string[]> {
172
319
  const { documentIds, ownedDatabaseIds } =
173
320
  await collectDocumentSubtreeForDelete(db, id, ownerEmail);
321
+ return deleteCollectedDocuments(
322
+ db,
323
+ documentIds,
324
+ ownedDatabaseIds,
325
+ ownerEmail,
326
+ );
327
+ }
328
+
329
+ async function deleteCollectedDocuments(
330
+ db: ReturnType<typeof getDb>,
331
+ documentIds: string[],
332
+ ownedDatabaseIds: string[],
333
+ ownerEmail: string,
334
+ ): Promise<string[]> {
174
335
  await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
175
336
 
176
337
  const propertyDefinitionIds: string[] = [];
@@ -353,8 +514,72 @@ export async function deleteDocumentRecursive(
353
514
  return documentIds;
354
515
  }
355
516
 
517
+ export async function deleteTrashedDocumentSubtree(
518
+ db: ReturnType<typeof getDb>,
519
+ id: string,
520
+ ownerEmail: string,
521
+ ): Promise<string[]> {
522
+ const [root] = await db
523
+ .select({ id: schema.documents.id })
524
+ .from(schema.documents)
525
+ .where(
526
+ and(
527
+ eq(schema.documents.id, id),
528
+ eq(schema.documents.ownerEmail, ownerEmail),
529
+ eq(schema.documents.trashRootId, id),
530
+ isNotNull(schema.documents.trashedAt),
531
+ ),
532
+ )
533
+ .limit(1);
534
+ if (!root) {
535
+ throw new Error(
536
+ "Document must be in Trash and be a Trash root before permanent deletion",
537
+ );
538
+ }
539
+
540
+ const documentIds = (
541
+ await db
542
+ .select({ id: schema.documents.id })
543
+ .from(schema.documents)
544
+ .where(
545
+ and(
546
+ eq(schema.documents.ownerEmail, ownerEmail),
547
+ eq(schema.documents.trashRootId, id),
548
+ isNotNull(schema.documents.trashedAt),
549
+ ),
550
+ )
551
+ ).map((document) => document.id);
552
+ const ownedDatabaseIds = await selectOwnedDatabaseIds(
553
+ db,
554
+ documentIds,
555
+ ownerEmail,
556
+ ).then((rows) => rows.map((database) => database.id));
557
+
558
+ await db
559
+ .update(schema.documents)
560
+ .set({ parentId: null, updatedAt: new Date().toISOString() })
561
+ .where(
562
+ and(
563
+ eq(schema.documents.ownerEmail, ownerEmail),
564
+ inArray(schema.documents.parentId, documentIds),
565
+ or(
566
+ isNull(schema.documents.trashRootId),
567
+ ne(schema.documents.trashRootId, id),
568
+ ),
569
+ ),
570
+ );
571
+
572
+ return deleteCollectedDocuments(
573
+ db,
574
+ documentIds,
575
+ ownedDatabaseIds,
576
+ ownerEmail,
577
+ );
578
+ }
579
+
356
580
  export default defineAction({
357
- description: "Delete a document and all its children recursively.",
581
+ description:
582
+ "Move a document and all its children to Trash. Use permanently-delete-document to destroy an item already in Trash.",
358
583
  schema: z.object({
359
584
  id: z.string().optional().describe("Document ID (required)"),
360
585
  databaseDocumentId: z
@@ -408,10 +633,12 @@ export default defineAction({
408
633
  if (systemDatabase?.systemRole) {
409
634
  throw new Error("System Content database documents cannot be deleted");
410
635
  }
411
- const deleted = await deleteDocumentRecursive(
412
- db,
413
- id,
414
- existing.ownerEmail as string,
636
+ const deleted = await db.transaction((tx) =>
637
+ trashDocumentSubtree(
638
+ tx as unknown as ReturnType<typeof getDb>,
639
+ id,
640
+ existing.ownerEmail as string,
641
+ ),
415
642
  );
416
643
 
417
644
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -46,6 +46,7 @@ export default defineAction({
46
46
  ? eq(schema.contentDatabaseItems.id, itemId)
47
47
  : eq(schema.contentDatabaseItems.documentId, documentId!),
48
48
  isNull(schema.contentDatabases.deletedAt),
49
+ isNull(schema.documents.trashedAt),
49
50
  ),
50
51
  );
51
52
 
@@ -70,7 +70,10 @@ export default defineAction({
70
70
  statusCode: 404,
71
71
  });
72
72
  }
73
- if (await isSoftDeletedDatabaseDocument(args.id)) {
73
+ if (
74
+ access.resource.trashedAt ||
75
+ (await isSoftDeletedDatabaseDocument(args.id))
76
+ ) {
74
77
  throw Object.assign(new Error(`Document "${args.id}" not found`), {
75
78
  statusCode: 404,
76
79
  });
@@ -60,6 +60,7 @@ export default defineAction({
60
60
  .where(
61
61
  and(
62
62
  accessFilter(schema.documents, schema.documentShares),
63
+ isNull(schema.documents.trashedAt),
63
64
  documentDiscoveryFilter(),
64
65
  isNull(schema.contentDatabases.deletedAt),
65
66
  excludedDatabaseIds.size === 1
@@ -106,6 +106,7 @@ export default defineAction({
106
106
  accessFilter(schema.documents, schema.documentShares, context),
107
107
  ),
108
108
  ),
109
+ isNull(schema.documents.trashedAt),
109
110
  documentDiscoveryFilter({
110
111
  userEmail,
111
112
  orgIds: authorizedOrgIds,
@@ -34,6 +34,7 @@ export default defineAction({
34
34
  deletedAt: schema.contentDatabases.deletedAt,
35
35
  documentTitle: schema.documents.title,
36
36
  documentParentId: schema.documents.parentId,
37
+ documentTrashRootId: schema.documents.trashRootId,
37
38
  })
38
39
  .from(schema.contentDatabases)
39
40
  .innerJoin(
@@ -47,6 +48,10 @@ export default defineAction({
47
48
  .where(
48
49
  and(
49
50
  isNotNull(schema.contentDatabases.deletedAt),
51
+ or(
52
+ isNull(schema.documents.trashRootId),
53
+ eq(schema.documents.trashRootId, schema.documents.id),
54
+ ),
50
55
  or(
51
56
  and(
52
57
  isBlockOwned,
@@ -82,8 +87,9 @@ export default defineAction({
82
87
  ownerDocumentId: row.ownerDocumentId,
83
88
  deletedAt: row.deletedAt!,
84
89
  canPermanentlyDelete:
85
- row.ownerDocumentId === null ||
86
- row.documentParentId !== row.ownerDocumentId,
90
+ row.documentTrashRootId === row.documentId &&
91
+ (row.ownerDocumentId === null ||
92
+ row.documentParentId !== row.ownerDocumentId),
87
93
  })),
88
94
  };
89
95
  },
@@ -0,0 +1,50 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import type { ListTrashedDocumentsResponse } from "../shared/api.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "List trashed page roots the current user can manage. Titles are returned only after document access filtering.",
12
+ schema: z.object({}),
13
+ http: { method: "GET" },
14
+ readOnly: true,
15
+ run: async (): Promise<ListTrashedDocumentsResponse> => {
16
+ const rows = await getDb()
17
+ .select({
18
+ documentId: schema.documents.id,
19
+ title: schema.documents.title,
20
+ trashedAt: schema.documents.trashedAt,
21
+ })
22
+ .from(schema.documents)
23
+ .leftJoin(
24
+ schema.contentDatabases,
25
+ eq(schema.contentDatabases.documentId, schema.documents.id),
26
+ )
27
+ .where(
28
+ and(
29
+ isNotNull(schema.documents.trashedAt),
30
+ eq(schema.documents.trashRootId, schema.documents.id),
31
+ isNull(schema.contentDatabases.id),
32
+ accessFilter(
33
+ schema.documents,
34
+ schema.documentShares,
35
+ undefined,
36
+ "admin",
37
+ ),
38
+ ),
39
+ )
40
+ .orderBy(desc(schema.documents.trashedAt));
41
+
42
+ return {
43
+ documents: rows.map((row) => ({
44
+ documentId: row.documentId,
45
+ title: row.title.trim() || "Untitled",
46
+ trashedAt: row.trashedAt!,
47
+ })),
48
+ };
49
+ },
50
+ });
@@ -0,0 +1,28 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { writeAppState } from "@agent-native/core/application-state";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
+ import { z } from "zod";
5
+
6
+ import { getDb } from "../server/db/index.js";
7
+ import { deleteTrashedDocumentSubtree } from "./delete-document.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "Permanently delete a document subtree that is already in Trash. This cannot be undone.",
12
+ schema: z.object({
13
+ id: z.string().describe("Trashed root document ID"),
14
+ }),
15
+ run: async ({ id }) => {
16
+ const access = await assertAccess("document", id, "admin");
17
+ const db = getDb();
18
+ const deleted = await db.transaction((tx) =>
19
+ deleteTrashedDocumentSubtree(
20
+ tx as unknown as ReturnType<typeof getDb>,
21
+ id,
22
+ access.resource.ownerEmail as string,
23
+ ),
24
+ );
25
+ await writeAppState("refresh-signal", { ts: Date.now() });
26
+ return { success: true, deleted: deleted.length };
27
+ },
28
+ });
@@ -4,6 +4,7 @@ import { resolveAccess } from "@agent-native/core/sharing";
4
4
  import { z } from "zod";
5
5
 
6
6
  import "../server/db/index.js";
7
+ import { isSoftDeletedDatabaseDocument } from "./_database-utils.js";
7
8
  import { flushOpenDocumentEditorToSql } from "./_document-flush.js";
8
9
 
9
10
  /**
@@ -62,7 +63,13 @@ export default defineAction({
62
63
  publicAgent: { expose: true, readOnly: true, requiresAuth: true },
63
64
  run: async ({ id, format }) => {
64
65
  const access = await resolveAccess("document", id);
65
- if (!access) throw new Error(`Document "${id}" not found`);
66
+ if (
67
+ !access ||
68
+ access.resource.trashedAt ||
69
+ (await isSoftDeletedDatabaseDocument(id))
70
+ ) {
71
+ throw new Error(`Document "${id}" not found`);
72
+ }
66
73
 
67
74
  // If a live Yjs collab session is open, the in-memory editor doc is fresher
68
75
  // than the SQL column. Ask the open editor to serialize + save, then wait
@@ -74,7 +81,13 @@ export default defineAction({
74
81
 
75
82
  // Re-resolve so we read the now-fresh row (and re-check access).
76
83
  const fresh = await resolveAccess("document", id);
77
- if (!fresh) throw new Error(`Document "${id}" not found`);
84
+ if (
85
+ !fresh ||
86
+ fresh.resource.trashedAt ||
87
+ (await isSoftDeletedDatabaseDocument(id))
88
+ ) {
89
+ throw new Error(`Document "${id}" not found`);
90
+ }
78
91
  const doc = fresh.resource;
79
92
  const markdown = (doc.content as string) ?? "";
80
93
  const content = formatDocumentContent(markdown, format);
@@ -1,7 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { resolveAccess } from "@agent-native/core/sharing";
4
- import { eq } from "drizzle-orm";
4
+ import { and, eq, isNotNull } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
@@ -9,6 +9,7 @@ import {
9
9
  assertContentDatabaseLifecycleAccess,
10
10
  collectInlineDatabaseOwnerBlockIds,
11
11
  } from "./_content-database-lifecycle.js";
12
+ import { restoreDocumentSubtree } from "./delete-document.js";
12
13
  import pullDocumentAction from "./pull-document.js";
13
14
 
14
15
  async function shouldClearStaleInlineOwnership(args: {
@@ -47,16 +48,52 @@ export default defineAction({
47
48
  ownerBlockId: ownership.database.ownerBlockId,
48
49
  });
49
50
 
50
- await db
51
- .update(schema.contentDatabases)
52
- .set({
53
- deletedAt: null,
54
- updatedAt: now,
55
- ...(clearInlineOwnership
56
- ? { ownerDocumentId: null, ownerBlockId: null }
57
- : {}),
58
- })
59
- .where(eq(schema.contentDatabases.id, databaseId));
51
+ await db.transaction(async (tx) => {
52
+ const [backingDocument] = await tx
53
+ .select({
54
+ trashedAt: schema.documents.trashedAt,
55
+ trashRootId: schema.documents.trashRootId,
56
+ })
57
+ .from(schema.documents)
58
+ .where(eq(schema.documents.id, ownership.database.documentId))
59
+ .limit(1);
60
+ if (!backingDocument) {
61
+ throw new Error(`Database "${databaseId}" not found`);
62
+ }
63
+ if (
64
+ backingDocument.trashedAt &&
65
+ backingDocument.trashRootId !== ownership.database.documentId
66
+ ) {
67
+ throw new Error("Restore the parent Trash item instead");
68
+ }
69
+
70
+ const restoredDocumentIds = await restoreDocumentSubtree(
71
+ tx as unknown as ReturnType<typeof getDb>,
72
+ ownership.database.documentId,
73
+ ownership.database.ownerEmail,
74
+ );
75
+ if (
76
+ backingDocument.trashedAt &&
77
+ !restoredDocumentIds.includes(ownership.database.documentId)
78
+ ) {
79
+ throw new Error("Database backing page was not restored");
80
+ }
81
+ await tx
82
+ .update(schema.contentDatabases)
83
+ .set({
84
+ deletedAt: null,
85
+ updatedAt: now,
86
+ ...(clearInlineOwnership
87
+ ? { ownerDocumentId: null, ownerBlockId: null }
88
+ : {}),
89
+ })
90
+ .where(
91
+ and(
92
+ eq(schema.contentDatabases.id, databaseId),
93
+ isNotNull(schema.contentDatabases.deletedAt),
94
+ ),
95
+ );
96
+ });
60
97
 
61
98
  await writeAppState("refresh-signal", { ts: Date.now() });
62
99
 
@@ -0,0 +1,28 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { writeAppState } from "@agent-native/core/application-state";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
+ import { z } from "zod";
5
+
6
+ import { getDb } from "../server/db/index.js";
7
+ import { restoreDocumentSubtree } from "./delete-document.js";
8
+
9
+ export default defineAction({
10
+ description: "Restore a page subtree from Trash.",
11
+ schema: z.object({
12
+ id: z.string().describe("Trashed root document ID"),
13
+ }),
14
+ run: async ({ id }) => {
15
+ const access = await assertAccess("document", id, "admin");
16
+ const restored = await getDb().transaction((tx) =>
17
+ restoreDocumentSubtree(
18
+ tx as unknown as ReturnType<typeof getDb>,
19
+ id,
20
+ access.resource.ownerEmail as string,
21
+ ),
22
+ );
23
+ if (restored.length === 0) throw new Error("Document is not in Trash");
24
+
25
+ await writeAppState("refresh-signal", { ts: Date.now() });
26
+ return { success: true, restored: restored.length, documentId: id };
27
+ },
28
+ });
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, sql } from "drizzle-orm";
3
+ import { and, isNull, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -74,6 +74,7 @@ export default defineAction({
74
74
  .where(
75
75
  and(
76
76
  accessFilter(schema.documents, schema.documentShares),
77
+ isNull(schema.documents.trashedAt),
77
78
  documentDiscoveryFilter(),
78
79
  sql`(${schema.documents.title} LIKE ${pattern} ESCAPE '\\' OR ${schema.documents.description} LIKE ${pattern} ESCAPE '\\' OR ${schema.documents.content} LIKE ${pattern} ESCAPE '\\')`,
79
80
  ),
@@ -4,7 +4,7 @@ import {
4
4
  readAppStateForCurrentTab,
5
5
  } from "@agent-native/core/application-state";
6
6
  import { accessFilter, resolveAccess } from "@agent-native/core/sharing";
7
- import { and, asc, inArray } from "drizzle-orm";
7
+ import { and, asc, inArray, isNull } from "drizzle-orm";
8
8
  import { z } from "zod";
9
9
 
10
10
  import { getDb, schema } from "../server/db/index.js";
@@ -765,6 +765,7 @@ export default defineAction({
765
765
  .where(
766
766
  and(
767
767
  accessFilter(schema.documents, schema.documentShares),
768
+ isNull(schema.documents.trashedAt),
768
769
  documentDiscoveryFilter(),
769
770
  ),
770
771
  )