@luckydraw/cumulus 1.0.1 → 1.0.3
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 +15 -0
- package/dist/gateway/adapters/webchat.d.ts +10 -3
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +30 -5
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.d.ts +3 -1
- package/dist/gateway/gateway-agents-mcp.d.ts.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +3 -1
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/server.js +1 -1
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/static/blex-render.js +69 -11
- package/dist/gateway/static/chat.html +6 -0
- package/dist/gateway/static/mermaid-esm.js +2143 -0
- package/dist/lib/gateway.d.ts +61 -5
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +81 -10
- package/dist/lib/gateway.js.map +1 -1
- package/dist/mcp/index.js +4 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +10 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-handler.d.ts +11 -0
- package/dist/mcp/tool-handler.d.ts.map +1 -1
- package/dist/mcp/tool-handler.js +28 -2
- package/dist/mcp/tool-handler.js.map +1 -1
- package/docs/web-app-agent-guide.md +79 -22
- package/examples/web-app-agent/README.md +63 -4
- package/examples/web-app-agent/agent/apply-thread-configs.mjs +139 -0
- package/examples/web-app-agent/public/index.html +13 -0
- package/examples/web-app-agent/server.js +34 -10
- 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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Apply this app's two thread configs to the gateway — the base thread you work
|
|
3
|
+
in, and the '-v' sub-namespace every visitor turn inherits.
|
|
4
|
+
*
|
|
5
|
+
* WHY A SCRIPT
|
|
6
|
+
* The visitor config is the one piece of per-app setup with no UI and no obvious
|
|
7
|
+
* home: it lives in a file on the gateway host, its name encodes a namespace rule
|
|
8
|
+
* (see thread-config.visitor.example.json), and getting it wrong silently costs
|
|
9
|
+
* money — every anonymous visitor runs whatever model your management thread uses.
|
|
10
|
+
* One command, run once per app, beats a paragraph telling you to hand-write JSON
|
|
11
|
+
* in a directory you may not have shell access to.
|
|
12
|
+
*
|
|
13
|
+
* GATEWAY_ORIGIN=http://127.0.0.1:8080 \
|
|
14
|
+
* GATEWAY_ADMIN_KEY=sk-... \
|
|
15
|
+
* node agent/apply-thread-configs.mjs [--namespace demoapp] [--dry-run]
|
|
16
|
+
*
|
|
17
|
+
* GATEWAY_ADMIN_KEY, not the app's scoped key: a namespace covers '<ns>-*' only,
|
|
18
|
+
* so a scoped key can write '<ns>-v' but is refused (403) on the bare '<ns>' base
|
|
19
|
+
* thread. That asymmetry is the P7 access model working, not a bug — this is an
|
|
20
|
+
* operator action.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
|
|
28
|
+
const ORIGIN = process.env.GATEWAY_ORIGIN;
|
|
29
|
+
const KEY = process.env.GATEWAY_ADMIN_KEY;
|
|
30
|
+
|
|
31
|
+
/* Near-miss names fail loudly rather than falling back to a default that might be
|
|
32
|
+
somebody's real gateway — the same rule server.js applies. */
|
|
33
|
+
const ALIASES = {
|
|
34
|
+
GATEWAY_URL: 'GATEWAY_ORIGIN',
|
|
35
|
+
AGENT_GATEWAY_ORIGIN: 'GATEWAY_ORIGIN',
|
|
36
|
+
GATEWAY_KEY: 'GATEWAY_ADMIN_KEY',
|
|
37
|
+
ADMIN_KEY: 'GATEWAY_ADMIN_KEY',
|
|
38
|
+
GATEWAY_API_KEY: 'GATEWAY_ADMIN_KEY (the scoped app key cannot write the base thread)',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function die(msg) {
|
|
42
|
+
console.error(`\n ${msg}\n`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const [wrong, right] of Object.entries(ALIASES)) {
|
|
47
|
+
if (process.env[wrong] && !(wrong === 'GATEWAY_API_KEY' && KEY)) {
|
|
48
|
+
die(`${wrong} is set but this script reads ${right}.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!ORIGIN) die('GATEWAY_ORIGIN is required (no default — it must not guess a live gateway).');
|
|
52
|
+
if (!KEY) die('GATEWAY_ADMIN_KEY is required.');
|
|
53
|
+
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
const dryRun = args.includes('--dry-run');
|
|
56
|
+
const nsFlag = args.indexOf('--namespace');
|
|
57
|
+
const namespace = nsFlag !== -1 ? args[nsFlag + 1] : 'demoapp';
|
|
58
|
+
if (!/^[a-z0-9][a-z0-9-]*$/i.test(namespace)) die(`invalid --namespace: ${namespace}`);
|
|
59
|
+
|
|
60
|
+
/** Read an example config and drop the `_`-prefixed annotation keys. */
|
|
61
|
+
function load(file) {
|
|
62
|
+
const full = path.join(ROOT, file);
|
|
63
|
+
if (!fs.existsSync(full)) die(`missing ${file}`);
|
|
64
|
+
const parsed = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
65
|
+
return Object.fromEntries(Object.entries(parsed).filter(([k]) => !k.startsWith('_')));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const targets = [
|
|
69
|
+
{ thread: namespace, file: 'thread-config.example.json' },
|
|
70
|
+
{ thread: `${namespace}-v`, file: 'thread-config.visitor.example.json' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
let failed = false;
|
|
74
|
+
for (const { thread, file } of targets) {
|
|
75
|
+
const config = load(file);
|
|
76
|
+
if (String(config.projectDir || '').startsWith('/absolute/path/')) {
|
|
77
|
+
die(`${file} still has the placeholder projectDir — edit it before applying.`);
|
|
78
|
+
}
|
|
79
|
+
const label = `${thread.padEnd(24)} <- ${file}`;
|
|
80
|
+
if (dryRun) {
|
|
81
|
+
console.log(`DRY RUN ${label}\n ${JSON.stringify(config)}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let res;
|
|
85
|
+
try {
|
|
86
|
+
res = await fetch(`${ORIGIN}/api/thread/${encodeURIComponent(thread)}/config`, {
|
|
87
|
+
method: 'PUT',
|
|
88
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': KEY },
|
|
89
|
+
body: JSON.stringify(config),
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
die(`cannot reach ${ORIGIN}: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (res.ok) {
|
|
95
|
+
console.log(`OK ${label}`);
|
|
96
|
+
/* The config API applies a whitelist (projectDir, template, model, effort,
|
|
97
|
+
claudeModel, contextLimit). alwaysInclude and disallowedTools are NOT in it,
|
|
98
|
+
deliberately: combined with projectDir, alwaysInclude would let a
|
|
99
|
+
namespace-scoped key — which ships in your public page — read an arbitrary
|
|
100
|
+
file into its own prompt. So rather than assume, read the config back and
|
|
101
|
+
name anything that did not stick. A silent drop here would look like a
|
|
102
|
+
working persona that was never installed. */
|
|
103
|
+
try {
|
|
104
|
+
const check = await fetch(`${ORIGIN}/api/thread/${encodeURIComponent(thread)}/config`, {
|
|
105
|
+
headers: { 'X-API-Key': KEY },
|
|
106
|
+
});
|
|
107
|
+
if (check.ok) {
|
|
108
|
+
const stored = (await check.json()) ?? {};
|
|
109
|
+
const missing = Object.keys(config).filter(k => stored[k] === undefined);
|
|
110
|
+
if (missing.length) {
|
|
111
|
+
console.log(` not applied by the API: ${missing.join(', ')}`);
|
|
112
|
+
console.log(
|
|
113
|
+
` add them by hand to ~/.cumulus/threads/${thread}.config.json on the`
|
|
114
|
+
);
|
|
115
|
+
console.log(' gateway host — this file is exactly those contents.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
/* the write succeeded; a failed read-back is not worth failing over */
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
failed = true;
|
|
123
|
+
const body = await res.text();
|
|
124
|
+
console.error(`FAILED ${label}\n ${res.status} ${body.slice(0, 200)}`);
|
|
125
|
+
if (res.status === 403) {
|
|
126
|
+
console.error(' 403 on the base thread means the key is namespace-scoped.');
|
|
127
|
+
console.error(' Use the gateway admin key for this one-time setup.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (failed) process.exit(1);
|
|
133
|
+
if (!dryRun) {
|
|
134
|
+
console.log(
|
|
135
|
+
`\nDone. Visitor turns on ${namespace}-v-<deviceId> now read ${namespace}-v.config.json;\n` +
|
|
136
|
+
`your own ${namespace} thread is unaffected. No gateway reload needed —\n` +
|
|
137
|
+
'thread config is read per turn.'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -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. */
|
|
@@ -168,9 +168,15 @@ function assetUrlToFile(pathname) {
|
|
|
168
168
|
// blex-chart.min.js is an OPT-IN companion global (blex.min.js inlines Chart.js
|
|
169
169
|
// and never fetches it). Nothing requests it today; it is allowlisted so an
|
|
170
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.
|
|
171
175
|
if (pathname.startsWith('/agent/blex/')) {
|
|
172
176
|
const name = path.basename(pathname);
|
|
173
|
-
if (!STATIC_DIR || !/^(blex\.min|blex-render|blex-chart\.min)\.js$/.test(name))
|
|
177
|
+
if (!STATIC_DIR || !/^(blex\.min|blex-render|blex-chart\.min|mermaid-esm)\.js$/.test(name)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
174
180
|
return path.join(STATIC_DIR, name);
|
|
175
181
|
}
|
|
176
182
|
const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
@@ -191,16 +197,34 @@ const RUNTIME_LOADED = [
|
|
|
191
197
|
|
|
192
198
|
const ASSET_MAP_MARKER = 'window.__AGENT_ASSET_V = {};';
|
|
193
199
|
|
|
194
|
-
/** Stamp served HTML: `?v=` on every same-origin .js/.css src/href
|
|
195
|
-
version map for the runtime-loaded set.
|
|
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 `/`. */
|
|
196
208
|
function stampHtml(html) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
);
|
|
204
228
|
const map = {};
|
|
205
229
|
for (const url of RUNTIME_LOADED) {
|
|
206
230
|
const v = assetVersion(assetUrlToFile(url));
|
|
@@ -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.3",
|
|
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",
|