@renxqoo/renx-code 0.0.7 → 0.0.9

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Renx
2
2
 
3
- `renx` is a terminal AI coding assistant.
3
+ `renx` is a terminal AI coding assistant for reading code, editing files, fixing errors, and working directly inside your current project.
4
4
 
5
5
  - npm package: `@renxqoo/renx-code`
6
6
  - command: `renx`
@@ -23,24 +23,24 @@ Start:
23
23
  renx
24
24
  ```
25
25
 
26
- No separate Bun installation is required for published npm installs.
26
+ ## Quick Start
27
27
 
28
- ## Basic Usage
29
-
30
- Run `renx` inside any project directory:
28
+ Open a project directory and run:
31
29
 
32
30
  ```bash
31
+ cd your-project
33
32
  renx
34
33
  ```
35
34
 
36
35
  `renx` uses the current terminal directory as the workspace by default.
37
36
 
38
- Common things you can do:
37
+ Common things you can ask it to do:
39
38
 
40
- - read and explain code
41
- - modify files
42
- - investigate errors
43
- - work inside the current project directory
39
+ - explain a codebase or a single file
40
+ - fix build or runtime errors
41
+ - modify files directly
42
+ - help write features, tests, and scripts
43
+ - investigate logs and terminal output
44
44
 
45
45
  Built-in commands:
46
46
 
@@ -71,37 +71,19 @@ Default user data locations:
71
71
  - `RENX_HOME/task/`
72
72
  - `RENX_HOME/data.db`
73
73
 
74
- ## Environment Variables
75
-
76
- Runtime:
77
-
78
- - `RENX_HOME`
79
- - `AGENT_MODEL`
80
- - `AGENT_MAX_STEPS`
81
- - `AGENT_MAX_RETRY_COUNT`
82
- - `AGENT_TOOL_CONFIRMATION_MODE`
83
- - `AGENT_CONVERSATION_ID`
84
- - `AGENT_SESSION_ID`
85
-
86
- Logging:
87
-
88
- - `AGENT_LOG_LEVEL`
89
- - `AGENT_LOG_FORMAT`
90
- - `AGENT_LOG_CONSOLE`
91
- - `AGENT_LOG_FILE_ENABLED`
92
-
93
- File history:
74
+ Project config example:
94
75
 
95
- - `AGENT_FILE_HISTORY_ENABLED`
96
- - `AGENT_FILE_HISTORY_MAX_PER_FILE`
97
- - `AGENT_FILE_HISTORY_MAX_AGE_DAYS`
98
- - `AGENT_FILE_HISTORY_MAX_TOTAL_MB`
76
+ ```text
77
+ your-project/.renx/config.json
78
+ ```
99
79
 
100
- Provider API keys are still passed through their own environment variables, for example:
80
+ Global config example:
101
81
 
102
- - `GLM_API_KEY`
82
+ ```text
83
+ ~/.renx/config.json
84
+ ```
103
85
 
104
- ## Config Example
86
+ ## Basic Config Example
105
87
 
106
88
  ```json
107
89
  {
@@ -127,14 +109,92 @@ Provider API keys are still passed through their own environment variables, for
127
109
  }
128
110
  ```
129
111
 
130
- Project config example:
112
+ ## Custom Model Config
131
113
 
132
- ```text
133
- your-project/.renx/config.json
114
+ You can define your own model in global or project `config.json` and then point `agent.defaultModel` to it.
115
+
116
+ Example:
117
+
118
+ ```json
119
+ {
120
+ "agent": {
121
+ "defaultModel": "my-openai-compatible"
122
+ },
123
+ "models": {
124
+ "my-openai-compatible": {
125
+ "provider": "openai",
126
+ "name": "My OpenAI Compatible",
127
+ "baseURL": "https://example.com/v1",
128
+ "endpointPath": "/chat/completions",
129
+ "envApiKey": "MY_API_KEY",
130
+ "envBaseURL": "MY_API_BASE",
131
+ "model": "my-model",
132
+ "max_tokens": 8000,
133
+ "LLMMAX_TOKENS": 128000,
134
+ "features": ["streaming", "function-calling"]
135
+ },
136
+ "gpt-5.4": {
137
+ "baseURL": "https://my-proxy.example.com/v1"
138
+ }
139
+ }
140
+ }
134
141
  ```
