@luckydraw/cumulus 0.31.66 → 1.0.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.
- package/CHANGELOG.md +12 -556
- package/LICENSE +150 -0
- package/README.md +27 -8
- package/dist/gateway/adapters/webchat.d.ts +15 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +78 -5
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/config.d.ts +17 -2
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +10 -3
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.d.ts +3 -1
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +128 -39
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/namespaces.d.ts +34 -0
- package/dist/gateway/namespaces.d.ts.map +1 -1
- package/dist/gateway/namespaces.js +58 -0
- package/dist/gateway/namespaces.js.map +1 -1
- package/dist/gateway/server.d.ts +8 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +150 -41
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/setup.d.ts +32 -0
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +23 -3
- package/dist/gateway/setup.js.map +1 -1
- package/dist/gateway/static/blex-render.js +341 -0
- package/dist/gateway/static/chat.html +1 -0
- package/dist/gateway/static/widget.js +1009 -738
- package/dist/lib/gateway.d.ts +30 -8
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +36 -11
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/history.d.ts +22 -0
- package/dist/lib/history.d.ts.map +1 -1
- package/dist/lib/history.js +59 -21
- package/dist/lib/history.js.map +1 -1
- package/dist/lib/huggingface-provider.d.ts.map +1 -1
- package/dist/lib/huggingface-provider.js +11 -3
- package/dist/lib/huggingface-provider.js.map +1 -1
- package/dist/lib/license.d.ts +76 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +141 -0
- package/dist/lib/license.js.map +1 -0
- package/docs/agentic-harness-primer.md +283 -0
- package/docs/conditional-continuation.md +167 -0
- package/docs/web-app-agent-guide.md +559 -0
- package/examples/web-app-agent/README.md +334 -0
- package/examples/web-app-agent/agent/mcp-shim.js +105 -0
- package/examples/web-app-agent/gateway.config.example.json +70 -0
- package/examples/web-app-agent/package.json +13 -0
- package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
- package/examples/web-app-agent/public/agent/bridge-mount.js +91 -0
- package/examples/web-app-agent/public/agent/chat-client.js +104 -0
- package/examples/web-app-agent/public/agent/commands.js +256 -0
- package/examples/web-app-agent/public/agent/device-thread.js +48 -0
- package/examples/web-app-agent/public/agent/panel.css +113 -0
- package/examples/web-app-agent/public/agent/panel.js +392 -0
- package/examples/web-app-agent/public/app.js +250 -0
- package/examples/web-app-agent/public/index.html +126 -0
- package/examples/web-app-agent/server.js +379 -0
- package/package.json +7 -3
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>Demo Notes — cumulus web-app agent</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* The six variables the agent panel themes itself from. Define these and
|
|
9
|
+
panel.css needs no edits. */
|
|
10
|
+
:root {
|
|
11
|
+
--panel: #16181d;
|
|
12
|
+
--panel2: #1e2128;
|
|
13
|
+
--panel3: #272b34;
|
|
14
|
+
--line: #333844;
|
|
15
|
+
--text: #e6e8ee;
|
|
16
|
+
--muted: #8a91a3;
|
|
17
|
+
--accent: #5b8cff;
|
|
18
|
+
}
|
|
19
|
+
@media (prefers-color-scheme: light) {
|
|
20
|
+
:root {
|
|
21
|
+
--panel: #ffffff; --panel2: #f4f5f8; --panel3: #e9ebf0;
|
|
22
|
+
--line: #d8dbe3; --text: #1b1e25; --muted: #6b7280; --accent: #3b6fe0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
body {
|
|
27
|
+
margin: 0; padding: 32px 20px 160px;
|
|
28
|
+
background: var(--panel); color: var(--text);
|
|
29
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
30
|
+
}
|
|
31
|
+
main { max-width: 620px; margin: 0 auto; }
|
|
32
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
33
|
+
.sub { color: var(--muted); font-size: 13px; margin: 0 0 24px; }
|
|
34
|
+
|
|
35
|
+
input, button, textarea { font: inherit; }
|
|
36
|
+
.field {
|
|
37
|
+
width: 100%; padding: 9px 12px; border-radius: 9px;
|
|
38
|
+
background: var(--panel2); border: 1px solid var(--line); color: var(--text); outline: none;
|
|
39
|
+
}
|
|
40
|
+
.field:focus { border-color: var(--accent); }
|
|
41
|
+
.btn {
|
|
42
|
+
padding: 9px 16px; border-radius: 9px; cursor: pointer;
|
|
43
|
+
background: var(--accent); color: #fff; border: none;
|
|
44
|
+
}
|
|
45
|
+
.btn.ghost { background: var(--panel3); color: var(--text); border: 1px solid var(--line); }
|
|
46
|
+
.row { display: flex; gap: 8px; margin-bottom: 14px; }
|
|
47
|
+
.row .field { flex: 1; }
|
|
48
|
+
|
|
49
|
+
ul.notes { list-style: none; padding: 0; margin: 0; }
|
|
50
|
+
ul.notes li {
|
|
51
|
+
display: flex; align-items: center; gap: 10px;
|
|
52
|
+
padding: 10px 12px; margin-bottom: 6px;
|
|
53
|
+
background: var(--panel2); border: 1px solid var(--line); border-radius: 9px;
|
|
54
|
+
}
|
|
55
|
+
ul.notes li.done .text { color: var(--muted); text-decoration: line-through; }
|
|
56
|
+
ul.notes .text { flex: 1; }
|
|
57
|
+
ul.notes .id { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
58
|
+
.empty { color: var(--muted); padding: 18px 2px; }
|
|
59
|
+
|
|
60
|
+
#login { max-width: 300px; margin: 60px auto; }
|
|
61
|
+
#app[hidden], #login[hidden] { display: none; }
|
|
62
|
+
.err { color: #f2665e; font-size: 13px; min-height: 18px; }
|
|
63
|
+
.topbar { display: flex; align-items: baseline; justify-content: space-between; }
|
|
64
|
+
.link { background: none; border: none; color: var(--muted); cursor: pointer; text-decoration: underline; padding: 0; }
|
|
65
|
+
</style>
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
|
|
69
|
+
<section id="login">
|
|
70
|
+
<h1>Demo Notes</h1>
|
|
71
|
+
<p class="sub">Sign in to load the assistant. Default password: <code>demo</code></p>
|
|
72
|
+
<div class="row">
|
|
73
|
+
<input id="password" class="field" type="password" placeholder="Password" data-testid="login-password" autocomplete="current-password">
|
|
74
|
+
<button id="login-submit" class="btn" data-testid="login-submit">Sign in</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div id="login-error" class="err" data-testid="login-error"></div>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<main id="app" hidden>
|
|
80
|
+
<div class="topbar">
|
|
81
|
+
<div>
|
|
82
|
+
<h1>Demo Notes</h1>
|
|
83
|
+
<p class="sub">The assistant at the bottom can read this list and change it.</p>
|
|
84
|
+
</div>
|
|
85
|
+
<button id="logout" class="link" data-testid="logout">Sign out</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="row">
|
|
89
|
+
<input id="new-note" class="field" placeholder="Add a note…" data-testid="note-input">
|
|
90
|
+
<button id="add-note" class="btn" data-testid="note-add">Add</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="row">
|
|
94
|
+
<input id="filter" class="field" placeholder="Filter…" data-testid="filter-input">
|
|
95
|
+
<button id="clear-filter" class="btn ghost" data-testid="filter-clear">Clear</button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<ul class="notes" id="note-list" data-testid="note-list" data-loading="false"></ul>
|
|
99
|
+
</main>
|
|
100
|
+
|
|
101
|
+
<!-- Asset versions. server.js rewrites the empty map at serve time (stampHtml),
|
|
102
|
+
and stamps the src/href attributes below with the same hashes. The map
|
|
103
|
+
exists for assets that are loaded from inside JavaScript — panel.css and
|
|
104
|
+
the blex pair — because an HTML-level stamp cannot see a URL that only
|
|
105
|
+
appears in a script. Left as {} by a server that does not do the rewrite,
|
|
106
|
+
in which case agentAsset() returns URLs unchanged. -->
|
|
107
|
+
<script>
|
|
108
|
+
window.__AGENT_ASSET_V = {};
|
|
109
|
+
window.agentAsset = function (url) {
|
|
110
|
+
var v = window.__AGENT_ASSET_V[url];
|
|
111
|
+
return v ? url + '?v=' + v : url;
|
|
112
|
+
};
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<!-- Load order matters: the registry and its dependencies must exist before
|
|
116
|
+
bridge-mount runs. bridge-mount is a module (it imports the cumulus
|
|
117
|
+
BridgeClient), so it is deferred automatically. -->
|
|
118
|
+
<script src="/app.js"></script>
|
|
119
|
+
<script src="/agent/device-thread.js"></script>
|
|
120
|
+
<script src="/agent/commands.js"></script>
|
|
121
|
+
<script src="/agent/chat-client.js"></script>
|
|
122
|
+
<script src="/agent/blex-mount.js"></script>
|
|
123
|
+
<script src="/agent/panel.js"></script>
|
|
124
|
+
<script type="module" src="/agent/bridge-mount.js"></script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// Serving layer for the demo app.
|
|
2
|
+
//
|
|
3
|
+
// The only part of this file that matters for the agent integration is
|
|
4
|
+
// GET /api/agent-config: the scoped gateway key is handed ONLY to an
|
|
5
|
+
// authenticated session, so it never appears in the static page. That is the
|
|
6
|
+
// difference between "anyone who views source can talk to your gateway" and
|
|
7
|
+
// "only your logged-in users can".
|
|
8
|
+
//
|
|
9
|
+
// Everything else here is a deliberately boring static file server so the kit
|
|
10
|
+
// runs with zero dependencies. Your real app already has this part.
|
|
11
|
+
//
|
|
12
|
+
// Env (same names the MCP shim uses — one vocabulary across the kit):
|
|
13
|
+
// PORT default 8199
|
|
14
|
+
// APP_PASSWORD default "demo" — the app's own login
|
|
15
|
+
// GATEWAY_API_KEY scoped cumulus key (namespace "demoapp"). Absent =>
|
|
16
|
+
// /api/agent-config 404s and the app runs agent-dark.
|
|
17
|
+
// GATEWAY_ORIGIN REQUIRED when GATEWAY_API_KEY is set. There is no default
|
|
18
|
+
// on purpose: whatever is listening on the usual gateway
|
|
19
|
+
// port is usually a REAL gateway, so a mistyped variable
|
|
20
|
+
// would silently point a demo app at production instead
|
|
21
|
+
// of failing.
|
|
22
|
+
|
|
23
|
+
import http from 'node:http';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import crypto from 'node:crypto';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
|
|
30
|
+
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const PUBLIC = path.join(ROOT, 'public');
|
|
32
|
+
|
|
33
|
+
/* ---- env, checked loudly --------------------------------------------------
|
|
34
|
+
An unread environment variable is silent by nature, and the value most
|
|
35
|
+
likely to be mistyped here is the gateway origin — whose old default was a
|
|
36
|
+
live gateway. So: names that are NOT read but are plausible mistakes for
|
|
37
|
+
ones that are get named explicitly and refuse to start. */
|
|
38
|
+
const ENV_ALIASES = {
|
|
39
|
+
AGENT_GATEWAY_ORIGIN: 'GATEWAY_ORIGIN',
|
|
40
|
+
AGENT_GATEWAY_URL: 'GATEWAY_ORIGIN',
|
|
41
|
+
GATEWAY_URL: 'GATEWAY_ORIGIN',
|
|
42
|
+
CUMULUS_GATEWAY_URL: 'GATEWAY_ORIGIN',
|
|
43
|
+
AGENT_API_KEY: 'GATEWAY_API_KEY',
|
|
44
|
+
CUMULUS_API_KEY: 'GATEWAY_API_KEY',
|
|
45
|
+
API_KEY: 'GATEWAY_API_KEY',
|
|
46
|
+
DEMO_PASSWORD: 'APP_PASSWORD',
|
|
47
|
+
PASSWORD: 'APP_PASSWORD',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function checkEnv(env) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
for (const [wrong, right] of Object.entries(ENV_ALIASES)) {
|
|
53
|
+
if (env[wrong] !== undefined && env[right] === undefined) {
|
|
54
|
+
errors.push(`${wrong} is not read by this server. Use ${right}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (env.GATEWAY_API_KEY && !env.GATEWAY_ORIGIN) {
|
|
58
|
+
errors.push(
|
|
59
|
+
'GATEWAY_API_KEY is set but GATEWAY_ORIGIN is not. Set it explicitly ' +
|
|
60
|
+
'(e.g. GATEWAY_ORIGIN=http://127.0.0.1:8080) — there is no default, because ' +
|
|
61
|
+
'guessing one would point this app at whatever gateway happens to be running.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (env.GATEWAY_ORIGIN) {
|
|
65
|
+
try {
|
|
66
|
+
const u = new URL(env.GATEWAY_ORIGIN);
|
|
67
|
+
if (!/^https?:$/.test(u.protocol)) throw new Error('not http(s)');
|
|
68
|
+
} catch {
|
|
69
|
+
errors.push(`GATEWAY_ORIGIN is not a valid http(s) origin: ${env.GATEWAY_ORIGIN}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const envErrors = checkEnv(process.env);
|
|
76
|
+
if (envErrors.length > 0) {
|
|
77
|
+
for (const e of envErrors) console.error(`[env] ${e}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const PORT = Number(process.env.PORT || 8199);
|
|
82
|
+
const PASSWORD = process.env.APP_PASSWORD || 'demo';
|
|
83
|
+
const API_KEY = process.env.GATEWAY_API_KEY || '';
|
|
84
|
+
const GATEWAY_ORIGIN = process.env.GATEWAY_ORIGIN || '';
|
|
85
|
+
|
|
86
|
+
/* ---- where the browser's BridgeClient comes from --------------------------
|
|
87
|
+
Cumulus owns the bridge client (Rule #8: one implementation). We do NOT
|
|
88
|
+
vendor a copy into this example — we serve the compiled files straight out
|
|
89
|
+
of the package, so this kit can never drift from the gateway it talks to.
|
|
90
|
+
`dist/gateway/bridge/*.js` is plain ESM with no Node imports, so a browser
|
|
91
|
+
loads it directly. */
|
|
92
|
+
function resolveBridgeDir() {
|
|
93
|
+
const inRepo = path.join(ROOT, '..', '..', 'dist', 'gateway', 'bridge');
|
|
94
|
+
if (fs.existsSync(path.join(inRepo, 'client.js'))) return inRepo;
|
|
95
|
+
try {
|
|
96
|
+
const require = createRequire(import.meta.url);
|
|
97
|
+
return path.dirname(require.resolve('@luckydraw/cumulus/dist/gateway/bridge/client.js'));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const BRIDGE_DIR = resolveBridgeDir();
|
|
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
|
+
if (pathname.startsWith('/agent/blex/')) {
|
|
172
|
+
const name = path.basename(pathname);
|
|
173
|
+
if (!STATIC_DIR || !/^(blex\.min|blex-render|blex-chart\.min)\.js$/.test(name)) return null;
|
|
174
|
+
return path.join(STATIC_DIR, name);
|
|
175
|
+
}
|
|
176
|
+
const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
177
|
+
const file = path.join(PUBLIC, rel);
|
|
178
|
+
return file === PUBLIC || file.startsWith(PUBLIC + path.sep) ? file : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Assets loaded from INSIDE JavaScript — a <link> built at runtime, a script
|
|
182
|
+
element appended by blex-mount. An HTML-level stamp cannot see a URL that
|
|
183
|
+
only exists in JS, so these get their hashes through the map below instead.
|
|
184
|
+
(This is the "a loader that fetches its own dependencies must propagate the
|
|
185
|
+
version token" case, applied where it is actually true.) */
|
|
186
|
+
const RUNTIME_LOADED = [
|
|
187
|
+
'/agent/panel.css',
|
|
188
|
+
'/agent/blex/blex.min.js',
|
|
189
|
+
'/agent/blex/blex-render.js',
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const ASSET_MAP_MARKER = 'window.__AGENT_ASSET_V = {};';
|
|
193
|
+
|
|
194
|
+
/** Stamp served HTML: `?v=` on every same-origin .js/.css src/href, plus the
|
|
195
|
+
version map for the runtime-loaded set. */
|
|
196
|
+
function stampHtml(html) {
|
|
197
|
+
const stamped = html.replace(
|
|
198
|
+
/(\s(?:src|href)=")(\/[^"?#]+\.(?:js|css))(")/g,
|
|
199
|
+
(m, pre, url, post) => {
|
|
200
|
+
const v = assetVersion(assetUrlToFile(url));
|
|
201
|
+
return v ? `${pre}${url}?v=${v}${post}` : m;
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
const map = {};
|
|
205
|
+
for (const url of RUNTIME_LOADED) {
|
|
206
|
+
const v = assetVersion(assetUrlToFile(url));
|
|
207
|
+
if (v) map[url] = v;
|
|
208
|
+
}
|
|
209
|
+
return stamped.replace(ASSET_MAP_MARKER, `window.__AGENT_ASSET_V = ${JSON.stringify(map)};`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ---- sessions (in-memory; your app has real ones) ------------------------ */
|
|
213
|
+
const sessions = new Set();
|
|
214
|
+
|
|
215
|
+
function sessionOf(req) {
|
|
216
|
+
const raw = req.headers.cookie || '';
|
|
217
|
+
const match = raw.match(/(?:^|;\s*)sid=([a-f0-9]{32})/);
|
|
218
|
+
return match && sessions.has(match[1]) ? match[1] : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* ---- helpers ------------------------------------------------------------- */
|
|
222
|
+
const MIME = {
|
|
223
|
+
'.html': 'text/html; charset=utf-8',
|
|
224
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
225
|
+
'.css': 'text/css; charset=utf-8',
|
|
226
|
+
'.json': 'application/json; charset=utf-8',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function json(res, status, body, headers = {}) {
|
|
230
|
+
res.writeHead(status, { 'content-type': MIME['.json'], ...headers });
|
|
231
|
+
res.end(JSON.stringify(body));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function readBody(req) {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
let raw = '';
|
|
237
|
+
req.on('data', c => {
|
|
238
|
+
raw += c;
|
|
239
|
+
if (raw.length > 1e5) reject(new Error('body too large'));
|
|
240
|
+
});
|
|
241
|
+
req.on('end', () => {
|
|
242
|
+
try {
|
|
243
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
244
|
+
} catch {
|
|
245
|
+
reject(new Error('invalid JSON'));
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
req.on('error', reject);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function sendFile(res, file, requestedVersion) {
|
|
253
|
+
fs.readFile(file, (err, buf) => {
|
|
254
|
+
if (err) {
|
|
255
|
+
res.writeHead(404);
|
|
256
|
+
return res.end('not found');
|
|
257
|
+
}
|
|
258
|
+
const ext = path.extname(file);
|
|
259
|
+
const body = ext === '.html' ? Buffer.from(stampHtml(buf.toString('utf8')), 'utf8') : buf;
|
|
260
|
+
// A ?v= that matches the CURRENT hash is content-addressed, so it is safe to
|
|
261
|
+
// cache forever. A stale or forged one must NOT be — that would pin today's
|
|
262
|
+
// bytes under a key that no longer describes them. HTML is never immutable:
|
|
263
|
+
// it is what carries the stamps.
|
|
264
|
+
const contentAddressed =
|
|
265
|
+
ext !== '.html' && requestedVersion !== undefined && requestedVersion === assetVersion(file);
|
|
266
|
+
res.writeHead(200, {
|
|
267
|
+
'content-type': MIME[ext] || 'application/octet-stream',
|
|
268
|
+
'cache-control': contentAddressed
|
|
269
|
+
? 'public, max-age=31536000, immutable'
|
|
270
|
+
: 'no-cache, must-revalidate',
|
|
271
|
+
});
|
|
272
|
+
res.end(body);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ---- routes -------------------------------------------------------------- */
|
|
277
|
+
const server = http.createServer(async (req, res) => {
|
|
278
|
+
const url = new URL(req.url, 'http://localhost');
|
|
279
|
+
const p = url.pathname;
|
|
280
|
+
|
|
281
|
+
if (req.method === 'POST' && p === '/api/login') {
|
|
282
|
+
let body;
|
|
283
|
+
try {
|
|
284
|
+
body = await readBody(req);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return json(res, 400, { error: e.message });
|
|
287
|
+
}
|
|
288
|
+
if (body.password !== PASSWORD) return json(res, 401, { error: 'wrong password' });
|
|
289
|
+
const sid = crypto.randomBytes(16).toString('hex');
|
|
290
|
+
sessions.add(sid);
|
|
291
|
+
return json(
|
|
292
|
+
res,
|
|
293
|
+
200,
|
|
294
|
+
{ ok: true },
|
|
295
|
+
{ 'set-cookie': `sid=${sid}; HttpOnly; SameSite=Lax; Path=/` }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (req.method === 'POST' && p === '/api/logout') {
|
|
300
|
+
const sid = sessionOf(req);
|
|
301
|
+
if (sid) sessions.delete(sid);
|
|
302
|
+
return json(res, 200, { ok: true }, { 'set-cookie': 'sid=; Max-Age=0; Path=/' });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Session probe. "Am I signed in?" is a different question from "give me the
|
|
306
|
+
// key", so it answers 200 either way. Asking the key endpoint instead would
|
|
307
|
+
// make every logged-out page load emit a 401 in the browser console — correct
|
|
308
|
+
// behaviour, alarming signal.
|
|
309
|
+
if (req.method === 'GET' && p === '/api/session') {
|
|
310
|
+
return json(res, 200, { authenticated: !!sessionOf(req) });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// THE ONE THAT MATTERS. The scoped key reaches only authenticated sessions.
|
|
314
|
+
// 404 when no key is configured, so the front end simply stays agent-dark
|
|
315
|
+
// instead of erroring.
|
|
316
|
+
if (req.method === 'GET' && p === '/api/agent-config') {
|
|
317
|
+
if (!sessionOf(req)) return json(res, 401, { error: 'not signed in' });
|
|
318
|
+
if (!API_KEY) return json(res, 404, { error: 'agent not configured' });
|
|
319
|
+
return json(res, 200, {
|
|
320
|
+
GATEWAY_URL: GATEWAY_ORIGIN,
|
|
321
|
+
BRIDGE_URL: GATEWAY_ORIGIN.replace(/^http/, 'ws') + '/bridge',
|
|
322
|
+
// Base name only. device-thread.js appends the per-visitor suffix in the
|
|
323
|
+
// browser, so the full thread name never travels from server to client.
|
|
324
|
+
THREAD_ID: 'demoapp-v',
|
|
325
|
+
API_KEY,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Static assets. assetUrlToFile owns the URL -> disk mapping (and the
|
|
330
|
+
// allowlists for the two directories outside PUBLIC); the routes below only
|
|
331
|
+
// pick the right 404 message when it declines.
|
|
332
|
+
const version = url.searchParams.get('v') ?? undefined;
|
|
333
|
+
|
|
334
|
+
// Bridge client, served from the cumulus package rather than a local copy.
|
|
335
|
+
if (p.startsWith('/agent/bridge-client/')) {
|
|
336
|
+
const file = assetUrlToFile(p);
|
|
337
|
+
if (!file) {
|
|
338
|
+
res.writeHead(404);
|
|
339
|
+
return res.end('bridge client unavailable — build cumulus or npm i @luckydraw/cumulus');
|
|
340
|
+
}
|
|
341
|
+
return sendFile(res, file, version);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Blex renderer + library, likewise served from the package.
|
|
345
|
+
if (p.startsWith('/agent/blex/')) {
|
|
346
|
+
const file = assetUrlToFile(p);
|
|
347
|
+
if (!file) {
|
|
348
|
+
res.writeHead(404);
|
|
349
|
+
return res.end('blex unavailable — build cumulus or npm i @luckydraw/cumulus');
|
|
350
|
+
}
|
|
351
|
+
return sendFile(res, file, version);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (req.method === 'GET') {
|
|
355
|
+
const file = assetUrlToFile(p);
|
|
356
|
+
if (!file) {
|
|
357
|
+
res.writeHead(403);
|
|
358
|
+
return res.end('forbidden');
|
|
359
|
+
}
|
|
360
|
+
return sendFile(res, file, version);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
res.writeHead(404);
|
|
364
|
+
res.end('not found');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
368
|
+
// The bound port, not the requested one — PORT=0 means "pick one for me".
|
|
369
|
+
const bound = server.address().port;
|
|
370
|
+
console.log(`demo app: http://127.0.0.1:${bound} (password: ${PASSWORD})`);
|
|
371
|
+
console.log(
|
|
372
|
+
`bridge client: ${BRIDGE_DIR ?? 'NOT FOUND — run `npm run build` in the cumulus repo'}`
|
|
373
|
+
);
|
|
374
|
+
console.log(
|
|
375
|
+
API_KEY
|
|
376
|
+
? `agent: enabled -> ${GATEWAY_ORIGIN}`
|
|
377
|
+
: 'agent: disabled (set GATEWAY_API_KEY + GATEWAY_ORIGIN)'
|
|
378
|
+
);
|
|
379
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luckydraw/cumulus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist/",
|
|
27
|
+
"examples/",
|
|
28
|
+
"docs/web-app-agent-guide.md",
|
|
29
|
+
"docs/agentic-harness-primer.md",
|
|
30
|
+
"docs/conditional-continuation.md",
|
|
27
31
|
"package.json",
|
|
28
32
|
"CHANGELOG.md"
|
|
29
33
|
],
|
|
@@ -53,8 +57,8 @@
|
|
|
53
57
|
"context",
|
|
54
58
|
"mcp"
|
|
55
59
|
],
|
|
56
|
-
"author": "",
|
|
57
|
-
"license": "
|
|
60
|
+
"author": "Lucky Draw LLC",
|
|
61
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
58
62
|
"publishConfig": {
|
|
59
63
|
"access": "public"
|
|
60
64
|
},
|