@kadi.build/core 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/config.d.ts +113 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +271 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +368 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -206,6 +206,27 @@ const client = new KadiClient({
|
|
|
206
206
|
});
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
**Tool snapshot on reconnection:** When an agent first registers with a broker, kadi-core captures a snapshot of the tools sent during that initial registration. On every subsequent reconnection, the same snapshot is replayed — tools that were registered locally after the initial connection (e.g. via `loadNative`) are **not** leaked to the broker.
|
|
210
|
+
|
|
211
|
+
This means abilities that use the native-mode pattern (connect first, then register tools locally) will correctly announce zero tools to the broker on reconnection, just as they did on the initial connect.
|
|
212
|
+
|
|
213
|
+
If you dynamically register new tools after the initial connection and want the broker to know about them, call `refreshBrokerTools()`:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Register a new tool after the initial connection
|
|
217
|
+
client.registerTool({
|
|
218
|
+
name: 'new-dynamic-tool',
|
|
219
|
+
description: 'Added at runtime',
|
|
220
|
+
input: z.object({ data: z.string() }),
|
|
221
|
+
}, async ({ data }) => ({ result: data }));
|
|
222
|
+
|
|
223
|
+
// Recapture the tool snapshot and re-announce to the broker
|
|
224
|
+
await client.refreshBrokerTools();
|
|
225
|
+
|
|
226
|
+
// Or target a specific broker
|
|
227
|
+
await client.refreshBrokerTools('production');
|
|
228
|
+
```
|
|
229
|
+
|
|
209
230
|
---
|
|
210
231
|
|
|
211
232
|
## Loading Abilities
|
|
@@ -745,6 +766,7 @@ new KadiClient(config: ClientConfig)
|
|
|
745
766
|
| `publish(channel, data, options?)` | Publish event to broker |
|
|
746
767
|
| `emit(event, data)` | Emit event to consumer (when serving as ability) |
|
|
747
768
|
| `serve(mode)` | Serve as ability (`'stdio'` or `'broker'`) |
|
|
769
|
+
| `refreshBrokerTools(broker?)` | Recapture tool snapshot and re-announce to broker. See [Reconnection](#reconnection) |
|
|
748
770
|
| `getConnectedBrokers()` | List connected broker names |
|
|
749
771
|
|
|
750
772
|
**Properties:**
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader for KADI agents and abilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized config.yml discovery with a 2-tier fallback:
|
|
5
|
+
* 1. **Project-level** — walk up from startDir looking for config.yml
|
|
6
|
+
* 2. **Global-level** — ~/.kadi/config.yml (shared KADI infrastructure settings)
|
|
7
|
+
*
|
|
8
|
+
* Resolution order per-field (highest-priority first):
|
|
9
|
+
* 1. Environment variables (PREFIX_KEY)
|
|
10
|
+
* 2. Project config.yml section
|
|
11
|
+
* 3. Global config.yml section (~/.kadi/config.yml)
|
|
12
|
+
* 4. Built-in defaults
|
|
13
|
+
*
|
|
14
|
+
* Secrets (API keys, tokens) should NEVER appear in config.yml.
|
|
15
|
+
* Use secret-ability / kadi-secret for credentials.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { loadConfig } from '@kadi.build/core';
|
|
20
|
+
*
|
|
21
|
+
* const { config, configPath, source } = loadConfig({
|
|
22
|
+
* section: 'memory',
|
|
23
|
+
* envPrefix: 'MEMORY',
|
|
24
|
+
* defaults: { database: 'kadi_memory', embedding_model: 'text-embedding-3-small' },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* TypeScript equivalent of: kadi-core-py/src/kadi/config.py
|
|
29
|
+
*/
|
|
30
|
+
/** How the config file was discovered. */
|
|
31
|
+
export type ConfigSource = 'project' | 'global' | 'default';
|
|
32
|
+
/** Options for {@link loadConfig}. */
|
|
33
|
+
export interface LoadConfigOptions {
|
|
34
|
+
/**
|
|
35
|
+
* YAML section key to extract (e.g. `'tunnel'`, `'memory'`, `'kadi_agent'`).
|
|
36
|
+
* The top-level key in config.yml whose value becomes the config object.
|
|
37
|
+
*/
|
|
38
|
+
section: string;
|
|
39
|
+
/**
|
|
40
|
+
* Directory to start the upward walk from.
|
|
41
|
+
* @default process.cwd()
|
|
42
|
+
*/
|
|
43
|
+
startDir?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Config filename to search for.
|
|
46
|
+
* @default 'config.yml'
|
|
47
|
+
*/
|
|
48
|
+
filename?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Environment variable prefix for automatic overrides.
|
|
51
|
+
*
|
|
52
|
+
* When set, loadConfig will scan `process.env` for keys starting with
|
|
53
|
+
* `PREFIX_` and merge them into the config (highest priority).
|
|
54
|
+
*
|
|
55
|
+
* Mapping: `PREFIX_KEY_NAME` → config field `key_name` (lowercased).
|
|
56
|
+
*
|
|
57
|
+
* @example envPrefix: 'MEMORY' → MEMORY_DATABASE overrides config.database
|
|
58
|
+
*/
|
|
59
|
+
envPrefix?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Built-in defaults (lowest priority).
|
|
62
|
+
* These are used when neither env vars nor config.yml provide a value.
|
|
63
|
+
*/
|
|
64
|
+
defaults?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** Result of {@link loadConfig}. */
|
|
67
|
+
export interface ConfigResult<T = Record<string, unknown>> {
|
|
68
|
+
/** Merged config object: env > project config.yml > global config.yml > defaults. */
|
|
69
|
+
config: T;
|
|
70
|
+
/** Absolute path to the config.yml that was loaded, or null if none found. */
|
|
71
|
+
configPath: string | null;
|
|
72
|
+
/** Where the primary config was found. */
|
|
73
|
+
source: ConfigSource;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Walk up the directory tree looking for a file.
|
|
77
|
+
*
|
|
78
|
+
* @param filename - File to search for (default: `'config.yml'`)
|
|
79
|
+
* @param startDir - Directory to start from (default: `process.cwd()`)
|
|
80
|
+
* @returns Absolute path if found, `null` otherwise.
|
|
81
|
+
*/
|
|
82
|
+
export declare function findConfigFile(filename?: string, startDir?: string): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Find the global KADI config file.
|
|
85
|
+
*
|
|
86
|
+
* @param filename - Config filename (default: `'config.yml'`)
|
|
87
|
+
* @returns Absolute path to `~/.kadi/<filename>` if it exists, `null` otherwise.
|
|
88
|
+
*/
|
|
89
|
+
export declare function findGlobalConfigFile(filename?: string): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Load configuration for a KADI agent or ability.
|
|
92
|
+
*
|
|
93
|
+
* Resolution order per-field (highest-priority first):
|
|
94
|
+
* 1. Environment variables (`envPrefix` + `_KEY`)
|
|
95
|
+
* 2. Project `config.yml` section (walk-up from startDir)
|
|
96
|
+
* 3. Global `~/.kadi/config.yml` section
|
|
97
|
+
* 4. Built-in `defaults`
|
|
98
|
+
*
|
|
99
|
+
* @typeParam T - The shape of the merged config object.
|
|
100
|
+
* @param options - Configuration loading options.
|
|
101
|
+
* @returns Merged config with metadata about where it was found.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const { config } = loadConfig<TunnelConfig>({
|
|
106
|
+
* section: 'tunnel',
|
|
107
|
+
* envPrefix: 'KADI_TUNNEL',
|
|
108
|
+
* defaults: { default_service: 'kadi', server_port: 7000 },
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function loadConfig<T = Record<string, unknown>>(options: LoadConfigOptions): ConfigResult<T>;
|
|
113
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAuDH,0CAA0C;AAC1C,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5D,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,oCAAoC;AACpC,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,qFAAqF;IACrF,MAAM,EAAE,CAAC,CAAC;IACV,8EAA8E;IAC9E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,0CAA0C;IAC1C,MAAM,EAAE,YAAY,CAAC;CACtB;AAMD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAyB,EACnC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAmBf;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,GAAE,MAAyB,GAClC,MAAM,GAAG,IAAI,CAUf;AAgFD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpD,OAAO,EAAE,iBAAiB,GACzB,YAAY,CAAC,CAAC,CAAC,CAsDjB"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader for KADI agents and abilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized config.yml discovery with a 2-tier fallback:
|
|
5
|
+
* 1. **Project-level** — walk up from startDir looking for config.yml
|
|
6
|
+
* 2. **Global-level** — ~/.kadi/config.yml (shared KADI infrastructure settings)
|
|
7
|
+
*
|
|
8
|
+
* Resolution order per-field (highest-priority first):
|
|
9
|
+
* 1. Environment variables (PREFIX_KEY)
|
|
10
|
+
* 2. Project config.yml section
|
|
11
|
+
* 3. Global config.yml section (~/.kadi/config.yml)
|
|
12
|
+
* 4. Built-in defaults
|
|
13
|
+
*
|
|
14
|
+
* Secrets (API keys, tokens) should NEVER appear in config.yml.
|
|
15
|
+
* Use secret-ability / kadi-secret for credentials.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { loadConfig } from '@kadi.build/core';
|
|
20
|
+
*
|
|
21
|
+
* const { config, configPath, source } = loadConfig({
|
|
22
|
+
* section: 'memory',
|
|
23
|
+
* envPrefix: 'MEMORY',
|
|
24
|
+
* defaults: { database: 'kadi_memory', embedding_model: 'text-embedding-3-small' },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* TypeScript equivalent of: kadi-core-py/src/kadi/config.py
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as os from 'node:os';
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
34
|
+
// YAML import — optional runtime dependency
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
36
|
+
/**
|
|
37
|
+
* We lazy-import js-yaml so that kadi-core does NOT require it as a hard
|
|
38
|
+
* dependency. Consumers that use loadConfig() need js-yaml installed;
|
|
39
|
+
* everything else in kadi-core works without it.
|
|
40
|
+
*
|
|
41
|
+
* In practice every KADI agent/ability already has js-yaml, so this is
|
|
42
|
+
* a formality rather than a real constraint.
|
|
43
|
+
*/
|
|
44
|
+
let yaml = null;
|
|
45
|
+
let yamlResolved = false;
|
|
46
|
+
function requireYaml() {
|
|
47
|
+
if (!yamlResolved) {
|
|
48
|
+
yamlResolved = true;
|
|
49
|
+
try {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
|
+
yaml = require('js-yaml');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
try {
|
|
55
|
+
// ESM fallback — createRequire for pure-ESM environments
|
|
56
|
+
const { createRequire } = require('node:module');
|
|
57
|
+
const localRequire = createRequire(process.cwd() + '/');
|
|
58
|
+
yaml = localRequire('js-yaml');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
yaml = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return yaml;
|
|
66
|
+
}
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
68
|
+
// Constants
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
70
|
+
/** Default config filename. */
|
|
71
|
+
const DEFAULT_FILENAME = 'config.yml';
|
|
72
|
+
/** Global KADI config directory. */
|
|
73
|
+
const GLOBAL_KADI_DIR = path.join(os.homedir(), '.kadi');
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
75
|
+
// File discovery
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
77
|
+
/**
|
|
78
|
+
* Walk up the directory tree looking for a file.
|
|
79
|
+
*
|
|
80
|
+
* @param filename - File to search for (default: `'config.yml'`)
|
|
81
|
+
* @param startDir - Directory to start from (default: `process.cwd()`)
|
|
82
|
+
* @returns Absolute path if found, `null` otherwise.
|
|
83
|
+
*/
|
|
84
|
+
export function findConfigFile(filename = DEFAULT_FILENAME, startDir) {
|
|
85
|
+
let dir = path.resolve(startDir ?? process.cwd());
|
|
86
|
+
// eslint-disable-next-line no-constant-condition
|
|
87
|
+
while (true) {
|
|
88
|
+
const candidate = path.join(dir, filename);
|
|
89
|
+
try {
|
|
90
|
+
if (fs.statSync(candidate).isFile()) {
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Not found, keep walking
|
|
96
|
+
}
|
|
97
|
+
const parent = path.dirname(dir);
|
|
98
|
+
if (parent === dir)
|
|
99
|
+
break; // Filesystem root
|
|
100
|
+
dir = parent;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Find the global KADI config file.
|
|
106
|
+
*
|
|
107
|
+
* @param filename - Config filename (default: `'config.yml'`)
|
|
108
|
+
* @returns Absolute path to `~/.kadi/<filename>` if it exists, `null` otherwise.
|
|
109
|
+
*/
|
|
110
|
+
export function findGlobalConfigFile(filename = DEFAULT_FILENAME) {
|
|
111
|
+
const globalPath = path.join(GLOBAL_KADI_DIR, filename);
|
|
112
|
+
try {
|
|
113
|
+
if (fs.statSync(globalPath).isFile()) {
|
|
114
|
+
return globalPath;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Not found
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
123
|
+
// Section extraction
|
|
124
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
125
|
+
/**
|
|
126
|
+
* Parse a YAML file and extract a top-level section.
|
|
127
|
+
*/
|
|
128
|
+
function readSection(filePath, section) {
|
|
129
|
+
const lib = requireYaml();
|
|
130
|
+
if (!lib)
|
|
131
|
+
return {};
|
|
132
|
+
try {
|
|
133
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
134
|
+
const parsed = lib.load(content);
|
|
135
|
+
if (!parsed || typeof parsed !== 'object')
|
|
136
|
+
return {};
|
|
137
|
+
const sectionData = parsed[section];
|
|
138
|
+
if (!sectionData || typeof sectionData !== 'object')
|
|
139
|
+
return {};
|
|
140
|
+
return sectionData;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
147
|
+
// Environment variable overlay
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
149
|
+
/**
|
|
150
|
+
* Build an overrides object from environment variables matching a prefix.
|
|
151
|
+
*
|
|
152
|
+
* `PREFIX_KEY_NAME=value` → `{ key_name: value }`
|
|
153
|
+
*
|
|
154
|
+
* Numeric strings are auto-coerced to numbers.
|
|
155
|
+
* `'true'` / `'false'` are coerced to booleans.
|
|
156
|
+
*/
|
|
157
|
+
function envOverrides(prefix, knownKeys) {
|
|
158
|
+
const result = {};
|
|
159
|
+
const prefixUpper = prefix.toUpperCase() + '_';
|
|
160
|
+
for (const [envKey, envVal] of Object.entries(process.env)) {
|
|
161
|
+
if (!envKey.startsWith(prefixUpper) || envVal === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
const configKey = envKey.slice(prefixUpper.length).toLowerCase();
|
|
164
|
+
// Skip if we have a known-keys set and this key isn't in it
|
|
165
|
+
if (knownKeys && !knownKeys.has(configKey))
|
|
166
|
+
continue;
|
|
167
|
+
result[configKey] = coerce(envVal);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Coerce a string environment value to its natural JS type.
|
|
173
|
+
*/
|
|
174
|
+
function coerce(value) {
|
|
175
|
+
if (value === 'true')
|
|
176
|
+
return true;
|
|
177
|
+
if (value === 'false')
|
|
178
|
+
return false;
|
|
179
|
+
if (value === '')
|
|
180
|
+
return value;
|
|
181
|
+
// Attempt numeric coercion only for things that look numeric
|
|
182
|
+
const num = Number(value);
|
|
183
|
+
if (!Number.isNaN(num) && value.trim() !== '')
|
|
184
|
+
return num;
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
188
|
+
// Main API
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
190
|
+
/**
|
|
191
|
+
* Load configuration for a KADI agent or ability.
|
|
192
|
+
*
|
|
193
|
+
* Resolution order per-field (highest-priority first):
|
|
194
|
+
* 1. Environment variables (`envPrefix` + `_KEY`)
|
|
195
|
+
* 2. Project `config.yml` section (walk-up from startDir)
|
|
196
|
+
* 3. Global `~/.kadi/config.yml` section
|
|
197
|
+
* 4. Built-in `defaults`
|
|
198
|
+
*
|
|
199
|
+
* @typeParam T - The shape of the merged config object.
|
|
200
|
+
* @param options - Configuration loading options.
|
|
201
|
+
* @returns Merged config with metadata about where it was found.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* const { config } = loadConfig<TunnelConfig>({
|
|
206
|
+
* section: 'tunnel',
|
|
207
|
+
* envPrefix: 'KADI_TUNNEL',
|
|
208
|
+
* defaults: { default_service: 'kadi', server_port: 7000 },
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function loadConfig(options) {
|
|
213
|
+
const { section, startDir, filename = DEFAULT_FILENAME, envPrefix, defaults = {}, } = options;
|
|
214
|
+
// ── Layer 4: Defaults ──────────────────────────────────────────────
|
|
215
|
+
let merged = { ...defaults };
|
|
216
|
+
// ── Layer 3: Global config.yml (~/.kadi/config.yml) ────────────────
|
|
217
|
+
let configPath = null;
|
|
218
|
+
let source = 'default';
|
|
219
|
+
const globalPath = findGlobalConfigFile(filename);
|
|
220
|
+
if (globalPath) {
|
|
221
|
+
const globalSection = readSection(globalPath, section);
|
|
222
|
+
if (Object.keys(globalSection).length > 0) {
|
|
223
|
+
merged = shallowMerge(merged, globalSection);
|
|
224
|
+
configPath = globalPath;
|
|
225
|
+
source = 'global';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ── Layer 2: Project config.yml (walk-up) ──────────────────────────
|
|
229
|
+
const projectPath = findConfigFile(filename, startDir);
|
|
230
|
+
if (projectPath) {
|
|
231
|
+
// Only use project config if it's not the same file as global
|
|
232
|
+
const isDistinct = !globalPath || path.resolve(projectPath) !== path.resolve(globalPath);
|
|
233
|
+
if (isDistinct) {
|
|
234
|
+
const projectSection = readSection(projectPath, section);
|
|
235
|
+
if (Object.keys(projectSection).length > 0) {
|
|
236
|
+
merged = shallowMerge(merged, projectSection);
|
|
237
|
+
configPath = projectPath;
|
|
238
|
+
source = 'project';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// ── Layer 1: Environment variables (highest priority) ──────────────
|
|
243
|
+
if (envPrefix) {
|
|
244
|
+
const env = envOverrides(envPrefix);
|
|
245
|
+
if (Object.keys(env).length > 0) {
|
|
246
|
+
merged = shallowMerge(merged, env);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
config: merged,
|
|
251
|
+
configPath,
|
|
252
|
+
source,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
256
|
+
// Helpers
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
258
|
+
/**
|
|
259
|
+
* Shallow merge — source keys overwrite target keys.
|
|
260
|
+
* Only copies defined (non-undefined) values.
|
|
261
|
+
*/
|
|
262
|
+
function shallowMerge(target, source) {
|
|
263
|
+
const result = { ...target };
|
|
264
|
+
for (const [key, val] of Object.entries(source)) {
|
|
265
|
+
if (val !== undefined) {
|
|
266
|
+
result[key] = val;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,0EAA0E;AAC1E,4CAA4C;AAC5C,0EAA0E;AAE1E;;;;;;;GAOG;AACH,IAAI,IAAI,GAA8C,IAAI,CAAC;AAC3D,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,SAAS,WAAW;IAClB,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,YAAY,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YACH,iEAAiE;YACjE,IAAI,GAAG,OAAO,CAAC,SAAS,CAAgB,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC;gBACH,yDAAyD;gBACzD,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;gBACjD,MAAM,YAAY,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC;gBACxD,IAAI,GAAG,YAAY,CAAC,SAAS,CAAgB,CAAC;YAChD,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0EAA0E;AAC1E,YAAY;AACZ,0EAA0E;AAE1E,+BAA+B;AAC/B,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAEtC,oCAAoC;AACpC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AA0DzD,0EAA0E;AAC1E,iBAAiB;AACjB,0EAA0E;AAE1E;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,WAAmB,gBAAgB,EACnC,QAAiB;IAEjB,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAElD,iDAAiD;IACjD,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;gBACpC,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM,CAAC,kBAAkB;QAC7C,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,WAAmB,gBAAgB;IAEnC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC;QACH,IAAI,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YACrC,OAAO,UAAU,CAAC;QACpB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,YAAY;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0EAA0E;AAC1E,qBAAqB;AACrB,0EAA0E;AAE1E;;GAEG;AACH,SAAS,WAAW,CAClB,QAAgB,EAChB,OAAe;IAEf,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;IAC1B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IAEpB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAmC,CAAC;QACnE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAC/D,OAAO,WAAsC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,+BAA+B;AAC/B,0EAA0E;AAE1E;;;;;;;GAOG;AACH,SAAS,YAAY,CACnB,MAAc,EACd,SAAuB;IAEvB,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC;IAE/C,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,MAAM,KAAK,SAAS;YAAE,SAAS;QAEtE,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAEjE,4DAA4D;QAC5D,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,SAAS;QAErD,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,MAAM,CAAC,KAAa;IAC3B,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAE/B,6DAA6D;IAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,GAAG,CAAC;IAE1D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0EAA0E;AAC1E,WAAW;AACX,0EAA0E;AAE1E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,UAAU,CACxB,OAA0B;IAE1B,MAAM,EACJ,OAAO,EACP,QAAQ,EACR,QAAQ,GAAG,gBAAgB,EAC3B,SAAS,EACT,QAAQ,GAAG,EAAE,GACd,GAAG,OAAO,CAAC;IAEZ,sEAAsE;IACtE,IAAI,MAAM,GAA4B,EAAE,GAAG,QAAQ,EAAE,CAAC;IAEtD,sEAAsE;IACtE,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,MAAM,GAAiB,SAAS,CAAC;IAErC,MAAM,UAAU,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAClD,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,aAAa,GAAG,WAAW,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACvD,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;YAC7C,UAAU,GAAG,UAAU,CAAC;YACxB,MAAM,GAAG,QAAQ,CAAC;QACpB,CAAC;IACH,CAAC;IAED,sEAAsE;IACtE,MAAM,WAAW,GAAG,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACvD,IAAI,WAAW,EAAE,CAAC;QAChB,8DAA8D;QAC9D,MAAM,UAAU,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACzF,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,cAAc,GAAG,WAAW,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YACzD,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3C,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;gBAC9C,UAAU,GAAG,WAAW,CAAC;gBACzB,MAAM,GAAG,SAAS,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAED,sEAAsE;IACtE,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;QACpC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,EAAE,MAAW;QACnB,UAAU;QACV,MAAM;KACP,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,UAAU;AACV,0EAA0E;AAE1E;;;GAGG;AACH,SAAS,YAAY,CACnB,MAA+B,EAC/B,MAA+B;IAE/B,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAChD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export type { ClientConfig, BrokerEntry, ResolvedConfig, ToolDefinition, BrokerT
|
|
|
32
32
|
export { AgentJsonManager } from './agent-json.js';
|
|
33
33
|
export { ProcessManager, ManagedProcess } from './process-manager.js';
|
|
34
34
|
export { getByPath, setByPath, deleteByPath, deepMerge } from './utils.js';
|
|
35
|
+
export { loadConfig, findConfigFile, findGlobalConfigFile } from './config.js';
|
|
36
|
+
export type { LoadConfigOptions, ConfigResult, ConfigSource } from './config.js';
|
|
35
37
|
export { StdioMessageReader, StdioMessageWriter } from './stdio-framing.js';
|
|
36
38
|
export { zodToJsonSchema, isZodSchema } from './zod.js';
|
|
37
39
|
export { findProjectRoot, readLockFile, findAbilityEntry, resolveAbilityPath, resolveAbilityEntry, getInstalledAbilityNames, } from './lockfile.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3D,YAAY,EAEV,YAAY,EACZ,WAAW,EACX,cAAc,EAGd,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,UAAU,EACV,cAAc,EAGd,aAAa,EACb,aAAa,EACb,aAAa,EACb,YAAY,EAGZ,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EAGnB,WAAW,EACX,YAAY,EACZ,cAAc,EACd,iBAAiB,EAGjB,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,EAGX,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,EAGd,aAAa,EACb,gBAAgB,EAGhB,cAAc,EAGd,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,EACX,cAAc,EAGd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG3E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAG5E,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGxD,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGrE,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAG1C,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACjF,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3D,YAAY,EAEV,YAAY,EACZ,WAAW,EACX,cAAc,EAGd,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,UAAU,EACV,cAAc,EAGd,aAAa,EACb,aAAa,EACb,aAAa,EACb,YAAY,EAGZ,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EAGnB,WAAW,EACX,YAAY,EACZ,cAAc,EACd,iBAAiB,EAGjB,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,EAGX,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,EAGd,aAAa,EACb,gBAAgB,EAGhB,cAAc,EAGd,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,EACX,cAAc,EAGd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG3E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC/E,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGjF,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAG5E,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGxD,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGrE,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAG1C,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACjF,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,8 @@ export { AgentJsonManager } from './agent-json.js';
|
|
|
36
36
|
export { ProcessManager, ManagedProcess } from './process-manager.js';
|
|
37
37
|
// Dot-path utilities
|
|
38
38
|
export { getByPath, setByPath, deleteByPath, deepMerge } from './utils.js';
|
|
39
|
+
// Config loader (walk-up config.yml + ~/.kadi/ global fallback)
|
|
40
|
+
export { loadConfig, findConfigFile, findGlobalConfigFile } from './config.js';
|
|
39
41
|
// Stdio framing (for advanced use cases — building custom bridge protocols)
|
|
40
42
|
export { StdioMessageReader, StdioMessageWriter } from './stdio-framing.js';
|
|
41
43
|
// Zod utilities
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uCAAuC;AACvC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc;AACd,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,SAAS;AACT,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiFxC,qBAAqB;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,kBAAkB;AAClB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtE,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE3E,4EAA4E;AAC5E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAExD,sBAAsB;AACtB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAEvB,sCAAsC;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG7D,4BAA4B;AAC5B,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAE1C,kDAAkD;AAClD,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uCAAuC;AACvC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,cAAc;AACd,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,SAAS;AACT,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiFxC,qBAAqB;AACrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,kBAAkB;AAClB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtE,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE3E,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAG/E,4EAA4E;AAC5E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAExD,sBAAsB;AACtB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAEvB,sCAAsC;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG7D,4BAA4B;AAC5B,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAE1C,kDAAkD;AAClD,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC"}
|
package/package.json
CHANGED
package/src/config.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader for KADI agents and abilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized config.yml discovery with a 2-tier fallback:
|
|
5
|
+
* 1. **Project-level** — walk up from startDir looking for config.yml
|
|
6
|
+
* 2. **Global-level** — ~/.kadi/config.yml (shared KADI infrastructure settings)
|
|
7
|
+
*
|
|
8
|
+
* Resolution order per-field (highest-priority first):
|
|
9
|
+
* 1. Environment variables (PREFIX_KEY)
|
|
10
|
+
* 2. Project config.yml section
|
|
11
|
+
* 3. Global config.yml section (~/.kadi/config.yml)
|
|
12
|
+
* 4. Built-in defaults
|
|
13
|
+
*
|
|
14
|
+
* Secrets (API keys, tokens) should NEVER appear in config.yml.
|
|
15
|
+
* Use secret-ability / kadi-secret for credentials.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { loadConfig } from '@kadi.build/core';
|
|
20
|
+
*
|
|
21
|
+
* const { config, configPath, source } = loadConfig({
|
|
22
|
+
* section: 'memory',
|
|
23
|
+
* envPrefix: 'MEMORY',
|
|
24
|
+
* defaults: { database: 'kadi_memory', embedding_model: 'text-embedding-3-small' },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* TypeScript equivalent of: kadi-core-py/src/kadi/config.py
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import * as os from 'node:os';
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
36
|
+
// YAML import — optional runtime dependency
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* We lazy-import js-yaml so that kadi-core does NOT require it as a hard
|
|
41
|
+
* dependency. Consumers that use loadConfig() need js-yaml installed;
|
|
42
|
+
* everything else in kadi-core works without it.
|
|
43
|
+
*
|
|
44
|
+
* In practice every KADI agent/ability already has js-yaml, so this is
|
|
45
|
+
* a formality rather than a real constraint.
|
|
46
|
+
*/
|
|
47
|
+
let yaml: { load(content: string): unknown } | null = null;
|
|
48
|
+
let yamlResolved = false;
|
|
49
|
+
|
|
50
|
+
function requireYaml(): typeof yaml {
|
|
51
|
+
if (!yamlResolved) {
|
|
52
|
+
yamlResolved = true;
|
|
53
|
+
try {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
|
+
yaml = require('js-yaml') as typeof yaml;
|
|
56
|
+
} catch {
|
|
57
|
+
try {
|
|
58
|
+
// ESM fallback — createRequire for pure-ESM environments
|
|
59
|
+
const { createRequire } = require('node:module');
|
|
60
|
+
const localRequire = createRequire(process.cwd() + '/');
|
|
61
|
+
yaml = localRequire('js-yaml') as typeof yaml;
|
|
62
|
+
} catch {
|
|
63
|
+
yaml = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return yaml;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
71
|
+
// Constants
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
/** Default config filename. */
|
|
75
|
+
const DEFAULT_FILENAME = 'config.yml';
|
|
76
|
+
|
|
77
|
+
/** Global KADI config directory. */
|
|
78
|
+
const GLOBAL_KADI_DIR = path.join(os.homedir(), '.kadi');
|
|
79
|
+
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
81
|
+
// Types
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
/** How the config file was discovered. */
|
|
85
|
+
export type ConfigSource = 'project' | 'global' | 'default';
|
|
86
|
+
|
|
87
|
+
/** Options for {@link loadConfig}. */
|
|
88
|
+
export interface LoadConfigOptions {
|
|
89
|
+
/**
|
|
90
|
+
* YAML section key to extract (e.g. `'tunnel'`, `'memory'`, `'kadi_agent'`).
|
|
91
|
+
* The top-level key in config.yml whose value becomes the config object.
|
|
92
|
+
*/
|
|
93
|
+
section: string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Directory to start the upward walk from.
|
|
97
|
+
* @default process.cwd()
|
|
98
|
+
*/
|
|
99
|
+
startDir?: string;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Config filename to search for.
|
|
103
|
+
* @default 'config.yml'
|
|
104
|
+
*/
|
|
105
|
+
filename?: string;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Environment variable prefix for automatic overrides.
|
|
109
|
+
*
|
|
110
|
+
* When set, loadConfig will scan `process.env` for keys starting with
|
|
111
|
+
* `PREFIX_` and merge them into the config (highest priority).
|
|
112
|
+
*
|
|
113
|
+
* Mapping: `PREFIX_KEY_NAME` → config field `key_name` (lowercased).
|
|
114
|
+
*
|
|
115
|
+
* @example envPrefix: 'MEMORY' → MEMORY_DATABASE overrides config.database
|
|
116
|
+
*/
|
|
117
|
+
envPrefix?: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Built-in defaults (lowest priority).
|
|
121
|
+
* These are used when neither env vars nor config.yml provide a value.
|
|
122
|
+
*/
|
|
123
|
+
defaults?: Record<string, unknown>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Result of {@link loadConfig}. */
|
|
127
|
+
export interface ConfigResult<T = Record<string, unknown>> {
|
|
128
|
+
/** Merged config object: env > project config.yml > global config.yml > defaults. */
|
|
129
|
+
config: T;
|
|
130
|
+
/** Absolute path to the config.yml that was loaded, or null if none found. */
|
|
131
|
+
configPath: string | null;
|
|
132
|
+
/** Where the primary config was found. */
|
|
133
|
+
source: ConfigSource;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
137
|
+
// File discovery
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Walk up the directory tree looking for a file.
|
|
142
|
+
*
|
|
143
|
+
* @param filename - File to search for (default: `'config.yml'`)
|
|
144
|
+
* @param startDir - Directory to start from (default: `process.cwd()`)
|
|
145
|
+
* @returns Absolute path if found, `null` otherwise.
|
|
146
|
+
*/
|
|
147
|
+
export function findConfigFile(
|
|
148
|
+
filename: string = DEFAULT_FILENAME,
|
|
149
|
+
startDir?: string,
|
|
150
|
+
): string | null {
|
|
151
|
+
let dir = path.resolve(startDir ?? process.cwd());
|
|
152
|
+
|
|
153
|
+
// eslint-disable-next-line no-constant-condition
|
|
154
|
+
while (true) {
|
|
155
|
+
const candidate = path.join(dir, filename);
|
|
156
|
+
try {
|
|
157
|
+
if (fs.statSync(candidate).isFile()) {
|
|
158
|
+
return candidate;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Not found, keep walking
|
|
162
|
+
}
|
|
163
|
+
const parent = path.dirname(dir);
|
|
164
|
+
if (parent === dir) break; // Filesystem root
|
|
165
|
+
dir = parent;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find the global KADI config file.
|
|
173
|
+
*
|
|
174
|
+
* @param filename - Config filename (default: `'config.yml'`)
|
|
175
|
+
* @returns Absolute path to `~/.kadi/<filename>` if it exists, `null` otherwise.
|
|
176
|
+
*/
|
|
177
|
+
export function findGlobalConfigFile(
|
|
178
|
+
filename: string = DEFAULT_FILENAME,
|
|
179
|
+
): string | null {
|
|
180
|
+
const globalPath = path.join(GLOBAL_KADI_DIR, filename);
|
|
181
|
+
try {
|
|
182
|
+
if (fs.statSync(globalPath).isFile()) {
|
|
183
|
+
return globalPath;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// Not found
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
192
|
+
// Section extraction
|
|
193
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parse a YAML file and extract a top-level section.
|
|
197
|
+
*/
|
|
198
|
+
function readSection(
|
|
199
|
+
filePath: string,
|
|
200
|
+
section: string,
|
|
201
|
+
): Record<string, unknown> {
|
|
202
|
+
const lib = requireYaml();
|
|
203
|
+
if (!lib) return {};
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
207
|
+
const parsed = lib.load(content) as Record<string, unknown> | null;
|
|
208
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
209
|
+
const sectionData = parsed[section];
|
|
210
|
+
if (!sectionData || typeof sectionData !== 'object') return {};
|
|
211
|
+
return sectionData as Record<string, unknown>;
|
|
212
|
+
} catch {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
218
|
+
// Environment variable overlay
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build an overrides object from environment variables matching a prefix.
|
|
223
|
+
*
|
|
224
|
+
* `PREFIX_KEY_NAME=value` → `{ key_name: value }`
|
|
225
|
+
*
|
|
226
|
+
* Numeric strings are auto-coerced to numbers.
|
|
227
|
+
* `'true'` / `'false'` are coerced to booleans.
|
|
228
|
+
*/
|
|
229
|
+
function envOverrides(
|
|
230
|
+
prefix: string,
|
|
231
|
+
knownKeys?: Set<string>,
|
|
232
|
+
): Record<string, unknown> {
|
|
233
|
+
const result: Record<string, unknown> = {};
|
|
234
|
+
const prefixUpper = prefix.toUpperCase() + '_';
|
|
235
|
+
|
|
236
|
+
for (const [envKey, envVal] of Object.entries(process.env)) {
|
|
237
|
+
if (!envKey.startsWith(prefixUpper) || envVal === undefined) continue;
|
|
238
|
+
|
|
239
|
+
const configKey = envKey.slice(prefixUpper.length).toLowerCase();
|
|
240
|
+
|
|
241
|
+
// Skip if we have a known-keys set and this key isn't in it
|
|
242
|
+
if (knownKeys && !knownKeys.has(configKey)) continue;
|
|
243
|
+
|
|
244
|
+
result[configKey] = coerce(envVal);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Coerce a string environment value to its natural JS type.
|
|
252
|
+
*/
|
|
253
|
+
function coerce(value: string): unknown {
|
|
254
|
+
if (value === 'true') return true;
|
|
255
|
+
if (value === 'false') return false;
|
|
256
|
+
if (value === '') return value;
|
|
257
|
+
|
|
258
|
+
// Attempt numeric coercion only for things that look numeric
|
|
259
|
+
const num = Number(value);
|
|
260
|
+
if (!Number.isNaN(num) && value.trim() !== '') return num;
|
|
261
|
+
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
266
|
+
// Main API
|
|
267
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Load configuration for a KADI agent or ability.
|
|
271
|
+
*
|
|
272
|
+
* Resolution order per-field (highest-priority first):
|
|
273
|
+
* 1. Environment variables (`envPrefix` + `_KEY`)
|
|
274
|
+
* 2. Project `config.yml` section (walk-up from startDir)
|
|
275
|
+
* 3. Global `~/.kadi/config.yml` section
|
|
276
|
+
* 4. Built-in `defaults`
|
|
277
|
+
*
|
|
278
|
+
* @typeParam T - The shape of the merged config object.
|
|
279
|
+
* @param options - Configuration loading options.
|
|
280
|
+
* @returns Merged config with metadata about where it was found.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const { config } = loadConfig<TunnelConfig>({
|
|
285
|
+
* section: 'tunnel',
|
|
286
|
+
* envPrefix: 'KADI_TUNNEL',
|
|
287
|
+
* defaults: { default_service: 'kadi', server_port: 7000 },
|
|
288
|
+
* });
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export function loadConfig<T = Record<string, unknown>>(
|
|
292
|
+
options: LoadConfigOptions,
|
|
293
|
+
): ConfigResult<T> {
|
|
294
|
+
const {
|
|
295
|
+
section,
|
|
296
|
+
startDir,
|
|
297
|
+
filename = DEFAULT_FILENAME,
|
|
298
|
+
envPrefix,
|
|
299
|
+
defaults = {},
|
|
300
|
+
} = options;
|
|
301
|
+
|
|
302
|
+
// ── Layer 4: Defaults ──────────────────────────────────────────────
|
|
303
|
+
let merged: Record<string, unknown> = { ...defaults };
|
|
304
|
+
|
|
305
|
+
// ── Layer 3: Global config.yml (~/.kadi/config.yml) ────────────────
|
|
306
|
+
let configPath: string | null = null;
|
|
307
|
+
let source: ConfigSource = 'default';
|
|
308
|
+
|
|
309
|
+
const globalPath = findGlobalConfigFile(filename);
|
|
310
|
+
if (globalPath) {
|
|
311
|
+
const globalSection = readSection(globalPath, section);
|
|
312
|
+
if (Object.keys(globalSection).length > 0) {
|
|
313
|
+
merged = shallowMerge(merged, globalSection);
|
|
314
|
+
configPath = globalPath;
|
|
315
|
+
source = 'global';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Layer 2: Project config.yml (walk-up) ──────────────────────────
|
|
320
|
+
const projectPath = findConfigFile(filename, startDir);
|
|
321
|
+
if (projectPath) {
|
|
322
|
+
// Only use project config if it's not the same file as global
|
|
323
|
+
const isDistinct = !globalPath || path.resolve(projectPath) !== path.resolve(globalPath);
|
|
324
|
+
if (isDistinct) {
|
|
325
|
+
const projectSection = readSection(projectPath, section);
|
|
326
|
+
if (Object.keys(projectSection).length > 0) {
|
|
327
|
+
merged = shallowMerge(merged, projectSection);
|
|
328
|
+
configPath = projectPath;
|
|
329
|
+
source = 'project';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Layer 1: Environment variables (highest priority) ──────────────
|
|
335
|
+
if (envPrefix) {
|
|
336
|
+
const env = envOverrides(envPrefix);
|
|
337
|
+
if (Object.keys(env).length > 0) {
|
|
338
|
+
merged = shallowMerge(merged, env);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
config: merged as T,
|
|
344
|
+
configPath,
|
|
345
|
+
source,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
350
|
+
// Helpers
|
|
351
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Shallow merge — source keys overwrite target keys.
|
|
355
|
+
* Only copies defined (non-undefined) values.
|
|
356
|
+
*/
|
|
357
|
+
function shallowMerge(
|
|
358
|
+
target: Record<string, unknown>,
|
|
359
|
+
source: Record<string, unknown>,
|
|
360
|
+
): Record<string, unknown> {
|
|
361
|
+
const result = { ...target };
|
|
362
|
+
for (const [key, val] of Object.entries(source)) {
|
|
363
|
+
if (val !== undefined) {
|
|
364
|
+
result[key] = val;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,10 @@ export { ProcessManager, ManagedProcess } from './process-manager.js';
|
|
|
122
122
|
// Dot-path utilities
|
|
123
123
|
export { getByPath, setByPath, deleteByPath, deepMerge } from './utils.js';
|
|
124
124
|
|
|
125
|
+
// Config loader (walk-up config.yml + ~/.kadi/ global fallback)
|
|
126
|
+
export { loadConfig, findConfigFile, findGlobalConfigFile } from './config.js';
|
|
127
|
+
export type { LoadConfigOptions, ConfigResult, ConfigSource } from './config.js';
|
|
128
|
+
|
|
125
129
|
// Stdio framing (for advanced use cases — building custom bridge protocols)
|
|
126
130
|
export { StdioMessageReader, StdioMessageWriter } from './stdio-framing.js';
|
|
127
131
|
|