@syrin/cli 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +36 -0
  2. package/dist/cli/commands/config.d.ts +47 -0
  3. package/dist/cli/commands/config.js +360 -0
  4. package/dist/cli/commands/dev.d.ts +6 -0
  5. package/dist/cli/commands/dev.js +67 -15
  6. package/dist/cli/commands/doctor.js +49 -13
  7. package/dist/cli/commands/init.d.ts +2 -0
  8. package/dist/cli/commands/init.js +89 -18
  9. package/dist/cli/commands/status.d.ts +10 -0
  10. package/dist/cli/commands/status.js +162 -0
  11. package/dist/cli/index.js +211 -12
  12. package/dist/cli/prompts/init-prompt.d.ts +18 -0
  13. package/dist/cli/prompts/init-prompt.js +159 -99
  14. package/dist/cli/utils/command-error-handler.js +2 -5
  15. package/dist/config/env-checker.d.ts +12 -2
  16. package/dist/config/env-checker.js +88 -38
  17. package/dist/config/env-templates.d.ts +15 -0
  18. package/dist/config/env-templates.js +49 -0
  19. package/dist/config/generator.js +17 -0
  20. package/dist/config/global-loader.d.ts +50 -0
  21. package/dist/config/global-loader.js +244 -0
  22. package/dist/config/loader.d.ts +28 -0
  23. package/dist/config/loader.js +95 -9
  24. package/dist/config/merger.d.ts +37 -0
  25. package/dist/config/merger.js +68 -0
  26. package/dist/config/schema.d.ts +26 -1
  27. package/dist/config/schema.js +73 -8
  28. package/dist/config/types.d.ts +19 -0
  29. package/dist/config/types.js +26 -1
  30. package/dist/constants/messages.d.ts +7 -0
  31. package/dist/constants/messages.js +8 -0
  32. package/dist/constants/paths.d.ts +6 -0
  33. package/dist/constants/paths.js +10 -0
  34. package/dist/events/emitter.js +7 -7
  35. package/dist/index.js +0 -0
  36. package/dist/presentation/config-ui.d.ts +34 -0
  37. package/dist/presentation/config-ui.js +139 -0
  38. package/dist/presentation/doctor-ui.d.ts +11 -0
  39. package/dist/presentation/doctor-ui.js +52 -1
  40. package/dist/presentation/init-ui.d.ts +9 -0
  41. package/dist/presentation/init-ui.js +33 -0
  42. package/dist/runtime/analysis/analyser.js +2 -2
  43. package/dist/runtime/analysis/rules/warnings/w104-generic-description.d.ts +1 -1
  44. package/dist/runtime/analysis/rules/warnings/w104-generic-description.js +1 -1
  45. package/dist/runtime/dev/event-mapper.js +19 -3
  46. package/dist/runtime/dev/session.d.ts +4 -0
  47. package/dist/runtime/dev/session.js +52 -3
  48. package/dist/runtime/llm/ollama.js +4 -4
  49. package/dist/runtime/mcp/client/manager.js +3 -3
  50. package/dist/runtime/sandbox/executor.js +5 -5
  51. package/dist/runtime/test/orchestrator.js +4 -4
  52. package/dist/utils/editor.d.ts +37 -0
  53. package/dist/utils/editor.js +137 -0
  54. package/dist/utils/logger.d.ts +24 -6
  55. package/dist/utils/logger.js +51 -8
  56. package/package.json +4 -4
  57. package/dist/runtime/analysis/rules/errors/e001-missing-output-schema.d.ts +0 -22
  58. package/dist/runtime/analysis/rules/errors/e001-missing-output-schema.js +0 -30
  59. package/dist/runtime/analysis/rules/errors/e002-underspecified-input.d.ts +0 -24
  60. package/dist/runtime/analysis/rules/errors/e002-underspecified-input.js +0 -52
  61. package/dist/runtime/analysis/rules/errors/e003-type-mismatch.d.ts +0 -23
  62. package/dist/runtime/analysis/rules/errors/e003-type-mismatch.js +0 -73
  63. package/dist/runtime/analysis/rules/errors/e004-free-text-propagation.d.ts +0 -23
  64. package/dist/runtime/analysis/rules/errors/e004-free-text-propagation.js +0 -47
  65. package/dist/runtime/analysis/rules/errors/e005-tool-ambiguity.d.ts +0 -25
  66. package/dist/runtime/analysis/rules/errors/e005-tool-ambiguity.js +0 -73
  67. package/dist/runtime/analysis/rules/errors/e006-param-not-in-description.d.ts +0 -22
  68. package/dist/runtime/analysis/rules/errors/e006-param-not-in-description.js +0 -57
  69. package/dist/runtime/analysis/rules/errors/e007-output-not-guaranteed.d.ts +0 -23
  70. package/dist/runtime/analysis/rules/errors/e007-output-not-guaranteed.js +0 -56
  71. package/dist/runtime/analysis/rules/errors/e008-circular-dependency.d.ts +0 -22
  72. package/dist/runtime/analysis/rules/errors/e008-circular-dependency.js +0 -84
  73. package/dist/runtime/analysis/rules/errors/e009-implicit-user-input.d.ts +0 -23
  74. package/dist/runtime/analysis/rules/errors/e009-implicit-user-input.js +0 -89
  75. package/dist/runtime/analysis/rules/errors/e010-non-serializable.d.ts +0 -25
  76. package/dist/runtime/analysis/rules/errors/e010-non-serializable.js +0 -46
  77. package/dist/runtime/analysis/rules/errors/e011-missing-tool-description.d.ts +0 -24
  78. package/dist/runtime/analysis/rules/errors/e011-missing-tool-description.js +0 -33
  79. package/dist/runtime/analysis/rules/errors/e012-side-effect-detected.d.ts +0 -39
  80. package/dist/runtime/analysis/rules/errors/e012-side-effect-detected.js +0 -40
  81. package/dist/runtime/analysis/rules/errors/e013-non-deterministic-output.d.ts +0 -37
  82. package/dist/runtime/analysis/rules/errors/e013-non-deterministic-output.js +0 -34
  83. package/dist/runtime/analysis/rules/errors/e013-output-explosion.d.ts +0 -39
  84. package/dist/runtime/analysis/rules/errors/e013-output-explosion.js +0 -36
  85. package/dist/runtime/analysis/rules/errors/e014-hidden-dependency.d.ts +0 -42
  86. package/dist/runtime/analysis/rules/errors/e014-hidden-dependency.js +0 -46
  87. package/dist/runtime/analysis/rules/errors/e014-output-explosion.d.ts +0 -39
  88. package/dist/runtime/analysis/rules/errors/e014-output-explosion.js +0 -36
  89. package/dist/runtime/analysis/rules/errors/e015-hidden-dependency.d.ts +0 -42
  90. package/dist/runtime/analysis/rules/errors/e015-hidden-dependency.js +0 -46
  91. package/dist/runtime/analysis/rules/errors/e015-unbounded-execution.d.ts +0 -44
  92. package/dist/runtime/analysis/rules/errors/e015-unbounded-execution.js +0 -66
  93. package/dist/runtime/analysis/rules/errors/e016-output-validation-failed.d.ts +0 -43
  94. package/dist/runtime/analysis/rules/errors/e016-output-validation-failed.js +0 -42
  95. package/dist/runtime/analysis/rules/errors/e016-unbounded-execution.d.ts +0 -44
  96. package/dist/runtime/analysis/rules/errors/e016-unbounded-execution.js +0 -66
  97. package/dist/runtime/analysis/rules/errors/e017-input-validation-failed.d.ts +0 -57
  98. package/dist/runtime/analysis/rules/errors/e017-input-validation-failed.js +0 -80
  99. package/dist/runtime/analysis/rules/errors/e017-output-validation-failed.d.ts +0 -43
  100. package/dist/runtime/analysis/rules/errors/e017-output-validation-failed.js +0 -42
  101. package/dist/runtime/analysis/rules/errors/e018-input-validation-failed.d.ts +0 -57
  102. package/dist/runtime/analysis/rules/errors/e018-input-validation-failed.js +0 -80
  103. package/dist/runtime/analysis/rules/errors/e018-tool-execution-failed.d.ts +0 -38
  104. package/dist/runtime/analysis/rules/errors/e018-tool-execution-failed.js +0 -37
  105. package/dist/runtime/analysis/rules/errors/e019-tool-execution-failed.d.ts +0 -38
  106. package/dist/runtime/analysis/rules/errors/e019-tool-execution-failed.js +0 -37
  107. package/dist/runtime/analysis/rules/errors/e019-unexpected-test-result.d.ts +0 -65
  108. package/dist/runtime/analysis/rules/errors/e019-unexpected-test-result.js +0 -109
  109. package/dist/runtime/analysis/rules/errors/e020-unexpected-test-result.d.ts +0 -65
  110. package/dist/runtime/analysis/rules/errors/e020-unexpected-test-result.js +0 -109
  111. package/dist/runtime/analysis/rules/warnings/w001-implicit-dependency.d.ts +0 -22
  112. package/dist/runtime/analysis/rules/warnings/w001-implicit-dependency.js +0 -39
  113. package/dist/runtime/analysis/rules/warnings/w002-free-text-without-normalization.d.ts +0 -24
  114. package/dist/runtime/analysis/rules/warnings/w002-free-text-without-normalization.js +0 -40
  115. package/dist/runtime/analysis/rules/warnings/w003-missing-examples.d.ts +0 -22
  116. package/dist/runtime/analysis/rules/warnings/w003-missing-examples.js +0 -84
  117. package/dist/runtime/analysis/rules/warnings/w004-overloaded-responsibility.d.ts +0 -23
  118. package/dist/runtime/analysis/rules/warnings/w004-overloaded-responsibility.js +0 -96
  119. package/dist/runtime/analysis/rules/warnings/w005-generic-description.d.ts +0 -53
  120. package/dist/runtime/analysis/rules/warnings/w005-generic-description.js +0 -108
  121. package/dist/runtime/analysis/rules/warnings/w006-optional-as-required.d.ts +0 -22
  122. package/dist/runtime/analysis/rules/warnings/w006-optional-as-required.js +0 -44
  123. package/dist/runtime/analysis/rules/warnings/w007-broad-output-schema.d.ts +0 -23
  124. package/dist/runtime/analysis/rules/warnings/w007-broad-output-schema.js +0 -37
  125. package/dist/runtime/analysis/rules/warnings/w008-multiple-entry-points.d.ts +0 -22
  126. package/dist/runtime/analysis/rules/warnings/w008-multiple-entry-points.js +0 -97
  127. package/dist/runtime/analysis/rules/warnings/w009-hidden-side-effects.d.ts +0 -23
  128. package/dist/runtime/analysis/rules/warnings/w009-hidden-side-effects.js +0 -88
  129. package/dist/runtime/analysis/rules/warnings/w010-output-not-reusable.d.ts +0 -22
  130. package/dist/runtime/analysis/rules/warnings/w010-output-not-reusable.js +0 -81
  131. package/dist/runtime/analysis/rules/warnings/w021-weak-schema.d.ts +0 -40
  132. package/dist/runtime/analysis/rules/warnings/w021-weak-schema.js +0 -32
  133. package/dist/runtime/analysis/rules/warnings/w022-high-entropy-output.d.ts +0 -39
  134. package/dist/runtime/analysis/rules/warnings/w022-high-entropy-output.js +0 -36
  135. package/dist/runtime/analysis/rules/warnings/w023-unstable-defaults.d.ts +0 -38
  136. package/dist/runtime/analysis/rules/warnings/w023-unstable-defaults.js +0 -36
  137. package/dist/runtime/test/dependency-tracker.d.ts +0 -66
  138. package/dist/runtime/test/dependency-tracker.js +0 -80
  139. package/dist/runtime/test/formatters.d.ts +0 -18
  140. package/dist/runtime/test/formatters.js +0 -172
  141. package/dist/runtime/test/input-generator.d.ts +0 -33
  142. package/dist/runtime/test/input-generator.js +0 -498
  143. package/dist/runtime/test/mcp-root-detector.d.ts +0 -31
  144. package/dist/runtime/test/mcp-root-detector.js +0 -105
  145. package/dist/runtime/test/retry-tester.d.ts +0 -44
  146. package/dist/runtime/test/retry-tester.js +0 -103
  147. package/dist/runtime/test/synthetic-input-generator.d.ts +0 -11
  148. package/dist/runtime/test/synthetic-input-generator.js +0 -154
  149. package/dist/runtime/test/test-runner.d.ts +0 -28
  150. package/dist/runtime/test/test-runner.js +0 -55
