@uniqbit/mate-core 0.15.1 → 0.15.2-canary.0
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/package.json +1 -1
- package/src/cli/commands/report/adapter.ts +73 -0
- package/src/cli/commands/report/contract.ts +257 -0
- package/src/cli/commands/report/delivery.ts +79 -0
- package/src/cli/commands/report/index.ts +171 -110
- package/src/cli/commands/report/renderer.ts +228 -73
- package/src/cli/commands/report/types.ts +77 -0
- package/src/cli/main.ts +7 -6
- package/src/cli/plugin-commands.ts +27 -2
- package/src/cli/usage.ts +3 -1
- package/src/tools/setup/capabilities/openspec.ts +7 -50
- package/src/tools/setup/mate.ts +124 -0
- package/src/tools/setup/plugins/gitignore.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { formatCost, formatTokens } from "./formatter";
|
|
2
|
+
import type {
|
|
3
|
+
ReportData,
|
|
4
|
+
ReportDocument,
|
|
5
|
+
ReportKeyValue,
|
|
6
|
+
ReportMetric,
|
|
7
|
+
ReportSection,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { REPORT_DOCUMENT_VERSION } from "./types";
|
|
10
|
+
|
|
11
|
+
export function reportDataToDocument(data: ReportData): ReportDocument {
|
|
12
|
+
const metadata: ReportKeyValue[] = [
|
|
13
|
+
{ label: "Window", value: `Last ${data.days} days` },
|
|
14
|
+
{ label: "Generated", value: data.generatedAt },
|
|
15
|
+
{ label: "Working repository", value: data.workingRepoPath },
|
|
16
|
+
{ label: "Companion repository", value: data.companionRepoPath },
|
|
17
|
+
{ label: "Active agents", value: data.activeAgents.join(", ") || "None" },
|
|
18
|
+
{ label: "Capabilities", value: data.enabledCapabilities.join(", ") || "None" },
|
|
19
|
+
];
|
|
20
|
+
const summary: ReportMetric[] = [
|
|
21
|
+
{ label: "Total spending", value: formatCost(data.totalSpending) },
|
|
22
|
+
{ label: "Total savings", value: formatCost(data.totalSavings) },
|
|
23
|
+
{ label: "Net spend", value: formatCost(data.netSpend) },
|
|
24
|
+
];
|
|
25
|
+
const sections: ReportSection[] = [
|
|
26
|
+
{
|
|
27
|
+
id: "spending",
|
|
28
|
+
title: "Spending",
|
|
29
|
+
type: "table",
|
|
30
|
+
columns: ["Model", "Input", "Output", "Cache Read", "Cache Write", "Cost (est.)"],
|
|
31
|
+
rows: data.spending.map((entry) => [
|
|
32
|
+
entry.model,
|
|
33
|
+
formatTokens(entry.inputTokens),
|
|
34
|
+
formatTokens(entry.outputTokens),
|
|
35
|
+
formatTokens(entry.cacheReadTokens),
|
|
36
|
+
formatTokens(entry.cacheWriteTokens),
|
|
37
|
+
formatCost(entry.cost),
|
|
38
|
+
]),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "savings",
|
|
42
|
+
title: "Savings",
|
|
43
|
+
type: "table",
|
|
44
|
+
columns: ["Tool", "Tokens Saved", "Calls", "Cost Saved", "Efficiency"],
|
|
45
|
+
rows: data.savings.map((entry) => [
|
|
46
|
+
entry.tool,
|
|
47
|
+
entry.tokensSaved === 0 ? "N/A" : formatTokens(entry.tokensSaved),
|
|
48
|
+
String(entry.calls),
|
|
49
|
+
entry.costSaved === 0 ? "N/A" : formatCost(entry.costSaved),
|
|
50
|
+
entry.efficiency,
|
|
51
|
+
]),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "tool-status",
|
|
55
|
+
title: "Tool Status",
|
|
56
|
+
type: "statuses",
|
|
57
|
+
items: data.toolStatus.map((entry) => ({
|
|
58
|
+
label: entry.name,
|
|
59
|
+
status: entry.status,
|
|
60
|
+
})),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
version: REPORT_DOCUMENT_VERSION,
|
|
66
|
+
title: "Token Usage Report",
|
|
67
|
+
generatedAt: data.generatedAt,
|
|
68
|
+
period: `Last ${data.days} days`,
|
|
69
|
+
metadata,
|
|
70
|
+
summary,
|
|
71
|
+
sections,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REPORT_DOCUMENT_VERSION,
|
|
3
|
+
type ReportDocument,
|
|
4
|
+
type ReportKeyValue,
|
|
5
|
+
type ReportMetric,
|
|
6
|
+
type ReportSection,
|
|
7
|
+
type ReportStatus,
|
|
8
|
+
type ReportValue,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export interface ReportValidationIssue {
|
|
12
|
+
path: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ReportValidationError extends Error {
|
|
17
|
+
readonly issues: ReportValidationIssue[];
|
|
18
|
+
|
|
19
|
+
constructor(issues: ReportValidationIssue[]) {
|
|
20
|
+
super(issues.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
|
|
21
|
+
this.name = "ReportValidationError";
|
|
22
|
+
this.issues = issues;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
27
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
const isReportValue = (value: unknown): value is ReportValue =>
|
|
30
|
+
value === null ||
|
|
31
|
+
typeof value === "string" ||
|
|
32
|
+
typeof value === "number" ||
|
|
33
|
+
typeof value === "boolean";
|
|
34
|
+
|
|
35
|
+
function requiredString(
|
|
36
|
+
value: unknown,
|
|
37
|
+
path: string,
|
|
38
|
+
issues: ReportValidationIssue[],
|
|
39
|
+
): value is string {
|
|
40
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
41
|
+
issues.push({ path, message: "must be a non-empty string" });
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateKeyValues(value: unknown, path: string, issues: ReportValidationIssue[]): boolean {
|
|
48
|
+
if (!Array.isArray(value)) {
|
|
49
|
+
issues.push({ path, message: "must be an array" });
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
value.forEach((item, index) => {
|
|
54
|
+
const itemPath = `${path}[${index}]`;
|
|
55
|
+
if (!isRecord(item)) {
|
|
56
|
+
issues.push({ path: itemPath, message: "must be an object" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
requiredString(item.label, `${itemPath}.label`, issues);
|
|
60
|
+
if (!isReportValue(item.value)) {
|
|
61
|
+
issues.push({
|
|
62
|
+
path: `${itemPath}.value`,
|
|
63
|
+
message: "must be a string, number, boolean, or null",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateMetrics(value: unknown, path: string, issues: ReportValidationIssue[]): boolean {
|
|
71
|
+
if (!Array.isArray(value)) {
|
|
72
|
+
issues.push({ path, message: "must be an array" });
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
value.forEach((item, index) => {
|
|
77
|
+
const itemPath = `${path}[${index}]`;
|
|
78
|
+
if (!isRecord(item)) {
|
|
79
|
+
issues.push({ path: itemPath, message: "must be an object" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
requiredString(item.label, `${itemPath}.label`, issues);
|
|
83
|
+
if (!isReportValue(item.value)) {
|
|
84
|
+
issues.push({
|
|
85
|
+
path: `${itemPath}.value`,
|
|
86
|
+
message: "must be a string, number, boolean, or null",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (item.detail !== undefined && typeof item.detail !== "string") {
|
|
90
|
+
issues.push({ path: `${itemPath}.detail`, message: "must be a string" });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateStatuses(value: unknown, path: string, issues: ReportValidationIssue[]): boolean {
|
|
97
|
+
if (!Array.isArray(value)) {
|
|
98
|
+
issues.push({ path, message: "must be an array" });
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
value.forEach((item, index) => {
|
|
103
|
+
const itemPath = `${path}[${index}]`;
|
|
104
|
+
if (!isRecord(item)) {
|
|
105
|
+
issues.push({ path: itemPath, message: "must be an object" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
requiredString(item.label, `${itemPath}.label`, issues);
|
|
109
|
+
requiredString(item.status, `${itemPath}.status`, issues);
|
|
110
|
+
if (item.detail !== undefined && typeof item.detail !== "string") {
|
|
111
|
+
issues.push({ path: `${itemPath}.detail`, message: "must be a string" });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateSection(value: unknown, path: string, issues: ReportValidationIssue[]): void {
|
|
118
|
+
if (!isRecord(value)) {
|
|
119
|
+
issues.push({ path, message: "must be an object" });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
requiredString(value.id, `${path}.id`, issues);
|
|
124
|
+
requiredString(value.title, `${path}.title`, issues);
|
|
125
|
+
if (typeof value.type !== "string") {
|
|
126
|
+
issues.push({ path: `${path}.type`, message: "must be a supported section type" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (value.type) {
|
|
131
|
+
case "metadata":
|
|
132
|
+
case "key-value":
|
|
133
|
+
validateKeyValues(value.items, `${path}.items`, issues);
|
|
134
|
+
return;
|
|
135
|
+
case "metrics":
|
|
136
|
+
validateMetrics(value.items, `${path}.items`, issues);
|
|
137
|
+
return;
|
|
138
|
+
case "statuses":
|
|
139
|
+
validateStatuses(value.items, `${path}.items`, issues);
|
|
140
|
+
return;
|
|
141
|
+
case "table": {
|
|
142
|
+
if (!Array.isArray(value.columns) || value.columns.length === 0) {
|
|
143
|
+
issues.push({ path: `${path}.columns`, message: "must be a non-empty string array" });
|
|
144
|
+
} else {
|
|
145
|
+
value.columns.forEach((column, index) => {
|
|
146
|
+
if (typeof column !== "string" || column.trim() === "") {
|
|
147
|
+
issues.push({
|
|
148
|
+
path: `${path}.columns[${index}]`,
|
|
149
|
+
message: "must be a non-empty string",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(value.rows)) {
|
|
155
|
+
issues.push({ path: `${path}.rows`, message: "must be an array" });
|
|
156
|
+
} else {
|
|
157
|
+
value.rows.forEach((row, rowIndex) => {
|
|
158
|
+
if (!Array.isArray(row)) {
|
|
159
|
+
issues.push({ path: `${path}.rows[${rowIndex}]`, message: "must be an array" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(value.columns) && row.length !== value.columns.length) {
|
|
163
|
+
issues.push({
|
|
164
|
+
path: `${path}.rows[${rowIndex}]`,
|
|
165
|
+
message: `must contain ${value.columns.length} values`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
row.forEach((cell, cellIndex) => {
|
|
169
|
+
if (!isReportValue(cell)) {
|
|
170
|
+
issues.push({
|
|
171
|
+
path: `${path}.rows[${rowIndex}][${cellIndex}]`,
|
|
172
|
+
message: "must be a string, number, boolean, or null",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
case "text":
|
|
181
|
+
if (typeof value.content !== "string") {
|
|
182
|
+
issues.push({ path: `${path}.content`, message: "must be a string" });
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
default:
|
|
186
|
+
issues.push({
|
|
187
|
+
path: `${path}.type`,
|
|
188
|
+
message: "must be one of metadata, metrics, key-value, table, statuses, or text",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getReportDocumentIssues(input: unknown): ReportValidationIssue[] {
|
|
194
|
+
const issues: ReportValidationIssue[] = [];
|
|
195
|
+
if (!isRecord(input)) {
|
|
196
|
+
return [{ path: "$", message: "must be an object" }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (input.version !== REPORT_DOCUMENT_VERSION) {
|
|
200
|
+
issues.push({ path: "version", message: `must be ${REPORT_DOCUMENT_VERSION}` });
|
|
201
|
+
}
|
|
202
|
+
requiredString(input.title, "title", issues);
|
|
203
|
+
requiredString(input.generatedAt, "generatedAt", issues);
|
|
204
|
+
if (input.period !== undefined && typeof input.period !== "string") {
|
|
205
|
+
issues.push({ path: "period", message: "must be a string" });
|
|
206
|
+
}
|
|
207
|
+
if (input.context !== undefined && typeof input.context !== "string") {
|
|
208
|
+
issues.push({ path: "context", message: "must be a string" });
|
|
209
|
+
}
|
|
210
|
+
validateKeyValues(input.metadata, "metadata", issues);
|
|
211
|
+
validateMetrics(input.summary, "summary", issues);
|
|
212
|
+
if (!Array.isArray(input.sections)) {
|
|
213
|
+
issues.push({ path: "sections", message: "must be an array" });
|
|
214
|
+
} else {
|
|
215
|
+
const ids = new Set<string>();
|
|
216
|
+
input.sections.forEach((section, index) => {
|
|
217
|
+
validateSection(section, `sections[${index}]`, issues);
|
|
218
|
+
if (isRecord(section) && typeof section.id === "string") {
|
|
219
|
+
if (ids.has(section.id)) {
|
|
220
|
+
issues.push({ path: `sections[${index}].id`, message: "must be unique" });
|
|
221
|
+
}
|
|
222
|
+
ids.add(section.id);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return issues;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function validateReportDocument(input: unknown): ReportDocument {
|
|
230
|
+
const issues = getReportDocumentIssues(input);
|
|
231
|
+
if (issues.length > 0) throw new ReportValidationError(issues);
|
|
232
|
+
return input as ReportDocument;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function parseReportDocument(json: string): ReportDocument {
|
|
236
|
+
let input: unknown;
|
|
237
|
+
try {
|
|
238
|
+
input = JSON.parse(json);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw new ReportValidationError([
|
|
241
|
+
{
|
|
242
|
+
path: "$",
|
|
243
|
+
message: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
return validateReportDocument(input);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export type {
|
|
251
|
+
ReportDocument,
|
|
252
|
+
ReportKeyValue,
|
|
253
|
+
ReportMetric,
|
|
254
|
+
ReportSection,
|
|
255
|
+
ReportStatus,
|
|
256
|
+
ReportValue,
|
|
257
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface TemporaryReportDeps {
|
|
7
|
+
makeTempDir?: () => Promise<string>;
|
|
8
|
+
writeFile?: (filePath: string, content: string) => Promise<void>;
|
|
9
|
+
tempDir?: () => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function writeTemporaryReport(
|
|
13
|
+
html: string,
|
|
14
|
+
deps: TemporaryReportDeps = {},
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const makeTempDir =
|
|
17
|
+
deps.makeTempDir ?? (() => mkdtemp(path.join((deps.tempDir ?? os.tmpdir)(), "mate-report-")));
|
|
18
|
+
const reportDirectory = await makeTempDir();
|
|
19
|
+
const reportPath = path.join(reportDirectory, "report.html");
|
|
20
|
+
const write = deps.writeFile ?? ((filePath, content) => writeFile(filePath, content, "utf8"));
|
|
21
|
+
await write(reportPath, html);
|
|
22
|
+
return reportPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BrowserLauncherDeps {
|
|
26
|
+
platform?: () => NodeJS.Platform;
|
|
27
|
+
spawn?: typeof spawn;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function browserCommand(
|
|
31
|
+
platform: NodeJS.Platform,
|
|
32
|
+
reportPath: string,
|
|
33
|
+
): {
|
|
34
|
+
command: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
} {
|
|
37
|
+
if (platform === "darwin") {
|
|
38
|
+
return { command: "open", args: [reportPath] };
|
|
39
|
+
}
|
|
40
|
+
if (platform === "win32") {
|
|
41
|
+
return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", reportPath] };
|
|
42
|
+
}
|
|
43
|
+
return { command: "xdg-open", args: [reportPath] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function openReportInBrowser(
|
|
47
|
+
reportPath: string,
|
|
48
|
+
deps: BrowserLauncherDeps = {},
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const launch = deps.spawn ?? spawn;
|
|
51
|
+
const { command, args } = browserCommand(
|
|
52
|
+
(deps.platform ?? (() => process.platform))(),
|
|
53
|
+
reportPath,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let settled = false;
|
|
58
|
+
const settle = (error?: Error) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
if (error) reject(error);
|
|
62
|
+
else resolve();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let child: ChildProcess;
|
|
66
|
+
try {
|
|
67
|
+
child = launch(command, args, { detached: true, stdio: "ignore" });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
settle(error instanceof Error ? error : new Error(String(error)));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
child.once("error", (error) => settle(error));
|
|
74
|
+
child.once("spawn", () => {
|
|
75
|
+
child.unref();
|
|
76
|
+
settle();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|