geeto 0.6.6 → 0.9.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.
Files changed (205) hide show
  1. package/README.md +23 -9
  2. package/lib/api/copilot-adapter.d.ts +14 -5
  3. package/lib/api/copilot-adapter.d.ts.map +1 -1
  4. package/lib/api/copilot-adapter.js +15 -21
  5. package/lib/api/copilot-adapter.js.map +1 -1
  6. package/lib/api/copilot-sdk.d.ts +3 -16
  7. package/lib/api/copilot-sdk.d.ts.map +1 -1
  8. package/lib/api/copilot-sdk.js +186 -454
  9. package/lib/api/copilot-sdk.js.map +1 -1
  10. package/lib/api/copilot.d.ts +3 -4
  11. package/lib/api/copilot.d.ts.map +1 -1
  12. package/lib/api/copilot.js +28 -28
  13. package/lib/api/copilot.js.map +1 -1
  14. package/lib/api/gemini-sdk.d.ts.map +1 -1
  15. package/lib/api/gemini-sdk.js +11 -77
  16. package/lib/api/gemini-sdk.js.map +1 -1
  17. package/lib/api/gemini.d.ts +2 -2
  18. package/lib/api/gemini.d.ts.map +1 -1
  19. package/lib/api/gemini.js +24 -19
  20. package/lib/api/gemini.js.map +1 -1
  21. package/lib/api/gitlab.d.ts +80 -0
  22. package/lib/api/gitlab.d.ts.map +1 -0
  23. package/lib/api/gitlab.js +192 -0
  24. package/lib/api/gitlab.js.map +1 -0
  25. package/lib/api/openrouter-sdk.d.ts.map +1 -1
  26. package/lib/api/openrouter-sdk.js +11 -76
  27. package/lib/api/openrouter-sdk.js.map +1 -1
  28. package/lib/api/openrouter.d.ts.map +1 -1
  29. package/lib/api/openrouter.js +2 -16
  30. package/lib/api/openrouter.js.map +1 -1
  31. package/lib/api/platform.d.ts +78 -0
  32. package/lib/api/platform.d.ts.map +1 -0
  33. package/lib/api/platform.js +218 -0
  34. package/lib/api/platform.js.map +1 -0
  35. package/lib/cli/input.d.ts +2 -2
  36. package/lib/cli/input.d.ts.map +1 -1
  37. package/lib/cli/input.js +23 -27
  38. package/lib/cli/input.js.map +1 -1
  39. package/lib/cli/menu.d.ts +1 -1
  40. package/lib/cli/menu.d.ts.map +1 -1
  41. package/lib/cli/menu.js +123 -100
  42. package/lib/cli/menu.js.map +1 -1
  43. package/lib/core/copilot-setup.d.ts +9 -8
  44. package/lib/core/copilot-setup.d.ts.map +1 -1
  45. package/lib/core/copilot-setup.js +81 -264
  46. package/lib/core/copilot-setup.js.map +1 -1
  47. package/lib/core/gemini-setup.js +7 -7
  48. package/lib/core/gemini-setup.js.map +1 -1
  49. package/lib/core/gitlab-setup.d.ts +5 -0
  50. package/lib/core/gitlab-setup.d.ts.map +1 -0
  51. package/lib/core/gitlab-setup.js +85 -0
  52. package/lib/core/gitlab-setup.js.map +1 -0
  53. package/lib/core/openrouter-setup.d.ts.map +1 -1
  54. package/lib/core/openrouter-setup.js +17 -0
  55. package/lib/core/openrouter-setup.js.map +1 -1
  56. package/lib/index.js +518 -704
  57. package/lib/index.js.map +1 -1
  58. package/lib/types/index.d.ts +10 -6
  59. package/lib/types/index.d.ts.map +1 -1
  60. package/lib/utils/ai-provider-helpers.d.ts +5 -0
  61. package/lib/utils/ai-provider-helpers.d.ts.map +1 -0
  62. package/lib/utils/ai-provider-helpers.js +23 -0
  63. package/lib/utils/ai-provider-helpers.js.map +1 -0
  64. package/lib/utils/ai-text.d.ts +23 -0
  65. package/lib/utils/ai-text.d.ts.map +1 -0
  66. package/lib/utils/ai-text.js +57 -0
  67. package/lib/utils/ai-text.js.map +1 -0
  68. package/lib/utils/ai-workflow.d.ts +18 -0
  69. package/lib/utils/ai-workflow.d.ts.map +1 -0
  70. package/lib/utils/ai-workflow.js +66 -0
  71. package/lib/utils/ai-workflow.js.map +1 -0
  72. package/lib/utils/branch-naming.d.ts.map +1 -1
  73. package/lib/utils/branch-naming.js +1 -3
  74. package/lib/utils/branch-naming.js.map +1 -1
  75. package/lib/utils/config.d.ts +13 -1
  76. package/lib/utils/config.d.ts.map +1 -1
  77. package/lib/utils/config.js +38 -1
  78. package/lib/utils/config.js.map +1 -1
  79. package/lib/utils/display.d.ts.map +1 -1
  80. package/lib/utils/display.js +4 -3
  81. package/lib/utils/display.js.map +1 -1
  82. package/lib/utils/exec.d.ts.map +1 -1
  83. package/lib/utils/exec.js +10 -2
  84. package/lib/utils/exec.js.map +1 -1
  85. package/lib/utils/git-ai.js +13 -13
  86. package/lib/utils/git-ai.js.map +1 -1
  87. package/lib/utils/git-errors.d.ts.map +1 -1
  88. package/lib/utils/git-errors.js +2 -6
  89. package/lib/utils/git-errors.js.map +1 -1
  90. package/lib/utils/git.d.ts.map +1 -1
  91. package/lib/utils/git.js +5 -0
  92. package/lib/utils/git.js.map +1 -1
  93. package/lib/utils/github-helpers.d.ts +33 -0
  94. package/lib/utils/github-helpers.d.ts.map +1 -0
  95. package/lib/utils/github-helpers.js +101 -0
  96. package/lib/utils/github-helpers.js.map +1 -0
  97. package/lib/utils/prompt-loader.d.ts +9 -0
  98. package/lib/utils/prompt-loader.d.ts.map +1 -0
  99. package/lib/utils/prompt-loader.js +42 -0
  100. package/lib/utils/prompt-loader.js.map +1 -0
  101. package/lib/utils/prompts-embedded.d.ts +2 -0
  102. package/lib/utils/prompts-embedded.d.ts.map +1 -0
  103. package/lib/utils/prompts-embedded.js +255 -0
  104. package/lib/utils/prompts-embedded.js.map +1 -0
  105. package/lib/utils/scramble.d.ts +9 -86
  106. package/lib/utils/scramble.d.ts.map +1 -1
  107. package/lib/utils/scramble.js +27 -279
  108. package/lib/utils/scramble.js.map +1 -1
  109. package/lib/version.d.ts +1 -1
  110. package/lib/version.js +1 -1
  111. package/lib/workflows/alias.d.ts.map +1 -1
  112. package/lib/workflows/alias.js +1 -0
  113. package/lib/workflows/alias.js.map +1 -1
  114. package/lib/workflows/amend.d.ts.map +1 -1
  115. package/lib/workflows/amend.js +1 -5
  116. package/lib/workflows/amend.js.map +1 -1
  117. package/lib/workflows/branch-helpers.d.ts.map +1 -1
  118. package/lib/workflows/branch-helpers.js +0 -1
  119. package/lib/workflows/branch-helpers.js.map +1 -1
  120. package/lib/workflows/commit.d.ts.map +1 -1
  121. package/lib/workflows/commit.js +160 -187
  122. package/lib/workflows/commit.js.map +1 -1
  123. package/lib/workflows/doctor.d.ts +7 -0
  124. package/lib/workflows/doctor.d.ts.map +1 -0
  125. package/lib/workflows/doctor.js +284 -0
  126. package/lib/workflows/doctor.js.map +1 -0
  127. package/lib/workflows/issue.d.ts +1 -1
  128. package/lib/workflows/issue.d.ts.map +1 -1
  129. package/lib/workflows/issue.js +28 -115
  130. package/lib/workflows/issue.js.map +1 -1
  131. package/lib/workflows/main-helpers.d.ts +34 -0
  132. package/lib/workflows/main-helpers.d.ts.map +1 -0
  133. package/lib/workflows/main-helpers.js +346 -0
  134. package/lib/workflows/main-helpers.js.map +1 -0
  135. package/lib/workflows/main-steps.d.ts.map +1 -1
  136. package/lib/workflows/main-steps.js +9 -134
  137. package/lib/workflows/main-steps.js.map +1 -1
  138. package/lib/workflows/main.d.ts +2 -6
  139. package/lib/workflows/main.d.ts.map +1 -1
  140. package/lib/workflows/main.js +44 -381
  141. package/lib/workflows/main.js.map +1 -1
  142. package/lib/workflows/pr.d.ts +2 -2
  143. package/lib/workflows/pr.d.ts.map +1 -1
  144. package/lib/workflows/pr.js +49 -137
  145. package/lib/workflows/pr.js.map +1 -1
  146. package/lib/workflows/prune.d.ts.map +1 -1
  147. package/lib/workflows/prune.js +2 -10
  148. package/lib/workflows/prune.js.map +1 -1
  149. package/lib/workflows/pull.d.ts.map +1 -1
  150. package/lib/workflows/pull.js +2 -24
  151. package/lib/workflows/pull.js.map +1 -1
  152. package/lib/workflows/release-merge.d.ts +12 -0
  153. package/lib/workflows/release-merge.d.ts.map +1 -0
  154. package/lib/workflows/release-merge.js +593 -0
  155. package/lib/workflows/release-merge.js.map +1 -0
  156. package/lib/workflows/release-notes.d.ts +13 -0
  157. package/lib/workflows/release-notes.d.ts.map +1 -0
  158. package/lib/workflows/release-notes.js +141 -0
  159. package/lib/workflows/release-notes.js.map +1 -0
  160. package/lib/workflows/release-recover.d.ts +5 -0
  161. package/lib/workflows/release-recover.d.ts.map +1 -0
  162. package/lib/workflows/release-recover.js +137 -0
  163. package/lib/workflows/release-recover.js.map +1 -0
  164. package/lib/workflows/release-sync.d.ts +7 -0
  165. package/lib/workflows/release-sync.d.ts.map +1 -0
  166. package/lib/workflows/release-sync.js +378 -0
  167. package/lib/workflows/release-sync.js.map +1 -0
  168. package/lib/workflows/release-utils.d.ts +36 -0
  169. package/lib/workflows/release-utils.d.ts.map +1 -0
  170. package/lib/workflows/release-utils.js +150 -0
  171. package/lib/workflows/release-utils.js.map +1 -0
  172. package/lib/workflows/release.d.ts.map +1 -1
  173. package/lib/workflows/release.js +92 -719
  174. package/lib/workflows/release.js.map +1 -1
  175. package/lib/workflows/repo-settings.d.ts +2 -2
  176. package/lib/workflows/repo-settings.d.ts.map +1 -1
  177. package/lib/workflows/repo-settings.js +33 -24
  178. package/lib/workflows/repo-settings.js.map +1 -1
  179. package/lib/workflows/reword.d.ts.map +1 -1
  180. package/lib/workflows/reword.js +154 -151
  181. package/lib/workflows/reword.js.map +1 -1
  182. package/lib/workflows/security-gate.d.ts.map +1 -1
  183. package/lib/workflows/security-gate.js +15 -75
  184. package/lib/workflows/security-gate.js.map +1 -1
  185. package/lib/workflows/settings.d.ts +3 -1
  186. package/lib/workflows/settings.d.ts.map +1 -1
  187. package/lib/workflows/settings.js +319 -19
  188. package/lib/workflows/settings.js.map +1 -1
  189. package/lib/workflows/submodules.d.ts +6 -0
  190. package/lib/workflows/submodules.d.ts.map +1 -0
  191. package/lib/workflows/submodules.js +344 -0
  192. package/lib/workflows/submodules.js.map +1 -0
  193. package/lib/workflows/trello-menu.d.ts +2 -5
  194. package/lib/workflows/trello-menu.d.ts.map +1 -1
  195. package/lib/workflows/trello-menu.js +67 -228
  196. package/lib/workflows/trello-menu.js.map +1 -1
  197. package/package.json +4 -6
  198. package/prompts/branch-name-prompt.md +4 -0
  199. package/prompts/commit-message-prompt.md +12 -0
  200. package/prompts/issue-prompt.md +19 -0
  201. package/prompts/issue-review.with-context.prompt.yml +77 -0
  202. package/prompts/pr-prompt.md +14 -0
  203. package/prompts/release-notes-prompt.md +35 -0
  204. package/prompts/repo-description-prompt.md +1 -0
  205. package/prompts/security-gate-prompt.md +80 -0
