@vibes.diy/api-svc 12.2.11 → 12.3.0
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/cf-serve.js +29 -19
- package/cf-serve.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/intern/first-message-embedding.js +2 -2
- package/intern/first-message-embedding.js.map +1 -1
- package/intern/llm-tool-loop.d.ts +19 -0
- package/intern/llm-tool-loop.js +70 -49
- package/intern/llm-tool-loop.js.map +1 -1
- package/intern/prompt-assembly.js +4 -2
- package/intern/prompt-assembly.js.map +1 -1
- package/intern/work-tools.d.ts +24 -2
- package/intern/work-tools.js +44 -9
- package/intern/work-tools.js.map +1 -1
- package/intern/work-turn-status.d.ts +19 -0
- package/intern/work-turn-status.js +53 -0
- package/intern/work-turn-status.js.map +1 -0
- package/package.json +11 -11
- package/public/cold-pull-timing.d.ts +1 -0
- package/public/cold-pull-timing.js +1 -1
- package/public/cold-pull-timing.js.map +1 -1
- package/public/get-application-chat.js +2 -2
- package/public/get-application-chat.js.map +1 -1
- package/public/get-chat-response.js +2 -2
- package/public/get-chat-response.js.map +1 -1
- package/public/get-vibe-chips.js +2 -2
- package/public/get-vibe-chips.js.map +1 -1
- package/public/llm-dispatch.d.ts +1 -1
- package/public/prompt-chat-section.js +72 -16
- package/public/prompt-chat-section.js.map +1 -1
- package/public/stream-assembly.d.ts +14 -1
- package/public/stream-assembly.js +120 -2
- package/public/stream-assembly.js.map +1 -1
- package/sessions-dispatch-timing.d.ts +11 -1
- package/sessions-dispatch-timing.js +22 -8
- package/sessions-dispatch-timing.js.map +1 -1
- package/svc-ws-send-provider.js +5 -1
- package/svc-ws-send-provider.js.map +1 -1
- package/usage-report/check-env.sh +188 -0
- package/usage-report/user-journey.js +29 -3
- package/usage-report/user-journey.js.map +1 -1
- package/usage-report/vibe-usage.py +117 -53
|
@@ -15,12 +15,19 @@ Usage:
|
|
|
15
15
|
OWNER=owner APP_SLUG=my-app CF_PATH='/vibe/owner/my-app%' python3 vibe-usage.py 168
|
|
16
16
|
|
|
17
17
|
Requires env (present in cloud sessions): CLOUDFLARE_API_TOKEN, CLERK_SECRET_KEY.
|
|
18
|
+
Both are ENFORCED — a missing key exits 2 rather than zeroing a leg of the
|
|
19
|
+
funnel. Check with usage-report/check-env.sh --tool vibe-usage; opt out of a
|
|
20
|
+
leg deliberately with --skip-visitors / --skip-signups.
|
|
18
21
|
Postgres goes through the repo's db:inspect (prod, read-only).
|
|
19
22
|
"""
|
|
20
23
|
import json, os, subprocess, sys, urllib.request, urllib.error
|
|
21
24
|
from datetime import datetime, timedelta, timezone
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
_FLAGS = {a for a in sys.argv[1:] if a.startswith("--")}
|
|
27
|
+
_POSITIONAL = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
28
|
+
HOURS = int(_POSITIONAL[0]) if _POSITIONAL else 48
|
|
29
|
+
SKIP_VISITORS = "--skip-visitors" in _FLAGS
|
|
30
|
+
SKIP_SIGNUPS = "--skip-signups" in _FLAGS
|
|
24
31
|
# App identity in Postgres is keyed by (userSlug, appSlug) — filtering on appSlug
|
|
25
32
|
# alone would aggregate dev/remix/test twins under other handles. OWNER scopes it
|
|
26
33
|
# to the one owner whose /vibe/<owner>/<slug> path the CF_PATH also targets.
|
|
@@ -38,9 +45,46 @@ since_iso = since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
38
45
|
now_iso = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
39
46
|
since_ms = int(since.timestamp() * 1000)
|
|
40
47
|
|
|
48
|
+
def load_dev_vars():
|
|
49
|
+
"""Mirror loadDevVars() in usage-report-util.ts: api/svc/.dev.vars is a
|
|
50
|
+
supported source for these keys, so the TS tools, check-env.sh and this
|
|
51
|
+
script must all agree on what 'set' means. First occurrence wins, quotes
|
|
52
|
+
are stripped, and a real environment variable always takes precedence."""
|
|
53
|
+
for path in (os.path.join(DB_DIR, ".dev.vars"), ".dev.vars"):
|
|
54
|
+
if not os.path.exists(path):
|
|
55
|
+
continue
|
|
56
|
+
with open(path, encoding="utf8") as fh:
|
|
57
|
+
for raw in fh:
|
|
58
|
+
line = raw.strip()
|
|
59
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
60
|
+
continue
|
|
61
|
+
key, _, value = line.partition("=")
|
|
62
|
+
key, value = key.strip(), value.strip()
|
|
63
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
64
|
+
value = value[1:-1]
|
|
65
|
+
os.environ.setdefault(key, value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
load_dev_vars()
|
|
41
69
|
CF_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN")
|
|
42
70
|
CLERK_KEY = os.environ.get("CLERK_SECRET_KEY")
|
|
43
71
|
|
|
72
|
+
# A missing key must not silently zero a leg of the funnel. Without CF_TOKEN the
|
|
73
|
+
# requests go out as "Bearer None", the HTTPError is swallowed, the counters stay
|
|
74
|
+
# at their zero initialisers, and the last line reads "FUNNEL: ~0 visits" — a
|
|
75
|
+
# plausible number about a dead app rather than a visible failure. Fail closed;
|
|
76
|
+
# --skip-visitors / --skip-signups opt out explicitly and stamp the funnel.
|
|
77
|
+
_missing = []
|
|
78
|
+
if not CF_TOKEN and not SKIP_VISITORS:
|
|
79
|
+
_missing.append("CLOUDFLARE_API_TOKEN (visitor traffic; without it the funnel would read ~0 visits)")
|
|
80
|
+
if not CLERK_KEY and not SKIP_SIGNUPS:
|
|
81
|
+
_missing.append("CLERK_SECRET_KEY (sign-ups; without it the funnel would read 0 sign-ups)")
|
|
82
|
+
if _missing:
|
|
83
|
+
print("refusing to run — missing credentials:\n " + "\n ".join(_missing), file=sys.stderr)
|
|
84
|
+
print("\ncheck your env: usage-report/check-env.sh --tool vibe-usage"
|
|
85
|
+
"\nor accept a partial funnel with --skip-visitors / --skip-signups", file=sys.stderr)
|
|
86
|
+
sys.exit(2)
|
|
87
|
+
|
|
44
88
|
|
|
45
89
|
def db(sql):
|
|
46
90
|
"""Run a read-only query via db:inspect, return parsed rows."""
|
|
@@ -109,63 +153,83 @@ print(f" writers new-to-app={split['new']} returning={split['returning']}")
|
|
|
109
153
|
# ---- 2. Cloudflare: read-only visitors (day-sliced; zone caps at 1d) ------
|
|
110
154
|
print("\nTRAFFIC (all visitors, incl. non-signed-in; sampled + IP counts overlap days)")
|
|
111
155
|
tot_ip = tot_visits = tot_req = 0
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
cf_failed = False
|
|
157
|
+
if SKIP_VISITORS:
|
|
158
|
+
print(" SKIPPED (--skip-visitors) — Cloudflare was never queried. The zeros below mean")
|
|
159
|
+
print(" 'not measured', not 'no traffic'.")
|
|
160
|
+
else:
|
|
161
|
+
slice_start = since
|
|
162
|
+
while slice_start < now:
|
|
163
|
+
slice_end = min(slice_start + timedelta(hours=24), now)
|
|
164
|
+
q = f'''query {{ viewer {{ zones(filter: {{zoneTag: "{CF_ZONE}"}}) {{
|
|
165
|
+
httpRequestsAdaptiveGroups(limit: 5000, filter: {{
|
|
166
|
+
datetime_geq: "{slice_start.strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
167
|
+
datetime_leq: "{slice_end.strftime('%Y-%m-%dT%H:%M:%SZ')}",
|
|
168
|
+
clientRequestPath_like: "{CF_PATH}"}}) {{
|
|
169
|
+
dimensions {{ clientIP }} sum {{ visits }} count }} }} }} }}'''
|
|
170
|
+
res = cf_graphql(q)
|
|
171
|
+
if res.get("errors") or res.get("_error"):
|
|
172
|
+
cf_failed = True
|
|
173
|
+
print(f" [{slice_start:%m-%d %H:%M}] cf error: {res.get('errors') or res.get('_error')}")
|
|
174
|
+
else:
|
|
175
|
+
rows = res["data"]["viewer"]["zones"][0]["httpRequestsAdaptiveGroups"]
|
|
176
|
+
ips = len(rows); v = sum(r["sum"]["visits"] for r in rows); c = sum(r["count"] for r in rows)
|
|
177
|
+
tot_ip += ips; tot_visits += v; tot_req += c
|
|
178
|
+
print(f" [{slice_start:%m-%d %H:%M}→{slice_end:%H:%M}] IPs={ips} visits={v} requests={c}")
|
|
179
|
+
slice_start = slice_end
|
|
180
|
+
print(f" TOTAL: IPs={tot_ip} (summed per-slice — cross-day overlap, not exact uniques)"
|
|
181
|
+
f" visits={tot_visits} requests={tot_req}")
|
|
182
|
+
# A slice that errored contributes 0, so a partly-failed run must not present
|
|
183
|
+
# its total as a measurement.
|
|
184
|
+
if cf_failed:
|
|
185
|
+
print(" ⚠ at least one slice errored — the total above is a FLOOR, not a count.")
|
|
132
186
|
|
|
133
187
|
# ---- 3. Clerk: sign-ups + baseline ---------------------------------------
|
|
134
188
|
print("\nSIGN-UPS (Clerk, platform-wide)")
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
doms
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
if SKIP_SIGNUPS:
|
|
190
|
+
print(" SKIPPED (--skip-signups) — Clerk was never queried; sign-ups are UNKNOWN, not 0.")
|
|
191
|
+
new = []
|
|
192
|
+
else:
|
|
193
|
+
total = clerk("/users/count").get("total_count", "?")
|
|
194
|
+
active = clerk(f"/users/count?last_active_at_since={since_ms}").get("total_count", "?")
|
|
195
|
+
# Page back until we pass the baseline window's start, else the newest 200 would
|
|
196
|
+
# silently cap `new`/`prior` during exactly the spike/long-window runs this targets.
|
|
197
|
+
base_start = since_ms - 14 * 86400000
|
|
198
|
+
users, offset, capped = [], 0, False
|
|
199
|
+
while True:
|
|
200
|
+
page = clerk(f"/users?order_by=-created_at&limit=500&offset={offset}")
|
|
201
|
+
if not isinstance(page, list) or not page:
|
|
202
|
+
break
|
|
203
|
+
users += page
|
|
204
|
+
if page[-1]["created_at"] < base_start: # covered the whole window+baseline
|
|
205
|
+
break
|
|
206
|
+
offset += 500
|
|
207
|
+
if offset >= 5000: # 10k-account safety stop
|
|
208
|
+
capped = True
|
|
209
|
+
break
|
|
210
|
+
new = [u for u in users if u["created_at"] >= since_ms]
|
|
211
|
+
prior = [u for u in users if base_start <= u["created_at"] < since_ms]
|
|
212
|
+
base_per = len(prior) / (14 / (HOURS / 24))
|
|
213
|
+
print(f" total accounts(cumulative)={total} active-in-window={active}")
|
|
214
|
+
cap_note = " ⚠ paging capped at 10k — new/baseline may undercount" if capped else ""
|
|
215
|
+
print(f" new sign-ups={len(new)} (prior baseline ≈{base_per:.1f} per {HOURS}h){cap_note}")
|
|
216
|
+
doms = {}
|
|
217
|
+
for u in new:
|
|
218
|
+
d = (u["email_addresses"][0]["email_address"].split("@")[-1]
|
|
219
|
+
if u.get("email_addresses") else "?")
|
|
220
|
+
doms[d] = doms.get(d, 0) + 1
|
|
221
|
+
if doms:
|
|
222
|
+
print(" domains: " + ", ".join(f"{k}={v}" for k, v in sorted(doms.items(), key=lambda x: -x[1])))
|
|
165
223
|
|
|
166
224
|
# funnel cross-ref: which new Clerk accounts wrote in the app
|
|
167
225
|
writers = {r["userId"] for r in db(f'''SELECT DISTINCT "userId" FROM "AppDocuments"
|
|
168
226
|
WHERE {APP_FILTER} AND created >= '{since_iso}' ''')}
|
|
169
227
|
new_ids = {u["id"] for u in new}
|
|
170
|
-
|
|
171
|
-
|
|
228
|
+
visits_txt = "UNKNOWN" if SKIP_VISITORS else (f"~{tot_visits}+" if cf_failed else f"~{tot_visits}")
|
|
229
|
+
signups_txt = "UNKNOWN" if SKIP_SIGNUPS else str(len(new))
|
|
230
|
+
# The cross-reference is derived from `new`, so skipping Clerk makes it
|
|
231
|
+
# unmeasured too — printing "0 of them brand-new" would be the same
|
|
232
|
+
# plausible-negative this whole change exists to remove.
|
|
233
|
+
new_txt = "UNKNOWN" if SKIP_SIGNUPS else str(len(writers & new_ids))
|
|
234
|
+
print(f"\nFUNNEL: {visits_txt} visits → {signups_txt} sign-ups → "
|
|
235
|
+
f"{len(writers)} active writers ({new_txt} of them brand-new accounts)")
|