@lensmcp/core 1.24.4 → 1.25.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/index.d.ts +3 -0
- package/index.js +1 -1
- package/lib/bus-rotation.d.ts +2 -0
- package/lib/bus-rotation.js +1 -1
- package/lib/event-redactor.js +1 -1
- package/lib/flow-shape.d.ts +54 -2
- package/lib/flow-shape.js +1 -1
- package/lib/http-exchange.d.ts +8 -0
- package/lib/http-exchange.js +1 -0
- package/lib/http-shape.d.ts +47 -0
- package/lib/http-shape.js +1 -0
- package/lib/lens-dir.d.ts +24 -0
- package/lib/lens-dir.js +1 -0
- package/lib/redact.d.ts +12 -0
- package/lib/redact.js +1 -1
- package/lib/route-template.d.ts +17 -0
- package/lib/route-template.js +1 -0
- package/lib/sql-shape.d.ts +45 -0
- package/lib/sql-shape.js +3 -0
- package/lib/stores.d.ts +82 -0
- package/lib/stores.js +2 -0
- package/package.json +22 -2
package/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export * from './lib/graph-store.js';
|
|
|
8
8
|
export * from './lib/fingerprint.js';
|
|
9
9
|
export * from './lib/reducer-registry.js';
|
|
10
10
|
export * from './lib/story.js';
|
|
11
|
+
export * from './lib/route-template.js';
|
|
12
|
+
export * from './lib/sql-shape.js';
|
|
13
|
+
export * from './lib/http-shape.js';
|
|
11
14
|
export * from './lib/flow-shape.js';
|
|
12
15
|
export * from './lib/flow-segments.js';
|
|
13
16
|
export * from './lib/flow-categories.js';
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/event-redactor.js";export*from"./lib/resource-store.js";export*from"./lib/answer-budget.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/flow-shape.js";export*from"./lib/flow-segments.js";export*from"./lib/flow-categories.js";export*from"./lib/headless-flow.js";export*from"./lib/flows-config.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/graph-from-events.js";
|
|
1
|
+
"use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/event-redactor.js";export*from"./lib/resource-store.js";export*from"./lib/answer-budget.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/route-template.js";export*from"./lib/sql-shape.js";export*from"./lib/http-shape.js";export*from"./lib/flow-shape.js";export*from"./lib/flow-segments.js";export*from"./lib/flow-categories.js";export*from"./lib/headless-flow.js";export*from"./lib/flows-config.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/graph-from-events.js";
|
package/lib/bus-rotation.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface BusRotationOptions extends BusCaps {
|
|
|
38
38
|
}
|
|
39
39
|
/** The environment {@link busCapsFromEnv} reads. */
|
|
40
40
|
export type BusEnv = Readonly<Record<string, string | undefined>>;
|
|
41
|
+
/** The most generations `LENSMCP_EVENT_MAX_FILES` may ask for — and how far a rotation sweeps for leftovers. */
|
|
42
|
+
export declare const MAX_BUS_GENERATIONS = 20;
|
|
41
43
|
/** A writer stats the bus at most ~once per this many bytes it appends itself. */
|
|
42
44
|
export declare const BUS_ROTATE_CHECK_BYTES: number;
|
|
43
45
|
/**
|
package/lib/bus-rotation.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var l=Object.defineProperty;var
|
|
1
|
+
"use strict";var l=Object.defineProperty;var i=(t,r)=>l(t,"name",{value:r,configurable:!0});var S=Object.defineProperty,u=i((t,r)=>S(t,"name",{value:r,configurable:!0}),"u");export const MAX_BUS_GENERATIONS=20,BUS_ROTATE_CHECK_BYTES=1048576;export function busCapsFromEnv(t=process.env){const r=Number(t.LENSMCP_EVENT_MAX_BYTES),e=Number(t.LENSMCP_EVENT_MAX_FILES);return{maxBytes:Number.isFinite(r)&&r>0?r:50*1024*1024,maxFiles:Number.isFinite(e)&&e>=1&&e<=20?Math.floor(e):5}}i(busCapsFromEnv,"busCapsFromEnv"),u(busCapsFromEnv,"busCapsFromEnv");export function rotateBusFile(t,r){const{fs:e,maxBytes:y,maxFiles:a}=r,n=`${t}.rotate.lock`,o=r.staleLockMs??3e4;let s;for(let c=0;c<2&&s===void 0;c++)try{s=e.openSync(n,"wx")}catch{try{if(Date.now()-e.statSync(n).mtimeMs<=o)return"busy";e.unlinkSync(n)}catch{}}if(s===void 0)return"busy";try{try{if(e.statSync(t).size<=y)return"not-needed"}catch{return"not-needed"}try{e.unlinkSync(`${t}.${a}`)}catch{}for(let c=a+1;c<=20;c++)try{e.unlinkSync(`${t}.${c}`)}catch{}for(let c=a-1;c>=1;c--)try{e.renameSync(`${t}.${c}`,`${t}.${c+1}`)}catch{}try{e.renameSync(t,`${t}.1`)}catch{}return"rotated"}finally{try{e.closeSync(s)}catch{}try{e.unlinkSync(n)}catch{}}}i(rotateBusFile,"rotateBusFile"),u(rotateBusFile,"rotateBusFile");export function createBusRotator(t,r){const{fs:e,maxBytes:y}=r,a=Math.max(0,Math.min(r.checkEveryBytes??1048576,y));let n=a,o=0;return s=>{if(n>=a||o+n>y)try{o=e.statSync(t).size,n=0,o>y&&rotateBusFile(t,r)==="rotated"&&(o=0)}catch{}n+=s}}i(createBusRotator,"createBusRotator"),u(createBusRotator,"createBusRotator");
|
package/lib/event-redactor.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var u=Object.defineProperty;var o=(r,t)=>u(r,"name",{value:t,configurable:!0});var l=Object.defineProperty,i=o((r,t)=>l(r,"name",{value:t,configurable:!0}),"a");import{MAX_MESSAGE_CHARS as S,MAX_TITLE_CHARS as T,redactDeep as y,redactSqlParams as _,redactText as p,safeText as E,safeTitle as g}from"./redact.js";export const BUS_REDACT_MAX_DEPTH=12;export function createEventRedactor(r={}){const t=r.maxDepth??BUS_REDACT_MAX_DEPTH;let s=0,a=0;return{redact(e){s+=1;const c=e.title===void 0?void 0:g(p(e.title)),n=e.message===void 0?void 0:E(p(e.message),S),d=e.raw===void 0?void 0:v(y(e.raw,0,t)),A=c!==e.title,f=n!==e.message,m=d!==e.raw;return!A&&!f&&!m?e:(a+=1,{...e,...c!==void 0?{title:c}:{},...n!==void 0?{message:n}:{},...d!==void 0?{raw:d}:{}})},stats:i(()=>({inspected:s,redacted:a}),"stats"),reset:i(()=>{s=0,a=0},"reset")}}o(createEventRedactor,"createEventRedactor"),i(createEventRedactor,"createEventRedactor");function v(r){if(!r||typeof r!="object"||Array.isArray(r))return r;const t=r,s=t.query,a=t.params;if(typeof s!="string"||!Array.isArray(a))return r;const e=_(s,a);return e===a?r:{...t,params:e}}o(v,"T"),i(v,"redactSqlShape");export{T as MAX_TITLE_CHARS};
|
package/lib/flow-shape.d.ts
CHANGED
|
@@ -19,6 +19,30 @@ export interface FlowShapeOptions {
|
|
|
19
19
|
*/
|
|
20
20
|
includeRaw?: boolean;
|
|
21
21
|
}
|
|
22
|
+
/** What an HTTP step was, as plain fields — see {@link httpFacts}. */
|
|
23
|
+
export interface HttpFacts {
|
|
24
|
+
method?: string;
|
|
25
|
+
/** The route template (`/companies/:id`): the framework's own when it says, else the path with its ids folded. */
|
|
26
|
+
route?: string;
|
|
27
|
+
/** The path as requested, without the query string. */
|
|
28
|
+
rawPath?: string;
|
|
29
|
+
status?: number;
|
|
30
|
+
host?: string;
|
|
31
|
+
/** On an east-west hop through the gateway: the service that made the call. */
|
|
32
|
+
caller?: string;
|
|
33
|
+
/** The response was streamed (an AI provider's SSE). Not `stream`: a log step's `stream` names stdout / stderr. */
|
|
34
|
+
streamed?: boolean;
|
|
35
|
+
/** A call out whose request and response the egress tap captured (`/api/exchange?id=`, R6). */
|
|
36
|
+
exchangeId?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Method, route, status, host and caller of every step that is an HTTP exchange: a browser fetch, a
|
|
40
|
+
* gateway hop, a service handling a request, a call out to another host, an AI provider call. Views
|
|
41
|
+
* group failures by cause and draw service → service arcs from these, instead of parsing titles.
|
|
42
|
+
*
|
|
43
|
+
* Paths never keep their query string, and pass the same credential redaction as every URL.
|
|
44
|
+
*/
|
|
45
|
+
export declare function httpFacts(kind: string | undefined, raw: Record<string, unknown>): HttpFacts;
|
|
22
46
|
export declare function trimFlowEvent(event: BaseEvent, opts?: FlowShapeOptions): {
|
|
23
47
|
steps?: string[] | undefined;
|
|
24
48
|
detail?: string | undefined;
|
|
@@ -26,6 +50,7 @@ export declare function trimFlowEvent(event: BaseEvent, opts?: FlowShapeOptions)
|
|
|
26
50
|
fetchStartedAt?: number | undefined;
|
|
27
51
|
durationMs?: number | undefined;
|
|
28
52
|
project?: string | undefined;
|
|
53
|
+
renderWalkStoppedAt?: number | undefined;
|
|
29
54
|
renderTotal?: number | undefined;
|
|
30
55
|
renderPublished?: number | undefined;
|
|
31
56
|
cleanup?: boolean | undefined;
|
|
@@ -44,13 +69,26 @@ export declare function trimFlowEvent(event: BaseEvent, opts?: FlowShapeOptions)
|
|
|
44
69
|
error?: string | undefined;
|
|
45
70
|
outcome?: "returned" | "threw" | undefined;
|
|
46
71
|
source: "nx" | "vite" | "typescript" | "eslint" | "rollup" | "client-runtime" | "chrome" | "react" | "valtio" | "nestjs" | "nextjs" | "db" | "redis" | "queue" | "memory" | "visual" | "perf" | "security" | "gateway" | "external" | "ai" | "lens";
|
|
47
|
-
category: "db" | "memory" | "visual" | "security" | "ai" | "build" | "lint" | "typecheck" | "test" | "runtime" | "render" | "trace" | "state" | "network" | "backend" | "performance" | "deps" | "cluster";
|
|
72
|
+
category: "db" | "memory" | "visual" | "security" | "ai" | "build" | "lint" | "typecheck" | "test" | "runtime" | "render" | "trace" | "state" | "network" | "backend" | "performance" | "deps" | "cluster" | "agent";
|
|
48
73
|
severity: "debug" | "info" | "warning" | "error" | "fatal";
|
|
49
74
|
title: string;
|
|
50
75
|
kind: string | undefined;
|
|
51
76
|
area: FlowArea;
|
|
52
77
|
jobId?: string | undefined;
|
|
53
78
|
queue?: string | undefined;
|
|
79
|
+
method?: string;
|
|
80
|
+
/** The route template (`/companies/:id`): the framework's own when it says, else the path with its ids folded. */
|
|
81
|
+
route?: string;
|
|
82
|
+
/** The path as requested, without the query string. */
|
|
83
|
+
rawPath?: string;
|
|
84
|
+
status?: number;
|
|
85
|
+
host?: string;
|
|
86
|
+
/** On an east-west hop through the gateway: the service that made the call. */
|
|
87
|
+
caller?: string;
|
|
88
|
+
/** The response was streamed (an AI provider's SSE). Not `stream`: a log step's `stream` names stdout / stderr. */
|
|
89
|
+
streamed?: boolean;
|
|
90
|
+
/** A call out whose request and response the egress tap captured (`/api/exchange?id=`, R6). */
|
|
91
|
+
exchangeId?: string;
|
|
54
92
|
urlKey?: string | undefined;
|
|
55
93
|
sig?: string | undefined;
|
|
56
94
|
id: string;
|
|
@@ -81,6 +119,7 @@ export declare function buildFlowDetail(rawEvents: BaseEvent[], opts?: FlowShape
|
|
|
81
119
|
fetchStartedAt?: number | undefined;
|
|
82
120
|
durationMs?: number | undefined;
|
|
83
121
|
project?: string | undefined;
|
|
122
|
+
renderWalkStoppedAt?: number | undefined;
|
|
84
123
|
renderTotal?: number | undefined;
|
|
85
124
|
renderPublished?: number | undefined;
|
|
86
125
|
cleanup?: boolean | undefined;
|
|
@@ -99,13 +138,26 @@ export declare function buildFlowDetail(rawEvents: BaseEvent[], opts?: FlowShape
|
|
|
99
138
|
error?: string | undefined;
|
|
100
139
|
outcome?: "returned" | "threw" | undefined;
|
|
101
140
|
source: "nx" | "vite" | "typescript" | "eslint" | "rollup" | "client-runtime" | "chrome" | "react" | "valtio" | "nestjs" | "nextjs" | "db" | "redis" | "queue" | "memory" | "visual" | "perf" | "security" | "gateway" | "external" | "ai" | "lens";
|
|
102
|
-
category: "db" | "memory" | "visual" | "security" | "ai" | "build" | "lint" | "typecheck" | "test" | "runtime" | "render" | "trace" | "state" | "network" | "backend" | "performance" | "deps" | "cluster";
|
|
141
|
+
category: "db" | "memory" | "visual" | "security" | "ai" | "build" | "lint" | "typecheck" | "test" | "runtime" | "render" | "trace" | "state" | "network" | "backend" | "performance" | "deps" | "cluster" | "agent";
|
|
103
142
|
severity: "debug" | "info" | "warning" | "error" | "fatal";
|
|
104
143
|
title: string;
|
|
105
144
|
kind: string | undefined;
|
|
106
145
|
area: FlowArea;
|
|
107
146
|
jobId?: string | undefined;
|
|
108
147
|
queue?: string | undefined;
|
|
148
|
+
method?: string;
|
|
149
|
+
/** The route template (`/companies/:id`): the framework's own when it says, else the path with its ids folded. */
|
|
150
|
+
route?: string;
|
|
151
|
+
/** The path as requested, without the query string. */
|
|
152
|
+
rawPath?: string;
|
|
153
|
+
status?: number;
|
|
154
|
+
host?: string;
|
|
155
|
+
/** On an east-west hop through the gateway: the service that made the call. */
|
|
156
|
+
caller?: string;
|
|
157
|
+
/** The response was streamed (an AI provider's SSE). Not `stream`: a log step's `stream` names stdout / stderr. */
|
|
158
|
+
streamed?: boolean;
|
|
159
|
+
/** A call out whose request and response the egress tap captured (`/api/exchange?id=`, R6). */
|
|
160
|
+
exchangeId?: string;
|
|
109
161
|
urlKey?: string | undefined;
|
|
110
162
|
sig?: string | undefined;
|
|
111
163
|
id: string;
|
package/lib/flow-shape.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var yt=Object.defineProperty;var R=(d,v)=>yt(d,"name",{value:v,configurable:!0});var kt=Object.defineProperty,y=R((d,v)=>kt(d,"name",{value:v,configurable:!0}),"$");import{redactDeep as Mt,redactText as rt}from"./redact.js";import{SQL_ACCOUNT_KIND as wt,sqlAccountingOf as vt}from"./flow-segments.js";import{boundedClone as qt}from"@lensmcp/protocol-types";export function areaOf(d){const{source:v,category:n,title:M,kind:m}=d;return v==="ai"?"ai":m?.startsWith("memory")||m==="db-query"?"data":v==="gateway"||m==="gateway-request"?"gateway":m==="queue-process"||m==="queue-active"||m==="queue-completed"||m==="queue-failed"?"services":v==="db"||v==="redis"||v==="queue"||m==="redis-op"||m?.startsWith("queue-")?"data":v==="external"||m==="egress"?"network":v==="nestjs"||v==="nest"?/(?:Repository|Repo|Dao|Store)\./.test(M)?"data":"services":v==="valtio"?"state":n==="network"?"network":"browser"}R(areaOf,"areaOf"),y(areaOf,"areaOf");const ct=240,It=new Set(["systemPrompt","prompt","prompts","messages","completion","completions","input","output","draft","text","content","result","arguments","params"]),xt=new Set(["tenantId","caller","userId","accountId","email","subject"]),bt=8;function lt(d){const v=tt(d,0);return qt(Mt({redacted:!0,...v}),{rawByteLimit:16*1024}).value}R(lt,"It"),y(lt,"aiProjection");function tt(d,v){if(typeof d=="string")return ft(d);if(d===null||typeof d!="object")return d;if(v>=bt)return;if(Array.isArray(d))return d.slice(0,32).map(M=>tt(M,v+1));const n={};for(const[M,m]of Object.entries(d))if(!xt.has(M)){if(It.has(M)){typeof m=="string"?n[`${M}Chars`]=m.length:Array.isArray(m)?n[`${M}Count`]=m.length:m!=null&&(n[`${M}Present`]=!0);continue}n[M]=tt(m,v+1)}return n}R(tt,"ot"),y(tt,"projectAi");function ft(d){return d.length<=ct?rt(d):`${rt(d.slice(0,ct))}\u2026`}R(ft,"qt"),y(ft,"clipText");export function trimFlowEvent(d,v={}){const n=d.raw??{},M=n.render??{},m=n.span??{},C=n.request??{},p=y(a=>typeof a=="number"?a:void 0,"num"),O=n.loop??{},b=n.effect??{},T=p(n.durationMs)??p(m.durationMs)??p(C.durationMs)??p(O.durationMs)??p(M.actualDurationMs)??p(b.durationMs),g=typeof n.kind=="string"?n.kind:void 0,F=g==="fetch-done"||g==="fetch-error",X=p(M.commitTime),et=g==="render"&&X!==void 0&&Math.abs(X-d.timestamp)<6e4,P=F?d.timestamp:et?X:p(n.startedAt)??p(m.startTime)??p(C.startedAt)??p(O.startedAt)??(T!==void 0?d.timestamp-T:d.timestamp);let I;if(typeof n.query=="string"){const a=Array.isArray(n.params)?` [${n.params.join(", ")}]`:"",h=typeof n.result=="string"?` \u2192 ${n.result}`:"";I=n.query+a+h}else if(typeof n.command=="string"){const a=typeof n.value=="string"?` \u2192 ${n.value}`:n.hit===!1?" \u2192 (miss)":"";I=n.command+a}else if(g==="state-update"){const a=n.update??{};let h="";try{h=JSON.stringify(a.preview)}catch{}I=`${a.storeId??""}.${a.path??""} = ${h}`}else if(g==="queue-completed"&&typeof n.returned=="string")I=`returned ${n.returned}`;else if(g==="queue-enqueued")I=`${n.job??"job"}#${n.jobId??"?"} \u2192 ${n.queue??""}${typeof n.data=="string"?" "+n.data:""}`;else if(g==="log"){const a=C.error??{},h=typeof a.stack=="string"?a.stack:typeof n.text=="string"?n.text:void 0;h&&h!==d.title&&(I=h)}let N,U,L,E;if(g==="span"){const a=m.outcome;if(N=a==="threw"||a==="returned"?a:m.status==="error"?"threw":m.status==="ok"?"returned":void 0,N==="threw"){const A=m.error??{},S=typeof A.name=="string"?A.name:void 0,V=typeof A.message=="string"?A.message:typeof d.message=="string"?d.message:void 0;L=S,U=rt([S,V].filter(Boolean).join(": "))||void 0}const h=typeof m.args=="string"?m.args:void 0,B=typeof m.result=="string"?m.result:void 0;if(h!==void 0||B!==void 0){E=[...h!==void 0?["args"]:[],...B!==void 0?["result"]:[]];const A=y(S=>S.length>pt?`${S.slice(0,pt)}\u2026`:S,"short");I=[h!==void 0?`args ${A(h)}`:"",B!==void 0?`returned ${A(B)}`:""].filter(Boolean).join(" \u2192 ")}}let K;if(g==="gateway-request")K=`${n.host??""}${String(n.url??"").split("?")[0]}`;else if(g==="fetch-start"||g==="fetch-done"||g==="fetch-error")try{const a=new URL(String(n.href??n.url??""));K=`${a.hostname}${a.pathname}`}catch{}let _;if(g==="gateway-request"&&Array.isArray(n.steps))_=n.steps.map(a=>{const h=[String(a.step)];return a.project&&h.push(String(a.project)),a.prefix&&h.push(String(a.prefix)),a.mode&&h.push(String(a.mode)+(a.ok===!1?" \u2717":a.ok===!0?" \u2713":"")),a.caller&&h.push("by "+String(a.caller)),a.pod&&h.push(String(a.pod)),typeof a.atMs=="number"&&h.push(`@${a.atMs}ms`),h.join(" ")}),I=_.map(a=>a.replace(/ @\d+ms$/,"")).join(" \u2192 ");else if(g==="egress")I=`${n.method??"GET"} ${n.host??""} \u2192 ${n.status??"?"}`;else if(d.source==="ai"){if(g==="ai-call"){const a=n.usage??{},h=a.promptTokens!==void 0||a.completionTokens!==void 0?` \xB7 ${p(a.promptTokens)??"?"}\u2192${p(a.completionTokens)??"?"} tok`:"",B=n.status!==void 0?` \xB7 ${String(n.status)}`:n.error?" \xB7 failed":"",A=p(n.toolCallCount)?` \xB7 ${p(n.toolCallCount)} tool call${p(n.toolCallCount)===1?"":"s"}`:"",S=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${n.provider??""}/${n.model??"?"}${h}${B}${A}${S}${n.stream===!0?" \xB7 stream":""}`}else if(g==="ai-round"){const a=n.model??{},h=n.usage??{},B=Array.isArray(n.toolCalls)?n.toolCalls:[],A=`${p(h.promptTokens)??"?"}\u2192${p(h.completionTokens)??"?"} tok`,S=B.length?` \xB7 ${B.length} tool call${B.length===1?"":"s"}`:"",V=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${a.provider??""}/${a.model??""} \xB7 ${A}${S}${V}`}else if(g==="ai-turn-start"){const a=Array.isArray(n.tools)?n.tools:[];I=`skill ${n.skill??""} \xB7 ${n.output??""}${a.length?" \xB7 tools: "+a.join(", "):""}`}else if(g==="ai-tool-call"){const a=n.call??{},h=n.result??{},B=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${a.name??"tool"} ${h.error!=null?"\u2717 "+String(h.error):"\u2713"}${B}`}else if(g==="ai-turn-end"){const a=n.usage??{},h=p(n.rounds)!==void 0?` \xB7 ${p(n.rounds)} round${p(n.rounds)===1?"":"s"}`:"",B=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${n.ok===!1?"FAILED":"ok"} \xB7 ${p(a.promptTokens)??"?"}+${p(a.completionTokens)??"?"} tok${h}${B}`}}let f;g==="db-query"&&typeof n.query=="string"?f=n.query:g==="redis-op"?f=`${n.op??""} ${String(n.key??"").replace(/\d+/g,"#")}`:g==="egress"?f=`${n.method??"GET"} ${n.host??""}`:g==="ai-call"?f=`ai ${n.provider??""} ${n.model??""} ${n.api??""}`:g==="queue-enqueued"?f=`enqueue ${n.queue??""}`:g==="span"&&typeof m.name=="string"?f=m.name:g==="memory-mutation"&&(f=d.title);const D=d.context??{},W=M.commitId??b.commitId??n.commitId,G=Array.isArray(M.why)?M.why.map(a=>typeof a?.type=="string"?a.type:void 0).filter(a=>!!a):[];return{id:d.id,t:d.timestamp,start:P,...f?{sig:f}:{},...K?{urlKey:K}:{},...typeof n.queue=="string"?{queue:n.queue}:{},...n.jobId!=null?{jobId:String(n.jobId)}:{},source:d.source,category:d.category,severity:d.severity,title:N==="threw"&&typeof m.name=="string"?`${m.name} \u2014 threw${L?` ${L}`:""}${T!==void 0?` after ${T}ms`:""}`:d.title,kind:g,area:areaOf({source:d.source,category:d.category,title:d.title,kind:g}),...N?{outcome:N}:{},...U?{error:U}:{},...E?{captured:E}:{},...typeof m.role=="string"?{role:m.role}:{},...g==="log"&&typeof n.level=="string"?{level:n.level}:{},...g==="log"&&typeof n.logger=="string"?{logger:n.logger}:{},...g==="log"&&typeof n.stream=="string"?{stream:n.stream}:{},...typeof D.requestId=="string"?{requestId:D.requestId}:{},...typeof D.parentRequestId=="string"?{parentRequestId:D.parentRequestId}:{},...typeof W=="string"?{commitId:W}:{},...typeof M.componentName=="string"?{component:M.componentName}:{},...typeof M.phase=="string"?{phase:M.phase}:{},...G.length?{why:G}:{},...g==="effect-run"&&typeof b.hookLogicalId=="string"?{hook:b.hookLogicalId}:{},...g==="effect-run"&&b.cleanupRan===!0?{cleanup:!0}:{},...g==="render-commit-truncated"?{renderTotal:p(n.total),renderPublished:p(n.published)}:{},...typeof D.projectName=="string"?{project:D.projectName}:typeof n.project=="string"?{project:n.project}:{},...T!==void 0?{durationMs:T}:{},...F&&p(n.startedAt)!==void 0?{fetchStartedAt:p(n.startedAt)}:{},...d.source==="ai"?{ai:v.includeRaw?n:lt(n)}:{},...I?{detail:I}:{},..._?{steps:_}:{}}}R(trimFlowEvent,"trimFlowEvent"),y(trimFlowEvent,"trimFlowEvent");const pt=300;export const OUTLIVED_PARENT_MIN_MS=250;const jt=new Set(["egress","db-query","redis-op","ai-call","span","gateway-request","server-request"]),mt=1;function gt(d){return d==="flow-origin"?0:d?.endsWith("-start")?1:d==="gateway-request"?2:d==="server-request"?3:d==="span"?4:d==="render"||d==="render-commit-truncated"?6:d==="effect-run"?7:5}R(gt,"jt"),y(gt,"kindRank");function J(d){return d.kind==="span"&&d.role==="controller"?3.5:d.kind==="queue-process"?3:gt(d.kind)}R(J,"Z"),y(J,"causalRank");export function buildFlowDetail(d,v={}){const n=vt(d),M=d.slice().filter(t=>{const e=t.raw??{},r=typeof e.kind=="string"?e.kind:"";if(r==="subscription-added"||r==="subscription-removed"||r==="hook-run"||r==="store-created"||r==="singleton-instance"||r===wt)return!1;const o=e.span;return!(typeof o?.name=="string"&&o.name.startsWith("nest.request "))}).map(t=>trimFlowEvent(t,v)),m=new Set;for(let t=M.length-1;t>=0;t--)m.has(M[t].id)?M.splice(t,1):m.add(M[t].id);const C=new Map;for(const t of M)(t.kind==="gateway-request"||t.kind==="queue-process")&&t.requestId&&!C.has(t.requestId)&&C.set(t.requestId,t.start);const p=y(t=>t.source!=="react"&&t.source!=="valtio"&&t.requestId&&C.has(t.requestId)?C.get(t.requestId):t.start,"anchorOf");M.sort((t,e)=>p(t)-p(e)||t.start-e.start||J(t)-J(e)||(e.durationMs??0)-(t.durationMs??0)||t.t-e.t);const O=y(t=>(t.kind==="span"||t.kind==="server-request"||t.kind==="gateway-request"||t.kind==="queue-process")&&(t.durationMs??0)>0,"spanLike"),b=M;for(const t of b)t.group=t.source==="react"||t.source==="valtio"||t.source==="client-runtime"||t.source==="visual"?"browser":"server";const T=new Map;for(const t of b)if((t.kind==="gateway-request"||t.kind==="queue-process")&&t.requestId&&t.project){const e=T.get(t.requestId)??[];e.push({project:t.project,start:t.start,end:t.start+(t.durationMs??0)}),T.set(t.requestId,e)}const g=y((t,e)=>{const r=T.get(t);if(r?.length)return r.every(o=>o.project===r[0].project)?r[0].project:r.filter(o=>o.start<=e+1&&o.end>=e-1).sort((o,s)=>s.start-o.start)[0]?.project},"projectOf");for(const t of b)if(!t.project&&t.source==="nestjs"&&t.requestId){const e=g(t.requestId,t.start);e&&(t.project=e)}for(const t of b){let e;const r=t.requestId;for(const o of b){if(o===t||!O(o)||o.group!==t.group||o.requestId!==r)continue;const s=o.project,c=t.project;if(o.kind!=="gateway-request"&&t.kind!=="gateway-request"&&s!==void 0&&c!==void 0&&s!==c)continue;const $=o.durationMs??0,k=t.durationMs??0,i=Math.abs(o.start-t.start)<=mt,u=J(o),l=J(t),w=o.start<=t.start;if(!(o.start+$>t.start&&(w||i&&u<l)&&(!i||(u!==l?u<l:w&&$>k))))continue;const j=e?J(e):-1,q=e?Math.abs(o.start-e.start)<=mt:!1;(!e||!q&&o.start>e.start||q&&u>j||q&&u===j&&(o.durationMs??0)<(e.durationMs??0))&&(e=o)}e&&(t.parent=e.id)}const F=new Map;for(const t of b){if(!t.requestId)continue;const e=F.get(t.requestId);e?e.push(t):F.set(t.requestId,[t])}const X=new Map(b.map(t=>[t.id,t])),et=y((t,e)=>{const r=(F.get(t)??[]).filter(o=>O(o)&&o.start<=e&&o.start+(o.durationMs??0)>=e);if(r.length!==0)return r.sort((o,s)=>s.start-o.start||(o.durationMs??0)-(s.durationMs??0)),r[0]},"callerNodeFor");for(const t of b){if(!t.parentRequestId||t.kind!=="server-request"&&t.kind!=="gateway-request")continue;const e=t.parent?X.get(t.parent):void 0;if(e&&e.requestId===t.requestId)continue;const r=et(t.parentRequestId,t.start);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}const P=b,I=new Map;for(const t of P)t.kind==="gateway-request"&&t.requestId&&!I.has(t.requestId)&&I.set(t.requestId,t);for(const t of P)if(t.group==="server"&&!t.parent&&t.requestId&&t.kind!=="gateway-request"){const e=I.get(t.requestId);e&&e!==t&&e.start<=t.start&&(t.parent=e.id)}const N=new Set,U=new Set,L=[];for(const t of P){if(t.kind!=="gateway-request")continue;if(!t.requestId){L.push(t);continue}const e=F.get(t.requestId)??[];if(e.filter(s=>s.kind==="gateway-request").sort((s,c)=>s.start-c.start)[0]!==t)continue;const r=e.find(s=>s.kind==="fetch-start"),o=e.find(s=>s.kind==="fetch-done"||s.kind==="fetch-error");if(!r&&!o){L.push(t);continue}r&&(t.fetchStartId=r.id,N.add(r.id)),o&&(t.fetchDoneId=o.id,U.add(o.id))}const E=new Map;for(const t of P){if(!t.urlKey)continue;const e=E.get(t.urlKey)??{gws:[],starts:[],dones:[]};t.kind==="gateway-request"?L.includes(t)&&e.gws.push(t):t.kind==="fetch-start"?N.has(t.id)||e.starts.push(t):(t.kind==="fetch-done"||t.kind==="fetch-error")&&(U.has(t.id)||e.dones.push(t)),E.set(t.urlKey,e)}for(const{gws:t,starts:e,dones:r}of E.values()){t.sort((s,c)=>s.start-c.start),e.sort((s,c)=>s.start-c.start);const o=new Set;t.forEach((s,c)=>{const $=e[c];$&&(s.fetchStartId=$.id);const k=$?$.t:s.start;let i,u=1/0;for(const l of r){if(o.has(l.id))continue;const w=l.fetchStartedAt??l.start-(l.durationMs??0),j=Math.abs(w-k);j<u&&(u=j,i=l)}i&&(s.fetchDoneId=i.id,o.add(i.id))})}const K=new Map,_=new Map;for(const t of P)!t.queue||!t.jobId||(t.kind==="queue-enqueued"&&K.set(`${t.queue}#${t.jobId}`,t),t.kind==="queue-process"&&_.set(`${t.queue}#${t.jobId}`,t));for(const t of P){if(!t.queue||!t.jobId)continue;const e=`${t.queue}#${t.jobId}`;if(t.kind==="queue-process"&&!t.parent){const r=K.get(e);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}else if(t.kind==="queue-active"||t.kind==="queue-completed"||t.kind==="queue-failed"){const r=_.get(e);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}}{const t=new Map(b.map(e=>[e.id,e]));for(const e of b){if(!e.parent||!jt.has(e.kind??"")||e.durationMs===void 0)continue;const r=t.get(e.parent);if(!r||r.kind!=="span"||!((r.durationMs??0)>0))continue;const o=Math.round(e.start+e.durationMs-(r.start+r.durationMs));if(o<OUTLIVED_PARENT_MIN_MS)continue;e.outlivedParentMs=o;const s=r.leftRunning??{count:0,longestMs:0};r.leftRunning={count:s.count+1,longestMs:Math.max(s.longestMs,o)}}for(const e of b){const r=[];e.leftRunning&&r.push(`ended before ${e.leftRunning.count===1?"a call":`${e.leftRunning.count} calls`} it started had finished \u2014 ${e.leftRunning.count===1?"it":"the longest"} ran on ${e.leftRunning.longestMs}ms (a timeout that gave up, or work not awaited)`),e.outlivedParentMs!==void 0&&r.push(`ran on ${e.outlivedParentMs}ms after the span that started it had ended`),r.length>0&&(e.detail=[e.detail,...r].filter(Boolean).join(" \xB7 "))}}let f=b;{const t=new Map;for(const r of f)r.kind==="fetch-start"&&t.set(r.id,r);const e=new Map;for(const r of f){if(r.kind!=="fetch-done"&&r.kind!=="fetch-error")continue;const o=[...t.values()].find($=>$.requestId&&$.requestId===r.requestId)?.id,s=o?t.get(o):void 0;if(!s)continue;const c=r.fetchStartedAt??s.start;s.title=r.title,s.severity=r.severity,s.durationMs=r.durationMs??Math.max(0,r.t-c),s.kind="fetch",r.detail&&!s.detail&&(s.detail=r.detail),e.set(r.id,s.id)}if(e.size>0){f=f.filter(r=>!e.has(r.id));for(const r of f)for(r.fetchDoneId&&e.has(r.fetchDoneId)&&(r.fetchDoneId=e.get(r.fetchDoneId));r.parent&&e.has(r.parent);)r.parent=e.get(r.parent)}}const D=y(t=>!!t.commitId&&(t.kind==="render"||t.kind==="effect-run"||t.kind==="render-commit-truncated"),"commitMember"),W=y(t=>[...new Set(t)],"uniq"),G=y(t=>W(t.map(e=>e.component).filter(e=>!!e)),"named"),a=y(t=>t.length<=2?t.join(" + "):`${t[0]} + ${t.length-1} more`,"nameList"),h=y(t=>{const e=new Map;for(const r of t)r.component&&e.set(r.component,(e.get(r.component)??0)+1);return[...e].map(([r,o])=>o>1?`${r} \xD7${o}`:r).join(", ")},"tally"),B=y(t=>{if(!t)return"effect";const e=/([^/\\]+?):(\d+)(?::\d+)?$/.exec(t);return e?`${e[1]}:${e[2]}`:t},"shortHook"),A=y(t=>`${B(t.hook)}${t.cleanup?" (cleanup)":""}`,"hookLabel"),S=y(t=>{const e=Math.min(...t.map(o=>o.start)),r=Math.max(...t.map(o=>o.start+(o.durationMs??0)));return{start:e,durationMs:r-e}},"extent"),V=y(t=>(delete t.component,delete t.why,delete t.phase,delete t.hook,delete t.cleanup,delete t.renderTotal,delete t.renderPublished,t),"asGroup");function ot(t){const e=t[0],r=t.filter(x=>x.kind==="render"),o=t.filter(x=>x.kind==="effect-run"),s=t.find(x=>x.kind==="render-commit-truncated"),c=[...r].sort((x,Y)=>(Y.durationMs??0)-(x.durationMs??0)),$=c.filter(x=>(x.why??[]).some(Y=>Y!=="parent-render")),k=W($.flatMap(x=>(x.why??[]).filter(Y=>Y!=="parent-render"))),i=G($),u=r.filter(x=>x.phase==="mount"),l=r.filter(x=>x.phase!=="mount"&&!$.includes(x)),w=c[0]?.component??"view",j=r.length>0&&u.length===r.length,q=k.length?k.join(" + "):void 0,z=j?"mount":"update",H=i.length?`view update \xB7 ${a(i)} \u2190 ${q}`:j?`view mount \xB7 ${w}`:`view update \xB7 ${w}`,dt=s?.renderTotal??r.length,$t=[u.length?`mounted ${h(u)}`:"",l.length?`re-rendered ${h(l)}`:"",!i.length&&!j&&r.length?"trigger not recorded \u2014 every captured render came from its parent":"",s?`${dt} renders, ${s.renderPublished??r.length} recorded (per-commit cap)`:`${r.length} render${r.length===1?"":"s"}`,o.length?`${o.length} effect${o.length===1?"":"s"}: ${o.map(A).join(", ")}`:""].filter(Boolean).join(" \xB7 "),ut=G(r);return V({...e,...S(t),kind:"view-update",title:H,detail:$t,commitId:e.commitId,viewPhase:z,...q?{cause:q}:{},causeComponents:i,components:ut,mounted:G(u),renderCount:dt,recordedRenders:r.length,effects:o.length,effectHooks:o.map(A),memberIds:t.map(x=>x.id),sig:`commit:${z}:${q??"-"}:${i.join(",")}:${ut.slice().sort().join(",")}`,severity:t.some(x=>x.severity==="error")?"error":e.severity})}R(ot,"ut"),y(ot,"viewUpdateNode");function nt(t){const e=t[0],r=t.map(A);return V({...e,...S(t),kind:"commit-effects",title:t.length===1?`effect \xB7 ${r[0]}`:`effects \xB7 ${r[0]} + ${t.length-1} more`,detail:r.join(", "),commitId:e.commitId,effects:t.length,effectHooks:r,memberIds:t.map(o=>o.id),sig:`effects:${r.slice().sort().join(",")}`})}R(nt,"ft"),y(nt,"effectsNode");const ht=y(t=>W(t.map(e=>e==="state-update"?"state update":e==="fetch-start"?"request":e==="flow-origin"?"interaction":"response")).join(" + "),"describeInputs");function st(t,e){const r=t[0],o=t[t.length-1],s=t.length,c=r.detail,$=Math.min(...t.map(l=>l.start)),k=Math.max(...t.map(l=>l.start+(l.durationMs??0))),i=Math.round(t.reduce((l,w)=>l+(w.durationMs??0),0)/s*10)/10,u=Math.round((k-$)*10)/10;r.count=s,r.avgMs=i,r.wallMs=u,r.durationMs=k-$,r.items=t.slice(0,12).map(l=>({start:l.start-$,...l.durationMs!=null?{durationMs:l.durationMs}:{}})),r.memberIds=t.flatMap(l=>l.memberIds??[l.id]),r.title=`\xD7${s} ${r.title}`,t.some(l=>l.severity==="error")&&(r.severity="error"),r.kind==="view-update"?r.detail=[`${s} commits, ${e.length?`one per ${ht(e)}`:"back to back"}`,`avg ${i}ms \xB7 wall ${u}ms`,c??""].filter(Boolean).join(" \xB7 "):r.kind==="state-update"&&c&&o.detail?r.detail=`${s} updates in a row \xB7 wall ${u}ms \xB7 first ${c} \xB7 last ${o.detail}`:r.detail=[`${s} in a row \xB7 wall ${u}ms`,c??""].filter(Boolean).join(" \xB7 ")}R(st,"pt"),y(st,"collapseRepeat");const Q=[],Z=new Map,at=new Set;for(let t=0;t<f.length;t++){if(at.has(t))continue;const e=f[t];if(!D(e)){Q.push(e);continue}const r=[t];for(let c=t+1;c<f.length;c++){const $=f[c];if($.group==="browser"){if(!D($)||$.commitId!==e.commitId)break;r.push(c)}}for(const c of r)at.add(c);const o=r.map(c=>f[c]),s=o.some(c=>c.kind!=="effect-run")?ot(o):nt(o);for(const c of o)c.id!==s.id&&Z.set(c.id,s.id);Q.push(s)}if(Z.size>0||Q.some(t=>t.kind==="view-update"||t.kind==="commit-effects")){f=Q;for(const t of f)for(;t.parent&&Z.has(t.parent);)t.parent=Z.get(t.parent)}{const t=new Map;for(const e of f)e.kind==="view-update"&&e.commitId&&!t.has(e.commitId)&&t.set(e.commitId,e);for(const e of f){if(e.kind!=="commit-effects"||e.parent||!e.commitId)continue;const r=t.get(e.commitId);r&&r.start<=e.start&&(e.parent=r.id)}}{const t=new Set(["state-update","fetch","fetch-start","fetch-done","fetch-error","flow-origin"]),e=f.map((i,u)=>i.group==="browser"?u:-1).filter(i=>i>=0),r=y(i=>`${i.kind}|${i.sig??i.title}`,"keyOf"),o=e.map(i=>r(f[i])),s=y((i,u,l)=>{for(let w=0;w<l;w++)if(o[i+w]!==o[u+w])return!1;return!0},"sameBlock"),c=y(i=>o[i].split("|")[0],"kindAt"),$=y(i=>{let u=0,l=1;for(let w=1;w<=4&&i+w<=o.length;w++){let j=1;for(;i+(j+1)*w<=o.length&&s(i,i+j*w,w);)j+=1;j>=3&&w*j>u*l&&(u=w,l=j)}return{k:u,r:l}},"bestBlockAt"),k=new Map;for(let i=0;i<o.length;){const{k:u,r:l}=$(i);if(u===0){i+=1;continue}const w=[...Array(u).keys()].find(q=>t.has(c(i+q)));if(w!==void 0&&w>0){const q=$(i+w);if(q.k===u&&q.r>=3&&q.r>=l-1){i+=w;continue}}const j=o.slice(i,i+u).map(q=>q.split("|")[0]).filter(q=>t.has(q));for(let q=0;q<u;q++){const z=[];for(let H=0;H<l;H++)z.push(f[e[i+q+H*u]]);st(z,j);for(const H of z.slice(1))k.set(H.id,z[0].id)}i+=u*l}if(k.size>0){f=f.filter(i=>!k.has(i.id));for(const i of f)for(;i.parent&&k.has(i.parent);)i.parent=k.get(i.parent)}}{const t=new Map(f.map(e=>[e.id,e]));for(const e of f){const r=new Set([e.id]);let o=e;for(;o?.parent;){if(r.has(o.parent)){o.parent=void 0;break}r.add(o.parent),o=t.get(o.parent)}}}const it=y(t=>{const e=new Map;for(const o of f){if(!o.sig||O(o)!==t||o.kind==="server-request"||o.kind==="gateway-request"||o.group==="browser")continue;const s=`${o.parent??"root"}|${o.kind}|${o.sig}`,c=e.get(s);c?c.push(o):e.set(s,[o])}const r=new Map;for(const o of e.values()){if(o.length<3)continue;o.sort((u,l)=>u.start-l.start);const s=o[0];let c=!1;for(let u=0;u+1<o.length;u++)if(o[u+1].start<o[u].start+(o[u].durationMs??0)){c=!0;break}const $=s.start,k=Math.max(...o.map(u=>u.start+(u.durationMs??0))),i=o.reduce((u,l)=>u+(l.durationMs??0),0);s.count=o.length,s.parallel=c,s.avgMs=Math.round(i/o.length*10)/10,s.wallMs=k-$,s.durationMs=k-$,s.title=`\xD7${o.length} ${s.title.replace(/\s*\((?:\d+ rows?, )?\d+ms\)$/,"")}`,s.detail=`${s.sig} \xD7${o.length} ${c?"\u2016 parallel":"\u21BB sequential loop"} \xB7 avg ${s.avgMs}ms \xB7 wall ${s.wallMs}ms`,s.items=o.slice(0,12).map(u=>({start:u.start-$,...u.durationMs!=null?{durationMs:u.durationMs}:{}})),s.memberIds=o.flatMap(u=>u.memberIds??[u.id]),o.some(u=>u.severity==="error")&&(s.severity="error");for(const u of o.slice(1))r.set(u.id,s.id)}if(r.size!==0){f=f.filter(o=>!r.has(o.id));for(const o of f)for(;o.parent&&r.has(o.parent);)o.parent=r.get(o.parent)}},"aggregatePass");it(!0),it(!1);for(const t of f){if(!O(t))continue;const e=t.durationMs??0;if(e<40)continue;const r=f.filter(k=>k.parent===t.id&&(k.durationMs??0)>0).map(k=>[k.start,k.start+(k.durationMs??0)]).sort((k,i)=>k[0]-i[0]);let o=0,s=-1,c=-1;for(const[k,i]of r)k>c?(c>s&&(o+=c-s),s=k,c=i):i>c&&(c=i);c>s&&(o+=c-s);const $=Math.max(0,e-o);($>=50||$/e>=.7&&e>=40)&&(t.selfMs=$,t.detail=`\u26A0 cpu-heavy: ${$}ms self-time of ${e}ms${t.detail?" \xB7 "+t.detail:""}`)}return{events:f,...n?{sql:n}:{}}}R(buildFlowDetail,"buildFlowDetail"),y(buildFlowDetail,"buildFlowDetail");
|
|
1
|
+
"use strict";var kt=Object.defineProperty;var T=(a,p)=>kt(a,"name",{value:p,configurable:!0});var vt=Object.defineProperty,$=T((a,p)=>vt(a,"name",{value:p,configurable:!0}),"g");import{redactDeep as wt,redactText as ot,redactUrlParams as qt}from"./redact.js";import{templatisePath as ct}from"./route-template.js";import{SQL_ACCOUNT_KIND as Mt,sqlAccountingOf as It}from"./flow-segments.js";import{boundedClone as xt}from"@lensmcp/protocol-types";export function areaOf(a){const{source:p,category:o,title:v,kind:m}=a;return p==="ai"?"ai":m?.startsWith("memory")||m==="db-query"?"data":p==="gateway"||m==="gateway-request"?"gateway":m==="queue-process"||m==="queue-active"||m==="queue-completed"||m==="queue-failed"?"services":p==="db"||p==="redis"||p==="queue"||m==="redis-op"||m?.startsWith("queue-")?"data":p==="external"||m==="egress"?"network":p==="nestjs"||p==="nest"?/(?:Repository|Repo|Dao|Store)\./.test(v)?"data":"services":p==="valtio"?"state":o==="network"?"network":"browser"}T(areaOf,"areaOf"),$(areaOf,"areaOf");const lt=240,bt=new Set(["systemPrompt","prompt","prompts","messages","completion","completions","input","output","draft","text","content","result","arguments","params"]),jt=new Set(["tenantId","caller","userId","accountId","email","subject"]),Bt=8;function ft(a){const p=tt(a,0);return xt(wt({redacted:!0,...p}),{rawByteLimit:16*1024}).value}T(ft,"bt"),$(ft,"aiProjection");function tt(a,p){if(typeof a=="string")return pt(a);if(a===null||typeof a!="object")return a;if(p>=Bt)return;if(Array.isArray(a))return a.slice(0,32).map(v=>tt(v,p+1));const o={};for(const[v,m]of Object.entries(a))if(!jt.has(v)){if(bt.has(v)){typeof m=="string"?o[`${v}Chars`]=m.length:Array.isArray(m)?o[`${v}Count`]=m.length:m!=null&&(o[`${v}Present`]=!0);continue}o[v]=tt(m,p+1)}return o}T(tt,"ot"),$(tt,"projectAi");function pt(a){return a.length<=lt?ot(a):`${ot(a.slice(0,lt))}\u2026`}T(pt,"jt"),$(pt,"clipText");export function httpFacts(a,p){const o=$(q=>typeof q=="string"&&q.length>0?q:void 0,"str"),v=typeof p.status=="number"?p.status:void 0,m=$(q=>qt(q.split("?")[0]??q)||"/","pathOf"),D=$((q,w,b={})=>({...q?{method:q.toUpperCase()}:{},...w?{rawPath:w,route:ct(w)}:{},...v!==void 0?{status:v}:{},...b}),"facts");if(a==="gateway-request"){const q=o(p.url);return D(o(p.method),q?m(q):void 0,{...o(p.host)?{host:o(p.host)}:{},...o(p.caller)?{caller:o(p.caller)}:{}})}if(a==="egress"){const q=o(p.path),w=o(p.exchangeId);return D(o(p.method)??"GET",q?m(q):void 0,{...o(p.host)?{host:o(p.host)}:{},...w?{exchangeId:w}:{}})}if(a==="fetch-start"||a==="fetch-done"||a==="fetch-error"){const q=o(p.href)??o(p.url);let w,b;if(q)try{const f=new URL(q,"http://relative.invalid");w=m(f.pathname),f.hostname!=="relative.invalid"&&(b=f.hostname)}catch{}return D(o(p.method)??"GET",w,b?{host:b}:{})}if(a==="ai-call")return{...v!==void 0?{status:v}:{},...p.stream===!0?{streamed:!0}:{}};const l=p.request;if(l&&typeof l=="object"){const q=l,w=o(q.route),b=o(q.method);if(!w&&!b)return{};const f=typeof q.status=="number"?q.status:void 0,C=w?m(w):void 0;return{...b?{method:b.toUpperCase()}:{},...C?{rawPath:C,route:C.includes(":")?C:ct(C)}:{},...f!==void 0?{status:f}:{}}}return{}}T(httpFacts,"httpFacts"),$(httpFacts,"httpFacts");export function trimFlowEvent(a,p={}){const o=a.raw??{},v=o.render??{},m=o.span??{},D=o.request??{},l=$(i=>typeof i=="number"?i:void 0,"num"),q=o.loop??{},w=o.effect??{},b=l(o.durationMs)??l(m.durationMs)??l(D.durationMs)??l(q.durationMs)??l(v.actualDurationMs)??l(w.durationMs),f=typeof o.kind=="string"?o.kind:void 0,C=f==="fetch-done"||f==="fetch-error",X=l(v.commitTime),et=f==="render"&&X!==void 0&&Math.abs(X-a.timestamp)<6e4,N=C?a.timestamp:et?X:l(o.startedAt)??l(m.startTime)??l(D.startedAt)??l(q.startedAt)??(b!==void 0?a.timestamp-b:a.timestamp);let j;if(typeof o.query=="string"){const i=Array.isArray(o.params)?` [${o.params.join(", ")}]`:"",y=typeof o.result=="string"?` \u2192 ${o.result}`:"";j=o.query+i+y}else if(typeof o.command=="string"){const i=typeof o.value=="string"?` \u2192 ${o.value}`:o.hit===!1?" \u2192 (miss)":"";j=o.command+i}else if(f==="state-update"){const i=o.update??{};let y="";try{y=JSON.stringify(i.preview)}catch{}j=`${i.storeId??""}.${i.path??""} = ${y}`}else if(f==="queue-completed"&&typeof o.returned=="string")j=`returned ${o.returned}`;else if(f==="queue-enqueued")j=`${o.job??"job"}#${o.jobId??"?"} \u2192 ${o.queue??""}${typeof o.data=="string"?" "+o.data:""}`;else if(f==="log"){const i=D.error??{},y=typeof i.stack=="string"?i.stack:typeof o.text=="string"?o.text:void 0;y&&y!==a.title&&(j=y)}let F,K,O,E;if(f==="span"){const i=m.outcome;if(F=i==="threw"||i==="returned"?i:m.status==="error"?"threw":m.status==="ok"?"returned":void 0,F==="threw"){const R=m.error??{},P=typeof R.name=="string"?R.name:void 0,V=typeof R.message=="string"?R.message:typeof a.message=="string"?a.message:void 0;O=P,K=ot([P,V].filter(Boolean).join(": "))||void 0}const y=typeof m.args=="string"?m.args:void 0,A=typeof m.result=="string"?m.result:void 0;if(y!==void 0||A!==void 0){E=[...y!==void 0?["args"]:[],...A!==void 0?["result"]:[]];const R=$(P=>P.length>mt?`${P.slice(0,mt)}\u2026`:P,"short");j=[y!==void 0?`args ${R(y)}`:"",A!==void 0?`returned ${R(A)}`:""].filter(Boolean).join(" \u2192 ")}}let U;if(f==="gateway-request")U=`${o.host??""}${String(o.url??"").split("?")[0]}`;else if(f==="fetch-start"||f==="fetch-done"||f==="fetch-error")try{const i=new URL(String(o.href??o.url??""));U=`${i.hostname}${i.pathname}`}catch{}let W;if(f==="gateway-request"&&Array.isArray(o.steps))W=o.steps.map(i=>{const y=[String(i.step)];return i.project&&y.push(String(i.project)),i.prefix&&y.push(String(i.prefix)),i.mode&&y.push(String(i.mode)+(i.ok===!1?" \u2717":i.ok===!0?" \u2713":"")),i.caller&&y.push("by "+String(i.caller)),i.pod&&y.push(String(i.pod)),typeof i.atMs=="number"&&y.push(`@${i.atMs}ms`),y.join(" ")}),j=W.map(i=>i.replace(/ @\d+ms$/,"")).join(" \u2192 ");else if(f==="egress")j=`${o.method??"GET"} ${o.host??""} \u2192 ${o.status??"?"}`;else if(a.source==="ai"){if(f==="ai-call"){const i=o.usage??{},y=i.promptTokens!==void 0||i.completionTokens!==void 0?` \xB7 ${l(i.promptTokens)??"?"}\u2192${l(i.completionTokens)??"?"} tok`:"",A=o.status!==void 0?` \xB7 ${String(o.status)}`:o.error?" \xB7 failed":"",R=l(o.toolCallCount)?` \xB7 ${l(o.toolCallCount)} tool call${l(o.toolCallCount)===1?"":"s"}`:"",P=l(o.durationMs)!==void 0?` \xB7 ${(l(o.durationMs)/1e3).toFixed(1)}s`:"";j=`${o.provider??""}/${o.model??"?"}${y}${A}${R}${P}${o.stream===!0?" \xB7 stream":""}`}else if(f==="ai-round"){const i=o.model??{},y=o.usage??{},A=Array.isArray(o.toolCalls)?o.toolCalls:[],R=`${l(y.promptTokens)??"?"}\u2192${l(y.completionTokens)??"?"} tok`,P=A.length?` \xB7 ${A.length} tool call${A.length===1?"":"s"}`:"",V=l(o.durationMs)!==void 0?` \xB7 ${(l(o.durationMs)/1e3).toFixed(1)}s`:"";j=`${i.provider??""}/${i.model??""} \xB7 ${R}${P}${V}`}else if(f==="ai-turn-start"){const i=Array.isArray(o.tools)?o.tools:[];j=`skill ${o.skill??""} \xB7 ${o.output??""}${i.length?" \xB7 tools: "+i.join(", "):""}`}else if(f==="ai-tool-call"){const i=o.call??{},y=o.result??{},A=l(o.durationMs)!==void 0?` \xB7 ${(l(o.durationMs)/1e3).toFixed(1)}s`:"";j=`${i.name??"tool"} ${y.error!=null?"\u2717 "+String(y.error):"\u2713"}${A}`}else if(f==="ai-turn-end"){const i=o.usage??{},y=l(o.rounds)!==void 0?` \xB7 ${l(o.rounds)} round${l(o.rounds)===1?"":"s"}`:"",A=l(o.durationMs)!==void 0?` \xB7 ${(l(o.durationMs)/1e3).toFixed(1)}s`:"";j=`${o.ok===!1?"FAILED":"ok"} \xB7 ${l(i.promptTokens)??"?"}+${l(i.completionTokens)??"?"} tok${y}${A}`}}let g;f==="db-query"&&typeof o.query=="string"?g=o.query:f==="redis-op"?g=`${o.op??""} ${String(o.key??"").replace(/\d+/g,"#")}`:f==="egress"?g=`${o.method??"GET"} ${o.host??""}`:f==="ai-call"?g=`ai ${o.provider??""} ${o.model??""} ${o.api??""}`:f==="queue-enqueued"?g=`enqueue ${o.queue??""}`:f==="span"&&typeof m.name=="string"?g=m.name:f==="memory-mutation"&&(g=a.title);const L=a.context??{},z=v.commitId??w.commitId??o.commitId,H=Array.isArray(v.why)?v.why.map(i=>typeof i?.type=="string"?i.type:void 0).filter(i=>!!i):[];return{id:a.id,t:a.timestamp,start:N,...g?{sig:g}:{},...U?{urlKey:U}:{},...httpFacts(f,o),...typeof o.queue=="string"?{queue:o.queue}:{},...o.jobId!=null?{jobId:String(o.jobId)}:{},source:a.source,category:a.category,severity:a.severity,title:F==="threw"&&typeof m.name=="string"?`${m.name} \u2014 threw${O?` ${O}`:""}${b!==void 0?` after ${b}ms`:""}`:a.title,kind:f,area:areaOf({source:a.source,category:a.category,title:a.title,kind:f}),...F?{outcome:F}:{},...K?{error:K}:{},...E?{captured:E}:{},...typeof m.role=="string"?{role:m.role}:{},...f==="log"&&typeof o.level=="string"?{level:o.level}:{},...f==="log"&&typeof o.logger=="string"?{logger:o.logger}:{},...f==="log"&&typeof o.stream=="string"?{stream:o.stream}:{},...typeof L.requestId=="string"?{requestId:L.requestId}:{},...typeof L.parentRequestId=="string"?{parentRequestId:L.parentRequestId}:{},...typeof z=="string"?{commitId:z}:{},...typeof v.componentName=="string"?{component:v.componentName}:{},...typeof v.phase=="string"?{phase:v.phase}:{},...H.length?{why:H}:{},...f==="effect-run"&&typeof w.hookLogicalId=="string"?{hook:w.hookLogicalId}:{},...f==="effect-run"&&w.cleanupRan===!0?{cleanup:!0}:{},...f==="render-commit-truncated"?{renderTotal:l(o.total),renderPublished:l(o.published),...l(o.walkStoppedAt)!==void 0?{renderWalkStoppedAt:l(o.walkStoppedAt)}:{}}:{},...typeof L.projectName=="string"?{project:L.projectName}:typeof o.project=="string"?{project:o.project}:{},...b!==void 0?{durationMs:b}:{},...C&&l(o.startedAt)!==void 0?{fetchStartedAt:l(o.startedAt)}:{},...a.source==="ai"?{ai:p.includeRaw?o:ft(o)}:{},...j?{detail:j}:{},...W?{steps:W}:{}}}T(trimFlowEvent,"trimFlowEvent"),$(trimFlowEvent,"trimFlowEvent");const mt=300;export const OUTLIVED_PARENT_MIN_MS=250;const St=new Set(["egress","db-query","redis-op","ai-call","span","gateway-request","server-request"]),ht=1;function gt(a){return a==="flow-origin"?0:a?.endsWith("-start")?1:a==="gateway-request"?2:a==="server-request"?3:a==="span"?4:a==="render"||a==="render-commit-truncated"?6:a==="effect-run"?7:5}T(gt,"St"),$(gt,"kindRank");function J(a){return a.kind==="span"&&a.role==="controller"?3.5:a.kind==="queue-process"?3:gt(a.kind)}T(J,"Z"),$(J,"causalRank");export function buildFlowDetail(a,p={}){const o=It(a),v=a.slice().filter(t=>{const e=t.raw??{},r=typeof e.kind=="string"?e.kind:"";if(r==="subscription-added"||r==="subscription-removed"||r==="hook-run"||r==="store-created"||r==="singleton-instance"||r===Mt)return!1;const n=e.span;return!(typeof n?.name=="string"&&n.name.startsWith("nest.request "))}).map(t=>trimFlowEvent(t,p)),m=new Set;for(let t=v.length-1;t>=0;t--)m.has(v[t].id)?v.splice(t,1):m.add(v[t].id);const D=new Map;for(const t of v)(t.kind==="gateway-request"||t.kind==="queue-process")&&t.requestId&&!D.has(t.requestId)&&D.set(t.requestId,t.start);const l=$(t=>t.source!=="react"&&t.source!=="valtio"&&t.requestId&&D.has(t.requestId)?D.get(t.requestId):t.start,"anchorOf");v.sort((t,e)=>l(t)-l(e)||t.start-e.start||J(t)-J(e)||(e.durationMs??0)-(t.durationMs??0)||t.t-e.t);const q=$(t=>(t.kind==="span"||t.kind==="server-request"||t.kind==="gateway-request"||t.kind==="queue-process")&&(t.durationMs??0)>0,"spanLike"),w=v;for(const t of w)t.group=t.source==="react"||t.source==="valtio"||t.source==="client-runtime"||t.source==="visual"?"browser":"server";const b=new Map;for(const t of w)if((t.kind==="gateway-request"||t.kind==="queue-process")&&t.requestId&&t.project){const e=b.get(t.requestId)??[];e.push({project:t.project,start:t.start,end:t.start+(t.durationMs??0)}),b.set(t.requestId,e)}const f=$((t,e)=>{const r=b.get(t);if(r?.length)return r.every(n=>n.project===r[0].project)?r[0].project:r.filter(n=>n.start<=e+1&&n.end>=e-1).sort((n,s)=>s.start-n.start)[0]?.project},"projectOf");for(const t of w)if(!t.project&&t.source==="nestjs"&&t.requestId){const e=f(t.requestId,t.start);e&&(t.project=e)}for(const t of w){let e;const r=t.requestId;for(const n of w){if(n===t||!q(n)||n.group!==t.group||n.requestId!==r)continue;const s=n.project,c=t.project;if(n.kind!=="gateway-request"&&t.kind!=="gateway-request"&&s!==void 0&&c!==void 0&&s!==c)continue;const k=n.durationMs??0,M=t.durationMs??0,u=Math.abs(n.start-t.start)<=ht,d=J(n),h=J(t),I=n.start<=t.start;if(!(n.start+k>t.start&&(I||u&&d<h)&&(!u||(d!==h?d<h:I&&k>M))))continue;const S=e?J(e):-1,x=e?Math.abs(n.start-e.start)<=ht:!1;(!e||!x&&n.start>e.start||x&&d>S||x&&d===S&&(n.durationMs??0)<(e.durationMs??0))&&(e=n)}e&&(t.parent=e.id)}const C=new Map;for(const t of w){if(!t.requestId)continue;const e=C.get(t.requestId);e?e.push(t):C.set(t.requestId,[t])}const X=new Map(w.map(t=>[t.id,t])),et=$((t,e)=>{const r=(C.get(t)??[]).filter(n=>q(n)&&n.start<=e&&n.start+(n.durationMs??0)>=e);if(r.length!==0)return r.sort((n,s)=>s.start-n.start||(n.durationMs??0)-(s.durationMs??0)),r[0]},"callerNodeFor");for(const t of w){if(!t.parentRequestId||t.kind!=="server-request"&&t.kind!=="gateway-request")continue;const e=t.parent?X.get(t.parent):void 0;if(e&&e.requestId===t.requestId)continue;const r=et(t.parentRequestId,t.start);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}const N=w,j=new Map;for(const t of N)t.kind==="gateway-request"&&t.requestId&&!j.has(t.requestId)&&j.set(t.requestId,t);for(const t of N)if(t.group==="server"&&!t.parent&&t.requestId&&t.kind!=="gateway-request"){const e=j.get(t.requestId);e&&e!==t&&e.start<=t.start&&(t.parent=e.id)}const F=new Set,K=new Set,O=[];for(const t of N){if(t.kind!=="gateway-request")continue;if(!t.requestId){O.push(t);continue}const e=C.get(t.requestId)??[];if(e.filter(s=>s.kind==="gateway-request").sort((s,c)=>s.start-c.start)[0]!==t)continue;const r=e.find(s=>s.kind==="fetch-start"),n=e.find(s=>s.kind==="fetch-done"||s.kind==="fetch-error");if(!r&&!n){O.push(t);continue}r&&(t.fetchStartId=r.id,F.add(r.id)),n&&(t.fetchDoneId=n.id,K.add(n.id))}const E=new Map;for(const t of N){if(!t.urlKey)continue;const e=E.get(t.urlKey)??{gws:[],starts:[],dones:[]};t.kind==="gateway-request"?O.includes(t)&&e.gws.push(t):t.kind==="fetch-start"?F.has(t.id)||e.starts.push(t):(t.kind==="fetch-done"||t.kind==="fetch-error")&&(K.has(t.id)||e.dones.push(t)),E.set(t.urlKey,e)}for(const{gws:t,starts:e,dones:r}of E.values()){t.sort((s,c)=>s.start-c.start),e.sort((s,c)=>s.start-c.start);const n=new Set;t.forEach((s,c)=>{const k=e[c];k&&(s.fetchStartId=k.id);const M=k?k.t:s.start;let u,d=1/0;for(const h of r){if(n.has(h.id))continue;const I=h.fetchStartedAt??h.start-(h.durationMs??0),S=Math.abs(I-M);S<d&&(d=S,u=h)}u&&(s.fetchDoneId=u.id,n.add(u.id))})}const U=new Map,W=new Map;for(const t of N)!t.queue||!t.jobId||(t.kind==="queue-enqueued"&&U.set(`${t.queue}#${t.jobId}`,t),t.kind==="queue-process"&&W.set(`${t.queue}#${t.jobId}`,t));for(const t of N){if(!t.queue||!t.jobId)continue;const e=`${t.queue}#${t.jobId}`;if(t.kind==="queue-process"&&!t.parent){const r=U.get(e);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}else if(t.kind==="queue-active"||t.kind==="queue-completed"||t.kind==="queue-failed"){const r=W.get(e);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}}{const t=new Map(w.map(e=>[e.id,e]));for(const e of w){if(!e.parent||!St.has(e.kind??"")||e.durationMs===void 0)continue;const r=t.get(e.parent);if(!r||r.kind!=="span"||!((r.durationMs??0)>0))continue;const n=Math.round(e.start+e.durationMs-(r.start+r.durationMs));if(n<OUTLIVED_PARENT_MIN_MS)continue;e.outlivedParentMs=n;const s=r.leftRunning??{count:0,longestMs:0};r.leftRunning={count:s.count+1,longestMs:Math.max(s.longestMs,n)}}for(const e of w){const r=[];e.leftRunning&&r.push(`ended before ${e.leftRunning.count===1?"a call":`${e.leftRunning.count} calls`} it started had finished \u2014 ${e.leftRunning.count===1?"it":"the longest"} ran on ${e.leftRunning.longestMs}ms (a timeout that gave up, or work not awaited)`),e.outlivedParentMs!==void 0&&r.push(`ran on ${e.outlivedParentMs}ms after the span that started it had ended`),r.length>0&&(e.detail=[e.detail,...r].filter(Boolean).join(" \xB7 "))}}let g=w;{const t=new Map;for(const r of g)r.kind==="fetch-start"&&t.set(r.id,r);const e=new Map;for(const r of g){if(r.kind!=="fetch-done"&&r.kind!=="fetch-error")continue;const n=[...t.values()].find(k=>k.requestId&&k.requestId===r.requestId)?.id,s=n?t.get(n):void 0;if(!s)continue;const c=r.fetchStartedAt??s.start;s.title=r.title,s.severity=r.severity,s.durationMs=r.durationMs??Math.max(0,r.t-c),s.kind="fetch",r.detail&&!s.detail&&(s.detail=r.detail),e.set(r.id,s.id)}if(e.size>0){g=g.filter(r=>!e.has(r.id));for(const r of g)for(r.fetchDoneId&&e.has(r.fetchDoneId)&&(r.fetchDoneId=e.get(r.fetchDoneId));r.parent&&e.has(r.parent);)r.parent=e.get(r.parent)}}const L=$(t=>!!t.commitId&&(t.kind==="render"||t.kind==="effect-run"||t.kind==="render-commit-truncated"),"commitMember"),z=$(t=>[...new Set(t)],"uniq"),H=$(t=>z(t.map(e=>e.component).filter(e=>!!e)),"named"),i=$(t=>t.length<=2?t.join(" + "):`${t[0]} + ${t.length-1} more`,"nameList"),y=$(t=>{const e=new Map;for(const r of t)r.component&&e.set(r.component,(e.get(r.component)??0)+1);return[...e].map(([r,n])=>n>1?`${r} \xD7${n}`:r).join(", ")},"tally"),A=$(t=>{if(!t)return"effect";const e=/([^/\\]+?):(\d+)(?::\d+)?$/.exec(t);return e?`${e[1]}:${e[2]}`:t},"shortHook"),R=$(t=>`${A(t.hook)}${t.cleanup?" (cleanup)":""}`,"hookLabel"),P=$(t=>{const e=Math.min(...t.map(n=>n.start)),r=Math.max(...t.map(n=>n.start+(n.durationMs??0)));return{start:e,durationMs:r-e}},"extent"),V=$(t=>(delete t.component,delete t.why,delete t.phase,delete t.hook,delete t.cleanup,delete t.renderTotal,delete t.renderPublished,t),"asGroup");function nt(t){const e=t[0],r=t.filter(B=>B.kind==="render"),n=t.filter(B=>B.kind==="effect-run"),s=t.find(B=>B.kind==="render-commit-truncated"),c=[...r].sort((B,Y)=>(Y.durationMs??0)-(B.durationMs??0)),k=c.filter(B=>(B.why??[]).some(Y=>Y!=="parent-render")),M=z(k.flatMap(B=>(B.why??[]).filter(Y=>Y!=="parent-render"))),u=H(k),d=r.filter(B=>B.phase==="mount"),h=r.filter(B=>B.phase!=="mount"&&!k.includes(B)),I=c[0]?.component??"view",S=r.length>0&&d.length===r.length,x=M.length?M.join(" + "):void 0,G=S?"mount":"update",_=u.length?`view update \xB7 ${i(u)} \u2190 ${x}`:S?`view mount \xB7 ${I}`:`view update \xB7 ${I}`,rt=s?.renderTotal??r.length,yt=[d.length?`mounted ${y(d)}`:"",h.length?`re-rendered ${y(h)}`:"",!u.length&&!S&&r.length?"trigger not recorded \u2014 every captured render came from its parent":"",s?.renderWalkStoppedAt!==void 0?`at least ${rt} renders (the walk stopped at ${s.renderWalkStoppedAt} fibers), ${s.renderPublished??r.length} recorded`:s?`${rt} renders, ${s.renderPublished??r.length} recorded (per-commit cap)`:`${r.length} render${r.length===1?"":"s"}`,n.length?`${n.length} effect${n.length===1?"":"s"}: ${n.map(R).join(", ")}`:""].filter(Boolean).join(" \xB7 "),dt=H(r);return V({...e,...P(t),kind:"view-update",title:_,detail:yt,commitId:e.commitId,viewPhase:G,...x?{cause:x}:{},causeComponents:u,components:dt,mounted:H(d),renderCount:rt,...s?.renderWalkStoppedAt!==void 0?{renderCountAtLeast:!0}:{},recordedRenders:r.length,effects:n.length,effectHooks:n.map(R),memberIds:t.map(B=>B.id),sig:`commit:${G}:${x??"-"}:${u.join(",")}:${dt.slice().sort().join(",")}`,severity:t.some(B=>B.severity==="error")?"error":e.severity})}T(nt,"ft"),$(nt,"viewUpdateNode");function st(t){const e=t[0],r=t.map(R);return V({...e,...P(t),kind:"commit-effects",title:t.length===1?`effect \xB7 ${r[0]}`:`effects \xB7 ${r[0]} + ${t.length-1} more`,detail:r.join(", "),commitId:e.commitId,effects:t.length,effectHooks:r,memberIds:t.map(n=>n.id),sig:`effects:${r.slice().sort().join(",")}`})}T(st,"lt"),$(st,"effectsNode");const $t=$(t=>z(t.map(e=>e==="state-update"?"state update":e==="fetch-start"?"request":e==="flow-origin"?"interaction":"response")).join(" + "),"describeInputs");function at(t,e){const r=t[0],n=t[t.length-1],s=t.length,c=r.detail,k=Math.min(...t.map(h=>h.start)),M=Math.max(...t.map(h=>h.start+(h.durationMs??0))),u=Math.round(t.reduce((h,I)=>h+(I.durationMs??0),0)/s*10)/10,d=Math.round((M-k)*10)/10;r.count=s,r.avgMs=u,r.wallMs=d,r.durationMs=M-k,r.items=t.slice(0,12).map(h=>({start:h.start-k,...h.durationMs!=null?{durationMs:h.durationMs}:{}})),r.memberIds=t.flatMap(h=>h.memberIds??[h.id]),r.title=`\xD7${s} ${r.title}`,t.some(h=>h.severity==="error")&&(r.severity="error"),r.kind==="view-update"?r.detail=[`${s} commits, ${e.length?`one per ${$t(e)}`:"back to back"}`,`avg ${u}ms \xB7 wall ${d}ms`,c??""].filter(Boolean).join(" \xB7 "):r.kind==="state-update"&&c&&n.detail?r.detail=`${s} updates in a row \xB7 wall ${d}ms \xB7 first ${c} \xB7 last ${n.detail}`:r.detail=[`${s} in a row \xB7 wall ${d}ms`,c??""].filter(Boolean).join(" \xB7 ")}T(at,"mt"),$(at,"collapseRepeat");const Q=[],Z=new Map,it=new Set;for(let t=0;t<g.length;t++){if(it.has(t))continue;const e=g[t];if(!L(e)){Q.push(e);continue}const r=[t];for(let c=t+1;c<g.length;c++){const k=g[c];if(k.group==="browser"){if(!L(k)||k.commitId!==e.commitId)break;r.push(c)}}for(const c of r)it.add(c);const n=r.map(c=>g[c]),s=n.some(c=>c.kind!=="effect-run")?nt(n):st(n);for(const c of n)c.id!==s.id&&Z.set(c.id,s.id);Q.push(s)}if(Z.size>0||Q.some(t=>t.kind==="view-update"||t.kind==="commit-effects")){g=Q;for(const t of g)for(;t.parent&&Z.has(t.parent);)t.parent=Z.get(t.parent)}{const t=new Map;for(const e of g)e.kind==="view-update"&&e.commitId&&!t.has(e.commitId)&&t.set(e.commitId,e);for(const e of g){if(e.kind!=="commit-effects"||e.parent||!e.commitId)continue;const r=t.get(e.commitId);r&&r.start<=e.start&&(e.parent=r.id)}}{const t=new Set(["state-update","fetch","fetch-start","fetch-done","fetch-error","flow-origin"]),e=g.map((u,d)=>u.group==="browser"?d:-1).filter(u=>u>=0),r=$(u=>`${u.kind}|${u.sig??u.title}`,"keyOf"),n=e.map(u=>r(g[u])),s=$((u,d,h)=>{for(let I=0;I<h;I++)if(n[u+I]!==n[d+I])return!1;return!0},"sameBlock"),c=$(u=>n[u].split("|")[0],"kindAt"),k=$(u=>{let d=0,h=1;for(let I=1;I<=4&&u+I<=n.length;I++){let S=1;for(;u+(S+1)*I<=n.length&&s(u,u+S*I,I);)S+=1;S>=3&&I*S>d*h&&(d=I,h=S)}return{k:d,r:h}},"bestBlockAt"),M=new Map;for(let u=0;u<n.length;){const{k:d,r:h}=k(u);if(d===0){u+=1;continue}const I=[...Array(d).keys()].find(x=>t.has(c(u+x)));if(I!==void 0&&I>0){const x=k(u+I);if(x.k===d&&x.r>=3&&x.r>=h-1){u+=I;continue}}const S=n.slice(u,u+d).map(x=>x.split("|")[0]).filter(x=>t.has(x));for(let x=0;x<d;x++){const G=[];for(let _=0;_<h;_++)G.push(g[e[u+x+_*d]]);at(G,S);for(const _ of G.slice(1))M.set(_.id,G[0].id)}u+=d*h}if(M.size>0){g=g.filter(u=>!M.has(u.id));for(const u of g)for(;u.parent&&M.has(u.parent);)u.parent=M.get(u.parent)}}{const t=new Map(g.map(e=>[e.id,e]));for(const e of g){const r=new Set([e.id]);let n=e;for(;n?.parent;){if(r.has(n.parent)){n.parent=void 0;break}r.add(n.parent),n=t.get(n.parent)}}}const ut=$(t=>{const e=new Map;for(const n of g){if(!n.sig||q(n)!==t||n.kind==="server-request"||n.kind==="gateway-request"||n.group==="browser")continue;const s=`${n.parent??"root"}|${n.kind}|${n.sig}`,c=e.get(s);c?c.push(n):e.set(s,[n])}const r=new Map;for(const n of e.values()){if(n.length<3)continue;n.sort((d,h)=>d.start-h.start);const s=n[0];let c=!1;for(let d=0;d+1<n.length;d++)if(n[d+1].start<n[d].start+(n[d].durationMs??0)){c=!0;break}const k=s.start,M=Math.max(...n.map(d=>d.start+(d.durationMs??0))),u=n.reduce((d,h)=>d+(h.durationMs??0),0);s.count=n.length,s.parallel=c,s.avgMs=Math.round(u/n.length*10)/10,s.wallMs=M-k,s.durationMs=M-k,s.title=`\xD7${n.length} ${s.title.replace(/\s*\((?:\d+ rows?, )?\d+ms\)$/,"")}`,s.detail=`${s.sig} \xD7${n.length} ${c?"\u2016 parallel":"\u21BB sequential loop"} \xB7 avg ${s.avgMs}ms \xB7 wall ${s.wallMs}ms`,s.items=n.slice(0,12).map(d=>({start:d.start-k,...d.durationMs!=null?{durationMs:d.durationMs}:{}})),s.memberIds=n.flatMap(d=>d.memberIds??[d.id]),n.some(d=>d.severity==="error")&&(s.severity="error");for(const d of n.slice(1))r.set(d.id,s.id)}if(r.size!==0){g=g.filter(n=>!r.has(n.id));for(const n of g)for(;n.parent&&r.has(n.parent);)n.parent=r.get(n.parent)}},"aggregatePass");ut(!0),ut(!1);for(const t of g){if(!q(t))continue;const e=t.durationMs??0;if(e<40)continue;const r=g.filter(M=>M.parent===t.id&&(M.durationMs??0)>0).map(M=>[M.start,M.start+(M.durationMs??0)]).sort((M,u)=>M[0]-u[0]);let n=0,s=-1,c=-1;for(const[M,u]of r)M>c?(c>s&&(n+=c-s),s=M,c=u):u>c&&(c=u);c>s&&(n+=c-s);const k=Math.max(0,e-n);(k>=50||k/e>=.7&&e>=40)&&(t.selfMs=k,t.detail=`\u26A0 cpu-heavy: ${k}ms self-time of ${e}ms${t.detail?" \xB7 "+t.detail:""}`)}return{events:g,...o?{sql:o}:{}}}T(buildFlowDetail,"buildFlowDetail"),$(buildFlowDetail,"buildFlowDetail");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HttpExchange } from '@lensmcp/protocol-types';
|
|
2
|
+
export type { HttpExchange, HttpExchangeBody, HttpExchangeStreamEvent } from '@lensmcp/protocol-types';
|
|
3
|
+
export declare const HTTP_EXCHANGE_DIR = "http";
|
|
4
|
+
export declare const isHttpExchangeId: (value: unknown) => value is string;
|
|
5
|
+
export declare function httpExchangeDir(busFile: string): string;
|
|
6
|
+
export declare function httpExchangeFile(busFile: string, id: string): string;
|
|
7
|
+
/** One exchange, or `undefined` — a malformed id, a file never written or rotated away, or unreadable JSON. */
|
|
8
|
+
export declare function readHttpExchange(busFile: string, id: string): HttpExchange | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var o=Object.defineProperty;var r=(t,e)=>o(t,"name",{value:e,configurable:!0});var c=Object.defineProperty,a=r((t,e)=>c(t,"name",{value:e,configurable:!0}),"e");import*as p from"node:fs";import{dirname as h,join as i}from"node:path";export const HTTP_EXCHANGE_DIR="http";const s=/^hx_[0-9a-z]{8,40}$/;export const isHttpExchangeId=a(t=>typeof t=="string"&&s.test(t),"isHttpExchangeId");export function httpExchangeDir(t){return i(h(t),HTTP_EXCHANGE_DIR)}r(httpExchangeDir,"httpExchangeDir"),a(httpExchangeDir,"httpExchangeDir");export function httpExchangeFile(t,e){if(!isHttpExchangeId(e))throw new Error("invalid http exchange id");return i(httpExchangeDir(t),`${e}.json`)}r(httpExchangeFile,"httpExchangeFile"),a(httpExchangeFile,"httpExchangeFile");export function readHttpExchange(t,e){if(isHttpExchangeId(e))try{const n=JSON.parse(p.readFileSync(httpExchangeFile(t,e),"utf8"));return n&&n.schemaVersion===1&&n.id===e?n:void 0}catch{return}}r(readHttpExchange,"readHttpExchange"),a(readHttpExchange,"readHttpExchange");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an HTTP exchange looked like — never what it carried.
|
|
3
|
+
*
|
|
4
|
+
* A body becomes its SHAPE: the keys of its objects, the types of their values, the length range of its arrays, the
|
|
5
|
+
* FORMAT of a string (uuid, date-time, email, url, jwt…) — never a value. An object whose keys look like data (ids,
|
|
6
|
+
* emails, dates, numbers) is a map, its keys counted, not kept: a key is a field name or it is not kept at all. A
|
|
7
|
+
* field named like a credential (`password`, `accessToken`) keeps its type and is marked `cred`; no length under it
|
|
8
|
+
* is kept. An object's fields are a list carrying their names, never property keys — the bus redacts whatever sits
|
|
9
|
+
* under a key named like a credential, which would take such a field's shape with it.
|
|
10
|
+
* Headers become FACTS: the media type, whether a precondition or an idempotency key came, the credential's scheme
|
|
11
|
+
* (never the credential), each cookie's attributes (never its value), the cache and CORS policy.
|
|
12
|
+
*
|
|
13
|
+
* The services' instrumentation records one exchange per route now and then (`@lensmcp/nest-instrumentation`); the
|
|
14
|
+
* route catalog merges what it records into the route's observed contract (`mergeShapes`), and a route's review is
|
|
15
|
+
* shown it (`shapeText`). Dependency-free.
|
|
16
|
+
*/
|
|
17
|
+
import type { DashHttpRequestFacts, DashHttpResponseFacts, DashShape, DashShapeField, DashStringFormat } from '@lensmcp/protocol-types';
|
|
18
|
+
/** A value's shape (`DashShape`). */
|
|
19
|
+
export type Shape = DashShape;
|
|
20
|
+
export type ShapeField = DashShapeField;
|
|
21
|
+
export type StringFormat = DashStringFormat;
|
|
22
|
+
/** A string's format, if it has one the lens names. */
|
|
23
|
+
export declare function stringFormat(s: string): StringFormat | undefined;
|
|
24
|
+
/** The shape of a value: bounded in depth and keys, no value in it. `bare`: under a credential's name — no length. */
|
|
25
|
+
export declare function shapeOf(value: unknown, depth?: number, bare?: boolean): Shape;
|
|
26
|
+
/**
|
|
27
|
+
* Is this a shape as this version records it? A kept history can hold exchanges an older instrumentation recorded
|
|
28
|
+
* (an object's fields once were keyed by name); those are not merged — `mergeShapes` assumes the current form.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isShape(v: unknown, depth?: number): v is Shape;
|
|
31
|
+
/** A field's shape without its name and marks. */
|
|
32
|
+
export declare function fieldShape(f: ShapeField): Shape;
|
|
33
|
+
/** Two samples' shapes as one: types joined, keys one sample lacked optional, ranges widened. */
|
|
34
|
+
export declare function mergeShapes(a: Shape, b: Shape): Shape;
|
|
35
|
+
/**
|
|
36
|
+
* A shape as a reviewer reads it: `{ id: uuid, items: [1..20] { sku: string, qty: int }, note?: string|null }`; a
|
|
37
|
+
* field named like a credential is marked: `accessToken (credential-named): string(jwt)`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function shapeText(s: Shape, depth?: number): string;
|
|
40
|
+
export type RequestFacts = DashHttpRequestFacts;
|
|
41
|
+
export type ResponseFacts = DashHttpResponseFacts;
|
|
42
|
+
type Headers = Record<string, string | string[] | number | undefined>;
|
|
43
|
+
/** A request's facts from its headers, query and path parameters. */
|
|
44
|
+
export declare function requestFacts(headers: Headers, query?: Record<string, unknown>, params?: Record<string, unknown>): RequestFacts;
|
|
45
|
+
/** A response's facts from its headers (`res.getHeaders()`), against the request's `Origin`. */
|
|
46
|
+
export declare function responseFacts(headers: Headers, requestOrigin?: string): ResponseFacts;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var v=Object.defineProperty;var m=(e,r)=>v(e,"name",{value:r,configurable:!0});var k=Object.defineProperty,l=m((e,r)=>k(e,"name",{value:r,configurable:!0}),"a");import{isSecretParam as j}from"./redact.js";const b=6,d=60,S=4,M=[["uuid",/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i],["ulid",/^[0-9A-HJKMNP-TV-Z]{26}$/],["date-time",/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/],["date",/^\d{4}-\d{2}-\d{2}$/],["email",/^[^\s@]+@[^\s@]+\.[^\s@]+$/],["url",/^https?:\/\/\S+$/i],["jwt",/^eyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}$/],["int",/^-?\d{1,30}$/],["decimal",/^-?\d+\.\d+$/],["phone",/^\+\d{7,15}$/],["hex",/^[0-9a-f]{16,}$/i],["base64",/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[A-Za-z0-9+/]{32,}={0,2}$/]];export function stringFormat(e){for(const[r,n]of M)if(n.test(e))return r}m(stringFormat,"stringFormat"),l(stringFormat,"stringFormat");const x=l(e=>e.length>64||/\s/.test(e)||stringFormat(e)!==void 0&&!/^[a-z]/i.test(e)||/@/.test(e)||/^[0-9a-f]{8}-/i.test(e),"dataKey");export function shapeOf(e,r=0,n=!1){if(e==null)return{t:"null"};if(typeof e=="boolean")return{t:"bool"};if(typeof e=="number")return Number.isInteger(e)?{t:"num",int:!0}:{t:"num"};if(typeof e=="bigint")return{t:"num",int:!0};if(typeof e=="string"){const t=stringFormat(e);return{t:"str",...t?{f:t}:{},...n?{}:{max:e.length}}}if(e instanceof Date)return{t:"str",f:"date-time"};if(r>=b)return Array.isArray(e)?{t:"arr",n:[e.length,e.length]}:{t:"obj",fields:[]};if(Array.isArray(e)){const t=e.slice(0,20).map(o=>shapeOf(o,r+1,n)).reduce((o,i)=>o?mergeShapes(o,i):i,void 0);return{t:"arr",n:[e.length,e.length],...t?{of:t}:{}}}if(typeof e=="object"){const t=Object.entries(e).filter(([,o])=>typeof o!="function");if(t.length&&t.some(([o])=>x(o))){const o=t.slice(0,20).map(([,s])=>shapeOf(s,r+1,n)).reduce((s,c)=>s?mergeShapes(s,c):c,void 0),i=t.map(([s])=>stringFormat(s)).find(Boolean);return{t:"map",n:[t.length,t.length],...i?{key:i}:{},...o?{of:o}:{}}}return{t:"obj",fields:t.slice(0,d).map(([o,i])=>{const s=j(o);return{...shapeOf(i,r+1,n||s),name:o,...s?{cred:!0}:{}}}),...t.length>d?{more:t.length-d}:{}}}return{t:"null"}}m(shapeOf,"shapeOf"),l(shapeOf,"shapeOf");const $=l((e,r)=>e.t===r.t,"same"),F=new Set(["null","bool","num","str","arr","obj","map","any"]);export function isShape(e,r=0){if(!e||typeof e!="object"||r>b*2)return!1;const n=e;return typeof n.t!="string"||!F.has(n.t)?!1:n.t==="obj"?Array.isArray(n.fields)&&n.fields.every(t=>typeof t?.name=="string"&&isShape(t,r+1)):n.t==="any"?Array.isArray(n.of)&&n.of.every(t=>isShape(t,r+1)):n.t==="arr"||n.t==="map"?Array.isArray(n.n)&&(n.of===void 0||isShape(n.of,r+1)):!0}m(isShape,"isShape"),l(isShape,"isShape");export function fieldShape(e){const r={...e};return delete r.name,delete r.opt,delete r.cred,r}m(fieldShape,"fieldShape"),l(fieldShape,"fieldShape");export function mergeShapes(e,r){if(e.t==="any"||r.t==="any"){const n=[...e.t==="any"?e.of:[e],...r.t==="any"?r.of:[r]],t=[];for(const o of n){const i=t.findIndex(s=>$(s,o));i>=0?t[i]=mergeShapes(t[i],o):t.push(o)}return t.length===1?t[0]:{t:"any",of:t.slice(0,S)}}if(!$(e,r))return{t:"any",of:[e,r]};switch(e.t){case"num":return e.int&&r.int?e:{t:"num"};case"str":{const n=r,t=e.f===n.f?e.f:void 0;return{t:"str",...t?{f:t}:{},...e.max!==void 0||n.max!==void 0?{max:Math.max(e.max??0,n.max??0)}:{}}}case"arr":{const n=r,t=e.of&&n.of?mergeShapes(e.of,n.of):e.of??n.of;return{t:"arr",n:[Math.min(e.n[0],n.n[0]),Math.max(e.n[1],n.n[1])],...t?{of:t}:{}}}case"map":{const n=r,t=e.of&&n.of?mergeShapes(e.of,n.of):e.of??n.of;return{t:"map",n:[Math.min(e.n[0],n.n[0]),Math.max(e.n[1],n.n[1])],...e.key===n.key&&e.key?{key:e.key}:{},...t?{of:t}:{}}}case"obj":{const n=r,t=new Map(e.fields.map(c=>[c.name,c])),o=new Map(n.fields.map(c=>[c.name,c])),i=[...new Set([...t.keys(),...o.keys()])].slice(0,d).map(c=>{const f=t.get(c),u=o.get(c);return{...f&&u?mergeShapes(fieldShape(f),fieldShape(u)):fieldShape(f??u),name:c,...!f||!u||f.opt||u.opt?{opt:!0}:{},...f?.cred||u?.cred?{cred:!0}:{}}}),s=Math.max(e.more??0,n.more??0);return{t:"obj",fields:i,...s?{more:s}:{}}}default:return e}}m(mergeShapes,"mergeShapes"),l(mergeShapes,"mergeShapes");export function shapeText(e,r=0){switch(e.t){case"null":return"null";case"bool":return"boolean";case"num":return e.int?"int":"number";case"str":return e.f?`string(${e.f})`:"string";case"arr":return`[${e.n[0]===e.n[1]?e.n[0]:`${e.n[0]}..${e.n[1]}`}]${e.of?` ${shapeText(e.of,r+1)}`:""}`;case"map":return`map<${e.key??"string"}, ${e.of?shapeText(e.of,r+1):"?"}> (${e.n[0]===e.n[1]?e.n[0]:`${e.n[0]}..${e.n[1]}`} entries)`;case"any":return e.of.map(n=>shapeText(n,r)).join(" | ");case"obj":return`{ ${e.fields.map(n=>`${n.name}${n.opt?"?":""}${n.cred?" (credential-named)":""}: ${shapeText(fieldShape(n),r+1)}`).join(", ")}${e.more?`, \u2026 ${e.more} more`:""} }`}}m(shapeText,"shapeText"),l(shapeText,"shapeText");const a=l((e,r)=>{const n=e[r]??e[r.toLowerCase()];return n===void 0?void 0:Array.isArray(n)?n.join(", "):String(n)},"h"),y=l(e=>e?.split(";")[0]?.trim().toLowerCase()||void 0,"media");export function requestFacts(e,r,n){const t=a(e,"authorization")?.split(/\s+/)[0],o=a(e,"cookie"),i=Number(a(e,"content-length")),s=r?Object.keys(r).filter(f=>!x(f)).slice(0,30):[],c=n?Object.keys(n).slice(0,10):[];return{...y(a(e,"content-type"))?{contentType:y(a(e,"content-type"))}:{},...a(e,"accept")?{accept:a(e,"accept").slice(0,80)}:{},...t?{scheme:/^[A-Za-z][\w-]{1,20}$/.test(t)?t:"other"}:{},...o?{cookies:o.split(";").filter(f=>f.includes("=")).length}:{},...a(e,"if-match")?{ifMatch:!0}:{},...a(e,"if-none-match")?{ifNoneMatch:!0}:{},...a(e,"idempotency-key")?{idempotencyKey:!0}:{},...a(e,"origin")?{origin:!0}:{},...Number.isFinite(i)&&i>0?{bytes:i}:{},...s.length?{query:s}:{},...c.length?{params:c}:{}}}m(requestFacts,"requestFacts"),l(requestFacts,"requestFacts");export function responseFacts(e,r){const n=a(e,"etag"),t=Number(a(e,"content-length")),o=e["set-cookie"],i=(Array.isArray(o)?o:o!==void 0?[String(o)]:[]).slice(0,10).map(c=>{const[f="",...u]=c.split(";").map(h=>h.trim()),p=l(h=>u.find(A=>A.toLowerCase().startsWith(h)),"a"),g=p("samesite")?.split("=")[1],w=p("path=")?.slice(5);return{name:f.split("=")[0].slice(0,40),...p("secure")?{secure:!0}:{},...p("httponly")?{httpOnly:!0}:{},...g?{sameSite:g}:{},...p("domain=")?{domain:!0}:{},...w?{path:w}:{},...p("max-age=")?{maxAge:!0}:{},...p("expires=")?{expires:!0}:{}}}),s=a(e,"access-control-allow-origin");return{...y(a(e,"content-type"))?{contentType:y(a(e,"content-type"))}:{},...Number.isFinite(t)&&t>=0&&a(e,"content-length")!==void 0?{bytes:t}:{},...a(e,"cache-control")?{cacheControl:a(e,"cache-control").slice(0,120)}:{},...n?{etag:n.startsWith("W/")?"weak":"strong"}:{},...a(e,"last-modified")?{lastModified:!0}:{},...a(e,"location")?{location:!0}:{},...a(e,"vary")?{vary:a(e,"vary").slice(0,120)}:{},...a(e,"retry-after")?{retryAfter:!0}:{},...a(e,"www-authenticate")?{wwwAuthenticate:a(e,"www-authenticate").split(/\s+/)[0].slice(0,20)}:{},...a(e,"allow")?{allow:a(e,"allow").slice(0,80)}:{},...i.length?{cookies:i}:{},...s?{cors:{origin:s==="*"?"*":r&&s===r?"echoed":"fixed",...a(e,"access-control-allow-credentials")==="true"?{creds:!0}:{},...a(e,"access-control-allow-methods")?{methods:a(e,"access-control-allow-methods").slice(0,80)}:{},...a(e,"access-control-allow-headers")?{headers:a(e,"access-control-allow-headers").slice(0,120)}:{}}}:{},...a(e,"strict-transport-security")?{hsts:!0}:{},...a(e,"x-content-type-options")?{nosniff:!0}:{},...a(e,"x-powered-by")||a(e,"server")?{poweredBy:!0}:{},...a(e,"deprecation")?{deprecation:!0}:{},...a(e,"sunset")?{sunset:!0}:{},...a(e,"ratelimit")||a(e,"ratelimit-policy")||a(e,"x-ratelimit-limit")?{rateLimit:!0}:{},...a(e,"x-request-id")?{requestId:!0}:{}}}m(responseFacts,"responseFacts"),l(responseFacts,"responseFacts");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What in a project's `.lensmcp/` is the TEAM'S — kept with the code and committed — and what is this machine's.
|
|
3
|
+
*
|
|
4
|
+
* Everything the lens writes there at run time is the machine's and must never be committed: the event bus and its
|
|
5
|
+
* rotations, logs, catalogs, the decision ledger, analyzer runs, heap snapshots, sockets, tokens, ports
|
|
6
|
+
* (`config.json`), worktree state. Those files hold request values, SQL parameters and secrets.
|
|
7
|
+
*
|
|
8
|
+
* The team's parts are the folders listed here — today each analyzer's instruction folder, the Markdown its project
|
|
9
|
+
* instructions are kept in (`@lensmcp/ai` files.ts): `.lensmcp/sql-review/`. A new analyzer adds its folder here.
|
|
10
|
+
*
|
|
11
|
+
* The .gitignore that keeps both right ignores the folder's children — at any depth, as `.lensmcp/` did — and lets
|
|
12
|
+
* the team's back in:
|
|
13
|
+
*
|
|
14
|
+
* **\/.lensmcp/*
|
|
15
|
+
* !**\/.lensmcp/sql-review/
|
|
16
|
+
*
|
|
17
|
+
* (`.lensmcp/` — the folder itself ignored — cannot be undone by a `!` line: git does not look inside it. And
|
|
18
|
+
* `.lensmcp/*`, with its slash in the middle, would hold at the repository's root only — a project's own `.lensmcp/`
|
|
19
|
+
* deeper down would be committed.)
|
|
20
|
+
* Lock-step copies: `@lensmcp/nx-plugin` init (writes the lines) and `lensmcp doctor` (checks them).
|
|
21
|
+
*/
|
|
22
|
+
export declare const LENS_SHARED_DIRS: readonly string[];
|
|
23
|
+
/** The .gitignore lines for `.lensmcp/`: at any depth (prefix `**\/`), or at the root only (`/`). */
|
|
24
|
+
export declare function lensGitignoreLines(prefix?: '**/' | '/'): string[];
|
package/lib/lens-dir.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var n=Object.defineProperty;var i=(e,r)=>n(e,"name",{value:r,configurable:!0});var t=Object.defineProperty,o=i((e,r)=>t(e,"name",{value:r,configurable:!0}),"i");export const LENS_SHARED_DIRS=["sql-review","route-review","flow-review","ui-review","cache-review","queue-review"];export function lensGitignoreLines(e="**/"){return[`${e}.lensmcp/*`,...LENS_SHARED_DIRS.map(r=>`!${e}.lensmcp/${r}/`)]}i(lensGitignoreLines,"lensGitignoreLines"),o(lensGitignoreLines,"lensGitignoreLines");
|
package/lib/redact.d.ts
CHANGED
|
@@ -118,3 +118,15 @@ export declare function safeText(value: string | undefined, max?: number): strin
|
|
|
118
118
|
/** Redact then bound — the standard treatment for a captured title. */
|
|
119
119
|
export declare function safeTitle(value: string): string;
|
|
120
120
|
export declare function redactDeep(value: unknown, depth?: number, maxDepth?: number): unknown;
|
|
121
|
+
/**
|
|
122
|
+
* Mask the parameters of a SQL statement that are bound to a credential-named column.
|
|
123
|
+
*
|
|
124
|
+
* {@link redactDeep} sees a parameter as a bare string in an array, with no name to judge it by, so
|
|
125
|
+
* `select * from users where email=$1 and reset_token=$2` kept the live reset token through every
|
|
126
|
+
* pattern (issues/closed/codex-security-13 named it; the bus fix could not reach it). The column the
|
|
127
|
+
* statement binds it to IS its name, judged by the same {@link isSecretParam} as a property name.
|
|
128
|
+
*
|
|
129
|
+
* `params[i]` is `$i+1`. Values in ordinary columns are kept: they are what a developer debugs with.
|
|
130
|
+
* Returns `params` itself when nothing was masked.
|
|
131
|
+
*/
|
|
132
|
+
export declare function redactSqlParams<T>(sql: string, params: T[]): Array<T | typeof REDACTED>;
|
package/lib/redact.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var b=Object.defineProperty;var u=(e,r)=>b(e,"name",{value:r,configurable:!0});var E=Object.defineProperty,c=u((e,r)=>E(e,"name",{value:r,configurable:!0}),"f");export const REDACTED="REDACTED",MAX_TITLE_CHARS=300,MAX_MESSAGE_CHARS=4096,MAX_URL_CHARS=1024;const k=["token","secret","password","passwd","apikey","credential","assertion","signature"],_=new Set(["key","auth","authorization","jwt","code","sig","pwd","pass","session","sid","bearer","otp","cookie","set-cookie"]),y=/^(?:(?:prompt|completion|total|input|output|cache|cached|cacheread|cachewrite|reasoning|thinking|max|min|remaining|used|billed)tokens|tokens?(?:count|used|limit|remaining))$/;export function isSecretParam(e){const r=e.toLowerCase();if(y.test(r.replace(/[-_.]+/g,"")))return!1;if(k.some(n=>r.includes(n)))return!0;for(const n of r.split(/[-_.]+/))if(n&&_.has(n))return!0;return!1}u(isSecretParam,"isSecretParam"),c(isSecretParam,"isSecretParam");const z=/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@\s]*@/i,R=/([?&#])([^=&#\s]+)=([^&#\s]*)/g;export function redactUrlParams(e){return e&&e.replace(z,`$1${REDACTED}@`).replace(R,(r,n,t)=>isSecretParam(t)?`${n}${t}=${REDACTED}`:r)}u(redactUrlParams,"redactUrlParams"),c(redactUrlParams,"redactUrlParams");export function redactUrl(e){return e&&clip(redactUrlParams(e),1024)}u(redactUrl,"redactUrl"),c(redactUrl,"redactUrl");const I=/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g,v=/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,C=/\b(authorization|x-api-key|api-key|cookie|set-cookie|proxy-authorization)\b(\s*[:=]\s*)(["']?)((?:(?:bearer|basic)\s+)?[^\s"',;]{4,})\3/gi;export function redactText(e){return redactUrlParams(e).replace(C,(r,n,t,a)=>`${n}${t}${a}${REDACTED}${a}`).replace(I,REDACTED).replace(v,(r,n)=>`${n} ${REDACTED}`)}u(redactText,"redactText"),c(redactText,"redactText");export function clip(e,r){return e.length<=r?e:`${e.slice(0,r)}\u2026[lensmcp truncated: ${e.length-r} more chars]`}u(clip,"clip"),c(clip,"clip");export function safeText(e,r=4096){if(e!==void 0)return clip(redactText(e),r)}u(safeText,"safeText"),c(safeText,"safeText");export function safeTitle(e){return clip(redactText(e),300)}u(safeTitle,"safeTitle"),c(safeTitle,"safeTitle");const D=6,N=64,P=/^TS\d{1,5}$/,O=c((e,r)=>e==="code"&&typeof r=="string"&&P.test(r),"isDiagnosticCode");export function redactDeep(e,r=0,n=D){if(typeof e=="string")return redactText(e);if(e===null||typeof e!="object"||r>=n)return e;if(Array.isArray(e)){let o=!1;const s=e.map((i,l)=>{if(l>=N)return i;const p=redactDeep(i,r+1,n);return p!==i&&(o=!0),p});return o?s:e}let t=!1;const a={};for(const[o,s]of Object.entries(e)){const i=isSecretParam(o)&&s!==void 0&&s!==null&&!O(o,s)?REDACTED:redactDeep(s,r+1,n);i!==s&&(t=!0),a[o]=i}return t?a:e}u(redactDeep,"redactDeep"),c(redactDeep,"redactDeep");const g=String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*)`,d=String.raw`${g}(?:\s*\.\s*${g})*`,M=String.raw`(?:=|<>|!=|<=|>=|<|>|(?:NOT\s+)?I?LIKE|IS\s+(?:NOT\s+)?DISTINCT\s+FROM)`,U=new RegExp(String.raw`(${d})\s*${M}\s*(?:[A-Za-z_]\w*\s*\(\s*)?\$(\d+)`,"gi"),L=new RegExp(String.raw`(${d})\s+(?:NOT\s+)?IN\s*\(([^()]*)\)`,"gi"),Z=new RegExp(String.raw`\bINSERT\s+INTO\s+${d}\s*\(([^()]*)\)\s*VALUES\s*`,"gi"),j=new RegExp(g,"g"),$=/\$(\d+)/g,x=16384,H=256,X=4096,f=new Map,h=c(e=>{const r=e.match(j)??[],n=r[r.length-1]??"";return n.startsWith('"')?n.slice(1,-1).replace(/""/g,'"'):n},"lastIdent");function A(e,r){const n=[];let t=r;for(;;){for(;t<e.length&&/\s/.test(e[t]);)t++;if(e[t]!=="(")return n;const a=[];let o=t+1,s=0,i=!1;for(t+=1;t<e.length;t++){const l=e[t];if(i){l==="'"&&(i=!1);continue}if(l==="'")i=!0;else if(l==="(")s++;else if(l===")"){if(s===0)break;s--}else l===","&&s===0&&(a.push(e.slice(o,t)),o=t+1)}if(t>=e.length)return n;for(a.push(e.slice(o,t)),n.push(a),t+=1;t<e.length&&/\s/.test(e[t]);)t++;if(e[t]!==",")return n;t+=1}}u(A,"U"),c(A,"valuesTuples");function S(e){const r=e.length<=X,n=r?f.get(e):void 0;if(n)return n;const t=e.length>x?e.slice(0,x):e,a=new Set,o=c((s,i)=>{i&&isSecretParam(i)&&a.add(Number(s))},"bind");for(const s of t.matchAll(U))o(s[2],h(s[1]));for(const s of t.matchAll(L)){const i=h(s[1]);for(const l of s[2].matchAll($))o(l[1],i)}for(const s of t.matchAll(Z)){const i=s[1].split(",").map(l=>h(l.trim()));for(const l of A(t,(s.index??0)+s[0].length))l.forEach((p,w)=>{const m=i[w];if(m)for(const T of p.matchAll($))o(T[1],m)})}if(r){if(f.size>=H){const s=f.keys().next().value;s!==void 0&&f.delete(s)}f.set(e,a)}return a}u(S,"P"),c(S,"secretPlaceholders");export function redactSqlParams(e,r){if(r.length===0||!e.includes("$"))return r;const n=S(e);if(n.size===0)return r;let t=!1;const a=r.map((o,s)=>o==null||!n.has(s+1)?o:(t=!0,REDACTED));return t?a:r}u(redactSqlParams,"redactSqlParams"),c(redactSqlParams,"redactSqlParams");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One URL, one name: fold the identifiers in a path so `/companies/8f2a…/profile` and
|
|
3
|
+
* `/companies/41c0…/profile` are the same route.
|
|
4
|
+
*
|
|
5
|
+
* Shared by the egress endpoints (`net://egress/hosts`) and every HTTP step of a flow
|
|
6
|
+
* (`DashFlowNode.route`), so a route in a flow and an endpoint in a catalog are named alike. The
|
|
7
|
+
* query string is dropped; ids become `:id`, the way Express and Nest write route templates.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately on the lens side, not in a tap: taps are preloaded into every pod and upgrade with a
|
|
10
|
+
* workspace-wide reinstall, while this ships in the CLI bundle and is freely tunable. The raw path is
|
|
11
|
+
* kept beside it wherever it is shown, so nothing is lost.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Collapse identifiers so one endpoint is one row: `/rest/api/3/issue/PROJ-1234`
|
|
15
|
+
* and `/rest/api/3/issue/PROJ-9` are the same endpoint, not two.
|
|
16
|
+
*/
|
|
17
|
+
export declare function templatisePath(path: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var u=Object.defineProperty;var s=(t,e)=>u(t,"name",{value:e,configurable:!0});var l=Object.defineProperty,r=s((t,e)=>l(t,"name",{value:e,configurable:!0}),"r");function f(t){return t.length===0?!1:!!(/^\d{2,}$/.test(t)||/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)||/^[A-Z][A-Z0-9]*-\d+$/.test(t)||t.length>=16&&/^[A-Za-z0-9_-]+$/.test(t)&&/\d/.test(t))}s(f,"a"),r(f,"isIdSegment");export function templatisePath(t){const[e="/"]=t.split("?"),o=e.split("/"),n=[];for(const i of o){if(n.length>12){n.push("\u2026");break}n.push(f(i)?":id":i)}const a=n.join("/");return a.length===0?"/":a}s(templatisePath,"templatisePath"),r(templatisePath,"templatisePath");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A SQL statement's shape: one id for every execution of the same statement whatever its values, the
|
|
3
|
+
* statement as a template, what it does, and the tables it touches.
|
|
4
|
+
*
|
|
5
|
+
* The pg tap names each event by it (so the flood bucket and the catalogs key on the statement, not on
|
|
6
|
+
* a guess at its first table) and the SQL catalog groups executions by it. One implementation for both:
|
|
7
|
+
* a second one would drift, and the catalog would split one statement into two.
|
|
8
|
+
*
|
|
9
|
+
* A small lexer, not a parser — it has to read what an ORM sends (quoted aliases, casts, CTEs, `ANY()`,
|
|
10
|
+
* `FOR UPDATE`, function calls with `FROM` inside), tolerate a statement cut short (old taps clip at 500
|
|
11
|
+
* characters), and never throw. Dependency-free: imported by the tap through `@lensmcp/core/sql-shape`.
|
|
12
|
+
*/
|
|
13
|
+
export interface SqlShape {
|
|
14
|
+
/** 12 hex characters — the same for every execution of the statement, whatever its values. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** The statement with literals as `?`, value lists folded to `(…)`, comments dropped and whitespace collapsed. */
|
|
17
|
+
template: string;
|
|
18
|
+
/** SELECT · INSERT · UPDATE · DELETE · MERGE · …; a `WITH` resolves to the statement it leads to. */
|
|
19
|
+
verb: string;
|
|
20
|
+
/** Every table it reads or writes, in order of first appearance; CTE names, functions and subqueries excluded. */
|
|
21
|
+
tables: string[];
|
|
22
|
+
/** The table an INSERT / UPDATE / DELETE / MERGE writes. */
|
|
23
|
+
target?: string;
|
|
24
|
+
/** A leading comment naming the statement (`/* registry.product-composition *\/`). */
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
/** The shape of one statement. Never throws; a statement it cannot read is its own shape, verb `QUERY`. */
|
|
28
|
+
export declare function sqlShape(sql: string): SqlShape;
|
|
29
|
+
/**
|
|
30
|
+
* How many statements a text holds: `;` separates them outside strings, quoted names and comments, and a trailing
|
|
31
|
+
* `;` only ends the last. The lens explains a statement only when this is exactly 1 — a second statement after the
|
|
32
|
+
* `EXPLAIN` would run.
|
|
33
|
+
*/
|
|
34
|
+
export declare function statementCount(sql: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* A key that is the same for a statement and its `pg_stat_statements` row. The extension writes every constant as
|
|
37
|
+
* `$n` (a boolean too) and keeps the casing it first saw; the app may have written a value or a parameter, in any
|
|
38
|
+
* case — so every literal, parameter and boolean is one placeholder here, and keywords and plain names one case.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shapeMatchKey(sql: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* A `--` comment in a statement that has no line breaks left: whatever followed it on its own line is inside the
|
|
43
|
+
* comment now (a tap that put the statement on one line did that), so the text is not the statement that ran.
|
|
44
|
+
*/
|
|
45
|
+
export declare function lineCommentSwallows(sql: string): boolean;
|
package/lib/sql-shape.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";var M=Object.defineProperty;var a=(t,n)=>M(t,"name",{value:n,configurable:!0});var b=Object.defineProperty,f=a((t,n)=>b(t,"name",{value:n,configurable:!0}),"p");const $=/[A-Za-z_\u0080-]/,F=/[A-Za-z0-9_$\u0080-]/;function I(t){const n=[],c=[];let o=0;const r=t.length;let s=0;const i=f((u,e)=>n.push({kind:u,text:e,up:u==="word"?e.toUpperCase():e}),"push");for(;s<r;){const u=t[s];if(u===" "||u===`
|
|
2
|
+
`||u===" "||u==="\r")s++;else if(u==="-"&&t[s+1]==="-"){o++;const e=t.indexOf(`
|
|
3
|
+
`,s);c.push(t.slice(s+2,e<0?r:e).trim()),s=e<0?r:e+1}else if(u==="/"&&t[s+1]==="*"){const e=t.indexOf("*/",s+2);c.push(t.slice(s+2,e<0?r:e).trim()),s=e<0?r:e+2}else if(u==="'"||(u==="E"||u==="e")&&t[s+1]==="'"){let e=u==="'"?s+1:s+2;for(;e<r;)if(t[e]==="\\"&&u!=="'")e+=2;else if(t[e]==="'"&&t[e+1]==="'")e+=2;else{if(t[e]==="'")break;e++}i("string",t.slice(s,e+1)),s=e+1}else if(u==="$"&&/[0-9]/.test(t[s+1]??"")){let e=s+1;for(;e<r&&/[0-9]/.test(t[e]);)e++;i("param",t.slice(s,e)),s=e}else if(u==="$"){const e=/^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(t.slice(s,s+64));if(e){const E=t.indexOf(e[0],s+e[0].length),S=E<0?r:E+e[0].length;i("string",t.slice(s,S)),s=S}else i("punct",u),s++}else if(u==='"'){let e=s+1;for(;e<r&&!(t[e]==='"'&&t[e+1]!=='"');)e+=t[e]==='"'?2:1;e<r&&i("qident",t.slice(s,e+1)),s=e+1}else if(/[0-9]/.test(u)||u==="."&&/[0-9]/.test(t[s+1]??"")){let e=s;for(;e<r&&/[0-9.eE]/.test(t[e]);)e++;i("number",t.slice(s,e)),s=e}else if($.test(u)){let e=s;for(;e<r&&F.test(t[e]);)e++;i("word",t.slice(s,e)),s=e}else if("(),;[].".includes(u))i("punct",u),s++;else{let e=s;for(;e<r&&/[+\-*/<>=~!@#%^&|`?:]/.test(t[e])&&!(t[e]==="-"&&t[e+1]==="-")&&!(t[e]==="/"&&t[e+1]==="*");)e++;e===s&&(e=s+1),i("op",t.slice(s,e)),s=e}}return{tokens:n,comments:c,lineComments:o}}a(I,"I"),f(I,"lex");function U(t){const n=new Map,c=[],o=[],r=[];t.forEach((s,i)=>{if(s.text===")"||s.text==="]"){const u=r.pop();u!==void 0&&n.set(u,i)}c.push(r.length),o.push(r[r.length-1]??-1),(s.text==="("||s.text==="[")&&r.push(i)});for(const s of r)n.set(s,t.length);return{close:n,depth:c,opener:o}}a(U,"D"),f(U,"structure");const G=new Set(["SELECT","INSERT","UPDATE","DELETE","MERGE","VALUES","TABLE"]),R=new Set(["SELECT","FROM","WHERE","JOIN","LEFT","RIGHT","INNER","OUTER","FULL","CROSS","NATURAL","ON","USING","GROUP","ORDER","BY","HAVING","LIMIT","OFFSET","FETCH","UNION","EXCEPT","INTERSECT","RETURNING","SET","VALUES","WINDOW","FOR","LATERAL","ONLY","AS","WITH","INTO","UPDATE","DELETE","INSERT","DEFAULT","DO","NOTHING","CONFLICT","AND","OR","NOT","IS","NULL","IN","EXISTS","CASE","WHEN","THEN","ELSE","END","TABLESAMPLE","OVERRIDING","MATCHED","MERGE"]);function O(t,n,c=!1){const o=f(i=>i?i.kind==="qident"?i.text.slice(1,-1).replace(/""/g,'"'):i.kind==="word"&&!R.has(i.up)?i.text:void 0:void 0,"part");let r=o(t[n]);if(!r)return;let s=n+1;for(;t[s]?.text==="."&&o(t[s+1]);)r=`${r}.${o(t[s+1])}`,s+=2;if(!(t[s]?.text==="("&&!c))return{name:r.replace(/^public\./i,""),next:s}}a(O,"g"),f(O,"tableAt");function C(t,n,c){t[n]?.up==="AS"&&n++;const o=t[n];return o&&(o.kind==="qident"||o.kind==="word"&&!R.has(o.up))&&(n++,t[n]?.text==="("&&(n=(c.get(n)??n)+1)),n}a(C,"k"),f(C,"skipAlias");function m(t){let n=2166136261,c=16777619;for(let o=0;o<t.length;o++){const r=t.charCodeAt(o);n=Math.imul(n^r,16777619)>>>0,c=Math.imul(c^r,1540483477)>>>0}return(n.toString(16).padStart(8,"0")+c.toString(16).padStart(8,"0")).slice(0,12)}a(m,"w"),f(m,"hashId");function w(t,n){const c=f((i,u)=>i.kind==="string"||i.kind==="number"||i.kind==="param"||[",","::","-","[","]"].includes(i.text)||i.kind==="word"&&(/^(NULL|TRUE|FALSE|DEFAULT)$/i.test(i.text)||u?.text==="::"),"isValue"),o=[];let r=0;for(;r<t.length;){const i=t[r],u=t[r-1];if(i.text==="("&&(u?.up==="IN"||u?.up==="VALUES")){const e=n.get(r)??t.length,E=t.slice(r+1,e);if(E.length&&E.every((S,N)=>c(S,E[N-1]))){if(o.push({kind:"punct",text:"(\u2026)",up:"(\u2026)"}),r=e+1,u.up==="VALUES")for(;t[r]?.text===","&&t[r+1]?.text==="(";)r=(n.get(r+1)??t.length)+1;continue}}o.push(i.kind==="string"||i.kind==="number"?{kind:"op",text:"?",up:"?"}:i),r++}let s="";return o.forEach((i,u)=>{const e=o[u-1],E=!e||["(","[",".","::"].includes(e.text)||[")","]",",",".","::"].includes(i.text)||(i.text==="("||i.text==="(\u2026)"||i.text==="[")&&(e.kind==="qident"||e.kind==="word"&&!R.has(e.up));s+=(E?"":" ")+i.text}),s.trim()}a(w,"$"),f(w,"templateOf");const x=new Map,H=512;export function sqlShape(t){const n=x.get(t);if(n)return n;let c;try{c=D(t)}catch{c={id:m(t),template:t.trim().replace(/\s+/g," "),verb:"QUERY",tables:[]}}return x.set(t,c),x.size>H&&x.delete(x.keys().next().value),c}a(sqlShape,"sqlShape"),f(sqlShape,"sqlShape");function D(t){const{tokens:n,comments:c}=I(t),{close:o,depth:r,opener:s}=U(n);let i=n.find(p=>p.kind==="word")?.up??"QUERY";const u=new Set;n.forEach((p,d)=>{if(p.up!=="WITH"||n[d-1]?.up==="START")return;let l=d+1;for(n[l]?.up==="RECURSIVE"&&l++;;){const T=n[l];if(!T||T.kind!=="word"&&T.kind!=="qident"||(u.add(T.kind==="qident"?T.text.slice(1,-1):T.text),l++,n[l]?.text==="("&&(l=(o.get(l)??l)+1),n[l]?.up!=="AS")||(l++,n[l]?.up==="NOT"&&l++,n[l]?.up==="MATERIALIZED"&&l++,n[l]?.text!=="(")||(l=(o.get(l)??n.length)+1,n[l]?.text!==","))break;l++}}),i==="WITH"&&(i=n.find((p,d)=>r[d]===0&&G.has(p.up)&&d>0)?.up??"SELECT");const e=[];let E;const S=f(p=>{u.has(p)||/^\$/.test(p)||e.includes(p)||e.push(p)},"add");if(n.forEach((p,d)=>{const l=n[d-1]?.up,T=p.up==="FROM"||p.up==="JOIN",k=p.up==="INTO"&&(l==="INSERT"||l==="MERGE")||p.up==="UPDATE"&&l!=="DO"&&l!=="FOR"&&l!=="KEY"&&l!=="NO"||p.up==="USING"||p.up==="TABLE"&&d===0;if(!T&&!k)return;const A=s[d]??-1;if(T&&A>=0&&n[A-1]?.kind==="word"&&!["IN","EXISTS","ANY","ALL","AS","FROM","JOIN","LATERAL","SOME","ARRAY"].includes(n[A-1].up)&&n[A+1]?.up!=="SELECT"&&n[A+1]?.up!=="WITH")return;let h=d+1;for(;n[h]?.up==="ONLY"||n[h]?.up==="LATERAL";)h++;for(;;){const L=O(n,h,p.up==="INTO");if(!L||(S(L.name),k&&!E&&p.up!=="USING"&&(E=L.name),h=C(n,L.next,o),!T||n[h]?.text!==","))break;h++}}),(i==="DELETE"||i==="UPDATE"||i==="INSERT"||i==="MERGE")&&!E){const p=n.findIndex((d,l)=>d.up==="FROM"&&n[l-1]?.up==="DELETE");p>=0&&(E=O(n,p+1)?.name)}const N=w(n,o),g=c.find(p=>/^[\w.\-/:]{3,80}$/.test(p));return{id:m(`${i} ${N.replace(/\$\d+/g,"$?")}`),template:N,verb:i,tables:e,...E?{target:E}:{},...g?{name:g}:{}}}a(D,"G"),f(D,"compute");export function statementCount(t){let n=0,c=!1;for(const o of I(t).tokens)o.text===";"?(c&&n++,c=!1):c=!0;return n+(c?1:0)}a(statementCount,"statementCount"),f(statementCount,"statementCount");export function shapeMatchKey(t){const n=sqlShape(t),c=n.template.replace(/"(?:[^"]|"")*"|[^"]+/g,o=>o.startsWith('"')?o:o.toLowerCase());return`${n.verb} ${c.replace(/\$\d+/g,"?").replace(/\b(?:true|false)\b/g,"?")}`}a(shapeMatchKey,"shapeMatchKey"),f(shapeMatchKey,"shapeMatchKey");export function lineCommentSwallows(t){return!/\n/.test(t)&&I(t).lineComments>0}a(lineCommentSwallows,"lineCommentSwallows"),f(lineCommentSwallows,"lineCommentSwallows");
|
package/lib/stores.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project's backing stores, declared once in `nx.json` → `lensmcp.stores`:
|
|
3
|
+
*
|
|
4
|
+
* "lensmcp": { "stores": {
|
|
5
|
+
* "postgres": { "kind": "postgres", "url": "${PG_URL}", "databases": ["${REFERENCE_PG_URL}"] },
|
|
6
|
+
* "valkey": { "kind": "redis", "url": "${REDIS_URL}" }
|
|
7
|
+
* } }
|
|
8
|
+
*
|
|
9
|
+
* A store is a SERVER — one Postgres, one Redis or Valkey — the unit lensmcp will one day run as a container (a
|
|
10
|
+
* `run` key, reserved). A worktree's databases live in the same server, so one declaration covers every checkout:
|
|
11
|
+
* `url` names THIS checkout's database on it, and is expanded per checkout — in a worktree `${PG_URL}` comes from
|
|
12
|
+
* `.lensmcp/instance.resources.env`, which `lensmcp instance start` wrote with the worktree's own database.
|
|
13
|
+
*
|
|
14
|
+
* `databases` lists the checkout's other databases on the same server (URLs, or bare names on `url`'s server).
|
|
15
|
+
* `lens.url` is a connection for the lens's own reads — a read-only role — when it should not use `url`;
|
|
16
|
+
* `lens.read: false` stops the lens reading the store at all. The lens reads in READ ONLY transactions; before a SQL
|
|
17
|
+
* review it may also refresh the planner's statistics of the statement's tables (`ANALYZE`) and measure a SELECT's
|
|
18
|
+
* plan (`EXPLAIN ANALYZE` in a READ ONLY transaction, rolled back) — `lens.prepare: false` stops both.
|
|
19
|
+
*/
|
|
20
|
+
export type StoreKind = 'postgres' | 'redis';
|
|
21
|
+
export interface StoreDecl {
|
|
22
|
+
kind: StoreKind;
|
|
23
|
+
/** `${VAR}`-templated connection URL: this checkout's database (Postgres) or the server (Redis). */
|
|
24
|
+
url: string;
|
|
25
|
+
/** More databases of this checkout on the same server: URLs or bare names, `${VAR}`-templated. */
|
|
26
|
+
databases: string[];
|
|
27
|
+
lens: {
|
|
28
|
+
url?: string;
|
|
29
|
+
read: boolean;
|
|
30
|
+
prepare?: boolean;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface StoresDeclResult {
|
|
34
|
+
stores: Record<string, StoreDecl>;
|
|
35
|
+
errors: string[];
|
|
36
|
+
/** Things that are accepted but do nothing yet (`run`). */
|
|
37
|
+
notes: string[];
|
|
38
|
+
}
|
|
39
|
+
/** `nx.json` → `lensmcp.stores`, checked. Empty when the project declares none or `nx.json` is unreadable. */
|
|
40
|
+
export declare function readStoresDecl(root: string): StoresDeclResult;
|
|
41
|
+
export declare function validateStoresDecl(raw: unknown): StoresDeclResult;
|
|
42
|
+
/** Parse a dotenv file: `KEY=value`, `export`, quotes, `#` comments, `${VAR}` over `scope` and earlier keys. */
|
|
43
|
+
export declare function parseDotenv(text: string, scope?: Readonly<Record<string, string>>): Record<string, string>;
|
|
44
|
+
/**
|
|
45
|
+
* What a checkout's services see: its `.env` / `.env.local`, the process environment over those (as dotenv does),
|
|
46
|
+
* and — in a worktree — `.lensmcp/instance.resources.env` then the user's `.lensmcp/instance.env` over everything,
|
|
47
|
+
* as the gateway layers them for the worktree's pods.
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkoutEnv(root: string, processEnv?: NodeJS.ProcessEnv): Record<string, string>;
|
|
50
|
+
/** Expand `${VAR}`s over `scope`; the names it lacks are returned, not guessed. */
|
|
51
|
+
export declare function expandVars(template: string, scope: Readonly<Record<string, string>>): {
|
|
52
|
+
value: string;
|
|
53
|
+
missing: string[];
|
|
54
|
+
};
|
|
55
|
+
export interface ResolvedDatabase {
|
|
56
|
+
/** The database name. */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Its connection URL, as the services use it. Holds credentials — never shown. */
|
|
59
|
+
url: string;
|
|
60
|
+
}
|
|
61
|
+
export interface ResolvedStore {
|
|
62
|
+
name: string;
|
|
63
|
+
kind: StoreKind;
|
|
64
|
+
/** `host:port` — what a screen may show. */
|
|
65
|
+
server: string;
|
|
66
|
+
/** Postgres: this checkout's databases on the server, the first being `url`'s. */
|
|
67
|
+
databases: ResolvedDatabase[];
|
|
68
|
+
/** The URL the lens reads with (`lens.url`, else `url`), unless reading is off or a variable is unset. */
|
|
69
|
+
readUrl?: string;
|
|
70
|
+
/** Why the lens does not read it, when it does not. */
|
|
71
|
+
noRead?: string;
|
|
72
|
+
/** Whether a review may refresh its statistics and measure a SELECT's plan (`lens.prepare`, on unless set false). */
|
|
73
|
+
prepare: boolean;
|
|
74
|
+
/** `${VAR}`s that are not set in this checkout. */
|
|
75
|
+
missing: string[];
|
|
76
|
+
}
|
|
77
|
+
/** `url` with its path set to `database`. */
|
|
78
|
+
export declare function withDatabase(url: string, database: string): string;
|
|
79
|
+
/** A connection URL as a screen may show it: no password, no query string. */
|
|
80
|
+
export declare function redactUrl(url: string): string;
|
|
81
|
+
/** The declared stores as THIS checkout sees them: URLs expanded over its environment. */
|
|
82
|
+
export declare function resolveStores(stores: Readonly<Record<string, StoreDecl>>, env: Readonly<Record<string, string>>): ResolvedStore[];
|
package/lib/stores.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var x=Object.defineProperty;var p=(n,e)=>x(n,"name",{value:e,configurable:!0});var U=Object.defineProperty,l=p((n,e)=>U(n,"name",{value:e,configurable:!0}),"u");import{readFileSync as $}from"node:fs";import{join as v}from"node:path";const R=/^[A-Za-z0-9_-]+$/,j=new Set(["kind","url","databases","lens","run"]);export function readStoresDecl(n){let e;try{e=JSON.parse($(v(n,"nx.json"),"utf8"))}catch{return{stores:{},errors:[],notes:[]}}const t=e?.lensmcp?.stores;return t===void 0?{stores:{},errors:[],notes:[]}:validateStoresDecl(t)}p(readStoresDecl,"readStoresDecl"),l(readStoresDecl,"readStoresDecl");export function validateStoresDecl(n){const e=[],t=[],i={};if(!n||typeof n!="object"||Array.isArray(n))return{stores:i,errors:["`lensmcp.stores` must be an object"],notes:t};for(const[s,a]of Object.entries(n)){const r=`stores.${s}`;if(!R.test(s)){e.push(`${r}: a store name is letters, digits, '-' and '_'`);continue}if(!a||typeof a!="object"||Array.isArray(a)){e.push(`${r}: must be an object`);continue}const o=a,f=o.kind;if(f!=="postgres"&&f!=="redis"){e.push(`${r}.kind: one of "postgres", "redis"`);continue}const h=typeof o.url=="string"?o.url.trim():"";if(!h){e.push(`${r}.url: the connection URL is required (e.g. "\${${f==="postgres"?"PG_URL":"REDIS_URL"}}")`);continue}const m=o.databases??[];if(!Array.isArray(m)||!m.every(u=>typeof u=="string"&&u.trim())){e.push(`${r}.databases: a list of URLs or database names`);continue}if(m.length&&f!=="postgres"){e.push(`${r}.databases: only a postgres store has databases`);continue}const d=o.lens??{};if(!d||typeof d!="object"||Array.isArray(d)){e.push(`${r}.lens: must be an object`);continue}const c=d;if(c.url!==void 0&&(typeof c.url!="string"||!c.url.trim())){e.push(`${r}.lens.url: a connection URL`);continue}if(c.read!==void 0&&typeof c.read!="boolean"){e.push(`${r}.lens.read: true or false`);continue}if(c.prepare!==void 0&&typeof c.prepare!="boolean"){e.push(`${r}.lens.prepare: true or false`);continue}o.run!==void 0&&t.push(`${r}.run: planned \u2014 lensmcp does not start stores yet; the store is used as declared`);for(const u of Object.keys(o))j.has(u)||t.push(`${r}.${u}: not a store setting \u2014 ignored`);i[s]={kind:f,url:h,databases:m.map(u=>u.trim()),lens:{...typeof c.url=="string"?{url:c.url.trim()}:{},read:c.read!==!1,prepare:c.prepare!==!1}}}return{stores:i,errors:e,notes:t}}p(validateStoresDecl,"validateStoresDecl"),l(validateStoresDecl,"validateStoresDecl");export function parseDotenv(n,e={}){const t={},i=l(s=>s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,r)=>t[r]??e[r]??""),"expand");for(const s of n.split(/\r?\n/)){const a=/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(s);if(!a)continue;const r=a[1],o=a[2];o.startsWith("'")&&o.indexOf("'",1)>0?t[r]=o.slice(1,o.indexOf("'",1)):o.startsWith('"')&&o.indexOf('"',1)>0?t[r]=i(o.slice(1,o.indexOf('"',1)).replace(/\\n/g,`
|
|
2
|
+
`)):t[r]=i(o.replace(/\s+#.*$/,""))}return t}p(parseDotenv,"parseDotenv"),l(parseDotenv,"parseDotenv");function g(n,e){try{return parseDotenv($(n,"utf8"),e)}catch{return{}}}p(g,"$"),l(g,"readDotenv");export function checkoutEnv(n,e=process.env){let t={};for(const s of[".env",".env.local"])t={...t,...g(v(n,s),{...t})};const i=Object.fromEntries(Object.entries(e).filter(s=>typeof s[1]=="string"));t={...t,...i};for(const s of[v(".lensmcp","instance.resources.env"),v(".lensmcp","instance.env")])t={...t,...g(v(n,s),{...t})};return t}p(checkoutEnv,"checkoutEnv"),l(checkoutEnv,"checkoutEnv");export function expandVars(n,e){const t=[];return{value:n.replace(/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g,(i,s)=>{const a=e[s];return(a===void 0||a==="")&&t.push(s),a??""}),missing:t}}p(expandVars,"expandVars"),l(expandVars,"expandVars");const y=l(n=>n.port||(n.protocol.startsWith("redis")?"6379":"5432"),"portOf");export function withDatabase(n,e){const t=new URL(n);return t.pathname=`/${e}`,t.toString()}p(withDatabase,"withDatabase"),l(withDatabase,"withDatabase");export function redactUrl(n){try{const e=new URL(n);return`${e.protocol}//${e.username?`${decodeURIComponent(e.username)}@`:""}${e.hostname}:${y(e)}${e.pathname}`}catch{return"(not a URL)"}}p(redactUrl,"redactUrl"),l(redactUrl,"redactUrl");export function resolveStores(n,e){return Object.entries(n).map(([t,i])=>{const s=expandVars(i.url,e),a=[...s.missing];let r;try{r=s.missing.length?void 0:new URL(s.value)}catch{r=void 0}const o=[];if(r&&i.kind==="postgres"){o.push({name:decodeURIComponent(r.pathname.replace(/^\//,""))||"postgres",url:s.value});for(const m of i.databases){const d=expandVars(m,e);if(d.missing.length){a.push(...d.missing);continue}const c=/^[a-z]+:\/\//i.test(d.value)?d.value:withDatabase(s.value,d.value);try{const u=decodeURIComponent(new URL(c).pathname.replace(/^\//,""));u&&!o.some(b=>b.name===u)&&o.push({name:u,url:c})}catch{}}}const f=i.lens.url?expandVars(i.lens.url,e):void 0;f?.missing.length&&a.push(...f.missing);const h=i.lens.read?a.length?`${[...new Set(a)].map(m=>`\${${m}}`).join(", ")} not set in this checkout`:r?void 0:"`url` is not a connection URL":"reading is off (`lens.read: false`)";return{name:t,kind:i.kind,server:r?`${r.hostname}:${y(r)}`:"?",databases:o,...h?{noRead:h}:{readUrl:f?.value||s.value},prepare:i.lens.prepare!==!1,missing:[...new Set(a)]}})}p(resolveStores,"resolveStores"),l(resolveStores,"resolveStores");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -17,6 +17,21 @@
|
|
|
17
17
|
"import": "./lib/redact.js",
|
|
18
18
|
"default": "./lib/redact.js"
|
|
19
19
|
},
|
|
20
|
+
"./sql-shape": {
|
|
21
|
+
"types": "./lib/sql-shape.d.ts",
|
|
22
|
+
"import": "./lib/sql-shape.js",
|
|
23
|
+
"default": "./lib/sql-shape.js"
|
|
24
|
+
},
|
|
25
|
+
"./stores": {
|
|
26
|
+
"types": "./lib/stores.d.ts",
|
|
27
|
+
"import": "./lib/stores.js",
|
|
28
|
+
"default": "./lib/stores.js"
|
|
29
|
+
},
|
|
30
|
+
"./lens-dir": {
|
|
31
|
+
"types": "./lib/lens-dir.d.ts",
|
|
32
|
+
"import": "./lib/lens-dir.js",
|
|
33
|
+
"default": "./lib/lens-dir.js"
|
|
34
|
+
},
|
|
20
35
|
"./bus-rotation": {
|
|
21
36
|
"types": "./lib/bus-rotation.d.ts",
|
|
22
37
|
"import": "./lib/bus-rotation.js",
|
|
@@ -32,6 +47,11 @@
|
|
|
32
47
|
"import": "./lib/ai-content.js",
|
|
33
48
|
"default": "./lib/ai-content.js"
|
|
34
49
|
},
|
|
50
|
+
"./http-exchange": {
|
|
51
|
+
"types": "./lib/http-exchange.d.ts",
|
|
52
|
+
"import": "./lib/http-exchange.js",
|
|
53
|
+
"default": "./lib/http-exchange.js"
|
|
54
|
+
},
|
|
35
55
|
"./headless-flow": {
|
|
36
56
|
"types": "./lib/headless-flow.d.ts",
|
|
37
57
|
"import": "./lib/headless-flow.js",
|
|
@@ -39,7 +59,7 @@
|
|
|
39
59
|
}
|
|
40
60
|
},
|
|
41
61
|
"dependencies": {
|
|
42
|
-
"@lensmcp/protocol-types": "1.
|
|
62
|
+
"@lensmcp/protocol-types": "1.25.0",
|
|
43
63
|
"tslib": "^2.3.0"
|
|
44
64
|
},
|
|
45
65
|
"license": "Apache-2.0",
|