package/README.md CHANGED
@@ -97,6 +97,42 @@ It makes MCP systems understandable and testable.
97
97
 
98
98
  ---
99
99
 
100
+ ## Global Configuration
101
+
102
+ Syrin supports both **local** (project-specific) and **global** (user-wide) configurations. This allows you to:
103
+
104
+ - Use Syrin from any directory without initializing a project
105
+ - Share LLM API keys across multiple projects
106
+ - Set default agent names and LLM providers globally
107
+
108
+ ### Quick Setup
109
+
110
+ ```bash
111
+ # Set up global configuration
112
+ syrin config setup --global
113
+
114
+ # Set API keys in global .env
115
+ syrin config edit-env --global
116
+
117
+ # Use Syrin from any directory
118
+ syrin dev --exec --transport http --mcp-url http://localhost:8000/mcp
119
+ ```
120
+
121
+ ### Configuration Management
122
+
123
+ ```bash
124
+ # View global config
125
+ syrin config list --global
126
+
127
+ # Set global LLM provider
128
+ syrin config set openai.model "gpt-4-turbo" --global
129
+
130
+ # Set default provider
131
+ syrin config set-default claude --global
132
+ ```
133
+
134
+ See the [Configuration Guide](docs/Commands/syrin-config.md) for more details.
135
+
100
136
  ## Key Capabilities
