@duckmind/deepquark-darwin-arm64 0.9.83 → 0.9.88

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 (70) hide show
  1. package/.deepquark/skills/bundled/knowledge-graph/SKILL.md +385 -0
  2. package/.deepquark/skills/bundled/knowledge-graph/STANDARDS.md +461 -0
  3. package/.deepquark/skills/bundled/knowledge-graph/lib/cli.ts +588 -0
  4. package/.deepquark/skills/bundled/knowledge-graph/lib/config.ts +630 -0
  5. package/.deepquark/skills/bundled/knowledge-graph/lib/connection-profile.ts +629 -0
  6. package/.deepquark/skills/bundled/knowledge-graph/lib/container.ts +756 -0
  7. package/.deepquark/skills/bundled/knowledge-graph/lib/mcp-client.ts +1310 -0
  8. package/.deepquark/skills/bundled/knowledge-graph/lib/output-formatter.ts +997 -0
  9. package/.deepquark/skills/bundled/knowledge-graph/lib/token-metrics.ts +335 -0
  10. package/.deepquark/skills/bundled/knowledge-graph/lib/transformation-log.ts +137 -0
  11. package/.deepquark/skills/bundled/knowledge-graph/lib/wrapper-config.ts +113 -0
  12. package/.deepquark/skills/bundled/knowledge-graph/server/.env.example +129 -0
  13. package/.deepquark/skills/bundled/knowledge-graph/server/compare-embeddings.ts +175 -0
  14. package/.deepquark/skills/bundled/knowledge-graph/server/config-falkordb.yaml +108 -0
  15. package/.deepquark/skills/bundled/knowledge-graph/server/config-neo4j.yaml +111 -0
  16. package/.deepquark/skills/bundled/knowledge-graph/server/diagnose.ts +483 -0
  17. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb-dev.yml +146 -0
  18. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb.yml +151 -0
  19. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev-local.yml +161 -0
  20. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev.yml +161 -0
  21. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j.yml +169 -0
  22. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-production.yml +128 -0
  23. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-test.yml +10 -0
  24. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose.yml +84 -0
  25. package/.deepquark/skills/bundled/knowledge-graph/server/entrypoint.sh +40 -0
  26. package/.deepquark/skills/bundled/knowledge-graph/server/install.ts +2054 -0
  27. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-falkordb.yml +78 -0
  28. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-neo4j.yml +88 -0
  29. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose.yml +83 -0
  30. package/.deepquark/skills/bundled/knowledge-graph/server/test-all-llms-mcp.ts +387 -0
  31. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-models.ts +201 -0
  32. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-providers.ts +641 -0
  33. package/.deepquark/skills/bundled/knowledge-graph/server/test-graphiti-model.ts +217 -0
  34. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-correct.ts +141 -0
  35. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-llms-mcp.ts +386 -0
  36. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-models.ts +173 -0
  37. package/.deepquark/skills/bundled/knowledge-graph/server/test-llama-extraction.ts +188 -0
  38. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-final.ts +240 -0
  39. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-live.ts +187 -0
  40. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-session.ts +127 -0
  41. package/.deepquark/skills/bundled/knowledge-graph/server/test-model-combinations.ts +316 -0
  42. package/.deepquark/skills/bundled/knowledge-graph/server/test-ollama-models.ts +228 -0
  43. package/.deepquark/skills/bundled/knowledge-graph/server/test-openrouter-models.ts +460 -0
  44. package/.deepquark/skills/bundled/knowledge-graph/server/test-real-life-mcp.ts +311 -0
  45. package/.deepquark/skills/bundled/knowledge-graph/server/test-search-debug.ts +199 -0
  46. package/.deepquark/skills/bundled/knowledge-graph/tools/Install.md +104 -0
  47. package/.deepquark/skills/bundled/knowledge-graph/tools/README.md +120 -0
  48. package/.deepquark/skills/bundled/knowledge-graph/tools/knowledge-cli.ts +996 -0
  49. package/.deepquark/skills/bundled/knowledge-graph/tools/server-cli.ts +531 -0
  50. package/.deepquark/skills/bundled/knowledge-graph/workflows/BulkImport.md +514 -0
  51. package/.deepquark/skills/bundled/knowledge-graph/workflows/CaptureEpisode.md +242 -0
  52. package/.deepquark/skills/bundled/knowledge-graph/workflows/ClearGraph.md +392 -0
  53. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetRecent.md +352 -0
  54. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetStatus.md +373 -0
  55. package/.deepquark/skills/bundled/knowledge-graph/workflows/HealthReport.md +212 -0
  56. package/.deepquark/skills/bundled/knowledge-graph/workflows/InvestigateEntity.md +142 -0
  57. package/.deepquark/skills/bundled/knowledge-graph/workflows/OntologyManagement.md +201 -0
  58. package/.deepquark/skills/bundled/knowledge-graph/workflows/RunMaintenance.md +302 -0
  59. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchByDate.md +255 -0
  60. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchFacts.md +382 -0
  61. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchKnowledge.md +374 -0
  62. package/.deepquark/skills/bundled/knowledge-graph/workflows/StixImport.md +212 -0
  63. package/bin/deepquark +0 -0
  64. package/package.json +1 -1
  65. package/.deepquark/skills/bundled/ge-payroll/SKILL.md +0 -153
  66. package/.deepquark/skills/bundled/ge-payroll/evals/evals.json +0 -23
  67. package/.deepquark/skills/bundled/ge-payroll/references/pain-points-improvements.md +0 -106
  68. package/.deepquark/skills/bundled/ge-payroll/references/process-detail.md +0 -217
  69. package/.deepquark/skills/bundled/ge-payroll/references/raci-stakeholders.md +0 -85
  70. package/.deepquark/skills/bundled/ge-payroll/references/timeline-mandays.md +0 -64
