@nikitadmitrieff/feedback-chat 0.1.2 → 0.2.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/dist/cli/deploy-agent.js +177 -0
- package/dist/cli/init.js +183 -34
- package/dist/client/index.d.ts +6 -1
- package/dist/client/index.js +58 -11
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +24 -9
- package/package.json +4 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/deploy-agent.ts
|
|
4
|
+
import prompts from "prompts";
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { join, resolve } from "path";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
function checkCommand(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readEnvFile(cwd) {
|
|
17
|
+
const envPath = join(cwd, ".env.local");
|
|
18
|
+
if (!existsSync(envPath)) return {};
|
|
19
|
+
const content = readFileSync(envPath, "utf-8");
|
|
20
|
+
const vars = {};
|
|
21
|
+
for (const line of content.split("\n")) {
|
|
22
|
+
const match = line.match(/^([A-Z_]+)=(.+)$/);
|
|
23
|
+
if (match) vars[match[1]] = match[2];
|
|
24
|
+
}
|
|
25
|
+
return vars;
|
|
26
|
+
}
|
|
27
|
+
async function main() {
|
|
28
|
+
const cwd = resolve(process.cwd());
|
|
29
|
+
console.error();
|
|
30
|
+
console.error(" feedback-chat agent deployment script generator");
|
|
31
|
+
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
32
|
+
console.error();
|
|
33
|
+
const hasRailway = checkCommand("railway");
|
|
34
|
+
const hasGh = checkCommand("gh");
|
|
35
|
+
if (!hasRailway) {
|
|
36
|
+
console.error(" \u2717 Railway CLI not found. Install: npm install -g @railway/cli");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (!hasGh) {
|
|
40
|
+
console.error(" \u2717 GitHub CLI not found. Install: https://cli.github.com/");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const envVars = readEnvFile(cwd);
|
|
44
|
+
const githubToken = envVars["GITHUB_TOKEN"] || "";
|
|
45
|
+
const githubRepo = envVars["GITHUB_REPO"] || "";
|
|
46
|
+
const anthropicKey = envVars["ANTHROPIC_API_KEY"] || "";
|
|
47
|
+
if (!githubToken || !githubRepo) {
|
|
48
|
+
console.error(" \u2717 GITHUB_TOKEN and GITHUB_REPO must be set in .env.local");
|
|
49
|
+
console.error(" Run `npx feedback-chat init` first with the +GitHub or +Pipeline tier.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const { authMethod } = await prompts({
|
|
53
|
+
type: "select",
|
|
54
|
+
name: "authMethod",
|
|
55
|
+
message: "Claude authentication for the agent",
|
|
56
|
+
choices: [
|
|
57
|
+
{
|
|
58
|
+
title: "Claude Max subscription ($0/run)",
|
|
59
|
+
description: "Uses OAuth credentials from your local keychain",
|
|
60
|
+
value: "max"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "API key (pay per token)",
|
|
64
|
+
description: "Uses ANTHROPIC_API_KEY from .env.local",
|
|
65
|
+
value: "api"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
if (!authMethod) {
|
|
70
|
+
console.error(" Cancelled.");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
let credentialsJson = "";
|
|
74
|
+
if (authMethod === "max") {
|
|
75
|
+
console.error();
|
|
76
|
+
console.error(" To get your Claude credentials, run:");
|
|
77
|
+
console.error(' security find-generic-password -s "Claude Code-credentials" -a "$USER" -w');
|
|
78
|
+
console.error();
|
|
79
|
+
console.error(" Copy the JSON output (the claudeAiOauth portion).");
|
|
80
|
+
console.error();
|
|
81
|
+
const { creds } = await prompts({
|
|
82
|
+
type: "text",
|
|
83
|
+
name: "creds",
|
|
84
|
+
message: "Paste your Claude credentials JSON (or press Enter to include a placeholder)"
|
|
85
|
+
});
|
|
86
|
+
credentialsJson = creds || "${CLAUDE_CREDENTIALS_JSON}";
|
|
87
|
+
}
|
|
88
|
+
const webhookSecret = "$(openssl rand -hex 32)";
|
|
89
|
+
const envLocalPath = join(cwd, ".env.local");
|
|
90
|
+
const authLine = authMethod === "max" ? `
|
|
91
|
+
CLAUDE_CREDENTIALS_JSON='${credentialsJson.replace(/'/g, "'\\''")}'` : anthropicKey ? `
|
|
92
|
+
ANTHROPIC_API_KEY="${anthropicKey}"` : "";
|
|
93
|
+
const script = `#!/usr/bin/env bash
|
|
94
|
+
set -euo pipefail
|
|
95
|
+
|
|
96
|
+
# \u2500\u2500 feedback-chat agent deployment script \u2500\u2500
|
|
97
|
+
# Generated by: npx feedback-chat deploy-agent
|
|
98
|
+
# Review this script before running it.
|
|
99
|
+
|
|
100
|
+
echo "==> Cloning feedback-chat agent..."
|
|
101
|
+
TMPDIR=$(mktemp -d)
|
|
102
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
103
|
+
git clone --depth 1 https://github.com/NikitaDmitrieff/feedback-chat "$TMPDIR/feedback-chat"
|
|
104
|
+
cd "$TMPDIR/feedback-chat/packages/agent"
|
|
105
|
+
|
|
106
|
+
echo "==> Creating Railway project..."
|
|
107
|
+
railway init
|
|
108
|
+
|
|
109
|
+
echo "==> First deploy (creates the service)..."
|
|
110
|
+
DEPLOY_OUTPUT=$(railway up --detach 2>&1)
|
|
111
|
+
echo "$DEPLOY_OUTPUT"
|
|
112
|
+
|
|
113
|
+
echo "==> Waiting for service to register..."
|
|
114
|
+
sleep 5
|
|
115
|
+
|
|
116
|
+
echo "==> Finding and linking service..."
|
|
117
|
+
SERVICE_NAME=$(railway service status --all 2>&1 | grep -oE '^[a-z][-a-z0-9]*' | head -1)
|
|
118
|
+
if [ -z "$SERVICE_NAME" ]; then
|
|
119
|
+
echo "\u2717 Could not detect service name. Run 'railway service status --all' manually."
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
echo " Linking service: $SERVICE_NAME"
|
|
123
|
+
railway service link "$SERVICE_NAME"
|
|
124
|
+
|
|
125
|
+
echo "==> Setting environment variables..."
|
|
126
|
+
WEBHOOK_SECRET=${webhookSecret}
|
|
127
|
+
railway variables set \\
|
|
128
|
+
GITHUB_TOKEN="${githubToken}" \\
|
|
129
|
+
GITHUB_REPO="${githubRepo}" \\
|
|
130
|
+
WEBHOOK_SECRET="$WEBHOOK_SECRET"${authLine ? ` \\${authLine}` : ""}
|
|
131
|
+
|
|
132
|
+
echo "==> Getting public domain..."
|
|
133
|
+
DOMAIN_OUTPUT=$(railway domain 2>&1)
|
|
134
|
+
AGENT_URL=$(echo "$DOMAIN_OUTPUT" | grep -oE 'https://[^ ]+')
|
|
135
|
+
if [ -z "$AGENT_URL" ]; then
|
|
136
|
+
echo "\u2717 Could not extract domain URL. Output was:"
|
|
137
|
+
echo "$DOMAIN_OUTPUT"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
echo " Agent URL: $AGENT_URL"
|
|
141
|
+
|
|
142
|
+
echo "==> Setting up GitHub webhook..."
|
|
143
|
+
REPO_OWNER=$(echo "${githubRepo}" | cut -d/ -f1)
|
|
144
|
+
REPO_NAME=$(echo "${githubRepo}" | cut -d/ -f2)
|
|
145
|
+
gh api "repos/$REPO_OWNER/$REPO_NAME/hooks" \\
|
|
146
|
+
-f name=web -f active=true \\
|
|
147
|
+
-f "config[url]=$AGENT_URL/webhook/github" \\
|
|
148
|
+
-f "config[content_type]=json" \\
|
|
149
|
+
-f "config[secret]=$WEBHOOK_SECRET" \\
|
|
150
|
+
-f 'events[]=issues'
|
|
151
|
+
|
|
152
|
+
echo "==> Updating consumer .env.local..."
|
|
153
|
+
echo "AGENT_URL=$AGENT_URL" >> "${envLocalPath}"
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
echo "==> Done! Summary:"
|
|
157
|
+
echo " Agent URL: $AGENT_URL"
|
|
158
|
+
echo " Webhook: $AGENT_URL/webhook/github"
|
|
159
|
+
echo " Webhook secret: $WEBHOOK_SECRET"
|
|
160
|
+
echo ""
|
|
161
|
+
echo " Next steps:"
|
|
162
|
+
echo " 1. Wait for Railway deploy to finish"
|
|
163
|
+
echo " 2. Verify: curl $AGENT_URL/health"
|
|
164
|
+
echo " 3. Test by submitting feedback through the widget"
|
|
165
|
+
`;
|
|
166
|
+
process.stdout.write(script);
|
|
167
|
+
console.error();
|
|
168
|
+
console.error(" Script generated! To run it:");
|
|
169
|
+
console.error(" npx feedback-chat deploy-agent | bash");
|
|
170
|
+
console.error(" Or save and review first:");
|
|
171
|
+
console.error(" npx feedback-chat deploy-agent > deploy.sh && chmod +x deploy.sh && cat deploy.sh");
|
|
172
|
+
console.error();
|
|
173
|
+
}
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error(err);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
package/dist/cli/init.js
CHANGED
|
@@ -4,27 +4,79 @@
|
|
|
4
4
|
import prompts from "prompts";
|
|
5
5
|
import { existsSync, writeFileSync, appendFileSync, readFileSync, mkdirSync } from "fs";
|
|
6
6
|
import { join, resolve } from "path";
|
|
7
|
-
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
var CHAT_ROUTE_TEMPLATE = (tier) => `import { createFeedbackHandler } from '@nikitadmitrieff/feedback-chat/server'
|
|
8
9
|
|
|
9
10
|
const handler = createFeedbackHandler({
|
|
10
|
-
password: process.env.FEEDBACK_PASSWORD!,${
|
|
11
|
+
password: process.env.FEEDBACK_PASSWORD!,${tier !== "chat" ? `
|
|
11
12
|
github: {
|
|
12
13
|
token: process.env.GITHUB_TOKEN!,
|
|
13
14
|
repo: process.env.GITHUB_REPO!,
|
|
14
15
|
},` : ""}
|
|
16
|
+
// projectContext: 'Describe your app here so the AI gives better responses',
|
|
15
17
|
})
|
|
16
18
|
|
|
17
19
|
export const POST = handler.POST
|
|
18
20
|
`;
|
|
19
|
-
var STATUS_ROUTE_TEMPLATE =
|
|
21
|
+
var STATUS_ROUTE_TEMPLATE = (tier) => {
|
|
22
|
+
if (tier === "chat") {
|
|
23
|
+
return `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
|
|
20
24
|
|
|
21
25
|
const handler = createStatusHandler({
|
|
22
26
|
password: process.env.FEEDBACK_PASSWORD!,
|
|
23
27
|
})
|
|
24
28
|
|
|
25
29
|
export const { GET, POST } = handler
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
if (tier === "github") {
|
|
33
|
+
return `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
|
|
34
|
+
|
|
35
|
+
const handler = createStatusHandler({
|
|
36
|
+
password: process.env.FEEDBACK_PASSWORD!,
|
|
37
|
+
github: {
|
|
38
|
+
token: process.env.GITHUB_TOKEN!,
|
|
39
|
+
repo: process.env.GITHUB_REPO!,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const { GET, POST } = handler
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
return `import { createStatusHandler } from '@nikitadmitrieff/feedback-chat/server'
|
|
47
|
+
|
|
48
|
+
const handler = createStatusHandler({
|
|
49
|
+
password: process.env.FEEDBACK_PASSWORD!,
|
|
50
|
+
github: {
|
|
51
|
+
token: process.env.GITHUB_TOKEN!,
|
|
52
|
+
repo: process.env.GITHUB_REPO!,
|
|
53
|
+
},
|
|
54
|
+
agentUrl: process.env.AGENT_URL,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const { GET, POST } = handler
|
|
58
|
+
`;
|
|
59
|
+
};
|
|
60
|
+
var FEEDBACK_BUTTON_TEMPLATE = `'use client'
|
|
61
|
+
|
|
62
|
+
import { useState } from 'react'
|
|
63
|
+
import { FeedbackPanel } from '@nikitadmitrieff/feedback-chat'
|
|
64
|
+
import '@nikitadmitrieff/feedback-chat/styles.css'
|
|
65
|
+
|
|
66
|
+
export function FeedbackButton() {
|
|
67
|
+
const [open, setOpen] = useState(false)
|
|
68
|
+
return <FeedbackPanel isOpen={open} onToggle={() => setOpen(!open)} />
|
|
69
|
+
}
|
|
26
70
|
`;
|
|
27
71
|
var SOURCE_DIRECTIVE = '@source "../node_modules/@nikitadmitrieff/feedback-chat/dist/**/*.js";';
|
|
72
|
+
var GITHUB_LABELS = [
|
|
73
|
+
{ name: "feedback-bot", color: "0E8A16", description: "Created by feedback widget" },
|
|
74
|
+
{ name: "auto-implement", color: "1D76DB", description: "Agent should implement this" },
|
|
75
|
+
{ name: "in-progress", color: "FBCA04", description: "Agent is working on this" },
|
|
76
|
+
{ name: "agent-failed", color: "D93F0B", description: "Agent build/lint failed" },
|
|
77
|
+
{ name: "preview-pending", color: "C5DEF5", description: "PR ready, preview deploying" },
|
|
78
|
+
{ name: "rejected", color: "E4E669", description: "User rejected changes" }
|
|
79
|
+
];
|
|
28
80
|
function findAppDir(cwd) {
|
|
29
81
|
const candidates = [
|
|
30
82
|
join(cwd, "src", "app"),
|
|
@@ -84,12 +136,65 @@ function appendEnvVar(envPath, key, value) {
|
|
|
84
136
|
appendFileSync(envPath, `${key}=${value}
|
|
85
137
|
`);
|
|
86
138
|
}
|
|
139
|
+
function checkReactVersion(cwd) {
|
|
140
|
+
const reactPkgPath = join(cwd, "node_modules", "react", "package.json");
|
|
141
|
+
if (!existsSync(reactPkgPath)) return;
|
|
142
|
+
try {
|
|
143
|
+
const pkg = JSON.parse(readFileSync(reactPkgPath, "utf-8"));
|
|
144
|
+
const version = pkg.version;
|
|
145
|
+
if (version === "19.1.0" || version === "19.1.1") {
|
|
146
|
+
console.error(` \u2717 react@${version} detected \u2014 @ai-sdk/react excludes this version.`);
|
|
147
|
+
console.error(" Fix: npm install react@latest react-dom@latest");
|
|
148
|
+
console.error();
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function hasGhCli() {
|
|
155
|
+
try {
|
|
156
|
+
execSync("which gh", { stdio: "ignore" });
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function createGitHubLabels(cwd) {
|
|
163
|
+
if (!hasGhCli()) {
|
|
164
|
+
console.log(" \u26A0 GitHub CLI (gh) not found. Create these labels manually:");
|
|
165
|
+
console.log();
|
|
166
|
+
for (const label of GITHUB_LABELS) {
|
|
167
|
+
console.log(` gh label create ${label.name} --color ${label.color} --description "${label.description}" --force`);
|
|
168
|
+
}
|
|
169
|
+
console.log();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(" Creating GitHub labels...");
|
|
173
|
+
for (const label of GITHUB_LABELS) {
|
|
174
|
+
try {
|
|
175
|
+
execSync(
|
|
176
|
+
`gh label create ${label.name} --color ${label.color} --description "${label.description}" --force`,
|
|
177
|
+
{ cwd, stdio: "ignore" }
|
|
178
|
+
);
|
|
179
|
+
console.log(` Created label: ${label.name}`);
|
|
180
|
+
} catch {
|
|
181
|
+
console.log(` \u26A0 Could not create label: ${label.name}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function detectComponentsDir(cwd) {
|
|
186
|
+
if (existsSync(join(cwd, "src", "app"))) {
|
|
187
|
+
return join(cwd, "src", "components");
|
|
188
|
+
}
|
|
189
|
+
return join(cwd, "components");
|
|
190
|
+
}
|
|
87
191
|
async function main() {
|
|
88
192
|
const cwd = resolve(process.cwd());
|
|
89
193
|
console.log();
|
|
90
194
|
console.log(" feedback-chat setup wizard");
|
|
91
195
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
92
196
|
console.log();
|
|
197
|
+
checkReactVersion(cwd);
|
|
93
198
|
const appDir = findAppDir(cwd);
|
|
94
199
|
if (!appDir) {
|
|
95
200
|
console.error(" Could not find app/ or src/app/ directory.");
|
|
@@ -98,7 +203,26 @@ async function main() {
|
|
|
98
203
|
}
|
|
99
204
|
console.log(` Found Next.js app directory: ${appDir}`);
|
|
100
205
|
console.log();
|
|
101
|
-
console.log(" \u2500\u2500
|
|
206
|
+
console.log(" \u2500\u2500 Tier Selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
207
|
+
console.log();
|
|
208
|
+
const tierAnswer = await prompts({
|
|
209
|
+
type: "select",
|
|
210
|
+
name: "tier",
|
|
211
|
+
message: "Choose your tier",
|
|
212
|
+
choices: [
|
|
213
|
+
{ title: "Chat only (AI conversations, localStorage persistence)", value: "chat" },
|
|
214
|
+
{ title: "+ GitHub (Chat + auto-creates GitHub issues)", value: "github" },
|
|
215
|
+
{ title: "+ Pipeline (Chat + GitHub + agent \u2192 PR \u2192 preview \u2192 approve)", value: "pipeline" }
|
|
216
|
+
],
|
|
217
|
+
initial: 0
|
|
218
|
+
});
|
|
219
|
+
if (tierAnswer.tier === void 0) {
|
|
220
|
+
console.log(" Cancelled.");
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
const tier = tierAnswer.tier;
|
|
224
|
+
console.log();
|
|
225
|
+
console.log(" \u2500\u2500 Widget Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
102
226
|
console.log();
|
|
103
227
|
const widgetAnswers = await prompts([
|
|
104
228
|
{
|
|
@@ -116,23 +240,17 @@ async function main() {
|
|
|
116
240
|
console.log(" Cancelled.");
|
|
117
241
|
process.exit(0);
|
|
118
242
|
}
|
|
119
|
-
console.log();
|
|
120
|
-
console.log(" \u2500\u2500 GitHub Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
121
|
-
console.log();
|
|
122
|
-
const githubAnswer = await prompts({
|
|
123
|
-
type: "confirm",
|
|
124
|
-
name: "enabled",
|
|
125
|
-
message: "Enable GitHub issues? (feedback creates issues for tracking)",
|
|
126
|
-
initial: true
|
|
127
|
-
});
|
|
128
243
|
let githubToken = "";
|
|
129
244
|
let githubRepo = "";
|
|
130
|
-
if (
|
|
245
|
+
if (tier !== "chat") {
|
|
246
|
+
console.log();
|
|
247
|
+
console.log(" \u2500\u2500 GitHub Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
248
|
+
console.log();
|
|
131
249
|
const ghAnswers = await prompts([
|
|
132
250
|
{
|
|
133
251
|
type: "password",
|
|
134
252
|
name: "token",
|
|
135
|
-
message: "GitHub token (needs repo scope)"
|
|
253
|
+
message: "GitHub token (needs repo scope, must start with ghp_)"
|
|
136
254
|
},
|
|
137
255
|
{
|
|
138
256
|
type: "text",
|
|
@@ -143,7 +261,18 @@ async function main() {
|
|
|
143
261
|
githubToken = ghAnswers.token || "";
|
|
144
262
|
githubRepo = ghAnswers.repo || "";
|
|
145
263
|
}
|
|
146
|
-
|
|
264
|
+
let agentUrl = "";
|
|
265
|
+
if (tier === "pipeline") {
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(" \u2500\u2500 Agent Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
268
|
+
console.log();
|
|
269
|
+
const agentAnswer = await prompts({
|
|
270
|
+
type: "text",
|
|
271
|
+
name: "url",
|
|
272
|
+
message: "Agent URL (e.g., https://your-agent.railway.app)"
|
|
273
|
+
});
|
|
274
|
+
agentUrl = agentAnswer.url || "";
|
|
275
|
+
}
|
|
147
276
|
let overwrite = false;
|
|
148
277
|
const chatRoutePath = join(appDir, "api", "feedback", "chat", "route.ts");
|
|
149
278
|
const statusRoutePath = join(appDir, "api", "feedback", "status", "route.ts");
|
|
@@ -157,12 +286,17 @@ async function main() {
|
|
|
157
286
|
overwrite = overwriteAnswer.overwrite ?? false;
|
|
158
287
|
}
|
|
159
288
|
console.log();
|
|
160
|
-
if (safeWriteFile(chatRoutePath, CHAT_ROUTE_TEMPLATE(
|
|
289
|
+
if (safeWriteFile(chatRoutePath, CHAT_ROUTE_TEMPLATE(tier), overwrite)) {
|
|
161
290
|
console.log(` Created ${chatRoutePath}`);
|
|
162
291
|
}
|
|
163
|
-
if (safeWriteFile(statusRoutePath, STATUS_ROUTE_TEMPLATE, overwrite)) {
|
|
292
|
+
if (safeWriteFile(statusRoutePath, STATUS_ROUTE_TEMPLATE(tier), overwrite)) {
|
|
164
293
|
console.log(` Created ${statusRoutePath}`);
|
|
165
294
|
}
|
|
295
|
+
const componentsDir = detectComponentsDir(cwd);
|
|
296
|
+
const feedbackButtonPath = join(componentsDir, "FeedbackButton.tsx");
|
|
297
|
+
if (safeWriteFile(feedbackButtonPath, FEEDBACK_BUTTON_TEMPLATE, overwrite)) {
|
|
298
|
+
console.log(` Created ${feedbackButtonPath}`);
|
|
299
|
+
}
|
|
166
300
|
const globalsCss = findGlobalsCss(cwd);
|
|
167
301
|
if (globalsCss) {
|
|
168
302
|
if (injectSourceDirective(globalsCss)) {
|
|
@@ -178,28 +312,43 @@ async function main() {
|
|
|
178
312
|
const envPath = join(cwd, ".env.local");
|
|
179
313
|
appendEnvVar(envPath, "ANTHROPIC_API_KEY", widgetAnswers.apiKey);
|
|
180
314
|
appendEnvVar(envPath, "FEEDBACK_PASSWORD", widgetAnswers.password);
|
|
181
|
-
if (
|
|
182
|
-
appendEnvVar(envPath, "GITHUB_TOKEN", githubToken);
|
|
183
|
-
appendEnvVar(envPath, "GITHUB_REPO", githubRepo);
|
|
315
|
+
if (tier !== "chat") {
|
|
316
|
+
if (githubToken) appendEnvVar(envPath, "GITHUB_TOKEN", githubToken);
|
|
317
|
+
if (githubRepo) appendEnvVar(envPath, "GITHUB_REPO", githubRepo);
|
|
318
|
+
}
|
|
319
|
+
if (tier === "pipeline" && agentUrl) {
|
|
320
|
+
appendEnvVar(envPath, "AGENT_URL", agentUrl);
|
|
184
321
|
}
|
|
185
322
|
console.log(` Updated ${envPath}`);
|
|
323
|
+
if (tier !== "chat") {
|
|
324
|
+
console.log();
|
|
325
|
+
createGitHubLabels(cwd);
|
|
326
|
+
}
|
|
186
327
|
console.log();
|
|
187
|
-
console.log(" \u2500\u2500
|
|
328
|
+
console.log(" \u2500\u2500 Next Steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
188
329
|
console.log();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
console.log("
|
|
192
|
-
console.log(" import { FeedbackPanel } from '@nikitadmitrieff/feedback-chat'");
|
|
193
|
-
console.log(" import '@nikitadmitrieff/feedback-chat/styles.css'");
|
|
330
|
+
const usesSrc = existsSync(join(cwd, "src", "app"));
|
|
331
|
+
const importPath = usesSrc ? "@/components/FeedbackButton" : "@/components/FeedbackButton";
|
|
332
|
+
console.log(" 1. Add to your layout.tsx:");
|
|
194
333
|
console.log();
|
|
195
|
-
console.log(
|
|
196
|
-
console.log(" const [open, setOpen] = useState(false)");
|
|
197
|
-
console.log(" return <FeedbackPanel isOpen={open} onToggle={() => setOpen(!open)} />");
|
|
198
|
-
console.log(" }");
|
|
334
|
+
console.log(` import { FeedbackButton } from '${importPath}'`);
|
|
199
335
|
console.log();
|
|
200
|
-
console.log("
|
|
201
|
-
console.log("
|
|
202
|
-
console.log(
|
|
336
|
+
console.log(" // Inside <body>:");
|
|
337
|
+
console.log(" <FeedbackButton />");
|
|
338
|
+
console.log();
|
|
339
|
+
if (tier === "chat") {
|
|
340
|
+
console.log(" 2. Run npm run dev and open the app.");
|
|
341
|
+
console.log(" Click the feedback bar at the bottom, enter your password, and chat.");
|
|
342
|
+
} else if (tier === "github") {
|
|
343
|
+
console.log(" 2. Run npm run dev and open the app.");
|
|
344
|
+
console.log(" Submit feedback to see issues created on your repo.");
|
|
345
|
+
} else {
|
|
346
|
+
console.log(" 2. Run npm run dev and open the app.");
|
|
347
|
+
console.log(" Submit feedback to see issues created on your repo.");
|
|
348
|
+
console.log();
|
|
349
|
+
console.log(" 3. Deploy the agent service:");
|
|
350
|
+
console.log(" See docs/agent-deployment.md or run: npx feedback-chat deploy-agent");
|
|
351
|
+
}
|
|
203
352
|
console.log();
|
|
204
353
|
console.log(" Done.");
|
|
205
354
|
console.log();
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
3
|
type Stage = 'created' | 'queued' | 'running' | 'validating' | 'preview_ready' | 'deployed' | 'failed' | 'rejected';
|
|
4
|
+
type ActivityEntry = {
|
|
5
|
+
message: string;
|
|
6
|
+
time: string;
|
|
7
|
+
};
|
|
4
8
|
type StatusResponse = {
|
|
5
9
|
stage: Stage;
|
|
6
10
|
issueNumber: number;
|
|
@@ -9,6 +13,7 @@ type StatusResponse = {
|
|
|
9
13
|
previewUrl?: string;
|
|
10
14
|
prNumber?: number;
|
|
11
15
|
prUrl?: string;
|
|
16
|
+
activity?: ActivityEntry[];
|
|
12
17
|
};
|
|
13
18
|
type Conversation = {
|
|
14
19
|
id: string;
|
|
@@ -40,4 +45,4 @@ type PipelineTrackerProps = {
|
|
|
40
45
|
};
|
|
41
46
|
declare function PipelineTracker({ issueUrl, statusEndpoint, }: PipelineTrackerProps): react_jsx_runtime.JSX.Element;
|
|
42
47
|
|
|
43
|
-
export { type Conversation, FeedbackPanel, type FeedbackPanelProps, PipelineTracker, type Stage, type StatusResponse, useConversations };
|
|
48
|
+
export { type ActivityEntry, type Conversation, FeedbackPanel, type FeedbackPanelProps, PipelineTracker, type Stage, type StatusResponse, useConversations };
|
package/dist/client/index.js
CHANGED
|
@@ -1700,6 +1700,19 @@ var STAGE_INDEX = {
|
|
|
1700
1700
|
failed: -1,
|
|
1701
1701
|
rejected: -1
|
|
1702
1702
|
};
|
|
1703
|
+
var STAGE_MESSAGE = {
|
|
1704
|
+
created: "Creating issue\u2026",
|
|
1705
|
+
queued: "Waiting for agent\u2026",
|
|
1706
|
+
running: "Running\u2026",
|
|
1707
|
+
validating: "Checking build\u2026",
|
|
1708
|
+
preview_ready: "Deploying preview\u2026"
|
|
1709
|
+
};
|
|
1710
|
+
function formatElapsed(ms) {
|
|
1711
|
+
const totalSeconds = Math.floor(ms / 1e3);
|
|
1712
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
1713
|
+
const seconds = totalSeconds % 60;
|
|
1714
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
1715
|
+
}
|
|
1703
1716
|
var TERMINAL_STAGES = ["deployed", "failed", "rejected"];
|
|
1704
1717
|
var POLL_INTERVAL_MS = 5e3;
|
|
1705
1718
|
var PREVIEW_POLL_INTERVAL_MS = 15e3;
|
|
@@ -1763,6 +1776,8 @@ function PipelineTracker({
|
|
|
1763
1776
|
const [changeComment, setChangeComment] = useState6("");
|
|
1764
1777
|
const [confirmReject, setConfirmReject] = useState6(false);
|
|
1765
1778
|
const previousStageRef = useRef3(null);
|
|
1779
|
+
const stageStartRef = useRef3(Date.now());
|
|
1780
|
+
const [elapsed, setElapsed] = useState6(0);
|
|
1766
1781
|
const fetchStatus = useCallback3(async () => {
|
|
1767
1782
|
try {
|
|
1768
1783
|
const res = await fetch(`${statusEndpoint}?issue=${issueNumber}`);
|
|
@@ -1781,6 +1796,17 @@ function PipelineTracker({
|
|
|
1781
1796
|
previousStageRef.current = status.stage;
|
|
1782
1797
|
}
|
|
1783
1798
|
}, [status.stage]);
|
|
1799
|
+
useEffect3(() => {
|
|
1800
|
+
stageStartRef.current = Date.now();
|
|
1801
|
+
setElapsed(0);
|
|
1802
|
+
}, [status.stage]);
|
|
1803
|
+
useEffect3(() => {
|
|
1804
|
+
if (TERMINAL_STAGES.includes(status.stage)) return;
|
|
1805
|
+
const interval = setInterval(() => {
|
|
1806
|
+
setElapsed(Date.now() - stageStartRef.current);
|
|
1807
|
+
}, 1e3);
|
|
1808
|
+
return () => clearInterval(interval);
|
|
1809
|
+
}, [status.stage]);
|
|
1784
1810
|
useEffect3(() => {
|
|
1785
1811
|
setPipelineActive(issueNumber, "created");
|
|
1786
1812
|
fetchStatus();
|
|
@@ -1828,18 +1854,39 @@ function PipelineTracker({
|
|
|
1828
1854
|
const currentIndex = STAGE_INDEX[status.stage];
|
|
1829
1855
|
const isFailed = status.stage === "failed";
|
|
1830
1856
|
const failedAtIndex = isFailed ? getFailedStepIndex(previousStageRef.current) : -1;
|
|
1857
|
+
const progressIndex = isFailed ? failedAtIndex : currentIndex;
|
|
1858
|
+
const latestActivity = status.activity?.at(-1)?.message;
|
|
1859
|
+
const activityMessage = latestActivity || STAGE_MESSAGE[status.stage] || "";
|
|
1860
|
+
const isActive = !TERMINAL_STAGES.includes(status.stage);
|
|
1831
1861
|
return /* @__PURE__ */ jsxs10("div", { className: "rounded-2xl border border-border bg-card p-3 space-y-2", children: [
|
|
1832
|
-
/* @__PURE__ */
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1862
|
+
/* @__PURE__ */ jsxs10("div", { className: "relative", children: [
|
|
1863
|
+
/* @__PURE__ */ jsx14("div", { className: "absolute left-[7.5px] top-3 bottom-3 w-px bg-muted-foreground/15" }),
|
|
1864
|
+
progressIndex > 0 && /* @__PURE__ */ jsx14(
|
|
1865
|
+
"div",
|
|
1866
|
+
{
|
|
1867
|
+
className: `absolute left-[7.5px] top-3 w-px transition-all duration-700 ease-out ${isFailed ? "bg-destructive/50" : "bg-emerald-500/50"}`,
|
|
1868
|
+
style: { height: `${progressIndex * 24}px` }
|
|
1869
|
+
}
|
|
1870
|
+
),
|
|
1871
|
+
STEPS.map((step, i) => {
|
|
1872
|
+
const state = deriveStepState(i, currentIndex, failedAtIndex, status.stage);
|
|
1873
|
+
return /* @__PURE__ */ jsxs10("div", { children: [
|
|
1874
|
+
/* @__PURE__ */ jsxs10("div", { className: "relative flex items-center gap-2 h-6", children: [
|
|
1875
|
+
/* @__PURE__ */ jsx14(StepDot, { state }),
|
|
1876
|
+
/* @__PURE__ */ jsx14("span", { className: `text-xs truncate ${STEP_LABEL_CLASS[state]}`, children: step.label }),
|
|
1877
|
+
i === 0 && /* @__PURE__ */ jsxs10("span", { className: "ml-auto text-[11px] text-muted-foreground/60 shrink-0", children: [
|
|
1878
|
+
"#",
|
|
1879
|
+
issueNumber
|
|
1880
|
+
] })
|
|
1881
|
+
] }),
|
|
1882
|
+
state === "failed" && status.failReason && /* @__PURE__ */ jsx14("div", { className: "pl-6 pb-0.5", children: /* @__PURE__ */ jsx14("span", { className: "text-[10px] text-destructive/80 line-clamp-2", children: status.failReason }) }),
|
|
1883
|
+
state === "active" && isActive && activityMessage && /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 pl-6 h-5", children: [
|
|
1884
|
+
/* @__PURE__ */ jsx14("span", { className: "text-[10px] text-muted-foreground truncate flex-1", children: activityMessage }),
|
|
1885
|
+
/* @__PURE__ */ jsx14("span", { className: "text-[10px] text-muted-foreground/60 tabular-nums shrink-0", children: formatElapsed(elapsed) })
|
|
1886
|
+
] })
|
|
1887
|
+
] }, step.stage);
|
|
1888
|
+
})
|
|
1889
|
+
] }),
|
|
1843
1890
|
status.stage === "preview_ready" && status.previewUrl && /* @__PURE__ */ jsxs10("div", { className: "space-y-2.5 pt-2 border-t border-border", children: [
|
|
1844
1891
|
/* @__PURE__ */ jsxs10(
|
|
1845
1892
|
"a",
|
package/dist/server/index.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ type StatusHandlerConfig = {
|
|
|
37
37
|
agentUrl?: string;
|
|
38
38
|
};
|
|
39
39
|
type Stage = 'created' | 'queued' | 'running' | 'validating' | 'preview_ready' | 'deployed' | 'failed' | 'rejected';
|
|
40
|
+
type ActivityEntry = {
|
|
41
|
+
message: string;
|
|
42
|
+
time: string;
|
|
43
|
+
};
|
|
40
44
|
type StatusResponse = {
|
|
41
45
|
stage: Stage;
|
|
42
46
|
issueNumber: number;
|
|
@@ -45,6 +49,7 @@ type StatusResponse = {
|
|
|
45
49
|
previewUrl?: string;
|
|
46
50
|
prNumber?: number;
|
|
47
51
|
prUrl?: string;
|
|
52
|
+
activity?: ActivityEntry[];
|
|
48
53
|
};
|
|
49
54
|
/**
|
|
50
55
|
* Creates Next.js App Router GET and POST handlers for the feedback status endpoint.
|
|
@@ -112,4 +117,4 @@ declare function createGitHubIssue({ title, body, labels, }: {
|
|
|
112
117
|
labels?: string[];
|
|
113
118
|
}): Promise<string | null>;
|
|
114
119
|
|
|
115
|
-
export { type FeedbackHandlerConfig, type GitHubIssueCreator, type Stage, type StatusHandlerConfig, type StatusResponse, buildDefaultPrompt, createFeedbackHandler, createGitHubIssue, createStatusHandler, createTools };
|
|
120
|
+
export { type ActivityEntry, type FeedbackHandlerConfig, type GitHubIssueCreator, type Stage, type StatusHandlerConfig, type StatusResponse, buildDefaultPrompt, createFeedbackHandler, createGitHubIssue, createStatusHandler, createTools };
|
package/dist/server/index.js
CHANGED
|
@@ -284,17 +284,22 @@ async function getPreviewUrl(config, sha) {
|
|
|
284
284
|
}
|
|
285
285
|
return null;
|
|
286
286
|
}
|
|
287
|
-
async function
|
|
287
|
+
async function getActivity(config, issueNumber) {
|
|
288
288
|
const res = await fetch(
|
|
289
289
|
`${issueEndpoint(config, issueNumber)}/comments?per_page=5&direction=desc`,
|
|
290
290
|
{ headers: githubHeaders(config.token), cache: "no-store" }
|
|
291
291
|
);
|
|
292
|
-
if (!res.ok) return
|
|
292
|
+
if (!res.ok) return [];
|
|
293
293
|
const comments = await res.json();
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
if (!Array.isArray(comments)) return [];
|
|
295
|
+
return comments.slice(0, 3).map((c) => {
|
|
296
|
+
const raw = c.body ?? "";
|
|
297
|
+
const clean = raw.replace(/<[^>]*>/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]*`/g, "").replace(/[#*_~>\-|]/g, "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
298
|
+
return {
|
|
299
|
+
message: clean,
|
|
300
|
+
time: c.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
301
|
+
};
|
|
302
|
+
}).reverse();
|
|
298
303
|
}
|
|
299
304
|
async function isAgentRunning(agentUrl, issueNumber) {
|
|
300
305
|
if (!agentUrl) return false;
|
|
@@ -313,8 +318,13 @@ async function deriveStage(config, issueNumber, agentUrl) {
|
|
|
313
318
|
const labels = (issue.labels ?? []).map((l) => l.name);
|
|
314
319
|
const issueUrl = issue.html_url;
|
|
315
320
|
if (labels.includes("agent-failed")) {
|
|
316
|
-
const
|
|
317
|
-
|
|
321
|
+
const activity = await getActivity(config, issueNumber);
|
|
322
|
+
const failComment = activity.find((a) => a.message.startsWith("Agent failed:"));
|
|
323
|
+
let failReason = failComment?.message.replace("Agent failed:", "").trim();
|
|
324
|
+
if (failReason) {
|
|
325
|
+
failReason = failReason.replace(/<[^>]*>/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]*`/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
326
|
+
}
|
|
327
|
+
return { stage: "failed", issueUrl, failReason, activity };
|
|
318
328
|
}
|
|
319
329
|
if (labels.includes("rejected")) {
|
|
320
330
|
return { stage: "rejected", issueUrl };
|
|
@@ -454,7 +464,8 @@ async function handleRequestChanges(config, issueNumber, comment) {
|
|
|
454
464
|
await fetch(`${url}/comments`, {
|
|
455
465
|
method: "POST",
|
|
456
466
|
headers,
|
|
457
|
-
|
|
467
|
+
// Must match the string the agent's retry detection looks for
|
|
468
|
+
body: JSON.stringify({ body: `**Modifications demand\xE9es :**
|
|
458
469
|
|
|
459
470
|
${comment}` })
|
|
460
471
|
});
|
|
@@ -501,6 +512,10 @@ function createStatusHandler(config) {
|
|
|
501
512
|
if (!result) {
|
|
502
513
|
return Response.json({ error: "Issue not found" }, { status: 404 });
|
|
503
514
|
}
|
|
515
|
+
const terminal = ["deployed", "rejected"];
|
|
516
|
+
if (!terminal.includes(result.stage) && !result.activity) {
|
|
517
|
+
result.activity = await getActivity(ghConfig, issueNumber);
|
|
518
|
+
}
|
|
504
519
|
const response = { issueNumber, ...result };
|
|
505
520
|
return Response.json(response);
|
|
506
521
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nikitadmitrieff/feedback-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/client/index.js",
|
|
6
6
|
"types": "./dist/client/index.d.ts",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
-
"feedback-chat": "./dist/cli/init.js"
|
|
24
|
+
"feedback-chat": "./dist/cli/init.js",
|
|
25
|
+
"feedback-chat-deploy-agent": "./dist/cli/deploy-agent.js"
|
|
25
26
|
},
|
|
26
|
-
"files": ["dist"],
|
|
27
|
+
"files": ["dist", "README.md"],
|
|
27
28
|
"scripts": {
|
|
28
29
|
"build": "tsup && cp src/client/styles.css dist/styles.css",
|
|
29
30
|
"dev": "tsup --watch",
|