@cf-vibesdk/sdk 0.0.1 → 0.0.3
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/README.md +140 -0
- package/dist/index.d.ts +198 -13
- package/dist/index.js +626 -56
- package/dist/node.d.ts +10 -0
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# @cf-vibesdk/sdk
|
|
2
|
+
|
|
3
|
+
Client SDK for the VibeSDK platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cf-vibesdk/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart (Bun)
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { PhasicClient } from '@cf-vibesdk/sdk';
|
|
15
|
+
|
|
16
|
+
const client = new PhasicClient({
|
|
17
|
+
baseUrl: 'http://localhost:5173',
|
|
18
|
+
apiKey: process.env.VIBESDK_API_KEY!,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const session = await client.build('Build a simple hello world page.', {
|
|
22
|
+
projectType: 'app',
|
|
23
|
+
autoGenerate: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// High-level lifecycle waits
|
|
27
|
+
await session.wait.generationStarted();
|
|
28
|
+
await session.wait.deployable();
|
|
29
|
+
|
|
30
|
+
// Preview deployment (command + awaitable)
|
|
31
|
+
const previewWait = session.wait.previewDeployed();
|
|
32
|
+
session.deployPreview();
|
|
33
|
+
const deployed = await previewWait;
|
|
34
|
+
|
|
35
|
+
console.log('Preview URL:', deployed.previewURL);
|
|
36
|
+
|
|
37
|
+
// Workspace is always kept in sync from agent state + WS events
|
|
38
|
+
console.log(session.files.listPaths());
|
|
39
|
+
console.log(session.files.read('README.md'));
|
|
40
|
+
|
|
41
|
+
session.close();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart (Node)
|
|
45
|
+
|
|
46
|
+
Node requires a WebSocket factory (the browser `WebSocket` global is not available):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { PhasicClient } from '@cf-vibesdk/sdk';
|
|
50
|
+
import { createNodeWebSocketFactory } from '@cf-vibesdk/sdk/node';
|
|
51
|
+
|
|
52
|
+
const client = new PhasicClient({
|
|
53
|
+
baseUrl: 'http://localhost:5173',
|
|
54
|
+
apiKey: process.env.VIBESDK_API_KEY!,
|
|
55
|
+
webSocketFactory: createNodeWebSocketFactory(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const session = await client.build('Build a simple hello world page.', {
|
|
59
|
+
projectType: 'app',
|
|
60
|
+
autoGenerate: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await session.wait.generationStarted();
|
|
64
|
+
await session.wait.deployable();
|
|
65
|
+
session.close();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Authentication
|
|
69
|
+
|
|
70
|
+
Use either:
|
|
71
|
+
|
|
72
|
+
- `apiKey`: a VibeSDK API key (recommended for CLIs and automation)
|
|
73
|
+
- `token`: an already-minted JWT access token
|
|
74
|
+
|
|
75
|
+
When `apiKey` is provided, the SDK exchanges it for a short-lived access token and caches it.
|
|
76
|
+
|
|
77
|
+
## Workspace (no platform file APIs)
|
|
78
|
+
|
|
79
|
+
The SDK reconstructs and maintains a local view of the codebase using:
|
|
80
|
+
|
|
81
|
+
- `agent_connected.state.generatedFilesMap`
|
|
82
|
+
- `cf_agent_state.state.generatedFilesMap`
|
|
83
|
+
- incremental `file_*` messages
|
|
84
|
+
|
|
85
|
+
APIs:
|
|
86
|
+
|
|
87
|
+
- `session.files.listPaths()`
|
|
88
|
+
- `session.files.read(path)`
|
|
89
|
+
- `session.files.snapshot()`
|
|
90
|
+
- `session.files.tree()`
|
|
91
|
+
|
|
92
|
+
## Waiting primitives
|
|
93
|
+
|
|
94
|
+
Use high-level waits instead of depending on agent-internal message ordering:
|
|
95
|
+
|
|
96
|
+
- `session.wait.generationStarted()`
|
|
97
|
+
- `session.wait.generationComplete()`
|
|
98
|
+
- `session.wait.deployable()` (phasic resolves on `phase_validated`)
|
|
99
|
+
- `session.wait.previewDeployed()`
|
|
100
|
+
- `session.wait.cloudflareDeployed()`
|
|
101
|
+
|
|
102
|
+
All waits default to a long timeout (10 minutes). You can override per call:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
await session.wait.generationComplete({ timeoutMs: 2 * 60_000 });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Reliable WebSocket connections
|
|
109
|
+
|
|
110
|
+
Connections automatically reconnect with exponential backoff + jitter.
|
|
111
|
+
|
|
112
|
+
Events:
|
|
113
|
+
|
|
114
|
+
- `session.on('ws:reconnecting', ({ attempt, delayMs, reason }) => { ... })`
|
|
115
|
+
|
|
116
|
+
To disable reconnect:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
session.connect({ retry: { enabled: false } });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Low-level access
|
|
123
|
+
|
|
124
|
+
For advanced clients, you can subscribe to the raw typed WS stream:
|
|
125
|
+
|
|
126
|
+
- `session.on('ws:message', (msg) => { ... })`
|
|
127
|
+
|
|
128
|
+
The SDK also exposes `ws:raw` when the platform sends malformed/untyped payloads.
|
|
129
|
+
|
|
130
|
+
## Tests
|
|
131
|
+
|
|
132
|
+
From `sdk/`:
|
|
133
|
+
|
|
134
|
+
- Unit: `bun run test`
|
|
135
|
+
- Integration (requires local platform + API key): `bun run test:integration`
|
|
136
|
+
|
|
137
|
+
Integration expects:
|
|
138
|
+
|
|
139
|
+
- `VIBESDK_INTEGRATION_API_KEY`
|
|
140
|
+
- optional `VIBESDK_INTEGRATION_BASE_URL` (default `http://localhost:5173`)
|
package/dist/index.d.ts
CHANGED
|
@@ -398,7 +398,7 @@ declare const TemplateDetailsSchema: z.ZodObject<{
|
|
|
398
398
|
renderMode?: "sandbox" | "browser" | undefined;
|
|
399
399
|
slideDirectory?: string | undefined;
|
|
400
400
|
}>;
|
|
401
|
-
|
|
401
|
+
type TemplateDetails = z.infer<typeof TemplateDetailsSchema>;
|
|
402
402
|
declare const PreviewSchema: z.ZodObject<{
|
|
403
403
|
runId: z.ZodOptional<z.ZodString>;
|
|
404
404
|
previewURL: z.ZodOptional<z.ZodString>;
|
|
@@ -676,6 +676,14 @@ declare const StaticAnalysisResponseSchema: z.ZodObject<{
|
|
|
676
676
|
error?: string | undefined;
|
|
677
677
|
}>;
|
|
678
678
|
type StaticAnalysisResponse = z.infer<typeof StaticAnalysisResponseSchema>;
|
|
679
|
+
type ToolCall = {
|
|
680
|
+
id: string;
|
|
681
|
+
type: "function";
|
|
682
|
+
function: {
|
|
683
|
+
name: string;
|
|
684
|
+
arguments: string;
|
|
685
|
+
};
|
|
686
|
+
};
|
|
679
687
|
type MessageRole = "system" | "user" | "assistant" | "function" | "tool";
|
|
680
688
|
type TextContent = {
|
|
681
689
|
type: "text";
|
|
@@ -689,14 +697,6 @@ type ImageContent = {
|
|
|
689
697
|
};
|
|
690
698
|
};
|
|
691
699
|
type MessageContent = string | (TextContent | ImageContent)[] | null;
|
|
692
|
-
type ToolCall = {
|
|
693
|
-
id: string;
|
|
694
|
-
type: "function";
|
|
695
|
-
function: {
|
|
696
|
-
name: string;
|
|
697
|
-
arguments: string;
|
|
698
|
-
};
|
|
699
|
-
};
|
|
700
700
|
type Message = {
|
|
701
701
|
role: MessageRole;
|
|
702
702
|
content: MessageContent;
|
|
@@ -1326,6 +1326,12 @@ export interface AgentConnectionData {
|
|
|
1326
1326
|
agentId: string;
|
|
1327
1327
|
}
|
|
1328
1328
|
export type AgentPreviewResponse = PreviewType;
|
|
1329
|
+
type RetryConfig = {
|
|
1330
|
+
enabled?: boolean;
|
|
1331
|
+
initialDelayMs?: number;
|
|
1332
|
+
maxDelayMs?: number;
|
|
1333
|
+
maxRetries?: number;
|
|
1334
|
+
};
|
|
1329
1335
|
type BehaviorType$1 = BehaviorType;
|
|
1330
1336
|
type ProjectType$1 = ProjectType;
|
|
1331
1337
|
export type Credentials = NonNullable<CodeGenArgs["credentials"]>;
|
|
@@ -1427,13 +1433,24 @@ export type AgentEventMap = {
|
|
|
1427
1433
|
"ws:error": {
|
|
1428
1434
|
error: unknown;
|
|
1429
1435
|
};
|
|
1430
|
-
|
|
1436
|
+
"ws:reconnecting": {
|
|
1437
|
+
attempt: number;
|
|
1438
|
+
delayMs: number;
|
|
1439
|
+
reason: "close" | "error";
|
|
1440
|
+
};
|
|
1441
|
+
/** Server payload that isn't a well-formed typed message. */
|
|
1442
|
+
"ws:raw": {
|
|
1443
|
+
raw: unknown;
|
|
1444
|
+
};
|
|
1445
|
+
/** Raw server->client message (typed) */
|
|
1431
1446
|
"ws:message": AgentWsServerMessage;
|
|
1432
1447
|
connected: WsMessageOf<"agent_connected">;
|
|
1433
1448
|
conversation: WsMessageOf<"conversation_response" | "conversation_state">;
|
|
1434
1449
|
phase: WsMessageOf<"phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated">;
|
|
1435
1450
|
file: WsMessageOf<"file_chunk_generated" | "file_generated" | "file_generating" | "file_regenerating" | "file_regenerated">;
|
|
1451
|
+
generation: WsMessageOf<"generation_started" | "generation_complete" | "generation_stopped" | "generation_resumed">;
|
|
1436
1452
|
preview: WsMessageOf<"deployment_completed" | "deployment_started" | "deployment_failed">;
|
|
1453
|
+
cloudflare: WsMessageOf<"cloudflare_deployment_started" | "cloudflare_deployment_completed" | "cloudflare_deployment_error">;
|
|
1437
1454
|
/** User-friendly error derived from `{ type: 'error' }` message */
|
|
1438
1455
|
error: {
|
|
1439
1456
|
error: string;
|
|
@@ -1454,6 +1471,16 @@ export type AgentConnectionOptions = {
|
|
|
1454
1471
|
headers?: Record<string, string>;
|
|
1455
1472
|
/** Optional WebSocket factory for Node/Bun runtimes. */
|
|
1456
1473
|
webSocketFactory?: (url: string, protocols?: string | string[], headers?: Record<string, string>) => WebSocketLike;
|
|
1474
|
+
/**
|
|
1475
|
+
* Auto-reconnect config (enabled by default).
|
|
1476
|
+
* Set `{ enabled: false }` to disable.
|
|
1477
|
+
*/
|
|
1478
|
+
retry?: {
|
|
1479
|
+
enabled?: boolean;
|
|
1480
|
+
initialDelayMs?: number;
|
|
1481
|
+
maxDelayMs?: number;
|
|
1482
|
+
maxRetries?: number;
|
|
1483
|
+
};
|
|
1457
1484
|
};
|
|
1458
1485
|
export type AgentConnection = {
|
|
1459
1486
|
send: (msg: AgentWsClientMessage) => void;
|
|
@@ -1462,6 +1489,34 @@ export type AgentConnection = {
|
|
|
1462
1489
|
onAny: (cb: (event: keyof AgentEventMap, payload: AgentEventMap[keyof AgentEventMap]) => void) => () => void;
|
|
1463
1490
|
waitFor: <K extends keyof AgentEventMap>(event: K, predicate?: (payload: AgentEventMap[K]) => boolean, timeoutMs?: number) => Promise<AgentEventMap[K]>;
|
|
1464
1491
|
};
|
|
1492
|
+
type FileTreeNode$1 = {
|
|
1493
|
+
type: "dir";
|
|
1494
|
+
name: string;
|
|
1495
|
+
path: string;
|
|
1496
|
+
children: FileTreeNode$1[];
|
|
1497
|
+
} | {
|
|
1498
|
+
type: "file";
|
|
1499
|
+
name: string;
|
|
1500
|
+
path: string;
|
|
1501
|
+
};
|
|
1502
|
+
export type SessionFiles = {
|
|
1503
|
+
listPaths: () => string[];
|
|
1504
|
+
read: (path: string) => string | null;
|
|
1505
|
+
snapshot: () => Record<string, string>;
|
|
1506
|
+
tree: () => FileTreeNode$1[];
|
|
1507
|
+
};
|
|
1508
|
+
export type WaitOptions = {
|
|
1509
|
+
timeoutMs?: number;
|
|
1510
|
+
};
|
|
1511
|
+
export type PhaseEventType = "phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated";
|
|
1512
|
+
export type WaitForPhaseOptions = WaitOptions & {
|
|
1513
|
+
type: PhaseEventType;
|
|
1514
|
+
};
|
|
1515
|
+
export type SessionDeployable = {
|
|
1516
|
+
files: number;
|
|
1517
|
+
reason: "generation_complete" | "phase_validated";
|
|
1518
|
+
previewUrl?: string;
|
|
1519
|
+
};
|
|
1465
1520
|
export type VibeClientOptions = {
|
|
1466
1521
|
baseUrl: string;
|
|
1467
1522
|
/**
|
|
@@ -1476,9 +1531,106 @@ export type VibeClientOptions = {
|
|
|
1476
1531
|
/** Optional WebSocket factory for Node/Bun runtimes. */
|
|
1477
1532
|
webSocketFactory?: AgentConnectionOptions["webSocketFactory"];
|
|
1478
1533
|
fetchFn?: typeof fetch;
|
|
1534
|
+
/** HTTP retry configuration for transient failures (5xx, network errors). */
|
|
1535
|
+
retry?: RetryConfig;
|
|
1479
1536
|
};
|
|
1480
|
-
type
|
|
1481
|
-
|
|
1537
|
+
type GenerationState = {
|
|
1538
|
+
status: "idle";
|
|
1539
|
+
} | {
|
|
1540
|
+
status: "running";
|
|
1541
|
+
totalFiles?: number;
|
|
1542
|
+
} | {
|
|
1543
|
+
status: "stopped";
|
|
1544
|
+
instanceId?: string;
|
|
1545
|
+
} | {
|
|
1546
|
+
status: "complete";
|
|
1547
|
+
instanceId?: string;
|
|
1548
|
+
previewURL?: string;
|
|
1549
|
+
};
|
|
1550
|
+
type PhaseState$1 = {
|
|
1551
|
+
status: "idle";
|
|
1552
|
+
} | {
|
|
1553
|
+
status: "generating" | "generated" | "implementing" | "implemented" | "validating" | "validated";
|
|
1554
|
+
name?: string;
|
|
1555
|
+
description?: string;
|
|
1556
|
+
};
|
|
1557
|
+
type PreviewDeploymentState = {
|
|
1558
|
+
status: "idle";
|
|
1559
|
+
} | {
|
|
1560
|
+
status: "running";
|
|
1561
|
+
} | {
|
|
1562
|
+
status: "failed";
|
|
1563
|
+
error: string;
|
|
1564
|
+
} | {
|
|
1565
|
+
status: "complete";
|
|
1566
|
+
previewURL: string;
|
|
1567
|
+
tunnelURL: string;
|
|
1568
|
+
instanceId: string;
|
|
1569
|
+
};
|
|
1570
|
+
type CloudflareDeploymentState = {
|
|
1571
|
+
status: "idle";
|
|
1572
|
+
} | {
|
|
1573
|
+
status: "running";
|
|
1574
|
+
instanceId?: string;
|
|
1575
|
+
} | {
|
|
1576
|
+
status: "failed";
|
|
1577
|
+
error: string;
|
|
1578
|
+
instanceId?: string;
|
|
1579
|
+
} | {
|
|
1580
|
+
status: "complete";
|
|
1581
|
+
deploymentUrl: string;
|
|
1582
|
+
instanceId: string;
|
|
1583
|
+
workersUrl?: string;
|
|
1584
|
+
};
|
|
1585
|
+
type ConversationState$1 = WsMessageOf<"conversation_state">["state"];
|
|
1586
|
+
export type SessionState = {
|
|
1587
|
+
conversationState?: ConversationState$1;
|
|
1588
|
+
lastConversationResponse?: WsMessageOf<"conversation_response">;
|
|
1589
|
+
generation: GenerationState;
|
|
1590
|
+
phase: PhaseState$1;
|
|
1591
|
+
/** Best-known preview url (from agent_connected, generation_complete, deployment_completed). */
|
|
1592
|
+
previewUrl?: string;
|
|
1593
|
+
preview: PreviewDeploymentState;
|
|
1594
|
+
cloudflare: CloudflareDeploymentState;
|
|
1595
|
+
lastError?: string;
|
|
1596
|
+
};
|
|
1597
|
+
export declare class SessionStateStore {
|
|
1598
|
+
private state;
|
|
1599
|
+
private emitter;
|
|
1600
|
+
get(): SessionState;
|
|
1601
|
+
onChange(cb: (next: SessionState, prev: SessionState) => void): () => void;
|
|
1602
|
+
applyWsMessage(msg: AgentWsServerMessage): void;
|
|
1603
|
+
private setState;
|
|
1604
|
+
clear(): void;
|
|
1605
|
+
}
|
|
1606
|
+
type WorkspaceChange = {
|
|
1607
|
+
type: "reset";
|
|
1608
|
+
files: number;
|
|
1609
|
+
} | {
|
|
1610
|
+
type: "upsert";
|
|
1611
|
+
path: string;
|
|
1612
|
+
} | {
|
|
1613
|
+
type: "delete";
|
|
1614
|
+
path: string;
|
|
1615
|
+
};
|
|
1616
|
+
export declare class WorkspaceStore {
|
|
1617
|
+
private files;
|
|
1618
|
+
private emitter;
|
|
1619
|
+
paths(): string[];
|
|
1620
|
+
read(path: string): string | null;
|
|
1621
|
+
snapshot(): Record<string, string>;
|
|
1622
|
+
onChange(cb: (change: WorkspaceChange) => void): () => void;
|
|
1623
|
+
/** Apply authoritative snapshot from an AgentState. */
|
|
1624
|
+
applyStateSnapshot(state: AgentState): void;
|
|
1625
|
+
/** Apply a single file upsert from WS file events. */
|
|
1626
|
+
applyFileUpsert(file: unknown): void;
|
|
1627
|
+
applyWsMessage(msg: AgentWsServerMessage): void;
|
|
1628
|
+
clear(): void;
|
|
1629
|
+
}
|
|
1630
|
+
type WaitUntilReadyOptions = WaitOptions;
|
|
1631
|
+
type BuildSessionConnectOptions = AgentConnectionOptions & {
|
|
1632
|
+
/** If true (default), send `get_conversation_state` on socket open. */
|
|
1633
|
+
autoRequestConversationState?: boolean;
|
|
1482
1634
|
};
|
|
1483
1635
|
type BuildSessionInit = {
|
|
1484
1636
|
getAuthToken?: () => string | undefined;
|
|
@@ -1492,9 +1644,28 @@ export declare class BuildSession {
|
|
|
1492
1644
|
readonly behaviorType: BehaviorType$1 | undefined;
|
|
1493
1645
|
readonly projectType: ProjectType$1 | string | undefined;
|
|
1494
1646
|
private connection;
|
|
1647
|
+
readonly workspace: WorkspaceStore;
|
|
1648
|
+
readonly state: SessionStateStore;
|
|
1649
|
+
readonly files: SessionFiles;
|
|
1650
|
+
readonly wait: {
|
|
1651
|
+
generationStarted: (options?: WaitOptions) => Promise<{
|
|
1652
|
+
type: "generation_started";
|
|
1653
|
+
message: string;
|
|
1654
|
+
totalFiles: number;
|
|
1655
|
+
}>;
|
|
1656
|
+
generationComplete: (options?: WaitOptions) => Promise<{
|
|
1657
|
+
type: "generation_complete";
|
|
1658
|
+
instanceId?: string;
|
|
1659
|
+
previewURL?: string;
|
|
1660
|
+
}>;
|
|
1661
|
+
phase: (options: WaitForPhaseOptions) => Promise<WsMessageOf<PhaseEventType>>;
|
|
1662
|
+
deployable: (options?: WaitOptions) => Promise<SessionDeployable>;
|
|
1663
|
+
previewDeployed: (options?: WaitOptions) => Promise<DeploymentCompletedMessage>;
|
|
1664
|
+
cloudflareDeployed: (options?: WaitOptions) => Promise<CloudflareDeploymentCompletedMessage>;
|
|
1665
|
+
};
|
|
1495
1666
|
constructor(clientOptions: VibeClientOptions, start: BuildStartEvent, init?: BuildSessionInit);
|
|
1496
1667
|
isConnected(): boolean;
|
|
1497
|
-
connect(options?:
|
|
1668
|
+
connect(options?: BuildSessionConnectOptions): AgentConnection;
|
|
1498
1669
|
startGeneration(): void;
|
|
1499
1670
|
stop(): void;
|
|
1500
1671
|
followUp(message: string, options?: {
|
|
@@ -1503,6 +1674,19 @@ export declare class BuildSession {
|
|
|
1503
1674
|
requestConversationState(): void;
|
|
1504
1675
|
deployPreview(): void;
|
|
1505
1676
|
deployCloudflare(): void;
|
|
1677
|
+
resume(): void;
|
|
1678
|
+
clearConversation(): void;
|
|
1679
|
+
private getDefaultTimeoutMs;
|
|
1680
|
+
private waitForWsMessage;
|
|
1681
|
+
waitForGenerationStarted(options?: WaitOptions): Promise<WsMessageOf<"generation_started">>;
|
|
1682
|
+
waitForGenerationComplete(options?: WaitOptions): Promise<WsMessageOf<"generation_complete">>;
|
|
1683
|
+
waitForPhase(options: WaitForPhaseOptions): Promise<WsMessageOf<PhaseEventType>>;
|
|
1684
|
+
waitForDeployable(options?: WaitOptions): Promise<SessionDeployable>;
|
|
1685
|
+
waitForPreviewDeployed(options?: WaitOptions): Promise<WsMessageOf<"deployment_completed">>;
|
|
1686
|
+
waitForCloudflareDeployed(options?: WaitOptions): Promise<WsMessageOf<"cloudflare_deployment_completed">>;
|
|
1687
|
+
/**
|
|
1688
|
+
* Legacy alias. Prefer `session.wait.generationStarted()`.
|
|
1689
|
+
*/
|
|
1506
1690
|
waitUntilReady(options?: WaitUntilReadyOptions): Promise<void>;
|
|
1507
1691
|
on: AgentConnection["on"];
|
|
1508
1692
|
onAny: AgentConnection["onAny"];
|
|
@@ -1556,6 +1740,7 @@ export declare class PhasicClient extends VibeClient {
|
|
|
1556
1740
|
export {
|
|
1557
1741
|
BehaviorType$1 as BehaviorType,
|
|
1558
1742
|
CodeGenArgs$1 as CodeGenArgs,
|
|
1743
|
+
FileTreeNode$1 as FileTreeNode,
|
|
1559
1744
|
ProjectType$1 as ProjectType,
|
|
1560
1745
|
};
|
|
1561
1746
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// src/retry.ts
|
|
3
|
+
function normalizeRetryConfig(retry, defaults) {
|
|
4
|
+
return {
|
|
5
|
+
enabled: retry?.enabled ?? defaults.enabled,
|
|
6
|
+
initialDelayMs: retry?.initialDelayMs ?? defaults.initialDelayMs,
|
|
7
|
+
maxDelayMs: retry?.maxDelayMs ?? defaults.maxDelayMs,
|
|
8
|
+
maxRetries: retry?.maxRetries ?? defaults.maxRetries
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function computeBackoffMs(attempt, cfg) {
|
|
12
|
+
const base = Math.min(cfg.maxDelayMs, cfg.initialDelayMs * Math.pow(2, Math.max(0, attempt)));
|
|
13
|
+
const jitter = base * 0.2;
|
|
14
|
+
return Math.max(0, Math.floor(base - jitter + Math.random() * jitter * 2));
|
|
15
|
+
}
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
2
20
|
// src/http.ts
|
|
21
|
+
var HTTP_RETRY_DEFAULTS = {
|
|
22
|
+
enabled: true,
|
|
23
|
+
initialDelayMs: 1000,
|
|
24
|
+
maxDelayMs: 1e4,
|
|
25
|
+
maxRetries: 3
|
|
26
|
+
};
|
|
27
|
+
function isRetryableStatus(status) {
|
|
28
|
+
return status >= 500 && status < 600;
|
|
29
|
+
}
|
|
30
|
+
|
|
3
31
|
class HttpClient {
|
|
4
32
|
opts;
|
|
5
33
|
cachedAccessToken = null;
|
|
34
|
+
retryCfg;
|
|
6
35
|
constructor(opts) {
|
|
7
36
|
this.opts = opts;
|
|
37
|
+
this.retryCfg = normalizeRetryConfig(opts.retry, HTTP_RETRY_DEFAULTS);
|
|
8
38
|
}
|
|
9
39
|
get baseUrl() {
|
|
10
40
|
return this.opts.baseUrl.replace(/\/$/, "");
|
|
@@ -34,7 +64,10 @@ class HttpClient {
|
|
|
34
64
|
}
|
|
35
65
|
});
|
|
36
66
|
if (!resp.ok) {
|
|
37
|
-
const text = await resp.text().catch(() => "");
|
|
67
|
+
const text = (await resp.text().catch(() => "")).slice(0, 1000);
|
|
68
|
+
if (resp.status === 401) {
|
|
69
|
+
throw new Error(`HTTP 401 for /api/auth/exchange-api-key: invalid API key (regenerate in Settings \u2192 API Keys). ${text || ""}`.trim());
|
|
70
|
+
}
|
|
38
71
|
throw new Error(`HTTP ${resp.status} for /api/auth/exchange-api-key: ${text || resp.statusText}`);
|
|
39
72
|
}
|
|
40
73
|
const parsed = await resp.json();
|
|
@@ -57,21 +90,59 @@ class HttpClient {
|
|
|
57
90
|
}
|
|
58
91
|
async fetchJson(path, init) {
|
|
59
92
|
const url = `${this.baseUrl}${path}`;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
93
|
+
let lastError = null;
|
|
94
|
+
for (let attempt = 0;attempt <= this.retryCfg.maxRetries; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
const resp = await this.fetchFn(url, init);
|
|
97
|
+
if (!resp.ok) {
|
|
98
|
+
const text = (await resp.text().catch(() => "")).slice(0, 1000);
|
|
99
|
+
const error = new Error(`HTTP ${resp.status} for ${path}: ${text || resp.statusText}`);
|
|
100
|
+
if (this.retryCfg.enabled && isRetryableStatus(resp.status) && attempt < this.retryCfg.maxRetries) {
|
|
101
|
+
lastError = error;
|
|
102
|
+
await sleep(computeBackoffMs(attempt, this.retryCfg));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
return await resp.json();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof TypeError && this.retryCfg.enabled && attempt < this.retryCfg.maxRetries) {
|
|
110
|
+
lastError = error;
|
|
111
|
+
await sleep(computeBackoffMs(attempt, this.retryCfg));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
64
116
|
}
|
|
65
|
-
|
|
117
|
+
throw lastError ?? new Error(`Failed after ${this.retryCfg.maxRetries} retries`);
|
|
66
118
|
}
|
|
67
119
|
async fetchRaw(path, init) {
|
|
68
120
|
const url = `${this.baseUrl}${path}`;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
121
|
+
let lastError = null;
|
|
122
|
+
for (let attempt = 0;attempt <= this.retryCfg.maxRetries; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
const resp = await this.fetchFn(url, init);
|
|
125
|
+
if (!resp.ok) {
|
|
126
|
+
const text = (await resp.text().catch(() => "")).slice(0, 1000);
|
|
127
|
+
const error = new Error(`HTTP ${resp.status} for ${path}: ${text || resp.statusText}`);
|
|
128
|
+
if (this.retryCfg.enabled && isRetryableStatus(resp.status) && attempt < this.retryCfg.maxRetries) {
|
|
129
|
+
lastError = error;
|
|
130
|
+
await sleep(computeBackoffMs(attempt, this.retryCfg));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
return resp;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof TypeError && this.retryCfg.enabled && attempt < this.retryCfg.maxRetries) {
|
|
138
|
+
lastError = error;
|
|
139
|
+
await sleep(computeBackoffMs(attempt, this.retryCfg));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
73
144
|
}
|
|
74
|
-
|
|
145
|
+
throw lastError ?? new Error(`Failed after ${this.retryCfg.maxRetries} retries`);
|
|
75
146
|
}
|
|
76
147
|
}
|
|
77
148
|
|
|
@@ -134,6 +205,177 @@ class TypedEmitter {
|
|
|
134
205
|
for (const cb of set)
|
|
135
206
|
cb(payload);
|
|
136
207
|
}
|
|
208
|
+
clear() {
|
|
209
|
+
this.listeners.clear();
|
|
210
|
+
this.anyListeners.clear();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/state.ts
|
|
215
|
+
var INITIAL_STATE = {
|
|
216
|
+
generation: { status: "idle" },
|
|
217
|
+
phase: { status: "idle" },
|
|
218
|
+
preview: { status: "idle" },
|
|
219
|
+
cloudflare: { status: "idle" }
|
|
220
|
+
};
|
|
221
|
+
function extractPhaseInfo(msg) {
|
|
222
|
+
const phase = msg?.phase;
|
|
223
|
+
return {
|
|
224
|
+
name: phase?.name,
|
|
225
|
+
description: phase?.description
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class SessionStateStore {
|
|
230
|
+
state = INITIAL_STATE;
|
|
231
|
+
emitter = new TypedEmitter;
|
|
232
|
+
get() {
|
|
233
|
+
return this.state;
|
|
234
|
+
}
|
|
235
|
+
onChange(cb) {
|
|
236
|
+
return this.emitter.on("change", ({ prev, next }) => cb(next, prev));
|
|
237
|
+
}
|
|
238
|
+
applyWsMessage(msg) {
|
|
239
|
+
switch (msg.type) {
|
|
240
|
+
case "conversation_state": {
|
|
241
|
+
const m = msg;
|
|
242
|
+
this.setState({ conversationState: m.state });
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "conversation_response": {
|
|
246
|
+
const m = msg;
|
|
247
|
+
this.setState({ lastConversationResponse: m });
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case "generation_started": {
|
|
251
|
+
const m = msg;
|
|
252
|
+
this.setState({ generation: { status: "running", totalFiles: m.totalFiles } });
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case "generation_complete": {
|
|
256
|
+
const m = msg;
|
|
257
|
+
const previewURL = m.previewURL;
|
|
258
|
+
this.setState({
|
|
259
|
+
generation: {
|
|
260
|
+
status: "complete",
|
|
261
|
+
instanceId: m.instanceId,
|
|
262
|
+
previewURL
|
|
263
|
+
},
|
|
264
|
+
...previewURL ? { previewUrl: previewURL } : {}
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "generation_stopped": {
|
|
269
|
+
const m = msg;
|
|
270
|
+
this.setState({ generation: { status: "stopped", instanceId: m.instanceId } });
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "generation_resumed": {
|
|
274
|
+
this.setState({ generation: { status: "running" } });
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case "phase_generating": {
|
|
278
|
+
const m = msg;
|
|
279
|
+
this.setState({ phase: { status: "generating", ...extractPhaseInfo(m) } });
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "phase_generated": {
|
|
283
|
+
const m = msg;
|
|
284
|
+
this.setState({ phase: { status: "generated", ...extractPhaseInfo(m) } });
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "phase_implementing": {
|
|
288
|
+
const m = msg;
|
|
289
|
+
this.setState({ phase: { status: "implementing", ...extractPhaseInfo(m) } });
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "phase_implemented": {
|
|
293
|
+
const m = msg;
|
|
294
|
+
this.setState({ phase: { status: "implemented", ...extractPhaseInfo(m) } });
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case "phase_validating": {
|
|
298
|
+
const m = msg;
|
|
299
|
+
this.setState({ phase: { status: "validating", ...extractPhaseInfo(m) } });
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
case "phase_validated": {
|
|
303
|
+
const m = msg;
|
|
304
|
+
this.setState({ phase: { status: "validated", ...extractPhaseInfo(m) } });
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "deployment_started": {
|
|
308
|
+
this.setState({ preview: { status: "running" } });
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case "deployment_failed": {
|
|
312
|
+
const m = msg;
|
|
313
|
+
this.setState({ preview: { status: "failed", error: m.error } });
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "deployment_completed": {
|
|
317
|
+
const m = msg;
|
|
318
|
+
this.setState({
|
|
319
|
+
previewUrl: m.previewURL,
|
|
320
|
+
preview: {
|
|
321
|
+
status: "complete",
|
|
322
|
+
previewURL: m.previewURL,
|
|
323
|
+
tunnelURL: m.tunnelURL,
|
|
324
|
+
instanceId: m.instanceId
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case "cloudflare_deployment_started": {
|
|
330
|
+
const m = msg;
|
|
331
|
+
this.setState({ cloudflare: { status: "running", instanceId: m.instanceId } });
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case "cloudflare_deployment_error": {
|
|
335
|
+
const m = msg;
|
|
336
|
+
this.setState({
|
|
337
|
+
cloudflare: { status: "failed", error: m.error, instanceId: m.instanceId }
|
|
338
|
+
});
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case "cloudflare_deployment_completed": {
|
|
342
|
+
const m = msg;
|
|
343
|
+
this.setState({
|
|
344
|
+
cloudflare: {
|
|
345
|
+
status: "complete",
|
|
346
|
+
deploymentUrl: m.deploymentUrl,
|
|
347
|
+
workersUrl: m.workersUrl,
|
|
348
|
+
instanceId: m.instanceId
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "agent_connected": {
|
|
354
|
+
const m = msg;
|
|
355
|
+
const previewUrl = m.previewUrl;
|
|
356
|
+
if (previewUrl)
|
|
357
|
+
this.setState({ previewUrl });
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "error": {
|
|
361
|
+
const m = msg;
|
|
362
|
+
this.setState({ lastError: m.error });
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
default:
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
setState(patch) {
|
|
370
|
+
const prev = this.state;
|
|
371
|
+
const next = { ...prev, ...patch };
|
|
372
|
+
this.state = next;
|
|
373
|
+
this.emitter.emit("change", { prev, next });
|
|
374
|
+
}
|
|
375
|
+
clear() {
|
|
376
|
+
this.state = INITIAL_STATE;
|
|
377
|
+
this.emitter.clear();
|
|
378
|
+
}
|
|
137
379
|
}
|
|
138
380
|
|
|
139
381
|
// src/ws.ts
|
|
@@ -143,39 +385,121 @@ function toWsCloseEvent(ev) {
|
|
|
143
385
|
reason: typeof ev.reason === "string" ? ev.reason : ""
|
|
144
386
|
};
|
|
145
387
|
}
|
|
388
|
+
var WS_RETRY_DEFAULTS = {
|
|
389
|
+
enabled: true,
|
|
390
|
+
initialDelayMs: 1000,
|
|
391
|
+
maxDelayMs: 30000,
|
|
392
|
+
maxRetries: Infinity
|
|
393
|
+
};
|
|
146
394
|
function createAgentConnection(url, options = {}) {
|
|
147
395
|
const emitter = new TypedEmitter;
|
|
396
|
+
const retryCfg = normalizeRetryConfig(options.retry, WS_RETRY_DEFAULTS);
|
|
148
397
|
const headers = { ...options.headers ?? {} };
|
|
149
398
|
if (options.origin)
|
|
150
399
|
headers.Origin = options.origin;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
400
|
+
let ws = null;
|
|
401
|
+
let isOpen = false;
|
|
402
|
+
let closedByUser = false;
|
|
403
|
+
let reconnectAttempts = 0;
|
|
404
|
+
let reconnectTimer = null;
|
|
405
|
+
const pendingSends = [];
|
|
406
|
+
const maxPendingSends = 1000;
|
|
407
|
+
function clearReconnectTimer() {
|
|
408
|
+
if (!reconnectTimer)
|
|
409
|
+
return;
|
|
410
|
+
clearTimeout(reconnectTimer);
|
|
411
|
+
reconnectTimer = null;
|
|
412
|
+
}
|
|
413
|
+
function makeWebSocket() {
|
|
414
|
+
if (options.webSocketFactory)
|
|
415
|
+
return options.webSocketFactory(url, undefined, headers);
|
|
416
|
+
return new WebSocket(url);
|
|
417
|
+
}
|
|
418
|
+
function flushPendingSends() {
|
|
419
|
+
if (!ws || !isOpen)
|
|
420
|
+
return;
|
|
421
|
+
for (const data of pendingSends)
|
|
422
|
+
ws.send(data);
|
|
423
|
+
pendingSends.length = 0;
|
|
424
|
+
}
|
|
425
|
+
function scheduleReconnect(reason) {
|
|
426
|
+
if (closedByUser)
|
|
427
|
+
return;
|
|
428
|
+
if (!retryCfg.enabled)
|
|
429
|
+
return;
|
|
430
|
+
if (reconnectAttempts >= retryCfg.maxRetries)
|
|
431
|
+
return;
|
|
432
|
+
if (reconnectTimer)
|
|
433
|
+
return;
|
|
434
|
+
const delayMs = computeBackoffMs(reconnectAttempts, retryCfg);
|
|
435
|
+
emitter.emit("ws:reconnecting", {
|
|
436
|
+
attempt: reconnectAttempts + 1,
|
|
437
|
+
delayMs,
|
|
438
|
+
reason
|
|
172
439
|
});
|
|
173
|
-
|
|
174
|
-
|
|
440
|
+
reconnectAttempts += 1;
|
|
441
|
+
reconnectTimer = setTimeout(() => {
|
|
442
|
+
reconnectTimer = null;
|
|
443
|
+
connectNow();
|
|
444
|
+
}, delayMs);
|
|
445
|
+
}
|
|
446
|
+
function onOpen() {
|
|
447
|
+
isOpen = true;
|
|
448
|
+
reconnectAttempts = 0;
|
|
449
|
+
emitter.emit("ws:open", undefined);
|
|
450
|
+
flushPendingSends();
|
|
451
|
+
}
|
|
452
|
+
function onClose(ev) {
|
|
453
|
+
isOpen = false;
|
|
454
|
+
emitter.emit("ws:close", toWsCloseEvent(ev));
|
|
455
|
+
scheduleReconnect("close");
|
|
456
|
+
}
|
|
457
|
+
function onError(error) {
|
|
458
|
+
emitter.emit("ws:error", { error });
|
|
459
|
+
scheduleReconnect("error");
|
|
460
|
+
}
|
|
461
|
+
function looksLikeAgentState(obj) {
|
|
462
|
+
if (!obj || typeof obj !== "object")
|
|
463
|
+
return false;
|
|
464
|
+
const behaviorType = obj.behaviorType;
|
|
465
|
+
const projectType = obj.projectType;
|
|
466
|
+
return typeof behaviorType === "string" && typeof projectType === "string";
|
|
467
|
+
}
|
|
468
|
+
function normalizeServerPayload(raw) {
|
|
469
|
+
if (!raw || typeof raw !== "object")
|
|
470
|
+
return null;
|
|
471
|
+
const t = raw.type;
|
|
472
|
+
if (typeof t === "string") {
|
|
473
|
+
const trimmed = t.trim();
|
|
474
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
475
|
+
try {
|
|
476
|
+
const inner = JSON.parse(trimmed);
|
|
477
|
+
const normalizedInner = normalizeServerPayload(inner);
|
|
478
|
+
if (normalizedInner)
|
|
479
|
+
return normalizedInner;
|
|
480
|
+
emitter.emit("ws:raw", { raw: inner });
|
|
481
|
+
return null;
|
|
482
|
+
} catch {}
|
|
483
|
+
}
|
|
484
|
+
return raw;
|
|
485
|
+
}
|
|
486
|
+
const state = raw.state;
|
|
487
|
+
if (looksLikeAgentState(state)) {
|
|
488
|
+
return { type: "cf_agent_state", state };
|
|
489
|
+
}
|
|
490
|
+
if (looksLikeAgentState(raw)) {
|
|
491
|
+
return { type: "cf_agent_state", state: raw };
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
175
494
|
}
|
|
176
|
-
function
|
|
495
|
+
function onMessage(data) {
|
|
177
496
|
try {
|
|
178
|
-
const
|
|
497
|
+
const raw = JSON.parse(String(data));
|
|
498
|
+
const parsed = normalizeServerPayload(raw);
|
|
499
|
+
if (!parsed) {
|
|
500
|
+
emitter.emit("ws:raw", { raw });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
179
503
|
emitter.emit("ws:message", parsed);
|
|
180
504
|
switch (parsed.type) {
|
|
181
505
|
case "agent_connected":
|
|
@@ -200,11 +524,22 @@ function createAgentConnection(url, options = {}) {
|
|
|
200
524
|
case "file_regenerated":
|
|
201
525
|
emitter.emit("file", parsed);
|
|
202
526
|
break;
|
|
527
|
+
case "generation_started":
|
|
528
|
+
case "generation_complete":
|
|
529
|
+
case "generation_stopped":
|
|
530
|
+
case "generation_resumed":
|
|
531
|
+
emitter.emit("generation", parsed);
|
|
532
|
+
break;
|
|
203
533
|
case "deployment_completed":
|
|
204
534
|
case "deployment_started":
|
|
205
535
|
case "deployment_failed":
|
|
206
536
|
emitter.emit("preview", parsed);
|
|
207
537
|
break;
|
|
538
|
+
case "cloudflare_deployment_started":
|
|
539
|
+
case "cloudflare_deployment_completed":
|
|
540
|
+
case "cloudflare_deployment_error":
|
|
541
|
+
emitter.emit("cloudflare", parsed);
|
|
542
|
+
break;
|
|
208
543
|
case "error":
|
|
209
544
|
emitter.emit("error", { error: String(parsed.error ?? "Unknown error") });
|
|
210
545
|
break;
|
|
@@ -212,14 +547,56 @@ function createAgentConnection(url, options = {}) {
|
|
|
212
547
|
break;
|
|
213
548
|
}
|
|
214
549
|
} catch (error) {
|
|
215
|
-
|
|
550
|
+
onError(error);
|
|
216
551
|
}
|
|
217
552
|
}
|
|
553
|
+
function connectNow() {
|
|
554
|
+
if (closedByUser)
|
|
555
|
+
return;
|
|
556
|
+
clearReconnectTimer();
|
|
557
|
+
try {
|
|
558
|
+
ws = makeWebSocket();
|
|
559
|
+
} catch (error) {
|
|
560
|
+
onError(error);
|
|
561
|
+
scheduleReconnect("error");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (ws.addEventListener) {
|
|
565
|
+
ws.addEventListener("open", () => onOpen());
|
|
566
|
+
ws.addEventListener("close", (ev) => onClose(ev));
|
|
567
|
+
ws.addEventListener("error", (ev) => onError(ev));
|
|
568
|
+
ws.addEventListener("message", (ev) => onMessage(ev.data));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (ws.on) {
|
|
572
|
+
ws.on("open", () => onOpen());
|
|
573
|
+
ws.on("close", (code, reason) => onClose({ code: typeof code === "number" ? code : undefined, reason: typeof reason === "string" ? reason : undefined }));
|
|
574
|
+
ws.on("error", (error) => onError(error));
|
|
575
|
+
ws.on("message", (data) => onMessage(data));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
connectNow();
|
|
218
579
|
function send(msg) {
|
|
219
|
-
|
|
580
|
+
const data = JSON.stringify(msg);
|
|
581
|
+
if (isOpen && ws) {
|
|
582
|
+
ws.send(data);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
pendingSends.push(data);
|
|
586
|
+
if (pendingSends.length > maxPendingSends) {
|
|
587
|
+
pendingSends.shift();
|
|
588
|
+
emitter.emit("ws:error", {
|
|
589
|
+
error: new Error(`Message queue overflow: dropped oldest message (queue size: ${maxPendingSends})`)
|
|
590
|
+
});
|
|
591
|
+
}
|
|
220
592
|
}
|
|
221
593
|
function close() {
|
|
222
|
-
|
|
594
|
+
closedByUser = true;
|
|
595
|
+
isOpen = false;
|
|
596
|
+
pendingSends.length = 0;
|
|
597
|
+
clearReconnectTimer();
|
|
598
|
+
ws?.close();
|
|
599
|
+
ws = null;
|
|
223
600
|
}
|
|
224
601
|
async function waitFor(event, predicate, timeoutMs = 60000) {
|
|
225
602
|
return new Promise((resolve, reject) => {
|
|
@@ -245,7 +622,121 @@ function createAgentConnection(url, options = {}) {
|
|
|
245
622
|
};
|
|
246
623
|
}
|
|
247
624
|
|
|
625
|
+
// src/workspace.ts
|
|
626
|
+
function isRecord(value) {
|
|
627
|
+
return typeof value === "object" && value !== null;
|
|
628
|
+
}
|
|
629
|
+
function isFileOutputType(value) {
|
|
630
|
+
if (!isRecord(value))
|
|
631
|
+
return false;
|
|
632
|
+
return typeof value.filePath === "string" && typeof value.fileContents === "string";
|
|
633
|
+
}
|
|
634
|
+
function extractGeneratedFilesFromState(state) {
|
|
635
|
+
const out = [];
|
|
636
|
+
for (const file of Object.values(state.generatedFilesMap ?? {})) {
|
|
637
|
+
if (!isFileOutputType(file))
|
|
638
|
+
continue;
|
|
639
|
+
out.push({ path: file.filePath, content: file.fileContents });
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
function extractGeneratedFileFromMessageFile(file) {
|
|
644
|
+
if (!isFileOutputType(file))
|
|
645
|
+
return null;
|
|
646
|
+
return { path: file.filePath, content: file.fileContents };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
class WorkspaceStore {
|
|
650
|
+
files = new Map;
|
|
651
|
+
emitter = new TypedEmitter;
|
|
652
|
+
paths() {
|
|
653
|
+
return Array.from(this.files.keys()).sort();
|
|
654
|
+
}
|
|
655
|
+
read(path) {
|
|
656
|
+
return this.files.get(path) ?? null;
|
|
657
|
+
}
|
|
658
|
+
snapshot() {
|
|
659
|
+
const out = {};
|
|
660
|
+
for (const [path, content] of this.files.entries())
|
|
661
|
+
out[path] = content;
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
onChange(cb) {
|
|
665
|
+
return this.emitter.on("change", cb);
|
|
666
|
+
}
|
|
667
|
+
applyStateSnapshot(state) {
|
|
668
|
+
this.files.clear();
|
|
669
|
+
for (const f of extractGeneratedFilesFromState(state)) {
|
|
670
|
+
this.files.set(f.path, f.content);
|
|
671
|
+
}
|
|
672
|
+
this.emitter.emit("change", { type: "reset", files: this.files.size });
|
|
673
|
+
}
|
|
674
|
+
applyFileUpsert(file) {
|
|
675
|
+
const f = extractGeneratedFileFromMessageFile(file);
|
|
676
|
+
if (!f)
|
|
677
|
+
return;
|
|
678
|
+
this.files.set(f.path, f.content);
|
|
679
|
+
this.emitter.emit("change", { type: "upsert", path: f.path });
|
|
680
|
+
}
|
|
681
|
+
applyWsMessage(msg) {
|
|
682
|
+
switch (msg.type) {
|
|
683
|
+
case "agent_connected":
|
|
684
|
+
this.applyStateSnapshot(msg.state);
|
|
685
|
+
break;
|
|
686
|
+
case "cf_agent_state":
|
|
687
|
+
this.applyStateSnapshot(msg.state);
|
|
688
|
+
break;
|
|
689
|
+
case "file_generated":
|
|
690
|
+
this.applyFileUpsert(msg.file);
|
|
691
|
+
break;
|
|
692
|
+
case "file_regenerated":
|
|
693
|
+
this.applyFileUpsert(msg.file);
|
|
694
|
+
break;
|
|
695
|
+
default:
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
clear() {
|
|
700
|
+
this.files.clear();
|
|
701
|
+
this.emitter.clear();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
248
705
|
// src/session.ts
|
|
706
|
+
function buildFileTree(paths) {
|
|
707
|
+
const root = { name: "", path: "", dirs: new Map, files: [] };
|
|
708
|
+
for (const p of paths) {
|
|
709
|
+
const parts = p.split("/").filter(Boolean);
|
|
710
|
+
let curr = root;
|
|
711
|
+
for (let i = 0;i < parts.length; i += 1) {
|
|
712
|
+
const part = parts[i];
|
|
713
|
+
const isLast = i === parts.length - 1;
|
|
714
|
+
if (isLast) {
|
|
715
|
+
curr.files.push({ type: "file", name: part, path: p });
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const nextPath = curr.path ? `${curr.path}/${part}` : part;
|
|
719
|
+
let next = curr.dirs.get(part);
|
|
720
|
+
if (!next) {
|
|
721
|
+
next = { name: part, path: nextPath, dirs: new Map, files: [] };
|
|
722
|
+
curr.dirs.set(part, next);
|
|
723
|
+
}
|
|
724
|
+
curr = next;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function toNodes(dir) {
|
|
728
|
+
const dirs = Array.from(dir.dirs.values()).sort((a, b) => a.name.localeCompare(b.name)).map((d) => ({
|
|
729
|
+
type: "dir",
|
|
730
|
+
name: d.name,
|
|
731
|
+
path: d.path,
|
|
732
|
+
children: toNodes(d)
|
|
733
|
+
}));
|
|
734
|
+
const files = dir.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
735
|
+
return [...dirs, ...files];
|
|
736
|
+
}
|
|
737
|
+
return toNodes(root);
|
|
738
|
+
}
|
|
739
|
+
|
|
249
740
|
class BuildSession {
|
|
250
741
|
clientOptions;
|
|
251
742
|
init;
|
|
@@ -254,6 +745,22 @@ class BuildSession {
|
|
|
254
745
|
behaviorType;
|
|
255
746
|
projectType;
|
|
256
747
|
connection = null;
|
|
748
|
+
workspace = new WorkspaceStore;
|
|
749
|
+
state = new SessionStateStore;
|
|
750
|
+
files = {
|
|
751
|
+
listPaths: () => this.workspace.paths(),
|
|
752
|
+
read: (path) => this.workspace.read(path),
|
|
753
|
+
snapshot: () => this.workspace.snapshot(),
|
|
754
|
+
tree: () => buildFileTree(this.workspace.paths())
|
|
755
|
+
};
|
|
756
|
+
wait = {
|
|
757
|
+
generationStarted: (options = {}) => this.waitForGenerationStarted(options),
|
|
758
|
+
generationComplete: (options = {}) => this.waitForGenerationComplete(options),
|
|
759
|
+
phase: (options) => this.waitForPhase(options),
|
|
760
|
+
deployable: (options = {}) => this.waitForDeployable(options),
|
|
761
|
+
previewDeployed: (options = {}) => this.waitForPreviewDeployed(options),
|
|
762
|
+
cloudflareDeployed: (options = {}) => this.waitForCloudflareDeployed(options)
|
|
763
|
+
};
|
|
257
764
|
constructor(clientOptions, start, init = {}) {
|
|
258
765
|
this.clientOptions = clientOptions;
|
|
259
766
|
this.init = init;
|
|
@@ -268,29 +775,38 @@ class BuildSession {
|
|
|
268
775
|
connect(options = {}) {
|
|
269
776
|
if (this.connection)
|
|
270
777
|
return this.connection;
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
778
|
+
const { autoRequestConversationState, ...agentOptions } = options;
|
|
779
|
+
const origin = agentOptions.origin ?? this.clientOptions.websocketOrigin;
|
|
780
|
+
const webSocketFactory = agentOptions.webSocketFactory ?? this.clientOptions.webSocketFactory;
|
|
781
|
+
const headers = { ...agentOptions.headers ?? {} };
|
|
274
782
|
const token = this.init.getAuthToken?.();
|
|
275
783
|
if (token && !headers.Authorization) {
|
|
276
784
|
headers.Authorization = `Bearer ${token}`;
|
|
277
785
|
}
|
|
278
786
|
const connectOptions = {
|
|
279
|
-
...
|
|
787
|
+
...agentOptions,
|
|
280
788
|
...origin ? { origin } : {},
|
|
281
789
|
...Object.keys(headers).length ? { headers } : {},
|
|
282
790
|
...webSocketFactory ? { webSocketFactory } : {}
|
|
283
791
|
};
|
|
284
792
|
this.connection = createAgentConnection(this.websocketUrl, connectOptions);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this.
|
|
793
|
+
this.connection.on("ws:message", (m) => {
|
|
794
|
+
this.workspace.applyWsMessage(m);
|
|
795
|
+
this.state.applyWsMessage(m);
|
|
796
|
+
});
|
|
797
|
+
const credentials = agentOptions.credentials ?? this.init.defaultCredentials;
|
|
798
|
+
const shouldRequestConversationState = autoRequestConversationState ?? true;
|
|
799
|
+
this.connection.on("ws:open", () => {
|
|
800
|
+
if (credentials) {
|
|
288
801
|
this.connection?.send({
|
|
289
802
|
type: "session_init",
|
|
290
803
|
credentials
|
|
291
804
|
});
|
|
292
|
-
}
|
|
293
|
-
|
|
805
|
+
}
|
|
806
|
+
if (shouldRequestConversationState) {
|
|
807
|
+
this.connection?.send({ type: "get_conversation_state" });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
294
810
|
return this.connection;
|
|
295
811
|
}
|
|
296
812
|
startGeneration() {
|
|
@@ -321,15 +837,65 @@ class BuildSession {
|
|
|
321
837
|
this.assertConnected();
|
|
322
838
|
this.connection.send({ type: "deploy" });
|
|
323
839
|
}
|
|
324
|
-
|
|
840
|
+
resume() {
|
|
325
841
|
this.assertConnected();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
842
|
+
this.connection.send({ type: "resume_generation" });
|
|
843
|
+
}
|
|
844
|
+
clearConversation() {
|
|
845
|
+
this.assertConnected();
|
|
846
|
+
this.connection.send({ type: "clear_conversation" });
|
|
847
|
+
}
|
|
848
|
+
getDefaultTimeoutMs() {
|
|
849
|
+
return 10 * 60000;
|
|
850
|
+
}
|
|
851
|
+
async waitForWsMessage(predicate, timeoutMs) {
|
|
852
|
+
this.assertConnected();
|
|
853
|
+
return await this.connection.waitFor("ws:message", predicate, timeoutMs);
|
|
854
|
+
}
|
|
855
|
+
async waitForGenerationStarted(options = {}) {
|
|
856
|
+
return await this.waitForMessageType("generation_started", options.timeoutMs ?? this.getDefaultTimeoutMs());
|
|
857
|
+
}
|
|
858
|
+
async waitForGenerationComplete(options = {}) {
|
|
859
|
+
return await this.waitForMessageType("generation_complete", options.timeoutMs ?? this.getDefaultTimeoutMs());
|
|
860
|
+
}
|
|
861
|
+
async waitForPhase(options) {
|
|
862
|
+
return await this.waitForMessageType(options.type, options.timeoutMs ?? this.getDefaultTimeoutMs());
|
|
863
|
+
}
|
|
864
|
+
async waitForDeployable(options = {}) {
|
|
865
|
+
const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
|
|
866
|
+
if (this.behaviorType === "phasic") {
|
|
867
|
+
await this.waitForPhase({ type: "phase_validated", timeoutMs });
|
|
868
|
+
return {
|
|
869
|
+
files: this.workspace.paths().length,
|
|
870
|
+
reason: "phase_validated",
|
|
871
|
+
previewUrl: this.state.get().previewUrl
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
await this.waitForGenerationComplete({ timeoutMs });
|
|
875
|
+
return {
|
|
876
|
+
files: this.workspace.paths().length,
|
|
877
|
+
reason: "generation_complete",
|
|
878
|
+
previewUrl: this.state.get().previewUrl
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
async waitForPreviewDeployed(options = {}) {
|
|
882
|
+
const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
|
|
883
|
+
const msg = await this.waitForWsMessage((m) => m.type === "deployment_completed" || m.type === "deployment_failed", timeoutMs);
|
|
884
|
+
if (msg.type === "deployment_failed") {
|
|
885
|
+
throw new Error(msg.error);
|
|
886
|
+
}
|
|
887
|
+
return msg;
|
|
888
|
+
}
|
|
889
|
+
async waitForCloudflareDeployed(options = {}) {
|
|
890
|
+
const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
|
|
891
|
+
const msg = await this.waitForWsMessage((m) => m.type === "cloudflare_deployment_completed" || m.type === "cloudflare_deployment_error", timeoutMs);
|
|
892
|
+
if (msg.type === "cloudflare_deployment_error") {
|
|
893
|
+
throw new Error(msg.error);
|
|
331
894
|
}
|
|
332
|
-
|
|
895
|
+
return msg;
|
|
896
|
+
}
|
|
897
|
+
async waitUntilReady(options = {}) {
|
|
898
|
+
await this.waitForGenerationStarted(options);
|
|
333
899
|
}
|
|
334
900
|
on = (event, cb) => {
|
|
335
901
|
this.assertConnected();
|
|
@@ -348,11 +914,13 @@ class BuildSession {
|
|
|
348
914
|
}
|
|
349
915
|
async waitForMessageType(type, timeoutMs) {
|
|
350
916
|
this.assertConnected();
|
|
351
|
-
return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs);
|
|
917
|
+
return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs ?? this.getDefaultTimeoutMs());
|
|
352
918
|
}
|
|
353
919
|
close() {
|
|
354
920
|
this.connection?.close();
|
|
355
921
|
this.connection = null;
|
|
922
|
+
this.workspace.clear();
|
|
923
|
+
this.state.clear();
|
|
356
924
|
}
|
|
357
925
|
assertConnected() {
|
|
358
926
|
if (!this.connection) {
|
|
@@ -494,7 +1062,9 @@ class PhasicClient extends VibeClient {
|
|
|
494
1062
|
}
|
|
495
1063
|
}
|
|
496
1064
|
export {
|
|
1065
|
+
WorkspaceStore,
|
|
497
1066
|
VibeClient,
|
|
1067
|
+
SessionStateStore,
|
|
498
1068
|
PhasicClient,
|
|
499
1069
|
BuildSession,
|
|
500
1070
|
AgenticClient
|
package/dist/node.d.ts
CHANGED
|
@@ -61,6 +61,16 @@ type AgentConnectionOptions = {
|
|
|
61
61
|
headers?: Record<string, string>;
|
|
62
62
|
/** Optional WebSocket factory for Node/Bun runtimes. */
|
|
63
63
|
webSocketFactory?: (url: string, protocols?: string | string[], headers?: Record<string, string>) => WebSocketLike;
|
|
64
|
+
/**
|
|
65
|
+
* Auto-reconnect config (enabled by default).
|
|
66
|
+
* Set `{ enabled: false }` to disable.
|
|
67
|
+
*/
|
|
68
|
+
retry?: {
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
initialDelayMs?: number;
|
|
71
|
+
maxDelayMs?: number;
|
|
72
|
+
maxRetries?: number;
|
|
73
|
+
};
|
|
64
74
|
};
|
|
65
75
|
export declare function createNodeWebSocketFactory(): NonNullable<AgentConnectionOptions["webSocketFactory"]>;
|
|
66
76
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cf-vibesdk/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"build": "rm -rf ./dist && bun build ./src/index.ts ./src/node.ts --outdir ./dist --target bun",
|
|
23
23
|
"bundle-types": "dts-bundle-generator --export-referenced-types false --project ./tsconfig.protocol.json -o ./dist/index.d.ts ./src/index.ts && dts-bundle-generator --export-referenced-types false --project ./tsconfig.protocol.json -o ./dist/node.d.ts ./src/node.ts",
|
|
24
24
|
"typecheck": "tsc -p ./tsconfig.json --noEmit",
|
|
25
|
-
"test": "bun test",
|
|
26
|
-
"
|
|
25
|
+
"test": "bun test test/*.test.ts",
|
|
26
|
+
"test:integration": "bun test --timeout 600000 test/integration/*.test.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"ws": "^8.18.3"
|
|
@@ -35,3 +35,4 @@
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
|