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.
@@ -15,6 +15,8 @@ const { findMergeBase, mergeTreeEntries, autoMerge } = require('../utils/merge-e
15
15
  const { storeTree, readBlob, hashBlob } = require('../utils/hash-engine');
16
16
  const pet = require('./pet');
17
17
  const journal = require('../utils/journal');
18
+ const ai = require('../utils/ai-service');
19
+ const reviewCommand = require('./review');
18
20
 
19
21
  /**
20
22
  * Merge a branch into the current branch
@@ -25,6 +27,11 @@ async function merge(sourceBranch, options) {
25
27
  const spinner = ora(`Merging '${sourceBranch}'...`).start();
26
28
 
27
29
  try {
30
+ if (options.ai) {
31
+ await ai.prime();
32
+ if (!ai.isEnabled()) throw new Error(ai.disabledHint());
33
+ }
34
+
28
35
  const gentPath = await getGentPath();
29
36
  const cwd = path.dirname(gentPath);
30
37
  const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
@@ -95,6 +102,10 @@ async function merge(sourceBranch, options) {
95
102
 
96
103
  spinner.succeed(chalk.green(`Fast-forward merge: ${currentBranch} → ${theirsHash.substring(0, 7)}`));
97
104
  await pet.celebrate('merge');
105
+ if (options.ai) {
106
+ console.log(chalk.bold.cyan('\nAI review of the completed merge'));
107
+ await reviewCommand(theirsHash, { head: true });
108
+ }
98
109
  return;
99
110
  }
100
111
 
@@ -134,8 +145,10 @@ async function merge(sourceBranch, options) {
134
145
  }
135
146
  }
136
147
 
137
- console.log(chalk.yellow(`\nConflict markers: <<<<<<< HEAD / ======= / >>>>>>> ${sourceBranch}`));
138
- console.log(chalk.cyan('Resolve conflicts, then run "gent add" and "gent commit"'));
148
+ if (!options.ai) {
149
+ console.log(chalk.yellow(`\nConflict markers: <<<<<<< HEAD / ======= / >>>>>>> ${sourceBranch}`));
150
+ console.log(chalk.cyan('Resolve conflicts, then run "gent resolve"'));
151
+ }
139
152
  }
140
153
 
141
154
  // Store merged tree
@@ -199,6 +212,10 @@ async function merge(sourceBranch, options) {
199
212
  console.log(chalk.gray(` Ours: ${oursHash.substring(0, 7)} Theirs: ${theirsHash.substring(0, 7)}`));
200
213
  console.log(chalk.green(` ${autoResolved} file(s) merged automatically`));
201
214
  await pet.celebrate('merge');
215
+ if (options.ai) {
216
+ console.log(chalk.bold.cyan('\nAI review of the completed merge'));
217
+ await reviewCommand(mergeCommit.hash, { head: true });
218
+ }
202
219
  } else {
203
220
  // Stage the merge state for manual resolution
204
221
  const staging = await readJSON(path.join(gentPath, STAGING_FILE));
@@ -212,7 +229,12 @@ async function merge(sourceBranch, options) {
212
229
  conflicts: mergeResult.conflicts
213
230
  };
214
231
  await writeJSON(path.join(gentPath, STAGING_FILE), staging);
215
- process.exitCode = 1;
232
+ if (options.ai) {
233
+ process.exitCode = 0;
234
+ await require('./resolve')({ ai: true });
235
+ } else {
236
+ process.exitCode = 1;
237
+ }
216
238
  }
217
239
 
218
240
  } catch (error) {
@@ -5,25 +5,50 @@
5
5
  const chalk = require('chalk');
6
6
  const ora = require('ora');
7
7
  const path = require('path');
8
- const { API_ENDPOINTS } = require('../utils/constants');
8
+ const inquirer = require('inquirer');
9
+ const { API_ENDPOINTS, GENT_DIR, CONFIG_FILE } = require('../utils/constants');
9
10
  const apiClient = require('../utils/api-client');
10
11
  const authStorage = require('../utils/auth-storage');
12
+ const { pathExists, readJSON, writeJSON } = require('../utils/fileSystem');
13
+ const interactive = require('../utils/interactive');
14
+ const initCommand = require('./init');
15
+ const repository = require('../utils/repository');
11
16
 
12
17
  /**
13
18
  * List or create remote repositories
14
19
  * @param {Object} options
15
20
  */
16
- async function repos(options) {
21
+ async function repos(extraNames, options) {
17
22
  try {
23
+ options = options || {};
24
+ extraNames = extraNames || [];
25
+ let local = null;
26
+
27
+ if (options.create) {
28
+ const validation = validateRepositoryName(options.create, extraNames);
29
+ if (!validation.valid) {
30
+ console.error(chalk.red(validation.message));
31
+ console.log(chalk.yellow(`Use "gent repos --create ${validation.suggestion}" instead.`));
32
+ console.log(chalk.gray('No repository was created.'));
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ local = await prepareLocalRepository(options);
37
+ if (!local) return;
38
+ } else if (extraNames.length) {
39
+ throw new Error('unexpected repository name; use --create <name>');
40
+ }
41
+
18
42
  const isAuth = await authStorage.isAuthenticated();
19
43
  if (!isAuth) {
20
44
  console.error(chalk.red('Not authenticated'));
21
45
  console.log(chalk.yellow('Run "gent login" first'));
46
+ process.exitCode = 1;
22
47
  return;
23
48
  }
24
49
 
25
50
  if (options.create) {
26
- await createRepo(options);
51
+ await createRepo(options, local);
27
52
  return;
28
53
  }
29
54
 
@@ -73,7 +98,7 @@ async function listRepos() {
73
98
  /**
74
99
  * Create a new remote repository
75
100
  */
76
- async function createRepo(options) {
101
+ async function createRepo(options, local) {
77
102
  const name = options.create;
78
103
  if (typeof name !== 'string' || !name) {
79
104
  console.error(chalk.red('Usage: gent repos --create <name>'));
@@ -91,13 +116,93 @@ async function createRepo(options) {
91
116
  if (options.defaultBranch) {
92
117
  payload.default_branch = options.defaultBranch;
93
118
  }
119
+ if (local.kind === 'canonical') payload.object_format = 'sha256';
94
120
 
95
- const data = await apiClient.post(API_ENDPOINTS.REPOS_CREATE, payload);
96
- const repo = data.repository || data;
121
+ let data;
122
+ try {
123
+ data = await apiClient.post(API_ENDPOINTS.REPOS_CREATE, payload);
124
+ } catch (error) {
125
+ spinner.stop();
126
+ throw error;
127
+ }
128
+ const remoteRepo = data.repository || data;
129
+ const remoteUrl = await linkOrigin(local, remoteRepo);
130
+
131
+ spinner.succeed(chalk.green(`Created repository '${remoteRepo.name}'`));
132
+ console.log(chalk.gray(` URL: /api/repos/${remoteRepo.owner_id}/${remoteRepo.name}`));
133
+ console.log(chalk.green(` Linked local repository to '${remoteUrl}' as origin`));
134
+ }
135
+
136
+ function validateRepositoryName(name, extraNames = []) {
137
+ const words = [name, ...extraNames].filter(value => typeof value === 'string' && value.trim());
138
+ const combined = words.join(' ').trim();
139
+ const suggestion = combined
140
+ .toLowerCase()
141
+ .replace(/[^a-z0-9._-]+/g, '-')
142
+ .replace(/-+/g, '-')
143
+ .replace(/^[-.]+|[-.]+$/g, '') || 'repository-name';
144
+ const valid = words.length === 1 && /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(name);
145
+ return {
146
+ valid,
147
+ suggestion,
148
+ message: `Invalid repository name '${combined || String(name || '')}'. Repository names cannot contain spaces or unsupported characters.`,
149
+ };
150
+ }
151
+
152
+ async function prepareLocalRepository(options) {
153
+ const gentPath = path.join(process.cwd(), GENT_DIR);
154
+ if (!await pathExists(gentPath)) {
155
+ if (!options.yes) {
156
+ if (!interactive.isInteractive()) {
157
+ console.error(chalk.red('No local Gent repository exists in this folder.'));
158
+ console.log(chalk.yellow('Run "gent init" first, or retry with --yes to initialize and link it automatically.'));
159
+ process.exitCode = 1;
160
+ return null;
161
+ }
162
+ const { initialize } = await inquirer.prompt([{
163
+ type: 'confirm',
164
+ name: 'initialize',
165
+ message: 'No local Gent repository exists in this folder. Create it before creating and linking the remote?',
166
+ default: true,
167
+ }]);
168
+ if (!initialize) {
169
+ console.log(chalk.yellow('Cancelled. No local or remote repository was created.'));
170
+ return null;
171
+ }
172
+ }
173
+ await initCommand({ yes: true });
174
+ }
175
+
176
+ const legacyConfig = path.join(gentPath, CONFIG_FILE);
177
+ if (await pathExists(legacyConfig)) {
178
+ const config = await readJSON(legacyConfig);
179
+ if (config.remotes?.origin) throw new Error("remote 'origin' already exists; no repository was created");
180
+ return { kind: 'legacy', config, configPath: legacyConfig };
181
+ }
182
+
183
+ const canonical = await repository.open(process.cwd());
184
+ if (canonical.config.get('remote.origin.url')) throw new Error("remote 'origin' already exists; no repository was created");
185
+ return { kind: 'canonical', repo: canonical };
186
+ }
187
+
188
+ async function linkOrigin(local, remoteRepo) {
189
+ if (local.kind === 'legacy') {
190
+ const url = `/api/repos/${remoteRepo.owner_id}/${remoteRepo.name}`;
191
+ local.config.remotes = local.config.remotes || {};
192
+ local.config.remotes.origin = { url };
193
+ await writeJSON(local.configPath, local.config);
194
+ return url;
195
+ }
97
196
 
98
- spinner.succeed(chalk.green(`Created repository '${repo.name}'`));
99
- console.log(chalk.gray(` URL: /api/repos/${repo.owner_id}/${repo.name}`));
100
- console.log(chalk.gray(` Use "gent remote add origin /api/repos/${repo.owner_id}/${repo.name}" to link`));
197
+ const base = (await apiClient.resolveBaseUrl()).replace(/\/$/, '');
198
+ const owner = remoteRepo.owner_username || remoteRepo.owner_id;
199
+ const url = `${base}/${encodeURIComponent(String(owner))}/${encodeURIComponent(remoteRepo.name)}.git`;
200
+ local.repo.localConfig.set('remote.origin.url', url);
201
+ local.repo.localConfig.set('remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*');
202
+ await local.repo.localConfig.save();
203
+ return url;
101
204
  }
102
205
 
103
206
  module.exports = repos;
207
+ module.exports.validateRepositoryName = validateRepositoryName;
208
+ module.exports.prepareLocalRepository = prepareLocalRepository;
@@ -31,9 +31,17 @@ const { generateCommitHash } = require('../utils/helpers');
31
31
  const authStorage = require('../utils/auth-storage');
32
32
  const journal = require('../utils/journal');
33
33
  const ai = require('../utils/ai-service');
34
+ const reviewCommand = require('./review');
34
35
 
35
- async function resolve() {
36
+ async function resolve(options = {}) {
36
37
  try {
38
+ await ai.prime();
39
+ if (options.ai && !ai.isEnabled()) {
40
+ console.error(chalk.red(ai.disabledHint()));
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+
37
45
  const gentPath = await getGentPath();
38
46
  const cwd = process.cwd();
39
47
 
@@ -57,6 +65,19 @@ async function resolve() {
57
65
  return;
58
66
  }
59
67
 
68
+ if (!options.ai && ai.isEnabled() && process.stdin.isTTY && process.stdout.isTTY) {
69
+ const { mode } = await inquirer.prompt([{
70
+ type: 'list',
71
+ name: 'mode',
72
+ message: 'How should Gent resolve this merge?',
73
+ choices: [
74
+ { name: 'Merge with AI (fast) — resolve, commit, then review', value: 'ai' },
75
+ { name: 'Resolve manually — choose each conflict', value: 'manual' },
76
+ ],
77
+ }]);
78
+ options.ai = mode === 'ai';
79
+ }
80
+
60
81
  console.log(chalk.bold.cyan(`\nResolving merge of '${mergeState.sourceBranch}' — ${markerFiles.length} file(s)\n`));
61
82
 
62
83
  // Working copy of merged tree entries (we patch hashes as files resolve).
@@ -92,7 +113,7 @@ async function resolve() {
92
113
  continue;
93
114
  }
94
115
  idx++;
95
- const resolvedLines = await resolveHunk(seg, file, idx, conflictCount);
116
+ const resolvedLines = await resolveHunk(seg, file, idx, conflictCount, options);
96
117
  if (resolvedLines === null) { aborted = true; break; }
97
118
  out.push(...resolvedLines);
98
119
  }
@@ -119,23 +140,28 @@ async function resolve() {
119
140
 
120
141
  if (unresolvedFiles > 0) {
121
142
  console.log(chalk.yellow(`\n${unresolvedFiles} file(s) still have conflicts. Re-run "gent resolve" when ready.`));
143
+ process.exitCode = 1;
122
144
  return;
123
145
  }
124
146
 
125
147
  // All conflicts resolved — offer to finalize the merge commit.
126
- const { finalize } = await inquirer.prompt([{
148
+ const finalize = options.ai || (await inquirer.prompt([{
127
149
  type: 'confirm',
128
150
  name: 'finalize',
129
151
  message: 'All conflicts resolved. Create the merge commit now?',
130
152
  default: true
131
- }]);
153
+ }])).finalize;
132
154
 
133
155
  if (!finalize) {
134
156
  console.log(chalk.cyan('Resolved files staged. Run "gent commit" when ready.'));
135
157
  return;
136
158
  }
137
159
 
138
- await finalizeMerge(gentPath, staging, mergeState, entriesByName);
160
+ const mergeCommit = await finalizeMerge(gentPath, staging, mergeState, entriesByName);
161
+ if (options.ai) {
162
+ console.log(chalk.bold.cyan('\nAI review of the completed merge'));
163
+ await reviewCommand(mergeCommit.hash, { head: true });
164
+ }
139
165
  } catch (error) {
140
166
  if (error.code === 'ENOENT' && error.message.includes('.gent')) {
141
167
  console.error(chalk.red('Error: Not a gent repository'));
@@ -151,7 +177,7 @@ async function resolve() {
151
177
  * Prompt for one conflict hunk. Returns the chosen lines, or null to abort
152
178
  * (leave the rest of the file as-is with markers).
153
179
  */
154
- async function resolveHunk(seg, file, idx, total) {
180
+ async function resolveHunk(seg, file, idx, total, options = {}) {
155
181
  console.log(chalk.gray(` Conflict ${idx}/${total}:`));
156
182
  console.log(chalk.green(' <<< ours'));
157
183
  seg.ours.forEach(l => console.log(chalk.green(` ${l}`)));
@@ -169,6 +195,12 @@ async function resolveHunk(seg, file, idx, total) {
169
195
  }
170
196
  choices.push({ name: 'Skip the rest of this file', value: 'skip' });
171
197
 
198
+ if (options.ai) {
199
+ const suggestion = await askAiForHunk(seg, file, true);
200
+ if (suggestion !== null) return suggestion;
201
+ return null;
202
+ }
203
+
172
204
  const { choice } = await inquirer.prompt([{
173
205
  type: 'list',
174
206
  name: 'choice',
@@ -191,28 +223,40 @@ async function resolveHunk(seg, file, idx, total) {
191
223
  return text.replace(/\n$/, '').split('\n');
192
224
  }
193
225
  case 'ai': {
194
- try {
195
- const suggestion = await ai.resolveConflictHunk({
196
- ours: seg.ours.join('\n'),
197
- theirs: seg.theirs.join('\n'),
198
- fileName: file
199
- });
200
- console.log(chalk.cyan(' AI suggestion:'));
201
- suggestion.split('\n').forEach(l => console.log(chalk.cyan(` ${l}`)));
202
- const { accept } = await inquirer.prompt([{
203
- type: 'confirm', name: 'accept', message: 'Use this suggestion?', default: true
204
- }]);
205
- if (accept) return suggestion.split('\n');
206
- return resolveHunk(seg, file, idx, total); // re-ask
207
- } catch (err) {
208
- console.log(chalk.yellow(` AI failed (${err.message}); choose another option.`));
209
- return resolveHunk(seg, file, idx, total);
210
- }
226
+ const suggestion = await askAiForHunk(seg, file);
227
+ if (suggestion !== null) return suggestion;
228
+ return resolveHunk(seg, file, idx, total, { ai: false });
211
229
  }
212
230
  default: return seg.ours;
213
231
  }
214
232
  }
215
233
 
234
+ async function askAiForHunk(seg, file, autoAccept = false) {
235
+ try {
236
+ const suggestion = await ai.resolveConflictHunk({
237
+ ours: seg.ours.join('\n'),
238
+ theirs: seg.theirs.join('\n'),
239
+ fileName: file
240
+ });
241
+ if (autoAccept) {
242
+ console.log(chalk.green(` ✓ AI resolved ${file}`));
243
+ return suggestion.split('\n');
244
+ }
245
+ console.log(chalk.cyan(' AI suggestion (review before accepting):'));
246
+ suggestion.split('\n').forEach(line => console.log(chalk.cyan(` ${line}`)));
247
+ const { accept } = await inquirer.prompt([{
248
+ type: 'confirm',
249
+ name: 'accept',
250
+ message: 'Use this AI suggestion?',
251
+ default: false,
252
+ }]);
253
+ return accept ? suggestion.split('\n') : null;
254
+ } catch (error) {
255
+ console.log(chalk.yellow(` AI failed (${error.message}); no file was changed.`));
256
+ return null;
257
+ }
258
+ }
259
+
216
260
  /** Store the resolved file as a blob, patch the tree entry, and stage it. */
217
261
  async function stageResolved(gentPath, staging, entriesByName, file, content) {
218
262
  const hash = await storeBlob(gentPath, content);
@@ -276,6 +320,7 @@ async function finalizeMerge(gentPath, staging, mergeState, entriesByName) {
276
320
  await writeJSON(path.join(gentPath, STAGING_FILE), staging);
277
321
 
278
322
  console.log(chalk.green(`\n✓ Merge committed — ${mergeCommit.hash.substring(0, 7)}`));
323
+ return mergeCommit;
279
324
  }
280
325
 
281
326
  module.exports = resolve;
@@ -7,7 +7,7 @@
7
7
  * gent review <ref> → review diff for that commit
8
8
  *
9
9
  * Output: prioritized bug/risk list followed by smaller polish suggestions.
10
- * Without an AI key, prints the raw diff so the command still has value.
10
+ * Without local AI, prints the raw diff so the command still has value.
11
11
  */
12
12
 
13
13
  const path = require('path');
@@ -73,6 +73,7 @@ async function review(refArg, options = {}) {
73
73
 
74
74
  console.log(chalk.bold.cyan(`\n${title}\n`));
75
75
 
76
+ await ai.prime();
76
77
  if (!ai.isEnabled()) {
77
78
  console.log(trimmed);
78
79
  console.log(chalk.gray(`\n${ai.disabledHint()}`));
@@ -82,6 +83,7 @@ async function review(refArg, options = {}) {
82
83
  const spinner = ora(`Reviewing with ${ai.getModel()}...`).start();
83
84
  try {
84
85
  const out = await ai.complete({
86
+ profile: 'review',
85
87
  system:
86
88
  'You are a senior code reviewer. Given a unified diff, list concrete ' +
87
89
  'issues you would block on, then smaller suggestions. Format:\n' +
@@ -90,8 +92,7 @@ async function review(refArg, options = {}) {
90
92
  '🟢 Looks good\n - one-line positive note\n' +
91
93
  'Be specific. If nothing is wrong, say so plainly.',
92
94
  prompt: `Review this diff:\n\n${trimmed}`,
93
- maxTokens: 1500,
94
- thinking: true,
95
+ maxTokens: 800,
95
96
  });
96
97
  spinner.stop();
97
98
  console.log(out + '\n');
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * gent setup
5
5
  *
6
- * Walks the user through: backend URL → login/register → AI key → identity.
6
+ * Walks the user through: backend URL → login/register → identity.
7
7
  * Each step is skippable; nothing is required.
8
8
  */
9
9
 
@@ -15,7 +15,6 @@ const axios = require('axios');
15
15
  const userConfig = require('../utils/user-config');
16
16
  const authStorage = require('../utils/auth-storage');
17
17
  const authService = require('../services/auth-service');
18
- const ai = require('../utils/ai-service');
19
18
 
20
19
  async function setup() {
21
20
  console.log(boxen(
@@ -26,7 +25,6 @@ async function setup() {
26
25
 
27
26
  await stepBackend();
28
27
  await stepAuth();
29
- await stepAiKey();
30
28
  await stepIdentity();
31
29
 
32
30
  console.log(chalk.green('\n✓ Setup complete!'));
@@ -125,56 +123,8 @@ async function stepAuth() {
125
123
  }
126
124
  }
127
125
 
128
- async function stepAiKey() {
129
- console.log(chalk.bold('\n3. AI features (optional)'));
130
- const existing = await ai.resolveKey();
131
- if (existing.value) {
132
- console.log(chalk.gray(` Key already configured [${existing.source}] — skipping.`));
133
- return;
134
- }
135
-
136
- console.log(chalk.gray(' Gent uses Anthropic Claude for commit-message suggestions,'));
137
- console.log(chalk.gray(' diff explanations, AI conflict resolution, code review, and more.'));
138
- console.log(chalk.gray(' Get a key at: https://console.anthropic.com/settings/keys'));
139
-
140
- const { provide } = await inquirer.prompt([{
141
- type: 'confirm',
142
- name: 'provide',
143
- message: 'Add an Anthropic API key now?',
144
- default: true,
145
- }]);
146
- if (!provide) return;
147
-
148
- const { key } = await inquirer.prompt([{
149
- type: 'password',
150
- name: 'key',
151
- message: 'Anthropic API key:',
152
- mask: '*',
153
- validate: (v) => v.length > 0 || 'Cannot be empty',
154
- }]);
155
-
156
- await userConfig.set('ai.api_key', key);
157
-
158
- const { testNow } = await inquirer.prompt([{
159
- type: 'confirm',
160
- name: 'testNow',
161
- message: 'Test the key now (1 small request)?',
162
- default: true,
163
- }]);
164
- if (testNow) {
165
- const spinner = ora('Asking Claude to say hi...').start();
166
- try {
167
- await ai.complete({ prompt: 'Reply with the single word: ok', maxTokens: 4 });
168
- spinner.succeed(chalk.green('AI key works'));
169
- } catch (err) {
170
- spinner.fail(chalk.red(err.message));
171
- console.log(chalk.yellow(' You can fix this with `gent config set ai.api_key <key>`.'));
172
- }
173
- }
174
- }
175
-
176
126
  async function stepIdentity() {
177
- console.log(chalk.bold('\n4. Default identity'));
127
+ console.log(chalk.bold('\n3. Default identity'));
178
128
  const currentName = await userConfig.getResolved('user.name');
179
129
  const currentEmail = await userConfig.getResolved('user.email');
180
130
 
@@ -150,12 +150,16 @@ async function summary(options = {}) {
150
150
  }));
151
151
 
152
152
  if (options.ai) {
153
+ await ai.prime();
153
154
  if (!ai.isEnabled()) {
154
155
  console.log(chalk.gray(ai.disabledHint()));
155
156
  } else {
156
157
  try {
157
158
  const facts = lines.join('\n').replace(/\[[0-9;]*m/g, ''); // strip colors
158
- const narrative = await ai.explainChanges(`Repository stats:\n${facts}\n\nGive a 2-3 sentence health assessment.`);
159
+ const narrative = await ai.explainChanges(
160
+ `Repository stats:\n${facts}\n\nGive a 2-3 sentence health assessment.`,
161
+ 'summary',
162
+ );
159
163
  console.log(chalk.cyan(narrative));
160
164
  } catch (err) {
161
165
  console.log(chalk.yellow(`AI summary failed: ${err.message}`));
package/src/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * History: commit, log, show, tag, explain
14
14
  * Branching: branch, checkout, merge, resolve, stash
15
15
  * Safety: undo, redo
16
- * Insight: summary, ask, review, docs, changelog
16
+ * Insight: summary, ask, chat, review, docs, changelog
17
17
  * Remote: remote, repos, members, push, pull, search, web, share
18
18
  * Auth: register, login, logout, whoami, password
19
19
  * AI: ai (status|test|models)
@@ -72,6 +72,7 @@ const doctorCommand = require('./commands/doctor');
72
72
  const setupCommand = require('./commands/setup');
73
73
  const aiCommand = require('./commands/ai');
74
74
  const askCommand = route('ask', require('./commands/ask'));
75
+ const chatCommand = require('./commands/chat');
75
76
  const reviewCommand = route('review', require('./commands/review'));
76
77
  const docsCommand = route('docs', require('./commands/docs'));
77
78
  const changelogCommand = route('changelog', require('./commands/changelog'));
@@ -188,7 +189,7 @@ program
188
189
  .description('Record changes to the repository')
189
190
  .option('-m, --message <message>', 'Commit message')
190
191
  .option('-a, --all', 'Automatically stage all modified files')
191
- .option('--ai', 'Suggest a commit message with AI (needs ANTHROPIC_API_KEY)')
192
+ .option('--ai', 'Suggest a commit message with local Gent AI')
192
193
  .action(commitCommand);
193
194
 
194
195
  program
@@ -222,7 +223,7 @@ program
222
223
  program
223
224
  .command('summary')
224
225
  .description('Show a repository health & statistics dashboard')
225
- .option('--ai', 'Add an AI-written health narrative (needs ANTHROPIC_API_KEY)')
226
+ .option('--ai', 'Add an AI-written health narrative')
226
227
  .action(summaryCommand);
227
228
 
228
229
  // ─── Branching & Merging ────────────────────────────────
@@ -258,6 +259,7 @@ program
258
259
  .command('merge [branch]')
259
260
  .description('Merge a branch into the current branch (3-way smart merge)')
260
261
  .option('-m, --message <message>', 'Merge commit message')
262
+ .option('--ai', 'Resolve conflicts with AI, commit, then review the merge')
261
263
  .option('--continue', 'Finish a resolved canonical merge')
262
264
  .option('--abort', 'Abort a canonical merge')
263
265
  .action(async (branch, options) => {
@@ -284,6 +286,7 @@ program
284
286
  program
285
287
  .command('resolve')
286
288
  .description('Interactively resolve merge conflicts left by "gent merge"')
289
+ .option('--ai', 'Resolve every text conflict with AI, commit, then review')
287
290
  .action(resolveCommand);
288
291
 
289
292
  program
@@ -316,12 +319,13 @@ program
316
319
  .action(remoteCommand);
317
320
 
318
321
  program
319
- .command('repos')
322
+ .command('repos [names...]')
320
323
  .description('List or create remote repositories')
321
324
  .option('--create <name>', 'Create a new remote repository')
322
325
  .option('--description <text>', 'Repository description (with --create)')
323
326
  .option('--private', 'Make repository private (with --create)')
324
327
  .option('--default-branch <name>', 'Default branch name (with --create)')
328
+ .option('-y, --yes', 'Initialize the current folder without prompting')
325
329
  .action(reposCommand);
326
330
 
327
331
  program
@@ -350,18 +354,18 @@ program
350
354
 
351
355
  program
352
356
  .command('setup')
353
- .description('Interactive first-run wizard (backend URL, login, AI key, identity)')
357
+ .description('Interactive first-run wizard (backend URL, login, identity)')
354
358
  .action(setupCommand);
355
359
 
356
360
  program
357
361
  .command('config [subcommand] [args...]')
358
- .description('Manage CLI settings (list|get|set|unset|path) — e.g. gent config set ai.api_key <key>')
362
+ .description('Manage CLI settings (list|get|set|unset|path)')
359
363
  .action(configCommand);
360
364
 
361
365
  program
362
366
  .command('doctor')
363
- .description('Run a health check across node, repo, auth, backend, and AI key')
364
- .option('--ai', 'Also live-test the AI key with a tiny request')
367
+ .description('Run a health check across node, repo, auth, backend, and Gent AI')
368
+ .option('--ai', 'Also live-test Gent AI with a tiny request')
365
369
  .action(doctorCommand);
366
370
 
367
371
  program
@@ -373,7 +377,7 @@ program
373
377
 
374
378
  program
375
379
  .command('ask [question]')
376
- .description('Ask Claude a question about this repo (needs AI key)')
380
+ .description('Ask local Gent AI a question about this repo')
377
381
  .action(async (question, options) => {
378
382
  if (!question && interactive.isInteractive()) {
379
383
  question = await interactive.promptAsk();
@@ -388,6 +392,11 @@ program
388
392
  .option('--head', 'Force review of HEAD commit')
389
393
  .action(reviewCommand);
390
394
 
395
+ program
396
+ .command('chat [message]')
397
+ .description('Chat with Gent AI about the current repository')
398
+ .action(chatCommand);
399
+
391
400
  program
392
401
  .command('docs')
393
402
  .description('Generate a README.md draft for this repo using AI')
@@ -550,7 +559,7 @@ function showQuickstart() {
550
559
  console.log(chalk.gray('A Git-like VCS with cloud sync + AI superpowers.\n'));
551
560
  console.log(chalk.bold('First time? Try:'));
552
561
  console.log(` ${chalk.cyan('gent auto')} ${chalk.gray('guided init → commit → push (interactive)')}`);
553
- console.log(` ${chalk.cyan('gent setup')} ${chalk.gray('configure login + AI key + remote')}`);
562
+ console.log(` ${chalk.cyan('gent setup')} ${chalk.gray('configure login + identity + remote')}`);
554
563
  console.log(` ${chalk.cyan('gent doctor')} ${chalk.gray('check everything is wired up')}`);
555
564
  console.log(` ${chalk.cyan('gent template list')} ${chalk.gray('scaffold a starter project')}`);
556
565
  console.log();
@@ -558,9 +567,11 @@ function showQuickstart() {
558
567
  console.log(` ${chalk.cyan('gent init && gent add -A && gent commit -m "init"')}`);
559
568
  console.log(` ${chalk.cyan('gent push')} / ${chalk.cyan('gent pull')} / ${chalk.cyan('gent merge <branch>')}`);
560
569
  console.log();
561
- console.log(chalk.bold('AI features (need an Anthropic key):'));
570
+ console.log(chalk.bold('Local AI features:'));
571
+ console.log(` ${chalk.cyan('gent chat')} ${chalk.gray('interactive repository chat')}`);
562
572
  console.log(` ${chalk.cyan('gent ask "what does this repo do?"')}`);
563
573
  console.log(` ${chalk.cyan('gent review')} ${chalk.gray('review staged changes')}`);
574
+ console.log(` ${chalk.cyan('gent merge dev --ai')} ${chalk.gray('resolve, commit, then review')}`);
564
575
  console.log(` ${chalk.cyan('gent docs --write')} ${chalk.gray('generate README.md')}`);
565
576
  console.log(` ${chalk.cyan('gent changelog')} ${chalk.gray('grouped release notes')}`);
566
577
  console.log();