gent-cli 7.0.0 → 8.0.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.
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Template Command - Quick-start from a baked-in starter.
3
+ *
4
+ * gent template list → show available templates
5
+ * gent template use <name> [directory] → scaffold a starter
6
+ *
7
+ * Templates are tiny inline definitions — no network needed. Keep them small
8
+ * so the CLI bundle stays light.
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const { pathExists, ensureDir } = require('../utils/fileSystem');
15
+ const initCommand = require('./init');
16
+
17
+ const TEMPLATES = {
18
+ node: {
19
+ description: 'Minimal Node.js project (package.json + index.js)',
20
+ files: {
21
+ 'package.json': JSON.stringify({
22
+ name: '__NAME__',
23
+ version: '0.1.0',
24
+ main: 'index.js',
25
+ scripts: { start: 'node index.js' },
26
+ }, null, 2) + '\n',
27
+ 'index.js': "console.log('hello from __NAME__');\n",
28
+ '.gentignore': 'node_modules/\n.env\n',
29
+ 'README.md': '# __NAME__\n\nA Node.js project scaffolded with `gent template use node`.\n',
30
+ },
31
+ },
32
+ python: {
33
+ description: 'Minimal Python project (main.py + requirements.txt)',
34
+ files: {
35
+ 'main.py': "def main():\n print('hello from __NAME__')\n\nif __name__ == '__main__':\n main()\n",
36
+ 'requirements.txt': '',
37
+ '.gentignore': '__pycache__/\n.venv/\n*.pyc\n.env\n',
38
+ 'README.md': '# __NAME__\n\nA Python project scaffolded with `gent template use python`.\n',
39
+ },
40
+ },
41
+ react: {
42
+ description: 'Vite + React skeleton (vite-style index.html + src/main.jsx)',
43
+ files: {
44
+ 'package.json': JSON.stringify({
45
+ name: '__NAME__',
46
+ version: '0.1.0',
47
+ type: 'module',
48
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
49
+ dependencies: { react: '^18.3.1', 'react-dom': '^18.3.1' },
50
+ devDependencies: { vite: '^5.4.0', '@vitejs/plugin-react': '^4.3.1' },
51
+ }, null, 2) + '\n',
52
+ 'index.html': '<!doctype html>\n<html><head><title>__NAME__</title></head>\n<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>\n',
53
+ 'src/main.jsx': "import React from 'react';\nimport { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render(<h1>__NAME__</h1>);\n",
54
+ '.gentignore': 'node_modules/\ndist/\n.env\n',
55
+ 'README.md': '# __NAME__\n\nRun `npm install && npm run dev` to start.\n',
56
+ },
57
+ },
58
+ 'django-api': {
59
+ description: 'Minimal Django project shell (manage.py + settings stub)',
60
+ files: {
61
+ 'manage.py': "#!/usr/bin/env python\nimport os, sys\nif __name__ == '__main__':\n os.environ.setdefault('DJANGO_SETTINGS_MODULE', '__NAME__.settings')\n from django.core.management import execute_from_command_line\n execute_from_command_line(sys.argv)\n",
62
+ 'requirements.txt': 'Django>=5.0\n',
63
+ '.gentignore': '__pycache__/\n*.pyc\n.venv/\ndb.sqlite3\n.env\n',
64
+ 'README.md': '# __NAME__\n\nRun `python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt`.\n',
65
+ },
66
+ },
67
+ };
68
+
69
+ async function template(subcommand, args = [], options = {}) {
70
+ const sub = (subcommand || 'list').toLowerCase();
71
+ switch (sub) {
72
+ case 'list': return list();
73
+ case 'use': return use(args[0], args[1], options);
74
+ default:
75
+ console.error(chalk.red(`Unknown subcommand '${sub}'`));
76
+ console.log(chalk.gray('Usage: gent template <list|use>'));
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ function list() {
82
+ console.log(chalk.bold.cyan('\nAvailable templates\n'));
83
+ for (const [name, t] of Object.entries(TEMPLATES)) {
84
+ console.log(` ${chalk.cyan(name.padEnd(14))} ${chalk.gray(t.description)}`);
85
+ }
86
+ console.log(chalk.gray('\n Usage: gent template use <name> [directory]\n'));
87
+ }
88
+
89
+ async function use(name, directory) {
90
+ if (!name) {
91
+ console.error(chalk.red('Usage: gent template use <name> [directory]'));
92
+ process.exit(1);
93
+ }
94
+ const tpl = TEMPLATES[name];
95
+ if (!tpl) {
96
+ console.error(chalk.red(`Unknown template '${name}'`));
97
+ console.log(chalk.gray(`Available: ${Object.keys(TEMPLATES).join(', ')}`));
98
+ process.exit(1);
99
+ }
100
+
101
+ const targetDir = directory || name;
102
+ const targetPath = path.resolve(process.cwd(), targetDir);
103
+ const projectName = path.basename(targetPath).replace(/[^A-Za-z0-9_-]/g, '-');
104
+
105
+ if (await pathExists(targetPath)) {
106
+ const entries = await fs.readdir(targetPath);
107
+ if (entries.length > 0) {
108
+ console.error(chalk.red(`Directory '${targetDir}' is not empty.`));
109
+ process.exit(1);
110
+ }
111
+ } else {
112
+ await ensureDir(targetPath);
113
+ }
114
+
115
+ for (const [rel, content] of Object.entries(tpl.files)) {
116
+ const filePath = path.join(targetPath, rel);
117
+ await ensureDir(path.dirname(filePath));
118
+ const body = content.replace(/__NAME__/g, projectName);
119
+ await fs.writeFile(filePath, body, 'utf-8');
120
+ }
121
+
122
+ console.log(chalk.green(`✓ Scaffolded '${name}' into ${targetDir}/`));
123
+
124
+ // Auto-init a gent repo so the next steps work
125
+ const originalCwd = process.cwd();
126
+ try {
127
+ process.chdir(targetPath);
128
+ await initCommand({});
129
+ } finally {
130
+ process.chdir(originalCwd);
131
+ }
132
+ console.log(chalk.gray(' Next: cd ' + targetDir + ' && gent add -A && gent commit -m "init"'));
133
+ }
134
+
135
+ module.exports = template;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Web Command - Open the current repo (or a specific commit/branch) in browser.
3
+ *
4
+ * gent web → open repo page
5
+ * gent web --branch <name> → open a specific branch
6
+ * gent web --commit <hash> → open a specific commit
7
+ * gent web --print → don't launch, just print the URL
8
+ *
9
+ * Builds the URL from the configured api.base_url and the remote's owner_id/repo_name.
10
+ */
11
+
12
+ const path = require('path');
13
+ const { exec } = require('child_process');
14
+ const chalk = require('chalk');
15
+ const { getGentPath, readJSON } = require('../utils/fileSystem');
16
+ const { CONFIG_FILE, parseRemoteUrl } = require('../utils/constants');
17
+ const userConfig = require('../utils/user-config');
18
+
19
+ async function web(options = {}) {
20
+ try {
21
+ const gentPath = await getGentPath();
22
+ const config = await readJSON(path.join(gentPath, CONFIG_FILE));
23
+ const remote = (config.remotes || {}).origin;
24
+ if (!remote) {
25
+ console.error(chalk.red('No origin remote configured.'));
26
+ console.log(chalk.yellow('Run `gent remote add origin <url>` or `gent init --remote`.'));
27
+ process.exit(1);
28
+ }
29
+ const info = parseRemoteUrl(remote.url);
30
+ if (!info) {
31
+ console.error(chalk.red(`Origin URL '${remote.url}' isn't a recognized gent URL.`));
32
+ process.exit(1);
33
+ }
34
+
35
+ const { value: baseUrl } = await userConfig.getResolved('api.base_url');
36
+ // Strip /api suffix if present so we get the web host
37
+ const webHost = baseUrl.replace(/\/api\/?$/, '').replace(/\/$/, '');
38
+
39
+ let url = `${webHost}/${info.owner_id}/${info.repo_name}`;
40
+ if (options.branch) url += `/tree/${encodeURIComponent(options.branch)}`;
41
+ if (options.commit) url += `/commit/${encodeURIComponent(options.commit)}`;
42
+
43
+ if (options.print) {
44
+ console.log(url);
45
+ return;
46
+ }
47
+
48
+ console.log(chalk.gray(`Opening ${url}`));
49
+ openInBrowser(url);
50
+ } catch (error) {
51
+ if (error.code === 'ENOENT' && error.message.includes('.gent')) {
52
+ console.error(chalk.red('Error: Not a gent repository'));
53
+ } else {
54
+ console.error(chalk.red('Error:'), error.message);
55
+ }
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ function openInBrowser(url) {
61
+ const cmd = process.platform === 'darwin' ? 'open'
62
+ : process.platform === 'win32' ? 'start ""'
63
+ : 'xdg-open';
64
+ exec(`${cmd} "${url}"`, (err) => {
65
+ if (err) {
66
+ console.error(chalk.yellow('Could not auto-open. URL:'));
67
+ console.log(url);
68
+ }
69
+ });
70
+ }
71
+
72
+ module.exports = web;
package/src/index.js CHANGED
@@ -7,19 +7,25 @@
7
7
  * ============================================================================
8
8
  *
9
9
  * COMMANDS:
10
+ * Setup: setup, config, doctor
10
11
  * Repository: init, clone
11
12
  * Staging: add, rm, reset, status, diff
12
13
  * History: commit, log, show, tag, explain
13
14
  * Branching: branch, checkout, merge, resolve, stash
14
15
  * Safety: undo, redo
15
- * Insight: summary
16
- * Remote: remote, push, pull
16
+ * Insight: summary, ask, review, docs, changelog
17
+ * Remote: remote, repos, push, pull, search, web, share
17
18
  * Auth: register, login, logout, whoami
19
+ * AI: ai (status|test|models)
20
+ * Templates: template (list|use)
18
21
  *
19
22
  * @author Abdalrahman Kanawati
20
23
  * @version 7.0.0
21
24
  */
22
25
 
26
+ // Boot: load env files BEFORE anything else reads process.env.
27
+ require('./utils/env-loader').load();
28
+
23
29
  const { program } = require('commander');
24
30
  const chalk = require('chalk');
25
31
  const packageJson = require('../package.json');
@@ -55,6 +61,20 @@ const loginCommand = require('./commands/login');
55
61
  const logoutCommand = require('./commands/logout');
56
62
  const whoamiCommand = require('./commands/whoami');
57
63
 
64
+ // Import new gent-platform commands
65
+ const configCommand = require('./commands/config');
66
+ const doctorCommand = require('./commands/doctor');
67
+ const setupCommand = require('./commands/setup');
68
+ const aiCommand = require('./commands/ai');
69
+ const askCommand = require('./commands/ask');
70
+ const reviewCommand = require('./commands/review');
71
+ const docsCommand = require('./commands/docs');
72
+ const changelogCommand = require('./commands/changelog');
73
+ const webCommand = require('./commands/web');
74
+ const shareCommand = require('./commands/share');
75
+ const searchCommand = require('./commands/search');
76
+ const templateCommand = require('./commands/template');
77
+
58
78
  // Configure CLI
59
79
  program
60
80
  .name('gent')
@@ -84,10 +104,16 @@ program
84
104
  .action(statusCommand);
85
105
 
86
106
  program
87
- .command('add <files...>')
88
- .description('Add file contents to the staging area')
107
+ .command('add [files...]')
108
+ .description('Add file contents to the staging area (use -A/--all to add everything)')
89
109
  .option('-A, --all', 'Add all files')
90
- .action(addCommand);
110
+ .action((files, options) => {
111
+ if ((!files || files.length === 0) && !options.all) {
112
+ console.error('error: specify files to add, or use -A to add all');
113
+ process.exit(1);
114
+ }
115
+ return addCommand(files || [], options);
116
+ });
91
117
 
92
118
  program
93
119
  .command('rm <files...>')
@@ -226,6 +252,83 @@ program
226
252
  .description('Pull and merge remote commits')
227
253
  .action(pullCommand);
228
254
 
255
+ // ─── Setup, Config & Diagnostics ────────────────────────
256
+
257
+ program
258
+ .command('setup')
259
+ .description('Interactive first-run wizard (backend URL, login, AI key, identity)')
260
+ .action(setupCommand);
261
+
262
+ program
263
+ .command('config [subcommand] [args...]')
264
+ .description('Manage CLI settings (list|get|set|unset|path) — e.g. gent config set ai.api_key <key>')
265
+ .action(configCommand);
266
+
267
+ program
268
+ .command('doctor')
269
+ .description('Run a health check across node, repo, auth, backend, and AI key')
270
+ .option('--ai', 'Also live-test the AI key with a tiny request')
271
+ .action(doctorCommand);
272
+
273
+ program
274
+ .command('ai [subcommand]')
275
+ .description('Inspect AI integration (status|test|models)')
276
+ .action(aiCommand);
277
+
278
+ // ─── Platform-special (AI-powered) ──────────────────────
279
+
280
+ program
281
+ .command('ask <question>')
282
+ .description('Ask Claude a question about this repo (needs AI key)')
283
+ .action(askCommand);
284
+
285
+ program
286
+ .command('review [ref]')
287
+ .description('AI code review on staged changes (default), HEAD, or a specific commit')
288
+ .option('--staged', 'Force review of staged changes')
289
+ .option('--head', 'Force review of HEAD commit')
290
+ .action(reviewCommand);
291
+
292
+ program
293
+ .command('docs')
294
+ .description('Generate a README.md draft for this repo using AI')
295
+ .option('--write', 'Write the draft to README.md instead of stdout')
296
+ .option('--section <name>', 'Only generate a single named section')
297
+ .action(docsCommand);
298
+
299
+ program
300
+ .command('changelog [range]')
301
+ .description('Print a changelog. range = <from>..<to> or <from> (default: since last tag)')
302
+ .option('--plain', 'Skip AI grouping — flat commit list')
303
+ .action(changelogCommand);
304
+
305
+ program
306
+ .command('web')
307
+ .description('Open the current repo (or a branch/commit) on the gent web app')
308
+ .option('--branch <name>', 'Open a specific branch')
309
+ .option('--commit <hash>', 'Open a specific commit')
310
+ .option('--print', 'Print the URL instead of launching a browser')
311
+ .action(webCommand);
312
+
313
+ program
314
+ .command('share')
315
+ .description('Print a shareable link to current branch tip (or --branch/--commit)')
316
+ .option('--branch <name>', 'Link to a specific branch')
317
+ .option('--commit <hash>', 'Link to a specific commit')
318
+ .action(shareCommand);
319
+
320
+ program
321
+ .command('search [query]')
322
+ .description('Search your repositories on the gent backend')
323
+ .option('--mine', 'Only repos you own')
324
+ .option('--json', 'Output as JSON')
325
+ .action(searchCommand);
326
+
327
+ program
328
+ .command('template [subcommand] [args...]')
329
+ .description('Quick-start from a baked-in template (list|use <name> [directory])')
330
+ .action(templateCommand);
331
+
229
332
  // ─── Authentication ─────────────────────────────────────
230
333
 
231
334
  program
@@ -267,19 +370,57 @@ program
267
370
  }
268
371
  });
269
372
 
373
+ // Friendlier global error mapping. Per-command handlers still own their own
374
+ // errors; this catches anything that bubbles up (e.g. unknown command).
375
+ function explainError(err) {
376
+ if (!err) return '';
377
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
378
+ return `Cannot reach the gent backend. Check the URL with \`gent config get api.base_url\` and try \`gent doctor\`.`;
379
+ }
380
+ if (err.code === 'commander.unknownCommand') {
381
+ return `${err.message}\n\nRun \`gent\` (no args) to see the command list, or \`gent help <command>\` for details.`;
382
+ }
383
+ return err.message;
384
+ }
385
+
386
+ function showQuickstart() {
387
+ console.log();
388
+ console.log(chalk.bold.cyan('Gent CLI ') + chalk.gray(`v${packageJson.version}`));
389
+ console.log(chalk.gray('A Git-like VCS with cloud sync + AI superpowers.\n'));
390
+ console.log(chalk.bold('First time? Try:'));
391
+ console.log(` ${chalk.cyan('gent setup')} ${chalk.gray('interactive walkthrough (login + AI key + remote)')}`);
392
+ console.log(` ${chalk.cyan('gent doctor')} ${chalk.gray('check everything is wired up')}`);
393
+ console.log(` ${chalk.cyan('gent template list')} ${chalk.gray('scaffold a starter project')}`);
394
+ console.log();
395
+ console.log(chalk.bold('Everyday flow:'));
396
+ console.log(` ${chalk.cyan('gent init && gent add -A && gent commit -m "init"')}`);
397
+ console.log(` ${chalk.cyan('gent push')} / ${chalk.cyan('gent pull')} / ${chalk.cyan('gent merge <branch>')}`);
398
+ console.log();
399
+ console.log(chalk.bold('AI features (need an Anthropic key):'));
400
+ console.log(` ${chalk.cyan('gent ask "what does this repo do?"')}`);
401
+ console.log(` ${chalk.cyan('gent review')} ${chalk.gray('review staged changes')}`);
402
+ console.log(` ${chalk.cyan('gent docs --write')} ${chalk.gray('generate README.md')}`);
403
+ console.log(` ${chalk.cyan('gent changelog')} ${chalk.gray('grouped release notes')}`);
404
+ console.log();
405
+ console.log(chalk.gray('Full command list: ') + chalk.cyan('gent --help'));
406
+ console.log();
407
+ }
408
+
270
409
  // Error handling
