@mcoda/integrations 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +9 -0
  4. package/dist/docdex/DocdexClient.d.ts +50 -0
  5. package/dist/docdex/DocdexClient.d.ts.map +1 -0
  6. package/dist/docdex/DocdexClient.js +216 -0
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +5 -0
  10. package/dist/issues/IssuesClient.d.ts +3 -0
  11. package/dist/issues/IssuesClient.d.ts.map +1 -0
  12. package/dist/issues/IssuesClient.js +2 -0
  13. package/dist/qa/ChromiumQaAdapter.d.ts +10 -0
  14. package/dist/qa/ChromiumQaAdapter.d.ts.map +1 -0
  15. package/dist/qa/ChromiumQaAdapter.js +85 -0
  16. package/dist/qa/CliQaAdapter.d.ts +10 -0
  17. package/dist/qa/CliQaAdapter.d.ts.map +1 -0
  18. package/dist/qa/CliQaAdapter.js +91 -0
  19. package/dist/qa/MaestroQaAdapter.d.ts +10 -0
  20. package/dist/qa/MaestroQaAdapter.d.ts.map +1 -0
  21. package/dist/qa/MaestroQaAdapter.js +87 -0
  22. package/dist/qa/QaAdapter.d.ts +7 -0
  23. package/dist/qa/QaAdapter.d.ts.map +1 -0
  24. package/dist/qa/QaAdapter.js +1 -0
  25. package/dist/qa/QaClient.d.ts +3 -0
  26. package/dist/qa/QaClient.d.ts.map +1 -0
  27. package/dist/qa/QaClient.js +2 -0
  28. package/dist/qa/QaTypes.d.ts +24 -0
  29. package/dist/qa/QaTypes.d.ts.map +1 -0
  30. package/dist/qa/QaTypes.js +1 -0
  31. package/dist/qa/index.d.ts +6 -0
  32. package/dist/qa/index.d.ts.map +1 -0
  33. package/dist/qa/index.js +5 -0
  34. package/dist/system/SystemClient.d.ts +12 -0
  35. package/dist/system/SystemClient.d.ts.map +1 -0
  36. package/dist/system/SystemClient.js +47 -0
  37. package/dist/telemetry/TelemetryClient.d.ts +79 -0
  38. package/dist/telemetry/TelemetryClient.d.ts.map +1 -0
  39. package/dist/telemetry/TelemetryClient.js +80 -0
  40. package/dist/vcs/VcsClient.d.ts +29 -0
  41. package/dist/vcs/VcsClient.d.ts.map +1 -0
  42. package/dist/vcs/VcsClient.js +201 -0
  43. package/package.json +47 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+ - Initial public packaging for @mcoda/integrations.
