@marver-design/marver 0.8.0 → 0.8.1

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 (48) hide show
  1. package/CHANGELOG.md +97 -5
  2. package/README.md +72 -16
  3. package/dist/{auth-B36fMCM3.mjs → auth-KQ9Aj-nB.mjs} +1 -1
  4. package/dist/{build-B8z902TW.mjs → build-BBVQRetk.mjs} +5 -5
  5. package/dist/cli.mjs +16 -7
  6. package/dist/{collab-pLGzSvm5.mjs → collab-s3k5byM1.mjs} +3 -3
  7. package/dist/{comments-DSwMhdbZ.mjs → comments-BZBKhKRO.mjs} +2 -2
  8. package/dist/{comments-BrpC86Kf.mjs → comments-J06jqCVV.mjs} +3 -3
  9. package/dist/{daemon-C-huGHAM.mjs → daemon-DkyNOwIt.mjs} +30 -51
  10. package/dist/{dev-BYm9ebfN.mjs → dev-BTAhTie-.mjs} +19 -5
  11. package/dist/{init-DrLomSWq.mjs → init-DWdhjJD5.mjs} +16 -3
  12. package/dist/{ledger-wFvEIEGi.mjs → ledger-CbzTJrV2.mjs} +1 -1
  13. package/dist/{manifest-D3eaARf4.mjs → manifest-B4zcDGBf.mjs} +3 -3
  14. package/dist/{plugin-BVFuRfEo.mjs → plugin-BdQEeTLg.mjs} +41 -9
  15. package/dist/{serve-CwAfayJk.mjs → serve-CZqPnj19.mjs} +32 -8
  16. package/dist/{sync-Ch4Bymb1.mjs → sync-BJKKmy1n.mjs} +2 -2
  17. package/dist/work-CLrmY-vQ.mjs +97 -0
  18. package/dist/work-lzC-lPY0.mjs +76 -0
  19. package/package.json +16 -1
  20. package/src/client/const.ts +1 -1
  21. package/src/client/content/diagram.tsx +1 -1
  22. package/src/client/content/index.tsx +2 -2
  23. package/src/client/content/md.ts +1 -1
  24. package/src/client/content/palette.ts +2 -2
  25. package/src/client/frame-host/bridge.js +1 -1
  26. package/src/client/frame-host/inspect.js +1 -1
  27. package/src/client/frame-host/serialize.ts +2 -2
  28. package/src/client/shell/App.tsx +79 -22
  29. package/src/client/shell/Comments.tsx +24 -18
  30. package/src/client/shell/Play.tsx +2 -2
  31. package/src/client/shell/canvas/Canvas.tsx +1 -1
  32. package/src/client/shell/canvas/FrameNode.tsx +16 -11
  33. package/src/client/shell/canvas/snapshots.ts +3 -3
  34. package/src/client/shell/comments-store.ts +115 -42
  35. package/src/client/shell/hash.ts +2 -2
  36. package/src/client/shell/icons.tsx +1 -1
  37. package/src/client/shell/keys.ts +39 -0
  38. package/src/client/shell/mentions.ts +1 -1
  39. package/src/client/shell/perf.ts +1 -1
  40. package/src/client/shell/store.ts +99 -39
  41. package/src/client/shell/styles.css +12 -12
  42. package/src/client/shell/tidy.ts +5 -5
  43. package/src/client/stage/main.tsx +2 -2
  44. package/src/shared/events.ts +2 -2
  45. package/src/shared/utm.ts +22 -0
  46. package/templates/AGENTS-embedded.md +48 -1
  47. package/templates/AGENTS-studio.md +48 -1
  48. package/templates/instructions/publish.md +1 -1
@@ -1,6 +1,6 @@
1
1
  import { n as NAME, r as PKG } from "./cli.mjs";
