@kadi.build/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +424 -1
  2. package/agent.json +19 -0
  3. package/dist/agent-json.d.ts +231 -0
  4. package/dist/agent-json.d.ts.map +1 -0
  5. package/dist/agent-json.js +554 -0
  6. package/dist/agent-json.js.map +1 -0
  7. package/dist/client.d.ts +34 -0
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +50 -0
  10. package/dist/client.js.map +1 -1
  11. package/dist/errors.d.ts +1 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/process-manager.d.ts +235 -0
  19. package/dist/process-manager.d.ts.map +1 -0
  20. package/dist/process-manager.js +647 -0
  21. package/dist/process-manager.js.map +1 -0
  22. package/dist/stdio-framing.d.ts +88 -0
  23. package/dist/stdio-framing.d.ts.map +1 -0
  24. package/dist/stdio-framing.js +194 -0
  25. package/dist/stdio-framing.js.map +1 -0
  26. package/dist/transports/stdio.d.ts.map +1 -1
  27. package/dist/transports/stdio.js +3 -181
  28. package/dist/transports/stdio.js.map +1 -1
  29. package/dist/types.d.ts +256 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/utils.d.ts +107 -0
  32. package/dist/utils.d.ts.map +1 -0
  33. package/dist/utils.js +212 -0
  34. package/dist/utils.js.map +1 -0
  35. package/package.json +3 -1
  36. package/scripts/symlink.mjs +131 -0
  37. package/src/agent-json.ts +655 -0
  38. package/src/client.ts +56 -0
  39. package/src/errors.ts +15 -0
  40. package/src/index.ts +32 -0
  41. package/src/process-manager.ts +821 -0
  42. package/src/stdio-framing.ts +227 -0
  43. package/src/transports/stdio.ts +4 -221
  44. package/src/types.ts +277 -0
  45. package/src/utils.ts +246 -0
package/src/types.ts CHANGED
@@ -1073,3 +1073,280 @@ export interface EmitOptions {
1073
1073
  */
1074
1074
  network?: string;
1075
1075
  }
