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.
Files changed (110) hide show
  1. package/README.md +82 -0
  2. package/TASK-AGENT-POSTS.md +112 -0
  3. package/assets/shout-default.svg +5 -0
  4. package/bin/shout +68 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +29 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/ai.d.ts +13 -0
  10. package/dist/lib/ai.d.ts.map +1 -0
  11. package/dist/lib/ai.js +135 -0
  12. package/dist/lib/ai.js.map +1 -0
  13. package/dist/lib/content-filter.d.ts +74 -0
  14. package/dist/lib/content-filter.d.ts.map +1 -0
  15. package/dist/lib/content-filter.js +188 -0
  16. package/dist/lib/content-filter.js.map +1 -0
  17. package/dist/lib/context-extractor.d.ts +39 -0
  18. package/dist/lib/context-extractor.d.ts.map +1 -0
  19. package/dist/lib/context-extractor.js +170 -0
  20. package/dist/lib/context-extractor.js.map +1 -0
  21. package/dist/lib/match-engine.d.ts +31 -0
  22. package/dist/lib/match-engine.d.ts.map +1 -0
  23. package/dist/lib/match-engine.js +322 -0
  24. package/dist/lib/match-engine.js.map +1 -0
  25. package/dist/lib/metadata.d.ts +7 -0
  26. package/dist/lib/metadata.d.ts.map +1 -0
  27. package/dist/lib/metadata.js +311 -0
  28. package/dist/lib/metadata.js.map +1 -0
  29. package/dist/lib/skills.d.ts +3 -0
  30. package/dist/lib/skills.d.ts.map +1 -0
  31. package/dist/lib/skills.js +20 -0
  32. package/dist/lib/skills.js.map +1 -0
  33. package/dist/lib/supabase.d.ts +2 -0
  34. package/dist/lib/supabase.d.ts.map +1 -0
  35. package/dist/lib/supabase.js +8 -0
  36. package/dist/lib/supabase.js.map +1 -0
  37. package/dist/tools/collections.d.ts +3 -0
  38. package/dist/tools/collections.d.ts.map +1 -0
  39. package/dist/tools/collections.js +142 -0
  40. package/dist/tools/collections.js.map +1 -0
  41. package/dist/tools/intros.d.ts +3 -0
  42. package/dist/tools/intros.d.ts.map +1 -0
  43. package/dist/tools/intros.js +483 -0
  44. package/dist/tools/intros.js.map +1 -0
  45. package/dist/tools/links.d.ts +3 -0
  46. package/dist/tools/links.d.ts.map +1 -0
  47. package/dist/tools/links.js +424 -0
  48. package/dist/tools/links.js.map +1 -0
  49. package/dist/tools/posts.d.ts +3 -0
  50. package/dist/tools/posts.d.ts.map +1 -0
  51. package/dist/tools/posts.js +212 -0
  52. package/dist/tools/posts.js.map +1 -0
  53. package/dist/tools/settings.d.ts +3 -0
  54. package/dist/tools/settings.d.ts.map +1 -0
  55. package/dist/tools/settings.js +45 -0
  56. package/dist/tools/settings.js.map +1 -0
  57. package/dist/tools/shout_agent_curate.d.ts +28 -0
  58. package/dist/tools/shout_agent_curate.d.ts.map +1 -0
  59. package/dist/tools/shout_agent_curate.js +80 -0
  60. package/dist/tools/shout_agent_curate.js.map +1 -0
  61. package/dist/tools/social.d.ts +3 -0
  62. package/dist/tools/social.d.ts.map +1 -0
  63. package/dist/tools/social.js +169 -0
  64. package/dist/tools/social.js.map +1 -0
  65. package/dist/types.d.ts +60 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +3 -0
  68. package/dist/types.js.map +1 -0
  69. package/package.json +24 -0
  70. package/quick-test.ts +22 -0
  71. package/regenerate-summaries.ts +111 -0
  72. package/save-jeffries-shout.ts +38 -0
  73. package/save-openai-shout.ts +35 -0
  74. package/save-prcarly.ts +46 -0
  75. package/save-shout.ts +35 -0
  76. package/save-techcrunch-shout.ts +59 -0
  77. package/save-zdnet-shout.ts +36 -0
  78. package/skills/collection-routing/SKILL.md +34 -0
  79. package/skills/link-summary/SKILL.md +53 -0
  80. package/skills/tagging-and-routing/SKILL.md +54 -0
  81. package/src/index.ts +32 -0
  82. package/src/lib/ai.ts +166 -0
  83. package/src/lib/content-filter.ts +258 -0
  84. package/src/lib/metadata.ts +353 -0
  85. package/src/lib/skills.ts +21 -0
  86. package/src/lib/supabase.ts +12 -0
  87. package/src/tools/collections.ts +182 -0
  88. package/src/tools/links.ts +524 -0
  89. package/src/tools/posts.ts +264 -0
  90. package/src/tools/settings.ts +55 -0
  91. package/src/tools/shout_agent_curate.ts +95 -0
  92. package/src/tools/social.ts +206 -0
  93. package/src/types.ts +66 -0
  94. package/supabase/.temp/cli-latest +1 -0
  95. package/supabase/.temp/gotrue-version +1 -0
  96. package/supabase/.temp/pooler-url +1 -0
  97. package/supabase/.temp/postgres-version +1 -0
  98. package/supabase/.temp/project-ref +1 -0
  99. package/supabase/.temp/rest-version +1 -0
  100. package/supabase/.temp/storage-migration +1 -0
  101. package/supabase/.temp/storage-version +1 -0
  102. package/supabase/migrations/001_initial_schema.sql +147 -0
  103. package/supabase/migrations/20260317010000_decouple_profiles_from_auth.sql +9 -0
  104. package/supabase/migrations/20260317020000_agent_curation.sql +10 -0
  105. package/supabase/migrations/20260320000000_agent_posts.sql +41 -0
  106. package/supabase/migrations/20260320120000_fix_draft_fk.sql +2 -0
  107. package/supabase/migrations/20260320130000_fix_identity.sql +17 -0
  108. package/supabase/migrations/20260320170000_intros.sql +118 -0
  109. package/test-model-comparison.ts +89 -0
  110. 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,2 @@
1
+ ALTER TABLE draft_posts DROP CONSTRAINT IF EXISTS draft_posts_user_id_fkey;
2
+ ALTER TABLE draft_posts ADD CONSTRAINT draft_posts_user_id_fkey FOREIGN KEY (user_id) REFERENCES profiles(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
+ }