@venturewild/workspace 0.1.14 → 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.
- package/LICENSE +21 -21
- package/README.md +112 -112
- package/package.json +83 -76
- package/server/bin/wild-workspace.mjs +825 -763
- package/server/src/agent.mjs +453 -386
- package/server/src/bazaar/core.mjs +579 -0
- package/server/src/bazaar/index.mjs +75 -0
- package/server/src/bazaar/mcp-server.mjs +328 -0
- package/server/src/bazaar/mock-tickup.mjs +97 -0
- package/server/src/bazaar/preview-server.mjs +95 -0
- package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -0
- package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -0
- package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -0
- package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -0
- package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -0
- package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -0
- package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -0
- package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -0
- package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -0
- package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +32 -0
- package/server/src/canvas/core.mjs +324 -0
- package/server/src/canvas/index.mjs +42 -0
- package/server/src/canvas/mcp-server.mjs +253 -0
- package/server/src/config.mjs +365 -365
- package/server/src/daemon-supervisor.mjs +216 -216
- package/server/src/inbox.mjs +86 -86
- package/server/src/index.mjs +1948 -1721
- package/server/src/logpaths.mjs +98 -98
- package/server/src/service.mjs +419 -419
- package/server/src/share.mjs +182 -148
- package/server/src/sync.mjs +248 -248
- package/server/src/turn-mcp.mjs +46 -0
- package/web/dist/assets/index-DVWgeTl_.js +91 -0
- package/web/dist/assets/index-Dl0VT5e6.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bj-mdLGj.css +0 -1
- package/web/dist/assets/index-Dc6jo84c.js +0 -89
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
// Bazaar core — the shelf, search/ranking, and the transaction ledger.
|
|
2
|
+
//
|
|
3
|
+
// Faithful to UX §3.7: a *network* of producers' recipes that a consumer's agent
|
|
4
|
+
// reaches onto, picks by "which actually works" (measured outcomes), absorbs, and
|
|
5
|
+
// builds — recording a three-way transaction (consumer got it · producer earns ·
|
|
6
|
+
// platform recorded it).
|
|
7
|
+
//
|
|
8
|
+
// State lives ENTIRELY under ~/.wild-workspace/bazaar/ (an absolute path OUTSIDE
|
|
9
|
+
// the user's repo — CLAUDE.md rule #1). One module, imported by BOTH the main
|
|
10
|
+
// server and the spawned MCP server, so there is a single source of truth and no
|
|
11
|
+
// HTTP/port handshake between processes:
|
|
12
|
+
// - events.jsonl append-only event log (use · service-call · publish). Append
|
|
13
|
+
// is multi-writer safe (the MCP process writes use/publish; the
|
|
14
|
+
// main process's preview server writes service-call).
|
|
15
|
+
// - listings.json user-published listings (the flip side). MCP writes, main reads.
|
|
16
|
+
// - preview.json the current preview target {dir,recipeId}. MCP writes, main reads.
|
|
17
|
+
//
|
|
18
|
+
// Seed recipes (our product content — the shelf) ship in-repo under seed-recipes/.
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import crypto from 'node:crypto';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
import { normalizeTheme } from '../canvas/core.mjs';
|
|
27
|
+
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const SEED_DIR = path.join(__dirname, 'seed-recipes');
|
|
30
|
+
|
|
31
|
+
// Seed THEMES — the first cross-user marketplace artifact (a theme is a hex-token
|
|
32
|
+
// bundle: pure data, zero execution risk, and its own preview). These ship as a
|
|
33
|
+
// small network of "producers" so the bazaar's Themes shelf is populated out of
|
|
34
|
+
// the box; a user's own published theme lands beside them (listings.json).
|
|
35
|
+
export const SEED_THEMES = [
|
|
36
|
+
{
|
|
37
|
+
id: 'theme-midnight-cyan', kind: 'theme', source: 'theme',
|
|
38
|
+
title: 'Midnight Cyan', pitch: 'The venturewild.llc night look — deep navy, electric cyan.',
|
|
39
|
+
summary: 'A focused dark workspace: near-black navy with a bright cyan accent and cool slate cards.',
|
|
40
|
+
tags: ['dark', 'cyan', 'focus'],
|
|
41
|
+
producer: { name: 'VentureWild', handle: 'venturewild', kind: 'vendor' },
|
|
42
|
+
theme: { mode: 'dark', accent: '#22d3ee', tokens: { bg: '#0a0c10', surface: '#11141a', text: '#e8eaed', textMuted: '#8b95a3', border: '#1f242d', canvas1: '#0b1620', canvas2: '#0c1320', canvas3: '#0a0f14' } },
|
|
43
|
+
outcomeScore: 0.92, outcomeStats: { builds: 0, working: 0 }, safetyBadge: 'verified',
|
|
44
|
+
rating: { stars: 5, count: 24 }, reward: { model: 'one-time', unit: 'per apply', oneTimeValue: 1.5, perUseValue: 0 },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'theme-sunset', kind: 'theme', source: 'theme',
|
|
48
|
+
title: 'Warm Sunset', pitch: 'Dusty plum to amber — warm, low-glare, easy at night.',
|
|
49
|
+
summary: 'A cosy dark theme: plum background fading to amber, with a glowing orange accent.',
|
|
50
|
+
tags: ['dark', 'warm', 'orange'],
|
|
51
|
+
producer: { name: 'Lina', handle: 'lina', kind: 'maker' },
|
|
52
|
+
theme: { mode: 'dark', accent: '#ff7a3c', tokens: { bg: '#1a1014', surface: '#2a1a1f', text: '#fbeee6', textMuted: '#c7a89a', border: '#43292f', canvas1: '#2d1620', canvas2: '#5a2a2c', canvas3: '#b85c2e' } },
|
|
53
|
+
outcomeScore: 0.84, outcomeStats: { builds: 0, working: 0 }, safetyBadge: 'verified',
|
|
54
|
+
rating: { stars: 4, count: 11 }, reward: { model: 'one-time', unit: 'per apply', oneTimeValue: 1.5, perUseValue: 0 },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'theme-forest', kind: 'theme', source: 'theme',
|
|
58
|
+
title: 'Forest Light', pitch: 'Soft sage & cream — a calm, bright daytime desk.',
|
|
59
|
+
summary: 'A gentle light theme: sage-green wallpaper, cream cards, a deep forest accent.',
|
|
60
|
+
tags: ['light', 'green', 'calm'],
|
|
61
|
+
producer: { name: 'Mateo', handle: 'mateo', kind: 'maker' },
|
|
62
|
+
theme: { mode: 'light', accent: '#15803d', tokens: { bg: '#fbfdfb', surface: '#ffffff', text: '#1a2b22', textMuted: '#5c7064', border: '#dde9e0', canvas1: '#e3f3e7', canvas2: '#eaf4ec', canvas3: '#fbfdf6' } },
|
|
63
|
+
outcomeScore: 0.79, outcomeStats: { builds: 0, working: 0 }, safetyBadge: 'verified',
|
|
64
|
+
rating: { stars: 4, count: 8 }, reward: { model: 'one-time', unit: 'per apply', oneTimeValue: 1.5, perUseValue: 0 },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'theme-mono', kind: 'theme', source: 'theme',
|
|
68
|
+
title: 'Mono Slate', pitch: 'Greyscale, no distractions — one quiet blue accent.',
|
|
69
|
+
summary: 'A neutral light theme for heads-down work: warm greys, a single muted-blue accent.',
|
|
70
|
+
tags: ['light', 'minimal', 'mono'],
|
|
71
|
+
producer: { name: 'Priya', handle: 'priya', kind: 'maker' },
|
|
72
|
+
theme: { mode: 'light', accent: '#475569', tokens: { bg: '#fafafa', surface: '#ffffff', text: '#1e2530', textMuted: '#6b7280', border: '#e6e8ec', canvas1: '#eef0f3', canvas2: '#f3f4f6', canvas3: '#fafafa' } },
|
|
73
|
+
outcomeScore: 0.74, outcomeStats: { builds: 0, working: 0 }, safetyBadge: 'verified',
|
|
74
|
+
rating: { stars: 4, count: 6 }, reward: { model: 'one-time', unit: 'per apply', oneTimeValue: 1.5, perUseValue: 0 },
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// ~/.wild-workspace/bazaar — mirrors logpaths.globalDir() but kept dependency-free
|
|
79
|
+
// here so the MCP child can import this module standalone.
|
|
80
|
+
export function defaultBazaarDir(env = process.env) {
|
|
81
|
+
const base = env.WILD_WORKSPACE_GLOBAL_DIR || path.join(os.homedir(), '.wild-workspace');
|
|
82
|
+
return path.join(base, 'bazaar');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rid() {
|
|
86
|
+
return crypto.randomUUID().slice(0, 12);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readJsonSafe(file, fallback) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
92
|
+
} catch {
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeJsonAtomic(file, value) {
|
|
98
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2));
|
|
100
|
+
fs.renameSync(tmp, file); // Node rename replaces the destination on all platforms
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- recipe loading -------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function loadSeedRecipes(seedDir = SEED_DIR) {
|
|
106
|
+
let entries = [];
|
|
107
|
+
try {
|
|
108
|
+
entries = fs.readdirSync(seedDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const recipes = [];
|
|
113
|
+
for (const e of entries) {
|
|
114
|
+
const dir = path.join(seedDir, e.name);
|
|
115
|
+
const meta = readJsonSafe(path.join(dir, 'recipe.json'), null);
|
|
116
|
+
if (!meta || !meta.id) continue;
|
|
117
|
+
let knowHow = '';
|
|
118
|
+
try {
|
|
119
|
+
knowHow = fs.readFileSync(path.join(dir, 'know-how.md'), 'utf8');
|
|
120
|
+
} catch {
|
|
121
|
+
/* a recipe with no know-how file is still listable */
|
|
122
|
+
}
|
|
123
|
+
recipes.push({ ...meta, knowHow, source: 'seed' });
|
|
124
|
+
}
|
|
125
|
+
return recipes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- search / ranking -----------------------------------------------------
|
|
129
|
+
// The agent's pick signal is "which recipe actually works" (outcomeScore), NOT
|
|
130
|
+
// marketing or stars (§3.7). Tag relevance gates the candidates; outcomeScore
|
|
131
|
+
// breaks ties and floats proven recipes to the top of the shelf.
|
|
132
|
+
|
|
133
|
+
function normalize(s) {
|
|
134
|
+
return String(s || '')
|
|
135
|
+
.toLowerCase()
|
|
136
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
137
|
+
.trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stem(word) {
|
|
141
|
+
// ultra-light: drop a trailing plural 's' so "candidates" matches "candidate"
|
|
142
|
+
return word.length > 3 && word.endsWith('s') ? word.slice(0, -1) : word;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function scoreRecipe(recipe, need) {
|
|
146
|
+
const needNorm = ` ${normalize(need)} `;
|
|
147
|
+
const needWords = new Set(needNorm.trim().split(' ').filter(Boolean).map(stem));
|
|
148
|
+
let score = 0;
|
|
149
|
+
const matched = [];
|
|
150
|
+
for (const tag of recipe.tags || []) {
|
|
151
|
+
const tagNorm = normalize(tag);
|
|
152
|
+
if (!tagNorm) continue;
|
|
153
|
+
if (tagNorm.includes(' ')) {
|
|
154
|
+
// multi-word tag: a phrase hit is a strong signal
|
|
155
|
+
if (needNorm.includes(` ${tagNorm} `) || needNorm.includes(tagNorm)) {
|
|
156
|
+
score += 3;
|
|
157
|
+
matched.push(tag);
|
|
158
|
+
}
|
|
159
|
+
} else if (needWords.has(stem(tagNorm))) {
|
|
160
|
+
score += 1;
|
|
161
|
+
matched.push(tag);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { score, matched };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- the bazaar instance --------------------------------------------------
|
|
168
|
+
|
|
169
|
+
export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
170
|
+
const dir = baseDir || defaultBazaarDir();
|
|
171
|
+
const eventsFile = path.join(dir, 'events.jsonl');
|
|
172
|
+
const listingsFile = path.join(dir, 'listings.json');
|
|
173
|
+
const previewFile = path.join(dir, 'preview.json');
|
|
174
|
+
// Self-seed drafts (cold-start, Producer #1): recipes the agent EXTRACTED from
|
|
175
|
+
// the user's existing work, staged here for REVIEW. Nothing reaches the shelf
|
|
176
|
+
// until the user approves a draft (publishDraft). The safety control is that a
|
|
177
|
+
// draft holds only generalized know-how — never client files/names/secrets.
|
|
178
|
+
const draftsFile = path.join(dir, 'drafts.json');
|
|
179
|
+
|
|
180
|
+
function ensureDir() {
|
|
181
|
+
try {
|
|
182
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
183
|
+
} catch {
|
|
184
|
+
/* read-only fs — degrades to no persistence */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function shelf() {
|
|
189
|
+
const seed = loadSeedRecipes(seedDir);
|
|
190
|
+
const listings = readJsonSafe(listingsFile, []);
|
|
191
|
+
return [...seed, ...SEED_THEMES, ...(Array.isArray(listings) ? listings : [])];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getRecipe(id) {
|
|
195
|
+
return shelf().find((r) => r.id === id) || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Public, agent-facing card for a recipe (no know-how dump until opened).
|
|
199
|
+
function card(r, extra = {}) {
|
|
200
|
+
return {
|
|
201
|
+
id: r.id,
|
|
202
|
+
kind: r.kind || 'recipe',
|
|
203
|
+
title: r.title,
|
|
204
|
+
pitch: r.pitch,
|
|
205
|
+
producer: r.producer,
|
|
206
|
+
summary: r.summary,
|
|
207
|
+
vendorDescription: r.vendorDescription,
|
|
208
|
+
outcomeScore: r.outcomeScore,
|
|
209
|
+
outcomeStats: r.outcomeStats,
|
|
210
|
+
safetyBadge: r.safetyBadge,
|
|
211
|
+
rating: r.rating,
|
|
212
|
+
reward: r.reward,
|
|
213
|
+
hasService: Boolean(r.service),
|
|
214
|
+
buildDir: r.buildDir,
|
|
215
|
+
// A theme card carries its own bundle — it IS its own preview (just hex), so
|
|
216
|
+
// the storefront can render a live swatch and apply it without a fetch.
|
|
217
|
+
...(r.kind === 'theme' && r.theme ? { theme: r.theme } : {}),
|
|
218
|
+
...extra,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function search(need, { limit = 4 } = {}) {
|
|
223
|
+
const ranked = shelf()
|
|
224
|
+
.map((r) => {
|
|
225
|
+
const { score, matched } = scoreRecipe(r, need);
|
|
226
|
+
return { r, score, matched };
|
|
227
|
+
})
|
|
228
|
+
.filter((x) => x.score > 0)
|
|
229
|
+
.sort((a, b) => b.score - a.score || (b.r.outcomeScore || 0) - (a.r.outcomeScore || 0))
|
|
230
|
+
.slice(0, limit);
|
|
231
|
+
return ranked.map((x) => card(x.r, { relevance: x.score, matched: x.matched }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function appendEvent(evt) {
|
|
235
|
+
ensureDir();
|
|
236
|
+
const enriched = { id: rid(), ts: Date.now(), ...evt };
|
|
237
|
+
try {
|
|
238
|
+
fs.appendFileSync(eventsFile, `${JSON.stringify(enriched)}\n`);
|
|
239
|
+
} catch {
|
|
240
|
+
/* persistence best-effort */
|
|
241
|
+
}
|
|
242
|
+
return enriched;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function events() {
|
|
246
|
+
let raw = '';
|
|
247
|
+
try {
|
|
248
|
+
raw = fs.readFileSync(eventsFile, 'utf8');
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
return raw
|
|
253
|
+
.split('\n')
|
|
254
|
+
.map((l) => l.trim())
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map((l) => {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(l);
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
.filter(Boolean);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// A consumer's agent absorbed a recipe and built it: the three-way moment.
|
|
267
|
+
function recordUse({ recipeId, summary, consumer = 'you' }) {
|
|
268
|
+
const r = getRecipe(recipeId);
|
|
269
|
+
if (!r) return { ok: false, error: `no recipe "${recipeId}" on the shelf` };
|
|
270
|
+
const recurring = r.reward?.perUseValue || 0;
|
|
271
|
+
// A recurring SERVICE recipe (tool/API) earns per-use, NOT a build fee — so the
|
|
272
|
+
// build itself credits 0, and the meter ticks up per match. This keeps the live
|
|
273
|
+
// meter ("$X · N matches") reconciled with the "earns per match, ongoing" story.
|
|
274
|
+
// Pure know-how recipes earn their one-time build-time slice here.
|
|
275
|
+
const oneTime = r.service ? 0 : (r.reward?.oneTimeValue || 0);
|
|
276
|
+
const evt = appendEvent({
|
|
277
|
+
type: 'use',
|
|
278
|
+
recipeId: r.id,
|
|
279
|
+
title: r.title,
|
|
280
|
+
producer: r.producer,
|
|
281
|
+
consumer,
|
|
282
|
+
summary: summary || r.title,
|
|
283
|
+
value: oneTime,
|
|
284
|
+
unit: r.service ? 'recurring-start' : 'one-time',
|
|
285
|
+
});
|
|
286
|
+
const threeWay = {
|
|
287
|
+
you: `You got: ${summary || r.title}`,
|
|
288
|
+
producer: r.service
|
|
289
|
+
? `${r.producer?.name || 'The producer'} just gained a customer — your site uses their matching on every candidate, ongoing.`
|
|
290
|
+
: `${r.producer?.name || 'The producer'} earns a one-time share for the know-how you built on.`,
|
|
291
|
+
platform: 'VentureWild matched you to a proven recipe — and recorded it.',
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
event: evt,
|
|
296
|
+
recipe: card(r),
|
|
297
|
+
threeWay,
|
|
298
|
+
credit: { producer: r.producer, oneTime, recurring, unit: r.reward?.unit },
|
|
299
|
+
service: r.service || null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// TickUp's matching service ran once — recurring credit to the producer.
|
|
304
|
+
function recordServiceCall({ recipeId, serviceId, matches = 0 }) {
|
|
305
|
+
const r = getRecipe(recipeId) || shelf().find((x) => x.service?.id === serviceId);
|
|
306
|
+
const per = r?.reward?.perUseValue || 0;
|
|
307
|
+
return appendEvent({
|
|
308
|
+
type: 'service-call',
|
|
309
|
+
recipeId: r?.id || recipeId || null,
|
|
310
|
+
serviceId: serviceId || r?.service?.id || null,
|
|
311
|
+
producer: r?.producer || null,
|
|
312
|
+
matches,
|
|
313
|
+
value: per,
|
|
314
|
+
unit: 'per-use',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// The flip side (§3.7): the user's agent packaged a build into a listing.
|
|
319
|
+
function publishListing({ id, title, pitch, summary, tags = [], knowHow = '', producer, buildDir }) {
|
|
320
|
+
ensureDir();
|
|
321
|
+
const listings = readJsonSafe(listingsFile, []);
|
|
322
|
+
const arr = Array.isArray(listings) ? listings : [];
|
|
323
|
+
const slug =
|
|
324
|
+
id ||
|
|
325
|
+
`${normalize(title).split(' ').slice(0, 4).join('-') || 'listing'}-${rid().slice(0, 4)}`;
|
|
326
|
+
const listing = {
|
|
327
|
+
id: slug,
|
|
328
|
+
title: title || 'Untitled',
|
|
329
|
+
pitch: pitch || '',
|
|
330
|
+
summary: summary || pitch || '',
|
|
331
|
+
vendorDescription: summary || pitch || '',
|
|
332
|
+
producer: producer || { name: 'You', handle: 'you', kind: 'maker' },
|
|
333
|
+
tags,
|
|
334
|
+
knowHow,
|
|
335
|
+
buildDir: buildDir || null,
|
|
336
|
+
outcomeScore: 0.7, // new listing: a starting, unproven score (earns its place by working)
|
|
337
|
+
outcomeStats: { builds: 0, working: 0 },
|
|
338
|
+
safetyBadge: 'new',
|
|
339
|
+
rating: { stars: 0, count: 0 },
|
|
340
|
+
reward: { model: 'one-time', unit: 'per build', perUseValue: 0, oneTimeValue: 5.0 },
|
|
341
|
+
source: 'listing',
|
|
342
|
+
createdAt: Date.now(),
|
|
343
|
+
};
|
|
344
|
+
// de-dupe by id (re-publish overwrites)
|
|
345
|
+
const next = arr.filter((l) => l.id !== listing.id);
|
|
346
|
+
next.push(listing);
|
|
347
|
+
writeJsonAtomic(listingsFile, next);
|
|
348
|
+
appendEvent({
|
|
349
|
+
type: 'publish',
|
|
350
|
+
recipeId: listing.id,
|
|
351
|
+
title: listing.title,
|
|
352
|
+
producer: listing.producer,
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
listing: card(listing),
|
|
357
|
+
earning: {
|
|
358
|
+
model: listing.reward.model,
|
|
359
|
+
oneTimeValue: listing.reward.oneTimeValue,
|
|
360
|
+
note: 'Represented earnings — you earn each time someone builds on this. (Money is for later.)',
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- themes on the shelf (the safest cross-user artifact) -----------------
|
|
366
|
+
|
|
367
|
+
// Publish a theme as a listing. The payload is a hex-token bundle (validated by
|
|
368
|
+
// normalizeTheme — same security boundary as set_theme: data, never CSS). It
|
|
369
|
+
// lands in listings.json with kind:'theme' and shows on the Themes shelf.
|
|
370
|
+
function publishTheme({ title, pitch, summary, tags = [], producer, theme } = {}) {
|
|
371
|
+
ensureDir();
|
|
372
|
+
const bundle = normalizeTheme(theme || {});
|
|
373
|
+
const listings = readJsonSafe(listingsFile, []);
|
|
374
|
+
const arr = Array.isArray(listings) ? listings : [];
|
|
375
|
+
const slug = `theme-${normalize(title).split(' ').slice(0, 3).join('-') || 'custom'}-${rid().slice(0, 4)}`;
|
|
376
|
+
const listing = {
|
|
377
|
+
id: slug,
|
|
378
|
+
kind: 'theme',
|
|
379
|
+
source: 'theme',
|
|
380
|
+
title: title || bundle.name || 'Custom theme',
|
|
381
|
+
pitch: pitch || '',
|
|
382
|
+
summary: summary || pitch || `A ${bundle.mode} theme.`,
|
|
383
|
+
vendorDescription: summary || pitch || '',
|
|
384
|
+
producer: producer || { name: 'You', handle: 'you', kind: 'maker' },
|
|
385
|
+
tags,
|
|
386
|
+
theme: bundle,
|
|
387
|
+
outcomeScore: 0.7,
|
|
388
|
+
outcomeStats: { builds: 0, working: 0 },
|
|
389
|
+
safetyBadge: 'new',
|
|
390
|
+
rating: { stars: 0, count: 0 },
|
|
391
|
+
reward: { model: 'one-time', unit: 'per apply', oneTimeValue: 1.5, perUseValue: 0 },
|
|
392
|
+
createdAt: Date.now(),
|
|
393
|
+
};
|
|
394
|
+
const next = arr.filter((l) => l.id !== listing.id);
|
|
395
|
+
next.push(listing);
|
|
396
|
+
writeJsonAtomic(listingsFile, next);
|
|
397
|
+
appendEvent({ type: 'publish', recipeId: listing.id, title: listing.title, producer: listing.producer });
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
listing: card(listing),
|
|
401
|
+
earning: { model: 'one-time', oneTimeValue: listing.reward.oneTimeValue, note: 'You earn each time someone applies it. (Money is for later.)' },
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// A consumer applied a theme from the shelf: record the three-way moment (they
|
|
406
|
+
// got the look · the producer earns · platform recorded it) and hand back the
|
|
407
|
+
// validated bundle for the browser to apply.
|
|
408
|
+
function recordThemeApply({ themeId, consumer = 'you' } = {}) {
|
|
409
|
+
const r = getRecipe(themeId);
|
|
410
|
+
if (!r || r.kind !== 'theme') return { ok: false, error: `no theme "${themeId}" on the shelf` };
|
|
411
|
+
const oneTime = r.reward?.oneTimeValue || 0;
|
|
412
|
+
const evt = appendEvent({
|
|
413
|
+
type: 'use',
|
|
414
|
+
recipeId: r.id,
|
|
415
|
+
title: r.title,
|
|
416
|
+
producer: r.producer,
|
|
417
|
+
consumer,
|
|
418
|
+
summary: `Applied the "${r.title}" theme`,
|
|
419
|
+
value: oneTime,
|
|
420
|
+
unit: 'one-time',
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
ok: true,
|
|
424
|
+
event: evt,
|
|
425
|
+
theme: normalizeTheme(r.theme || {}),
|
|
426
|
+
title: r.title,
|
|
427
|
+
threeWay: {
|
|
428
|
+
you: `Your workspace now wears "${r.title}".`,
|
|
429
|
+
producer: `${r.producer?.name || 'The producer'} earns a one-time share for the look you adopted.`,
|
|
430
|
+
platform: 'VentureWild matched you to a proven theme — and recorded it.',
|
|
431
|
+
},
|
|
432
|
+
credit: { producer: r.producer, oneTime, unit: r.reward?.unit },
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// --- self-seed drafts (review-gated) --------------------------------------
|
|
437
|
+
|
|
438
|
+
function listDrafts() {
|
|
439
|
+
const d = readJsonSafe(draftsFile, []);
|
|
440
|
+
return Array.isArray(d) ? d : [];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function getDraft(id) {
|
|
444
|
+
return listDrafts().find((d) => d.id === id) || null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Stage a recipe the agent extracted from the user's existing work — does NOT
|
|
448
|
+
// publish. `sourceNote` records (for the user's review) what it was generalized
|
|
449
|
+
// from. Returns a review-shaped object the UI renders for approval.
|
|
450
|
+
function stageDraft({ title, pitch, summary, tags = [], knowHow = '', sourceNote = '' }) {
|
|
451
|
+
ensureDir();
|
|
452
|
+
const drafts = listDrafts();
|
|
453
|
+
const draft = {
|
|
454
|
+
id: `draft-${rid()}`,
|
|
455
|
+
title: title || 'Untitled',
|
|
456
|
+
pitch: pitch || '',
|
|
457
|
+
summary: summary || pitch || '',
|
|
458
|
+
tags: Array.isArray(tags) ? tags : [],
|
|
459
|
+
knowHow: knowHow || '',
|
|
460
|
+
sourceNote: sourceNote || '',
|
|
461
|
+
createdAt: Date.now(),
|
|
462
|
+
};
|
|
463
|
+
drafts.push(draft);
|
|
464
|
+
writeJsonAtomic(draftsFile, drafts);
|
|
465
|
+
return draft;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function discardDraft(id) {
|
|
469
|
+
const drafts = listDrafts();
|
|
470
|
+
const next = drafts.filter((d) => d.id !== id);
|
|
471
|
+
writeJsonAtomic(draftsFile, next);
|
|
472
|
+
return { ok: next.length !== drafts.length };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Promote a reviewed draft onto the shelf — publishes EXACTLY what was reviewed
|
|
476
|
+
// (you publish what you saw), then removes it from the draft queue.
|
|
477
|
+
function publishDraft(id, { producer } = {}) {
|
|
478
|
+
const draft = getDraft(id);
|
|
479
|
+
if (!draft) return { ok: false, error: `no draft "${id}"` };
|
|
480
|
+
const res = publishListing({
|
|
481
|
+
title: draft.title,
|
|
482
|
+
pitch: draft.pitch,
|
|
483
|
+
summary: draft.summary,
|
|
484
|
+
tags: draft.tags,
|
|
485
|
+
knowHow: draft.knowHow,
|
|
486
|
+
producer: producer || { name: 'You', handle: 'you', kind: 'maker' },
|
|
487
|
+
});
|
|
488
|
+
discardDraft(id);
|
|
489
|
+
return res;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function setPreview({ dir: buildDir, recipeId, port = 4000 }) {
|
|
493
|
+
ensureDir();
|
|
494
|
+
const payload = { dir: buildDir, recipeId: recipeId || null, port, ts: Date.now() };
|
|
495
|
+
writeJsonAtomic(previewFile, payload);
|
|
496
|
+
return payload;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function getPreview() {
|
|
500
|
+
return readJsonSafe(previewFile, null);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Aggregate everything the UI needs: transactions, per-producer earnings,
|
|
504
|
+
// per-service usage meters, and the user's own listings.
|
|
505
|
+
function computeState() {
|
|
506
|
+
const evs = events();
|
|
507
|
+
const earnings = {}; // handle -> { name, total, uses, serviceCalls, matches }
|
|
508
|
+
const meters = {}; // serviceId -> { calls, matches, value, producer }
|
|
509
|
+
const transactions = [];
|
|
510
|
+
for (const e of evs) {
|
|
511
|
+
const h = e.producer?.handle || 'unknown';
|
|
512
|
+
const bucket = (earnings[h] ??= {
|
|
513
|
+
name: e.producer?.name || h,
|
|
514
|
+
handle: h,
|
|
515
|
+
total: 0,
|
|
516
|
+
uses: 0,
|
|
517
|
+
serviceCalls: 0,
|
|
518
|
+
matches: 0,
|
|
519
|
+
});
|
|
520
|
+
if (e.type === 'use') {
|
|
521
|
+
bucket.total += e.value || 0;
|
|
522
|
+
bucket.uses += 1;
|
|
523
|
+
transactions.push(e);
|
|
524
|
+
} else if (e.type === 'service-call') {
|
|
525
|
+
bucket.total += e.value || 0;
|
|
526
|
+
bucket.serviceCalls += 1;
|
|
527
|
+
bucket.matches += e.matches || 0;
|
|
528
|
+
const sid = e.serviceId || 'service';
|
|
529
|
+
const m = (meters[sid] ??= { calls: 0, matches: 0, value: 0, producer: e.producer || null });
|
|
530
|
+
m.calls += 1;
|
|
531
|
+
m.matches += e.matches || 0;
|
|
532
|
+
m.value += e.value || 0;
|
|
533
|
+
} else if (e.type === 'publish') {
|
|
534
|
+
transactions.push(e);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
transactions: transactions.slice(-50),
|
|
539
|
+
earnings: Object.values(earnings).sort((a, b) => b.total - a.total),
|
|
540
|
+
meters,
|
|
541
|
+
listings: (readJsonSafe(listingsFile, []) || []).map((l) => card(l)),
|
|
542
|
+
totals: {
|
|
543
|
+
events: evs.length,
|
|
544
|
+
creditPaid: Object.values(earnings).reduce((s, b) => s + b.total, 0),
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
dir,
|
|
551
|
+
eventsFile,
|
|
552
|
+
listingsFile,
|
|
553
|
+
previewFile,
|
|
554
|
+
draftsFile,
|
|
555
|
+
shelf,
|
|
556
|
+
getRecipe,
|
|
557
|
+
card,
|
|
558
|
+
search,
|
|
559
|
+
recordUse,
|
|
560
|
+
recordServiceCall,
|
|
561
|
+
publishListing,
|
|
562
|
+
publishTheme,
|
|
563
|
+
recordThemeApply,
|
|
564
|
+
listDrafts,
|
|
565
|
+
getDraft,
|
|
566
|
+
stageDraft,
|
|
567
|
+
discardDraft,
|
|
568
|
+
publishDraft,
|
|
569
|
+
setPreview,
|
|
570
|
+
getPreview,
|
|
571
|
+
computeState,
|
|
572
|
+
events,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function money(n) {
|
|
577
|
+
const v = Number(n) || 0;
|
|
578
|
+
return `$${v.toFixed(2)}`;
|
|
579
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Bazaar runtime glue for the main server: the agent-facing system prompt and the
|
|
2
|
+
// generated --mcp-config that wires `claude` to the bazaar MCP server.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
export const MCP_SERVER_PATH = path.join(__dirname, 'mcp-server.mjs');
|
|
11
|
+
|
|
12
|
+
// Appended to the agent's system prompt on user chat turns. Deliberately sets a
|
|
13
|
+
// *disposition* ("consult the shelf when it plausibly fits; surface only on a
|
|
14
|
+
// strong match; otherwise build normally and stay silent") rather than a blind
|
|
15
|
+
// "always search" habit — the ranking decides "strong enough", not this text. Also
|
|
16
|
+
// the copy guardrails: never expose plumbing words; sell the dish, not the meat.
|
|
17
|
+
export const BAZAAR_SYSTEM_PROMPT = [
|
|
18
|
+
"This workspace has a *bazaar* — a shelf of proven recipes other producers have shared, each",
|
|
19
|
+
"measured by how often it actually gets someone a working result. You can reach onto it with the",
|
|
20
|
+
"bazaar tools (mcp__bazaar__search_shelf, open_recipe, launch_preview, record_use, publish_listing,",
|
|
21
|
+
"draft_recipe, publish_draft).",
|
|
22
|
+
"",
|
|
23
|
+
"When the user describes something to build or an outcome they want, and it PLAUSIBLY matches a",
|
|
24
|
+
"known proven build, quietly call search_shelf first. If there is a STRONG, clearly-relevant match,",
|
|
25
|
+
"surface it in your own voice before building from scratch: say in one line that there's a proven",
|
|
26
|
+
"way to do exactly this and who it's from, and offer to use it. If nothing is a strong match, just",
|
|
27
|
+
"build normally and do NOT mention the bazaar.",
|
|
28
|
+
"",
|
|
29
|
+
"When the user accepts a recipe: call open_recipe to absorb the producer's know-how, build it",
|
|
30
|
+
"one-shot following that know-how, call launch_preview with the build folder so the live preview",
|
|
31
|
+
"opens, then call record_use so the producer gets credit and the transaction is recorded.",
|
|
32
|
+
"",
|
|
33
|
+
"After the user has something that works and is reusable, you may offer to package it so others can",
|
|
34
|
+
"build on it (\"want me to make this usable by others? you'd earn when they do\"). If they accept, or",
|
|
35
|
+
"if they ask to list/sell it, call publish_listing.",
|
|
36
|
+
"",
|
|
37
|
+
"The bazaar also has THEMES (workspace looks). If the user loves a look they're using and wants to",
|
|
38
|
+
"share/list/sell it — or asks you to — call publish_theme with its colours (mode, accent, and the",
|
|
39
|
+
"token hex values). Themes are the safest thing to share (just colours), and the user earns each time",
|
|
40
|
+
"someone applies theirs. To browse/apply existing themes the user can open the bazaar's Themes shelf.",
|
|
41
|
+
"",
|
|
42
|
+
"To SEED the shelf from the user's own past work: when they point you at a folder, read it, find the",
|
|
43
|
+
"*reusable patterns* (the architecture/approach — not the client's content), and for each call",
|
|
44
|
+
"draft_recipe with GENERALIZED know-how. STRIP every client name, domain, credential, and piece of",
|
|
45
|
+
"data; write the method so another agent could rebuild a similar thing from scratch. Stage one draft",
|
|
46
|
+
"per pattern, show the user a short summary of each, and call publish_draft ONLY for the ones they",
|
|
47
|
+
"explicitly approve. Never publish straight from their real work without review.",
|
|
48
|
+
"",
|
|
49
|
+
"Speak like a capable assistant, never an engineer. NEVER say \"API\", \"endpoint\", \"service URL\", or",
|
|
50
|
+
"\"port\" to the user — say things like \"TickUp's matching, their method stays on their side\". Keep",
|
|
51
|
+
"replies short. Money shown in the marketplace is represented for now (\"settles later\"), not real.",
|
|
52
|
+
].join('\n');
|
|
53
|
+
|
|
54
|
+
// Write the MCP config `claude` loads via --mcp-config. Points at the bazaar MCP
|
|
55
|
+
// server (run with the same node) and pins the global dir so the child shares our
|
|
56
|
+
// state. Returns the config path, or null if it can't be written.
|
|
57
|
+
export function writeBazaarMcpConfig({ baseDir, globalDir, nodePath = process.execPath } = {}) {
|
|
58
|
+
const cfg = {
|
|
59
|
+
mcpServers: {
|
|
60
|
+
bazaar: {
|
|
61
|
+
command: nodePath,
|
|
62
|
+
args: [MCP_SERVER_PATH],
|
|
63
|
+
env: globalDir ? { WILD_WORKSPACE_GLOBAL_DIR: globalDir } : {},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const file = path.join(baseDir, 'mcp-config.json');
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2));
|
|
71
|
+
return file;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|