@jmanuelcorral/openteam 0.1.21 → 0.1.23

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 (43) hide show
  1. package/README.md +29 -31
  2. package/dist/cli/dashboardServe.d.ts +15 -8
  3. package/dist/cli/dashboardServe.d.ts.map +1 -1
  4. package/dist/cli.js +936 -286
  5. package/dist/commands/dashboard.d.ts +4 -4
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/orchestratorAgent.d.ts.map +1 -1
  8. package/dist/commands/setup.d.ts.map +1 -1
  9. package/dist/commands/slashCommand.d.ts +32 -8
  10. package/dist/commands/slashCommand.d.ts.map +1 -1
  11. package/dist/contract/opencode.d.ts +1 -1
  12. package/dist/contract/opencode.d.ts.map +1 -1
  13. package/dist/dashboard/render.d.ts.map +1 -1
  14. package/dist/dashboard/state.d.ts +14 -6
  15. package/dist/dashboard/state.d.ts.map +1 -1
  16. package/dist/dashboard/types.d.ts +14 -3
  17. package/dist/dashboard/types.d.ts.map +1 -1
  18. package/dist/index.d.ts +8 -9
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +476 -802
  21. package/dist/orchestrator/coordinator.d.ts +14 -2
  22. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  23. package/dist/plugin/capture.d.ts +46 -0
  24. package/dist/plugin/capture.d.ts.map +1 -0
  25. package/dist/plugin/hooks.d.ts +2 -2
  26. package/dist/plugin/hooks.d.ts.map +1 -1
  27. package/dist/plugin/toolcalls.d.ts +21 -0
  28. package/dist/plugin/toolcalls.d.ts.map +1 -0
  29. package/dist/telemetry/aggregate.d.ts +89 -0
  30. package/dist/telemetry/aggregate.d.ts.map +1 -0
  31. package/dist/telemetry/decisions.d.ts +25 -0
  32. package/dist/telemetry/decisions.d.ts.map +1 -0
  33. package/dist/telemetry/eventLog.d.ts +63 -0
  34. package/dist/telemetry/eventLog.d.ts.map +1 -0
  35. package/dist/telemetry/events.d.ts +217 -0
  36. package/dist/telemetry/events.d.ts.map +1 -0
  37. package/dist/web/snapshot.d.ts +13 -7
  38. package/dist/web/snapshot.d.ts.map +1 -1
  39. package/dist/web/start.d.ts +10 -7
  40. package/dist/web/start.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/web/activity.d.ts +0 -12
  43. 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,29 +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 dentro de opencode, pon "dashboard": { "enabled": true }',
978
- "en .opencode/openteam.json y recarga opencode. Se servirá (loopback) en:",
979
- ` ${url}`,
980
- "",
981
- "O véelo ahora mismo sin editar la config:",
982
- " openteam dashboard --serve (añade --open para abrir el navegador)"
983
- ].join(`
984
- `);
985
- }
986
1173
  return [
987
- "Dashboard: habilitado.",
1174
+ "Dashboard (multi-sesión, se lanza desde la CLI):",
988
1175
  ` URL: ${url}`,
989
1176
  ` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
990
1177
  ` Rutas: últimas ${dashboard.recentRoutes}`,
991
1178
  dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
992
1179
  "",
993
- "Nota: solo escucha en loopback y nunca expone prompts (solo hashes).",
994
- "El servidor corre dentro de la sesión de opencode; se cierra al salir.",
995
- "Para verlo fuera de opencode: openteam dashboard --serve"
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)."
996
1186
  ].join(`
997
1187
  `);
998
1188
  }
@@ -1146,8 +1336,8 @@ var HELP = [
1146
1336
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
1147
1337
  " openteam doctor Diagnóstico de runtimes y config",
1148
1338
  " openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
1149
- " openteam dashboard Estado y URL del dashboard web (loopback)",
1150
- " openteam dashboard --serve Levanta el dashboard web (Ctrl+C para parar; --open abre el navegador)",
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",
1151
1341
  " openteam report Resumen de coste/ahorro (telemetría)",
1152
1342
  " openteam yolo status Muestra si el modo YOLO está activo",
1153
1343
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -1819,81 +2009,67 @@ function toCostRecord(decision2, context) {
1819
2009
  return record;
1820
2010
  }
1821
2011
 
1822
- // src/telemetry/hash.ts
1823
- var FNV_OFFSET_BASIS = 2166136261;
1824
- var FNV_PRIME = 16777619;
1825
- function hashPrompt(prompt) {
1826
- let hash = FNV_OFFSET_BASIS;
1827
- for (let index = 0;index < prompt.length; index += 1) {
1828
- hash ^= prompt.charCodeAt(index);
1829
- hash = Math.imul(hash, FNV_PRIME);
1830
- }
1831
- return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
1832
- }
1833
-
1834
2012
  // src/telemetry/types.ts
1835
- import { z as z3 } from "zod";
1836
-
1837
- // src/capabilities/types.ts
1838
- import { z as z2 } from "zod";
1839
- var CapabilityTierSchema = z2.union([
1840
- z2.literal(0),
1841
- z2.literal(1),
1842
- z2.literal(2),
1843
- z2.literal(3),
1844
- z2.literal(4),
1845
- z2.literal(5)
1846
- ]);
1847
- var ComplexityTierSchema = z2.enum([
1848
- "trivial",
1849
- "simple",
1850
- "moderate",
1851
- "hard"
1852
- ]);
1853
- var ModelCapabilityProfileSchema = z2.object({
1854
- ref: z2.object({
1855
- providerID: z2.string().min(1),
1856
- modelID: z2.string().min(1)
1857
- }),
1858
- kind: z2.enum(["local", "frontier", "router"]),
1859
- contextWindow: z2.number().int().positive(),
1860
- maxOutputTokens: z2.number().int().positive(),
1861
- supportsToolCalling: z2.boolean(),
1862
- supportsVision: z2.boolean(),
1863
- reasoningTier: CapabilityTierSchema,
1864
- codeQualityTier: CapabilityTierSchema,
1865
- costPer1M: z2.object({
1866
- inputUSD: z2.number().min(0),
1867
- outputUSD: z2.number().min(0)
1868
- }),
1869
- availability: z2.enum(["available", "degraded", "unavailable"])
1870
- });
1871
-
1872
- // src/telemetry/types.ts
1873
- var CostRecordSchema = z3.object({
1874
- ts: z3.number().finite(),
1875
- sessionID: z3.string().min(1).optional(),
1876
- promptHash: z3.string().min(1),
1877
- 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),
1878
2019
  tier: ComplexityTierSchema,
1879
- routeKind: z3.enum(["local", "frontier"]),
2020
+ routeKind: z4.enum(["local", "frontier"]),
1880
2021
  selected: ModelRefSchema,
1881
- rationale: z3.string(),
1882
- estimatedCostUSD: z3.number().finite().min(0),
1883
- baselineCostUSD: z3.number().finite().min(0),
1884
- estimatedSavingsUSD: z3.number().finite(),
1885
- budgetAction: z3.string().min(1),
1886
- tokensIn: z3.number().int().min(0).optional(),
1887
- 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()
1888
2029
  });
1889
2030
 
1890
- // src/telemetry/sink.ts
1891
- 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;
1892
2067
  return {
1893
- record: async (record) => {
2068
+ emit: async (event) => {
1894
2069
  try {
1895
- const parsed = CostRecordSchema.parse(record);
1896
- 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)}
1897
2073
  `);
