@codiac.io/codiac-cli 1.3.253 → 1.3.254

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 (31) hide show
  1. package/README.md +502 -163
  2. package/dist/apis/codiac-relay/gen/client.d.ts +12 -1
  3. package/dist/apis/codiac-relay/gen/client.js +23 -0
  4. package/dist/apis/codiac-relay/gen/client.js.map +1 -1
  5. package/dist/apis/codiac-relay/gen/contracts/types.gen.d.ts +120 -0
  6. package/dist/apis/codiac-relay/gen/contracts/types.gen.js.map +1 -1
  7. package/dist/apis/codiac-relay/i-client.d.ts +11 -0
  8. package/dist/apis/codiac-relay-nats/gen/client.d.ts +15 -0
  9. package/dist/apis/codiac-relay-nats/gen/client.js +27 -0
  10. package/dist/apis/codiac-relay-nats/gen/client.js.map +1 -1
  11. package/dist/apis/codiac-relay-nats/gen/contracts/types.gen.d.ts +49 -0
  12. package/dist/commands/ai/model/attach.d.ts +37 -0
  13. package/dist/commands/ai/model/attach.js +173 -0
  14. package/dist/commands/ai/model/attach.js.map +1 -0
  15. package/dist/commands/ai/model/install.d.ts +105 -0
  16. package/dist/commands/ai/model/install.js +655 -0
  17. package/dist/commands/ai/model/install.js.map +1 -0
  18. package/dist/commands/ai/model/list.d.ts +37 -0
  19. package/dist/commands/ai/model/list.js +160 -0
  20. package/dist/commands/ai/model/list.js.map +1 -0
  21. package/dist/commands/ai/model/uninstall.d.ts +40 -0
  22. package/dist/commands/ai/model/uninstall.js +144 -0
  23. package/dist/commands/ai/model/uninstall.js.map +1 -0
  24. package/dist/commands/ai/model/view.d.ts +35 -0
  25. package/dist/commands/ai/model/view.js +143 -0
  26. package/dist/commands/ai/model/view.js.map +1 -0
  27. package/dist/uilogic/model-spec.d.ts +40 -0
  28. package/dist/uilogic/model-spec.js +71 -0
  29. package/dist/uilogic/model-spec.js.map +1 -0
  30. package/oclif.manifest.json +568 -1
  31. package/package.json +1 -1
