gent-cli 24.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 +6 -5
- package/README.md +7 -6
- package/package.json +3 -3
- package/src/commands/ai.js +15 -30
- package/src/commands/ask.js +4 -1
- package/src/commands/canonical.js +73 -14
- package/src/commands/changelog.js +2 -0
- package/src/commands/chat.js +80 -0
- package/src/commands/commit.js +1 -0
- package/src/commands/config.js +1 -16
- package/src/commands/docs.js +2 -0
- package/src/commands/doctor.js +12 -12
- package/src/commands/explain.js +3 -3
- package/src/commands/merge.js +25 -3
- package/src/commands/resolve.js +30 -6
- package/src/commands/review.js +4 -3
- package/src/commands/setup.js +2 -52
- package/src/commands/summary.js +5 -1
- package/src/index.js +20 -11
- package/src/utils/ai-service.js +86 -163
- package/src/utils/user-config.js +3 -33
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
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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": "
|
|
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 tests/repos.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 tests/repos.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",
|
package/src/commands/ai.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Command - Manage and verify AI integration.
|
|
3
3
|
*
|
|
4
|
-
* gent ai status → show
|
|
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 →
|
|
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:
|
|
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 (
|
|
40
|
-
console.log(` ${chalk.green('●')}
|
|
30
|
+
if (available) {
|
|
31
|
+
console.log(` ${chalk.green('●')} Service: ${chalk.white(`direct OpenAI [${source}]`)}`);
|
|
41
32
|
} else {
|
|
42
|
-
console.log(` ${chalk.gray('○')}
|
|
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: ${
|
|
46
|
-
console.log(chalk.gray('\n Run `gent ai test` to verify the
|
|
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:
|
|
52
|
-
if (!
|
|
53
|
-
console.error(chalk.red(
|
|
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
|
|
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
|
-
|
|
74
|
-
console.log(chalk.
|
|
75
|
-
for
|
|
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;
|
package/src/commands/ask.js
CHANGED
|
@@ -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
|
|
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;
|
|
@@ -156,16 +156,29 @@ const handlers = {
|
|
|
156
156
|
async merge(repo, branch, options) {
|
|
157
157
|
if (options.abort) return merge.abortMerge(repo);
|
|
158
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;
|
|
159
164
|
const result = await merge.merge(repo, branch, options);
|
|
160
165
|
if (result.status === 'conflicts') {
|
|
161
166
|
for (const conflict of result.conflicts) {
|
|
162
167
|
if (conflict.kind === 'content') console.log(`Auto-merging ${conflict.path}`);
|
|
163
168
|
console.log(`CONFLICT (${conflict.kind}): Merge conflict in ${conflict.path}`);
|
|
164
169
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
182
|
},
|
|
170
183
|
async resolve(repo, options = {}) {
|
|
171
184
|
const index = await GitIndex.read(repo.indexPath);
|
|
@@ -174,6 +187,18 @@ const handlers = {
|
|
|
174
187
|
console.log('No merge conflicts to resolve.');
|
|
175
188
|
return;
|
|
176
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
|
+
}
|
|
177
202
|
if (!options.ai) {
|
|
178
203
|
for (const name of names) console.log(name);
|
|
179
204
|
console.log('Edit conflicted files, then gent add <path> and gent merge --continue.');
|
|
@@ -197,14 +222,6 @@ const handlers = {
|
|
|
197
222
|
theirs: sides.theirs?.toString('utf8') || '',
|
|
198
223
|
fileName: name,
|
|
199
224
|
});
|
|
200
|
-
console.log(`\nAI suggestion for ${name} (review before accepting):\n${suggestion}`);
|
|
201
|
-
const { accept } = await inquirer.prompt([{
|
|
202
|
-
type: 'confirm',
|
|
203
|
-
name: 'accept',
|
|
204
|
-
message: `Apply and stage this resolution for ${name}?`,
|
|
205
|
-
default: false,
|
|
206
|
-
}]);
|
|
207
|
-
if (!accept) continue;
|
|
208
225
|
worktree.assertSafeCheckoutPath(repo, name);
|
|
209
226
|
await worktree.assertNoSymlinkParent(repo, name);
|
|
210
227
|
const absolute = path.join(repo.worktree, name);
|
|
@@ -217,8 +234,15 @@ const handlers = {
|
|
|
217
234
|
console.log(`AI did not resolve ${name}: ${error.message}`);
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
|
-
console.log(`${resolved} of ${names.length} conflict(s) resolved with
|
|
221
|
-
|
|
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);
|
|
222
246
|
},
|
|
223
247
|
async stash(repo, sub = 'push', options = {}) {
|
|
224
248
|
const position = Number(options.index || 0);
|
|
@@ -280,6 +304,41 @@ const handlers = {
|
|
|
280
304
|
}
|
|
281
305
|
};
|
|
282
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
|
+
|
|
283
342
|
async function printTreeDiff(repo, before, after, files = [], stat = false) {
|
|
284
343
|
const wanted = files.map(name => repo.relativePath(path.resolve(name)));
|
|
285
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;
|
package/src/commands/commit.js
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
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]) {
|
package/src/commands/docs.js
CHANGED
|
@@ -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 ' +
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
|
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:
|
|
135
|
-
if (!
|
|
134
|
+
const { value: available, source } = await ai.resolveKey();
|
|
135
|
+
if (!available) {
|
|
136
136
|
return {
|
|
137
|
-
name: 'AI
|
|
137
|
+
name: 'AI service',
|
|
138
138
|
status: 'warn',
|
|
139
|
-
detail: '
|
|
140
|
-
hint: '
|
|
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
|
|
146
|
+
name: 'AI service',
|
|
147
147
|
status: 'pass',
|
|
148
|
-
detail: `
|
|
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
|
|
155
|
+
name: 'AI service',
|
|
156
156
|
status: 'pass',
|
|
157
|
-
detail:
|
|
157
|
+
detail: 'verified — OpenAI responded',
|
|
158
158
|
};
|
|
159
159
|
} catch (err) {
|
|
160
160
|
return {
|
|
161
|
-
name: 'AI
|
|
161
|
+
name: 'AI service',
|
|
162
162
|
status: 'fail',
|
|
163
163
|
detail: err.message,
|
|
164
|
-
hint: '
|
|
164
|
+
hint: 'Retry later or ask the Gent distributor to check AI quota and credentials.',
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
}
|
package/src/commands/explain.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* ============================================================================
|
|
5
5
|
*
|
|
6
6
|
* PURPOSE:
|
|
7
|
-
* Turn a diff into a human explanation.
|
|
8
|
-
*
|
|
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()}`));
|
package/src/commands/merge.js
CHANGED
|
@@ -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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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) {
|