@aryasaatvik/motel 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/AGENTS.md +180 -0
  2. package/LICENSE +21 -0
  3. package/README.md +137 -0
  4. package/package.json +119 -0
  5. package/skills/motel-debug/SKILL.md +203 -0
  6. package/skills/motel-debug/references/effect.md +38 -0
  7. package/src/App.tsx +414 -0
  8. package/src/StartupGate.test.ts +80 -0
  9. package/src/StartupGate.tsx +263 -0
  10. package/src/cli.ts +205 -0
  11. package/src/config.ts +45 -0
  12. package/src/daemon.test.ts +547 -0
  13. package/src/daemon.ts +486 -0
  14. package/src/domain.test.ts +62 -0
  15. package/src/domain.ts +291 -0
  16. package/src/httpApi.ts +391 -0
  17. package/src/httpListPolicy.test.ts +76 -0
  18. package/src/httpListPolicy.ts +129 -0
  19. package/src/index.tsx +25 -0
  20. package/src/instructions.ts +72 -0
  21. package/src/launchAgent.test.ts +430 -0
  22. package/src/launchAgent.ts +462 -0
  23. package/src/localServer.ts +568 -0
  24. package/src/locator.ts +138 -0
  25. package/src/mcp.ts +433 -0
  26. package/src/motel.test.ts +176 -0
  27. package/src/motel.ts +88 -0
  28. package/src/motelClient.ts +353 -0
  29. package/src/opentui-jsx.d.ts +11 -0
  30. package/src/otlp.test.ts +65 -0
  31. package/src/otlp.ts +162 -0
  32. package/src/otlpProtobuf.ts +35 -0
  33. package/src/queryFilters.ts +39 -0
  34. package/src/registry.ts +115 -0
  35. package/src/runtime.ts +40 -0
  36. package/src/server.ts +11 -0
  37. package/src/services/AsyncIngest.ts +82 -0
  38. package/src/services/LogQueryService.ts +29 -0
  39. package/src/services/TelemetryQuery.ts +62 -0
  40. package/src/services/TelemetryStore.ts +2554 -0
  41. package/src/services/TraceQueryService.ts +43 -0
  42. package/src/services/ingestRpc.ts +39 -0
  43. package/src/services/queryRpc.ts +15 -0
  44. package/src/services/telemetryQueryWorker.ts +32 -0
  45. package/src/services/telemetryWorker.ts +59 -0
  46. package/src/startupBench.ts +19 -0
  47. package/src/startupLifecycle.ts +57 -0
  48. package/src/storybook/aiChatStory.tsx +244 -0
  49. package/src/storybook/fixtures/errorState.ts +44 -0
  50. package/src/storybook/fixtures/imagePaste.ts +34 -0
  51. package/src/storybook/fixtures/index.ts +62 -0
  52. package/src/storybook/fixtures/kitchenSink.ts +148 -0
  53. package/src/storybook/fixtures/rawPrompt.ts +15 -0
  54. package/src/storybook/fixtures/short.ts +27 -0
  55. package/src/storybook/fixtures/toolHeavy.ts +65 -0
  56. package/src/telemetry.test.ts +1053 -0
  57. package/src/ui/AiChatView.tsx +308 -0
  58. package/src/ui/AttrFilterModal.tsx +120 -0
  59. package/src/ui/ServiceLogs.tsx +115 -0
  60. package/src/ui/SpanContentView.tsx +184 -0
  61. package/src/ui/SpanDetail.tsx +215 -0
  62. package/src/ui/SpanDetailFull.tsx +224 -0
  63. package/src/ui/SpanDetailPane.tsx +90 -0
  64. package/src/ui/TraceDetailsPane.tsx +175 -0
  65. package/src/ui/TraceList.tsx +254 -0
  66. package/src/ui/Waterfall.tsx +389 -0
  67. package/src/ui/aiChatModel.test.ts +391 -0
  68. package/src/ui/aiChatModel.ts +773 -0
  69. package/src/ui/aiState.ts +59 -0
  70. package/src/ui/app/TraceListPane.tsx +39 -0
  71. package/src/ui/app/TraceWorkspace.tsx +452 -0
  72. package/src/ui/app/useAppLayout.ts +101 -0
  73. package/src/ui/app/useTraceScreenData.ts +590 -0
  74. package/src/ui/atoms.ts +131 -0
  75. package/src/ui/cachedLoader.test.ts +23 -0
  76. package/src/ui/cachedLoader.ts +60 -0
  77. package/src/ui/filterParser.test.ts +56 -0
  78. package/src/ui/filterParser.ts +45 -0
  79. package/src/ui/format.ts +128 -0
  80. package/src/ui/loaders.ts +101 -0
  81. package/src/ui/persistence.ts +41 -0
  82. package/src/ui/primitives.tsx +198 -0
  83. package/src/ui/state.ts +6 -0
  84. package/src/ui/theme.ts +160 -0
  85. package/src/ui/traceDetailsWidth.repro.test.ts +126 -0
  86. package/src/ui/traceSortNav.repro.seed.ts +62 -0
  87. package/src/ui/traceSortNav.repro.test.ts +231 -0
  88. package/src/ui/useAttrFilterPicker.ts +89 -0
  89. package/src/ui/useKeyboardNav.ts +1023 -0
  90. package/src/ui/waterfallFilter.test.ts +84 -0
  91. package/src/ui/waterfallFilter.ts +59 -0
  92. package/src/ui/waterfallModel.ts +130 -0
  93. package/src/ui/waterfallNav.repro.seed.ts +86 -0
  94. package/src/ui/waterfallNav.repro.test.ts +271 -0
  95. package/src/ui/waterfallNav.test.ts +453 -0
  96. package/src/ui/waterfallNav.ts +75 -0
  97. package/web/dist/assets/index-B01z9BaO.css +2 -0
  98. package/web/dist/assets/index-CYlBaY9O.js +22 -0
  99. package/web/dist/index.html +13 -0
