callkaro-cli-unofficial 0.4.1 → 0.5.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.
@@ -0,0 +1,378 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collect = void 0;
4
+ exports.contactsListCommand = contactsListCommand;
5
+ exports.contactsGetCommand = contactsGetCommand;
6
+ exports.contactsCreateCommand = contactsCreateCommand;
7
+ exports.contactsUpdateCommand = contactsUpdateCommand;
8
+ exports.contactsDeleteCommand = contactsDeleteCommand;
9
+ exports.contactsUploadCommand = contactsUploadCommand;
10
+ exports.contactsExportCommand = contactsExportCommand;
11
+ exports.contactsAssignCommand = contactsAssignCommand;
12
+ exports.attributesListCommand = attributesListCommand;
13
+ exports.attributesSetCommand = attributesSetCommand;
14
+ exports.attributesDeleteCommand = attributesDeleteCommand;
15
+ const fs_1 = require("fs");
16
+ const session_1 = require("../lib/session");
17
+ const ui_1 = require("../lib/ui");
18
+ const collect = (val, acc) => {
19
+ acc.push(val);
20
+ return acc;
21
+ };
22
+ exports.collect = collect;
23
+ const ATTRIBUTE_TYPES = [
24
+ 'string', 'description', 'enum', 'number', 'date', 'boolean', 'datetime', 'time', 'array', 'object',
25
+ ];
26
+ /** ["city=Pune", "age=30"] -> { city: "Pune", age: "30" }. The server coerces by attribute type. */
27
+ function parsePairs(pairs, flag) {
28
+ const out = {};
29
+ for (const pair of pairs || []) {
30
+ const i = pair.indexOf('=');
31
+ if (i < 1)
32
+ throw new Error(`${flag} expects name=value, got "${pair}"`);
33
+ out[pair.slice(0, i).trim()] = pair.slice(i + 1);
34
+ }
35
+ return out;
36
+ }
37
+ function filterQuery(o) {
38
+ const attrs = parsePairs(o.attr, '--attr');
39
+ return {
40
+ search: o.search,
41
+ channel: o.channel,
42
+ lastMessageStates: o.lastMessageState,
43
+ createdFrom: o.createdFrom,
44
+ createdTo: o.createdTo,
45
+ updatedFrom: o.updatedFrom,
46
+ updatedTo: o.updatedTo,
47
+ attrs: Object.keys(attrs).length ? JSON.stringify(attrs) : undefined,
48
+ };
49
+ }
50
+ /** Attribute values are stored as { value, comment, source, ... }; show just the value. */
51
+ const attrValue = (c, name) => {
52
+ const a = c.attributes?.[name];
53
+ const v = a && typeof a === 'object' && 'value' in a ? a.value : a;
54
+ return v === undefined || v === null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
55
+ };
56
+ function output(value, opts, label) {
57
+ if (opts.file) {
58
+ (0, fs_1.writeFileSync)(opts.file, JSON.stringify(value, null, 2));
59
+ (0, ui_1.success)(`Saved ${label} to ${opts.file}`);
60
+ return true;
61
+ }
62
+ if (opts.json) {
63
+ console.log(JSON.stringify(value, null, 2));
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ // ---- contacts -------------------------------------------------------------
69
+ async function contactsListCommand(opts) {
70
+ const api = (0, session_1.requireApi)();
71
+ try {
72
+ const { contacts, pagination } = await api.listContacts({
73
+ ...filterQuery(opts),
74
+ assignedToMe: opts.assignedToMe ? 'true' : undefined,
75
+ page: opts.page,
76
+ pageSize: opts.pageSize,
77
+ });
78
+ if (output({ contacts, pagination }, opts, 'contacts'))
79
+ return;
80
+ if (!contacts.length)
81
+ return void (0, ui_1.info)('No contacts match.');
82
+ const names = opts.columns
83
+ ? String(opts.columns).split(',').map((s) => s.trim()).filter(Boolean)
84
+ : [...new Set(contacts.flatMap((c) => Object.keys(c.attributes || {})))].slice(0, 3);
85
+ (0, ui_1.table)(['PHONE', 'CHANNELS', ...names], contacts.map((c) => [c.phone_number, (c.channels || []).join(','), ...names.map((n) => attrValue(c, n))]));
86
+ (0, ui_1.info)(`Page ${pagination.page}/${pagination.totalPages || 1} — ${pagination.total} contact(s)${pagination.hasNext ? ' (use --page to see more)' : ''}`);
87
+ }
88
+ catch (err) {
89
+ (0, ui_1.handleError)(err);
90
+ }
91
+ }
92
+ async function contactsGetCommand(phone, opts) {
93
+ const api = (0, session_1.requireApi)();
94
+ try {
95
+ const { contact } = await api.getContact(phone);
96
+ if (output(contact, opts, 'contact'))
97
+ return;
98
+ (0, ui_1.keyValue)({
99
+ Phone: contact.phone_number,
100
+ Channels: (contact.channels || []).join(', '),
101
+ Created: contact.createdAt,
102
+ Updated: contact.updatedAt,
103
+ });
104
+ const names = Object.keys(contact.attributes || {});
105
+ if (names.length) {
106
+ console.log();
107
+ (0, ui_1.table)(['ATTRIBUTE', 'VALUE'], names.map((n) => [n, attrValue(contact, n)]));
108
+ }
109
+ }
110
+ catch (err) {
111
+ (0, ui_1.handleError)(err);
112
+ }
113
+ }
114
+ async function contactsCreateCommand(phone, opts) {
115
+ const api = (0, session_1.requireApi)();
116
+ try {
117
+ const attributes = parsePairs(opts.attr, '--attr');
118
+ const { contact } = await api.saveContact({
119
+ phone_number: phone,
120
+ channels: opts.channel?.length ? opts.channel : undefined,
121
+ });
122
+ if (Object.keys(attributes).length)
123
+ await api.updateContactAttributes(contact.phone_number, attributes);
124
+ (0, ui_1.success)(`Contact ${contact.phone_number} saved.`);
125
+ }
126
+ catch (err) {
127
+ (0, ui_1.handleError)(err);
128
+ }
129
+ }
130
+ async function contactsUpdateCommand(phone, opts) {
131
+ const api = (0, session_1.requireApi)();
132
+ try {
133
+ const attributes = parsePairs(opts.attr, '--attr');
134
+ for (const name of opts.clear || [])
135
+ attributes[name] = '';
136
+ if (!Object.keys(attributes).length) {
137
+ throw new Error('Nothing to update: pass --attr name=value and/or --clear name.');
138
+ }
139
+ const { msg } = await api.updateContactAttributes(phone, attributes);
140
+ (0, ui_1.success)(msg || 'Contact updated.');
141
+ }
142
+ catch (err) {
143
+ (0, ui_1.handleError)(err);
144
+ }
145
+ }
146
+ async function contactsDeleteCommand(phone, opts) {
147
+ const api = (0, session_1.requireApi)();
148
+ try {
149
+ if (!(await (0, ui_1.confirm)(`Delete contact ${phone}? This cannot be undone.`, opts.yes)))
150
+ return;
151
+ await api.deleteContact(phone);
152
+ (0, ui_1.success)('Contact deleted.');
153
+ }
154
+ catch (err) {
155
+ (0, ui_1.handleError)(err);
156
+ }
157
+ }
158
+ async function contactsUploadCommand(file, opts) {
159
+ const api = (0, session_1.requireApi)();
160
+ try {
161
+ if (!(0, fs_1.existsSync)(file))
162
+ throw new Error(`File not found: ${file}`);
163
+ const { upload, invalid } = await api.uploadContacts(file, opts.ignoreUnknown);
164
+ (0, ui_1.success)(`${upload.contactsWritten} contact(s) written from ${upload.rowsRead} row(s).`);
165
+ if (upload.ignoredColumns?.length)
166
+ (0, ui_1.info)(`Ignored columns: ${upload.ignoredColumns.join(', ')}`);
167
+ if (upload.skippedCells) {
168
+ (0, ui_1.info)(`${upload.skippedCells} cell(s) skipped:`);
169
+ for (const bad of invalid || []) {
170
+ console.error(` row ${bad.row} ${bad.column}: ${bad.reason}${bad.value ? ` ("${bad.value}")` : ''}`);
171
+ }
172
+ }
173
+ }
174
+ catch (err) {
175
+ (0, ui_1.handleError)(err);
176
+ }
177
+ }
178
+ async function contactsExportCommand(opts) {
179
+ const api = (0, session_1.requireApi)();
180
+ try {
181
+ const res = await api.exportContacts({
182
+ ...filterQuery(opts),
183
+ export_fields: opts.fields,
184
+ user_email: opts.email,
185
+ });
186
+ if (res.processing)
187
+ return void (0, ui_1.info)(res.msg || `Export started; it will be emailed to ${opts.email}.`);
188
+ if (opts.file) {
189
+ (0, fs_1.writeFileSync)(opts.file, res.csv || '');
190
+ return void (0, ui_1.success)(`Saved contacts to ${opts.file}`);
191
+ }
192
+ process.stdout.write(res.csv || '');
193
+ }
194
+ catch (err) {
195
+ (0, ui_1.handleError)(err);
196
+ }
197
+ }
198
+ /**
199
+ * What `assign` would do, without starting anything. Selects contacts the way the server does:
200
+ * --phones keeps only numbers that are contacts (the rest are silently skipped on a real assign,
201
+ * so say which), otherwise the filters pick the set. The server stops at 20000 contacts.
202
+ */
203
+ async function assignDryRun(api, opts, phones, q) {
204
+ let target = `voice agent ${opts.agent}`;
205
+ if (opts.workflow) {
206
+ const wf = await api.getWorkflow(opts.workflow);
207
+ const steps = wf.graph?.nodes?.length ?? 0;
208
+ if (wf.type && wf.type !== 'normal')
209
+ throw new Error('This workflow is deprecated and cannot take contacts.');
210
+ if (!steps)
211
+ throw new Error(`Workflow "${wf.workFlowName}" has no steps yet; the server would refuse this assign.`);
212
+ target = `workflow "${wf.workFlowName}" (${steps} nodes)`;
213
+ }
214
+ let total;
215
+ let sample;
216
+ const missing = [];
217
+ if (phones?.length) {
218
+ const wanted = [...new Set(phones.map((p) => p.replace(/\D/g, '')).filter(Boolean))];
219
+ const found = [];
220
+ for (let i = 0; i < wanted.length; i += 8) {
221
+ await Promise.all(wanted.slice(i, i + 8).map(async (p) => {
222
+ try {
223
+ await api.getContact(p);
224
+ found.push(p);
225
+ }
226
+ catch {
227
+ missing.push(p);
228
+ }
229
+ }));
230
+ }
231
+ total = found.length;
232
+ sample = wanted.filter((p) => found.includes(p)).slice(0, 5);
233
+ }
234
+ else {
235
+ const { contacts, pagination } = await api.listContacts({ ...q, page: 1, pageSize: 5 });
236
+ total = pagination.total;
237
+ sample = contacts.map((c) => c.phone_number);
238
+ }
239
+ (0, ui_1.keyValue)({
240
+ Target: target,
241
+ 'Would assign': `${total} contact(s)${total > 20000 ? ' (the server caps one assign at 20000)' : ''}`,
242
+ Sample: sample.join(', ') || undefined,
243
+ 'Not contacts (skipped)': missing.length ? missing.join(', ') : undefined,
244
+ Starts: opts.at || 'now',
245
+ 'Daily window': opts.window,
246
+ });
247
+ (0, ui_1.info)('Dry run: nothing was started. Contacts already running in the workflow are skipped by a real assign.');
248
+ }
249
+ /** Add contacts to a workflow (--workflow) or call them with a voice agent (--agent). */
250
+ async function contactsAssignCommand(opts) {
251
+ const api = (0, session_1.requireApi)();
252
+ try {
253
+ if (Boolean(opts.workflow) === Boolean(opts.agent)) {
254
+ throw new Error('Pass exactly one of --workflow <id> or --agent <id>.');
255
+ }
256
+ const phones = opts.phones?.split(',').map((p) => p.trim()).filter(Boolean);
257
+ const q = filterQuery(opts);
258
+ const hasFilters = Object.values(q).some((v) => v !== undefined);
259
+ if (opts.dryRun)
260
+ return await assignDryRun(api, opts, phones, q);
261
+ if (!phones?.length && !hasFilters && !(await (0, ui_1.confirm)('No --phones or filters given: assign EVERY contact?', opts.yes))) {
262
+ return;
263
+ }
264
+ let minTriggerTime;
265
+ let maxTriggerTime;
266
+ if (opts.window) {
267
+ const m = String(opts.window).match(/^(\d{2}:\d{2})-(\d{2}:\d{2})$/);
268
+ if (!m)
269
+ throw new Error('--window must be HH:MM-HH:MM, e.g. 10:00-19:00');
270
+ [, minTriggerTime, maxTriggerTime] = m;
271
+ }
272
+ const res = await api.assignContacts({
273
+ scope: phones?.length ? 'contacts' : 'filtered',
274
+ phones,
275
+ filters: { ...q, attrs: q.attrs ? JSON.parse(q.attrs) : undefined },
276
+ target: opts.workflow ? 'workflow' : 'voice',
277
+ workflowId: opts.workflow,
278
+ agentId: opts.agent,
279
+ name: opts.name,
280
+ language: opts.language,
281
+ scheduleLater: Boolean(opts.at),
282
+ scheduleAt: opts.at,
283
+ minTriggerTime,
284
+ maxTriggerTime,
285
+ numberOfRetries: opts.retries !== undefined ? Number(opts.retries) : undefined,
286
+ gapBetweenRetries: opts.gaps,
287
+ carryOver: opts.carryOver,
288
+ priority: opts.priority !== undefined ? Number(opts.priority) : undefined,
289
+ force: opts.force,
290
+ });
291
+ (res.contactsAssigned === 0 ? ui_1.info : ui_1.success)(res.msg || 'Assigned.');
292
+ if (res.alreadyRunning)
293
+ (0, ui_1.info)(`${res.alreadyRunning} already in this workflow (skipped; use --force to restart them).`);
294
+ if (res.truncated)
295
+ (0, ui_1.info)('The selection was capped at the server limit; narrow the filters to cover the rest.');
296
+ if (res.batchId)
297
+ (0, ui_1.info)(`Batch id: ${res.batchId}`);
298
+ }
299
+ catch (err) {
300
+ (0, ui_1.handleError)(err);
301
+ }
302
+ }
303
+ // ---- attribute definitions --------------------------------------------------
304
+ async function attributesListCommand(opts) {
305
+ const api = (0, session_1.requireApi)();
306
+ try {
307
+ const { definitions } = await api.listContactAttributes();
308
+ if (output(definitions, opts, 'attributes'))
309
+ return;
310
+ if (!definitions.length) {
311
+ return void (0, ui_1.info)('No attributes defined yet. Create one with `cku contacts attributes set <name>`.');
312
+ }
313
+ (0, ui_1.table)(['NAME', 'TYPE', 'LABEL', 'OPTIONS', 'AI-UPDATABLE', 'REQUIRED'], definitions.map((d) => [
314
+ d.name,
315
+ d.type,
316
+ d.display_name || '',
317
+ (d.options || []).join(','),
318
+ d.updateOn === 'manual' ? 'no' : 'yes',
319
+ d.required ? 'yes' : '',
320
+ ]));
321
+ }
322
+ catch (err) {
323
+ (0, ui_1.handleError)(err);
324
+ }
325
+ }
326
+ /**
327
+ * Create or edit an attribute. Saving replaces the whole definition server-side,
328
+ * so an existing one is read first and only the flags given are changed.
329
+ */
330
+ async function attributesSetCommand(name, opts) {
331
+ const api = (0, session_1.requireApi)();
332
+ try {
333
+ if (opts.type && !ATTRIBUTE_TYPES.includes(opts.type)) {
334
+ throw new Error(`--type must be one of: ${ATTRIBUTE_TYPES.join(', ')}`);
335
+ }
336
+ if (opts.defaultMode && !['drop', 'type_default', 'custom'].includes(opts.defaultMode)) {
337
+ throw new Error('--default-mode must be drop, type_default or custom');
338
+ }
339
+ const { definitions } = await api.listContactAttributes();
340
+ const old = definitions.find((d) => d.name === name);
341
+ if (!old && !opts.type) {
342
+ throw new Error(`"${name}" does not exist yet; pass --type (${ATTRIBUTE_TYPES.join(', ')}).`);
343
+ }
344
+ const merged = {
345
+ ...(old || {}),
346
+ name,
347
+ type: opts.type ?? old?.type,
348
+ display_name: opts.label ?? old?.display_name,
349
+ description: opts.description ?? old?.description ?? '',
350
+ options: opts.options !== undefined
351
+ ? String(opts.options).split(',').map((s) => s.trim()).filter(Boolean)
352
+ : old?.options ?? [],
353
+ updateOn: opts.manualOnly ? 'manual' : opts.aiUpdatable ? 'both' : old?.updateOn ?? 'both',
354
+ required: opts.required ? true : opts.optional ? false : old?.required ?? false,
355
+ showOnScreen: opts.hidden ? false : opts.visible ? true : old?.showOnScreen ?? true,
356
+ default_mode: opts.defaultMode ?? old?.default_mode ?? 'custom',
357
+ default_value: opts.defaultValue ?? old?.default_value ?? '',
358
+ };
359
+ const { definition } = await api.saveContactAttribute(merged);
360
+ (0, ui_1.success)(`Attribute "${definition.name}" ${old ? 'updated' : 'created'} (${definition.type}).`);
361
+ }
362
+ catch (err) {
363
+ (0, ui_1.handleError)(err);
364
+ }
365
+ }
366
+ async function attributesDeleteCommand(name, opts) {
367
+ const api = (0, session_1.requireApi)();
368
+ try {
369
+ if (!(await (0, ui_1.confirm)(`Delete attribute "${name}"? Values already on contacts are kept but hidden.`, opts.yes))) {
370
+ return;
371
+ }
372
+ await api.deleteContactAttribute(name);
373
+ (0, ui_1.success)(`Attribute "${name}" deleted.`);
374
+ }
375
+ catch (err) {
376
+ (0, ui_1.handleError)(err);
377
+ }
378
+ }
@@ -9,6 +9,7 @@ exports.toTestFunctions = toTestFunctions;
9
9
  exports.simTestsListCommand = simTestsListCommand;
10
10
  exports.simCreateCommand = simCreateCommand;
11
11
  exports.simUpdateCommand = simUpdateCommand;
12
+ exports.simModelsCommand = simModelsCommand;
12
13
  exports.simDeleteCommand = simDeleteCommand;
13
14
  exports.waitForBatch = waitForBatch;
14
15
  exports.simRunCommand = simRunCommand;
@@ -66,31 +67,42 @@ async function simCreateCommand(agentId, opts) {
66
67
  // Parse the JSON flags before prompting, so a typo fails before any typing.
67
68
  const testVariables = opts.variables ? toTestVariables(await (0, jsonInput_1.readJsonFlag)(opts.variables, '--variables')) : {};
68
69
  const testFunctions = opts.functions ? toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions')) : [];
69
- let { name, prompt, criteria } = opts;
70
+ let { name, persona, prompt, criteria } = opts;
70
71
  if (!name || !prompt || !criteria) {
71
72
  (0, ui_1.needTty)('Pass --name, --prompt and --criteria.');
72
73
  const answers = await (0, prompts_1.default)([
73
74
  { type: name ? null : 'text', name: 'name', message: 'Test case name' },
75
+ {
76
+ type: persona !== undefined ? null : 'text',
77
+ name: 'persona',
78
+ message: 'Caller persona (who they are, tone, what they know)',
79
+ },
74
80
  { type: prompt ? null : 'text', name: 'prompt', message: 'Simulated user prompt' },
75
81
  { type: criteria ? null : 'text', name: 'criteria', message: 'Success criteria' },
76
82
  ], { onCancel: () => process.exit(1) });
77
83
  name = name || answers.name;
84
+ if (persona === undefined && answers.persona)
85
+ persona = answers.persona;
78
86
  prompt = prompt || answers.prompt;
79
87
  criteria = criteria || answers.criteria;
80
88
  }
81
89
  if (!name || !prompt || !criteria) {
82
90
  return (0, ui_1.handleError)(new Error('--name, --prompt and --criteria are all required (or answer the prompts)'));
83
91
  }
84
- const res = await api.createTestCase({
92
+ const payload = {
85
93
  agentId,
86
94
  testcaseName: name,
87
95
  userPrompt: prompt,
88
96
  successCriteria: criteria,
89
- testLLMModel: opts.model || 'gpt-4.1-mini',
90
97
  testVariables,
91
98
  testFunctions,
92
- });
93
- (0, ui_1.success)(`Created test case ${res.testCase?._id || ''}.`);
99
+ };
100
+ if (opts.model !== undefined)
101
+ payload.testLLMModel = opts.model;
102
+ if (persona !== undefined)
103
+ payload.userPersona = persona;
104
+ const res = await api.createTestCase(payload);
105
+ (0, ui_1.success)(`Created test case ${res.testCase?._id || ''} (model: ${res.testCase?.testLLMModel || opts.model || 'default'}).`);
94
106
  }
95
107
  catch (err) {
96
108
  (0, ui_1.handleError)(err);
@@ -107,6 +119,8 @@ async function simUpdateCommand(testCaseId, opts) {
107
119
  const body = {};
108
120
  if (opts.name !== undefined)
109
121
  body.testcaseName = opts.name;
122
+ if (opts.persona !== undefined)
123
+ body.userPersona = opts.persona;
110
124
  if (opts.prompt !== undefined)
111
125
  body.userPrompt = opts.prompt;
112
126
  if (opts.criteria !== undefined)
@@ -118,7 +132,7 @@ async function simUpdateCommand(testCaseId, opts) {
118
132
  if (opts.functions !== undefined)
119
133
  body.testFunctions = toTestFunctions(await (0, jsonInput_1.readJsonFlag)(opts.functions, '--functions'));
120
134
  if (!Object.keys(body).length) {
121
- return (0, ui_1.handleError)(new Error('Nothing to update. Pass at least one of --name, --prompt, --criteria, --model, --variables, --functions.'));
135
+ return (0, ui_1.handleError)(new Error('Nothing to update. Pass at least one of --name, --persona, --prompt, --criteria, --model, --variables, --functions.'));
122
136
  }
123
137
  const res = await api.updateTestCase(testCaseId, body);
124
138
  (0, ui_1.success)(`Updated test case ${res.testCase?._id || testCaseId}: ${Object.keys(body).join(', ')}.`);
@@ -127,6 +141,19 @@ async function simUpdateCommand(testCaseId, opts) {
127
141
  (0, ui_1.handleError)(err);
128
142
  }
129
143
  }
144
+ async function simModelsCommand(opts) {
145
+ const api = (0, session_1.requireApi)();
146
+ try {
147
+ const res = await api.simulationModels();
148
+ if (opts.json)
149
+ return void console.log(JSON.stringify(res, null, 2));
150
+ const def = res.categories?.[0]?.defaultModel;
151
+ (0, ui_1.table)(['MODEL', 'DEFAULT'], (res.models || []).map((m) => [m.id, m.id === def ? 'yes' : '']));
152
+ }
153
+ catch (err) {
154
+ (0, ui_1.handleError)(err);
155
+ }
156
+ }
130
157
  async function simDeleteCommand(testCaseId, opts) {
131
158
  const api = (0, session_1.requireApi)();
132
159
  try {
@@ -174,11 +201,17 @@ function resultTable(rows) {
174
201
  (0, ui_1.table)(['TEST CASE', 'VERSION', 'RESULT', 'TURNS', 'SUMMARY'], rows.map((r) => [
175
202
  r.testcaseId?.testcaseName || r.testcaseName || r.testcaseId || '',
176
203
  r.versionId?.versionName || r.versionId || '',
177
- r.isTestSucceed === undefined ? '' : r.isTestSucceed ? 'PASS' : 'FAIL',
204
+ r.analysisStatus === 'failed'
205
+ ? 'GRADING FAILED'
206
+ : r.isTestSucceed === undefined
207
+ ? ''
208
+ : r.isTestSucceed
209
+ ? 'PASS'
210
+ : 'FAIL',
178
211
  r.total_turns ?? '',
179
212
  String(r.testResultSummary || '').replace(/\s+/g, ' ').slice(0, 70),
180
213
  ]));
181
- const passed = rows.filter((r) => r.isTestSucceed).length;
214
+ const passed = rows.filter((r) => r.analysisStatus !== 'failed' && r.isTestSucceed).length;
182
215
  (0, ui_1.info)(`\n${passed}/${rows.length} passed.`);
183
216
  }
184
217
  async function simRunCommand(agentId, opts) {
@@ -213,7 +246,7 @@ async function simRunCommand(agentId, opts) {
213
246
  if (!done) {
214
247
  return (0, ui_1.handleError)(new Error(`Timed out with ${rows.length}${total ? `/${total}` : ''} results. The run is still going: cku sim results ${batchId} --wait`));
215
248
  }
216
- if (rows.some((r) => !r.isTestSucceed))
249
+ if (rows.some((r) => r.analysisStatus === 'failed' || !r.isTestSucceed))
217
250
  process.exitCode = 1;
218
251
  }
219
252
  catch (err) {
@@ -236,6 +269,9 @@ async function simRunsListCommand(agentId, opts) {
236
269
  r.epoch ?? '',
237
270
  r.createdAt ? new Date(r.createdAt).toLocaleString('en-US') : '',
238
271
  ]));
272
+ if (typeof res.totalCost === 'number') {
273
+ console.log(`Total cost, all runs: ${res.totalCost > 0 ? res.totalCost.toFixed(3) : 'N/A'}`);
274
+ }
239
275
  }
240
276
  catch (err) {
241
277
  (0, ui_1.handleError)(err);
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.updateCommand = updateCommand;
37
37
  const child_process_1 = require("child_process");
38
38
  const ui_1 = require("../lib/ui");
39
+ const win = process.platform === 'win32';
40
+ const bin = (name) => (win ? `${name}.cmd` : name);
39
41
  /**
40
42
  * `cku update` — upgrade the CLI itself from npm, then refresh the skill pack
41
43
  * (which lives in its own repo, so it may have moved on even if the CLI hasn't).
@@ -46,7 +48,7 @@ async function updateCommand(opts) {
46
48
  try {
47
49
  let legacyInstalled = false;
48
50
  try {
49
- (0, child_process_1.execFileSync)('npm', ['list', '--global', '@callkaro/cli', '--depth=0'], { stdio: 'ignore' });
51
+ (0, child_process_1.execFileSync)(bin('npm'), ['list', '--global', '@callkaro/cli', '--depth=0'], { stdio: 'ignore', shell: win });
50
52
  legacyInstalled = true;
51
53
  }
52
54
  catch {
@@ -54,19 +56,24 @@ async function updateCommand(opts) {
54
56
  }
55
57
  if (legacyInstalled) {
56
58
  (0, ui_1.info)('Removing legacy @callkaro/cli installation …');
57
- (0, child_process_1.execFileSync)('npm', ['uninstall', '--global', '@callkaro/cli'], { stdio: 'ignore' });
59
+ (0, child_process_1.execFileSync)(bin('npm'), ['uninstall', '--global', '@callkaro/cli'], { stdio: 'ignore', shell: win });
58
60
  }
59
- (0, child_process_1.execFileSync)('npm', ['install', '--global', 'callkaro-cli-unofficial'], { stdio: 'ignore' });
61
+ (0, child_process_1.execFileSync)(bin('npm'), ['install', '--global', 'callkaro-cli-unofficial'], { stdio: 'ignore', shell: win });
60
62
  }
61
63
  catch {
62
- (0, ui_1.fail)('npm could not write to the global folder (it needs elevated permissions).');
63
- (0, ui_1.info)('Re-run as:\n sudo cku update');
64
+ if (win) {
65
+ (0, ui_1.fail)('npm could not update the CLI. Run PowerShell as Administrator, or re-run:\n npm install --global callkaro-cli-unofficial');
66
+ }
67
+ else {
68
+ (0, ui_1.fail)('npm could not write to the global folder (it needs elevated permissions).');
69
+ (0, ui_1.info)('Re-run as:\n sudo cku update');
70
+ }
64
71
  process.exitCode = 1;
65
72
  return;
66
73
  }
67
74
  let version = '';
68
75
  try {
69
- version = (0, child_process_1.execFileSync)('cku', ['--version'], { encoding: 'utf8' }).trim();
76
+ version = (0, child_process_1.execFileSync)(bin('cku'), ['--version'], { encoding: 'utf8', shell: win }).trim();
70
77
  }
71
78
  catch {
72
79
  /* not on PATH yet in this shell */