@docyrus/docyrus 0.0.20 → 0.0.21

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 (65) hide show
  1. package/agent-loader.js +32 -1
  2. package/agent-loader.js.map +2 -2
  3. package/main.js +321 -70
  4. package/main.js.map +4 -4
  5. package/package.json +12 -2
  6. package/resources/chrome-tools/browser-content.js +103 -0
  7. package/resources/chrome-tools/browser-cookies.js +35 -0
  8. package/resources/chrome-tools/browser-eval.js +53 -0
  9. package/resources/chrome-tools/browser-hn-scraper.js +108 -0
  10. package/resources/chrome-tools/browser-nav.js +44 -0
  11. package/resources/chrome-tools/browser-pick.js +162 -0
  12. package/resources/chrome-tools/browser-screenshot.js +34 -0
  13. package/resources/chrome-tools/browser-start.js +86 -0
  14. package/resources/pi-agent/extensions/answer.ts +532 -0
  15. package/resources/pi-agent/extensions/context.ts +578 -0
  16. package/resources/pi-agent/extensions/control.ts +1779 -0
  17. package/resources/pi-agent/extensions/diff.ts +218 -0
  18. package/resources/pi-agent/extensions/files.ts +199 -0
  19. package/resources/pi-agent/extensions/loop.ts +446 -0
  20. package/resources/pi-agent/extensions/multi-edit.ts +835 -0
  21. package/resources/pi-agent/extensions/notify.ts +88 -0
  22. package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
  23. package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
  24. package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
  25. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
  26. package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
  27. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
  28. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
  29. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
  30. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
  54. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
  55. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
  56. package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
  57. package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
  58. package/resources/pi-agent/extensions/redraws.ts +24 -0
  59. package/resources/pi-agent/extensions/review.ts +2160 -0
  60. package/resources/pi-agent/extensions/todos.ts +2076 -0
  61. package/resources/pi-agent/extensions/tps.ts +47 -0
  62. package/resources/pi-agent/extensions/whimsical.ts +474 -0
  63. package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
  64. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
  65. package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +51 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Diff Extension
