shakers-cli 0.17.0 → 0.18.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/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +29 -19
- package/bin/register.js +10 -29
- package/bin/shakers-cli.js +14 -9
- package/package.json +1 -1
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +5 -3
- 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 +47 -4
- package/src/config.js +0 -16
- package/src/consent-flow.js +7 -2
- package/src/detect-repos.js +4 -8
- 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 +162 -88
- package/src/mcp-ai-usage-result-tool.js +6 -4
- package/src/mcp-ai-usage-tool.js +72 -46
- 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 +1 -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 +380 -68
- package/src/skill-install.js +102 -0
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +12 -3
- 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
package/src/onboarding-flow.js
CHANGED
|
@@ -249,7 +249,6 @@ function makeDeps(overrides = {}) {
|
|
|
249
249
|
const ai = require('./ai');
|
|
250
250
|
const inventory = require('./inventory-client');
|
|
251
251
|
const auth = require('./auth-session-store');
|
|
252
|
-
const authClient = require('./auth-client');
|
|
253
252
|
const signup = require('./signup-client');
|
|
254
253
|
const livekit = require('./onboarding-livekit-client');
|
|
255
254
|
return {
|
|
@@ -257,17 +256,14 @@ function makeDeps(overrides = {}) {
|
|
|
257
256
|
sessionStatus: auth.sessionStatus,
|
|
258
257
|
saveAuthSession: auth.saveAuthSession,
|
|
259
258
|
ensureFreshSession: require('./session-refresh').ensureFreshSession,
|
|
260
|
-
getSignUpEndpoint: config.getSignUpEndpoint,
|
|
261
259
|
getCompleteRegistrationEndpoint: config.getCompleteRegistrationEndpoint,
|
|
262
|
-
getLoginEndpoint: config.getLoginEndpoint,
|
|
263
260
|
getAuthTokenEndpoint: config.getAuthTokenEndpoint,
|
|
264
261
|
getDeviceAuthorizeEndpoint: config.getDeviceAuthorizeEndpoint,
|
|
265
262
|
getDeviceTokenEndpoint: config.getDeviceTokenEndpoint,
|
|
266
263
|
runDeviceLogin: require('./device-login').runDeviceLogin,
|
|
267
264
|
requestCompleteRegistration: signup.requestCompleteRegistration,
|
|
268
265
|
buildRegistrationContext: signup.buildRegistrationContext,
|
|
269
|
-
|
|
270
|
-
requestLogin: authClient.requestLogin,
|
|
266
|
+
buildRegistrationConsent: signup.buildRegistrationConsent,
|
|
271
267
|
getImportProfileEndpoint: config.getImportProfileEndpoint,
|
|
272
268
|
getProfessionalDetailsEndpoint: config.getProfessionalDetailsEndpoint,
|
|
273
269
|
getPricingRateEndpoint: config.getPricingRateEndpoint,
|
|
@@ -305,7 +301,6 @@ function makeDeps(overrides = {}) {
|
|
|
305
301
|
};
|
|
306
302
|
}
|
|
307
303
|
|
|
308
|
-
// Refreshes the email session's hub JWT from its stored cookie when the cached one has lapsed (session-refresh.js), then gates on the durable session status.
|
|
309
304
|
async function requireActiveSession(deps) {
|
|
310
305
|
const session = typeof deps.ensureFreshSession === 'function'
|
|
311
306
|
? await deps.ensureFreshSession(process.env, { loadAuthSession: deps.loadAuthSession, saveAuthSession: deps.saveAuthSession })
|
|
@@ -316,79 +311,26 @@ async function requireActiveSession(deps) {
|
|
|
316
311
|
return { ok: true, session };
|
|
317
312
|
}
|
|
318
313
|
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function signUp(deps, fields = {}) {
|
|
324
|
-
// The CLI no longer asks freelanceType at signup → transient default (corrected later from the work situation via professional-details).
|
|
325
|
-
if (!isAcceptablePassword(fields.password)) return { ok: false, reason: 'weak-password' };
|
|
326
|
-
const signUpEndpoint = deps.getSignUpEndpoint();
|
|
327
|
-
if (!signUpEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
328
|
-
const completeRegistrationEndpoint = typeof deps.getCompleteRegistrationEndpoint === 'function'
|
|
329
|
-
? deps.getCompleteRegistrationEndpoint()
|
|
330
|
-
: null;
|
|
331
|
-
return deps.requestSignUp(
|
|
332
|
-
{
|
|
333
|
-
name: fields.name,
|
|
334
|
-
lastName: fields.lastName,
|
|
335
|
-
email: fields.email,
|
|
336
|
-
password: fields.password,
|
|
337
|
-
preferredLanguage: fields.preferredLanguage,
|
|
338
|
-
newsletterConsent: fields.newsletterConsent === true,
|
|
339
|
-
freelanceType: normalizeFreelanceType(fields.freelanceType) || DEFAULT_SIGNUP_FREELANCE_TYPE,
|
|
340
|
-
freelanceIntent: fields.freelanceIntent,
|
|
341
|
-
claimCode: fields.claimCode || null,
|
|
342
|
-
},
|
|
343
|
-
{ signUpEndpoint, completeRegistrationEndpoint },
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function establishSessionFromCredentials(deps, { email, password } = {}) {
|
|
348
|
-
const signInEndpoint = deps.getLoginEndpoint();
|
|
349
|
-
if (!signInEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
350
|
-
const tokenEndpoint = typeof deps.getAuthTokenEndpoint === 'function' ? deps.getAuthTokenEndpoint() : null;
|
|
351
|
-
const result = await deps.requestLogin({ email, password }, { signInEndpoint, tokenEndpoint });
|
|
352
|
-
if (!result.ok) return { ok: false, reason: result.reason || 'login-failed', status: result.status || null };
|
|
353
|
-
try {
|
|
354
|
-
// For an email session the durable credential is the cookie; `accessToken` is
|
|
355
|
-
// the fresh JWT and `expiresAt` its own exp (stored as the refresh horizon).
|
|
356
|
-
deps.saveAuthSession({
|
|
357
|
-
accessToken: result.accessToken,
|
|
358
|
-
expiresAt: result.expiresAt,
|
|
359
|
-
accessTokenExpiresAt: result.expiresAt,
|
|
360
|
-
cookie: result.cookie || null,
|
|
361
|
-
cookieExpiresAt: result.cookieExpiresAt || null,
|
|
362
|
-
email: result.email || email || null,
|
|
363
|
-
hubAccessToken: result.hubAccessToken || null,
|
|
364
|
-
});
|
|
365
|
-
} catch {
|
|
366
|
-
return { ok: false, reason: 'session-persist-failed' };
|
|
367
|
-
}
|
|
368
|
-
return { ok: true };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Register a talent via a social provider (device flow, RFC 8628); a live session results either way.
|
|
372
|
-
async function registerWithSocialAccount(deps, { provider = 'google', freelanceType, language } = {}, { openBrowser, onAuthUrl } = {}) {
|
|
373
|
-
const { appendProviderParam, deviceSessionRecord } = require('./device-login');
|
|
314
|
+
async function registerWithDeviceFlow(deps, { provider = 'email', freelanceType, language, newsletterConsent = false } = {}, { openBrowser, onAuthUrl } = {}) {
|
|
315
|
+
const { withDeviceParams, deviceSessionRecord } = require('./device-login');
|
|
374
316
|
const authorizeEndpoint = deps.getDeviceAuthorizeEndpoint();
|
|
375
317
|
const tokenEndpoint = deps.getDeviceTokenEndpoint();
|
|
376
318
|
const authTokenEndpoint = typeof deps.getAuthTokenEndpoint === 'function' ? deps.getAuthTokenEndpoint() : null;
|
|
377
319
|
const completeRegistrationEndpoint = typeof deps.getCompleteRegistrationEndpoint === 'function' ? deps.getCompleteRegistrationEndpoint() : null;
|
|
378
320
|
if (!authorizeEndpoint || !tokenEndpoint || !authTokenEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
379
321
|
|
|
380
|
-
// freelanceType is a transient default (corrected later); feeds complete-registration for a new account.
|
|
381
322
|
const registrationContext = deps.buildRegistrationContext({
|
|
382
323
|
preferredLanguage: language,
|
|
383
|
-
newsletterConsent:
|
|
324
|
+
newsletterConsent: newsletterConsent === true,
|
|
384
325
|
freelanceType: normalizeFreelanceType(freelanceType) || DEFAULT_SIGNUP_FREELANCE_TYPE,
|
|
385
326
|
});
|
|
327
|
+
const consent = deps.buildRegistrationConsent({ newsletterConsent: newsletterConsent === true });
|
|
386
328
|
|
|
387
329
|
const result = await deps.runDeviceLogin(
|
|
388
|
-
{ authorizeEndpoint, tokenEndpoint, authTokenEndpoint, completeRegistrationEndpoint, registrationContext },
|
|
330
|
+
{ authorizeEndpoint, tokenEndpoint, authTokenEndpoint, completeRegistrationEndpoint, registrationContext, consent },
|
|
389
331
|
{
|
|
390
332
|
onPrompt: ({ verificationUriComplete, userCode }) => {
|
|
391
|
-
const url =
|
|
333
|
+
const url = withDeviceParams(verificationUriComplete, { provider, mode: 'register', surface: 'cli' });
|
|
392
334
|
if (typeof onAuthUrl === 'function') onAuthUrl(url, userCode);
|
|
393
335
|
if (typeof openBrowser === 'function') { try { openBrowser(url); } catch { /* printed URL covers it */ } }
|
|
394
336
|
},
|
|
@@ -402,7 +344,6 @@ async function registerWithSocialAccount(deps, { provider = 'google', freelanceT
|
|
|
402
344
|
return { ok: false, reason: 'session-persist-failed' };
|
|
403
345
|
}
|
|
404
346
|
|
|
405
|
-
// isNewUser distinguishes a fresh registration from signing an existing account in.
|
|
406
347
|
return { ok: true, kind: result.isNewUser ? 'registered' : 'account-already-exists', email: result.email || null };
|
|
407
348
|
}
|
|
408
349
|
|
|
@@ -697,58 +638,27 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
|
|
|
697
638
|
return { ok: false, reason, step: 'signup' };
|
|
698
639
|
};
|
|
699
640
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
notifyOk(io, c.accountReady);
|
|
720
|
-
} else {
|
|
721
|
-
const account = await io.askSignUp(c);
|
|
722
|
-
if (!account || !account.email) return accountFailed('missing-fields');
|
|
723
|
-
if (!isAcceptablePassword(account.password)) {
|
|
724
|
-
io.error(c.weakPassword);
|
|
725
|
-
return accountFailed('weak-password');
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const su = await signUp(deps, { ...account, preferredLanguage: account.preferredLanguage || io.lang });
|
|
729
|
-
if (!su.ok) {
|
|
730
|
-
io.error(su.message || c.signupError);
|
|
731
|
-
return accountFailed(su.reason);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (su.accountExists) {
|
|
735
|
-
// Existing account: sign them in, then STOP — no wizard re-run.
|
|
736
|
-
io.warn(c.accountExists);
|
|
737
|
-
const logged = await io.login(account.email);
|
|
738
|
-
if (!logged) return accountFailed('login-failed');
|
|
739
|
-
funnel.completed('account_creation', { auth_method: 'email', auth_attempts: 1, is_returning_account: true });
|
|
740
|
-
funnel.identified({ newAccount: false });
|
|
741
|
-
return { ok: true, alreadyRegistered: true };
|
|
742
|
-
}
|
|
743
|
-
const sess = await establishSessionFromCredentials(deps, { email: account.email, password: account.password });
|
|
744
|
-
if (!sess.ok) {
|
|
745
|
-
io.error(c.sessionError);
|
|
746
|
-
return accountFailed(sess.reason);
|
|
747
|
-
}
|
|
748
|
-
funnel.completed('account_creation', { auth_method: 'email', auth_attempts: 1, is_returning_account: false });
|
|
749
|
-
funnel.identified();
|
|
750
|
-
notifyOk(io, c.accountReady);
|
|
641
|
+
const terms = await io.askTerms(c);
|
|
642
|
+
if (!terms.accepted) {
|
|
643
|
+
io.warn(c.termsDeclined);
|
|
644
|
+
return accountFailed('terms-declined');
|
|
645
|
+
}
|
|
646
|
+
const reg = await registerWithDeviceFlow(
|
|
647
|
+
deps,
|
|
648
|
+
{ provider: method, language: io.lang, newsletterConsent: terms.newsletterConsent },
|
|
649
|
+
{ openBrowser, onAuthUrl },
|
|
650
|
+
);
|
|
651
|
+
if (!reg.ok) {
|
|
652
|
+
io.error(c.signupError);
|
|
653
|
+
return accountFailed(reg.reason);
|
|
654
|
+
}
|
|
655
|
+
funnel.completed('account_creation', { auth_method: method, auth_attempts: 1, is_returning_account: reg.kind === 'account-already-exists' });
|
|
656
|
+
funnel.identified({ newAccount: reg.kind !== 'account-already-exists' });
|
|
657
|
+
if (reg.kind === 'account-already-exists') {
|
|
658
|
+
io.warn(c.accountExists);
|
|
659
|
+
return { ok: true, alreadyRegistered: true };
|
|
751
660
|
}
|
|
661
|
+
notifyOk(io, c.accountReady);
|
|
752
662
|
|
|
753
663
|
io.section(c.title);
|
|
754
664
|
funnel.viewed('import_sources', { sources_offered: ['linkedin', 'cv', 'github', 'web_page'] });
|
|
@@ -1044,15 +954,13 @@ async function runMainRoleFinalStep(io, deps, { onRolesShown } = {}) {
|
|
|
1044
954
|
}
|
|
1045
955
|
}
|
|
1046
956
|
|
|
1047
|
-
// Overwrite-repeat: PATCH /interviews/onboarding/restart.
|
|
1048
|
-
// the talent hub JWT (same token roles use); falls back to accessToken when equal.
|
|
957
|
+
// Overwrite-repeat: PATCH /interviews/onboarding/restart. Candidate certs JWT, like its onboarding siblings.
|
|
1049
958
|
async function restartOnboarding(deps) {
|
|
1050
959
|
const gate = await requireActiveSession(deps);
|
|
1051
960
|
if (!gate.ok) return gate;
|
|
1052
961
|
const endpoint = deps.getOnboardingRestartEndpoint();
|
|
1053
962
|
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
1054
|
-
|
|
1055
|
-
return deps.requestRestartOnboardingInterview({ accessToken: token }, { endpoint });
|
|
963
|
+
return deps.requestRestartOnboardingInterview({ accessToken: gate.session.accessToken }, { endpoint });
|
|
1056
964
|
}
|
|
1057
965
|
|
|
1058
966
|
// `shakers onboarding --repeat`: warn + explicit confirm, then overwrite the prior
|
|
@@ -1144,7 +1052,6 @@ module.exports = {
|
|
|
1144
1052
|
validatePricing,
|
|
1145
1053
|
validateAvailability,
|
|
1146
1054
|
buildPricingUpsert,
|
|
1147
|
-
isAcceptablePassword,
|
|
1148
1055
|
deriveCurrentEmploymentStatus,
|
|
1149
1056
|
showsFreelanceOpinion,
|
|
1150
1057
|
deriveFreelanceIntent,
|
|
@@ -1153,9 +1060,7 @@ module.exports = {
|
|
|
1153
1060
|
validateLanguageRows,
|
|
1154
1061
|
validatePhone,
|
|
1155
1062
|
makeDeps,
|
|
1156
|
-
|
|
1157
|
-
establishSessionFromCredentials,
|
|
1158
|
-
registerWithSocialAccount,
|
|
1063
|
+
registerWithDeviceFlow,
|
|
1159
1064
|
startProfileImport,
|
|
1160
1065
|
reattachConsentIdentity,
|
|
1161
1066
|
saveWorkSituation,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getHomeDir } = require('./env-paths');
|
|
6
|
+
|
|
7
|
+
function loadSqlite() {
|
|
8
|
+
const emitWarning = process.emitWarning;
|
|
9
|
+
process.emitWarning = (warning, ...rest) => {
|
|
10
|
+
const type = typeof rest[0] === 'string' ? rest[0] : rest[0] && rest[0].type;
|
|
11
|
+
if (type === 'ExperimentalWarning' && /sqlite/i.test(String(warning))) return;
|
|
12
|
+
emitWarning.call(process, warning, ...rest);
|
|
13
|
+
};
|
|
14
|
+
try {
|
|
15
|
+
return require('node:sqlite');
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
} finally {
|
|
19
|
+
process.emitWarning = emitWarning;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function opencodeDbPath(env = process.env) {
|
|
24
|
+
const dataDir = env.XDG_DATA_HOME
|
|
25
|
+
? path.join(env.XDG_DATA_HOME, 'opencode')
|
|
26
|
+
: path.join(getHomeDir(env), '.local', 'share', 'opencode');
|
|
27
|
+
if (env.OPENCODE_DB) return path.isAbsolute(env.OPENCODE_DB) ? env.OPENCODE_DB : path.join(dataDir, env.OPENCODE_DB);
|
|
28
|
+
return path.join(dataDir, 'opencode.db');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SESSIONS_SQL = 'SELECT id, directory, parent_id AS parentId, agent FROM session ORDER BY time_created DESC LIMIT ?';
|
|
32
|
+
const MESSAGES_SQL = `SELECT id, time_created AS createdAt, json_extract(data, '$.role') AS role, json_extract(data, '$.agent') AS agent
|
|
33
|
+
FROM message WHERE session_id = ? ORDER BY time_created, id`;
|
|
34
|
+
const PARTS_SQL = `SELECT p.message_id AS messageId, json_extract(p.data, '$.type') AS type, json_extract(p.data, '$.text') AS text,
|
|
35
|
+
json_extract(p.data, '$.tool') AS tool, json_extract(p.data, '$.synthetic') AS synthetic
|
|
36
|
+
FROM part p JOIN message m ON m.id = p.message_id WHERE m.session_id = ? ORDER BY p.id`;
|
|
37
|
+
|
|
38
|
+
function isoOf(ms) {
|
|
39
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionObjects(db, session) {
|
|
43
|
+
const parts = new Map();
|
|
44
|
+
for (const p of db.prepare(PARTS_SQL).all(session.id)) {
|
|
45
|
+
if (!parts.has(p.messageId)) parts.set(p.messageId, []);
|
|
46
|
+
parts.get(p.messageId).push(p);
|
|
47
|
+
}
|
|
48
|
+
const head = {};
|
|
49
|
+
if (typeof session.directory === 'string' && session.directory) head.cwd = session.directory;
|
|
50
|
+
if (session.agent === 'plan') head.planMode = true;
|
|
51
|
+
const objects = [head];
|
|
52
|
+
for (const m of db.prepare(MESSAGES_SQL).all(session.id)) {
|
|
53
|
+
const timestamp = isoOf(Number(m.createdAt));
|
|
54
|
+
const own = parts.get(m.id) || [];
|
|
55
|
+
if (m.agent === 'plan') head.planMode = true;
|
|
56
|
+
if (m.role === 'user') {
|
|
57
|
+
const text = own.filter((p) => p.type === 'text' && !p.synthetic && typeof p.text === 'string').map((p) => p.text).join('').trim();
|
|
58
|
+
if (text) {
|
|
59
|
+
objects.push({ type: 'user', timestamp, message: { role: 'user', content: [{ type: 'text', text }] } });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const uses = own.filter((p) => p.type === 'tool' && typeof p.tool === 'string' && p.tool).map((p) => ({ type: 'tool_use', name: p.tool }));
|
|
64
|
+
const o = timestamp ? { timestamp } : {};
|
|
65
|
+
if (uses.length) o.message = { role: 'assistant', content: uses };
|
|
66
|
+
if (o.timestamp || o.message) objects.push(o);
|
|
67
|
+
}
|
|
68
|
+
return objects;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readOpencodeSessions(env, limit) {
|
|
72
|
+
const file = opencodeDbPath(env);
|
|
73
|
+
if (!fs.existsSync(file)) return [];
|
|
74
|
+
const sqlite = loadSqlite();
|
|
75
|
+
const Database = sqlite && (sqlite.DatabaseSync || sqlite.Database);
|
|
76
|
+
if (!Database) return [];
|
|
77
|
+
let db;
|
|
78
|
+
try {
|
|
79
|
+
db = new Database(file, { readOnly: true });
|
|
80
|
+
return db.prepare(SESSIONS_SQL).all(limit).map((session) => ({
|
|
81
|
+
file: `${file}#${session.id}`,
|
|
82
|
+
objects: sessionObjects(db, session),
|
|
83
|
+
subagent: Boolean(session.parentId),
|
|
84
|
+
}));
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
} finally {
|
|
88
|
+
if (db) db.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { readOpencodeSessions, opencodeDbPath };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requestListAgents } = require('./agents-client');
|
|
4
|
+
const { getAgentsListEndpoint } = require('./config');
|
|
5
|
+
const { ensureFreshSession } = require('./session-refresh');
|
|
6
|
+
|
|
7
|
+
const TALENT_ADDED_SOURCES = new Set(['DECLARED_BY_TALENT', 'CLI_DETECTED']);
|
|
8
|
+
|
|
9
|
+
async function fetchProfileAgentNames(env = process.env, deps = {}) {
|
|
10
|
+
const {
|
|
11
|
+
refresh = ensureFreshSession,
|
|
12
|
+
list = requestListAgents,
|
|
13
|
+
endpoint = getAgentsListEndpoint(env),
|
|
14
|
+
} = deps;
|
|
15
|
+
try {
|
|
16
|
+
const session = await refresh(env);
|
|
17
|
+
if (!session || !session.hubAccessToken) return [];
|
|
18
|
+
const listed = await list({ accessToken: session.accessToken, hubAccessToken: session.hubAccessToken }, { endpoint });
|
|
19
|
+
if (!listed || !listed.ok || !Array.isArray(listed.agents)) return [];
|
|
20
|
+
return listed.agents.filter((a) => a && TALENT_ADDED_SOURCES.has(a.source) && a.name).map((a) => a.name);
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { fetchProfileAgentNames, TALENT_ADDED_SOURCES };
|
|
@@ -27,10 +27,10 @@ async function runToggleSave({ io, rawOut, session, catalog, mode, opts = {}, de
|
|
|
27
27
|
}
|
|
28
28
|
const positionId = resolved.id;
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const res = await io.withProgress(label, () => (mode === 'save'
|
|
30
|
+
const toggle = () => (mode === 'save'
|
|
32
31
|
? deps.savePosition({ hubAccessToken, positionId })
|
|
33
|
-
: deps.unsavePosition({ hubAccessToken, positionId }))
|
|
32
|
+
: deps.unsavePosition({ hubAccessToken, positionId }));
|
|
33
|
+
const res = opts.json ? await toggle() : await io.withProgress(mode === 'save' ? fp.saving : fp.unsaving, toggle);
|
|
34
34
|
if (!res.ok) {
|
|
35
35
|
io.error(res.reason === 'not-found' ? fp.notFound : fp.failed(res.reason));
|
|
36
36
|
return { ok: false, reason: res.reason };
|
package/src/scanner.js
CHANGED
|
@@ -12,7 +12,8 @@ const { detectMcpServers, buildMcpAggregate } = require('./mcp-detector');
|
|
|
12
12
|
const { analyzeMemoryStructure } = require('./memory-structure-detector');
|
|
13
13
|
const { detectAutomations } = require('./automations-detector');
|
|
14
14
|
const { detectBrowserTools } = require('./browser-tools-detector');
|
|
15
|
-
const { getHomeDir } = require('./env-paths');
|
|
15
|
+
const { getHomeDir, codexHome, piAgentDir } = require('./env-paths');
|
|
16
|
+
const { OWN_SKILL_NAMES } = require('./cli-skill');
|
|
16
17
|
|
|
17
18
|
/* ---------- existence-check utilities (existence only, never content) ---------- */
|
|
18
19
|
|
|
@@ -85,14 +86,30 @@ function resolveSignalPath(sig, root) {
|
|
|
85
86
|
|
|
86
87
|
/* ---------- DEPTH probes: return ONLY numbers ---------- */
|
|
87
88
|
|
|
88
|
-
function
|
|
89
|
+
function countSkillDirs(p) {
|
|
89
90
|
try {
|
|
90
|
-
return fs.readdirSync(p, { withFileTypes: true })
|
|
91
|
+
return fs.readdirSync(p, { withFileTypes: true })
|
|
92
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !OWN_SKILL_NAMES.has(e.name)).length;
|
|
91
93
|
} catch {
|
|
92
94
|
return 0;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function countFilesDeep(p, ext, depth = 3) {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(p, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
let n = 0;
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (e.isFile() && e.name.endsWith(ext)) n += 1;
|
|
108
|
+
else if (e.isDirectory() && depth > 0) n += countFilesDeep(path.join(p, e.name), ext, depth - 1);
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
function countFiles(p, ext) {
|
|
97
114
|
try {
|
|
98
115
|
return fs.readdirSync(p).filter((f) => (ext ? f.endsWith(ext) : true)).length;
|
|
@@ -113,6 +130,49 @@ function countJsonKeys(file, key) {
|
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
function claudeHooks(root) {
|
|
134
|
+
return (
|
|
135
|
+
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
136
|
+
countJsonKeys(path.join(root, '.claude', 'settings.local.json'), 'hooks') +
|
|
137
|
+
countJsonKeys(path.join(getHomeDir(), '.claude', 'settings.json'), 'hooks')
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const TOML_HOOK_TABLE = /^\s*\[\[?\s*hooks\.([A-Za-z]+)/;
|
|
142
|
+
|
|
143
|
+
function countTomlHookEvents(file) {
|
|
144
|
+
let raw;
|
|
145
|
+
try {
|
|
146
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
147
|
+
} catch {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const events = new Set();
|
|
151
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
152
|
+
const m = TOML_HOOK_TABLE.exec(line);
|
|
153
|
+
if (m && m[1] !== 'state') events.add(m[1]);
|
|
154
|
+
}
|
|
155
|
+
return events.size;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function codexHooks(root) {
|
|
159
|
+
return [codexHome(), path.join(root, '.codex')].reduce(
|
|
160
|
+
(n, dir) => n + countJsonKeys(path.join(dir, 'hooks.json'), 'hooks') + countTomlHookEvents(path.join(dir, 'config.toml')),
|
|
161
|
+
0,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const CLINE_HOOK_TYPES = new Set(['TaskStart', 'TaskResume', 'TaskCancel', 'TaskComplete', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Notification', 'PreCompact']);
|
|
166
|
+
|
|
167
|
+
function countClineHooks(dir) {
|
|
168
|
+
try {
|
|
169
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
170
|
+
.filter((e) => e.isFile() && CLINE_HOOK_TYPES.has(path.parse(e.name).name)).length;
|
|
171
|
+
} catch {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
116
176
|
/* ---------- config footprint: ONLY size in bytes and file count ---------- */
|
|
117
177
|
/* No file name or path is ever stored: only numbers are aggregated. */
|
|
118
178
|
|
|
@@ -273,17 +333,20 @@ const probes = {
|
|
|
273
333
|
'claude-code': (root, ctx) => ({
|
|
274
334
|
mcpServers: mcpCount(ctx, 'claude-code'),
|
|
275
335
|
skills:
|
|
276
|
-
|
|
277
|
-
|
|
336
|
+
countSkillDirs(path.join(root, '.claude', 'skills')) +
|
|
337
|
+
countSkillDirs(path.join(getHomeDir(), '.claude', 'skills')),
|
|
278
338
|
commands: countFiles(path.join(root, '.claude', 'commands'), '.md'),
|
|
279
339
|
instructions: exists(path.join(root, 'CLAUDE.md')) ? 1 : 0,
|
|
280
|
-
hooks:
|
|
340
|
+
hooks: claudeHooks(root),
|
|
281
341
|
}),
|
|
282
342
|
cursor: (root, ctx) => ({
|
|
283
343
|
rules:
|
|
284
344
|
countFiles(path.join(root, '.cursor', 'rules'), '.mdc') +
|
|
285
345
|
(exists(path.join(root, '.cursorrules')) ? 1 : 0),
|
|
286
346
|
mcpServers: mcpCount(ctx, 'cursor'),
|
|
347
|
+
hooks:
|
|
348
|
+
countJsonKeys(path.join(root, '.cursor', 'hooks.json'), 'hooks') +
|
|
349
|
+
countJsonKeys(path.join(getHomeDir(), '.cursor', 'hooks.json'), 'hooks'),
|
|
287
350
|
}),
|
|
288
351
|
'github-copilot': (root) => ({
|
|
289
352
|
instructions: exists(path.join(root, '.github', 'copilot-instructions.md')) ? 1 : 0,
|
|
@@ -307,10 +370,41 @@ const probes = {
|
|
|
307
370
|
'gemini-cli': (root, ctx) => ({
|
|
308
371
|
instructions: exists(path.join(root, 'GEMINI.md')) ? 1 : 0,
|
|
309
372
|
mcpServers: mcpCount(ctx, 'gemini-cli'),
|
|
373
|
+
commands:
|
|
374
|
+
countFilesDeep(path.join(root, '.gemini', 'commands'), '.toml') +
|
|
375
|
+
countFilesDeep(path.join(getHomeDir(), '.gemini', 'commands'), '.toml'),
|
|
376
|
+
hooks:
|
|
377
|
+
countJsonKeys(path.join(root, '.gemini', 'settings.json'), 'hooks') +
|
|
378
|
+
countJsonKeys(path.join(getHomeDir(), '.gemini', 'settings.json'), 'hooks'),
|
|
310
379
|
}),
|
|
311
|
-
'codex-cli': (root) => ({
|
|
380
|
+
'codex-cli': (root, ctx) => ({
|
|
312
381
|
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
382
|
+
mcpServers: mcpCount(ctx, 'codex-cli'),
|
|
383
|
+
skills: countSkillDirs(path.join(codexHome(), 'skills')),
|
|
384
|
+
commands: countFiles(path.join(codexHome(), 'prompts'), '.md'),
|
|
385
|
+
hooks: codexHooks(root),
|
|
313
386
|
}),
|
|
387
|
+
cline: (root) => ({
|
|
388
|
+
hooks:
|
|
389
|
+
countClineHooks(path.join(root, '.clinerules', 'hooks')) +
|
|
390
|
+
countClineHooks(path.join(getHomeDir(), 'Documents', 'Cline', 'Hooks')),
|
|
391
|
+
}),
|
|
392
|
+
pi: (root, ctx) => ({
|
|
393
|
+
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
394
|
+
mcpServers: mcpCount(ctx, 'pi'),
|
|
395
|
+
skills: countSkillDirs(path.join(root, '.pi', 'skills')) + countSkillDirs(path.join(piAgentDir(), 'skills')),
|
|
396
|
+
commands: countFiles(path.join(root, '.pi', 'prompts'), '.md') + countFiles(path.join(piAgentDir(), 'prompts'), '.md'),
|
|
397
|
+
}),
|
|
398
|
+
opencode: (root, ctx) => {
|
|
399
|
+
const home = path.join(getHomeDir(), '.config', 'opencode');
|
|
400
|
+
const dirs = [path.join(root, '.opencode'), home];
|
|
401
|
+
return {
|
|
402
|
+
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
403
|
+
mcpServers: mcpCount(ctx, 'opencode'),
|
|
404
|
+
skills: dirs.reduce((n, d) => n + countSkillDirs(path.join(d, 'skills')) + countSkillDirs(path.join(d, 'skill')), 0),
|
|
405
|
+
commands: dirs.reduce((n, d) => n + countFiles(path.join(d, 'commands'), '.md') + countFiles(path.join(d, 'command'), '.md'), 0),
|
|
406
|
+
};
|
|
407
|
+
},
|
|
314
408
|
trae: (root) => ({
|
|
315
409
|
// Low confidence: `.trae/rules` structure not verified (see detectors.js).
|
|
316
410
|
rules: countFiles(path.join(root, '.trae', 'rules')),
|
|
@@ -325,16 +419,14 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
|
325
419
|
return {
|
|
326
420
|
agents: agentsCount,
|
|
327
421
|
skills:
|
|
328
|
-
|
|
329
|
-
|
|
422
|
+
countSkillDirs(path.join(root, '.claude', 'skills')) +
|
|
423
|
+
countSkillDirs(path.join(home, '.claude', 'skills')),
|
|
330
424
|
commands:
|
|
331
425
|
countFiles(path.join(root, '.claude', 'commands'), '.md') +
|
|
332
426
|
countFiles(path.join(home, '.claude', 'commands'), '.md'),
|
|
333
427
|
// The third copy of the MCP paths lived here.
|
|
334
428
|
mcpServers: (mcp && typeof mcp.total === 'number' ? mcp.total : 0),
|
|
335
|
-
hooks:
|
|
336
|
-
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
337
|
-
countJsonKeys(path.join(home, '.claude', 'settings.json'), 'hooks'),
|
|
429
|
+
hooks: claudeHooks(root),
|
|
338
430
|
};
|
|
339
431
|
}
|
|
340
432
|
|
package/src/session-gate.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Whether a non-exempt command may run: an active session, or a durable cookie
|
|
4
|
-
// that can mint a fresh hub JWT right now. Reject only when neither holds.
|
|
5
|
-
|
|
6
3
|
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
7
4
|
const { ensureFreshSession } = require('./session-refresh');
|
|
8
5
|
|
|
@@ -20,7 +17,7 @@ async function hasUsableSession(env = process.env, deps = {}) {
|
|
|
20
17
|
|
|
21
18
|
const session = load(env);
|
|
22
19
|
if (sessionStatus(session, now) === 'active') return true;
|
|
23
|
-
if (!session ||
|
|
20
|
+
if (!session || !session.sessionToken) return false;
|
|
24
21
|
|
|
25
22
|
let refreshed = null;
|
|
26
23
|
try { refreshed = await refresh(env, { now }); } catch { refreshed = null; }
|