@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,991 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, cpSync, rmSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { ensureDevEmail, getAccountToken } from "./login.js";
5
+ import { templates, skillTemplates, docTemplates } from "../templates.js";
6
+ import { readFilesManifest, readDatabasesManifest, writeFilesManifest, writeDatabasesManifest, computeFileSha256 } from "../manifest.js";
7
+ import { checkCliVersion } from "../version-check.js";
8
+ import { migrateConfig, configNeedsMigration } from "../connector-migration.js";
9
+ const MUG_START = "<!-- mug:start -->";
10
+ const MUG_END = "<!-- mug:end -->";
11
+ function loadMugJson(dir) {
12
+ const path = join(dir, "mug.json");
13
+ if (!existsSync(path)) {
14
+ console.error("No mug.json found. Run `mug init` first.");
15
+ process.exit(1);
16
+ }
17
+ return JSON.parse(readFileSync(path, "utf-8"));
18
+ }
19
+ function generateInstructions(config, cwd) {
20
+ const name = config.name;
21
+ const databases = config.databases ?? {};
22
+ const sources = config.sources ?? {};
23
+ const workflows = config.workflows ?? {};
24
+ let md = `## Mug Workspace: ${name}\n\n`;
25
+ md += `This is a Mug workspace. Write TypeScript using Mug's framework APIs.\n\n`;
26
+ md += `### Framework APIs\n\n`;
27
+ md += `**Sources** — sync external data (use \`/connector\` skill for guided setup):\n`;
28
+ md += `- \`import { source } from "@mugwork/mug"\`\n`;
29
+ md += `- \`source({ name, database, tables: [{ name, primaryKey, fetch(ctx) }] })\`\n`;
30
+ md += `- \`ctx.credential(name?)\` — returns API key/token from workspace secrets\n`;
31
+ md += `- Place connector files in \`connectors/\` — auto-discovered by \`mug deploy\`\n`;
32
+ md += `- Trigger: \`curl -X POST http://localhost:8787/sync/my-api\`\n\n`;
33
+ md += `**AI** — multi-provider with tier-based routing (use \`/ai\` skill for guided setup, \`.mug/docs/ai.md\` for full reference):\n`;
34
+ md += `- \`ctx.ai("fast", { prompt, system })\` — use workspace's fast model (cheapest). **You must set \`prompt\` (user message) and \`system\` (system prompt).**\n`;
35
+ md += `- \`ctx.ai("balanced", { prompt, system })\` — mid-tier model. \`ctx.ai("powerful", { prompt, system })\` — strongest model.\n`;
36
+ md += `- \`ctx.ai("auto", { prompt, system })\` — Mug picks the tier based on prompt complexity\n`;
37
+ md += `- \`ctx.ai("provider/model", { prompt, system })\` — direct call: \`"openai/gpt-5.4-nano"\`, \`"anthropic/claude-sonnet-4-6"\`\n`;
38
+ md += `- Tiers configured in \`mug.json\` \`ai.routing\` (fast/balanced/powerful → provider/model)\n`;
39
+ md += `- **BYOK**: \`mug secret set ai.anthropic=<key>\` → set \`"ai.anthropic"\` in \`mug.json\` \`ai.billing\` for unlimited AI at zero Mug credit cost\n`;
40
+ md += `- Per-call billing: \`billing: "ai.anthropic"\` or \`billing: "mug-metered"\`. Per-workflow: \`workflow(name, handler, { billing: "ai.anthropic" })\`\n\n`;
41
+ md += `**Search** — find relevant data across synced sources:\n`;
42
+ md += `- FTS5 auto-indexing: every synced text column gets a full-text index automatically. Query via \`ctx.query(db, "SELECT * FROM {table}_fts WHERE {table}_fts MATCH ? ORDER BY rank", [keyword])\`\n`;
43
+ md += `- \`ctx.search(query, { source?, limit? })\` — semantic similarity search. Returns \`{ score, table, primaryKey, row }[]\`. Requires deployed workspace (uses Vectorize).\n`;
44
+ md += `- \`ctx.ask(question, { source?, limit?, model?, system? })\` — full RAG: searches relevant data, feeds to LLM, returns grounded answer. Returns \`{ answer, sources, usage }\`.\n`;
45
+ md += `- Three layers: FTS5 for keyword search (free, local), \`ctx.search()\` for semantic (deployed), \`ctx.ask()\` for natural language answers (deployed).\n`;
46
+ md += `- See \`.mug/docs/api.md\` for full signatures and \`.mug/docs/ai.md\` for when to use each layer.\n\n`;
47
+ md += `**Workflows** — multi-step automation (use \`/workflow\` skill for guided setup):\n`;
48
+ md += `- \`workflow(name, async (ctx) => { ... }, options?)\` — register a workflow. Options: \`{ description?, billing?, webhook?: true | { auth, secret }, inbound?: "sms" | "email" | "slack", trigger?: { source, table?, on?, includeInitialSync? } }\`\n`;
49
+ md += `- Every workflow must have a \`description\` in options. Add \`//\` comments above each \`ctx.*\` call and \`return\` — these appear as step descriptions in the explorer.\n`;
50
+ md += `- \`ctx.query(database, sql, params?)\` — read from any workspace database\n`;
51
+ md += `- \`ctx.exec(database, sql, params?)\` — write to any workspace database\n`;
52
+ md += `- \`ctx.ai()\` — see **AI** section above\n`;
53
+ md += `- \`ctx.notify.email({ to, message, subject?, fromName?, cta? })\` / \`.sms({ to, message })\` / \`.slack({ to, message, blocks?, thread_ts? })\` / \`.channel(name, options)\` for custom channels\n`;
54
+ md += `- \`ctx.surfaceUrl(surfaceId, path?)\` — generate URL to a surface (dev/prod-aware)\n`;
55
+ md += `- \`ctx.file(path)\` — read a file from \`files/\` as ArrayBuffer (R2 in prod, local in dev)\n`;
56
+ md += `- \`ctx.fileText(path)\` — read a file as UTF-8 string\n`;
57
+ md += `- \`ctx.secret(name)\` — read a workspace secret by name (from \`.mug/secrets\`). Throws if not found.\n`;
58
+ md += `- \`ctx.waitFor(eventName, options?)\` — pause workflow until an external event arrives. Returns \`{ payload, type, timedOut }\`. Options: \`{ timeout?: "1 hour" | "24 hours", message?: string }\`. Zero cost while waiting.\n`;
59
+ md += `- \`ctx.waitForUrl(eventName)\` — generate a one-time callback URL for embedding in notifications. Returns a URL that, when visited, sends the event to the waiting workflow.\n`;
60
+ md += `- \`ctx.http(url, options?)\` — outbound HTTP request. Returns \`{ status, headers, body, json, ok }\`. Throws on non-2xx by default (\`throwOnError: false\` to handle manually). Auto-retries connection errors and 429 with exponential backoff. Options: \`{ method?, headers?, body?, throwOnError?, retry?: { attempts? } | false, timeout?, sign?: { secret, header? } }\`\n`;
61
+ md += `- \`ctx.respond(body, status?)\` — set a custom HTTP response for webhook-triggered workflows. First call wins. Use for Slack challenge verification, Twilio TwiML, etc. If not called, webhook returns default \`{ ok: true }\`.\n`;
62
+ md += `- Place workflow files in \`workflows/\` — auto-discovered by \`mug deploy\`\n\n`;
63
+ md += `**Notifications** — email and SMS from workflows (use \`/notify\` skill for guided setup):\n`;
64
+ md += `- \`ctx.notify.email({ to, message, subject?, fromName?, cta?: { label, url } })\` — styled HTML email with optional CTA button\n`;
65
+ md += `- \`ctx.notify.sms({ to, message })\` — SMS via Twilio (E.164 format)\n`;
66
+ md += `- \`ctx.surfaceUrl("approvals")\` — returns correct URL for dev (\`localhost:8787\`) or prod (\`workspace.mug.work\`)\n`;
67
+ md += `- In local dev, emails send via Resend (real delivery) + console log\n`;
68
+ md += `- BYOK: set your own Resend/Twilio keys via \`mug secret set\` for unlimited sends\n`;
69
+ md += `- **Usage limits**: email/SMS/AI have per-tier limits. \`ctx.notify\` and \`ctx.ai\` throw when limits are exceeded — catch in workflows to handle gracefully. \`mug buy-pack\` or \`mug workspace plan\` to increase limits. BYOK bypasses metered limits. See \`.mug/docs/billing.md\` for tier details.\n\n`;
70
+ md += `**Slack surface** — send Block Kit messages, slash commands, interactive buttons, Home Tab (use \`/slack\` skill for guided setup):\n`;
71
+ md += `- \`ctx.notify.slack({ to, message, blocks?, thread_ts? })\` — send messages with raw Block Kit. \`to\` is a channel ID or name.\n`;
72
+ md += `- \`ctx.slack.updateMessage({ channel, ts, text?, blocks? })\` — update a message after an action (replace buttons with result)\n`;
73
+ md += `- \`ctx.slack.openModal({ triggerId, view })\` — open a modal from a slash command workflow\n`;
74
+ md += `- Action ID convention: \`mug:<workflow>:<action>\` routes button clicks to specific workflows\n`;
75
+ md += `- Slash command trigger: \`"trigger": { "type": "slack_command", "command": "/dispatch" }\` in workflow config\n`;
76
+ md += `- Event trigger: \`"trigger": { "type": "slack_event", "event": "message" }\` in workflow config\n`;
77
+ md += `- Slack data auto-synced: \`slack_users\` (user_id, email, display_name) and \`slack_channels\` (channel_id, name) tables\n`;
78
+ md += `- Block Kit reference: agents already know Block Kit — use it directly, no Mug abstraction layer\n`;
79
+ md += `- Slack API docs (LLM-optimized): \`https://docs.slack.dev/apis/web-api.md\`\n\n`;
80
+ md += `**Inbound messages** — receive SMS replies, email replies, Slack interactions:\n`;
81
+ md += `- Configure in workflow options: \`workflow("handle-sms", handler, { inbound: "sms" })\`\n`;
82
+ md += `- Webhook URLs shown after \`mug deploy\`: \`https://api.mug.work/inbound/sms/<workspace>\`, etc. Run \`mug webhooks\` to see all URLs.\n`;
83
+ md += `- SMS workflow receives: \`ctx.params = { channel: "sms", from: "+1...", body: "YES", messageSid: "SM..." }\`\n`;
84
+ md += `- Email workflow receives: \`ctx.params = { channel: "email", from: "user@...", subject: "Re: ...", body: "..." }\`\n`;
85
+ md += `- Slack workflow receives: \`ctx.params = { channel: "slack", userId: "U...", actionId: "approve_btn", actionValue: "..." }\`\n`;
86
+ md += `- **Pattern**: "send and wait for reply" uses two workflows — one sends + sets a status field in DB, the other catches the inbound reply + checks that field to determine what to do\n\n`;
87
+ md += `**Forms** — collect data from users (use \`/form\` skill for guided setup):\n`;
88
+ md += `- Config-driven JSON files in \`surfaces/<name>.json\` with \`"type": "form"\` — form is live immediately in \`mug dev\`\n`;
89
+ md += `- Field types: text, email, phone, number, select, multiselect, date, textarea, file, calculated, hidden\n`;
90
+ md += `- **Field names must be unique across the entire form** (including across pages and showWhen fields) — duplicates cause silent data loss. \`mug form validate\` catches this.\n`;
91
+ md += `- Field properties: \`default\` (static value), \`prefill\` (from auth row, URL param, or DB), \`locked\` (read-only + server-enforced), \`helpText\` (hint below label), \`validate\` (custom rules with error messages)\n`;
92
+ md += `- Validation + help text support \`{{column}}\` templates resolved from auth row (e.g., \`helpText: "You have {{pto_hours}} hours available"\`)\n`;
93
+ md += `- Conditional fields: \`showWhen: [{ field, op, value }]\` (ops: eq, neq, in, gt, lt, filled, empty)\n`;
94
+ md += `- Multi-page: \`pages: [{ id, title, fields }]\` with optional branching\n`;
95
+ md += `- Access modes: \`public\` (anyone), \`identify\` (verify email/phone), \`auth\` (restricted to known users — also provides auth row for prefill)\n`;
96
+ md += `- Handler workflow receives submissions via \`ctx.params\`. Place handler in \`workflows/handle-<name>.ts\` — auto-discovered.\n`;
97
+ md += `- Auth prefill: \`prefill: { source: "auth", column: "name" }\` auto-fills from the user's auth row. Combine with \`locked: true\` for identity fields. Use \`access.query\` with \`:identity\` placeholder for computed columns (JOINs, calculations) — no denormalized state needed.\n`;
98
+ md += `- Dynamic forms: \`ctx.collect(options)\` creates forms programmatically at runtime (advanced — see \`.mug/docs/forms.md\`)\n`;
99
+ md += `- Breadcrumbs: add \`?from=/portal&fromLabel=Back to Portal\` to surface links — target surface shows a back link above the header\n\n`;
100
+ md += `**Portals** — tabbed, section-based data pages (use \`/portal\` skill for guided setup):\n`;
101
+ md += `- Config-driven JSON files in \`surfaces/<name>.json\` with \`"type": "portal"\`\n`;
102
+ md += `- \`tabs\` — array of tabs, each with \`id\`, \`label\`, and \`sections\` array. Single-tab portals hide the tab bar. Tab options: \`color\` (text/underline color), \`countQuery\` (dynamic badge count).\n`;
103
+ md += `- Top-level \`sections\` on the config render above the tab bar and stay visible on all tabs. Stats items support \`href\` to make cards clickable links (e.g., \`"href": "?tab=pending"\`).\n`;
104
+ md += `- Section types: \`table\` (paginated rows → detail pages → action buttons), \`stats\` (metric cards), \`progress\` (progress bars), \`text\` (markdown), \`chart\` (bar/donut SVG), \`gallery\` (image grid), \`accordion\` (collapsible container)\n`;
105
+ md += `- Table sections have \`query\`, \`columns\`, \`detail\`, \`actions\`, \`pageSize\`. Use \`:user\` in SQL for session identity, \`:auth.column\` for any auth table column.\n`;
106
+ md += `- Action button styles: \`"success"\` (green), \`"danger"\` (red), \`"warning"\` (amber), \`"primary"\` (accent), \`"default"\` (gray). Add \`"color": "#hex"\` for custom override. Use success/danger for approve/deny.\n`;
107
+ md += `- Actions are **optimistic** — button replaced instantly with status message. \`afterMessage\`: custom text (\`{{timestamp}}\` for date/time). \`afterColor\`: hex color for the status message. \`afterActions\`: follow-up buttons (e.g., revoke/undo) that fire the same workflow.\n`;
108
+ md += `- **Timeline playback**: action buttons can trigger scripted visual sequences instead of workflows. Set \`"timeline"\` (array) instead of \`"workflow"\` (string). Events: \`toast\` (notification popup), \`preview\` (email/SMS card), \`stream\` (char-by-char AI-style text), \`update\` (swap table cell value), \`highlight\` (pulse-highlight row via \`row\` or button via \`action\`, with optional \`message\` badge). Each event has \`delay\` (seconds from trigger). Button disabled during playback.\n`;
109
+ md += `- **Autoplay**: portal config accepts \`"autoplay": []\` — same event array, fires on page load. Use for guided demos (\`highlight\` with \`delay: 0\`) or AI summary streams on dashboards.\n`;
110
+ md += `- **Embed mode**: append \`?embed=true\` to any surface URL to strip header chrome (logo, session, logout, breadcrumbs) for iframe embedding. CSP allows framing from \`mug.work\` and \`*.mug.work\`.\n`;
111
+ md += `- Stats \`valueColor\`: \`"match"\` (inherit border color — default when \`color\` set), \`"neutral"\` (dark text), or hex. Badge \`badgeColors\`: \`{ "PTO": "#3b82f6" }\` for custom non-status values.\n`;
112
+ md += `- Access modes: same as forms (\`public\`, \`identify\`, \`auth\`)\n`;
113
+ md += `- Column \`dateFormat\`: \`"short"\` for compact dates (\`5/15/26, 3:30 PM\`). Default is long format (\`May 15, 2026, 3:30 PM\`).\n`;
114
+ md += `- Column/field/stat formats: \`"currency"\` ($X,XXX.XX in tables, whole in stats), \`"currency-whole"\` ($X,XXX), \`"currency-short"\` ($12.5K/$1.2M), \`"number"\` (comma-separated), \`"percent"\` (XX%). Stat values that overflow auto-abbreviate to K/M.\n`;
115
+ md += `- Chart sections: \`labelColumn\`/\`valueColumn\` default to \`"label"\`/\`"value"\`. \`colors\`: \`{ "complete": "#16a34a", "error": "#ef4444" }\` maps label values to colors.\n`;
116
+ md += `- All \`date\`/\`datetime\` columns render in the workspace timezone (\`settings.timezone\` in mug.json). Override per-surface with \`"timezone": "America/New_York"\` in the surface JSON.\n\n`;
117
+ md += `**Demo mode** — share deployed auth'd surfaces with stakeholders without requiring them to verify:\n`;
118
+ md += `- \`mug demo enable <surface> --as <email-or-phone>\` — surface renders pre-authenticated as that identity\n`;
119
+ md += `- Use \`_home\` as the surface ID to demo the workspace home screen (e.g. \`mug demo enable _home --as demo@example.com\`)\n`;
120
+ md += `- \`mug demo disable <surface>\` / \`mug demo status\` — manage active demos\n`;
121
+ md += `- Notification routing (automatic, no code guards needed):\n`;
122
+ md += ` - \`--notify demo-user\` (default) — notifications redirect to \`--as\` identity (matching channels only, others suppressed)\n`;
123
+ md += ` - \`--notify dev\` — notifications redirect to the developer's account email\n`;
124
+ md += ` - \`--notify off\` — suppress all notifications (logged but not sent)\n`;
125
+ md += ` - Per-channel overrides: \`--email-to <addr>\`, \`--sms-to <phone>\`, \`--slack-to <channel>\` (override any mode)\n`;
126
+ md += `- \`--no-workflows\` — suppress workflow execution entirely (surface still renders and accepts input)\n`;
127
+ md += `- \`ctx.isDemo\` is \`true\` in demo workflows — use to guard non-notification side effects (destructive writes, external API calls)\n`;
128
+ md += `- Create a demo persona (e.g. \`demo@example.com\`) in your auth table with curated data. The demo viewer sees exactly what that user would see.\n\n`;
129
+ md += `**Branding** — custom logo and accent color on all surfaces and emails:\n`;
130
+ md += `- Configure in \`mug.json\`: \`"branding": { "logo": "assets/logo.png", "logoSquare": "assets/icon.png", "accentColor": "#1a5276", "ogImage": "assets/og-image.png" }\`\n`;
131
+ md += `- Two logo variants: \`logo\` (rectangle, for headers) and \`logoSquare\` (square, for icons). If only one provided, used everywhere.\n`;
132
+ md += `- \`ogImage\` — custom 1200x630 PNG for link previews (Slack, iMessage, LinkedIn, Discord). Falls back to Mug default if not set.\n`;
133
+ md += `- Logo paths are relative to workspace root. Uploaded to R2 on \`mug deploy\`, served at \`https://<workspace>.mug.work/_branding/\`\n`;
134
+ md += `- Accent color applies to buttons, links, focus rings, progress bars via CSS variable \`--accent\`. Also tints the browser favicon.\n`;
135
+ md += `- All surfaces automatically emit Open Graph and Twitter Card meta tags (title, description, image) for link previews.\n`;
136
+ md += `- Emails automatically use workspace branding (logo in header, accent on CTA buttons)\n`;
137
+ md += `- **Per-surface overrides**: add \`"branding": { "logoSquare": "files/logo.png", "accentColor": "#2563eb" }\` to any surface JSON to override workspace branding for that surface. Surface branding takes precedence; workspace branding is the fallback.\n`;
138
+ md += `- In dev, logos served from local files. Changes to mug.json branding hot-reload.\n\n`;
139
+ md += `**Home Screen** — workspace root URL (\`subdomain.mug.work/\`) shows branded auth + surface directory:\n`;
140
+ md += `- By default, the home screen requires authentication — visitors verify via email/phone before seeing content.\n`;
141
+ md += `- **Public mode**: add \`"access": { "mode": "public" }\` to \`_home.json\` to show public surfaces without auth. Optional \`"showLogin": true\` adds a "Log in" button (only renders if at least one surface has non-public access).\n`;
142
+ md += `- Auth is configless: scans workspace owner/admin + all surface auth tables. No setup needed.\n`;
143
+ md += `- Auth flow: demo mode check → session cookie check → auth gate (email/phone verification).\n`;
144
+ md += `- After login, shows a directory of surfaces the user has access to.\n`;
145
+ md += `- Demo mode: \`mug demo enable _home --as <identity>\` — the home screen is surface ID \`_home\` for demo purposes.\n`;
146
+ md += `- Configure layout in \`surfaces/_home.json\`: \`{ "title": "...", "description": "...", "groups": [...] }\`\n`;
147
+ md += `- \`title\` → rendered as heading in header, page title, OG meta (fallback: title-cased workspace slug). \`description\` → rendered below title in header + OG/Twitter meta.\n`;
148
+ md += `- Groups combine a button row (actions) with card list (destinations):\n`;
149
+ md += ` \`{ "label": "HR", "description": "...", "icon": "users", "buttons": [{ "surface": "time-off", "label": "Request Time Off" }], "cards": [{ "surface": "portal", "label": "Dashboard", "description": "...", "icon": "layout-dashboard" }] }\`\n`;
150
+ md += `- Optional \`color\` on groups (tints border + bg + title + icons), buttons (bg override), cards (colored border + tint).\n`;
151
+ md += `- Optional \`icon\` on groups and cards — any [Lucide icon](https://lucide.dev) name (e.g., \`"hard-hat"\`, \`"receipt"\`, \`"bar-chart-3"\`).\n`;
152
+ md += `- Optional \`"collapsible": true\` on groups — renders as accordion. \`"collapsed": true\` starts closed (default: open).\n`;
153
+ md += `- Named color palette: Red \`#dc2626\`, Orange \`#ea580c\`, Gold \`#ca8a04\`, Brown \`#92400e\`, Green \`#16a34a\`, Teal \`#0f766e\`, Blue \`#2563eb\`, Navy \`#1e3a8a\`, Purple \`#7c3aed\`, Pink \`#db2777\`, Maroon \`#6b2140\`, Gray \`#475569\`.\n`;
154
+ md += `- Surfaces not in any group appear at the bottom as default cards.\n`;
155
+ md += `- No \`_home.json\` = all surfaces shown alphabetically as cards.\n`;
156
+ md += `- Uses workspace branding (logo, accent color, OG meta). Fully mobile responsive.\n`;
157
+ md += `- In dev, \`localhost:8787/\` shows home screen with all surfaces and dev identity. Changes to \`_home.json\` hot-reload.\n\n`;
158
+ md += `**CLI commands:**\n`;
159
+ md += `\`\`\`bash\n`;
160
+ md += `mug dev # start local dev server (auto-detects ports from 8787)\n`;
161
+ md += `mug dev --port <port> # pin to a specific port\n`;
162
+ md += `mug dev --tunnel # expose via Cloudflare Quick Tunnel (requires cloudflared)\n`;
163
+ md += `mug shutdown # gracefully stop the running dev server\n`;
164
+ md += `mug run <workflow> # run workflow locally\n`;
165
+ md += `mug run <workflow> --production # run in production (CF Workflows)\n`;
166
+ md += `mug logs [workflow] # view execution log (--json, --limit N, --production)\n`;
167
+ md += `mug status <workflow> <instanceId> # check production status (--json)\n`;
168
+ md += `mug sql <db> <sql> # run SQL against databases/<db>.db (--json, --production, --dev, --port)\n`;
169
+ md += `mug usage # usage across all 6 billing dimensions (--json, --period)\n`;
170
+ md += `mug workspace plan # view or change workspace plan tier (opens Stripe for paid tiers)\n`;
171
+ md += `mug buy-pack # purchase 25% overage pack for current period\n`;
172
+ md += `mug billing # view/update billing settings (--auto-packs, --max-packs, --email)\n`;
173
+ md += `mug deploy # deploy to production (includes database push)\n`;
174
+ md += `mug push databases/<name> # upload local database to production\n`;
175
+ md += `mug push --all # upload all local files and databases\n`;
176
+ md += `mug push --all --force # push all without confirmation prompt\n`;
177
+ md += `mug pull databases/<name> # download production database locally\n`;
178
+ md += `mug pull --all # download all remote files and databases\n`;
179
+ md += `mug form init <name> # scaffold form + handler workflows\n`;
180
+ md += `mug form validate # check form schemas for errors\n`;
181
+ md += `mug form list # list forms and URLs\n`;
182
+ md += `mug portal init <name> # scaffold portal config in surfaces/\n`;
183
+ md += `mug portal list # list portal surfaces and URLs\n`;
184
+ md += `mug secret set KEY=VALUE # store secret (--production to sync)\n`;
185
+ md += `mug secret list # show stored secret keys\n`;
186
+ md += `mug secret remove KEY # remove a secret\n`;
187
+ md += `mug connector search <query> # search community connector catalog\n`;
188
+ md += `mug connector pull --slug <name> # download connector spec from catalog\n`;
189
+ md += `mug connector discover <product> # record API availability\n`;
190
+ md += `mug connector gather --slug <name> # produce OpenAPI spec\n`;
191
+ md += `mug connector verify --slug <name> # verify against live API\n`;
192
+ md += `mug connector scaffold --slug <name> # generate TypeScript source\n`;
193
+ md += `mug connector init <product> # full pipeline (discover→gather→verify→scaffold)\n`;
194
+ md += `mug auth <provider> # connect provider via OAuth (e.g. airtable)\n`;
195
+ md += `mug demo enable <surface> --as <id> # share surface pre-auth'd (--expires, --notify, --no-workflows)\n`;
196
+ md += `mug demo disable <surface> # revoke demo access\n`;
197
+ md += `mug demo status # show active demos\n`;
198
+ md += `mug webhooks # list webhook URLs, inbound channels, event triggers\n`;
199
+ md += `mug brain <agent> # inspect agent brain memory (struggles, entities, outcomes, sessions)\n`;
200
+ md += `mug brain <agent> struggles # review unresolved struggles — knowledge gaps and edge cases\n`;
201
+ md += `mug login # authenticate via email verification\n`;
202
+ md += `mug create workspace <name> # register workspace (--subdomain, --tier free|starter|pro|business)\n`;
203
+ md += `mug workspace status # workspace info and plan tier\n`;
204
+ md += `mug workspace plan # view or change plan tier\n`;
205
+ md += `mug workspace invite <email> # invite admin to workspace\n`;
206
+ md += `mug workspace transfer <email> # transfer ownership (sends invite)\n`;
207
+ md += `mug workspace remove <email> # remove member\n`;
208
+ md += `mug workspace members # list members + pending invites\n`;
209
+ md += `mug workspace cancel-invite <id> # cancel a pending invite\n`;
210
+ md += `mug workspace check-subdomain <slug> # check subdomain availability\n`;
211
+ md += `mug workspace archive # archive workspace (365-day retention)\n`;
212
+ md += `mug workspace restore # restore an archived workspace\n`;
213
+ md += `mug workspace delete # permanently delete archived workspace\n`;
214
+ md += `mug workspace export # export workspace data (--categories, --all) as .tar\n`;
215
+ md += `mug account email <new-email> # change email (verifies both old and new)\n`;
216
+ md += `mug account invites # show pending incoming and sent invites\n`;
217
+ md += `mug account accept <id> # accept a workspace invite\n`;
218
+ md += `mug account decline <id> # decline a workspace invite\n`;
219
+ md += `mug whoami # show account, workspaces, and pending invites\n`;
220
+ md += `mug issue # file a bug report or feature request\n`;
221
+ md += `\`\`\`\n`;
222
+ md += `Use \`/mug dev\`, \`/mug sync\`, or \`/mug deploy\` to run these commands via the \`/mug\` skill.\n\n`;
223
+ md += `### Building Sources\n\n`;
224
+ md += `Use \`/connector <product>\` to build a source — it walks through the full pipeline step by step.\n\n`;
225
+ md += `The pipeline: **discover → gather → verify → scaffold**. You research the API, the CLI handles storage, normalization, and code generation. Run \`mug connector --help\` for CLI reference.\n\n`;
226
+ md += `**Source config in mug.json** (metadata only — no credentials here):\n`;
227
+ md += `\`\`\`json\n`;
228
+ md += `"sources": {\n`;
229
+ md += ` "<name>": {\n`;
230
+ md += ` "auth": { "type": "bearer|api-key|basic|oauth2", "value": "<credential>" },\n`;
231
+ md += ` "baseUrl": "<API root URL>",\n`;
232
+ md += ` "syncs": { "<sync-name>": { "database": "<db>", "schedule": "*/15 * * * *" } }\n`;
233
+ md += ` }\n`;
234
+ md += `}\n`;
235
+ md += `\`\`\`\n\n`;
236
+ md += `### Credentials\n\n`;
237
+ md += `Store API keys and secrets in \`.mug/secrets\` (gitignored), not in mug.json:\n`;
238
+ md += `\`\`\`bash\n`;
239
+ md += `mug secret set AIRTABLE_API_KEY=pat_xxxxx # writes to .mug/secrets\n`;
240
+ md += `mug secret list # shows keys (not values)\n`;
241
+ md += `\`\`\`\n`;
242
+ md += `Secrets are loaded automatically by \`mug dev\` and sent to production by \`mug deploy\`.\n\n`;
243
+ md += `### Deploy to Production\n\n`;
244
+ md += `\`\`\`bash\n`;
245
+ md += `mug secret set MUG_API_KEY=<your-key> # one-time setup (workspace API key)\n`;
246
+ md += `mug dev # test locally first\n`;
247
+ md += `mug deploy # bundle and deploy to Cloudflare\n`;
248
+ md += `\`\`\`\n`;
249
+ md += `MUG_API_KEY is used for all production operations: deploy, run --production, status, usage.\n\n`;
250
+ md += `### Platform Docs\n\n`;
251
+ md += `Full API reference with detailed examples in \`.mug/docs/\`:\n`;
252
+ md += `- \`.mug/docs/api.md\` — **unified API reference**: all ctx.* methods, ctx.params shapes, error handling, data patterns, mug.json config, CLI quick reference\n`;
253
+ md += `- \`.mug/docs/sources.md\` — SourceDef, pagination, rate limits, sync config\n`;
254
+ md += `- \`.mug/docs/workflows.md\` — workflow registration, scheduling, two-workflow pattern, webhook workflows\n`;
255
+ md += `- \`.mug/docs/forms.md\` — field types, conditionals, multi-page, access modes, file uploads\n`;
256
+ md += `- \`.mug/docs/portals.md\` — portal tabs, section types (table/stats/progress/text/chart/gallery/accordion), actions, :user queries\n`;
257
+ md += `- \`.mug/docs/notifications.md\` — email templates, CTA buttons, markdown support, BYOK\n`;
258
+ md += `- \`.mug/docs/cli.md\` — complete CLI command reference\n`;
259
+ md += `- \`.mug/docs/demo.md\` — demo mode: notification routing, workflow suppression, ctx.isDemo\n`;
260
+ md += `- \`.mug/docs/slack.md\` — slack.json schema, Home Tab sections, shortcuts, unfurl, deploy process\n\n`;
261
+ md += `### Conventions\n\n`;
262
+ md += `- **Always filter deleted rows** when querying synced data: \`WHERE _mug_deleted_at IS NULL\`. Synced tables have \`_mug_synced_at\` and \`_mug_deleted_at\` system columns.\n`;
263
+ md += `- **databases/ is the local source of truth.** Each \`databases/<name>.db\` is a SQLite file. \`mug sql\` reads/writes these directly — no dev server needed. \`mug push\`/\`mug pull\` sync them with production.\n`;
264
+ md += `- When running \`mug dev\`, databases are seeded into Durable Objects on startup and written back on shutdown (also on \`mug shutdown\`). If data seems stale after a workflow run, the DO writeback may be pending — check \`databases/<name>.db\` directly.\n`;
265
+ md += `- The dev server has **hot reload** — file changes to surfaces, workflows, agents, and connectors auto-refresh the browser. The workspace explorer at \`/explorer\` updates live.\n`;
266
+ md += `- Test individual workflows via \`/_dev/run/<workflow-name>\` (POST) — returns step-by-step results with timing.\n`;
267
+ md += `- The \`_mug_ops\` database is implicit (workflow_runs, workflow_steps)\n`;
268
+ md += `- All \`ctx.*\` methods throw on failure — use try/catch for graceful error handling\n`;
269
+ md += `- Use \`"auto"\` for AI calls — smart routing picks the cheapest model that works\n`;
270
+ md += `- Workflows can be triggered by schedule, CLI (\`mug run\`), form submissions, portal actions, or webhooks\n\n`;
271
+ if (Object.keys(sources).length > 0) {
272
+ md += `### Sources\n\n`;
273
+ for (const [key, val] of Object.entries(sources)) {
274
+ const src = val;
275
+ const auth = src.auth;
276
+ const authType = auth?.type ?? "unknown";
277
+ const baseUrl = src.baseUrl ?? "";
278
+ const syncs = src.syncs ?? {};
279
+ const syncCount = Object.keys(syncs).length;
280
+ md += `- **${key}** — auth: ${authType}${baseUrl ? `, base: \`${baseUrl}\`` : ""}${syncCount > 0 ? `, ${syncCount} sync(s)` : ""}\n`;
281
+ }
282
+ md += `\n`;
283
+ }
284
+ if (Object.keys(databases).length > 0) {
285
+ md += `### Databases\n\n`;
286
+ for (const [dbName, val] of Object.entries(databases)) {
287
+ const db = val;
288
+ const tables = db.tables ? Object.keys(db.tables) : [];
289
+ md += `- **${dbName}** — tables: ${tables.length > 0 ? tables.join(", ") : "(none yet)"}\n`;
290
+ }
291
+ md += `\n`;
292
+ }
293
+ if (Object.keys(workflows).length > 0) {
294
+ md += `### Workflows\n\n`;
295
+ for (const [wfName, val] of Object.entries(workflows)) {
296
+ const wf = val;
297
+ const file = wf.file ?? `workflows/${wfName}.ts`;
298
+ const schedule = wf.schedule ? `schedule: \`${wf.schedule}\`` : "manual";
299
+ md += `- **${wfName}** — ${schedule} → \`${file}\`\n`;
300
+ }
301
+ md += `\n`;
302
+ }
303
+ md += `### Agents\n\n`;
304
+ md += `**Custom AI agents** — autonomous multi-step work with tools, memory, and structured output.\n\n`;
305
+ md += `Each agent is a folder in \`agents/<name>/\` with:\n`;
306
+ md += `- \`agent.json\` — config: model, tools, caps, memory, requireApproval\n`;
307
+ md += `- \`soul.md\` — core identity, role, and primary instructions (always loaded)\n`;
308
+ md += `- \`skills/\` — agent-specific skills (auto-discovered, loaded on demand)\n`;
309
+ md += `- \`brain.db\` — persistent memory (created on first deploy, managed by runtime)\n\n`;
310
+ md += `Shared skills available to all agents: \`agents/shared-skills/<skill-name>/SKILL.md\`\n\n`;
311
+ md += `**agent.json fields:**\n`;
312
+ md += `- \`model\`: \`"claude-sonnet"\` (fixed) or \`{ "fast": "claude-haiku", "balanced": "claude-sonnet", "powerful": "claude-opus" }\` (dynamic routing)\n`;
313
+ md += `- \`tools\`: \`["query", "search", "ask", "notify", "http", "workspace", "ai"]\`\n`;
314
+ md += `- \`memory\`: \`{ "entities": true, "outcomes": true, "struggles": true }\`\n`;
315
+ md += `- \`caps\`: \`{ "maxTurns": 30, "maxCredits": 200, "maxDuration": 300 }\`\n`;
316
+ md += `- \`requireApproval\`: \`["notify"]\` — pause for human approval before these tools\n\n`;
317
+ md += `**Invoke from workflows:** \`const result = await ctx.agent("name", { goal, context? })\`\n`;
318
+ md += `- \`result\`: \`{ response, output?, usage: { credits, turns, duration }, capped? }\`\n`;
319
+ md += `- Agents run autonomously (multiple turns with tools), unlike \`ctx.ai()\` which is single-shot\n\n`;
320
+ const agentsDir = cwd ? (existsSync(join(cwd, "agents")) ? join(cwd, "agents") : join(cwd, "src", "agents")) : null;
321
+ if (agentsDir && existsSync(agentsDir)) {
322
+ const agentFolders = readdirSync(agentsDir).filter((f) => {
323
+ if (f === "shared-skills")
324
+ return false;
325
+ const p = join(agentsDir, f);
326
+ return statSync(p).isDirectory() && existsSync(join(p, "agent.json"));
327
+ });
328
+ if (agentFolders.length > 0) {
329
+ md += `**Configured agents:**\n`;
330
+ for (const folder of agentFolders) {
331
+ const configPath = join(agentsDir, folder, "agent.json");
332
+ try {
333
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
334
+ const model = typeof config.model === "string" ? config.model : "dynamic routing";
335
+ const tools = Array.isArray(config.tools) ? config.tools.join(", ") : "none";
336
+ const agentBase = agentsDir.includes("/src/agents") ? "src/agents" : "agents";
337
+ md += `- **${folder}** → \`${agentBase}/${folder}/\` (model: ${model}, tools: ${tools})\n`;
338
+ }
339
+ catch {
340
+ const agentBase = agentsDir.includes("/src/agents") ? "src/agents" : "agents";
341
+ md += `- **${folder}** → \`${agentBase}/${folder}/\` (invalid agent.json)\n`;
342
+ }
343
+ }
344
+ md += `\n`;
345
+ }
346
+ const sharedSkillsDir = join(agentsDir, "shared-skills");
347
+ if (existsSync(sharedSkillsDir)) {
348
+ const sharedSkills = readdirSync(sharedSkillsDir).filter((f) => {
349
+ const p = join(sharedSkillsDir, f);
350
+ return statSync(p).isDirectory() && existsSync(join(p, "SKILL.md"));
351
+ });
352
+ if (sharedSkills.length > 0) {
353
+ md += `**Shared agent skills:** ${sharedSkills.join(", ")}\n\n`;
354
+ }
355
+ }
356
+ }
357
+ md += `### Workspace Structure\n\n`;
358
+ const isNew = cwd ? isNewStructure(cwd) : false;
359
+ if (isNew) {
360
+ md += `\`\`\`\n`;
361
+ md += `agents/ — AI agents (each agent is a folder with agent.json + soul.md + skills/)\n`;
362
+ md += `connectors/ — data sync from external APIs\n`;
363
+ md += `workflows/ — automation logic\n`;
364
+ md += `surfaces/ — forms, portals, dashboards (.json)\n`;
365
+ md += `files/ — static files synced to R2 (assets, templates, CSVs)\n`;
366
+ md += `databases/ — local SQLite files synced to production DOs (.db files gitignored)\n`;
367
+ md += `mug.json — workspace config\n`;
368
+ md += `slack.json — Slack app config\n`;
369
+ md += `\`\`\`\n`;
370
+ }
371
+ else {
372
+ md += `\`\`\`\n`;
373
+ md += `src/sources/ — data sync from external APIs\n`;
374
+ md += `src/workflows/ — automation logic\n`;
375
+ md += `src/agents/ — AI agents (each agent is a folder with agent.json + soul.md + skills/)\n`;
376
+ md += `files/ — static files synced to R2 (assets, templates, CSVs)\n`;
377
+ md += `databases/ — local SQLite files synced to production DOs (.db files gitignored)\n`;
378
+ md += `mug.json — workspace config\n`;
379
+ md += `\`\`\`\n`;
380
+ }
381
+ return md;
382
+ }
383
+ function injectBlock(content, block) {
384
+ const startIdx = content.indexOf(MUG_START);
385
+ const endIdx = content.indexOf(MUG_END);
386
+ if (startIdx === -1 || endIdx === -1)
387
+ return content;
388
+ return content.slice(0, startIdx + MUG_START.length) + "\n" + block + "\n" + content.slice(endIdx);
389
+ }
390
+ export function repairScaffolding(cwd) {
391
+ const dirs = ["files", "databases"];
392
+ for (const d of dirs) {
393
+ const dirPath = join(cwd, d);
394
+ if (!existsSync(dirPath)) {
395
+ mkdirSync(dirPath, { recursive: true });
396
+ console.log(`Created ${d}/`);
397
+ }
398
+ const remotePath = join(dirPath, ".remote");
399
+ if (!existsSync(remotePath)) {
400
+ const empty = d === "files"
401
+ ? { synced_at: null, files: {} }
402
+ : { synced_at: null, databases: {} };
403
+ writeFileSync(remotePath, JSON.stringify(empty, null, 2) + "\n");
404
+ console.log(`Created ${d}/.remote`);
405
+ }
406
+ }
407
+ // Ensure new-structure dirs exist if workspace uses new layout
408
+ if (isNewStructure(cwd)) {
409
+ for (const d of ["agents", "connectors", "workflows", "surfaces"]) {
410
+ if (!existsSync(join(cwd, d))) {
411
+ mkdirSync(join(cwd, d), { recursive: true });
412
+ }
413
+ }
414
+ if (!existsSync(join(cwd, "slack.json"))) {
415
+ writeFileSync(join(cwd, "slack.json"), JSON.stringify({ enabled: false }, null, 2) + "\n");
416
+ console.log("Created slack.json");
417
+ }
418
+ }
419
+ }
420
+ const API_URL = "https://api.mug.work";
421
+ function walkDir(dir) {
422
+ const results = [];
423
+ if (!existsSync(dir))
424
+ return results;
425
+ for (const entry of readdirSync(dir)) {
426
+ if (entry === ".remote")
427
+ continue;
428
+ const fullPath = join(dir, entry);
429
+ const stat = statSync(fullPath);
430
+ if (stat.isDirectory()) {
431
+ results.push(...walkDir(fullPath));
432
+ }
433
+ else {
434
+ results.push(fullPath);
435
+ }
436
+ }
437
+ return results;
438
+ }
439
+ export async function syncLocalFiles(cwd, config) {
440
+ const workspaceId = config.id;
441
+ const workspaceName = config.name;
442
+ if (!workspaceId)
443
+ return;
444
+ let token;
445
+ try {
446
+ token = getAccountToken();
447
+ }
448
+ catch {
449
+ return;
450
+ }
451
+ const filesDir = join(cwd, "files");
452
+ const localFiles = walkDir(filesDir);
453
+ if (localFiles.length === 0)
454
+ return;
455
+ const manifest = readFilesManifest(cwd);
456
+ const toUpload = [];
457
+ for (const localPath of localFiles) {
458
+ const remotePath = relative(filesDir, localPath).replace(/\\/g, "/");
459
+ const sha256 = computeFileSha256(localPath);
460
+ const existing = manifest.files[remotePath];
461
+ if (!existing || existing.sha256 !== sha256) {
462
+ toUpload.push({ localPath, remotePath, sha256 });
463
+ }
464
+ }
465
+ if (toUpload.length === 0)
466
+ return;
467
+ let uploaded = 0;
468
+ for (const file of toUpload) {
469
+ try {
470
+ const content = readFileSync(file.localPath);
471
+ const res = await fetch(`${API_URL}/workspace/${workspaceName}/files/upload`, {
472
+ method: "POST",
473
+ headers: {
474
+ Authorization: `Bearer ${token}`,
475
+ "X-File-Path": file.remotePath,
476
+ "X-File-SHA256": file.sha256,
477
+ },
478
+ body: content,
479
+ signal: AbortSignal.timeout(30000),
480
+ });
481
+ if (res.ok) {
482
+ manifest.files[file.remotePath] = {
483
+ size: content.byteLength,
484
+ sha256: file.sha256,
485
+ updated_at: new Date().toISOString(),
486
+ };
487
+ uploaded++;
488
+ }
489
+ else {
490
+ console.warn(` Failed to upload ${file.remotePath}: ${res.statusText}`);
491
+ }
492
+ }
493
+ catch (err) {
494
+ console.warn(` Failed to upload ${file.remotePath}: ${err instanceof Error ? err.message : err}`);
495
+ }
496
+ }
497
+ if (uploaded > 0) {
498
+ manifest.synced_at = new Date().toISOString();
499
+ writeFilesManifest(cwd, manifest);
500
+ console.log(`Uploaded ${uploaded} file${uploaded !== 1 ? "s" : ""} (${toUpload.map((f) => f.remotePath).join(", ")})`);
501
+ }
502
+ }
503
+ export async function syncLocalDatabases(cwd, config) {
504
+ const workspaceId = config.id;
505
+ const workspaceName = config.name;
506
+ if (!workspaceId)
507
+ return;
508
+ let token;
509
+ try {
510
+ token = getAccountToken();
511
+ }
512
+ catch {
513
+ return;
514
+ }
515
+ const dbDir = join(cwd, "databases");
516
+ if (!existsSync(dbDir))
517
+ return;
518
+ const dbFiles = readdirSync(dbDir).filter((f) => f.endsWith(".db"));
519
+ if (dbFiles.length === 0)
520
+ return;
521
+ const manifest = readDatabasesManifest(cwd);
522
+ let uploaded = 0;
523
+ for (const dbFile of dbFiles) {
524
+ const dbPath = join(dbDir, dbFile);
525
+ const dbName = dbFile.replace(/\.db$/, "");
526
+ const localSha = computeFileSha256(dbPath);
527
+ const existing = manifest.databases[dbName];
528
+ if (existing && existing.updated_at) {
529
+ const localMtime = statSync(dbPath).mtime.toISOString();
530
+ if (existing.updated_at >= localMtime)
531
+ continue;
532
+ }
533
+ let db;
534
+ try {
535
+ db = new Database(dbPath, { readonly: true });
536
+ }
537
+ catch (err) {
538
+ console.warn(` Skipping ${dbFile}: not a valid SQLite database`);
539
+ continue;
540
+ }
541
+ try {
542
+ const tableRows = db.prepare("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
543
+ const tables = {};
544
+ for (const { name, sql: ddl } of tableRows) {
545
+ const rows = db.prepare(`SELECT * FROM "${name}"`).all();
546
+ tables[name] = { ddl, rows };
547
+ }
548
+ const res = await fetch(`${API_URL}/workspace/${workspaceName}/db/${dbName}/upload`, {
549
+ method: "POST",
550
+ headers: {
551
+ Authorization: `Bearer ${token}`,
552
+ "Content-Type": "application/json",
553
+ },
554
+ body: JSON.stringify({ tables }),
555
+ signal: AbortSignal.timeout(60000),
556
+ });
557
+ if (res.ok) {
558
+ const result = (await res.json());
559
+ manifest.databases[dbName] = {
560
+ size_mb: Math.round(statSync(dbPath).size / 1024 / 1024 * 10) / 10,
561
+ tables: Object.fromEntries(Object.entries(tables).map(([tName, tData]) => [
562
+ tName,
563
+ {
564
+ columns: db.prepare(`PRAGMA table_info("${tName}")`).all().map((c) => ({
565
+ name: String(c.name),
566
+ type: String(c.type || "TEXT"),
567
+ })),
568
+ row_count: tData.rows.length,
569
+ },
570
+ ])),
571
+ updated_at: new Date().toISOString(),
572
+ };
573
+ uploaded++;
574
+ console.log(` ${dbName}: ${result.tables_created} table${result.tables_created !== 1 ? "s" : ""}, ${result.rows_inserted} row${result.rows_inserted !== 1 ? "s" : ""}`);
575
+ }
576
+ else {
577
+ console.warn(` Failed to upload ${dbName}: ${res.statusText}`);
578
+ }
579
+ }
580
+ finally {
581
+ db.close();
582
+ }
583
+ }
584
+ if (uploaded > 0) {
585
+ manifest.synced_at = new Date().toISOString();
586
+ writeDatabasesManifest(cwd, manifest);
587
+ console.log(`Uploaded ${uploaded} database${uploaded !== 1 ? "s" : ""}`);
588
+ }
589
+ }
590
+ async function syncRemoteManifests(cwd, config) {
591
+ const workspaceId = config.id;
592
+ const workspaceName = config.name;
593
+ if (!workspaceId)
594
+ return;
595
+ let token;
596
+ try {
597
+ token = getAccountToken();
598
+ }
599
+ catch {
600
+ return;
601
+ }
602
+ const databases = config.databases ?? {};
603
+ const sourcesConfig = config.sources ?? {};
604
+ const dbNames = new Set(Object.keys(databases));
605
+ for (const src of Object.values(sourcesConfig)) {
606
+ for (const sync of Object.values(src.syncs ?? {})) {
607
+ if (sync.database)
608
+ dbNames.add(sync.database);
609
+ }
610
+ }
611
+ dbNames.add("_mug_ops");
612
+ const dbParam = [...dbNames].join(",");
613
+ try {
614
+ const res = await fetch(`${API_URL}/workspace/${workspaceName}/manifest?databases=${dbParam}`, {
615
+ headers: { Authorization: `Bearer ${token}` },
616
+ signal: AbortSignal.timeout(10000),
617
+ });
618
+ if (!res.ok) {
619
+ if (res.status === 401)
620
+ return;
621
+ console.warn(` Remote manifest fetch failed: ${res.statusText}`);
622
+ return;
623
+ }
624
+ const data = (await res.json());
625
+ const filesManifest = {
626
+ synced_at: new Date().toISOString(),
627
+ files: {},
628
+ };
629
+ for (const f of data.files) {
630
+ filesManifest.files[f.path] = { size: f.size, sha256: f.sha256, updated_at: f.uploaded_at };
631
+ }
632
+ writeFilesManifest(cwd, filesManifest);
633
+ const dbManifest = {
634
+ synced_at: new Date().toISOString(),
635
+ databases: {},
636
+ };
637
+ for (const [dbName, schema] of Object.entries(data.databases)) {
638
+ const tables = {};
639
+ const dbSchema = schema;
640
+ for (const [tableName, info] of Object.entries(dbSchema.tables)) {
641
+ tables[tableName] = { columns: info.columns, row_count: info.row_count };
642
+ }
643
+ dbManifest.databases[dbName] = {
644
+ size_mb: 0,
645
+ tables,
646
+ updated_at: new Date().toISOString(),
647
+ };
648
+ }
649
+ writeDatabasesManifest(cwd, dbManifest);
650
+ const fileCount = data.files.length;
651
+ const dbCount = Object.keys(data.databases).length;
652
+ const tableCount = Object.values(data.databases).reduce((sum, db) => sum + Object.keys(db.tables).length, 0);
653
+ if (fileCount > 0 || dbCount > 0) {
654
+ console.log(`Remote: ${dbCount} database${dbCount !== 1 ? "s" : ""} (${tableCount} table${tableCount !== 1 ? "s" : ""}), ${fileCount} file${fileCount !== 1 ? "s" : ""}`);
655
+ }
656
+ }
657
+ catch {
658
+ // offline or timeout — skip silently
659
+ }
660
+ }
661
+ function ensureAiDefaults(mugJsonPath) {
662
+ const raw = JSON.parse(readFileSync(mugJsonPath, "utf-8"));
663
+ let changed = false;
664
+ if (!raw.ai) {
665
+ raw.ai = {};
666
+ changed = true;
667
+ }
668
+ if (!raw.ai.routing) {
669
+ raw.ai.routing = {
670
+ fast: "openai/gpt-5.4-nano",
671
+ balanced: "@cf/moonshotai/kimi-k2.6",
672
+ powerful: "anthropic/claude-sonnet-4-6",
673
+ };
674
+ changed = true;
675
+ }
676
+ if (!raw.ai.billing) {
677
+ raw.ai.billing = {
678
+ default: "mug-metered",
679
+ fast: "mug-metered",
680
+ balanced: "mug-metered",
681
+ powerful: "mug-metered",
682
+ };
683
+ changed = true;
684
+ }
685
+ if (changed) {
686
+ writeFileSync(mugJsonPath, JSON.stringify(raw, null, 2) + "\n");
687
+ }
688
+ }
689
+ export function ensureSettings(mugJsonPath) {
690
+ const raw = JSON.parse(readFileSync(mugJsonPath, "utf-8"));
691
+ let changed = false;
692
+ if (!raw.settings) {
693
+ raw.settings = {};
694
+ changed = true;
695
+ }
696
+ if (!raw.settings.timezone) {
697
+ raw.settings.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
698
+ changed = true;
699
+ }
700
+ if (changed) {
701
+ writeFileSync(mugJsonPath, JSON.stringify(raw, null, 2) + "\n");
702
+ }
703
+ }
704
+ export function migrateMugJsonSources(mugJsonPath) {
705
+ if (!existsSync(mugJsonPath))
706
+ return;
707
+ const raw = JSON.parse(readFileSync(mugJsonPath, "utf-8"));
708
+ if (!configNeedsMigration(raw))
709
+ return;
710
+ const sources = migrateConfig(raw);
711
+ raw.sources = sources;
712
+ delete raw.connections;
713
+ delete raw.connectors;
714
+ writeFileSync(mugJsonPath, JSON.stringify(raw, null, 2) + "\n");
715
+ console.log("Migrated mug.json: connections + connectors → sources");
716
+ }
717
+ const FRAMEWORK_TEMPLATES = {
718
+ "app.ts": "app.ts",
719
+ "source.ts": "source.ts",
720
+ "sync-runtime.ts": "sync-runtime.ts",
721
+ "context.ts": "context.ts",
722
+ "form-types.ts": "form-types.ts",
723
+ "types.ts": "types.ts",
724
+ "workflow.ts": "workflow.ts",
725
+ "workflow-entrypoint.ts": "workflow-entrypoint.ts",
726
+ "do/workspace-database.ts": "do/workspace-database.ts",
727
+ "ai-router.ts": "ai-router.ts",
728
+ "agent-types.ts": "agent-types.ts",
729
+ "chunker.ts": "chunker.ts",
730
+ };
731
+ function isNewStructure(cwd) {
732
+ return existsSync(join(cwd, "connectors")) || existsSync(join(cwd, "workflows")) && !existsSync(join(cwd, "src", "workflows"));
733
+ }
734
+ export function syncFrameworkFiles(cwd) {
735
+ if (isNewStructure(cwd))
736
+ return 0;
737
+ const prefix = "src";
738
+ let updated = 0;
739
+ for (const [fileName, templateKey] of Object.entries(FRAMEWORK_TEMPLATES)) {
740
+ const filePath = join(prefix, fileName);
741
+ const fullPath = join(cwd, filePath);
742
+ const latest = templates[templateKey];
743
+ if (!latest)
744
+ continue;
745
+ const current = existsSync(fullPath) ? readFileSync(fullPath, "utf-8") : null;
746
+ if (current !== latest) {
747
+ mkdirSync(join(cwd, filePath, ".."), { recursive: true });
748
+ writeFileSync(fullPath, latest);
749
+ updated++;
750
+ }
751
+ }
752
+ return updated;
753
+ }
754
+ function migrateToRootStructure(cwd) {
755
+ // Detect old structure: src/workflows/ exists but root workflows/ does not
756
+ const hasOldWorkflows = existsSync(join(cwd, "src", "workflows"));
757
+ const hasNewWorkflows = existsSync(join(cwd, "workflows"));
758
+ if (!hasOldWorkflows || hasNewWorkflows)
759
+ return;
760
+ console.log("\nMigrating workspace to root-level structure...");
761
+ const moved = [];
762
+ // Move user code directories
763
+ const migrations = [
764
+ ["src/workflows", "workflows"],
765
+ ["src/sources", "connectors"],
766
+ ["src/agents", "agents"],
767
+ ];
768
+ for (const [oldDir, newDir] of migrations) {
769
+ const oldPath = join(cwd, oldDir);
770
+ if (!existsSync(oldPath))
771
+ continue;
772
+ const files = readdirSync(oldPath);
773
+ if (files.length === 0)
774
+ continue;
775
+ mkdirSync(join(cwd, newDir), { recursive: true });
776
+ cpSync(oldPath, join(cwd, newDir), { recursive: true });
777
+ rmSync(oldPath, { recursive: true });
778
+ moved.push(`${oldDir}/ → ${newDir}/`);
779
+ }
780
+ // Surfaces: may be in src/surfaces/ (not always created in old init)
781
+ const oldSurfaces = join(cwd, "src", "surfaces");
782
+ if (existsSync(oldSurfaces) && readdirSync(oldSurfaces).length > 0) {
783
+ mkdirSync(join(cwd, "surfaces"), { recursive: true });
784
+ cpSync(oldSurfaces, join(cwd, "surfaces"), { recursive: true });
785
+ rmSync(oldSurfaces, { recursive: true });
786
+ moved.push("src/surfaces/ → surfaces/");
787
+ }
788
+ // Delete framework files from src/ — runtime comes from CLI package now
789
+ const frameworkFiles = ["app.ts", "source.ts", "workflow.ts", "workflow-entrypoint.ts", "context.ts",
790
+ "types.ts", "form-types.ts", "ai-router.ts", "agent-types.ts", "sync-runtime.ts", "chunker.ts",
791
+ "index.ts", "connector.ts"];
792
+ for (const f of frameworkFiles) {
793
+ const old = join(cwd, "src", f);
794
+ if (existsSync(old))
795
+ rmSync(old);
796
+ }
797
+ try {
798
+ rmSync(join(cwd, "src", "do"), { recursive: true });
799
+ }
800
+ catch { }
801
+ moved.push("Removed framework files from src/ (runtime comes from CLI package)");
802
+ // Clean up empty src/ if possible
803
+ try {
804
+ const remaining = readdirSync(join(cwd, "src"));
805
+ if (remaining.length === 0) {
806
+ rmSync(join(cwd, "src"), { recursive: true });
807
+ moved.push("Removed empty src/");
808
+ }
809
+ }
810
+ catch { }
811
+ // Rewrite imports in user .ts files: "../workflow.js" → "@mugwork/mug"
812
+ const importRewrites = [
813
+ [/from\s+["']\.\.\/workflow\.js["']/g, 'from "@mugwork/mug"'],
814
+ [/from\s+["']\.\.\/source\.js["']/g, 'from "@mugwork/mug"'],
815
+ [/from\s+["']\.\.\/connector\.js["']/g, 'from "@mugwork/mug"'],
816
+ [/from\s+["']\.\.\/agent-types\.js["']/g, 'from "@mugwork/mug"'],
817
+ ];
818
+ for (const dir of ["workflows", "connectors", "agents"]) {
819
+ const dirPath = join(cwd, dir);
820
+ if (!existsSync(dirPath))
821
+ continue;
822
+ for (const f of readdirSync(dirPath, { recursive: true })) {
823
+ if (!f.endsWith(".ts") && !f.endsWith(".js"))
824
+ continue;
825
+ const filePath = join(dirPath, f);
826
+ if (!statSync(filePath).isFile())
827
+ continue;
828
+ let content = readFileSync(filePath, "utf-8");
829
+ let changed = false;
830
+ for (const [pattern, replacement] of importRewrites) {
831
+ const updated = content.replace(pattern, replacement);
832
+ if (updated !== content) {
833
+ content = updated;
834
+ changed = true;
835
+ }
836
+ }
837
+ if (changed) {
838
+ writeFileSync(filePath, content);
839
+ }
840
+ }
841
+ }
842
+ // Create slack.json if missing
843
+ if (!existsSync(join(cwd, "slack.json"))) {
844
+ writeFileSync(join(cwd, "slack.json"), JSON.stringify({ enabled: false }, null, 2) + "\n");
845
+ moved.push("Created slack.json");
846
+ }
847
+ // Migrate mug.json slack key → slack.json
848
+ const mugJsonPath = join(cwd, "mug.json");
849
+ const raw = JSON.parse(readFileSync(mugJsonPath, "utf-8"));
850
+ if (raw.slack && existsSync(join(cwd, "slack.json"))) {
851
+ const slackJson = JSON.parse(readFileSync(join(cwd, "slack.json"), "utf-8"));
852
+ if (!slackJson.enabled && raw.slack.enabled) {
853
+ writeFileSync(join(cwd, "slack.json"), JSON.stringify(raw.slack, null, 2) + "\n");
854
+ delete raw.slack;
855
+ writeFileSync(mugJsonPath, JSON.stringify(raw, null, 2) + "\n");
856
+ moved.push("Migrated mug.json slack → slack.json");
857
+ }
858
+ }
859
+ // Update tsconfig.json
860
+ const tsconfigPath = join(cwd, "tsconfig.json");
861
+ if (existsSync(tsconfigPath)) {
862
+ try {
863
+ const tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
864
+ if (JSON.stringify(tsconfig.include) === '["src/**/*"]') {
865
+ tsconfig.include = ["**/*.ts"];
866
+ writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
867
+ moved.push("Updated tsconfig.json include");
868
+ }
869
+ }
870
+ catch { }
871
+ }
872
+ // Print deprecation notices for mug.json object sections
873
+ if (raw.workflows && Object.keys(raw.workflows).length > 0) {
874
+ console.log(" ⚠ mug.json \"workflows\" section is deprecated — move schedule/trigger config to workflow .ts options");
875
+ }
876
+ if (raw.surfaces && Object.keys(raw.surfaces).length > 0) {
877
+ console.log(" ⚠ mug.json \"surfaces\" section is deprecated — surfaces are auto-discovered from surfaces/ directory");
878
+ }
879
+ if (moved.length > 0) {
880
+ console.log(" " + moved.join("\n "));
881
+ console.log("Migration complete.\n");
882
+ }
883
+ }
884
+ export async function sync(dir) {
885
+ const cwd = dir || process.cwd();
886
+ checkCliVersion();
887
+ ensureDevEmail(join(cwd, "mug.json"));
888
+ ensureAiDefaults(join(cwd, "mug.json"));
889
+ ensureSettings(join(cwd, "mug.json"));
890
+ migrateMugJsonSources(join(cwd, "mug.json"));
891
+ migrateToRootStructure(cwd);
892
+ repairScaffolding(cwd);
893
+ const config = loadMugJson(cwd);
894
+ const instructions = generateInstructions(config, cwd);
895
+ const targets = [
896
+ "CLAUDE.md",
897
+ "AGENTS.md",
898
+ join(".cursor", "rules", "mug.mdc"),
899
+ ];
900
+ let updated = 0;
901
+ for (const target of targets) {
902
+ const path = join(cwd, target);
903
+ if (!existsSync(path))
904
+ continue;
905
+ const content = readFileSync(path, "utf-8");
906
+ const newContent = injectBlock(content, instructions);
907
+ if (newContent !== content) {
908
+ writeFileSync(path, newContent);
909
+ updated++;
910
+ }
911
+ }
912
+ let frameworkUpdated = syncFrameworkFiles(cwd);
913
+ // Migrate old monolithic src/index.ts → src/app.ts + slim index.ts
914
+ const indexPath = join(cwd, "src", "index.ts");
915
+ if (existsSync(indexPath)) {
916
+ const indexContent = readFileSync(indexPath, "utf-8");
917
+ if (indexContent.includes("new Hono") && indexContent.includes('app.get("/health"')) {
918
+ const lines = indexContent.split("\n");
919
+ const userImports = [];
920
+ for (const line of lines) {
921
+ if (/^import\s+["']\.\/(connectors|sources)\//.test(line) ||
922
+ /^import\s+["']\.\/workflows\//.test(line) ||
923
+ /^import\s+["']\.\/agents\//.test(line)) {
924
+ userImports.push(line);
925
+ }
926
+ }
927
+ const sourceImports = userImports.filter(l => l.includes("/connectors/") || l.includes("/sources/"));
928
+ const workflowImports = userImports.filter(l => l.includes("/workflows/") || l.includes("/agents/"));
929
+ const newIndex = [
930
+ "// Import sources here:",
931
+ ...sourceImports,
932
+ "",
933
+ "// Import workflows here:",
934
+ ...workflowImports,
935
+ "",
936
+ 'export { WorkspaceDatabase } from "./do/workspace-database.js";',
937
+ 'export { default } from "./app.js";',
938
+ "",
939
+ ].join("\n");
940
+ writeFileSync(indexPath, newIndex);
941
+ frameworkUpdated++;
942
+ console.log(" Migrated src/index.ts → src/app.ts (framework routes extracted)");
943
+ }
944
+ }
945
+ // Update skills from CLI templates
946
+ const skillPaths = {};
947
+ for (const [skillPath, content] of Object.entries(skillTemplates)) {
948
+ const parts = skillPath.split("/");
949
+ const skillName = parts[0];
950
+ const fileName = parts[parts.length - 1];
951
+ if (fileName.endsWith(".md")) {
952
+ skillPaths[join(".claude", "skills", skillName, fileName)] = content;
953
+ skillPaths[join(".agents", "skills", skillName, fileName)] = content;
954
+ }
955
+ if (fileName.endsWith(".mdc")) {
956
+ skillPaths[join(".cursor", "rules", fileName)] = content;
957
+ }
958
+ }
959
+ let skillsUpdated = 0;
960
+ for (const [filePath, content] of Object.entries(skillPaths)) {
961
+ const fullPath = join(cwd, filePath);
962
+ const current = existsSync(fullPath) ? readFileSync(fullPath, "utf-8") : null;
963
+ if (current !== content) {
964
+ mkdirSync(join(fullPath, ".."), { recursive: true });
965
+ writeFileSync(fullPath, content);
966
+ skillsUpdated++;
967
+ }
968
+ }
969
+ // Update docs from CLI templates
970
+ let docsUpdated = 0;
971
+ for (const [docName, content] of Object.entries(docTemplates)) {
972
+ const fullPath = join(cwd, ".mug", "docs", docName);
973
+ const current = existsSync(fullPath) ? readFileSync(fullPath, "utf-8") : null;
974
+ if (current !== content) {
975
+ mkdirSync(join(fullPath, ".."), { recursive: true });
976
+ writeFileSync(fullPath, content);
977
+ docsUpdated++;
978
+ }
979
+ }
980
+ const parts = [`Synced ${updated} instruction file${updated !== 1 ? "s" : ""}`];
981
+ if (frameworkUpdated > 0)
982
+ parts.push(`${frameworkUpdated} framework file${frameworkUpdated !== 1 ? "s" : ""}`);
983
+ if (skillsUpdated > 0)
984
+ parts.push(`${skillsUpdated} skill${skillsUpdated !== 1 ? "s" : ""}`);
985
+ if (docsUpdated > 0)
986
+ parts.push(`${docsUpdated} doc${docsUpdated !== 1 ? "s" : ""}`);
987
+ console.log(parts.join(", "));
988
+ await syncLocalFiles(cwd, config);
989
+ await syncLocalDatabases(cwd, config);
990
+ await syncRemoteManifests(cwd, config);
991
+ }