shakers-cli 0.16.0 → 0.18.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 +21 -6
- package/bin/ai-usage.js +31 -12
- package/bin/command-audit-worker.js +19 -0
- package/bin/login.js +4 -8
- package/bin/mcp.js +32 -21
- package/bin/register.js +12 -30
- package/bin/shakers-cli.js +15 -10
- package/package.json +1 -1
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/agents-client.js +1 -0
- package/src/ai-usage-review.js +103 -0
- package/src/auth-client.js +7 -107
- package/src/auth-session-store.js +12 -11
- package/src/backend-request.js +3 -24
- package/src/cli-skill.js +77 -0
- package/src/command-audit.js +47 -4
- package/src/config.js +2 -16
- package/src/consent-flow.js +9 -3
- package/src/detect-repos.js +4 -8
- 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 +186 -102
- package/src/mcp-ai-usage-result-tool.js +38 -14
- package/src/mcp-ai-usage-tool.js +317 -172
- 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 +292 -53
- package/src/mcp-instructions.js +43 -17
- package/src/mcp-onboarding-tools.js +1 -3
- package/src/mcp-prompts.js +8 -6
- package/src/mcp-register-tools.js +25 -14
- package/src/mcp-role-tools.js +59 -18
- 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 +250 -133
- package/src/onboarding-flow.js +72 -146
- package/src/onboarding-funnel.js +46 -6
- package/src/opencode-sessions.js +92 -0
- package/src/profile-agents.js +26 -0
- package/src/render-report-text.js +1 -1
- package/src/report-gating.js +4 -2
- package/src/report-store.js +7 -2
- package/src/roles-flow.js +8 -5
- 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-consent-state.js +2 -2
- 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 +545 -58
- package/src/skill-install.js +102 -0
- package/src/stdin-ask.js +13 -1
- package/src/tier-engine.js +14 -2
- package/src/trust-narration.js +32 -0
- package/src/usage-run-steps.js +68 -6
- package/src/usage-scan.js +11 -8
- package/src/brand-svg.js +0 -9
- package/src/inter-fonts.js +0 -6
- package/src/mcp-auth-form.js +0 -480
package/src/report-gating.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { loadAuthSession, sessionStatus, isPlatformTalent } = require('./auth-session-store');
|
|
4
4
|
const { getProfile } = require('./config');
|
|
5
|
+
const { hasAccountTrace } = require('./account-trace');
|
|
5
6
|
|
|
6
7
|
// THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY.
|
|
7
8
|
|
|
8
9
|
// Pure: identity in, policy out.
|
|
9
|
-
function resolveReportGate({ loggedIn = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
|
|
10
|
+
function resolveReportGate({ loggedIn = false, accountTrace = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
|
|
10
11
|
const showEverything = !loggedIn;
|
|
11
12
|
const resolvedProfile = profile === 'external' ? 'external' : 'talent';
|
|
12
13
|
return Object.freeze({
|
|
@@ -16,7 +17,7 @@ function resolveReportGate({ loggedIn = false, status = null, email = null, user
|
|
|
16
17
|
email: loggedIn && typeof email === 'string' && email ? email : null,
|
|
17
18
|
// --roadmap AND its server-side computation/egress (see bin/report.js): a
|
|
18
19
|
// logged-in Talent's roadmap is not merely hidden, it is not requested.
|
|
19
|
-
showRoadmap: showEverything,
|
|
20
|
+
showRoadmap: showEverything && !accountTrace,
|
|
20
21
|
// Per-agent "how to improve this agent" tips in the footprint report.
|
|
21
22
|
showAgentSuggestions: showEverything,
|
|
22
23
|
// The copyable remediation prompt in a `certify` verdict.
|
|
@@ -35,6 +36,7 @@ function reportGateForIdentity(env = process.env, now = Date.now()) {
|
|
|
35
36
|
const status = sessionStatus(session, now);
|
|
36
37
|
return resolveReportGate({
|
|
37
38
|
loggedIn: status === 'active',
|
|
39
|
+
accountTrace: hasAccountTrace(env),
|
|
38
40
|
status,
|
|
39
41
|
email: session ? session.email : null,
|
|
40
42
|
userType: session ? session.userType : null,
|
package/src/report-store.js
CHANGED
|
@@ -54,10 +54,10 @@ function migrateFromV1(parsed) {
|
|
|
54
54
|
return { schemaVersion: SCHEMA_VERSION, updatedAt: (parsed && parsed.updatedAt) || null, projects };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function loadState() {
|
|
57
|
+
function loadState(env = process.env) {
|
|
58
58
|
let parsed;
|
|
59
59
|
try {
|
|
60
|
-
parsed = JSON.parse(fs.readFileSync(statePath(), 'utf8'));
|
|
60
|
+
parsed = JSON.parse(fs.readFileSync(statePath(env), 'utf8'));
|
|
61
61
|
} catch {
|
|
62
62
|
return freshState();
|
|
63
63
|
}
|
|
@@ -154,6 +154,10 @@ function recordBackendAcceptance({ root, kind, backend }) {
|
|
|
154
154
|
return stampAndSaveState(state, absRoot);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function hasRecordedUpload(env = process.env) {
|
|
158
|
+
return Object.values(loadState(env).projects).some((p) => !!(p && p.backendAcceptance && Object.keys(p.backendAcceptance).length));
|
|
159
|
+
}
|
|
160
|
+
|
|
157
161
|
function loadFootprint({ root } = {}) {
|
|
158
162
|
const absRoot = path.resolve(root || process.cwd());
|
|
159
163
|
const state = loadState();
|
|
@@ -220,6 +224,7 @@ module.exports = {
|
|
|
220
224
|
loadAgentClassifications,
|
|
221
225
|
loadFootprint,
|
|
222
226
|
recordBackendAcceptance,
|
|
227
|
+
hasRecordedUpload,
|
|
223
228
|
materializeProjectReport,
|
|
224
229
|
certKey,
|
|
225
230
|
SCHEMA_VERSION,
|
package/src/roles-flow.js
CHANGED
|
@@ -88,6 +88,7 @@ async function runAddRole({ io, session, catalog, deps = makeRolesDeps() }) {
|
|
|
88
88
|
// deduped, for setting/switching the MAIN role.
|
|
89
89
|
async function fetchHeldRoles({ deps, hubAccessToken }) {
|
|
90
90
|
const held = [];
|
|
91
|
+
const assigned = [];
|
|
91
92
|
let mainClusterId = null;
|
|
92
93
|
const certEndpoint = deps.getMyCertificationsEndpoint();
|
|
93
94
|
if (certEndpoint) {
|
|
@@ -99,10 +100,11 @@ async function fetchHeldRoles({ deps, hubAccessToken }) {
|
|
|
99
100
|
}
|
|
100
101
|
const assignedEndpoint = deps.getAssignedClustersEndpoint();
|
|
101
102
|
if (assignedEndpoint) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
103
|
+
const res = await deps.requestAssignedClusters({ hubAccessToken }, { endpoint: assignedEndpoint });
|
|
104
|
+
if (res.ok) assigned.push(...(res.clusters || []));
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
held.push(...assigned);
|
|
107
|
+
return { held: dedupeByCluster(held), assigned, mainClusterId };
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// `change-role`: switch the MAIN role to one of the roles the talent already holds.
|
|
@@ -130,12 +132,12 @@ async function runChangeMainRole({ io, session, catalog, deps = makeRolesDeps()
|
|
|
130
132
|
|
|
131
133
|
// The onboarding final step: pick a MAIN role from the roles resolved for the
|
|
132
134
|
// talent (interview recommendations + any held). Degrades gracefully to a skip.
|
|
133
|
-
async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps() }) {
|
|
135
|
+
async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps(), onRolesShown = () => {} }) {
|
|
134
136
|
const r = catalog.roles;
|
|
135
137
|
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
136
138
|
|
|
137
139
|
io.section(r.mainRoleTitle);
|
|
138
|
-
const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
|
|
140
|
+
const { held, assigned, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
|
|
139
141
|
|
|
140
142
|
// Prefer the talent's own/recommended roles; fall back to the catalog for a
|
|
141
143
|
// fresh talent so a main role can always be set (picking one adds it first).
|
|
@@ -150,6 +152,7 @@ async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRole
|
|
|
150
152
|
io.notify(r.mainRolePending);
|
|
151
153
|
return { ok: true, chosen: null };
|
|
152
154
|
}
|
|
155
|
+
onRolesShown({ suggested: assigned.filter((c) => c.type === 'RECOMMENDED').map((c) => c.clusterId), clustersCount: assigned.length });
|
|
153
156
|
const choice = await chooseRole({
|
|
154
157
|
io, roles, header: r.mainRolePrompt, hint: r.selectHint, initialClusterId: mainClusterId,
|
|
155
158
|
});
|
|
@@ -27,10 +27,10 @@ async function runToggleSave({ io, rawOut, session, catalog, mode, opts = {}, de
|
|
|
27
27
|
}
|
|
28
28
|
const positionId = resolved.id;
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const res = await io.withProgress(label, () => (mode === 'save'
|
|
30
|
+
const toggle = () => (mode === 'save'
|
|
32
31
|
? deps.savePosition({ hubAccessToken, positionId })
|
|
33
|
-
: deps.unsavePosition({ hubAccessToken, positionId }))
|
|
32
|
+
: deps.unsavePosition({ hubAccessToken, positionId }));
|
|
33
|
+
const res = opts.json ? await toggle() : await io.withProgress(mode === 'save' ? fp.saving : fp.unsaving, toggle);
|
|
34
34
|
if (!res.ok) {
|
|
35
35
|
io.error(res.reason === 'not-found' ? fp.notFound : fp.failed(res.reason));
|
|
36
36
|
return { ok: false, reason: res.reason };
|
package/src/scanner.js
CHANGED
|
@@ -12,7 +12,8 @@ const { detectMcpServers, buildMcpAggregate } = require('./mcp-detector');
|
|
|
12
12
|
const { analyzeMemoryStructure } = require('./memory-structure-detector');
|
|
13
13
|
const { detectAutomations } = require('./automations-detector');
|
|
14
14
|
const { detectBrowserTools } = require('./browser-tools-detector');
|
|
15
|
-
const { getHomeDir } = require('./env-paths');
|
|
15
|
+
const { getHomeDir, codexHome, piAgentDir } = require('./env-paths');
|
|
16
|
+
const { OWN_SKILL_NAMES } = require('./cli-skill');
|
|
16
17
|
|
|
17
18
|
/* ---------- existence-check utilities (existence only, never content) ---------- */
|
|
18
19
|
|
|
@@ -85,14 +86,30 @@ function resolveSignalPath(sig, root) {
|
|
|
85
86
|
|
|
86
87
|
/* ---------- DEPTH probes: return ONLY numbers ---------- */
|
|
87
88
|
|
|
88
|
-
function
|
|
89
|
+
function countSkillDirs(p) {
|
|
89
90
|
try {
|
|
90
|
-
return fs.readdirSync(p, { withFileTypes: true })
|
|
91
|
+
return fs.readdirSync(p, { withFileTypes: true })
|
|
92
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !OWN_SKILL_NAMES.has(e.name)).length;
|
|
91
93
|
} catch {
|
|
92
94
|
return 0;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function countFilesDeep(p, ext, depth = 3) {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(p, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
let n = 0;
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (e.isFile() && e.name.endsWith(ext)) n += 1;
|
|
108
|
+
else if (e.isDirectory() && depth > 0) n += countFilesDeep(path.join(p, e.name), ext, depth - 1);
|
|
109
|
+
}
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
function countFiles(p, ext) {
|
|
97
114
|
try {
|
|
98
115
|
return fs.readdirSync(p).filter((f) => (ext ? f.endsWith(ext) : true)).length;
|
|
@@ -113,6 +130,49 @@ function countJsonKeys(file, key) {
|
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
function claudeHooks(root) {
|
|
134
|
+
return (
|
|
135
|
+
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
136
|
+
countJsonKeys(path.join(root, '.claude', 'settings.local.json'), 'hooks') +
|
|
137
|
+
countJsonKeys(path.join(getHomeDir(), '.claude', 'settings.json'), 'hooks')
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const TOML_HOOK_TABLE = /^\s*\[\[?\s*hooks\.([A-Za-z]+)/;
|
|
142
|
+
|
|
143
|
+
function countTomlHookEvents(file) {
|
|
144
|
+
let raw;
|
|
145
|
+
try {
|
|
146
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
147
|
+
} catch {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const events = new Set();
|
|
151
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
152
|
+
const m = TOML_HOOK_TABLE.exec(line);
|
|
153
|
+
if (m && m[1] !== 'state') events.add(m[1]);
|
|
154
|
+
}
|
|
155
|
+
return events.size;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function codexHooks(root) {
|
|
159
|
+
return [codexHome(), path.join(root, '.codex')].reduce(
|
|
160
|
+
(n, dir) => n + countJsonKeys(path.join(dir, 'hooks.json'), 'hooks') + countTomlHookEvents(path.join(dir, 'config.toml')),
|
|
161
|
+
0,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const CLINE_HOOK_TYPES = new Set(['TaskStart', 'TaskResume', 'TaskCancel', 'TaskComplete', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Notification', 'PreCompact']);
|
|
166
|
+
|
|
167
|
+
function countClineHooks(dir) {
|
|
168
|
+
try {
|
|
169
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
170
|
+
.filter((e) => e.isFile() && CLINE_HOOK_TYPES.has(path.parse(e.name).name)).length;
|
|
171
|
+
} catch {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
116
176
|
/* ---------- config footprint: ONLY size in bytes and file count ---------- */
|
|
117
177
|
/* No file name or path is ever stored: only numbers are aggregated. */
|
|
118
178
|
|
|
@@ -273,17 +333,20 @@ const probes = {
|
|
|
273
333
|
'claude-code': (root, ctx) => ({
|
|
274
334
|
mcpServers: mcpCount(ctx, 'claude-code'),
|
|
275
335
|
skills:
|
|
276
|
-
|
|
277
|
-
|
|
336
|
+
countSkillDirs(path.join(root, '.claude', 'skills')) +
|
|
337
|
+
countSkillDirs(path.join(getHomeDir(), '.claude', 'skills')),
|
|
278
338
|
commands: countFiles(path.join(root, '.claude', 'commands'), '.md'),
|
|
279
339
|
instructions: exists(path.join(root, 'CLAUDE.md')) ? 1 : 0,
|
|
280
|
-
hooks:
|
|
340
|
+
hooks: claudeHooks(root),
|
|
281
341
|
}),
|
|
282
342
|
cursor: (root, ctx) => ({
|
|
283
343
|
rules:
|
|
284
344
|
countFiles(path.join(root, '.cursor', 'rules'), '.mdc') +
|
|
285
345
|
(exists(path.join(root, '.cursorrules')) ? 1 : 0),
|
|
286
346
|
mcpServers: mcpCount(ctx, 'cursor'),
|
|
347
|
+
hooks:
|
|
348
|
+
countJsonKeys(path.join(root, '.cursor', 'hooks.json'), 'hooks') +
|
|
349
|
+
countJsonKeys(path.join(getHomeDir(), '.cursor', 'hooks.json'), 'hooks'),
|
|
287
350
|
}),
|
|
288
351
|
'github-copilot': (root) => ({
|
|
289
352
|
instructions: exists(path.join(root, '.github', 'copilot-instructions.md')) ? 1 : 0,
|
|
@@ -307,10 +370,41 @@ const probes = {
|
|
|
307
370
|
'gemini-cli': (root, ctx) => ({
|
|
308
371
|
instructions: exists(path.join(root, 'GEMINI.md')) ? 1 : 0,
|
|
309
372
|
mcpServers: mcpCount(ctx, 'gemini-cli'),
|
|
373
|
+
commands:
|
|
374
|
+
countFilesDeep(path.join(root, '.gemini', 'commands'), '.toml') +
|
|
375
|
+
countFilesDeep(path.join(getHomeDir(), '.gemini', 'commands'), '.toml'),
|
|
376
|
+
hooks:
|
|
377
|
+
countJsonKeys(path.join(root, '.gemini', 'settings.json'), 'hooks') +
|
|
378
|
+
countJsonKeys(path.join(getHomeDir(), '.gemini', 'settings.json'), 'hooks'),
|
|
310
379
|
}),
|
|
311
|
-
'codex-cli': (root) => ({
|
|
380
|
+
'codex-cli': (root, ctx) => ({
|
|
312
381
|
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
382
|
+
mcpServers: mcpCount(ctx, 'codex-cli'),
|
|
383
|
+
skills: countSkillDirs(path.join(codexHome(), 'skills')),
|
|
384
|
+
commands: countFiles(path.join(codexHome(), 'prompts'), '.md'),
|
|
385
|
+
hooks: codexHooks(root),
|
|
313
386
|
}),
|
|
387
|
+
cline: (root) => ({
|
|
388
|
+
hooks:
|
|
389
|
+
countClineHooks(path.join(root, '.clinerules', 'hooks')) +
|
|
390
|
+
countClineHooks(path.join(getHomeDir(), 'Documents', 'Cline', 'Hooks')),
|
|
391
|
+
}),
|
|
392
|
+
pi: (root, ctx) => ({
|
|
393
|
+
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
394
|
+
mcpServers: mcpCount(ctx, 'pi'),
|
|
395
|
+
skills: countSkillDirs(path.join(root, '.pi', 'skills')) + countSkillDirs(path.join(piAgentDir(), 'skills')),
|
|
396
|
+
commands: countFiles(path.join(root, '.pi', 'prompts'), '.md') + countFiles(path.join(piAgentDir(), 'prompts'), '.md'),
|
|
397
|
+
}),
|
|
398
|
+
opencode: (root, ctx) => {
|
|
399
|
+
const home = path.join(getHomeDir(), '.config', 'opencode');
|
|
400
|
+
const dirs = [path.join(root, '.opencode'), home];
|
|
401
|
+
return {
|
|
402
|
+
instructions: exists(path.join(root, 'AGENTS.md')) ? 1 : 0,
|
|
403
|
+
mcpServers: mcpCount(ctx, 'opencode'),
|
|
404
|
+
skills: dirs.reduce((n, d) => n + countSkillDirs(path.join(d, 'skills')) + countSkillDirs(path.join(d, 'skill')), 0),
|
|
405
|
+
commands: dirs.reduce((n, d) => n + countFiles(path.join(d, 'commands'), '.md') + countFiles(path.join(d, 'command'), '.md'), 0),
|
|
406
|
+
};
|
|
407
|
+
},
|
|
314
408
|
trae: (root) => ({
|
|
315
409
|
// Low confidence: `.trae/rules` structure not verified (see detectors.js).
|
|
316
410
|
rules: countFiles(path.join(root, '.trae', 'rules')),
|
|
@@ -325,16 +419,14 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
|
325
419
|
return {
|
|
326
420
|
agents: agentsCount,
|
|
327
421
|
skills:
|
|
328
|
-
|
|
329
|
-
|
|
422
|
+
countSkillDirs(path.join(root, '.claude', 'skills')) +
|
|
423
|
+
countSkillDirs(path.join(home, '.claude', 'skills')),
|
|
330
424
|
commands:
|
|
331
425
|
countFiles(path.join(root, '.claude', 'commands'), '.md') +
|
|
332
426
|
countFiles(path.join(home, '.claude', 'commands'), '.md'),
|
|
333
427
|
// The third copy of the MCP paths lived here.
|
|
334
428
|
mcpServers: (mcp && typeof mcp.total === 'number' ? mcp.total : 0),
|
|
335
|
-
hooks:
|
|
336
|
-
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
337
|
-
countJsonKeys(path.join(home, '.claude', 'settings.json'), 'hooks'),
|
|
429
|
+
hooks: claudeHooks(root),
|
|
338
430
|
};
|
|
339
431
|
}
|
|
340
432
|
|
package/src/session-gate.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
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
3
|
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
7
4
|
const { ensureFreshSession } = require('./session-refresh');
|
|
8
5
|
|
|
@@ -20,7 +17,7 @@ async function hasUsableSession(env = process.env, deps = {}) {
|
|
|
20
17
|
|
|
21
18
|
const session = load(env);
|
|
22
19
|
if (sessionStatus(session, now) === 'active') return true;
|
|
23
|
-
if (!session ||
|
|
20
|
+
if (!session || !session.sessionToken) return false;
|
|
24
21
|
|
|
25
22
|
let refreshed = null;
|
|
26
23
|
try { refreshed = await refresh(env, { now }); } catch { refreshed = null; }
|
|
@@ -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;
|