5
+
6
+ ## 0.1.4
7
+ - Initial release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 bekir dag
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @mcoda/integrations
2
+
3
+ External integrations for mcoda (vcs, QA, telemetry).
4
+
5
+ ## Usage
6
+ This package is primarily an internal dependency of `mcoda`.
7
+
8
+ ## License
9
+ MIT - see `LICENSE`.
@@ -0,0 +1,50 @@
1
+ export interface DocdexSegment {
2
+ id: string;
3
+ docId: string;
4
+ index: number;
5
+ content: string;
6
+ heading?: string;
7
+ }
8
+ export interface DocdexDocument {
9
+ id: string;
10
+ docType: string;
11
+ path?: string;
12
+ title?: string;
13
+ content?: string;
14
+ metadata?: Record<string, unknown>;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ segments?: DocdexSegment[];
18
+ }
19
+ export interface RegisterDocumentInput {
20
+ docType: string;
21
+ path?: string;
22
+ title?: string;
23
+ content: string;
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+ export declare class DocdexClient {
27
+ private options;
28
+ constructor(options?: {
29
+ workspaceRoot?: string;
30
+ storePath?: string;
31
+ baseUrl?: string;
32
+ authToken?: string;
33
+ });
34
+ private getStorePath;
35
+ private normalizePath;
36
+ private loadStore;
37
+ private fetchRemote;
38
+ private saveStore;
39
+ fetchDocumentById(id: string): Promise<DocdexDocument>;
40
+ findDocumentByPath(docPath: string, docType?: string): Promise<DocdexDocument | undefined>;
41
+ search(filter: {
42
+ docType?: string;
43
+ projectKey?: string;
44
+ query?: string;
45
+ profile?: string;
46
+ }): Promise<DocdexDocument[]>;
47
+ registerDocument(input: RegisterDocumentInput): Promise<DocdexDocument>;
48
+ ensureRegisteredFromFile(docPath: string, docType: string, metadata?: Record<string, unknown>): Promise<DocdexDocument>;
49
+ }
50
+ //# sourceMappingURL=DocdexClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocdexClient.d.ts","sourceRoot":"","sources":["../../src/docdex/DocdexClient.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAyCD,qBAAa,YAAY;IAErB,OAAO,CAAC,OAAO;gBAAP,OAAO,GAAE;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACf;IAGR,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,aAAa;YAYP,SAAS;YAaT,WAAW;YAYX,SAAS;IAOjB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAoBtD,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAU1F,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA0BtH,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IA6DvE,wBAAwB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,cAAc,CAAC;CAO3B"}
@@ -0,0 +1,216 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ const nowIso = () => new Date().toISOString();
5
+ const segmentize = (docId, content) => {
6
+ const lines = content.split(/\r?\n/);
7
+ const segments = [];
8
+ let buffer = [];
9
+ let heading = "";
10
+ const flush = () => {
11
+ if (buffer.length === 0)
12
+ return;
13
+ segments.push({
14
+ id: `${docId}-seg-${segments.length + 1}`,
15
+ docId,
16
+ index: segments.length,
17
+ content: buffer.join("\n").trim(),
18
+ heading: heading || undefined,
19
+ });
20
+ buffer = [];
21
+ heading = "";
22
+ };
23
+ for (const line of lines) {
24
+ if (/^#{1,6}\s+/.test(line)) {
25
+ flush();
26
+ heading = line.replace(/^#{1,6}\s+/, "").trim();
27
+ }
28
+ buffer.push(line);
29
+ if (buffer.join("").length > 1500) {
30
+ flush();
31
+ }
32
+ }
33
+ flush();
34
+ return segments;
35
+ };
36
+ export class DocdexClient {
37
+ constructor(options = {}) {
38
+ this.options = options;
39
+ }
40
+ getStorePath() {
41
+ const base = this.options.storePath
42
+ ? path.resolve(this.options.storePath)
43
+ : path.join(this.options.workspaceRoot ?? process.cwd(), ".mcoda", "docdex", "documents.json");
44
+ return base;
45
+ }
46
+ normalizePath(inputPath) {
47
+ if (!inputPath)
48
+ return undefined;
49
+ const absolute = path.resolve(inputPath);
50
+ if (this.options.workspaceRoot) {
51
+ const root = path.resolve(this.options.workspaceRoot);
52
+ if (absolute.startsWith(root)) {
53
+ return path.relative(root, absolute);
54
+ }
55
+ }
56
+ return absolute;
57
+ }
58
+ async loadStore() {
59
+ const storePath = this.getStorePath();
60
+ try {
61
+ const raw = await fs.readFile(storePath, "utf8");
62
+ const parsed = JSON.parse(raw);
63
+ parsed.documents = parsed.documents ?? [];
64
+ parsed.segments = parsed.segments ?? [];
65
+ return parsed;
66
+ }
67
+ catch {
68
+ return { updatedAt: nowIso(), documents: [], segments: [] };
69
+ }
70
+ }
71
+ async fetchRemote(pathname, init) {
72
+ if (!this.options.baseUrl)
73
+ throw new Error("Docdex baseUrl not configured");
74
+ const url = new URL(pathname, this.options.baseUrl);
75
+ const headers = { "Content-Type": "application/json" };
76
+ if (this.options.authToken)
77
+ headers.authorization = `Bearer ${this.options.authToken}`;
78
+ const response = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
79
+ if (!response.ok) {
80
+ throw new Error(`Docdex request failed (${response.status}): ${await response.text()}`);
81
+ }
82
+ return (await response.json());
83
+ }
84
+ async saveStore(store) {
85
+ const storePath = this.getStorePath();
86
+ await fs.mkdir(path.dirname(storePath), { recursive: true });
87
+ const payload = { ...store, updatedAt: nowIso() };
88
+ await fs.writeFile(storePath, JSON.stringify(payload, null, 2), "utf8");
89
+ }
90
+ async fetchDocumentById(id) {
91
+ if (this.options.baseUrl) {
92
+ try {
93
+ const doc = await this.fetchRemote(`/documents/${id}`);
94
+ return doc;
95
+ }
96
+ catch (error) {
97
+ // fall through to local if remote fails
98
+ // eslint-disable-next-line no-console
99
+ console.warn(`Docdex remote fetch failed, falling back to local: ${error.message}`);
100
+ }
101
+ }
102
+ const store = await this.loadStore();
103
+ const doc = store.documents.find((d) => d.id === id);
104
+ if (!doc) {
105
+ throw new Error(`Docdex document not found: ${id}`);
106
+ }
107
+ const segments = store.segments.filter((s) => s.docId === id);
108
+ return { ...doc, segments };
109
+ }
110
+ async findDocumentByPath(docPath, docType) {
111
+ const normalized = this.normalizePath(docPath);
112
+ const store = await this.loadStore();
113
+ const doc = store.documents.find((d) => d.path === normalized && (!docType || d.docType.toLowerCase() === docType.toLowerCase()));
114
+ if (!doc)
115
+ return undefined;
116
+ return { ...doc, segments: store.segments.filter((s) => s.docId === doc.id) };
117
+ }
118
+ async search(filter) {
119
+ if (this.options.baseUrl) {
120
+ try {
121
+ const params = new URLSearchParams();
122
+ if (filter.docType)
123
+ params.set("doc_type", filter.docType);
124
+ if (filter.projectKey)
125
+ params.set("project_key", filter.projectKey);
126
+ if (filter.query)
127
+ params.set("q", filter.query);
128
+ if (filter.profile)
129
+ params.set("profile", filter.profile);
130
+ const path = `/documents?${params.toString()}`;
131
+ const docs = await this.fetchRemote(path);
132
+ return docs;
133
+ }
134
+ catch (error) {
135
+ // eslint-disable-next-line no-console
136
+ console.warn(`Docdex remote search failed, falling back to local: ${error.message}`);
137
+ }
138
+ }
139
+ const store = await this.loadStore();
140
+ return store.documents
141
+ .filter((doc) => {
142
+ if (filter.docType && doc.docType.toLowerCase() !== filter.docType.toLowerCase())
143
+ return false;
144
+ if (filter.projectKey && doc.metadata && doc.metadata.projectKey !== filter.projectKey)
145
+ return false;
146
+ return true;
147
+ })
148
+ .map((doc) => ({ ...doc, segments: store.segments.filter((s) => s.docId === doc.id) }));
149
+ }
150
+ async registerDocument(input) {
151
+ if (this.options.baseUrl) {
152
+ try {
153
+ const registered = await this.fetchRemote(`/documents`, {
154
+ method: "POST",
155
+ body: JSON.stringify({
156
+ doc_type: input.docType,
157
+ path: input.path,
158
+ title: input.title,
159
+ content: input.content,
160
+ metadata: input.metadata,
161
+ }),
162
+ });
163
+ return registered;
164
+ }
165
+ catch (error) {
166
+ // eslint-disable-next-line no-console
167
+ console.warn(`Docdex remote register failed, falling back to local: ${error.message}`);
168
+ }
169
+ }
170
+ const store = await this.loadStore();
171
+ const normalizedPath = this.normalizePath(input.path);
172
+ const existingByPath = normalizedPath
173
+ ? store.documents.find((d) => d.path === normalizedPath &&
174
+ d.docType.toLowerCase() === input.docType.toLowerCase() &&
175
+ (input.metadata?.projectKey ? d.metadata?.projectKey === input.metadata.projectKey : true))
176
+ : undefined;
177
+ const now = nowIso();
178
+ if (existingByPath) {
179
+ const updated = {
180
+ ...existingByPath,
181
+ content: input.content ?? existingByPath.content,
182
+ metadata: { ...(existingByPath.metadata ?? {}), ...(input.metadata ?? {}) },
183
+ title: input.title ?? existingByPath.title,
184
+ updatedAt: now,
185
+ };
186
+ const segments = segmentize(updated.id, input.content);
187
+ store.documents[store.documents.findIndex((d) => d.id === existingByPath.id)] = updated;
188
+ store.segments = store.segments.filter((s) => s.docId !== updated.id).concat(segments);
189
+ await this.saveStore(store);
190
+ return { ...updated, segments };
191
+ }
192
+ const doc = {
193
+ id: randomUUID(),
194
+ docType: input.docType,
195
+ path: normalizedPath,
196
+ content: input.content,
197
+ metadata: input.metadata,
198
+ title: input.title,
199
+ createdAt: now,
200
+ updatedAt: now,
201
+ };
202
+ const segments = segmentize(doc.id, input.content);
203
+ store.documents.push(doc);
204
+ store.segments.push(...segments);
205
+ await this.saveStore(store);
206
+ return { ...doc, segments };
207
+ }
208
+ async ensureRegisteredFromFile(docPath, docType, metadata) {
209
+ const normalizedPath = this.normalizePath(docPath) ?? docPath;
210
+ const existing = await this.findDocumentByPath(normalizedPath, docType);
211
+ if (existing)
212
+ return existing;
213
+ const content = await fs.readFile(docPath, "utf8");
214
+ return this.registerDocument({ docType, path: docPath, content, metadata });
215
+ }
216
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./docdex/DocdexClient.js";
2
+ export * from "./telemetry/TelemetryClient.js";
3
+ export * from "./vcs/VcsClient.js";
4
+ export * from "./qa/index.js";
5
+ export * from "./system/SystemClient.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./docdex/DocdexClient.js";
2
+ export * from "./telemetry/TelemetryClient.js";
3
+ export * from "./vcs/VcsClient.js";
4
+ export * from "./qa/index.js";
5
+ export * from "./system/SystemClient.js";
@@ -0,0 +1,3 @@
1
+ export declare class IssuesClient {
2
+ }
3
+ //# sourceMappingURL=IssuesClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IssuesClient.d.ts","sourceRoot":"","sources":["../../src/issues/IssuesClient.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAY;CAAG"}
@@ -0,0 +1,2 @@
1
+ export class IssuesClient {
2
+ }
@@ -0,0 +1,10 @@
1
+ import { QaProfile } from '@mcoda/shared/qa/QaProfile.js';
2
+ import { QaAdapter } from './QaAdapter.js';
3
+ import { QaContext, QaEnsureResult, QaRunResult } from './QaTypes.js';
4
+ export declare class ChromiumQaAdapter implements QaAdapter {
5
+ private resolveCwd;
6
+ ensureInstalled(profile: QaProfile, ctx: QaContext): Promise<QaEnsureResult>;
7
+ private persistLogs;
8
+ invoke(profile: QaProfile, ctx: QaContext): Promise<QaRunResult>;
9
+ }
10
+ //# sourceMappingURL=ChromiumQaAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChromiumQaAdapter.d.ts","sourceRoot":"","sources":["../../src/qa/ChromiumQaAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOtE,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,UAAU;IASZ,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC;YAiBpE,WAAW;IAYnB,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAqCvE"}
@@ -0,0 +1,85 @@
1
+ import path from 'node:path';
2
+ import { exec as execCb } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ const exec = promisify(execCb);
6
+ const shouldSkipInstall = (ctx) => process.env.MCODA_QA_SKIP_INSTALL === '1' || ctx.env?.MCODA_QA_SKIP_INSTALL === '1';
7
+ export class ChromiumQaAdapter {
8
+ resolveCwd(profile, ctx) {
9
+ if (profile.working_dir) {
10
+ return path.isAbsolute(profile.working_dir)
11
+ ? profile.working_dir
12
+ : path.join(ctx.workspaceRoot, profile.working_dir);
13
+ }
14
+ return ctx.workspaceRoot;
15
+ }
16
+ async ensureInstalled(profile, ctx) {
17
+ if (shouldSkipInstall(ctx))
18
+ return { ok: true, details: { skipped: true } };
19
+ const cwd = this.resolveCwd(profile, ctx);
20
+ try {
21
+ await exec('npx playwright --version', { cwd, env: { ...process.env, ...profile.env, ...ctx.env } });
22
+ return { ok: true };
23
+ }
24
+ catch (versionError) {
25
+ const installCommand = profile.install_command ?? 'npx playwright install chromium';
26
+ try {
27
+ await exec(installCommand, { cwd, env: { ...process.env, ...profile.env, ...ctx.env } });
28
+ return { ok: true, details: { installedVia: installCommand } };
29
+ }
30
+ catch (error) {
31
+ return { ok: false, message: error?.message ?? versionError?.message ?? 'Chromium QA install failed' };
32
+ }
33
+ }
34
+ }
35
+ async persistLogs(ctx, stdout, stderr) {
36
+ const artifacts = [];
37
+ if (!ctx.artifactDir)
38
+ return artifacts;
39
+ await fs.mkdir(ctx.artifactDir, { recursive: true });
40
+ const outPath = path.join(ctx.artifactDir, 'stdout.log');
41
+ const errPath = path.join(ctx.artifactDir, 'stderr.log');
42
+ await fs.writeFile(outPath, stdout ?? '', 'utf8');
43
+ await fs.writeFile(errPath, stderr ?? '', 'utf8');
44
+ artifacts.push(path.relative(ctx.workspaceRoot, outPath), path.relative(ctx.workspaceRoot, errPath));
45
+ return artifacts;
46
+ }
47
+ async invoke(profile, ctx) {
48
+ const command = ctx.testCommandOverride ?? profile.test_command ?? 'npx playwright test --reporter=list';
49
+ const startedAt = new Date().toISOString();
50
+ const cwd = this.resolveCwd(profile, ctx);
51
+ try {
52
+ const { stdout, stderr } = await exec(command, {
53
+ cwd,
54
+ env: { ...process.env, ...profile.env, ...ctx.env },
55
+ });
56
+ const finishedAt = new Date().toISOString();
57
+ const artifacts = await this.persistLogs(ctx, stdout, stderr);
58
+ return {
59
+ outcome: 'pass',
60
+ exitCode: 0,
61
+ stdout,
62
+ stderr,
63
+ artifacts,
64
+ startedAt,
65
+ finishedAt,
66
+ };
67
+ }
68
+ catch (error) {
69
+ const stdout = error?.stdout ?? '';
70
+ const stderr = error?.stderr ?? String(error);
71
+ const exitCode = typeof error?.code === 'number' ? error.code : null;
72
+ const finishedAt = new Date().toISOString();
73
+ const artifacts = await this.persistLogs(ctx, stdout, stderr);
74
+ return {
75
+ outcome: exitCode === null ? 'infra_issue' : exitCode === 0 ? 'pass' : 'fail',
76
+ exitCode,
77
+ stdout,
78
+ stderr,
79
+ artifacts,
80
+ startedAt,
81
+ finishedAt,
82
+ };
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,10 @@
1
+ import { QaProfile } from '@mcoda/shared/qa/QaProfile.js';
2
+ import { QaAdapter } from './QaAdapter.js';
3
+ import { QaContext, QaEnsureResult, QaRunResult } from './QaTypes.js';
4
+ export declare class CliQaAdapter implements QaAdapter {
5
+ private resolveCwd;
6
+ ensureInstalled(profile: QaProfile, ctx: QaContext): Promise<QaEnsureResult>;
7
+ private persistLogs;
8
+ invoke(profile: QaProfile, ctx: QaContext): Promise<QaRunResult>;
9
+ }
10
+ //# sourceMappingURL=CliQaAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CliQaAdapter.d.ts","sourceRoot":"","sources":["../../src/qa/CliQaAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKtE,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,CAAC,UAAU;IASZ,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC;YAapE,WAAW;IAYnB,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAiDvE"}
@@ -0,0 +1,91 @@
1
+ import path from 'node:path';
2
+ import { exec as execCb } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ const exec = promisify(execCb);
6
+ export class CliQaAdapter {
7
+ resolveCwd(profile, ctx) {
8
+ if (profile.working_dir) {
9
+ return path.isAbsolute(profile.working_dir)
10
+ ? profile.working_dir
11
+ : path.join(ctx.workspaceRoot, profile.working_dir);
12
+ }
13
+ return ctx.workspaceRoot;
14
+ }
15
+ async ensureInstalled(profile, ctx) {
16
+ if (!profile.install_command)
17
+ return { ok: true };
18
+ try {
19
+ await exec(profile.install_command, {
20
+ cwd: this.resolveCwd(profile, ctx),
21
+ env: { ...process.env, ...profile.env, ...ctx.env },
22
+ });
23
+ return { ok: true };
24
+ }
25
+ catch (error) {
26
+ return { ok: false, message: error?.message ?? 'QA install failed' };
27
+ }
28
+ }
29
+ async persistLogs(ctx, stdout, stderr) {
30
+ const artifacts = [];
31
+ if (!ctx.artifactDir)
32
+ return artifacts;
33
+ await fs.mkdir(ctx.artifactDir, { recursive: true });
34
+ const outPath = path.join(ctx.artifactDir, 'stdout.log');
35
+ const errPath = path.join(ctx.artifactDir, 'stderr.log');
36
+ await fs.writeFile(outPath, stdout ?? '', 'utf8');
37
+ await fs.writeFile(errPath, stderr ?? '', 'utf8');
38
+ artifacts.push(path.relative(ctx.workspaceRoot, outPath), path.relative(ctx.workspaceRoot, errPath));
39
+ return artifacts;
40
+ }
41
+ async invoke(profile, ctx) {
42
+ const command = ctx.testCommandOverride ?? profile.test_command;
43
+ const startedAt = new Date().toISOString();
44
+ if (!command) {
45
+ const finishedAt = new Date().toISOString();
46
+ return {
47
+ outcome: 'infra_issue',
48
+ exitCode: null,
49
+ stdout: '',
50
+ stderr: 'No test_command configured for QA profile',
51
+ artifacts: [],
52
+ startedAt,
53
+ finishedAt,
54
+ };
55
+ }
56
+ const cwd = this.resolveCwd(profile, ctx);
57
+ try {
58
+ const { stdout, stderr } = await exec(command, {
59
+ cwd,
60
+ env: { ...process.env, ...profile.env, ...ctx.env },
61
+ });
62
+ const finishedAt = new Date().toISOString();
63
+ const artifacts = await this.persistLogs(ctx, stdout, stderr);
64
+ return {
65
+ outcome: 'pass',
66
+ exitCode: 0,
67
+ stdout,
68
+ stderr,
69
+ artifacts,
70
+ startedAt,
71
+ finishedAt,
72
+ };
73
+ }
74
+ catch (error) {
75
+ const stdout = error?.stdout ?? '';
76
+ const stderr = error?.stderr ?? String(error);
77
+ const exitCode = typeof error?.code === 'number' ? error.code : null;
78
+ const finishedAt = new Date().toISOString();
79
+ const artifacts = await this.persistLogs(ctx, stdout, stderr);
80
+ return {
81
+ outcome: exitCode === null ? 'infra_issue' : exitCode === 0 ? 'pass' : 'fail',
82
+ exitCode,
83
+ stdout,
84
+ stderr,
85
+ artifacts,
86
+ startedAt,
87
+ finishedAt,
88
+ };
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,10 @@
1
+ import { QaProfile } from '@mcoda/shared/qa/QaProfile.js';
2
+ import { QaAdapter } from './QaAdapter.js';
3
+ import { QaContext, QaEnsureResult, QaRunResult } from './QaTypes.js';
4
+ export declare class MaestroQaAdapter implements QaAdapter {
5
+ private resolveCwd;
6
+ ensureInstalled(profile: QaProfile, ctx: QaContext): Promise<QaEnsureResult>;
7
+ private persistLogs;
8
+ invoke(profile: QaProfile, ctx: QaContext): Promise<QaRunResult>;
9
+ }
10
+ //# sourceMappingURL=MaestroQaAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MaestroQaAdapter.d.ts","sourceRoot":"","sources":["../../src/qa/MaestroQaAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOtE,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,UAAU;IASZ,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC;YAmBpE,WAAW;IAYnB,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAqCvE"}
@@ -0,0 +1,87 @@
1
+ import path from 'node:path';
2
+ import { exec as execCb } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ const exec = promisify(execCb);
6
+ const shouldSkipInstall = (ctx) => process.env.MCODA_QA_SKIP_INSTALL === '1' || ctx.env?.MCODA_QA_SKIP_INSTALL === '1';
7
+ export class MaestroQaAdapter {
8
+ resolveCwd(profile, ctx) {
9
+ if (profile.working_dir) {
10
+ return path.isAbsolute(profile.working_dir)
11
+ ? profile.working_dir
12
+ : path.join(ctx.workspaceRoot, profile.working_dir);
13
+ }
14
+ return ctx.workspaceRoot;
15
+ }
16
+ async ensureInstalled(profile, ctx) {
17
+ if (shouldSkipInstall(ctx))
18
+ return { ok: true, details: { skipped: true } };
19
+ const cwd = this.resolveCwd(profile, ctx);
20
+ try {
21
+ await exec('maestro --version', { cwd, env: { ...process.env, ...profile.env, ...ctx.env } });
22
+ return { ok: true };
23
+ }
24
+ catch (versionError) {
25
+ if (!profile.install_command) {
26
+ return { ok: false, message: versionError?.message ?? 'Maestro not available' };
27
+ }
28
+ try {
29
+ await exec(profile.install_command, { cwd, env: { ...process.env, ...profile.env, ...ctx.env } });
30
+ return { ok: true, details: { installedVia: profile.install_command } };
31
+ }
32
+ catch (error) {
33
+ return { ok: false, message: error?.message ?? versionError?.message ?? 'Maestro install failed' };
34
+ }
35
+ }
36
+ }
37
+ async persistLogs(ctx, stdout, stderr) {
38
+ const artifacts = [];
39
+ if (!ctx.artifactDir)
40
+ return artifacts;
41
+ await fs.mkdir(ctx.artifactDir, { recursive: true });
42
+ const outPath = path.join(ctx.artifactDir, 'stdout.log');
43
+ const errPath = path.join(ctx.artifactDir, 'stderr.log');
44
+ await fs.writeFile(outPath, stdout ?? '', 'utf8');
45
+ await fs.writeFile(errPath, stderr ?? '', 'utf8');
46
+ artifacts.push(path.relative(ctx.workspaceRoot, outPath), path.relative(ctx.workspaceRoot, errPath));
47
+ return artifacts;
48
+ }
49
+ async invoke(profile, ctx) {
50
+ const command = ctx.testCommandOverride ?? profile.test_command ?? 'maestro test';
51
+ const startedAt = new Date().toISOString();
52
+ const cwd = this.resolveCwd(profile, ctx);
53
+ try {
54
+ const { stdout, stderr } = await exec(command, {
55
+ cwd,
56
+ env: { ...process.env, ...profile.env, ...ctx.env },
57
+ });
58
+ const finishedAt = new Date().toISOString();
59
+ const artifacts = await this.persistLogs(ctx, stdout, stderr);
60
+ return {
61
+ outcome: 'pass',
62
+ exitCode: 0,
63
+ stdout,
64
+ stderr,
65
+ artifacts,
66
+ startedAt,
67
+ finishedAt,
68
+ };
69
+ }
70
+ catch (error) {
71
+ const stdout = error?.stdout ?? '';
72
+ const stderr = error?.stderr ?? String(error);
73
+ const exitCode = typeof error?.code === 'number' ? error.code : null;
74
+ const finishedAt = new Date().toISOString();
75
+ const artifacts = await this.persistLogs(ctx, stdout, stderr);
76
+ return {
77
+ outcome: exitCode === null ? 'infra_issue' : exitCode === 0 ? 'pass' : 'fail',
78
+ exitCode,
79
+ stdout,
80
+ stderr,
81
+ artifacts,
82
+ startedAt,
83
+ finishedAt,
84
+ };
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,7 @@
1
+ import { QaContext, QaEnsureResult, QaRunResult } from './QaTypes.js';
2
+ import { QaProfile } from '@mcoda/shared/qa/QaProfile.js';
3
+ export interface QaAdapter {
4
+ ensureInstalled(profile: QaProfile, ctx: QaContext): Promise<QaEnsureResult>;
5
+ invoke(profile: QaProfile, ctx: QaContext): Promise<QaRunResult>;
6
+ }
7
+ //# sourceMappingURL=QaAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QaAdapter.d.ts","sourceRoot":"","sources":["../../src/qa/QaAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE1D,MAAM,WAAW,SAAS;IACxB,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7E,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAClE"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare class QaClient {
2
+ }
3
+ //# sourceMappingURL=QaClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QaClient.d.ts","sourceRoot":"","sources":["../../src/qa/QaClient.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAQ;CAAG"}
@@ -0,0 +1,2 @@
1
+ export class QaClient {
2
+ }
@@ -0,0 +1,24 @@
1
+ export type QaOutcome = 'pass' | 'fail' | 'infra_issue';
2
+ export interface QaRunResult {
3
+ outcome: QaOutcome;
4
+ exitCode: number | null;
5
+ stdout: string;
6
+ stderr: string;
7
+ artifacts: string[];
8
+ startedAt: string;
9
+ finishedAt: string;
10
+ }
11
+ export interface QaEnsureResult {
12
+ ok: boolean;
13
+ message?: string;
14
+ details?: Record<string, unknown>;
15
+ }
16
+ export interface QaContext {
17
+ workspaceRoot: string;
18
+ jobId: string;
19
+ taskKey: string;
20
+ env: NodeJS.ProcessEnv;
21
+ testCommandOverride?: string;
22
+ artifactDir?: string;
23
+ }
24
+ //# sourceMappingURL=QaTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QaTypes.d.ts","sourceRoot":"","sources":["../../src/qa/QaTypes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC;AAExD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export * from './QaTypes.js';
2
+ export * from './QaAdapter.js';
3
+ export * from './CliQaAdapter.js';
4
+ export * from './ChromiumQaAdapter.js';
5
+ export * from './MaestroQaAdapter.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/qa/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './QaTypes.js';
2
+ export * from './QaAdapter.js';
3
+ export * from './CliQaAdapter.js';
4
+ export * from './ChromiumQaAdapter.js';
5
+ export * from './MaestroQaAdapter.js';
@@ -0,0 +1,12 @@
1
+ import { ApplyUpdateResponse, UpdateChannel, UpdateInfo } from "@mcoda/shared";
2
+ export declare class SystemClient {
3
+ private baseUrl?;
4
+ constructor(baseUrl?: string | undefined);
5
+ private ensureBaseUrl;
6
+ private fetchJson;
7
+ checkUpdate(channel?: UpdateChannel): Promise<UpdateInfo>;
8
+ applyUpdate(body?: {
9
+ channel?: UpdateChannel;
10
+ }): Promise<ApplyUpdateResponse>;
11
+ }
12
+ //# sourceMappingURL=SystemClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SystemClient.d.ts","sourceRoot":"","sources":["../../src/system/SystemClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAQ/E,qBAAa,YAAY;IACX,OAAO,CAAC,OAAO,CAAC;gBAAR,OAAO,CAAC,EAAE,MAAM,YAAA;IAEpC,OAAO,CAAC,aAAa;YAOP,SAAS;IAYjB,WAAW,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC;IAUzD,WAAW,CAAC,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;CASpF"}
@@ -0,0 +1,47 @@
1
+ const toQueryChannel = (channel) => {
2
+ if (!channel)
3
+ return undefined;
4
+ if (channel === "nightly")
5
+ return "nightly";
6
+ return channel;
7
+ };
8
+ export class SystemClient {
9
+ constructor(baseUrl) {
10
+ this.baseUrl = baseUrl;
11
+ }
12
+ ensureBaseUrl() {
13
+ if (!this.baseUrl) {
14
+ throw new Error("System update API is not configured (set MCODA_API_BASE_URL or MCODA_SYSTEM_API_URL).");
15
+ }
16
+ return this.baseUrl;
17
+ }
18
+ async fetchJson(input, init) {
19
+ const resp = await fetch(input, {
20
+ ...init,
21
+ headers: { accept: "application/json", ...init?.headers },
22
+ });
23
+ if (!resp.ok) {
24
+ const detail = await resp.text().catch(() => "");
25
+ throw new Error(`System update request failed (${resp.status}): ${detail || resp.statusText}`);
26
+ }
27
+ return (await resp.json());
28
+ }
29
+ async checkUpdate(channel) {
30
+ const base = this.ensureBaseUrl();
31
+ const url = new URL("/system/update", base);
32
+ const queryChannel = toQueryChannel(channel);
33
+ if (queryChannel) {
34
+ url.searchParams.set("channel", queryChannel);
35
+ }
36
+ return this.fetchJson(url);
37
+ }
38
+ async applyUpdate(body) {
39
+ const base = this.ensureBaseUrl();
40
+ const url = new URL("/system/update", base);
41
+ return this.fetchJson(url, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json" },
44
+ body: body && body.channel ? JSON.stringify({ channel: toQueryChannel(body.channel) }) : undefined,
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,79 @@
1
+ export interface TokenUsageSummaryRow {
2
+ workspace_id: string;
3
+ project_id?: string | null;
4
+ agent_id?: string | null;
5
+ model_name?: string | null;
6
+ command_name?: string | null;
7
+ action?: string | null;
8
+ job_id?: string | null;
9
+ day?: string | null;
10
+ calls: number;
11
+ tokens_prompt: number;
12
+ tokens_completion: number;
13
+ tokens_total: number;
14
+ cost_estimate: number | null;
15
+ }
16
+ export interface TokenUsageRow {
17
+ workspace_id: string;
18
+ agent_id: string | null;
19
+ model_name: string | null;
20
+ job_id: string | null;
21
+ command_run_id: string | null;
22
+ task_run_id: string | null;
23
+ task_id: string | null;
24
+ project_id: string | null;
25
+ epic_id: string | null;
26
+ user_story_id: string | null;
27
+ tokens_prompt: number | null;
28
+ tokens_completion: number | null;
29
+ tokens_total: number | null;
30
+ cost_estimate: number | null;
31
+ duration_seconds: number | null;
32
+ timestamp: string;
33
+ command_name?: string | null;
34
+ action?: string | null;
35
+ error_kind?: string | null;
36
+ metadata?: Record<string, unknown>;
37
+ }
38
+ export interface TelemetryConfig {
39
+ localRecording: boolean;
40
+ remoteExport: boolean;
41
+ optOut: boolean;
42
+ strict: boolean;
43
+ [key: string]: unknown;
44
+ }
45
+ export declare class TelemetryClient {
46
+ private options;
47
+ constructor(options: {
48
+ baseUrl: string;
49
+ authToken?: string;
50
+ });
51
+ private request;
52
+ getSummary(params: {
53
+ workspaceId: string;
54
+ projectId?: string;
55
+ agentId?: string;
56
+ commandName?: string;
57
+ jobId?: string;
58
+ from?: string;
59
+ to?: string;
60
+ groupBy?: string[];
61
+ }): Promise<TokenUsageSummaryRow[]>;
62
+ getTokenUsage(params: {
63
+ workspaceId: string;
64
+ projectId?: string;
65
+ agentId?: string;
66
+ commandName?: string;
67
+ jobId?: string;
68
+ taskId?: string;
69
+ from?: string;
70
+ to?: string;
71
+ page?: number;
72
+ pageSize?: number;
73
+ sort?: string;
74
+ }): Promise<TokenUsageRow[]>;
75
+ getConfig(workspaceId: string): Promise<TelemetryConfig>;
76
+ optOut(workspaceId: string, strict?: boolean): Promise<TelemetryConfig>;
77
+ optIn(workspaceId: string): Promise<TelemetryConfig>;
78
+ }
79
+ //# sourceMappingURL=TelemetryClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TelemetryClient.d.ts","sourceRoot":"","sources":["../../src/telemetry/TelemetryClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ export class TelemetryClient {
2
+ constructor(options) {
3
+ this.options = options;
4
+ }
5
+ async request(pathname, init) {
6
+ const url = new URL(pathname, this.options.baseUrl);
7
+ const headers = { "Content-Type": "application/json" };
8
+ if (this.options.authToken)
9
+ headers.authorization = `Bearer ${this.options.authToken}`;
10
+ const response = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
11
+ if (!response.ok) {
12
+ throw new Error(`Telemetry request failed (${response.status}): ${await response.text()}`);
13
+ }
14
+ if (response.status === 204)
15
+ return undefined;
16
+ return (await response.json());
17
+ }
18
+ async getSummary(params) {
19
+ const search = new URLSearchParams();
20
+ search.set("workspace_id", params.workspaceId);
21
+ if (params.projectId)
22
+ search.set("project_id", params.projectId);
23
+ if (params.agentId)
24
+ search.set("agent_id", params.agentId);
25
+ if (params.commandName)
26
+ search.set("command_name", params.commandName);
27
+ if (params.jobId)
28
+ search.set("job_id", params.jobId);
29
+ if (params.from)
30
+ search.set("from", params.from);
31
+ if (params.to)
32
+ search.set("to", params.to);
33
+ if (params.groupBy && params.groupBy.length > 0) {
34
+ search.set("group_by", params.groupBy.join(","));
35
+ }
36
+ return this.request(`/telemetry/summary?${search.toString()}`);
37
+ }
38
+ async getTokenUsage(params) {
39
+ const search = new URLSearchParams();
40
+ search.set("workspace_id", params.workspaceId);
41
+ if (params.projectId)
42
+ search.set("project_id", params.projectId);
43
+ if (params.agentId)
44
+ search.set("agent_id", params.agentId);
45
+ if (params.commandName)
46
+ search.set("command_name", params.commandName);
47
+ if (params.jobId)
48
+ search.set("job_id", params.jobId);
49
+ if (params.taskId)
50
+ search.set("task_id", params.taskId);
51
+ if (params.from)
52
+ search.set("from", params.from);
53
+ if (params.to)
54
+ search.set("to", params.to);
55
+ if (params.page !== undefined)
56
+ search.set("page", String(params.page));
57
+ if (params.pageSize !== undefined)
58
+ search.set("page_size", String(params.pageSize));
59
+ if (params.sort)
60
+ search.set("sort", params.sort);
61
+ return this.request(`/telemetry/token-usage?${search.toString()}`);
62
+ }
63
+ async getConfig(workspaceId) {
64
+ const search = new URLSearchParams();
65
+ search.set("workspace_id", workspaceId);
66
+ return this.request(`/telemetry/config?${search.toString()}`);
67
+ }
68
+ async optOut(workspaceId, strict) {
69
+ return this.request(`/telemetry/opt-out`, {
70
+ method: "POST",
71
+ body: JSON.stringify({ workspace_id: workspaceId, strict: strict ?? false }),
72
+ });
73
+ }
74
+ async optIn(workspaceId) {
75
+ return this.request(`/telemetry/opt-in`, {
76
+ method: "POST",
77
+ body: JSON.stringify({ workspace_id: workspaceId }),
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,29 @@
1
+ export declare class VcsClient {
2
+ private runGit;
3
+ private gitDirExists;
4
+ ensureRepo(cwd: string): Promise<void>;
5
+ hasRemote(cwd: string): Promise<boolean>;
6
+ currentBranch(cwd: string): Promise<string | null>;
7
+ branchExists(cwd: string, branch: string): Promise<boolean>;
8
+ private hasCommits;
9
+ private ensureMainBranch;
10
+ ensureBaseBranch(cwd: string, base: string): Promise<void>;
11
+ checkoutBranch(cwd: string, branch: string): Promise<void>;
12
+ createOrCheckoutBranch(cwd: string, branch: string, base: string): Promise<void>;
13
+ applyPatch(cwd: string, patch: string): Promise<void>;
14
+ stage(cwd: string, paths: string[]): Promise<void>;
15
+ commit(cwd: string, message: string, options?: {
16
+ noVerify?: boolean;
17
+ noGpgSign?: boolean;
18
+ }): Promise<void>;
19
+ merge(cwd: string, source: string, target: string, ensureClean?: boolean): Promise<void>;
20
+ push(cwd: string, remote: string, branch: string): Promise<void>;
21
+ pull(cwd: string, remote: string, branch: string, ffOnly?: boolean): Promise<void>;
22
+ status(cwd: string): Promise<string>;
23
+ dirtyPaths(cwd: string): Promise<string[]>;
24
+ conflictPaths(cwd: string): Promise<string[]>;
25
+ ensureClean(cwd: string, ignoreDotMcoda?: boolean): Promise<void>;
26
+ lastCommitSha(cwd: string): Promise<string>;
27
+ diff(cwd: string, base: string, head: string, paths?: string[]): Promise<string>;
28
+ }
29
+ //# sourceMappingURL=VcsClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VcsClient.d.ts","sourceRoot":"","sources":["../../src/vcs/VcsClient.ts"],"names":[],"mappings":"AAQA,qBAAa,SAAS;YACN,MAAM;YAKN,YAAY;IASpB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASxC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASlD,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YASnD,UAAU;YASV,gBAAgB;IAaxB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhF,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBrD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,MAAM,CACV,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACxD,OAAO,CAAC,IAAI,CAAC;IAOV,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtF,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKpC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsB1C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS7C,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9D,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK3C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAQvF"}
@@ -0,0 +1,201 @@
1
+ import { exec as execCb, execFile as execFileCb } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ const exec = promisify(execCb);
6
+ const execFile = promisify(execFileCb);
7
+ export class VcsClient {
8
+ async runGit(cwd, args) {
9
+ const { stdout, stderr } = await execFile("git", args, { cwd });
10
+ return { stdout, stderr };
11
+ }
12
+ async gitDirExists(cwd) {
13
+ try {
14
+ await fs.access(path.join(cwd, ".git"));
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ async ensureRepo(cwd) {
22
+ if (await this.gitDirExists(cwd))
23
+ return;
24
+ await this.runGit(cwd, ["init"]);
25
+ }
26
+ async hasRemote(cwd) {
27
+ try {
28
+ const { stdout } = await this.runGit(cwd, ["remote"]);
29
+ return stdout.trim().length > 0;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ async currentBranch(cwd) {
36
+ try {
37
+ const { stdout } = await this.runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
38
+ return stdout.trim();
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ async branchExists(cwd, branch) {
45
+ try {
46
+ await this.runGit(cwd, ["show-ref", "--verify", `refs/heads/${branch}`]);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async hasCommits(cwd) {
54
+ try {
55
+ await this.runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ async ensureMainBranch(cwd) {
63
+ await this.ensureRepo(cwd);
64
+ if (await this.branchExists(cwd, "main"))
65
+ return;
66
+ const hasHistory = await this.hasCommits(cwd);
67
+ if (!hasHistory) {
68
+ await this.runGit(cwd, ["checkout", "--orphan", "main"]);
69
+ await this.runGit(cwd, ["commit", "--allow-empty", "-m", "chore:init-repo"]);
70
+ return;
71
+ }
72
+ const current = (await this.currentBranch(cwd)) ?? "HEAD";
73
+ await this.runGit(cwd, ["checkout", "-b", "main", current]);
74
+ }
75
+ async ensureBaseBranch(cwd, base) {
76
+ await this.ensureMainBranch(cwd);
77
+ if (await this.branchExists(cwd, base))
78
+ return;
79
+ const hasHistory = await this.hasCommits(cwd);
80
+ const baseFrom = hasHistory
81
+ ? (await this.branchExists(cwd, "main")) ? "main" : (await this.currentBranch(cwd)) ?? "HEAD"
82
+ : undefined;
83
+ const args = ["checkout", "-b", base];
84
+ if (baseFrom)
85
+ args.push(baseFrom);
86
+ await this.runGit(cwd, args);
87
+ }
88
+ async checkoutBranch(cwd, branch) {
89
+ await this.runGit(cwd, ["checkout", branch]);
90
+ }
91
+ async createOrCheckoutBranch(cwd, branch, base) {
92
+ if (await this.branchExists(cwd, branch)) {
93
+ await this.checkoutBranch(cwd, branch);
94
+ return;
95
+ }
96
+ await this.runGit(cwd, ["checkout", "-b", branch, base]);
97
+ }
98
+ async applyPatch(cwd, patch) {
99
+ const opts = { cwd, shell: true };
100
+ const applyCmd = `cat <<'__PATCH__' | git apply --whitespace=nowarn\n${patch}\n__PATCH__`;
101
+ try {
102
+ await exec(applyCmd, opts);
103
+ return;
104
+ }
105
+ catch (error) {
106
+ // If the patch is already applied, a reverse --check succeeds; treat that as a no-op.
107
+ const reverseCheckCmd = `cat <<'__PATCH__' | git apply --reverse --check --whitespace=nowarn\n${patch}\n__PATCH__`;
108
+ try {
109
+ await exec(reverseCheckCmd, opts);
110
+ return;
111
+ }
112
+ catch {
113
+ throw error;
114
+ }
115
+ }
116
+ }
117
+ async stage(cwd, paths) {
118
+ await this.runGit(cwd, ["add", ...paths]);
119
+ }
120
+ async commit(cwd, message, options = {}) {
121
+ const args = ["commit", "-m", message];
122
+ if (options.noVerify)
123
+ args.push("--no-verify");
124
+ if (options.noGpgSign)
125
+ args.push("--no-gpg-sign");
126
+ await this.runGit(cwd, args);
127
+ }
128
+ async merge(cwd, source, target, ensureClean = false) {
129
+ await this.checkoutBranch(cwd, target);
130
+ if (ensureClean) {
131
+ await this.ensureClean(cwd);
132
+ }
133
+ await this.runGit(cwd, ["merge", "--no-edit", source]);
134
+ }
135
+ async push(cwd, remote, branch) {
136
+ await this.runGit(cwd, ["push", remote, branch]);
137
+ }
138
+ async pull(cwd, remote, branch, ffOnly = true) {
139
+ const args = ["pull"];
140
+ if (ffOnly)
141
+ args.push("--ff-only");
142
+ args.push(remote, branch);
143
+ await this.runGit(cwd, args);
144
+ }
145
+ async status(cwd) {
146
+ const { stdout } = await this.runGit(cwd, ["status", "--porcelain"]);
147
+ return stdout;
148
+ }
149
+ async dirtyPaths(cwd) {
150
+ // Use NUL-terminated porcelain to avoid quoting/escaping issues and to handle renames reliably.
151
+ // Format: `XY <path>\0` or for renames/copies: `XY <old>\0<new>\0`.
152
+ const { stdout } = await this.runGit(cwd, ["status", "--porcelain", "-z"]);
153
+ const entries = stdout.split("\0").filter(Boolean);
154
+ const paths = [];
155
+ for (let i = 0; i < entries.length; i += 1) {
156
+ const entry = entries[i];
157
+ if (entry.length < 4)
158
+ continue;
159
+ const x = entry[0];
160
+ const path1 = entry.slice(3);
161
+ if ((x === "R" || x === "C") && i + 1 < entries.length) {
162
+ const path2 = entries[i + 1];
163
+ if (path2)
164
+ paths.push(path2);
165
+ i += 1;
166
+ continue;
167
+ }
168
+ if (path1)
169
+ paths.push(path1);
170
+ }
171
+ return paths;
172
+ }
173
+ async conflictPaths(cwd) {
174
+ try {
175
+ const { stdout } = await this.runGit(cwd, ["diff", "--name-only", "--diff-filter=U"]);
176
+ return stdout.split(/\r?\n/).filter(Boolean);
177
+ }
178
+ catch {
179
+ return [];
180
+ }
181
+ }
182
+ async ensureClean(cwd, ignoreDotMcoda = true) {
183
+ const dirty = await this.dirtyPaths(cwd);
184
+ const filtered = ignoreDotMcoda ? dirty.filter((p) => !p.startsWith(".mcoda")) : dirty;
185
+ if (filtered.length) {
186
+ throw new Error(`Working tree dirty: ${filtered.join(", ")}`);
187
+ }
188
+ }
189
+ async lastCommitSha(cwd) {
190
+ const { stdout } = await this.runGit(cwd, ["rev-parse", "HEAD"]);
191
+ return stdout.trim();
192
+ }
193
+ async diff(cwd, base, head, paths) {
194
+ const args = ["diff", `${base}...${head}`];
195
+ if (paths && paths.length) {
196
+ args.push("--", ...paths);
197
+ }
198
+ const { stdout } = await this.runGit(cwd, args);
199
+ return stdout;
200
+ }
201
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@mcoda/integrations",
3
+ "version": "0.1.4",
4
+ "description": "External integrations for mcoda (vcs, QA, telemetry).",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./qa/*": "./dist/qa/*",
11
+ "./docdex/*": "./dist/docdex/*",
12
+ "./vcs/*": "./dist/vcs/*",
13
+ "./telemetry/*": "./dist/telemetry/*"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "!dist/**/__tests__/**",
18
+ "!dist/**/*.test.*",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json",
25
+ "lint": "echo \"lint not configured\"",
26
+ "test": "pnpm run build && node ../../scripts/run-node-tests.js dist"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/bekirdag/mcoda.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/bekirdag/mcoda/issues"
37
+ },
38
+ "homepage": "https://github.com/bekirdag/mcoda#readme",
39
+ "license": "MIT",
40
+ "author": "bekir dag",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@mcoda/shared": "workspace:*"
46
+ }
47
+ }