@jmanuelcorral/openteam 0.1.0 → 0.1.2

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.
@@ -0,0 +1,12 @@
1
+ import type { OpenTeamConfig } from "../config/schema";
2
+ import { type ModelRef } from "../config/schema";
3
+ import type { CommandOutcome } from "./types";
4
+ /**
5
+ * Parse a `provider/model` string into a validated {@link ModelRef}.
6
+ * Splits on the first `/` so model IDs may themselves contain `/` or `:`.
7
+ */
8
+ export declare function parseModelRef(input: string): ModelRef;
9
+ export declare function showBaseline(config: OpenTeamConfig): CommandOutcome;
10
+ export declare function setBaseline(config: OpenTeamConfig, ref: ModelRef): CommandOutcome;
11
+ export declare function autoBaseline(config: OpenTeamConfig): CommandOutcome;
12
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/commands/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,KAAK,QAAQ,EAAkB,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAiB9C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAcrD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAUnE;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,QAAQ,GACZ,cAAc,CAchB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAcnE"}
@@ -0,0 +1,17 @@
1
+ import type { OpenTeamConfig } from "../config/schema";
2
+ import type { LocalRuntimeSnapshot } from "../local/types";
3
+ import type { CostRecord } from "../telemetry/types";
4
+ export type CliDeps = {
5
+ loadConfig: (path: string) => Promise<OpenTeamConfig>;
6
+ saveConfig: (config: OpenTeamConfig, path: string) => Promise<void>;
7
+ probe: (config: OpenTeamConfig) => Promise<LocalRuntimeSnapshot[]>;
8
+ readTelemetry: (path: string) => Promise<CostRecord[]>;
9
+ configPath: string;
10
+ telemetryPath: string;
11
+ };
12
+ export type CliResult = {
13
+ exitCode: number;
14
+ stdout: string;
15
+ };
16
+ export declare function runCli(argv: readonly string[], deps: CliDeps): Promise<CliResult>;
17
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/commands/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAUrD,MAAM,MAAM,OAAO,GAAG;IACpB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACtD,UAAU,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,KAAK,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IACnE,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AA4FF,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,SAAS,CAAC,CAgDpB"}
@@ -0,0 +1,10 @@
1
+ import type { OpenTeamConfig } from "../config/schema";
2
+ import type { LocalRuntimeSnapshot } from "../local/types";
3
+ export type DoctorInput = {
4
+ config: OpenTeamConfig;
5
+ snapshots: readonly LocalRuntimeSnapshot[];
6
+ telemetryPath: string;
7
+ telemetryRecords: number;
8
+ };
9
+ export declare function renderDoctor(input: DoctorInput): string;
10
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAWF,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAkCvD"}
@@ -0,0 +1,22 @@
1
+ import type { ComplexityTier } from "../capabilities/types";
2
+ import type { CostRecord } from "../telemetry/types";
3
+ export type TierSummary = {
4
+ count: number;
5
+ estimatedUSD: number;
6
+ savingsUSD: number;
7
+ };
8
+ export type CostReport = {
9
+ count: number;
10
+ localCount: number;
11
+ frontierCount: number;
12
+ totalEstimatedUSD: number;
13
+ totalBaselineUSD: number;
14
+ totalSavingsUSD: number;
15
+ savingsPct: number;
16
+ tokensIn: number;
17
+ tokensOut: number;
18
+ byTier: Record<ComplexityTier, TierSummary>;
19
+ };
20
+ export declare function summarizeCostRecords(records: readonly CostRecord[]): CostReport;
21
+ export declare function renderReport(report: CostReport): string;
22
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/commands/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AASrD,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;CAC7C,CAAC;AAWF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,UAAU,CAuCZ;AAMD,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAuBvD"}
@@ -0,0 +1,6 @@
1
+ import type { OpenTeamConfig } from "../config/schema";
2
+ export type CommandOutcome = {
3
+ message: string;
4
+ config?: OpenTeamConfig;
5
+ };
6
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/commands/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAEhB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { type OpenTeamConfig } from "./schema";
3
+ export declare const DEFAULT_CONFIG_PATH = ".opencode/openteam.json";
4
+ /**
5
+ * Validate and serialize a config to canonical JSON (2-space indent, trailing newline).
6
+ * Pure: no filesystem access.
7
+ */
8
+ export declare function serializeOpenTeamConfig(config: OpenTeamConfig): string;
9
+ export type WriteConfigDeps = {
10
+ mkdir?: typeof mkdir;
11
+ writeFile?: typeof writeFile;
12
+ };
13
+ export declare function writeOpenTeamConfigFile(config: OpenTeamConfig, path?: string, deps?: WriteConfigDeps): Promise<void>;
14
+ //# sourceMappingURL=persist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/config/persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGpD,OAAO,EAAE,KAAK,cAAc,EAAwB,MAAM,UAAU,CAAC;AAErE,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAE7D;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAGtE;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;CAC9B,CAAC;AAEF,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,cAAc,EACtB,IAAI,SAAsB,EAC1B,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CAMf"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises";
2
2
  import type { Plugin, PluginModule } from "@opencode-ai/plugin";
