@nooticr/mcp 1.26.41 → 1.26.43
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 +17 -8
- package/dist/shared/amazon.js +85 -14
- package/dist/shared/backend-only.js +24 -0
- package/dist/shared/connections.js +4 -2
- package/dist/shared/evidence-digest.js +83 -20
- package/dist/shared/getting-started.js +201 -0
- package/dist/shared/jobs.js +20 -10
- package/dist/shared/loading-plans.js +21 -0
- package/dist/shared/nooticr.js +29 -1
- package/dist/shared/oauth.js +3 -2
- package/dist/shared/output-schemas.js +143 -4
- package/dist/shared/own-account.js +62 -0
- package/dist/shared/prompts.js +5 -1
- package/dist/shared/spend.js +4 -0
- package/dist/shared/tools-def.js +61 -7
- package/dist/shared/tools.js +290 -8
- package/dist/shared/ui-template.js +222 -5
- package/dist/shared/watchlist.js +39 -0
- package/package.json +1 -1
|
@@ -338,6 +338,68 @@ export function registerOwnAccountTools(server, makeClient) {
|
|
|
338
338
|
return failed("get_brand_playbook failed", err);
|
|
339
339
|
}
|
|
340
340
|
});
|
|
341
|
+
/**
|
|
342
|
+
* The three connector reads (#104): GA4, Search Console, PostHog, as of
|
|
343
|
+
* their last sync into nooticr. They are the only tools here that say
|
|
344
|
+
* whether social moved a business metric, which is why they exist; the
|
|
345
|
+
* staleness is why the sync time is said in the text as well as the
|
|
346
|
+
* payload. A model that reads "1,204 clicks" without "as of" states a
|
|
347
|
+
* week-old number as today's.
|
|
348
|
+
*/
|
|
349
|
+
const connectorRead = (name, connector, title, description) => server.registerTool(name, {
|
|
350
|
+
title,
|
|
351
|
+
description,
|
|
352
|
+
_meta: viewMeta(name),
|
|
353
|
+
// nooticr's own stored copy: nothing reaches Google or PostHog.
|
|
354
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
355
|
+
inputSchema: z
|
|
356
|
+
.object({
|
|
357
|
+
appId: z
|
|
358
|
+
.number()
|
|
359
|
+
.int()
|
|
360
|
+
.optional()
|
|
361
|
+
.describe("Your product's id. Omit only with a single-app workspace."),
|
|
362
|
+
})
|
|
363
|
+
.strict(),
|
|
364
|
+
outputSchema: OUTPUT_SCHEMAS[name],
|
|
365
|
+
}, async (args, extra) => {
|
|
366
|
+
const client = await makeClient({ ...extra, arguments: args });
|
|
367
|
+
try {
|
|
368
|
+
const proxy = await client.callTool(name, args);
|
|
369
|
+
const data = { ...(proxy.structured ?? {}), connector };
|
|
370
|
+
const synced = String(data.synced_at ?? data.lastSync ?? data.lastRefresh ?? "");
|
|
371
|
+
const lead = data.message
|
|
372
|
+
? String(data.message)
|
|
373
|
+
: synced
|
|
374
|
+
? `${title.replace(/^Get /, "")} for ${String(data.appName ?? "your product")}, as of its last sync at ` +
|
|
375
|
+
`${synced} — not live. Say that date whenever you quote a number from it.`
|
|
376
|
+
: `${title.replace(/^Get /, "")} for ${String(data.appName ?? "your product")}; the sync time was not reported, so do not present these as current.`;
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: `${lead}\n\n${JSON.stringify(data, null, 2)}` }],
|
|
379
|
+
structuredContent: data,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
return failed(`${name} failed`, err);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
connectorRead("get_google_analytics", "google_analytics", "Get Google Analytics", "Your own product's Google Analytics (GA4) sync, as of its last sync into nooticr — not a live " +
|
|
387
|
+
"GA4 call: the property, the date range synced and how many daily rows came in. The metrics " +
|
|
388
|
+
"themselves (installs, opens, purchases, revenue) feed get_post_performance and growth_brief " +
|
|
389
|
+
"rather than being repeated here. Returns a message rather than an error when nothing has " +
|
|
390
|
+
"ever been synced; connect GA4 in API Connections first. Takes appId (optional when your " +
|
|
391
|
+
"workspace has only one product). No cost to call.");
|
|
392
|
+
connectorRead("get_search_console_data", "search_console", "Get Search Console Data", "Your own product's Google Search Console data — total clicks and impressions and the top " +
|
|
393
|
+
"search queries — as of its last sync into nooticr, not a live Search Console call. Use it " +
|
|
394
|
+
"to see whether people search for what your social content is about. Returns a message " +
|
|
395
|
+
"rather than an error when nothing has been synced; connect Search Console in API " +
|
|
396
|
+
"Connections first. Takes appId (optional when your workspace has only one product). No " +
|
|
397
|
+
"cost to call.");
|
|
398
|
+
connectorRead("get_posthog_analytics", "posthog", "Get PostHog Analytics", "Your own product's PostHog pageview trend as of its last sync into nooticr — not a live " +
|
|
399
|
+
"PostHog call. Use it to line a post's date up against traffic to your site. Returns a " +
|
|
400
|
+
"message rather than an error when nothing has been synced; connect PostHog in API " +
|
|
401
|
+
"Connections first. Takes appId (optional when your workspace has only one product). No " +
|
|
402
|
+
"cost to call.");
|
|
341
403
|
server.registerTool("analyze_product", {
|
|
342
404
|
title: "Analyze Product",
|
|
343
405
|
description: "Start an AI analysis of your own product: fetches an excerpt of the product's own website " +
|
package/dist/shared/prompts.js
CHANGED
|
@@ -286,7 +286,11 @@ export function registerPrompts(server) {
|
|
|
286
286
|
`name or a domain into an account. Hand back a compact result before calling anything paid: ` +
|
|
287
287
|
`{tiktok: <handle or null>, instagram: <handle or null>, youtube: <handle or null>, ...} for ` +
|
|
288
288
|
`whichever networks you can confirm, plus 2-3 niche keywords describing what they post about` +
|
|
289
|
-
`${niche ? ` (you already gave me: ${niche})` : ""}.
|
|
289
|
+
`${niche ? ` (you already gave me: ${niche})` : ""}. Once one handle is confirmed on TikTok, ` +
|
|
290
|
+
`Instagram or Xiaohongshu, suggest_creator_identity on it looks for the same account on the ` +
|
|
291
|
+
`other two, with the evidence for each (6 credits) — instead of guessing handles network by ` +
|
|
292
|
+
`network. Its results are suggestions: show me the evidence and let me confirm before you ` +
|
|
293
|
+
`treat any of them as theirs. A wrong handle still spends 2 credits per ` +
|
|
290
294
|
`network call and returns nothing — and that looks exactly like a genuinely quiet account, so ` +
|
|
291
295
|
`get the handle right before you spend anything on it.\n\n` +
|
|
292
296
|
`Once you have a handle${platform ? ` on ${platform}` : ""}, work in this order:\n` +
|
package/dist/shared/spend.js
CHANGED
|
@@ -100,6 +100,10 @@ export const BACKEND_CALL_CREDITS = {
|
|
|
100
100
|
discover_social_posts: 2,
|
|
101
101
|
search_creators: 2,
|
|
102
102
|
get_similar_creators: 2,
|
|
103
|
+
// At its default: the seed lookup plus the two other searchable networks.
|
|
104
|
+
// The price moves with `platforms`, but it never exceeds this, which is why
|
|
105
|
+
// the tool needs no confirmation (it sits at CONFIRM_ABOVE_CREDITS).
|
|
106
|
+
suggest_creator_identity: 6,
|
|
103
107
|
discover_sounds: 2,
|
|
104
108
|
discover_hashtags: 2,
|
|
105
109
|
get_post_frames: 2,
|
package/dist/shared/tools-def.js
CHANGED
|
@@ -48,6 +48,12 @@ export const TOOL_DEFINITIONS = [
|
|
|
48
48
|
description: "Find lookalike creators for a given handle — TikTok similar-user recommendations or Instagram similar users. Useful for scaling: 'if this creator works, here are more like them'. (20 free credits for new users). Use when one creator already fits and you want more of the same. Consumes 2 nooticr credits.",
|
|
49
49
|
inputSchema: z.object({ username: z.string().describe("Seed creator handle, e.g. 'zoundsapp'."), platform: z.enum(["tiktok", "instagram"]).optional().describe("Which platform (default tiktok).") }).strict(),
|
|
50
50
|
},
|
|
51
|
+
{
|
|
52
|
+
name: "suggest_creator_identity",
|
|
53
|
+
title: "Same Person Elsewhere",
|
|
54
|
+
description: "Given a handle on one network, find accounts on the other networks that may be the same person, with the evidence for each (shared bio link, cross-link, identical handle, matching display name). It SUGGESTS and never merges; a human confirms, and follower counts are never added together. Serves tiktok, instagram, xiaohongshu. Consumes 2 nooticr credits per network searched plus 2 to find the handle: 6 by default.",
|
|
55
|
+
inputSchema: z.object({ handle: z.string().describe("The account you already know, with or without @."), platform: z.enum(["tiktok", "instagram", "xiaohongshu"]).optional().describe("Which network that handle is on (default tiktok)."), platforms: z.array(z.enum(["tiktok", "instagram", "xiaohongshu"])).optional().describe("Networks to look on. Defaults to every searchable one except the handle's own.") }).strict(),
|
|
56
|
+
},
|
|
51
57
|
{
|
|
52
58
|
name: "discover_sounds",
|
|
53
59
|
title: "Discover Sounds",
|
|
@@ -60,17 +66,23 @@ export const TOOL_DEFINITIONS = [
|
|
|
60
66
|
description: "The same frames and transcript analyze_post returns, asked a different question: describe what physically happens on screen, in order, with every observation anchored to a frame. Supports TikTok, Instagram, YouTube, X, Reddit, Douyin, Xiaohongshu, Weibo, Bilibili and LinkedIn. Use when you need the events rather than the strategy; analyze_post puts the strategic question to the same material. Fans out to get_post_frames and get_post_transcript: 3 nooticr credits, 2 for the frames plus 1 for the transcript.",
|
|
61
67
|
inputSchema: z.object({ url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube/X/Reddit/Douyin/Xiaohongshu/Weibo/Bilibili/LinkedIn)."), focus: z.string().optional().describe("Extra instruction, e.g. 'focus on the CTA'.") }).strict(),
|
|
62
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "detect_spoken_mentions",
|
|
71
|
+
title: "Detect Spoken Mentions",
|
|
72
|
+
description: "The brands a creator names OUT LOUD in one video, for you to find: the spoken transcript and the written caption side by side. List each brand, product or company named with the verbatim sentence, the creator's framing, and mentionedIn spoken, caption or both. Give brands to focus the watch; others named are still reported. Waits for the audio to be transcribed. Costs 2 nooticr credits — 1 for get_post_transcript plus 1 for get_social_media — and nothing when no transcript can be made.",
|
|
73
|
+
inputSchema: z.object({ url: z.string(), brands: z.array(z.string()).max(20).optional(), language: z.string().optional() }).strict(),
|
|
74
|
+
},
|
|
63
75
|
{
|
|
64
76
|
name: "get_post_transcript",
|
|
65
77
|
title: "Get Post Transcript",
|
|
66
|
-
description: "Get the words actually spoken in a post, on any platform nooticr reads. Where the platform publishes a caption track (TikTok, Douyin, YouTube) it is read as-is; everywhere else the audio is transcribed asynchronously — a first call returning available:false with transcribing:true and a retryAfterMs is the job accepted, not a failure, so call again with the same url. Cheap and exact — use this before analyze_post when you need the script, hook wording or CTA verbatim rather than an interpretation. Returns plain text with a word count, or available:false with a reason. A poll costs nothing, and neither does a call that comes back with no transcript. The listening route needs speech-to-text configured on the server and cannot reach Reddit or Bilibili at all. Use before any analysis when the exact wording matters. Consumes 1 nooticr credit.",
|
|
67
|
-
inputSchema: z.object({ url: z.string().describe("Post URL (TikTok or YouTube)."), language: z.string().optional().describe("Preferred language code, e.g. 'en'.") }).strict(),
|
|
78
|
+
description: "Get the words actually spoken in a post, on any platform nooticr reads. Where the platform publishes a caption track (TikTok, Douyin, YouTube) it is read as-is; everywhere else the audio is transcribed asynchronously — a first call returning available:false with transcribing:true and a retryAfterMs is the job accepted, not a failure, so call again with the same url. Cheap and exact — use this before analyze_post when you need the script, hook wording or CTA verbatim rather than an interpretation. Returns plain text with a word count, or available:false with a reason; format 'srt' or 'vtt' also returns a ready caption file built from the track's own timing. A poll costs nothing, and neither does a call that comes back with no transcript. The listening route needs speech-to-text configured on the server and cannot reach Reddit or Bilibili at all. Use before any analysis when the exact wording matters. Consumes 1 nooticr credit.",
|
|
79
|
+
inputSchema: z.object({ url: z.string().describe("Post URL (TikTok or YouTube)."), language: z.string().optional().describe("Preferred language code, e.g. 'en'."), format: z.enum(["text", "srt", "vtt"]).optional().describe("Also return a caption file ('srt' or 'vtt'). Default 'text'.") }).strict(),
|
|
68
80
|
},
|
|
69
81
|
{
|
|
70
82
|
name: "analyze_comments",
|
|
71
83
|
title: "Analyze Comments",
|
|
72
84
|
description: "A post's comment section, fetched and laid out for you to classify: every comment with a stable id, plus whatever themes the platform clustered them into. Label each one's sentiment and what it is doing — praise, complaint, bug report, question, request, comparison, spam — then summarise the themes, the questions worth answering, the objections, and what to make next. The result names the exact labels, and show_comment_review draws them for free afterwards. Use when the goal is 'what should I make next' rather than 'what did people write'. Costs 2 nooticr credits, for the one get_post_comments call it makes.",
|
|
73
|
-
inputSchema: z.object({ url: z.string().describe("Full public post URL."), limit: z.number().int().optional().describe("Comments to read (default 50, max 100).") }).strict(),
|
|
85
|
+
inputSchema: z.object({ url: z.string().describe("Full public post URL."), limit: z.number().int().optional().describe("Comments to read (default 50, max 100)."), verbatim: z.boolean().optional().describe("Render every comment whole in the text you read (default false).") }).strict(),
|
|
74
86
|
},
|
|
75
87
|
{
|
|
76
88
|
name: "compare_posts",
|
|
@@ -186,12 +198,30 @@ export const TOOL_DEFINITIONS = [
|
|
|
186
198
|
description: "Display the rewritten copy you produced after repurpose_post handed you the source post's material. Free, and makes no requests — it only draws what you pass it: one entry per surface you rewrote it for. Call this after you have done the rewriting, not instead of it.",
|
|
187
199
|
inputSchema: z.object({ sourceUrl: z.string(), versions: z.array(z.object({ surface: z.string(), content: z.string() })).min(1) }).strict(),
|
|
188
200
|
},
|
|
201
|
+
{
|
|
202
|
+
name: "nooticr_getting_started",
|
|
203
|
+
title: "Getting Started with nooticr",
|
|
204
|
+
description: "Start here: help on what nooticr can do and what to try first. Free, and makes no paid call. Reads this account's state (credit balance, connected accounts, watchlist) and returns the next calls worth making, each with its price and an example. No cost to call.",
|
|
205
|
+
inputSchema: z.object({}).strict(),
|
|
206
|
+
},
|
|
189
207
|
{
|
|
190
208
|
name: "check_nooticr_credits",
|
|
191
209
|
title: "Check Nooticr Credits",
|
|
192
210
|
description: "Check your nooticr credit balance. No cost to call — call anytime to see remaining credits before running other tools. Nothing here sells or tops up credits: this server offers no purchase of any kind, so when the balance is short, say that it is and that topping up happens on the nooticr website, and do not offer a link or a price. Use before a run of paid calls to confirm the balance covers it.",
|
|
193
211
|
inputSchema: z.object({}).strict(),
|
|
194
212
|
},
|
|
213
|
+
{
|
|
214
|
+
name: "list_tool_runs",
|
|
215
|
+
title: "List Tool Runs",
|
|
216
|
+
description: "Your own tool-call history, newest first: which tool ran, when, whether it worked, how long it took and the credits it actually took (0 for a free, refunded, cached or replayed call). Filter by tool, time window, success or minCredits; page with before/nextBefore. scope workspace shows every member's runs to the workspace's owner and admins. No cost to call. Use when a balance moved more than expected.",
|
|
217
|
+
inputSchema: z.object({ tool: z.string().optional(), from: z.string().optional(), to: z.string().optional(), success: z.boolean().optional(), minCredits: z.number().int().min(0).optional(), limit: z.number().int().min(1).max(100).optional(), before: z.number().int().optional(), scope: z.enum(["mine", "workspace"]).optional() }).strict(),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "get_tool_run",
|
|
221
|
+
title: "Get Tool Run",
|
|
222
|
+
description: "One run from list_tool_runs by its id, with its full error text, credentials scrubbed. A run outside your scope reads as not found. No cost to call. Use to look closely at one charge.",
|
|
223
|
+
inputSchema: z.object({ id: z.number().int(), scope: z.enum(["mine", "workspace"]).optional() }).strict(),
|
|
224
|
+
},
|
|
195
225
|
{
|
|
196
226
|
name: "nooticr_login",
|
|
197
227
|
title: "Nooticr Login",
|
|
@@ -204,6 +234,12 @@ export const TOOL_DEFINITIONS = [
|
|
|
204
234
|
description: "Add a creator to your watchlist so you can ask later what they have posted since. Stores the handle only \u2014 nothing is fetched here. Use catch_up_watchlist to see what changed. Use when you want to follow someone over time rather than look at them once. No cost to call.",
|
|
205
235
|
inputSchema: z.object({ username: z.string().describe("Creator handle, with or without @."), platform: z.string().optional().describe("Platform (default tiktok)."), note: z.string().optional().describe("Why you are watching them \u2014 shown back to you later.") }).strict(),
|
|
206
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: "list_watchlist",
|
|
239
|
+
title: "List Watchlist",
|
|
240
|
+
description: "The creators on your watchlist: handle, platform, your note, when you added them and when you last caught up on them. Reads the stored list only, so there is no cost. Use it to answer 'who am I watching?' or to find the exact handle unwatch_creator needs. No cost to call.",
|
|
241
|
+
inputSchema: z.object({}).strict(),
|
|
242
|
+
},
|
|
207
243
|
{
|
|
208
244
|
name: "unwatch_creator",
|
|
209
245
|
title: "Unwatch Creator",
|
|
@@ -288,6 +324,24 @@ export const TOOL_DEFINITIONS = [
|
|
|
288
324
|
description: "Your own most recently synced video performance stats across every connected creator — views, likes, comments, shares, plus a running total. Reads the last sync; does not trigger a new one. No cost to call.",
|
|
289
325
|
inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace."), limit: z.number().int().optional().describe("Max videos (default 20, capped at 50).") }).strict(),
|
|
290
326
|
},
|
|
327
|
+
{
|
|
328
|
+
name: "get_google_analytics",
|
|
329
|
+
title: "Get Google Analytics",
|
|
330
|
+
description: "Your own product's Google Analytics (GA4) sync, as of its last sync into nooticr — not a live GA4 call: the property, the date range synced and how many daily rows came in. Connect GA4 in API Connections first. No cost to call.",
|
|
331
|
+
inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace.") }).strict(),
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "get_search_console_data",
|
|
335
|
+
title: "Get Search Console Data",
|
|
336
|
+
description: "Your own product's Google Search Console clicks, impressions and top queries as of its last sync into nooticr — not a live Search Console call. Connect Search Console in API Connections first. No cost to call.",
|
|
337
|
+
inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace.") }).strict(),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "get_posthog_analytics",
|
|
341
|
+
title: "Get PostHog Analytics",
|
|
342
|
+
description: "Your own product's PostHog pageview trend as of its last sync into nooticr — not a live PostHog call. Connect PostHog in API Connections first. No cost to call.",
|
|
343
|
+
inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace.") }).strict(),
|
|
344
|
+
},
|
|
291
345
|
{
|
|
292
346
|
name: "get_content_plan",
|
|
293
347
|
title: "Get Content Plan",
|
|
@@ -327,7 +381,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
327
381
|
{
|
|
328
382
|
name: "list_social_connections",
|
|
329
383
|
title: "List Social Connections",
|
|
330
|
-
description: "List the social accounts your workspace has connected and what each connection is allowed to do — read the account, publish a post, manage comments. Also returns which platforms can be connected at all. No cost to call.",
|
|
384
|
+
description: "List the social accounts your workspace has connected and what each connection is allowed to do — read the account, publish a post, manage comments. Manage comments reports the grant; no nooticr tool posts or replies to a comment. Also returns which platforms can be connected at all. No cost to call.",
|
|
331
385
|
inputSchema: z.object({}).strict(),
|
|
332
386
|
},
|
|
333
387
|
{
|
|
@@ -339,7 +393,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
339
393
|
{
|
|
340
394
|
name: "answer_my_audience",
|
|
341
395
|
title: "Answer My Audience",
|
|
342
|
-
description: "The questions waiting for you under your own posts. Fetches a creator's recent posts, reads the comments on each, and returns them grouped under the post they were left on \u2014 every comment with a stable id, and the ones that read like questions or requests flagged and sorted to the top. Finds and drafts; it cannot post a reply, because
|
|
396
|
+
description: "The questions waiting for you under your own posts. Fetches a creator's recent posts, reads the comments on each, and returns them grouped under the post they were left on \u2014 every comment with a stable id, and the ones that read like questions or requests flagged and sorted to the top. Finds and drafts; it cannot post a reply, because nooticr does not post comments whatever a connection allows \u2014 the drafts are for a person to paste in themselves. Use when the job is to answer your own audience rather than to read about strangers. Consumes 2 nooticr credits for the post list plus 2 per post opened \u2014 14 credits at the default of 6 posts.",
|
|
343
397
|
inputSchema: z.object({ username: z.string().describe("Your handle, with or without @."), platform: z.string().optional().describe("Platform (default tiktok)."), limit: z.number().int().optional().describe("Posts to open (default 6, max 12). Each one is a comment fetch, so this is the price."), commentsPerPost: z.number().int().optional().describe("Comments to read per post (default 20, max 50)."), since: z.string().optional().describe("Only posts published on or after this date, as YYYY-MM-DD. Windows the posts, not the comments.") }).strict(),
|
|
344
398
|
},
|
|
345
399
|
{
|
|
@@ -394,7 +448,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
394
448
|
name: "find_people_with_problem",
|
|
395
449
|
title: "Find People With A Problem",
|
|
396
450
|
description: "Find people describing a problem in their own words — prospects, early users, anyone whose complaint your product answers. Say the problem the way a person would say it (\"I spend hours checking competitors by hand\") and this searches for posts shaped like that complaint, not for the topic it is about. That difference is the point: searching the TOPIC finds marketing about it, which is what discover_social_posts is for. Widens the query into the forms a complaint takes — the plain phrasing, the does-anyone-else question, the is-there-a-tool ask — and merges what each returns. It is a wide cheap net and NOT a filter: the posts come back for you to judge, and many will be off-target. Consumes 2 nooticr credits per search, and each platform is searched once per query shape — three by default, so 6 credits per platform. Defaults to reddit, where people describe workflow pain in sentences; add twitter for volume. Not searchable here: linkedin.",
|
|
397
|
-
inputSchema: z.object({ problem: z.string().describe("The problem in a person's own words, not a topic. \"I waste an hour a day checking what competitors posted\" finds people; \"competitor analysis\" finds marketing about competitor analysis."), platforms: z.array(z.enum(["reddit", "twitter", "youtube", "tiktok", "instagram"])).optional().describe("Where to look (default reddit). Each one is a separate paid search per query shape. Not searchable here: linkedin — no keyword post search exists for it upstream, so asking for it could only ever return nothing, which is why it is absent from this list rather than merely untested."), limit: z.number().int().optional().describe("Posts per query shape (default 6, max 15)."), readComments: z.boolean().optional().describe("Open the most promising results and read their comments too (default false). On Reddit especially, the person with the problem is at least as often replying under someone else's thread as posting their own, and a post-only search cannot see any of them. Costs 2 nooticr credits per post opened, up to 5 \u2014 and they are opened on the MERGED, deduped results, so it is five extra calls in total rather than five per query shape per platform."), queries: z.array(z.string()).optional().describe("Search these exact phrasings instead of the ones derived from `problem` (max 4). You are better at this than a string template is: write how a person would actually word the complaint on the network being searched \u2014 a Reddit title, an X post \u2014 and keep each one short, because this is a keyword search and a long sentence matches on its commonest words.") }).strict(),
|
|
451
|
+
inputSchema: z.object({ problem: z.string().describe("The problem in a person's own words, not a topic. \"I waste an hour a day checking what competitors posted\" finds people; \"competitor analysis\" finds marketing about competitor analysis."), platforms: z.array(z.enum(["reddit", "twitter", "youtube", "tiktok", "instagram"])).optional().describe("Where to look (default reddit). Each one is a separate paid search per query shape. Not searchable here: linkedin — no keyword post search exists for it upstream, so asking for it could only ever return nothing, which is why it is absent from this list rather than merely untested."), limit: z.number().int().optional().describe("Posts per query shape (default 6, max 15)."), readComments: z.boolean().optional().describe("Open the most promising results and read their comments too (default false). On Reddit especially, the person with the problem is at least as often replying under someone else's thread as posting their own, and a post-only search cannot see any of them. Costs 2 nooticr credits per post opened, up to 5 \u2014 and they are opened on the MERGED, deduped results, so it is five extra calls in total rather than five per query shape per platform."), queries: z.array(z.string()).optional().describe("Search these exact phrasings instead of the ones derived from `problem` (max 4). You are better at this than a string template is: write how a person would actually word the complaint on the network being searched \u2014 a Reddit title, an X post \u2014 and keep each one short, because this is a keyword search and a long sentence matches on its commonest words."), verbatim: z.boolean().optional().describe("Render every post and reply whole in the text you read (default false).") }).strict(),
|
|
398
452
|
},
|
|
399
453
|
{
|
|
400
454
|
name: "search_spoken_mentions",
|
|
@@ -453,7 +507,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
453
507
|
{
|
|
454
508
|
name: "show_amazon_category_insights",
|
|
455
509
|
title: "Show Amazon Category Insights",
|
|
456
|
-
description: "Display the category read you produced from scan_amazon_category: purchase drivers, barriers, what each brand does well, the gaps, and the positioning angles \u2014 drawn beside the listings and their reviews, so a person can click a product and check any claim against the text it came from.
|
|
510
|
+
description: "Display the category read you produced from scan_amazon_category: purchase drivers, barriers, what each brand does well, the gaps, and the positioning angles \u2014 drawn beside the listings and their reviews, so a person can click a product and check any claim against the text it came from. Pass the scanId: the listings are then re-read from the scan itself (free) rather than taken from you, and any ASIN you cite that the scan does not contain is flagged. Without a scanId, listings you pass are drawn marked as not checked against a scan. Free. Your read is attributed to you rather than presented as a nooticr rating of anyone's product. Call this after you have read the reviews, not instead of reading them.",
|
|
457
511
|
inputSchema: z.object({ category: z.string().describe("The category this is a read of."), summary: z.string().optional(), drivers: z.array(z.any()).optional().describe("What makes someone buy, each with the review evidence behind it."), barriers: z.array(z.any()).optional().describe("What stops them buying, or makes them return it."), strengths: z.array(z.any()).optional().describe("What each incumbent does well."), gaps: z.array(z.any()).optional().describe("What buyers keep asking for that nobody serves."), positioning: z.array(z.any()).optional().describe("Angles a new entrant could take."), products: z.array(z.any()).optional().describe("The listings from the scan, passed straight through."), rollup: z.record(z.unknown()).optional().describe("The rollup from the scan, passed straight through."), scanId: z.string().optional() }).passthrough(),
|
|
458
512
|
},
|
|
459
513
|
];
|