shakers-cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +15 -5
  2. package/bin/add-agent.js +12 -6
  3. package/bin/ai-usage.js +2 -1
  4. package/bin/apply.js +66 -0
  5. package/bin/command-audit-worker.js +19 -0
  6. package/bin/login.js +4 -8
  7. package/bin/mcp.js +32 -19
  8. package/bin/register.js +10 -29
  9. package/bin/shakers-cli.js +21 -12
  10. package/package.json +1 -1
  11. package/src/agents-client.js +1 -0
  12. package/src/ai-usage-review.js +5 -3
  13. package/src/apply-client.js +164 -0
  14. package/src/apply-flow.js +105 -0
  15. package/src/auth-client.js +7 -107
  16. package/src/auth-session-store.js +6 -12
  17. package/src/backend-request.js +1 -24
  18. package/src/cli-skill.js +77 -0
  19. package/src/command-audit.js +58 -5
  20. package/src/config.js +15 -16
  21. package/src/consent-flow.js +7 -2
  22. package/src/detect-repos.js +4 -8
  23. package/src/detected-agents.js +40 -0
  24. package/src/detectors.js +23 -0
  25. package/src/device-auth-store.js +2 -2
  26. package/src/device-login.js +11 -9
  27. package/src/env-paths.js +9 -1
  28. package/src/i18n.js +258 -88
  29. package/src/mcp-ai-usage-result-tool.js +6 -4
  30. package/src/mcp-ai-usage-tool.js +73 -47
  31. package/src/mcp-apply-tools.js +152 -0
  32. package/src/mcp-choice.js +171 -34
  33. package/src/mcp-detector.js +26 -2
  34. package/src/mcp-device-login-tools.js +40 -8
  35. package/src/mcp-install.js +15 -11
  36. package/src/mcp-instructions.js +43 -17
  37. package/src/mcp-onboarding-tools.js +90 -3
  38. package/src/mcp-prompts.js +6 -4
  39. package/src/mcp-register-tools.js +25 -14
  40. package/src/mcp-role-tools.js +35 -17
  41. package/src/mcp-server-name.js +7 -0
  42. package/src/mcp-server.js +35 -7
  43. package/src/mcp-signup-client.js +3 -6
  44. package/src/mcp-signup-flow.js +389 -0
  45. package/src/mcp-signup-tools.js +237 -126
  46. package/src/onboarding-flow.js +30 -125
  47. package/src/opencode-sessions.js +92 -0
  48. package/src/profile-agents.js +26 -0
  49. package/src/saved-positions-flow.js +3 -3
  50. package/src/scanner.js +104 -12
  51. package/src/session-gate.js +1 -4
  52. package/src/session-normalizers.js +114 -8
  53. package/src/session-refresh.js +5 -16
  54. package/src/session-scan.js +167 -22
  55. package/src/share-payload.js +4 -1
  56. package/src/share.js +2 -2
  57. package/src/signup-client.js +12 -39
  58. package/src/signup-copy.js +506 -68
  59. package/src/skill-install.js +102 -0
  60. package/src/start-add-agents.js +106 -3
  61. package/src/tier-engine.js +14 -2
  62. package/src/trust-narration.js +32 -0
  63. package/src/usage-run-steps.js +16 -4
  64. package/src/usage-scan.js +3 -0
  65. package/src/brand-svg.js +0 -9
  66. package/src/inter-fonts.js +0 -6
  67. package/src/mcp-auth-form.js +0 -484
@@ -26,8 +26,6 @@ function textFromParts(parts) {
26
26
  }
27
27
 