1076
+
1077
+ // ═══════════════════════════════════════════════════════════════════════
1078
+ // 9. AGENT.JSON TYPES
1079
+ // ═══════════════════════════════════════════════════════════════════════
1080
+
1081
+ /**
1082
+ * Lifecycle scripts in agent.json.
1083
+ * These are executed by kadi-install and kadi-run at various stages.
1084
+ */
1085
+ export interface AgentJsonScripts {
1086
+ /** Run before setup — environment checks, secret retrieval */
1087
+ preflight?: string;
1088
+ /** Install dependencies and build */
1089
+ setup?: string;
1090
+ /** Start the agent/ability */
1091
+ start?: string;
1092
+ /** Stop the agent/ability */
1093
+ stop?: string;
1094
+ /** Development mode start */
1095
+ dev?: string;
1096
+ /** Cleanup generated files */
1097
+ clean?: string;
1098
+ /** Any other custom scripts */
1099
+ [key: string]: string | undefined;
1100
+ }
1101
+
1102
+ /**
1103
+ * Container build profile in agent.json.
1104
+ */
1105
+ export interface AgentJsonBuildProfile {
1106
+ /** Base image (e.g., 'node:22-slim') */
1107
+ from: string;
1108
+ /** Target platform (e.g., 'linux/amd64') */
1109
+ platform?: string;
1110
+ /** KADI CLI version to install in container */
1111
+ cli?: string;
1112
+ /** RUN commands for Dockerfile */
1113
+ run?: string[];
1114
+ /** CMD for the container */
1115
+ cmd?: string[];
1116
+ /** Additional fields */
1117
+ [key: string]: unknown;
1118
+ }
1119
+
1120
+ /**
1121
+ * Deployment profile in agent.json.
1122
+ */
1123
+ export interface AgentJsonDeployProfile {
1124
+ /** Deployment target (e.g., 'local', 'akash') */
1125
+ target: string;
1126
+ /** Container engine for local deployments (e.g., 'docker') */
1127
+ engine?: string;
1128
+ /** Network name for Akash deployments */
1129
+ network?: string;
1130
+ /** Deposit amount for Akash */
1131
+ deposit?: string;
1132
+ /** Service definitions */
1133
+ services?: Record<string, unknown>;
1134
+ /** Secret delivery configuration */
1135
+ secrets?: Record<string, unknown>;
1136
+ /** Additional fields */
1137
+ [key: string]: unknown;
1138
+ }
1139
+
1140
+ /**
1141
+ * Full agent.json schema — union of all known fields across the ecosystem.
1142
+ *
1143
+ * Agent.json files can exist in three locations:
1144
+ * 1. **Project root** — the agent's own config
1145
+ * 2. **Installed ability** — an ability's config within abilities/
1146
+ * 3. **KADI home** — global CLI config at ~/.kadi/
1147
+ *
1148
+ * @example
1149
+ * ```typescript
1150
+ * const agentJson: AgentJson = {
1151
+ * name: 'my-agent',
1152
+ * version: '1.0.0',
1153
+ * type: 'ability',
1154
+ * entrypoint: 'dist/index.js',
1155
+ * abilities: { 'secret-ability': '^0.9.0' },
1156
+ * scripts: { setup: 'npm install && npm run build', start: 'node dist/index.js' },
1157
+ * deploy: { local: { target: 'local', engine: 'docker' } },
1158
+ * };
1159
+ * ```
1160
+ */
1161
+ export interface AgentJson {
1162
+ /** Name of the agent/ability/plugin (required) */
1163
+ name: string;
1164
+ /** Semver version */
1165
+ version?: string;
1166
+ /** Human-readable description */
1167
+ description?: string;
1168
+ /** License identifier */
1169
+ license?: string;
1170
+ /** Package type */
1171
+ type?: 'ability' | 'plugin' | 'agent';
1172
+ /** Entry point file for native loading */
1173
+ entrypoint?: string;
1174
+ /** Alternate entry field (used in some Python projects) */
1175
+ entry?: string;
1176
+ /** Git repository URL */
1177
+ repo?: string;
1178
+ /** Archive download URL */
1179
+ lib?: string;
1180
+ /** CLI commands this plugin provides */
1181
+ commands?: string[];
1182
+ /** Ability dependencies (name → semver range) */
1183
+ abilities?: Record<string, string>;
1184
+ /** Broker connections (name → URL or BrokerEntry) */
1185
+ brokers?: Record<string, string | { url: string; networks?: string[] }>;
1186
+ /** Lifecycle scripts */
1187
+ scripts?: AgentJsonScripts;
1188
+ /** Container build profiles */
1189
+ build?: Record<string, AgentJsonBuildProfile>;
1190
+ /** Deployment profiles */
1191
+ deploy?: Record<string, AgentJsonDeployProfile>;
1192
+ /** CLI plugin registrations */
1193
+ cliPlugins?: Record<string, unknown>;
1194
+ /** Extensible — any additional fields */
1195
+ [key: string]: unknown;
1196
+ }
1197
+
1198
+ /**
1199
+ * Options for creating an AgentJsonManager.
1200
+ */
1201
+ export interface AgentJsonManagerOptions {
1202
+ /** Explicit project root. If omitted, auto-detected via findProjectRoot(). */
1203
+ projectRoot?: string;
1204
+ /** Custom KADI home path. Default: ~/.kadi */
1205
+ kadiHome?: string;
1206
+ /** If true, creates agent.json files that don't exist on first write. Default: true */
1207
+ createOnWrite?: boolean;
1208
+ }
1209
+
1210
+ /**
1211
+ * Options for reading an ability's agent.json.
1212
+ */
1213
+ export interface ReadAbilityOptions {
1214
+ /** Specific field to read (dot-notation path). */
1215
+ field?: string;
1216
+ /** Specific version to target when multiple versions are installed. */
1217
+ version?: string;
1218
+ }
1219
+
1220
+ /**
1221
+ * Summary info about an installed ability.
1222
+ */
1223
+ export interface AbilityInfo {
1224
+ /** Ability name */
1225
+ name: string;
1226
+ /** Installed version */
1227
+ version: string;
1228
+ /** Absolute path to the ability directory */
1229
+ path: string;
1230
+ }
1231
+
1232
+ /**
1233
+ * Resolved paths for all known agent.json files.
1234
+ */
1235
+ export interface AgentJsonPaths {
1236
+ /** Path to project root agent.json */
1237
+ project: string;
1238
+ /** Path to KADI home agent.json */
1239
+ home: string;
1240
+ /** Map of "name@version" → absolute path to agent.json */
1241
+ abilities: Record<string, string>;
1242
+ }
1243
+
1244
+ // ═══════════════════════════════════════════════════════════════════════
1245
+ // 10. PROCESS MANAGER TYPES
1246
+ // ═══════════════════════════════════════════════════════════════════════
1247
+
1248
+ /**
1249
+ * Execution mode for a managed process.
1250
+ *
1251
+ * - **headless**: Fire-and-forget. No communication channel. Best for builds, cleanup.
1252
+ * - **piped**: stdout/stderr are streamed back. Best for monitoring deploys, tailing logs.
1253
+ * - **bridge**: Full JSON-RPC stdio bridge. Best for interactive tools, worker agents.
1254
+ */
1255
+ export type ProcessMode = 'headless' | 'piped' | 'bridge';
1256
+
1257
+ /**
1258
+ * Lifecycle state of a managed process.
1259
+ */
1260
+ export type ProcessState = 'running' | 'exited' | 'killed' | 'errored';
1261
+
1262
+ /**
1263
+ * Options for spawning a managed process.
1264
+ */
1265
+ export interface SpawnOptions {
1266
+ /** Shell command to execute */
1267
+ command: string;
1268
+ /** Arguments to the command */
1269
+ args?: string[];
1270
+ /** Working directory */
1271
+ cwd?: string;
1272
+ /** Environment variables (merged with process.env) */
1273
+ env?: Record<string, string>;
1274
+ /** Execution mode */
1275
+ mode: ProcessMode;
1276
+ /** Timeout in ms — auto-kills if exceeded. 0 = no timeout. Default: 0 */
1277
+ timeout?: number;
1278
+ /** Grace period before SIGKILL after SIGTERM (ms). Default: 5000 */
1279
+ killGracePeriod?: number;
1280
+ /** Maximum output buffer size in bytes (piped mode). Default: 10MB */
1281
+ maxOutputBuffer?: number;
1282
+ }
1283
+
1284
+ /**
1285
+ * Information about a managed process.
1286
+ */
1287
+ export interface ProcessInfo {
1288
+ /** Unique identifier (the name passed to spawn) */
1289
+ id: string;
1290
+ /** OS process ID */
1291
+ pid: number;
1292
+ /** Current lifecycle state */
1293
+ state: ProcessState;
1294
+ /** Execution mode */
1295
+ mode: ProcessMode;
1296
+ /** When the process was started */
1297
+ startedAt: Date;
1298
+ /** When the process ended (null if still running) */
1299
+ endedAt: Date | null;
1300
+ /** Exit code (null if still running or killed by signal) */
1301
+ exitCode: number | null;
1302
+ /** Signal that killed the process (null if exited normally) */
1303
+ signal: string | null;
1304
+ /** Duration in ms (null if still running) */
1305
+ duration: number | null;
1306
+ /** The command that was run */
1307
+ command: string;
1308
+ /** Arguments passed to the command */
1309
+ args: string[];
1310
+ /** Working directory */
1311
+ cwd: string;
1312
+ }
1313
+
1314
+ /**
1315
+ * Exit information for a completed process.
1316
+ */
1317
+ export interface ProcessExitInfo {
1318
+ /** Exit code (null if killed by signal) */
1319
+ exitCode: number | null;
1320
+ /** Signal that killed the process (null if exited normally) */
1321
+ signal: string | null;
1322
+ /** Duration in ms from start to exit */
1323
+ duration: number;
1324
+ }
1325
+
1326
+ /**
1327
+ * Buffered output from a piped process.
1328
+ */
1329
+ export interface ProcessOutput {
1330
+ /** All captured stdout data */
1331
+ stdout: string;
1332
+ /** All captured stderr data */
1333
+ stderr: string;
1334
+ }
1335
+
1336
+ /**
1337
+ * Options for listing processes.
1338
+ */
1339
+ export interface ProcessListOptions {
1340
+ /** Filter by lifecycle state */
1341
+ state?: ProcessState;
1342
+ /** Filter by execution mode */
1343
+ mode?: ProcessMode;
1344
+ }
1345
+
1346
+ /**
1347
+ * Options for pruning dead processes.
1348
+ */
1349
+ export interface ProcessPruneOptions {
1350
+ /** Remove exited processes older than this many seconds */
1351
+ olderThan?: number;
1352
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Dot-path utility functions for kadi-core
3
+ *
4
+ * Provides get/set/delete operations on nested objects using dot-notation paths.
5
+ * Used by AgentJsonManager for field-level reads and writes.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const obj = { deploy: { local: { target: 'docker' } } };
10
+ *
11
+ * getByPath(obj, 'deploy.local.target');
12
+ * // → 'docker'
13
+ *
14
+ * setByPath(obj, 'deploy.staging.target', 'akash');
15
+ * // obj is now: { deploy: { local: { target: 'docker' }, staging: { target: 'akash' } } }
16
+ *
17
+ * deleteByPath(obj, 'deploy.staging');
18
+ * // obj is now: { deploy: { local: { target: 'docker' } } }
19
+ * ```
20
+ */
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════
23
+ // PATH PARSING
24
+ // ═══════════════════════════════════════════════════════════════════════
25
+
26
+ /**
27
+ * Split a dot-notation path into segments.
28
+ *
29
+ * If the full path exists as a literal key in the object, it takes precedence
30
+ * over dot-splitting. This handles the (rare) case where a key literally
31
+ * contains a dot.
32
+ *
33
+ * @param path - Dot-notation path (e.g., 'deploy.local.services')
34
+ * @returns Array of path segments
35
+ */
36
+ export function splitPath(path: string): string[] {
37
+ if (!path) return [];
38
+ return path.split('.');
39
+ }
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════
42
+ // GET
43
+ // ═══════════════════════════════════════════════════════════════════════
44
+
45
+ /**
46
+ * Get a value from a nested object by dot-notation path.
47
+ *
48
+ * @param obj - The object to read from
49
+ * @param path - Dot-notation path (e.g., 'deploy.local.target')
50
+ * @returns The value at the path, or undefined if not found
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const config = { deploy: { local: { target: 'docker' } } };
55
+ *
56
+ * getByPath(config, 'deploy.local.target'); // 'docker'
57
+ * getByPath(config, 'deploy.local'); // { target: 'docker' }
58
+ * getByPath(config, 'missing'); // undefined
59
+ * getByPath(config, 'deploy.missing.deep'); // undefined
60
+ * ```
61
+ */
62
+ export function getByPath(obj: Record<string, unknown>, path: string): unknown {
63
+ // Check if the full path exists as a literal key first
64
+ if (path in obj) {
65
+ return obj[path];
66
+ }
67
+
68
+ const segments = splitPath(path);
69
+ let current: unknown = obj;
70
+
71
+ for (const segment of segments) {
72
+ if (current === null || current === undefined || typeof current !== 'object') {
73
+ return undefined;
74
+ }
75
+ current = (current as Record<string, unknown>)[segment];
76
+ }
77
+
78
+ return current;
79
+ }
80
+
81
+ // ═══════════════════════════════════════════════════════════════════════
82
+ // SET
83
+ // ═══════════════════════════════════════════════════════════════════════
84
+
85
+ /**
86
+ * Deep merge two objects. Arrays and scalars are replaced, not merged.
87
+ *
88
+ * @param target - The object to merge into
89
+ * @param source - The object to merge from
90
+ * @returns The merged target object
91
+ */
92
+ export function deepMerge(
93
+ target: Record<string, unknown>,
94
+ source: Record<string, unknown>,
95
+ ): Record<string, unknown> {
96
+ for (const key of Object.keys(source)) {
97
+ const sourceVal = source[key];
98
+ const targetVal = target[key];
99
+
100
+ if (
101
+ sourceVal !== null &&
102
+ typeof sourceVal === 'object' &&
103
+ !Array.isArray(sourceVal) &&
104
+ targetVal !== null &&
105
+ typeof targetVal === 'object' &&
106
+ !Array.isArray(targetVal)
107
+ ) {
108
+ // Both are plain objects — recurse
109
+ target[key] = deepMerge(
110
+ targetVal as Record<string, unknown>,
111
+ sourceVal as Record<string, unknown>,
112
+ );
113
+ } else {
114
+ // Scalar, array, or type mismatch — replace
115
+ target[key] = sourceVal;
116
+ }
117
+ }
118
+ return target;
119
+ }
120
+
121
+ /**
122
+ * Set a value in a nested object by dot-notation path.
123
+ *
124
+ * Creates intermediate objects as needed. For object values, performs
125
+ * a deep merge with existing values (preserving sibling keys).
126
+ * For scalars and arrays, replaces the value.
127
+ *
128
+ * @param obj - The object to modify (mutated in place)
129
+ * @param path - Dot-notation path (e.g., 'deploy.staging.target')
130
+ * @param value - The value to set
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const config = { deploy: { local: { target: 'docker', engine: 'docker' } } };
135
+ *
136
+ * // Set a scalar — replaces
137
+ * setByPath(config, 'deploy.local.target', 'akash');
138
+ * // { deploy: { local: { target: 'akash', engine: 'docker' } } }
139
+ *
140
+ * // Set an object — deep merges
141
+ * setByPath(config, 'deploy.local', { network: 'mainnet' });
142
+ * // { deploy: { local: { target: 'akash', engine: 'docker', network: 'mainnet' } } }
143
+ *
144
+ * // Set on non-existent path — creates intermediates
145
+ * setByPath(config, 'build.arm64.from', 'node:22-slim');
146
+ * // { deploy: {...}, build: { arm64: { from: 'node:22-slim' } } }
147
+ * ```
148
+ */
149
+ export function setByPath(
150
+ obj: Record<string, unknown>,
151
+ path: string,
152
+ value: unknown,
153
+ ): void {
154
+ const segments = splitPath(path);
155
+
156
+ if (segments.length === 0) return;
157
+
158
+ // Walk to the parent of the target key, creating intermediates
159
+ let current: Record<string, unknown> = obj;
160
+ for (let i = 0; i < segments.length - 1; i++) {
161
+ const segment = segments[i]!;
162
+ const next = current[segment];
163
+
164
+ if (next === null || next === undefined || typeof next !== 'object' || Array.isArray(next)) {
165
+ // Create intermediate object
166
+ const newObj: Record<string, unknown> = {};
167
+ current[segment] = newObj;
168
+ current = newObj;
169
+ } else {
170
+ current = next as Record<string, unknown>;
171
+ }
172
+ }
173
+
174
+ const lastSegment = segments[segments.length - 1]!;
175
+ const existing = current[lastSegment];
176
+
177
+ // Deep merge if both existing and new are plain objects
178
+ if (
179
+ value !== null &&
180
+ typeof value === 'object' &&
181
+ !Array.isArray(value) &&
182
+ existing !== null &&
183
+ typeof existing === 'object' &&
184
+ !Array.isArray(existing)
185
+ ) {
186
+ current[lastSegment] = deepMerge(
187
+ existing as Record<string, unknown>,
188
+ value as Record<string, unknown>,
189
+ );
190
+ } else {
191
+ current[lastSegment] = value;
192
+ }
193
+ }
194
+
195
+ // ═══════════════════════════════════════════════════════════════════════
196
+ // DELETE
197
+ // ═══════════════════════════════════════════════════════════════════════
198
+
199
+ /**
200
+ * Delete a key from a nested object by dot-notation path.
201
+ *
202
+ * @param obj - The object to modify (mutated in place)
203
+ * @param path - Dot-notation path to the key to delete
204
+ * @returns true if the key was found and deleted, false otherwise
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const config = { deploy: { local: { target: 'docker' }, staging: { target: 'akash' } } };
209
+ *
210
+ * deleteByPath(config, 'deploy.staging');
211
+ * // config is now: { deploy: { local: { target: 'docker' } } }
212
+ * // returns true
213
+ *
214
+ * deleteByPath(config, 'missing.key');
215
+ * // returns false
216
+ * ```
217
+ */
218
+ export function deleteByPath(obj: Record<string, unknown>, path: string): boolean {
219
+ const segments = splitPath(path);
220
+
221
+ if (segments.length === 0) return false;
222
+
223
+ // Walk to the parent of the target key
224
+ let current: unknown = obj;
225
+ for (let i = 0; i < segments.length - 1; i++) {
226
+ const segment = segments[i]!;
227
+ if (current === null || current === undefined || typeof current !== 'object') {
228
+ return false;
229
+ }
230
+ current = (current as Record<string, unknown>)[segment];
231
+ }
232
+
233
+ if (current === null || current === undefined || typeof current !== 'object') {
234
+ return false;
235
+ }
236
+
237
+ const lastSegment = segments[segments.length - 1]!;
238
+ const parent = current as Record<string, unknown>;
239
+
240
+ if (!(lastSegment in parent)) {
241
+ return false;
242
+ }
243
+
244
+ delete parent[lastSegment];
245
+ return true;
246
+ }