Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
ai-editor.ts7.9 KB · 230 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
/**
 * AI editor API routes — inline suggestions, code explanation, and fix generation.
 *
 * All endpoints require an authenticated session and `ANTHROPIC_API_KEY`.
 * Rate-limited to 30 suggestion requests per user per hour (in-memory).
 *
 * Routes:
 *   POST /api/ai/suggest  — code completion (ghost text)
 *   POST /api/ai/explain  — explain selected code
 *   POST /api/ai/fix      — fix code given a GateTest / error message
 */

import { Hono } from "hono";
import { requireAuth } from "../middleware/auth";
import { isAiAvailable, getAnthropic, MODEL_HAIKU, MODEL_SONNET, extractText } from "../lib/ai-client";
import type { AuthEnv } from "../middleware/auth";

const aiEditor = new Hono<AuthEnv>();

// ─── Rate-limit store ────────────────────────────────────────────────────────
// Simple in-memory map: userId → array of request timestamps (ms).
// Cleaned lazily to avoid unbounded growth on high-traffic servers.
const QUOTA_WINDOW_MS = 60 * 60 * 1_000; // 1 hour
const QUOTA_MAX = 30;

const suggestQuota = new Map<number, number[]>();

function checkQuota(userId: number): { allowed: boolean; resetInMinutes: number } {
  const now = Date.now();
  const cutoff = now - QUOTA_WINDOW_MS;
  let timestamps = suggestQuota.get(userId) ?? [];
  // Purge expired entries
  timestamps = timestamps.filter((t) => t > cutoff);

  if (timestamps.length >= QUOTA_MAX) {
    const oldest = timestamps[0]!;
    const resetInMs = oldest + QUOTA_WINDOW_MS - now;
    const resetInMinutes = Math.ceil(resetInMs / 60_000);
    suggestQuota.set(userId, timestamps);
    return { allowed: false, resetInMinutes };
  }

  timestamps.push(now);
  suggestQuota.set(userId, timestamps);
  return { allowed: true, resetInMinutes: 0 };
}

// ─── POST /api/ai/suggest ────────────────────────────────────────────────────
aiEditor.post("/api/ai/suggest", requireAuth, async (c) => {
  if (!isAiAvailable()) {
    return c.json(
      { error: "AI suggestions unavailable — ANTHROPIC_API_KEY not configured." },
      503
    );
  }

  const user = c.get("user")!;
  const quota = checkQuota(user.id);
  if (!quota.allowed) {
    return c.json(
      {
        error: `AI suggestion quota reached. Resets in ${quota.resetInMinutes} minute${quota.resetInMinutes === 1 ? "" : "s"}.`,
      },
      429
    );
  }

  let body: { code?: string; language?: string; cursor?: number };
  try {
    body = await c.req.json();
  } catch {
    return c.json({ error: "Invalid JSON body." }, 400);
  }

  const code = String(body.code ?? "").slice(0, 2000);
  const language = String(body.language ?? "plaintext");
  const cursor = Number(body.cursor ?? code.length);

  const beforeCursor = code.slice(0, cursor);
  const afterCursor = code.slice(cursor);

  const prompt =
    `Language: ${language}\n\n` +
    `Code before cursor:\n${beforeCursor}\n\n` +
    (afterCursor.trim()
      ? `Code after cursor (context only — do NOT repeat it):\n${afterCursor}\n\n`
      : "") +
    "Complete the code at the cursor position. Return ONLY the completion text to insert, no explanation, no markdown, no code fences.";

  try {
    const anthropic = getAnthropic();
    const message = await anthropic.messages.create({
      model: MODEL_SONNET,
      max_tokens: 256,
      system:
        "You are a code completion AI. Complete the code snippet at the cursor. " +
        "Return ONLY the completion text — no explanation, no markdown, no code fences. " +
        "Keep completions concise (prefer single-line or a few lines). " +
        "If you have nothing useful to add, return an empty string.",
      messages: [{ role: "user", content: prompt }],
    });

    const suggestion = extractText(message).trimEnd();
    return c.json({ suggestion });
  } catch (err) {
    const msg = err instanceof Error ? err.message : "AI request failed.";
    return c.json({ error: msg }, 500);
  }
});

