nod-shout 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/TASK-AGENT-POSTS.md +112 -0
- package/assets/shout-default.svg +5 -0
- package/bin/shout +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ai.d.ts +13 -0
- package/dist/lib/ai.d.ts.map +1 -0
- package/dist/lib/ai.js +135 -0
- package/dist/lib/ai.js.map +1 -0
- package/dist/lib/content-filter.d.ts +74 -0
- package/dist/lib/content-filter.d.ts.map +1 -0
- package/dist/lib/content-filter.js +188 -0
- package/dist/lib/content-filter.js.map +1 -0
- package/dist/lib/context-extractor.d.ts +39 -0
- package/dist/lib/context-extractor.d.ts.map +1 -0
- package/dist/lib/context-extractor.js +170 -0
- package/dist/lib/context-extractor.js.map +1 -0
- package/dist/lib/match-engine.d.ts +31 -0
- package/dist/lib/match-engine.d.ts.map +1 -0
- package/dist/lib/match-engine.js +322 -0
- package/dist/lib/match-engine.js.map +1 -0
- package/dist/lib/metadata.d.ts +7 -0
- package/dist/lib/metadata.d.ts.map +1 -0
- package/dist/lib/metadata.js +311 -0
- package/dist/lib/metadata.js.map +1 -0
- package/dist/lib/skills.d.ts +3 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +20 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/supabase.d.ts +2 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +8 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/tools/collections.d.ts +3 -0
- package/dist/tools/collections.d.ts.map +1 -0
- package/dist/tools/collections.js +142 -0
- package/dist/tools/collections.js.map +1 -0
- package/dist/tools/intros.d.ts +3 -0
- package/dist/tools/intros.d.ts.map +1 -0
- package/dist/tools/intros.js +483 -0
- package/dist/tools/intros.js.map +1 -0
- package/dist/tools/links.d.ts +3 -0
- package/dist/tools/links.d.ts.map +1 -0
- package/dist/tools/links.js +424 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/posts.d.ts +3 -0
- package/dist/tools/posts.d.ts.map +1 -0
- package/dist/tools/posts.js +212 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/settings.d.ts +3 -0
- package/dist/tools/settings.d.ts.map +1 -0
- package/dist/tools/settings.js +45 -0
- package/dist/tools/settings.js.map +1 -0
- package/dist/tools/shout_agent_curate.d.ts +28 -0
- package/dist/tools/shout_agent_curate.d.ts.map +1 -0
- package/dist/tools/shout_agent_curate.js +80 -0
- package/dist/tools/shout_agent_curate.js.map +1 -0
- package/dist/tools/social.d.ts +3 -0
- package/dist/tools/social.d.ts.map +1 -0
- package/dist/tools/social.js +169 -0
- package/dist/tools/social.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +24 -0
- package/quick-test.ts +22 -0
- package/regenerate-summaries.ts +111 -0
- package/save-jeffries-shout.ts +38 -0
- package/save-openai-shout.ts +35 -0
- package/save-prcarly.ts +46 -0
- package/save-shout.ts +35 -0
- package/save-techcrunch-shout.ts +59 -0
- package/save-zdnet-shout.ts +36 -0
- package/skills/collection-routing/SKILL.md +34 -0
- package/skills/link-summary/SKILL.md +53 -0
- package/skills/tagging-and-routing/SKILL.md +54 -0
- package/src/index.ts +32 -0
- package/src/lib/ai.ts +166 -0
- package/src/lib/content-filter.ts +258 -0
- package/src/lib/metadata.ts +353 -0
- package/src/lib/skills.ts +21 -0
- package/src/lib/supabase.ts +12 -0
- package/src/tools/collections.ts +182 -0
- package/src/tools/links.ts +524 -0
- package/src/tools/posts.ts +264 -0
- package/src/tools/settings.ts +55 -0
- package/src/tools/shout_agent_curate.ts +95 -0
- package/src/tools/social.ts +206 -0
- package/src/types.ts +66 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/migrations/001_initial_schema.sql +147 -0
- package/supabase/migrations/20260317010000_decouple_profiles_from_auth.sql +9 -0
- package/supabase/migrations/20260317020000_agent_curation.sql +10 -0
- package/supabase/migrations/20260320000000_agent_posts.sql +41 -0
- package/supabase/migrations/20260320120000_fix_draft_fk.sql +2 -0
- package/supabase/migrations/20260320130000_fix_identity.sql +17 -0
- package/supabase/migrations/20260320170000_intros.sql +118 -0
- package/test-model-comparison.ts +89 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v14.4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fix-optimized-search-function
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v1.43.3
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
-- nod-shout initial schema
|
|
2
|
+
-- run this against your supabase project
|
|
3
|
+
|
|
4
|
+
-- profiles table (extends auth.users)
|
|
5
|
+
create table if not exists profiles (
|
|
6
|
+
id uuid primary key references auth.users on delete cascade,
|
|
7
|
+
username text unique not null,
|
|
8
|
+
display_name text,
|
|
9
|
+
bio text,
|
|
10
|
+
avatar_url text,
|
|
11
|
+
created_at timestamptz default now(),
|
|
12
|
+
updated_at timestamptz default now()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- collections table
|
|
16
|
+
create table if not exists collections (
|
|
17
|
+
id uuid primary key default gen_random_uuid(),
|
|
18
|
+
user_id uuid not null references profiles(id) on delete cascade,
|
|
19
|
+
name text not null,
|
|
20
|
+
description text,
|
|
21
|
+
slug text not null,
|
|
22
|
+
visibility text default 'public' check (visibility in ('public', 'private', 'unlisted')),
|
|
23
|
+
auto_rules jsonb,
|
|
24
|
+
created_at timestamptz default now(),
|
|
25
|
+
updated_at timestamptz default now(),
|
|
26
|
+
unique(user_id, slug)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- shouts table
|
|
30
|
+
create table if not exists shouts (
|
|
31
|
+
id uuid primary key default gen_random_uuid(),
|
|
32
|
+
user_id uuid not null references profiles(id) on delete cascade,
|
|
33
|
+
url text not null,
|
|
34
|
+
title text,
|
|
35
|
+
description text,
|
|
36
|
+
summary text,
|
|
37
|
+
user_take text,
|
|
38
|
+
image_url text,
|
|
39
|
+
tags text[],
|
|
40
|
+
category text,
|
|
41
|
+
collection_id uuid references collections(id) on delete set null,
|
|
42
|
+
source text default 'conversation',
|
|
43
|
+
agent_context text,
|
|
44
|
+
visibility text default 'public' check (visibility in ('public', 'private', 'unlisted')),
|
|
45
|
+
created_at timestamptz default now(),
|
|
46
|
+
updated_at timestamptz default now()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
-- subscriptions table
|
|
50
|
+
create table if not exists subscriptions (
|
|
51
|
+
id uuid primary key default gen_random_uuid(),
|
|
52
|
+
follower_id uuid not null references profiles(id) on delete cascade,
|
|
53
|
+
following_id uuid not null references profiles(id) on delete cascade,
|
|
54
|
+
collection_id uuid references collections(id) on delete cascade,
|
|
55
|
+
notify boolean default true,
|
|
56
|
+
created_at timestamptz default now()
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
-- indexes
|
|
60
|
+
create index if not exists idx_shouts_user_created on shouts(user_id, created_at desc);
|
|
61
|
+
create index if not exists idx_shouts_url on shouts(url);
|
|
62
|
+
create index if not exists idx_collections_user on collections(user_id);
|
|
63
|
+
create index if not exists idx_subscriptions_follower on subscriptions(follower_id);
|
|
64
|
+
create index if not exists idx_subscriptions_following on subscriptions(following_id);
|
|
65
|
+
|
|
66
|
+
-- updated_at trigger function
|
|
67
|
+
create or replace function update_updated_at()
|
|
68
|
+
returns trigger as $$
|
|
69
|
+
begin
|
|
70
|
+
new.updated_at = now();
|
|
71
|
+
return new;
|
|
72
|
+
end;
|
|
73
|
+
$$ language plpgsql;
|
|
74
|
+
|
|
75
|
+
-- apply updated_at triggers
|
|
76
|
+
create trigger profiles_updated_at
|
|
77
|
+
before update on profiles
|
|
78
|
+
for each row execute function update_updated_at();
|
|
79
|
+
|
|
80
|
+
create trigger collections_updated_at
|
|
81
|
+
before update on collections
|
|
82
|
+
for each row execute function update_updated_at();
|
|
83
|
+
|
|
84
|
+
create trigger shouts_updated_at
|
|
85
|
+
before update on shouts
|
|
86
|
+
for each row execute function update_updated_at();
|
|
87
|
+
|
|
88
|
+
-- RLS policies
|
|
89
|
+
alter table profiles enable row level security;
|
|
90
|
+
alter table shouts enable row level security;
|
|
91
|
+
alter table collections enable row level security;
|
|
92
|
+
alter table subscriptions enable row level security;
|
|
93
|
+
|
|
94
|
+
-- profiles: readable by all, editable by owner
|
|
95
|
+
create policy "profiles_select_all" on profiles
|
|
96
|
+
for select using (true);
|
|
97
|
+
|
|
98
|
+
create policy "profiles_update_own" on profiles
|
|
99
|
+
for update using (auth.uid() = id);
|
|
100
|
+
|
|
101
|
+
create policy "profiles_insert_own" on profiles
|
|
102
|
+
for insert with check (auth.uid() = id);
|
|
103
|
+
|
|
104
|
+
-- shouts: read public, manage own
|
|
105
|
+
create policy "shouts_select_public" on shouts
|
|
106
|
+
for select using (
|
|
107
|
+
visibility = 'public'
|
|
108
|
+
or user_id = auth.uid()
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
create policy "shouts_insert_own" on shouts
|
|
112
|
+
for insert with check (user_id = auth.uid());
|
|
113
|
+
|
|
114
|
+
create policy "shouts_update_own" on shouts
|
|
115
|
+
for update using (user_id = auth.uid());
|
|
116
|
+
|
|
117
|
+
create policy "shouts_delete_own" on shouts
|
|
118
|
+
for delete using (user_id = auth.uid());
|
|
119
|
+
|
|
120
|
+
-- collections: read public, manage own
|
|
121
|
+
create policy "collections_select_public" on collections
|
|
122
|
+
for select using (
|
|
123
|
+
visibility = 'public'
|
|
124
|
+
or user_id = auth.uid()
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
create policy "collections_insert_own" on collections
|
|
128
|
+
for insert with check (user_id = auth.uid());
|
|
129
|
+
|
|
130
|
+
create policy "collections_update_own" on collections
|
|
131
|
+
for update using (user_id = auth.uid());
|
|
132
|
+
|
|
133
|
+
create policy "collections_delete_own" on collections
|
|
134
|
+
for delete using (user_id = auth.uid());
|
|
135
|
+
|
|
136
|
+
-- subscriptions: manageable by follower
|
|
137
|
+
create policy "subscriptions_select_own" on subscriptions
|
|
138
|
+
for select using (
|
|
139
|
+
follower_id = auth.uid()
|
|
140
|
+
or following_id = auth.uid()
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
create policy "subscriptions_insert_own" on subscriptions
|
|
144
|
+
for insert with check (follower_id = auth.uid());
|
|
145
|
+
|
|
146
|
+
create policy "subscriptions_delete_own" on subscriptions
|
|
147
|
+
for delete using (follower_id = auth.uid());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- decouple profiles from auth.users so mcp server can create profiles directly
|
|
2
|
+
-- we'll re-add the link when we have real auth flow
|
|
3
|
+
|
|
4
|
+
alter table profiles drop constraint profiles_id_fkey;
|
|
5
|
+
|
|
6
|
+
-- also allow the mcp server to create profiles without auth context
|
|
7
|
+
-- by adding a service-level insert policy
|
|
8
|
+
create policy "profiles_service_insert" on profiles
|
|
9
|
+
for insert with check (true);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- add draft visibility for agent-curated links pending approval
|
|
2
|
+
alter table shouts drop constraint if exists shouts_visibility_check;
|
|
3
|
+
alter table shouts add constraint shouts_visibility_check
|
|
4
|
+
check (visibility in ('public', 'private', 'unlisted', 'draft'));
|
|
5
|
+
|
|
6
|
+
-- add index for source filtering
|
|
7
|
+
create index if not exists idx_shouts_source on shouts(source);
|
|
8
|
+
|
|
9
|
+
-- add index for draft review
|
|
10
|
+
create index if not exists idx_shouts_draft on shouts(user_id, visibility) where visibility = 'draft';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Agent text posts: draft_posts table + shouts extensions
|
|
2
|
+
-- Migration: 20260320_agent_posts
|
|
3
|
+
|
|
4
|
+
-- Draft posts table
|
|
5
|
+
CREATE TABLE IF NOT EXISTS draft_posts (
|
|
6
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
7
|
+
user_id UUID NOT NULL REFERENCES auth.users(id),
|
|
8
|
+
text TEXT NOT NULL,
|
|
9
|
+
filtered_text TEXT,
|
|
10
|
+
tags TEXT[] DEFAULT '{}',
|
|
11
|
+
category TEXT,
|
|
12
|
+
collection_id UUID REFERENCES collections(id),
|
|
13
|
+
visibility TEXT DEFAULT 'public' CHECK (visibility IN ('public', 'private', 'unlisted')),
|
|
14
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'published')),
|
|
15
|
+
filter_report TEXT,
|
|
16
|
+
source TEXT DEFAULT 'agent',
|
|
17
|
+
agent_id TEXT,
|
|
18
|
+
published_shout_id UUID REFERENCES shouts(id),
|
|
19
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
20
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX idx_draft_posts_user_status ON draft_posts(user_id, status);
|
|
24
|
+
CREATE INDEX idx_draft_posts_created ON draft_posts(created_at DESC);
|
|
25
|
+
|
|
26
|
+
-- Create user_settings table if it doesn't exist
|
|
27
|
+
CREATE TABLE IF NOT EXISTS user_settings (
|
|
28
|
+
user_id UUID PRIMARY KEY REFERENCES auth.users(id),
|
|
29
|
+
auto_publish BOOLEAN DEFAULT false,
|
|
30
|
+
auto_publish_filter_level TEXT DEFAULT 'strict',
|
|
31
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
32
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Add columns in case table existed without them
|
|
36
|
+
ALTER TABLE user_settings ADD COLUMN IF NOT EXISTS auto_publish BOOLEAN DEFAULT false;
|
|
37
|
+
ALTER TABLE user_settings ADD COLUMN IF NOT EXISTS auto_publish_filter_level TEXT DEFAULT 'strict';
|
|
38
|
+
|
|
39
|
+
-- Add shout_type to shouts table
|
|
40
|
+
ALTER TABLE shouts ADD COLUMN IF NOT EXISTS shout_type TEXT DEFAULT 'link' CHECK (shout_type IN ('link', 'post'));
|
|
41
|
+
ALTER TABLE shouts ADD COLUMN IF NOT EXISTS draft_id UUID REFERENCES draft_posts(id);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Fix identity: draft_posts should reference profiles, not auth.users
|
|
2
|
+
-- This lets mock user IDs (like fubz) work with drafts
|
|
3
|
+
|
|
4
|
+
ALTER TABLE draft_posts DROP CONSTRAINT IF EXISTS draft_posts_user_id_fkey;
|
|
5
|
+
ALTER TABLE draft_posts ADD CONSTRAINT draft_posts_user_id_fkey FOREIGN KEY (user_id) REFERENCES profiles(id);
|
|
6
|
+
|
|
7
|
+
-- Also fix user_settings to reference profiles
|
|
8
|
+
ALTER TABLE user_settings DROP CONSTRAINT IF EXISTS user_settings_user_id_fkey;
|
|
9
|
+
ALTER TABLE user_settings DROP CONSTRAINT IF EXISTS user_settings_pkey;
|
|
10
|
+
ALTER TABLE user_settings ADD PRIMARY KEY (user_id);
|
|
11
|
+
|
|
12
|
+
-- Move all data from real auth user to mock fubz profile
|
|
13
|
+
UPDATE draft_posts SET user_id = '00000000-0000-0000-0000-000000000001'
|
|
14
|
+
WHERE user_id = '8c18bb5d-5d3c-44c8-b93c-cb96285156bc';
|
|
15
|
+
|
|
16
|
+
-- Clean up the fubz-agent profile (no longer needed)
|
|
17
|
+
DELETE FROM profiles WHERE id = '8c18bb5d-5d3c-44c8-b93c-cb96285156bc';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Migration: nod intros — agent-brokered warm introductions
|
|
3
|
+
-- ============================================================================
|
|
4
|
+
|
|
5
|
+
-- clean slate (safe for fresh deploy)
|
|
6
|
+
DROP TABLE IF EXISTS intro_consent_log CASCADE;
|
|
7
|
+
DROP TABLE IF EXISTS intros CASCADE;
|
|
8
|
+
DROP TABLE IF EXISTS intro_matches CASCADE;
|
|
9
|
+
DROP TABLE IF EXISTS intro_profiles CASCADE;
|
|
10
|
+
|
|
11
|
+
-- context profiles: what users are working on, need, and can offer
|
|
12
|
+
CREATE TABLE IF NOT EXISTS intro_profiles (
|
|
13
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
14
|
+
user_id TEXT NOT NULL UNIQUE, -- matches profiles.user_id from shout
|
|
15
|
+
opted_in BOOLEAN DEFAULT false,
|
|
16
|
+
|
|
17
|
+
-- structured context (auto-extracted + manually set)
|
|
18
|
+
current_projects JSONB DEFAULT '[]', -- [{name, description, stage, updated_at}]
|
|
19
|
+
needs JSONB DEFAULT '[]', -- [{description, urgency, category, created_at}]
|
|
20
|
+
offers JSONB DEFAULT '[]', -- [{description, category, confidence}]
|
|
21
|
+
interests TEXT[] DEFAULT '{}',
|
|
22
|
+
expertise TEXT[] DEFAULT '{}',
|
|
23
|
+
industries TEXT[] DEFAULT '{}',
|
|
24
|
+
|
|
25
|
+
-- preferences
|
|
26
|
+
intro_frequency TEXT DEFAULT 'weekly' CHECK (intro_frequency IN ('daily', 'weekly', 'monthly', 'on_demand')),
|
|
27
|
+
trust_radius TEXT DEFAULT 'open' CHECK (trust_radius IN ('contacts_only', 'friends_of_friends', 'open')),
|
|
28
|
+
blocklist TEXT[] DEFAULT '{}',
|
|
29
|
+
preferred_intro_method TEXT DEFAULT 'agent_chat' CHECK (preferred_intro_method IN ('agent_chat', 'email', 'text')),
|
|
30
|
+
|
|
31
|
+
-- state
|
|
32
|
+
paused BOOLEAN DEFAULT false,
|
|
33
|
+
last_match_check TIMESTAMPTZ,
|
|
34
|
+
last_profile_update TIMESTAMPTZ DEFAULT NOW(),
|
|
35
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
36
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- matches: potential intros detected by the match engine
|
|
40
|
+
CREATE TABLE IF NOT EXISTS intro_matches (
|
|
41
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
42
|
+
user_a_id TEXT NOT NULL, -- first user
|
|
43
|
+
user_b_id TEXT NOT NULL, -- second user
|
|
44
|
+
|
|
45
|
+
-- match details
|
|
46
|
+
score FLOAT NOT NULL,
|
|
47
|
+
match_reason TEXT NOT NULL, -- human-readable explanation
|
|
48
|
+
match_details JSONB DEFAULT '{}', -- {need_offer_score, interest_score, etc}
|
|
49
|
+
|
|
50
|
+
-- consent state
|
|
51
|
+
user_a_status TEXT DEFAULT 'pending' CHECK (user_a_status IN ('pending', 'approved', 'declined', 'deferred', 'expired')),
|
|
52
|
+
user_b_status TEXT DEFAULT 'pending' CHECK (user_b_status IN ('pending', 'approved', 'declined', 'deferred', 'expired')),
|
|
53
|
+
user_a_responded_at TIMESTAMPTZ,
|
|
54
|
+
user_b_responded_at TIMESTAMPTZ,
|
|
55
|
+
|
|
56
|
+
-- what each user sees (sanitized — no raw profile data)
|
|
57
|
+
user_a_pitch TEXT, -- what user A sees about user B
|
|
58
|
+
user_b_pitch TEXT, -- what user B sees about user A
|
|
59
|
+
|
|
60
|
+
-- lifecycle
|
|
61
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'matched', 'completed', 'expired', 'declined')),
|
|
62
|
+
expires_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '7 days'),
|
|
63
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
64
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
65
|
+
|
|
66
|
+
-- prevent duplicate matches
|
|
67
|
+
UNIQUE(user_a_id, user_b_id)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
-- completed intros: record of successful connections
|
|
71
|
+
CREATE TABLE IF NOT EXISTS intros (
|
|
72
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
73
|
+
match_id UUID REFERENCES intro_matches(id) ON DELETE SET NULL,
|
|
74
|
+
user_a_id TEXT NOT NULL,
|
|
75
|
+
user_b_id TEXT NOT NULL,
|
|
76
|
+
|
|
77
|
+
-- intro context shared with both parties
|
|
78
|
+
shared_context TEXT,
|
|
79
|
+
intro_method TEXT NOT NULL, -- how they were connected
|
|
80
|
+
|
|
81
|
+
-- outcome tracking
|
|
82
|
+
outcome TEXT CHECK (outcome IN ('connected', 'no_response', 'positive', 'neutral', 'negative')),
|
|
83
|
+
user_a_feedback TEXT,
|
|
84
|
+
user_b_feedback TEXT,
|
|
85
|
+
|
|
86
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
87
|
+
completed_at TIMESTAMPTZ
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- consent log: audit trail for all consent-related actions
|
|
91
|
+
CREATE TABLE IF NOT EXISTS intro_consent_log (
|
|
92
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
93
|
+
user_id TEXT NOT NULL,
|
|
94
|
+
action TEXT NOT NULL, -- 'opt_in', 'opt_out', 'approve_match', 'decline_match', 'defer_match', 'pause', 'unpause', 'forget'
|
|
95
|
+
target_match_id UUID REFERENCES intro_matches(id) ON DELETE SET NULL,
|
|
96
|
+
metadata JSONB DEFAULT '{}',
|
|
97
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
-- indexes
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_intro_profiles_user ON intro_profiles(user_id);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_intro_profiles_opted_in ON intro_profiles(opted_in) WHERE opted_in IS TRUE AND paused IS NOT TRUE;
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_intro_matches_users ON intro_matches(user_a_id, user_b_id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_intro_matches_status ON intro_matches(status) WHERE status = 'pending';
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_intros_users ON intros(user_a_id, user_b_id);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_consent_log_user ON intro_consent_log(user_id);
|
|
107
|
+
|
|
108
|
+
-- RLS
|
|
109
|
+
ALTER TABLE intro_profiles ENABLE ROW LEVEL SECURITY;
|
|
110
|
+
ALTER TABLE intro_matches ENABLE ROW LEVEL SECURITY;
|
|
111
|
+
ALTER TABLE intros ENABLE ROW LEVEL SECURITY;
|
|
112
|
+
ALTER TABLE intro_consent_log ENABLE ROW LEVEL SECURITY;
|
|
113
|
+
|
|
114
|
+
-- service role access (MCP server uses service key)
|
|
115
|
+
CREATE POLICY "Service role full access" ON intro_profiles FOR ALL USING (true);
|
|
116
|
+
CREATE POLICY "Service role full access" ON intro_matches FOR ALL USING (true);
|
|
117
|
+
CREATE POLICY "Service role full access" ON intros FOR ALL USING (true);
|
|
118
|
+
CREATE POLICY "Service role full access" ON intro_consent_log FOR ALL USING (true);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
const env = Object.fromEntries(
|
|
5
|
+
readFileSync('.env', 'utf8').split('\n').filter(l => l.includes('=')).map(l => {
|
|
6
|
+
const [k, ...v] = l.split('=');
|
|
7
|
+
return [k.trim(), v.join('=').trim()];
|
|
8
|
+
})
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
|
|
12
|
+
const OPENAI_API_KEY = env.OPENAI_API_KEY;
|
|
13
|
+
|
|
14
|
+
const prompt = (url: string, title: string, bodyText: string) => `Write a nod shout card summary.
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
- 2 short sentences max
|
|
18
|
+
- sentence 1: what the thing is or what happened
|
|
19
|
+
- sentence 2: a reason to click or a key detail
|
|
20
|
+
- include concrete specifics when available (names, numbers, claims)
|
|
21
|
+
- write like you're texting a friend, not writing a blurb
|
|
22
|
+
- never start with: "this article discusses", "this post is about", "the page explains", "X tweeted that", "post by", "page", "clicking gives", "clicking provides", "clicking reveals"
|
|
23
|
+
- never use: "not just X but Y", "isn't just X"
|
|
24
|
+
- no filler like "thought-provoking", "must-read", "fascinating", "insightful"
|
|
25
|
+
|
|
26
|
+
url: ${url}
|
|
27
|
+
title: ${title || "unknown"}
|
|
28
|
+
${bodyText ? `page content: ${bodyText}` : ""}
|
|
29
|
+
|
|
30
|
+
respond in json only, no markdown:
|
|
31
|
+
{"summary": "..."}`;
|
|
32
|
+
|
|
33
|
+
async function summarize(model: string, url: string, title: string, bodyText: string): Promise<string> {
|
|
34
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${OPENAI_API_KEY}` },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model,
|
|
39
|
+
messages: [{ role: "user", content: prompt(url, title, bodyText) }],
|
|
40
|
+
...(model.startsWith('gpt-5')
|
|
41
|
+
? { max_completion_tokens: 250 }
|
|
42
|
+
: { temperature: 0.7, max_tokens: 250 }),
|
|
43
|
+
response_format: { type: "json_object" },
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
if (data.error) return `ERROR: ${data.error.message}`;
|
|
48
|
+
const parsed = JSON.parse(data.choices?.[0]?.message?.content || '{}');
|
|
49
|
+
return parsed.summary || 'no summary';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const { data: profile } = await supabase.from('profiles').select('id').eq('username', 'fubz').single();
|
|
54
|
+
if (!profile) throw new Error('no profile');
|
|
55
|
+
|
|
56
|
+
const { data: shouts } = await supabase
|
|
57
|
+
.from('shouts')
|
|
58
|
+
.select('id, url, title, summary')
|
|
59
|
+
.eq('user_id', profile.id)
|
|
60
|
+
.order('created_at', { ascending: false })
|
|
61
|
+
.limit(6);
|
|
62
|
+
|
|
63
|
+
if (!shouts) throw new Error('no shouts');
|
|
64
|
+
|
|
65
|
+
for (const shout of shouts) {
|
|
66
|
+
let bodyText = "";
|
|
67
|
+
try {
|
|
68
|
+
const pageRes = await fetch(shout.url, {
|
|
69
|
+
headers: { "User-Agent": "Mozilla/5.0 (compatible; NodShout/0.1)" },
|
|
70
|
+
signal: AbortSignal.timeout(10000),
|
|
71
|
+
});
|
|
72
|
+
if (pageRes.ok) {
|
|
73
|
+
const html = await pageRes.text();
|
|
74
|
+
bodyText = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 1500);
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
const [current, gpt5mini] = await Promise.all([
|
|
79
|
+
Promise.resolve(shout.summary || ''),
|
|
80
|
+
summarize('gpt-5-mini', shout.url, shout.title || '', bodyText),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
console.log(`=== ${(shout.title || shout.url).slice(0, 55)}`);
|
|
84
|
+
console.log(` 4.1-mini: ${current.slice(0, 140)}`);
|
|
85
|
+
console.log(` 5-mini: ${gpt5mini.slice(0, 140)}`);
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|