File size: 15,110 Bytes
6111b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
/**

 * OmniRoute Copilot β€” Tool definitions

 *

 * Tools the copilot can execute to configure OmniRoute on behalf of the user,

 * query the codebase via CodeGraph, and execute CLI commands for full control.

 */

import { execSync } from "node:child_process";
import { createCombo, getCombos, updateCombo } from "@/lib/db/combos";
import { getProviderConnections } from "@/lib/db/providers";
import { createApiKey, revokeApiKey, getApiKeys } from "@/lib/db/apiKeys";
import {
  searchSymbols,
  findCallers,
  findCallees,
  getFileContext,
  listFiles,
  getCodeGraphStats,
  isCodeGraphAvailable,
  type CodeGraphQueryResult,
} from "./codegraphKnowledge";
import { getAllKeyGroups } from "@/lib/db/apiKeyGroups";

// ── Tool Types ───────────────────────────────────────────────────────────────

export interface CopilotToolParam {
  name: string;
  type: "string" | "number" | "boolean";
  description: string;
  required: boolean;
}

export interface CopilotTool {
  name: string;
  description: string;
  parameters: CopilotToolParam[];
  handler: (args: Record<string, unknown>) => Promise<string>;
}

// ── Helper: format CodeGraph results ─────────────────────────────────────────

function formatCodeGraphResult(result: CodeGraphQueryResult): string {
  if (!result.success) {
    if (result.engine === "none") {
      return `CodeGraph not available: ${result.error || "DB not found"}. The app runs without the development code index in production.`;
    }
    return `CodeGraph query error: ${result.error}`;
  }

  const rows = result.data as Record<string, unknown>[];
  if (!rows || rows.length === 0) return "No results found.";

  return (
    JSON.stringify(rows.slice(0, 30), null, 2) +
    (rows.length > 30 ? `\n... and ${rows.length - 30} more` : "")
  );
}

// ── Helper: check if omniroute CLI is available ──────────────────────────────

function getOmniRouteCliPath(): string | null {
  try {
    const result = execSync("which omniroute 2>/dev/null || command -v omniroute 2>/dev/null", {
      encoding: "utf-8",
      timeout: 3000,
    }).trim();
    return result || null;
  } catch {
    return null;
  }
}

// ── Tool Definitions ─────────────────────────────────────────────────────────