101
137
 
102
138
  ### Static Tool Contract Analysis (`syrin analyse`)
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `syrin config` command implementation.
3
+ * Manages both local and global Syrin configurations.
4
+ */
5
+ export interface ConfigCommandOptions {
6
+ /** Operate on global config */
7
+ global?: boolean;
8
+ /** Operate on local config */
9
+ local?: boolean;
10
+ /** Key to get/set (e.g., "openai.model", "agent_name") */
11
+ key?: string;
12
+ /** Value to set */
13
+ value?: string;
14
+ }
15
+ /**
16
+ * Execute config set command.
17
+ */
18
+ export declare function executeConfigSet(key: string, value: string, options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
19
+ /**
20
+ * Execute config get command.
21
+ */
22
+ export declare function executeConfigGet(key: string, options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
23
+ /**
24
+ * Execute config list command.
25
+ */
26
+ export declare function executeConfigList(options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
27
+ /**
28
+ * Execute config show command (same as list for now).
29
+ */
30
+ export declare function executeConfigShow(options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
31
+ /**
32
+ * Execute config edit command.
33
+ */
34
+ export declare function executeConfigEdit(options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
35
+ /**
36
+ * Execute config edit-env command.
37
+ */
38
+ export declare function executeConfigEditEnv(options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
39
+ /**
40
+ * Execute config set-default command.
41
+ */
42
+ export declare function executeConfigSetDefault(provider: string, options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
43
+ /**
44
+ * Execute config remove command.
45
+ */
46
+ export declare function executeConfigRemove(provider: string, options?: ConfigCommandOptions, projectRoot?: string): Promise<void>;
47
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,360 @@
1
+ /**
2
+ * `syrin config` command implementation.
3
+ * Manages both local and global Syrin configurations.
4
+ */
5
+ import * as path from 'path';
6
+ import * as os from 'os';
7
+ import * as fs from 'fs';
8
+ import { loadConfigOptional, saveLocalConfig } from '../../config/loader.js';
9
+ import { loadGlobalConfig, saveGlobalConfig, getGlobalConfigPath, globalConfigExists, } from '../../config/global-loader.js';
10
+ import { getGlobalEnvPath } from '../../config/global-loader.js';
11
+ import { Paths } from '../../constants/index.js';
12
+ import { log } from '../../utils/logger.js';
13
+ import { openInEditor } from '../../utils/editor.js';
14
+ import { displayConfigList, displayConfigUpdated, displayEditorOpening, displayEditorClosed, displayDefaultProviderSet, displayProviderRemoved, } from '../../presentation/config-ui.js';
15
+ import { getGlobalEnvTemplate, getLocalEnvTemplate, } from '../../config/env-templates.js';
16
+ import { makeAgentName, makeAPIKey, makeModelName } from '../../types/factories.js';
17
+ /**
18
+ * Detect config context (local or global).
19
+ * @param projectRoot - Project root directory
20
+ * @param options - Command options
21
+ * @returns 'local' | 'global' | null
22
+ */
23
+ function detectConfigContext(projectRoot, options) {
24
+ // Explicit flags take precedence
25
+ if (options.local)
26
+ return 'local';
27
+ if (options.global)
28
+ return 'global';
29
+ // Auto-detect: local if exists, else global
30
+ const localConfigPath = path.join(projectRoot, Paths.CONFIG_FILE);
31
+ const localExists = fs.existsSync(localConfigPath);
32
+ if (localExists)
33
+ return 'local';
34
+ if (globalConfigExists())
35
+ return 'global';
36
+ return null;
37
+ }
38
+ /**
39
+ * Get config based on context.
40
+ * @param skipValidation - Skip validation (for editing operations)
41
+ */
42
+ function getConfig(projectRoot, context, skipValidation = false) {
43
+ if (context === 'local') {
44
+ return loadConfigOptional(projectRoot);
45
+ }
46
+ return loadGlobalConfig(skipValidation);
47
+ }
48
+ /**
49
+ * Save config based on context.
50
+ */
51
+ function saveConfig(config, context, projectRoot, skipValidation = true) {
52
+ if (context === 'global') {
53
+ if ('transport' in config) {
54
+ throw new Error('Cannot save full config to global. Global config only supports LLM settings.');
55
+ }
56
+ saveGlobalConfig(config, skipValidation);
57
+ }
58
+ else {
59
+ if (!('transport' in config)) {
60
+ throw new Error('Cannot save global config to local. Local config requires transport and other fields.');
61
+ }
62
+ saveLocalConfig(config, projectRoot);
63
+ }
64
+ }
65
+ /**
66
+ * Parse a config key path (e.g., "openai.model" -> { provider: "openai", field: "model" }).
67
+ */
68
+ function parseConfigKey(key) {
69
+ const parts = key.split('.');
70
+ if (parts.length === 1) {
71
+ const field = parts[0];
72
+ if (!field) {
73
+ throw new Error(`Invalid config key: ${key}. Field cannot be empty.`);
74
+ }
75
+ return { field };
76
+ }
77
+ if (parts.length === 2) {
78
+ const provider = parts[0];
79
+ const field = parts[1];
80
+ if (!provider || !field) {
81
+ throw new Error(`Invalid config key: ${key}. Provider and field cannot be empty.`);
82
+ }
83
+ return { provider, field };
84
+ }
85
+ throw new Error(`Invalid config key: ${key}. Use format "field" or "provider.field"`);
86
+ }
87
+ /**
88
+ * Set a config value.
89
+ */
90
+ function setConfigValue(config, key, value, context) {
91
+ const parsed = parseConfigKey(key);
92
+ // Validate that global config only allows LLM-related fields
93
+ if (context === 'global') {
94
+ const allowedFields = ['agent_name', 'openai', 'claude', 'ollama'];
95
+ const allowedLLMFields = ['api_key', 'model_name', 'model', 'default'];
96
+ if (parsed.provider) {
97
+ // LLM provider field
98
+ if (!allowedFields.includes(parsed.provider)) {
99
+ throw new Error(`Cannot set "${key}" in global config. Global config only supports LLM provider settings.`);
100
+ }
101
+ if (!allowedLLMFields.includes(parsed.field)) {
102
+ throw new Error(`Invalid LLM field: ${parsed.field}. Allowed: api_key, model_name, model, default`);
103
+ }
104
+ }
105
+ else {
106
+ // Top-level field
107
+ if (parsed.field !== 'agent_name') {
108
+ throw new Error(`Cannot set "${key}" in global config. Global config only supports "agent_name" and LLM provider settings.`);
109
+ }
110
+ }
111
+ }
112
+ // Clone config
113
+ // Note: JSON.parse(JSON.stringify()) strips branded/opaque types (e.g., makeAPIKey, makeModelName)
114
+ // This is intentional - we reapply the factories when setting new values below
115
+ const updated = JSON.parse(JSON.stringify(config));
116
+ if (parsed.provider) {
117
+ // LLM provider field
118
+ if (!updated.llm[parsed.provider]) {
119
+ updated.llm[parsed.provider] = {};
120
+ }
121
+ const fieldMap = {
122
+ api_key: 'API_KEY',
123
+ model_name: 'MODEL_NAME',
124
+ model: 'MODEL_NAME',
125
+ };
126
+ const actualField = fieldMap[parsed.field] || parsed.field.toUpperCase();
127
+ if (parsed.field === 'default') {
128
+ // Set default provider
129
+ Object.keys(updated.llm).forEach(p => {
130
+ updated.llm[p].default = p === parsed.provider;
131
+ });
132
+ }
133
+ else {
134
+ updated.llm[parsed.provider][actualField] =
135
+ actualField === 'API_KEY' ? makeAPIKey(value) : makeModelName(value);
136
+ }
137
+ }
138
+ else {
139
+ // Top-level field
140
+ if (parsed.field === 'agent_name') {
141
+ updated.agent_name = makeAgentName(value);
142
+ }
143
+ else {
144
+ throw new Error(`Cannot set top-level field "${parsed.field}" via config command.`);
145
+ }
146
+ }
147
+ return updated;
148
+ }
149
+ /**
150
+ * Get a config value.
151
+ */
152
+ function getConfigValue(config, key) {
153
+ const parsed = parseConfigKey(key);
154
+ if (parsed.provider) {
155
+ // LLM provider field
156
+ const provider = config.llm[parsed.provider];
157
+ if (!provider) {
158
+ return undefined;
159
+ }
160
+ const fieldMap = {
161
+ api_key: 'API_KEY',
162
+ model_name: 'MODEL_NAME',
163
+ model: 'MODEL_NAME',
164
+ };
165
+ const actualField = fieldMap[parsed.field] || parsed.field.toUpperCase();
166
+ if (parsed.field === 'default') {
167
+ return provider.default || false;
168
+ }
169
+ return provider[actualField];
170
+ }
171
+ // Top-level field
172
+ const configRecord = config;
173
+ return configRecord[parsed.field];
174
+ }
175
+ /**
176
+ * Execute config set command.
177
+ */
178
+ export async function executeConfigSet(key, value, options = {}, projectRoot = process.cwd()) {
179
+ const context = detectConfigContext(projectRoot, options);
180
+ if (!context) {
181
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
182
+ }
183
+ // For set operations, skip validation to allow partial provider configs
184
+ const config = getConfig(projectRoot, context, true);
185
+ if (!config) {
186
+ if (context === 'global') {
187
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
188
+ }
189
+ else {
190
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
191
+ }
192
+ }
193
+ const updated = setConfigValue(config, key, value, context);
194
+ // Save without strict validation (allow partial provider configs during editing)
195
+ // Validation will happen when config is actually used (in dev mode, etc.)
196
+ saveConfig(updated, context, projectRoot);
197
+ const configPath = context === 'local'
198
+ ? path.join(projectRoot, Paths.CONFIG_FILE)
199
+ : getGlobalConfigPath();
200
+ displayConfigUpdated(key, value, context, configPath);
201
+ }
202
+ /**
203
+ * Execute config get command.
204
+ */
205
+ export async function executeConfigGet(key, options = {}, projectRoot = process.cwd()) {
206
+ const context = detectConfigContext(projectRoot, options);
207
+ if (!context) {
208
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
209
+ }
210
+ const config = getConfig(projectRoot, context);
211
+ if (!config) {
212
+ if (context === 'global') {
213
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
214
+ }
215
+ else {
216
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
217
+ }
218
+ }
219
+ const value = getConfigValue(config, key);
220
+ if (value === undefined) {
221
+ log.error(`Key "${key}" not found in ${context} config.`);
222
+ process.exit(1);
223
+ }
224
+ log.plain(String(value));
225
+ }
226
+ /**
227
+ * Execute config list command.
228
+ */
229
+ export async function executeConfigList(options = {}, projectRoot = process.cwd()) {
230
+ const context = detectConfigContext(projectRoot, options);
231
+ if (!context) {
232
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
233
+ }
234
+ const config = getConfig(projectRoot, context);
235
+ if (!config) {
236
+ if (context === 'global') {
237
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
238
+ }
239
+ else {
240
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
241
+ }
242
+ }
243
+ const configPath = context === 'local'
244
+ ? path.join(projectRoot, Paths.CONFIG_FILE)
245
+ : getGlobalConfigPath();
246
+ displayConfigList(config, context, configPath);
247
+ }
248
+ /**
249
+ * Execute config show command (same as list for now).
250
+ */
251
+ export async function executeConfigShow(options = {}, projectRoot = process.cwd()) {
252
+ await executeConfigList(options, projectRoot);
253
+ }
254
+ /**
255
+ * Execute config edit command.
256
+ */
257
+ export async function executeConfigEdit(options = {}, projectRoot = process.cwd()) {
258
+ const context = detectConfigContext(projectRoot, options);
259
+ if (!context) {
260
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
261
+ }
262
+ const configPath = context === 'local'
263
+ ? path.join(projectRoot, Paths.CONFIG_FILE)
264
+ : getGlobalConfigPath();
265
+ const editorName = os.platform() === 'win32' ? 'notepad' : 'nano';
266
+ displayEditorOpening(context, configPath, editorName);
267
+ await openInEditor(configPath);
268
+ displayEditorClosed('config');
269
+ }
270
+ /**
271
+ * Execute config edit-env command.
272
+ */
273
+ export async function executeConfigEditEnv(options = {}, projectRoot = process.cwd()) {
274
+ const context = detectConfigContext(projectRoot, options);
275
+ if (!context) {
276
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
277
+ }
278
+ const envPath = context === 'local'
279
+ ? path.join(projectRoot, Paths.ENV_FILE)
280
+ : getGlobalEnvPath();
281
+ const template = context === 'local' ? getLocalEnvTemplate() : getGlobalEnvTemplate();
282
+ const editorName = os.platform() === 'win32' ? 'notepad' : 'nano';
283
+ displayEditorOpening(context, envPath, editorName);
284
+ await openInEditor(envPath, true, template);
285
+ displayEditorClosed('env');
286
+ }
287
+ /**
288
+ * Execute config set-default command.
289
+ */
290
+ export async function executeConfigSetDefault(provider, options = {}, projectRoot = process.cwd()) {
291
+ const context = detectConfigContext(projectRoot, options);
292
+ if (!context) {
293
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
294
+ }
295
+ const config = getConfig(projectRoot, context);
296
+ if (!config) {
297
+ if (context === 'global') {
298
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
299
+ }
300
+ else {
301
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
302
+ }
303
+ }
304
+ if (!config.llm[provider]) {
305
+ throw new Error(`LLM provider "${provider}" not found in ${context} config.`);
306
+ }
307
+ // Clone config to avoid mutating the original
308
+ const updated = JSON.parse(JSON.stringify(config));
309
+ // Set all providers to non-default, then set the specified one to default
310
+ Object.keys(updated.llm).forEach(p => {
311
+ const providerConfig = updated.llm[p];
312
+ if (providerConfig) {
313
+ providerConfig.default = p === provider;
314
+ }
315
+ });
316
+ saveConfig(updated, context, projectRoot);
317
+ displayDefaultProviderSet(provider, context);
318
+ }
319
+ /**
320
+ * Execute config remove command.
321
+ */
322
+ export async function executeConfigRemove(provider, options = {}, projectRoot = process.cwd()) {
323
+ const context = detectConfigContext(projectRoot, options);
324
+ if (!context) {
325
+ throw new Error('No config found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
326
+ }
327
+ const config = getConfig(projectRoot, context);
328
+ if (!config) {
329
+ if (context === 'global') {
330
+ throw new Error('No global config found. Run `syrin init --global` to create one.');
331
+ }
332
+ else {
333
+ throw new Error('No local config found. Run `syrin init` to create one, or use `--global` flag for global config.');
334
+ }
335
+ }
336
+ if (!config.llm[provider]) {
337
+ throw new Error(`LLM provider "${provider}" not found in ${context} config.`);
338
+ }
339
+ // Check if it's the only provider
340
+ if (Object.keys(config.llm).length === 1) {
341
+ throw new Error('Cannot remove the last LLM provider. At least one provider is required.');
342
+ }
343
+ // Clone config to avoid mutating the original
344
+ const updated = JSON.parse(JSON.stringify(config));
345
+ // Check if it's the default provider
346
+ if (updated.llm[provider]?.default) {
347
+ // Set another provider as default
348
+ const otherProvider = Object.keys(updated.llm).find(p => p !== provider);
349
+ if (otherProvider) {
350
+ const otherProviderConfig = updated.llm[otherProvider];
351
+ if (otherProviderConfig) {
352
+ otherProviderConfig.default = true;
353
+ }
354
+ }
355
+ }
356
+ delete updated.llm[provider];
357
+ saveConfig(updated, context, projectRoot);
358
+ displayProviderRemoved(provider, context);
359
+ }
360
+ //# sourceMappingURL=config.js.map
@@ -15,6 +15,12 @@ export interface DevCommandOptions {
15
15
  eventFile?: string;
16
16
  /** Run script to spawn server internally */
17
17
  runScript?: boolean;
18
+ /** Transport type override */
19
+ transport?: 'stdio' | 'http';
20
+ /** MCP URL override (for http transport) */
21
+ mcpUrl?: string;
22
+ /** Script command override (for stdio transport) */
23
+ script?: string;
18
24
  }
19
25
  /**
20
26
  * Execute the dev command.
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { loadConfig } from '../../config/loader.js';
7
+ import { loadConfigWithGlobal, configExists } from '../../config/loader.js';
8
+ import { loadGlobalConfig } from '../../config/global-loader.js';
8
9
  import { getLLMProvider } from '../../runtime/llm/factory.js';
9
10
  import { createMCPClientManager } from '../../runtime/mcp/client/manager.js';
10
11
  import { RuntimeEventEmitter } from '../../events/emitter.js';
@@ -15,7 +16,7 @@ import { ChatUI } from '../../presentation/dev/chat-ui.js';
15
16
  import { DevEventMapper } from '../../runtime/dev/event-mapper.js';
16
17
  import { ConfigurationError } from '../../utils/errors.js';
17
18
  import { handleCommandError } from '../../cli/utils/index.js';
18
- import { logger, log } from '../../utils/logger.js';
19
+ import { log } from '../../utils/logger.js';
19
20
  import { Messages, Paths, TransportTypes, FileExtensions } from '../../constants/index.js';
20
21
  import { makeSessionID } from '../../types/factories.js';
21
22
  import { v4 as uuidv4 } from 'uuid';
@@ -51,8 +52,59 @@ function resolveServerCommand(config, runScript) {
51
52
  export async function executeDev(options = {}) {
52
53
  const projectRoot = options.projectRoot || process.cwd();
53
54
  try {
54
- // Load configuration
55
- const config = loadConfig(projectRoot);
55
+ // Load configuration (with global fallback)
56
+ const localExists = configExists(projectRoot);
57
+ const globalExists = loadGlobalConfig() !== null;
58
+ let config;
59
+ let configSource;
60
+ try {
61
+ const configResult = loadConfigWithGlobal(projectRoot, {
62
+ transport: options.transport,
63
+ mcp_url: options.mcpUrl,
64
+ script: options.script,
65
+ });
66
+ config = configResult.config;
67
+ configSource = configResult.source;
68
+ }
69
+ catch (error) {
70
+ if (error instanceof Error &&
71
+ error.message.includes('No configuration found')) {
72
+ log.blank();
73
+ log.error('❌ No configuration found.');
74
+ log.blank();
75
+ if (!localExists && !globalExists) {
76
+ log.info('💡 Options:');
77
+ log.info(' 1. Create local config: syrin init');
78
+ log.info(' 2. Set up global config: syrin init --global');
79
+ log.blank();
80
+ }
81
+ else if (!localExists) {
82
+ log.info('💡 Using global config requires CLI flags:');
83
+ log.info(' --transport <stdio|http>');
84
+ if (options.transport === 'http') {
85
+ log.info(' --mcp-url <url>');
86
+ }
87
+ else if (options.transport === 'stdio') {
88
+ log.info(' --script <command>');
89
+ }
90
+ else {
91
+ // Show both options when transport is not specified
92
+ log.info(' --mcp-url <url> (for http transport)');
93
+ log.info(' --script <command> (for stdio transport)');
94
+ }
95
+ log.blank();
96
+ }
97
+ throw error;
98
+ }
99
+ throw error;
100
+ }
101
+ // Show config source
102
+ if (configSource === 'global') {
103
+ log.blank();
104
+ log.info('â„šī¸ Using global configuration from ~/.syrin/syrin.yaml');
105
+ log.info(' (No local syrin.yaml found in current directory)');
106
+ log.blank();
107
+ }
56
108
  // Validate transport configuration
57
109
  if (config.transport === TransportTypes.HTTP && !config.mcp_url) {
58
110
  throw new ConfigurationError(Messages.TRANSPORT_URL_REQUIRED_CONFIG);
@@ -88,7 +140,7 @@ export async function executeDev(options = {}) {
88
140
  }
89
141
  eventStore = new FileEventStore(eventsDir);
90
142
  const eventFilePath = path.join(eventsDir, `${sessionId}${FileExtensions.JSONL}`);
91
- logger.info(`Events will be saved to: ${eventFilePath}`);
143
+ log.info(`Events will be saved to: ${eventFilePath}`);
92
144
  log.blank();
93
145
  log.info(`Events are being saved to: ${eventFilePath}`);
94
146
  log.blank();
@@ -117,7 +169,7 @@ export async function executeDev(options = {}) {
117
169
  }
118
170
  catch (error) {
119
171
  const err = error instanceof Error ? error : new Error(String(error));
120
- logger.error('Error during cleanup', err);
172
+ log.error(`Error: ${err.message}`);
121
173
  // Even if disconnect fails, try to kill the process directly
122
174
  // This is a safety net to ensure cleanup happens
123
175
  }
@@ -133,7 +185,7 @@ export async function executeDev(options = {}) {
133
185
  // Handle uncaught exceptions
134
186
  process.on('uncaughtException', (error) => {
135
187
  void (async () => {
136
- logger.error('Uncaught exception', error);
188
+ log.error(`Error: ${error.message}`);
137
189
  await cleanup();
138
190
  process.exit(1);
139
191
  })();
@@ -141,7 +193,7 @@ export async function executeDev(options = {}) {
141
193
  // Handle unhandled promise rejections
142
194
  process.on('unhandledRejection', (reason) => {
143
195
  void (async () => {
144
- logger.error('Unhandled rejection', reason instanceof Error ? reason : new Error(String(reason)));
196
+ log.error(`Error: ${reason instanceof Error ? reason.message : String(reason)}`);
145
197
  await cleanup();
146
198
  process.exit(1);
147
199
  })();
@@ -302,14 +354,14 @@ export async function executeDev(options = {}) {
302
354
  const errorMessage = error instanceof Error ? error.message : String(error);
303
355
  chatUI.addMessage('system', `❌ Error loading tool result: ${errorMessage}`);
304
356
  const err = error instanceof Error ? error : new Error(String(error));
305
- logger.error('Error loading tool result for save', err);
357
+ log.error(`Error: ${err.message}`);
306
358
  }
307
359
  }
308
360
  catch (error) {
309
361
  const errorMessage = error instanceof Error ? error.message : String(error);
310
362
  chatUI.addMessage('system', `❌ Error saving JSON: ${errorMessage}`);
311
363
  const err = error instanceof Error ? error : new Error(String(error));
312
- logger.error('Error saving JSON to file', err);
364
+ log.error(`Error: ${err.message}`);
313
365
  }
314
366
  return;
315
367
  }
@@ -333,7 +385,7 @@ export async function executeDev(options = {}) {
333
385
  const errorMessage = error instanceof Error ? error.message : String(error);
334
386
  chatUI.addMessage('system', Messages.DEV_ERROR_READING_HISTORY(errorMessage));
335
387
  const err = error instanceof Error ? error : new Error(String(error));
336
- logger.error('Error reading history file', err);
388
+ log.error(`Error: ${err.message}`);
337
389
  }
338
390
  return;
339
391
  }
@@ -352,7 +404,7 @@ export async function executeDev(options = {}) {
352
404
  const errorMessage = error instanceof Error ? error.message : String(error);
353
405
  chatUI.addMessage('system', `❌ Error: ${errorMessage}`);
354
406
  const err = error instanceof Error ? error : new Error(String(error));
355
- logger.error('Error processing user input', err);
407
+ log.error(`Error: ${err.message}`);
356
408
  }
357
409
  },
358
410
  onExit: async () => {
@@ -365,12 +417,12 @@ export async function executeDev(options = {}) {
365
417
  // Close file event store if used
366
418
  if (options.saveEvents && eventStore instanceof FileEventStore) {
367
419
  await eventStore.close();
368
- logger.info('Event store closed');
420
+ log.info('Event store closed');
369
421
  }
370
422
  }
371
423
  catch (error) {
372
424
  const err = error instanceof Error ? error : new Error(String(error));
373
- logger.error('Error during cleanup', err);
425
+ log.error(`Error: ${err.message}`);
374
426
  }
375
427
  },
376
428
  });
@@ -406,7 +458,7 @@ export async function executeDev(options = {}) {
406
458
  }
407
459
  catch (error) {
408
460
  const err = error instanceof Error ? error : new Error(String(error));
409
- logger.error('Error during SIGINT cleanup', err);
461
+ log.error(`Error: ${err.message}`);
410
462
  }
411
463
  process.exit(0);
412
464
  })();