gent-cli 7.0.0 → 9.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/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
17
- * Auth: register, login, logout, whoami
16
+ * Insight: summary, ask, review, docs, changelog
17
+ * Remote: remote, repos, members, push, pull, search, web, share
18
+ * Auth: register, login, logout, whoami, password
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');
@@ -44,6 +50,7 @@ const remoteCommand = require('./commands/remote');
44
50
  const pushCommand = require('./commands/push');
45
51
  const pullCommand = require('./commands/pull');
46
52
  const reposCommand = require('./commands/repos');
53
+ const membersCommand = require('./commands/members');
47
54
  const undoCommand = require('./commands/undo');
48
55
  const resolveCommand = require('./commands/resolve');
49
56
  const summaryCommand = require('./commands/summary');
@@ -54,6 +61,21 @@ const registerCommand = require('./commands/register');
54
61
  const loginCommand = require('./commands/login');
55
62
  const logoutCommand = require('./commands/logout');
56
63
  const whoamiCommand = require('./commands/whoami');
64
+ const passwordCommand = require('./commands/password');
65
+
66
+ // Import new gent-platform commands
67
+ const configCommand = require('./commands/config');
68
+ const doctorCommand = require('./commands/doctor');
69
+ const setupCommand = require('./commands/setup');
70
+ const aiCommand = require('./commands/ai');
71
+ const askCommand = require('./commands/ask');
72
+ const reviewCommand = require('./commands/review');
73
+ const docsCommand = require('./commands/docs');
74
+ const changelogCommand = require('./commands/changelog');
75
+ const webCommand = require('./commands/web');
76
+ const shareCommand = require('./commands/share');
77
+ const searchCommand = require('./commands/search');
78
+ const templateCommand = require('./commands/template');
57
79
 
58
80
  // Configure CLI
59
81
  program
@@ -84,10 +106,16 @@ program
84
106
  .action(statusCommand);
85
107
 
86
108
  program
87
- .command('add <files...>')
88
- .description('Add file contents to the staging area')
109
+ .command('add [files...]')
110
+ .description('Add file contents to the staging area (use -A/--all to add everything)')
89
111
  .option('-A, --all', 'Add all files')
90
- .action(addCommand);
112
+ .action((files, options) => {
113
+ if ((!files || files.length === 0) && !options.all) {
114
+ console.error('error: specify files to add, or use -A to add all');
115
+ process.exit(1);
116
+ }
117
+ return addCommand(files || [], options);
118
+ });
91
119
 
92
120
  program
93
121
  .command('rm <files...>')
@@ -215,6 +243,12 @@ program
215
243
  .option('--default-branch <name>', 'Default branch name (with --create)')
216
244
  .action(reposCommand);
217
245
 
246
+ program
247
+ .command('members [action] [email]')
248
+ .description('Manage repo collaborators (list | add <email> | remove <email>)')
249
+ .option('--role <role>', 'Role when adding a member: write or read', 'write')
250
+ .action(membersCommand);
251
+
218
252
  program
219
253
  .command('push [remote] [branch]')
220
254
  .description('Push local commits to remote')
@@ -226,6 +260,83 @@ program
226
260
  .description('Pull and merge remote commits')
227
261
  .action(pullCommand);
228
262
 
