geeto 0.6.6 → 0.9.0

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 (198) 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 +166 -456
  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 +8 -7
  44. package/lib/core/copilot-setup.d.ts.map +1 -1
  45. package/lib/core/copilot-setup.js +59 -230
  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 +501 -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/git-ai.js +13 -13
  83. package/lib/utils/git-ai.js.map +1 -1
  84. package/lib/utils/git-errors.d.ts.map +1 -1
  85. package/lib/utils/git-errors.js +2 -6
  86. package/lib/utils/git-errors.js.map +1 -1
  87. package/lib/utils/git.d.ts.map +1 -1
  88. package/lib/utils/git.js +5 -0
  89. package/lib/utils/git.js.map +1 -1
  90. package/lib/utils/github-helpers.d.ts +33 -0
  91. package/lib/utils/github-helpers.d.ts.map +1 -0
  92. package/lib/utils/github-helpers.js +101 -0
  93. package/lib/utils/github-helpers.js.map +1 -0
  94. package/lib/utils/prompt-loader.d.ts +9 -0
  95. package/lib/utils/prompt-loader.d.ts.map +1 -0
  96. package/lib/utils/prompt-loader.js +42 -0
  97. package/lib/utils/prompt-loader.js.map +1 -0
  98. package/lib/utils/prompts-embedded.d.ts +2 -0
  99. package/lib/utils/prompts-embedded.d.ts.map +1 -0
  100. package/lib/utils/prompts-embedded.js +255 -0
  101. package/lib/utils/prompts-embedded.js.map +1 -0
  102. package/lib/utils/scramble.d.ts +9 -86
  103. package/lib/utils/scramble.d.ts.map +1 -1
  104. package/lib/utils/scramble.js +27 -279
  105. package/lib/utils/scramble.js.map +1 -1
  106. package/lib/version.d.ts +1 -1
  107. package/lib/version.js +1 -1
  108. package/lib/workflows/alias.d.ts.map +1 -1
  109. package/lib/workflows/alias.js +1 -0
  110. package/lib/workflows/alias.js.map +1 -1
  111. package/lib/workflows/amend.d.ts.map +1 -1
  112. package/lib/workflows/amend.js +1 -5
  113. package/lib/workflows/amend.js.map +1 -1
  114. package/lib/workflows/branch-helpers.d.ts.map +1 -1
  115. package/lib/workflows/branch-helpers.js +0 -1
  116. package/lib/workflows/branch-helpers.js.map +1 -1
  117. package/lib/workflows/commit.d.ts.map +1 -1
  118. package/lib/workflows/commit.js +160 -187
  119. package/lib/workflows/commit.js.map +1 -1
  120. package/lib/workflows/doctor.d.ts +7 -0
  121. package/lib/workflows/doctor.d.ts.map +1 -0
  122. package/lib/workflows/doctor.js +284 -0
  123. package/lib/workflows/doctor.js.map +1 -0
  124. package/lib/workflows/issue.d.ts +1 -1
  125. package/lib/workflows/issue.d.ts.map +1 -1
  126. package/lib/workflows/issue.js +28 -115
  127. package/lib/workflows/issue.js.map +1 -1
  128. package/lib/workflows/main-helpers.d.ts +34 -0
  129. package/lib/workflows/main-helpers.d.ts.map +1 -0
  130. package/lib/workflows/main-helpers.js +346 -0
  131. package/lib/workflows/main-helpers.js.map +1 -0
  132. package/lib/workflows/main-steps.d.ts.map +1 -1
  133. package/lib/workflows/main-steps.js +9 -134
  134. package/lib/workflows/main-steps.js.map +1 -1
  135. package/lib/workflows/main.d.ts +2 -6
  136. package/lib/workflows/main.d.ts.map +1 -1
  137. package/lib/workflows/main.js +44 -381
  138. package/lib/workflows/main.js.map +1 -1
  139. package/lib/workflows/pr.d.ts +2 -2
  140. package/lib/workflows/pr.d.ts.map +1 -1
  141. package/lib/workflows/pr.js +49 -137
  142. package/lib/workflows/pr.js.map +1 -1
  143. package/lib/workflows/prune.d.ts.map +1 -1
  144. package/lib/workflows/prune.js +2 -10
  145. package/lib/workflows/prune.js.map +1 -1
  146. package/lib/workflows/pull.d.ts.map +1 -1
  147. package/lib/workflows/pull.js +2 -24
  148. package/lib/workflows/pull.js.map +1 -1
  149. package/lib/workflows/release-merge.d.ts +12 -0
  150. package/lib/workflows/release-merge.d.ts.map +1 -0
  151. package/lib/workflows/release-merge.js +569 -0
  152. package/lib/workflows/release-merge.js.map +1 -0
  153. package/lib/workflows/release-notes.d.ts +13 -0
  154. package/lib/workflows/release-notes.d.ts.map +1 -0
  155. package/lib/workflows/release-notes.js +141 -0
  156. package/lib/workflows/release-notes.js.map +1 -0
  157. package/lib/workflows/release-recover.d.ts +5 -0
  158. package/lib/workflows/release-recover.d.ts.map +1 -0
  159. package/lib/workflows/release-recover.js +137 -0
  160. package/lib/workflows/release-recover.js.map +1 -0
  161. package/lib/workflows/release-sync.d.ts +7 -0
  162. package/lib/workflows/release-sync.d.ts.map +1 -0
  163. package/lib/workflows/release-sync.js +321 -0
  164. package/lib/workflows/release-sync.js.map +1 -0
  165. package/lib/workflows/release-utils.d.ts +36 -0
  166. package/lib/workflows/release-utils.d.ts.map +1 -0
  167. package/lib/workflows/release-utils.js +150 -0
  168. package/lib/workflows/release-utils.js.map +1 -0
  169. package/lib/workflows/release.d.ts.map +1 -1
  170. package/lib/workflows/release.js +92 -719
  171. package/lib/workflows/release.js.map +1 -1
  172. package/lib/workflows/repo-settings.d.ts +2 -2
  173. package/lib/workflows/repo-settings.d.ts.map +1 -1
  174. package/lib/workflows/repo-settings.js +33 -24
  175. package/lib/workflows/repo-settings.js.map +1 -1
  176. package/lib/workflows/reword.d.ts.map +1 -1
  177. package/lib/workflows/reword.js +154 -151
  178. package/lib/workflows/reword.js.map +1 -1
  179. package/lib/workflows/security-gate.d.ts.map +1 -1
  180. package/lib/workflows/security-gate.js +15 -75
  181. package/lib/workflows/security-gate.js.map +1 -1
  182. package/lib/workflows/settings.d.ts +2 -1
  183. package/lib/workflows/settings.d.ts.map +1 -1
  184. package/lib/workflows/settings.js +289 -19
  185. package/lib/workflows/settings.js.map +1 -1
  186. package/lib/workflows/trello-menu.d.ts +2 -5
  187. package/lib/workflows/trello-menu.d.ts.map +1 -1
  188. package/lib/workflows/trello-menu.js +67 -228
  189. package/lib/workflows/trello-menu.js.map +1 -1
  190. package/package.json +3 -6
  191. package/prompts/branch-name-prompt.md +4 -0
  192. package/prompts/commit-message-prompt.md +12 -0
  193. package/prompts/issue-prompt.md +19 -0
  194. package/prompts/issue-review.with-context.prompt.yml +77 -0
  195. package/prompts/pr-prompt.md +14 -0
  196. package/prompts/release-notes-prompt.md +35 -0
  197. package/prompts/repo-description-prompt.md +1 -0
  198. package/prompts/security-gate-prompt.md +80 -0
