pi-para 0.6.6 → 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.
- package/ARCHITECTURE.md +4 -6
- package/CHANGELOG.md +22 -0
- package/CONFIGURATION.md +1 -16
- package/README.md +2 -13
- package/SETUP.md +1 -7
- package/STRUCTURE.md +7 -18
- package/dist/cli-legacy.d.ts +2 -1
- package/dist/cli-legacy.d.ts.map +1 -1
- package/dist/cli-legacy.js +13 -25
- package/dist/cli-legacy.js.map +1 -1
- package/dist/cli.d.ts +2 -24
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -271
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +1 -28
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +1 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -1
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -64
- package/dist/index.js.map +1 -1
- package/dist/link-utils.d.ts +1 -1
- package/dist/link-utils.js +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +9 -3
- package/dist/scheduler/index.js.map +1 -1
- package/dist/settings.d.ts +0 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -11
- package/dist/settings.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +48 -16
- package/dist/store.js.map +1 -1
- package/dist/tools/edit-read.d.ts +18 -0
- package/dist/tools/edit-read.d.ts.map +1 -0
- package/dist/tools/edit-read.js +161 -0
- package/dist/tools/edit-read.js.map +1 -0
- package/dist/tools/ingest.d.ts +12 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +48 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/lint.d.ts +10 -0
- package/dist/tools/lint.d.ts.map +1 -0
- package/dist/tools/lint.js +57 -0
- package/dist/tools/lint.js.map +1 -0
- package/dist/tools/migrate.d.ts +6 -0
- package/dist/tools/migrate.d.ts.map +1 -0
- package/dist/tools/migrate.js +36 -0
- package/dist/tools/migrate.js.map +1 -0
- package/dist/tools/move.d.ts +9 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +61 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/query.d.ts +12 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +47 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/schemas.d.ts +109 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +80 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/shared.d.ts +14 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +25 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/summarize.d.ts +8 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +82 -0
- package/dist/tools/summarize.js.map +1 -0
- package/dist/tools/write.d.ts +23 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +215 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/tools.d.ts +1 -8
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -795
- package/dist/tools.js.map +1 -1
- package/dist/wiki-tool-guidance.js +2 -2
- package/dist/wiki-tool-guidance.js.map +1 -1
- package/package.json +2 -6
- package/src/cli-legacy.ts +14 -26
- package/src/cli.ts +29 -293
- package/src/commands.ts +0 -21
- package/src/config.ts +1 -6
- package/src/context.ts +2 -1
- package/src/index.ts +5 -66
- package/src/link-utils.ts +1 -1
- package/src/scheduler/index.ts +9 -3
- package/src/settings.ts +0 -14
- package/src/store.ts +52 -15
- package/src/tools/edit-read.ts +193 -0
- package/src/tools/ingest.ts +63 -0
- package/src/tools/lint.ts +74 -0
- package/src/tools/migrate.ts +44 -0
- package/src/tools/move.ts +79 -0
- package/src/tools/query.ts +63 -0
- package/src/tools/schemas.ts +157 -0
- package/src/tools/shared.ts +35 -0
- package/src/tools/summarize.ts +89 -0
- package/src/tools/write.ts +252 -0
- package/src/tools.ts +18 -1046
- package/src/wiki-tool-guidance.ts +2 -2
- package/dist/daemon.d.ts +0 -49
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -226
- package/dist/daemon.js.map +0 -1
- package/dist/link-discovery.d.ts +0 -40
- package/dist/link-discovery.d.ts.map +0 -1
- package/dist/link-discovery.js +0 -122
- package/dist/link-discovery.js.map +0 -1
- package/dist/maintainer.d.ts +0 -24
- package/dist/maintainer.d.ts.map +0 -1
- package/dist/maintainer.js +0 -358
- package/dist/maintainer.js.map +0 -1
- package/dist/watcher.d.ts +0 -25
- package/dist/watcher.d.ts.map +0 -1
- package/dist/watcher.js +0 -84
- package/dist/watcher.js.map +0 -1
- package/dist/webui/client/dist/index.html +0 -1885
- package/dist/webui/server.d.ts +0 -31
- package/dist/webui/server.d.ts.map +0 -1
- package/dist/webui/server.js +0 -566
- package/dist/webui/server.js.map +0 -1
- package/scripts/build-assets.mjs +0 -17
- package/src/daemon.ts +0 -288
- package/src/link-discovery.ts +0 -170
- package/src/maintainer.ts +0 -404
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +0 -198
- package/src/python/rlm_capture.py +0 -215
- package/src/watcher.ts +0 -101
- package/src/webui/client/dist/index.html +0 -1885
- 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
|
-
}
|
|
Binary file
|
package/src/python/minimax_lm.py
DELETED
|
@@ -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)
|