Skip to content

Commit df16170

Browse files
committed
More tweaks
1 parent b3595c7 commit df16170

3 files changed

Lines changed: 285 additions & 60 deletions

File tree

claude-agent-github-wiki/app/chat/[runId]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export default function ChatPage({ params }: { params: { runId: string } }) {
120120
})
121121
.subscribe();
122122

123+
// Store the channel reference so we can use it to send messages
124+
channelRef.current = channel;
125+
123126
// Cleanup on unmount
124127
return () => {
125128
console.log("[Chat] Cleaning up channel subscription");
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2+
import { logger, metadata, schemaTask, wait } from "@trigger.dev/sdk";
3+
import { mkdtemp, rm } from "node:fs/promises";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import { z } from "zod";
7+
8+
type CHUNK = { iteration: number; message: SDKMessage };
9+
10+
export type STREAMS = {
11+
claude: CHUNK;
12+
};
13+
14+
/**
15+
* Advanced example: Full development workflow with bash execution
16+
* - Isolated temp directory per task run
17+
* - Minimal environment variables
18+
* - Bash execution enabled for running commands
19+
* - Automatic cleanup
20+
* Good for: npm install, running tests, build commands, git operations
21+
*/
22+
export const codeTaskAdvanced = schemaTask({
23+
id: "claude-code-advanced",
24+
schema: z.object({
25+
prompt: z.string(),
26+
maxTurns: z.number().default(5),
27+
maxIterations: z.number().default(10),
28+
}),
29+
run: async ({ prompt, maxTurns, maxIterations }, { signal }) => {
30+
const abortController = new AbortController();
31+
32+
signal.addEventListener("abort", () => {
33+
abortController.abort();
34+
});
35+
36+
// Create a unique isolated directory for this task run
37+
const tempDir = await mkdtemp(join(tmpdir(), "claude-agent-adv-"));
38+
39+
// Minimal environment variables
40+
// Add NODE_ENV, npm/node paths if needed for your use case
41+
const safeEnv = {
42+
PATH: process.env.PATH ?? "",
43+
TMPDIR: tempDir,
44+
// Uncomment if needed for npm/node operations:
45+
// NODE_ENV: "development",
46+
};
47+
48+
logger.log("Starting advanced agent loop", {
49+
cwd: tempDir,
50+
});
51+
52+
let $currentPrompt = prompt;
53+
let sessionId: string | undefined;
54+
55+
const { stream, write } = createStream<CHUNK>();
56+
57+
await metadata.stream("claude", stream);
58+
59+
try {
60+
for (let i = 0; i < maxIterations; i++) {
61+
logger.info("Starting iteration", {
62+
iteration: i,
63+
prompt: $currentPrompt,
64+
sessionId,
65+
});
66+
67+
const messages: SDKMessage[] = [];
68+
69+
const result = query({
70+
prompt: $currentPrompt,
71+
options: {
72+
model: "claude-sonnet-4-20250514",
73+
maxThinkingTokens: 8192,
74+
abortController,
75+
resume: sessionId,
76+
cwd: tempDir,
77+
env: safeEnv,
78+
maxTurns,
79+
// SECURITY: "acceptEdits" auto-approves file edits but still requires
80+
// the Bash tool to be in allowedTools list for command execution
81+
// Alternative modes:
82+
permissionMode: "acceptEdits",
83+
allowedTools: [
84+
// All tools including Bash for full development workflow
85+
"Task",
86+
"Bash", // SECURITY: Enables command execution
87+
"Glob",
88+
"Grep",
89+
"Read",
90+
"Edit",
91+
"Write",
92+
"TodoRead",
93+
"TodoWrite",
94+
// Optional: enable if needed
95+
// "WebFetch",
96+
// "WebSearch",
97+
],
98+
},
99+
});
100+
101+
for await (const message of result) {
102+
if (message.type === "system" && message.subtype === "init") {
103+
sessionId = message.session_id;
104+
}
105+
106+
messages.push(message);
107+
write({ iteration: i, message });
108+
logger.log("message", { message, iteration: i });
109+
}
110+
111+
await saveMessages(messages);
112+
113+
// This creates a token that will be used to continue the conversation
114+
const continueToken = await wait.createToken({ timeout: "7d" });
115+
116+
const nextPrompt = await wait.forToken<{ prompt: string }>(
117+
continueToken,
118+
);
119+
120+
if (nextPrompt.ok) {
121+
logger.info("Continuing with prompt", {
122+
prompt: nextPrompt.output.prompt,
123+
});
124+
125+
$currentPrompt = nextPrompt.output.prompt;
126+
} else {
127+
logger.info("No more prompts", { iteration: i });
128+
break;
129+
}
130+
}
131+
} finally {
132+
// Always cleanup the temp directory
133+
try {
134+
await rm(tempDir, { recursive: true, force: true });
135+
logger.info("Cleaned up temp directory", { tempDir });
136+
} catch (error) {
137+
logger.error("Failed to cleanup temp directory", {
138+
tempDir,
139+
error,
140+
});
141+
}
142+
}
143+
144+
return {
145+
success: true,
146+
};
147+
},
148+
});
149+
150+
export function createStream<T>(): {
151+
stream: ReadableStream<T>;
152+
write: (data: T) => void;
153+
} {
154+
let controller!: ReadableStreamDefaultController<T>;
155+
156+
const stream = new ReadableStream({
157+
start(controllerArg) {
158+
controller = controllerArg;
159+
},
160+
});
161+
162+
function safeEnqueue(data: T) {
163+
try {
164+
controller.enqueue(data);
165+
} catch (error) {
166+
// suppress errors when the stream has been closed
167+
}
168+
}
169+
170+
return {
171+
stream,
172+
write: safeEnqueue,
173+
};
174+
}
175+
176+
async function saveMessages(messages: SDKMessage[]) {
177+
logger.log("Saving messages", { messages });
178+
// TODO: save messages to a database for audit trail
179+
}

claude-agent-github-wiki/trigger/repo-chat-session.ts

Lines changed: 103 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,86 +16,129 @@ export const repoChatSession = schemaTask({
1616
run: async ({ tempDir, sessionId, repoName }, { signal }) => {
1717
console.log("[repo-chat] Starting session:", { sessionId, repoName });
1818

19-
// Create Supabase client - use publishable key for listening
19+
// Create Supabase client - use secret key for server-side operations
2020
const supabase = createClient(
2121
process.env.SUPABASE_URL!,
22-
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!,
22+
process.env.SUPABASE_SECRET_KEY!,
2323
{
2424
realtime: {
2525
params: { eventsPerSecond: 10 },
2626
},
27-
}
27+
},
2828
);
2929

30-
// // Create stream writer
31-
// const streamWriter = agentStream.writer();
32-
33-
// // Write initial message
34-
// streamWriter.write({
35-
// type: "text",
36-
// text: "Chat session ready! Ask questions about the repository.",
37-
// } as unknown as SDKMessage);
30+
// Write initial message
31+
agentStream.writer({
32+
execute: async ({ write }) => {
33+
// Send a properly formatted assistant message
34+
write({
35+
type: "assistant",
36+
message: {
37+
role: "assistant",
38+
content: [{
39+
type: "text",
40+
text: "Chat session ready! Ask questions about the repository.",
41+
}],
42+
},
43+
} as SDKMessage);
44+
},
45+
});
3846

39-
// Subscribe to Supabase channel AT TASK LEVEL (not inside writer!)
47+
// Create and subscribe to channel FIRST
48+
console.log("[repo-chat] Subscribing to channel...");
4049
const channel = supabase.channel(`session:${sessionId}`);
4150

42-
// Listen for questions
43-
channel.on("broadcast", { event: "question" }, async ({ payload }) => {
44-
console.log("[repo-chat] Question received:", payload?.question);
51+
const status = await new Promise<string>((resolve, reject) => {
52+
const timeout = setTimeout(() => {
53+
reject(new Error("Subscription timeout after 30s"));
54+
}, 30000);
4555

46-
const userQuestion = payload?.question;
47-
// if (!userQuestion) return;
48-
49-
// // Echo question
50-
// streamWriter.write({
51-
// type: "text",
52-
// text: `User: ${userQuestion}`,
53-
// } as unknown as SDKMessage);
54-
55-
// try {
56-
// // Process with Claude
57-
// const result = query({
58-
// prompt: userQuestion,
59-
// options: {
60-
// model: "claude-sonnet-4-20250514",
61-
// cwd: tempDir,
62-
// maxTurns: 10,
63-
// permissionMode: "acceptEdits",
64-
// allowedTools: ["Task", "Bash", "Glob", "Grep", "Read", "Edit", "Write"],
65-
// },
66-
// });
67-
68-
// // Stream responses
69-
// for await (const message of result) {
70-
// console.log("[repo-chat] Streaming:", message.type);
71-
// streamWriter.write(message);
72-
// }
73-
// } catch (error: any) {
74-
// console.error("[repo-chat] Error:", error.message);
75-
// streamWriter.write({
76-
// type: "text",
77-
// text: `Error: ${error.message}`,
78-
// } as unknown as SDKMessage);
79-
// }
80-
// });
81-
82-
// Subscribe to channel
83-
console.log("[repo-chat] Subscribing to channel...");
84-
const status = await new Promise<string>((resolve) => {
8556
channel.subscribe((status) => {
8657
console.log(`[repo-chat] Status: ${status}`);
87-
if (status === "SUBSCRIBED" || status === "CLOSED" || status === "CHANNEL_ERROR") {
58+
if (status === "SUBSCRIBED") {
59+
clearTimeout(timeout);
8860
resolve(status);
61+
} else if (status === "CLOSED" || status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
62+
clearTimeout(timeout);
63+
reject(new Error(`Subscription failed: ${status}`));
8964
}
9065
});
9166
});
9267

93-
if (status !== "SUBSCRIBED") {
94-
throw new Error(`Failed to subscribe: ${status}`);
95-
}
96-
9768
console.log("[repo-chat] ✅ Subscribed successfully");
9869

70+
// NOW add the event listener after successful subscription
71+
channel.on("broadcast", { event: "question" }, async ({ payload }) => {
72+
console.log("[repo-chat] Question received:", payload?.question);
73+
74+
const userQuestion = payload?.question;
75+
if (!userQuestion) return;
76+
77+
// Echo question
78+
agentStream.writer({
79+
execute: async ({ write }) => {
80+
write({
81+
type: "assistant",
82+
message: {
83+
role: "assistant",
84+
content: [{
85+
type: "text",
86+
text: `User: ${userQuestion}`,
87+
}],
88+
},
89+
} as SDKMessage);
90+
},
91+
});
92+
93+
try {
94+
// Process with Claude
95+
const result = query({
96+
prompt: userQuestion,
97+
options: {
98+
model: "claude-sonnet-4-20250514",
99+
cwd: tempDir,
100+
maxTurns: 10,
101+
permissionMode: "acceptEdits",
102+
allowedTools: [
103+
"Task",
104+
"Bash",
105+
"Glob",
106+
"Grep",
107+
"Read",
108+
"Edit",
109+
"Write",
110+
],
111+
},
112+
});
113+
114+
// Stream responses
115+
for await (const message of result) {
116+
console.log("[repo-chat] Streaming:", message.type);
117+
agentStream.writer({
118+
execute: async ({ write }) => {
119+
write(message);
120+
},
121+
});
122+
}
123+
} catch (error: any) {
124+
console.error("[repo-chat] Error:", error.message);
125+
agentStream.writer({
126+
execute: async ({ write }) => {
127+
write({
128+
type: "assistant",
129+
message: {
130+
role: "assistant",
131+
content: [{
132+
type: "text",
133+
text: `Error: ${error.message}`,
134+
}],
135+
},
136+
} as SDKMessage);
137+
},
138+
});
139+
}
140+
});
141+
99142
// Keep task alive until abort
100143
await new Promise((resolve) => {
101144
const interval = setInterval(() => {
@@ -119,4 +162,4 @@ export const repoChatSession = schemaTask({
119162

120163
return { sessionId, repoName, status: "completed" };
121164
},
122-
});
165+
});

0 commit comments

Comments
 (0)