@@ -1,426 +1,189 @@
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;
23
- };
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
+ // ── Token Management ────────────────────────────────────────────────────
21
+ let cachedToken = null;
24
22
  /**
25
- * Read cached copilot binary info from file.
23
+ * Get GitHub auth token for Copilot API access.
24
+ * Priority: 1) cached token, 2) GITHUB_TOKEN env, 3) `gh auth token`
26
25
  */
27
- const readCache = () => {
26
+ const getToken = () => {
27
+ if (cachedToken)
28
+ return cachedToken;
29
+ // Try env vars first (fast path)
30
+ const envToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? null;
31
+ if (envToken) {
32
+ cachedToken = envToken;
33
+ return envToken;
34
+ }
35
+ // Fall back to gh CLI
28
36
  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;
37
+ const token = exec('gh auth token', true).trim();
38
+ if (token) {
39
+ cachedToken = token;
40
+ return token;
35
41
  }
36
42
  }
37
43
  catch {
38
- // ignore
44
+ // gh CLI not available or not authenticated
39
45
  }
40
46
  return null;
41
47
  };
42
- /**
43
- * Write copilot binary info to cache file.
44
- */
45
- const writeCache = (info) => {
46
- try {
47
- const cacheDir = path.dirname(CACHE_FILE);
48
- if (!fs.existsSync(cacheDir)) {
49
- fs.mkdirSync(cacheDir, { recursive: true });
50
- }
51
- fs.writeFileSync(CACHE_FILE, JSON.stringify({ ...info, timestamp: Date.now() }));
52
- }
53
- catch {
54
- // ignore cache write failures
48
+ const chatCompletion = async (messages, model) => {
49
+ const token = getToken();
50
+ if (!token) {
51
+ log.warn('No GitHub token available. Run `gh auth login` to authenticate.');
52
+ return null;
55
53
  }
56
- };
57
- /**
58
- * Get version from a specific copilot binary path.
59
- */
60
- const getVersionFromPath = (binPath) => {
54
+ const body = {
55
+ model: model ?? 'gpt-5-mini',
56
+ messages,
57
+ };
61
58
  try {
62
- const verOut = exec(`"${binPath}" --version`, true);
63
- const m = verOut.match(/(\d+\.\d+\.\d+)/);
64
- return m?.[1] ?? null;
59
+ const res = await fetch(CHAT_ENDPOINT, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Authorization': `Bearer ${token}`,
63
+ 'Content-Type': 'application/json',
64
+ 'Accept': 'application/json',
65
+ },
66
+ body: JSON.stringify(body),
67
+ });
68
+ if (!res.ok) {
69
+ const errorText = await res.text().catch(() => '');
70
+ if (res.status === 401 || res.status === 403) {
71
+ cachedToken = null;
72
+ log.clearLine();
73
+ log.warn('GitHub token expired or unauthorized. Run `gh auth login` to re-authenticate.');
74
+ }
75
+ else {
76
+ log.clearLine();
77
+ log.warn(`Copilot API error (${res.status}): ${errorText.slice(0, 200)}`);
78
+ }
79
+ return null;
80
+ }
81
+ const data = (await res.json());
82
+ const content = data.choices?.[0]?.message?.content;
83
+ return content ?? null;
65
84
  }
66
- catch {
85
+ catch (error) {
86
+ log.clearLine();
87
+ log.error('Copilot API request failed: ' + String(error));
67
88
  return null;
68
89
  }
69
90
  };
91
+ // ── Public API (same exports as before) ─────────────────────────────────
70
92
  /**
71
- * Find the best (newest) copilot binary from known locations.
72
- * Uses file-based caching to avoid slow exec calls on every startup.
93
+ * Check if Copilot API is accessible (has valid token).
73
94
  */
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)
95
+ export const isAvailable = async () => {
96
+ const token = getToken();
97
+ if (!token)
98
+ return false;
84
99
  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;
95
- }
96
- }
97
- }
100
+ const res = await fetch(MODELS_ENDPOINT, {
101
+ method: 'GET',
102
+ headers: { Authorization: `Bearer ${token}` },
103
+ });
104
+ return res.ok;
98
105
  }
