@marver-design/marver 0.21.0 → 0.22.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 (45) hide show
  1. package/CHANGELOG.md +112 -15
  2. package/README.md +17 -4
  3. package/dist/{bake-BID6mo-N.mjs → bake-BSX4XR_U.mjs} +1 -1
  4. package/dist/board-status-CWGHIdo_.mjs +1307 -0
  5. package/dist/boards-BTGNPVMx.mjs +187 -0
  6. package/dist/{build-C7MqQ7hq.mjs → build-Dd2Qm3OG.mjs} +74 -13
  7. package/dist/cli.mjs +67 -8
  8. package/dist/config-DJxMRVD8.mjs +373 -0
  9. package/dist/context-DUAENlJ6.mjs +600 -0
  10. package/dist/{daemon-CRZFpl6K.mjs → daemon-BmkwErpC.mjs} +1 -1
  11. package/dist/{dev-BNZF4Mup.mjs → dev-BW7a5kDF.mjs} +8 -6
  12. package/dist/{init-C34BY3R4.mjs → init-NJ3xjLVu.mjs} +39 -44
  13. package/dist/managed-write-Bo-oPc-i.mjs +71 -0
  14. package/dist/{manifest-mMfUhPtL.mjs → manifest-BqBcMJcd.mjs} +19 -376
  15. package/dist/{plugin-omHLCn91.mjs → plugin-8XntSCx_.mjs} +234 -105
  16. package/dist/{poster-BvxiAzy1.mjs → poster-DRZDszTI.mjs} +1 -1
  17. package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-CCWRV9iX.mjs} +2 -2
  18. package/dist/{shot-DswS4iRK.mjs → shot-C22Ues04.mjs} +2 -2
  19. package/docs/boards-and-folders.md +161 -0
  20. package/docs/context.md +117 -0
  21. package/docs/sharing.md +12 -2
  22. package/package.json +1 -1
  23. package/src/client/shell/BoardList.tsx +33 -3
  24. package/src/client/shell/ContextMenu.tsx +16 -5
  25. package/src/client/shell/StatusPicker.tsx +91 -0
  26. package/src/client/shell/board-icons.tsx +75 -0
  27. package/src/client/shell/store.ts +88 -9
  28. package/src/client/shell/styles.css +34 -3
  29. package/src/shared/board-tree.ts +30 -13
  30. package/src/shared/board-types.ts +103 -0
  31. package/src/shared/context.ts +265 -0
  32. package/src/shared/status.ts +157 -0
  33. package/templates/AGENTS-embedded.md +1 -0
  34. package/templates/AGENTS-studio.md +1 -0
  35. package/templates/context/INDEX.md +50 -0
  36. package/templates/context/map.json +6 -0
  37. package/templates/context/shipped-knowledge.md +19 -0
  38. package/templates/context/shipped.md +28 -0
  39. package/templates/instructions/boards.md +31 -0
  40. package/templates/instructions/context.md +114 -0
  41. package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
  42. package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
  43. package/templates/playbooks/reorganize-context/eval.md +93 -0
  44. package/dist/boards-BwiDAmPf.mjs +0 -337
  45. package/dist/boards-DnLewfj8.mjs +0 -71