// ─── POST /api/ai/explain ────────────────────────────────────────────────────
aiEditor.post("/api/ai/explain", requireAuth, async (c) => {
  if (!isAiAvailable()) {
    return c.json(
      { error: "AI explanations unavailable — ANTHROPIC_API_KEY not configured." },
      503
    );
  }

  let body: { code?: string; language?: string };
  try {
    body = await c.req.json();
  } catch {
    return c.json({ error: "Invalid JSON body." }, 400);
  }

  const code = String(body.code ?? "").slice(0, 4000);
  const language = String(body.language ?? "plaintext");

  if (!code.trim()) {
    return c.json({ error: "No code provided." }, 400);
  }

  try {
    const anthropic = getAnthropic();
    const message = await anthropic.messages.create({
      model: MODEL_SONNET,
      max_tokens: 512,
      system:
        "You are a senior engineer explaining code to a fellow developer. " +
        "Be concise and precise. Use plain text (no markdown headers). " +
        "One or two short paragraphs maximum.",
      messages: [
        {
          role: "user",
          content: `Language: ${language}\n\nExplain this code:\n\`\`\`\n${code}\n\`\`\``,
        },
      ],
    });

    const explanation = extractText(message).trim();
    return c.json({ explanation });
  } catch (err) {
    const msg = err instanceof Error ? err.message : "AI request failed.";
    return c.json({ error: msg }, 500);
  }
});

// ─── POST /api/ai/fix ────────────────────────────────────────────────────────
aiEditor.post("/api/ai/fix", requireAuth, async (c) => {
  if (!isAiAvailable()) {
    return c.json(
      { error: "AI fix unavailable — ANTHROPIC_API_KEY not configured." },
      503
    );
  }

  let body: { code?: string; error?: string; language?: string };
  try {
    body = await c.req.json();
  } catch {
    return c.json({ error: "Invalid JSON body." }, 400);
  }

  const code = String(body.code ?? "").slice(0, 4000);
  const errorMsg = String(body.error ?? "").slice(0, 1000);
  const language = String(body.language ?? "plaintext");

  if (!code.trim()) {
    return c.json({ error: "No code provided." }, 400);
  }

  const prompt =
    `Language: ${language}\n\n` +
    `Error message:\n${errorMsg}\n\n` +
    `Code with the problem:\n\`\`\`\n${code}\n\`\`\`\n\n` +
    "Return a JSON object with two fields: " +
    '"fix" (the corrected code, complete file content) and ' +
    '"explanation" (one short sentence describing what was wrong).';

  try {
    const anthropic = getAnthropic();
    const message = await anthropic.messages.create({
      model: MODEL_SONNET,
      max_tokens: 1024,
      system:
        "You are an expert code fixer. Given an error and code, return valid JSON with " +
        '"fix" (corrected code, no markdown fences) and "explanation" (one-sentence reason). ' +
        "Return ONLY the JSON object, nothing else.",
      messages: [{ role: "user", content: prompt }],
    });

    const raw = extractText(message).trim();

    // Try to parse the JSON response
    let fix = "";
    let explanation = "";
    try {
      // Strip possible ```json fences
      const cleaned = raw
        .replace(/^```(?:json)?\s*/i, "")
        .replace(/\s*```$/i, "")
        .trim();
      const parsed = JSON.parse(cleaned);
      fix = String(parsed.fix ?? "");
      explanation = String(parsed.explanation ?? "");
    } catch {
      // Fallback: return raw as fix text
      fix = raw;
      explanation = "AI returned a fix but could not parse structured response.";
    }

    return c.json({ fix, explanation });
  } catch (err) {
    const msg = err instanceof Error ? err.message : "AI request failed.";
    return c.json({ error: msg }, 500);
  }
});

export default aiEditor;