diff --git a/components/settings/components/tool-selection-form.tsx b/components/settings/components/tool-selection-form.tsx
index 2fcf440e..1eaabc6f 100644
--- a/components/settings/components/tool-selection-form.tsx
+++ b/components/settings/components/tool-selection-form.tsx
@@ -1,6 +1,5 @@
"use client";
-import { useState, useEffect } from "react";
import type { UseFormReturn } from "react-hook-form";
import {
FormField,
@@ -20,11 +19,7 @@ import {
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { startSkyfiConnection, getSkyfiConnectionStatus, disconnectSkyfi } from "@/lib/actions/skyfi";
-import { Earth, Orbit, ShieldCheck, Loader2 } from "lucide-react";
-import { useToast } from "@/components/ui/hooks/use-toast";
-import { cn } from "@/lib/utils";
+import { Earth, Orbit } from "lucide-react";
interface ToolSelectionFormProps {
form: UseFormReturn
;
@@ -50,32 +45,6 @@ const tools = [
];
export function ToolSelectionForm({ form }: ToolSelectionFormProps) {
- const selectedModel = form.watch("selectedModel");
- const { toast } = useToast();
- const [skyfiConnected, setSkyfiConnected] = useState(false);
- const [skyfiBudget, setSkyfiBudget] = useState(null);
- const [loadingStatus, setLoadingStatus] = useState(true);
- const [isConnecting, setIsConnecting] = useState(false);
- const [isDisconnecting, setIsDisconnecting] = useState(false);
-
- useEffect(() => {
- async function loadStatus() {
- if (selectedModel === "SkyFi") {
- setLoadingStatus(true);
- try {
- const status = await getSkyfiConnectionStatus();
- setSkyfiConnected(status.connected);
- setSkyfiBudget(status.budget || null);
- } catch (err) {
- console.error("Failed to load SkyFi connection status:", err);
- } finally {
- setLoadingStatus(false);
- }
- }
- }
- loadStatus();
- }, [selectedModel]);
-
return (
-
- {selectedModel === "SkyFi" && (
-
-
-
-
-
SkyFi Account Connection
-
- {loadingStatus ? (
-
-
- Checking SkyFi connection status...
-
- ) : skyfiConnected ? (
-
-
-
-
-
SkyFi Account Linked
- {skyfiBudget ? (
-
- {skyfiBudget}
-
- ) : (
-
Successfully authenticated and ready to query satellite imagery.
- )}
-
-
-
-
- ) : (
-
-
- Connect your SkyFi account to search existing satellite imagery, task new captures, and manage Areas of Interest (AOIs) directly from your chats.
-
-
-
- )}
-
-
- )}
)}
/>
diff --git a/components/skyfi-section.tsx b/components/skyfi-section.tsx
deleted file mode 100644
index 69b56ad4..00000000
--- a/components/skyfi-section.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-'use client'
-
-import { Section } from './section'
-import { ToolBadge } from './tool-badge'
-import { SearchSkeleton } from './search-skeleton'
-import { MemoizedReactMarkdown } from './ui/markdown'
-import rehypeExternalLinks from 'rehype-external-links'
-import remarkGfm from 'remark-gfm'
-import { StreamableValue, useStreamableValue } from 'ai/rsc'
-import { cn } from '@/lib/utils'
-
-export type SkyfiSectionProps = {
- result?: StreamableValue
-}
-
-export function SkyfiSection({ result }: SkyfiSectionProps) {
- const [data, error, pending] = useStreamableValue(result)
-
- let queryType = 'Query'
- let resultText = ''
- let hasError = false
-
- if (data) {
- try {
- const parsed = JSON.parse(data)
- queryType = parsed.queryType || 'Query'
- resultText = parsed.result || ''
- if (parsed.error) {
- hasError = true
- resultText = parsed.error
- }
- } catch {
- resultText = data
- }
- }
-
- // Handle stream error explicitly
- if (error) {
- return (
-
-
-
-
- {(error as any).message || String(error)}
-
-
-
- )
- }
-
- return (
-
- {!pending && data ? (
- <>
-
- {`SkyFi MCP: ${queryType}`}
-
-
- >
- ) : (
-
- )}
-
- )
-}
diff --git a/components/tool-badge.tsx b/components/tool-badge.tsx
index eb39af41..f2a22c10 100644
--- a/components/tool-badge.tsx
+++ b/components/tool-badge.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { Search, Orbit } from 'lucide-react'
+import { Search } from 'lucide-react'
import { Badge } from './ui/badge'
type ToolBadgeProps = {
@@ -14,8 +14,7 @@ export const ToolBadge: React.FC = ({
className
}) => {
const icon: Record = {
- search: ,
- skyfiQueryTool:
+ search:
}
return (
diff --git a/drizzle/migrations/0009_add_skyfi_oauth_tokens.sql b/drizzle/migrations/0009_add_skyfi_oauth_tokens.sql
deleted file mode 100644
index c3d70384..00000000
--- a/drizzle/migrations/0009_add_skyfi_oauth_tokens.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE IF NOT EXISTS "skyfi_oauth_tokens" (
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
- "user_id" uuid NOT NULL,
- "access_token" text,
- "refresh_token" text,
- "token_expiry" timestamp with time zone,
- "client_id" text,
- "client_secret" text,
- "code_verifier" text,
- "state" text,
- "registration_client_uri" text,
- "registration_access_token" text,
- "created_at" timestamp with time zone DEFAULT now() NOT NULL,
- "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
- CONSTRAINT "skyfi_oauth_tokens_user_id_unique" UNIQUE("user_id")
-);
---> statement-breakpoint
-ALTER TABLE "skyfi_oauth_tokens" ADD CONSTRAINT "skyfi_oauth_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
diff --git a/drizzle/migrations/meta/0006_snapshot.json b/drizzle/migrations/meta/0006_snapshot.json
deleted file mode 100644
index 068620d1..00000000
--- a/drizzle/migrations/meta/0006_snapshot.json
+++ /dev/null
@@ -1,1081 +0,0 @@
-{
- "id": "cb88b9e7-0bb2-4a61-b151-a12cf1071b91",
- "prevId": "fd89f5aa-13b0-4dd4-9f50-4e921eea0f4f",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.calendar_notes": {
- "name": "calendar_notes",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "chat_id": {
- "name": "chat_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false
- },
- "date": {
- "name": "date",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "location_tags": {
- "name": "location_tags",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- },
- "user_tags": {
- "name": "user_tags",
- "type": "text[]",
- "primaryKey": false,
- "notNull": false
- },
- "map_feature_id": {
- "name": "map_feature_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "calendar_notes_user_id_users_id_fk": {
- "name": "calendar_notes_user_id_users_id_fk",
- "tableFrom": "calendar_notes",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "calendar_notes_chat_id_chats_id_fk": {
- "name": "calendar_notes_chat_id_chats_id_fk",
- "tableFrom": "calendar_notes",
- "tableTo": "chats",
- "columnsFrom": [
- "chat_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.chat_participants": {
- "name": "chat_participants",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "chat_id": {
- "name": "chat_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "role": {
- "name": "role",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'collaborator'"
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "chat_participants_chat_id_chats_id_fk": {
- "name": "chat_participants_chat_id_chats_id_fk",
- "tableFrom": "chat_participants",
- "tableTo": "chats",
- "columnsFrom": [
- "chat_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "chat_participants_user_id_users_id_fk": {
- "name": "chat_participants_user_id_users_id_fk",
- "tableFrom": "chat_participants",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chat_participants_chat_user_unique": {
- "name": "chat_participants_chat_user_unique",
- "nullsNotDistinct": false,
- "columns": [
- "chat_id",
- "user_id"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.chats": {
- "name": "chats",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'Untitled Chat'"
- },
- "visibility": {
- "name": "visibility",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "default": "'private'"
- },
- "path": {
- "name": "path",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "share_path": {
- "name": "share_path",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "shareable_link_id": {
- "name": "shareable_link_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false,
- "default": "gen_random_uuid()"
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "chats_user_id_users_id_fk": {
- "name": "chats_user_id_users_id_fk",
- "tableFrom": "chats",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "chats_shareable_link_id_unique": {
- "name": "chats_shareable_link_id_unique",
- "nullsNotDistinct": false,
- "columns": [
- "shareable_link_id"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.document_chunks": {
- "name": "document_chunks",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "document_id": {
- "name": "document_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "chunk_text": {
- "name": "chunk_text",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "embedding": {
- "name": "embedding",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "document_chunks_document_id_documents_id_fk": {
- "name": "document_chunks_document_id_documents_id_fk",
- "tableFrom": "document_chunks",
- "tableTo": "documents",
- "columnsFrom": [
- "document_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.documents": {
- "name": "documents",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "chat_id": {
- "name": "chat_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false
- },
- "storage_path": {
- "name": "storage_path",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "mime": {
- "name": "mime",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "default": "'pending'"
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "documents_user_id_users_id_fk": {
- "name": "documents_user_id_users_id_fk",
- "tableFrom": "documents",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "documents_chat_id_chats_id_fk": {
- "name": "documents_chat_id_chats_id_fk",
- "tableFrom": "documents",
- "tableTo": "chats",
- "columnsFrom": [
- "chat_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.locations": {
- "name": "locations",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "chat_id": {
- "name": "chat_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false
- },
- "geojson": {
- "name": "geojson",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "geometry": {
- "name": "geometry",
- "type": "geometry(GEOMETRY, 4326)",
- "primaryKey": false,
- "notNull": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "locations_user_id_users_id_fk": {
- "name": "locations_user_id_users_id_fk",
- "tableFrom": "locations",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "locations_chat_id_chats_id_fk": {
- "name": "locations_chat_id_chats_id_fk",
- "tableFrom": "locations",
- "tableTo": "chats",
- "columnsFrom": [
- "chat_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.messages": {
- "name": "messages",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "chat_id": {
- "name": "chat_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "role": {
- "name": "role",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "content": {
- "name": "content",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "embedding": {
- "name": "embedding",
- "type": "vector(1536)",
- "primaryKey": false,
- "notNull": false
- },
- "location_id": {
- "name": "location_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "messages_chat_id_chats_id_fk": {
- "name": "messages_chat_id_chats_id_fk",
- "tableFrom": "messages",
- "tableTo": "chats",
- "columnsFrom": [
- "chat_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "messages_user_id_users_id_fk": {
- "name": "messages_user_id_users_id_fk",
- "tableFrom": "messages",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "messages_location_id_locations_id_fk": {
- "name": "messages_location_id_locations_id_fk",
- "tableFrom": "messages",
- "tableTo": "locations",
- "columnsFrom": [
- "location_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "set null",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.prompt_generation_jobs": {
- "name": "prompt_generation_jobs",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "domain": {
- "name": "domain",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'pending'"
- },
- "result_prompt": {
- "name": "result_prompt",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "error_message": {
- "name": "error_message",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "prompt_generation_jobs_user_id_users_id_fk": {
- "name": "prompt_generation_jobs_user_id_users_id_fk",
- "tableFrom": "prompt_generation_jobs",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.skyfi_oauth_tokens": {
- "name": "skyfi_oauth_tokens",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "access_token": {
- "name": "access_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token": {
- "name": "refresh_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "token_expiry": {
- "name": "token_expiry",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false
- },
- "client_id": {
- "name": "client_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "client_secret": {
- "name": "client_secret",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "code_verifier": {
- "name": "code_verifier",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "state": {
- "name": "state",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "skyfi_oauth_tokens_user_id_users_id_fk": {
- "name": "skyfi_oauth_tokens_user_id_users_id_fk",
- "tableFrom": "skyfi_oauth_tokens",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "skyfi_oauth_tokens_user_id_unique": {
- "name": "skyfi_oauth_tokens_user_id_unique",
- "nullsNotDistinct": false,
- "columns": [
- "user_id"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.system_prompts": {
- "name": "system_prompts",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "prompt": {
- "name": "prompt",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "system_prompts_user_id_users_id_fk": {
- "name": "system_prompts_user_id_users_id_fk",
- "tableFrom": "system_prompts",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "role": {
- "name": "role",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "default": "'viewer'"
- },
- "selected_model": {
- "name": "selected_model",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "system_prompt": {
- "name": "system_prompt",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "clerk_user_id": {
- "name": "clerk_user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "avatar_url": {
- "name": "avatar_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "first_name": {
- "name": "first_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "last_name": {
- "name": "last_name",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- },
- "username": {
- "name": "username",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "phone_number": {
- "name": "phone_number",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "metadata": {
- "name": "metadata",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": [
- "email"
- ]
- },
- "users_clerk_user_id_unique": {
- "name": "users_clerk_user_id_unique",
- "nullsNotDistinct": false,
- "columns": [
- "clerk_user_id"
- ]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- },
- "public.visualizations": {
- "name": "visualizations",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "chat_id": {
- "name": "chat_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false
- },
- "type": {
- "name": "type",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "default": "'map_layer'"
- },
- "data": {
- "name": "data",
- "type": "jsonb",
- "primaryKey": false,
- "notNull": true
- },
- "geometry": {
- "name": "geometry",
- "type": "geometry(GEOMETRY, 4326)",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp with time zone",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "visualizations_user_id_users_id_fk": {
- "name": "visualizations_user_id_users_id_fk",
- "tableFrom": "visualizations",
- "tableTo": "users",
- "columnsFrom": [
- "user_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- },
- "visualizations_chat_id_chats_id_fk": {
- "name": "visualizations_chat_id_chats_id_fk",
- "tableFrom": "visualizations",
- "tableTo": "chats",
- "columnsFrom": [
- "chat_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
\ No newline at end of file
diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json
index d416fd22..ee11d5f4 100644
--- a/drizzle/migrations/meta/_journal.json
+++ b/drizzle/migrations/meta/_journal.json
@@ -43,34 +43,6 @@
"when": 1783699200000,
"tag": "0005_add_documents_rag",
"breakpoints": true
- },
- {
- "idx": 6,
- "version": "7",
- "when": 1784838292036,
- "tag": "0006_add_user_id_indexes",
- "breakpoints": true
- },
- {
- "idx": 7,
- "version": "7",
- "when": 1784838292037,
- "tag": "0007_create_prompt_generation_jobs",
- "breakpoints": true
- },
- {
- "idx": 8,
- "version": "7",
- "when": 1784838292038,
- "tag": "0008_allow_data_message_role",
- "breakpoints": true
- },
- {
- "idx": 9,
- "version": "7",
- "when": 1784838292039,
- "tag": "0009_add_skyfi_oauth_tokens",
- "breakpoints": true
}
]
}
diff --git a/lib/actions/chat.ts b/lib/actions/chat.ts
index b14de1ed..2ca37a6b 100644
--- a/lib/actions/chat.ts
+++ b/lib/actions/chat.ts
@@ -85,6 +85,12 @@ export async function getCrossSessionContext(userId?: string): Promise {
export async function generateReportContext(messages: AIMessage[]) {
try {
+ const userId = await getCurrentUserIdOnServer()
+ const [systemPrompt, domain] = await Promise.all([
+ userId ? getSystemPrompt(userId) : null,
+ userId ? getUserDomain(userId) : null
+ ])
+
const crossSessionContext = await getCrossSessionContext()
const activeMessages = messages
@@ -109,8 +115,8 @@ export async function generateReportContext(messages: AIMessage[]) {
const strategicContent = activeMessages.filter(msg => msg.role === 'assistant')
const [execSummary, strategicSynthesis] = await Promise.all([
- executiveSummaryAgent(crossSessionContext, activeMessages),
- strategicSynthesisAgent(sensorFusionFindings, strategicContent)
+ executiveSummaryAgent(crossSessionContext, activeMessages, systemPrompt, domain),
+ strategicSynthesisAgent(sensorFusionFindings, strategicContent, systemPrompt, domain)
])
return {
@@ -156,6 +162,54 @@ export async function getChat(id: string, userId: string): Promise {
+ if (!userId) return null
+
+ try {
+ const result = await db.select({ metadata: users.metadata })
+ .from(users)
+ .where(eq(users.id, userId))
+ .limit(1);
+
+ const metadata = result[0]?.metadata as { domain?: string } | null;
+ return metadata?.domain || null;
+ } catch (error) {
+ console.error('getUserDomain: Error:', error)
+ return null
+ }
+}
+
+export async function saveUserDomain(
+ userId: string,
+ domain: string
+): Promise<{ success?: boolean; error?: string }> {
+ if (!userId) return { error: 'User ID is required' }
+
+ try {
+ const result = await db.select({ metadata: users.metadata })
+ .from(users)
+ .where(eq(users.id, userId))
+ .limit(1);
+
+ const currentMetadata = (result[0]?.metadata || {}) as Record;
+ await db.update(users)
+ .set({
+ metadata: {
+ ...currentMetadata,
+ domain: domain || null
+ }
+ })
+ .where(eq(users.id, userId));
+
+ return { success: true }
+ } catch (error) {
+ console.error('saveUserDomain: Error:', error)
+ return { error: 'Failed to save user domain' }
+ }
+}
+
/**
* Retrieves all messages for a specific chat.
*/
diff --git a/lib/actions/skyfi.ts b/lib/actions/skyfi.ts
deleted file mode 100644
index 8bc5cd3c..00000000
--- a/lib/actions/skyfi.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-'use server';
-
-import { revalidatePath } from 'next/cache';
-import { db } from '@/lib/db';
-import { skyfiOAuthTokens } from '@/lib/db/schema';
-import { eq } from 'drizzle-orm';
-import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user';
-import { SkyfiOAuthProvider } from '@/lib/skyfi/provider';
-import crypto from 'crypto';
-
-import { headers } from 'next/headers';
-
-export async function getRedirectUri(): Promise {
- try {
- const headersList = await headers();
- const host = headersList.get('host');
- if (host) {
- const protocol = host.startsWith('localhost') || host.startsWith('127.0.0.1') ? 'http' : 'https';
- return `${protocol}://${host}/api/skyfi/callback`;
- }
- } catch (e) {
- console.warn('[Skyfi] Failed to get host from headers, falling back to ENV:', e);
- }
- const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
- return `${baseUrl}/api/skyfi/callback`;
-}
-
-/**
- * Ensures the client is registered dynamically with the SkyFi MCP server.
- * Uses an AbortController with a 10s timeout to prevent hanging.
- */
-async function ensureClientRegistered(provider: SkyfiOAuthProvider, forceRegister: boolean = false): Promise {
- if (!forceRegister) {
- const currentInfo = await provider.clientInformation();
- if (currentInfo?.client_id) {
- return currentInfo.client_id;
- }
- }
-
- console.log('[SkyFiAction] Client not registered or force-register active. Registering dynamically...');
-
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000);
-
- try {
- const res = await fetch('https://mcp.skyfi.com/oauth/register', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- client_name: 'QCX Planetary Copilot',
- redirect_uris: [provider.redirectUrl?.toString()],
- token_endpoint_auth_method: 'none',
- grant_types: ['authorization_code', 'refresh_token'],
- response_types: ['code'],
- }),
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
-
- if (!res.ok) {
- throw new Error(`Dynamic Client Registration failed: ${await res.text()}`);
- }
-
- const data = await res.json();
- await provider.saveClientInformation({
- client_id: data.client_id,
- client_secret: data.client_secret || null,
- registration_client_uri: data.registration_client_uri || null,
- registration_access_token: data.registration_access_token || null,
- } as any);
-
- return data.client_id;
- } catch (error: any) {
- clearTimeout(timeoutId);
- throw error;
- }
-}
-
-/**
- * Starts the SkyFi OAuth connection flow.
- * Performs DCR, generates PKCE, and returns the authorization URL.
- */
-export async function startSkyfiConnection(): Promise<{ url?: string; error?: string; authRequired?: boolean }> {
- const userId = await getCurrentUserIdOnServer();
- if (!userId) {
- return { authRequired: true, error: 'Please sign in to connect your SkyFi account.' };
- }
-
- try {
- const redirectUri = await getRedirectUri();
- const provider = new SkyfiOAuthProvider(userId, redirectUri);
-
- const clientId = await ensureClientRegistered(provider, true);
-
- const verifier = generateCodeVerifier();
- const challenge = generateCodeChallenge(verifier);
- const state = crypto.randomBytes(16).toString('hex');
-
- await provider.saveCodeVerifier(verifier);
- await provider.saveState(state);
-
- const authorizeUrl = new URL('https://mcp.skyfi.com/oauth/authorize');
- authorizeUrl.searchParams.set('response_type', 'code');
- authorizeUrl.searchParams.set('client_id', clientId);
- authorizeUrl.searchParams.set('redirect_uri', redirectUri);
- authorizeUrl.searchParams.set('code_challenge', challenge);
- authorizeUrl.searchParams.set('code_challenge_method', 'S256');
- authorizeUrl.searchParams.set('state', state);
- authorizeUrl.searchParams.set('scope', 'skyfi:read skyfi:write skyfi:order');
-
- return { url: authorizeUrl.toString() };
- } catch (error: any) {
- console.error('[SkyFiAction: startSkyfiConnection] Error:', error.message);
- return { error: error.message || 'Failed to start SkyFi connection.' };
- }
-}
-
-/**
- * Returns whether a valid SkyFi connection exists for the current user.
- * Uses an AbortController with a 10s timeout to prevent hanging.
- */
-export async function getSkyfiConnectionStatus(): Promise<{ connected: boolean; email?: string; budget?: string }> {
- try {
- const userId = await getCurrentUserIdOnServer();
- if (!userId) {
- return { connected: false };
- }
-
- const redirectUri = await getRedirectUri();
- const provider = new SkyfiOAuthProvider(userId, redirectUri);
- const tokens = await provider.tokens();
-
- if (!tokens || !tokens.access_token) {
- return { connected: false };
- }
-
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000);
-
- try {
- // Try a simple whoami call to verify token validity and get email/budget
- const res = await fetch('https://mcp.skyfi.com/mcp', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Skyfi-Api-Key': tokens.access_token,
- },
- body: JSON.stringify({
- jsonrpc: '2.0',
- id: 1,
- method: 'tools/call',
- params: {
- name: 'skyfi_whoami',
- arguments: {},
- },
- }),
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
-
- if (res.ok) {
- const data = await res.json();
- const content = data?.result?.content?.[0]?.text || '';
- return { connected: true, budget: content };
- }
- } catch (fetchError) {
- clearTimeout(timeoutId);
- console.warn('[SkyFiAction: getSkyfiConnectionStatus] Failed to query whoami:', fetchError);
- }
-
- return { connected: true };
- } catch (error: any) {
- console.error('[SkyFiAction: getSkyfiConnectionStatus] Error:', error.message);
- return { connected: false };
- }
-}
-
-/**
- * Disconnects the user's SkyFi account by removing token storage.
- */
-export async function disconnectSkyfi(): Promise<{ success: boolean; error?: string }> {
- try {
- const userId = await getCurrentUserIdOnServer();
- if (!userId) {
- return { success: false, error: 'Unauthorized: User not authenticated.' };
- }
-
- await db.delete(skyfiOAuthTokens)
- .where(eq(skyfiOAuthTokens.userId, userId));
-
- revalidatePath('/settings');
- return { success: true };
- } catch (error: any) {
- console.error('[SkyFiAction: disconnectSkyfi] Error:', error.message);
- return { success: false, error: error.message || 'Failed to disconnect SkyFi.' };
- }
-}
-
-function generateCodeVerifier(): string {
- return crypto.randomBytes(32).toString('base64url');
-}
-
-function generateCodeChallenge(verifier: string): string {
- return crypto.createHash('sha256').update(verifier).digest('base64url');
-}
diff --git a/lib/actions/system-prompt.ts b/lib/actions/system-prompt.ts
index ae933a99..afa0a1c6 100644
--- a/lib/actions/system-prompt.ts
+++ b/lib/actions/system-prompt.ts
@@ -11,14 +11,32 @@ const domainSchema = z.string().min(1).refine((val) => {
try {
// Basic validation to check if it's a valid URL or domain
const url = val.startsWith('http') ? val : `https://${val}`;
- new URL(url);
+ const parsed = new URL(url);
+ const hostname = parsed.hostname;
+
+ // Explicitly reject localhost and internal/local hostnames
+ if (hostname === 'localhost') return false;
+ if (hostname.endsWith('.local') || hostname.endsWith('.internal')) return false;
+
+ // Check private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and 127.0.0.1
+ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+ const match = hostname.match(ipv4Regex);
+ if (match) {
+ const octet1 = parseInt(match[1], 10);
+ const octet2 = parseInt(match[2], 10);
+ if (octet1 === 10) return false;
+ if (octet1 === 172 && octet2 >= 16 && octet2 <= 31) return false;
+ if (octet1 === 192 && octet2 === 168) return false;
+ if (octet1 === 127) return false; // Loopback
+ }
+
return true;
} catch (e) {
return false;
}
-}, { message: "Invalid domain or URL" });
+}, { message: "Invalid domain or URL. Only public domains/URLs are allowed." });
-const STALENESS_THRESHOLD_MS = 1000 * 60 * 5; // 5 minutes
+const STALENESS_THRESHOLD_MS = 1000 * 60 * 3; // 3 minutes
const WORKER_TIMEOUT_MS = 1000 * 60 * 2; // 2 minutes
export async function startSystemPromptGeneration(domain: string) {
@@ -51,60 +69,128 @@ export async function startSystemPromptGeneration(domain: string) {
}
}
+async function scrapeDomain(domain: string): Promise {
+ const targetUrl = domain.startsWith('http') ? domain : `https://${domain}`;
+ const apiKey = process.env.FIRECRAWL_API_KEY;
+ if (apiKey && apiKey !== 'mock_key' && apiKey !== '') {
+ try {
+ const firecrawl = getFirecrawlClient();
+ const scrapeResult = await firecrawl.scrapeUrl(targetUrl, {
+ formats: ['markdown'],
+ });
+ if (!scrapeResult) {
+ throw new Error('Firecrawl returned null or undefined response.');
+ }
+ if ('success' in scrapeResult && !scrapeResult.success) {
+ const errorMsg = (scrapeResult as any).error || 'Firecrawl success flag was false.';
+ throw new Error(errorMsg);
+ }
+ if ('markdown' in scrapeResult && scrapeResult.markdown) {
+ return scrapeResult.markdown;
+ }
+ throw new Error('Firecrawl response did not contain markdown content.');
+ } catch (e: any) {
+ console.warn('Firecrawl scraping failed, falling back to standard fetch scraper:', e);
+ }
+ }
+
+ // Fallback scraper using standard fetch
+ try {
+ const response = await fetch(targetUrl, {
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ }
+ });
+ if (!response.ok) {
+ throw new Error(`Standard fetch request failed with status ${response.status}`);
+ }
+ const html = await response.text();
+ // Basic extraction of visible text from HTML if it contains tags
+ let textContent = html;
+ if (html.includes(')<[^<]*)*<\/script>/gi, '')
+ .replace(/