44reports-mcp 1.0.9 → 1.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.
@@ -52,6 +52,23 @@ export interface Folder {
52
52
  name: string;
53
53
  description: string | null;
54
54
  allowed_domains: string[];
55
+ allowed_emails: string[];
56
+ created_at: string;
57
+ updated_at: string;
58
+ }
59
+ export interface BacklogEntry {
60
+ id: string;
61
+ product_slug: string | null;
62
+ content: string;
63
+ source: string | null;
64
+ source_context: string | null;
65
+ domain_hint: string | null;
66
+ tags: string | null;
67
+ status: string;
68
+ result_summary: string | null;
69
+ applied_changes: string | null;
70
+ rejection_reason: string | null;
71
+ processed_at: string | null;
55
72
  created_at: string;
56
73
  updated_at: string;
57
74
  }
@@ -92,7 +109,8 @@ export declare class ApiClient {
92
109
  name: string;
93
110
  slug?: string;
94
111
  description?: string;
95
- allowed_domains: string[];
112
+ allowed_domains?: string[];
113
+ allowed_emails?: string[];
96
114
  }): Promise<{
97
115
  folder: Folder;
98
116
  }>;
@@ -104,6 +122,7 @@ export declare class ApiClient {
104
122
  name?: string;
105
123
  description?: string;
106
124
  allowed_domains?: string[];
125
+ allowed_emails?: string[];
107
126
  }): Promise<{
108
127
  folder: Folder;
109
128
  }>;
@@ -117,4 +136,147 @@ export declare class ApiClient {
117
136
  removeReportFromFolder(folderSlug: string, reportSlug: string): Promise<{
118
137
  success: boolean;
119
138
  }>;
139
+ listProducts(options?: {
140
+ search?: string;
141
+ limit?: number;
142
+ offset?: number;
143
+ }): Promise<{
144
+ products: Array<{
145
+ id: string;
146
+ slug: string;
147
+ name: string;
148
+ description: string | null;
149
+ created_at: string;
150
+ updated_at: string;
151
+ }>;
152
+ total: number;
153
+ }>;
154
+ getProduct(slug: string, domain?: string): Promise<{
155
+ product: {
156
+ id: string;
157
+ slug: string;
158
+ name: string;
159
+ description: string | null;
160
+ created_at: string;
161
+ updated_at: string;
162
+ };
163
+ knowledge: Record<string, unknown>;
164
+ doc_files: Array<{
165
+ path: string;
166
+ size: number;
167
+ domain: string;
168
+ }>;
169
+ binary_files: Array<{
170
+ path: string;
171
+ size: number;
172
+ content_type: string;
173
+ domain: string;
174
+ }>;
175
+ }>;
176
+ getProductFile(slug: string, filepath: string): Promise<{
177
+ path: string;
178
+ content: unknown;
179
+ content_type: string;
180
+ size?: number;
181
+ }>;
182
+ updateProductFile(slug: string, filepath: string, body: {
183
+ content: unknown;
184
+ updated_by?: string;
185
+ }): Promise<{
186
+ success: boolean;
187
+ path: string;
188
+ merged?: unknown;
189
+ }>;
190
+ uploadProductFiles(slug: string, files: Array<{
191
+ path: string;
192
+ content: string;
193
+ content_type?: string;
194
+ }>): Promise<{
195
+ success: boolean;
196
+ uploaded: string[];
197
+ }>;
198
+ getProductHealth(slug: string): Promise<{
199
+ slug: string;
200
+ name: string;
201
+ overall_score: number;
202
+ files: Record<string, unknown>;
203
+ recommendations: string[];
204
+ }>;
205
+ getAllProductsHealth(): Promise<{
206
+ products: Array<{
207
+ slug: string;
208
+ name: string;
209
+ overall_score: number;
210
+ files: Record<string, {
211
+ score: number;
212
+ missing_required: string[];
213
+ }>;
214
+ }>;
215
+ }>;
216
+ createProduct(data: {
217
+ name: string;
218
+ slug?: string;
219
+ description?: string;
220
+ }): Promise<{
221
+ product: {
222
+ id: string;
223
+ slug: string;
224
+ name: string;
225
+ description: string | null;
226
+ created_at: string;
227
+ updated_at: string;
228
+ };
229
+ }>;
230
+ deleteProduct(slug: string): Promise<{
231
+ success: boolean;
232
+ }>;
233
+ deleteProductFile(slug: string, filepath: string): Promise<{
234
+ success: boolean;
235
+ }>;
236
+ getProductIndex(slug: string, domain?: string): Promise<{
237
+ product_slug: string;
238
+ updated_at: string;
239
+ files: Array<{
240
+ path: string;
241
+ title: string;
242
+ summary: string;
243
+ sections: string[];
244
+ domain: string;
245
+ content_type: string;
246
+ size: number;
247
+ updated_at: string;
248
+ }>;
249
+ }>;
250
+ pullProductFiles(slug: string, options?: {
251
+ files?: string[];
252
+ include_knowledge?: boolean;
253
+ }): Promise<{
254
+ files: PulledFile[];
255
+ }>;
256
+ addBacklogEntry(data: {
257
+ content: string;
258
+ product_slug?: string;
259
+ source?: string;
260
+ source_context?: string;
261
+ domain_hint?: string;
262
+ tags?: string[];
263
+ }): Promise<{
264
+ entry: BacklogEntry;
265
+ }>;
266
+ listBacklog(options?: {
267
+ product_slug?: string;
268
+ status?: string;
269
+ limit?: number;
270
+ offset?: number;
271
+ }): Promise<{
272
+ entries: BacklogEntry[];
273
+ total: number;
274
+ }>;
275
+ deleteBacklogEntry(id: string): Promise<{
276
+ success: boolean;
277
+ }>;
278
+ processBacklog(options?: {
279
+ product_slug?: string;
280
+ dry_run?: boolean;
281
+ }): Promise<unknown>;
120
282
  }