3
+ *
4
+ * /diff command shows modified/deleted/new files from git status and opens
5
+ * the selected file in VS Code's diff view.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
10
+ import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
11
+
12
+ interface FileInfo {
13
+ status: string;
14
+ statusLabel: string;
15
+ file: string;
16
+ }
17
+
18
+ export default function (pi: ExtensionAPI) {
19
+ pi.registerCommand("diff", {
20
+ description: "Show git changes and open in VS Code diff view",
21
+ handler: async (_args, ctx) => {
22
+ if (!ctx.hasUI) {
23
+ ctx.ui.notify("No UI available", "error");
24
+ return;
25
+ }
26
+
27
+ // Get changed files from git status
28
+ const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
29
+
30
+ if (result.code !== 0) {
31
+ ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
32
+ return;
33
+ }
34
+
35
+ if (!result.stdout || !result.stdout.trim()) {
36
+ ctx.ui.notify("No changes in working tree", "info");
37
+ return;
38
+ }
39
+
40
+ // Parse git status output
41
+ // Format: XY filename (where XY is two-letter status, then space, then filename)
42
+ const lines = result.stdout.split("\n");
43
+ const files: FileInfo[] = [];
44
+
45
+ for (const line of lines) {
46
+ if (line.length < 4) continue; // Need at least "XY f"
47
+
48
+ const status = line.slice(0, 2);
49
+ const file = line.slice(2).trimStart();
50
+
51
+ // Translate status codes to short labels
52
+ let statusLabel: string;
53
+ if (status.includes("M")) statusLabel = "M";
54
+ else if (status.includes("A")) statusLabel = "A";
55
+ else if (status.includes("D")) statusLabel = "D";
56
+ else if (status.includes("?")) statusLabel = "?";
57
+ else if (status.includes("R")) statusLabel = "R";
58
+ else if (status.includes("C")) statusLabel = "C";
59
+ else statusLabel = status.trim() || "~";
60
+
61
+ files.push({ status: statusLabel, statusLabel, file });
62
+ }
63
+
64
+ if (files.length === 0) {
65
+ ctx.ui.notify("No changes found", "info");
66
+ return;
67
+ }
68
+
69
+ const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
70
+ const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`;
71
+
72
+ const openWithCode = async (file: string) => {
73
+ if (process.platform === "win32") {
74
+ if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) {
75
+ ctx.ui.notify(
76
+ `Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,
77
+ "error",
78
+ );
79
+ return null;
80
+ }
81
+ const commandLine = `code -g ${quoteCmdArg(file)}`;
82
+ return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd });
83
+ }
84
+ return pi.exec("code", ["-g", file], { cwd: ctx.cwd });
85
+ };
86
+
87
+ const openSelected = async (fileInfo: FileInfo): Promise<void> => {
88
+ try {
89
+ // Open in VS Code diff view.
90
+ // For untracked files, git difftool won't work, so fall back to just opening the file.
91
+ if (fileInfo.status === "?") {
92
+ const openResult = await openWithCode(fileInfo.file);
93
+ if (!openResult) return;
94
+ if (openResult.code !== 0) {
95
+ const openStderr = openResult.stderr.trim();
96
+ ctx.ui.notify(
97
+ `Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
98
+ "error",
99
+ );
100
+ }
101
+ return;
102
+ }
103
+
104
+ const diffResult = await pi.exec("git", ["difftool", "-y", "--tool=vscode", fileInfo.file], {
105
+ cwd: ctx.cwd,
106
+ });
107
+ if (diffResult.code !== 0) {
108
+ const diffStderr = diffResult.stderr.trim();
109
+ ctx.ui.notify(
110
+ `Failed to show diff with vscode for ${fileInfo.file} (exit ${diffResult.code})${diffStderr ? `: ${diffStderr}` : ""}`,
111
+ "error",
112
+ );
113
+ ctx.ui.notify(
114
+ "Troubleshooting: check git difftool config (e.g. `git config --get difftool.vscode.cmd`).",
115
+ "info",
116
+ );
117
+
118
+ const openResult = await openWithCode(fileInfo.file);
119
+ if (!openResult) return;
120
+ if (openResult.code !== 0) {
121
+ const openStderr = openResult.stderr.trim();
122
+ ctx.ui.notify(
123
+ `Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
124
+ "error",
125
+ );
126
+ }
127
+ }
128
+ } catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error");
131
+ }
132
+ };
133
+
134
+ // Show file picker with SelectList
135
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
136
+ const container = new Container();
137
+
138
+ // Top border
139
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
140
+
141
+ // Title
142
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
143
+
144
+ // Build select items with colored status
145
+ const items: SelectItem[] = files.map((f) => {
146
+ let statusColor: string;
147
+ switch (f.status) {
148
+ case "M":
149
+ statusColor = theme.fg("warning", f.status);
150
+ break;
151
+ case "A":
152
+ statusColor = theme.fg("success", f.status);
153
+ break;
154
+ case "D":
155
+ statusColor = theme.fg("error", f.status);
156
+ break;
157
+ case "?":
158
+ statusColor = theme.fg("muted", f.status);
159
+ break;
160
+ default:
161
+ statusColor = theme.fg("dim", f.status);
162
+ }
163
+ return {
164
+ value: f,
165
+ label: `${statusColor} ${f.file}`,
166
+ };
167
+ });
168
+
169
+ const visibleRows = Math.min(files.length, 15);
170
+ let currentIndex = 0;
171
+
172
+ const selectList = new SelectList(items, visibleRows, {
173
+ selectedPrefix: (t) => theme.fg("accent", t),
174
+ selectedText: (t) => t, // Keep existing colors
175
+ description: (t) => theme.fg("muted", t),
176
+ scrollInfo: (t) => theme.fg("dim", t),
177
+ noMatch: (t) => theme.fg("warning", t),
178
+ });
179
+ selectList.onSelect = (item) => {
180
+ void openSelected(item.value as FileInfo);
181
+ };
182
+ selectList.onCancel = () => done();
183
+ selectList.onSelectionChange = (item) => {
184
+ currentIndex = items.indexOf(item);
185
+ };
186
+ container.addChild(selectList);
187
+
188
+ // Help text
189
+ container.addChild(
190
+ new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
191
+ );
192
+
193
+ // Bottom border
194
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
195
+
196
+ return {
197
+ render: (w) => container.render(w),
198
+ invalidate: () => container.invalidate(),
199
+ handleInput: (data) => {
200
+ // Add paging with left/right
201
+ if (matchesKey(data, Key.left)) {
202
+ // Page up - clamp to 0
203
+ currentIndex = Math.max(0, currentIndex - visibleRows);
204
+ selectList.setSelectedIndex(currentIndex);
205
+ } else if (matchesKey(data, Key.right)) {
206
+ // Page down - clamp to last
207
+ currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
208
+ selectList.setSelectedIndex(currentIndex);
209
+ } else {
210
+ selectList.handleInput(data);
211
+ }
212
+ tui.requestRender();
213
+ },
214
+ };
215
+ });
216
+ },
217
+ });
218
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Files Extension
3
+ *
4
+ * /files command lists all files the model has read/written/edited in the active session branch,
5
+ * coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
10
+ import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
11
+
12
+ interface FileEntry {
13
+ path: string;
14
+ operations: Set<"read" | "write" | "edit">;
15
+ lastTimestamp: number;
16
+ }
17
+
18
+ type FileToolName = "read" | "write" | "edit";
19
+
20
+ export default function (pi: ExtensionAPI) {
21
+ pi.registerCommand("files", {
22
+ description: "Show files read/written/edited in this session",
23
+ handler: async (_args, ctx) => {
24
+ if (!ctx.hasUI) {
25
+ ctx.ui.notify("No UI available", "error");
26
+ return;
27
+ }
28
+
29
+ // Get the current branch (path from leaf to root)
30
+ const branch = ctx.sessionManager.getBranch();
31
+
32
+ // First pass: collect tool calls (id -> {path, name}) from assistant messages
33
+ const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
34
+
35
+ for (const entry of branch) {
36
+ if (entry.type !== "message") continue;
37
+ const msg = entry.message;
38
+
39
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
40
+ for (const block of msg.content) {
41
+ if (block.type === "toolCall") {
42
+ const name = block.name;
43
+ if (name === "read" || name === "write" || name === "edit") {
44
+ const path = block.arguments?.path;
45
+ if (path && typeof path === "string") {
46
+ toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ // Second pass: match tool results to get the actual execution timestamp
55
+ const fileMap = new Map<string, FileEntry>();
56
+
57
+ for (const entry of branch) {
58
+ if (entry.type !== "message") continue;
59
+ const msg = entry.message;
60
+
61
+ if (msg.role === "toolResult") {
62
+ const toolCall = toolCalls.get(msg.toolCallId);
63
+ if (!toolCall) continue;
64
+
65
+ const { path, name } = toolCall;
66
+ const timestamp = msg.timestamp;
67
+
68
+ const existing = fileMap.get(path);
69
+ if (existing) {
70
+ existing.operations.add(name);
71
+ if (timestamp > existing.lastTimestamp) {
72
+ existing.lastTimestamp = timestamp;
73
+ }
74
+ } else {
75
+ fileMap.set(path, {
76
+ path,
77
+ operations: new Set([name]),
78
+ lastTimestamp: timestamp,
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ if (fileMap.size === 0) {
85
+ ctx.ui.notify("No files read/written/edited in this session", "info");
86
+ return;
87
+ }
88
+
89
+ // Sort by most recent first
90
+ const files = Array.from(fileMap.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp);
91
+
92
+ const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
93
+ const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`;
94
+
95
+ const openWithCode = async (path: string) => {
96
+ if (process.platform === "win32") {
97
+ if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(path)) {
98
+ ctx.ui.notify(
99
+ `Refusing to open ${path}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,
100
+ "error",
101
+ );
102
+ return null;
103
+ }
104
+ const commandLine = `code -g ${quoteCmdArg(path)}`;
105
+ return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd });
106
+ }
107
+ return pi.exec("code", ["-g", path], { cwd: ctx.cwd });
108
+ };
109
+
110
+ const openSelected = async (file: FileEntry): Promise<void> => {
111
+ try {
112
+ const openResult = await openWithCode(file.path);
113
+ if (!openResult) return;
114
+ if (openResult.code !== 0) {
115
+ const openStderr = openResult.stderr.trim();
116
+ ctx.ui.notify(
117
+ `Failed to open ${file.path} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
118
+ "error",
119
+ );
120
+ }
121
+ } catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
124
+ }
125
+ };
126
+
127
+ // Show file picker with SelectList
128
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
129
+ const container = new Container();
130
+
131
+ // Top border
132
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
133
+
134
+ // Title
135
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
136
+
137
+ // Build select items with colored operations
138
+ const items: SelectItem[] = files.map((f) => {
139
+ const ops: string[] = [];
140
+ if (f.operations.has("read")) ops.push(theme.fg("muted", "R"));
141
+ if (f.operations.has("write")) ops.push(theme.fg("success", "W"));
142
+ if (f.operations.has("edit")) ops.push(theme.fg("warning", "E"));
143
+ const opsLabel = ops.join("");
144
+ return {
145
+ value: f,
146
+ label: `${opsLabel} ${f.path}`,
147
+ };
148
+ });
149
+
150
+ const visibleRows = Math.min(files.length, 15);
151
+ let currentIndex = 0;
152
+
153
+ const selectList = new SelectList(items, visibleRows, {
154
+ selectedPrefix: (t) => theme.fg("accent", t),
155
+ selectedText: (t) => t, // Keep existing colors
156
+ description: (t) => theme.fg("muted", t),
157
+ scrollInfo: (t) => theme.fg("dim", t),
158
+ noMatch: (t) => theme.fg("warning", t),
159
+ });
160
+ selectList.onSelect = (item) => {
161
+ void openSelected(item.value as FileEntry);
162
+ };
163
+ selectList.onCancel = () => done();
164
+ selectList.onSelectionChange = (item) => {
165
+ currentIndex = items.indexOf(item);
166
+ };
167
+ container.addChild(selectList);
168
+
169
+ // Help text
170
+ container.addChild(
171
+ new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
172
+ );
173
+
174
+ // Bottom border
175
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
176
+
177
+ return {
178
+ render: (w) => container.render(w),
179
+ invalidate: () => container.invalidate(),
180
+ handleInput: (data) => {
181
+ // Add paging with left/right
182
+ if (matchesKey(data, Key.left)) {
183
+ // Page up - clamp to 0
184
+ currentIndex = Math.max(0, currentIndex - visibleRows);
185
+ selectList.setSelectedIndex(currentIndex);
186
+ } else if (matchesKey(data, Key.right)) {
187
+ // Page down - clamp to last
188
+ currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
189
+ selectList.setSelectedIndex(currentIndex);
190
+ } else {
191
+ selectList.handleInput(data);
192
+ }
193
+ tui.requestRender();
194
+ },
195
+ };
196
+ });
197
+ },
198
+ });
199
+ }