gtm-now 0.10.9 → 0.10.11

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
@@ -11444,7 +11444,20 @@ var init_extract = __esm({
11444
11444
  },
11445
11445
  profile: {
11446
11446
  type: "string",
11447
- description: "LinkedIn URL or publicIdentifier (required for linkedin_posts)"
11447
+ description: "LinkedIn URL or publicIdentifier (required for linkedin_posts, single profile)"
11448
+ },
11449
+ profiles: {
11450
+ type: "array",
11451
+ items: { type: "string" },
11452
+ description: "Batch mode: array of LinkedIn URLs or publicIdentifiers. Checks each for recent posts concurrently. Returns lightweight results (posted/not, count, last date). Use with posted_within to filter for recent activity."
11453
+ },
11454
+ csv_file: {
11455
+ type: "string",
11456
+ description: 'Path to a CSV file containing LinkedIn profile URLs. Reads the "LinkedIn Profile" or "linkedin_url" column automatically. Alternative to profiles[] for large lists.'
11457
+ },
11458
+ concurrency: {
11459
+ type: "number",
11460
+ description: "Max concurrent API requests for batch mode (default: 5, max: 20). Higher = faster but more likely to hit rate limits."
11448
11461
  },
11449
11462
  company: {
11450
11463
  type: "string",
@@ -11483,6 +11496,15 @@ var init_extract = __esm({
11483
11496
  type: "string",
11484
11497
  enum: ["summary", "full"],
11485
11498
  description: 'Response detail level. "summary" (default) returns counts and key fields only \u2014 truncates post text, omits individual engagement items. "full" returns complete data including all post content and engager lists.'
11499
+ },
11500
+ output: {
11501
+ type: "string",
11502
+ enum: ["file", "csv", "inline"],
11503
+ description: 'Output mode for batch results. "csv" writes a CSV file (default for batch). "file" writes JSONL. "inline" returns in response (only for small batches).'
11504
+ },
11505
+ review: {
11506
+ type: "boolean",
11507
+ description: "Create a viewer table with batch results for human review."
11486
11508
  }
11487
11509
  }
11488
11510
  }
@@ -11831,13 +11853,19 @@ var init_review = __esm({
11831
11853
  items: { type: "object" },
11832
11854
  description: "Array of row objects (for create)"
11833
11855
  },
11834
- table_id: { type: "string", description: "Table ID (for update, get_selected, or optional client-provided ID on create)" },
11856
+ table_id: {
11857
+ type: "string",
11858
+ description: "Table ID (for update, get_selected, or optional client-provided ID on create)"
11859
+ },
11835
11860
  rows: {
11836
11861
  type: "array",
11837
11862
  items: { type: "object" },
11838
11863
  description: "Updated row objects with _row_id (for update)"
11839
11864
  },
11840
- ttl_seconds: { type: "number", description: "Optional TTL override in seconds (default: 24h). For create only." }
11865
+ ttl_seconds: {
11866
+ type: "number",
11867
+ description: "Optional TTL override in seconds (default: 24h). For create only."
11868
+ }
11841
11869
  },
11842
11870
  required: ["action"]
11843
11871
  }
@@ -13301,6 +13329,7 @@ var extract_exports = {};
13301
13329
  __export(extract_exports, {
13302
13330
  handleExtract: () => handleExtract
13303
13331
  });
