@jmanuelcorral/openteam 0.1.20 → 0.1.22

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 (45) hide show
  1. package/README.md +29 -17
  2. package/dist/cli/dashboardServe.d.ts +52 -0
  3. package/dist/cli/dashboardServe.d.ts.map +1 -0
  4. package/dist/cli.js +1816 -499
  5. package/dist/commands/dashboard.d.ts +4 -4
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/dispatch.d.ts.map +1 -1
  8. package/dist/commands/orchestratorAgent.d.ts.map +1 -1
  9. package/dist/contract/opencode.d.ts +1 -1
  10. package/dist/contract/opencode.d.ts.map +1 -1
  11. package/dist/dashboard/render.d.ts.map +1 -1
  12. package/dist/dashboard/state.d.ts +14 -6
  13. package/dist/dashboard/state.d.ts.map +1 -1
  14. package/dist/dashboard/types.d.ts +14 -3
  15. package/dist/dashboard/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +8 -9
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +476 -780
  19. package/dist/orchestrator/coordinator.d.ts +14 -2
  20. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  21. package/dist/plugin/capture.d.ts +46 -0
  22. package/dist/plugin/capture.d.ts.map +1 -0
  23. package/dist/plugin/hooks.d.ts +2 -2
  24. package/dist/plugin/hooks.d.ts.map +1 -1
  25. package/dist/plugin/toolcalls.d.ts +21 -0
  26. package/dist/plugin/toolcalls.d.ts.map +1 -0
  27. package/dist/telemetry/aggregate.d.ts +89 -0
  28. package/dist/telemetry/aggregate.d.ts.map +1 -0
  29. package/dist/telemetry/decisions.d.ts +25 -0
  30. package/dist/telemetry/decisions.d.ts.map +1 -0
  31. package/dist/telemetry/eventLog.d.ts +63 -0
  32. package/dist/telemetry/eventLog.d.ts.map +1 -0
  33. package/dist/telemetry/events.d.ts +217 -0
  34. package/dist/telemetry/events.d.ts.map +1 -0
  35. package/dist/web/git.d.ts +9 -0
  36. package/dist/web/git.d.ts.map +1 -0
  37. package/dist/web/server.d.ts +8 -7
  38. package/dist/web/server.d.ts.map +1 -1
  39. package/dist/web/snapshot.d.ts +13 -7
  40. package/dist/web/snapshot.d.ts.map +1 -1
  41. package/dist/web/start.d.ts +11 -8
  42. package/dist/web/start.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/dist/web/activity.d.ts +0 -12
  45. package/dist/web/activity.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  appendFile,
4
4
  mkdir as mkdir2,
5
5
  readdir,
6
- readFile as readFile2,
6
+ readFile,
7
7
  writeFile as writeFile2
8
8
  } from "node:fs/promises";
9
9
  import { dirname as dirname2, join } from "node:path";
@@ -110,7 +110,12 @@ function buildOrchestratorAgent(frontier, options = {}) {
110
110
  "- **scribe** — memoria silenciosa del equipo. Registra decisiones y",
111
111
  " aprendizajes en un log compartido (`.opencode/openteam-decisions.md`) sin",
112
112
  " ejecutar cambios de código. Modelo **local**; permisos de solo lectura más",
113
- " edición de ese log.",
113
+ " edición de ese log. **Formato parseable** para el dashboard: una decisión",
114
+ " por línea como item de lista Markdown, ya **redactada** (sin prompts ni",
115
+ " datos sensibles):",
116
+ " `- YYYY-MM-DD [agente] Resumen breve de la decisión #tag1 #tag2`",
117
+ " La fecha, el `[agente]` y los `#tags` son opcionales; el resumen es",
118
+ " obligatorio.",
114
119
  "- **ralph** — automatización y triage. Monitoriza el trabajo pendiente",
115
120
  " (issues/tareas), lo prioriza, coordina la ejecución delegando en los",
116
121
  " especialistas y **escala al humano** ante bloqueos, riesgos o aprobaciones.",
@@ -751,6 +756,201 @@ function createAvailabilityCache(options) {
751
756
  };
752
757
  }
753
758
 
