@docyrus/docyrus 0.0.34 → 0.0.35
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 -0
- package/agent-loader.js +3 -2
- package/agent-loader.js.map +2 -2
- package/main.js +82162 -46093
- package/main.js.map +4 -4
- package/package.json +12 -3
- package/resources/chrome-tools/browser-content.js +46 -46
- package/resources/chrome-tools/browser-cookies.js +16 -16
- package/resources/chrome-tools/browser-eval.js +27 -27
- package/resources/chrome-tools/browser-hn-scraper.js +1 -1
- package/resources/chrome-tools/browser-nav.js +23 -23
- package/resources/chrome-tools/browser-pick.js +127 -127
- package/resources/chrome-tools/browser-screenshot.js +10 -10
- package/resources/chrome-tools/browser-start.js +38 -38
- package/resources/pi-agent/extensions/answer.ts +392 -384
- package/resources/pi-agent/extensions/context.ts +415 -415
- package/resources/pi-agent/extensions/control.ts +1287 -1287
- package/resources/pi-agent/extensions/diff.ts +171 -171
- package/resources/pi-agent/extensions/files.ts +155 -155
- package/resources/pi-agent/extensions/knowledge.ts +664 -0
- package/resources/pi-agent/extensions/loop.ts +375 -375
- package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
- package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
- package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
- package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
- package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
- package/resources/pi-agent/extensions/redraws.ts +14 -14
- package/resources/pi-agent/extensions/review.ts +1533 -1533
- package/resources/pi-agent/extensions/todos.ts +1735 -1735
- package/resources/pi-agent/extensions/tps.ts +40 -40
- package/resources/pi-agent/extensions/whimsical.ts +3 -3
- package/resources/pi-agent/prompts/agent-system.md +2 -0
- package/resources/pi-agent/prompts/coder-system.md +2 -0
- package/server-loader.js +82 -1
- package/server-loader.js.map +3 -3
- package/tui.mjs +2 -0
- package/tui.mjs.map +1 -1
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import type {
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionCommandContext,
|
|
8
|
+
ExtensionContext,
|
|
9
|
+
ToolResultEvent,
|
|
10
|
+
TurnEndEvent,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import type { Theme } from "@mariozechner/pi-tui";
|
|
14
|
+
import { Box, Markdown, Text } from "@mariozechner/pi-tui";
|
|
15
|
+
|
|
16
|
+
const PREVIEW_LINES = 4;
|
|
17
|
+
const KNOWLEDGE_STATE_TYPE = "knowledge-session";
|
|
18
|
+
const KNOWLEDGE_WIDGET_KEY = "knowledge";
|
|
19
|
+
const KNOWLEDGE_REMINDER_INTERVAL_MS = 10 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
type Scope = "local" | "global";
|
|
22
|
+
type KnowledgeSeverity = "ok" | "watch" | "drift" | "critical";
|
|
23
|
+
type KnowledgeToolName = "read" | "write" | "edit";
|
|
24
|
+
|
|
25
|
+
interface ICliEnvironment {
|
|
26
|
+
executable: string;
|
|
27
|
+
entryPath: string;
|
|
28
|
+
scope: Scope;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ICommandResult {
|
|
32
|
+
code: number | null;
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IKnowledgeListImpactedPayload {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
impactedSections: Array<{ id: string; reason: string; confidence: number; filePaths: string[] }>;
|
|
40
|
+
warnings: Array<{ code: string; message: string; confidence: number }>;
|
|
41
|
+
blockingErrors: Array<{ code: string; message: string; confidence: number }>;
|
|
42
|
+
suggestedSectionIds: string[];
|
|
43
|
+
diffStats: {
|
|
44
|
+
codeLines: number;
|
|
45
|
+
knowledgeLines: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IKnowledgeCheckPayload {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
errors: {
|
|
52
|
+
markdown: unknown[];
|
|
53
|
+
codeRefs: unknown[];
|
|
54
|
+
index: unknown[];
|
|
55
|
+
sections: unknown[];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface IKnowledgeSessionState {
|
|
60
|
+
active: boolean;
|
|
61
|
+
filesRead: string[];
|
|
62
|
+
filesEdited: string[];
|
|
63
|
+
filesWritten: string[];
|
|
64
|
+
impactedSectionIds: string[];
|
|
65
|
+
stalenessScore: number;
|
|
66
|
+
severity: KnowledgeSeverity;
|
|
67
|
+
recommendedRefresh: boolean;
|
|
68
|
+
commitWouldFail: boolean;
|
|
69
|
+
diffStats?: {
|
|
70
|
+
codeLines: number;
|
|
71
|
+
knowledgeLines: number;
|
|
72
|
+
observedAt: string;
|
|
73
|
+
};
|
|
74
|
+
lastKnowledgeCheckAt?: string;
|
|
75
|
+
lastSyncSuggestionAt?: string;
|
|
76
|
+
lastReminderAt?: string;
|
|
77
|
+
updatedAt?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findKnowledgeDir(from = process.cwd()): string | null {
|
|
81
|
+
let currentDir = resolvePath(from);
|
|
82
|
+
while (true) {
|
|
83
|
+
const candidate = join(currentDir, "docyrus", "knowledge");
|
|
84
|
+
if (existsSync(candidate)) {
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
const parentDir = dirname(currentDir);
|
|
88
|
+
if (parentDir === currentDir) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
currentDir = parentDir;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readCliEnvironment(env: NodeJS.ProcessEnv = process.env): ICliEnvironment {
|
|
96
|
+
const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
|
|
97
|
+
const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
|
|
98
|
+
const scope = env.DOCYRUS_CLI_SCOPE?.trim() as Scope | undefined;
|
|
99
|
+
if (!executable || !entryPath || (scope !== "local" && scope !== "global")) {
|
|
100
|
+
throw new Error("Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE, DOCYRUS_CLI_ENTRY, and DOCYRUS_CLI_SCOPE.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { executable, entryPath, scope };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildCommandArgs(environment: ICliEnvironment, args: string[], useJson = false): string[] {
|
|
107
|
+
const scopedArgs = environment.scope === "global" ? ["-g", ...args] : args;
|
|
108
|
+
return [environment.entryPath, ...scopedArgs, ...(useJson ? ["--json"] : [])];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function summarizeFailure(result: ICommandResult): string {
|
|
112
|
+
const stderr = result.stderr.trim();
|
|
113
|
+
if (stderr) {
|
|
114
|
+
return stderr;
|
|
115
|
+
}
|
|
116
|
+
const stdout = result.stdout.trim();
|
|
117
|
+
if (stdout) {
|
|
118
|
+
return stdout.split(/\r?\n/u).at(-1) || stdout;
|
|
119
|
+
}
|
|
120
|
+
if (typeof result.code === "number") {
|
|
121
|
+
return `Command exited with code ${result.code}.`;
|
|
122
|
+
}
|
|
123
|
+
return "Command failed.";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function runCli(environment: ICliEnvironment, args: string[], cwd: string, useJson = false): Promise<ICommandResult> {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
execFile(environment.executable, buildCommandArgs(environment, args, useJson), {
|
|
129
|
+
cwd,
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
132
|
+
env: { ...process.env },
|
|
133
|
+
}, (error, stdout, stderr) => {
|
|
134
|
+
if (error && typeof (error as NodeJS.ErrnoException).code === "string" && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
135
|
+
reject(error);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
resolve({
|
|
139
|
+
code: typeof (error as { code?: number } | null)?.code === "number" ? (error as { code?: number }).code || null : null,
|
|
140
|
+
stdout,
|
|
141
|
+
stderr,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function runCliJson<TValue>(environment: ICliEnvironment, args: string[], cwd: string): Promise<{ payload: TValue; raw: ICommandResult }> {
|
|
148
|
+
const result = await runCli(environment, args, cwd, true);
|
|
149
|
+
const output = result.stdout.trim() || result.stderr.trim();
|
|
150
|
+
if (!output) {
|
|
151
|
+
throw new Error("Docyrus CLI produced no JSON output.");
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
payload: JSON.parse(output) as TValue,
|
|
155
|
+
raw: result,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function collapsibleResult(
|
|
160
|
+
result: { content: Array<{ type: string; text?: string }> },
|
|
161
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
162
|
+
theme: Theme,
|
|
163
|
+
) {
|
|
164
|
+
const text = result.content?.[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "";
|
|
165
|
+
if (!text) {
|
|
166
|
+
return new Text(theme.fg("dim", "(empty)"), 0, 0);
|
|
167
|
+
}
|
|
168
|
+
if (options.isPartial) {
|
|
169
|
+
return new Text(theme.fg("dim", "…"), 0, 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const markdownTheme = getMarkdownTheme();
|
|
173
|
+
if (options.expanded) {
|
|
174
|
+
return new Markdown(text, 0, 0, markdownTheme);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lines = text.split("\n");
|
|
178
|
+
if (lines.length <= PREVIEW_LINES) {
|
|
179
|
+
return new Markdown(text, 0, 0, markdownTheme);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const preview = lines.slice(0, PREVIEW_LINES).join("\n");
|
|
183
|
+
const remaining = lines.length - PREVIEW_LINES;
|
|
184
|
+
const hint = keyHint("expandTools", "to expand");
|
|
185
|
+
return new Text(`${preview}\n${theme.fg("dim", `… ${remaining} more lines (${hint})`)}`, 0, 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeTrackedPath(pathValue: string, cwd: string): string {
|
|
189
|
+
const trimmed = pathValue.trim();
|
|
190
|
+
if (!trimmed) {
|
|
191
|
+
return trimmed;
|
|
192
|
+
}
|
|
193
|
+
const absolutePath = trimmed.startsWith("/") ? trimmed : join(cwd, trimmed);
|
|
194
|
+
return relative(cwd, absolutePath);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function unique(values: string[]): string[] {
|
|
198
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaultKnowledgeState(): IKnowledgeSessionState {
|
|
202
|
+
return {
|
|
203
|
+
active: false,
|
|
204
|
+
filesRead: [],
|
|
205
|
+
filesEdited: [],
|
|
206
|
+
filesWritten: [],
|
|
207
|
+
impactedSectionIds: [],
|
|
208
|
+
stalenessScore: 0,
|
|
209
|
+
severity: "ok",
|
|
210
|
+
recommendedRefresh: false,
|
|
211
|
+
commitWouldFail: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function computeSeverity(score: number): KnowledgeSeverity {
|
|
216
|
+
if (score >= 75) {
|
|
217
|
+
return "critical";
|
|
218
|
+
}
|
|
219
|
+
if (score >= 45) {
|
|
220
|
+
return "drift";
|
|
221
|
+
}
|
|
222
|
+
if (score >= 20) {
|
|
223
|
+
return "watch";
|
|
224
|
+
}
|
|
225
|
+
return "ok";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function scoreKnowledgeState(params: {
|
|
229
|
+
state: IKnowledgeSessionState;
|
|
230
|
+
impacted: IKnowledgeListImpactedPayload;
|
|
231
|
+
knowledgeCheckOk?: boolean;
|
|
232
|
+
}): { score: number; severity: KnowledgeSeverity; commitWouldFail: boolean; recommendedRefresh: boolean } {
|
|
233
|
+
let score = 0;
|
|
234
|
+
score += Math.min(25, params.impacted.diffStats.codeLines / 4);
|
|
235
|
+
if (params.impacted.diffStats.knowledgeLines === 0 && params.impacted.diffStats.codeLines > 0) {
|
|
236
|
+
score += 15;
|
|
237
|
+
}
|
|
238
|
+
score += Math.min(20, params.impacted.impactedSections.length * 6);
|
|
239
|
+
score += Math.min(20, params.impacted.warnings.length * 8);
|
|
240
|
+
score += params.impacted.blockingErrors.length > 0 ? 40 : 0;
|
|
241
|
+
score += Math.min(10, params.state.filesEdited.length * 2);
|
|
242
|
+
score += Math.min(5, params.state.filesWritten.length);
|
|
243
|
+
if (params.knowledgeCheckOk === false) {
|
|
244
|
+
score += 35;
|
|
245
|
+
}
|
|
246
|
+
const normalizedScore = Math.max(0, Math.min(100, Math.round(score)));
|
|
247
|
+
const severity = computeSeverity(normalizedScore);
|
|
248
|
+
return {
|
|
249
|
+
score: normalizedScore,
|
|
250
|
+
severity,
|
|
251
|
+
commitWouldFail: params.impacted.blockingErrors.length > 0 || params.knowledgeCheckOk === false || severity === "critical",
|
|
252
|
+
recommendedRefresh: severity === "drift" || severity === "critical",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function shouldNotifyKnowledgeReminder(previous: IKnowledgeSessionState, next: IKnowledgeSessionState): boolean {
|
|
257
|
+
if (!next.recommendedRefresh) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const previousRank = previous.severity === "critical" ? 3 : previous.severity === "drift" ? 2 : previous.severity === "watch" ? 1 : 0;
|
|
261
|
+
const nextRank = next.severity === "critical" ? 3 : next.severity === "drift" ? 2 : next.severity === "watch" ? 1 : 0;
|
|
262
|
+
if (nextRank > previousRank) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
if (!next.lastReminderAt) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return Date.now() - new Date(next.lastReminderAt).getTime() >= KNOWLEDGE_REMINDER_INTERVAL_MS;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getLatestKnowledgeState(ctx: ExtensionContext): IKnowledgeSessionState {
|
|
272
|
+
const entries = ctx.sessionManager.getEntries();
|
|
273
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
274
|
+
const entry = entries[index] as { type: string; customType?: string; data?: IKnowledgeSessionState };
|
|
275
|
+
if (entry.type === "custom" && entry.customType === KNOWLEDGE_STATE_TYPE && entry.data) {
|
|
276
|
+
return entry.data;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return defaultKnowledgeState();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function persistKnowledgeState(pi: ExtensionAPI, state: IKnowledgeSessionState): void {
|
|
283
|
+
pi.appendEntry(KNOWLEDGE_STATE_TYPE, state);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function updateKnowledgeWidget(ctx: ExtensionContext, state: IKnowledgeSessionState): void {
|
|
287
|
+
if (!ctx.hasUI) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!state.active) {
|
|
291
|
+
ctx.ui.setWidget(KNOWLEDGE_WIDGET_KEY, undefined);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const label = state.commitWouldFail
|
|
296
|
+
? `Knowledge: commit blocked (${state.stalenessScore})`
|
|
297
|
+
: state.recommendedRefresh
|
|
298
|
+
? `Knowledge: refresh suggested (${state.stalenessScore})`
|
|
299
|
+
: `Knowledge: in sync (${state.stalenessScore})`;
|
|
300
|
+
const color = state.commitWouldFail ? "warning" : state.recommendedRefresh ? "accent" : "success";
|
|
301
|
+
ctx.ui.setWidget(KNOWLEDGE_WIDGET_KEY, [ctx.ui.theme.fg(color as never, label)]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function summarizeStatus(state: IKnowledgeSessionState): string {
|
|
305
|
+
const parts = [
|
|
306
|
+
`severity: ${state.severity}`,
|
|
307
|
+
`score: ${state.stalenessScore}`,
|
|
308
|
+
`recommendedRefresh: ${state.recommendedRefresh}`,
|
|
309
|
+
`commitWouldFail: ${state.commitWouldFail}`,
|
|
310
|
+
`impactedSections: ${state.impactedSectionIds.length > 0 ? state.impactedSectionIds.join(", ") : "(none)"}`,
|
|
311
|
+
`filesRead: ${state.filesRead.length}`,
|
|
312
|
+
`filesEdited: ${state.filesEdited.length}`,
|
|
313
|
+
`filesWritten: ${state.filesWritten.length}`,
|
|
314
|
+
];
|
|
315
|
+
if (state.diffStats) {
|
|
316
|
+
parts.push(`diffStats: code=${state.diffStats.codeLines}, knowledge=${state.diffStats.knowledgeLines}`);
|
|
317
|
+
}
|
|
318
|
+
if (state.lastKnowledgeCheckAt) {
|
|
319
|
+
parts.push(`lastKnowledgeCheckAt: ${state.lastKnowledgeCheckAt}`);
|
|
320
|
+
}
|
|
321
|
+
return parts.join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function refreshKnowledgeState(
|
|
325
|
+
pi: ExtensionAPI,
|
|
326
|
+
ctx: ExtensionContext,
|
|
327
|
+
options?: {
|
|
328
|
+
runCheck?: boolean;
|
|
329
|
+
allowReminder?: boolean;
|
|
330
|
+
},
|
|
331
|
+
): Promise<IKnowledgeSessionState> {
|
|
332
|
+
const knowledgeDir = findKnowledgeDir(ctx.cwd);
|
|
333
|
+
if (!knowledgeDir) {
|
|
334
|
+
const inactive = defaultKnowledgeState();
|
|
335
|
+
updateKnowledgeWidget(ctx, inactive);
|
|
336
|
+
persistKnowledgeState(pi, inactive);
|
|
337
|
+
return inactive;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const previous = getLatestKnowledgeState(ctx);
|
|
341
|
+
const environment = readCliEnvironment();
|
|
342
|
+
const impacted = (await runCliJson<IKnowledgeListImpactedPayload>(environment, ["knowledge", "list-impacted"], ctx.cwd)).payload;
|
|
343
|
+
let knowledgeCheckOk: boolean | undefined;
|
|
344
|
+
const now = new Date().toISOString();
|
|
345
|
+
|
|
346
|
+
if (options?.runCheck) {
|
|
347
|
+
const check = (await runCliJson<IKnowledgeCheckPayload>(environment, ["knowledge", "check"], ctx.cwd)).payload;
|
|
348
|
+
knowledgeCheckOk = check.ok;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const scored = scoreKnowledgeState({
|
|
352
|
+
state: previous,
|
|
353
|
+
impacted,
|
|
354
|
+
knowledgeCheckOk,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const next: IKnowledgeSessionState = {
|
|
358
|
+
active: true,
|
|
359
|
+
filesRead: previous.filesRead,
|
|
360
|
+
filesEdited: previous.filesEdited,
|
|
361
|
+
filesWritten: previous.filesWritten,
|
|
362
|
+
impactedSectionIds: impacted.suggestedSectionIds,
|
|
363
|
+
stalenessScore: scored.score,
|
|
364
|
+
severity: scored.severity,
|
|
365
|
+
recommendedRefresh: scored.recommendedRefresh,
|
|
366
|
+
commitWouldFail: scored.commitWouldFail,
|
|
367
|
+
diffStats: {
|
|
368
|
+
codeLines: impacted.diffStats.codeLines,
|
|
369
|
+
knowledgeLines: impacted.diffStats.knowledgeLines,
|
|
370
|
+
observedAt: now,
|
|
371
|
+
},
|
|
372
|
+
lastKnowledgeCheckAt: knowledgeCheckOk ? now : previous.lastKnowledgeCheckAt,
|
|
373
|
+
lastSyncSuggestionAt: previous.lastSyncSuggestionAt,
|
|
374
|
+
lastReminderAt: previous.lastReminderAt,
|
|
375
|
+
updatedAt: now,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (options?.allowReminder && shouldNotifyKnowledgeReminder(previous, next)) {
|
|
379
|
+
next.lastReminderAt = now;
|
|
380
|
+
next.lastSyncSuggestionAt = now;
|
|
381
|
+
if (ctx.hasUI) {
|
|
382
|
+
ctx.ui.notify(
|
|
383
|
+
next.commitWouldFail
|
|
384
|
+
? "Knowledge drift is severe enough that commit-time checks would likely fail. Run /knowledge-refresh."
|
|
385
|
+
: "Knowledge drift detected. Run /knowledge-refresh to update the likely impacted sections.",
|
|
386
|
+
next.commitWouldFail ? "warning" : "info",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
persistKnowledgeState(pi, next);
|
|
392
|
+
updateKnowledgeWidget(ctx, next);
|
|
393
|
+
return next;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function trackToolResult(pi: ExtensionAPI, ctx: ExtensionContext, event: ToolResultEvent): void {
|
|
397
|
+
if (event.isError) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (event.toolName !== "read" && event.toolName !== "write" && event.toolName !== "edit") {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const inputPath = typeof event.input.path === "string" ? event.input.path : "";
|
|
404
|
+
if (!inputPath) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const state = getLatestKnowledgeState(ctx);
|
|
409
|
+
const normalized = normalizeTrackedPath(inputPath, ctx.cwd);
|
|
410
|
+
const next: IKnowledgeSessionState = {
|
|
411
|
+
...state,
|
|
412
|
+
active: Boolean(findKnowledgeDir(ctx.cwd)),
|
|
413
|
+
filesRead: event.toolName === "read" ? unique([...state.filesRead, normalized]) : state.filesRead,
|
|
414
|
+
filesEdited: event.toolName === "edit" ? unique([...state.filesEdited, normalized]) : state.filesEdited,
|
|
415
|
+
filesWritten: event.toolName === "write" ? unique([...state.filesWritten, normalized]) : state.filesWritten,
|
|
416
|
+
updatedAt: new Date().toISOString(),
|
|
417
|
+
};
|
|
418
|
+
persistKnowledgeState(pi, next);
|
|
419
|
+
updateKnowledgeWidget(ctx, next);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function registerKnowledgeTool(
|
|
423
|
+
pi: ExtensionAPI,
|
|
424
|
+
name: string,
|
|
425
|
+
label: string,
|
|
426
|
+
description: string,
|
|
427
|
+
argsBuilder: (params: Record<string, unknown>) => string[],
|
|
428
|
+
schema: object,
|
|
429
|
+
preview: (args: Record<string, unknown>, theme: Theme) => string,
|
|
430
|
+
) {
|
|
431
|
+
pi.registerTool({
|
|
432
|
+
name,
|
|
433
|
+
label,
|
|
434
|
+
description,
|
|
435
|
+
promptSnippet: description,
|
|
436
|
+
parameters: schema,
|
|
437
|
+
async execute(_id, params) {
|
|
438
|
+
const environment = readCliEnvironment();
|
|
439
|
+
const result = await runCli(environment, argsBuilder(params as Record<string, unknown>), process.cwd(), false);
|
|
440
|
+
const text = result.stdout.trim() || summarizeFailure(result);
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: "text", text }],
|
|
443
|
+
...(result.code && result.code !== 0 ? { isError: true } : {}),
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
renderCall(args, theme) {
|
|
447
|
+
return new Text(preview(args as Record<string, unknown>, theme), 0, 0);
|
|
448
|
+
},
|
|
449
|
+
renderResult: collapsibleResult,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function parseCommandArgs(rawArgs: string): string | null {
|
|
454
|
+
const trimmed = rawArgs.trim();
|
|
455
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export default function(pi: ExtensionAPI) {
|
|
459
|
+
registerKnowledgeTool(
|
|
460
|
+
pi,
|
|
461
|
+
"docyrus_knowledge_search",
|
|
462
|
+
"knowledge search",
|
|
463
|
+
"Semantic search across docyrus/knowledge sections",
|
|
464
|
+
(params) => {
|
|
465
|
+
const args = ["knowledge", "search"];
|
|
466
|
+
if (typeof params.query === "string" && params.query.trim().length > 0) {
|
|
467
|
+
args.push(params.query);
|
|
468
|
+
}
|
|
469
|
+
if (typeof params.limit === "number") {
|
|
470
|
+
args.push("--limit", String(params.limit));
|
|
471
|
+
}
|
|
472
|
+
return args;
|
|
473
|
+
},
|
|
474
|
+
Type.Object({
|
|
475
|
+
query: Type.String({ description: "Search query in natural language" }),
|
|
476
|
+
limit: Type.Optional(Type.Number({ description: "Maximum matches", default: 5 })),
|
|
477
|
+
}),
|
|
478
|
+
(args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge search "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
registerKnowledgeTool(
|
|
482
|
+
pi,
|
|
483
|
+
"docyrus_knowledge_section",
|
|
484
|
+
"knowledge section",
|
|
485
|
+
"Show a full knowledge section with refs and backlinks",
|
|
486
|
+
(params) => ["knowledge", "section", String(params.query || "")],
|
|
487
|
+
Type.Object({
|
|
488
|
+
query: Type.String({ description: "Section id or heading query" }),
|
|
489
|
+
}),
|
|
490
|
+
(args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge section "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
registerKnowledgeTool(
|
|
494
|
+
pi,
|
|
495
|
+
"docyrus_knowledge_locate",
|
|
496
|
+
"knowledge locate",
|
|
497
|
+
"Find knowledge sections by exact, short, or fuzzy match",
|
|
498
|
+
(params) => ["knowledge", "locate", String(params.query || "")],
|
|
499
|
+
Type.Object({
|
|
500
|
+
query: Type.String({ description: "Section id or heading query" }),
|
|
501
|
+
}),
|
|
502
|
+
(args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge locate "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
registerKnowledgeTool(
|
|
506
|
+
pi,
|
|
507
|
+
"docyrus_knowledge_refs",
|
|
508
|
+
"knowledge refs",
|
|
509
|
+
"Find markdown and code references to a knowledge section or source symbol",
|
|
510
|
+
(params) => {
|
|
511
|
+
const args = ["knowledge", "refs", String(params.query || "")];
|
|
512
|
+
if (typeof params.scope === "string" && params.scope.length > 0) {
|
|
513
|
+
args.push("--scope", params.scope);
|
|
514
|
+
}
|
|
515
|
+
return args;
|
|
516
|
+
},
|
|
517
|
+
Type.Object({
|
|
518
|
+
query: Type.String({ description: "Section id, source file, or source symbol query" }),
|
|
519
|
+
scope: Type.Optional(Type.String({ description: "md, code, or md+code" })),
|
|
520
|
+
}),
|
|
521
|
+
(args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge refs "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
registerKnowledgeTool(
|
|
525
|
+
pi,
|
|
526
|
+
"docyrus_knowledge_expand",
|
|
527
|
+
"knowledge expand",
|
|
528
|
+
"Expand [[refs]] into resolved section context",
|
|
529
|
+
(params) => ["knowledge", "expand", String(params.text || "")],
|
|
530
|
+
Type.Object({
|
|
531
|
+
text: Type.String({ description: "Text containing [[refs]]" }),
|
|
532
|
+
}),
|
|
533
|
+
(args, theme) => {
|
|
534
|
+
const text = String(args.text || "");
|
|
535
|
+
const preview = text.length > 60 ? `${text.slice(0, 60)}…` : text;
|
|
536
|
+
return `${theme.fg("toolTitle", theme.bold("knowledge expand "))}${theme.fg("dim", `"${preview}"`)}`;
|
|
537
|
+
},
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
registerKnowledgeTool(
|
|
541
|
+
pi,
|
|
542
|
+
"docyrus_knowledge_check",
|
|
543
|
+
"knowledge check",
|
|
544
|
+
"Validate the repo knowledge graph",
|
|
545
|
+
() => ["knowledge", "check"],
|
|
546
|
+
Type.Object({}),
|
|
547
|
+
(_args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge check"))}`,
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
pi.registerCommand("knowledge-init", {
|
|
551
|
+
description: "Bootstrap docyrus/knowledge from the current repo and install hook guidance",
|
|
552
|
+
handler: async(rawArgs, ctx: ExtensionCommandContext) => {
|
|
553
|
+
const environment = readCliEnvironment();
|
|
554
|
+
const brief = parseCommandArgs(rawArgs);
|
|
555
|
+
const args = ["knowledge", "init", ...(brief ? ["--brief", brief] : [])];
|
|
556
|
+
const result = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
|
|
557
|
+
if (ctx.hasUI) {
|
|
558
|
+
ctx.ui.notify(`Knowledge init complete: ${result.payload.knowledgeDir || "docyrus/knowledge/"}`, "info");
|
|
559
|
+
}
|
|
560
|
+
await refreshKnowledgeState(pi, ctx, {
|
|
561
|
+
runCheck: true,
|
|
562
|
+
allowReminder: false,
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
pi.registerCommand("knowledge-status", {
|
|
568
|
+
description: "Show current knowledge staleness status and likely impacted sections",
|
|
569
|
+
handler: async(_rawArgs, ctx: ExtensionCommandContext) => {
|
|
570
|
+
const state = await refreshKnowledgeState(pi, ctx, {
|
|
571
|
+
runCheck: true,
|
|
572
|
+
allowReminder: false,
|
|
573
|
+
});
|
|
574
|
+
if (!ctx.hasUI) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
ctx.ui.notify(summarizeStatus(state), state.commitWouldFail ? "warning" : "info");
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
pi.registerCommand("knowledge-refresh", {
|
|
582
|
+
description: "Refresh the managed knowledge files for the likely impacted sections",
|
|
583
|
+
handler: async(rawArgs, ctx: ExtensionCommandContext) => {
|
|
584
|
+
const environment = readCliEnvironment();
|
|
585
|
+
const state = await refreshKnowledgeState(pi, ctx, {
|
|
586
|
+
runCheck: false,
|
|
587
|
+
allowReminder: false,
|
|
588
|
+
});
|
|
589
|
+
const brief = parseCommandArgs(rawArgs);
|
|
590
|
+
const args = ["knowledge", "refresh"];
|
|
591
|
+
if (state.impactedSectionIds.length > 0) {
|
|
592
|
+
args.push("--sections", state.impactedSectionIds.join(","));
|
|
593
|
+
}
|
|
594
|
+
if (brief) {
|
|
595
|
+
args.push(brief);
|
|
596
|
+
}
|
|
597
|
+
const result = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
|
|
598
|
+
if (ctx.hasUI) {
|
|
599
|
+
ctx.ui.notify(`Knowledge refresh complete for ${state.impactedSectionIds.length > 0 ? state.impactedSectionIds.length : "default"} target(s).`, "info");
|
|
600
|
+
}
|
|
601
|
+
const nextState = await refreshKnowledgeState(pi, ctx, {
|
|
602
|
+
runCheck: true,
|
|
603
|
+
allowReminder: false,
|
|
604
|
+
});
|
|
605
|
+
persistKnowledgeState(pi, {
|
|
606
|
+
...nextState,
|
|
607
|
+
lastSyncSuggestionAt: new Date().toISOString(),
|
|
608
|
+
});
|
|
609
|
+
void result;
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
|
|
614
|
+
trackToolResult(pi, ctx, event);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
pi.on("session_start", async(_event, ctx) => {
|
|
618
|
+
await refreshKnowledgeState(pi, ctx, {
|
|
619
|
+
runCheck: false,
|
|
620
|
+
allowReminder: true,
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
pi.on("session_switch", async(_event, ctx) => {
|
|
625
|
+
await refreshKnowledgeState(pi, ctx, {
|
|
626
|
+
runCheck: false,
|
|
627
|
+
allowReminder: true,
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
pi.on("turn_end", async(_event: TurnEndEvent, ctx) => {
|
|
632
|
+
await refreshKnowledgeState(pi, ctx, {
|
|
633
|
+
runCheck: false,
|
|
634
|
+
allowReminder: true,
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
pi.on("agent_end", async(_event, ctx) => {
|
|
639
|
+
const state = await refreshKnowledgeState(pi, ctx, {
|
|
640
|
+
runCheck: true,
|
|
641
|
+
allowReminder: true,
|
|
642
|
+
});
|
|
643
|
+
if (ctx.hasUI && state.commitWouldFail) {
|
|
644
|
+
ctx.ui.notify("Knowledge drift is severe or integrity checks failed. A commit would likely be blocked until knowledge is updated.", "warning");
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
pi.on("session_shutdown", async(_event, ctx) => {
|
|
649
|
+
updateKnowledgeWidget(ctx, defaultKnowledgeState());
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
pi.registerMessageRenderer("knowledge-reminder", (message, { expanded }, theme) => {
|
|
653
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
654
|
+
if (expanded) {
|
|
655
|
+
box.addChild(new Text(theme.fg("accent", "Knowledge"), 0, 0));
|
|
656
|
+
box.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
|
|
657
|
+
return box;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const hint = keyHint("expandTools", "to expand");
|
|
661
|
+
box.addChild(new Text(`${theme.fg("accent", "Knowledge")} ${theme.fg("dim", `Watch the knowledge widget and use /knowledge-refresh when drift appears. (${hint})`)}`, 0, 0));
|
|
662
|
+
return box;
|
|
663
|
+
});
|
|
664
|
+
}
|