@photon-cli/flux 0.1.5 → 0.1.6

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/src/cli.ts DELETED
@@ -1,475 +0,0 @@
1
- /**
2
- * Flux CLI - gRPC Client for iMessage Bridge
3
- * ==========================================
4
- * This code connects the Flux CLI to the Flux Server's iMessage bridge.
5
- * Users define their LangChain agent in agent.ts with `export default agent`
6
- */
7
-
8
- import { Service, server, client, bidi, createGrpcClient } from "better-grpc";
9
- import { renderChatUI } from "@photon-ai/rapid/cli-chat";
10
- import * as fs from "fs";
11
- import * as path from "path";
12
- import * as readline from "readline";
13
- import { pathToFileURL } from "url";
14
-
15
- // --- Configuration ---
16
- const GRPC_SERVER_ADDRESS = process.env.FLUX_SERVER_ADDRESS || "localhost:50051";
17
- const CONFIG_DIR = path.join(process.env.HOME || "~", ".flux");
18
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
19
- const AGENT_FILE_NAME = "agent.ts";
20
-
21
- // --- Auth Storage ---
22
-
23
- interface FluxConfig {
24
- phoneNumber?: string;
25
- }
26
-
27
- function loadConfig(): FluxConfig {
28
- try {
29
- if (fs.existsSync(CONFIG_FILE)) {
30
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
31
- }
32
- } catch {}
33
- return {};
34
- }
35
-
36
- function saveConfig(config: FluxConfig): void {
37
- if (!fs.existsSync(CONFIG_DIR)) {
38
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
39
- }
40
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
41
- }
42
-
43
- function clearConfig(): void {
44
- if (fs.existsSync(CONFIG_FILE)) {
45
- fs.unlinkSync(CONFIG_FILE);
46
- }
47
- }
48
-
49
- async function prompt(question: string): Promise<string> {
50
- const rl = readline.createInterface({
51
- input: process.stdin,
52
- output: process.stdout,
53
- });
54
- return new Promise((resolve) => {
55
- rl.question(question, (answer) => {
56
- rl.close();
57
- resolve(answer.trim());
58
- });
59
- });
60
- }
61
-
62
- async function login(): Promise<string> {
63
- const phoneNumber = await prompt("Enter your phone number (e.g. +15551234567): ");
64
- if (!phoneNumber.match(/^\+?[0-9]{10,15}$/)) {
65
- console.error("Invalid phone number format.");
66
- process.exit(1);
67
- }
68
-
69
- // Validate with server (checks/creates user in Firebase)
70
- console.log("[FLUX] Validating with server...");
71
- try {
72
- const clientImpl = FluxService.Client({
73
- async onIncomingMessage() {
74
- return { received: true };
75
- },
76
- });
77
- const client = await createGrpcClient(GRPC_SERVER_ADDRESS, clientImpl);
78
- const result = await client.FluxService.validateUser(phoneNumber);
79
-
80
- if (result.error) {
81
- console.error(`[FLUX] Login failed: ${result.error}`);
82
- process.exit(1);
83
- }
84
-
85
- if (result.created) {
86
- console.log(`[FLUX] New account created for ${phoneNumber}`);
87
- } else if (result.exists) {
88
- console.log(`[FLUX] Welcome back, ${phoneNumber}`);
89
- }
90
-
91
- saveConfig({ phoneNumber });
92
- console.log(`[FLUX] Logged in as ${phoneNumber}`);
93
- return phoneNumber;
94
- } catch (error: any) {
95
- console.error(`[FLUX] Failed to connect to server: ${error.message}`);
96
- console.error(`[FLUX] Make sure the Flux server is running at ${GRPC_SERVER_ADDRESS}`);
97
- process.exit(1);
98
- }
99
- }
100
-
101
- function logout(): void {
102
- clearConfig();
103
- console.log("[FLUX] Logged out.");
104
- }
105
-
106
- async function getPhoneNumber(): Promise<string> {
107
- const config = loadConfig();
108
- if (config.phoneNumber) {
109
- return config.phoneNumber;
110
- }
111
- console.log("[FLUX] Not logged in.");
112
- return await login();
113
- }
114
-
115
- // --- Agent Types ---
116
-
117
- /**
118
- * FluxAgent interface - users must export default an object matching this interface
119
- * The agent receives a message and returns a response string
120
- */
121
- export interface FluxAgent {
122
- invoke: (input: { message: string; userPhoneNumber: string; imageBase64?: string }) => Promise<string>;
123
- }
124
-
125
- // --- Agent Loader ---
126
-
127
- /**
128
- * Find agent.ts in the current working directory
129
- */
130
- function findAgentFile(): string | null {
131
- const cwd = process.cwd();
132
- const agentPath = path.join(cwd, AGENT_FILE_NAME);
133
-
134
- if (fs.existsSync(agentPath)) {
135
- return agentPath;
136
- }
137
-
138
- // Also check for agent.js
139
- const jsPath = path.join(cwd, "agent.js");
140
- if (fs.existsSync(jsPath)) {
141
- return jsPath;
142
- }
143
-
144
- return null;
145
- }
146
-
147
- /**
148
- * Validate that the agent file exports a default agent with invoke method
149
- */
150
- async function validateAgentFile(agentPath: string): Promise<{ valid: boolean; error?: string }> {
151
- try {
152
- const moduleUrl = pathToFileURL(agentPath).href;
153
- const agentModule = await import(moduleUrl);
154
-
155
- if (!agentModule.default) {
156
- return { valid: false, error: "No default export found. Use `export default agent`" };
157
- }
158
-
159
- const agent = agentModule.default;
160
-
161
- if (typeof agent.invoke !== "function") {
162
- return { valid: false, error: "Agent must have an `invoke` method" };
163
- }
164
-
165
- return { valid: true };
166
- } catch (error: any) {
167
- return { valid: false, error: `Failed to load agent: ${error.message}` };
168
- }
169
- }
170
-
171
- /**
172
- * Load the agent from agent.ts
173
- */
174
- async function loadAgent(agentPath: string): Promise<FluxAgent> {
175
- const moduleUrl = pathToFileURL(agentPath).href;
176
- const agentModule = await import(moduleUrl);
177
- return agentModule.default as FluxAgent;
178
- }
179
-
180
- // --- Message Types ---
181
-
182
- export interface IncomingMessage {
183
- userPhoneNumber: string;
184
- text: string;
185
- imageBase64?: string;
186
- chatGuid: string;
187
- messageGuid: string;
188
- }
189
-
190
- export interface OutgoingMessage {
191
- userPhoneNumber: string;
192
- text: string;
193
- chatGuid?: string;
194
- }
195
-
196
- // --- FluxService Definition (must match server) ---
197
-
198
- abstract class FluxService extends Service("FluxService") {
199
- sendMessage = server<(message: OutgoingMessage) => { success: boolean; error?: string }>();
200
- messageStream = bidi<(message: IncomingMessage | { ack: string }) => void>();
201
- registerAgent = server<(phoneNumber: string) => { success: boolean; error?: string }>();
202
- unregisterAgent = server<(phoneNumber: string) => { success: boolean }>();
203
- onIncomingMessage = client<(message: IncomingMessage) => { received: boolean }>();
204
- // Login validation - checks if user exists in Firebase
205
- validateUser = server<(phoneNumber: string) => { exists: boolean; created: boolean; error?: string }>();
206
- }
207
-
208
- // --- FluxClient Class ---
209
-
210
- export class FluxClient {
211
- private client: Awaited<ReturnType<typeof createGrpcClient>> | null = null;
212
- private phoneNumber: string;
213
- private onMessage: (message: IncomingMessage) => Promise<string | void>;
214
-
215
- constructor(
216
- phoneNumber: string,
217
- onMessage: (message: IncomingMessage) => Promise<string | void>
218
- ) {
219
- this.phoneNumber = phoneNumber.replace(/[\s\-\(\)]/g, "");
220
- this.onMessage = onMessage;
221
- }
222
-
223
- async connect(): Promise<void> {
224
- const clientImpl = FluxService.Client({
225
- async onIncomingMessage(message: IncomingMessage) {
226
- return { received: true };
227
- },
228
- });
229
-
230
- this.client = await createGrpcClient(GRPC_SERVER_ADDRESS, clientImpl);
231
- console.log(`[FLUX] Connected to server at ${GRPC_SERVER_ADDRESS}`);
232
- }
233
-
234
- async register(): Promise<boolean> {
235
- if (!this.client) throw new Error("Not connected. Call connect() first.");
236
-
237
- const result = await this.client.FluxService.registerAgent(this.phoneNumber);
238
- if (result.success) {
239
- console.log(`[FLUX] Registered agent for ${this.phoneNumber}`);
240
- this.startMessageStream();
241
- } else {
242
- console.error(`[FLUX] Registration failed: ${result.error}`);
243
- }
244
- return result.success;
245
- }
246
-
247
- private async startMessageStream(): Promise<void> {
248
- if (!this.client) return;
249
-
250
- (async () => {
251
- for await (const [message] of this.client!.FluxService.messageStream) {
252
- if ("ack" in message) {
253
- console.log(`[FLUX] Received ack: ${message.ack}`);
254
- } else {
255
- console.log(`[FLUX] Incoming message from ${message.userPhoneNumber}: ${message.text}`);
256
-
257
- // Acknowledge receipt
258
- await this.client!.FluxService.messageStream({ ack: message.messageGuid });
259
-
260
- // Process with user's agent and get response
261
- const response = await this.onMessage(message);
262
-
263
- // Send response if agent returned one
264
- if (response) {
265
- await this.sendMessage(message.userPhoneNumber, response, message.chatGuid);
266
- }
267
- }
268
- }
269
- })();
270
- }
271
-
272
- async sendMessage(to: string, text: string, chatGuid?: string): Promise<boolean> {
273
- if (!this.client) throw new Error("Not connected. Call connect() first.");
274
-
275
- const result = await this.client.FluxService.sendMessage({
276
- userPhoneNumber: to,
277
- text,
278
- chatGuid,
279
- });
280
-
281
- if (!result.success) {
282
- console.error(`[FLUX] Send failed: ${result.error}`);
283
- }
284
- return result.success;
285
- }
286
-
287
- async disconnect(): Promise<void> {
288
- if (!this.client) return;
289
-
290
- await this.client.FluxService.unregisterAgent(this.phoneNumber);
291
- console.log(`[FLUX] Unregistered agent for ${this.phoneNumber}`);
292
- this.client = null;
293
- }
294
- }
295
-
296
- // --- CLI Commands ---
297
-
298
- /**
299
- * Validate command - checks if agent.ts exports correctly
300
- */
301
- async function validateCommand(): Promise<boolean> {
302
- const agentPath = findAgentFile();
303
-
304
- if (!agentPath) {
305
- console.error("[FLUX] No agent.ts or agent.js found in current directory.");
306
- console.error("[FLUX] Create an agent.ts file with `export default agent`");
307
- return false;
308
- }
309
-
310
- console.log(`[FLUX] Validating ${path.basename(agentPath)}...`);
311
- const result = await validateAgentFile(agentPath);
312
-
313
- if (result.valid) {
314
- console.log("[FLUX] ✓ Agent is valid!");
315
- return true;
316
- } else {
317
- console.error(`[FLUX] ✗ Validation failed: ${result.error}`);
318
- return false;
319
- }
320
- }
321
-
322
- /**
323
- * Run agent locally (for testing without connecting to bridge)
324
- * Uses the Rapid TUI chat interface
325
- */
326
- async function runLocal() {
327
- const agentPath = findAgentFile();
328
-
329
- if (!agentPath) {
330
- console.error("[FLUX] No agent.ts or agent.js found in current directory.");
331
- console.error("[FLUX] Create an agent.ts file with `export default agent`");
332
- process.exit(1);
333
- }
334
-
335
- // Validate first
336
- const validation = await validateAgentFile(agentPath);
337
- if (!validation.valid) {
338
- console.error(`[FLUX] Agent validation failed: ${validation.error}`);
339
- process.exit(1);
340
- }
341
-
342
- // Load the agent
343
- const agent = await loadAgent(agentPath);
344
-
345
- // Start the TUI chat interface
346
- const chat = renderChatUI();
347
-
348
- chat.sendMessage("Welcome to Flux! Your agent is loaded. Type a message to test it.");
349
-
350
- chat.onInput(async (input) => {
351
- chat.sendMessage("Thinking...");
352
-
353
- try {
354
- const response = await agent.invoke({
355
- message: input,
356
- userPhoneNumber: "+1234567890", // Mock phone number for local testing
357
- });
358
- chat.sendMessage(response);
359
- } catch (error: any) {
360
- chat.sendMessage(`Error: ${error.message}`);
361
- }
362
- });
363
-
364
- // Keep the Ink app alive. Press Ctrl+C to exit.
365
- await new Promise(() => {});
366
- }
367
-
368
- /**
369
- * Run agent in production mode (connected to bridge)
370
- */
371
- async function runProd() {
372
- const phoneNumber = await getPhoneNumber();
373
- const agentPath = findAgentFile();
374
-
375
- if (!agentPath) {
376
- console.error("[FLUX] No agent.ts or agent.js found in current directory.");
377
- console.error("[FLUX] Create an agent.ts file with `export default agent`");
378
- process.exit(1);
379
- }
380
-
381
- // Validate first
382
- const validation = await validateAgentFile(agentPath);
383
- if (!validation.valid) {
384
- console.error(`[FLUX] Agent validation failed: ${validation.error}`);
385
- process.exit(1);
386
- }
387
-
388
- // Load the agent
389
- console.log(`[FLUX] Loading agent from ${path.basename(agentPath)}...`);
390
- const agent = await loadAgent(agentPath);
391
- console.log("[FLUX] Agent loaded successfully!");
392
-
393
- // Create client with the user's agent as the message handler
394
- const flux = new FluxClient(phoneNumber, async (message) => {
395
- console.log(`[FLUX] Processing message from ${message.userPhoneNumber}: ${message.text}`);
396
-
397
- try {
398
- const response = await agent.invoke({
399
- message: message.text,
400
- userPhoneNumber: message.userPhoneNumber,
401
- imageBase64: message.imageBase64,
402
- });
403
- console.log(`[FLUX] Agent response: ${response}`);
404
- return response;
405
- } catch (error: any) {
406
- console.error(`[FLUX] Agent error: ${error.message}`);
407
- return "Sorry, I encountered an error processing your message.";
408
- }
409
- });
410
-
411
- // Connect and register
412
- await flux.connect();
413
- await flux.register();
414
-
415
- console.log("[FLUX] Agent running in production mode. Press Ctrl+C to stop.");
416
- console.log(`[FLUX] Messages to ${phoneNumber} will be processed by your agent.\n`);
417
-
418
- // Handle shutdown
419
- process.on("SIGINT", async () => {
420
- console.log("\n[FLUX] Shutting down...");
421
- await flux.disconnect();
422
- process.exit(0);
423
- });
424
-
425
- // Keep alive
426
- await new Promise(() => {});
427
- }
428
-
429
- async function main() {
430
- const command = process.argv[2];
431
- const flag = process.argv[3];
432
-
433
- switch (command) {
434
- case "login":
435
- await login();
436
- break;
437
- case "logout":
438
- logout();
439
- break;
440
- case "run":
441
- if (flag === "--local") {
442
- await runLocal();
443
- } else if (flag === "--prod" || !flag) {
444
- // Default to prod mode
445
- await runProd();
446
- } else {
447
- console.error(`[FLUX] Unknown flag: ${flag}`);
448
- console.log("Usage: flux run [--local | --prod]");
449
- }
450
- break;
451
- case "validate":
452
- await validateCommand();
453
- break;
454
- case "whoami":
455
- const config = loadConfig();
456
- if (config.phoneNumber) {
457
- console.log(`[FLUX] Logged in as ${config.phoneNumber}`);
458
- } else {
459
- console.log("[FLUX] Not logged in.");
460
- }
461
- break;
462
- default:
463
- console.log("Flux CLI - Connect LangChain agents to iMessage\n");
464
- console.log("Commands:");
465
- console.log(" flux login - Log in with your phone number");
466
- console.log(" flux logout - Log out");
467
- console.log(" flux validate - Check if agent.ts exports correctly");
468
- console.log(" flux run --local - Test agent locally (no server connection)");
469
- console.log(" flux run --prod - Run agent connected to bridge (default)");
470
- console.log(" flux whoami - Show current logged in user");
471
- break;
472
- }
473
- }
474
-
475
- main().catch(console.error);
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "esModuleInterop": true,
7
- "strict": true,
8
- "skipLibCheck": true,
9
- "outDir": "./dist",
10
- "rootDir": "./src",
11
- "declaration": true
12
- },
13
- "include": ["src/**/*"],
14
- "exclude": ["node_modules", "dist"]
15
- }
package/tsup.config.ts DELETED
@@ -1,27 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/cli.ts"],
5
- format: ["cjs", "esm"],
6
- dts: true,
7
- sourcemap: true,
8
- clean: true,
9
- treeshake: true,
10
- minify: false,
11
- target: "es2022",
12
- outDir: "dist",
13
- platform: "node",
14
- splitting: false,
15
- bundle: true,
16
- banner: {
17
- js: "#!/usr/bin/env node",
18
- },
19
- external: [
20
- "zod",
21
- "nice-grpc",
22
- "@grpc/grpc-js",
23
- "nice-grpc-common",
24
- "better-grpc",
25
- "it-pushable",
26
- ],
27
- });
File without changes