hearback 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/bin/hearback.js +121 -20
  2. package/package.json +1 -1
package/bin/hearback.js CHANGED
@@ -279,15 +279,60 @@ function openBrowser(url) {
279
279
  }
280
280
  }
281
281
 
282
+ /**
283
+ * Normalize any common GitHub repo reference to `"owner/repo"`.
284
+ * Accepts:
285
+ * owner/repo
286
+ * github.com/owner/repo
287
+ * https://github.com/owner/repo(.git)?(/...)?
288
+ * git@github.com:owner/repo.git
289
+ * Returns null if the input can't be parsed as a valid repo ref.
290
+ */
291
+ function parseRepoInput(input) {
292
+ if (!input) return null;
293
+ let s = String(input).trim();
294
+ if (!s) return null;
295
+
296
+ // git@github.com:owner/repo(.git)
297
+ const sshMatch = s.match(/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/);
298
+ if (sshMatch) return `${sshMatch[1]}/${sshMatch[2]}`;
299
+
300
+ // Strip common URL prefixes
301
+ s = s.replace(/^https?:\/\//, '').replace(/^github\.com\//, '');
302
+
303
+ // Trim trailing slashes
304
+ s = s.replace(/\/+$/, '');
305
+
306
+ // Strip trailing .git
307
+ s = s.replace(/\.git$/, '');
308
+
309
+ // Keep only the first two path segments
310
+ const parts = s.split('/').filter(Boolean);
311
+ if (parts.length < 2) return null;
312
+ const [owner, repo] = parts;
313
+
314
+ // Minimal validity check on owner/repo
315
+ const valid = /^[A-Za-z0-9._-]+$/;
316
+ if (!valid.test(owner) || !valid.test(repo)) return null;
317
+
318
+ return `${owner}/${repo}`;
319
+ }
320
+
282
321
  /**
283
322
  * Build a GitHub fine-grained token URL with as much pre-filled as possible.
284
- * Sadly GitHub does NOT support pre-filling permissions via URL params, but
285
- * we can pre-fill the name + description to save time.
323
+ * GitHub's template URL supports name/description/target_name/expires_in plus
324
+ * a subset of permissions (we need issues + contents read/write).
325
+ * Note: fine-grained tokens with read-write permissions max out at 90 days.
286
326
  */
287
- function buildTokenUrl(repoName) {
327
+ function buildTokenUrl(repoFullName) {
328
+ const [owner, name] = repoFullName.split('/');
288
329
  const params = new URLSearchParams({
289
- name: `hearback-${repoName.split('/').pop()}`,
330
+ name: `hearback-${name}`,
290
331
  description: 'Token for hearback feedback widget',
332
+ target_name: owner,
333
+ expires_in: '90',
334
+ contents: 'write',
335
+ issues: 'write',
291
336
  });
292
337
  return `https://github.com/settings/personal-access-tokens/new?${params}`;
293
338
  }
@@ -319,10 +364,40 @@ function detectProject(cwd) {
319
364
  const NEXTJS_ROUTE = `import { NextRequest, NextResponse } from 'next/server';
320
365
  import { createFeedbackHandler } from 'hearback-server';
321
366
 
367
+ // Auto-detect LLM provider from env — falls through to Form mode if none set.
368
+ const llmConfig = (() => {
369
+ if (process.env.OPENAI_API_KEY) {
370
+ return { apiKey: process.env.OPENAI_API_KEY };
371
+ }
372
+ if (process.env.ANTHROPIC_API_KEY) {
373
+ return {
374
+ apiKey: process.env.ANTHROPIC_API_KEY,
375
+ baseUrl: 'https://api.anthropic.com/v1',
376
+ model: 'claude-sonnet-4-6',
377
+ };
378
+ }
379
+ if (process.env.OPENROUTER_API_KEY) {
380
+ return {
381
+ apiKey: process.env.OPENROUTER_API_KEY,
382
+ baseUrl: 'https://openrouter.ai/api/v1',
383
+ model: 'moonshotai/kimi-k2',
384
+ };
385
+ }
386
+ // Escape hatch: custom OpenAI-compatible provider
387
+ if (process.env.LLM_API_KEY) {
388
+ return {
389
+ apiKey: process.env.LLM_API_KEY,
390
+ baseUrl: process.env.LLM_BASE_URL,
391
+ model: process.env.LLM_MODEL,
392
+ };
393
+ }
394
+ return undefined;
395
+ })();
396
+
322
397
  const handler = createFeedbackHandler({
323
398
  repo: process.env.FEEDBACK_REPO!,
324
399
  githubToken: process.env.GITHUB_TOKEN!,
325
- llm: process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : undefined,
400
+ llm: llmConfig,
326
401
  });
327
402
 
328
403
  async function handleRequest(req: NextRequest) {
@@ -426,9 +501,19 @@ ${BOLD}Usage:${RESET}
426
501
 
427
502
  // Step 2: Collect info
428
503
  step(2, 'Configuration');
429
- const repo = await ask('GitHub repo for issues (owner/name):');
430
- if (!repo || !repo.includes('/')) {
431
- warn('Invalid repo format. Expected: owner/name');
504
+ let repo = null;
505
+ for (let attempt = 0; attempt < 3 && !repo; attempt++) {
506
+ const raw = await ask('GitHub repo for issues (owner/name or URL):');
507
+ repo = parseRepoInput(raw);
508
+ if (!repo) {
509
+ warn('Could not parse that. Examples:');
510
+ info(' owner/repo');
511
+ info(' https://github.com/owner/repo');
512
+ info(' git@github.com:owner/repo.git');
513
+ }
514
+ }
515
+ if (!repo) {
516
+ warn('Giving up after 3 invalid entries. Re-run when ready.');
432
517
  process.exit(1);
433
518
  }
434
519
 
@@ -576,14 +661,14 @@ ${BOLD}Usage:${RESET}
576
661
 
577
662
  if (!token && (choice === '2' || choice === '1')) {
578
663
  const url = buildTokenUrl(repo);
579
- info('Opening GitHub in your browser...');
580
- info(`If it does not open, paste this URL manually:`);
581
- log(` ${DIM}${url}${RESET}`);
582
664
  log('');
583
- info(`Set these permissions:`);
584
- log(` ${DIM}- Repository access: Only select repositories → ${repo}${RESET}`);
585
- log(` ${DIM}- Permissions Issues: Read and write${RESET}`);
586
- log(` ${DIM}- Permissions Contents: Read and write (for screenshots)${RESET}`);
665
+ log(` ${BOLD}Opening GitHub. Three things:${RESET}`);
666
+ log(` 1. Click "Only select repositories"pick ${repo}`);
667
+ log(` 2. Click "Generate token"`);
668
+ log(` 3. Copy the token back here`);
669
+ log('');
670
+ info(`If the browser does not open, paste this URL manually:`);
671
+ log(` ${DIM}${url}${RESET}`);
587
672
  log('');
588
673
  openBrowser(url);
589
674
  token = (await ask('Paste your token (starts with github_pat_...):')).trim();
@@ -609,9 +694,19 @@ ${BOLD}Usage:${RESET}
609
694
  }
610
695
  }
611
696
 
612
- if (newVars.length > 0) {
613
- const addition = '\n# hearback\n' + newVars.join('\n') + '\n';
614
- writeFileSync(envPath, envContent + addition);
697
+ // Hint at LLM keys — widget auto-detects and flips to Chat mode if any is set.
698
+ const hasAnyLlmKey = /(?:^|\n)\s*(?:OPENAI|ANTHROPIC|OPENROUTER|LLM)_API_KEY=/m.test(envContent);
699
+ const llmCommentBlock = !hasAnyLlmKey ? [
700
+ '',
701
+ '# Optional: uncomment one to auto-enable AI chat mode',
702
+ '# OPENAI_API_KEY=',
703
+ '# ANTHROPIC_API_KEY=',
704
+ '# OPENROUTER_API_KEY=',
705
+ ] : [];
706
+
707
+ if (newVars.length > 0 || llmCommentBlock.length > 0) {
708
+ const lines = ['', '# hearback', ...newVars, ...llmCommentBlock, ''];
709
+ writeFileSync(envPath, envContent + lines.join('\n'));
615
710
  done(token ? `Wrote FEEDBACK_REPO + GITHUB_TOKEN to ${envFile}` : `Wrote FEEDBACK_REPO to ${envFile}`);
616
711
  }
617
712
 
@@ -633,11 +728,17 @@ ${BOLD}Usage:${RESET}
633
728
  log(`${DIM}Next steps:`);
634
729
  if (!token && !hasExistingToken) {
635
730
  log(` 1. Add your GITHUB_TOKEN to ${envFile}`);
636
- log(` 2. Run your dev server and test the feedback button${RESET}\n`);
731
+ log(` 2. Run your dev server and test the feedback button${RESET}`);
637
732
  } else {
638
733
  log(` 1. Run your dev server (e.g. npm run dev / bun dev)`);
639
- log(` 2. Look for the feedback button in the bottom-right corner${RESET}\n`);
734
+ log(` 2. Look for the feedback button in the bottom-right corner${RESET}`);
640
735
  }
736
+ log('');
737
+ log(`${DIM}Deploying to production?`);
738
+ log(` • Add FEEDBACK_REPO and GITHUB_TOKEN to your platform's env`);
739
+ log(` (Vercel/Netlify/Docker — whatever you use)`);
740
+ log(` • Optionally add OPENAI_API_KEY/ANTHROPIC_API_KEY/OPENROUTER_API_KEY`);
741
+ log(` to enable AI chat mode in production${RESET}\n`);
641
742
  }
642
743
 
643
744
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hearback",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Add user feedback → GitHub Issues → email notification loop to any product with one command. Works with Next.js, Express, Hono, and AI agents.",
5
5
  "type": "module",
6
6
  "bin": {