shakers-cli-beta 0.8.0-beta.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 (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +81 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +120 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ const { collectGitActivity } = require('./git-activity');
4
+ const { collectWorkStreams } = require('./work-streams');
5
+ const { collectDetectionEvidence } = require('./detection-evidence');
6
+
7
+ function round2(n) {
8
+ return Math.round(n * 100) / 100;
9
+ }
10
+
11
+ function maxOrNull(a, b) {
12
+ if (a === null || a === undefined) return b ?? null;
13
+ if (b === null || b === undefined) return a;
14
+ return Math.max(a, b);
15
+ }
16
+
17
+ function minOrNull(a, b) {
18
+ if (a === null || a === undefined) return b ?? null;
19
+ if (b === null || b === undefined) return a;
20
+ return Math.min(a, b);
21
+ }
22
+
23
+ function mergeGitActivity(list) {
24
+ const present = list.filter(Boolean);
25
+ if (present.length === 0) return null;
26
+ const filesByType = {};
27
+ let commitCount = 0;
28
+ let authoredCommitCount = 0;
29
+ let linesAdded = 0;
30
+ let linesDeleted = 0;
31
+ let spanDays = 0;
32
+ for (const g of present) {
33
+ commitCount += g.commitCount;
34
+ authoredCommitCount += g.authoredCommitCount;
35
+ linesAdded += g.linesAdded;
36
+ linesDeleted += g.linesDeleted;
37
+ spanDays = Math.max(spanDays, g.spanDays);
38
+ for (const [ext, c] of Object.entries(g.filesByType || {})) {
39
+ filesByType[ext] = (filesByType[ext] || 0) + c;
40
+ }
41
+ }
42
+ return {
43
+ commitCount,
44
+ authoredCommitCount,
45
+ linesAdded,
46
+ linesDeleted,
47
+ filesByType,
48
+ velocity: round2(commitCount / Math.max(spanDays, 1)),
49
+ spanDays,
50
+ };
51
+ }
52
+
53
+ function mergeWorkStreams(list) {
54
+ const present = list.filter(Boolean);
55
+ if (present.length === 0) return null;
56
+ let streamCount = 0;
57
+ let multiDayStreams = 0;
58
+ let maxStreamSpanDays = 0;
59
+ let sumAvg = 0;
60
+ for (const w of present) {
61
+ streamCount += w.streamCount;
62
+ multiDayStreams += w.multiDayStreams;
63
+ maxStreamSpanDays = Math.max(maxStreamSpanDays, w.maxStreamSpanDays);
64
+ sumAvg += w.avgCommitsPerSession;
65
+ }
66
+ return {
67
+ streamCount,
68
+ multiDayStreams,
69
+ avgCommitsPerSession: round2(sumAvg / present.length),
70
+ maxStreamSpanDays,
71
+ };
72
+ }
73
+
74
+ function mergeCandidate(existing, incoming) {
75
+ existing.authoredFileCount += incoming.authoredFileCount;
76
+ existing.totalFileCount += incoming.totalFileCount;
77
+ existing.authorshipKnown = existing.authorshipKnown || incoming.authorshipKnown;
78
+ existing.depth = maxOrNull(existing.depth, incoming.depth);
79
+ existing.recencyDays = minOrNull(existing.recencyDays, incoming.recencyDays);
80
+ existing.vendored = existing.vendored && incoming.vendored;
81
+ }
82
+
83
+ function agentKey(a) {
84
+ return `${a.name}|${(a.tools || []).join(',')}|${a.model || ''}`;
85
+ }
86
+
87
+ function mergeProvenance(a, b) {
88
+ if (!b) return a;
89
+ if (!a) return { ...b };
90
+ return {
91
+ fork: a.fork || b.fork,
92
+ forkSignal: a.forkSignal || b.forkSignal || null,
93
+ boilerplate: a.boilerplate || b.boilerplate,
94
+ boilerplateMarker: a.boilerplateMarker || b.boilerplateMarker || null,
95
+ };
96
+ }
97
+
98
+ function mergeDetectionEvidence(list) {
99
+ const present = list.filter(Boolean);
100
+ if (present.length === 0) return null;
101
+ const skillsByTech = new Map();
102
+ const agentsByKey = new Map();
103
+ let provenance = null;
104
+ for (const de of present) {
105
+ for (const s of de.skills || []) {
106
+ const existing = skillsByTech.get(s.tech);
107
+ if (!existing) skillsByTech.set(s.tech, { ...s });
108
+ else mergeCandidate(existing, s);
109
+ }
110
+ for (const a of de.agents || []) {
111
+ const k = agentKey(a);
112
+ const existing = agentsByKey.get(k);
113
+ if (!existing) agentsByKey.set(k, { ...a });
114
+ else mergeCandidate(existing, a);
115
+ }
116
+ provenance = mergeProvenance(provenance, de.provenance);
117
+ }
118
+ return {
119
+ skills: [...skillsByTech.values()],
120
+ agents: [...agentsByKey.values()],
121
+ provenance,
122
+ };
123
+ }
124
+
125
+ function collectScopedGitActivity(toplevels, verifiedEmail) {
126
+ return mergeGitActivity(
127
+ toplevels.map((root) => collectGitActivity(root, verifiedEmail)),
128
+ );
129
+ }
130
+
131
+ function collectScopedWorkStreams(toplevels) {
132
+ return mergeWorkStreams(toplevels.map((root) => collectWorkStreams(root)));
133
+ }
134
+
135
+ function collectScopedDetectionEvidence(toplevels, verifiedEmail, report) {
136
+ return mergeDetectionEvidence(
137
+ toplevels.map((root) => collectDetectionEvidence(root, verifiedEmail, report)),
138
+ );
139
+ }
140
+
141
+ module.exports = {
142
+ mergeGitActivity,
143
+ mergeWorkStreams,
144
+ mergeDetectionEvidence,
145
+ collectScopedGitActivity,
146
+ collectScopedWorkStreams,
147
+ collectScopedDetectionEvidence,
148
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ // SECRET PROMPT — reads a secret WITHOUT painting it (issue 102).
4
+
5
+ const CTRL_C = '\u0003';
6
+ const CTRL_D = '\u0004';
7
+ const BACKSPACE = /[\u0008\u007f]/;
8
+
9
+ // Reads a secret from a RAW tty.
10
+ function readSecretRaw({ input, output, mask = '' } = {}) {
11
+ return new Promise((resolve, reject) => {
12
+ let value = '';
13
+ let done = false;
14
+
15
+ const restore = () => {
16
+ if (done) return;
17
+ done = true;
18
+ // Order matters: stop listening BEFORE leaving raw mode, so a keystroke that
19
+ // arrives during teardown is not delivered to a half-restored terminal.
20
+ try { input.removeListener('data', onData); } catch { /* already gone */ }
21
+ if (input.setRawMode) { try { input.setRawMode(false); } catch { /* not a TTY any more */ } }
22
+ };
23
+
24
+ function onData(chunk) {
25
+ const s = String(chunk);
26
+ for (const ch of s) {
27
+ if (ch === '\r' || ch === '\n') {
28
+ restore();
29
+ output.write('\n');
30
+ resolve(value);
31
+ return;
32
+ }
33
+ if (ch === CTRL_C || ch === CTRL_D) {
34
+ // Restore FIRST, then report the cancellation: whatever the caller does
35
+ // with it (exit, retry), the terminal is already usable.
36
+ restore();
37
+ output.write('\n');
38
+ resolve(null);
39
+ return;
40
+ }
41
+ if (BACKSPACE.test(ch)) {
42
+ if (value.length) {
43
+ value = value.slice(0, -1);
44
+ // Only erase on screen if something was drawn there.
45
+ if (mask) output.write('\b \b');
46
+ }
47
+ continue;
48
+ }
49
+ // Ignore the rest of the C0 controls (arrows arrive as escape sequences and
50
+ // would otherwise land inside the secret).
51
+ if (ch < ' ') continue;
52
+ value += ch;
53
+ if (mask) output.write(mask);
54
+ }
55
+ }
56
+
57
+ try {
58
+ if (input.setRawMode) input.setRawMode(true);
59
+ if (input.resume) input.resume();
60
+ input.on('data', onData);
61
+ } catch (err) {
62
+ restore();
63
+ reject(err);
64
+ }
65
+ });
66
+ }
67
+
68
+ function canPromptSecretly(input) {
69
+ return !!(input && input.isTTY && typeof input.setRawMode === 'function');
70
+ }
71
+
72
+ module.exports = { readSecretRaw, canPromptSecretly };
@@ -0,0 +1,30 @@
1
+ 'use strict';
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
+ const { loadAuthSession, sessionStatus } = require('./auth-session-store');
7
+ const { ensureFreshSession } = require('./session-refresh');
8
+
9
+ function jwtUsable(session, now) {
10
+ const exp = Date.parse(String((session && session.accessTokenExpiresAt) || ''));
11
+ return Number.isFinite(exp) && exp > now;
12
+ }
13
+
14
+ async function hasUsableSession(env = process.env, deps = {}) {
15
+ const {
16
+ loadAuthSession: load = loadAuthSession,
17
+ ensureFreshSession: refresh = ensureFreshSession,
18
+ now = Date.now(),
19
+ } = deps;
20
+
21
+ const session = load(env);
22
+ if (sessionStatus(session, now) === 'active') return true;
23
+ if (!session || !session.cookie) return false;
24
+
25
+ let refreshed = null;
26
+ try { refreshed = await refresh(env, { now }); } catch { refreshed = null; }
27
+ return jwtUsable(refreshed || load(env), now);
28
+ }
29
+
30
+ module.exports = { hasUsableSession, jwtUsable };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ // On-demand hub-JWT minting for an email (better-auth) session.
4
+
5
+ const REFRESH_MARGIN_MS = 120 * 1000;
6
+
7
+ function jwtStillFresh(accessTokenExpiresAt, now, marginMs) {
8
+ const exp = Date.parse(String(accessTokenExpiresAt || ''));
9
+ if (!Number.isFinite(exp)) return false;
10
+ return exp - now > marginMs;
11
+ }
12
+
13
+ async function ensureFreshSession(env = process.env, deps = {}) {
14
+ const {
15
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
16
+ saveAuthSession = require('./auth-session-store').saveAuthSession,
17
+ getAuthTokenEndpoint = require('./config').getAuthTokenEndpoint,
18
+ requestAuthToken = require('./auth-client').requestAuthToken,
19
+ now = Date.now(),
20
+ marginMs = REFRESH_MARGIN_MS,
21
+ } = deps;
22
+
23
+ const session = loadAuthSession(env);
24
+ if (!session) return null;
25
+ if (!session.cookie) return session;
26
+ if (session.accessToken && jwtStillFresh(session.accessTokenExpiresAt, now, marginMs)) return session;
27
+
28
+ const endpoint = getAuthTokenEndpoint(env);
29
+ if (!endpoint) return session;
30
+
31
+ const tok = await requestAuthToken({ cookie: session.cookie }, { endpoint });
32
+ if (!tok.ok) return session;
33
+
34
+ try {
35
+ saveAuthSession(
36
+ {
37
+ ...session,
38
+ accessToken: tok.accessToken,
39
+ hubAccessToken: tok.accessToken,
40
+ accessTokenExpiresAt: tok.expiresAt,
41
+ cookieExpiresAt: session.expiresAt,
42
+ },
43
+ env,
44
+ );
45
+ } catch {
46
+ return session;
47
+ }
48
+ return loadAuthSession(env);
49
+ }
50
+
51
+ module.exports = { ensureFreshSession, jwtStillFresh, REFRESH_MARGIN_MS };
@@ -0,0 +1,336 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getHomeDir } = require('./env-paths');
6
+ const { scrubSecrets } = require('./agent-synthesis');
7
+
8
+ // GLOBAL, machine-wide inventory of AI coding-tool SESSIONS (hybrid evaluation, Slice 1).
9
+
10
+ const MAX_FILES_PER_TOOL = 2000;
11
+ const MAX_BYTES_PER_FILE = 8 * 1024 * 1024;
12
+ const MAX_COMMAND_SAMPLE = 20;
13
+ const MAX_COMMAND_LEN = 200;
14
+ const IDLE_GAP_CAP_MS = 25 * 60 * 1000;
15
+ // Recency window for sessions90d / activeDaysPerWeek only; all-time metrics untouched.
16
+ const TRACTION_WINDOW_DAYS = 90;
17
+ const TRACTION_WINDOW_MS = TRACTION_WINDOW_DAYS * 24 * 60 * 60 * 1000;
18
+ const DAY_MS = 24 * 60 * 60 * 1000;
19
+
20
+ function activeMsForObjects(objects) {
21
+ const stamps = [];
22
+ for (const o of objects) {
23
+ if (o && typeof o.timestamp === 'string') {
24
+ const t = Date.parse(o.timestamp);
25
+ if (Number.isFinite(t)) stamps.push(t);
26
+ }
27
+ }
28
+ stamps.sort((a, b) => a - b);
29
+ let ms = 0;
30
+ for (let i = 1; i < stamps.length; i++) {
31
+ ms += Math.min(stamps[i] - stamps[i - 1], IDLE_GAP_CAP_MS);
32
+ }
33
+ return ms;
34
+ }
35
+
36
+ // Most-recent parseable timestamp = when the session was last active; null if none.
37
+ function latestTimestampOf(objects) {
38
+ let latest = null;
39
+ for (const o of objects) {
40
+ if (o && typeof o.timestamp === 'string') {
41
+ const t = Date.parse(o.timestamp);
42
+ if (Number.isFinite(t) && (latest === null || t > latest)) latest = t;
43
+ }
44
+ }
45
+ return latest;
46
+ }
47
+
48
+ // distinct active days ÷ weeks of the observed active span (clamped ≥1 week), capped at 7.
49
+ function activeDaysPerWeekFrom(activeDayStrings) {
50
+ const dayEpochs = [...activeDayStrings].map((d) => Date.parse(`${d}T00:00:00Z`));
51
+ if (dayEpochs.length === 0) return 0;
52
+ const spanDays =
53
+ Math.floor((Math.max(...dayEpochs) - Math.min(...dayEpochs)) / DAY_MS) + 1;
54
+ const weeks = Math.max(1, spanDays / 7);
55
+ return Math.min(7, Math.round((dayEpochs.length / weeks) * 10) / 10);
56
+ }
57
+
58
+ function isTranscriptFile(name) {
59
+ if (name.endsWith('.meta.json')) return false;
60
+ if (name === 'sessions-index.json') return false;
61
+ return name.endsWith('.jsonl') || name.endsWith('.json');
62
+ }
63
+
64
+ function isSubagentFile(filePath) {
65
+ return filePath.split(path.sep).includes('subagents');
66
+ }
67
+ const FILE_TOOL_NAMES = new Set([
68
+ 'Read',
69
+ 'Edit',
70
+ 'MultiEdit',
71
+ 'Write',
72
+ 'NotebookEdit',
73
+ ]);
74
+ const PLANNING_TOOL_NAMES = new Set(['ExitPlanMode', 'TodoWrite']);
75
+
76
+ function toolSources(env = process.env) {
77
+ const home = getHomeDir(env);
78
+ return [
79
+ { tool: 'claude', dir: path.join(home, '.claude', 'projects') },
80
+ { tool: 'codex', dir: path.join(home, '.codex', 'sessions') },
81
+ { tool: 'cursor', dir: path.join(home, '.cursor', 'chats') },
82
+ ];
83
+ }
84
+
85
+ function listSessionFiles(root) {
86
+ const out = [];
87
+ const stack = [root];
88
+ while (stack.length > 0) {
89
+ const dir = stack.pop();
90
+ let entries;
91
+ try {
92
+ entries = fs.readdirSync(dir, { withFileTypes: true });
93
+ } catch {
94
+ continue;
95
+ }
96
+ for (const entry of entries) {
97
+ const full = path.join(dir, entry.name);
98
+ if (entry.isDirectory()) {
99
+ stack.push(full);
100
+ } else if (entry.isFile() && isTranscriptFile(entry.name)) {
101
+ out.push(full);
102
+ if (out.length >= MAX_FILES_PER_TOOL) return out;
103
+ }
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+
109
+ function pushCommand(acc, command) {
110
+ if (typeof command !== 'string') return;
111
+ acc.bashCommandsCount += 1;
112
+ if (acc.commandSample.length >= MAX_COMMAND_SAMPLE) return;
113
+ const scrubbed = scrubSecrets(command).trim().slice(0, MAX_COMMAND_LEN);
114
+ if (scrubbed && !acc.commandSample.includes(scrubbed)) {
115
+ acc.commandSample.push(scrubbed);
116
+ }
117
+ }
118
+
119
+ function harvestToolUse(node, acc) {
120
+ if (!node || typeof node !== 'object') return;
121
+ const name = node.name;
122
+ const input = node.input && typeof node.input === 'object' ? node.input : null;
123
+ if (name === 'Bash' && input) {
124
+ pushCommand(acc, input.command);
125
+ } else if (name === 'Shell' && input) {
126
+ pushCommand(acc, input.command);
127
+ } else if (FILE_TOOL_NAMES.has(name) && input) {
128
+ const p = input.file_path || input.notebook_path;
129
+ if (typeof p === 'string' && p) acc.filesTouched.add(p);
130
+ } else if (PLANNING_TOOL_NAMES.has(name)) {
131
+ acc.planningSignalCount = (acc.planningSignalCount || 0) + 1;
132
+ }
133
+ }
134
+
135
+ function harvestObject(o, acc) {
136
+ const msg = o && o.message ? o.message : o;
137
+ const content = msg && Array.isArray(msg.content) ? msg.content : null;
138
+ if (content) {
139
+ for (const c of content) {
140
+ if (c && c.type === 'tool_use') harvestToolUse(c, acc);
141
+ }
142
+ }
143
+ if (o && typeof o.cwd === 'string' && o.cwd) acc.projectKeys.add(o.cwd);
144
+ }
145
+
146
+ function readSessionObjects(file) {
147
+ const out = [];
148
+ let fd;
149
+ try {
150
+ fd = fs.openSync(file, 'r');
151
+ } catch {
152
+ return out;
153
+ }
154
+ try {
155
+ const { size } = fs.fstatSync(fd);
156
+ const readLen = Math.min(size, MAX_BYTES_PER_FILE);
157
+ const buf = Buffer.allocUnsafe(readLen);
158
+ fs.readSync(fd, buf, 0, readLen, 0);
159
+ const lines = buf.toString('utf8').split('\n');
160
+ const complete = size > MAX_BYTES_PER_FILE ? lines.slice(0, -1) : lines;
161
+ for (const ln of complete) {
162
+ if (!ln) continue;
163
+ let o;
164
+ try {
165
+ o = JSON.parse(ln);
166
+ } catch {
167
+ continue;
168
+ }
169
+ out.push(o);
170
+ }
171
+ } catch {
172
+ /* unreadable mid-stream — return what parsed */
173
+ } finally {
174
+ try {
175
+ fs.closeSync(fd);
176
+ } catch {
177
+ /* already closed */
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function scanFile(file, acc) {
184
+ for (const o of readSessionObjects(file)) harvestObject(o, acc);
185
+ }
186
+
187
+ function resolveFileCwd(objects) {
188
+ for (const o of objects) {
189
+ if (o && typeof o.cwd === 'string' && o.cwd) return o.cwd;
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function fileInScope(cwd, selectedCwds) {
195
+ if (selectedCwds === null) return true;
196
+ return cwd !== null && selectedCwds.has(cwd);
197
+ }
198
+
199
+ // Yields every parsed session object across every known AI-tool session directory on this machine, tagged with the tool it came from.
200
+ function* iterateSessionObjects(env = process.env) {
201
+ for (const source of toolSources(env)) {
202
+ const files = listSessionFiles(source.dir);
203
+ for (const file of files) {
204
+ const objects = readSessionObjects(file);
205
+ const cwd = resolveFileCwd(objects);
206
+ for (const obj of objects) {
207
+ yield { tool: source.tool, obj, cwd, filePath: file };
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Scan every known AI-tool session directory on this machine.
214
+ function makeSessionsAccumulator() {
215
+ const toolsSeen = [];
216
+ let sessionCount = 0;
217
+ let subagentRunCount = 0;
218
+ let activeMs = 0;
219
+ const filesTouched = new Set();
220
+ let bashCommandsCount = 0;
221
+ let planningSignalCount = 0;
222
+ const commandSample = [];
223
+ const projectKeysByTool = new Map();
224
+ // Recency-windowed session signals, from the same objects already read.
225
+ const windowCutoff = Date.now() - TRACTION_WINDOW_MS;
226
+ let sessions90d = 0;
227
+ const activeDaysInWindow = new Set();
228
+ let cur = null;
229
+
230
+ return {
231
+ startTool(tool) {
232
+ const projectKeys = new Set();
233
+ cur = {
234
+ tool,
235
+ projectKeys,
236
+ rootSessions: 0,
237
+ subagentRuns: 0,
238
+ acc: {
239
+ filesTouched,
240
+ commandSample,
241
+ projectKeys,
242
+ bashCommandsCount: 0,
243
+ planningSignalCount: 0,
244
+ },
245
+ };
246
+ },
247
+ addFile(file, objects) {
248
+ for (const o of objects) harvestObject(o, cur.acc);
249
+ if (isSubagentFile(file)) {
250
+ cur.subagentRuns += 1;
251
+ } else {
252
+ cur.rootSessions += 1;
253
+ activeMs += activeMsForObjects(objects);
254
+ // Root session in the 90d window → count it + record its (UTC) active day.
255
+ const lastTs = latestTimestampOf(objects);
256
+ if (lastTs !== null && lastTs >= windowCutoff) {
257
+ sessions90d += 1;
258
+ activeDaysInWindow.add(new Date(lastTs).toISOString().slice(0, 10));
259
+ }
260
+ }
261
+ },
262
+ endTool() {
263
+ if (cur.rootSessions === 0 && cur.subagentRuns === 0) {
264
+ cur = null;
265
+ return;
266
+ }
267
+ toolsSeen.push(cur.tool);
268
+ sessionCount += cur.rootSessions;
269
+ subagentRunCount += cur.subagentRuns;
270
+ bashCommandsCount += cur.acc.bashCommandsCount;
271
+ planningSignalCount += cur.acc.planningSignalCount;
272
+ projectKeysByTool.set(cur.tool, cur.projectKeys);
273
+ cur = null;
274
+ },
275
+ result() {
276
+ if (toolsSeen.length === 0) return null;
277
+ const keyToolCount = new Map();
278
+ for (const keys of projectKeysByTool.values()) {
279
+ for (const key of keys) {
280
+ keyToolCount.set(key, (keyToolCount.get(key) || 0) + 1);
281
+ }
282
+ }
283
+ let crossToolLinks = 0;
284
+ for (const count of keyToolCount.values()) {
285
+ if (count > 1) crossToolLinks += 1;
286
+ }
287
+ return {
288
+ toolsSeen,
289
+ sessionCount,
290
+ subagentRunCount,
291
+ activeHours: Math.round((activeMs / 3600000) * 10) / 10,
292
+ crossToolLinks,
293
+ filesTouchedCount: filesTouched.size,
294
+ bashCommandsCount,
295
+ planningSignalCount,
296
+ sessions90d,
297
+ activeDaysPerWeek: activeDaysPerWeekFrom(activeDaysInWindow),
298
+ redactedCommandSample: commandSample,
299
+ };
300
+ },
301
+ };
302
+ }
303
+
304
+ function walkSessionSignals(env = process.env, selectedCwds = null, onHumanObject = null) {
305
+ const sessions = makeSessionsAccumulator();
306
+ for (const source of toolSources(env)) {
307
+ const files = listSessionFiles(source.dir);
308
+ if (files.length === 0) continue;
309
+ sessions.startTool(source.tool);
310
+ for (const f of files) {
311
+ const objects = readSessionObjects(f);
312
+ if (!fileInScope(resolveFileCwd(objects), selectedCwds)) continue;
313
+ sessions.addFile(f, objects);
314
+ if (onHumanObject) {
315
+ for (const o of objects) onHumanObject(o);
316
+ }
317
+ }
318
+ sessions.endTool();
319
+ }
320
+ return sessions.result();
321
+ }
322
+
323
+ function collectSessions(env = process.env, selectedCwds = null) {
324
+ return walkSessionSignals(env, selectedCwds, null);
325
+ }
326
+
327
+ module.exports = {
328
+ collectSessions,
329
+ makeSessionsAccumulator,
330
+ walkSessionSignals,
331
+ toolSources,
332
+ listSessionFiles,
333
+ readSessionObjects,
334
+ resolveFileCwd,
335
+ iterateSessionObjects,
336
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { walkSessionSignals } = require('./session-scan');
4
+ const { humanTextsFromObject } = require('./transcript-turns');
5
+ const { makeSteeringAccumulator } = require('./steering-extract');
6
+ const { makeDecisionsAccumulator } = require('./decisions-extract');
7
+ const { makeBlindApprovalAccumulator } = require('./blind-approval-extract');
8
+ const { makeSpecificityAccumulator } = require('./specificity-extract');
9
+
10
+ function collectSessionSignals(env = process.env, selectedCwds = null) {
11
+ const steering = makeSteeringAccumulator();
12
+ const decisions = makeDecisionsAccumulator();
13
+ const blindApproval = makeBlindApprovalAccumulator();
14
+ const specificity = makeSpecificityAccumulator();
15
+ const sessions = walkSessionSignals(env, selectedCwds, (obj) => {
16
+ for (const text of humanTextsFromObject(obj)) {
17
+ steering.add(text);
18
+ decisions.add(text);
19
+ blindApproval.add(text);
20
+ specificity.add(text);
21
+ }
22
+ });
23
+ if (sessions) {
24
+ const approval = blindApproval.result();
25
+ const specific = specificity.result();
26
+ if (approval) Object.assign(sessions, approval);
27
+ if (specific) Object.assign(sessions, specific);
28
+ }
29
+ return {
30
+ sessions,
31
+ steering: steering.result(),
32
+ decisions: decisions.result(),
33
+ };
34
+ }
35
+
36
+ module.exports = { collectSessionSignals };