@mcoda/core 0.1.4
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/api/AgentsApi.d.ts +36 -0
- package/dist/api/AgentsApi.d.ts.map +1 -0
- package/dist/api/AgentsApi.js +176 -0
- package/dist/api/QaTasksApi.d.ts +8 -0
- package/dist/api/QaTasksApi.d.ts.map +1 -0
- package/dist/api/QaTasksApi.js +36 -0
- package/dist/api/TasksApi.d.ts +7 -0
- package/dist/api/TasksApi.d.ts.map +1 -0
- package/dist/api/TasksApi.js +34 -0
- package/dist/config/ConfigService.d.ts +3 -0
- package/dist/config/ConfigService.d.ts.map +1 -0
- package/dist/config/ConfigService.js +2 -0
- package/dist/domain/dependencies/Dependency.d.ts +3 -0
- package/dist/domain/dependencies/Dependency.d.ts.map +1 -0
- package/dist/domain/dependencies/Dependency.js +2 -0
- package/dist/domain/epics/Epic.d.ts +3 -0
- package/dist/domain/epics/Epic.d.ts.map +1 -0
- package/dist/domain/epics/Epic.js +2 -0
- package/dist/domain/projects/Project.d.ts +3 -0
- package/dist/domain/projects/Project.d.ts.map +1 -0
- package/dist/domain/projects/Project.js +2 -0
- package/dist/domain/tasks/Task.d.ts +3 -0
- package/dist/domain/tasks/Task.d.ts.map +1 -0
- package/dist/domain/tasks/Task.js +2 -0
- package/dist/domain/userStories/UserStory.d.ts +3 -0
- package/dist/domain/userStories/UserStory.d.ts.map +1 -0
- package/dist/domain/userStories/UserStory.js +2 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/prompts/PdrPrompts.d.ts +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -0
- package/dist/prompts/PdrPrompts.js +21 -0
- package/dist/prompts/PromptLoader.d.ts +3 -0
- package/dist/prompts/PromptLoader.d.ts.map +1 -0
- package/dist/prompts/PromptLoader.js +2 -0
- package/dist/prompts/SdsPrompts.d.ts +5 -0
- package/dist/prompts/SdsPrompts.d.ts.map +1 -0
- package/dist/prompts/SdsPrompts.js +44 -0
- package/dist/services/agents/AgentManagementService.d.ts +3 -0
- package/dist/services/agents/AgentManagementService.d.ts.map +1 -0
- package/dist/services/agents/AgentManagementService.js +2 -0
- package/dist/services/agents/GatewayAgentService.d.ts +92 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -0
- package/dist/services/agents/GatewayAgentService.js +870 -0
- package/dist/services/agents/RoutingApiClient.d.ts +23 -0
- package/dist/services/agents/RoutingApiClient.d.ts.map +1 -0
- package/dist/services/agents/RoutingApiClient.js +62 -0
- package/dist/services/agents/RoutingService.d.ts +50 -0
- package/dist/services/agents/RoutingService.d.ts.map +1 -0
- package/dist/services/agents/RoutingService.js +386 -0
- package/dist/services/agents/generated/RoutingApiClient.d.ts +21 -0
- package/dist/services/agents/generated/RoutingApiClient.d.ts.map +1 -0
- package/dist/services/agents/generated/RoutingApiClient.js +68 -0
- package/dist/services/backlog/BacklogService.d.ts +98 -0
- package/dist/services/backlog/BacklogService.d.ts.map +1 -0
- package/dist/services/backlog/BacklogService.js +453 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +88 -0
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -0
- package/dist/services/backlog/TaskOrderingService.js +675 -0
- package/dist/services/docs/DocsService.d.ts +82 -0
- package/dist/services/docs/DocsService.d.ts.map +1 -0
- package/dist/services/docs/DocsService.js +1631 -0
- package/dist/services/estimate/EstimateService.d.ts +12 -0
- package/dist/services/estimate/EstimateService.d.ts.map +1 -0
- package/dist/services/estimate/EstimateService.js +103 -0
- package/dist/services/estimate/VelocityService.d.ts +19 -0
- package/dist/services/estimate/VelocityService.d.ts.map +1 -0
- package/dist/services/estimate/VelocityService.js +237 -0
- package/dist/services/estimate/types.d.ts +30 -0
- package/dist/services/estimate/types.d.ts.map +1 -0
- package/dist/services/estimate/types.js +1 -0
- package/dist/services/execution/ExecutionService.d.ts +3 -0
- package/dist/services/execution/ExecutionService.d.ts.map +1 -0
- package/dist/services/execution/ExecutionService.js +2 -0
- package/dist/services/execution/QaFollowupService.d.ts +38 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -0
- package/dist/services/execution/QaFollowupService.js +236 -0
- package/dist/services/execution/QaProfileService.d.ts +22 -0
- package/dist/services/execution/QaProfileService.d.ts.map +1 -0
- package/dist/services/execution/QaProfileService.js +142 -0
- package/dist/services/execution/QaTasksService.d.ts +101 -0
- package/dist/services/execution/QaTasksService.d.ts.map +1 -0
- package/dist/services/execution/QaTasksService.js +1117 -0
- package/dist/services/execution/TaskSelectionService.d.ts +50 -0
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -0
- package/dist/services/execution/TaskSelectionService.js +281 -0
- package/dist/services/execution/TaskStateService.d.ts +19 -0
- package/dist/services/execution/TaskStateService.d.ts.map +1 -0
- package/dist/services/execution/TaskStateService.js +59 -0
- package/dist/services/execution/WorkOnTasksService.d.ts +80 -0
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -0
- package/dist/services/execution/WorkOnTasksService.js +1833 -0
- package/dist/services/jobs/JobInsightsService.d.ts +97 -0
- package/dist/services/jobs/JobInsightsService.d.ts.map +1 -0
- package/dist/services/jobs/JobInsightsService.js +263 -0
- package/dist/services/jobs/JobResumeService.d.ts +16 -0
- package/dist/services/jobs/JobResumeService.d.ts.map +1 -0
- package/dist/services/jobs/JobResumeService.js +113 -0
- package/dist/services/jobs/JobService.d.ts +149 -0
- package/dist/services/jobs/JobService.d.ts.map +1 -0
- package/dist/services/jobs/JobService.js +490 -0
- package/dist/services/jobs/JobsApiClient.d.ts +73 -0
- package/dist/services/jobs/JobsApiClient.d.ts.map +1 -0
- package/dist/services/jobs/JobsApiClient.js +67 -0
- package/dist/services/openapi/OpenApiService.d.ts +54 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -0
- package/dist/services/openapi/OpenApiService.js +503 -0
- package/dist/services/planning/CreateTasksService.d.ts +68 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -0
- package/dist/services/planning/CreateTasksService.js +989 -0
- package/dist/services/planning/KeyHelpers.d.ts +5 -0
- package/dist/services/planning/KeyHelpers.d.ts.map +1 -0
- package/dist/services/planning/KeyHelpers.js +62 -0
- package/dist/services/planning/PlanningService.d.ts +3 -0
- package/dist/services/planning/PlanningService.d.ts.map +1 -0
- package/dist/services/planning/PlanningService.js +2 -0
- package/dist/services/planning/RefineTasksService.d.ts +56 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -0
- package/dist/services/planning/RefineTasksService.js +1328 -0
- package/dist/services/review/CodeReviewService.d.ts +103 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -0
- package/dist/services/review/CodeReviewService.js +1187 -0
- package/dist/services/system/SystemUpdateService.d.ts +55 -0
- package/dist/services/system/SystemUpdateService.d.ts.map +1 -0
- package/dist/services/system/SystemUpdateService.js +136 -0
- package/dist/services/tasks/TaskApiResolver.d.ts +7 -0
- package/dist/services/tasks/TaskApiResolver.d.ts.map +1 -0
- package/dist/services/tasks/TaskApiResolver.js +41 -0
- package/dist/services/tasks/TaskDetailService.d.ts +106 -0
- package/dist/services/tasks/TaskDetailService.d.ts.map +1 -0
- package/dist/services/tasks/TaskDetailService.js +332 -0
- package/dist/services/telemetry/TelemetryService.d.ts +53 -0
- package/dist/services/telemetry/TelemetryService.d.ts.map +1 -0
- package/dist/services/telemetry/TelemetryService.js +434 -0
- package/dist/workspace/WorkspaceManager.d.ts +35 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -0
- package/dist/workspace/WorkspaceManager.js +201 -0
- package/package.json +45 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Connection, GlobalRepository } from "@mcoda/db";
|
|
4
|
+
import { PathHelper } from "@mcoda/shared";
|
|
5
|
+
import { TelemetryClient } from "@mcoda/integrations";
|
|
6
|
+
const hasTables = async (db, required) => {
|
|
7
|
+
const rows = await db.all(`SELECT name FROM sqlite_master WHERE type='table'`);
|
|
8
|
+
const names = new Set(rows.map((r) => r.name));
|
|
9
|
+
return required.every((name) => names.has(name));
|
|
10
|
+
};
|
|
11
|
+
const parseMetadata = (raw) => {
|
|
12
|
+
if (!raw)
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const addCost = (current, value) => {
|
|
22
|
+
if (value === null || value === undefined)
|
|
23
|
+
return current;
|
|
24
|
+
return (current ?? 0) + value;
|
|
25
|
+
};
|
|
26
|
+
const parseDurationMs = (input) => {
|
|
27
|
+
const match = input.trim().match(/^(\d+)([smhdw])$/i);
|
|
28
|
+
if (!match)
|
|
29
|
+
return undefined;
|
|
30
|
+
const amount = Number(match[1]);
|
|
31
|
+
const unit = match[2].toLowerCase();
|
|
32
|
+
const multipliers = {
|
|
33
|
+
s: 1000,
|
|
34
|
+
m: 60 * 1000,
|
|
35
|
+
h: 60 * 60 * 1000,
|
|
36
|
+
d: 24 * 60 * 60 * 1000,
|
|
37
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
38
|
+
};
|
|
39
|
+
return multipliers[unit] ? amount * multipliers[unit] : undefined;
|
|
40
|
+
};
|
|
41
|
+
const parseTimeInput = (input) => {
|
|
42
|
+
if (!input)
|
|
43
|
+
return undefined;
|
|
44
|
+
const duration = parseDurationMs(input);
|
|
45
|
+
if (duration !== undefined) {
|
|
46
|
+
return new Date(Date.now() - duration).toISOString();
|
|
47
|
+
}
|
|
48
|
+
const parsed = Date.parse(input);
|
|
49
|
+
if (!Number.isNaN(parsed)) {
|
|
50
|
+
return new Date(parsed).toISOString();
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
const normalizeGroupBy = (groupBy) => {
|
|
55
|
+
if (!groupBy || groupBy.length === 0) {
|
|
56
|
+
return ["project", "command", "agent"];
|
|
57
|
+
}
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
for (const dim of groupBy) {
|
|
60
|
+
if (["project", "agent", "command", "day", "model", "job", "action"].includes(dim)) {
|
|
61
|
+
seen.add(dim);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Array.from(seen);
|
|
65
|
+
};
|
|
66
|
+
const dayFromTimestamp = (timestamp) => {
|
|
67
|
+
if (!timestamp)
|
|
68
|
+
return null;
|
|
69
|
+
const parsed = Date.parse(timestamp);
|
|
70
|
+
if (Number.isNaN(parsed))
|
|
71
|
+
return null;
|
|
72
|
+
return new Date(parsed).toISOString().slice(0, 10);
|
|
73
|
+
};
|
|
74
|
+
export class TelemetryService {
|
|
75
|
+
constructor(workspace, deps) {
|
|
76
|
+
this.workspace = workspace;
|
|
77
|
+
this.db = deps.db;
|
|
78
|
+
this.connection = deps.connection;
|
|
79
|
+
this.client = deps.client;
|
|
80
|
+
}
|
|
81
|
+
static async create(workspace, options = {}) {
|
|
82
|
+
const baseUrl = workspace.config?.telemetry?.endpoint ?? process.env.MCODA_TELEMETRY_API;
|
|
83
|
+
const authToken = workspace.config?.telemetry?.authToken ?? process.env.MCODA_TELEMETRY_TOKEN;
|
|
84
|
+
if (baseUrl) {
|
|
85
|
+
return new TelemetryService(workspace, { client: new TelemetryClient({ baseUrl, authToken }) });
|
|
86
|
+
}
|
|
87
|
+
if (options.requireApi) {
|
|
88
|
+
throw new Error("Telemetry API is not configured (set MCODA_TELEMETRY_API/MCODA_TELEMETRY_TOKEN or telemetry.endpoint in workspace config).");
|
|
89
|
+
}
|
|
90
|
+
const dbPath = PathHelper.getWorkspaceDbPath(workspace.workspaceRoot);
|
|
91
|
+
try {
|
|
92
|
+
await fs.access(dbPath);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
if (!options.allowMissingTelemetry) {
|
|
96
|
+
throw new Error(`No workspace DB found at ${dbPath}. Run mcoda init or create-tasks first.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const connection = await Connection.open(dbPath);
|
|
100
|
+
const ok = await hasTables(connection.db, ["token_usage"]);
|
|
101
|
+
if (!ok && !options.allowMissingTelemetry) {
|
|
102
|
+
await connection.close();
|
|
103
|
+
throw new Error("Workspace DB is missing telemetry tables (token_usage). Run create-tasks to initialize it.");
|
|
104
|
+
}
|
|
105
|
+
return new TelemetryService(workspace, { db: connection.db, connection });
|
|
106
|
+
}
|
|
107
|
+
async close() {
|
|
108
|
+
if (this.connection) {
|
|
109
|
+
await this.connection.close();
|
|
110
|
+
}
|
|
111
|
+
if (this.globalRepo) {
|
|
112
|
+
await this.globalRepo.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
get configPath() {
|
|
116
|
+
return path.join(this.workspace.mcodaDir, "config.json");
|
|
117
|
+
}
|
|
118
|
+
async readConfigFile() {
|
|
119
|
+
try {
|
|
120
|
+
const raw = await fs.readFile(this.configPath, "utf8");
|
|
121
|
+
return JSON.parse(raw);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async writeConfigFile(config) {
|
|
128
|
+
await fs.mkdir(this.workspace.mcodaDir, { recursive: true });
|
|
129
|
+
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), "utf8");
|
|
130
|
+
}
|
|
131
|
+
async resolveProjectId(projectKey) {
|
|
132
|
+
if (!projectKey || !this.db)
|
|
133
|
+
return undefined;
|
|
134
|
+
const row = await this.db.get(`SELECT id FROM projects WHERE key = ?`, projectKey);
|
|
135
|
+
return row?.id;
|
|
136
|
+
}
|
|
137
|
+
async getGlobalRepo() {
|
|
138
|
+
if (!this.globalRepo) {
|
|
139
|
+
this.globalRepo = await GlobalRepository.create();
|
|
140
|
+
}
|
|
141
|
+
return this.globalRepo;
|
|
142
|
+
}
|
|
143
|
+
async resolveAgentId(agent) {
|
|
144
|
+
if (!agent)
|
|
145
|
+
return undefined;
|
|
146
|
+
try {
|
|
147
|
+
const repo = await this.getGlobalRepo();
|
|
148
|
+
const asId = await repo.getAgentById(agent);
|
|
149
|
+
if (asId)
|
|
150
|
+
return asId.id;
|
|
151
|
+
const bySlug = await repo.getAgentBySlug(agent);
|
|
152
|
+
if (bySlug)
|
|
153
|
+
return bySlug.id;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// ignore lookup failures; fall through to raw value
|
|
157
|
+
}
|
|
158
|
+
return agent;
|
|
159
|
+
}
|
|
160
|
+
filterRows(rows, options) {
|
|
161
|
+
const filtered = [];
|
|
162
|
+
for (const row of rows) {
|
|
163
|
+
const metadata = parseMetadata(row.metadata_json);
|
|
164
|
+
const commandName = metadata.commandName ??
|
|
165
|
+
metadata.command_name ??
|
|
166
|
+
metadata.command;
|
|
167
|
+
if (options.command && commandName && options.command !== commandName) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
filtered.push({ row, metadata });
|
|
171
|
+
}
|
|
172
|
+
return filtered;
|
|
173
|
+
}
|
|
174
|
+
mapConfig(apiConfig) {
|
|
175
|
+
return {
|
|
176
|
+
...apiConfig,
|
|
177
|
+
localRecording: apiConfig.localRecording ?? true,
|
|
178
|
+
remoteExport: apiConfig.remoteExport ?? true,
|
|
179
|
+
optOut: apiConfig.optOut ?? false,
|
|
180
|
+
strict: apiConfig.strict ?? false,
|
|
181
|
+
configPath: this.configPath,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async getSummary(options = {}) {
|
|
185
|
+
const groupBy = normalizeGroupBy(options.groupBy);
|
|
186
|
+
const since = parseTimeInput(options.since ?? "7d") ?? undefined;
|
|
187
|
+
const until = parseTimeInput(options.until);
|
|
188
|
+
const projectId = options.projectKey ? await this.resolveProjectId(options.projectKey) : undefined;
|
|
189
|
+
if (options.projectKey && !projectId && !this.client) {
|
|
190
|
+
throw new Error(`Unknown project key: ${options.projectKey}`);
|
|
191
|
+
}
|
|
192
|
+
const agentId = await this.resolveAgentId(options.agent);
|
|
193
|
+
if (this.client) {
|
|
194
|
+
return this.client.getSummary({
|
|
195
|
+
workspaceId: this.workspace.workspaceId,
|
|
196
|
+
projectId: projectId ?? options.projectKey,
|
|
197
|
+
agentId: agentId ?? options.agent,
|
|
198
|
+
commandName: options.command,
|
|
199
|
+
jobId: options.jobId,
|
|
200
|
+
from: since,
|
|
201
|
+
to: until,
|
|
202
|
+
groupBy,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (!this.db) {
|
|
206
|
+
throw new Error("Telemetry DB not available and no telemetry client configured.");
|
|
207
|
+
}
|
|
208
|
+
const clauses = ["workspace_id = ?"];
|
|
209
|
+
const params = [this.workspace.workspaceId];
|
|
210
|
+
if (projectId) {
|
|
211
|
+
clauses.push("project_id = ?");
|
|
212
|
+
params.push(projectId);
|
|
213
|
+
}
|
|
214
|
+
if (agentId) {
|
|
215
|
+
clauses.push("agent_id = ?");
|
|
216
|
+
params.push(agentId);
|
|
217
|
+
}
|
|
218
|
+
if (options.jobId) {
|
|
219
|
+
clauses.push("job_id = ?");
|
|
220
|
+
params.push(options.jobId);
|
|
221
|
+
}
|
|
222
|
+
if (since) {
|
|
223
|
+
clauses.push("timestamp >= ?");
|
|
224
|
+
params.push(since);
|
|
225
|
+
}
|
|
226
|
+
if (until) {
|
|
227
|
+
clauses.push("timestamp <= ?");
|
|
228
|
+
params.push(until);
|
|
229
|
+
}
|
|
230
|
+
const query = `SELECT * FROM token_usage ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""}`;
|
|
231
|
+
const rows = await this.db.all(query, ...params);
|
|
232
|
+
const filtered = this.filterRows(rows, { command: options.command });
|
|
233
|
+
const summary = new Map();
|
|
234
|
+
for (const { row, metadata } of filtered) {
|
|
235
|
+
const commandName = metadata.commandName ??
|
|
236
|
+
metadata.command_name ??
|
|
237
|
+
metadata.command ??
|
|
238
|
+
null;
|
|
239
|
+
const action = metadata.action ?? metadata.phase ?? null;
|
|
240
|
+
const keyParts = groupBy.map((dim) => {
|
|
241
|
+
switch (dim) {
|
|
242
|
+
case "project":
|
|
243
|
+
return row.project_id ?? "";
|
|
244
|
+
case "agent":
|
|
245
|
+
return row.agent_id ?? "";
|
|
246
|
+
case "model":
|
|
247
|
+
return row.model_name ?? "";
|
|
248
|
+
case "command":
|
|
249
|
+
return commandName ?? "";
|
|
250
|
+
case "day":
|
|
251
|
+
return dayFromTimestamp(row.timestamp) ?? "";
|
|
252
|
+
case "job":
|
|
253
|
+
return row.job_id ?? "";
|
|
254
|
+
case "action":
|
|
255
|
+
return action ?? "";
|
|
256
|
+
default:
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const key = keyParts.join("|");
|
|
261
|
+
let record = summary.get(key);
|
|
262
|
+
if (!record) {
|
|
263
|
+
record = {
|
|
264
|
+
workspace_id: this.workspace.workspaceId,
|
|
265
|
+
project_id: groupBy.includes("project") ? row.project_id ?? null : undefined,
|
|
266
|
+
agent_id: groupBy.includes("agent") ? row.agent_id ?? null : undefined,
|
|
267
|
+
model_name: groupBy.includes("model") ? row.model_name ?? null : undefined,
|
|
268
|
+
command_name: groupBy.includes("command") ? commandName : undefined,
|
|
269
|
+
action: groupBy.includes("action") ? action : undefined,
|
|
270
|
+
job_id: groupBy.includes("job") ? row.job_id ?? null : undefined,
|
|
271
|
+
day: groupBy.includes("day") ? dayFromTimestamp(row.timestamp) : undefined,
|
|
272
|
+
calls: 0,
|
|
273
|
+
tokens_prompt: 0,
|
|
274
|
+
tokens_completion: 0,
|
|
275
|
+
tokens_total: 0,
|
|
276
|
+
cost_estimate: null,
|
|
277
|
+
};
|
|
278
|
+
summary.set(key, record);
|
|
279
|
+
}
|
|
280
|
+
record.calls += 1;
|
|
281
|
+
record.tokens_prompt += row.tokens_prompt ?? 0;
|
|
282
|
+
record.tokens_completion += row.tokens_completion ?? 0;
|
|
283
|
+
record.tokens_total += row.tokens_total ?? 0;
|
|
284
|
+
record.cost_estimate = addCost(record.cost_estimate, row.cost_estimate);
|
|
285
|
+
}
|
|
286
|
+
return Array.from(summary.values());
|
|
287
|
+
}
|
|
288
|
+
async getTokenUsage(options = {}) {
|
|
289
|
+
const since = parseTimeInput(options.since);
|
|
290
|
+
const until = parseTimeInput(options.until);
|
|
291
|
+
const projectId = options.projectKey ? await this.resolveProjectId(options.projectKey) : undefined;
|
|
292
|
+
if (options.projectKey && !projectId && !this.client) {
|
|
293
|
+
throw new Error(`Unknown project key: ${options.projectKey}`);
|
|
294
|
+
}
|
|
295
|
+
const agentId = await this.resolveAgentId(options.agent);
|
|
296
|
+
if (this.client) {
|
|
297
|
+
const rows = await this.client.getTokenUsage({
|
|
298
|
+
workspaceId: this.workspace.workspaceId,
|
|
299
|
+
projectId: projectId ?? options.projectKey,
|
|
300
|
+
agentId: agentId ?? options.agent,
|
|
301
|
+
commandName: options.command,
|
|
302
|
+
jobId: options.jobId,
|
|
303
|
+
from: since,
|
|
304
|
+
to: until,
|
|
305
|
+
page: options.page,
|
|
306
|
+
pageSize: options.pageSize,
|
|
307
|
+
sort: "timestamp:asc",
|
|
308
|
+
});
|
|
309
|
+
return rows.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
310
|
+
}
|
|
311
|
+
if (!this.db) {
|
|
312
|
+
throw new Error("Telemetry DB not available and no telemetry client configured.");
|
|
313
|
+
}
|
|
314
|
+
const clauses = ["workspace_id = ?"];
|
|
315
|
+
const params = [this.workspace.workspaceId];
|
|
316
|
+
if (projectId) {
|
|
317
|
+
clauses.push("project_id = ?");
|
|
318
|
+
params.push(projectId);
|
|
319
|
+
}
|
|
320
|
+
if (agentId) {
|
|
321
|
+
clauses.push("agent_id = ?");
|
|
322
|
+
params.push(agentId);
|
|
323
|
+
}
|
|
324
|
+
if (options.jobId) {
|
|
325
|
+
clauses.push("job_id = ?");
|
|
326
|
+
params.push(options.jobId);
|
|
327
|
+
}
|
|
328
|
+
if (since) {
|
|
329
|
+
clauses.push("timestamp >= ?");
|
|
330
|
+
params.push(since);
|
|
331
|
+
}
|
|
332
|
+
if (until) {
|
|
333
|
+
clauses.push("timestamp <= ?");
|
|
334
|
+
params.push(until);
|
|
335
|
+
}
|
|
336
|
+
const query = `SELECT * FROM token_usage ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""}`;
|
|
337
|
+
const rows = await this.db.all(query, ...params);
|
|
338
|
+
const filtered = this.filterRows(rows, { command: options.command });
|
|
339
|
+
const mapped = filtered
|
|
340
|
+
.map(({ row, metadata }) => {
|
|
341
|
+
const commandName = metadata.commandName ??
|
|
342
|
+
metadata.command_name ??
|
|
343
|
+
metadata.command ??
|
|
344
|
+
null;
|
|
345
|
+
const action = metadata.action ?? metadata.phase ?? null;
|
|
346
|
+
const errorKind = metadata.error_kind ??
|
|
347
|
+
metadata.errorKind ??
|
|
348
|
+
metadata.error ??
|
|
349
|
+
null;
|
|
350
|
+
return {
|
|
351
|
+
workspace_id: row.workspace_id,
|
|
352
|
+
agent_id: row.agent_id,
|
|
353
|
+
model_name: row.model_name,
|
|
354
|
+
job_id: row.job_id,
|
|
355
|
+
command_run_id: row.command_run_id,
|
|
356
|
+
task_run_id: row.task_run_id,
|
|
357
|
+
task_id: row.task_id,
|
|
358
|
+
project_id: row.project_id,
|
|
359
|
+
epic_id: row.epic_id,
|
|
360
|
+
user_story_id: row.user_story_id,
|
|
361
|
+
tokens_prompt: row.tokens_prompt,
|
|
362
|
+
tokens_completion: row.tokens_completion,
|
|
363
|
+
tokens_total: row.tokens_total,
|
|
364
|
+
cost_estimate: row.cost_estimate,
|
|
365
|
+
duration_seconds: row.duration_seconds,
|
|
366
|
+
timestamp: row.timestamp,
|
|
367
|
+
command_name: commandName,
|
|
368
|
+
action,
|
|
369
|
+
error_kind: errorKind,
|
|
370
|
+
metadata,
|
|
371
|
+
};
|
|
372
|
+
})
|
|
373
|
+
.sort((a, b) => {
|
|
374
|
+
const aTs = Date.parse(a.timestamp);
|
|
375
|
+
const bTs = Date.parse(b.timestamp);
|
|
376
|
+
if (Number.isNaN(aTs) || Number.isNaN(bTs))
|
|
377
|
+
return 0;
|
|
378
|
+
return aTs - bTs;
|
|
379
|
+
});
|
|
380
|
+
if (options.page && options.pageSize) {
|
|
381
|
+
const start = (options.page - 1) * options.pageSize;
|
|
382
|
+
return mapped.slice(start, start + options.pageSize);
|
|
383
|
+
}
|
|
384
|
+
return mapped;
|
|
385
|
+
}
|
|
386
|
+
async getConfig() {
|
|
387
|
+
if (this.client) {
|
|
388
|
+
const config = await this.client.getConfig(this.workspace.workspaceId);
|
|
389
|
+
return this.mapConfig(config);
|
|
390
|
+
}
|
|
391
|
+
const config = await this.readConfigFile();
|
|
392
|
+
const telemetry = config.telemetry ?? {};
|
|
393
|
+
const envOptOut = (process.env.MCODA_TELEMETRY ?? "").toLowerCase() === "off";
|
|
394
|
+
const optOut = telemetry.optOut === true || telemetry.optedOut === true || false;
|
|
395
|
+
const strict = telemetry.strict === true || false;
|
|
396
|
+
const remoteExport = !optOut && !envOptOut;
|
|
397
|
+
const localRecording = !strict;
|
|
398
|
+
return {
|
|
399
|
+
localRecording,
|
|
400
|
+
remoteExport,
|
|
401
|
+
optOut,
|
|
402
|
+
strict,
|
|
403
|
+
configPath: this.configPath,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async optOut(strict = false) {
|
|
407
|
+
if (this.client) {
|
|
408
|
+
const config = await this.client.optOut(this.workspace.workspaceId, strict);
|
|
409
|
+
return this.mapConfig(config);
|
|
410
|
+
}
|
|
411
|
+
const config = await this.readConfigFile();
|
|
412
|
+
config.telemetry = {
|
|
413
|
+
...(config.telemetry ?? {}),
|
|
414
|
+
optOut: true,
|
|
415
|
+
strict: strict || (config.telemetry?.strict ?? false),
|
|
416
|
+
};
|
|
417
|
+
await this.writeConfigFile(config);
|
|
418
|
+
return this.getConfig();
|
|
419
|
+
}
|
|
420
|
+
async optIn() {
|
|
421
|
+
if (this.client) {
|
|
422
|
+
const config = await this.client.optIn(this.workspace.workspaceId);
|
|
423
|
+
return this.mapConfig(config);
|
|
424
|
+
}
|
|
425
|
+
const config = await this.readConfigFile();
|
|
426
|
+
config.telemetry = {
|
|
427
|
+
...(config.telemetry ?? {}),
|
|
428
|
+
optOut: false,
|
|
429
|
+
strict: false,
|
|
430
|
+
};
|
|
431
|
+
await this.writeConfigFile(config);
|
|
432
|
+
return this.getConfig();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface WorkspaceResolution {
|
|
2
|
+
workspaceRoot: string;
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
id: string;
|
|
5
|
+
legacyWorkspaceIds: string[];
|
|
6
|
+
mcodaDir: string;
|
|
7
|
+
workspaceDbPath: string;
|
|
8
|
+
globalDbPath: string;
|
|
9
|
+
config?: WorkspaceConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface TelemetryPreferences {
|
|
12
|
+
optOut?: boolean;
|
|
13
|
+
strict?: boolean;
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
authToken?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WorkspaceConfig {
|
|
18
|
+
mirrorDocs?: boolean;
|
|
19
|
+
branch?: string;
|
|
20
|
+
docdexUrl?: string;
|
|
21
|
+
velocity?: {
|
|
22
|
+
implementationSpPerHour?: number;
|
|
23
|
+
reviewSpPerHour?: number;
|
|
24
|
+
qaSpPerHour?: number;
|
|
25
|
+
alpha?: number;
|
|
26
|
+
};
|
|
27
|
+
telemetry?: TelemetryPreferences;
|
|
28
|
+
}
|
|
29
|
+
export declare class WorkspaceResolver {
|
|
30
|
+
static resolveWorkspace(input: {
|
|
31
|
+
cwd?: string;
|
|
32
|
+
explicitWorkspace?: string;
|
|
33
|
+
}): Promise<WorkspaceResolution>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=WorkspaceManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkspaceManager.d.ts","sourceRoot":"","sources":["../../src/workspace/WorkspaceManager.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE;QACT,uBAAuB,CAAC,EAAE,MAAM,CAAC;QACjC,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,SAAS,CAAC,EAAE,oBAAoB,CAAC;CAClC;AAqHD,qBAAa,iBAAiB;WACf,gBAAgB,CAAC,KAAK,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAiFjH"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { PathHelper } from "@mcoda/shared";
|
|
5
|
+
const fileExists = async (candidate) => {
|
|
6
|
+
try {
|
|
7
|
+
await fs.access(candidate);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const findGitRoot = async (start) => {
|
|
15
|
+
// Only consider the provided directory; do not walk upward.
|
|
16
|
+
const current = path.resolve(start);
|
|
17
|
+
const gitPath = path.join(current, ".git");
|
|
18
|
+
if (await fileExists(gitPath))
|
|
19
|
+
return current;
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
const findWorkspaceMarker = async (start) => {
|
|
23
|
+
// Only consider the provided directory; do not walk upward.
|
|
24
|
+
const current = path.resolve(start);
|
|
25
|
+
const marker = path.join(current, ".mcoda", "workspace.json");
|
|
26
|
+
const mcoda = path.join(current, ".mcoda");
|
|
27
|
+
if (await fileExists(marker))
|
|
28
|
+
return current;
|
|
29
|
+
if (await fileExists(mcoda))
|
|
30
|
+
return current;
|
|
31
|
+
return undefined;
|
|
32
|
+
};
|
|
33
|
+
const ensureGitignore = async (workspaceRoot) => {
|
|
34
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
35
|
+
const entry = ".mcoda/\n";
|
|
36
|
+
try {
|
|
37
|
+
const content = await fs.readFile(gitignorePath, "utf8");
|
|
38
|
+
if (content.includes(".mcoda/"))
|
|
39
|
+
return;
|
|
40
|
+
await fs.writeFile(gitignorePath, `${content.trimEnd()}\n${entry}`, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
await fs.writeFile(gitignorePath, entry, "utf8");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const readWorkspaceConfig = async (mcodaDir) => {
|
|
47
|
+
const configPath = path.join(mcodaDir, "config.json");
|
|
48
|
+
try {
|
|
49
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const readWorkspaceIdentity = async (mcodaDir) => {
|
|
57
|
+
const workspacePath = path.join(mcodaDir, "workspace.json");
|
|
58
|
+
try {
|
|
59
|
+
const raw = await fs.readFile(workspacePath, "utf8");
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (parsed?.id)
|
|
62
|
+
return parsed;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const writeWorkspaceIdentity = async (mcodaDir, identity) => {
|
|
70
|
+
const workspacePath = path.join(mcodaDir, "workspace.json");
|
|
71
|
+
await fs.writeFile(workspacePath, JSON.stringify(identity, null, 2), "utf8");
|
|
72
|
+
};
|
|
73
|
+
const looksLikeWorkspaceId = (value) => /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value.trim());
|
|
74
|
+
const migrateWorkspaceDbIds = async (workspace, legacyIds) => {
|
|
75
|
+
if (!legacyIds.length)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
const { Connection } = await import("@mcoda/db");
|
|
79
|
+
const conn = await Connection.open(workspace.workspaceDbPath);
|
|
80
|
+
const db = conn.db;
|
|
81
|
+
const placeholders = legacyIds.map(() => "?").join(",");
|
|
82
|
+
const params = [workspace.workspaceId, ...legacyIds];
|
|
83
|
+
const tables = ["jobs", "command_runs", "token_usage"];
|
|
84
|
+
for (const table of tables) {
|
|
85
|
+
await db.run(`UPDATE ${table} SET workspace_id = ? WHERE workspace_id IN (${placeholders})`, params);
|
|
86
|
+
}
|
|
87
|
+
await conn.close();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* best effort */
|
|
91
|
+
}
|
|
92
|
+
const updateJsonArray = async (filePath) => {
|
|
93
|
+
try {
|
|
94
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (!Array.isArray(parsed) || !parsed.length)
|
|
97
|
+
return;
|
|
98
|
+
let changed = false;
|
|
99
|
+
const updated = parsed.map((row) => {
|
|
100
|
+
if (row?.workspaceId && legacyIds.includes(row.workspaceId)) {
|
|
101
|
+
changed = true;
|
|
102
|
+
return { ...row, workspaceId: workspace.workspaceId };
|
|
103
|
+
}
|
|
104
|
+
return row;
|
|
105
|
+
});
|
|
106
|
+
if (changed) {
|
|
107
|
+
await fs.writeFile(filePath, JSON.stringify(updated, null, 2), "utf8");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* ignore */
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
await updateJsonArray(path.join(workspace.workspaceRoot, ".mcoda", "command_runs.json"));
|
|
115
|
+
await updateJsonArray(path.join(workspace.workspaceRoot, ".mcoda", "token_usage.json"));
|
|
116
|
+
};
|
|
117
|
+
export class WorkspaceResolver {
|
|
118
|
+
static async resolveWorkspace(input) {
|
|
119
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
120
|
+
let explicit = input.explicitWorkspace;
|
|
121
|
+
let explicitPath;
|
|
122
|
+
if (explicit) {
|
|
123
|
+
const candidatePath = path.resolve(explicit);
|
|
124
|
+
if (await fileExists(candidatePath)) {
|
|
125
|
+
explicitPath = candidatePath;
|
|
126
|
+
}
|
|
127
|
+
else if (await fileExists(path.join(candidatePath, ".mcoda"))) {
|
|
128
|
+
explicitPath = candidatePath;
|
|
129
|
+
}
|
|
130
|
+
else if (looksLikeWorkspaceId(explicit)) {
|
|
131
|
+
throw new Error(`Workspace id ${explicit} not recognized. Workspace registry lookups are not yet supported; pass a workspace path instead.`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
throw new Error(`Workspace path ${explicit} not found`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const fromMarker = await findWorkspaceMarker(explicitPath ?? cwd);
|
|
138
|
+
const gitRoot = await findGitRoot(explicitPath ?? cwd);
|
|
139
|
+
const workspaceRoot = explicitPath ?? fromMarker ?? gitRoot ?? cwd;
|
|
140
|
+
const mcodaDir = path.join(workspaceRoot, ".mcoda");
|
|
141
|
+
await PathHelper.ensureDir(mcodaDir);
|
|
142
|
+
await ensureGitignore(workspaceRoot);
|
|
143
|
+
const existingIdentity = await readWorkspaceIdentity(mcodaDir);
|
|
144
|
+
let identity;
|
|
145
|
+
let legacyIds = [];
|
|
146
|
+
if (existingIdentity) {
|
|
147
|
+
const existingLegacy = new Set(existingIdentity.legacyIds ?? []);
|
|
148
|
+
let updatedIdentity = false;
|
|
149
|
+
legacyIds = [...(existingIdentity.legacyIds ?? [])];
|
|
150
|
+
if (existingIdentity.id && existingIdentity.id !== workspaceRoot) {
|
|
151
|
+
legacyIds.push(workspaceRoot);
|
|
152
|
+
updatedIdentity = true;
|
|
153
|
+
}
|
|
154
|
+
if (!looksLikeWorkspaceId(existingIdentity.id)) {
|
|
155
|
+
legacyIds.push(existingIdentity.id);
|
|
156
|
+
identity = {
|
|
157
|
+
...existingIdentity,
|
|
158
|
+
id: randomUUID(),
|
|
159
|
+
legacyIds: Array.from(new Set(legacyIds)),
|
|
160
|
+
};
|
|
161
|
+
await writeWorkspaceIdentity(mcodaDir, identity);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
identity = {
|
|
165
|
+
...existingIdentity,
|
|
166
|
+
legacyIds: Array.from(new Set(legacyIds)),
|
|
167
|
+
};
|
|
168
|
+
if ((identity.legacyIds?.length ?? 0) !== existingLegacy.size) {
|
|
169
|
+
updatedIdentity = true;
|
|
170
|
+
}
|
|
171
|
+
if (updatedIdentity) {
|
|
172
|
+
await writeWorkspaceIdentity(mcodaDir, identity);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
identity = {
|
|
178
|
+
id: randomUUID(),
|
|
179
|
+
name: path.basename(workspaceRoot),
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
legacyIds: [workspaceRoot],
|
|
182
|
+
};
|
|
183
|
+
await writeWorkspaceIdentity(mcodaDir, identity);
|
|
184
|
+
}
|
|
185
|
+
const legacyWorkspaceIds = Array.from(new Set([...(identity.legacyIds ?? []), workspaceRoot].filter(Boolean)));
|
|
186
|
+
const config = await readWorkspaceConfig(mcodaDir);
|
|
187
|
+
const resolution = {
|
|
188
|
+
workspaceRoot,
|
|
189
|
+
workspaceId: identity.id,
|
|
190
|
+
id: identity.id,
|
|
191
|
+
legacyWorkspaceIds,
|
|
192
|
+
mcodaDir,
|
|
193
|
+
workspaceDbPath: PathHelper.getWorkspaceDbPath(workspaceRoot),
|
|
194
|
+
globalDbPath: PathHelper.getGlobalDbPath(),
|
|
195
|
+
config,
|
|
196
|
+
};
|
|
197
|
+
// Best-effort migration of workspace_id columns and JSON logs from legacy IDs.
|
|
198
|
+
await migrateWorkspaceDbIds(resolution, legacyWorkspaceIds.filter((id) => id !== identity.id));
|
|
199
|
+
return resolution;
|
|
200
|
+
}
|
|
201
|
+
}
|