stagent 0.3.6 → 0.5.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/README.md +70 -23
- package/dist/cli.js +44 -10
- package/docs/.last-generated +1 -1
- package/docs/features/chat.md +54 -49
- package/docs/features/schedules.md +38 -32
- package/docs/features/settings.md +105 -50
- package/docs/manifest.json +8 -8
- package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
- package/drizzle.config.ts +3 -1
- package/package.json +5 -1
- package/src/app/api/book/bookmarks/route.ts +73 -0
- package/src/app/api/book/progress/route.ts +79 -0
- package/src/app/api/book/regenerate/route.ts +111 -0
- package/src/app/api/book/stage/route.ts +13 -0
- package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
- package/src/app/api/chat/conversations/[id]/respond/route.ts +19 -20
- package/src/app/api/chat/conversations/[id]/route.ts +2 -1
- package/src/app/api/chat/entities/search/route.ts +97 -0
- package/src/app/api/documents/[id]/file/route.ts +4 -1
- package/src/app/api/documents/[id]/route.ts +34 -2
- package/src/app/api/documents/route.ts +91 -0
- package/src/app/api/projects/[id]/route.ts +119 -9
- package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
- package/src/app/api/settings/browser-tools/route.ts +68 -0
- package/src/app/api/settings/runtime/route.ts +29 -8
- package/src/app/book/page.tsx +14 -0
- package/src/app/chat/page.tsx +7 -1
- package/src/app/globals.css +375 -0
- package/src/app/projects/[id]/page.tsx +31 -6
- package/src/app/settings/page.tsx +2 -0
- package/src/app/{playbook → user-guide}/[slug]/page.tsx +12 -2
- package/src/app/{playbook → user-guide}/page.tsx +2 -2
- package/src/app/workflows/[id]/page.tsx +28 -2
- package/src/components/book/book-reader.tsx +801 -0
- package/src/components/book/chapter-generation-bar.tsx +109 -0
- package/src/components/book/content-blocks.tsx +432 -0
- package/src/components/book/path-progress.tsx +33 -0
- package/src/components/book/path-selector.tsx +42 -0
- package/src/components/book/try-it-now.tsx +164 -0
- package/src/components/chat/chat-activity-indicator.tsx +92 -0
- package/src/components/chat/chat-command-popover.tsx +277 -0
- package/src/components/chat/chat-input.tsx +85 -10
- package/src/components/chat/chat-message-list.tsx +3 -0
- package/src/components/chat/chat-message.tsx +29 -7
- package/src/components/chat/chat-permission-request.tsx +5 -1
- package/src/components/chat/chat-question.tsx +3 -0
- package/src/components/chat/chat-shell.tsx +159 -24
- package/src/components/chat/conversation-list.tsx +8 -2
- package/src/components/chat/screenshot-gallery.tsx +96 -0
- package/src/components/monitoring/log-entry.tsx +61 -27
- package/src/components/playbook/adoption-heatmap.tsx +1 -1
- package/src/components/playbook/journey-card.tsx +1 -1
- package/src/components/playbook/playbook-card.tsx +1 -1
- package/src/components/playbook/playbook-detail-view.tsx +15 -5
- package/src/components/playbook/playbook-homepage.tsx +1 -1
- package/src/components/playbook/playbook-updated-badge.tsx +1 -1
- package/src/components/projects/project-detail.tsx +160 -27
- package/src/components/projects/project-form-sheet.tsx +6 -2
- package/src/components/projects/project-list.tsx +1 -1
- package/src/components/schedules/schedule-create-sheet.tsx +24 -330
- package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
- package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
- package/src/components/schedules/schedule-form.tsx +410 -0
- package/src/components/schedules/schedule-list.tsx +16 -0
- package/src/components/settings/browser-tools-section.tsx +247 -0
- package/src/components/settings/runtime-timeout-section.tsx +117 -37
- package/src/components/shared/app-sidebar.tsx +7 -1
- package/src/components/shared/command-palette.tsx +4 -33
- package/src/components/shared/screenshot-lightbox.tsx +151 -0
- package/src/hooks/use-caret-position.ts +104 -0
- package/src/hooks/use-chapter-generation.ts +255 -0
- package/src/hooks/use-chat-autocomplete.ts +290 -0
- package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/browser-mcp.ts +119 -0
- package/src/lib/agents/claude-agent.ts +78 -14
- package/src/lib/book/chapter-generator.ts +193 -0
- package/src/lib/book/chapter-mapping.ts +91 -0
- package/src/lib/book/content.ts +251 -0
- package/src/lib/book/markdown-parser.ts +317 -0
- package/src/lib/book/reading-paths.ts +82 -0
- package/src/lib/book/types.ts +152 -0
- package/src/lib/book/update-detector.ts +157 -0
- package/src/lib/chat/codex-engine.ts +537 -0
- package/src/lib/chat/command-data.ts +50 -0
- package/src/lib/chat/context-builder.ts +145 -7
- package/src/lib/chat/engine.ts +207 -49
- package/src/lib/chat/model-discovery.ts +13 -5
- package/src/lib/chat/permission-bridge.ts +14 -2
- package/src/lib/chat/slash-commands.ts +191 -0
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +16 -1
- package/src/lib/chat/tool-catalog.ts +185 -0
- package/src/lib/chat/tools/chat-history-tools.ts +177 -0
- package/src/lib/chat/tools/document-tools.ts +241 -0
- package/src/lib/chat/tools/settings-tools.ts +29 -3
- package/src/lib/chat/types.ts +19 -2
- package/src/lib/constants/settings.ts +5 -0
- package/src/lib/data/chat.ts +83 -2
- package/src/lib/data/clear.ts +24 -4
- package/src/lib/db/bootstrap.ts +29 -0
- package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
- package/src/lib/db/schema.ts +37 -0
- package/src/lib/docs/types.ts +9 -0
- package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
- package/src/lib/screenshots/persist.ts +114 -0
- package/src/lib/utils/stagent-paths.ts +4 -0
- /package/src/app/api/{playbook → user-guide}/status/route.ts +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock the settings helpers before importing the module under test
|
|
4
|
+
vi.mock("@/lib/settings/helpers", () => ({
|
|
5
|
+
getSetting: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { getSetting } from "@/lib/settings/helpers";
|
|
9
|
+
import {
|
|
10
|
+
getBrowserMcpServers,
|
|
11
|
+
getBrowserAllowedToolPatterns,
|
|
12
|
+
isBrowserTool,
|
|
13
|
+
isBrowserReadOnly,
|
|
14
|
+
} from "@/lib/agents/browser-mcp";
|
|
15
|
+
|
|
16
|
+
const mockGetSetting = vi.mocked(getSetting);
|
|
17
|
+
|
|
18
|
+
describe("browser-mcp", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("getBrowserMcpServers", () => {
|
|
24
|
+
it("returns empty object when neither server is enabled", async () => {
|
|
25
|
+
mockGetSetting.mockResolvedValue(null);
|
|
26
|
+
const servers = await getBrowserMcpServers();
|
|
27
|
+
expect(servers).toEqual({});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns chrome-devtools config when enabled", async () => {
|
|
31
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
32
|
+
if (key === "browser.chromeDevtoolsEnabled") return "true";
|
|
33
|
+
return null;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const servers = await getBrowserMcpServers();
|
|
37
|
+
expect(servers["chrome-devtools"]).toBeDefined();
|
|
38
|
+
expect(servers["chrome-devtools"].command).toBe("npx");
|
|
39
|
+
expect(servers["chrome-devtools"].args).toContain("chrome-devtools-mcp@latest");
|
|
40
|
+
expect(servers.playwright).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns playwright config when enabled", async () => {
|
|
44
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
45
|
+
if (key === "browser.playwrightEnabled") return "true";
|
|
46
|
+
return null;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const servers = await getBrowserMcpServers();
|
|
50
|
+
expect(servers.playwright).toBeDefined();
|
|
51
|
+
expect(servers.playwright.command).toBe("npx");
|
|
52
|
+
expect(servers.playwright.args).toContain("@playwright/mcp@latest");
|
|
53
|
+
expect(servers["chrome-devtools"]).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns both when both enabled", async () => {
|
|
57
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
58
|
+
if (key === "browser.chromeDevtoolsEnabled") return "true";
|
|
59
|
+
if (key === "browser.playwrightEnabled") return "true";
|
|
60
|
+
return null;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const servers = await getBrowserMcpServers();
|
|
64
|
+
expect(Object.keys(servers)).toHaveLength(2);
|
|
65
|
+
expect(servers["chrome-devtools"]).toBeDefined();
|
|
66
|
+
expect(servers.playwright).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("appends extra args from config", async () => {
|
|
70
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
71
|
+
if (key === "browser.chromeDevtoolsEnabled") return "true";
|
|
72
|
+
if (key === "browser.chromeDevtoolsConfig") return "--headless --browser-url http://localhost:9222";
|
|
73
|
+
return null;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const servers = await getBrowserMcpServers();
|
|
77
|
+
expect(servers["chrome-devtools"].args).toContain("--headless");
|
|
78
|
+
expect(servers["chrome-devtools"].args).toContain("--browser-url");
|
|
79
|
+
expect(servers["chrome-devtools"].args).toContain("http://localhost:9222");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles JSON array config format", async () => {
|
|
83
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
84
|
+
if (key === "browser.playwrightEnabled") return "true";
|
|
85
|
+
if (key === "browser.playwrightConfig") return '["--browser", "firefox"]';
|
|
86
|
+
return null;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const servers = await getBrowserMcpServers();
|
|
90
|
+
expect(servers.playwright.args).toContain("--browser");
|
|
91
|
+
expect(servers.playwright.args).toContain("firefox");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("handles invalid JSON config as space-separated args", async () => {
|
|
95
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
96
|
+
if (key === "browser.chromeDevtoolsEnabled") return "true";
|
|
97
|
+
if (key === "browser.chromeDevtoolsConfig") return "[invalid json";
|
|
98
|
+
return null;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const servers = await getBrowserMcpServers();
|
|
102
|
+
// Invalid JSON starting with [ falls back to space-split
|
|
103
|
+
expect(servers["chrome-devtools"].args).toEqual(["-y", "chrome-devtools-mcp@latest", "[invalid", "json"]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("getBrowserAllowedToolPatterns", () => {
|
|
108
|
+
it("returns empty when nothing enabled", async () => {
|
|
109
|
+
mockGetSetting.mockResolvedValue(null);
|
|
110
|
+
const patterns = await getBrowserAllowedToolPatterns();
|
|
111
|
+
expect(patterns).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns chrome pattern when chrome enabled", async () => {
|
|
115
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
116
|
+
if (key === "browser.chromeDevtoolsEnabled") return "true";
|
|
117
|
+
return null;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const patterns = await getBrowserAllowedToolPatterns();
|
|
121
|
+
expect(patterns).toContain("mcp__chrome-devtools__*");
|
|
122
|
+
expect(patterns).not.toContain("mcp__playwright__*");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("returns both patterns when both enabled", async () => {
|
|
126
|
+
mockGetSetting.mockImplementation(async (key: string) => {
|
|
127
|
+
if (key === "browser.chromeDevtoolsEnabled") return "true";
|
|
128
|
+
if (key === "browser.playwrightEnabled") return "true";
|
|
129
|
+
return null;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const patterns = await getBrowserAllowedToolPatterns();
|
|
133
|
+
expect(patterns).toContain("mcp__chrome-devtools__*");
|
|
134
|
+
expect(patterns).toContain("mcp__playwright__*");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("isBrowserTool", () => {
|
|
139
|
+
it("identifies chrome devtools tools", () => {
|
|
140
|
+
expect(isBrowserTool("mcp__chrome-devtools__click")).toBe(true);
|
|
141
|
+
expect(isBrowserTool("mcp__chrome-devtools__take_screenshot")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("identifies playwright tools", () => {
|
|
145
|
+
expect(isBrowserTool("mcp__playwright__browser_navigate")).toBe(true);
|
|
146
|
+
expect(isBrowserTool("mcp__playwright__browser_snapshot")).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("rejects non-browser tools", () => {
|
|
150
|
+
expect(isBrowserTool("mcp__stagent__list_tasks")).toBe(false);
|
|
151
|
+
expect(isBrowserTool("Read")).toBe(false);
|
|
152
|
+
expect(isBrowserTool("Bash")).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("isBrowserReadOnly", () => {
|
|
157
|
+
it("identifies read-only chrome devtools tools", () => {
|
|
158
|
+
expect(isBrowserReadOnly("mcp__chrome-devtools__take_screenshot")).toBe(true);
|
|
159
|
+
expect(isBrowserReadOnly("mcp__chrome-devtools__list_pages")).toBe(true);
|
|
160
|
+
expect(isBrowserReadOnly("mcp__chrome-devtools__lighthouse_audit")).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("identifies read-only playwright tools", () => {
|
|
164
|
+
expect(isBrowserReadOnly("mcp__playwright__browser_snapshot")).toBe(true);
|
|
165
|
+
expect(isBrowserReadOnly("mcp__playwright__browser_tabs")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("rejects mutation browser tools", () => {
|
|
169
|
+
expect(isBrowserReadOnly("mcp__chrome-devtools__click")).toBe(false);
|
|
170
|
+
expect(isBrowserReadOnly("mcp__chrome-devtools__fill")).toBe(false);
|
|
171
|
+
expect(isBrowserReadOnly("mcp__playwright__browser_navigate")).toBe(false);
|
|
172
|
+
expect(isBrowserReadOnly("mcp__playwright__browser_click")).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -132,6 +132,9 @@ vi.mock("@/lib/agents/pattern-extractor", () => ({
|
|
|
132
132
|
vi.mock("@/lib/agents/sweep", () => ({
|
|
133
133
|
processSweepResult: mockProcessSweepResult,
|
|
134
134
|
}));
|
|
135
|
+
vi.mock("@/lib/agents/browser-mcp", () => ({
|
|
136
|
+
getBrowserMcpServers: vi.fn().mockResolvedValue({}),
|
|
137
|
+
}));
|
|
135
138
|
|
|
136
139
|
// Static imports (works because vi.mock is hoisted)
|
|
137
140
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getSetting } from "@/lib/settings/helpers";
|
|
2
|
+
import { SETTINGS_KEYS } from "@/lib/constants/settings";
|
|
3
|
+
|
|
4
|
+
// ── MCP server config type (matches Claude Agent SDK shape) ──────────
|
|
5
|
+
|
|
6
|
+
interface McpServerConfig {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ── Read-only browser tools — auto-approved in chat & task permission callbacks
|
|
12
|
+
|
|
13
|
+
export const BROWSER_READ_ONLY_TOOLS = new Set([
|
|
14
|
+
// Chrome DevTools MCP — read-only
|
|
15
|
+
"mcp__chrome-devtools__take_screenshot",
|
|
16
|
+
"mcp__chrome-devtools__take_snapshot",
|
|
17
|
+
"mcp__chrome-devtools__take_memory_snapshot",
|
|
18
|
+
"mcp__chrome-devtools__list_pages",
|
|
19
|
+
"mcp__chrome-devtools__list_console_messages",
|
|
20
|
+
"mcp__chrome-devtools__list_network_requests",
|
|
21
|
+
"mcp__chrome-devtools__get_console_message",
|
|
22
|
+
"mcp__chrome-devtools__get_network_request",
|
|
23
|
+
"mcp__chrome-devtools__lighthouse_audit",
|
|
24
|
+
"mcp__chrome-devtools__performance_start_trace",
|
|
25
|
+
"mcp__chrome-devtools__performance_stop_trace",
|
|
26
|
+
"mcp__chrome-devtools__performance_analyze_insight",
|
|
27
|
+
// Playwright MCP — read-only
|
|
28
|
+
"mcp__playwright__browser_snapshot",
|
|
29
|
+
"mcp__playwright__browser_console_messages",
|
|
30
|
+
"mcp__playwright__browser_network_requests",
|
|
31
|
+
"mcp__playwright__browser_tabs",
|
|
32
|
+
"mcp__playwright__browser_take_screenshot",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// ── Helper: check if a tool name belongs to a browser MCP server ─────
|
|
36
|
+
|
|
37
|
+
export function isBrowserTool(toolName: string): boolean {
|
|
38
|
+
return (
|
|
39
|
+
toolName.startsWith("mcp__chrome-devtools__") ||
|
|
40
|
+
toolName.startsWith("mcp__playwright__")
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isBrowserReadOnly(toolName: string): boolean {
|
|
45
|
+
return BROWSER_READ_ONLY_TOOLS.has(toolName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Config builder ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function parseExtraArgs(config: string | null): string[] {
|
|
51
|
+
if (!config) return [];
|
|
52
|
+
const trimmed = config.trim();
|
|
53
|
+
if (!trimmed) return [];
|
|
54
|
+
|
|
55
|
+
// Try JSON array first (e.g. '["--browser", "firefox"]')
|
|
56
|
+
if (trimmed.startsWith("[")) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(trimmed);
|
|
59
|
+
if (Array.isArray(parsed)) return parsed.filter((a): a is string => typeof a === "string");
|
|
60
|
+
} catch {
|
|
61
|
+
// Fall through to space-split
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Plain string: split on whitespace (e.g. "--headless --browser-url http://localhost:9222")
|
|
66
|
+
return trimmed.split(/\s+/).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read browser MCP settings from DB and return MCP server configs
|
|
71
|
+
* for any enabled browser servers.
|
|
72
|
+
*
|
|
73
|
+
* Returns `{}` when neither server is enabled — zero overhead.
|
|
74
|
+
*/
|
|
75
|
+
export async function getBrowserMcpServers(): Promise<Record<string, McpServerConfig>> {
|
|
76
|
+
const [chromeEnabled, playwrightEnabled, chromeConfig, playwrightConfig] =
|
|
77
|
+
await Promise.all([
|
|
78
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
|
|
79
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
|
|
80
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG),
|
|
81
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const servers: Record<string, McpServerConfig> = {};
|
|
85
|
+
|
|
86
|
+
if (chromeEnabled === "true") {
|
|
87
|
+
const extraArgs = parseExtraArgs(chromeConfig);
|
|
88
|
+
servers["chrome-devtools"] = {
|
|
89
|
+
command: "npx",
|
|
90
|
+
args: ["-y", "chrome-devtools-mcp@latest", ...extraArgs],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (playwrightEnabled === "true") {
|
|
95
|
+
const extraArgs = parseExtraArgs(playwrightConfig);
|
|
96
|
+
servers.playwright = {
|
|
97
|
+
command: "npx",
|
|
98
|
+
args: ["-y", "@playwright/mcp@latest", ...extraArgs],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return servers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build the allowedTools glob patterns for enabled browser MCP servers.
|
|
107
|
+
* Returns an empty array when no browser servers are enabled.
|
|
108
|
+
*/
|
|
109
|
+
export async function getBrowserAllowedToolPatterns(): Promise<string[]> {
|
|
110
|
+
const [chromeEnabled, playwrightEnabled] = await Promise.all([
|
|
111
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
|
|
112
|
+
getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const patterns: string[] = [];
|
|
116
|
+
if (chromeEnabled === "true") patterns.push("mcp__chrome-devtools__*");
|
|
117
|
+
if (playwrightEnabled === "true") patterns.push("mcp__playwright__*");
|
|
118
|
+
return patterns;
|
|
119
|
+
}
|
|
@@ -17,9 +17,11 @@ import { resolveProfileRuntimePayload, type ResolvedProfileRuntimePayload } from
|
|
|
17
17
|
import type { CanUseToolPolicy } from "./profiles/types";
|
|
18
18
|
import { buildClaudeSdkEnv } from "./runtime/claude-sdk";
|
|
19
19
|
import { getActiveLearnedContext } from "./learned-context";
|
|
20
|
-
import { getLaunchCwd } from "@/lib/environment/workspace-context";
|
|
20
|
+
import { getLaunchCwd, getWorkspaceContext } from "@/lib/environment/workspace-context";
|
|
21
21
|
import { analyzeForLearnedPatterns } from "./pattern-extractor";
|
|
22
22
|
import { processSweepResult } from "./sweep";
|
|
23
|
+
import { getBrowserMcpServers } from "./browser-mcp";
|
|
24
|
+
import { persistScreenshot, SCREENSHOT_TOOL_NAMES } from "@/lib/screenshots/persist";
|
|
23
25
|
import {
|
|
24
26
|
extractUsageSnapshot,
|
|
25
27
|
mergeUsageSnapshot,
|
|
@@ -210,6 +212,9 @@ async function processAgentStream(
|
|
|
210
212
|
let receivedResult = false;
|
|
211
213
|
let turnCount = 0;
|
|
212
214
|
|
|
215
|
+
// Screenshot interception state
|
|
216
|
+
const pendingScreenshotTools = new Set<string>();
|
|
217
|
+
|
|
213
218
|
for await (const raw of response) {
|
|
214
219
|
const message = raw as AgentStreamMessage;
|
|
215
220
|
applyUsageSnapshot(usageState, raw);
|
|
@@ -266,6 +271,10 @@ async function processAgentStream(
|
|
|
266
271
|
turnCount++;
|
|
267
272
|
for (const block of message.message.content) {
|
|
268
273
|
if (block.type === "tool_use") {
|
|
274
|
+
// Track screenshot tool_use IDs for result interception
|
|
275
|
+
if (typeof block.name === "string" && SCREENSHOT_TOOL_NAMES.has(block.name) && typeof block.id === "string") {
|
|
276
|
+
pendingScreenshotTools.add(block.id);
|
|
277
|
+
}
|
|
269
278
|
await db.insert(agentLogs).values({
|
|
270
279
|
id: crypto.randomUUID(),
|
|
271
280
|
taskId,
|
|
@@ -281,6 +290,47 @@ async function processAgentStream(
|
|
|
281
290
|
}
|
|
282
291
|
}
|
|
283
292
|
|
|
293
|
+
// Intercept tool results containing screenshot image data
|
|
294
|
+
if (message.type === "user" && pendingScreenshotTools.size > 0) {
|
|
295
|
+
const userMsg = (raw as Record<string, unknown>).message as Record<string, unknown> | undefined;
|
|
296
|
+
const userContent = userMsg?.content as Array<Record<string, unknown>> | undefined;
|
|
297
|
+
if (userContent) {
|
|
298
|
+
for (const block of userContent) {
|
|
299
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string" && pendingScreenshotTools.has(block.tool_use_id)) {
|
|
300
|
+
pendingScreenshotTools.delete(block.tool_use_id);
|
|
301
|
+
const resultContent = block.content as Array<Record<string, unknown>> | undefined;
|
|
302
|
+
if (resultContent) {
|
|
303
|
+
for (const item of resultContent) {
|
|
304
|
+
if (item.type === "image" && typeof item.source === "object" && item.source !== null) {
|
|
305
|
+
const source = item.source as Record<string, unknown>;
|
|
306
|
+
if (source.type === "base64" && typeof source.data === "string") {
|
|
307
|
+
const attachment = await persistScreenshot(source.data, {
|
|
308
|
+
taskId,
|
|
309
|
+
toolName: `screenshot_${block.tool_use_id}`,
|
|
310
|
+
});
|
|
311
|
+
if (attachment) {
|
|
312
|
+
await db.insert(agentLogs).values({
|
|
313
|
+
id: crypto.randomUUID(),
|
|
314
|
+
taskId,
|
|
315
|
+
agentType: agentProfileId,
|
|
316
|
+
event: "screenshot",
|
|
317
|
+
payload: JSON.stringify({
|
|
318
|
+
documentId: attachment.documentId,
|
|
319
|
+
thumbnailUrl: attachment.thumbnailUrl,
|
|
320
|
+
toolName: `screenshot_${block.tool_use_id}`,
|
|
321
|
+
}),
|
|
322
|
+
timestamp: new Date(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
284
334
|
// Handle result — skip if task was cancelled mid-stream
|
|
285
335
|
if (message.type === "result" && "result" in raw) {
|
|
286
336
|
if (abortController.signal.aborted) {
|
|
@@ -415,11 +465,6 @@ async function buildTaskQueryContext(
|
|
|
415
465
|
? `## Learned Context\nPatterns and insights learned from previous tasks:\n\n${learnedCtx}`
|
|
416
466
|
: "";
|
|
417
467
|
|
|
418
|
-
// F1: Separate system instructions from user content
|
|
419
|
-
const systemInstructions = [profileInstructions, learnedCtxBlock, docContext, outputInstructions]
|
|
420
|
-
.filter(Boolean)
|
|
421
|
-
.join("\n\n");
|
|
422
|
-
|
|
423
468
|
// Resolve working directory: project's workingDirectory > launch cwd
|
|
424
469
|
let cwd = getLaunchCwd();
|
|
425
470
|
if (task.projectId) {
|
|
@@ -432,6 +477,17 @@ async function buildTaskQueryContext(
|
|
|
432
477
|
}
|
|
433
478
|
}
|
|
434
479
|
|
|
480
|
+
// Add worktree guidance when running inside a git worktree
|
|
481
|
+
const ws = getWorkspaceContext();
|
|
482
|
+
const worktreeNote = ws.isWorktree
|
|
483
|
+
? `## Workspace Note\nYou are operating inside a git worktree (branch: ${ws.gitBranch ?? "unknown"}). All file operations MUST use paths relative to the working directory: ${cwd}. Do NOT navigate to or create files in the main repository directory.`
|
|
484
|
+
: "";
|
|
485
|
+
|
|
486
|
+
// F1: Separate system instructions from user content
|
|
487
|
+
const systemInstructions = [worktreeNote, profileInstructions, learnedCtxBlock, docContext, outputInstructions]
|
|
488
|
+
.filter(Boolean)
|
|
489
|
+
.join("\n\n");
|
|
490
|
+
|
|
435
491
|
// F9: Use profile maxTurns or fall back to default
|
|
436
492
|
const maxTurns = profile?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
437
493
|
|
|
@@ -464,6 +520,11 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
|
|
|
464
520
|
await prepareTaskOutputDirectory(taskId, { clearExisting: true });
|
|
465
521
|
const ctx = await buildTaskQueryContext(task, agentProfileId);
|
|
466
522
|
|
|
523
|
+
// Merge browser MCP servers when enabled globally
|
|
524
|
+
const browserServers = await getBrowserMcpServers();
|
|
525
|
+
const profileMcpServers = ctx.payload?.mcpServers ?? {};
|
|
526
|
+
const mergedMcpServers = { ...profileMcpServers, ...browserServers };
|
|
527
|
+
|
|
467
528
|
const authEnv = await getAuthEnv();
|
|
468
529
|
const response = query({
|
|
469
530
|
prompt: ctx.userPrompt,
|
|
@@ -481,10 +542,9 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
|
|
|
481
542
|
// F4: Per-execution budget cap
|
|
482
543
|
maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
|
|
483
544
|
...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
|
|
484
|
-
...(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}),
|
|
545
|
+
...(Object.keys(mergedMcpServers).length > 0 && {
|
|
546
|
+
mcpServers: mergedMcpServers,
|
|
547
|
+
}),
|
|
488
548
|
// @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
|
|
489
549
|
canUseTool: async (
|
|
490
550
|
toolName: string,
|
|
@@ -570,6 +630,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
|
570
630
|
await prepareTaskOutputDirectory(taskId);
|
|
571
631
|
const ctx = await buildTaskQueryContext(task, profileId);
|
|
572
632
|
|
|
633
|
+
// Merge browser MCP servers when enabled globally
|
|
634
|
+
const browserServers = await getBrowserMcpServers();
|
|
635
|
+
const profileMcpServers = ctx.payload?.mcpServers ?? {};
|
|
636
|
+
const mergedMcpServers = { ...profileMcpServers, ...browserServers };
|
|
637
|
+
|
|
573
638
|
const authEnv = await getAuthEnv();
|
|
574
639
|
const response = query({
|
|
575
640
|
prompt: ctx.userPrompt,
|
|
@@ -588,10 +653,9 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
|
|
|
588
653
|
// F4: Per-execution budget cap
|
|
589
654
|
maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
|
|
590
655
|
...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
|
|
591
|
-
...(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}),
|
|
656
|
+
...(Object.keys(mergedMcpServers).length > 0 && {
|
|
657
|
+
mcpServers: mergedMcpServers,
|
|
658
|
+
}),
|
|
595
659
|
// @ts-expect-error Agent SDK canUseTool types are incomplete — our async handler is compatible at runtime
|
|
596
660
|
canUseTool: async (
|
|
597
661
|
toolName: string,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { CHAPTER_MAPPING } from "./chapter-mapping";
|
|
4
|
+
import { getChapter } from "./content";
|
|
5
|
+
|
|
6
|
+
/** Shared context gathered from disk for prompt assembly */
|
|
7
|
+
interface ChapterContext {
|
|
8
|
+
chapterId: string;
|
|
9
|
+
chapterNumber: number;
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle: string;
|
|
12
|
+
partNumber: number;
|
|
13
|
+
partTitle: string;
|
|
14
|
+
readingTime: number;
|
|
15
|
+
slug: string;
|
|
16
|
+
relatedDocs: string[];
|
|
17
|
+
relatedJourney: string | undefined;
|
|
18
|
+
currentMarkdown: string | null;
|
|
19
|
+
sourceContents: string[];
|
|
20
|
+
strategy: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Gather all context needed for chapter generation from disk */
|
|
24
|
+
export function gatherChapterContext(chapterId: string): ChapterContext {
|
|
25
|
+
const chapter = getChapter(chapterId);
|
|
26
|
+
if (!chapter) {
|
|
27
|
+
throw new Error(`Chapter not found: ${chapterId}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mapping = CHAPTER_MAPPING[chapterId];
|
|
31
|
+
const sourceDocSlugs = mapping?.docs ?? [];
|
|
32
|
+
const slug = chapterIdToSlug(chapterId);
|
|
33
|
+
|
|
34
|
+
// Read the current chapter markdown (if it exists)
|
|
35
|
+
const chapterMdPath = join(process.cwd(), "book", "chapters", `${slug}.md`);
|
|
36
|
+
const currentMarkdown = existsSync(chapterMdPath)
|
|
37
|
+
? readFileSync(chapterMdPath, "utf-8")
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
// Read related playbook docs for content
|
|
41
|
+
const sourceContents: string[] = [];
|
|
42
|
+
for (const docSlug of sourceDocSlugs) {
|
|
43
|
+
const docPath = join(process.cwd(), "docs", "features", `${docSlug}.md`);
|
|
44
|
+
if (existsSync(docPath)) {
|
|
45
|
+
const content = readFileSync(docPath, "utf-8");
|
|
46
|
+
sourceContents.push(`### Feature: ${docSlug}\n${content}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read the book strategy document
|
|
51
|
+
const strategyPath = join(process.cwd(), "ai-native-notes", "ai-native-book-strategy.md");
|
|
52
|
+
const strategy = existsSync(strategyPath)
|
|
53
|
+
? readFileSync(strategyPath, "utf-8")
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
chapterId,
|
|
58
|
+
chapterNumber: chapter.number,
|
|
59
|
+
title: chapter.title,
|
|
60
|
+
subtitle: chapter.subtitle,
|
|
61
|
+
partNumber: chapter.part.number,
|
|
62
|
+
partTitle: chapter.part.title,
|
|
63
|
+
readingTime: chapter.readingTime,
|
|
64
|
+
slug,
|
|
65
|
+
relatedDocs: chapter.relatedDocs ?? [],
|
|
66
|
+
relatedJourney: chapter.relatedJourney,
|
|
67
|
+
currentMarkdown,
|
|
68
|
+
sourceContents,
|
|
69
|
+
strategy,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build an agent prompt for generating or regenerating a book chapter.
|
|
75
|
+
* Assembles context from the current chapter, source files,
|
|
76
|
+
* and the book strategy document.
|
|
77
|
+
*/
|
|
78
|
+
export function buildChapterRegenerationPrompt(chapterId: string): string {
|
|
79
|
+
const ctx = gatherChapterContext(chapterId);
|
|
80
|
+
const isNew = ctx.currentMarkdown === null;
|
|
81
|
+
const verb = isNew ? "generating" : "regenerating";
|
|
82
|
+
const outputPath = `book/chapters/${ctx.slug}.md`;
|
|
83
|
+
|
|
84
|
+
// Assemble the prompt
|
|
85
|
+
const sections: string[] = [
|
|
86
|
+
`# Chapter ${isNew ? "Generation" : "Regeneration"}: ${ctx.title}`,
|
|
87
|
+
"",
|
|
88
|
+
`You are ${verb} Chapter ${ctx.chapterNumber}: "${ctx.title}" — ${ctx.subtitle}`,
|
|
89
|
+
`This chapter belongs to Part ${ctx.partNumber}: ${ctx.partTitle}.`,
|
|
90
|
+
"",
|
|
91
|
+
"## Instructions",
|
|
92
|
+
"",
|
|
93
|
+
`${isNew ? "Write" : "Regenerate"} this book chapter following these rules:`,
|
|
94
|
+
"1. **Narrative voice**: Write in first-person plural ('we') with a technical-but-approachable tone",
|
|
95
|
+
"2. **Structure**: Follow the Problem → Solution → Implementation → Lessons pattern",
|
|
96
|
+
"3. **Code examples**: Include real code snippets from the Stagent codebase with filename comments",
|
|
97
|
+
"4. **Markdown format**: Use the conventions below for content blocks",
|
|
98
|
+
"5. **Reading time**: Target approximately " + ctx.readingTime + " minutes",
|
|
99
|
+
...(isNew ? [] : ["6. **Preserve Author's Notes**: Keep any existing `> [!authors-note]` blocks unchanged"]),
|
|
100
|
+
"",
|
|
101
|
+
"## Tools Available",
|
|
102
|
+
"",
|
|
103
|
+
"You have access to **Read**, **Write**, and **Edit** tools.",
|
|
104
|
+
"- Use **Read** to examine Stagent source code files for real code examples",
|
|
105
|
+
"- Use **Write** to create the final chapter file",
|
|
106
|
+
"",
|
|
107
|
+
"## Markdown Conventions",
|
|
108
|
+
"",
|
|
109
|
+
"- Sections: `## Section Title`",
|
|
110
|
+
"- Code blocks: ` ```language ` with `<!-- filename: path -->` before the block",
|
|
111
|
+
"- Callouts: `> [!tip]`, `> [!warning]`, `> [!info]`, `> [!lesson]`, `> [!authors-note]`",
|
|
112
|
+
"- Interactive links: `[Try: label](href)`",
|
|
113
|
+
"- Images: ``",
|
|
114
|
+
"",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (ctx.currentMarkdown) {
|
|
118
|
+
sections.push(
|
|
119
|
+
"## Current Chapter Content",
|
|
120
|
+
"",
|
|
121
|
+
"Update this content to reflect any changes in the source material:",
|
|
122
|
+
"",
|
|
123
|
+
"```markdown",
|
|
124
|
+
ctx.currentMarkdown,
|
|
125
|
+
"```",
|
|
126
|
+
""
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (ctx.sourceContents.length > 0) {
|
|
131
|
+
sections.push(
|
|
132
|
+
"## Source Material (Playbook Feature Docs)",
|
|
133
|
+
"",
|
|
134
|
+
"Use these feature docs as the primary source of technical details:",
|
|
135
|
+
"",
|
|
136
|
+
...ctx.sourceContents,
|
|
137
|
+
""
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (ctx.strategy) {
|
|
142
|
+
sections.push(
|
|
143
|
+
"## Book Strategy Reference",
|
|
144
|
+
"",
|
|
145
|
+
"Follow the themes and narrative arc described here:",
|
|
146
|
+
"",
|
|
147
|
+
ctx.strategy.slice(0, 4000), // Truncate to avoid excessive context
|
|
148
|
+
""
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
sections.push(
|
|
154
|
+
"## Output",
|
|
155
|
+
"",
|
|
156
|
+
`Write the chapter to the file: \`${outputPath}\``,
|
|
157
|
+
"",
|
|
158
|
+
"The file must include this frontmatter at the top:",
|
|
159
|
+
"",
|
|
160
|
+
"```yaml",
|
|
161
|
+
"---",
|
|
162
|
+
`title: "${ctx.title}"`,
|
|
163
|
+
`subtitle: "${ctx.subtitle}"`,
|
|
164
|
+
`chapter: ${ctx.chapterNumber}`,
|
|
165
|
+
`part: ${ctx.partNumber}`,
|
|
166
|
+
`readingTime: ${ctx.readingTime}`,
|
|
167
|
+
`lastGeneratedBy: "${now}"`,
|
|
168
|
+
...(ctx.relatedDocs.length > 0 ? [`relatedDocs: ${JSON.stringify(ctx.relatedDocs)}`] : []),
|
|
169
|
+
...(ctx.relatedJourney ? [`relatedJourney: "${ctx.relatedJourney}"`] : []),
|
|
170
|
+
"---",
|
|
171
|
+
"```",
|
|
172
|
+
"",
|
|
173
|
+
"After the frontmatter, write the chapter body starting with the first `## Section Title` heading.",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return sections.join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Map chapter ID to markdown filename slug */
|
|
180
|
+
function chapterIdToSlug(chapterId: string): string {
|
|
181
|
+
const slugMap: Record<string, string> = {
|
|
182
|
+
"ch-1": "ch-1-project-management",
|
|
183
|
+
"ch-2": "ch-2-task-execution",
|
|
184
|
+
"ch-3": "ch-3-document-processing",
|
|
185
|
+
"ch-4": "ch-4-workflow-orchestration",
|
|
186
|
+
"ch-5": "ch-5-scheduled-intelligence",
|
|
187
|
+
"ch-6": "ch-6-agent-self-improvement",
|
|
188
|
+
"ch-7": "ch-7-multi-agent-swarms",
|
|
189
|
+
"ch-8": "ch-8-human-in-the-loop",
|
|
190
|
+
"ch-9": "ch-9-autonomous-organization",
|
|
191
|
+
};
|
|
192
|
+
return slugMap[chapterId] ?? chapterId;
|
|
193
|
+
}
|