@empirical-gauzza/cli 1.0.9
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/bin/empirical.js +125 -0
- package/lib/auth.js +804 -0
- package/lib/constants.js +491 -0
- package/lib/deviceConfig.js +94 -0
- package/lib/doc.js +50 -0
- package/lib/doctor.js +116 -0
- package/lib/env.js +49 -0
- package/lib/fsUtils.js +90 -0
- package/lib/help.js +113 -0
- package/lib/http.js +179 -0
- package/lib/installWizard.js +383 -0
- package/lib/installers/claude.js +89 -0
- package/lib/installers/codex.js +78 -0
- package/lib/installers/copilot.js +77 -0
- package/lib/installers/detect.js +47 -0
- package/lib/installers/hooks.js +128 -0
- package/lib/installers/nativePlugin.js +87 -0
- package/lib/installers/promptBlocks.js +403 -0
- package/lib/kitCommands.js +622 -0
- package/lib/memoryCommands.js +528 -0
- package/lib/oauthClient.js +222 -0
- package/lib/pluginCommands.js +81 -0
- package/lib/pluginUpdateCheck.js +143 -0
- package/lib/policyCommands.js +144 -0
- package/lib/scheduledSync.js +331 -0
- package/lib/skillSync.js +508 -0
- package/lib/ui.js +280 -0
- package/lib/workspaceCommands.js +323 -0
- package/lib/workspaceContext.js +76 -0
- package/package.json +25 -0
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log, warn, ANSI, colorize, supportsColor } = require('./ui');
|
|
4
|
+
const { httpJson } = require('./http');
|
|
5
|
+
const { parseOption } = require('./env');
|
|
6
|
+
const { resolveAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
|
|
7
|
+
const { withCliAnalyticsContext } = require('./memoryCommands');
|
|
8
|
+
|
|
9
|
+
async function runDoctor(args) {
|
|
10
|
+
return withCliAnalyticsContext({ command: 'doctor' }, async () => {
|
|
11
|
+
const outputJson = parseOption(args, '--output', '') === 'json';
|
|
12
|
+
const checks = [];
|
|
13
|
+
let overallFail = false;
|
|
14
|
+
let overallWarn = false;
|
|
15
|
+
|
|
16
|
+
function addCheck(name, status, message) {
|
|
17
|
+
checks.push({ name, status, message });
|
|
18
|
+
if (status === 'fail') overallFail = true;
|
|
19
|
+
if (status === 'warn') overallWarn = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const token = resolveAccessToken();
|
|
23
|
+
if (!token) {
|
|
24
|
+
addCheck('token_presence', 'fail', 'No access token found. Run: empirical auth login, or configure headless credentials with empirical oauth bootstrap headless.');
|
|
25
|
+
} else {
|
|
26
|
+
addCheck('token_presence', 'ok', 'Access token found.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let tokenType = null;
|
|
30
|
+
if (token) {
|
|
31
|
+
const parts = token.split('.');
|
|
32
|
+
if (parts.length === 3) {
|
|
33
|
+
tokenType = 'jwt';
|
|
34
|
+
addCheck('token_type', 'ok', 'Token is a JWT (3-part).');
|
|
35
|
+
} else {
|
|
36
|
+
tokenType = 'apikey';
|
|
37
|
+
addCheck('token_type', 'ok', 'Token is an API key (long alphanumeric).');
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
addCheck('token_type', 'fail', 'Cannot determine token type — no token found.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (token && tokenType === 'jwt') {
|
|
44
|
+
const parts = token.split('.');
|
|
45
|
+
try {
|
|
46
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
47
|
+
if (payload && typeof payload.exp === 'number') {
|
|
48
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
49
|
+
if (payload.exp < nowSec) {
|
|
50
|
+
addCheck('token_expiry', 'fail', `Token expired at ${new Date(payload.exp * 1000).toISOString()}.`);
|
|
51
|
+
} else {
|
|
52
|
+
const remaining = payload.exp - nowSec;
|
|
53
|
+
addCheck('token_expiry', 'ok', `Token expires in ${remaining}s (at ${new Date(payload.exp * 1000).toISOString()}).`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
addCheck('token_expiry', 'warn', 'JWT has no `exp` claim — expiry unknown.');
|
|
57
|
+
}
|
|
58
|
+
} catch (_error) {
|
|
59
|
+
addCheck('token_expiry', 'warn', 'Could not decode JWT payload — expiry unknown.');
|
|
60
|
+
}
|
|
61
|
+
} else if (token && tokenType === 'apikey') {
|
|
62
|
+
addCheck('token_expiry', 'ok', 'API key — no expiry.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let userData = null;
|
|
66
|
+
if (token) {
|
|
67
|
+
const baseUrl = resolveApiBaseUrl();
|
|
68
|
+
const meUrl = new URL('/api/v1/users/me', baseUrl).toString();
|
|
69
|
+
try {
|
|
70
|
+
const response = await httpJson(meUrl, 'GET', null, buildApiAuthHeaders(token));
|
|
71
|
+
userData = (response && response.data) ? response.data : response;
|
|
72
|
+
addCheck('api_connectivity', 'ok', `Connected. User: ${userData.email || userData.userId || 'unknown'}.`);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
addCheck('api_connectivity', 'fail', `API request failed: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
addCheck('api_connectivity', 'fail', 'Skipped — no token available.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (userData) {
|
|
81
|
+
const wsId = userData.workspaceId || userData.workspaceName || null;
|
|
82
|
+
if (wsId) {
|
|
83
|
+
addCheck('workspace', 'ok', `Workspace ID: ${wsId}.`);
|
|
84
|
+
} else {
|
|
85
|
+
addCheck('workspace', 'warn', 'No active workspace — using default brain.');
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
addCheck('workspace', 'fail', 'Skipped — API connectivity check failed.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const overall = overallFail ? 'fail' : (overallWarn ? 'warn' : 'ok');
|
|
92
|
+
|
|
93
|
+
if (outputJson) {
|
|
94
|
+
log(JSON.stringify({ checks, overall }));
|
|
95
|
+
} else {
|
|
96
|
+
const statusLabel = (status) => {
|
|
97
|
+
if (status === 'ok') return colorize(ANSI.green, '[OK] ');
|
|
98
|
+
if (status === 'warn') return colorize(ANSI.yellow, '[WARN]');
|
|
99
|
+
return colorize(ANSI.red, '[FAIL]');
|
|
100
|
+
};
|
|
101
|
+
for (const check of checks) {
|
|
102
|
+
log(`${statusLabel(check.status)} ${check.name.padEnd(20)} ${check.message}`);
|
|
103
|
+
}
|
|
104
|
+
log('');
|
|
105
|
+
log(`Overall: ${overall.toUpperCase()}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (overallFail) {
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
runDoctor
|
|
116
|
+
};
|
package/lib/env.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ask } = require('./ui');
|
|
4
|
+
|
|
5
|
+
function getEnv(name, fallback = '') {
|
|
6
|
+
return String(process.env[name] ?? fallback).trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function requireEnv(name) {
|
|
10
|
+
const value = getEnv(name);
|
|
11
|
+
if (!value) {
|
|
12
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function resolveRequiredValueFromEnvOrPrompt(envName, promptLabel) {
|
|
18
|
+
const value = getEnv(envName);
|
|
19
|
+
if (value) return value;
|
|
20
|
+
|
|
21
|
+
if (!process.stdin.isTTY) {
|
|
22
|
+
throw new Error(`Missing required env var: ${envName}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const answer = (await ask(`${promptLabel}: `)).trim();
|
|
26
|
+
if (!answer) {
|
|
27
|
+
throw new Error(`${promptLabel} is required`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return answer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseFlag(args, flagName) {
|
|
34
|
+
return args.includes(flagName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseOption(args, optionName, fallback = '') {
|
|
38
|
+
const index = args.indexOf(optionName);
|
|
39
|
+
if (index === -1) return fallback;
|
|
40
|
+
return String(args[index + 1] || '').trim() || fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
getEnv,
|
|
45
|
+
requireEnv,
|
|
46
|
+
resolveRequiredValueFromEnvOrPrompt,
|
|
47
|
+
parseFlag,
|
|
48
|
+
parseOption
|
|
49
|
+
};
|
package/lib/fsUtils.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function commandExists(cmd) {
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
const escaped = cmd.replace(/'/g, "''");
|
|
10
|
+
const script = `if (Get-Command '${escaped}' -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }`;
|
|
11
|
+
const result = spawnSync('pwsh', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
|
|
12
|
+
return result.status === 0;
|
|
13
|
+
}
|
|
14
|
+
const result = spawnSync('which', [cmd], { stdio: 'ignore' });
|
|
15
|
+
return result.status === 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureDir(dirPath) {
|
|
19
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readUtf8IfExists(filePath) {
|
|
23
|
+
if (!fs.existsSync(filePath)) return '';
|
|
24
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJsonIfExists(filePath) {
|
|
28
|
+
if (!fs.existsSync(filePath)) return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
31
|
+
} catch (_error) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeFile(filePath, content) {
|
|
37
|
+
ensureDir(path.dirname(filePath));
|
|
38
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function quotePowerShellArg(value) {
|
|
42
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runResolvedCommand(command, args, options = {}) {
|
|
46
|
+
// `input`, when given, is piped to the child's stdin (e.g. `crontab -` on Linux, which reads
|
|
47
|
+
// the new crontab body from stdin) — spawnSync ignores `stdio` in that case and manages stdin
|
|
48
|
+
// itself, so we only pass the `stdio` option through when there's no `input` to feed.
|
|
49
|
+
const { input } = options;
|
|
50
|
+
const stdio = options.stdio || 'inherit';
|
|
51
|
+
|
|
52
|
+
if (process.platform !== 'win32') {
|
|
53
|
+
return spawnSync(command, args, {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
...(input !== undefined ? { input } : { stdio })
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const script = `& ${quotePowerShellArg(command)} ${args.map(quotePowerShellArg).join(' ')}`;
|
|
60
|
+
return spawnSync('pwsh', ['-NoProfile', '-Command', script], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
...(input !== undefined ? { input } : { stdio })
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function summarizeProcessOutput(result) {
|
|
67
|
+
if (result.error) {
|
|
68
|
+
return String(result.error.message || result.error);
|
|
69
|
+
}
|
|
70
|
+
const stdout = (result.stdout || '').trim();
|
|
71
|
+
const stderr = (result.stderr || '').trim();
|
|
72
|
+
const lines = [stderr, stdout]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join('\n')
|
|
75
|
+
.split(/\r?\n/)
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.slice(0, 8);
|
|
78
|
+
return lines.join(' | ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
commandExists,
|
|
83
|
+
ensureDir,
|
|
84
|
+
readUtf8IfExists,
|
|
85
|
+
readJsonIfExists,
|
|
86
|
+
writeFile,
|
|
87
|
+
quotePowerShellArg,
|
|
88
|
+
runResolvedCommand,
|
|
89
|
+
summarizeProcessOutput
|
|
90
|
+
};
|
package/lib/help.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { log } = require('./ui');
|
|
4
|
+
const { version } = require('../package.json');
|
|
5
|
+
|
|
6
|
+
function showVersion() {
|
|
7
|
+
log(version);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function showHelp() {
|
|
11
|
+
log('Empirical CLI');
|
|
12
|
+
log(`Version: ${version}`);
|
|
13
|
+
log('');
|
|
14
|
+
log('Usage:');
|
|
15
|
+
log(' empirical install');
|
|
16
|
+
log(' empirical -install');
|
|
17
|
+
log(' empirical --install');
|
|
18
|
+
log(' empirical plugin install <codex|copilot|claude|all> [--all-optional]');
|
|
19
|
+
log(' empirical plugin check [--force]');
|
|
20
|
+
log(' empirical policy show [--refresh]');
|
|
21
|
+
log(' empirical policy set --json <policy>');
|
|
22
|
+
log(' empirical auth login');
|
|
23
|
+
log(' empirical auth logout');
|
|
24
|
+
log(' empirical oauth bootstrap headless [--print-access-token]');
|
|
25
|
+
log(' empirical oauth bootstrap headless --write-env [--env-output <path>]');
|
|
26
|
+
log(' empirical oauth refresh headless [--print-access-token]');
|
|
27
|
+
log(' empirical oauth refresh headless --write-env [--env-output <path>]');
|
|
28
|
+
log(' empirical oauth bootstrap docker [--print-access-token]');
|
|
29
|
+
log(' empirical oauth refresh docker [--print-access-token]');
|
|
30
|
+
log(' empirical oauth client set [--client-id <id>] [--client-secret <secret>]');
|
|
31
|
+
log(' empirical oauth client show');
|
|
32
|
+
log(' empirical oauth client clear');
|
|
33
|
+
log(' empirical workspace list');
|
|
34
|
+
log(' empirical workspace current');
|
|
35
|
+
log(' empirical workspace select [<number|name>]');
|
|
36
|
+
log(' empirical workspace use <number|name|id>');
|
|
37
|
+
log(' empirical workspace create <name> [--description <text>]');
|
|
38
|
+
log(' empirical workspace invite create [--role reader|writer|manager] [--expires-in-days <n>] [--max-uses <n>]');
|
|
39
|
+
log(' empirical workspace invite list');
|
|
40
|
+
log(' empirical workspace invite revoke <token>');
|
|
41
|
+
log(' empirical workspace delete <number|name|id> [--confirm]');
|
|
42
|
+
log(' empirical oauth bootstrap openclaw [--print-access-token] (legacy alias)');
|
|
43
|
+
log(' empirical oauth refresh openclaw [--print-access-token] (legacy alias)');
|
|
44
|
+
log(' empirical --oauth-bootstrap-headless [--print-access-token]');
|
|
45
|
+
log(' empirical --oauth-refresh-headless [--print-access-token]');
|
|
46
|
+
log(' empirical --oauth-bootstrap-openclaw [--print-access-token] (legacy alias)');
|
|
47
|
+
log(' empirical --oauth-refresh-openclaw [--print-access-token] (legacy alias)');
|
|
48
|
+
log('');
|
|
49
|
+
log('Options:');
|
|
50
|
+
log(' install, -install, --install Interactive installer for Codex/Copilot/Claude CLI integration');
|
|
51
|
+
log(' plugin install <target> Install core plugin; add --all-optional or capability flags');
|
|
52
|
+
log(' plugin check [--force] Check for native plugin updates');
|
|
53
|
+
log(' policy show/set Inspect or update native Empirical behavior policy');
|
|
54
|
+
log(' Step 1 checks/provisions OAuth client credentials locally');
|
|
55
|
+
log(' --oauth-bootstrap-headless, --oauth-bootstrap-openclaw');
|
|
56
|
+
log(' Headless OAuth bootstrap using client_credentials (client ID + client secret)');
|
|
57
|
+
log(' --oauth-refresh-headless, --oauth-refresh-openclaw');
|
|
58
|
+
log(' Refresh headless token from stored refresh token file (if available)');
|
|
59
|
+
log(' --client-id <id> Client ID override for headless bootstrap');
|
|
60
|
+
log(' --client-secret <sec> Client secret override for headless bootstrap');
|
|
61
|
+
log(' --print-access-token Print only access token (for scripting)');
|
|
62
|
+
log(' --write-env Write a dotenv file for container startup');
|
|
63
|
+
log(' --env-output <path> Env output path (default: .empirical/headless.env)');
|
|
64
|
+
log(' oauth client set Persist OAuth client credentials at ~/.empirical/oauth-client.json');
|
|
65
|
+
log(' oauth client show Show the currently saved OAuth client id/secret mask');
|
|
66
|
+
log(' oauth client clear Remove saved OAuth client credentials');
|
|
67
|
+
log(' -h, --help Show help');
|
|
68
|
+
log(' -v, --version Show CLI version');
|
|
69
|
+
log('');
|
|
70
|
+
log('Docs & Agent Skills:');
|
|
71
|
+
log(' empirical doc List available documentation topics');
|
|
72
|
+
log(' empirical doc <topic> Print agent-readable instructions for a topic');
|
|
73
|
+
log(' empirical doctor Check token, API connectivity, and workspace status');
|
|
74
|
+
log(' empirical memory query Search memories by natural language');
|
|
75
|
+
log(' empirical memory get Retrieve one memory by exact ID');
|
|
76
|
+
log(' empirical memory list Browse memories with filters and pagination');
|
|
77
|
+
log(' empirical memory record Save a new memory');
|
|
78
|
+
log(' empirical memory note Add a note to an existing memory');
|
|
79
|
+
log(' empirical memory update Update an existing memory');
|
|
80
|
+
log(' empirical memory delete Delete an existing memory');
|
|
81
|
+
log(' empirical memory duplicates List duplicate candidates for a memory');
|
|
82
|
+
log(' empirical memory merge Merge a duplicate memory into a canonical one');
|
|
83
|
+
log(' empirical memory export Export memories as portable JSON [--scope <personal|workspace>] [--out <path>]');
|
|
84
|
+
log(' empirical kit create <name> Create a new Kit [--workspace <workspaceId>]');
|
|
85
|
+
log(' empirical kit add-source <kit> <git-url>');
|
|
86
|
+
log(' Add a git Source to a Kit [--ref <ref>] [--path <subpath>]');
|
|
87
|
+
log(' empirical kit remove-source <kit> <source-id>');
|
|
88
|
+
log(' Remove a Source (and its Skills) from a Kit');
|
|
89
|
+
log(' empirical kit list List Kits owned by the current user');
|
|
90
|
+
log(' empirical kit autosync <kit> on|off');
|
|
91
|
+
log(' Enable/disable auto-sync for a Kit');
|
|
92
|
+
log(' empirical kit devices <kit> List devices registered to a Kit');
|
|
93
|
+
log(' empirical kit deactivate <kit> <deviceId>');
|
|
94
|
+
log(' Deactivate a device on a Kit');
|
|
95
|
+
log(' empirical kit use <kit> Activate a named Kit on this device and sync [--dry-run] [--quiet] [--force]');
|
|
96
|
+
log(' empirical kit sync Sync the active Kit\'s Skills into detected agents [--dry-run] [--quiet] [--force]');
|
|
97
|
+
log(' empirical skill list List Skills across owned Kits');
|
|
98
|
+
log(' empirical skill push <path/to/SKILL.md>');
|
|
99
|
+
log(' Push a local SKILL.md as an authored Skill [--kit <kitId>]');
|
|
100
|
+
log(' Wrote or edited a SKILL.md? Push it so it follows you');
|
|
101
|
+
log(' across machines/projects instead of staying local.');
|
|
102
|
+
log(' empirical skill history <authoredSkillId>');
|
|
103
|
+
log(' Show version history for an authored Skill');
|
|
104
|
+
log(' empirical skill rollback <authoredSkillId> --to-version <n>');
|
|
105
|
+
log(' Roll an authored Skill back to a prior version');
|
|
106
|
+
log(' empirical device list List registered devices and active Kits');
|
|
107
|
+
log(' empirical device show <deviceId> Show effective Skills for a device');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
showHelp,
|
|
112
|
+
showVersion
|
|
113
|
+
};
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { warn } = require('./ui');
|
|
6
|
+
|
|
7
|
+
function httpGet(urlString, timeoutMs = 8000) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const parsed = new URL(urlString);
|
|
10
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
11
|
+
const req = lib.request(
|
|
12
|
+
parsed,
|
|
13
|
+
{
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'User-Agent': 'empirical-installer/1.0'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
(res) => {
|
|
20
|
+
let body = '';
|
|
21
|
+
res.setEncoding('utf8');
|
|
22
|
+
res.on('data', (chunk) => {
|
|
23
|
+
body += chunk;
|
|
24
|
+
});
|
|
25
|
+
res.on('end', () => {
|
|
26
|
+
resolve({
|
|
27
|
+
statusCode: res.statusCode || 0,
|
|
28
|
+
headers: res.headers || {},
|
|
29
|
+
body
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
req.setTimeout(timeoutMs, () => {
|
|
36
|
+
req.destroy(new Error('request timeout'));
|
|
37
|
+
});
|
|
38
|
+
req.on('error', reject);
|
|
39
|
+
req.end();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function httpErrorFromResponse(method, urlString, statusCode, data) {
|
|
44
|
+
const baseMsg = data.error_description || data.message || data.error || `HTTP ${statusCode}`;
|
|
45
|
+
const upgradeUrl = data.upgrade_url || data.details?.upgradeUrl || null;
|
|
46
|
+
const retryAfterSeconds = data.retry_after_seconds ?? null;
|
|
47
|
+
|
|
48
|
+
const parts = [`${method} ${urlString} failed: ${baseMsg}`];
|
|
49
|
+
if (retryAfterSeconds != null) parts.push(`Retry after ${retryAfterSeconds}s.`);
|
|
50
|
+
if (upgradeUrl) parts.push(`Upgrade: ${upgradeUrl}`);
|
|
51
|
+
|
|
52
|
+
const error = new Error(parts.join(' '));
|
|
53
|
+
error.statusCode = statusCode;
|
|
54
|
+
error.code = data.code || null;
|
|
55
|
+
error.upgradeUrl = upgradeUrl;
|
|
56
|
+
error.retryAfterSeconds = retryAfterSeconds;
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Prints a structured JSON error line to stderr so scripts/agents parsing CLI
|
|
61
|
+
// output can branch on `code` (e.g. RATE_LIMITED, PLAN_LIMIT_MEMORY_REACHED)
|
|
62
|
+
// and act on `upgradeUrl`/`retryAfterSeconds` without string-parsing `message`.
|
|
63
|
+
function printApiError(error) {
|
|
64
|
+
warn(JSON.stringify({
|
|
65
|
+
error: true,
|
|
66
|
+
code: error.code || 'api_error',
|
|
67
|
+
message: error.message,
|
|
68
|
+
...(error.retryAfterSeconds != null && { retryAfterSeconds: error.retryAfterSeconds }),
|
|
69
|
+
...(error.upgradeUrl && { upgradeUrl: error.upgradeUrl })
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const parsed = new URL(urlString);
|
|
76
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
77
|
+
const body = payload ? JSON.stringify(payload) : null;
|
|
78
|
+
|
|
79
|
+
const req = lib.request(
|
|
80
|
+
parsed,
|
|
81
|
+
{
|
|
82
|
+
method,
|
|
83
|
+
headers: {
|
|
84
|
+
Accept: 'application/json',
|
|
85
|
+
...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } : {}),
|
|
86
|
+
...headers
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
(res) => {
|
|
90
|
+
let raw = '';
|
|
91
|
+
res.setEncoding('utf8');
|
|
92
|
+
res.on('data', (chunk) => {
|
|
93
|
+
raw += chunk;
|
|
94
|
+
});
|
|
95
|
+
res.on('end', () => {
|
|
96
|
+
let data = {};
|
|
97
|
+
try {
|
|
98
|
+
data = raw ? JSON.parse(raw) : {};
|
|
99
|
+
} catch (_error) {
|
|
100
|
+
data = { raw };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const statusCode = res.statusCode || 0;
|
|
104
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
105
|
+
reject(httpErrorFromResponse(method, urlString, statusCode, data));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve(data);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
req.on('error', reject);
|
|
115
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
116
|
+
req.setTimeout(timeoutMs, () => {
|
|
117
|
+
req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (body) req.write(body);
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function httpForm(urlString, method, params, headers = {}) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const parsed = new URL(urlString);
|
|
128
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
129
|
+
const body = new URLSearchParams(params).toString();
|
|
130
|
+
|
|
131
|
+
const req = lib.request(
|
|
132
|
+
parsed,
|
|
133
|
+
{
|
|
134
|
+
method,
|
|
135
|
+
headers: {
|
|
136
|
+
Accept: 'application/json',
|
|
137
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
138
|
+
'Content-Length': Buffer.byteLength(body),
|
|
139
|
+
...headers
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
(res) => {
|
|
143
|
+
let raw = '';
|
|
144
|
+
res.setEncoding('utf8');
|
|
145
|
+
res.on('data', (chunk) => {
|
|
146
|
+
raw += chunk;
|
|
147
|
+
});
|
|
148
|
+
res.on('end', () => {
|
|
149
|
+
let data = {};
|
|
150
|
+
try {
|
|
151
|
+
data = raw ? JSON.parse(raw) : {};
|
|
152
|
+
} catch (_error) {
|
|
153
|
+
data = { raw };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const statusCode = res.statusCode || 0;
|
|
157
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
158
|
+
reject(httpErrorFromResponse(method, urlString, statusCode, data));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resolve(data);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
req.on('error', reject);
|
|
168
|
+
req.write(body);
|
|
169
|
+
req.end();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
httpGet,
|
|
175
|
+
httpErrorFromResponse,
|
|
176
|
+
printApiError,
|
|
177
|
+
httpJson,
|
|
178
|
+
httpForm
|
|
179
|
+
};
|