@shogo-ai/core 1.10.0 → 2.4.1

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/index.cjs CHANGED
@@ -20,19 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BINARY_FILE_BASENAMES: () => BINARY_FILE_BASENAMES,
23
24
  BINARY_FILE_EXTENSIONS: () => BINARY_FILE_EXTENSIONS,
25
+ BINARY_FILE_TRANSIENT_SUFFIXES: () => BINARY_FILE_TRANSIENT_SUFFIXES,
24
26
  StreamBufferStore: () => StreamBufferStore,
25
27
  TECH_STACK_REGISTRY: () => TECH_STACK_REGISTRY,
28
+ TURN_COMPLETE_EVENT_TYPE: () => TURN_COMPLETE_EVENT_TYPE,
29
+ TURN_SEQ_EVENT_TYPE: () => TURN_SEQ_EVENT_TYPE,
30
+ compactSseFrames: () => compactSseFrames,
26
31
  createBufferingTransform: () => createBufferingTransform,
27
32
  createLogger: () => createLogger,
33
+ emitLogToSink: () => emitLogToSink,
34
+ encodeTurnCompleteFrame: () => encodeTurnCompleteFrame,
35
+ encodeTurnSeqFrame: () => encodeTurnSeqFrame,
28
36
  extractUserText: () => extractUserText,
29
37
  findLastUserMessage: () => findLastUserMessage,
38
+ getDeclaredPorts: () => getDeclaredPorts,
39
+ getMinimumInstanceSize: () => getMinimumInstanceSize,
30
40
  getStackEntry: () => getStackEntry,
31
41
  initInstrumentation: () => initInstrumentation,
42
+ isBinaryBuffer: () => isBinaryBuffer,
32
43
  isBinaryFilePath: () => isBinaryFilePath,
44
+ isDockerTechStack: () => isDockerTechStack,
33
45
  isMacOSJunkName: () => isMacOSJunkName,
34
46
  isMacOSJunkPath: () => isMacOSJunkPath,
35
47
  isMobileTechStack: () => isMobileTechStack,
48
+ setOtelLogSink: () => setOtelLogSink,
49
+ setTraceContextProvider: () => setTraceContextProvider,
36
50
  shutdownInstrumentation: () => shutdownInstrumentation,
37
51
  stackSeedsItself: () => stackSeedsItself,
38
52
  traceOperation: () => traceOperation,
@@ -60,6 +74,21 @@ function formatEntry(entry) {
60
74
  const extraStr = Object.keys(extra).length > 0 ? " " + JSON.stringify(extra) : "";
61
75
  return `[${service}] ${msg}${extraStr}`;
62
76
  }
