@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,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plannotator Config
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes ~/.plannotator/config.json for persistent user settings.
|
|
5
|
+
* Runtime-agnostic: uses only node:fs, node:os, node:child_process.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { getPlannotatorDataDir } from "./data-dir";
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
|
|
13
|
+
export type DefaultDiffType = 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all';
|
|
14
|
+
export type DiffLineBgIntensity = 'subtle' | 'normal' | 'strong';
|
|
15
|
+
|
|
16
|
+
export interface DiffOptions {
|
|
17
|
+
diffStyle?: 'split' | 'unified';
|
|
18
|
+
overflow?: 'scroll' | 'wrap';
|
|
19
|
+
diffIndicators?: 'bars' | 'classic' | 'none';
|
|
20
|
+
lineDiffType?: 'word-alt' | 'word' | 'char' | 'none';
|
|
21
|
+
showLineNumbers?: boolean;
|
|
22
|
+
showDiffBackground?: boolean;
|
|
23
|
+
fontFamily?: string;
|
|
24
|
+
fontSize?: string;
|
|
25
|
+
tabSize?: number;
|
|
26
|
+
hideWhitespace?: boolean;
|
|
27
|
+
expandUnchanged?: boolean;
|
|
28
|
+
defaultDiffType?: DefaultDiffType;
|
|
29
|
+
lineBgIntensity?: DiffLineBgIntensity;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Single conventional comment label entry stored in config.json */
|
|
33
|
+
export interface CCLabelConfig {
|
|
34
|
+
label: string;
|
|
35
|
+
display: string;
|
|
36
|
+
blocking: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PromptSectionOverrides = Record<string, string | undefined>;
|
|
40
|
+
|
|
41
|
+
export type PromptRuntime =
|
|
42
|
+
| "claude-code"
|
|
43
|
+
| "amp"
|
|
44
|
+
| "droid"
|
|
45
|
+
| "kiro-cli"
|
|
46
|
+
| "opencode"
|
|
47
|
+
| "copilot-cli"
|
|
48
|
+
| "pi"
|
|
49
|
+
| "codex"
|
|
50
|
+
| "gemini-cli";
|
|
51
|
+
|
|
52
|
+
interface PromptSectionConfig {
|
|
53
|
+
[key: string]: string | Partial<Record<PromptRuntime, PromptSectionOverrides>> | undefined;
|
|
54
|
+
runtimes?: Partial<Record<PromptRuntime, PromptSectionOverrides>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PromptConfig {
|
|
58
|
+
review?: PromptSectionConfig & {
|
|
59
|
+
approved?: string;
|
|
60
|
+
denied?: string;
|
|
61
|
+
};
|
|
62
|
+
plan?: PromptSectionConfig & {
|
|
63
|
+
approved?: string;
|
|
64
|
+
approvedWithNotes?: string;
|
|
65
|
+
autoApproved?: string;
|
|
66
|
+
denied?: string;
|
|
67
|
+
};
|
|
68
|
+
annotate?: PromptSectionConfig & {
|
|
69
|
+
fileFeedback?: string;
|
|
70
|
+
messageFeedback?: string;
|
|
71
|
+
approved?: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const PROMPT_SECTIONS = ["review", "plan", "annotate"] as const;
|
|
76
|
+
|
|
77
|
+
export function mergePromptConfig(
|
|
78
|
+
current?: PromptConfig,
|
|
79
|
+
partial?: PromptConfig,
|
|
80
|
+
): PromptConfig | undefined {
|
|
81
|
+
if (!current && !partial) return undefined;
|
|
82
|
+
|
|
83
|
+
const result: Record<string, any> = { ...current, ...partial };
|
|
84
|
+
|
|
85
|
+
for (const section of PROMPT_SECTIONS) {
|
|
86
|
+
const cur = current?.[section];
|
|
87
|
+
const par = partial?.[section];
|
|
88
|
+
if (cur || par) {
|
|
89
|
+
result[section] = {
|
|
90
|
+
...cur,
|
|
91
|
+
...par,
|
|
92
|
+
runtimes: (cur?.runtimes || par?.runtimes)
|
|
93
|
+
? { ...cur?.runtimes, ...par?.runtimes }
|
|
94
|
+
: undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result as PromptConfig;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface PlannotatorConfig {
|
|
103
|
+
displayName?: string;
|
|
104
|
+
diffOptions?: DiffOptions;
|
|
105
|
+
prompts?: PromptConfig;
|
|
106
|
+
conventionalComments?: boolean;
|
|
107
|
+
/** null = explicitly cleared (use defaults), undefined = not set */
|
|
108
|
+
conventionalLabels?: CCLabelConfig[] | null;
|
|
109
|
+
/**
|
|
110
|
+
* Enable `gh attestation verify` during CLI installation/upgrade.
|
|
111
|
+
* Read by scripts/install.sh|ps1|cmd on every run (not by any runtime code).
|
|
112
|
+
* When true, the installer runs build-provenance verification after the
|
|
113
|
+
* SHA256 checksum check; requires `gh` CLI installed and authenticated
|
|
114
|
+
* (`gh auth login`). OS-level opt-in only — no UI surface. Default: false.
|
|
115
|
+
*/
|
|
116
|
+
verifyAttestation?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Enable Jina Reader for URL-to-markdown conversion during annotation.
|
|
119
|
+
* When true (default), `plannotator annotate <url>` routes through
|
|
120
|
+
* r.jina.ai for better JS-rendered page support and reader-mode extraction.
|
|
121
|
+
* Set to false to always use plain fetch + Turndown.
|
|
122
|
+
*/
|
|
123
|
+
jina?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Inject a Plannotator Flavored Markdown reminder into every EnterPlanMode
|
|
126
|
+
* call so the agent is aware it can enrich plans with code-file links,
|
|
127
|
+
* callouts, tables, diagrams, task lists, and the other PFM extensions.
|
|
128
|
+
* Read by the `improve-context` PreToolUse handler. Default: false.
|
|
129
|
+
*/
|
|
130
|
+
pfmReminder?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Open Plannotator in a Glimpse native window when available.
|
|
133
|
+
* When true (default), the server spawns `glimpseui` if it is on PATH,
|
|
134
|
+
* no explicit browser is configured, and the session is local.
|
|
135
|
+
* Set to false to always use the system browser even when Glimpse is installed.
|
|
136
|
+
*/
|
|
137
|
+
glimpse?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const CONFIG_DIR = getPlannotatorDataDir();
|
|
141
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Load config from ~/.plannotator/config.json.
|
|
145
|
+
* Returns {} on missing file or malformed JSON.
|
|
146
|
+
*/
|
|
147
|
+
export function loadConfig(): PlannotatorConfig {
|
|
148
|
+
try {
|
|
149
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
150
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
process.stderr.write(`[plannotator] Warning: failed to read config.json: ${e}\n`);
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Save config by merging partial values into the existing file.
|
|
161
|
+
* Creates ~/.plannotator/ directory if needed.
|
|
162
|
+
*/
|
|
163
|
+
export function saveConfig(partial: Partial<PlannotatorConfig>): void {
|
|
164
|
+
try {
|
|
165
|
+
const current = loadConfig();
|
|
166
|
+
const mergedDiffOptions = (current.diffOptions || partial.diffOptions)
|
|
167
|
+
? { ...current.diffOptions, ...partial.diffOptions }
|
|
168
|
+
: undefined;
|
|
169
|
+
const mergedPrompts = mergePromptConfig(current.prompts, partial.prompts);
|
|
170
|
+
const merged = {
|
|
171
|
+
...current,
|
|
172
|
+
...partial,
|
|
173
|
+
diffOptions: mergedDiffOptions,
|
|
174
|
+
prompts: mergedPrompts,
|
|
175
|
+
};
|
|
176
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
177
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
178
|
+
} catch (e) {
|
|
179
|
+
process.stderr.write(`[plannotator] Warning: failed to write config.json: ${e}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Detect the git user name from `git config user.name`.
|
|
185
|
+
* Returns null if git is unavailable, not in a repo, or user.name is not set.
|
|
186
|
+
*/
|
|
187
|
+
export function detectGitUser(): string | null {
|
|
188
|
+
try {
|
|
189
|
+
const name = execSync("git config user.name", { encoding: "utf-8", timeout: 3000 }).trim();
|
|
190
|
+
return name || null;
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the serverConfig payload for API responses.
|
|
198
|
+
* Reads config.json fresh each call so the response reflects the latest file on disk.
|
|
199
|
+
*/
|
|
200
|
+
export function getServerConfig(gitUser: string | null): {
|
|
201
|
+
displayName?: string;
|
|
202
|
+
diffOptions?: DiffOptions;
|
|
203
|
+
gitUser?: string;
|
|
204
|
+
conventionalComments?: boolean;
|
|
205
|
+
conventionalLabels?: CCLabelConfig[] | null;
|
|
206
|
+
} {
|
|
207
|
+
const cfg = loadConfig();
|
|
208
|
+
return {
|
|
209
|
+
displayName: cfg.displayName,
|
|
210
|
+
diffOptions: cfg.diffOptions,
|
|
211
|
+
gitUser: gitUser ?? undefined,
|
|
212
|
+
...(cfg.conventionalComments !== undefined && { conventionalComments: cfg.conventionalComments }),
|
|
213
|
+
...(cfg.conventionalLabels !== undefined && { conventionalLabels: cfg.conventionalLabels }),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Read the user's preferred default diff type from config, falling back to 'unstaged'.
|
|
219
|
+
*/
|
|
220
|
+
export function resolveDefaultDiffType(cfg?: PlannotatorConfig): DefaultDiffType {
|
|
221
|
+
const v = cfg?.diffOptions?.defaultDiffType as string | undefined;
|
|
222
|
+
if (v === 'branch') return 'merge-base';
|
|
223
|
+
return v === 'uncommitted' || v === 'unstaged' || v === 'staged' || v === 'merge-base' || v === 'all' ? v : 'unstaged';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve whether to use Glimpse native window.
|
|
228
|
+
*
|
|
229
|
+
* Priority (highest wins):
|
|
230
|
+
* PLANNOTATOR_GLIMPSE env var → config.glimpse → default true
|
|
231
|
+
*/
|
|
232
|
+
export function resolveUseGlimpse(config: PlannotatorConfig): boolean {
|
|
233
|
+
const envVal = process.env.PLANNOTATOR_GLIMPSE;
|
|
234
|
+
if (envVal !== undefined) {
|
|
235
|
+
return envVal === "1" || envVal.toLowerCase() === "true";
|
|
236
|
+
}
|
|
237
|
+
if (config.glimpse !== undefined) return config.glimpse;
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolve whether to use Jina Reader for URL annotation.
|
|
243
|
+
*
|
|
244
|
+
* Priority (highest wins):
|
|
245
|
+
* --no-jina CLI flag → PLANNOTATOR_JINA env var → config.jina → default true
|
|
246
|
+
*/
|
|
247
|
+
export function resolveUseJina(cliNoJina: boolean, config: PlannotatorConfig): boolean {
|
|
248
|
+
// CLI flag has highest priority
|
|
249
|
+
if (cliNoJina) return false;
|
|
250
|
+
|
|
251
|
+
// Environment variable
|
|
252
|
+
const envVal = process.env.PLANNOTATOR_JINA;
|
|
253
|
+
if (envVal !== undefined) {
|
|
254
|
+
return envVal === "1" || envVal.toLowerCase() === "true";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Config file
|
|
258
|
+
if (config.jina !== undefined) return config.jina;
|
|
259
|
+
|
|
260
|
+
// Default: enabled
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end encryption tests for the paste service.
|
|
3
|
+
*
|
|
4
|
+
* Tests the full pipeline: compress → encrypt → store → retrieve → decrypt → decompress
|
|
5
|
+
* Run: bun test packages/shared/crypto.test.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { encrypt, decrypt } from "./crypto";
|
|
10
|
+
import { deflateSync, inflateSync } from "bun";
|
|
11
|
+
|
|
12
|
+
// Bun's test runner doesn't have CompressionStream (browser API).
|
|
13
|
+
// Use Bun's native zlib for the same deflate-raw + base64url pipeline.
|
|
14
|
+
function compress(data: unknown): string {
|
|
15
|
+
const json = JSON.stringify(data);
|
|
16
|
+
const compressed = deflateSync(new TextEncoder().encode(json));
|
|
17
|
+
let binary = "";
|
|
18
|
+
for (let i = 0; i < compressed.length; i++) {
|
|
19
|
+
binary += String.fromCharCode(compressed[i]);
|
|
20
|
+
}
|
|
21
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decompress(b64: string): unknown {
|
|
25
|
+
const base64 = b64.replace(/-/g, "+").replace(/_/g, "/");
|
|
26
|
+
const binary = atob(base64);
|
|
27
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
28
|
+
const decompressed = inflateSync(bytes);
|
|
29
|
+
return JSON.parse(new TextDecoder().decode(decompressed));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PASTE_API = "https://plannotator-paste.plannotator.workers.dev";
|
|
33
|
+
|
|
34
|
+
// Realistic plan payload matching SharePayload shape
|
|
35
|
+
const SAMPLE_PAYLOAD = {
|
|
36
|
+
p: "# Implementation Plan\n\n## Step 1: Add authentication\n\nWe'll use JWT tokens stored in httpOnly cookies.\n\n## Step 2: Create middleware\n\nAdd auth middleware to all protected routes.\n\n```typescript\nconst authMiddleware = async (req: Request) => {\n const token = getCookie(req, 'auth');\n if (!token) throw new UnauthorizedError();\n return verify(token, SECRET);\n};\n```\n\n## Step 3: Update database schema\n\nAdd users table with email, password_hash, created_at columns.",
|
|
37
|
+
a: [
|
|
38
|
+
["C", "Add auth middleware", "Consider rate limiting on login endpoint", "reviewer", []],
|
|
39
|
+
["R", "JWT tokens stored in httpOnly cookies", "Use refresh token rotation instead of single JWT", null, []],
|
|
40
|
+
["D", "Add users table with email, password_hash, created_at columns.", null, []],
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// --- Unit tests (no network) ---
|
|
45
|
+
|
|
46
|
+
describe("encrypt / decrypt round-trip", () => {
|
|
47
|
+
test("encrypts and decrypts a string", async () => {
|
|
48
|
+
const plaintext = "hello world";
|
|
49
|
+
const { ciphertext, key } = await encrypt(plaintext);
|
|
50
|
+
|
|
51
|
+
expect(ciphertext).not.toBe(plaintext);
|
|
52
|
+
expect(key.length).toBeGreaterThan(0);
|
|
53
|
+
|
|
54
|
+
const decrypted = await decrypt(ciphertext, key);
|
|
55
|
+
expect(decrypted).toBe(plaintext);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("each encryption produces a unique ciphertext (random IV)", async () => {
|
|
59
|
+
const plaintext = "same input";
|
|
60
|
+
const a = await encrypt(plaintext);
|
|
61
|
+
const b = await encrypt(plaintext);
|
|
62
|
+
|
|
63
|
+
expect(a.ciphertext).not.toBe(b.ciphertext);
|
|
64
|
+
expect(a.key).not.toBe(b.key);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("wrong key fails to decrypt", async () => {
|
|
68
|
+
const { ciphertext } = await encrypt("secret");
|
|
69
|
+
const { key: wrongKey } = await encrypt("other");
|
|
70
|
+
|
|
71
|
+
expect(decrypt(ciphertext, wrongKey)).rejects.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("tampered ciphertext fails to decrypt", async () => {
|
|
75
|
+
const { ciphertext, key } = await encrypt("secret");
|
|
76
|
+
|
|
77
|
+
// Flip a character in the middle of the ciphertext
|
|
78
|
+
const mid = Math.floor(ciphertext.length / 2);
|
|
79
|
+
const tampered = ciphertext.slice(0, mid) +
|
|
80
|
+
(ciphertext[mid] === 'A' ? 'B' : 'A') +
|
|
81
|
+
ciphertext.slice(mid + 1);
|
|
82
|
+
|
|
83
|
+
expect(decrypt(tampered, key)).rejects.toThrow();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("handles large payloads", async () => {
|
|
87
|
+
const large = "x".repeat(100_000);
|
|
88
|
+
const { ciphertext, key } = await encrypt(large);
|
|
89
|
+
const decrypted = await decrypt(ciphertext, key);
|
|
90
|
+
expect(decrypted).toBe(large);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("full pipeline: compress → encrypt → decrypt → decompress", () => {
|
|
95
|
+
test("round-trips a SharePayload", async () => {
|
|
96
|
+
const compressed = await compress(SAMPLE_PAYLOAD);
|
|
97
|
+
const { ciphertext, key } = await encrypt(compressed);
|
|
98
|
+
|
|
99
|
+
// Ciphertext should not contain the original plan text
|
|
100
|
+
expect(ciphertext).not.toContain("Implementation Plan");
|
|
101
|
+
|
|
102
|
+
const decrypted = await decrypt(ciphertext, key);
|
|
103
|
+
const decompressed = await decompress(decrypted);
|
|
104
|
+
|
|
105
|
+
expect(decompressed).toEqual(SAMPLE_PAYLOAD);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// --- Integration tests (hit live paste service) ---
|
|
110
|
+
|
|
111
|
+
describe("live paste service E2E", () => {
|
|
112
|
+
test("encrypt → POST → GET → decrypt → decompress", async () => {
|
|
113
|
+
// 1. Compress
|
|
114
|
+
const compressed = await compress(SAMPLE_PAYLOAD);
|
|
115
|
+
|
|
116
|
+
// 2. Encrypt
|
|
117
|
+
const { ciphertext, key } = await encrypt(compressed);
|
|
118
|
+
|
|
119
|
+
// 3. Store — server sees only ciphertext
|
|
120
|
+
const postRes = await fetch(`${PASTE_API}/api/paste`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify({ data: ciphertext }),
|
|
124
|
+
});
|
|
125
|
+
expect(postRes.status).toBe(201);
|
|
126
|
+
const { id } = (await postRes.json()) as { id: string };
|
|
127
|
+
expect(id).toMatch(/^[A-Za-z0-9]{8}$/);
|
|
128
|
+
|
|
129
|
+
// 4. Retrieve
|
|
130
|
+
const getRes = await fetch(`${PASTE_API}/api/paste/${id}`);
|
|
131
|
+
expect(getRes.status).toBe(200);
|
|
132
|
+
|
|
133
|
+
// Verify Cache-Control header
|
|
134
|
+
expect(getRes.headers.get("cache-control")).toBe("private, no-store");
|
|
135
|
+
|
|
136
|
+
const { data: storedData } = (await getRes.json()) as { data: string };
|
|
137
|
+
|
|
138
|
+
// 5. Verify server stores only ciphertext (not readable plan data)
|
|
139
|
+
expect(storedData).toBe(ciphertext);
|
|
140
|
+
expect(storedData).not.toContain("Implementation Plan");
|
|
141
|
+
|
|
142
|
+
// 6. Decrypt
|
|
143
|
+
const decrypted = await decrypt(storedData, key);
|
|
144
|
+
|
|
145
|
+
// 7. Decompress
|
|
146
|
+
const result = await decompress(decrypted);
|
|
147
|
+
expect(result).toEqual(SAMPLE_PAYLOAD);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("GET without key returns opaque ciphertext", async () => {
|
|
151
|
+
const compressed = await compress({ p: "secret plan", a: [] });
|
|
152
|
+
const { ciphertext } = await encrypt(compressed);
|
|
153
|
+
|
|
154
|
+
const postRes = await fetch(`${PASTE_API}/api/paste`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({ data: ciphertext }),
|
|
158
|
+
});
|
|
159
|
+
const { id } = (await postRes.json()) as { id: string };
|
|
160
|
+
|
|
161
|
+
const getRes = await fetch(`${PASTE_API}/api/paste/${id}`);
|
|
162
|
+
const { data } = (await getRes.json()) as { data: string };
|
|
163
|
+
|
|
164
|
+
// Data is opaque — cannot be decompressed without decryption
|
|
165
|
+
expect(() => decompress(data)).toThrow();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("expired/nonexistent paste returns 404", async () => {
|
|
169
|
+
const res = await fetch(`${PASTE_API}/api/paste/ZZZZZZZZ`);
|
|
170
|
+
expect(res.status).toBe(404);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption for zero-knowledge paste storage.
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Crypto API — works in browsers, Bun, and edge runtimes.
|
|
5
|
+
* The key never leaves the client; it lives in the URL fragment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Encrypt a compressed base64url string with a fresh AES-256-GCM key.
|
|
10
|
+
*
|
|
11
|
+
* Returns { ciphertext, key } where:
|
|
12
|
+
* - ciphertext: base64url-encoded (12-byte IV prepended to GCM output)
|
|
13
|
+
* - key: base64url-encoded 256-bit key for the URL fragment
|
|
14
|
+
*/
|
|
15
|
+
export async function encrypt(
|
|
16
|
+
compressedData: string
|
|
17
|
+
): Promise<{ ciphertext: string; key: string }> {
|
|
18
|
+
const cryptoKey = await crypto.subtle.generateKey(
|
|
19
|
+
{ name: 'AES-GCM', length: 256 },
|
|
20
|
+
true,
|
|
21
|
+
['encrypt']
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
25
|
+
const plaintext = new TextEncoder().encode(compressedData);
|
|
26
|
+
|
|
27
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
28
|
+
{ name: 'AES-GCM', iv },
|
|
29
|
+
cryptoKey,
|
|
30
|
+
plaintext
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Prepend IV to ciphertext (IV || ciphertext+tag)
|
|
34
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
35
|
+
combined.set(iv, 0);
|
|
36
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
37
|
+
|
|
38
|
+
const rawKey = await crypto.subtle.exportKey('raw', cryptoKey);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
ciphertext: bytesToBase64url(combined),
|
|
42
|
+
key: bytesToBase64url(new Uint8Array(rawKey)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decrypt a ciphertext string using a base64url-encoded AES-256-GCM key.
|
|
48
|
+
*
|
|
49
|
+
* Expects ciphertext format: base64url(IV || encrypted+tag)
|
|
50
|
+
* Returns the original compressed base64url string.
|
|
51
|
+
*/
|
|
52
|
+
export async function decrypt(
|
|
53
|
+
ciphertext: string,
|
|
54
|
+
key: string
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const combined = base64urlToBytes(ciphertext);
|
|
57
|
+
const rawKey = base64urlToBytes(key);
|
|
58
|
+
|
|
59
|
+
const iv = combined.slice(0, 12);
|
|
60
|
+
const encrypted = combined.slice(12);
|
|
61
|
+
|
|
62
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
63
|
+
'raw',
|
|
64
|
+
rawKey.buffer as ArrayBuffer,
|
|
65
|
+
{ name: 'AES-GCM', length: 256 },
|
|
66
|
+
false,
|
|
67
|
+
['decrypt']
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
71
|
+
{ name: 'AES-GCM', iv },
|
|
72
|
+
cryptoKey,
|
|
73
|
+
encrypted
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return new TextDecoder().decode(decrypted);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Helpers ---
|
|
80
|
+
|
|
81
|
+
function bytesToBase64url(bytes: Uint8Array): string {
|
|
82
|
+
// Loop to avoid RangeError on large payloads (same approach as compress.ts)
|
|
83
|
+
let binary = '';
|
|
84
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
85
|
+
binary += String.fromCharCode(bytes[i]);
|
|
86
|
+
}
|
|
87
|
+
return btoa(binary)
|
|
88
|
+
.replace(/\+/g, '-')
|
|
89
|
+
.replace(/\//g, '_')
|
|
90
|
+
.replace(/=/g, '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function base64urlToBytes(b64: string): Uint8Array {
|
|
94
|
+
const base64 = b64.replace(/-/g, '+').replace(/_/g, '/');
|
|
95
|
+
const binary = atob(base64);
|
|
96
|
+
return Uint8Array.from(binary, c => c.charCodeAt(0));
|
|
97
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plannotator Data Directory
|
|
3
|
+
*
|
|
4
|
+
* Returns the base directory for all Plannotator data files.
|
|
5
|
+
*
|
|
6
|
+
* Priority:
|
|
7
|
+
* 1. PLANNOTATOR_DATA_DIR environment variable (with ~ expansion)
|
|
8
|
+
* 2. Default: ~/.plannotator
|
|
9
|
+
*
|
|
10
|
+
* This mirrors PASTE_DATA_DIR for the paste service and allows users
|
|
11
|
+
* to relocate all data (plans, history, drafts, config, hooks, sessions,
|
|
12
|
+
* debug logs, IPC registry, etc.) via a single variable — useful for
|
|
13
|
+
* XDG-style home directory cleanliness on Unix systems.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { join, resolve } from "path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the Plannotator data directory.
|
|
21
|
+
*
|
|
22
|
+
* If PLANNOTATOR_DATA_DIR is set and non-empty, the value is used
|
|
23
|
+
* as the base directory. Leading ~ is expanded to the user's home
|
|
24
|
+
* directory.
|
|
25
|
+
*
|
|
26
|
+
* Falls back to ~/.plannotator when the env var is absent or empty.
|
|
27
|
+
*/
|
|
28
|
+
export function getPlannotatorDataDir(): string {
|
|
29
|
+
const envDir = process.env.PLANNOTATOR_DATA_DIR?.trim();
|
|
30
|
+
if (!envDir) {
|
|
31
|
+
return join(homedir(), ".plannotator");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Expand ~ to home directory
|
|
35
|
+
const home = homedir();
|
|
36
|
+
if (envDir === "~") return home;
|
|
37
|
+
if (envDir.startsWith("~/") || envDir.startsWith("~\\")) {
|
|
38
|
+
return join(home, envDir.slice(2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return resolve(envDir);
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parseDiffFilePathLines, parsePatchPathToken } from "./diff-paths";
|
|
3
|
+
|
|
4
|
+
describe("diff path parsing", () => {
|
|
5
|
+
test("strips tab metadata from unquoted file path lines", () => {
|
|
6
|
+
expect(parseDiffFilePathLines([
|
|
7
|
+
"--- a/my file\t",
|
|
8
|
+
"+++ b/my file\t",
|
|
9
|
+
"@@ -1 +1 @@",
|
|
10
|
+
])).toEqual({
|
|
11
|
+
oldPath: "my file",
|
|
12
|
+
newPath: "my file",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("preserves escaped tabs inside quoted file paths", () => {
|
|
17
|
+
expect(parseDiffFilePathLines([
|
|
18
|
+
'--- "a/my\\tfile"',
|
|
19
|
+
'+++ "b/my\\tfile"',
|
|
20
|
+
"@@ -1 +1 @@",
|
|
21
|
+
])).toEqual({
|
|
22
|
+
oldPath: "my\tfile",
|
|
23
|
+
newPath: "my\tfile",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("preserves dev null paths with tab metadata", () => {
|
|
28
|
+
expect(parsePatchPathToken("/dev/null\t", "a")).toBe("/dev/null");
|
|
29
|
+
});
|
|
30
|
+
});
|