kiro-memory 1.9.0 → 3.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/README.md +5 -1
- package/package.json +5 -5
- package/plugin/dist/cli/contextkit.js +2611 -345
- package/plugin/dist/hooks/agentSpawn.js +853 -223
- package/plugin/dist/hooks/kiro-hooks.js +841 -211
- package/plugin/dist/hooks/postToolUse.js +853 -222
- package/plugin/dist/hooks/stop.js +850 -220
- package/plugin/dist/hooks/userPromptSubmit.js +848 -216
- package/plugin/dist/index.js +843 -340
- package/plugin/dist/plugins/github/github-client.js +152 -0
- package/plugin/dist/plugins/github/index.js +412 -0
- package/plugin/dist/plugins/github/issue-parser.js +54 -0
- package/plugin/dist/plugins/slack/formatter.js +90 -0
- package/plugin/dist/plugins/slack/index.js +215 -0
- package/plugin/dist/sdk/index.js +841 -215
- package/plugin/dist/servers/mcp-server.js +4461 -397
- package/plugin/dist/services/search/EmbeddingService.js +146 -37
- package/plugin/dist/services/search/HybridSearch.js +564 -116
- package/plugin/dist/services/search/VectorSearch.js +187 -60
- package/plugin/dist/services/search/index.js +565 -254
- package/plugin/dist/services/sqlite/Backup.js +416 -0
- package/plugin/dist/services/sqlite/Database.js +126 -153
- package/plugin/dist/services/sqlite/ImportExport.js +452 -0
- package/plugin/dist/services/sqlite/Observations.js +314 -19
- package/plugin/dist/services/sqlite/Prompts.js +1 -1
- package/plugin/dist/services/sqlite/Search.js +41 -29
- package/plugin/dist/services/sqlite/Summaries.js +4 -4
- package/plugin/dist/services/sqlite/index.js +1428 -208
- package/plugin/dist/viewer.css +1 -0
- package/plugin/dist/viewer.html +2 -179
- package/plugin/dist/viewer.js +23 -24942
- package/plugin/dist/viewer.js.map +7 -0
- package/plugin/dist/worker-service.js +427 -5569
- package/plugin/dist/worker-service.js.map +7 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/plugins/github/github-client.ts
|
|
4
|
+
var GitHubClient = class {
|
|
5
|
+
token;
|
|
6
|
+
baseUrl;
|
|
7
|
+
maxRetries;
|
|
8
|
+
cacheTtlMs;
|
|
9
|
+
logger;
|
|
10
|
+
/** Cache in-memory per le issue (chiave: owner/repo#number) */
|
|
11
|
+
issueCache = /* @__PURE__ */ new Map();
|
|
12
|
+
/** Ultimo stato rate limit noto */
|
|
13
|
+
rateLimit = { remaining: 5e3, resetAt: 0 };
|
|
14
|
+
constructor(config, logger) {
|
|
15
|
+
this.token = config.token;
|
|
16
|
+
this.baseUrl = (config.baseUrl || "https://api.github.com").replace(/\/$/, "");
|
|
17
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
18
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 5 * 60 * 1e3;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
}
|
|
21
|
+
// ── Metodi pubblici ──────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Recupera i dati di una issue specifica.
|
|
24
|
+
* Usa cache in-memory per evitare chiamate ripetute.
|
|
25
|
+
*/
|
|
26
|
+
async getIssue(owner, repo, issueNumber) {
|
|
27
|
+
const cacheKey = `${owner}/${repo}#${issueNumber}`;
|
|
28
|
+
const cached = this.issueCache.get(cacheKey);
|
|
29
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
30
|
+
this.logger.info(`Cache hit per issue ${cacheKey}`);
|
|
31
|
+
return cached.data;
|
|
32
|
+
}
|
|
33
|
+
const url = `${this.baseUrl}/repos/${owner}/${repo}/issues/${issueNumber}`;
|
|
34
|
+
const data = await this.request(url);
|
|
35
|
+
this.issueCache.set(cacheKey, {
|
|
36
|
+
data,
|
|
37
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
38
|
+
});
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Aggiunge un commento a una issue.
|
|
43
|
+
* Non usa cache (operazione di scrittura).
|
|
44
|
+
*/
|
|
45
|
+
async addComment(owner, repo, issueNumber, body) {
|
|
46
|
+
const url = `${this.baseUrl}/repos/${owner}/${repo}/issues/${issueNumber}/comments`;
|
|
47
|
+
return this.request(url, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify({ body })
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Restituisce le informazioni correnti sul rate limit.
|
|
54
|
+
*/
|
|
55
|
+
getRateLimit() {
|
|
56
|
+
return { ...this.rateLimit };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Svuota la cache delle issue.
|
|
60
|
+
*/
|
|
61
|
+
clearCache() {
|
|
62
|
+
this.issueCache.clear();
|
|
63
|
+
this.logger.info("Cache issue svuotata");
|
|
64
|
+
}
|
|
65
|
+
// ── Request engine con retry ─────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Esegue una richiesta HTTP verso le GitHub API.
|
|
68
|
+
* Gestisce automaticamente:
|
|
69
|
+
* - Headers di autenticazione
|
|
70
|
+
* - Parsing della risposta JSON
|
|
71
|
+
* - Aggiornamento rate limit
|
|
72
|
+
* - Retry su 403/429 con backoff esponenziale
|
|
73
|
+
*/
|
|
74
|
+
async request(url, options = {}) {
|
|
75
|
+
const headers = {
|
|
76
|
+
"Authorization": `token ${this.token}`,
|
|
77
|
+
"Accept": "application/vnd.github.v3+json",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"User-Agent": "kiro-memory-plugin-github/1.0.0"
|
|
80
|
+
};
|
|
81
|
+
let lastError = null;
|
|
82
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
83
|
+
if (this.rateLimit.remaining === 0) {
|
|
84
|
+
const waitMs = this.rateLimit.resetAt * 1e3 - Date.now();
|
|
85
|
+
if (waitMs > 0 && waitMs < 6e4) {
|
|
86
|
+
this.logger.warn(`Rate limit esaurito. Attendo ${Math.ceil(waitMs / 1e3)}s prima del reset.`);
|
|
87
|
+
await this.sleep(waitMs);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
...options,
|
|
93
|
+
headers: { ...headers, ...options.headers || {} }
|
|
94
|
+
});
|
|
95
|
+
this.updateRateLimit(response);
|
|
96
|
+
if (response.ok) {
|
|
97
|
+
return await response.json();
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 404) {
|
|
100
|
+
throw new Error(`Risorsa non trovata: ${url}`);
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 401) {
|
|
103
|
+
throw new Error("Token GitHub non valido o scaduto");
|
|
104
|
+
}
|
|
105
|
+
if ((response.status === 403 || response.status === 429) && attempt < this.maxRetries) {
|
|
106
|
+
const retryAfter = response.headers.get("retry-after");
|
|
107
|
+
const backoffMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
108
|
+
this.logger.warn(`Rate limit (${response.status}). Retry ${attempt + 1}/${this.maxRetries} tra ${Math.ceil(backoffMs / 1e3)}s`);
|
|
109
|
+
await this.sleep(backoffMs);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const errorBody = await response.text().catch(() => "corpo non leggibile");
|
|
113
|
+
throw new Error(`GitHub API errore ${response.status}: ${errorBody}`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
116
|
+
if (lastError.message.includes("non trovata") || lastError.message.includes("non valido") || lastError.message.includes("scaduto")) {
|
|
117
|
+
throw lastError;
|
|
118
|
+
}
|
|
119
|
+
if (attempt < this.maxRetries) {
|
|
120
|
+
const backoffMs = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
121
|
+
this.logger.warn(`Errore di rete, retry ${attempt + 1}/${this.maxRetries}: ${lastError.message}`);
|
|
122
|
+
await this.sleep(backoffMs);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw lastError || new Error(`Richiesta fallita dopo ${this.maxRetries} tentativi`);
|
|
128
|
+
}
|
|
129
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* Aggiorna le informazioni di rate limit dalla risposta HTTP.
|
|
132
|
+
*/
|
|
133
|
+
updateRateLimit(response) {
|
|
134
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
135
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
136
|
+
if (remaining !== null) {
|
|
137
|
+
this.rateLimit.remaining = parseInt(remaining, 10);
|
|
138
|
+
}
|
|
139
|
+
if (reset !== null) {
|
|
140
|
+
this.rateLimit.resetAt = parseInt(reset, 10);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Utility per await su un delay (usato per backoff e attesa rate limit).
|
|
145
|
+
*/
|
|
146
|
+
sleep(ms) {
|
|
147
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
export {
|
|
151
|
+
GitHubClient
|
|
152
|
+
};
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/plugins/github/github-client.ts
|
|
4
|
+
var GitHubClient = class {
|
|
5
|
+
token;
|
|
6
|
+
baseUrl;
|
|
7
|
+
maxRetries;
|
|
8
|
+
cacheTtlMs;
|
|
9
|
+
logger;
|
|
10
|
+
/** Cache in-memory per le issue (chiave: owner/repo#number) */
|
|
11
|
+
issueCache = /* @__PURE__ */ new Map();
|
|
12
|
+
/** Ultimo stato rate limit noto */
|
|
13
|
+
rateLimit = { remaining: 5e3, resetAt: 0 };
|
|
14
|
+
constructor(config, logger) {
|
|
15
|
+
this.token = config.token;
|
|
16
|
+
this.baseUrl = (config.baseUrl || "https://api.github.com").replace(/\/$/, "");
|
|
17
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
18
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 5 * 60 * 1e3;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
}
|
|
21
|
+
// ── Metodi pubblici ──────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Recupera i dati di una issue specifica.
|
|
24
|
+
* Usa cache in-memory per evitare chiamate ripetute.
|
|
25
|
+
*/
|
|
26
|
+
async getIssue(owner, repo, issueNumber) {
|
|
27
|
+
const cacheKey = `${owner}/${repo}#${issueNumber}`;
|
|
28
|
+
const cached = this.issueCache.get(cacheKey);
|
|
29
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
30
|
+
this.logger.info(`Cache hit per issue ${cacheKey}`);
|
|
31
|
+
return cached.data;
|
|
32
|
+
}
|
|
33
|
+
const url = `${this.baseUrl}/repos/${owner}/${repo}/issues/${issueNumber}`;
|
|
34
|
+
const data = await this.request(url);
|
|
35
|
+
this.issueCache.set(cacheKey, {
|
|
36
|
+
data,
|
|
37
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
38
|
+
});
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Aggiunge un commento a una issue.
|
|
43
|
+
* Non usa cache (operazione di scrittura).
|
|
44
|
+
*/
|
|
45
|
+
async addComment(owner, repo, issueNumber, body) {
|
|
46
|
+
const url = `${this.baseUrl}/repos/${owner}/${repo}/issues/${issueNumber}/comments`;
|
|
47
|
+
return this.request(url, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify({ body })
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Restituisce le informazioni correnti sul rate limit.
|
|
54
|
+
*/
|
|
55
|
+
getRateLimit() {
|
|
56
|
+
return { ...this.rateLimit };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Svuota la cache delle issue.
|
|
60
|
+
*/
|
|
61
|
+
clearCache() {
|
|
62
|
+
this.issueCache.clear();
|
|
63
|
+
this.logger.info("Cache issue svuotata");
|
|
64
|
+
}
|
|
65
|
+
// ── Request engine con retry ─────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Esegue una richiesta HTTP verso le GitHub API.
|
|
68
|
+
* Gestisce automaticamente:
|
|
69
|
+
* - Headers di autenticazione
|
|
70
|
+
* - Parsing della risposta JSON
|
|
71
|
+
* - Aggiornamento rate limit
|
|
72
|
+
* - Retry su 403/429 con backoff esponenziale
|
|
73
|
+
*/
|
|
74
|
+
async request(url, options = {}) {
|
|
75
|
+
const headers = {
|
|
76
|
+
"Authorization": `token ${this.token}`,
|
|
77
|
+
"Accept": "application/vnd.github.v3+json",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"User-Agent": "kiro-memory-plugin-github/1.0.0"
|
|
80
|
+
};
|
|
81
|
+
let lastError = null;
|
|
82
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
83
|
+
if (this.rateLimit.remaining === 0) {
|
|
84
|
+
const waitMs = this.rateLimit.resetAt * 1e3 - Date.now();
|
|
85
|
+
if (waitMs > 0 && waitMs < 6e4) {
|
|
86
|
+
this.logger.warn(`Rate limit esaurito. Attendo ${Math.ceil(waitMs / 1e3)}s prima del reset.`);
|
|
87
|
+
await this.sleep(waitMs);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
...options,
|
|
93
|
+
headers: { ...headers, ...options.headers || {} }
|
|
94
|
+
});
|
|
95
|
+
this.updateRateLimit(response);
|
|
96
|
+
if (response.ok) {
|
|
97
|
+
return await response.json();
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 404) {
|
|
100
|
+
throw new Error(`Risorsa non trovata: ${url}`);
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 401) {
|
|
103
|
+
throw new Error("Token GitHub non valido o scaduto");
|
|
104
|
+
}
|
|
105
|
+
if ((response.status === 403 || response.status === 429) && attempt < this.maxRetries) {
|
|
106
|
+
const retryAfter = response.headers.get("retry-after");
|
|
107
|
+
const backoffMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
108
|
+
this.logger.warn(`Rate limit (${response.status}). Retry ${attempt + 1}/${this.maxRetries} tra ${Math.ceil(backoffMs / 1e3)}s`);
|
|
109
|
+
await this.sleep(backoffMs);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const errorBody = await response.text().catch(() => "corpo non leggibile");
|
|
113
|
+
throw new Error(`GitHub API errore ${response.status}: ${errorBody}`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
116
|
+
if (lastError.message.includes("non trovata") || lastError.message.includes("non valido") || lastError.message.includes("scaduto")) {
|
|
117
|
+
throw lastError;
|
|
118
|
+
}
|
|
119
|
+
if (attempt < this.maxRetries) {
|
|
120
|
+
const backoffMs = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
121
|
+
this.logger.warn(`Errore di rete, retry ${attempt + 1}/${this.maxRetries}: ${lastError.message}`);
|
|
122
|
+
await this.sleep(backoffMs);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw lastError || new Error(`Richiesta fallita dopo ${this.maxRetries} tentativi`);
|
|
128
|
+
}
|
|
129
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
130
|
+
/**
|
|
131
|
+
* Aggiorna le informazioni di rate limit dalla risposta HTTP.
|
|
132
|
+
*/
|
|
133
|
+
updateRateLimit(response) {
|
|
134
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
135
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
136
|
+
if (remaining !== null) {
|
|
137
|
+
this.rateLimit.remaining = parseInt(remaining, 10);
|
|
138
|
+
}
|
|
139
|
+
if (reset !== null) {
|
|
140
|
+
this.rateLimit.resetAt = parseInt(reset, 10);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Utility per await su un delay (usato per backoff e attesa rate limit).
|
|
145
|
+
*/
|
|
146
|
+
sleep(ms) {
|
|
147
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// src/plugins/github/issue-parser.ts
|
|
152
|
+
var KEYWORD_PATTERN = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+))?#(\d+)\b/gi;
|
|
153
|
+
var FULL_REF_PATTERN = /(?:^|[\s,(])([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)#(\d+)\b/g;
|
|
154
|
+
var STANDALONE_PATTERN = /(?:^|[\s,(:])#(\d+)\b/g;
|
|
155
|
+
function parseIssueReferences(text) {
|
|
156
|
+
if (!text || typeof text !== "string") return [];
|
|
157
|
+
const refs = /* @__PURE__ */ new Map();
|
|
158
|
+
let match;
|
|
159
|
+
KEYWORD_PATTERN.lastIndex = 0;
|
|
160
|
+
while ((match = KEYWORD_PATTERN.exec(text)) !== null) {
|
|
161
|
+
const owner = match[1] || void 0;
|
|
162
|
+
const repo = match[2] || void 0;
|
|
163
|
+
const number = parseInt(match[3], 10);
|
|
164
|
+
const keyword = match[0].split(/\s/)[0].toLowerCase();
|
|
165
|
+
const key = makeKey(owner, repo, number);
|
|
166
|
+
refs.set(key, { number, owner, repo, keyword });
|
|
167
|
+
}
|
|
168
|
+
FULL_REF_PATTERN.lastIndex = 0;
|
|
169
|
+
while ((match = FULL_REF_PATTERN.exec(text)) !== null) {
|
|
170
|
+
const owner = match[1];
|
|
171
|
+
const repo = match[2];
|
|
172
|
+
const number = parseInt(match[3], 10);
|
|
173
|
+
const key = makeKey(owner, repo, number);
|
|
174
|
+
if (!refs.has(key)) {
|
|
175
|
+
refs.set(key, { number, owner, repo });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
STANDALONE_PATTERN.lastIndex = 0;
|
|
179
|
+
while ((match = STANDALONE_PATTERN.exec(text)) !== null) {
|
|
180
|
+
const number = parseInt(match[1], 10);
|
|
181
|
+
const key = makeKey(void 0, void 0, number);
|
|
182
|
+
if (!refs.has(key) && !hasRefWithNumber(refs, number)) {
|
|
183
|
+
refs.set(key, { number });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return Array.from(refs.values());
|
|
187
|
+
}
|
|
188
|
+
function makeKey(owner, repo, number) {
|
|
189
|
+
if (owner && repo) {
|
|
190
|
+
return `${owner}/${repo}#${number}`;
|
|
191
|
+
}
|
|
192
|
+
return `#${number}`;
|
|
193
|
+
}
|
|
194
|
+
function hasRefWithNumber(refs, number) {
|
|
195
|
+
for (const ref of refs.values()) {
|
|
196
|
+
if (ref.number === number) return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/plugins/github/index.ts
|
|
202
|
+
var GitHubPlugin = class {
|
|
203
|
+
name = "kiro-memory-plugin-github";
|
|
204
|
+
version = "1.0.0";
|
|
205
|
+
description = "Integrazione GitHub: rileva issue references e commenta a fine sessione";
|
|
206
|
+
minKiroVersion = "2.0.0";
|
|
207
|
+
/** Client HTTP per le GitHub API */
|
|
208
|
+
client = null;
|
|
209
|
+
/** Logger iniettato dal registry */
|
|
210
|
+
logger = null;
|
|
211
|
+
/** Configurazione validata */
|
|
212
|
+
config = null;
|
|
213
|
+
/** Mappa issue linkate nella sessione corrente (chiave: "owner/repo#number") */
|
|
214
|
+
linkedIssues = /* @__PURE__ */ new Map();
|
|
215
|
+
/** Hook esposti al registry */
|
|
216
|
+
hooks = {
|
|
217
|
+
onObservation: async (obs) => this.handleObservation(obs),
|
|
218
|
+
onSessionEnd: async (session) => this.handleSessionEnd(session)
|
|
219
|
+
};
|
|
220
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
221
|
+
async init(context) {
|
|
222
|
+
this.logger = context.logger;
|
|
223
|
+
this.config = this.parseConfig(context.config);
|
|
224
|
+
if (!this.config.token) {
|
|
225
|
+
throw new Error('Configurazione mancante: "token" \xE8 obbligatorio per il plugin GitHub');
|
|
226
|
+
}
|
|
227
|
+
this.client = new GitHubClient(
|
|
228
|
+
{
|
|
229
|
+
token: this.config.token,
|
|
230
|
+
baseUrl: this.config.baseUrl
|
|
231
|
+
},
|
|
232
|
+
this.logger
|
|
233
|
+
);
|
|
234
|
+
this.logger.info("Plugin GitHub inizializzato");
|
|
235
|
+
if (this.config.repo) {
|
|
236
|
+
this.logger.info(`Repository default: ${this.config.repo}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async destroy() {
|
|
240
|
+
this.client?.clearCache();
|
|
241
|
+
this.linkedIssues.clear();
|
|
242
|
+
this.client = null;
|
|
243
|
+
this.config = null;
|
|
244
|
+
this.logger?.info("Plugin GitHub distrutto");
|
|
245
|
+
this.logger = null;
|
|
246
|
+
}
|
|
247
|
+
// ── Hook: onObservation ────────────────────────────────────────────────────
|
|
248
|
+
/**
|
|
249
|
+
* Rileva riferimenti issue nel titolo dell'osservazione.
|
|
250
|
+
* Accumula le issue trovate per il commento a fine sessione.
|
|
251
|
+
*/
|
|
252
|
+
async handleObservation(obs) {
|
|
253
|
+
if (!this.client || !this.config) return;
|
|
254
|
+
const refs = parseIssueReferences(obs.title);
|
|
255
|
+
if (refs.length === 0) return;
|
|
256
|
+
this.logger?.info(`Trovati ${refs.length} riferimenti issue in osservazione "${obs.title}"`);
|
|
257
|
+
for (const ref of refs) {
|
|
258
|
+
this.trackIssue(ref, obs.title);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ── Hook: onSessionEnd ─────────────────────────────────────────────────────
|
|
262
|
+
/**
|
|
263
|
+
* Commenta automaticamente sulle issue linkate con un riepilogo della sessione.
|
|
264
|
+
* Chiamato alla chiusura di ogni sessione.
|
|
265
|
+
*/
|
|
266
|
+
async handleSessionEnd(session) {
|
|
267
|
+
if (!this.client || !this.config) return;
|
|
268
|
+
if (this.config.autoComment === false) return;
|
|
269
|
+
if (this.linkedIssues.size === 0) return;
|
|
270
|
+
if (!session.summary) {
|
|
271
|
+
this.logger?.info("Nessun summary di sessione, skip commento su issue");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.logger?.info(`Fine sessione: commento su ${this.linkedIssues.size} issue linkate`);
|
|
275
|
+
const issues = Array.from(this.linkedIssues.values());
|
|
276
|
+
const results = await Promise.allSettled(
|
|
277
|
+
issues.map((issue) => this.commentOnIssue(issue, session))
|
|
278
|
+
);
|
|
279
|
+
let successi = 0;
|
|
280
|
+
let errori = 0;
|
|
281
|
+
for (const result of results) {
|
|
282
|
+
if (result.status === "fulfilled") {
|
|
283
|
+
successi++;
|
|
284
|
+
} else {
|
|
285
|
+
errori++;
|
|
286
|
+
this.logger?.warn(`Errore commento su issue: ${result.reason}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.logger?.info(`Commenti: ${successi} riusciti, ${errori} falliti`);
|
|
290
|
+
this.linkedIssues.clear();
|
|
291
|
+
}
|
|
292
|
+
// ── Metodi privati ─────────────────────────────────────────────────────────
|
|
293
|
+
/**
|
|
294
|
+
* Traccia una issue reference trovata in un'osservazione.
|
|
295
|
+
* Se la issue è già tracciata, aggiunge il titolo dell'osservazione.
|
|
296
|
+
*/
|
|
297
|
+
trackIssue(ref, observationTitle) {
|
|
298
|
+
const { owner, repo } = this.resolveOwnerRepo(ref);
|
|
299
|
+
if (!owner || !repo) {
|
|
300
|
+
this.logger?.warn(`Impossibile risolvere owner/repo per issue #${ref.number} \u2014 configura "repo" nel plugin`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const key = `${owner}/${repo}#${ref.number}`;
|
|
304
|
+
const existing = this.linkedIssues.get(key);
|
|
305
|
+
if (existing) {
|
|
306
|
+
existing.observationTitles.push(observationTitle);
|
|
307
|
+
if (ref.keyword) existing.keywords.add(ref.keyword);
|
|
308
|
+
} else {
|
|
309
|
+
this.linkedIssues.set(key, {
|
|
310
|
+
number: ref.number,
|
|
311
|
+
owner,
|
|
312
|
+
repo,
|
|
313
|
+
observationTitles: [observationTitle],
|
|
314
|
+
keywords: new Set(ref.keyword ? [ref.keyword] : [])
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Risolve owner e repo da un IssueReference.
|
|
320
|
+
* Usa il reference se specifico, altrimenti il default dalla configurazione.
|
|
321
|
+
*/
|
|
322
|
+
resolveOwnerRepo(ref) {
|
|
323
|
+
if (ref.owner && ref.repo) {
|
|
324
|
+
return { owner: ref.owner, repo: ref.repo };
|
|
325
|
+
}
|
|
326
|
+
if (this.config?.repo) {
|
|
327
|
+
const parts = this.config.repo.split("/");
|
|
328
|
+
if (parts.length === 2) {
|
|
329
|
+
return { owner: parts[0], repo: parts[1] };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Genera il corpo del commento e lo posta sulla issue.
|
|
336
|
+
*/
|
|
337
|
+
async commentOnIssue(issue, session) {
|
|
338
|
+
if (!this.client || !session.summary) return;
|
|
339
|
+
const body = this.formatComment(issue, session);
|
|
340
|
+
await this.client.addComment(
|
|
341
|
+
issue.owner,
|
|
342
|
+
issue.repo,
|
|
343
|
+
issue.number,
|
|
344
|
+
body
|
|
345
|
+
);
|
|
346
|
+
this.logger?.info(`Commento postato su ${issue.owner}/${issue.repo}#${issue.number}`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Formatta il corpo del commento per una issue.
|
|
350
|
+
*/
|
|
351
|
+
formatComment(issue, session) {
|
|
352
|
+
const lines = [];
|
|
353
|
+
lines.push(`### \u{1F9E0} Kiro Memory \u2014 Sessione \`${session.id}\``);
|
|
354
|
+
lines.push("");
|
|
355
|
+
lines.push(`**Progetto:** ${session.project}`);
|
|
356
|
+
lines.push("");
|
|
357
|
+
if (issue.keywords.size > 0) {
|
|
358
|
+
const keywords = Array.from(issue.keywords).join(", ");
|
|
359
|
+
lines.push(`**Azioni:** ${keywords}`);
|
|
360
|
+
lines.push("");
|
|
361
|
+
}
|
|
362
|
+
if (issue.observationTitles.length > 0) {
|
|
363
|
+
lines.push("**Osservazioni correlate:**");
|
|
364
|
+
for (const title of issue.observationTitles.slice(0, 10)) {
|
|
365
|
+
lines.push(`- ${title}`);
|
|
366
|
+
}
|
|
367
|
+
if (issue.observationTitles.length > 10) {
|
|
368
|
+
lines.push(`- _...e altre ${issue.observationTitles.length - 10}_`);
|
|
369
|
+
}
|
|
370
|
+
lines.push("");
|
|
371
|
+
}
|
|
372
|
+
lines.push("**Riepilogo sessione:**");
|
|
373
|
+
lines.push(session.summary || "_Nessun riepilogo disponibile_");
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push("---");
|
|
376
|
+
lines.push("_Commento generato automaticamente da [kiro-memory](https://github.com/Auriti-Labs/kiro-memory)_");
|
|
377
|
+
return lines.join("\n");
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Parsa la configurazione grezza del plugin.
|
|
381
|
+
* Valida i campi e restituisce un oggetto tipizzato.
|
|
382
|
+
*/
|
|
383
|
+
parseConfig(raw) {
|
|
384
|
+
return {
|
|
385
|
+
token: typeof raw["token"] === "string" ? raw["token"] : "",
|
|
386
|
+
repo: typeof raw["repo"] === "string" ? raw["repo"] : void 0,
|
|
387
|
+
baseUrl: typeof raw["baseUrl"] === "string" ? raw["baseUrl"] : void 0,
|
|
388
|
+
autoComment: raw["autoComment"] !== false
|
|
389
|
+
// default: true
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// ── Metodi esposti per testing ─────────────────────────────────────────────
|
|
393
|
+
/**
|
|
394
|
+
* Restituisce le issue attualmente tracciate nella sessione.
|
|
395
|
+
* @internal Usato solo nei test.
|
|
396
|
+
*/
|
|
397
|
+
_getLinkedIssues() {
|
|
398
|
+
return this.linkedIssues;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Restituisce il client HTTP interno.
|
|
402
|
+
* @internal Usato solo nei test.
|
|
403
|
+
*/
|
|
404
|
+
_getClient() {
|
|
405
|
+
return this.client;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
var index_default = new GitHubPlugin();
|
|
409
|
+
export {
|
|
410
|
+
GitHubPlugin,
|
|
411
|
+
index_default as default
|
|
412
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/plugins/github/issue-parser.ts
|
|
4
|
+
var KEYWORD_PATTERN = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+))?#(\d+)\b/gi;
|
|
5
|
+
var FULL_REF_PATTERN = /(?:^|[\s,(])([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)#(\d+)\b/g;
|
|
6
|
+
var STANDALONE_PATTERN = /(?:^|[\s,(:])#(\d+)\b/g;
|
|
7
|
+
function parseIssueReferences(text) {
|
|
8
|
+
if (!text || typeof text !== "string") return [];
|
|
9
|
+
const refs = /* @__PURE__ */ new Map();
|
|
10
|
+
let match;
|
|
11
|
+
KEYWORD_PATTERN.lastIndex = 0;
|
|
12
|
+
while ((match = KEYWORD_PATTERN.exec(text)) !== null) {
|
|
13
|
+
const owner = match[1] || void 0;
|
|
14
|
+
const repo = match[2] || void 0;
|
|
15
|
+
const number = parseInt(match[3], 10);
|
|
16
|
+
const keyword = match[0].split(/\s/)[0].toLowerCase();
|
|
17
|
+
const key = makeKey(owner, repo, number);
|
|
18
|
+
refs.set(key, { number, owner, repo, keyword });
|
|
19
|
+
}
|
|
20
|
+
FULL_REF_PATTERN.lastIndex = 0;
|
|
21
|
+
while ((match = FULL_REF_PATTERN.exec(text)) !== null) {
|
|
22
|
+
const owner = match[1];
|
|
23
|
+
const repo = match[2];
|
|
24
|
+
const number = parseInt(match[3], 10);
|
|
25
|
+
const key = makeKey(owner, repo, number);
|
|
26
|
+
if (!refs.has(key)) {
|
|
27
|
+
refs.set(key, { number, owner, repo });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
STANDALONE_PATTERN.lastIndex = 0;
|
|
31
|
+
while ((match = STANDALONE_PATTERN.exec(text)) !== null) {
|
|
32
|
+
const number = parseInt(match[1], 10);
|
|
33
|
+
const key = makeKey(void 0, void 0, number);
|
|
34
|
+
if (!refs.has(key) && !hasRefWithNumber(refs, number)) {
|
|
35
|
+
refs.set(key, { number });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Array.from(refs.values());
|
|
39
|
+
}
|
|
40
|
+
function makeKey(owner, repo, number) {
|
|
41
|
+
if (owner && repo) {
|
|
42
|
+
return `${owner}/${repo}#${number}`;
|
|
43
|
+
}
|
|
44
|
+
return `#${number}`;
|
|
45
|
+
}
|
|
46
|
+
function hasRefWithNumber(refs, number) {
|
|
47
|
+
for (const ref of refs.values()) {
|
|
48
|
+
if (ref.number === number) return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
parseIssueReferences
|
|
54
|
+
};
|