shakers-cli 0.0.0-stage → 0.14.1
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/LICENSE +59 -0
- package/README.md +1233 -2
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +99 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +124 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +216 -0
- package/bin/mcp.js +124 -0
- package/bin/mcpb.js +5 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +337 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +363 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +40 -4
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +215 -0
- package/src/authorship-mismatch.js +62 -0
- package/src/authorship.js +204 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +93 -0
- package/src/availability-flow.js +124 -0
- package/src/backend-request.js +191 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +163 -0
- package/src/certify-dimension-flow.js +135 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/command-audit.js +105 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +877 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/cv-file.js +62 -0
- package/src/cv-text.js +92 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +120 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +30 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +148 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +124 -0
- package/src/git-log-prefetch.js +53 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3341 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +72 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +111 -0
- package/src/mcp-ai-usage-tool.js +401 -0
- package/src/mcp-alma-tools.js +95 -0
- package/src/mcp-applications-tools.js +44 -0
- package/src/mcp-auth-form.js +480 -0
- package/src/mcp-availability-tools.js +45 -0
- package/src/mcp-certifications-tools.js +44 -0
- package/src/mcp-certify-dimension-tools.js +66 -0
- package/src/mcp-choice.js +95 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +177 -0
- package/src/mcp-find-projects-tools.js +69 -0
- package/src/mcp-install.js +275 -0
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-invitations-tools.js +44 -0
- package/src/mcp-lang-tools.js +41 -0
- package/src/mcp-onboarding-tools.js +379 -0
- package/src/mcp-portfolios-tools.js +50 -0
- package/src/mcp-profile-tools.js +48 -0
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +44 -0
- package/src/mcp-register-tools.js +441 -0
- package/src/mcp-report-tools.js +105 -0
- package/src/mcp-role-tools.js +372 -0
- package/src/mcp-saved-positions-tools.js +59 -0
- package/src/mcp-server.js +290 -0
- package/src/mcp-show-project-tools.js +52 -0
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +1171 -0
- package/src/mcp-socials-tools.js +41 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +374 -0
- package/src/onboarding-flow.js +1162 -0
- package/src/onboarding-funnel.js +272 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +57 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +133 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +66 -0
- package/src/rate-client.js +81 -0
- package/src/rate-flow.js +137 -0
- package/src/register-context.js +145 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +112 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +624 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-normalizers.js +174 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +475 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +185 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +103 -0
- package/src/signup-copy.js +450 -0
- package/src/signup-language.js +21 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +207 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +354 -0
- package/src/work-streams.js +79 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { getConfigDir } = require('./config-dir');
|
|
7
|
+
|
|
8
|
+
// Names and values mirror works-frontend onboarding-funnel-enums.ts.
|
|
9
|
+
|
|
10
|
+
const FLOW_VERSION = 'signup_new_works_v1';
|
|
11
|
+
|
|
12
|
+
const STEP_INDEX = Object.freeze({
|
|
13
|
+
start: 1,
|
|
14
|
+
account_creation: 2,
|
|
15
|
+
import_sources: 3,
|
|
16
|
+
work_situation: 4,
|
|
17
|
+
availability: 5,
|
|
18
|
+
rates: 6,
|
|
19
|
+
languages: 7,
|
|
20
|
+
location: 8,
|
|
21
|
+
profile_summary: 9,
|
|
22
|
+
pre_interview: 10,
|
|
23
|
+
interview: 11,
|
|
24
|
+
supercluster: 12,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const EVENT_TYPES = Object.freeze([
|
|
28
|
+
'onboarding_step_viewed',
|
|
29
|
+
'onboarding_step_completed',
|
|
30
|
+
'onboarding_step_skipped',
|
|
31
|
+
'onboarding_step_errored',
|
|
32
|
+
'onboarding_step_exited',
|
|
33
|
+
'source_import_started',
|
|
34
|
+
'source_import_completed',
|
|
35
|
+
'source_import_failed',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const SURFACES = Object.freeze(['cli', 'mcp']);
|
|
39
|
+
const MCP_CLIENTS = Object.freeze(['claude', 'chatgpt', 'cursor', 'other']);
|
|
40
|
+
const SOURCE_TYPES = Object.freeze(['linkedin', 'cv', 'github', 'web_page']);
|
|
41
|
+
const ERROR_TYPES = Object.freeze(['validation', 'server', 'timeout', 'auth', 'import']);
|
|
42
|
+
|
|
43
|
+
const WORK_SITUATION_VALUES = Object.freeze({
|
|
44
|
+
FREELANCE: 'freelance',
|
|
45
|
+
EMPLOYED: 'employed',
|
|
46
|
+
BETWEEN_JOBS: 'between-jobs',
|
|
47
|
+
STUDYING: 'studying',
|
|
48
|
+
OTHER: 'other',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const ONCE_PER_STEP = new Set(['onboarding_step_viewed', 'onboarding_step_completed']);
|
|
52
|
+
const MAX_EVENTS_PER_REQUEST = 50;
|
|
53
|
+
const SEND_TIMEOUT_MS = 3000;
|
|
54
|
+
const DEVICE_FILE_NAME = 'analytics-device.json';
|
|
55
|
+
|
|
56
|
+
function mcpClientFor(clientName) {
|
|
57
|
+
const name = typeof clientName === 'string' ? clientName.toLowerCase() : '';
|
|
58
|
+
if (name.includes('claude')) return 'claude';
|
|
59
|
+
if (name.includes('chatgpt') || name.includes('openai')) return 'chatgpt';
|
|
60
|
+
if (name.includes('cursor')) return 'cursor';
|
|
61
|
+
return 'other';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sourceTypeFor(source) {
|
|
65
|
+
if (source === 'linkedin' || source === 'cv' || source === 'github') return source;
|
|
66
|
+
return 'web_page';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function workSituationValue(code) {
|
|
70
|
+
return WORK_SITUATION_VALUES[code] || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Same ÷4 as the web; '> 160' has no number.
|
|
74
|
+
function weeklyHours(monthlyHours) {
|
|
75
|
+
const n = Number(monthlyHours);
|
|
76
|
+
return monthlyHours != null && monthlyHours !== '' && Number.isFinite(n) ? Math.round(n / 4) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// As on the web: part-time project price is hourly, full-time is annual.
|
|
80
|
+
function rateTypes({ partTimeSelected, fullTimeSelected } = {}) {
|
|
81
|
+
return [partTimeSelected === true && 'hourly', fullTimeSelected === true && 'annual'].filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function errorTypeFor(reason) {
|
|
85
|
+
const r = String(reason || '');
|
|
86
|
+
if (r === 'no-session' || r === 'no-hub-token' || r === 'http-401' || r === 'http-403') return 'auth';
|
|
87
|
+
if (r === 'timeout') return 'timeout';
|
|
88
|
+
if (r === 'no-endpoint') return 'server';
|
|
89
|
+
if (r.startsWith('bad-') || r.startsWith('no-') || r === 'duplicate-language' || r === 'unresolved-languages') return 'validation';
|
|
90
|
+
return 'server';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cleanProperties(properties) {
|
|
94
|
+
const ok = (v) => typeof v === 'string' || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v))
|
|
95
|
+
|| (Array.isArray(v) && v.every((x) => typeof x === 'string'));
|
|
96
|
+
return Object.fromEntries(Object.entries(properties || {}).filter(([, v]) => ok(v)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function deviceFilePath(env) {
|
|
100
|
+
return path.join(getConfigDir(env), DEVICE_FILE_NAME);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadOrCreateDeviceId(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
104
|
+
const target = deviceFilePath(env);
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(fsImpl.readFileSync(target, 'utf8'));
|
|
107
|
+
if (parsed && typeof parsed.deviceId === 'string' && parsed.deviceId) return parsed.deviceId;
|
|
108
|
+
} catch { /* create one */ }
|
|
109
|
+
const deviceId = crypto.randomUUID();
|
|
110
|
+
const dir = path.dirname(target);
|
|
111
|
+
const tmp = path.join(dir, `.${DEVICE_FILE_NAME}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
112
|
+
let fd = null;
|
|
113
|
+
try {
|
|
114
|
+
fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
115
|
+
fd = fsImpl.openSync(tmp, 'wx', 0o600);
|
|
116
|
+
fsImpl.writeFileSync(fd, `${JSON.stringify({ deviceId }, null, 2)}\n`);
|
|
117
|
+
try { fsImpl.fsyncSync(fd); } catch { /* e.g. EINVAL on some filesystems */ }
|
|
118
|
+
fsImpl.closeSync(fd);
|
|
119
|
+
fd = null;
|
|
120
|
+
fsImpl.renameSync(tmp, target);
|
|
121
|
+
return deviceId;
|
|
122
|
+
} catch {
|
|
123
|
+
if (fd !== null) { try { fsImpl.closeSync(fd); } catch { /* already closed */ } }
|
|
124
|
+
try { fsImpl.unlinkSync(tmp); } catch { /* never existed */ }
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function defaultSend({ endpoint, events, hubAccessToken }) {
|
|
130
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
131
|
+
const res = await postJsonWithTimeout(endpoint, { events }, SEND_TIMEOUT_MS, 'POST', null, { Authorization: `Bearer ${hubAccessToken}` });
|
|
132
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function defaultHubToken() {
|
|
136
|
+
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
137
|
+
const session = loadAuthSession();
|
|
138
|
+
return sessionStatus(session) === 'active' && session && session.hubAccessToken ? session.hubAccessToken : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createFunnel({
|
|
142
|
+
surface,
|
|
143
|
+
hubToken = defaultHubToken,
|
|
144
|
+
deviceId = () => loadOrCreateDeviceId(),
|
|
145
|
+
endpoint = () => require('./config').getOnboardingEventsEndpoint(),
|
|
146
|
+
send = defaultSend,
|
|
147
|
+
now = () => Date.now(),
|
|
148
|
+
uuid = () => crypto.randomUUID(),
|
|
149
|
+
} = {}) {
|
|
150
|
+
if (!SURFACES.includes(surface)) throw new Error(`unknown funnel surface: ${surface}`);
|
|
151
|
+
let pending = [];
|
|
152
|
+
let lastTime = 0;
|
|
153
|
+
let seq = 0;
|
|
154
|
+
let ready = false;
|
|
155
|
+
let account = null;
|
|
156
|
+
let active = false;
|
|
157
|
+
let mcpClient = null;
|
|
158
|
+
let cachedDeviceId;
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
const inflight = new Set();
|
|
161
|
+
|
|
162
|
+
function track(eventType, stepName, properties = {}) {
|
|
163
|
+
if (!active || !EVENT_TYPES.includes(eventType) || !STEP_INDEX[stepName]) return false;
|
|
164
|
+
if (ONCE_PER_STEP.has(eventType)) {
|
|
165
|
+
const key = `${eventType}:${stepName}`;
|
|
166
|
+
if (seen.has(key)) return false;
|
|
167
|
+
seen.add(key);
|
|
168
|
+
}
|
|
169
|
+
const common = { step_name: stepName, step_index: STEP_INDEX[stepName], flow_version: FLOW_VERSION, surface };
|
|
170
|
+
if (surface === 'mcp') common.mcp_client = mcpClient || 'other';
|
|
171
|
+
pending.push({ eventType, insertId: uuid(), at: now(), seq: seq++, properties: { ...cleanProperties(properties), ...common } });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Funnel order, each time strictly after the previous one sent.
|
|
176
|
+
function stamp(batch) {
|
|
177
|
+
const ordered = [...batch].sort((a, b) => a.properties.step_index - b.properties.step_index || a.seq - b.seq);
|
|
178
|
+
const id = cachedDeviceId === undefined ? (cachedDeviceId = safe(deviceId)) : cachedDeviceId;
|
|
179
|
+
return ordered.map((e) => {
|
|
180
|
+
lastTime = Math.max(e.at, lastTime + 1);
|
|
181
|
+
return { event_type: e.eventType, insert_id: e.insertId, time: lastTime, ...(id ? { device_id: id } : {}), properties: e.properties };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function flush() {
|
|
186
|
+
if (!ready || !pending.length) return Promise.resolve();
|
|
187
|
+
const token = safe(hubToken);
|
|
188
|
+
const url = safe(endpoint);
|
|
189
|
+
// Never send under another account signed in since.
|
|
190
|
+
if (!token || !url || accountOf(token) !== account) return Promise.resolve();
|
|
191
|
+
const events = stamp(pending);
|
|
192
|
+
pending = [];
|
|
193
|
+
const sends = [];
|
|
194
|
+
for (let i = 0; i < events.length; i += MAX_EVENTS_PER_REQUEST) {
|
|
195
|
+
sends.push(Promise.resolve().then(() => send({ endpoint: url, events: events.slice(i, i + MAX_EVENTS_PER_REQUEST), hubAccessToken: token })).catch(() => null));
|
|
196
|
+
}
|
|
197
|
+
const done = Promise.all(sends).then(() => undefined, () => undefined);
|
|
198
|
+
inflight.add(done);
|
|
199
|
+
done.then(() => inflight.delete(done));
|
|
200
|
+
return done;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
surface,
|
|
205
|
+
begin() {
|
|
206
|
+
seen.clear();
|
|
207
|
+
active = true;
|
|
208
|
+
},
|
|
209
|
+
get active() { return active; },
|
|
210
|
+
setMcpClient(clientName) { mcpClient = mcpClientFor(clientName); },
|
|
211
|
+
track,
|
|
212
|
+
viewed: (step, props) => track('onboarding_step_viewed', step, props),
|
|
213
|
+
completed: (step, props) => track('onboarding_step_completed', step, props),
|
|
214
|
+
skipped: (step, props) => track('onboarding_step_skipped', step, props),
|
|
215
|
+
errored: (step, reason, props = {}) => track('onboarding_step_errored', step, { error_type: errorTypeFor(reason), ...props }),
|
|
216
|
+
identified() {
|
|
217
|
+
const token = safe(hubToken);
|
|
218
|
+
if (!token) return Promise.resolve();
|
|
219
|
+
account = accountOf(token);
|
|
220
|
+
ready = true;
|
|
221
|
+
return flush();
|
|
222
|
+
},
|
|
223
|
+
flush,
|
|
224
|
+
idle: () => Promise.all([...inflight]).then(() => undefined),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A refreshed token keeps the same account.
|
|
229
|
+
function accountOf(token) {
|
|
230
|
+
const payload = require('./auth-session-store').decodeJwtPayload(token);
|
|
231
|
+
return (payload && (payload.sub || payload.email)) || token;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function safe(fn) {
|
|
235
|
+
try { return fn(); } catch { return null; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const NOOP_FUNNEL = Object.freeze({
|
|
239
|
+
surface: null,
|
|
240
|
+
begin() {},
|
|
241
|
+
active: false,
|
|
242
|
+
setMcpClient() {},
|
|
243
|
+
track: () => false,
|
|
244
|
+
viewed: () => false,
|
|
245
|
+
completed: () => false,
|
|
246
|
+
skipped: () => false,
|
|
247
|
+
errored: () => false,
|
|
248
|
+
identified: () => Promise.resolve(),
|
|
249
|
+
flush: () => Promise.resolve(),
|
|
250
|
+
idle: () => Promise.resolve(),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
FLOW_VERSION,
|
|
255
|
+
STEP_INDEX,
|
|
256
|
+
EVENT_TYPES,
|
|
257
|
+
SURFACES,
|
|
258
|
+
MCP_CLIENTS,
|
|
259
|
+
SOURCE_TYPES,
|
|
260
|
+
ERROR_TYPES,
|
|
261
|
+
MAX_EVENTS_PER_REQUEST,
|
|
262
|
+
DEVICE_FILE_NAME,
|
|
263
|
+
NOOP_FUNNEL,
|
|
264
|
+
createFunnel,
|
|
265
|
+
mcpClientFor,
|
|
266
|
+
sourceTypeFor,
|
|
267
|
+
workSituationValue,
|
|
268
|
+
weeklyHours,
|
|
269
|
+
rateTypes,
|
|
270
|
+
errorTypeFor,
|
|
271
|
+
loadOrCreateDeviceId,
|
|
272
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Thin certs adapter: exchange a created onboarding interview for LiveKit room credentials so the CLI can join over text (lk.chat).
|
|
4
|
+
|
|
5
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
6
|
+
const { reasonForError } = require('./onboarding-client');
|
|
7
|
+
const { toUpperLang } = require('./lang-codes');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
10
|
+
// Sub-path appended to `{base}/{interviewId}/` — the real post-merge certs
|
|
11
|
+
// route (sibling of `text-session`). Change this ONE constant if it moves.
|
|
12
|
+
const LIVEKIT_SESSION_PATH = 'livekit-text-session';
|
|
13
|
+
// Completion route: `PATCH {base}/{interviewId}/complete`.
|
|
14
|
+
const COMPLETE_SESSION_PATH = 'complete';
|
|
15
|
+
|
|
16
|
+
function parseJson(raw) {
|
|
17
|
+
try {
|
|
18
|
+
return raw ? JSON.parse(raw) : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function envelopeData(raw) {
|
|
25
|
+
const json = parseJson(raw);
|
|
26
|
+
if (!json || typeof json !== 'object' || json.status !== 'OK') return undefined;
|
|
27
|
+
return json.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bearerHeaders(accessToken) {
|
|
31
|
+
const headers = {};
|
|
32
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pickString(obj, keys) {
|
|
37
|
+
for (const k of keys) {
|
|
38
|
+
const v = obj && obj[k];
|
|
39
|
+
if (typeof v === 'string' && v) return v;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function requestStartOnboardingLivekitSession(
|
|
45
|
+
{ interviewId, language, accessToken } = {},
|
|
46
|
+
{ base, path = LIVEKIT_SESSION_PATH, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
47
|
+
) {
|
|
48
|
+
if (!base) return { ok: false, reason: 'no-endpoint' };
|
|
49
|
+
if (!interviewId) return { ok: false, reason: 'no-interview' };
|
|
50
|
+
|
|
51
|
+
const url = `${base.replace(/\/+$/, '')}/${encodeURIComponent(interviewId)}/${path}`;
|
|
52
|
+
const body = { language: toUpperLang(language) };
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await postJsonWithTimeout(url, body, timeoutMs, 'POST', null, bearerHeaders(accessToken));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
58
|
+
}
|
|
59
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
60
|
+
|
|
61
|
+
const data = envelopeData(res.raw);
|
|
62
|
+
if (data === undefined || !data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
63
|
+
|
|
64
|
+
const livekitUrl = pickString(data, ['livekitUrl', 'url', 'wsUrl', 'serverUrl']);
|
|
65
|
+
const token = pickString(data, ['token', 'accessToken', 'participantToken']);
|
|
66
|
+
const roomName = pickString(data, ['roomName', 'room', 'room_name']);
|
|
67
|
+
const closingMessage = pickString(data, ['closingMessage', 'closing']);
|
|
68
|
+
const resolvedId = pickString(data, ['interviewId', 'id']) || interviewId;
|
|
69
|
+
|
|
70
|
+
if (!livekitUrl || !token) return { ok: false, reason: 'bad-response' };
|
|
71
|
+
return { ok: true, livekitUrl, token, roomName: roomName || null, interviewId: resolvedId, closingMessage: closingMessage || null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Persist the onboarding interview's transcript on session end (the LiveKit text flow is the one path that was not sending it; certs already stores + serves it).
|
|
75
|
+
async function requestCompleteOnboardingLivekitSession(
|
|
76
|
+
{ interviewId, durationSeconds, transcripts, accessToken } = {},
|
|
77
|
+
{ base, path = COMPLETE_SESSION_PATH, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
78
|
+
) {
|
|
79
|
+
if (!base) return { ok: false, reason: 'no-endpoint' };
|
|
80
|
+
if (!interviewId) return { ok: false, reason: 'no-interview' };
|
|
81
|
+
if (!Array.isArray(transcripts) || transcripts.length === 0) return { ok: false, reason: 'no-transcript' };
|
|
82
|
+
|
|
83
|
+
const url = `${base.replace(/\/+$/, '')}/${encodeURIComponent(interviewId)}/${path}`;
|
|
84
|
+
const body = { transcripts };
|
|
85
|
+
if (Number.isInteger(durationSeconds) && durationSeconds >= 0) body.durationSeconds = durationSeconds;
|
|
86
|
+
|
|
87
|
+
let res;
|
|
88
|
+
try {
|
|
89
|
+
res = await postJsonWithTimeout(url, body, timeoutMs, 'PATCH', null, bearerHeaders(accessToken));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
92
|
+
}
|
|
93
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
94
|
+
return { ok: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
requestStartOnboardingLivekitSession,
|
|
99
|
+
requestCompleteOnboardingLivekitSession,
|
|
100
|
+
LIVEKIT_SESSION_PATH,
|
|
101
|
+
COMPLETE_SESSION_PATH,
|
|
102
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 12000;
|
|
6
|
+
|
|
7
|
+
function parseJson(raw) {
|
|
8
|
+
try {
|
|
9
|
+
return raw ? JSON.parse(raw) : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function authHeaders({ accessToken } = {}) {
|
|
16
|
+
const headers = {};
|
|
17
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
18
|
+
return headers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function requestOnboardingStatus({ accessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
22
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
23
|
+
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders({ accessToken }));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (res.status < 200 || res.status >= 300) {
|
|
32
|
+
return { ok: false, reason: `http-${res.status}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const json = parseJson(res.raw);
|
|
36
|
+
const data = json && typeof json === 'object' ? json.data : null;
|
|
37
|
+
if (!data || typeof data !== 'object') return { ok: false, reason: 'bad-response' };
|
|
38
|
+
|
|
39
|
+
return { ok: true, completed: data.completed === true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// GET <certs>/interviews/:id/finalization -> COLLECTING | PROCESSING | READY | FAILED for a completed interview's evaluation.
|
|
43
|
+
async function requestInterviewFinalization({ interviewId, accessToken } = {}, { base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
44
|
+
if (!base || !interviewId) return { ok: false, reason: 'no-endpoint' };
|
|
45
|
+
let res;
|
|
46
|
+
try {
|
|
47
|
+
res = await postJsonWithTimeout(`${base}/${encodeURIComponent(interviewId)}/finalization`, null, timeoutMs, 'GET', null, authHeaders({ accessToken }));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
50
|
+
}
|
|
51
|
+
if (res.status < 200 || res.status >= 300) return { ok: false, reason: `http-${res.status}` };
|
|
52
|
+
const json = parseJson(res.raw);
|
|
53
|
+
const status = json && json.data && typeof json.data.status === 'string' ? json.data.status : null;
|
|
54
|
+
return status ? { ok: true, status } : { ok: false, reason: 'bad-response' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { requestOnboardingStatus, requestInterviewFinalization };
|
package/src/open-file.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Zero-dependency, best-effort "open this file in the OS default app" (ADR-016, used by the `report` command to open the generated HTML in the browser).
|
|
6
|
+
function openPath(target) {
|
|
7
|
+
let cmd;
|
|
8
|
+
let args;
|
|
9
|
+
if (process.platform === 'darwin') {
|
|
10
|
+
cmd = 'open';
|
|
11
|
+
args = [target];
|
|
12
|
+
} else if (process.platform === 'win32') {
|
|
13
|
+
// `start` needs an (empty) title arg first when the target may be quoted.
|
|
14
|
+
cmd = 'cmd';
|
|
15
|
+
args = ['/c', 'start', '', target];
|
|
16
|
+
} else {
|
|
17
|
+
cmd = 'xdg-open';
|
|
18
|
+
args = [target];
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
22
|
+
// An absent opener (ENOENT) surfaces as an async 'error' — swallow it so it
|
|
23
|
+
// never becomes an unhandled exception that breaks the REPL.
|
|
24
|
+
child.on('error', () => {});
|
|
25
|
+
child.unref();
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { openPath };
|
package/src/osc-link.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// OSC 8 terminal hyperlink helper (talents-ai-score, user request: links were printed as plain text and could NOT be clicked in iTerm2).
|
|
4
|
+
|
|
5
|
+
const OSC = '\x1b]8;;';
|
|
6
|
+
const ST = '\x1b\\';
|
|
7
|
+
|
|
8
|
+
// Wrap `label` as an OSC 8 hyperlink to `url`.
|
|
9
|
+
function oscLink(url, label) {
|
|
10
|
+
const target = url == null ? '' : String(url);
|
|
11
|
+
const text = label == null || label === '' ? target : String(label);
|
|
12
|
+
if (!target) return text;
|
|
13
|
+
return `${OSC}${target}${ST}${text}${OSC}${ST}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { oscLink };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
|
|
4
|
+
const { reasonForError, WORKS_CODE_TO_REASON } = require('./skills-client');
|
|
5
|
+
|
|
6
|
+
// Talent-portfolio client (talents-ai-score, ADR-059 — `start`'s "Add project to portfolio" route, src/start-add-portfolio.js).
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
9
|
+
|
|
10
|
+
function parseJson(raw) {
|
|
11
|
+
try {
|
|
12
|
+
return raw ? JSON.parse(raw) : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function authHeaders({ accessToken, hubAccessToken } = {}) {
|
|
19
|
+
const headers = {};
|
|
20
|
+
if (typeof accessToken === 'string' && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
|
21
|
+
if (typeof hubAccessToken === 'string' && hubAccessToken) headers['X-Hub-Token'] = hubAccessToken;
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hubHeaders({ hubAccessToken } = {}) {
|
|
26
|
+
const headers = {};
|
|
27
|
+
if (typeof hubAccessToken === 'string' && hubAccessToken) headers.Authorization = `Bearer ${hubAccessToken}`;
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `GET .../portfolios`.
|
|
32
|
+
async function requestListPortfolios({ accessToken, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
33
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
34
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
35
|
+
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, authHeaders({ accessToken, hubAccessToken }));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (res.status < 200 || res.status >= 300) {
|
|
44
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const json = parseJson(res.raw);
|
|
48
|
+
const data = json && typeof json === 'object' && Array.isArray(json.data) ? json.data : null;
|
|
49
|
+
if (!data) return { ok: false, reason: 'bad-response' };
|
|
50
|
+
|
|
51
|
+
const portfolios = data
|
|
52
|
+
.map((entry) => ({
|
|
53
|
+
id: entry && typeof entry.id === 'string' ? entry.id : null,
|
|
54
|
+
name: entry && typeof entry.name === 'string' ? entry.name : null,
|
|
55
|
+
type: entry && typeof entry.type === 'string' ? entry.type : null,
|
|
56
|
+
skillIds: entry && Array.isArray(entry.skillIds) ? entry.skillIds : [],
|
|
57
|
+
startDate: entry && typeof entry.startDate === 'string' ? entry.startDate : null,
|
|
58
|
+
endDate: entry && typeof entry.endDate === 'string' ? entry.endDate : null,
|
|
59
|
+
isCurrent: entry ? entry.isCurrent === true : false,
|
|
60
|
+
}))
|
|
61
|
+
.filter((p) => p.name !== null);
|
|
62
|
+
return { ok: true, portfolios };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `POST .../portfolios/declare`.
|
|
66
|
+
async function requestDeclarePortfolio(
|
|
67
|
+
{ name, type, skillIds, description, url, clientName, clientDomain, startDate, endDate, accessToken, hubAccessToken } = {},
|
|
68
|
+
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
69
|
+
) {
|
|
70
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
71
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
72
|
+
|
|
73
|
+
const body = { name, type };
|
|
74
|
+
if (Array.isArray(skillIds) && skillIds.length > 0) body.skillIds = skillIds;
|
|
75
|
+
if (typeof description === 'string' && description) body.description = description;
|
|
76
|
+
if (typeof url === 'string' && url) body.url = url;
|
|
77
|
+
if (typeof clientName === 'string' && clientName) body.clientName = clientName;
|
|
78
|
+
if (typeof clientDomain === 'string' && clientDomain) body.clientDomain = clientDomain;
|
|
79
|
+
if (typeof startDate === 'string' && startDate) body.startDate = startDate;
|
|
80
|
+
if (typeof endDate === 'string' && endDate) body.endDate = endDate;
|
|
81
|
+
body.generatedWith = 'AI';
|
|
82
|
+
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken, hubAccessToken }));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (res.status < 200 || res.status >= 300) {
|
|
91
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const json = parseJson(res.raw);
|
|
95
|
+
if (!json || json.status !== 'OK') return { ok: false, reason: 'bad-response' };
|
|
96
|
+
return { ok: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// `POST .../portfolios/draft-description`.
|
|
100
|
+
async function requestDraftDescription(
|
|
101
|
+
{ name, technologies, readme, packageDescription, accessToken } = {},
|
|
102
|
+
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
103
|
+
) {
|
|
104
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
105
|
+
|
|
106
|
+
const body = {
|
|
107
|
+
consent: true,
|
|
108
|
+
name,
|
|
109
|
+
technologies: Array.isArray(technologies) ? technologies.filter((t) => typeof t === 'string' && t) : [],
|
|
110
|
+
};
|
|
111
|
+
if (typeof readme === 'string' && readme) body.readme = readme;
|
|
112
|
+
if (typeof packageDescription === 'string' && packageDescription) body.packageDescription = packageDescription;
|
|
113
|
+
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = await postJsonWithTimeout(endpoint, body, timeoutMs, 'POST', null, authHeaders({ accessToken }));
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (res.status < 200 || res.status >= 300) {
|
|
122
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const json = parseJson(res.raw);
|
|
126
|
+
const description = json && json.status === 'OK' && json.data && typeof json.data.description === 'string'
|
|
127
|
+
? json.data.description
|
|
128
|
+
: null;
|
|
129
|
+
if (!description) return { ok: false, reason: 'bad-response' };
|
|
130
|
+
return { ok: true, description };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function requestUpdatePortfolioSkills(
|
|
134
|
+
{ portfolioId, skillIds, hubAccessToken } = {},
|
|
135
|
+
{ endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
136
|
+
) {
|
|
137
|
+
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
138
|
+
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
139
|
+
if (!portfolioId) return { ok: false, reason: 'no-portfolio' };
|
|
140
|
+
|
|
141
|
+
const url = `${endpoint.replace(/\/+$/, '')}/${encodeURIComponent(portfolioId)}`;
|
|
142
|
+
const fields = { skillIds: (Array.isArray(skillIds) ? skillIds : []).join(',') };
|
|
143
|
+
let res;
|
|
144
|
+
try {
|
|
145
|
+
res = await postMultipartWithTimeout(url, fields, timeoutMs, 'PATCH', hubHeaders({ hubAccessToken }));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return { ok: false, reason: (e && e.kind) || 'network-error' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (res.status < 200 || res.status >= 300) {
|
|
151
|
+
return { ok: false, reason: reasonForError(res.status, res.raw) };
|
|
152
|
+
}
|
|
153
|
+
const json = parseJson(res.raw);
|
|
154
|
+
if (!json || json.status !== 'OK') return { ok: false, reason: 'bad-response' };
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
requestListPortfolios,
|
|
160
|
+
requestDeclarePortfolio,
|
|
161
|
+
requestDraftDescription,
|
|
162
|
+
requestUpdatePortfolioSkills,
|
|
163
|
+
WORKS_CODE_TO_REASON,
|
|
164
|
+
};
|