263
+ // ─── Setup, Config & Diagnostics ────────────────────────
264
+
265
+ program
266
+ .command('setup')
267
+ .description('Interactive first-run wizard (backend URL, login, AI key, identity)')
268
+ .action(setupCommand);
269
+
270
+ program
271
+ .command('config [subcommand] [args...]')
272
+ .description('Manage CLI settings (list|get|set|unset|path) — e.g. gent config set ai.api_key <key>')
273
+ .action(configCommand);
274
+
275
+ program
276
+ .command('doctor')
277
+ .description('Run a health check across node, repo, auth, backend, and AI key')
278
+ .option('--ai', 'Also live-test the AI key with a tiny request')
279
+ .action(doctorCommand);
280
+
281
+ program
282
+ .command('ai [subcommand]')
283
+ .description('Inspect AI integration (status|test|models)')
284
+ .action(aiCommand);
285
+
286
+ // ─── Platform-special (AI-powered) ──────────────────────
287
+
288
+ program
289
+ .command('ask <question>')
290
+ .description('Ask Claude a question about this repo (needs AI key)')
291
+ .action(askCommand);
292
+
293
+ program
294
+ .command('review [ref]')
295
+ .description('AI code review on staged changes (default), HEAD, or a specific commit')
296
+ .option('--staged', 'Force review of staged changes')
297
+ .option('--head', 'Force review of HEAD commit')
298
+ .action(reviewCommand);
299
+
300
+ program
301
+ .command('docs')
302
+ .description('Generate a README.md draft for this repo using AI')
303
+ .option('--write', 'Write the draft to README.md instead of stdout')
304
+ .option('--section <name>', 'Only generate a single named section')
305
+ .action(docsCommand);
306
+
307
+ program
308
+ .command('changelog [range]')
309
+ .description('Print a changelog. range = <from>..<to> or <from> (default: since last tag)')
310
+ .option('--plain', 'Skip AI grouping — flat commit list')
311
+ .action(changelogCommand);
312
+
313
+ program
314
+ .command('web')
315
+ .description('Open the current repo (or a branch/commit) on the gent web app')
316
+ .option('--branch <name>', 'Open a specific branch')
317
+ .option('--commit <hash>', 'Open a specific commit')
318
+ .option('--print', 'Print the URL instead of launching a browser')
319
+ .action(webCommand);
320
+
321
+ program
322
+ .command('share')
323
+ .description('Print a shareable link to current branch tip (or --branch/--commit)')
324
+ .option('--branch <name>', 'Link to a specific branch')
325
+ .option('--commit <hash>', 'Link to a specific commit')
326
+ .action(shareCommand);
327
+
328
+ program
329
+ .command('search [query]')
330
+ .description('Search your repositories on the gent backend')
331
+ .option('--mine', 'Only repos you own')
332
+ .option('--json', 'Output as JSON')
333
+ .action(searchCommand);
334
+
335
+ program
336
+ .command('template [subcommand] [args...]')
337
+ .description('Quick-start from a baked-in template (list|use <name> [directory])')
338
+ .action(templateCommand);
339
+
229
340
  // ─── Authentication ─────────────────────────────────────
230
341
 
231
342
  program
@@ -255,6 +366,12 @@ program
255
366
  .description('Display current user information')
256
367
  .action(whoamiCommand);
257
368
 
369
+ program
370
+ .command('password [action]')
371
+ .description('Change or reset your password (change | reset [email] | reset-confirm)')
372
+ .option('-e, --email <email>', 'Account email (for reset)')
373
+ .action(passwordCommand);
374
+
258
375
  // Help command
259
376
  program
260
377
  .command('help [command]')
@@ -267,19 +384,57 @@ program
267
384
  }
268
385
  });
269
386
 
387
+ // Friendlier global error mapping. Per-command handlers still own their own
388
+ // errors; this catches anything that bubbles up (e.g. unknown command).
389
+ function explainError(err) {
390
+ if (!err) return '';
391
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
392
+ return `Cannot reach the gent backend. Check the URL with \`gent config get api.base_url\` and try \`gent doctor\`.`;
393
+ }
394
+ if (err.code === 'commander.unknownCommand') {
395
+ return `${err.message}\n\nRun \`gent\` (no args) to see the command list, or \`gent help <command>\` for details.`;
396
+ }
397
+ return err.message;
398
+ }
399
+
400
+ function showQuickstart() {
401
+ console.log();
402
+ console.log(chalk.bold.cyan('Gent CLI ') + chalk.gray(`v${packageJson.version}`));
403
+ console.log(chalk.gray('A Git-like VCS with cloud sync + AI superpowers.\n'));
404
+ console.log(chalk.bold('First time? Try:'));
405
+ console.log(` ${chalk.cyan('gent setup')} ${chalk.gray('interactive walkthrough (login + AI key + remote)')}`);
406
+ console.log(` ${chalk.cyan('gent doctor')} ${chalk.gray('check everything is wired up')}`);
407
+ console.log(` ${chalk.cyan('gent template list')} ${chalk.gray('scaffold a starter project')}`);
408
+ console.log();
409
+ console.log(chalk.bold('Everyday flow:'));
410
+ console.log(` ${chalk.cyan('gent init && gent add -A && gent commit -m "init"')}`);
411
+ console.log(` ${chalk.cyan('gent push')} / ${chalk.cyan('gent pull')} / ${chalk.cyan('gent merge <branch>')}`);
412
+ console.log();
413
+ console.log(chalk.bold('AI features (need an Anthropic key):'));
414
+ console.log(` ${chalk.cyan('gent ask "what does this repo do?"')}`);
415
+ console.log(` ${chalk.cyan('gent review')} ${chalk.gray('review staged changes')}`);
416
+ console.log(` ${chalk.cyan('gent docs --write')} ${chalk.gray('generate README.md')}`);
417
+ console.log(` ${chalk.cyan('gent changelog')} ${chalk.gray('grouped release notes')}`);
418
+ console.log();
419
+ console.log(chalk.gray('Full command list: ') + chalk.cyan('gent --help'));
420
+ console.log();
421
+ }
422
+
270
423
  // Error handling
