@venturewild/workspace 0.1.13 → 0.2.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 (38) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +83 -76
  4. package/server/bin/wild-workspace.mjs +825 -763
  5. package/server/src/agent.mjs +453 -386
  6. package/server/src/bazaar/core.mjs +579 -0
  7. package/server/src/bazaar/index.mjs +75 -0
  8. package/server/src/bazaar/mcp-server.mjs +328 -0
  9. package/server/src/bazaar/mock-tickup.mjs +97 -0
  10. package/server/src/bazaar/preview-server.mjs +95 -0
  11. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -0
  12. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -0
  13. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -0
  14. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -0
  15. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -0
  16. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -0
  17. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -0
  18. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -0
  19. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -0
  20. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +32 -0
  21. package/server/src/canvas/core.mjs +324 -0
  22. package/server/src/canvas/index.mjs +42 -0
  23. package/server/src/canvas/mcp-server.mjs +253 -0
  24. package/server/src/config.mjs +365 -365
  25. package/server/src/daemon-supervisor.mjs +216 -216
  26. package/server/src/inbox.mjs +86 -86
  27. package/server/src/index.mjs +1948 -1726
  28. package/server/src/logpaths.mjs +98 -98
  29. package/server/src/pairing.mjs +9 -18
  30. package/server/src/service.mjs +419 -419
  31. package/server/src/share.mjs +182 -148
  32. package/server/src/sync.mjs +248 -248
  33. package/server/src/turn-mcp.mjs +46 -0
  34. package/web/dist/assets/index-DVWgeTl_.js +91 -0
  35. package/web/dist/assets/index-Dl0VT5e6.css +1 -0
  36. package/web/dist/index.html +2 -2
  37. package/web/dist/assets/index-Bj-mdLGj.css +0 -1
  38. package/web/dist/assets/index-CAzFAt7W.js +0 -89
