@myvillage/cli 1.5.0 → 1.5.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/package.json +8 -7
- package/src/agent-runtime/loop.js +9 -9
- package/src/agent-runtime/mcp-client.js +31 -338
- package/src/commands/agent-local.js +24 -7
- package/src/commands/login.js +159 -72
- package/src/commands/logout.js +43 -6
- package/src/commands/soulprint.js +1379 -0
- package/src/index.js +113 -0
- package/src/utils/agent-scaffolder.js +6 -6
- package/src/utils/api.js +1 -1
- package/src/utils/config.js +1 -0
- package/src/utils/soulprint-api.js +136 -0
- package/src/utils/soulprint-workspace.js +158 -0
package/src/commands/login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
2
3
|
import { randomBytes, createHash } from 'crypto';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import ora from 'ora';
|
|
@@ -21,8 +22,37 @@ function generateCodeChallenge(verifier) {
|
|
|
21
22
|
.digest('base64url');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
// Detect headless environments where no browser is available
|
|
26
|
+
function isHeadless() {
|
|
27
|
+
// No graphical display on Linux
|
|
28
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Running inside SSH session
|
|
32
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Running inside a Docker container or CI
|
|
36
|
+
if (process.env.container || process.env.CI) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Prompt the user to paste a URL from their browser
|
|
43
|
+
function promptForCallbackUrl() {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
46
|
+
rl.question(chalk.cyan('\nPaste the URL here: '), (answer) => {
|
|
47
|
+
rl.close();
|
|
48
|
+
resolve(answer.trim());
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function loginCommand(options) {
|
|
25
54
|
const config = getConfig();
|
|
55
|
+
const headless = options.browser === false || isHeadless();
|
|
26
56
|
|
|
27
57
|
// Check if already logged in
|
|
28
58
|
const existing = loadCredentials();
|
|
@@ -52,96 +82,153 @@ export async function loginCommand() {
|
|
|
52
82
|
|
|
53
83
|
const authUrl = `${config.oauthBaseUrl}/authorize?${params.toString()}`;
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
const authResult = await new Promise((resolve, reject) => {
|
|
57
|
-
let timeout;
|
|
58
|
-
const server = createServer(async (req, res) => {
|
|
59
|
-
const url = new URL(req.url, `http://localhost:${config.callbackPort}`);
|
|
85
|
+
let authResult;
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
if (headless) {
|
|
88
|
+
// ── Headless / manual flow ──────────────────────────
|
|
89
|
+
// No local server — the user authenticates in a browser elsewhere,
|
|
90
|
+
// then pastes the redirect URL (which their browser can't reach) back here.
|
|
91
|
+
spinner.stop();
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(brand.gold('No browser detected — using manual authentication.'));
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(brand.teal('1. Open this URL in any browser:'));
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(` ${chalk.underline(authUrl)}`);
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(brand.teal('2. Log in and authorize the CLI.'));
|
|
100
|
+
console.log(brand.teal('3. Your browser will redirect to a localhost URL that won\'t load — this is expected.'));
|
|
101
|
+
console.log(brand.teal('4. Copy the full URL from your browser\'s address bar and paste it below.'));
|
|
102
|
+
console.log(brand.teal(' It will look like: http://localhost:3737/callback?code=...&state=...'));
|
|
66
103
|
|
|
67
|
-
|
|
68
|
-
const returnedState = url.searchParams.get('state');
|
|
69
|
-
const error = url.searchParams.get('error');
|
|
70
|
-
const errorDescription = url.searchParams.get('error_description');
|
|
104
|
+
const callbackInput = await promptForCallbackUrl();
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
77
|
-
<div style="text-align: center;">
|
|
78
|
-
<h1 style="color: #FF6B6B;">Authentication Failed</h1>
|
|
79
|
-
<p>${errorDescription || error}</p>
|
|
80
|
-
<p>You can close this window.</p>
|
|
81
|
-
</div>
|
|
82
|
-
</body></html>
|
|
83
|
-
`);
|
|
84
|
-
} else {
|
|
85
|
-
res.end(`
|
|
86
|
-
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
87
|
-
<div style="text-align: center;">
|
|
88
|
-
<h1 style="color: #FFD700;">Success!</h1>
|
|
89
|
-
<p>You are now logged in to MyVillageOS CLI.</p>
|
|
90
|
-
<p>You can close this window and return to your terminal.</p>
|
|
91
|
-
</div>
|
|
92
|
-
</body></html>
|
|
93
|
-
`);
|
|
94
|
-
}
|
|
106
|
+
if (!callbackInput) {
|
|
107
|
+
console.log(chalk.red('No URL provided. Authentication cancelled.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
try {
|
|
112
|
+
const callbackUrl = new URL(callbackInput);
|
|
113
|
+
const code = callbackUrl.searchParams.get('code');
|
|
114
|
+
const returnedState = callbackUrl.searchParams.get('state');
|
|
115
|
+
const error = callbackUrl.searchParams.get('error');
|
|
116
|
+
const errorDescription = callbackUrl.searchParams.get('error_description');
|
|
99
117
|
|
|
100
118
|
if (error) {
|
|
101
|
-
|
|
119
|
+
console.log(chalk.red(`Authentication failed: ${errorDescription || error}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!code) {
|
|
124
|
+
console.log(chalk.red('No authorization code found in the URL. Please try again.'));
|
|
102
125
|
return;
|
|
103
126
|
}
|
|
104
127
|
|
|
105
|
-
// Validate state parameter
|
|
106
128
|
if (returnedState !== state) {
|
|
107
|
-
|
|
129
|
+
console.log(chalk.red('State mismatch — possible CSRF attack. Please try again.'));
|
|
108
130
|
return;
|
|
109
131
|
}
|
|
110
132
|
|
|
111
|
-
|
|
112
|
-
}
|
|
133
|
+
authResult = code;
|
|
134
|
+
} catch {
|
|
135
|
+
console.log(chalk.red('Invalid URL. Please copy the full URL from your browser\'s address bar.'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
spinner.start('Exchanging authorization code for tokens...');
|
|
140
|
+
} else {
|
|
141
|
+
// ── Browser flow (original) ─────────────────────────
|
|
142
|
+
authResult = await new Promise((resolve, reject) => {
|
|
143
|
+
let timeout;
|
|
144
|
+
const server = createServer(async (req, res) => {
|
|
145
|
+
const url = new URL(req.url, `http://localhost:${config.callbackPort}`);
|
|
146
|
+
|
|
147
|
+
if (url.pathname !== '/callback') {
|
|
148
|
+
res.writeHead(404);
|
|
149
|
+
res.end('Not found');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
113
152
|
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
const code = url.searchParams.get('code');
|
|
154
|
+
const returnedState = url.searchParams.get('state');
|
|
155
|
+
const error = url.searchParams.get('error');
|
|
156
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
116
157
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
// Send response to the browser
|
|
159
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
160
|
+
if (error) {
|
|
161
|
+
res.end(`
|
|
162
|
+
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
163
|
+
<div style="text-align: center;">
|
|
164
|
+
<h1 style="color: #FF6B6B;">Authentication Failed</h1>
|
|
165
|
+
<p>${errorDescription || error}</p>
|
|
166
|
+
<p>You can close this window.</p>
|
|
167
|
+
</div>
|
|
168
|
+
</body></html>
|
|
169
|
+
`);
|
|
170
|
+
} else {
|
|
171
|
+
res.end(`
|
|
172
|
+
<html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
|
|
173
|
+
<div style="text-align: center;">
|
|
174
|
+
<h1 style="color: #FFD700;">Success!</h1>
|
|
175
|
+
<p>You are now logged in to MyVillageOS CLI.</p>
|
|
176
|
+
<p>You can close this window and return to your terminal.</p>
|
|
177
|
+
</div>
|
|
178
|
+
</body></html>
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Close the server and clear timeout
|
|
183
|
+
server.close();
|
|
184
|
+
clearTimeout(timeout);
|
|
185
|
+
|
|
186
|
+
if (error) {
|
|
187
|
+
reject(new Error(errorDescription || error));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Validate state parameter
|
|
192
|
+
if (returnedState !== state) {
|
|
193
|
+
reject(new Error('State mismatch - possible CSRF attack. Please try again.'));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
resolve(code);
|
|
123
198
|
});
|
|
124
199
|
|
|
125
|
-
|
|
126
|
-
|
|
200
|
+
server.listen(config.callbackPort, () => {
|
|
201
|
+
spinner.text = 'Opening browser for authentication...';
|
|
127
202
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
203
|
+
// Open browser to authorization URL
|
|
204
|
+
open(authUrl).catch(() => {
|
|
205
|
+
spinner.stop();
|
|
206
|
+
console.log(chalk.yellow('\nCould not open browser automatically.'));
|
|
207
|
+
console.log(brand.teal('Please open this URL in your browser:'));
|
|
208
|
+
console.log(brand.gold(authUrl));
|
|
209
|
+
});
|
|
135
210
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
211
|
+
spinner.text = 'Waiting for authentication in browser...';
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
server.on('error', (err) => {
|
|
215
|
+
if (err.code === 'EADDRINUSE') {
|
|
216
|
+
reject(new Error(`Port ${config.callbackPort} is already in use. Close other applications and try again.`));
|
|
217
|
+
} else {
|
|
218
|
+
reject(err);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Timeout after 5 minutes
|
|
223
|
+
timeout = setTimeout(() => {
|
|
224
|
+
server.close();
|
|
225
|
+
reject(new Error('Authentication timed out. Please try again.'));
|
|
226
|
+
}, 5 * 60 * 1000);
|
|
227
|
+
}).catch((err) => {
|
|
228
|
+
spinner.fail(err.message);
|
|
229
|
+
return null;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
145
232
|
|
|
146
233
|
if (!authResult) return;
|
|
147
234
|
|
package/src/commands/logout.js
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { brand } from '../utils/brand.js';
|
|
3
|
-
import { clearCredentials } from '../utils/auth.js';
|
|
2
|
+
import { loadCredentials, clearCredentials } from '../utils/auth.js';
|
|
3
|
+
import { getConfig } from '../utils/config.js';
|
|
4
4
|
|
|
5
5
|
export async function logoutCommand() {
|
|
6
|
-
const
|
|
6
|
+
const creds = loadCredentials();
|
|
7
7
|
|
|
8
|
-
if (
|
|
9
|
-
console.log(brand.green(' \u2713 Successfully logged out.'));
|
|
10
|
-
} else {
|
|
8
|
+
if (!creds) {
|
|
11
9
|
console.log(brand.teal(' No stored credentials found. You are not logged in.'));
|
|
10
|
+
return;
|
|
12
11
|
}
|
|
12
|
+
|
|
13
|
+
const { oauthBaseUrl, clientId } = getConfig();
|
|
14
|
+
|
|
15
|
+
// Revoke tokens server-side (best-effort)
|
|
16
|
+
if (creds.access_token) {
|
|
17
|
+
try {
|
|
18
|
+
await fetch(`${oauthBaseUrl}/revoke`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
21
|
+
body: new URLSearchParams({
|
|
22
|
+
token: creds.access_token,
|
|
23
|
+
token_type_hint: 'access_token',
|
|
24
|
+
client_id: clientId,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// Best-effort — continue with local cleanup
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (creds.refresh_token) {
|
|
33
|
+
try {
|
|
34
|
+
await fetch(`${oauthBaseUrl}/revoke`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
37
|
+
body: new URLSearchParams({
|
|
38
|
+
token: creds.refresh_token,
|
|
39
|
+
token_type_hint: 'refresh_token',
|
|
40
|
+
client_id: clientId,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
// Best-effort — continue with local cleanup
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clearCredentials();
|
|
49
|
+
console.log(brand.green(' \u2713 Successfully logged out.'));
|
|
13
50
|
}
|