2
- import { o as loadConfig, s as detectHost } from "./manifest-D3eaARf4.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BVFuRfEo.mjs";
2
+ import { o as loadConfig, s as detectHost } from "./manifest-B4zcDGBf.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BdQEeTLg.mjs";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
@@ -141,7 +141,7 @@ async function dev(root, portFlag) {
141
141
  const port = typeof addr === "object" && addr ? addr.port : picked.port;
142
142
  if (picked.fellBack) console.log(`\n port ${desiredPort} is in use - serving "${projectName}" on ${port} instead`);
143
143
  console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
144
- const { loadCollab, syncOnce } = await import("./sync-Ch4Bymb1.mjs");
144
+ const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
145
145
  if (loadCollab(root)) console.log(` comments: syncing with the published canvas (design/.local/collab.json)\n`);
146
146
  let syncing = false;
147
147
  const tick = async () => {
@@ -156,9 +156,23 @@ async function dev(root, portFlag) {
156
156
  };
157
157
  tick();
158
158
  setInterval(tick, 3e4).unref();
159
+ const { workActivity, writeDevInfo, removeDevInfo } = await import("./work-CLrmY-vQ.mjs");
160
+ const unsubscribe = workActivity.onChange((frames) => server.ws.send("sh:jam-activity", { frames }));
161
+ const sweep = setInterval(() => workActivity.sweep(), 15e3);
162
+ sweep.unref?.();
163
+ writeDevInfo(root, port);
164
+ {
165
+ const close = server.close.bind(server);
166
+ server.close = (async () => {
167
+ unsubscribe();
168
+ clearInterval(sweep);
169
+ removeDevInfo(root);
170
+ return close();
171
+ });
172
+ }
159
173
  if (config.jam?.agent) {
160
- const { startJam } = await import("./daemon-C-huGHAM.mjs");
161
- const jam = startJam(root, config.jam, (m) => console.log(m), (frames) => server.ws.send("sh:jam-activity", { frames }), (board) => server.ws.send("sh:jam-comment", { board }));
174
+ const { startJam } = await import("./daemon-DkyNOwIt.mjs");
175
+ const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
162
176
  if (jam) {
163
177
  const close = server.close.bind(server);
164
178
  server.close = (async () => {
@@ -1,5 +1,5 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
- import { a as DEFAULTS, c as readJson, i as writeManifest, r as scanFrames, s as detectHost } from "./manifest-D3eaARf4.mjs";
2
+ import { a as DEFAULTS, c as readJson, i as writeManifest, r as scanFrames, s as detectHost } from "./manifest-B4zcDGBf.mjs";
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -105,6 +105,18 @@ function init(root, opts) {
105
105
  created.push("CLAUDE.md (updated)");
106
106
  }
107
107
  }
108
+ const rootAgentsMd = join(root, "AGENTS.md");
109
+ const pointerLine = `Before any design or canvas work, read design/AGENTS.md and follow it - it is binding.`;
110
+ if (!existsSync(rootAgentsMd)) {
111
+ writeFileSync(rootAgentsMd, `# ${NAME}\n\nThis repo designs with ${NAME}. ${pointerLine}\n`);
112
+ created.push("AGENTS.md");
113
+ } else {
114
+ const cur = readFileSync(rootAgentsMd, "utf8");
115
+ if (!cur.includes("design/AGENTS.md")) {
116
+ writeFileSync(rootAgentsMd, cur.replace(/\n*$/, "") + `\n\n${pointerLine}\n`);
117
+ created.push("AGENTS.md (updated)");
118
+ }
119
+ }
108
120
  const instrRoot = join(templates, "instructions");
109
121
  for (const e of readdirSync(instrRoot, { withFileTypes: true })) if (e.isDirectory()) {
110
122
  for (const f of readdirSync(join(instrRoot, e.name))) if (f.endsWith(".md")) writeManaged(`instructions/${e.name}/${f}`, readFileSync(join(instrRoot, e.name, f), "utf8"));
@@ -152,7 +164,8 @@ function init(root, opts) {
152
164
  writeFileSync(join(design, "boards", ".gitkeep"), "");
153
165
  created.push("design/boards/");
154
166
  }
155
- if (opts.demo && !existsSync(join(design, "scenes", "demo"))) {
167
+ const sceneDirs = existsSync(join(design, "scenes")) ? readdirSync(join(design, "scenes"), { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_")).length : 0;
168
+ if (opts.demo && sceneDirs === 0 && !existsSync(join(design, "scenes", "demo"))) {
156
169
  cpSync(join(templates, "demo"), join(design, "scenes", "demo"), { recursive: true });
157
170
  created.push("design/scenes/demo/ (3 frames)");
158
171
  }
@@ -174,7 +187,7 @@ function init(root, opts) {
174
187
  console.log(`\n commit design/ - only .local/ is ignored`);
175
188
  console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
176
189
  if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
177
- console.log(`\n next: npx ${NAME} dev (canvas on http://localhost:${DEFAULTS.port} by default)\n`);
190
+ console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
178
191
  if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
179
192
  }
180
193
  const MANAGED_PREFIX = "<!-- marver:managed ";
@@ -14,7 +14,7 @@ var __exportAll = (all, no_symbols) => {
14
14
  //#endregion
15
15
  //#region src/server/jam/ledger.ts
16
16
  /**
17
- * The device-bound authorization ledger (SPEC-live-jam §1) - the whole trust boundary.
17
+ * The device-bound authorization ledger - the whole trust boundary.
18
18
  *
19
19
  * When the dev POST accepts an owner-gated write, it records that event's id here. The
20
20
  * daemon's owner-trigger check is `has(root, id)`, never a synced field: sync copies
@@ -214,7 +214,7 @@ function validViewports(v) {
214
214
  //#region src/server/manifest.ts
215
215
  const FRAME_EXT = /\.(tsx|jsx|html)$/;
216
216
  const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
217
- /** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted (spec §6). */
217
+ /** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
218
218
  function extractMeta(src) {
219
219
  const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
220
220
  if (!m) return {};
@@ -238,7 +238,7 @@ function extractMeta(src) {
238
238
  if (intent) out.intent = intent;
239
239
  return out;
240
240
  }
241
- /** Content-frame detection (SPEC-026): LEXICAL by convention - the frame file
241
+ /** Content-frame detection: LEXICAL by convention - the frame file
242
242
  * itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
243
243
  * string in a UI frame can never misbadge it; barrels/re-exports are not
244
244
  * detected - meta.intent is the taught path and always works). Returns the
@@ -358,7 +358,7 @@ function scanFrames(root) {
358
358
  })).sort((a, b) => a.name.localeCompare(b.name))
359
359
  };
360
360
  }
361
- /** Variant groups (SPEC-023 §1). A group = 2+ frames in one DIRECTORY whose basenames
361
+ /** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
362
362
  * are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
363
363
  * directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
364
364
  * Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
@@ -1,6 +1,6 @@
1
1
  import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
2
2
  import { n as localProfile, t as isConnected } from "./profile-BkiWglVE.mjs";
3
- import { i as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-D3eaARf4.mjs";
3
+ import { i as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-B4zcDGBf.mjs";
4
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -14,7 +14,7 @@ function cookie(req, name) {
14
14
  const m = new RegExp(`(?:^|;\\s*)${name}=([^;]+)`).exec(String(req.headers.cookie ?? ""));
15
15
  return m ? m[1] : "";
16
16
  }
17
- /** The owner gate (SPEC-live-jam §1). The dev server is 127.0.0.1-only, so the live threat is a
17
+ /** The owner gate. The dev server is 127.0.0.1-only, so the live threat is a
18
18
  * cross-origin drive-by page firing a POST at localhost. Two defenses, mirroring the published
19
19
  * side (collab.ts:206): a double-submit cookie (mv_c is JS-readable same-origin, so a cross-origin
20
20
  * page cannot read it to echo x-mv-c), plus an Origin allowlist. Only a POST that passes this is
@@ -185,7 +185,7 @@ function apiMiddleware(root) {
185
185
  }
186
186
  const dir = join(root, "design", ".local");
187
187
  mkdirSync(dir, { recursive: true });
188
- const { validAvatar } = await import("./collab-pLGzSvm5.mjs");
188
+ const { validAvatar } = await import("./collab-s3k5byM1.mjs");
189
189
  let cur = {};
190
190
  try {
191
191
  cur = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8"));
@@ -199,8 +199,40 @@ function apiMiddleware(root) {
199
199
  atomicWrite(join(dir, "profile.json"), JSON.stringify(prof, null, 2) + "\n");
200
200
  return json(res, 200, { user: localProfile(root) });
201
201
  }
202
+ if (path === "work") {
203
+ const { workActivity, readDevInfo, WORK_TTL_DEFAULT, WORK_TTL_MAX } = await import("./work-CLrmY-vQ.mjs");
204
+ const tokenOk = () => {
205
+ const t = readDevInfo(root)?.token;
206
+ return !!t && req.headers["x-mv-work"] === t;
207
+ };
208
+ if (req.method === "GET") {
209
+ if (!ownerGated(req) && !tokenOk()) return json(res, 403, { error: "forbidden" });
210
+ return json(res, 200, { frames: workActivity.active() });
211
+ }
212
+ if (req.method === "POST") {
213
+ if (!ownerGated(req) && !tokenOk()) return json(res, 403, { error: "forbidden" });
214
+ const raw = await readBody(req);
215
+ if (raw == null) return json(res, 400, { error: "body too large" });
216
+ let b;
217
+ try {
218
+ b = JSON.parse(raw);
219
+ } catch {
220
+ return json(res, 400, { error: "malformed JSON" });
221
+ }
222
+ const frames = Array.isArray(b.frames) ? b.frames.filter((f) => typeof f === "string" && f) : [];
223
+ const on = b.on !== false;
224
+ if (!on && b.all === true) {
225
+ workActivity.clearAll("cli");
226
+ return json(res, 200, { frames: workActivity.active() });
227
+ }
228
+ if (!frames.length) return json(res, 400, { error: "frames required" });
229
+ const ttl = Math.min(Math.max(Number(b.ttlMs) || WORK_TTL_DEFAULT, 1e4), WORK_TTL_MAX);
230
+ for (const f of frames) on ? workActivity.mark(f, ttl, "cli") : workActivity.clear(f, "cli");
231
+ return json(res, 200, { frames: workActivity.active() });
232
+ }
233
+ }
202
234
  if (path === "boards.rights" && req.method === "GET") {
203
- const { listBoards } = await import("./comments-DSwMhdbZ.mjs");
235
+ const { listBoards } = await import("./comments-BZBKhKRO.mjs");
204
236
  return json(res, 200, {
205
237
  local: true,
206
238
  stored: listBoards(join(root, "design", "comments"))
@@ -208,7 +240,7 @@ function apiMiddleware(root) {
208
240
  }
209
241
  const cm = /^comments\/([a-z0-9][a-z0-9-]*)$/.exec(path);
210
242
  if (cm) {
211
- const { appendEvents, readLog } = await import("./comments-DSwMhdbZ.mjs");
243
+ const { appendEvents, readLog } = await import("./comments-BZBKhKRO.mjs");
212
244
  const dir = join(root, "design", "comments");
213
245
  if (req.method === "GET") return json(res, 200, { events: readLog(dir, cm[1]) });
214
246
  if (req.method === "POST") {
@@ -237,14 +269,14 @@ function apiMiddleware(root) {
237
269
  };
238
270
  });
239
271
  const fresh = appendEvents(dir, cm[1], stamped);
240
- const { record } = await import("./ledger-wFvEIEGi.mjs").then((n) => n.n);
272
+ const { record } = await import("./ledger-CbzTJrV2.mjs").then((n) => n.n);
241
273
  for (const ev of fresh) if (ev.type === "create" || ev.type === "reply") record(root, cm[1], ev.id);
242
274
  backgroundPush(root);
243
275
  return json(res, 200, { accepted: fresh.length });
244
276
  }
245
277
  }
246
278
  if (path === "sync" && req.method === "POST") {
247
- const { loadCollab, syncOnce } = await import("./sync-Ch4Bymb1.mjs");
279
+ const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
248
280
  const collab = loadCollab(root);
249
281
  if (!collab) return json(res, 200, { connected: false });
250
282
  try {
@@ -273,7 +305,7 @@ function backgroundPush(root) {
273
305
  pushTimer = setTimeout(async () => {
274
306
  pushTimer = null;
275
307
  try {
276
- const { loadCollab, syncOnce } = await import("./sync-Ch4Bymb1.mjs");
308
+ const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
277
309
  const collab = loadCollab(root);
278
310
  if (collab) await syncOnce(root, collab);
279
311
  } catch {}
@@ -454,7 +486,7 @@ function marverPlugin(ctx) {
454
486
  zoomSpeed: config.zoomSpeed,
455
487
  noTheme: themeFile() == null,
456
488
  setup: setupPending,
457
- projectName: basename(root)
489
+ projectName: config.share.name || basename(root)
458
490
  })}`;
459
491
  }
460
492
  if (id === "\0virtual:sh-data") return "export default null";
@@ -3,10 +3,29 @@ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
5
5
  import { createServer } from "node:http";
6
+ //#region src/shared/utm.ts
7
+ /** Automatic attribution for the powered-by links: every canvas tags its marver.design
8
+ * link so site analytics can see WHICH canvas sent a visitor - zero user setup.
9
+ * Conventional UTM semantics (stable groupings first, the variable last):
10
+ * utm_source = the surface class ('published-canvas' | 'dev-canvas')
11
+ * utm_medium = the link unit ('powered-by')
12
+ * utm_campaign = THIS canvas's name, slugged ("Marver tour" -> "marver-tour")
13
+ * utm_content = the placement ('gate' badge | 'shell' wordmark)
14
+ */
15
+ function poweredByUrl(canvasName, source, content) {
16
+ const slug = (canvasName ?? "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
17
+ return `https://marver.design/?${new URLSearchParams({
18
+ utm_source: source,
19
+ utm_medium: "powered-by",
20
+ ...slug ? { utm_campaign: slug } : {},
21
+ utm_content: content
22
+ }).toString()}`;
23
+ }
24
+ //#endregion
6
25
  //#region src/server/serve.ts
7
26
  /**
8
27
  * `marver serve` - a zero-dependency static server for design/.dist with an optional
9
- * password gate (SPEC-M2 §4b). No MARVER_PASSWORD → plain static serving. With it, the
28
+ * password gate. No MARVER_PASSWORD → plain static serving. With it, the
10
29
  * bundle is never sent pre-auth: every unauthenticated request gets the gate page, auth
11
30
  * is a POST compare + an HMAC-signed 30-day cookie keyed off the password itself (all
12
31
  * instances agree, nothing stored server-side).
@@ -49,9 +68,9 @@ async function serve(root, portFlag) {
49
68
  let bearerCheck = null;
50
69
  let sessionCheck = null;
51
70
  if (process.env.MARVER_DATA_DIR) {
52
- const { collabHandler } = await import("./collab-pLGzSvm5.mjs");
53
- const { appendEvents, readLog } = await import("./comments-DSwMhdbZ.mjs");
54
- const { dataDir } = await import("./comments-DSwMhdbZ.mjs");
71
+ const { collabHandler } = await import("./collab-s3k5byM1.mjs");
72
+ const { appendEvents, readLog } = await import("./comments-BZBKhKRO.mjs");
73
+ const { dataDir } = await import("./comments-BZBKhKRO.mjs");
55
74
  const dir = dataDir();
56
75
  const seedDir = join(dist, "design", "comments");
57
76
  if (existsSync(seedDir)) for (const f of readdirSync(seedDir)) {
@@ -60,7 +79,7 @@ async function serve(root, portFlag) {
60
79
  appendEvents(join(dir, "comments"), board, readLog(seedDir, board));
61
80
  }
62
81
  collab = collabHandler(dir, dist);
63
- const { loadStore, createInvite, normEmail, sessionUser } = await import("./auth-B36fMCM3.mjs");
82
+ const { loadStore, createInvite, normEmail, sessionUser } = await import("./auth-KQ9Aj-nB.mjs");
64
83
  bearerCheck = (token) => sessionUser(dir, token);
65
84
  sessionCheck = (req) => {
66
85
  const tok = /(?:^|;\s*)mv_s=([\w-]+)/.exec(String(req.headers.cookie ?? ""))?.[1];
@@ -128,7 +147,12 @@ async function serve(root, portFlag) {
128
147
  if (!bearerOk && !preGate) return gate(res, meta, !!collab);
129
148
  }
130
149
  }
131
- if (collab && url.pathname.startsWith("/__mv/api/")) {
150
+ if (url.pathname.startsWith("/__mv/api/")) {
151
+ if (!collab) {
152
+ res.statusCode = 404;
153
+ res.setHeader("content-type", "application/json");
154
+ return res.end("{\"error\":\"collaboration is not enabled on this canvas\"}");
155
+ }
132
156
  collab(req, res, url).then((handled) => {
133
157
  if (!handled) {
134
158
  res.statusCode = 404;
@@ -188,7 +212,7 @@ const MARK_AT = (size) => `<svg viewBox="0 0 256 256" width="${size}" height="${
188
212
  const MARK = MARK_AT(17);
189
213
  const MARK_LG = MARK_AT(24);
190
214
  /** The gate: the canvas's own antechamber - light ground, dot grid, glass card in the
191
- * shell's exact token language. Three doors, one credential each (SPEC-M3 gate v2):
215
+ * shell's exact token language. Three doors, one credential each:
192
216
  * guests pay the canvas password, members sign in with their OWN password, and an
193
217
  * invite link (#/i/<token>) opens straight into the claim. The member and claim
194
218
  * states exist only on collaboration canvases - a static canvas keeps one field. */
@@ -423,7 +447,7 @@ ${collabOn ? `
423
447
  ` : ""}
424
448
  })()
425
449
  <\/script>
426
- ${meta.branding ? `<footer><a href="https://marver.design" target="_blank" rel="noopener">${MARK} <span>Powered by <span class="md">Marver.design</span></span> <svg class="up" viewBox="0 0 256 256" width="11" height="11" fill="currentColor" aria-hidden><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"/></svg></a></footer>` : ""}
450
+ ${meta.branding ? `<footer><a href="${poweredByUrl(meta.name, "published-canvas", "gate")}" target="_blank" rel="noopener">${MARK} <span>Powered by <span class="md">Marver.design</span></span> <svg class="up" viewBox="0 0 256 256" width="11" height="11" fill="currentColor" aria-hidden><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"/></svg></a></footer>` : ""}
427
451
  </main></body></html>`);
428
452
  }
429
453
  const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
@@ -1,10 +1,10 @@
1
1
  import { t as diffEvents } from "./events-BMtBvvgU.mjs";
2
- import { appendEvents, listBoards, readLog } from "./comments-DSwMhdbZ.mjs";
2
+ import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
3
3
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  //#region src/server/sync.ts
6
6
  /**
7
- * Dev ↔ published comment sync (SPEC-M3 §2) - one merge rule, run from the dev side.
7
+ * Dev ↔ published comment sync - one merge rule, run from the dev side.
8
8
  *
9
9
  * The published serve is the canonical home; the repo's design/comments/ is the
10
10
  * mirror the agent reads. Each exchange is: pull remote events → union into local
@@ -0,0 +1,97 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ //#region src/server/jam/activity.ts
5
+ function createActivity(ttlMs = 9e4) {
6
+ const m = /* @__PURE__ */ new Map();
7
+ const cbs = [];
8
+ const frames = () => [...new Set([...m.keys()].map((k) => k.slice(k.indexOf("|") + 1)))];
9
+ const emit = () => {
10
+ const f = frames();
11
+ for (const cb of cbs) cb(f);
12
+ };
13
+ return {
14
+ mark(frame, ttl, src = "cli") {
15
+ if (!frame) return;
16
+ m.set(`${src}|${frame}`, Date.now() + (ttl ?? ttlMs));
17
+ emit();
18
+ },
19
+ clear(frame, src = "cli") {
20
+ if (m.delete(`${src}|${frame}`)) emit();
21
+ },
22
+ clearAll(src = "cli") {
23
+ let changed = false;
24
+ for (const k of [...m.keys()]) if (k.startsWith(`${src}|`)) {
25
+ m.delete(k);
26
+ changed = true;
27
+ }
28
+ if (changed) emit();
29
+ },
30
+ active: frames,
31
+ sweep() {
32
+ const now = Date.now();
33
+ let changed = false;
34
+ for (const [k, until] of m) if (until <= now) {
35
+ m.delete(k);
36
+ changed = true;
37
+ }
38
+ if (changed) emit();
39
+ },
40
+ onChange(fn) {
41
+ cbs.push(fn);
42
+ return () => {
43
+ const i = cbs.indexOf(fn);
44
+ if (i !== -1) cbs.splice(i, 1);
45
+ };
46
+ }
47
+ };
48
+ }
49
+ //#endregion
50
+ //#region src/server/work.ts
51
+ /**
52
+ * The working-state rail for CHAT-driven agents (the jam daemon has its own in-process
53
+ * path into the same set). A coding agent that just accepted a request creates the frame
54
+ * files first, then `npx marver work start <frame...>` - the canvas shows the live
55
+ * working shimmer before a single component exists. `marver work done` clears it.
56
+ *
57
+ * Transport: the dev server writes design/.local/dev.json ({port, token}) at boot; the
58
+ * CLI reads it to find the server and authenticate. The token makes the endpoint
59
+ * unreachable from a drive-by browser page (which cannot read local files), while any
60
+ * process that can read the repo - the owner's own tools - is trusted by definition.
61
+ * Presence itself NEVER touches disk (activity.ts).
62
+ */
63
+ /** One per process - the dev server, the jam daemon, and the API all share it. */
64
+ const workActivity = createActivity();
65
+ /** The longest a CLI mark may glow unrefreshed - a forgotten `done` self-heals. */
66
+ const WORK_TTL_MAX = 18e5;
67
+ const WORK_TTL_DEFAULT = 6e5;
68
+ const infoPath = (root) => join(root, "design", ".local", "dev.json");
69
+ /** Written once per boot (dev.ts), removed on close - the CLI's discovery + credential. */
70
+ function writeDevInfo(root, port) {
71
+ const token = randomBytes(24).toString("base64url");
72
+ mkdirSync(join(root, "design", ".local"), { recursive: true });
73
+ writeFileSync(infoPath(root), JSON.stringify({
74
+ port,
75
+ token,
76
+ ts: Date.now()
77
+ }, null, 2) + "\n");
78
+ return token;
79
+ }
80
+ function removeDevInfo(root) {
81
+ try {
82
+ rmSync(infoPath(root), { force: true });
83
+ } catch {}
84
+ }
85
+ function readDevInfo(root) {
86
+ try {
87
+ const v = JSON.parse(readFileSync(infoPath(root), "utf8"));
88
+ return typeof v?.port === "number" && typeof v?.token === "string" ? {
89
+ port: v.port,
90
+ token: v.token
91
+ } : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ //#endregion
97
+ export { WORK_TTL_DEFAULT, WORK_TTL_MAX, readDevInfo, removeDevInfo, workActivity, writeDevInfo };
@@ -0,0 +1,76 @@
1
+ import { n as NAME } from "./cli.mjs";
2
+ import { WORK_TTL_DEFAULT, WORK_TTL_MAX, readDevInfo } from "./work-CLrmY-vQ.mjs";
3
+ //#region src/cli/work.ts
4
+ /**
5
+ * `marver work` - the coding agent's hand on the canvas working state.
6
+ *
7
+ * work start <scene/frame ...> [--ttl <minutes>] mark frames actively working
8
+ * work done <scene/frame ...> | --all clear the glow
9
+ * work list what is glowing right now
10
+ *
11
+ * The intended choreography (taught in design/AGENTS.md): on accepting a request,
12
+ * create the frame FILES first (name, meta, a minimal skeleton), pin them on the
13
+ * right board, `work start` them - the human sees the request land on the canvas
14
+ * within seconds - build (independent frames in parallel, one subagent each), then
15
+ * `work done`. Marks self-expire (default 10 min, max 30) so a crashed agent can
16
+ * never leave a frame glowing forever.
17
+ */
18
+ async function workCommand(root, action, frames, opts) {
19
+ const info = readDevInfo(root);
20
+ if (!info) throw new Error(`\`${NAME} dev\` is not running in this repo (design/.local/dev.json not found) - start it first.`);
21
+ const call = async (method, body) => {
22
+ let res;
23
+ try {
24
+ res = await fetch(`http://localhost:${info.port}/__mv/api/work`, {
25
+ method,
26
+ headers: {
27
+ "content-type": "application/json",
28
+ "x-mv-work": info.token
29
+ },
30
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
31
+ });
32
+ } catch {
33
+ throw new Error(`could not reach \`${NAME} dev\` on port ${info.port} - is it still running?`);
34
+ }
35
+ const data = await res.json().catch(() => ({}));
36
+ if (res.status === 403) throw new Error(`the dev server refused the token - it likely restarted; try again (design/.local/dev.json is re-read each run)`);
37
+ if (!res.ok) throw new Error(String(data?.error ?? `work ${action} failed (${res.status})`));
38
+ if (!Array.isArray(data?.frames)) throw new Error(`port ${info.port} did not answer like \`${NAME} dev\` - is it still running?`);
39
+ return data;
40
+ };
41
+ switch (action) {
42
+ case "start": {
43
+ if (!frames.length) throw new Error("name the frames: work start <scene/frame ...>");
44
+ const min = Number(opts.ttl);
45
+ const ttlMs = Number.isFinite(min) && min > 0 ? Math.min(Math.max(min * 6e4, 1e4), WORK_TTL_MAX) : WORK_TTL_DEFAULT;
46
+ const { frames: active } = await call("POST", {
47
+ frames,
48
+ on: true,
49
+ ttlMs
50
+ });
51
+ const pretty = ttlMs >= 6e4 ? `${Math.round(ttlMs / 6e4)} min` : `${Math.round(ttlMs / 1e3)} s`;
52
+ console.log(`working: ${active.join(", ")} (auto-expires in ${pretty} - re-run to extend, \`work done\` to clear)`);
53
+ return;
54
+ }
55
+ case "done": {
56
+ if (!frames.length && !opts.all) throw new Error("name the frames (or --all): work done <scene/frame ...>");
57
+ const { frames: active } = await call("POST", opts.all ? {
58
+ on: false,
59
+ all: true
60
+ } : {
61
+ frames,
62
+ on: false
63
+ });
64
+ console.log(active.length ? `still working: ${active.join(", ")}` : "nothing working - all clear");
65
+ return;
66
+ }
67
+ case "list": {
68
+ const { frames: active } = await call("GET");
69
+ console.log(active.length ? active.join("\n") : "nothing working");
70
+ return;
71
+ }
72
+ default: throw new Error(`unknown action "${action}" - use start · done · list`);
73
+ }
74
+ }
75
+ //#endregion
76
+ export { workCommand };
package/package.json CHANGED
@@ -1,10 +1,24 @@
1
1
  {
2
2
  "name": "@marver-design/marver",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components. The tool ships no AI - your coding agent is the designer.",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "license": "Apache-2.0",
8
+ "author": "Nic Touron (https://github.com/TNEP4)",
9
+ "keywords": [
10
+ "design",
11
+ "canvas",
12
+ "agent",
13
+ "prototyping",
14
+ "design-tool",
15
+ "react",
16
+ "vite",
17
+ "tailwind"
18
+ ],
19
+ "bugs": {
20
+ "url": "https://github.com/TNEP4/marver/issues"
21
+ },
8
22
  "bin": {
9
23
  "marver": "dist/cli.mjs"
10
24
  },
@@ -55,6 +69,7 @@
55
69
  "engines": {
56
70
  "node": ">=22.18.0"
57
71
  },
72
+ "packageManager": "pnpm@11.0.8",
58
73
  "homepage": "https://marver.design",
59
74
  "repository": {
60
75
  "type": "git",
@@ -4,7 +4,7 @@ export const NAME = 'marver'
4
4
  export const PKG = '@marver-design/marver' // registry identity; bin stays `marver`
5
5
  export const ROUTE = '/__mv'
6
6
 
7
- /** Content-frame natural widths (SPEC-026): Doc layout -> own-size width.
7
+ /** Content-frame natural widths: Doc layout -> own-size width.
8
8
  * Shared by the Doc primitive (measurement messages) and the server-side
9
9
  * manifest scan (defaultSize for content frames) - one source, no drift. */
10
10
  export const CONTENT_WIDTH: Record<string, number> = { document: 760, wide: 1280 }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The Diagram block - first-class Mermaid (SPEC-026).
2
+ * The Diagram block - first-class Mermaid.
3
3
  * Mermaid is marver's dependency behind a dynamic import: a workspace with no
4
4
  * Diagram loads no mermaid bytes. Source-level theme overrides (%%{init}%%,
5
5
  * yaml frontmatter) are stripped so the marver palette always holds; the
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Content-frame block primitives (SPEC-026): Doc, Row, Col, Space, Md, Diagram, Img.
2
+ * Content-frame block primitives: Doc, Row, Col, Space, Md, Diagram, Img.
3
3
  * One vocabulary at two scales: boards arrange frames in rows/columns with space
4
4
  * units; these primitives do the same for blocks INSIDE a frame - as CSS flex,
5
- * deliberately NOT the SPEC-024 lane grammar (shared vocabulary, not semantics).
5
+ * deliberately NOT the board lane-flow grammar (shared vocabulary, not semantics).
6
6
  *
7
7
  * Doc owns the measurement protocol: it reports its content height so the shell
8
8
  * can give the frame a natural size (the shell alone owns node dimensions).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Markdown rendering for the Md block (SPEC-026 security section).
2
+ * Markdown rendering for the Md block.
3
3
  * Raw HTML is inert (escaped, never parsed). Links and images have SEPARATE
4
4
  * policies: links may be goto:/http(s)/mailto (external ones open a new tab,
5
5
  * never navigate the iframe); images are local-only - design/assets/ paths.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The marver diagram identity (SPEC-026): the full Apple system palette (HIG),
2
+ * The marver diagram identity: the full Apple system palette (HIG),
3
3
  * every value the HIG's own light/dark pair - never inverted. Structure comes
4
4
  * from the systemGray ramp, emphasis from blue+purple, series diversity from
5
5
  * the 12-color set.
@@ -43,7 +43,7 @@ export function themeVars(dark: boolean): Record<string, string> {
43
43
  const purple = dark ? ACCENT.purple.dark : ACCENT.purple.light
44
44
  // Nodes are ACCENTED by default - a plain flowchart only ever touches
45
45
  // primaryColor, so a gray primary meant every default diagram rendered
46
- // gray-on-gray (Nic's dogfood catch, 2026-08-13). Structure (lines, clusters,
46
+ // gray-on-gray. Structure (lines, clusters,
47
47
  // labels) stays grayscale; the shapes carry the marver blue.
48
48
  const blueWash = dark ? '#123A5C' : '#E4F0FF'
49
49
  const purpleWash = dark ? '#3A1440' : '#F8E4FC'
@@ -12,7 +12,7 @@ const isHtmlFrame = new URL(import.meta.url).searchParams.get('html') === '1'
12
12
  const post = (msg) => { if (window.parent !== window) window.parent.postMessage(msg, location.origin) }
13
13
  const id = new URLSearchParams(location.search).get('id') ?? location.pathname
14
14
 
15
- // SPEC-M5: the shell serialises this frame's DOM (same origin) for the lean facade. Open shadow roots
15
+ // The shell serialises this frame's DOM (same origin) for the lean facade. Open shadow roots
16
16
  // are walkable, but a CLOSED root is invisible after the fact - flag it at creation so the serialiser
17
17
  // degrades the frame (keeps it live) instead of shipping a lean copy missing its shadow content.
18
18
  const _attachShadow = Element.prototype.attachShadow
@@ -1,4 +1,4 @@
1
- // Shared frame-inspect controller (SPEC-M3 §5/§7 + prototype-review #4/#5).
1
+ // Shared frame-inspect controller.
2
2
  //
3
3
  // Laser outlines, comment-pick, anchor capture/resolve, and a persistent element
4
4
  // lock ("highlight") - the review machinery shared by the canvas frame bridge
@@ -1,4 +1,4 @@
1
- // SPEC-M5 slice 1: the DOM-snapshot serializer. Turns a live, same-origin frame Document into a
1
+ // The DOM-snapshot serializer. Turns a live, same-origin frame Document into a
2
2
  // self-contained STATIC html string: post-render DOM + full inlined CSS (styleSheets +
3
3
  // adoptedStyleSheets), JS stripped. Rendered in a `sandbox="allow-same-origin"` (NO allow-scripts)
4
4
  // iframe srcdoc, it reflows / theme-flips / device-sweeps with the browser's own layout engine and
@@ -19,7 +19,7 @@ export interface SerializeResult {
19
19
 
20
20
  const STRIP_TAGS = new Set(['SCRIPT', 'NOSCRIPT'])
21
21
 
22
- /** Absolutize relative url() in a sheet's cssText against that sheet's href (codex P1: consolidating
22
+ /** Absolutize relative url() in a sheet's cssText against that sheet's href (consolidating
23
23
  * rules into one <style> moves the url() base from each sheet to the srcdoc base). */
24
24
  function absolutizeUrls(css: string, sheetHref: string | null): string {
25
25
  if (!sheetHref) return css