@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.
- package/.deepquark/skills/bundled/knowledge-graph/SKILL.md +385 -0
- package/.deepquark/skills/bundled/knowledge-graph/STANDARDS.md +461 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/cli.ts +588 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/config.ts +630 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/connection-profile.ts +629 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/container.ts +756 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/mcp-client.ts +1310 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/output-formatter.ts +997 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/token-metrics.ts +335 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/transformation-log.ts +137 -0
- package/.deepquark/skills/bundled/knowledge-graph/lib/wrapper-config.ts +113 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/.env.example +129 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/compare-embeddings.ts +175 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/config-falkordb.yaml +108 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/config-neo4j.yaml +111 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/diagnose.ts +483 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb-dev.yml +146 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb.yml +151 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev-local.yml +161 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev.yml +161 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j.yml +169 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-production.yml +128 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-test.yml +10 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose.yml +84 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/entrypoint.sh +40 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/install.ts +2054 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-falkordb.yml +78 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-neo4j.yml +88 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose.yml +83 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-all-llms-mcp.ts +387 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-models.ts +201 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-providers.ts +641 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-graphiti-model.ts +217 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-correct.ts +141 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-llms-mcp.ts +386 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-models.ts +173 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-llama-extraction.ts +188 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-final.ts +240 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-live.ts +187 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-session.ts +127 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-model-combinations.ts +316 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-ollama-models.ts +228 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-openrouter-models.ts +460 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-real-life-mcp.ts +311 -0
- package/.deepquark/skills/bundled/knowledge-graph/server/test-search-debug.ts +199 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/Install.md +104 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/README.md +120 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/knowledge-cli.ts +996 -0
- package/.deepquark/skills/bundled/knowledge-graph/tools/server-cli.ts +531 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/BulkImport.md +514 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/CaptureEpisode.md +242 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/ClearGraph.md +392 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/GetRecent.md +352 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/GetStatus.md +373 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/HealthReport.md +212 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/InvestigateEntity.md +142 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/OntologyManagement.md +201 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/RunMaintenance.md +302 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchByDate.md +255 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchFacts.md +382 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchKnowledge.md +374 -0
- package/.deepquark/skills/bundled/knowledge-graph/workflows/StixImport.md +212 -0
- package/bin/deepquark +0 -0
- package/package.json +1 -1
- package/.deepquark/skills/bundled/ge-payroll/SKILL.md +0 -153
- package/.deepquark/skills/bundled/ge-payroll/evals/evals.json +0 -23
- package/.deepquark/skills/bundled/ge-payroll/references/pain-points-improvements.md +0 -106
- package/.deepquark/skills/bundled/ge-payroll/references/process-detail.md +0 -217
- package/.deepquark/skills/bundled/ge-payroll/references/raci-stakeholders.md +0 -85
- 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();
|