3
+ import type { CliDeps } from "./commands/dispatch";
4
+ import type { OpenTeamConfig } from "./config/schema";
5
+ import { RuntimeRegistry } from "./local/registry";
3
6
  type FileAppenderDeps = {
4
7
  mkdir?: typeof mkdir;
5
8
  appendFile?: typeof appendFile;
@@ -7,6 +10,8 @@ type FileAppenderDeps = {
7
10
  export declare function logAvailabilityRefreshError(error: unknown): void;
8
11
  export declare function logTelemetryError(error: unknown): void;
9
12
  export declare function createFileAppender(filePath: string, deps?: FileAppenderDeps): (line: string) => Promise<void>;
13
+ export declare function isMissingFile(error: unknown): boolean;
14
+ export declare function createCliDeps(config: OpenTeamConfig, registry: RuntimeRegistry, telemetryPath: string): CliDeps;
10
15
  export declare const server: Plugin;
11
16
  declare const plugin: PluginModule;
12
17
  export default plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,KAAK,EAAE,MAAM,EAAe,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAoB7E,KAAK,gBAAgB,GAAG;IACtB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,UAAU,CAAC;CAChC,CAAC;AAcF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGhE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGtD;AA0BD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,gBAAqB,GAC1B,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAQjC;AAeD,eAAO,MAAM,MAAM,EAAE,MAyBpB,CAAC;AAEF,QAAA,MAAM,MAAM,EAAE,YAGb,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAY,MAAM,kBAAkB,CAAC;AAG/D,OAAO,KAAK,EAAE,MAAM,EAAe,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAgBnD,KAAK,gBAAgB,GAAG;IACtB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,UAAU,CAAC;CAChC,CAAC;AAcF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGhE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAGtD;AA0BD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,gBAAqB,GAC1B,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAQjC;AAeD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOrD;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,eAAe,EACzB,aAAa,EAAE,MAAM,GACpB,OAAO,CAkBT;AAED,eAAO,MAAM,MAAM,EAAE,MA8BpB,CAAC;AAEF,QAAA,MAAM,MAAM,EAAE,YAGb,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
- import { appendFile, mkdir } from "node:fs/promises";
3
- import { dirname } from "node:path";
2
+ import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "node:fs/promises";
3
+ import { dirname as dirname2 } from "node:path";
4
4
 
5
5
  // src/config/schema.ts
6
6
  import { z } from "zod";
@@ -84,6 +84,22 @@ function loadOpenTeamConfig(raw) {
84
84
  return OpenTeamConfigSchema.parse(raw ?? {});
85
85
  }
86
86
 
87
+ // src/config/persist.ts
88
+ import { mkdir, writeFile } from "node:fs/promises";
89
+ import { dirname } from "node:path";
90
+ var DEFAULT_CONFIG_PATH = ".opencode/openteam.json";
91
+ function serializeOpenTeamConfig(config) {
92
+ const validated = OpenTeamConfigSchema.parse(config);
93
+ return `${JSON.stringify(validated, null, 2)}
94
+ `;
95
+ }
96
+ async function writeOpenTeamConfigFile(config, path = DEFAULT_CONFIG_PATH, deps = {}) {
97
+ const mkdirFn = deps.mkdir ?? mkdir;
98
+ const writeFileFn = deps.writeFile ?? writeFile;
99
+ await mkdirFn(dirname(path), { recursive: true });
100
+ await writeFileFn(path, serializeOpenTeamConfig(config), "utf8");
101
+ }
102
+
87
103
  // src/local/openai-compatible.ts
88
104
  function normalizeBaseURL(baseURL) {
89
105
  return baseURL.replace(/\/+$/, "");
@@ -520,6 +536,323 @@ function createAvailabilityCache(options) {
520
536
  };
521
537
  }
522
538
 
539
+ // src/plugin/commandTool.ts
540
+ import { tool } from "@opencode-ai/plugin";
541
+
542
+ // src/commands/baseline.ts
543
+ function formatRef(ref) {
544
+ return ref === null ? "—" : `${ref.providerID}/${ref.modelID}`;
545
+ }
546
+ function effectiveBaseline(config) {
547
+ if (config.baseline.mode === "pinned" && config.baseline.pinnedModel !== null) {
548
+ return `${formatRef(config.baseline.pinnedModel)} (pinned)`;
549
+ }
550
+ return "cheapest-capable (auto)";
551
+ }
552
+ function parseModelRef(input) {
553
+ const trimmed = input.trim();
554
+ const slash = trimmed.indexOf("/");
555
+ if (slash <= 0 || slash === trimmed.length - 1) {
556
+ throw new Error(`Modelo inválido "${input}". Usa el formato provider/model, p.ej. anthropic/claude-sonnet-4-5.`);
557
+ }
558
+ return ModelRefSchema.parse({
559
+ providerID: trimmed.slice(0, slash),
560
+ modelID: trimmed.slice(slash + 1)
561
+ });
562
+ }
563
+ function showBaseline(config) {
564
+ const lines = [
565
+ "openteam baseline:",
566
+ ` modo: ${config.baseline.mode}`,
567
+ ` pinned: ${formatRef(config.baseline.pinnedModel)}`,
568
+ ` hardDefault: ${formatRef(config.baseline.hardDefault)}`,
569
+ ` efectivo: ${effectiveBaseline(config)}`
570
+ ];
571
+ return { message: lines.join(`
572
+ `) };
573
+ }
574
+ function setBaseline(config, ref) {
575
+ const next = {
576
+ ...config,
577
+ baseline: {
578
+ ...config.baseline,
579
+ mode: "pinned",
580
+ pinnedModel: ref
581
+ }
582
+ };
583
+ return {
584
+ config: next,
585
+ message: `Baseline fijado a ${formatRef(ref)} (modo pinned).`
586
+ };
587
+ }
588
+ function autoBaseline(config) {
589
+ const next = {
590
+ ...config,
591
+ baseline: {
592
+ ...config.baseline,
593
+ mode: "auto",
594
+ pinnedModel: null
595
+ }
596
+ };
597
+ return {
598
+ config: next,
599
+ message: "Baseline en modo auto (cheapest-capable)."
600
+ };
601
+ }
602
+
603
+ // src/commands/doctor.ts
604
+ function runtimeLine(snapshot) {
605
+ const mark = snapshot.reachable ? "✓" : "✗";
606
+ const detail = snapshot.reachable ? `${snapshot.models.length} modelo(s)` : snapshot.error ?? "inalcanzable";
607
+ return ` ${mark} ${snapshot.id.padEnd(14)} ${snapshot.baseURL || "(sin baseURL)"} — ${detail}`;
608
+ }
609
+ function renderDoctor(input) {
610
+ const enabledRuntimes = input.config.local.runtimes.filter((r) => r.enabled);
611
+ const reachable = input.snapshots.filter((s) => s.reachable).length;
612
+ const lines = [
613
+ "openteam doctor:",
614
+ ` baseline: ${input.config.baseline.mode}` + (input.config.baseline.mode === "pinned" && input.config.baseline.pinnedModel !== null ? ` (${input.config.baseline.pinnedModel.providerID}/${input.config.baseline.pinnedModel.modelID})` : ""),
615
+ ` privacidad: ${input.config.privacyMode}`,
616
+ ` runtimes locales (${reachable}/${enabledRuntimes.length} alcanzables):`
617
+ ];
618
+ if (input.snapshots.length === 0) {
619
+ lines.push(" (ningún runtime habilitado)");
620
+ } else {
621
+ for (const snapshot of input.snapshots) {
622
+ lines.push(runtimeLine(snapshot));
623
+ }
624
+ }
625
+ lines.push(` telemetría: ${input.telemetryPath} — ${input.telemetryRecords} registro(s)`);
626
+ if (reachable === 0) {
627
+ lines.push(" ⚠ Ningún runtime local alcanzable: todo el tráfico irá a frontier.");
628
+ }
629
+ return lines.join(`
630
+ `);
631
+ }
632
+
633
+ // src/commands/report.ts
634
+ var TIERS = [
635
+ "trivial",
636
+ "simple",
637
+ "moderate",
638
+ "hard"
639
+ ];
640
+ function emptyTierSummary() {
641
+ return {
642
+ trivial: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
643
+ simple: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
644
+ moderate: { count: 0, estimatedUSD: 0, savingsUSD: 0 },
645
+ hard: { count: 0, estimatedUSD: 0, savingsUSD: 0 }
646
+ };
647
+ }
648
+ function summarizeCostRecords(records) {
649
+ const report = {
650
+ count: 0,
651
+ localCount: 0,
652
+ frontierCount: 0,
653
+ totalEstimatedUSD: 0,
654
+ totalBaselineUSD: 0,
655
+ totalSavingsUSD: 0,
656
+ savingsPct: 0,
657
+ tokensIn: 0,
658
+ tokensOut: 0,
659
+ byTier: emptyTierSummary()
660
+ };
661
+ for (const record of records) {
662
+ report.count += 1;
663
+ if (record.routeKind === "local") {
664
+ report.localCount += 1;
665
+ } else {
666
+ report.frontierCount += 1;
667
+ }
668
+ report.totalEstimatedUSD += record.estimatedCostUSD;
669
+ report.totalBaselineUSD += record.baselineCostUSD;
670
+ report.totalSavingsUSD += record.estimatedSavingsUSD;
671
+ report.tokensIn += record.tokensIn ?? 0;
672
+ report.tokensOut += record.tokensOut ?? 0;
673
+ const tier = report.byTier[record.tier];
674
+ tier.count += 1;
675
+ tier.estimatedUSD += record.estimatedCostUSD;
676
+ tier.savingsUSD += record.estimatedSavingsUSD;
677
+ }
678
+ report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
679
+ return report;
680
+ }
681
+ function usd(value) {
682
+ return `$${value.toFixed(5)}`;
683
+ }
684
+ function renderReport(report) {
685
+ if (report.count === 0) {
686
+ return "openteam report: sin registros de telemetría todavía.";
687
+ }
688
+ const lines = [
689
+ "openteam report:",
690
+ ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
691
+ ` coste real: ${usd(report.totalEstimatedUSD)}`,
692
+ ` baseline: ${usd(report.totalBaselineUSD)}`,
693
+ ` ahorro: ${usd(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
694
+ ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
695
+ " por tier:"
696
+ ];
697
+ for (const tier of TIERS) {
698
+ const summary = report.byTier[tier];
699
+ lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd(summary.estimatedUSD)} · ahorro ${usd(summary.savingsUSD)}`);
700
+ }
701
+ return lines.join(`
702
+ `);
703
+ }
704
+
705
+ // src/commands/dispatch.ts
706
+ var HELP = [
707
+ "openteam — comandos runtime",
708
+ "",
709
+ "Uso:",
710
+ " openteam baseline show Muestra el baseline efectivo",
711
+ " openteam baseline set <p/model> Fija el baseline (modo pinned)",
712
+ " openteam baseline auto Baseline cheapest-capable (modo auto)",
713
+ " openteam doctor Diagnóstico de runtimes y config",
714
+ " openteam report Resumen de coste/ahorro (telemetría)",
715
+ "",
716
+ "Opciones:",
717
+ " --config <path> Ruta de .opencode/openteam.json",
718
+ " --telemetry <path> Ruta del JSONL de telemetría"
719
+ ].join(`
720
+ `);
721
+ function parseArgs(argv) {
722
+ const positionals = [];
723
+ const result = { positionals };
724
+ for (let i = 0;i < argv.length; i += 1) {
725
+ const arg = argv[i];
726
+ if (arg === "--config") {
727
+ const value = argv[i + 1];
728
+ if (value !== undefined) {
729
+ result.configPath = value;
730
+ }
731
+ i += 1;
732
+ } else if (arg === "--telemetry") {
733
+ const value = argv[i + 1];
734
+ if (value !== undefined) {
735
+ result.telemetryPath = value;
736
+ }
737
+ i += 1;
738
+ } else if (arg !== undefined) {
739
+ positionals.push(arg);
740
+ }
741
+ }
742
+ return result;
743
+ }
744
+ async function runBaseline(positionals, deps, configPath) {
745
+ const sub = positionals[1];
746
+ if (sub === "show" || sub === undefined) {
747
+ const config = await deps.loadConfig(configPath);
748
+ return { exitCode: 0, stdout: showBaseline(config).message };
749
+ }
750
+ if (sub === "auto") {
751
+ const config = await deps.loadConfig(configPath);
752
+ const outcome = autoBaseline(config);
753
+ if (outcome.config !== undefined) {
754
+ await deps.saveConfig(outcome.config, configPath);
755
+ }
756
+ return { exitCode: 0, stdout: outcome.message };
757
+ }
758
+ if (sub === "set") {
759
+ const target = positionals[2];
760
+ if (target === undefined) {
761
+ return {
762
+ exitCode: 1,
763
+ stdout: "Falta el modelo. Uso: openteam baseline set <provider/model>"
764
+ };
765
+ }
766
+ const config = await deps.loadConfig(configPath);
767
+ const outcome = setBaseline(config, parseModelRef(target));
768
+ if (outcome.config !== undefined) {
769
+ await deps.saveConfig(outcome.config, configPath);
770
+ }
771
+ return { exitCode: 0, stdout: outcome.message };
772
+ }
773
+ return {
774
+ exitCode: 1,
775
+ stdout: `Subcomando de baseline desconocido: ${sub}
776
+
777
+ ${HELP}`
778
+ };
779
+ }
780
+ async function runCli(argv, deps) {
781
+ const parsed = parseArgs(argv);
782
+ const command = parsed.positionals[0];
783
+ const configPath = parsed.configPath ?? deps.configPath;
784
+ const telemetryPath = parsed.telemetryPath ?? deps.telemetryPath;
785
+ try {
786
+ if (command === "baseline") {
787
+ return await runBaseline(parsed.positionals, deps, configPath);
788
+ }
789
+ if (command === "doctor") {
790
+ const config = await deps.loadConfig(configPath);
791
+ const [snapshots, records] = await Promise.all([
792
+ deps.probe(config),
793
+ deps.readTelemetry(telemetryPath)
794
+ ]);
795
+ return {
796
+ exitCode: 0,
797
+ stdout: renderDoctor({
798
+ config,
799
+ snapshots,
800
+ telemetryPath,
801
+ telemetryRecords: records.length
802
+ })
803
+ };
804
+ }
805
+ if (command === "report") {
806
+ const records = await deps.readTelemetry(telemetryPath);
807
+ return {
808
+ exitCode: 0,
809
+ stdout: renderReport(summarizeCostRecords(records))
810
+ };
811
+ }
812
+ if (command === undefined || command === "help" || command === "--help") {
813
+ return { exitCode: 0, stdout: HELP };
814
+ }
815
+ return {
816
+ exitCode: 1,
817
+ stdout: `Comando desconocido: ${command}
818
+
819
+ ${HELP}`
820
+ };
821
+ } catch (error) {
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ return { exitCode: 1, stdout: `Error: ${message}` };
824
+ }
825
+ }
826
+
827
+ // src/plugin/commandTool.ts
828
+ function commandArgv(action, model) {
829
+ switch (action) {
830
+ case "show":
831
+ return ["baseline", "show"];
832
+ case "auto":
833
+ return ["baseline", "auto"];
834
+ case "set":
835
+ return model === undefined ? ["baseline", "set"] : ["baseline", "set", model];
836
+ case "doctor":
837
+ return ["doctor"];
838
+ case "report":
839
+ return ["report"];
840
+ }
841
+ }
842
+ function createCommandTool(deps) {
843
+ return tool({
844
+ description: "Comandos runtime de openteam: baseline (show/set/auto), doctor y report.",
845
+ args: {
846
+ action: tool.schema.enum(["show", "set", "auto", "doctor", "report"]).describe("Acción a ejecutar"),
847
+ model: tool.schema.string().optional().describe("Modelo provider/model (solo para action=set)")
848
+ },
849
+ async execute(args) {
850
+ const result = await runCli(commandArgv(args.action, args.model), deps);
851
+ return result.stdout;
852
+ }
853
+ });
854
+ }
855
+
523
856
  // src/capabilities/budget.ts
524
857
  var WARNING_UTILIZATION = 0.8;
525
858
  function projectedSpend(current, estimatedCostUSD) {
@@ -1113,8 +1446,47 @@ function createHooks(config, getAvailable, options = {}) {
1113
1446
  };
1114
1447
  }
1115
1448
 
1116
- // src/index.ts
1449
+ // src/telemetry/read.ts
1450
+ import { readFile } from "node:fs/promises";
1117
1451
  var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
1452
+ function parseCostRecordsJsonl(text) {
1453
+ const records = [];
1454
+ for (const line of text.split(`
1455
+ `)) {
1456
+ const trimmed = line.trim();
1457
+ if (trimmed.length === 0) {
1458
+ continue;
1459
+ }
1460
+ let candidate;
1461
+ try {
1462
+ candidate = JSON.parse(trimmed);
1463
+ } catch {
1464
+ continue;
1465
+ }
1466
+ const parsed = CostRecordSchema.safeParse(candidate);
1467
+ if (parsed.success) {
1468
+ records.push(parsed.data);
1469
+ }
1470
+ }
1471
+ return records;
1472
+ }
1473
+ function isMissingFile(error) {
1474
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1475
+ }
1476
+ async function readCostRecords(path, deps = {}) {
1477
+ const readFileFn = deps.readFile ?? readFile;
1478
+ try {
1479
+ const content = await readFileFn(path, "utf8");
1480
+ return parseCostRecordsJsonl(content);
1481
+ } catch (error) {
1482
+ if (isMissingFile(error)) {
1483
+ return [];
1484
+ }
1485
+ throw error;
1486
+ }
1487
+ }
1488
+
1489
+ // src/index.ts
1118
1490
  function createShellExec($) {
1119
1491
  return async (command, args) => {
1120
1492
  const output = await $`${command} ${[...args]}`.nothrow().quiet();
@@ -1143,10 +1515,10 @@ function telemetryOptions(rawOptions) {
1143
1515
  };
1144
1516
  }
1145
1517
  function createFileAppender(filePath, deps = {}) {
1146
- const mkdirFn = deps.mkdir ?? mkdir;
1518
+ const mkdirFn = deps.mkdir ?? mkdir2;
1147
1519
  const appendFileFn = deps.appendFile ?? appendFile;
1148
1520
  return async (line) => {
1149
- await mkdirFn(dirname(filePath), { recursive: true });
1521
+ await mkdirFn(dirname2(filePath), { recursive: true });
1150
1522
  await appendFileFn(filePath, line, "utf8");
1151
1523
  };
1152
1524
  }
@@ -1160,6 +1532,28 @@ function createTelemetrySink(rawOptions) {
1160
1532
  onError: logTelemetryError
1161
1533
  });
1162
1534
  }
