callkaro-cli-unofficial 0.9.3 → 0.9.5
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/dist/commands/agentMemory.js +72 -0
- package/dist/commands/agentVersions.js +3 -1
- package/dist/commands/agents.js +46 -22
- package/dist/commands/skills.js +14 -9
- package/dist/endpoints.json +0 -0
- package/dist/index.js +26 -0
- package/dist/lib/agentMemory.js +177 -0
- package/dist/lib/agentUpdate.js +35 -5
- package/dist/lib/liveVoiceCheck.js +55 -0
- package/dist/lib/updateCheck.js +67 -10
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentsMemoryCommand = agentsMemoryCommand;
|
|
4
|
+
exports.agentsVoiceRotationCommand = agentsVoiceRotationCommand;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const session_1 = require("../lib/session");
|
|
7
|
+
const ui_1 = require("../lib/ui");
|
|
8
|
+
const versions_1 = require("../lib/versions");
|
|
9
|
+
const looseJson_1 = require("../lib/looseJson");
|
|
10
|
+
const liveVoiceCheck_1 = require("../lib/liveVoiceCheck");
|
|
11
|
+
const agentMemory_1 = require("../lib/agentMemory");
|
|
12
|
+
async function run(agentId, opts, flags, change, describe, what, verify) {
|
|
13
|
+
const api = (0, session_1.requireApi)();
|
|
14
|
+
try {
|
|
15
|
+
const versionId = (0, versions_1.firstVersion)(opts.versions);
|
|
16
|
+
if (!versionId)
|
|
17
|
+
throw new Error('--versions <id> is required (find it with `cku agents versions <agentId>`)');
|
|
18
|
+
const { agent } = await api.getAgent(agentId, versionId);
|
|
19
|
+
const current = agent.agentMemory;
|
|
20
|
+
if (!(0, agentMemory_1.hasChanges)(flags)) {
|
|
21
|
+
if (opts.json)
|
|
22
|
+
console.log(JSON.stringify(current ?? {}, null, 2));
|
|
23
|
+
else
|
|
24
|
+
(0, ui_1.keyValue)(describe(current));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const next = change(current);
|
|
28
|
+
if (verify)
|
|
29
|
+
await verify(api, next, agent);
|
|
30
|
+
const body = { agentMemory: next };
|
|
31
|
+
if (opts.commit)
|
|
32
|
+
body.commitMessage = opts.commit;
|
|
33
|
+
const result = await api.updateAgent(agentId, versionId, body);
|
|
34
|
+
if (opts.json)
|
|
35
|
+
console.log(JSON.stringify(next, null, 2));
|
|
36
|
+
else {
|
|
37
|
+
(0, ui_1.success)(result.msg || `${what} updated.`);
|
|
38
|
+
(0, ui_1.keyValue)(describe(next));
|
|
39
|
+
(0, ui_1.info)(' Takes effect on the next call. Publish the version if it is not live yet.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
(0, ui_1.handleError)(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function agentsMemoryCommand(agentId, opts) {
|
|
47
|
+
const { versions, commit, json, ...flags } = opts;
|
|
48
|
+
return run(agentId, opts, flags, (memory) => (0, agentMemory_1.applyMemoryFlags)(memory, flags), agentMemory_1.describeMemory, 'Call memory');
|
|
49
|
+
}
|
|
50
|
+
function agentsVoiceRotationCommand(agentId, opts) {
|
|
51
|
+
const { versions, commit, json, voices, ...rest } = opts;
|
|
52
|
+
const flags = rest;
|
|
53
|
+
if (voices !== undefined) {
|
|
54
|
+
const file = String(voices).startsWith('@') ? String(voices).slice(1) : String(voices);
|
|
55
|
+
if (!(0, fs_1.existsSync)(file)) {
|
|
56
|
+
(0, ui_1.handleError)(new Error(`File not found: ${file}`));
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
flags.voices = JSON.parse((0, looseJson_1.readJsonFile)(file));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
(0, ui_1.handleError)(new Error(`--voices is not valid JSON: ${error.message}`));
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const verify = flags.voices === undefined ? undefined : async (api, memory, version) => {
|
|
68
|
+
const entries = Object.fromEntries((memory.voice_rotation_config?.voices || []).map((voice, index) => [`voices[${index}]`, voice]));
|
|
69
|
+
await (0, liveVoiceCheck_1.checkVoiceConfigs)(api, entries, async () => (0, liveVoiceCheck_1.savedVoices)(version));
|
|
70
|
+
};
|
|
71
|
+
return run(agentId, opts, flags, (memory) => (0, agentMemory_1.applyRotationFlags)(memory, flags), agentMemory_1.describeRotation, 'Voice rotation', verify);
|
|
72
|
+
}
|
|
@@ -19,6 +19,7 @@ const dashboardApi_1 = require("../lib/dashboardApi");
|
|
|
19
19
|
const ui_1 = require("../lib/ui");
|
|
20
20
|
const versions_1 = require("../lib/versions");
|
|
21
21
|
const agentUpdate_1 = require("../lib/agentUpdate");
|
|
22
|
+
const liveVoiceCheck_1 = require("../lib/liveVoiceCheck");
|
|
22
23
|
const setJson_1 = require("./setJson");
|
|
23
24
|
/** Keys a clone may override. Anything else is rejected before the request. */
|
|
24
25
|
const CLONE_OVERRIDE_KEYS = new Set([
|
|
@@ -60,7 +61,8 @@ async function agentsCloneVersionCommand(agentId, opts) {
|
|
|
60
61
|
if (unsupported.length) {
|
|
61
62
|
return (0, ui_1.handleError)(new Error(`Unsupported clone override(s): ${unsupported.join(', ')}\n allowed: ${[...CLONE_OVERRIDE_KEYS].join(', ')}`));
|
|
62
63
|
}
|
|
63
|
-
overrides = (0, agentUpdate_1.preparePatch)(parsed).patch;
|
|
64
|
+
overrides = (0, agentUpdate_1.preparePatch)(parsed, { skipVoices: true }).patch;
|
|
65
|
+
await (0, liveVoiceCheck_1.checkVoiceConfigs)(api, { voice_configuration: overrides.voice_configuration, secondary_voice_configuration: overrides.secondary_voice_configuration }, sourceVersionId ? async () => (0, liveVoiceCheck_1.savedVoices)((await api.getAgent(agentId, sourceVersionId)).agent) : undefined);
|
|
64
66
|
}
|
|
65
67
|
const res = await api.cloneAgentVersion(agentId, sourceVersionId, {
|
|
66
68
|
...overrides,
|
package/dist/commands/agents.js
CHANGED
|
@@ -53,6 +53,7 @@ const ui_1 = require("../lib/ui");
|
|
|
53
53
|
const versions_1 = require("../lib/versions");
|
|
54
54
|
const agentUpdate_1 = require("../lib/agentUpdate");
|
|
55
55
|
Object.defineProperty(exports, "preparePatch", { enumerable: true, get: function () { return agentUpdate_1.preparePatch; } });
|
|
56
|
+
const liveVoiceCheck_1 = require("../lib/liveVoiceCheck");
|
|
56
57
|
const dashboardAuth_1 = require("../lib/dashboardAuth");
|
|
57
58
|
const agentImport_1 = require("../lib/agentImport");
|
|
58
59
|
const dbFields_1 = require("../lib/dbFields");
|
|
@@ -400,7 +401,7 @@ async function agentsUpdateCommand(agentId, opts) {
|
|
|
400
401
|
const dbf = (0, dbFields_1.dbManagedFields)(obj);
|
|
401
402
|
if (dbf.length)
|
|
402
403
|
throw new Error((0, dbFields_1.dbManagedFieldsError)(dbf, '--set object'));
|
|
403
|
-
const prepared = (0, agentUpdate_1.preparePatch)(obj
|
|
404
|
+
const prepared = (0, agentUpdate_1.preparePatch)(obj, { skipVoices: true });
|
|
404
405
|
patch = prepared.patch;
|
|
405
406
|
versionKeys = prepared.versionKeys;
|
|
406
407
|
wholesaleKeys = prepared.wholesaleKeys;
|
|
@@ -421,37 +422,60 @@ async function agentsUpdateCommand(agentId, opts) {
|
|
|
421
422
|
' Run the command once per version id.'));
|
|
422
423
|
}
|
|
423
424
|
try {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
// version. Wholesale keys are version-level, so --versions is set here.
|
|
430
|
-
// `functions` is a whole array on the backend: merge by name unless the
|
|
431
|
-
// caller asked to replace the list.
|
|
425
|
+
const vid = (0, versions_1.firstVersion)(opts.versions);
|
|
426
|
+
await (0, liveVoiceCheck_1.checkVoiceConfigs)(api, { voice_configuration: patch.voice_configuration, secondary_voice_configuration: patch.secondary_voice_configuration }, vid ? async () => (0, liveVoiceCheck_1.savedVoices)((await api.getAgent(agentId, vid)).agent) : undefined);
|
|
427
|
+
let versionTarget = vid;
|
|
428
|
+
if (opts.commit && !versionTarget)
|
|
429
|
+
versionTarget = (await api.getAgent(agentId)).agent.versionId;
|
|
432
430
|
const mergeFns = Array.isArray(patch.functions) && !opts.replaceFunctions;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
431
|
+
let current;
|
|
432
|
+
try {
|
|
433
|
+
current = (await api.getAgent(agentId, versionTarget)).agent;
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (mergeFns)
|
|
437
|
+
throw error;
|
|
438
|
+
(0, ui_1.info)(' (could not read the current values; sending the patch as given)');
|
|
439
|
+
}
|
|
440
|
+
let effectivePatch = patch;
|
|
441
|
+
if (current) {
|
|
442
|
+
if (wholesaleKeys.length)
|
|
443
|
+
effectivePatch = (0, agentUpdate_1.mergeWholesaleKeys)(effectivePatch, current);
|
|
436
444
|
if (mergeFns)
|
|
437
|
-
|
|
445
|
+
effectivePatch.functions = (0, agentUpdate_1.mergeFunctions)(effectivePatch.functions, current.functions);
|
|
446
|
+
}
|
|
447
|
+
let { send, changed, unchanged } = current
|
|
448
|
+
? (0, agentUpdate_1.planUpdate)(current, effectivePatch)
|
|
449
|
+
: { send: effectivePatch, changed: Object.keys(effectivePatch), unchanged: [] };
|
|
450
|
+
if (opts.dryRun) {
|
|
451
|
+
if (opts.json)
|
|
452
|
+
console.log(JSON.stringify({ dryRun: true, changing: changed, unchanged }, null, 2));
|
|
453
|
+
else {
|
|
454
|
+
(0, ui_1.info)(` would change: ${changed.join(', ') || '(nothing)'}`);
|
|
455
|
+
if (unchanged.length)
|
|
456
|
+
(0, ui_1.info)(` already set, skipped: ${unchanged.join(', ')}`);
|
|
457
|
+
(0, ui_1.info)(' Nothing saved (--dry-run).');
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!changed.length) {
|
|
462
|
+
(0, ui_1.info)(`Nothing to change: ${unchanged.join(', ')} already hold the requested value(s).`);
|
|
463
|
+
return;
|
|
438
464
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
let versionTarget = (0, versions_1.firstVersion)(opts.versions);
|
|
443
|
-
if (opts.commit && !versionTarget) {
|
|
444
|
-
const { agent } = await api.getAgent(agentId);
|
|
445
|
-
versionTarget = agent.versionId;
|
|
465
|
+
if (changed.length > agentUpdate_1.MAX_FIELDS_WITHOUT_YES && !opts.yes) {
|
|
466
|
+
return (0, ui_1.handleError)(new Error(`This would change ${changed.length} fields (${changed.join(', ')}).\n` +
|
|
467
|
+
' Send only the fields you mean to change. If this is intended, re-run with --yes (or preview with --dry-run).'));
|
|
446
468
|
}
|
|
447
|
-
const body = { ...
|
|
469
|
+
const body = { ...send };
|
|
448
470
|
if (opts.commit)
|
|
449
471
|
body.commitMessage = opts.commit;
|
|
450
472
|
const res = await api.updateAgent(agentId, versionTarget, body);
|
|
451
473
|
if (opts.json)
|
|
452
474
|
return void console.log(JSON.stringify(res, null, 2));
|
|
453
475
|
(0, ui_1.success)(res.msg || 'Agent updated.');
|
|
454
|
-
(0, ui_1.info)(` updated: ${
|
|
476
|
+
(0, ui_1.info)(` updated: ${changed.join(', ')}`);
|
|
477
|
+
if (unchanged.length)
|
|
478
|
+
(0, ui_1.info)(` already set, skipped: ${unchanged.join(', ')}`);
|
|
455
479
|
if (opts.commit)
|
|
456
480
|
(0, ui_1.info)(` commit: ${opts.commit}`);
|
|
457
481
|
}
|
package/dist/commands/skills.js
CHANGED
|
@@ -12,6 +12,7 @@ const os_1 = require("os");
|
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
13
|
const ui_1 = require("../lib/ui");
|
|
14
14
|
const config_1 = require("../lib/config");
|
|
15
|
+
const updateCheck_1 = require("../lib/updateCheck");
|
|
15
16
|
/**
|
|
16
17
|
* Skills live in their own public repo (Mail-Daddy-AI/callkaro-skills), NOT in
|
|
17
18
|
* this package — so a skills update never requires a CLI release. Install and
|
|
@@ -157,15 +158,15 @@ function restoreOwnershipIfSudo(dest) {
|
|
|
157
158
|
/** Codex has no skills system — wire a pointer block into ~/.codex/AGENTS.md. */
|
|
158
159
|
function wireCodexPointer() {
|
|
159
160
|
const codexHome = (0, path_1.join)((0, os_1.homedir)(), '.codex');
|
|
160
|
-
const block = `${CODEX_MARK_START}
|
|
161
|
-
## CallKaro voice agents
|
|
162
|
-
For ANY task involving CallKaro (voice agents, calls, batch campaigns,
|
|
163
|
-
simulations, phone numbers, analytics): FIRST read
|
|
164
|
-
~/.codex/skills/callkaro/SKILL.md and follow the files it routes to
|
|
165
|
-
(INSTRUCTIONS.md for how to operate; authoring-pipeline/ when building agents).
|
|
166
|
-
All operations run through the \`cku\` CLI (this fork) — it must be logged in (\`cku login\`).
|
|
167
|
-
If you have the official CLI instead, substitute \`ck\` for \`cku\` throughout.
|
|
168
|
-
Prefer \`--json\` output and never invent ids — list first.
|
|
161
|
+
const block = `${CODEX_MARK_START}
|
|
162
|
+
## CallKaro voice agents
|
|
163
|
+
For ANY task involving CallKaro (voice agents, calls, batch campaigns,
|
|
164
|
+
simulations, phone numbers, analytics): FIRST read
|
|
165
|
+
~/.codex/skills/callkaro/SKILL.md and follow the files it routes to
|
|
166
|
+
(INSTRUCTIONS.md for how to operate; authoring-pipeline/ when building agents).
|
|
167
|
+
All operations run through the \`cku\` CLI (this fork) — it must be logged in (\`cku login\`).
|
|
168
|
+
If you have the official CLI instead, substitute \`ck\` for \`cku\` throughout.
|
|
169
|
+
Prefer \`--json\` output and never invent ids — list first.
|
|
169
170
|
${CODEX_MARK_END}`;
|
|
170
171
|
const agentsMd = (0, path_1.join)(codexHome, 'AGENTS.md');
|
|
171
172
|
let content = (0, fs_1.existsSync)(agentsMd) ? (0, fs_1.readFileSync)(agentsMd, 'utf8') : '';
|
|
@@ -209,6 +210,9 @@ async function installOrUpdate(opts, verb, pack) {
|
|
|
209
210
|
(0, ui_1.info)(`\nPack: ${which}`);
|
|
210
211
|
(0, ui_1.info)('Open a new agent session and ask it anything about CallKaro.');
|
|
211
212
|
(0, ui_1.info)('Requires the `cku` CLI to be logged in (`cku login`) — the skill pack documents that CLI.');
|
|
213
|
+
// A shared pack serves several targets; its caller records the install once.
|
|
214
|
+
if (!pack)
|
|
215
|
+
await (0, updateCheck_1.recordSkillsInstalled)(repo, (0, config_1.skillsBranch)());
|
|
212
216
|
}
|
|
213
217
|
finally {
|
|
214
218
|
cleanup();
|
|
@@ -299,6 +303,7 @@ async function skillsUpdateCommand(opts) {
|
|
|
299
303
|
try {
|
|
300
304
|
for (const t of targets)
|
|
301
305
|
await installOrUpdate(t.opts, 'Updated', shared);
|
|
306
|
+
await (0, updateCheck_1.recordSkillsInstalled)(repo, (0, config_1.skillsBranch)());
|
|
302
307
|
}
|
|
303
308
|
finally {
|
|
304
309
|
pack.cleanup();
|
package/dist/endpoints.json
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ const KPI_COLUMNS = 'action_items | in_progress | testing_ab | live_prod | done
|
|
|
51
51
|
const analyzers_1 = require("./commands/analyzers");
|
|
52
52
|
const models_1 = require("./commands/models");
|
|
53
53
|
const agents_1 = require("./commands/agents");
|
|
54
|
+
const agentMemory_1 = require("./commands/agentMemory");
|
|
54
55
|
const agentChat_1 = require("./commands/agentChat");
|
|
55
56
|
const scheduled_1 = require("./commands/scheduled");
|
|
56
57
|
const agentVersions_1 = require("./commands/agentVersions");
|
|
@@ -877,8 +878,33 @@ agents.command('update <agentId>').description('update fields via --set \'{json}
|
|
|
877
878
|
.option('--versions <id>', 'target a specific version (needed for version-level fields)')
|
|
878
879
|
.option('--commit <msg>', 'commit message for the version change')
|
|
879
880
|
.option('--replace-functions', 'replace the whole functions list instead of merging by name (default merges; omitted functions are kept)')
|
|
881
|
+
.option('--dry-run', 'show which fields would change, without saving')
|
|
882
|
+
.option('--yes', 'allow an update that changes more than 8 fields')
|
|
880
883
|
.option('--json', 'output the raw updated-agent response')
|
|
881
884
|
.action(agents_1.agentsUpdateCommand);
|
|
885
|
+
agents.command('memory <agentId>').description('show or change call memory (x_call_history for functions); no flags = show')
|
|
886
|
+
.option('--versions <id>', 'version id (required)')
|
|
887
|
+
.option('--enable', 'turn call memory on').option('--disable', 'turn call memory off')
|
|
888
|
+
.option('--max-calls <n>', 'earlier calls to load, -1 (all, up to 200) to 200')
|
|
889
|
+
.option('--days <n>', 'only calls from the last n days, 0 = no limit')
|
|
890
|
+
.option('--only-connected', 'only calls that connected').option('--all-calls', 'include calls that did not connect')
|
|
891
|
+
.option('--with-transcript', 'include transcripts (large)').option('--without-transcript', 'leave transcripts out')
|
|
892
|
+
.option('--agents <scope>', 'self | all | <agentId,agentId,...> - whose calls to read')
|
|
893
|
+
.option('--commit <msg>', 'commit message for the version change')
|
|
894
|
+
.option('--json', 'output the raw agentMemory object')
|
|
895
|
+
.action(agentMemory_1.agentsMemoryCommand);
|
|
896
|
+
agents.command('voice-rotation <agentId>').description('show or change voice rotation on retry; no flags = show')
|
|
897
|
+
.option('--versions <id>', 'version id (required)')
|
|
898
|
+
.option('--enable', 'turn rotation on').option('--disable', 'turn rotation off')
|
|
899
|
+
.option('--trigger <mode>', 'instant_hangup | custom')
|
|
900
|
+
.option('--status <status>', 'custom trigger: previous call connected | not_connected')
|
|
901
|
+
.option('--condition <expr>', 'custom trigger condition source[.field]:operator[:value] (repeatable)', (value, previous = []) => [...previous, value])
|
|
902
|
+
.option('--clear-conditions', 'remove all conditions')
|
|
903
|
+
.option('--voices <@file.json>', 'JSON array of retry voice configurations (max 5)')
|
|
904
|
+
.option('--on-exhausted <mode>', 'last | primary | cycle')
|
|
905
|
+
.option('--commit <msg>', 'commit message for the version change')
|
|
906
|
+
.option('--json', 'output the raw agentMemory object')
|
|
907
|
+
.action(agentMemory_1.agentsVoiceRotationCommand);
|
|
882
908
|
// Phone assignment (agent-centric). The one-inbound-agent rule is enforced server-side.
|
|
883
909
|
agents.command('set-inbound <agentId>').description("add/remove an agent's inbound phone number")
|
|
884
910
|
.option('--number <id>', 'phone number id')
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasChanges = exports.CONDITION_OPERATORS = exports.CONDITION_SOURCES = exports.ON_EXHAUSTED = exports.PREVIOUS_STATUSES = exports.TRIGGER_MODES = exports.MAX_CONDITIONS = exports.MAX_ROTATION_VOICES = exports.MAX_DAYS_RANGE = exports.MAX_CALLS_RANGE = void 0;
|
|
4
|
+
exports.applyMemoryFlags = applyMemoryFlags;
|
|
5
|
+
exports.parseCondition = parseCondition;
|
|
6
|
+
exports.applyRotationFlags = applyRotationFlags;
|
|
7
|
+
exports.describeMemory = describeMemory;
|
|
8
|
+
exports.describeRotation = describeRotation;
|
|
9
|
+
/** Helpers for call memory and retry voice rotation stored in version.agentMemory. */
|
|
10
|
+
exports.MAX_CALLS_RANGE = [-1, 200];
|
|
11
|
+
exports.MAX_DAYS_RANGE = [0, 3650];
|
|
12
|
+
exports.MAX_ROTATION_VOICES = 5;
|
|
13
|
+
exports.MAX_CONDITIONS = 20;
|
|
14
|
+
exports.TRIGGER_MODES = ['instant_hangup', 'custom'];
|
|
15
|
+
exports.PREVIOUS_STATUSES = ['connected', 'not_connected'];
|
|
16
|
+
exports.ON_EXHAUSTED = ['last', 'primary', 'cycle'];
|
|
17
|
+
exports.CONDITION_SOURCES = ['post_call_variable', 'hangup_reason', 'call_duration', 'conversion_status'];
|
|
18
|
+
exports.CONDITION_OPERATORS = ['equals', 'not_equals', 'empty', 'not_empty', 'eq', 'gt', 'gte', 'lt', 'lte'];
|
|
19
|
+
const OBJECT_ID = /^[0-9a-fA-F]{24}$/;
|
|
20
|
+
const clone = (value) => JSON.parse(JSON.stringify(value ?? {}));
|
|
21
|
+
function oneOf(label, value, allowed) {
|
|
22
|
+
if (!allowed.includes(value))
|
|
23
|
+
throw new Error(`Invalid ${label} "${value}"\n allowed: ${allowed.join(', ')}`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function intIn(label, raw, [min, max]) {
|
|
27
|
+
const value = Number(raw);
|
|
28
|
+
if (raw.trim() === '' || !Number.isInteger(value) || value < min || value > max) {
|
|
29
|
+
throw new Error(`Invalid ${label} "${raw}": expected a whole number from ${min} to ${max}`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function pairFlag(label, on, off, onFlag, offFlag) {
|
|
34
|
+
if (on && off)
|
|
35
|
+
throw new Error(`${onFlag} and ${offFlag} cannot be used together (${label})`);
|
|
36
|
+
}
|
|
37
|
+
const hasChanges = (flags) => Object.values(flags).some((value) => value !== undefined && value !== false);
|
|
38
|
+
exports.hasChanges = hasChanges;
|
|
39
|
+
function applyMemoryFlags(current, flags) {
|
|
40
|
+
pairFlag('on/off', flags.enable, flags.disable, '--enable', '--disable');
|
|
41
|
+
pairFlag('connected filter', flags.onlyConnected, flags.allCalls, '--only-connected', '--all-calls');
|
|
42
|
+
pairFlag('transcript', flags.withTranscript, flags.withoutTranscript, '--with-transcript', '--without-transcript');
|
|
43
|
+
const out = clone(current);
|
|
44
|
+
const filters = (out.call_filters = out.call_filters || {});
|
|
45
|
+
if (flags.enable)
|
|
46
|
+
out.enabled_for_call = true;
|
|
47
|
+
if (flags.disable)
|
|
48
|
+
out.enabled_for_call = false;
|
|
49
|
+
if (flags.maxCalls !== undefined)
|
|
50
|
+
filters.maxCalls = intIn('--max-calls', String(flags.maxCalls), exports.MAX_CALLS_RANGE);
|
|
51
|
+
if (flags.days !== undefined)
|
|
52
|
+
filters.maxDays = intIn('--days', String(flags.days), exports.MAX_DAYS_RANGE);
|
|
53
|
+
if (flags.onlyConnected)
|
|
54
|
+
filters.onlyConnected = true;
|
|
55
|
+
if (flags.allCalls)
|
|
56
|
+
filters.onlyConnected = false;
|
|
57
|
+
if (flags.withTranscript)
|
|
58
|
+
filters.includeTranscript = true;
|
|
59
|
+
if (flags.withoutTranscript)
|
|
60
|
+
filters.includeTranscript = false;
|
|
61
|
+
if (flags.agents !== undefined) {
|
|
62
|
+
const raw = String(flags.agents).trim();
|
|
63
|
+
if (raw === 'self' || raw === 'all')
|
|
64
|
+
filters.additionalAgents = { mode: raw, agentIds: [] };
|
|
65
|
+
else {
|
|
66
|
+
const ids = raw.split(',').map((id) => id.trim()).filter(Boolean);
|
|
67
|
+
const bad = ids.filter((id) => !OBJECT_ID.test(id));
|
|
68
|
+
if (!ids.length || bad.length) {
|
|
69
|
+
throw new Error(`Invalid --agents "${raw}": use self, all, or a comma-separated list of agent ids${bad.length ? ` (bad: ${bad.join(', ')})` : ''}`);
|
|
70
|
+
}
|
|
71
|
+
if (ids.length > 50)
|
|
72
|
+
throw new Error('--agents takes at most 50 agent ids');
|
|
73
|
+
filters.additionalAgents = { mode: 'selected', agentIds: ids };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** Parse source[.field]:operator[:value]. */
|
|
79
|
+
function parseCondition(text) {
|
|
80
|
+
const [lhs, operator, ...rest] = text.split(':');
|
|
81
|
+
if (!lhs || !operator)
|
|
82
|
+
throw new Error(`Bad --condition "${text}": expected source[.field]:operator[:value], e.g. hangup_reason:equals:user_busy`);
|
|
83
|
+
const dot = lhs.indexOf('.');
|
|
84
|
+
const source = dot < 0 ? lhs : lhs.slice(0, dot);
|
|
85
|
+
const field = dot < 0 ? undefined : lhs.slice(dot + 1);
|
|
86
|
+
oneOf('condition source', source, exports.CONDITION_SOURCES);
|
|
87
|
+
oneOf('condition operator', operator, exports.CONDITION_OPERATORS);
|
|
88
|
+
if (source === 'post_call_variable' && !field)
|
|
89
|
+
throw new Error(`Bad --condition "${text}": post_call_variable needs a field, e.g. post_call_variable.interested:equals:false`);
|
|
90
|
+
if (source !== 'post_call_variable' && field)
|
|
91
|
+
throw new Error(`Bad --condition "${text}": only post_call_variable takes a .field`);
|
|
92
|
+
const condition = { source, operator };
|
|
93
|
+
if (field)
|
|
94
|
+
condition.field = field;
|
|
95
|
+
if (operator !== 'empty' && operator !== 'not_empty') {
|
|
96
|
+
if (!rest.length)
|
|
97
|
+
throw new Error(`Bad --condition "${text}": operator "${operator}" needs a value`);
|
|
98
|
+
const raw = rest.join(':');
|
|
99
|
+
try {
|
|
100
|
+
condition.value = JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
condition.value = raw;
|
|
104
|
+
}
|
|
105
|
+
if (typeof condition.value === 'object')
|
|
106
|
+
condition.value = raw;
|
|
107
|
+
}
|
|
108
|
+
return condition;
|
|
109
|
+
}
|
|
110
|
+
function applyRotationFlags(current, flags) {
|
|
111
|
+
pairFlag('on/off', flags.enable, flags.disable, '--enable', '--disable');
|
|
112
|
+
if (flags.clearConditions && flags.condition?.length)
|
|
113
|
+
throw new Error('--clear-conditions and --condition cannot be used together');
|
|
114
|
+
const out = clone(current);
|
|
115
|
+
const rotation = (out.voice_rotation_config = out.voice_rotation_config || {});
|
|
116
|
+
const trigger = (rotation.trigger = rotation.trigger || {});
|
|
117
|
+
if (flags.enable)
|
|
118
|
+
rotation.enabled = true;
|
|
119
|
+
if (flags.disable)
|
|
120
|
+
rotation.enabled = false;
|
|
121
|
+
if (flags.trigger !== undefined)
|
|
122
|
+
trigger.mode = oneOf('--trigger', flags.trigger, exports.TRIGGER_MODES);
|
|
123
|
+
if (flags.status !== undefined)
|
|
124
|
+
trigger.previous_call_status = oneOf('--status', flags.status, exports.PREVIOUS_STATUSES);
|
|
125
|
+
if (flags.onExhausted !== undefined)
|
|
126
|
+
rotation.on_exhausted = oneOf('--on-exhausted', flags.onExhausted, exports.ON_EXHAUSTED);
|
|
127
|
+
if (flags.clearConditions)
|
|
128
|
+
trigger.conditions = [];
|
|
129
|
+
if (flags.condition?.length)
|
|
130
|
+
trigger.conditions = flags.condition.map(parseCondition);
|
|
131
|
+
if ((trigger.conditions || []).length > exports.MAX_CONDITIONS)
|
|
132
|
+
throw new Error(`At most ${exports.MAX_CONDITIONS} conditions`);
|
|
133
|
+
if ((trigger.conditions || []).length && (trigger.mode || 'instant_hangup') !== 'custom') {
|
|
134
|
+
throw new Error('Conditions only apply with --trigger custom (instant_hangup ignores them)');
|
|
135
|
+
}
|
|
136
|
+
if (flags.voices !== undefined) {
|
|
137
|
+
if (!Array.isArray(flags.voices) || flags.voices.some((voice) => !voice || typeof voice !== 'object' || Array.isArray(voice))) {
|
|
138
|
+
throw new Error('--voices must be a JSON array of voice configuration objects');
|
|
139
|
+
}
|
|
140
|
+
if (flags.voices.length > exports.MAX_ROTATION_VOICES)
|
|
141
|
+
throw new Error(`At most ${exports.MAX_ROTATION_VOICES} retry voices (got ${flags.voices.length})`);
|
|
142
|
+
flags.voices.forEach((voice, index) => {
|
|
143
|
+
if (typeof voice.voice_provider !== 'string' || !voice.voice_provider.trim()) {
|
|
144
|
+
throw new Error(`voices[${index}]: voice_provider is required - copy the "configuration" object from \`cku voices --json\` unchanged`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
rotation.voices = flags.voices;
|
|
148
|
+
}
|
|
149
|
+
if (rotation.enabled && !(rotation.voices || []).length)
|
|
150
|
+
throw new Error('Rotation needs at least one retry voice - pass --voices @voices.json (a JSON array), or leave it disabled');
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function describeMemory(memory) {
|
|
154
|
+
const value = memory || {};
|
|
155
|
+
const filters = value.call_filters || {};
|
|
156
|
+
const agents = filters.additionalAgents || {};
|
|
157
|
+
const maxCalls = filters.maxCalls ?? 10;
|
|
158
|
+
return {
|
|
159
|
+
'call memory': value.enabled_for_call ? 'on' : 'off',
|
|
160
|
+
'max calls': maxCalls < 0 ? 'all (up to 200)' : String(maxCalls),
|
|
161
|
+
'max age (days)': (filters.maxDays ?? 0) > 0 ? String(filters.maxDays) : 'no limit',
|
|
162
|
+
'connected only': filters.onlyConnected === false ? 'no' : 'yes',
|
|
163
|
+
transcripts: filters.includeTranscript ? 'yes' : 'no',
|
|
164
|
+
'agents read': agents.mode === 'selected' ? `selected (${(agents.agentIds || []).length})` : agents.mode || 'self',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function describeRotation(memory) {
|
|
168
|
+
const rotation = (memory || {}).voice_rotation_config || {};
|
|
169
|
+
const trigger = rotation.trigger || {};
|
|
170
|
+
const mode = trigger.mode || 'instant_hangup';
|
|
171
|
+
return {
|
|
172
|
+
'voice rotation': rotation.enabled ? 'on' : 'off',
|
|
173
|
+
trigger: mode === 'custom' ? `custom (previous call ${trigger.previous_call_status || 'connected'}, ${(trigger.conditions || []).length} condition(s))` : 'instant_hangup',
|
|
174
|
+
'retry voices': String((rotation.voices || []).length),
|
|
175
|
+
'when exhausted': rotation.on_exhausted || 'last',
|
|
176
|
+
};
|
|
177
|
+
}
|
package/dist/lib/agentUpdate.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ENUMS = exports.STRING_ARRAY_KEYS = exports.REPLACE_WHOLESALE_KEYS = void 0;
|
|
3
|
+
exports.MAX_FIELDS_WITHOUT_YES = exports.ENUMS = exports.STRING_ARRAY_KEYS = exports.REPLACE_WHOLESALE_KEYS = void 0;
|
|
4
4
|
exports.applyLiveModels = applyLiveModels;
|
|
5
5
|
exports.validateEnums = validateEnums;
|
|
6
6
|
exports.validateCallSettings = validateCallSettings;
|
|
7
7
|
exports.validateProviderConfigs = validateProviderConfigs;
|
|
8
8
|
exports.preparePatch = preparePatch;
|
|
9
|
+
exports.jsonEqual = jsonEqual;
|
|
10
|
+
exports.planUpdate = planUpdate;
|
|
9
11
|
exports.coerce = coerce;
|
|
10
12
|
exports.deepMerge = deepMerge;
|
|
11
13
|
exports.mergeWholesaleKeys = mergeWholesaleKeys;
|
|
@@ -28,7 +30,7 @@ const catalog_1 = require("./catalog");
|
|
|
28
30
|
exports.REPLACE_WHOLESALE_KEYS = new Set([
|
|
29
31
|
'preFormatVariables', 'customPronunciations', 'transcriber', 'secondary_transcriber',
|
|
30
32
|
'voice_configuration', 'secondary_voice_configuration', 'callFlow_json',
|
|
31
|
-
'variableSource', 'callPropertyMapping',
|
|
33
|
+
'variableSource', 'callPropertyMapping', 'agentMemory',
|
|
32
34
|
]);
|
|
33
35
|
/** String-array fields the web trims + drops empties on; we match that. */
|
|
34
36
|
exports.STRING_ARRAY_KEYS = new Set([
|
|
@@ -160,8 +162,10 @@ const PROVIDER_CONFIGS = [
|
|
|
160
162
|
* Patches may be partial (a dotted `--set` touching only `voice_speed`), so each
|
|
161
163
|
* check runs only when the keys it needs are actually present.
|
|
162
164
|
*/
|
|
163
|
-
function validateProviderConfigs(obj) {
|
|
165
|
+
function validateProviderConfigs(obj, opts = {}) {
|
|
164
166
|
for (const { key, kind } of PROVIDER_CONFIGS) {
|
|
167
|
+
if (opts.skipVoices && kind === 'voice')
|
|
168
|
+
continue;
|
|
165
169
|
const cfg = obj[key];
|
|
166
170
|
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
|
|
167
171
|
continue;
|
|
@@ -199,7 +203,7 @@ function validateProviderConfigs(obj) {
|
|
|
199
203
|
* `wholesaleKeys` lists the replace-wholesale objects present in the patch, so
|
|
200
204
|
* the caller can merge them against the agent's current value before sending.
|
|
201
205
|
*/
|
|
202
|
-
function preparePatch(obj) {
|
|
206
|
+
function preparePatch(obj, opts = {}) {
|
|
203
207
|
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
204
208
|
throw new Error('--set must be a JSON object of fields, e.g. \'{"name":"x","temperature":5}\'');
|
|
205
209
|
}
|
|
@@ -207,7 +211,7 @@ function preparePatch(obj) {
|
|
|
207
211
|
validateEnums(source);
|
|
208
212
|
validateCallSettings(source);
|
|
209
213
|
const patch = { ...source };
|
|
210
|
-
validateProviderConfigs(patch);
|
|
214
|
+
validateProviderConfigs(patch, opts);
|
|
211
215
|
for (const key of Object.keys(patch)) {
|
|
212
216
|
if (exports.STRING_ARRAY_KEYS.has(key))
|
|
213
217
|
patch[key] = normalizeStringArray(patch[key]);
|
|
@@ -216,6 +220,32 @@ function preparePatch(obj) {
|
|
|
216
220
|
const wholesaleKeys = Object.keys(patch).filter((k) => exports.REPLACE_WHOLESALE_KEYS.has(k) && patch[k] && typeof patch[k] === 'object' && !Array.isArray(patch[k]));
|
|
217
221
|
return { patch, versionKeys, wholesaleKeys };
|
|
218
222
|
}
|
|
223
|
+
exports.MAX_FIELDS_WITHOUT_YES = 8;
|
|
224
|
+
const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
225
|
+
/** Structural JSON equality: object key order does not matter; array order does. */
|
|
226
|
+
function jsonEqual(a, b) {
|
|
227
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
228
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => jsonEqual(value, b[i]));
|
|
229
|
+
if (isObject(a) && isObject(b))
|
|
230
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => jsonEqual(a[key], b[key]));
|
|
231
|
+
return a === b;
|
|
232
|
+
}
|
|
233
|
+
function planUpdate(current, patch) {
|
|
234
|
+
const send = {};
|
|
235
|
+
const changed = [];
|
|
236
|
+
const unchanged = [];
|
|
237
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
238
|
+
const before = current[key];
|
|
239
|
+
const next = exports.REPLACE_WHOLESALE_KEYS.has(key) && isObject(before) && isObject(value) ? deepMerge(before, value) : value;
|
|
240
|
+
if (jsonEqual(before, next))
|
|
241
|
+
unchanged.push(key);
|
|
242
|
+
else {
|
|
243
|
+
send[key] = next;
|
|
244
|
+
changed.push(key);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { send, changed, unchanged };
|
|
248
|
+
}
|
|
219
249
|
/** JSON-first value coercion: `true`→bool, `5`→num, `[...]`→array, else the raw string. */
|
|
220
250
|
function coerce(raw) {
|
|
221
251
|
try {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkVoiceConfigs = checkVoiceConfigs;
|
|
4
|
+
exports.savedVoices = savedVoices;
|
|
5
|
+
/** Validate TTS provider/model pairs against the account's live voice catalogue. */
|
|
6
|
+
const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
7
|
+
async function checkVoiceConfigs(api, entries, saved) {
|
|
8
|
+
const todo = Object.entries(entries).filter(([, config]) => !!config && typeof config === 'object' && !Array.isArray(config) && typeof config.voice_provider === 'string');
|
|
9
|
+
if (!todo.length)
|
|
10
|
+
return;
|
|
11
|
+
let providers;
|
|
12
|
+
try {
|
|
13
|
+
providers = (await api.getVoiceCatalog()).providers || [];
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(`! Could not check voices against the live catalogue (${error.message}); the backend will still validate.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let savedPairs;
|
|
20
|
+
const alreadySaved = async (config) => {
|
|
21
|
+
if (!saved)
|
|
22
|
+
return false;
|
|
23
|
+
if (!savedPairs) {
|
|
24
|
+
try {
|
|
25
|
+
savedPairs = new Set((await saved()).filter((voice) => typeof voice?.voice_provider === 'string')
|
|
26
|
+
.map((voice) => `${slug(voice.voice_provider)}|${voice.voice_model ?? ''}`));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
savedPairs = new Set();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return savedPairs.has(`${slug(String(config.voice_provider))}|${config.voice_model ?? ''}`);
|
|
33
|
+
};
|
|
34
|
+
for (const [label, raw] of todo) {
|
|
35
|
+
const config = raw;
|
|
36
|
+
const name = String(config.voice_provider);
|
|
37
|
+
const provider = providers.find((candidate) => slug(candidate.provider) === slug(name));
|
|
38
|
+
if (!provider) {
|
|
39
|
+
if (await alreadySaved(config))
|
|
40
|
+
continue;
|
|
41
|
+
throw new Error(`Unknown voice_provider "${name}" in ${label}\n allowed (live): ${providers.map((candidate) => candidate.provider).join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
if (name !== provider.provider)
|
|
44
|
+
config.voice_provider = provider.provider;
|
|
45
|
+
const models = provider.models || [];
|
|
46
|
+
if (typeof config.voice_model === 'string' && config.voice_model && models.length && !models.includes(config.voice_model)) {
|
|
47
|
+
if (await alreadySaved(config))
|
|
48
|
+
continue;
|
|
49
|
+
throw new Error(`"${config.voice_model}" is not a ${provider.provider} voice_model (in ${label})\n allowed (live): ${models.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function savedVoices(version) {
|
|
54
|
+
return [version?.voice_configuration, version?.secondary_voice_configuration, ...(version?.agentMemory?.voice_rotation_config?.voices || [])].filter(Boolean);
|
|
55
|
+
}
|
package/dist/lib/updateCheck.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.skillsOutdated = skillsOutdated;
|
|
7
|
+
exports.recordSkillsInstalled = recordSkillsInstalled;
|
|
6
8
|
exports.isNewer = isNewer;
|
|
7
9
|
exports.notifyIfOutdated = notifyIfOutdated;
|
|
8
10
|
const os_1 = require("os");
|
|
@@ -10,9 +12,56 @@ const path_1 = require("path");
|
|
|
10
12
|
const fs_1 = require("fs");
|
|
11
13
|
const child_process_1 = require("child_process");
|
|
12
14
|
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const config_1 = require("./config");
|
|
13
16
|
const CACHE = (0, path_1.join)(process.env.CK_CONFIG_DIR || (0, path_1.join)((0, os_1.homedir)(), '.config', 'callkaro-unofficial'), 'update-check.json');
|
|
17
|
+
/** Which commit of each skill pack was installed, keyed `<repo>@<branch>`. */
|
|
18
|
+
const SKILLS_INSTALLED = (0, path_1.join)((0, path_1.dirname)(CACHE), 'skills-installed.json');
|
|
14
19
|
const DAY = 24 * 60 * 60 * 1000;
|
|
15
20
|
const REGISTRY = 'https://registry.npmjs.org/callkaro-cli-unofficial/latest';
|
|
21
|
+
// With this Accept header GitHub answers with the bare commit sha as text.
|
|
22
|
+
const SHA_HEADERS = { Accept: 'application/vnd.github.sha', 'User-Agent': 'cku' };
|
|
23
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
24
|
+
const shaUrl = (repo, branch) => `https://api.github.com/repos/${repo}/commits/${branch}`;
|
|
25
|
+
const skillsKey = () => `${(0, config_1.skillsRepo)()}@${(0, config_1.skillsBranch)()}`;
|
|
26
|
+
const readJson = (file) => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse((0, fs_1.readFileSync)(file, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* True when the skill pack recorded at install time is not the repo's current
|
|
36
|
+
* head. Without a recorded install there is nothing to compare, so no notice.
|
|
37
|
+
*/
|
|
38
|
+
function skillsOutdated(installed, latest) {
|
|
39
|
+
return Boolean(installed && latest && installed !== latest);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Record which commit of the skill pack was just installed, so a later run can
|
|
43
|
+
* tell when the repo has moved on. The tarball does not carry its sha, so this
|
|
44
|
+
* asks GitHub for the branch head right after the install. Best effort: a
|
|
45
|
+
* failure only means no notice until the next `cku skills update`.
|
|
46
|
+
*/
|
|
47
|
+
async function recordSkillsInstalled(repo, branch) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(shaUrl(repo, branch), { headers: SHA_HEADERS, signal: AbortSignal.timeout(5000) });
|
|
50
|
+
const sha = res.ok ? (await res.text()).trim() : '';
|
|
51
|
+
if (!SHA.test(sha))
|
|
52
|
+
return;
|
|
53
|
+
const key = `${repo}@${branch}`;
|
|
54
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(CACHE), { recursive: true });
|
|
55
|
+
(0, fs_1.writeFileSync)(SKILLS_INSTALLED, JSON.stringify({ ...readJson(SKILLS_INSTALLED), [key]: sha }));
|
|
56
|
+
// Keep the cached head in step, or a head cached before this install would
|
|
57
|
+
// report the pack outdated until the next daily check.
|
|
58
|
+
if (key === skillsKey())
|
|
59
|
+
(0, fs_1.writeFileSync)(CACHE, JSON.stringify({ ...readJson(CACHE), skillsSha: sha }));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* recording the install must never break it */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
16
65
|
/** True when `latest` is a higher x.y.z than `current` (pre-release tags ignored). */
|
|
17
66
|
function isNewer(latest, current) {
|
|
18
67
|
const n = (v) => v.split('-')[0].split('.').map((x) => parseInt(x, 10) || 0);
|
|
@@ -29,6 +78,7 @@ function isNewer(latest, current) {
|
|
|
29
78
|
* Reads the answer cached by an earlier run and prints it to stderr after the
|
|
30
79
|
* command's own output, so stdout / --json stay clean. The npm lookup runs in a
|
|
31
80
|
* detached child at most once a day, so it never delays or breaks a command.
|
|
81
|
+
* The same child reads the skill repo's head, for the skill-pack notice.
|
|
32
82
|
* Opt out with CK_NO_UPDATE_CHECK=1 (or when running under `node --test`).
|
|
33
83
|
*/
|
|
34
84
|
function notifyIfOutdated(current, argv) {
|
|
@@ -37,12 +87,15 @@ function notifyIfOutdated(current, argv) {
|
|
|
37
87
|
return;
|
|
38
88
|
if (argv.some((a) => ['update', '-v', '--version', '-h', '--help'].includes(a)))
|
|
39
89
|
return;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
const cache = readJson(CACHE);
|
|
91
|
+
if (cache.skillsSha) {
|
|
92
|
+
process.on('exit', () => {
|
|
93
|
+
// Read at exit, not at start: `cku skills install` moves both records during this very run.
|
|
94
|
+
if (!skillsOutdated(readJson(SKILLS_INSTALLED)[skillsKey()], readJson(CACHE).skillsSha))
|
|
95
|
+
return;
|
|
96
|
+
process.stderr.write(chalk_1.default.yellow(`\nThe CallKaro skill pack (${(0, config_1.skillsRepo)()}) has changed since it was installed.\n` +
|
|
97
|
+
`AI agents: tell the user the skills are outdated and ask if they want to refresh; if yes, run \`cku skills update\`.\n`));
|
|
98
|
+
});
|
|
46
99
|
}
|
|
47
100
|
if (cache.latest && isNewer(cache.latest, current)) {
|
|
48
101
|
process.on('exit', () => {
|
|
@@ -53,14 +106,18 @@ function notifyIfOutdated(current, argv) {
|
|
|
53
106
|
if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
|
|
54
107
|
const script = `
|
|
55
108
|
const fs = require('fs'), path = require('path');
|
|
56
|
-
const file = process.argv
|
|
109
|
+
const [, file, shaUrl] = process.argv;
|
|
57
110
|
(async () => {
|
|
58
|
-
let latest;
|
|
111
|
+
let latest, skillsSha;
|
|
59
112
|
try { latest = (await (await fetch(${JSON.stringify(REGISTRY)}, { signal: AbortSignal.timeout(5000) })).json()).version; } catch {}
|
|
113
|
+
try {
|
|
114
|
+
const sha = (await (await fetch(shaUrl, { headers: ${JSON.stringify(SHA_HEADERS)}, signal: AbortSignal.timeout(5000) })).text()).trim();
|
|
115
|
+
if (${String(SHA)}.test(sha)) skillsSha = sha;
|
|
116
|
+
} catch {}
|
|
60
117
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
61
|
-
fs.writeFileSync(file, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
118
|
+
fs.writeFileSync(file, JSON.stringify({ checkedAt: Date.now(), latest, skillsSha }));
|
|
62
119
|
})();`;
|
|
63
|
-
(0, child_process_1.spawn)(process.execPath, ['-e', script, CACHE], { detached: true, stdio: 'ignore' }).unref();
|
|
120
|
+
(0, child_process_1.spawn)(process.execPath, ['-e', script, CACHE, shaUrl((0, config_1.skillsRepo)(), (0, config_1.skillsBranch)())], { detached: true, stdio: 'ignore' }).unref();
|
|
64
121
|
}
|
|
65
122
|
}
|
|
66
123
|
catch {
|