@jmanuelcorral/openteam 0.1.21 → 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.
- package/README.md +29 -31
- package/dist/cli/dashboardServe.d.ts +15 -8
- package/dist/cli/dashboardServe.d.ts.map +1 -1
- package/dist/cli.js +889 -276
- package/dist/commands/dashboard.d.ts +4 -4
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/orchestratorAgent.d.ts.map +1 -1
- package/dist/contract/opencode.d.ts +1 -1
- package/dist/contract/opencode.d.ts.map +1 -1
- package/dist/dashboard/render.d.ts.map +1 -1
- package/dist/dashboard/state.d.ts +14 -6
- package/dist/dashboard/state.d.ts.map +1 -1
- package/dist/dashboard/types.d.ts +14 -3
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +476 -802
- package/dist/orchestrator/coordinator.d.ts +14 -2
- package/dist/orchestrator/coordinator.d.ts.map +1 -1
- package/dist/plugin/capture.d.ts +46 -0
- package/dist/plugin/capture.d.ts.map +1 -0
- package/dist/plugin/hooks.d.ts +2 -2
- package/dist/plugin/hooks.d.ts.map +1 -1
- package/dist/plugin/toolcalls.d.ts +21 -0
- package/dist/plugin/toolcalls.d.ts.map +1 -0
- package/dist/telemetry/aggregate.d.ts +89 -0
- package/dist/telemetry/aggregate.d.ts.map +1 -0
- package/dist/telemetry/decisions.d.ts +25 -0
- package/dist/telemetry/decisions.d.ts.map +1 -0
- package/dist/telemetry/eventLog.d.ts +63 -0
- package/dist/telemetry/eventLog.d.ts.map +1 -0
- package/dist/telemetry/events.d.ts +217 -0
- package/dist/telemetry/events.d.ts.map +1 -0
- package/dist/web/snapshot.d.ts +13 -7
- package/dist/web/snapshot.d.ts.map +1 -1
- package/dist/web/start.d.ts +10 -7
- package/dist/web/start.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/web/activity.d.ts +0 -12
- 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
|
|
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:
|
|
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
|
-
"
|
|
994
|
-
"
|
|
995
|
-
"
|
|
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
|
|
1150
|
-
" openteam dashboard --
|
|
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
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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:
|
|
2020
|
+
routeKind: z4.enum(["local", "frontier"]),
|
|
1880
2021
|
selected: ModelRefSchema,
|
|
1881
|
-
rationale:
|
|
1882
|
-
estimatedCostUSD:
|
|
1883
|
-
baselineCostUSD:
|
|
1884
|
-
estimatedSavingsUSD:
|
|
1885
|
-
budgetAction:
|
|
1886
|
-
tokensIn:
|
|
1887
|
-
tokensOut:
|
|
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/
|
|
1891
|
-
|
|
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
|
-
|
|
2068
|
+
emit: async (event) => {
|
|
1894
2069
|
try {
|
|
1895
|
-
const parsed =
|
|
1896
|
-
|
|
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
|
|
1905
|
-
return {
|
|
1906
|
-
|
|
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,
|
|
1941
|
-
Promise.resolve().then(() => sink.
|
|
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 ??
|
|
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/
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
|
2018
|
-
|
|
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
|
-
|
|
2063
|
-
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
"moderate",
|
|
2084
|
-
"hard"
|
|
2085
|
-
];
|
|
2086
|
-
function escapeHtml(value) {
|
|
2087
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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
|
|
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
|
|
2357
|
+
return createNullEventSink();
|
|
2680
2358
|
}
|
|
2681
|
-
return
|
|
2682
|
-
|
|
2359
|
+
return createEventLogSink({
|
|
2360
|
+
dir: options.sessionsDir,
|
|
2361
|
+
appendLine: createPathAppender(),
|
|
2362
|
+
join,
|
|
2683
2363
|
onError: logTelemetryError
|
|
2684
2364
|
});
|
|
2685
2365
|
}
|
|
2686
|
-
function
|
|
2366
|
+
function isMissingFile(error) {
|
|
2687
2367
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2688
2368
|
}
|
|
2689
|
-
|
|
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
|
|
2373
|
+
return loadOpenTeamConfig(JSON.parse(await readFile(path, "utf8")));
|
|
2716
2374
|
} catch (error) {
|
|
2717
|
-
if (
|
|
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
|
|
2384
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
2727
2385
|
} catch (error) {
|
|
2728
|
-
if (
|
|
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 (
|
|
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
|
|
2414
|
+
contents: await readFile(join(dir, entry), "utf8")
|
|
2757
2415
|
})));
|
|
2758
2416
|
},
|
|
2759
2417
|
probe: (nextConfig) => registry.probe(nextConfig.local.runtimes),
|
|
2760
|
-
readTelemetry: (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
|
|
2783
|
-
|
|
2784
|
-
});
|
|
2785
|
-
const cliDeps = createCliDeps(config, registry, telemetryPath);
|
|
2786
|
-
|
|
2787
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
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
|
-
|
|
2506
|
+
isMissingFile,
|
|
2834
2507
|
src_default as default,
|
|
2508
|
+
createPathAppender,
|
|
2835
2509
|
createFileAppender,
|
|
2836
|
-
|
|
2837
|
-
|
|
2510
|
+
createEventSink,
|
|
2511
|
+
createCliDeps
|
|
2838
2512
|
};
|