package/src/domain.ts ADDED
@@ -0,0 +1,291 @@
1
+ import { Schema } from "effect"
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Reusable helpers
5
+ // ---------------------------------------------------------------------------
6
+
7
+ const StringRecord = Schema.Record(Schema.String, Schema.String)
8
+
9
+ const DateFromString = Schema.DateFromString
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Core telemetry domain types (Schema is the single source of truth)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export const TraceSpanStatus = Schema.Literals(["ok", "error"])
16
+ export type TraceSpanStatus = typeof TraceSpanStatus.Type
17
+
18
+ export const TraceSpanEvent = Schema.Struct({
19
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Event name" })),
20
+ timestamp: DateFromString.pipe(Schema.annotateKey({ description: "ISO 8601 timestamp" })),
21
+ attributes: StringRecord.pipe(Schema.annotateKey({ description: "Key-value attributes attached to the event" })),
22
+ }).annotate({ identifier: "TraceSpanEvent" })
23
+
24
+ export type TraceSpanEvent = typeof TraceSpanEvent.Type
25
+
26
+ export const TraceSpanItem = Schema.Struct({
27
+ spanId: Schema.String,
28
+ parentSpanId: Schema.NullOr(Schema.String),
29
+ serviceName: Schema.String,
30
+ scopeName: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Instrumentation scope (e.g. module or library name)" })),
31
+ kind: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Span kind: client, server, producer, consumer, or internal" })),
32
+ operationName: Schema.String.pipe(Schema.annotateKey({ description: "The operation this span represents (e.g. HTTP handler, DB query)" })),
33
+ startTime: DateFromString.pipe(Schema.annotateKey({ description: "ISO 8601 timestamp" })),
34
+ isRunning: Schema.Boolean.pipe(Schema.annotateKey({ description: "True when the span has not reported an end timestamp yet" })),
35
+ durationMs: Schema.Number.pipe(Schema.annotateKey({ description: "Wall-clock duration in milliseconds" })),
36
+ status: TraceSpanStatus.pipe(Schema.annotateKey({ description: "ok or error" })),
37
+ depth: Schema.Number.pipe(Schema.annotateKey({ description: "Nesting depth in the span tree (root = 0)" })),
38
+ tags: StringRecord.pipe(Schema.annotateKey({ description: "Span attributes as key-value pairs" })),
39
+ warnings: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Structural warnings (e.g. missing parent span)" })),
40
+ events: Schema.Array(TraceSpanEvent),
41
+ }).annotate({ identifier: "TraceSpan" })
42
+
43
+ export type TraceSpanItem = typeof TraceSpanItem.Type
44
+
45
+ export const TraceItem = Schema.Struct({
46
+ traceId: Schema.String,
47
+ serviceName: Schema.String.pipe(Schema.annotateKey({ description: "Service that owns the root span" })),
48
+ rootOperationName: Schema.String.pipe(Schema.annotateKey({ description: "Operation name of the root span" })),
49
+ startedAt: DateFromString.pipe(Schema.annotateKey({ description: "ISO 8601 timestamp of the earliest span" })),
50
+ isRunning: Schema.Boolean.pipe(Schema.annotateKey({ description: "True when any span in the trace is still open" })),
51
+ durationMs: Schema.Number.pipe(Schema.annotateKey({ description: "End-to-end trace duration in milliseconds" })),
52
+ spanCount: Schema.Number,
53
+ errorCount: Schema.Number.pipe(Schema.annotateKey({ description: "Number of spans with status=error" })),
54
+ warnings: Schema.Array(Schema.String),
55
+ spans: Schema.Array(TraceSpanItem).pipe(Schema.annotateKey({ description: "Spans ordered by parent-child hierarchy, depth-first" })),
56
+ }).annotate({ identifier: "Trace" })
57
+
58
+ export type TraceItem = typeof TraceItem.Type
59
+
60
+ export const TraceSummaryItem = Schema.Struct({
61
+ traceId: Schema.String,
62
+ serviceName: Schema.String,
63
+ rootOperationName: Schema.String,
64
+ startedAt: DateFromString.pipe(Schema.annotateKey({ description: "ISO 8601 timestamp" })),
65
+ isRunning: Schema.Boolean,
66
+ durationMs: Schema.Number,
67
+ spanCount: Schema.Number,
68
+ errorCount: Schema.Number,
69
+ warnings: Schema.Array(Schema.String),
70
+ }).annotate({ identifier: "TraceSummary" })
71
+
72
+ export type TraceSummaryItem = typeof TraceSummaryItem.Type
73
+
74
+ export const SpanItem = Schema.Struct({
75
+ traceId: Schema.String,
76
+ rootOperationName: Schema.String.pipe(Schema.annotateKey({ description: "Operation name of the trace's root span, for context" })),
77
+ parentOperationName: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Parent span operation name, if present" })),
78
+ span: TraceSpanItem,
79
+ }).annotate({ identifier: "SpanWithContext" })
80
+
81
+ export type SpanItem = typeof SpanItem.Type
82
+
83
+ export const LogItem = Schema.Struct({
84
+ id: Schema.String,
85
+ timestamp: DateFromString.pipe(Schema.annotateKey({ description: "ISO 8601 timestamp" })),
86
+ serviceName: Schema.String,
87
+ severityText: Schema.String.pipe(Schema.annotateKey({ description: "Log level: TRACE, DEBUG, INFO, WARN, ERROR, FATAL" })),
88
+ body: Schema.String.pipe(Schema.annotateKey({ description: "Log message body" })),
89
+ traceId: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Associated trace ID, if the log was emitted inside a span" })),
90
+ spanId: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Associated span ID, if the log was emitted inside a span" })),
91
+ scopeName: Schema.NullOr(Schema.String),
92
+ attributes: StringRecord.pipe(Schema.annotateKey({ description: "Merged resource + log attributes as key-value pairs" })),
93
+ }).annotate({ identifier: "Log" })
94
+
95
+ export type LogItem = typeof LogItem.Type
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Shared query result types
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export const FacetItem = Schema.Struct({
102
+ value: Schema.String.pipe(Schema.annotateKey({ description: "Distinct value for the faceted field" })),
103
+ count: Schema.Number.pipe(Schema.annotateKey({ description: "Number of occurrences" })),
104
+ }).annotate({ identifier: "Facet" })
105
+
106
+ export type FacetItem = typeof FacetItem.Type
107
+
108
+ export const StatsItem = Schema.Struct({
109
+ group: Schema.String.pipe(Schema.annotateKey({ description: "Grouping key" })),
110
+ value: Schema.Number.pipe(Schema.annotateKey({ description: "Aggregate value for the chosen metric" })),
111
+ count: Schema.Number.pipe(Schema.annotateKey({ description: "Number of samples in the group" })),
112
+ }).annotate({ identifier: "Stat" })
113
+
114
+ export type StatsItem = typeof StatsItem.Type
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // AI Call types
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /** Maps normalized field names to the AI SDK attribute keys in span_attributes */
121
+ export const AI_ATTR_MAP = {
122
+ operationId: "ai.operationId",
123
+ functionId: "ai.telemetry.functionId",
124
+ provider: "ai.model.provider",
125
+ model: "ai.model.id",
126
+ sessionId: "ai.telemetry.metadata.sessionId",
127
+ userId: "ai.telemetry.metadata.userId",
128
+ finishReason: "ai.response.finishReason",
129
+ inputTokens: "ai.usage.inputTokens",
130
+ outputTokens: "ai.usage.outputTokens",
131
+ totalTokens: "ai.usage.totalTokens",
132
+ cachedInputTokens: "ai.usage.cachedInputTokens",
133
+ reasoningTokens: "ai.usage.reasoningTokens",
134
+ msToFirstChunk: "ai.response.msToFirstChunk",
135
+ msToFinish: "ai.response.msToFinish",
136
+ avgOutputTokensPerSecond: "ai.response.avgOutputTokensPerSecond",
137
+ promptMessages: "ai.prompt.messages",
138
+ prompt: "ai.prompt",
139
+ responseText: "ai.response.text",
140
+ tools: "ai.prompt.tools",
141
+ toolChoice: "ai.prompt.toolChoice",
142
+ providerMetadata: "ai.response.providerMetadata",
143
+ responseModel: "ai.response.model",
144
+ responseId: "ai.response.id",
145
+ responseTimestamp: "ai.response.timestamp",
146
+ } as const
147
+
148
+ /**
149
+ * Attribute keys that carry LLM prompt/response content and should be
150
+ * indexed in the span-attribute FTS table. These are the keys emitted by
151
+ * well-known LLM instrumentation conventions:
152
+ *
153
+ * - **Vercel AI SDK** (`ai.*`): rich, SDK-specific attributes captured by
154
+ * `experimental_telemetry` on `generateText` / `streamText` / `generateObject`.
155
+ * - **OpenTelemetry GenAI semantic conventions** (`gen_ai.*`): the
156
+ * cross-vendor standard. The singular `prompt`/`completion` attrs are
157
+ * deprecated in favor of event-based capture but are still emitted by
158
+ * most instrumentations, so we keep them.
159
+ * - **OpenInference** (`input.value` / `output.value`): Arize Phoenix /
160
+ * LangChain-style normalized input/output.
161
+ *
162
+ * Keys here trigger FTS indexing on insert via a trigger in TelemetryStore.
163
+ * Adding a key requires a one-time backfill; removing one leaves orphan
164
+ * FTS entries that get cleaned up on next retention pass.
165
+ */
166
+ export const AI_FTS_KEYS = [
167
+ // Vercel AI SDK
168
+ "ai.prompt",
169
+ "ai.prompt.messages",
170
+ "ai.prompt.tools",
171
+ "ai.prompt.toolChoice",
172
+ "ai.response.text",
173
+ "ai.response.toolCalls",
174
+ "ai.response.reasoning",
175
+ "ai.response.object",
176
+ "ai.toolCall.args",
177
+ "ai.toolCall.result",
178
+ // OpenTelemetry GenAI semantic conventions
179
+ "gen_ai.prompt",
180
+ "gen_ai.completion",
181
+ "gen_ai.input.messages",
182
+ "gen_ai.output.messages",
183
+ "gen_ai.system_instructions",
184
+ "gen_ai.tool.definitions",
185
+ "gen_ai.tool.message.content",
186
+ // OpenInference (Phoenix, LangChain, etc.)
187
+ "input.value",
188
+ "output.value",
189
+ ] as const
190
+
191
+ /**
192
+ * Back-compat alias. The `text` filter on `/api/ai/calls` historically
193
+ * LIKE-searched these four keys; now FTS indexes the broader AI_FTS_KEYS
194
+ * set so the filter transparently covers more content.
195
+ */
196
+ export const AI_TEXT_SEARCH_KEYS = AI_FTS_KEYS
197
+
198
+ /**
199
+ * True if a span's tags contain any of the AI content keys we track.
200
+ * Used as the single source of truth for "this span has LLM payloads
201
+ * worth a specialized view" — drives the ✦ marker in the waterfall row
202
+ * and picks the chat-flavored renderer when the user drills into the
203
+ * span's detail. Scanning happens once per row during render so this
204
+ * needs to stay O(AI_FTS_KEYS.length) with cheap `in` checks rather
205
+ * than an `Object.keys(...).some(...)` allocation.
206
+ */
207
+ export const isAiSpan = (tags: Readonly<Record<string, string>>): boolean => {
208
+ for (const key of AI_FTS_KEYS) {
209
+ if (key in tags) return true
210
+ }
211
+ return false
212
+ }
213
+
214
+ const PREVIEW_LENGTH = 200
215
+
216
+ export const truncatePreview = (value: string | null | undefined): string | null => {
217
+ if (!value) return null
218
+ if (value.length <= PREVIEW_LENGTH) return value
219
+ return value.slice(0, PREVIEW_LENGTH) + "..."
220
+ }
221
+
222
+ export const AiUsage = Schema.Struct({
223
+ inputTokens: Schema.NullOr(Schema.Number),
224
+ outputTokens: Schema.NullOr(Schema.Number),
225
+ totalTokens: Schema.NullOr(Schema.Number),
226
+ cachedInputTokens: Schema.NullOr(Schema.Number),
227
+ reasoningTokens: Schema.NullOr(Schema.Number),
228
+ }).annotate({ identifier: "AiUsage" })
229
+
230
+ export type AiUsage = typeof AiUsage.Type
231
+
232
+ export const AiCallSummary = Schema.Struct({
233
+ traceId: Schema.String,
234
+ spanId: Schema.String,
235
+ operation: Schema.String.pipe(Schema.annotateKey({ description: "AI operation: streamText, generateText, streamObject, etc." })),
236
+ service: Schema.String,
237
+ functionId: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "ai.telemetry.functionId" })),
238
+ provider: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "ai.model.provider (e.g. openai.responses)" })),
239
+ model: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "ai.model.id (e.g. gpt-5.4)" })),
240
+ status: TraceSpanStatus,
241
+ startedAt: Schema.String.pipe(Schema.annotateKey({ description: "ISO 8601 timestamp" })),
242
+ durationMs: Schema.Number,
243
+ sessionId: Schema.NullOr(Schema.String),
244
+ userId: Schema.NullOr(Schema.String),
245
+ promptPreview: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "First ~200 chars of prompt content" })),
246
+ responsePreview: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "First ~200 chars of response text" })),
247
+ finishReason: Schema.NullOr(Schema.String),
248
+ toolCallCount: Schema.Number.pipe(Schema.annotateKey({ description: "Number of tool call child spans" })),
249
+ usage: Schema.NullOr(AiUsage),
250
+ }).annotate({ identifier: "AiCallSummary" })
251
+
252
+ export type AiCallSummary = typeof AiCallSummary.Type
253
+
254
+ export const AiToolCall = Schema.Struct({
255
+ name: Schema.String,
256
+ spanId: Schema.NullOr(Schema.String),
257
+ status: TraceSpanStatus,
258
+ durationMs: Schema.NullOr(Schema.Number),
259
+ }).annotate({ identifier: "AiToolCall" })
260
+
261
+ export type AiToolCall = typeof AiToolCall.Type
262
+
263
+ export const AiCallDetail = Schema.Struct({
264
+ traceId: Schema.String,
265
+ spanId: Schema.String,
266
+ operation: Schema.String,
267
+ service: Schema.String,
268
+ functionId: Schema.NullOr(Schema.String),
269
+ provider: Schema.NullOr(Schema.String),
270
+ model: Schema.NullOr(Schema.String),
271
+ status: TraceSpanStatus,
272
+ startedAt: Schema.String,
273
+ durationMs: Schema.Number,
274
+ sessionId: Schema.NullOr(Schema.String),
275
+ userId: Schema.NullOr(Schema.String),
276
+ finishReason: Schema.NullOr(Schema.String),
277
+ promptMessages: Schema.NullOr(Schema.Unknown).pipe(Schema.annotateKey({ description: "Full parsed ai.prompt.messages or ai.prompt" })),
278
+ responseText: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Full ai.response.text" })),
279
+ toolCalls: Schema.Array(AiToolCall),
280
+ toolsAvailable: Schema.NullOr(Schema.Unknown).pipe(Schema.annotateKey({ description: "Full ai.prompt.tools" })),
281
+ providerMetadata: Schema.NullOr(Schema.Unknown),
282
+ usage: Schema.NullOr(AiUsage),
283
+ timing: Schema.Struct({
284
+ msToFirstChunk: Schema.NullOr(Schema.Number),
285
+ msToFinish: Schema.NullOr(Schema.Number),
286
+ avgOutputTokensPerSecond: Schema.NullOr(Schema.Number),
287
+ }),
288
+ logs: Schema.Array(Schema.Unknown).pipe(Schema.annotateKey({ description: "Correlated log records" })),
289
+ }).annotate({ identifier: "AiCallDetail" })
290
+
291
+ export type AiCallDetail = typeof AiCallDetail.Type