export const COPILOT_TOOLS: CopilotTool[] = [
  // ── Provider Tools ──
  {
    name: "listProviders",
    description:
      "List all configured provider connections, optionally filtered by type (apikey, oauth, local, free)",
    parameters: [
      {
        name: "type",
        type: "string",
        description: "Filter: apikey, oauth, local, free, or empty for all",
        required: false,
      },
    ],
    handler: async (args) => {
      const filter: Record<string, unknown> = {};
      if (args.type) filter.type = args.type;
      const connections = await getProviderConnections(filter);
      const connectionsAny = connections as any[];
      if (connectionsAny.length === 0) return "No provider connections found.";
      let output = `**${connectionsAny.length} provider(s) configured**\n\n`;
      for (const c of connectionsAny) {
        const status = c.isActive ? "βœ…" : "β›”";
        const models = c.models
          ? `(${(Array.isArray(c.models) ? c.models : JSON.parse(c.models || "[]")).length} models)`
          : "";
        output += `${status} **${c.displayName || c.name}** β€” \`${c.id}\` (${c.type}) ${models}\n`;
      }
      return output;
    },
  },

  // ── Combo Tools ──
  {
    name: "listCombos",
    description: "List all configured combos with their strategy and target count",
    parameters: [],
    handler: async () => {
      const combos = await getCombos();
      if (!combos || combos.length === 0)
        return "No combos configured. Create one with createCombo.";
      let output = `**${combos.length} combo(s) configured**\n\n`;
      for (const c of combos as any[]) {
        const active = c.isActive ? "βœ…" : "β›”";
        const targets = c.targets
          ? typeof c.targets === "string"
            ? JSON.parse(c.targets).length
            : c.targets.length
          : 0;
        output += `${active} **${c.name}** β€” strategy: \`${c.strategy}\` β€” ${targets} target(s)\n`;
      }
      return output;
    },
  },
  {
    name: "createCombo",
    description: "Create a new routing combo with specified targets",
    parameters: [
      { name: "name", type: "string", description: "Combo display name", required: true },
      {
        name: "strategy",
        type: "string",
        description: "Routing strategy: priority, weighted, round-robin, cost-optimized, auto",
        required: true,
      },
      {
        name: "targets",
        type: "string",
        description: "JSON array of targets: [{provider, model, weight?}]",
        required: true,
      },
    ],
    handler: async (args) => {
      const name = args.name as string;
      const strategy = args.strategy as string;
      if (!name || !strategy) return "Error: name and strategy are required.";
      let targets: unknown[];
      try {
        targets = JSON.parse(args.targets as string);
      } catch {
        return "Error: targets must be valid JSON array.";
      }
      if (!Array.isArray(targets) || targets.length === 0) {
        return "Error: targets must be a non-empty array.";
      }
      const combo = await createCombo({
        name,
        strategy,
        targets: JSON.stringify(targets),
        isActive: true,
      });
      const anyCombo = combo as any;
      return `βœ… Combo **${anyCombo.name || name}** created (ID: \`${anyCombo.id || "?"}\`) with ${targets.length} target(s).`;
    },
  },

  // ── API Key Tools ──
  {
    name: "listApiKeys",
    description: "List all API keys with their status and scope",
    parameters: [],
    handler: async () => {
      const keys = await getApiKeys();
      const keysAny = keys as any[];
      if (!keysAny || keysAny.length === 0) return "No API keys configured.";
      let output = `**${keysAny.length} API key(s)**\n\n`;
      for (const k of keysAny) {
        const status = k.isActive && !k.revokedAt ? "βœ…" : "β›”";
        output += `${status} **${k.name}** β€” \`${k.keyPrefix || k.id}\` β€” ${k.scopes ? JSON.stringify(k.scopes) : "no scopes"}\n`;
      }
      return output;
    },
  },
  {
    name: "createApiKey",
    description: "Create a new API key with optional scopes",
    parameters: [
      { name: "name", type: "string", description: "Human-readable key name", required: true },
      { name: "machineId", type: "string", description: "Machine identifier", required: false },
      {
        name: "scopes",
        type: "string",
        description: "Comma-separated scopes (e.g., manage,read)",
        required: false,
      },
    ],
    handler: async (args) => {
      const name = args.name as string;
      if (!name) return "Error: name is required.";
      const scopes = args.scopes
        ? (args.scopes as string).split(",").map((s) => s.trim())
        : undefined;
      const result = await createApiKey(name, (args.machineId as string) || "copilot", scopes);
      const r = result as any;
      return `βœ… API key **${name}** created:\n\`\`\`\n${r.key}\n\`\`\`\nSave this now β€” it won't be shown again.`;
    },
  },
  {
    name: "revokeApiKey",
    description: "Revoke an API key by ID",
    parameters: [
      { name: "id", type: "string", description: "API key ID to revoke", required: true },
    ],
    handler: async (args) => {
      const id = args.id as string;
      if (!id) return "Error: id is required.";
      await revokeApiKey(id);
      return `βœ… API key \`${id}\` revoked.`;
    },
  },

  // ── Key Group Tools ──
  {
    name: "listKeyGroups",
    description: "List all API key groups with their model permissions",
    parameters: [],
    handler: async () => {
      const groups = await getAllKeyGroups();
      const gArr = groups as any[];
      if (!gArr || gArr.length === 0) return "No key groups configured.";
      let output = `**${gArr.length} key group(s)**\n\n`;
      for (const g of gArr) {
        const perms = g.allowedModels
          ? (typeof g.allowedModels === "string"
              ? JSON.parse(g.allowedModels)
              : g.allowedModels
            ).join(", ")
          : "all models";
        output += `πŸ“¦ **${g.name}** β€” models: ${perms}\n`;
      }
      return output;
    },
  },

  // ── CodeGraph Tools ──
  {
    name: "searchCodeGraph",
    description:
      "Search for symbols in the OmniRoute codebase by name (functions, classes, types, variables). Use this to understand how the app works internally.",
    parameters: [
      {
        name: "query",
        type: "string",
        description:
          "Symbol name or partial name to search (e.g., 'handleChat', 'sanitizeMessage', 'CircuitBreaker')",
        required: true,
      },
      { name: "limit", type: "number", description: "Max results (default 20)", required: false },
    ],
    handler: async (args) => {
      const q = args.query as string;
      const limit = (args.limit as number) || 20;
      if (!q) return "Please provide a search query.";
      const result = searchSymbols(q, limit);
      return formatCodeGraphResult(result);
    },
  },
  {
    name: "findCallers",
    description:
      "Find all code that calls or references a specific function/symbol. Useful for impact analysis β€” 'what would break if I changed X?'",
    parameters: [
      {
        name: "symbol",
        type: "string",
        description: "Symbol name to find callers for (e.g., 'handleChatCore', 'translateRequest')",
        required: true,
      },
      { name: "limit", type: "number", description: "Max results (default 20)", required: false },
    ],
    handler: async (args) => {
      const symbol = args.symbol as string;
      const limit = (args.limit as number) || 20;
      if (!symbol) return "Please provide a symbol name.";
      const result = findCallers(symbol, limit);
      return formatCodeGraphResult(result);
    },
  },
  {
    name: "findCallees",
    description:
      "Find all functions/symbols that a specific function calls. Useful for understanding dependencies and code flow within OmniRoute.",
    parameters: [
      {
        name: "symbol",
        type: "string",
        description: "Symbol name to find callees for (e.g., 'handleChatCore', 'getExecutor')",
        required: true,
      },
      { name: "limit", type: "number", description: "Max results (default 20)", required: false },
    ],
    handler: async (args) => {
      const symbol = args.symbol as string;
      const limit = (args.limit as number) || 20;
      if (!symbol) return "Please provide a symbol name.";
      const result = findCallees(symbol, limit);
      return formatCodeGraphResult(result);
    },
  },
  {
    name: "getFileContext",
    description:
      "Get all symbols defined in a specific file. Useful to understand a file's exports and structure at a glance.",
    parameters: [
      {
        name: "filePath",
        type: "string",
        description: "File path (partial or full, e.g., 'chatCore.ts', 'combo.ts', 'src/lib/db/')",
        required: true,
      },
    ],
    handler: async (args) => {
      const fp = args.filePath as string;
      if (!fp) return "Please provide a file path.";
      const result = getFileContext(fp);
      return formatCodeGraphResult(result);
    },
  },
  {
    name: "listCodeGraphFiles",
    description:
      "List all files indexed by CodeGraph, optionally filtered by language. Tells you what parts of the codebase are available for analysis.",
    parameters: [
      {
        name: "language",
        type: "string",
        description: "Filter by language: typescript, javascript, python, etc.",
        required: false,
      },
    ],
    handler: async (args) => {
      const lang = args.language as string | undefined;
      const result = listFiles(lang);
      return formatCodeGraphResult(result);
    },
  },
  {
    name: "codeGraphStats",
    description:
      "Get summary stats about the CodeGraph index: total nodes, edges, files, languages, and node kinds indexed.",
    parameters: [],
    handler: async () => {
      const result = getCodeGraphStats();
      return formatCodeGraphResult(result);
    },
  },

  // ── CLI Execution Tool ──
  {
    name: "runOmniRouteCli",
    description:
      "Execute an 'omniroute' CLI command to configure or query the OmniRoute app. Gives complete control over the app β€” use for advanced operations not covered by other tools. Common commands: omniroute list-keys, omniroute switch-combo [id], omniroute set-budget 10, omniroute set-strategy [id] priority, omniroute health, omniroute mcp (starts MCP server), omniroute db-health, omniroute reset-password.",
    parameters: [
      {
        name: "command",
        type: "string",
        description:
          "CLI command arguments (everything after 'omniroute'). Example: 'list-keys', 'switch-combo abc123', 'health'",
        required: true,
      },
    ],
    handler: async (args) => {
      const cmd = args.command as string;
      if (!cmd) return "Please provide a command to execute.";

      const cliPath = getOmniRouteCliPath();
      if (!cliPath) return "omniroute CLI not found in PATH. Install OmniRoute first.";

      try {
        const output = execSync(`omniroute ${cmd}`, {
          encoding: "utf-8",
          timeout: 30000,
          maxBuffer: 1024 * 1024,
        });
        return `\`\`\`\n${output.trim()}\n\`\`\``;
      } catch (err: unknown) {
        const e = err as { stderr?: string; stdout?: string; message?: string };
        return `Error executing CLI command:\n${e.stderr || e.stdout || e.message || "Unknown error"}`;
      }
    },
  },
];

// ── Tool Lookup ──────────────────────────────────────────────────────────────

export function getCopilotTool(name: string): CopilotTool | undefined {
  return COPILOT_TOOLS.find((t) => t.name === name);
}

export function getCopilotToolDescriptions(): string {
  return COPILOT_TOOLS.map((t) => `- **${t.name}**: ${t.description}`).join("\n");
}