99
106
  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
- }
133
- }
134
- }
135
- }
136
- return null;
137
- };
138
- /**
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.
142
- */
143
- export const checkCopilotCliVersion = () => {
144
- const best = findBestCopilotBinary();
145
- if (!best) {
146
107
  return false;
147
108
  }
148
- return parseParts(best.version) >= parseParts(MIN_COPILOT_VERSION);
149
109
  };
150
- let client = null;
151
110
  /**
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).
111
+ * No-op for backwards compatibility. REST API is stateless.
158
112
  */
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
- }
113
+ export const stopClient = async () => {
114
+ // REST API is stateless — nothing to stop
223
115
  };
224
- const withSession = async (model, fn) => {
225
- const ok = await ensureClient();
226
- if (!ok || !client) {
227
- return null;
228
- }
116
+ export const generateBranchName = async (text, correction, model) => {
117
+ const prompt = buildPromptWithCorrection('branch-name-prompt.md', text, 'Input', correction);
229
118
  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
- }
119
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
120
+ if (!content)
121
+ return null;
122
+ const first = content
123
+ .trim()
124
+ .split('\n')
125
+ .find((l) => !!l) ?? '';
126
+ return normalizeBranchName(first) || null;
251
127
  }
252
128
  catch (error) {
253
- log.warn('Failed to create Copilot session: ' + String(error));
129
+ log.clearLine();
130
+ log.gap();
131
+ log.error('Copilot Error: ' + String(error));
254
132
  return null;
255
133
  }
256
134
  };
