Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^24.10.2",
"patch-package": "^8.0.1",
"typescript": "^5.5.0",
"vitest": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/consultationSessionDO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class ConsultationSessionDO extends DurableObject {
}

// Use blockConcurrencyWhile to ensure atomic updates
await this.ctx.storage.blockConcurrencyWhile(async () => {
await this.ctx.blockConcurrencyWhile(async () => {
const updates = (await this.ctx.storage.get('updates') as any[]) || [];
updates.push({
...update,
Expand Down
173 changes: 129 additions & 44 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export { ConsultationSessionDO } from './consultationSessionDO';
// Export RPC Entrypoint for Service Bindings
export { CloudflareManagerRPC } from './rpc-entrypoint';

// Export Workflow Entrypoints
export { ProvisioningWorkflow } from './workflows/provision';

// Create Hono app
const app = new Hono<{ Bindings: Env; Variables: Variables }>();

Expand Down Expand Up @@ -50,20 +53,19 @@ const authMiddleware = async (c: any, next: any) => {
/**
* Cloudflare SDK Initialization Middleware
* Initializes SDK with worker's own CLOUDFLARE_TOKEN
*
* OPTIMIZATION: Cloudflare client and AI instances are initialized once per request
* and stored in context (c.set) to avoid re-instantiating heavy objects.
* This follows the singleton-per-request pattern for optimal performance.
*/
const cfInitMiddleware = async (c: any, next: any) => {
// Temporarily log the types of all secrets to debug the binding issue
// console.log('--- Secret Binding Types ---');
// console.log('typeof c.env.CLOUDFLARE_ACCOUNT_ID:', typeof c.env.CLOUDFLARE_ACCOUNT_ID);
// console.log('typeof c.env.CLOUDFLARE_TOKEN:', typeof c.env.CLOUDFLARE_TOKEN);
// console.log('typeof c.env.CLIENT_AUTH_TOKEN:', typeof c.env.CLIENT_AUTH_TOKEN);
// console.log('--------------------------');

// Initialize Cloudflare SDK client (once per request)
const cf = new Cloudflare({ apiToken: c.env.CLOUDFLARE_TOKEN });

// Extract account ID from environment
const accountId = c.env.CLOUDFLARE_ACCOUNT_ID;

// Store in context for reuse throughout the request lifecycle
c.set('cf', cf);
c.set('accountId', accountId);
c.set('startTime', Date.now());
Expand All @@ -73,7 +75,7 @@ const cfInitMiddleware = async (c: any, next: any) => {
};

// PATCHED: Token middleware fix for /api/tokens routes
const apiClientMiddleware = async (c: Context<{ Bindings: Env; Variables: Variables }>, next: Next) => {
const apiClientMiddleware = async (c: any, next: any) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The parameters for apiClientMiddleware were changed from specific Hono types to any. This is a regression in type safety that can hide potential bugs and makes the code harder to maintain. Please consider reverting to using the Context and Next types to leverage TypeScript's full benefits. You may need to import them from 'hono'.

Suggested change
const apiClientMiddleware = async (c: any, next: any) => {
const apiClientMiddleware = async (c: Context<{ Bindings: Env; Variables: Variables }>, next: Next) => {

const urlPath = new URL(c.req.url).pathname;
const isUserTokenRoute = urlPath.startsWith('/api/tokens');

Expand Down Expand Up @@ -317,58 +319,141 @@ app.post('/mcp', async (c) => {
});

/**
* AI Agent Endpoint
* Natural language interface with cloudflare-docs integration
* AI Agent Endpoint with ReAct Loop
* Uses Llama 3 for reasoning and tool execution
*/
app.post('/agent', async (c) => {
try {
const { prompt } = await c.req.json();
const { prompt, conversationHistory = [] } = await c.req.json();

// Check if AI binding is available
if (!c.env.AI) {
return c.json({
success: false,
error: 'AI binding not configured. Please add AI binding to wrangler.jsonc'
}, 500);
}

const cf = c.get('cf');
const accountId = c.get('accountId');
const { listMCPTools, getMCPTool } = await import('./mcp/index');
const tools = listMCPTools();

// Build system prompt with available tools
const systemPrompt = `You are an AI assistant that helps manage Cloudflare infrastructure.

You have access to the following tools:
${tools.map(tool => `- ${tool.name}: ${tool.description}`).join('\n')}

When you need to use a tool, respond with a JSON object in this format:
{
"tool": "tool_name",
"arguments": { ...tool arguments... },
"reasoning": "Why you're using this tool"
}

// Basic intent detection (in production, use Workers AI or external LLM)
const promptLower = prompt.toLowerCase();
After receiving tool results, provide a natural language response to the user.

Always be helpful, concise, and accurate. If you're unsure, ask for clarification.`;

// ReAct Loop - Maximum 5 iterations to prevent infinite loops
const maxIterations = 5;
let iteration = 0;
const actions: any[] = [];
let response = '';

if (promptLower.includes('create') && promptLower.includes('token')) {
// Token creation flow
// In production, agent would:
// 1. Use cloudflare-docs MCP to lookup permissions
// 2. Determine exact permissions needed
// 3. Call /flows/token/create
response = `To create a token, I need to know:
1. What will this token be used for?
2. Which resources does it need access to?
3. Should it have an expiration (TTL)?

I can use the cloudflare-docs to determine the exact permissions needed. Please provide more details about the token's purpose.`;
} else if (promptLower.includes('list') && promptLower.includes('worker')) {
const workers = await cf.workers.scripts.list({ account_id: accountId });
console.log(JSON.stringify(workers));
actions.push({ type: 'list_workers', result: workers });
const workerCount = Array.isArray(workers.result) ? workers.result.length : 0;
response = `Found ${workerCount} workers in your account.`;
} else {
response = `I can help you manage your Cloudflare infrastructure. I can:

- Create managed API tokens (with secure storage and auditing)
- List and manage Workers, Pages, and storage resources
- Create complete project stacks with bindings
- Setup CI/CD pipelines
- And more...

What would you like to do?`;
let finalResponse = '';

// Build conversation context
const messages = [
{ role: 'system', content: systemPrompt },
...conversationHistory,
{ role: 'user', content: prompt },
];

while (iteration < maxIterations) {
iteration++;

// Call Llama 3 model
const aiResponse = await c.env.AI.run('@cf/meta/llama-3-8b-instruct', {
messages,
max_tokens: 512,
});

const responseText = aiResponse.response || '';

// Check if the response contains a tool call (JSON format)
let toolCall = null;
try {
// Try to extract JSON from the response - look for complete JSON objects
const jsonMatch = responseText.match(/\{[\s\S]*?\}(?=\s|$)/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
// Validate it has the expected structure for a tool call
if (parsed.tool && typeof parsed.tool === 'string') {
toolCall = parsed;
}
}
Comment on lines +387 to +394
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regular expression used to extract the JSON tool call is brittle. It can fail if the model includes surrounding text or wraps the JSON in markdown code fences, which is a common behavior. To make parsing more reliable, I recommend updating the system prompt to instruct the model to use markdown fences (e.g., ```json ... ```) and using a more robust regex that can handle both raw JSON and fenced JSON.

        const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```|(\{[\s\S]*\})/);
        if (jsonMatch) {
          // Use the first non-null capture group, which will be either the content of the JSON block or the raw JSON object.
          const jsonString = jsonMatch[1] || jsonMatch[2];
          const parsed = JSON.parse(jsonString);
          // Validate it has the expected structure for a tool call
          if (parsed.tool && typeof parsed.tool === 'string') {
            toolCall = parsed;
          }
        }

} catch (e) {
// Not a valid tool call, treat as final response
}

if (toolCall && toolCall.tool) {
// Execute the tool
const tool = getMCPTool(toolCall.tool);

if (!tool) {
messages.push({
role: 'assistant',
content: `Error: Tool '${toolCall.tool}' not found. Available tools: ${tools.map(t => t.name).join(', ')}`
});
continue;
}

try {
const toolResult = await tool.handler(toolCall.arguments || {}, c.env);

actions.push({
tool: toolCall.tool,
arguments: toolCall.arguments,
reasoning: toolCall.reasoning,
result: toolResult,
});

// Add tool result to conversation
messages.push({
role: 'assistant',
content: `Used tool: ${toolCall.tool}`
});
messages.push({
role: 'user',
content: `Tool result: ${JSON.stringify(toolResult)}. Now provide a natural language response to the user.`
});
} catch (error: any) {
messages.push({
role: 'assistant',
content: `Error executing tool ${toolCall.tool}: ${error.message}`
});
}
} else {
// No tool call, this is the final response
finalResponse = responseText;
break;
}
}

if (iteration >= maxIterations && !finalResponse) {
finalResponse = 'I apologize, but I reached the maximum number of reasoning steps. Please try rephrasing your request or breaking it into smaller tasks.';
}

return c.json({
success: true,
result: {
message: response,
message: finalResponse,
actions,
iterations: iteration,
},
});
} catch (error: any) {
console.error('Agent error:', error);
return c.json({ success: false, error: error.message }, 500);
}
});
Expand Down
Loading