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
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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -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":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AAClD,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,cAAc,EAAE,qBAAqB,EAAE,oBAAoB,CAAC,CAAC,CAAC;AAChG,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,IAAI;IACJ,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,UAAU;IACV,OAAO;IACP,QAAQ;IACR,SAAS;IACT,aAAa;IACb,UAAU;IACV,UAAU;IACV,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,eAAe;CAChB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAMrC;IACC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;IAElE,2CAA2C;IAC3C,IAAI,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,OAAO,MAAM,WAAW,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,sDAAsD,EAAE,GAAG,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,OAAO,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;AAC1C,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,GAAW,EACX,KAAoB,EACpB,WAA0B,EAC1B,QAAuB,EACvB,WAA0B;IAE1B,MAAM,MAAM,GAAG,gDAAgD,aAAa;;;;;;;;;;;;;6BAajD,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;OAG/D,GAAG;SACD,KAAK,IAAI,SAAS;eACZ,WAAW,IAAI,MAAM;EAClC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE;EAC3C,WAAW,CAAC,CAAC,CAAC,iBAAiB,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE;;;8DAGW,CAAC;IAE7D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,4CAA4C,EAAE;QACzE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,cAAc,EAAE;SAC1C;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,cAAc;YACrB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;YAC7C,WAAW,EAAE,GAAG;YAChB,UAAU,EAAE,GAAG;YACf,eAAe,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;SACzC,CAAC;QACF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;KACnC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAE/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,OAAO,eAAe,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,WAAW,IAAI,sBAAsB;QAChE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;QAC/D,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,eAAe;KAC7C,CAAC,CAAC;AACL,CAAC;AAED,SAAS,eAAe,CAAC,MAAuB;IAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,sBAAsB,CAAC;SAC7D,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAEjB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CACrB,IAAI,GAAG,CACL,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SAChB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;SAC9C,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;SAC9C,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;SACtC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;SACpD,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAC5F,CACF,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEd,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACzF,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE;QAC9C,CAAC,CAAC,eAAe,CAAC;IAEpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACrC,CAAC;AAED,SAAS,YAAY,CACnB,KAAoB,EACpB,WAA0B;IAE1B,MAAM,OAAO,GAAG,WAAW,IAAI,KAAK,IAAI,sBAAsB,CAAC;IAC/D,MAAM,IAAI,GAAG,GAAG,KAAK,IAAI,EAAE,IAAI,WAAW,IAAI,EAAE,EAAE,CAAC,WAAW,EAAE,CAAC;IACjE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;QACxB,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;QAC1D,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;QAC1D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO;QAC1D,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM;QACzD,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK;QAC3D,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ;QACxD,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM;QACvD,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;QAC5D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK;QAC1D,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK;QAC7D,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;KACzD,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI;SACd,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;SAC5B,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SAChD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACf,MAAM,QAAQ,GAAG,eAAe,CAAC;IACjC,OAAO,eAAe,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;AACtD,CAAC"}
@@ -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"}