257
- export const isAvailable = async () => {
258
- return ensureClient();
259
- };
260
- export const stopClient = async () => {
261
- if (!client) {
262
- return;
263
- }
135
+ export const generateCommitMessage = async (diff, correction, model) => {
136
+ const prompt = buildPromptWithCorrection('commit-message-prompt.md', diff, 'Diff', correction);
264
137
  try {
265
- await client.stop();
138
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
139
+ if (!content)
140
+ return null;
141
+ return cleanAIContent(content, {
142
+ normalizeBlankLines: true,
143
+ minLength: MIN_AI_RESPONSE_LENGTH,
144
+ });
266
145
  }
267
- catch {
268
- try {
269
- // Some runtime implementations expose forceStop; call if present
270
- await client.forceStop();
271
- }
272
- catch {
273
- /* ignore */
274
- }
146
+ catch (error) {
147
+ log.clearLine();
148
+ log.gap();
149
+ log.error('Copilot Error: ' + String(error));
150
+ return null;
275
151
  }
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
152
  };
344
153
  export const getAvailableModelsDetailed = async () => {
154
+ const token = getToken();
155
+ if (!token)
156
+ return null;
345
157
  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)) {
158
+ const res = await fetch(MODELS_ENDPOINT, {
159
+ headers: { Authorization: `Bearer ${token}` },
160
+ });
161
+ if (!res.ok)
358
162
  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;
163
+ const data = (await res.json());
164
+ const list = data.data;
165
+ if (!Array.isArray(list))
166
+ return null;
167
+ const result = [];
168
+ for (const m of list) {
169
+ if (!m?.id)
170
+ continue;
171
+ // Skip non-chat models (embeddings, etc.)
172
+ const type = m.capabilities?.type;
173
+ if (type && type !== 'chat')
174
+ continue;
175
+ const id = String(m.id);
176
+ const name = String(m.name ?? id);
177
+ const limits = m.capabilities?.limits;
178
+ const inputTokenLimit = typeof limits?.max_prompt_tokens === 'number' ? limits.max_prompt_tokens : null;
179
+ const outputTokenLimit = typeof limits?.max_output_tokens === 'number' ? limits.max_output_tokens : null;
180
+ const policyState = m.policy?.state ?? null;
181
+ const needsEnable = Boolean(policyState && String(policyState).toLowerCase() !== 'enabled');
182
+ const billing = m.capabilities?.supports?.billing ?? null;
183
+ const isPremium = Boolean(billing?.premium_billing);
184
+ const multiplierRaw = billing?.copilot_premium_request_multiplier ?? null;
383
185
  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 {
186
+ result.push({
424
187
  id,
425
188
  name,
426
189
  inputTokenLimit,
@@ -428,15 +191,11 @@ export const getAvailableModelsDetailed = async () => {
428
191
  needsEnable,
429
192
  isPremium,
430
193
  multiplier,
431
- label,
432
- value,
433
- };
434
- }));
435
- const out = mapped.filter(Boolean);
436
- if (out.length > 0) {
437
- return out;
194
+ label: name,
195
+ value: id,
196
+ });
438
197
  }
439
- return null;
198
+ return result.length > 0 ? result : null;
440
199
  }
441
200
  catch {
442
201
  return null;
@@ -444,7 +203,6 @@ export const getAvailableModelsDetailed = async () => {
444
203
  };
445
204
  export const getAvailableModelChoices = async (defaultModelId) => {
446
205
  const detailed = (await getAvailableModelsDetailed()) ?? [];
447
- // compute max lengths using simple loops (avoid reduce)
448
206
  let maxNameLen = 0;
449
207
  let maxIoLen = 0;
450
208
  let maxMultLen = 0;
@@ -487,79 +245,31 @@ export const getAvailableModelChoices = async (defaultModelId) => {
487
245
  return choices;
488
246
  };
489
247
  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));
248
+ const prompt = buildReleaseNotesPrompt(commits, language, correction);
249
+ try {
250
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
251
+ if (!content)
541
252
  return null;
542
- }
543
- });
544
- return result;
253
+ return cleanAIContent(content);
254
+ }
255
+ catch (error) {
256
+ log.clearLine();
257
+ log.gap();
258
+ log.error('Copilot Error: ' + String(error));
259
+ return null;
260
+ }
545
261
  };
546
262
  /** Send a raw prompt to Copilot and return the text response. */
547
263
  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 {
264
+ try {
265
+ const content = await chatCompletion([{ role: 'user', content: prompt }], model);
266
+ if (!content)
559
267
  return null;
560
- }
561
- });
562
- return result;
268
+ return cleanAIContent(content);
269
+ }
270
+ catch {
271
+ return null;
272
+ }
563
273
  };
564
274
  export default {
565
275
  generateBranchName,