174 lines
5.4 KiB
TypeScript
174 lines
5.4 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
|
import {
|
|
check,
|
|
date,
|
|
doublePrecision,
|
|
index,
|
|
integer,
|
|
text,
|
|
uniqueIndex
|
|
} from 'drizzle-orm/pg-core';
|
|
import { aiSchema } from './schemas';
|
|
import { id, timestamps, timestamptz } from './helpers';
|
|
|
|
// The AI tracker (/trackers/ai). No live tracking: rows arrive through the idempotent
|
|
// import scripts (scripts/ai-news-import.ts, scripts/ai-bots-import.ts) that Daisy runs on
|
|
// the server, plus the owner's manual usage form. News is public; bot_status and
|
|
// usage_entries are owner-only (checked in the server loads and actions). Like tasks and
|
|
// health, the schema is granted to the app role only (no personal_ro).
|
|
|
|
export const AI_NEWS_SECTIONS = ['ai', 'software'] as const;
|
|
export type AiNewsSection = (typeof AI_NEWS_SECTIONS)[number];
|
|
export const AI_PROVIDERS = ['claude', 'chatgpt', 'gemini', 'api'] as const;
|
|
export type AiProvider = (typeof AI_PROVIDERS)[number];
|
|
/** scheduled = a scheduled task in the provider app; ccusage = ccusage on the server. */
|
|
export const AI_USAGE_SOURCES = ['scheduled', 'ccusage', 'export', 'manual'] as const;
|
|
export type AiUsageSource = (typeof AI_USAGE_SOURCES)[number];
|
|
|
|
/** Digest items from Ilya's weekday AI/software digest; `url` is the import key. */
|
|
export const aiNewsItems = aiSchema.table(
|
|
'news_items',
|
|
{
|
|
id: id(),
|
|
date: date('date').notNull(),
|
|
title: text('title').notNull(),
|
|
url: text('url').notNull(),
|
|
note: text('note'),
|
|
section: text('section').notNull().default('ai'),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
uniqueIndex('news_items_url_key').on(t.url),
|
|
index('news_items_date_idx').on(t.date),
|
|
check('news_items_section_check', sql`${t.section} IN ('ai','software')`),
|
|
check('news_items_title_check', sql`length(btrim(${t.title})) > 0`)
|
|
]
|
|
);
|
|
|
|
/** One row per Grok Bot agent: short neutral summaries only, never raw transcripts. */
|
|
export const aiBotStatus = aiSchema.table(
|
|
'bot_status',
|
|
{
|
|
id: id(),
|
|
name: text('name').notNull(),
|
|
focus: text('focus'),
|
|
recent: text('recent'),
|
|
lastActive: timestamptz('last_active'),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
uniqueIndex('bot_status_name_key').on(t.name),
|
|
check('bot_status_name_check', sql`length(btrim(${t.name})) > 0`)
|
|
]
|
|
);
|
|
|
|
/** Dated usage numbers per provider and metric (one value per day, metric and source). */
|
|
export const aiUsageEntries = aiSchema.table(
|
|
'usage_entries',
|
|
{
|
|
id: id(),
|
|
provider: text('provider').notNull(),
|
|
date: date('date').notNull(),
|
|
metric: text('metric').notNull(),
|
|
value: doublePrecision('value').notNull(),
|
|
unit: text('unit'),
|
|
source: text('source').notNull().default('manual'),
|
|
note: text('note'),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
uniqueIndex('usage_entries_day_key').on(t.provider, t.metric, t.date, t.source),
|
|
index('usage_entries_provider_idx').on(t.provider, t.metric, t.date),
|
|
check(
|
|
'usage_entries_provider_check',
|
|
sql`${t.provider} IN ('claude','chatgpt','gemini','api')`
|
|
),
|
|
check(
|
|
'usage_entries_source_check',
|
|
sql`${t.source} IN ('scheduled','ccusage','export','manual')`
|
|
),
|
|
check('usage_entries_metric_check', sql`length(btrim(${t.metric})) > 0`)
|
|
]
|
|
);
|
|
|
|
/**
|
|
* Setup tab (owner-only): how each AI provider is set up. One row per provider card; entries
|
|
* are grouped into sections. Plan/API keys are labels only: the app refuses values that look
|
|
* like a secret, and nothing here is ever a key value.
|
|
*/
|
|
export const AI_SETUP_SECTIONS = [
|
|
'connections',
|
|
'skills',
|
|
'instructions',
|
|
'plugins',
|
|
'routines',
|
|
'agents',
|
|
'plan',
|
|
'notes'
|
|
] as const;
|
|
export type AiSetupSection = (typeof AI_SETUP_SECTIONS)[number];
|
|
export const AI_SETUP_SCOPES = ['global', 'per-repo', 'app'] as const;
|
|
export type AiSetupScope = (typeof AI_SETUP_SCOPES)[number];
|
|
export const AI_SETUP_COLORS = [
|
|
'orange',
|
|
'green',
|
|
'blue',
|
|
'violet',
|
|
'pink',
|
|
'sky',
|
|
'amber',
|
|
'zinc'
|
|
] as const;
|
|
|
|
export const aiSetupProviders = aiSchema.table(
|
|
'provider_setup_providers',
|
|
{
|
|
id: id(),
|
|
name: text('name').notNull(),
|
|
/** The products this card covers, e.g. "claude.ai + Claude Code". */
|
|
products: text('products'),
|
|
color: text('color').notNull().default('zinc'),
|
|
sortOrder: integer('sort_order').notNull().default(0),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
uniqueIndex('provider_setup_providers_name_key').on(t.name),
|
|
check('provider_setup_providers_name_check', sql`length(btrim(${t.name})) > 0`),
|
|
check(
|
|
'provider_setup_providers_color_check',
|
|
sql`${t.color} IN ('orange','green','blue','violet','pink','sky','amber','zinc')`
|
|
)
|
|
]
|
|
);
|
|
|
|
export const aiSetupEntries = aiSchema.table(
|
|
'provider_setup_entries',
|
|
{
|
|
id: id(),
|
|
providerId: integer('provider_id')
|
|
.notNull()
|
|
.references(() => aiSetupProviders.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
|
section: text('section').notNull(),
|
|
name: text('name').notNull(),
|
|
note: text('note'),
|
|
scope: text('scope'),
|
|
link: text('link'),
|
|
/** Free markdown text (custom instructions, longer notes). */
|
|
body: text('body'),
|
|
sortOrder: integer('sort_order').notNull().default(0),
|
|
...timestamps
|
|
},
|
|
(t) => [
|
|
index('provider_setup_entries_provider_idx').on(t.providerId, t.section, t.sortOrder),
|
|
check(
|
|
'provider_setup_entries_section_check',
|
|
sql`${t.section} IN ('connections','skills','instructions','plugins','routines','agents','plan','notes')`
|
|
),
|
|
check(
|
|
'provider_setup_entries_scope_check',
|
|
sql`${t.scope} IS NULL OR ${t.scope} IN ('global','per-repo','app')`
|
|
),
|
|
check('provider_setup_entries_name_check', sql`length(btrim(${t.name})) > 0`)
|
|
]
|
|
);
|