arendi-cli 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +1459 -0
  2. package/package.json +55 -0
package/dist/index.js ADDED
@@ -0,0 +1,1459 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import "dotenv/config";
5
+
6
+ // src/cli.ts
7
+ import { mkdirSync, writeFileSync } from "fs";
8
+ import { dirname, resolve } from "path";
9
+ import { homedir } from "os";
10
+ import { Command } from "commander";
11
+
12
+ // ../../packages/arendi/dist/types.js
13
+ import { z } from "zod";
14
+ var CATEGORIES = [
15
+ "books",
16
+ "business",
17
+ "education",
18
+ "entertainment",
19
+ "fashion",
20
+ "finance",
21
+ "fitness",
22
+ "food",
23
+ "gaming",
24
+ "health",
25
+ "lifestyle",
26
+ "marketing",
27
+ "music",
28
+ "pets",
29
+ "real-estate",
30
+ "saas",
31
+ "social-media",
32
+ "sports",
33
+ "technology",
34
+ "travel"
35
+ ];
36
+ var PROVIDERS = ["openai", "anthropic", "google"];
37
+ var OUTPUT_FORMATS = ["markdown", "json", "csv", "text"];
38
+ var optionsSchema = z.object({
39
+ country: z.string().optional(),
40
+ city: z.string().optional(),
41
+ categories: z.array(z.string()).optional(),
42
+ limit: z.number().min(1).max(50).default(5),
43
+ keywords: z.array(z.string()).optional(),
44
+ provider: z.enum(PROVIDERS).default("openai"),
45
+ model: z.string().optional(),
46
+ apiKey: z.string().min(1, "LLM provider API key is required"),
47
+ tavilyApiKey: z.string().min(1, "Tavily API key is required"),
48
+ /** Optional ISO 3166-1 alpha-2 code for Google Trends (e.g. "US", "ID"). Auto-detected from `country` if omitted. */
49
+ geo: z.string().length(2).optional()
50
+ });
51
+ var tavilyResultSchema = z.object({
52
+ title: z.string(),
53
+ url: z.string(),
54
+ content: z.string(),
55
+ score: z.number()
56
+ });
57
+ var tavilyResponseSchema = z.object({
58
+ results: z.array(tavilyResultSchema)
59
+ });
60
+ var businessIdeaSchema = z.object({
61
+ title: z.string().describe("A concise, compelling title for the business idea"),
62
+ description: z.string().describe("A 2-3 sentence description of the business idea"),
63
+ category: z.string().describe("The primary category this idea falls under"),
64
+ targetAudience: z.string().describe("Who this business targets (demographics, psychographics)"),
65
+ painPoint: z.string().describe("The specific pain point or unmet need this addresses"),
66
+ opportunityType: z.enum(["trending", "pain-point", "gap", "trend-transfer"]).describe("How this opportunity was identified: trending topic, pain point, market gap, or trend transferred from another market"),
67
+ estimatedDifficulty: z.enum(["low", "medium", "high"]).describe("How difficult it is to start this business"),
68
+ potentialRevenue: z.enum(["low", "medium", "high"]).describe("Estimated revenue potential"),
69
+ reasoning: z.string().describe("Why this is a good opportunity right now, grounded in the research data"),
70
+ actionableSteps: z.array(z.string()).describe("3-5 concrete first steps to pursue this idea"),
71
+ references: z.array(z.string()).describe("URLs or sources that support this idea")
72
+ });
73
+ var arendiResultSchema = z.object({
74
+ ideas: z.array(businessIdeaSchema)
75
+ });
76
+
77
+ // ../../packages/arendi/dist/format.js
78
+ function toMarkdown(ideas) {
79
+ const lines = [`# Business Ideas & Opportunities`, ""];
80
+ for (let i = 0; i < ideas.length; i++) {
81
+ const idea = ideas[i];
82
+ const num = i + 1;
83
+ lines.push(`## ${num}. ${idea.title}`);
84
+ lines.push("");
85
+ lines.push(idea.description);
86
+ lines.push("");
87
+ lines.push(`| Field | Value |`);
88
+ lines.push(`| --- | --- |`);
89
+ lines.push(`| **Category** | ${idea.category} |`);
90
+ lines.push(`| **Opportunity type** | ${idea.opportunityType} |`);
91
+ lines.push(`| **Target audience** | ${idea.targetAudience} |`);
92
+ lines.push(`| **Pain point** | ${idea.painPoint} |`);
93
+ lines.push(`| **Difficulty** | ${idea.estimatedDifficulty} |`);
94
+ lines.push(`| **Revenue potential** | ${idea.potentialRevenue} |`);
95
+ lines.push("");
96
+ lines.push(`### Why now?`);
97
+ lines.push("");
98
+ lines.push(idea.reasoning);
99
+ lines.push("");
100
+ lines.push(`### Actionable steps`);
101
+ lines.push("");
102
+ for (const step of idea.actionableSteps) {
103
+ lines.push(`1. ${step}`);
104
+ }
105
+ lines.push("");
106
+ if (idea.references.length > 0) {
107
+ lines.push(`### References`);
108
+ lines.push("");
109
+ for (const ref of idea.references) {
110
+ lines.push(`- ${ref}`);
111
+ }
112
+ lines.push("");
113
+ }
114
+ lines.push("---");
115
+ lines.push("");
116
+ }
117
+ return lines.join("\n");
118
+ }
119
+ function toJson(ideas) {
120
+ return JSON.stringify(ideas, null, 2);
121
+ }
122
+ function escapeCsv(value) {
123
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
124
+ return `"${value.replace(/"/g, '""')}"`;
125
+ }
126
+ return value;
127
+ }
128
+ function toCsv(ideas) {
129
+ const headers = [
130
+ "title",
131
+ "description",
132
+ "category",
133
+ "targetAudience",
134
+ "painPoint",
135
+ "opportunityType",
136
+ "estimatedDifficulty",
137
+ "potentialRevenue",
138
+ "reasoning",
139
+ "actionableSteps",
140
+ "references"
141
+ ];
142
+ const rows = ideas.map((idea) => [
143
+ escapeCsv(idea.title),
144
+ escapeCsv(idea.description),
145
+ escapeCsv(idea.category),
146
+ escapeCsv(idea.targetAudience),
147
+ escapeCsv(idea.painPoint),
148
+ escapeCsv(idea.opportunityType),
149
+ escapeCsv(idea.estimatedDifficulty),
150
+ escapeCsv(idea.potentialRevenue),
151
+ escapeCsv(idea.reasoning),
152
+ escapeCsv(idea.actionableSteps.join("; ")),
153
+ escapeCsv(idea.references.join("; "))
154
+ ].join(","));
155
+ return [headers.join(","), ...rows].join("\n");
156
+ }
157
+ function toText(ideas) {
158
+ const lines = [
159
+ "BUSINESS IDEAS & OPPORTUNITIES",
160
+ "=".repeat(40),
161
+ ""
162
+ ];
163
+ for (let i = 0; i < ideas.length; i++) {
164
+ const idea = ideas[i];
165
+ const num = i + 1;
166
+ lines.push(`${num}. ${idea.title.toUpperCase()}`);
167
+ lines.push("-".repeat(40));
168
+ lines.push(idea.description);
169
+ lines.push("");
170
+ lines.push(` Category: ${idea.category}`);
171
+ lines.push(` Opportunity type: ${idea.opportunityType}`);
172
+ lines.push(` Target audience: ${idea.targetAudience}`);
173
+ lines.push(` Pain point: ${idea.painPoint}`);
174
+ lines.push(` Difficulty: ${idea.estimatedDifficulty}`);
175
+ lines.push(` Revenue potential: ${idea.potentialRevenue}`);
176
+ lines.push("");
177
+ lines.push(` Why now?`);
178
+ lines.push(` ${idea.reasoning}`);
179
+ lines.push("");
180
+ lines.push(` Steps:`);
181
+ for (const step of idea.actionableSteps) {
182
+ lines.push(` - ${step}`);
183
+ }
184
+ lines.push("");
185
+ if (idea.references.length > 0) {
186
+ lines.push(` References:`);
187
+ for (const ref of idea.references) {
188
+ lines.push(` - ${ref}`);
189
+ }
190
+ lines.push("");
191
+ }
192
+ lines.push("");
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+ function formatIdeas(ideas, format) {
197
+ switch (format) {
198
+ case "markdown":
199
+ return toMarkdown(ideas);
200
+ case "json":
201
+ return toJson(ideas);
202
+ case "csv":
203
+ return toCsv(ideas);
204
+ case "text":
205
+ return toText(ideas);
206
+ }
207
+ }
208
+
209
+ // ../../packages/arendi/dist/search.js
210
+ import { createHash } from "crypto";
211
+
212
+ // ../../packages/arendi/dist/trends.js
213
+ import googleTrends from "google-trends-api";
214
+ var COUNTRY_GEO_MAP = {
215
+ afghanistan: "AF",
216
+ albania: "AL",
217
+ algeria: "DZ",
218
+ argentina: "AR",
219
+ australia: "AU",
220
+ austria: "AT",
221
+ bangladesh: "BD",
222
+ belgium: "BE",
223
+ bolivia: "BO",
224
+ brazil: "BR",
225
+ cambodia: "KH",
226
+ cameroon: "CM",
227
+ canada: "CA",
228
+ chile: "CL",
229
+ china: "CN",
230
+ colombia: "CO",
231
+ "costa rica": "CR",
232
+ croatia: "HR",
233
+ "czech republic": "CZ",
234
+ czechia: "CZ",
235
+ denmark: "DK",
236
+ "dominican republic": "DO",
237
+ ecuador: "EC",
238
+ egypt: "EG",
239
+ "el salvador": "SV",
240
+ ethiopia: "ET",
241
+ finland: "FI",
242
+ france: "FR",
243
+ germany: "DE",
244
+ ghana: "GH",
245
+ greece: "GR",
246
+ guatemala: "GT",
247
+ honduras: "HN",
248
+ "hong kong": "HK",
249
+ hungary: "HU",
250
+ india: "IN",
251
+ indonesia: "ID",
252
+ iran: "IR",
253
+ iraq: "IQ",
254
+ ireland: "IE",
255
+ israel: "IL",
256
+ italy: "IT",
257
+ jamaica: "JM",
258
+ japan: "JP",
259
+ jordan: "JO",
260
+ kazakhstan: "KZ",
261
+ kenya: "KE",
262
+ "south korea": "KR",
263
+ korea: "KR",
264
+ kuwait: "KW",
265
+ laos: "LA",
266
+ lebanon: "LB",
267
+ libya: "LY",
268
+ malaysia: "MY",
269
+ mexico: "MX",
270
+ morocco: "MA",
271
+ myanmar: "MM",
272
+ nepal: "NP",
273
+ netherlands: "NL",
274
+ "new zealand": "NZ",
275
+ nicaragua: "NI",
276
+ nigeria: "NG",
277
+ norway: "NO",
278
+ oman: "OM",
279
+ pakistan: "PK",
280
+ panama: "PA",
281
+ paraguay: "PY",
282
+ peru: "PE",
283
+ philippines: "PH",
284
+ poland: "PL",
285
+ portugal: "PT",
286
+ qatar: "QA",
287
+ romania: "RO",
288
+ russia: "RU",
289
+ "saudi arabia": "SA",
290
+ senegal: "SN",
291
+ serbia: "RS",
292
+ singapore: "SG",
293
+ slovakia: "SK",
294
+ "south africa": "ZA",
295
+ spain: "ES",
296
+ "sri lanka": "LK",
297
+ sweden: "SE",
298
+ switzerland: "CH",
299
+ taiwan: "TW",
300
+ tanzania: "TZ",
301
+ thailand: "TH",
302
+ tunisia: "TN",
303
+ turkey: "TR",
304
+ turkiye: "TR",
305
+ uganda: "UG",
306
+ ukraine: "UA",
307
+ "united arab emirates": "AE",
308
+ uae: "AE",
309
+ "united kingdom": "GB",
310
+ uk: "GB",
311
+ "united states": "US",
312
+ usa: "US",
313
+ us: "US",
314
+ uruguay: "UY",
315
+ uzbekistan: "UZ",
316
+ venezuela: "VE",
317
+ vietnam: "VN",
318
+ zambia: "ZM",
319
+ zimbabwe: "ZW"
320
+ };
321
+ function countryToGeo(country) {
322
+ const trimmed = country.trim();
323
+ if (/^[A-Za-z]{2}$/.test(trimmed)) {
324
+ return trimmed.toUpperCase();
325
+ }
326
+ return COUNTRY_GEO_MAP[trimmed.toLowerCase()] ?? null;
327
+ }
328
+ async function fetchDailyTrends(geo) {
329
+ try {
330
+ const raw = await googleTrends.dailyTrends({ geo });
331
+ const parsed = JSON.parse(raw);
332
+ const days = parsed?.["default"];
333
+ const trendingDays = days?.["trendingSearchesDays"] ?? [];
334
+ const results = [];
335
+ for (const day of trendingDays) {
336
+ const searches = day["trendingSearches"] ?? [];
337
+ for (const search of searches) {
338
+ const title = search["title"]?.["query"] ?? "";
339
+ const traffic = search["formattedTraffic"] ?? "";
340
+ const relatedQueriesRaw = search["relatedQueries"] ?? [];
341
+ const relatedQueries = relatedQueriesRaw.map((q) => q["query"] ?? "").filter(Boolean);
342
+ if (title) {
343
+ results.push({ title, traffic, relatedQueries });
344
+ }
345
+ }
346
+ }
347
+ return results;
348
+ } catch (error) {
349
+ console.warn(`Warning: Failed to fetch daily trends for geo "${geo}":`, error instanceof Error ? error.message : String(error));
350
+ return [];
351
+ }
352
+ }
353
+ async function fetchRelatedQueries(keywords, geo) {
354
+ const ninetyDaysAgo = /* @__PURE__ */ new Date();
355
+ ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
356
+ const results = await Promise.all(keywords.map(async (keyword) => {
357
+ try {
358
+ const raw = await googleTrends.relatedQueries({
359
+ keyword,
360
+ startTime: ninetyDaysAgo,
361
+ geo: geo ?? ""
362
+ });
363
+ const parsed = JSON.parse(raw);
364
+ const rankedList = parsed?.["default"]?.["rankedList"];
365
+ if (!rankedList || rankedList.length === 0) {
366
+ return { keyword, rising: [], top: [] };
367
+ }
368
+ const topList = rankedList[0]?.["rankedKeyword"] ?? [];
369
+ const risingList = rankedList[1]?.["rankedKeyword"] ?? [];
370
+ const top = topList.map((item) => ({
371
+ query: item["query"] ?? "",
372
+ value: item["value"] ?? 0
373
+ })).filter((item) => item.query);
374
+ const rising = risingList.map((item) => ({
375
+ query: item["query"] ?? "",
376
+ value: item["formattedValue"] ?? item["value"] ?? 0
377
+ })).filter((item) => item.query);
378
+ return { keyword, rising, top };
379
+ } catch (error) {
380
+ console.warn(`Warning: Failed to fetch related queries for "${keyword}":`, error instanceof Error ? error.message : String(error));
381
+ return null;
382
+ }
383
+ }));
384
+ return results.filter((r) => r !== null);
385
+ }
386
+ async function fetchRelatedTopics(keywords, geo) {
387
+ const ninetyDaysAgo = /* @__PURE__ */ new Date();
388
+ ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
389
+ const results = await Promise.all(keywords.map(async (keyword) => {
390
+ try {
391
+ const raw = await googleTrends.relatedTopics({
392
+ keyword,
393
+ startTime: ninetyDaysAgo,
394
+ geo: geo ?? ""
395
+ });
396
+ const parsed = JSON.parse(raw);
397
+ const rankedList = parsed?.["default"]?.["rankedList"];
398
+ if (!rankedList || rankedList.length === 0) {
399
+ return { keyword, rising: [], top: [] };
400
+ }
401
+ const topList = rankedList[0]?.["rankedKeyword"] ?? [];
402
+ const risingList = rankedList[1]?.["rankedKeyword"] ?? [];
403
+ const top = topList.map((item) => {
404
+ const topic = item["topic"];
405
+ return {
406
+ title: topic?.["title"] ?? "",
407
+ type: topic?.["type"] ?? "",
408
+ value: item["value"] ?? 0
409
+ };
410
+ }).filter((item) => item.title);
411
+ const rising = risingList.map((item) => {
412
+ const topic = item["topic"];
413
+ return {
414
+ title: topic?.["title"] ?? "",
415
+ type: topic?.["type"] ?? "",
416
+ value: item["formattedValue"] ?? item["value"] ?? 0
417
+ };
418
+ }).filter((item) => item.title);
419
+ return { keyword, rising, top };
420
+ } catch (error) {
421
+ console.warn(`Warning: Failed to fetch related topics for "${keyword}":`, error instanceof Error ? error.message : String(error));
422
+ return null;
423
+ }
424
+ }));
425
+ return results.filter((r) => r !== null);
426
+ }
427
+ async function trendsResearch(options) {
428
+ const geo = options.geo ? options.geo.toUpperCase() : options.country ? countryToGeo(options.country) : null;
429
+ const keywords = options.keywords?.length ? [...options.keywords] : [];
430
+ if (options.categories?.length && keywords.length === 0) {
431
+ keywords.push(...options.categories);
432
+ }
433
+ const [dailyTrends, relatedQueries, relatedTopics] = await Promise.all([
434
+ geo ? fetchDailyTrends(geo) : Promise.resolve([]),
435
+ keywords.length > 0 ? fetchRelatedQueries(keywords, geo ?? void 0) : Promise.resolve([]),
436
+ keywords.length > 0 ? fetchRelatedTopics(keywords, geo ?? void 0) : Promise.resolve([])
437
+ ]);
438
+ return { dailyTrends, relatedQueries, relatedTopics };
439
+ }
440
+ function summarizeTrends(ctx) {
441
+ const sections = [];
442
+ if (ctx.dailyTrends.length > 0) {
443
+ const items = ctx.dailyTrends.slice(0, 20).map((t) => {
444
+ let line = `- **${t.title}**`;
445
+ if (t.traffic)
446
+ line += ` (${t.traffic} searches)`;
447
+ if (t.relatedQueries.length > 0) {
448
+ line += ` \u2014 related: ${t.relatedQueries.join(", ")}`;
449
+ }
450
+ return line;
451
+ }).join("\n");
452
+ sections.push(`### Daily Trending Searches
453
+
454
+ These are the top trending search terms in the target market right now.
455
+
456
+ ${items}`);
457
+ }
458
+ const risingQueries = ctx.relatedQueries.filter((r) => r.rising.length > 0);
459
+ if (risingQueries.length > 0) {
460
+ const items = risingQueries.map((r) => {
461
+ const queries = r.rising.slice(0, 10).map((q) => {
462
+ const label = typeof q.value === "string" ? q.value : `+${q.value}%`;
463
+ return ` - "${q.query}" (${label})`;
464
+ }).join("\n");
465
+ return `- **${r.keyword}**:
466
+ ${queries}`;
467
+ }).join("\n");
468
+ sections.push(`### Rising Search Queries
469
+
470
+ Queries with the biggest recent increase in search frequency. "Breakout" means massive growth from near-zero.
471
+
472
+ ${items}`);
473
+ }
474
+ const risingTopics = ctx.relatedTopics.filter((r) => r.rising.length > 0);
475
+ if (risingTopics.length > 0) {
476
+ const items = risingTopics.map((r) => {
477
+ const topics = r.rising.slice(0, 10).map((t) => {
478
+ const label = typeof t.value === "string" ? t.value : `+${t.value}%`;
479
+ return ` - ${t.title} (${t.type}) \u2014 ${label}`;
480
+ }).join("\n");
481
+ return `- **${r.keyword}**:
482
+ ${topics}`;
483
+ }).join("\n");
484
+ sections.push(`### Rising Related Topics
485
+
486
+ Topics with the biggest recent increase in search interest.
487
+
488
+ ${items}`);
489
+ }
490
+ if (sections.length === 0)
491
+ return "";
492
+ return `## Google Trends Data
493
+
494
+ Real-time search demand signals from Google Trends showing what people are actively searching for.
495
+
496
+ ${sections.join("\n\n")}`;
497
+ }
498
+
499
+ // ../../packages/arendi/dist/search.js
500
+ var TAVILY_API_URL = "https://api.tavily.com/search";
501
+ var TAVILY_CACHE_TTL_MS = 864e5;
502
+ function buildCacheKey(query, includeDomains) {
503
+ const parts = [query];
504
+ if (includeDomains?.length) {
505
+ parts.push(...[...includeDomains].sort());
506
+ }
507
+ const hash = createHash("sha256").update(parts.join("|")).digest("hex");
508
+ return `tavily:${hash}`;
509
+ }
510
+ var GLOBAL_DOMAINS = /* @__PURE__ */ new Set([
511
+ "wikipedia.org",
512
+ "youtube.com",
513
+ "reddit.com",
514
+ "medium.com",
515
+ "facebook.com",
516
+ "twitter.com",
517
+ "x.com",
518
+ "linkedin.com",
519
+ "instagram.com",
520
+ "tiktok.com",
521
+ "quora.com",
522
+ "forbes.com",
523
+ "bloomberg.com",
524
+ "cnbc.com",
525
+ "bbc.com",
526
+ "cnn.com",
527
+ "reuters.com"
528
+ ]);
529
+ async function tavilySearch(query, apiKey, options) {
530
+ const { cache, cacheStats } = options ?? {};
531
+ if (cache) {
532
+ const key = buildCacheKey(query, options?.includeDomains);
533
+ try {
534
+ const cached = await cache.get(key);
535
+ if (cached !== null) {
536
+ if (cacheStats)
537
+ cacheStats.hits++;
538
+ return JSON.parse(cached);
539
+ }
540
+ } catch {
541
+ }
542
+ if (cacheStats)
543
+ cacheStats.misses++;
544
+ }
545
+ const body = {
546
+ api_key: apiKey,
547
+ query,
548
+ search_depth: "advanced",
549
+ max_results: 5,
550
+ include_answer: false
551
+ };
552
+ if (options?.includeDomains?.length) {
553
+ body["include_domains"] = options.includeDomains;
554
+ }
555
+ const response = await fetch(TAVILY_API_URL, {
556
+ method: "POST",
557
+ headers: {
558
+ "Content-Type": "application/json"
559
+ },
560
+ body: JSON.stringify(body)
561
+ });
562
+ if (!response.ok) {
563
+ const text = await response.text();
564
+ throw new Error(`Tavily search failed (${response.status}): ${text}`);
565
+ }
566
+ const data = await response.json();
567
+ const parsed = tavilyResponseSchema.safeParse(data);
568
+ if (!parsed.success) {
569
+ console.warn(`Warning: Could not parse Tavily response for "${query}"`);
570
+ return [];
571
+ }
572
+ const results = parsed.data.results;
573
+ if (cache) {
574
+ const key = buildCacheKey(query, options?.includeDomains);
575
+ try {
576
+ await cache.set(key, JSON.stringify(results), TAVILY_CACHE_TTL_MS);
577
+ } catch {
578
+ }
579
+ }
580
+ return results;
581
+ }
582
+ async function discoverLocalSources(options, cacheStats) {
583
+ const locationParts = [];
584
+ if (options.city)
585
+ locationParts.push(options.city);
586
+ if (options.country)
587
+ locationParts.push(options.country);
588
+ const location = locationParts.join(", ");
589
+ if (!location)
590
+ return [];
591
+ const discoveryQueries = [
592
+ `top local business news websites in ${location}`,
593
+ `popular local media sources ${location} economy startups`,
594
+ `${location} local online newspapers business publications`
595
+ ];
596
+ const allResults = await Promise.all(discoveryQueries.map((q) => tavilySearch(q, options.tavilyApiKey, {
597
+ cache: options.cache,
598
+ cacheStats
599
+ })));
600
+ const domains = /* @__PURE__ */ new Set();
601
+ for (const results of allResults) {
602
+ for (const result of results) {
603
+ try {
604
+ const url = new URL(result.url);
605
+ const hostname = url.hostname.replace(/^www\./, "");
606
+ if (!GLOBAL_DOMAINS.has(hostname)) {
607
+ domains.add(hostname);
608
+ }
609
+ } catch {
610
+ }
611
+ }
612
+ }
613
+ return Array.from(domains);
614
+ }
615
+ function buildLocalSearchQueries(options) {
616
+ const locationParts = [];
617
+ if (options.city)
618
+ locationParts.push(options.city);
619
+ if (options.country)
620
+ locationParts.push(options.country);
621
+ const location = locationParts.join(" ");
622
+ if (!location)
623
+ return [];
624
+ const cats = options.categories?.join(", ") ?? "";
625
+ const kw = options.keywords?.join(", ") ?? "";
626
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
627
+ const queries = [];
628
+ queries.push([`business opportunities trends`, cats, location, year].filter(Boolean).join(" "));
629
+ queries.push([`consumer needs problems`, cats, location, year].filter(Boolean).join(" "));
630
+ queries.push([`startup ecosystem market growth`, cats, location, year].filter(Boolean).join(" "));
631
+ if (kw) {
632
+ queries.push([kw, `market demand`, location, year].filter(Boolean).join(" "));
633
+ }
634
+ return queries;
635
+ }
636
+ function buildGlobalSearchQueries(options) {
637
+ const parts = [];
638
+ if (options.country)
639
+ parts.push(options.country);
640
+ if (options.city)
641
+ parts.push(options.city);
642
+ const location = parts.join(" ");
643
+ const cats = options.categories?.join(", ") ?? "";
644
+ const kw = options.keywords?.join(", ") ?? "";
645
+ const queries = [];
646
+ queries.push([
647
+ "trending business opportunities",
648
+ cats,
649
+ location,
650
+ (/* @__PURE__ */ new Date()).getFullYear()
651
+ ].filter(Boolean).join(" "));
652
+ queries.push(["consumer pain points", cats, location, (/* @__PURE__ */ new Date()).getFullYear()].filter(Boolean).join(" "));
653
+ queries.push(["market gaps underserved needs", cats, location].filter(Boolean).join(" "));
654
+ if (kw) {
655
+ queries.push(["emerging trends", kw, cats, "business", (/* @__PURE__ */ new Date()).getFullYear()].filter(Boolean).join(" "));
656
+ }
657
+ queries.push([
658
+ "successful business models trending globally",
659
+ cats,
660
+ (/* @__PURE__ */ new Date()).getFullYear()
661
+ ].filter(Boolean).join(" "));
662
+ return queries;
663
+ }
664
+ async function localResearch(options, localDomains, cacheStats) {
665
+ const queries = buildLocalSearchQueries(options);
666
+ if (queries.length === 0)
667
+ return [];
668
+ const searchOpts = {
669
+ includeDomains: localDomains.length > 0 ? localDomains : void 0,
670
+ cache: options.cache,
671
+ cacheStats
672
+ };
673
+ const results = await Promise.all(queries.map(async (query) => {
674
+ const res = await tavilySearch(query, options.tavilyApiKey, searchOpts);
675
+ return { query, results: res };
676
+ }));
677
+ return results;
678
+ }
679
+ async function globalResearch(options, cacheStats) {
680
+ const queries = buildGlobalSearchQueries(options);
681
+ const results = await Promise.all(queries.map(async (query) => {
682
+ const res = await tavilySearch(query, options.tavilyApiKey, {
683
+ cache: options.cache,
684
+ cacheStats
685
+ });
686
+ return { query, results: res };
687
+ }));
688
+ return results;
689
+ }
690
+ function summarizeContexts(contexts) {
691
+ return contexts.map((ctx) => {
692
+ const snippets = ctx.results.map((r) => `- [${r.title}](${r.url}): ${r.content}`).join("\n");
693
+ return `### Search: "${ctx.query}"
694
+ ${snippets || "No results found."}`;
695
+ }).join("\n\n");
696
+ }
697
+ function summarizeAllResearch(localContexts, globalContexts, trendsContext) {
698
+ const sections = [];
699
+ if (localContexts.length > 0) {
700
+ const localHasResults = localContexts.some((c) => c.results.length > 0);
701
+ if (localHasResults) {
702
+ sections.push(`## Local Sources
703
+
704
+ These results come from local and regional media in the target market.
705
+
706
+ ${summarizeContexts(localContexts)}`);
707
+ }
708
+ }
709
+ if (globalContexts.length > 0) {
710
+ sections.push(`## Global Sources
711
+
712
+ These results come from international and global publications.
713
+
714
+ ${summarizeContexts(globalContexts)}`);
715
+ }
716
+ if (trendsContext) {
717
+ const trendsSummary = summarizeTrends(trendsContext);
718
+ if (trendsSummary) {
719
+ sections.push(trendsSummary);
720
+ }
721
+ }
722
+ return sections.join("\n\n---\n\n");
723
+ }
724
+
725
+ // ../../packages/arendi/dist/generate.js
726
+ import { generateObject } from "ai";
727
+
728
+ // ../../packages/arendi/dist/provider.js
729
+ import { createOpenAI } from "@ai-sdk/openai";
730
+ import { createAnthropic } from "@ai-sdk/anthropic";
731
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
732
+ var DEFAULT_MODELS = {
733
+ openai: "gpt-4o",
734
+ anthropic: "claude-sonnet-4-20250514",
735
+ google: "gemini-2.0-flash"
736
+ };
737
+ function createModel(provider, apiKey, model) {
738
+ const modelId = model ?? DEFAULT_MODELS[provider];
739
+ switch (provider) {
740
+ case "openai": {
741
+ const openai = createOpenAI({ apiKey });
742
+ return openai(modelId);
743
+ }
744
+ case "anthropic": {
745
+ const anthropic = createAnthropic({ apiKey });
746
+ return anthropic(modelId);
747
+ }
748
+ case "google": {
749
+ const google = createGoogleGenerativeAI({ apiKey });
750
+ return google(modelId);
751
+ }
752
+ }
753
+ }
754
+
755
+ // ../../packages/arendi/dist/generate.js
756
+ function buildSystemPrompt() {
757
+ return `You are a world-class business strategist and market analyst. Your job is to
758
+ analyze real-time research data and generate actionable, creative, and well-reasoned
759
+ business ideas and opportunities.
760
+
761
+ You will receive THREE types of research data:
762
+
763
+ 1. **Local Sources** \u2013 Information from local and regional media, news outlets, and
764
+ publications in the target market. These provide on-the-ground insights about what
765
+ is actually happening in the specific country, city, or region.
766
+ 2. **Global Sources** \u2013 Information from international publications and global trend
767
+ reports. These provide a broader view of worldwide trends, successful models, and
768
+ emerging opportunities.
769
+ 3. **Google Trends Data** \u2013 Real-time search demand signals from Google Trends showing
770
+ what people are actively searching for right now, which queries are spiking
771
+ ("Breakout" = massive growth from near-zero), and which topics are rising in
772
+ popularity. This data reveals actual consumer intent and demand.
773
+
774
+ **Local sources should be your PRIMARY foundation.** Use them to understand the real
775
+ conditions, needs, pain points, and cultural context of the target market. Then use
776
+ global sources to identify transferable models, worldwide trends that could be
777
+ localized, and broader market sizing data. Use Google Trends data to validate demand
778
+ signals \u2014 rising search queries indicate real consumer interest and unmet needs.
779
+
780
+ When analyzing the research data you MUST consider:
781
+
782
+ 1. **Local context first** \u2013 What do local sources say about the market? What problems
783
+ are people actually talking about on the ground?
784
+ 2. **Trending topics** \u2013 What is gaining popularity and momentum right now? Cross-
785
+ reference Google Trends rising queries with news articles for strongest signals.
786
+ 3. **Pain points** \u2013 What are consumers or businesses struggling with? What frustrations
787
+ exist that are not being adequately addressed?
788
+ 4. **Market gaps** \u2013 Where is there underserved demand? What products or services are
789
+ missing or poorly executed? Rising search queries with no clear market leader are
790
+ strong gap indicators.
791
+ 5. **Trend transfers** \u2013 What successful business models or trends in other countries,
792
+ cities, or industries could be adapted to the target market? Use local sources to
793
+ validate whether a transfer makes sense culturally and economically.
794
+
795
+ For each idea you produce:
796
+ - Ground it in the research data provided. Prefer citing local sources when available.
797
+ - Be specific about the target audience (demographics, psychographics).
798
+ - Explain WHY this is a good opportunity RIGHT NOW (timing, market conditions).
799
+ Reference Google Trends data where it supports the timing argument.
800
+ - Provide realistic, concrete first steps someone could take this week.
801
+ - Assess difficulty and revenue potential honestly.
802
+ - Include source URLs from the research \u2014 prioritize local source URLs but include
803
+ global ones where they add supporting evidence.
804
+
805
+ Aim for diversity across opportunity types. Do not produce generic ideas like
806
+ "start a blog" or "become a freelancer" \u2014 be specific and creative.`;
807
+ }
808
+ function buildUserPrompt(options, researchSummary) {
809
+ const contextParts = [];
810
+ if (options.country)
811
+ contextParts.push(`Country: ${options.country}`);
812
+ if (options.city)
813
+ contextParts.push(`City: ${options.city}`);
814
+ if (options.categories?.length)
815
+ contextParts.push(`Categories: ${options.categories.join(", ")}`);
816
+ if (options.keywords?.length)
817
+ contextParts.push(`Keywords: ${options.keywords.join(", ")}`);
818
+ const context = contextParts.length > 0 ? `## Target Market
819
+ ${contextParts.join("\n")}` : "## Target Market\nNo specific market constraints provided \u2014 generate globally relevant ideas.";
820
+ return `${context}
821
+
822
+ ## Research Data
823
+
824
+ The following research was gathered from the web just now. It is divided into
825
+ **Local Sources** (from the target market's own media), **Global Sources**
826
+ (from international publications), and **Google Trends Data** (real-time search
827
+ demand signals). Prioritize local insights when forming ideas, use global sources
828
+ for supporting evidence, and use Google Trends data to validate demand and timing.
829
+
830
+ ${researchSummary}
831
+
832
+ ## Task
833
+
834
+ Generate exactly ${options.limit} business ideas or opportunities based on the research data
835
+ and target market above. Make sure each idea is distinct and covers a different angle
836
+ or opportunity type.
837
+
838
+ When citing references, prefer local source URLs where applicable. Each idea should
839
+ ideally reference at least one local source if the research data provides relevant ones.
840
+ Where Google Trends data shows rising demand for a topic, mention it in the reasoning.`;
841
+ }
842
+ async function generate(options, researchSummary) {
843
+ const model = createModel(options.provider, options.apiKey, options.model);
844
+ const { object } = await generateObject({
845
+ model,
846
+ schema: arendiResultSchema,
847
+ system: buildSystemPrompt(),
848
+ prompt: buildUserPrompt(options, researchSummary)
849
+ });
850
+ return object.ideas;
851
+ }
852
+
853
+ // ../../packages/arendi/dist/index.js
854
+ import { randomUUID } from "crypto";
855
+
856
+ // ../../packages/arendi/dist/cache.js
857
+ function createCacheStats() {
858
+ return { hits: 0, misses: 0 };
859
+ }
860
+
861
+ // ../../packages/arendi/dist/index.js
862
+ async function generateIdeas(rawOptions, callbacks) {
863
+ const runId = randomUUID();
864
+ const runStart = performance.now();
865
+ const { cache, store, ...serializableOptions } = rawOptions;
866
+ const options = {
867
+ ...optionsSchema.parse(serializableOptions),
868
+ cache,
869
+ store
870
+ };
871
+ const cacheStats = createCacheStats();
872
+ callbacks?.onSourceDiscoveryStart?.();
873
+ const sourceDiscoveryStart = performance.now();
874
+ const localDomains = await discoverLocalSources(options, cacheStats);
875
+ const sourceDiscoveryDuration = performance.now() - sourceDiscoveryStart;
876
+ callbacks?.onSourceDiscoveryComplete?.(localDomains);
877
+ callbacks?.onLocalResearchStart?.();
878
+ callbacks?.onGlobalResearchStart?.();
879
+ callbacks?.onTrendsResearchStart?.();
880
+ const parallelStart = performance.now();
881
+ const [localContexts, globalContexts, trendsContext] = await Promise.all([
882
+ localResearch(options, localDomains, cacheStats),
883
+ globalResearch(options, cacheStats),
884
+ trendsResearch(options)
885
+ ]);
886
+ const parallelDuration = performance.now() - parallelStart;
887
+ const localResultCount = localContexts.reduce((sum, c) => sum + c.results.length, 0);
888
+ callbacks?.onLocalResearchComplete?.(localContexts.length, localResultCount);
889
+ const globalResultCount = globalContexts.reduce((sum, c) => sum + c.results.length, 0);
890
+ callbacks?.onGlobalResearchComplete?.(globalContexts.length, globalResultCount);
891
+ callbacks?.onTrendsResearchComplete?.(trendsContext);
892
+ const researchSummary = summarizeAllResearch(localContexts, globalContexts, trendsContext);
893
+ callbacks?.onGenerateStart?.();
894
+ const generateStart = performance.now();
895
+ const ideas = await generate(options, researchSummary);
896
+ const generateDuration = performance.now() - generateStart;
897
+ callbacks?.onGenerateComplete?.(ideas.length);
898
+ const totalDurationMs = Math.round(performance.now() - runStart);
899
+ if (store) {
900
+ const runData = {
901
+ id: runId,
902
+ createdAt: /* @__PURE__ */ new Date(),
903
+ options: {
904
+ country: options.country,
905
+ city: options.city,
906
+ categories: options.categories,
907
+ keywords: options.keywords,
908
+ limit: options.limit,
909
+ provider: options.provider,
910
+ model: options.model,
911
+ geo: options.geo
912
+ },
913
+ sourceDiscovery: {
914
+ domains: localDomains,
915
+ durationMs: Math.round(sourceDiscoveryDuration)
916
+ },
917
+ localResearch: {
918
+ contexts: localContexts,
919
+ durationMs: Math.round(parallelDuration)
920
+ },
921
+ globalResearch: {
922
+ contexts: globalContexts,
923
+ durationMs: Math.round(parallelDuration)
924
+ },
925
+ trendsResearch: {
926
+ context: trendsContext,
927
+ durationMs: Math.round(parallelDuration)
928
+ },
929
+ generation: {
930
+ ideas,
931
+ durationMs: Math.round(generateDuration)
932
+ },
933
+ researchSummary,
934
+ totalDurationMs,
935
+ cacheStats: { ...cacheStats }
936
+ };
937
+ store.save(runData).catch((err) => {
938
+ console.warn("Warning: Failed to save run data:", err instanceof Error ? err.message : String(err));
939
+ });
940
+ }
941
+ return { ideas, runId };
942
+ }
943
+
944
+ // ../../packages/sqlite/dist/index.js
945
+ import Database from "better-sqlite3";
946
+
947
+ // ../../packages/sqlite/dist/schema.js
948
+ function initSchema(db) {
949
+ db.exec(`
950
+ -- Tavily search result cache
951
+ CREATE TABLE IF NOT EXISTS cache (
952
+ key TEXT PRIMARY KEY,
953
+ value TEXT NOT NULL,
954
+ created_at INTEGER NOT NULL DEFAULT (unixepoch('now') * 1000),
955
+ expires_at INTEGER
956
+ );
957
+
958
+ -- Run metadata
959
+ CREATE TABLE IF NOT EXISTS runs (
960
+ id TEXT PRIMARY KEY,
961
+ created_at INTEGER NOT NULL,
962
+ options TEXT NOT NULL,
963
+ research_summary TEXT,
964
+ total_duration_ms INTEGER,
965
+ cache_hits INTEGER DEFAULT 0,
966
+ cache_misses INTEGER DEFAULT 0
967
+ );
968
+
969
+ -- Discovered local source domains
970
+ CREATE TABLE IF NOT EXISTS run_sources (
971
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
972
+ run_id TEXT NOT NULL REFERENCES runs(id),
973
+ domain TEXT NOT NULL
974
+ );
975
+
976
+ -- Individual Tavily searches executed during a run
977
+ CREATE TABLE IF NOT EXISTS run_searches (
978
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
979
+ run_id TEXT NOT NULL REFERENCES runs(id),
980
+ phase TEXT NOT NULL,
981
+ query TEXT NOT NULL,
982
+ include_domains TEXT,
983
+ duration_ms INTEGER,
984
+ cache_hit INTEGER DEFAULT 0
985
+ );
986
+
987
+ -- Results returned by each Tavily search
988
+ CREATE TABLE IF NOT EXISTS run_search_results (
989
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
990
+ search_id INTEGER NOT NULL REFERENCES run_searches(id),
991
+ title TEXT NOT NULL,
992
+ url TEXT NOT NULL,
993
+ content TEXT NOT NULL,
994
+ score REAL
995
+ );
996
+
997
+ -- Google Trends data snapshots
998
+ CREATE TABLE IF NOT EXISTS run_trends (
999
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1000
+ run_id TEXT NOT NULL REFERENCES runs(id),
1001
+ type TEXT NOT NULL,
1002
+ data TEXT NOT NULL
1003
+ );
1004
+
1005
+ -- Generated business ideas
1006
+ CREATE TABLE IF NOT EXISTS run_ideas (
1007
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1008
+ run_id TEXT NOT NULL REFERENCES runs(id),
1009
+ title TEXT NOT NULL,
1010
+ description TEXT NOT NULL,
1011
+ category TEXT,
1012
+ target_audience TEXT,
1013
+ pain_point TEXT,
1014
+ opportunity_type TEXT,
1015
+ estimated_difficulty TEXT,
1016
+ potential_revenue TEXT,
1017
+ reasoning TEXT,
1018
+ actionable_steps TEXT,
1019
+ refs TEXT
1020
+ );
1021
+
1022
+ -- Indexes for common queries
1023
+ CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
1024
+ CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
1025
+ CREATE INDEX IF NOT EXISTS idx_run_sources_run ON run_sources(run_id);
1026
+ CREATE INDEX IF NOT EXISTS idx_run_searches_run ON run_searches(run_id);
1027
+ CREATE INDEX IF NOT EXISTS idx_run_search_results_search ON run_search_results(search_id);
1028
+ CREATE INDEX IF NOT EXISTS idx_run_trends_run ON run_trends(run_id);
1029
+ CREATE INDEX IF NOT EXISTS idx_run_ideas_run ON run_ideas(run_id);
1030
+ `);
1031
+ }
1032
+
1033
+ // ../../packages/sqlite/dist/cache.js
1034
+ var SqliteCacheProvider = class {
1035
+ db;
1036
+ stmtGet;
1037
+ stmtSet;
1038
+ stmtDelete;
1039
+ constructor(db) {
1040
+ this.db = db;
1041
+ this.stmtGet = this.db.prepare("SELECT value, expires_at FROM cache WHERE key = ?");
1042
+ this.stmtSet = this.db.prepare("INSERT OR REPLACE INTO cache (key, value, created_at, expires_at) VALUES (?, ?, ?, ?)");
1043
+ this.stmtDelete = this.db.prepare("DELETE FROM cache WHERE key = ?");
1044
+ }
1045
+ async get(key) {
1046
+ const row = this.stmtGet.get(key);
1047
+ if (!row)
1048
+ return null;
1049
+ if (row.expires_at !== null && Date.now() > row.expires_at) {
1050
+ this.stmtDelete.run(key);
1051
+ return null;
1052
+ }
1053
+ return row.value;
1054
+ }
1055
+ async set(key, value, ttlMs) {
1056
+ const now = Date.now();
1057
+ const expiresAt = ttlMs != null ? now + ttlMs : null;
1058
+ this.stmtSet.run(key, value, now, expiresAt);
1059
+ }
1060
+ /**
1061
+ * Remove all expired entries. Call periodically if the database grows large.
1062
+ */
1063
+ prune() {
1064
+ const result = this.db.prepare("DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at < ?").run(Date.now());
1065
+ return result.changes;
1066
+ }
1067
+ };
1068
+
1069
+ // ../../packages/sqlite/dist/store.js
1070
+ var SqliteRunStore = class {
1071
+ db;
1072
+ constructor(db) {
1073
+ this.db = db;
1074
+ }
1075
+ // -------------------------------------------------------------------------
1076
+ // save – persists a full RunData in a single transaction
1077
+ // -------------------------------------------------------------------------
1078
+ async save(run) {
1079
+ const tx = this.db.transaction(() => {
1080
+ this.db.prepare(`INSERT INTO runs (id, created_at, options, research_summary, total_duration_ms, cache_hits, cache_misses)
1081
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(run.id, run.createdAt.getTime(), JSON.stringify(run.options), run.researchSummary, run.totalDurationMs, run.cacheStats.hits, run.cacheStats.misses);
1082
+ const insertSource = this.db.prepare("INSERT INTO run_sources (run_id, domain) VALUES (?, ?)");
1083
+ for (const domain of run.sourceDiscovery.domains) {
1084
+ insertSource.run(run.id, domain);
1085
+ }
1086
+ const insertSearch = this.db.prepare(`INSERT INTO run_searches (run_id, phase, query, include_domains, duration_ms)
1087
+ VALUES (?, ?, ?, ?, ?)`);
1088
+ const insertResult = this.db.prepare(`INSERT INTO run_search_results (search_id, title, url, content, score)
1089
+ VALUES (?, ?, ?, ?, ?)`);
1090
+ const writeContexts = (contexts, phase, durationMs) => {
1091
+ for (const ctx of contexts) {
1092
+ const info = insertSearch.run(run.id, phase, ctx.query, null, durationMs);
1093
+ const searchId = info.lastInsertRowid;
1094
+ for (const r of ctx.results) {
1095
+ insertResult.run(searchId, r.title, r.url, r.content, r.score);
1096
+ }
1097
+ }
1098
+ };
1099
+ writeContexts(run.localResearch.contexts, "local", run.localResearch.durationMs);
1100
+ writeContexts(run.globalResearch.contexts, "global", run.globalResearch.durationMs);
1101
+ const insertTrend = this.db.prepare("INSERT INTO run_trends (run_id, type, data) VALUES (?, ?, ?)");
1102
+ const trends = run.trendsResearch.context;
1103
+ if (trends.dailyTrends.length > 0) {
1104
+ insertTrend.run(run.id, "daily", JSON.stringify(trends.dailyTrends));
1105
+ }
1106
+ if (trends.relatedQueries.length > 0) {
1107
+ insertTrend.run(run.id, "related_queries", JSON.stringify(trends.relatedQueries));
1108
+ }
1109
+ if (trends.relatedTopics.length > 0) {
1110
+ insertTrend.run(run.id, "related_topics", JSON.stringify(trends.relatedTopics));
1111
+ }
1112
+ const insertIdea = this.db.prepare(`INSERT INTO run_ideas
1113
+ (run_id, title, description, category, target_audience, pain_point,
1114
+ opportunity_type, estimated_difficulty, potential_revenue, reasoning,
1115
+ actionable_steps, refs)
1116
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1117
+ for (const idea of run.generation.ideas) {
1118
+ insertIdea.run(run.id, idea.title, idea.description, idea.category, idea.targetAudience, idea.painPoint, idea.opportunityType, idea.estimatedDifficulty, idea.potentialRevenue, idea.reasoning, JSON.stringify(idea.actionableSteps), JSON.stringify(idea.references));
1119
+ }
1120
+ });
1121
+ tx();
1122
+ }
1123
+ // -------------------------------------------------------------------------
1124
+ // get – reconstructs a full RunData from the database
1125
+ // -------------------------------------------------------------------------
1126
+ async get(id) {
1127
+ const row = this.db.prepare("SELECT * FROM runs WHERE id = ?").get(id);
1128
+ if (!row)
1129
+ return null;
1130
+ return this.hydrateRun(row);
1131
+ }
1132
+ // -------------------------------------------------------------------------
1133
+ // list – returns runs in reverse-chronological order
1134
+ // -------------------------------------------------------------------------
1135
+ async list(options) {
1136
+ const limit = options?.limit ?? 20;
1137
+ const offset = options?.offset ?? 0;
1138
+ const rows = this.db.prepare("SELECT * FROM runs ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
1139
+ return rows.map((row) => this.hydrateRun(row));
1140
+ }
1141
+ // -------------------------------------------------------------------------
1142
+ // Private helpers
1143
+ // -------------------------------------------------------------------------
1144
+ hydrateRun(row) {
1145
+ const id = row.id;
1146
+ const sources = this.db.prepare("SELECT domain FROM run_sources WHERE run_id = ?").all(id);
1147
+ const searches = this.db.prepare("SELECT * FROM run_searches WHERE run_id = ? ORDER BY id").all(id);
1148
+ const localContexts = [];
1149
+ const globalContexts = [];
1150
+ let localDurationMs = 0;
1151
+ let globalDurationMs = 0;
1152
+ for (const s of searches) {
1153
+ const results = this.db.prepare("SELECT title, url, content, score FROM run_search_results WHERE search_id = ? ORDER BY id").all(s.id);
1154
+ const ctx = { query: s.query, results };
1155
+ if (s.phase === "local") {
1156
+ localContexts.push(ctx);
1157
+ localDurationMs = s.duration_ms ?? 0;
1158
+ } else if (s.phase === "global") {
1159
+ globalContexts.push(ctx);
1160
+ globalDurationMs = s.duration_ms ?? 0;
1161
+ }
1162
+ }
1163
+ const trendRows = this.db.prepare("SELECT type, data FROM run_trends WHERE run_id = ?").all(id);
1164
+ const trendsContext = {
1165
+ dailyTrends: [],
1166
+ relatedQueries: [],
1167
+ relatedTopics: []
1168
+ };
1169
+ for (const t of trendRows) {
1170
+ if (t.type === "daily") {
1171
+ trendsContext.dailyTrends = JSON.parse(t.data);
1172
+ } else if (t.type === "related_queries") {
1173
+ trendsContext.relatedQueries = JSON.parse(t.data);
1174
+ } else if (t.type === "related_topics") {
1175
+ trendsContext.relatedTopics = JSON.parse(t.data);
1176
+ }
1177
+ }
1178
+ const ideaRows = this.db.prepare("SELECT * FROM run_ideas WHERE run_id = ? ORDER BY id").all(id);
1179
+ const ideas = ideaRows.map((i) => ({
1180
+ title: i.title,
1181
+ description: i.description,
1182
+ category: i.category ?? "",
1183
+ targetAudience: i.target_audience ?? "",
1184
+ painPoint: i.pain_point ?? "",
1185
+ opportunityType: i.opportunity_type ?? "trending",
1186
+ estimatedDifficulty: i.estimated_difficulty ?? "medium",
1187
+ potentialRevenue: i.potential_revenue ?? "medium",
1188
+ reasoning: i.reasoning ?? "",
1189
+ actionableSteps: i.actionable_steps ? JSON.parse(i.actionable_steps) : [],
1190
+ references: i.refs ? JSON.parse(i.refs) : []
1191
+ }));
1192
+ return {
1193
+ id: row.id,
1194
+ createdAt: new Date(row.created_at),
1195
+ options: JSON.parse(row.options),
1196
+ sourceDiscovery: {
1197
+ domains: sources.map((s) => s.domain),
1198
+ durationMs: 0
1199
+ // Not stored per-phase for discovery — captured in total
1200
+ },
1201
+ localResearch: {
1202
+ contexts: localContexts,
1203
+ durationMs: localDurationMs
1204
+ },
1205
+ globalResearch: {
1206
+ contexts: globalContexts,
1207
+ durationMs: globalDurationMs
1208
+ },
1209
+ trendsResearch: {
1210
+ context: trendsContext,
1211
+ durationMs: 0
1212
+ },
1213
+ generation: {
1214
+ ideas,
1215
+ durationMs: 0
1216
+ },
1217
+ researchSummary: row.research_summary ?? "",
1218
+ totalDurationMs: row.total_duration_ms ?? 0,
1219
+ cacheStats: {
1220
+ hits: row.cache_hits ?? 0,
1221
+ misses: row.cache_misses ?? 0
1222
+ }
1223
+ };
1224
+ }
1225
+ };
1226
+
1227
+ // ../../packages/sqlite/dist/index.js
1228
+ function createSqliteProviders(dbPath) {
1229
+ const db = new Database(dbPath);
1230
+ db.pragma("journal_mode = WAL");
1231
+ db.pragma("foreign_keys = ON");
1232
+ initSchema(db);
1233
+ const cache = new SqliteCacheProvider(db);
1234
+ const store = new SqliteRunStore(db);
1235
+ return {
1236
+ cache,
1237
+ store,
1238
+ close: () => db.close()
1239
+ };
1240
+ }
1241
+
1242
+ // src/cli.ts
1243
+ var program = new Command();
1244
+ program.name("arendi").description(
1245
+ "AI-powered business idea generator. Researches trending topics, pain points, and market gaps, then generates actionable business ideas."
1246
+ ).version("0.1.0").option("--country <country>", "Target country (e.g., US, Indonesia)").option("--city <city>", "Target city (e.g., Jakarta, New York)").option(
1247
+ "--categories <categories>",
1248
+ `Comma-separated categories: ${CATEGORIES.join(", ")}`
1249
+ ).option("--limit <number>", "Number of ideas to generate", "10").option("--keywords <keywords>", "Comma-separated keywords to focus on").option(
1250
+ "--format <format>",
1251
+ `Output format: ${OUTPUT_FORMATS.join(", ")}`,
1252
+ "markdown"
1253
+ ).option("--output <file>", "Write output to file instead of stdout").option(
1254
+ "--provider <provider>",
1255
+ `AI provider: ${PROVIDERS.join(", ")}`,
1256
+ "openai"
1257
+ ).option(
1258
+ "--model <model>",
1259
+ "Model name override (e.g., gpt-4o, claude-sonnet-4-20250514)"
1260
+ ).option(
1261
+ "--geo <code>",
1262
+ "ISO 3166-1 alpha-2 country code for Google Trends (e.g., US, ID). Auto-detected from --country if omitted."
1263
+ ).option(
1264
+ "--db <path>",
1265
+ "Path to the SQLite database for caching and run history",
1266
+ resolve(homedir(), ".arendi", "arendi.db")
1267
+ ).option("--no-cache", "Disable Tavily search result caching").option("--no-store", "Disable run history persistence");
1268
+ function getApiKey(provider) {
1269
+ const envMap = {
1270
+ openai: "OPENAI_API_KEY",
1271
+ anthropic: "ANTHROPIC_API_KEY",
1272
+ google: "GOOGLE_GENERATIVE_AI_API_KEY"
1273
+ };
1274
+ const envVar = envMap[provider];
1275
+ const key = process.env[envVar];
1276
+ if (!key) {
1277
+ console.error(
1278
+ `Error: Missing environment variable ${envVar} for provider "${provider}".`
1279
+ );
1280
+ console.error(`Set it via: export ${envVar}=your-key-here`);
1281
+ process.exit(1);
1282
+ }
1283
+ return key;
1284
+ }
1285
+ function getTavilyApiKey() {
1286
+ const key = process.env["TAVILY_API_KEY"];
1287
+ if (!key) {
1288
+ console.error("Error: Missing environment variable TAVILY_API_KEY.");
1289
+ console.error("Get a free key at https://tavily.com and set it via:");
1290
+ console.error(" export TAVILY_API_KEY=your-key-here");
1291
+ process.exit(1);
1292
+ }
1293
+ return key;
1294
+ }
1295
+ program.action(async (opts) => {
1296
+ const provider = opts["provider"] ?? "openai";
1297
+ const format = opts["format"] ?? "markdown";
1298
+ const limit = parseInt(String(opts["limit"] ?? "5"), 10);
1299
+ if (!PROVIDERS.includes(provider)) {
1300
+ console.error(
1301
+ `Error: Invalid provider "${provider}". Choose from: ${PROVIDERS.join(", ")}`
1302
+ );
1303
+ process.exit(1);
1304
+ }
1305
+ if (!OUTPUT_FORMATS.includes(format)) {
1306
+ console.error(
1307
+ `Error: Invalid format "${format}". Choose from: ${OUTPUT_FORMATS.join(", ")}`
1308
+ );
1309
+ process.exit(1);
1310
+ }
1311
+ if (isNaN(limit) || limit < 1 || limit > 50) {
1312
+ console.error("Error: --limit must be a number between 1 and 50.");
1313
+ process.exit(1);
1314
+ }
1315
+ const apiKey = getApiKey(provider);
1316
+ const tavilyApiKey = getTavilyApiKey();
1317
+ const categories = opts["categories"]?.split(",").map((c) => c.trim()).filter(Boolean);
1318
+ const keywords = opts["keywords"]?.split(",").map((k) => k.trim()).filter(Boolean);
1319
+ const dbPath = opts["db"];
1320
+ const useCache = opts["cache"] !== false;
1321
+ const useStore = opts["store"] !== false;
1322
+ let cache;
1323
+ let store;
1324
+ let closeDb;
1325
+ if (useCache || useStore) {
1326
+ try {
1327
+ mkdirSync(dirname(dbPath), { recursive: true });
1328
+ const providers = createSqliteProviders(dbPath);
1329
+ if (useCache) cache = providers.cache;
1330
+ if (useStore) store = providers.store;
1331
+ closeDb = providers.close;
1332
+ console.error(`Database: ${dbPath}`);
1333
+ } catch (err) {
1334
+ console.warn(
1335
+ "Warning: Could not open SQLite database, proceeding without cache/store:",
1336
+ err instanceof Error ? err.message : String(err)
1337
+ );
1338
+ }
1339
+ }
1340
+ try {
1341
+ const hasLocation = opts["country"] || opts["city"];
1342
+ const { ideas, runId } = await generateIdeas(
1343
+ {
1344
+ country: opts["country"],
1345
+ city: opts["city"],
1346
+ categories,
1347
+ limit,
1348
+ keywords,
1349
+ provider,
1350
+ model: opts["model"],
1351
+ apiKey,
1352
+ tavilyApiKey,
1353
+ geo: opts["geo"],
1354
+ cache,
1355
+ store
1356
+ },
1357
+ {
1358
+ onSourceDiscoveryStart() {
1359
+ if (hasLocation) {
1360
+ console.error("Discovering local sources for the target market...");
1361
+ }
1362
+ },
1363
+ onSourceDiscoveryComplete(sources) {
1364
+ if (sources.length > 0) {
1365
+ console.error(
1366
+ `Found ${sources.length} local sources: ${sources.join(", ")}`
1367
+ );
1368
+ } else if (hasLocation) {
1369
+ console.error(
1370
+ "No local sources discovered \u2014 will rely on global research."
1371
+ );
1372
+ }
1373
+ },
1374
+ onLocalResearchStart() {
1375
+ if (hasLocation) {
1376
+ console.error("Researching local trends and opportunities...");
1377
+ }
1378
+ },
1379
+ onLocalResearchComplete(queryCount, resultCount) {
1380
+ if (hasLocation) {
1381
+ console.error(
1382
+ `Local research complete: ${queryCount} queries, ${resultCount} results.`
1383
+ );
1384
+ }
1385
+ },
1386
+ onGlobalResearchStart() {
1387
+ console.error(
1388
+ "Researching global trends, pain points, and market gaps..."
1389
+ );
1390
+ },
1391
+ onGlobalResearchComplete(queryCount, resultCount) {
1392
+ console.error(
1393
+ `Global research complete: ${queryCount} queries, ${resultCount} results.`
1394
+ );
1395
+ },
1396
+ onTrendsResearchStart() {
1397
+ console.error(
1398
+ "Fetching Google Trends data (daily trends, rising queries)..."
1399
+ );
1400
+ },
1401
+ onTrendsResearchComplete(trendsContext) {
1402
+ const parts = [];
1403
+ if (trendsContext.dailyTrends.length > 0) {
1404
+ parts.push(
1405
+ `${trendsContext.dailyTrends.length} daily trending searches`
1406
+ );
1407
+ }
1408
+ if (trendsContext.relatedQueries.length > 0) {
1409
+ const risingCount = trendsContext.relatedQueries.reduce(
1410
+ (sum, r) => sum + r.rising.length,
1411
+ 0
1412
+ );
1413
+ parts.push(`${risingCount} rising queries`);
1414
+ }
1415
+ if (trendsContext.relatedTopics.length > 0) {
1416
+ const risingCount = trendsContext.relatedTopics.reduce(
1417
+ (sum, r) => sum + r.rising.length,
1418
+ 0
1419
+ );
1420
+ parts.push(`${risingCount} rising topics`);
1421
+ }
1422
+ if (parts.length > 0) {
1423
+ console.error(`Google Trends complete: ${parts.join(", ")}.`);
1424
+ } else {
1425
+ console.error(
1426
+ "Google Trends: no data available (geo may not be supported)."
1427
+ );
1428
+ }
1429
+ },
1430
+ onGenerateStart() {
1431
+ console.error(`Generating ${limit} business ideas...`);
1432
+ },
1433
+ onGenerateComplete(ideaCount) {
1434
+ console.error(`Generated ${ideaCount} ideas.`);
1435
+ }
1436
+ }
1437
+ );
1438
+ if (store) {
1439
+ console.error(`Run saved: ${runId}`);
1440
+ }
1441
+ const output = formatIdeas(ideas, format);
1442
+ if (opts["output"]) {
1443
+ const filePath = resolve(opts["output"]);
1444
+ writeFileSync(filePath, output, "utf-8");
1445
+ console.error(`Output written to ${filePath}`);
1446
+ } else {
1447
+ console.log(output);
1448
+ }
1449
+ } catch (error) {
1450
+ const message = error instanceof Error ? error.message : String(error);
1451
+ console.error(`Error: ${message}`);
1452
+ process.exit(1);
1453
+ } finally {
1454
+ closeDb?.();
1455
+ }
1456
+ });
1457
+
1458
+ // src/index.ts
1459
+ program.parse();