@synergenius/flow-weaver 0.23.0 → 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. */
@@ -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