@syrin/cli 1.3.2 → 1.4.1

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 +184 -152
  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 +23 -23
  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
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Global configuration file loader.
3
+ * Loads and saves the global syrin.yaml file at ~/.syrin/syrin.yaml
4
+ */
5
+ import type { GlobalSyrinConfig } from './types.js';
6
+ /**
7
+ * Get the path to the global Syrin directory.
8
+ * @returns Absolute path to ~/.syrin/
9
+ */
10
+ export declare function getGlobalSyrinDir(): string;
11
+ /**
12
+ * Get the path to the global configuration file.
13
+ * @returns Absolute path to ~/.syrin/syrin.yaml
14
+ */
15
+ export declare function getGlobalConfigPath(): string;
16
+ /**
17
+ * Get the path to the global environment file.
18
+ * @returns Absolute path to ~/.syrin/.env
19
+ */
20
+ export declare function getGlobalEnvPath(): string;
21
+ /**
22
+ * Ensure the global Syrin directory exists.
23
+ * Creates ~/.syrin/ if it doesn't exist.
24
+ */
25
+ export declare function ensureGlobalSyrinDir(): void;
26
+ /**
27
+ * Check if global configuration file exists.
28
+ * @returns true if global config file exists
29
+ */
30
+ export declare function globalConfigExists(): boolean;
31
+ /**
32
+ * Load global configuration from ~/.syrin/syrin.yaml file.
33
+ * @param skipValidation - If true, skip validation (for editing operations)
34
+ * @returns Loaded and validated global configuration, or null if file doesn't exist
35
+ * @throws {ConfigurationError} If config file exists but is invalid
36
+ */
37
+ export declare function loadGlobalConfig(skipValidation?: boolean): GlobalSyrinConfig | null;
38
+ /**
39
+ * Save global configuration to ~/.syrin/syrin.yaml file.
40
+ * @param config - Global configuration to save
41
+ * @param _skipValidation - Skip validation (for editing operations) - TODO: Implement validation skip logic when needed for future editing operations
42
+ * @throws {ConfigurationError} If saving fails
43
+ */
44
+ export declare function saveGlobalConfig(config: GlobalSyrinConfig, _skipValidation?: boolean): void;
45
+ /**
46
+ * Create a default global configuration.
47
+ * @returns Default global configuration with system username as agent name
48
+ */
49
+ export declare function createDefaultGlobalConfig(): GlobalSyrinConfig;
50
+ //# sourceMappingURL=global-loader.d.ts.map
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Global configuration file loader.
3
+ * Loads and saves the global syrin.yaml file at ~/.syrin/syrin.yaml
4
+ */
5
+ import * as fs from 'fs';
6
+ import { load, dump } from 'js-yaml';
7
+ import { validateGlobalConfig } from './schema.js';
8
+ import { ConfigurationError } from '../utils/errors.js';
9
+ import { Paths, Messages } from '../constants/index.js';
10
+ import { getDefaultAgentName } from './types.js';
11
+ import { makeSyrinVersion, makeAgentName } from '../types/factories.js';
12
+ /**
13
+ * Get the path to the global Syrin directory.
14
+ * @returns Absolute path to ~/.syrin/
15
+ */
16
+ export function getGlobalSyrinDir() {
17
+ return Paths.GLOBAL_SYRIN_DIR;
18
+ }
19
+ /**
20
+ * Get the path to the global configuration file.
21
+ * @returns Absolute path to ~/.syrin/syrin.yaml
22
+ */
23
+ export function getGlobalConfigPath() {
24
+ return Paths.GLOBAL_CONFIG_FILE;
25
+ }
26
+ /**
27
+ * Get the path to the global environment file.
28
+ * @returns Absolute path to ~/.syrin/.env
29
+ */
30
+ export function getGlobalEnvPath() {
31
+ return Paths.GLOBAL_ENV_FILE;
32
+ }
33
+ /**
34
+ * Ensure the global Syrin directory exists.
35
+ * Creates ~/.syrin/ if it doesn't exist.
36
+ */
37
+ export function ensureGlobalSyrinDir() {
38
+ const dir = getGlobalSyrinDir();
39
+ if (!fs.existsSync(dir)) {
40
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ }
42
+ }
43
+ /**
44
+ * Check if global configuration file exists.
45
+ * @returns true if global config file exists
46
+ */
47
+ export function globalConfigExists() {
48
+ return fs.existsSync(getGlobalConfigPath());
49
+ }
50
+ /**
51
+ * Load global configuration from ~/.syrin/syrin.yaml file (raw, no validation).
52
+ * @returns Raw config data, or null if file doesn't exist
53
+ */
54
+ function loadGlobalConfigRaw() {
55
+ const configPath = getGlobalConfigPath();
56
+ if (!fs.existsSync(configPath)) {
57
+ return null;
58
+ }
59
+ try {
60
+ const configContent = fs.readFileSync(configPath, 'utf-8');
61
+ const configData = load(configContent);
62
+ if (!configData || typeof configData !== 'object') {
63
+ return null;
64
+ }
65
+ return configData;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Load global configuration from ~/.syrin/syrin.yaml file.
73
+ * @param skipValidation - If true, skip validation (for editing operations)
74
+ * @returns Loaded and validated global configuration, or null if file doesn't exist
75
+ * @throws {ConfigurationError} If config file exists but is invalid
76
+ */
77
+ export function loadGlobalConfig(skipValidation = false) {
78
+ const configPath = getGlobalConfigPath();
79
+ // Check if config file exists
80
+ if (!fs.existsSync(configPath)) {
81
+ return null;
82
+ }
83
+ try {
84
+ // Read and parse YAML file
85
+ const configContent = fs.readFileSync(configPath, 'utf-8');
86
+ const configData = load(configContent);
87
+ if (!configData || typeof configData !== 'object') {
88
+ throw new ConfigurationError(Messages.ERROR_CONFIG_EMPTY_OR_INVALID, {
89
+ context: { configPath },
90
+ });
91
+ }
92
+ // Skip validation if requested (for editing operations)
93
+ if (skipValidation) {
94
+ const data = configData;
95
+ // Sanitize version: coerce to string and use makeSyrinVersion or default
96
+ let version;
97
+ if (typeof data.version === 'string' && data.version.length > 0) {
98
+ version = makeSyrinVersion(data.version);
99
+ }
100
+ else {
101
+ version = makeSyrinVersion('1.0');
102
+ }
103
+ // Sanitize agent_name: coerce to string and use makeAgentName or default
104
+ let agent_name;
105
+ if (typeof data.agent_name === 'string' && data.agent_name.length > 0) {
106
+ agent_name = makeAgentName(data.agent_name);
107
+ }
108
+ else {
109
+ agent_name = makeAgentName(getDefaultAgentName());
110
+ }
111
+ // Sanitize llm: ensure it's an object or fallback to empty object
112
+ let llm;
113
+ if (typeof data.llm === 'object' &&
114
+ data.llm !== null &&
115
+ !Array.isArray(data.llm)) {
116
+ llm = data.llm;
117
+ }
118
+ else {
119
+ llm = {};
120
+ }
121
+ // Return sanitized structure (not using 'as GlobalSyrinConfig' to avoid unsafe cast)
122
+ return {
123
+ version,
124
+ project_name: 'GlobalSyrin',
125
+ agent_name,
126
+ llm,
127
+ };
128
+ }
129
+ // Validate and transform to opaque types
130
+ const validatedConfig = validateGlobalConfig(configData);
131
+ return validatedConfig;
132
+ }
133
+ catch (error) {
134
+ if (error instanceof ConfigurationError && !skipValidation) {
135
+ throw error;
136
+ }
137
+ if (skipValidation) {
138
+ // For editing, return a basic structure even if invalid
139
+ const raw = loadGlobalConfigRaw();
140
+ if (raw) {
141
+ // Sanitize version
142
+ let version;
143
+ if (typeof raw.version === 'string' && raw.version.length > 0) {
144
+ version = makeSyrinVersion(raw.version);
145
+ }
146
+ else {
147
+ version = makeSyrinVersion('1.0');
148
+ }
149
+ // Sanitize agent_name
150
+ let agent_name;
151
+ if (typeof raw.agent_name === 'string' && raw.agent_name.length > 0) {
152
+ agent_name = makeAgentName(raw.agent_name);
153
+ }
154
+ else {
155
+ agent_name = makeAgentName(getDefaultAgentName());
156
+ }
157
+ // Sanitize llm
158
+ let llm;
159
+ if (typeof raw.llm === 'object' &&
160
+ raw.llm !== null &&
161
+ !Array.isArray(raw.llm)) {
162
+ llm = raw.llm;
163
+ }
164
+ else {
165
+ llm = {};
166
+ }
167
+ return {
168
+ version,
169
+ project_name: 'GlobalSyrin',
170
+ agent_name,
171
+ llm,
172
+ };
173
+ }
174
+ return {
175
+ version: makeSyrinVersion('1.0'),
176
+ project_name: 'GlobalSyrin',
177
+ agent_name: makeAgentName(getDefaultAgentName()),
178
+ llm: {},
179
+ };
180
+ }
181
+ throw new ConfigurationError(`${Messages.ERROR_LOADING_CONFIG}: ${error instanceof Error ? error.message : String(error)}`, {
182
+ cause: error instanceof Error ? error : new Error(String(error)),
183
+ context: { configPath },
184
+ });
185
+ }
186
+ }
187
+ /**
188
+ * Save global configuration to ~/.syrin/syrin.yaml file.
189
+ * @param config - Global configuration to save
190
+ * @param _skipValidation - Skip validation (for editing operations) - TODO: Implement validation skip logic when needed for future editing operations
191
+ * @throws {ConfigurationError} If saving fails
192
+ */
193
+ export function saveGlobalConfig(config, _skipValidation = false) {
194
+ // TODO: When implementing validation skip, use _skipValidation to bypass
195
+ // validateGlobalConfig() before saving. Currently always validates.
196
+ const configPath = getGlobalConfigPath();
197
+ try {
198
+ // Ensure directory exists
199
+ ensureGlobalSyrinDir();
200
+ // Convert config to plain object for YAML serialization
201
+ const configObject = {
202
+ version: config.version,
203
+ project_name: config.project_name,
204
+ agent_name: config.agent_name,
205
+ llm: Object.fromEntries(Object.entries(config.llm).map(([key, provider]) => [
206
+ key,
207
+ {
208
+ API_KEY: provider.API_KEY,
209
+ MODEL_NAME: provider.MODEL_NAME,
210
+ default: provider.default,
211
+ },
212
+ ])),
213
+ };
214
+ // Serialize to YAML
215
+ const yamlContent = dump(configObject, {
216
+ indent: 2,
217
+ lineWidth: 100,
218
+ quotingType: '"',
219
+ });
220
+ // Write to file
221
+ fs.writeFileSync(configPath, yamlContent, 'utf-8');
222
+ // Set secure permissions (read/write owner only)
223
+ fs.chmodSync(configPath, 0o600);
224
+ }
225
+ catch (error) {
226
+ throw new ConfigurationError(`Failed to save global configuration: ${error instanceof Error ? error.message : String(error)}`, {
227
+ cause: error instanceof Error ? error : new Error(String(error)),
228
+ context: { configPath },
229
+ });
230
+ }
231
+ }
232
+ /**
233
+ * Create a default global configuration.
234
+ * @returns Default global configuration with system username as agent name
235
+ */
236
+ export function createDefaultGlobalConfig() {
237
+ return {
238
+ version: makeSyrinVersion('1.0'),
239
+ project_name: 'GlobalSyrin',
240
+ agent_name: makeAgentName(getDefaultAgentName()),
241
+ llm: {},
242
+ };
243
+ }
244
+ //# sourceMappingURL=global-loader.js.map
@@ -3,6 +3,7 @@
3
3
  * Loads and parses the syrin.yaml file.
4
4
  */
5
5
  import type { SyrinConfig } from './types.js';
6
+ import { type CLIConfigFlags } from './merger.js';
6
7
  /**
7
8
  * Load configuration from syrin.yaml file.
8
9
  * @param projectRoot - Root directory of the project (defaults to current working directory)
@@ -16,4 +17,31 @@ export declare function loadConfig(projectRoot?: string): SyrinConfig;
16
17
  * @returns true if config file exists
17
18
  */
18
19
  export declare function configExists(projectRoot?: string): boolean;
20
+ /**
21
+ * Load configuration from syrin.yaml file (optional).
22
+ * Returns null if file doesn't exist instead of throwing.
23
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
24
+ * @returns Loaded and validated configuration, or null if file doesn't exist
25
+ * @throws {ConfigurationError} If config file exists but is invalid
26
+ */
27
+ export declare function loadConfigOptional(projectRoot?: string): SyrinConfig | null;
28
+ /**
29
+ * Load configuration with global fallback.
30
+ * Tries local config first, then global config, then throws error.
31
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
32
+ * @param flags - CLI flags that can override config values
33
+ * @returns Loaded and merged configuration
34
+ * @throws {Error} If neither local nor global config exists
35
+ */
36
+ export declare function loadConfigWithGlobal(projectRoot?: string, flags?: CLIConfigFlags): {
37
+ config: SyrinConfig;
38
+ source: 'local' | 'global';
39
+ };
40
+ /**
41
+ * Save local configuration to syrin.yaml file.
42
+ * @param config - Configuration to save
43
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
44
+ * @throws {ConfigurationError} If saving fails
45
+ */
46
+ export declare function saveLocalConfig(config: SyrinConfig, projectRoot?: string): void;
19
47
  //# sourceMappingURL=loader.d.ts.map
@@ -4,10 +4,12 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { load } from 'js-yaml';
7
+ import { load, dump } from 'js-yaml';
8
8
  import { validateConfig } from './schema.js';
9
9
  import { ConfigurationError } from '../utils/errors.js';
10
10
  import { Paths, Messages, ToolCommands } from '../constants/index.js';
11
+ import { loadGlobalConfig } from './global-loader.js';
12
+ import { mergeConfigs, createConfigFromGlobal, } from './merger.js';
11
13
  /**
12
14
  * Load configuration from syrin.yaml file.
13
15
  * @param projectRoot - Root directory of the project (defaults to current working directory)
@@ -15,13 +17,37 @@ import { Paths, Messages, ToolCommands } from '../constants/index.js';
15
17
  * @throws {ConfigurationError} If config file is missing or invalid
16
18
  */
17
19
  export function loadConfig(projectRoot = process.cwd()) {
18
- const configPath = path.join(projectRoot, Paths.CONFIG_FILE);
19
- // Check if config file exists
20
- if (!fs.existsSync(configPath)) {
20
+ const config = loadConfigOptional(projectRoot);
21
+ if (config === null) {
22
+ const configPath = path.join(projectRoot, Paths.CONFIG_FILE);
21
23
  throw new ConfigurationError(Messages.ERROR_CONFIG_NOT_FOUND(ToolCommands.INIT), {
22
24
  context: { projectRoot, configPath },
23
25
  });
24
26
  }
27
+ return config;
28
+ }
29
+ /**
30
+ * Check if a configuration file exists.
31
+ * @param projectRoot - Root directory of the project
32
+ * @returns true if config file exists
33
+ */
34
+ export function configExists(projectRoot = process.cwd()) {
35
+ const configPath = path.join(projectRoot, Paths.CONFIG_FILE);
36
+ return fs.existsSync(configPath);
37
+ }
38
+ /**
39
+ * Load configuration from syrin.yaml file (optional).
40
+ * Returns null if file doesn't exist instead of throwing.
41
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
42
+ * @returns Loaded and validated configuration, or null if file doesn't exist
43
+ * @throws {ConfigurationError} If config file exists but is invalid
44
+ */
45
+ export function loadConfigOptional(projectRoot = process.cwd()) {
46
+ const configPath = path.join(projectRoot, Paths.CONFIG_FILE);
47
+ // Check if config file exists
48
+ if (!fs.existsSync(configPath)) {
49
+ return null;
50
+ }
25
51
  try {
26
52
  // Read and parse YAML file
27
53
  const configContent = fs.readFileSync(configPath, 'utf-8');
@@ -46,12 +72,72 @@ export function loadConfig(projectRoot = process.cwd()) {
46
72
  }
47
73
  }
48
74
  /**
49
- * Check if a configuration file exists.
50
- * @param projectRoot - Root directory of the project
51
- * @returns true if config file exists
75
+ * Load configuration with global fallback.
76
+ * Tries local config first, then global config, then throws error.
77
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
78
+ * @param flags - CLI flags that can override config values
79
+ * @returns Loaded and merged configuration
80
+ * @throws {Error} If neither local nor global config exists
52
81
  */
53
- export function configExists(projectRoot = process.cwd()) {
82
+ export function loadConfigWithGlobal(projectRoot = process.cwd(), flags = {}) {
83
+ const localConfig = loadConfigOptional(projectRoot);
84
+ const globalConfig = loadGlobalConfig();
85
+ if (localConfig) {
86
+ return { config: mergeConfigs(localConfig, globalConfig), source: 'local' };
87
+ }
88
+ if (globalConfig) {
89
+ const adaptedConfig = createConfigFromGlobal(globalConfig, flags);
90
+ return { config: adaptedConfig, source: 'global' };
91
+ }
92
+ throw new ConfigurationError(Messages.ERROR_CONFIG_NOT_FOUND(ToolCommands.INIT), {
93
+ context: { projectRoot },
94
+ });
95
+ }
96
+ /**
97
+ * Save local configuration to syrin.yaml file.
98
+ * @param config - Configuration to save
99
+ * @param projectRoot - Root directory of the project (defaults to current working directory)
100
+ * @throws {ConfigurationError} If saving fails
101
+ */
102
+ export function saveLocalConfig(config, projectRoot = process.cwd()) {
54
103
  const configPath = path.join(projectRoot, Paths.CONFIG_FILE);
55
- return fs.existsSync(configPath);
104
+ try {
105
+ // Convert config to plain object for YAML serialization
106
+ const configObject = {
107
+ version: config.version,
108
+ project_name: config.project_name,
109
+ agent_name: config.agent_name,
110
+ transport: config.transport,
111
+ ...(config.mcp_url ? { mcp_url: config.mcp_url } : {}),
112
+ ...(config.script ? { script: config.script } : {}),
113
+ llm: Object.fromEntries(Object.entries(config.llm).map(([key, provider]) => [
114
+ key,
115
+ {
116
+ ...(provider.API_KEY !== undefined
117
+ ? { API_KEY: provider.API_KEY }
118
+ : {}),
119
+ ...(provider.MODEL_NAME !== undefined
120
+ ? { MODEL_NAME: provider.MODEL_NAME }
121
+ : {}),
122
+ default: provider.default,
123
+ },
124
+ ])),
125
+ ...(config.check ? { check: config.check } : {}),
126
+ };
127
+ // Serialize to YAML
128
+ const yamlContent = dump(configObject, {
129
+ indent: 2,
130
+ lineWidth: 100,
131
+ quotingType: '"',
132
+ });
133
+ // Write to file
134
+ fs.writeFileSync(configPath, yamlContent, 'utf-8');
135
+ }
136
+ catch (error) {
137
+ throw new ConfigurationError(`Failed to save local configuration: ${error instanceof Error ? error.message : String(error)}`, {
138
+ cause: error instanceof Error ? error : new Error(String(error)),
139
+ context: { configPath },
140
+ });
141
+ }
56
142
  }
57
143
  //# sourceMappingURL=loader.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Configuration merger.
3
+ * Merges local and global configurations with proper precedence.
4
+ */
5
+ import type { SyrinConfig, GlobalSyrinConfig } from './types.js';
6
+ /**
7
+ * CLI flags that can override config values.
8
+ */
9
+ export interface CLIConfigFlags {
10
+ /** Transport type override */
11
+ transport?: 'stdio' | 'http';
12
+ /** MCP URL override (for http transport) */
13
+ mcp_url?: string;
14
+ /** Script command override (for stdio transport) */
15
+ script?: string;
16
+ /** LLM provider override */
17
+ llm?: string;
18
+ }
19
+ /**
20
+ * Merge local and global configurations.
21
+ * Local LLM providers override global, but other global providers are kept.
22
+ * @param local - Local configuration (from ./syrin.yaml) or null
23
+ * @param global - Global configuration (from ~/.syrin/syrin.yaml) or null
24
+ * @param flags - CLI flags that override config values
25
+ * @returns Merged configuration
26
+ * @throws {Error} If neither local nor global config exists
27
+ */
28
+ export declare function mergeConfigs(local: SyrinConfig | null, global: GlobalSyrinConfig | null, flags?: CLIConfigFlags): SyrinConfig;
29
+ /**
30
+ * Create a full SyrinConfig from global config and CLI flags.
31
+ * @param global - Global configuration
32
+ * @param flags - CLI flags that provide required fields
33
+ * @returns Full configuration
34
+ * @throws {Error} If required fields are missing
35
+ */
36
+ export declare function createConfigFromGlobal(global: GlobalSyrinConfig, flags?: CLIConfigFlags): SyrinConfig;
37
+ //# sourceMappingURL=merger.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Configuration merger.
3
+ * Merges local and global configurations with proper precedence.
4
+ */
5
+ import { makeProjectName, makeMCPURL, makeScriptCommand, } from '../types/factories.js';
6
+ /**
7
+ * Merge local and global configurations.
8
+ * Local LLM providers override global, but other global providers are kept.
9
+ * @param local - Local configuration (from ./syrin.yaml) or null
10
+ * @param global - Global configuration (from ~/.syrin/syrin.yaml) or null
11
+ * @param flags - CLI flags that override config values
12
+ * @returns Merged configuration
13
+ * @throws {Error} If neither local nor global config exists
14
+ */
15
+ export function mergeConfigs(local, global, flags = {}) {
16
+ // If local config exists, use it as base and merge LLM providers
17
+ if (local) {
18
+ const mergedLLM = {
19
+ ...(global?.llm || {}), // Start with global LLM providers
20
+ ...local.llm, // Override with local LLM providers
21
+ };
22
+ // Apply CLI flag overrides
23
+ const finalConfig = {
24
+ ...local,
25
+ llm: mergedLLM,
26
+ transport: flags.transport || local.transport,
27
+ mcp_url: flags.mcp_url ? makeMCPURL(flags.mcp_url) : local.mcp_url,
28
+ script: flags.script ? makeScriptCommand(flags.script) : local.script,
29
+ };
30
+ return finalConfig;
31
+ }
32
+ // No local config - create from global with defaults
33
+ if (!global) {
34
+ throw new Error('No configuration found. Create a local config with `syrin init` or set up global config with `syrin init --global`.');
35
+ }
36
+ // Create config from global
37
+ return createConfigFromGlobal(global, flags);
38
+ }
39
+ /**
40
+ * Create a full SyrinConfig from global config and CLI flags.
41
+ * @param global - Global configuration
42
+ * @param flags - CLI flags that provide required fields
43
+ * @returns Full configuration
44
+ * @throws {Error} If required fields are missing
45
+ */
46
+ export function createConfigFromGlobal(global, flags = {}) {
47
+ // Transport is required - must come from flags
48
+ if (!flags.transport) {
49
+ throw new Error('Transport type is required when using global config. Use --transport <stdio|http> flag.');
50
+ }
51
+ // Validate transport-specific requirements
52
+ if (flags.transport === 'http' && !flags.mcp_url) {
53
+ throw new Error('MCP URL is required for HTTP transport. Use --url <url> flag.');
54
+ }
55
+ if (flags.transport === 'stdio' && !flags.script) {
56
+ throw new Error('Script command is required for stdio transport. Use --script <command> flag.');
57
+ }
58
+ return {
59
+ version: global.version,
60
+ project_name: makeProjectName('GlobalSyrin'),
61
+ agent_name: global.agent_name,
62
+ transport: flags.transport,
63
+ mcp_url: flags.mcp_url ? makeMCPURL(flags.mcp_url) : undefined,
64
+ script: flags.script ? makeScriptCommand(flags.script) : undefined,
65
+ llm: global.llm,
66
+ };
67
+ }
68
+ //# sourceMappingURL=merger.js.map
@@ -3,7 +3,7 @@
3
3
  * Provides runtime validation for the Syrin configuration file.
4
4
  */
5
5
  import { z } from 'zod';
6
- import type { SyrinConfig } from '../config/types.js';
6
+ import type { SyrinConfig, GlobalSyrinConfig } from '../config/types.js';
7
7
  /**
8
8
  * Main configuration schema.
9
9
  */
@@ -38,5 +38,30 @@ export declare const ConfigSchema: z.ZodObject<{
38
38
  * Type inference from schema.
39
39
  */
40
40
  export type ConfigSchemaType = z.infer<typeof ConfigSchema>;
41
+ /**
42
+ * Global configuration schema (LLM settings only).
43
+ * No transport, mcp_url, or script fields.
44
+ */
45
+ export declare const GlobalConfigSchema: z.ZodObject<{
46
+ version: z.ZodString;
47
+ project_name: z.ZodLiteral<"GlobalSyrin">;
48
+ agent_name: z.ZodString;
49
+ llm: z.ZodRecord<z.ZodString, z.ZodObject<{
50
+ API_KEY: z.ZodOptional<z.ZodString>;
51
+ MODEL_NAME: z.ZodOptional<z.ZodString>;
52
+ default: z.ZodOptional<z.ZodBoolean>;
53
+ }, z.core.$strip>>;
54
+ }, z.core.$strip>;
55
+ /**
56
+ * Type inference from global schema.
57
+ */
58
+ export type GlobalConfigSchemaType = z.infer<typeof GlobalConfigSchema>;
41
59
  export declare function validateConfig(config: unknown): SyrinConfig;
60
+ /**
61
+ * Validate a global configuration object against the schema.
62
+ * @param config - Global configuration object to validate
63
+ * @returns Validated global configuration with opaque types
64
+ * @throws {ConfigurationError} If validation fails
65
+ */
66
+ export declare function validateGlobalConfig(config: unknown): GlobalSyrinConfig;
42
67
  //# sourceMappingURL=schema.d.ts.map