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.
Files changed (34) hide show
  1. package/cli.js +57 -0
  2. package/default/infra/vibe/README.md +43 -0
  3. package/default/infra/vibe/cloudflare.ini.template +26 -0
  4. package/default/infra/vibe/ecosystem.vibe.config.cjs +44 -0
  5. package/default/infra/vibe/nginx-vibe-orchestrator.conf.template +50 -0
  6. package/default/infra/vibe/nginx-vibe-staging.conf.template +73 -0
  7. package/default/infra/vibe/vibe-gateway.service +38 -0
  8. package/default/infra/vibe/vibe-orchestrator.service +44 -0
  9. package/default/infra/vibe/vibe.env.template +24 -0
  10. package/default/mango/config/settings.json +35 -1
  11. package/default/vite.config.js +46 -0
  12. package/lib/devProxyGateway.js +173 -0
  13. package/lib/staging-gateway.js +23 -1
  14. package/lib/vibe-orchestrator/README.md +76 -0
  15. package/lib/vibe-orchestrator/scripts/fake-claude.mjs +35 -0
  16. package/lib/vibe-orchestrator/scripts/path-guard-hook.mjs +70 -0
  17. package/lib/vibe-orchestrator/scripts/vibe-recover.sh +63 -0
  18. package/lib/vibe-orchestrator/server.js +344 -0
  19. package/lib/vibe-orchestrator/src/attachments.js +98 -0
  20. package/lib/vibe-orchestrator/src/claudeRunner.js +233 -0
  21. package/lib/vibe-orchestrator/src/config.js +227 -0
  22. package/lib/vibe-orchestrator/src/costMirror.js +64 -0
  23. package/lib/vibe-orchestrator/src/costStore.js +209 -0
  24. package/lib/vibe-orchestrator/src/ownerToken.js +113 -0
  25. package/lib/vibe-orchestrator/src/pathGuard.js +114 -0
  26. package/lib/vibe-orchestrator/src/preamble.js +139 -0
  27. package/lib/vibe-orchestrator/src/publisher.js +376 -0
  28. package/lib/vibe-orchestrator/src/recovery.js +199 -0
  29. package/lib/vibe-orchestrator/src/screenshot.js +38 -0
  30. package/lib/vibe-orchestrator/src/sessionManager.js +291 -0
  31. package/lib/vibe-orchestrator/src/streamParser.js +188 -0
  32. package/lib/vibe-orchestrator/src/tokenMeter.js +64 -0
  33. package/package.json +1 -1
  34. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mango-cms",
3
- "version": "0.3.33",
3
+ "version": "0.3.35",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
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)