spine-framework 0.3.145 → 0.4.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.
- package/.env.example +76 -0
- package/.framework/.spine-manifest.json +19 -0
- package/.framework/cli/commands/agents.ts +61 -33
- package/.framework/cli/commands/auth.ts +60 -28
- package/.framework/cli/commands/create-app.ts +212 -80
- package/.framework/cli/commands/dev.ts +184 -68
- package/.framework/cli/commands/doctor.ts +324 -152
- package/.framework/cli/commands/generate.ts +119 -68
- package/.framework/cli/commands/init.ts +141 -242
- package/.framework/cli/commands/install-app.ts +1336 -314
- package/.framework/cli/commands/items.ts +31 -29
- package/.framework/cli/commands/migrate.ts +299 -156
- package/.framework/cli/commands/migrations.ts +62 -30
- package/.framework/cli/commands/pipelines.ts +31 -29
- package/.framework/cli/commands/status.ts +105 -45
- package/.framework/cli/commands/system.ts +47 -42
- package/.framework/cli/commands/test.ts +161 -102
- package/.framework/cli/commands/upgrade.ts +260 -0
- package/.framework/cli/commands/validate-app.ts +517 -161
- package/.framework/cli/context.ts +117 -85
- package/.framework/cli/env-loader.ts +74 -43
- package/.framework/cli/index.ts +70 -46
- package/.framework/cli/lib/project-files.ts +404 -0
- package/.framework/docs/guides/testing-guide.md +1 -1
- package/.framework/docs/runtime-types.md +72 -72
- package/.framework/functions/_shared/access.ts +84 -0
- package/.framework/functions/_shared/agent-runner.ts +5067 -1026
- package/.framework/functions/_shared/app-manifest.ts +214 -94
- package/.framework/functions/_shared/ast.ts +214 -0
- package/.framework/functions/_shared/audit.ts +91 -55
- package/.framework/functions/_shared/cil.ts +368 -127
- package/.framework/functions/_shared/db.ts +337 -68
- package/.framework/functions/_shared/funnel.ts +228 -0
- package/.framework/functions/_shared/geo.ts +105 -0
- package/.framework/functions/_shared/hash-api-key.ts +94 -17
- package/.framework/functions/_shared/index.ts +22 -224
- package/.framework/functions/_shared/kb.ts +832 -58
- package/.framework/functions/_shared/llm.ts +118 -18
- package/.framework/functions/_shared/logging.ts +317 -19
- package/.framework/functions/_shared/middleware.ts +701 -131
- package/.framework/functions/_shared/notify.ts +70 -0
- package/.framework/functions/_shared/outcomes.ts +34 -19
- package/.framework/functions/_shared/permissions.ts +501 -315
- package/.framework/functions/_shared/pipeline-runner.ts +704 -155
- package/.framework/functions/_shared/principal.ts +436 -263
- package/.framework/functions/_shared/rate-limit.ts +233 -0
- package/.framework/functions/_shared/resolve-credentials.ts +86 -19
- package/.framework/functions/_shared/resolve-ids.ts +203 -139
- package/.framework/functions/_shared/retry-deliveries.ts +51 -16
- package/.framework/functions/_shared/schema-utils.ts +602 -521
- package/.framework/functions/_shared/security.ts +345 -0
- package/.framework/functions/_shared/testing.ts +197 -172
- package/.framework/functions/_shared/tracing.ts +351 -21
- package/.framework/functions/_shared/trigger-engine.ts +425 -136
- package/.framework/functions/_shared/webhook-registration.ts +71 -70
- package/.framework/functions/_shared/webhook-registry.ts +126 -83
- package/.framework/functions/account-nodes.ts +30 -30
- package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
- package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
- package/.framework/functions/adapters/openai-outbound.ts +15 -16
- package/.framework/functions/admin-data.ts +596 -173
- package/.framework/functions/admin-funnel.ts +64 -60
- package/.framework/functions/admin-install-funnel.ts +84 -66
- package/.framework/functions/admin-signals.ts +69 -51
- package/.framework/functions/ai-agents.ts +431 -106
- package/.framework/functions/ai-interactions.ts +186 -112
- package/.framework/functions/api-keys.ts +107 -107
- package/.framework/functions/apps.ts +154 -136
- package/.framework/functions/auth.ts +83 -65
- package/.framework/functions/cil-agent-ingest.ts +189 -232
- package/.framework/functions/cil-agent.ts +433 -177
- package/.framework/functions/cil-callmap.ts +642 -430
- package/.framework/functions/cil-config.ts +139 -135
- package/.framework/functions/cil-context.ts +214 -127
- package/.framework/functions/cil-doc.ts +587 -458
- package/.framework/functions/cil-embeddings.ts +453 -364
- package/.framework/functions/cil-graph.ts +427 -142
- package/.framework/functions/cil-ingest.ts +1418 -1154
- package/.framework/functions/cil-pipeline.ts +276 -21
- package/.framework/functions/cil-project.ts +659 -132
- package/.framework/functions/cil-validate.ts +198 -164
- package/.framework/functions/debug-auth.ts +57 -55
- package/.framework/functions/deliveries.ts +80 -21
- package/.framework/functions/embeddings.ts +1023 -318
- package/.framework/functions/eval-harness.ts +146 -108
- package/.framework/functions/integration-outbound.ts +205 -75
- package/.framework/functions/integration-routes.ts +128 -23
- package/.framework/functions/integrations.ts +125 -100
- package/.framework/functions/invites.ts +1095 -460
- package/.framework/functions/item-progress.ts +158 -102
- package/.framework/functions/kb.ts +34 -10
- package/.framework/functions/logs.ts +133 -133
- package/.framework/functions/observability.ts +152 -122
- package/.framework/functions/pipeline-executions.ts +147 -132
- package/.framework/functions/pipelines.ts +134 -119
- package/.framework/functions/prompt-configs.ts +96 -80
- package/.framework/functions/roles.ts +112 -82
- package/.framework/functions/security.ts +450 -0
- package/.framework/functions/system-cron.ts +117 -80
- package/.framework/functions/system.ts +158 -98
- package/.framework/functions/tests.ts +29 -29
- package/.framework/functions/timers.ts +94 -95
- package/.framework/functions/trigger-scheduler.ts +10 -0
- package/.framework/functions/triggers.ts +179 -149
- package/.framework/functions/types.ts +137 -125
- package/.framework/mcp/index.ts +133 -0
- package/.framework/mcp/load-env.ts +60 -0
- package/.framework/mcp/shared/app-manifest.ts +275 -0
- package/.framework/mcp/shared/db.ts +477 -0
- package/.framework/mcp/shared/kb.ts +118 -0
- package/.framework/mcp/tools/architect.ts +431 -0
- package/.framework/mcp/tools/cil/context.ts +345 -0
- package/.framework/mcp/tools/cil/graph.ts +242 -0
- package/.framework/mcp/tools/cil/ingest.ts +1217 -0
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
- package/.framework/mcp/tools/cil/pipeline.ts +767 -0
- package/.framework/mcp/tools/cil/project.ts +791 -0
- package/.framework/mcp/tools/cil/retrieval.ts +336 -0
- package/.framework/mcp/tools/cil/search-core.ts +454 -0
- package/.framework/mcp/tools/cil/search.ts +97 -0
- package/.framework/mcp/tools/cil/suggest.ts +186 -0
- package/.framework/mcp/tools/cil/validate.ts +1369 -0
- package/.framework/mcp/tools/kb/delete.ts +139 -0
- package/.framework/mcp/tools/kb/index.ts +52 -0
- package/.framework/mcp/tools/kb/ingest.ts +84 -0
- package/.framework/mcp/tools/kb/list-types.ts +102 -0
- package/.framework/mcp/tools/kb/publish.ts +100 -0
- package/.framework/mcp/tools/kb/reindex.ts +86 -0
- package/.framework/mcp/tools/kb/revoke.ts +85 -0
- package/.framework/mcp/tools/kb/search.ts +101 -0
- package/.framework/mcp/tools/kb/upsert.ts +125 -0
- package/.framework/mcp/tools/observability.ts +510 -0
- package/.framework/migrations/001_core_schema.sql +15 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
- package/.framework/migrations/003_core_system_data.sql +16 -1
- package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
- package/.framework/migrations/005_core_observability.sql +17 -1
- package/.framework/migrations/006_security_hardening_rls.sql +15 -0
- package/.framework/migrations/007_kb_article_list.sql +17 -1
- package/.framework/migrations/008_items_target.sql +15 -0
- package/.framework/migrations/009_roles_slug_unique.sql +15 -0
- package/.framework/migrations/010_auth_user_trigger.sql +15 -0
- package/.framework/migrations/011_spine_system_account.sql +16 -0
- package/.framework/migrations/012_service_role_grants.sql +16 -0
- package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
- package/.framework/migrations/014_apps_required_roles.sql +16 -0
- package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
- package/.framework/migrations/016_cil_types.sql +281 -0
- package/.framework/migrations/017_cil_stable_ids.sql +37 -0
- package/.framework/migrations/018_runtime_tables.sql +129 -0
- package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
- package/.framework/migrations/020_schema_reconciliation.sql +67 -0
- package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
- package/.framework/migrations/032_cil_status_migration.sql +73 -0
- package/.framework/migrations/033_cil_versioning.sql +73 -0
- package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
- package/.framework/migrations/035_rate_limits.sql +55 -0
- package/.framework/migrations/036_demo_record_flags.sql +72 -0
- package/.framework/migrations/037_kb_types_canonical.sql +244 -0
- package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
- package/.framework/migrations/042_registration_hierarchy.sql +309 -0
- package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
- package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
- package/.framework/migrations/045_security_critical_rls.sql +139 -0
- package/.framework/migrations/046_security_missing_rls.sql +79 -0
- package/.framework/migrations/047_people_updated_by.sql +27 -0
- package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
- package/.framework/migrations/049_security_blocklist.sql +148 -0
- package/.framework/migrations/050_notifications.sql +49 -0
- package/.framework/migrations/051_message_agent_fields.sql +74 -0
- package/.framework/migrations/052_registration_security.sql +421 -0
- package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
- package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
- package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
- package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
- package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
- package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
- package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
- package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
- package/.framework/migrations/061_fk_dedup.sql +32 -0
- package/.framework/public/spine-logo.svg +10 -3
- package/.framework/scripts/app-install-cli.ts +190 -93
- package/.framework/scripts/assemble-frontend.sh +127 -44
- package/.framework/scripts/assemble-functions.sh +128 -30
- package/.framework/scripts/boundary-check.sh +6 -2
- package/.framework/scripts/build-manifest.sh +24 -68
- package/.framework/scripts/check-core-integrity.sh +15 -76
- package/.framework/scripts/checksum-manifest.mts +343 -0
- package/.framework/scripts/cil-migrate-markers.ts +403 -0
- package/.framework/scripts/fix-cil-markers.mjs +176 -0
- package/.framework/scripts/verify-integrity.sh +21 -57
- package/.framework/scripts/verify-manifest.mts +305 -0
- package/.framework/seeds/types/funnel_signal_use.json +74 -21
- package/.framework/seeds/types/spine_instance.json +73 -21
- package/.framework/src/App.tsx +118 -72
- package/.framework/src/apps/admin/index.tsx +855 -58
- package/.framework/src/components/AppWrapper.tsx +43 -45
- package/.framework/src/components/CustomAppLoader.tsx +160 -116
- package/.framework/src/components/admin/AdminListPage.tsx +46 -44
- package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
- package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
- package/.framework/src/components/admin/PageInstructions.tsx +98 -2
- package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
- package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
- package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
- package/.framework/src/components/app-sidebar.tsx +42 -45
- package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
- package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
- package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
- package/.framework/src/components/kb/KBArticleList.tsx +251 -10
- package/.framework/src/components/layout/AppShell.tsx +51 -45
- package/.framework/src/components/layout/Header.tsx +42 -43
- package/.framework/src/components/layout/Layout.tsx +46 -46
- package/.framework/src/components/layout/Sidebar.tsx +196 -96
- package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
- package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
- package/.framework/src/components/runtime/DataFilters.tsx +49 -74
- package/.framework/src/components/runtime/DataHeader.tsx +53 -32
- package/.framework/src/components/runtime/DataListPage.tsx +51 -155
- package/.framework/src/components/runtime/DataStats.tsx +66 -44
- package/.framework/src/components/runtime/DataTable.tsx +49 -149
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
- package/.framework/src/components/runtime/index.ts +119 -16
- package/.framework/src/components/search-form.tsx +46 -29
- package/.framework/src/components/shared/AgentView.tsx +65 -48
- package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
- package/.framework/src/components/shared/InviteManager.tsx +207 -97
- package/.framework/src/components/shared/MemberList.tsx +97 -0
- package/.framework/src/components/shared/SchemaFields.tsx +107 -86
- package/.framework/src/components/ui/DataTable.tsx +80 -68
- package/.framework/src/components/ui/Form.tsx +157 -146
- package/.framework/src/components/ui/ItemCard.tsx +74 -67
- package/.framework/src/components/ui/ItemListView.tsx +73 -68
- package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
- package/.framework/src/components/ui/Modal.tsx +78 -68
- package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
- package/.framework/src/components/ui/accordion.tsx +85 -86
- package/.framework/src/components/ui/alert-dialog.tsx +197 -184
- package/.framework/src/components/ui/alert.tsx +105 -94
- package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
- package/.framework/src/components/ui/avatar.tsx +122 -107
- package/.framework/src/components/ui/badge.tsx +60 -55
- package/.framework/src/components/ui/breadcrumb.tsx +127 -128
- package/.framework/src/components/ui/button-group.tsx +86 -72
- package/.framework/src/components/ui/button.tsx +71 -50
- package/.framework/src/components/ui/calendar.tsx +72 -57
- package/.framework/src/components/ui/card.tsx +136 -129
- package/.framework/src/components/ui/carousel.tsx +226 -107
- package/.framework/src/components/ui/chart.tsx +252 -147
- package/.framework/src/components/ui/checkbox.tsx +43 -44
- package/.framework/src/components/ui/collapsible.tsx +86 -71
- package/.framework/src/components/ui/combobox.tsx +282 -249
- package/.framework/src/components/ui/command.tsx +158 -146
- package/.framework/src/components/ui/context-menu.tsx +239 -240
- package/.framework/src/components/ui/dialog.tsx +174 -157
- package/.framework/src/components/ui/direction.tsx +72 -56
- package/.framework/src/components/ui/drawer.tsx +168 -169
- package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
- package/.framework/src/components/ui/empty.tsx +131 -120
- package/.framework/src/components/ui/field.tsx +205 -173
- package/.framework/src/components/ui/hover-card.tsx +71 -72
- package/.framework/src/components/ui/input-group.tsx +148 -133
- package/.framework/src/components/ui/input-otp.tsx +99 -86
- package/.framework/src/components/ui/input.tsx +43 -41
- package/.framework/src/components/ui/item.tsx +207 -185
- package/.framework/src/components/ui/kbd.tsx +57 -56
- package/.framework/src/components/ui/label.tsx +45 -41
- package/.framework/src/components/ui/menubar.tsx +268 -236
- package/.framework/src/components/ui/native-select.tsx +85 -86
- package/.framework/src/components/ui/navigation-menu.tsx +166 -156
- package/.framework/src/components/ui/pagination.tsx +131 -141
- package/.framework/src/components/ui/popover.tsx +125 -128
- package/.framework/src/components/ui/progress.tsx +47 -44
- package/.framework/src/components/ui/radio-group.tsx +60 -55
- package/.framework/src/components/ui/resizable.tsx +85 -72
- package/.framework/src/components/ui/scroll-area.tsx +57 -58
- package/.framework/src/components/ui/select.tsx +182 -169
- package/.framework/src/components/ui/separator.tsx +42 -43
- package/.framework/src/components/ui/sheet.tsx +170 -159
- package/.framework/src/components/ui/sidebar.tsx +395 -398
- package/.framework/src/components/ui/skeleton.tsx +42 -43
- package/.framework/src/components/ui/slider.tsx +61 -44
- package/.framework/src/components/ui/sonner.tsx +44 -44
- package/.framework/src/components/ui/spinner.tsx +47 -42
- package/.framework/src/components/ui/switch.tsx +56 -42
- package/.framework/src/components/ui/table-primitives.tsx +272 -132
- package/.framework/src/components/ui/table.tsx +140 -142
- package/.framework/src/components/ui/tabs.tsx +110 -93
- package/.framework/src/components/ui/textarea.tsx +43 -44
- package/.framework/src/components/ui/toggle-group.tsx +87 -71
- package/.framework/src/components/ui/toggle.tsx +56 -56
- package/.framework/src/components/ui/tooltip.tsx +99 -85
- package/.framework/src/contexts/AppContext.tsx +166 -114
- package/.framework/src/contexts/AuthContext.tsx +232 -137
- package/.framework/src/hooks/use-mobile.ts +43 -44
- package/.framework/src/hooks/useApi.ts +185 -172
- package/.framework/src/hooks/useAppPath.ts +31 -29
- package/.framework/src/hooks/useApps.ts +77 -42
- package/.framework/src/hooks/useEntityList.ts +66 -29
- package/.framework/src/hooks/useEntityRecord.ts +76 -32
- package/.framework/src/hooks/useForm.ts +62 -30
- package/.framework/src/hooks/useListSchema.ts +60 -43
- package/.framework/src/hooks/useSchemaRecord.ts +115 -95
- package/.framework/src/lib/api.ts +258 -134
- package/.framework/src/lib/registration.ts +181 -0
- package/.framework/src/lib/supabase.ts +118 -81
- package/.framework/src/lib/utils.ts +266 -266
- package/.framework/src/main.tsx +43 -44
- package/.framework/src/pages/DashboardPage.tsx +44 -43
- package/.framework/src/pages/NoAppPage.tsx +50 -0
- package/.framework/src/pages/NotFoundPage.tsx +32 -29
- package/.framework/src/pages/SetupPage.tsx +34 -33
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
- package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
- package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
- package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
- package/.framework/src/pages/admin/AppsPage.tsx +45 -42
- package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
- package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
- package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
- package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
- package/.framework/src/pages/admin/LogsPage.tsx +87 -137
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
- package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
- package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
- package/.framework/src/pages/admin/RolesPage.tsx +47 -164
- package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
- package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
- package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
- package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
- package/.framework/src/pages/admin/TimersPage.tsx +44 -42
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
- package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
- package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
- package/.framework/src/pages/admin/TypesPage.tsx +45 -42
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
- package/.framework/src/pages/auth/LoginPage.tsx +38 -32
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
- package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
- package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
- package/.framework/src/types/auth.ts +101 -93
- package/.framework/src/types/types.ts +283 -262
- package/.framework/templates/project.json +87 -0
- package/.framework/templates/spine.config.neutral.json +7 -0
- package/.framework/tests/api/api-surface.test.ts +381 -0
- package/.framework/tests/fixtures/seed.ts +427 -0
- package/.framework/tests/fixtures/teardown.ts +228 -0
- package/.framework/tests/reporter.ts +351 -0
- package/.framework/tests/setup.ts +135 -0
- package/.framework/tests/ui/ui-sweep.ts +278 -0
- package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
- package/.framework/tests/unit/agent-runner.test.ts +2389 -0
- package/.framework/tests/unit/ai-interactions.test.ts +81 -0
- package/.framework/tests/unit/app-manifest.test.ts +359 -0
- package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
- package/.framework/tests/unit/cil-determinism.test.ts +85 -0
- package/.framework/tests/unit/core-isolation.test.ts +153 -0
- package/.framework/tests/unit/funnel-signals.test.ts +135 -0
- package/.framework/tests/unit/install-app.test.ts +1759 -0
- package/.framework/tests/unit/invites-security.test.ts +572 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
- package/.framework/tests/unit/kb-search.test.ts +119 -0
- package/.framework/tests/unit/migrate.test.ts +509 -0
- package/.framework/tests/unit/notify.test.ts +136 -0
- package/.framework/tests/unit/permissions.test.ts +269 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
- package/.framework/tests/unit/principal-cache.test.ts +138 -0
- package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
- package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
- package/.framework/tests/unit/schema-utils.test.ts +225 -0
- package/.framework/tests/unit/trigger-engine.test.ts +513 -0
- package/.framework/tests/unit/validate-app.test.ts +527 -0
- package/README.md +34 -3
- package/bin/spine-framework.cjs +1 -0
- package/config/tailwind.config.cjs +7 -0
- package/config/tsconfig.functions.json +42 -0
- package/config/tsconfig.json +37 -9
- package/config/vite.config.ts +31 -2
- package/netlify.toml +31 -3
- package/package.json +46 -29
- package/scripts/assemble-frontend.sh +127 -44
- package/scripts/assemble-functions.sh +127 -29
- package/scripts/boundary-check.sh +6 -2
- package/scripts/build-manifest.sh +24 -68
- package/scripts/checksum-manifest.mts +345 -0
- package/scripts/dev-clean.sh +86 -0
- package/scripts/dev-netlify.sh +49 -0
- package/scripts/netlify-dev-wrapper.sh +2 -0
- package/scripts/verify-install.ts +358 -0
- package/scripts/verify-integrity.sh +21 -57
- package/scripts/verify-manifest.mts +305 -0
- package/templates/project/gitignore +45 -0
- package/{config → templates/project}/package-lock.json +22335 -9366
- package/vitest.config.ts +46 -0
- package/.framework/public/spine.config.json +0 -91
- package/config/deno.lock +0 -108
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/vite.config.d.ts +0 -2
- package/custom/.devin/AGENT.md +0 -357
- package/custom/.devin/skills/install-app/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
- package/custom/.devin/workflows/app-install.yaml +0 -83
- package/custom/README.md +0 -79
- package/docs/README.md +0 -15
- package/scripts/app-install-cli.ts +0 -286
- package/scripts/check-core-integrity.sh +0 -82
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/kb-ingest-docs.ts +0 -203
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/test-login.js +0 -33
- package/spine.config.json +0 -91
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
// CIL[BEGIN stable_id="~/.framework/mcp/tools/cil/ingest.ts:mcp_ingest_tools:file" type="file" identity="mcp_ingest_tools" parent="~/.framework/mcp/tools/cil/ingest.ts"]
|
|
2
|
+
// CIL[DOC-MACRO purpose="Register all CIL ingest, discovery, chunking, embedding, mapping, and pipeline orchestration MCP tools on a shared McpServer"]
|
|
3
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Expose the MCP-facing surface for CIL ingestion, embedding, mapping, and pipeline orchestration"]
|
|
4
|
+
// CIL[DOC-CONTEXT module="cil-ingest" layer="mcp-tools" related_files=".framework/functions/_shared/cil.ts,.framework/mcp/shared/db.ts,.framework/mcp/tools/cil/ingest.ts"]
|
|
5
|
+
// CIL[DOC-CONTRACT inputs="McpServer instance, raw chunk content strings" outputs="Registered tool handlers" errors="Database and function call failures" side_effects="Registers handlers on McpServer; reads/writes CIL project, chunk, and file records"]
|
|
6
|
+
// CIL[DOC-MICRO logic_summary="Binds one handler per CIL tool, delegating to adminDb, fetchFunction, and helpers for chunking, embedding, mapping, and pipeline steps" notable_decisions="Uses helper normaliseAgentChunkContent for chunk hashing; pipeline steps 3-8 dispatch to cil-doc, cil-embeddings, cil-callmap, and cil-config functions"]
|
|
7
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
|
|
8
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
9
|
+
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import { adminDb, resolveLinkTypeId, resolveTypeId, fetchFunction } from '../../shared/db.js'
|
|
13
|
+
import { handler as callmapHandler } from '../../../functions/cil-callmap.js'
|
|
14
|
+
import { SYSTEM_PRINCIPAL } from '../../../functions/_shared/principal.js'
|
|
15
|
+
import { adminDb as functionsAdminDb } from '../../../functions/_shared/db.js'
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import crypto from 'node:crypto'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
import { computeContentHash, isSelectedPath } from '../../../functions/_shared/cil.js'
|
|
20
|
+
|
|
21
|
+
function normaliseAgentChunkContent(raw: string): string {
|
|
22
|
+
const lines = raw.replace(/\r\n/g, '\n').split('\n')
|
|
23
|
+
const filtered = lines.filter(line => {
|
|
24
|
+
const trimmed = line.trim()
|
|
25
|
+
return !trimmed.match(/^(?:(?:\/\/|--|#)\s*|<!--\s*)CIL\[(BEGIN|END)/) && !trimmed.match(/^(?:(?:\/\/|--|#)\s*|<!--\s*)CIL\[DOC-/)
|
|
26
|
+
})
|
|
27
|
+
const collapsed: string[] = []
|
|
28
|
+
let lastBlank = false
|
|
29
|
+
for (const line of filtered) {
|
|
30
|
+
const isBlank = line.trim() === ''
|
|
31
|
+
if (isBlank && lastBlank) continue
|
|
32
|
+
collapsed.push(line)
|
|
33
|
+
lastBlank = isBlank
|
|
34
|
+
}
|
|
35
|
+
return collapsed.join('\n').trim()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function computeChunkHash(content: string): string {
|
|
39
|
+
return crypto.createHash('sha256').update(normaliseAgentChunkContent(content), 'utf8').digest('hex').slice(0, 16)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function computeFileHash(content: string): string {
|
|
43
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getChunkByStableId(projectId: string, stableId: string, versionId?: string) {
|
|
47
|
+
let q = adminDb.from('items').select('id, title, data')
|
|
48
|
+
.filter('data->>kb_type', 'eq', 'cil_chunk')
|
|
49
|
+
.filter('data->>project_id', 'eq', projectId)
|
|
50
|
+
.filter('data->>stable_id', 'eq', stableId)
|
|
51
|
+
.eq('is_active', true)
|
|
52
|
+
if (versionId) q = (q as any).filter('data->>version_id', 'eq', versionId)
|
|
53
|
+
const { data } = await (q as any).maybeSingle()
|
|
54
|
+
return data
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function callCallmapMapProject(
|
|
58
|
+
accountId: string,
|
|
59
|
+
project_id: string,
|
|
60
|
+
file_path: string | undefined,
|
|
61
|
+
force_regenerate: boolean,
|
|
62
|
+
fast: boolean,
|
|
63
|
+
limit: number,
|
|
64
|
+
offset: number
|
|
65
|
+
): Promise<any> {
|
|
66
|
+
const event = {
|
|
67
|
+
requestId: crypto.randomUUID(),
|
|
68
|
+
principal: {
|
|
69
|
+
...SYSTEM_PRINCIPAL,
|
|
70
|
+
accountId,
|
|
71
|
+
provenance: { sourceType: 'mcp', createdBy: null, invokedAt: new Date().toISOString() },
|
|
72
|
+
},
|
|
73
|
+
db: functionsAdminDb,
|
|
74
|
+
accountId,
|
|
75
|
+
appId: null,
|
|
76
|
+
query: { action: 'map_project', method: 'POST' },
|
|
77
|
+
httpMethod: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
requestPath: '/.netlify/functions/cil-callmap',
|
|
80
|
+
path: '/.netlify/functions/cil-callmap',
|
|
81
|
+
source: 'spine-framework-mcp',
|
|
82
|
+
trace: {},
|
|
83
|
+
body: null,
|
|
84
|
+
} as any
|
|
85
|
+
const body = {
|
|
86
|
+
project_id,
|
|
87
|
+
file_path,
|
|
88
|
+
force_regenerate,
|
|
89
|
+
fast,
|
|
90
|
+
limit,
|
|
91
|
+
offset,
|
|
92
|
+
}
|
|
93
|
+
return callmapHandler(event, body)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function register(server: McpServer) {
|
|
97
|
+
|
|
98
|
+
server.registerTool(
|
|
99
|
+
'spine_cil_detect_changes',
|
|
100
|
+
{
|
|
101
|
+
description: `Detect which selected files have drifted since their last ingestion by comparing per-chunk hashes on disk to stored hashes.
|
|
102
|
+
Returns three lists: drifted (file changed — re-run spine_cil_store_markers), missing (file deleted or renamed — run spine_cil_mark_dead), current (no changes).
|
|
103
|
+
Run this before starting an incremental ingestion pass to build an accurate work queue.`,
|
|
104
|
+
inputSchema: z.object({
|
|
105
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
async ({ project_id }) => {
|
|
109
|
+
const { data: projectRows } = await adminDb.from('items').select('data')
|
|
110
|
+
.filter('data->>kb_type', 'eq', 'cil_project')
|
|
111
|
+
.filter('id', 'eq', project_id)
|
|
112
|
+
.eq('is_active', true)
|
|
113
|
+
|
|
114
|
+
const project = (projectRows || [])[0]
|
|
115
|
+
|
|
116
|
+
if (!project) {
|
|
117
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const repoPath: string = project.data?.repo_path || ''
|
|
121
|
+
|
|
122
|
+
if (!repoPath) {
|
|
123
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Project has no repo_path' }) }] }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const selectedPaths: string[] = project.data?.selected_paths || []
|
|
127
|
+
|
|
128
|
+
if (selectedPaths.length === 0) {
|
|
129
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, drifted: [], missing: [], current: [], message: 'No selected_paths on project' }) }] }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const activeVersionId = project.data?.active_version_id
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
let chunkQuery = adminDb.from('items').select('id, data')
|
|
137
|
+
.filter('data->>kb_type', 'eq', 'cil_chunk')
|
|
138
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
139
|
+
.eq('is_active', true)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if (activeVersionId) {
|
|
143
|
+
chunkQuery = chunkQuery.filter('data->>version_id', 'eq', activeVersionId)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
const { data: chunks } = await chunkQuery
|
|
148
|
+
|
|
149
|
+
const chunksByFile: Record<string, any[]> = {}
|
|
150
|
+
|
|
151
|
+
for (const c of chunks || []) {
|
|
152
|
+
|
|
153
|
+
const fp = c.data?.file_path
|
|
154
|
+
|
|
155
|
+
if (fp) {
|
|
156
|
+
if (!chunksByFile[fp]) chunksByFile[fp] = []
|
|
157
|
+
chunksByFile[fp].push(c)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const drifted: { file_path: string; drifted_chunks: number; total_chunks: number; drifted_chunk_ids?: string[] }[] = []
|
|
163
|
+
|
|
164
|
+
const missing: { file_path: string; reason: string }[] = []
|
|
165
|
+
|
|
166
|
+
const current: { file_path: string; chunk_count: number }[] = []
|
|
167
|
+
|
|
168
|
+
for (const filePath of selectedPaths) {
|
|
169
|
+
|
|
170
|
+
const absolutePath = path.join(repoPath, filePath.replace(/^\//, ''))
|
|
171
|
+
|
|
172
|
+
const fileChunks = chunksByFile[filePath] || []
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(absolutePath)) {
|
|
175
|
+
missing.push({ file_path: filePath, reason: 'file not found on disk' })
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (fileChunks.length === 0) {
|
|
180
|
+
drifted.push({ file_path: filePath, drifted_chunks: 0, total_chunks: 0 })
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const fileContent = fs.readFileSync(absolutePath, 'utf-8')
|
|
185
|
+
|
|
186
|
+
const lines = fileContent.split('\n')
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
let driftCount = 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
const driftedStableIds: string[] = []
|
|
194
|
+
|
|
195
|
+
for (const chunk of fileChunks) {
|
|
196
|
+
|
|
197
|
+
const cd = chunk.data || {}
|
|
198
|
+
|
|
199
|
+
const lineStart = (cd.line_start || 1) - 1
|
|
200
|
+
|
|
201
|
+
const lineEnd = (cd.line_end || 1) - 1
|
|
202
|
+
|
|
203
|
+
if (lineEnd >= lines.length) {
|
|
204
|
+
driftCount++
|
|
205
|
+
driftedStableIds.push(cd.stable_id || cd.legacy_chunk_id || cd.chunk_id || chunk.id)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const extracted = lines.slice(lineStart, lineEnd + 1).join('\n')
|
|
210
|
+
|
|
211
|
+
const computed = cd.content_hash ? computeContentHash(extracted) : computeChunkHash(extracted)
|
|
212
|
+
|
|
213
|
+
const storedHash = cd.content_hash || cd.stored_hash
|
|
214
|
+
|
|
215
|
+
if (computed !== storedHash) {
|
|
216
|
+
driftCount++
|
|
217
|
+
driftedStableIds.push(cd.stable_id || cd.legacy_chunk_id || cd.chunk_id || chunk.id)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if (driftCount > 0) {
|
|
224
|
+
drifted.push({ file_path: filePath, drifted_chunks: driftCount, total_chunks: fileChunks.length, drifted_chunk_ids: driftedStableIds })
|
|
225
|
+
} else {
|
|
226
|
+
current.push({ file_path: filePath, chunk_count: fileChunks.length })
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
content: [{
|
|
232
|
+
type: 'text',
|
|
233
|
+
text: JSON.stringify({
|
|
234
|
+
ok: true,
|
|
235
|
+
project_id,
|
|
236
|
+
summary: { drifted: drifted.length, missing: missing.length, current: current.length },
|
|
237
|
+
drifted,
|
|
238
|
+
missing,
|
|
239
|
+
current,
|
|
240
|
+
}, null, 2),
|
|
241
|
+
}],
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
server.registerTool(
|
|
247
|
+
'spine_cil_discover',
|
|
248
|
+
{
|
|
249
|
+
description: 'Discover source files for a CIL project by walking repo_path with its configured globs and exclude_globs. Creates cil_file records with status 0-discovered and updates the project file_tree.',
|
|
250
|
+
inputSchema: z.object({
|
|
251
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
252
|
+
file_path: z.string().optional().describe('Optional single path or glob to discover within the project'),
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
async ({ project_id, file_path }) => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await fetchFunction('cil-ingest', 'discover', {}, { project_id, file_path })
|
|
258
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, ...result }) }] }
|
|
259
|
+
} catch (err: any) {
|
|
260
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err?.message || String(err) }) }] }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
server.registerTool(
|
|
266
|
+
'spine_cil_mark_dead',
|
|
267
|
+
{
|
|
268
|
+
description: `Mark all chunks for a given file_path as inactive (is_active: false).
|
|
269
|
+
Use this when a file has been deleted or renamed and its chunks should be retired from the index.
|
|
270
|
+
For renames: first call spine_cil_store_markers on the new path to create fresh chunks, then call spine_cil_mark_dead on the old path.
|
|
271
|
+
IMPORTANT: this is irreversible via the MCP — deleted chunks cannot be restored without re-running spine_cil_store_markers.`,
|
|
272
|
+
inputSchema: z.object({
|
|
273
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
274
|
+
file_path: z.string().describe('Relative file path from repo root whose chunks should be marked inactive'),
|
|
275
|
+
reason: z.string().optional().describe('Optional reason for marking dead, e.g. "file deleted", "file renamed to X"'),
|
|
276
|
+
}),
|
|
277
|
+
},
|
|
278
|
+
async ({ project_id, file_path, reason }) => {
|
|
279
|
+
const { data: chunks } = await adminDb.from('items').select('id, data')
|
|
280
|
+
.filter('data->>kb_type', 'eq', 'cil_chunk')
|
|
281
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
282
|
+
.filter('data->>file_path', 'eq', file_path)
|
|
283
|
+
.eq('is_active', true)
|
|
284
|
+
.limit(1000)
|
|
285
|
+
|
|
286
|
+
if (!chunks || chunks.length === 0) {
|
|
287
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, file_path, marked_inactive: 0, message: 'No active chunks found for this file' }) }] }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const now = new Date().toISOString()
|
|
291
|
+
|
|
292
|
+
const errors: string[] = []
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
let marked = 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
for (const chunk of chunks) {
|
|
300
|
+
|
|
301
|
+
const newData = { ...chunk.data, is_dead_code: true, dead_reason: reason || 'marked via spine_cil_mark_dead', dead_at: now }
|
|
302
|
+
|
|
303
|
+
const { error: updateError } = await adminDb.from('items')
|
|
304
|
+
.update({ is_active: false, data: newData, updated_at: now })
|
|
305
|
+
.eq('id', chunk.id)
|
|
306
|
+
if (updateError) errors.push(`${chunk.id}: ${updateError.message}`)
|
|
307
|
+
else marked++
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
content: [{
|
|
313
|
+
type: 'text',
|
|
314
|
+
text: JSON.stringify({
|
|
315
|
+
ok: errors.length === 0,
|
|
316
|
+
project_id,
|
|
317
|
+
file_path,
|
|
318
|
+
marked_inactive: marked,
|
|
319
|
+
reason: reason || 'not specified',
|
|
320
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
321
|
+
}, null, 2),
|
|
322
|
+
}],
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
server.registerTool(
|
|
328
|
+
'spine_cil_generate_embeddings',
|
|
329
|
+
{
|
|
330
|
+
description: `DO NOT call this tool during the pipeline. Steps 6 and 9 (embed) are triggered by the user clicking the UI button — the server runs embeddings automatically and advances file_statuses.
|
|
331
|
+
|
|
332
|
+
This tool exists for direct/manual embedding outside the standard pipeline flow only. If you are running Steps 4, 5, or 8, use spine_cil_store_markers instead.`,
|
|
333
|
+
inputSchema: z.object({
|
|
334
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
335
|
+
file_path: z.string().optional().describe('Relative file path from repo root. If omitted, embeds all approved chunks in the project.'),
|
|
336
|
+
force_regenerate: z.boolean().optional().describe('Force regeneration of existing embeddings'),
|
|
337
|
+
batch_size: z.number().int().min(1).max(50).optional().describe('Batch size for embedding API calls'),
|
|
338
|
+
}),
|
|
339
|
+
},
|
|
340
|
+
async ({ project_id, file_path, force_regenerate = false, batch_size = 20 }) => {
|
|
341
|
+
|
|
342
|
+
const { data: project } = await adminDb.from('items').select('account_id, data')
|
|
343
|
+
.eq('id', project_id).maybeSingle()
|
|
344
|
+
if (!project) {
|
|
345
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const accountId = project.account_id
|
|
349
|
+
|
|
350
|
+
if (!accountId) {
|
|
351
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Project has no account_id' }) }] }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
let query = adminDb.from('items').select('id, title, account_id, data')
|
|
357
|
+
.filter('data->>kb_type', 'eq', 'cil_chunk')
|
|
358
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
359
|
+
.eq('is_active', true)
|
|
360
|
+
.limit(1000)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if (file_path) {
|
|
364
|
+
query = query.filter('data->>file_path', 'eq', file_path)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
const { data: chunks, error: fetchError } = await query
|
|
369
|
+
if (fetchError) {
|
|
370
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch chunks: ${fetchError.message}` }) }] }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!chunks || chunks.length === 0) {
|
|
374
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'No chunks found to embed', project_id, file_path, embedded_count: 0 }) }] }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { embedChunk } = await import('../../../functions/cil-embeddings.ts')
|
|
378
|
+
|
|
379
|
+
const results: any[] = []
|
|
380
|
+
|
|
381
|
+
const errors: string[] = []
|
|
382
|
+
|
|
383
|
+
for (const chunk of chunks) {
|
|
384
|
+
try {
|
|
385
|
+
|
|
386
|
+
const r = await embedChunk(chunk, accountId, force_regenerate)
|
|
387
|
+
|
|
388
|
+
results.push({
|
|
389
|
+
chunk_id: chunk.id,
|
|
390
|
+
chunk_name: chunk.data?.chunk_name || '',
|
|
391
|
+
created: r.created,
|
|
392
|
+
skipped: r.skipped,
|
|
393
|
+
})
|
|
394
|
+
if (r.error) errors.push(`${chunk.id}: ${r.error}`)
|
|
395
|
+
} catch (e: any) {
|
|
396
|
+
errors.push(`${chunk.id}: ${e.message}`)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
content: [{
|
|
403
|
+
type: 'text',
|
|
404
|
+
text: JSON.stringify({
|
|
405
|
+
ok: errors.length === 0,
|
|
406
|
+
project_id,
|
|
407
|
+
file_path: file_path || '(all files)',
|
|
408
|
+
chunk_count: chunks.length,
|
|
409
|
+
processed: results.length,
|
|
410
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
411
|
+
results,
|
|
412
|
+
}, null, 2),
|
|
413
|
+
}],
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
server.registerTool(
|
|
419
|
+
'spine_cil_generate_mappings',
|
|
420
|
+
{
|
|
421
|
+
description: `DO NOT call this tool during the pipeline. Steps 7 and 10 (map) are triggered by the user clicking the UI button — the server runs callgraph analysis automatically and advances file_statuses.
|
|
422
|
+
|
|
423
|
+
This tool exists for direct/manual mapping outside the standard pipeline flow only. If you are running Steps 4, 5, or 8, use spine_cil_store_markers instead.`,
|
|
424
|
+
inputSchema: z.object({
|
|
425
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
426
|
+
file_path: z.string().optional().describe('Limit mapping to a single file. If omitted, maps the whole project.'),
|
|
427
|
+
force_regenerate: z.boolean().optional().describe('Force regeneration of existing links'),
|
|
428
|
+
fast: z.boolean().optional().describe('Skip updating file_statuses and file_tree; only create links.'),
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
async ({ project_id, file_path, force_regenerate = false, fast = false }) => {
|
|
432
|
+
|
|
433
|
+
const { data: project } = await adminDb.from('items').select('account_id, data')
|
|
434
|
+
.eq('id', project_id).maybeSingle()
|
|
435
|
+
if (!project) {
|
|
436
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const accountId = project.account_id
|
|
440
|
+
|
|
441
|
+
if (!accountId) {
|
|
442
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Project has no account_id' }) }] }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
let fileQuery = adminDb.from('items').select('id, data')
|
|
448
|
+
.filter('data->>kb_type', 'eq', 'cil_file')
|
|
449
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
450
|
+
.eq('is_active', true)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
if (file_path) {
|
|
454
|
+
fileQuery = fileQuery.filter('data->>file_path', 'eq', file_path)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
const { data: files, error: fileError } = await fileQuery
|
|
459
|
+
if (fileError) {
|
|
460
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch files: ${fileError.message}` }) }] }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!files || files.length === 0) {
|
|
464
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, message: 'No files found to map', project_id, file_path, mapped_files: 0 }) }] }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const results: any[] = []
|
|
468
|
+
|
|
469
|
+
const errors: string[] = []
|
|
470
|
+
|
|
471
|
+
if (file_path) {
|
|
472
|
+
const { data: fileItems } = await adminDb.from('items').select('id, title, data')
|
|
473
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
474
|
+
.filter('data->>kb_type', 'eq', 'cil_file')
|
|
475
|
+
.filter('data->>file_path', 'eq', file_path)
|
|
476
|
+
.eq('is_active', true)
|
|
477
|
+
.limit(1)
|
|
478
|
+
|
|
479
|
+
const fileItem = (fileItems || [])[0]
|
|
480
|
+
|
|
481
|
+
if (!fileItem) {
|
|
482
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `No cil_file for path: ${file_path}` }) }] }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let r: any = {}
|
|
486
|
+
try {
|
|
487
|
+
r = await callCallmapMapProject(accountId, project_id, file_path, force_regenerate, fast, 1, 0)
|
|
488
|
+
} catch (err: any) {
|
|
489
|
+
errors.push(`map_project(${file_path}): ${err?.message || String(err)}`)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
results.push({
|
|
493
|
+
file_path,
|
|
494
|
+
file_id: fileItem.id,
|
|
495
|
+
...r,
|
|
496
|
+
})
|
|
497
|
+
if (r.errors) errors.push(...r.errors)
|
|
498
|
+
} else {
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
let offset = 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
let hasMore = true
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
while (hasMore) {
|
|
511
|
+
|
|
512
|
+
let r: any = { files_processed: 0 }
|
|
513
|
+
try {
|
|
514
|
+
r = await callCallmapMapProject(accountId, project_id, undefined, force_regenerate, fast, 15, offset)
|
|
515
|
+
} catch (err: any) {
|
|
516
|
+
errors.push(`map_project(offset ${offset}): ${err?.message || String(err)}`)
|
|
517
|
+
hasMore = false
|
|
518
|
+
continue
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
results.push(r)
|
|
522
|
+
if (r.errors) errors.push(...r.errors)
|
|
523
|
+
hasMore = r.has_more === true
|
|
524
|
+
offset = r.next_offset ?? offset + (r.files_processed || 0)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
content: [{
|
|
533
|
+
type: 'text',
|
|
534
|
+
text: JSON.stringify({
|
|
535
|
+
ok: errors.length === 0,
|
|
536
|
+
project_id,
|
|
537
|
+
file_path: file_path || '(all files)',
|
|
538
|
+
file_count: file_path ? 1 : results.reduce((sum, r) => sum + (r.files_processed || 0), 0),
|
|
539
|
+
processed: file_path ? results.length : results.reduce((sum, r) => sum + (r.files_processed || 0), 0),
|
|
540
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
541
|
+
results,
|
|
542
|
+
}, null, 2),
|
|
543
|
+
}],
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
server.registerTool(
|
|
549
|
+
'spine_cil_get_pending_vectors',
|
|
550
|
+
{
|
|
551
|
+
description: `List all chunks that have vectors awaiting human-authored content (status="pending_human_input").
|
|
552
|
+
Used by the CIL UI to surface chunks needing human input for implementation_pattern and user_journey vectors.
|
|
553
|
+
Returns stable_id, version_id, chunk_name, file_path, and which vector types are pending for each chunk.`,
|
|
554
|
+
inputSchema: z.object({
|
|
555
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
556
|
+
vector_type: z.string().optional().describe('Filter to a specific vector type, e.g. "user_journey". If omitted, returns all pending.'),
|
|
557
|
+
}),
|
|
558
|
+
},
|
|
559
|
+
async ({ project_id, vector_type }) => {
|
|
560
|
+
const { data: chunks } = await adminDb.from('items').select('id, data')
|
|
561
|
+
.filter('data->>kb_type', 'eq', 'cil_chunk')
|
|
562
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
563
|
+
.eq('is_active', true)
|
|
564
|
+
|
|
565
|
+
const pending = (chunks || [])
|
|
566
|
+
.filter((c: any) => {
|
|
567
|
+
const pendingList: string[] = c.data?.vectors_pending_human_review || []
|
|
568
|
+
if (pendingList.length === 0) return false
|
|
569
|
+
if (vector_type) return pendingList.includes(vector_type)
|
|
570
|
+
return true
|
|
571
|
+
})
|
|
572
|
+
.map((c: any) => ({
|
|
573
|
+
id: c.id,
|
|
574
|
+
stable_id: c.data?.stable_id || '',
|
|
575
|
+
version_id: c.data?.version_id || null,
|
|
576
|
+
chunk_name: c.data?.chunk_name || '',
|
|
577
|
+
file_path: c.data?.file_path || '',
|
|
578
|
+
chunk_type: c.data?.chunk_type || '',
|
|
579
|
+
macro: c.data?.macro || '',
|
|
580
|
+
pending_vectors: vector_type
|
|
581
|
+
? [vector_type]
|
|
582
|
+
: (c.data?.vectors_pending_human_review || []),
|
|
583
|
+
}))
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
content: [{
|
|
587
|
+
type: 'text',
|
|
588
|
+
text: JSON.stringify({
|
|
589
|
+
ok: true,
|
|
590
|
+
project_id,
|
|
591
|
+
filter: vector_type || 'all',
|
|
592
|
+
pending_count: pending.length,
|
|
593
|
+
chunks: pending,
|
|
594
|
+
}, null, 2),
|
|
595
|
+
}],
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
server.registerTool(
|
|
602
|
+
'spine_cil_set_vector_content',
|
|
603
|
+
{
|
|
604
|
+
description: `Store human-authored content for an implementation_pattern or user_journey vector on a chunk.
|
|
605
|
+
|
|
606
|
+
REQUIRED sequence — the file is always source of truth:
|
|
607
|
+
1. Update the DOC-VECTOR tag in the source file on disk first
|
|
608
|
+
2. Run spine_cil_store_markers to sync the file to the DB
|
|
609
|
+
3. THEN call this tool to mark the vector ready for embedding
|
|
610
|
+
|
|
611
|
+
Do NOT call this tool before updating the file — that would create DB/file drift.`,
|
|
612
|
+
inputSchema: z.object({
|
|
613
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
614
|
+
stable_id: z.string().describe('The stable_id of the chunk to update'),
|
|
615
|
+
version_id: z.string().optional().describe('Optional version UUID to scope the lookup'),
|
|
616
|
+
vector_type: z.string().describe('The vector type to update, e.g. "implementation_pattern", "user_journey"'),
|
|
617
|
+
content: z.string().describe('The human-authored content for this vector'),
|
|
618
|
+
}),
|
|
619
|
+
},
|
|
620
|
+
async ({ project_id, stable_id, version_id, vector_type, content }) => {
|
|
621
|
+
|
|
622
|
+
const chunk = await getChunkByStableId(project_id, stable_id, version_id)
|
|
623
|
+
|
|
624
|
+
if (!chunk) {
|
|
625
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Chunk not found: ${stable_id}` }) }] }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const cd = chunk.data || {}
|
|
629
|
+
|
|
630
|
+
const vectorContent: Record<string, { content: string; status: string }> = { ...(cd.vector_content || {}) }
|
|
631
|
+
|
|
632
|
+
vectorContent[vector_type] = { content, status: 'ready' }
|
|
633
|
+
|
|
634
|
+
const pendingList: string[] = (cd.vectors_pending_human_review || []).filter((v: string) => v !== vector_type)
|
|
635
|
+
|
|
636
|
+
const now = new Date().toISOString()
|
|
637
|
+
|
|
638
|
+
const newData = {
|
|
639
|
+
...cd,
|
|
640
|
+
vector_content: vectorContent,
|
|
641
|
+
vectors_pending_human_review: pendingList,
|
|
642
|
+
vector_content_updated_at: now,
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const { error } = await adminDb.from('items')
|
|
646
|
+
.update({ data: newData, updated_at: now })
|
|
647
|
+
.eq('id', chunk.id)
|
|
648
|
+
|
|
649
|
+
if (error) {
|
|
650
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: error.message }) }] }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
content: [{
|
|
655
|
+
type: 'text',
|
|
656
|
+
text: JSON.stringify({
|
|
657
|
+
ok: true,
|
|
658
|
+
chunk_id: chunk.id,
|
|
659
|
+
stable_id,
|
|
660
|
+
vector_type,
|
|
661
|
+
content_length: content.length,
|
|
662
|
+
remaining_pending: pendingList,
|
|
663
|
+
note: 'DB updated. Also update the DOC-VECTOR tag in the source file and re-run spine_cil_store_markers to keep file as source of truth.',
|
|
664
|
+
}, null, 2),
|
|
665
|
+
}],
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
server.registerTool(
|
|
671
|
+
'spine_cil_unmapped_files',
|
|
672
|
+
{
|
|
673
|
+
description: `List selected cil_file records for a CIL project that currently have no mapping links. A file is considered unmapped when neither the file id nor any of its chunk ids appear as source_id in links of type cil_imports, cil_calls, cil_exports, or cil_tested_by.`,
|
|
674
|
+
inputSchema: z.object({
|
|
675
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
676
|
+
limit: z.number().int().min(1).optional().describe('Maximum number of unmapped files to return (default 50)'),
|
|
677
|
+
}),
|
|
678
|
+
},
|
|
679
|
+
async ({ project_id, limit = 50 }) => {
|
|
680
|
+
const { data: projectRows } = await adminDb.from('items').select('data')
|
|
681
|
+
.filter('data->>kb_type', 'eq', 'cil_project')
|
|
682
|
+
.eq('id', project_id)
|
|
683
|
+
.eq('is_active', true)
|
|
684
|
+
.limit(1)
|
|
685
|
+
|
|
686
|
+
const project = (projectRows || [])[0]
|
|
687
|
+
|
|
688
|
+
if (!project) {
|
|
689
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const selectedPaths: string[] = project.data?.selected_paths || []
|
|
693
|
+
|
|
694
|
+
if (selectedPaths.length === 0) {
|
|
695
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, total_selected: 0, unmapped_count: 0, unmapped: [], message: 'No selected_paths on project' }) }] }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const { data: files, error: fileError } = await adminDb.from('items').select('id, data')
|
|
699
|
+
.filter('data->>kb_type', 'eq', 'cil_file')
|
|
700
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
701
|
+
.eq('is_active', true)
|
|
702
|
+
.limit(100000)
|
|
703
|
+
|
|
704
|
+
if (fileError) {
|
|
705
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch files: ${fileError.message}` }) }] }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const rawFiles = files || []
|
|
709
|
+
|
|
710
|
+
const selectedFiles = rawFiles.filter((f: any) => isSelectedPath(f.data?.file_path || f.title || '', selectedPaths))
|
|
711
|
+
|
|
712
|
+
const total_selected = selectedFiles.length
|
|
713
|
+
|
|
714
|
+
if (total_selected === 0) {
|
|
715
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, project_id, total_selected: 0, unmapped_count: 0, unmapped: [] }) }] }
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const { data: chunks, error: chunkError } = await adminDb.from('items').select('id, data')
|
|
719
|
+
.filter('data->>kb_type', 'eq', 'cil_chunk')
|
|
720
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
721
|
+
.eq('is_active', true)
|
|
722
|
+
.limit(100000)
|
|
723
|
+
|
|
724
|
+
if (chunkError) {
|
|
725
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch chunks: ${chunkError.message}` }) }] }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const chunksByFile: Record<string, any[]> = {}
|
|
729
|
+
|
|
730
|
+
for (const c of chunks || []) {
|
|
731
|
+
|
|
732
|
+
const fp = c.data?.file_path
|
|
733
|
+
|
|
734
|
+
if (fp) {
|
|
735
|
+
if (!chunksByFile[fp]) chunksByFile[fp] = []
|
|
736
|
+
chunksByFile[fp].push(c)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const linkTypeMap: Record<string, string> = {}
|
|
742
|
+
|
|
743
|
+
for (const slug of ['cil_imports', 'cil_calls', 'cil_exports', 'cil_tested_by']) {
|
|
744
|
+
try {
|
|
745
|
+
linkTypeMap[slug] = await resolveLinkTypeId(slug)
|
|
746
|
+
} catch {
|
|
747
|
+
// ignore missing link types
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const linkTypeIds = Object.values(linkTypeMap).filter(Boolean)
|
|
753
|
+
|
|
754
|
+
const allSourceIds: string[] = []
|
|
755
|
+
|
|
756
|
+
for (const f of selectedFiles) {
|
|
757
|
+
allSourceIds.push(f.id)
|
|
758
|
+
|
|
759
|
+
const filePath = f.data?.file_path || f.title || ''
|
|
760
|
+
|
|
761
|
+
for (const c of chunksByFile[filePath] || []) {
|
|
762
|
+
allSourceIds.push(c.id)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const linkCounts: Record<string, number> = {}
|
|
768
|
+
|
|
769
|
+
const UUID_IN_LIMIT = 100
|
|
770
|
+
|
|
771
|
+
if (linkTypeIds.length > 0 && allSourceIds.length > 0) {
|
|
772
|
+
for (let i = 0; i < allSourceIds.length; i += UUID_IN_LIMIT) {
|
|
773
|
+
|
|
774
|
+
const batch = allSourceIds.slice(i, i + UUID_IN_LIMIT)
|
|
775
|
+
|
|
776
|
+
const { data: links, error: linkError } = await adminDb.from('links').select('id, link_type_id, source_id')
|
|
777
|
+
.in('link_type_id', linkTypeIds)
|
|
778
|
+
.in('source_id', batch)
|
|
779
|
+
|
|
780
|
+
if (linkError) {
|
|
781
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to fetch links: ${linkError.message}` }) }] }
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
for (const l of links || []) {
|
|
785
|
+
linkCounts[l.source_id] = (linkCounts[l.source_id] || 0) + 1
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const unmapped: { file_path: string; file_id: string; chunk_count: number; link_count: number }[] = []
|
|
791
|
+
|
|
792
|
+
for (const f of selectedFiles) {
|
|
793
|
+
|
|
794
|
+
const filePath = f.data?.file_path || f.title || ''
|
|
795
|
+
|
|
796
|
+
const fileChunks = chunksByFile[filePath] || []
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
let totalLinks = linkCounts[f.id] || 0
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
for (const c of fileChunks) {
|
|
804
|
+
totalLinks += linkCounts[c.id] || 0
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
if (totalLinks === 0) {
|
|
809
|
+
unmapped.push({
|
|
810
|
+
file_path: filePath,
|
|
811
|
+
file_id: f.id,
|
|
812
|
+
chunk_count: fileChunks.length,
|
|
813
|
+
link_count: 0,
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
unmapped.sort((a, b) => a.file_path.localeCompare(b.file_path))
|
|
819
|
+
|
|
820
|
+
const limited = unmapped.slice(0, limit)
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
content: [{
|
|
824
|
+
type: 'text',
|
|
825
|
+
text: JSON.stringify({
|
|
826
|
+
ok: true,
|
|
827
|
+
project_id,
|
|
828
|
+
raw_file_count: rawFiles.length,
|
|
829
|
+
selected_paths_count: selectedPaths.length,
|
|
830
|
+
total_selected,
|
|
831
|
+
unmapped_count: unmapped.length,
|
|
832
|
+
limit,
|
|
833
|
+
unmapped: limited,
|
|
834
|
+
}, null, 2),
|
|
835
|
+
}],
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
}
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
server.registerTool(
|
|
842
|
+
'spine_cil_validate_chunking',
|
|
843
|
+
{
|
|
844
|
+
description: `Validate whether a source file's CIL markers still match the current AST. Returns not_chunked, valid, or needs_rechunk, plus marker agreement details. Use this before deciding to chunk or rechunk a file.`,
|
|
845
|
+
inputSchema: z.object({
|
|
846
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
847
|
+
file_path: z.string().describe('Relative file path from repo root, e.g. ".framework/functions/cil-ingest.ts"'),
|
|
848
|
+
}),
|
|
849
|
+
},
|
|
850
|
+
async ({ project_id, file_path }) => {
|
|
851
|
+
try {
|
|
852
|
+
|
|
853
|
+
const result = await fetchFunction('cil-ingest', 'validate_chunking', {}, { project_id, file_path })
|
|
854
|
+
|
|
855
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
856
|
+
} catch (e: any) {
|
|
857
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: e.message || String(e) }) }] }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
}
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
async function getCilFileId(project_id: string, file_path: string) {
|
|
864
|
+
const { data } = await adminDb
|
|
865
|
+
.from('items')
|
|
866
|
+
.select('id')
|
|
867
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
868
|
+
.filter('data->>kb_type', 'eq', 'cil_file')
|
|
869
|
+
.filter('data->>file_path', 'eq', file_path)
|
|
870
|
+
.eq('is_active', true)
|
|
871
|
+
.maybeSingle()
|
|
872
|
+
if (!data) throw new Error(`No cil_file for path ${file_path}`)
|
|
873
|
+
return data.id
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const STEP_SOURCE_STATUS: Record<number, string> = {
|
|
877
|
+
3: '2-chunked',
|
|
878
|
+
4: '3-documented',
|
|
879
|
+
5: '4-chunk-validated',
|
|
880
|
+
6: '5-doc-validated',
|
|
881
|
+
7: '6-enriched',
|
|
882
|
+
8: '7-embedded-mapped',
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function findNextFileForStep(project_id: string, sourceStatus: string, selectedPaths: string[]) {
|
|
886
|
+
const { data } = await adminDb
|
|
887
|
+
.from('items')
|
|
888
|
+
.select('id, data')
|
|
889
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
890
|
+
.filter('data->>kb_type', 'eq', 'cil_file')
|
|
891
|
+
.filter('data->>status', 'eq', sourceStatus)
|
|
892
|
+
.eq('is_active', true)
|
|
893
|
+
const ready = (data || [])
|
|
894
|
+
.map((f: any) => f.data?.file_path || f.title)
|
|
895
|
+
.filter((fp: string) => isSelectedPath(fp, selectedPaths))
|
|
896
|
+
.sort((a: string, b: string) => a.localeCompare(b))
|
|
897
|
+
return ready[0] || null
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function runPipelineStep(project_id: string, step: number, limit: number, file_path?: string, force_reprocess?: boolean) {
|
|
901
|
+
const { data: projectRows } = await adminDb
|
|
902
|
+
.from('items')
|
|
903
|
+
.select('data')
|
|
904
|
+
.filter('data->>kb_type', 'eq', 'cil_project')
|
|
905
|
+
.eq('id', project_id)
|
|
906
|
+
.maybeSingle()
|
|
907
|
+
|
|
908
|
+
if (!projectRows) throw new Error('Project not found')
|
|
909
|
+
const project = projectRows.data || {}
|
|
910
|
+
const selectedPaths: string[] = project.selected_paths || []
|
|
911
|
+
const stepCursors: Record<string, any> = project.step_cursors || {}
|
|
912
|
+
|
|
913
|
+
if (step === 0 || step === 1 || step === 2) {
|
|
914
|
+
return await fetchFunction('cil-ingest', 'step', {}, { project_id, step, limit, file_path, force_reprocess })
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const sourceStatus = STEP_SOURCE_STATUS[step]
|
|
918
|
+
if (!sourceStatus) throw new Error(`step ${step} is not supported`)
|
|
919
|
+
|
|
920
|
+
const stepCursorsKey = String(step)
|
|
921
|
+
const cursor = stepCursors[stepCursorsKey] || null
|
|
922
|
+
const targetPath: string = file_path || (cursor && typeof cursor === 'object' ? cursor.file_path : '')
|
|
923
|
+
const offset: number = cursor && typeof cursor === 'object' ? (cursor.offset || 0) : 0
|
|
924
|
+
|
|
925
|
+
if (!targetPath) {
|
|
926
|
+
if (step === 3) {
|
|
927
|
+
file_path = ''
|
|
928
|
+
} else {
|
|
929
|
+
const nextPath = await findNextFileForStep(project_id, sourceStatus, selectedPaths)
|
|
930
|
+
if (!nextPath) {
|
|
931
|
+
return { ok: true, step, status: 'complete', processed: 0, has_more: false, message: 'No files ready for this step' }
|
|
932
|
+
}
|
|
933
|
+
file_path = nextPath
|
|
934
|
+
}
|
|
935
|
+
} else {
|
|
936
|
+
file_path = targetPath
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
let result: any
|
|
940
|
+
if (step === 3) {
|
|
941
|
+
result = await fetchFunction('cil-doc', 'document', {}, { project_id, limit, offset })
|
|
942
|
+
} else if (step === 4 || step === 7) {
|
|
943
|
+
result = await fetchFunction('cil-embeddings', 'embed_approved', {}, { project_id, file_path, limit, offset, second_pass: step === 7 })
|
|
944
|
+
} else if (step === 5 || step === 8) {
|
|
945
|
+
const fileId = await getCilFileId(project_id, file_path)
|
|
946
|
+
result = await fetchFunction('cil-callmap', 'map_file', {}, { project_id, file_id: fileId })
|
|
947
|
+
} else if (step === 6) {
|
|
948
|
+
const repoPath = project.repo_path
|
|
949
|
+
if (!repoPath) throw new Error('Project has no repo_path')
|
|
950
|
+
result = await fetchFunction('cil-config', 'run', {}, { project_id, repo_path, file_path })
|
|
951
|
+
} else {
|
|
952
|
+
throw new Error(`step ${step} is not supported`)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const hasMore = result?.has_more === true
|
|
956
|
+
const nextOffset = hasMore ? (result?.next_offset ?? (offset + limit)) : 0
|
|
957
|
+
const newCursor = hasMore ? { file_path, offset: nextOffset } : null
|
|
958
|
+
|
|
959
|
+
const { data: projectAfter } = await adminDb
|
|
960
|
+
.from('items')
|
|
961
|
+
.select('data')
|
|
962
|
+
.eq('id', project_id)
|
|
963
|
+
.maybeSingle()
|
|
964
|
+
|
|
965
|
+
if (projectAfter) {
|
|
966
|
+
const now = new Date().toISOString()
|
|
967
|
+
const newCursors = { ...stepCursors, [stepCursorsKey]: newCursor }
|
|
968
|
+
await adminDb.from('items')
|
|
969
|
+
.update({ data: { ...projectAfter.data, step_cursors: newCursors }, updated_at: now })
|
|
970
|
+
.eq('id', project_id)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
ok: true,
|
|
975
|
+
step,
|
|
976
|
+
file_path,
|
|
977
|
+
status: hasMore ? 'in_progress' : 'in_progress',
|
|
978
|
+
has_more: hasMore,
|
|
979
|
+
next_offset: nextOffset,
|
|
980
|
+
...result,
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
server.registerTool(
|
|
985
|
+
'spine_cil_run_step',
|
|
986
|
+
{
|
|
987
|
+
description: `Run one unit of a CIL ingestion step (0-8). Steps 0-2 go through cil-ingest. Steps 3-8 dispatch to cil-doc, cil-embeddings, cil-callmap, or cil-config. For steps 3, 4, 7 the tool tracks page offset in project.data.step_cursors. Repeat calls until has_more is false and no more files are found.`,
|
|
988
|
+
inputSchema: z.object({
|
|
989
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
990
|
+
step: z.number().int().min(0).max(8).default(2).describe('Pipeline step number to run (0-8)'),
|
|
991
|
+
limit: z.number().int().min(1).default(1).describe('Page/batch size for steps 3, 4, 7. Steps 2, 5, 6, 8 process one file per call.'),
|
|
992
|
+
file_path: z.string().optional().describe('Optional specific file path to process; if omitted the next pending file is chosen from the cursor or the work queue'),
|
|
993
|
+
force_reprocess: z.boolean().optional().default(false).describe('Force reprocessing even if the file has already been chunked (step 2)'),
|
|
994
|
+
}),
|
|
995
|
+
},
|
|
996
|
+
async ({ project_id, step, limit, file_path, force_reprocess }) => {
|
|
997
|
+
try {
|
|
998
|
+
|
|
999
|
+
const result = await runPipelineStep(project_id, step, limit, file_path, force_reprocess)
|
|
1000
|
+
|
|
1001
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
1002
|
+
} catch (e: any) {
|
|
1003
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: e.message || String(e) }) }] }
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
server.registerTool(
|
|
1010
|
+
'spine_cil_get_project_items',
|
|
1011
|
+
{
|
|
1012
|
+
description: `List active CIL file and/or chunk records for a project, optionally filtered by kb_type and file_path. Optionally include the raw chunk source (code_content) when include_source is true. Use this to build the id map for manual mapping or to fetch all chunks for a file in one call.`,
|
|
1013
|
+
inputSchema: z.object({
|
|
1014
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
1015
|
+
kb_type: z.enum(['cil_file', 'cil_chunk']).optional().describe('Filter by kb_type; omit to return both'),
|
|
1016
|
+
file_path: z.string().optional().describe('Filter by file_path'),
|
|
1017
|
+
chunk_name: z.string().optional().describe('Filter by chunk_name (exact match)'),
|
|
1018
|
+
include_source: z.boolean().optional().default(false).describe('Include the raw code_content for chunks; can be large'),
|
|
1019
|
+
limit: z.number().int().min(1).max(1000).optional().default(1000).describe('Maximum items to return (max 1000)'),
|
|
1020
|
+
offset: z.number().int().min(0).optional().default(0).describe('Pagination offset'),
|
|
1021
|
+
}),
|
|
1022
|
+
},
|
|
1023
|
+
async ({ project_id, kb_type, file_path, chunk_name, include_source = false, limit = 1000, offset = 0 }) => {
|
|
1024
|
+
const { data: project } = await adminDb.from('items').select('id, data, account_id')
|
|
1025
|
+
.filter('data->>kb_type', 'eq', 'cil_project')
|
|
1026
|
+
.eq('id', project_id)
|
|
1027
|
+
.maybeSingle()
|
|
1028
|
+
|
|
1029
|
+
if (!project) {
|
|
1030
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const repoPath: string = project.data?.repo_path || ''
|
|
1034
|
+
const accountId: string = project.account_id || ''
|
|
1035
|
+
|
|
1036
|
+
let q = adminDb.from('items').select('id, title, data')
|
|
1037
|
+
.filter('data->>project_id', 'eq', project_id)
|
|
1038
|
+
.eq('is_active', true)
|
|
1039
|
+
.order('id')
|
|
1040
|
+
.range(offset, offset + limit)
|
|
1041
|
+
|
|
1042
|
+
if (kb_type) q = q.filter('data->>kb_type', 'eq', kb_type)
|
|
1043
|
+
if (file_path) q = q.filter('data->>file_path', 'eq', file_path)
|
|
1044
|
+
if (chunk_name) q = q.filter('data->>chunk_name', 'eq', chunk_name)
|
|
1045
|
+
|
|
1046
|
+
const { data: rows, error } = await q
|
|
1047
|
+
|
|
1048
|
+
if (error) {
|
|
1049
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `DB query failed: ${error.message}` }) }] }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const hasMore = (rows || []).length > limit
|
|
1053
|
+
const pageRows = (rows || []).slice(0, limit)
|
|
1054
|
+
|
|
1055
|
+
const items = pageRows.map((r: any) => {
|
|
1056
|
+
const cd = r.data || {}
|
|
1057
|
+
const out: any = {
|
|
1058
|
+
id: r.id,
|
|
1059
|
+
kb_type: cd.kb_type || '',
|
|
1060
|
+
file_path: cd.file_path || r.title || '',
|
|
1061
|
+
chunk_name: cd.chunk_name || '',
|
|
1062
|
+
stable_id: cd.stable_id || '',
|
|
1063
|
+
chunk_type: cd.chunk_type || '',
|
|
1064
|
+
line_start: cd.line_start || 0,
|
|
1065
|
+
line_end: cd.line_end || 0,
|
|
1066
|
+
macro: cd.macro || '',
|
|
1067
|
+
doc_confidence: cd.doc_confidence || '',
|
|
1068
|
+
}
|
|
1069
|
+
if (cd.kb_type === 'cil_chunk' && include_source) {
|
|
1070
|
+
if (cd.code_content) {
|
|
1071
|
+
out.code_content = cd.code_content
|
|
1072
|
+
} else if (repoPath && cd.file_path && cd.line_start && cd.line_end) {
|
|
1073
|
+
try {
|
|
1074
|
+
const abs = path.join(repoPath, cd.file_path.replace(/^\//, ''))
|
|
1075
|
+
const lines = fs.readFileSync(abs, 'utf-8').split('\n')
|
|
1076
|
+
const start = Math.max(0, (cd.line_start || 1) - 1)
|
|
1077
|
+
const end = Math.min(lines.length, cd.line_end || lines.length)
|
|
1078
|
+
out.code_content = lines.slice(start, end).join('\n')
|
|
1079
|
+
} catch {
|
|
1080
|
+
out.code_content = ''
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
out.code_content = ''
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return out
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
content: [{
|
|
1091
|
+
type: 'text',
|
|
1092
|
+
text: JSON.stringify({
|
|
1093
|
+
ok: true,
|
|
1094
|
+
project_id,
|
|
1095
|
+
account_id: accountId,
|
|
1096
|
+
repo_path: repoPath,
|
|
1097
|
+
kb_type: kb_type || null,
|
|
1098
|
+
file_path: file_path || null,
|
|
1099
|
+
include_source,
|
|
1100
|
+
limit,
|
|
1101
|
+
offset,
|
|
1102
|
+
returned: items.length,
|
|
1103
|
+
has_more: hasMore,
|
|
1104
|
+
next_offset: hasMore ? offset + limit : null,
|
|
1105
|
+
items,
|
|
1106
|
+
}, null, 2),
|
|
1107
|
+
}],
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
server.registerTool(
|
|
1113
|
+
'spine_cil_upsert_links',
|
|
1114
|
+
{
|
|
1115
|
+
description: `Batch upsert CIL mapping links (cil_calls, cil_imports, cil_exports) for a project. Provide source_id and target_id as CIL item UUIDs. If force is true, existing links for each source_id/link_type pair are cleared before inserting. Use this with spine_cil_get_project_items for manual, chunk-level mapping that does not wipe the whole graph.`,
|
|
1116
|
+
inputSchema: z.object({
|
|
1117
|
+
project_id: z.string().uuid().describe('The cil_project UUID'),
|
|
1118
|
+
links: z.array(z.object({
|
|
1119
|
+
source_id: z.string().uuid(),
|
|
1120
|
+
source_type: z.enum(['cil_file', 'cil_chunk']),
|
|
1121
|
+
target_id: z.string().uuid(),
|
|
1122
|
+
target_type: z.enum(['cil_file', 'cil_chunk']),
|
|
1123
|
+
link_type: z.enum(['cil_calls', 'cil_imports', 'cil_exports', 'cil_tested_by']),
|
|
1124
|
+
metadata: z.record(z.any()).optional(),
|
|
1125
|
+
})).min(1).max(1000).describe('Links to create'),
|
|
1126
|
+
force: z.boolean().optional().default(false).describe('When true, delete existing links for each source_id/link_type before creating new ones'),
|
|
1127
|
+
}),
|
|
1128
|
+
},
|
|
1129
|
+
async ({ project_id, links, force = false }) => {
|
|
1130
|
+
const { data: project } = await adminDb.from('items').select('id, data, account_id')
|
|
1131
|
+
.filter('data->>kb_type', 'eq', 'cil_project')
|
|
1132
|
+
.eq('id', project_id)
|
|
1133
|
+
.maybeSingle()
|
|
1134
|
+
|
|
1135
|
+
if (!project) {
|
|
1136
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Project not found: ${project_id}` }) }] }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const accountId: string = project.account_id || ''
|
|
1140
|
+
const linksTypeId = await resolveTypeId('link', 'link')
|
|
1141
|
+
|
|
1142
|
+
const linkTypeIds: Record<string, string> = {}
|
|
1143
|
+
for (const slug of ['cil_calls', 'cil_imports', 'cil_exports', 'cil_tested_by']) {
|
|
1144
|
+
try {
|
|
1145
|
+
linkTypeIds[slug] = await resolveLinkTypeId(slug)
|
|
1146
|
+
} catch {
|
|
1147
|
+
// ignore missing link types
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const invalid = links.filter(l => !linkTypeIds[l.link_type])
|
|
1152
|
+
if (invalid.length > 0) {
|
|
1153
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Some links use an unknown link type', invalid }, null, 2) }] }
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (force) {
|
|
1157
|
+
const linksByType: Record<string, string[]> = {}
|
|
1158
|
+
for (const l of links) {
|
|
1159
|
+
if (!linksByType[l.link_type]) linksByType[l.link_type] = []
|
|
1160
|
+
if (!linksByType[l.link_type].includes(l.source_id)) linksByType[l.link_type].push(l.source_id)
|
|
1161
|
+
}
|
|
1162
|
+
for (const [slug, ltId] of Object.entries(linkTypeIds)) {
|
|
1163
|
+
const sourceIds = linksByType[slug]
|
|
1164
|
+
if (!sourceIds || !ltId) continue
|
|
1165
|
+
for (let i = 0; i < sourceIds.length; i += 100) {
|
|
1166
|
+
await adminDb.from('links')
|
|
1167
|
+
.delete()
|
|
1168
|
+
.eq('link_type_id', ltId)
|
|
1169
|
+
.in('source_id', sourceIds.slice(i, i + 100))
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const inserts = links.map((l: any) => ({
|
|
1175
|
+
type_id: linksTypeId,
|
|
1176
|
+
account_id: accountId,
|
|
1177
|
+
link_type_id: linkTypeIds[l.link_type],
|
|
1178
|
+
source_type: l.source_type,
|
|
1179
|
+
source_id: l.source_id,
|
|
1180
|
+
target_type: l.target_type,
|
|
1181
|
+
target_id: l.target_id,
|
|
1182
|
+
link_type: l.link_type,
|
|
1183
|
+
metadata: l.metadata || {},
|
|
1184
|
+
}))
|
|
1185
|
+
|
|
1186
|
+
let created = 0
|
|
1187
|
+
for (const row of inserts) {
|
|
1188
|
+
const { data, error } = await adminDb.from('links').insert(row).select('id')
|
|
1189
|
+
if (error) {
|
|
1190
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Insert failed: ${error.message}` }) }] }
|
|
1191
|
+
}
|
|
1192
|
+
if (data?.id) created++
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return {
|
|
1196
|
+
content: [{
|
|
1197
|
+
type: 'text',
|
|
1198
|
+
text: JSON.stringify({
|
|
1199
|
+
ok: true,
|
|
1200
|
+
project_id,
|
|
1201
|
+
requested: links.length,
|
|
1202
|
+
created,
|
|
1203
|
+
force,
|
|
1204
|
+
}, null, 2),
|
|
1205
|
+
}],
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
)
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// CIL[DOC-VECTOR type="context" content="purpose='Register all CIL ingest, discovery, chunking, embedding, mapping, and pipeline orchestration MCP tools on a shared McpServer'; No verified external callers; imports: .framework/mcp/shared/db.ts,.framework/functions/_shared/cil.ts"]
|
|
1212
|
+
// CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
|
|
1213
|
+
// CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
|
|
1214
|
+
// CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
|
|
1215
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
1216
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
1217
|
+
// CIL[END stable_id="~/.framework/mcp/tools/cil/ingest.ts:mcp_ingest_tools:file"]
|