stagent 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +272 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -2
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/documents/[id]/route.ts +5 -1
  17. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  18. package/src/app/api/documents/route.ts +5 -1
  19. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  20. package/src/app/api/projects/[id]/route.ts +72 -3
  21. package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
  22. package/src/app/api/schedules/route.ts +19 -1
  23. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  24. package/src/app/api/snapshots/[id]/route.ts +44 -0
  25. package/src/app/api/snapshots/route.ts +54 -0
  26. package/src/app/api/snapshots/settings/route.ts +67 -0
  27. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  28. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  29. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  30. package/src/app/api/tables/[id]/export/route.ts +94 -0
  31. package/src/app/api/tables/[id]/history/route.ts +15 -0
  32. package/src/app/api/tables/[id]/import/route.ts +111 -0
  33. package/src/app/api/tables/[id]/route.ts +86 -0
  34. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  35. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  36. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  37. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  38. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  39. package/src/app/api/tables/route.ts +65 -0
  40. package/src/app/api/tables/templates/route.ts +92 -0
  41. package/src/app/api/tasks/[id]/route.ts +37 -2
  42. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  43. package/src/app/api/tasks/route.ts +8 -9
  44. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  45. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  46. package/src/app/api/workflows/[id]/route.ts +16 -3
  47. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  48. package/src/app/api/workflows/route.ts +13 -2
  49. package/src/app/documents/page.tsx +5 -1
  50. package/src/app/layout.tsx +0 -1
  51. package/src/app/manifest.ts +3 -3
  52. package/src/app/projects/[id]/page.tsx +62 -2
  53. package/src/app/settings/page.tsx +2 -0
  54. package/src/app/tables/[id]/page.tsx +67 -0
  55. package/src/app/tables/page.tsx +21 -0
  56. package/src/app/tables/templates/page.tsx +19 -0
  57. package/src/components/chat/chat-table-result.tsx +139 -0
  58. package/src/components/documents/document-browser.tsx +1 -1
  59. package/src/components/documents/document-chip-bar.tsx +17 -1
  60. package/src/components/documents/document-detail-view.tsx +51 -0
  61. package/src/components/documents/document-grid.tsx +5 -0
  62. package/src/components/documents/document-table.tsx +4 -0
  63. package/src/components/documents/types.ts +3 -0
  64. package/src/components/projects/project-form-sheet.tsx +109 -2
  65. package/src/components/schedules/schedule-form.tsx +91 -1
  66. package/src/components/settings/data-management-section.tsx +17 -12
  67. package/src/components/settings/database-snapshots-section.tsx +469 -0
  68. package/src/components/shared/app-sidebar.tsx +2 -0
  69. package/src/components/shared/document-picker-sheet.tsx +486 -0
  70. package/src/components/tables/table-browser.tsx +234 -0
  71. package/src/components/tables/table-cell-editor.tsx +226 -0
  72. package/src/components/tables/table-chart-builder.tsx +288 -0
  73. package/src/components/tables/table-chart-view.tsx +146 -0
  74. package/src/components/tables/table-column-header.tsx +103 -0
  75. package/src/components/tables/table-column-sheet.tsx +331 -0
  76. package/src/components/tables/table-create-sheet.tsx +240 -0
  77. package/src/components/tables/table-detail-sheet.tsx +144 -0
  78. package/src/components/tables/table-detail-tabs.tsx +278 -0
  79. package/src/components/tables/table-grid.tsx +61 -0
  80. package/src/components/tables/table-history-tab.tsx +148 -0
  81. package/src/components/tables/table-import-wizard.tsx +542 -0
  82. package/src/components/tables/table-list-table.tsx +95 -0
  83. package/src/components/tables/table-relation-combobox.tsx +217 -0
  84. package/src/components/tables/table-spreadsheet.tsx +499 -0
  85. package/src/components/tables/table-template-gallery.tsx +162 -0
  86. package/src/components/tables/table-template-preview.tsx +219 -0
  87. package/src/components/tables/table-toolbar.tsx +79 -0
  88. package/src/components/tables/table-triggers-tab.tsx +446 -0
  89. package/src/components/tables/types.ts +6 -0
  90. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  91. package/src/components/tables/utils.ts +29 -0
  92. package/src/components/tasks/task-card.tsx +8 -1
  93. package/src/components/tasks/task-create-panel.tsx +111 -14
  94. package/src/components/tasks/task-detail-view.tsx +47 -0
  95. package/src/components/tasks/task-edit-dialog.tsx +103 -2
  96. package/src/components/workflows/workflow-form-view.tsx +207 -7
  97. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  98. package/src/components/workflows/workflow-list.tsx +90 -45
  99. package/src/components/workflows/workflow-status-view.tsx +168 -23
  100. package/src/instrumentation.ts +3 -0
  101. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  102. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  103. package/src/lib/agents/claude-agent.ts +3 -1
  104. package/src/lib/agents/profiles/registry.ts +6 -3
  105. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  106. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  107. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  108. package/src/lib/book/chapter-generator.ts +4 -19
  109. package/src/lib/book/chapter-mapping.ts +17 -0
  110. package/src/lib/book/content.ts +5 -16
  111. package/src/lib/book/update-detector.ts +3 -16
  112. package/src/lib/chat/engine.ts +1 -0
  113. package/src/lib/chat/stagent-tools.ts +2 -0
  114. package/src/lib/chat/system-prompt.ts +9 -1
  115. package/src/lib/chat/tool-catalog.ts +35 -0
  116. package/src/lib/chat/tools/settings-tools.ts +109 -0
  117. package/src/lib/chat/tools/table-tools.ts +955 -0
  118. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  119. package/src/lib/constants/table-status.ts +68 -0
  120. package/src/lib/data/__tests__/clear.test.ts +1 -1
  121. package/src/lib/data/clear.ts +57 -0
  122. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  123. package/src/lib/data/seed-data/conversations.ts +350 -42
  124. package/src/lib/data/seed-data/documents.ts +564 -591
  125. package/src/lib/data/seed-data/learned-context.ts +101 -22
  126. package/src/lib/data/seed-data/notifications.ts +344 -70
  127. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  128. package/src/lib/data/seed-data/profiles.ts +144 -46
  129. package/src/lib/data/seed-data/projects.ts +50 -18
  130. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  131. package/src/lib/data/seed-data/schedules.ts +208 -41
  132. package/src/lib/data/seed-data/table-templates.ts +234 -0
  133. package/src/lib/data/seed-data/tasks.ts +614 -116
  134. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  135. package/src/lib/data/seed-data/user-tables.ts +203 -0
  136. package/src/lib/data/seed-data/views.ts +52 -7
  137. package/src/lib/data/seed-data/workflows.ts +231 -84
  138. package/src/lib/data/seed.ts +55 -14
  139. package/src/lib/data/tables.ts +417 -0
  140. package/src/lib/db/bootstrap.ts +275 -0
  141. package/src/lib/db/index.ts +9 -0
  142. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  143. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  144. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  145. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  146. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  147. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  148. package/src/lib/db/schema.ts +445 -0
  149. package/src/lib/docs/reader.ts +2 -3
  150. package/src/lib/documents/context-builder.ts +75 -2
  151. package/src/lib/documents/document-resolver.ts +119 -0
  152. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  153. package/src/lib/schedules/scheduler.ts +31 -1
  154. package/src/lib/snapshots/auto-backup.ts +132 -0
  155. package/src/lib/snapshots/retention.ts +64 -0
  156. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  157. package/src/lib/tables/computed.ts +61 -0
  158. package/src/lib/tables/context-builder.ts +139 -0
  159. package/src/lib/tables/formula-engine.ts +415 -0
  160. package/src/lib/tables/history.ts +115 -0
  161. package/src/lib/tables/import.ts +343 -0
  162. package/src/lib/tables/query-builder.ts +152 -0
  163. package/src/lib/tables/trigger-evaluator.ts +146 -0
  164. package/src/lib/tables/types.ts +141 -0
  165. package/src/lib/tables/validation.ts +119 -0
  166. package/src/lib/utils/app-root.ts +20 -0
  167. package/src/lib/utils/stagent-paths.ts +20 -0
  168. package/src/lib/validators/__tests__/task.test.ts +43 -10
  169. package/src/lib/validators/task.ts +7 -1
  170. package/src/lib/workflows/blueprints/registry.ts +3 -3
  171. package/src/lib/workflows/engine.ts +24 -8
  172. package/src/lib/workflows/types.ts +14 -0
  173. package/tsconfig.json +3 -1
  174. package/public/icon.svg +0 -13
  175. package/src/components/tasks/file-upload.tsx +0 -120
  176. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -37,6 +37,7 @@ export const tasks = sqliteTable(
37
37
  sourceType: text("source_type", {
38
38
  enum: ["manual", "scheduled", "heartbeat", "workflow"],
39
39
  }),
40
+ workflowRunNumber: integer("workflow_run_number"),
40
41
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
41
42
  updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
42
43
  },