@@ -1,426 +1,211 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
4
- import fs from 'node:fs';
5
- import os from 'node:os';
6
- import path from 'node:path';
7
- import { CopilotClient } from '@github/copilot-sdk';
8
- import { exec } from '../utils/exec.js';
9
- import { log } from '../utils/logging.js';
10
- // Copilot SDK wrapper (lazy-load, optional)
11
- // Minimum Copilot CLI version required for SDK compatibility
12
- export const MIN_COPILOT_VERSION = '0.0.400';
13
- // Cache file path for storing copilot binary info
14
- const CACHE_FILE = path.join(os.homedir(), '.cache', 'geeto', 'copilot-bin.json');
15
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
3
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
16
4
  /**
17
- * Parse version string to numeric value for comparison.
5
+ * Copilot AI provider direct REST API client.
6
+ *
7
+ * Uses GitHub Copilot Chat Completions API (api.individual.githubcopilot.com)
8
+ * instead of the @github/copilot-sdk. This removes the dependency on the
9
+ * Copilot CLI binary and avoids protocol version mismatch issues.
10
+ *
11
+ * Auth: uses `gh auth token` (GitHub CLI) or GITHUB_TOKEN env var.
18
12
  */
19
- export const parseParts = (v) => {
20
- const parts = v.split('.').map((n) => Number.parseInt(n, 10));
21
- const [major = 0, minor = 0, patch = 0] = parts;
22
- return major * 1_000_000 + minor * 1_000 + patch;
13
+ import { buildPromptWithCorrection, buildReleaseNotesPrompt, cleanAIContent, MIN_AI_RESPONSE_LENGTH, normalizeBranchName, } from '../utils/ai-text.js';
14
+ import { exec } from '../utils/exec.js';
15
+ import { log } from '../utils/logging.js';
16
+ // ── API Configuration ───────────────────────────────────────────────────
17
+ const COPILOT_API_BASE = 'https://api.individual.githubcopilot.com';
18
+ const CHAT_ENDPOINT = `${COPILOT_API_BASE}/chat/completions`;
19
+ const MODELS_ENDPOINT = `${COPILOT_API_BASE}/models`;
20
+ // Required by the Copilot API — requests without these headers get 403
21
+ const COPILOT_HEADERS = {
22
+ 'Copilot-Integration-Id': 'vscode-chat',
23
+ 'Editor-Version': 'vscode/1.96.0',
24
+ 'Editor-Plugin-Version': 'copilot-chat/0.24.0',
25
+ 'User-Agent': 'GitHubCopilotChat/0.24.0',
26
+ 'Openai-Intent': 'conversation-panel',
23
27
  };
28
+ // ── Token Management ────────────────────────────────────────────────────
29
+ let cachedToken = null;
24
30
  /**
25
- * Read cached copilot binary info from file.
31
+ * Get GitHub auth token for Copilot API access.
32
+ * Priority: 1) cached token, 2) GITHUB_TOKEN env, 3) `gh auth token`
26
33
  */
27
- const readCache = () => {
28
- try {
29
- if (!fs.existsSync(CACHE_FILE))
30
- return null;
31
- const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
32
- // Check if cache is still valid (within TTL and binary still exists)
33
- if (Date.now() - data.timestamp < CACHE_TTL_MS && fs.existsSync(data.path)) {
34
- return data;
35
- }
34
+ const getToken = () => {
35
+ if (cachedToken)
36
+ return cachedToken;
37
+ // Try env vars first (fast path)
38
+ const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? null;
39
+ if (envToken) {
40
+ cachedToken = envToken;
41
+ return envToken;
36
42
  }
37
- catch {
38
- // ignore
39
- }
40
- return null;
41
- };
42
- /**
43
- * Write copilot binary info to cache file.
44
- */
45
- const writeCache = (info) => {
43
+ // Fall back to gh CLI
46
44
  try {
47
- const cacheDir = path.dirname(CACHE_FILE);
48
- if (!fs.existsSync(cacheDir)) {
49
- fs.mkdirSync(cacheDir, { recursive: true });
45
+ const token = exec('gh auth token', true).trim();
46
+ if (token) {
47
+ cachedToken = token;
48
+ return token;
50
49
  }
51
- fs.writeFileSync(CACHE_FILE, JSON.stringify({ ...info, timestamp: Date.now() }));
52
50
  }
53
51
  catch {
54
- // ignore cache write failures
52
+ // gh CLI not available or not authenticated
55
53
  }
54
+ return null;
56
55
  };
57
- /**
58
- * Get version from a specific copilot binary path.
59
- */
60
- const getVersionFromPath = (binPath) => {
61
- try {
62
- const verOut = exec(`"${binPath}" --version`, true);
63
- const m = verOut.match(/(\d+\.\d+\.\d+)/);
64
- return m?.[1] ?? null;
65
- }
66
- catch {
56
+ const chatCompletion = async (messages, model) => {
57
+ const token = getToken();
58
+ if (!token) {
59
+ log.warn('No GitHub token available. Run `gh auth login` to authenticate.');
67
60
  return null;
68
61
  }
69
- };
70
- /**
71
- * Find the best (newest) copilot binary from known locations.
72
- * Uses file-based caching to avoid slow exec calls on every startup.
73
- */
74
- export const findBestCopilotBinary = () => {
75
- const minNum = parseParts(MIN_COPILOT_VERSION);
76
- const isWin = os.platform() === 'win32';
77
- const copilotBin = isWin ? 'copilot.exe' : 'copilot';
78
- // Super fast path: check cache first
79
- const cached = readCache();
80
- if (cached && fs.existsSync(cached.path) && parseParts(cached.version) >= minNum) {
81
- return { path: cached.path, version: cached.version };
82
- }
83
- // Check PATH first (most common case)
84
- try {
85
- const whichCmd = isWin ? 'where copilot' : 'which copilot';
86
- const pathBin = exec(whichCmd, true).trim().split('\n')[0]?.trim(); // `where` may return multiple
87
- if (pathBin && fs.existsSync(pathBin)) {
88
- const ver = getVersionFromPath(pathBin);
89
- if (ver) {
90
- const num = parseParts(ver);
91
- if (num >= minNum) {
92
- const result = { path: pathBin, version: ver };
93
- writeCache(result);
94
- return result;
62
+ const body = {
63
+ model: model ?? 'gpt-5-mini',
64
+ messages,
65
+ };
66
+ const maxRetries = 2;
67
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
68
+ try {
69
+ const res = await fetch(CHAT_ENDPOINT, {
70
+ method: 'POST',
71
+ headers: {
72
+ ...COPILOT_HEADERS,
73
+ 'Authorization': `Bearer ${token}`,
74
+ 'Content-Type': 'application/json',
75
+ 'Accept': 'application/json',
76
+ },
77
+ body: JSON.stringify(body),
78
+ });
79
+ if (!res.ok) {
80
+ const errorText = await res.text().catch(() => '');
81
+ // Retry on 403 (Copilot intermittent rate-limit)
82
+ if (res.status === 403 && attempt < maxRetries) {
83
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
84
+ continue;
85
+ }
86
+ if (res.status === 401 || res.status === 403) {
87
+ cachedToken = null;
88
+ log.clearLine();
89
+ log.warn('GitHub token expired or unauthorized. Run `gh auth login` to re-authenticate.');
95
90
  }
91
+ else {
92
+ log.clearLine();
93
+ log.warn(`Copilot API error (${res.status}): ${errorText.slice(0, 200)}`);
94
+ }
95
+ return null;
96
96
  }
97
+ const data = (await res.json());
98
+ const content = data.choices?.[0]?.message?.content;
99
+ return content ?? null;
97
100
  }
98
- }
99
- catch {
100
- // ignore, will check other locations
101
- }
102
- // PATH version is outdated or not found - scan known locations
103
- const home = os.homedir();
104
- const knownPaths = isWin
105
- ? [
106
- // Windows install locations (matches copilot-setup.ts)
107
- path.join(process.env.LOCALAPPDATA ?? '', 'Programs', 'GitHub CLI', copilotBin),
108
- 'C:\\Program Files\\GitHub CLI\\' + copilotBin,
109
- 'C:\\Program Files (x86)\\GitHub CLI\\' + copilotBin,
110
- path.join(home, 'AppData', 'Roaming', 'npm', copilotBin),
111
- path.join(home, 'scoop', 'shims', copilotBin),
112
- path.join(home, '.config', 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'copilotCli', copilotBin),
113
- ]
114
- : [
115
- // macOS / Linux install locations (matches copilot-setup.ts)
116
- '/opt/homebrew/bin/copilot',
117
- '/usr/local/bin/copilot',
118
- '/usr/bin/copilot',
119
- path.join(home, '.config/Code/User/globalStorage/github.copilot-chat/copilotCli/copilot'),
120
- path.join(home, '.npm-global/bin/copilot'),
121
- path.join(home, '.local/bin/copilot'),
122
- ];
123
- for (const binPath of knownPaths) {
124
- if (fs.existsSync(binPath)) {
125
- const ver = getVersionFromPath(binPath);
126
- if (ver) {
127
- const num = parseParts(ver);
128
- if (num >= minNum) {
129
- const result = { path: binPath, version: ver };
130
- writeCache(result);
131
- return result;
132
- }
101
+ catch (error) {
102
+ if (attempt < maxRetries) {
103
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
104
+ continue;
133
105
  }
106
+ log.clearLine();
107
+ log.error('Copilot API request failed: ' + String(error));
108
+ return null;
134
109
  }
135
110
  }
136
111
  return null;
137
112
  };
113
+ // ── Public API (same exports as before) ─────────────────────────────────
138
114
  /**
139
- * Check if Copilot CLI version is compatible with SDK.
140
- * The standalone CLI is used as fallback when the bundled CLI is unavailable
141
- * (e.g. in bun-compiled binaries). Also used for setup guidance.
115
+ * Check if Copilot API is accessible (has valid token).
142
116
  */
143
- export const checkCopilotCliVersion = () => {
144
- const best = findBestCopilotBinary();
145
- if (!best) {
117
+ export const isAvailable = async () => {
118
+ const token = getToken();
119
+ if (!token)
120
+ return false;
121
+ try {
122
+ const res = await fetch(MODELS_ENDPOINT, {
123
+ method: 'GET',
124
+ headers: { ...COPILOT_HEADERS, Authorization: `Bearer ${token}` },
125
+ });
126
+ return res.ok;
127
+ }
128
+ catch {
146
129
  return false;
147
130
  }
148
- return parseParts(best.version) >= parseParts(MIN_COPILOT_VERSION);
149
131
  };
150
- let client = null;
151
132
  /**
152
- * Lazily start the Copilot SDK client.
153
- *
154
- * Resolution order:
155
- * 1. Bundled CLI – SDK resolves @github/copilot from node_modules (npm/bun install).
156
- * 2. System CLI – Standalone Copilot CLI binary found on PATH or known locations
157
- * (used when bundled CLI is unavailable, e.g. Bun-compiled binary).
133
+ * No-op for backwards compatibility. REST API is stateless.
158
134
  */
159
- const ensureClient = async () => {
160
- if (client) {
161
- return true;
162
- }
163
- // Suppress Node.js experimental warnings from copilot subprocess
164
- process.env.NODE_NO_WARNINGS = '1';
165
- // ── Attempt 1: bundled CLI (default SDK behaviour) ────────────────────
166
- try {
167
- client = new CopilotClient({ autoStart: true });
168
- await client.start();
169
- return true;
170
- }
171
- catch (bundledError) {
172
- const bundledMsg = bundledError instanceof Error ? bundledError.message : String(bundledError);
173
- // Only fall through to system CLI when the bundled CLI cannot be resolved
174
- // (e.g. running inside a bun-compiled binary where node_modules don't exist).
175
- const isMissingModule = bundledMsg.includes('Cannot find module') ||
176
- bundledMsg.includes('ResolveMessage') ||
177
- bundledMsg.includes('ENOENT');
178
- if (!isMissingModule) {
179
- // Not a resolution error — report and bail out
180
- log.clearLine();
181
- log.gap();
182
- if (bundledMsg.includes('headless') || bundledMsg.includes('Unknown flag')) {
183
- log.info('Copilot SDK: bundled CLI does not support --headless (Bun compat issue).');
184
- log.info('Upgrade SDK: bun add @github/copilot-sdk@latest');
185
- }
186
- else {
187
- log.info(`Copilot SDK unavailable: ${bundledMsg}`);
188
- }
189
- log.gap();
190
- client = null;
191
- return false;
192
- }
193
- // ── Attempt 2: system Copilot CLI (standalone native binary) ────────
194
- const systemCli = findBestCopilotBinary();
195
- if (!systemCli) {
196
- log.clearLine();
197
- log.gap();
198
- log.info('Copilot SDK: bundled CLI not found and no system Copilot CLI available.');
199
- log.info('Install Copilot CLI: brew install copilot-cli');
200
- log.gap();
201
- client = null;
202
- return false;
203
- }
204
- try {
205
- log.clearLine();
206
- log.gap();
207
- log.info(`Using system Copilot CLI (v${systemCli.version})`);
208
- log.gap();
209
- client = new CopilotClient({ cliPath: systemCli.path, autoStart: true });
210
- await client.start();
211
- return true;
212
- }
213
- catch (systemError) {
214
- const sysMsg = systemError instanceof Error ? systemError.message : String(systemError);
215
- log.clearLine();
216
- log.gap();
217
- log.info(`System Copilot CLI failed: ${sysMsg}`);
218
- log.gap();
219
- client = null;
220
- return false;
221
- }
222
- }
135
+ export const stopClient = async () => {
136
+ // REST API is stateless — nothing to stop
223
137
  };
224
- const withSession = async (model, fn) => {
225
- const ok = await ensureClient();
226
- if (!ok || !client) {
227
- return null;
228
- }
138
+ export const generateBranchName = async (text, correction, model) => {
139
+ const prompt = buildPromptWithCorrection('branch-name-prompt.md', text, 'Input', correction);
229
140
  try {
230
- const session = await client.createSession({ model });
231
- try {
232
- const res = await fn(session);
233
- // best-effort destroy session workspace
234
- try {
235
- await session.destroy();
236
- }
237
- catch {
238
- // ignore
239
- }
240
- return res;
241
- }
242
- catch (error) {
243
- try {
244
- await session.destroy();
245
- }
246
- catch {
247
- // ignore
248
- }
249
- throw error;
250
- }
141
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
142
+ if (!content)
143
+ return null;
144
+ const first = content
145
+ .trim()
146
+ .split('\n')
147
+ .find((l) => !!l) ?? '';
148
+ return normalizeBranchName(first) || null;
251
149
  }
252
150
  catch (error) {
253
- log.warn('Failed to create Copilot session: ' + String(error));
151
+ log.clearLine();
152
+ log.gap();
153
+ log.error('Copilot Error: ' + String(error));
254
154
  return null;
255
155
  }
256
156
  };
257
- export const isAvailable = async () => {
258
- return ensureClient();
259
- };
260
- export const stopClient = async () => {
261
- if (!client) {
262
- return;
263
- }
157
+ export const generateCommitMessage = async (diff, correction, model) => {
158
+ const prompt = buildPromptWithCorrection('commit-message-prompt.md', diff, 'Diff', correction);
264
159
  try {
265
- await client.stop();
160
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
161
+ if (!content)
162
+ return null;
163
+ return cleanAIContent(content, {
164
+ normalizeBlankLines: true,
165
+ minLength: MIN_AI_RESPONSE_LENGTH,
166
+ });
266
167
  }
267
- catch {
268
- try {
269
- // Some runtime implementations expose forceStop; call if present
270
- await client.forceStop();
271
- }
272
- catch {
273
- /* ignore */
274
- }
168
+ catch (error) {
169
+ log.clearLine();
170
+ log.gap();
171
+ log.error('Copilot Error: ' + String(error));
172
+ return null;
275
173
  }
276
- client = null;
277
- };
278
- export const generateBranchName = async (text, correction, model) => {
279
- const promptBase = `Generate a git branch name suffix from this input. Output ONLY the kebab-case suffix (lowercase-with-hyphens), 3-50 chars, nothing else.`;
280
- const prompt = correction
281
- ? `${promptBase}\n\nInput:\n${text}\n\nAdjustment: ${correction}`
282
- : `${promptBase}\n\nInput:\n${text}`;
283
- const result = await withSession(model, async (session) => {
284
- // sendAndWait returns the assistant message event with data.content
285
- try {
286
- const response = await session.sendAndWait({ prompt });
287
- const content = response?.data?.content ?? '';
288
- const first = String(content)
289
- .trim()
290
- .split('\n')
291
- .find((l) => !!l) ?? '';
292
- // sanitize to kebab-case
293
- const cleaned = String(first)
294
- .toLowerCase()
295
- .replaceAll(/[^\d\sa-z-]/g, ' ')
296
- .trim()
297
- .replaceAll(/\s+/g, '-')
298
- .replaceAll(/-+/g, '-')
299
- .replaceAll(/^-|-$/g, '');
300
- return cleaned || null;
301
- }
302
- catch (error) {
303
- log.clearLine();
304
- log.gap();
305
- log.error('Copilot Error: ' + String(error));
306
- return null;
307
- }
308
- });
309
- return result;
310
- };
311
- export const generateCommitMessage = async (diff, correction, model) => {
312
- const promptBase = `Generate a conventional commit message from this git diff. Output ONLY the commit message in this format:\n\n<type>(<scope>): <short summary>\n\n<Detailed multi-line body explaining the change. Wrap lines at ~72 characters. LIMITS: subject max 100 chars; body max 360 chars. Include why the change was made and any important notes. Separate subject and body by a single blank line. Do not include any extraneous commentary or markers. Use imperative mood.
313
-
314
- Example:
315
- refactor(ai): migrate providers to SDKs
316
-
317
- Replaces direct API/CLI calls for Copilot and Gemini with SDK integrations.
318
- This simplifies code, improves maintainability, and adds dynamic model
319
- fetching. Updates .gitignore for geeto binaries.`;
320
- const prompt = correction
321
- ? `${promptBase}\n\nDiff:\n${diff}\n\nAdjustment: ${correction}`
322
- : `${promptBase}\n\nDiff:\n${diff}`;
323
- const result = await withSession(model, async (session) => {
324
- try {
325
- const response = await session.sendAndWait({ prompt });
326
- const content = response?.data?.content ?? '';
327
- // Normalize full response: remove fenced blocks, trim surrounding quotes, collapse extra blank lines
328
- const cleaned = String(content)
329
- .replaceAll(/```[\S\s]*?```/g, '')
330
- .replaceAll(/^"+|"+$/g, '')
331
- .trim();
332
- const normalized = cleaned.replaceAll(/\n\s*\n+/g, '\n\n').trim();
333
- return normalized && normalized.length >= 8 ? normalized : null;
334
- }
335
- catch (error) {
336
- log.clearLine();
337
- log.gap();
338
- log.error('Copilot Error: ' + String(error));
339
- return null;
340
- }
341
- });
342
- return result;
343
174
  };
344
175
  export const getAvailableModelsDetailed = async () => {
176
+ const token = getToken();
177
+ if (!token)
178
+ return null;
345
179
  try {
346
- const runtime = client;
347
- let list = null;
348
- if (typeof runtime.listModels === 'function') {
349
- list = await runtime.listModels();
350
- }
351
- else if (typeof runtime.getModels === 'function') {
352
- list = await runtime.getModels();
353
- }
354
- else if (Array.isArray(runtime.models)) {
355
- list = runtime.models;
356
- }
357
- if (!list || !Array.isArray(list)) {
180
+ const res = await fetch(MODELS_ENDPOINT, {
181
+ headers: { ...COPILOT_HEADERS, Authorization: `Bearer ${token}` },
182
+ });
183
+ if (!res.ok)
358
184
  return null;
359
- }
360
- const mapped = await Promise.all(list.map(async (m) => {
361
- if (!m) {
362
- return null;
363
- }
364
- if (typeof m === 'string') {
365
- return {
366
- id: m,
367
- name: m,
368
- inputTokenLimit: null,
369
- outputTokenLimit: null,
370
- needsEnable: false,
371
- isPremium: false,
372
- multiplier: null,
373
- label: m,
374
- value: m,
375
- };
376
- }
377
- const id = String(m.id ?? m.model ?? m.name ?? '');
378
- const name = String(m.name ?? m.title ?? id);
379
- const policyState = m.policy?.state ?? m.state ?? null;
380
- const billing = m.billing ?? m.pricing ?? null;
381
- const isPremium = Boolean(billing?.is_premium ?? billing?.premium ?? false);
382
- const multiplierRaw = billing?.multiplier ?? billing?.cost_multiplier ?? null;
185
+ const data = (await res.json());
186
+ const list = data.data;
187
+ if (!Array.isArray(list))
188
+ return null;
189
+ const result = [];
190
+ for (const m of list) {
191
+ if (!m?.id)
192
+ continue;
193
+ // Skip non-chat models (embeddings, etc.)
194
+ const type = m.capabilities?.type;
195
+ if (type && type !== 'chat')
196
+ continue;
197
+ const id = String(m.id);
198
+ const name = String(m.name ?? id);
199
+ const limits = m.capabilities?.limits;
200
+ const inputTokenLimit = typeof limits?.max_prompt_tokens === 'number' ? limits.max_prompt_tokens : null;
201
+ const outputTokenLimit = typeof limits?.max_output_tokens === 'number' ? limits.max_output_tokens : null;
202
+ const policyState = m.policy?.state ?? null;
203
+ const needsEnable = Boolean(policyState && String(policyState).toLowerCase() !== 'enabled');
204
+ const billing = m.capabilities?.supports?.billing ?? null;
205
+ const isPremium = Boolean(billing?.premium_billing);
206
+ const multiplierRaw = billing?.copilot_premium_request_multiplier ?? null;
383
207
  const multiplier = typeof multiplierRaw === 'number' ? multiplierRaw : null;
384
- let needsEnable = Boolean(policyState && String(policyState).toLowerCase() !== 'enabled');
385
- // If the policy state is unknown, try to ping the model via adapter to check enablement
386
- if (policyState === null) {
387
- try {
388
- const adapter = await import('./copilot-adapter.js');
389
- if (adapter && typeof adapter.pingModel === 'function') {
390
- try {
391
- const pingOk = await adapter.pingModel(id || name);
392
- needsEnable = !pingOk;
393
- }
394
- catch {
395
- // If ping fails, leave needsEnable as false to avoid blocking users
396
- needsEnable = false;
397
- }
398
- }
399
- }
400
- catch {
401
- // adapter import failed; ignore and proceed
402
- }
403
- }
404
- // capabilities.limits may contain token limits for the runtime
405
- let inputTokenLimit = null;
406
- let outputTokenLimit = null;
407
- try {
408
- const limits = m.capabilities?.limits;
409
- if (limits) {
410
- if (typeof limits.max_prompt_tokens === 'number') {
411
- inputTokenLimit = limits.max_prompt_tokens;
412
- }
413
- if (typeof limits.max_output_tokens === 'number') {
414
- outputTokenLimit = limits.max_output_tokens;
415
- }
416
- }
417
- }
418
- catch {
419
- // ignore parsing errors; leave as null
420
- }
421
- const label = `${name}`;
422
- const value = id || label;
423
- return {
208
+ result.push({
424
209
  id,
425
210
  name,
426
211
  inputTokenLimit,
@@ -428,15 +213,11 @@ export const getAvailableModelsDetailed = async () => {
428
213
  needsEnable,
429
214
  isPremium,
430
215
  multiplier,
431
- label,
432
- value,
433
- };
434
- }));
435
- const out = mapped.filter(Boolean);
436
- if (out.length > 0) {
437
- return out;
216
+ label: name,
217
+ value: id,
218
+ });
438
219
  }
439
- return null;
220
+ return result.length > 0 ? result : null;
440
221
  }
441
222
  catch {
442
223
  return null;
@@ -444,7 +225,6 @@ export const getAvailableModelsDetailed = async () => {
444
225
  };
445
226
  export const getAvailableModelChoices = async (defaultModelId) => {
446
227
  const detailed = (await getAvailableModelsDetailed()) ?? [];
447
- // compute max lengths using simple loops (avoid reduce)
448
228
  let maxNameLen = 0;
449
229
  let maxIoLen = 0;
450
230
  let maxMultLen = 0;
@@ -487,79 +267,31 @@ export const getAvailableModelChoices = async (defaultModelId) => {
487
267
  return choices;
488
268
  };
489
269
  export const generateReleaseNotes = async (commits, language, correction, model) => {
490
- const langLabel = language === 'id' ? 'Indonesian (Bahasa Indonesia)' : 'English';
491
- const promptBase = `You are a release notes writer. Given a list of git commit messages, generate user-friendly release notes in ${langLabel}. Output ONLY the release notes content (no title/heading, no version number, no date — those are added separately).
492
-
493
- Rules:
494
- - Start with "### What's New?" as the top-level section
495
- - Group changes into subsections: "#### New Features", "#### Bug Fixes", "#### Other Improvements"
496
- - Only include subsections that have items (skip empty ones)
497
- - Use simple, non-technical language that end users can understand
498
- - Each item should be a bullet point starting with "-"
499
- - Strip conventional commit prefixes (feat:, fix:, chore:, etc.)
500
- - Keep it concise but informative
501
- - If there are breaking changes, add a "#### Breaking Changes" subsection at the top
502
- - Do NOT include commit hashes or author names
503
-
504
- Formatting (follow EXACTLY — this is markdownlint-compliant):
505
- - Always put ONE blank line after EVERY heading (### or ####) before the first bullet
506
- - Always put ONE blank line after the last bullet in a section before the next #### heading
507
- - Never have more than one consecutive blank line
508
- - Example output:
509
-
510
- ### What's New?
511
-
512
- #### New Features
513
-
514
- - Feature description here
515
- - Another feature
516
-
517
- #### Bug Fixes
518
-
519
- - Fix description here
520
-
521
- #### Other Improvements
522
-
523
- - Improvement here`;
524
- const prompt = correction
525
- ? `${promptBase}\n\nCommits:\n${commits}\n\nAdjustment: ${correction}`
526
- : `${promptBase}\n\nCommits:\n${commits}`;
527
- const result = await withSession(model, async (session) => {
528
- try {
529
- const response = await session.sendAndWait({ prompt });
530
- const content = response?.data?.content ?? '';
531
- const cleaned = String(content)
532
- .replaceAll(/```[\S\s]*?```/g, '')
533
- .replaceAll(/^"+|"+$/g, '')
534
- .trim();
535
- return cleaned || null;
536
- }
537
- catch (error) {
538
- log.clearLine();
539
- log.gap();
540
- log.error('Copilot Error: ' + String(error));
270
+ const prompt = buildReleaseNotesPrompt(commits, language, correction);
271
+ try {
272
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
273
+ if (!content)
541
274
  return null;
542
- }
543
- });
544
- return result;
275
+ return cleanAIContent(content);
276
+ }
277
+ catch (error) {
278
+ log.clearLine();
279
+ log.gap();
280
+ log.error('Copilot Error: ' + String(error));
281
+ return null;
282
+ }
545
283
  };
546
284
  /** Send a raw prompt to Copilot and return the text response. */
547
285
  export const generateText = async (prompt, model) => {
548
- const result = await withSession(model, async (session) => {
549
- try {
550
- const response = await session.sendAndWait({ prompt });
551
- const content = response?.data?.content ?? '';
552
- const cleaned = String(content)
553
- .replaceAll(/```[\S\s]*?```/g, '')
554
- .replaceAll(/^"+|"+$/g, '')
555
- .trim();
556
- return cleaned || null;
557
- }
558
- catch {
286
+ try {
287
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
288
+ if (!content)
559
289
  return null;
560
- }
561
- });
562
- return result;
290
+ return cleanAIContent(content);
291
+ }
292
+ catch {
293
+ return null;
294
+ }
563
295
  };
564
296
  export default {
565
297
  generateBranchName,