@clawmem-ai/clawmem 0.1.8 → 0.1.10
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 +25 -23
- package/openclaw.plugin.json +17 -85
- package/package.json +1 -1
- package/src/config.test.ts +82 -0
- package/src/config.ts +26 -8
- package/src/conversation.test.ts +14 -20
- package/src/conversation.ts +180 -21
- package/src/github-client.ts +55 -1
- package/src/memory.test.ts +371 -0
- package/src/memory.ts +368 -47
- package/src/service.ts +517 -34
- package/src/state.ts +16 -0
- package/src/types.ts +22 -5
- package/src/utils.ts +13 -0
package/src/conversation.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
5
|
-
import { AGENT_LABEL_PREFIX, DEFAULT_LABELS,
|
|
5
|
+
import { AGENT_LABEL_PREFIX, DEFAULT_LABELS, SESSION_TITLE_PREFIX, extractLabelNames } from "./config.js";
|
|
6
6
|
import type { GitHubIssueClient } from "./github-client.js";
|
|
7
7
|
import { normalizeMessages, readTranscriptSnapshot } from "./transcript.js";
|
|
8
8
|
import type { ClawMemPluginConfig, NormalizedMessage, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
9
9
|
import { fmtTranscript, localDate, localDateTime, sha256, subKey } from "./utils.js";
|
|
10
|
-
import { stringifyFlatYaml } from "./yaml.js";
|
|
10
|
+
import { parseFlatYaml, stringifyFlatYaml } from "./yaml.js";
|
|
11
11
|
|
|
12
12
|
export class ConversationMirror {
|
|
13
13
|
constructor(private readonly client: GitHubIssueClient, private readonly api: OpenClawPluginApi, private readonly config: ClawMemPluginConfig) {}
|
|
@@ -47,7 +47,7 @@ export class ConversationMirror {
|
|
|
47
47
|
);
|
|
48
48
|
this.resetIssueBinding(session);
|
|
49
49
|
}
|
|
50
|
-
// Use
|
|
50
|
+
// Use placeholder title; real title is generated by LLM once enough messages are available.
|
|
51
51
|
const title = deriveInitialTitle(snapshot.messages, session.sessionId);
|
|
52
52
|
const labels = this.buildLabels(session, snapshot, false);
|
|
53
53
|
const body = this.renderBody(session, snapshot, "pending", false);
|
|
@@ -55,6 +55,7 @@ export class ConversationMirror {
|
|
|
55
55
|
const issue = await this.client.createIssue({ title, body, labels });
|
|
56
56
|
session.issueNumber = issue.number;
|
|
57
57
|
session.issueTitle = issue.title ?? title;
|
|
58
|
+
session.titleSource = "placeholder";
|
|
58
59
|
session.lastSummaryHash = sha256(`${title}\n${body}\nopen`);
|
|
59
60
|
session.createdAt = new Date().toISOString();
|
|
60
61
|
session.updatedAt = session.createdAt;
|
|
@@ -69,6 +70,7 @@ export class ConversationMirror {
|
|
|
69
70
|
if (hash === session.lastSummaryHash) return;
|
|
70
71
|
await this.client.updateIssue(session.issueNumber, { title, body, ...(closed ? { state: "closed" as const } : {}) });
|
|
71
72
|
session.issueTitle = title;
|
|
73
|
+
if (titleOverride?.trim()) session.titleSource = "llm";
|
|
72
74
|
session.lastSummaryHash = hash;
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -99,17 +101,17 @@ export class ConversationMirror {
|
|
|
99
101
|
"Do not include markdown, bullet points, or analysis.",
|
|
100
102
|
"",
|
|
101
103
|
"Title rules:",
|
|
102
|
-
"- Under 50 characters,
|
|
103
|
-
"- Should
|
|
104
|
+
"- Under 50 characters, accurately describe the main topic or task.",
|
|
105
|
+
"- Should let someone immediately know what the conversation is about.",
|
|
104
106
|
"- Must be in the same language as the majority of the conversation content.",
|
|
105
|
-
"- Good:
|
|
107
|
+
"- Good: precise, descriptive, specific. Bad: vague, overly creative, generic.",
|
|
106
108
|
"", "<conversation>", fmtTranscript(snapshot.messages), "</conversation>",
|
|
107
109
|
].join("\n");
|
|
108
110
|
try {
|
|
109
111
|
const run = await subagent.run({
|
|
110
112
|
sessionKey, message, deliver: false, lane: "clawmem-summary",
|
|
111
113
|
idempotencyKey: sha256(`${session.sessionId}:${snapshot.messages.length}:summary-v2`),
|
|
112
|
-
extraSystemPrompt: "You summarize
|
|
114
|
+
extraSystemPrompt: "You summarize conversations and generate accurate, descriptive titles. Output JSON only with string fields summary and title.",
|
|
113
115
|
});
|
|
114
116
|
const wait = await subagent.waitForRun({ runId: run.runId, timeoutMs: this.config.summaryWaitTimeoutMs });
|
|
115
117
|
if (wait.status === "timeout") throw new Error("summary subagent timed out");
|
|
@@ -121,20 +123,158 @@ export class ConversationMirror {
|
|
|
121
123
|
} finally { subagent.deleteSession({ sessionKey, deleteTranscript: true }).catch(() => {}); }
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
/** If the title has not yet been generated by LLM, generate an accurate title from the full conversation and update the issue. */
|
|
127
|
+
async syncTitle(session: SessionMirrorState, snapshot: TranscriptSnapshot): Promise<void> {
|
|
128
|
+
if (!session.issueNumber) return;
|
|
129
|
+
if (session.titleSource === "llm") return;
|
|
130
|
+
if (snapshot.messages.length < 2) return;
|
|
131
|
+
try {
|
|
132
|
+
const title = await this.generateTitle(session, snapshot);
|
|
133
|
+
if (title) {
|
|
134
|
+
await this.client.updateIssue(session.issueNumber, { title });
|
|
135
|
+
session.issueTitle = title;
|
|
136
|
+
session.titleSource = "llm";
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
this.api.logger.warn(`clawmem: title sync failed: ${String(e)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Generate an accurate, descriptive title from the full conversation content via LLM. */
|
|
144
|
+
async generateTitle(session: SessionMirrorState, snapshot: TranscriptSnapshot): Promise<string | undefined> {
|
|
145
|
+
if (snapshot.messages.length === 0) return undefined;
|
|
146
|
+
const subagent = this.api.runtime.subagent;
|
|
147
|
+
const sessionKey = subKey(session, "title");
|
|
148
|
+
const message = [
|
|
149
|
+
"Generate a short, accurate title for the following conversation.",
|
|
150
|
+
'Return valid JSON only in the form {"title":"..."}',
|
|
151
|
+
"",
|
|
152
|
+
"Title rules:",
|
|
153
|
+
"- Under 50 characters.",
|
|
154
|
+
"- Accurately describe the main topic or task of the conversation.",
|
|
155
|
+
"- Should let someone immediately know what the conversation is about.",
|
|
156
|
+
"- Must be in the same language as the majority of the conversation content.",
|
|
157
|
+
"- Good: precise, descriptive, specific. Bad: vague, overly creative, generic.",
|
|
158
|
+
"",
|
|
159
|
+
"<conversation>",
|
|
160
|
+
fmtTranscript(snapshot.messages),
|
|
161
|
+
"</conversation>",
|
|
162
|
+
].join("\n");
|
|
163
|
+
try {
|
|
164
|
+
const run = await subagent.run({
|
|
165
|
+
sessionKey, message, deliver: false, lane: "clawmem-title",
|
|
166
|
+
idempotencyKey: sha256(`${session.sessionId}:${snapshot.messages.length}:title-v1`),
|
|
167
|
+
extraSystemPrompt: "You generate accurate, descriptive titles for conversations. Output JSON only with a string field title.",
|
|
168
|
+
});
|
|
169
|
+
const wait = await subagent.waitForRun({ runId: run.runId, timeoutMs: 30000 });
|
|
170
|
+
if (wait.status === "timeout" || wait.status === "error") return undefined;
|
|
171
|
+
const msgs = normalizeMessages((await subagent.getSessionMessages({ sessionKey, limit: 10 })).messages);
|
|
172
|
+
const text = [...msgs].reverse().find((e) => e.role === "assistant" && e.text.trim())?.text;
|
|
173
|
+
if (!text) return undefined;
|
|
174
|
+
return parseTitle(text);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.api.logger.warn(`clawmem: title generation failed: ${String(e)}`);
|
|
177
|
+
return undefined;
|
|
178
|
+
} finally {
|
|
179
|
+
subagent.deleteSession({ sessionKey, deleteTranscript: true }).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Re-title all existing conversation issues. Uses summary when available, falls back to reading comments. */
|
|
184
|
+
async retitleConversations(): Promise<{ updated: number; skipped: number; failed: number; retitledIssues: number[] }> {
|
|
185
|
+
let updated = 0, skipped = 0, failed = 0;
|
|
186
|
+
const retitledIssues: number[] = [];
|
|
187
|
+
let page = 1;
|
|
188
|
+
while (true) {
|
|
189
|
+
const issues = await this.client.listIssues({ labels: ["type:conversation"], state: "all", page, perPage: 50 });
|
|
190
|
+
if (issues.length === 0) break;
|
|
191
|
+
for (const issue of issues) {
|
|
192
|
+
try {
|
|
193
|
+
const yaml = parseFlatYaml(issue.body || "");
|
|
194
|
+
const summary = yaml.summary;
|
|
195
|
+
let titleInput: string | undefined;
|
|
196
|
+
if (summary && summary !== "pending" && !summary.startsWith("failed:")) {
|
|
197
|
+
titleInput = summary;
|
|
198
|
+
} else {
|
|
199
|
+
// No usable summary — reconstruct conversation from issue comments.
|
|
200
|
+
const comments = await this.client.listComments(issue.number, { perPage: 50 });
|
|
201
|
+
const conversationText = comments
|
|
202
|
+
.map((c) => c.body?.trim())
|
|
203
|
+
.filter((b): b is string => Boolean(b))
|
|
204
|
+
.join("\n\n");
|
|
205
|
+
if (conversationText.length >= 20) {
|
|
206
|
+
// Cap to avoid excessive token usage in LLM call.
|
|
207
|
+
titleInput = conversationText.length > 4000 ? conversationText.slice(0, 4000) + "\n..." : conversationText;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!titleInput) { skipped++; continue; }
|
|
211
|
+
const title = await this.generateTitleFromText(titleInput, `retitle-${issue.number}`);
|
|
212
|
+
if (!title) { skipped++; continue; }
|
|
213
|
+
await this.client.updateIssue(issue.number, { title });
|
|
214
|
+
this.api.logger.info?.(`clawmem: retitled issue #${issue.number} -> "${title}"`);
|
|
215
|
+
retitledIssues.push(issue.number);
|
|
216
|
+
updated++;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
this.api.logger.warn(`clawmem: retitle failed for issue #${issue.number}: ${String(e)}`);
|
|
219
|
+
failed++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (issues.length < 50) break;
|
|
223
|
+
page++;
|
|
224
|
+
}
|
|
225
|
+
return { updated, skipped, failed, retitledIssues };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async generateTitleFromText(text: string, uniqueKey: string): Promise<string | undefined> {
|
|
229
|
+
const subagent = this.api.runtime.subagent;
|
|
230
|
+
const sessionKey = `clawmem-${uniqueKey}`;
|
|
231
|
+
const message = [
|
|
232
|
+
"Generate a short, accurate title based on the following conversation content.",
|
|
233
|
+
'Return valid JSON only in the form {"title":"..."}',
|
|
234
|
+
"",
|
|
235
|
+
"Title rules:",
|
|
236
|
+
"- Under 50 characters.",
|
|
237
|
+
"- Accurately describe the main topic or task.",
|
|
238
|
+
"- Should let someone immediately know what the conversation was about.",
|
|
239
|
+
"- Must be in the same language as the content.",
|
|
240
|
+
"- Good: precise, descriptive, specific. Bad: vague, overly creative, generic.",
|
|
241
|
+
"",
|
|
242
|
+
"<content>",
|
|
243
|
+
text,
|
|
244
|
+
"</content>",
|
|
245
|
+
].join("\n");
|
|
246
|
+
try {
|
|
247
|
+
const run = await subagent.run({
|
|
248
|
+
sessionKey, message, deliver: false, lane: "clawmem-retitle",
|
|
249
|
+
idempotencyKey: sha256(`retitle:${uniqueKey}:${text.slice(0, 200)}`),
|
|
250
|
+
extraSystemPrompt: "You generate accurate, descriptive titles. Output JSON only with a string field title.",
|
|
251
|
+
});
|
|
252
|
+
const wait = await subagent.waitForRun({ runId: run.runId, timeoutMs: 30000 });
|
|
253
|
+
if (wait.status === "timeout" || wait.status === "error") return undefined;
|
|
254
|
+
const msgs = normalizeMessages((await subagent.getSessionMessages({ sessionKey, limit: 10 })).messages);
|
|
255
|
+
const raw = [...msgs].reverse().find((e) => e.role === "assistant" && e.text.trim())?.text;
|
|
256
|
+
if (!raw) return undefined;
|
|
257
|
+
return parseTitle(raw);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
this.api.logger.warn(`clawmem: title generation from text failed (${uniqueKey}): ${String(e)}`);
|
|
260
|
+
return undefined;
|
|
261
|
+
} finally {
|
|
262
|
+
subagent.deleteSession({ sessionKey, deleteTranscript: true }).catch(() => {});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private buildLabels(session: SessionMirrorState, _snapshot: TranscriptSnapshot, _closed: boolean): string[] {
|
|
267
|
+
const labels = new Set([...DEFAULT_LABELS, "type:conversation", `session:${session.sessionId}`]);
|
|
127
268
|
if (session.agentId) labels.add(`${AGENT_LABEL_PREFIX}${session.agentId}`);
|
|
128
|
-
labels.add(closed ? LABEL_CLOSED : LABEL_ACTIVE);
|
|
129
269
|
return [...labels].filter((l) => l.trim().length > 0);
|
|
130
270
|
}
|
|
131
271
|
|
|
132
|
-
private renderBody(session: SessionMirrorState, snapshot: TranscriptSnapshot, summary: string,
|
|
272
|
+
private renderBody(session: SessionMirrorState, snapshot: TranscriptSnapshot, summary: string, _closed: boolean): string {
|
|
133
273
|
const dates = this.resolveDates(session, snapshot.messages);
|
|
134
274
|
return stringifyFlatYaml([
|
|
135
275
|
["type", "conversation"], ["session_id", session.sessionId], ["date", dates.date],
|
|
136
276
|
["start_at", dates.startAt], ["end_at", dates.endAt],
|
|
137
|
-
["
|
|
277
|
+
["summary", summary],
|
|
138
278
|
]);
|
|
139
279
|
}
|
|
140
280
|
|
|
@@ -180,6 +320,7 @@ export class ConversationMirror {
|
|
|
180
320
|
private resetIssueBinding(session: SessionMirrorState): void {
|
|
181
321
|
session.issueNumber = undefined;
|
|
182
322
|
session.issueTitle = undefined;
|
|
323
|
+
session.titleSource = undefined;
|
|
183
324
|
session.lastSummaryHash = undefined;
|
|
184
325
|
session.lastMirroredCount = 0;
|
|
185
326
|
session.turnCount = 0;
|
|
@@ -192,14 +333,8 @@ function isNotFoundError(error: unknown): boolean {
|
|
|
192
333
|
const text = String(error);
|
|
193
334
|
return text.includes("HTTP 404");
|
|
194
335
|
}
|
|
195
|
-
/** Derive a conversation title
|
|
196
|
-
export function deriveInitialTitle(
|
|
197
|
-
const firstUserMsg = messages.find((m) => m.role === "user")?.text?.trim() ?? "";
|
|
198
|
-
// Strip markdown, collapse whitespace.
|
|
199
|
-
const clean = firstUserMsg.replace(/[#*`~>|]/g, "").replace(/\s+/g, " ").trim();
|
|
200
|
-
if (clean.length >= 5) {
|
|
201
|
-
return clean.length <= 50 ? clean : clean.slice(0, 49).trimEnd() + "…";
|
|
202
|
-
}
|
|
336
|
+
/** Derive an initial placeholder title for a new conversation. The real title is generated by LLM once enough messages are available. */
|
|
337
|
+
export function deriveInitialTitle(_messages: NormalizedMessage[], sessionId: string): string {
|
|
203
338
|
return `${SESSION_TITLE_PREFIX}${sessionId}`;
|
|
204
339
|
}
|
|
205
340
|
|
|
@@ -231,3 +366,27 @@ function parseSummaryAndTitle(raw: string): { summary: string; title?: string }
|
|
|
231
366
|
if (f?.[1]) { const nested = tryParse(f[1].trim()); if (nested) return nested; }
|
|
232
367
|
return { summary: t };
|
|
233
368
|
}
|
|
369
|
+
|
|
370
|
+
function parseTitle(raw: string): string | undefined {
|
|
371
|
+
const tryParse = (s: string): string | undefined => {
|
|
372
|
+
try {
|
|
373
|
+
const p = JSON.parse(s) as { title?: unknown };
|
|
374
|
+
return typeof p?.title === "string" && p.title.trim() ? p.title.trim() : undefined;
|
|
375
|
+
} catch {
|
|
376
|
+
const i = s.indexOf("{"), j = s.lastIndexOf("}");
|
|
377
|
+
if (i >= 0 && j > i) {
|
|
378
|
+
try {
|
|
379
|
+
const p = JSON.parse(s.slice(i, j + 1)) as { title?: unknown };
|
|
380
|
+
return typeof p?.title === "string" && p.title.trim() ? p.title.trim() : undefined;
|
|
381
|
+
} catch { return undefined; }
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const t = raw.trim();
|
|
387
|
+
const direct = tryParse(t);
|
|
388
|
+
if (direct) return direct;
|
|
389
|
+
const f = /^```(?:json)?\s*([\s\S]*?)```$/i.exec(t);
|
|
390
|
+
if (f?.[1]) return tryParse(f[1].trim());
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
package/src/github-client.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// GitHub Issues API client for clawmem. No label caching — idempotent create-if-absent.
|
|
2
2
|
import { resolveLabelColor, labelDescription, extractLabelNames, isManagedLabel } from "./config.js";
|
|
3
|
-
import type { AnonymousSessionResponse, ClawMemResolvedRoute } from "./types.js";
|
|
3
|
+
import type { AgentRegistrationResponse, AnonymousSessionResponse, ClawMemResolvedRoute } from "./types.js";
|
|
4
4
|
|
|
5
5
|
type IssueResponse = { number: number; title?: string; body?: string; state?: string; labels?: Array<{ name?: string } | string> };
|
|
6
|
+
type SearchIssuesResponse = { items?: IssueResponse[]; total_count?: number; incomplete_results?: boolean };
|
|
7
|
+
type CommentResponse = { id?: number; body?: string; created_at?: string };
|
|
8
|
+
type LabelResponse = { name?: string; color?: string; description?: string };
|
|
9
|
+
type RepoResponse = { name?: string; full_name?: string; description?: string; private?: boolean; owner?: { login?: string } };
|
|
6
10
|
type ReqOpts = { allowNotFound?: boolean; allowValidationError?: boolean; omitAuth?: boolean };
|
|
7
11
|
|
|
8
12
|
export class GitHubIssueClient {
|
|
9
13
|
constructor(private readonly config: ClawMemResolvedRoute, private readonly log: { warn?: (msg: string) => void }) {}
|
|
10
14
|
|
|
15
|
+
repo(): string | undefined {
|
|
16
|
+
return this.config.repo?.trim() || undefined;
|
|
17
|
+
}
|
|
18
|
+
defaultRepo(): string | undefined {
|
|
19
|
+
return this.config.defaultRepo?.trim() || undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
async createIssue(params: { title: string; body: string; labels: string[] }): Promise<IssueResponse> {
|
|
12
23
|
return this.req<IssueResponse>(this.repoPath("issues"), { method: "POST", body: JSON.stringify(params) });
|
|
13
24
|
}
|
|
@@ -20,12 +31,46 @@ export class GitHubIssueClient {
|
|
|
20
31
|
async createComment(issueNumber: number, body: string): Promise<void> {
|
|
21
32
|
await this.req(this.repoPath(`issues/${issueNumber}/comments`), { method: "POST", body: JSON.stringify({ body }) });
|
|
22
33
|
}
|
|
34
|
+
async listComments(issueNumber: number, params?: { page?: number; perPage?: number }): Promise<CommentResponse[]> {
|
|
35
|
+
const q = new URLSearchParams();
|
|
36
|
+
q.set("page", String(params?.page ?? 1));
|
|
37
|
+
q.set("per_page", String(params?.perPage ?? 100));
|
|
38
|
+
return this.req<CommentResponse[]>(`${this.repoPath(`issues/${issueNumber}/comments`)}?${q}`, { method: "GET" });
|
|
39
|
+
}
|
|
23
40
|
async listIssues(params: { labels?: string[]; state?: "open" | "closed" | "all"; page?: number; perPage?: number }): Promise<IssueResponse[]> {
|
|
24
41
|
const q = new URLSearchParams();
|
|
25
42
|
q.set("state", params.state ?? "open"); q.set("page", String(params.page ?? 1)); q.set("per_page", String(params.perPage ?? 100));
|
|
26
43
|
if (params.labels?.length) q.set("labels", params.labels.join(","));
|
|
27
44
|
return this.req<IssueResponse[]>(`${this.repoPath("issues")}?${q}`, { method: "GET" });
|
|
28
45
|
}
|
|
46
|
+
async searchIssues(query: string, params?: { page?: number; perPage?: number }): Promise<IssueResponse[]> {
|
|
47
|
+
const q = new URLSearchParams();
|
|
48
|
+
q.set("q", query);
|
|
49
|
+
q.set("page", String(params?.page ?? 1));
|
|
50
|
+
q.set("per_page", String(params?.perPage ?? 100));
|
|
51
|
+
const res = await this.req<SearchIssuesResponse>(`search/issues?${q}`, { method: "GET" });
|
|
52
|
+
return Array.isArray(res?.items) ? res.items : [];
|
|
53
|
+
}
|
|
54
|
+
async listLabels(params?: { page?: number; perPage?: number }): Promise<LabelResponse[]> {
|
|
55
|
+
const q = new URLSearchParams();
|
|
56
|
+
q.set("page", String(params?.page ?? 1));
|
|
57
|
+
q.set("per_page", String(params?.perPage ?? 100));
|
|
58
|
+
return this.req<LabelResponse[]>(`${this.repoPath("labels")}?${q}`, { method: "GET" });
|
|
59
|
+
}
|
|
60
|
+
async listUserRepos(): Promise<RepoResponse[]> {
|
|
61
|
+
return this.req<RepoResponse[]>("user/repos", { method: "GET" });
|
|
62
|
+
}
|
|
63
|
+
async createUserRepo(params: { name: string; description?: string; private?: boolean; autoInit?: boolean }): Promise<RepoResponse> {
|
|
64
|
+
return this.req<RepoResponse>("user/repos", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
name: params.name,
|
|
68
|
+
...(params.description ? { description: params.description } : {}),
|
|
69
|
+
private: params.private ?? true,
|
|
70
|
+
auto_init: params.autoInit ?? false,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
29
74
|
async ensureLabels(labels: string[]): Promise<void> {
|
|
30
75
|
for (const label of labels) {
|
|
31
76
|
if (!label.trim()) continue;
|
|
@@ -44,6 +89,15 @@ export class GitHubIssueClient {
|
|
|
44
89
|
async updateRepoDescription(description: string): Promise<void> {
|
|
45
90
|
await this.req(this.repoPath("").replace(/\/$/, ""), { method: "PATCH", body: JSON.stringify({ description }) });
|
|
46
91
|
}
|
|
92
|
+
async registerAgent(prefixLogin: string, defaultRepoName: string): Promise<AgentRegistrationResponse> {
|
|
93
|
+
return this.req<AgentRegistrationResponse>("agents", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
prefix_login: prefixLogin,
|
|
97
|
+
default_repo_name: defaultRepoName,
|
|
98
|
+
}),
|
|
99
|
+
}, { omitAuth: true });
|
|
100
|
+
}
|
|
47
101
|
async createAnonymousSession(locale?: string): Promise<AnonymousSessionResponse> {
|
|
48
102
|
const body = locale ? JSON.stringify({ locale }) : undefined;
|
|
49
103
|
return this.req<AnonymousSessionResponse>("anonymous/session", { method: "POST", ...(body ? { body } : {}) }, { omitAuth: true });
|