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 +8 -9
- package/bin/propensity-monitor.mjs +151 -88
- package/lib/s3.mjs +63 -30
- package/package.json +1 -1
- package/postinstall.mjs +19 -5
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
|
-
|
|
9
|
-
envseed register # authenticates via GitHub
|
|
8
|
+
npm i -g envseed # installs hooks, auto-launches login
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
Or install
|
|
11
|
+
Or one-shot (no global install):
|
|
13
12
|
|
|
14
13
|
```bash
|
|
15
|
-
|
|
16
|
-
envseed
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
console.log(
|
|
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.
|
|
757
|
-
console.
|
|
758
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
'Content-Type': 'application/json',
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
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(
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
821
|
+
process.stdout.write(` ${C.dim}Waiting for you to authorize...${C.reset}`);
|
|
784
822
|
|
|
785
|
-
// Step
|
|
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 <
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
'Content-Type': 'application/json',
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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
|
|
833
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
39
|
+
* Make an HTTP request. Returns { statusCode, body }.
|
|
40
40
|
*/
|
|
41
|
-
function
|
|
41
|
+
function httpRequest(urlStr, options = {}) {
|
|
42
42
|
return new Promise((resolve, reject) => {
|
|
43
|
-
const url = new URL(
|
|
44
|
-
const
|
|
45
|
-
method: '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 (
|
|
99
|
-
return { success:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
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
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|