@cf-vibesdk/sdk 0.0.1 → 0.0.2
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 +188 -13
- package/dist/index.js +537 -45
- 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
|
+
Type-safe 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;
|
|
@@ -1427,13 +1427,24 @@ export type AgentEventMap = {
|
|
|
1427
1427
|
"ws:error": {
|
|
1428
1428
|
error: unknown;
|
|
1429
1429
|
};
|
|
1430
|
-
|
|
1430
|
+
"ws:reconnecting": {
|
|
1431
|
+
attempt: number;
|
|
1432
|
+
delayMs: number;
|
|
1433
|
+
reason: "close" | "error";
|
|
1434
|
+
};
|
|
1435
|
+
/** Server payload that isn't a well-formed typed message. */
|
|
1436
|
+
"ws:raw": {
|
|
1437
|
+
raw: unknown;
|
|
1438
|
+
};
|
|
1439
|
+
/** Raw server->client message (typed) */
|
|
1431
1440
|
"ws:message": AgentWsServerMessage;
|
|
1432
1441
|
connected: WsMessageOf<"agent_connected">;
|
|
1433
1442
|
conversation: WsMessageOf<"conversation_response" | "conversation_state">;
|
|
1434
1443
|
phase: WsMessageOf<"phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated">;
|
|
1435
1444
|
file: WsMessageOf<"file_chunk_generated" | "file_generated" | "file_generating" | "file_regenerating" | "file_regenerated">;
|
|
1445
|
+
generation: WsMessageOf<"generation_started" | "generation_complete" | "generation_stopped" | "generation_resumed">;
|
|
1436
1446
|
preview: WsMessageOf<"deployment_completed" | "deployment_started" | "deployment_failed">;
|
|
1447
|
+
cloudflare: WsMessageOf<"cloudflare_deployment_started" | "cloudflare_deployment_completed" | "cloudflare_deployment_error">;
|
|
1437
1448
|
/** User-friendly error derived from `{ type: 'error' }` message */
|
|
1438
1449
|
error: {
|
|
1439
1450
|
error: string;
|
|
@@ -1454,6 +1465,16 @@ export type AgentConnectionOptions = {
|
|
|
1454
1465
|
headers?: Record<string, string>;
|
|
1455
1466
|
/** Optional WebSocket factory for Node/Bun runtimes. */
|
|
1456
1467
|
webSocketFactory?: (url: string, protocols?: string | string[], headers?: Record<string, string>) => WebSocketLike;
|
|
1468
|
+
/**
|
|
1469
|
+
* Auto-reconnect config (enabled by default).
|
|
1470
|
+
* Set `{ enabled: false }` to disable.
|
|
1471
|
+
*/
|
|
1472
|
+
retry?: {
|
|
1473
|
+
enabled?: boolean;
|
|
1474
|
+
initialDelayMs?: number;
|
|
1475
|
+
maxDelayMs?: number;
|
|
1476
|
+
maxRetries?: number;
|
|
1477
|
+
};
|
|
1457
1478
|
};
|
|
1458
1479
|
export type AgentConnection = {
|
|
1459
1480
|
send: (msg: AgentWsClientMessage) => void;
|
|
@@ -1462,6 +1483,34 @@ export type AgentConnection = {
|
|
|
1462
1483
|
onAny: (cb: (event: keyof AgentEventMap, payload: AgentEventMap[keyof AgentEventMap]) => void) => () => void;
|
|
1463
1484
|
waitFor: <K extends keyof AgentEventMap>(event: K, predicate?: (payload: AgentEventMap[K]) => boolean, timeoutMs?: number) => Promise<AgentEventMap[K]>;
|
|
1464
1485
|
};
|
|
1486
|
+
type FileTreeNode$1 = {
|
|
1487
|
+
type: "dir";
|
|
1488
|
+
name: string;
|
|
1489
|
+
path: string;
|
|
1490
|
+
children: FileTreeNode$1[];
|
|
1491
|
+
} | {
|
|
1492
|
+
type: "file";
|
|
1493
|
+
name: string;
|
|
1494
|
+
path: string;
|
|
1495
|
+
};
|
|
1496
|
+
export type SessionFiles = {
|
|
1497
|
+
listPaths: () => string[];
|
|
1498
|
+
read: (path: string) => string | null;
|
|
1499
|
+
snapshot: () => Record<string, string>;
|
|
1500
|
+
tree: () => FileTreeNode$1[];
|
|
1501
|
+
};
|
|
1502
|
+
export type WaitOptions = {
|
|
1503
|
+
timeoutMs?: number;
|
|
1504
|
+
};
|
|
1505
|
+
export type PhaseEventType = "phase_generating" | "phase_generated" | "phase_implementing" | "phase_implemented" | "phase_validating" | "phase_validated";
|
|
1506
|
+
export type WaitForPhaseOptions = WaitOptions & {
|
|
1507
|
+
type: PhaseEventType;
|
|
1508
|
+
};
|
|
1509
|
+
export type SessionDeployable = {
|
|
1510
|
+
files: number;
|
|
1511
|
+
reason: "generation_complete" | "phase_validated";
|
|
1512
|
+
previewUrl?: string;
|
|
1513
|
+
};
|
|
1465
1514
|
export type VibeClientOptions = {
|
|
1466
1515
|
baseUrl: string;
|
|
1467
1516
|
/**
|
|
@@ -1477,8 +1526,101 @@ export type VibeClientOptions = {
|
|
|
1477
1526
|
webSocketFactory?: AgentConnectionOptions["webSocketFactory"];
|
|
1478
1527
|
fetchFn?: typeof fetch;
|
|
1479
1528
|
};
|
|
1480
|
-
type
|
|
1481
|
-
|
|
1529
|
+
type GenerationState = {
|
|
1530
|
+
status: "idle";
|
|
1531
|
+
} | {
|
|
1532
|
+
status: "running";
|
|
1533
|
+
totalFiles?: number;
|
|
1534
|
+
} | {
|
|
1535
|
+
status: "stopped";
|
|
1536
|
+
instanceId?: string;
|
|
1537
|
+
} | {
|
|
1538
|
+
status: "complete";
|
|
1539
|
+
instanceId?: string;
|
|
1540
|
+
previewURL?: string;
|
|
1541
|
+
};
|
|
1542
|
+
type PhaseState$1 = {
|
|
1543
|
+
status: "idle";
|
|
1544
|
+
} | {
|
|
1545
|
+
status: "generating" | "generated" | "implementing" | "implemented" | "validating" | "validated";
|
|
1546
|
+
name?: string;
|
|
1547
|
+
description?: string;
|
|
1548
|
+
};
|
|
1549
|
+
type PreviewDeploymentState = {
|
|
1550
|
+
status: "idle";
|
|
1551
|
+
} | {
|
|
1552
|
+
status: "running";
|
|
1553
|
+
} | {
|
|
1554
|
+
status: "failed";
|
|
1555
|
+
error: string;
|
|
1556
|
+
} | {
|
|
1557
|
+
status: "complete";
|
|
1558
|
+
previewURL: string;
|
|
1559
|
+
tunnelURL: string;
|
|
1560
|
+
instanceId: string;
|
|
1561
|
+
};
|
|
1562
|
+
type CloudflareDeploymentState = {
|
|
1563
|
+
status: "idle";
|
|
1564
|
+
} | {
|
|
1565
|
+
status: "running";
|
|
1566
|
+
instanceId?: string;
|
|
1567
|
+
} | {
|
|
1568
|
+
status: "failed";
|
|
1569
|
+
error: string;
|
|
1570
|
+
instanceId?: string;
|
|
1571
|
+
} | {
|
|
1572
|
+
status: "complete";
|
|
1573
|
+
deploymentUrl: string;
|
|
1574
|
+
instanceId: string;
|
|
1575
|
+
workersUrl?: string;
|
|
1576
|
+
};
|
|
1577
|
+
type ConversationState$1 = WsMessageOf<"conversation_state">["state"];
|
|
1578
|
+
export type SessionState = {
|
|
1579
|
+
conversationState?: ConversationState$1;
|
|
1580
|
+
lastConversationResponse?: WsMessageOf<"conversation_response">;
|
|
1581
|
+
generation: GenerationState;
|
|
1582
|
+
phase: PhaseState$1;
|
|
1583
|
+
/** Best-known preview url (from agent_connected, generation_complete, deployment_completed). */
|
|
1584
|
+
previewUrl?: string;
|
|
1585
|
+
preview: PreviewDeploymentState;
|
|
1586
|
+
cloudflare: CloudflareDeploymentState;
|
|
1587
|
+
lastError?: string;
|
|
1588
|
+
};
|
|
1589
|
+
export declare class SessionStateStore {
|
|
1590
|
+
private state;
|
|
1591
|
+
private emitter;
|
|
1592
|
+
get(): SessionState;
|
|
1593
|
+
onChange(cb: (next: SessionState, prev: SessionState) => void): () => void;
|
|
1594
|
+
applyWsMessage(msg: AgentWsServerMessage): void;
|
|
1595
|
+
private setState;
|
|
1596
|
+
}
|
|
1597
|
+
type WorkspaceChange = {
|
|
1598
|
+
type: "reset";
|
|
1599
|
+
files: number;
|
|
1600
|
+
} | {
|
|
1601
|
+
type: "upsert";
|
|
1602
|
+
path: string;
|
|
1603
|
+
} | {
|
|
1604
|
+
type: "delete";
|
|
1605
|
+
path: string;
|
|
1606
|
+
};
|
|
1607
|
+
export declare class WorkspaceStore {
|
|
1608
|
+
private files;
|
|
1609
|
+
private emitter;
|
|
1610
|
+
paths(): string[];
|
|
1611
|
+
read(path: string): string | null;
|
|
1612
|
+
snapshot(): Record<string, string>;
|
|
1613
|
+
onChange(cb: (change: WorkspaceChange) => void): () => void;
|
|
1614
|
+
/** Apply authoritative snapshot from an AgentState. */
|
|
1615
|
+
applyStateSnapshot(state: AgentState): void;
|
|
1616
|
+
/** Apply a single file upsert from WS file events. */
|
|
1617
|
+
applyFileUpsert(file: unknown): void;
|
|
1618
|
+
applyWsMessage(msg: AgentWsServerMessage): void;
|
|
1619
|
+
}
|
|
1620
|
+
type WaitUntilReadyOptions = WaitOptions;
|
|
1621
|
+
type BuildSessionConnectOptions = AgentConnectionOptions & {
|
|
1622
|
+
/** If true (default), send `get_conversation_state` on socket open. */
|
|
1623
|
+
autoRequestConversationState?: boolean;
|
|
1482
1624
|
};
|
|
1483
1625
|
type BuildSessionInit = {
|
|
1484
1626
|
getAuthToken?: () => string | undefined;
|
|
@@ -1492,9 +1634,28 @@ export declare class BuildSession {
|
|
|
1492
1634
|
readonly behaviorType: BehaviorType$1 | undefined;
|
|
1493
1635
|
readonly projectType: ProjectType$1 | string | undefined;
|
|
1494
1636
|
private connection;
|
|
1637
|
+
readonly workspace: WorkspaceStore;
|
|
1638
|
+
readonly state: SessionStateStore;
|
|
1639
|
+
readonly files: SessionFiles;
|
|
1640
|
+
readonly wait: {
|
|
1641
|
+
generationStarted: (options?: WaitOptions) => Promise<{
|
|
1642
|
+
type: "generation_started";
|
|
1643
|
+
message: string;
|
|
1644
|
+
totalFiles: number;
|
|
1645
|
+
}>;
|
|
1646
|
+
generationComplete: (options?: WaitOptions) => Promise<{
|
|
1647
|
+
type: "generation_complete";
|
|
1648
|
+
instanceId?: string;
|
|
1649
|
+
previewURL?: string;
|
|
1650
|
+
}>;
|
|
1651
|
+
phase: (options: WaitForPhaseOptions) => Promise<WsMessageOf<PhaseEventType>>;
|
|
1652
|
+
deployable: (options?: WaitOptions) => Promise<SessionDeployable>;
|
|
1653
|
+
previewDeployed: (options?: WaitOptions) => Promise<DeploymentCompletedMessage>;
|
|
1654
|
+
cloudflareDeployed: (options?: WaitOptions) => Promise<CloudflareDeploymentCompletedMessage>;
|
|
1655
|
+
};
|
|
1495
1656
|
constructor(clientOptions: VibeClientOptions, start: BuildStartEvent, init?: BuildSessionInit);
|
|
1496
1657
|
isConnected(): boolean;
|
|
1497
|
-
connect(options?:
|
|
1658
|
+
connect(options?: BuildSessionConnectOptions): AgentConnection;
|
|
1498
1659
|
startGeneration(): void;
|
|
1499
1660
|
stop(): void;
|
|
1500
1661
|
followUp(message: string, options?: {
|
|
@@ -1503,6 +1664,19 @@ export declare class BuildSession {
|
|
|
1503
1664
|
requestConversationState(): void;
|
|
1504
1665
|
deployPreview(): void;
|
|
1505
1666
|
deployCloudflare(): void;
|
|
1667
|
+
resume(): void;
|
|
1668
|
+
clearConversation(): void;
|
|
1669
|
+
private getDefaultTimeoutMs;
|
|
1670
|
+
private waitForWsMessage;
|
|
1671
|
+
waitForGenerationStarted(options?: WaitOptions): Promise<WsMessageOf<"generation_started">>;
|
|
1672
|
+
waitForGenerationComplete(options?: WaitOptions): Promise<WsMessageOf<"generation_complete">>;
|
|
1673
|
+
waitForPhase(options: WaitForPhaseOptions): Promise<WsMessageOf<PhaseEventType>>;
|
|
1674
|
+
waitForDeployable(options?: WaitOptions): Promise<SessionDeployable>;
|
|
1675
|
+
waitForPreviewDeployed(options?: WaitOptions): Promise<WsMessageOf<"deployment_completed">>;
|
|
1676
|
+
waitForCloudflareDeployed(options?: WaitOptions): Promise<WsMessageOf<"cloudflare_deployment_completed">>;
|
|
1677
|
+
/**
|
|
1678
|
+
* Legacy alias. Prefer `session.wait.generationStarted()`.
|
|
1679
|
+
*/
|
|
1506
1680
|
waitUntilReady(options?: WaitUntilReadyOptions): Promise<void>;
|
|
1507
1681
|
on: AgentConnection["on"];
|
|
1508
1682
|
onAny: AgentConnection["onAny"];
|
|
@@ -1556,6 +1730,7 @@ export declare class PhasicClient extends VibeClient {
|
|
|
1556
1730
|
export {
|
|
1557
1731
|
BehaviorType$1 as BehaviorType,
|
|
1558
1732
|
CodeGenArgs$1 as CodeGenArgs,
|
|
1733
|
+
FileTreeNode$1 as FileTreeNode,
|
|
1559
1734
|
ProjectType$1 as ProjectType,
|
|
1560
1735
|
};
|
|
1561
1736
|
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,9 @@ class HttpClient {
|
|
|
35
35
|
});
|
|
36
36
|
if (!resp.ok) {
|
|
37
37
|
const text = await resp.text().catch(() => "");
|
|
38
|
+
if (resp.status === 401) {
|
|
39
|
+
throw new Error(`HTTP 401 for /api/auth/exchange-api-key: invalid API key (regenerate in Settings \u2192 API Keys). ${text || ""}`.trim());
|
|
40
|
+
}
|
|
38
41
|
throw new Error(`HTTP ${resp.status} for /api/auth/exchange-api-key: ${text || resp.statusText}`);
|
|
39
42
|
}
|
|
40
43
|
const parsed = await resp.json();
|
|
@@ -136,6 +139,168 @@ class TypedEmitter {
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
// src/state.ts
|
|
143
|
+
var INITIAL_STATE = {
|
|
144
|
+
generation: { status: "idle" },
|
|
145
|
+
phase: { status: "idle" },
|
|
146
|
+
preview: { status: "idle" },
|
|
147
|
+
cloudflare: { status: "idle" }
|
|
148
|
+
};
|
|
149
|
+
function extractPhaseInfo(msg) {
|
|
150
|
+
return {
|
|
151
|
+
name: msg.phase?.name,
|
|
152
|
+
description: msg.phase?.description
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class SessionStateStore {
|
|
157
|
+
state = INITIAL_STATE;
|
|
158
|
+
emitter = new TypedEmitter;
|
|
159
|
+
get() {
|
|
160
|
+
return this.state;
|
|
161
|
+
}
|
|
162
|
+
onChange(cb) {
|
|
163
|
+
return this.emitter.on("change", ({ prev, next }) => cb(next, prev));
|
|
164
|
+
}
|
|
165
|
+
applyWsMessage(msg) {
|
|
166
|
+
switch (msg.type) {
|
|
167
|
+
case "conversation_state": {
|
|
168
|
+
const m = msg;
|
|
169
|
+
this.setState({ conversationState: m.state });
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "conversation_response": {
|
|
173
|
+
const m = msg;
|
|
174
|
+
this.setState({ lastConversationResponse: m });
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "generation_started": {
|
|
178
|
+
const m = msg;
|
|
179
|
+
this.setState({ generation: { status: "running", totalFiles: m.totalFiles } });
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "generation_complete": {
|
|
183
|
+
const m = msg;
|
|
184
|
+
const previewURL = m.previewURL;
|
|
185
|
+
this.setState({
|
|
186
|
+
generation: {
|
|
187
|
+
status: "complete",
|
|
188
|
+
instanceId: m.instanceId,
|
|
189
|
+
previewURL
|
|
190
|
+
},
|
|
191
|
+
...previewURL ? { previewUrl: previewURL } : {}
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "generation_stopped": {
|
|
196
|
+
const m = msg;
|
|
197
|
+
this.setState({ generation: { status: "stopped", instanceId: m.instanceId } });
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "generation_resumed": {
|
|
201
|
+
this.setState({ generation: { status: "running" } });
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "phase_generating": {
|
|
205
|
+
const m = msg;
|
|
206
|
+
this.setState({ phase: { status: "generating", ...extractPhaseInfo(m) } });
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "phase_generated": {
|
|
210
|
+
const m = msg;
|
|
211
|
+
this.setState({ phase: { status: "generated", ...extractPhaseInfo(m) } });
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "phase_implementing": {
|
|
215
|
+
const m = msg;
|
|
216
|
+
this.setState({ phase: { status: "implementing", ...extractPhaseInfo(m) } });
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "phase_implemented": {
|
|
220
|
+
const m = msg;
|
|
221
|
+
this.setState({ phase: { status: "implemented", ...extractPhaseInfo(m) } });
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "phase_validating": {
|
|
225
|
+
const m = msg;
|
|
226
|
+
this.setState({ phase: { status: "validating", ...extractPhaseInfo(m) } });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "phase_validated": {
|
|
230
|
+
const m = msg;
|
|
231
|
+
this.setState({ phase: { status: "validated", ...extractPhaseInfo(m) } });
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "deployment_started": {
|
|
235
|
+
this.setState({ preview: { status: "running" } });
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "deployment_failed": {
|
|
239
|
+
const m = msg;
|
|
240
|
+
this.setState({ preview: { status: "failed", error: m.error } });
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "deployment_completed": {
|
|
244
|
+
const m = msg;
|
|
245
|
+
this.setState({
|
|
246
|
+
previewUrl: m.previewURL,
|
|
247
|
+
preview: {
|
|
248
|
+
status: "complete",
|
|
249
|
+
previewURL: m.previewURL,
|
|
250
|
+
tunnelURL: m.tunnelURL,
|
|
251
|
+
instanceId: m.instanceId
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "cloudflare_deployment_started": {
|
|
257
|
+
const m = msg;
|
|
258
|
+
this.setState({ cloudflare: { status: "running", instanceId: m.instanceId } });
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case "cloudflare_deployment_error": {
|
|
262
|
+
const m = msg;
|
|
263
|
+
this.setState({
|
|
264
|
+
cloudflare: { status: "failed", error: m.error, instanceId: m.instanceId }
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "cloudflare_deployment_completed": {
|
|
269
|
+
const m = msg;
|
|
270
|
+
this.setState({
|
|
271
|
+
cloudflare: {
|
|
272
|
+
status: "complete",
|
|
273
|
+
deploymentUrl: m.deploymentUrl,
|
|
274
|
+
workersUrl: m.workersUrl,
|
|
275
|
+
instanceId: m.instanceId
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "agent_connected": {
|
|
281
|
+
const m = msg;
|
|
282
|
+
const previewUrl = m.previewUrl;
|
|
283
|
+
if (previewUrl)
|
|
284
|
+
this.setState({ previewUrl });
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "error": {
|
|
288
|
+
const m = msg;
|
|
289
|
+
this.setState({ lastError: m.error });
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
setState(patch) {
|
|
297
|
+
const prev = this.state;
|
|
298
|
+
const next = { ...prev, ...patch };
|
|
299
|
+
this.state = next;
|
|
300
|
+
this.emitter.emit("change", { prev, next });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
139
304
|
// src/ws.ts
|
|
140
305
|
function toWsCloseEvent(ev) {
|
|
141
306
|
return {
|
|
@@ -143,39 +308,129 @@ function toWsCloseEvent(ev) {
|
|
|
143
308
|
reason: typeof ev.reason === "string" ? ev.reason : ""
|
|
144
309
|
};
|
|
145
310
|
}
|
|
311
|
+
function normalizeRetryConfig(retry) {
|
|
312
|
+
const enabled = retry?.enabled ?? true;
|
|
313
|
+
return {
|
|
314
|
+
enabled,
|
|
315
|
+
initialDelayMs: retry?.initialDelayMs ?? 1000,
|
|
316
|
+
maxDelayMs: retry?.maxDelayMs ?? 30000,
|
|
317
|
+
maxRetries: retry?.maxRetries ?? Infinity
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function computeBackoffMs(attempt, cfg) {
|
|
321
|
+
const base = Math.min(cfg.maxDelayMs, cfg.initialDelayMs * Math.pow(2, Math.max(0, attempt)));
|
|
322
|
+
const jitter = base * 0.2;
|
|
323
|
+
return Math.max(0, Math.floor(base - jitter + Math.random() * jitter * 2));
|
|
324
|
+
}
|
|
146
325
|
function createAgentConnection(url, options = {}) {
|
|
147
326
|
const emitter = new TypedEmitter;
|
|
327
|
+
const retryCfg = normalizeRetryConfig(options.retry);
|
|
148
328
|
const headers = { ...options.headers ?? {} };
|
|
149
329
|
if (options.origin)
|
|
150
330
|
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
|
-
|
|
331
|
+
let ws = null;
|
|
332
|
+
let isOpen = false;
|
|
333
|
+
let closedByUser = false;
|
|
334
|
+
let reconnectAttempts = 0;
|
|
335
|
+
let reconnectTimer = null;
|
|
336
|
+
const pendingSends = [];
|
|
337
|
+
const maxPendingSends = 1000;
|
|
338
|
+
function clearReconnectTimer() {
|
|
339
|
+
if (!reconnectTimer)
|
|
340
|
+
return;
|
|
341
|
+
clearTimeout(reconnectTimer);
|
|
342
|
+
reconnectTimer = null;
|
|
343
|
+
}
|
|
344
|
+
function makeWebSocket() {
|
|
345
|
+
if (options.webSocketFactory)
|
|
346
|
+
return options.webSocketFactory(url, undefined, headers);
|
|
347
|
+
return new WebSocket(url);
|
|
348
|
+
}
|
|
349
|
+
function flushPendingSends() {
|
|
350
|
+
if (!ws || !isOpen)
|
|
351
|
+
return;
|
|
352
|
+
for (const data of pendingSends)
|
|
353
|
+
ws.send(data);
|
|
354
|
+
pendingSends.length = 0;
|
|
355
|
+
}
|
|
356
|
+
function scheduleReconnect(reason) {
|
|
357
|
+
if (closedByUser)
|
|
358
|
+
return;
|
|
359
|
+
if (!retryCfg.enabled)
|
|
360
|
+
return;
|
|
361
|
+
if (reconnectAttempts >= retryCfg.maxRetries)
|
|
362
|
+
return;
|
|
363
|
+
if (reconnectTimer)
|
|
364
|
+
return;
|
|
365
|
+
const delayMs = computeBackoffMs(reconnectAttempts, retryCfg);
|
|
366
|
+
emitter.emit("ws:reconnecting", {
|
|
367
|
+
attempt: reconnectAttempts + 1,
|
|
368
|
+
delayMs,
|
|
369
|
+
reason
|
|
172
370
|
});
|
|
173
|
-
|
|
174
|
-
|
|
371
|
+
reconnectAttempts += 1;
|
|
372
|
+
reconnectTimer = setTimeout(() => {
|
|
373
|
+
reconnectTimer = null;
|
|
374
|
+
connectNow();
|
|
375
|
+
}, delayMs);
|
|
376
|
+
}
|
|
377
|
+
function onOpen() {
|
|
378
|
+
isOpen = true;
|
|
379
|
+
reconnectAttempts = 0;
|
|
380
|
+
emitter.emit("ws:open", undefined);
|
|
381
|
+
flushPendingSends();
|
|
382
|
+
}
|
|
383
|
+
function onClose(ev) {
|
|
384
|
+
isOpen = false;
|
|
385
|
+
emitter.emit("ws:close", toWsCloseEvent(ev));
|
|
386
|
+
scheduleReconnect("close");
|
|
387
|
+
}
|
|
388
|
+
function onError(error) {
|
|
389
|
+
emitter.emit("ws:error", { error });
|
|
390
|
+
scheduleReconnect("error");
|
|
391
|
+
}
|
|
392
|
+
function looksLikeAgentState(obj) {
|
|
393
|
+
if (!obj || typeof obj !== "object")
|
|
394
|
+
return false;
|
|
395
|
+
const behaviorType = obj.behaviorType;
|
|
396
|
+
const projectType = obj.projectType;
|
|
397
|
+
return typeof behaviorType === "string" && typeof projectType === "string";
|
|
398
|
+
}
|
|
399
|
+
function normalizeServerPayload(raw) {
|
|
400
|
+
if (!raw || typeof raw !== "object")
|
|
401
|
+
return null;
|
|
402
|
+
const t = raw.type;
|
|
403
|
+
if (typeof t === "string") {
|
|
404
|
+
const trimmed = t.trim();
|
|
405
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
406
|
+
try {
|
|
407
|
+
const inner = JSON.parse(trimmed);
|
|
408
|
+
const normalizedInner = normalizeServerPayload(inner);
|
|
409
|
+
if (normalizedInner)
|
|
410
|
+
return normalizedInner;
|
|
411
|
+
emitter.emit("ws:raw", { raw: inner });
|
|
412
|
+
return null;
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
return raw;
|
|
416
|
+
}
|
|
417
|
+
const state = raw.state;
|
|
418
|
+
if (looksLikeAgentState(state)) {
|
|
419
|
+
return { type: "cf_agent_state", state };
|
|
420
|
+
}
|
|
421
|
+
if (looksLikeAgentState(raw)) {
|
|
422
|
+
return { type: "cf_agent_state", state: raw };
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
175
425
|
}
|
|
176
|
-
function
|
|
426
|
+
function onMessage(data) {
|
|
177
427
|
try {
|
|
178
|
-
const
|
|
428
|
+
const raw = JSON.parse(String(data));
|
|
429
|
+
const parsed = normalizeServerPayload(raw);
|
|
430
|
+
if (!parsed) {
|
|
431
|
+
emitter.emit("ws:raw", { raw });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
179
434
|
emitter.emit("ws:message", parsed);
|
|
180
435
|
switch (parsed.type) {
|
|
181
436
|
case "agent_connected":
|
|
@@ -200,11 +455,22 @@ function createAgentConnection(url, options = {}) {
|
|
|
200
455
|
case "file_regenerated":
|
|
201
456
|
emitter.emit("file", parsed);
|
|
202
457
|
break;
|
|
458
|
+
case "generation_started":
|
|
459
|
+
case "generation_complete":
|
|
460
|
+
case "generation_stopped":
|
|
461
|
+
case "generation_resumed":
|
|
462
|
+
emitter.emit("generation", parsed);
|
|
463
|
+
break;
|
|
203
464
|
case "deployment_completed":
|
|
204
465
|
case "deployment_started":
|
|
205
466
|
case "deployment_failed":
|
|
206
467
|
emitter.emit("preview", parsed);
|
|
207
468
|
break;
|
|
469
|
+
case "cloudflare_deployment_started":
|
|
470
|
+
case "cloudflare_deployment_completed":
|
|
471
|
+
case "cloudflare_deployment_error":
|
|
472
|
+
emitter.emit("cloudflare", parsed);
|
|
473
|
+
break;
|
|
208
474
|
case "error":
|
|
209
475
|
emitter.emit("error", { error: String(parsed.error ?? "Unknown error") });
|
|
210
476
|
break;
|
|
@@ -212,14 +478,52 @@ function createAgentConnection(url, options = {}) {
|
|
|
212
478
|
break;
|
|
213
479
|
}
|
|
214
480
|
} catch (error) {
|
|
215
|
-
|
|
481
|
+
onError(error);
|
|
216
482
|
}
|
|
217
483
|
}
|
|
484
|
+
function connectNow() {
|
|
485
|
+
if (closedByUser)
|
|
486
|
+
return;
|
|
487
|
+
clearReconnectTimer();
|
|
488
|
+
try {
|
|
489
|
+
ws = makeWebSocket();
|
|
490
|
+
} catch (error) {
|
|
491
|
+
onError(error);
|
|
492
|
+
scheduleReconnect("error");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (ws.addEventListener) {
|
|
496
|
+
ws.addEventListener("open", () => onOpen());
|
|
497
|
+
ws.addEventListener("close", (ev) => onClose(ev));
|
|
498
|
+
ws.addEventListener("error", (ev) => onError(ev));
|
|
499
|
+
ws.addEventListener("message", (ev) => onMessage(ev.data));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (ws.on) {
|
|
503
|
+
ws.on("open", () => onOpen());
|
|
504
|
+
ws.on("close", (code, reason) => onClose({ code, reason }));
|
|
505
|
+
ws.on("error", (error) => onError(error));
|
|
506
|
+
ws.on("message", (data) => onMessage(data));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
connectNow();
|
|
218
510
|
function send(msg) {
|
|
219
|
-
|
|
511
|
+
const data = JSON.stringify(msg);
|
|
512
|
+
if (isOpen && ws) {
|
|
513
|
+
ws.send(data);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
pendingSends.push(data);
|
|
517
|
+
if (pendingSends.length > maxPendingSends)
|
|
518
|
+
pendingSends.shift();
|
|
220
519
|
}
|
|
221
520
|
function close() {
|
|
222
|
-
|
|
521
|
+
closedByUser = true;
|
|
522
|
+
isOpen = false;
|
|
523
|
+
pendingSends.length = 0;
|
|
524
|
+
clearReconnectTimer();
|
|
525
|
+
ws?.close();
|
|
526
|
+
ws = null;
|
|
223
527
|
}
|
|
224
528
|
async function waitFor(event, predicate, timeoutMs = 60000) {
|
|
225
529
|
return new Promise((resolve, reject) => {
|
|
@@ -245,7 +549,118 @@ function createAgentConnection(url, options = {}) {
|
|
|
245
549
|
};
|
|
246
550
|
}
|
|
247
551
|
|
|
552
|
+
// src/workspace.ts
|
|
553
|
+
function isRecord(value) {
|
|
554
|
+
return typeof value === "object" && value !== null;
|
|
555
|
+
}
|
|
556
|
+
function extractGeneratedFilesFromState(state) {
|
|
557
|
+
const out = [];
|
|
558
|
+
for (const file of Object.values(state.generatedFilesMap ?? {})) {
|
|
559
|
+
const path = file.filePath;
|
|
560
|
+
const content = file.fileContents;
|
|
561
|
+
if (typeof path === "string" && typeof content === "string") {
|
|
562
|
+
out.push({ path, content });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
function extractGeneratedFileFromMessageFile(file) {
|
|
568
|
+
if (!isRecord(file))
|
|
569
|
+
return null;
|
|
570
|
+
const path = file.filePath;
|
|
571
|
+
const content = file.fileContents;
|
|
572
|
+
if (typeof path !== "string" || typeof content !== "string")
|
|
573
|
+
return null;
|
|
574
|
+
return { path, content };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
class WorkspaceStore {
|
|
578
|
+
files = new Map;
|
|
579
|
+
emitter = new TypedEmitter;
|
|
580
|
+
paths() {
|
|
581
|
+
return Array.from(this.files.keys()).sort();
|
|
582
|
+
}
|
|
583
|
+
read(path) {
|
|
584
|
+
return this.files.get(path) ?? null;
|
|
585
|
+
}
|
|
586
|
+
snapshot() {
|
|
587
|
+
const out = {};
|
|
588
|
+
for (const [path, content] of this.files.entries())
|
|
589
|
+
out[path] = content;
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
onChange(cb) {
|
|
593
|
+
return this.emitter.on("change", cb);
|
|
594
|
+
}
|
|
595
|
+
applyStateSnapshot(state) {
|
|
596
|
+
this.files.clear();
|
|
597
|
+
for (const f of extractGeneratedFilesFromState(state)) {
|
|
598
|
+
this.files.set(f.path, f.content);
|
|
599
|
+
}
|
|
600
|
+
this.emitter.emit("change", { type: "reset", files: this.files.size });
|
|
601
|
+
}
|
|
602
|
+
applyFileUpsert(file) {
|
|
603
|
+
const f = extractGeneratedFileFromMessageFile(file);
|
|
604
|
+
if (!f)
|
|
605
|
+
return;
|
|
606
|
+
this.files.set(f.path, f.content);
|
|
607
|
+
this.emitter.emit("change", { type: "upsert", path: f.path });
|
|
608
|
+
}
|
|
609
|
+
applyWsMessage(msg) {
|
|
610
|
+
switch (msg.type) {
|
|
611
|
+
case "agent_connected":
|
|
612
|
+
this.applyStateSnapshot(msg.state);
|
|
613
|
+
break;
|
|
614
|
+
case "cf_agent_state":
|
|
615
|
+
this.applyStateSnapshot(msg.state);
|
|
616
|
+
break;
|
|
617
|
+
case "file_generated":
|
|
618
|
+
this.applyFileUpsert(msg.file);
|
|
619
|
+
break;
|
|
620
|
+
case "file_regenerated":
|
|
621
|
+
this.applyFileUpsert(msg.file);
|
|
622
|
+
break;
|
|
623
|
+
default:
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
248
629
|
// src/session.ts
|
|
630
|
+
function buildFileTree(paths) {
|
|
631
|
+
const root = { name: "", path: "", dirs: new Map, files: [] };
|
|
632
|
+
for (const p of paths) {
|
|
633
|
+
const parts = p.split("/").filter(Boolean);
|
|
634
|
+
let curr = root;
|
|
635
|
+
for (let i = 0;i < parts.length; i += 1) {
|
|
636
|
+
const part = parts[i];
|
|
637
|
+
const isLast = i === parts.length - 1;
|
|
638
|
+
if (isLast) {
|
|
639
|
+
curr.files.push({ type: "file", name: part, path: p });
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const nextPath = curr.path ? `${curr.path}/${part}` : part;
|
|
643
|
+
let next = curr.dirs.get(part);
|
|
644
|
+
if (!next) {
|
|
645
|
+
next = { name: part, path: nextPath, dirs: new Map, files: [] };
|
|
646
|
+
curr.dirs.set(part, next);
|
|
647
|
+
}
|
|
648
|
+
curr = next;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function toNodes(dir) {
|
|
652
|
+
const dirs = Array.from(dir.dirs.values()).sort((a, b) => a.name.localeCompare(b.name)).map((d) => ({
|
|
653
|
+
type: "dir",
|
|
654
|
+
name: d.name,
|
|
655
|
+
path: d.path,
|
|
656
|
+
children: toNodes(d)
|
|
657
|
+
}));
|
|
658
|
+
const files = dir.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
659
|
+
return [...dirs, ...files];
|
|
660
|
+
}
|
|
661
|
+
return toNodes(root);
|
|
662
|
+
}
|
|
663
|
+
|
|
249
664
|
class BuildSession {
|
|
250
665
|
clientOptions;
|
|
251
666
|
init;
|
|
@@ -254,6 +669,22 @@ class BuildSession {
|
|
|
254
669
|
behaviorType;
|
|
255
670
|
projectType;
|
|
256
671
|
connection = null;
|
|
672
|
+
workspace = new WorkspaceStore;
|
|
673
|
+
state = new SessionStateStore;
|
|
674
|
+
files = {
|
|
675
|
+
listPaths: () => this.workspace.paths(),
|
|
676
|
+
read: (path) => this.workspace.read(path),
|
|
677
|
+
snapshot: () => this.workspace.snapshot(),
|
|
678
|
+
tree: () => buildFileTree(this.workspace.paths())
|
|
679
|
+
};
|
|
680
|
+
wait = {
|
|
681
|
+
generationStarted: (options = {}) => this.waitForGenerationStarted(options),
|
|
682
|
+
generationComplete: (options = {}) => this.waitForGenerationComplete(options),
|
|
683
|
+
phase: (options) => this.waitForPhase(options),
|
|
684
|
+
deployable: (options = {}) => this.waitForDeployable(options),
|
|
685
|
+
previewDeployed: (options = {}) => this.waitForPreviewDeployed(options),
|
|
686
|
+
cloudflareDeployed: (options = {}) => this.waitForCloudflareDeployed(options)
|
|
687
|
+
};
|
|
257
688
|
constructor(clientOptions, start, init = {}) {
|
|
258
689
|
this.clientOptions = clientOptions;
|
|
259
690
|
this.init = init;
|
|
@@ -268,29 +699,38 @@ class BuildSession {
|
|
|
268
699
|
connect(options = {}) {
|
|
269
700
|
if (this.connection)
|
|
270
701
|
return this.connection;
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
702
|
+
const { autoRequestConversationState, ...agentOptions } = options;
|
|
703
|
+
const origin = agentOptions.origin ?? this.clientOptions.websocketOrigin;
|
|
704
|
+
const webSocketFactory = agentOptions.webSocketFactory ?? this.clientOptions.webSocketFactory;
|
|
705
|
+
const headers = { ...agentOptions.headers ?? {} };
|
|
274
706
|
const token = this.init.getAuthToken?.();
|
|
275
707
|
if (token && !headers.Authorization) {
|
|
276
708
|
headers.Authorization = `Bearer ${token}`;
|
|
277
709
|
}
|
|
278
710
|
const connectOptions = {
|
|
279
|
-
...
|
|
711
|
+
...agentOptions,
|
|
280
712
|
...origin ? { origin } : {},
|
|
281
713
|
...Object.keys(headers).length ? { headers } : {},
|
|
282
714
|
...webSocketFactory ? { webSocketFactory } : {}
|
|
283
715
|
};
|
|
284
716
|
this.connection = createAgentConnection(this.websocketUrl, connectOptions);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this.
|
|
717
|
+
this.connection.on("ws:message", (m) => {
|
|
718
|
+
this.workspace.applyWsMessage(m);
|
|
719
|
+
this.state.applyWsMessage(m);
|
|
720
|
+
});
|
|
721
|
+
const credentials = agentOptions.credentials ?? this.init.defaultCredentials;
|
|
722
|
+
const shouldRequestConversationState = autoRequestConversationState ?? true;
|
|
723
|
+
this.connection.on("ws:open", () => {
|
|
724
|
+
if (credentials) {
|
|
288
725
|
this.connection?.send({
|
|
289
726
|
type: "session_init",
|
|
290
727
|
credentials
|
|
291
728
|
});
|
|
292
|
-
}
|
|
293
|
-
|
|
729
|
+
}
|
|
730
|
+
if (shouldRequestConversationState) {
|
|
731
|
+
this.connection?.send({ type: "get_conversation_state" });
|
|
732
|
+
}
|
|
733
|
+
});
|
|
294
734
|
return this.connection;
|
|
295
735
|
}
|
|
296
736
|
startGeneration() {
|
|
@@ -321,15 +761,65 @@ class BuildSession {
|
|
|
321
761
|
this.assertConnected();
|
|
322
762
|
this.connection.send({ type: "deploy" });
|
|
323
763
|
}
|
|
324
|
-
|
|
764
|
+
resume() {
|
|
325
765
|
this.assertConnected();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
766
|
+
this.connection.send({ type: "resume_generation" });
|
|
767
|
+
}
|
|
768
|
+
clearConversation() {
|
|
769
|
+
this.assertConnected();
|
|
770
|
+
this.connection.send({ type: "clear_conversation" });
|
|
771
|
+
}
|
|
772
|
+
getDefaultTimeoutMs() {
|
|
773
|
+
return 10 * 60000;
|
|
774
|
+
}
|
|
775
|
+
async waitForWsMessage(predicate, timeoutMs) {
|
|
776
|
+
this.assertConnected();
|
|
777
|
+
return await this.connection.waitFor("ws:message", predicate, timeoutMs);
|
|
778
|
+
}
|
|
779
|
+
async waitForGenerationStarted(options = {}) {
|
|
780
|
+
return await this.waitForMessageType("generation_started", options.timeoutMs ?? this.getDefaultTimeoutMs());
|
|
781
|
+
}
|
|
782
|
+
async waitForGenerationComplete(options = {}) {
|
|
783
|
+
return await this.waitForMessageType("generation_complete", options.timeoutMs ?? this.getDefaultTimeoutMs());
|
|
784
|
+
}
|
|
785
|
+
async waitForPhase(options) {
|
|
786
|
+
return await this.waitForMessageType(options.type, options.timeoutMs ?? this.getDefaultTimeoutMs());
|
|
787
|
+
}
|
|
788
|
+
async waitForDeployable(options = {}) {
|
|
789
|
+
const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
|
|
790
|
+
if (this.behaviorType === "phasic") {
|
|
791
|
+
await this.waitForPhase({ type: "phase_validated", timeoutMs });
|
|
792
|
+
return {
|
|
793
|
+
files: this.workspace.paths().length,
|
|
794
|
+
reason: "phase_validated",
|
|
795
|
+
previewUrl: this.state.get().previewUrl
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
await this.waitForGenerationComplete({ timeoutMs });
|
|
799
|
+
return {
|
|
800
|
+
files: this.workspace.paths().length,
|
|
801
|
+
reason: "generation_complete",
|
|
802
|
+
previewUrl: this.state.get().previewUrl
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async waitForPreviewDeployed(options = {}) {
|
|
806
|
+
const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
|
|
807
|
+
const msg = await this.waitForWsMessage((m) => m.type === "deployment_completed" || m.type === "deployment_failed", timeoutMs);
|
|
808
|
+
if (msg.type === "deployment_failed") {
|
|
809
|
+
throw new Error(msg.error);
|
|
331
810
|
}
|
|
332
|
-
|
|
811
|
+
return msg;
|
|
812
|
+
}
|
|
813
|
+
async waitForCloudflareDeployed(options = {}) {
|
|
814
|
+
const timeoutMs = options.timeoutMs ?? this.getDefaultTimeoutMs();
|
|
815
|
+
const msg = await this.waitForWsMessage((m) => m.type === "cloudflare_deployment_completed" || m.type === "cloudflare_deployment_error", timeoutMs);
|
|
816
|
+
if (msg.type === "cloudflare_deployment_error") {
|
|
817
|
+
throw new Error(msg.error);
|
|
818
|
+
}
|
|
819
|
+
return msg;
|
|
820
|
+
}
|
|
821
|
+
async waitUntilReady(options = {}) {
|
|
822
|
+
await this.waitForGenerationStarted(options);
|
|
333
823
|
}
|
|
334
824
|
on = (event, cb) => {
|
|
335
825
|
this.assertConnected();
|
|
@@ -348,7 +838,7 @@ class BuildSession {
|
|
|
348
838
|
}
|
|
349
839
|
async waitForMessageType(type, timeoutMs) {
|
|
350
840
|
this.assertConnected();
|
|
351
|
-
return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs);
|
|
841
|
+
return await this.connection.waitFor("ws:message", (msg) => msg.type === type, timeoutMs ?? this.getDefaultTimeoutMs());
|
|
352
842
|
}
|
|
353
843
|
close() {
|
|
354
844
|
this.connection?.close();
|
|
@@ -494,7 +984,9 @@ class PhasicClient extends VibeClient {
|
|
|
494
984
|
}
|
|
495
985
|
}
|
|
496
986
|
export {
|
|
987
|
+
WorkspaceStore,
|
|
497
988
|
VibeClient,
|
|
989
|
+
SessionStateStore,
|
|
498
990
|
PhasicClient,
|
|
499
991
|
BuildSession,
|
|
500
992
|
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.2",
|
|
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
|
+
|