@luckydraw/cumulus 1.0.0 → 1.0.2

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.
@@ -6,7 +6,14 @@
6
6
 
7
7
  Because the agent config arrives only after login, this is a function the
8
8
  app calls (AgentStart) rather than something that runs at load. AgentStop
9
- tears it down on logout so a second login re-mounts cleanly. */
9
+ tears it down on logout so a second login re-mounts cleanly.
10
+
11
+ KNOWN LIMIT of the version stamp: the import below is a static specifier, so
12
+ client.js — and protocol.js, which client.js imports in turn — are fetched
13
+ unstamped. Both ship from the cumulus package and change only on upgrade, and
14
+ the server sends `no-cache, must-revalidate` on them, so the browser is
15
+ correct; an edge that overrides that is the exposure. If you put a CDN in
16
+ front of this app, exclude /agent/ from it. */
10
17
  import { BridgeClient } from './bridge-client/client.js';
11
18
 
12
19
  let bridge = null;
@@ -16,20 +23,29 @@ window.AgentStart = function () {
16
23
  const cfg = window.__AGENT_CONFIG__;
17
24
  if (!cfg || !cfg.API_KEY || !window.AgentRegistry || !window.agentDeviceThread) return;
18
25
 
19
- window.agentDeviceThread(cfg); // THREAD_ID -> demoapp-v-<deviceId>
26
+ window.agentDeviceThread(cfg); // THREAD_ID -> <your-namespace>-v-<deviceId>
20
27
  console.info('[agent] thread: ' + cfg.THREAD_ID);
21
28
 
22
29
  if (!document.getElementById('agent-css')) {
23
30
  const link = document.createElement('link');
24
31
  link.id = 'agent-css';
25
32
  link.rel = 'stylesheet';
26
- link.href = '/agent/panel.css';
33
+ // Built here, so the server's HTML stamp cannot see it — it takes its hash
34
+ // from the map index.html publishes. Bare URL if that map is absent.
35
+ link.href =
36
+ typeof window.agentAsset === 'function'
37
+ ? window.agentAsset('/agent/panel.css')
38
+ : '/agent/panel.css';
27
39
  document.head.appendChild(link);
28
40
  }
29
41
 
30
42
  window.AgentChat.init(cfg);
31
43
  window.AgentPanel.init();
32
44
 
45
+ // Rich blocks, loaded same-origin from /agent/blex/. Fire-and-forget: the panel renders
46
+ // plain markdown until these arrive, and keeps doing so if they never do.
47
+ if (window.AgentBlex) window.AgentBlex.load(cfg);
48
+
33
49
  bridge = new BridgeClient({
34
50
  url: cfg.BRIDGE_URL,
35
51
  thread: cfg.THREAD_ID,
@@ -11,12 +11,18 @@
11
11
 
12
12
  Frozen contract:
13
13
  command { name, description, params, risk, execute(params) }
14
- risk "read" answers a question, changes nothing
14
+ risk "export" the ONLY gated tier. ALWAYS routed through the
15
+ confirm chip by the gateway; the model cannot bypass
16
+ it, and neither can this file.
17
+ "read" — answers a question, changes nothing
15
18
  "display" — changes what's on screen only, trivially undoable
16
19
  "mutate" — changes stored data
17
- "export" — irreversible or leaves the app. ALWAYS routed
18
- through the confirm chip by the gateway; the model
19
- cannot bypass it, and neither can this file.
20
+ The last three are ADVISORY: they dispatch on arrival. The tier
21
+ rides in the tool description the model sees ("[mutate] …") so it
22
+ can weigh the call, but nothing stops it. Tier by "must a human
23
+ see this first?", not by "is this irreversible?" — if yes, it is
24
+ "export". A command that reads as guarded and isn't fails
25
+ silently: it just runs.
20
26
  execute -> { summary, data?, affected? } (may be async)
21
27
  call() never throws — failures become { ok: false, summary }
22
28
 
@@ -5,16 +5,16 @@
5
5
  name must be unguessable and must never be minted server-side and broadcast.
6
6
 
7
7
  This mints 16 hex characters (64 bits) once per browser, keeps it in
8
- localStorage, and pins THREAD_ID = <base>-<deviceId>. The base the server
9
- sends is "demoapp-v" — a sub-namespace under "demoapp", so visitor threads
10
- get their own gateway config (cheap model, tight prompt) while the base
11
- "demoapp" thread stays yours for working on the app.
8
+ localStorage, and pins THREAD_ID = <base>-<deviceId>. The base comes from the
9
+ server (this file never names your app). Send "<ns>-v" — a sub-namespace
10
+ under "<ns>" — so visitor threads get their own gateway config (cheap model,
11
+ tight prompt) while the base "<ns>" thread stays yours for working on the app.
12
12
 
13
13
  Do not shorten the id. 8 hex characters is 32 bits, which is brute-forceable
14
14
  against a live gateway. */
15
15
  (function () {
16
16
  'use strict';
17
- var KEY = 'demoapp.deviceId';
17
+ var KEY = 'agent.deviceId'; // app-neutral: localStorage is origin-partitioned
18
18
 
19
19
  window.agentDeviceThread = function (cfg) {
20
20
  if (!cfg || !cfg.THREAD_ID) return null;
@@ -79,6 +79,12 @@
79
79
  #agent-root .agmsg table { border-collapse: collapse; font-size: 12.5px; }
80
80
  #agent-root .agmsg th, #agent-root .agmsg td { border: 1px solid var(--line); padding: 4px 8px; text-align: left; }
81
81
 
82
+ /* Blex blocks. The container is what the shared renderer (blex-render.js) fills;
83
+ .blex-fallback is the raw fence, shown when nothing claims the container —
84
+ a denied type, or blex failing to load. Both beat a blank rectangle. */
85
+ #agent-root .blex-block-container { margin: 6px 0; border-radius: 8px; overflow: hidden; max-width: 100%; }
86
+ #agent-root .blex-fallback { margin: 0; padding: 8px 10px; background: var(--panel3); border: 1px solid var(--line); border-radius: 8px; font-size: 11.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; opacity: .85; overflow-x: auto; }
87
+
82
88
  #agent-root .agactivity { align-self: flex-start; color: var(--muted); font-size: 12px; padding: 0 4px; animation: agpulse 1.4s ease-in-out infinite; }
83
89
  @keyframes agpulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
84
90
  /* Browser tests set data-test-mode to kill animation timing races. */
@@ -9,7 +9,9 @@
9
9
  window.AgentPanel = { init(), setState(s), confirm(req), notify(text), reset() } */
10
10
  (function () {
11
11
  'use strict';
12
- var LS_KEY = 'demoapp-agent-v1';
12
+ /* App-neutral on purpose: localStorage is already origin-partitioned, so an
13
+ app-name prefix buys nothing, and this file is meant to be copied as-is. */
14
+ var LS_KEY = 'agent-panel-v1';
13
15
  var root, barEl, winEl, logEl, inputEl, dotEl;
14
16
  var prefs = { expanded: false, draft: '' };
15
17
  var streaming = false;
@@ -78,6 +80,20 @@
78
80
  });
79
81
  }
80
82
 
83
+ /* Render markdown, then blex blocks into it.
84
+
85
+ Blex fences are pulled out FIRST so the code-block branch below cannot eat
86
+ them, and the placeholders go back in last. Returns the extracted blocks so
87
+ the caller can hand them to AgentBlex.render once the HTML is in the DOM.
88
+ Without blex loaded this is exactly the old behaviour. */
89
+ function renderRich(text) {
90
+ var blex = window.AgentBlex;
91
+ var extracted = blex ? blex.extract(String(text)) : { text: String(text), blocks: [] };
92
+ var html = renderMarkdown(extracted.text);
93
+ if (blex) html = blex.insertPlaceholders(html, extracted.blocks);
94
+ return { html: html, blocks: extracted.blocks };
95
+ }
96
+
81
97
  function renderMarkdown(text) {
82
98
  var lines = String(text).split('\n');
83
99
  var html = '',
@@ -170,8 +186,11 @@
170
186
 
171
187
  function addMsg(role, content, asMarkdown) {
172
188
  var m = el('div', 'agmsg ' + role, logEl);
173
- if (asMarkdown) m.innerHTML = renderMarkdown(content);
174
- else m.textContent = content;
189
+ if (asMarkdown) {
190
+ var r = renderRich(content);
191
+ m.innerHTML = r.html;
192
+ if (window.AgentBlex) window.AgentBlex.render(m, r.blocks);
193
+ } else m.textContent = content;
175
194
  scrollLog();
176
195
  return m;
177
196
  }
@@ -235,7 +254,11 @@
235
254
  streaming = false;
236
255
  setLoading(false);
237
256
  activity.remove();
238
- bubble.innerHTML = renderMarkdown(acc);
257
+ // Blocks are rendered only at `done` — mid-stream a fence is usually
258
+ // incomplete, and re-rendering each token would churn the handles.
259
+ var done = renderRich(acc);
260
+ bubble.innerHTML = done.html;
261
+ if (window.AgentBlex) window.AgentBlex.render(bubble, done.blocks);
239
262
  scrollLog();
240
263
  },
241
264
  });
@@ -4,6 +4,19 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
6
  <title>Demo Notes — cumulus web-app agent</title>
7
+ <!-- KEEP THIS if you want ~~~blex:mermaid diagrams to render in the panel.
8
+ blex's mermaid renderer resolves the bare specifier "mermaid", and an import
9
+ map is the only mechanism in a browser that can satisfy one. The library is
10
+ served out of the installed cumulus package by this app's own /agent/blex/
11
+ route (see server.js) — not vendored, not cross-origin. The URL is stamped
12
+ with a content hash at serve time.
13
+ Drop this tag and nothing breaks: blex-render.js checks for the mapping and
14
+ leaves mermaid fences as readable text instead of painting an error box. -->
15
+ <script type="importmap">{"imports":{"mermaid":"/agent/blex/mermaid-esm.js"}}</script>
16
+ <!-- Diagram colours are derived from the blex card's own background (the
17
+ --blex-bg variable), so they follow whatever you theme blex to with no
18
+ declaration here. Set window.__CUMULUS_MERMAID_THEME to a mermaid theme name
19
+ ('default' | 'dark' | 'neutral' | 'forest' | 'base') only to override it. -->
7
20
  <style>
8
21
  /* The six variables the agent panel themes itself from. Define these and
9
22
  panel.css needs no edits. */
@@ -98,6 +111,20 @@
98
111
  <ul class="notes" id="note-list" data-testid="note-list" data-loading="false"></ul>
99
112
  </main>
100
113
 
114
+ <!-- Asset versions. server.js rewrites the empty map at serve time (stampHtml),
115
+ and stamps the src/href attributes below with the same hashes. The map
116
+ exists for assets that are loaded from inside JavaScript — panel.css and
117
+ the blex pair — because an HTML-level stamp cannot see a URL that only
118
+ appears in a script. Left as {} by a server that does not do the rewrite,
119
+ in which case agentAsset() returns URLs unchanged. -->
120
+ <script>
121
+ window.__AGENT_ASSET_V = {};
122
+ window.agentAsset = function (url) {
123
+ var v = window.__AGENT_ASSET_V[url];
124
+ return v ? url + '?v=' + v : url;
125
+ };
126
+ </script>
127
+
101
128
  <!-- Load order matters: the registry and its dependencies must exist before
102
129
  bridge-mount runs. bridge-mount is a module (it imports the cumulus
103
130
  BridgeClient), so it is deferred automatically. -->
@@ -105,6 +132,7 @@
105
132
  <script src="/agent/device-thread.js"></script>
106
133
  <script src="/agent/commands.js"></script>
107
134
  <script src="/agent/chat-client.js"></script>
135
+ <script src="/agent/blex-mount.js"></script>
108
136
  <script src="/agent/panel.js"></script>
109
137
  <script type="module" src="/agent/bridge-mount.js"></script>
110
138
  </body>
@@ -101,6 +101,138 @@ function resolveBridgeDir() {
101
101
  }
102
102
  const BRIDGE_DIR = resolveBridgeDir();
103
103
 
104
+ /* ---- where the browser's blex renderer comes from -------------------------
105
+ Same rule as the bridge client above, and for a sharper reason than symmetry.
106
+ The obvious alternative — point a <script> at GATEWAY_ORIGIN + '/blex.min.js'
107
+ — is only correct for an app on a DIFFERENT origin from the gateway. The
108
+ common production shape is the opposite: GATEWAY_ORIGIN is your own hostname
109
+ and an edge (Caddy/Cloudflare) routes just `/bridge*` and `/api/thread/*` to
110
+ the gateway. There is no `/blex.min.js` on your hostname, so the cross-origin
111
+ recipe 404s and blex silently degrades to plain text — a failure that reads
112
+ like "my route is broken" rather than "my asset is missing". Serving the
113
+ assets ourselves is origin-agnostic and still not a vendored copy.
114
+ (Measured by @cdda, which hit exactly this.) */
115
+ function resolveStaticDir() {
116
+ const inRepo = path.join(ROOT, '..', '..', 'dist', 'gateway', 'static');
117
+ if (fs.existsSync(path.join(inRepo, 'blex-render.js'))) return inRepo;
118
+ try {
119
+ const require = createRequire(import.meta.url);
120
+ return path.dirname(require.resolve('@luckydraw/cumulus/dist/gateway/static/blex-render.js'));
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ const STATIC_DIR = resolveStaticDir();
126
+
127
+ /* ---- asset versioning -----------------------------------------------------
128
+ Without this, a CDN in front of your app decides how long your agent surface
129
+ stays stale. Measured at a real Cloudflare edge: it overrides an origin's
130
+ `no-cache` with `max-age=14400`, so an edit to commands.js — the file you
131
+ change most — can take four hours to reach a browser, and the panel's files
132
+ expire on independent clocks, so a visitor can hold two files from DIFFERENT
133
+ deploys. `?v=<content hash>` makes the URL change when the bytes change,
134
+ which no cache policy can override.
135
+
136
+ Computed at SERVE time, not build time: this kit has no build step, and its
137
+ blex/bridge assets come out of the installed cumulus package, so a build-time
138
+ hash would describe the wrong bytes after `npm i`. */
139
+ const assetStampCache = new Map();
140
+
141
+ /** First 8 hex of sha1, cached and invalidated on mtime+size — so an edit is
142
+ picked up with no restart. undefined if the file cannot be read. */
143
+ function assetVersion(file) {
144
+ if (!file) return undefined;
145
+ try {
146
+ const stat = fs.statSync(file);
147
+ const hit = assetStampCache.get(file);
148
+ if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) return hit.hash;
149
+ const hash = crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex').slice(0, 8);
150
+ assetStampCache.set(file, { hash, mtimeMs: stat.mtimeMs, size: stat.size });
151
+ return hash;
152
+ } catch {
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ /** URL path -> file on disk, or null if it is not an asset we serve. ONE owner
158
+ for this mapping: the routes below and the stamp both go through it, so a
159
+ URL can never be versioned as one file and served as another. The basename
160
+ allowlists are the whole traversal defence for the two directories outside
161
+ PUBLIC — do not loosen them to a prefix match. */
162
+ function assetUrlToFile(pathname) {
163
+ if (pathname.startsWith('/agent/bridge-client/')) {
164
+ const name = path.basename(pathname);
165
+ if (!BRIDGE_DIR || !/^(client|protocol)\.js$/.test(name)) return null;
166
+ return path.join(BRIDGE_DIR, name);
167
+ }
168
+ // blex-chart.min.js is an OPT-IN companion global (blex.min.js inlines Chart.js
169
+ // and never fetches it). Nothing requests it today; it is allowlisted so an
170
+ // adopter who wants it can add one script tag instead of editing this server.
171
+ // mermaid-esm.js is the vendored mermaid library wrapped as an ES module. It is
172
+ // fetched by the browser resolving the bare specifier "mermaid" through the
173
+ // import map in index.html — never by a script tag — so it must be served from
174
+ // this app's own origin like the rest.
175
+ if (pathname.startsWith('/agent/blex/')) {
176
+ const name = path.basename(pathname);
177
+ if (!STATIC_DIR || !/^(blex\.min|blex-render|blex-chart\.min|mermaid-esm)\.js$/.test(name)) {
178
+ return null;
179
+ }
180
+ return path.join(STATIC_DIR, name);
181
+ }
182
+ const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
183
+ const file = path.join(PUBLIC, rel);
184
+ return file === PUBLIC || file.startsWith(PUBLIC + path.sep) ? file : null;
185
+ }
186
+
187
+ /* Assets loaded from INSIDE JavaScript — a <link> built at runtime, a script
188
+ element appended by blex-mount. An HTML-level stamp cannot see a URL that
189
+ only exists in JS, so these get their hashes through the map below instead.
190
+ (This is the "a loader that fetches its own dependencies must propagate the
191
+ version token" case, applied where it is actually true.) */
192
+ const RUNTIME_LOADED = [
193
+ '/agent/panel.css',
194
+ '/agent/blex/blex.min.js',
195
+ '/agent/blex/blex-render.js',
196
+ ];
197
+
198
+ const ASSET_MAP_MARKER = 'window.__AGENT_ASSET_V = {};';
199
+
200
+ /** Stamp served HTML: `?v=` on every same-origin .js/.css src/href AND on
201
+ import-map values, plus the version map for the runtime-loaded set.
202
+
203
+ Import maps need their own pass: the module URL lives in JSON inside a
204
+ `<script type="importmap">`, where the src/href rule cannot see it — and an
205
+ unstamped module URL is exactly the multi-hour edge staleness this mechanism
206
+ exists to prevent. Import-map *keys* are bare specifiers, so they cannot match
207
+ a pattern that requires a leading `/`. */
208
+ function stampHtml(html) {
209
+ const stampUrl = url => {
210
+ const v = assetVersion(assetUrlToFile(url));
211
+ return v ? `${url}?v=${v}` : undefined;
212
+ };
213
+ const stamped = html
214
+ .replace(/(\s(?:src|href)=")(\/[^"?#]+\.(?:js|css))(")/g, (m, pre, url, post) => {
215
+ const s = stampUrl(url);
216
+ return s ? `${pre}${s}${post}` : m;
217
+ })
218
+ .replace(
219
+ /(<script[^>]*type="importmap"[^>]*>)([\s\S]*?)(<\/script>)/gi,
220
+ (m, open, body, close) =>
221
+ open +
222
+ body.replace(/"(\/[^"?#]+\.m?js)"/g, (inner, url) => {
223
+ const s = stampUrl(url);
224
+ return s ? `"${s}"` : inner;
225
+ }) +
226
+ close
227
+ );
228
+ const map = {};
229
+ for (const url of RUNTIME_LOADED) {
230
+ const v = assetVersion(assetUrlToFile(url));
231
+ if (v) map[url] = v;
232
+ }
233
+ return stamped.replace(ASSET_MAP_MARKER, `window.__AGENT_ASSET_V = ${JSON.stringify(map)};`);
234
+ }
235
+
104
236
  /* ---- sessions (in-memory; your app has real ones) ------------------------ */
105
237
  const sessions = new Set();
106
238
 
@@ -141,14 +273,27 @@ function readBody(req) {
141
273
  });
142
274
  }
143
275
 
144
- function sendFile(res, file) {
276
+ function sendFile(res, file, requestedVersion) {
145
277
  fs.readFile(file, (err, buf) => {
146
278
  if (err) {
147
279
  res.writeHead(404);
148
280
  return res.end('not found');
149
281
  }
150
- res.writeHead(200, { 'content-type': MIME[path.extname(file)] || 'application/octet-stream' });
151
- res.end(buf);
282
+ const ext = path.extname(file);
283
+ const body = ext === '.html' ? Buffer.from(stampHtml(buf.toString('utf8')), 'utf8') : buf;
284
+ // A ?v= that matches the CURRENT hash is content-addressed, so it is safe to
285
+ // cache forever. A stale or forged one must NOT be — that would pin today's
286
+ // bytes under a key that no longer describes them. HTML is never immutable:
287
+ // it is what carries the stamps.
288
+ const contentAddressed =
289
+ ext !== '.html' && requestedVersion !== undefined && requestedVersion === assetVersion(file);
290
+ res.writeHead(200, {
291
+ 'content-type': MIME[ext] || 'application/octet-stream',
292
+ 'cache-control': contentAddressed
293
+ ? 'public, max-age=31536000, immutable'
294
+ : 'no-cache, must-revalidate',
295
+ });
296
+ res.end(body);
152
297
  });
153
298
  }
154
299
 
@@ -205,24 +350,38 @@ const server = http.createServer(async (req, res) => {
205
350
  });
206
351
  }
207
352
 
353
+ // Static assets. assetUrlToFile owns the URL -> disk mapping (and the
354
+ // allowlists for the two directories outside PUBLIC); the routes below only
355
+ // pick the right 404 message when it declines.
356
+ const version = url.searchParams.get('v') ?? undefined;
357
+
208
358
  // Bridge client, served from the cumulus package rather than a local copy.
209
359
  if (p.startsWith('/agent/bridge-client/')) {
210
- const name = path.basename(p);
211
- if (!BRIDGE_DIR || !/^(client|protocol)\.js$/.test(name)) {
360
+ const file = assetUrlToFile(p);
361
+ if (!file) {
212
362
  res.writeHead(404);
213
363
  return res.end('bridge client unavailable — build cumulus or npm i @luckydraw/cumulus');
214
364
  }
215
- return sendFile(res, path.join(BRIDGE_DIR, name));
365
+ return sendFile(res, file, version);
366
+ }
367
+
368
+ // Blex renderer + library, likewise served from the package.
369
+ if (p.startsWith('/agent/blex/')) {
370
+ const file = assetUrlToFile(p);
371
+ if (!file) {
372
+ res.writeHead(404);
373
+ return res.end('blex unavailable — build cumulus or npm i @luckydraw/cumulus');
374
+ }
375
+ return sendFile(res, file, version);
216
376
  }
217
377
 
218
378
  if (req.method === 'GET') {
219
- const rel = p === '/' ? 'index.html' : p.replace(/^\/+/, '');
220
- const file = path.join(PUBLIC, rel);
221
- if (!file.startsWith(PUBLIC)) {
379
+ const file = assetUrlToFile(p);
380
+ if (!file) {
222
381
  res.writeHead(403);
223
382
  return res.end('forbidden');
224
383
  }
225
- return sendFile(res, file);
384
+ return sendFile(res, file, version);
226
385
  }
227
386
 
228
387
  res.writeHead(404);
@@ -230,7 +389,9 @@ const server = http.createServer(async (req, res) => {
230
389
  });
231
390
 
232
391
  server.listen(PORT, '127.0.0.1', () => {
233
- console.log(`demo app: http://127.0.0.1:${PORT} (password: ${PASSWORD})`);
392
+ // The bound port, not the requested one — PORT=0 means "pick one for me".
393
+ const bound = server.address().port;
394
+ console.log(`demo app: http://127.0.0.1:${bound} (password: ${PASSWORD})`);
234
395
  console.log(
235
396
  `bridge client: ${BRIDGE_DIR ?? 'NOT FOUND — run `npm run build` in the cumulus repo'}`
236
397
  );
@@ -0,0 +1,22 @@
1
+ {
2
+ "_readme": [
3
+ "BASE THREAD CONFIG -> ~/.cumulus/threads/demoapp.config.json",
4
+ "",
5
+ "This is YOUR management thread for the app — the one you talk to while",
6
+ "building it. It is NOT a visitor thread: a namespace covers '<ns>-*' only, so",
7
+ "the bare 'demoapp' name stays owned by your admin key and the app's scoped key",
8
+ "is rejected (403) on it.",
9
+ "",
10
+ "Give this one a strong model. Visitor turns read the sibling file",
11
+ "thread-config.visitor.example.json instead — see the '_readme' in there for how",
12
+ "the two are kept apart.",
13
+ "",
14
+ "Apply both with: node agent/apply-thread-configs.mjs",
15
+ "Keys starting with '_' are annotations and are stripped before sending."
16
+ ],
17
+
18
+ "projectDir": "/absolute/path/to/your/app",
19
+ "model": "claude",
20
+ "effort": "high",
21
+ "alwaysInclude": ["docs/demoapp-system-prompt.md"]
22
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "_readme": [
3
+ "VISITOR THREAD CONFIG -> ~/.cumulus/threads/demoapp-v.config.json",
4
+ "",
5
+ "WHY THIS FILE EXISTS, AND WHY THE '-v' MATTERS",
6
+ "The server hands the browser THREAD_ID = 'demoapp-v' and device-thread.js",
7
+ "appends 16 hex characters, so every visitor gets 'demoapp-v-<deviceId>'.",
8
+ "Config is resolved by prefix-fallback: a turn on 'demoapp-v-a3f8c2d1' looks for",
9
+ "its own exact file, then strips one trailing '-segment' at a time and takes the",
10
+ "longest match:",
11
+ "",
12
+ " demoapp-v-a3f8c2d1.config.json (none — visitors never get their own)",
13
+ " demoapp-v.config.json <- THIS FILE. Every visitor turn.",
14
+ " demoapp.config.json (only if this file is absent)",
15
+ "",
16
+ "That middle layer is the whole point. Without it, visitor turns inherit your",
17
+ "management thread's config and run your expensive model for every anonymous",
18
+ "visitor. With it, the two are set independently and neither can affect the",
19
+ "other — writes are always exact, so a visitor session can never mutate this",
20
+ "file or the base.",
21
+ "",
22
+ "COST IS THE MAIN DIAL. Visitor traffic is unbounded and mostly shallow, so a",
23
+ "small fast model is usually right here even when the base thread runs a large",
24
+ "one. Both live gateway apps on this box do exactly that.",
25
+ "",
26
+ "Apply with: node agent/apply-thread-configs.mjs",
27
+ "Keys starting with '_' are annotations and are stripped before sending."
28
+ ],
29
+
30
+ "projectDir": "/absolute/path/to/your/app",
31
+ "model": "claude",
32
+ "claudeModel": "claude-haiku-4-5",
33
+ "effort": "medium",
34
+ "alwaysInclude": ["docs/demoapp-system-prompt.md"],
35
+
36
+ "_disallowedTools": [
37
+ "Visitor-facing threads should not be able to stop and ask the operator a",
38
+ "question — there is nobody on the other end, and the turn would hang. Strip it:"
39
+ ],
40
+ "disallowedTools": ["AskUserQuestion"]
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydraw/cumulus",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "RLM-based CLI chat wrapper for Claude with external history context management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "cumulus-gateway": "./dist/gateway/daemon.js"
38
38
  },
39
39
  "scripts": {
40
- "build": "rm -rf dist && tsc && cp -r src/gateway/static dist/gateway/static && cp node_modules/@luckydraw/blex/dist/blex.min.global.js dist/gateway/static/blex.min.js && cp node_modules/@luckydraw/blex/dist/blex-chart.min.global.js dist/gateway/static/blex-chart.min.js",
40
+ "build": "rm -rf dist && tsc && cp -r src/gateway/static dist/gateway/static && cp node_modules/@luckydraw/blex/dist/blex.min.global.js dist/gateway/static/blex.min.js && cp node_modules/@luckydraw/blex/dist/blex-chart.min.global.js dist/gateway/static/blex-chart.min.js && node scripts/build-mermaid-esm.mjs",
41
41
  "dev": "tsc --watch",
42
42
  "lint": "eslint src",
43
43
  "lint:fix": "eslint src --fix",
@@ -88,6 +88,7 @@
88
88
  "husky": "^9.1.7",
89
89
  "ink-testing-library": "^4.0.0",
90
90
  "lint-staged": "^16.2.7",
91
+ "mermaid": "^10.9.8",
91
92
  "prettier": "^3.8.1",
92
93
  "typescript": "^5.9.3",
93
94
  "typescript-eslint": "^8.54.0",