pi-para 0.6.7 → 0.7.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.
Files changed (124) hide show
  1. package/ARCHITECTURE.md +4 -6
  2. package/CHANGELOG.md +14 -0
  3. package/CONFIGURATION.md +1 -16
  4. package/README.md +2 -13
  5. package/SETUP.md +1 -7
  6. package/STRUCTURE.md +7 -18
  7. package/dist/cli-legacy.d.ts +2 -1
  8. package/dist/cli-legacy.d.ts.map +1 -1
  9. package/dist/cli-legacy.js +13 -25
  10. package/dist/cli-legacy.js.map +1 -1
  11. package/dist/cli.d.ts +2 -24
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +29 -271
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +1 -28
  17. package/dist/commands.js.map +1 -1
  18. package/dist/config.d.ts +1 -5
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +0 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -64
  24. package/dist/index.js.map +1 -1
  25. package/dist/link-utils.d.ts +1 -1
  26. package/dist/link-utils.js +1 -1
  27. package/dist/settings.d.ts +0 -3
  28. package/dist/settings.d.ts.map +1 -1
  29. package/dist/settings.js +0 -11
  30. package/dist/settings.js.map +1 -1
  31. package/dist/tools/edit-read.d.ts +18 -0
  32. package/dist/tools/edit-read.d.ts.map +1 -0
  33. package/dist/tools/edit-read.js +161 -0
  34. package/dist/tools/edit-read.js.map +1 -0
  35. package/dist/tools/ingest.d.ts +12 -0
  36. package/dist/tools/ingest.d.ts.map +1 -0
  37. package/dist/tools/ingest.js +48 -0
  38. package/dist/tools/ingest.js.map +1 -0
  39. package/dist/tools/lint.d.ts +10 -0
  40. package/dist/tools/lint.d.ts.map +1 -0
  41. package/dist/tools/lint.js +57 -0
  42. package/dist/tools/lint.js.map +1 -0
  43. package/dist/tools/migrate.d.ts +6 -0
  44. package/dist/tools/migrate.d.ts.map +1 -0
  45. package/dist/tools/migrate.js +36 -0
  46. package/dist/tools/migrate.js.map +1 -0
  47. package/dist/tools/move.d.ts +9 -0
  48. package/dist/tools/move.d.ts.map +1 -0
  49. package/dist/tools/move.js +61 -0
  50. package/dist/tools/move.js.map +1 -0
  51. package/dist/tools/query.d.ts +12 -0
  52. package/dist/tools/query.d.ts.map +1 -0
  53. package/dist/tools/query.js +47 -0
  54. package/dist/tools/query.js.map +1 -0
  55. package/dist/tools/schemas.d.ts +109 -0
  56. package/dist/tools/schemas.d.ts.map +1 -0
  57. package/dist/tools/schemas.js +80 -0
  58. package/dist/tools/schemas.js.map +1 -0
  59. package/dist/tools/shared.d.ts +14 -0
  60. package/dist/tools/shared.d.ts.map +1 -0
  61. package/dist/tools/shared.js +25 -0
  62. package/dist/tools/shared.js.map +1 -0
  63. package/dist/tools/summarize.d.ts +8 -0
  64. package/dist/tools/summarize.d.ts.map +1 -0
  65. package/dist/tools/summarize.js +82 -0
  66. package/dist/tools/summarize.js.map +1 -0
  67. package/dist/tools/write.d.ts +23 -0
  68. package/dist/tools/write.d.ts.map +1 -0
  69. package/dist/tools/write.js +215 -0
  70. package/dist/tools/write.js.map +1 -0
  71. package/dist/tools.d.ts +1 -8
  72. package/dist/tools.d.ts.map +1 -1
  73. package/dist/tools.js +13 -795
  74. package/dist/tools.js.map +1 -1
  75. package/package.json +2 -6
  76. package/src/cli-legacy.ts +14 -26
  77. package/src/cli.ts +29 -293
  78. package/src/commands.ts +0 -21
  79. package/src/config.ts +1 -6
  80. package/src/index.ts +5 -66
  81. package/src/link-utils.ts +1 -1
  82. package/src/settings.ts +0 -14
  83. package/src/tools/edit-read.ts +193 -0
  84. package/src/tools/ingest.ts +63 -0
  85. package/src/tools/lint.ts +74 -0
  86. package/src/tools/migrate.ts +44 -0
  87. package/src/tools/move.ts +79 -0
  88. package/src/tools/query.ts +63 -0
  89. package/src/tools/schemas.ts +157 -0
  90. package/src/tools/shared.ts +35 -0
  91. package/src/tools/summarize.ts +89 -0
  92. package/src/tools/write.ts +252 -0
  93. package/src/tools.ts +18 -1046
  94. package/dist/daemon.d.ts +0 -49
  95. package/dist/daemon.d.ts.map +0 -1
  96. package/dist/daemon.js +0 -226
  97. package/dist/daemon.js.map +0 -1
  98. package/dist/link-discovery.d.ts +0 -40
  99. package/dist/link-discovery.d.ts.map +0 -1
  100. package/dist/link-discovery.js +0 -122
  101. package/dist/link-discovery.js.map +0 -1
  102. package/dist/maintainer.d.ts +0 -24
  103. package/dist/maintainer.d.ts.map +0 -1
  104. package/dist/maintainer.js +0 -358
  105. package/dist/maintainer.js.map +0 -1
  106. package/dist/watcher.d.ts +0 -25
  107. package/dist/watcher.d.ts.map +0 -1
  108. package/dist/watcher.js +0 -84
  109. package/dist/watcher.js.map +0 -1
  110. package/dist/webui/client/dist/index.html +0 -1885
  111. package/dist/webui/server.d.ts +0 -31
  112. package/dist/webui/server.d.ts.map +0 -1
  113. package/dist/webui/server.js +0 -566
  114. package/dist/webui/server.js.map +0 -1
  115. package/scripts/build-assets.mjs +0 -17
  116. package/src/daemon.ts +0 -288
  117. package/src/link-discovery.ts +0 -170
  118. package/src/maintainer.ts +0 -404
  119. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  120. package/src/python/minimax_lm.py +0 -198
  121. package/src/python/rlm_capture.py +0 -215
  122. package/src/watcher.ts +0 -101
  123. package/src/webui/client/dist/index.html +0 -1885
  124. package/src/webui/server.ts +0 -765