271
410
  program.exitOverride();
272
411
 
273
412
  try {
274
- program.parse(process.argv);
275
-
276
- // Show help if no command provided
413
+ // Show quickstart if no command provided (instead of raw help).
277
414
  if (!process.argv.slice(2).length) {
278
- program.outputHelp();
415
+ showQuickstart();
416
+ process.exit(0);
279
417
  }
418
+
419
+ program.parse(process.argv);
420
+
280
421
  } catch (err) {
281
422
  if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed' && err.code !== 'commander.version') {
282
- console.error(chalk.red('Error:'), err.message);
423
+ console.error(chalk.red('Error:'), explainError(err));
283
424
  process.exit(1);
284
425
  }
285
426
  }
@@ -11,9 +11,11 @@
11
11
  * is absent or the request fails.
12
12
  *
13
13
  * ENABLEMENT:
14
- * Set ANTHROPIC_API_KEY in the environment to enable. Optionally set
15
- * GENT_AI_MODEL to pick a model (default: claude-opus-4-8). For a cheaper /
16
- * faster option set GENT_AI_MODEL=claude-haiku-4-5.
14
+ * Either set ANTHROPIC_API_KEY in the environment, OR save it once with
15
+ * `gent config set ai.api_key <key>` (stored in ~/.gent/cli-config.json).
16
+ * Optionally pick a model with GENT_AI_MODEL or `gent config set ai.model`.
17
+ * Default model: claude-opus-4-7. For a cheaper / faster option try
18
+ * claude-haiku-4-5 or claude-sonnet-4-6.
17
19
  *
