@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.
Files changed (36) hide show
  1. package/README.md +206 -2
  2. package/dist/help.js +1 -1
  3. package/dist/initPrompt.js +1 -1
  4. package/dist/setup.js +5 -4
  5. package/dist/statusCard.js +1 -1
  6. package/dist/statusline.js +1 -1
  7. package/dist/tui/RemiApp.js +55 -7
  8. package/dist/tui/hooksPanel.js +2 -2
  9. package/dist/tui/renderers/MessageList.js +10 -7
  10. package/dist/version.js +1 -1
  11. package/node_modules/@remi/compact/dist/index.js +13 -5
  12. package/node_modules/@remi/compact/package.json +1 -1
  13. package/node_modules/@remi/config/dist/index.js +816 -38
  14. package/node_modules/@remi/config/package.json +1 -1
  15. package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
  16. package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
  17. package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
  18. package/node_modules/@remi/core/dist/index.js +630 -33
  19. package/node_modules/@remi/core/dist/stableHash.js +30 -0
  20. package/node_modules/@remi/core/package.json +1 -1
  21. package/node_modules/@remi/hooks/package.json +1 -1
  22. package/node_modules/@remi/llm/package.json +1 -1
  23. package/node_modules/@remi/mcp/dist/index.js +657 -0
  24. package/node_modules/@remi/mcp/package.json +8 -0
  25. package/node_modules/@remi/memory/package.json +1 -1
  26. package/node_modules/@remi/permissions/package.json +1 -1
  27. package/node_modules/@remi/sessions/dist/index.js +38 -0
  28. package/node_modules/@remi/sessions/package.json +1 -1
  29. package/node_modules/@remi/skills/dist/index.js +2 -2
  30. package/node_modules/@remi/skills/package.json +1 -1
  31. package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
  32. package/node_modules/@remi/terminal-markdown/package.json +1 -1
  33. package/node_modules/@remi/tools/dist/index.js +1 -1
  34. package/node_modules/@remi/tools/package.json +1 -1
  35. package/package.json +14 -12
  36. 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 createDeepSeekPresetConfig() {
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
- activeProfile: 'daily',
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.flash': {
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.pro': {
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.flash', 'deepseek.pro'],
107
+ deepseek: ['deepseek-v4-flash', 'deepseek-v4-pro'],
81
108
  },
82
109
  profiles: {
83
- daily: {
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.pro',
98
- planning: 'deepseek.pro',
99
- compact: 'deepseek.pro',
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: 'high',
119
+ effort: 'max',
102
120
  },
103
- flash: {
104
- extends: 'daily',
121
+ 'deepseek-v4-pro': {
105
122
  roles: {
106
- main: 'deepseek.flash',
107
- planning: 'deepseek.flash',
108
- compact: 'deepseek.flash',
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: 'low',
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.json');
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), `${JSON.stringify(config, null, 2)}\n`);
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.json');
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), `${JSON.stringify(config, null, 2)}\n`);
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 = JSON.parse(readFileSync(path, 'utf8'));
537
+ const parsed = parseToml(readFileSync(path, 'utf8'));
518
538
  if (!isRecord(parsed)) {
519
- throw new Error('Config root must be a JSON object');
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
+ }