@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 +95 -15
- package/lib/config.js +1 -1
- package/package.json +1 -1
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
|
|
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('--
|
|
26
|
-
.option('--
|
|
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
|
-
// --
|
|
58
|
-
if (options.
|
|
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 --
|
|
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
|
|
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 --
|
|
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
|
|
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
|
-
// --
|
|
103
|
-
if (options.
|
|
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 --
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 --
|
|
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() {
|