13332
+ import * as fs2 from "fs";
13304
13333
  function slimPost(post) {
13305
13334
  const slim = {};
13306
13335
  for (const [key, value] of Object.entries(post)) {
@@ -13381,6 +13410,11 @@ async function handleLinkedinExtract(type, args, ctx) {
13381
13410
  };
13382
13411
  }
13383
13412
  case "linkedin_posts": {
13413
+ const profiles = args.profiles;
13414
+ const csvFile = args.csv_file;
13415
+ if (profiles?.length || csvFile) {
13416
+ return handleBatchLinkedinPosts(args, adapter);
13417
+ }
13384
13418
  const result = await adapter.getProfilePosts({
13385
13419
  profile: args.profile,
13386
13420
  postedLimit: args.posted_within
@@ -13445,12 +13479,234 @@ async function handleLinkedinExtract(type, args, ctx) {
13445
13479
  throw new Error(`Unknown extract type: ${type}`);
13446
13480
  }
13447
13481
  }
13448
- var POST_TEXT_LIMIT;
13482
+ async function handleBatchLinkedinPosts(args, adapter) {
13483
+ const profilesArg = args.profiles;
13484
+ const csvFile = args.csv_file;
13485
+ const postedWithin = args.posted_within ?? "month";
13486
+ const concurrency = Math.min(
13487
+ args.concurrency ?? DEFAULT_BATCH_CONCURRENCY,
13488
+ MAX_BATCH_CONCURRENCY
13489
+ );
13490
+ const output = args.output;
13491
+ const shouldReview = args.review ?? false;
13492
+ let profiles;
13493
+ if (csvFile) {
13494
+ profiles = parseCsvProfiles(csvFile);
13495
+ } else {
13496
+ profiles = (profilesArg ?? []).map((url) => ({ linkedin_url: url }));
13497
+ }
13498
+ if (profiles.length === 0) {
13499
+ throw createGtmError("EXTRACTION_FAILED", "No LinkedIn profiles found in input.");
13500
+ }
13501
+ const results = new Array(profiles.length);
13502
+ let nextIndex = 0;
13503
+ let completed = 0;
13504
+ const deadline = Date.now() + BATCH_TIMEOUT_MS;
13505
+ async function worker() {
13506
+ while (nextIndex < profiles.length) {
13507
+ const i = nextIndex++;
13508
+ const entry = profiles[i];
13509
+ if (Date.now() > deadline) {
13510
+ results[i] = {
13511
+ linkedin_url: entry.linkedin_url,
13512
+ name: entry.name,
13513
+ company: entry.company,
13514
+ has_posts: false,
13515
+ post_count: 0,
13516
+ error: "timeout"
13517
+ };
13518
+ completed++;
13519
+ continue;
13520
+ }
13521
+ try {
13522
+ const result = await adapter.getProfilePosts({
13523
+ profile: entry.linkedin_url,
13524
+ postedLimit: postedWithin
13525
+ });
13526
+ const posts = result.elements;
13527
+ let lastPostDate;
13528
+ if (posts.length > 0) {
13529
+ const first = posts[0];
13530
+ lastPostDate = first.postedAt ?? first.postedDate ?? first.posted_at;
13531
+ }
13532
+ results[i] = {
13533
+ linkedin_url: entry.linkedin_url,
13534
+ name: entry.name,
13535
+ company: entry.company,
13536
+ has_posts: posts.length > 0,
13537
+ post_count: posts.length,
13538
+ last_post_date: lastPostDate
13539
+ };
13540
+ } catch (error) {
13541
+ logError("extract:batchLinkedinPosts", error instanceof Error ? error : new Error(String(error)), {
13542
+ profile: entry.linkedin_url
13543
+ });
13544
+ results[i] = {
13545
+ linkedin_url: entry.linkedin_url,
13546
+ name: entry.name,
13547
+ company: entry.company,
13548
+ has_posts: false,
13549
+ post_count: 0,
13550
+ error: error instanceof Error ? error.message : String(error)
13551
+ };
13552
+ }
13553
+ completed++;
13554
+ }
13555
+ }
13556
+ await Promise.all(
13557
+ Array.from({ length: Math.min(concurrency, profiles.length) }, () => worker())
13558
+ );
13559
+ const activeProfiles = results.filter((r) => r.has_posts);
13560
+ const errors = results.filter((r) => r.error);
13561
+ let tableId;
13562
+ let tableUrl;
13563
+ if (shouldReview) {
13564
+ const tableResult = await createReviewTable(
13565
+ `LinkedIn Activity Check (${postedWithin})`,
13566
+ results
13567
+ );
13568
+ if (tableResult) {
13569
+ tableId = tableResult.table_id;
13570
+ tableUrl = tableResult.url;
13571
+ }
13572
+ }
13573
+ const effectiveOutput = output ?? (profiles.length > 20 ? "csv" : "inline");
13574
+ if (effectiveOutput === "csv") {
13575
+ const filePath = writeResultsCsv(
13576
+ "linkedin_activity",
13577
+ results
13578
+ );
13579
+ return {
13580
+ type: "linkedin_posts_batch",
13581
+ provider: "harvest",
13582
+ total: profiles.length,
13583
+ active: activeProfiles.length,
13584
+ inactive: profiles.length - activeProfiles.length - errors.length,
13585
+ errors: errors.length,
13586
+ posted_within: postedWithin,
13587
+ output: "csv",
13588
+ file: filePath,
13589
+ preview: activeProfiles.slice(0, 5),
13590
+ ...tableId && { table_id: tableId, url: tableUrl }
13591
+ };
13592
+ }
13593
+ if (effectiveOutput === "file") {
13594
+ const filePath = writeResultsFile(
13595
+ "linkedin_activity",
13596
+ results,
13597
+ { query: { posted_within: postedWithin }, provider: "harvest", count: results.length, has_more: false }
13598
+ );
13599
+ return {
13600
+ type: "linkedin_posts_batch",
13601
+ provider: "harvest",
13602
+ total: profiles.length,
13603
+ active: activeProfiles.length,
13604
+ inactive: profiles.length - activeProfiles.length - errors.length,
13605
+ errors: errors.length,
13606
+ posted_within: postedWithin,
13607
+ output: "file",
13608
+ file: filePath,
13609
+ preview: activeProfiles.slice(0, 5),
13610
+ ...tableId && { table_id: tableId, url: tableUrl }
13611
+ };
13612
+ }
13613
+ return {
13614
+ type: "linkedin_posts_batch",
13615
+ provider: "harvest",
13616
+ total: profiles.length,
13617
+ active: activeProfiles.length,
13618
+ inactive: profiles.length - activeProfiles.length - errors.length,
13619
+ errors: errors.length,
13620
+ posted_within: postedWithin,
13621
+ results,
13622
+ ...tableId && { table_id: tableId, url: tableUrl }
13623
+ };
13624
+ }
13625
+ function parseCsvProfiles(filePath) {
13626
+ if (!fs2.existsSync(filePath)) {
13627
+ throw createGtmError("EXTRACTION_FAILED", `CSV file not found: ${filePath}`);
13628
+ }
13629
+ const content = fs2.readFileSync(filePath, "utf-8");
13630
+ const lines = content.split("\n").filter((line) => line.trim().length > 0);
13631
+ if (lines.length < 2) {
13632
+ throw createGtmError("EXTRACTION_FAILED", "CSV file has no data rows.");
13633
+ }
13634
+ const headerLine = lines[0];
13635
+ const headers = parseCsvLine(headerLine).map((h) => h.toLowerCase().trim());
13636
+ const linkedinIdx = headers.findIndex((h) => LINKEDIN_URL_COLUMNS.includes(h));
13637
+ if (linkedinIdx === -1) {
13638
+ throw createGtmError(
13639
+ "EXTRACTION_FAILED",
13640
+ `No LinkedIn URL column found in CSV. Expected one of: ${LINKEDIN_URL_COLUMNS.join(", ")}. Found columns: ${headers.join(", ")}`
13641
+ );
13642
+ }
13643
+ const nameIdx = headers.findIndex((h) => NAME_COLUMNS.includes(h));
13644
+ const companyIdx = headers.findIndex((h) => COMPANY_COLUMNS.includes(h));
13645
+ const entries = [];
13646
+ for (let i = 1; i < lines.length; i++) {
13647
+ const fields = parseCsvLine(lines[i]);
13648
+ const url = fields[linkedinIdx]?.trim();
13649
+ if (!url || !url.includes("linkedin.com")) continue;
13650
+ entries.push({
13651
+ linkedin_url: url,
13652
+ name: nameIdx >= 0 ? fields[nameIdx]?.trim() : void 0,
13653
+ company: companyIdx >= 0 ? fields[companyIdx]?.trim() : void 0
13654
+ });
13655
+ }
13656
+ return entries;
13657
+ }
13658
+ function parseCsvLine(line) {
13659
+ const fields = [];
13660
+ let current = "";
13661
+ let inQuotes = false;
13662
+ for (let i = 0; i < line.length; i++) {
13663
+ const char = line[i];
13664
+ if (inQuotes) {
13665
+ if (char === '"') {
13666
+ if (i + 1 < line.length && line[i + 1] === '"') {
13667
+ current += '"';
13668
+ i++;
13669
+ } else {
13670
+ inQuotes = false;
13671
+ }
13672
+ } else {
13673
+ current += char;
13674
+ }
13675
+ } else if (char === '"') {
13676
+ inQuotes = true;
13677
+ } else if (char === ",") {
13678
+ fields.push(current);
13679
+ current = "";
13680
+ } else {
13681
+ current += char;
13682
+ }
13683
+ }
13684
+ fields.push(current);
13685
+ return fields;
13686
+ }
13687
+ var POST_TEXT_LIMIT, DEFAULT_BATCH_CONCURRENCY, MAX_BATCH_CONCURRENCY, BATCH_TIMEOUT_MS, LINKEDIN_URL_COLUMNS, NAME_COLUMNS, COMPANY_COLUMNS;
13449
13688
  var init_extract2 = __esm({
13450
13689
  "src/handlers/extract.ts"() {
13451
13690
  "use strict";
13691
+ init_dist();
13452
13692
  init_errors();
13693
+ init_table_bridge();
13694
+ init_result_writer();
13453
13695
  POST_TEXT_LIMIT = 200;
13696
+ DEFAULT_BATCH_CONCURRENCY = 5;
13697
+ MAX_BATCH_CONCURRENCY = 20;
13698
+ BATCH_TIMEOUT_MS = 10 * 60 * 1e3;
13699
+ LINKEDIN_URL_COLUMNS = [
13700
+ "linkedin profile",
13701
+ "linkedin_profile",
13702
+ "linkedin url",
13703
+ "linkedin_url",
13704
+ "linkedin",
13705
+ "profile url",
13706
+ "profile_url"
13707
+ ];
13708
+ NAME_COLUMNS = ["full name", "full_name", "name"];
13709
+ COMPANY_COLUMNS = ["company name", "company_name", "company"];
13454
13710
  }
13455
13711
  });
13456
13712
 
@@ -13857,7 +14113,9 @@ async function handleCrm(args, ctx) {
13857
14113
  if (rows.length === 0) {
13858
14114
  return {
13859
14115
  created: 0,
13860
- hints: ["No selected rows found in the review table. Select rows in the viewer and retry."]
14116
+ hints: [
14117
+ "No selected rows found in the review table. Select rows in the viewer and retry."
14118
+ ]
13861
14119
  };
13862
14120
  }
13863
14121
  const contacts = mapRowsToContacts(rows);
@@ -13887,7 +14145,10 @@ async function handleCrm(args, ctx) {
13887
14145
  case "find_company": {
13888
14146
  const query = args.query;
13889
14147
  const name = args.name ?? query?.name;
13890
- if (!name) throw new Error('name is required for find_company action (pass as name="X" or query={"name":"X"})');
14148
+ if (!name)
14149
+ throw new Error(
14150
+ 'name is required for find_company action (pass as name="X" or query={"name":"X"})'
14151
+ );
13891
14152
  if (!manager.findCompany) throw new Error("Provider does not support company lookup");
13892
14153
  return manager.findCompany(name);
13893
14154
  }
@@ -14089,53 +14350,49 @@ __export(feedback_exports, {
14089
14350
  handleFeedback: () => handleFeedback
14090
14351
  });
14091
14352
  async function handleFeedback(args) {
14092
- const input = args;
14093
- if (!SUPABASE_KEY) {
14094
- throw createGtmError(
14095
- "PROVIDER_NOT_CONFIGURED",
14096
- "Supabase not configured for feedback. Set GTM_SUPABASE_KEY or SUPABASE_SERVICE_ROLE_KEY env var."
14097
- );
14098
- }
14099
- const row = {
14100
- type: input.type,
14101
- message: input.message.trim(),
14102
- context: input.context ?? {}
14103
- };
14104
- const response = await fetch(`${SUPABASE_URL}/rest/v1/gtm_feedback`, {
14105
- method: "POST",
14106
- headers: {
14107
- "Content-Type": "application/json",
14108
- apikey: SUPABASE_KEY,
14109
- Authorization: `Bearer ${SUPABASE_KEY}`,
14110
- Prefer: "return=representation"
14111
- },
14112
- body: JSON.stringify(row)
14113
- });
14114
- if (!response.ok) {
14115
- const body = await response.text().catch(() => "unknown");
14353
+ const type = args.type;
14354
+ const message = args.message.trim();
14355
+ const context = args.context ?? {};
14356
+ try {
14357
+ const response = await fetch(`${VIEWER_URL2}/api/feedback`, {
14358
+ method: "POST",
14359
+ headers: { "Content-Type": "application/json" },
14360
+ body: JSON.stringify({ type, message, context })
14361
+ });
14362
+ if (!response.ok) {
14363
+ const body = await response.text().catch(() => "unknown");
14364
+ throw createGtmError(
14365
+ "PROVIDER_REQUEST_FAILED",
14366
+ `Failed to submit feedback: ${response.status}`,
14367
+ {
14368
+ details: { status: response.status, body }
14369
+ }
14370
+ );
14371
+ }
14372
+ const data = await response.json();
14373
+ const created = data[0];
14374
+ return {
14375
+ submitted: true,
14376
+ id: created?.id,
14377
+ type,
14378
+ message: `Feedback submitted. Thank you! (ID: ${created?.id ?? "unknown"})`
14379
+ };
14380
+ } catch (error) {
14381
+ if (error && typeof error === "object" && "error_code" in error) throw error;
14382
+ logError("feedback:submit", error instanceof Error ? error : new Error(String(error)));
14116
14383
  throw createGtmError(
14117
14384
  "PROVIDER_REQUEST_FAILED",
14118
- `Failed to submit feedback: ${response.status}`,
14119
- {
14120
- details: { status: response.status, body }
14121
- }
14385
+ "Failed to submit feedback. Please try again later."
14122
14386
  );
14123
14387
  }
14124
- const [created] = await response.json();
14125
- return {
14126
- submitted: true,
14127
- id: created?.id,
14128
- type: input.type,
14129
- message: `Feedback submitted. Thank you! (ID: ${created?.id ?? "unknown"})`
14130
- };
14131
14388
  }
14132
- var SUPABASE_URL, SUPABASE_KEY;
14389
+ var VIEWER_URL2;
14133
14390
  var init_feedback2 = __esm({
14134
14391
  "src/handlers/feedback.ts"() {
14135
14392
  "use strict";
14393
+ init_dist();
14136
14394
  init_errors();
14137
- SUPABASE_URL = process.env.GTM_SUPABASE_URL ?? process.env.SUPABASE_URL ?? "https://qvawbxpijxlwdkolmjrs.supabase.co";
14138
- SUPABASE_KEY = process.env.GTM_SUPABASE_KEY ?? process.env.SUPABASE_SERVICE_ROLE_KEY ?? "";
14395
+ VIEWER_URL2 = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
14139
14396
  }
14140
14397
  });
14141
14398
 
@@ -14166,7 +14423,7 @@ async function createTable(args) {
14166
14423
  ...row,
14167
14424
  _row_id: row._row_id ?? `r${i + 1}`
14168
14425
  }));
14169
- const response = await fetch(`${VIEWER_URL2}/api/tables`, {
14426
+ const response = await fetch(`${VIEWER_URL3}/api/tables`, {
14170
14427
  method: "POST",
14171
14428
  headers: { "Content-Type": "application/json" },
14172
14429
  body: JSON.stringify({
@@ -14186,7 +14443,7 @@ async function createTable(args) {
14186
14443
  async function updateTable(args) {
14187
14444
  const tableId = args.table_id;
14188
14445
  const rows = args.rows;
14189
- const response = await fetch(`${VIEWER_URL2}/api/tables/${tableId}`, {
14446
+ const response = await fetch(`${VIEWER_URL3}/api/tables/${tableId}`, {
14190
14447
  method: "PATCH",
14191
14448
  headers: { "Content-Type": "application/json" },
14192
14449
  body: JSON.stringify({ rows })
@@ -14199,18 +14456,18 @@ async function updateTable(args) {
14199
14456
  }
14200
14457
  async function getSelected(args) {
14201
14458
  const tableId = args.table_id;
14202
- const response = await fetch(`${VIEWER_URL2}/api/tables/${tableId}/selected`);
14459
+ const response = await fetch(`${VIEWER_URL3}/api/tables/${tableId}/selected`);
14203
14460
  if (!response.ok) {
14204
14461
  const body = await response.json().catch(() => ({}));
14205
14462
  throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
14206
14463
  }
14207
14464
  return response.json();
14208
14465
  }
14209
- var VIEWER_URL2;
14466
+ var VIEWER_URL3;
14210
14467
  var init_review2 = __esm({
14211
14468
  "src/handlers/review.ts"() {
14212
14469
  "use strict";
14213
- VIEWER_URL2 = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
14470
+ VIEWER_URL3 = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
14214
14471
  }
14215
14472
  });
14216
14473
 
@@ -14539,6 +14796,9 @@ var init_validation = __esm({
14539
14796
  linkedin_url: z2.string().optional(),
14540
14797
  find_email: z2.boolean().optional(),
14541
14798
  profile: z2.string().optional(),
14799
+ profiles: z2.array(z2.string()).optional(),
14800
+ csv_file: z2.string().optional(),
14801
+ concurrency: z2.number().int().min(1).max(20).optional(),
14542
14802
  company: z2.string().optional(),
14543
14803
  posted_within: z2.enum(["24h", "week", "month"]).optional(),
14544
14804
  post_url: z2.string().optional(),
@@ -14547,14 +14807,16 @@ var init_validation = __esm({
14547
14807
  sort: z2.enum(["relevance", "date"]).optional(),
14548
14808
  limit: z2.number().int().min(1).max(100).optional(),
14549
14809
  fields: z2.array(z2.string()).optional(),
14550
- detail: detailSchema
14810
+ detail: detailSchema,
14811
+ output: z2.enum(["file", "csv", "inline"]).optional(),
14812
+ review: z2.boolean().optional()
14551
14813
  }).refine(
14552
14814
  (data) => {
14553
14815
  switch (data.type) {
14554
14816
  case "linkedin_profile":
14555
14817
  return !!(data.linkedin_url || data.url);
14556
14818
  case "linkedin_posts":
14557
- return !!data.profile;
14819
+ return !!(data.profile || data.profiles?.length || data.csv_file);
14558
14820
  case "linkedin_engagement":
14559
14821
  return !!data.post_url;
14560
14822
  case "linkedin_search":
@@ -14815,7 +15077,7 @@ var init_server = __esm({
14815
15077
  });
14816
15078
 
14817
15079
  // src/cli-commands.ts
14818
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
15080
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
14819
15081
  function registerCommands(program, exec2) {
14820
15082
  buildServeCommand(program);
14821
15083
  buildStatusCommand(program, exec2);
@@ -14944,11 +15206,11 @@ function parseContactsArg(value, writeError2) {
14944
15206
  writeError2("Failed to parse --contacts as JSON", { received: trimmed.slice(0, 200) });
14945
15207
  }
14946
15208
  }
14947
- if (!existsSync2(value)) {
15209
+ if (!existsSync3(value)) {
14948
15210
  writeError2(`Contacts file not found: ${value}`);
14949
15211
  }
14950
15212
  try {
14951
- const content = readFileSync2(value, "utf-8");
15213
+ const content = readFileSync3(value, "utf-8");
14952
15214
  const parsed = JSON.parse(content);
14953
15215
  return Array.isArray(parsed) ? parsed : [parsed];
14954
15216
  } catch (err) {