shakers-cli 0.17.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/README.md +15 -5
- package/bin/add-agent.js +12 -6
- package/bin/ai-usage.js +2 -1
- package/bin/apply.js +66 -0
- package/bin/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +32 -19
- package/bin/register.js +10 -29
- package/bin/shakers-cli.js +21 -12
- package/package.json +1 -1
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +5 -3
- package/src/apply-client.js +164 -0
- package/src/apply-flow.js +105 -0
- package/src/auth-client.js +7 -107
- package/src/auth-session-store.js +6 -12
- package/src/backend-request.js +1 -24
- package/src/cli-skill.js +77 -0
- package/src/command-audit.js +58 -5
- package/src/config.js +15 -16
- package/src/consent-flow.js +7 -2
- package/src/detect-repos.js +4 -8
- package/src/detected-agents.js +40 -0
- package/src/detectors.js +23 -0
- package/src/device-auth-store.js +2 -2
- package/src/device-login.js +11 -9
- package/src/env-paths.js +9 -1
- package/src/i18n.js +258 -88
- package/src/mcp-ai-usage-result-tool.js +6 -4
- package/src/mcp-ai-usage-tool.js +73 -47
- package/src/mcp-apply-tools.js +152 -0
- package/src/mcp-choice.js +171 -34
- package/src/mcp-detector.js +26 -2
- package/src/mcp-device-login-tools.js +40 -8
- package/src/mcp-install.js +15 -11
- package/src/mcp-instructions.js +43 -17
- package/src/mcp-onboarding-tools.js +90 -3
- package/src/mcp-prompts.js +6 -4
- package/src/mcp-register-tools.js +25 -14
- package/src/mcp-role-tools.js +35 -17
- package/src/mcp-server-name.js +7 -0
- package/src/mcp-server.js +35 -7
- package/src/mcp-signup-client.js +3 -6
- package/src/mcp-signup-flow.js +389 -0
- package/src/mcp-signup-tools.js +237 -126
- package/src/onboarding-flow.js +30 -125
- package/src/opencode-sessions.js +92 -0
- package/src/profile-agents.js +26 -0
- package/src/saved-positions-flow.js +3 -3
- package/src/scanner.js +104 -12
- package/src/session-gate.js +1 -4
- package/src/session-normalizers.js +114 -8
- package/src/session-refresh.js +5 -16
- package/src/session-scan.js +167 -22
- package/src/share-payload.js +4 -1
- package/src/share.js +2 -2
- package/src/signup-client.js +12 -39
- package/src/signup-copy.js +506 -68
- package/src/skill-install.js +102 -0
- package/src/start-add-agents.js +106 -3
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +16 -4
- package/src/usage-scan.js +3 -0
- package/src/brand-svg.js +0 -9
- package/src/inter-fonts.js +0 -6
- package/src/mcp-auth-form.js +0 -484
|
@@ -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/auth-client.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { postJsonWithTimeout
|
|
4
|
-
|
|
5
|
-
// Login / token acquisition against the Hub's better-auth email engine.
|
|
3
|
+
const { postJsonWithTimeout } = require('./backend-request');
|
|
6
4
|
|
|
7
5
|
const DEFAULT_TIMEOUT_MS = 20000;
|
|
8
6
|
const SIGN_OUT_TIMEOUT_MS = 3000;
|
|
9
|
-
const SESSION_COOKIE_NAME = 'better-auth.session_token';
|
|
10
|
-
// Refresh the cookie session's own validity from the Set-Cookie Max-Age; fall
|
|
11
|
-
// back to 7 days (better-auth's default) when the attribute is absent.
|
|
12
|
-
const DEFAULT_COOKIE_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
13
7
|
|
|
14
8
|
function parseJson(raw) {
|
|
15
9
|
try { return raw ? JSON.parse(raw) : null; } catch { return null; }
|
|
@@ -28,71 +22,10 @@ function expiresAtFromJwt(token) {
|
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
// `Expires`) of the named cookie; defaults to +7 days when neither is present.
|
|
33
|
-
function cookieExpiresAtFromHeaders(headers, name = SESSION_COOKIE_NAME) {
|
|
34
|
-
const raw = headers && headers['set-cookie'];
|
|
35
|
-
const list = Array.isArray(raw) ? raw : (typeof raw === 'string' && raw ? [raw] : []);
|
|
36
|
-
// Match the base name with or without better-auth's HTTPS `__Secure-`/`__Host-` prefixes.
|
|
37
|
-
const names = [name, `__Secure-${name}`, `__Host-${name}`];
|
|
38
|
-
const entry = list.find((e) => typeof e === 'string' && names.some((n) => e.startsWith(`${n}=`)));
|
|
39
|
-
if (entry) {
|
|
40
|
-
const maxAge = /(?:^|;\s*)max-age=(\d+)/i.exec(entry);
|
|
41
|
-
if (maxAge) return new Date(Date.now() + Number(maxAge[1]) * 1000).toISOString();
|
|
42
|
-
const expires = /(?:^|;\s*)expires=([^;]+)/i.exec(entry);
|
|
43
|
-
if (expires) {
|
|
44
|
-
const t = Date.parse(expires[1]);
|
|
45
|
-
if (Number.isFinite(t)) return new Date(t).toISOString();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return new Date(Date.now() + DEFAULT_COOKIE_TTL_SECONDS * 1000).toISOString();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeResponse(status, raw) {
|
|
52
|
-
if (status >= 200 && status < 300) {
|
|
53
|
-
let json = null;
|
|
54
|
-
try { json = raw ? JSON.parse(raw) : null; } catch { json = null; }
|
|
55
|
-
const payload = json && typeof json.data === 'object' && json.data !== null ? json.data : json;
|
|
56
|
-
const accessToken = payload && typeof payload.accessToken === 'string' && payload.accessToken ? payload.accessToken : null;
|
|
57
|
-
if (!accessToken) return { ok: false, reason: 'bad-response', status };
|
|
58
|
-
const expiresAt = payload && typeof payload.expiresAt === 'string' && payload.expiresAt ? payload.expiresAt : null;
|
|
59
|
-
const emailRaw = payload && (payload.userEmail || payload.email);
|
|
60
|
-
const email = typeof emailRaw === 'string' && emailRaw ? emailRaw : null;
|
|
61
|
-
const hubAccessToken = payload && typeof payload.hubAccessToken === 'string' && payload.hubAccessToken ? payload.hubAccessToken : null;
|
|
62
|
-
return { ok: true, accessToken, expiresAt, email, hubAccessToken };
|
|
63
|
-
}
|
|
64
|
-
if (status === 401) return { ok: false, reason: 'invalid-credentials', status };
|
|
65
|
-
if (status === 409 || status === 422) return { ok: false, reason: 'no-email-password', status };
|
|
66
|
-
if (status === 502 || status === 503) return { ok: false, reason: 'upstream', status };
|
|
67
|
-
return { ok: false, reason: 'http-error', status };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Maps the sign-in POST outcome to a captured cookie + email, or a named reason.
|
|
71
|
-
function normalizeSignIn(status, raw, headers) {
|
|
72
|
-
if (status >= 200 && status < 300) {
|
|
73
|
-
const cookie = extractSetCookie(headers, SESSION_COOKIE_NAME);
|
|
74
|
-
if (!cookie) return { ok: false, reason: 'bad-response', status };
|
|
75
|
-
const body = parseJson(raw);
|
|
76
|
-
const user = body && typeof body === 'object' ? body.user : null;
|
|
77
|
-
const emailRaw = user && typeof user.email === 'string' ? user.email : null;
|
|
78
|
-
return {
|
|
79
|
-
ok: true,
|
|
80
|
-
cookie,
|
|
81
|
-
cookieExpiresAt: cookieExpiresAtFromHeaders(headers),
|
|
82
|
-
email: emailRaw || null,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (status === 401) return { ok: false, reason: 'invalid-credentials', status };
|
|
86
|
-
if (status === 409 || status === 422) return { ok: false, reason: 'no-email-password', status };
|
|
87
|
-
if (status === 502 || status === 503) return { ok: false, reason: 'upstream', status };
|
|
88
|
-
return { ok: false, reason: 'http-error', status };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Mints a hub JWT from a better-auth session cookie: `GET {tokenEndpoint}` with the `Cookie` header.
|
|
92
|
-
async function requestAuthToken({ cookie, sessionToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
25
|
+
async function requestAuthToken({ sessionToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
93
26
|
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
94
|
-
if (!
|
|
95
|
-
const credential =
|
|
27
|
+
if (!sessionToken) return { ok: false, reason: 'no-session-token' };
|
|
28
|
+
const credential = { Authorization: `Bearer ${sessionToken}` };
|
|
96
29
|
let res;
|
|
97
30
|
try {
|
|
98
31
|
res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, credential);
|
|
@@ -106,38 +39,9 @@ async function requestAuthToken({ cookie, sessionToken } = {}, { endpoint, timeo
|
|
|
106
39
|
return { ok: true, accessToken: token, expiresAt: expiresAtFromJwt(token) };
|
|
107
40
|
}
|
|
108
41
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
{ signInEndpoint, tokenEndpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
113
|
-
) {
|
|
114
|
-
if (!signInEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
115
|
-
let signIn;
|
|
116
|
-
try {
|
|
117
|
-
const { status, raw, headers } = await postJsonWithTimeout(signInEndpoint, { email, password }, timeoutMs);
|
|
118
|
-
signIn = normalizeSignIn(status, raw, headers);
|
|
119
|
-
} catch (e) {
|
|
120
|
-
return { ok: false, reason: (e && e.kind) || 'local-error' };
|
|
121
|
-
}
|
|
122
|
-
if (!signIn.ok) return signIn;
|
|
123
|
-
|
|
124
|
-
const tok = await requestAuthToken({ cookie: signIn.cookie }, { endpoint: tokenEndpoint, timeoutMs });
|
|
125
|
-
if (!tok.ok) return { ok: false, reason: 'token-fetch-failed', status: tok.status || null };
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
ok: true,
|
|
129
|
-
accessToken: tok.accessToken,
|
|
130
|
-
hubAccessToken: tok.accessToken,
|
|
131
|
-
expiresAt: tok.expiresAt,
|
|
132
|
-
email: signIn.email || (typeof email === 'string' ? email : null),
|
|
133
|
-
cookie: signIn.cookie,
|
|
134
|
-
cookieExpiresAt: signIn.cookieExpiresAt,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function requestSignOut({ cookie, sessionToken } = {}, { endpoint, origin = null, timeoutMs = SIGN_OUT_TIMEOUT_MS } = {}) {
|
|
139
|
-
if (!endpoint || (!cookie && !sessionToken)) return { ok: false, reason: 'no-credential' };
|
|
140
|
-
const credential = cookie ? { Cookie: cookie, ...(origin ? { Origin: origin } : {}) } : { Authorization: `Bearer ${sessionToken}` };
|
|
42
|
+
async function requestSignOut({ sessionToken } = {}, { endpoint, timeoutMs = SIGN_OUT_TIMEOUT_MS } = {}) {
|
|
43
|
+
if (!endpoint || !sessionToken) return { ok: false, reason: 'no-credential' };
|
|
44
|
+
const credential = { Authorization: `Bearer ${sessionToken}` };
|
|
141
45
|
try {
|
|
142
46
|
const res = await postJsonWithTimeout(endpoint, {}, timeoutMs, 'POST', null, credential);
|
|
143
47
|
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
@@ -148,11 +52,7 @@ async function requestSignOut({ cookie, sessionToken } = {}, { endpoint, origin
|
|
|
148
52
|
|
|
149
53
|
module.exports = {
|
|
150
54
|
requestSignOut,
|
|
151
|
-
requestLogin,
|
|
152
55
|
requestAuthToken,
|
|
153
|
-
normalizeSignIn,
|
|
154
|
-
normalizeResponse,
|
|
155
56
|
expiresAtFromJwt,
|
|
156
|
-
SESSION_COOKIE_NAME,
|
|
157
57
|
DEFAULT_TIMEOUT_MS,
|
|
158
58
|
};
|
|
@@ -89,12 +89,11 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
89
89
|
const explicitHubToken = typeof session.hubAccessToken === 'string' && session.hubAccessToken ? session.hubAccessToken : null;
|
|
90
90
|
const hubAccessToken = explicitHubToken || (typeof session.accessToken === 'string' && session.accessToken ? session.accessToken : null);
|
|
91
91
|
|
|
92
|
-
const cookie = typeof session.cookie === 'string' && session.cookie ? session.cookie : null;
|
|
93
92
|
const sessionToken = typeof session.sessionToken === 'string' && session.sessionToken ? session.sessionToken : null;
|
|
94
93
|
let expiresAt;
|
|
95
94
|
let accessTokenExpiresAt;
|
|
96
|
-
if (
|
|
97
|
-
expiresAt = [session.
|
|
95
|
+
if (sessionToken) {
|
|
96
|
+
expiresAt = [session.sessionExpiresAt, session.expiresAt]
|
|
98
97
|
.find((v) => typeof v === 'string' && v) || expiresAtFromToken(session.accessToken);
|
|
99
98
|
accessTokenExpiresAt = (typeof session.accessTokenExpiresAt === 'string' && session.accessTokenExpiresAt)
|
|
100
99
|
? session.accessTokenExpiresAt
|
|
@@ -109,7 +108,6 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
109
108
|
accessToken: typeof session.accessToken === 'string' && session.accessToken ? session.accessToken : null,
|
|
110
109
|
expiresAt: expiresAt ?? null,
|
|
111
110
|
accessTokenExpiresAt: accessTokenExpiresAt ?? null,
|
|
112
|
-
cookie,
|
|
113
111
|
sessionToken,
|
|
114
112
|
email: (typeof session.email === 'string' && session.email ? session.email : null)
|
|
115
113
|
|| emailFromHubToken(hubAccessToken),
|
|
@@ -142,7 +140,7 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
142
140
|
try { fsImpl.unlinkSync(tmp); } catch { /* never existed */ }
|
|
143
141
|
throw e;
|
|
144
142
|
}
|
|
145
|
-
recordAccountMarker(
|
|
143
|
+
recordAccountMarker(sessionToken ? 'device' : 'token', env, { fs: fsImpl });
|
|
146
144
|
return target;
|
|
147
145
|
}
|
|
148
146
|
|
|
@@ -156,19 +154,16 @@ function loadAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
156
154
|
}
|
|
157
155
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
158
156
|
if (parsed.version !== SESSION_FILE_VERSION) return null;
|
|
159
|
-
// A usable session needs at least ONE credential: the durable cookie (email/
|
|
160
|
-
// better-auth) or a JWT (Google/legacy, or an email session whose JWT is cached).
|
|
161
|
-
const hasCookie = typeof parsed.cookie === 'string' && parsed.cookie;
|
|
162
157
|
const hasToken = typeof parsed.accessToken === 'string' && parsed.accessToken;
|
|
163
158
|
const sessionToken = typeof parsed.sessionToken === 'string' && parsed.sessionToken ? parsed.sessionToken : null;
|
|
164
|
-
if (
|
|
159
|
+
if (typeof parsed.cookie === 'string' && parsed.cookie && !sessionToken) return null;
|
|
160
|
+
if (!hasToken && !sessionToken) return null;
|
|
165
161
|
const hubAccessToken = typeof parsed.hubAccessToken === 'string' && parsed.hubAccessToken ? parsed.hubAccessToken : null;
|
|
166
162
|
const email = (typeof parsed.email === 'string' && parsed.email ? parsed.email : null) || emailFromHubToken(hubAccessToken);
|
|
167
163
|
return {
|
|
168
164
|
accessToken: hasToken ? parsed.accessToken : null,
|
|
169
165
|
expiresAt: typeof parsed.expiresAt === 'string' ? parsed.expiresAt : null,
|
|
170
166
|
accessTokenExpiresAt: typeof parsed.accessTokenExpiresAt === 'string' ? parsed.accessTokenExpiresAt : null,
|
|
171
|
-
cookie: hasCookie ? parsed.cookie : null,
|
|
172
167
|
sessionToken,
|
|
173
168
|
email,
|
|
174
169
|
hubAccessToken,
|
|
@@ -193,8 +188,7 @@ function clearAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
|
|
|
193
188
|
|
|
194
189
|
// `none | active | expired`, the three states the caller needs to give the Talent a true message.
|
|
195
190
|
function sessionStatus(session, now = Date.now()) {
|
|
196
|
-
|
|
197
|
-
const hasCred = !!(session && [session.accessToken, session.cookie, session.sessionToken].some((v) => typeof v === 'string' && v));
|
|
191
|
+
const hasCred = !!(session && [session.accessToken, session.sessionToken].some((v) => typeof v === 'string' && v));
|
|
198
192
|
if (!hasCred) return 'none';
|
|
199
193
|
const expiry = Date.parse(String(session.expiresAt || ''));
|
|
200
194
|
if (!Number.isFinite(expiry)) return 'expired';
|
package/src/backend-request.js
CHANGED
|
@@ -80,8 +80,7 @@ function postJsonWithTimeout(url, body, timeoutMs, method = 'POST', idempotencyK
|
|
|
80
80
|
raw += chunk;
|
|
81
81
|
if (raw.length > MAX_RESPONSE_BYTES) req.destroy();
|
|
82
82
|
});
|
|
83
|
-
|
|
84
|
-
res.on('end', () => resolve({ status: res.statusCode, raw, headers: res.headers }));
|
|
83
|
+
res.on('end', () => resolve({ status: res.statusCode, raw }));
|
|
85
84
|
},
|
|
86
85
|
);
|
|
87
86
|
req.on('timeout', () => req.destroy(Object.assign(new Error('request timed out'), { kind: 'timeout' })));
|
|
@@ -91,27 +90,6 @@ function postJsonWithTimeout(url, body, timeoutMs, method = 'POST', idempotencyK
|
|
|
91
90
|
});
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
// Pulls one `name=value` cookie pair out of a response's `Set-Cookie` header (Node exposes it as an array, or a single string).
|
|
95
|
-
// Accepts the base cookie name with or without the RFC6265 `__Secure-`/`__Host-`
|
|
96
|
-
// prefixes that better-auth applies over HTTPS, and returns the EXACT server-set
|
|
97
|
-
// name (prefix included) so it can be forwarded back verbatim in a `Cookie:` header.
|
|
98
|
-
function extractSetCookie(headers, name) {
|
|
99
|
-
if (!headers) return null;
|
|
100
|
-
const raw = headers['set-cookie'];
|
|
101
|
-
const list = Array.isArray(raw) ? raw : (typeof raw === 'string' && raw ? [raw] : []);
|
|
102
|
-
const candidates = [name, `__Secure-${name}`, `__Host-${name}`];
|
|
103
|
-
for (const candidate of candidates) {
|
|
104
|
-
const prefix = `${candidate}=`;
|
|
105
|
-
for (const entry of list) {
|
|
106
|
-
if (typeof entry === 'string' && entry.startsWith(prefix)) {
|
|
107
|
-
const value = entry.slice(prefix.length).split(';')[0];
|
|
108
|
-
if (value) return `${candidate}=${value}`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
93
|
function postMultipartWithTimeout(url, fields, timeoutMs, method = 'POST', extraHeaders = null) {
|
|
116
94
|
return new Promise((resolve, reject) => {
|
|
117
95
|
let u;
|
|
@@ -204,7 +182,6 @@ module.exports = {
|
|
|
204
182
|
MAX_RESPONSE_BYTES,
|
|
205
183
|
postJsonWithTimeout,
|
|
206
184
|
postMultipartWithTimeout,
|
|
207
|
-
extractSetCookie,
|
|
208
185
|
requestBackend,
|
|
209
186
|
setClientIdentity,
|
|
210
187
|
};
|
package/src/cli-skill.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SERVER_NAME } = require('./mcp-server-name');
|
|
4
|
+
|
|
5
|
+
const CLI_SKILL_NAME = 'shakers-cli';
|
|
6
|
+
|
|
7
|
+
const OWN_SKILL_NAMES = new Set([CLI_SKILL_NAME, SERVER_NAME]);
|
|
8
|
+
|
|
9
|
+
const CLI_SKILL_DESCRIPTION = 'Use Shakers, the freelance talent marketplace, from the terminal through the `shakers` CLI: find, view and save projects, read the talent profile, certifications, applications and invitations, and get the link to certify a role dimension. Use when the talent wants to work with Shakers from the terminal or mentions the `shakers` command.';
|
|
10
|
+
|
|
11
|
+
const BODY = `# Shakers CLI
|
|
12
|
+
|
|
13
|
+
Shakers is a freelance talent marketplace. The \`shakers\` CLI acts on the talent's own account. You run it for the talent, with their permission.
|
|
14
|
+
|
|
15
|
+
## Rules
|
|
16
|
+
|
|
17
|
+
- Before running any command, show the talent the exact command and ask them first.
|
|
18
|
+
- Run only the \`shakers\` commands listed in this skill. If \`shakers\` is not on PATH, run the same command as \`npx -y shakers-cli <command>\`.
|
|
19
|
+
- Install only the official npm package \`shakers-cli\`. Never install a package with a similar name.
|
|
20
|
+
- Never run \`shakers update\` without \`--check\` unless the talent explicitly asks to update.
|
|
21
|
+
- Put the command name first (\`shakers me --json\`, not \`shakers --json me\`).
|
|
22
|
+
- Prefer \`--json\` and read ids from the JSON. Never pass the \`N)\` numbers of a printed list: a number refers to the last human listing, not to your JSON.
|
|
23
|
+
- If the Shakers MCP server tools are available in this session and the talent did not ask for the terminal, use those tools instead of this skill.
|
|
24
|
+
|
|
25
|
+
## Not logged in
|
|
26
|
+
|
|
27
|
+
A command that needs a session exits 1 and prints on stderr that it requires an active session. Do not try to log in for the talent. Tell them to run \`shakers login\` in their own terminal, then retry the command.
|
|
28
|
+
|
|
29
|
+
Without \`--json\`, each command first prints on stderr one line saying what Shakers does on this computer and in the account (what it reads, changes or sends). Relay that line to the talent word for word.
|
|
30
|
+
|
|
31
|
+
## Commands you may run
|
|
32
|
+
|
|
33
|
+
Projects:
|
|
34
|
+
- \`shakers find-projects --json [--page N] [--limit N] [--tab all|saved] [--attendance remote|hybrid|in-person] [--country ES] [--recommended]\`: projects available to the talent. The id of each item is \`items[].positionId\`.
|
|
35
|
+
- \`shakers show-project <positionId> --json\`: detail of one project.
|
|
36
|
+
- \`shakers save-project <positionId> --json\` and \`shakers unsave-project <positionId> --json\`: add or remove a saved project.
|
|
37
|
+
- \`shakers applications --json\`: projects the talent applied to and their status (\`items[].positionId\`).
|
|
38
|
+
- \`shakers invitations --json\`: the talent's unread project invitations.
|
|
39
|
+
|
|
40
|
+
Profile (read only):
|
|
41
|
+
- \`shakers me --json\`: profile summary.
|
|
42
|
+
- \`shakers rate --json\`, \`shakers availability --json\`, \`shakers lang --json\`, \`shakers socials --json\`.
|
|
43
|
+
- \`shakers experiences --json\`, \`shakers portfolios --json\`.
|
|
44
|
+
|
|
45
|
+
Certifications:
|
|
46
|
+
- \`shakers certifications --json\`: certified, uncertified and expired dimensions.
|
|
47
|
+
- \`shakers certify --dimension <key>\`: only when the talent named the dimension. It prints a link; the interview happens on the web. Show the talent the link. It has no \`--json\`.
|
|
48
|
+
|
|
49
|
+
Account:
|
|
50
|
+
- \`shakers config list\` and \`shakers config get <key>\`: read the CLI configuration.
|
|
51
|
+
- \`shakers update --check --json\`: tells whether a newer version exists. Installs nothing.
|
|
52
|
+
- \`shakers logout\`: only when the talent asks to sign out.
|
|
53
|
+
|
|
54
|
+
## Hand these to the talent
|
|
55
|
+
|
|
56
|
+
Never run these yourself. They need the talent at their own keyboard: they ask for consent, reviews or choices, and \`login\` and \`register\` open a Shakers page on shakersworks.com with a code the talent checks and approves (the password is typed only there, never in a terminal). Tell the talent the exact command to run in their own terminal, or point them to the web (https://www.shakersworks.com) or the Shakers MCP server.
|
|
57
|
+
|
|
58
|
+
- \`shakers register\` and \`shakers onboarding\`: sign-up and the onboarding interview.
|
|
59
|
+
- \`shakers login\`.
|
|
60
|
+
- \`shakers ai-usage\`: the AI-usage analysis and upload. The talent gives consent and reviews what is sent.
|
|
61
|
+
- Profile changes: \`shakers rate --set\`, \`shakers availability --set\`, \`shakers lang --set\`, \`shakers socials --set\`.
|
|
62
|
+
- \`shakers add-agent\`, \`shakers add-skill\`, \`shakers add-project\`, \`shakers add-role\`, \`shakers change-role\`.
|
|
63
|
+
- \`shakers config set\`.
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
function cliSkill() {
|
|
67
|
+
return [
|
|
68
|
+
'---',
|
|
69
|
+
`name: ${CLI_SKILL_NAME}`,
|
|
70
|
+
`description: ${JSON.stringify(CLI_SKILL_DESCRIPTION)}`,
|
|
71
|
+
'---',
|
|
72
|
+
'',
|
|
73
|
+
BODY,
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { cliSkill, CLI_SKILL_NAME, OWN_SKILL_NAMES };
|