271
424
  program.exitOverride();
272
425
 
273
426
  try {
274
- program.parse(process.argv);
275
-
276
- // Show help if no command provided
427
+ // Show quickstart if no command provided (instead of raw help).
277
428
  if (!process.argv.slice(2).length) {
278
- program.outputHelp();
429
+ showQuickstart();
430
+ process.exit(0);
279
431
  }
432
+
433
+ program.parse(process.argv);
434
+
280
435
  } catch (err) {
281
436
  if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed' && err.code !== 'commander.version') {
282
- console.error(chalk.red('Error:'), err.message);
437
+ console.error(chalk.red('Error:'), explainError(err));
283
438
  process.exit(1);
284
439
  }
285
440
  }
@@ -138,10 +138,9 @@ async function refreshToken() {
138
138
  refresh: refreshToken
139
139
  });
140
140
 
141
- const { access } = response;
142
-
143
- // Update only access token
144
- await authStorage.updateAccessToken(access);
141
+ // Backend rotates refresh tokens, so persist the new one too.
142
+ const { access, refresh } = response;
143
+ await authStorage.updateTokens(access, refresh);
145
144
 
146
145
  return access;
147
146
  } catch (error) {
@@ -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
  };
@@ -5,11 +5,26 @@
5
5
 
6
6
  const axios = require('axios');
7
7
  const { API_BASE_URL } = require('./constants');
8
+ const userConfig = require('./user-config');
8
9
  const authStorage = require('./auth-storage');
9
10
 