@@ -0,0 +1,629 @@
1
+ /**
2
+ * Connection Profile Management Library
3
+ *
4
+ * Manages connection profiles for remote MCP server access.
5
+ * Loads and validates profiles from YAML configuration files.
6
+ *
7
+ * Profile file locations:
8
+ * - $PAI_DIR/config/knowledge-profiles.yaml (priority)
9
+ * - ~/.claude/config/knowledge-profiles.yaml (fallback)
10
+ *
11
+ * @module connection-profile
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
15
+ import { resolve } from 'node:path';
16
+ import yaml from 'js-yaml';
17
+
18
+ /**
19
+ * Connection profile configuration
20
+ */
21
+ export interface ConnectionProfileData {
22
+ /** Unique profile identifier */
23
+ name: string;
24
+ /** Hostname or IP address */
25
+ host: string;
26
+ /** TCP port */
27
+ port: number;
28
+ /** Protocol: http or https */
29
+ protocol: 'http' | 'https';
30
+ /** URL path prefix (default: /mcp) */
31
+ basePath?: string;
32
+ /** Request timeout in milliseconds (default: 30000) */
33
+ timeout?: number;
34
+ /** TLS configuration (required for https) */
35
+ tls?: TLSConfig;
36
+ }
37
+
38
+ /**
39
+ * TLS/SSL configuration for HTTPS connections
40
+ */
41
+ export interface TLSConfig {
42
+ /** Enable certificate verification (default: true) */
43
+ verify?: boolean;
44
+ /** Path to CA certificate file (PEM format) */
45
+ ca?: string;
46
+ /** Path to client certificate file (PEM format) */
47
+ cert?: string;
48
+ /** Path to client private key file (PEM format) */
49
+ key?: string;
50
+ /** Minimum TLS protocol version (default: TLSv1.2) */
51
+ minVersion?: 'TLSv1.2' | 'TLSv1.3';
52
+ }
53
+
54
+ /**
55
+ * Profile configuration file structure
56
+ */
57
+ export interface ProfileConfigFile {
58
+ /** Config version */
59
+ version: string;
60
+ /** Default profile name */
61
+ default_profile: string;
62
+ /** Profile definitions */
63
+ profiles: Record<string, Omit<ConnectionProfileData, 'name'>>;
64
+ }
65
+
66
+ /**
67
+ * Runtime connection state
68
+ */
69
+ export interface ConnectionState {
70
+ /** Active profile name */
71
+ profile: string;
72
+ /** Connection status */
73
+ status: 'connected' | 'disconnected' | 'error' | 'unknown';
74
+ /** Last successful connection time */
75
+ lastConnected?: Date;
76
+ /** Last error message */
77
+ lastError?: string;
78
+ /** MCP server version */
79
+ serverVersion?: string;
80
+ /** Connected host */
81
+ host?: string;
82
+ /** Connected port */
83
+ port?: number;
84
+ /** Connection protocol */
85
+ protocol?: string;
86
+ }
87
+
88
+ /**
89
+ * Profile validation result
90
+ */
91
+ export interface ProfileValidationResult {
92
+ /** Whether profile is valid */
93
+ valid: boolean;
94
+ /** Array of validation error messages */
95
+ errors: string[];
96
+ }
97
+
98
+ /**
99
+ * Connection Profile Manager
100
+ *
101
+ * Loads and validates connection profiles from YAML files.
102
+ */
103
+ export class ConnectionProfileManager {
104
+ private configPath: string | null = null;
105
+ private configFile: ProfileConfigFile | null = null;
106
+
107
+ /**
108
+ * Find the profile configuration file
109
+ * Checks $PAI_DIR/config first, then ~/.claude/config
110
+ */
111
+ private findConfigFile(): string | null {
112
+ // Check $PAI_DIR/config first (priority)
113
+ const paiDir = process.env.PAI_DIR;
114
+ if (paiDir) {
115
+ const paiConfigPath = resolve(paiDir, 'config', 'knowledge-profiles.yaml');
116
+ if (existsSync(paiConfigPath)) {
117
+ return paiConfigPath;
118
+ }
119
+ }
120
+
121
+ // Fallback to ~/.claude/config
122
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
123
+ if (homeDir) {
124
+ const claudeConfigPath = resolve(homeDir, '.claude', 'config', 'knowledge-profiles.yaml');
125
+ if (existsSync(claudeConfigPath)) {
126
+ return claudeConfigPath;
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Load the configuration file
135
+ */
136
+ private loadConfigFile(): void {
137
+ if (this.configFile !== null) {
138
+ return; // Already loaded
139
+ }
140
+
141
+ const configPath = this.findConfigFile();
142
+ if (!configPath) {
143
+ this.configFile = null;
144
+ this.configPath = null;
145
+ return;
146
+ }
147
+
148
+ this.configPath = configPath;
149
+ try {
150
+ const content = readFileSync(configPath, 'utf-8');
151
+ this.configFile = yaml.load(content) as ProfileConfigFile;
152
+ } catch (_error) {
153
+ // Treat malformed YAML as missing config - fall back to defaults
154
+ this.configFile = null;
155
+ this.configPath = null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * List all available profile names
161
+ * @returns Array of profile names
162
+ */
163
+ listProfiles(): string[] {
164
+ this.loadConfigFile();
165
+
166
+ if (!this.configFile) {
167
+ return [];
168
+ }
169
+
170
+ return Object.keys(this.configFile.profiles);
171
+ }
172
+
173
+ /**
174
+ * Get the default profile name from config
175
+ * @returns Default profile name or 'default'
176
+ */
177
+ getDefaultProfile(): string {
178
+ this.loadConfigFile();
179
+
180
+ if (!this.configFile) {
181
+ return 'default';
182
+ }
183
+
184
+ return this.configFile.default_profile || 'default';
185
+ }
186
+
187
+ /**
188
+ * Load a profile by name
189
+ * @param profileName - Profile name to load
190
+ * @returns Profile configuration or null if not found
191
+ */
192
+ loadProfile(profileName: string): ConnectionProfileData | null {
193
+ this.loadConfigFile();
194
+
195
+ if (!this.configFile) {
196
+ return null;
197
+ }
198
+
199
+ const profileData = this.configFile.profiles[profileName];
200
+ if (!profileData) {
201
+ return null;
202
+ }
203
+
204
+ return {
205
+ name: profileName,
206
+ ...profileData,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Load a profile with helpful error if not found
212
+ * @param profileName - Profile name to load
213
+ * @returns Profile configuration
214
+ * @throws Error if profile not found with list of available profiles
215
+ */
216
+ loadProfileOrThrow(profileName: string): ConnectionProfileData {
217
+ this.loadConfigFile();
218
+
219
+ if (!this.configFile) {
220
+ throw new Error(
221
+ `Profile '${profileName}' not found. No configuration file found.\n\n` +
222
+ `Expected one of:\n` +
223
+ ` - $PAI_DIR/config/knowledge-profiles.yaml\n` +
224
+ ` - ~/.claude/config/knowledge-profiles.yaml`
225
+ );
226
+ }
227
+
228
+ const profileData = this.configFile.profiles[profileName];
229
+ if (!profileData) {
230
+ const available = this.listProfiles();
231
+ throw new Error(
232
+ `Profile '${profileName}' not found.\n\n` +
233
+ `Available profiles: ${available.length > 0 ? available.join(', ') : '(none)'}\n` +
234
+ `Default profile: ${this.getDefaultProfile()}`
235
+ );
236
+ }
237
+
238
+ return {
239
+ name: profileName,
240
+ ...profileData,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Validate profile configuration
246
+ * @param profile - Profile to validate
247
+ * @returns Validation result with errors if invalid
248
+ */
249
+ validateProfile(profile: ConnectionProfileData): ProfileValidationResult {
250
+ const errors: string[] = [];
251
+
252
+ // Validate name
253
+ if (!profile.name || typeof profile.name !== 'string') {
254
+ errors.push('Profile name is required and must be a string');
255
+ } else if (!/^[a-zA-Z0-9_-]+$/.test(profile.name)) {
256
+ errors.push('Profile name must contain only alphanumeric characters, hyphens, and underscores');
257
+ }
258
+
259
+ // Validate host
260
+ if (!profile.host || typeof profile.host !== 'string') {
261
+ errors.push('Host is required and must be a string');
262
+ } else {
263
+ // Basic hostname validation (allows IP addresses and hostnames)
264
+ const hostPattern = /^[a-zA-Z0-9.-]+$/;
265
+ if (!hostPattern.test(profile.host)) {
266
+ errors.push('Host must be a valid hostname or IP address');
267
+ }
268
+ }
269
+
270
+ // Validate port
271
+ if (!profile.port || typeof profile.port !== 'number') {
272
+ errors.push('Port is required and must be a number');
273
+ } else if (profile.port < 1 || profile.port > 65535) {
274
+ errors.push('Port must be between 1 and 65535');
275
+ }
276
+
277
+ // Validate protocol
278
+ if (profile.protocol !== 'http' && profile.protocol !== 'https') {
279
+ errors.push('Protocol must be either "http" or "https"');
280
+ }
281
+
282
+ // Validate timeout if provided
283
+ if (profile.timeout !== undefined) {
284
+ if (typeof profile.timeout !== 'number') {
285
+ errors.push('Timeout must be a number');
286
+ } else if (profile.timeout <= 0) {
287
+ errors.push('Timeout must be greater than 0');
288
+ }
289
+ }
290
+
291
+ // Validate TLS if protocol is https
292
+ if (profile.protocol === 'https' && !profile.tls) {
293
+ errors.push('TLS configuration is required when using HTTPS protocol');
294
+ }
295
+
296
+ // Validate TLS config if provided
297
+ if (profile.tls) {
298
+ if (profile.tls.ca && typeof profile.tls.ca !== 'string') {
299
+ errors.push('TLS CA path must be a string');
300
+ }
301
+ if (profile.tls.cert && typeof profile.tls.cert !== 'string') {
302
+ errors.push('TLS cert path must be a string');
303
+ }
304
+ if (profile.tls.key && typeof profile.tls.key !== 'string') {
305
+ errors.push('TLS key path must be a string');
306
+ }
307
+ // Mutual TLS: both cert and key required
308
+ if ((profile.tls.cert && !profile.tls.key) || (!profile.tls.cert && profile.tls.key)) {
309
+ errors.push('Both TLS cert and key must be provided for mutual TLS');
310
+ }
311
+ if (profile.tls.minVersion && profile.tls.minVersion !== 'TLSv1.2' && profile.tls.minVersion !== 'TLSv1.3') {
312
+ errors.push('TLS minVersion must be either "TLSv1.2" or "TLSv1.3"');
313
+ }
314
+ }
315
+
316
+ return {
317
+ valid: errors.length === 0,
318
+ errors,
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Get the config file path (for debugging)
324
+ */
325
+ getConfigPath(): string | null {
326
+ this.loadConfigFile();
327
+ return this.configPath;
328
+ }
329
+
330
+ /**
331
+ * Save or update a profile in the configuration file
332
+ * Creates the config file if it doesn't exist
333
+ * @param profile - Profile to save (name is included in the profile object)
334
+ * @param makeDefault - Whether to make this the default profile (default: false for new profiles, true when updating default)
335
+ * @returns The path to the config file that was created/updated
336
+ */
337
+ saveProfile(profile: ConnectionProfileData, makeDefault: boolean = false): string {
338
+ const { name, ...profileData } = profile;
339
+ const paiDir = process.env.PAI_DIR;
340
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
341
+
342
+ // Determine config directory: $PAI_DIR/config takes priority
343
+ let configDir: string;
344
+ if (paiDir) {
345
+ configDir = resolve(paiDir, 'config');
346
+ } else {
347
+ configDir = resolve(homeDir || '', '.claude', 'config');
348
+ }
349
+
350
+ // Ensure config directory exists
351
+ if (!existsSync(configDir)) {
352
+ mkdirSync(configDir, { recursive: true });
353
+ }
354
+
355
+ const configPath = resolve(configDir, 'knowledge-profiles.yaml');
356
+
357
+ // Load existing config or create new one
358
+ let configFile: ProfileConfigFile;
359
+ if (existsSync(configPath)) {
360
+ try {
361
+ const content = readFileSync(configPath, 'utf-8');
362
+ configFile = yaml.load(content) as ProfileConfigFile;
363
+ } catch (_error) {
364
+ // If parse fails, create new config
365
+ configFile = {
366
+ version: '1.0',
367
+ default_profile: name,
368
+ profiles: {},
369
+ };
370
+ }
371
+ } else {
372
+ // Create new config file
373
+ configFile = {
374
+ version: '1.0',
375
+ default_profile: name,
376
+ profiles: {},
377
+ };
378
+ }
379
+
380
+ // Update profile data
381
+ configFile.profiles[name] = profileData;
382
+
383
+ // Update default profile if requested
384
+ if (makeDefault) {
385
+ configFile.default_profile = name;
386
+ }
387
+
388
+ // Write back to file
389
+ const yamlContent = yaml.dump(configFile, { indent: 2, lineWidth: 120 });
390
+ writeFileSync(configPath, yamlContent, 'utf-8');
391
+
392
+ // Reload our cached config
393
+ this.configFile = null;
394
+ this.configPath = null;
395
+ this.loadConfigFile();
396
+
397
+ return configPath;
398
+ }
399
+
400
+ /**
401
+ * Create or update multiple profiles at once
402
+ * Useful for setting up both local and development profiles
403
+ * @param profiles - Array of profiles to save
404
+ * @param defaultProfileName - Which profile should be the default
405
+ * @returns The path to the config file that was created/updated
406
+ */
407
+ saveProfiles(profiles: ConnectionProfileData[], defaultProfileName: string): string {
408
+ const paiDir = process.env.PAI_DIR;
409
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
410
+
411
+ // Determine config directory: $PAI_DIR/config takes priority
412
+ let configDir: string;
413
+ if (paiDir) {
414
+ configDir = resolve(paiDir, 'config');
415
+ } else {
416
+ configDir = resolve(homeDir || '', '.claude', 'config');
417
+ }
418
+
419
+ // Ensure config directory exists
420
+ if (!existsSync(configDir)) {
421
+ mkdirSync(configDir, { recursive: true });
422
+ }
423
+
424
+ const configPath = resolve(configDir, 'knowledge-profiles.yaml');
425
+
426
+ // Create config file with all profiles
427
+ const configFile: ProfileConfigFile = {
428
+ version: '1.0',
429
+ default_profile: defaultProfileName,
430
+ profiles: {},
431
+ };
432
+
433
+ for (const profile of profiles) {
434
+ const { name, ...profileData } = profile;
435
+ configFile.profiles[name] = profileData;
436
+ }
437
+
438
+ // Write to file
439
+ const yamlContent = yaml.dump(configFile, { indent: 2, lineWidth: 120 });
440
+ writeFileSync(configPath, yamlContent, 'utf-8');
441
+
442
+ // Reload our cached config
443
+ this.configFile = null;
444
+ this.configPath = null;
445
+ this.loadConfigFile();
446
+
447
+ return configPath;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Environment variable to profile field mappings
453
+ */
454
+ const ENV_MAPPINGS: Record<string, keyof ConnectionProfileData | string> = {
455
+ MADEINOZ_KNOWLEDGE_HOST: 'host',
456
+ MADEINOZ_KNOWLEDGE_PORT: 'port',
457
+ MADEINOZ_KNOWLEDGE_PROTOCOL: 'protocol',
458
+ MADEINOZ_KNOWLEDGE_BASE_PATH: 'basePath',
459
+ MADEINOZ_KNOWLEDGE_TIMEOUT: 'timeout',
460
+ MADEINOZ_KNOWLEDGE_TLS_VERIFY: 'tls.verify',
461
+ MADEINOZ_KNOWLEDGE_TLS_CA: 'tls.ca',
462
+ MADEINOZ_KNOWLEDGE_TLS_CERT: 'tls.cert',
463
+ MADEINOZ_KNOWLEDGE_TLS_KEY: 'tls.key',
464
+ };
465
+
466
+ /**
467
+ * Parse environment variable value to appropriate type
468
+ */
469
+ function parseEnvValue(key: string, value: string): string | number | boolean {
470
+ if (key === 'MADEINOZ_KNOWLEDGE_PORT' || key === 'MADEINOZ_KNOWLEDGE_TIMEOUT') {
471
+ return Number.parseInt(value, 10);
472
+ }
473
+ if (key === 'MADEINOZ_KNOWLEDGE_TLS_VERIFY') {
474
+ return value.toLowerCase() === 'true' || value === '1';
475
+ }
476
+ return value;
477
+ }
478
+
479
+ /**
480
+ * Set nested object property using dot notation
481
+ * Guards against prototype pollution by validating key names
482
+ */
483
+ function setNestedProperty(obj: Record<string, unknown>, path: string, value: unknown): void {
484
+ // Dangerous properties that could lead to prototype pollution
485
+ const DANGEROUS_PROPERTIES = new Set([
486
+ '__proto__',
487
+ 'constructor',
488
+ 'prototype',
489
+ 'toString',
490
+ 'toLocaleString',
491
+ 'valueOf',
492
+ 'hasOwnProperty',
493
+ 'isPrototypeOf',
494
+ 'propertyIsEnumerable',
495
+ ]);
496
+
497
+ // Ensure we only ever traverse and mutate plain objects
498
+ const isPlainObject = (candidate: unknown): candidate is Record<string, unknown> => {
499
+ if (candidate === null || typeof candidate !== 'object') {
500
+ return false;
501
+ }
502
+ if (Object.prototype.toString.call(candidate) !== '[object Object]') {
503
+ return false;
504
+ }
505
+ const proto = Object.getPrototypeOf(candidate);
506
+ return proto === Object.prototype || proto === null;
507
+ };
508
+
509
+ if (!isPlainObject(obj)) {
510
+ throw new Error('setNestedProperty can only be used with plain object targets.');
511
+ }
512
+
513
+ const keys = path.split('.');
514
+ let current: Record<string, unknown> = obj;
515
+
516
+ for (let i = 0; i < keys.length - 1; i++) {
517
+ const key = keys[i];
518
+
519
+ // Guard against prototype pollution: block dangerous properties explicitly
520
+ if (DANGEROUS_PROPERTIES.has(key)) {
521
+ throw new Error(`Invalid property name: "${key}". This property is not allowed for security reasons.`);
522
+ }
523
+
524
+ // Additional validation: only allow alphanumeric characters and underscores
525
+ // First char must be letter or underscore, subsequent can include numbers
526
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
527
+ throw new Error(`Invalid property name: "${key}". Property names must be alphanumeric.`);
528
+ }
529
+
530
+ if (!isPlainObject(current)) {
531
+ throw new Error('Encountered non-plain object while setting nested property.');
532
+ }
533
+
534
+ if (!(key in current)) {
535
+ current[key] = {};
536
+ }
537
+
538
+ const next = current[key];
539
+ if (!isPlainObject(next)) {
540
+ // Prevent overwriting non-plain objects or mutating unexpected prototypes
541
+ throw new Error(`Cannot create nested property "${key}" on non-plain object.`);
542
+ }
543
+
544
+ current = next;
545
+ }
546
+
547
+ const finalKey = keys[keys.length - 1];
548
+
549
+ // Validate final key against dangerous properties
550
+ if (DANGEROUS_PROPERTIES.has(finalKey)) {
551
+ throw new Error(`Invalid property name: "${finalKey}". This property is not allowed for security reasons.`);
552
+ }
553
+
554
+ // Validate final key format
555
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(finalKey)) {
556
+ throw new Error(`Invalid property name: "${finalKey}". Property names must be alphanumeric.`);
557
+ }
558
+
559
+ if (!isPlainObject(current)) {
560
+ throw new Error('Encountered non-plain object while setting final nested property.');
561
+ }
562
+
563
+ current[finalKey] = value;
564
+ }
565
+
566
+ /**
567
+ * Load profile with environment variable overrides
568
+ * @param profileName - Profile name to load (defaults to MADEINOZ_KNOWLEDGE_PROFILE env var or 'default')
569
+ * @returns Profile configuration with env vars applied
570
+ */
571
+ export function loadProfileWithOverrides(profileName?: string): ConnectionProfileData {
572
+ const manager = new ConnectionProfileManager();
573
+
574
+ // Determine which profile to load
575
+ const envProfile = process.env.MADEINOZ_KNOWLEDGE_PROFILE;
576
+ const targetProfile = profileName || envProfile || manager.getDefaultProfile();
577
+
578
+ // Load the profile, fall back to defaults only for missing/malformed config
579
+ let profile: ConnectionProfileData;
580
+ try {
581
+ profile = manager.loadProfileOrThrow(targetProfile);
582
+ } catch (error) {
583
+ // Only fall back to defaults if the error is about missing/malformed config file
584
+ // Re-throw errors for specific profile not found in valid config
585
+ const errorMessage = error instanceof Error ? error.message : String(error);
586
+ if (errorMessage.includes('not found') && !errorMessage.includes('No configuration file found')) {
587
+ // Profile name not found in valid config - re-throw
588
+ throw error;
589
+ }
590
+ // Missing or malformed config file - fall back to code defaults
591
+ profile = {
592
+ name: targetProfile,
593
+ host: 'localhost',
594
+ port: 8001,
595
+ protocol: 'http',
596
+ };
597
+ }
598
+
599
+ // Apply environment variable overrides
600
+ for (const [envKey, profilePath] of Object.entries(ENV_MAPPINGS)) {
601
+ const envValue = process.env[envKey];
602
+ if (envValue !== undefined) {
603
+ const parsedValue = parseEnvValue(envKey, envValue);
604
+ setNestedProperty(profile as unknown as Record<string, unknown>, profilePath, parsedValue);
605
+ }
606
+ }
607
+
608
+ return profile;
609
+ }
610
+
611
+ /**
612
+ * Get connection profile name from environment
613
+ * @returns Profile name from environment, or default profile from config, or 'default' as final fallback
614
+ */
615
+ export function getProfileName(): string {
616
+ const envProfile = process.env.MADEINOZ_KNOWLEDGE_PROFILE;
617
+ if (envProfile) {
618
+ return envProfile;
619
+ }
620
+
621
+ // Read default profile from config file
622
+ const manager = new ConnectionProfileManager();
623
+ return manager.getDefaultProfile();
624
+ }
625
+
626
+ /**
627
+ * Export singleton instance
628
+ */
629
+ export const profileManager = new ConnectionProfileManager();