@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.
@@ -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
@@ -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
@@ -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
@@ -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 Login\x1b[0m');
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
- // Validate platform reachable
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}/ready`);
13
- if (!resp.ok)
14
- throw new Error();
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 platform');
18
- console.error(` Check: ${platformUrl}`);
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
- let token;
22
- let email;
23
- let plan;
24
- let userId;
25
- if (options.apiKey) {
26
- // API key auth
27
- token = options.apiKey;
28
- const client = new PlatformClient({ token, email: '', plan: 'free', platformUrl, expiresAt: Infinity });
29
- try {
30
- const user = await client.getUser();
31
- email = user.email;
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
- else {
42
- // Email/password auth
43
- email = options.email ?? await prompt(' Email: ');
44
- const password = await prompt(' Password: ', true);
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/login`, {
47
- method: 'POST',
48
- headers: { 'Content-Type': 'application/json' },
49
- body: JSON.stringify({ email, password }),
50
- });
51
- if (!resp.ok) {
52
- const err = await resp.json().catch(() => ({ error: 'Login failed' }));
53
- console.error(`\n \x1b[31m✗\x1b[0m ${err.error}`);
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
- const data = await resp.json();
58
- token = data.token;
59
- email = data.user.email;
60
- plan = data.user.plan;
61
- userId = data.user.id;
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 (err) {
64
- console.error(`\n \x1b[31m✗\x1b[0m ${err instanceof Error ? err.message : 'Login failed'}`);
65
- process.exit(1);
66
- return;
117
+ catch {
118
+ // Network error keep trying
67
119
  }
68
120
  }
69
- const expiresAt = options.apiKey
70
- ? Date.now() + 365 * 24 * 60 * 60 * 1000 // 1 year for API keys
71
- : Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days for JWT
72
- saveCredentials({ token, email, plan: plan, platformUrl, expiresAt, userId });
73
- console.log('');
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
- console.log(' What\'s unlocked:');
77
- console.log(' \x1b[36mfw deploy src/workflow.ts\x1b[0m \x1b[2m# deploy to cloud\x1b[0m');
78
- console.log(' \x1b[36mfw status\x1b[0m \x1b[2m# see deployments + usage\x1b[0m');
79
- console.log(' \x1b[36mweaver assistant\x1b[0m \x1b[2m# AI with platform credits\x1b[0m');
80
- console.log('');
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,2 @@
1
+ export declare function handleConnect(projectDir: string): Promise<void>;
2
+ //# sourceMappingURL=connect.d.ts.map
@@ -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
@@ -18,6 +18,8 @@ export interface InitOptions {
18
18
  useCase?: string;
19
19
  mcp?: boolean;
20
20
  agent?: boolean;
21
+ withWeaver?: boolean;
22
+ weaver?: boolean;
21
23
  }
22
24
  export interface InitConfig {
23
25
  projectName: string;