@mugwork/mug 0.1.0

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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/explorer.js +3 -0
  4. package/dist/packages/email-template/src/email-template.d.ts +18 -0
  5. package/dist/packages/email-template/src/email-template.js +74 -0
  6. package/dist/packages/email-template/src/index.d.ts +1 -0
  7. package/dist/packages/email-template/src/index.js +1 -0
  8. package/dist/packages/surface-renderer/src/form-renderer.d.ts +117 -0
  9. package/dist/packages/surface-renderer/src/form-renderer.js +719 -0
  10. package/dist/packages/surface-renderer/src/index.d.ts +4 -0
  11. package/dist/packages/surface-renderer/src/index.js +2 -0
  12. package/dist/packages/surface-renderer/src/portal-renderer.d.ts +177 -0
  13. package/dist/packages/surface-renderer/src/portal-renderer.js +1089 -0
  14. package/dist/packages/surface-renderer/src/workspace-home.d.ts +46 -0
  15. package/dist/packages/surface-renderer/src/workspace-home.js +345 -0
  16. package/dist/runtime/agent-types.d.ts +48 -0
  17. package/dist/runtime/agent-types.js +3 -0
  18. package/dist/runtime/ai-router.d.ts +32 -0
  19. package/dist/runtime/ai-router.js +112 -0
  20. package/dist/runtime/app.d.ts +6 -0
  21. package/dist/runtime/app.js +399 -0
  22. package/dist/runtime/chunker.d.ts +6 -0
  23. package/dist/runtime/chunker.js +30 -0
  24. package/dist/runtime/context.d.ts +115 -0
  25. package/dist/runtime/context.js +440 -0
  26. package/dist/runtime/do/workspace-database.d.ts +10 -0
  27. package/dist/runtime/do/workspace-database.js +199 -0
  28. package/dist/runtime/form-types.d.ts +143 -0
  29. package/dist/runtime/form-types.js +1 -0
  30. package/dist/runtime/runtime.d.ts +9 -0
  31. package/dist/runtime/runtime.js +7 -0
  32. package/dist/runtime/source-types.d.ts +15 -0
  33. package/dist/runtime/source-types.js +1 -0
  34. package/dist/runtime/source.d.ts +70 -0
  35. package/dist/runtime/source.js +21 -0
  36. package/dist/runtime/sync-runtime.d.ts +10 -0
  37. package/dist/runtime/sync-runtime.js +185 -0
  38. package/dist/runtime/types.d.ts +21 -0
  39. package/dist/runtime/types.js +1 -0
  40. package/dist/runtime/workflow-entrypoint.d.ts +31 -0
  41. package/dist/runtime/workflow-entrypoint.js +1297 -0
  42. package/dist/runtime/workflow.d.ts +285 -0
  43. package/dist/runtime/workflow.js +1008 -0
  44. package/dist/src/cli.d.ts +2 -0
  45. package/dist/src/cli.js +44116 -0
  46. package/dist/src/commands/ai-gateway-route.d.ts +24 -0
  47. package/dist/src/commands/ai-gateway-route.js +192 -0
  48. package/dist/src/commands/auth.d.ts +1 -0
  49. package/dist/src/commands/auth.js +42 -0
  50. package/dist/src/commands/billing.d.ts +6 -0
  51. package/dist/src/commands/billing.js +76 -0
  52. package/dist/src/commands/brain.d.ts +1 -0
  53. package/dist/src/commands/brain.js +194 -0
  54. package/dist/src/commands/demo.d.ts +12 -0
  55. package/dist/src/commands/demo.js +147 -0
  56. package/dist/src/commands/deploy.d.ts +1 -0
  57. package/dist/src/commands/deploy.js +1052 -0
  58. package/dist/src/commands/dev.d.ts +14 -0
  59. package/dist/src/commands/dev.js +2818 -0
  60. package/dist/src/commands/form.d.ts +8 -0
  61. package/dist/src/commands/form.js +396 -0
  62. package/dist/src/commands/init.d.ts +1 -0
  63. package/dist/src/commands/init.js +139 -0
  64. package/dist/src/commands/issue.d.ts +7 -0
  65. package/dist/src/commands/issue.js +191 -0
  66. package/dist/src/commands/login.d.ts +9 -0
  67. package/dist/src/commands/login.js +163 -0
  68. package/dist/src/commands/logs.d.ts +8 -0
  69. package/dist/src/commands/logs.js +113 -0
  70. package/dist/src/commands/portal.d.ts +2 -0
  71. package/dist/src/commands/portal.js +111 -0
  72. package/dist/src/commands/pull.d.ts +3 -0
  73. package/dist/src/commands/pull.js +184 -0
  74. package/dist/src/commands/push.d.ts +4 -0
  75. package/dist/src/commands/push.js +183 -0
  76. package/dist/src/commands/run.d.ts +6 -0
  77. package/dist/src/commands/run.js +91 -0
  78. package/dist/src/commands/secret.d.ts +7 -0
  79. package/dist/src/commands/secret.js +105 -0
  80. package/dist/src/commands/shutdown.d.ts +1 -0
  81. package/dist/src/commands/shutdown.js +46 -0
  82. package/dist/src/commands/sql.d.ts +8 -0
  83. package/dist/src/commands/sql.js +142 -0
  84. package/dist/src/commands/status.d.ts +5 -0
  85. package/dist/src/commands/status.js +39 -0
  86. package/dist/src/commands/sync.d.ts +7 -0
  87. package/dist/src/commands/sync.js +991 -0
  88. package/dist/src/commands/usage.d.ts +6 -0
  89. package/dist/src/commands/usage.js +78 -0
  90. package/dist/src/commands/webhooks.d.ts +1 -0
  91. package/dist/src/commands/webhooks.js +102 -0
  92. package/dist/src/commands/workspace.d.ts +23 -0
  93. package/dist/src/commands/workspace.js +590 -0
  94. package/dist/src/connector-migration.d.ts +20 -0
  95. package/dist/src/connector-migration.js +43 -0
  96. package/dist/src/connector-parser.d.ts +14 -0
  97. package/dist/src/connector-parser.js +94 -0
  98. package/dist/src/connector-service/discover.d.ts +37 -0
  99. package/dist/src/connector-service/discover.js +79 -0
  100. package/dist/src/connector-service/gather.d.ts +22 -0
  101. package/dist/src/connector-service/gather.js +89 -0
  102. package/dist/src/connector-service/init.d.ts +14 -0
  103. package/dist/src/connector-service/init.js +109 -0
  104. package/dist/src/connector-service/scaffold.d.ts +17 -0
  105. package/dist/src/connector-service/scaffold.js +194 -0
  106. package/dist/src/connector-service/spec-storage.d.ts +8 -0
  107. package/dist/src/connector-service/spec-storage.js +48 -0
  108. package/dist/src/connector-service/types.d.ts +57 -0
  109. package/dist/src/connector-service/types.js +2 -0
  110. package/dist/src/connector-service/verify.d.ts +24 -0
  111. package/dist/src/connector-service/verify.js +575 -0
  112. package/dist/src/email-template.d.ts +2 -0
  113. package/dist/src/email-template.js +1 -0
  114. package/dist/src/manifest.d.ts +31 -0
  115. package/dist/src/manifest.js +25 -0
  116. package/dist/src/mug-icon.d.ts +1 -0
  117. package/dist/src/mug-icon.js +12 -0
  118. package/dist/src/slack-manifest.d.ts +119 -0
  119. package/dist/src/slack-manifest.js +163 -0
  120. package/dist/src/source-migration.d.ts +20 -0
  121. package/dist/src/source-migration.js +43 -0
  122. package/dist/src/surface-renderer.d.ts +5 -0
  123. package/dist/src/surface-renderer.js +3 -0
  124. package/dist/src/templates.d.ts +3 -0
  125. package/dist/src/templates.js +48 -0
  126. package/dist/src/version-check.d.ts +1 -0
  127. package/dist/src/version-check.js +28 -0
  128. package/dist/src/workflow-parser.d.ts +95 -0
  129. package/dist/src/workflow-parser.js +526 -0
  130. package/dist/worker/src/agent-types.d.ts +27 -0
  131. package/dist/worker/src/agent-types.js +3 -0
  132. package/dist/worker/src/source-types.d.ts +14 -0
  133. package/dist/worker/src/source-types.js +1 -0
  134. package/package.json +90 -0
  135. package/src/data/model-capabilities.json +171 -0
