@papercraneai/cli 1.4.4 → 1.5.0

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/papercrane.js CHANGED
@@ -4,12 +4,61 @@ import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
5
  import readline from 'readline';
6
6
  import fs from 'fs/promises';
7
- import { http } from '../lib/axios-client.js';
7
+ import http from 'http';
8
+ import { http as httpClient } from '../lib/axios-client.js';
8
9
  import { setApiKey, clearConfig, isLoggedIn, setDefaultWorkspace, getDefaultWorkspace } from '../lib/config.js';
9
10
  import { validateApiKey } from '../lib/cloud-client.js';
10
11
  import { listFunctions, getFunction, runFunction, formatDescribe, formatDescribeRoot, formatFlat, formatResult, formatUnconnected } from '../lib/function-client.js';
11
12
  import { listWorkspaces, resolveWorkspaceId, getFileTree, readFile, writeFile, editFile, deleteFile, getLocalWorkspacePath, pullWorkspace, pushWorkspace } from '../lib/environment-client.js';
12
13
 
14
+ /**
15
+ * Test if browser can actually open URLs by starting a local server and
16
+ * checking if we receive a request when we try to open it.
17
+ * @param {number} timeout - Max time to wait in ms
18
+ * @returns {Promise<boolean>} - true if browser can open URLs
19
+ */
20
+ async function canOpenBrowser(timeout = 2500) {
21
+ return new Promise((resolve) => {
22
+ let resolved = false;
23
+ const done = (result) => {
24
+ if (!resolved) {
25
+ resolved = true;
26
+ resolve(result);
27
+ }
28
+ };
29
+
30
+ const server = http.createServer((req, res) => {
31
+ res.writeHead(200, { 'Content-Type': 'text/html' });
32
+ res.end('<html><body><script>window.close()</script>Checking browser... you can close this tab.</body></html>');
33
+ server.close();
34
+ done(true);
35
+ });
36
+
37
+ server.on('error', () => done(false));
38
+
39
+ server.listen(0, '127.0.0.1', async () => {
40
+ const port = server.address().port;
41
+ const checkUrl = `http://127.0.0.1:${port}/browser-check`;
42
+
43
+ try {
44
+ const open = (await import('open')).default;
45
+ await open(checkUrl);
46
+ } catch {
47
+ // open() threw an error
48
+ server.close();
49
+ done(false);
50
+ return;
51
+ }
52
+
53
+ // Wait for browser to hit our server
54
+ setTimeout(() => {
55
+ server.close();
56
+ done(false);
57
+ }, timeout);
58
+ });
59
+ });
60
+ }
61
+
13
62
  const program = new Command();
14
63
 
15
64
  program
@@ -22,8 +71,8 @@ program
22
71
  .description('Login to Papercrane. Opens browser for authentication, or use --api-key for direct login.')
23
72
  .option('--api-key <key>', 'API key for direct login (skips browser)')
24
73
  .option('--url <url>', 'API base URL (saves to config)')