@@ -59,6 +60,7 @@ export const workflows = sqliteTable("workflows", {
59
60
  })
60
61
  .default("draft")
61
62
  .notNull(),
63
+ runNumber: integer("run_number").default(0).notNull(),
62
64
  createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
63
65
  updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
64
66
  });
@@ -678,6 +680,407 @@ export const agentMessages = sqliteTable(
678
680
  ]
679
681
  );
680
682
 
683
+ // ── Workflow Document Pool ───────────────────────────────────────────
684
+
685
+ export const workflowDocumentInputs = sqliteTable(
686
+ "workflow_document_inputs",
687
+ {
688
+ id: text("id").primaryKey(),
689
+ workflowId: text("workflow_id")
690
+ .references(() => workflows.id)
691
+ .notNull(),
692
+ documentId: text("document_id")
693
+ .references(() => documents.id)
694
+ .notNull(),
695
+ /** null = document available to all steps; set = scoped to specific step */
696
+ stepId: text("step_id"),
697
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
698
+ },
699
+ (table) => [
700
+ index("idx_wdi_workflow").on(table.workflowId),
701
+ index("idx_wdi_document").on(table.documentId),
702
+ uniqueIndex("idx_wdi_workflow_doc_step").on(
703
+ table.workflowId,
704
+ table.documentId,
705
+ table.stepId
706
+ ),
707
+ ]
708
+ );
709
+
710
+ export type WorkflowDocumentInputRow = InferSelectModel<typeof workflowDocumentInputs>;
711
+
712
+ export const scheduleDocumentInputs = sqliteTable(
713
+ "schedule_document_inputs",
714
+ {
715
+ id: text("id").primaryKey(),
716
+ scheduleId: text("schedule_id")
717
+ .references(() => schedules.id)
718
+ .notNull(),
719
+ documentId: text("document_id")
720
+ .references(() => documents.id)
721
+ .notNull(),
722
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
723
+ },
724
+ (table) => [
725
+ index("idx_sdi_schedule").on(table.scheduleId),
726
+ uniqueIndex("idx_sdi_schedule_doc").on(
727
+ table.scheduleId,
728
+ table.documentId
729
+ ),
730
+ ]
731
+ );
732
+
733
+ export type ScheduleDocumentInputRow = InferSelectModel<typeof scheduleDocumentInputs>;
734
+
735
+ export const projectDocumentDefaults = sqliteTable(
736
+ "project_document_defaults",
737
+ {
738
+ id: text("id").primaryKey(),
739
+ projectId: text("project_id")
740
+ .references(() => projects.id)
741
+ .notNull(),
742
+ documentId: text("document_id")
743
+ .references(() => documents.id)
744
+ .notNull(),
745
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
746
+ },
747
+ (table) => [
748
+ index("idx_pdd_project").on(table.projectId),
749
+ uniqueIndex("idx_pdd_project_doc").on(
750
+ table.projectId,
751
+ table.documentId
752
+ ),
753
+ ]
754
+ );
755
+
756
+ export type ProjectDocumentDefaultRow = InferSelectModel<typeof projectDocumentDefaults>;
757
+
758
+ // ── User-Defined Tables (structured data) ───────────────────────────────
759
+
760
+ export const userTables = sqliteTable(
761
+ "user_tables",
762
+ {
763
+ id: text("id").primaryKey(),
764
+ projectId: text("project_id").references(() => projects.id),
765
+ name: text("name").notNull(),
766
+ description: text("description"),
767
+ /** JSON array of column definitions — denormalized for fast reads */
768
+ columnSchema: text("column_schema").notNull().default("[]"),
769
+ /** Denormalized row count for list views */
770
+ rowCount: integer("row_count").default(0).notNull(),
771
+ /** How this table was created */
772
+ source: text("source", {
773
+ enum: ["manual", "imported", "agent", "template"],
774
+ })
775
+ .default("manual")
776
+ .notNull(),
777
+ /** Template ID if created from a template */
778
+ templateId: text("template_id"),
779
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
780
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
781
+ },
782
+ (table) => [
783
+ index("idx_user_tables_project_id").on(table.projectId),
784
+ index("idx_user_tables_source").on(table.source),
785
+ ]
786
+ );
787
+
788
+ export const userTableColumns = sqliteTable(
789
+ "user_table_columns",
790
+ {
791
+ id: text("id").primaryKey(),
792
+ tableId: text("table_id")
793
+ .references(() => userTables.id)
794
+ .notNull(),
795
+ name: text("name").notNull(),
796
+ displayName: text("display_name").notNull(),
797
+ dataType: text("data_type", {
798
+ enum: [
799
+ "text",
800
+ "number",
801
+ "date",
802
+ "boolean",
803
+ "select",
804
+ "url",
805
+ "email",
806
+ "relation",
807
+ "computed",
808
+ ],
809
+ }).notNull(),
810
+ position: integer("position").notNull(),
811
+ required: integer("required", { mode: "boolean" }).default(false).notNull(),
812
+ defaultValue: text("default_value"),
813
+ /** JSON config for type-specific settings (select options, formula, relation target, etc.) */
814
+ config: text("config"),
815
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
816
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
817
+ },
818
+ (table) => [
819
+ index("idx_user_table_columns_table_id").on(table.tableId),
820
+ index("idx_user_table_columns_position").on(table.tableId, table.position),
821
+ ]
822
+ );
823
+
824
+ export const userTableRows = sqliteTable(
825
+ "user_table_rows",
826
+ {
827
+ id: text("id").primaryKey(),
828
+ tableId: text("table_id")
829
+ .references(() => userTables.id)
830
+ .notNull(),
831
+ /** JSON object with column values keyed by column name */
832
+ data: text("data").notNull().default("{}"),
833
+ position: integer("position").notNull(),
834
+ /** Who created this row: 'user' or agent profile ID */
835
+ createdBy: text("created_by").default("user"),
836
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
837
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
838
+ },
839
+ (table) => [
840
+ index("idx_user_table_rows_table_id").on(table.tableId),
841
+ index("idx_user_table_rows_position").on(table.tableId, table.position),
842
+ ]
843
+ );
844
+
845
+ export const userTableViews = sqliteTable(
846
+ "user_table_views",
847
+ {
848
+ id: text("id").primaryKey(),
849
+ tableId: text("table_id")
850
+ .references(() => userTables.id)
851
+ .notNull(),
852
+ name: text("name").notNull(),
853
+ type: text("type", { enum: ["grid", "chart", "joined"] })
854
+ .default("grid")
855
+ .notNull(),
856
+ /** JSON config: filters, sorting, column visibility, chart config, join config */
857
+ config: text("config"),
858
+ isDefault: integer("is_default", { mode: "boolean" })
859
+ .default(false)
860
+ .notNull(),
861
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
862
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
863
+ },
864
+ (table) => [
865
+ index("idx_user_table_views_table_id").on(table.tableId),
866
+ ]
867
+ );
868
+
869
+ export const userTableRelationships = sqliteTable(
870
+ "user_table_relationships",
871
+ {
872
+ id: text("id").primaryKey(),
873
+ fromTableId: text("from_table_id")
874
+ .references(() => userTables.id)
875
+ .notNull(),
876
+ fromColumn: text("from_column").notNull(),
877
+ toTableId: text("to_table_id")
878
+ .references(() => userTables.id)
879
+ .notNull(),
880
+ toColumn: text("to_column").notNull(),
881
+ relationshipType: text("relationship_type", {
882
+ enum: ["one_to_one", "one_to_many", "many_to_many"],
883
+ }).notNull(),
884
+ /** JSON config for display column, cascade behavior, etc. */
885
+ config: text("config"),
886
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
887
+ },
888
+ (table) => [
889
+ index("idx_user_table_rels_from").on(table.fromTableId),
890
+ index("idx_user_table_rels_to").on(table.toTableId),
891
+ ]
892
+ );
893
+
894
+ export const userTableTemplates = sqliteTable(
895
+ "user_table_templates",
896
+ {
897
+ id: text("id").primaryKey(),
898
+ name: text("name").notNull(),
899
+ description: text("description"),
900
+ category: text("category", {
901
+ enum: ["business", "personal", "pm", "finance", "content"],
902
+ }).notNull(),
903
+ /** JSON array of column definitions */
904
+ columnSchema: text("column_schema").notNull(),
905
+ /** JSON array of sample row data */
906
+ sampleData: text("sample_data"),
907
+ scope: text("scope", { enum: ["system", "user"] })
908
+ .default("system")
909
+ .notNull(),
910
+ icon: text("icon"),
911
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
912
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
913
+ },
914
+ (table) => [
915
+ index("idx_user_table_templates_category").on(table.category),
916
+ index("idx_user_table_templates_scope").on(table.scope),
917
+ ]
918
+ );
919
+
920
+ export const userTableImports = sqliteTable(
921
+ "user_table_imports",
922
+ {
923
+ id: text("id").primaryKey(),
924
+ tableId: text("table_id")
925
+ .references(() => userTables.id)
926
+ .notNull(),
927
+ documentId: text("document_id").references(() => documents.id),
928
+ /** Number of rows imported */
929
+ rowCount: integer("row_count").default(0).notNull(),
930
+ /** Number of rows that failed validation */
931
+ errorCount: integer("error_count").default(0).notNull(),
932
+ /** JSON array of error details */
933
+ errors: text("errors"),
934
+ status: text("status", { enum: ["pending", "completed", "failed"] })
935
+ .default("pending")
936
+ .notNull(),
937
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
938
+ },
939
+ (table) => [
940
+ index("idx_user_table_imports_table_id").on(table.tableId),
941
+ ]
942
+ );
943
+
944
+ // ── Table Junction Tables ───────────────────────────────────────────────
945
+
946
+ export const tableDocumentInputs = sqliteTable(
947
+ "table_document_inputs",
948
+ {
949
+ id: text("id").primaryKey(),
950
+ tableId: text("table_id")
951
+ .references(() => userTables.id)
952
+ .notNull(),
953
+ documentId: text("document_id")
954
+ .references(() => documents.id)
955
+ .notNull(),
956
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
957
+ },
958
+ (table) => [
959
+ index("idx_tdi_table").on(table.tableId),
960
+ uniqueIndex("idx_tdi_table_doc").on(table.tableId, table.documentId),
961
+ ]
962
+ );
963
+
964
+ export const taskTableInputs = sqliteTable(
965
+ "task_table_inputs",
966
+ {
967
+ id: text("id").primaryKey(),
968
+ taskId: text("task_id")
969
+ .references(() => tasks.id)
970
+ .notNull(),
971
+ tableId: text("table_id")
972
+ .references(() => userTables.id)
973
+ .notNull(),
974
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
975
+ },
976
+ (table) => [
977
+ index("idx_tti_task").on(table.taskId),
978
+ uniqueIndex("idx_tti_task_table").on(table.taskId, table.tableId),
979
+ ]
980
+ );
981
+
982
+ export const workflowTableInputs = sqliteTable(
983
+ "workflow_table_inputs",
984
+ {
985
+ id: text("id").primaryKey(),
986
+ workflowId: text("workflow_id")
987
+ .references(() => workflows.id)
988
+ .notNull(),
989
+ tableId: text("table_id")
990
+ .references(() => userTables.id)
991
+ .notNull(),
992
+ /** null = table available to all steps; set = scoped to specific step */
993
+ stepId: text("step_id"),
994
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
995
+ },
996
+ (table) => [
997
+ index("idx_wti_workflow").on(table.workflowId),
998
+ uniqueIndex("idx_wti_workflow_table_step").on(
999
+ table.workflowId,
1000
+ table.tableId,
1001
+ table.stepId
1002
+ ),
1003
+ ]
1004
+ );
1005
+
1006
+ export const scheduleTableInputs = sqliteTable(
1007
+ "schedule_table_inputs",
1008
+ {
1009
+ id: text("id").primaryKey(),
1010
+ scheduleId: text("schedule_id")
1011
+ .references(() => schedules.id)
1012
+ .notNull(),
1013
+ tableId: text("table_id")
1014
+ .references(() => userTables.id)
1015
+ .notNull(),
1016
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
1017
+ },
1018
+ (table) => [
1019
+ index("idx_sti_schedule").on(table.scheduleId),
1020
+ uniqueIndex("idx_sti_schedule_table").on(
1021
+ table.scheduleId,
1022
+ table.tableId
1023
+ ),
1024
+ ]
1025
+ );
1026
+
1027
+ // ── Table Workflow Triggers ──────────────────────────────────────────
1028
+
1029
+ export const userTableTriggers = sqliteTable(
1030
+ "user_table_triggers",
1031
+ {
1032
+ id: text("id").primaryKey(),
1033
+ tableId: text("table_id")
1034
+ .references(() => userTables.id)
1035
+ .notNull(),
1036
+ name: text("name").notNull(),
1037
+ triggerEvent: text("trigger_event", {
1038
+ enum: ["row_added", "row_updated", "row_deleted"],
1039
+ }).notNull(),
1040
+ /** JSON condition using filter format (null = always fire) */
1041
+ condition: text("condition"),
1042
+ actionType: text("action_type", {
1043
+ enum: ["run_workflow", "create_task"],
1044
+ }).notNull(),
1045
+ /** JSON config: { workflowId } or { title, description, projectId } */
1046
+ actionConfig: text("action_config").notNull(),
1047
+ status: text("status", { enum: ["active", "paused"] })
1048
+ .default("active")
1049
+ .notNull(),
1050
+ fireCount: integer("fire_count").default(0).notNull(),
1051
+ lastFiredAt: integer("last_fired_at", { mode: "timestamp" }),
1052
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
1053
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
1054
+ },
1055
+ (table) => [
1056
+ index("idx_user_table_triggers_table_id").on(table.tableId),
1057
+ index("idx_user_table_triggers_status").on(table.status),
1058
+ ]
1059
+ );
1060
+
1061
+ // ── Table Row Version History ────────────────────────────────────────
1062
+
1063
+ export const userTableRowHistory = sqliteTable(
1064
+ "user_table_row_history",
1065
+ {
1066
+ id: text("id").primaryKey(),
1067
+ rowId: text("row_id").notNull(),
1068
+ tableId: text("table_id")
1069
+ .references(() => userTables.id)
1070
+ .notNull(),
1071
+ /** JSON snapshot of the row data before the change */
1072
+ previousData: text("previous_data").notNull(),
1073
+ changedBy: text("changed_by").default("user"),
1074
+ changeType: text("change_type", { enum: ["update", "delete"] }).notNull(),
1075
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
1076
+ },
1077
+ (table) => [
1078
+ index("idx_row_history_row_id").on(table.rowId),
1079
+ index("idx_row_history_table_id").on(table.tableId),
1080
+ index("idx_row_history_created_at").on(table.createdAt),
1081
+ ]
1082
+ );
1083
+
681
1084
  // Shared types derived from schema — use these in components instead of `as any`
