capyai 0.3.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capyai",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Unofficial Capy.ai CLI for agent orchestration with quality gates",
6
6
  "bin": {
package/src/api.ts CHANGED
@@ -1,15 +1,10 @@
1
1
  import * as config from "./config.js";
2
2
  import type { Task, Thread, ThreadMessage, DiffData, Model, ListResponse, PullRequestRef } from "./types.js";
3
3
 
4
- async function request(method: string, path: string, body?: unknown): Promise<any> {
5
- const cfg = config.load();
6
- if (!cfg.apiKey) {
7
- console.error("capy: API key not configured. Run: capy init");
8
- process.exit(1);
9
- }
10
- const url = `${cfg.server}${path}`;
4
+ async function rawRequest(apiKey: string, server: string, method: string, path: string, body?: unknown): Promise<any> {
5
+ const url = `${server}${path}`;
11
6
  const headers: Record<string, string> = {
12
- "Authorization": `Bearer ${cfg.apiKey}`,
7
+ "Authorization": `Bearer ${apiKey}`,
13
8
  "Accept": "application/json",
14
9
  };
15
10
  const init: RequestInit = { method, headers };
@@ -40,6 +35,34 @@ async function request(method: string, path: string, body?: unknown): Promise<an
40
35
  }
41
36
  }
42
37
 
38
+ async function request(method: string, path: string, body?: unknown): Promise<any> {
39
+ const cfg = config.load();
40
+ if (!cfg.apiKey) {
41
+ console.error("capy: API key not configured. Run: capy init");
42
+ process.exit(1);
43
+ }
44
+ return rawRequest(cfg.apiKey, cfg.server, method, path, body);
45
+ }
46
+
47
+ // --- Init helpers (accept key directly, before config is saved) ---
48
+ export interface Project {
49
+ id: string;
50
+ name: string;
51
+ description?: string | null;
52
+ taskCode: string;
53
+ repos: { repoFullName: string; branch: string }[];
54
+ }
55
+
56
+ export async function listProjects(apiKey: string, server = "https://capy.ai/api/v1"): Promise<Project[]> {
57
+ const data = await rawRequest(apiKey, server, "GET", "/projects");
58
+ return data.items || [];
59
+ }
60
+
61
+ export async function listModelsWithKey(apiKey: string, server = "https://capy.ai/api/v1"): Promise<Model[]> {
62
+ const data = await rawRequest(apiKey, server, "GET", "/models");
63
+ return data.models || [];
64
+ }
65
+
43
66
  // --- Threads ---
44
67
  export async function createThread(prompt: string, model?: string, repos?: unknown[]): Promise<Thread> {
45
68
  const cfg = config.load();
@@ -7,55 +7,92 @@ export const init = defineCommand({
7
7
  async run() {
8
8
  const p = await import("@clack/prompts");
9
9
  const config = await import("../config.js");
10
+ const api = await import("../api.js");
10
11
 
11
12
  p.intro("capy setup");
12
13
 
13
14
  const cfg = config.load();
14
- const result = await p.group({
15
- apiKey: () => p.password({
16
- message: "Capy API key",
17
- mask: "*",
18
- validate: (v) => { if (!v) return "API key is required"; },
19
- }),
20
- projectId: () => p.text({
21
- message: "Project ID",
22
- initialValue: cfg.projectId || "",
23
- validate: (v) => { if (!v) return "Project ID is required"; },
24
- }),
25
- repos: () => p.text({
26
- message: "Repos (owner/repo:branch, comma-separated)",
27
- initialValue: cfg.repos.map(r => `${r.repoFullName}:${r.branch}`).join(", ") || "",
28
- placeholder: "owner/repo:main",
29
- }),
30
- defaultModel: () => p.text({
31
- message: "Default model",
32
- initialValue: cfg.defaultModel,
33
- }),
34
- reviewProvider: () => p.select({
35
- message: "Review provider",
36
- initialValue: cfg.quality?.reviewProvider || "greptile",
37
- options: [
38
- { value: "greptile", label: "Greptile", hint: "AI code review via Greptile API" },
39
- { value: "capy", label: "Capy", hint: "GitHub unresolved review threads" },
40
- { value: "both", label: "Both", hint: "Strictest: Greptile + GitHub threads" },
41
- { value: "none", label: "None", hint: "Skip review gates" },
42
- ],
43
- }),
15
+
16
+ // 1. API key
17
+ const apiKey = await p.password({
18
+ message: "Capy API key",
19
+ mask: "*",
20
+ validate: (v) => { if (!v) return "API key is required"; },
44
21
  });
22
+ if (p.isCancel(apiKey)) { p.cancel("Setup cancelled."); process.exit(0); }
23
+
24
+ // 2. Fetch projects + models in parallel using the key
25
+ const s = p.spinner();
26
+ s.start("Fetching your projects and models...");
27
+ const [projects, models] = await Promise.all([
28
+ api.listProjects(apiKey, cfg.server),
29
+ api.listModelsWithKey(apiKey, cfg.server),
30
+ ]);
31
+ s.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}, ${models.length} models`);
45
32
 
46
- if (p.isCancel(result)) {
47
- p.cancel("Setup cancelled.");
48
- process.exit(0);
33
+ if (!projects.length) {
34
+ p.log.error("No projects found for this API key. Create one at capy.ai first.");
35
+ process.exit(1);
49
36
  }
50
37
 
51
- cfg.apiKey = result.apiKey;
52
- cfg.projectId = result.projectId;
53
- cfg.repos = (result.repos || "").split(",").filter(Boolean).map(s => {
54
- const [repo, branch] = s.trim().split(":");
55
- return { repoFullName: repo, branch: branch || "main" };
38
+ // 3. Select project
39
+ const projectId = projects.length === 1
40
+ ? (() => { p.log.info(`Project: ${projects[0].name} (${projects[0].taskCode})`); return projects[0].id; })()
41
+ : await (async () => {
42
+ const sel = await p.select({
43
+ message: "Select project",
44
+ initialValue: cfg.projectId || projects[0].id,
45
+ options: projects.map(proj => ({
46
+ value: proj.id,
47
+ label: proj.name,
48
+ hint: `${proj.taskCode} \u2022 ${proj.repos.length} repo${proj.repos.length !== 1 ? "s" : ""}`,
49
+ })),
50
+ });
51
+ if (p.isCancel(sel)) { p.cancel("Setup cancelled."); process.exit(0); }
52
+ return sel;
53
+ })();
54
+
55
+ const selectedProject = projects.find(proj => proj.id === projectId)!;
56
+
57
+ // 4. Show repos from project (auto-populated, no typing needed)
58
+ if (selectedProject.repos.length) {
59
+ p.log.info(`Repos:\n${selectedProject.repos.map(r => ` ${r.repoFullName} (${r.branch})`).join("\n")}`);
60
+ } else {
61
+ p.log.warn("No repos configured on this project. Add them at capy.ai.");
62
+ }
63
+
64
+ // 5. Select default model
65
+ const captainModels = models.filter(m => m.captainEligible);
66
+ const defaultModel = await p.select({
67
+ message: "Default model",
68
+ initialValue: cfg.defaultModel || "gpt-5.4",
69
+ options: captainModels.map(m => ({
70
+ value: m.id,
71
+ label: m.name || m.id,
72
+ hint: m.provider || undefined,
73
+ })),
56
74
  });
57
- cfg.defaultModel = result.defaultModel;
58
- cfg.quality.reviewProvider = result.reviewProvider as string;
75
+ if (p.isCancel(defaultModel)) { p.cancel("Setup cancelled."); process.exit(0); }
76
+
77
+ // 6. Review provider
78
+ const reviewProvider = await p.select({
79
+ message: "Review provider",
80
+ initialValue: cfg.quality?.reviewProvider || "greptile",
81
+ options: [
82
+ { value: "greptile", label: "Greptile", hint: "AI code review via Greptile API" },
83
+ { value: "capy", label: "Capy", hint: "GitHub unresolved review threads" },
84
+ { value: "both", label: "Both", hint: "Strictest: Greptile + GitHub threads" },
85
+ { value: "none", label: "None", hint: "Skip review gates" },
86
+ ],
87
+ });
88
+ if (p.isCancel(reviewProvider)) { p.cancel("Setup cancelled."); process.exit(0); }
89
+
90
+ // Save
91
+ cfg.apiKey = apiKey;
92
+ cfg.projectId = projectId as string;
93
+ cfg.repos = selectedProject.repos;
94
+ cfg.defaultModel = defaultModel as string;
95
+ cfg.quality.reviewProvider = reviewProvider as string;
59
96
 
60
97
  config.save(cfg);
61
98
  p.outro(`Config saved to ${config.CONFIG_PATH}`);
package/src/types.ts CHANGED
@@ -87,6 +87,7 @@ export interface DiffData {
87
87
 
88
88
  export interface Model {
89
89
  id: string;
90
+ name?: string;
90
91
  provider?: string;
91
92
  captainEligible?: boolean;
92
93
  }