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
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const os = require('os');
4
5
  const path = require('path');
6
+ const { spawn } = require('child_process');
5
7
  const { scrubSecrets } = require('./agent-synthesis');
6
8
  const { loadAuthSession, decodeJwtPayload } = require('./auth-session-store');
7
9
  const { getCertsBase } = require('./config');
@@ -13,6 +15,13 @@ const AUDIT_TIMEOUT_MS = 1500;
13
15
  // Capped at 200 chars AFTER scrubbing (mirrors MAX_COMMAND_LEN in session-scan.js) so a secret straddling the cut cannot leak.
14
16
  const MAX_ARGS_LEN = 200;
15
17
 
18
+ const WORKER = path.join(__dirname, '..', 'bin', 'command-audit-worker.js');
19
+ const PAYLOAD_VAR = 'SHAKERS_CLI_AUDIT_PAYLOAD';
20
+
21
+ function auditDisabled(env) {
22
+ return !!env && env.SHAKERS_CLI_NO_AUDIT === '1';
23
+ }
24
+
16
25
  function cliVersion() {
17
26
  try {
18
27
  return require(path.join(__dirname, '..', 'package.json')).version || null;
@@ -28,6 +37,16 @@ function redactArgs(argv) {
28
37
  return scrubbed || null;
29
38
  }
30
39
 
40
+ // `apply` takes only the position ref: anything typed after it could be the talent's message, so it never reaches the log.
41
+ function withoutFreeText(argv) {
42
+ if (!Array.isArray(argv)) return argv;
43
+ const words = argv.filter((a) => typeof a === 'string' && !a.startsWith('-')).map((w) => w.toLowerCase());
44
+ const at = words[0] === 'apply' ? 0 : words[0] === 'projects' && words[1] === 'apply' ? 1 : -1;
45
+ if (at === -1) return argv;
46
+ const kept = argv.filter((a) => typeof a === 'string' && !a.startsWith('-'));
47
+ return kept.slice(0, at + 2);
48
+ }
49
+
31
50
  // Anonymous runs carry only a best-effort machine hint; authenticated runs identify via the Bearer instead.
32
51
  function machineHint() {
33
52
  let osUser = null;
@@ -52,7 +71,7 @@ function jwtExpired(token, now) {
52
71
  }
53
72
 
54
73
  // Builds the record + headers; returns null when there is no endpoint to post to.
55
- function buildAudit(command, argv, env) {
74
+ function buildAudit(command, argv, env, id = null) {
56
75
  let base;
57
76
  try {
58
77
  base = getCertsBase(env);
@@ -66,8 +85,9 @@ function buildAudit(command, argv, env) {
66
85
  const token = stored && !jwtExpired(stored, Date.now()) ? stored : null;
67
86
 
68
87
  const body = {
88
+ ...(id ? { id } : {}),
69
89
  command: String(command || 'unknown').slice(0, 100),
70
- args: redactArgs(argv),
90
+ args: redactArgs(withoutFreeText(argv)),
71
91
  cliVersion: cliVersion(),
72
92
  clientTimestamp: new Date().toISOString(),
73
93
  };
@@ -85,9 +105,10 @@ function buildAudit(command, argv, env) {
85
105
  }
86
106
 
87
107
  // Emits ONE fire-and-forget audit record; never throws or rejects. Returns the `.catch`-guarded promise so tests can await it (production does not).
88
- function emitCommandAudit({ command, argv = [], env = process.env } = {}) {
108
+ function emitCommandAudit({ command, argv = [], env = process.env, id = null } = {}) {
89
109
  try {
90
- const audit = buildAudit(command, argv, env);
110
+ if (auditDisabled(env)) return Promise.resolve();
111
+ const audit = buildAudit(command, argv, env, id);
91
112
  if (!audit) return Promise.resolve();
92
113
  return postJsonWithTimeout(
93
114
  audit.endpoint,
@@ -105,4 +126,36 @@ function emitCommandAudit({ command, argv = [], env = process.env } = {}) {
105
126
  }
106
127
  }
107
128
 
108
- module.exports = { emitCommandAudit, redactArgs, buildAudit };
129
+ async function logCommandRun({ command, argv = [], env = process.env } = {}) {
130
+ if (auditDisabled(env)) return null;
131
+ const id = crypto.randomUUID();
132
+ await emitCommandAudit({ command, argv, env, id });
133
+ return id;
134
+ }
135
+
136
+ let runId = null;
137
+
138
+ function startCommandAudit({ command, argv = [], env = process.env } = {}) {
139
+ if (auditDisabled(env)) return null;
140
+ const id = crypto.randomUUID();
141
+ try {
142
+ const child = spawn(process.execPath, [WORKER], {
143
+ detached: true,
144
+ stdio: 'ignore',
145
+ windowsHide: true,
146
+ env: { ...env, [PAYLOAD_VAR]: JSON.stringify({ id, command, args: redactArgs(withoutFreeText(argv)) }) },
147
+ });
148
+ child.on('error', () => {});
149
+ child.unref();
150
+ } catch {
151
+ return null;
152
+ }
153
+ runId = id;
154
+ return id;
155
+ }
156
+
157
+ function currentRunId() {
158
+ return runId;
159
+ }
160
+
161
+ module.exports = { emitCommandAudit, startCommandAudit, logCommandRun, currentRunId, redactArgs, withoutFreeText, buildAudit, PAYLOAD_VAR };
package/src/config.js CHANGED
@@ -300,14 +300,6 @@ function getAgentEvaluationEndpoint(env = process.env) {
300
300
  return deriveFromIngest(env, 'agent-evaluation');
301
301
  }
302
302
 
303
- // Login endpoint — better-auth email sign-in: `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
304
- function getLoginEndpoint(env = process.env) {
305
- const explicit = readEnv(env, 'LOGIN_ENDPOINT');
306
- if (explicit && explicit.trim()) return explicit.trim();
307
- return deriveFromHub(env, 'auth/sign-in/email');
308
- }
309
-
310
- // better-auth JWT endpoint: `GET {hubBase}/auth/token` with the session cookie → `{token}` (ES256/JWKS, ~15-min TTL).
311
303
  function getSignOutEndpoint(env = process.env) {
312
304
  return deriveFromHub(env, 'auth/sign-out');
313
305
  }
@@ -421,6 +413,19 @@ function getUnsavePositionEndpoint(positionId, env = process.env) {
421
413
  return deriveFromHub(env, `works/talent/saved-positions/${encodeURIComponent(positionId)}`);
422
414
  }
423
415
 
416
+ function getPositionApplicationEndpoint(positionId, env = process.env) {
417
+ const explicit = readEnv(env, 'POSITION_APPLICATION_ENDPOINT');
418
+ if (explicit && explicit.trim()) return explicit.trim();
419
+ if (positionId == null || typeof positionId === 'object') return null;
420
+ return deriveFromHub(env, `works/talent/positions/${encodeURIComponent(positionId)}/application`);
421
+ }
422
+
423
+ function getCandidaturesEndpoint(env = process.env) {
424
+ const explicit = readEnv(env, 'CANDIDATURES_ENDPOINT');
425
+ if (explicit && explicit.trim()) return explicit.trim();
426
+ return deriveFromHub(env, 'works/talent/candidatures');
427
+ }
428
+
424
429
  function getReceivedInvitationsEndpoint(env = process.env) {
425
430
  const explicit = readEnv(env, 'RECEIVED_INVITATIONS_ENDPOINT');
426
431
  if (explicit && explicit.trim()) return explicit.trim();
@@ -589,12 +594,6 @@ function deriveFromHub(env, relativePath) {
589
594
  return `${base}/${relativePath}`;
590
595
  }
591
596
 
592
- function getSignUpEndpoint(env = process.env) {
593
- const explicit = readEnv(env, 'SIGNUP_ENDPOINT');
594
- if (explicit && explicit.trim()) return explicit.trim();
595
- return deriveFromHub(env, 'auth/sign-up/email');
596
- }
597
-
598
597
  function getImportProfileEndpoint(env = process.env) {
599
598
  const explicit = readEnv(env, 'IMPORT_PROFILE_ENDPOINT');
600
599
  if (explicit && explicit.trim()) return explicit.trim();
@@ -832,7 +831,6 @@ module.exports = {
832
831
  getSynthesisEndpoint,
833
832
  getRoadmapEndpoint,
834
833
  getAgentEvaluationEndpoint,
835
- getLoginEndpoint,
836
834
  getAuthTokenEndpoint,
837
835
  getSignOutEndpoint,
838
836
  getCompleteRegistrationEndpoint,
@@ -852,6 +850,8 @@ module.exports = {
852
850
  // Marketplace (show-project / save-project / unsave-project / invitations).
853
851
  getPositionDetailEndpoint,
854
852
  getSavePositionEndpoint,
853
+ getPositionApplicationEndpoint,
854
+ getCandidaturesEndpoint,
855
855
  getUnsavePositionEndpoint,
856
856
  getReceivedInvitationsEndpoint,
857
857
  getMyCandidaturesEndpoint,
@@ -884,7 +884,6 @@ module.exports = {
884
884
  getCertsBase,
885
885
  getHubBase,
886
886
  sanitizeStaleBakedEndpoints,
887
- getSignUpEndpoint,
888
887
  getImportProfileEndpoint,
889
888
  getMcpSignupEndpoint,
890
889
  getMyImportStatusEndpoint,
@@ -15,9 +15,12 @@ const MAX_ATTEMPTS = 5;
15
15
  const YES_RE = /^(y|yes|s|si|sí)$/i;
16
16
  const NO_RE = /^(n|no)$/i;
17
17
 
18
- async function askYesNo(ask, question, catalog, { defaultNo = false, defaultYes = false } = {}) {
18
+ const UNANSWERED = Symbol('unanswered');
19
+
20
+ async function askYesNo(ask, question, catalog, { defaultNo = false, defaultYes = false, blankUnanswered = false } = {}) {
19
21
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
20
22
  const raw = String(await ask(question)).trim();
23
+ if (blankUnanswered && raw === '') return UNANSWERED;
21
24
  if (defaultNo && raw === '') return false;
22
25
  if (defaultYes && raw === '') return true;
23
26
  if (YES_RE.test(raw)) return true;
@@ -56,6 +59,7 @@ async function runConsentPrompt({
56
59
  sessionEmail = null,
57
60
  drainBeforeAsk = false,
58
61
  profile = 'talent',
62
+ requireExplicitAnswer = false,
59
63
  }) {
60
64
  const c = catalog.consent;
61
65
  // Presentation only: framed by src/legal-notice.js on a
@@ -69,7 +73,8 @@ async function runConsentPrompt({
69
73
  }
70
74
 
71
75
  // defaultNo: a blank Enter means "do not save" (legal requirement).
72
- const accepted = await askYesNo(ask, c.persistQuestion, catalog, { defaultNo: true });
76
+ const accepted = await askYesNo(ask, c.persistQuestion, catalog, { defaultNo: !requireExplicitAnswer, blankUnanswered: requireExplicitAnswer });
77
+ if (accepted === UNANSWERED) return 'unanswered';
73
78
  if (accepted === null) {
74
79
  process.stdout.write(` ${c.notObtained}\n\n`);
75
80
  return null;
@@ -2,12 +2,7 @@
2
2
 
3
3
  const { execFileSync } = require('child_process');
4
4
  const path = require('path');
5
- const {
6
- toolSources,
7
- listSessionFiles,
8
- readSessionObjects,
9
- resolveFileCwd,
10
- } = require('./session-scan');
5
+ const { toolSources, sourceSessions, resolveFileCwd } = require('./session-scan');
11
6
  const { normalizeRemote } = require('./authorship');
12
7
 
13
8
  const GIT_TIMEOUT_MS = 15000;
@@ -37,8 +32,9 @@ function sessionCwdCounts(env = process.env) {
37
32
  const counts = new Map();
38
33
  let unassigned = 0;
39
34
  for (const source of toolSources(env)) {
40
- for (const file of listSessionFiles(source.dir)) {
41
- const cwd = resolveFileCwd(readSessionObjects(file));
35
+ if (source.repoLevel) continue;
36
+ for (const { objects } of sourceSessions(source)) {
37
+ const cwd = resolveFileCwd(objects);
42
38
  if (cwd === null) {
43
39
  unassigned += 1;
44
40
  continue;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { normalizeAgentName } = require('./tier-engine');
4
+ const { scrubSecrets } = require('./agent-synthesis');
5
+ const { requestDeclareAgent } = require('./agents-client');
6
+
7
+ function detectedAgentsFromReport(report) {
8
+ const synthesis = report && report.agentSynthesis && Array.isArray(report.agentSynthesis.agents) ? report.agentSynthesis.agents : [];
9
+ const whatItDoesByName = new Map(synthesis.filter((a) => a && a.name).map((a) => [a.name, a.whatItDoes]));
10
+ const agents = report && Array.isArray(report.agents) ? report.agents : [];
11
+ return agents
12
+ .filter((a) => a && typeof a.name === 'string' && a.name)
13
+ .map((a) => ({ name: a.name, whatItDoes: whatItDoesByName.get(a.name) || null }));
14
+ }
15
+
16
+ function newDetectedAgents(candidates, profileAgents) {
17
+ const taken = new Set((Array.isArray(profileAgents) ? profileAgents : []).map((a) => normalizeAgentName(a && a.name)));
18
+ const fresh = [];
19
+ for (const agent of Array.isArray(candidates) ? candidates : []) {
20
+ const key = normalizeAgentName(agent && agent.name);
21
+ if (!key || taken.has(key)) continue;
22
+ taken.add(key);
23
+ fresh.push({ name: agent.name.trim(), whatItDoes: typeof agent.whatItDoes === 'string' && agent.whatItDoes ? agent.whatItDoes : null });
24
+ }
25
+ return fresh;
26
+ }
27
+
28
+ async function declareDetectedAgents(agents, { accessToken, hubAccessToken }, { endpoint, declare = requestDeclareAgent } = {}) {
29
+ const added = [];
30
+ const failed = [];
31
+ for (const agent of agents) {
32
+ const whatItDoes = agent.whatItDoes ? scrubSecrets(agent.whatItDoes) : undefined;
33
+ const result = await declare({ name: agent.name, whatItDoes, accessToken, hubAccessToken }, { endpoint });
34
+ if (result && result.ok) added.push(agent);
35
+ else failed.push({ agent, reason: (result && result.reason) || 'declare-failed' });
36
+ }
37
+ return { added, failed };
38
+ }
39
+
40
+ module.exports = { detectedAgentsFromReport, newDetectedAgents, declareDetectedAgents };
package/src/detectors.js CHANGED
@@ -115,6 +115,29 @@ const detectors = [
115
115
  { type: 'bin', name: 'codex' },
116
116
  ],
117
117
  },
118
+ {
119
+ id: 'pi',
120
+ name: 'Pi',
121
+ vendor: 'Earendil Works',
122
+ category: CATEGORIES.AGENTIC_CLI,
123
+ signals: [
124
+ { type: 'projectPath', path: '.pi' },
125
+ { type: 'homePath', path: '.pi/agent' },
126
+ ],
127
+ },
128
+ {
129
+ id: 'opencode',
130
+ name: 'OpenCode',
131
+ vendor: 'OpenCode',
132
+ category: CATEGORIES.AGENTIC_CLI,
133
+ signals: [
134
+ { type: 'projectPath', path: 'opencode.json' },
135
+ { type: 'projectPath', path: '.opencode' },
136
+ { type: 'homePath', path: '.config/opencode' },
137
+ { type: 'homePath', path: '.local/share/opencode' },
138
+ { type: 'bin', name: 'opencode' },
139
+ ],
140
+ },
118
141
  {
119
142
  id: 'cody',
120
143
  name: 'Cody',
@@ -26,7 +26,7 @@ function savePendingDevice(pending, env = process.env, { fs: fsImpl = fs } = {})
26
26
  interval: typeof pending.interval === 'number' && pending.interval > 0 ? Math.floor(pending.interval) : 5,
27
27
  expiresAt: typeof pending.expiresAt === 'string' && pending.expiresAt ? pending.expiresAt : null,
28
28
  mode: pending.mode === 'register' ? 'register' : 'login',
29
- provider: pending.provider === 'linkedin' ? 'linkedin' : 'google',
29
+ provider: pending.provider === 'linkedin' || pending.provider === 'email' ? pending.provider : 'google',
30
30
  registerContext: pending.registerContext && typeof pending.registerContext === 'object' ? pending.registerContext : null,
31
31
  email: typeof pending.email === 'string' && pending.email ? pending.email : null,
32
32
  version: DEVICE_FILE_VERSION,
@@ -75,7 +75,7 @@ function loadPendingDevice(env = process.env, { fs: fsImpl = fs } = {}) {
75
75
  interval: typeof parsed.interval === 'number' && parsed.interval > 0 ? Math.floor(parsed.interval) : 5,
76
76
  expiresAt: typeof parsed.expiresAt === 'string' && parsed.expiresAt ? parsed.expiresAt : null,
77
77
  mode: parsed.mode === 'register' ? 'register' : 'login',
78
- provider: parsed.provider === 'linkedin' ? 'linkedin' : 'google',
78
+ provider: parsed.provider === 'linkedin' || parsed.provider === 'email' ? parsed.provider : 'google',
79
79
  registerContext: parsed.registerContext && typeof parsed.registerContext === 'object' ? parsed.registerContext : null,
80
80
  email: typeof parsed.email === 'string' && parsed.email ? parsed.email : null,
81
81
  };
@@ -10,15 +10,16 @@ function defaultSleep(ms) {
10
10
  return new Promise((resolve) => { setTimeout(resolve, ms); });
11
11
  }
12
12
 
13
- // The /cli-login page reads `provider` from the query to pick the social button.
14
- function appendProviderParam(url, provider) {
15
- if (!url || !provider) return url;
13
+ function withDeviceParams(url, params = {}) {
14
+ const entries = Object.entries(params).filter(([, v]) => typeof v === 'string' && v);
15
+ if (!url || !entries.length) return url;
16
16
  try {
17
17
  const u = new URL(url);
18
- u.searchParams.set('provider', provider);
18
+ for (const [k, v] of entries) u.searchParams.set(k, v);
19
19
  return u.href;
20
20
  } catch {
21
- return `${url}${url.includes('?') ? '&' : '?'}provider=${encodeURIComponent(provider)}`;
21
+ const query = entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
22
+ return `${url}${url.includes('?') ? '&' : '?'}${query}`;
22
23
  }
23
24
  }
24
25
 
@@ -32,7 +33,7 @@ async function pollDeviceToken({ deviceCode, tokenEndpoint, clientId, timeoutMs
32
33
 
33
34
  // ORDER: a NEW user needs complete-registration BEFORE GET /auth/token (else the Hub 500s: no domain user).
34
35
  async function finalizeDeviceSession(
35
- { sessionToken, isNewUser, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, claimCode = null, timeoutMs } = {},
36
+ { sessionToken, isNewUser, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, consent = null, claimCode = null, timeoutMs } = {},
36
37
  {
37
38
  requestCompleteRegistration = require('./signup-client').requestCompleteRegistration,
38
39
  requestApiToken: apiToken = requestApiToken,
@@ -42,7 +43,7 @@ async function finalizeDeviceSession(
42
43
 
43
44
  let claimed = false;
44
45
  if (isNewUser && completeRegistrationEndpoint) {
45
- const done = await requestCompleteRegistration(registrationContext || {}, { endpoint: completeRegistrationEndpoint, bearerToken: sessionToken, claimCode, timeoutMs });
46
+ const done = await requestCompleteRegistration(registrationContext || {}, { endpoint: completeRegistrationEndpoint, bearerToken: sessionToken, claimCode, consent, timeoutMs });
46
47
  if (!done.ok) return { ok: false, reason: done.reason || 'complete-registration-failed' };
47
48
  claimed = done.claimed === true;
48
49
  }
@@ -53,7 +54,7 @@ async function finalizeDeviceSession(
53
54
  }
54
55
 
55
56
  async function runDeviceLogin(
56
- { authorizeEndpoint, tokenEndpoint, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, clientId, scope, timeoutMs } = {},
57
+ { authorizeEndpoint, tokenEndpoint, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, consent = null, clientId, scope, timeoutMs } = {},
57
58
  {
58
59
  onPrompt = null,
59
60
  onWaiting = null,
@@ -92,6 +93,7 @@ async function runDeviceLogin(
92
93
  authTokenEndpoint,
93
94
  completeRegistrationEndpoint,
94
95
  registrationContext,
96
+ consent,
95
97
  timeoutMs,
96
98
  });
97
99
  if (!fin.ok) return { ok: false, reason: fin.reason };
@@ -124,7 +126,7 @@ module.exports = {
124
126
  pollDeviceToken,
125
127
  finalizeDeviceSession,
126
128
  runDeviceLogin,
127
- appendProviderParam,
129
+ withDeviceParams,
128
130
  defaultSleep,
129
131
  SLOW_DOWN_STEP_SECONDS,
130
132
  DEFAULT_MAX_WAIT_MS,
package/src/env-paths.js CHANGED
@@ -13,6 +13,14 @@ function getHomeDir(env = process.env) {
13
13
  return readEnv(env, 'HOME_DIR') || os.homedir();
14
14
  }
15
15
 
16
+ function codexHome(env = process.env) {
17
+ return env.CODEX_HOME || require('path').join(getHomeDir(env), '.codex');
18
+ }
19
+
20
+ function piAgentDir(env = process.env) {
21
+ return env.PI_CODING_AGENT_DIR ? expandHome(env.PI_CODING_AGENT_DIR, env) : require('path').join(getHomeDir(env), '.pi', 'agent');
22
+ }
23
+
16
24
  // `~/Documents/cv.pdf` -> absolute, so a model can pass the short form it was shown.
17
25
  function expandHome(file, env = process.env) {
18
26
  if (typeof file !== 'string') return file;
@@ -27,4 +35,4 @@ function tildify(file, env = process.env) {
27
35
  return typeof file === 'string' && file.startsWith(`${home}/`) ? `~/${file.slice(home.length + 1)}` : file;
28
36
  }
29
37
 
30
- module.exports = { getHomeDir, readEnv, expandHome, tildify };
38
+ module.exports = { getHomeDir, codexHome, piAgentDir, readEnv, expandHome, tildify };