@wangzhizhi/remi 0.1.224 → 0.1.244
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 +206 -2
- package/dist/help.js +1 -1
- package/dist/initPrompt.js +1 -1
- package/dist/setup.js +5 -4
- package/dist/statusCard.js +1 -1
- package/dist/statusline.js +1 -1
- package/dist/tui/RemiApp.js +55 -7
- package/dist/tui/hooksPanel.js +2 -2
- package/dist/tui/renderers/MessageList.js +10 -7
- package/dist/version.js +1 -1
- package/node_modules/@remi/compact/dist/index.js +13 -5
- package/node_modules/@remi/compact/package.json +1 -1
- package/node_modules/@remi/config/dist/index.js +816 -38
- package/node_modules/@remi/config/package.json +1 -1
- package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
- package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
- package/node_modules/@remi/core/dist/index.js +630 -33
- package/node_modules/@remi/core/dist/stableHash.js +30 -0
- package/node_modules/@remi/core/package.json +1 -1
- package/node_modules/@remi/hooks/package.json +1 -1
- package/node_modules/@remi/llm/package.json +1 -1
- package/node_modules/@remi/mcp/dist/index.js +657 -0
- package/node_modules/@remi/mcp/package.json +8 -0
- package/node_modules/@remi/memory/package.json +1 -1
- package/node_modules/@remi/permissions/package.json +1 -1
- package/node_modules/@remi/sessions/dist/index.js +38 -0
- package/node_modules/@remi/sessions/package.json +1 -1
- package/node_modules/@remi/skills/dist/index.js +2 -2
- package/node_modules/@remi/skills/package.json +1 -1
- package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
- package/node_modules/@remi/terminal-markdown/package.json +1 -1
- package/node_modules/@remi/tools/dist/index.js +1 -1
- package/node_modules/@remi/tools/package.json +1 -1
- package/package.json +14 -12
- package/prompt/tool-use-system.md +19 -3
|
@@ -37,11 +37,38 @@ export const hookEventNames = [
|
|
|
37
37
|
];
|
|
38
38
|
const deepSeekApiKey = '${DEEPSEEK_API_KEY}';
|
|
39
39
|
const defaultAnthropicBaseURL = 'https://api.anthropic.com';
|
|
40
|
-
export function
|
|
40
|
+
export function createDefaultFilesystemMcpServerConfig(platform = process.platform) {
|
|
41
|
+
if (platform === 'win32') {
|
|
42
|
+
return {
|
|
43
|
+
type: 'stdio',
|
|
44
|
+
command: 'cmd',
|
|
45
|
+
args: ['/c', 'npx', '-y', '@modelcontextprotocol/server-filesystem', '.'],
|
|
46
|
+
timeoutMs: 15000,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
41
49
|
return {
|
|
42
|
-
|
|
50
|
+
type: 'stdio',
|
|
51
|
+
command: 'npx',
|
|
52
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
|
|
53
|
+
timeoutMs: 15000,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function createDeepSeekPresetConfig(options = {}) {
|
|
57
|
+
return {
|
|
58
|
+
activeProfile: 'deepseek-v4-flash',
|
|
59
|
+
mcp: {
|
|
60
|
+
servers: {
|
|
61
|
+
filesystem: createDefaultFilesystemMcpServerConfig(options.platform),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
43
64
|
providers: {
|
|
44
|
-
deepseek: {
|
|
65
|
+
'deepseek-v4-flash': {
|
|
66
|
+
type: 'openai-compatible',
|
|
67
|
+
baseURL: 'https://api.deepseek.com',
|
|
68
|
+
apiKey: deepSeekApiKey,
|
|
69
|
+
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
70
|
+
},
|
|
71
|
+
'deepseek-v4-pro': {
|
|
45
72
|
type: 'openai-compatible',
|
|
46
73
|
baseURL: 'https://api.deepseek.com',
|
|
47
74
|
apiKey: deepSeekApiKey,
|
|
@@ -49,8 +76,8 @@ export function createDeepSeekPresetConfig() {
|
|
|
49
76
|
},
|
|
50
77
|
},
|
|
51
78
|
models: {
|
|
52
|
-
'deepseek
|
|
53
|
-
provider: 'deepseek',
|
|
79
|
+
'deepseek-v4-flash': {
|
|
80
|
+
provider: 'deepseek-v4-flash',
|
|
54
81
|
model: 'deepseek-v4-flash',
|
|
55
82
|
displayName: 'DeepSeek-V4-Flash',
|
|
56
83
|
contextWindow: 1_000_000,
|
|
@@ -62,8 +89,8 @@ export function createDeepSeekPresetConfig() {
|
|
|
62
89
|
usageShape: 'openai-compatible',
|
|
63
90
|
cacheUsageShape: 'openai-prompt-token-details',
|
|
64
91
|
},
|
|
65
|
-
'deepseek
|
|
66
|
-
provider: 'deepseek',
|
|
92
|
+
'deepseek-v4-pro': {
|
|
93
|
+
provider: 'deepseek-v4-pro',
|
|
67
94
|
model: 'deepseek-v4-pro',
|
|
68
95
|
displayName: 'DeepSeek-V4-Pro',
|
|
69
96
|
contextWindow: 1_000_000,
|
|
@@ -77,37 +104,30 @@ export function createDeepSeekPresetConfig() {
|
|
|
77
104
|
},
|
|
78
105
|
},
|
|
79
106
|
modelSets: {
|
|
80
|
-
deepseek: ['deepseek
|
|
107
|
+
deepseek: ['deepseek-v4-flash', 'deepseek-v4-pro'],
|
|
81
108
|
},
|
|
82
109
|
profiles: {
|
|
83
|
-
|
|
84
|
-
roles: {
|
|
85
|
-
main: 'deepseek.flash',
|
|
86
|
-
planning: 'deepseek.pro',
|
|
87
|
-
subagent: 'deepseek.flash',
|
|
88
|
-
compact: 'deepseek.flash',
|
|
89
|
-
memory: 'deepseek.flash',
|
|
90
|
-
toolRepair: 'deepseek.flash',
|
|
91
|
-
},
|
|
92
|
-
effort: 'medium',
|
|
93
|
-
},
|
|
94
|
-
pro: {
|
|
95
|
-
extends: 'daily',
|
|
110
|
+
'deepseek-v4-flash': {
|
|
96
111
|
roles: {
|
|
97
|
-
main: 'deepseek
|
|
98
|
-
planning: 'deepseek
|
|
99
|
-
|
|
112
|
+
main: 'deepseek-v4-flash',
|
|
113
|
+
planning: 'deepseek-v4-flash',
|
|
114
|
+
subagent: 'deepseek-v4-flash',
|
|
115
|
+
compact: 'deepseek-v4-flash',
|
|
116
|
+
memory: 'deepseek-v4-flash',
|
|
117
|
+
toolRepair: 'deepseek-v4-flash',
|
|
100
118
|
},
|
|
101
|
-
effort: '
|
|
119
|
+
effort: 'max',
|
|
102
120
|
},
|
|
103
|
-
|
|
104
|
-
extends: 'daily',
|
|
121
|
+
'deepseek-v4-pro': {
|
|
105
122
|
roles: {
|
|
106
|
-
main: 'deepseek
|
|
107
|
-
planning: 'deepseek
|
|
108
|
-
|
|
123
|
+
main: 'deepseek-v4-pro',
|
|
124
|
+
planning: 'deepseek-v4-pro',
|
|
125
|
+
subagent: 'deepseek-v4-pro',
|
|
126
|
+
compact: 'deepseek-v4-pro',
|
|
127
|
+
memory: 'deepseek-v4-pro',
|
|
128
|
+
toolRepair: 'deepseek-v4-pro',
|
|
109
129
|
},
|
|
110
|
-
effort: '
|
|
130
|
+
effort: 'max',
|
|
111
131
|
},
|
|
112
132
|
},
|
|
113
133
|
};
|
|
@@ -172,7 +192,7 @@ export function configPaths(options = {}) {
|
|
|
172
192
|
return sources.map(source => ({ ...source, exists: existsSync(source.path) }));
|
|
173
193
|
}
|
|
174
194
|
export function userConfigPath(homeDir = process.env['REMI_HOME'] ?? homedir()) {
|
|
175
|
-
return join(remiHomeDir(homeDir), 'config.
|
|
195
|
+
return join(remiHomeDir(homeDir), 'config.toml');
|
|
176
196
|
}
|
|
177
197
|
export function remiHomeDir(homeDir = process.env['REMI_HOME'] ?? homedir()) {
|
|
178
198
|
const resolvedHomeDir = resolve(homeDir);
|
|
@@ -193,10 +213,10 @@ export function readUserRemiConfig(homeDir = process.env['REMI_HOME'] ?? homedir
|
|
|
193
213
|
}
|
|
194
214
|
export function writeUserRemiConfig(config, homeDir = process.env['REMI_HOME'] ?? homedir()) {
|
|
195
215
|
mkdirSync(remiHomeDir(homeDir), { recursive: true });
|
|
196
|
-
writeFileSync(userConfigPath(homeDir),
|
|
216
|
+
writeFileSync(userConfigPath(homeDir), serializeRemiConfigToToml(config));
|
|
197
217
|
}
|
|
198
218
|
export function projectConfigPath(cwd = process.cwd(), homeDir = process.env['REMI_HOME'] ?? homedir()) {
|
|
199
|
-
return join(projectStateDir(cwd, homeDir), 'config.
|
|
219
|
+
return join(projectStateDir(cwd, homeDir), 'config.toml');
|
|
200
220
|
}
|
|
201
221
|
export function readProjectRemiConfig(cwd = process.cwd(), homeDir = process.env['REMI_HOME'] ?? homedir()) {
|
|
202
222
|
const path = projectConfigPath(cwd, homeDir);
|
|
@@ -207,7 +227,7 @@ export function readProjectRemiConfig(cwd = process.cwd(), homeDir = process.env
|
|
|
207
227
|
}
|
|
208
228
|
export function writeProjectRemiConfig(config, cwd = process.cwd(), homeDir = process.env['REMI_HOME'] ?? homedir()) {
|
|
209
229
|
mkdirSync(projectStateDir(cwd, homeDir), { recursive: true });
|
|
210
|
-
writeFileSync(projectConfigPath(cwd, homeDir),
|
|
230
|
+
writeFileSync(projectConfigPath(cwd, homeDir), serializeRemiConfigToToml(config));
|
|
211
231
|
}
|
|
212
232
|
export function isPermissionProfile(value) {
|
|
213
233
|
return typeof value === 'string' && permissionProfiles.includes(value);
|
|
@@ -514,11 +534,11 @@ function hasConfigContent(config) {
|
|
|
514
534
|
Object.keys(config.profiles ?? {}).length > 0);
|
|
515
535
|
}
|
|
516
536
|
function parseConfigFile(path) {
|
|
517
|
-
const parsed =
|
|
537
|
+
const parsed = parseToml(readFileSync(path, 'utf8'));
|
|
518
538
|
if (!isRecord(parsed)) {
|
|
519
|
-
throw new Error('Config root must be a
|
|
539
|
+
throw new Error('Config root must be a TOML table');
|
|
520
540
|
}
|
|
521
|
-
return parsed;
|
|
541
|
+
return remiConfigFromToml(parsed);
|
|
522
542
|
}
|
|
523
543
|
function isRecord(value) {
|
|
524
544
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -527,3 +547,761 @@ function uniqueStrings(values) {
|
|
|
527
547
|
const unique = Array.from(new Set(values.map(value => value.trim()).filter(Boolean)));
|
|
528
548
|
return unique.length > 0 ? unique : undefined;
|
|
529
549
|
}
|
|
550
|
+
function remiConfigFromToml(parsed) {
|
|
551
|
+
const config = {};
|
|
552
|
+
assignString(parsed, 'active_profile', value => {
|
|
553
|
+
config.activeProfile = value;
|
|
554
|
+
});
|
|
555
|
+
assignString(parsed, 'language', value => {
|
|
556
|
+
if (languageCodes.includes(value)) {
|
|
557
|
+
config.language = value;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
assignString(parsed, 'response_style', value => {
|
|
561
|
+
config.responseStyle = value;
|
|
562
|
+
});
|
|
563
|
+
assignString(parsed, 'syntax_theme', value => {
|
|
564
|
+
config.syntaxTheme = value;
|
|
565
|
+
});
|
|
566
|
+
assignString(parsed, 'accent_color', value => {
|
|
567
|
+
config.accentColor = value;
|
|
568
|
+
});
|
|
569
|
+
const permissions = parsed['permissions'];
|
|
570
|
+
if (isRecord(permissions)) {
|
|
571
|
+
config.permissions = {
|
|
572
|
+
...(typeof permissions['mode'] === 'string' ? { mode: permissions['mode'] } : {}),
|
|
573
|
+
...(typeof permissions['profile'] === 'string' ? { profile: permissions['profile'] } : {}),
|
|
574
|
+
};
|
|
575
|
+
const allow = parsePermissionAllowRules(permissions['allow']);
|
|
576
|
+
if (allow.length > 0) {
|
|
577
|
+
config.permissions.allow = allow;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const agent = parsed['agent'];
|
|
581
|
+
if (isRecord(agent)) {
|
|
582
|
+
config.agent = numberFields(agent, [
|
|
583
|
+
['max_turns', 'maxTurns'],
|
|
584
|
+
['max_tool_calls', 'maxToolCalls'],
|
|
585
|
+
['auto_compact_threshold_percent', 'autoCompactThresholdPercent'],
|
|
586
|
+
['context_safety_buffer_tokens', 'contextSafetyBufferTokens'],
|
|
587
|
+
['max_output_reserve_tokens', 'maxOutputReserveTokens'],
|
|
588
|
+
]);
|
|
589
|
+
if (typeof agent['auto_compact'] === 'boolean')
|
|
590
|
+
config.agent.autoCompact = agent['auto_compact'];
|
|
591
|
+
if (agent['compact_strategy'] === 'model' || agent['compact_strategy'] === 'deterministic') {
|
|
592
|
+
config.agent.compactStrategy = agent['compact_strategy'];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const statusLine = parsed['status_line'];
|
|
596
|
+
if (isRecord(statusLine) && isStringArray(statusLine['items'])) {
|
|
597
|
+
config.statusLine = { items: statusLine['items'] };
|
|
598
|
+
}
|
|
599
|
+
const skills = parsed['skills'];
|
|
600
|
+
if (isRecord(skills) && isStringArray(skills['disabled'])) {
|
|
601
|
+
config.skills = { disabled: skills['disabled'] };
|
|
602
|
+
}
|
|
603
|
+
const mcp = parseMcpConfig(parsed['mcp']);
|
|
604
|
+
if (mcp !== undefined) {
|
|
605
|
+
config.mcp = mcp;
|
|
606
|
+
}
|
|
607
|
+
const simplifiedProfiles = parseSimpleProfiles(parsed['profiles']);
|
|
608
|
+
if (simplifiedProfiles.length > 0) {
|
|
609
|
+
Object.assign(config, mergeRemiConfig(config, remiConfigFromSimpleProfiles(simplifiedProfiles)));
|
|
610
|
+
}
|
|
611
|
+
const profileRoles = parseProfileRoles(parsed['profile_roles']);
|
|
612
|
+
if (profileRoles.length > 0) {
|
|
613
|
+
Object.assign(config, mergeRemiConfig(config, remiConfigFromProfileRoles(profileRoles)));
|
|
614
|
+
}
|
|
615
|
+
const simplifiedHooks = parseSimpleHooks(parsed['hooks']);
|
|
616
|
+
if (simplifiedHooks.length > 0) {
|
|
617
|
+
const hooks = mergeHooksConfigs(config.hooks, hooksConfigFromSimpleHooks(simplifiedHooks));
|
|
618
|
+
if (hooks !== undefined) {
|
|
619
|
+
config.hooks = hooks;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return config;
|
|
623
|
+
}
|
|
624
|
+
function assignString(record, key, assign) {
|
|
625
|
+
const value = record[key];
|
|
626
|
+
if (typeof value === 'string' && value.trim()) {
|
|
627
|
+
assign(value);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function numberFields(record, fields) {
|
|
631
|
+
const result = {};
|
|
632
|
+
for (const [tomlKey, configKey] of fields) {
|
|
633
|
+
const value = record[tomlKey];
|
|
634
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
635
|
+
result[String(configKey)] = value;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
function parseSimpleProfiles(value) {
|
|
641
|
+
if (!Array.isArray(value)) {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
return value.flatMap(entry => {
|
|
645
|
+
if (!isRecord(entry)) {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
const provider = stringValue(entry['provider']);
|
|
649
|
+
const modelId = stringValue(entry['model_id']);
|
|
650
|
+
const baseUrl = stringValue(entry['base_url']);
|
|
651
|
+
const contextWindow = numberValue(entry['context_window']);
|
|
652
|
+
if (!provider || !modelId || !baseUrl || contextWindow === undefined) {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
const name = stringValue(entry['name']) ?? modelId;
|
|
656
|
+
return [
|
|
657
|
+
{
|
|
658
|
+
name,
|
|
659
|
+
provider: provider,
|
|
660
|
+
modelId,
|
|
661
|
+
baseUrl,
|
|
662
|
+
contextWindow,
|
|
663
|
+
...(stringValue(entry['api_key']) ? { apiKey: stringValue(entry['api_key']) } : {}),
|
|
664
|
+
...(stringValue(entry['api_key_env']) ? { apiKeyEnv: stringValue(entry['api_key_env']) } : {}),
|
|
665
|
+
...(stringValue(entry['display_name']) ? { displayName: stringValue(entry['display_name']) } : {}),
|
|
666
|
+
...(numberValue(entry['max_output_tokens']) !== undefined ? { maxOutputTokens: numberValue(entry['max_output_tokens']) } : {}),
|
|
667
|
+
...(typeof entry['supports_tools'] === 'boolean' ? { supportsTools: entry['supports_tools'] } : {}),
|
|
668
|
+
...(typeof entry['supports_reasoning'] === 'boolean' ? { supportsReasoning: entry['supports_reasoning'] } : {}),
|
|
669
|
+
...(stringValue(entry['effort']) ? { effort: stringValue(entry['effort']) } : {}),
|
|
670
|
+
},
|
|
671
|
+
];
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
function parsePermissionAllowRules(value) {
|
|
675
|
+
if (!Array.isArray(value)) {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
return value.flatMap(entry => {
|
|
679
|
+
if (!isRecord(entry)) {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
const kind = stringValue(entry['kind']);
|
|
683
|
+
const createdAt = stringValue(entry['created_at']);
|
|
684
|
+
let rule;
|
|
685
|
+
if (kind === 'shell-prefix') {
|
|
686
|
+
const prefix = stringValue(entry['prefix']);
|
|
687
|
+
if (!prefix) {
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
690
|
+
const cwd = stringValue(entry['cwd']);
|
|
691
|
+
rule = {
|
|
692
|
+
kind,
|
|
693
|
+
prefix,
|
|
694
|
+
...(cwd ? { cwd } : {}),
|
|
695
|
+
...(createdAt ? { createdAt } : {}),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
else if (kind === 'filesystem-write-root') {
|
|
699
|
+
const root = stringValue(entry['root']);
|
|
700
|
+
if (!root) {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
rule = {
|
|
704
|
+
kind,
|
|
705
|
+
root,
|
|
706
|
+
...(isStringArray(entry['operations']) ? { operations: entry['operations'] } : {}),
|
|
707
|
+
...(createdAt ? { createdAt } : {}),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
else if (kind === 'network-tool') {
|
|
711
|
+
const toolName = stringValue(entry['tool_name']);
|
|
712
|
+
if (!toolName) {
|
|
713
|
+
return [];
|
|
714
|
+
}
|
|
715
|
+
rule = {
|
|
716
|
+
kind,
|
|
717
|
+
toolName,
|
|
718
|
+
...(createdAt ? { createdAt } : {}),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return rule ? [rule] : [];
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
function parseMcpConfig(value) {
|
|
725
|
+
if (!isRecord(value) || !isRecord(value['servers'])) {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
const servers = {};
|
|
729
|
+
for (const [name, entry] of Object.entries(value['servers'])) {
|
|
730
|
+
if (!isRecord(entry)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const command = stringValue(entry['command']);
|
|
734
|
+
if (!command) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const server = {
|
|
738
|
+
command,
|
|
739
|
+
};
|
|
740
|
+
if (entry['type'] === 'stdio')
|
|
741
|
+
server.type = 'stdio';
|
|
742
|
+
if (isStringArray(entry['args']))
|
|
743
|
+
server.args = entry['args'];
|
|
744
|
+
if (typeof entry['cwd'] === 'string')
|
|
745
|
+
server.cwd = entry['cwd'];
|
|
746
|
+
if (typeof entry['enabled'] === 'boolean')
|
|
747
|
+
server.enabled = entry['enabled'];
|
|
748
|
+
if (typeof entry['timeout_ms'] === 'number' && Number.isFinite(entry['timeout_ms'])) {
|
|
749
|
+
server.timeoutMs = entry['timeout_ms'];
|
|
750
|
+
}
|
|
751
|
+
if (isRecord(entry['env'])) {
|
|
752
|
+
const env = Object.fromEntries(Object.entries(entry['env']).filter(([, envValue]) => typeof envValue === 'string'));
|
|
753
|
+
if (Object.keys(env).length > 0) {
|
|
754
|
+
server.env = env;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
servers[name] = server;
|
|
758
|
+
}
|
|
759
|
+
return Object.keys(servers).length > 0 ? { servers } : undefined;
|
|
760
|
+
}
|
|
761
|
+
function remiConfigFromSimpleProfiles(profiles) {
|
|
762
|
+
const providers = {};
|
|
763
|
+
const models = {};
|
|
764
|
+
const modelProfiles = {};
|
|
765
|
+
for (const profile of profiles) {
|
|
766
|
+
const profileName = profile.name ?? profile.modelId;
|
|
767
|
+
providers[profileName] = {
|
|
768
|
+
type: profile.provider,
|
|
769
|
+
baseURL: profile.baseUrl,
|
|
770
|
+
...(profile.apiKey ? { apiKey: profile.apiKey } : {}),
|
|
771
|
+
...(profile.apiKeyEnv ? { apiKeyEnv: profile.apiKeyEnv } : {}),
|
|
772
|
+
};
|
|
773
|
+
const model = {
|
|
774
|
+
provider: profileName,
|
|
775
|
+
model: profile.modelId,
|
|
776
|
+
contextWindow: profile.contextWindow,
|
|
777
|
+
supportsTools: profile.supportsTools ?? true,
|
|
778
|
+
supportsReasoning: profile.supportsReasoning ?? true,
|
|
779
|
+
maxOutputTokens: profile.maxOutputTokens ?? defaultSimpleMaxOutputTokens(profile.modelId),
|
|
780
|
+
tokenEstimator: profile.modelId.toLowerCase().includes('deepseek') ? 'deepseek-local-tokenizer' : 'heuristic',
|
|
781
|
+
tokenEstimateProfile: 'code-heavy',
|
|
782
|
+
usageShape: profile.provider === 'anthropic-compatible' ? 'anthropic-compatible' : 'openai-compatible',
|
|
783
|
+
cacheUsageShape: profile.provider === 'anthropic-compatible' ? 'anthropic-cache-input' : 'openai-prompt-token-details',
|
|
784
|
+
};
|
|
785
|
+
if (profile.displayName) {
|
|
786
|
+
model.displayName = profile.displayName;
|
|
787
|
+
}
|
|
788
|
+
models[profileName] = model;
|
|
789
|
+
modelProfiles[profileName] = {
|
|
790
|
+
roles: modelRoles.reduce((roles, role) => {
|
|
791
|
+
if (role !== 'embedding') {
|
|
792
|
+
roles[role] = profileName;
|
|
793
|
+
}
|
|
794
|
+
return roles;
|
|
795
|
+
}, {}),
|
|
796
|
+
effort: profile.effort ?? 'max',
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
providers,
|
|
801
|
+
models,
|
|
802
|
+
modelSets: { configured: profiles.map(profile => profile.name ?? profile.modelId) },
|
|
803
|
+
profiles: modelProfiles,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function defaultSimpleMaxOutputTokens(modelId) {
|
|
807
|
+
const normalized = modelId.toLowerCase();
|
|
808
|
+
if (normalized.includes('deepseek')) {
|
|
809
|
+
return 384_000;
|
|
810
|
+
}
|
|
811
|
+
if (normalized.includes('mimo')) {
|
|
812
|
+
return 128_000;
|
|
813
|
+
}
|
|
814
|
+
if (normalized.includes('doubao')) {
|
|
815
|
+
return 32_000;
|
|
816
|
+
}
|
|
817
|
+
return 64_000;
|
|
818
|
+
}
|
|
819
|
+
function parseProfileRoles(value) {
|
|
820
|
+
if (!Array.isArray(value)) {
|
|
821
|
+
return [];
|
|
822
|
+
}
|
|
823
|
+
return value.flatMap(entry => {
|
|
824
|
+
if (!isRecord(entry)) {
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
const name = stringValue(entry['name']);
|
|
828
|
+
if (!name) {
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
const roles = {};
|
|
832
|
+
for (const role of modelRoles) {
|
|
833
|
+
const modelAlias = stringValue(entry[role]);
|
|
834
|
+
if (modelAlias) {
|
|
835
|
+
roles[role] = modelAlias;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (Object.keys(roles).length === 0) {
|
|
839
|
+
return [];
|
|
840
|
+
}
|
|
841
|
+
return [
|
|
842
|
+
{
|
|
843
|
+
name,
|
|
844
|
+
roles,
|
|
845
|
+
...(stringValue(entry['extends']) ? { extends: stringValue(entry['extends']) } : {}),
|
|
846
|
+
...(stringValue(entry['effort']) ? { effort: stringValue(entry['effort']) } : {}),
|
|
847
|
+
},
|
|
848
|
+
];
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function remiConfigFromProfileRoles(profileRoles) {
|
|
852
|
+
const profiles = {};
|
|
853
|
+
for (const profile of profileRoles) {
|
|
854
|
+
profiles[profile.name] = {
|
|
855
|
+
roles: profile.roles,
|
|
856
|
+
...(profile.extends ? { extends: profile.extends } : {}),
|
|
857
|
+
...(profile.effort ? { effort: profile.effort } : {}),
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return { profiles };
|
|
861
|
+
}
|
|
862
|
+
function parseSimpleHooks(value) {
|
|
863
|
+
if (!Array.isArray(value)) {
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
return value.flatMap(entry => {
|
|
867
|
+
if (!isRecord(entry)) {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
const event = normalizeHookEventName(stringValue(entry['event']));
|
|
871
|
+
const command = stringValue(entry['command']);
|
|
872
|
+
if (!event || !command) {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
return [
|
|
876
|
+
{
|
|
877
|
+
event,
|
|
878
|
+
command,
|
|
879
|
+
...(stringValue(entry['matcher']) ? { matcher: stringValue(entry['matcher']) } : {}),
|
|
880
|
+
...(numberValue(entry['timeout']) !== undefined ? { timeout: numberValue(entry['timeout']) } : {}),
|
|
881
|
+
...(stringValue(entry['status_message']) ? { statusMessage: stringValue(entry['status_message']) } : {}),
|
|
882
|
+
...(stringValue(entry['if']) ? { if: stringValue(entry['if']) } : {}),
|
|
883
|
+
...(typeof entry['once'] === 'boolean' ? { once: entry['once'] } : {}),
|
|
884
|
+
},
|
|
885
|
+
];
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function hooksConfigFromSimpleHooks(hooks) {
|
|
889
|
+
const result = {};
|
|
890
|
+
for (const hook of hooks) {
|
|
891
|
+
const matcher = hook.matcher ?? '*';
|
|
892
|
+
const existing = result[hook.event] ?? [];
|
|
893
|
+
const group = existing.find(item => (item.matcher ?? '*') === matcher);
|
|
894
|
+
const command = {
|
|
895
|
+
type: 'command',
|
|
896
|
+
command: hook.command,
|
|
897
|
+
...(hook.timeout !== undefined ? { timeout: hook.timeout } : {}),
|
|
898
|
+
...(hook.statusMessage ? { statusMessage: hook.statusMessage } : {}),
|
|
899
|
+
...(hook.if ? { if: hook.if } : {}),
|
|
900
|
+
...(hook.once !== undefined ? { once: hook.once } : {}),
|
|
901
|
+
};
|
|
902
|
+
if (group) {
|
|
903
|
+
group.hooks.push(command);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
existing.push({ matcher, hooks: [command] });
|
|
907
|
+
result[hook.event] = existing;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
function normalizeHookEventName(value) {
|
|
913
|
+
if (!value) {
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
const direct = hookEventNames.find(eventName => eventName === value);
|
|
917
|
+
if (direct) {
|
|
918
|
+
return direct;
|
|
919
|
+
}
|
|
920
|
+
return hookEventNames.find(eventName => hookEventNameToToml(eventName) === value);
|
|
921
|
+
}
|
|
922
|
+
function hookEventNameToToml(eventName) {
|
|
923
|
+
return eventName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
924
|
+
}
|
|
925
|
+
function stringValue(value) {
|
|
926
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
927
|
+
}
|
|
928
|
+
function numberValue(value) {
|
|
929
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
930
|
+
}
|
|
931
|
+
function isStringArray(value) {
|
|
932
|
+
return Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
933
|
+
}
|
|
934
|
+
export function serializeRemiConfigToToml(config) {
|
|
935
|
+
const lines = [];
|
|
936
|
+
writeTomlRoot(lines, 'active_profile', config.activeProfile);
|
|
937
|
+
writeTomlRoot(lines, 'language', config.language);
|
|
938
|
+
writeTomlRoot(lines, 'response_style', config.responseStyle);
|
|
939
|
+
writeTomlRoot(lines, 'syntax_theme', config.syntaxTheme);
|
|
940
|
+
writeTomlRoot(lines, 'accent_color', config.accentColor);
|
|
941
|
+
writeTomlSection(lines, 'permissions', {
|
|
942
|
+
mode: config.permissions?.mode,
|
|
943
|
+
profile: config.permissions?.profile,
|
|
944
|
+
});
|
|
945
|
+
for (const rule of config.permissions?.allow ?? []) {
|
|
946
|
+
writeTomlTableArray(lines, 'permissions.allow', permissionAllowRuleToToml(rule));
|
|
947
|
+
}
|
|
948
|
+
writeTomlSection(lines, 'agent', {
|
|
949
|
+
max_turns: config.agent?.maxTurns,
|
|
950
|
+
max_tool_calls: config.agent?.maxToolCalls,
|
|
951
|
+
auto_compact: config.agent?.autoCompact,
|
|
952
|
+
compact_strategy: config.agent?.compactStrategy,
|
|
953
|
+
auto_compact_threshold_percent: config.agent?.autoCompactThresholdPercent,
|
|
954
|
+
context_safety_buffer_tokens: config.agent?.contextSafetyBufferTokens,
|
|
955
|
+
max_output_reserve_tokens: config.agent?.maxOutputReserveTokens,
|
|
956
|
+
});
|
|
957
|
+
writeTomlSection(lines, 'status_line', {
|
|
958
|
+
items: config.statusLine?.items,
|
|
959
|
+
});
|
|
960
|
+
writeTomlSection(lines, 'skills', {
|
|
961
|
+
disabled: config.skills?.disabled,
|
|
962
|
+
});
|
|
963
|
+
writeMcpConfig(lines, config.mcp);
|
|
964
|
+
for (const profile of simpleProfilesFromRemiConfig(config)) {
|
|
965
|
+
const profileName = profile.name ?? profile.modelId;
|
|
966
|
+
const defaultMaxOutputTokens = defaultSimpleMaxOutputTokens(profile.modelId);
|
|
967
|
+
writeTomlTableArray(lines, 'profiles', {
|
|
968
|
+
name: profileName === profile.modelId ? undefined : profileName,
|
|
969
|
+
provider: profile.provider,
|
|
970
|
+
model_id: profile.modelId,
|
|
971
|
+
base_url: profile.baseUrl,
|
|
972
|
+
api_key: profile.apiKey,
|
|
973
|
+
api_key_env: profile.apiKeyEnv,
|
|
974
|
+
display_name: profile.displayName,
|
|
975
|
+
context_window: profile.contextWindow,
|
|
976
|
+
max_output_tokens: profile.maxOutputTokens !== undefined && profile.maxOutputTokens !== defaultMaxOutputTokens ? profile.maxOutputTokens : undefined,
|
|
977
|
+
supports_tools: profile.supportsTools === false ? false : undefined,
|
|
978
|
+
supports_reasoning: profile.supportsReasoning === false ? false : undefined,
|
|
979
|
+
effort: profile.effort && profile.effort !== 'max' ? profile.effort : undefined,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
for (const profile of profileRolesFromRemiConfig(config)) {
|
|
983
|
+
writeTomlTableArray(lines, 'profile_roles', {
|
|
984
|
+
name: profile.name,
|
|
985
|
+
extends: profile.extends,
|
|
986
|
+
main: profile.roles.main,
|
|
987
|
+
planning: profile.roles.planning,
|
|
988
|
+
subagent: profile.roles.subagent,
|
|
989
|
+
compact: profile.roles.compact,
|
|
990
|
+
memory: profile.roles.memory,
|
|
991
|
+
toolRepair: profile.roles.toolRepair,
|
|
992
|
+
embedding: profile.roles.embedding,
|
|
993
|
+
effort: profile.effort,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
for (const hook of simpleHooksFromRemiConfig(config.hooks)) {
|
|
997
|
+
writeTomlTableArray(lines, 'hooks', {
|
|
998
|
+
event: hookEventNameToToml(hook.event),
|
|
999
|
+
matcher: hook.matcher,
|
|
1000
|
+
command: hook.command,
|
|
1001
|
+
timeout: hook.timeout,
|
|
1002
|
+
status_message: hook.statusMessage,
|
|
1003
|
+
if: hook.if,
|
|
1004
|
+
once: hook.once,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
|
|
1008
|
+
}
|
|
1009
|
+
function writeMcpConfig(lines, mcp) {
|
|
1010
|
+
for (const [name, server] of Object.entries(mcp?.servers ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
1011
|
+
writeTomlSection(lines, `mcp.servers.${name}`, {
|
|
1012
|
+
type: server.type,
|
|
1013
|
+
command: server.command,
|
|
1014
|
+
args: server.args,
|
|
1015
|
+
cwd: server.cwd,
|
|
1016
|
+
enabled: server.enabled,
|
|
1017
|
+
timeout_ms: server.timeoutMs,
|
|
1018
|
+
});
|
|
1019
|
+
writeTomlSection(lines, `mcp.servers.${name}.env`, server.env ?? {});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function permissionAllowRuleToToml(rule) {
|
|
1023
|
+
if (rule.kind === 'shell-prefix') {
|
|
1024
|
+
return {
|
|
1025
|
+
kind: rule.kind,
|
|
1026
|
+
prefix: rule.prefix,
|
|
1027
|
+
cwd: rule.cwd,
|
|
1028
|
+
created_at: rule.createdAt,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (rule.kind === 'filesystem-write-root') {
|
|
1032
|
+
return {
|
|
1033
|
+
kind: rule.kind,
|
|
1034
|
+
root: rule.root,
|
|
1035
|
+
operations: rule.operations,
|
|
1036
|
+
created_at: rule.createdAt,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
kind: rule.kind,
|
|
1041
|
+
tool_name: rule.toolName,
|
|
1042
|
+
created_at: rule.createdAt,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function simpleProfilesFromRemiConfig(config) {
|
|
1046
|
+
return Object.entries(config.models ?? {}).flatMap(([name, model]) => {
|
|
1047
|
+
const provider = config.providers?.[model.provider];
|
|
1048
|
+
if (!provider) {
|
|
1049
|
+
return [];
|
|
1050
|
+
}
|
|
1051
|
+
const profile = config.profiles?.[name];
|
|
1052
|
+
return [
|
|
1053
|
+
{
|
|
1054
|
+
name,
|
|
1055
|
+
provider: provider.type,
|
|
1056
|
+
modelId: model.model,
|
|
1057
|
+
baseUrl: provider.baseURL,
|
|
1058
|
+
contextWindow: model.contextWindow,
|
|
1059
|
+
...(provider.apiKey ? { apiKey: provider.apiKey } : {}),
|
|
1060
|
+
...(provider.apiKeyEnv && !provider.apiKey ? { apiKeyEnv: provider.apiKeyEnv } : {}),
|
|
1061
|
+
...(model.displayName ? { displayName: model.displayName } : {}),
|
|
1062
|
+
...(model.maxOutputTokens !== undefined ? { maxOutputTokens: model.maxOutputTokens } : {}),
|
|
1063
|
+
supportsTools: model.supportsTools,
|
|
1064
|
+
supportsReasoning: model.supportsReasoning,
|
|
1065
|
+
...(profile?.effort ? { effort: profile.effort } : {}),
|
|
1066
|
+
},
|
|
1067
|
+
];
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
function profileRolesFromRemiConfig(config) {
|
|
1071
|
+
return Object.entries(config.profiles ?? {}).flatMap(([name, profile]) => {
|
|
1072
|
+
if (config.models?.[name]) {
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
return [
|
|
1076
|
+
{
|
|
1077
|
+
name,
|
|
1078
|
+
roles: profile.roles,
|
|
1079
|
+
...(profile.extends ? { extends: profile.extends } : {}),
|
|
1080
|
+
...(profile.effort ? { effort: profile.effort } : {}),
|
|
1081
|
+
},
|
|
1082
|
+
];
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
function simpleHooksFromRemiConfig(hooks) {
|
|
1086
|
+
return hookEventNames.flatMap(event => (hooks?.[event] ?? []).flatMap(group => group.hooks.flatMap(hook => hook.type === 'command'
|
|
1087
|
+
? [
|
|
1088
|
+
{
|
|
1089
|
+
event,
|
|
1090
|
+
matcher: group.matcher,
|
|
1091
|
+
command: hook.command,
|
|
1092
|
+
timeout: hook.timeout,
|
|
1093
|
+
statusMessage: hook.statusMessage,
|
|
1094
|
+
if: hook.if,
|
|
1095
|
+
once: hook.once,
|
|
1096
|
+
},
|
|
1097
|
+
]
|
|
1098
|
+
: [])));
|
|
1099
|
+
}
|
|
1100
|
+
function writeTomlRoot(lines, key, value) {
|
|
1101
|
+
if (value !== undefined) {
|
|
1102
|
+
lines.push(`${key} = ${formatTomlValue(value)}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function writeTomlSection(lines, name, values) {
|
|
1106
|
+
const entries = Object.entries(values).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0));
|
|
1107
|
+
if (entries.length === 0) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
ensureBlankLine(lines);
|
|
1111
|
+
lines.push(`[${name}]`);
|
|
1112
|
+
for (const [key, value] of entries) {
|
|
1113
|
+
lines.push(`${key} = ${formatTomlValue(value)}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function writeTomlTableArray(lines, name, values) {
|
|
1117
|
+
const entries = Object.entries(values).filter(([, value]) => value !== undefined);
|
|
1118
|
+
if (entries.length === 0) {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
ensureBlankLine(lines);
|
|
1122
|
+
lines.push(`[[${name}]]`);
|
|
1123
|
+
for (const [key, value] of entries) {
|
|
1124
|
+
lines.push(`${key} = ${formatTomlValue(value)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function ensureBlankLine(lines) {
|
|
1128
|
+
if (lines.length > 0 && lines.at(-1) !== '') {
|
|
1129
|
+
lines.push('');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function formatTomlValue(value) {
|
|
1133
|
+
if (typeof value === 'string') {
|
|
1134
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
1135
|
+
}
|
|
1136
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1137
|
+
return String(value);
|
|
1138
|
+
}
|
|
1139
|
+
if (Array.isArray(value)) {
|
|
1140
|
+
return `[${value.map(formatTomlValue).join(', ')}]`;
|
|
1141
|
+
}
|
|
1142
|
+
return '""';
|
|
1143
|
+
}
|
|
1144
|
+
function parseToml(text) {
|
|
1145
|
+
const root = {};
|
|
1146
|
+
let current = root;
|
|
1147
|
+
for (const rawLine of text.split(/\r?\n/u)) {
|
|
1148
|
+
const line = stripTomlComment(rawLine).trim();
|
|
1149
|
+
if (!line) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
const tableArrayMatch = /^\[\[([^\]]+)\]\]$/u.exec(line);
|
|
1153
|
+
if (tableArrayMatch?.[1]) {
|
|
1154
|
+
const items = getTomlArray(root, tableArrayMatch[1]);
|
|
1155
|
+
current = {};
|
|
1156
|
+
items.push(current);
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
const tableMatch = /^\[([^\]]+)\]$/u.exec(line);
|
|
1160
|
+
if (tableMatch?.[1]) {
|
|
1161
|
+
current = getTomlTable(root, tableMatch[1]);
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const assignment = splitTomlAssignment(line);
|
|
1165
|
+
if (!assignment) {
|
|
1166
|
+
throw new Error(`Invalid TOML line: ${rawLine}`);
|
|
1167
|
+
}
|
|
1168
|
+
current[assignment.key] = parseTomlValue(assignment.value);
|
|
1169
|
+
}
|
|
1170
|
+
return root;
|
|
1171
|
+
}
|
|
1172
|
+
function stripTomlComment(line) {
|
|
1173
|
+
let inString = false;
|
|
1174
|
+
let escaped = false;
|
|
1175
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
1176
|
+
const char = line[index];
|
|
1177
|
+
if (escaped) {
|
|
1178
|
+
escaped = false;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
if (char === '\\') {
|
|
1182
|
+
escaped = true;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (char === '"') {
|
|
1186
|
+
inString = !inString;
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (char === '#' && !inString) {
|
|
1190
|
+
return line.slice(0, index);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return line;
|
|
1194
|
+
}
|
|
1195
|
+
function splitTomlAssignment(line) {
|
|
1196
|
+
let inString = false;
|
|
1197
|
+
let escaped = false;
|
|
1198
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
1199
|
+
const char = line[index];
|
|
1200
|
+
if (escaped) {
|
|
1201
|
+
escaped = false;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (char === '\\') {
|
|
1205
|
+
escaped = true;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (char === '"') {
|
|
1209
|
+
inString = !inString;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (char === '=' && !inString) {
|
|
1213
|
+
const key = line.slice(0, index).trim();
|
|
1214
|
+
const value = line.slice(index + 1).trim();
|
|
1215
|
+
return key && value ? { key: unquoteTomlString(key), value } : undefined;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return undefined;
|
|
1219
|
+
}
|
|
1220
|
+
function parseTomlValue(value) {
|
|
1221
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1222
|
+
return unquoteTomlString(value);
|
|
1223
|
+
}
|
|
1224
|
+
if (value === 'true')
|
|
1225
|
+
return true;
|
|
1226
|
+
if (value === 'false')
|
|
1227
|
+
return false;
|
|
1228
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
1229
|
+
return splitTomlArray(value.slice(1, -1)).map(parseTomlValue);
|
|
1230
|
+
}
|
|
1231
|
+
const numeric = Number(value.replace(/_/g, ''));
|
|
1232
|
+
if (Number.isFinite(numeric)) {
|
|
1233
|
+
return numeric;
|
|
1234
|
+
}
|
|
1235
|
+
throw new Error(`Unsupported TOML value: ${value}`);
|
|
1236
|
+
}
|
|
1237
|
+
function splitTomlArray(value) {
|
|
1238
|
+
const items = [];
|
|
1239
|
+
let current = '';
|
|
1240
|
+
let inString = false;
|
|
1241
|
+
let escaped = false;
|
|
1242
|
+
for (const char of value) {
|
|
1243
|
+
if (escaped) {
|
|
1244
|
+
current += char;
|
|
1245
|
+
escaped = false;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (char === '\\') {
|
|
1249
|
+
current += char;
|
|
1250
|
+
escaped = true;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (char === '"') {
|
|
1254
|
+
current += char;
|
|
1255
|
+
inString = !inString;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (char === ',' && !inString) {
|
|
1259
|
+
if (current.trim())
|
|
1260
|
+
items.push(current.trim());
|
|
1261
|
+
current = '';
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
current += char;
|
|
1265
|
+
}
|
|
1266
|
+
if (current.trim())
|
|
1267
|
+
items.push(current.trim());
|
|
1268
|
+
return items;
|
|
1269
|
+
}
|
|
1270
|
+
function unquoteTomlString(value) {
|
|
1271
|
+
const trimmed = value.trim();
|
|
1272
|
+
const unquoted = trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
1273
|
+
return unquoted.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
1274
|
+
}
|
|
1275
|
+
function getTomlTable(root, path) {
|
|
1276
|
+
let current = root;
|
|
1277
|
+
for (const part of tomlPathParts(path)) {
|
|
1278
|
+
const existing = current[part];
|
|
1279
|
+
if (existing !== undefined && !isRecord(existing)) {
|
|
1280
|
+
throw new Error(`TOML table conflicts with existing value: ${path}`);
|
|
1281
|
+
}
|
|
1282
|
+
if (!existing) {
|
|
1283
|
+
current[part] = {};
|
|
1284
|
+
}
|
|
1285
|
+
current = current[part];
|
|
1286
|
+
}
|
|
1287
|
+
return current;
|
|
1288
|
+
}
|
|
1289
|
+
function getTomlArray(root, path) {
|
|
1290
|
+
const parts = tomlPathParts(path);
|
|
1291
|
+
const key = parts.pop();
|
|
1292
|
+
if (!key) {
|
|
1293
|
+
throw new Error(`Invalid TOML table array: ${path}`);
|
|
1294
|
+
}
|
|
1295
|
+
const parent = parts.length > 0 ? getTomlTable(root, parts.join('.')) : root;
|
|
1296
|
+
const existing = parent[key];
|
|
1297
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
1298
|
+
throw new Error(`TOML table array conflicts with existing value: ${path}`);
|
|
1299
|
+
}
|
|
1300
|
+
if (!existing) {
|
|
1301
|
+
parent[key] = [];
|
|
1302
|
+
}
|
|
1303
|
+
return parent[key];
|
|
1304
|
+
}
|
|
1305
|
+
function tomlPathParts(path) {
|
|
1306
|
+
return path.split('.').map(part => unquoteTomlString(part.trim())).filter(Boolean);
|
|
1307
|
+
}
|