shakers-cli 0.15.0 → 0.17.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.
Files changed (132) hide show
  1. package/README.md +56 -1203
  2. package/bin/ai-usage.js +32 -58
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +17 -12
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +19 -5
  8. package/bin/report-html.js +2 -2
  9. package/bin/shakers-cli.js +352 -0
  10. package/bin/shakers.js +20 -344
  11. package/bin/share.js +1 -1
  12. package/bin/superadmin.js +5 -5
  13. package/bin/version.js +3 -0
  14. package/package.json +12 -3
  15. package/src/account-marker.js +40 -0
  16. package/src/account-trace.js +23 -0
  17. package/src/agent-category.js +1 -1
  18. package/src/agent-evaluation.js +3 -3
  19. package/src/agent-org-chart.js +5 -5
  20. package/src/agent-synthesis.js +3 -3
  21. package/src/agent-usage.js +1 -1
  22. package/src/agents-client.js +4 -4
  23. package/src/ai-usage-review.js +101 -0
  24. package/src/ai.js +1 -1
  25. package/src/ansi.js +1 -1
  26. package/src/auth-client.js +18 -4
  27. package/src/auth-method-picker.js +1 -1
  28. package/src/auth-methods.js +1 -1
  29. package/src/auth-session-store.js +18 -8
  30. package/src/authorship-mismatch.js +1 -1
  31. package/src/authorship.js +2 -2
  32. package/src/automations-detector.js +1 -1
  33. package/src/backend-request.js +5 -3
  34. package/src/brand-ansi.js +1 -1
  35. package/src/browser-tools-detector.js +1 -1
  36. package/src/certify-disclaimer.js +1 -1
  37. package/src/certify-remediation-prompt.js +1 -1
  38. package/src/cli-args.js +2 -2
  39. package/src/command-audit.js +8 -5
  40. package/src/config-dir.js +1 -1
  41. package/src/config.js +114 -48
  42. package/src/consent-flow.js +7 -6
  43. package/src/consent-skip.js +1 -1
  44. package/src/detection-evidence.js +2 -2
  45. package/src/detection-noise.js +1 -1
  46. package/src/detectors.js +1 -1
  47. package/src/device-login.js +12 -1
  48. package/src/email-verification.js +3 -3
  49. package/src/env-paths.js +1 -1
  50. package/src/find-projects-client.js +12 -0
  51. package/src/find-projects-flow.js +1 -13
  52. package/src/git-activity.js +1 -1
  53. package/src/graph-certs.js +3 -3
  54. package/src/graph-scan.js +9 -9
  55. package/src/i18n.js +148 -138
  56. package/src/interactive-select.js +1 -1
  57. package/src/interview-client.js +6 -1
  58. package/src/interview-transcript.js +1 -1
  59. package/src/legal-notice.js +1 -1
  60. package/src/maturity.js +6 -6
  61. package/src/mcp-ai-usage-result-tool.js +34 -12
  62. package/src/mcp-ai-usage-tool.js +282 -163
  63. package/src/mcp-auth-form.js +7 -3
  64. package/src/mcp-detector.js +3 -3
  65. package/src/mcp-device-login-tools.js +2 -1
  66. package/src/mcp-find-projects-tools.js +1 -2
  67. package/src/mcp-install.js +279 -44
  68. package/src/mcp-instructions.js +1 -1
  69. package/src/mcp-onboarding-tools.js +4 -4
  70. package/src/mcp-prompts.js +4 -4
  71. package/src/mcp-register-tools.js +1 -0
  72. package/src/mcp-role-tools.js +24 -1
  73. package/src/mcp-server.js +5 -1
  74. package/src/mcp-signup-client.js +2 -2
  75. package/src/mcp-signup-tools.js +39 -9
  76. package/src/memory-structure-detector.js +1 -1
  77. package/src/model-call-record.js +2 -2
  78. package/src/onboarding-flow.js +63 -42
  79. package/src/onboarding-funnel.js +153 -20
  80. package/src/open-file.js +1 -1
  81. package/src/osc-link.js +1 -1
  82. package/src/portfolio-client.js +11 -1
  83. package/src/portfolio-git-info.js +1 -1
  84. package/src/profile-flow.js +1 -1
  85. package/src/profile-switch.js +5 -12
  86. package/src/render-certification.js +10 -10
  87. package/src/render-html.js +6 -6
  88. package/src/render-report-text.js +1 -1
  89. package/src/render-sheet.js +10 -10
  90. package/src/render-terminal.js +6 -6
  91. package/src/repo-scope.js +132 -0
  92. package/src/report-gating.js +5 -3
  93. package/src/report-store.js +13 -8
  94. package/src/roadmap-content.js +5 -5
  95. package/src/roadmap-personalization.js +3 -3
  96. package/src/roadmap-prompt.js +2 -2
  97. package/src/roles-flow.js +8 -5
  98. package/src/scan-exclusions.js +1 -1
  99. package/src/scanner.js +14 -14
  100. package/src/secret-prompt.js +1 -1
  101. package/src/session-gate.js +1 -1
  102. package/src/session-refresh.js +33 -3
  103. package/src/session-scan.js +1 -1
  104. package/src/shakers-wordmark.js +1 -1
  105. package/src/share-card.js +2 -2
  106. package/src/share-certification.js +4 -4
  107. package/src/share-consent-state.js +9 -9
  108. package/src/share-payload.js +7 -7
  109. package/src/share.js +7 -7
  110. package/src/signup-client.js +1 -1
  111. package/src/signup-copy.js +177 -2
  112. package/src/skills-client.js +1 -1
  113. package/src/start-add-agents.js +2 -2
  114. package/src/start-add-portfolio.js +3 -13
  115. package/src/start-add-skills.js +2 -2
  116. package/src/stdin-ask.js +16 -4
  117. package/src/strip-internal-comments.js +1 -1
  118. package/src/tech-detector.js +2 -2
  119. package/src/templates/report-sheet.html +11 -13
  120. package/src/terminal-agents.js +8 -8
  121. package/src/terminal-format.js +3 -3
  122. package/src/terminal-sections.js +11 -11
  123. package/src/tier-engine.js +3 -3
  124. package/src/tier-projection.js +2 -2
  125. package/src/transcript-turns.js +1 -1
  126. package/src/typewriter.js +1 -1
  127. package/src/usage-model-calls.js +6 -6
  128. package/src/usage-oneshot-commands.js +5 -5
  129. package/src/usage-repo-scope.js +11 -129
  130. package/src/usage-run-steps.js +82 -181
  131. package/src/usage-scan.js +235 -0
  132. package/src/work-streams.js +1 -1
