nod-shout 0.1.2 → 0.2.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/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/link-queue.d.ts +3 -0
- package/dist/tools/link-queue.d.ts.map +1 -0
- package/dist/tools/link-queue.js +171 -0
- package/dist/tools/link-queue.js.map +1 -0
- package/dist/tools/settings.d.ts.map +1 -1
- package/dist/tools/settings.js +109 -35
- package/dist/tools/settings.js.map +1 -1
- package/dist/tools/text-posts.d.ts +3 -0
- package/dist/tools/text-posts.d.ts.map +1 -0
- package/dist/tools/text-posts.js +148 -0
- package/dist/tools/text-posts.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +5 -1
- package/src/tools/link-queue.ts +214 -0
- package/src/tools/settings.ts +110 -38
- package/src/tools/text-posts.ts +176 -0
- package/supabase/migrations/20260320200000_proactive_shout.sql +48 -0
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { registerCollectionTools } from "./tools/collections.js";
|
|
|
5
5
|
import { registerSocialTools } from "./tools/social.js";
|
|
6
6
|
import { registerSettingsTools } from "./tools/settings.js";
|
|
7
7
|
import { registerPostTools } from "./tools/posts.js";
|
|
8
|
+
import { registerLinkQueueTools } from "./tools/link-queue.js";
|
|
9
|
+
import { registerTextPostTools } from "./tools/text-posts.js";
|
|
8
10
|
// agent curate tool is registered inside registerLinkTools
|
|
9
11
|
// accept username: npx nod-shout makaeel OR npx nod-shout --user makaeel
|
|
10
12
|
const args = process.argv.slice(2);
|
|
@@ -22,7 +24,7 @@ if (!process.env.NOD_USER_ID) {
|
|
|
22
24
|
}
|
|
23
25
|
const server = new McpServer({
|
|
24
26
|
name: "nod-shout",
|
|
25
|
-
version: "0.
|
|
27
|
+
version: "0.2.0",
|
|
26
28
|
});
|
|
27
29
|
// register all tools
|
|
28
30
|
registerLinkTools(server);
|
|
@@ -30,6 +32,8 @@ registerCollectionTools(server);
|
|
|
30
32
|
registerSocialTools(server);
|
|
31
33
|
registerSettingsTools(server);
|
|
32
34
|
registerPostTools(server);
|
|
35
|
+
registerLinkQueueTools(server);
|
|
36
|
+
registerTextPostTools(server);
|
|
33
37
|
// start the server on stdio transport
|
|
34
38
|
async function main() {
|
|
35
39
|
const transport = new StdioServerTransport();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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,2EAA2E;AAC3E,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3C,IAAI,WAAW,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,EAAE,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC;KAAM,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7B,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,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;
|
|
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,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,2DAA2D;AAE3D,2EAA2E;AAC3E,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3C,IAAI,WAAW,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,EAAE,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC;KAAM,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;IAC7B,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,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;AAC1B,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC/B,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAE9B,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,+BAA+B,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;AAC1E,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 @@
|
|
|
1
|
+
{"version":3,"file":"link-queue.d.ts","sourceRoot":"","sources":["../../src/tools/link-queue.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QA+MvD"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
const USER_ID = process.env.NOD_USER_ID || "anonymous";
|
|
4
|
+
export function registerLinkQueueTools(server) {
|
|
5
|
+
// queue_link - agent spots a link in conversation and queues it for later
|
|
6
|
+
server.tool("queue_link", "queue a link spotted in conversation for the user to review later. use this when a user shares or discusses a link but doesn't explicitly ask to shout it. the agent collects these throughout the day and prompts the user to review them later.", {
|
|
7
|
+
url: z.string().url().describe("the url to queue"),
|
|
8
|
+
title: z.string().optional().describe("page title if known"),
|
|
9
|
+
context: z.string().optional().describe("what the user said about this link or why they shared it"),
|
|
10
|
+
agent_note: z.string().optional().describe("why the agent thinks this link is worth saving — be specific about what makes it interesting"),
|
|
11
|
+
source: z.enum(["conversation", "browsing", "research"]).default("conversation").describe("where the link came from"),
|
|
12
|
+
}, async ({ url, title, context, agent_note, source }) => {
|
|
13
|
+
// look up user
|
|
14
|
+
const { data: profile } = await supabase
|
|
15
|
+
.from("profiles")
|
|
16
|
+
.select("id")
|
|
17
|
+
.eq("username", USER_ID)
|
|
18
|
+
.single();
|
|
19
|
+
if (!profile) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: `no profile found for user ${USER_ID}. run get_profile first to set up.` }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// check for duplicate url in pending queue
|
|
25
|
+
const { data: existing } = await supabase
|
|
26
|
+
.from("link_queue")
|
|
27
|
+
.select("id")
|
|
28
|
+
.eq("user_id", profile.id)
|
|
29
|
+
.eq("url", url)
|
|
30
|
+
.eq("status", "pending")
|
|
31
|
+
.single();
|
|
32
|
+
if (existing) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: `this link is already in the queue.` }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const { data, error } = await supabase
|
|
38
|
+
.from("link_queue")
|
|
39
|
+
.insert({
|
|
40
|
+
user_id: profile.id,
|
|
41
|
+
url,
|
|
42
|
+
title: title || null,
|
|
43
|
+
context: context || null,
|
|
44
|
+
agent_note: agent_note || null,
|
|
45
|
+
source,
|
|
46
|
+
})
|
|
47
|
+
.select()
|
|
48
|
+
.single();
|
|
49
|
+
if (error) {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: `error queuing link: ${error.message}` }],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: `queued "${title || url}" for later review. i'll remind you to review your queued links later.` }],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
// review_queue - show pending links for the user to approve/dismiss
|
|
59
|
+
server.tool("review_queue", "show the user their pending queued links from today's conversations. use this to prompt them to shout, dismiss, or save links for later. call this at natural pauses in conversation or end of day.", {
|
|
60
|
+
limit: z.number().min(1).max(50).default(10).describe("max links to show"),
|
|
61
|
+
since_hours: z.number().min(1).max(168).default(24).describe("show links from the last N hours"),
|
|
62
|
+
}, async ({ limit, since_hours }) => {
|
|
63
|
+
const { data: profile } = await supabase
|
|
64
|
+
.from("profiles")
|
|
65
|
+
.select("id")
|
|
66
|
+
.eq("username", USER_ID)
|
|
67
|
+
.single();
|
|
68
|
+
if (!profile) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: `no profile found for user ${USER_ID}.` }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const since = new Date(Date.now() - since_hours * 60 * 60 * 1000).toISOString();
|
|
74
|
+
const { data: links, error } = await supabase
|
|
75
|
+
.from("link_queue")
|
|
76
|
+
.select("*")
|
|
77
|
+
.eq("user_id", profile.id)
|
|
78
|
+
.eq("status", "pending")
|
|
79
|
+
.gte("spotted_at", since)
|
|
80
|
+
.order("spotted_at", { ascending: false })
|
|
81
|
+
.limit(limit);
|
|
82
|
+
if (error) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `error fetching queue: ${error.message}` }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!links || links.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: `no pending links in the queue. all caught up!` }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const summary = links.map((l, i) => {
|
|
93
|
+
let line = `${i + 1}. ${l.title || l.url}`;
|
|
94
|
+
if (l.title)
|
|
95
|
+
line += `\n ${l.url}`;
|
|
96
|
+
if (l.context)
|
|
97
|
+
line += `\n you said: "${l.context}"`;
|
|
98
|
+
if (l.agent_note)
|
|
99
|
+
line += `\n why it's interesting: ${l.agent_note}`;
|
|
100
|
+
line += `\n spotted: ${new Date(l.spotted_at).toLocaleString()}`;
|
|
101
|
+
line += `\n id: ${l.id}`;
|
|
102
|
+
return line;
|
|
103
|
+
}).join("\n\n");
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `you have ${links.length} link${links.length === 1 ? "" : "s"} from the last ${since_hours} hours:\n\n${summary}\n\nwant to shout any of these? you can say "shout #1" or "dismiss #3" or "shout all".`,
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
// resolve_queue_item - mark a queued link as shouted or dismissed
|
|
112
|
+
server.tool("resolve_queue_item", "mark a queued link as shouted or dismissed. use after the user decides what to do with a queued link.", {
|
|
113
|
+
queue_id: z.string().uuid().describe("the queue item id"),
|
|
114
|
+
action: z.enum(["shouted", "dismissed"]).describe("what happened to the link"),
|
|
115
|
+
}, async ({ queue_id, action }) => {
|
|
116
|
+
const { error } = await supabase
|
|
117
|
+
.from("link_queue")
|
|
118
|
+
.update({
|
|
119
|
+
status: action,
|
|
120
|
+
resolved_at: new Date().toISOString(),
|
|
121
|
+
})
|
|
122
|
+
.eq("id", queue_id);
|
|
123
|
+
if (error) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: `error updating queue item: ${error.message}` }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: `link ${action}.` }],
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
// queue_stats - quick summary of queue activity
|
|
133
|
+
server.tool("queue_stats", "get a quick summary of queued links — how many pending, shouted today, dismissed.", {}, async () => {
|
|
134
|
+
const { data: profile } = await supabase
|
|
135
|
+
.from("profiles")
|
|
136
|
+
.select("id")
|
|
137
|
+
.eq("username", USER_ID)
|
|
138
|
+
.single();
|
|
139
|
+
if (!profile) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: `no profile found for user ${USER_ID}.` }],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const today = new Date();
|
|
145
|
+
today.setHours(0, 0, 0, 0);
|
|
146
|
+
const { data: pending } = await supabase
|
|
147
|
+
.from("link_queue")
|
|
148
|
+
.select("id", { count: "exact" })
|
|
149
|
+
.eq("user_id", profile.id)
|
|
150
|
+
.eq("status", "pending");
|
|
151
|
+
const { data: shoutedToday } = await supabase
|
|
152
|
+
.from("link_queue")
|
|
153
|
+
.select("id", { count: "exact" })
|
|
154
|
+
.eq("user_id", profile.id)
|
|
155
|
+
.eq("status", "shouted")
|
|
156
|
+
.gte("resolved_at", today.toISOString());
|
|
157
|
+
const { data: dismissedToday } = await supabase
|
|
158
|
+
.from("link_queue")
|
|
159
|
+
.select("id", { count: "exact" })
|
|
160
|
+
.eq("user_id", profile.id)
|
|
161
|
+
.eq("status", "dismissed")
|
|
162
|
+
.gte("resolved_at", today.toISOString());
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `queue stats:\n- pending: ${pending?.length || 0}\n- shouted today: ${shoutedToday?.length || 0}\n- dismissed today: ${dismissedToday?.length || 0}`,
|
|
167
|
+
}],
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=link-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-queue.js","sourceRoot":"","sources":["../../src/tools/link-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/tools/settings.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/tools/settings.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAwHtD"}
|
package/dist/tools/settings.js
CHANGED
|
@@ -1,45 +1,119 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const userSettings = new Map();
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
const USER_ID = process.env.NOD_USER_ID || "anonymous";
|
|
5
4
|
export function registerSettingsTools(server) {
|
|
6
|
-
// shout_settings - configure behavior
|
|
7
|
-
server.tool("shout_settings", "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
5
|
+
// shout_settings - configure behavior (now persisted to supabase)
|
|
6
|
+
server.tool("shout_settings", "view or update your shout preferences — link detection, digest schedule, agent posting permissions, auto-post rules.", {
|
|
7
|
+
auto_detect_links: z.boolean().optional().describe("should the agent auto-detect links in conversation and queue them?"),
|
|
8
|
+
default_visibility: z.enum(["public", "private", "unlisted"]).optional().describe("default visibility for new shouts"),
|
|
9
|
+
digest_frequency: z.enum(["daily", "weekly", "never"]).optional().describe("how often to prompt you to review queued links"),
|
|
10
|
+
digest_time: z.string().optional().describe("preferred time for daily digest prompt (HH:MM format, e.g. '20:00')"),
|
|
11
|
+
digest_timezone: z.string().optional().describe("your timezone (e.g. 'America/New_York')"),
|
|
12
|
+
agent_post_mode: z.enum(["ask", "auto", "curated"]).optional().describe("'ask' = agent always asks before posting. 'auto' = agent posts freely. 'curated' = auto-post for certain tags, ask for others."),
|
|
13
|
+
auto_post_tags: z.array(z.string()).optional().describe("when agent_post_mode is 'curated', auto-post for these tags without asking"),
|
|
14
|
+
agent_text_posts: z.boolean().optional().describe("can the agent draft text posts (status updates, observations)?"),
|
|
15
|
+
agent_text_auto_post: z.boolean().optional().describe("can the agent auto-publish text posts without asking?"),
|
|
16
|
+
}, async ({ auto_detect_links, default_visibility, digest_frequency, digest_time, digest_timezone, agent_post_mode, auto_post_tags, agent_text_posts, agent_text_auto_post }) => {
|
|
17
|
+
// look up user
|
|
18
|
+
const { data: profile } = await supabase
|
|
19
|
+
.from("profiles")
|
|
20
|
+
.select("id")
|
|
21
|
+
.eq("username", USER_ID)
|
|
22
|
+
.single();
|
|
23
|
+
if (!profile) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: `no profile found for user ${USER_ID}.` }],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// get current settings or create defaults
|
|
29
|
+
const { data: current } = await supabase
|
|
30
|
+
.from("user_settings")
|
|
31
|
+
.select("*")
|
|
32
|
+
.eq("user_id", profile.id)
|
|
33
|
+
.single();
|
|
34
|
+
const updates = { updated_at: new Date().toISOString() };
|
|
35
|
+
if (auto_detect_links !== undefined)
|
|
36
|
+
updates.auto_detect_links = auto_detect_links;
|
|
29
37
|
if (default_visibility !== undefined)
|
|
30
|
-
|
|
38
|
+
updates.default_visibility = default_visibility;
|
|
31
39
|
if (digest_frequency !== undefined)
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
updates.digest_frequency = digest_frequency;
|
|
41
|
+
if (digest_time !== undefined)
|
|
42
|
+
updates.digest_time = digest_time;
|
|
43
|
+
if (digest_timezone !== undefined)
|
|
44
|
+
updates.digest_timezone = digest_timezone;
|
|
45
|
+
if (agent_post_mode !== undefined)
|
|
46
|
+
updates.agent_post_mode = agent_post_mode;
|
|
47
|
+
if (auto_post_tags !== undefined)
|
|
48
|
+
updates.auto_post_tags = auto_post_tags;
|
|
49
|
+
if (agent_text_posts !== undefined)
|
|
50
|
+
updates.agent_text_posts = agent_text_posts;
|
|
51
|
+
if (agent_text_auto_post !== undefined)
|
|
52
|
+
updates.agent_text_auto_post = agent_text_auto_post;
|
|
53
|
+
// no fields to update — just show current settings
|
|
54
|
+
const hasUpdates = Object.keys(updates).length > 1; // more than just updated_at
|
|
55
|
+
if (!hasUpdates && current) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `your current shout settings:\n\n` +
|
|
60
|
+
`- auto-detect links: ${current.auto_detect_links}\n` +
|
|
61
|
+
`- default visibility: ${current.default_visibility}\n` +
|
|
62
|
+
`- digest: ${current.digest_frequency} at ${current.digest_time} (${current.digest_timezone})\n` +
|
|
63
|
+
`- agent post mode: ${current.agent_post_mode}\n` +
|
|
64
|
+
`- auto-post tags: ${(current.auto_post_tags || []).join(", ") || "none"}\n` +
|
|
65
|
+
`- agent text posts: ${current.agent_text_posts}\n` +
|
|
66
|
+
`- agent text auto-post: ${current.agent_text_auto_post}`,
|
|
67
|
+
}],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (current) {
|
|
71
|
+
// update existing
|
|
72
|
+
const { error } = await supabase
|
|
73
|
+
.from("user_settings")
|
|
74
|
+
.update(updates)
|
|
75
|
+
.eq("user_id", profile.id);
|
|
76
|
+
if (error) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: `error updating settings: ${error.message}` }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// insert new with defaults
|
|
84
|
+
const { error } = await supabase
|
|
85
|
+
.from("user_settings")
|
|
86
|
+
.insert({ user_id: profile.id, ...updates });
|
|
87
|
+
if (error) {
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: `error creating settings: ${error.message}` }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// fetch final state
|
|
94
|
+
const { data: final } = await supabase
|
|
95
|
+
.from("user_settings")
|
|
96
|
+
.select("*")
|
|
97
|
+
.eq("user_id", profile.id)
|
|
98
|
+
.single();
|
|
99
|
+
if (!final) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: `settings saved.` }],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
34
104
|
return {
|
|
35
|
-
content: [
|
|
36
|
-
{
|
|
105
|
+
content: [{
|
|
37
106
|
type: "text",
|
|
38
|
-
text: `settings updated:\n
|
|
39
|
-
|
|
40
|
-
|
|
107
|
+
text: `settings updated:\n\n` +
|
|
108
|
+
`- auto-detect links: ${final.auto_detect_links}\n` +
|
|
109
|
+
`- default visibility: ${final.default_visibility}\n` +
|
|
110
|
+
`- digest: ${final.digest_frequency} at ${final.digest_time} (${final.digest_timezone})\n` +
|
|
111
|
+
`- agent post mode: ${final.agent_post_mode}\n` +
|
|
112
|
+
`- auto-post tags: ${(final.auto_post_tags || []).join(", ") || "none"}\n` +
|
|
113
|
+
`- agent text posts: ${final.agent_text_posts}\n` +
|
|
114
|
+
`- agent text auto-post: ${final.agent_text_auto_post}`,
|
|
115
|
+
}],
|
|
41
116
|
};
|
|
42
117
|
});
|
|
43
|
-
// shout_generate_digest is registered in links.ts
|
|
44
118
|
}
|
|
45
119
|
//# sourceMappingURL=settings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/tools/settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/tools/settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-posts.d.ts","sourceRoot":"","sources":["../../src/tools/text-posts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAwKtD"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
import { filterShoutContentFull } from "../lib/content-filter.js";
|
|
4
|
+
const USER_ID = process.env.NOD_USER_ID || "anonymous";
|
|
5
|
+
export function registerTextPostTools(server) {
|
|
6
|
+
// draft_text_post - agent writes a text post about what's going on
|
|
7
|
+
server.tool("draft_text_post", "draft a text post (not a link share) for the user's shout page. these are status updates, observations, quick takes, or notes about what the user/agent has been working on. always drafts first — user approves before publishing unless they've enabled auto-post for text.", {
|
|
8
|
+
text: z.string().min(1).max(2000).describe("the post content. write in the agent's voice if it's the agent's page, or the user's voice if it's the user's page. be specific and interesting, not generic."),
|
|
9
|
+
tags: z.array(z.string()).optional().describe("tags for the post"),
|
|
10
|
+
category: z.string().optional().describe("category like 'building', 'reading', 'thinking', 'shipping'"),
|
|
11
|
+
collection_slug: z.string().optional().describe("collection to add this to"),
|
|
12
|
+
}, async ({ text, tags, category, collection_slug }) => {
|
|
13
|
+
const { data: profile } = await supabase
|
|
14
|
+
.from("profiles")
|
|
15
|
+
.select("id")
|
|
16
|
+
.eq("username", USER_ID)
|
|
17
|
+
.single();
|
|
18
|
+
if (!profile) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: `no profile found for user ${USER_ID}.` }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// check user settings for auto-post permission
|
|
24
|
+
const { data: settings } = await supabase
|
|
25
|
+
.from("user_settings")
|
|
26
|
+
.select("agent_text_auto_post, auto_post_tags")
|
|
27
|
+
.eq("user_id", profile.id)
|
|
28
|
+
.single();
|
|
29
|
+
// PII filter
|
|
30
|
+
const filtered = await filterShoutContentFull({ take: text, skipLLMForMetadata: true });
|
|
31
|
+
if (filtered.blocked) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: `blocked by content filter: ${filtered.blockReason}` }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const finalText = filtered.take || text;
|
|
37
|
+
// resolve collection
|
|
38
|
+
let collectionId = null;
|
|
39
|
+
if (collection_slug) {
|
|
40
|
+
const { data: col } = await supabase
|
|
41
|
+
.from("collections")
|
|
42
|
+
.select("id")
|
|
43
|
+
.eq("user_id", profile.id)
|
|
44
|
+
.eq("slug", collection_slug)
|
|
45
|
+
.single();
|
|
46
|
+
if (col)
|
|
47
|
+
collectionId = col.id;
|
|
48
|
+
}
|
|
49
|
+
// check if auto-post is enabled
|
|
50
|
+
const autoPost = settings?.agent_text_auto_post || false;
|
|
51
|
+
const autoTags = settings?.auto_post_tags || [];
|
|
52
|
+
const shouldAutoPost = autoPost || (tags && tags.some(t => autoTags.includes(t)));
|
|
53
|
+
if (shouldAutoPost) {
|
|
54
|
+
// auto-publish directly
|
|
55
|
+
const { data: shout, error } = await supabase
|
|
56
|
+
.from("shouts")
|
|
57
|
+
.insert({
|
|
58
|
+
user_id: profile.id,
|
|
59
|
+
url: null,
|
|
60
|
+
title: null,
|
|
61
|
+
description: finalText,
|
|
62
|
+
summary: finalText,
|
|
63
|
+
tags: tags || [],
|
|
64
|
+
category: category || "status",
|
|
65
|
+
collection_id: collectionId,
|
|
66
|
+
post_type: "text",
|
|
67
|
+
source: "agent",
|
|
68
|
+
visibility: "public",
|
|
69
|
+
})
|
|
70
|
+
.select()
|
|
71
|
+
.single();
|
|
72
|
+
if (error) {
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: `error publishing: ${error.message}` }],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `auto-published text post to your shout page:\n\n"${finalText}"\n\ntags: ${(tags || []).join(", ") || "none"}\nview: https://nodsocial.com/shout/${USER_ID}`,
|
|
81
|
+
}],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// draft mode — save as draft for approval
|
|
85
|
+
const { data: draft, error } = await supabase
|
|
86
|
+
.from("draft_posts")
|
|
87
|
+
.insert({
|
|
88
|
+
user_id: profile.id,
|
|
89
|
+
text: finalText,
|
|
90
|
+
tags: tags || [],
|
|
91
|
+
category: category || "status",
|
|
92
|
+
collection_id: collectionId,
|
|
93
|
+
status: "pending",
|
|
94
|
+
post_type: "text",
|
|
95
|
+
})
|
|
96
|
+
.select()
|
|
97
|
+
.single();
|
|
98
|
+
if (error) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `error creating draft: ${error.message}` }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `drafted a text post for your shout page:\n\n"${finalText}"\n\ntags: ${(tags || []).join(", ") || "none"}\n\nwant me to publish it? say "publish" or "edit" or "drop it".`,
|
|
107
|
+
}],
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
// agent_observe - agent logs an observation about the day for potential text post
|
|
111
|
+
server.tool("agent_observe", "log something interesting the agent noticed during conversation — a pattern, a milestone, a shift in what the user is working on. these observations can be turned into text posts later. use this throughout the day as you notice things worth noting.", {
|
|
112
|
+
observation: z.string().describe("what the agent noticed. be specific."),
|
|
113
|
+
mood: z.enum(["building", "shipping", "thinking", "learning", "debugging", "celebrating", "grinding"]).optional().describe("what kind of moment this is"),
|
|
114
|
+
could_post: z.boolean().default(true).describe("is this interesting enough to potentially post about?"),
|
|
115
|
+
}, async ({ observation, mood, could_post }) => {
|
|
116
|
+
const { data: profile } = await supabase
|
|
117
|
+
.from("profiles")
|
|
118
|
+
.select("id")
|
|
119
|
+
.eq("username", USER_ID)
|
|
120
|
+
.single();
|
|
121
|
+
if (!profile) {
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: `no profile found for user ${USER_ID}.` }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// store as a queued item with no url
|
|
127
|
+
const { error } = await supabase
|
|
128
|
+
.from("link_queue")
|
|
129
|
+
.insert({
|
|
130
|
+
user_id: profile.id,
|
|
131
|
+
url: `observation://${Date.now()}`,
|
|
132
|
+
title: mood ? `[${mood}] observation` : "observation",
|
|
133
|
+
context: observation,
|
|
134
|
+
agent_note: could_post ? "could make a good text post" : "just noting for context",
|
|
135
|
+
source: "conversation",
|
|
136
|
+
status: could_post ? "pending" : "dismissed",
|
|
137
|
+
});
|
|
138
|
+
if (error) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: `error logging observation: ${error.message}` }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: `noted${mood ? ` [${mood}]` : ""}: "${observation}"` }],
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=text-posts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-posts.js","sourceRoot":"","sources":["../../src/tools/text-posts.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { registerCollectionTools } from "./tools/collections.js";
|
|
|
5
5
|
import { registerSocialTools } from "./tools/social.js";
|
|
6
6
|
import { registerSettingsTools } from "./tools/settings.js";
|
|
7
7
|
import { registerPostTools } from "./tools/posts.js";
|
|
8
|
+
import { registerLinkQueueTools } from "./tools/link-queue.js";
|
|
9
|
+
import { registerTextPostTools } from "./tools/text-posts.js";
|
|
8
10
|
// agent curate tool is registered inside registerLinkTools
|
|
9
11
|
|
|
10
12
|
// accept username: npx nod-shout makaeel OR npx nod-shout --user makaeel
|
|
@@ -24,7 +26,7 @@ if (!process.env.NOD_USER_ID) {
|
|
|
24
26
|
|
|
25
27
|
const server = new McpServer({
|
|
26
28
|
name: "nod-shout",
|
|
27
|
-
version: "0.
|
|
29
|
+
version: "0.2.0",
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
// register all tools
|
|
@@ -33,6 +35,8 @@ registerCollectionTools(server);
|
|
|
33
35
|
registerSocialTools(server);
|
|
34
36
|
registerSettingsTools(server);
|
|
35
37
|
registerPostTools(server);
|
|
38
|
+
registerLinkQueueTools(server);
|
|
39
|
+
registerTextPostTools(server);
|
|
36
40
|
|
|
37
41
|
// start the server on stdio transport
|
|
38
42
|
async function main() {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
|
|
5
|
+
const USER_ID = process.env.NOD_USER_ID || "anonymous";
|
|
6
|
+
|
|
7
|
+
export function registerLinkQueueTools(server: McpServer) {
|
|
8
|
+
// queue_link - agent spots a link in conversation and queues it for later
|
|
9
|
+
server.tool(
|
|
10
|
+
"queue_link",
|
|
11
|
+
"queue a link spotted in conversation for the user to review later. use this when a user shares or discusses a link but doesn't explicitly ask to shout it. the agent collects these throughout the day and prompts the user to review them later.",
|
|
12
|
+
{
|
|
13
|
+
url: z.string().url().describe("the url to queue"),
|
|
14
|
+
title: z.string().optional().describe("page title if known"),
|
|
15
|
+
context: z.string().optional().describe("what the user said about this link or why they shared it"),
|
|
16
|
+
agent_note: z.string().optional().describe("why the agent thinks this link is worth saving — be specific about what makes it interesting"),
|
|
17
|
+
source: z.enum(["conversation", "browsing", "research"]).default("conversation").describe("where the link came from"),
|
|
18
|
+
},
|
|
19
|
+
async ({ url, title, context, agent_note, source }) => {
|
|
20
|
+
// look up user
|
|
21
|
+
const { data: profile } = await supabase
|
|
22
|
+
.from("profiles")
|
|
23
|
+
.select("id")
|
|
24
|
+
.eq("username", USER_ID)
|
|
25
|
+
.single();
|
|
26
|
+
|
|
27
|
+
if (!profile) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text" as const, text: `no profile found for user ${USER_ID}. run get_profile first to set up.` }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// check for duplicate url in pending queue
|
|
34
|
+
const { data: existing } = await supabase
|
|
35
|
+
.from("link_queue")
|
|
36
|
+
.select("id")
|
|
37
|
+
.eq("user_id", profile.id)
|
|
38
|
+
.eq("url", url)
|
|
39
|
+
.eq("status", "pending")
|
|
40
|
+
.single();
|
|
41
|
+
|
|
42
|
+
if (existing) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text" as const, text: `this link is already in the queue.` }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { data, error } = await supabase
|
|
49
|
+
.from("link_queue")
|
|
50
|
+
.insert({
|
|
51
|
+
user_id: profile.id,
|
|
52
|
+
url,
|
|
53
|
+
title: title || null,
|
|
54
|
+
context: context || null,
|
|
55
|
+
agent_note: agent_note || null,
|
|
56
|
+
source,
|
|
57
|
+
})
|
|
58
|
+
.select()
|
|
59
|
+
.single();
|
|
60
|
+
|
|
61
|
+
if (error) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text" as const, text: `error queuing link: ${error.message}` }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text" as const, text: `queued "${title || url}" for later review. i'll remind you to review your queued links later.` }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// review_queue - show pending links for the user to approve/dismiss
|
|
74
|
+
server.tool(
|
|
75
|
+
"review_queue",
|
|
76
|
+
"show the user their pending queued links from today's conversations. use this to prompt them to shout, dismiss, or save links for later. call this at natural pauses in conversation or end of day.",
|
|
77
|
+
{
|
|
78
|
+
limit: z.number().min(1).max(50).default(10).describe("max links to show"),
|
|
79
|
+
since_hours: z.number().min(1).max(168).default(24).describe("show links from the last N hours"),
|
|
80
|
+
},
|
|
81
|
+
async ({ limit, since_hours }) => {
|
|
82
|
+
const { data: profile } = await supabase
|
|
83
|
+
.from("profiles")
|
|
84
|
+
.select("id")
|
|
85
|
+
.eq("username", USER_ID)
|
|
86
|
+
.single();
|
|
87
|
+
|
|
88
|
+
if (!profile) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text" as const, text: `no profile found for user ${USER_ID}.` }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const since = new Date(Date.now() - since_hours * 60 * 60 * 1000).toISOString();
|
|
95
|
+
|
|
96
|
+
const { data: links, error } = await supabase
|
|
97
|
+
.from("link_queue")
|
|
98
|
+
.select("*")
|
|
99
|
+
.eq("user_id", profile.id)
|
|
100
|
+
.eq("status", "pending")
|
|
101
|
+
.gte("spotted_at", since)
|
|
102
|
+
.order("spotted_at", { ascending: false })
|
|
103
|
+
.limit(limit);
|
|
104
|
+
|
|
105
|
+
if (error) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text" as const, text: `error fetching queue: ${error.message}` }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!links || links.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text" as const, text: `no pending links in the queue. all caught up!` }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const summary = links.map((l, i) => {
|
|
118
|
+
let line = `${i + 1}. ${l.title || l.url}`;
|
|
119
|
+
if (l.title) line += `\n ${l.url}`;
|
|
120
|
+
if (l.context) line += `\n you said: "${l.context}"`;
|
|
121
|
+
if (l.agent_note) line += `\n why it's interesting: ${l.agent_note}`;
|
|
122
|
+
line += `\n spotted: ${new Date(l.spotted_at).toLocaleString()}`;
|
|
123
|
+
line += `\n id: ${l.id}`;
|
|
124
|
+
return line;
|
|
125
|
+
}).join("\n\n");
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content: [{
|
|
129
|
+
type: "text" as const,
|
|
130
|
+
text: `you have ${links.length} link${links.length === 1 ? "" : "s"} from the last ${since_hours} hours:\n\n${summary}\n\nwant to shout any of these? you can say "shout #1" or "dismiss #3" or "shout all".`,
|
|
131
|
+
}],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// resolve_queue_item - mark a queued link as shouted or dismissed
|
|
137
|
+
server.tool(
|
|
138
|
+
"resolve_queue_item",
|
|
139
|
+
"mark a queued link as shouted or dismissed. use after the user decides what to do with a queued link.",
|
|
140
|
+
{
|
|
141
|
+
queue_id: z.string().uuid().describe("the queue item id"),
|
|
142
|
+
action: z.enum(["shouted", "dismissed"]).describe("what happened to the link"),
|
|
143
|
+
},
|
|
144
|
+
async ({ queue_id, action }) => {
|
|
145
|
+
const { error } = await supabase
|
|
146
|
+
.from("link_queue")
|
|
147
|
+
.update({
|
|
148
|
+
status: action,
|
|
149
|
+
resolved_at: new Date().toISOString(),
|
|
150
|
+
})
|
|
151
|
+
.eq("id", queue_id);
|
|
152
|
+
|
|
153
|
+
if (error) {
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text" as const, text: `error updating queue item: ${error.message}` }],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text" as const, text: `link ${action}.` }],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// queue_stats - quick summary of queue activity
|
|
166
|
+
server.tool(
|
|
167
|
+
"queue_stats",
|
|
168
|
+
"get a quick summary of queued links — how many pending, shouted today, dismissed.",
|
|
169
|
+
{},
|
|
170
|
+
async () => {
|
|
171
|
+
const { data: profile } = await supabase
|
|
172
|
+
.from("profiles")
|
|
173
|
+
.select("id")
|
|
174
|
+
.eq("username", USER_ID)
|
|
175
|
+
.single();
|
|
176
|
+
|
|
177
|
+
if (!profile) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text" as const, text: `no profile found for user ${USER_ID}.` }],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const today = new Date();
|
|
184
|
+
today.setHours(0, 0, 0, 0);
|
|
185
|
+
|
|
186
|
+
const { data: pending } = await supabase
|
|
187
|
+
.from("link_queue")
|
|
188
|
+
.select("id", { count: "exact" })
|
|
189
|
+
.eq("user_id", profile.id)
|
|
190
|
+
.eq("status", "pending");
|
|
191
|
+
|
|
192
|
+
const { data: shoutedToday } = await supabase
|
|
193
|
+
.from("link_queue")
|
|
194
|
+
.select("id", { count: "exact" })
|
|
195
|
+
.eq("user_id", profile.id)
|
|
196
|
+
.eq("status", "shouted")
|
|
197
|
+
.gte("resolved_at", today.toISOString());
|
|
198
|
+
|
|
199
|
+
const { data: dismissedToday } = await supabase
|
|
200
|
+
.from("link_queue")
|
|
201
|
+
.select("id", { count: "exact" })
|
|
202
|
+
.eq("user_id", profile.id)
|
|
203
|
+
.eq("status", "dismissed")
|
|
204
|
+
.gte("resolved_at", today.toISOString());
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: "text" as const,
|
|
209
|
+
text: `queue stats:\n- pending: ${pending?.length || 0}\n- shouted today: ${shoutedToday?.length || 0}\n- dismissed today: ${dismissedToday?.length || 0}`,
|
|
210
|
+
}],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
package/src/tools/settings.ts
CHANGED
|
@@ -1,55 +1,127 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
2
3
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
// TODO: persist to supabase user_settings table
|
|
6
|
-
const userSettings: Map<string, Record<string, unknown>> = new Map();
|
|
5
|
+
const USER_ID = process.env.NOD_USER_ID || "anonymous";
|
|
7
6
|
|
|
8
7
|
export function registerSettingsTools(server: McpServer) {
|
|
9
|
-
// shout_settings - configure behavior
|
|
8
|
+
// shout_settings - configure behavior (now persisted to supabase)
|
|
10
9
|
server.tool(
|
|
11
10
|
"shout_settings",
|
|
12
|
-
"
|
|
11
|
+
"view or update your shout preferences — link detection, digest schedule, agent posting permissions, auto-post rules.",
|
|
13
12
|
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
digest_frequency: z
|
|
24
|
-
.enum(["daily", "weekly", "monthly", "never"])
|
|
25
|
-
.optional()
|
|
26
|
-
.describe("how often to generate digests"),
|
|
13
|
+
auto_detect_links: z.boolean().optional().describe("should the agent auto-detect links in conversation and queue them?"),
|
|
14
|
+
default_visibility: z.enum(["public", "private", "unlisted"]).optional().describe("default visibility for new shouts"),
|
|
15
|
+
digest_frequency: z.enum(["daily", "weekly", "never"]).optional().describe("how often to prompt you to review queued links"),
|
|
16
|
+
digest_time: z.string().optional().describe("preferred time for daily digest prompt (HH:MM format, e.g. '20:00')"),
|
|
17
|
+
digest_timezone: z.string().optional().describe("your timezone (e.g. 'America/New_York')"),
|
|
18
|
+
agent_post_mode: z.enum(["ask", "auto", "curated"]).optional().describe("'ask' = agent always asks before posting. 'auto' = agent posts freely. 'curated' = auto-post for certain tags, ask for others."),
|
|
19
|
+
auto_post_tags: z.array(z.string()).optional().describe("when agent_post_mode is 'curated', auto-post for these tags without asking"),
|
|
20
|
+
agent_text_posts: z.boolean().optional().describe("can the agent draft text posts (status updates, observations)?"),
|
|
21
|
+
agent_text_auto_post: z.boolean().optional().describe("can the agent auto-publish text posts without asking?"),
|
|
27
22
|
},
|
|
28
|
-
async ({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
async ({ auto_detect_links, default_visibility, digest_frequency, digest_time, digest_timezone, agent_post_mode, auto_post_tags, agent_text_posts, agent_text_auto_post }) => {
|
|
24
|
+
// look up user
|
|
25
|
+
const { data: profile } = await supabase
|
|
26
|
+
.from("profiles")
|
|
27
|
+
.select("id")
|
|
28
|
+
.eq("username", USER_ID)
|
|
29
|
+
.single();
|
|
34
30
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
if (!profile) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text" as const, text: `no profile found for user ${USER_ID}.` }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
// get current settings or create defaults
|
|
38
|
+
const { data: current } = await supabase
|
|
39
|
+
.from("user_settings")
|
|
40
|
+
.select("*")
|
|
41
|
+
.eq("user_id", profile.id)
|
|
42
|
+
.single();
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
|
|
45
|
+
if (auto_detect_links !== undefined) updates.auto_detect_links = auto_detect_links;
|
|
46
|
+
if (default_visibility !== undefined) updates.default_visibility = default_visibility;
|
|
47
|
+
if (digest_frequency !== undefined) updates.digest_frequency = digest_frequency;
|
|
48
|
+
if (digest_time !== undefined) updates.digest_time = digest_time;
|
|
49
|
+
if (digest_timezone !== undefined) updates.digest_timezone = digest_timezone;
|
|
50
|
+
if (agent_post_mode !== undefined) updates.agent_post_mode = agent_post_mode;
|
|
51
|
+
if (auto_post_tags !== undefined) updates.auto_post_tags = auto_post_tags;
|
|
52
|
+
if (agent_text_posts !== undefined) updates.agent_text_posts = agent_text_posts;
|
|
53
|
+
if (agent_text_auto_post !== undefined) updates.agent_text_auto_post = agent_text_auto_post;
|
|
54
|
+
|
|
55
|
+
// no fields to update — just show current settings
|
|
56
|
+
const hasUpdates = Object.keys(updates).length > 1; // more than just updated_at
|
|
57
|
+
|
|
58
|
+
if (!hasUpdates && current) {
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
46
61
|
type: "text" as const,
|
|
47
|
-
text: `
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
text: `your current shout settings:\n\n` +
|
|
63
|
+
`- auto-detect links: ${current.auto_detect_links}\n` +
|
|
64
|
+
`- default visibility: ${current.default_visibility}\n` +
|
|
65
|
+
`- digest: ${current.digest_frequency} at ${current.digest_time} (${current.digest_timezone})\n` +
|
|
66
|
+
`- agent post mode: ${current.agent_post_mode}\n` +
|
|
67
|
+
`- auto-post tags: ${(current.auto_post_tags || []).join(", ") || "none"}\n` +
|
|
68
|
+
`- agent text posts: ${current.agent_text_posts}\n` +
|
|
69
|
+
`- agent text auto-post: ${current.agent_text_auto_post}`,
|
|
70
|
+
}],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (current) {
|
|
75
|
+
// update existing
|
|
76
|
+
const { error } = await supabase
|
|
77
|
+
.from("user_settings")
|
|
78
|
+
.update(updates)
|
|
79
|
+
.eq("user_id", profile.id);
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text" as const, text: `error updating settings: ${error.message}` }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// insert new with defaults
|
|
88
|
+
const { error } = await supabase
|
|
89
|
+
.from("user_settings")
|
|
90
|
+
.insert({ user_id: profile.id, ...updates });
|
|
91
|
+
|
|
92
|
+
if (error) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text" as const, text: `error creating settings: ${error.message}` }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// fetch final state
|
|
100
|
+
const { data: final } = await supabase
|
|
101
|
+
.from("user_settings")
|
|
102
|
+
.select("*")
|
|
103
|
+
.eq("user_id", profile.id)
|
|
104
|
+
.single();
|
|
105
|
+
|
|
106
|
+
if (!final) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text" as const, text: `settings saved.` }],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text" as const,
|
|
115
|
+
text: `settings updated:\n\n` +
|
|
116
|
+
`- auto-detect links: ${final.auto_detect_links}\n` +
|
|
117
|
+
`- default visibility: ${final.default_visibility}\n` +
|
|
118
|
+
`- digest: ${final.digest_frequency} at ${final.digest_time} (${final.digest_timezone})\n` +
|
|
119
|
+
`- agent post mode: ${final.agent_post_mode}\n` +
|
|
120
|
+
`- auto-post tags: ${(final.auto_post_tags || []).join(", ") || "none"}\n` +
|
|
121
|
+
`- agent text posts: ${final.agent_text_posts}\n` +
|
|
122
|
+
`- agent text auto-post: ${final.agent_text_auto_post}`,
|
|
123
|
+
}],
|
|
50
124
|
};
|
|
51
125
|
}
|
|
52
126
|
);
|
|
53
|
-
|
|
54
|
-
// shout_generate_digest is registered in links.ts
|
|
55
127
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { supabase } from "../lib/supabase.js";
|
|
3
|
+
import { filterPII, filterShoutContentFull } from "../lib/content-filter.js";
|
|
4
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
|
|
6
|
+
const USER_ID = process.env.NOD_USER_ID || "anonymous";
|
|
7
|
+
|
|
8
|
+
export function registerTextPostTools(server: McpServer) {
|
|
9
|
+
// draft_text_post - agent writes a text post about what's going on
|
|
10
|
+
server.tool(
|
|
11
|
+
"draft_text_post",
|
|
12
|
+
"draft a text post (not a link share) for the user's shout page. these are status updates, observations, quick takes, or notes about what the user/agent has been working on. always drafts first — user approves before publishing unless they've enabled auto-post for text.",
|
|
13
|
+
{
|
|
14
|
+
text: z.string().min(1).max(2000).describe("the post content. write in the agent's voice if it's the agent's page, or the user's voice if it's the user's page. be specific and interesting, not generic."),
|
|
15
|
+
tags: z.array(z.string()).optional().describe("tags for the post"),
|
|
16
|
+
category: z.string().optional().describe("category like 'building', 'reading', 'thinking', 'shipping'"),
|
|
17
|
+
collection_slug: z.string().optional().describe("collection to add this to"),
|
|
18
|
+
},
|
|
19
|
+
async ({ text, tags, category, collection_slug }) => {
|
|
20
|
+
const { data: profile } = await supabase
|
|
21
|
+
.from("profiles")
|
|
22
|
+
.select("id")
|
|
23
|
+
.eq("username", USER_ID)
|
|
24
|
+
.single();
|
|
25
|
+
|
|
26
|
+
if (!profile) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text" as const, text: `no profile found for user ${USER_ID}.` }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// check user settings for auto-post permission
|
|
33
|
+
const { data: settings } = await supabase
|
|
34
|
+
.from("user_settings")
|
|
35
|
+
.select("agent_text_auto_post, auto_post_tags")
|
|
36
|
+
.eq("user_id", profile.id)
|
|
37
|
+
.single();
|
|
38
|
+
|
|
39
|
+
// PII filter
|
|
40
|
+
const filtered = await filterShoutContentFull({ take: text, skipLLMForMetadata: true });
|
|
41
|
+
if (filtered.blocked) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text" as const, text: `blocked by content filter: ${filtered.blockReason}` }],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const finalText = filtered.take || text;
|
|
48
|
+
|
|
49
|
+
// resolve collection
|
|
50
|
+
let collectionId: string | null = null;
|
|
51
|
+
if (collection_slug) {
|
|
52
|
+
const { data: col } = await supabase
|
|
53
|
+
.from("collections")
|
|
54
|
+
.select("id")
|
|
55
|
+
.eq("user_id", profile.id)
|
|
56
|
+
.eq("slug", collection_slug)
|
|
57
|
+
.single();
|
|
58
|
+
if (col) collectionId = col.id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// check if auto-post is enabled
|
|
62
|
+
const autoPost = settings?.agent_text_auto_post || false;
|
|
63
|
+
const autoTags = settings?.auto_post_tags || [];
|
|
64
|
+
const shouldAutoPost = autoPost || (tags && tags.some(t => autoTags.includes(t)));
|
|
65
|
+
|
|
66
|
+
if (shouldAutoPost) {
|
|
67
|
+
// auto-publish directly
|
|
68
|
+
const { data: shout, error } = await supabase
|
|
69
|
+
.from("shouts")
|
|
70
|
+
.insert({
|
|
71
|
+
user_id: profile.id,
|
|
72
|
+
url: null,
|
|
73
|
+
title: null,
|
|
74
|
+
description: finalText,
|
|
75
|
+
summary: finalText,
|
|
76
|
+
tags: tags || [],
|
|
77
|
+
category: category || "status",
|
|
78
|
+
collection_id: collectionId,
|
|
79
|
+
post_type: "text",
|
|
80
|
+
source: "agent",
|
|
81
|
+
visibility: "public",
|
|
82
|
+
})
|
|
83
|
+
.select()
|
|
84
|
+
.single();
|
|
85
|
+
|
|
86
|
+
if (error) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text" as const, text: `error publishing: ${error.message}` }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
content: [{
|
|
94
|
+
type: "text" as const,
|
|
95
|
+
text: `auto-published text post to your shout page:\n\n"${finalText}"\n\ntags: ${(tags || []).join(", ") || "none"}\nview: https://nodsocial.com/shout/${USER_ID}`,
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// draft mode — save as draft for approval
|
|
101
|
+
const { data: draft, error } = await supabase
|
|
102
|
+
.from("draft_posts")
|
|
103
|
+
.insert({
|
|
104
|
+
user_id: profile.id,
|
|
105
|
+
text: finalText,
|
|
106
|
+
tags: tags || [],
|
|
107
|
+
category: category || "status",
|
|
108
|
+
collection_id: collectionId,
|
|
109
|
+
status: "pending",
|
|
110
|
+
post_type: "text",
|
|
111
|
+
})
|
|
112
|
+
.select()
|
|
113
|
+
.single();
|
|
114
|
+
|
|
115
|
+
if (error) {
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text" as const, text: `error creating draft: ${error.message}` }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
content: [{
|
|
123
|
+
type: "text" as const,
|
|
124
|
+
text: `drafted a text post for your shout page:\n\n"${finalText}"\n\ntags: ${(tags || []).join(", ") || "none"}\n\nwant me to publish it? say "publish" or "edit" or "drop it".`,
|
|
125
|
+
}],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// agent_observe - agent logs an observation about the day for potential text post
|
|
131
|
+
server.tool(
|
|
132
|
+
"agent_observe",
|
|
133
|
+
"log something interesting the agent noticed during conversation — a pattern, a milestone, a shift in what the user is working on. these observations can be turned into text posts later. use this throughout the day as you notice things worth noting.",
|
|
134
|
+
{
|
|
135
|
+
observation: z.string().describe("what the agent noticed. be specific."),
|
|
136
|
+
mood: z.enum(["building", "shipping", "thinking", "learning", "debugging", "celebrating", "grinding"]).optional().describe("what kind of moment this is"),
|
|
137
|
+
could_post: z.boolean().default(true).describe("is this interesting enough to potentially post about?"),
|
|
138
|
+
},
|
|
139
|
+
async ({ observation, mood, could_post }) => {
|
|
140
|
+
const { data: profile } = await supabase
|
|
141
|
+
.from("profiles")
|
|
142
|
+
.select("id")
|
|
143
|
+
.eq("username", USER_ID)
|
|
144
|
+
.single();
|
|
145
|
+
|
|
146
|
+
if (!profile) {
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text" as const, text: `no profile found for user ${USER_ID}.` }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// store as a queued item with no url
|
|
153
|
+
const { error } = await supabase
|
|
154
|
+
.from("link_queue")
|
|
155
|
+
.insert({
|
|
156
|
+
user_id: profile.id,
|
|
157
|
+
url: `observation://${Date.now()}`,
|
|
158
|
+
title: mood ? `[${mood}] observation` : "observation",
|
|
159
|
+
context: observation,
|
|
160
|
+
agent_note: could_post ? "could make a good text post" : "just noting for context",
|
|
161
|
+
source: "conversation",
|
|
162
|
+
status: could_post ? "pending" : "dismissed",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (error) {
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text" as const, text: `error logging observation: ${error.message}` }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text" as const, text: `noted${mood ? ` [${mood}]` : ""}: "${observation}"` }],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- proactive shout features: link queue, text posts, permission tiers
|
|
2
|
+
|
|
3
|
+
-- link_queue: links spotted in conversation, not yet shouted
|
|
4
|
+
create table if not exists link_queue (
|
|
5
|
+
id uuid primary key default gen_random_uuid(),
|
|
6
|
+
user_id uuid not null references profiles(id) on delete cascade,
|
|
7
|
+
url text not null,
|
|
8
|
+
title text,
|
|
9
|
+
context text, -- what the user said when sharing
|
|
10
|
+
source text default 'conversation', -- conversation, browsing, research
|
|
11
|
+
agent_note text, -- why the agent thinks this is interesting
|
|
12
|
+
status text default 'pending' check (status in ('pending', 'shouted', 'dismissed', 'expired')),
|
|
13
|
+
spotted_at timestamptz default now(),
|
|
14
|
+
resolved_at timestamptz
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
create index if not exists idx_link_queue_user_status on link_queue(user_id, status);
|
|
18
|
+
create index if not exists idx_link_queue_spotted on link_queue(spotted_at);
|
|
19
|
+
|
|
20
|
+
-- text_posts: agent-authored status updates (not link shares)
|
|
21
|
+
-- these go through draft_posts table which already exists
|
|
22
|
+
-- adding post_type to distinguish link shouts from text posts
|
|
23
|
+
alter table shouts add column if not exists post_type text default 'link' check (post_type in ('link', 'text', 'status'));
|
|
24
|
+
|
|
25
|
+
-- user_settings: persisted settings (replacing in-memory store)
|
|
26
|
+
create table if not exists user_settings (
|
|
27
|
+
user_id uuid primary key references profiles(id) on delete cascade,
|
|
28
|
+
auto_detect_links boolean default true,
|
|
29
|
+
default_visibility text default 'public' check (default_visibility in ('public', 'private', 'unlisted')),
|
|
30
|
+
digest_frequency text default 'daily' check (digest_frequency in ('daily', 'weekly', 'never')),
|
|
31
|
+
digest_time text default '20:00', -- preferred time for daily digest (HH:MM in user's tz)
|
|
32
|
+
digest_timezone text default 'America/New_York',
|
|
33
|
+
|
|
34
|
+
-- permission tiers for agent posting
|
|
35
|
+
agent_post_mode text default 'ask' check (agent_post_mode in ('ask', 'auto', 'curated')),
|
|
36
|
+
-- for 'curated' mode: auto-post for these tags, ask for everything else
|
|
37
|
+
auto_post_tags text[] default '{}',
|
|
38
|
+
|
|
39
|
+
-- text post permissions (separate from link shouts)
|
|
40
|
+
agent_text_posts boolean default true, -- can agent draft text posts?
|
|
41
|
+
agent_text_auto_post boolean default false, -- can agent auto-publish text posts?
|
|
42
|
+
|
|
43
|
+
created_at timestamptz default now(),
|
|
44
|
+
updated_at timestamptz default now()
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- add post_type to draft_posts if it exists
|
|
48
|
+
alter table draft_posts add column if not exists post_type text default 'link' check (post_type in ('link', 'text', 'status'));
|