envseed 0.1.0 → 0.1.1

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 CHANGED
@@ -5,15 +5,14 @@ A safety research tool that monitors Claude Code sessions for **eval opportuniti
5
5
  ## Quick install (external users)
6
6
 
7
7
  ```bash
8
- npx envseed # installs hooks and shows status
9
- envseed register # authenticates via GitHub
8
+ npm i -g envseed # installs hooks, auto-launches login
10
9
  ```
11
10
 
12
- Or install globally:
11
+ Or one-shot (no global install):
13
12
 
14
13
  ```bash
15
- npm i -g envseed
16
- envseed register
14
+ npx envseed # installs hooks and shows status
15
+ envseed login # sign in with GitHub (opens browser)
17
16
  ```
18
17
 
19
18
  Restart Claude Code after installing. That's it — incidents upload automatically via HTTPS.
@@ -165,7 +164,7 @@ Each simulation:
165
164
  ## CLI
166
165
 
167
166
  ```bash
168
- envseed register # Authenticate via GitHub, get API key
167
+ envseed login # Authenticate via GitHub, get API key
169
168
  envseed status # Check installation health
170
169
  envseed on|off # Enable/disable monitoring
171
170
  envseed alerts [--last N] # Show critical events
@@ -188,7 +187,7 @@ envseed dashboard [--port 3456] # Open web dashboard
188
187
  Incident data can be uploaded two ways:
189
188
 
190
189
  1. **Direct S3** (METR internal) — uses `aws s3 sync` with the staging profile. Requires AWS SSO credentials.
191
- 2. **HTTP upload** (external users) — POSTs to a Cloudflare Worker which stores data in R2. Requires an API key obtained via `envseed register`.
190
+ 2. **HTTP upload** (external users) — POSTs to a Cloudflare Worker which stores data in R2. Requires an API key obtained via `envseed login`.
192
191
 
193
192
  The upload path is chosen automatically: if `s3Profile` is set and AWS auth works, direct S3 is used. Otherwise, HTTP upload via the Worker endpoint.
194
193
 
@@ -205,7 +204,7 @@ Worker source: `infra/worker/`. Deploy with `wrangler deploy`.
205
204
 
206
205
  ### Registration flow
207
206
 
208
- `envseed register` uses GitHub Device Flow:
207
+ `envseed login` uses GitHub Device Flow:
209
208
  1. Shows a code and URL
210
209
  2. User authorizes in browser
211
210
  3. Worker verifies the GitHub token and issues an API key
@@ -250,7 +249,7 @@ Worker source: `infra/worker/`. Deploy with `wrangler deploy`.
250
249
  | `s3Profile` | AWS CLI profile for S3 authentication |
251
250
  | `uploadEndpoint` | Cloudflare Worker URL for HTTP uploads |
252
251
  | `githubClientId` | GitHub OAuth App client ID for registration |
253
- | `apiKey` | API key for HTTP uploads (set by `envseed register`) |
252
+ | `apiKey` | API key for HTTP uploads (set by `envseed login`) |
254
253
  | `simulationCount` | Number of persona simulations per incident |
255
254
  | `simulationMaxTurns` | Max Claude turns per simulation |
256
255
  | `simulationConcurrency` | How many simulations to run in parallel |
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import https from 'node:https';
6
+ import { execSync as execSyncImport, spawnSync } from 'node:child_process';
6
7
 
7
8
  const DATA_DIR = path.join(process.env.HOME, '.propensity-monitor', 'data');
8
9
  const INSTALL_DIR = path.join(process.env.HOME, '.propensity-monitor');
@@ -713,7 +714,6 @@ async function startDashboard(args) {
713
714
  console.error('Dashboard not installed. Run install.sh to update.');
714
715
  process.exit(1);
715
716
  }
716
- const { spawnSync } = await import('child_process');
717
717
  spawnSync('node', [dashboardScript, '--port', port], { stdio: 'inherit' });
718
718
  }
719
719
 
@@ -735,123 +735,185 @@ function httpsRequest(options, body) {
735
735
 
736
736
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
737
737
 
738
- async function registerCommand() {
738
+ function openBrowser(url) {
739
+ try {
740
+ if (process.platform === 'darwin') execSyncImport(`open "${url}"`, { stdio: 'ignore' });
741
+ else if (process.platform === 'linux') execSyncImport(`xdg-open "${url}"`, { stdio: 'ignore' });
742
+ else if (process.platform === 'win32') execSyncImport(`start "${url}"`, { stdio: 'ignore' });
743
+ else return false;
744
+ return true;
745
+ } catch { return false; }
746
+ }
747
+
748
+ async function loginCommand(args) {
749
+ const opts = parseArgs(args);
739
750
  const config = readJson(path.join(INSTALL_DIR, 'config.json')) || {};
740
751
 
741
- if (config.apiKey) {
742
- console.log(`Already registered with API key: ${config.apiKey.substring(0, 8)}...`);
743
- console.log(`To re-register, remove apiKey from ${INSTALL_DIR}/config.json`);
752
+ // Already logged in
753
+ if (config.apiKey && !opts.force) {
754
+ console.log('');
755
+ console.log(` ${C.green}${C.bold}Already logged in${C.reset}`);
756
+ console.log(` API key: ${config.apiKey.substring(0, 12)}...`);
757
+ console.log('');
758
+ console.log(` To log in with a different account: ${C.dim}envseed login --force${C.reset}`);
759
+ console.log(` To log out: ${C.dim}envseed logout${C.reset}`);
744
760
  return;
745
761
  }
746
762
 
747
763
  const clientId = config.githubClientId || GITHUB_CLIENT_ID;
748
- if (!clientId) {
749
- console.error('No GitHub client ID configured.');
750
- console.error(`Set githubClientId in ${INSTALL_DIR}/config.json`);
751
- process.exit(1);
752
- }
753
-
754
764
  const uploadEndpoint = config.uploadEndpoint;
765
+
755
766
  if (!uploadEndpoint) {
756
- console.error('No upload endpoint configured.');
757
- console.error(`Set uploadEndpoint in ${INSTALL_DIR}/config.json`);
758
- process.exit(1);
767
+ console.log('');
768
+ console.log(` ${C.yellow}No upload endpoint configured.${C.reset}`);
769
+ console.log(' Login is only needed for uploading incidents to the envseed server.');
770
+ console.log(' Local monitoring works without logging in.');
771
+ console.log('');
772
+ console.log(` If you have an endpoint, add it to: ${C.dim}${INSTALL_DIR}/config.json${C.reset}`);
773
+ return;
759
774
  }
760
775
 
776
+ console.log('');
777
+ console.log(` ${C.bold}envseed login${C.reset}`);
778
+ console.log(` ${C.dim}Sign in with GitHub to upload incidents to the envseed server.${C.reset}`);
779
+ console.log(` ${C.dim}This only needs read:user access (your public profile).${C.reset}`);
780
+ console.log('');
781
+
761
782
  // Step 1: Request device code
762
- console.log('Starting GitHub authentication...');
763
- const codeRes = await httpsRequest({
764
- hostname: 'github.com',
765
- path: '/login/device/code',
766
- method: 'POST',
767
- headers: {
768
- 'Content-Type': 'application/json',
769
- Accept: 'application/json',
770
- },
771
- }, JSON.stringify({ client_id: clientId, scope: 'read:user' }));
772
-
773
- const codeData = JSON.parse(codeRes.body);
783
+ let codeData;
784
+ try {
785
+ const codeRes = await httpsRequest({
786
+ hostname: 'github.com',
787
+ path: '/login/device/code',
788
+ method: 'POST',
789
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
790
+ }, JSON.stringify({ client_id: clientId, scope: 'read:user' }));
791
+ codeData = JSON.parse(codeRes.body);
792
+ } catch (e) {
793
+ console.error(` ${C.red}Could not reach GitHub: ${e.message}${C.reset}`);
794
+ process.exit(1);
795
+ }
796
+
774
797
  if (!codeData.device_code) {
775
- console.error('Failed to start device flow:', codeData);
798
+ console.error(` ${C.red}GitHub returned an error: ${JSON.stringify(codeData)}${C.reset}`);
776
799
  process.exit(1);
777
800
  }
778
801
 
802
+ // Step 2: Show code and open browser
803
+ const verifyUrl = `${codeData.verification_uri}?code=${codeData.user_code}`;
804
+
805
+ console.log(' ┌──────────────────────────────────────────────┐');
806
+ console.log(` │ Your code: ${C.bold}${C.green}${codeData.user_code}${C.reset} │`);
807
+ console.log(' └──────────────────────────────────────────────┘');
779
808
  console.log('');
780
- console.log(` Visit: ${C.bold}${codeData.verification_uri}${C.reset}`);
781
- console.log(` Enter code: ${C.bold}${C.green}${codeData.user_code}${C.reset}`);
809
+
810
+ const opened = openBrowser(verifyUrl);
811
+ if (opened) {
812
+ console.log(` ${C.green}Opened GitHub in your browser.${C.reset}`);
813
+ console.log(` Paste the code above if it isn't pre-filled.`);
814
+ } else {
815
+ console.log(` Open this URL in your browser:`);
816
+ console.log(` ${C.bold}${codeData.verification_uri}${C.reset}`);
817
+ console.log(` Then enter the code: ${C.bold}${C.green}${codeData.user_code}${C.reset}`);
818
+ }
819
+
782
820
  console.log('');
