gent-cli 23.0.0 → 25.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.
package/QUICKSTART.md CHANGED
@@ -60,7 +60,7 @@ gent status -s # short format
60
60
  gent commit -m "Initial commit"
61
61
  ```
62
62
 
63
- Let AI suggest a message from your staged diff (requires `ANTHROPIC_API_KEY`):
63
+ Let local Gent AI suggest a message from your staged diff:
64
64
 
65
65
  ```bash
66
66
  gent commit --ai
@@ -158,16 +158,17 @@ gent summary --ai # + a short AI-written health narrative
158
158
 
159
159
  ## Optional AI Features
160
160
 
161
- All AI features are off by default and have a non-AI fallback.
161
+ AI calls run directly from the CLI using the installation-provisioned credential.
162
+ Users are never prompted for a provider key. Commands with a non-AI path
163
+ continue to work offline.
162
164
 
163
165
  ```bash
164
- export ANTHROPIC_API_KEY=sk-ant-...
165
- export GENT_AI_MODEL=claude-haiku-4-5 # optional; default is claude-opus-4-8
166
-
167
166
  gent commit --ai # AI-suggested commit message
168
167
  gent explain # plain-language diff summary
169
168
  gent resolve # adds "Ask AI" option per conflict hunk
170
169
  gent summary --ai # health narrative
170
+ gent chat # interactive repository chat
171
+ gent merge dev --ai # merge, AI-resolve conflicts, commit, then review
171
172
  ```
172
173
 
173
174
  ---
package/README.md CHANGED
@@ -24,7 +24,7 @@ Beyond a faithful git-like workflow, Gent adds:
24
24
  - **`gent undo` / `gent redo`** — a one-command safety net over an operation journal (friendlier than `git reflog`).
25
25
  - **`gent resolve`** — an interactive conflict resolver (ours / theirs / both / edit / AI).
26
26
  - **`gent summary`** — a repository health dashboard, plus **`gent log --graph`**.
27
- - **Optional AI** (`gent commit --ai`, `gent explain`, `gent summary --ai`, AI option in `gent resolve`) — off by default, enabled with `ANTHROPIC_API_KEY`.
27
+ - **Direct local AI** (`gent chat`, `gent review`, `gent merge --ai`, `gent resolve --ai`) — low-latency OpenAI calls from the CLI with task-specific prompts and no per-command key prompt.
28
28
  - **Genti, your terminal mascot** — a mint one-eyed sky-jelly that *acts out* your workflow: it floats a file crate to the cloud on `gent push`, carries one home on `gent pull`, and reconciles two branches on `gent merge`. It plays once (in place, no scrollback spam) after a successful command. Meet it directly with `gent pet` (add `--loop` to keep it running; try `gent pet push|pull|merge|auth`). Set `GENT_NO_PET=1` (or run in CI / a non-interactive shell) to turn the celebrations off.
29
29
 
30
30
  See [docs/COMMANDS.md](docs/COMMANDS.md) for the full reference and
@@ -489,7 +489,9 @@ Undo never deletes your working files; for content-discarding operations
489
489
 
490
490
  ```bash
491
491
  gent summary # repository health & statistics dashboard
492
- gent summary --ai # + a short AI-written assessment (needs a key)
492
+ gent summary --ai # + a short AI-written assessment
493
+ gent chat # interactive repository chat
494
+ gent merge feature --ai # merge, AI-resolve conflicts, commit, then review
493
495
  gent log --graph # ASCII commit graph with branches and merges
494
496
  gent explain # explain the latest commit in plain language
495
497
  gent explain <commit> # explain a specific commit
@@ -498,12 +500,11 @@ gent explain --staged # explain currently staged changes
498
500
 
499
501
  ### Optional AI features
500
502
 
501
- AI is off by default and every feature has a non-AI fallback. Enable it with:
503
+ AI calls run directly from the CLI using the credential provisioned for the
504
+ installation. Commands never prompt users for a provider key. Fast task-specific
505
+ prompts keep review, merge resolution, chat, and summaries concise.
502
506
 
503
507
  ```bash
504
- export ANTHROPIC_API_KEY=sk-ant-...
505
- export GENT_AI_MODEL=claude-haiku-4-5 # optional; default is claude-opus-4-8
506
-
507
508
  gent commit --ai # suggest a commit message from the staged diff