28
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
29
  function codexCommand(args) {
32
30
  let a = args;
33
31
  if (typeof a === 'string') {
@@ -60,7 +58,15 @@ function normalizeCodexRecord(raw) {
60
58
  }
61
59
 
62
60
  const type = str(raw.type) || str(item.type);
61
+ if (type === 'turn_context' && item.collaboration_mode && item.collaboration_mode.mode === 'plan') {
62
+ out.planMode = true;
63
+ return out;
64
+ }
63
65
  const name = str(item.name) || (type === 'function_call' ? 'shell' : null);
66
+ if (name === 'update_plan') {
67
+ out.message = { content: [{ type: 'tool_use', name }] };
68
+ return out;
69
+ }
64
70
  if (name && /shell|exec|bash|terminal|command/i.test(name)) {
65
71
  const command = codexCommand(item.arguments ?? item.input ?? item.args);
66
72
  if (command) {
@@ -73,11 +79,16 @@ function normalizeCodexRecord(raw) {
73
79
  }
74
80
 
75
81
  // --- 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
82
  function normalizeGeminiRecord(raw) {
80
83
  if (!raw || typeof raw !== 'object') return null;
84
+ if (Array.isArray(raw.messages)) {
85
+ const out = [];
86
+ for (const m of raw.messages) {
87
+ const o = normalizeGeminiRecord(m);
88
+ if (o) out.push(o);
89
+ }
90
+ return out;
91
+ }
81
92
  const out = {};
82
93
  const ts = str(raw.timestamp) || str(raw.time);
83
94
  if (ts) out.timestamp = ts;
@@ -91,6 +102,11 @@ function normalizeGeminiRecord(raw) {
91
102
  return out;
92
103
  }
93
104
  }
105
+ const calls = Array.isArray(raw.toolCalls) ? raw.toolCalls.map((c) => toolUse(c && c.name)).filter(Boolean) : [];
106
+ if (calls.length) {
107
+ out.message = { role: 'assistant', content: calls };
108
+ return out;
109
+ }
94
110
  return out.timestamp ? out : null;
95
111
  }
96
112
 
@@ -111,7 +127,7 @@ function normalizeContinueRecord(raw) {
111
127
  }
112
128
  return null;
113
129
  }
114
- const out = [];
130
+ const out = str(raw.mode) === 'plan' ? [cwd ? { cwd, planMode: true } : { planMode: true }] : [];
115
131
  for (const entry of history) {
116
132
  const msg = entry && entry.message && typeof entry.message === 'object' ? entry.message : entry;
117
133
  if (!msg || typeof msg !== 'object') continue;
@@ -127,15 +143,30 @@ function normalizeContinueRecord(raw) {
127
143
  }
128
144
 
129
145
  // --- 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
146
  function normalizeClineRecord(raw) {
133
147
  if (!raw || typeof raw !== 'object') return null;
148
+ if (str(raw.role) === 'assistant') return clinePlanningTurn(raw.content);
134
149
  if (str(raw.role) !== 'user') return null;
135
150
  const text = typeof raw.content === 'string' ? raw.content.trim() : textFromParts(raw.content);
136
151
  return text ? oneUser(text, null, null) : null;
137
152
  }
138
153
 
154
+ const CLINE_PLANNING_TAG = /<(plan_mode_respond|update_todo_list)>/g;
155
+
156
+ function clinePlanningTurn(content) {
157
+ const blocks = typeof content === 'string' ? [{ type: 'text', text: content }] : (Array.isArray(content) ? content : []);
158
+ const calls = [];
159
+ for (const b of blocks) {
160
+ if (!b || typeof b !== 'object') continue;
161
+ if (b.type === 'tool_use') calls.push(toolUse(b.name));
162
+ else if (b.type === 'text' && typeof b.text === 'string') {
163
+ for (const m of b.text.matchAll(CLINE_PLANNING_TAG)) calls.push(toolUse(m[1]));
164
+ }
165
+ }
166
+ const uses = calls.filter(Boolean);
167
+ return uses.length ? { message: { role: 'assistant', content: uses } } : null;
168
+ }
169
+
139
170
  // --- Aider: per-repo .aider.chat.history.md (markdown) ---
140
171
  // Repo-level source → cwd is the repo toplevel (good attribution). Markdown parse:
141
172
  // '# aider chat started at <ts>' sets the session time; '#### <text>' lines are user
@@ -158,6 +189,78 @@ function parseAiderHistory(text, cwd) {
158
189
  return out;
159
190
  }
160
191
 
192
+ function normalizeClineSessionRecord(raw) {
193
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.messages)) return null;
194
+ const out = [];
195
+ for (const m of raw.messages) {
196
+ if (!m || typeof m !== 'object') continue;
197
+ const ts = Number.isFinite(m.ts) ? new Date(m.ts).toISOString() : null;
198
+ if (str(m.role) === 'user') {
199
+ const text = textFromParts((Array.isArray(m.content) ? m.content : []).filter((b) => b && b.type === 'text'));
200
+ if (text) out.push(oneUser(text, null, ts));
201
+ continue;
202
+ }
203
+ if (str(m.role) !== 'assistant') continue;
204
+ const uses = (Array.isArray(m.content) ? m.content : []).filter((b) => b && b.type === 'tool_use').map((b) => toolUse(b.name)).filter(Boolean);
205
+ const o = ts ? { timestamp: ts } : {};
206
+ if (uses.length) o.message = { role: 'assistant', content: uses };
207
+ if (o.timestamp || o.message) out.push(o);
208
+ }
209
+ return out;
210
+ }
211
+
212
+ const CURSOR_QUERY = /^\s*<user_query>([\s\S]*?)<\/user_query>\s*$/;
213
+
214
+ function normalizeCursorRecord(raw) {
215
+ if (!raw || typeof raw !== 'object' || !raw.message || !Array.isArray(raw.message.content)) return null;
216
+ const role = str(raw.role);
217
+ if (role === 'user') {
218
+ const text = textFromParts(raw.message.content.filter((b) => b && b.type === 'text').map((b) => {
219
+ const m = CURSOR_QUERY.exec(b.text || '');
220
+ return m ? m[1] : b.text;
221
+ }));
222
+ return text ? oneUser(text, null, null) : null;
223
+ }
224
+ if (role !== 'assistant') return null;
225
+ const uses = raw.message.content.filter((b) => b && b.type === 'tool_use' && str(b.name));
226
+ return uses.length ? { message: { role: 'assistant', content: uses } } : null;
227
+ }
228
+
229
+ function normalizePiRecord(raw) {
230
+ if (!raw || typeof raw !== 'object') return null;
231
+ const ts = str(raw.timestamp);
232
+ const type = str(raw.type);
233
+ if (type === 'session') {
234
+ const cwd = str(raw.cwd);
235
+ if (!cwd && !ts) return null;
236
+ const o = {};
237
+ if (cwd) o.cwd = cwd;
238
+ if (ts) o.timestamp = ts;
239
+ return o;
240
+ }
241
+ if (type === 'custom' && raw.customType === 'plan-mode' && raw.data && raw.data.enabled === true) {
242
+ return ts ? { timestamp: ts, planMode: true } : { planMode: true };
243
+ }
244
+ if (type !== 'message' || !raw.message || typeof raw.message !== 'object') return null;
245
+ const role = str(raw.message.role);
246
+ if (role === 'user') {
247
+ const text = textFromParts(raw.message.content);
248
+ return text ? oneUser(text, null, ts) : null;
249
+ }
250
+ if (role !== 'assistant') return ts ? { timestamp: ts } : null;
251
+ const uses = (Array.isArray(raw.message.content) ? raw.message.content : [])
252
+ .filter((b) => b && b.type === 'toolCall')
253
+ .map((b) => toolUse(b.name))
254
+ .filter(Boolean);
255
+ const o = ts ? { timestamp: ts } : {};
256
+ if (uses.length) o.message = { role: 'assistant', content: uses };
257
+ return o.timestamp || o.message ? o : null;
258
+ }
259
+
260
+ function toolUse(name) {
261
+ return typeof name === 'string' && name ? { type: 'tool_use', name } : null;
262
+ }
263
+
161
264
  function oneUser(text, cwd, ts) {
162
265
  const o = { type: 'user', message: { role: 'user', content: [{ type: 'text', text }] } };
163
266
  if (cwd) o.cwd = cwd;
@@ -170,5 +273,8 @@ module.exports = {
170
273
  normalizeGeminiRecord,
171
274
  normalizeContinueRecord,
172
275
  normalizeClineRecord,
276
+ normalizeClineSessionRecord,
277
+ normalizeCursorRecord,
278
+ normalizePiRecord,
173
279
  parseAiderHistory,
174
280
  };
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // On-demand hub-JWT minting for an email (better-auth) session.
4
-
5
3
  const REFRESH_MARGIN_MS = 120 * 1000;
6
4
  const REVOKED_STATUSES = new Set([401, 403]);
7
5
 
@@ -24,13 +22,13 @@ async function ensureFreshSession(env = process.env, deps = {}) {
24
22
 
25
23
  const session = loadAuthSession(env);
26
24
  if (!session) return null;
27
- if (!session.cookie && !session.sessionToken) return session;
25
+ if (!session.sessionToken) return session;
28
26
  if (session.accessToken && jwtStillFresh(session.accessTokenExpiresAt, now, marginMs)) return session;
29
27
 
30
28
  const endpoint = getAuthTokenEndpoint(env);
31
29
  if (!endpoint) return session;
32
30
 
33
- const tok = await requestAuthToken({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint });
31
+ const tok = await requestAuthToken({ sessionToken: session.sessionToken }, { endpoint });
34
32
  if (!tok.ok && REVOKED_STATUSES.has(tok.status)) {
35
33
  clearAuthSession(env);
36
34
  return null;
@@ -44,7 +42,7 @@ async function ensureFreshSession(env = process.env, deps = {}) {
44
42
  accessToken: tok.accessToken,
45
43
  hubAccessToken: tok.accessToken,
46
44
  accessTokenExpiresAt: tok.expiresAt,
47
- cookieExpiresAt: session.expiresAt,
45
+ sessionExpiresAt: session.expiresAt,
48
46
  },
49
47
  env,
50
48
  );
@@ -54,25 +52,16 @@ async function ensureFreshSession(env = process.env, deps = {}) {
54
52
  return loadAuthSession(env);
55
53
  }
56
54
 
57
- function webOrigin(url) {
58
- try {
59
- return new URL(url).origin;
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
55
  async function endSession(env = process.env, deps = {}) {
66
56
  const {
67
57
  loadAuthSession = require('./auth-session-store').loadAuthSession,
68
58
  clearAuthSession = require('./auth-session-store').clearAuthSession,
69
59
  getSignOutEndpoint = require('./config').getSignOutEndpoint,
70
- getTalentProfileUrl = require('./config').getTalentProfileUrl,
71
60
  requestSignOut = require('./auth-client').requestSignOut,
72
61
  } = deps;
73
62
  const session = loadAuthSession(env);
74
- if (session && (session.sessionToken || session.cookie)) {
75
- await requestSignOut({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint: getSignOutEndpoint(env), origin: webOrigin(getTalentProfileUrl(env)) });
63
+ if (session && session.sessionToken) {
64
+ await requestSignOut({ sessionToken: session.sessionToken }, { endpoint: getSignOutEndpoint(env) });
76
65
  }
77
66
  clearAuthSession(env);
78
67
  return session !== null;
@@ -2,15 +2,19 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { getHomeDir } = require('./env-paths');
5
+ const { getHomeDir, codexHome, piAgentDir, expandHome } = require('./env-paths');
6
6
  const { scrubSecrets } = require('./agent-synthesis');
7
7
  const {
8
8
  normalizeCodexRecord,
9
9
  normalizeGeminiRecord,
10
10
  normalizeContinueRecord,
11
11
  normalizeClineRecord,
12
+ normalizeClineSessionRecord,
13
+ normalizeCursorRecord,
14
+ normalizePiRecord,
12
15
  parseAiderHistory,
13
16
  } = require('./session-normalizers');
17
+ const { readOpencodeSessions } = require('./opencode-sessions');
14
18
 
15
19
  // GLOBAL, machine-wide inventory of AI coding-tool SESSIONS.
16
20
 
@@ -78,11 +82,130 @@ const FILE_TOOL_NAMES = new Set([
78
82
  'Write',
79
83
  'NotebookEdit',
80
84
  ]);
81
- const PLANNING_TOOL_NAMES = new Set(['ExitPlanMode', 'TodoWrite']);
85
+ const PLANNING_TOOL_NAMES = new Set([
86
+ 'ExitPlanMode',
87
+ 'TodoWrite',
88
+ 'update_plan',
89
+ 'write_todos',
90
+ 'enter_plan_mode',
91
+ 'exit_plan_mode',
92
+ 'tracker_create_task',
93
+ 'tracker_update_task',
94
+ 'update_todo_list',
95
+ 'plan_mode_respond',
96
+ 'todowrite',
97
+ 'todo',
98
+ ]);
82
99
 
83
100
  // Only logs.json + session-*.jsonl under ~/.gemini/tmp are transcripts (skip checkpoints/config).
84
101
  function isGeminiTranscriptFile(name) {
85
- return name === 'logs.json' || (name.startsWith('session-') && name.endsWith('.jsonl'));
102
+ return name === 'logs.json' || (name.startsWith('session-') && (name.endsWith('.jsonl') || name.endsWith('.json')));
103
+ }
104
+
105
+ function isCursorTranscriptFile(name, full) {
106
+ return name.endsWith('.jsonl') && full.split(path.sep).includes('agent-transcripts');
107
+ }
108
+
109
+ function isClineSessionFile(name) {
110
+ return name.endsWith('.messages.json');
111
+ }
112
+
113
+ function readJsonFile(file) {
114
+ try {
115
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ function geminiContext(geminiDir) {
122
+ let slugs = null;
123
+ return (file) => {
124
+ if (slugs === null) {
125
+ slugs = new Map();
126
+ const registry = readJsonFile(path.join(geminiDir, 'projects.json'));
127
+ const projects = registry && registry.projects && typeof registry.projects === 'object' ? registry.projects : {};
128
+ for (const [dir, slug] of Object.entries(projects)) {
129
+ if (typeof slug === 'string') slugs.set(slug, dir);
130
+ }
131
+ }
132
+ const slug = path.relative(path.join(geminiDir, 'tmp'), file).split(path.sep)[0];
133
+ const cwd = slugs.get(slug);
134
+ return cwd ? { cwd } : null;
135
+ };
136
+ }
137
+
138
+ function slugOf(name) {
139
+ return name.replace(/[^A-Za-z0-9]/g, '-');
140
+ }
141
+
142
+ function resolveSlugPath(slug, dir = path.parse(process.cwd()).root, depth = 0) {
143
+ if (!slug) return dir;
144
+ if (depth > 24) return null;
145
+ let entries;
146
+ try {
147
+ entries = fs.readdirSync(dir, { withFileTypes: true });
148
+ } catch {
149
+ return null;
150
+ }
151
+ for (const e of entries) {
152
+ if (!e.isDirectory()) continue;
153
+ const part = slugOf(e.name);
154
+ if (slug === part) return path.join(dir, e.name);
155
+ if (part && slug.startsWith(`${part}-`)) {
156
+ const hit = resolveSlugPath(slug.slice(part.length + 1), path.join(dir, e.name), depth + 1);
157
+ if (hit) return hit;
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function cursorContext(projectsDir) {
164
+ const cwds = new Map();
165
+ return (file) => {
166
+ const slug = path.relative(projectsDir, file).split(path.sep)[0];
167
+ if (!cwds.has(slug)) cwds.set(slug, resolveSlugPath(slug));
168
+ const out = {};
169
+ const cwd = cwds.get(slug);
170
+ if (cwd) out.cwd = cwd;
171
+ try {
172
+ out.timestamp = fs.statSync(file).mtime.toISOString();
173
+ } catch {
174
+ return cwd ? out : null;
175
+ }
176
+ return out;
177
+ };
178
+ }
179
+
180
+ function clineSessionStem(file) {
181
+ return path.basename(file).slice(0, -'.messages.json'.length);
182
+ }
183
+
184
+ function isClineSubagentFile(file) {
185
+ return clineSessionStem(file) !== path.basename(path.dirname(file));
186
+ }
187
+
188
+ function clineContext(file) {
189
+ if (isClineSubagentFile(file)) return null;
190
+ const manifest = readJsonFile(path.join(path.dirname(file), `${clineSessionStem(file)}.json`));
191
+ if (!manifest || typeof manifest !== 'object') return null;
192
+ const out = {};
193
+ const cwd = manifest.cwd || manifest.workspace_root;
194
+ if (typeof cwd === 'string' && cwd) out.cwd = cwd;
195
+ if (typeof manifest.started_at === 'string') out.timestamp = manifest.started_at;
196
+ if (manifest.metadata && manifest.metadata.mode === 'plan') out.planMode = true;
197
+ return Object.keys(out).length ? out : null;
198
+ }
199
+
200
+ function clineSessionsDir(env, home) {
201
+ if (env.CLINE_SESSION_DATA_DIR) return env.CLINE_SESSION_DATA_DIR;
202
+ const dataDir = env.CLINE_DATA_DIR || path.join(env.CLINE_DIR || path.join(home, '.cline'), 'data');
203
+ return path.join(dataDir, 'sessions');
204
+ }
205
+
206
+ function piSessionsDir(env) {
207
+ if (env.PI_CODING_AGENT_SESSION_DIR) return expandHome(env.PI_CODING_AGENT_SESSION_DIR, env);
208
+ return path.join(piAgentDir(env), 'sessions');
86
209
  }
87
210
 
88
211
  // Cline / Roo Code conversation file (both forks use this filename under tasks/<id>/).
@@ -99,14 +222,18 @@ function vscodeGlobalStorageDir(home) {
99
222
 
100
223
  function toolSources(env = process.env) {
101
224
  const home = getHomeDir(env);
225
+ const codex = codexHome(env);
226
+ const gemini = path.join(home, '.gemini');
227
+ const cursorProjects = path.join(home, '.cursor', 'projects');
102
228
  return [
103
- { tool: 'claude', dir: path.join(home, '.claude', 'projects') },
104
- { tool: 'codex', dir: path.join(home, '.codex', 'sessions'), normalize: normalizeCodexRecord },
105
- { tool: 'gemini', dir: path.join(home, '.gemini', 'tmp'), fileFilter: isGeminiTranscriptFile, normalize: normalizeGeminiRecord },
106
- // Kept by decision: ~/.cursor/chats is OBSOLETE (real Cursor chat is SQLite in App Support); awaiting a future SQLite extractor, no tool removed.
107
- { tool: 'cursor', dir: path.join(home, '.cursor', 'chats') },
108
- { tool: 'continue', dir: path.join(home, '.continue', 'sessions'), normalize: normalizeContinueRecord },
109
- { tool: 'cline', dir: vscodeGlobalStorageDir(home), fileFilter: isClineHistoryFile, normalize: normalizeClineRecord },
229
+ { tool: 'claude', dirs: [path.join(home, '.claude', 'projects')] },
230
+ { tool: 'codex', dirs: [path.join(codex, 'sessions'), path.join(codex, 'archived_sessions')], normalize: normalizeCodexRecord },
231
+ { tool: 'gemini', dirs: [path.join(gemini, 'tmp')], fileFilter: isGeminiTranscriptFile, normalize: normalizeGeminiRecord, context: geminiContext(gemini) },
232
+ { tool: 'cursor', dirs: [cursorProjects], fileFilter: isCursorTranscriptFile, normalize: normalizeCursorRecord, context: cursorContext(cursorProjects) },
233
+ { tool: 'continue', dirs: [path.join(home, '.continue', 'sessions')], normalize: normalizeContinueRecord },
234
+ { tool: 'cline', dirs: [vscodeGlobalStorageDir(home), clineSessionsDir(env, home)], fileFilter: (name) => isClineHistoryFile(name) || isClineSessionFile(name), normalize: (r) => normalizeClineSessionRecord(r) || normalizeClineRecord(r), context: (file) => (isClineSessionFile(path.basename(file)) ? clineContext(file) : null), subagent: (file) => isSubagentFile(file) || (isClineSessionFile(path.basename(file)) && isClineSubagentFile(file)) },
235
+ { tool: 'pi', dirs: [piSessionsDir(env)], normalize: normalizePiRecord },
236
+ { tool: 'opencode', sessions: () => readOpencodeSessions(env, MAX_FILES_PER_TOOL) },
110
237
  // Repo-level: .aider.chat.history.md lives INSIDE each git repo, so cwd = the repo toplevel.
111
238
  { tool: 'aider', repoLevel: true, file: '.aider.chat.history.md', parseText: parseAiderHistory },
112
239
  ];
@@ -127,7 +254,7 @@ function listSessionFiles(root, fileFilter = isTranscriptFile) {
127
254
  const full = path.join(dir, entry.name);
128
255
  if (entry.isDirectory()) {
129
256
  stack.push(full);
130
- } else if (entry.isFile() && fileFilter(entry.name)) {
257
+ } else if (entry.isFile() && fileFilter(entry.name, full)) {
131
258
  out.push(full);
132
259
  if (out.length >= MAX_FILES_PER_TOOL) return out;
133
260
  }
@@ -169,7 +296,8 @@ function parseRawRecords(file) {
169
296
  function readSourceObjects(source, file) {
170
297
  const raw = parseRawRecords(file);
171
298
  if (typeof source.normalize !== 'function') return raw;
172
- const out = [];
299
+ const context = typeof source.context === 'function' ? source.context(file) : null;
300
+ const out = context ? [context] : [];
173
301
  for (const r of raw) {
174
302
  const o = source.normalize(r);
175
303
  if (Array.isArray(o)) out.push(...o);
@@ -178,6 +306,26 @@ function readSourceObjects(source, file) {
178
306
  return out;
179
307
  }
180
308
 
309
+ function listSourceFiles(source) {
310
+ const out = [];
311
+ for (const dir of source.dirs) {
312
+ out.push(...listSessionFiles(dir, source.fileFilter || isTranscriptFile));
313
+ if (out.length >= MAX_FILES_PER_TOOL) return out.slice(0, MAX_FILES_PER_TOOL);
314
+ }
315
+ return out;
316
+ }
317
+
318
+ function* sourceSessions(source) {
319
+ if (typeof source.sessions === 'function') {
320
+ yield* source.sessions();
321
+ return;
322
+ }
323
+ const subagent = source.subagent || isSubagentFile;
324
+ for (const file of listSourceFiles(source)) {
325
+ yield { file, objects: readSourceObjects(source, file), subagent: subagent(file) };
326
+ }
327
+ }
328
+
181
329
  // Bounded whole-file text read (for repo-level markdown sources, e.g. Aider).
182
330
  function readTextFile(file) {
183
331
  let fd;
@@ -304,9 +452,7 @@ function* iterateSessionObjects(env = process.env) {
304
452
  const seenCwds = new Set();
305
453
  for (const source of sources) {
306
454
  if (source.repoLevel) continue;
307
- const files = listSessionFiles(source.dir, source.fileFilter || isTranscriptFile);
308
- for (const file of files) {
309
- const objects = readSourceObjects(source, file);
455
+ for (const { file, objects } of sourceSessions(source)) {
310
456
  const cwd = resolveFileCwd(objects);
311
457
  if (cwd) seenCwds.add(cwd);
312
458
  for (const obj of objects) {
@@ -360,9 +506,10 @@ function makeSessionsAccumulator() {
360
506
  },
361
507
  };
362
508
  },
363
- addFile(file, objects) {
509
+ addFile(file, objects, subagent = isSubagentFile(file)) {
364
510
  for (const o of objects) harvestObject(o, cur.acc);
365
- if (isSubagentFile(file)) {
511
+ if (objects.some((o) => o && o.planMode === true)) cur.acc.planningSignalCount += 1;
512
+ if (subagent) {
366
513
  cur.subagentRuns += 1;
367
514
  } else {
368
515
  cur.rootSessions += 1;
@@ -424,15 +571,12 @@ function walkSessionSignals(env = process.env, selectedCwds = null, onHumanObjec
424
571
  // Phase 1: home-dir sources; remember the repo toplevels (cwds) they reveal.
425
572
  for (const source of sources) {
426
573
  if (source.repoLevel) continue;
427
- const files = listSessionFiles(source.dir, source.fileFilter || isTranscriptFile);
428
- if (files.length === 0) continue;
429
574
  sessions.startTool(source.tool);
430
- for (const f of files) {
431
- const objects = readSourceObjects(source, f);
575
+ for (const { file, objects, subagent } of sourceSessions(source)) {
432
576
  const cwd = resolveFileCwd(objects);
433
577
  if (cwd) seenCwds.add(cwd);
434
578
  if (!fileInScope(cwd, selectedCwds)) continue;
435
- sessions.addFile(f, objects);
579
+ sessions.addFile(file, objects, subagent);
436
580
  if (onHumanObject) {
437
581
  for (const o of objects) onHumanObject(o);
438
582
  }
@@ -468,6 +612,7 @@ module.exports = {
468
612
  makeSessionsAccumulator,
469
613
  walkSessionSignals,
470
614
  toolSources,
615
+ sourceSessions,
471
616
  listSessionFiles,
472
617
  readSessionObjects,
473
618
  resolveFileCwd,
@@ -48,7 +48,9 @@ function agentDefinitionsByName(report) {
48
48
  return byName;
49
49
  }
50
50
 
51
- function derivePayload(report, maturity) {
51
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
52
+
53
+ function derivePayload(report, maturity, { cliCommandId = null } = {}) {
52
54
  const agents = Array.isArray(report.agents) ? report.agents : [];
53
55
  const catalogIdsByName = agentCatalogIdsByName(report);
54
56
  const definitionsByName = agentDefinitionsByName(report);
@@ -141,6 +143,7 @@ function derivePayload(report, maturity) {
141
143
  decisions: whitelistDecisions(report.decisions),
142
144
  // PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
143
145
  detectionEvidence: whitelistDetectionEvidence(report.detectionEvidence),
146
+ ...(typeof cliCommandId === 'string' && UUID_RE.test(cliCommandId) ? { cliCommandId } : {}),
144
147
  };
145
148
  }
146
149
 
package/src/share.js CHANGED
@@ -43,7 +43,7 @@ async function requestJsonBackend(endpoint, body, { timeoutMs = 15000, idempoten
43
43
 
44
44
  /* ---------- automatic sending ---------- */
45
45
 
46
- async function autoShare(report, maturity, { root, bypassThrottle = false } = {}) {
46
+ async function autoShare(report, maturity, { root, bypassThrottle = false, cliCommandId = null } = {}) {
47
47
  const state = loadConsentState();
48
48
  const decision = getConsentDecision(state);
49
49
 
@@ -67,7 +67,7 @@ async function autoShare(report, maturity, { root, bypassThrottle = false } = {}
67
67
  return { ok: false, skipped: true, reason: 'no-endpoint-configured' };
68
68
  }
69
69
 
70
- const payload = derivePayload(report, maturity);
70
+ const payload = derivePayload(report, maturity, { cliCommandId });
71
71
 
72
72
  const idempotencyKey = crypto.randomUUID();
73
73
  let res;
@@ -1,11 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { postJsonWithTimeout, extractSetCookie } = require('./backend-request');
4
- const { SESSION_COOKIE_NAME } = require('./auth-client');
3
+ const { postJsonWithTimeout } = require('./backend-request');
5
4
  const { toUpperLang } = require('./lang-codes');
6
5
 
7
- // Register a talent via the Hub's better-auth email engine, in two steps (WEB-login parity): 1.
8
-
9
6
  const DEFAULT_TIMEOUT_MS = 30000;
10
7
 
11
8
  function parseJson(raw) {
@@ -29,6 +26,12 @@ function buildRegistrationContext({ preferredLanguage, newsletterConsent, freela
29
26
  return context;
30
27
  }
31
28
 
29
+ const TERMS_VERSION = '2026-10-22';
30
+
31
+ function buildRegistrationConsent({ newsletterConsent } = {}) {
32
+ return { termsAccepted: true, termsVersion: TERMS_VERSION, newsletterConsent: newsletterConsent === true };
33
+ }
34
+
32
35
  // Hub refuses these claims with a coded error and creates nothing, so registering without the code is safe.
33
36
  const UNCLAIMABLE_CODES = new Set(['auth-engine.claim_code_invalid', 'users.unregistered_talent_not_claimable']);
34
37
 
@@ -53,13 +56,14 @@ async function postCompleteRegistration(body, { endpoint, headers, timeoutMs })
53
56
  // Shared complete-registration call. With a claimCode it claims the MCP sign-up's UNREGISTERED profile; an unclaimable code still registers the account, with claimed:false.
54
57
  async function requestCompleteRegistration(
55
58
  context = {},
56
- { endpoint, cookie = null, bearerToken = null, claimCode = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
59
+ { endpoint, bearerToken = null, claimCode = null, consent = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
57
60
  ) {
58
61
  if (!endpoint) return { ok: false, reason: 'no-endpoint' };
59
62
  const headers = {};
60
- if (cookie) headers.Cookie = cookie;
61
63
  if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`;
62
- const base = { userType: 'WORKS_TALENT', context };
64
+ const base = consent && consent.termsAccepted === true
65
+ ? { userType: 'WORKS_TALENT', context, consent }
66
+ : { userType: 'WORKS_TALENT', context };
63
67
  if (typeof claimCode !== 'string' || !claimCode) return postCompleteRegistration(base, { endpoint, headers, timeoutMs });
64
68
 
65
69
  const claimed = await postCompleteRegistration({ ...base, claimCode }, { endpoint, headers, timeoutMs });
@@ -69,35 +73,4 @@ async function requestCompleteRegistration(
69
73
  return plain.ok ? { ok: true, claimed: false, claimError: claimed.code } : plain;
70
74
  }
71
75
 
72
- async function requestSignUp(
73
- { name, lastName, email, password, preferredLanguage, newsletterConsent, freelanceType, freelanceIntent, claimCode = null } = {},
74
- { signUpEndpoint, completeRegistrationEndpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
75
- ) {
76
- if (!signUpEndpoint) return { ok: false, reason: 'no-endpoint' };
77
- if (!name || !lastName || !email || !password) return { ok: false, reason: 'missing-fields' };
78
-
79
- let signUp;
80
- try {
81
- signUp = await postJsonWithTimeout(signUpEndpoint, { email, password, name, lastName }, timeoutMs);
82
- } catch (e) {
83
- return { ok: false, reason: (e && e.kind) || 'network-error' };
84
- }
85
-
86
- // A duplicate email is a normal outcome the caller handles by switching to login.
87
- if (signUp.status === 409 || signUp.status === 422) return { ok: true, accountExists: true };
88
- if (signUp.status < 200 || signUp.status >= 300) {
89
- return { ok: false, reason: `http-${signUp.status}`, status: signUp.status, message: messageFrom(signUp.raw) };
90
- }
91
-
92
- const cookie = extractSetCookie(signUp.headers, SESSION_COOKIE_NAME);
93
- if (!cookie) return { ok: false, reason: 'bad-response' };
94
-
95
- if (!completeRegistrationEndpoint) return { ok: false, reason: 'no-endpoint' };
96
-
97
- const context = buildRegistrationContext({ preferredLanguage, newsletterConsent, freelanceType, freelanceIntent });
98
- const complete = await requestCompleteRegistration(context, { endpoint: completeRegistrationEndpoint, cookie, claimCode, timeoutMs });
99
- if (!complete.ok) return complete;
100
- return claimCode ? { ok: true, accountExists: false, claimed: complete.claimed === true } : { ok: true, accountExists: false };
101
- }
102
-
103
- module.exports = { requestSignUp, requestCompleteRegistration, buildRegistrationContext };
76
+ module.exports = { requestCompleteRegistration, buildRegistrationContext, buildRegistrationConsent, TERMS_VERSION };