783
- console.log('Waiting for authorization...');
821
+ process.stdout.write(` ${C.dim}Waiting for you to authorize...${C.reset}`);
784
822
 
785
- // Step 2: Poll for access token
823
+ // Step 3: Poll for access token
786
824
  const interval = (codeData.interval || 5) * 1000;
787
825
  let githubToken = null;
826
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
827
+ let frame = 0;
788
828
 
789
- for (let i = 0; i < 60; i++) {
829
+ for (let i = 0; i < 120; i++) {
790
830
  await sleep(interval);
831
+ process.stdout.write(`\r ${spinner[frame++ % spinner.length]} ${C.dim}Waiting for you to authorize...${C.reset} `);
791
832
 
792
- const tokenRes = await httpsRequest({
793
- hostname: 'github.com',
794
- path: '/login/oauth/access_token',
795
- method: 'POST',
796
- headers: {
797
- 'Content-Type': 'application/json',
798
- Accept: 'application/json',
799
- },
800
- }, JSON.stringify({
801
- client_id: clientId,
802
- device_code: codeData.device_code,
803
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
804
- }));
805
-
806
- const tokenData = JSON.parse(tokenRes.body);
807
-
808
- if (tokenData.access_token) {
809
- githubToken = tokenData.access_token;
810
- break;
811
- }
812
- if (tokenData.error === 'authorization_pending') continue;
813
- if (tokenData.error === 'slow_down') {
814
- await sleep(5000);
815
- continue;
816
- }
817
- if (tokenData.error === 'expired_token') {
818
- console.error('Authorization timed out. Please try again.');
819
- process.exit(1);
820
- }
821
- if (tokenData.error) {
822
- console.error(`GitHub error: ${tokenData.error_description || tokenData.error}`);
823
- process.exit(1);
824
- }
833
+ try {
834
+ const tokenRes = await httpsRequest({
835
+ hostname: 'github.com',
836
+ path: '/login/oauth/access_token',
837
+ method: 'POST',
838
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
839
+ }, JSON.stringify({
840
+ client_id: clientId,
841
+ device_code: codeData.device_code,
842
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
843
+ }));
844
+
845
+ const tokenData = JSON.parse(tokenRes.body);
846
+
847
+ if (tokenData.access_token) {
848
+ githubToken = tokenData.access_token;
849
+ break;
850
+ }
851
+ if (tokenData.error === 'authorization_pending') continue;
852
+ if (tokenData.error === 'slow_down') { await sleep(5000); continue; }
853
+ if (tokenData.error === 'expired_token') {
854
+ process.stdout.write('\r');
855
+ console.log(` ${C.red}Code expired. Run ${C.bold}envseed login${C.reset}${C.red} to try again.${C.reset}`);
856
+ process.exit(1);
857
+ }
858
+ if (tokenData.error) {
859
+ process.stdout.write('\r');
860
+ console.error(` ${C.red}GitHub error: ${tokenData.error_description || tokenData.error}${C.reset}`);
861
+ process.exit(1);
862
+ }
863
+ } catch { /* network blip, keep trying */ }
825
864
  }
826
865
 
827
866
  if (!githubToken) {
828
- console.error('Timed out waiting for authorization.');
867
+ process.stdout.write('\r');
868
+ console.log(` ${C.red}Timed out. Run ${C.bold}envseed login${C.reset}${C.red} to try again.${C.reset}`);
829
869
  process.exit(1);
830
870
  }
831
871
 
832
- // Step 3: Exchange GitHub token for envseed API key
833
- console.log('Registering with envseed...');
834
- const regRes = await httpsRequest({
835
- hostname: new URL(uploadEndpoint).hostname,
836
- path: '/register',
837
- method: 'POST',
838
- headers: {
839
- 'Content-Type': 'application/json',
840
- },
841
- }, JSON.stringify({ githubToken }));
872
+ // Step 4: Exchange for envseed API key
873
+ process.stdout.write(`\r ${C.dim}Exchanging token...${C.reset} `);
842
874
 
843
- if (regRes.statusCode !== 200) {
844
- console.error(`Registration failed: ${regRes.body}`);
845
- process.exit(1);
846
- }
875
+ try {
876
+ const regRes = await httpsRequest({
877
+ hostname: new URL(uploadEndpoint).hostname,
878
+ path: '/register',
879
+ method: 'POST',
880
+ headers: { 'Content-Type': 'application/json' },
881
+ }, JSON.stringify({ githubToken }));
847
882
 
848
- const regData = JSON.parse(regRes.body);
849
- config.apiKey = regData.apiKey;
850
- fs.writeFileSync(path.join(INSTALL_DIR, 'config.json'), JSON.stringify(config, null, 2) + '\n');
883
+ if (regRes.statusCode !== 200) {
884
+ process.stdout.write('\r');
885
+ console.error(` ${C.red}Server error (${regRes.statusCode}): ${regRes.body}${C.reset}`);
886
+ process.exit(1);
887
+ }
851
888
 
852
- console.log('');
853
- console.log(`${C.green}${C.bold}Registered as @${regData.githubUser}. Your garden is ready.${C.reset}`);
854
- console.log(`API key saved to ${INSTALL_DIR}/config.json`);
889
+ const regData = JSON.parse(regRes.body);
890
+ config.apiKey = regData.apiKey;
891
+ fs.writeFileSync(path.join(INSTALL_DIR, 'config.json'), JSON.stringify(config, null, 2) + '\n');
892
+
893
+ process.stdout.write('\r');
894
+ console.log(` ${C.green}${C.bold}Logged in as @${regData.githubUser}${C.reset} `);
895
+ console.log('');
896
+ console.log(` ${C.dim}API key saved to ${INSTALL_DIR}/config.json${C.reset}`);
897
+ console.log(` ${C.dim}Incidents will now upload automatically.${C.reset}`);
898
+ console.log('');
899
+ console.log(` ${C.bold}Next:${C.reset} Restart Claude Code to activate monitoring.`);
900
+ } catch (e) {
901
+ process.stdout.write('\r');
902
+ console.error(` ${C.red}Could not reach envseed server: ${e.message}${C.reset}`);
903
+ console.log(` ${C.dim}Local monitoring still works — you can login later.${C.reset}`);
904
+ }
905
+ }
906
+
907
+ function logoutCommand() {
908
+ const configPath = path.join(INSTALL_DIR, 'config.json');
909
+ const config = readJson(configPath) || {};
910
+ if (!config.apiKey) {
911
+ console.log('Not logged in.');
912
+ return;
913
+ }
914
+ delete config.apiKey;
915
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
916
+ console.log('Logged out. Incidents will no longer upload.');
855
917
  }
