unbrowse 3.1.0 → 3.2.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/dist/cli.js +455 -96
- package/dist/index.js +2 -6
- package/dist/mcp.js +695 -46
- package/dist/server.js +25811 -0
- package/package.json +1 -2
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/vendor/kuri/manifest.json +7 -10
- package/runtime-src/agent-outcome.ts +0 -166
- package/runtime-src/analytics-session.ts +0 -55
- package/runtime-src/api/browse-index.ts +0 -317
- package/runtime-src/api/browse-session.ts +0 -572
- package/runtime-src/api/browse-submit-prereqs.ts +0 -48
- package/runtime-src/api/browse-submit.ts +0 -1184
- package/runtime-src/api/routes.ts +0 -1823
- package/runtime-src/auth/browser-cookies.ts +0 -423
- package/runtime-src/auth/index.ts +0 -535
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -659
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/build-info.generated.ts +0 -6
- package/runtime-src/capture/index.ts +0 -1794
- package/runtime-src/capture/prefetch.ts +0 -95
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/cli.ts +0 -1572
- package/runtime-src/client/graph-client.ts +0 -100
- package/runtime-src/client/index.ts +0 -1425
- package/runtime-src/debug-trace.ts +0 -18
- package/runtime-src/domain.ts +0 -38
- package/runtime-src/execution/index.ts +0 -3397
- package/runtime-src/execution/retry.ts +0 -46
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/extraction/index.ts +0 -1507
- package/runtime-src/foundry/publish-bundle.ts +0 -392
- package/runtime-src/graph/agent-augment.ts +0 -315
- package/runtime-src/graph/index.ts +0 -1524
- package/runtime-src/graph/local-fixtures.ts +0 -393
- package/runtime-src/graph/local-harness.ts +0 -646
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/index.ts +0 -24
- package/runtime-src/indexer/index.ts +0 -465
- package/runtime-src/intent-match.ts +0 -1515
- package/runtime-src/kuri/client.ts +0 -1839
- package/runtime-src/logger.ts +0 -30
- package/runtime-src/marketplace/index.ts +0 -103
- package/runtime-src/mcp.ts +0 -1747
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -257
- package/runtime-src/orchestrator/first-pass-action.ts +0 -403
- package/runtime-src/orchestrator/index.ts +0 -4480
- package/runtime-src/orchestrator/passive-publish.ts +0 -187
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -270
- package/runtime-src/payments/lobster-pay.ts +0 -182
- package/runtime-src/payments/wallet.ts +0 -98
- package/runtime-src/publish/review-context.ts +0 -93
- package/runtime-src/publish/sanitize.ts +0 -197
- package/runtime-src/publish/schema-review.ts +0 -192
- package/runtime-src/publish-admission.ts +0 -388
- package/runtime-src/ratelimit/index.ts +0 -23
- package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
- package/runtime-src/reverse-engineer/index.ts +0 -1551
- package/runtime-src/router.ts +0 -17
- package/runtime-src/routing-telemetry.ts +0 -395
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-auth.ts +0 -12
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/local-server.ts +0 -311
- package/runtime-src/runtime/paths.ts +0 -99
- package/runtime-src/runtime/setup.ts +0 -251
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/runtime/update-hints.ts +0 -351
- package/runtime-src/server.ts +0 -100
- package/runtime-src/session-logs.ts +0 -142
- package/runtime-src/settings.ts +0 -221
- package/runtime-src/single-binary.ts +0 -143
- package/runtime-src/site-policy.ts +0 -54
- package/runtime-src/stale-cleanup-runner.ts +0 -144
- package/runtime-src/stale-cleanup.ts +0 -133
- package/runtime-src/telemetry-attribution.ts +0 -120
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/template-params.ts +0 -141
- package/runtime-src/transform/drift.ts +0 -60
- package/runtime-src/transform/index.ts +0 -277
- package/runtime-src/types/index.ts +0 -1
- package/runtime-src/types/skill.ts +0 -912
- package/runtime-src/vault/index.ts +0 -196
- package/runtime-src/verification/auth-gate.ts +0 -8
- package/runtime-src/verification/candidates.ts +0 -27
- package/runtime-src/verification/index.ts +0 -120
- package/runtime-src/verification/matrix.ts +0 -30
- package/runtime-src/version.ts +0 -148
- package/runtime-src/workflow/artifact.ts +0 -161
- package/runtime-src/workflow/compile.ts +0 -808
- package/runtime-src/workflow/publish.ts +0 -225
- package/runtime-src/workflow/runtime.ts +0 -213
- package/vendor/kuri/win-x64/kuri.exe +0 -0
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
export const TELEMETRY_ATTRIBUTION_KEYS = [
|
|
2
|
-
"utm_source",
|
|
3
|
-
"utm_medium",
|
|
4
|
-
"utm_campaign",
|
|
5
|
-
"utm_content",
|
|
6
|
-
"utm_term",
|
|
7
|
-
"utm_id",
|
|
8
|
-
"gclid",
|
|
9
|
-
"wbraid",
|
|
10
|
-
"gbraid",
|
|
11
|
-
"fbclid",
|
|
12
|
-
"twclid",
|
|
13
|
-
"ttclid",
|
|
14
|
-
"msclkid",
|
|
15
|
-
"li_fat_id",
|
|
16
|
-
"referrer_host",
|
|
17
|
-
"channel",
|
|
18
|
-
"campaign_id",
|
|
19
|
-
"campaign_name",
|
|
20
|
-
"content_id",
|
|
21
|
-
"content_type",
|
|
22
|
-
"creative_id",
|
|
23
|
-
"ad_id",
|
|
24
|
-
"adset_id",
|
|
25
|
-
"inferred_icp",
|
|
26
|
-
"variant_id",
|
|
27
|
-
"experiment_id",
|
|
28
|
-
"icp",
|
|
29
|
-
] as const;
|
|
30
|
-
|
|
31
|
-
export type TelemetryAttributionKey = (typeof TELEMETRY_ATTRIBUTION_KEYS)[number];
|
|
32
|
-
|
|
33
|
-
export interface TelemetryAttribution {
|
|
34
|
-
utm_source?: string;
|
|
35
|
-
utm_medium?: string;
|
|
36
|
-
utm_campaign?: string;
|
|
37
|
-
utm_content?: string;
|
|
38
|
-
utm_term?: string;
|
|
39
|
-
utm_id?: string;
|
|
40
|
-
gclid?: string;
|
|
41
|
-
wbraid?: string;
|
|
42
|
-
gbraid?: string;
|
|
43
|
-
fbclid?: string;
|
|
44
|
-
twclid?: string;
|
|
45
|
-
ttclid?: string;
|
|
46
|
-
msclkid?: string;
|
|
47
|
-
li_fat_id?: string;
|
|
48
|
-
referrer_host?: string;
|
|
49
|
-
channel?: string;
|
|
50
|
-
campaign_id?: string;
|
|
51
|
-
campaign_name?: string;
|
|
52
|
-
content_id?: string;
|
|
53
|
-
content_type?: string;
|
|
54
|
-
creative_id?: string;
|
|
55
|
-
ad_id?: string;
|
|
56
|
-
adset_id?: string;
|
|
57
|
-
inferred_icp?: string;
|
|
58
|
-
variant_id?: string;
|
|
59
|
-
experiment_id?: string;
|
|
60
|
-
icp?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const MAX_ATTRIBUTION_VALUE_LENGTH = 160;
|
|
64
|
-
|
|
65
|
-
export function sanitizeAttributionValue(value: unknown): string | undefined {
|
|
66
|
-
if (typeof value !== "string") return undefined;
|
|
67
|
-
const trimmed = value.trim();
|
|
68
|
-
if (!trimmed) return undefined;
|
|
69
|
-
return trimmed.slice(0, MAX_ATTRIBUTION_VALUE_LENGTH);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function hasTelemetryAttribution(value: Partial<TelemetryAttribution> | null | undefined): boolean {
|
|
73
|
-
if (!value) return false;
|
|
74
|
-
return TELEMETRY_ATTRIBUTION_KEYS.some((key) => Boolean(sanitizeAttributionValue(value[key])));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function sanitizeTelemetryAttribution(
|
|
78
|
-
raw: Partial<Record<TelemetryAttributionKey, unknown>> | null | undefined,
|
|
79
|
-
): TelemetryAttribution | undefined {
|
|
80
|
-
if (!raw) return undefined;
|
|
81
|
-
const cleaned: TelemetryAttribution = {};
|
|
82
|
-
for (const key of TELEMETRY_ATTRIBUTION_KEYS) {
|
|
83
|
-
const value = sanitizeAttributionValue(raw[key]);
|
|
84
|
-
if (value) cleaned[key] = value;
|
|
85
|
-
}
|
|
86
|
-
return hasTelemetryAttribution(cleaned) ? cleaned : undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function mergeTelemetryAttribution(
|
|
90
|
-
base?: Partial<TelemetryAttribution> | null,
|
|
91
|
-
incoming?: Partial<TelemetryAttribution> | null,
|
|
92
|
-
): TelemetryAttribution | undefined {
|
|
93
|
-
const merged = sanitizeTelemetryAttribution({
|
|
94
|
-
...(base ?? {}),
|
|
95
|
-
...(incoming ?? {}),
|
|
96
|
-
});
|
|
97
|
-
return merged;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function mergeTelemetryProperties(
|
|
101
|
-
properties?: Record<string, unknown>,
|
|
102
|
-
attribution?: Partial<TelemetryAttribution> | null,
|
|
103
|
-
): Record<string, unknown> | undefined {
|
|
104
|
-
const cleanedAttribution = sanitizeTelemetryAttribution(attribution);
|
|
105
|
-
if (!cleanedAttribution) return properties;
|
|
106
|
-
return {
|
|
107
|
-
...cleanedAttribution,
|
|
108
|
-
...(properties ?? {}),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function decodeTelemetryAttribution(value: string | undefined): TelemetryAttribution | undefined {
|
|
113
|
-
if (!value) return undefined;
|
|
114
|
-
try {
|
|
115
|
-
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
116
|
-
return sanitizeTelemetryAttribution(JSON.parse(decoded) as Partial<Record<TelemetryAttributionKey, unknown>>);
|
|
117
|
-
} catch {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
}
|
package/runtime-src/telemetry.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route trace telemetry — issue #28
|
|
3
|
-
*
|
|
4
|
-
* Persists anonymized routing artifacts to ~/.unbrowse/traces/ for future
|
|
5
|
-
* ML training (RAG over traces, contextual bandits, KGE extraction).
|
|
6
|
-
*
|
|
7
|
-
* Anonymization rules (per spec):
|
|
8
|
-
* NEVER store: raw cookies, auth tokens, CSRF tokens, full request/response
|
|
9
|
-
* bodies, user-entered secrets, raw chain-of-thought, PII.
|
|
10
|
-
* DO store: normalized binding names, hashed binding values, response
|
|
11
|
-
* schema shape, response hashes, route fingerprints,
|
|
12
|
-
* error class / failure reason taxonomy.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
import type { RouteTraceArtifact, TraceFailureReason } from "./types/index.js";
|
|
19
|
-
import { TRACE_VERSION } from "./version.js";
|
|
20
|
-
|
|
21
|
-
function getTraceDir(): string {
|
|
22
|
-
return process.env.UNBROWSE_TRACES_DIR ??
|
|
23
|
-
join(process.env.HOME ?? "/tmp", ".unbrowse", "traces");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Whether trace emission is enabled (opt-out via env). */
|
|
27
|
-
export function isTracingEnabled(): boolean {
|
|
28
|
-
return process.env.UNBROWSE_DISABLE_TRACES !== "1";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* SHA-256 hex digest of a string value.
|
|
33
|
-
* Used to hash binding values so dependency matching works without exposing
|
|
34
|
-
* the actual value.
|
|
35
|
-
*/
|
|
36
|
-
export function hashValue(value: string): string {
|
|
37
|
-
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Sensitive field name patterns — values of matching keys are stripped. */
|
|
41
|
-
const SENSITIVE_PATTERNS = [
|
|
42
|
-
/cookie/i, /token/i, /auth/i, /key/i, /secret/i, /password/i,
|
|
43
|
-
/csrf/i, /session/i, /bearer/i, /credential/i, /apikey/i,
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
function isSensitiveName(name: string): boolean {
|
|
47
|
-
return SENSITIVE_PATTERNS.some((re) => re.test(name));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Return only the binding *names* that are safe to surface in a trace.
|
|
52
|
-
* Values are never included; sensitive names are omitted entirely.
|
|
53
|
-
*/
|
|
54
|
-
export function safeBindingNames(bindings: Record<string, unknown>): string[] {
|
|
55
|
-
return Object.keys(bindings).filter((k) => !isSensitiveName(k));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Strip the query string and fragment from a URL so traces never contain
|
|
60
|
-
* user-entered search terms or personal identifiers embedded in URLs.
|
|
61
|
-
*/
|
|
62
|
-
export function anonymizeUrl(url: string): string {
|
|
63
|
-
try {
|
|
64
|
-
const u = new URL(url);
|
|
65
|
-
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
66
|
-
} catch {
|
|
67
|
-
return url.replace(/\?.*$/, "").replace(/#.*$/, "");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Produce a SHA-256 hex digest of the serialized result body.
|
|
73
|
-
* This lets the backend detect duplicate responses without storing content.
|
|
74
|
-
*/
|
|
75
|
-
export function hashResponseBody(result: unknown): string {
|
|
76
|
-
const str = typeof result === "string" ? result : JSON.stringify(result ?? "");
|
|
77
|
-
return createHash("sha256").update(str).digest("hex").slice(0, 32);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Classify an error message into a `TraceFailureReason` taxonomy value so
|
|
82
|
-
* negative traces carry structured labels instead of free-form strings.
|
|
83
|
-
*/
|
|
84
|
-
export function classifyFailure(error: string | undefined): TraceFailureReason {
|
|
85
|
-
if (!error) return "unknown";
|
|
86
|
-
const e = error.toLowerCase();
|
|
87
|
-
if (e.includes("auth") || e.includes("401") || e.includes("403")) return "auth_failed";
|
|
88
|
-
if (e.includes("schema") || e.includes("mismatch")) return "schema_mismatch";
|
|
89
|
-
if (e.includes("timeout") || e.includes("timed out")) return "timeout";
|
|
90
|
-
if (e.includes("empty") || e.includes("no result")) return "empty_response";
|
|
91
|
-
if (e.includes("missing") || e.includes("dependency")) return "dependency_missing";
|
|
92
|
-
if (e.includes("robots") || e.includes("disallowed")) return "robots_disallowed";
|
|
93
|
-
return "unknown";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface EmitTraceParams {
|
|
97
|
-
trace_id: string;
|
|
98
|
-
session_scope: string;
|
|
99
|
-
goal: string;
|
|
100
|
-
domain: string;
|
|
101
|
-
started_at: string;
|
|
102
|
-
skill_id?: string;
|
|
103
|
-
endpoint_id?: string;
|
|
104
|
-
source: RouteTraceArtifact["source"];
|
|
105
|
-
status_code?: number;
|
|
106
|
-
response_bytes: number;
|
|
107
|
-
result?: unknown;
|
|
108
|
-
schema_match?: boolean;
|
|
109
|
-
candidates_considered: number;
|
|
110
|
-
bindings_before: Record<string, unknown>;
|
|
111
|
-
bindings_resolved: Record<string, unknown>;
|
|
112
|
-
bindings_missing: string[];
|
|
113
|
-
outcome: RouteTraceArtifact["outcome"];
|
|
114
|
-
error?: string;
|
|
115
|
-
rationale?: string;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Write an anonymized `RouteTraceArtifact` to the local trace directory.
|
|
120
|
-
* Returns the file path written, or null if tracing is disabled or the write fails.
|
|
121
|
-
*/
|
|
122
|
-
export function emitRouteTrace(params: EmitTraceParams): string | null {
|
|
123
|
-
if (!isTracingEnabled()) return null;
|
|
124
|
-
|
|
125
|
-
const now = new Date().toISOString();
|
|
126
|
-
const artifact: RouteTraceArtifact = {
|
|
127
|
-
trace_id: params.trace_id,
|
|
128
|
-
session_scope: params.session_scope,
|
|
129
|
-
goal: params.goal,
|
|
130
|
-
domain: params.domain,
|
|
131
|
-
started_at: params.started_at,
|
|
132
|
-
completed_at: now,
|
|
133
|
-
latency_ms: Date.now() - new Date(params.started_at).getTime(),
|
|
134
|
-
skill_id: params.skill_id,
|
|
135
|
-
endpoint_id: params.endpoint_id,
|
|
136
|
-
source: params.source,
|
|
137
|
-
status_code: params.status_code,
|
|
138
|
-
response_bytes: params.response_bytes,
|
|
139
|
-
response_hash: params.result != null ? hashResponseBody(params.result) : undefined,
|
|
140
|
-
schema_match: params.schema_match,
|
|
141
|
-
candidates_considered: params.candidates_considered,
|
|
142
|
-
bindings_before: safeBindingNames(params.bindings_before),
|
|
143
|
-
bindings_resolved: safeBindingNames(params.bindings_resolved),
|
|
144
|
-
bindings_missing: params.bindings_missing.filter((k) => !isSensitiveName(k)),
|
|
145
|
-
outcome: params.outcome,
|
|
146
|
-
failure_reason: params.outcome !== "success"
|
|
147
|
-
? classifyFailure(params.error)
|
|
148
|
-
: undefined,
|
|
149
|
-
rationale: params.rationale,
|
|
150
|
-
trace_version: TRACE_VERSION,
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const traceDir = getTraceDir();
|
|
155
|
-
if (!existsSync(traceDir)) mkdirSync(traceDir, { recursive: true });
|
|
156
|
-
const stamp = params.started_at.replace(/[:.]/g, "-");
|
|
157
|
-
const file = join(traceDir, `${stamp}-${params.outcome}-${params.trace_id}.json`);
|
|
158
|
-
writeFileSync(file, JSON.stringify(artifact, null, 2), "utf-8");
|
|
159
|
-
return file;
|
|
160
|
-
} catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Error accumulator — auto-file GitHub issues on repeated failures
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
interface AccumulatedError {
|
|
170
|
-
count: number;
|
|
171
|
-
firstSeen: string;
|
|
172
|
-
lastSeen: string;
|
|
173
|
-
error: string;
|
|
174
|
-
intent: string;
|
|
175
|
-
url?: string;
|
|
176
|
-
domain: string;
|
|
177
|
-
skillId?: string;
|
|
178
|
-
endpointId?: string;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const errorAccumulator = new Map<string, AccumulatedError>();
|
|
182
|
-
const AUTO_FILE_THRESHOLD = 3;
|
|
183
|
-
const autoFiledKeys = new Set<string>();
|
|
184
|
-
|
|
185
|
-
export interface FailureContext {
|
|
186
|
-
skillId?: string;
|
|
187
|
-
endpointId?: string;
|
|
188
|
-
domain: string;
|
|
189
|
-
intent: string;
|
|
190
|
-
url?: string;
|
|
191
|
-
error: string;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Record a failure. After AUTO_FILE_THRESHOLD consecutive failures for the
|
|
196
|
-
* same skill+endpoint, auto-file a GitHub issue via the backend.
|
|
197
|
-
*/
|
|
198
|
-
export function recordFailure(ctx: FailureContext): void {
|
|
199
|
-
const key = `${ctx.skillId ?? "unknown"}:${ctx.endpointId ?? "unknown"}`;
|
|
200
|
-
const existing = errorAccumulator.get(key);
|
|
201
|
-
const now = new Date().toISOString();
|
|
202
|
-
|
|
203
|
-
if (existing) {
|
|
204
|
-
existing.count++;
|
|
205
|
-
existing.lastSeen = now;
|
|
206
|
-
existing.error = ctx.error;
|
|
207
|
-
} else {
|
|
208
|
-
errorAccumulator.set(key, {
|
|
209
|
-
count: 1,
|
|
210
|
-
firstSeen: now,
|
|
211
|
-
lastSeen: now,
|
|
212
|
-
error: ctx.error,
|
|
213
|
-
intent: ctx.intent,
|
|
214
|
-
url: ctx.url,
|
|
215
|
-
domain: ctx.domain,
|
|
216
|
-
skillId: ctx.skillId,
|
|
217
|
-
endpointId: ctx.endpointId,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const entry = errorAccumulator.get(key)!;
|
|
222
|
-
if (entry.count >= AUTO_FILE_THRESHOLD && !autoFiledKeys.has(key)) {
|
|
223
|
-
autoFiledKeys.add(key);
|
|
224
|
-
// Fire-and-forget auto-file
|
|
225
|
-
import("./client/index.js")
|
|
226
|
-
.then((client) =>
|
|
227
|
-
client.autoFileIssue({
|
|
228
|
-
skill_id: ctx.skillId ?? "unknown",
|
|
229
|
-
endpoint_id: ctx.endpointId ?? "unknown",
|
|
230
|
-
domain: ctx.domain,
|
|
231
|
-
intent: ctx.intent,
|
|
232
|
-
url: ctx.url,
|
|
233
|
-
error: ctx.error,
|
|
234
|
-
failure_count: entry.count,
|
|
235
|
-
first_seen: entry.firstSeen,
|
|
236
|
-
last_seen: entry.lastSeen,
|
|
237
|
-
kuri_version: process.env.KURI_VERSION ?? "unknown",
|
|
238
|
-
}),
|
|
239
|
-
)
|
|
240
|
-
.catch((err) =>
|
|
241
|
-
console.warn(`[telemetry] auto-file failed: ${(err as Error).message}`),
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function getAccumulatedErrors(): Map<string, AccumulatedError> {
|
|
247
|
-
return new Map(errorAccumulator);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function resetErrorAccumulatorForTests(): void {
|
|
251
|
-
errorAccumulator.clear();
|
|
252
|
-
autoFiledKeys.clear();
|
|
253
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
function sanitizeBindingKey(key: string): string {
|
|
2
|
-
return key
|
|
3
|
-
.replace(/\[\]/g, "_item")
|
|
4
|
-
.replace(/\[([^\]]+)\]/g, (_m, inner: string) => `_${inner}`)
|
|
5
|
-
.replace(/[{}]/g, "")
|
|
6
|
-
.replace(/[^a-zA-Z0-9_]+/g, "_")
|
|
7
|
-
.replace(/_+/g, "_")
|
|
8
|
-
.replace(/^_+|_+$/g, "");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function normalizeQueryBindingKey(key: string): string {
|
|
12
|
-
const normalized = sanitizeBindingKey(key);
|
|
13
|
-
if (!normalized) return "query";
|
|
14
|
-
return /^[0-9]/.test(normalized) ? `query_${normalized}` : normalized;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildQueryBindingMap(keys: Iterable<string>): Record<string, string> {
|
|
18
|
-
const out: Record<string, string> = {};
|
|
19
|
-
const used = new Set<string>();
|
|
20
|
-
for (const rawKey of keys) {
|
|
21
|
-
if (!rawKey || out[rawKey]) continue;
|
|
22
|
-
const base = normalizeQueryBindingKey(rawKey);
|
|
23
|
-
let next = base;
|
|
24
|
-
let suffix = 2;
|
|
25
|
-
while (used.has(next)) next = `${base}_${suffix++}`;
|
|
26
|
-
used.add(next);
|
|
27
|
-
out[rawKey] = next;
|
|
28
|
-
}
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function extractTemplateQueryBindings(urlTemplate: string): Record<string, string> {
|
|
33
|
-
try {
|
|
34
|
-
const templateUrl = new URL(urlTemplate);
|
|
35
|
-
const out: Record<string, string> = {};
|
|
36
|
-
for (const [key, value] of templateUrl.searchParams.entries()) {
|
|
37
|
-
const placeholder = value.match(/^\{([^}]+)\}$/)?.[1];
|
|
38
|
-
if (placeholder) out[key] = placeholder;
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
} catch {
|
|
42
|
-
return {};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function deriveTemplateParamsFromContextUrl(
|
|
47
|
-
urlTemplate: string,
|
|
48
|
-
contextUrl?: string,
|
|
49
|
-
): Record<string, string> {
|
|
50
|
-
if (!contextUrl) return {};
|
|
51
|
-
try {
|
|
52
|
-
const templateUrl = new URL(urlTemplate);
|
|
53
|
-
const actualUrl = new URL(contextUrl);
|
|
54
|
-
const out: Record<string, string> = {};
|
|
55
|
-
|
|
56
|
-
const templateParts = templateUrl.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
|
57
|
-
const actualParts = actualUrl.pathname.split("/").filter(Boolean);
|
|
58
|
-
if (templateParts.length === actualParts.length) {
|
|
59
|
-
let compatible = true;
|
|
60
|
-
for (let i = 0; i < templateParts.length; i++) {
|
|
61
|
-
const templatePart = templateParts[i];
|
|
62
|
-
const actualPart = actualParts[i];
|
|
63
|
-
const placeholder = templatePart.match(/^\{([^}]+)\}$/)?.[1];
|
|
64
|
-
if (placeholder) {
|
|
65
|
-
out[placeholder] = decodeURIComponent(actualPart);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (templatePart !== actualPart) {
|
|
69
|
-
compatible = false;
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (!compatible) {
|
|
74
|
-
for (const key of Object.keys(out)) delete out[key];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const [key, value] of templateUrl.searchParams.entries()) {
|
|
79
|
-
const placeholder = value.match(/^\{([^}]+)\}$/)?.[1];
|
|
80
|
-
if (!placeholder) continue;
|
|
81
|
-
const actualValue = actualUrl.searchParams.get(key);
|
|
82
|
-
if (actualValue != null && actualValue !== "") {
|
|
83
|
-
out[placeholder] = actualValue;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
} catch {
|
|
88
|
-
return {};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function deriveSemanticTemplateParams(
|
|
93
|
-
urlTemplate: string,
|
|
94
|
-
contextUrl?: string,
|
|
95
|
-
): Record<string, string> {
|
|
96
|
-
if (!contextUrl) return {};
|
|
97
|
-
try {
|
|
98
|
-
const templateNames = [...urlTemplate.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]).filter(Boolean);
|
|
99
|
-
if (templateNames.length === 0) return {};
|
|
100
|
-
const actualUrl = new URL(contextUrl);
|
|
101
|
-
const actualParts = actualUrl.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
|
102
|
-
const lowerParts = actualParts.map((part) => part.toLowerCase());
|
|
103
|
-
const out: Record<string, string> = {};
|
|
104
|
-
|
|
105
|
-
for (const name of templateNames) {
|
|
106
|
-
const lower = name.toLowerCase();
|
|
107
|
-
if (lower === "tag" || lower === "tags") {
|
|
108
|
-
const prefixIndex = lowerParts.findIndex((part) => part === "t" || part === "tag" || part === "tags");
|
|
109
|
-
if (prefixIndex >= 0 && actualParts[prefixIndex + 1]) out[name] = actualParts[prefixIndex + 1]!;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return out;
|
|
114
|
-
} catch {
|
|
115
|
-
return {};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function mergeContextTemplateParams(
|
|
120
|
-
params: Record<string, unknown>,
|
|
121
|
-
urlTemplate: string,
|
|
122
|
-
contextUrl?: string,
|
|
123
|
-
): Record<string, unknown> {
|
|
124
|
-
const merged: Record<string, unknown> = { ...params };
|
|
125
|
-
const inferred = deriveTemplateParamsFromContextUrl(urlTemplate, contextUrl);
|
|
126
|
-
for (const [key, value] of Object.entries(inferred)) {
|
|
127
|
-
if (merged[key] == null || merged[key] === "") merged[key] = value;
|
|
128
|
-
}
|
|
129
|
-
for (const [key, value] of Object.entries(deriveSemanticTemplateParams(urlTemplate, contextUrl))) {
|
|
130
|
-
if (merged[key] == null || merged[key] === "") merged[key] = value;
|
|
131
|
-
}
|
|
132
|
-
for (const [rawKey, placeholder] of Object.entries(extractTemplateQueryBindings(urlTemplate))) {
|
|
133
|
-
if ((merged[placeholder] == null || merged[placeholder] === "") && merged[rawKey] != null && merged[rawKey] !== "") {
|
|
134
|
-
merged[placeholder] = merged[rawKey];
|
|
135
|
-
}
|
|
136
|
-
if ((merged[rawKey] == null || merged[rawKey] === "") && merged[placeholder] != null && merged[placeholder] !== "") {
|
|
137
|
-
merged[rawKey] = merged[placeholder];
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return merged;
|
|
141
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { DriftResult, ResponseSchema } from "../types/index.js";
|
|
2
|
-
import { inferSchema } from "./index.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Collect all field paths from a schema (dot-notation).
|
|
6
|
-
*/
|
|
7
|
-
function collectPaths(schema: ResponseSchema, prefix = ""): Map<string, string> {
|
|
8
|
-
const paths = new Map<string, string>();
|
|
9
|
-
if (schema.type === "object" && schema.properties) {
|
|
10
|
-
for (const [key, val] of Object.entries(schema.properties)) {
|
|
11
|
-
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
12
|
-
paths.set(fullPath, val.type);
|
|
13
|
-
if (val.type === "object" && val.properties) {
|
|
14
|
-
for (const [p, t] of collectPaths(val, fullPath)) {
|
|
15
|
-
paths.set(p, t);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (val.type === "array" && val.items?.type === "object") {
|
|
19
|
-
for (const [p, t] of collectPaths(val.items, `${fullPath}[]`)) {
|
|
20
|
-
paths.set(p, t);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return paths;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Compare stored schema against a fresh response sample.
|
|
30
|
-
* Returns added/removed fields and type changes.
|
|
31
|
-
*/
|
|
32
|
-
export function detectSchemaDrift(existing: ResponseSchema, newSample: unknown): DriftResult {
|
|
33
|
-
const newSchema = inferSchema([newSample]);
|
|
34
|
-
const oldPaths = collectPaths(existing);
|
|
35
|
-
const newPaths = collectPaths(newSchema);
|
|
36
|
-
|
|
37
|
-
const added_fields: string[] = [];
|
|
38
|
-
const removed_fields: string[] = [];
|
|
39
|
-
const type_changes: Array<{ path: string; was: string; now: string }> = [];
|
|
40
|
-
|
|
41
|
-
for (const [path, type] of newPaths) {
|
|
42
|
-
if (!oldPaths.has(path)) {
|
|
43
|
-
added_fields.push(path);
|
|
44
|
-
} else if (oldPaths.get(path) !== type) {
|
|
45
|
-
type_changes.push({ path, was: oldPaths.get(path)!, now: type });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
for (const path of oldPaths.keys()) {
|
|
49
|
-
if (!newPaths.has(path)) {
|
|
50
|
-
removed_fields.push(path);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
drifted: added_fields.length > 0 || removed_fields.length > 0 || type_changes.length > 0,
|
|
56
|
-
added_fields,
|
|
57
|
-
removed_fields,
|
|
58
|
-
type_changes,
|
|
59
|
-
};
|
|
60
|
-
}
|