@velaro/cli 1.4.89 → 1.4.90
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/bin/velaro.js +2 -0
- package/lib/commands/billing.js +1 -1
- package/lib/commands/chat-intelligence-topics.js +47 -0
- package/lib/commands/executive-insights.js +9 -7
- package/lib/commands/index.js +10 -2
- package/lib/commands/ingest.js +3 -2
- package/lib/commands/kb.js +2 -1
- package/lib/commands/recruiting.js +89 -1
- package/lib/commands/rule.js +4 -2
- package/lib/recruiting-apply.js +53 -0
- package/lib/recruiting-apply.test.js +28 -0
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -56,6 +56,7 @@ import { keywordIntelligenceCommand } from '../lib/commands/keyword-intelligence
|
|
|
56
56
|
import { aiFieldExtractionCommand } from '../lib/commands/ai-field-extraction.js';
|
|
57
57
|
import { sentimentCommand, qualityCommand, chatIntelligenceThemesCommand } from '../lib/commands/quality-reports.js';
|
|
58
58
|
import { cannedLearningCommand } from '../lib/commands/canned-learning.js';
|
|
59
|
+
import { chatIntelligenceTopicsCommand } from '../lib/commands/chat-intelligence-topics.js';
|
|
59
60
|
import { focusCommand } from '../lib/commands/focus.js';
|
|
60
61
|
import { triageCommand } from '../lib/commands/triage.js';
|
|
61
62
|
import { automationDraftCommand } from '../lib/commands/automation-draft.js';
|
|
@@ -187,6 +188,7 @@ await yargs(hideBin(process.argv))
|
|
|
187
188
|
.command(qualityCommand)
|
|
188
189
|
.command(chatIntelligenceThemesCommand)
|
|
189
190
|
.command(cannedLearningCommand)
|
|
191
|
+
.command(chatIntelligenceTopicsCommand)
|
|
190
192
|
.command(focusCommand)
|
|
191
193
|
.command(triageCommand)
|
|
192
194
|
.command(automationDraftCommand)
|
package/lib/commands/billing.js
CHANGED
|
@@ -13,7 +13,7 @@ function fmtDate(iso) {
|
|
|
13
13
|
// ── plan-status ───────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
async function planStatus() {
|
|
16
|
-
const s = await request('GET', '/BillingPayment/plan-status');
|
|
16
|
+
const s = await request('GET', '/api/BillingPayment/plan-status');
|
|
17
17
|
|
|
18
18
|
console.log(`\nPlan Status — Site ${s.siteId}\n${'─'.repeat(50)}`);
|
|
19
19
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
const print = (data) => console.log(JSON.stringify(data, null, 2));
|
|
5
|
+
const list = (v) => (v == null ? [] : String(v).split(',').map((s) => s.trim()).filter(Boolean));
|
|
6
|
+
|
|
7
|
+
// velaro chat-topics <show|set|template|merge|rename|retire|group|group-suggestions|suggestions|accept|dismiss|
|
|
8
|
+
// reclassify-estimate|reclassify|analysis-estimate|analysis|proposal>
|
|
9
|
+
// REST: messaging /ChatIntelligenceTopics/* (site comes from the CLI session, never an argument)
|
|
10
|
+
export const chatIntelligenceTopicsCommand = {
|
|
11
|
+
command: 'chat-topics <action>',
|
|
12
|
+
describe: 'Chat Intelligence topics: edit the list, merge/rename/group (no AI cost), suggested topics, optional reclassify and analysis',
|
|
13
|
+
builder: (y) => y
|
|
14
|
+
.positional('action', { choices: ['show', 'set', 'template', 'merge', 'rename', 'retire', 'group', 'group-suggestions', 'suggestions', 'accept', 'dismiss', 'reclassify-estimate', 'reclassify', 'analysis-estimate', 'analysis', 'proposal'] })
|
|
15
|
+
.option('topics', { type: 'string', describe: 'set: comma-separated topic list' })
|
|
16
|
+
.option('industry', { type: 'string', describe: 'template: insurance|healthcare|retail|ecommerce|financial|saas|government|manufacturing' })
|
|
17
|
+
.option('from', { type: 'string', describe: 'merge/rename/retire/group: comma-separated topics' })
|
|
18
|
+
.option('to', { type: 'string', describe: 'merge/rename: target topic' })
|
|
19
|
+
.option('group', { type: 'string', describe: 'group: group name (empty clears)' })
|
|
20
|
+
.option('suggestion', { type: 'string', describe: 'accept/dismiss/merge a suggested topic' })
|
|
21
|
+
.option('scope', { type: 'string', default: 'other', describe: 'reclassify: other (default) or all' })
|
|
22
|
+
.option('days', { type: 'number', default: 90 })
|
|
23
|
+
.option('max', { type: 'number', describe: 'reclassify: cap (server hard cap 500)' })
|
|
24
|
+
.option('confirm', { type: 'boolean', default: false, describe: 'reclassify/analysis: required after reading the estimate' })
|
|
25
|
+
.option('id', { type: 'string', describe: 'proposal: proposal id' })
|
|
26
|
+
.option('accept', { type: 'boolean', default: true, describe: 'proposal: --no-accept rejects' }),
|
|
27
|
+
handler: runCommand(async (a) => {
|
|
28
|
+
const base = '/ChatIntelligenceTopics';
|
|
29
|
+
const ops = (op) => messagingPost(`${base}/mapping`, { ops: [{ op, from: list(a.from), to: a.to ?? null, group: a.group ?? null }] });
|
|
30
|
+
switch (a.action) {
|
|
31
|
+
case 'show': return print(await messagingGet(base));
|
|
32
|
+
case 'set': return print(await messagingPut(base, { topics: list(a.topics) }));
|
|
33
|
+
case 'template': return print(await messagingPost(`${base}/template`, { industry: a.industry }));
|
|
34
|
+
case 'merge': case 'rename': case 'retire': case 'group': return print(await ops(a.action));
|
|
35
|
+
case 'group-suggestions': return print(await messagingGet(`${base}/group-suggestions?days=${a.days}`));
|
|
36
|
+
case 'suggestions': return print(await messagingGet(`${base}/suggestions?days=${a.days}`));
|
|
37
|
+
case 'accept': return print(await messagingPost(`${base}/suggestions/resolve`, { suggestion: a.suggestion, action: a.to ? 'merge' : 'accept', target: a.to ?? null }));
|
|
38
|
+
case 'dismiss': return print(await messagingPost(`${base}/suggestions/resolve`, { suggestion: a.suggestion, action: 'dismiss' }));
|
|
39
|
+
case 'reclassify-estimate': return print(await messagingGet(`${base}/reclassify/estimate?scope=${a.scope}&days=${a.days}${a.max ? `&max=${a.max}` : ''}`));
|
|
40
|
+
case 'reclassify': return print(await messagingPost(`${base}/reclassify`, { scope: a.scope, days: a.days, max: a.max ?? null, confirm: a.confirm }));
|
|
41
|
+
case 'analysis-estimate': return print(await messagingGet(`${base}/analysis/estimate`));
|
|
42
|
+
case 'analysis': return print(a.confirm ? await messagingPost(`${base}/analysis`, { confirm: true }) : await messagingGet(`${base}/analysis`));
|
|
43
|
+
case 'proposal': return print(await messagingPost(`${base}/analysis/proposals`, { id: a.id, accept: a.accept }));
|
|
44
|
+
default: throw new Error(`Unknown action ${a.action}`);
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { messagingGet, messagingPost } from '../api.js';
|
|
2
|
+
import { messagingGet, messagingPost, messagingRequestText } from '../api.js';
|
|
3
3
|
import { runCommand } from '../run.js';
|
|
4
4
|
|
|
5
5
|
// -- velaro executive-insights ---------------------------------------------------
|
|
@@ -27,7 +27,7 @@ const frictionCommand = {
|
|
|
27
27
|
handler: runCommand(async (argv) => {
|
|
28
28
|
const qs = rangeArgs(argv);
|
|
29
29
|
qs.set('limit', String(argv.limit ?? 20));
|
|
30
|
-
const r = await messagingGet(`/executive-insights/friction-radar?${qs}`);
|
|
30
|
+
const r = await messagingGet(`/api/executive-insights/friction-radar?${qs}`);
|
|
31
31
|
console.log(`Friction radar: ${r.conversationsScanned} conversations scanned${r.truncated ? ' (truncated)' : ''}`);
|
|
32
32
|
for (const t of r.topics ?? []) {
|
|
33
33
|
const growth = t.isNew ? 'new' : t.growthPct == null ? 'n/a' : `${t.growthPct}%`;
|
|
@@ -41,7 +41,7 @@ const briefCommand = {
|
|
|
41
41
|
describe: 'Executive brief: volume, sentiment, QA, Experience Impact, rising keywords, top friction',
|
|
42
42
|
builder: rangeOptions,
|
|
43
43
|
handler: runCommand(async (argv) => {
|
|
44
|
-
const r = await messagingGet(`/executive-insights/brief?${rangeArgs(argv)}`);
|
|
44
|
+
const r = await messagingGet(`/api/executive-insights/brief?${rangeArgs(argv)}`);
|
|
45
45
|
console.log(JSON.stringify(r.brief, null, 2));
|
|
46
46
|
}),
|
|
47
47
|
};
|
|
@@ -51,12 +51,14 @@ const exportCommand = {
|
|
|
51
51
|
describe: 'CSV of conversations with sentiment, QA score, tag, escalations, handle time and keywords',
|
|
52
52
|
builder: (y) => rangeOptions(y).option('out', { type: 'string', describe: 'Write the CSV to this file (default: stdout)' }),
|
|
53
53
|
handler: runCommand(async (argv) => {
|
|
54
|
-
|
|
54
|
+
// The endpoint is GET api/executive-insights/export.csv and answers text/csv, not JSON.
|
|
55
|
+
const csv = await messagingRequestText('GET', `/api/executive-insights/export.csv?${rangeArgs(argv)}`);
|
|
55
56
|
if (argv.out) {
|
|
56
|
-
writeFileSync(argv.out,
|
|
57
|
-
|
|
57
|
+
writeFileSync(argv.out, csv, 'utf8');
|
|
58
|
+
const rows = Math.max(csv.split('\n').filter((l) => l.trim().length > 0).length - 1, 0);
|
|
59
|
+
console.log(`Wrote ${rows} rows to ${argv.out}`);
|
|
58
60
|
} else {
|
|
59
|
-
process.stdout.write(
|
|
61
|
+
process.stdout.write(csv);
|
|
60
62
|
}
|
|
61
63
|
}),
|
|
62
64
|
};
|
package/lib/commands/index.js
CHANGED
|
@@ -74,7 +74,12 @@ const listCommand = {
|
|
|
74
74
|
]);
|
|
75
75
|
|
|
76
76
|
// Quota from subscription
|
|
77
|
-
|
|
77
|
+
// GET /Subscription lives on the MESSAGING api (the old admin /Subscription/Get never existed, and its
|
|
78
|
+
// .catch(() => null) hid that, so the quota line silently never rendered).
|
|
79
|
+
const sub = await messagingGet('/Subscription').catch((err) => {
|
|
80
|
+
console.error(`\x1b[33m⚠ Could not read subscription quota: ${err.message}\x1b[0m`);
|
|
81
|
+
return null;
|
|
82
|
+
});
|
|
78
83
|
const chunkCap = sub?.maxIndexedChunkTotal ?? 0;
|
|
79
84
|
const usedChunks = indexes.reduce((s, i) => s + (i.documentCount ?? 0), 0);
|
|
80
85
|
|
|
@@ -169,7 +174,10 @@ const reingestCommand = {
|
|
|
169
174
|
const creds = await getCredentials();
|
|
170
175
|
|
|
171
176
|
// Check quota before allowing reingest
|
|
172
|
-
const sub = await
|
|
177
|
+
const sub = await messagingGet('/Subscription').catch((err) => {
|
|
178
|
+
console.error(`\x1b[33m⚠ Could not read subscription quota (index quota pre-check skipped): ${err.message}\x1b[0m`);
|
|
179
|
+
return null;
|
|
180
|
+
});
|
|
173
181
|
if (!isVelaroAdmin(creds) && sub?.maxIndexedChunkTotal > 0) {
|
|
174
182
|
const indexes = await messagingGet('/AzureIndexes/List').catch(() => []);
|
|
175
183
|
const usedChunks = indexes.reduce((s, i) => s + (i.documentCount ?? 0), 0);
|
package/lib/commands/ingest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCredentials,
|
|
1
|
+
import { getCredentials, messagingPost } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
4
|
export const ingestCommand = {
|
|
@@ -18,7 +18,8 @@ export const ingestCommand = {
|
|
|
18
18
|
|
|
19
19
|
console.log(`Triggering ingestion for job ${argv['job-id']} on site ${creds.siteId}...`);
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// AzureIndexesController (IngestJobDirect) lives on the MESSAGING api; the admin api has no such route.
|
|
22
|
+
const result = await messagingPost('/AzureIndexes/IngestJobDirect', {
|
|
22
23
|
siteId: creds.siteId,
|
|
23
24
|
jobId: argv['job-id'],
|
|
24
25
|
});
|
package/lib/commands/kb.js
CHANGED
|
@@ -122,7 +122,8 @@ const reindexCommand = {
|
|
|
122
122
|
}
|
|
123
123
|
} else {
|
|
124
124
|
console.log("Reindexing your site's KB articles\u2026");
|
|
125
|
-
|
|
125
|
+
// KBSearchIndexController is routed api/kb/search (the old /KBSearchIndex/reindex 404'd).
|
|
126
|
+
const result = await post('/api/kb/search/reindex', {});
|
|
126
127
|
if (result.success === false) {
|
|
127
128
|
console.error(`❌ Failed: ${result.message}`);
|
|
128
129
|
} else {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from 'fs';
|
|
2
2
|
import { messagingGet, messagingPost, messagingPut, messagingDel, getCredentials } from '../api.js';
|
|
3
3
|
import { runCommand } from '../run.js';
|
|
4
|
+
import { applicationIdPath, buildApplySettingsBody, buildBotSettingsBody } from '../recruiting-apply.js';
|
|
4
5
|
|
|
5
6
|
// -- velaro recruiting --------------------------------------------------------
|
|
6
7
|
// CLI surface for the Recruiting Bot: job-feed sync (JSON/XML/RSS/Indeed/
|
|
@@ -16,6 +17,7 @@ import { runCommand } from '../run.js';
|
|
|
16
17
|
// GET Recruiting/Jobs?search=&location=&department= -> jobListing[]
|
|
17
18
|
// DELETE Recruiting/Jobs/{id} -> { success }
|
|
18
19
|
// GET Recruiting/Applications -> candidateApplication[]
|
|
20
|
+
// GET Recruiting/Applications/AtsFailures; POST Recruiting/Applications/{id}/AtsRetry; PUT Recruiting/ApplySettings { publicApplyEnabled?, autoCreateCaseTypeId? }; GET Recruiting/Applications/{id}/Resume (binary)
|
|
19
21
|
// GET Recruiting/Applications/ExportEeoc -> text/csv (Professional tier+, EnableRecruitingBotEeoc)
|
|
20
22
|
|
|
21
23
|
const BOLD = '\x1b[1m';
|
|
@@ -123,6 +125,56 @@ async function applicationsExportEeocHandler(argv) {
|
|
|
123
125
|
console.log(`\n${GRN}OK${RST} EEOC export saved to ${outPath}\n`);
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
async function atsFailuresHandler() {
|
|
129
|
+
const rows = await messagingGet('/Recruiting/Applications/AtsFailures');
|
|
130
|
+
if (!Array.isArray(rows) || !rows.length) {
|
|
131
|
+
console.log('\n No applications with pending or failed ATS write-back.\n');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log(`\n${BOLD}ATS write-back queue${RST} (${rows.length})\n`);
|
|
135
|
+
for (const r of rows) {
|
|
136
|
+
console.log(` ${DIM}#${r.id}${RST} ${r.candidateName} ${r.atsSyncStatus} ${DIM}attempts=${r.atsSyncAttempts} next=${r.atsSyncNextRetryAt || '-'} ${r.atsSyncLastError || ''}${RST}`);
|
|
137
|
+
}
|
|
138
|
+
console.log('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function atsRetryHandler(argv) {
|
|
142
|
+
const r = await messagingPost(applicationIdPath(argv.applicationId, 'AtsRetry'), {});
|
|
143
|
+
console.log(`\n${GRN}OK${RST} Application ${r.id} ATS status: ${r.atsSyncStatus} (attempts ${r.atsSyncAttempts})${r.atsSyncLastError ? ` ${RED}${r.atsSyncLastError}${RST}` : ''}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function botSettingsHandler(argv) {
|
|
147
|
+
const keys = ['pageContext', 'revealPostedDate', 'revealDescription', 'revealApplyLink', 'publicApply', 'applyMode', 'tone', 'maxResults', 'greeting'];
|
|
148
|
+
const given = Object.fromEntries(keys.filter((k) => argv[k] !== undefined).map((k) => [k, argv[k]]));
|
|
149
|
+
// No flags = show current settings.
|
|
150
|
+
const r = Object.keys(given).length ? await messagingPut('/Recruiting/BotSettings', buildBotSettingsBody(given)) : await messagingGet('/Recruiting/BotSettings');
|
|
151
|
+
console.log(`\n${GRN}OK${RST} Recruiting bot settings:\n${JSON.stringify(r, null, 2)}\n`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function applySettingsHandler(argv) {
|
|
155
|
+
const r = await messagingPut('/Recruiting/ApplySettings', buildApplySettingsBody({ publicApply: argv.publicApply, caseTypeId: argv.caseTypeId }));
|
|
156
|
+
console.log(`\n${GRN}OK${RST} Apply settings: publicApplyEnabled=${r.publicApplyEnabled}, autoCreateCaseTypeId=${r.autoCreateCaseTypeId ?? '(none)'}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Resume is a binary download; fetch raw (messagingGet JSON-parses) with the same auth/site header.
|
|
160
|
+
async function resumeDownloadHandler(argv) {
|
|
161
|
+
const creds = await getCredentials();
|
|
162
|
+
const res = await fetch(`${creds.messagingApiBase.replace(/\/+$/, '')}${applicationIdPath(argv.applicationId, 'Resume')}`, {
|
|
163
|
+
headers: {
|
|
164
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
165
|
+
...(creds.siteId ? { 'vmsg-siteId': String(creds.siteId) } : {}),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const text = await res.text().catch(() => '');
|
|
170
|
+
throw new Error(`GET Resume -> ${res.status}: ${text.slice(0, 300)}`);
|
|
171
|
+
}
|
|
172
|
+
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(res.headers.get('content-disposition') || '');
|
|
173
|
+
const outPath = argv.out || (m ? decodeURIComponent(m[1]) : `resume-${argv.applicationId}`);
|
|
174
|
+
writeFileSync(outPath, Buffer.from(await res.arrayBuffer()));
|
|
175
|
+
console.log(`\n${GRN}OK${RST} Resume saved to ${outPath}\n`);
|
|
176
|
+
}
|
|
177
|
+
|
|
126
178
|
const syncCommand = {
|
|
127
179
|
command: 'sync',
|
|
128
180
|
describe: 'Manually trigger a Recruiting Bot job-feed sync for this site',
|
|
@@ -192,7 +244,43 @@ const applicationsCommand = {
|
|
|
192
244
|
builder: (y2) => y2.option('out', { describe: 'Output CSV file path (default: eeoc-export-<timestamp>.csv)', type: 'string' }),
|
|
193
245
|
handler: runCommand(applicationsExportEeocHandler),
|
|
194
246
|
})
|
|
195
|
-
.
|
|
247
|
+
.command({ command: 'ats-failures', describe: 'List applications whose ATS write-back is Pending/Failed/Abandoned', handler: runCommand(atsFailuresHandler) })
|
|
248
|
+
.command({
|
|
249
|
+
command: 'ats-retry <applicationId>',
|
|
250
|
+
describe: 'Retry the ATS write-back for one application now (resets attempts)',
|
|
251
|
+
builder: (y2) => y2.positional('applicationId', { type: 'number' }),
|
|
252
|
+
handler: runCommand(atsRetryHandler),
|
|
253
|
+
})
|
|
254
|
+
.command({
|
|
255
|
+
command: 'resume <applicationId>',
|
|
256
|
+
describe: 'Download an applicant resume',
|
|
257
|
+
builder: (y2) => y2.positional('applicationId', { type: 'number' }).option('out', { describe: 'Output file path (default: original file name)', type: 'string' }),
|
|
258
|
+
handler: runCommand(resumeDownloadHandler),
|
|
259
|
+
})
|
|
260
|
+
.command({
|
|
261
|
+
command: 'apply-settings',
|
|
262
|
+
describe: 'Set public apply form options (apply-if-present)',
|
|
263
|
+
builder: (y2) => y2
|
|
264
|
+
.option('public-apply', { describe: 'Enable/disable the public apply endpoint for this site', type: 'boolean' })
|
|
265
|
+
.option('case-type-id', { describe: 'Case type to auto-create a case per application (0 clears)', type: 'number' }),
|
|
266
|
+
handler: runCommand(applySettingsHandler),
|
|
267
|
+
})
|
|
268
|
+
.command({
|
|
269
|
+
command: 'bot-settings',
|
|
270
|
+
describe: 'Show or set what the recruiting bot reveals and how it behaves (omit flags to show)',
|
|
271
|
+
builder: (y2) => y2
|
|
272
|
+
.option('page-context', { describe: 'Use the job from the host page (Velaro context call)', type: 'boolean' })
|
|
273
|
+
.option('reveal-posted-date', { describe: 'Let the bot share when a job was first listed', type: 'boolean' })
|
|
274
|
+
.option('reveal-description', { describe: 'Let the bot share the full job description', type: 'boolean' })
|
|
275
|
+
.option('reveal-apply-link', { describe: 'Let the bot share the listing link', type: 'boolean' })
|
|
276
|
+
.option('public-apply', { describe: 'Enable the public apply endpoint', type: 'boolean' })
|
|
277
|
+
.option('apply-mode', { describe: 'both | link | inproduct', type: 'string' })
|
|
278
|
+
.option('tone', { describe: 'friendly | professional | concise', type: 'string' })
|
|
279
|
+
.option('max-results', { describe: 'Most jobs per search (1-10)', type: 'number' })
|
|
280
|
+
.option('greeting', { describe: 'Opening greeting (500 chars max)', type: 'string' }),
|
|
281
|
+
handler: runCommand(botSettingsHandler),
|
|
282
|
+
})
|
|
283
|
+
.demandCommand(1, 'Specify an applications subcommand: list | export-eeoc | ats-failures | ats-retry | resume | apply-settings | bot-settings'),
|
|
196
284
|
handler: () => {},
|
|
197
285
|
};
|
|
198
286
|
|
package/lib/commands/rule.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { messagingGet as get, messagingPost as post,
|
|
1
|
+
import { messagingGet as get, messagingPost as post, messagingRequest } from '../api.js';
|
|
2
2
|
import { requireFeature } from '../subscription.js';
|
|
3
3
|
import { runCommand } from '../run.js';
|
|
4
4
|
|
|
@@ -79,7 +79,9 @@ const ruleDeleteCommand = {
|
|
|
79
79
|
builder: (y) => y.positional('id', { type: 'number', describe: 'Rule ID' }),
|
|
80
80
|
handler: runCommand(async (argv) => {
|
|
81
81
|
await requireFeature('enableWorkflowRules', 'Routing Rules');
|
|
82
|
-
|
|
82
|
+
// RulesController.DeleteAsync is `[HttpDelete]` taking the rule in the BODY (DELETE /Rules {id}); there is no
|
|
83
|
+
// DELETE /Rules/{id} route (it 404'd). Verified live on staging 2026-10-12: DELETE /Rules {"id":N} -> 200.
|
|
84
|
+
await messagingRequest('DELETE', '/Rules', { id: argv.id });
|
|
83
85
|
console.log(`Rule ${argv.id} deleted.`);
|
|
84
86
|
}),
|
|
85
87
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Pure helpers for the Recruiting public-apply / ATS write-back CLI commands.
|
|
2
|
+
// Backend: velaro-messaging RecruitingController (Applications/AtsFailures, Applications/{id}/AtsRetry,
|
|
3
|
+
// ApplySettings, Applications/{id}/Resume).
|
|
4
|
+
|
|
5
|
+
export function applicationIdPath(id, suffix) {
|
|
6
|
+
const n = Number(id);
|
|
7
|
+
if (!Number.isInteger(n) || n <= 0) throw new Error('applicationId must be a positive integer.');
|
|
8
|
+
return `/Recruiting/Applications/${n}/${suffix}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Apply-if-present body for PUT /Recruiting/ApplySettings. caseTypeId 0 clears the auto-case link.
|
|
12
|
+
export function buildApplySettingsBody({ publicApply, caseTypeId } = {}) {
|
|
13
|
+
const body = {};
|
|
14
|
+
if (publicApply !== undefined) body.publicApplyEnabled = Boolean(publicApply);
|
|
15
|
+
if (caseTypeId !== undefined) {
|
|
16
|
+
const n = Number(caseTypeId);
|
|
17
|
+
if (!Number.isInteger(n) || n < 0) throw new Error('caseTypeId must be an integer >= 0 (0 clears).');
|
|
18
|
+
body.autoCreateCaseTypeId = n;
|
|
19
|
+
}
|
|
20
|
+
if (!Object.keys(body).length) throw new Error('Specify at least one of publicApply, caseTypeId.');
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const APPLY_MODES = ['both', 'link', 'inproduct'];
|
|
25
|
+
const TONES = ['friendly', 'professional', 'concise'];
|
|
26
|
+
|
|
27
|
+
// Apply-if-present body for PUT /Recruiting/BotSettings (customer controls for the recruiting bot).
|
|
28
|
+
export function buildBotSettingsBody(o = {}) {
|
|
29
|
+
const body = {};
|
|
30
|
+
const bools = { pageContext: 'pageContextEnabled', revealPostedDate: 'revealPostedDate', revealDescription: 'revealDescription', revealApplyLink: 'revealApplyLink', publicApply: 'publicApplyEnabled' };
|
|
31
|
+
for (const [k, field] of Object.entries(bools)) if (o[k] !== undefined) body[field] = Boolean(o[k]);
|
|
32
|
+
if (o.applyMode !== undefined) {
|
|
33
|
+
const m = String(o.applyMode).toLowerCase();
|
|
34
|
+
if (!APPLY_MODES.includes(m)) throw new Error(`applyMode must be one of: ${APPLY_MODES.join(', ')}.`);
|
|
35
|
+
body.applyMode = m;
|
|
36
|
+
}
|
|
37
|
+
if (o.tone !== undefined) {
|
|
38
|
+
const t = String(o.tone).toLowerCase();
|
|
39
|
+
if (!TONES.includes(t)) throw new Error(`tone must be one of: ${TONES.join(', ')}.`);
|
|
40
|
+
body.tone = t;
|
|
41
|
+
}
|
|
42
|
+
if (o.maxResults !== undefined) {
|
|
43
|
+
const n = Number(o.maxResults);
|
|
44
|
+
if (!Number.isInteger(n) || n < 1 || n > 10) throw new Error('maxResults must be an integer from 1 to 10.');
|
|
45
|
+
body.maxResults = n;
|
|
46
|
+
}
|
|
47
|
+
if (o.greeting !== undefined) {
|
|
48
|
+
if (String(o.greeting).length > 500) throw new Error('greeting must be 500 characters or fewer.');
|
|
49
|
+
body.greeting = String(o.greeting);
|
|
50
|
+
}
|
|
51
|
+
if (!Object.keys(body).length) throw new Error('Specify at least one bot setting.');
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { applicationIdPath, buildApplySettingsBody, buildBotSettingsBody } from './recruiting-apply.js';
|
|
4
|
+
|
|
5
|
+
test('application paths validate id', () => {
|
|
6
|
+
assert.equal(applicationIdPath(12, 'AtsRetry'), '/Recruiting/Applications/12/AtsRetry');
|
|
7
|
+
assert.equal(applicationIdPath('5', 'Resume'), '/Recruiting/Applications/5/Resume');
|
|
8
|
+
assert.throws(() => applicationIdPath('x', 'Resume'));
|
|
9
|
+
assert.throws(() => applicationIdPath(0, 'Resume'));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('apply settings body is apply-if-present', () => {
|
|
13
|
+
assert.deepEqual(buildApplySettingsBody({ publicApply: true }), { publicApplyEnabled: true });
|
|
14
|
+
assert.deepEqual(buildApplySettingsBody({ caseTypeId: 0 }), { autoCreateCaseTypeId: 0 });
|
|
15
|
+
assert.deepEqual(buildApplySettingsBody({ publicApply: false, caseTypeId: 3 }), { publicApplyEnabled: false, autoCreateCaseTypeId: 3 });
|
|
16
|
+
assert.throws(() => buildApplySettingsBody({}));
|
|
17
|
+
assert.throws(() => buildApplySettingsBody({ caseTypeId: -1 }));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('bot settings body is apply-if-present and validated', () => {
|
|
21
|
+
assert.deepEqual(buildBotSettingsBody({ revealPostedDate: false }), { revealPostedDate: false });
|
|
22
|
+
assert.deepEqual(buildBotSettingsBody({ applyMode: 'LINK', tone: 'Concise', maxResults: 3, pageContext: true, publicApply: false }), { pageContextEnabled: true, publicApplyEnabled: false, applyMode: 'link', tone: 'concise', maxResults: 3 });
|
|
23
|
+
assert.throws(() => buildBotSettingsBody({}));
|
|
24
|
+
assert.throws(() => buildBotSettingsBody({ applyMode: 'x' }));
|
|
25
|
+
assert.throws(() => buildBotSettingsBody({ tone: 'angry' }));
|
|
26
|
+
assert.throws(() => buildBotSettingsBody({ maxResults: 11 }));
|
|
27
|
+
assert.throws(() => buildBotSettingsBody({ greeting: 'g'.repeat(501) }));
|
|
28
|
+
});
|
package/package.json
CHANGED