25
- .option('--nowait', 'Print auth URL and exit immediately (for AI assistants)')
26
- .option('--check', 'Check if pending login was completed')
74
+ .option('--print-url', 'Print auth URL and exit (use with --complete after authorizing)')
75
+ .option('--complete', 'Complete a pending login started with --print-url')
27
76
  .action(async (options) => {
28
77
  try {
29
78
  const { getApiBaseUrl, setApiBaseUrl, getPendingSession, setPendingSession, clearPendingSession } = await import('../lib/config.js');
@@ -54,15 +103,15 @@ program
54
103
  return;
55
104
  }
56
105
 
57
- // --check: Check if pending login was completed
58
- if (options.check) {
106
+ // --complete: Check if pending login was completed
107
+ if (options.complete) {
59
108
  const pending = await getPendingSession();
60
109
  if (!pending) {
61
- console.log(chalk.yellow('No pending login session. Run: papercrane login --nowait'));
110
+ console.log(chalk.yellow('No pending login session. Run: papercrane login --print-url'));
62
111
  process.exit(1);
63
112
  }
64
113
 
65
- const statusRes = await http.get(`${pending.baseUrl}/api/cli-auth/status?session=${encodeURIComponent(pending.session)}`, {
114
+ const statusRes = await httpClient.get(`${pending.baseUrl}/api/cli-auth/status?session=${encodeURIComponent(pending.session)}`, {
66
115
  validateStatus: () => true
67
116
  });
68
117
 
@@ -78,7 +127,22 @@ program
78
127
  }
79
128
  }
80
129
 
81
- console.log(chalk.yellow('Not yet authorized. Please click the login URL, then run: papercrane login --check'));
130
+ console.log(chalk.yellow('Not yet authorized. Please click the login URL, then run: papercrane login --complete'));
131
+ process.exit(1);
132
+ }
133
+
134
+ // Check server connectivity before starting auth flow
135
+ // Extract domain from baseUrl for allowlist message
136
+ const urlDomain = new URL(baseUrl).hostname;
137
+ try {
138
+ await httpClient.get(`${baseUrl}/api/cli-auth/status?session=connectivity-check`, { timeout: 5000 });
139
+ } catch (connectError) {
140
+ console.log(chalk.yellow('\n⚠️ Cannot reach Papercrane server.'));
141
+ console.log(chalk.dim(`\nCould not connect to: ${baseUrl}`));
142
+ console.log(chalk.dim('\nIf you\'re using Claude Desktop or Claude app:'));
143
+ console.log(chalk.dim(' 1. Go to Settings → Capabilities → Domain allowlist'));
144
+ console.log(chalk.dim(` 2. Add: ${urlDomain}`));
145
+ console.log(chalk.dim(' 3. Try again\n'));
82
146
  process.exit(1);
83
147
  }
84
148
 
@@ -88,7 +152,7 @@ program
88
152
  const session = generateState();
89
153
 
90
154
  // Initialize session on server
91
- const initRes = await http.post(`${baseUrl}/api/cli-auth/init`, { session }, {
155
+ const initRes = await httpClient.post(`${baseUrl}/api/cli-auth/init`, { session }, {
92
156
  headers: { 'Content-Type': 'application/json' },
93
157
  validateStatus: () => true
94
158
  });
@@ -99,25 +163,41 @@ program
99
163
 
100
164
  const authUrl = `${baseUrl}/cli-auth?session=${session}`;
101
165
 
102
- // --nowait: Print URL and exit immediately (for AI assistants)
103
- if (options.nowait) {
166
+ // --print-url: Print URL and exit immediately (for AI assistants)
167
+ if (options.printUrl) {
104
168
  await setPendingSession({ session, baseUrl });
105
169
  console.log(chalk.cyan('\nOpen this URL to authenticate:\n'));
106
170
  console.log(` ${authUrl}\n`);
107
- console.log(chalk.dim('After authorizing, run: papercrane login --check\n'));
171
+ console.log(chalk.dim('After authorizing, run: papercrane login --complete\n'));
108
172
  process.exit(0);
109
173
  }
110
174
 
175
+ // Test if browser can actually open URLs
176
+ console.log(chalk.dim('\nChecking browser availability...'));
177
+ const browserWorks = await canOpenBrowser();
178
+
111
179
  console.log(chalk.cyan('\nOpen this URL to authenticate:\n'));
112
180
  console.log(` ${authUrl}\n`);
113
181
 
114
- // Try to open browser automatically
182
+ if (!browserWorks) {
183
+ // Browser can't open - save session and exit (non-blocking)
184
+ await setPendingSession({ session, baseUrl });
185
+ console.log(chalk.yellow('Browser could not be opened automatically.'));
186
+ console.log(chalk.dim('\nPlease open the URL above manually, then run: papercrane login --complete\n'));
187
+ process.exit(0);
188
+ }
189
+
190
+ // Browser works - open the auth URL and poll
115
191
  try {
116
192
  const open = (await import('open')).default;
117
193
  await open(authUrl);
118
194
  console.log(chalk.dim('(Browser opened automatically)'));
119
195
  } catch {
120
- console.log(chalk.yellow('Could not open browser automatically. Please click or copy the URL above.'));
196
+ // Shouldn't happen since canOpenBrowser passed, but handle it
197
+ await setPendingSession({ session, baseUrl });
198
+ console.log(chalk.yellow('Could not open browser.'));
199
+ console.log(chalk.dim('\nPlease open the URL above manually, then run: papercrane login --complete\n'));
200
+ process.exit(0);
121
201
  }
122
202
 
123
203
  console.log(chalk.dim('\nWaiting for authorization... (press Ctrl+C to cancel)\n'));
@@ -128,7 +208,7 @@ program
128
208
  const startTime = Date.now();
129
209
 
130
210
  while (Date.now() - startTime < timeout) {
131
- const statusRes = await http.get(`${baseUrl}/api/cli-auth/status?session=${encodeURIComponent(session)}`, {
211
+ const statusRes = await httpClient.get(`${baseUrl}/api/cli-auth/status?session=${encodeURIComponent(session)}`, {
132
212
  validateStatus: () => true
133
213
  });
134
214
 
package/lib/config.js CHANGED
@@ -125,7 +125,7 @@ export async function clearDefaultWorkspace() {
125
125
  }
126
126
 
127
127
  /**
128
- * Get pending login session (for --nowait / --check flow)
128
+ * Get pending login session (for --print-url / --complete flow)
129
129
  * @returns {Promise<{session: string, baseUrl: string}|null>}
130
130
  */
131
131
  export async function getPendingSession() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papercraneai/cli",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "description": "CLI tool for managing OAuth credentials for LLM integrations",
5
5
  "main": "index.js",
6
6
  "type": "module",