135
142
 
136
- Global config example:
143
+ This supports both:
137
144
 
138
- ```text
139
- ~/.renx/config.json
145
+ - adding a completely new model
146
+ - overriding part of a built-in model, such as `baseURL`
147
+
148
+ ## Environment Variables
149
+
150
+ Runtime:
151
+
152
+ - `RENX_HOME`
153
+ - `AGENT_MODEL`
154
+ - `AGENT_MAX_STEPS`
155
+ - `AGENT_MAX_RETRY_COUNT`
156
+ - `AGENT_TOOL_CONFIRMATION_MODE`
157
+ - `AGENT_CONVERSATION_ID`
158
+ - `AGENT_SESSION_ID`
159
+
160
+ Logging:
161
+
162
+ - `AGENT_LOG_LEVEL`
163
+ - `AGENT_LOG_FORMAT`
164
+ - `AGENT_LOG_CONSOLE`
165
+ - `AGENT_LOG_FILE_ENABLED`
166
+
167
+ File history:
168
+
169
+ - `AGENT_FILE_HISTORY_ENABLED`
170
+ - `AGENT_FILE_HISTORY_MAX_PER_FILE`
171
+ - `AGENT_FILE_HISTORY_MAX_AGE_DAYS`
172
+ - `AGENT_FILE_HISTORY_MAX_TOTAL_MB`
173
+
174
+ Custom models:
175
+
176
+ - `RENX_CUSTOM_MODELS_JSON`
177
+
178
+ Provider API keys are passed through their own environment variables, for example:
179
+
180
+ - `OPENAI_API_KEY`
181
+ - `GLM_API_KEY`
182
+ - `QWEN_API_KEY`
183
+
184
+ ## Publish
185
+
186
+ Useful local release commands:
187
+
188
+ ```bash
189
+ npm run pack:dry
190
+ npm run pack:tgz
191
+ npm run publish:patch
192
+ npm run publish:minor
193
+ npm run publish:major
140
194
  ```
195
+
196
+ Version rules:
197
+
198
+ - `publish:patch`: small fix, `0.0.1`
199
+ - `publish:minor`: new feature, `0.1.0`
200
+ - `publish:major`: breaking change or refactor, `1.0.0`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renxqoo/renx-code",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "module": "src/index.tsx",
5
5
  "type": "module",
6
6
  "private": false,
@@ -25,6 +25,7 @@ describe('loadConfigToEnv', () => {
25
25
  delete process.env.AGENT_TOOL_CONFIRMATION_MODE;
26
26
  delete process.env.AGENT_MODEL;
27
27
  delete process.env.AGENT_MAX_STEPS;
28
+ delete process.env.RENX_CUSTOM_MODELS_JSON;
28
29
  });
29
30
 
30
31
  afterEach(() => {
@@ -126,4 +127,112 @@ describe('loadConfigToEnv', () => {
126
127
  expect(process.env.AGENT_MODEL).toBe('qwen3.5-max');
127
128
  expect(process.env.AGENT_MAX_STEPS).toBe('100');
128
129
  });
130
+
131
+ it('should merge custom models into RENX_CUSTOM_MODELS_JSON', () => {
132
+ fs.mkdirSync(globalDir, { recursive: true });
133
+ fs.writeFileSync(
134
+ path.join(globalDir, 'config.json'),
135
+ JSON.stringify({
136
+ models: {
137
+ 'shared-model': {
138
+ provider: 'openai',
139
+ name: 'Shared Model',
140
+ baseURL: 'https://global.example.com/v1',
141
+ endpointPath: '/chat/completions',
142
+ envApiKey: 'SHARED_API_KEY',
143
+ envBaseURL: 'SHARED_API_BASE',
144
+ model: 'shared-global',
145
+ max_tokens: 4096,
146
+ LLMMAX_TOKENS: 64000,
147
+ features: ['streaming'],
148
+ },
149
+ },
150
+ })
151
+ );
152
+
153
+ const projectConfigDir = path.join(tmpDir, '.renx');
154
+ fs.mkdirSync(projectConfigDir, { recursive: true });
155
+ fs.writeFileSync(
156
+ path.join(projectConfigDir, 'config.json'),
157
+ JSON.stringify({
158
+ models: {
159
+ 'shared-model': {
160
+ baseURL: 'https://project.example.com/v1',
161
+ model: 'shared-project',
162
+ },
163
+ 'project-model': {
164
+ provider: 'openai',
165
+ name: 'Project Model',
166
+ baseURL: 'https://project-only.example.com/v1',
167
+ endpointPath: '/responses',
168
+ envApiKey: 'PROJECT_API_KEY',
169
+ envBaseURL: 'PROJECT_API_BASE',
170
+ model: 'project-model',
171
+ max_tokens: 8000,
172
+ LLMMAX_TOKENS: 128000,
173
+ features: ['streaming', 'function-calling'],
174
+ },
175
+ },
176
+ })
177
+ );
178
+
179
+ loadConfigToEnv({ projectRoot: tmpDir, globalDir });
180
+
181
+ const models = JSON.parse(process.env.RENX_CUSTOM_MODELS_JSON ?? '{}') as Record<
182
+ string,
183
+ Record<string, unknown>
184
+ >;
185
+
186
+ expect(models['shared-model']).toMatchObject({
187
+ provider: 'openai',
188
+ baseURL: 'https://project.example.com/v1',
189
+ model: 'shared-project',
190
+ });
191
+ expect(models['project-model']).toMatchObject({
192
+ endpointPath: '/responses',
193
+ });
194
+ });
195
+
196
+ it('should keep existing RENX_CUSTOM_MODELS_JSON values over config files', () => {
197
+ process.env.RENX_CUSTOM_MODELS_JSON = JSON.stringify({
198
+ 'shared-model': {
199
+ baseURL: 'https://env.example.com/v1',
200
+ model: 'env-model',
201
+ },
202
+ });
203
+
204
+ fs.mkdirSync(globalDir, { recursive: true });
205
+ fs.writeFileSync(
206
+ path.join(globalDir, 'config.json'),
207
+ JSON.stringify({
208
+ models: {
209
+ 'shared-model': {
210
+ provider: 'openai',
211
+ name: 'Shared Model',
212
+ baseURL: 'https://global.example.com/v1',
213
+ endpointPath: '/chat/completions',
214
+ envApiKey: 'SHARED_API_KEY',
215
+ envBaseURL: 'SHARED_API_BASE',
216
+ model: 'shared-global',
217
+ max_tokens: 4096,
218
+ LLMMAX_TOKENS: 64000,
219
+ features: ['streaming'],
220
+ },
221
+ },
222
+ })
223
+ );
224
+
225
+ loadConfigToEnv({ projectRoot: tmpDir, globalDir });
226
+
227
+ const models = JSON.parse(process.env.RENX_CUSTOM_MODELS_JSON ?? '{}') as Record<
228
+ string,
229
+ Record<string, unknown>
230
+ >;
231
+ expect(models['shared-model']).toMatchObject({
232
+ baseURL: 'https://env.example.com/v1',
233
+ model: 'env-model',
234
+ provider: 'openai',
235
+ envApiKey: 'SHARED_API_KEY',
236
+ });
237
+ });
129
238
  });
@@ -144,6 +144,74 @@ describe('Renx Config Loader', () => {
144
144
  expect(config.sources.global).toBe(path.join(globalDir, 'config.json'));
145
145
  expect(config.sources.project).toBe(path.join(projectConfigDir, 'config.json'));
146
146
  });
147
+
148
+ it('should merge custom models from global and project config', () => {
149
+ fs.mkdirSync(globalDir, { recursive: true });
150
+ fs.writeFileSync(
151
+ path.join(globalDir, 'config.json'),
152
+ JSON.stringify({
153
+ models: {
154
+ 'shared-model': {
155
+ provider: 'openai',
156
+ name: 'Shared Model',
157
+ baseURL: 'https://global.example.com/v1',
158
+ endpointPath: '/chat/completions',
159
+ envApiKey: 'SHARED_API_KEY',
160
+ envBaseURL: 'SHARED_API_BASE',
161
+ model: 'shared-global',
162
+ max_tokens: 4096,
163
+ LLMMAX_TOKENS: 32000,
164
+ features: ['streaming'],
165
+ },
166
+ },
167
+ })
168
+ );
169
+
170
+ const projectConfigDir = path.join(tmpDir, '.renx');
171
+ fs.mkdirSync(projectConfigDir, { recursive: true });
172
+ fs.writeFileSync(
173
+ path.join(projectConfigDir, 'config.json'),
174
+ JSON.stringify({
175
+ models: {
176
+ 'shared-model': {
177
+ baseURL: 'https://project.example.com/v1',
178
+ model: 'shared-project',
179
+ },
180
+ 'project-model': {
181
+ provider: 'openai',
182
+ name: 'Project Model',
183
+ baseURL: 'https://project-only.example.com/v1',
184
+ endpointPath: '/responses',
185
+ envApiKey: 'PROJECT_API_KEY',
186
+ envBaseURL: 'PROJECT_API_BASE',
187
+ model: 'project-model',
188
+ max_tokens: 8000,
189
+ LLMMAX_TOKENS: 128000,
190
+ features: ['streaming', 'function-calling'],
191
+ },
192
+ },
193
+ })
194
+ );
195
+
196
+ const config = loadConfig({
197
+ projectRoot: tmpDir,
198
+ globalDir,
199
+ loadEnv: false,
200
+ env: { RENX_HOME: renxHome },
201
+ });
202
+
203
+ expect(config.models['shared-model']).toMatchObject({
204
+ provider: 'openai',
205
+ name: 'Shared Model',
206
+ baseURL: 'https://project.example.com/v1',
207
+ model: 'shared-project',
208
+ envApiKey: 'SHARED_API_KEY',
209
+ });
210
+ expect(config.models['project-model']).toMatchObject({
211
+ provider: 'openai',
212
+ endpointPath: '/responses',
213
+ });
214
+ });
147
215
  });
148
216
 
149
217
  describe('loadConfig with env overrides', () => {
@@ -190,6 +258,52 @@ describe('Renx Config Loader', () => {
190
258
  expect(config.log.format).toBe('json');
191
259
  expect(config.agent.confirmationMode).toBe('auto-deny');
192
260
  });
261
+
262
+ it('should let RENX_CUSTOM_MODELS_JSON override file-based model config', () => {
263
+ const projectConfigDir = path.join(tmpDir, '.renx');
264
+ fs.mkdirSync(projectConfigDir, { recursive: true });
265
+ fs.writeFileSync(
266
+ path.join(projectConfigDir, 'config.json'),
267
+ JSON.stringify({
268
+ models: {
269
+ 'custom-openai': {
270
+ provider: 'openai',
271
+ name: 'Custom OpenAI',
272
+ baseURL: 'https://file.example.com/v1',
273
+ endpointPath: '/chat/completions',
274
+ envApiKey: 'CUSTOM_API_KEY',
275
+ envBaseURL: 'CUSTOM_API_BASE',
276
+ model: 'file-model',
277
+ max_tokens: 4096,
278
+ LLMMAX_TOKENS: 64000,
279
+ features: ['streaming'],
280
+ },
281
+ },
282
+ })
283
+ );
284
+
285
+ const config = loadConfig({
286
+ projectRoot: tmpDir,
287
+ globalDir,
288
+ env: {
289
+ RENX_HOME: renxHome,
290
+ RENX_CUSTOM_MODELS_JSON: JSON.stringify({
291
+ 'custom-openai': {
292
+ baseURL: 'https://env.example.com/v1',
293
+ model: 'env-model',
294
+ features: ['streaming', 'function-calling'],
295
+ },
296
+ }),
297
+ },
298
+ });
299
+
300
+ expect(config.models['custom-openai']).toMatchObject({
301
+ provider: 'openai',
302
+ baseURL: 'https://env.example.com/v1',
303
+ model: 'env-model',
304
+ features: ['streaming', 'function-calling'],
305
+ });
306
+ });
193
307
  });
194
308
 
195
309
  describe('writeProjectConfig', () => {
@@ -19,6 +19,7 @@ export type {
19
19
  StorageConfig,
20
20
  FileHistoryConfig,
21
21
  AgentConfig,
22
+ ConfigModelDefinition,
22
23
  } from './types';
23
24
 
24
25
  // Config loaders
@@ -7,10 +7,17 @@ import {
7
7
  resolveRenxLogsDir,
8
8
  resolveRenxStorageRoot,
9
9
  } from './paths';
10
- import type { LoadConfigOptions, LogConfig, RenxConfig, ResolvedConfig } from './types';
10
+ import type {
11
+ ConfigModelDefinition,
12
+ LoadConfigOptions,
13
+ LogConfig,
14
+ RenxConfig,
15
+ ResolvedConfig,
16
+ } from './types';
11
17
 
12
18
  const PROJECT_DIR_NAME = '.renx';
13
19
  const CONFIG_FILENAME = 'config.json';
20
+ const CUSTOM_MODELS_ENV_VAR = 'RENX_CUSTOM_MODELS_JSON';
14
21
 
15
22
  const DEFAULTS: RenxConfig = {
16
23
  log: {
@@ -57,6 +64,37 @@ function readJsonFile<T>(filePath: string): T | null {
57
64
  }
58
65
  }
59
66
 
67
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ }
70
+
71
+ function parseCustomModelsEnv(
72
+ raw: string | undefined
73
+ ): Record<string, ConfigModelDefinition> | null {
74
+ if (!raw) {
75
+ return null;
76
+ }
77
+
78
+ try {
79
+ const parsed = JSON.parse(raw) as unknown;
80
+ if (!isPlainObject(parsed)) {
81
+ return null;
82
+ }
83
+
84
+ const result: Record<string, ConfigModelDefinition> = {};
85
+ for (const [modelId, modelConfig] of Object.entries(parsed)) {
86
+ if (!isPlainObject(modelConfig)) {
87
+ continue;
88
+ }
89
+ result[modelId] = modelConfig as ConfigModelDefinition;
90
+ }
91
+
92
+ return result;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
60
98
  function parseLogLevelValue(raw: string | undefined): LogLevel | null {
61
99
  if (!raw) {
62
100
  return null;
@@ -242,6 +280,11 @@ function applyEnvOverrides(config: RenxConfig, env: NodeJS.ProcessEnv): RenxConf
242
280
  }
243
281
  }
244
282
 
283
+ const customModels = parseCustomModelsEnv(env[CUSTOM_MODELS_ENV_VAR]);
284
+ if (customModels) {
285
+ result.models = deepMerge(result.models ?? {}, customModels);
286
+ }
287
+
245
288
  return result;
246
289
  }
247
290
 
@@ -281,6 +324,7 @@ function resolveConfig(
281
324
  confirmationMode: merged.agent?.confirmationMode ?? 'manual',
282
325
  defaultModel: merged.agent?.defaultModel ?? 'qwen3.5-plus',
283
326
  },
327
+ models: merged.models ?? {},
284
328
  sources,
285
329
  };
286
330
  }
@@ -324,25 +368,30 @@ export function loadConfigToEnv(options: LoadConfigOptions = {}): string[] {
324
368
  const loadedFiles: string[] = [];
325
369
 
326
370
  const protectedEnvKeys = new Set(Object.keys(process.env));
371
+ const protectedCustomModels = parseCustomModelsEnv(process.env[CUSTOM_MODELS_ENV_VAR]) ?? {};
327
372
 
328
373
  const globalConfigPath = ensureGlobalConfigFile(globalDir);
329
374
  const globalConfig = readJsonFile<RenxConfig>(globalConfigPath);
330
375
  if (globalConfig) {
331
- applyConfigToEnv(globalConfig, protectedEnvKeys);
376
+ applyConfigToEnv(globalConfig, protectedEnvKeys, protectedCustomModels);
332
377
  loadedFiles.push(globalConfigPath);
333
378
  }
334
379
 
335
380
  const projectConfigPath = path.join(projectRoot, PROJECT_DIR_NAME, CONFIG_FILENAME);
336
381
  const projectConfig = readJsonFile<RenxConfig>(projectConfigPath);
337
382
  if (projectConfig) {
338
- applyConfigToEnv(projectConfig, protectedEnvKeys);
383
+ applyConfigToEnv(projectConfig, protectedEnvKeys, protectedCustomModels);
339
384
  loadedFiles.push(projectConfigPath);
340
385
  }
341
386
 
342
387
  return loadedFiles;
343
388
  }
344
389
 
345
- function applyConfigToEnv(config: RenxConfig, protectedEnvKeys: Set<string>): void {
390
+ function applyConfigToEnv(
391
+ config: RenxConfig,
392
+ protectedEnvKeys: Set<string>,
393
+ protectedCustomModels: Record<string, ConfigModelDefinition>
394
+ ): void {
346
395
  const setIfUnset = (key: string, value: string | undefined) => {
347
396
  if (value !== undefined && !protectedEnvKeys.has(key)) {
348
397
  process.env[key] = value;
@@ -397,6 +446,20 @@ function applyConfigToEnv(config: RenxConfig, protectedEnvKeys: Set<string>): vo
397
446
  config.agent.maxSteps !== undefined ? String(config.agent.maxSteps) : undefined
398
447
  );
399
448
  }
449
+
450
+ if (config.models && Object.keys(config.models).length > 0) {
451
+ const existingModels = parseCustomModelsEnv(process.env[CUSTOM_MODELS_ENV_VAR]) ?? {};
452
+ const mergedModels = deepMerge(existingModels, config.models);
453
+
454
+ if (protectedEnvKeys.has(CUSTOM_MODELS_ENV_VAR)) {
455
+ process.env[CUSTOM_MODELS_ENV_VAR] = JSON.stringify(
456
+ deepMerge(mergedModels, protectedCustomModels)
457
+ );
458
+ return;
459
+ }
460
+
461
+ process.env[CUSTOM_MODELS_ENV_VAR] = JSON.stringify(mergedModels);
462
+ }
400
463
  }
401
464
 
402
465
  export function getGlobalConfigDir(): string {
@@ -1,4 +1,5 @@
1
1
  import type { LogLevel } from '../logger';
2
+ import type { ProviderType } from '../providers/types';
2
3
 
3
4
  export interface LogConfig {
4
5
  level?: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
@@ -24,10 +25,34 @@ export interface AgentConfig {
24
25
  defaultModel?: string;
25
26
  }
26
27
 
28
+ export interface ConfigModelDefinition {
29
+ provider?: ProviderType;
30
+ name?: string;
31
+ endpointPath?: string;
32
+ envApiKey?: string;
33
+ envBaseURL?: string;
34
+ baseURL?: string;
35
+ model?: string;
36
+ max_tokens?: number;
37
+ LLMMAX_TOKENS?: number;
38
+ features?: string[];
39
+ modalities?: {
40
+ image?: boolean;
41
+ audio?: boolean;
42
+ video?: boolean;
43
+ };
44
+ temperature?: number;
45
+ tool_stream?: boolean;
46
+ thinking?: boolean;
47
+ timeout?: number;
48
+ model_reasoning_effort?: 'low' | 'medium' | 'high';
49
+ }
50
+
27
51
  export interface RenxConfig {
28
52
  log?: LogConfig;
29
53
  storage?: StorageConfig;
30
54
  agent?: AgentConfig;
55
+ models?: Record<string, ConfigModelDefinition>;
31
56
  }
32
57
 
33
58
  export interface ResolvedConfig {
@@ -56,6 +81,7 @@ export interface ResolvedConfig {
56
81
  confirmationMode: 'manual' | 'auto-approve' | 'auto-deny';
57
82
  defaultModel: string;
58
83
  };
84
+ models: Record<string, ConfigModelDefinition>;
59
85
  sources: {
60
86
  global: string | null;
61
87
  project: string | null;