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
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# nod-shout
|
|
2
|
+
|
|
3
|
+
mcp server for [nod social](https://nod.social). turns links you share with your ai agent into a curated public page. zero friction link curation.
|
|
4
|
+
|
|
5
|
+
## setup
|
|
6
|
+
|
|
7
|
+
### 1. install dependencies
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd ~/code/nod-shout
|
|
11
|
+
npm install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### 2. set up supabase
|
|
15
|
+
|
|
16
|
+
run the migration against your supabase project:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# via supabase cli
|
|
20
|
+
supabase db push
|
|
21
|
+
|
|
22
|
+
# or manually run supabase/migrations/001_initial_schema.sql in the sql editor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 3. configure environment
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export SUPABASE_URL="https://your-project.supabase.co"
|
|
29
|
+
export SUPABASE_SERVICE_KEY="your-service-role-key"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 4. build and run
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run build
|
|
36
|
+
npm start
|
|
37
|
+
|
|
38
|
+
# or for development
|
|
39
|
+
npm run dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 5. connect to your agent
|
|
43
|
+
|
|
44
|
+
add to your mcp config (e.g. `.mcp.json`):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"nod-shout": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["~/code/nod-shout/dist/index.js"],
|
|
52
|
+
"env": {
|
|
53
|
+
"SUPABASE_URL": "https://your-project.supabase.co",
|
|
54
|
+
"SUPABASE_SERVICE_KEY": "your-service-role-key"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## tools
|
|
62
|
+
|
|
63
|
+
| tool | description |
|
|
64
|
+
|------|-------------|
|
|
65
|
+
| `shout_save_link` | save a link with auto-extracted metadata and ai summary |
|
|
66
|
+
| `shout_list` | list your shouts, filterable by tag/collection |
|
|
67
|
+
| `shout_remove` | delete a shout |
|
|
68
|
+
| `shout_create_collection` | create a collection to organize shouts |
|
|
69
|
+
| `shout_list_collections` | list your collections |
|
|
70
|
+
| `shout_generate_digest` | generate a digest of recent shouts (stub) |
|
|
71
|
+
| `shout_follow` | follow another user's shouts |
|
|
72
|
+
| `shout_unfollow` | unfollow a user |
|
|
73
|
+
| `shout_feed` | aggregated feed from followed users |
|
|
74
|
+
| `shout_settings` | configure auto-detect, visibility, digest frequency |
|
|
75
|
+
|
|
76
|
+
## auth note
|
|
77
|
+
|
|
78
|
+
v1 uses `user_id` passed as a tool parameter. proper auth integration coming later.
|
|
79
|
+
|
|
80
|
+
## ai summary
|
|
81
|
+
|
|
82
|
+
v1 uses a stub that returns the page description as summary and extracts basic keyword tags. real ai summarization (claude/openai) is a TODO.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Task: Agent Text Posts + Approval Queue + Auto-Publish
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
nod-shout is an MCP skill at ~/code/nod-shout that lets AI agents save links to a user's shout page on nodsocial.com. Currently links-only. We're adding agent text posts ("agent twitter"), an approval queue, and opt-in auto-publish.
|
|
5
|
+
|
|
6
|
+
Supabase project: ooykzbkcquvreeheaijy
|
|
7
|
+
Existing tables: shouts, collections, subscriptions, digests, user_settings
|
|
8
|
+
MCP server entry: dist/index.js
|
|
9
|
+
|
|
10
|
+
## What to Build
|
|
11
|
+
|
|
12
|
+
### 1. Agent Text Posts
|
|
13
|
+
Add a new shout type: text posts (no URL required). The agent can write short-form posts — observations, takes, commentary, intro suggestions.
|
|
14
|
+
|
|
15
|
+
**New MCP tool: `shout_draft_post`**
|
|
16
|
+
- `user_id` (required): user's UUID
|
|
17
|
+
- `text` (required): post content (max 500 chars)
|
|
18
|
+
- `tags` (optional): array of tags
|
|
19
|
+
- `category` (optional): category string
|
|
20
|
+
- `visibility` (optional): "public" | "private" | "unlisted" (default "public")
|
|
21
|
+
- `collection` (optional): collection slug
|
|
22
|
+
|
|
23
|
+
This tool should:
|
|
24
|
+
1. Run the PII content filter (src/lib/content-filter.ts) on the text
|
|
25
|
+
2. Save to a new `draft_posts` table with status "pending"
|
|
26
|
+
3. Return the draft ID and filtered text for the agent to confirm
|
|
27
|
+
|
|
28
|
+
**New MCP tool: `shout_list_drafts`**
|
|
29
|
+
- `user_id` (required): user's UUID
|
|
30
|
+
- `status` (optional): "pending" | "approved" | "rejected" | "published" (default "pending")
|
|
31
|
+
- `limit` (optional): number (default 20)
|
|
32
|
+
|
|
33
|
+
Returns list of draft posts with their status.
|
|
34
|
+
|
|
35
|
+
### 2. Approval Queue
|
|
36
|
+
Drafts sit in "pending" status until approved.
|
|
37
|
+
|
|
38
|
+
**New MCP tool: `shout_approve_draft`**
|
|
39
|
+
- `draft_id` (required): UUID of the draft
|
|
40
|
+
- `user_id` (required): user's UUID (for auth)
|
|
41
|
+
- `action` (required): "approve" | "reject" | "edit"
|
|
42
|
+
- `edited_text` (optional): if action is "edit", the revised text
|
|
43
|
+
|
|
44
|
+
When approved:
|
|
45
|
+
1. Run PII filter again on final text
|
|
46
|
+
2. Insert into `shouts` table with source "agent_post"
|
|
47
|
+
3. Update draft status to "published"
|
|
48
|
+
4. Return the published shout
|
|
49
|
+
|
|
50
|
+
When rejected:
|
|
51
|
+
1. Update draft status to "rejected"
|
|
52
|
+
|
|
53
|
+
### 3. Auto-Publish Setting
|
|
54
|
+
Add a user setting for auto-publish.
|
|
55
|
+
|
|
56
|
+
**New MCP tool: `shout_set_auto_publish`**
|
|
57
|
+
- `user_id` (required): user's UUID
|
|
58
|
+
- `enabled` (required): boolean
|
|
59
|
+
- `filter_level` (optional): "strict" | "standard" (default "strict")
|
|
60
|
+
|
|
61
|
+
When auto-publish is ON:
|
|
62
|
+
- `shout_draft_post` still creates a draft but immediately auto-approves and publishes it
|
|
63
|
+
- PII filter runs regardless (hard constraint, can't be disabled)
|
|
64
|
+
- Log the auto-publish event
|
|
65
|
+
|
|
66
|
+
### 4. Supabase Migration
|
|
67
|
+
|
|
68
|
+
```sql
|
|
69
|
+
-- Draft posts table
|
|
70
|
+
CREATE TABLE IF NOT EXISTS draft_posts (
|
|
71
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
72
|
+
user_id UUID NOT NULL REFERENCES auth.users(id),
|
|
73
|
+
text TEXT NOT NULL,
|
|
74
|
+
filtered_text TEXT,
|
|
75
|
+
tags TEXT[] DEFAULT '{}',
|
|
76
|
+
category TEXT,
|
|
77
|
+
collection_id UUID REFERENCES collections(id),
|
|
78
|
+
visibility TEXT DEFAULT 'public' CHECK (visibility IN ('public', 'private', 'unlisted')),
|
|
79
|
+
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'published')),
|
|
80
|
+
filter_report TEXT,
|
|
81
|
+
source TEXT DEFAULT 'agent',
|
|
82
|
+
agent_id TEXT,
|
|
83
|
+
published_shout_id UUID REFERENCES shouts(id),
|
|
84
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
85
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE INDEX idx_draft_posts_user_status ON draft_posts(user_id, status);
|
|
89
|
+
CREATE INDEX idx_draft_posts_created ON draft_posts(created_at DESC);
|
|
90
|
+
|
|
91
|
+
-- Add auto_publish to user settings (or create user_settings if needed)
|
|
92
|
+
ALTER TABLE user_settings ADD COLUMN IF NOT EXISTS auto_publish BOOLEAN DEFAULT false;
|
|
93
|
+
ALTER TABLE user_settings ADD COLUMN IF NOT EXISTS auto_publish_filter_level TEXT DEFAULT 'strict';
|
|
94
|
+
|
|
95
|
+
-- Add shout_type to shouts table
|
|
96
|
+
ALTER TABLE shouts ADD COLUMN IF NOT EXISTS shout_type TEXT DEFAULT 'link' CHECK (shout_type IN ('link', 'post'));
|
|
97
|
+
ALTER TABLE shouts ADD COLUMN IF NOT EXISTS draft_id UUID REFERENCES draft_posts(id);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. File Structure
|
|
101
|
+
- `src/tools/posts.ts` — new file with shout_draft_post, shout_list_drafts, shout_approve_draft, shout_set_auto_publish
|
|
102
|
+
- `src/lib/content-filter.ts` — already exists, import and use
|
|
103
|
+
- `src/index.ts` — register the new tools
|
|
104
|
+
- Run the SQL migration on supabase
|
|
105
|
+
|
|
106
|
+
### Constraints
|
|
107
|
+
- PII filter ALWAYS runs, even with auto-publish ON
|
|
108
|
+
- Never skip content filtering regardless of user settings
|
|
109
|
+
- All text posts have max 500 char limit
|
|
110
|
+
- Use the existing supabase client from src/lib/supabase.ts
|
|
111
|
+
- Follow existing code style (zod schemas, McpServer tool registration)
|
|
112
|
+
- Build with TypeScript, compile with tsc
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="1200" height="630" fill="#FAFAFA"/>
|
|
3
|
+
<circle cx="600" cy="280" r="32" fill="#FACC15"/>
|
|
4
|
+
<text x="600" y="380" text-anchor="middle" font-family="-apple-system, system-ui, sans-serif" font-size="28" fill="#A3A3A3" letter-spacing="4">SHOUT</text>
|
|
5
|
+
</svg>
|
package/bin/shout
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shout CLI - save links to nod shout from the command line
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
6
|
+
cd "$SCRIPT_DIR"
|
|
7
|
+
|
|
8
|
+
# Load env
|
|
9
|
+
set -a
|
|
10
|
+
source .env 2>/dev/null || true
|
|
11
|
+
set +a
|
|
12
|
+
|
|
13
|
+
ACTION="${1:-help}"
|
|
14
|
+
shift || true
|
|
15
|
+
|
|
16
|
+
case "$ACTION" in
|
|
17
|
+
save)
|
|
18
|
+
# Usage: shout save <url> [--take "agent commentary"] [--tags tag1,tag2]
|
|
19
|
+
URL="${1:?Usage: shout save <url> [--take \"commentary\"] [--tags tag1,tag2]}"
|
|
20
|
+
shift || true
|
|
21
|
+
TAKE=""
|
|
22
|
+
TAGS=""
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--take) TAKE="$2"; shift 2 ;;
|
|
26
|
+
--tags) TAGS="$2"; shift 2 ;;
|
|
27
|
+
*) shift ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
node -e "
|
|
31
|
+
import('./dist/tools/shout_agent_curate.js').then(async m => {
|
|
32
|
+
const result = await m.shoutAgentCurate({
|
|
33
|
+
url: '$URL',
|
|
34
|
+
agent_take: '$TAKE' || undefined,
|
|
35
|
+
tags: '$TAGS' ? '$TAGS'.split(',') : undefined,
|
|
36
|
+
user_id: (await import('./dist/lib/supabase.js')).supabase.from('profiles').select('id').eq('username','fubz').single().then(r => r.data.id),
|
|
37
|
+
});
|
|
38
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39
|
+
}).catch(e => { console.error(e.message); process.exit(1); });
|
|
40
|
+
"
|
|
41
|
+
;;
|
|
42
|
+
list)
|
|
43
|
+
node -e "
|
|
44
|
+
import('./dist/lib/supabase.js').then(async ({ supabase }) => {
|
|
45
|
+
const { data } = await supabase.from('shouts').select('title, url, category, tags, created_at').order('created_at', { ascending: false }).limit(${1:-20});
|
|
46
|
+
data.forEach(s => console.log(\`[\${s.category}] \${s.title}\n \${s.url}\n tags: \${(s.tags||[]).join(', ')}\n\`));
|
|
47
|
+
});
|
|
48
|
+
"
|
|
49
|
+
;;
|
|
50
|
+
collections)
|
|
51
|
+
node -e "
|
|
52
|
+
import('./dist/lib/supabase.js').then(async ({ supabase }) => {
|
|
53
|
+
const { data } = await supabase.from('collections').select('name, slug, description');
|
|
54
|
+
if (!data?.length) { console.log('No collections yet.'); return; }
|
|
55
|
+
data.forEach(c => console.log(\`\${c.name} (\${c.slug}): \${c.description || 'no description'}\`));
|
|
56
|
+
});
|
|
57
|
+
"
|
|
58
|
+
;;
|
|
59
|
+
help|*)
|
|
60
|
+
echo "shout - nod social link curation CLI"
|
|
61
|
+
echo ""
|
|
62
|
+
echo "Commands:"
|
|
63
|
+
echo " save <url> [--take \"commentary\"] [--tags tag1,tag2] Save a link"
|
|
64
|
+
echo " list [count] List recent shouts"
|
|
65
|
+
echo " collections List collections"
|
|
66
|
+
echo ""
|
|
67
|
+
;;
|
|
68
|
+
esac
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { registerLinkTools } from "./tools/links.js";
|
|
4
|
+
import { registerCollectionTools } from "./tools/collections.js";
|
|
5
|
+
import { registerSocialTools } from "./tools/social.js";
|
|
6
|
+
import { registerSettingsTools } from "./tools/settings.js";
|
|
7
|
+
import { registerPostTools } from "./tools/posts.js";
|
|
8
|
+
// agent curate tool is registered inside registerLinkTools
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: "nod-shout",
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
});
|
|
13
|
+
// register all tools
|
|
14
|
+
registerLinkTools(server);
|
|
15
|
+
registerCollectionTools(server);
|
|
16
|
+
registerSocialTools(server);
|
|
17
|
+
registerSettingsTools(server);
|
|
18
|
+
registerPostTools(server);
|
|
19
|
+
// start the server on stdio transport
|
|
20
|
+
async function main() {
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
console.error("nod-shout mcp server running on stdio");
|
|
24
|
+
}
|
|
25
|
+
main().catch((err) => {
|
|
26
|
+
console.error("fatal error:", err);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,2DAA2D;AAE3D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,qBAAqB;AACrB,iBAAiB,CAAC,MAAM,CAAC,CAAC;AAC1B,uBAAuB,CAAC,MAAM,CAAC,CAAC;AAChC,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAC5B,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAC9B,iBAAiB,CAAC,MAAM,CAAC,CAAC;AAE1B,sCAAsC;AACtC,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;AACzD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/lib/ai.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AISummaryResult } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* generate a summary, tags, and category for a link using gpt-4.1-mini.
|
|
4
|
+
* falls back to basic extraction if no api key or on error.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateSummary(params: {
|
|
7
|
+
url: string;
|
|
8
|
+
title: string | null;
|
|
9
|
+
description: string | null;
|
|
10
|
+
bodyText: string | null;
|
|
11
|
+
userContext: string | null;
|
|
12
|
+
}): Promise<AISummaryResult>;
|
|
13
|
+
//# sourceMappingURL=ai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/lib/ai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAyBnD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,GAAG,OAAO,CAAC,eAAe,CAAC,CAc3B"}
|
package/dist/lib/ai.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { loadSkills } from "./skills.js";
|
|
2
|
+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
3
|
+
const SKILL_CONTEXT = loadSkills(["link-summary", "tagging-and-routing", "collection-routing"]);
|
|
4
|
+
const ALLOWED_CATEGORIES = new Set([
|
|
5
|
+
"ai",
|
|
6
|
+
"agents",
|
|
7
|
+
"devtools",
|
|
8
|
+
"design",
|
|
9
|
+
"growth",
|
|
10
|
+
"marketing",
|
|
11
|
+
"startups",
|
|
12
|
+
"media",
|
|
13
|
+
"social",
|
|
14
|
+
"product",
|
|
15
|
+
"engineering",
|
|
16
|
+
"research",
|
|
17
|
+
"security",
|
|
18
|
+
"finance",
|
|
19
|
+
"crypto",
|
|
20
|
+
"policy",
|
|
21
|
+
"uncategorized",
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* generate a summary, tags, and category for a link using gpt-4.1-mini.
|
|
25
|
+
* falls back to basic extraction if no api key or on error.
|
|
26
|
+
*/
|
|
27
|
+
export async function generateSummary(params) {
|
|
28
|
+
const { url, title, description, bodyText, userContext } = params;
|
|
29
|
+
// try ai summarization if key is available
|
|
30
|
+
if (OPENAI_API_KEY) {
|
|
31
|
+
try {
|
|
32
|
+
return await aiSummarize(url, title, description, bodyText, userContext);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error("ai summary failed, falling back to basic extraction:", err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// fallback: basic extraction
|
|
39
|
+
return basicExtract(title, description);
|
|
40
|
+
}
|
|
41
|
+
async function aiSummarize(url, title, description, bodyText, userContext) {
|
|
42
|
+
const prompt = `You are generating data for a nod shout card.${SKILL_CONTEXT}
|
|
43
|
+
|
|
44
|
+
Follow the skills above. Output one short summary, 3-5 tags, and one category.
|
|
45
|
+
|
|
46
|
+
Hard rules:
|
|
47
|
+
- summary should help someone decide whether to click
|
|
48
|
+
- 2 short sentences max
|
|
49
|
+
- sentence 1: what the thing is or what happened
|
|
50
|
+
- sentence 2: a reason to click or a key detail
|
|
51
|
+
- include concrete specifics when available
|
|
52
|
+
- 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"
|
|
53
|
+
- never use: "not just X but Y", "isn't just X"
|
|
54
|
+
- tags must be lowercase
|
|
55
|
+
- category must be one of: ${Array.from(ALLOWED_CATEGORIES).join(", ")}
|
|
56
|
+
- if unsure, use uncategorized
|
|
57
|
+
|
|
58
|
+
url: ${url}
|
|
59
|
+
title: ${title || "unknown"}
|
|
60
|
+
description: ${description || "none"}
|
|
61
|
+
${bodyText ? `page content: ${bodyText}` : ""}
|
|
62
|
+
${userContext ? `user context: ${userContext}` : ""}
|
|
63
|
+
|
|
64
|
+
respond in json only, no markdown:
|
|
65
|
+
{"summary": "...", "tags": ["...", "..."], "category": "..."}`;
|
|
66
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
model: "gpt-4.1-mini",
|
|
74
|
+
messages: [{ role: "user", content: prompt }],
|
|
75
|
+
temperature: 0.7,
|
|
76
|
+
max_tokens: 200,
|
|
77
|
+
response_format: { type: "json_object" },
|
|
78
|
+
}),
|
|
79
|
+
signal: AbortSignal.timeout(15000),
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`openai api error: ${response.status}`);
|
|
83
|
+
}
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
const content = data.choices?.[0]?.message?.content;
|
|
86
|
+
if (!content)
|
|
87
|
+
throw new Error("no content in openai response");
|
|
88
|
+
const parsed = JSON.parse(content);
|
|
89
|
+
return normalizeResult({
|
|
90
|
+
summary: parsed.summary || description || "no summary available",
|
|
91
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.slice(0, 5) : [],
|
|
92
|
+
category: parsed.category || "uncategorized",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function normalizeResult(result) {
|
|
96
|
+
const summary = String(result.summary || "no summary available")
|
|
97
|
+
.replace(/\s+/g, " ")
|
|
98
|
+
.trim()
|
|
99
|
+
.slice(0, 280);
|
|
100
|
+
const tags = Array.from(new Set((result.tags || [])
|
|
101
|
+
.map((tag) => String(tag).toLowerCase().trim())
|
|
102
|
+
.map((tag) => tag.replace(/[^a-z0-9\s-]/g, ""))
|
|
103
|
+
.map((tag) => tag.replace(/\s+/g, "-"))
|
|
104
|
+
.filter((tag) => tag.length >= 2 && tag.length <= 32)
|
|
105
|
+
.filter((tag) => !["technology", "tech", "news", "innovation", "business"].includes(tag)))).slice(0, 5);
|
|
106
|
+
const category = ALLOWED_CATEGORIES.has(String(result.category || "").toLowerCase().trim())
|
|
107
|
+
? String(result.category).toLowerCase().trim()
|
|
108
|
+
: "uncategorized";
|
|
109
|
+
return { summary, tags, category };
|
|
110
|
+
}
|
|
111
|
+
function basicExtract(title, description) {
|
|
112
|
+
const summary = description || title || "no summary available";
|
|
113
|
+
const text = `${title || ""} ${description || ""}`.toLowerCase();
|
|
114
|
+
const stopWords = new Set([
|
|
115
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
|
116
|
+
"being", "have", "has", "had", "do", "does", "did", "will",
|
|
117
|
+
"would", "could", "should", "may", "might", "can", "shall",
|
|
118
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from",
|
|
119
|
+
"as", "into", "through", "during", "before", "after", "and",
|
|
120
|
+
"but", "or", "nor", "not", "so", "yet", "both", "either",
|
|
121
|
+
"neither", "each", "every", "all", "any", "few", "more",
|
|
122
|
+
"most", "other", "some", "such", "no", "only", "own", "same",
|
|
123
|
+
"than", "too", "very", "just", "that", "this", "it", "its",
|
|
124
|
+
"how", "what", "which", "who", "whom", "where", "when", "why",
|
|
125
|
+
"about", "up", "out", "if", "then", "also", "new", "one",
|
|
126
|
+
]);
|
|
127
|
+
const tags = text
|
|
128
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
129
|
+
.split(/\s+/)
|
|
130
|
+
.filter((w) => w.length > 2 && !stopWords.has(w))
|
|
131
|
+
.slice(0, 5);
|
|
132
|
+
const category = "uncategorized";
|
|
133
|
+
return normalizeResult({ summary, tags, category });
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.js","sourceRoot":"","sources":["../../src/lib/ai.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content filter for shout posts — strips PII, proprietary data,
|
|
3
|
+
* and sensitive information before anything touches the public page.
|
|
4
|
+
*
|
|
5
|
+
* Two layers:
|
|
6
|
+
* 1. Regex — catches format-based PII (emails, phones, keys, etc.)
|
|
7
|
+
* 2. LLM — catches context-based leaks ("our revenue is...", "client X told me...")
|
|
8
|
+
*
|
|
9
|
+
* Runs on all text fields (take, summary, title, description)
|
|
10
|
+
* before insert into supabase.
|
|
11
|
+
*/
|
|
12
|
+
export type FilterResult = {
|
|
13
|
+
text: string;
|
|
14
|
+
filtered: boolean;
|
|
15
|
+
removals: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare function filterPII(text: string | null | undefined): FilterResult;
|
|
18
|
+
/**
|
|
19
|
+
* Filter all text fields on a shout before saving.
|
|
20
|
+
* Returns the filtered fields and a report of what was removed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function filterShoutContent(fields: {
|
|
23
|
+
take?: string | null;
|
|
24
|
+
summary?: string | null;
|
|
25
|
+
title?: string | null;
|
|
26
|
+
description?: string | null;
|
|
27
|
+
}): {
|
|
28
|
+
take: string | null;
|
|
29
|
+
summary: string | null;
|
|
30
|
+
title: string | null;
|
|
31
|
+
description: string | null;
|
|
32
|
+
filterReport: string | null;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Layer 2: LLM-based content safety check.
|
|
36
|
+
* Catches context-dependent PII/proprietary leaks that regex misses.
|
|
37
|
+
*
|
|
38
|
+
* Returns { safe: true } for clean content, or { safe: false, reason, redacted }
|
|
39
|
+
* for content that needs redaction.
|
|
40
|
+
*
|
|
41
|
+
* Designed for LOW false positives:
|
|
42
|
+
* - Public info (news, articles, open-source projects) = safe
|
|
43
|
+
* - General opinions and commentary = safe
|
|
44
|
+
* - User's own professional interests/skills = safe
|
|
45
|
+
* - Only flags SPECIFIC private data about identifiable people/companies
|
|
46
|
+
*/
|
|
47
|
+
type LLMFilterResult = {
|
|
48
|
+
safe: boolean;
|
|
49
|
+
reason: string | null;
|
|
50
|
+
redactedText: string | null;
|
|
51
|
+
};
|
|
52
|
+
export declare function llmContentFilter(text: string): Promise<LLMFilterResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Full content filter: regex + LLM.
|
|
55
|
+
* Use this for user-facing text (takes, posts, commentary).
|
|
56
|
+
* Skip LLM for metadata from fetched web pages (titles, descriptions) — those are public by definition.
|
|
57
|
+
*/
|
|
58
|
+
export declare function filterShoutContentFull(fields: {
|
|
59
|
+
take?: string | null;
|
|
60
|
+
summary?: string | null;
|
|
61
|
+
title?: string | null;
|
|
62
|
+
description?: string | null;
|
|
63
|
+
skipLLMForMetadata?: boolean;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
take: string | null;
|
|
66
|
+
summary: string | null;
|
|
67
|
+
title: string | null;
|
|
68
|
+
description: string | null;
|
|
69
|
+
filterReport: string | null;
|
|
70
|
+
blocked: boolean;
|
|
71
|
+
blockReason: string | null;
|
|
72
|
+
}>;
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=content-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-filter.d.ts","sourceRoot":"","sources":["../../src/lib/content-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkCH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAmBvE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG;IACF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAoBA;AAED;;;;;;;;;;;;GAYG;AAEH,KAAK,eAAe,GAAG;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAiCF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAgD7E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GAAG,OAAO,CAAC;IACV,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC,CA6BD"}
|