1535
+ function isMissingFile2(error) {
1536
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1537
+ }
1538
+ function createCliDeps(config, registry, telemetryPath) {
1539
+ return {
1540
+ loadConfig: async (path) => {
1541
+ try {
1542
+ return loadOpenTeamConfig(JSON.parse(await readFile2(path, "utf8")));
1543
+ } catch (error) {
1544
+ if (isMissingFile2(error)) {
1545
+ return config;
1546
+ }
1547
+ throw error;
1548
+ }
1549
+ },
1550
+ saveConfig: (nextConfig, path) => writeOpenTeamConfigFile(nextConfig, path),
1551
+ probe: (nextConfig) => registry.probe(nextConfig.local.runtimes),
1552
+ readTelemetry: (path) => readCostRecords(path),
1553
+ configPath: DEFAULT_CONFIG_PATH,
1554
+ telemetryPath
1555
+ };
1556
+ }
1163
1557
  var server = async (ctx, rawOptions) => {
1164
1558
  const config = loadOpenTeamConfig(rawOptions ?? {});
1165
1559
  const registry = new RuntimeRegistry({
@@ -1173,11 +1567,16 @@ var server = async (ctx, rawOptions) => {
1173
1567
  onRefreshError: logAvailabilityRefreshError
1174
1568
  });
1175
1569
  cache.refresh();
1570
+ const telemetryPath = telemetryOptions(rawOptions).path;
1176
1571
  const hooks = createHooks(config, cache.get, {
1177
1572
  sink: createTelemetrySink(rawOptions)
1178
1573
  });
1574
+ const cliDeps = createCliDeps(config, registry, telemetryPath);
1179
1575
  return {
1180
1576
  ...hooks,
1577
+ tool: {
1578
+ openteam: createCommandTool(cliDeps)
1579
+ },
1181
1580
  event: async ({ event }) => {
1182
1581
  if (event.type === "session.created" || event.type === "session.idle") {
1183
1582
  cache.refresh();
@@ -1194,6 +1593,8 @@ export {
1194
1593
  server,
1195
1594
  logTelemetryError,
1196
1595
  logAvailabilityRefreshError,
1596
+ isMissingFile2 as isMissingFile,
1197
1597
  src_default as default,
1198
- createFileAppender
1598
+ createFileAppender,
1599
+ createCliDeps
1199
1600
  };
@@ -0,0 +1,10 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import { type CliDeps } from "../commands/dispatch";
3
+ export type CommandAction = "show" | "set" | "auto" | "doctor" | "report";
4
+ /**
5
+ * Map a tool action (+ optional model) to the argv understood by {@link runCli}.
6
+ * Pure so it can be unit-tested without the opencode runtime.
7
+ */
8
+ export declare function commandArgv(action: CommandAction, model?: string): string[];
9
+ export declare function createCommandTool(deps: CliDeps): ToolDefinition;
10
+ //# sourceMappingURL=commandTool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commandTool.d.ts","sourceRoot":"","sources":["../../src/plugin/commandTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAEhE,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1E;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAe3E;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAkB/D"}
@@ -0,0 +1,14 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { type CostRecord } from "./types";
3
+ export declare const DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
4
+ /**
5
+ * Parse newline-delimited JSON cost records.
6
+ * Pure: silently skips blank lines and lines that fail schema validation,
7
+ * so a partially corrupted telemetry file never throws.
8
+ */
9
+ export declare function parseCostRecordsJsonl(text: string): CostRecord[];
10
+ export type ReadCostRecordsDeps = {
11
+ readFile?: typeof readFile;
12
+ };
13
+ export declare function readCostRecords(path: string, deps?: ReadCostRecordsDeps): Promise<CostRecord[]>;
14
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/telemetry/read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,SAAS,CAAC;AAE5D,eAAO,MAAM,sBAAsB,uCAAuC,CAAC;AAE3E;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAuBhE;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,OAAO,QAAQ,CAAC;CAC5B,CAAC;AAWF,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,UAAU,EAAE,CAAC,CAYvB"}