aidimag 1.0.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/LICENSE +102 -0
- package/README.md +113 -0
- package/dist/capture/bootstrap.d.ts +25 -0
- package/dist/capture/bootstrap.js +188 -0
- package/dist/capture/commit-miner.d.ts +59 -0
- package/dist/capture/commit-miner.js +381 -0
- package/dist/capture/harvest.d.ts +50 -0
- package/dist/capture/harvest.js +207 -0
- package/dist/capture/pr-miner.d.ts +57 -0
- package/dist/capture/pr-miner.js +185 -0
- package/dist/capture/session-briefing.d.ts +36 -0
- package/dist/capture/session-briefing.js +150 -0
- package/dist/capture/session-extraction.d.ts +23 -0
- package/dist/capture/session-extraction.js +54 -0
- package/dist/capture/triage.d.ts +30 -0
- package/dist/capture/triage.js +108 -0
- package/dist/cli/commands/capture.d.ts +5 -0
- package/dist/cli/commands/capture.js +357 -0
- package/dist/cli/commands/hosts.d.ts +5 -0
- package/dist/cli/commands/hosts.js +98 -0
- package/dist/cli/commands/knowledge.d.ts +5 -0
- package/dist/cli/commands/knowledge.js +121 -0
- package/dist/cli/commands/memory.d.ts +6 -0
- package/dist/cli/commands/memory.js +392 -0
- package/dist/cli/commands/sync.d.ts +5 -0
- package/dist/cli/commands/sync.js +307 -0
- package/dist/cli/commands/tickets.d.ts +6 -0
- package/dist/cli/commands/tickets.js +328 -0
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +136 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +46 -0
- package/dist/cli/shared.d.ts +37 -0
- package/dist/cli/shared.js +175 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.js +51 -0
- package/dist/context/generate.d.ts +24 -0
- package/dist/context/generate.js +115 -0
- package/dist/critique/critique.d.ts +40 -0
- package/dist/critique/critique.js +110 -0
- package/dist/db/schema.d.ts +28 -0
- package/dist/db/schema.js +269 -0
- package/dist/db/store.d.ts +182 -0
- package/dist/db/store.js +906 -0
- package/dist/debug.d.ts +14 -0
- package/dist/debug.js +25 -0
- package/dist/embeddings/provider.d.ts +16 -0
- package/dist/embeddings/provider.js +100 -0
- package/dist/embeddings/search.d.ts +22 -0
- package/dist/embeddings/search.js +95 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/knowledge/chunk.d.ts +9 -0
- package/dist/knowledge/chunk.js +78 -0
- package/dist/knowledge/extract.d.ts +34 -0
- package/dist/knowledge/extract.js +113 -0
- package/dist/knowledge/ingest.d.ts +79 -0
- package/dist/knowledge/ingest.js +305 -0
- package/dist/knowledge/llm.d.ts +21 -0
- package/dist/knowledge/llm.js +110 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +532 -0
- package/dist/security/evidence.d.ts +11 -0
- package/dist/security/evidence.js +15 -0
- package/dist/security/seal.d.ts +3 -0
- package/dist/security/seal.js +28 -0
- package/dist/security/sync-push.d.ts +2 -0
- package/dist/security/sync-push.js +37 -0
- package/dist/security/url.d.ts +6 -0
- package/dist/security/url.js +67 -0
- package/dist/sync/client.d.ts +84 -0
- package/dist/sync/client.js +391 -0
- package/dist/sync/server.d.ts +75 -0
- package/dist/sync/server.js +659 -0
- package/dist/tickets/provider.d.ts +80 -0
- package/dist/tickets/provider.js +375 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +5 -0
- package/dist/ui/page.d.ts +5 -0
- package/dist/ui/page.js +841 -0
- package/dist/ui/server.d.ts +10 -0
- package/dist/ui/server.js +437 -0
- package/dist/verify/check.d.ts +38 -0
- package/dist/verify/check.js +121 -0
- package/dist/verify/engine.d.ts +39 -0
- package/dist/verify/engine.js +178 -0
- package/dist/verify/hooks.d.ts +24 -0
- package/dist/verify/hooks.js +125 -0
- package/dist/verify/runners.d.ts +26 -0
- package/dist/verify/runners.js +193 -0
- package/package.json +78 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket-aware capture (TICKETS_DESIGN.md, phases T1–T2).
|
|
3
|
+
*
|
|
4
|
+
* Architecture principle: contract + adapters, not a mandatory service.
|
|
5
|
+
* aidimag core only ever knows `TicketProvider.getTicket(id)`. All API
|
|
6
|
+
* parsing, auth, and rate-limit handling lives behind that boundary:
|
|
7
|
+
*
|
|
8
|
+
* JiraProvider direct API, local creds
|
|
9
|
+
* GitHubProvider direct API (issues), local creds
|
|
10
|
+
* HttpProvider any URL implementing the contract (BYO middleware)
|
|
11
|
+
* (RemoteProvider via the sync server lands with T3)
|
|
12
|
+
*
|
|
13
|
+
* Hard rule: ticket FETCH is lazy and non-blocking — the post-commit hook
|
|
14
|
+
* only extracts the ticket id (regex, offline); getTicket runs at review
|
|
15
|
+
* time or on demand (`dim ticket show`).
|
|
16
|
+
*/
|
|
17
|
+
export interface Ticket {
|
|
18
|
+
id: string;
|
|
19
|
+
url: string;
|
|
20
|
+
title: string;
|
|
21
|
+
/** description, truncated (~2KB) */
|
|
22
|
+
body: string;
|
|
23
|
+
type: "bug" | "story" | "task" | "epic" | "other";
|
|
24
|
+
status: "open" | "in_progress" | "done" | "other";
|
|
25
|
+
labels: string[];
|
|
26
|
+
parent?: {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface TicketProvider {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
getTicket(id: string): Promise<Ticket | null>;
|
|
34
|
+
}
|
|
35
|
+
export interface BranchRules {
|
|
36
|
+
pattern?: string;
|
|
37
|
+
exempt?: string[];
|
|
38
|
+
enforce?: "push" | "warn" | "off";
|
|
39
|
+
}
|
|
40
|
+
export interface TicketsConfig {
|
|
41
|
+
provider?: "jira" | "github" | "linear" | "http" | "remote";
|
|
42
|
+
/** ticket-id regex for branch/commit-message extraction */
|
|
43
|
+
pattern?: string;
|
|
44
|
+
/** Jira site / GitHub repo URL / HttpProvider endpoint (unused for remote) */
|
|
45
|
+
baseUrl?: string;
|
|
46
|
+
branch?: BranchRules;
|
|
47
|
+
}
|
|
48
|
+
/** Where each provider's API token lives — used by the interactive connect flow. */
|
|
49
|
+
export declare const TOKEN_PAGES: Record<string, string>;
|
|
50
|
+
export declare const DEFAULT_TICKET_PATTERN = "[A-Z][A-Z0-9]+-\\d+";
|
|
51
|
+
export declare function readTicketsConfig(repoRoot: string): TicketsConfig;
|
|
52
|
+
export declare function writeTicketsConfig(repoRoot: string, tickets: TicketsConfig): void;
|
|
53
|
+
/** Ticket credentials live alongside sync tokens, keyed `ticket:<baseUrl>`. */
|
|
54
|
+
export declare function getTicketCredential(baseUrl: string): string | null;
|
|
55
|
+
export declare function saveTicketCredential(baseUrl: string, credential: string): void;
|
|
56
|
+
/** Extract the first ticket id from text (branch name, commit subject/body). */
|
|
57
|
+
export declare function extractTicketId(text: string, pattern?: string): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Ticket id implied by the CURRENT branch (offline, instant). The best prompt
|
|
60
|
+
* is the one the branch name already answered — used by the MCP session-end
|
|
61
|
+
* flow and the VSCode extension.
|
|
62
|
+
*/
|
|
63
|
+
export declare function detectBranchTicket(repoRoot: string): string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Build a direct (non-remote) provider from raw parts — used locally AND by
|
|
66
|
+
* the sync server's /v1/ticket proxy (T3), so adapter logic lives in one place.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildDirectProvider(provider: string, baseUrl: string, credential: string | null): TicketProvider | null;
|
|
69
|
+
/** Build the configured provider, or null when tickets aren't set up / no credential. */
|
|
70
|
+
export declare function ticketProviderFor(repoRoot: string): TicketProvider | null;
|
|
71
|
+
export interface BranchCheckResult {
|
|
72
|
+
branch: string;
|
|
73
|
+
ok: boolean;
|
|
74
|
+
exempt: boolean;
|
|
75
|
+
enforce: "push" | "warn" | "off";
|
|
76
|
+
pattern: string | null;
|
|
77
|
+
}
|
|
78
|
+
export declare function checkBranchName(repoRoot: string, branch: string): BranchCheckResult;
|
|
79
|
+
/** Build a conforming branch name: feature/XXX-2100-serialize-token-refresh */
|
|
80
|
+
export declare function buildBranchName(ticketId: string, title?: string, prefix?: string): string;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket-aware capture (TICKETS_DESIGN.md, phases T1–T2).
|
|
3
|
+
*
|
|
4
|
+
* Architecture principle: contract + adapters, not a mandatory service.
|
|
5
|
+
* aidimag core only ever knows `TicketProvider.getTicket(id)`. All API
|
|
6
|
+
* parsing, auth, and rate-limit handling lives behind that boundary:
|
|
7
|
+
*
|
|
8
|
+
* JiraProvider direct API, local creds
|
|
9
|
+
* GitHubProvider direct API (issues), local creds
|
|
10
|
+
* HttpProvider any URL implementing the contract (BYO middleware)
|
|
11
|
+
* (RemoteProvider via the sync server lands with T3)
|
|
12
|
+
*
|
|
13
|
+
* Hard rule: ticket FETCH is lazy and non-blocking — the post-commit hook
|
|
14
|
+
* only extracts the ticket id (regex, offline); getTicket runs at review
|
|
15
|
+
* time or on demand (`dim ticket show`).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { isAllowedTicketBaseUrl } from "../security/url.js";
|
|
22
|
+
/** Where each provider's API token lives — used by the interactive connect flow. */
|
|
23
|
+
export const TOKEN_PAGES = {
|
|
24
|
+
jira: "https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
25
|
+
github: "https://github.com/settings/tokens",
|
|
26
|
+
linear: "https://linear.app/settings/account/security",
|
|
27
|
+
};
|
|
28
|
+
export const DEFAULT_TICKET_PATTERN = "[A-Z][A-Z0-9]+-\\d+";
|
|
29
|
+
function configPath(repoRoot) {
|
|
30
|
+
return path.join(repoRoot, ".aidimag", "config.json");
|
|
31
|
+
}
|
|
32
|
+
export function readTicketsConfig(repoRoot) {
|
|
33
|
+
try {
|
|
34
|
+
const cfg = JSON.parse(readFileSync(configPath(repoRoot), "utf8"));
|
|
35
|
+
return cfg.tickets ?? {};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function writeTicketsConfig(repoRoot, tickets) {
|
|
42
|
+
const p = configPath(repoRoot);
|
|
43
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
44
|
+
let existing = {};
|
|
45
|
+
try {
|
|
46
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// fresh file
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(p, JSON.stringify({ ...existing, tickets }, null, 2) + "\n");
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------- credentials (never the repo)
|
|
54
|
+
function credentialsPath() {
|
|
55
|
+
return path.join(homedir(), ".aidimag", "credentials.json");
|
|
56
|
+
}
|
|
57
|
+
/** Ticket credentials live alongside sync tokens, keyed `ticket:<baseUrl>`. */
|
|
58
|
+
export function getTicketCredential(baseUrl) {
|
|
59
|
+
if (process.env.AIDIMAG_TICKET_TOKEN)
|
|
60
|
+
return process.env.AIDIMAG_TICKET_TOKEN;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(credentialsPath(), "utf8"))[`ticket:${baseUrl}`] ?? null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function saveTicketCredential(baseUrl, credential) {
|
|
69
|
+
const p = credentialsPath();
|
|
70
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
71
|
+
const creds = existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : {};
|
|
72
|
+
creds[`ticket:${baseUrl}`] = credential;
|
|
73
|
+
writeFileSync(p, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
|
|
74
|
+
try {
|
|
75
|
+
chmodSync(p, 0o600);
|
|
76
|
+
}
|
|
77
|
+
catch { /* best-effort */ }
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------- ticket-id extraction (T1 — offline)
|
|
80
|
+
/** Extract the first ticket id from text (branch name, commit subject/body). */
|
|
81
|
+
export function extractTicketId(text, pattern = DEFAULT_TICKET_PATTERN) {
|
|
82
|
+
try {
|
|
83
|
+
const m = text.match(new RegExp(pattern));
|
|
84
|
+
return m ? m[0] : null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null; // bad user regex — never break capture
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Ticket id implied by the CURRENT branch (offline, instant). The best prompt
|
|
92
|
+
* is the one the branch name already answered — used by the MCP session-end
|
|
93
|
+
* flow and the VSCode extension.
|
|
94
|
+
*/
|
|
95
|
+
export function detectBranchTicket(repoRoot) {
|
|
96
|
+
try {
|
|
97
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
98
|
+
cwd: repoRoot,
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
101
|
+
}).trim();
|
|
102
|
+
return extractTicketId(branch, readTicketsConfig(repoRoot).pattern ?? DEFAULT_TICKET_PATTERN);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null; // detached HEAD / not a repo
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------- providers
|
|
109
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
110
|
+
const BODY_LIMIT = 2_048;
|
|
111
|
+
async function fetchJson(url, headers) {
|
|
112
|
+
const ctl = new AbortController();
|
|
113
|
+
const t = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(url, { headers, signal: ctl.signal });
|
|
116
|
+
if (res.status === 404)
|
|
117
|
+
return null;
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
throw new Error(`HTTP ${res.status} from ${new URL(url).host}`);
|
|
120
|
+
return (await res.json());
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(t);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function truncate(s) {
|
|
127
|
+
return s.length > BODY_LIMIT ? s.slice(0, BODY_LIMIT) + "…" : s;
|
|
128
|
+
}
|
|
129
|
+
/** Jira Cloud/Server: GET /rest/api/2/issue/<id>. Credential: "email:apiToken" (Basic) or a PAT (Bearer). */
|
|
130
|
+
class JiraProvider {
|
|
131
|
+
baseUrl;
|
|
132
|
+
credential;
|
|
133
|
+
name = "jira";
|
|
134
|
+
constructor(baseUrl, credential) {
|
|
135
|
+
this.baseUrl = baseUrl;
|
|
136
|
+
this.credential = credential;
|
|
137
|
+
}
|
|
138
|
+
async getTicket(id) {
|
|
139
|
+
const auth = this.credential.includes(":")
|
|
140
|
+
? `Basic ${Buffer.from(this.credential).toString("base64")}`
|
|
141
|
+
: `Bearer ${this.credential}`;
|
|
142
|
+
const raw = await fetchJson(`${this.baseUrl}/rest/api/2/issue/${encodeURIComponent(id)}`, {
|
|
143
|
+
Authorization: auth,
|
|
144
|
+
Accept: "application/json",
|
|
145
|
+
});
|
|
146
|
+
if (!raw)
|
|
147
|
+
return null;
|
|
148
|
+
const f = (raw.fields ?? {});
|
|
149
|
+
const issueType = String(f.issuetype?.name ?? "").toLowerCase();
|
|
150
|
+
const statusCat = String(f.status?.statusCategory?.key ?? "");
|
|
151
|
+
const parent = f.parent;
|
|
152
|
+
return {
|
|
153
|
+
id: String(raw.key ?? id),
|
|
154
|
+
url: `${this.baseUrl}/browse/${raw.key ?? id}`,
|
|
155
|
+
title: String(f.summary ?? ""),
|
|
156
|
+
body: truncate(String(f.description ?? "")),
|
|
157
|
+
type: issueType.includes("bug")
|
|
158
|
+
? "bug"
|
|
159
|
+
: issueType.includes("story")
|
|
160
|
+
? "story"
|
|
161
|
+
: issueType.includes("epic")
|
|
162
|
+
? "epic"
|
|
163
|
+
: issueType.includes("task")
|
|
164
|
+
? "task"
|
|
165
|
+
: "other",
|
|
166
|
+
status: statusCat === "done" ? "done" : statusCat === "indeterminate" ? "in_progress" : statusCat === "new" ? "open" : "other",
|
|
167
|
+
labels: Array.isArray(f.labels) ? f.labels.map(String) : [],
|
|
168
|
+
parent: parent
|
|
169
|
+
? { id: String(parent.key), title: String((parent.fields ?? {}).summary ?? "") }
|
|
170
|
+
: undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** GitHub Issues: baseUrl is the repo URL (https://github.com/owner/repo); ids are issue numbers ("123" or "#123"). */
|
|
175
|
+
class GitHubProvider {
|
|
176
|
+
baseUrl;
|
|
177
|
+
credential;
|
|
178
|
+
name = "github";
|
|
179
|
+
constructor(baseUrl, credential) {
|
|
180
|
+
this.baseUrl = baseUrl;
|
|
181
|
+
this.credential = credential;
|
|
182
|
+
}
|
|
183
|
+
async getTicket(id) {
|
|
184
|
+
const m = this.baseUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
185
|
+
if (!m)
|
|
186
|
+
throw new Error(`tickets.baseUrl must look like https://github.com/owner/repo (got ${this.baseUrl})`);
|
|
187
|
+
const num = id.replace(/^#/, "");
|
|
188
|
+
const raw = await fetchJson(`https://api.github.com/repos/${m[1]}/${m[2].replace(/\.git$/, "")}/issues/${num}`, {
|
|
189
|
+
Authorization: `Bearer ${this.credential}`,
|
|
190
|
+
Accept: "application/vnd.github+json",
|
|
191
|
+
});
|
|
192
|
+
if (!raw)
|
|
193
|
+
return null;
|
|
194
|
+
const labels = Array.isArray(raw.labels)
|
|
195
|
+
? raw.labels.map((l) => String(l.name ?? l))
|
|
196
|
+
: [];
|
|
197
|
+
const lower = labels.map((l) => l.toLowerCase());
|
|
198
|
+
return {
|
|
199
|
+
id: `#${raw.number}`,
|
|
200
|
+
url: String(raw.html_url ?? ""),
|
|
201
|
+
title: String(raw.title ?? ""),
|
|
202
|
+
body: truncate(String(raw.body ?? "")),
|
|
203
|
+
type: lower.some((l) => l.includes("bug")) ? "bug" : lower.some((l) => l.includes("enhancement") || l.includes("feature")) ? "story" : "other",
|
|
204
|
+
status: raw.state === "closed" ? "done" : "open",
|
|
205
|
+
labels,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Bring-your-own middleware: GET <baseUrl>/ticket/<id> returning the normalized Ticket JSON. */
|
|
210
|
+
class HttpProvider {
|
|
211
|
+
baseUrl;
|
|
212
|
+
credential;
|
|
213
|
+
name = "http";
|
|
214
|
+
constructor(baseUrl, credential) {
|
|
215
|
+
this.baseUrl = baseUrl;
|
|
216
|
+
this.credential = credential;
|
|
217
|
+
}
|
|
218
|
+
async getTicket(id) {
|
|
219
|
+
const raw = await fetchJson(`${this.baseUrl.replace(/\/$/, "")}/ticket/${encodeURIComponent(id)}`, {
|
|
220
|
+
...(this.credential ? { Authorization: `Bearer ${this.credential}` } : {}),
|
|
221
|
+
Accept: "application/json",
|
|
222
|
+
});
|
|
223
|
+
return raw ? raw : null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Linear: GraphQL API, ids like ENG-123. Credential: a Linear API key. */
|
|
227
|
+
class LinearProvider {
|
|
228
|
+
credential;
|
|
229
|
+
name = "linear";
|
|
230
|
+
constructor(credential) {
|
|
231
|
+
this.credential = credential;
|
|
232
|
+
}
|
|
233
|
+
async getTicket(id) {
|
|
234
|
+
const ctl = new AbortController();
|
|
235
|
+
const t = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch("https://api.linear.app/graphql", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: { Authorization: this.credential, "Content-Type": "application/json" },
|
|
240
|
+
signal: ctl.signal,
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
query: `query($id: String!) { issue(id: $id) {
|
|
243
|
+
identifier url title description
|
|
244
|
+
state { type } labels { nodes { name } }
|
|
245
|
+
parent { identifier title }
|
|
246
|
+
} }`,
|
|
247
|
+
variables: { id },
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
if (!res.ok)
|
|
251
|
+
throw new Error(`HTTP ${res.status} from api.linear.app`);
|
|
252
|
+
const json = (await res.json());
|
|
253
|
+
const issue = json.data?.issue;
|
|
254
|
+
if (!issue)
|
|
255
|
+
return null;
|
|
256
|
+
const stateType = String(issue.state?.type ?? "");
|
|
257
|
+
const labels = (issue.labels?.nodes ?? []).map((l) => l.name);
|
|
258
|
+
const parent = issue.parent;
|
|
259
|
+
return {
|
|
260
|
+
id: String(issue.identifier ?? id),
|
|
261
|
+
url: String(issue.url ?? ""),
|
|
262
|
+
title: String(issue.title ?? ""),
|
|
263
|
+
body: truncate(String(issue.description ?? "")),
|
|
264
|
+
type: labels.some((l) => l.toLowerCase().includes("bug")) ? "bug" : "story",
|
|
265
|
+
status: stateType === "completed" || stateType === "canceled" ? "done" : stateType === "started" ? "in_progress" : "open",
|
|
266
|
+
labels,
|
|
267
|
+
parent: parent ? { id: String(parent.identifier), title: String(parent.title ?? "") } : undefined,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
clearTimeout(t);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** T3: asks the team sync server — credentials live server-side, members reuse their sync token. */
|
|
276
|
+
class RemoteProvider {
|
|
277
|
+
server;
|
|
278
|
+
brain;
|
|
279
|
+
token;
|
|
280
|
+
name = "remote";
|
|
281
|
+
constructor(server, brain, token) {
|
|
282
|
+
this.server = server;
|
|
283
|
+
this.brain = brain;
|
|
284
|
+
this.token = token;
|
|
285
|
+
}
|
|
286
|
+
async getTicket(id) {
|
|
287
|
+
const raw = await fetchJson(`${this.server}/v1/ticket?brain=${encodeURIComponent(this.brain)}&id=${encodeURIComponent(id)}`, { Authorization: `Bearer ${this.token}`, Accept: "application/json" });
|
|
288
|
+
return raw ? raw : null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Build a direct (non-remote) provider from raw parts — used locally AND by
|
|
293
|
+
* the sync server's /v1/ticket proxy (T3), so adapter logic lives in one place.
|
|
294
|
+
*/
|
|
295
|
+
export function buildDirectProvider(provider, baseUrl, credential) {
|
|
296
|
+
switch (provider) {
|
|
297
|
+
case "jira":
|
|
298
|
+
return credential ? new JiraProvider(baseUrl.replace(/\/$/, ""), credential) : null;
|
|
299
|
+
case "github":
|
|
300
|
+
return credential ? new GitHubProvider(baseUrl, credential) : null;
|
|
301
|
+
case "linear":
|
|
302
|
+
return credential ? new LinearProvider(credential) : null;
|
|
303
|
+
case "http":
|
|
304
|
+
if (!isAllowedTicketBaseUrl(baseUrl))
|
|
305
|
+
return null;
|
|
306
|
+
return new HttpProvider(baseUrl, credential); // credential optional for internal services
|
|
307
|
+
default:
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Build the configured provider, or null when tickets aren't set up / no credential. */
|
|
312
|
+
export function ticketProviderFor(repoRoot) {
|
|
313
|
+
const cfg = readTicketsConfig(repoRoot);
|
|
314
|
+
if (!cfg.provider)
|
|
315
|
+
return null;
|
|
316
|
+
if (cfg.provider === "remote") {
|
|
317
|
+
// lazy import avoids a cycle: sync/client imports nothing from tickets
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
319
|
+
try {
|
|
320
|
+
const p = path.join(repoRoot, ".aidimag", "config.json");
|
|
321
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
322
|
+
if (!raw.server || !raw.brain)
|
|
323
|
+
return null;
|
|
324
|
+
const token = process.env.AIDIMAG_API_KEY ??
|
|
325
|
+
JSON.parse(readFileSync(credentialsPath(), "utf8"))[raw.server] ??
|
|
326
|
+
null;
|
|
327
|
+
return token ? new RemoteProvider(raw.server, raw.brain, token) : null;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!cfg.baseUrl && cfg.provider !== "linear")
|
|
334
|
+
return null;
|
|
335
|
+
const credKey = cfg.baseUrl ?? "linear";
|
|
336
|
+
return buildDirectProvider(cfg.provider, cfg.baseUrl ?? "", getTicketCredential(credKey));
|
|
337
|
+
}
|
|
338
|
+
export function checkBranchName(repoRoot, branch) {
|
|
339
|
+
const rules = readTicketsConfig(repoRoot).branch ?? {};
|
|
340
|
+
const enforce = rules.enforce ?? "off";
|
|
341
|
+
const pattern = rules.pattern ?? null;
|
|
342
|
+
if (!pattern || enforce === "off")
|
|
343
|
+
return { branch, ok: true, exempt: false, enforce, pattern };
|
|
344
|
+
const exemptList = rules.exempt ?? ["main", "master", "develop", "release/.*", "HEAD"];
|
|
345
|
+
const isExempt = exemptList.some((e) => {
|
|
346
|
+
try {
|
|
347
|
+
return new RegExp(`^(${e})$`).test(branch);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return e === branch;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
if (isExempt)
|
|
354
|
+
return { branch, ok: true, exempt: true, enforce, pattern };
|
|
355
|
+
let ok = false;
|
|
356
|
+
try {
|
|
357
|
+
ok = new RegExp(pattern).test(branch);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
ok = true; // bad admin regex must not lock everyone out
|
|
361
|
+
}
|
|
362
|
+
return { branch, ok, exempt: false, enforce, pattern };
|
|
363
|
+
}
|
|
364
|
+
/** Build a conforming branch name: feature/XXX-2100-serialize-token-refresh */
|
|
365
|
+
export function buildBranchName(ticketId, title, prefix = "feature") {
|
|
366
|
+
const slug = (title ?? "")
|
|
367
|
+
.toLowerCase()
|
|
368
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
369
|
+
.replace(/^-+|-+$/g, "")
|
|
370
|
+
.split("-")
|
|
371
|
+
.slice(0, 6)
|
|
372
|
+
.join("-");
|
|
373
|
+
return slug ? `${prefix}/${ticketId}-${slug}` : `${prefix}/${ticketId}`;
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=provider.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aidimag core types — mirrors the data model in DESIGN.md.
|
|
3
|
+
*/
|
|
4
|
+
export type MemoryKind = "DECISION" | "CONVENTION" | "GOTCHA" | "FAILED_APPROACH" | "ARCHITECTURE" | "INVARIANT" | "TODO_CONTEXT" | "GUARDRAIL" | "SKILL";
|
|
5
|
+
export type MemoryStatus = "VERIFIED" | "UNVERIFIED" | "STALE" | "REFUTED";
|
|
6
|
+
/**
|
|
7
|
+
* Guardrail enforcement level (Karpathy's always/ask-first/never bucket).
|
|
8
|
+
* Only meaningful when kind === "GUARDRAIL".
|
|
9
|
+
*/
|
|
10
|
+
export type GuardrailLevel = "always" | "ask-first" | "never";
|
|
11
|
+
export type EvidenceType = "COMMIT_REF" | "TEST_RESULT" | "EXEC_TRACE" | "STATIC_CHECK" | "HUMAN_ATTESTED" | "TICKET_REF";
|
|
12
|
+
export type EvidenceResult = "PASS" | "FAIL" | "UNKNOWN";
|
|
13
|
+
export interface Evidence {
|
|
14
|
+
id: string;
|
|
15
|
+
memoryId: string;
|
|
16
|
+
type: EvidenceType;
|
|
17
|
+
/** ref / script / assertion — interpretation depends on `type` */
|
|
18
|
+
payload: string;
|
|
19
|
+
lastRun: string | null;
|
|
20
|
+
result: EvidenceResult;
|
|
21
|
+
}
|
|
22
|
+
export type LinkRelation = "supports" | "contradicts" | "refines";
|
|
23
|
+
export interface MemoryLink {
|
|
24
|
+
fromId: string;
|
|
25
|
+
toId: string;
|
|
26
|
+
relation: LinkRelation;
|
|
27
|
+
}
|
|
28
|
+
export interface MemoryScope {
|
|
29
|
+
/** glob-ish path patterns this memory applies to; empty = whole repo */
|
|
30
|
+
paths: string[];
|
|
31
|
+
/** optional symbol names (functions, classes) */
|
|
32
|
+
symbols: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface MemoryEntry {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: MemoryKind;
|
|
37
|
+
/** written as a falsifiable statement */
|
|
38
|
+
claim: string;
|
|
39
|
+
scope: MemoryScope;
|
|
40
|
+
confidence: number;
|
|
41
|
+
status: MemoryStatus;
|
|
42
|
+
/**
|
|
43
|
+
* Pinned memories are exempt from time-based confidence decay (they never
|
|
44
|
+
* demote to UNVERIFIED just from age) — but evidence failure still marks
|
|
45
|
+
* them STALE. "Never decays" ≠ "never falsifiable".
|
|
46
|
+
*/
|
|
47
|
+
pinned: boolean;
|
|
48
|
+
createdBy: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
verifiedAt: string | null;
|
|
51
|
+
supersededBy: string | null;
|
|
52
|
+
/** last local mutation time (sync LWW anchor) */
|
|
53
|
+
updatedAt: string | null;
|
|
54
|
+
grounding: Evidence[];
|
|
55
|
+
links: MemoryLink[];
|
|
56
|
+
/** enforcement level — present only when kind === "GUARDRAIL" */
|
|
57
|
+
guardrailLevel?: GuardrailLevel;
|
|
58
|
+
}
|
|
59
|
+
export interface MemoryWriteInput {
|
|
60
|
+
kind: MemoryKind;
|
|
61
|
+
claim: string;
|
|
62
|
+
paths?: string[];
|
|
63
|
+
symbols?: string[];
|
|
64
|
+
createdBy?: string;
|
|
65
|
+
evidence?: Array<{
|
|
66
|
+
type: EvidenceType;
|
|
67
|
+
payload: string;
|
|
68
|
+
}>;
|
|
69
|
+
/** create the memory pinned (exempt from time decay) */
|
|
70
|
+
pinned?: boolean;
|
|
71
|
+
/** required when kind === "GUARDRAIL": always | ask-first | never */
|
|
72
|
+
guardrailLevel?: GuardrailLevel;
|
|
73
|
+
/**
|
|
74
|
+
* When true, executable evidence (STATIC_CHECK / TEST_RESULT / EXEC_TRACE) is
|
|
75
|
+
* trusted for local verify. Default false — use for CLI human writes and
|
|
76
|
+
* human-approved proposals only.
|
|
77
|
+
*/
|
|
78
|
+
trustExecutableEvidence?: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface MemorySearchOptions {
|
|
81
|
+
query: string;
|
|
82
|
+
kind?: MemoryKind;
|
|
83
|
+
status?: MemoryStatus;
|
|
84
|
+
/** restrict to memories scoped to (or overlapping) these paths */
|
|
85
|
+
paths?: string[];
|
|
86
|
+
limit?: number;
|
|
87
|
+
includeRefuted?: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface MemoryStatusSummary {
|
|
90
|
+
total: number;
|
|
91
|
+
byStatus: Record<MemoryStatus, number>;
|
|
92
|
+
byKind: Partial<Record<MemoryKind, number>>;
|
|
93
|
+
/** memories exempt from time decay */
|
|
94
|
+
pinned?: number;
|
|
95
|
+
dbPath: string;
|
|
96
|
+
pendingProposals?: number;
|
|
97
|
+
}
|
|
98
|
+
export type ProposalStatus = "PENDING" | "APPROVED" | "REJECTED";
|
|
99
|
+
export interface ProposalInput {
|
|
100
|
+
kind: MemoryKind;
|
|
101
|
+
claim: string;
|
|
102
|
+
paths?: string[];
|
|
103
|
+
symbols?: string[];
|
|
104
|
+
evidence?: Array<{
|
|
105
|
+
type: EvidenceType;
|
|
106
|
+
payload: string;
|
|
107
|
+
}>;
|
|
108
|
+
/** where this proposal came from: 'commit-miner' | 'session:<agent-id>' | 'human' */
|
|
109
|
+
source: string;
|
|
110
|
+
/** e.g. commit sha the proposal was mined from */
|
|
111
|
+
sourceRef?: string;
|
|
112
|
+
/** why the source thinks this is worth remembering */
|
|
113
|
+
rationale?: string;
|
|
114
|
+
/** ticket id (e.g. XXX-2100) extracted from branch/commit message */
|
|
115
|
+
ticketRef?: string;
|
|
116
|
+
/** enforcement level for GUARDRAIL proposals */
|
|
117
|
+
guardrailLevel?: GuardrailLevel;
|
|
118
|
+
}
|
|
119
|
+
export interface Proposal extends ProposalInput {
|
|
120
|
+
id: string;
|
|
121
|
+
createdAt: string;
|
|
122
|
+
status: ProposalStatus;
|
|
123
|
+
/** memory id, set when approved */
|
|
124
|
+
memoryId: string | null;
|
|
125
|
+
/** last local mutation time (sync LWW anchor) */
|
|
126
|
+
updatedAt?: string | null;
|
|
127
|
+
paths: string[];
|
|
128
|
+
symbols: string[];
|
|
129
|
+
evidence: Array<{
|
|
130
|
+
type: EvidenceType;
|
|
131
|
+
payload: string;
|
|
132
|
+
}>;
|
|
133
|
+
}
|