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