508
509
  gent explain # narrate a diff instead of just printing it
509
510
  gent resolve # adds an "Ask AI" choice per conflict hunk
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gent-cli",
3
- "version": "23.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "A modern, Git-like version control CLI with cloud sync, AI-powered superpowers (ask/review/docs/changelog), and zero-friction setup (gent setup/doctor/config).",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node src/index.js",
11
- "test": "node --check src/index.js && node --test tests/diff.test.js tests/merge.test.js tests/hash.test.js tests/merge-base.test.js tests/file-system.test.js tests/web-urls.test.js && node tests/offline-e2e.js && node --test tests/git-compat/engine.test.js tests/git-compat/cli.test.js tests/git-compat/transport.test.js tests/git-compat/migrate.test.js",
12
- "test:unit": "node --test tests/diff.test.js tests/merge.test.js tests/hash.test.js tests/merge-base.test.js tests/file-system.test.js tests/web-urls.test.js",
11
+ "test": "node --check src/index.js && node --test tests/diff.test.js tests/merge.test.js tests/hash.test.js tests/merge-base.test.js tests/file-system.test.js tests/web-urls.test.js tests/repos.test.js tests/ai-service.test.js && node tests/offline-e2e.js && node tests/ai-merge.e2e.js && node --test tests/git-compat/engine.test.js tests/git-compat/cli.test.js tests/git-compat/transport.test.js tests/git-compat/migrate.test.js",
12
+ "test:unit": "node --test tests/diff.test.js tests/merge.test.js tests/hash.test.js tests/merge-base.test.js tests/file-system.test.js tests/web-urls.test.js tests/repos.test.js tests/ai-service.test.js",
13
13
  "test:e2e": "node tests/offline-e2e.js",
14
14
  "test:remote:e2e": "node tests/remote-e2e.js",
15
15
  "demo": "bash demo.sh",
@@ -1,21 +1,14 @@
1
1
  /**
2
2
  * AI Command - Manage and verify AI integration.
3
3
  *
4
- * gent ai status → show key source, model, where it came from
4
+ * gent ai status → show the managed service status
5
5
  * gent ai test → make a tiny live request to confirm it works
6
- * gent ai models → list the model ids gent suggests
6
+ * gent ai models → show provider management information
7
7
  */
8
8
 
9
9
  const chalk = require('chalk');
10
10
  const ora = require('ora');
11
11
  const ai = require('../utils/ai-service');
12
- const userConfig = require('../utils/user-config');
13
-
14
- const SUGGESTED_MODELS = [
15
- { id: 'claude-opus-4-7', tag: 'flagship', note: 'Highest quality' },
16
- { id: 'claude-sonnet-4-6', tag: 'balanced', note: 'Strong, faster, cheaper' },
17
- { id: 'claude-haiku-4-5', tag: 'fastest', note: 'Fastest & cheapest' },
18
- ];
19
12
 