759
+ // src/telemetry/events.ts
760
+ import { z as z3 } from "zod";
761
+
762
+ // src/capabilities/types.ts
763
+ import { z as z2 } from "zod";
764
+ var CapabilityTierSchema = z2.union([
765
+ z2.literal(0),
766
+ z2.literal(1),
767
+ z2.literal(2),
768
+ z2.literal(3),
769
+ z2.literal(4),
770
+ z2.literal(5)
771
+ ]);
772
+ var ComplexityTierSchema = z2.enum([
773
+ "trivial",
774
+ "simple",
775
+ "moderate",
776
+ "hard"
777
+ ]);
778
+ var ModelCapabilityProfileSchema = z2.object({
779
+ ref: z2.object({
780
+ providerID: z2.string().min(1),
781
+ modelID: z2.string().min(1)
782
+ }),
783
+ kind: z2.enum(["local", "frontier", "router"]),
784
+ contextWindow: z2.number().int().positive(),
785
+ maxOutputTokens: z2.number().int().positive(),
786
+ supportsToolCalling: z2.boolean(),
787
+ supportsVision: z2.boolean(),
788
+ reasoningTier: CapabilityTierSchema,
789
+ codeQualityTier: CapabilityTierSchema,
790
+ costPer1M: z2.object({
791
+ inputUSD: z2.number().min(0),
792
+ outputUSD: z2.number().min(0)
793
+ }),
794
+ availability: z2.enum(["available", "degraded", "unavailable"])
795
+ });
796
+
797
+ // src/telemetry/events.ts
798
+ var EVENT_SCHEMA_VERSION = 1;
799
+ var EventBaseSchema = z3.object({
800
+ v: z3.literal(EVENT_SCHEMA_VERSION),
801
+ ts: z3.number().finite(),
802
+ sessionID: z3.string().min(1)
803
+ });
804
+ var RouteEventSchema = EventBaseSchema.extend({
805
+ type: z3.literal("route"),
806
+ promptHash: z3.string().min(1),
807
+ promptChars: z3.number().int().min(0),
808
+ tier: ComplexityTierSchema,
809
+ routeKind: z3.enum(["local", "frontier"]),
810
+ selected: ModelRefSchema,
811
+ rationale: z3.string(),
812
+ estimatedCostUSD: z3.number().finite().min(0),
813
+ baselineCostUSD: z3.number().finite().min(0),
814
+ estimatedSavingsUSD: z3.number().finite(),
815
+ budgetAction: z3.string().min(1),
816
+ tokensIn: z3.number().int().min(0).optional(),
817
+ tokensOut: z3.number().int().min(0).optional(),
818
+ decisionID: z3.string().min(1).optional(),
819
+ agent: z3.string().min(1).optional(),
820
+ success: z3.boolean().optional(),
821
+ batchID: z3.string().min(1).optional(),
822
+ failureReason: z3.string().min(1).optional(),
823
+ failureStage: z3.string().min(1).optional()
824
+ });
825
+ var MessageEventSchema = EventBaseSchema.extend({
826
+ type: z3.literal("message"),
827
+ providerID: z3.string().min(1),
828
+ modelID: z3.string().min(1),
829
+ agent: z3.string().min(1).optional(),
830
+ mode: z3.string().min(1).optional(),
831
+ messageID: z3.string().min(1).optional(),
832
+ costUSD: z3.number().finite().min(0),
833
+ tokensIn: z3.number().int().min(0),
834
+ tokensOut: z3.number().int().min(0),
835
+ tokensReasoning: z3.number().int().min(0),
836
+ tokensCacheRead: z3.number().int().min(0),
837
+ tokensCacheWrite: z3.number().int().min(0),
838
+ durationMs: z3.number().int().min(0)
839
+ });
840
+ var ToolcallEventSchema = EventBaseSchema.extend({
841
+ type: z3.literal("toolcall"),
842
+ tool: z3.string().min(1),
843
+ callID: z3.string().min(1),
844
+ agent: z3.string().min(1).optional(),
845
+ durationMs: z3.number().int().min(0),
846
+ ok: z3.boolean(),
847
+ title: z3.string().optional()
848
+ });
849
+ var MeetingEventSchema = EventBaseSchema.extend({
850
+ type: z3.literal("meeting"),
851
+ batchID: z3.string().min(1),
852
+ purpose: z3.string().optional(),
853
+ roles: z3.array(z3.string().min(1)),
854
+ decisionIDs: z3.array(z3.string().min(1)).optional()
855
+ });
856
+ var DecisionEventSchema = EventBaseSchema.extend({
857
+ type: z3.literal("decision"),
858
+ agent: z3.string().min(1).optional(),
859
+ summary: z3.string().min(1),
860
+ tags: z3.array(z3.string().min(1)).optional()
861
+ });
862
+ var ActivityEventSchema = EventBaseSchema.extend({
863
+ type: z3.literal("activity"),
864
+ kind: z3.enum(["route", "agent", "decision", "commit"]),
865
+ agent: z3.string().min(1).optional(),
866
+ summary: z3.string().min(1)
867
+ });
868
+ var OpenTeamEventSchema = z3.discriminatedUnion("type", [
869
+ RouteEventSchema,
870
+ MessageEventSchema,
871
+ ToolcallEventSchema,
872
+ MeetingEventSchema,
873
+ DecisionEventSchema,
874
+ ActivityEventSchema
875
+ ]);
876
+
877
+ // src/plugin/capture.ts
878
+ function nonNeg(value) {
879
+ return typeof value === "number" && value >= 0 ? Math.trunc(value) : 0;
880
+ }
881
+ function messageEventFrom(info, now) {
882
+ if (info.role !== "assistant") {
883
+ return;
884
+ }
885
+ if (info.time?.completed === undefined) {
886
+ return;
887
+ }
888
+ if (info.sessionID === undefined || info.providerID === undefined || info.modelID === undefined) {
889
+ return;
890
+ }
891
+ const created = info.time.created ?? info.time.completed;
892
+ const durationMs = Math.max(0, Math.trunc(info.time.completed - created));
893
+ const event = {
894
+ v: EVENT_SCHEMA_VERSION,
895
+ type: "message",
896
+ ts: now(),
897
+ sessionID: info.sessionID,
898
+ providerID: info.providerID,
899
+ modelID: info.modelID,
900
+ costUSD: typeof info.cost === "number" && info.cost >= 0 ? info.cost : 0,
901
+ tokensIn: nonNeg(info.tokens?.input),
902
+ tokensOut: nonNeg(info.tokens?.output),
903
+ tokensReasoning: nonNeg(info.tokens?.reasoning),
904
+ tokensCacheRead: nonNeg(info.tokens?.cache?.read),
905
+ tokensCacheWrite: nonNeg(info.tokens?.cache?.write),
906
+ durationMs
907
+ };
908
+ if (info.mode !== undefined) {
909
+ event.mode = info.mode;
910
+ }
911
+ if (info.id !== undefined) {
912
+ event.messageID = info.id;
913
+ }
914
+ return event;
915
+ }
916
+ var ACTIVITY_SUMMARIES = {
917
+ "session.created": "Sesión creada",
918
+ "session.idle": "Sesión en reposo",
919
+ "session.error": "Error de sesión"
920
+ };
921
+ function sessionIDFromEvent(event) {
922
+ const properties = event.properties;
923
+ if (properties === null || typeof properties !== "object") {
924
+ return;
925
+ }
926
+ const record = properties;
927
+ if (typeof record.sessionID === "string") {
928
+ return record.sessionID;
929
+ }
930
+ if (typeof record.info?.id === "string") {
931
+ return record.info.id;
932
+ }
933
+ return;
934
+ }
935
+ function activityEventFrom(event, now) {
936
+ const summary = ACTIVITY_SUMMARIES[event.type];
937
+ if (summary === undefined) {
938
+ return;
939
+ }
940
+ const sessionID = sessionIDFromEvent(event);
941
+ if (sessionID === undefined) {
942
+ return;
943
+ }
944
+ return {
945
+ v: EVENT_SCHEMA_VERSION,
946
+ type: "activity",
947
+ ts: now(),
948
+ sessionID,
949
+ kind: "agent",
950
+ summary
951
+ };
952
+ }
953
+
754
954
  // src/plugin/commandTool.ts
755
955
  import { tool } from "@opencode-ai/plugin";
756
956
 