682
1085
  export type ProjectRow = InferSelectModel<typeof projects>;
683
1086
  export type TaskRow = InferSelectModel<typeof tasks>;
@@ -703,3 +1106,45 @@ export type ReadingProgressRow = InferSelectModel<typeof readingProgress>;
703
1106
  export type BookmarkRow = InferSelectModel<typeof bookmarks>;
704
1107
  export type RepoImportRow = InferSelectModel<typeof repoImports>;
705
1108
  export type AgentMessageRow = InferSelectModel<typeof agentMessages>;
1109
+ export type UserTableRow = InferSelectModel<typeof userTables>;
1110
+ export type UserTableColumnRow = InferSelectModel<typeof userTableColumns>;
1111
+ export type UserTableRowRow = InferSelectModel<typeof userTableRows>;
1112
+ export type UserTableViewRow = InferSelectModel<typeof userTableViews>;
1113
+ export type UserTableRelationshipRow = InferSelectModel<typeof userTableRelationships>;
1114
+ export type UserTableTemplateRow = InferSelectModel<typeof userTableTemplates>;
1115
+ export type UserTableImportRow = InferSelectModel<typeof userTableImports>;
1116
+ export type TableDocumentInputRow = InferSelectModel<typeof tableDocumentInputs>;
1117
+ export type TaskTableInputRow = InferSelectModel<typeof taskTableInputs>;
1118
+ export type WorkflowTableInputRow = InferSelectModel<typeof workflowTableInputs>;
1119
+ export type ScheduleTableInputRow = InferSelectModel<typeof scheduleTableInputs>;
1120
+ export type UserTableTriggerRow = InferSelectModel<typeof userTableTriggers>;
1121
+ export type UserTableRowHistoryRow = InferSelectModel<typeof userTableRowHistory>;
1122
+
1123
+ // ── Snapshots ──────────────────────────────────────────────────────────
1124
+
1125
+ export const snapshots = sqliteTable(
1126
+ "snapshots",
1127
+ {
1128
+ id: text("id").primaryKey(),
1129
+ label: text("label").notNull(),
1130
+ type: text("type", { enum: ["manual", "auto"] })
1131
+ .default("manual")
1132
+ .notNull(),
1133
+ status: text("status", { enum: ["in_progress", "completed", "failed"] })
1134
+ .default("in_progress")
1135
+ .notNull(),
1136
+ filePath: text("file_path").notNull(),
1137
+ sizeBytes: integer("size_bytes").default(0).notNull(),
1138
+ dbSizeBytes: integer("db_size_bytes").default(0).notNull(),
1139
+ filesSizeBytes: integer("files_size_bytes").default(0).notNull(),
1140
+ fileCount: integer("file_count").default(0).notNull(),
1141
+ error: text("error"),
1142
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
1143
+ },
1144
+ (table) => [
1145
+ index("idx_snapshots_type").on(table.type),
1146
+ index("idx_snapshots_created_at").on(table.createdAt),
1147
+ ]
1148
+ );
1149
+
1150
+ export type SnapshotRow = InferSelectModel<typeof snapshots>;
@@ -1,12 +1,11 @@
1
1
  import { readFileSync, readdirSync, existsSync } from "fs";