@@ -3,7 +3,7 @@
3
3
  const { postJsonWithTimeout } = require('./backend-request');
4
4
  const { reasonForError, WORKS_CODE_TO_REASON } = require('./skills-client');
5
5
 
6
- // Talent-agent client for `start`'s "Add agent" route (talents-ai-score Phase 2).
6
+ // Talent-agent client for `start`'s "Add agent" route.
7
7
 
8
8
  const DEFAULT_TIMEOUT_MS = 20000;
9
9
 
@@ -88,14 +88,14 @@ async function requestDeclareAgent(
88
88
 
89
89
  const json = parseJson(res.raw);
90
90
  if (!json || json.status !== 'OK') return { ok: false, reason: 'bad-response' };
91
- // Thread the created agent's id back (relayed from Hub's declare response) so the relate step can PATCH `.../agents/:id/portfolios` (ADR-041).
91
+ // Thread the created agent's id back (relayed from Hub's declare response) so the relate step can PATCH `.../agents/:id/portfolios`.
92
92
  const data = json.data && typeof json.data === 'object' ? json.data : null;
93
93
  const rawId = data ? data.id : null;
94
94
  const agentId = typeof rawId === 'string' || typeof rawId === 'number' ? String(rawId) : null;
95
95
  return { ok: true, agentId };
96
96
  }
97
97
 
