44reports-mcp 1.1.0 → 1.3.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.
@@ -56,6 +56,22 @@ export interface Folder {
56
56
  created_at: string;
57
57
  updated_at: string;
58
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;
72
+ created_at: string;
73
+ updated_at: string;
74
+ }
59
75
  export declare class ApiClient {
60
76
  private readonly config;
61
77
  constructor(config: Config);
@@ -120,17 +136,22 @@ export declare class ApiClient {
120
136
  removeReportFromFolder(folderSlug: string, reportSlug: string): Promise<{
121
137
  success: boolean;
122
138
  }>;
123
- listProducts(): Promise<{
139
+ listProducts(options?: {
140
+ search?: string;
141
+ limit?: number;
142
+ offset?: number;
143
+ }): Promise<{
124
144
  products: Array<{
125
145
  id: string;
126
146
  slug: string;
127
147
  name: string;
128
148
  description: string | null;
129
- json_file_count: number;
130
- binary_file_count: number;
149
+ created_at: string;
150
+ updated_at: string;
131
151
  }>;
152
+ total: number;
132
153
  }>;
133
- getProduct(slug: string): Promise<{
154
+ getProduct(slug: string, domain?: string): Promise<{
134
155
  product: {
135
156
  id: string;
136
157
  slug: string;
@@ -140,10 +161,16 @@ export declare class ApiClient {
140
161
  updated_at: string;
141
162
  };
142
163
  knowledge: Record<string, unknown>;
164
+ doc_files: Array<{
165
+ path: string;
166
+ size: number;
167
+ domain: string;
168
+ }>;
143
169
  binary_files: Array<{
144
170
  path: string;
145
171
  size: number;
146
172
  content_type: string;
173
+ domain: string;
147
174
  }>;
148
175
  }>;
149
176
  getProductFile(slug: string, filepath: string): Promise<{
@@ -168,4 +195,88 @@ export declare class ApiClient {
168
195
  success: boolean;
169
196
  uploaded: string[];
170
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>;
171
282
  }
@@ -16,7 +16,11 @@ export class ApiClient {
16
16
  });
17
17
  if (!response.ok) {
18
18
  const error = await response.json().catch(() => ({ error: response.statusText }));
19
- throw new Error(error.error || `HTTP ${response.status}`);
19
+ const message = error.error || `HTTP ${response.status}`;
20
+ if (response.status === 401) {
21
+ throw new Error(`${message}. Reporter token rejected. Generate or rotate yours at ${this.config.apiUrl.replace('reporter.44pixels.workers.dev', 'reporter-directory.pages.dev')} → Settings, then update REPORTER_API_KEY in your Claude config and restart Claude Code.`);
22
+ }
23
+ throw new Error(message);
20
24
  }
21
25
  return response.json();
22
26
  }
@@ -91,11 +95,19 @@ export class ApiClient {
91
95
  });
92
96
  }
93
97
  // --- Product Knowledge ---
94
- async listProducts() {
95
- return this.request('/api/products');
98
+ async listProducts(options) {
99
+ const params = new URLSearchParams();
100
+ for (const [key, value] of Object.entries(options ?? {})) {
101
+ if (value !== undefined) {
102
+ params.set(key, String(value));
103
+ }
104
+ }
105
+ const query = params.toString();
106
+ return this.request(`/api/products${query ? `?${query}` : ''}`);
96
107
  }
97
- async getProduct(slug) {
98
- return this.request(`/api/products/${slug}`);
108
+ async getProduct(slug, domain) {
109
+ const query = domain ? `?domain=${encodeURIComponent(domain)}` : '';
110
+ return this.request(`/api/products/${slug}${query}`);
99
111
  }
100
112
  async getProductFile(slug, filepath) {
101
113
  return this.request(`/api/products/${slug}/files/${filepath}`);
@@ -112,4 +124,69 @@ export class ApiClient {
112
124
  body: JSON.stringify({ files }),
113
125
  });
114
126
  }
127
+ async getProductHealth(slug) {
128
+ return this.request(`/api/products/${slug}/health`);
129
+ }
130
+ async getAllProductsHealth() {
131
+ return this.request('/api/products/health');
132
+ }
133
+ async createProduct(data) {
134
+ return this.request('/api/products', {
135
+ method: 'POST',
136
+ body: JSON.stringify(data),
137
+ });
138
+ }
139
+ async deleteProduct(slug) {
140
+ return this.request(`/api/products/${slug}`, {
141
+ method: 'DELETE',
142
+ });
143
+ }
144
+ async deleteProductFile(slug, filepath) {
145
+ return this.request(`/api/products/${slug}/files/${filepath}`, {
146
+ method: 'DELETE',
147
+ });
148
+ }
149
+ async getProductIndex(slug, domain) {
150
+ const query = domain ? `?domain=${encodeURIComponent(domain)}` : '';
151
+ return this.request(`/api/products/${slug}/index${query}`);
152
+ }
153
+ async pullProductFiles(slug, options) {
154
+ return this.request(`/api/products/${slug}/pull`, {
155
+ method: 'POST',
156
+ body: JSON.stringify(options ?? {}),
157
+ });
158
+ }
159
+ // --- Agent Backlog ---
160
+ async addBacklogEntry(data) {
161
+ return this.request('/api/backlog', {
162
+ method: 'POST',
163
+ body: JSON.stringify(data),
164
+ });
165
+ }
166
+ async listBacklog(options) {
167
+ const params = new URLSearchParams();
168
+ for (const [key, value] of Object.entries(options ?? {})) {
169
+ if (value !== undefined) {
170
+ params.set(key, String(value));
171
+ }
172
+ }
173
+ const query = params.toString();
174
+ return this.request(`/api/backlog${query ? `?${query}` : ''}`);
175
+ }
176
+ async deleteBacklogEntry(id) {
177
+ return this.request(`/api/backlog/${id}`, {
178
+ method: 'DELETE',
179
+ });
180
+ }
181
+ async processBacklog(options) {
182
+ const params = new URLSearchParams();
183
+ if (options?.product_slug)
184
+ params.set('product_slug', options.product_slug);
185
+ if (options?.dry_run !== undefined)
186
+ params.set('dry_run', String(options.dry_run));
187
+ const query = params.toString();
188
+ return this.request(`/api/backlog/process${query ? `?${query}` : ''}`, {
189
+ method: 'POST',
190
+ });
191
+ }
115
192
  }
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ 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, createListProductsTool, createGetProductTool, createReadProductFileTool, createUpdateProductKnowledgeTool, createUploadProductFilesTool, createPullProductFilesTool, } from './tools/products.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 { createUploadProductDirectoryTool, uploadProductDirectorySchema, UPLOAD_DIRECTORY_DESCRIPTION, } from './tools/upload-directory.js';
13
+ import { addToBacklogSchema, listBacklogSchema, processBacklogSchema, createAddToBacklogTool, createListBacklogTool, createProcessBacklogTool, } from './tools/backlog.js';
12
14
  const config = loadConfig();
13
15
  const client = new ApiClient(config);
14
16
  function prop(type, description, extra = {}) {
@@ -139,18 +141,24 @@ const toolDefinitions = [
139
141
  },
140
142
  {
141
143
  name: 'list_products',
142
- description: 'List all 44pixels products in the knowledge repository (Cue, Deep, Pixi, Clara, Vivi, Wordcast, etc.) with their knowledge file inventory. Call this first to discover available products and which knowledge files are already populated.',
143
- inputSchema: { type: 'object', properties: {} },
144
+ 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.',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ search: prop('string', 'Search term to filter products by name or description'),
149
+ },
150
+ },
144
151
  schema: listProductsSchema,
145
152
  handler: createListProductsTool(client),
146
153
  },
