gtm-now 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10713,8 +10713,8 @@ var init_prospect = __esm({
10713
10713
  },
10714
10714
  output: {
10715
10715
  type: "string",
10716
- enum: ["file", "inline"],
10717
- description: 'Output mode. "file" writes results to JSONL file (auto when >20 results). "inline" returns in response.'
10716
+ enum: ["file", "inline", "csv"],
10717
+ description: 'Output mode. "file" writes JSONL (auto when >20 results). "csv" writes a CSV file for spreadsheet review. "inline" returns in response.'
10718
10718
  },
10719
10719
  detail: {
10720
10720
  type: "string",
@@ -10825,8 +10825,8 @@ var init_prospect = __esm({
10825
10825
  },
10826
10826
  output: {
10827
10827
  type: "string",
10828
- enum: ["file", "inline"],
10829
- description: 'Output mode. "file" writes results to JSONL file (auto when >20 results). "inline" returns in response.'
10828
+ enum: ["file", "inline", "csv"],
10829
+ description: 'Output mode. "file" writes JSONL (auto when >20 results). "csv" writes a CSV file for spreadsheet review. "inline" returns in response.'
10830
10830
  },
10831
10831
  detail: {
10832
10832
  type: "string",
@@ -10849,7 +10849,7 @@ var init_enrich = __esm({
10849
10849
  enrichTools = [
10850
10850
  {
10851
10851
  name: "gtm_enrich",
10852
- description: "Email/phone enrichment via waterfall \u2014 tries providers in order, stops on first match. Always dry_run first for batches > 5.",
10852
+ description: 'Email enrichment via waterfall \u2014 tries providers in order, stops on first match. Always dry_run first for batches > 5. Defaults to email only. Phone costs extra \u2014 only add find: ["email", "phone"] when the user explicitly requests phone numbers.',
10853
10853
  inputSchema: {
10854
10854
  type: "object",
10855
10855
  properties: {
@@ -10876,7 +10876,7 @@ var init_enrich = __esm({
10876
10876
  enum: ["email", "phone", "title", "company", "funding", "tech_stack"]
10877
10877
  },
10878
10878
  default: ["email"],
10879
- description: "Fields to find. Currently supported: email, phone. Additional fields (title, company, funding, tech_stack) are accepted but return an informative error until Phase 2."
10879
+ description: 'Fields to find. Defaults to ["email"] only. Add "phone" ONLY when explicitly requested \u2014 phone lookups cost extra. Additional fields (title, company, funding, tech_stack) return an informative error until Phase 2.'
10880
10880
  },
10881
10881
  waterfall: {
10882
10882
  type: "array",
@@ -11307,6 +11307,43 @@ var init_feedback = __esm({
11307
11307
  }
11308
11308
  });
11309
11309
 
11310
+ // src/tools/review.ts
11311
+ var reviewTools;
11312
+ var init_review = __esm({
11313
+ "src/tools/review.ts"() {
11314
+ "use strict";
11315
+ reviewTools = [
11316
+ {
11317
+ name: "gtm_review",
11318
+ description: "Push data to a visual review table. Users see an interactive grid in their browser and can select/deselect rows. Use after finding companies or contacts to let the user review before enrichment.",
11319
+ inputSchema: {
11320
+ type: "object",
11321
+ properties: {
11322
+ action: {
11323
+ type: "string",
11324
+ enum: ["create", "update", "get_selected"],
11325
+ description: "create: new table from data. update: modify existing table rows. get_selected: fetch user-approved rows."
11326
+ },
11327
+ title: { type: "string", description: "Table title (for create)" },
11328
+ data: {
11329
+ type: "array",
11330
+ items: { type: "object" },
11331
+ description: "Array of row objects (for create)"
11332
+ },
11333
+ table_id: { type: "string", description: "Table ID (for update, get_selected)" },
11334
+ rows: {
11335
+ type: "array",
11336
+ items: { type: "object" },
11337
+ description: "Updated row objects with _row_id (for update)"
11338
+ }
11339
+ },
11340
+ required: ["action"]
11341
+ }
11342
+ }
11343
+ ];
11344
+ }
11345
+ });
11346
+
11310
11347
  // src/tools/descriptions.ts
11311
11348
  var TOOL_DESCRIPTIONS;
11312
11349
  var init_descriptions = __esm({
@@ -11347,7 +11384,9 @@ After finding people, use:
11347
11384
 
11348
11385
  Known limitations:
11349
11386
  - department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other.`,
11350
- gtm_enrich: `Email and phone enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.
11387
+ gtm_enrich: `Email enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.
11388
+
11389
+ IMPORTANT: Defaults to email only. Phone enrichment costs extra per lookup \u2014 only include when the user explicitly asks for phone numbers.
11351
11390
 
11352
11391
  Always do a dry_run first for batches > 5 to preview cost.
11353
11392
 
@@ -11388,7 +11427,8 @@ Note: HarvestAPI charges per request. For engagement mining, each post lookup co
11388
11427
  gtm_guide: "CALL THIS FIRST before any multi-step GTM task. Returns step-by-step workflow recipes for common tasks like building a TAM list, running enrichment, or launching cold outreach. Always check the guide before starting work to ensure you follow the correct sequence and avoid wasting credits.",
11389
11428
  gtm_tool_help: "Get the full parameter schema for a specific GTM tool. Use this to see exactly what arguments a tool accepts before calling gtm_execute.",
11390
11429
  gtm_execute: "Gateway to execute any GTM tool by name. Use gtm_guide for workflow recipes, gtm_tool_help for parameter schemas, then this to execute.",
11391
- gtm_feedback: "Submit feedback, bug reports, feature requests, or provider requests. Feedback is stored for the gtm-now maintainers to review."
11430
+ gtm_feedback: "Submit feedback, bug reports, feature requests, or provider requests. Feedback is stored for the gtm-now maintainers to review.",
11431
+ gtm_review: "Push data to a visual review table hosted at view.gtmnow.com. Users see an interactive AG Grid in their browser with checkboxes to select/deselect rows. Creates a temporary table (24h TTL) with a shareable URL. Use after finding companies or contacts to let the user review before spending enrichment credits. Three actions: create (new table from data array), update (modify existing table \u2014 add columns, update rows), get_selected (fetch only the rows the user has checked)."
11392
11432
  };
11393
11433
  }
11394
11434
  });
@@ -11408,6 +11448,7 @@ var init_tools = __esm({
11408
11448
  init_status();
11409
11449
  init_configure();
11410
11450
  init_feedback();
11451
+ init_review();
11411
11452
  init_prospect();
11412
11453
  init_enrich();
11413
11454
  init_extract();
@@ -11417,6 +11458,7 @@ var init_tools = __esm({
11417
11458
  init_crm();
11418
11459
  init_status();
11419
11460
  init_configure();
11461
+ init_review();
11420
11462
  init_descriptions();
11421
11463
  capabilityTools = [
11422
11464
  ...prospectTools,
@@ -11428,7 +11470,8 @@ var init_tools = __esm({
11428
11470
  ...crmTools,
11429
11471
  ...statusTools,
11430
11472
  ...configureTools,
11431
- ...feedbackTools
11473
+ ...feedbackTools,
11474
+ ...reviewTools
11432
11475
  ];
11433
11476
  guideTool = {
11434
11477
  name: "gtm_guide",
@@ -11444,6 +11487,7 @@ var init_tools = __esm({
11444
11487
  "cold_outreach",
11445
11488
  "linkedin_outreach",
11446
11489
  "crm_sync",
11490
+ "export_to_webhook",
11447
11491
  "list_tasks"
11448
11492
  ],
11449
11493
  description: "The workflow to get guidance for"
@@ -11536,6 +11580,12 @@ Start by identifying what kind of companies you're targeting:
11536
11580
  3. gtm_enrich \u2014 get emails
11537
11581
  4. gtm_campaign \u2014 personalized outreach referencing their engagement
11538
11582
 
11583
+ ## Review before enriching
11584
+ After finding companies, review the list before spending enrichment credits:
11585
+ - Use output: 'csv' to write results to a spreadsheet for visual review
11586
+ - Or export to Clay/webhook via gtm_fetch POST for team review (see export_to_webhook guide)
11587
+ - Use gtm_ai with action: 'classify' to auto-filter competitors or bad fits
11588
+
11539
11589
  ## Key principle
11540
11590
  Always dry_run enrichment first. Credits are spent per successful find \u2014 previewing cost avoids surprises.`,
11541
11591
  enrich_list: `# Enrich a Contact List \u2014 Workflow
@@ -11611,6 +11661,26 @@ LinkedIn outreach requires LinkedIn profile URLs \u2014 not emails. Extract prof
11611
11661
 
11612
11662
  ## Key principle
11613
11663
  Always search before creating to avoid duplicates. Most CRMs have built-in email dedup but better to check first.`,
11664
+ export_to_webhook: `# Export Data to Webhook \u2014 Workflow
11665
+
11666
+ Push GTM data to Clay, Make, Zapier, or any webhook endpoint for visual review or further processing.
11667
+
11668
+ ## Steps
11669
+ 1. FIND \u2014 Get your companies or contacts
11670
+ \u2192 gtm_find_companies({ icp_text: "...", limit: 50 })
11671
+ \u2192 gtm_find_people({ company_domain: "...", job_title: "CEO" })
11672
+
11673
+ 2. EXPORT \u2014 Push to webhook via gtm_fetch
11674
+ \u2192 gtm_fetch({ url: "https://your-webhook-url", method: "POST", body: { results: [...] } })
11675
+
11676
+ ## Clay Integration
11677
+ Clay table webhooks accept JSON directly:
11678
+ \u2192 gtm_fetch({ url: "https://api.clay.com/v3/sources/webhook/...", method: "POST", body: { first_name: "...", last_name: "...", company: "...", ... } })
11679
+
11680
+ Send one record per request, or batch them depending on the webhook's format.
11681
+
11682
+ ## Key principle
11683
+ Review data visually before spending enrichment credits. Export \u2192 review \u2192 enrich only the good leads.`,
11614
11684
  list_tasks: `# Available GTM Workflows
11615
11685
 
11616
11686
  Call gtm_guide with one of these tasks:
@@ -11620,6 +11690,7 @@ Call gtm_guide with one of these tasks:
11620
11690
  - cold_outreach \u2014 Enrich + personalize + push to cold email campaign
11621
11691
  - linkedin_outreach \u2014 Extract profiles + personalize + push to LinkedIn campaign
11622
11692
  - crm_sync \u2014 Find/create/update people and deals in CRM
11693
+ - export_to_webhook \u2014 Push data to Clay, Make, Zapier, or any webhook for review
11623
11694
 
11624
11695
  New tools available:
11625
11696
  - gtm_find_companies (provider: storeleads) \u2014 Search 1.2M+ e-commerce stores by category, revenue, platform
@@ -11703,6 +11774,7 @@ import * as os from "os";
11703
11774
  function shouldWriteToFile(count, output) {
11704
11775
  if (output === "file") return true;
11705
11776
  if (output === "inline") return false;
11777
+ if (output === "csv") return false;
11706
11778
  return count > FILE_OUTPUT_THRESHOLD;
11707
11779
  }
11708
11780
  function writeResultsFile(tool, results, meta, resultsDir = DEFAULT_RESULTS_DIR) {
@@ -11720,6 +11792,40 @@ function writeResultsFile(tool, results, meta, resultsDir = DEFAULT_RESULTS_DIR)
11720
11792
  }
11721
11793
  return filePath;
11722
11794
  }
11795
+ function writeResultsCsv(tool, results, resultsDir = DEFAULT_RESULTS_DIR) {
11796
+ fs.mkdirSync(resultsDir, { recursive: true });
11797
+ const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
11798
+ const filename = `${tool}-${timestamp}.csv`;
11799
+ const filePath = path.join(resultsDir, filename);
11800
+ if (results.length === 0) {
11801
+ fs.writeFileSync(filePath, "", "utf-8");
11802
+ return filePath;
11803
+ }
11804
+ const headers = Object.keys(results[0]);
11805
+ const headerLine = headers.map(escapeCsvField).join(",");
11806
+ const dataLines = results.map(
11807
+ (row) => headers.map((h) => escapeCsvField(formatCsvValue(row[h]))).join(",")
11808
+ );
11809
+ const content = [headerLine, ...dataLines].join("\n") + "\n";
11810
+ fs.writeFileSync(filePath, content, "utf-8");
11811
+ try {
11812
+ cleanOldResults(7, resultsDir);
11813
+ } catch {
11814
+ }
11815
+ return filePath;
11816
+ }
11817
+ function formatCsvValue(value) {
11818
+ if (value === null || value === void 0) return "";
11819
+ if (Array.isArray(value)) return value.join(";");
11820
+ if (typeof value === "object") return JSON.stringify(value);
11821
+ return String(value);
11822
+ }
11823
+ function escapeCsvField(value) {
11824
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
11825
+ return `"${value.replace(/"/g, '""')}"`;
11826
+ }
11827
+ return value;
11828
+ }
11723
11829
  function cleanOldResults(maxAgeDays, resultsDir = DEFAULT_RESULTS_DIR) {
11724
11830
  let entries;
11725
11831
  try {
@@ -11843,6 +11949,19 @@ async function findCompanies(args, ctx) {
11843
11949
  if (query.max_revenue != null) applied_filters.max_revenue = query.max_revenue;
11844
11950
  const providerUsed = provider.name;
11845
11951
  const output = args.output;
11952
+ if (output === "csv") {
11953
+ const filePath = writeResultsCsv("find_companies", slimmedResults);
11954
+ return {
11955
+ provider: providerUsed,
11956
+ count: results.length,
11957
+ has_more,
11958
+ output: "csv",
11959
+ file: filePath,
11960
+ preview: slimmedResults.slice(0, 3),
11961
+ columns: slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [],
11962
+ ...warnings?.length ? { warnings } : {}
11963
+ };
11964
+ }
11846
11965
  if (shouldWriteToFile(results.length, output)) {
11847
11966
  const meta = {
11848
11967
  query: applied_filters,
@@ -11936,6 +12055,19 @@ async function findPeople(args, ctx) {
11936
12055
  if (query.revenue_range) applied_filters.revenue_range = query.revenue_range;
11937
12056
  const providerUsed = provider.name;
11938
12057
  const output = args.output;
12058
+ if (output === "csv") {
12059
+ const filePath = writeResultsCsv("find_people", slimmedResults);
12060
+ return {
12061
+ provider: providerUsed,
12062
+ count: dedupedResults.length,
12063
+ has_more,
12064
+ output: "csv",
12065
+ file: filePath,
12066
+ preview: slimmedResults.slice(0, 3),
12067
+ columns: slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [],
12068
+ ...warnings?.length ? { warnings } : {}
12069
+ };
12070
+ }
11939
12071
  if (shouldWriteToFile(dedupedResults.length, output)) {
11940
12072
  const meta = {
11941
12073
  query: applied_filters,
@@ -13111,6 +13243,73 @@ var init_feedback2 = __esm({
13111
13243
  }
13112
13244
  });
13113
13245
 
13246
+ // src/handlers/review.ts
13247
+ var review_exports = {};
13248
+ __export(review_exports, {
13249
+ handleReview: () => handleReview
13250
+ });
13251
+ async function handleReview(args) {
13252
+ const action = args.action;
13253
+ switch (action) {
13254
+ case "create":
13255
+ return createTable(args);
13256
+ case "update":
13257
+ return updateTable(args);
13258
+ case "get_selected":
13259
+ return getSelected(args);
13260
+ default:
13261
+ throw new Error(`Unknown review action: ${action}`);
13262
+ }
13263
+ }
13264
+ async function createTable(args) {
13265
+ const data = args.data;
13266
+ const title = args.title ?? "Review Table";
13267
+ const rows = data.map((row, i) => ({
13268
+ ...row,
13269
+ _row_id: row._row_id ?? `r${i + 1}`
13270
+ }));
13271
+ const response = await fetch(`${VIEWER_URL}/api/tables`, {
13272
+ method: "POST",
13273
+ headers: { "Content-Type": "application/json" },
13274
+ body: JSON.stringify({ title, rows })
13275
+ });
13276
+ if (!response.ok) {
13277
+ const body = await response.json().catch(() => ({}));
13278
+ throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
13279
+ }
13280
+ return response.json();
13281
+ }
13282
+ async function updateTable(args) {
13283
+ const tableId = args.table_id;
13284
+ const rows = args.rows;
13285
+ const response = await fetch(`${VIEWER_URL}/api/tables/${tableId}`, {
13286
+ method: "PATCH",
13287
+ headers: { "Content-Type": "application/json" },
13288
+ body: JSON.stringify({ rows })
13289
+ });
13290
+ if (!response.ok) {
13291
+ const body = await response.json().catch(() => ({}));
13292
+ throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
13293
+ }
13294
+ return response.json();
13295
+ }
13296
+ async function getSelected(args) {
13297
+ const tableId = args.table_id;
13298
+ const response = await fetch(`${VIEWER_URL}/api/tables/${tableId}/selected`);
13299
+ if (!response.ok) {
13300
+ const body = await response.json().catch(() => ({}));
13301
+ throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
13302
+ }
13303
+ return response.json();
13304
+ }
13305
+ var VIEWER_URL;
13306
+ var init_review2 = __esm({
13307
+ "src/handlers/review.ts"() {
13308
+ "use strict";
13309
+ VIEWER_URL = process.env.GTM_VIEWER_URL ?? "https://view.gtmnow.com";
13310
+ }
13311
+ });
13312
+
13114
13313
  // src/handlers/index.ts
13115
13314
  function isRateLimitError2(error) {
13116
13315
  if (error instanceof Error && "status" in error) {
@@ -13230,6 +13429,10 @@ async function dispatch(name, args, ctx) {
13230
13429
  const { handleFeedback: handleFeedback2 } = await Promise.resolve().then(() => (init_feedback2(), feedback_exports));
13231
13430
  return handleFeedback2(args);
13232
13431
  }
13432
+ case "gtm_review": {
13433
+ const { handleReview: handleReview2 } = await Promise.resolve().then(() => (init_review2(), review_exports));
13434
+ return handleReview2(args);
13435
+ }
13233
13436
  // ─── Meta-Tools ───────────────────────────────────────
13234
13437
  case "gtm_guide": {
13235
13438
  const task = args.task;
@@ -13344,7 +13547,7 @@ var init_validation = __esm({
13344
13547
  platform: z2.string().optional(),
13345
13548
  min_revenue: z2.number().optional(),
13346
13549
  max_revenue: z2.number().optional(),
13347
- output: z2.enum(["file", "inline"]).optional(),
13550
+ output: z2.enum(["file", "inline", "csv"]).optional(),
13348
13551
  detail: detailSchema
13349
13552
  }),
13350
13553
  // 2. People/contact search
@@ -13385,7 +13588,7 @@ var init_validation = __esm({
13385
13588
  max_employees: z2.number().int().min(1).optional(),
13386
13589
  revenue_range: z2.string().optional(),
13387
13590
  fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
13388
- output: z2.enum(["file", "inline"]).optional(),
13591
+ output: z2.enum(["file", "inline", "csv"]).optional(),
13389
13592
  detail: detailSchema
13390
13593
  }).refine(
13391
13594
  (data) => !!(data.job_title || data.company_name || data.company_domain || data.company_domains && data.company_domains.length > 0 || data.seniority && data.seniority.length > 0 || data.filter_industry && data.filter_industry.length > 0 || data.skills && data.skills.length > 0 || data.location),
@@ -13520,6 +13723,7 @@ var init_validation = __esm({
13520
13723
  "cold_outreach",
13521
13724
  "linkedin_outreach",
13522
13725
  "crm_sync",
13726
+ "export_to_webhook",
13523
13727
  "list_tasks"
13524
13728
  ])
13525
13729
  }),
@@ -13539,7 +13743,24 @@ var init_validation = __esm({
13539
13743
  }),
13540
13744
  message: z2.string().min(1, "message is required"),
13541
13745
  context: z2.record(z2.unknown()).optional()
13542
- })
13746
+ }),
13747
+ // 17. Review table
13748
+ gtm_review: z2.discriminatedUnion("action", [
13749
+ z2.object({
13750
+ action: z2.literal("create"),
13751
+ title: z2.string().optional(),
13752
+ data: z2.array(z2.record(z2.unknown())).min(1, "data must contain at least 1 row")
13753
+ }),
13754
+ z2.object({
13755
+ action: z2.literal("update"),
13756
+ table_id: z2.string().min(1, "table_id is required for update"),
13757
+ rows: z2.array(z2.record(z2.unknown())).min(1, "rows must contain at least 1 row")
13758
+ }),
13759
+ z2.object({
13760
+ action: z2.literal("get_selected"),
13761
+ table_id: z2.string().min(1, "table_id is required for get_selected")
13762
+ })
13763
+ ])
13543
13764
  };
13544
13765
  TOOL_NAMES = Object.keys(toolSchemas);
13545
13766
  }