@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.
- package/CHANGELOG.md +17 -0
- package/dist/gateway/adapters/webchat.d.ts +20 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +81 -3
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/static/blex-render.js +399 -0
- package/dist/gateway/static/chat.html +7 -0
- package/dist/gateway/static/mermaid-esm.js +2143 -0
- package/dist/gateway/static/widget.js +112 -127
- package/docs/web-app-agent-guide.md +117 -21
- package/examples/web-app-agent/README.md +225 -19
- package/examples/web-app-agent/agent/apply-thread-configs.mjs +139 -0
- package/examples/web-app-agent/gateway.config.example.json +22 -4
- package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
- package/examples/web-app-agent/public/agent/bridge-mount.js +19 -3
- package/examples/web-app-agent/public/agent/commands.js +10 -4
- package/examples/web-app-agent/public/agent/device-thread.js +5 -5
- package/examples/web-app-agent/public/agent/panel.css +6 -0
- package/examples/web-app-agent/public/agent/panel.js +27 -4
- package/examples/web-app-agent/public/index.html +28 -0
- package/examples/web-app-agent/server.js +172 -11
- package/examples/web-app-agent/thread-config.example.json +22 -0
- package/examples/web-app-agent/thread-config.visitor.example.json +41 -0
- package/package.json +3 -2
|
@@ -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 ->
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
get their own gateway config (cheap model,
|
|
11
|
-
"
|
|
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 = '
|
|
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
|
-
|
|
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)
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
211
|
-
if (!
|
|
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,
|
|
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
|
|
220
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|