@@ -970,25 +1170,19 @@ function autoBaseline(config) {
970
1170
  // src/commands/dashboard.ts
971
1171
  function renderDashboardStatus(dashboard) {
972
1172
  const url = `http://${dashboard.host}:${dashboard.port}`;
973
- if (!dashboard.enabled) {
974
- return [
975
- "Dashboard: deshabilitado.",
976
- "",
977
- 'Para activarlo, pon "dashboard": { "enabled": true } en .opencode/openteam.json',
978
- "y recarga opencode. Se servirá (loopback) en:",
979
- ` ${url}`
980
- ].join(`
981
- `);
982
- }
983
1173
  return [
984
- "Dashboard: habilitado.",
1174
+ "Dashboard (multi-sesión, se lanza desde la CLI):",
985
1175
  ` URL: ${url}`,
986
1176
  ` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
987
1177
  ` Rutas: últimas ${dashboard.recentRoutes}`,
988
1178
  dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
989
1179
  "",
990
- "Nota: solo escucha en loopback y nunca expone prompts (solo hashes).",
991
- "El servidor corre dentro de la sesión de opencode; se cierra al salir."
1180
+ "Lánzalo con:",
1181
+ " openteam dashboard (Ctrl+C para parar; --open abre el navegador)",
1182
+ "",
1183
+ "Agrega TODAS las sesiones de opencode que escriben eventos en",
1184
+ " .opencode/openteam/sessions/*.jsonl",
1185
+ "Solo escucha en loopback y nunca expone prompts (solo hashes)."
992
1186
  ].join(`
993
1187
  `);
994
1188
  }
@@ -1142,7 +1336,8 @@ var HELP = [
1142
1336
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
1143
1337
  " openteam doctor Diagnóstico de runtimes y config",
1144
1338
  " openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
1145
- " openteam dashboard Estado y URL del dashboard web (loopback)",
1339
+ " openteam dashboard Lanza el dashboard web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
1340
+ " openteam dashboard --status Muestra la config del dashboard sin lanzarlo",
1146
1341
  " openteam report Resumen de coste/ahorro (telemetría)",
1147
1342
  " openteam yolo status Muestra si el modo YOLO está activo",
1148
1343
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -1814,81 +2009,67 @@ function toCostRecord(decision2, context) {
1814
2009
  return record;
1815
2010
  }
1816
2011
 
1817
- // src/telemetry/hash.ts
1818
- var FNV_OFFSET_BASIS = 2166136261;
1819
- var FNV_PRIME = 16777619;
1820
- function hashPrompt(prompt) {
1821
- let hash = FNV_OFFSET_BASIS;
1822
- for (let index = 0;index < prompt.length; index += 1) {
1823
- hash ^= prompt.charCodeAt(index);
1824
- hash = Math.imul(hash, FNV_PRIME);
1825
- }
1826
- return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
1827
- }
1828
-
1829
2012
  // src/telemetry/types.ts
1830
- import { z as z3 } from "zod";
1831
-
1832
- // src/capabilities/types.ts
1833
- import { z as z2 } from "zod";
1834
- var CapabilityTierSchema = z2.union([
1835
- z2.literal(0),
1836
- z2.literal(1),
1837
- z2.literal(2),
1838
- z2.literal(3),
1839
- z2.literal(4),
1840
- z2.literal(5)
1841
- ]);
1842
- var ComplexityTierSchema = z2.enum([
1843
- "trivial",
1844
- "simple",
1845
- "moderate",
1846
- "hard"
1847
- ]);
1848
- var ModelCapabilityProfileSchema = z2.object({
1849
- ref: z2.object({
1850
- providerID: z2.string().min(1),
1851
- modelID: z2.string().min(1)
1852
- }),
1853
- kind: z2.enum(["local", "frontier", "router"]),
1854
- contextWindow: z2.number().int().positive(),
1855
- maxOutputTokens: z2.number().int().positive(),
1856
- supportsToolCalling: z2.boolean(),
1857
- supportsVision: z2.boolean(),
1858
- reasoningTier: CapabilityTierSchema,
1859
- codeQualityTier: CapabilityTierSchema,
1860
- costPer1M: z2.object({
1861
- inputUSD: z2.number().min(0),
1862
- outputUSD: z2.number().min(0)
1863
- }),
1864
- availability: z2.enum(["available", "degraded", "unavailable"])
1865
- });
1866
-
1867
- // src/telemetry/types.ts
1868
- var CostRecordSchema = z3.object({
1869
- ts: z3.number().finite(),
1870
- sessionID: z3.string().min(1).optional(),
1871
- promptHash: z3.string().min(1),
1872
- promptChars: z3.number().int().min(0),
2013
+ import { z as z4 } from "zod";
2014
+ var CostRecordSchema = z4.object({
2015
+ ts: z4.number().finite(),
2016
+ sessionID: z4.string().min(1).optional(),
2017
+ promptHash: z4.string().min(1),
2018
+ promptChars: z4.number().int().min(0),
1873
2019
  tier: ComplexityTierSchema,
1874
- routeKind: z3.enum(["local", "frontier"]),
2020
+ routeKind: z4.enum(["local", "frontier"]),
1875
2021
  selected: ModelRefSchema,
1876
- rationale: z3.string(),
1877
- estimatedCostUSD: z3.number().finite().min(0),
1878
- baselineCostUSD: z3.number().finite().min(0),
1879
- estimatedSavingsUSD: z3.number().finite(),
1880
- budgetAction: z3.string().min(1),
1881
- tokensIn: z3.number().int().min(0).optional(),
1882
- tokensOut: z3.number().int().min(0).optional()
2022
+ rationale: z4.string(),
2023
+ estimatedCostUSD: z4.number().finite().min(0),
2024
+ baselineCostUSD: z4.number().finite().min(0),
2025
+ estimatedSavingsUSD: z4.number().finite(),
2026
+ budgetAction: z4.string().min(1),
2027
+ tokensIn: z4.number().int().min(0).optional(),
2028
+ tokensOut: z4.number().int().min(0).optional()
1883
2029
  });
1884
2030
 
1885
- // src/telemetry/sink.ts
1886
- function createJsonlSink(deps) {
2031
+ // src/telemetry/read.ts
2032
+ var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
2033
+ function parseCostRecordsJsonl(text) {
2034
+ const records = [];
2035
+ for (const line of text.split(`
2036
+ `)) {
2037
+ const trimmed = line.trim();
2038
+ if (trimmed.length === 0) {
2039
+ continue;
2040
+ }
2041
+ let candidate;
2042
+ try {
2043
+ candidate = JSON.parse(trimmed);
2044
+ } catch {
2045
+ continue;
2046
+ }
2047
+ const parsed = CostRecordSchema.safeParse(candidate);
2048
+ if (parsed.success) {
2049
+ records.push(parsed.data);
2050
+ }
2051
+ }
2052
+ return records;
2053
+ }
2054
+
2055
+ // src/telemetry/eventLog.ts
2056
+ var DEFAULT_SESSIONS_DIR = ".opencode/openteam/sessions";
2057
+ var LEGACY_SESSION_ID = "legacy";
2058
+ function sessionFileName(sessionID) {
2059
+ const safe = sessionID.replace(/[^A-Za-z0-9._-]/g, "_");
2060
+ return `${safe}.jsonl`;
2061
+ }
2062
+ function defaultJoin(dir, file) {
2063
+ return dir.endsWith("/") ? `${dir}${file}` : `${dir}/${file}`;
2064
+ }
2065
+ function createEventLogSink(deps) {
2066
+ const join = deps.join ?? defaultJoin;
1887
2067
  return {
1888
- record: async (record) => {
2068
+ emit: async (event) => {
1889
2069
  try {
1890
- const parsed = CostRecordSchema.parse(record);
1891
- await deps.append(`${JSON.stringify(parsed)}
2070
+ const parsed = OpenTeamEventSchema.parse(event);
2071
+ const path = join(deps.dir, sessionFileName(parsed.sessionID));
2072
+ await deps.appendLine(path, `${JSON.stringify(parsed)}
1892
2073
  `);
1893
2074
  } catch (error) {
1894
2075
  deps.onError?.(error);
@@ -1896,10 +2077,117 @@ function createJsonlSink(deps) {
1896
2077
  }
1897
2078
  };
1898
2079
  }
1899
- function createNullSink() {
1900
- return {
1901
- record: () => {}
2080
+ function createNullEventSink() {
2081
+ return { emit: () => {} };
2082
+ }
2083
+ function parseEventsJsonl(text) {
2084
+ const events = [];
2085
+ for (const line of text.split(`
2086
+ `)) {
2087
+ const trimmed = line.trim();
2088
+ if (trimmed.length === 0) {
2089
+ continue;
2090
+ }
2091
+ let candidate;
2092
+ try {
2093
+ candidate = JSON.parse(trimmed);
2094
+ } catch {
2095
+ continue;
2096
+ }
2097
+ const parsed = OpenTeamEventSchema.safeParse(candidate);
2098
+ if (parsed.success) {
2099
+ events.push(parsed.data);
2100
+ }
2101
+ }
2102
+ return events;
2103
+ }
2104
+ function costRecordToRouteEvent(record) {
2105
+ const event = {
2106
+ v: EVENT_SCHEMA_VERSION,
2107
+ type: "route",
2108
+ ts: record.ts,
2109
+ sessionID: record.sessionID ?? LEGACY_SESSION_ID,
2110
+ promptHash: record.promptHash,
2111
+ promptChars: record.promptChars,
2112
+ tier: record.tier,
2113
+ routeKind: record.routeKind,
2114
+ selected: record.selected,
2115
+ rationale: record.rationale,
2116
+ estimatedCostUSD: record.estimatedCostUSD,
2117
+ baselineCostUSD: record.baselineCostUSD,
2118
+ estimatedSavingsUSD: record.estimatedSavingsUSD,
2119
+ budgetAction: record.budgetAction
1902
2120
  };
2121
+ if (record.tokensIn !== undefined) {
2122
+ event.tokensIn = record.tokensIn;
2123
+ }
2124
+ if (record.tokensOut !== undefined) {
2125
+ event.tokensOut = record.tokensOut;
2126
+ }
2127
+ return event;
2128
+ }
2129
+ function routeEventToCostRecord(event) {
2130
+ const record = {
2131
+ ts: event.ts,
2132
+ sessionID: event.sessionID,
2133
+ promptHash: event.promptHash,
2134
+ promptChars: event.promptChars,
2135
+ tier: event.tier,
2136
+ routeKind: event.routeKind,
2137
+ selected: event.selected,
2138
+ rationale: event.rationale,
2139
+ estimatedCostUSD: event.estimatedCostUSD,
2140
+ baselineCostUSD: event.baselineCostUSD,
2141
+ estimatedSavingsUSD: event.estimatedSavingsUSD,
2142
+ budgetAction: event.budgetAction
2143
+ };
2144
+ if (event.tokensIn !== undefined) {
2145
+ record.tokensIn = event.tokensIn;
2146
+ }
2147
+ if (event.tokensOut !== undefined) {
2148
+ record.tokensOut = event.tokensOut;
2149
+ }
2150
+ return record;
2151
+ }
2152
+ async function readRouteCostRecords(dir, deps) {
2153
+ const events = await readSessionEvents(dir, deps);
2154
+ const records = [];
2155
+ for (const event of events) {
2156
+ if (event.type === "route") {
2157
+ records.push(routeEventToCostRecord(event));
2158
+ }
2159
+ }
2160
+ return records;
2161
+ }
2162
+ async function readSessionEvents(dir, deps) {
2163
+ const join = deps.join ?? defaultJoin;
2164
+ const files = (await deps.listFiles(dir)).filter((file) => file.endsWith(".jsonl"));
2165
+ const perFile = await Promise.all(files.map(async (file) => {
2166
+ const text = await deps.readText(join(dir, file));
2167
+ return text === undefined ? [] : parseEventsJsonl(text);
2168
+ }));
2169
+ const events = perFile.flat();
2170
+ if (deps.legacyTelemetryPath !== undefined) {
2171
+ const legacyText = await deps.readText(deps.legacyTelemetryPath);
2172
+ if (legacyText !== undefined) {
2173
+ for (const record of parseCostRecordsJsonl(legacyText)) {
2174
+ events.push(costRecordToRouteEvent(record));
2175
+ }
2176
+ }
2177
+ }
2178
+ return events.sort((a, b) => a.ts - b.ts);
2179
+ }
2180
+
2181
+ // src/telemetry/hash.ts
2182
+ var FNV_OFFSET_BASIS = 2166136261;
2183
+ var FNV_PRIME = 16777619;
2184
+ function hashPrompt(prompt) {
2185
+ let hash = FNV_OFFSET_BASIS;
2186
+ for (let index = 0;index < prompt.length; index += 1) {
2187
+ hash ^= prompt.charCodeAt(index);
2188
+ hash = Math.imul(hash, FNV_PRIME);
2189
+ }
2190
+ return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
1903
2191
  }
1904
2192
 
1905
2193
  // src/plugin/hooks.ts
@@ -1932,8 +2220,8 @@ function promptText(output) {
1932
2220
  return output.parts.map(partText).join(`
1933
2221
  `);
1934
2222
  }
1935
- function emitTelemetry(sink, record) {
1936
- Promise.resolve().then(() => sink.record(record)).catch(() => {});
2223
+ function emitTelemetry(sink, event) {
2224
+ Promise.resolve().then(() => sink.emit(event)).catch(() => {});
1937
2225
  }
1938
2226
  function deriveTaskSignals(input, output) {
1939
2227
  const prompt = promptText(output);
@@ -1954,7 +2242,7 @@ function deriveTaskSignals(input, output) {
1954
2242
  return signals;
1955
2243
  }
1956
2244
  function createChatMessageHook(config, getAvailable, options = {}) {
1957
- const sink = options.sink ?? createNullSink();
2245
+ const sink = options.sink ?? createNullEventSink();
1958
2246
  const now = options.now ?? Date.now;
1959
2247
  return async (input, output) => {
1960
2248
  const signals = deriveTaskSignals(input, output);
@@ -1976,7 +2264,7 @@ function createChatMessageHook(config, getAvailable, options = {}) {
1976
2264
  if (signals.estimatedOutputTokens !== undefined) {
1977
2265
  telemetryContext.tokensOut = signals.estimatedOutputTokens;
1978
2266
  }
1979
- emitTelemetry(sink, toCostRecord(decision2, telemetryContext));
2267
+ emitTelemetry(sink, costRecordToRouteEvent(toCostRecord(decision2, telemetryContext)));
1980
2268
  };
1981
2269
  }
1982
2270
  function createHooks(config, getAvailable, options = {}) {
@@ -1985,615 +2273,34 @@ function createHooks(config, getAvailable, options = {}) {
1985
2273
  };
1986
2274
  }
1987
2275
 
1988
- // src/telemetry/read.ts
1989
- import { readFile } from "node:fs/promises";
1990
- var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
1991
- function parseCostRecordsJsonl(text) {
1992
- const records = [];
1993
- for (const line of text.split(`
1994
- `)) {
1995
- const trimmed = line.trim();
1996
- if (trimmed.length === 0) {
1997
- continue;
1998
- }
1999
- let candidate;
2000
- try {
2001
- candidate = JSON.parse(trimmed);
2002
- } catch {
2003
- continue;
2004
- }
2005
- const parsed = CostRecordSchema.safeParse(candidate);
2006
- if (parsed.success) {
2007
- records.push(parsed.data);
2008
- }
2009
- }
2010
- return records;
2276
+ // src/plugin/toolcalls.ts
2277
+ function key(sessionID, callID) {
2278
+ return `${sessionID}\x00${callID}`;
2011
2279
  }
2012
- function isMissingFile(error) {
2013
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2014
- }
2015
- async function readCostRecords(path, deps = {}) {
2016
- const readFileFn = deps.readFile ?? readFile;
2017
- try {
2018
- const content = await readFileFn(path, "utf8");
2019
- return parseCostRecordsJsonl(content);
2020
- } catch (error) {
2021
- if (isMissingFile(error)) {
2022
- return [];
2023
- }
2024
- throw error;
2025
- }
2026
- }
2027
-
2028
- // src/web/paths.ts
2029
- var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
2030
-
2031
- // src/web/start.ts
2032
- import { watch as fsWatch } from "node:fs";
2033
-
2034
- // src/web/activity.ts
2035
- function createActivityBuffer(max = 100) {
2036
- const entries = [];
2280
+ function createToolcallTracker(deps) {
2281
+ const now = deps.now ?? Date.now;
2282
+ const started = new Map;
2037
2283
  return {
2038
- push(entry) {
2039
- entries.push(entry);
2040
- if (entries.length > max) {
2041
- entries.splice(0, entries.length - max);
2042
- }
2284
+ before(input) {
2285
+ started.set(key(input.sessionID, input.callID), now());
2043
2286
  },
2044
- list() {
2045
- return [...entries];
2046
- }
2047
- };
2048
- }
2049
-
2050
- // src/dashboard/render.ts
2051
- var TIERS2 = [
2052
- "trivial",
2053
- "simple",
2054
- "moderate",
2055
- "hard"
2056
- ];
2057
- function escapeHtml(value) {
2058
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2059
- }
2060
- function usd2(value) {
2061
- return `$${value.toFixed(5)}`;
2062
- }
2063
- function pct(value) {
2064
- return `${value.toFixed(2)}%`;
2065
- }
2066
- function agentModel(agent) {
2067
- return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
2068
- }
2069
- function summaryPanel(cost) {
2070
- return [
2071
- '<section class="panel" id="panel-summary">',
2072
- "<h2>Resumen de routing</h2>",
2073
- '<div class="grid">',
2074
- `<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
2075
- `<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
2076
- `<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
2077
- `<div class="stat"><span class="k">Coste real</span><span class="v">${usd2(cost.totalEstimatedUSD)}</span></div>`,
2078
- `<div class="stat"><span class="k">Baseline</span><span class="v">${usd2(cost.totalBaselineUSD)}</span></div>`,
2079
- `<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd2(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
2080
- `<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
2081
- `<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
2082
- "</div>",
2083
- "</section>"
2084
- ].join("");
2085
- }
2086
- function tierPanel(cost) {
2087
- const rows = TIERS2.map((tier) => {
2088
- const summary = cost.byTier[tier];
2089
- return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd2(summary.estimatedUSD)}</td><td class="good">${usd2(summary.savingsUSD)}</td></tr>`;
2090
- }).join("");
2091
- return [
2092
- '<section class="panel" id="panel-tier">',
2093
- "<h2>Por tier</h2>",
2094
- "<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
2095
- `<tbody>${rows}</tbody></table>`,
2096
- "</section>"
2097
- ].join("");
2098
- }
2099
- function loopItemRow(item) {
2100
- const box = item.done ? "☑" : "☐";
2101
- const cls = item.done ? "done" : "open";
2102
- const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
2103
- return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
2104
- }
2105
- function loopPanel(loop) {
2106
- if (loop === undefined) {
2107
- return [
2108
- '<section class="panel" id="panel-loop">',
2109
- "<h2>Loop</h2>",
2110
- '<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
2111
- "</section>"
2112
- ].join("");
2113
- }
2114
- const items = loop.items.map(loopItemRow).join("");
2115
- const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
2116
- return [
2117
- '<section class="panel" id="panel-loop">',
2118
- "<h2>Loop</h2>",
2119
- `<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
2120
- `<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
2121
- `<ul class="items">${items}</ul>`,
2122
- commit,
2123
- "</section>"
2124
- ].join("");
2125
- }
2126
- function teamPanel(team) {
2127
- if (team.length === 0) {
2128
- return [
2129
- '<section class="panel" id="panel-team">',
2130
- "<h2>Equipo</h2>",
2131
- '<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
2132
- "</section>"
2133
- ].join("");
2134
- }
2135
- const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
2136
- return [
2137
- '<section class="panel" id="panel-team">',
2138
- "<h2>Equipo</h2>",
2139
- "<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
2140
- `<tbody>${rows}</tbody></table>`,
2141
- "</section>"
2142
- ].join("");
2143
- }
2144
- function routeRow(route) {
2145
- return `<tr><td>${route.tier}</td><td class="${route.routeKind}">${route.routeKind}</td><td>${escapeHtml(route.model)}</td><td class="hash">${escapeHtml(route.promptHash)}</td><td>${usd2(route.estimatedCostUSD)}</td><td class="good">${usd2(route.estimatedSavingsUSD)}</td></tr>`;
2146
- }
2147
- function routesPanel(routes) {
2148
- if (routes.length === 0) {
2149
- return [
2150
- '<section class="panel" id="panel-routes">',
2151
- "<h2>Decisiones recientes</h2>",
2152
- '<p class="muted">Sin decisiones de routing todavía.</p>',
2153
- "</section>"
2154
- ].join("");
2155
- }
2156
- const rows = routes.map(routeRow).join("");
2157
- return [
2158
- '<section class="panel" id="panel-routes">',
2159
- "<h2>Decisiones recientes</h2>",
2160
- "<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
2161
- `<tbody>${rows}</tbody></table>`,
2162
- "</section>"
2163
- ].join("");
2164
- }
2165
- function activityRow(entry) {
2166
- const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
2167
- return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
2168
- }
2169
- function activityPanel(activity) {
2170
- if (activity.length === 0) {
2171
- return [
2172
- '<section class="panel" id="panel-activity">',
2173
- "<h2>Actividad</h2>",
2174
- '<p class="muted">Sin actividad registrada.</p>',
2175
- "</section>"
2176
- ].join("");
2177
- }
2178
- const rows = activity.map(activityRow).join("");
2179
- return [
2180
- '<section class="panel" id="panel-activity">',
2181
- "<h2>Actividad</h2>",
2182
- `<ul class="timeline">${rows}</ul>`,
2183
- "</section>"
2184
- ].join("");
2185
- }
2186
- var STYLE = `
2187
- :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
2188
- *{box-sizing:border-box}
2189
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
2190
- header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
2191
- header h1{font-size:18px;margin:0}
2192
- header .meta{color:var(--muted);font-size:12px}
2193
- main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
2194
- .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
2195
- .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
2196
- .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
2197
- .stat{display:flex;flex-direction:column;gap:2px}
2198
- .stat .k{color:var(--muted);font-size:12px}
2199
- .stat .v{font-size:18px;font-weight:600}
2200
- .good{color:var(--good)}
2201
- .local{color:var(--local)}
2202
- .frontier{color:var(--frontier)}
2203
- .muted{color:var(--muted)}
2204
- table{width:100%;border-collapse:collapse;font-size:13px}
2205
- th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
2206
- th{color:var(--muted);font-weight:600}
2207
- .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
2208
- .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
2209
- .progress .bar{height:100%;background:var(--good)}
2210
- ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
2211
- ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
2212
- ul.items li.done{color:var(--muted)}
2213
- ul.items .box{font-family:monospace}
2214
- .who{color:var(--local)}
2215
- .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
2216
- code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
2217
- `;
2218
- var CLIENT_JS = `
2219
- (function(){
2220
- function reloadIfChanged(prev){
2221
- fetch('/api/state').then(function(r){return r.json()}).then(function(s){
2222
- if(s.generatedAt!==prev){location.reload()}
2223
- }).catch(function(){});
2224
- }
2225
- var current=document.documentElement.getAttribute('data-generated')||'';
2226
- if('EventSource' in window){
2227
- try{
2228
- var es=new EventSource('/events');
2229
- es.addEventListener('state',function(){location.reload()});
2230
- es.onerror=function(){/* fallback below */};
2231
- }catch(e){/* ignore */}
2232
- }
2233
- var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
2234
- setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
2235
- })();
2236
- `;
2237
- function renderDashboardHtml(state, options = {}) {
2238
- const refreshMs = options.refreshMs ?? 2000;
2239
- const body = [
2240
- summaryPanel(state.cost),
2241
- loopPanel(state.loop),
2242
- teamPanel(state.team),
2243
- tierPanel(state.cost),
2244
- routesPanel(state.recentRoutes),
2245
- activityPanel(state.activity)
2246
- ].join("");
2247
- const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
2248
- return [
2249
- "<!doctype html>",
2250
- `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}">`,
2251
- "<head>",
2252
- '<meta charset="utf-8">',
2253
- '<meta name="viewport" content="width=device-width,initial-scale=1">',
2254
- "<title>openteam dashboard</title>",
2255
- `<style>${STYLE}</style>`,
2256
- "</head>",
2257
- "<body>",
2258
- "<header>",
2259
- "<h1>openteam dashboard</h1>",
2260
- `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
2261
- "</header>",
2262
- `<main>${body}</main>`,
2263
- `<script>${CLIENT_JS}</script>`,
2264
- "</body>",
2265
- "</html>"
2266
- ].join("");
2267
- }
2268
-
2269
- // src/dashboard/backlog.ts
2270
- var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
2271
- var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
2272
- function parseAssignee(text) {
2273
- const match = ASSIGNEE_RE.exec(text);
2274
- if (match === null) {
2275
- return { text: text.trim() };
2276
- }
2277
- const assignee = (match[1] ?? "").trim();
2278
- const rest = (match[2] ?? "").trim();
2279
- if (assignee.length === 0) {
2280
- return { text: text.trim() };
2281
- }
2282
- return { assignee, text: rest };
2283
- }
2284
- function parseBacklog(text) {
2285
- const items = [];
2286
- for (const line of text.split(/\r?\n/)) {
2287
- const match = ITEM_RE.exec(line);
2288
- if (match === null) {
2289
- continue;
2290
- }
2291
- const done = (match[1] ?? " ").toLowerCase() === "x";
2292
- const rawText = (match[2] ?? "").trim();
2293
- const { assignee, text: itemText } = parseAssignee(rawText);
2294
- const item = { text: itemText, done };
2295
- if (assignee !== undefined) {
2296
- item.assignee = assignee;
2297
- }
2298
- items.push(item);
2299
- }
2300
- return items;
2301
- }
2302
- function buildLoopSnapshot(backlogPath, items) {
2303
- const total = items.length;
2304
- const done = items.filter((item) => item.done).length;
2305
- const open = total - done;
2306
- const progressPct = total === 0 ? 100 : Math.round(done / total * 100);
2307
- return {
2308
- backlogPath,
2309
- total,
2310
- done,
2311
- open,
2312
- progressPct,
2313
- items: [...items]
2314
- };
2315
- }
2316
-
2317
- // src/dashboard/state.ts
2318
- var DEFAULT_RECENT_ROUTES = 50;
2319
- var DEFAULT_ACTIVITY_LIMIT = 100;
2320
- function toRouteView(record) {
2321
- return {
2322
- ts: record.ts,
2323
- tier: record.tier,
2324
- routeKind: record.routeKind,
2325
- model: `${record.selected.providerID}/${record.selected.modelID}`,
2326
- promptHash: record.promptHash,
2327
- estimatedCostUSD: record.estimatedCostUSD,
2328
- estimatedSavingsUSD: record.estimatedSavingsUSD,
2329
- budgetAction: record.budgetAction
2330
- };
2331
- }
2332
- function recentRoutes(records, limit) {
2333
- return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
2334
- }
2335
- function recentActivity(entries, limit) {
2336
- return [...entries].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
2337
- }
2338
- function loopFrom(backlog) {
2339
- if (backlog === undefined) {
2340
- return;
2341
- }
2342
- const snapshot = buildLoopSnapshot(backlog.path, backlog.items);
2343
- if (backlog.lastCommit !== undefined) {
2344
- snapshot.lastCommit = backlog.lastCommit;
2345
- }
2346
- return snapshot;
2347
- }
2348
- function buildDashboardState(inputs) {
2349
- const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
2350
- const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
2351
- const state = {
2352
- generatedAt: inputs.generatedAt,
2353
- session: {},
2354
- cost: summarizeCostRecords(inputs.costRecords),
2355
- recentRoutes: recentRoutes(inputs.costRecords, recentRoutesLimit),
2356
- team: [...inputs.team],
2357
- activity: recentActivity(inputs.activity, activityLimit)
2358
- };
2359
- if (inputs.session?.id !== undefined) {
2360
- state.session.id = inputs.session.id;
2361
- }
2362
- if (inputs.session?.startedAt !== undefined) {
2363
- state.session.startedAt = inputs.session.startedAt;
2364
- }
2365
- const loop = loopFrom(inputs.backlog);
2366
- if (loop !== undefined) {
2367
- state.loop = loop;
2368
- }
2369
- return state;
2370
- }
2371
-
2372
- // src/web/server.ts
2373
- var SSE_HEADERS = {
2374
- "content-type": "text/event-stream",
2375
- "cache-control": "no-cache",
2376
- connection: "keep-alive"
2377
- };
2378
- var JSON_HEADERS = { "content-type": "application/json" };
2379
- var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
2380
- function isAddressInUse(error) {
2381
- return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
2382
- }
2383
- async function renderState(deps) {
2384
- return renderDashboardHtml(buildDashboardState(await deps.readSnapshot()), {
2385
- refreshMs: deps.config.refreshMs
2386
- });
2387
- }
2388
- async function stateJson(deps) {
2389
- return JSON.stringify(buildDashboardState(await deps.readSnapshot()));
2390
- }
2391
- function sseMessage(json) {
2392
- return `event: state
2393
- data: ${json}
2394
-
2395
- `;
2396
- }
2397
- function createDashboardServer(deps) {
2398
- const serve = deps.serve ?? Bun.serve;
2399
- const log = deps.log ?? ((message) => console.log(message));
2400
- const encoder = new TextEncoder;
2401
- const clients = new Set;
2402
- const handler = async (request) => {
2403
- const url2 = new URL(request.url);
2404
- if (request.method !== "GET") {
2405
- return new Response("method not allowed", { status: 405 });
2406
- }
2407
- if (url2.pathname === "/" || url2.pathname === "/index.html") {
2408
- return new Response(await renderState(deps), { headers: HTML_HEADERS });
2409
- }
2410
- if (url2.pathname === "/api/state") {
2411
- return new Response(await stateJson(deps), { headers: JSON_HEADERS });
2412
- }
2413
- if (url2.pathname === "/healthz") {
2414
- return new Response(JSON.stringify({ ok: true }), {
2415
- headers: JSON_HEADERS
2416
- });
2417
- }
2418
- if (url2.pathname === "/favicon.ico") {
2419
- return new Response(null, { status: 204 });
2420
- }
2421
- if (url2.pathname === "/events") {
2422
- const initial = await stateJson(deps);
2423
- const stream = new ReadableStream({
2424
- start(controller) {
2425
- controller.enqueue(encoder.encode(sseMessage(initial)));
2426
- clients.add(controller);
2427
- },
2428
- cancel(controller) {
2429
- clients.delete(controller);
2430
- }
2431
- });
2432
- return new Response(stream, { headers: SSE_HEADERS });
2433
- }
2434
- return new Response("not found", { status: 404 });
2435
- };
2436
- const maxAttempts = deps.config.autoPortFallback ? 20 : 1;
2437
- let server;
2438
- let lastError;
2439
- for (let offset = 0;offset < maxAttempts; offset += 1) {
2440
- const port = deps.config.port + offset;
2441
- if (port > 65535) {
2442
- break;
2443
- }
2444
- try {
2445
- server = serve({
2446
- hostname: deps.config.host,
2447
- port,
2448
- fetch: handler
2449
- });
2450
- break;
2451
- } catch (error) {
2452
- lastError = error;
2453
- if (!isAddressInUse(error)) {
2454
- throw error;
2455
- }
2456
- }
2457
- }
2458
- if (server === undefined) {
2459
- throw lastError ?? new Error("dashboard: no available port");
2460
- }
2461
- const boundPort = server.port ?? deps.config.port;
2462
- const url = `http://${deps.config.host}:${boundPort}`;
2463
- log(`[openteam] dashboard en ${url}`);
2464
- const notify = async () => {
2465
- if (clients.size === 0) {
2466
- return;
2467
- }
2468
- const message = encoder.encode(sseMessage(await stateJson(deps)));
2469
- for (const controller of clients) {
2470
- try {
2471
- controller.enqueue(message);
2472
- } catch {
2473
- clients.delete(controller);
2474
- }
2475
- }
2476
- };
2477
- const close = () => {
2478
- for (const controller of clients) {
2479
- try {
2480
- controller.close();
2481
- } catch {}
2482
- }
2483
- clients.clear();
2484
- server.stop(true);
2485
- };
2486
- return { url, port: boundPort, notify, close };
2487
- }
2488
-
2489
- // src/web/snapshot.ts
2490
- function createSnapshotReader(deps, paths, context) {
2491
- return async () => {
2492
- const [telemetryText, backlogText, agentFiles, lastCommit] = await Promise.all([
2493
- deps.readText(paths.telemetryPath),
2494
- deps.readText(paths.backlogPath),
2495
- deps.listAgentFiles(paths.agentDir),
2496
- deps.gitLastCommit?.() ?? Promise.resolve(undefined)
2497
- ]);
2498
- const costRecords = parseCostRecordsJsonl(telemetryText ?? "");
2499
- const team = agentFiles.map(parseAgentFile);
2500
- const inputs = {
2501
- generatedAt: deps.now(),
2502
- costRecords,
2503
- team,
2504
- activity: context.activity()
2505
- };
2506
- if (backlogText !== undefined) {
2507
- const backlog = {
2508
- path: paths.backlogPath,
2509
- items: parseBacklog(backlogText)
2287
+ after(input) {
2288
+ const mapKey = key(input.sessionID, input.callID);
2289
+ const start = started.get(mapKey);
2290
+ started.delete(mapKey);
2291
+ const end = now();
2292
+ const durationMs = start === undefined ? 0 : Math.max(0, end - start);
2293
+ const event = {
2294
+ v: EVENT_SCHEMA_VERSION,
2295
+ type: "toolcall",
2296
+ ts: end,
2297
+ sessionID: input.sessionID,
2298
+ tool: input.tool,
2299
+ callID: input.callID,
2300
+ durationMs,
2301
+ ok: true
2510
2302
  };
2511
- if (lastCommit !== undefined) {
2512
- backlog.lastCommit = lastCommit;
2513
- }
2514
- inputs.backlog = backlog;
2515
- }
2516
- if (context.session !== undefined) {
2517
- inputs.session = context.session;
2518
- }
2519
- if (context.recentRoutesLimit !== undefined) {
2520
- inputs.recentRoutesLimit = context.recentRoutesLimit;
2521
- }
2522
- return inputs;
2523
- };
2524
- }
2525
-
2526
- // src/web/watch.ts
2527
- function watchSources(paths, onChange, deps, debounceMs = 250) {
2528
- const setTimer = deps.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
2529
- const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
2530
- const watchers = [];
2531
- let pending;
2532
- const trigger = () => {
2533
- if (pending !== undefined) {
2534
- clearTimer(pending);
2535
- }
2536
- pending = setTimer(() => {
2537
- pending = undefined;
2538
- onChange();
2539
- }, debounceMs);
2540
- };
2541
- for (const path of paths) {
2542
- try {
2543
- watchers.push(deps.watch(path, trigger));
2544
- } catch {}
2545
- }
2546
- return {
2547
- close() {
2548
- if (pending !== undefined) {
2549
- clearTimer(pending);
2550
- pending = undefined;
2551
- }
2552
- for (const watcher of watchers) {
2553
- watcher.close();
2554
- }
2555
- }
2556
- };
2557
- }
2558
-
2559
- // src/web/start.ts
2560
- function startDashboard(deps) {
2561
- const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
2562
- const activity = createActivityBuffer(deps.config.recentRoutes);
2563
- const readSnapshot = createSnapshotReader({
2564
- readText: deps.readText,
2565
- listAgentFiles: deps.listAgentFiles,
2566
- now: deps.now,
2567
- ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
2568
- }, {
2569
- telemetryPath: deps.telemetryPath,
2570
- backlogPath,
2571
- agentDir: deps.agentDir
2572
- }, {
2573
- activity: () => activity.list(),
2574
- recentRoutesLimit: deps.config.recentRoutes,
2575
- ...deps.session !== undefined ? { session: deps.session } : {}
2576
- });
2577
- const createServer = deps.serve ?? createDashboardServer;
2578
- const server = createServer({
2579
- readSnapshot,
2580
- config: deps.config,
2581
- ...deps.log !== undefined ? { log: deps.log } : {}
2582
- });
2583
- const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
2584
- const watcher = watchSources([deps.telemetryPath, backlogPath, deps.agentDir], () => {
2585
- server.notify();
2586
- }, { watch: watchFn });
2587
- return {
2588
- url: server.url,
2589
- port: server.port,
2590
- pushActivity(entry) {
2591
- activity.push(entry);
2592
- server.notify();
2593
- },
2594
- close() {
2595
- watcher.close();
2596
- server.close();
2303
+ return deps.sink.emit(event);
2597
2304
  }
2598
2305
  };
2599
2306
  }
@@ -2621,9 +2328,11 @@ function telemetryOptions(rawOptions) {
2621
2328
  const telemetry = rawOptions !== null && typeof rawOptions === "object" && "telemetry" in rawOptions && rawOptions.telemetry !== null && typeof rawOptions.telemetry === "object" ? rawOptions.telemetry : {};
2622
2329
  const enabled = !("enabled" in telemetry) || telemetry.enabled !== false;
2623
2330
  const configuredPath = "path" in telemetry && typeof telemetry.path === "string" ? telemetry.path : DEFAULT_TELEMETRY_PATH;
2331
+ const configuredSessionsDir = "sessionsDir" in telemetry && typeof telemetry.sessionsDir === "string" ? telemetry.sessionsDir : DEFAULT_SESSIONS_DIR;
2624
2332
  return {
2625
2333
  enabled,
2626
- path: configuredPath
2334
+ path: configuredPath,
2335
+ sessionsDir: configuredSessionsDir
2627
2336
  };
2628
2337
  }
2629
2338
  function createFileAppender(filePath, deps = {}) {
@@ -2634,65 +2343,36 @@ function createFileAppender(filePath, deps = {}) {
2634
2343
  await appendFileFn(filePath, line, "utf8");
2635
2344
  };
2636
2345
  }
2637
- function createTelemetrySink(rawOptions) {
2346
+ function createPathAppender(deps = {}) {
2347
+ const mkdirFn = deps.mkdir ?? mkdir2;
2348
+ const appendFileFn = deps.appendFile ?? appendFile;
2349
+ return async (path, line) => {
2350
+ await mkdirFn(dirname2(path), { recursive: true });
2351
+ await appendFileFn(path, line, "utf8");
2352
+ };
2353
+ }
2354
+ function createEventSink(rawOptions) {
2638
2355
  const options = telemetryOptions(rawOptions);
2639
2356
  if (!options.enabled) {
2640
- return createNullSink();
2357
+ return createNullEventSink();
2641
2358
  }
2642
- return createJsonlSink({
2643
- append: createFileAppender(options.path),
2359
+ return createEventLogSink({
2360
+ dir: options.sessionsDir,
2361
+ appendLine: createPathAppender(),
2362
+ join,
2644
2363
  onError: logTelemetryError
2645
2364
  });
2646
2365
  }
2647
- function isMissingFile2(error) {
2366
+ function isMissingFile(error) {
2648
2367
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2649
2368
  }
2650
- async function readTextOptional(path) {
2651
- try {
2652
- return await readFile2(path, "utf8");
2653
- } catch (error) {
2654
- if (isMissingFile2(error)) {
2655
- return;
2656
- }
2657
- throw error;
2658
- }
2659
- }
2660
- function createGitLastCommit(exec) {
2661
- return async () => {
2662
- const output = await exec("git", [
2663
- "log",
2664
- "-1",
2665
- "--pretty=format:%h%x1f%s%x1f%cI"
2666
- ]);
2667
- if (output.exitCode !== 0) {
2668
- return;
2669
- }
2670
- const [hash, subject, at] = output.stdout.trim().split("\x1F");
2671
- if (hash === undefined || subject === undefined || at === undefined) {
2672
- return;
2673
- }
2674
- return { hash, subject, at };
2675
- };
2676
- }
2677
- var DASHBOARD_EVENT_SUMMARIES = {
2678
- "session.created": "Sesión creada",
2679
- "session.idle": "Sesión en reposo",
2680
- "session.error": "Error de sesión"
2681
- };
2682
- function activityFromEvent(event, now) {
2683
- const summary = DASHBOARD_EVENT_SUMMARIES[event.type];
2684
- if (summary === undefined) {
2685
- return;
2686
- }
2687
- return { ts: now(), kind: "agent", summary };
2688
- }
2689
- function createCliDeps(config, registry, telemetryPath) {
2369
+ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SESSIONS_DIR) {
2690
2370
  return {
2691
2371
  loadConfig: async (path) => {
2692
2372
  try {
2693
- return loadOpenTeamConfig(JSON.parse(await readFile2(path, "utf8")));
2373
+ return loadOpenTeamConfig(JSON.parse(await readFile(path, "utf8")));
2694
2374
  } catch (error) {
2695
- if (isMissingFile2(error)) {
2375
+ if (isMissingFile(error)) {
2696
2376
  return config;
2697
2377
  }
2698
2378
  throw error;
@@ -2701,9 +2381,9 @@ function createCliDeps(config, registry, telemetryPath) {
2701
2381
  saveConfig: (nextConfig, path) => writeOpenTeamConfigFile(nextConfig, path),
2702
2382
  readOpencodeConfig: async (path) => {
2703
2383
  try {
2704
- return JSON.parse(await readFile2(path, "utf8"));
2384
+ return JSON.parse(await readFile(path, "utf8"));
2705
2385
  } catch (error) {
2706
- if (isMissingFile2(error)) {
2386
+ if (isMissingFile(error)) {
2707
2387
  return;
2708
2388
  }
2709
2389
  throw error;
@@ -2723,7 +2403,7 @@ function createCliDeps(config, registry, telemetryPath) {
2723
2403
  try {
2724
2404
  entries = await readdir(dir);
2725
2405
  } catch (error) {
2726
- if (isMissingFile2(error)) {
2406
+ if (isMissingFile(error)) {
2727
2407
  return [];
2728
2408
  }
2729
2409
  throw error;
@@ -2731,11 +2411,33 @@ function createCliDeps(config, registry, telemetryPath) {
2731
2411
  const mdFiles = entries.filter((entry) => entry.endsWith(".md"));
2732
2412
  return Promise.all(mdFiles.map(async (entry) => ({
2733
2413
  name: entry.replace(/\.md$/, ""),
2734
- contents: await readFile2(join(dir, entry), "utf8")
2414
+ contents: await readFile(join(dir, entry), "utf8")
2735
2415
  })));
2736
2416
  },
2737
2417
  probe: (nextConfig) => registry.probe(nextConfig.local.runtimes),
2738
- readTelemetry: (path) => readCostRecords(path),
2418
+ readTelemetry: (path) => readRouteCostRecords(sessionsDir, {
2419
+ listFiles: async (dir) => {
2420
+ try {
2421
+ return await readdir(dir);
2422
+ } catch (error) {
2423
+ if (isMissingFile(error)) {
2424
+ return [];
2425
+ }
2426
+ throw error;
2427
+ }
2428
+ },
2429
+ readText: async (filePath) => {
2430
+ try {
2431
+ return await readFile(filePath, "utf8");
2432
+ } catch (error) {
2433
+ if (isMissingFile(error)) {
2434
+ return;
2435
+ }
2436
+ throw error;
2437
+ }
2438
+ },
2439
+ legacyTelemetryPath: path
2440
+ }),
2739
2441
  configPath: DEFAULT_CONFIG_PATH,
2740
2442
  telemetryPath,
2741
2443
  opencodeConfigPath: OPENCODE_CONFIG_PATH,
@@ -2757,43 +2459,36 @@ var server = async (ctx, rawOptions) => {
2757
2459
  });
2758
2460
  cache.refresh();
2759
2461
  const telemetryPath = telemetryOptions(rawOptions).path;
2760
- const hooks = createHooks(config, cache.get, {
2761
- sink: createTelemetrySink(rawOptions)
2762
- });
2763
- const cliDeps = createCliDeps(config, registry, telemetryPath);
2764
- let dashboard;
2765
- if (config.dashboard.enabled) {
2766
- try {
2767
- dashboard = startDashboard({
2768
- config: config.dashboard,
2769
- telemetryPath,
2770
- agentDir: dirname2(ORCHESTRATOR_AGENT_PATH),
2771
- backlogPath: DEFAULT_BACKLOG_PATH,
2772
- readText: readTextOptional,
2773
- listAgentFiles: cliDeps.listAgentFiles,
2774
- now: () => new Date().toISOString(),
2775
- gitLastCommit: createGitLastCommit(createShellExec(ctx.$)),
2776
- log: (message) => console.log(`[openteam] ${message}`)
2777
- });
2778
- console.log(`[openteam] dashboard en ${dashboard.url}`);
2779
- } catch (error) {
2780
- const message = error instanceof Error ? error.message : String(error);
2781
- console.warn(`[openteam] no se pudo iniciar el dashboard: ${message}`);
2782
- }
2783
- }
2462
+ const sessionsDir = telemetryOptions(rawOptions).sessionsDir;
2463
+ const sink = createEventSink(rawOptions);
2464
+ const hooks = createHooks(config, cache.get, { sink });
2465
+ const cliDeps = createCliDeps(config, registry, telemetryPath, sessionsDir);
2466
+ const toolcalls = createToolcallTracker({ sink });
2467
+ const now = () => Date.now();
2784
2468
  return {
2785
2469
  ...hooks,
2786
2470
  tool: {
2787
2471
  openteam: createCommandTool(cliDeps)
2788
2472
  },
2473
+ "tool.execute.before": async (input) => {
2474
+ toolcalls.before(input);
2475
+ },
2476
+ "tool.execute.after": async (input) => {
2477
+ await toolcalls.after(input);
2478
+ },
2789
2479
  event: async ({ event }) => {
2790
2480
  if (event.type === "session.created" || event.type === "session.idle") {
2791
2481
  cache.refresh();
2792
2482
  }
2793
- if (dashboard !== undefined) {
2794
- const entry = activityFromEvent(event, () => Date.now());
2795
- if (entry !== undefined) {
2796
- dashboard.pushActivity(entry);
2483
+ const activity = activityEventFrom(event, now);
2484
+ if (activity !== undefined) {
2485
+ sink.emit(activity);
2486
+ }
2487
+ if (event.type === "message.updated") {
2488
+ const info = event.properties?.info;
2489
+ const message = info === undefined ? undefined : messageEventFrom(info, now);
2490
+ if (message !== undefined) {
2491
+ sink.emit(message);
2797
2492
  }
2798
2493
  }
2799
2494
  }
@@ -2808,9 +2503,10 @@ export {
2808
2503
  server,
2809
2504
  logTelemetryError,
2810
2505
  logAvailabilityRefreshError,
2811
- isMissingFile2 as isMissingFile,
2506
+ isMissingFile,
2812
2507
  src_default as default,
2508
+ createPathAppender,
2813
2509
  createFileAppender,
2814
- createCliDeps,
2815
- activityFromEvent
2510
+ createEventSink,
2511
+ createCliDeps
2816
2512
  };