shakers-cli 0.0.0-stage → 0.14.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 (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  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 +99 -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 +124 -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 +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -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,174 @@
1
+ 'use strict';
2
+
3
+ // Input normalizers: map a RAW session record into the Claude-shaped internal object model
4
+ // that session-scan harvest + transcript-turns already consume
5
+ // ({ cwd, timestamp, type, message:{ role, content:[{type:'text'|'tool_use', ...}] } }).
6
+ // They ONLY translate input shape — downstream redaction/upload (share.js#derivePayload
7
+ // -> POST usage/reports) is unchanged; no field is read that the Claude path didn't expose.
8
+ // FAIL-CLOSED: every normalizer returns null / [] when a record does not match the expected
9
+ // shape — it never fabricates or guesses fields. A documented format that differs from the
10
+ // real one yields "no data for that tool", never wrong data.
11
+
12
+ function str(v) {
13
+ return typeof v === 'string' && v ? v : null;
14
+ }
15
+
16
+ function textFromParts(parts) {
17
+ if (typeof parts === 'string') return parts.trim() || null;
18
+ if (!Array.isArray(parts)) return null;
19
+ const out = [];
20
+ for (const p of parts) {
21
+ if (typeof p === 'string') out.push(p);
22
+ else if (p && typeof p === 'object' && typeof p.text === 'string') out.push(p.text);
23
+ }
24
+ const joined = out.join('').trim();
25
+ return joined || null;
26
+ }
27
+
28
+ // --- Codex (~/.codex/sessions/**/rollout-*.jsonl) ---
29
+ // Reads timestamp, cwd (session_meta), user text, and shell commands; a record matching none
30
+ // of those passes through only its own real timestamp/cwd, else is dropped.
31
+ function codexCommand(args) {
32
+ let a = args;
33
+ if (typeof a === 'string') {
34
+ try { a = JSON.parse(a); } catch { return a.trim() || null; }
35
+ }
36
+ if (!a || typeof a !== 'object') return null;
37
+ const cmd = a.command ?? a.cmd ?? a.commandLine;
38
+ if (Array.isArray(cmd)) return cmd.map(String).join(' ').trim() || null;
39
+ return str(cmd);
40
+ }
41
+
42
+ function normalizeCodexRecord(raw) {
43
+ if (!raw || typeof raw !== 'object') return null;
44
+ const payload = raw.payload && typeof raw.payload === 'object' ? raw.payload : null;
45
+ const item = payload || raw;
46
+ const out = {};
47
+ const ts = str(raw.timestamp) || str(item.timestamp);
48
+ if (ts) out.timestamp = ts;
49
+ const cwd = str(raw.cwd) || str(item.cwd);
50
+ if (cwd) out.cwd = cwd;
51
+
52
+ const role = str(item.role);
53
+ if (role === 'user') {
54
+ const text = textFromParts(item.content);
55
+ if (text) {
56
+ out.type = 'user';
57
+ out.message = { role: 'user', content: [{ type: 'text', text }] };
58
+ return out;
59
+ }
60
+ }
61
+
62
+ const type = str(raw.type) || str(item.type);
63
+ const name = str(item.name) || (type === 'function_call' ? 'shell' : null);
64
+ if (name && /shell|exec|bash|terminal|command/i.test(name)) {
65
+ const command = codexCommand(item.arguments ?? item.input ?? item.args);
66
+ if (command) {
67
+ out.message = { content: [{ type: 'tool_use', name: 'Shell', input: { command } }] };
68
+ return out;
69
+ }
70
+ }
71
+
72
+ return out.timestamp || out.cwd ? out : null;
73
+ }
74
+
75
+ // --- Gemini CLI (~/.gemini/tmp/<project-hash>/chats/logs.json | session-*.jsonl) ---
76
+ // logs.json = JSON array of { type:'user'|'model', message, timestamp }; session-*.jsonl =
77
+ // per-line { role, parts }. No cwd is recorded (the dir is a one-way SHA-256 hash), so cwd
78
+ // stays null — global session/time metrics still count; per-repo scoping can't.
79
+ function normalizeGeminiRecord(raw) {
80
+ if (!raw || typeof raw !== 'object') return null;
81
+ const out = {};
82
+ const ts = str(raw.timestamp) || str(raw.time);
83
+ if (ts) out.timestamp = ts;
84
+
85
+ const isUser = str(raw.type) === 'user' || str(raw.role) === 'user';
86
+ if (isUser) {
87
+ const text = str(raw.message) || textFromParts(raw.parts) || textFromParts(raw.content);
88
+ if (text) {
89
+ out.type = 'user';
90
+ out.message = { role: 'user', content: [{ type: 'text', text }] };
91
+ return out;
92
+ }
93
+ }
94
+ return out.timestamp ? out : null;
95
+ }
96
+
97
+ // --- Continue (continue.dev) ~/.continue/sessions/*.json ---
98
+ // Each file is one session object; expand its history into per-turn objects, carrying
99
+ // workspaceDirectory as cwd (good repo attribution).
100
+ function normalizeContinueRecord(raw) {
101
+ if (!raw || typeof raw !== 'object') return null;
102
+ const cwd = str(raw.workspaceDirectory) || str(raw.workspace) || str(raw.cwd);
103
+ const history = Array.isArray(raw.history) ? raw.history
104
+ : (Array.isArray(raw.messages) ? raw.messages : (Array.isArray(raw.conversation) ? raw.conversation : null));
105
+ // A single message object (not a session) — handle defensively too.
106
+ if (!history) {
107
+ const msg = raw.message && typeof raw.message === 'object' ? raw.message : raw;
108
+ if (str(msg.role) === 'user') {
109
+ const text = typeof msg.content === 'string' ? msg.content.trim() : textFromParts(msg.content);
110
+ if (text) return oneUser(text, cwd, str(raw.timestamp) || str(raw.createdAt));
111
+ }
112
+ return null;
113
+ }
114
+ const out = [];
115
+ for (const entry of history) {
116
+ const msg = entry && entry.message && typeof entry.message === 'object' ? entry.message : entry;
117
+ if (!msg || typeof msg !== 'object') continue;
118
+ const ts = str(entry.timestamp) || str(entry.createdAt) || str(msg.timestamp);
119
+ // Only emit on a real user turn (with text) or a record carrying a real timestamp.
120
+ if (str(msg.role) === 'user') {
121
+ const text = typeof msg.content === 'string' ? msg.content.trim() : textFromParts(msg.content);
122
+ if (text) { out.push(oneUser(text, cwd, ts)); continue; }
123
+ }
124
+ if (ts) out.push(cwd ? { cwd, timestamp: ts } : { timestamp: ts });
125
+ }
126
+ return out.length ? out : null;
127
+ }
128
+
129
+ // --- Cline / Roo Code: VS Code globalStorage .../tasks/<id>/api_conversation_history.json ---
130
+ // One JSON array of Anthropic messages (role/content); no cwd and no timestamp in this file
131
+ // (ui_messages.json has ts) → cwd/time attribution absent. Emits only real user turns.
132
+ function normalizeClineRecord(raw) {
133
+ if (!raw || typeof raw !== 'object') return null;
134
+ if (str(raw.role) !== 'user') return null;
135
+ const text = typeof raw.content === 'string' ? raw.content.trim() : textFromParts(raw.content);
136
+ return text ? oneUser(text, null, null) : null;
137
+ }
138
+
139
+ // --- Aider: per-repo .aider.chat.history.md (markdown) ---
140
+ // Repo-level source → cwd is the repo toplevel (good attribution). Markdown parse:
141
+ // '# aider chat started at <ts>' sets the session time; '#### <text>' lines are user
142
+ // instructions. A file matching neither pattern yields 0 records.
143
+ function parseAiderHistory(text, cwd) {
144
+ if (typeof text !== 'string' || !text) return [];
145
+ const out = [];
146
+ let ts = null;
147
+ for (const line of text.split('\n')) {
148
+ const started = /^#\s*aider chat started at\s+(.+?)\s*$/i.exec(line);
149
+ if (started) {
150
+ const t = Date.parse(started[1].replace(' ', 'T'));
151
+ ts = Number.isFinite(t) ? new Date(t).toISOString() : null;
152
+ if (ts) out.push(cwd ? { cwd, timestamp: ts } : { timestamp: ts });
153
+ continue;
154
+ }
155
+ const user = /^####\s+(.*)$/.exec(line);
156
+ if (user && user[1].trim()) out.push(oneUser(user[1].trim(), cwd, ts));
157
+ }
158
+ return out;
159
+ }
160
+
161
+ function oneUser(text, cwd, ts) {
162
+ const o = { type: 'user', message: { role: 'user', content: [{ type: 'text', text }] } };
163
+ if (cwd) o.cwd = cwd;
164
+ if (ts) o.timestamp = ts;
165
+ return o;
166
+ }
167
+
168
+ module.exports = {
169
+ normalizeCodexRecord,
170
+ normalizeGeminiRecord,
171
+ normalizeContinueRecord,
172
+ normalizeClineRecord,
173
+ parseAiderHistory,
174
+ };
@@ -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 };