@kindgi/handler-runtime 0.0.0-bootstrap.0 → 0.1.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/LICENSE +201 -0
- package/README.md +218 -2
- package/dist/build-extensions.d.ts +75 -0
- package/dist/build-extensions.d.ts.map +1 -0
- package/dist/build-extensions.js +27 -0
- package/dist/build-extensions.js.map +1 -0
- package/dist/discovery.d.ts +19 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +81 -0
- package/dist/discovery.js.map +1 -0
- package/dist/entrypoint.d.ts +10 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +24 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/handler-runner.d.ts +127 -0
- package/dist/handler-runner.d.ts.map +1 -0
- package/dist/handler-runner.js +318 -0
- package/dist/handler-runner.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/kindgi-index-main.d.ts +2 -0
- package/dist/kindgi-index-main.d.ts.map +1 -0
- package/dist/kindgi-index-main.js +15 -0
- package/dist/kindgi-index-main.js.map +1 -0
- package/dist/kindgi-index.d.ts +316 -0
- package/dist/kindgi-index.d.ts.map +1 -0
- package/dist/kindgi-index.js +1201 -0
- package/dist/kindgi-index.js.map +1 -0
- package/dist/pack-env.d.ts +66 -0
- package/dist/pack-env.d.ts.map +1 -0
- package/dist/pack-env.js +97 -0
- package/dist/pack-env.js.map +1 -0
- package/dist/pack-service/index.d.ts +7 -0
- package/dist/pack-service/index.d.ts.map +1 -0
- package/dist/pack-service/index.js +6 -0
- package/dist/pack-service/index.js.map +1 -0
- package/dist/pack-service/main.d.ts +47 -0
- package/dist/pack-service/main.d.ts.map +1 -0
- package/dist/pack-service/main.js +201 -0
- package/dist/pack-service/main.js.map +1 -0
- package/dist/pack-service/service.d.ts +61 -0
- package/dist/pack-service/service.d.ts.map +1 -0
- package/dist/pack-service/service.js +341 -0
- package/dist/pack-service/service.js.map +1 -0
- package/dist/pack-service/supervisor.d.ts +138 -0
- package/dist/pack-service/supervisor.d.ts.map +1 -0
- package/dist/pack-service/supervisor.js +424 -0
- package/dist/pack-service/supervisor.js.map +1 -0
- package/dist/protocol.d.ts +104 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +116 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +87 -4
- package/src/build-extensions.ts +100 -0
- package/src/discovery.ts +89 -0
- package/src/entrypoint.ts +23 -0
- package/src/handler-runner.ts +500 -0
- package/src/index.ts +66 -0
- package/src/kindgi-index-main.ts +17 -0
- package/src/kindgi-index.ts +1605 -0
- package/src/pack-env.ts +148 -0
- package/src/pack-service/index.ts +17 -0
- package/src/pack-service/main.ts +246 -0
- package/src/pack-service/service.ts +478 -0
- package/src/pack-service/supervisor.ts +600 -0
- package/src/protocol.ts +214 -0
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* `kindgi-index` — the pack indexer, which writes a pack's `index.json`.
|
|
5
|
+
* `kindgi dev` runs it over the dev bundles; a pack image's indexer
|
|
6
|
+
* stage runs it (process entry `kindgi-index-main`, bundled as
|
|
7
|
+
* `dist/kindgi-index.mjs`) over the image's bundles, and `kindgi build`
|
|
8
|
+
* runs the same bundle locally, so the two indexes compare.
|
|
9
|
+
*
|
|
10
|
+
* The indexer:
|
|
11
|
+
*
|
|
12
|
+
* 1. Loads `kindgi.config.ts` (or `.js` / `.mjs`) at the pack root
|
|
13
|
+
* to read `pack.id` / `pack.version` + optional `discovery` glob
|
|
14
|
+
* overrides.
|
|
15
|
+
* 2. Discovers files under the four default folders — `tools/`,
|
|
16
|
+
* `guardrails/`, `agents/`, `flows/` — using the config's
|
|
17
|
+
* discovery patterns; or, indexing a build, takes the files a
|
|
18
|
+
* bundle map lists (`bundleMap`), classified by the same patterns.
|
|
19
|
+
* 3. Dynamically imports each file (from its bundle, given a map).
|
|
20
|
+
* Reads the `default` export
|
|
21
|
+
* (accepting either the raw primitive shape or a `Result`-wrapped
|
|
22
|
+
* envelope from `defineTool` / `defineGuardrail` / etc.).
|
|
23
|
+
* 4. Kind-maps the discovered file by its default folder (primary) or
|
|
24
|
+
* by structural shape of the default export (fallback for
|
|
25
|
+
* custom-pattern files).
|
|
26
|
+
* 5. For every Zod-typed schema (`tool.inputZod`, `tool.outputZod`,
|
|
27
|
+
* `check.configZod`) calls `@kindgi/schema.toJSONSchemaSync` to
|
|
28
|
+
* derive the JSON Schema wire form. When JSON Schema is authored
|
|
29
|
+
* directly, it is emitted verbatim.
|
|
30
|
+
* 6. Assembles a `v: 1` `index.json` envelope and writes it
|
|
31
|
+
* atomically (write-to-tmp, then rename) to the configured output
|
|
32
|
+
* path.
|
|
33
|
+
*
|
|
34
|
+
* ## Fail loud at every boundary
|
|
35
|
+
*
|
|
36
|
+
* Missing config, missing default export, kind mismatch, Zod conversion
|
|
37
|
+
* failure, filesystem write error — each is a distinct typed
|
|
38
|
+
* `IndexerError` variant with the file path attached. No silent skips.
|
|
39
|
+
*
|
|
40
|
+
* ## Determinism
|
|
41
|
+
*
|
|
42
|
+
* The integrity gate (locally verify the server's build) diffs an indexer output computed on the developer's
|
|
43
|
+
* laptop against the server's output. Byte-identical inputs must
|
|
44
|
+
* produce byte-identical outputs. All list fields are sorted
|
|
45
|
+
* lexicographically by id; JSON serialization uses compact stable
|
|
46
|
+
* output; `publishedAt` is accepted as an explicit `opts.publishedAt`
|
|
47
|
+
* for reproducible builds (the pack build passes it) and defaults to
|
|
48
|
+
* `new Date()` only for local dev (documented in the README).
|
|
49
|
+
*/
|
|
50
|
+
import * as fs from 'node:fs/promises';
|
|
51
|
+
import * as path from 'node:path';
|
|
52
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
53
|
+
import { loadFlow } from '@kindgi/flow';
|
|
54
|
+
import { isZodSchema, loadZodConverterSync, toJSONSchemaSync } from '@kindgi/schema';
|
|
55
|
+
import { parse as parseToml } from 'smol-toml';
|
|
56
|
+
import { TEST_FILE_REGEX, globStaticPrefix, globToRegex } from './discovery.js';
|
|
57
|
+
import { resolvePackEnv } from './pack-env.js';
|
|
58
|
+
// -----------------------------------------------------------------------
|
|
59
|
+
// Public envelope + shape types
|
|
60
|
+
// -----------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Envelope version emitted on `index.json`. Bump on breaking change;
|
|
63
|
+
* additive fields on an existing shape don't bump. Consumers (the
|
|
64
|
+
* controller) MUST refuse an envelope version they
|
|
65
|
+
* don't recognize.
|
|
66
|
+
*/
|
|
67
|
+
export const INDEX_ENVELOPE_VERSION = 1;
|
|
68
|
+
/** The language of a loaded config's pack code. */
|
|
69
|
+
export function packLanguage(config) {
|
|
70
|
+
return config.language ?? 'node';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Default discovery patterns. Extensions
|
|
74
|
+
* `.ts` / `.js` / `.mjs` are all accepted — a pack build may transpile
|
|
75
|
+
* `.ts` to `.js` before the indexer runs inside the Dockerfile's indexer
|
|
76
|
+
* stage, but the indexer accepts either shape so local development (no
|
|
77
|
+
* bundle) and CI-built images (post-bundle) share the same code path.
|
|
78
|
+
*/
|
|
79
|
+
export const DEFAULT_DISCOVERY = {
|
|
80
|
+
tools: 'tools/**/*.{ts,js,mjs}',
|
|
81
|
+
guardrails: 'guardrails/**/*.{ts,js,mjs}',
|
|
82
|
+
agents: 'agents/**/*.{ts,js,mjs}',
|
|
83
|
+
flows: 'flows/**/*.{ts,js,mjs}',
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Default discovery patterns of a Python pack (`python -m kindgi.pack index`
|
|
87
|
+
* reads the same keys from `[tool.kindgi.discovery]`).
|
|
88
|
+
*/
|
|
89
|
+
export const DEFAULT_PYTHON_DISCOVERY = {
|
|
90
|
+
tools: 'tools/**/*.py',
|
|
91
|
+
guardrails: 'guardrails/**/*.py',
|
|
92
|
+
agents: 'agents/**/*.py',
|
|
93
|
+
flows: 'flows/**/*.py',
|
|
94
|
+
};
|
|
95
|
+
/** A config's discovery patterns with the language's defaults filled in. */
|
|
96
|
+
export function resolveDiscovery(discovery, language = 'node') {
|
|
97
|
+
const defaults = language === 'python' ? DEFAULT_PYTHON_DISCOVERY : DEFAULT_DISCOVERY;
|
|
98
|
+
return { ...defaults, ...(discovery ?? {}) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Envelope version for the flow payload embedded inside `index.json`.
|
|
102
|
+
* Distinct from the top-level `v` envelope so the flow substrate can
|
|
103
|
+
* evolve its own shape independently — matches the `packages/flow`
|
|
104
|
+
* schema-version convention (currently 1.8.0, which added input and
|
|
105
|
+
* output mappings). Additive fields don't bump this.
|
|
106
|
+
*/
|
|
107
|
+
export const KERNEL_PAYLOAD_VERSION = 1;
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
// Entry point
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Run the indexer stage. Never throws — every failure is captured as a
|
|
113
|
+
* typed `IndexerError`.
|
|
114
|
+
*/
|
|
115
|
+
export async function runIndexer(opts) {
|
|
116
|
+
const packDir = path.resolve(opts.packDir);
|
|
117
|
+
const importModule = opts.importModule ?? defaultImportModule;
|
|
118
|
+
const configResult = await loadConfig(packDir, opts.configPath, importModule);
|
|
119
|
+
if (configResult.kind === 'err')
|
|
120
|
+
return configResult;
|
|
121
|
+
const config = configResult.value;
|
|
122
|
+
if (packLanguage(config) !== 'node') {
|
|
123
|
+
return {
|
|
124
|
+
kind: 'err',
|
|
125
|
+
error: {
|
|
126
|
+
code: 'language-mismatch',
|
|
127
|
+
message: `${packDir} is a ${packLanguage(config)} pack; index it with its own indexer (python -m kindgi.pack index)`,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const env = resolvePackEnv(config.env);
|
|
132
|
+
if (env.kind === 'err') {
|
|
133
|
+
return {
|
|
134
|
+
kind: 'err',
|
|
135
|
+
error: { code: 'config-invalid', field: 'env', message: `kindgi.config: ${env.message}` },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const discovery = resolveDiscovery(config.discovery);
|
|
139
|
+
const bundleMap = opts.bundleMap;
|
|
140
|
+
const moduleRoot = path.resolve(opts.moduleRoot ?? packDir);
|
|
141
|
+
const discovered = bundleMap !== undefined
|
|
142
|
+
? discoverInBundleMap(bundleMap, discovery)
|
|
143
|
+
: await discoverFiles(packDir, discovery);
|
|
144
|
+
if (discovered.length === 0) {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'err',
|
|
147
|
+
error: {
|
|
148
|
+
code: 'discovery-empty',
|
|
149
|
+
message: bundleMap !== undefined
|
|
150
|
+
? 'The bundle map lists no file the discovery patterns match. Check kindgi.config.ts discovery patterns.'
|
|
151
|
+
: `Indexer discovered zero files under ${packDir}. Check kindgi.config.ts discovery patterns.`,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const zodConverter = loadZodConverterSync();
|
|
156
|
+
const tools = [];
|
|
157
|
+
const guardrails = [];
|
|
158
|
+
const agents = [];
|
|
159
|
+
const flows = [];
|
|
160
|
+
const fileErrors = [];
|
|
161
|
+
for (const { relPath, expectedKind } of discovered) {
|
|
162
|
+
const bundle = bundleMap?.[relPath];
|
|
163
|
+
const absPath = bundle !== undefined ? path.resolve(moduleRoot, bundle) : path.join(packDir, relPath);
|
|
164
|
+
const fileUrl = pathToFileURL(absPath).href;
|
|
165
|
+
let module_;
|
|
166
|
+
try {
|
|
167
|
+
module_ = await importModule(fileUrl);
|
|
168
|
+
}
|
|
169
|
+
catch (cause) {
|
|
170
|
+
fileErrors.push({
|
|
171
|
+
code: 'file-import-failed',
|
|
172
|
+
message: `Failed to import ${relPath}: ${stringifyError(cause)}`,
|
|
173
|
+
filePath: relPath,
|
|
174
|
+
cause: serializeCause(cause),
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const defExport = extractDefaultExport(module_);
|
|
179
|
+
if (defExport === undefined) {
|
|
180
|
+
fileErrors.push({
|
|
181
|
+
code: 'no-default-export',
|
|
182
|
+
message: `File ${relPath} has no default export`,
|
|
183
|
+
filePath: relPath,
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const errResult = detectResultError(defExport);
|
|
188
|
+
if (errResult !== undefined) {
|
|
189
|
+
fileErrors.push({
|
|
190
|
+
code: 'manifest-validation-failed',
|
|
191
|
+
message: `File ${relPath} default export is a Result-wrapped error: ${resultErrorMessage(errResult)}`,
|
|
192
|
+
filePath: relPath,
|
|
193
|
+
cause: errResult,
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const unwrapped = unwrapResult(defExport);
|
|
198
|
+
const detected = detectKind(unwrapped);
|
|
199
|
+
if (detected === undefined) {
|
|
200
|
+
fileErrors.push({
|
|
201
|
+
code: 'ambiguous-kind',
|
|
202
|
+
message: `File ${relPath} default export does not match any primitive shape (tool / guardrail / agent / flow)`,
|
|
203
|
+
filePath: relPath,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (expectedKind !== undefined && detected !== expectedKind) {
|
|
208
|
+
fileErrors.push({
|
|
209
|
+
code: 'kind-mismatch',
|
|
210
|
+
message: `File ${relPath} is under the default folder for kind '${expectedKind}' but default-exports a '${detected}'`,
|
|
211
|
+
filePath: relPath,
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
switch (detected) {
|
|
216
|
+
case 'tool': {
|
|
217
|
+
const built = buildTool(unwrapped, relPath, zodConverter);
|
|
218
|
+
if (built.kind === 'err') {
|
|
219
|
+
fileErrors.push(built.error);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
tools.push(built.value);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'guardrail': {
|
|
226
|
+
const built = buildGuardrail(unwrapped, relPath, zodConverter);
|
|
227
|
+
if (built.kind === 'err') {
|
|
228
|
+
fileErrors.push(built.error);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
guardrails.push(built.value);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 'agent': {
|
|
235
|
+
const built = buildAgent(unwrapped, relPath);
|
|
236
|
+
if (built.kind === 'err') {
|
|
237
|
+
fileErrors.push(built.error);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
agents.push(built.value);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'flow': {
|
|
244
|
+
const built = buildGraph(unwrapped, relPath);
|
|
245
|
+
if (built.kind === 'err') {
|
|
246
|
+
fileErrors.push(built.error);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
flows.push(built.value);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const outputPath = opts.outputPath !== undefined
|
|
255
|
+
? path.resolve(opts.outputPath)
|
|
256
|
+
: path.join(packDir, 'index.json');
|
|
257
|
+
const artifactVersion = opts.artifactVersion ?? (await computeAutoArtifactVersion(outputPath));
|
|
258
|
+
const publishedAt = opts.publishedAt ?? new Date().toISOString();
|
|
259
|
+
const index = {
|
|
260
|
+
v: INDEX_ENVELOPE_VERSION,
|
|
261
|
+
packId: config.pack.id,
|
|
262
|
+
packVersion: config.pack.version,
|
|
263
|
+
artifactVersion,
|
|
264
|
+
publishedAt,
|
|
265
|
+
tools: sortById(tools),
|
|
266
|
+
guardrails: sortById(guardrails),
|
|
267
|
+
agents: sortById(agents),
|
|
268
|
+
flows: sortById(flows),
|
|
269
|
+
...(env.value !== undefined && { env: env.value }),
|
|
270
|
+
};
|
|
271
|
+
const written = await atomicWriteJson(outputPath, index);
|
|
272
|
+
if (written.kind === 'err')
|
|
273
|
+
return written;
|
|
274
|
+
return {
|
|
275
|
+
kind: 'ok',
|
|
276
|
+
value: {
|
|
277
|
+
packId: config.pack.id,
|
|
278
|
+
packVersion: config.pack.version,
|
|
279
|
+
artifactVersion,
|
|
280
|
+
publishedAt,
|
|
281
|
+
counts: {
|
|
282
|
+
tools: tools.length,
|
|
283
|
+
guardrails: guardrails.length,
|
|
284
|
+
agents: agents.length,
|
|
285
|
+
flows: flows.length,
|
|
286
|
+
},
|
|
287
|
+
outputPath,
|
|
288
|
+
fileErrors,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// -----------------------------------------------------------------------
|
|
293
|
+
// CLI entry
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
/**
|
|
296
|
+
* The `kindgi-index` command (process entry: `kindgi-index-main`) — a
|
|
297
|
+
* pack image's indexer stage runs it over the image's bundles. Args:
|
|
298
|
+
*
|
|
299
|
+
* --pack-dir <path> root of the pack (required)
|
|
300
|
+
* --config <path> explicit config file (optional)
|
|
301
|
+
* --output <path> explicit output path (optional)
|
|
302
|
+
* --bundle-map <path> index a build: the bundle map (JSON, optional)
|
|
303
|
+
* --module-root <path> where the bundle map's paths resolve (optional)
|
|
304
|
+
* --artifact-version <str> pin artifact version (optional)
|
|
305
|
+
* --published-at <iso> pin publishedAt (optional; enables reproducible builds)
|
|
306
|
+
* --help
|
|
307
|
+
* --version
|
|
308
|
+
*/
|
|
309
|
+
export async function main(argv) {
|
|
310
|
+
const parsed = parseArgs(argv);
|
|
311
|
+
if (parsed.kind === 'help') {
|
|
312
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
if (parsed.kind === 'version') {
|
|
316
|
+
process.stdout.write('kindgi-index v0.0.0\n');
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
if (parsed.kind === 'err') {
|
|
320
|
+
process.stderr.write(`kindgi-index: ${parsed.error}\n`);
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
let options = parsed.value.options;
|
|
324
|
+
if (parsed.value.bundleMapPath !== undefined) {
|
|
325
|
+
const bundleMap = await readBundleMap(parsed.value.bundleMapPath);
|
|
326
|
+
if (bundleMap.kind === 'err') {
|
|
327
|
+
process.stderr.write(`kindgi-index: ${bundleMap.message}\n`);
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
options = { ...options, bundleMap: bundleMap.value };
|
|
331
|
+
}
|
|
332
|
+
const outcome = await runIndexer(options);
|
|
333
|
+
if (outcome.kind === 'err') {
|
|
334
|
+
process.stderr.write(`kindgi-index: ${outcome.error.code}: ${outcome.error.message}\n`);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
process.stdout.write(`${JSON.stringify(outcome.value, null, 2)}\n`);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
export const HELP_TEXT = `kindgi-index — build-time pack manifest indexer
|
|
341
|
+
|
|
342
|
+
Usage:
|
|
343
|
+
kindgi-index --pack-dir <path> [--config <path>] [--output <path>]
|
|
344
|
+
[--bundle-map <path> [--module-root <path>]]
|
|
345
|
+
[--artifact-version <str>] [--published-at <iso>]
|
|
346
|
+
|
|
347
|
+
Options:
|
|
348
|
+
--pack-dir <path> Root of the pack (required).
|
|
349
|
+
--config <path> Explicit kindgi.config.* path.
|
|
350
|
+
--output <path> Output path for index.json (default: <packDir>/index.json).
|
|
351
|
+
--bundle-map <path> Index a build: JSON mapping each source path to its bundle.
|
|
352
|
+
--module-root <path> Where the bundle map's paths resolve (default: --pack-dir).
|
|
353
|
+
--artifact-version <str> Pin artifact version (default: YYYYMMDD.N auto).
|
|
354
|
+
--published-at <iso> Pin publishedAt for reproducible builds.
|
|
355
|
+
--help Show this help.
|
|
356
|
+
--version Print version and exit.`;
|
|
357
|
+
function parseArgs(argv) {
|
|
358
|
+
let packDir;
|
|
359
|
+
let configPath;
|
|
360
|
+
let outputPath;
|
|
361
|
+
let artifactVersion;
|
|
362
|
+
let publishedAt;
|
|
363
|
+
let bundleMapPath;
|
|
364
|
+
let moduleRoot;
|
|
365
|
+
let i = 0;
|
|
366
|
+
while (i < argv.length) {
|
|
367
|
+
const arg = argv[i];
|
|
368
|
+
if (arg === undefined)
|
|
369
|
+
break;
|
|
370
|
+
if (arg === '--help' || arg === '-h')
|
|
371
|
+
return { kind: 'help' };
|
|
372
|
+
if (arg === '--version' || arg === '-v')
|
|
373
|
+
return { kind: 'version' };
|
|
374
|
+
const takeValue = () => {
|
|
375
|
+
const value = argv[i + 1];
|
|
376
|
+
if (value === undefined)
|
|
377
|
+
return { err: `${arg} requires a value` };
|
|
378
|
+
return value;
|
|
379
|
+
};
|
|
380
|
+
if (arg === '--pack-dir') {
|
|
381
|
+
const value = takeValue();
|
|
382
|
+
if (typeof value !== 'string')
|
|
383
|
+
return { kind: 'err', error: value.err };
|
|
384
|
+
packDir = value;
|
|
385
|
+
i += 2;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (arg === '--config') {
|
|
389
|
+
const value = takeValue();
|
|
390
|
+
if (typeof value !== 'string')
|
|
391
|
+
return { kind: 'err', error: value.err };
|
|
392
|
+
configPath = value;
|
|
393
|
+
i += 2;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (arg === '--output') {
|
|
397
|
+
const value = takeValue();
|
|
398
|
+
if (typeof value !== 'string')
|
|
399
|
+
return { kind: 'err', error: value.err };
|
|
400
|
+
outputPath = value;
|
|
401
|
+
i += 2;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (arg === '--artifact-version') {
|
|
405
|
+
const value = takeValue();
|
|
406
|
+
if (typeof value !== 'string')
|
|
407
|
+
return { kind: 'err', error: value.err };
|
|
408
|
+
artifactVersion = value;
|
|
409
|
+
i += 2;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (arg === '--published-at') {
|
|
413
|
+
const value = takeValue();
|
|
414
|
+
if (typeof value !== 'string')
|
|
415
|
+
return { kind: 'err', error: value.err };
|
|
416
|
+
publishedAt = value;
|
|
417
|
+
i += 2;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (arg === '--bundle-map') {
|
|
421
|
+
const value = takeValue();
|
|
422
|
+
if (typeof value !== 'string')
|
|
423
|
+
return { kind: 'err', error: value.err };
|
|
424
|
+
bundleMapPath = value;
|
|
425
|
+
i += 2;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (arg === '--module-root') {
|
|
429
|
+
const value = takeValue();
|
|
430
|
+
if (typeof value !== 'string')
|
|
431
|
+
return { kind: 'err', error: value.err };
|
|
432
|
+
moduleRoot = value;
|
|
433
|
+
i += 2;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
return { kind: 'err', error: `Unknown argument: ${arg}` };
|
|
437
|
+
}
|
|
438
|
+
if (packDir === undefined) {
|
|
439
|
+
return { kind: 'err', error: '--pack-dir is required' };
|
|
440
|
+
}
|
|
441
|
+
if (moduleRoot !== undefined && bundleMapPath === undefined) {
|
|
442
|
+
return { kind: 'err', error: '--module-root goes with --bundle-map' };
|
|
443
|
+
}
|
|
444
|
+
const options = {
|
|
445
|
+
packDir,
|
|
446
|
+
...(configPath !== undefined && { configPath }),
|
|
447
|
+
...(outputPath !== undefined && { outputPath }),
|
|
448
|
+
...(artifactVersion !== undefined && { artifactVersion }),
|
|
449
|
+
...(publishedAt !== undefined && { publishedAt }),
|
|
450
|
+
...(moduleRoot !== undefined && { moduleRoot }),
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
kind: 'ok',
|
|
454
|
+
value: { options, ...(bundleMapPath !== undefined && { bundleMapPath }) },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* A bundle map file: a JSON object of source path → bundle path, both
|
|
459
|
+
* strings (`kindgi build` writes `dist/bundle-map.json`).
|
|
460
|
+
*/
|
|
461
|
+
export async function readBundleMap(file) {
|
|
462
|
+
let parsed;
|
|
463
|
+
try {
|
|
464
|
+
parsed = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
465
|
+
}
|
|
466
|
+
catch (cause) {
|
|
467
|
+
return { kind: 'err', message: `Cannot read the bundle map ${file}: ${stringifyError(cause)}` };
|
|
468
|
+
}
|
|
469
|
+
if (parsed === null ||
|
|
470
|
+
typeof parsed !== 'object' ||
|
|
471
|
+
Array.isArray(parsed) ||
|
|
472
|
+
!Object.values(parsed).every((v) => typeof v === 'string' && v !== '')) {
|
|
473
|
+
return {
|
|
474
|
+
kind: 'err',
|
|
475
|
+
message: `The bundle map ${file} must be a JSON object of source path → bundle path.`,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
return { kind: 'ok', value: parsed };
|
|
479
|
+
}
|
|
480
|
+
// -----------------------------------------------------------------------
|
|
481
|
+
// Config loading
|
|
482
|
+
// -----------------------------------------------------------------------
|
|
483
|
+
/** Config file names looked up at a pack root, in order. */
|
|
484
|
+
export const KINDGI_CONFIG_FILENAMES = [
|
|
485
|
+
'kindgi.config.ts',
|
|
486
|
+
// `.mts` is always ESM — the config name for a CommonJS app, where a
|
|
487
|
+
// `.ts` config would be reparsed with a Node warning.
|
|
488
|
+
'kindgi.config.mts',
|
|
489
|
+
'kindgi.config.mjs',
|
|
490
|
+
'kindgi.config.js',
|
|
491
|
+
'kindgi.config.cjs',
|
|
492
|
+
];
|
|
493
|
+
/**
|
|
494
|
+
* Load and minimally validate a pack's `kindgi.config.*` — the one
|
|
495
|
+
* loader behind the indexer and any tooling that reads pack config.
|
|
496
|
+
* Never throws:
|
|
497
|
+
* `config-not-found` when no config file exists, `config-parse-failed`
|
|
498
|
+
* (with the underlying cause) when it exists but cannot be imported or
|
|
499
|
+
* lacks `pack.id` / `pack.version`. Imports are cache-busted by mtime so
|
|
500
|
+
* a long-running process sees edits.
|
|
501
|
+
*/
|
|
502
|
+
export async function loadKindgiConfig(packDir, options = {}) {
|
|
503
|
+
return loadConfig(path.resolve(packDir), options.configPath, options.importModule ?? defaultImportModule);
|
|
504
|
+
}
|
|
505
|
+
/** `pyproject.toml` — a Python pack keeps its config in the `[tool.kindgi]` table. */
|
|
506
|
+
export const PYPROJECT_FILENAME = 'pyproject.toml';
|
|
507
|
+
/**
|
|
508
|
+
* Where a pack's config lives: the first `kindgi.config.*` at `packDir`
|
|
509
|
+
* (`KINDGI_CONFIG_FILENAMES` order), else a `pyproject.toml` with a
|
|
510
|
+
* `[tool.kindgi]` table. `undefined` when neither — a `pyproject.toml`
|
|
511
|
+
* without the table is a Python project, not a pack.
|
|
512
|
+
*/
|
|
513
|
+
export async function findKindgiConfig(packDir) {
|
|
514
|
+
for (const name of KINDGI_CONFIG_FILENAMES) {
|
|
515
|
+
const candidate = path.join(packDir, name);
|
|
516
|
+
if (await exists(candidate))
|
|
517
|
+
return { path: candidate, format: 'module' };
|
|
518
|
+
}
|
|
519
|
+
const pyproject = path.join(packDir, PYPROJECT_FILENAME);
|
|
520
|
+
let text;
|
|
521
|
+
try {
|
|
522
|
+
text = await fs.readFile(pyproject, 'utf8');
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
const tool = parseToml(text).tool;
|
|
529
|
+
return isObject(tool) && 'kindgi' in tool
|
|
530
|
+
? { path: pyproject, format: 'pyproject' }
|
|
531
|
+
: undefined;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Unparsable: a pack only if it clearly meant to be one (the loader reports the error).
|
|
535
|
+
return /^\s*\[tool\.kindgi[\].]/m.test(text)
|
|
536
|
+
? { path: pyproject, format: 'pyproject' }
|
|
537
|
+
: undefined;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function exists(file) {
|
|
541
|
+
try {
|
|
542
|
+
await fs.access(file);
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function loadConfig(packDir, explicitPath, importModule) {
|
|
550
|
+
let file;
|
|
551
|
+
if (explicitPath !== undefined) {
|
|
552
|
+
const resolved = path.resolve(explicitPath);
|
|
553
|
+
if (await exists(resolved)) {
|
|
554
|
+
const format = path.basename(resolved) === PYPROJECT_FILENAME ? 'pyproject' : 'module';
|
|
555
|
+
file = { path: resolved, format };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
file = await findKindgiConfig(packDir);
|
|
560
|
+
}
|
|
561
|
+
if (file === undefined) {
|
|
562
|
+
return {
|
|
563
|
+
kind: 'err',
|
|
564
|
+
error: {
|
|
565
|
+
code: 'config-not-found',
|
|
566
|
+
message: `No kindgi.config.{ts,mts,mjs,js,cjs}, or pyproject.toml with a [tool.kindgi] table, found at pack root ${packDir}`,
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const read = file.format === 'pyproject'
|
|
571
|
+
? await readPyprojectTable(file.path)
|
|
572
|
+
: await importConfigModule(file.path, importModule);
|
|
573
|
+
if (read.kind === 'err')
|
|
574
|
+
return read;
|
|
575
|
+
const checked = checkConfig(read.value, file.path);
|
|
576
|
+
if (checked.kind === 'err')
|
|
577
|
+
return checked;
|
|
578
|
+
const record = checked.value;
|
|
579
|
+
return {
|
|
580
|
+
kind: 'ok',
|
|
581
|
+
value: file.format === 'pyproject' && record.language === undefined
|
|
582
|
+
? { ...record, language: 'python' }
|
|
583
|
+
: record,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function parseFailed(filePath, message, cause) {
|
|
587
|
+
return {
|
|
588
|
+
kind: 'err',
|
|
589
|
+
error: {
|
|
590
|
+
code: 'config-parse-failed',
|
|
591
|
+
message,
|
|
592
|
+
filePath,
|
|
593
|
+
...(cause !== undefined && { cause: serializeCause(cause) }),
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
async function importConfigModule(filePath, importModule) {
|
|
598
|
+
let module_;
|
|
599
|
+
try {
|
|
600
|
+
module_ = await importModule(pathToFileURL(filePath).href);
|
|
601
|
+
}
|
|
602
|
+
catch (cause) {
|
|
603
|
+
return parseFailed(filePath, `Failed to import ${filePath}: ${stringifyError(cause)}`, cause);
|
|
604
|
+
}
|
|
605
|
+
const defExport = extractDefaultExport(module_);
|
|
606
|
+
if (defExport === undefined || typeof defExport !== 'object' || defExport === null) {
|
|
607
|
+
return parseFailed(filePath, `Config file ${filePath} has no default export (or it is not an object)`);
|
|
608
|
+
}
|
|
609
|
+
return { kind: 'ok', value: defExport };
|
|
610
|
+
}
|
|
611
|
+
async function readPyprojectTable(filePath) {
|
|
612
|
+
let document;
|
|
613
|
+
try {
|
|
614
|
+
document = parseToml(await fs.readFile(filePath, 'utf8'));
|
|
615
|
+
}
|
|
616
|
+
catch (cause) {
|
|
617
|
+
return parseFailed(filePath, `Failed to read ${filePath}: ${stringifyError(cause)}`, cause);
|
|
618
|
+
}
|
|
619
|
+
const tool = document.tool;
|
|
620
|
+
const table = isObject(tool) ? tool.kindgi : undefined;
|
|
621
|
+
if (!isObject(table)) {
|
|
622
|
+
return {
|
|
623
|
+
kind: 'err',
|
|
624
|
+
error: {
|
|
625
|
+
code: 'config-not-found',
|
|
626
|
+
message: `${filePath} has no [tool.kindgi] table`,
|
|
627
|
+
filePath,
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return { kind: 'ok', value: table };
|
|
632
|
+
}
|
|
633
|
+
/** The checks every config passes, whatever file it came from. */
|
|
634
|
+
function checkConfig(record, filePath) {
|
|
635
|
+
const pack = record.pack;
|
|
636
|
+
if (typeof pack !== 'object' || pack === null) {
|
|
637
|
+
return parseFailed(filePath, `Config file ${filePath}: 'pack' field is missing or not an object`);
|
|
638
|
+
}
|
|
639
|
+
const packRec = pack;
|
|
640
|
+
if (typeof packRec.id !== 'string' || packRec.id.length === 0) {
|
|
641
|
+
return parseFailed(filePath, `Config file ${filePath}: 'pack.id' is missing or not a non-empty string`);
|
|
642
|
+
}
|
|
643
|
+
if (typeof packRec.version !== 'string' || packRec.version.length === 0) {
|
|
644
|
+
return parseFailed(filePath, `Config file ${filePath}: 'pack.version' is missing or not a non-empty string`);
|
|
645
|
+
}
|
|
646
|
+
if (record.language !== undefined && record.language !== 'node' && record.language !== 'python') {
|
|
647
|
+
return parseFailed(filePath, `Config file ${filePath}: 'language' must be "node" or "python"`);
|
|
648
|
+
}
|
|
649
|
+
return { kind: 'ok', value: record };
|
|
650
|
+
}
|
|
651
|
+
const DEFAULT_FOLDERS = {
|
|
652
|
+
tool: 'tools',
|
|
653
|
+
guardrail: 'guardrails',
|
|
654
|
+
agent: 'agents',
|
|
655
|
+
flow: 'flows',
|
|
656
|
+
};
|
|
657
|
+
async function discoverFiles(packDir, discovery) {
|
|
658
|
+
const seen = new Map();
|
|
659
|
+
for (const { kind, pattern } of kindPatterns(discovery)) {
|
|
660
|
+
for (const rel of await globMatch(packDir, pattern))
|
|
661
|
+
classifyDiscovered(seen, rel, kind);
|
|
662
|
+
}
|
|
663
|
+
return sortedDiscovered(seen);
|
|
664
|
+
}
|
|
665
|
+
/** A bundle map's source paths, classified as `discoverFiles` classifies what it finds. */
|
|
666
|
+
function discoverInBundleMap(bundleMap, discovery) {
|
|
667
|
+
const sources = Object.keys(bundleMap).sort();
|
|
668
|
+
const seen = new Map();
|
|
669
|
+
for (const { kind, pattern } of kindPatterns(discovery)) {
|
|
670
|
+
const regex = globToRegex(pattern);
|
|
671
|
+
for (const rel of sources)
|
|
672
|
+
if (regex.test(rel))
|
|
673
|
+
classifyDiscovered(seen, rel, kind);
|
|
674
|
+
}
|
|
675
|
+
return sortedDiscovered(seen);
|
|
676
|
+
}
|
|
677
|
+
function kindPatterns(discovery) {
|
|
678
|
+
return [
|
|
679
|
+
{ kind: 'tool', pattern: discovery.tools },
|
|
680
|
+
{ kind: 'guardrail', pattern: discovery.guardrails },
|
|
681
|
+
{ kind: 'agent', pattern: discovery.agents },
|
|
682
|
+
{ kind: 'flow', pattern: discovery.flows },
|
|
683
|
+
];
|
|
684
|
+
}
|
|
685
|
+
/** Record `rel`, matched by `kind`'s pattern, unless it's a test file or already seen. */
|
|
686
|
+
function classifyDiscovered(seen, rel, kind) {
|
|
687
|
+
// Skip test / spec files — they live next to the primitive
|
|
688
|
+
// (`tools/echo/index.ts` + `tools/echo/index.test.ts`)
|
|
689
|
+
// and would break the indexer if imported as a primitive
|
|
690
|
+
// module. Matches conventional test-file suffixes across
|
|
691
|
+
// vitest / jest / node:test.
|
|
692
|
+
if (TEST_FILE_REGEX.test(rel))
|
|
693
|
+
return;
|
|
694
|
+
if (seen.has(rel))
|
|
695
|
+
return;
|
|
696
|
+
const firstSegment = rel.split('/')[0];
|
|
697
|
+
const inDefaultFolder = firstSegment === DEFAULT_FOLDERS[kind];
|
|
698
|
+
seen.set(rel, inDefaultFolder ? kind : undefined);
|
|
699
|
+
}
|
|
700
|
+
function sortedDiscovered(seen) {
|
|
701
|
+
const out = [];
|
|
702
|
+
for (const [relPath, expectedKind] of seen)
|
|
703
|
+
out.push({ relPath, expectedKind });
|
|
704
|
+
out.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
|
|
705
|
+
return out;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Minimal glob matcher supporting `**` (any depth), `*` (single-segment
|
|
709
|
+
* wildcard), and `{a,b,c}` alternation. Sufficient for the default
|
|
710
|
+
* discovery patterns and typical custom overrides. Negations and
|
|
711
|
+
* extglob patterns are not supported.
|
|
712
|
+
*/
|
|
713
|
+
async function globMatch(root, pattern) {
|
|
714
|
+
const regex = globToRegex(pattern);
|
|
715
|
+
const matches = [];
|
|
716
|
+
// Walk only the pattern's static prefix — a pack embedded in a large
|
|
717
|
+
// app (`kindgi/tools/**`) must not crawl the whole host repo.
|
|
718
|
+
await walk(root, globStaticPrefix(pattern), matches, regex);
|
|
719
|
+
return matches;
|
|
720
|
+
}
|
|
721
|
+
async function walk(root, rel, matches, regex) {
|
|
722
|
+
const abs = rel === '' ? root : path.join(root, rel);
|
|
723
|
+
let entries;
|
|
724
|
+
try {
|
|
725
|
+
entries = await fs.readdir(abs, { withFileTypes: true });
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
for (const entry of entries) {
|
|
731
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
732
|
+
continue;
|
|
733
|
+
const childRel = rel === '' ? entry.name : `${rel}/${entry.name}`;
|
|
734
|
+
if (entry.isDirectory()) {
|
|
735
|
+
await walk(root, childRel, matches, regex);
|
|
736
|
+
}
|
|
737
|
+
else if (entry.isFile()) {
|
|
738
|
+
if (regex.test(childRel))
|
|
739
|
+
matches.push(childRel);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// -----------------------------------------------------------------------
|
|
744
|
+
// Module import + default export extraction
|
|
745
|
+
// -----------------------------------------------------------------------
|
|
746
|
+
/**
|
|
747
|
+
* Cache-busted dynamic import. Node's module cache is keyed by URL, so
|
|
748
|
+
* a plain `import(fileUrl)` inside a long-running process (a dev watch
|
|
749
|
+
* loop) returns the *originally loaded* module even after the author
|
|
750
|
+
* edits the file on disk — the indexer would then re-emit stale
|
|
751
|
+
* manifests every watch tick.
|
|
752
|
+
*
|
|
753
|
+
* Fix: append `?v=<mtimeMs>` to the URL. Node treats each distinct URL
|
|
754
|
+
* as a distinct module entry, so an edited file (with a newer mtime)
|
|
755
|
+
* triggers a fresh load; an unchanged file keeps the same URL and hits
|
|
756
|
+
* the cache — no wasted work, no memory churn.
|
|
757
|
+
*
|
|
758
|
+
* Best-effort: if `fs.stat` fails (e.g. an unusual URL scheme or a
|
|
759
|
+
* race), fall back to the plain `import()` so the indexer still
|
|
760
|
+
* completes — logged behavior remains the same, just without cache
|
|
761
|
+
* invalidation.
|
|
762
|
+
*/
|
|
763
|
+
async function defaultImportModule(fileUrl) {
|
|
764
|
+
try {
|
|
765
|
+
const abs = fileURLToPath(fileUrl);
|
|
766
|
+
const { mtimeMs } = await fs.stat(abs);
|
|
767
|
+
return await import(`${fileUrl}?v=${mtimeMs}`);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return await import(fileUrl);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function extractDefaultExport(module_) {
|
|
774
|
+
if (module_ === null || typeof module_ !== 'object')
|
|
775
|
+
return undefined;
|
|
776
|
+
const record = module_;
|
|
777
|
+
return record.default;
|
|
778
|
+
}
|
|
779
|
+
// -----------------------------------------------------------------------
|
|
780
|
+
// Result-wrapper handling
|
|
781
|
+
// -----------------------------------------------------------------------
|
|
782
|
+
function detectResultError(value) {
|
|
783
|
+
if (value === null || typeof value !== 'object')
|
|
784
|
+
return undefined;
|
|
785
|
+
const record = value;
|
|
786
|
+
if (record.kind === 'err' && 'error' in record)
|
|
787
|
+
return record.error;
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
function unwrapResult(value) {
|
|
791
|
+
if (value === null || typeof value !== 'object')
|
|
792
|
+
return value;
|
|
793
|
+
const record = value;
|
|
794
|
+
if (record.kind === 'ok' && 'value' in record)
|
|
795
|
+
return record.value;
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
function resultErrorMessage(err) {
|
|
799
|
+
if (err === null || typeof err !== 'object')
|
|
800
|
+
return String(err);
|
|
801
|
+
const rec = err;
|
|
802
|
+
if (typeof rec.message === 'string')
|
|
803
|
+
return rec.message;
|
|
804
|
+
return JSON.stringify(err);
|
|
805
|
+
}
|
|
806
|
+
// -----------------------------------------------------------------------
|
|
807
|
+
// Structural kind detection
|
|
808
|
+
// -----------------------------------------------------------------------
|
|
809
|
+
function detectKind(value) {
|
|
810
|
+
if (value === null || typeof value !== 'object')
|
|
811
|
+
return undefined;
|
|
812
|
+
const rec = value;
|
|
813
|
+
if (typeof rec.handler === 'function' && ('input' in rec || 'inputZod' in rec)) {
|
|
814
|
+
return 'tool';
|
|
815
|
+
}
|
|
816
|
+
const isGuardrailAction = typeof rec.action === 'object' &&
|
|
817
|
+
rec.action !== null &&
|
|
818
|
+
'on-violation' in rec.action;
|
|
819
|
+
if (isGuardrailAction && 'kind' in rec)
|
|
820
|
+
return 'guardrail';
|
|
821
|
+
if (typeof rec.instructions === 'string' && Array.isArray(rec.tools))
|
|
822
|
+
return 'agent';
|
|
823
|
+
if (Array.isArray(rec.nodes) && Array.isArray(rec.edges))
|
|
824
|
+
return 'flow';
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
// -----------------------------------------------------------------------
|
|
828
|
+
// Manifest builders
|
|
829
|
+
// -----------------------------------------------------------------------
|
|
830
|
+
function buildTool(raw, relPath, converter) {
|
|
831
|
+
const rec = raw;
|
|
832
|
+
if (typeof rec.id !== 'string') {
|
|
833
|
+
return manifestErr(relPath, `'id' is missing or not a string`);
|
|
834
|
+
}
|
|
835
|
+
const inputWire = resolveSchema(rec, 'input', 'inputZod', relPath, converter);
|
|
836
|
+
if (inputWire.kind === 'err')
|
|
837
|
+
return inputWire;
|
|
838
|
+
const outputWire = resolveSchema(rec, 'output', 'outputZod', relPath, converter);
|
|
839
|
+
if (outputWire.kind === 'err')
|
|
840
|
+
return outputWire;
|
|
841
|
+
const tool = {
|
|
842
|
+
id: rec.id,
|
|
843
|
+
...(typeof rec.description === 'string' && { description: rec.description }),
|
|
844
|
+
...(typeof rec.version === 'string' && { version: rec.version }),
|
|
845
|
+
input: inputWire.value,
|
|
846
|
+
output: outputWire.value,
|
|
847
|
+
...(Array.isArray(rec.effects) && {
|
|
848
|
+
effects: rec.effects,
|
|
849
|
+
}),
|
|
850
|
+
...(Array.isArray(rec.needs) && {
|
|
851
|
+
needs: rec.needs,
|
|
852
|
+
}),
|
|
853
|
+
...(isObject(rec.needsSpec) && {
|
|
854
|
+
needsSpec: rec.needsSpec,
|
|
855
|
+
}),
|
|
856
|
+
...(typeof rec.sandbox === 'string' && { sandbox: rec.sandbox }),
|
|
857
|
+
...(isObject(rec.limits) && {
|
|
858
|
+
limits: rec.limits,
|
|
859
|
+
}),
|
|
860
|
+
...(isObject(rec.network) && {
|
|
861
|
+
network: rec.network,
|
|
862
|
+
}),
|
|
863
|
+
...(isObject(rec.spec) && { spec: rec.spec }),
|
|
864
|
+
...(typeof rec.mutating === 'boolean' && { mutating: rec.mutating }),
|
|
865
|
+
modulePath: normalizeModulePath(relPath),
|
|
866
|
+
};
|
|
867
|
+
return { kind: 'ok', value: tool };
|
|
868
|
+
}
|
|
869
|
+
function buildGuardrail(raw, relPath, converter) {
|
|
870
|
+
const rec = raw;
|
|
871
|
+
if (typeof rec.id !== 'string') {
|
|
872
|
+
return manifestErr(relPath, `'id' is missing or not a string`);
|
|
873
|
+
}
|
|
874
|
+
if (typeof rec.kind !== 'string') {
|
|
875
|
+
return manifestErr(relPath, `'kind' is missing or not a string`);
|
|
876
|
+
}
|
|
877
|
+
if (!isObject(rec.action)) {
|
|
878
|
+
return manifestErr(relPath, `'action' is missing or not an object`);
|
|
879
|
+
}
|
|
880
|
+
// configSchema resolution: look in three places, in priority order:
|
|
881
|
+
// 1. guardrail.configZod (top-level, DefinedCheck-style)
|
|
882
|
+
// 2. guardrail.check.configZod (nested inside the check object)
|
|
883
|
+
// 3. guardrail.configSchema / guardrail.check.configSchema (already JSON Schema)
|
|
884
|
+
const check = isObject(rec.check) ? rec.check : undefined;
|
|
885
|
+
const zodConfig = (isZodSchema(rec.configZod) ? rec.configZod : undefined) ??
|
|
886
|
+
(check && isZodSchema(check.configZod) ? check.configZod : undefined);
|
|
887
|
+
let configSchema;
|
|
888
|
+
if (zodConfig !== undefined) {
|
|
889
|
+
const converted = toJSONSchemaSync(zodConfig, converter, 'input');
|
|
890
|
+
if (converted.kind === 'err') {
|
|
891
|
+
return {
|
|
892
|
+
kind: 'err',
|
|
893
|
+
error: {
|
|
894
|
+
code: 'zod-conversion-failed',
|
|
895
|
+
message: `Guardrail ${rec.id} configSchema Zod conversion failed: ${converted.error.message}`,
|
|
896
|
+
filePath: relPath,
|
|
897
|
+
field: 'configSchema',
|
|
898
|
+
cause: converted.error.cause,
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
configSchema = converted.value;
|
|
903
|
+
}
|
|
904
|
+
else if (isObject(rec.configSchema)) {
|
|
905
|
+
configSchema = rec.configSchema;
|
|
906
|
+
}
|
|
907
|
+
else if (check !== undefined && isObject(check.configSchema)) {
|
|
908
|
+
configSchema = check.configSchema;
|
|
909
|
+
}
|
|
910
|
+
else if (check !== undefined && isObject(check.configJsonSchema)) {
|
|
911
|
+
// `defineCheck` (guardrails pkg) already stores the converted JSON
|
|
912
|
+
// Schema on `configJsonSchema` — accept that shape without a second
|
|
913
|
+
// conversion pass.
|
|
914
|
+
configSchema = check.configJsonSchema;
|
|
915
|
+
}
|
|
916
|
+
const checkId = typeof rec.check === 'string'
|
|
917
|
+
? rec.check
|
|
918
|
+
: check !== undefined && typeof check.id === 'string'
|
|
919
|
+
? check.id
|
|
920
|
+
: undefined;
|
|
921
|
+
const guardrail = {
|
|
922
|
+
id: rec.id,
|
|
923
|
+
...(typeof rec.name === 'string' && { name: rec.name }),
|
|
924
|
+
kind: rec.kind,
|
|
925
|
+
action: rec.action,
|
|
926
|
+
...(typeof rec.severity === 'string' && { severity: rec.severity }),
|
|
927
|
+
...(isObject(rec.scope) && {
|
|
928
|
+
scope: rec.scope,
|
|
929
|
+
}),
|
|
930
|
+
checkModulePath: normalizeModulePath(relPath),
|
|
931
|
+
...(checkId !== undefined && { checkId }),
|
|
932
|
+
...(configSchema !== undefined && { configSchema }),
|
|
933
|
+
...(isObject(rec.config) && { config: rec.config }),
|
|
934
|
+
...(typeof rec.sandbox === 'string' && { sandbox: rec.sandbox }),
|
|
935
|
+
...(isObject(rec.limits) && {
|
|
936
|
+
limits: rec.limits,
|
|
937
|
+
}),
|
|
938
|
+
...(isObject(rec.network) && {
|
|
939
|
+
network: rec.network,
|
|
940
|
+
}),
|
|
941
|
+
};
|
|
942
|
+
return { kind: 'ok', value: guardrail };
|
|
943
|
+
}
|
|
944
|
+
function buildAgent(raw, relPath) {
|
|
945
|
+
const rec = raw;
|
|
946
|
+
if (typeof rec.id !== 'string') {
|
|
947
|
+
return manifestErr(relPath, `'id' is missing or not a string`);
|
|
948
|
+
}
|
|
949
|
+
if (typeof rec.version !== 'string') {
|
|
950
|
+
return manifestErr(relPath, `'version' is missing or not a string`);
|
|
951
|
+
}
|
|
952
|
+
if (typeof rec.name !== 'string') {
|
|
953
|
+
return manifestErr(relPath, `'name' is missing or not a string`);
|
|
954
|
+
}
|
|
955
|
+
if (typeof rec.instructions !== 'string') {
|
|
956
|
+
return manifestErr(relPath, `'instructions' is missing or not a string`);
|
|
957
|
+
}
|
|
958
|
+
if (!Array.isArray(rec.capabilities)) {
|
|
959
|
+
return manifestErr(relPath, `'capabilities' is missing or not an array`);
|
|
960
|
+
}
|
|
961
|
+
if (!Array.isArray(rec.tools)) {
|
|
962
|
+
return manifestErr(relPath, `'tools' is missing or not an array`);
|
|
963
|
+
}
|
|
964
|
+
const agent = {
|
|
965
|
+
id: rec.id,
|
|
966
|
+
version: rec.version,
|
|
967
|
+
name: rec.name,
|
|
968
|
+
instructions: rec.instructions,
|
|
969
|
+
capabilities: rec.capabilities,
|
|
970
|
+
tools: rec.tools,
|
|
971
|
+
...optionalAgentFields(rec),
|
|
972
|
+
modulePath: normalizeModulePath(relPath),
|
|
973
|
+
};
|
|
974
|
+
return { kind: 'ok', value: agent };
|
|
975
|
+
}
|
|
976
|
+
/** The agent's optional fields, carried when present with the right shape. */
|
|
977
|
+
function optionalAgentFields(rec) {
|
|
978
|
+
const out = {};
|
|
979
|
+
for (const key of ['retrieval', 'guardrails', 'parameters', 'tags']) {
|
|
980
|
+
if (Array.isArray(rec[key]))
|
|
981
|
+
out[key] = rec[key];
|
|
982
|
+
}
|
|
983
|
+
for (const key of ['budget', 'conversationPolicy', 'output', 'toolErrors']) {
|
|
984
|
+
if (isObject(rec[key]))
|
|
985
|
+
out[key] = rec[key];
|
|
986
|
+
}
|
|
987
|
+
for (const key of ['preferredProvider', 'preferredModel', 'description']) {
|
|
988
|
+
if (typeof rec[key] === 'string')
|
|
989
|
+
out[key] = rec[key];
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Validate a discovered flow with the same loader the API uses, so a
|
|
995
|
+
* broken flow fails indexing with the loader's message instead of
|
|
996
|
+
* failing later at publish or run time, and carry every field it has.
|
|
997
|
+
*/
|
|
998
|
+
function buildGraph(raw, relPath) {
|
|
999
|
+
const loaded = loadFlow(raw);
|
|
1000
|
+
if (loaded.kind === 'err')
|
|
1001
|
+
return manifestErr(relPath, loaded.error.message);
|
|
1002
|
+
const f = loaded.value;
|
|
1003
|
+
const flow = {
|
|
1004
|
+
id: f.id,
|
|
1005
|
+
version: f.version,
|
|
1006
|
+
...(f.name !== undefined && { name: f.name }),
|
|
1007
|
+
...(f.description !== undefined && { description: f.description }),
|
|
1008
|
+
nodes: f.nodes,
|
|
1009
|
+
edges: f.edges,
|
|
1010
|
+
...(f.maxParallelism !== undefined && { maxParallelism: f.maxParallelism }),
|
|
1011
|
+
...(f.metadata !== undefined && { metadata: f.metadata }),
|
|
1012
|
+
...(f.output !== undefined && { output: f.output }),
|
|
1013
|
+
kernelPayloadVersion: KERNEL_PAYLOAD_VERSION,
|
|
1014
|
+
modulePath: normalizeModulePath(relPath),
|
|
1015
|
+
};
|
|
1016
|
+
return { kind: 'ok', value: flow };
|
|
1017
|
+
}
|
|
1018
|
+
function manifestErr(relPath, msg) {
|
|
1019
|
+
return {
|
|
1020
|
+
kind: 'err',
|
|
1021
|
+
error: {
|
|
1022
|
+
code: 'manifest-validation-failed',
|
|
1023
|
+
message: `${relPath}: ${msg}`,
|
|
1024
|
+
filePath: relPath,
|
|
1025
|
+
},
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
// -----------------------------------------------------------------------
|
|
1029
|
+
// Schema resolution — Zod → JSON Schema pass
|
|
1030
|
+
// -----------------------------------------------------------------------
|
|
1031
|
+
function resolveSchema(rec, wireField, zodField, relPath, converter) {
|
|
1032
|
+
// A tool's `input` is what a caller sends; its `output`, what it returns.
|
|
1033
|
+
const side = wireField;
|
|
1034
|
+
const zodValue = rec[zodField];
|
|
1035
|
+
if (zodValue !== undefined && isZodSchema(zodValue)) {
|
|
1036
|
+
const converted = toJSONSchemaSync(zodValue, converter, side);
|
|
1037
|
+
if (converted.kind === 'err') {
|
|
1038
|
+
return {
|
|
1039
|
+
kind: 'err',
|
|
1040
|
+
error: {
|
|
1041
|
+
code: 'zod-conversion-failed',
|
|
1042
|
+
message: `${relPath}: Zod conversion for '${zodField}' failed: ${converted.error.message}`,
|
|
1043
|
+
filePath: relPath,
|
|
1044
|
+
field: zodField,
|
|
1045
|
+
cause: converted.error.cause,
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
return { kind: 'ok', value: converted.value };
|
|
1050
|
+
}
|
|
1051
|
+
const wireValue = rec[wireField];
|
|
1052
|
+
// `wireValue` may itself be a Zod schema (author-time authored with Zod
|
|
1053
|
+
// but not yet run through defineTool). Convert lazily in that case.
|
|
1054
|
+
if (isZodSchema(wireValue)) {
|
|
1055
|
+
const converted = toJSONSchemaSync(wireValue, converter, side);
|
|
1056
|
+
if (converted.kind === 'err') {
|
|
1057
|
+
return {
|
|
1058
|
+
kind: 'err',
|
|
1059
|
+
error: {
|
|
1060
|
+
code: 'zod-conversion-failed',
|
|
1061
|
+
message: `${relPath}: Zod conversion for '${wireField}' failed: ${converted.error.message}`,
|
|
1062
|
+
filePath: relPath,
|
|
1063
|
+
field: wireField,
|
|
1064
|
+
cause: converted.error.cause,
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
return { kind: 'ok', value: converted.value };
|
|
1069
|
+
}
|
|
1070
|
+
if (!isObject(wireValue)) {
|
|
1071
|
+
return {
|
|
1072
|
+
kind: 'err',
|
|
1073
|
+
error: {
|
|
1074
|
+
code: 'manifest-validation-failed',
|
|
1075
|
+
message: `${relPath}: '${wireField}' is missing or not an object`,
|
|
1076
|
+
filePath: relPath,
|
|
1077
|
+
field: wireField,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return { kind: 'ok', value: wireValue };
|
|
1082
|
+
}
|
|
1083
|
+
// -----------------------------------------------------------------------
|
|
1084
|
+
// Output serialization — atomic write + auto artifact version
|
|
1085
|
+
// -----------------------------------------------------------------------
|
|
1086
|
+
async function atomicWriteJson(outputPath, index) {
|
|
1087
|
+
const tmpPath = `${outputPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1088
|
+
const serialized = `${stableStringify(index)}\n`;
|
|
1089
|
+
try {
|
|
1090
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
1091
|
+
await fs.writeFile(tmpPath, serialized, 'utf8');
|
|
1092
|
+
await fs.rename(tmpPath, outputPath);
|
|
1093
|
+
}
|
|
1094
|
+
catch (cause) {
|
|
1095
|
+
// Best-effort tmp cleanup — swallow errors here (the original write
|
|
1096
|
+
// error is what the caller needs).
|
|
1097
|
+
try {
|
|
1098
|
+
await fs.unlink(tmpPath);
|
|
1099
|
+
}
|
|
1100
|
+
catch {
|
|
1101
|
+
// ignore
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
kind: 'err',
|
|
1105
|
+
error: {
|
|
1106
|
+
code: 'output-write-failed',
|
|
1107
|
+
message: `Failed to write ${outputPath}: ${stringifyError(cause)}`,
|
|
1108
|
+
filePath: outputPath,
|
|
1109
|
+
cause: serializeCause(cause),
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
return { kind: 'ok', value: undefined };
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Stable JSON serializer — keys sorted at every level. Guarantees
|
|
1117
|
+
* byte-identical output for byte-identical semantic input, which is the
|
|
1118
|
+
* property the local integrity gate depends on.
|
|
1119
|
+
*/
|
|
1120
|
+
function stableStringify(value) {
|
|
1121
|
+
return JSON.stringify(canonicalize(value), null, 2);
|
|
1122
|
+
}
|
|
1123
|
+
function canonicalize(value) {
|
|
1124
|
+
if (value === null || typeof value !== 'object')
|
|
1125
|
+
return value;
|
|
1126
|
+
if (Array.isArray(value))
|
|
1127
|
+
return value.map(canonicalize);
|
|
1128
|
+
const rec = value;
|
|
1129
|
+
const out = {};
|
|
1130
|
+
for (const key of Object.keys(rec).sort()) {
|
|
1131
|
+
out[key] = canonicalize(rec[key]);
|
|
1132
|
+
}
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
async function computeAutoArtifactVersion(outputPath) {
|
|
1136
|
+
const today = new Date();
|
|
1137
|
+
const yyyymmdd = today.getUTCFullYear().toString().padStart(4, '0') +
|
|
1138
|
+
(today.getUTCMonth() + 1).toString().padStart(2, '0') +
|
|
1139
|
+
today.getUTCDate().toString().padStart(2, '0');
|
|
1140
|
+
let n = 1;
|
|
1141
|
+
try {
|
|
1142
|
+
const existing = await fs.readFile(outputPath, 'utf8');
|
|
1143
|
+
const parsed = JSON.parse(existing);
|
|
1144
|
+
if (typeof parsed.artifactVersion === 'string') {
|
|
1145
|
+
const match = /^(\d{8})\.(\d+)$/.exec(parsed.artifactVersion);
|
|
1146
|
+
const dayPart = match?.[1];
|
|
1147
|
+
const nPart = match?.[2];
|
|
1148
|
+
if (dayPart === yyyymmdd && nPart !== undefined) {
|
|
1149
|
+
const parsedN = Number.parseInt(nPart, 10);
|
|
1150
|
+
if (Number.isFinite(parsedN))
|
|
1151
|
+
n = parsedN + 1;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
// No prior index.json — start at .1
|
|
1157
|
+
}
|
|
1158
|
+
return `${yyyymmdd}.${n}`;
|
|
1159
|
+
}
|
|
1160
|
+
// -----------------------------------------------------------------------
|
|
1161
|
+
// Utilities
|
|
1162
|
+
// -----------------------------------------------------------------------
|
|
1163
|
+
function isObject(v) {
|
|
1164
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
1165
|
+
}
|
|
1166
|
+
function sortById(items) {
|
|
1167
|
+
return [...items].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1168
|
+
}
|
|
1169
|
+
function stringifyError(err) {
|
|
1170
|
+
if (err instanceof Error)
|
|
1171
|
+
return `${err.name}: ${err.message}`;
|
|
1172
|
+
return String(err);
|
|
1173
|
+
}
|
|
1174
|
+
function serializeCause(err) {
|
|
1175
|
+
if (err instanceof Error) {
|
|
1176
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
1177
|
+
}
|
|
1178
|
+
if (err === undefined)
|
|
1179
|
+
return null;
|
|
1180
|
+
if (err === null ||
|
|
1181
|
+
typeof err === 'string' ||
|
|
1182
|
+
typeof err === 'number' ||
|
|
1183
|
+
typeof err === 'boolean') {
|
|
1184
|
+
return err;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
JSON.stringify(err);
|
|
1188
|
+
return err;
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
return String(err);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function normalizeModulePath(relPath) {
|
|
1195
|
+
// Forward slashes only, no leading './'
|
|
1196
|
+
let normalized = relPath.replace(/\\/g, '/');
|
|
1197
|
+
if (normalized.startsWith('./'))
|
|
1198
|
+
normalized = normalized.slice(2);
|
|
1199
|
+
return normalized;
|
|
1200
|
+
}
|
|
1201
|
+
//# sourceMappingURL=kindgi-index.js.map
|