18
20
  * IMPLEMENTATION NOTE:
19
21
  * Calls the Anthropic Messages API (POST /v1/messages) directly over the
@@ -26,39 +28,63 @@
26
28
  */
27
29
 
28
30
  const axios = require('axios');
31
+ const userConfig = require('./user-config');
29
32
 
30
33
  const API_URL = 'https://api.anthropic.com/v1/messages';
31
34
  const API_VERSION = '2023-06-01';
32
- const DEFAULT_MODEL = 'claude-opus-4-8';
35
+ const DEFAULT_MODEL = 'claude-opus-4-7';
33
36
 
34
- /**
35
- * Resolve the API key (env only — keeps secrets out of the repo).
36
- * @returns {String|null}
37
- */
38
- function getApiKey() {
39
- return process.env.ANTHROPIC_API_KEY || null;
37
+ // Per-process cache so repeated AI calls don't keep hitting disk.
38
+ let _resolvedKey;
39
+ let _resolvedKeySource;
40
+ let _resolvedModel;
41
+
42
+ async function resolveKey() {
43
+ if (_resolvedKey !== undefined) {
44
+ return { value: _resolvedKey, source: _resolvedKeySource };
45
+ }
46
+ const r = await userConfig.getResolved('ai.api_key');
47
+ _resolvedKey = r.value || null;
48
+ _resolvedKeySource = r.source;
49
+ return { value: _resolvedKey, source: _resolvedKeySource };
40
50
  }
41
51
 
42
- /**
43
- * @returns {Boolean} whether AI features are enabled.
44
- */
45
- function isEnabled() {
46
- return !!getApiKey();
52
+ async function resolveModel() {
53
+ if (_resolvedModel) return _resolvedModel;
54
+ const r = await userConfig.getResolved('ai.model');
55
+ _resolvedModel = r.value || DEFAULT_MODEL;
56
+ return _resolvedModel;
47
57
  }
48
58
 
49
59
  /**
50
- * @returns {String} the model id to use.
60
+ * Synchronous getter used in hot paths. Returns whatever was last resolved,
61
+ * or falls back to env-only (the original behavior) on cold start.
51
62
  */
63
+ function getApiKey() {
64
+ if (_resolvedKey !== undefined) return _resolvedKey;
65
+ return process.env.ANTHROPIC_API_KEY || null;
66
+ }
67
+
52
68
  function getModel() {
69
+ if (_resolvedModel) return _resolvedModel;
53
70
  return process.env.GENT_AI_MODEL || DEFAULT_MODEL;
54
71
  }
55
72
 
56
73
  /**
57
- * One-line hint shown by commands when AI is requested but no key is set.
58
- * @returns {String}
74
+ * Async pre-flight resolver call once from a command before doing AI work
75
+ * so isEnabled()/getModel() see the user-config values even if env is empty.
59
76
  */
77
+ async function prime() {
78
+ await resolveKey();
79
+ await resolveModel();
80
+ }
81
+
82
+ function isEnabled() {
83
+ return !!getApiKey();
84
+ }
85
+
60
86
  function disabledHint() {
61
- return 'AI features are off — set ANTHROPIC_API_KEY to enable (optional: GENT_AI_MODEL).';
87
+ return 'AI features are off — save a key with `gent config set ai.api_key <key>` or set ANTHROPIC_API_KEY in your env.';
62
88
  }
63
89
 
64
90
  /**
@@ -69,7 +95,11 @@ function disabledHint() {
69
95
  * @param {Number} [opts.maxTokens]
70
96
  * @returns {Promise<String>}
71
97
  */
72
- async function complete({ prompt, system, maxTokens = 1024 }) {
98
+ async function complete({ prompt, system, maxTokens = 1024, thinking = false }) {
99
+ // Make sure env/config-stored values are resolved even if the caller
100
+ // didn't prime() first.
101
+ await prime();
102
+
73
103
  const apiKey = getApiKey();
74
104
  if (!apiKey) throw new Error('AI not enabled');
75
105
 
@@ -79,22 +109,50 @@ async function complete({ prompt, system, maxTokens = 1024 }) {
79
109
  messages: [{ role: 'user', content: prompt }]
80
110
  };
81
111
  if (system) body.system = system;
112
+ // Adaptive thinking — opt-in per caller. We leave display at the API
113
+ // default ("omitted") so reasoning never leaks into CLI output; this just
114
+ // lets the model think harder on complex tasks (review, conflict resolve)
115
+ // without changing what the user sees.
116
+ if (thinking) body.thinking = { type: 'adaptive' };
117
+
118
+ try {
119
+ const res = await axios.post(API_URL, body, {
120
+ headers: {
121
+ 'x-api-key': apiKey,
122
+ 'anthropic-version': API_VERSION,
123
+ 'content-type': 'application/json'
124
+ },
125
+ timeout: 60000
126
+ });
127
+
128
+ const blocks = (res.data && res.data.content) || [];
129
+ return blocks
130
+ .filter(b => b.type === 'text')
131
+ .map(b => b.text)
132
+ .join('')
133
+ .trim();
134
+ } catch (err) {
135
+ throw enrichAiError(err);
136
+ }
137
+ }
82
138
 
83
- const res = await axios.post(API_URL, body, {
84
- headers: {
85
- 'x-api-key': apiKey,
86
- 'anthropic-version': API_VERSION,
87
- 'content-type': 'application/json'
88
- },
89
- timeout: 60000
90
- });
91
-
92
- const blocks = (res.data && res.data.content) || [];
93
- return blocks
94
- .filter(b => b.type === 'text')
95
- .map(b => b.text)
96
- .join('')
97
- .trim();
139
+ /**
140
+ * Wrap raw Anthropic errors with hints that actually help the user.
141
+ */
142
+ function enrichAiError(err) {
143
+ const status = err?.response?.status;
144
+ const apiMsg = err?.response?.data?.error?.message || err?.response?.data?.message;
145
+ if (status === 401) {
146
+ return new Error('Anthropic rejected the API key (401). Check `gent config get ai.api_key` and try `gent ai test`.');
147
+ }
148
+ if (status === 404 || (apiMsg && /model/i.test(apiMsg))) {
149
+ return new Error(`Anthropic rejected the model "${getModel()}" — set a valid one with \`gent config set ai.model claude-opus-4-7\`.`);
150
+ }
151
+ if (status === 429) {
152
+ return new Error('Anthropic rate-limited the request (429). Retry in a moment or switch to a lighter model.');
153
+ }
154
+ if (apiMsg) return new Error(`AI request failed: ${apiMsg}`);
155
+ return err;
98
156
  }
99
157
 
100
158
  // ─── High-level helpers ─────────────────────────────────
@@ -142,15 +200,20 @@ async function resolveConflictHunk({ base, ours, theirs, fileName }) {
142
200
  `======= OURS\n${ours}\n` +
143
201
  `======= THEIRS\n${theirs}\n>>>>>>>\n\n` +
144
202
  'Return the merged result for this section.';
145
- return complete({ system, prompt, maxTokens: 2048 });
203
+ return complete({ system, prompt, maxTokens: 2048, thinking: true });
146
204
  }
147
205
 
148
206
  module.exports = {
149
207
  isEnabled,
150
208
  getModel,
209
+ getApiKey,
151
210
  disabledHint,
211
+ prime,
212
+ resolveKey,
213
+ resolveModel,
152
214
  complete,
153
215
  suggestCommitMessage,
154
216
  explainChanges,
155
- resolveConflictHunk
217
+ resolveConflictHunk,
218
+ DEFAULT_MODEL,
156
219
  };