lopata 0.19.2 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/types/bindings/ai-search.d.ts +44 -0
  2. package/dist/types/bindings/analytics-engine-sql.d.ts +107 -0
  3. package/dist/types/bindings/artifacts-git-http.d.ts +19 -0
  4. package/dist/types/bindings/artifacts.d.ts +108 -0
  5. package/dist/types/bindings/flagship.d.ts +37 -0
  6. package/dist/types/bindings/vpc-network.d.ts +22 -0
  7. package/dist/types/bindings/worker-loader-entry.d.ts +71 -0
  8. package/dist/types/bindings/worker-loader.d.ts +81 -0
  9. package/dist/types/config.d.ts +23 -0
  10. package/dist/types/env.d.ts +3 -1
  11. package/dist/types/generation-manager.d.ts +6 -0
  12. package/dist/types/tsconfig.tsbuildinfo +1 -1
  13. package/dist/types/vite-plugin/dev-server-plugin.d.ts +8 -0
  14. package/dist/types/worker-thread/executor.d.ts +2 -0
  15. package/dist/types/worker-thread/protocol.d.ts +2 -0
  16. package/dist/types/worker-thread/thread-env.d.ts +3 -1
  17. package/package.json +1 -1
  18. package/src/bindings/ai-search.ts +185 -0
  19. package/src/bindings/analytics-engine-sql.ts +1138 -0
  20. package/src/bindings/artifacts-git-http.ts +249 -0
  21. package/src/bindings/artifacts.ts +453 -0
  22. package/src/bindings/do-worker-env.ts +4 -0
  23. package/src/bindings/flagship.ts +117 -0
  24. package/src/bindings/vpc-network.ts +40 -0
  25. package/src/bindings/worker-loader-entry.ts +143 -0
  26. package/src/bindings/worker-loader.ts +325 -0
  27. package/src/cli/dev.ts +25 -3
  28. package/src/config.ts +20 -0
  29. package/src/db.ts +54 -0
  30. package/src/env.ts +71 -0
  31. package/src/generation-manager.ts +5 -1
  32. package/src/generation.ts +20 -3
  33. package/src/plugin.ts +37 -0
  34. package/src/vite-plugin/dev-server-plugin.ts +18 -2
  35. package/src/worker-thread/entry.ts +1 -0
  36. package/src/worker-thread/executor.ts +3 -0
  37. package/src/worker-thread/protocol.ts +2 -0
  38. package/src/worker-thread/thread-env.ts +72 -1
@@ -28,4 +28,12 @@ interface DevServerPluginOptions {
28
28
  * via link:), so dynamic imports here run through Bun's native loader.
29
29
  */
30
30
  export declare function devServerPlugin(options: DevServerPluginOptions): Plugin;
31
+ /**
32
+ * Convert Response headers to a node writeHead record. `set-cookie` must go
33
+ * through `getSetCookie()`: Headers iteration yields it once per cookie (and a
34
+ * keyed record would keep only the last one), so a multi-cookie response —
35
+ * e.g. better-auth's session_token + session_data — would silently lose
36
+ * cookies. Exported for tests.
37
+ */
38
+ export declare function buildNodeHeaders(response: Response): Record<string, string | string[]>;
31
39
  export {};
@@ -24,6 +24,8 @@ export interface WorkerThreadExecutorOptions {
24
24
  executablePath?: string;
25
25
  headless?: boolean;
26
26
  };
27
+ /** Base URL for the Artifacts git remote (main's `/__artifacts/git` endpoint). */
28
+ artifactsBaseUrl?: string;
27
29
  /** Main-thread env holding the stateful binding instances the worker calls into via RPC. */
28
30
  mainEnv: Record<string, unknown>;
29
31
  }
@@ -92,6 +92,8 @@ export interface WorkerInitConfig {
92
92
  executablePath?: string;
93
93
  headless?: boolean;
94
94
  };
95
+ /** Base URL for the Artifacts git remote (main's `/__artifacts/git` endpoint). */
96
+ artifactsBaseUrl?: string;
95
97
  }
96
98
  /** Names of the worker handlers we know how to invoke via RPC. */
97
99
  export type WorkerHandlerName = 'fetch' | 'scheduled' | 'email' | 'queue';