856
918
 
857
919
  function showHelp() {
@@ -861,7 +923,8 @@ ${C.bold}Usage:${C.reset}
861
923
  envseed <command> [options]
862
924
 
863
925
  ${C.bold}Setup:${C.reset}
864
- register Authenticate via GitHub and get API key
926
+ login Sign in with GitHub
927
+ logout Remove saved credentials
865
928
  status Check installation health
866
929
 
867
930
  ${C.bold}Commands:${C.reset}
@@ -884,7 +947,7 @@ ${C.bold}Commands:${C.reset}
884
947
 
885
948
  // ── Main ────────────────────────────────────────────────────────────────────
886
949
 
887
- const COMMANDS = { on: turnOn, off: turnOff, dashboard: startDashboard, alerts: showAlerts, events: showEvents, sessions: showSessions, session: showSession, tail: tailEvents, stats: showStats, search: searchEvents, export: exportData, incidents: showIncidents, incident: showIncident, status: showStatus, register: registerCommand, help: showHelp };
950
+ const COMMANDS = { on: turnOn, off: turnOff, dashboard: startDashboard, alerts: showAlerts, events: showEvents, sessions: showSessions, session: showSession, tail: tailEvents, stats: showStats, search: searchEvents, export: exportData, incidents: showIncidents, incident: showIncident, status: showStatus, login: loginCommand, logout: logoutCommand, register: loginCommand, help: showHelp };
888
951
 
889
952
  const [command, ...args] = process.argv.slice(2);
890
953
  // Default: show status if installed, help if not
package/lib/s3.mjs CHANGED
@@ -36,22 +36,23 @@ function hasAwsAuth(config) {
36
36
  }
37
37
 
38
38
  /**
39
- * POST a file to the envseed upload endpoint.
39
+ * Make an HTTP request. Returns { statusCode, body }.
40
40
  */
41
- function httpPost(endpoint, pathSuffix, body, headers = {}) {
41
+ function httpRequest(urlStr, options = {}) {
42
42
  return new Promise((resolve, reject) => {
43
- const url = new URL(pathSuffix, endpoint);
44
- const options = {
45
- method: 'POST',
43
+ const url = new URL(urlStr);
44
+ const reqOptions = {
45
+ method: options.method || 'GET',
46
46
  hostname: url.hostname,
47
- path: url.pathname,
48
- headers: {
49
- ...headers,
50
- 'Content-Length': Buffer.byteLength(body),
51
- },
47
+ path: url.pathname + url.search,
48
+ headers: options.headers || {},
52
49
  };
53
50
 
54
- const req = https.request(options, (res) => {
51
+ if (options.body) {
52
+ reqOptions.headers['Content-Length'] = Buffer.byteLength(options.body);
53
+ }
54
+
55
+ const req = https.request(reqOptions, (res) => {
55
56
  let data = '';
56
57
  res.on('data', (chunk) => { data += chunk; });
57
58
  res.on('end', () => {
@@ -63,13 +64,40 @@ function httpPost(endpoint, pathSuffix, body, headers = {}) {
63
64
  });
64
65
  });
65
66
  req.on('error', reject);
67
+ if (options.body) req.write(options.body);
68
+ req.end();
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Upload a buffer directly to a presigned S3 URL via PUT.
74
+ */
75
+ function httpPutToPresigned(presignedUrl, body, contentType) {
76
+ return new Promise((resolve, reject) => {
77
+ const url = new URL(presignedUrl);
78
+ const req = https.request({
79
+ method: 'PUT',
80
+ hostname: url.hostname,
81
+ path: url.pathname + url.search,
82
+ headers: {
83
+ 'Content-Type': contentType,
84
+ 'Content-Length': Buffer.byteLength(body),
85
+ },
86
+ }, (res) => {
87
+ let data = '';
88
+ res.on('data', (chunk) => { data += chunk; });
89
+ res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
90
+ });
91
+ req.on('error', reject);
66
92
  req.write(body);
67
93
  req.end();
68
94
  });
69
95
  }
70
96
 
71
97
  /**
72
- * Upload an incident directory via HTTP (tar.gz → POST to /harvest/{incidentId}).
98
+ * Upload an incident directory via presigned URL.
99
+ * 1. GET /upload-url/{incidentId} → presigned PUT URL
100
+ * 2. PUT tar.gz directly to S3
73
101
  */
74
102
  async function httpUpload(localDir, incidentId, config) {
75
103
  if (!config.uploadEndpoint) {
@@ -83,22 +111,25 @@ async function httpUpload(localDir, incidentId, config) {
83
111
  const tarPath = path.join(INSTALL_DIR, 'data', `upload-${incidentId}.tar.gz`);
84
112
  try {
85
113
  await run('tar', ['czf', tarPath, '-C', path.dirname(localDir), path.basename(localDir)]);
86
-
87
114
  const body = fs.readFileSync(tarPath);
88
- const res = await httpPost(
89
- config.uploadEndpoint,
90
- `/harvest/${incidentId}`,
91
- body,
92
- {
93
- 'Content-Type': 'application/gzip',
94
- 'x-api-key': config.apiKey,
95
- },
115
+
116
+ // Get presigned upload URL
117
+ const urlRes = await httpRequest(
118
+ new URL(`/upload-url/${incidentId}`, config.uploadEndpoint).toString(),
119
+ { headers: { 'x-api-key': config.apiKey } },
96
120
  );
97
121
 
98
- if (res.statusCode === 200) {
99
- return { success: true, s3Path: res.body.s3Path };
122
+ if (urlRes.statusCode !== 200) {
123
+ return { success: false, error: `Failed to get upload URL: HTTP ${urlRes.statusCode}: ${JSON.stringify(urlRes.body)}` };
100
124
  }
101
- return { success: false, error: `HTTP ${res.statusCode}: ${JSON.stringify(res.body)}` };
125
+
126
+ // PUT directly to S3 via presigned URL
127
+ const putRes = await httpPutToPresigned(urlRes.body.uploadUrl, body, 'application/gzip');
128
+
129
+ if (putRes.statusCode >= 200 && putRes.statusCode < 300) {
130
+ return { success: true, s3Path: `s3://${urlRes.body.s3Key}` };
131
+ }
132
+ return { success: false, error: `S3 upload failed: HTTP ${putRes.statusCode}` };
102
133
  } finally {
103
134
  try { fs.unlinkSync(tarPath); } catch {}
104
135
  }
@@ -172,13 +203,15 @@ export async function s3Upload(localPath, s3Key) {
172
203
  const incidentId = extractIncidentId(s3Key);
173
204
  if (incidentId && s3Key.endsWith('status.json')) {
174
205
  const body = fs.readFileSync(localPath, 'utf8');
175
- const res = await httpPost(
176
- config.uploadEndpoint,
177
- `/harvest/${incidentId}/status`,
178
- body,
206
+ const res = await httpRequest(
207
+ new URL(`/harvest/${incidentId}/status`, config.uploadEndpoint).toString(),
179
208
  {
180
- 'Content-Type': 'application/json',
181
- 'x-api-key': config.apiKey,
209
+ method: 'POST',
210
+ body,
211
+ headers: {
212
+ 'Content-Type': 'application/json',
213
+ 'x-api-key': config.apiKey,
214
+ },
182
215
  },
183
216
  );
184
217
  if (res.statusCode === 200) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envseed",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Cultivate AI safety evals from real Claude Code sessions",
5
5
  "type": "module",
6
6
  "bin": {
package/postinstall.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { spawnSync } from 'node:child_process';
11
12
 
12
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
14
  const HOME = process.env.HOME || process.env.USERPROFILE;
@@ -153,11 +154,24 @@ try {
153
154
  console.log('');
154
155
  console.log('envseed planted successfully!');
155
156
  console.log('');
156
- console.log(' Next steps:');
157
- console.log(' 1. Run: envseed register');
158
- console.log(' 2. Restart Claude Code');
159
- console.log('');
160
- console.log(' Run "envseed status" to check health.');
157
+
158
+ // Auto-launch login if not already logged in and running interactively
159
+ if (!config.apiKey && process.stdout.isTTY) {
160
+ console.log(' Launching login...');
161
+ console.log('');
162
+ try {
163
+ const binPath = path.join(INSTALL_DIR, 'bin', 'propensity-monitor.mjs');
164
+ spawnSync('node', [binPath, 'login'], { stdio: 'inherit' });
165
+ } catch {
166
+ console.log(' Run "envseed login" to sign in.');
167
+ }
168
+ } else if (config.apiKey) {
169
+ console.log(` ${'\x1b[32m'}Already logged in.${'\x1b[0m'}`);
170
+ console.log(' Restart Claude Code to activate monitoring.');
171
+ } else {
172
+ console.log(' Next: run "envseed login" to sign in.');
173
+ console.log(' Then restart Claude Code.');
174
+ }
161
175
 
162
176
  } catch (err) {
163
177
  // Don't fail the npm install if postinstall has issues