@@ -0,0 +1,373 @@
1
+ import { U as readDescription } from "./board-status-CWGHIdo_.mjs";
2
+ import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
3
+ import { delimiter, isAbsolute, join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ //#region src/server/detect.ts
6
+ function readJson(file) {
7
+ try {
8
+ return JSON.parse(stripJsonComments(readFileSync(file, "utf8")));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ function stripJsonComments(s) {
14
+ let out = "", inStr = false, inLine = false, inBlock = false;
15
+ for (let i = 0; i < s.length; i++) {
16
+ const c = s[i], n = s[i + 1];
17
+ if (inLine) {
18
+ if (c === "\n") {
19
+ inLine = false;
20
+ out += c;
21
+ }
22
+ continue;
23
+ }
24
+ if (inBlock) {
25
+ if (c === "*" && n === "/") {
26
+ inBlock = false;
27
+ i++;
28
+ }
29
+ continue;
30
+ }
31
+ if (inStr) {
32
+ out += c;
33
+ if (c === "\\") {
34
+ out += n ?? "";
35
+ i++;
36
+ } else if (c === "\"") inStr = false;
37
+ continue;
38
+ }
39
+ if (c === "\"") {
40
+ inStr = true;
41
+ out += c;
42
+ continue;
43
+ }
44
+ if (c === "/" && n === "/") {
45
+ inLine = true;
46
+ i++;
47
+ continue;
48
+ }
49
+ if (c === "/" && n === "*") {
50
+ inBlock = true;
51
+ i++;
52
+ continue;
53
+ }
54
+ out += c;
55
+ }
56
+ let res = "", inS = false;
57
+ for (let i = 0; i < out.length; i++) {
58
+ const c = out[i];
59
+ if (inS) {
60
+ res += c;
61
+ if (c === "\\") {
62
+ res += out[i + 1] ?? "";
63
+ i++;
64
+ } else if (c === "\"") inS = false;
65
+ continue;
66
+ }
67
+ if (c === "\"") {
68
+ inS = true;
69
+ res += c;
70
+ continue;
71
+ }
72
+ if (c === ",") {
73
+ let j = i + 1;
74
+ while (j < out.length && /\s/.test(out[j])) j++;
75
+ if (out[j] === "}" || out[j] === "]") continue;
76
+ }
77
+ res += c;
78
+ }
79
+ return res;
80
+ }
81
+ function detectHost(root) {
82
+ const pkg = readJson(join(root, "package.json")) ?? {};
83
+ const deps = {
84
+ ...pkg.dependencies,
85
+ ...pkg.devDependencies
86
+ };
87
+ const twRange = deps["tailwindcss"];
88
+ const tailwind = !twRange ? null : /(^|[^\d])4\./.test(twRange) || twRange.startsWith("^4") || twRange.startsWith("~4") ? 4 : 3;
89
+ const router = deps["next"] ? "next" : deps["react-router"] || deps["react-router-dom"] ? "react-router" : null;
90
+ const routerPkg = deps["react-router-dom"] ? "react-router-dom" : "react-router";
91
+ const toaster = deps["sonner"] ? "sonner" : deps["react-hot-toast"] ? "react-hot-toast" : null;
92
+ let shadcn = null;
93
+ const comp = readJson(join(root, "components.json"));
94
+ if (comp?.tailwind?.css) shadcn = {
95
+ themeCss: comp.tailwind.css,
96
+ uiAlias: comp.aliases?.ui ?? "@/components/ui"
97
+ };
98
+ const themeCss = shadcn?.themeCss && existsSync(join(root, shadcn.themeCss)) ? shadcn.themeCss : firstExisting(root, [
99
+ "src/index.css",
100
+ "src/styles/theme.css",
101
+ "src/styles/globals.css",
102
+ "src/app/globals.css",
103
+ "app/globals.css",
104
+ "src/globals.css",
105
+ "src/style.css",
106
+ "styles/globals.css"
107
+ ]);
108
+ const ts = readJson(join(root, "tsconfig.json"));
109
+ const include = ts?.include;
110
+ const exclude = ts?.exclude ?? [];
111
+ const sweeps = ts != null && !exclude.some((e) => e === "design" || e.startsWith("design/")) && (!include || include.some((i) => i === "." || i.startsWith("**") || i.startsWith("design")));
112
+ return {
113
+ tailwind,
114
+ router,
115
+ routerPkg,
116
+ toaster,
117
+ shadcn,
118
+ themeCss,
119
+ tsconfigSweepsDesign: !!sweeps
120
+ };
121
+ }
122
+ function firstExisting(root, candidates) {
123
+ for (const c of candidates) if (existsSync(join(root, c))) return c;
124
+ return null;
125
+ }
126
+ //#endregion
127
+ //#region src/server/jam/agent.ts
128
+ /**
129
+ * Which coding agent drives this repo.
130
+ *
131
+ * Live Jam is ON by default, so this is the answer to "on by default with WHAT" - asked
132
+ * once by `init` (which writes the answer into design/config.ts, where it is visible and
133
+ * editable) and again at every dev boot, so a workspace that predates the block - or a
134
+ * human who switched tools - still jams without editing anything.
135
+ *
136
+ * The tool RUNNING us wins: `init` is usually run BY the agent, so its env markers are
137
+ * evidence rather than a guess. Whatever wins must still be on PATH - the daemon has to
138
+ * spawn it.
139
+ */
140
+ /** Ordered by preference when a machine has several installed and none is running us. */
141
+ const AGENTS = [
142
+ "claude",
143
+ "codex",
144
+ "cursor",
145
+ "droid",
146
+ "opencode",
147
+ "grok",
148
+ "pi"
149
+ ];
150
+ /** Env vars each CLI sets in the processes it spawns. Deliberately NOT CODEX_HOME or
151
+ * ANTHROPIC_API_KEY: those are configuration a human exports by hand, not evidence that
152
+ * the tool is running right now. (opencode's `AGENT=1` and pi's `AI_AGENT` are skipped
153
+ * for the same reason in reverse - too generic to prove WHICH tool is running.)
154
+ * droid and grok set no marker at all in the shells they spawn (verified against the
155
+ * grok-build source and droid 0.200.0), so they are found by PATH order only. */
156
+ const MARKERS = {
157
+ claude: ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"],
158
+ codex: ["CODEX_SANDBOX", "CODEX_THREAD_ID"],
159
+ cursor: ["CURSOR_AGENT"],
160
+ droid: [],
161
+ opencode: ["OPENCODE", "OPENCODE_PID"],
162
+ grok: [],
163
+ pi: ["PI_CODING_AGENT", "PI_SESSION_ID"]
164
+ };
165
+ /** The executable each agent name maps to. Only cursor differs: its CLI installs BOTH
166
+ * `cursor-agent` and a bare `agent` - and grok's installer symlinks `agent` too, so the
167
+ * short name is a coin flip on a machine with both. The long name is unambiguous. */
168
+ const AGENT_BIN = {
169
+ claude: "claude",
170
+ codex: "codex",
171
+ cursor: "cursor-agent",
172
+ droid: "droid",
173
+ opencode: "opencode",
174
+ grok: "grok",
175
+ pi: "pi"
176
+ };
177
+ /** The valid `jam.agent` values as an English list - one string, so every surface
178
+ * (config warning, init note, dev boot line) names the same set and none goes stale. */
179
+ const AGENT_NAMES = `${AGENTS.slice(0, -1).map((a) => `"${a}"`).join(", ")}, or "${AGENTS[AGENTS.length - 1]}"`;
180
+ /** Is `cmd` an executable FILE on PATH? A direct stat per PATH entry - no spawn, no shell,
181
+ * so this is safe to call on every dev boot. Two deliberate narrowings, both in service of
182
+ * "found means spawnable", because arming an agent that cannot run is worse than staying off:
183
+ *
184
+ * - `isFile`, because a directory carries the execute bit too (it means "traversable"), so an
185
+ * access check alone would call a folder named `claude` an agent.
186
+ * - the bare name only, no PATHEXT: the daemon spawns without a shell, and Node cannot run a
187
+ * Windows `.cmd`/`.bat` shim that way. Finding one would arm a job that fails on every run.
188
+ *
189
+ * Only ABSOLUTE PATH entries count. An empty entry means the current directory on POSIX, and
190
+ * a relative one (`.`, `bin`) resolves against it too - and the current directory is the repo
191
+ * that was just opened, so a `claude` binary shipped inside it is precisely what must never
192
+ * be found and spawned. */
193
+ function onPath(cmd, env = process.env) {
194
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
195
+ if (!isAbsolute(dir)) continue;
196
+ try {
197
+ const file = join(dir, cmd);
198
+ if (statSync(file).isFile()) {
199
+ accessSync(file, constants.X_OK);
200
+ return true;
201
+ }
202
+ } catch {}
203
+ }
204
+ return false;
205
+ }
206
+ /** The agent to jam with, or undefined when this machine has none installed.
207
+ *
208
+ * One knowingly-imperfect case: agents nest, and env is inherited, so a codex run started
209
+ * FROM Claude Code carries both marker families and this picks claude. Env has no depth to
210
+ * read, and walking the process tree to find the nearest agent ancestor costs more than the
211
+ * case is worth - so the answer is made visible instead of clever: `init` prints the agent
212
+ * it chose and writes it into design/config.ts, and instructions/jam.md has the agent confirm
213
+ * that line names the tool it actually is. One word to correct, once per repo. */
214
+ function detectAgent(env = process.env) {
215
+ const running = AGENTS.find((a) => MARKERS[a].some((k) => env[k]));
216
+ if (running && onPath(AGENT_BIN[running], env)) return running;
217
+ return AGENTS.find((a) => onPath(AGENT_BIN[a], env));
218
+ }
219
+ //#endregion
220
+ //#region src/server/config.ts
221
+ const DEFAULTS = {
222
+ mode: "studio",
223
+ theme: null,
224
+ viewports: {
225
+ mobile: {
226
+ width: 390,
227
+ height: 844
228
+ },
229
+ tablet: {
230
+ width: 768,
231
+ height: 1024
232
+ },
233
+ laptop: {
234
+ width: 1280,
235
+ height: 800
236
+ },
237
+ monitor: {
238
+ width: 1920,
239
+ height: 1080
240
+ }
241
+ },
242
+ themes: ["light", "dark"],
243
+ port: 5199,
244
+ zoomSpeed: 1,
245
+ share: { branding: true }
246
+ };
247
+ /** Load design/config.ts via native TS import (Node >= 22.18). Missing or broken fields fall back to defaults. */
248
+ async function loadConfig(root) {
249
+ const file = join(root, "design", "config.ts");
250
+ if (!existsSync(file)) return {
251
+ ...DEFAULTS,
252
+ ...jamFields(void 0)
253
+ };
254
+ try {
255
+ const user = (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default ?? {};
256
+ return {
257
+ ...DEFAULTS,
258
+ ...user,
259
+ viewports: validViewports(user.viewports) ?? DEFAULTS.viewports,
260
+ themes: Array.isArray(user.themes) && user.themes.length ? user.themes.map(String) : DEFAULTS.themes,
261
+ port: validPort(user.port) ?? DEFAULTS.port,
262
+ zoomSpeed: validZoom(user.zoomSpeed) ?? DEFAULTS.zoomSpeed,
263
+ description: readDescription(user.description),
264
+ share: {
265
+ branding: user.share?.branding !== false,
266
+ name: typeof user.share?.name === "string" ? user.share.name : void 0,
267
+ logo: typeof user.share?.logo === "string" ? user.share.logo : void 0,
268
+ frontDoor: user.share?.frontDoor !== false && user.share?.front_door !== false,
269
+ notify: user.share?.notify !== false
270
+ },
271
+ ...jamFields(user.jam)
272
+ };
273
+ } catch (err) {
274
+ console.error(`[marver] design/config.ts failed to load, using defaults and leaving Live Jam OFF:\n ${err.message}`);
275
+ return {
276
+ ...DEFAULTS,
277
+ jamOff: "unreadable"
278
+ };
279
+ }
280
+ }
281
+ const validDim = (n) => typeof n === "number" && Number.isFinite(n) && n >= 1 && n <= 2e4;
282
+ function validPort(n) {
283
+ return typeof n === "number" && Number.isInteger(n) && n > 0 && n < 65536 ? n : null;
284
+ }
285
+ function validZoom(n) {
286
+ return typeof n === "number" && Number.isFinite(n) && n >= .1 && n <= 10 ? n : null;
287
+ }
288
+ /** Six frames at once is what a jam actually feels like - at 3, half of a multi-frame
289
+ * ask sat waiting on the other half while the human watched. */
290
+ const DEFAULT_CONCURRENCY = 6;
291
+ /** A config value, printable in a warning. Never throws - a formatter that can crash inside an
292
+ * error path (JSON.stringify does, on a BigInt) turns a clear message into a mystery. */
293
+ const show = (v) => {
294
+ try {
295
+ return JSON.stringify(v) ?? String(v);
296
+ } catch {
297
+ return String(v);
298
+ }
299
+ };
300
+ /** A `{...}` written by hand, not a Date/Map/class instance that merely types as "object". */
301
+ const plainObject = (v) => {
302
+ if (!v || typeof v !== "object") return false;
303
+ const proto = Object.getPrototypeOf(v);
304
+ return proto === Object.prototype || proto === null;
305
+ };
306
+ /** The agent to arm with - or, when the human NAMED one we cannot use, why not (said once,
307
+ * here, because a bad `jam.agent` is a config-file error like any other).
308
+ *
309
+ * A named agent is never quietly swapped for another: running a different tool than the one
310
+ * asked for is worse than not running. Nor is one armed that cannot be spawned - that would
311
+ * claim every @marver mention, fail it, and never retry. */
312
+ function resolveAgent(named) {
313
+ if (named === void 0) return detectAgent() ?? "no-agent";
314
+ if (typeof named !== "string" || !Object.hasOwn(AGENT_BIN, named)) {
315
+ console.warn(`[marver] design/config.ts: jam.agent ${show(named)} is not an agent marver can spawn - Live Jam is off. Use ${AGENT_NAMES}.`);
316
+ return "bad-agent";
317
+ }
318
+ const agent = named;
319
+ if (!onPath(AGENT_BIN[agent])) {
320
+ console.warn(`[marver] design/config.ts names jam.agent "${agent}", but \`${AGENT_BIN[agent]}\` is not on PATH - Live Jam is off until it is installed.`);
321
+ return "bad-agent";
322
+ }
323
+ return agent;
324
+ }
325
+ /** Resolve the jam block. Live Jam is ON by default: an absent or partial `jam` resolves
326
+ * to whatever agent CLI this machine has, so a fresh workspace jams with nothing
327
+ * configured and an old one needs no re-init. `jam: false` is the off switch. */
328
+ function resolveJam(v) {
329
+ if (v === false) return "opted-out";
330
+ const shape = v === void 0 || v === true ? {} : typeof v === "string" ? { agent: v } : plainObject(v) ? v : null;
331
+ if (!shape) {
332
+ console.warn(`[marver] design/config.ts: jam must be false, an agent name, or an options object - got ${show(v)}. Live Jam is off.`);
333
+ return "bad-agent";
334
+ }
335
+ const j = shape;
336
+ const agent = resolveAgent(j.agent);
337
+ if (agent === "no-agent" || agent === "bad-agent") return agent;
338
+ const c = j.concurrency;
339
+ return {
340
+ agent,
341
+ concurrency: typeof c === "number" && Number.isInteger(c) && c >= 1 && c <= 16 ? c : DEFAULT_CONCURRENCY,
342
+ subagents: j.subagents !== false,
343
+ proactive: j.proactive === true
344
+ };
345
+ }
346
+ /** Exactly one of `jam` (armed) or `jamOff` (why not) - so the dev server can speak up about
347
+ * the one off-state nothing has reported yet, and stay quiet about the rest. BOTH keys are
348
+ * always written: these spread over the user's own `jam`, and a raw `jam: false` reaching the
349
+ * server as config would arm the daemon against a truthy object check. */
350
+ function jamFields(v) {
351
+ const r = resolveJam(v);
352
+ return typeof r === "string" ? {
353
+ jam: void 0,
354
+ jamOff: r
355
+ } : {
356
+ jam: r,
357
+ jamOff: void 0
358
+ };
359
+ }
360
+ function validViewports(v) {
361
+ if (!v || typeof v !== "object") return null;
362
+ const out = {};
363
+ for (const [k, val] of Object.entries(v)) {
364
+ const vp = val;
365
+ if (validDim(vp?.width) && validDim(vp?.height)) out[k] = {
366
+ width: Math.round(vp.width),
367
+ height: Math.round(vp.height)
368
+ };
369
+ }
370
+ return Object.keys(out).length ? out : null;
371
+ }
372
+ //#endregion
373
+ export { readJson as a, detectHost as i, loadConfig as n, detectAgent as r, DEFAULTS as t };