package/src/maintainer.ts DELETED
@@ -1,404 +0,0 @@
1
- /**
2
- * Wiki maintenance agent - uses LLM to intelligently maintain the wiki.
3
- *
4
- * Replaces brittle code-based maintenance (link discovery, dedup detection,
5
- * tag canonicalization, category review) with an LLM agent that understands
6
- * content and can make smarter decisions.
7
- *
8
- * Deterministic operations (secret redaction, schema validation, index rebuild)
9
- * stay in code. This agent handles everything that needs intelligence.
10
- */
11
-
12
- import { Agent } from "@earendil-works/pi-agent-core";
13
- import type { AgentTool } from "@earendil-works/pi-agent-core";
14
- import type { Model } from "@earendil-works/pi-ai";
15
- import type { QMDStore } from "qmd-engine";
16
- import { Type } from "typebox";
17
- import { StringEnum } from "@earendil-works/pi-ai";
18
-
19
- import {
20
- readPage,
21
- writePage,
22
- listPages,
23
- writeIndex,
24
- gitCommit,
25
- PARA_CATEGORIES,
26
- } from "./wiki.js";
27
- import type { WikiPage, ParaCategory, PageRef } from "./wiki.js";
28
- import { validateFrontmatter } from "./frontmatter.js";
29
- import { searchWiki, reindex } from "./store.js";
30
- import { extractWikilinks, syncFrontmatterLinks } from "./link-utils.js";
31
- import { normalizeTags, normalizeScopes } from "./tag-registry.js";
32
- import { redactSecrets } from "./redact.js";
33
- import { lintWiki } from "./lint.js";
34
-
35
- // -- Types -------------------------------------------------------------------
36
-
37
- export interface MaintenanceResult {
38
- pagesUpdated: number;
39
- pagesMerged: number;
40
- linksAdded: number;
41
- issuesFixed: number;
42
- summary: string;
43
- }
44
-
45
- // -- Maintenance prompt ------------------------------------------------------
46
-
47
- const MAINTENANCE_SYSTEM_PROMPT = `You are a wiki maintenance agent. Your job is to keep a PARA knowledge base healthy, well-linked, and free of duplicates.
48
-
49
- You have tools to inspect and modify the wiki:
50
- - wiki_list: List all pages with their scope, tags, and link counts
51
- - wiki_read: Read a page's full content
52
- - wiki_query: Search for pages by keyword
53
- - wiki_write: Update a page (merge content, add links, fix category)
54
- - wiki_merge: Merge two pages into one (keeps the target, deletes the source)
55
- - wiki_lint: Run automated checks and get a report of issues
56
-
57
- Your maintenance tasks (in priority order):
58
-
59
- 1. DUPLICATE DETECTION & MERGING
60
- - Find pages covering the same topic with different slugs
61
- - Merge them: combine content into the older/more complete page, delete the duplicate
62
- - Examples: "pi-para daemon architecture" and "pi-para daemon" are duplicates
63
-
64
- 2. WIKILINK DISCOVERY
65
- - Find pages that discuss related topics but aren't linked
66
- - Add [[wikilinks]] in body text and ## Connections sections
67
- - Every page should link to at least 2 related pages
68
-
69
- 3. CATEGORY REVIEW
70
- - resources/ should have reference docs, architecture, how-tos, patterns
71
- - areas/ should have ongoing responsibilities only
72
- - projects/ should ONLY have actual goals with end dates (almost never used)
73
- - Move miscategorized pages
74
-
75
- 4. TAG CLEANUP
76
- - Remove redundant tags (tags that duplicate scope values)
77
- - Canonicalize similar tags (e.g. "capture" and "session-capture" → keep "session-capture")
78
- - Ensure tags are kebab-case
79
-
80
- 5. CONTENT QUALITY
81
- - Pages should have: Topic, Key Facts, Connections sections at minimum
82
- - Flag pages with very thin content for expansion
83
- - Flag pages with no ## Connections section
84
-
85
- 6. STALENESS REVIEW
86
- - Identify pages not updated in >30 days that make specific claims about code, file paths, configs, ports, or API endpoints
87
- - These are high-risk for being outdated - add a note in Open Questions: "⚠️ This page has not been updated since [date]. Claims about [specific thing] should be verified."
88
- - If you can determine from other pages or context that a claim is now wrong, fix it directly
89
- - Pages about architecture decisions or historical events are less likely to go stale than pages about configs or deployment
90
-
91
- 7. PROJECT LIFECYCLE REVIEW
92
- - Review projects/ pages — only archive a project if ALL of these are true:
93
- a) The End Condition section explicitly says the goal is met or done
94
- b) There are NO unchecked items ([ ]) remaining anywhere in the page
95
- c) The page has no Open Questions or pending tasks
96
- - Do NOT archive just because status checkboxes are ticked — there may be remaining work not captured in checkboxes
97
- - When archiving: use wiki_write to create the page in archives/ with '(Completed)' in the title, then wiki_merge to remove the projects/ copy
98
-
99
- 8. AREAS HEALTH METRICS
100
- - Review areas/ pages - update current health metrics based on recent lint results and maintenance findings
101
- - Areas represent ongoing responsibilities; keep them current with the latest status
102
-
103
- Rules:
104
- - Work systematically: list all pages first, then process in batches
105
- - For merges: preserve ALL information from both pages, combine into the better-structured one
106
- - For links: only add links between genuinely related pages, not random connections
107
- - Be conservative: when unsure, leave it alone
108
- - Never include secrets/API keys in page content
109
- - Scope must be a kebab-case project name, not a topic description
110
-
111
- When done, summarize what you changed.`;
112
-
113
- // -- Tools -------------------------------------------------------------------
114
-
115
- function createMaintenanceTools(wikiDir: string, store: QMDStore): AgentTool[] {
116
- const WikiListParams = Type.Object({});
117
- const WikiReadParams = Type.Object({ path: Type.String({ description: "category/slug" }) });
118
- const WikiQueryParams = Type.Object({ query: Type.String() });
119
- const WikiWriteParams = Type.Object({
120
- category: StringEnum(["projects", "areas", "resources", "archives"] as const),
121
- slug: Type.String(),
122
- title: Type.String(),
123
- scope: Type.Array(Type.String()),
124
- tags: Type.Array(Type.String()),
125
- body: Type.String(),
126
- });
127
- const WikiMergeParams = Type.Object({
128
- keepCategory: StringEnum(["projects", "areas", "resources", "archives"] as const),
129
- keepSlug: Type.String({ description: "Slug of the page to keep (target)" }),
130
- deleteCategory: StringEnum(["projects", "areas", "resources", "archives"] as const),
131
- deleteSlug: Type.String({ description: "Slug of the page to delete (source)" }),
132
- mergedBody: Type.String({ description: "Combined body text for the kept page" }),
133
- });
134
- const WikiLintParams = Type.Object({});
135
-
136
- const wikiList: AgentTool<typeof WikiListParams> = {
137
- name: "wiki_list",
138
- label: "Wiki List",
139
- description: "List all wiki pages with scope, tags, and outgoing link count.",
140
- parameters: WikiListParams,
141
- execute: async () => {
142
- const refs = await listPages(wikiDir);
143
- const lines: string[] = [];
144
- for (const ref of refs) {
145
- const page = await readPage(wikiDir, ref.category, ref.slug);
146
- if (!page) continue;
147
- const linkCount = extractWikilinks(page.body).length;
148
- lines.push(
149
- `${ref.category}/${ref.slug} | "${page.frontmatter.title}" | scope:[${page.frontmatter.scope.join(",")}] | tags:[${page.frontmatter.tags.join(",")}] | links:${linkCount}`
150
- );
151
- }
152
- return {
153
- content: [{ type: "text", text: `${lines.length} pages:\n${lines.join("\n")}` }],
154
- details: { count: lines.length },
155
- };
156
- },
157
- };
158
-
159
- const wikiRead: AgentTool<typeof WikiReadParams> = {
160
- name: "wiki_read",
161
- label: "Wiki Read",
162
- description: "Read a wiki page by path (category/slug).",
163
- parameters: WikiReadParams,
164
- execute: async (_id, params: { path: string }) => {
165
- const parts = params.path.split("/");
166
- if (parts.length !== 2) {
167
- return { content: [{ type: "text", text: `Invalid path: ${params.path}` }], details: {} };
168
- }
169
- const page = await readPage(wikiDir, parts[0] as ParaCategory, parts[1]);
170
- if (!page) {
171
- return { content: [{ type: "text", text: `Not found: ${params.path}` }], details: {} };
172
- }
173
- return {
174
- content: [{ type: "text", text: `# ${page.frontmatter.title}\nCategory: ${page.category}\nScope: ${page.frontmatter.scope.join(", ")}\nTags: ${page.frontmatter.tags.join(", ")}\nLinks: ${page.frontmatter.links.join(", ") || "none"}\n\n${page.body}` }],
175
- details: { title: page.frontmatter.title },
176
- };
177
- },
178
- };
179
-
180
- const wikiQuery: AgentTool<typeof WikiQueryParams> = {
181
- name: "wiki_query",
182
- label: "Wiki Query",
183
- description: "Search wiki pages by keyword. Returns top matches with scores.",
184
- parameters: WikiQueryParams,
185
- execute: async (_id, params: { query: string }) => {
186
- const results = await searchWiki(store, params.query, { limit: 8 });
187
- if (results.length === 0) {
188
- return { content: [{ type: "text", text: "No results." }], details: { count: 0 } };
189
- }
190
- const text = results
191
- .map(r => `${r.page.path} | "${r.frontmatter.title}" | score:${r.score.toFixed(3)}`)
192
- .join("\n");
193
- return {
194
- content: [{ type: "text", text: `${results.length} results:\n${text}` }],
195
- details: { count: results.length },
196
- };
197
- },
198
- };
199
-
200
- const wikiWrite: AgentTool<typeof WikiWriteParams> = {
201
- name: "wiki_write",
202
- label: "Wiki Write",
203
- description: "Create or update a wiki page. Always redacts secrets and normalizes tags/scope.",
204
- parameters: WikiWriteParams,
205
- execute: async (_id, params: { category: ParaCategory; slug: string; title: string; scope: string[]; tags: string[]; body: string }) => {
206
- const now = new Date().toISOString();
207
- const slug = params.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
208
- const existing = await readPage(wikiDir, params.category, slug);
209
- const body = redactSecrets(params.body).text;
210
- const scope = normalizeScopes(params.scope);
211
- const tags = normalizeTags(params.tags, scope);
212
- const links = syncFrontmatterLinks(body);
213
-
214
- if (existing) {
215
- const updated: WikiPage = {
216
- ...existing,
217
- body,
218
- frontmatter: { ...existing.frontmatter, title: params.title, scope, tags, updated: now, links },
219
- };
220
- await writePage(wikiDir, updated);
221
- } else {
222
- const fm = validateFrontmatter({ title: params.title, para: params.category, scope, tags, sources: [], created: now, updated: now, links });
223
- await writePage(wikiDir, { category: params.category, slug, frontmatter: fm, body });
224
- }
225
- await reindex(store);
226
- return {
227
- content: [{ type: "text", text: `Wrote ${params.category}/${slug}` }],
228
- details: { path: `${params.category}/${slug}` },
229
- };
230
- },
231
- };
232
-
233
- const wikiMerge: AgentTool<typeof WikiMergeParams> = {
234
- name: "wiki_merge",
235
- label: "Wiki Merge",
236
- description: "Merge two pages: update the target page with combined content, delete the source page.",
237
- parameters: WikiMergeParams,
238
- execute: async (_id, params: { keepCategory: ParaCategory; keepSlug: string; deleteCategory: ParaCategory; deleteSlug: string; mergedBody: string }) => {
239
- const keepPage = await readPage(wikiDir, params.keepCategory, params.keepSlug);
240
- const deletePage = await readPage(wikiDir, params.deleteCategory, params.deleteSlug);
241
- if (!keepPage) return { content: [{ type: "text", text: `Target not found: ${params.keepCategory}/${params.keepSlug}` }], details: {} };
242
- if (!deletePage) return { content: [{ type: "text", text: `Source not found: ${params.deleteCategory}/${params.deleteSlug}` }], details: {} };
243
-
244
- const now = new Date().toISOString();
245
- const body = redactSecrets(params.mergedBody).text;
246
- const mergedScope = normalizeScopes([...new Set([...keepPage.frontmatter.scope, ...deletePage.frontmatter.scope])]);
247
- const mergedTags = normalizeTags([...new Set([...keepPage.frontmatter.tags, ...deletePage.frontmatter.tags])], mergedScope);
248
-
249
- // Update target
250
- const updated: WikiPage = {
251
- ...keepPage,
252
- body,
253
- frontmatter: {
254
- ...keepPage.frontmatter,
255
- scope: mergedScope,
256
- tags: mergedTags,
257
- updated: now,
258
- links: syncFrontmatterLinks(body),
259
- },
260
- };
261
- await writePage(wikiDir, updated);
262
-
263
- // Delete source
264
- const { unlink } = await import("node:fs/promises");
265
- const { join } = await import("node:path");
266
- await unlink(join(wikiDir, params.deleteCategory, params.deleteSlug + ".md"));
267
-
268
- await reindex(store);
269
- return {
270
- content: [{ type: "text", text: `Merged ${params.deleteCategory}/${params.deleteSlug} into ${params.keepCategory}/${params.keepSlug}, deleted source.` }],
271
- details: { kept: `${params.keepCategory}/${params.keepSlug}`, deleted: `${params.deleteCategory}/${params.deleteSlug}` },
272
- };
273
- },
274
- };
275
-
276
- const wikiLint: AgentTool<typeof WikiLintParams> = {
277
- name: "wiki_lint",
278
- label: "Wiki Lint",
279
- description: "Run lint checks and return a report. Does NOT auto-fix - you decide what to fix.",
280
- parameters: WikiLintParams,
281
- execute: async () => {
282
- const report = await lintWiki(wikiDir, { autoFix: false });
283
- const lines = report.issues.map(i => `[${i.severity}] ${i.category}: ${i.page ?? ""} - ${i.message}`);
284
- return {
285
- content: [{ type: "text", text: `${report.issues.length} issues:\n${lines.join("\n")}\n\nStats: ${JSON.stringify(report.stats.byCategory)}` }],
286
- details: { issueCount: report.issues.length },
287
- };
288
- },
289
- };
290
-
291
- return [wikiList, wikiRead, wikiQuery, wikiWrite, wikiMerge, wikiLint];
292
- }
293
-
294
- // -- Rebuild index helper (shared with processor) ----------------------------
295
-
296
- async function rebuildIndex(wikiDir: string): Promise<void> {
297
- const allPages = await listPages(wikiDir);
298
- const sections: Record<ParaCategory, string[]> = {
299
- projects: [], areas: [], resources: [], archives: [],
300
- };
301
- for (const ref of allPages) {
302
- const page = await readPage(wikiDir, ref.category, ref.slug);
303
- const title = page?.frontmatter.title ?? ref.title;
304
- const summary = page?.body.split("\n").find(l => l.trim() && !l.startsWith("#") && !l.startsWith("---"))?.trim() ?? "";
305
- const desc = summary.length > 120 ? summary.slice(0, 117) + "..." : summary;
306
- sections[ref.category].push(`- [[${ref.slug}]] - ${title}${desc ? ": " + desc : ""}`);
307
- }
308
- const indexLines = [
309
- "# Wiki Index", "",
310
- "## Projects", "", sections.projects.length > 0 ? sections.projects.join("\n") : "_No active projects._", "",
311
- "## Areas", "", sections.areas.length > 0 ? sections.areas.join("\n") : "_No areas._", "",
312
- "## Resources", "", sections.resources.length > 0 ? sections.resources.join("\n") : "_No resources._", "",
313
- "## Archives", "", sections.archives.length > 0 ? sections.archives.join("\n") : "_No archived items._",
314
- ];
315
- await writeIndex(wikiDir, indexLines.join("\n"));
316
- }
317
-
318
- // -- Main entry point --------------------------------------------------------
319
-
320
- /**
321
- * Run the maintenance agent. Called periodically by the daemon.
322
- */
323
- export async function runMaintenance(
324
- wikiDir: string,
325
- store: QMDStore,
326
- model: Model<any>,
327
- getApiKey: (provider: string) => Promise<string | undefined>,
328
- ): Promise<MaintenanceResult> {
329
- const tools = createMaintenanceTools(wikiDir, store);
330
-
331
- const agent = new Agent({
332
- initialState: {
333
- systemPrompt: MAINTENANCE_SYSTEM_PROMPT,
334
- model,
335
- tools,
336
- messages: [],
337
- },
338
- getApiKey,
339
- });
340
-
341
- const prompt = `Run a maintenance pass on the wiki. Start by listing all pages, then:
342
- 1. Look for duplicate pages (same topic, different slugs) and merge them
343
- 2. Find pages with 0 outgoing links and add relevant [[wikilinks]]
344
- 3. Check for any category misuse or tag issues
345
- 4. Review pages not updated in >30 days for staleness - add Open Questions warnings for pages making specific claims about code/configs/ports that may have changed
346
- Be thorough but conservative. Summarize what you changed at the end.`;
347
-
348
- try {
349
- await agent.prompt(prompt);
350
- } catch (err) {
351
- return {
352
- pagesUpdated: 0,
353
- pagesMerged: 0,
354
- linksAdded: 0,
355
- issuesFixed: 0,
356
- summary: `Agent error: ${err instanceof Error ? err.message : String(err)}`,
357
- };
358
- }
359
-
360
- // Rebuild index after all changes
361
- await rebuildIndex(wikiDir);
362
-
363
- // Extract summary from agent's last message
364
- const messages = agent.state.messages;
365
- const lastAssistant = [...messages].reverse().find(m => m.role === "assistant");
366
- let summary = "Maintenance completed.";
367
- if (lastAssistant && "content" in lastAssistant) {
368
- const content = lastAssistant.content;
369
- if (typeof content === "string") summary = content;
370
- else if (Array.isArray(content)) {
371
- const textBlock = content.find((b: any) => b.type === "text" && typeof b.text === "string") as { text: string } | undefined;
372
- if (textBlock) summary = textBlock.text;
373
- }
374
- }
375
-
376
- // Count changes from tool results
377
- let pagesUpdated = 0;
378
- let pagesMerged = 0;
379
- for (const msg of messages) {
380
- if (msg.role !== "toolResult") continue;
381
- const c = msg.content;
382
- if (!Array.isArray(c)) continue;
383
- for (const block of c) {
384
- const b = block as unknown as Record<string, unknown>;
385
- if (b.type === "text" && typeof b.text === "string") {
386
- if (b.text.startsWith("Wrote ")) pagesUpdated++;
387
- if (b.text.startsWith("Merged ")) pagesMerged++;
388
- }
389
- }
390
- }
391
-
392
- // Git commit all maintenance changes
393
- if (pagesUpdated > 0 || pagesMerged > 0) {
394
- await gitCommit(wikiDir, `maintenance: ${pagesUpdated} updated, ${pagesMerged} merged`);
395
- }
396
-
397
- return {
398
- pagesUpdated,
399
- pagesMerged,
400
- linksAdded: 0, // Agent handles this via wiki_write
401
- issuesFixed: pagesUpdated + pagesMerged,
402
- summary,
403
- };
404
- }
@@ -1,198 +0,0 @@
1
- """MiniMax LM for DSPy.
2
-
3
- MiniMax provides an Anthropic-compatible API at a custom base URL.
4
- This module wraps the Anthropic SDK to work with MiniMax endpoints.
5
-
6
- MiniMax Models:
7
- - MiniMax-M2: 196K context, 128K output, reasoning capable
8
- - MiniMax-M2.1: 196K context, 128K output, reasoning capable
9
-
10
- Endpoints:
11
- - International: https://api.minimax.io/anthropic
12
- - China: https://api.minimaxi.com/anthropic
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import logging
18
- import os
19
- from typing import Any, Literal
20
-
21
- import anthropic
22
- from dspy.clients.base_lm import BaseLM
23
-
24
- logger = logging.getLogger(__name__)
25
-
26
- # MiniMax API endpoints
27
- MINIMAX_BASE_URL = "https://api.minimax.io/anthropic"
28
- MINIMAX_CN_BASE_URL = "https://api.minimaxi.com/anthropic"
29
-
30
-
31
- def get_minimax_api_key(china: bool = False) -> str | None:
32
- """Get MiniMax API key from environment."""
33
- if china:
34
- return os.environ.get("MINIMAX_CN_API_KEY")
35
- return os.environ.get("MINIMAX_API_KEY")
36
-
37
-
38
- class MiniMaxLM(BaseLM):
39
- """DSPy-compatible LM for MiniMax models via Anthropic-compatible API.
40
-
41
- MiniMax provides an Anthropic Messages API compatible endpoint,
42
- so we use the Anthropic SDK with a custom base URL.
43
- """
44
-
45
- def __init__(
46
- self,
47
- model: str = "MiniMax-M2",
48
- api_key: str | None = None,
49
- china: bool = False,
50
- temperature: float | None = None,
51
- max_tokens: int | None = None,
52
- cache: bool = True,
53
- num_retries: int = 3,
54
- model_type: Literal["chat", "text"] = "chat",
55
- **kwargs,
56
- ):
57
- """Initialize MiniMax LM.
58
-
59
- Args:
60
- model: Model ID (e.g., "MiniMax-M2", "MiniMax-M2.1")
61
- api_key: MiniMax API key (or from MINIMAX_API_KEY env var)
62
- china: Use China endpoint (api.minimaxi.com) instead of international
63
- temperature: Sampling temperature
64
- max_tokens: Maximum tokens to generate
65
- """
66
- super().__init__(
67
- model=f"minimax/{model}" if not model.startswith("minimax/") else model,
68
- model_type=model_type,
69
- temperature=temperature or 0.0,
70
- max_tokens=max_tokens or 8192,
71
- cache=cache,
72
- num_retries=num_retries,
73
- )
74
-
75
- self._model_id = model.replace("minimax/", "") if model.startswith("minimax/") else model
76
- self._china = china
77
- self._temperature = temperature or 0.0
78
- self._max_tokens = max_tokens or 8192
79
-
80
- # Get API key
81
- self._api_key = api_key or get_minimax_api_key(china)
82
- if not self._api_key:
83
- env_var = "MINIMAX_CN_API_KEY" if china else "MINIMAX_API_KEY"
84
- raise ValueError(f"No MiniMax API key found. Set {env_var} environment variable.")
85
-
86
- # Create Anthropic client with MiniMax base URL
87
- self._base_url = MINIMAX_CN_BASE_URL if china else MINIMAX_BASE_URL
88
- self._client = anthropic.Anthropic(
89
- api_key=self._api_key,
90
- base_url=self._base_url,
91
- default_headers={
92
- "accept": "application/json",
93
- },
94
- )
95
-
96
- def __getstate__(self) -> dict:
97
- """Get state for pickling - exclude non-picklable client."""
98
- state = self.__dict__.copy()
99
- state.pop('_client', None)
100
- return state
101
-
102
- def __setstate__(self, state: dict) -> None:
103
- """Restore state from pickle - recreate client."""
104
- self.__dict__.update(state)
105
- self._client = anthropic.Anthropic(
106
- api_key=self._api_key,
107
- base_url=self._base_url,
108
- default_headers={"accept": "application/json"},
109
- )
110
-
111
- def _convert_messages(self, messages: list[dict]) -> tuple[list[dict], str | None]:
112
- """Convert to Anthropic message format."""
113
- converted = []
114
- system = None
115
-
116
- for msg in messages:
117
- role = msg.get("role", "user")
118
- content = msg.get("content", "")
119
-
120
- if role == "system":
121
- system = content
122
- elif role == "assistant":
123
- converted.append({"role": "assistant", "content": content})
124
- else:
125
- converted.append({"role": "user", "content": content})
126
-
127
- return converted, system
128
-
129
- def __call__(
130
- self,
131
- prompt: str | None = None,
132
- messages: list[dict] | None = None,
133
- **kwargs,
134
- ) -> list[dict]:
135
- """Call the MiniMax API.
136
-
137
- Args:
138
- prompt: Single prompt string
139
- messages: List of messages (OpenAI format)
140
- **kwargs: Additional parameters
141
-
142
- Returns:
143
- List of response dicts with 'text' key (DSPy format)
144
- """
145
- # Build messages
146
- if messages is None:
147
- messages = []
148
- if prompt:
149
- messages = [{"role": "user", "content": prompt}]
150
-
151
- # Convert to Anthropic format
152
- converted_messages, system = self._convert_messages(messages)
153
-
154
- # Build request kwargs
155
- request_kwargs: dict[str, Any] = {
156
- "model": self._model_id,
157
- "messages": converted_messages,
158
- "max_tokens": kwargs.get("max_tokens", self._max_tokens),
159
- "temperature": kwargs.get("temperature", self._temperature),
160
- }
161
-
162
- if system:
163
- request_kwargs["system"] = system
164
-
165
- # Call API with streaming to support large max_tokens
166
- # The Anthropic SDK requires streaming for operations > 10 minutes
167
- try:
168
- text = ""
169
- with self._client.messages.stream(**request_kwargs) as stream:
170
- for chunk in stream.text_stream:
171
- text += chunk
172
- except anthropic.APIError as e:
173
- logger.error("MiniMax API error: %s", e)
174
- raise ValueError(f"MiniMax API error: {e}")
175
-
176
- # Update history for BaseLM
177
- self.history.append({
178
- "prompt": prompt,
179
- "messages": messages,
180
- "kwargs": kwargs,
181
- "response": {"text": text},
182
- })
183
-
184
- # Return in DSPy format
185
- return [{"text": text}]
186
-
187
-
188
- def create_minimax_lm(model: str = "MiniMax-M2", china: bool = False, **kwargs) -> MiniMaxLM:
189
- """Create a MiniMax LM instance.
190
-
191
- Args:
192
- model: Model ID (without minimax/ prefix)
193
- china: Use China endpoint
194
- **kwargs: Additional parameters
195
- """
196
- if model.startswith("minimax/"):
197
- model = model[8:]
198
- return MiniMaxLM(model=model, china=china, **kwargs)