147
154
  {
148
155
  name: 'get_product',
149
- description: 'Get a product\'s complete knowledge — returns ALL JSON knowledge files (product.json, brand.json, copy.json, meta-ads.json, cpps.json) fully parsed, plus a listing of binary assets. This is the primary tool to read before generating creative work, writing ad copy, or configuring campaigns it gives you brand voice, product positioning, hooks, and ad account IDs in one call.',
156
+ 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.',
150
157
  inputSchema: {
151
158
  type: 'object',
152
159
  properties: {
153
160
  slug: prop('string', 'Product slug'),
161
+ domain: prop('string', 'Filter files by domain', { enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'] }),
154
162
  },
155
163
  required: ['slug'],
156
164
  },
@@ -159,12 +167,12 @@ const toolDefinitions = [
159
167
  },
160
168
  {
161
169
  name: 'read_product_file',
162
- 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 — especially useful for binary files (logos, PDFs) returned as base64 data URIs.',
170
+ 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.',
163
171
  inputSchema: {
164
172
  type: 'object',
165
173
  properties: {
166
174
  slug: prop('string', 'Product slug'),
167
- filepath: prop('string', 'File path within the product (e.g., "meta-ads.json", "brand-assets/logo.png")'),
175
+ filepath: prop('string', 'File path within the product (e.g., "docs/research/user-research.md", "meta-ads.json", "brand-assets/logo.png")'),
168
176
  },
169
177
  required: ['slug', 'filepath'],
170
178
  },
@@ -173,12 +181,12 @@ const toolDefinitions = [
173
181
  },
174
182
  {
175
183
  name: 'update_product_knowledge',
176
- description: 'Write or update product knowledge using deep merge-patch — only the keys you send are changed, all other data is preserved. This is the canonical way to store brand knowledge: ALWAYS use this (not deploy_context) for product profiles, brand guidelines, copy, ad accounts, and CPP config.\n\nKnowledge files per product:\n- product.json — core info, target audience, pain points, selling points, competitors\n- brand.json — colors, typography, voice/tone, brand archetypes, power words\n- copy.jsontaglines, hooks, CTAs, approved/banned claims\n- meta-ads.jsonMeta account IDs, pixels, creative test config\n- cpps.jsonCPP motivations and custom product page config\n\nAlways pass updated_by with your agent name so humans can see who wrote each update.',
184
+ 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.',
177
185
  inputSchema: {
178
186
  type: 'object',
179
187
  properties: {
180
188
  slug: prop('string', 'Product slug'),
181
- filepath: prop('string', 'Knowledge file to update (e.g., "meta-ads.json", "brand.json", "product.json", "copy.json", "cpps.json")'),
189
+ filepath: prop('string', 'Config file to update (e.g., "meta-ads.json", "cpps.json", "brand-config.json")'),
182
190
  content: prop('object', 'JSON object to deep merge-patch into the knowledge file. Use null values to delete keys.'),
183
191
  updated_by: prop('string', 'Agent name or ID writing the update (optional)'),
184
192
  },
@@ -189,7 +197,12 @@ const toolDefinitions = [
189
197
  },
190
198
  {
191
199
  name: 'upload_product_files',
192
- description: 'Upload binary assets to a product — logos, brand books, App Store screenshots, ad creatives, videos. Files are organized into sections:\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',
200
+ description: 'Upload one or more files to a product. For multiple files in a directory, prefer ' +
201
+ 'upload_product_directory.\n\n' +
202
+ 'Place files at media/<domain>/ for binaries, docs/<domain>/ for markdown:\n' +
203
+ ' media/brand/, media/product-research/, media/copy/, media/advertising/, media/app-store/\n' +
204
+ ' docs/brand/, docs/research/, docs/copy/, docs/advertising/, docs/app-store/\n\n' +
205
+ 'Pass content as a LOCAL PATH (preferred), a base64 data URI, or plain text.',
193
206
  inputSchema: {
194
207
  type: 'object',
195
208
  properties: {
@@ -211,21 +224,161 @@ const toolDefinitions = [
211
224
  schema: uploadProductFilesSchema,
212
225
  handler: createUploadProductFilesTool(client),
213
226
  },
227
+ {
228
+ name: 'upload_product_directory',
229
+ description: UPLOAD_DIRECTORY_DESCRIPTION,
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ slug: prop('string', 'Product slug'),
234
+ local_dir: prop('string', 'Absolute or relative path to the local directory to upload'),
235
+ domain: {
236
+ type: 'string',
237
+ enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'],
238
+ description: 'Product domain — determines target folder (media/<domain>/)',
239
+ },
240
+ dest_prefix: prop('string', 'Subpath under media/<domain>/. Defaults to basename of local_dir.'),
241
+ glob: prop('string', 'Include filter, e.g. "**/*.{png,jpg,mp4}". Default: "**/*".'),
242
+ exclude: prop('array', 'Exclude patterns. Default: [".*", "**/.*", "node_modules/**"].', {
243
+ items: { type: 'string' },
244
+ }),
245
+ dry_run: prop('boolean', 'If true, returns the planned uploads without sending. Default: false.'),
246
+ },
247
+ required: ['slug', 'local_dir', 'domain'],
248
+ },
249
+ schema: uploadProductDirectorySchema,
250
+ handler: createUploadProductDirectoryTool(client),
251
+ },
214
252
  {
215
253
  name: 'pull_product_files',
216
- description: 'Download binary files from a product to a local directory.',
254
+ 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"]).',
217
255
  inputSchema: {
218
256
  type: 'object',
219
257
  properties: {
220
258
  slug: prop('string', 'Product slug'),
221
259
  output_dir: prop('string', 'Local directory to write files to. Will be created if needed.'),
222
260
  files: prop('array', 'Specific file paths to pull. If omitted, all binary files are pulled.', { items: { type: 'string' } }),
261
+ include_knowledge: prop('boolean', 'Include knowledge JSON files in addition to binary files. Default: false.'),
223
262
  },
224
263
  required: ['slug', 'output_dir'],
225
264
  },
226
265
  schema: pullProductFilesSchema,
227
266
  handler: createPullProductFilesTool(client),
228
267
  },
268
+ {
269
+ name: 'get_product_health',
270
+ 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.',
271
+ inputSchema: {
272
+ type: 'object',
273
+ properties: {
274
+ slug: prop('string', 'Product slug. If omitted, returns health summary for all products.'),
275
+ },
276
+ },
277
+ schema: getProductHealthSchema,
278
+ handler: createGetProductHealthTool(client),
279
+ },
280
+ {
281
+ name: 'get_product_index',
282
+ 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.',
283
+ inputSchema: {
284
+ type: 'object',
285
+ properties: {
286
+ slug: prop('string', 'Product slug'),
287
+ domain: prop('string', 'Filter index entries by domain', { enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'] }),
288
+ },
289
+ required: ['slug'],
290
+ },
291
+ schema: getProductIndexSchema,
292
+ handler: createGetProductIndexTool(client),
293
+ },
294
+ {
295
+ name: 'create_product',
296
+ 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.',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ name: prop('string', 'Product name'),
301
+ slug: prop('string', 'URL-friendly slug (auto-generated from name if not provided)'),
302
+ description: prop('string', 'Product description'),
303
+ },
304
+ required: ['name'],
305
+ },
306
+ schema: createProductSchema,
307
+ handler: createCreateProductTool(client),
308
+ },
309
+ {
310
+ name: 'delete_product',
311
+ description: 'Delete a product and all its associated knowledge files and assets from the repository. This action is irreversible.',
312
+ inputSchema: {
313
+ type: 'object',
314
+ properties: {
315
+ slug: prop('string', 'Product slug to delete'),
316
+ },
317
+ required: ['slug'],
318
+ },
319
+ schema: deleteProductSchema,
320
+ handler: createDeleteProductTool(client),
321
+ },
322
+ {
323
+ name: 'delete_product_file',
324
+ description: 'Delete a single file from a product. Use for removing outdated assets or resetting a knowledge file.',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: {
328
+ slug: prop('string', 'Product slug'),
329
+ filepath: prop('string', 'File path within the product to delete (e.g., "brand-assets/old-logo.png")'),
330
+ },
331
+ required: ['slug', 'filepath'],
332
+ },
333
+ schema: deleteProductFileSchema,
334
+ handler: createDeleteProductFileTool(client),
335
+ },
336
+ {
337
+ name: 'add_to_backlog',
338
+ 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).',
339
+ inputSchema: {
340
+ type: 'object',
341
+ properties: {
342
+ content: prop('string', 'The observation or data (100-2000 chars typical). Be specific — include sources, numbers, quotes when available.'),
343
+ product_slug: prop('string', 'Product slug if known. Omit for global entries that will be auto-routed.'),
344
+ source: prop('string', 'Your agent name or ID (e.g., "research-agent", "competitor-monitor")'),
345
+ source_context: prop('string', 'Context slug this observation came from, if applicable'),
346
+ domain_hint: prop('string', 'Knowledge domain hint', { enum: ['product-research', 'brand', 'copy', 'advertising', 'app-store'] }),
347
+ tags: prop('array', 'Free-form tags for categorization', { items: { type: 'string' } }),
348
+ },
349
+ required: ['content'],
350
+ },
351
+ schema: addToBacklogSchema,
352
+ handler: createAddToBacklogTool(client),
353
+ },
354
+ {
355
+ name: 'list_backlog',
356
+ description: 'List backlog entries — inspect the queue of pending observations waiting to be processed into product knowledge. Filter by product, status, or both.',
357
+ inputSchema: {
358
+ type: 'object',
359
+ properties: {
360
+ product_slug: prop('string', 'Filter by product slug. Use "global" for unrouted entries without a product.'),
361
+ status: prop('string', 'Filter by status', { enum: ['pending', 'processing', 'applied', 'rejected', 'partial'] }),
362
+ limit: prop('number', 'Maximum number of results (default: 50)'),
363
+ offset: prop('number', 'Offset for pagination'),
364
+ },
365
+ },
366
+ schema: listBacklogSchema,
367
+ handler: createListBacklogTool(client),
368
+ },
369
+ {
370
+ name: 'process_backlog',
371
+ 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',
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ product_slug: prop('string', 'Process entries for a specific product only. Omit to process all products including global routing.'),
376
+ dry_run: prop('boolean', 'Preview changes without applying them. Default: true.'),
377
+ },
378
+ },
379
+ schema: processBacklogSchema,
380
+ handler: createProcessBacklogTool(client),
381
+ },
229
382
  ];
230
383
  const toolHandlers = Object.fromEntries(toolDefinitions.map((t) => [t.name, { schema: t.schema, handler: t.handler }]));
231
384
  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 {};