@@ -0,0 +1,655 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const core_1 = require("@oclif/core");
5
+ const _ = require("lodash");
6
+ const command_base_exec_1 = require("../../../command-base-exec");
7
+ const model_spec_1 = require("../../../uilogic/model-spec");
8
+ const framework_spec_1 = require("../../../uilogic/framework-spec");
9
+ const SOURCES = ['served', 'byo'];
10
+ const RUNTIME_TIERS = ['cpu', 'gpu'];
11
+ const ATTACH_KINDS = ['existing', 'new', 'none'];
12
+ class AiModelInstall extends command_base_exec_1.CommandBaseExec {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.adminOnly = true;
16
+ }
17
+ captureArgs() {
18
+ var _a;
19
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
20
+ const { flags } = yield this.parse(AiModelInstall);
21
+ let partial = {};
22
+ let explicit = {};
23
+ if (flags.enterpriseCode)
24
+ partial.enterprise = flags.enterpriseCode;
25
+ if (flags.source) {
26
+ partial.source = flags.source;
27
+ explicit.source = partial.source;
28
+ }
29
+ if (flags.model) {
30
+ partial.model = flags.model.trim();
31
+ explicit.model = partial.model;
32
+ }
33
+ if (flags.runtime) {
34
+ partial.runtime = flags.runtime;
35
+ explicit.runtime = partial.runtime;
36
+ }
37
+ if (flags['base-url']) {
38
+ partial.baseUrl = flags['base-url'].trim();
39
+ explicit.baseUrl = partial.baseUrl;
40
+ }
41
+ if (flags.key) {
42
+ partial.key = flags.key;
43
+ explicit.key = partial.key;
44
+ }
45
+ if (flags.cabinet) {
46
+ partial.cabinet = flags.cabinet;
47
+ explicit.cabinet = partial.cabinet;
48
+ }
49
+ if (flags.name) {
50
+ partial.name = flags.name.trim();
51
+ explicit.name = partial.name;
52
+ }
53
+ if (flags.attach) {
54
+ partial.attach = flags.attach;
55
+ explicit.attach = partial.attach;
56
+ }
57
+ if (flags.agent) {
58
+ partial.agent = flags.agent.trim();
59
+ explicit.agent = partial.agent;
60
+ }
61
+ if (flags.framework) {
62
+ partial.framework = flags.framework.trim();
63
+ explicit.framework = partial.framework;
64
+ }
65
+ // Thread --input onto the candidate here (fixing the ai-agent-install wart of re-parsing
66
+ // flags inside executeTask). buildCommandString + toProvisionRequest read it from the candidate.
67
+ const inputs = {};
68
+ for (const kv of ((_a = flags.input) !== null && _a !== void 0 ? _a : [])) {
69
+ const eq = kv.indexOf('=');
70
+ if (eq > 0)
71
+ inputs[kv.slice(0, eq)] = kv.slice(eq + 1);
72
+ }
73
+ if (Object.keys(inputs).length > 0)
74
+ partial.inputs = inputs;
75
+ const takeDefaults = flags['take-defaults'];
76
+ return [partial, takeDefaults, explicit];
77
+ });
78
+ }
79
+ buildCandidate_silently(partial, _rootMap, _explicit) {
80
+ var _a;
81
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
82
+ if (partial.source == undefined)
83
+ throw new Error("Must specify a model source (--source served|byo).");
84
+ if (!SOURCES.includes(partial.source))
85
+ throw new Error(`Invalid source [${partial.source}]; must be one of: served, byo.`);
86
+ // NOTE: presence-only validation here. Cross-checking the cabinet against the rootMap
87
+ // (existence, cluster assignment, etc.) is deferred to the relay's provisioning logic.
88
+ if (partial.cabinet == undefined || _.isEmpty(partial.cabinet))
89
+ throw new Error("Must specify cabinet.");
90
+ if (partial.source === 'served') {
91
+ if (partial.model == undefined || _.isEmpty(partial.model))
92
+ throw new Error(`A served model requires --model (one of: ${(0, model_spec_1.supportedModelKinds)().join(', ')}).`);
93
+ if (!(0, model_spec_1.isServedModelKind)(partial.model))
94
+ throw new Error(`Unknown or non-served model [${partial.model}]. Served catalog models: ${(0, model_spec_1.supportedModelKinds)().join(', ')} (use --source byo for external endpoints).`);
95
+ const spec = (0, model_spec_1.resolveModelSpec)(partial.model);
96
+ if (!spec.supported)
97
+ throw new Error(`Model [${partial.model}] is not yet supported.`);
98
+ if (partial.runtime == undefined)
99
+ throw new Error("A served model requires --runtime cpu|gpu.");
100
+ if (!RUNTIME_TIERS.includes(partial.runtime))
101
+ throw new Error(`Invalid runtime [${partial.runtime}]; must be cpu or gpu.`);
102
+ }
103
+ if (partial.source === 'byo') {
104
+ if (partial.baseUrl == undefined || _.isEmpty(partial.baseUrl))
105
+ throw new Error("A BYO model requires --base-url (the OpenAI-compatible endpoint, e.g. https://host/v1).");
106
+ }
107
+ const attach = (_a = partial.attach) !== null && _a !== void 0 ? _a : 'none';
108
+ if (!ATTACH_KINDS.includes(attach))
109
+ throw new Error(`Invalid attach [${attach}]; must be one of: existing, new, none.`);
110
+ if (attach === 'existing' && (partial.agent == undefined || _.isEmpty(partial.agent)))
111
+ throw new Error("--attach existing requires --agent <name> (the agent to wire the model into).");
112
+ if (attach === 'new' && (partial.framework == undefined || _.isEmpty(partial.framework)))
113
+ throw new Error("--attach new requires --framework <framework> (the agent framework to install and wire).");
114
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ enterprise: partial.enterprise, cabinet: partial.cabinet, source: partial.source }, (partial.model ? { model: partial.model } : {})), (partial.runtime ? { runtime: partial.runtime } : {})), (partial.baseUrl ? { baseUrl: partial.baseUrl } : {})), (partial.key ? { key: partial.key } : {})), { attach }), (attach === 'existing' && partial.agent ? { agent: partial.agent } : {})), (attach === 'new' && partial.framework ? { framework: partial.framework } : {})), (partial.name && !_.isEmpty(partial.name) ? { name: partial.name } : {})), (partial.inputs ? { inputs: partial.inputs } : {}));
115
+ });
116
+ }
117
+ buildCandidate_interactively(partial, rootMap, takeDefaults, explicit) {
118
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
119
+ const overrides = [];
120
+ // --- source -----------------------------------------------------------
121
+ partial.source = yield this.setSource(partial.source, overrides);
122
+ // --- served: model + runtime -----------------------------------------
123
+ if (partial.source === 'served') {
124
+ partial.model = yield this.setModel(partial.model, overrides);
125
+ partial.runtime = yield this.setRuntime(partial.model, partial.runtime, takeDefaults, overrides);
126
+ }
127
+ // --- cabinet ----------------------------------------------------------
128
+ // Reuse the rootMapUtils cabinet picker rather than hand-rolling selection/validation
129
+ // (it already implements the User Input Philosophy Rules 1-3).
130
+ const [cabinet] = yield this.rootMapUtils.promptForCabinet(partial.enterprise, rootMap, partial.cabinet, takeDefaults, undefined, explicit === null || explicit === void 0 ? void 0 : explicit.cabinet);
131
+ partial.cabinet = cabinet.name;
132
+ // --- attach -----------------------------------------------------------
133
+ partial.attach = yield this.setAttach(partial.attach, takeDefaults, overrides);
134
+ if (partial.attach === 'existing') {
135
+ partial.agent = yield this.setAgent(partial.agent, explicit === null || explicit === void 0 ? void 0 : explicit.agent);
136
+ }
137
+ else if (partial.attach === 'new') {
138
+ partial.framework = yield this.setFramework(partial.framework, explicit === null || explicit === void 0 ? void 0 : explicit.framework);
139
+ }
140
+ // --- CSP creds for the cabinet's cluster ------------------------------
141
+ // Any path that touches the cluster (a served deploy, or attaching to/redeploying an agent)
142
+ // needs the tenant's cloud credentials — the same gate `cod asset deploy` uses. A BYO
143
+ // registration with no attach never touches the cluster, so it needs no creds.
144
+ const touchesCluster = !(partial.source === 'byo' && partial.attach === 'none');
145
+ if (touchesCluster) {
146
+ const cluster = rootMap.clusters.find(c => c.name === cabinet.cluster);
147
+ if (cluster == undefined) {
148
+ throw new Error(`Cabinet [${cabinet.name}] has no recognized cluster [${cabinet.cluster}].`);
149
+ }
150
+ yield this.cspContextualizer.promptForCloudProviderCreds(this.ioc, cluster.provider, takeDefaults, undefined, undefined, cluster.providerSubscriptionId);
151
+ }
152
+ // --- byo: base-url + key ----------------------------------------------
153
+ if (partial.source === 'byo') {
154
+ partial.baseUrl = yield this.setBaseUrl(partial.baseUrl, explicit === null || explicit === void 0 ? void 0 : explicit.baseUrl);
155
+ partial.key = yield this.setKey(partial.key);
156
+ }
157
+ // --- name -------------------------------------------------------------
158
+ partial.name = yield this.setName(partial.name, takeDefaults, explicit === null || explicit === void 0 ? void 0 : explicit.name);
159
+ if (overrides.length > 0) {
160
+ this._ui.userMessage(this._styler.colorWarning("One or more provided values were invalid and have been replaced:"));
161
+ for (const o of overrides) {
162
+ this._ui.userMessage(` ${o.param}: ${this._styler.colorWarning(o.given)} → ${this._styler.hilite(o.using)}`);
163
+ }
164
+ const confirmed = yield this._ui.promptConfirm("confirmOverrides", "Proceed with the above substitutions?", true);
165
+ if (!confirmed)
166
+ return undefined;
167
+ }
168
+ return yield this.buildCandidate_silently(partial, rootMap, explicit);
169
+ });
170
+ }
171
+ setSource(userInput, overrides) {
172
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
173
+ let invalidInput = false;
174
+ if (userInput != undefined && !SOURCES.includes(userInput)) {
175
+ invalidInput = true;
176
+ overrides.push({ param: 'source', given: String(userInput), using: '(to be selected)' });
177
+ userInput = undefined;
178
+ }
179
+ if (userInput != undefined) {
180
+ this._ui.userMessage(`Model source: ${this._styler.hilite(userInput)}`);
181
+ return userInput;
182
+ }
183
+ if (invalidInput) {
184
+ this._ui.userMessage(this._styler.colorWarning("Provided source was not valid; please select from the list below."));
185
+ }
186
+ const selected = yield this._ui.promptSelect('source', 'Serve a catalog model in-cluster, or register a BYO endpoint?', [
187
+ { name: 'served — in-cluster (ClusterIP) catalog model', value: 'served' },
188
+ { name: 'byo — external OpenAI-compatible endpoint', value: 'byo' },
189
+ ], false, false);
190
+ const resolved = selected !== null && selected !== void 0 ? selected : SOURCES[0];
191
+ if (invalidInput)
192
+ overrides[overrides.length - 1].using = resolved;
193
+ return resolved;
194
+ });
195
+ }
196
+ setModel(userInput, overrides) {
197
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
198
+ let invalidInput = false;
199
+ if (userInput != undefined && !(0, model_spec_1.isServedModelKind)(userInput)) {
200
+ invalidInput = true;
201
+ overrides.push({ param: 'model', given: String(userInput), using: '(to be selected)' });
202
+ userInput = undefined;
203
+ }
204
+ if (userInput != undefined) {
205
+ const spec = (0, model_spec_1.resolveModelSpec)(userInput);
206
+ this._ui.userMessage(`Model: ${this._styler.hilite(userInput)} (${spec.defaultTag}, ${spec.license})`);
207
+ return userInput;
208
+ }
209
+ if (invalidInput) {
210
+ this._ui.userMessage(this._styler.colorWarning("Provided model is not in the served catalog; please select from the list below."));
211
+ }
212
+ const choices = (0, model_spec_1.supportedModelKinds)().map(k => {
213
+ const s = (0, model_spec_1.resolveModelSpec)(k);
214
+ return { name: `${k} — ${s.defaultTag} (${s.license}, ${s.runtimeTier})`, value: k };
215
+ });
216
+ const selected = yield this._ui.promptSelect('model', 'Select a catalog model to serve:', choices, false, false);
217
+ const resolved = selected !== null && selected !== void 0 ? selected : (0, model_spec_1.supportedModelKinds)()[0];
218
+ if (invalidInput)
219
+ overrides[overrides.length - 1].using = resolved;
220
+ return resolved;
221
+ });
222
+ }
223
+ setRuntime(modelKind, userInput, takeDefaults, overrides) {
224
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
225
+ const spec = (0, model_spec_1.resolveModelSpec)(modelKind);
226
+ let invalidInput = false;
227
+ if (userInput != undefined && !RUNTIME_TIERS.includes(userInput)) {
228
+ invalidInput = true;
229
+ overrides.push({ param: 'runtime', given: String(userInput), using: '(to be selected)' });
230
+ userInput = undefined;
231
+ }
232
+ if (userInput != undefined) {
233
+ this._ui.userMessage(`Runtime tier: ${this._styler.hilite(userInput)}`);
234
+ return userInput;
235
+ }
236
+ // One-and-only-ish default: the catalog's natural tier for this model.
237
+ const defaultTier = spec.runtimeTier;
238
+ if (!invalidInput) {
239
+ if (takeDefaults) {
240
+ this._ui.userMessage(`Runtime tier: ${this._styler.hilite(defaultTier)}`);
241
+ return defaultTier;
242
+ }
243
+ const hint = spec.minGpu ? ` (${modelKind} is impractical CPU-only; GPU recommended)` : '';
244
+ if (yield this._ui.promptConfirm('runtime', `Runtime tier: ${this._styler.hilite(defaultTier)}${hint} `, true)) {
245
+ return defaultTier;
246
+ }
247
+ }
248
+ else {
249
+ this._ui.userMessage(this._styler.colorWarning("Provided runtime was not valid; please select from the list below."));
250
+ }
251
+ const selected = yield this._ui.promptSelect('runtimeSelect', 'Select a runtime tier:', RUNTIME_TIERS, false, false);
252
+ const resolved = selected !== null && selected !== void 0 ? selected : defaultTier;
253
+ if (invalidInput)
254
+ overrides[overrides.length - 1].using = resolved;
255
+ return resolved;
256
+ });
257
+ }
258
+ setAttach(userInput, takeDefaults, overrides) {
259
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
260
+ let invalidInput = false;
261
+ if (userInput != undefined && !ATTACH_KINDS.includes(userInput)) {
262
+ invalidInput = true;
263
+ overrides.push({ param: 'attach', given: String(userInput), using: '(to be selected)' });
264
+ userInput = undefined;
265
+ }
266
+ if (userInput != undefined) {
267
+ this._ui.userMessage(`Attach to agent: ${this._styler.hilite(userInput)}`);
268
+ return userInput;
269
+ }
270
+ const defaultKind = 'none';
271
+ if (takeDefaults) {
272
+ this._ui.userMessage(`Attach to agent: ${this._styler.hilite(defaultKind)}`);
273
+ return defaultKind;
274
+ }
275
+ if (invalidInput) {
276
+ this._ui.userMessage(this._styler.colorWarning("Provided attach mode was not valid; please select from the list below."));
277
+ }
278
+ const selected = yield this._ui.promptSelect('attach', 'Wire this model to an agent?', [
279
+ { name: 'none — provision the model only', value: 'none' },
280
+ { name: 'existing — attach to an already-installed agent', value: 'existing' },
281
+ { name: 'new — install a fresh agent wired to this model', value: 'new' },
282
+ ], false, false);
283
+ const resolved = selected !== null && selected !== void 0 ? selected : defaultKind;
284
+ if (invalidInput)
285
+ overrides[overrides.length - 1].using = resolved;
286
+ return resolved;
287
+ });
288
+ }
289
+ setAgent(userInput, userSuppliedValue) {
290
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
291
+ if (userInput && !_.isEmpty(userInput) && userSuppliedValue === userInput) {
292
+ this._ui.userMessage(`Agent to attach: ${this._styler.hilite(userInput)}`);
293
+ return userInput;
294
+ }
295
+ const typed = yield this._ui.prompt('agent', 'Name of the agent to wire the model into:', true, userInput);
296
+ return (typed !== null && typed !== void 0 ? typed : '').trim();
297
+ });
298
+ }
299
+ setFramework(userInput, userSuppliedValue) {
300
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
301
+ if (userInput && !_.isEmpty(userInput) && userSuppliedValue === userInput) {
302
+ this._ui.userMessage(`Agent framework to install: ${this._styler.hilite(userInput)}`);
303
+ return userInput;
304
+ }
305
+ const typed = yield this._ui.prompt('framework', 'Agent framework to install and wire (e.g. hermes):', true, userInput);
306
+ return (typed !== null && typed !== void 0 ? typed : '').trim();
307
+ });
308
+ }
309
+ setBaseUrl(userInput, userSuppliedValue) {
310
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
311
+ if (userInput && !_.isEmpty(userInput) && userSuppliedValue === userInput) {
312
+ this._ui.userMessage(`Endpoint base URL: ${this._styler.hilite(userInput)}`);
313
+ return userInput;
314
+ }
315
+ const typed = yield this._ui.prompt('baseUrl', 'OpenAI-compatible base URL (e.g. https://host/v1):', true, userInput);
316
+ return (typed !== null && typed !== void 0 ? typed : '').trim();
317
+ });
318
+ }
319
+ setKey(userInput) {
320
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
321
+ // Already provided on the CLI (Rule 2 — do not re-prompt for an explicitly supplied value).
322
+ if (userInput && !_.isEmpty(userInput))
323
+ return userInput;
324
+ const key = yield this._ui.promptPassword('key', 'API key for the endpoint (leave blank if none):');
325
+ return (key && !_.isEmpty(key)) ? key : undefined;
326
+ });
327
+ }
328
+ setName(userInput, takeDefaults, userSuppliedValue) {
329
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
330
+ if (userInput && !_.isEmpty(userInput) && userSuppliedValue === userInput) {
331
+ this._ui.userMessage(`Model name: ${this._styler.hilite(userInput)}`);
332
+ return userInput;
333
+ }
334
+ // No explicit name: the server assigns a default handle. Offer to set a custom one, unless
335
+ // --take-defaults, in which case accept the server default silently.
336
+ if (takeDefaults)
337
+ return userInput && !_.isEmpty(userInput) ? userInput : undefined;
338
+ const setCustom = yield this._ui.promptConfirm('setName', 'Assign a custom model name? (default: server-assigned)', false);
339
+ if (!setCustom)
340
+ return undefined;
341
+ const typed = yield this._ui.prompt('name', 'Enter model name:', true, userInput);
342
+ return (typed && !_.isEmpty(typed)) ? typed.trim() : undefined;
343
+ });
344
+ }
345
+ buildCommandString(candidate, cmd) {
346
+ var _a;
347
+ cmd.push("-e", candidate.enterprise);
348
+ cmd.push("--source", candidate.source);
349
+ if (candidate.model)
350
+ cmd.push("-m", candidate.model);
351
+ if (candidate.runtime)
352
+ cmd.push("--runtime", candidate.runtime);
353
+ if (candidate.baseUrl)
354
+ cmd.push("--base-url", candidate.baseUrl);
355
+ if (candidate.key)
356
+ cmd.push("--key", candidate.key);
357
+ cmd.push("-c", candidate.cabinet);
358
+ if (candidate.name)
359
+ cmd.push("-n", candidate.name);
360
+ cmd.push("--attach", candidate.attach);
361
+ if (candidate.agent)
362
+ cmd.push("--agent", candidate.agent);
363
+ if (candidate.framework)
364
+ cmd.push("--framework", candidate.framework);
365
+ for (const [k, v] of Object.entries((_a = candidate.inputs) !== null && _a !== void 0 ? _a : {}))
366
+ cmd.push("--input", `${k}=${v}`);
367
+ cmd.push("--silent");
368
+ return cmd;
369
+ }
370
+ // NOTE: thin client over `post.ai-model-provision` — all provisioning logic (chart repo, GPU
371
+ // preflight, asset deploy, secret + scoped-settings, attach) lives server-side in the relay.
372
+ // This method only builds the request, calls the endpoint, and drives the needs_action loop.
373
+ executeTask(candidate) {
374
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
375
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
376
+ // Seed inputs from the candidate's threaded --input values (captured in captureArgs — this
377
+ // command never re-parses flags downstream). Remediation answers accumulate here across re-calls.
378
+ const inputs = Object.assign({}, ((_a = candidate.inputs) !== null && _a !== void 0 ? _a : {}));
379
+ const attemptedCodes = new Set();
380
+ while (true) {
381
+ const request = this.toProvisionRequest(candidate, inputs);
382
+ // ASYNC (ack-then-stream): the endpoint acks immediately; the rich result (ok / needs_action
383
+ // / failed) arrives as the terminal status event's resultPayload, surfaced here as this
384
+ // SingleResult. A long synchronous reply would hang across the tenant→hub NATS service-import.
385
+ const result = yield this.watcher.triggerAndWatch((correlationId, socketId) => this.genRelayClient.Call.aiModelProvision(Object.assign(Object.assign({}, request), { correlationId, socketId })));
386
+ if (result.error || !result.output) {
387
+ const label = candidate.source === 'byo' ? ((_b = candidate.baseUrl) !== null && _b !== void 0 ? _b : 'byo endpoint') : ((_c = candidate.model) !== null && _c !== void 0 ? _c : 'model');
388
+ const msg = `Error provisioning model [${label}]: ${(_d = result.message) !== null && _d !== void 0 ? _d : ''} ${(_f = (_e = result.error) === null || _e === void 0 ? void 0 : _e.message) !== null && _f !== void 0 ? _f : ''}`.trimEnd();
389
+ this.logger.error(msg, result.error);
390
+ this._ui.userMessage(`${this._styler.colorCritical('ERROR!')} ${msg}`);
391
+ throw (_g = result.error) !== null && _g !== void 0 ? _g : new Error(msg);
392
+ }
393
+ const out = result.output;
394
+ this.printStepResults(out.steps);
395
+ if (out.status === 'ok') {
396
+ this.printModelInfo(candidate, out.model);
397
+ // attach === 'new' is composed here, CLI-side (no new relay code): once the model is
398
+ // ready, install a fresh agent already wired to it by threading the model's endpoint
399
+ // through the existing agent-provision `overrides` (the modelBaseUrl/modelApiKey chart
400
+ // values Task 2 added to the hermes chart). 'existing'/'none' were handled server-side
401
+ // by the relay from the `attach` passed on the provision request above.
402
+ if (candidate.attach === 'new' && out.model) {
403
+ yield this.wireNewAgent(candidate, out.model);
404
+ }
405
+ return;
406
+ }
407
+ if (out.status === 'failed') {
408
+ const msg = `Failed provisioning model: ${(_j = (_h = out.error) === null || _h === void 0 ? void 0 : _h.message) !== null && _j !== void 0 ? _j : ''}`.trimEnd();
409
+ this.logger.notice(msg, out);
410
+ this._ui.userMessage(`${this._styler.colorCritical('FAILED!')} ${msg}`);
411
+ throw new Error(msg);
412
+ }
413
+ // status === 'needs_action'
414
+ const rem = out.nextAction;
415
+ const needs = (_l = (_k = rem.resolvedBy) === null || _k === void 0 ? void 0 : _k.needs) !== null && _l !== void 0 ? _l : [];
416
+ if (this.isSilentMode()) {
417
+ const missing = needs.map(f => f.name).join(', ');
418
+ const hint = ((_m = rem.resolvedBy) === null || _m === void 0 ? void 0 : _m.cliCommand) ? ` Resolve with: ${rem.resolvedBy.cliCommand}` : '';
419
+ throw new Error(`Cannot proceed in --silent mode: ${rem.reason}${missing ? ` (missing: ${missing})` : ''}.${hint}`);
420
+ }
421
+ this.renderRemediationGuidance(rem, candidate);
422
+ if (needs.length > 0) {
423
+ for (const f of needs) {
424
+ const value = f.secret
425
+ ? yield this._ui.promptPassword(f.name, `${rem.reason}\n${f.label}:`)
426
+ : yield this._ui.prompt(f.name, `${rem.reason}\n${f.label}:`, true);
427
+ // Only carry a non-empty answer forward — an empty string is "not provided", never a
428
+ // real value. Seeding inputs[key]="" would re-block on recheck.
429
+ if (value != undefined && value.trim() !== "")
430
+ inputs[f.name] = value;
431
+ }
432
+ }
433
+ else {
434
+ this._ui.userMessage(this._styler.colorWarning(`Action needed (${rem.code}): ${rem.reason}`));
435
+ if ((_o = rem.resolvedBy) === null || _o === void 0 ? void 0 : _o.cliCommand)
436
+ this._ui.userMessage(`Resolve with: ${this._styler.hilite(rem.resolvedBy.cliCommand)}`);
437
+ const proceed = yield this._ui.promptConfirm('recheck', "Re-check after you've resolved this?", true);
438
+ if (!proceed)
439
+ return;
440
+ }
441
+ // Loop guard: same remediation code twice with no new inputs → abort (never spin forever).
442
+ if (attemptedCodes.has(rem.code) && needs.length === 0) {
443
+ const msg = `Still blocked on [${rem.code}]: ${rem.reason}`;
444
+ this._ui.userMessage(this._styler.colorCritical(msg));
445
+ throw new Error(msg);
446
+ }
447
+ attemptedCodes.add(rem.code);
448
+ }
449
+ });
450
+ }
451
+ /** Maps the resolved candidate (plus any remediation inputs gathered so far) to the
452
+ * `post.ai-model-provision` request shape. */
453
+ toProvisionRequest(candidate, inputs) {
454
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ enterprise: candidate.enterprise, cabinet: candidate.cabinet, mode: candidate.source }, (candidate.model ? { model: candidate.model } : {})), (candidate.runtime ? { runtime: candidate.runtime } : {})), (candidate.baseUrl ? { baseUrl: candidate.baseUrl } : {})), (candidate.key ? { key: candidate.key } : {})), {
455
+ // Pass attach straight through — the relay handles kind 'existing'/'none'; 'new' is
456
+ // CLI-composed in Task 8 (see the TODO in executeTask).
457
+ attach: Object.assign(Object.assign({ kind: candidate.attach }, (candidate.agent ? { agentName: candidate.agent } : {})), (candidate.framework ? { framework: candidate.framework } : {})) }), (candidate.name ? { name: candidate.name } : {})), { inputs });
458
+ }
459
+ /** attach === 'new': installs a fresh agent already wired to the just-provisioned model, entirely
460
+ * CLI-side (no new relay endpoint) — it reuses the existing `aiAgentProvision` op, threading the
461
+ * model's endpoint through `overrides` as the `modelBaseUrl`/`modelApiKey` chart values the
462
+ * hermes chart consumes. Runs the same ack-then-stream needs_action loop shape as `cod ai agent
463
+ * install`. Never exposes ingress (`hasIngress:false`) so the wiring cannot demand a host-naming
464
+ * strategy — the wiring is env-only and must succeed headlessly; the agent still reaches the
465
+ * model in-cluster. */
466
+ wireNewAgent(candidate, model) {
467
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
468
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
469
+ const framework = candidate.framework;
470
+ const name = this.deriveAgentName(framework);
471
+ // Served models front an unauthenticated in-cluster Ollama, so a placeholder key is used;
472
+ // BYO models pass through the real --key the caller supplied (or the same placeholder if none).
473
+ const modelApiKey = candidate.source === 'byo' ? ((_a = candidate.key) !== null && _a !== void 0 ? _a : 'ollama') : 'ollama';
474
+ this._ui.userMessage(`Installing a new ${this._styler.hilite(framework)} agent [${this._styler.hilite(name)}] wired to model ${this._styler.hilite(model.handle)}...`);
475
+ const inputs = {};
476
+ const attemptedCodes = new Set();
477
+ while (true) {
478
+ const request = {
479
+ enterprise: candidate.enterprise,
480
+ cabinet: candidate.cabinet,
481
+ framework: framework,
482
+ name,
483
+ hasIngress: false,
484
+ overrides: { modelBaseUrl: model.endpoint, modelApiKey },
485
+ inputs,
486
+ };
487
+ // Same ASYNC ack-then-stream contract as the model provision above and `cod ai agent install`.
488
+ const result = yield this.watcher.triggerAndWatch((correlationId, socketId) => this.genRelayClient.Call.aiAgentProvision(Object.assign(Object.assign({}, request), { correlationId, socketId })));
489
+ if (result.error || !result.output) {
490
+ const msg = `Error installing the wired agent [${name}]: ${(_b = result.message) !== null && _b !== void 0 ? _b : ''} ${(_d = (_c = result.error) === null || _c === void 0 ? void 0 : _c.message) !== null && _d !== void 0 ? _d : ''}`.trimEnd();
491
+ this.logger.error(msg, result.error);
492
+ this._ui.userMessage(`${this._styler.colorCritical('ERROR!')} ${msg}`);
493
+ throw (_e = result.error) !== null && _e !== void 0 ? _e : new Error(msg);
494
+ }
495
+ const out = result.output;
496
+ this.printStepResults(out.steps);
497
+ if (out.status === 'ok') {
498
+ this.printAgentInfo(name, out.agent);
499
+ return;
500
+ }
501
+ if (out.status === 'failed') {
502
+ const msg = `Failed installing the wired agent [${name}]: ${(_g = (_f = out.error) === null || _f === void 0 ? void 0 : _f.message) !== null && _g !== void 0 ? _g : ''}`.trimEnd();
503
+ this.logger.notice(msg, out);
504
+ this._ui.userMessage(`${this._styler.colorCritical('FAILED!')} ${msg}`);
505
+ throw new Error(msg);
506
+ }
507
+ // status === 'needs_action'
508
+ const rem = out.nextAction;
509
+ const needs = (_j = (_h = rem.resolvedBy) === null || _h === void 0 ? void 0 : _h.needs) !== null && _j !== void 0 ? _j : [];
510
+ if (this.isSilentMode()) {
511
+ const missing = needs.map(f => f.name).join(', ');
512
+ const hint = ((_k = rem.resolvedBy) === null || _k === void 0 ? void 0 : _k.cliCommand) ? ` Resolve with: ${rem.resolvedBy.cliCommand}` : '';
513
+ throw new Error(`Cannot install the wired agent in --silent mode: ${rem.reason}${missing ? ` (missing: ${missing})` : ''}.${hint}`);
514
+ }
515
+ if (needs.length > 0) {
516
+ for (const f of needs) {
517
+ const value = f.secret
518
+ ? yield this._ui.promptPassword(f.name, `${rem.reason}\n${f.label}:`)
519
+ : yield this._ui.prompt(f.name, `${rem.reason}\n${f.label}:`, true);
520
+ if (value != undefined && value.trim() !== "")
521
+ inputs[f.name] = value;
522
+ }
523
+ }
524
+ else {
525
+ this._ui.userMessage(this._styler.colorWarning(`Action needed (${rem.code}): ${rem.reason}`));
526
+ if ((_l = rem.resolvedBy) === null || _l === void 0 ? void 0 : _l.cliCommand)
527
+ this._ui.userMessage(`Resolve with: ${this._styler.hilite(rem.resolvedBy.cliCommand)}`);
528
+ const proceed = yield this._ui.promptConfirm('recheckAgent', "Re-check after you've resolved this?", true);
529
+ if (!proceed)
530
+ return;
531
+ }
532
+ if (attemptedCodes.has(rem.code) && needs.length === 0) {
533
+ const msg = `Still blocked installing the wired agent on [${rem.code}]: ${rem.reason}`;
534
+ this._ui.userMessage(this._styler.colorCritical(msg));
535
+ throw new Error(msg);
536
+ }
537
+ attemptedCodes.add(rem.code);
538
+ }
539
+ });
540
+ }
541
+ /** Derives the fresh agent's name from its framework — the framework's catalog default name
542
+ * (e.g. hermes → "hermes-agent") when known, else "<framework>-agent". */
543
+ deriveAgentName(framework) {
544
+ try {
545
+ return (0, framework_spec_1.resolveFrameworkSpec)(framework).defaultName;
546
+ }
547
+ catch (_a) {
548
+ return `${framework}-agent`;
549
+ }
550
+ }
551
+ /** Best-effort note about the freshly wired agent — prefers the server-resolved URL, else states
552
+ * it is reachable in-cluster (attach=new never requests ingress). */
553
+ printAgentInfo(name, agent) {
554
+ if (agent === null || agent === void 0 ? void 0 : agent.url) {
555
+ this._ui.userMessage(`${this._styler.colorGood('READY')} Wired agent ${this._styler.hilite(name)}: ${this._styler.hilite(agent.url)}`);
556
+ }
557
+ else {
558
+ this._ui.userMessage(`${this._styler.colorGood('READY')} Wired agent ${this._styler.hilite(name)} installed (reachable in-cluster; no ingress).`);
559
+ }
560
+ }
561
+ /** Adds code-specific guidance ahead of the generic needs/consent handling for the model-side
562
+ * remediation codes. */
563
+ renderRemediationGuidance(rem, candidate) {
564
+ var _a, _b, _c, _d, _e, _f;
565
+ switch (rem.code) {
566
+ case 'NO_GPU_NODEPOOL': {
567
+ const cluster = (_f = (_c = (_b = (_a = rem.resolvedBy) === null || _a === void 0 ? void 0 : _a.params) === null || _b === void 0 ? void 0 : _b.cluster) !== null && _c !== void 0 ? _c : (_e = (_d = rem.resolvedBy) === null || _d === void 0 ? void 0 : _d.params) === null || _e === void 0 ? void 0 : _e.clusterName) !== null && _f !== void 0 ? _f : `cabinet ${candidate.cabinet}`;
568
+ this._ui.userMessage(this._styler.colorWarning(`No GPU node pool found. Add a GPU node pool to ${cluster}, then re-check — or re-run with --runtime cpu if this model is CPU-viable.`));
569
+ break;
570
+ }
571
+ case 'MODEL_NOT_READY':
572
+ this._ui.userMessage(this._styler.colorWarning(`The model endpoint is not ready yet: ${rem.reason}. Verify the endpoint is reachable, or pass --input skipReachabilityCheck=true to skip the probe.`));
573
+ break;
574
+ case 'NEEDS_AGENT_NAME':
575
+ this._ui.userMessage(this._styler.colorWarning(`An agent name is required to attach this model: ${rem.reason}. Provide one with --agent <name>.`));
576
+ break;
577
+ default:
578
+ break;
579
+ }
580
+ }
581
+ /** One line per step: `<step>: <status>` (+ detail when present). */
582
+ printStepResults(steps) {
583
+ for (const s of steps) {
584
+ const line = s.detail ? `${s.step}: ${s.status} — ${s.detail}` : `${s.step}: ${s.status}`;
585
+ if (s.status === 'failed')
586
+ this._ui.userMessage(this._styler.colorCritical(line));
587
+ else if (s.status === 'blocked')
588
+ this._ui.userMessage(this._styler.colorWarning(line));
589
+ else
590
+ this._ui.userMessage(this._styler.colorGood(line));
591
+ }
592
+ }
593
+ /** Prints the ready model's handle + in-cluster (or BYO) endpoint. */
594
+ printModelInfo(candidate, model) {
595
+ if (model) {
596
+ this._ui.userMessage(`${this._styler.colorGood('READY')} Model ${this._styler.hilite(model.handle)} (${model.mode}, ${model.exposure}) in cabinet [${model.cabinet}].`);
597
+ this._ui.userMessage(` Endpoint: ${this._styler.hilite(model.endpoint)}`);
598
+ }
599
+ else {
600
+ this._ui.userMessage(`${this._styler.colorGood('READY')} Model provisioned in cabinet [${this._styler.hilite(candidate.cabinet)}].`);
601
+ }
602
+ }
603
+ /** Silent mode disables all prompting (the base lifecycle rebinds `_ui` to a throwing
604
+ * provider) — mirrors the same detection `CommandBaseExec.runAsync()` uses, so a
605
+ * `needs_action` response can fail fast with a clear message. */
606
+ isSilentMode() {
607
+ return this.argv.includes('--silent') || this.pipedInput != undefined;
608
+ }
609
+ }
610
+ exports.default = AiModelInstall;
611
+ AiModelInstall.description = 'Provisions a private LLM into an enterprise cabinet — either an in-cluster (ClusterIP-only) catalog model served via Ollama, or a registered bring-your-own OpenAI-compatible endpoint.';
612
+ AiModelInstall.examples = [
613
+ {
614
+ description: 'Serve the Qwen3 catalog model (CPU) into the dev cabinet of the ben enterprise.',
615
+ command: '<%= config.bin %> <%= command.id %> -e ben --source served -m qwen3 --runtime cpu -c dev',
616
+ },
617
+ {
618
+ description: 'Non-interactive (scripted) served install; a served model needs both --model and --runtime.',
619
+ command: '<%= config.bin %> <%= command.id %> -e ben --source served -m qwen3 --runtime cpu -c dev --attach none --silent',
620
+ },
621
+ {
622
+ description: 'Register a BYO OpenAI-compatible endpoint and attach it to an existing agent.',
623
+ command: '<%= config.bin %> <%= command.id %> -e ben --source byo --base-url https://host/v1 --key sk-... -c dev --attach existing --agent hermes-agent --silent',
624
+ },
625
+ {
626
+ description: 'Renders the equivalent silent command and then executes.',
627
+ command: '<%= config.bin %> <%= command.id %> -e ben --source served -m qwen3 --runtime cpu -c dev --echo',
628
+ },
629
+ {
630
+ description: 'Renders the equivalent silent command without executing.',
631
+ command: '<%= config.bin %> <%= command.id %> -e ben --source served -m qwen3 --runtime cpu -c dev --to-script',
632
+ },
633
+ ];
634
+ AiModelInstall.flags = {
635
+ help: core_1.Flags.help({ char: 'h' }),
636
+ enterpriseCode: core_1.Flags.string({ char: 'e', required: false, description: 'Code name identifying the target enterprise.' }),
637
+ source: core_1.Flags.string({ options: ['served', 'byo'], description: 'Serve a license-clean catalog model in-cluster (served) or register an external OpenAI-compatible endpoint (byo).' }),
638
+ model: core_1.Flags.string({ char: 'm', description: `Catalog model kind to serve when --source served (one of: ${(0, model_spec_1.supportedModelKinds)().join(', ')}).` }),
639
+ runtime: core_1.Flags.string({ options: ['cpu', 'gpu'], description: 'Compute tier for a served model: cpu-only, or scheduled onto a GPU node pool.' }),
640
+ 'base-url': core_1.Flags.string({ description: 'OpenAI-compatible base URL of the endpoint to register when --source byo (e.g. https://host/v1).' }),
641
+ key: core_1.Flags.string({ description: 'API key for the BYO endpoint (prompted as a secret during install when omitted; stored server-side as a secret).' }),
642
+ cabinet: core_1.Flags.string({ char: 'c', description: 'Cabinet (environment) the model is deployed into (served) or registered against (byo).' }),
643
+ name: core_1.Flags.string({ char: 'n', description: 'Handle for the model workload (server-assigns a default when omitted).' }),
644
+ attach: core_1.Flags.string({ options: ['existing', 'new', 'none'], description: 'Wire the model to an agent once ready: attach to an existing agent, install a new one, or neither (defaults to none).' }),
645
+ agent: core_1.Flags.string({ description: 'Name of the already-installed agent to wire the model into (required with --attach existing).' }),
646
+ framework: core_1.Flags.string({ description: 'Agent framework to install and wire to the model (required with --attach new).' }),
647
+ input: core_1.Flags.string({ multiple: true, description: 'Pre-supply a needs_action input as key=value (repeatable), e.g. --input skipReachabilityCheck=true . Lets --silent/headless runs resolve secret/decision prompts without interaction.' }),
648
+ // Interaction mode flags (TODO: will eventually move to base class)
649
+ "take-defaults": core_1.Flags.boolean({ exclusive: ["silent"], description: "Prevents prompting for confirmation on parameters that were passed in or were automatically set to default values (Irrelevant in --silent mode). This behavior does NOT apply to scenarios where the command needs to override invalid user input; such cases will always prompt for confirmation." }),
650
+ silent: core_1.Flags.boolean({ exclusive: ["echo", "to-script"], description: 'Executes without any user interaction; fails on missing or invalid arguments.' }),
651
+ echo: core_1.Flags.boolean({ exclusive: ["to-script", "silent"], description: 'Renders the equivalent non-interactive command for future use before executing.' }),
652
+ "to-script": core_1.Flags.boolean({ exclusive: ["echo", "silent"], description: 'Renders the equivalent non-interactive command without executing it.' }),
653
+ };
654
+ AiModelInstall.args = {};
655
+ //# sourceMappingURL=install.js.map