20
13
  async function aiCommand(subcommand) {
21
14
  const sub = (subcommand || 'status').toLowerCase();
@@ -31,32 +24,28 @@ async function aiCommand(subcommand) {
31
24
  }
32
25
 
33
26
  async function status() {
34
- const { value: key, source: keySource } = await ai.resolveKey();
35
- const model = await ai.resolveModel();
36
- const { source: modelSource } = await userConfig.getResolved('ai.model');
27
+ const { value: available, source } = await ai.resolveKey();
37
28
 
38
29
  console.log(chalk.bold.cyan('\nGent AI status\n'));
39
- if (key) {
40
- console.log(` ${chalk.green('●')} API key: ${userConfig.maskSecret(key)} ${chalk.gray(`[${keySource}]`)}`);
30
+ if (available) {
31
+ console.log(` ${chalk.green('●')} Service: ${chalk.white(`direct OpenAI [${source}]`)}`);
41
32
  } else {
42
- console.log(` ${chalk.gray('○')} API key: ${chalk.gray('not set')}`);
33
+ console.log(` ${chalk.gray('○')} Service: ${chalk.gray('unavailable')}`);
43
34
  console.log(chalk.gray(' ↳ ' + ai.disabledHint()));
44
35
  }
45
- console.log(` ${chalk.green('●')} Model: ${model} ${chalk.gray(`[${modelSource}]`)}`);
46
- console.log(chalk.gray('\n Run `gent ai test` to verify the key actually works.'));
36
+ console.log(` ${chalk.green('●')} Model: ${await ai.resolveModel()}`);
37
+ console.log(chalk.gray('\n Run `gent ai test` to verify the service.'));
47
38
  console.log();
48
39
  }
49
40
 
50
41
  async function test() {
51
- const { value: key } = await ai.resolveKey();
52
- if (!key) {
53
- console.error(chalk.red('No AI key configured.'));
54
- console.log(chalk.yellow('Set one with `gent config set ai.api_key <key>`.'));
42
+ const { value: available } = await ai.resolveKey();
43
+ if (!available) {
44
+ console.error(chalk.red(ai.disabledHint()));
55
45
  process.exit(1);
56
46
  }
57
47
 
58
- const model = await ai.resolveModel();
59
- const spinner = ora(`Pinging Anthropic (${model})...`).start();
48
+ const spinner = ora('Pinging Gent AI...').start();
60
49
  try {
61
50
  const reply = await ai.complete({
62
51
  prompt: 'Reply with the single word: pong',
@@ -70,13 +59,9 @@ async function test() {
70
59
  }
71
60
 
72
61
  async function models() {
73
- const current = await ai.resolveModel();
74
- console.log(chalk.bold.cyan('\nSuggested Claude models\n'));
75
- for (const m of SUGGESTED_MODELS) {
76
- const active = m.id === current ? chalk.green(' (current)') : '';
77
- console.log(` ${chalk.cyan(m.id.padEnd(22))} ${chalk.gray(m.tag.padEnd(10))} ${m.note}${active}`);
78
- }
79
- console.log(chalk.gray('\n Switch with: gent config set ai.model <id>\n'));
62
+ console.log(chalk.bold.cyan('\nGent AI model\n'));
63
+ console.log(chalk.white(` ${await ai.resolveModel()} (current)`));
64
+ console.log(chalk.gray(' Optimized for low-latency review, merge, and chat.\n'));
80
65
  }
81
66
 
82
67
  module.exports = aiCommand;
@@ -6,7 +6,7 @@
6
6
  * gent ask "what's pending on the current branch?"
7
7
  *
8
8
  * Builds a compact repo summary (README + last N commits + tree listing) and
9
- * sends it as context. Falls back to a useful text dump if no AI key is set.
9
+ * sends it as context. Falls back to a useful text dump when signed out.
10
10
  */
11
11
 
12
12
  const path = require('path');
@@ -30,6 +30,7 @@ async function ask(question, options = {}) {
30
30
  const gentPath = await getGentPath();
31
31
  const context = await buildRepoContext(gentPath);
32
32
 
33
+ await ai.prime();
33
34
  if (!ai.isEnabled()) {
34
35
  console.log(chalk.yellow(ai.disabledHint()));
35
36
  console.log(chalk.gray('\nHere is the raw repo context you can pipe into another tool:\n'));
@@ -40,6 +41,7 @@ async function ask(question, options = {}) {
40
41
  const spinner = ora(`Asking ${ai.getModel()}...`).start();
41
42
  try {
42
43
  const answer = await ai.complete({
44
+ profile: 'chat',
43
45
  system:
44
46
  'You are a senior engineer answering questions about a software repository. ' +
45
47
  'Be concrete and concise. If the answer is not in the context, say so. ' +
@@ -119,3 +121,4 @@ async function findReadme(cwd) {
119
121
  }
120
122
 
121
123
  module.exports = ask;
124
+ module.exports.buildRepoContext = buildRepoContext;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  const fs = require('fs').promises;
5
5
  const path = require('path');
6
+ const inquirer = require('inquirer');
6
7
  const repository = require('../utils/repository');
7
8
  const ops = require('../utils/gent-ops');
8
9
  const merge = require('../utils/merge-ops');
@@ -13,6 +14,7 @@ const { GitIndex } = require('../utils/git-index');
13
14
  const { Lock } = require('../utils/lockfile');
14
15
  const { AttributesMatcher, looksBinary } = require('../utils/attributes');
15
16
  const { formatUnifiedDiff } = require('../utils/diff-engine');
17
+ const ai = require('../utils/ai-service');
16
18
 
17
19
  async function locatedCanonical() {
18
20
  let found;
@@ -154,21 +156,93 @@ const handlers = {
154
156
  async merge(repo, branch, options) {
155
157
  if (options.abort) return merge.abortMerge(repo);
156
158
  if (options.continue) return merge.concludeMerge(repo, options.message);
159
+ if (options.ai) {
160
+ await ai.prime();
161
+ if (!ai.isEnabled()) throw new Error(ai.disabledHint());
162
+ }
163
+ const beforeMerge = options.ai ? (await repo.refs.head()).oid : null;
157
164
  const result = await merge.merge(repo, branch, options);
158
165
  if (result.status === 'conflicts') {
159
166
  for (const conflict of result.conflicts) {
160
167
  if (conflict.kind === 'content') console.log(`Auto-merging ${conflict.path}`);
161
168
  console.log(`CONFLICT (${conflict.kind}): Merge conflict in ${conflict.path}`);
162
169
  }
163
- console.log('Automatic merge failed; fix conflicts and then commit the result.');
164
- console.log('Resolve files, stage with gent add, then gent merge --continue or gent commit -m <message>.');
165
- process.exitCode = 1;
166
- } else console.log(result.status);
170
+ if (options.ai) {
171
+ console.log('Resolving all text conflicts with Gent AI...');
172
+ await handlers.resolve(repo, { ai: true });
173
+ } else {
174
+ console.log('Automatic merge failed; fix conflicts and then commit the result.');
175
+ console.log('Resolve files, stage with gent add, then gent merge --continue or gent commit -m <message>.');
176
+ process.exitCode = 1;
177
+ }
178
+ } else {
179
+ console.log(result.status);
180
+ if (options.ai) await reviewCanonicalMerge(repo, beforeMerge, result.oid);
181
+ }
167
182
  },
168
- async resolve(repo) {
183
+ async resolve(repo, options = {}) {
169
184
  const index = await GitIndex.read(repo.indexPath);
170
- for (const name of index.conflicts().keys()) console.log(name);
171
- console.log('Edit conflicted files, then gent add <path> and gent merge --continue.');
185
+ const names = [...index.conflicts().keys()];
186
+ if (!names.length) {
187
+ console.log('No merge conflicts to resolve.');
188
+ return;
189
+ }
190
+ if (!options.ai && process.stdin.isTTY && process.stdout.isTTY) {
191
+ const { mode } = await inquirer.prompt([{
192
+ type: 'list',
193
+ name: 'mode',
194
+ message: 'How should Gent resolve this merge?',
195
+ choices: [
196
+ { name: 'Merge with AI (fast) — resolve, commit, then review', value: 'ai' },
197
+ { name: 'Resolve manually', value: 'manual' },
198
+ ],
199
+ }]);
200
+ options.ai = mode === 'ai';
201
+ }
202
+ if (!options.ai) {
203
+ for (const name of names) console.log(name);
204
+ console.log('Edit conflicted files, then gent add <path> and gent merge --continue.');
205
+ return;
206
+ }
207
+
208
+ await ai.prime();
209
+ if (!ai.isEnabled()) throw new Error(ai.disabledHint());
210
+
211
+ let resolved = 0;
212
+ for (const name of names) {
213
+ const sides = await merge.conflictSides(repo, name);
214
+ if ([sides.base, sides.ours, sides.theirs].some(value => value && looksBinary(value))) {
215
+ console.log(`Skipping binary conflict: ${name}`);
216
+ continue;
217
+ }
218
+ try {
219
+ const suggestion = await ai.resolveConflictHunk({
220
+ base: sides.base?.toString('utf8') || '',
221
+ ours: sides.ours?.toString('utf8') || '',
222
+ theirs: sides.theirs?.toString('utf8') || '',
223
+ fileName: name,
224
+ });
225
+ worktree.assertSafeCheckoutPath(repo, name);
226
+ await worktree.assertNoSymlinkParent(repo, name);
227
+ const absolute = path.join(repo.worktree, name);
228
+ await fs.mkdir(path.dirname(absolute), { recursive: true });
229
+ await fs.writeFile(absolute, suggestion, 'utf8');
230
+ await merge.markResolved(repo, name);
231
+ resolved++;
232
+ console.log(`Resolved and staged ${name}`);
233
+ } catch (error) {
234
+ console.log(`AI did not resolve ${name}: ${error.message}`);
235
+ }
236
+ }
237
+ console.log(`${resolved} of ${names.length} conflict(s) resolved with Gent AI.`);
238
+ if (resolved !== names.length) {
239
+ console.log('Unresolved conflicts remain; no merge commit was created.');
240
+ process.exitCode = 1;
241
+ return;
242
+ }
243
+ const commit = await merge.concludeMerge(repo);
244
+ console.log(`Merge committed: ${commit.oid.slice(0, 12)}`);
245
+ await reviewCanonicalMerge(repo, (await repo.objects.readCommit(commit.oid)).parents[0], commit.oid);
172
246
  },
173
247
  async stash(repo, sub = 'push', options = {}) {
174
248
  const position = Number(options.index || 0);
@@ -230,6 +304,41 @@ const handlers = {
230
304
  }
231
305
  };
232
306
 
307
+ async function reviewCanonicalMerge(repo, beforeOid, afterOid) {
308
+ if (!beforeOid || !afterOid) return;
309
+ const beforeCommit = await repo.objects.readCommit(beforeOid);
310
+ const afterCommit = await repo.objects.readCommit(afterOid);
311
+ const before = await worktree.readTreeRecursive(repo, beforeCommit.tree);
312
+ const after = await worktree.readTreeRecursive(repo, afterCommit.tree);
313
+ const diff = (await treeDiffText(repo, before, after)).slice(0, 16000);
314
+ if (!diff) {
315
+ console.log('AI review: no content changes to review.');
316
+ return;
317
+ }
318
+ console.log('\nAI review of the completed merge');
319
+ try {
320
+ console.log(await ai.reviewChanges(diff, 'Review the completed merge result. Focus on integration regressions.'));
321
+ } catch (error) {
322
+ console.log(`Post-merge AI review unavailable: ${error.message}`);
323
+ }
324
+ }
325
+
326
+ async function treeDiffText(repo, before, after) {
327
+ const parts = [];
328
+ for (const name of new Set([...before.keys(), ...after.keys()])) {
329
+ const a = before.get(name), b = after.get(name);
330
+ const read = item => item ? repo.objects.readBlob(item.oid) : Buffer.alloc(0);
331
+ const oldBytes = await read(a), newBytes = await read(b);
332
+ if (oldBytes.equals(newBytes) && a?.mode === b?.mode) continue;
333
+ if (looksBinary(oldBytes) || looksBinary(newBytes)) {
334
+ parts.push(`${name}: binary file changed`);
335
+ } else {
336
+ parts.push(formatUnifiedDiff(name, oldBytes.toString(), newBytes.toString()));
337
+ }
338
+ }
339
+ return parts.filter(Boolean).join('\n\n');
340
+ }
341
+
233
342
  async function printTreeDiff(repo, before, after, files = [], stat = false) {
234
343
  const wanted = files.map(name => repo.relativePath(path.resolve(name)));
235
344
  for (const name of new Set([...before.keys(), ...after.keys()])) {
@@ -40,6 +40,7 @@ async function changelog(range, options = {}) {
40
40
  )}`;
41
41
  console.log('\n' + header + '\n');
42
42
 
43
+ if (!options.plain) await ai.prime();
43
44
  if (options.plain || !ai.isEnabled()) {
44
45
  for (const c of selected) {
45
46
  const short = (c.hash || '').slice(0, 7);
@@ -59,6 +60,7 @@ async function changelog(range, options = {}) {
59
60
  const spinner = ora(`Grouping with ${ai.getModel()}...`).start();
60
61
  try {
61
62
  const out = await ai.complete({
63
+ profile: 'changelog',
62
64
  system:
63
65
  'You write release-note-style changelogs. Group commits into ' +
64
66
  'Features / Fixes / Improvements / Other. Keep each bullet to one line ' +
@@ -0,0 +1,80 @@
1
+ /** Interactive, repository-aware Gent AI chat. */
2
+
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const ora = require('ora');
6
+ const { getGentPath } = require('../utils/fileSystem');
7
+ const ai = require('../utils/ai-service');
8
+ const { buildRepoContext } = require('./ask');
9
+
10
+ const MAX_HISTORY_CHARS = 12000;
11
+
12
+ async function chat(initialMessage) {
13
+ try {
14
+ await ai.prime();
15
+ if (!ai.isEnabled()) {
16
+ console.error(chalk.red(ai.disabledHint()));
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+
21
+ const gentPath = await getGentPath();
22
+ const context = await buildRepoContext(gentPath);
23
+ const history = [];
24
+
25
+ if (initialMessage && initialMessage.trim()) {
26
+ await reply(initialMessage.trim(), context, history);
27
+ return;
28
+ }
29
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
30
+ console.error(chalk.red('Usage: gent chat "<message>"'));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+
35
+ console.log(chalk.bold.cyan(`\nGent AI chat (${ai.getModel()})`));
36
+ console.log(chalk.gray('Type exit or quit to finish.\n'));
37
+ while (true) {
38
+ const { message } = await inquirer.prompt([{
39
+ type: 'input',
40
+ name: 'message',
41
+ message: 'You:',
42
+ }]);
43
+ const text = message.trim();
44
+ if (!text) continue;
45
+ if (/^(exit|quit)$/i.test(text)) break;
46
+ await reply(text, context, history);
47
+ }
48
+ } catch (error) {
49
+ console.error(chalk.red('Chat failed:'), error.message);
50
+ process.exitCode = 1;
51
+ }
52
+ }
53
+
54
+ async function reply(message, context, history) {
55
+ const previous = history
56
+ .map(turn => `User: ${turn.user}\nAssistant: ${turn.assistant}`)
57
+ .join('\n\n')
58
+ .slice(-MAX_HISTORY_CHARS);
59
+ const spinner = ora('Thinking...').start();
60
+ try {
61
+ const answer = await ai.complete({
62
+ profile: 'chat',
63
+ prompt:
64
+ `Repository context:\n${context}\n\n` +
65
+ (previous ? `Conversation:\n${previous}\n\n` : '') +
66
+ `User: ${message}`,
67
+ maxTokens: 700,
68
+ });
69
+ spinner.stop();
70
+ console.log(chalk.cyan('Gent AI: ') + answer + '\n');
71
+ history.push({ user: message, assistant: answer });
72
+ while (JSON.stringify(history).length > MAX_HISTORY_CHARS && history.length > 1) {
73
+ history.shift();
74
+ }
75
+ } catch (error) {
76
+ spinner.fail(chalk.red(error.message));
77
+ }
78
+ }
79
+
80
+ module.exports = chat;
@@ -41,6 +41,7 @@ async function commit(options) {
41
41
 
42
42
  // Optional: AI-suggested commit message (`gent commit --ai`)
43
43
  if (!message && options.ai) {
44
+ await ai.prime();
44
45
  if (!ai.isEnabled()) {
45
46
  console.log(chalk.yellow(ai.disabledHint()));
46
47
  } else {
@@ -7,12 +7,10 @@
7
7
  * gent config unset <key> → remove a setting
8
8
  * gent config path → print config file location
9
9
  *
10
- * gent config set ai.api_key <key> ← stored obfuscated
11
10
  * gent config set api.base_url http://localhost:8000
12
11
  */
13
12
 
14
13
  const chalk = require('chalk');
15
- const inquirer = require('inquirer');
16
14
  const userConfig = require('../utils/user-config');
17
15
 
18
16
  async function config(subcommand, args, options) {
@@ -102,26 +100,13 @@ async function set(key, value, options) {
102
100
  process.exit(1);
103
101
  }
104
102
 
105
- // Secret prompt: if no value given for ai.api_key, prompt with masking.
106
- if ((value === undefined || value === '') && key === 'ai.api_key') {
107
- const answers = await inquirer.prompt([{
108
- type: 'password',
109
- name: 'value',
110
- message: 'Anthropic API key:',
111
- mask: '*',
112
- validate: (input) => input.length > 0 || 'Key cannot be empty',
113
- }]);
114
- value = answers.value;
115
- }
116
-
117
103
  if (value === undefined) {
118
104
  console.error(chalk.red('Usage: gent config set <key> <value>'));
119
105
  process.exit(1);
120
106
  }
121
107
 
122
108
  await userConfig.set(key, value);
123
- const display = key === 'ai.api_key' ? userConfig.maskSecret(value) : value;
124
- console.log(chalk.green(`✓ ${key} = ${display}`));
109
+ console.log(chalk.green(`✓ ${key} = ${value}`));
125
110
 
126
111
  const envName = userConfig.ENV_OVERRIDES[key];
127
112
  if (envName && process.env[envName]) {
@@ -30,6 +30,7 @@ async function docs(options = {}) {
30
30
  const cwd = process.cwd();
31
31
  const gentPath = await getGentPath();
32
32
 
33
+ await ai.prime();
33
34
  if (!ai.isEnabled()) {
34
35
  console.error(chalk.red('AI is required for `gent docs`.'));
35
36
  console.log(chalk.yellow(ai.disabledHint()));
@@ -45,6 +46,7 @@ async function docs(options = {}) {
45
46
  let draft;
46
47
  try {
47
48
  draft = await ai.complete({
49
+ profile: 'docs',
48
50
  system:
49
51
  'You write clear, accurate, well-formatted README.md files. ' +
50
52
  'Use plain GitHub-flavored Markdown. Do not invent features that are not ' +
@@ -2,7 +2,7 @@
2
2
  * Doctor Command - Health check for the gent CLI.
3
3
  *
4
4
  * gent doctor → run all checks
5
- * gent doctor --ai → also ping Anthropic with a 1-token request
5
+ * gent doctor --ai → also ping the Gent AI service
6
6
  *
7
7
  * Each row prints PASS / WARN / FAIL plus a hint on how to fix the issue.
8
8
  */
@@ -131,37 +131,37 @@ async function checkApi() {
131
131
  }
132
132
 
133
133
  async function checkAiKey(probe) {
134
- const { value: key, source } = await ai.resolveKey();
135
- if (!key) {
134
+ const { value: available, source } = await ai.resolveKey();
135
+ if (!available) {
136
136
  return {
137
- name: 'AI key',
137
+ name: 'AI service',
138
138
  status: 'warn',
139
- detail: 'not configured (AI features will be skipped, not failed)',
140
- hint: 'Run `gent config set ai.api_key <key>` or set ANTHROPIC_API_KEY.',
139
+ detail: 'unavailable in this CLI installation',
140
+ hint: 'Ask the Gent distributor to provision the local AI credential.',
141
141
  };
142
142
  }
143
143
 
144
144
  if (!probe) {
145
145
  return {
146
- name: 'AI key',
146
+ name: 'AI service',
147
147
  status: 'pass',
148
- detail: `present [${source}], model: ${await ai.resolveModel()} (use --ai to live-test)`,
148
+ detail: `direct OpenAI [${source}], model: ${await ai.resolveModel()} (use --ai to live-test)`,
149
149
  };
150
150
  }
151
151
 
152
152
  try {
153
153
  await ai.complete({ prompt: 'ping', maxTokens: 4 });
154
154
  return {
155
- name: 'AI key',
155
+ name: 'AI service',
156
156
  status: 'pass',
157
- detail: `verified — model ${await ai.resolveModel()} responded`,
157
+ detail: 'verified — OpenAI responded',
158
158
  };
159
159
  } catch (err) {
160
160
  return {
161
- name: 'AI key',
161
+ name: 'AI service',
162
162
  status: 'fail',
163
163
  detail: err.message,
164
- hint: 'Re-check the key (`gent config set ai.api_key`) or model (`gent config set ai.model`).',
164
+ hint: 'Retry later or ask the Gent distributor to check AI quota and credentials.',
165
165
  };
166
166
  }
167
167
  }
@@ -4,9 +4,8 @@
4
4
  * ============================================================================
5
5
  *
6
6
  * PURPOSE:
7
- * Turn a diff into a human explanation. With an API key set this uses Claude;
8
- * without one it still prints the unified diff plus a hint, so the command is
9
- * useful either way.
7
+ * Turn a diff into a human explanation. AI-enabled installations get a fast
8
+ * review; other installations still get the unified diff plus a hint.
10
9
  *
11
10
  * USAGE:
12
11
  * gent explain → explain the latest commit (HEAD)
@@ -116,6 +115,7 @@ async function explain(ref, options = {}) {
116
115
 
117
116
  console.log(chalk.bold.cyan(`\n${title}\n`));
118
117
 
118
+ await ai.prime();
119
119
  if (!ai.isEnabled()) {
120
120
  console.log(trimmed);
121
121
  console.log(chalk.gray(`\n${ai.disabledHint()}`));