@@ -0,0 +1,440 @@
1
+ import { scoreComplexity, resolveModel, parseModelSpec, resolveBilling } from "./ai-router.js";
2
+ function internalHeaders(env) {
3
+ return {
4
+ "Content-Type": "application/json",
5
+ "X-Mug-Internal": env.MUG_INTERNAL_SECRET ?? "",
6
+ };
7
+ }
8
+ function titleCase(slug) {
9
+ return (slug ?? "Mug").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
10
+ }
11
+ export class WorkspaceContext {
12
+ env;
13
+ isLocal;
14
+ constructor(env) {
15
+ this.env = env;
16
+ this.isLocal = !!env.WORKSPACE_DB;
17
+ }
18
+ getDatabaseStub(database) {
19
+ if (!this.env.WORKSPACE_DB)
20
+ throw new Error("WORKSPACE_DB binding not available");
21
+ return this.env.WORKSPACE_DB.get(this.env.WORKSPACE_DB.idFromName(database));
22
+ }
23
+ async query(database, sql, params) {
24
+ if (this.isLocal) {
25
+ const stub = this.env.WORKSPACE_DB.get(this.env.WORKSPACE_DB.idFromName(database));
26
+ const res = await stub.fetch(new URL("/query", "http://do").toString(), {
27
+ method: "POST",
28
+ body: JSON.stringify({ sql, params }),
29
+ headers: { "Content-Type": "application/json" },
30
+ });
31
+ const data = (await res.json());
32
+ return data.rows;
33
+ }
34
+ const res = await this.env.MUG_DATA.fetch(`https://mug-data/workspace/${this.env.WORKSPACE_ID}/db/${database}/query`, {
35
+ method: "POST",
36
+ body: JSON.stringify({ sql, params }),
37
+ headers: internalHeaders(this.env),
38
+ });
39
+ const data = (await res.json());
40
+ return data.rows;
41
+ }
42
+ async exec(database, sql, params, changeset) {
43
+ const csHeaders = {};
44
+ if (changeset?.id)
45
+ csHeaders["X-Changeset-Id"] = changeset.id;
46
+ if (changeset?.source)
47
+ csHeaders["X-Changeset-Source"] = changeset.source;
48
+ if (this.isLocal) {
49
+ const stub = this.env.WORKSPACE_DB.get(this.env.WORKSPACE_DB.idFromName(database));
50
+ const res = await stub.fetch(new URL("/exec", "http://do").toString(), {
51
+ method: "POST",
52
+ body: JSON.stringify({ sql, params }),
53
+ headers: { "Content-Type": "application/json", ...csHeaders },
54
+ });
55
+ const data = (await res.json());
56
+ return data.changes;
57
+ }
58
+ const res = await this.env.MUG_DATA.fetch(`https://mug-data/workspace/${this.env.WORKSPACE_ID}/db/${database}/exec`, {
59
+ method: "POST",
60
+ body: JSON.stringify({ sql, params }),
61
+ headers: { ...internalHeaders(this.env), ...csHeaders },
62
+ });
63
+ const data = (await res.json());
64
+ return data.changes;
65
+ }
66
+ getWorkspaceRouting() {
67
+ if (!this.env.MUG_AI_ROUTING)
68
+ return undefined;
69
+ try {
70
+ return JSON.parse(this.env.MUG_AI_ROUTING);
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ getWorkspaceBilling() {
77
+ if (!this.env.MUG_AI_BILLING)
78
+ return undefined;
79
+ try {
80
+ return JSON.parse(this.env.MUG_AI_BILLING);
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ async ai(model, options) {
87
+ let provider;
88
+ let resolvedModel;
89
+ let tier = null;
90
+ let routingMeta;
91
+ const tierNames = ["fast", "balanced", "powerful"];
92
+ if (model === "auto") {
93
+ const score = scoreComplexity(options.prompt, options);
94
+ tier = score.tier;
95
+ const resolved = resolveModel(tier, options.routing, this.getWorkspaceRouting());
96
+ provider = resolved.provider;
97
+ resolvedModel = resolved.model;
98
+ routingMeta = { tier, model: resolvedModel, provider, reason: score.reason };
99
+ }
100
+ else if (tierNames.includes(model)) {
101
+ tier = model;
102
+ const resolved = resolveModel(tier, options.routing, this.getWorkspaceRouting());
103
+ provider = resolved.provider;
104
+ resolvedModel = resolved.model;
105
+ routingMeta = { tier, model: resolvedModel, provider, reason: `tier:${tier}` };
106
+ }
107
+ else {
108
+ const parsed = parseModelSpec(model);
109
+ provider = parsed.provider;
110
+ resolvedModel = parsed.model;
111
+ }
112
+ const billingKey = resolveBilling(tier, options.billing, undefined, this.getWorkspaceBilling());
113
+ const billing = billingKey !== "mug-metered"
114
+ ? this.env[billingKey] ?? billingKey
115
+ : billingKey;
116
+ const body = JSON.stringify({
117
+ workspace: this.env.WORKSPACE_ID,
118
+ provider,
119
+ model: resolvedModel,
120
+ prompt: options.prompt,
121
+ system: options.system,
122
+ maxTokens: options.maxTokens,
123
+ routing: routingMeta ? { tier: routingMeta.tier, reason: routingMeta.reason } : undefined,
124
+ billing,
125
+ });
126
+ let res;
127
+ if (this.isLocal) {
128
+ res = await fetch("http://localhost:8787/_ai/complete", {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" },
131
+ body,
132
+ });
133
+ }
134
+ else {
135
+ res = await this.env.MUG_AI.fetch("https://mug-ai/complete", {
136
+ method: "POST",
137
+ body,
138
+ headers: internalHeaders(this.env),
139
+ });
140
+ }
141
+ if (!res.ok)
142
+ throw new Error(`AI request failed (${res.status}): ${await res.text()}`);
143
+ const data = (await res.json());
144
+ if (routingMeta)
145
+ data.routing = routingMeta;
146
+ return data;
147
+ }
148
+ async embed(texts) {
149
+ if (texts.length === 0)
150
+ return [];
151
+ const results = [];
152
+ for (let i = 0; i < texts.length; i += 100) {
153
+ const batch = texts.slice(i, i + 100);
154
+ const body = JSON.stringify({ workspace: this.env.WORKSPACE_ID, texts: batch });
155
+ let res;
156
+ if (this.isLocal) {
157
+ res = await fetch("http://localhost:8787/_ai/embed", {
158
+ method: "POST",
159
+ headers: { "Content-Type": "application/json" },
160
+ body,
161
+ });
162
+ }
163
+ else {
164
+ res = await this.env.MUG_AI.fetch("https://mug-ai/embed", {
165
+ method: "POST",
166
+ body,
167
+ headers: internalHeaders(this.env),
168
+ });
169
+ }
170
+ if (!res.ok)
171
+ throw new Error(`Embed request failed (${res.status}): ${await res.text()}`);
172
+ const data = (await res.json());
173
+ results.push(...data.vectors);
174
+ }
175
+ return results;
176
+ }
177
+ async search(query, options) {
178
+ if (this.isLocal) {
179
+ throw new Error("Semantic search not available in local dev — requires deployed workspace");
180
+ }
181
+ const limit = Math.min(options?.limit ?? 10, 50);
182
+ const [queryVector] = await this.embed([query]);
183
+ const filter = { ...options?.filter };
184
+ if (options?.source)
185
+ filter.table = options.source;
186
+ const res = await this.env.MUG_DISPATCH.fetch(`https://mug-dispatch/vector/${this.env.WORKSPACE_ID}/query`, {
187
+ method: "POST",
188
+ headers: { "Content-Type": "application/json", "X-Mug-Internal": this.env.MUG_INTERNAL_SECRET },
189
+ body: JSON.stringify({
190
+ vector: queryVector,
191
+ topK: limit * 2,
192
+ returnMetadata: true,
193
+ ...(Object.keys(filter).length > 0 ? { filter } : {}),
194
+ }),
195
+ });
196
+ const data = await res.json();
197
+ const matchesArr = (data.result?.matches ?? data.matches ?? []);
198
+ const best = new Map();
199
+ for (const match of matchesArr) {
200
+ const meta = match.metadata;
201
+ if (!meta?.table || !meta?.primary_key)
202
+ continue;
203
+ const key = `${meta.table}:${meta.primary_key}`;
204
+ const existing = best.get(key);
205
+ if (!existing || match.score > existing.score) {
206
+ best.set(key, { score: match.score, meta });
207
+ }
208
+ }
209
+ const results = [];
210
+ for (const [, { score, meta }] of best) {
211
+ if (results.length >= limit)
212
+ break;
213
+ try {
214
+ const rows = await this.query(meta.database, `SELECT * FROM "${meta.table}" WHERE "${meta.pk_column}" = ? AND _mug_deleted_at IS NULL`, [meta.primary_key]);
215
+ if (rows.length > 0) {
216
+ results.push({ score, table: meta.table, primaryKey: meta.primary_key, row: rows[0] });
217
+ }
218
+ }
219
+ catch {
220
+ results.push({ score, table: meta.table, primaryKey: meta.primary_key, row: {} });
221
+ }
222
+ }
223
+ return results;
224
+ }
225
+ async ask(question, options) {
226
+ const sources = await this.search(question, {
227
+ source: options?.source,
228
+ limit: options?.limit ?? 10,
229
+ });
230
+ const contextParts = [];
231
+ let tokenEstimate = 0;
232
+ for (const result of sources) {
233
+ const entry = `[${result.table}:${result.primaryKey} score=${result.score.toFixed(3)}]\n${JSON.stringify(result.row)}`;
234
+ const entryTokens = Math.ceil(entry.split(/\s+/).length * 1.3);
235
+ if (tokenEstimate + entryTokens > 3000)
236
+ break;
237
+ contextParts.push(entry);
238
+ tokenEstimate += entryTokens;
239
+ }
240
+ const baseSystem = "Answer the question based on the following business data. Cite which records informed your answer. If the data does not contain enough information, say so.";
241
+ const dataBlock = `\n\n--- Business Data ---\n${contextParts.join("\n\n")}`;
242
+ const system = (options?.system ? `${options.system}\n\n${baseSystem}` : baseSystem) + dataBlock;
243
+ const aiResult = await this.ai(options?.model ?? "balanced", {
244
+ prompt: question,
245
+ system,
246
+ });
247
+ return {
248
+ answer: aiResult.text,
249
+ sources,
250
+ usage: {
251
+ input_tokens: aiResult.usage.input_tokens,
252
+ output_tokens: aiResult.usage.output_tokens,
253
+ search_results: sources.length,
254
+ },
255
+ };
256
+ }
257
+ surfaceUrl(surfaceId, path) {
258
+ const suffix = path ?? "";
259
+ if (this.isLocal) {
260
+ return `http://localhost:8787/${surfaceId}${suffix}`;
261
+ }
262
+ return `https://${this.env.WORKSPACE_ID}.mug.work/${surfaceId}${suffix}`;
263
+ }
264
+ get notify() {
265
+ return {
266
+ email: (options) => this.sendNotification("email", options),
267
+ sms: (options) => this.sendNotification("sms", options),
268
+ slack: (options) => this.sendNotification("slack", options),
269
+ channel: (name, options) => this.sendNotification(name, options),
270
+ };
271
+ }
272
+ getBranding() {
273
+ if (!this.env.MUG_BRANDING)
274
+ return undefined;
275
+ try {
276
+ return JSON.parse(this.env.MUG_BRANDING);
277
+ }
278
+ catch {
279
+ return undefined;
280
+ }
281
+ }
282
+ async sendNotification(channel, options) {
283
+ const fromName = options.fromName ?? titleCase(this.env.WORKSPACE_ID);
284
+ const branding = this.getBranding();
285
+ if (this.isLocal) {
286
+ console.log(`[${channel}] to=${options.to}${options.subject ? ` subject=${options.subject}` : ""}`);
287
+ try {
288
+ const res = await fetch("http://localhost:8787/_notify/send", {
289
+ method: "POST",
290
+ headers: { "Content-Type": "application/json" },
291
+ body: JSON.stringify({
292
+ workspace: this.env.WORKSPACE_ID,
293
+ channel,
294
+ to: options.to,
295
+ message: options.message,
296
+ subject: options.subject,
297
+ fromName,
298
+ cta: options.cta,
299
+ branding,
300
+ ...(channel === "slack" ? {
301
+ blocks: options.blocks,
302
+ thread_ts: options.thread_ts,
303
+ unfurl_links: options.unfurl_links,
304
+ unfurl_media: options.unfurl_media,
305
+ slackBotToken: this.env.SLACK_BOT_TOKEN,
306
+ } : {}),
307
+ ...(channel === "sms" ? {
308
+ telnyxApiKey: this.env.TELNYX_API_KEY,
309
+ telnyxPhoneNumber: this.env.TELNYX_PHONE_NUMBER,
310
+ twilioAccountSid: this.env.TWILIO_ACCOUNT_SID,
311
+ twilioAuthToken: this.env.TWILIO_AUTH_TOKEN,
312
+ twilioPhoneNumber: this.env.TWILIO_PHONE_NUMBER,
313
+ } : {}),
314
+ }),
315
+ });
316
+ const result = await res.json();
317
+ if (result.status === "blocked") {
318
+ console.log(`[${channel}] BLOCKED: ${result.error}`);
319
+ }
320
+ else if (result.status === "delivery_failed") {
321
+ console.log(`[${channel}] DELIVERY FAILED: ${result.error ?? "no detail from provider"}`);
322
+ }
323
+ else if (result.status === "logged") {
324
+ console.log(`[${channel}] logged but not delivered — missing provider credentials`);
325
+ }
326
+ return result.status;
327
+ }
328
+ catch {
329
+ console.log(`[${channel}] delivery skipped — dev proxy not reachable`);
330
+ return "skipped";
331
+ }
332
+ }
333
+ const res = await this.env.MUG_NOTIFY.fetch("https://mug-notify/send", {
334
+ method: "POST",
335
+ body: JSON.stringify({
336
+ workspace: this.env.WORKSPACE_ID,
337
+ channel,
338
+ to: options.to,
339
+ message: options.message,
340
+ subject: options.subject,
341
+ fromName,
342
+ cta: options.cta,
343
+ branding,
344
+ ...(channel === "slack" ? {
345
+ blocks: options.blocks,
346
+ thread_ts: options.thread_ts,
347
+ unfurl_links: options.unfurl_links,
348
+ unfurl_media: options.unfurl_media,
349
+ slackBotToken: this.env.SLACK_BOT_TOKEN,
350
+ } : {}),
351
+ ...(channel === "sms" ? {
352
+ telnyxApiKey: this.env.TELNYX_API_KEY,
353
+ telnyxPhoneNumber: this.env.TELNYX_PHONE_NUMBER,
354
+ twilioAccountSid: this.env.TWILIO_ACCOUNT_SID,
355
+ twilioAuthToken: this.env.TWILIO_AUTH_TOKEN,
356
+ twilioPhoneNumber: this.env.TWILIO_PHONE_NUMBER,
357
+ } : {}),
358
+ }),
359
+ headers: internalHeaders(this.env),
360
+ });
361
+ const result = await res.json();
362
+ return result.status;
363
+ }
364
+ async slackApiCall(method, body) {
365
+ const token = this.env.SLACK_BOT_TOKEN;
366
+ if (!token)
367
+ throw new Error("SLACK_BOT_TOKEN not configured");
368
+ const res = await fetch(`https://slack.com/api/${method}`, {
369
+ method: "POST",
370
+ headers: {
371
+ "Content-Type": "application/json; charset=utf-8",
372
+ Authorization: `Bearer ${token}`,
373
+ },
374
+ body: JSON.stringify(body),
375
+ });
376
+ const data = await res.json();
377
+ if (!data.ok)
378
+ throw new Error(`Slack API ${method} failed: ${data.error ?? "unknown error"}`);
379
+ return data;
380
+ }
381
+ async file(path) {
382
+ if (this.isLocal) {
383
+ const res = await fetch(`http://localhost:8787/_files/${path}`);
384
+ if (!res.ok)
385
+ throw new Error(`File not found: ${path}`);
386
+ return res.arrayBuffer();
387
+ }
388
+ const res = await this.env.MUG_DATA.fetch(`https://mug-data/workspace/${this.env.WORKSPACE_ID}/files/read/${path}`, { headers: { "X-Mug-Internal": this.env.MUG_INTERNAL_SECRET ?? "" } });
389
+ if (!res.ok)
390
+ throw new Error(`File not found: ${path}`);
391
+ return res.arrayBuffer();
392
+ }
393
+ async fileText(path) {
394
+ const buffer = await this.file(path);
395
+ return new TextDecoder().decode(buffer);
396
+ }
397
+ async invokeAgent(name, options) {
398
+ const workspace = this.env.WORKSPACE_ID;
399
+ const res = await this.env.MUG_DISPATCH.fetch(`https://mug-dispatch/agent/${workspace}/invoke`, {
400
+ method: "POST",
401
+ body: JSON.stringify({ agent: name, ...options }),
402
+ headers: internalHeaders(this.env),
403
+ });
404
+ if (!res.ok) {
405
+ const errText = await res.text();
406
+ throw new Error(`Agent "${name}" failed: ${errText}`);
407
+ }
408
+ return res.json();
409
+ }
410
+ async collect(options) {
411
+ const surfaceId = options.id ?? crypto.randomUUID().slice(0, 8);
412
+ const workspace = this.env.WORKSPACE_ID;
413
+ const pages = options.pages ?? [{
414
+ id: "main",
415
+ fields: options.fields ?? [],
416
+ }];
417
+ const access = options.access ?? { mode: "public" };
418
+ const schema = {
419
+ title: options.title,
420
+ description: options.description,
421
+ submitText: options.submitText,
422
+ pages,
423
+ access,
424
+ editMode: options.editMode,
425
+ workflow: options.workflow,
426
+ };
427
+ const surfaceConfig = { workspace, surfaceId, ...schema };
428
+ const url = this.surfaceUrl(surfaceId);
429
+ if (this.isLocal) {
430
+ console.log(`[collect] Surface "${surfaceId}" created: ${url}`);
431
+ return url;
432
+ }
433
+ await this.env.MUG_DISPATCH.fetch("https://mug-dispatch/deploy-surface", {
434
+ method: "POST",
435
+ body: JSON.stringify(surfaceConfig),
436
+ headers: internalHeaders(this.env),
437
+ });
438
+ return url;
439
+ }
440
+ }
@@ -0,0 +1,10 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ import type { Env } from "../types.js";
3
+ export declare class WorkspaceDatabase extends DurableObject<Env> {
4
+ fetch(request: Request): Promise<Response>;
5
+ private handleSync;
6
+ private ensureFTS5;
7
+ private handleSeed;
8
+ private handleIntrospect;
9
+ private handleExport;
10
+ }
@@ -0,0 +1,199 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ function stringify(v) {
3
+ if (v === null || v === undefined)
4
+ return null;
5
+ if (typeof v === "object")
6
+ return JSON.stringify(v);
7
+ return String(v);
8
+ }
9
+ export class WorkspaceDatabase extends DurableObject {
10
+ async fetch(request) {
11
+ const url = new URL(request.url);
12
+ const action = url.pathname.split("/").pop();
13
+ try {
14
+ if (action === "sync") {
15
+ const body = (await request.json());
16
+ const result = this.handleSync(body);
17
+ return Response.json(result);
18
+ }
19
+ if (action === "seed") {
20
+ const body = (await request.json());
21
+ const result = this.handleSeed(body.tables);
22
+ return Response.json(result);
23
+ }
24
+ if (action === "export") {
25
+ return Response.json(this.handleExport());
26
+ }
27
+ if (action === "introspect") {
28
+ return Response.json(this.handleIntrospect());
29
+ }
30
+ const body = (await request.json());
31
+ const sql = this.ctx.storage.sql;
32
+ if (action === "query") {
33
+ const cursor = sql.exec(body.sql, ...(body.params ?? []));
34
+ return Response.json({ rows: cursor.toArray() });
35
+ }
36
+ if (action === "exec") {
37
+ const cursor = sql.exec(body.sql, ...(body.params ?? []));
38
+ return Response.json({ changes: cursor.rowsWritten });
39
+ }
40
+ return Response.json({ error: "Unknown action" }, { status: 400 });
41
+ }
42
+ catch (err) {
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ return Response.json({ error: msg }, { status: 500 });
45
+ }
46
+ }
47
+ handleSync(req) {
48
+ const sql = this.ctx.storage.sql;
49
+ const { table, primaryKey, rows } = req;
50
+ if (rows.length === 0) {
51
+ return { upserted: 0, deleted: 0, deletedPks: [] };
52
+ }
53
+ const columns = Object.keys(rows[0]);
54
+ const allColumns = [...columns, "_mug_synced_at", "_mug_deleted_at"];
55
+ sql.exec(`
56
+ CREATE TABLE IF NOT EXISTS "${table}" (
57
+ ${allColumns.map((c) => `"${c}" TEXT`).join(", ")},
58
+ PRIMARY KEY ("${primaryKey}")
59
+ )
60
+ `);
61
+ const existing = sql.exec(`PRAGMA table_info("${table}")`).toArray();
62
+ const existingNames = new Set(existing.map((r) => r.name));
63
+ for (const col of allColumns) {
64
+ if (!existingNames.has(col)) {
65
+ sql.exec(`ALTER TABLE "${table}" ADD COLUMN "${col}" TEXT`);
66
+ }
67
+ }
68
+ const now = new Date().toISOString();
69
+ const incomingPks = new Set();
70
+ const placeholders = allColumns.map(() => "?").join(", ");
71
+ const updateSet = allColumns
72
+ .filter((c) => c !== primaryKey)
73
+ .map((c) => `"${c}" = excluded."${c}"`)
74
+ .join(", ");
75
+ const upsertSql = `INSERT INTO "${table}" (${allColumns.map((c) => `"${c}"`).join(", ")})
76
+ VALUES (${placeholders})
77
+ ON CONFLICT ("${primaryKey}") DO UPDATE SET ${updateSet}`;
78
+ for (const row of rows) {
79
+ const pk = String(row[primaryKey]);
80
+ incomingPks.add(pk);
81
+ const values = columns.map((c) => stringify(row[c]));
82
+ values.push(now);
83
+ values.push(null);
84
+ sql.exec(upsertSql, ...values);
85
+ }
86
+ // Soft-delete rows no longer in source (decision #56)
87
+ const allPks = sql
88
+ .exec(`SELECT "${primaryKey}" FROM "${table}" WHERE _mug_deleted_at IS NULL`)
89
+ .toArray();
90
+ let deleted = 0;
91
+ const deletedPks = [];
92
+ for (const row of allPks) {
93
+ const pk = String(row[primaryKey]);
94
+ if (!incomingPks.has(pk)) {
95
+ sql.exec(`UPDATE "${table}" SET _mug_deleted_at = ? WHERE "${primaryKey}" = ?`, now, pk);
96
+ deleted++;
97
+ deletedPks.push(pk);
98
+ }
99
+ }
100
+ this.ensureFTS5(table, primaryKey);
101
+ return { upserted: rows.length, deleted, deletedPks };
102
+ }
103
+ ensureFTS5(table, primaryKey) {
104
+ const sql = this.ctx.storage.sql;
105
+ const cols = sql.exec(`PRAGMA table_info("${table}")`).toArray();
106
+ const skipCols = new Set([primaryKey, "_mug_synced_at", "_mug_deleted_at"]);
107
+ const textCols = cols.map((c) => c.name).filter((c) => !skipCols.has(c));
108
+ if (textCols.length === 0)
109
+ return;
110
+ const ftsTable = `${table}_fts`;
111
+ const colList = textCols.map((c) => `"${c}"`).join(", ");
112
+ const existing = sql
113
+ .exec("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", ftsTable)
114
+ .toArray();
115
+ if (existing.length > 0) {
116
+ const needsRebuild = textCols.some((c) => !existing[0].sql.includes(`"${c}"`));
117
+ if (!needsRebuild)
118
+ return;
119
+ sql.exec(`DROP TABLE IF EXISTS "${ftsTable}"`);
120
+ sql.exec(`DROP TRIGGER IF EXISTS "${table}_fts_insert"`);
121
+ sql.exec(`DROP TRIGGER IF EXISTS "${table}_fts_update"`);
122
+ sql.exec(`DROP TRIGGER IF EXISTS "${table}_fts_delete"`);
123
+ }
124
+ sql.exec(`CREATE VIRTUAL TABLE "${ftsTable}" USING fts5(${colList}, content="${table}", content_rowid=rowid)`);
125
+ const newColList = textCols.map((c) => `new."${c}"`).join(", ");
126
+ const oldColList = textCols.map((c) => `old."${c}"`).join(", ");
127
+ const ftsDelete = `INSERT INTO "${ftsTable}" ("${ftsTable}", rowid, ${colList})`;
128
+ sql.exec(`
129
+ CREATE TRIGGER "${table}_fts_insert" AFTER INSERT ON "${table}"
130
+ WHEN new._mug_deleted_at IS NULL
131
+ BEGIN
132
+ INSERT INTO "${ftsTable}" (rowid, ${colList}) VALUES (new.rowid, ${newColList});
133
+ END
134
+ `);
135
+ sql.exec(`
136
+ CREATE TRIGGER "${table}_fts_delete" AFTER DELETE ON "${table}"
137
+ BEGIN
138
+ ${ftsDelete} VALUES ('delete', old.rowid, ${oldColList});
139
+ END
140
+ `);
141
+ sql.exec(`
142
+ CREATE TRIGGER "${table}_fts_update" AFTER UPDATE ON "${table}"
143
+ BEGIN
144
+ ${ftsDelete} VALUES ('delete', old.rowid, ${oldColList});
145
+ INSERT INTO "${ftsTable}" (rowid, ${colList})
146
+ SELECT new.rowid, ${newColList} WHERE new._mug_deleted_at IS NULL;
147
+ END
148
+ `);
149
+ sql.exec(`INSERT INTO "${ftsTable}" (rowid, ${colList}) SELECT rowid, ${colList} FROM "${table}" WHERE _mug_deleted_at IS NULL`);
150
+ }
151
+ handleSeed(tables) {
152
+ const sql = this.ctx.storage.sql;
153
+ let tablesCreated = 0;
154
+ let rowsInserted = 0;
155
+ for (const [, tableData] of Object.entries(tables)) {
156
+ const tableName = tableData.ddl.match(/CREATE TABLE\s+"?([^"\s(]+)"?/i)?.[1];
157
+ if (!tableName)
158
+ continue;
159
+ sql.exec(`DROP TABLE IF EXISTS "${tableName}"`);
160
+ sql.exec(tableData.ddl);
161
+ tablesCreated++;
162
+ if (tableData.rows.length === 0)
163
+ continue;
164
+ const columns = Object.keys(tableData.rows[0]);
165
+ const placeholders = columns.map(() => "?").join(", ");
166
+ const insertSql = `INSERT INTO "${tableName}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
167
+ for (const row of tableData.rows) {
168
+ const values = columns.map((c) => stringify(row[c]));
169
+ sql.exec(insertSql, ...values);
170
+ rowsInserted++;
171
+ }
172
+ }
173
+ return { tables_created: tablesCreated, rows_inserted: rowsInserted };
174
+ }
175
+ handleIntrospect() {
176
+ const sql = this.ctx.storage.sql;
177
+ const tableRows = sql.exec("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%' AND name NOT LIKE '__miniflare%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%' ORDER BY name").toArray();
178
+ const tables = tableRows.map((t) => {
179
+ const cols = sql.exec(`PRAGMA table_info("${t.name}")`).toArray();
180
+ const count = sql.exec(`SELECT count(*) as c FROM "${t.name}"`).toArray()[0].c;
181
+ return {
182
+ name: t.name,
183
+ rowCount: count,
184
+ columns: cols.map((c) => ({ name: c.name, type: c.type, notnull: !!c.notnull, pk: !!c.pk })),
185
+ };
186
+ });
187
+ return { tables };
188
+ }
189
+ handleExport() {
190
+ const sql = this.ctx.storage.sql;
191
+ const tableRows = sql.exec("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%' AND name NOT LIKE '__miniflare%' AND name NOT LIKE '%_fts' AND name NOT LIKE '%_fts_%'").toArray();
192
+ const tables = {};
193
+ for (const { name, sql: ddl } of tableRows) {
194
+ const rows = sql.exec(`SELECT * FROM "${name}"`).toArray();
195
+ tables[name] = { ddl, rows };
196
+ }
197
+ return { tables };
198
+ }
199
+ }