mango-cms 0.3.33 → 0.3.35
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/cli.js +57 -0
- package/default/infra/vibe/README.md +43 -0
- package/default/infra/vibe/cloudflare.ini.template +26 -0
- package/default/infra/vibe/ecosystem.vibe.config.cjs +44 -0
- package/default/infra/vibe/nginx-vibe-orchestrator.conf.template +50 -0
- package/default/infra/vibe/nginx-vibe-staging.conf.template +73 -0
- package/default/infra/vibe/vibe-gateway.service +38 -0
- package/default/infra/vibe/vibe-orchestrator.service +44 -0
- package/default/infra/vibe/vibe.env.template +24 -0
- package/default/mango/config/settings.json +35 -1
- package/default/vite.config.js +46 -0
- package/lib/devProxyGateway.js +173 -0
- package/lib/staging-gateway.js +23 -1
- package/lib/vibe-orchestrator/README.md +76 -0
- package/lib/vibe-orchestrator/scripts/fake-claude.mjs +35 -0
- package/lib/vibe-orchestrator/scripts/path-guard-hook.mjs +70 -0
- package/lib/vibe-orchestrator/scripts/vibe-recover.sh +63 -0
- package/lib/vibe-orchestrator/server.js +344 -0
- package/lib/vibe-orchestrator/src/attachments.js +98 -0
- package/lib/vibe-orchestrator/src/claudeRunner.js +233 -0
- package/lib/vibe-orchestrator/src/config.js +227 -0
- package/lib/vibe-orchestrator/src/costMirror.js +64 -0
- package/lib/vibe-orchestrator/src/costStore.js +209 -0
- package/lib/vibe-orchestrator/src/ownerToken.js +113 -0
- package/lib/vibe-orchestrator/src/pathGuard.js +114 -0
- package/lib/vibe-orchestrator/src/preamble.js +139 -0
- package/lib/vibe-orchestrator/src/publisher.js +376 -0
- package/lib/vibe-orchestrator/src/recovery.js +199 -0
- package/lib/vibe-orchestrator/src/screenshot.js +38 -0
- package/lib/vibe-orchestrator/src/sessionManager.js +291 -0
- package/lib/vibe-orchestrator/src/streamParser.js +188 -0
- package/lib/vibe-orchestrator/src/tokenMeter.js +64 -0
- package/package.json +1 -1
- package/readme.md +6 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// streamParser.js
|
|
2
|
+
//
|
|
3
|
+
// Parses Claude Code headless `--output-format stream-json` output into a
|
|
4
|
+
// normalized stream of progress events the front-end can render.
|
|
5
|
+
//
|
|
6
|
+
// Claude Code emits newline-delimited JSON (NDJSON). stdout chunks can split a
|
|
7
|
+
// line anywhere, so we buffer until we see a newline. Each complete line is one
|
|
8
|
+
// JSON object; we normalize the shapes we care about and pass everything else
|
|
9
|
+
// through as `unknown` so nothing is silently dropped.
|
|
10
|
+
//
|
|
11
|
+
// Known top-level message types (Claude Code v2.x):
|
|
12
|
+
// { type: "system", subtype: "init", session_id, model, tools, cwd, ... }
|
|
13
|
+
// { type: "assistant", message: { content: [...], usage: {...} }, session_id }
|
|
14
|
+
// { type: "user", message: { content: [tool_result ...] }, session_id }
|
|
15
|
+
// { type: "result", subtype, total_cost_usd, duration_ms, usage, result, session_id }
|
|
16
|
+
//
|
|
17
|
+
// Pure module: no I/O, no process access. Fully unit-testable without a token.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} ProgressEvent
|
|
21
|
+
* @property {string} kind - normalized event kind (see below)
|
|
22
|
+
* @property {number} [ts] - caller-supplied timestamp (parser never reads the clock)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const KINDS = Object.freeze({
|
|
26
|
+
SESSION_START: 'session_start',
|
|
27
|
+
ASSISTANT_TEXT: 'assistant_text',
|
|
28
|
+
THINKING: 'thinking',
|
|
29
|
+
TOOL_USE: 'tool_use',
|
|
30
|
+
TOOL_RESULT: 'tool_result',
|
|
31
|
+
RESULT: 'result',
|
|
32
|
+
PARSE_ERROR: 'parse_error',
|
|
33
|
+
UNKNOWN: 'unknown',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** Pull a normalized usage object out of whatever shape it arrives in. */
|
|
37
|
+
function normalizeUsage(usage) {
|
|
38
|
+
if (!usage || typeof usage !== 'object') return null
|
|
39
|
+
const freshInput = usage.input_tokens || 0
|
|
40
|
+
const cacheCreation = usage.cache_creation_input_tokens || 0
|
|
41
|
+
const cacheRead = usage.cache_read_input_tokens || 0
|
|
42
|
+
const input = freshInput + cacheCreation + cacheRead
|
|
43
|
+
const output = usage.output_tokens || 0
|
|
44
|
+
return {
|
|
45
|
+
inputTokens: input, // total input incl. cached context (display)
|
|
46
|
+
outputTokens: output,
|
|
47
|
+
cacheReadTokens: cacheRead,
|
|
48
|
+
totalTokens: input + output, // full token volume (display)
|
|
49
|
+
// What actually counts against the per-session cap: NEW tokens only. The
|
|
50
|
+
// cached context (cache_read) is re-reported on every turn and is cheap
|
|
51
|
+
// already-paid context — summing it across turns balloons the count and
|
|
52
|
+
// would trip the cap on a trivial multi-turn edit. Exclude it.
|
|
53
|
+
billableTokens: freshInput + cacheCreation + output,
|
|
54
|
+
raw: usage,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Normalize a single already-parsed NDJSON object into 0+ progress events. */
|
|
59
|
+
function normalizeMessage(msg) {
|
|
60
|
+
if (!msg || typeof msg !== 'object') {
|
|
61
|
+
return [{ kind: KINDS.UNKNOWN, raw: msg }]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
switch (msg.type) {
|
|
65
|
+
case 'system':
|
|
66
|
+
if (msg.subtype === 'init') {
|
|
67
|
+
return [{
|
|
68
|
+
kind: KINDS.SESSION_START,
|
|
69
|
+
sessionId: msg.session_id || null,
|
|
70
|
+
model: msg.model || null,
|
|
71
|
+
tools: Array.isArray(msg.tools) ? msg.tools : [],
|
|
72
|
+
cwd: msg.cwd || null,
|
|
73
|
+
}]
|
|
74
|
+
}
|
|
75
|
+
return [{ kind: KINDS.UNKNOWN, raw: msg }]
|
|
76
|
+
|
|
77
|
+
case 'assistant': {
|
|
78
|
+
const content = msg.message?.content
|
|
79
|
+
const usage = normalizeUsage(msg.message?.usage)
|
|
80
|
+
const events = []
|
|
81
|
+
if (Array.isArray(content)) {
|
|
82
|
+
for (const block of content) {
|
|
83
|
+
if (block.type === 'text' && block.text) {
|
|
84
|
+
events.push({ kind: KINDS.ASSISTANT_TEXT, text: block.text })
|
|
85
|
+
} else if (block.type === 'thinking' && block.thinking) {
|
|
86
|
+
events.push({ kind: KINDS.THINKING, text: block.thinking })
|
|
87
|
+
} else if (block.type === 'tool_use') {
|
|
88
|
+
events.push({
|
|
89
|
+
kind: KINDS.TOOL_USE,
|
|
90
|
+
tool: block.name,
|
|
91
|
+
toolId: block.id,
|
|
92
|
+
input: block.input,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// An assistant turn with usage but no renderable block still carries token
|
|
98
|
+
// accounting we must not lose — emit a bare usage-only event.
|
|
99
|
+
if (events.length === 0 && usage) {
|
|
100
|
+
events.push({ kind: KINDS.ASSISTANT_TEXT, text: '' })
|
|
101
|
+
}
|
|
102
|
+
// One assistant message == one model API call == one usage report. Attach
|
|
103
|
+
// usage to the FIRST event only so the token meter counts it once per turn,
|
|
104
|
+
// not once per content block (a turn may carry text + tool_use together).
|
|
105
|
+
if (events.length && usage) events[0].usage = usage
|
|
106
|
+
return events
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'user': {
|
|
110
|
+
const content = msg.message?.content
|
|
111
|
+
const events = []
|
|
112
|
+
if (Array.isArray(content)) {
|
|
113
|
+
for (const block of content) {
|
|
114
|
+
if (block.type === 'tool_result') {
|
|
115
|
+
events.push({
|
|
116
|
+
kind: KINDS.TOOL_RESULT,
|
|
117
|
+
toolId: block.tool_use_id,
|
|
118
|
+
isError: !!block.is_error,
|
|
119
|
+
content: block.content,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return events.length ? events : [{ kind: KINDS.UNKNOWN, raw: msg }]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'result':
|
|
128
|
+
return [{
|
|
129
|
+
kind: KINDS.RESULT,
|
|
130
|
+
subtype: msg.subtype || null,
|
|
131
|
+
isError: !!msg.is_error,
|
|
132
|
+
durationMs: msg.duration_ms ?? null,
|
|
133
|
+
costUsd: msg.total_cost_usd ?? null,
|
|
134
|
+
usage: normalizeUsage(msg.usage),
|
|
135
|
+
numTurns: msg.num_turns ?? null,
|
|
136
|
+
sessionId: msg.session_id || null,
|
|
137
|
+
result: typeof msg.result === 'string' ? msg.result : null,
|
|
138
|
+
}]
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
return [{ kind: KINDS.UNKNOWN, raw: msg }]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Stateful NDJSON line buffer. Feed it raw stdout chunks; it returns the
|
|
147
|
+
* normalized progress events for every *complete* line seen so far.
|
|
148
|
+
*/
|
|
149
|
+
class StreamParser {
|
|
150
|
+
constructor() {
|
|
151
|
+
this.buffer = ''
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string|Buffer} chunk
|
|
156
|
+
* @returns {ProgressEvent[]}
|
|
157
|
+
*/
|
|
158
|
+
push(chunk) {
|
|
159
|
+
this.buffer += chunk.toString()
|
|
160
|
+
const events = []
|
|
161
|
+
let nl
|
|
162
|
+
while ((nl = this.buffer.indexOf('\n')) !== -1) {
|
|
163
|
+
const line = this.buffer.slice(0, nl).trim()
|
|
164
|
+
this.buffer = this.buffer.slice(nl + 1)
|
|
165
|
+
if (line) events.push(...this._parseLine(line))
|
|
166
|
+
}
|
|
167
|
+
return events
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Flush any trailing partial line at stream end. */
|
|
171
|
+
flush() {
|
|
172
|
+
const line = this.buffer.trim()
|
|
173
|
+
this.buffer = ''
|
|
174
|
+
return line ? this._parseLine(line) : []
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_parseLine(line) {
|
|
178
|
+
let parsed
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(line)
|
|
181
|
+
} catch {
|
|
182
|
+
return [{ kind: KINDS.PARSE_ERROR, raw: line }]
|
|
183
|
+
}
|
|
184
|
+
return normalizeMessage(parsed)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { StreamParser, normalizeMessage, normalizeUsage, KINDS }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// tokenMeter.js
|
|
2
|
+
//
|
|
3
|
+
// Cumulative token accounting + per-session cap. Fed the `usage` objects that
|
|
4
|
+
// streamParser attaches to assistant events. Pure/in-memory: one instance per
|
|
5
|
+
// session.
|
|
6
|
+
//
|
|
7
|
+
// The cap counts BILLABLE tokens (new input + cache creation + output), NOT the
|
|
8
|
+
// full `totalTokens` — Claude Code re-reports the whole cached context
|
|
9
|
+
// (cache_read) on every turn, so summing totals across turns balloons the count
|
|
10
|
+
// and would trip the cap on a trivial multi-turn edit. We still track the full
|
|
11
|
+
// volume for display.
|
|
12
|
+
|
|
13
|
+
class TokenMeter {
|
|
14
|
+
/** @param {number} cap - max cumulative BILLABLE tokens before tripping (0 = unlimited) */
|
|
15
|
+
constructor(cap = 0) {
|
|
16
|
+
this.cap = cap > 0 ? cap : 0
|
|
17
|
+
this.inputTokens = 0 // full input incl. re-read cache (display)
|
|
18
|
+
this.outputTokens = 0
|
|
19
|
+
this.cacheReadTokens = 0 // re-read cached context (display / cost breakdown)
|
|
20
|
+
this.billableTokens = 0 // new input + cache creation + output (cap basis)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get totalTokens() {
|
|
24
|
+
return this.inputTokens + this.outputTokens
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Record one assistant turn's usage. Returns true if the cap is now exceeded.
|
|
29
|
+
* Accepts the normalized usage from streamParser; falls back to totalTokens
|
|
30
|
+
* when billableTokens is absent (older callers / hand-built usage in tests).
|
|
31
|
+
* @param {{inputTokens?:number, outputTokens?:number, totalTokens?:number, billableTokens?:number}|null} usage
|
|
32
|
+
*/
|
|
33
|
+
record(usage) {
|
|
34
|
+
if (!usage) return this.exceeded
|
|
35
|
+
this.inputTokens += usage.inputTokens || 0
|
|
36
|
+
this.outputTokens += usage.outputTokens || 0
|
|
37
|
+
this.cacheReadTokens += usage.cacheReadTokens || 0
|
|
38
|
+
const billable = usage.billableTokens != null
|
|
39
|
+
? usage.billableTokens
|
|
40
|
+
: (usage.totalTokens != null
|
|
41
|
+
? usage.totalTokens
|
|
42
|
+
: (usage.inputTokens || 0) + (usage.outputTokens || 0))
|
|
43
|
+
this.billableTokens += billable
|
|
44
|
+
return this.exceeded
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get exceeded() {
|
|
48
|
+
return this.cap > 0 && this.billableTokens >= this.cap
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
snapshot() {
|
|
52
|
+
return {
|
|
53
|
+
inputTokens: this.inputTokens,
|
|
54
|
+
outputTokens: this.outputTokens,
|
|
55
|
+
cacheReadTokens: this.cacheReadTokens,
|
|
56
|
+
totalTokens: this.totalTokens,
|
|
57
|
+
billableTokens: this.billableTokens,
|
|
58
|
+
cap: this.cap,
|
|
59
|
+
exceeded: this.exceeded,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { TokenMeter }
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Mango is a powerful, code-first CMS built on MongoDB that emphasizes configuration through code. This documentation covers how to configure and use Mango CMS through the `config` folder.
|
|
4
4
|
|
|
5
|
+
> **Vibe (in-page AI coding):** to activate the optional ⌘K "edit your live site"
|
|
6
|
+
> experience on a Mango project, follow [docs/VIBE_ACTIVATION.md](docs/VIBE_ACTIVATION.md)
|
|
7
|
+
> — install/upgrade → flip `settings.vibe.enabled` → set `VITE_VIBE_SHELL` → run
|
|
8
|
+
> the bundled orchestrator + gateway behind the infra templates in
|
|
9
|
+
> [default/infra/vibe/](default/infra/vibe/). No source copying required.
|
|
10
|
+
|
|
5
11
|
## Table of Contents
|
|
6
12
|
|
|
7
13
|
1. [Collections](#collections)
|