hearback 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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/bin/hearback.js +489 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agent Team Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# hearback
|
|
2
|
+
|
|
3
|
+
Add [hearback](https://github.com/agent-team-foundation/hearback) — user feedback → GitHub Issues → email notification loop — to any product with one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx hearback init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Auto-detects your framework (Next.js, Express, Hono, or AI Agent), injects the middleware / widget / env vars, and optionally sets up the notification GitHub Action.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Next.js**: generates `app/api/feedback/[...path]/route.ts`, adds `<Script>` to `layout.tsx`
|
|
14
|
+
- **Express / Hono**: injects `feedbackHandler` middleware + widget `<script>` tag into your HTML
|
|
15
|
+
- **AI Agent (OpenAI / Anthropic / LangChain)**: injects `feedbackSkill` setup into your agent entry file
|
|
16
|
+
|
|
17
|
+
Writes `FEEDBACK_REPO` and `GITHUB_TOKEN` to `.env`. Optionally creates `.github/workflows/feedback-notify.yml` for email notifications on issue close.
|
|
18
|
+
|
|
19
|
+
See the [main repo](https://github.com/agent-team-foundation/hearback) for architecture, manual setup, and integration examples.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
MIT
|
package/bin/hearback.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const RESET = '\x1b[0m';
|
|
9
|
+
const BOLD = '\x1b[1m';
|
|
10
|
+
const DIM = '\x1b[2m';
|
|
11
|
+
const GREEN = '\x1b[32m';
|
|
12
|
+
const YELLOW = '\x1b[33m';
|
|
13
|
+
const CYAN = '\x1b[36m';
|
|
14
|
+
|
|
15
|
+
function log(msg) { console.log(msg); }
|
|
16
|
+
function step(n, msg) { log(`\n${CYAN}[${n}]${RESET} ${BOLD}${msg}${RESET}`); }
|
|
17
|
+
function done(msg) { log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
18
|
+
function info(msg) { log(` ${DIM}${msg}${RESET}`); }
|
|
19
|
+
function warn(msg) { log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
20
|
+
|
|
21
|
+
async function ask(question) {
|
|
22
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question(` ${question} `, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer.trim());
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function confirm(question) {
|
|
32
|
+
const answer = await ask(`${question} (Y/n)`);
|
|
33
|
+
return answer === '' || answer.toLowerCase().startsWith('y');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- File finders ---
|
|
37
|
+
|
|
38
|
+
function findServerEntry(cwd) {
|
|
39
|
+
// Common server entry file patterns
|
|
40
|
+
const candidates = [
|
|
41
|
+
'src/server.ts', 'src/server.js',
|
|
42
|
+
'src/app.ts', 'src/app.js',
|
|
43
|
+
'src/index.ts', 'src/index.js',
|
|
44
|
+
'server.ts', 'server.js',
|
|
45
|
+
'app.ts', 'app.js',
|
|
46
|
+
'index.ts', 'index.js',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
const fullPath = join(cwd, candidate);
|
|
51
|
+
if (!existsSync(fullPath)) continue;
|
|
52
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
53
|
+
// Must look like a server file (imports express/hono/fastify or has listen/app.use)
|
|
54
|
+
if (content.includes('express') || content.includes('hono') || content.includes('fastify') ||
|
|
55
|
+
content.includes('.listen(') || content.includes('app.use(') || content.includes('createServer')) {
|
|
56
|
+
return { path: candidate, fullPath, content };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findHtmlLayout(cwd) {
|
|
63
|
+
// Look for HTML files that have </body>
|
|
64
|
+
const candidates = [
|
|
65
|
+
'src/index.html', 'public/index.html', 'index.html',
|
|
66
|
+
'views/layout.ejs', 'views/layout.hbs',
|
|
67
|
+
'templates/base.html', 'templates/layout.html',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
const fullPath = join(cwd, candidate);
|
|
72
|
+
if (!existsSync(fullPath)) continue;
|
|
73
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
74
|
+
if (content.includes('</body>')) {
|
|
75
|
+
return { path: candidate, fullPath, content };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findNextjsLayout(cwd) {
|
|
82
|
+
const candidates = [
|
|
83
|
+
'src/app/layout.tsx', 'src/app/layout.jsx', 'src/app/layout.js',
|
|
84
|
+
'app/layout.tsx', 'app/layout.jsx', 'app/layout.js',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
const fullPath = join(cwd, candidate);
|
|
89
|
+
if (!existsSync(fullPath)) continue;
|
|
90
|
+
return { path: candidate, fullPath, content: readFileSync(fullPath, 'utf-8') };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findAgentEntry(cwd) {
|
|
96
|
+
const candidates = [
|
|
97
|
+
'src/agent.ts', 'src/agent.js',
|
|
98
|
+
'src/index.ts', 'src/index.js',
|
|
99
|
+
'agent.ts', 'agent.js',
|
|
100
|
+
'index.ts', 'index.js',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
const fullPath = join(cwd, candidate);
|
|
105
|
+
if (!existsSync(fullPath)) continue;
|
|
106
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
107
|
+
if (content.includes('openai') || content.includes('anthropic') || content.includes('langchain') ||
|
|
108
|
+
content.includes('ChatOpenAI') || content.includes('Anthropic')) {
|
|
109
|
+
return { path: candidate, fullPath, content };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Injectors ---
|
|
116
|
+
|
|
117
|
+
function injectImportAndMiddleware(content, isTs) {
|
|
118
|
+
const importLine = `import { feedbackHandler } from 'hearback-server';`;
|
|
119
|
+
const middlewareBlock = [
|
|
120
|
+
'',
|
|
121
|
+
'// hearback feedback widget',
|
|
122
|
+
`app.use('/feedback', feedbackHandler({`,
|
|
123
|
+
` repo: process.env.FEEDBACK_REPO${isTs ? '!' : ''},`,
|
|
124
|
+
` githubToken: process.env.GITHUB_TOKEN${isTs ? '!' : ''},`,
|
|
125
|
+
`}));`,
|
|
126
|
+
].join('\n');
|
|
127
|
+
|
|
128
|
+
if (content.includes('hearback-server')) return content;
|
|
129
|
+
|
|
130
|
+
const lines = content.split('\n');
|
|
131
|
+
const result = [];
|
|
132
|
+
let importInserted = false;
|
|
133
|
+
let middlewareInserted = false;
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
const line = lines[i];
|
|
137
|
+
|
|
138
|
+
// Insert import after the last import/require line
|
|
139
|
+
if (!importInserted && (line.startsWith('import ') || line.includes('require('))) {
|
|
140
|
+
// Check if next line is NOT an import — insert after this one
|
|
141
|
+
const nextLine = lines[i + 1] ?? '';
|
|
142
|
+
if (!nextLine.startsWith('import ') && !nextLine.includes('require(')) {
|
|
143
|
+
result.push(line);
|
|
144
|
+
result.push(importLine);
|
|
145
|
+
importInserted = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Insert middleware before .listen()
|
|
151
|
+
if (!middlewareInserted && line.includes('.listen(')) {
|
|
152
|
+
result.push(middlewareBlock);
|
|
153
|
+
middlewareInserted = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
result.push(line);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fallback: add import at top if no imports found
|
|
160
|
+
if (!importInserted) {
|
|
161
|
+
result.unshift(importLine);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Fallback: add middleware at end if no .listen() found
|
|
165
|
+
if (!middlewareInserted) {
|
|
166
|
+
result.push(middlewareBlock);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function injectWidgetScript(content, endpoint) {
|
|
173
|
+
const scriptTag = `<script src="https://unpkg.com/hearback-widget/dist/widget.js" data-endpoint="${endpoint}"></script>`;
|
|
174
|
+
|
|
175
|
+
if (content.includes('hearback-widget')) return content;
|
|
176
|
+
|
|
177
|
+
// Insert before </body>
|
|
178
|
+
return content.replace('</body>', ` ${scriptTag}\n</body>`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function injectNextjsWidget(content, endpoint) {
|
|
182
|
+
const scriptImport = `import Script from 'next/script';`;
|
|
183
|
+
const scriptTag = ` <Script src="https://unpkg.com/hearback-widget/dist/widget.js" data-endpoint="${endpoint}" strategy="lazyOnload" />`;
|
|
184
|
+
|
|
185
|
+
if (content.includes('hearback-widget')) return content;
|
|
186
|
+
|
|
187
|
+
// Add Script import if not present — find the LAST import statement and
|
|
188
|
+
// insert after it. If no imports exist, prepend to the file.
|
|
189
|
+
let result = content;
|
|
190
|
+
if (!result.includes("from 'next/script'") && !result.includes('from "next/script"')) {
|
|
191
|
+
const lines = result.split('\n');
|
|
192
|
+
let lastImportIdx = -1;
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
if (/^import\s/.test(lines[i])) lastImportIdx = i;
|
|
195
|
+
}
|
|
196
|
+
if (lastImportIdx >= 0) {
|
|
197
|
+
lines.splice(lastImportIdx + 1, 0, scriptImport);
|
|
198
|
+
} else {
|
|
199
|
+
lines.unshift(scriptImport);
|
|
200
|
+
}
|
|
201
|
+
result = lines.join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Add before </body>. Put on its own line (preserve surrounding whitespace).
|
|
205
|
+
result = result.replace(/(\s*)<\/body>/, `\n${scriptTag}$1</body>`);
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- Detect project type ---
|
|
211
|
+
|
|
212
|
+
function detectProject(cwd) {
|
|
213
|
+
const pkgPath = join(cwd, 'package.json');
|
|
214
|
+
if (!existsSync(pkgPath)) return { type: 'unknown' };
|
|
215
|
+
|
|
216
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
217
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
218
|
+
|
|
219
|
+
if (deps['next']) {
|
|
220
|
+
return { type: 'nextjs', pkg, deps };
|
|
221
|
+
}
|
|
222
|
+
if (deps['express']) return { type: 'express', pkg, deps };
|
|
223
|
+
if (deps['hono']) return { type: 'hono', pkg, deps };
|
|
224
|
+
if (deps['openai'] || deps['@anthropic-ai/sdk'] || deps['langchain'] || deps['@langchain/core']) {
|
|
225
|
+
return { type: 'agent', pkg, deps };
|
|
226
|
+
}
|
|
227
|
+
if (deps['fastify']) return { type: 'fastify', pkg, deps };
|
|
228
|
+
|
|
229
|
+
return { type: 'node', pkg, deps };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- Templates ---
|
|
233
|
+
|
|
234
|
+
const NEXTJS_ROUTE = `import { NextRequest, NextResponse } from 'next/server';
|
|
235
|
+
import { createFeedbackHandler } from 'hearback-server';
|
|
236
|
+
|
|
237
|
+
const handler = createFeedbackHandler({
|
|
238
|
+
repo: process.env.FEEDBACK_REPO!,
|
|
239
|
+
githubToken: process.env.GITHUB_TOKEN!,
|
|
240
|
+
llm: process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : undefined,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
async function handleRequest(req: NextRequest) {
|
|
244
|
+
const url = new URL(req.url);
|
|
245
|
+
const path = url.pathname.replace('/api/feedback', '') || '/';
|
|
246
|
+
const body = req.method === 'POST' ? await req.json() : undefined;
|
|
247
|
+
|
|
248
|
+
const result = await handler.handle({
|
|
249
|
+
method: req.method,
|
|
250
|
+
path,
|
|
251
|
+
body,
|
|
252
|
+
ip: req.headers.get('x-forwarded-for')?.split(',')[0] ?? '127.0.0.1',
|
|
253
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (result.body && typeof result.body === 'object' && 'getReader' in (result.body as object)) {
|
|
257
|
+
return new Response(result.body as ReadableStream, {
|
|
258
|
+
status: result.status,
|
|
259
|
+
headers: result.headers,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return NextResponse.json(result.body, {
|
|
264
|
+
status: result.status,
|
|
265
|
+
headers: result.headers,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const GET = handleRequest;
|
|
270
|
+
export const POST = handleRequest;
|
|
271
|
+
export const OPTIONS = handleRequest;
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
const NOTIFY_WORKFLOW = `name: Notify Reporter
|
|
275
|
+
on:
|
|
276
|
+
issues:
|
|
277
|
+
types: [closed, labeled]
|
|
278
|
+
jobs:
|
|
279
|
+
notify:
|
|
280
|
+
if: |
|
|
281
|
+
(github.event.action == 'closed' && contains(github.event.issue.labels.*.name, 'hearback')) ||
|
|
282
|
+
(github.event.action == 'labeled' && github.event.label.name == 'feedback-responded')
|
|
283
|
+
runs-on: ubuntu-latest
|
|
284
|
+
steps:
|
|
285
|
+
- uses: actions/checkout@v4
|
|
286
|
+
with:
|
|
287
|
+
repository: agent-team-foundation/hearback
|
|
288
|
+
ref: v0.1.0
|
|
289
|
+
path: .hearback
|
|
290
|
+
- uses: ./.hearback/.github/actions/notify
|
|
291
|
+
with:
|
|
292
|
+
github-token: \${{ secrets.GITHUB_TOKEN }}
|
|
293
|
+
resend-api-key: \${{ secrets.RESEND_API_KEY }}
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
// --- Main ---
|
|
297
|
+
|
|
298
|
+
async function main() {
|
|
299
|
+
const args = process.argv.slice(2);
|
|
300
|
+
const command = args[0];
|
|
301
|
+
|
|
302
|
+
if (!command || command === 'help' || command === '--help') {
|
|
303
|
+
log(`
|
|
304
|
+
${BOLD}hearback${RESET} — add user feedback loop to any product
|
|
305
|
+
|
|
306
|
+
${BOLD}Usage:${RESET}
|
|
307
|
+
npx hearback init Set up hearback in the current project
|
|
308
|
+
npx hearback help Show this help
|
|
309
|
+
`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (command !== 'init') {
|
|
314
|
+
log(`Unknown command: ${command}. Run "npx hearback help" for usage.`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const cwd = process.cwd();
|
|
319
|
+
|
|
320
|
+
log(`\n${BOLD}🎯 hearback init${RESET}`);
|
|
321
|
+
log(`${DIM}Add user feedback → GitHub Issues → email notification${RESET}`);
|
|
322
|
+
|
|
323
|
+
// Step 1: Detect
|
|
324
|
+
step(1, 'Detecting project...');
|
|
325
|
+
const project = detectProject(cwd);
|
|
326
|
+
|
|
327
|
+
if (project.type === 'unknown') {
|
|
328
|
+
warn('No package.json found. Run this from your project root.');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const typeLabels = {
|
|
333
|
+
nextjs: 'Next.js',
|
|
334
|
+
express: 'Express',
|
|
335
|
+
hono: 'Hono',
|
|
336
|
+
fastify: 'Fastify',
|
|
337
|
+
agent: 'AI Agent',
|
|
338
|
+
node: 'Node.js',
|
|
339
|
+
};
|
|
340
|
+
done(`${typeLabels[project.type] ?? project.type} project`);
|
|
341
|
+
|
|
342
|
+
// Step 2: Collect info
|
|
343
|
+
step(2, 'Configuration');
|
|
344
|
+
const repo = await ask('GitHub repo for issues (owner/name):');
|
|
345
|
+
if (!repo || !repo.includes('/')) {
|
|
346
|
+
warn('Invalid repo format. Expected: owner/name');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Step 3: Install
|
|
351
|
+
step(3, 'Installing packages...');
|
|
352
|
+
|
|
353
|
+
const pkgToInstall = project.type === 'agent' ? 'hearback-agent-skill' : 'hearback-server';
|
|
354
|
+
try {
|
|
355
|
+
execSync(`npm install ${pkgToInstall}`, { cwd, stdio: 'pipe' });
|
|
356
|
+
done(`${pkgToInstall} installed`);
|
|
357
|
+
} catch {
|
|
358
|
+
warn(`Could not install ${pkgToInstall} (not yet published to npm)`);
|
|
359
|
+
info(`When available, run: npm install ${pkgToInstall}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Step 4: Generate & inject
|
|
363
|
+
step(4, 'Injecting code...');
|
|
364
|
+
|
|
365
|
+
let widgetEndpoint = '/feedback';
|
|
366
|
+
|
|
367
|
+
if (project.type === 'nextjs') {
|
|
368
|
+
// Create API route
|
|
369
|
+
const appBase = existsSync(join(cwd, 'src/app')) ? 'src/app' : 'app';
|
|
370
|
+
const routeDir = join(cwd, appBase, 'api/feedback/[...path]');
|
|
371
|
+
mkdirSync(routeDir, { recursive: true });
|
|
372
|
+
writeFileSync(join(routeDir, 'route.ts'), NEXTJS_ROUTE);
|
|
373
|
+
done(`Created ${appBase}/api/feedback/[...path]/route.ts`);
|
|
374
|
+
|
|
375
|
+
widgetEndpoint = '/api/feedback';
|
|
376
|
+
|
|
377
|
+
// Inject widget into layout
|
|
378
|
+
const layout = findNextjsLayout(cwd);
|
|
379
|
+
if (layout) {
|
|
380
|
+
const updated = injectNextjsWidget(layout.content, widgetEndpoint);
|
|
381
|
+
if (updated !== layout.content) {
|
|
382
|
+
writeFileSync(layout.fullPath, updated);
|
|
383
|
+
done(`Added widget to ${layout.path}`);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
warn('Could not find layout file — add the widget script manually:');
|
|
387
|
+
info(`<Script src="https://unpkg.com/hearback-widget/dist/widget.js" data-endpoint="${widgetEndpoint}" strategy="lazyOnload" />`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
} else if (project.type === 'agent') {
|
|
391
|
+
// Agent project — inject skill setup
|
|
392
|
+
const entry = findAgentEntry(cwd);
|
|
393
|
+
if (entry) {
|
|
394
|
+
const isTs = entry.path.endsWith('.ts');
|
|
395
|
+
const importLine = `import { feedbackSkill } from 'hearback-agent-skill';\n`;
|
|
396
|
+
const setupBlock = `\n// hearback feedback skill\nconst hearbackSkill = feedbackSkill({\n repo: process.env.FEEDBACK_REPO${isTs ? '!' : ''},\n githubToken: process.env.GITHUB_TOKEN${isTs ? '!' : ''},\n});\n// hearbackSkill.instructions → add to system prompt\n// hearbackSkill.tools.openai → register as tools\n// hearbackSkill.handlers → execute tool calls\n`;
|
|
397
|
+
|
|
398
|
+
let content = entry.content;
|
|
399
|
+
if (!content.includes('hearback-agent-skill')) {
|
|
400
|
+
const lastImportIdx = Math.max(content.lastIndexOf('import '), content.lastIndexOf('require('));
|
|
401
|
+
if (lastImportIdx !== -1) {
|
|
402
|
+
const lineEnd = content.indexOf('\n', lastImportIdx);
|
|
403
|
+
content = content.slice(0, lineEnd + 1) + importLine + content.slice(lineEnd + 1);
|
|
404
|
+
} else {
|
|
405
|
+
content = importLine + content;
|
|
406
|
+
}
|
|
407
|
+
content += setupBlock;
|
|
408
|
+
writeFileSync(entry.fullPath, content);
|
|
409
|
+
done(`Injected skill setup into ${entry.path}`);
|
|
410
|
+
} else {
|
|
411
|
+
info(`${entry.path} already has hearback-agent-skill`);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
warn('Could not find agent entry file');
|
|
415
|
+
info('Add to your agent setup:');
|
|
416
|
+
log(`\n ${DIM}import { feedbackSkill } from 'hearback-agent-skill';${RESET}\n`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
} else {
|
|
420
|
+
// Express / Hono / Fastify / generic Node
|
|
421
|
+
const serverFile = findServerEntry(cwd);
|
|
422
|
+
if (serverFile) {
|
|
423
|
+
const isTs = serverFile.path.endsWith('.ts');
|
|
424
|
+
const updated = injectImportAndMiddleware(serverFile.content, isTs);
|
|
425
|
+
if (updated !== serverFile.content) {
|
|
426
|
+
writeFileSync(serverFile.fullPath, updated);
|
|
427
|
+
done(`Injected middleware into ${serverFile.path}`);
|
|
428
|
+
} else {
|
|
429
|
+
info(`${serverFile.path} already has hearback middleware`);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
warn('Could not find server entry file');
|
|
433
|
+
info('Add to your server:');
|
|
434
|
+
log(`\n ${DIM}import { feedbackHandler } from 'hearback-server';`);
|
|
435
|
+
log(` app.use('/feedback', feedbackHandler({ repo: process.env.FEEDBACK_REPO, githubToken: process.env.GITHUB_TOKEN }));${RESET}\n`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Inject widget into HTML
|
|
439
|
+
const html = findHtmlLayout(cwd);
|
|
440
|
+
if (html) {
|
|
441
|
+
const updated = injectWidgetScript(html.content, widgetEndpoint);
|
|
442
|
+
if (updated !== html.content) {
|
|
443
|
+
writeFileSync(html.fullPath, updated);
|
|
444
|
+
done(`Added widget to ${html.path}`);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
info('No HTML file found — add before </body>:');
|
|
448
|
+
log(`\n ${DIM}<script src="https://unpkg.com/hearback-widget/dist/widget.js" data-endpoint="${widgetEndpoint}"></script>${RESET}\n`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Env vars
|
|
453
|
+
const envFile = existsSync(join(cwd, '.env.local')) ? '.env.local' : '.env';
|
|
454
|
+
const envPath = join(cwd, envFile);
|
|
455
|
+
const envContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
|
|
456
|
+
|
|
457
|
+
const newVars = [];
|
|
458
|
+
if (!envContent.includes('FEEDBACK_REPO')) newVars.push(`FEEDBACK_REPO=${repo}`);
|
|
459
|
+
if (!envContent.includes('GITHUB_TOKEN')) newVars.push(`GITHUB_TOKEN= # Create at https://github.com/settings/tokens?type=beta (needs issues:write + contents:write)`);
|
|
460
|
+
|
|
461
|
+
if (newVars.length > 0) {
|
|
462
|
+
const addition = '\n# hearback\n' + newVars.join('\n') + '\n';
|
|
463
|
+
writeFileSync(envPath, envContent + addition);
|
|
464
|
+
done(`Added env vars to ${envFile}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Step 5: Notification workflow
|
|
468
|
+
step(5, 'Notification loop');
|
|
469
|
+
const wantNotify = await confirm('Set up email notifications when issues are fixed?');
|
|
470
|
+
|
|
471
|
+
if (wantNotify) {
|
|
472
|
+
const workflowDir = join(cwd, '.github/workflows');
|
|
473
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
474
|
+
writeFileSync(join(workflowDir, 'feedback-notify.yml'), NOTIFY_WORKFLOW);
|
|
475
|
+
done('Created .github/workflows/feedback-notify.yml');
|
|
476
|
+
info('Add RESEND_API_KEY to your repo secrets (free at resend.com)');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Done
|
|
480
|
+
log(`\n${GREEN}${BOLD}✓ hearback is ready!${RESET}\n`);
|
|
481
|
+
log(`${DIM}Next steps:`);
|
|
482
|
+
log(` 1. Add your GITHUB_TOKEN to ${envFile}`);
|
|
483
|
+
log(` 2. Run your dev server and test the feedback button${RESET}\n`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
main().catch((err) => {
|
|
487
|
+
console.error(err);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hearback",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hearback": "./bin/hearback.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "echo 'no build needed'",
|
|
16
|
+
"clean": "echo 'nothing to clean'"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"hearback",
|
|
20
|
+
"feedback",
|
|
21
|
+
"bug-report",
|
|
22
|
+
"github-issues",
|
|
23
|
+
"cli",
|
|
24
|
+
"init",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"widget",
|
|
27
|
+
"agent",
|
|
28
|
+
"nextjs",
|
|
29
|
+
"express"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/agent-team-foundation/hearback.git",
|
|
34
|
+
"directory": "packages/cli"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/agent-team-foundation/hearback/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/agent-team-foundation/hearback#readme",
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|