2
2
  import { join, basename } from "path";
3
3
  import type { DocManifest, ParsedDoc } from "./types";
4
+ import { getAppRoot } from "../utils/app-root";
4
5
 
5
6
  /** Resolve the docs directory relative to this source file (npx-safe) */
6
7
  function docsDir(): string {
7
- const dir = import.meta.dirname ?? __dirname;
8
- // src/lib/docs/ → project root → docs/
9
- return join(dir, "..", "..", "..", "docs");
8
+ return join(getAppRoot(import.meta.dirname, 3), "docs");
10
9
  }
11
10
 
12
11
  /** Read and parse docs/manifest.json */
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { db } from "@/lib/db";
7
- import { documents } from "@/lib/db/schema";
8
- import { and, eq } from "drizzle-orm";
7
+ import { documents, workflowDocumentInputs } from "@/lib/db/schema";
8
+ import { and, eq, inArray, isNull } from "drizzle-orm";
9
9
  import type { DocumentRow } from "@/lib/db/schema";
10
10
 
11
11
  const MAX_INLINE_TEXT = 10_000;
@@ -114,3 +114,76 @@ export async function buildWorkflowDocumentContext(
114
114
  return null;
115
115
  }
116
116
  }
117
+
118
+ /**
119
+ * Build document context from the workflow document pool (junction table).
120
+ * Queries workflow_document_inputs for documents bound to this workflow,
121
+ * optionally scoped to a specific step. Returns null if no pool documents.
122
+ *
123
+ * Includes both workflow-level bindings (stepId=null) and step-specific bindings.
124
+ */
125
+ export async function buildPoolDocumentContext(
126
+ workflowId: string,
127
+ stepId?: string
128
+ ): Promise<string | null> {
129
+ try {
130
+ // Get workflow-level (stepId=null) bindings — available to all steps
131
+ const globalBindings = await db
132
+ .select({ documentId: workflowDocumentInputs.documentId })
133
+ .from(workflowDocumentInputs)
134
+ .where(
135
+ and(
136
+ eq(workflowDocumentInputs.workflowId, workflowId),
137
+ isNull(workflowDocumentInputs.stepId)
138
+ )
139
+ );
140
+
141
+ // If a specific step, also get step-scoped bindings
142
+ let stepBindings: { documentId: string }[] = [];
143
+ if (stepId) {
144
+ stepBindings = await db
145
+ .select({ documentId: workflowDocumentInputs.documentId })
146
+ .from(workflowDocumentInputs)
147
+ .where(
148
+ and(
149
+ eq(workflowDocumentInputs.workflowId, workflowId),
150
+ eq(workflowDocumentInputs.stepId, stepId)
151
+ )
152
+ );
153
+ }
154
+
155
+ // Deduplicate document IDs
156
+ const docIdSet = new Set<string>();
157
+ for (const b of [...globalBindings, ...stepBindings]) {
158
+ docIdSet.add(b.documentId);
159
+ }
160
+
161
+ if (docIdSet.size === 0) return null;
162
+
163
+ const docs = await db
164
+ .select()
165
+ .from(documents)
166
+ .where(inArray(documents.id, [...docIdSet]));
167
+
168
+ if (docs.length === 0) return null;
169
+
170
+ const sections = docs.map((doc, i) => formatDocument(doc, i));
171
+ let result = sections.join("\n\n");
172
+
173
+ if (result.length > MAX_WORKFLOW_DOC_CONTEXT) {
174
+ result = result.slice(0, MAX_WORKFLOW_DOC_CONTEXT);
175
+ result += `\n\n(Pool document context truncated at ${MAX_WORKFLOW_DOC_CONTEXT} chars — use Read tool for full content)`;
176
+ }
177
+
178
+ return [
179
+ "--- Workflow Pool Documents ---",
180
+ "",
181
+ result,
182
+ "",
183
+ "--- End Workflow Pool Documents ---",
184
+ ].join("\n");
185
+ } catch (error) {
186
+ console.error("[context-builder] Failed to build pool document context:", error);
187
+ return null;
188
+ }
189
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Resolve document selectors against the project document pool.
3
+ * Used at workflow creation time for auto-discovery of relevant documents.
4
+ */
5
+
6
+ import { db } from "@/lib/db";
7
+ import { documents, workflows, tasks } from "@/lib/db/schema";
8
+ import { and, eq, desc, like, inArray } from "drizzle-orm";
9
+ import type { DocumentRow } from "@/lib/db/schema";
10
+ import type { DocumentSelector } from "@/lib/workflows/types";
11
+
12
+ /**
13
+ * Resolve a DocumentSelector against the project pool, returning matching documents.
14
+ * Used for auto-discovery at workflow creation time (not at execution time).
15
+ */
16
+ export async function resolveDocumentSelector(
17
+ projectId: string,
18
+ selector: DocumentSelector
19
+ ): Promise<DocumentRow[]> {
20
+ const conditions = [eq(documents.projectId, projectId)];
21
+
22
+ if (selector.direction) {
23
+ conditions.push(eq(documents.direction, selector.direction));
24
+ }
25
+
26
+ if (selector.category) {
27
+ conditions.push(eq(documents.category, selector.category));
28
+ }
29
+
30
+ if (selector.mimeType) {
31
+ conditions.push(eq(documents.mimeType, selector.mimeType));
32
+ }
33
+
34
+ if (selector.namePattern) {
35
+ // Convert glob pattern to SQL LIKE: * → %, ? → _
36
+ const likePattern = selector.namePattern
37
+ .replace(/\*/g, "%")
38
+ .replace(/\?/g, "_");
39
+ conditions.push(like(documents.originalName, likePattern));
40
+ }
41
+
42
+ // Filter by source workflow (via task → workflow relationship)
43
+ if (selector.fromWorkflowId) {
44
+ const workflowTaskIds = await db
45
+ .select({ id: tasks.id })
46
+ .from(tasks)
47
+ .where(eq(tasks.workflowId, selector.fromWorkflowId));
48
+
49
+ const taskIds = workflowTaskIds.map((t) => t.id);
50
+ if (taskIds.length === 0) return [];
51
+ conditions.push(inArray(documents.taskId, taskIds));
52
+ } else if (selector.fromWorkflowName) {
53
+ // Look up workflow by name, then get its task IDs
54
+ const matchingWorkflows = await db
55
+ .select({ id: workflows.id })
56
+ .from(workflows)
57
+ .where(
58
+ and(
59
+ eq(workflows.projectId, projectId),
60
+ like(workflows.name, `%${selector.fromWorkflowName}%`)
61
+ )
62
+ );
63
+
64
+ if (matchingWorkflows.length === 0) return [];
65
+
66
+ const wfIds = matchingWorkflows.map((w) => w.id);
67
+ const workflowTaskIds = await db
68
+ .select({ id: tasks.id })
69
+ .from(tasks)
70
+ .where(inArray(tasks.workflowId, wfIds));
71
+
72
+ const taskIds = workflowTaskIds.map((t) => t.id);
73
+ if (taskIds.length === 0) return [];
74
+ conditions.push(inArray(documents.taskId, taskIds));
75
+ }
76
+
77
+ // Only return ready documents (processed and available)
78
+ conditions.push(eq(documents.status, "ready"));
79
+
80
+ let query = db
81
+ .select()
82
+ .from(documents)
83
+ .where(and(...conditions))
84
+ .orderBy(desc(documents.createdAt));
85
+
86
+ if (selector.latest) {
87
+ query = query.limit(selector.latest) as typeof query;
88
+ }
89
+
90
+ return query;
91
+ }
92
+
93
+ /**
94
+ * Get all output documents from completed workflows in a project.
95
+ * Useful for browsing the project document pool.
96
+ */
97
+ export async function getProjectDocumentPool(
98
+ projectId: string,
99
+ options?: { direction?: "input" | "output"; search?: string }
100
+ ): Promise<DocumentRow[]> {
101
+ const conditions = [
102
+ eq(documents.projectId, projectId),
103
+ eq(documents.status, "ready"),
104
+ ];
105
+
106
+ if (options?.direction) {
107
+ conditions.push(eq(documents.direction, options.direction));
108
+ }
109
+
110
+ if (options?.search) {
111
+ conditions.push(like(documents.originalName, `%${options.search}%`));
112
+ }
113
+
114
+ return db
115
+ .select()
116
+ .from(documents)
117
+ .where(and(...conditions))
118
+ .orderBy(desc(documents.createdAt));
119
+ }
@@ -6,7 +6,8 @@ export async function processSpreadsheet(filePath: string): Promise<ProcessorRes
6
6
  const ExcelJS = await import("exceljs");
7
7
  const workbook = new ExcelJS.Workbook();
8
8
  const buffer = await readFile(filePath);
9
- await workbook.xlsx.load(buffer as unknown as Buffer);
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ await workbook.xlsx.load(buffer as any);
10
11
 
11
12
  const sheets: string[] = [];
12
13
  workbook.eachSheet((worksheet) => {