@@ -90,4 +90,99 @@ export class ApiClient {
90
90
  method: 'DELETE',
91
91
  });
92
92
  }
93
+ // --- Product Knowledge ---
94
+ async listProducts(options) {
95
+ const params = new URLSearchParams();
96
+ for (const [key, value] of Object.entries(options ?? {})) {
97
+ if (value !== undefined) {
98
+ params.set(key, String(value));
99
+ }
100
+ }
101
+ const query = params.toString();
102
+ return this.request(`/api/products${query ? `?${query}` : ''}`);
103
+ }
104
+ async getProduct(slug, domain) {
105
+ const query = domain ? `?domain=${encodeURIComponent(domain)}` : '';
106
+ return this.request(`/api/products/${slug}${query}`);
107
+ }
108
+ async getProductFile(slug, filepath) {
109
+ return this.request(`/api/products/${slug}/files/${filepath}`);
110
+ }
111
+ async updateProductFile(slug, filepath, body) {
112
+ return this.request(`/api/products/${slug}/files/${filepath}`, {
113
+ method: 'PUT',
114
+ body: JSON.stringify(body),
115
+ });
116
+ }
117
+ async uploadProductFiles(slug, files) {
118
+ return this.request(`/api/products/${slug}/files`, {
119
+ method: 'POST',
120
+ body: JSON.stringify({ files }),
121
+ });
122
+ }
123
+ async getProductHealth(slug) {
124
+ return this.request(`/api/products/${slug}/health`);
125
+ }
126
+ async getAllProductsHealth() {
127
+ return this.request('/api/products/health');
128
+ }
129
+ async createProduct(data) {
130
+ return this.request('/api/products', {
131
+ method: 'POST',
132
+ body: JSON.stringify(data),
133
+ });
134
+ }
135
+ async deleteProduct(slug) {
136
+ return this.request(`/api/products/${slug}`, {
137
+ method: 'DELETE',
138
+ });
139
+ }
140
+ async deleteProductFile(slug, filepath) {
141
+ return this.request(`/api/products/${slug}/files/${filepath}`, {
142
+ method: 'DELETE',
143
+ });
144
+ }
145
+ async getProductIndex(slug, domain) {
146
+ const query = domain ? `?domain=${encodeURIComponent(domain)}` : '';
147
+ return this.request(`/api/products/${slug}/index${query}`);
148
+ }
149
+ async pullProductFiles(slug, options) {
150
+ return this.request(`/api/products/${slug}/pull`, {
151
+ method: 'POST',
152
+ body: JSON.stringify(options ?? {}),
153
+ });
154
+ }
155
+ // --- Agent Backlog ---
156
+ async addBacklogEntry(data) {
157
+ return this.request('/api/backlog', {
158
+ method: 'POST',
159
+ body: JSON.stringify(data),
160
+ });
161
+ }
162
+ async listBacklog(options) {
163
+ const params = new URLSearchParams();
164
+ for (const [key, value] of Object.entries(options ?? {})) {
165
+ if (value !== undefined) {
166
+ params.set(key, String(value));
167
+ }
168
+ }
169
+ const query = params.toString();
170
+ return this.request(`/api/backlog${query ? `?${query}` : ''}`);
171
+ }
172
+ async deleteBacklogEntry(id) {
173
+ return this.request(`/api/backlog/${id}`, {
174
+ method: 'DELETE',
175
+ });
176
+ }
177
+ async processBacklog(options) {
178
+ const params = new URLSearchParams();
179
+ if (options?.product_slug)
180
+ params.set('product_slug', options.product_slug);
181
+ if (options?.dry_run !== undefined)
182
+ params.set('dry_run', String(options.dry_run));
183
+ const query = params.toString();
184
+ return this.request(`/api/backlog/process${query ? `?${query}` : ''}`, {
185
+ method: 'POST',
186
+ });
187
+ }
93
188
  }
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { deployReportSchema, updateReportSchema, createDeployTool, createUpdateT
8
8
  import { listReportsSchema, getReportSchema, deleteReportSchema, createListTool, createGetTool, createDeleteTool, } from './tools/reports.js';
9
9
  import { pullContextSchema, createPullTool, } from './tools/pull.js';
10
10
  import { manageFoldersSchema, createManageFoldersTool, } from './tools/folders.js';
11
+ import { listProductsSchema, getProductSchema, readProductFileSchema, updateProductKnowledgeSchema, uploadProductFilesSchema, pullProductFilesSchema, createProductSchema, deleteProductSchema, deleteProductFileSchema, getProductHealthSchema, getProductIndexSchema, createListProductsTool, createGetProductTool, createReadProductFileTool, createUpdateProductKnowledgeTool, createUploadProductFilesTool, createPullProductFilesTool, createGetProductHealthTool, createGetProductIndexTool, createCreateProductTool, createDeleteProductTool, createDeleteProductFileTool, } from './tools/products.js';
12
+ import { addToBacklogSchema, listBacklogSchema, processBacklogSchema, createAddToBacklogTool, createListBacklogTool, createProcessBacklogTool, } from './tools/backlog.js';
11
13
  const config = loadConfig();
12
14
  const client = new ApiClient(config);
13
15
  function prop(type, description, extra = {}) {
@@ -18,7 +20,7 @@ const FOLDER_PATH_DESC = 'RECOMMENDED for most contexts. Uploads all supported f
18
20
  const toolDefinitions = [
19
21
  {
20
22
  name: 'deploy_context',
21
- description: 'Deploy files to the context repository. Supports any text-based files (code, docs, data, config) and binary files (images, fonts). For simple single-file contexts, use content. For multi-file contexts, use folder_path (recommended). Returns a stable URL for viewing and sharing.',
23
+ description: 'Deploy an output artifact to the context repository use for one-time or versioned outputs: research reports, data analysis, dashboards, HTML visualizations, CSV exports, agent session summaries.\n\nIMPORTANT: Do NOT use this for brand or product knowledge (guidelines, copy, ad accounts, CPPs, product positioning). For persistent product knowledge that agents read and update over time, use update_product_knowledge instead.',
22
24
  inputSchema: {
23
25
  type: 'object',
24
26
  properties: {
@@ -40,7 +42,7 @@ const toolDefinitions = [
40
42
  },
41
43
  {
42
44
  name: 'update_context',
43
- description: 'Update an existing context. The URL remains stable. You can update metadata, content, or both.',
45
+ description: 'Update an existing output artifact context. The URL remains stable. You can update metadata, content, or both.\n\nIMPORTANT: Do NOT use this for brand or product knowledge. Use update_product_knowledge for product profiles, brand guidelines, copy, and ad accounts.',
44
46
  inputSchema: {
45
47
  type: 'object',
46
48
  properties: {
@@ -116,7 +118,7 @@ const toolDefinitions = [
116
118
  },
117
119
  {
118
120
  name: 'manage_folders',
119
- description: 'Manage folders for domain-scoped access control. Folders let you share specific contexts with external users by email domain (e.g., share investor reports with vgames.vc).',
121
+ description: 'Manage folders for access control. Folders let you share contexts with external users by email domain (e.g., vgames.vc) or individual email address (e.g., investor@gmail.com).',
120
122
  inputSchema: {
121
123
  type: 'object',
122
124
  properties: {
@@ -126,7 +128,8 @@ const toolDefinitions = [
126
128
  slug: prop('string', 'Folder slug (required for get/update/delete/add_reports/remove_report)'),
127
129
  name: prop('string', 'Folder name (required for create)'),
128
130
  description: prop('string', 'Folder description'),
129
- allowed_domains: prop('array', 'Email domains that can access this folder (required for create)', { items: { type: 'string' } }),
131
+ allowed_domains: prop('array', 'Email domains that can access this folder', { items: { type: 'string' } }),
132
+ allowed_emails: prop('array', 'Individual email addresses that can access this folder', { items: { type: 'string' } }),
130
133
  report_slugs: prop('array', 'Report slugs to add to folder (for add_reports)', { items: { type: 'string' } }),
131
134
  report_slug: prop('string', 'Report slug to remove (for remove_report)'),
132
135
  },
@@ -135,6 +138,216 @@ const toolDefinitions = [
135
138
  schema: manageFoldersSchema,
136
139
  handler: createManageFoldersTool(client),
137
140
  },
141
+ {
142
+ name: 'list_products',
143
+ description: 'List all products in the knowledge repository with their knowledge file inventory. Call this first to discover available products and which knowledge files are already populated.',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ search: prop('string', 'Search term to filter products by name or description'),
148
+ },
149
+ },
150
+ schema: listProductsSchema,
151
+ handler: createListProductsTool(client),
152
+ },
153
+ {
154
+ name: 'get_product',
155
+ description: 'Get a product\'s complete knowledge — returns all JSON config files (meta-ads.json, cpps.json, brand-config.json) fully parsed, plus listings of markdown docs and binary assets. This is the primary tool to read before generating creative work, writing ad copy, or configuring campaigns. For narrative knowledge (brand voice, product positioning, research), use get_product_index to find the right markdown doc, then read_product_file to get its content.\n\nOptionally filter by domain to get only files relevant to a specific area: product-research, brand, copy, advertising, or app-store. Note: JSON config files are domain-scoped (meta-ads.json → advertising, brand-config.json → brand, cpps.json → app-store), so domains without a config file will return an empty knowledge object.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ slug: prop('string', 'Product slug'),
160
+ domain: prop('string', 'Filter files by domain', { enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'] }),
161
+ },
162
+ required: ['slug'],
163
+ },
164
+ schema: getProductSchema,
165
+ handler: createGetProductTool(client),
166
+ },
167
+ {
168
+ name: 'read_product_file',
169
+ description: 'Read a single file from a product by path. Use get_product to read all knowledge files at once (more efficient). Use read_product_file only when you need one specific file — works for markdown docs, JSON configs, and binary files (logos, PDFs returned as base64 data URIs).\n\nTypical workflow: get_product_index → find the file you need → read_product_file.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ slug: prop('string', 'Product slug'),
174
+ filepath: prop('string', 'File path within the product (e.g., "docs/research/user-research.md", "meta-ads.json", "brand-assets/logo.png")'),
175
+ },
176
+ required: ['slug', 'filepath'],
177
+ },
178
+ schema: readProductFileSchema,
179
+ handler: createReadProductFileTool(client),
180
+ },
181
+ {
182
+ name: 'update_product_knowledge',
183
+ description: 'Write or update product knowledge JSON config using deep merge-patch — only the keys you send are changed, all other data is preserved. Use this for programmatic config files that agents plug into API calls.\n\nConfig files per product:\n- meta-ads.json — Meta account IDs, pixels, creative test config\n- cpps.json — CPP motivations and custom product page config\n- brand-config.json — brand color palette hex values, font families\n\nFor narrative knowledge (product info, brand voice, copy, research), create or update Markdown docs using upload_product_files instead. Place docs in the correct domain prefix:\n- docs/research/ — user research, competitive analysis, hypotheses, GTM strategy, roadmap\n- docs/brand/ — brand voice, positioning\n- docs/copy/ — copy guidelines, messaging\n- docs/advertising/ — campaign briefs, ad copy\n- docs/app-store/ — store listing copy, metadata\n\nAlways pass updated_by with your agent name so humans can see who wrote each update.',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ slug: prop('string', 'Product slug'),
188
+ filepath: prop('string', 'Config file to update (e.g., "meta-ads.json", "cpps.json", "brand-config.json")'),
189
+ content: prop('object', 'JSON object to deep merge-patch into the knowledge file. Use null values to delete keys.'),
190
+ updated_by: prop('string', 'Agent name or ID writing the update (optional)'),
191
+ },
192
+ required: ['slug', 'filepath', 'content'],
193
+ },
194
+ schema: updateProductKnowledgeSchema,
195
+ handler: createUpdateProductKnowledgeTool(client),
196
+ },
197
+ {
198
+ name: 'upload_product_files',
199
+ description: 'Upload files to a product — binary assets or Markdown knowledge docs. Files are organized into sections:\n\nBinary assets:\n- brand-assets/ — logos, brand book PDFs, color swatches\n- store-listings/ — App Store / Play Store screenshots and preview videos\n- assets/ — ad creatives, icons, campaign videos\n\nMarkdown docs (use these prefixes for knowledge documents):\n- docs/research/ — user research, competitive analysis, hypotheses, GTM strategy, roadmap\n- docs/brand/ — brand voice, positioning\n- docs/copy/ — copy guidelines, messaging\n- docs/advertising/ — campaign briefs, ad copy\n- docs/app-store/ — store listing copy, metadata',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ slug: prop('string', 'Product slug'),
204
+ files: prop('array', 'Files to upload', {
205
+ items: {
206
+ type: 'object',
207
+ properties: {
208
+ path: { type: 'string', description: 'File path within the product (e.g., "brand-assets/logo.png")' },
209
+ content: { type: 'string', description: 'File content as text, base64 data URI, or local file path' },
210
+ content_type: { type: 'string', description: 'MIME type (auto-detected if not provided)' },
211
+ },
212
+ required: ['path', 'content'],
213
+ },
214
+ }),
215
+ },
216
+ required: ['slug', 'files'],
217
+ },
218
+ schema: uploadProductFilesSchema,
219
+ handler: createUploadProductFilesTool(client),
220
+ },
221
+ {
222
+ name: 'pull_product_files',
223
+ description: 'Download files from a product to a local directory. By default pulls binary assets (logos, screenshots, videos). Set include_knowledge to also pull JSON config files. To pull markdown docs, specify their paths in the files array (e.g., ["docs/research/user-research.md"]).',
224
+ inputSchema: {
225
+ type: 'object',
226
+ properties: {
227
+ slug: prop('string', 'Product slug'),
228
+ output_dir: prop('string', 'Local directory to write files to. Will be created if needed.'),
229
+ files: prop('array', 'Specific file paths to pull. If omitted, all binary files are pulled.', { items: { type: 'string' } }),
230
+ include_knowledge: prop('boolean', 'Include knowledge JSON files in addition to binary files. Default: false.'),
231
+ },
232
+ required: ['slug', 'output_dir'],
233
+ },
234
+ schema: pullProductFilesSchema,
235
+ handler: createPullProductFilesTool(client),
236
+ },
237
+ {
238
+ name: 'get_product_health',
239
+ description: 'Get knowledge completeness scores for a product or all products. Scores both JSON config files (field-level completeness) and expected markdown docs (presence check). Shows missing required/optional fields and missing docs, with an overall health score (0-100). Use to identify gaps before generating content.',
240
+ inputSchema: {
241
+ type: 'object',
242
+ properties: {
243
+ slug: prop('string', 'Product slug. If omitted, returns health summary for all products.'),
244
+ },
245
+ },
246
+ schema: getProductHealthSchema,
247
+ handler: createGetProductHealthTool(client),
248
+ },
249
+ {
250
+ name: 'get_product_index',
251
+ description: 'Get a lightweight file index for a product — a table of contents listing every file with its title, summary, sections, domain, and size. Use this FIRST to discover what knowledge is available before pulling specific files. Much faster than get_product since it returns metadata only, not file contents.\n\nTypical workflow: list_products → get_product_index → read_product_file (for specific docs) or get_product (for all JSON configs at once).\n\nOptionally filter by domain to see only files in a specific area: product-research, brand, copy, advertising, or app-store.',
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {
255
+ slug: prop('string', 'Product slug'),
256
+ domain: prop('string', 'Filter index entries by domain', { enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'] }),
257
+ },
258
+ required: ['slug'],
259
+ },
260
+ schema: getProductIndexSchema,
261
+ handler: createGetProductIndexTool(client),
262
+ },
263
+ {
264
+ name: 'create_product',
265
+ description: 'Create a new product in the knowledge repository. The slug is auto-generated from the name if not provided. Use this before uploading knowledge files or assets.',
266
+ inputSchema: {
267
+ type: 'object',
268
+ properties: {
269
+ name: prop('string', 'Product name'),
270
+ slug: prop('string', 'URL-friendly slug (auto-generated from name if not provided)'),
271
+ description: prop('string', 'Product description'),
272
+ },
273
+ required: ['name'],
274
+ },
275
+ schema: createProductSchema,
276
+ handler: createCreateProductTool(client),
277
+ },
278
+ {
279
+ name: 'delete_product',
280
+ description: 'Delete a product and all its associated knowledge files and assets from the repository. This action is irreversible.',
281
+ inputSchema: {
282
+ type: 'object',
283
+ properties: {
284
+ slug: prop('string', 'Product slug to delete'),
285
+ },
286
+ required: ['slug'],
287
+ },
288
+ schema: deleteProductSchema,
289
+ handler: createDeleteProductTool(client),
290
+ },
291
+ {
292
+ name: 'delete_product_file',
293
+ description: 'Delete a single file from a product. Use for removing outdated assets or resetting a knowledge file.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ slug: prop('string', 'Product slug'),
298
+ filepath: prop('string', 'File path within the product to delete (e.g., "brand-assets/old-logo.png")'),
299
+ },
300
+ required: ['slug', 'filepath'],
301
+ },
302
+ schema: deleteProductFileSchema,
303
+ handler: createDeleteProductFileTool(client),
304
+ },
305
+ {
306
+ name: 'add_to_backlog',
307
+ description: 'Add an unstructured observation to the agent backlog for later processing. Low-friction way for any agent to contribute signals — competitor insights, user feedback, research findings, or any data that should eventually enrich product knowledge. Only content is required; everything else is optional.\n\nIf you know which product this relates to, pass product_slug. Otherwise, omit it — the backlog processor will auto-route it to the right product (or reject it as noise).',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ content: prop('string', 'The observation or data (100-2000 chars typical). Be specific — include sources, numbers, quotes when available.'),
312
+ product_slug: prop('string', 'Product slug if known. Omit for global entries that will be auto-routed.'),
313
+ source: prop('string', 'Your agent name or ID (e.g., "research-agent", "competitor-monitor")'),
314
+ source_context: prop('string', 'Context slug this observation came from, if applicable'),
315
+ domain_hint: prop('string', 'Knowledge domain hint', { enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'] }),
316
+ tags: prop('array', 'Free-form tags for categorization', { items: { type: 'string' } }),
317
+ },
318
+ required: ['content'],
319
+ },
320
+ schema: addToBacklogSchema,
321
+ handler: createAddToBacklogTool(client),
322
+ },
323
+ {
324
+ name: 'list_backlog',
325
+ description: 'List backlog entries — inspect the queue of pending observations waiting to be processed into product knowledge. Filter by product, status, or both.',
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ product_slug: prop('string', 'Filter by product slug. Use "global" for unrouted entries without a product.'),
330
+ status: prop('string', 'Filter by status', { enum: ['pending', 'processing', 'applied', 'rejected', 'partial'] }),
331
+ limit: prop('number', 'Maximum number of results (default: 50)'),
332
+ offset: prop('number', 'Offset for pagination'),
333
+ },
334
+ },
335
+ schema: listBacklogSchema,
336
+ handler: createListBacklogTool(client),
337
+ },
338
+ {
339
+ name: 'process_backlog',
340
+ description: 'Trigger backlog processing — routes global entries to products, then uses LLM to triage and apply changes to product knowledge (JSON configs and markdown docs). Defaults to dry_run=true for safety.\n\nProcessing stages:\n1. Route: Global entries (no product_slug) are matched to products or rejected\n2. Process: Per-product entries are classified and generate JSON patches or markdown operations\n3. Apply: Only high-confidence changes are auto-applied; medium/low are marked as partial with suggestions',
341
+ inputSchema: {
342
+ type: 'object',
343
+ properties: {
344
+ product_slug: prop('string', 'Process entries for a specific product only. Omit to process all products including global routing.'),
345
+ dry_run: prop('boolean', 'Preview changes without applying them. Default: true.'),
346
+ },
347
+ },
348
+ schema: processBacklogSchema,
349
+ handler: createProcessBacklogTool(client),
350
+ },
138
351
  ];
