shakers-cli 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/add-agent.js +12 -6
- package/bin/ai-usage.js +2 -1
- package/bin/apply.js +66 -0
- package/bin/mcp.js +5 -2
- package/bin/shakers-cli.js +7 -3
- package/package.json +1 -1
- package/src/apply-client.js +164 -0
- package/src/apply-flow.js +105 -0
- package/src/command-audit.js +13 -3
- package/src/config.js +15 -0
- package/src/detected-agents.js +40 -0
- package/src/i18n.js +98 -2
- package/src/mcp-ai-usage-tool.js +1 -1
- package/src/mcp-apply-tools.js +152 -0
- package/src/mcp-onboarding-tools.js +89 -0
- package/src/signup-copy.js +127 -1
- package/src/start-add-agents.js +106 -3
- package/src/usage-run-steps.js +4 -1
package/bin/add-agent.js
CHANGED
|
@@ -11,19 +11,21 @@ const VALID_LANGS = new Set(['es', 'en']);
|
|
|
11
11
|
|
|
12
12
|
const COPY = {
|
|
13
13
|
es: {
|
|
14
|
-
help: 'add-agent — añade a tu perfil de Talento un agente de tu inventario descubierto. Uso: add-agent [--lang es|en] [--accept-disclaimer]',
|
|
14
|
+
help: 'add-agent — añade a tu perfil de Talento un agente de tu inventario descubierto, o todos los nuevos con --all. Uso: add-agent [--all [--yes]] [--lang es|en] [--accept-disclaimer]',
|
|
15
15
|
gate: '"add-agent" requiere una sesión activa.\n Falta: iniciar sesión. Ejecuta primero: shakers login',
|
|
16
16
|
missingAccept: 'Modo no interactivo: falta --accept-disclaimer.\n add-agent es un flujo interactivo (elegir agente, editar campos) y su borrado por IA envía datos.\n Reejecuta con --accept-disclaimer, aportando las respuestas por stdin.',
|
|
17
|
+
missingYes: 'Modo no interactivo: falta --yes.\n add-agent --all añade a tu perfil todos los agentes detectados que aún no tienes.\n Reejecuta con --all --yes para confirmarlo sin terminal.',
|
|
17
18
|
},
|
|
18
19
|
en: {
|
|
19
|
-
help: 'add-agent — add an agent from your discovered inventory to your Talent profile. Usage: add-agent [--lang es|en] [--accept-disclaimer]',
|
|
20
|
+
help: 'add-agent — add an agent from your discovered inventory to your Talent profile, or every new one with --all. Usage: add-agent [--all [--yes]] [--lang es|en] [--accept-disclaimer]',
|
|
20
21
|
gate: '"add-agent" requires an active session.\n Missing: sign-in. Run `shakers login` first.',
|
|
21
22
|
missingAccept: 'Non-interactive mode: --accept-disclaimer is missing.\n add-agent is an interactive flow (pick an agent, edit fields) and its AI draft sends data.\n Re-run with --accept-disclaimer, feeding the answers over stdin.',
|
|
23
|
+
missingYes: 'Non-interactive mode: --yes is missing.\n add-agent --all adds every detected agent you do not have yet to your profile.\n Re-run with --all --yes to confirm it without a terminal.',
|
|
22
24
|
},
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
function parseArgs(argv) {
|
|
26
|
-
const o = { root: null, lang: null, help: false, acceptDisclaimer: false };
|
|
28
|
+
const o = { root: null, lang: null, help: false, acceptDisclaimer: false, all: false, yes: false };
|
|
27
29
|
for (let i = 0; i < argv.length; i++) {
|
|
28
30
|
const a = argv[i];
|
|
29
31
|
if (a === '--root') o.root = argv[++i];
|
|
@@ -31,6 +33,8 @@ function parseArgs(argv) {
|
|
|
31
33
|
else if (a === '--lang') o.lang = VALID_LANGS.has(argv[++i]) ? argv[i] : null;
|
|
32
34
|
else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = VALID_LANGS.has(v) ? v : null; }
|
|
33
35
|
else if (a === '--accept-disclaimer') o.acceptDisclaimer = true;
|
|
36
|
+
else if (a === '--all') o.all = true;
|
|
37
|
+
else if (a === '--yes' || a === '-y') o.yes = true;
|
|
34
38
|
else if (a === '--help' || a === '-h') o.help = true;
|
|
35
39
|
}
|
|
36
40
|
return o;
|
|
@@ -59,21 +63,23 @@ async function run(
|
|
|
59
63
|
|
|
60
64
|
const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
|
|
61
65
|
|
|
62
|
-
if (!stdinIsTTY && !o.acceptDisclaimer) {
|
|
63
|
-
process.stderr.write(`\n ${c.missingAccept}\n\n`);
|
|
66
|
+
if (!stdinIsTTY && (o.all ? !o.yes : !o.acceptDisclaimer)) {
|
|
67
|
+
process.stderr.write(`\n ${o.all ? c.missingYes : c.missingAccept}\n\n`);
|
|
64
68
|
process.exitCode = 1;
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const ask = injectedAsk || createStdinAsk();
|
|
69
73
|
try {
|
|
70
|
-
await runAddAgents({
|
|
74
|
+
return await runAddAgents({
|
|
71
75
|
ask,
|
|
72
76
|
catalog,
|
|
73
77
|
root: o.root || process.cwd(),
|
|
74
78
|
session,
|
|
75
79
|
stdinIsTTY,
|
|
76
80
|
preAccepted: o.acceptDisclaimer,
|
|
81
|
+
all: o.all,
|
|
82
|
+
yes: o.yes,
|
|
77
83
|
deps: { runLogin },
|
|
78
84
|
});
|
|
79
85
|
} finally {
|
package/bin/ai-usage.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
resolveAiConsent,
|
|
18
18
|
renderAndFinalize,
|
|
19
19
|
} = require('../src/usage-run-steps');
|
|
20
|
+
const { run: runLogin } = require('./login');
|
|
20
21
|
|
|
21
22
|
// Shared by the dispatcher and register flow: an injected `ask` is caller-owned, `bypassThrottle` is register-only.
|
|
22
23
|
async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypassThrottle = false, inRegistration = false } = {}) {
|
|
@@ -59,7 +60,7 @@ async function run(argv = process.argv.slice(2), { ask: injectedAsk = null, bypa
|
|
|
59
60
|
},
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle, repos, injectedAsk: ask });
|
|
63
|
+
await renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle, repos, injectedAsk: ask, offerAgents: inRegistration ? null : { runLogin } });
|
|
63
64
|
} finally {
|
|
64
65
|
if (!injectedAsk) ask.close();
|
|
65
66
|
}
|
package/bin/apply.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { detectReportLang, getCatalog } = require('../src/i18n');
|
|
5
|
+
const { loadAuthSession, sessionStatus } = require('../src/auth-session-store');
|
|
6
|
+
const { createStdinAsk } = require('../src/stdin-ask');
|
|
7
|
+
const { makeIo } = require('./register');
|
|
8
|
+
const { runApply, makeApplyDeps } = require('../src/apply-flow');
|
|
9
|
+
|
|
10
|
+
function parseApplyArgs(argv = []) {
|
|
11
|
+
const o = { lang: null, help: false, id: null };
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a === '--help' || a === '-h') o.help = true;
|
|
15
|
+
else if (a === '--lang') { const v = argv[++i]; o.lang = v === 'es' || v === 'en' ? v : null; }
|
|
16
|
+
else if (a.startsWith('--lang=')) { const v = a.slice(7); o.lang = v === 'es' || v === 'en' ? v : null; }
|
|
17
|
+
else if (a === '--id') o.id = argv[++i] != null ? argv[i] : null;
|
|
18
|
+
else if (a.startsWith('--id=')) o.id = a.slice('--id='.length);
|
|
19
|
+
else if (!a.startsWith('-') && o.id == null) o.id = a;
|
|
20
|
+
}
|
|
21
|
+
return o;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function run(
|
|
25
|
+
argv = process.argv.slice(2),
|
|
26
|
+
{ ask: injectedAsk = null, stdinIsTTY: ttyOverride = undefined, deps = null, session: injectedSession = null, out = null, input, output } = {},
|
|
27
|
+
) {
|
|
28
|
+
const opts = parseApplyArgs(argv);
|
|
29
|
+
const lang = opts.lang || detectReportLang();
|
|
30
|
+
const catalog = getCatalog(lang);
|
|
31
|
+
const fp = catalog.apply;
|
|
32
|
+
const write = out || ((s) => process.stdout.write(s));
|
|
33
|
+
|
|
34
|
+
if (opts.help) {
|
|
35
|
+
write(fp.help + '\n');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const session = injectedSession || loadAuthSession();
|
|
40
|
+
if (sessionStatus(session) !== 'active') {
|
|
41
|
+
process.stderr.write(`\n ${fp.loginRequired}\n\n`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stdinIsTTY = ttyOverride !== undefined ? ttyOverride : !!process.stdin.isTTY;
|
|
47
|
+
const ask = injectedAsk || createStdinAsk();
|
|
48
|
+
try {
|
|
49
|
+
const io = makeIo({ ask, lang, out: write, stdinIsTTY });
|
|
50
|
+
const result = await runApply({
|
|
51
|
+
io, ask, session, catalog,
|
|
52
|
+
opts: { id: opts.id, stdinIsTTY, input, output },
|
|
53
|
+
deps: deps || makeApplyDeps(),
|
|
54
|
+
});
|
|
55
|
+
if (result && result.ok === false) process.exitCode = 1;
|
|
56
|
+
return result;
|
|
57
|
+
} finally {
|
|
58
|
+
if (!injectedAsk) ask.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { run, parseApplyArgs };
|
|
63
|
+
|
|
64
|
+
if (require.main === module) {
|
|
65
|
+
run();
|
|
66
|
+
}
|
package/bin/mcp.js
CHANGED
|
@@ -8,6 +8,7 @@ const { makeCertifyDimensionTools } = require('../src/mcp-certify-dimension-tool
|
|
|
8
8
|
const { makeFindProjectsTools } = require('../src/mcp-find-projects-tools');
|
|
9
9
|
const { makeShowProjectTools } = require('../src/mcp-show-project-tools');
|
|
10
10
|
const { makeSavedPositionsTools } = require('../src/mcp-saved-positions-tools');
|
|
11
|
+
const { makeApplyTools } = require('../src/mcp-apply-tools');
|
|
11
12
|
const { makeInvitationsTools } = require('../src/mcp-invitations-tools');
|
|
12
13
|
const { makeApplicationsTools } = require('../src/mcp-applications-tools');
|
|
13
14
|
const { makeAvailabilityTools } = require('../src/mcp-availability-tools');
|
|
@@ -52,12 +53,14 @@ const HELP = `\n shakers mcp — local MCP server (stdio)\n\n`
|
|
|
52
53
|
+ ` MCP client, not run by hand.\n`
|
|
53
54
|
+ ` Tools: signup (the whole sign-up, step by step), ai_usage, ai_usage_upload,\n`
|
|
54
55
|
+ ` ai_usage_result, logout, social_signin_start, social_signin_poll,\n`
|
|
55
|
-
+ ` list_addable, add_skill, add_agent,
|
|
56
|
+
+ ` list_addable, add_skill, add_agent, add_detected_agents,\n`
|
|
57
|
+
+ ` add_project, open_web,\n`
|
|
56
58
|
+ ` onboarding_interview_start, onboarding_interview_turn,\n`
|
|
57
59
|
+ ` onboarding_interview_complete, repeat_onboarding_interview,\n`
|
|
58
60
|
+ ` onboarding_finish, list_available_roles, add_role, list_my_roles,\n`
|
|
59
61
|
+ ` set_main_role, report, share, list_certifiable_dimensions,\n`
|
|
60
62
|
+ ` find_projects, show_project, save_project, unsave_project,\n`
|
|
63
|
+
+ ` prepare_application, apply_to_project,\n`
|
|
61
64
|
+ ` list_invitations, list_applications, get_availability,\n`
|
|
62
65
|
+ ` list_certifications, get_profile, get_rate, get_languages,\n`
|
|
63
66
|
+ ` list_socials, list_experiences, list_portfolios, alma_chat, alma_decision.\n\n`;
|
|
@@ -71,7 +74,7 @@ const LOCAL_ONLY_TOOL_NAMES = new Set(['ai_usage', 'ai_usage_upload', 'ai_usage_
|
|
|
71
74
|
function buildServer({ localOnly = false, funnel = createFunnel({ surface: 'mcp' }), makeInterviewClient = null } = {}) {
|
|
72
75
|
const lang = detectFlowLang();
|
|
73
76
|
const aiUsage = makeAiUsageTools();
|
|
74
|
-
const allTools = [aiUsage.usage, aiUsage.upload, makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools({ lang, funnel }), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel, flowDeps: { makeInterviewClient } }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
|
|
77
|
+
const allTools = [aiUsage.usage, aiUsage.upload, makeAiUsageResultTool(), ...makeOnboardingTools(), ...makeRoleTools({ lang, funnel }), ...makeDeviceLoginTools({ lang, funnel }), ...makeSignupTools({ lang, funnel }), ...makeRegisterTools({ lang, funnel, flowDeps: { makeInterviewClient } }), ...makeReportTools(), ...makeCertifyDimensionTools(), ...makeFindProjectsTools(), ...makeShowProjectTools(), ...makeSavedPositionsTools(), ...makeApplyTools(), ...makeInvitationsTools(), ...makeApplicationsTools(), ...makeAvailabilityTools(), ...makeCertificationsTools(), ...makeProfileTools(), ...makeRateTools(), ...makeLangTools(), ...makeSocialsTools(), ...makePortfolioTools(), ...makeAlmaTools()];
|
|
75
78
|
// The sign-up runs through one tool (`signup`), so Claude Desktop asks the talent once instead of once per step.
|
|
76
79
|
const tools = localOnly
|
|
77
80
|
? allTools.filter((t) => LOCAL_ONLY_TOOL_NAMES.has(t.name))
|
package/bin/shakers-cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const { run: runChangeRole } = require('./change-role');
|
|
|
21
21
|
const { run: runFindProjects } = require('./find-projects');
|
|
22
22
|
const { run: runShowProject } = require('./show-project');
|
|
23
23
|
const { run: runSave, runUnsave } = require('./save');
|
|
24
|
+
const { run: runApply } = require('./apply');
|
|
24
25
|
const { run: runInvitations } = require('./invitations');
|
|
25
26
|
const { run: runApplications } = require('./applications');
|
|
26
27
|
const { run: runAvailability } = require('./availability');
|
|
@@ -74,6 +75,7 @@ const HANDLERS = {
|
|
|
74
75
|
'show-project': runShowProject,
|
|
75
76
|
'save-project': runSave,
|
|
76
77
|
'unsave-project': runUnsave,
|
|
78
|
+
apply: runApply,
|
|
77
79
|
invitations: runInvitations,
|
|
78
80
|
applications: runApplications,
|
|
79
81
|
availability: runAvailability,
|
|
@@ -111,7 +113,7 @@ const FAMILIES = {
|
|
|
111
113
|
certifications: 'certifications',
|
|
112
114
|
},
|
|
113
115
|
projects: {
|
|
114
|
-
find: 'find-projects', show: 'show-project', save: 'save-project', unsave: 'unsave-project',
|
|
116
|
+
find: 'find-projects', show: 'show-project', save: 'save-project', unsave: 'unsave-project', apply: 'apply',
|
|
115
117
|
invitations: 'invitations', applications: 'applications',
|
|
116
118
|
},
|
|
117
119
|
usage: { run: 'ai-usage', report: 'report', share: 'share' },
|
|
@@ -193,6 +195,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
193
195
|
+ ` find-projects Lista los proyectos/posiciones disponibles para ti\n`
|
|
194
196
|
+ ` show-project Detalle de un proyecto/posición por id\n`
|
|
195
197
|
+ ` save-project/unsave-project Guarda o quita un proyecto de tus guardados\n`
|
|
198
|
+
+ ` apply Aplica a un proyecto/posición con tu mensaje y respuestas\n`
|
|
196
199
|
+ ` invitations Tus invitaciones a proyectos\n`
|
|
197
200
|
+ ` applications Proyectos a los que has aplicado + estado\n`
|
|
198
201
|
+ ` availability Ver o fijar tu disponibilidad\n`
|
|
@@ -214,7 +217,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
214
217
|
+ ` Familias (agrupan los comandos de arriba; los nombres planos siguen valiendo como alias):\n`
|
|
215
218
|
+ ` add <skill|agent|project|role> · role <add|change>\n`
|
|
216
219
|
+ ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
|
|
217
|
-
+ ` projects <find|show|save|unsave|invitations|applications>\n`
|
|
220
|
+
+ ` projects <find|show|save|unsave|apply|invitations|applications>\n`
|
|
218
221
|
+ ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
|
|
219
222
|
+ ` Ejecuta \`shakers <familia>\` (sin subcomando) para ver sus subcomandos.\n\n`
|
|
220
223
|
+ ` Ejecuta shakers <comando> --help para las opciones de cada comando.\n\n`;
|
|
@@ -238,6 +241,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
238
241
|
+ ` find-projects List the Projects/Positions available to you\n`
|
|
239
242
|
+ ` show-project Detail of a project/position by id\n`
|
|
240
243
|
+ ` save-project/unsave-project Save or remove a project from your saved list\n`
|
|
244
|
+
+ ` apply Apply to a project/position with your message and answers\n`
|
|
241
245
|
+ ` invitations Your project invitations\n`
|
|
242
246
|
+ ` applications Projects you applied to + status\n`
|
|
243
247
|
+ ` availability View or set your availability\n`
|
|
@@ -259,7 +263,7 @@ function usageText(lang, { hideOnboarding = false } = {}) {
|
|
|
259
263
|
+ ` Families (group the commands above; the flat names still work as aliases):\n`
|
|
260
264
|
+ ` add <skill|agent|project|role> · role <add|change>\n`
|
|
261
265
|
+ ` profile <show|rate|languages|socials|experiences|portfolios|availability|certifications>\n`
|
|
262
|
-
+ ` projects <find|show|save|unsave|invitations|applications>\n`
|
|
266
|
+
+ ` projects <find|show|save|unsave|apply|invitations|applications>\n`
|
|
263
267
|
+ ` usage <run|report|share> · certs <list|certify> · account <login|logout|register|config|update>\n`
|
|
264
268
|
+ ` Run \`shakers <family>\` (no subcommand) to list its subcommands.\n\n`
|
|
265
269
|
+ ` Run \`shakers <command> --help\` for a command's options.\n\n`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shakers-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Shakers AI usage & skills evaluator: a local-first CLI (`shakers` binary) plus a local MCP server. Commands: ai-usage (scan + classify your AI-tool setup, T0-T7), certify (certify Skills from your code), share (branded LinkedIn card).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
4
|
+
const { reasonForError } = require('./onboarding-client');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
7
|
+
const APPLICATION_SOURCE = 'NO_SOURCE';
|
|
8
|
+
const SELECTED_MESSAGE = 'manual';
|
|
9
|
+
|
|
10
|
+
const CANDIDATURE_CODE_TO_REASON = {
|
|
11
|
+
'candidatures.already_exists': 'already-applied',
|
|
12
|
+
'candidatures.position_not_available_for_application': 'position-unavailable',
|
|
13
|
+
'candidatures.position_not_found': 'not-found',
|
|
14
|
+
'candidatures.project_not_found': 'not-found',
|
|
15
|
+
'candidatures.talent_not_found': 'talent-not-found',
|
|
16
|
+
'candidatures.creation_failed': 'creation-failed',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseJson(raw) {
|
|
20
|
+
try {
|
|
21
|
+
return raw ? JSON.parse(raw) : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bearerHeaders(accessToken) {
|
|
28
|
+
const headers = {};
|
|
29
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
34
|
+
|
|
35
|
+
function normalizeQuestion(q) {
|
|
36
|
+
if (!q || !text(q.text)) return null;
|
|
37
|
+
if (q.type === 'MULTIPLE_CHOICE') {
|
|
38
|
+
const options = Array.isArray(q.options) ? q.options.map(text).filter(Boolean) : [];
|
|
39
|
+
return options.length ? { type: 'MULTIPLE_CHOICE', text: q.text.trim(), options } : null;
|
|
40
|
+
}
|
|
41
|
+
return { type: 'OPEN', text: q.text.trim() };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function requestPositionApplication({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
45
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
46
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
47
|
+
let res;
|
|
48
|
+
try {
|
|
49
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
52
|
+
}
|
|
53
|
+
if (res.status === 404) return { ok: false, reason: 'not-found' };
|
|
54
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
55
|
+
const json = parseJson(res.raw);
|
|
56
|
+
const data = json && json.status === 'OK' && json.data && typeof json.data === 'object' ? json.data : null;
|
|
57
|
+
if (!data) return { ok: false, reason: 'bad-response' };
|
|
58
|
+
if (data.redirectUrl || !data.position) return { ok: true, open: false };
|
|
59
|
+
const p = data.position;
|
|
60
|
+
const questions = p.questions && typeof p.questions === 'object' ? p.questions : {};
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
open: true,
|
|
64
|
+
title: text(p.title),
|
|
65
|
+
doubleCheck: text(p.doubleCheck),
|
|
66
|
+
mainMotivationQuestion: text(questions.mainMotivationQuestion),
|
|
67
|
+
questions: (Array.isArray(questions.extraQuestions) ? questions.extraQuestions : []).map(normalizeQuestion).filter(Boolean),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapeHtml(s) {
|
|
72
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toHubMessage(message) {
|
|
76
|
+
return escapeHtml(message.trim()).replace(/\r\n?/g, '\n').replace(/\n/g, '<br>');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildCandidatureBody({ positionId, message, answers = [], mainMotivationQuestion }) {
|
|
80
|
+
const chat = text(message) ? toHubMessage(message) : null;
|
|
81
|
+
const extra = answers.filter((a) => a && text(a.question) && (Array.isArray(a.answer) ? a.answer.length > 0 : text(a.answer)));
|
|
82
|
+
const questions = {};
|
|
83
|
+
if (chat) questions.main_motivation = { question: mainMotivationQuestion, answer: chat };
|
|
84
|
+
if (extra.length) questions.extra_questions = extra.map((a) => ({ question: a.question, answer: Array.isArray(a.answer) ? a.answer : a.answer.trim() }));
|
|
85
|
+
const form = { selectedMessage: SELECTED_MESSAGE };
|
|
86
|
+
if (Object.keys(questions).length) form.questions = questions;
|
|
87
|
+
return {
|
|
88
|
+
positionId,
|
|
89
|
+
applicationSource: APPLICATION_SOURCE,
|
|
90
|
+
...(chat ? { sendPresentationChatMessage: chat } : {}),
|
|
91
|
+
form,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function requestCreateCandidature({ hubAccessToken } = {}, { endpoint, body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
96
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
97
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, bearerHeaders(hubAccessToken));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
103
|
+
}
|
|
104
|
+
const json = parseJson(res.raw);
|
|
105
|
+
if (res.status < 200 || res.status >= 300) {
|
|
106
|
+
const code = json && typeof json.code === 'string' ? json.code : null;
|
|
107
|
+
const reason = (code && CANDIDATURE_CODE_TO_REASON[code]) || reasonForError(res.status, res.raw);
|
|
108
|
+
const meta = json && json.meta && typeof json.meta === 'object' ? json.meta : {};
|
|
109
|
+
return reason === 'already-applied'
|
|
110
|
+
? { ok: false, reason, applicationStatus: text(meta.applicationStatus) }
|
|
111
|
+
: { ok: false, reason };
|
|
112
|
+
}
|
|
113
|
+
const data = json && json.status === 'OK' && json.data && typeof json.data === 'object' ? json.data : null;
|
|
114
|
+
if (!data) return { ok: false, reason: 'bad-response' };
|
|
115
|
+
return { ok: true, candidatureId: data.candidatureId || null, status: text(data.status), chatId: data.chatId || null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function loadApplication(deps = {}, { hubAccessToken, positionId, timeoutMs } = {}) {
|
|
119
|
+
const {
|
|
120
|
+
getPositionApplicationEndpoint = require('./config').getPositionApplicationEndpoint,
|
|
121
|
+
fetchPositionDetail = (opts) => require('./show-project-client').fetchPositionDetail({}, opts),
|
|
122
|
+
requestPositionApplication: request = requestPositionApplication,
|
|
123
|
+
} = deps;
|
|
124
|
+
const endpoint = getPositionApplicationEndpoint(positionId);
|
|
125
|
+
if (!endpoint) return { ok: false, reason: positionId ? 'no-endpoint' : 'no-id' };
|
|
126
|
+
const [application, detail] = await Promise.all([
|
|
127
|
+
request({ hubAccessToken }, { endpoint, timeoutMs }),
|
|
128
|
+
fetchPositionDetail({ hubAccessToken, positionId }).catch(() => null),
|
|
129
|
+
]);
|
|
130
|
+
if (!application.ok || !application.open) return application;
|
|
131
|
+
const company = detail && detail.ok && detail.position && detail.position.company ? text(detail.position.company.name) : null;
|
|
132
|
+
return { ...application, company };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function submitApplication(deps = {}, { hubAccessToken, positionId, message, answers, mainMotivationQuestion, timeoutMs } = {}) {
|
|
136
|
+
const {
|
|
137
|
+
getCandidaturesEndpoint = require('./config').getCandidaturesEndpoint,
|
|
138
|
+
requestCreateCandidature: request = requestCreateCandidature,
|
|
139
|
+
} = deps;
|
|
140
|
+
const body = buildCandidatureBody({ positionId, message, answers, mainMotivationQuestion });
|
|
141
|
+
return request({ hubAccessToken }, { endpoint: getCandidaturesEndpoint(), body, timeoutMs });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function applicationSummary(t, { title, company, doubleCheck }, { message, answers = [] }) {
|
|
145
|
+
return [
|
|
146
|
+
t.summaryTitle,
|
|
147
|
+
t.titleLine(title || t.untitled),
|
|
148
|
+
company ? t.companyLine(company) : null,
|
|
149
|
+
t.messageLine(text(message) || t.empty),
|
|
150
|
+
...answers.map((a) => t.answerLine(a.question, Array.isArray(a.answer) ? a.answer.join(', ') : a.answer)),
|
|
151
|
+
t.sendNotice(title || t.untitled),
|
|
152
|
+
doubleCheck && doubleCheck !== 'NO' ? t.reviewNotice : null,
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
APPLICATION_SOURCE,
|
|
158
|
+
requestPositionApplication,
|
|
159
|
+
requestCreateCandidature,
|
|
160
|
+
buildCandidatureBody,
|
|
161
|
+
loadApplication,
|
|
162
|
+
submitApplication,
|
|
163
|
+
applicationSummary,
|
|
164
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { styleLabelPrefix } = require('./ansi');
|
|
4
|
+
const { sanitizeRenderText: clean } = require('./sanitize-network-text');
|
|
5
|
+
const { applicationSummary } = require('./apply-client');
|
|
6
|
+
|
|
7
|
+
function makeApplyDeps(overrides = {}) {
|
|
8
|
+
const client = require('./apply-client');
|
|
9
|
+
return {
|
|
10
|
+
loadApplication: (opts) => client.loadApplication({}, opts),
|
|
11
|
+
submitApplication: (opts) => client.submitApplication({}, opts),
|
|
12
|
+
resolveRef: (raw) => require('./find-projects-cache').resolvePositionRef(raw),
|
|
13
|
+
promptSelect: (opts) => require('./prompt-select').promptSelect(opts),
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function askAnswers({ io, ask, fp, questions, opts, deps }) {
|
|
19
|
+
const answers = [];
|
|
20
|
+
for (const q of questions) {
|
|
21
|
+
if (q.type === 'MULTIPLE_CHOICE') {
|
|
22
|
+
const picked = await deps.promptSelect({
|
|
23
|
+
ask,
|
|
24
|
+
stdinIsTTY: true,
|
|
25
|
+
out: (s) => io.notify(s),
|
|
26
|
+
header: clean(q.text),
|
|
27
|
+
hint: fp.choiceHint,
|
|
28
|
+
items: [...q.options.map((value) => ({ label: clean(value), value })), { label: fp.skipChoice, value: null }],
|
|
29
|
+
labelFor: (x) => x.label,
|
|
30
|
+
input: opts.input,
|
|
31
|
+
output: opts.output,
|
|
32
|
+
});
|
|
33
|
+
if (picked && picked.value) answers.push({ question: q.text, answer: [picked.value] });
|
|
34
|
+
} else {
|
|
35
|
+
const answer = String(await io.ask(fp.askOpen(clean(q.text)))).trim();
|
|
36
|
+
if (answer) answers.push({ question: q.text, answer });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return answers;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function paintedSummary(fp, app, message, answers) {
|
|
43
|
+
return applicationSummary(fp, {
|
|
44
|
+
title: app.title && clean(app.title),
|
|
45
|
+
company: app.company && clean(app.company),
|
|
46
|
+
doubleCheck: app.doubleCheck,
|
|
47
|
+
}, {
|
|
48
|
+
message,
|
|
49
|
+
answers: answers.map((a) => ({ question: clean(a.question), answer: Array.isArray(a.answer) ? a.answer.map(clean) : a.answer })),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runApply({ io, ask, session, catalog, opts = {}, deps = makeApplyDeps() }) {
|
|
54
|
+
const fp = catalog.apply;
|
|
55
|
+
if (!opts.stdinIsTTY) {
|
|
56
|
+
io.error(fp.needsInteractive);
|
|
57
|
+
return { ok: false, reason: 'non-interactive' };
|
|
58
|
+
}
|
|
59
|
+
const resolved = deps.resolveRef(opts.id);
|
|
60
|
+
if (!resolved.ok) {
|
|
61
|
+
io.error(resolved.reason === 'no-cache' ? fp.refNoCache
|
|
62
|
+
: resolved.reason === 'out-of-range' ? fp.refOutOfRange(resolved.count)
|
|
63
|
+
: fp.idRequired);
|
|
64
|
+
return { ok: false, reason: resolved.reason };
|
|
65
|
+
}
|
|
66
|
+
const positionId = resolved.id;
|
|
67
|
+
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
68
|
+
|
|
69
|
+
const app = await io.withProgress(fp.loading, () => deps.loadApplication({ hubAccessToken, positionId }));
|
|
70
|
+
if (!app.ok) {
|
|
71
|
+
io.error(app.reason === 'not-found' ? fp.notFound : fp.fetchFailed(app.reason));
|
|
72
|
+
return { ok: false, reason: app.reason };
|
|
73
|
+
}
|
|
74
|
+
if (!app.open) {
|
|
75
|
+
io.notify(fp.notOpen);
|
|
76
|
+
return { ok: false, reason: 'not-open' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
io.section(app.title ? clean(app.title) : fp.untitled);
|
|
80
|
+
if (app.company) io.notify(styleLabelPrefix(fp.companyLine(clean(app.company))));
|
|
81
|
+
const mainMotivationQuestion = app.mainMotivationQuestion || fp.mainMotivationFallback;
|
|
82
|
+
const message = String(await io.ask(fp.askMessage(clean(mainMotivationQuestion)))).trim();
|
|
83
|
+
const answers = await askAnswers({ io, ask, fp, questions: app.questions, opts, deps });
|
|
84
|
+
|
|
85
|
+
const [heading, ...lines] = paintedSummary(fp, app, message, answers);
|
|
86
|
+
io.section(heading);
|
|
87
|
+
for (const line of lines) io.notify(line);
|
|
88
|
+
if (!(await io.confirm(fp.confirm))) {
|
|
89
|
+
io.notify(fp.cancelled);
|
|
90
|
+
return { ok: true, cancelled: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const res = await io.withProgress(fp.sending, () => deps.submitApplication({ hubAccessToken, positionId, message, answers, mainMotivationQuestion }));
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
if (res.reason === 'already-applied') io.notify(fp.alreadyApplied(res.applicationStatus));
|
|
96
|
+
else if (res.reason === 'position-unavailable') io.error(fp.notOpen);
|
|
97
|
+
else if (res.reason === 'not-found') io.error(fp.notFound);
|
|
98
|
+
else io.error(fp.failed(res.reason));
|
|
99
|
+
return { ok: false, reason: res.reason };
|
|
100
|
+
}
|
|
101
|
+
io.success(res.status === 'PENDING' ? fp.sentPending : fp.sent);
|
|
102
|
+
return { ok: true, status: res.status };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { makeApplyDeps, runApply };
|
package/src/command-audit.js
CHANGED
|
@@ -37,6 +37,16 @@ function redactArgs(argv) {
|
|
|
37
37
|
return scrubbed || null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// `apply` takes only the position ref: anything typed after it could be the talent's message, so it never reaches the log.
|
|
41
|
+
function withoutFreeText(argv) {
|
|
42
|
+
if (!Array.isArray(argv)) return argv;
|
|
43
|
+
const words = argv.filter((a) => typeof a === 'string' && !a.startsWith('-')).map((w) => w.toLowerCase());
|
|
44
|
+
const at = words[0] === 'apply' ? 0 : words[0] === 'projects' && words[1] === 'apply' ? 1 : -1;
|
|
45
|
+
if (at === -1) return argv;
|
|
46
|
+
const kept = argv.filter((a) => typeof a === 'string' && !a.startsWith('-'));
|
|
47
|
+
return kept.slice(0, at + 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
// Anonymous runs carry only a best-effort machine hint; authenticated runs identify via the Bearer instead.
|
|
41
51
|
function machineHint() {
|
|
42
52
|
let osUser = null;
|
|
@@ -77,7 +87,7 @@ function buildAudit(command, argv, env, id = null) {
|
|
|
77
87
|
const body = {
|
|
78
88
|
...(id ? { id } : {}),
|
|
79
89
|
command: String(command || 'unknown').slice(0, 100),
|
|
80
|
-
args: redactArgs(argv),
|
|
90
|
+
args: redactArgs(withoutFreeText(argv)),
|
|
81
91
|
cliVersion: cliVersion(),
|
|
82
92
|
clientTimestamp: new Date().toISOString(),
|
|
83
93
|
};
|
|
@@ -133,7 +143,7 @@ function startCommandAudit({ command, argv = [], env = process.env } = {}) {
|
|
|
133
143
|
detached: true,
|
|
134
144
|
stdio: 'ignore',
|
|
135
145
|
windowsHide: true,
|
|
136
|
-
env: { ...env, [PAYLOAD_VAR]: JSON.stringify({ id, command, args: redactArgs(argv) }) },
|
|
146
|
+
env: { ...env, [PAYLOAD_VAR]: JSON.stringify({ id, command, args: redactArgs(withoutFreeText(argv)) }) },
|
|
137
147
|
});
|
|
138
148
|
child.on('error', () => {});
|
|
139
149
|
child.unref();
|
|
@@ -148,4 +158,4 @@ function currentRunId() {
|
|
|
148
158
|
return runId;
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
module.exports = { emitCommandAudit, startCommandAudit, logCommandRun, currentRunId, redactArgs, buildAudit, PAYLOAD_VAR };
|
|
161
|
+
module.exports = { emitCommandAudit, startCommandAudit, logCommandRun, currentRunId, redactArgs, withoutFreeText, buildAudit, PAYLOAD_VAR };
|
package/src/config.js
CHANGED
|
@@ -413,6 +413,19 @@ function getUnsavePositionEndpoint(positionId, env = process.env) {
|
|
|
413
413
|
return deriveFromHub(env, `works/talent/saved-positions/${encodeURIComponent(positionId)}`);
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
function getPositionApplicationEndpoint(positionId, env = process.env) {
|
|
417
|
+
const explicit = readEnv(env, 'POSITION_APPLICATION_ENDPOINT');
|
|
418
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
419
|
+
if (positionId == null || typeof positionId === 'object') return null;
|
|
420
|
+
return deriveFromHub(env, `works/talent/positions/${encodeURIComponent(positionId)}/application`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getCandidaturesEndpoint(env = process.env) {
|
|
424
|
+
const explicit = readEnv(env, 'CANDIDATURES_ENDPOINT');
|
|
425
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
426
|
+
return deriveFromHub(env, 'works/talent/candidatures');
|
|
427
|
+
}
|
|
428
|
+
|
|
416
429
|
function getReceivedInvitationsEndpoint(env = process.env) {
|
|
417
430
|
const explicit = readEnv(env, 'RECEIVED_INVITATIONS_ENDPOINT');
|
|
418
431
|
if (explicit && explicit.trim()) return explicit.trim();
|
|
@@ -837,6 +850,8 @@ module.exports = {
|
|
|
837
850
|
// Marketplace (show-project / save-project / unsave-project / invitations).
|
|
838
851
|
getPositionDetailEndpoint,
|
|
839
852
|
getSavePositionEndpoint,
|
|
853
|
+
getPositionApplicationEndpoint,
|
|
854
|
+
getCandidaturesEndpoint,
|
|
840
855
|
getUnsavePositionEndpoint,
|
|
841
856
|
getReceivedInvitationsEndpoint,
|
|
842
857
|
getMyCandidaturesEndpoint,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { normalizeAgentName } = require('./tier-engine');
|
|
4
|
+
const { scrubSecrets } = require('./agent-synthesis');
|
|
5
|
+
const { requestDeclareAgent } = require('./agents-client');
|
|
6
|
+
|
|
7
|
+
function detectedAgentsFromReport(report) {
|
|
8
|
+
const synthesis = report && report.agentSynthesis && Array.isArray(report.agentSynthesis.agents) ? report.agentSynthesis.agents : [];
|
|
9
|
+
const whatItDoesByName = new Map(synthesis.filter((a) => a && a.name).map((a) => [a.name, a.whatItDoes]));
|
|
10
|
+
const agents = report && Array.isArray(report.agents) ? report.agents : [];
|
|
11
|
+
return agents
|
|
12
|
+
.filter((a) => a && typeof a.name === 'string' && a.name)
|
|
13
|
+
.map((a) => ({ name: a.name, whatItDoes: whatItDoesByName.get(a.name) || null }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function newDetectedAgents(candidates, profileAgents) {
|
|
17
|
+
const taken = new Set((Array.isArray(profileAgents) ? profileAgents : []).map((a) => normalizeAgentName(a && a.name)));
|
|
18
|
+
const fresh = [];
|
|
19
|
+
for (const agent of Array.isArray(candidates) ? candidates : []) {
|
|
20
|
+
const key = normalizeAgentName(agent && agent.name);
|
|
21
|
+
if (!key || taken.has(key)) continue;
|
|
22
|
+
taken.add(key);
|
|
23
|
+
fresh.push({ name: agent.name.trim(), whatItDoes: typeof agent.whatItDoes === 'string' && agent.whatItDoes ? agent.whatItDoes : null });
|
|
24
|
+
}
|
|
25
|
+
return fresh;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function declareDetectedAgents(agents, { accessToken, hubAccessToken }, { endpoint, declare = requestDeclareAgent } = {}) {
|
|
29
|
+
const added = [];
|
|
30
|
+
const failed = [];
|
|
31
|
+
for (const agent of agents) {
|
|
32
|
+
const whatItDoes = agent.whatItDoes ? scrubSecrets(agent.whatItDoes) : undefined;
|
|
33
|
+
const result = await declare({ name: agent.name, whatItDoes, accessToken, hubAccessToken }, { endpoint });
|
|
34
|
+
if (result && result.ok) added.push(agent);
|
|
35
|
+
else failed.push({ agent, reason: (result && result.reason) || 'declare-failed' });
|
|
36
|
+
}
|
|
37
|
+
return { added, failed };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { detectedAgentsFromReport, newDetectedAgents, declareDetectedAgents };
|