1898
2074
  } catch (error) {
1899
2075
  deps.onError?.(error);
@@ -1901,10 +2077,117 @@ function createJsonlSink(deps) {
1901
2077
  }
1902
2078
  };
1903
2079
  }
1904
- function createNullSink() {
1905
- return {
1906
- 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
1907
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")}`;
1908
2191
  }
1909
2192
 
1910
2193
  // src/plugin/hooks.ts
@@ -1937,8 +2220,8 @@ function promptText(output) {
1937
2220
  return output.parts.map(partText).join(`
1938
2221
  `);
1939
2222
  }
1940
- function emitTelemetry(sink, record) {
1941
- Promise.resolve().then(() => sink.record(record)).catch(() => {});
2223
+ function emitTelemetry(sink, event) {
2224
+ Promise.resolve().then(() => sink.emit(event)).catch(() => {});
1942
2225
  }
1943
2226
  function deriveTaskSignals(input, output) {
1944
2227
  const prompt = promptText(output);
@@ -1959,7 +2242,7 @@ function deriveTaskSignals(input, output) {
1959
2242
  return signals;
1960
2243
  }
1961
2244
  function createChatMessageHook(config, getAvailable, options = {}) {
1962
- const sink = options.sink ?? createNullSink();
2245
+ const sink = options.sink ?? createNullEventSink();
1963
2246
  const now = options.now ?? Date.now;
1964
2247
  return async (input, output) => {
1965
2248
  const signals = deriveTaskSignals(input, output);
@@ -1981,7 +2264,7 @@ function createChatMessageHook(config, getAvailable, options = {}) {
1981
2264
  if (signals.estimatedOutputTokens !== undefined) {
1982
2265
  telemetryContext.tokensOut = signals.estimatedOutputTokens;
1983
2266
  }
1984
- emitTelemetry(sink, toCostRecord(decision2, telemetryContext));
2267
+ emitTelemetry(sink, costRecordToRouteEvent(toCostRecord(decision2, telemetryContext)));
1985
2268
  };
1986
2269
  }
1987
2270
  function createHooks(config, getAvailable, options = {}) {
@@ -1990,649 +2273,34 @@ function createHooks(config, getAvailable, options = {}) {
1990
2273
  };
1991
2274
  }
1992
2275
 
1993
- // src/telemetry/read.ts
1994
- import { readFile } from "node:fs/promises";
1995
- var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
1996
- function parseCostRecordsJsonl(text) {
1997
- const records = [];
1998
- for (const line of text.split(`
1999
- `)) {
2000
- const trimmed = line.trim();
2001
- if (trimmed.length === 0) {
2002
- continue;
2003
- }
2004
- let candidate;
2005
- try {
2006
- candidate = JSON.parse(trimmed);
2007
- } catch {
2008
- continue;
2009
- }
2010
- const parsed = CostRecordSchema.safeParse(candidate);
2011
- if (parsed.success) {
2012
- records.push(parsed.data);
2013
- }
2014
- }
2015
- return records;
2276
+ // src/plugin/toolcalls.ts
2277
+ function key(sessionID, callID) {
2278
+ return `${sessionID}\x00${callID}`;
2016
2279
  }
2017
- function isMissingFile(error) {
2018
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2019
- }
2020
- async function readCostRecords(path, deps = {}) {
2021
- const readFileFn = deps.readFile ?? readFile;
2022
- try {
2023
- const content = await readFileFn(path, "utf8");
2024
- return parseCostRecordsJsonl(content);
2025
- } catch (error) {
2026
- if (isMissingFile(error)) {
2027
- return [];
2028
- }
2029
- throw error;
2030
- }
2031
- }
2032
-
2033
- // src/web/git.ts
2034
- function createGitLastCommit(exec) {
2035
- return async () => {
2036
- const output = await exec("git", [
2037
- "log",
2038
- "-1",
2039
- "--pretty=format:%h%x1f%s%x1f%cI"
2040
- ]);
2041
- if (output.exitCode !== 0) {
2042
- return;
2043
- }
2044
- const [hash, subject, at] = output.stdout.trim().split("\x1F");
2045
- if (hash === undefined || subject === undefined || at === undefined) {
2046
- return;
2047
- }
2048
- return { hash, subject, at };
2049
- };
2050
- }
2051
-
2052
- // src/web/paths.ts
2053
- var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
2054
-
2055
- // src/web/start.ts
2056
- import { watch as fsWatch } from "node:fs";
2057
-
2058
- // src/web/activity.ts
2059
- function createActivityBuffer(max = 100) {
2060
- const entries = [];
2280
+ function createToolcallTracker(deps) {
2281
+ const now = deps.now ?? Date.now;
2282
+ const started = new Map;
2061
2283
  return {
2062
- push(entry) {
2063
- entries.push(entry);
2064
- if (entries.length > max) {
2065
- entries.splice(0, entries.length - max);
2066
- }
2284
+ before(input) {
2285
+ started.set(key(input.sessionID, input.callID), now());
2067
2286
  },
2068
- list() {
2069
- return [...entries];
2070
- }
2071
- };
2072
- }
2073
-
2074
- // src/web/server.ts
2075
- import {
2076
- createServer as createHttpServer
2077
- } from "node:http";
2078
-
2079
- // src/dashboard/render.ts
2080
- var TIERS2 = [
2081
- "trivial",
2082
- "simple",
2083
- "moderate",
2084
- "hard"
2085
- ];
2086
- function escapeHtml(value) {
2087
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2088
- }
2089
- function usd2(value) {
2090
- return `$${value.toFixed(5)}`;
2091
- }
2092
- function pct(value) {
2093
- return `${value.toFixed(2)}%`;
2094
- }
2095
- function agentModel(agent) {
2096
- return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
2097
- }
2098
- function summaryPanel(cost) {
2099
- return [
2100
- '<section class="panel" id="panel-summary">',
2101
- "<h2>Resumen de routing</h2>",
2102
- '<div class="grid">',
2103
- `<div class="stat"><span class="k">Decisiones</span><span class="v">${cost.count}</span></div>`,
2104
- `<div class="stat"><span class="k">Local</span><span class="v">${cost.localCount}</span></div>`,
2105
- `<div class="stat"><span class="k">Frontier</span><span class="v">${cost.frontierCount}</span></div>`,
2106
- `<div class="stat"><span class="k">Coste real</span><span class="v">${usd2(cost.totalEstimatedUSD)}</span></div>`,
2107
- `<div class="stat"><span class="k">Baseline</span><span class="v">${usd2(cost.totalBaselineUSD)}</span></div>`,
2108
- `<div class="stat"><span class="k">Ahorro</span><span class="v good">${usd2(cost.totalSavingsUSD)} (${pct(cost.savingsPct)})</span></div>`,
2109
- `<div class="stat"><span class="k">Tokens in</span><span class="v">${cost.tokensIn}</span></div>`,
2110
- `<div class="stat"><span class="k">Tokens out</span><span class="v">${cost.tokensOut}</span></div>`,
2111
- "</div>",
2112
- "</section>"
2113
- ].join("");
2114
- }
2115
- function tierPanel(cost) {
2116
- const rows = TIERS2.map((tier) => {
2117
- const summary = cost.byTier[tier];
2118
- return `<tr><td>${tier}</td><td>${summary.count}</td><td>${usd2(summary.estimatedUSD)}</td><td class="good">${usd2(summary.savingsUSD)}</td></tr>`;
2119
- }).join("");
2120
- return [
2121
- '<section class="panel" id="panel-tier">',
2122
- "<h2>Por tier</h2>",
2123
- "<table><thead><tr><th>Tier</th><th>Nº</th><th>Coste</th><th>Ahorro</th></tr></thead>",
2124
- `<tbody>${rows}</tbody></table>`,
2125
- "</section>"
2126
- ].join("");
2127
- }
2128
- function loopItemRow(item) {
2129
- const box = item.done ? "☑" : "☐";
2130
- const cls = item.done ? "done" : "open";
2131
- const who = item.assignee === undefined ? "" : `<span class="who">@${escapeHtml(item.assignee)}</span> `;
2132
- return `<li class="${cls}"><span class="box">${box}</span> ${who}${escapeHtml(item.text)}</li>`;
2133
- }
2134
- function loopPanel(loop) {
2135
- if (loop === undefined) {
2136
- return [
2137
- '<section class="panel" id="panel-loop">',
2138
- "<h2>Loop</h2>",
2139
- '<p class="muted">Sin backlog activo (no hay <code>openteam-backlog.md</code>).</p>',
2140
- "</section>"
2141
- ].join("");
2142
- }
2143
- const items = loop.items.map(loopItemRow).join("");
2144
- const commit = loop.lastCommit === undefined ? "" : `<p class="muted">Último commit: <code>${escapeHtml(loop.lastCommit.hash)}</code> ${escapeHtml(loop.lastCommit.subject)}</p>`;
2145
- return [
2146
- '<section class="panel" id="panel-loop">',
2147
- "<h2>Loop</h2>",
2148
- `<div class="progress" role="progressbar" aria-valuenow="${loop.progressPct}" aria-valuemin="0" aria-valuemax="100"><div class="bar" style="width:${loop.progressPct}%"></div></div>`,
2149
- `<p class="muted">${loop.done}/${loop.total} completados · ${loop.open} abiertos · ${loop.progressPct}%</p>`,
2150
- `<ul class="items">${items}</ul>`,
2151
- commit,
2152
- "</section>"
2153
- ].join("");
2154
- }
2155
- function teamPanel(team) {
2156
- if (team.length === 0) {
2157
- return [
2158
- '<section class="panel" id="panel-team">',
2159
- "<h2>Equipo</h2>",
2160
- '<p class="muted">Sin agentes en <code>.opencode/agent/</code>.</p>',
2161
- "</section>"
2162
- ].join("");
2163
- }
2164
- const rows = team.map((agent) => `<tr><td>${escapeHtml(agent.name)}</td><td>${escapeHtml(agent.mode)}</td><td>${escapeHtml(agentModel(agent))}</td></tr>`).join("");
2165
- return [
2166
- '<section class="panel" id="panel-team">',
2167
- "<h2>Equipo</h2>",
2168
- "<table><thead><tr><th>Agente</th><th>Modo</th><th>LLM</th></tr></thead>",
2169
- `<tbody>${rows}</tbody></table>`,
2170
- "</section>"
2171
- ].join("");
2172
- }
2173
- function routeRow(route) {
2174
- 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>`;
2175
- }
2176
- function routesPanel(routes) {
2177
- if (routes.length === 0) {
2178
- return [
2179
- '<section class="panel" id="panel-routes">',
2180
- "<h2>Decisiones recientes</h2>",
2181
- '<p class="muted">Sin decisiones de routing todavía.</p>',
2182
- "</section>"
2183
- ].join("");
2184
- }
2185
- const rows = routes.map(routeRow).join("");
2186
- return [
2187
- '<section class="panel" id="panel-routes">',
2188
- "<h2>Decisiones recientes</h2>",
2189
- "<table><thead><tr><th>Tier</th><th>Ruta</th><th>Modelo</th><th>Prompt (hash)</th><th>Coste</th><th>Ahorro</th></tr></thead>",
2190
- `<tbody>${rows}</tbody></table>`,
2191
- "</section>"
2192
- ].join("");
2193
- }
2194
- function activityRow(entry) {
2195
- const who = entry.agent === undefined ? "" : `<span class="who">${escapeHtml(entry.agent)}</span> `;
2196
- return `<li class="${entry.kind}"><span class="kind">${entry.kind}</span> ${who}${escapeHtml(entry.summary)}</li>`;
2197
- }
2198
- function activityPanel(activity) {
2199
- if (activity.length === 0) {
2200
- return [
2201
- '<section class="panel" id="panel-activity">',
2202
- "<h2>Actividad</h2>",
2203
- '<p class="muted">Sin actividad registrada.</p>',
2204
- "</section>"
2205
- ].join("");
2206
- }
2207
- const rows = activity.map(activityRow).join("");
2208
- return [
2209
- '<section class="panel" id="panel-activity">',
2210
- "<h2>Actividad</h2>",
2211
- `<ul class="timeline">${rows}</ul>`,
2212
- "</section>"
2213
- ].join("");
2214
- }
2215
- var STYLE = `
2216
- :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
2217
- *{box-sizing:border-box}
2218
- body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}
2219
- header{padding:16px 24px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}
2220
- header h1{font-size:18px;margin:0}
2221
- header .meta{color:var(--muted);font-size:12px}
2222
- main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px;padding:24px}
2223
- .panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px}
2224
- .panel h2{font-size:14px;margin:0 0 12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
2225
- .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
2226
- .stat{display:flex;flex-direction:column;gap:2px}
2227
- .stat .k{color:var(--muted);font-size:12px}
2228
- .stat .v{font-size:18px;font-weight:600}
2229
- .good{color:var(--good)}
2230
- .local{color:var(--local)}
2231
- .frontier{color:var(--frontier)}
2232
- .muted{color:var(--muted)}
2233
- table{width:100%;border-collapse:collapse;font-size:13px}
2234
- th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)}
2235
- th{color:var(--muted);font-weight:600}
2236
- .hash{font-family:ui-monospace,Consolas,monospace;color:var(--muted)}
2237
- .progress{height:10px;background:#0b0d11;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
2238
- .progress .bar{height:100%;background:var(--good)}
2239
- ul.items,ul.timeline{list-style:none;margin:8px 0 0;padding:0;max-height:320px;overflow:auto}
2240
- ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
2241
- ul.items li.done{color:var(--muted)}
2242
- ul.items .box{font-family:monospace}
2243
- .who{color:var(--local)}
2244
- .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
2245
- code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
2246
- `;
2247
- var CLIENT_JS = `
2248
- (function(){
2249
- function reloadIfChanged(prev){
2250
- fetch('/api/state').then(function(r){return r.json()}).then(function(s){
2251
- if(s.generatedAt!==prev){location.reload()}
2252
- }).catch(function(){});
2253
- }
2254
- var current=document.documentElement.getAttribute('data-generated')||'';
2255
- if('EventSource' in window){
2256
- try{
2257
- var es=new EventSource('/events');
2258
- es.addEventListener('state',function(){location.reload()});
2259
- es.onerror=function(){/* fallback below */};
2260
- }catch(e){/* ignore */}
2261
- }
2262
- var ms=parseInt(document.documentElement.getAttribute('data-refresh')||'2000',10);
2263
- setInterval(function(){reloadIfChanged(current)},isNaN(ms)?2000:Math.max(250,ms));
2264
- })();
2265
- `;
2266
- function renderDashboardHtml(state, options = {}) {
2267
- const refreshMs = options.refreshMs ?? 2000;
2268
- const body = [
2269
- summaryPanel(state.cost),
2270
- loopPanel(state.loop),
2271
- teamPanel(state.team),
2272
- tierPanel(state.cost),
2273
- routesPanel(state.recentRoutes),
2274
- activityPanel(state.activity)
2275
- ].join("");
2276
- const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
2277
- return [
2278
- "<!doctype html>",
2279
- `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}">`,
2280
- "<head>",
2281
- '<meta charset="utf-8">',
2282
- '<meta name="viewport" content="width=device-width,initial-scale=1">',
2283
- "<title>openteam dashboard</title>",
2284
- `<style>${STYLE}</style>`,
2285
- "</head>",
2286
- "<body>",
2287
- "<header>",
2288
- "<h1>openteam dashboard</h1>",
2289
- `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
2290
- "</header>",
2291
- `<main>${body}</main>`,
2292
- `<script>${CLIENT_JS}</script>`,
2293
- "</body>",
2294
- "</html>"
2295
- ].join("");
2296
- }
2297
-
2298
- // src/dashboard/backlog.ts
2299
- var ITEM_RE = /^\s*[-*+]\s+\[( |x|X)\]\s+(.*)$/;
2300
- var ASSIGNEE_RE = /^\[@([^\]]+)\]\s*(.*)$/;
2301
- function parseAssignee(text) {
2302
- const match = ASSIGNEE_RE.exec(text);
2303
- if (match === null) {
2304
- return { text: text.trim() };
2305
- }
2306
- const assignee = (match[1] ?? "").trim();
2307
- const rest = (match[2] ?? "").trim();
2308
- if (assignee.length === 0) {
2309
- return { text: text.trim() };
2310
- }
2311
- return { assignee, text: rest };
2312
- }
2313
- function parseBacklog(text) {
2314
- const items = [];
2315
- for (const line of text.split(/\r?\n/)) {
2316
- const match = ITEM_RE.exec(line);
2317
- if (match === null) {
2318
- continue;
2319
- }
2320
- const done = (match[1] ?? " ").toLowerCase() === "x";
2321
- const rawText = (match[2] ?? "").trim();
2322
- const { assignee, text: itemText } = parseAssignee(rawText);
2323
- const item = { text: itemText, done };
2324
- if (assignee !== undefined) {
2325
- item.assignee = assignee;
2326
- }
2327
- items.push(item);
2328
- }
2329
- return items;
2330
- }
2331
- function buildLoopSnapshot(backlogPath, items) {
2332
- const total = items.length;
2333
- const done = items.filter((item) => item.done).length;
2334
- const open = total - done;
2335
- const progressPct = total === 0 ? 100 : Math.round(done / total * 100);
2336
- return {
2337
- backlogPath,
2338
- total,
2339
- done,
2340
- open,
2341
- progressPct,
2342
- items: [...items]
2343
- };
2344
- }
2345
-
2346
- // src/dashboard/state.ts
2347
- var DEFAULT_RECENT_ROUTES = 50;
2348
- var DEFAULT_ACTIVITY_LIMIT = 100;
2349
- function toRouteView(record) {
2350
- return {
2351
- ts: record.ts,
2352
- tier: record.tier,
2353
- routeKind: record.routeKind,
2354
- model: `${record.selected.providerID}/${record.selected.modelID}`,
2355
- promptHash: record.promptHash,
2356
- estimatedCostUSD: record.estimatedCostUSD,
2357
- estimatedSavingsUSD: record.estimatedSavingsUSD,
2358
- budgetAction: record.budgetAction
2359
- };
2360
- }
2361
- function recentRoutes(records, limit) {
2362
- return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
2363
- }
2364
- function recentActivity(entries, limit) {
2365
- return [...entries].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
2366
- }
2367
- function loopFrom(backlog) {
2368
- if (backlog === undefined) {
2369
- return;
2370
- }
2371
- const snapshot = buildLoopSnapshot(backlog.path, backlog.items);
2372
- if (backlog.lastCommit !== undefined) {
2373
- snapshot.lastCommit = backlog.lastCommit;
2374
- }
2375
- return snapshot;
2376
- }
2377
- function buildDashboardState(inputs) {
2378
- const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
2379
- const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
2380
- const state = {
2381
- generatedAt: inputs.generatedAt,
2382
- session: {},
2383
- cost: summarizeCostRecords(inputs.costRecords),
2384
- recentRoutes: recentRoutes(inputs.costRecords, recentRoutesLimit),
2385
- team: [...inputs.team],
2386
- activity: recentActivity(inputs.activity, activityLimit)
2387
- };
2388
- if (inputs.session?.id !== undefined) {
2389
- state.session.id = inputs.session.id;
2390
- }
2391
- if (inputs.session?.startedAt !== undefined) {
2392
- state.session.startedAt = inputs.session.startedAt;
2393
- }
2394
- const loop = loopFrom(inputs.backlog);
2395
- if (loop !== undefined) {
2396
- state.loop = loop;
2397
- }
2398
- return state;
2399
- }
2400
-
2401
- // src/web/server.ts
2402
- var SSE_HEADERS = {
2403
- "content-type": "text/event-stream",
2404
- "cache-control": "no-cache",
2405
- connection: "keep-alive"
2406
- };
2407
- var JSON_HEADERS = { "content-type": "application/json" };
2408
- var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
2409
- function isAddressInUse(error) {
2410
- return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
2411
- }
2412
- async function renderState(deps) {
2413
- return renderDashboardHtml(buildDashboardState(await deps.readSnapshot()), {
2414
- refreshMs: deps.config.refreshMs
2415
- });
2416
- }
2417
- async function stateJson(deps) {
2418
- return JSON.stringify(buildDashboardState(await deps.readSnapshot()));
2419
- }
2420
- function sseMessage(json) {
2421
- return `event: state
2422
- data: ${json}
2423
-
2424
- `;
2425
- }
2426
- function tryListen(server, host, port) {
2427
- return new Promise((resolve, reject) => {
2428
- const onError = (error) => {
2429
- server.removeListener("listening", onListening);
2430
- if (isAddressInUse(error)) {
2431
- resolve(false);
2432
- } else {
2433
- reject(error);
2434
- }
2435
- };
2436
- const onListening = () => {
2437
- server.removeListener("error", onError);
2438
- resolve(true);
2439
- };
2440
- server.once("error", onError);
2441
- server.once("listening", onListening);
2442
- server.listen(port, host);
2443
- });
2444
- }
2445
- async function createDashboardServer(deps) {
2446
- const create = deps.createServer ?? createHttpServer;
2447
- const log = deps.log ?? ((message) => console.log(message));
2448
- const clients = new Set;
2449
- const handle = async (request, response) => {
2450
- if (request.method !== "GET") {
2451
- response.writeHead(405).end("method not allowed");
2452
- return;
2453
- }
2454
- const url2 = new URL(request.url ?? "/", "http://localhost");
2455
- const path = url2.pathname;
2456
- if (path === "/" || path === "/index.html") {
2457
- response.writeHead(200, HTML_HEADERS).end(await renderState(deps));
2458
- return;
2459
- }
2460
- if (path === "/api/state") {
2461
- response.writeHead(200, JSON_HEADERS).end(await stateJson(deps));
2462
- return;
2463
- }
2464
- if (path === "/healthz") {
2465
- response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ ok: true }));
2466
- return;
2467
- }
2468
- if (path === "/favicon.ico") {
2469
- response.writeHead(204).end();
2470
- return;
2471
- }
2472
- if (path === "/events") {
2473
- response.writeHead(200, SSE_HEADERS);
2474
- response.write(sseMessage(await stateJson(deps)));
2475
- clients.add(response);
2476
- request.on("close", () => {
2477
- clients.delete(response);
2478
- });
2479
- return;
2480
- }
2481
- response.writeHead(404).end("not found");
2482
- };
2483
- const server = create((request, response) => {
2484
- handle(request, response);
2485
- });
2486
- const maxAttempts = deps.config.autoPortFallback ? 20 : 1;
2487
- let bound = false;
2488
- let boundPort = deps.config.port;
2489
- for (let offset = 0;offset < maxAttempts; offset += 1) {
2490
- const port = deps.config.port + offset;
2491
- if (port > 65535) {
2492
- break;
2493
- }
2494
- if (await tryListen(server, deps.config.host, port)) {
2495
- boundPort = port;
2496
- bound = true;
2497
- break;
2498
- }
2499
- }
2500
- if (!bound) {
2501
- throw new Error("dashboard: no available port");
2502
- }
2503
- const url = `http://${deps.config.host}:${boundPort}`;
2504
- log(`[openteam] dashboard en ${url}`);
2505
- const notify = async () => {
2506
- if (clients.size === 0) {
2507
- return;
2508
- }
2509
- const message = sseMessage(await stateJson(deps));
2510
- for (const response of clients) {
2511
- response.write(message);
2512
- }
2513
- };
2514
- const close = () => {
2515
- for (const response of clients) {
2516
- response.end();
2517
- }
2518
- clients.clear();
2519
- server.close();
2520
- server.closeAllConnections?.();
2521
- };
2522
- return { url, port: boundPort, notify, close };
2523
- }
2524
-
2525
- // src/web/snapshot.ts
2526
- function createSnapshotReader(deps, paths, context) {
2527
- return async () => {
2528
- const [telemetryText, backlogText, agentFiles, lastCommit] = await Promise.all([
2529
- deps.readText(paths.telemetryPath),
2530
- deps.readText(paths.backlogPath),
2531
- deps.listAgentFiles(paths.agentDir),
2532
- deps.gitLastCommit?.() ?? Promise.resolve(undefined)
2533
- ]);
2534
- const costRecords = parseCostRecordsJsonl(telemetryText ?? "");
2535
- const team = agentFiles.map(parseAgentFile);
2536
- const inputs = {
2537
- generatedAt: deps.now(),
2538
- costRecords,
2539
- team,
2540
- activity: context.activity()
2541
- };
2542
- if (backlogText !== undefined) {
2543
- const backlog = {
2544
- path: paths.backlogPath,
2545
- 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
2546
2302
  };
2547
- if (lastCommit !== undefined) {
2548
- backlog.lastCommit = lastCommit;
2549
- }
2550
- inputs.backlog = backlog;
2551
- }
2552
- if (context.session !== undefined) {
2553
- inputs.session = context.session;
2554
- }
2555
- if (context.recentRoutesLimit !== undefined) {
2556
- inputs.recentRoutesLimit = context.recentRoutesLimit;
2557
- }
2558
- return inputs;
2559
- };
2560
- }
2561
-
2562
- // src/web/watch.ts
2563
- function watchSources(paths, onChange, deps, debounceMs = 250) {
2564
- const setTimer = deps.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
2565
- const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
2566
- const watchers = [];
2567
- let pending;
2568
- const trigger = () => {
2569
- if (pending !== undefined) {
2570
- clearTimer(pending);
2571
- }
2572
- pending = setTimer(() => {
2573
- pending = undefined;
2574
- onChange();
2575
- }, debounceMs);
2576
- };
2577
- for (const path of paths) {
2578
- try {
2579
- watchers.push(deps.watch(path, trigger));
2580
- } catch {}
2581
- }
2582
- return {
2583
- close() {
2584
- if (pending !== undefined) {
2585
- clearTimer(pending);
2586
- pending = undefined;
2587
- }
2588
- for (const watcher of watchers) {
2589
- watcher.close();
2590
- }
2591
- }
2592
- };
2593
- }
2594
-
2595
- // src/web/start.ts
2596
- function startDashboard(deps) {
2597
- return createDashboardRuntime(deps);
2598
- }
2599
- async function createDashboardRuntime(deps) {
2600
- const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
2601
- const activity = createActivityBuffer(deps.config.recentRoutes);
2602
- const readSnapshot = createSnapshotReader({
2603
- readText: deps.readText,
2604
- listAgentFiles: deps.listAgentFiles,
2605
- now: deps.now,
2606
- ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
2607
- }, {
2608
- telemetryPath: deps.telemetryPath,
2609
- backlogPath,
2610
- agentDir: deps.agentDir
2611
- }, {
2612
- activity: () => activity.list(),
2613
- recentRoutesLimit: deps.config.recentRoutes,
2614
- ...deps.session !== undefined ? { session: deps.session } : {}
2615
- });
2616
- const createServer = deps.serve ?? createDashboardServer;
2617
- const server = await createServer({
2618
- readSnapshot,
2619
- config: deps.config,
2620
- ...deps.log !== undefined ? { log: deps.log } : {}
2621
- });
2622
- const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
2623
- const watcher = watchSources([deps.telemetryPath, backlogPath, deps.agentDir], () => {
2624
- server.notify();
2625
- }, { watch: watchFn });
2626
- return {
2627
- url: server.url,
2628
- port: server.port,
2629
- pushActivity(entry) {
2630
- activity.push(entry);
2631
- server.notify();
2632
- },
2633
- close() {
2634
- watcher.close();
2635
- server.close();
2303
+ return deps.sink.emit(event);
2636
2304
  }
2637
2305
  };
2638
2306
  }
@@ -2660,9 +2328,11 @@ function telemetryOptions(rawOptions) {
2660
2328
  const telemetry = rawOptions !== null && typeof rawOptions === "object" && "telemetry" in rawOptions && rawOptions.telemetry !== null && typeof rawOptions.telemetry === "object" ? rawOptions.telemetry : {};
2661
2329
  const enabled = !("enabled" in telemetry) || telemetry.enabled !== false;
2662
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;
2663
2332
  return {
2664
2333
  enabled,
2665
- path: configuredPath
2334
+ path: configuredPath,
2335
+ sessionsDir: configuredSessionsDir
2666
2336
  };
2667
2337
  }
2668
2338
  function createFileAppender(filePath, deps = {}) {
@@ -2673,48 +2343,36 @@ function createFileAppender(filePath, deps = {}) {
2673
2343
  await appendFileFn(filePath, line, "utf8");
2674
2344
  };
2675
2345
  }
2676
- 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) {
2677
2355
  const options = telemetryOptions(rawOptions);
2678
2356
  if (!options.enabled) {
2679
- return createNullSink();
2357
+ return createNullEventSink();
2680
2358
  }
2681
- return createJsonlSink({
2682
- append: createFileAppender(options.path),
2359
+ return createEventLogSink({
2360
+ dir: options.sessionsDir,
2361
+ appendLine: createPathAppender(),
2362
+ join,
2683
2363
  onError: logTelemetryError
2684
2364
  });
2685
2365
  }
2686
- function isMissingFile2(error) {
2366
+ function isMissingFile(error) {
2687
2367
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2688
2368
  }
2689
- async function readTextOptional(path) {
2690
- try {
2691
- return await readFile2(path, "utf8");
2692
- } catch (error) {
2693
- if (isMissingFile2(error)) {
2694
- return;
2695
- }
2696
- throw error;
2697
- }
2698
- }
2699
- var DASHBOARD_EVENT_SUMMARIES = {
2700
- "session.created": "Sesión creada",
2701
- "session.idle": "Sesión en reposo",
2702
- "session.error": "Error de sesión"
2703
- };
2704
- function activityFromEvent(event, now) {
2705
- const summary = DASHBOARD_EVENT_SUMMARIES[event.type];
2706
- if (summary === undefined) {
2707
- return;
2708
- }
2709
- return { ts: now(), kind: "agent", summary };
2710
- }
2711
- function createCliDeps(config, registry, telemetryPath) {
2369
+ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SESSIONS_DIR) {
2712
2370
  return {
2713
2371
  loadConfig: async (path) => {
2714
2372
  try {
2715
- return loadOpenTeamConfig(JSON.parse(await readFile2(path, "utf8")));
2373
+ return loadOpenTeamConfig(JSON.parse(await readFile(path, "utf8")));
2716
2374
  } catch (error) {
2717
- if (isMissingFile2(error)) {
2375
+ if (isMissingFile(error)) {
2718
2376
  return config;
2719
2377
  }
2720
2378
  throw error;
@@ -2723,9 +2381,9 @@ function createCliDeps(config, registry, telemetryPath) {
2723
2381
  saveConfig: (nextConfig, path) => writeOpenTeamConfigFile(nextConfig, path),
2724
2382
  readOpencodeConfig: async (path) => {
2725
2383
  try {
2726
- return JSON.parse(await readFile2(path, "utf8"));
2384
+ return JSON.parse(await readFile(path, "utf8"));
2727
2385
  } catch (error) {
2728
- if (isMissingFile2(error)) {
2386
+ if (isMissingFile(error)) {
2729
2387
  return;
2730
2388
  }
2731
2389
  throw error;
@@ -2745,7 +2403,7 @@ function createCliDeps(config, registry, telemetryPath) {
2745
2403
  try {
2746
2404
  entries = await readdir(dir);
2747
2405
  } catch (error) {
2748
- if (isMissingFile2(error)) {
2406
+ if (isMissingFile(error)) {
2749
2407
  return [];
2750
2408
  }
2751
2409
  throw error;
@@ -2753,11 +2411,33 @@ function createCliDeps(config, registry, telemetryPath) {
2753
2411
  const mdFiles = entries.filter((entry) => entry.endsWith(".md"));
2754
2412
  return Promise.all(mdFiles.map(async (entry) => ({
2755
2413
  name: entry.replace(/\.md$/, ""),
2756
- contents: await readFile2(join(dir, entry), "utf8")
2414
+ contents: await readFile(join(dir, entry), "utf8")
2757
2415
  })));
2758
2416
  },
2759
2417
  probe: (nextConfig) => registry.probe(nextConfig.local.runtimes),
2760
- 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
+ }),
2761
2441
  configPath: DEFAULT_CONFIG_PATH,
2762
2442
  telemetryPath,
2763
2443
  opencodeConfigPath: OPENCODE_CONFIG_PATH,
@@ -2779,43 +2459,36 @@ var server = async (ctx, rawOptions) => {
2779
2459
  });
2780
2460
  cache.refresh();
2781
2461
  const telemetryPath = telemetryOptions(rawOptions).path;
2782
- const hooks = createHooks(config, cache.get, {
2783
- sink: createTelemetrySink(rawOptions)
2784
- });
2785
- const cliDeps = createCliDeps(config, registry, telemetryPath);
2786
- let dashboard;
2787
- if (config.dashboard.enabled) {
2788
- try {
2789
- dashboard = await startDashboard({
2790
- config: config.dashboard,
2791
- telemetryPath,
2792
- agentDir: dirname2(ORCHESTRATOR_AGENT_PATH),
2793
- backlogPath: DEFAULT_BACKLOG_PATH,
2794
- readText: readTextOptional,
2795
- listAgentFiles: cliDeps.listAgentFiles,
2796
- now: () => new Date().toISOString(),
2797
- gitLastCommit: createGitLastCommit(createShellExec(ctx.$)),
2798
- log: (message) => console.log(`[openteam] ${message}`)
2799
- });
2800
- console.log(`[openteam] dashboard en ${dashboard.url}`);
2801
- } catch (error) {
2802
- const message = error instanceof Error ? error.message : String(error);
2803
- console.warn(`[openteam] no se pudo iniciar el dashboard: ${message}`);
2804
- }
2805
- }
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();
2806
2468
  return {
2807
2469
  ...hooks,
2808
2470
  tool: {
2809
2471
  openteam: createCommandTool(cliDeps)
2810
2472
  },
2473
+ "tool.execute.before": async (input) => {
2474
+ toolcalls.before(input);
2475
+ },
2476
+ "tool.execute.after": async (input) => {
2477
+ await toolcalls.after(input);
2478
+ },
2811
2479
  event: async ({ event }) => {
2812
2480
  if (event.type === "session.created" || event.type === "session.idle") {
2813
2481
  cache.refresh();
2814
2482
  }
2815
- if (dashboard !== undefined) {
2816
- const entry = activityFromEvent(event, () => Date.now());
2817
- if (entry !== undefined) {
2818
- 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);
2819
2492
  }
2820
2493
  }
2821
2494
  }
@@ -2830,9 +2503,10 @@ export {
2830
2503
  server,
2831
2504
  logTelemetryError,
2832
2505
  logAvailabilityRefreshError,
2833
- isMissingFile2 as isMissingFile,
2506
+ isMissingFile,
2834
2507
  src_default as default,
2508
+ createPathAppender,
2835
2509
  createFileAppender,
2836
- createCliDeps,
2837
- activityFromEvent
2510
+ createEventSink,
2511
+ createCliDeps
2838
2512
  };