@@ -0,0 +1,253 @@
1
+ // Canvas MCP server — the tools the agent uses to BUILD the user a block (§3.3).
2
+ //
3
+ // Same hand-rolled stdio JSON-RPC 2.0 shape as the bazaar MCP server (no new dep,
4
+ // wrap-don't-embed). `claude` spawns this per turn via --mcp-config and exposes the
5
+ // tools as mcp__canvas__<name>. It imports the SAME core.mjs the main server uses
6
+ // (state under ~/.wild-workspace/canvas/), so there is one source of truth and no
7
+ // HTTP/port handshake. Tool results are JSON envelopes the UI parses to mount the
8
+ // block on the canvas.
9
+ //
10
+ // stdout carries ONLY JSON-RPC. Any diagnostics go to stderr.
11
+
12
+ import readline from 'node:readline';
13
+ import { createCanvas, KINDS } from './core.mjs';
14
+
15
+ // The theme-token params set_theme accepts (each an optional hex color). Kept here
16
+ // as schema; core.mjs is the validator (hex-only, allowlisted). "Data, not CSS."
17
+ const THEME_TOKEN_PROPS = {
18
+ bg: { type: 'string', description: 'page background, hex e.g. "#0a0c10".' },
19
+ surface: { type: 'string', description: 'card / panel face, hex.' },
20
+ text: { type: 'string', description: 'primary text, hex.' },
21
+ textMuted: { type: 'string', description: 'secondary/muted text, hex.' },
22
+ border: { type: 'string', description: 'hairline borders, hex.' },
23
+ canvas1: { type: 'string', description: 'wallpaper gradient stop 1, hex.' },
24
+ canvas2: { type: 'string', description: 'wallpaper gradient stop 2, hex.' },
25
+ canvas3: { type: 'string', description: 'wallpaper gradient stop 3, hex.' },
26
+ };
27
+
28
+ const canvas = createCanvas(); // baseDir from WILD_WORKSPACE_GLOBAL_DIR (set by parent)
29
+ const PROTOCOL_VERSION = '2025-06-18';
30
+
31
+ function send(msg) {
32
+ process.stdout.write(`${JSON.stringify(msg)}\n`);
33
+ }
34
+ function result(id, res) {
35
+ send({ jsonrpc: '2.0', id, result: res });
36
+ }
37
+ function errorReply(id, code, message) {
38
+ send({ jsonrpc: '2.0', id, error: { code, message } });
39
+ }
40
+ function textContent(obj) {
41
+ return { content: [{ type: 'text', text: JSON.stringify(obj) }] };
42
+ }
43
+
44
+ // --- tool definitions -----------------------------------------------------
45
+
46
+ // Shared description of the spec vocabulary, kept in one place so make/update agree.
47
+ const SHAPE_HELP =
48
+ "Pick the simplest `kind` that fits and fill only that kind's fields:\n" +
49
+ "- metric: a single headline number. value (required, e.g. \"42\" or \"$1,240\"), " +
50
+ "label (what it counts), delta (\"+12 today\"), trend (\"up\"|\"down\"|\"flat\"), " +
51
+ "spark (array of recent numbers for a tiny chart).\n" +
52
+ "- list: items — an array of { label, value } (value optional). Good for leaderboards/breakdowns.\n" +
53
+ "- table: columns (array of headers) + rows (array of arrays of cells).\n" +
54
+ "- markdown: markdown — a short rich-text note.\n" +
55
+ "Compute the actual values yourself first (read files / run commands as needed), then pass them in. " +
56
+ "You are shipping DATA, not code.";
57
+
58
+ const TOOLS = [
59
+ {
60
+ name: 'make_block',
61
+ description:
62
+ "Build the user a custom block and pin it to their canvas. Use this when the user asks to " +
63
+ "\"make/add a block\", wants a dashboard/widget, or asks to \"show me <something>\" that's worth " +
64
+ "keeping in view. The block appears on their canvas immediately. " +
65
+ SHAPE_HELP,
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ title: { type: 'string', description: 'Short title shown on the block header.' },
70
+ kind: { type: 'string', enum: KINDS, description: 'The presentation primitive.' },
71
+ icon: { type: 'string', description: 'A single emoji for the block (optional).' },
72
+ note: { type: 'string', description: 'A one-line subtitle: what this shows (optional).' },
73
+ value: { type: 'string', description: 'metric: the headline value.' },
74
+ label: { type: 'string', description: 'metric: what the value counts.' },
75
+ delta: { type: 'string', description: 'metric: a change indicator, e.g. "+12 today".' },
76
+ trend: { type: 'string', enum: ['up', 'down', 'flat'], description: 'metric: colours the delta.' },
77
+ spark: { type: 'array', items: { type: 'number' }, description: 'metric: recent numbers for a sparkline.' },
78
+ items: {
79
+ type: 'array',
80
+ description: 'list: { label, value } entries.',
81
+ items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } } },
82
+ },
83
+ columns: { type: 'array', items: { type: 'string' }, description: 'table: column headers.' },
84
+ rows: { type: 'array', items: { type: 'array', items: { type: 'string' } }, description: 'table: rows of cells.' },
85
+ markdown: { type: 'string', description: 'markdown: the rich-text body.' },
86
+ },
87
+ required: ['title', 'kind'],
88
+ },
89
+ },
90
+ {
91
+ name: 'update_block',
92
+ description:
93
+ "Refresh an existing custom block with new data (this is how a block stays \"live\" — you re-push " +
94
+ "when the underlying numbers change). Pass the block id and only the fields that change. " +
95
+ SHAPE_HELP,
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ id: { type: 'string', description: 'The block id returned by make_block.' },
100
+ title: { type: 'string' },
101
+ kind: { type: 'string', enum: KINDS },
102
+ icon: { type: 'string' },
103
+ note: { type: 'string' },
104
+ value: { type: 'string' },
105
+ label: { type: 'string' },
106
+ delta: { type: 'string' },
107
+ trend: { type: 'string', enum: ['up', 'down', 'flat'] },
108
+ spark: { type: 'array', items: { type: 'number' } },
109
+ items: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } } } },
110
+ columns: { type: 'array', items: { type: 'string' } },
111
+ rows: { type: 'array', items: { type: 'array', items: { type: 'string' } } },
112
+ markdown: { type: 'string' },
113
+ },
114
+ required: ['id'],
115
+ },
116
+ },
117
+ {
118
+ name: 'list_blocks',
119
+ description:
120
+ "List the custom blocks currently on the user's canvas (id, title, kind) — use this to find the " +
121
+ "id of a block the user refers to before calling update_block.",
122
+ inputSchema: { type: 'object', properties: {} },
123
+ },
124
+ {
125
+ name: 'set_theme',
126
+ description:
127
+ "Restyle the user's whole workspace (the \"make it mine\" surface). Use this when the user asks to " +
128
+ "change the look — \"make it dark\", \"match my brand\", \"give me a sunset / forest / high-contrast " +
129
+ "theme\", etc. Pick a `mode` (light|dark) as the base, then the token colours that complete the look — " +
130
+ "the background, surfaces, text, and the three wallpaper stops. The change applies immediately. You " +
131
+ "ship COLOURS (hex values), not CSS. NOTE: you do NOT set the accent colour — that is the user's own " +
132
+ "signature colour (chosen in onboarding or the theme picker) and is preserved across every restyle. " +
133
+ "Describe the mode and background/wallpaper you applied; do NOT claim to have changed their accent.",
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ mode: { type: 'string', enum: ['light', 'dark'], description: 'Base palette: light or dark.' },
138
+ name: { type: 'string', description: 'A short name for the theme (optional, e.g. "Sunset").' },
139
+ ...THEME_TOKEN_PROPS,
140
+ },
141
+ },
142
+ },
143
+ {
144
+ name: 'get_theme',
145
+ description: "Read the user's current theme (mode, accent, tokens) — use before tweaking it.",
146
+ inputSchema: { type: 'object', properties: {} },
147
+ },
148
+ ];
149
+
150
+ // --- tool dispatch --------------------------------------------------------
151
+
152
+ function callTool(name, args = {}) {
153
+ switch (name) {
154
+ case 'make_block': {
155
+ try {
156
+ const block = canvas.addBlock(args);
157
+ return textContent({
158
+ kind: 'block',
159
+ op: 'make',
160
+ block,
161
+ note: 'Block is now on the user\'s canvas. Tell them in one short line what you added.',
162
+ });
163
+ } catch (e) {
164
+ return { ...textContent({ kind: 'error', error: e?.message || String(e) }), isError: true };
165
+ }
166
+ }
167
+ case 'update_block': {
168
+ const block = canvas.updateBlock(args.id, args);
169
+ if (!block) return { ...textContent({ kind: 'error', error: `no block "${args.id}"` }), isError: true };
170
+ return textContent({ kind: 'block', op: 'update', block, note: 'Block updated in place on the canvas.' });
171
+ }
172
+ case 'list_blocks': {
173
+ const blocks = canvas.listBlocks().map((b) => ({ id: b.id, title: b.title, kind: b.kind }));
174
+ return textContent({ kind: 'blocks', count: blocks.length, blocks });
175
+ }
176
+ case 'set_theme': {
177
+ const theme = canvas.setTheme(args);
178
+ return textContent({
179
+ kind: 'theme',
180
+ op: 'set',
181
+ theme,
182
+ note:
183
+ "The user's workspace is now restyled (mode + background/wallpaper). Their accent colour is " +
184
+ "preserved — it's their own. Tell them in one short line what look you applied; describe the " +
185
+ "mode and background, NOT an accent colour.",
186
+ });
187
+ }
188
+ case 'get_theme': {
189
+ return textContent({ kind: 'theme', op: 'get', theme: canvas.getTheme() });
190
+ }
191
+ default:
192
+ return { ...textContent({ kind: 'error', error: `unknown tool ${name}` }), isError: true };
193
+ }
194
+ }
195
+
196
+ // --- JSON-RPC loop --------------------------------------------------------
197
+
198
+ export function handleMessage(msg) {
199
+ if (!msg || msg.jsonrpc !== '2.0') return;
200
+ const { id, method, params } = msg;
201
+ const isNotification = id === undefined || id === null;
202
+
203
+ switch (method) {
204
+ case 'initialize':
205
+ return result(id, {
206
+ protocolVersion: params?.protocolVersion || PROTOCOL_VERSION,
207
+ capabilities: { tools: {} },
208
+ serverInfo: { name: 'canvas', version: '1.0.0' },
209
+ });
210
+ case 'notifications/initialized':
211
+ case 'initialized':
212
+ return; // notification, no response
213
+ case 'ping':
214
+ return result(id, {});
215
+ case 'tools/list':
216
+ return result(id, { tools: TOOLS });
217
+ case 'tools/call': {
218
+ try {
219
+ const out = callTool(params?.name, params?.arguments || {});
220
+ return result(id, out);
221
+ } catch (e) {
222
+ return errorReply(id, -32603, `tool error: ${e?.message || e}`);
223
+ }
224
+ }
225
+ default:
226
+ if (!isNotification) errorReply(id, -32601, `method not found: ${method}`);
227
+ return;
228
+ }
229
+ }
230
+
231
+ // Only run the stdio loop when executed as the MCP process (not when imported by a test).
232
+ const isDirectRun = process.argv[1] && process.argv[1].endsWith('mcp-server.mjs');
233
+ if (isDirectRun) {
234
+ const rl = readline.createInterface({ input: process.stdin });
235
+ rl.on('line', (line) => {
236
+ const trimmed = line.trim();
237
+ if (!trimmed) return;
238
+ let msg;
239
+ try {
240
+ msg = JSON.parse(trimmed);
241
+ } catch {
242
+ return; // ignore non-JSON noise
243
+ }
244
+ try {
245
+ handleMessage(msg);
246
+ } catch (e) {
247
+ process.stderr.write(`canvas mcp error: ${e?.message || e}\n`);
248
+ }
249
+ });
250
+ process.stderr.write('canvas mcp server ready\n');
251
+ }
252
+
253
+ export { TOOLS, callTool };