139
352
  const toolHandlers = Object.fromEntries(toolDefinitions.map((t) => [t.name, { schema: t.schema, handler: t.handler }]));
140
353
  const server = new Server({ name: '44reports', version: '1.0.0' }, { capabilities: { tools: {} } });
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import type { ApiClient } from '../api-client.js';
3
+ export declare const addToBacklogSchema: z.ZodObject<{
4
+ content: z.ZodString;
5
+ product_slug: z.ZodOptional<z.ZodString>;
6
+ source: z.ZodOptional<z.ZodString>;
7
+ source_context: z.ZodOptional<z.ZodString>;
8
+ domain_hint: z.ZodOptional<z.ZodEnum<["product-research", "brand", "copy", "advertising", "app-store"]>>;
9
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ content: string;
12
+ product_slug?: string | undefined;
13
+ tags?: string[] | undefined;
14
+ source?: string | undefined;
15
+ source_context?: string | undefined;
16
+ domain_hint?: "product-research" | "brand" | "copy" | "advertising" | "app-store" | undefined;
17
+ }, {
18
+ content: string;
19
+ product_slug?: string | undefined;
20
+ tags?: string[] | undefined;
21
+ source?: string | undefined;
22
+ source_context?: string | undefined;
23
+ domain_hint?: "product-research" | "brand" | "copy" | "advertising" | "app-store" | undefined;
24
+ }>;
25
+ export declare const listBacklogSchema: z.ZodObject<{
26
+ product_slug: z.ZodOptional<z.ZodString>;
27
+ status: z.ZodOptional<z.ZodEnum<["pending", "processing", "applied", "rejected", "partial"]>>;
28
+ limit: z.ZodOptional<z.ZodNumber>;
29
+ offset: z.ZodOptional<z.ZodNumber>;
30
+ }, "strip", z.ZodTypeAny, {
31
+ status?: "pending" | "processing" | "applied" | "rejected" | "partial" | undefined;
32
+ limit?: number | undefined;
33
+ offset?: number | undefined;
34
+ product_slug?: string | undefined;
35
+ }, {
36
+ status?: "pending" | "processing" | "applied" | "rejected" | "partial" | undefined;
37
+ limit?: number | undefined;
38
+ offset?: number | undefined;
39
+ product_slug?: string | undefined;
40
+ }>;
41
+ export declare const processBacklogSchema: z.ZodObject<{
42
+ product_slug: z.ZodOptional<z.ZodString>;
43
+ dry_run: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ dry_run: boolean;
46
+ product_slug?: string | undefined;
47
+ }, {
48
+ product_slug?: string | undefined;
49
+ dry_run?: boolean | undefined;
50
+ }>;
51
+ export declare function createAddToBacklogTool(client: ApiClient): (input: z.infer<typeof addToBacklogSchema>) => Promise<{
52
+ entry: import("../api-client.js").BacklogEntry;
53
+ }>;
54
+ export declare function createListBacklogTool(client: ApiClient): (input: z.infer<typeof listBacklogSchema>) => Promise<{
55
+ entries: import("../api-client.js").BacklogEntry[];
56
+ total: number;
57
+ }>;
58
+ export declare function createProcessBacklogTool(client: ApiClient): (input: z.infer<typeof processBacklogSchema>) => Promise<unknown>;
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ const DOMAIN_VALUES = ['product-research', 'brand', 'copy', 'advertising', 'app-store'];
3
+ const STATUS_VALUES = ['pending', 'processing', 'applied', 'rejected', 'partial'];
4
+ // --- Schemas ---
5
+ export const addToBacklogSchema = z.object({
6
+ content: z.string().describe('The observation or data to add to the backlog'),
7
+ product_slug: z.string().optional().describe('Product slug — omit for global entries that will be auto-routed'),
8
+ source: z.string().optional().describe('Agent name or ID submitting this entry'),
9
+ source_context: z.string().optional().describe('Context slug this observation came from'),
10
+ domain_hint: z.enum(DOMAIN_VALUES).optional().describe('Knowledge domain hint'),
11
+ tags: z.array(z.string()).optional().describe('Free-form tags for categorization'),
12
+ });
13
+ export const listBacklogSchema = z.object({
14
+ product_slug: z.string().optional().describe('Filter by product slug. Use "global" for unrouted entries.'),
15
+ status: z.enum(STATUS_VALUES).optional().describe('Filter by status'),
16
+ limit: z.number().optional().describe('Maximum number of results (default: 50)'),
17
+ offset: z.number().optional().describe('Offset for pagination'),
18
+ });
19
+ export const processBacklogSchema = z.object({
20
+ product_slug: z.string().optional().describe('Process entries for a specific product. Omit to process all including global routing.'),
21
+ dry_run: z.boolean().optional().default(true).describe('Preview changes without applying. Default: true.'),
22
+ });
23
+ // --- Tool factories ---
24
+ export function createAddToBacklogTool(client) {
25
+ return async (input) => {
26
+ return client.addBacklogEntry(input);
27
+ };
28
+ }
29
+ export function createListBacklogTool(client) {
30
+ return async (input) => {
31
+ return client.listBacklog({
32
+ product_slug: input.product_slug,
33
+ status: input.status,
34
+ limit: input.limit,
35
+ offset: input.offset,
36
+ });
37
+ };
38
+ }
39
+ export function createProcessBacklogTool(client) {
40
+ return async (input) => {
41
+ return client.processBacklog({
42
+ product_slug: input.product_slug,
43
+ dry_run: input.dry_run,
44
+ });
45
+ };
46
+ }
@@ -0,0 +1 @@
1
+ export {};