77
+ var logSink = null;
78
+ function setOtelLogSink(sink) {
79
+ logSink = sink;
80
+ }
81
+ function emitLogToSink(entry) {
82
+ if (!logSink) return;
83
+ try {
84
+ logSink({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
85
+ } catch {
86
+ }
87
+ }
88
+ var traceContextProvider = null;
89
+ function setTraceContextProvider(provider) {
90
+ traceContextProvider = provider;
91
+ }
63
92
  function createLogger(service, defaultExtra) {
64
93
  function log(level, msg, extra) {
65
94
  if (!shouldLog(level)) return;
@@ -71,6 +100,16 @@ function createLogger(service, defaultExtra) {
71
100
  ...defaultExtra,
72
101
  ...extra
73
102
  };
103
+ if (traceContextProvider) {
104
+ try {
105
+ const tc = traceContextProvider();
106
+ if (tc) {
107
+ entry.trace_id = tc.trace_id;
108
+ entry.span_id = tc.span_id;
109
+ }
110
+ } catch {
111
+ }
112
+ }
74
113
  const formatted = formatEntry(entry);
75
114
  switch (level) {
76
115
  case "error":
@@ -83,6 +122,12 @@ function createLogger(service, defaultExtra) {
83
122
  console.log(formatted);
84
123
  break;
85
124
  }
125
+ if (logSink) {
126
+ try {
127
+ logSink(entry);
128
+ } catch {
129
+ }
130
+ }
86
131
  }
87
132
  return {
88
133
  debug: (msg, extra) => log("debug", msg, extra),
@@ -95,13 +140,23 @@ function createLogger(service, defaultExtra) {
95
140
 
96
141
  // src/instrumentation.ts
97
142
  var import_api = require("@opentelemetry/api");
143
+ var import_api_logs = require("@opentelemetry/api-logs");
98
144
  var import_sdk_node = require("@opentelemetry/sdk-node");
99
145
  var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
146
+ var import_exporter_logs_otlp_http = require("@opentelemetry/exporter-logs-otlp-http");
100
147
  var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
148
+ var import_sdk_logs = require("@opentelemetry/sdk-logs");
101
149
  var import_resources = require("@opentelemetry/resources");
102
150
  var import_semantic_conventions = require("@opentelemetry/semantic-conventions");
151
+ var SEVERITY_BY_LEVEL = {
152
+ debug: { number: import_api_logs.SeverityNumber.DEBUG, text: "DEBUG" },
153
+ info: { number: import_api_logs.SeverityNumber.INFO, text: "INFO" },
154
+ warn: { number: import_api_logs.SeverityNumber.WARN, text: "WARN" },
155
+ error: { number: import_api_logs.SeverityNumber.ERROR, text: "ERROR" }
156
+ };
103
157
  var sdk = null;
104
158
  function initInstrumentation(config) {
159
+ if (sdk) return;
105
160
  const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
106
161
  if (!endpoint) {
107
162
  console.log(`[OTEL] Tracing disabled for ${config.serviceName} (OTEL_EXPORTER_OTLP_ENDPOINT not set)`);
@@ -115,18 +170,43 @@ function initInstrumentation(config) {
115
170
  }
116
171
  const serviceName = process.env.OTEL_SERVICE_NAME || config.serviceName;
117
172
  console.log(`[OTEL] Configuring ${serviceName} \u2192 ${endpoint}/v1/traces`);
173
+ const exportTimeoutMs = Number(process.env.OTEL_EXPORTER_TIMEOUT_MS ?? 3e3);
118
174
  let traceExporter;
119
175
  try {
120
176
  traceExporter = new import_exporter_trace_otlp_http.OTLPTraceExporter({
121
177
  url: `${endpoint}/v1/traces`,
122
- headers
178
+ headers,
179
+ timeoutMillis: exportTimeoutMs
123
180
  });
124
181
  } catch (err) {
125
182
  console.error(`[OTEL] Failed to create exporter: ${err.message}`);
126
183
  return;
127
184
  }
128
185
  const isProduction2 = process.env.NODE_ENV === "production" || process.env.NODE_ENV === "staging";
129
- const spanProcessor = isProduction2 ? new import_sdk_trace_base.BatchSpanProcessor(traceExporter) : new import_sdk_trace_base.SimpleSpanProcessor(traceExporter);
186
+ const spanProcessor = isProduction2 ? new import_sdk_trace_base.BatchSpanProcessor(traceExporter, {
187
+ maxQueueSize: 2048,
188
+ maxExportBatchSize: 512,
189
+ scheduledDelayMillis: 5e3,
190
+ exportTimeoutMillis: exportTimeoutMs
191
+ }) : new import_sdk_trace_base.SimpleSpanProcessor(traceExporter);
192
+ let logRecordProcessors = [];
193
+ try {
194
+ const logExporter = new import_exporter_logs_otlp_http.OTLPLogExporter({
195
+ url: `${endpoint}/v1/logs`,
196
+ headers,
197
+ timeoutMillis: exportTimeoutMs
198
+ });
199
+ logRecordProcessors = [
200
+ isProduction2 ? new import_sdk_logs.BatchLogRecordProcessor(logExporter, {
201
+ maxQueueSize: 2048,
202
+ maxExportBatchSize: 512,
203
+ scheduledDelayMillis: 5e3,
204
+ exportTimeoutMillis: exportTimeoutMs
205
+ }) : new import_sdk_logs.SimpleLogRecordProcessor(logExporter)
206
+ ];
207
+ } catch (err) {
208
+ console.warn(`[OTEL] Failed to create log exporter for ${serviceName}: ${err.message}`);
209
+ }
130
210
  sdk = new import_sdk_node.NodeSDK({
131
211
  resource: (0, import_resources.resourceFromAttributes)({
132
212
  [import_semantic_conventions.ATTR_SERVICE_NAME]: serviceName,
@@ -134,6 +214,7 @@ function initInstrumentation(config) {
134
214
  "deployment.environment.name": process.env.NODE_ENV || "development"
135
215
  }),
136
216
  spanProcessors: [spanProcessor],
217
+ logRecordProcessors,
137
218
  instrumentations: []
138
219
  });
139
220
  try {
@@ -144,6 +225,24 @@ function initInstrumentation(config) {
144
225
  sdk = null;
145
226
  return;
146
227
  }
228
+ setTraceContextProvider(() => {
229
+ const ctx = import_api.trace.getActiveSpan()?.spanContext();
230
+ return ctx && (0, import_api.isSpanContextValid)(ctx) ? { trace_id: ctx.traceId, span_id: ctx.spanId } : null;
231
+ });
232
+ if (logRecordProcessors.length > 0) {
233
+ const otelLogger = import_api_logs.logs.getLogger(serviceName);
234
+ setOtelLogSink((entry) => {
235
+ const { level, msg, service, timestamp, ...attrs } = entry;
236
+ const severity = SEVERITY_BY_LEVEL[level] ?? SEVERITY_BY_LEVEL.info;
237
+ otelLogger.emit({
238
+ severityNumber: severity.number,
239
+ severityText: severity.text,
240
+ body: msg,
241
+ attributes: { service, ...attrs }
242
+ });
243
+ });
244
+ console.log(`[OTEL] Logs exporter enabled for ${serviceName} \u2192 ${endpoint}/v1/logs`);
245
+ }
147
246
  try {
148
247
  const tracer = import_api.trace.getTracer(serviceName);
149
248
  const testSpan = tracer.startSpan("otel.startup-test", {
@@ -164,13 +263,24 @@ function initInstrumentation(config) {
164
263
  }
165
264
  }
166
265
  async function shutdownInstrumentation() {
167
- if (sdk) {
168
- try {
169
- await sdk.shutdown();
170
- console.log("[OTEL] Tracing shut down");
171
- } catch (err) {
172
- console.error("[OTEL] Error shutting down:", err.message);
173
- }
266
+ if (!sdk) return;
267
+ setOtelLogSink(null);
268
+ setTraceContextProvider(null);
269
+ const shutdownTimeoutMs = Number(process.env.OTEL_SHUTDOWN_TIMEOUT_MS ?? 2e3);
270
+ let timer;
271
+ try {
272
+ await Promise.race([
273
+ sdk.shutdown(),
274
+ new Promise((resolve) => {
275
+ timer = setTimeout(resolve, shutdownTimeoutMs);
276
+ })
277
+ ]);
278
+ console.log("[OTEL] Tracing shut down");
279
+ } catch (err) {
280
+ console.error("[OTEL] Error shutting down:", err.message);
281
+ } finally {
282
+ if (timer) clearTimeout(timer);
283
+ sdk = null;
174
284
  }
175
285
  }
176
286
  function traceOperation(tracerName, spanName, attributes, fn) {
@@ -190,13 +300,126 @@ function traceOperation(tracerName, spanName, attributes, fn) {
190
300
  });
191
301
  }
192
302
 
303
+ // src/stream-compaction.ts
304
+ var TURN_SEQ_EVENT_TYPE = "data-turn-seq";
305
+ var DELTA_PART = {
306
+ "text-delta": "text",
307
+ "reasoning-delta": "reasoning"
308
+ };
309
+ var PART_BOUNDARY = {
310
+ "text-end": "text",
311
+ "reasoning-end": "reasoning",
312
+ "text-start": "text",
313
+ "reasoning-start": "reasoning"
314
+ };
315
+ function compactSseFrames(frames) {
316
+ if (frames.length === 0) return new Uint8Array(0);
317
+ const decoder = new TextDecoder();
318
+ let text = "";
319
+ for (const frame of frames) text += decoder.decode(frame, { stream: true });
320
+ text += decoder.decode();
321
+ const events = text.split("\n\n");
322
+ const tail = events.pop() ?? "";
323
+ const out = [];
324
+ const openParts = /* @__PURE__ */ new Map();
325
+ const openToolInputs = /* @__PURE__ */ new Map();
326
+ const preliminaryOutputs = /* @__PURE__ */ new Map();
327
+ for (const event of events) {
328
+ const chunk = parseEvent(event);
329
+ if (!chunk) {
330
+ out.push({ raw: event });
331
+ continue;
332
+ }
333
+ const type = chunk.type;
334
+ if (type === TURN_SEQ_EVENT_TYPE) continue;
335
+ if (type in PART_BOUNDARY) {
336
+ openParts.delete(`${PART_BOUNDARY[type]}:${chunk.id}`);
337
+ out.push({ chunk });
338
+ continue;
339
+ }
340
+ if (type in DELTA_PART && typeof chunk.delta === "string") {
341
+ const key = `${DELTA_PART[type]}:${chunk.id}`;
342
+ const merged = openParts.get(key);
343
+ if (merged) {
344
+ merged.delta += chunk.delta;
345
+ if (chunk.providerMetadata !== void 0) merged.providerMetadata = chunk.providerMetadata;
346
+ continue;
347
+ }
348
+ const copy = { ...chunk };
349
+ openParts.set(key, copy);
350
+ out.push({ chunk: copy });
351
+ continue;
352
+ }
353
+ if (type === "tool-input-delta" && typeof chunk.inputTextDelta === "string") {
354
+ const index = openToolInputs.get(chunk.toolCallId);
355
+ const existing = index !== void 0 ? out[index] : null;
356
+ if (existing && "chunk" in existing) {
357
+ existing.chunk.inputTextDelta += chunk.inputTextDelta;
358
+ continue;
359
+ }
360
+ openToolInputs.set(chunk.toolCallId, out.length);
361
+ out.push({ chunk: { ...chunk } });
362
+ continue;
363
+ }
364
+ if (type === "tool-input-available" || type === "tool-input-error") {
365
+ const index = openToolInputs.get(chunk.toolCallId);
366
+ if (index !== void 0) out[index] = null;
367
+ openToolInputs.delete(chunk.toolCallId);
368
+ } else if (type === "tool-input-start") {
369
+ openToolInputs.delete(chunk.toolCallId);
370
+ } else if (type === "tool-output-available" || type === "tool-output-error") {
371
+ const superseded = preliminaryOutputs.get(chunk.toolCallId);
372
+ if (superseded !== void 0) {
373
+ out[superseded] = null;
374
+ preliminaryOutputs.delete(chunk.toolCallId);
375
+ }
376
+ if (type === "tool-output-available" && chunk.preliminary === true) {
377
+ preliminaryOutputs.set(chunk.toolCallId, out.length);
378
+ }
379
+ }
380
+ out.push({ chunk });
381
+ }
382
+ let result = "";
383
+ for (const entry of out) {
384
+ if (!entry) continue;
385
+ result += "chunk" in entry ? encodeEvent(entry.chunk) : `${entry.raw}
386
+
387
+ `;
388
+ }
389
+ result += tail;
390
+ return new TextEncoder().encode(result);
391
+ }
392
+ function encodeTurnSeqFrame(turnId, seq) {
393
+ return new TextEncoder().encode(
394
+ encodeEvent({ type: TURN_SEQ_EVENT_TYPE, data: { turnId, seq }, transient: true })
395
+ );
396
+ }
397
+ function encodeEvent(chunk) {
398
+ return `data: ${JSON.stringify(chunk)}
399
+
400
+ `;
401
+ }
402
+ function parseEvent(event) {
403
+ if (!event.startsWith("data: ") || event.includes("\n")) return null;
404
+ const payload = event.slice(6);
405
+ if (!payload.startsWith("{")) return null;
406
+ try {
407
+ const chunk = JSON.parse(payload);
408
+ return chunk && typeof chunk.type === "string" ? chunk : null;
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+
193
414
  // src/stream-buffer.ts
194
415
  var CLEANUP_INTERVAL_MS = 6e4;
195
416
  var MAX_BUFFER_AGE_MS = 30 * 6e4;
196
417
  var COMPLETED_GRACE_MS = 3e4;
418
+ var TURN_REPLY_TTL_MS = 60 * 6e4;
197
419
  var StreamBufferStore = class {
198
420
  constructor() {
199
421
  this.buffers = /* @__PURE__ */ new Map();
422
+ this.replies = /* @__PURE__ */ new Map();
200
423
  this.cleanupTimer = null;
201
424
  this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
202
425
  const handle = this.cleanupTimer;
@@ -310,6 +533,44 @@ var StreamBufferStore = class {
310
533
  lastEventAt: buf.lastEventAt
311
534
  };
312
535
  }
536
+ /**
537
+ * Keep the reply a turn finished with, so it can still be read after the
538
+ * buffer is evicted (30 s grace) — e.g. by the API's call follower, or by a
539
+ * reconciliation pass long after the stream is gone. Keyed by the same
540
+ * session key as the buffer.
541
+ */
542
+ recordTurnReply(key, reply) {
543
+ const trimmed = reply?.trim();
544
+ if (!trimmed) return;
545
+ const now = Date.now();
546
+ this.pruneReplies(now);
547
+ this.replies.set(key, {
548
+ turnId: this.buffers.get(key)?.turnId ?? "",
549
+ reply,
550
+ completedAt: now
551
+ });
552
+ }
553
+ /**
554
+ * The terminal reply for a session, or null when none was recorded or it
555
+ * expired. A reply belonging to an older turn of a session that has since
556
+ * started a new one is never returned.
557
+ */
558
+ turnReply(key) {
559
+ const entry = this.replies.get(key);
560
+ if (!entry) return null;
561
+ if (Date.now() - entry.completedAt > TURN_REPLY_TTL_MS) {
562
+ this.replies.delete(key);
563
+ return null;
564
+ }
565
+ const current = this.buffers.get(key);
566
+ if (current && entry.turnId && current.turnId !== entry.turnId) return null;
567
+ return entry;
568
+ }
569
+ pruneReplies(now) {
570
+ for (const [key, entry] of this.replies) {
571
+ if (now - entry.completedAt > TURN_REPLY_TTL_MS) this.replies.delete(key);
572
+ }
573
+ }
313
574
  /**
314
575
  * Create a ReadableStream that first replays all buffered chunks past
315
576
  * `opts.fromSeq` (default: 0 → replay everything), then subscribes for
@@ -322,15 +583,29 @@ var StreamBufferStore = class {
322
583
  if (!buf) return null;
323
584
  const fromSeq = Math.max(0, opts.fromSeq ?? 0);
324
585
  let subscribedController = null;
586
+ if (opts.buildReplay) return this.createBuiltReplayStream(buf, fromSeq, opts.buildReplay);
325
587
  return new ReadableStream({
326
588
  start(controller) {
327
- for (const frame of buf.frames) {
328
- if (frame.seq <= fromSeq) continue;
589
+ if (opts.compact) {
590
+ const pending = buf.frames.filter((frame) => frame.seq > fromSeq);
591
+ const compacted = compactSseFrames(pending.map((frame) => frame.chunk));
329
592
  try {
330
- controller.enqueue(frame.chunk);
593
+ if (compacted.byteLength > 0) controller.enqueue(compacted);
594
+ if (pending.length > 0 && buf.status === "active") {
595
+ controller.enqueue(encodeTurnSeqFrame(buf.turnId, buf.nextSeq - 1));
596
+ }
331
597
  } catch {
332
598
  return;
333
599
  }
600
+ } else {
601
+ for (const frame of buf.frames) {
602
+ if (frame.seq <= fromSeq) continue;
603
+ try {
604
+ controller.enqueue(frame.chunk);
605
+ } catch {
606
+ return;
607
+ }
608
+ }
334
609
  }
335
610
  if (buf.status !== "active") {
336
611
  try {
@@ -350,8 +625,59 @@ var StreamBufferStore = class {
350
625
  }
351
626
  });
352
627
  }
628
+ createBuiltReplayStream(buf, fromSeq, buildReplay) {
629
+ const pending = buf.frames.filter((frame) => frame.seq > fromSeq).map((frame) => frame.chunk);
630
+ const ctx = { turnId: buf.turnId, lastSeq: buf.nextSeq - 1 };
631
+ const held = [];
632
+ let ended = false;
633
+ let target = null;
634
+ const proxy = {
635
+ enqueue(chunk) {
636
+ if (target) target.enqueue(chunk);
637
+ else held.push(chunk);
638
+ },
639
+ close() {
640
+ if (target) target.close();
641
+ else ended = true;
642
+ },
643
+ error(reason) {
644
+ if (target) target.error(reason);
645
+ else ended = true;
646
+ }
647
+ };
648
+ const active = buf.status === "active";
649
+ if (active) buf.subscribers.add(proxy);
650
+ return new ReadableStream({
651
+ async start(controller) {
652
+ let replay;
653
+ try {
654
+ replay = await buildReplay(pending, ctx);
655
+ } catch {
656
+ replay = compactSseFrames(pending);
657
+ }
658
+ try {
659
+ if (replay.byteLength > 0) controller.enqueue(replay);
660
+ if (pending.length > 0 && active) controller.enqueue(encodeTurnSeqFrame(ctx.turnId, ctx.lastSeq));
661
+ for (const chunk of held) controller.enqueue(chunk);
662
+ held.length = 0;
663
+ if (!active || ended || buf.status !== "active") {
664
+ controller.close();
665
+ return;
666
+ }
667
+ target = controller;
668
+ } catch {
669
+ buf.subscribers.delete(proxy);
670
+ }
671
+ },
672
+ cancel() {
673
+ buf.subscribers.delete(proxy);
674
+ target = null;
675
+ }
676
+ });
677
+ }
353
678
  cleanup() {
354
679
  const now = Date.now();
680
+ this.pruneReplies(now);
355
681
  for (const [key, buf] of this.buffers) {
356
682
  if (buf.status !== "active" && buf.completedAt && now - buf.completedAt > COMPLETED_GRACE_MS) {
357
683
  this.buffers.delete(key);
@@ -370,6 +696,7 @@ var StreamBufferStore = class {
370
696
  this.completeBuffer(buf, "disposed");
371
697
  }
372
698
  this.buffers.clear();
699
+ this.replies.clear();
373
700
  }
374
701
  completeBuffer(buf, reason) {
375
702
  if (buf.status !== "active") return;
@@ -400,6 +727,14 @@ var StreamBufferStore = class {
400
727
  buf.subscribers.clear();
401
728
  }
402
729
  };
730
+ var TURN_COMPLETE_EVENT_TYPE = "data-turn-complete";
731
+ function encodeTurnCompleteFrame(data) {
732
+ return new TextEncoder().encode(
733
+ `data: ${JSON.stringify({ type: TURN_COMPLETE_EVENT_TYPE, data })}
734
+
735
+ `
736
+ );
737
+ }
403
738
  function generateTurnId() {
404
739
  try {
405
740
  if (typeof globalThis.crypto?.randomUUID === "function") {
@@ -467,14 +802,35 @@ var TECH_STACK_REGISTRY = {
467
802
  "react-app": { id: "react-app", target: "web" },
468
803
  "threejs-game": { id: "threejs-game", target: "web" },
469
804
  "phaser-game": { id: "phaser-game", target: "web" },
805
+ // Video (Remotion). `target: 'web'` because the canvas preview is the
806
+ // Remotion Player page the runtime serves from `dist/`; the stack's own
807
+ // `stack.json` declares `runtime.devServer: 'static-build'`, so no new
808
+ // target value (and therefore no new sizing/publish rules) is needed.
809
+ "video-editor": { id: "video-editor", target: "web", seedsOwnTemplate: true },
470
810
  // Mobile (Metro/Expo) — agent-runtime owns the seed.
471
811
  "expo-app": { id: "expo-app", target: "mobile", metro: true, seedsOwnTemplate: true },
472
812
  "expo-three": { id: "expo-three", target: "mobile", metro: true, seedsOwnTemplate: true },
473
813
  "react-native": { id: "react-native", target: "mobile", metro: true, seedsOwnTemplate: true },
474
814
  // Data / scripting
475
815
  "python-data": { id: "python-data", target: "data", seedsOwnTemplate: true },
816
+ // Multi-service backend (dockerd + docker compose). Requires the
817
+ // Docker-capable metal VM class; gated separately by
818
+ // `runtime.docker_class_enabled` (see apps/api/src/lib/runtime-class-setting.ts).
819
+ "docker-compose": {
820
+ id: "docker-compose",
821
+ target: "data",
822
+ seedsOwnTemplate: true,
823
+ vmClass: "docker",
824
+ minimumInstanceSize: "large",
825
+ ports: [
826
+ { port: 8e3, label: "app", protocol: "http", defaultVisibility: "preview" },
827
+ { port: 5432, label: "postgres", protocol: "tcp", defaultVisibility: "tunnel" }
828
+ ]
829
+ },
476
830
  // Native (full game engines)
477
831
  "unity-game": { id: "unity-game", target: "native", seedsOwnTemplate: true },
832
+ // Bring-your-own framework: the preview runs the project's static build.
833
+ custom: { id: "custom", target: "web", seedsOwnTemplate: true },
478
834
  // Bare / no-stack
479
835
  none: { id: "none", target: "none", seedsOwnTemplate: true }
480
836
  };
@@ -491,6 +847,15 @@ function usesMetroBundler(techStackId) {
491
847
  function stackSeedsItself(techStackId) {
492
848
  return getStackEntry(techStackId)?.seedsOwnTemplate === true;
493
849
  }
850
+ function isDockerTechStack(techStackId) {
851
+ return getStackEntry(techStackId)?.vmClass === "docker";
852
+ }
853
+ function getMinimumInstanceSize(techStackId) {
854
+ return getStackEntry(techStackId)?.minimumInstanceSize ?? null;
855
+ }
856
+ function getDeclaredPorts(techStackId) {
857
+ return getStackEntry(techStackId)?.ports ?? [];
858
+ }
494
859
 
495
860
  // src/file-types.ts
496
861
  var BINARY_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -538,6 +903,24 @@ var BINARY_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
538
903
  "aac",
539
904
  "opus",
540
905
  "pdf",
906
+ // Office / document containers
907
+ "xls",
908
+ "xlsx",
909
+ "xlsm",
910
+ "xlsb",
911
+ "doc",
912
+ "docx",
913
+ "docm",
914
+ "ppt",
915
+ "pptx",
916
+ "pptm",
917
+ "odt",
918
+ "ods",
919
+ "odp",
920
+ "pages",
921
+ "numbers",
922
+ "key",
923
+ "epub",
541
924
  // Fonts
542
925
  "woff",
543
926
  "woff2",
@@ -605,28 +988,92 @@ var BINARY_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
605
988
  "nc",
606
989
  "tfrecord"
607
990
  ]);
608
- function isBinaryFilePath(path) {
609
- const dot = path.lastIndexOf(".");
610
- if (dot < 0) return false;
991
+ var BINARY_FILE_TRANSIENT_SUFFIXES = /* @__PURE__ */ new Set([
992
+ "part",
993
+ "partial",
994
+ "crdownload",
995
+ "download"
996
+ ]);
997
+ var BINARY_FILE_BASENAMES = /* @__PURE__ */ new Set([
998
+ "aapt",
999
+ "aapt2",
1000
+ "adb",
1001
+ "apksigner",
1002
+ "bcc_compat",
1003
+ "dexdump",
1004
+ "dmtracedump",
1005
+ "etc1tool",
1006
+ "fastboot",
1007
+ "hprof-conv",
1008
+ "llvm-rs-cc",
1009
+ "mksdcard",
1010
+ "qemu-aarch64-static",
1011
+ "qemu-arm-static",
1012
+ "qemu-i386-static",
1013
+ "qemu-x86_64-static",
1014
+ "split-select",
1015
+ "sqlite3",
1016
+ "zipalign"
1017
+ ]);
1018
+ function basenameOf(path) {
611
1019
  const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
612
- if (dot < slash) return false;
613
- return BINARY_FILE_EXTENSIONS.has(path.slice(dot + 1).toLowerCase());
1020
+ return path.slice(slash + 1);
1021
+ }
1022
+ function extensionOfBasename(basename) {
1023
+ const dot = basename.lastIndexOf(".");
1024
+ if (dot <= 0 || dot === basename.length - 1) return null;
1025
+ return basename.slice(dot + 1).toLowerCase();
1026
+ }
1027
+ function isBinaryFilePath(path) {
1028
+ let basename = basenameOf(path).toLowerCase();
1029
+ if (BINARY_FILE_BASENAMES.has(basename)) return true;
1030
+ for (let i = 0; i < 2; i++) {
1031
+ const ext = extensionOfBasename(basename);
1032
+ if (!ext) return false;
1033
+ if (BINARY_FILE_EXTENSIONS.has(ext)) return true;
1034
+ if (!BINARY_FILE_TRANSIENT_SUFFIXES.has(ext)) return false;
1035
+ basename = basename.slice(0, -(ext.length + 1));
1036
+ }
1037
+ return false;
1038
+ }
1039
+ function isBinaryBuffer(bytes, inspectBytes = 8192) {
1040
+ const head = bytes.subarray(0, Math.min(bytes.length, inspectBytes));
1041
+ if (head.includes(0)) return true;
1042
+ const startsWith = (...signature) => signature.every((value, index) => head[index] === value);
1043
+ const ascii = (value) => Array.from(value).every(
1044
+ (char, index) => head[index] === char.charCodeAt(0)
1045
+ );
1046
+ return startsWith(80, 75, 3, 4) || startsWith(80, 75, 5, 6) || startsWith(208, 207, 17, 224, 161, 177, 26, 225) || ascii("%PDF-") || startsWith(137, 80, 78, 71, 13, 10, 26, 10) || startsWith(255, 216, 255) || startsWith(71, 73, 70, 56) || startsWith(82, 73, 70, 70);
614
1047
  }
615
1048
  // Annotate the CommonJS export names for ESM import in node:
616
1049
  0 && (module.exports = {
1050
+ BINARY_FILE_BASENAMES,
617
1051
  BINARY_FILE_EXTENSIONS,
1052
+ BINARY_FILE_TRANSIENT_SUFFIXES,
618
1053
  StreamBufferStore,
619
1054
  TECH_STACK_REGISTRY,
1055
+ TURN_COMPLETE_EVENT_TYPE,
1056
+ TURN_SEQ_EVENT_TYPE,
1057
+ compactSseFrames,
620
1058
  createBufferingTransform,
621
1059
  createLogger,
1060
+ emitLogToSink,
1061
+ encodeTurnCompleteFrame,
1062
+ encodeTurnSeqFrame,
622
1063
  extractUserText,
623
1064
  findLastUserMessage,
1065
+ getDeclaredPorts,
1066
+ getMinimumInstanceSize,
624
1067
  getStackEntry,
625
1068
  initInstrumentation,
1069
+ isBinaryBuffer,
626
1070
  isBinaryFilePath,
1071
+ isDockerTechStack,
627
1072
  isMacOSJunkName,
628
1073
  isMacOSJunkPath,
629
1074
  isMobileTechStack,
1075
+ setOtelLogSink,
1076
+ setTraceContextProvider,
630
1077
  shutdownInstrumentation,
631
1078
  stackSeedsItself,
632
1079
  traceOperation,
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- export { LogLevel, Logger, createLogger } from './logger.cjs';
1
+ export { LogEntry, LogLevel, LogSink, Logger, TraceContext, TraceContextProvider, createLogger, emitLogToSink, setOtelLogSink, setTraceContextProvider } from './logger.cjs';
2
2
  export { InstrumentationConfig, initInstrumentation, shutdownInstrumentation, traceOperation } from './instrumentation.cjs';
3
- export { ReplayOptions, StreamBufferStore, StreamBufferWriter, TurnSnapshot, TurnStatus, TurnTerminal, createBufferingTransform } from './stream-buffer.cjs';
3
+ export { ReplayOptions, StreamBufferStore, StreamBufferWriter, TURN_COMPLETE_EVENT_TYPE, TURN_SEQ_EVENT_TYPE, TurnCompleteFrameData, TurnReply, TurnSnapshot, TurnStatus, TurnTerminal, compactSseFrames, createBufferingTransform, encodeTurnCompleteFrame, encodeTurnSeqFrame } from './stream-buffer.cjs';
4
4
  export { extractUserText, findLastUserMessage } from './chat-message.cjs';
5
5
  export { isMacOSJunkName, isMacOSJunkPath } from './macos-junk.cjs';
6
- export { StackRegistryEntry, StackTarget, TECH_STACK_REGISTRY, getStackEntry, isMobileTechStack, stackSeedsItself, usesMetroBundler } from './tech-stack-registry.cjs';
7
- export { BINARY_FILE_EXTENSIONS, isBinaryFilePath } from './file-types.cjs';
6
+ export { StackRegistryEntry, StackTarget, TECH_STACK_REGISTRY, getDeclaredPorts, getMinimumInstanceSize, getStackEntry, isDockerTechStack, isMobileTechStack, stackSeedsItself, usesMetroBundler } from './tech-stack-registry.cjs';
7
+ export { BINARY_FILE_BASENAMES, BINARY_FILE_EXTENSIONS, BINARY_FILE_TRANSIENT_SUFFIXES, isBinaryBuffer, isBinaryFilePath } from './file-types.cjs';
8
8
  import '@opentelemetry/api';