@mainahq/core 0.2.0 → 0.4.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 +4 -1
- package/src/ai/__tests__/delegation.test.ts +105 -0
- package/src/ai/delegation.ts +111 -0
- package/src/ai/try-generate.ts +17 -0
- package/src/cloud/__tests__/auth.test.ts +164 -0
- package/src/cloud/__tests__/client.test.ts +253 -0
- package/src/cloud/auth.ts +232 -0
- package/src/cloud/client.ts +190 -0
- package/src/cloud/types.ts +106 -0
- package/src/context/relevance.ts +5 -0
- package/src/context/retrieval.ts +3 -0
- package/src/context/semantic.ts +3 -0
- package/src/feedback/__tests__/trace-analysis.test.ts +98 -0
- package/src/feedback/trace-analysis.ts +153 -0
- package/src/index.ts +55 -0
- package/src/init/__tests__/init.test.ts +51 -0
- package/src/init/index.ts +43 -0
- package/src/language/__tests__/detect.test.ts +61 -1
- package/src/language/__tests__/profile.test.ts +68 -1
- package/src/language/detect.ts +33 -3
- package/src/language/profile.ts +67 -2
- package/src/ticket/index.ts +5 -0
- package/src/verify/__tests__/consistency.test.ts +98 -0
- package/src/verify/__tests__/lighthouse.test.ts +215 -0
- package/src/verify/__tests__/linters/checkstyle.test.ts +23 -0
- package/src/verify/__tests__/linters/dotnet-format.test.ts +18 -0
- package/src/verify/__tests__/pipeline.test.ts +21 -2
- package/src/verify/__tests__/typecheck.test.ts +160 -0
- package/src/verify/__tests__/zap.test.ts +188 -0
- package/src/verify/consistency.ts +199 -0
- package/src/verify/detect.ts +13 -1
- package/src/verify/lighthouse.ts +173 -0
- package/src/verify/linters/checkstyle.ts +41 -0
- package/src/verify/linters/dotnet-format.ts +37 -0
- package/src/verify/pipeline.ts +20 -2
- package/src/verify/syntax-guard.ts +8 -0
- package/src/verify/typecheck.ts +178 -0
- package/src/verify/zap.ts +189 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth device-flow authentication.
|
|
3
|
+
*
|
|
4
|
+
* Implements the device authorization grant (RFC 8628) for CLI login.
|
|
5
|
+
* Tokens are stored at ~/.maina/auth.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import type { Result } from "../db/index";
|
|
18
|
+
import type { DeviceCodeResponse, TokenResponse } from "./types";
|
|
19
|
+
|
|
20
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function ok<T>(value: T): Result<T, string> {
|
|
23
|
+
return { ok: true, value };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function err(error: string): Result<never, string> {
|
|
27
|
+
return { ok: false, error };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sleep(ms: number): Promise<void> {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Auth Config Path ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface AuthConfig {
|
|
37
|
+
/** Bearer access token. */
|
|
38
|
+
accessToken: string;
|
|
39
|
+
/** Refresh token (if available). */
|
|
40
|
+
refreshToken?: string;
|
|
41
|
+
/** ISO-8601 timestamp when the token expires. */
|
|
42
|
+
expiresAt?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return the path to the auth config file.
|
|
47
|
+
* Uses `configDir` override for testing; defaults to `~/.maina/auth.json`.
|
|
48
|
+
*/
|
|
49
|
+
export function getAuthConfigPath(configDir?: string): string {
|
|
50
|
+
const dir = configDir ?? join(homedir(), ".maina");
|
|
51
|
+
return join(dir, "auth.json");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Load / Save / Clear ─────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load saved auth config from disk.
|
|
58
|
+
* Returns err if not logged in or the file is malformed.
|
|
59
|
+
*/
|
|
60
|
+
export function loadAuthConfig(configDir?: string): Result<AuthConfig, string> {
|
|
61
|
+
const path = getAuthConfigPath(configDir);
|
|
62
|
+
if (!existsSync(path)) {
|
|
63
|
+
return err("Not logged in. Run `maina login` first.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(path, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw) as AuthConfig;
|
|
69
|
+
if (!parsed.accessToken) {
|
|
70
|
+
return err("Auth config is missing accessToken.");
|
|
71
|
+
}
|
|
72
|
+
return ok(parsed);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return err(
|
|
75
|
+
`Failed to read auth config: ${e instanceof Error ? e.message : String(e)}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Persist auth config to disk.
|
|
82
|
+
*/
|
|
83
|
+
export function saveAuthConfig(
|
|
84
|
+
config: AuthConfig,
|
|
85
|
+
configDir?: string,
|
|
86
|
+
): Result<void, string> {
|
|
87
|
+
const path = getAuthConfigPath(configDir);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
91
|
+
writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
|
|
92
|
+
return ok(undefined);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return err(
|
|
95
|
+
`Failed to save auth config: ${e instanceof Error ? e.message : String(e)}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove auth config from disk (logout).
|
|
102
|
+
*/
|
|
103
|
+
export function clearAuthConfig(configDir?: string): Result<void, string> {
|
|
104
|
+
const path = getAuthConfigPath(configDir);
|
|
105
|
+
if (!existsSync(path)) {
|
|
106
|
+
return ok(undefined);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
unlinkSync(path);
|
|
111
|
+
return ok(undefined);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return err(
|
|
114
|
+
`Failed to clear auth config: ${e instanceof Error ? e.message : String(e)}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Device Flow ─────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Initiate the device authorization flow.
|
|
123
|
+
*
|
|
124
|
+
* Calls `POST /auth/device` on the cloud API to obtain a user code and
|
|
125
|
+
* verification URI.
|
|
126
|
+
*/
|
|
127
|
+
export async function startDeviceFlow(
|
|
128
|
+
baseUrl: string,
|
|
129
|
+
): Promise<Result<DeviceCodeResponse, string>> {
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(`${baseUrl}/auth/device`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
Accept: "application/json",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const text = await response.text();
|
|
141
|
+
return err(
|
|
142
|
+
`Device flow initiation failed (HTTP ${response.status}): ${text}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const body = (await response.json()) as {
|
|
147
|
+
data?: DeviceCodeResponse;
|
|
148
|
+
error?: string;
|
|
149
|
+
};
|
|
150
|
+
if (body.error) {
|
|
151
|
+
return err(body.error);
|
|
152
|
+
}
|
|
153
|
+
if (!body.data) {
|
|
154
|
+
return err("Invalid response: missing data");
|
|
155
|
+
}
|
|
156
|
+
return ok(body.data);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return err(
|
|
159
|
+
`Device flow request failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Poll the cloud API until the user completes the device flow or the code
|
|
166
|
+
* expires.
|
|
167
|
+
*
|
|
168
|
+
* Respects the `interval` returned by the server. Returns the token
|
|
169
|
+
* response on success.
|
|
170
|
+
*/
|
|
171
|
+
export async function pollForToken(
|
|
172
|
+
baseUrl: string,
|
|
173
|
+
deviceCode: string,
|
|
174
|
+
interval: number,
|
|
175
|
+
expiresIn: number,
|
|
176
|
+
): Promise<Result<TokenResponse, string>> {
|
|
177
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
178
|
+
const pollInterval = Math.max(interval, 5) * 1000;
|
|
179
|
+
|
|
180
|
+
while (Date.now() < deadline) {
|
|
181
|
+
await sleep(pollInterval);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${baseUrl}/auth/token`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
Accept: "application/json",
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
deviceCode,
|
|
192
|
+
grantType: "urn:ietf:params:oauth:grant-type:device_code",
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (response.status === 428 || response.status === 400) {
|
|
197
|
+
// "authorization_pending" — the user hasn't completed login yet
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
const text = await response.text();
|
|
203
|
+
return err(`Token request failed (HTTP ${response.status}): ${text}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const body = (await response.json()) as {
|
|
207
|
+
data?: TokenResponse;
|
|
208
|
+
error?: string;
|
|
209
|
+
};
|
|
210
|
+
if (body.error) {
|
|
211
|
+
// "slow_down" → increase interval
|
|
212
|
+
if (body.error === "slow_down") {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
return err(body.error);
|
|
216
|
+
}
|
|
217
|
+
if (!body.data) {
|
|
218
|
+
return err("Invalid token response: missing data");
|
|
219
|
+
}
|
|
220
|
+
return ok(body.data);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Network errors during polling are transient — keep trying
|
|
223
|
+
if (Date.now() >= deadline) {
|
|
224
|
+
return err(
|
|
225
|
+
`Token polling failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return err("Device code expired. Please try logging in again.");
|
|
232
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Provides authenticated access to the maina cloud API for prompt sync,
|
|
5
|
+
* team management, and feedback reporting. All methods return Result<T, string>.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Result } from "../db/index";
|
|
9
|
+
import type {
|
|
10
|
+
ApiResponse,
|
|
11
|
+
CloudConfig,
|
|
12
|
+
CloudFeedbackPayload,
|
|
13
|
+
PromptRecord,
|
|
14
|
+
TeamInfo,
|
|
15
|
+
TeamMember,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function ok<T>(value: T): Result<T, string> {
|
|
21
|
+
return { ok: true, value };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function err(error: string): Result<never, string> {
|
|
25
|
+
return { ok: false, error };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
29
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
30
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
31
|
+
|
|
32
|
+
/** Status codes that trigger a retry. */
|
|
33
|
+
function isRetryable(status: number): boolean {
|
|
34
|
+
return status === 429 || status >= 500;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sleep for the given number of milliseconds.
|
|
39
|
+
* Extracted so tests can verify backoff behaviour.
|
|
40
|
+
*/
|
|
41
|
+
function sleep(ms: number): Promise<void> {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Cloud Client ────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface CloudClient {
|
|
48
|
+
/** Check API availability. */
|
|
49
|
+
health(): Promise<Result<{ status: string }, string>>;
|
|
50
|
+
|
|
51
|
+
/** Download team prompts. */
|
|
52
|
+
getPrompts(): Promise<Result<PromptRecord[], string>>;
|
|
53
|
+
|
|
54
|
+
/** Upload local prompts. */
|
|
55
|
+
putPrompts(prompts: PromptRecord[]): Promise<Result<void, string>>;
|
|
56
|
+
|
|
57
|
+
/** Fetch team information. */
|
|
58
|
+
getTeam(): Promise<Result<TeamInfo, string>>;
|
|
59
|
+
|
|
60
|
+
/** List team members. */
|
|
61
|
+
getTeamMembers(): Promise<Result<TeamMember[], string>>;
|
|
62
|
+
|
|
63
|
+
/** Invite a new member by email. */
|
|
64
|
+
inviteTeamMember(
|
|
65
|
+
email: string,
|
|
66
|
+
role?: "admin" | "member",
|
|
67
|
+
): Promise<Result<{ invited: boolean }, string>>;
|
|
68
|
+
|
|
69
|
+
/** Report prompt feedback to the cloud. */
|
|
70
|
+
postFeedback(
|
|
71
|
+
payload: CloudFeedbackPayload,
|
|
72
|
+
): Promise<Result<{ recorded: boolean }, string>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a cloud API client.
|
|
77
|
+
*
|
|
78
|
+
* Every request attaches `Authorization: Bearer <token>` when a token is
|
|
79
|
+
* present in the config. Transient failures (429, 5xx) are retried up to
|
|
80
|
+
* `maxRetries` times with exponential backoff.
|
|
81
|
+
*/
|
|
82
|
+
export function createCloudClient(config: CloudConfig): CloudClient {
|
|
83
|
+
const {
|
|
84
|
+
baseUrl,
|
|
85
|
+
token,
|
|
86
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
87
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
88
|
+
} = config;
|
|
89
|
+
|
|
90
|
+
/** Build standard headers. */
|
|
91
|
+
function headers(): Record<string, string> {
|
|
92
|
+
const h: Record<string, string> = {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
Accept: "application/json",
|
|
95
|
+
};
|
|
96
|
+
if (token) {
|
|
97
|
+
h.Authorization = `Bearer ${token}`;
|
|
98
|
+
}
|
|
99
|
+
return h;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Make an HTTP request with retry + timeout. */
|
|
103
|
+
async function request<T>(
|
|
104
|
+
method: string,
|
|
105
|
+
path: string,
|
|
106
|
+
body?: unknown,
|
|
107
|
+
): Promise<Result<T, string>> {
|
|
108
|
+
let lastError = "Unknown error";
|
|
109
|
+
|
|
110
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
111
|
+
if (attempt > 0) {
|
|
112
|
+
const backoff = INITIAL_BACKOFF_MS * 2 ** (attempt - 1);
|
|
113
|
+
await sleep(backoff);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
119
|
+
|
|
120
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
121
|
+
method,
|
|
122
|
+
headers: headers(),
|
|
123
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
|
|
129
|
+
if (isRetryable(response.status) && attempt < maxRetries) {
|
|
130
|
+
lastError = `HTTP ${response.status}`;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
let message: string;
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(text) as ApiResponse<unknown>;
|
|
139
|
+
message = parsed.error ?? `HTTP ${response.status}`;
|
|
140
|
+
} catch {
|
|
141
|
+
message = text || `HTTP ${response.status}`;
|
|
142
|
+
}
|
|
143
|
+
return err(message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 204 No Content — return void-compatible
|
|
147
|
+
if (response.status === 204) {
|
|
148
|
+
return ok(undefined as unknown as T);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const json = (await response.json()) as ApiResponse<T>;
|
|
152
|
+
if (json.error) {
|
|
153
|
+
return err(json.error);
|
|
154
|
+
}
|
|
155
|
+
return ok(json.data as T);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (
|
|
158
|
+
e instanceof DOMException &&
|
|
159
|
+
e.name === "AbortError" &&
|
|
160
|
+
attempt < maxRetries
|
|
161
|
+
) {
|
|
162
|
+
lastError = "Request timed out";
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
166
|
+
// Transient network errors are retried via the outer loop
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return err(`Request failed after ${maxRetries + 1} attempts: ${lastError}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
health: () => request<{ status: string }>("GET", "/health"),
|
|
175
|
+
|
|
176
|
+
getPrompts: () => request<PromptRecord[]>("GET", "/prompts"),
|
|
177
|
+
|
|
178
|
+
putPrompts: (prompts) => request<void>("PUT", "/prompts", { prompts }),
|
|
179
|
+
|
|
180
|
+
getTeam: () => request<TeamInfo>("GET", "/team"),
|
|
181
|
+
|
|
182
|
+
getTeamMembers: () => request<TeamMember[]>("GET", "/team/members"),
|
|
183
|
+
|
|
184
|
+
inviteTeamMember: (email, role = "member") =>
|
|
185
|
+
request<{ invited: boolean }>("POST", "/team/invite", { email, role }),
|
|
186
|
+
|
|
187
|
+
postFeedback: (payload) =>
|
|
188
|
+
request<{ recorded: boolean }>("POST", "/feedback", payload),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud client shared types.
|
|
3
|
+
*
|
|
4
|
+
* Used by the cloud HTTP client, auth module, and CLI commands
|
|
5
|
+
* for syncing prompts, team management, and device-flow auth.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Configuration ───────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface CloudConfig {
|
|
11
|
+
/** Base URL of the maina cloud API (e.g. "https://api.maina.dev"). */
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
/** Bearer token for authenticated requests. */
|
|
14
|
+
token?: string;
|
|
15
|
+
/** Request timeout in milliseconds. Default: 10_000. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
/** Maximum retry attempts for transient failures. Default: 3. */
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Prompts ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface PromptRecord {
|
|
24
|
+
/** Unique prompt identifier. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** File path relative to .maina/prompts/ (e.g. "commit.md"). */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Full prompt content (markdown). */
|
|
29
|
+
content: string;
|
|
30
|
+
/** SHA-256 hash of the content for change detection. */
|
|
31
|
+
hash: string;
|
|
32
|
+
/** ISO-8601 timestamp of last modification. */
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Team ────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export interface TeamInfo {
|
|
39
|
+
/** Team identifier. */
|
|
40
|
+
id: string;
|
|
41
|
+
/** Display name. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Current billing plan. */
|
|
44
|
+
plan: string;
|
|
45
|
+
/** Number of seats used / total. */
|
|
46
|
+
seats: { used: number; total: number };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TeamMember {
|
|
50
|
+
/** User email. */
|
|
51
|
+
email: string;
|
|
52
|
+
/** Role within the team. */
|
|
53
|
+
role: "owner" | "admin" | "member";
|
|
54
|
+
/** ISO-8601 join date. */
|
|
55
|
+
joinedAt: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Device-Code OAuth ───────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export interface DeviceCodeResponse {
|
|
61
|
+
/** The code the user enters on the verification page. */
|
|
62
|
+
userCode: string;
|
|
63
|
+
/** The device code used to poll for token completion. */
|
|
64
|
+
deviceCode: string;
|
|
65
|
+
/** URL the user should visit. */
|
|
66
|
+
verificationUri: string;
|
|
67
|
+
/** Polling interval in seconds. */
|
|
68
|
+
interval: number;
|
|
69
|
+
/** Seconds until the device code expires. */
|
|
70
|
+
expiresIn: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface TokenResponse {
|
|
74
|
+
/** Bearer access token. */
|
|
75
|
+
accessToken: string;
|
|
76
|
+
/** Refresh token (if applicable). */
|
|
77
|
+
refreshToken?: string;
|
|
78
|
+
/** Seconds until the access token expires. */
|
|
79
|
+
expiresIn: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── API Envelope ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export interface ApiResponse<T> {
|
|
85
|
+
/** Response payload (present on success). */
|
|
86
|
+
data?: T;
|
|
87
|
+
/** Error message (present on failure). */
|
|
88
|
+
error?: string;
|
|
89
|
+
/** Optional metadata. */
|
|
90
|
+
meta?: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Feedback ────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export interface CloudFeedbackPayload {
|
|
96
|
+
/** Prompt hash the feedback refers to. */
|
|
97
|
+
promptHash: string;
|
|
98
|
+
/** Command that generated the output. */
|
|
99
|
+
command: string;
|
|
100
|
+
/** Whether the user accepted the output. */
|
|
101
|
+
accepted: boolean;
|
|
102
|
+
/** ISO-8601 timestamp. */
|
|
103
|
+
timestamp: string;
|
|
104
|
+
/** Optional context about the feedback. */
|
|
105
|
+
context?: string;
|
|
106
|
+
}
|
package/src/context/relevance.ts
CHANGED
package/src/context/retrieval.ts
CHANGED
|
@@ -236,6 +236,9 @@ export async function searchWithGrep(
|
|
|
236
236
|
"--include=*.py",
|
|
237
237
|
"--include=*.go",
|
|
238
238
|
"--include=*.rs",
|
|
239
|
+
"--include=*.cs",
|
|
240
|
+
"--include=*.java",
|
|
241
|
+
"--include=*.kt",
|
|
239
242
|
"--exclude-dir=node_modules",
|
|
240
243
|
"--exclude-dir=dist",
|
|
241
244
|
"--exclude-dir=.git",
|
package/src/context/semantic.ts
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for post-workflow RL trace analysis.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that analyzeWorkflowTrace() reads workflow context,
|
|
5
|
+
* correlates with feedback data, and generates prompt improvements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
describe("analyzeWorkflowTrace", () => {
|
|
14
|
+
let mainaDir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mainaDir = join(tmpdir(), `maina-trace-test-${Date.now()}`);
|
|
18
|
+
mkdirSync(join(mainaDir, "workflow"), { recursive: true });
|
|
19
|
+
mkdirSync(join(mainaDir, "prompts"), { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(mainaDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should parse workflow context into trace steps", async () => {
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(mainaDir, "workflow", "current.md"),
|
|
29
|
+
[
|
|
30
|
+
"# Workflow: feature/test",
|
|
31
|
+
"",
|
|
32
|
+
"## plan (2026-04-05T10:00:00.000Z)",
|
|
33
|
+
"Feature scaffolded.",
|
|
34
|
+
"",
|
|
35
|
+
"## commit (2026-04-05T10:05:00.000Z)",
|
|
36
|
+
"Verified: 8 tools, 0 findings. Committed.",
|
|
37
|
+
].join("\n"),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const { analyzeWorkflowTrace } = await import("../trace-analysis");
|
|
41
|
+
const result = await analyzeWorkflowTrace(mainaDir);
|
|
42
|
+
|
|
43
|
+
expect(result.steps).toHaveLength(2);
|
|
44
|
+
expect(result.steps[0]?.command).toBe("plan");
|
|
45
|
+
expect(result.steps[1]?.command).toBe("commit");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return empty improvements when no workflow exists", async () => {
|
|
49
|
+
rmSync(join(mainaDir, "workflow"), { recursive: true, force: true });
|
|
50
|
+
|
|
51
|
+
const { analyzeWorkflowTrace } = await import("../trace-analysis");
|
|
52
|
+
const result = await analyzeWorkflowTrace(mainaDir);
|
|
53
|
+
|
|
54
|
+
expect(result.steps).toEqual([]);
|
|
55
|
+
expect(result.improvements).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should generate improvements from trace patterns", async () => {
|
|
59
|
+
writeFileSync(
|
|
60
|
+
join(mainaDir, "workflow", "current.md"),
|
|
61
|
+
[
|
|
62
|
+
"# Workflow: feature/test",
|
|
63
|
+
"",
|
|
64
|
+
"## commit (2026-04-05T10:00:00.000Z)",
|
|
65
|
+
"Verified: 8 tools, 2 findings. Committed.",
|
|
66
|
+
"",
|
|
67
|
+
"## commit (2026-04-05T10:05:00.000Z)",
|
|
68
|
+
"Verified: 8 tools, 3 findings. Committed.",
|
|
69
|
+
"",
|
|
70
|
+
"## commit (2026-04-05T10:10:00.000Z)",
|
|
71
|
+
"Verified: 8 tools, 0 findings. Committed.",
|
|
72
|
+
].join("\n"),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const { analyzeWorkflowTrace } = await import("../trace-analysis");
|
|
76
|
+
const result = await analyzeWorkflowTrace(mainaDir);
|
|
77
|
+
|
|
78
|
+
expect(result.steps).toHaveLength(3);
|
|
79
|
+
// Should detect that early commits had findings, suggesting improvement
|
|
80
|
+
expect(typeof result.summary).toBe("string");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return TraceResult with correct shape", async () => {
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(mainaDir, "workflow", "current.md"),
|
|
86
|
+
"# Workflow: test\n",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const { analyzeWorkflowTrace } = await import("../trace-analysis");
|
|
90
|
+
const result = await analyzeWorkflowTrace(mainaDir);
|
|
91
|
+
|
|
92
|
+
expect(result).toHaveProperty("steps");
|
|
93
|
+
expect(result).toHaveProperty("improvements");
|
|
94
|
+
expect(result).toHaveProperty("summary");
|
|
95
|
+
expect(Array.isArray(result.steps)).toBe(true);
|
|
96
|
+
expect(Array.isArray(result.improvements)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|