@ilya-lesikov/pi-pi 0.1.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/3p/pi-ask-user/index.ts +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Storage Utility
|
|
3
|
+
*
|
|
4
|
+
* Saves plans and annotations to ~/.plannotator/plans/
|
|
5
|
+
* Cross-platform: works on Windows, macOS, and Linux.
|
|
6
|
+
*
|
|
7
|
+
* Runtime-agnostic: uses only node:fs, node:path, node:os.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join, resolve, sep } from "path";
|
|
11
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } from "fs";
|
|
12
|
+
import { sanitizeTag } from "./project";
|
|
13
|
+
import { resolveUserPath } from "./resolve-file";
|
|
14
|
+
import { getPlannotatorDataDir } from "./data-dir";
|
|
15
|
+
|
|
16
|
+
const DATA_DIR = getPlannotatorDataDir();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the plan storage directory, creating it if needed.
|
|
20
|
+
* Cross-platform: uses os.homedir() for Windows/macOS/Linux compatibility.
|
|
21
|
+
* @param customPath Optional custom path. Supports ~ for home directory.
|
|
22
|
+
*/
|
|
23
|
+
export function getPlanDir(customPath?: string | null): string {
|
|
24
|
+
let planDir: string;
|
|
25
|
+
|
|
26
|
+
if (customPath?.trim()) {
|
|
27
|
+
planDir = resolveUserPath(customPath);
|
|
28
|
+
} else {
|
|
29
|
+
planDir = join(DATA_DIR, "plans");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mkdirSync(planDir, { recursive: true });
|
|
33
|
+
return planDir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract the first heading from markdown content.
|
|
38
|
+
*/
|
|
39
|
+
function extractFirstHeading(markdown: string): string | null {
|
|
40
|
+
const match = markdown.match(/^#\s+(.+)$/m);
|
|
41
|
+
if (!match) return null;
|
|
42
|
+
return match[1].trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate a slug from plan content.
|
|
47
|
+
* Format: {sanitized-heading}-YYYY-MM-DD
|
|
48
|
+
*/
|
|
49
|
+
export function generateSlug(plan: string): string {
|
|
50
|
+
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
51
|
+
|
|
52
|
+
const heading = extractFirstHeading(plan);
|
|
53
|
+
const slug = heading ? sanitizeTag(heading) : null;
|
|
54
|
+
|
|
55
|
+
return slug ? `${slug}-${date}` : `plan-${date}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save the plan markdown to disk.
|
|
60
|
+
* Returns the full path to the saved file.
|
|
61
|
+
*/
|
|
62
|
+
export function savePlan(slug: string, content: string, customPath?: string | null): string {
|
|
63
|
+
const planDir = getPlanDir(customPath);
|
|
64
|
+
const filePath = join(planDir, `${slug}.md`);
|
|
65
|
+
writeFileSync(filePath, content, "utf-8");
|
|
66
|
+
return filePath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Save annotations to disk.
|
|
71
|
+
* Returns the full path to the saved file.
|
|
72
|
+
*/
|
|
73
|
+
export function saveAnnotations(slug: string, annotationsContent: string, customPath?: string | null): string {
|
|
74
|
+
const planDir = getPlanDir(customPath);
|
|
75
|
+
const filePath = join(planDir, `${slug}.annotations.md`);
|
|
76
|
+
writeFileSync(filePath, annotationsContent, "utf-8");
|
|
77
|
+
return filePath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Save the final snapshot on approve/deny.
|
|
82
|
+
* Combines plan and annotations into a single file with status suffix.
|
|
83
|
+
* Returns the full path to the saved file.
|
|
84
|
+
*/
|
|
85
|
+
export function saveFinalSnapshot(
|
|
86
|
+
slug: string,
|
|
87
|
+
status: "approved" | "denied",
|
|
88
|
+
plan: string,
|
|
89
|
+
annotations: string,
|
|
90
|
+
customPath?: string | null
|
|
91
|
+
): string {
|
|
92
|
+
const planDir = getPlanDir(customPath);
|
|
93
|
+
const filePath = join(planDir, `${slug}-${status}.md`);
|
|
94
|
+
|
|
95
|
+
// Combine plan with annotations appended
|
|
96
|
+
let content = plan;
|
|
97
|
+
if (annotations && annotations !== "No changes detected.") {
|
|
98
|
+
content += "\n\n---\n\n" + annotations;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
writeFileSync(filePath, content, "utf-8");
|
|
102
|
+
return filePath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Plan Archive ---
|
|
106
|
+
|
|
107
|
+
export interface ArchivedPlan {
|
|
108
|
+
filename: string;
|
|
109
|
+
title: string;
|
|
110
|
+
date: string;
|
|
111
|
+
timestamp: string; // ISO string from file mtime
|
|
112
|
+
status: "approved" | "denied" | "unknown";
|
|
113
|
+
size: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse an archive filename into metadata.
|
|
118
|
+
* Handles both old (DATE-heading-status.md) and new (heading-DATE-status.md) formats.
|
|
119
|
+
*/
|
|
120
|
+
export function parseArchiveFilename(filename: string): ArchivedPlan | null {
|
|
121
|
+
// Skip non-decision files
|
|
122
|
+
if (filename.endsWith(".annotations.md") || filename.endsWith(".diff.md")) return null;
|
|
123
|
+
|
|
124
|
+
const base = filename.replace(/\.md$/, "");
|
|
125
|
+
|
|
126
|
+
// Extract status suffix
|
|
127
|
+
let status: ArchivedPlan["status"] = "unknown";
|
|
128
|
+
let slug = base;
|
|
129
|
+
if (base.endsWith("-approved")) {
|
|
130
|
+
status = "approved";
|
|
131
|
+
slug = base.slice(0, -"-approved".length);
|
|
132
|
+
} else if (base.endsWith("-denied")) {
|
|
133
|
+
status = "denied";
|
|
134
|
+
slug = base.slice(0, -"-denied".length);
|
|
135
|
+
} else {
|
|
136
|
+
// Skip plain files (no decision status)
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract date (YYYY-MM-DD) — could be anywhere in the slug
|
|
141
|
+
const dateMatch = slug.match(/(\d{4}-\d{2}-\d{2})/);
|
|
142
|
+
const date = dateMatch ? dateMatch[1] : "";
|
|
143
|
+
|
|
144
|
+
// Title: remove date, convert hyphens to spaces, trim
|
|
145
|
+
const title = slug
|
|
146
|
+
.replace(/\d{4}-\d{2}-\d{2}/, "")
|
|
147
|
+
.replace(/^-+|-+$/g, "")
|
|
148
|
+
.replace(/-+/g, " ")
|
|
149
|
+
.trim() || "Untitled Plan";
|
|
150
|
+
|
|
151
|
+
return { filename, title, date, timestamp: "", status, size: 0 };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* List all archived plans (approved/denied decision snapshots).
|
|
156
|
+
* Returns plans sorted by date descending.
|
|
157
|
+
*/
|
|
158
|
+
export function listArchivedPlans(customPath?: string | null): ArchivedPlan[] {
|
|
159
|
+
const planDir = getPlanDir(customPath);
|
|
160
|
+
try {
|
|
161
|
+
const entries = readdirSync(planDir);
|
|
162
|
+
const plans: ArchivedPlan[] = [];
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (!entry.endsWith(".md")) continue;
|
|
165
|
+
const parsed = parseArchiveFilename(entry);
|
|
166
|
+
if (!parsed) continue;
|
|
167
|
+
try {
|
|
168
|
+
const stat = statSync(join(planDir, entry));
|
|
169
|
+
parsed.size = stat.size;
|
|
170
|
+
parsed.timestamp = stat.mtime.toISOString();
|
|
171
|
+
} catch { /* keep defaults */ }
|
|
172
|
+
plans.push(parsed);
|
|
173
|
+
}
|
|
174
|
+
return plans.sort((a, b) => b.date.localeCompare(a.date) || b.timestamp.localeCompare(a.timestamp));
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Read an archived plan file by filename.
|
|
182
|
+
* Returns null if the file doesn't exist or on read error.
|
|
183
|
+
*/
|
|
184
|
+
export function readArchivedPlan(filename: string, customPath?: string | null): string | null {
|
|
185
|
+
const planDir = getPlanDir(customPath);
|
|
186
|
+
const filePath = resolve(planDir, filename);
|
|
187
|
+
// Guard against path traversal (resolve + trailing separator, matching reference-handlers.ts)
|
|
188
|
+
if (!filePath.startsWith(planDir + sep)) return null;
|
|
189
|
+
try {
|
|
190
|
+
return readFileSync(filePath, "utf-8");
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Version History ---
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get the history directory for a project/slug combination, creating it if needed.
|
|
200
|
+
* History is always stored in ~/.plannotator/history/{project}/{slug}/.
|
|
201
|
+
* Not affected by the customPath setting (that only affects decision saves).
|
|
202
|
+
*/
|
|
203
|
+
export function getHistoryDir(project: string, slug: string): string {
|
|
204
|
+
const historyDir = join(DATA_DIR, "history", project, slug);
|
|
205
|
+
mkdirSync(historyDir, { recursive: true });
|
|
206
|
+
return historyDir;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Determine the next version number by scanning existing files.
|
|
211
|
+
* Returns 1 if no versions exist, otherwise max + 1.
|
|
212
|
+
*/
|
|
213
|
+
function getNextVersionNumber(historyDir: string): number {
|
|
214
|
+
try {
|
|
215
|
+
const entries = readdirSync(historyDir);
|
|
216
|
+
let max = 0;
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const match = entry.match(/^(\d+)\.md$/);
|
|
219
|
+
if (match) {
|
|
220
|
+
const num = parseInt(match[1], 10);
|
|
221
|
+
if (num > max) max = num;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return max + 1;
|
|
225
|
+
} catch {
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Save a plan version to the history directory.
|
|
232
|
+
* Deduplication: if the latest version has identical content, skip saving.
|
|
233
|
+
* Returns the version number, file path, and whether a new file was created.
|
|
234
|
+
*/
|
|
235
|
+
export function saveToHistory(
|
|
236
|
+
project: string,
|
|
237
|
+
slug: string,
|
|
238
|
+
plan: string
|
|
239
|
+
): { version: number; path: string; isNew: boolean } {
|
|
240
|
+
const historyDir = getHistoryDir(project, slug);
|
|
241
|
+
const nextVersion = getNextVersionNumber(historyDir);
|
|
242
|
+
|
|
243
|
+
// Deduplicate: check if latest version has identical content
|
|
244
|
+
if (nextVersion > 1) {
|
|
245
|
+
const latestPath = join(historyDir, `${String(nextVersion - 1).padStart(3, "0")}.md`);
|
|
246
|
+
try {
|
|
247
|
+
const existing = readFileSync(latestPath, "utf-8");
|
|
248
|
+
if (existing === plan) {
|
|
249
|
+
return { version: nextVersion - 1, path: latestPath, isNew: false };
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
// File read failed, proceed with saving
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const fileName = `${String(nextVersion).padStart(3, "0")}.md`;
|
|
257
|
+
const filePath = join(historyDir, fileName);
|
|
258
|
+
writeFileSync(filePath, plan, "utf-8");
|
|
259
|
+
return { version: nextVersion, path: filePath, isNew: true };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Read a specific version's content from history.
|
|
264
|
+
* Returns null if the version doesn't exist or on read error.
|
|
265
|
+
*/
|
|
266
|
+
export function getPlanVersion(
|
|
267
|
+
project: string,
|
|
268
|
+
slug: string,
|
|
269
|
+
version: number
|
|
270
|
+
): string | null {
|
|
271
|
+
const historyDir = join(DATA_DIR, "history", project, slug);
|
|
272
|
+
const fileName = `${String(version).padStart(3, "0")}.md`;
|
|
273
|
+
const filePath = join(historyDir, fileName);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
return readFileSync(filePath, "utf-8");
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get the file path for a specific version in history.
|
|
284
|
+
* Returns null if the version file doesn't exist.
|
|
285
|
+
*/
|
|
286
|
+
export function getPlanVersionPath(
|
|
287
|
+
project: string,
|
|
288
|
+
slug: string,
|
|
289
|
+
version: number
|
|
290
|
+
): string | null {
|
|
291
|
+
const historyDir = join(DATA_DIR, "history", project, slug);
|
|
292
|
+
const fileName = `${String(version).padStart(3, "0")}.md`;
|
|
293
|
+
const filePath = join(historyDir, fileName);
|
|
294
|
+
return existsSync(filePath) ? filePath : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get the number of versions stored for a project/slug.
|
|
299
|
+
* Returns 0 if the directory doesn't exist.
|
|
300
|
+
*/
|
|
301
|
+
export function getVersionCount(project: string, slug: string): number {
|
|
302
|
+
const historyDir = join(DATA_DIR, "history", project, slug);
|
|
303
|
+
try {
|
|
304
|
+
const entries = readdirSync(historyDir);
|
|
305
|
+
return entries.filter((e) => /^\d+\.md$/.test(e)).length;
|
|
306
|
+
} catch {
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* List all versions for a project/slug with metadata.
|
|
313
|
+
* Returns versions sorted ascending by version number.
|
|
314
|
+
*/
|
|
315
|
+
export function listVersions(
|
|
316
|
+
project: string,
|
|
317
|
+
slug: string
|
|
318
|
+
): Array<{ version: number; timestamp: string }> {
|
|
319
|
+
const historyDir = join(DATA_DIR, "history", project, slug);
|
|
320
|
+
try {
|
|
321
|
+
const entries = readdirSync(historyDir);
|
|
322
|
+
const versions: Array<{ version: number; timestamp: string }> = [];
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
const match = entry.match(/^(\d+)\.md$/);
|
|
325
|
+
if (match) {
|
|
326
|
+
const version = parseInt(match[1], 10);
|
|
327
|
+
const filePath = join(historyDir, entry);
|
|
328
|
+
try {
|
|
329
|
+
const stat = statSync(filePath);
|
|
330
|
+
versions.push({ version, timestamp: stat.mtime.toISOString() });
|
|
331
|
+
} catch {
|
|
332
|
+
versions.push({ version, timestamp: "" });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return versions.sort((a, b) => a.version - b.version);
|
|
337
|
+
} catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* List all plan slugs stored for a project.
|
|
344
|
+
* Returns slugs sorted by most recently modified first.
|
|
345
|
+
*/
|
|
346
|
+
export function listProjectPlans(
|
|
347
|
+
project: string
|
|
348
|
+
): Array<{ slug: string; versions: number; lastModified: string }> {
|
|
349
|
+
const projectDir = join(DATA_DIR, "history", project);
|
|
350
|
+
try {
|
|
351
|
+
const entries = readdirSync(projectDir, { withFileTypes: true });
|
|
352
|
+
const plans: Array<{ slug: string; versions: number; lastModified: string }> = [];
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
if (!entry.isDirectory()) continue;
|
|
355
|
+
const slugDir = join(projectDir, entry.name);
|
|
356
|
+
const files = readdirSync(slugDir).filter((f) => /^\d+\.md$/.test(f));
|
|
357
|
+
if (files.length === 0) continue;
|
|
358
|
+
|
|
359
|
+
// Find most recent file modification time
|
|
360
|
+
let latest = 0;
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
try {
|
|
363
|
+
const mtime = statSync(join(slugDir, file)).mtime.getTime();
|
|
364
|
+
if (mtime > latest) latest = mtime;
|
|
365
|
+
} catch { /* skip */ }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
plans.push({
|
|
369
|
+
slug: entry.name,
|
|
370
|
+
versions: files.length,
|
|
371
|
+
lastModified: latest ? new Date(latest).toISOString() : "",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return plans.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
375
|
+
} catch {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface TourDiffAnchor {
|
|
2
|
+
/** Relative file path within the repo. */
|
|
3
|
+
file: string;
|
|
4
|
+
/** Start line in the new file (post-change). */
|
|
5
|
+
line: number;
|
|
6
|
+
/** End line in the new file. */
|
|
7
|
+
end_line: number;
|
|
8
|
+
/** Raw unified diff hunk for this anchor. */
|
|
9
|
+
hunk: string;
|
|
10
|
+
/** One-line chip label, e.g. "Add retry logic". */
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TourKeyTakeaway {
|
|
15
|
+
/** One sentence — the takeaway. */
|
|
16
|
+
text: string;
|
|
17
|
+
/** Severity for visual styling. */
|
|
18
|
+
severity: "info" | "important" | "warning";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TourStop {
|
|
22
|
+
/** Short chapter title, friendly tone. */
|
|
23
|
+
title: string;
|
|
24
|
+
/** ONE sentence — the headline for this stop. Scannable without expanding. */
|
|
25
|
+
gist: string;
|
|
26
|
+
/** 2-3 sentences of additional context. Only shown when expanded. */
|
|
27
|
+
detail: string;
|
|
28
|
+
/** Connective phrase to the next stop, e.g. "Building on that..." (empty for last stop). */
|
|
29
|
+
transition: string;
|
|
30
|
+
/** Diff anchors — the code locations this stop references. */
|
|
31
|
+
anchors: TourDiffAnchor[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TourQAItem {
|
|
35
|
+
/** Product-level verification question. */
|
|
36
|
+
question: string;
|
|
37
|
+
/** Indices into stops[] that this question relates to. */
|
|
38
|
+
stop_indices: number[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CodeTourOutput {
|
|
42
|
+
/** One-line title for the entire tour. */
|
|
43
|
+
title: string;
|
|
44
|
+
/** 1-2 sentence friendly greeting + summary. Conversational, not formal. */
|
|
45
|
+
greeting: string;
|
|
46
|
+
/** 1-3 sentences: why this changeset exists — the motivation/problem being solved. */
|
|
47
|
+
intent: string;
|
|
48
|
+
/** What things looked like before this changeset — one sentence. */
|
|
49
|
+
before: string;
|
|
50
|
+
/** What things look like after — one sentence. */
|
|
51
|
+
after: string;
|
|
52
|
+
/** 3-5 key takeaways — the most critical info, scannable at a glance. */
|
|
53
|
+
key_takeaways: TourKeyTakeaway[];
|
|
54
|
+
/** Ordered tour stops — the detailed walk-through. */
|
|
55
|
+
stops: TourStop[];
|
|
56
|
+
/** Product-level QA checklist. */
|
|
57
|
+
qa_checklist: TourQAItem[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** UI-side tour shape: server output extended with persisted checklist state. */
|
|
61
|
+
export type CodeTourData = CodeTourOutput & { checklist: boolean[] };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"types": ["node"]
|
|
13
|
+
},
|
|
14
|
+
"exclude": ["**/*.test.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Editor annotations from VS Code extension (ephemeral, in-memory only)
|
|
2
|
+
export interface EditorAnnotation {
|
|
3
|
+
id: string;
|
|
4
|
+
filePath: string; // workspace-relative (e.g., "src/auth.ts")
|
|
5
|
+
selectedText: string;
|
|
6
|
+
lineStart: number; // 1-based
|
|
7
|
+
lineEnd: number; // 1-based
|
|
8
|
+
comment?: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Git review types shared between server and client
|
|
13
|
+
export type {
|
|
14
|
+
DiffOption,
|
|
15
|
+
WorktreeInfo,
|
|
16
|
+
GitContext,
|
|
17
|
+
JjEvoLogEntry,
|
|
18
|
+
RecentCommit,
|
|
19
|
+
AvailableBranches,
|
|
20
|
+
CompareTargetConfig,
|
|
21
|
+
CompareTargetPickerCopy,
|
|
22
|
+
RepositoryContext,
|
|
23
|
+
} from "./review-core";
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
WorkspaceDiffType,
|
|
27
|
+
WorkspaceRepoState,
|
|
28
|
+
WorkspaceReviewState,
|
|
29
|
+
} from "./review-workspace";
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { test, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { urlToMarkdown } from "./url-to-markdown";
|
|
3
|
+
|
|
4
|
+
// Track fetch calls to verify headers and URL selection
|
|
5
|
+
let fetchCalls: { url: string; headers: Record<string, string> }[] = [];
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
fetchCalls = [];
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
globalThis.fetch = originalFetch;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a mock fetch that responds based on the Accept header.
|
|
18
|
+
* When Accept includes text/markdown, returns markdown with the right content-type.
|
|
19
|
+
* Otherwise returns HTML.
|
|
20
|
+
*/
|
|
21
|
+
function mockFetchWithMarkdownSupport(markdown: string) {
|
|
22
|
+
return mock((url: string | URL | Request, init?: RequestInit) => {
|
|
23
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
24
|
+
fetchCalls.push({ url: String(url), headers: headers ?? {} });
|
|
25
|
+
|
|
26
|
+
const accept = headers?.Accept || headers?.accept || "";
|
|
27
|
+
if (accept.includes("text/markdown")) {
|
|
28
|
+
return Promise.resolve(
|
|
29
|
+
new Response(markdown, {
|
|
30
|
+
status: 200,
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
33
|
+
"x-markdown-tokens": "42",
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return Promise.resolve(
|
|
39
|
+
new Response("<html><body><p>Hello</p></body></html>", {
|
|
40
|
+
status: 200,
|
|
41
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Mock fetch that only returns HTML (no markdown support). */
|
|
48
|
+
function mockFetchHtmlOnly(html = "<html><body><p>Fallback</p></body></html>") {
|
|
49
|
+
return mock((url: string | URL | Request, init?: RequestInit) => {
|
|
50
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
51
|
+
fetchCalls.push({ url: String(url), headers: headers ?? {} });
|
|
52
|
+
return Promise.resolve(
|
|
53
|
+
new Response(html, {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("content negotiation: uses markdown when server supports it", async () => {
|
|
62
|
+
const md = "# Hello\n\nThis is markdown from the server.";
|
|
63
|
+
globalThis.fetch = mockFetchWithMarkdownSupport(md) as typeof fetch;
|
|
64
|
+
|
|
65
|
+
const result = await urlToMarkdown("https://example.com/page", { useJina: true });
|
|
66
|
+
|
|
67
|
+
expect(result.source).toBe("content-negotiation");
|
|
68
|
+
expect(result.markdown).toBe(md);
|
|
69
|
+
// Should only make one fetch (the content negotiation request)
|
|
70
|
+
expect(fetchCalls).toHaveLength(1);
|
|
71
|
+
expect(fetchCalls[0].headers.Accept).toContain("text/markdown");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("content negotiation: falls through to Jina when server returns HTML", async () => {
|
|
75
|
+
// First call (content negotiation) returns HTML, second (Jina) returns markdown
|
|
76
|
+
let callCount = 0;
|
|
77
|
+
globalThis.fetch = mock((url: string | URL | Request, init?: RequestInit) => {
|
|
78
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
79
|
+
fetchCalls.push({ url: String(url), headers: headers ?? {} });
|
|
80
|
+
callCount++;
|
|
81
|
+
|
|
82
|
+
if (callCount === 1) {
|
|
83
|
+
// Content negotiation attempt — server doesn't support it
|
|
84
|
+
return Promise.resolve(
|
|
85
|
+
new Response("<html><body>Hi</body></html>", {
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// Jina Reader call
|
|
92
|
+
return Promise.resolve(
|
|
93
|
+
new Response("# From Jina", {
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: { "content-type": "text/plain" },
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}) as typeof fetch;
|
|
99
|
+
|
|
100
|
+
const result = await urlToMarkdown("https://example.com/page", { useJina: true });
|
|
101
|
+
|
|
102
|
+
expect(result.source).toBe("jina");
|
|
103
|
+
expect(result.markdown).toBe("# From Jina");
|
|
104
|
+
// Content negotiation fetch + Jina fetch
|
|
105
|
+
expect(fetchCalls.length).toBeGreaterThanOrEqual(2);
|
|
106
|
+
expect(fetchCalls[1].url).toContain("r.jina.ai");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("content negotiation: skipped for local URLs", async () => {
|
|
110
|
+
let callCount = 0;
|
|
111
|
+
globalThis.fetch = mock((_url: string | URL | Request, init?: RequestInit) => {
|
|
112
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
113
|
+
fetchCalls.push({ url: String(_url), headers: headers ?? {} });
|
|
114
|
+
callCount++;
|
|
115
|
+
return Promise.resolve(
|
|
116
|
+
new Response("<html><body>Local</body></html>", {
|
|
117
|
+
status: 200,
|
|
118
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}) as typeof fetch;
|
|
122
|
+
|
|
123
|
+
const result = await urlToMarkdown("http://localhost:3000/readme", { useJina: false });
|
|
124
|
+
|
|
125
|
+
expect(result.source).toBe("fetch+turndown");
|
|
126
|
+
// No content negotiation request should have been made
|
|
127
|
+
// (first call should be the Turndown fetch, not a markdown request)
|
|
128
|
+
for (const call of fetchCalls) {
|
|
129
|
+
if (call.headers.Accept) {
|
|
130
|
+
expect(call.headers.Accept).not.toContain("text/markdown");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("content negotiation: handles server error gracefully", async () => {
|
|
136
|
+
let callCount = 0;
|
|
137
|
+
globalThis.fetch = mock((_url: string | URL | Request, init?: RequestInit) => {
|
|
138
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
139
|
+
fetchCalls.push({ url: String(_url), headers: headers ?? {} });
|
|
140
|
+
callCount++;
|
|
141
|
+
|
|
142
|
+
if (callCount === 1) {
|
|
143
|
+
// Content negotiation — server error
|
|
144
|
+
return Promise.resolve(new Response(null, { status: 500 }));
|
|
145
|
+
}
|
|
146
|
+
// Jina fallback
|
|
147
|
+
return Promise.resolve(
|
|
148
|
+
new Response("# Jina fallback", {
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: { "content-type": "text/plain" },
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
}) as typeof fetch;
|
|
154
|
+
|
|
155
|
+
const result = await urlToMarkdown("https://example.com/page", { useJina: true });
|
|
156
|
+
|
|
157
|
+
// Should fall through to Jina
|
|
158
|
+
expect(result.source).toBe("jina");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("raw .md URL: still takes priority over content negotiation", async () => {
|
|
162
|
+
globalThis.fetch = mock((_url: string | URL | Request, init?: RequestInit) => {
|
|
163
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
164
|
+
fetchCalls.push({ url: String(_url), headers: headers ?? {} });
|
|
165
|
+
return Promise.resolve(
|
|
166
|
+
new Response("# Raw markdown file", {
|
|
167
|
+
status: 200,
|
|
168
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
}) as typeof fetch;
|
|
172
|
+
|
|
173
|
+
const result = await urlToMarkdown("https://example.com/README.md", { useJina: true });
|
|
174
|
+
|
|
175
|
+
expect(result.source).toBe("fetch-raw");
|
|
176
|
+
expect(result.markdown).toBe("# Raw markdown file");
|
|
177
|
+
});
|