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.
- package/README.md +15 -5
- package/bin/add-agent.js +12 -6
- package/bin/ai-usage.js +2 -1
- package/bin/apply.js +66 -0
- package/bin/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +32 -19
- package/bin/register.js +10 -29
- package/bin/shakers-cli.js +21 -12
- package/package.json +1 -1
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +5 -3
- package/src/apply-client.js +164 -0
- package/src/apply-flow.js +105 -0
- package/src/auth-client.js +7 -107
- package/src/auth-session-store.js +6 -12
- package/src/backend-request.js +1 -24
- package/src/cli-skill.js +77 -0
- package/src/command-audit.js +58 -5
- package/src/config.js +15 -16
- package/src/consent-flow.js +7 -2
- package/src/detect-repos.js +4 -8
- package/src/detected-agents.js +40 -0
- package/src/detectors.js +23 -0
- package/src/device-auth-store.js +2 -2
- package/src/device-login.js +11 -9
- package/src/env-paths.js +9 -1
- package/src/i18n.js +258 -88
- package/src/mcp-ai-usage-result-tool.js +6 -4
- package/src/mcp-ai-usage-tool.js +73 -47
- package/src/mcp-apply-tools.js +152 -0
- package/src/mcp-choice.js +171 -34
- package/src/mcp-detector.js +26 -2
- package/src/mcp-device-login-tools.js +40 -8
- package/src/mcp-install.js +15 -11
- package/src/mcp-instructions.js +43 -17
- package/src/mcp-onboarding-tools.js +90 -3
- package/src/mcp-prompts.js +6 -4
- package/src/mcp-register-tools.js +25 -14
- package/src/mcp-role-tools.js +35 -17
- package/src/mcp-server-name.js +7 -0
- package/src/mcp-server.js +35 -7
- package/src/mcp-signup-client.js +3 -6
- package/src/mcp-signup-flow.js +389 -0
- package/src/mcp-signup-tools.js +237 -126
- package/src/onboarding-flow.js +30 -125
- package/src/opencode-sessions.js +92 -0
- package/src/profile-agents.js +26 -0
- package/src/saved-positions-flow.js +3 -3
- package/src/scanner.js +104 -12
- package/src/session-gate.js +1 -4
- package/src/session-normalizers.js +114 -8
- package/src/session-refresh.js +5 -16
- package/src/session-scan.js +167 -22
- package/src/share-payload.js +4 -1
- package/src/share.js +2 -2
- package/src/signup-client.js +12 -39
- package/src/signup-copy.js +506 -68
- package/src/skill-install.js +102 -0
- package/src/start-add-agents.js +106 -3
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +16 -4
- package/src/usage-scan.js +3 -0
- package/src/brand-svg.js +0 -9
- package/src/inter-fonts.js +0 -6
- package/src/mcp-auth-form.js +0 -484
|
@@ -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
|
};
|
package/src/session-refresh.js
CHANGED
|
@@ -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.
|
|
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({
|
|
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
|
-
|
|
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 &&
|
|
75
|
-
await requestSignOut({
|
|
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;
|
package/src/session-scan.js
CHANGED
|
@@ -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([
|
|
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',
|
|
104
|
-
{ tool: 'codex',
|
|
105
|
-
{ tool: 'gemini',
|
|
106
|
-
|
|
107
|
-
{ tool: '
|
|
108
|
-
{ tool: '
|
|
109
|
-
{ tool: '
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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,
|
package/src/share-payload.js
CHANGED
|
@@ -48,7 +48,9 @@ function agentDefinitionsByName(report) {
|
|
|
48
48
|
return byName;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
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;
|
package/src/signup-client.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { postJsonWithTimeout
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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 };
|