nothumanallowed 13.5.150 → 13.5.152

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.5.150",
3
+ "version": "13.5.152",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,12 +7,13 @@ import { fail, info } from '../ui.mjs';
7
7
  export async function cmdGoogle(args) {
8
8
  const sub = args[0] || 'auth';
9
9
  const config = loadConfig();
10
+ const manual = args.includes('--manual') || args.includes('--headless') || args.includes('--no-browser');
10
11
 
11
12
  switch (sub) {
12
13
  case 'auth':
13
14
  case 'login':
14
15
  case 'connect':
15
- return runAuthFlow(config);
16
+ return runAuthFlow(config, manual);
16
17
 
17
18
  case 'status':
18
19
  return showStatus();
@@ -236,7 +236,7 @@ export async function cmdUI(args) {
236
236
  port = parseInt(arg.split('=')[1], 10) || DEFAULT_PORT;
237
237
  } else if (arg === '--no-browser') {
238
238
  noBrowser = true;
239
- } else if (arg === '--lan') {
239
+ } else if (arg === '--lan' || arg === '--host' || arg === '--host=0.0.0.0') {
240
240
  lanMode = true;
241
241
  }
242
242
  }
@@ -6551,16 +6551,20 @@ CRITICAL WRITING RULES — ENFORCE STRICTLY:
6551
6551
  console.log('');
6552
6552
  console.log(` ${G}Local:${NC} ${localUrl}`);
6553
6553
  if (lanUrl) {
6554
- console.log(` ${G}Network:${NC} ${lanUrl} ${D}(mobile/tablet)${NC}`);
6554
+ console.log(` ${G}Network:${NC} ${lanUrl} ${D}(VM host, phone, tablet)${NC}`);
6555
+ }
6556
+ const providerLabel = config.llm.provider || 'nha';
6557
+ const providerDisplay = providerLabel === 'nha' ? `${G}nha (Liara — free, no key needed)${NC}` : providerLabel;
6558
+ console.log(` ${D}Provider:${NC} ${providerDisplay}`);
6559
+ if (providerLabel !== 'nha') {
6560
+ console.log(` ${D}API Key:${NC} ${config.llm.apiKey ? config.llm.apiKey.slice(0, 12) + '...' : '\x1b[0;31mnot set — run: nha config set provider nha\x1b[0m'}`);
6555
6561
  }
6556
- console.log(` ${D}Provider:${NC} ${config.llm.provider || 'not set'}`);
6557
- console.log(` ${D}API Key:${NC} ${config.llm.apiKey ? config.llm.apiKey.slice(0, 12) + '...' : '\x1b[0;31mnot set\x1b[0m'}`);
6558
6562
  console.log(` ${D}Agents loaded:${NC} ${agentCards.length}`);
6559
6563
  console.log('');
6560
6564
  if (lanUrl) {
6561
- console.log(` ${D}Open ${lanUrl} on your phone to use NHA from mobile.${NC}`);
6565
+ console.log(` ${D}Open ${lanUrl} on your host machine / phone / tablet.${NC}`);
6562
6566
  } else {
6563
- console.log(` ${D}Tip: use --lan to access from phone/tablet on same WiFi.${NC}`);
6567
+ console.log(` ${D}Tip: use --lan to access from VM host, phone or tablet on same network.${NC}`);
6564
6568
  }
6565
6569
  console.log(` ${D}Press Ctrl+C to stop${NC}`);
6566
6570
  console.log('');
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.5.150';
8
+ export const VERSION = '13.5.152';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Google OAuth 2.0 with PKCE — browser-based consent flow.
3
- * Runs ephemeral local HTTP server for callback.
4
- * Zero dependencies — uses Node.js native http + crypto.
3
+ * Runs ephemeral local HTTP server for callback, or manual code-paste for headless/VM.
4
+ * Zero dependencies — uses Node.js native http + crypto + readline.
5
5
  */
6
6
 
7
7
  import http from 'http';
8
8
  import crypto from 'crypto';
9
+ import readline from 'readline';
9
10
  import { execSync } from 'child_process';
10
11
  import os from 'os';
11
12
  import { saveTokens, loadTokens, deleteTokens } from './token-store.mjs';
@@ -40,19 +41,71 @@ function generatePKCE() {
40
41
 
41
42
  /**
42
43
  * Open URL in user's default browser.
44
+ * Returns true if browser opened successfully, false otherwise.
43
45
  */
44
46
  function openBrowser(url) {
45
47
  const platform = os.platform();
46
48
  try {
47
- if (platform === 'darwin') execSync(`open "${url}"`);
48
- else if (platform === 'win32') execSync(`start "" "${url}"`);
49
- else execSync(`xdg-open "${url}"`);
49
+ if (platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
50
+ else if (platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore' });
51
+ else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
52
+ return true;
50
53
  } catch {
51
- warn('Could not open browser automatically.');
52
- info(`Open this URL manually:\n\n ${url}\n`);
54
+ return false;
53
55
  }
54
56
  }
55
57
 
58
+ /**
59
+ * Manual mode: print the auth URL and ask the user to paste back
60
+ * the full redirect URL (http://127.0.0.1:PORT/callback?code=...&state=...)
61
+ * that appears in their browser's address bar after Google login.
62
+ * Works on any headless/VM/SSH setup — no local server needed.
63
+ */
64
+ function waitForManualCode(authUrl, state) {
65
+ return new Promise((resolve, reject) => {
66
+ console.log('\n\x1b[1;33m MANUAL AUTH MODE\x1b[0m');
67
+ console.log('\x1b[0;90m ─────────────────────────────────────────────────────\x1b[0m');
68
+ console.log(' 1. Open this URL on any device with a browser (phone, PC...):');
69
+ console.log('\n\x1b[0;36m ' + authUrl + '\x1b[0m\n');
70
+ console.log(' 2. Log in with Google and grant permissions.');
71
+ console.log(' 3. The browser will try to open a page that fails to load.');
72
+ console.log(' That is expected. Copy the full URL from the address bar.');
73
+ console.log(' It looks like: \x1b[0;32mhttp://127.0.0.1:19847/callback?code=4/0A...&state=...\x1b[0m');
74
+ console.log('\x1b[0;90m ─────────────────────────────────────────────────────\x1b[0m\n');
75
+
76
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
77
+ rl.question(' Paste the full redirect URL here: ', (answer) => {
78
+ rl.close();
79
+ const trimmed = answer.trim();
80
+ try {
81
+ // Accept both the full URL and just the code= value
82
+ let code, returnedState;
83
+ if (trimmed.startsWith('http')) {
84
+ const parsed = new URL(trimmed);
85
+ code = parsed.searchParams.get('code');
86
+ returnedState = parsed.searchParams.get('state');
87
+ } else {
88
+ // Maybe they pasted just the code directly
89
+ code = trimmed;
90
+ returnedState = state; // trust them
91
+ }
92
+
93
+ if (!code) {
94
+ reject(new Error('No authorization code found in the URL you pasted.'));
95
+ return;
96
+ }
97
+ if (returnedState && returnedState !== state) {
98
+ reject(new Error('State mismatch — the URL does not match this auth session. Try again.'));
99
+ return;
100
+ }
101
+ resolve(code);
102
+ } catch {
103
+ reject(new Error('Could not parse the URL. Make sure you copied the full address bar URL.'));
104
+ }
105
+ });
106
+ });
107
+ }
108
+
56
109
  /**
57
110
  * Start ephemeral HTTP server and wait for OAuth callback.
58
111
  * @returns {Promise<{code: string, port: number}>}
@@ -156,8 +209,9 @@ async function getUserEmail(accessToken) {
156
209
  /**
157
210
  * Run the full OAuth consent flow.
158
211
  * @param {object} config — NHA config
212
+ * @param {boolean} manual — force manual code-paste mode (for VMs/headless)
159
213
  */
160
- export async function runAuthFlow(config) {
214
+ export async function runAuthFlow(config, manual = false) {
161
215
  const clientId = config.google?.clientId || DEFAULT_CLIENT_ID;
162
216
  const clientSecret = config.google?.clientSecret || '';
163
217
 
@@ -174,8 +228,8 @@ export async function runAuthFlow(config) {
174
228
  return false;
175
229
  }
176
230
 
177
- // Find available port
178
- let port = 0;
231
+ // Find available port (used for redirect_uri even in manual mode)
232
+ let port = CALLBACK_PORTS[0];
179
233
  for (const p of CALLBACK_PORTS) {
180
234
  try {
181
235
  const srv = http.createServer();
@@ -187,10 +241,6 @@ export async function runAuthFlow(config) {
187
241
  break;
188
242
  } catch { continue; }
189
243
  }
190
- if (!port) {
191
- fail('No available port for OAuth callback (tried 19847-19851)');
192
- return false;
193
- }
194
244
 
195
245
  const redirectUri = `http://127.0.0.1:${port}/callback`;
196
246
  const { verifier, challenge } = generatePKCE();
@@ -207,14 +257,32 @@ export async function runAuthFlow(config) {
207
257
  authUrl.searchParams.set('access_type', 'offline');
208
258
  authUrl.searchParams.set('prompt', 'consent');
209
259
 
210
- info('Opening browser for Google authorization...');
211
- openBrowser(authUrl.toString());
212
- info('Waiting for authorization (5 min timeout)...\n');
260
+ const authUrlStr = authUrl.toString();
213
261
 
214
262
  try {
215
- const { code } = await waitForCallback(state, port);
216
- info('Authorization code received. Exchanging for tokens...');
263
+ let code;
264
+
265
+ if (manual) {
266
+ // Explicit manual mode
267
+ code = await waitForManualCode(authUrlStr, state);
268
+ } else {
269
+ // Try to open browser — if it fails, auto-switch to manual mode
270
+ info('Opening browser for Google authorization...');
271
+ const browserOpened = openBrowser(authUrlStr);
272
+
273
+ if (!browserOpened) {
274
+ // Headless/VM detected — auto-switch to manual mode
275
+ warn('No browser found. Switching to manual mode...');
276
+ code = await waitForManualCode(authUrlStr, state);
277
+ } else {
278
+ // Browser opened — wait for local callback
279
+ info('Waiting for authorization (5 min timeout)...\n');
280
+ const result = await waitForCallback(state, port);
281
+ code = result.code;
282
+ }
283
+ }
217
284
 
285
+ info('Authorization code received. Exchanging for tokens...');
218
286
  const tokenData = await exchangeCode(code, verifier, clientId, clientSecret, redirectUri);
219
287
  const email = await getUserEmail(tokenData.access_token);
220
288
 
@@ -228,7 +296,7 @@ export async function runAuthFlow(config) {
228
296
 
229
297
  saveTokens(tokens);
230
298
  ok(`Google account connected: ${email || 'unknown'}`);
231
- ok('Gmail + Calendar access granted.');
299
+ ok('Gmail + Calendar + Drive access granted.');
232
300
  info('Run "nha plan" to generate your first daily plan.');
233
301
  return true;
234
302
  } catch (err) {