@@ -26,6 +26,8 @@ export interface ThreadEnvOptions {
26
26
  executablePath?: string;
27
27
  headless?: boolean;
28
28
  };
29
+ /** Base URL for the Artifacts git remote (main's `/__artifacts/git` endpoint). */
30
+ artifactsBaseUrl?: string;
29
31
  }
30
32
  export interface ThreadEnvBuilt {
31
33
  env: Record<string, unknown>;
@@ -39,4 +41,4 @@ export interface ThreadEnvBuilt {
39
41
  binding: SqliteWorkflowBinding;
40
42
  }[];
41
43
  }
42
- export declare function buildThreadEnv({ config, baseDir, dataDir, rpc, envWsBridge, browserConfig }: ThreadEnvOptions): ThreadEnvBuilt;
44
+ export declare function buildThreadEnv({ config, baseDir, dataDir, rpc, envWsBridge, browserConfig, artifactsBaseUrl }: ThreadEnvOptions): ThreadEnvBuilt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopata",
3
- "version": "0.19.2",
3
+ "version": "0.20.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Local implementation of the Cloudflare AI Search namespace binding
3
+ * (`ai_search_namespaces`).
4
+ *
5
+ * Running vector search + RAG locally is impractical, so this binding
6
+ * proxies to the real Cloudflare AI Search REST API — same strategy as
7
+ * `AiBinding`. Every proxied call is logged to SQLite for the dashboard.
8
+ *
9
+ * Requires `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in `.dev.vars`
10
+ * or the environment.
11
+ */
12
+ import { randomUUIDv7 } from 'bun'
13
+ import type { Database } from 'bun:sqlite'
14
+
15
+ const MAX_LOG_SIZE = 1024
16
+
17
+ function truncate(value: unknown): string {
18
+ const str = typeof value === 'string' ? value : JSON.stringify(value)
19
+ if (!str) return ''
20
+ return str.length > MAX_LOG_SIZE ? str.slice(0, MAX_LOG_SIZE) + '…' : str
21
+ }
22
+
23
+ interface CreateInstanceOptions {
24
+ id: string
25
+ type?: string
26
+ source?: unknown
27
+ }
28
+
29
+ interface SearchOptions {
30
+ messages: { role: string; content: string }[]
31
+ ai_search_options?: {
32
+ instance_ids?: string[]
33
+ [key: string]: unknown
34
+ }
35
+ stream?: boolean
36
+ [key: string]: unknown
37
+ }
38
+
39
+ export class AiSearchInstance {
40
+ private readonly binding: AiSearchNamespaceBinding
41
+ readonly id: string
42
+ readonly metadata: Record<string, unknown>
43
+
44
+ constructor(binding: AiSearchNamespaceBinding, id: string, metadata: Record<string, unknown> = {}) {
45
+ this.binding = binding
46
+ this.id = id
47
+ this.metadata = metadata
48
+ }
49
+
50
+ async search(options: SearchOptions): Promise<unknown> {
51
+ return this.binding._proxyInstance('search', this.id, options)
52
+ }
53
+
54
+ async chatCompletions(options: SearchOptions): Promise<unknown> {
55
+ return this.binding._proxyInstance('chatCompletions', this.id, options)
56
+ }
57
+ }
58
+
59
+ export class AiSearchNamespaceBinding {
60
+ private readonly db: Database
61
+ private readonly namespace: string
62
+ private readonly accountId?: string
63
+ private readonly apiToken?: string
64
+
65
+ constructor(db: Database, namespace: string, accountId?: string, apiToken?: string) {
66
+ this.db = db
67
+ this.namespace = namespace
68
+ this.accountId = accountId
69
+ this.apiToken = apiToken
70
+ }
71
+
72
+ private ensureCredentials(): { accountId: string; apiToken: string } {
73
+ if (!this.accountId || !this.apiToken) {
74
+ throw new Error(
75
+ 'AI Search requires CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN in .dev.vars',
76
+ )
77
+ }
78
+ return { accountId: this.accountId, apiToken: this.apiToken }
79
+ }
80
+
81
+ async create(opts: CreateInstanceOptions): Promise<AiSearchInstance> {
82
+ const body = await this.proxy('POST', '/ai-search/instances', opts, 'create')
83
+ const result = (body as { result?: Record<string, unknown> }).result ?? {}
84
+ return new AiSearchInstance(this, opts.id, result)
85
+ }
86
+
87
+ async get(id: string): Promise<AiSearchInstance> {
88
+ const body = await this.proxy('GET', `/ai-search/instances/${encodeURIComponent(id)}`, undefined, 'get')
89
+ const result = (body as { result?: Record<string, unknown> }).result ?? {}
90
+ return new AiSearchInstance(this, id, result)
91
+ }
92
+
93
+ async list(opts?: Record<string, string>): Promise<unknown> {
94
+ const query = opts ? '?' + new URLSearchParams(opts).toString() : ''
95
+ return this.proxy('GET', `/ai-search/instances${query}`, undefined, 'list')
96
+ }
97
+
98
+ async delete(id: string): Promise<boolean> {
99
+ await this.proxy('DELETE', `/ai-search/instances/${encodeURIComponent(id)}`, undefined, 'delete')
100
+ return true
101
+ }
102
+
103
+ async search(options: SearchOptions): Promise<unknown> {
104
+ return this.proxy('POST', `/ai-search/namespaces/${encodeURIComponent(this.namespace)}/search`, options, 'search')
105
+ }
106
+
107
+ async chatCompletions(options: SearchOptions): Promise<unknown> {
108
+ return this.proxy(
109
+ 'POST',
110
+ `/ai-search/namespaces/${encodeURIComponent(this.namespace)}/chat/completions`,
111
+ options,
112
+ 'chatCompletions',
113
+ )
114
+ }
115
+
116
+ /** @internal — called by AiSearchInstance */
117
+ async _proxyInstance(method: 'search' | 'chatCompletions', instanceId: string, options: SearchOptions): Promise<unknown> {
118
+ const path = method === 'search'
119
+ ? `/ai-search/instances/${encodeURIComponent(instanceId)}/search`
120
+ : `/ai-search/instances/${encodeURIComponent(instanceId)}/chat/completions`
121
+ return this.proxy('POST', path, options, `instance.${method}`)
122
+ }
123
+
124
+ private async proxy(
125
+ httpMethod: string,
126
+ apiPath: string,
127
+ body: unknown,
128
+ operation: string,
129
+ ): Promise<unknown> {
130
+ const { accountId, apiToken } = this.ensureCredentials()
131
+ const id = randomUUIDv7()
132
+ const start = Date.now()
133
+ let status = 'ok'
134
+ let error: string | undefined
135
+ let outputSummary = ''
136
+
137
+ try {
138
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}${apiPath}`
139
+ const response = await fetch(url, {
140
+ method: httpMethod,
141
+ headers: {
142
+ Authorization: `Bearer ${apiToken}`,
143
+ 'Content-Type': 'application/json',
144
+ },
145
+ body: body === undefined ? undefined : JSON.stringify(body),
146
+ })
147
+
148
+ if (!response.ok) {
149
+ const text = await response.text()
150
+ status = 'error'
151
+ error = `HTTP ${response.status}: ${text}`
152
+ throw new Error(error)
153
+ }
154
+
155
+ if ((body as SearchOptions | undefined)?.stream) {
156
+ outputSummary = '<streaming>'
157
+ return response.body
158
+ }
159
+
160
+ const ct = response.headers.get('content-type') ?? ''
161
+ if (ct.includes('application/json')) {
162
+ const json = await response.json()
163
+ outputSummary = truncate(json)
164
+ return json
165
+ }
166
+ const text = await response.text()
167
+ outputSummary = truncate(text)
168
+ return text
169
+ } catch (err) {
170
+ if (status !== 'error') {
171
+ status = 'error'
172
+ error = err instanceof Error ? err.message : String(err)
173
+ }
174
+ throw err
175
+ } finally {
176
+ const duration = Date.now() - start
177
+ this.db
178
+ .prepare(
179
+ `INSERT INTO ai_search_requests (id, namespace, operation, input_summary, output_summary, duration_ms, status, error, created_at)
180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
181
+ )
182
+ .run(id, this.namespace, operation, truncate(body), outputSummary, duration, status, error ?? null, start)
183
+ }
184
+ }
185
+ }