10
- // Create axios instance with base configuration
11
+ // Resolved once per process so commands see a stable URL. CLI runs are short,
12
+ // so we don't bother with cache invalidation — the next invocation re-reads.
13
+ let _resolvedBaseUrl = null;
14
+ async function resolveBaseUrl() {
15
+ if (_resolvedBaseUrl) return _resolvedBaseUrl;
16
+ try {
17
+ const { value } = await userConfig.getResolved('api.base_url');
18
+ _resolvedBaseUrl = value || API_BASE_URL;
19
+ } catch {
20
+ _resolvedBaseUrl = API_BASE_URL;
21
+ }
22
+ return _resolvedBaseUrl;
23
+ }
24
+
25
+ // Create axios instance with base configuration. baseURL is set per-request
26
+ // by the interceptor below so config/env changes take effect immediately.
11
27
  const apiClient = axios.create({
12
- baseURL: API_BASE_URL,
13
28
  headers: {
14
29
  'Content-Type': 'application/json'
15
30
  },
@@ -37,9 +52,13 @@ function processQueue(error, token = null) {
37
52
  failedRequestsQueue = [];
38
53
  }
39
54
 
40
- // Request interceptor - Add JWT token to headers
55
+ // Request interceptor - Resolve base URL + add JWT token to headers
41
56
  apiClient.interceptors.request.use(
42
57
  async (config) => {
58
+ if (!config.baseURL) {
59
+ config.baseURL = await resolveBaseUrl();
60
+ }
61
+
43
62
  const token = await authStorage.getAccessToken();
44
63
 
45
64
  if (token) {
@@ -89,16 +108,19 @@ apiClient.interceptors.response.use(
89
108
  throw new Error('Session expired. Please login again.');
90
109
  }
91
110
 
92
- // Call refresh endpoint
111
+ // Call refresh endpoint (raw axios — bypasses our interceptor
112
+ // intentionally so a 401 here doesn't loop back into refresh).
113
+ const baseUrl = await resolveBaseUrl();
93
114
  const response = await axios.post(
94
- `${API_BASE_URL}/api/auth/token/refresh/`,
115
+ `${baseUrl}/api/auth/token/refresh/`,
95
116
  { refresh: refreshToken }
96
117
  );
97
118
 
98
- const { access } = response.data;
99
-
100
- // Update stored access token
101
- await authStorage.updateAccessToken(access);
119
+ // Backend rotates refresh tokens (ROTATE_REFRESH_TOKENS +
120
+ // BLACKLIST_AFTER_ROTATION); persist the new refresh or the
121
+ // next silent refresh sends a blacklisted token and 401s.
122
+ const { access, refresh } = response.data;
123
+ await authStorage.updateTokens(access, refresh);
102
124
 
103
125
  // Update authorization header
104
126
  originalRequest.headers.Authorization = `Bearer ${access}`;
@@ -188,5 +210,6 @@ module.exports = {
188
210
  put,
189
211
  delete: del,
190
212
  patch,
191
- apiClient // Export raw client if needed
213
+ apiClient, // Export raw client if needed
214
+ resolveBaseUrl,
192
215
  };
@@ -136,10 +136,13 @@ async function clearAuth() {
136
136
  }
137
137
 
138
138
  /**
139
- * Update only the access token (used after refresh)
139
+ * Update stored tokens after a refresh. The backend rotates refresh tokens
140
+ * (ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION), so the new refresh token
141
+ * MUST be persisted or the next refresh sends a blacklisted token and 401s.
140
142
  * @param {string} newAccessToken - New access token
143
+ * @param {string} [newRefreshToken] - New (rotated) refresh token, if returned
141
144
  */
142
- async function updateAccessToken(newAccessToken) {
145
+ async function updateTokens(newAccessToken, newRefreshToken) {
143
146
  const authData = await readAuthData();
144
147
 
145
148
  if (!authData) {
@@ -147,6 +150,9 @@ async function updateAccessToken(newAccessToken) {
147
150
  }
148
151
 
149
152
  authData.accessToken = newAccessToken;
153
+ if (newRefreshToken) {
154
+ authData.refreshToken = newRefreshToken;
155
+ }
150
156
  authData.timestamp = new Date().toISOString();
151
157
 
152
158
  const authFilePath = getAuthFilePath();
@@ -158,6 +164,11 @@ async function updateAccessToken(newAccessToken) {
158
164
  await fs.writeFile(authFilePath, JSON.stringify({ data: encryptedData }), 'utf8');
159
165
  }
160
166
 
167
+ // Back-compat alias: same as updateTokens with no rotated refresh.
168
+ async function updateAccessToken(newAccessToken) {
169
+ return updateTokens(newAccessToken);
170
+ }
171
+
161
172
  module.exports = {
162
173
  saveTokens,
163
174
  getAccessToken,
@@ -165,5 +176,6 @@ module.exports = {
165
176
  getUser,
166
177
  isAuthenticated,
167
178
  clearAuth,
168
- updateAccessToken
179
+ updateAccessToken,
180
+ updateTokens
169
181
  };
@@ -12,7 +12,9 @@ module.exports = {
12
12
  HEAD_FILE: 'HEAD',
13
13
  AUTH_FILE: 'auth.json',
14
14
 
15
- // API Configuration
15
+ // API Configuration — default used when no env/config override.
16
+ // Use getResolvedApiBaseUrl() in code that runs after process boot
17
+ // to respect GENT_API_URL env or user config (~/.gent/cli-config.json).
16
18
  API_BASE_URL: 'https://gent-api.onrender.com',
17
19
  API_ENDPOINTS: {
18
20
  // Auth
@@ -21,6 +23,9 @@ module.exports = {
21
23
  LOGOUT: '/api/auth/logout/',
22
24
  REFRESH: '/api/auth/token/refresh/',
23
25
  PROFILE: '/api/auth/profile/',
26
+ PASSWORD_CHANGE: '/api/auth/password/change/',
27
+ PASSWORD_RESET: '/api/auth/password/reset/',
28
+ PASSWORD_RESET_CONFIRM: '/api/auth/password/reset/confirm/',
24
29
 
25
30
  // Repository management
26
31
  REPOS: '/api/repos/',
@@ -28,9 +33,13 @@ module.exports = {
28
33
  // Template: /api/repos/{owner_id}/{repo_name}/
29
34
  REPO_DETAIL: '/api/repos/{owner_id}/{repo_name}/',
30
35
  REPO_DELETE: '/api/repos/{owner_id}/{repo_name}/delete/',
36
+ REPO_MEMBERS: '/api/repos/{owner_id}/{repo_name}/members/',
37
+ REPO_MEMBER_DETAIL: '/api/repos/{owner_id}/{repo_name}/members/{user_id}/',
31
38
 
32
- // Push
39
+ // Push / Pull / Clone
33
40
  REPO_PUSH: '/api/repos/{owner_id}/{repo_name}/push/',
41
+ REPO_PULL: '/api/repos/{owner_id}/{repo_name}/pull/',
42
+ REPO_CLONE: '/api/repos/{owner_id}/{repo_name}/clone/',
34
43
 
35
44
  // Branches
36
45
  REPO_BRANCHES: '/api/repos/{owner_id}/{repo_name}/branches/',