98
- // DIRECT to Hub (ADR-041), replace-set.
98
+ // DIRECT to Hub, replace-set.
99
99
  async function requestRelateAgentPortfolios(
100
100
  { agentId, items, hubAccessToken } = {},
101
101
  { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
@@ -124,7 +124,7 @@ async function requestRelateAgentPortfolios(
124
124
  }
125
125
 
126
126
  // `POST .../agents/draft-fields` -> { ok, whatItDoes, humanDecides } | { ok:false, reason }.
127
- // No X-Hub-Token. The CALLER gates consent (ADR-052), so `consent:true` is always sent.
127
+ // No X-Hub-Token. The CALLER gates consent, so `consent:true` is always sent.
128
128
  async function requestDraftAgentFields(
129
129
  { name, tools, model, parent, definition, accessToken } = {},
130
130
  { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { derivePayload } = require('./share-payload');
5
+ const { classify } = require('./maturity');
6
+ const { reviewCopy } = require('./signup-copy');
7
+ const { sanitizeRenderText } = require('./sanitize-network-text');
8
+
9
+ function repoName(repo) {
10
+ return path.basename(String(repo)) || String(repo);
11
+ }
12
+
13
+ function uploadFacts({ report, maturity, repos = [] }) {
14
+ const payload = derivePayload(report, maturity);
15
+ const sessions = payload.sessions || {};
16
+ const conversationExcerpts =
17
+ (payload.steering ? payload.steering.redactedSteeringExcerpts.length : 0)
18
+ + (payload.decisions ? payload.decisions.redactedDecisionExcerpts.length : 0);
19
+ const commandExcerpts = Array.isArray(sessions.redactedCommandSample) ? sessions.redactedCommandSample.length : 0;
20
+ return {
21
+ tools: (report.tools || []).filter((t) => t && t.detected).map((t) => t.name || t.id),
22
+ technologies: payload.technologies.filter((t) => typeof t === 'string'),
23
+ sessionCount: sessions.sessionCount || 0,
24
+ activeDaysPerWeek: typeof sessions.activeDaysPerWeek === 'number' ? sessions.activeDaysPerWeek : null,
25
+ commitCount: payload.gitActivity ? payload.gitActivity.commitCount : 0,
26
+ repos: repos.map(repoName),
27
+ agents: payload.agents.map((a) => a.name),
28
+ agentDefinitions: payload.agents.filter((a) => a.definition).length,
29
+ agentSummaries: payload.agentSynthesis.filter((a) => a.whatItDoes || a.symbolicName).length,
30
+ conversationExcerpts,
31
+ commandExcerpts,
32
+ };
33
+ }
34
+
35
+ function keepNamed(list, excluded) {
36
+ return Array.isArray(list) ? list.filter((x) => !(x && excluded.has(x.name))) : list;
37
+ }
38
+
39
+ function withoutExcluded(report, { agents = [], excerpts = true } = {}) {
40
+ const excluded = new Set(agents);
41
+ const next = structuredClone(report);
42
+ if (excluded.size > 0) {
43
+ next.agents = keepNamed(next.agents, excluded)
44
+ .map((a) => (a && excluded.has(a.parent) ? { ...a, parent: null } : a));
45
+ next.agentDefinitions = keepNamed(next.agentDefinitions, excluded);
46
+ next.agentDescriptions = keepNamed(next.agentDescriptions, excluded);
47
+ if (next.agentEvaluation) next.agentEvaluation.evaluations = keepNamed(next.agentEvaluation.evaluations, excluded);
48
+ if (next.agentSynthesis) next.agentSynthesis.agents = keepNamed(next.agentSynthesis.agents, excluded);
49
+ if (next.detectionEvidence) next.detectionEvidence.agents = keepNamed(next.detectionEvidence.agents, excluded);
50
+ if (next.agentUsage && next.agentUsage.byAgent) {
51
+ for (const name of excluded) delete next.agentUsage.byAgent[name];
52
+ }
53
+ if (next.agentCounts) next.agentCounts.agents = Array.isArray(next.agents) ? next.agents.length : 0;
54
+ }
55
+ if (!excerpts) {
56
+ if (next.steering) next.steering.redactedSteeringExcerpts = [];
57
+ if (next.decisions) next.decisions.redactedDecisionExcerpts = [];
58
+ if (next.sessions) next.sessions.redactedCommandSample = [];
59
+ }
60
+ return { report: next, maturity: classify(next) };
61
+ }
62
+
63
+ function joined(list) {
64
+ return list.join(', ');
65
+ }
66
+
67
+ function summaryText(lang, facts, { sent = false } = {}) {
68
+ const c = reviewCopy(lang);
69
+ return [
70
+ sent ? c.sentTitle : c.title,
71
+ facts.tools.length ? c.tools(joined(facts.tools)) : null,
72
+ facts.technologies.length ? c.technologies(joined(facts.technologies)) : null,
73
+ c.activity(facts.sessionCount, facts.activeDaysPerWeek),
74
+ facts.repos.length ? c.repos(joined(facts.repos), facts.commitCount) : null,
75
+ facts.agents.length ? c.agents(joined(facts.agents), facts.agentDefinitions, facts.agentSummaries) : null,
76
+ facts.conversationExcerpts + facts.commandExcerpts > 0 ? c.excerpts(facts.conversationExcerpts, facts.commandExcerpts) : c.noExcerpts,
77
+ c.notSent,
78
+ ].filter(Boolean).map((line, i) => (i === 0 ? line : `- ${line}`)).join('\n');
79
+ }
80
+
81
+ function explanationText(lang) {
82
+ const c = reviewCopy(lang);
83
+ return [c.explanationTitle, ...c.explanation, c.afterwards].join('\n\n');
84
+ }
85
+
86
+ function resultText(lang, preview) {
87
+ const c = reviewCopy(lang);
88
+ if (!preview) return [c.resultPending, c.afterwards].join('\n\n');
89
+ const m = preview.matrix || {};
90
+ const lines = [
91
+ c.resultTitle,
92
+ m.setupLevel ? c.setupLevel(c.levels[m.setupLevel] || m.setupLevel) : null,
93
+ preview.setup && preview.setup.explanation,
94
+ m.usageLevel ? c.usageLevel(c.levels[m.usageLevel] || m.usageLevel) : null,
95
+ preview.usage && preview.usage.explanation,
96
+ m.cell ? c.cell(m.cell) : null,
97
+ ].filter(Boolean).map(sanitizeRenderText);
98
+ return [lines.join('\n'), c.explanation[0], c.afterwards].join('\n\n');
99
+ }
100
+
101
+ module.exports = { uploadFacts, withoutExcluded, repoName, summaryText, explanationText, resultText };
package/src/ai.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // The single named surface for every model-backed (LLM) call this CLI makes (talents-ai-score, arch-cleanup #4).
3
+ // The single named surface for every model-backed (LLM) call this CLI makes.
4
4
 
5
5
  const agentSynthesis = require('./agent-synthesis');
6
6
  const agentEvaluation = require('./agent-evaluation');
package/src/ansi.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { isInteractive } = require('./terminal-progress');
4
4
  const { BRAND_ANSI } = require('./brand-ansi');
5
5
 
6
- // THE ONE GATE for terminal colour (talents-ai-score).
6
+ // THE ONE GATE for terminal colour.
7
7
 
8
8
  // `NO_COLOR` per the de-facto standard: present and non-empty disables colour, whatever the value.
9
9
  function noColorRequested(env = process.env) {
@@ -2,9 +2,10 @@
2
2
 
3
3
  const { postJsonWithTimeout, extractSetCookie } = require('./backend-request');
4
4
 
5
- // Login / token acquisition against the Hub's better-auth email engine (owner decision, supersedes ADR-031 for the email path).
5
+ // Login / token acquisition against the Hub's better-auth email engine.
6
6
 
7
7
  const DEFAULT_TIMEOUT_MS = 20000;
8
+ const SIGN_OUT_TIMEOUT_MS = 3000;
8
9
  const SESSION_COOKIE_NAME = 'better-auth.session_token';
9
10
  // Refresh the cookie session's own validity from the Set-Cookie Max-Age; fall
10
11
  // back to 7 days (better-auth's default) when the attribute is absent.
@@ -88,12 +89,13 @@ function normalizeSignIn(status, raw, headers) {
88
89
  }
89
90
 
90
91
  // Mints a hub JWT from a better-auth session cookie: `GET {tokenEndpoint}` with the `Cookie` header.
91
- async function requestAuthToken({ cookie } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
92
+ async function requestAuthToken({ cookie, sessionToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
92
93
  if (!endpoint) return { ok: false, reason: 'no-endpoint' };
93
- if (!cookie) return { ok: false, reason: 'no-cookie' };
94
+ if (!cookie && !sessionToken) return { ok: false, reason: 'no-cookie' };
95
+ const credential = cookie ? { Cookie: cookie } : { Authorization: `Bearer ${sessionToken}` };
94
96
  let res;
95
97
  try {
96
- res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, { Cookie: cookie });
98
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, credential);
97
99
  } catch (e) {
98
100
  return { ok: false, reason: (e && e.kind) || 'network-error' };
99
101
  }
@@ -133,7 +135,19 @@ async function requestLogin(
133
135
  };
134
136
  }
135
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}` };
141
+ try {
142
+ const res = await postJsonWithTimeout(endpoint, {}, timeoutMs, 'POST', null, credential);
143
+ return { ok: res.status >= 200 && res.status < 300, status: res.status };
144
+ } catch (e) {
145
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
146
+ }
147
+ }
148
+
136
149
  module.exports = {
150
+ requestSignOut,
137
151
  requestLogin,
138
152
  requestAuthToken,
139
153
  normalizeSignIn,
@@ -3,7 +3,7 @@
3
3
  const { runInteractiveMultiSelect } = require('./interactive-select');
4
4
  const { methodsForFlow, methodLabel } = require('./auth-methods');
5
5
 
6
- // The shared interactive method picker for the CLI (talents-ai-score).
6
+ // The shared interactive method picker for the CLI.
7
7
  async function askNumberedChoice(ask, promptText, count) {
8
8
  const raw = (await ask(promptText)).trim();
9
9
  if (!raw) return null;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // The SINGLE source of truth for which auth methods exist and where each one is offered (talents-ai-score).
3
+ // The SINGLE source of truth for which auth methods exist and where each one is offered.
4
4
  const AUTH_METHODS = [
5
5
  { id: 'email', labelKey: 'methodEmail', availableFor: { login: true, register: true } },
6
6
  { id: 'google', labelKey: 'methodGoogle', availableFor: { login: true, register: true } },
@@ -4,8 +4,9 @@ const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { getConfigDir } = require('./config-dir');
7
+ const { recordAccountMarker } = require('./account-marker');
7
8
 
8
- // `~/.config/shakers/auth-session.json` — the logged-in Talent's session (talents-ai-score, issue 122 / ADR-044).
9
+ // `~/.config/shakers/auth-session.json` — the logged-in Talent's session.
9
10
 
10
11
  // Hub's `UserSubtypeEnum` (shared/domain/enum/user-subtype.enum.ts), copied VERBATIM — this repo never invents a value the wire does not send.
11
12
  const PLATFORM_TALENT_USER_TYPES = new Set(['WORKS_TALENT', 'WORKS_PARTNER']);
@@ -89,12 +90,12 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
89
90
  const hubAccessToken = explicitHubToken || (typeof session.accessToken === 'string' && session.accessToken ? session.accessToken : null);
90
91
 
91
92
  const cookie = typeof session.cookie === 'string' && session.cookie ? session.cookie : null;
93
+ const sessionToken = typeof session.sessionToken === 'string' && session.sessionToken ? session.sessionToken : null;
92
94
  let expiresAt;
93
95
  let accessTokenExpiresAt;
94
- if (cookie) {
95
- expiresAt = (typeof session.cookieExpiresAt === 'string' && session.cookieExpiresAt)
96
- ? session.cookieExpiresAt
97
- : (typeof session.expiresAt === 'string' && session.expiresAt ? session.expiresAt : null);
96
+ if (cookie || sessionToken) {
97
+ expiresAt = [session.cookieExpiresAt, session.sessionExpiresAt, session.expiresAt]
98
+ .find((v) => typeof v === 'string' && v) || expiresAtFromToken(session.accessToken);
98
99
  accessTokenExpiresAt = (typeof session.accessTokenExpiresAt === 'string' && session.accessTokenExpiresAt)
99
100
  ? session.accessTokenExpiresAt
100
101
  : expiresAtFromToken(session.accessToken);
@@ -109,6 +110,7 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
109
110
  expiresAt: expiresAt ?? null,
110
111
  accessTokenExpiresAt: accessTokenExpiresAt ?? null,
111
112
  cookie,
113
+ sessionToken,
112
114
  email: (typeof session.email === 'string' && session.email ? session.email : null)
113
115
  || emailFromHubToken(hubAccessToken),
114
116
  hubAccessToken,
@@ -140,6 +142,7 @@ function saveAuthSession(session, env = process.env, { fs: fsImpl = fs } = {}) {
140
142
  try { fsImpl.unlinkSync(tmp); } catch { /* never existed */ }
141
143
  throw e;
142
144
  }
145
+ recordAccountMarker(cookie ? 'email' : sessionToken ? 'device' : 'token', env, { fs: fsImpl });
143
146
  return target;
144
147
  }
145
148
 
@@ -157,7 +160,8 @@ function loadAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
157
160
  // better-auth) or a JWT (Google/legacy, or an email session whose JWT is cached).
158
161
  const hasCookie = typeof parsed.cookie === 'string' && parsed.cookie;
159
162
  const hasToken = typeof parsed.accessToken === 'string' && parsed.accessToken;
160
- if (!hasCookie && !hasToken) return null;
163
+ const sessionToken = typeof parsed.sessionToken === 'string' && parsed.sessionToken ? parsed.sessionToken : null;
164
+ if (!hasCookie && !hasToken && !sessionToken) return null;
161
165
  const hubAccessToken = typeof parsed.hubAccessToken === 'string' && parsed.hubAccessToken ? parsed.hubAccessToken : null;
162
166
  const email = (typeof parsed.email === 'string' && parsed.email ? parsed.email : null) || emailFromHubToken(hubAccessToken);
163
167
  return {
@@ -165,12 +169,17 @@ function loadAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
165
169
  expiresAt: typeof parsed.expiresAt === 'string' ? parsed.expiresAt : null,
166
170
  accessTokenExpiresAt: typeof parsed.accessTokenExpiresAt === 'string' ? parsed.accessTokenExpiresAt : null,
167
171
  cookie: hasCookie ? parsed.cookie : null,
172
+ sessionToken,
168
173
  email,
169
174
  hubAccessToken,
170
175
  userType: typeof parsed.userType === 'string' && KNOWN_USER_TYPES.has(parsed.userType) ? parsed.userType : null,
171
176
  };
172
177
  }
173
178
 
179
+ function hasAuthSessionFile(env = process.env, { fs: fsImpl = fs } = {}) {
180
+ return fsImpl.existsSync(authSessionPath(env));
181
+ }
182
+
174
183
  // Removes the file. Idempotent and never throws — `logout` on no session is a
175
184
  // no-op, not a failure.
176
185
  function clearAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
@@ -184,8 +193,8 @@ function clearAuthSession(env = process.env, { fs: fsImpl = fs } = {}) {
184
193
 
185
194
  // `none | active | expired`, the three states the caller needs to give the Talent a true message.
186
195
  function sessionStatus(session, now = Date.now()) {
187
- // A credential is either the durable cookie (email) or a JWT (Google/legacy).
188
- const hasCred = !!(session && ((typeof session.accessToken === 'string' && session.accessToken) || (typeof session.cookie === 'string' && session.cookie)));
196
+ // A credential is a durable cookie or session token, or a JWT (Google/legacy).
197
+ const hasCred = !!(session && [session.accessToken, session.cookie, session.sessionToken].some((v) => typeof v === 'string' && v));
189
198
  if (!hasCred) return 'none';
190
199
  const expiry = Date.parse(String(session.expiresAt || ''));
191
200
  if (!Number.isFinite(expiry)) return 'expired';
@@ -204,6 +213,7 @@ module.exports = {
204
213
  authSessionPath,
205
214
  saveAuthSession,
206
215
  loadAuthSession,
216
+ hasAuthSessionFile,
207
217
  clearAuthSession,
208
218
  sessionStatus,
209
219
  isLoggedIn,
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Display-only authorship-transparency check (talents-ai-score).
3
+ // Display-only authorship-transparency check.
4
4
  //
5
5
  // Warns the talent when their commits in the selected repos are authored under
6
6
  // an email that is not their Shakers account email, so their own work would be
package/src/authorship.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { execFileSync } = require('child_process');
4
4
  const path = require('path');
5
5
 
6
- // Git AUTHORSHIP + provenance for the certify phase (skill-code-certification, ADR-017).
6
+ // Git AUTHORSHIP + provenance for the certify phase.
7
7
 
8
8
  const { takePrefetchedGitLog } = require('./git-log-prefetch');
9
9
 
@@ -164,7 +164,7 @@ function buildAttributionPredicate(verifiedEmail, authorizedSet) {
164
164
  };
165
165
  }
166
166
 
167
- // Applies the ADR-017 gate (with the ADR-023 widening for test identities) to ONE sampled Skill.
167
+ // Applies the authorship gate (widened for test identities) to ONE sampled Skill.
168
168
  function attributeSample(sample, verifiedEmail, authorship, authorizedSet = null) {
169
169
  const isAttributable = buildAttributionPredicate(verifiedEmail, authorizedSet);
170
170
  const files = Array.isArray(sample.files) ? sample.files : [];
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const { execFileSync } = require('child_process');
6
6
  const { getHomeDir } = require('./env-paths');
7
7
 
8
- // Deterministic (no-LLM) automations detector (talents-ai-score, issue 017 / ADR-013-014).
8
+ // Deterministic (no-LLM) automations detector.
9
9
 
10
10
  const AI_CLI_NAMES = ['claude', 'aider', 'gemini', 'codex'];
11
11
  const AI_CLI_RE = new RegExp(`\\b(${AI_CLI_NAMES.join('|')})\\b`, 'i');
@@ -5,7 +5,7 @@ const https = require('https');
5
5
  const crypto = require('crypto');
6
6
  const { getHubBase } = require('./config');
7
7
 
8
- // ONE BACKEND: the certifications service (talents-ai-score, ADR-042).
8
+ // ONE BACKEND: the certifications service.
9
9
 
10
10
  const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
11
11
 
@@ -31,7 +31,7 @@ function profileUpdateSourceHeader(url, method) {
31
31
  return { 'X-Profile-Update-Source': clientIdentity };
32
32
  }
33
33
 
34
- // Extra request headers, sanitized (talents-ai-score, issue 038 — the agent certification interview needs `Authorization: Bearer <sessionToken>`).
34
+ // Extra request headers, sanitized (the agent certification interview needs `Authorization: Bearer <sessionToken>`).
35
35
  const UNSAFE_HEADER_VALUE_RE = /[\r\n\0]/;
36
36
 
37
37
  function sanitizeExtraHeaders(extraHeaders) {
@@ -71,6 +71,7 @@ function postJsonWithTimeout(url, body, timeoutMs, method = 'POST', idempotencyK
71
71
  ...sanitizeExtraHeaders(extraHeaders),
72
72
  },
73
73
  timeout: timeoutMs,
74
+ agent: false,
74
75
  },
75
76
  (res) => {
76
77
  let raw = '';
@@ -151,6 +152,7 @@ function postMultipartWithTimeout(url, fields, timeoutMs, method = 'POST', extra
151
152
  ...sanitizeExtraHeaders(extraHeaders),
152
153
  },
153
154
  timeout: timeoutMs,
155
+ agent: false,
154
156
  },
155
157
  (res) => {
156
158
  let raw = '';
@@ -183,7 +185,7 @@ async function requestBackend({
183
185
  throw Object.assign(new Error('no endpoint configured'), { kind: 'no-endpoint' });
184
186
  }
185
187
 
186
- // Optional trace (talents-ai-score, issue 109).
188
+ // Optional trace.
187
189
  const note = (entry) => {
188
190
  if (Array.isArray(trace)) trace.push(entry);
189
191
  };
package/src/brand-ansi.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // BRAND_ANSI — the Shakers brand palette, reincarnated as ANSI 256-colour SGR codes (talents-ai-score, terminal design-consistency pass).
3
+ // BRAND_ANSI — the Shakers brand palette, reincarnated as ANSI 256-colour SGR codes.
4
4
 
5
5
  const BRAND_ANSI = {
6
6
  primary: '\x1b[38;5;73m',
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Deterministic (no-LLM) browser tools detector (talents-ai-score, issue 018 / ADR-013-014).
3
+ // Deterministic (no-LLM) browser tools detector.
4
4
 
5
5
  const BROWSER_DEPENDENCY_NAMES = new Set(['playwright', '@playwright/test', 'puppeteer', 'puppeteer-core']);
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Legal disclaimer + EXPLICIT acceptance gate (ADR-001), shown before egress. Skill-code certify was retired (certify is now a dimension LiveKit interview); consumers today are the add-skill/add-agent/add-project consent gates.
3
+ // Legal disclaimer + EXPLICIT acceptance gate, shown before egress. Skill-code certify was retired (certify is now a dimension LiveKit interview); consumers today are the add-skill/add-agent/add-project consent gates.
4
4
 
5
5
  const { renderLegalNotice } = require('./legal-notice');
6
6
  const { styleQuestion } = require('./ansi');
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { getCatalog } = require('./i18n');
4
4
 
5
- // Client-side "remediation prompt" for the certify report (skill-code- certification, issue 011).
5
+ // Client-side "remediation prompt" for the certify report.
6
6
  function buildRemediationPrompt(item, lang) {
7
7
  const r = getCatalog(lang).certify.report;
8
8
  const improvements =
package/src/cli-args.js CHANGED
@@ -39,7 +39,7 @@ function parseArgs(argv) {
39
39
  repo: false,
40
40
  scope: null,
41
41
  noAi: false,
42
- // Issue 103: turns the section reveal off. Same name in both parsers, and the
42
+ // Turns the section reveal off. Same name in both parsers, and the
43
43
  // env var `NO_ANIMATION` does the same thing (the `NO_COLOR` pattern of 079).
44
44
  noAnimation: false,
45
45
  };
@@ -59,7 +59,7 @@ function parseArgs(argv) {
59
59
  else if (a === '--show-endpoint') opts.showEndpoint = true;
60
60
  else if (a === '--build-next-level') opts.buildNextLevel = true;
61
61
  else if (a === '--force') opts.force = true;
62
- // ADR-016: the tier roadmap / "next steps" block is removed from the
62
+ // The tier roadmap / "next steps" block is removed from the
63
63
  // default footprint output; `--roadmap` (alias `--next`) shows it again.
64
64
  else if (a === '--roadmap' || a === '--next') opts.roadmap = true;
65
65
  else if (a === '--lang') opts.lang = VALID_LANGS.has(argv[++i]) ? argv[i] : null;
@@ -3,7 +3,7 @@
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const { scrubSecrets } = require('./agent-synthesis');
6
- const { loadAuthSession } = require('./auth-session-store');
6
+ const { loadAuthSession, decodeJwtPayload } = require('./auth-session-store');
7
7
  const { getCertsBase } = require('./config');
8
8
  const { postJsonWithTimeout } = require('./backend-request');
9
9
 
@@ -46,6 +46,11 @@ function machineHint() {
46
46
  return { osUser, hostname };
47
47
  }
48
48
 
49
+ function jwtExpired(token, now) {
50
+ const payload = decodeJwtPayload(token);
51
+ return !!payload && typeof payload.exp === 'number' && payload.exp * 1000 <= now;
52
+ }
53
+
49
54
  // Builds the record + headers; returns null when there is no endpoint to post to.
50
55
  function buildAudit(command, argv, env) {
51
56
  let base;
@@ -57,10 +62,8 @@ function buildAudit(command, argv, env) {
57
62
  if (!base) return null;
58
63
 
59
64
  const session = loadAuthSession(env);
60
- const token =
61
- session && (session.accessToken || session.hubAccessToken)
62
- ? session.accessToken || session.hubAccessToken
63
- : null;
65
+ const stored = (session && (session.accessToken || session.hubAccessToken)) || null;
66
+ const token = stored && !jwtExpired(stored, Date.now()) ? stored : null;
64
67
 
65
68
  const body = {
66
69
  command: String(command || 'unknown').slice(0, 100),
package/src/config-dir.js CHANGED
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const fs = require('fs');
5
5
  const { getHomeDir, readEnv } = require('./env-paths');
6
6
 
7
- // Single source of truth for the CLI's config directory (ADR-045).
7
+ // Single source of truth for the CLI's config directory.
8
8
 
9
9
  const DIR_NAME = 'shakers';
10
10
  const LEGACY_DIR_NAME = 'ai-footprint';