@synergenius/flow-weaver 0.22.10 → 0.23.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/dist/agent/device-connection.d.ts +72 -0
- package/dist/agent/device-connection.js +177 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/providers/claude-cli.d.ts +1 -0
- package/dist/agent/providers/claude-cli.js +3 -0
- package/dist/agent/types.d.ts +2 -0
- package/dist/cli/commands/auth.js +166 -58
- package/dist/cli/commands/connect.d.ts +2 -0
- package/dist/cli/commands/connect.js +69 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +78 -3
- package/dist/cli/config/credentials.js +1 -1
- package/dist/cli/flow-weaver.mjs +834 -393
- package/dist/cli/index.js +11 -0
- package/dist/cli/pack-commands.js +23 -0
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Connection — WebSocket client for connecting a local machine
|
|
3
|
+
* to the Flow Weaver platform as a mounted device.
|
|
4
|
+
*
|
|
5
|
+
* This is the transport layer. Packs register their own request handlers
|
|
6
|
+
* on top of this connection (e.g., improve status, bot management).
|
|
7
|
+
*
|
|
8
|
+
* The platform relays Studio requests to connected devices and forwards
|
|
9
|
+
* device events to Studio subscribers.
|
|
10
|
+
*/
|
|
11
|
+
export interface DeviceInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
hostname: string;
|
|
14
|
+
projectDir: string;
|
|
15
|
+
platform: string;
|
|
16
|
+
capabilities: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface DeviceConnectionOptions {
|
|
19
|
+
platformUrl: string;
|
|
20
|
+
token: string;
|
|
21
|
+
projectDir: string;
|
|
22
|
+
deviceName?: string;
|
|
23
|
+
onConnect?: () => void;
|
|
24
|
+
onDisconnect?: (code: number) => void;
|
|
25
|
+
onEvent?: (event: DeviceEvent) => void;
|
|
26
|
+
logger?: (msg: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface DeviceEvent {
|
|
29
|
+
type: string;
|
|
30
|
+
data: Record<string, unknown>;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
type RequestHandler = (method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
34
|
+
export declare class DeviceConnection {
|
|
35
|
+
private ws;
|
|
36
|
+
private heartbeatInterval;
|
|
37
|
+
private reconnectTimeout;
|
|
38
|
+
private requestHandlers;
|
|
39
|
+
private connected;
|
|
40
|
+
private shouldReconnect;
|
|
41
|
+
private readonly options;
|
|
42
|
+
private readonly deviceInfo;
|
|
43
|
+
private readonly log;
|
|
44
|
+
constructor(options: DeviceConnectionOptions);
|
|
45
|
+
/**
|
|
46
|
+
* Add a capability to advertise to the platform.
|
|
47
|
+
*/
|
|
48
|
+
addCapability(capability: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Register a handler for incoming requests from the platform.
|
|
51
|
+
*/
|
|
52
|
+
onRequest(method: string, handler: RequestHandler): void;
|
|
53
|
+
/**
|
|
54
|
+
* Connect to the platform. Reconnects automatically on disconnect.
|
|
55
|
+
*/
|
|
56
|
+
connect(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Emit an event to the platform.
|
|
59
|
+
*/
|
|
60
|
+
emit(event: DeviceEvent): void;
|
|
61
|
+
/**
|
|
62
|
+
* Disconnect from the platform. No auto-reconnect.
|
|
63
|
+
*/
|
|
64
|
+
disconnect(): void;
|
|
65
|
+
isConnected(): boolean;
|
|
66
|
+
getDeviceInfo(): Readonly<DeviceInfo>;
|
|
67
|
+
private send;
|
|
68
|
+
private handleMessage;
|
|
69
|
+
private scheduleReconnect;
|
|
70
|
+
}
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=device-connection.d.ts.map
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Connection — WebSocket client for connecting a local machine
|
|
3
|
+
* to the Flow Weaver platform as a mounted device.
|
|
4
|
+
*
|
|
5
|
+
* This is the transport layer. Packs register their own request handlers
|
|
6
|
+
* on top of this connection (e.g., improve status, bot management).
|
|
7
|
+
*
|
|
8
|
+
* The platform relays Studio requests to connected devices and forwards
|
|
9
|
+
* device events to Studio subscribers.
|
|
10
|
+
*/
|
|
11
|
+
export class DeviceConnection {
|
|
12
|
+
ws = null;
|
|
13
|
+
heartbeatInterval = null;
|
|
14
|
+
reconnectTimeout = null;
|
|
15
|
+
requestHandlers = new Map();
|
|
16
|
+
connected = false;
|
|
17
|
+
shouldReconnect = true;
|
|
18
|
+
options;
|
|
19
|
+
deviceInfo;
|
|
20
|
+
log;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.log = options.logger ?? (() => { });
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
this.deviceInfo = {
|
|
26
|
+
name: options.deviceName ?? os.hostname(),
|
|
27
|
+
hostname: os.hostname(),
|
|
28
|
+
projectDir: options.projectDir,
|
|
29
|
+
platform: process.platform,
|
|
30
|
+
capabilities: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Add a capability to advertise to the platform.
|
|
35
|
+
*/
|
|
36
|
+
addCapability(capability) {
|
|
37
|
+
if (!this.deviceInfo.capabilities.includes(capability)) {
|
|
38
|
+
this.deviceInfo.capabilities.push(capability);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register a handler for incoming requests from the platform.
|
|
43
|
+
*/
|
|
44
|
+
onRequest(method, handler) {
|
|
45
|
+
this.requestHandlers.set(method, handler);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Connect to the platform. Reconnects automatically on disconnect.
|
|
49
|
+
*/
|
|
50
|
+
async connect() {
|
|
51
|
+
const wsUrl = this.options.platformUrl
|
|
52
|
+
.replace(/^http/, 'ws')
|
|
53
|
+
.replace(/\/$/, '') + '/ws/device';
|
|
54
|
+
this.log(`Connecting to ${wsUrl}...`);
|
|
55
|
+
this.shouldReconnect = true;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
try {
|
|
58
|
+
this.ws = new WebSocket(`${wsUrl}?token=${encodeURIComponent(this.options.token)}`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
reject(err);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.ws.addEventListener('open', () => {
|
|
65
|
+
this.connected = true;
|
|
66
|
+
this.log(`Connected as "${this.deviceInfo.name}"`);
|
|
67
|
+
// Send device registration
|
|
68
|
+
this.send({ type: 'device:register', device: this.deviceInfo });
|
|
69
|
+
// Start heartbeat
|
|
70
|
+
this.heartbeatInterval = setInterval(() => {
|
|
71
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
72
|
+
this.send({ type: 'heartbeat', timestamp: Date.now() });
|
|
73
|
+
}
|
|
74
|
+
}, 30_000);
|
|
75
|
+
this.options.onConnect?.();
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
this.ws.addEventListener('message', async (event) => {
|
|
79
|
+
try {
|
|
80
|
+
const msg = JSON.parse(typeof event.data === 'string' ? event.data : String(event.data));
|
|
81
|
+
await this.handleMessage(msg);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
this.log(`Parse error: ${err instanceof Error ? err.message : err}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this.ws.addEventListener('close', (event) => {
|
|
88
|
+
this.connected = false;
|
|
89
|
+
if (this.heartbeatInterval)
|
|
90
|
+
clearInterval(this.heartbeatInterval);
|
|
91
|
+
this.options.onDisconnect?.(event.code);
|
|
92
|
+
if (this.shouldReconnect) {
|
|
93
|
+
this.log(`Disconnected (${event.code}). Reconnecting in 5s...`);
|
|
94
|
+
this.scheduleReconnect();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.ws.addEventListener('error', () => {
|
|
98
|
+
if (!this.connected) {
|
|
99
|
+
reject(new Error('WebSocket connection failed'));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.log('Connection error');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emit an event to the platform.
|
|
109
|
+
*/
|
|
110
|
+
emit(event) {
|
|
111
|
+
if (!this.connected)
|
|
112
|
+
return;
|
|
113
|
+
this.send({ type: 'device:event', event });
|
|
114
|
+
this.options.onEvent?.(event);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Disconnect from the platform. No auto-reconnect.
|
|
118
|
+
*/
|
|
119
|
+
disconnect() {
|
|
120
|
+
this.shouldReconnect = false;
|
|
121
|
+
if (this.heartbeatInterval)
|
|
122
|
+
clearInterval(this.heartbeatInterval);
|
|
123
|
+
if (this.reconnectTimeout)
|
|
124
|
+
clearTimeout(this.reconnectTimeout);
|
|
125
|
+
if (this.ws) {
|
|
126
|
+
this.ws.close(1000, 'Device disconnecting');
|
|
127
|
+
this.ws = null;
|
|
128
|
+
}
|
|
129
|
+
this.connected = false;
|
|
130
|
+
}
|
|
131
|
+
isConnected() {
|
|
132
|
+
return this.connected;
|
|
133
|
+
}
|
|
134
|
+
getDeviceInfo() {
|
|
135
|
+
return this.deviceInfo;
|
|
136
|
+
}
|
|
137
|
+
// --- Private ---
|
|
138
|
+
send(msg) {
|
|
139
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
140
|
+
this.ws.send(JSON.stringify(msg));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async handleMessage(msg) {
|
|
144
|
+
const type = String(msg.type ?? '');
|
|
145
|
+
const requestId = String(msg.requestId ?? '');
|
|
146
|
+
if (type === 'request') {
|
|
147
|
+
const method = String(msg.method ?? '');
|
|
148
|
+
const params = msg.params ?? {};
|
|
149
|
+
const handler = this.requestHandlers.get(method);
|
|
150
|
+
if (!handler) {
|
|
151
|
+
this.send({ type: 'response', requestId, success: false, error: `Unknown method: ${method}` });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const result = await handler(method, params);
|
|
156
|
+
this.send({ type: 'response', requestId, success: true, result });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
this.send({ type: 'response', requestId, success: false, error: err instanceof Error ? err.message : String(err) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
scheduleReconnect() {
|
|
164
|
+
if (this.reconnectTimeout)
|
|
165
|
+
clearTimeout(this.reconnectTimeout);
|
|
166
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
167
|
+
try {
|
|
168
|
+
await this.connect();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
this.log('Reconnect failed. Retrying in 10s...');
|
|
172
|
+
this.reconnectTimeout = setTimeout(() => this.scheduleReconnect(), 10_000);
|
|
173
|
+
}
|
|
174
|
+
}, 5_000);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=device-connection.js.map
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -17,4 +17,6 @@ export { createMcpBridge } from './mcp-bridge.js';
|
|
|
17
17
|
export { CliSession, getOrCreateCliSession, killCliSession, killAllCliSessions, } from './cli-session.js';
|
|
18
18
|
export { buildSafeEnv, buildSafeSpawnOpts, MINIMAL_PATH, ENV_ALLOWLIST } from './env-allowlist.js';
|
|
19
19
|
export { StreamJsonParser } from './streaming.js';
|
|
20
|
+
export { DeviceConnection } from './device-connection.js';
|
|
21
|
+
export type { DeviceConnectionOptions, DeviceInfo, DeviceEvent } from './device-connection.js';
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/agent/index.js
CHANGED
|
@@ -19,4 +19,6 @@ export { CliSession, getOrCreateCliSession, killCliSession, killAllCliSessions,
|
|
|
19
19
|
export { buildSafeEnv, buildSafeSpawnOpts, MINIMAL_PATH, ENV_ALLOWLIST } from './env-allowlist.js';
|
|
20
20
|
// Stream parser (for custom providers)
|
|
21
21
|
export { StreamJsonParser } from './streaming.js';
|
|
22
|
+
// Device connection (mount local machine into platform Studio)
|
|
23
|
+
export { DeviceConnection } from './device-connection.js';
|
|
22
24
|
//# sourceMappingURL=index.js.map
|
|
@@ -14,6 +14,7 @@ export declare class ClaudeCliProvider implements AgentProvider {
|
|
|
14
14
|
private mcpConfigPath;
|
|
15
15
|
private spawnFn;
|
|
16
16
|
private timeout;
|
|
17
|
+
private disallowedTools;
|
|
17
18
|
constructor(options?: ClaudeCliProviderOptions);
|
|
18
19
|
stream(messages: AgentMessage[], tools: ToolDefinition[], options?: StreamOptions): AsyncGenerator<StreamEvent>;
|
|
19
20
|
}
|
|
@@ -16,12 +16,14 @@ export class ClaudeCliProvider {
|
|
|
16
16
|
mcpConfigPath;
|
|
17
17
|
spawnFn;
|
|
18
18
|
timeout;
|
|
19
|
+
disallowedTools;
|
|
19
20
|
constructor(options = {}) {
|
|
20
21
|
this.binPath = options.binPath ?? 'claude';
|
|
21
22
|
this.cwd = options.cwd ?? process.cwd();
|
|
22
23
|
this.env = options.env ?? process.env;
|
|
23
24
|
this.model = options.model;
|
|
24
25
|
this.mcpConfigPath = options.mcpConfigPath;
|
|
26
|
+
this.disallowedTools = options.disallowedTools ?? [];
|
|
25
27
|
this.spawnFn = options.spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, { ...opts, stdio: opts.stdio }));
|
|
26
28
|
this.timeout = options.timeout ?? 600_000;
|
|
27
29
|
}
|
|
@@ -53,6 +55,7 @@ export class ClaudeCliProvider {
|
|
|
53
55
|
'bypassPermissions',
|
|
54
56
|
...(systemPrompt ? ['--system-prompt', systemPrompt] : []),
|
|
55
57
|
...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
|
|
58
|
+
...(this.disallowedTools.length > 0 ? ['--disallowed-tools', this.disallowedTools.join(' ')] : []),
|
|
56
59
|
...(model ? ['--model', model] : []),
|
|
57
60
|
];
|
|
58
61
|
// Spawn the CLI process
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface ClaudeCliProviderOptions {
|
|
|
131
131
|
spawnFn?: SpawnFn;
|
|
132
132
|
/** CLI timeout in milliseconds. Defaults to 120000. */
|
|
133
133
|
timeout?: number;
|
|
134
|
+
/** Disable specific built-in tools (e.g. ['Read', 'Edit', 'Write', 'Bash'] to force MCP tools). */
|
|
135
|
+
disallowedTools?: string[];
|
|
134
136
|
}
|
|
135
137
|
export interface CliSessionOptions {
|
|
136
138
|
/** Absolute path to the claude binary. */
|
|
@@ -4,80 +4,183 @@ import { PlatformClient } from '../config/platform-client.js';
|
|
|
4
4
|
export async function loginCommand(options) {
|
|
5
5
|
const platformUrl = options.platformUrl ?? getPlatformUrl();
|
|
6
6
|
console.log('');
|
|
7
|
-
console.log(' \x1b[1mFlow Weaver
|
|
8
|
-
console.log(` \x1b[2mPlatform: ${platformUrl}\x1b[0m`);
|
|
7
|
+
console.log(' \x1b[1mFlow Weaver Cloud\x1b[0m \x1b[2m(flowweaver.ai)\x1b[0m');
|
|
9
8
|
console.log('');
|
|
10
|
-
//
|
|
9
|
+
// API key mode (for CI/headless)
|
|
10
|
+
if (options.apiKey) {
|
|
11
|
+
await loginWithApiKey(options.apiKey, platformUrl);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Email mode (explicit --email flag)
|
|
15
|
+
if (options.email) {
|
|
16
|
+
await loginWithEmail(options.email, platformUrl);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Default: browser-first device auth
|
|
20
|
+
await loginWithBrowser(platformUrl);
|
|
21
|
+
}
|
|
22
|
+
async function loginWithBrowser(platformUrl) {
|
|
23
|
+
// Step 1: Request device code
|
|
24
|
+
let deviceCode;
|
|
25
|
+
let userCode;
|
|
26
|
+
let verificationUrl;
|
|
27
|
+
let interval;
|
|
11
28
|
try {
|
|
12
|
-
const resp = await fetch(`${platformUrl}/
|
|
13
|
-
if (!resp.ok)
|
|
14
|
-
|
|
29
|
+
const resp = await fetch(`${platformUrl}/auth/device`, { method: 'POST' });
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
// Platform doesn't support device auth — fall back to email
|
|
32
|
+
console.log(' \x1b[33m⚠\x1b[0m Device auth not available. Using email login.');
|
|
33
|
+
console.log('');
|
|
34
|
+
const email = await prompt(' Email: ');
|
|
35
|
+
await loginWithEmail(email, platformUrl);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const data = await resp.json();
|
|
39
|
+
deviceCode = data.deviceCode;
|
|
40
|
+
userCode = data.userCode;
|
|
41
|
+
verificationUrl = data.verificationUrl;
|
|
42
|
+
interval = data.interval ?? 5;
|
|
15
43
|
}
|
|
16
44
|
catch {
|
|
17
|
-
console.error(' \x1b[31m✗\x1b[0m Cannot connect to
|
|
18
|
-
console.error(
|
|
45
|
+
console.error(' \x1b[31m✗\x1b[0m Cannot connect to flowweaver.ai');
|
|
46
|
+
console.error(' Check your internet connection or set FW_PLATFORM_URL');
|
|
19
47
|
process.exit(1);
|
|
48
|
+
return;
|
|
20
49
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
plan = user.plan;
|
|
33
|
-
userId = user.id;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
console.error(' \x1b[31m✗\x1b[0m Invalid API key');
|
|
37
|
-
process.exit(1);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
50
|
+
// Step 2: Open browser
|
|
51
|
+
const authUrl = `${verificationUrl}?code=${userCode}`;
|
|
52
|
+
console.log(` Your code: \x1b[1m${userCode}\x1b[0m`);
|
|
53
|
+
console.log('');
|
|
54
|
+
try {
|
|
55
|
+
const { exec } = await import('child_process');
|
|
56
|
+
const openCmd = process.platform === 'darwin' ? 'open'
|
|
57
|
+
: process.platform === 'win32' ? 'start'
|
|
58
|
+
: 'xdg-open';
|
|
59
|
+
exec(`${openCmd} "${authUrl}"`);
|
|
60
|
+
console.log(' \x1b[2mOpening browser...\x1b[0m');
|
|
40
61
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
catch {
|
|
63
|
+
console.log(` Open this URL in your browser:`);
|
|
64
|
+
console.log(` \x1b[36m${authUrl}\x1b[0m`);
|
|
65
|
+
}
|
|
66
|
+
console.log('');
|
|
67
|
+
// Step 3: Poll for completion
|
|
68
|
+
process.stdout.write(' Waiting for authentication...');
|
|
69
|
+
let cancelled = false;
|
|
70
|
+
const sigHandler = () => { cancelled = true; };
|
|
71
|
+
process.on('SIGINT', sigHandler);
|
|
72
|
+
const maxAttempts = 120; // 10 minutes at 5s intervals
|
|
73
|
+
for (let i = 0; i < maxAttempts && !cancelled; i++) {
|
|
74
|
+
await new Promise(r => setTimeout(r, interval * 1000));
|
|
45
75
|
try {
|
|
46
|
-
const resp = await fetch(`${platformUrl}/auth/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
const resp = await fetch(`${platformUrl}/auth/device/poll?deviceCode=${deviceCode}`);
|
|
77
|
+
if (!resp.ok)
|
|
78
|
+
continue;
|
|
79
|
+
const data = await resp.json();
|
|
80
|
+
if (data.status === 'approved' && data.token && data.user) {
|
|
81
|
+
process.stdout.write(' \x1b[32m✓\x1b[0m\n\n');
|
|
82
|
+
saveCredentials({
|
|
83
|
+
token: data.token,
|
|
84
|
+
email: data.user.email,
|
|
85
|
+
plan: data.user.plan,
|
|
86
|
+
platformUrl,
|
|
87
|
+
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
|
|
88
|
+
userId: data.user.id,
|
|
89
|
+
});
|
|
90
|
+
console.log(` Logged in as \x1b[1m${data.user.email}\x1b[0m (${data.user.plan} plan)`);
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(' Try: \x1b[36mweaver assistant\x1b[0m');
|
|
93
|
+
console.log('');
|
|
94
|
+
process.removeListener('SIGINT', sigHandler);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (data.status === 'expired') {
|
|
98
|
+
process.stdout.write(' \x1b[31mtimed out\x1b[0m\n\n');
|
|
99
|
+
console.log(' Code expired. Run \x1b[36mfw login\x1b[0m again.');
|
|
100
|
+
console.log('');
|
|
101
|
+
process.removeListener('SIGINT', sigHandler);
|
|
54
102
|
process.exit(1);
|
|
55
103
|
return;
|
|
56
104
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
105
|
+
if (data.status === 'denied') {
|
|
106
|
+
process.stdout.write(' \x1b[31mdenied\x1b[0m\n\n');
|
|
107
|
+
console.log(' Access denied.');
|
|
108
|
+
console.log('');
|
|
109
|
+
process.removeListener('SIGINT', sigHandler);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Still pending — show a dot for progress
|
|
114
|
+
if (i % 4 === 3)
|
|
115
|
+
process.stdout.write('.');
|
|
62
116
|
}
|
|
63
|
-
catch
|
|
64
|
-
|
|
65
|
-
process.exit(1);
|
|
66
|
-
return;
|
|
117
|
+
catch {
|
|
118
|
+
// Network error — keep trying
|
|
67
119
|
}
|
|
68
120
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
121
|
+
process.removeListener('SIGINT', sigHandler);
|
|
122
|
+
if (cancelled) {
|
|
123
|
+
process.stdout.write(' \x1b[33mcancelled\x1b[0m\n\n');
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
process.stdout.write(' \x1b[31mtimed out\x1b[0m\n\n');
|
|
127
|
+
console.log(' Authentication timed out. Run \x1b[36mfw login\x1b[0m again.');
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function loginWithApiKey(apiKey, platformUrl) {
|
|
132
|
+
const client = new PlatformClient({ token: apiKey, email: '', plan: 'free', platformUrl, expiresAt: Infinity });
|
|
133
|
+
let email;
|
|
134
|
+
let plan;
|
|
135
|
+
let userId;
|
|
136
|
+
try {
|
|
137
|
+
const user = await client.getUser();
|
|
138
|
+
email = user.email;
|
|
139
|
+
plan = user.plan;
|
|
140
|
+
userId = user.id;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
console.error(' \x1b[31m✗\x1b[0m Invalid API key');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const expiresAt = Date.now() + 365 * 24 * 60 * 60 * 1000; // 1 year for API keys
|
|
148
|
+
saveCredentials({ token: apiKey, email, plan: plan, platformUrl, expiresAt, userId });
|
|
74
149
|
console.log(` \x1b[32m✓\x1b[0m Logged in as \x1b[1m${email}\x1b[0m (${plan} plan)`);
|
|
75
150
|
console.log('');
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
151
|
+
}
|
|
152
|
+
async function loginWithEmail(email, platformUrl) {
|
|
153
|
+
const password = await prompt(' Password: ', true);
|
|
154
|
+
try {
|
|
155
|
+
const resp = await fetch(`${platformUrl}/auth/login`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ email, password }),
|
|
159
|
+
});
|
|
160
|
+
if (!resp.ok) {
|
|
161
|
+
const err = await resp.json().catch(() => ({ error: 'Login failed' }));
|
|
162
|
+
console.error(`\n \x1b[31m✗\x1b[0m ${err.error}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const data = await resp.json();
|
|
167
|
+
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days for JWT
|
|
168
|
+
saveCredentials({
|
|
169
|
+
token: data.token,
|
|
170
|
+
email: data.user.email,
|
|
171
|
+
plan: data.user.plan,
|
|
172
|
+
platformUrl,
|
|
173
|
+
expiresAt,
|
|
174
|
+
userId: data.user.id,
|
|
175
|
+
});
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(` \x1b[32m✓\x1b[0m Logged in as \x1b[1m${data.user.email}\x1b[0m (${data.user.plan} plan)`);
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(`\n \x1b[31m✗\x1b[0m ${err instanceof Error ? err.message : 'Login failed'}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
81
184
|
}
|
|
82
185
|
export async function logoutCommand() {
|
|
83
186
|
clearCredentials();
|
|
@@ -99,6 +202,11 @@ export async function authStatusCommand() {
|
|
|
99
202
|
console.log(` Platform: ${creds.platformUrl}`);
|
|
100
203
|
console.log(` Token expires in: ${expiresIn}h`);
|
|
101
204
|
console.log('');
|
|
205
|
+
console.log(' Commands unlocked:');
|
|
206
|
+
console.log(' \x1b[36mfw deploy <file>\x1b[0m deploy to cloud');
|
|
207
|
+
console.log(' \x1b[36mfw cloud-status\x1b[0m see deployments + usage');
|
|
208
|
+
console.log(' \x1b[36mweaver assistant\x1b[0m AI with platform credits');
|
|
209
|
+
console.log('');
|
|
102
210
|
}
|
|
103
211
|
function prompt(message, hidden = false) {
|
|
104
212
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { DeviceConnection } from '../../agent/device-connection.js';
|
|
5
|
+
export async function handleConnect(projectDir) {
|
|
6
|
+
// Load credentials
|
|
7
|
+
const credPath = path.join(os.homedir(), '.fw', 'credentials.json');
|
|
8
|
+
if (!fs.existsSync(credPath)) {
|
|
9
|
+
console.error('\n Not logged in. Run: fw login\n');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
13
|
+
if (!creds.token || !creds.platformUrl || creds.expiresAt < Date.now()) {
|
|
14
|
+
console.error('\n Credentials expired. Run: fw login\n');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const conn = new DeviceConnection({
|
|
18
|
+
platformUrl: creds.platformUrl,
|
|
19
|
+
token: creds.token,
|
|
20
|
+
projectDir,
|
|
21
|
+
deviceName: path.basename(projectDir),
|
|
22
|
+
logger: (msg) => process.stderr.write(` \x1b[2m${msg}\x1b[0m\n`),
|
|
23
|
+
});
|
|
24
|
+
// Register basic file handlers (any project can use these)
|
|
25
|
+
conn.addCapability('file_read');
|
|
26
|
+
conn.addCapability('file_list');
|
|
27
|
+
conn.onRequest('file:read', async (_method, params) => {
|
|
28
|
+
const filePath = path.resolve(projectDir, String(params.path ?? ''));
|
|
29
|
+
if (!filePath.startsWith(projectDir))
|
|
30
|
+
throw new Error('Path outside project directory');
|
|
31
|
+
if (!fs.existsSync(filePath))
|
|
32
|
+
throw new Error('File not found');
|
|
33
|
+
const stat = fs.statSync(filePath);
|
|
34
|
+
if (stat.isDirectory())
|
|
35
|
+
return { type: 'directory', entries: fs.readdirSync(filePath) };
|
|
36
|
+
if (stat.size > 1_048_576)
|
|
37
|
+
throw new Error('File too large (>1MB)');
|
|
38
|
+
return { type: 'file', content: fs.readFileSync(filePath, 'utf-8') };
|
|
39
|
+
});
|
|
40
|
+
conn.onRequest('file:list', async (_method, params) => {
|
|
41
|
+
const dirPath = path.resolve(projectDir, String(params.path ?? '.'));
|
|
42
|
+
if (!dirPath.startsWith(projectDir))
|
|
43
|
+
throw new Error('Path outside project directory');
|
|
44
|
+
if (!fs.existsSync(dirPath))
|
|
45
|
+
throw new Error('Directory not found');
|
|
46
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
47
|
+
return entries
|
|
48
|
+
.filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist')
|
|
49
|
+
.map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', path: path.relative(projectDir, path.join(dirPath, e.name)) }));
|
|
50
|
+
});
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(' \x1b[1mflow-weaver connect\x1b[0m');
|
|
53
|
+
console.log(` \x1b[2mProject: ${path.basename(projectDir)}\x1b[0m`);
|
|
54
|
+
console.log(` \x1b[2mPlatform: ${creds.platformUrl}\x1b[0m`);
|
|
55
|
+
console.log('');
|
|
56
|
+
try {
|
|
57
|
+
await conn.connect();
|
|
58
|
+
console.log(' \x1b[2mPress Ctrl+C to disconnect.\x1b[0m\n');
|
|
59
|
+
await new Promise((resolve) => {
|
|
60
|
+
process.on('SIGINT', () => { console.log('\n \x1b[2mDisconnecting...\x1b[0m'); conn.disconnect(); resolve(); });
|
|
61
|
+
process.on('SIGTERM', () => { conn.disconnect(); resolve(); });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(` \x1b[31m✗\x1b[0m Connection failed: ${err instanceof Error ? err.message : err}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=connect.js.map
|