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.
- package/bin/hearback.js +121 -20
- 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
|
-
*
|
|
285
|
-
*
|
|
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(
|
|
327
|
+
function buildTokenUrl(repoFullName) {
|
|
328
|
+
const [owner, name] = repoFullName.split('/');
|
|
288
329
|
const params = new URLSearchParams({
|
|
289
|
-
name: `hearback-${
|
|
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:
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
584
|
-
log(`
|
|
585
|
-
log(`
|
|
586
|
-
log(`
|
|
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
|
|
613
|
-
|
|
614
|
-
|
|
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}
|
|
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}
|
|
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