Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ plannotator/
│ ├── shared/ # Shared types, utilities, and cross-runtime logic
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
│ │ ├── paths.ts # XDG base directory helpers (getDataBase, getStateBase, getConfigBase)
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ ├── editor/ # Plan review App.tsx
│ └── review-editor/ # Code review UI
Expand Down Expand Up @@ -104,9 +105,12 @@ claude --plugin-dir ./apps/hook
| `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing entirely. Default: enabled. |
| `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. |
| `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. |
| `PLANNOTATOR_JINA` | Set to `0` / `false` to disable Jina Reader for URL annotation, or `1` / `true` to enable. Default: enabled. Can also be set via `~/.plannotator/config.json` (`{ "jina": false }`) or per-invocation via `--no-jina`. |
| `PLANNOTATOR_JINA` | Set to `0` / `false` to disable Jina Reader for URL annotation, or `1` / `true` to enable. Default: enabled. Can also be set via the config file (`{ "jina": false }`) or per-invocation via `--no-jina`. |
| `JINA_API_KEY` | Optional Jina Reader API key for higher rate limits (500 RPM vs 20 RPM unauthenticated). Free keys include 10M tokens. |
| `PLANNOTATOR_VERIFY_ATTESTATION` | **Read by the install scripts only**, not by the runtime binary. Set to `1` / `true` to have `scripts/install.sh` / `install.ps1` / `install.cmd` run `gh attestation verify` on every install. Off by default. Can also be set persistently via `~/.plannotator/config.json` (`{ "verifyAttestation": true }`) or per-invocation via `--verify-attestation`. Requires `gh` installed and authenticated. |
| `PLANNOTATOR_VERIFY_ATTESTATION` | **Read by the install scripts only**, not by the runtime binary. Set to `1` / `true` to have `scripts/install.sh` / `install.ps1` / `install.cmd` run `gh attestation verify` on every install. Off by default. Can also be set persistently via the config file (`{ "verifyAttestation": true }`) or per-invocation via `--verify-attestation`. Requires `gh` installed and authenticated. |
| `XDG_DATA_HOME` | Base for persistent data (plans, version history). Default: `~/.local/share`. Plannotator stores data in `$XDG_DATA_HOME/plannotator/`. Ignored when `~/.plannotator` already exists (legacy compat). |
| `XDG_STATE_HOME` | Base for ephemeral state (drafts, sessions, debug logs). Default: `~/.local/state`. Plannotator stores state in `$XDG_STATE_HOME/plannotator/`. Ignored when `~/.plannotator` already exists (legacy compat). |
| `XDG_CONFIG_HOME` | Base for configuration (config.json, improvement hooks). Default: `~/.config`. Plannotator stores config in `$XDG_CONFIG_HOME/plannotator/`. Ignored when `~/.plannotator` already exists (legacy compat). |

**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected when `PLANNOTATOR_REMOTE` is unset. Set `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `0` / `false` to force local mode.

Expand Down
24 changes: 14 additions & 10 deletions packages/server/codex-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*/

import { join } from "node:path";
import { homedir, tmpdir } from "node:os";
import { tmpdir } from "node:os";
import { getStateBase } from "@plannotator/shared/paths";
import { appendFile, mkdir, unlink, writeFile, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import type { DiffType } from "./vcs";
Expand All @@ -18,17 +19,18 @@ import { toRelativePath } from "./path-utils";
// ---------------------------------------------------------------------------

const DEBUG_ENABLED = !!process.env.PLANNOTATOR_DEBUG;
const DEBUG_LOG_PATH = join(homedir(), ".plannotator", "codex-review-debug.log");

async function debugLog(label: string, data?: unknown): Promise<void> {
if (!DEBUG_ENABLED) return;
try {
await mkdir(join(homedir(), ".plannotator"), { recursive: true });
const stateBase = getStateBase();
const debugLogPath = join(stateBase, "codex-review-debug.log");
await mkdir(stateBase, { recursive: true });
const timestamp = new Date().toISOString();
const line = data !== undefined
? `[${timestamp}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}\n`
: `[${timestamp}] ${label}\n`;
await appendFile(DEBUG_LOG_PATH, line);
await appendFile(debugLogPath, line);
} catch { /* never fail the main flow */ }
}

Expand Down Expand Up @@ -80,21 +82,23 @@ const CODEX_REVIEW_SCHEMA = JSON.stringify({
additionalProperties: false,
});

const SCHEMA_DIR = join(homedir(), ".plannotator");
const SCHEMA_FILE = join(SCHEMA_DIR, "codex-review-schema.json");
let schemaMaterialized = false;
let schemaFilePath: string | undefined;

/** Ensure the schema file exists on disk and return its path. */
async function ensureSchemaFile(): Promise<string> {
if (!schemaMaterialized) {
await mkdir(SCHEMA_DIR, { recursive: true });
await writeFile(SCHEMA_FILE, CODEX_REVIEW_SCHEMA);
const schemaDir = getStateBase();
const filePath = join(schemaDir, "codex-review-schema.json");
await mkdir(schemaDir, { recursive: true });
await writeFile(filePath, CODEX_REVIEW_SCHEMA);
schemaMaterialized = true;
schemaFilePath = filePath;
}
return SCHEMA_FILE;
return schemaFilePath!;
}

export { SCHEMA_FILE as CODEX_REVIEW_SCHEMA_PATH };
export { schemaFilePath as CODEX_REVIEW_SCHEMA_PATH };

// ---------------------------------------------------------------------------
// System prompt — copied verbatim from codex-rs/core/review_prompt.md
Expand Down
4 changes: 2 additions & 2 deletions packages/server/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* so users can discover and reopen closed browser tabs.
*/

import { homedir } from "os";
import { join } from "path";
import { getStateBase } from "@plannotator/shared/paths";
import {
mkdirSync,
writeFileSync,
Expand All @@ -27,7 +27,7 @@ export interface SessionInfo {
}

function getSessionsDir(): string {
const dir = join(homedir(), ".plannotator", "sessions");
const dir = join(getStateBase(), "sessions");
mkdirSync(dir, { recursive: true });
return dir;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/shared/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* Plannotator Config
*
* Reads/writes ~/.plannotator/config.json for persistent user settings.
* Reads/writes config.json from the plannotator config directory.
* Runtime-agnostic: uses only node:fs, node:os, node:child_process.
*/

import { homedir } from "os";
import { join } from "path";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
import { execSync } from "child_process";
import { getConfigBase } from "./paths";

export type DefaultDiffType = 'uncommitted' | 'unstaged' | 'staged';

Expand Down Expand Up @@ -54,7 +54,7 @@ export interface PlannotatorConfig {
jina?: boolean;
}

const CONFIG_DIR = join(homedir(), ".plannotator");
const CONFIG_DIR = getConfigBase();
const CONFIG_PATH = join(CONFIG_DIR, "config.json");

/**
Expand Down
10 changes: 5 additions & 5 deletions packages/shared/draft.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
/**
* Draft Storage
*
* Persists annotation drafts to ~/.plannotator/drafts/ so they survive
* server crashes. Each draft is keyed by a content hash of the plan/diff
* it was created against.
* Persists annotation drafts to the plannotator state directory so they
* survive server crashes. Each draft is keyed by a content hash of the
* plan/diff it was created against.
*
* Runtime-agnostic: uses only node:fs, node:path, node:os, node:crypto.
*/

import { homedir } from "os";
import { join } from "path";
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
import { createHash } from "crypto";
import { getStateBase } from "./paths";

/**
* Get the drafts directory, creating it if needed.
*/
export function getDraftDir(): string {
const dir = join(homedir(), ".plannotator", "drafts");
const dir = join(getStateBase(), "drafts");
mkdirSync(dir, { recursive: true });
return dir;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/shared/improvement-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
import { homedir } from "os";
import { join } from "path";
import { readFileSync, statSync } from "fs";
import { getConfigBase } from "./paths";

/** Base directory for hook-injectable files (new path) */
const HOOKS_BASE_DIR = join(homedir(), ".plannotator", "hooks");
/** Base directory for hook-injectable files. */
const HOOKS_BASE_DIR = join(getConfigBase(), "hooks");

/** Legacy base directory (pre-migration path) */
/** Legacy base directory (pre-hooks-subdir path, always ~/.plannotator). */
const LEGACY_BASE_DIR = join(homedir(), ".plannotator");

/** Maximum file size to read (50 KB) */
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"./agent-jobs": "./agent-jobs.ts",
"./config": "./config.ts",
"./improvement-hooks": "./improvement-hooks.ts",
"./paths": "./paths.ts",
"./worktree": "./worktree.ts",
"./html-to-markdown": "./html-to-markdown.ts",
"./url-to-markdown": "./url-to-markdown.ts"
Expand Down
40 changes: 40 additions & 0 deletions packages/shared/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* XDG Base Directory helpers for plannotator.
*
* Each helper falls back to ~/.plannotator when that directory already exists,
* preserving backwards-compatibility for existing installations.
*
* New installations use the XDG Base Directory Specification:
* data → $XDG_DATA_HOME/plannotator (default: ~/.local/share/plannotator)
* state → $XDG_STATE_HOME/plannotator (default: ~/.local/state/plannotator)
* config → $XDG_CONFIG_HOME/plannotator (default: ~/.config/plannotator)
*
* Runtime-agnostic: uses only node:os, node:path, node:fs.
*/

import { homedir } from "os";
import { join } from "path";
import { existsSync } from "fs";

const LEGACY_DIR = join(homedir(), ".plannotator");

/** Persistent user data: plans, version history. */
export function getDataBase(): string {
if (existsSync(LEGACY_DIR)) return LEGACY_DIR;
const xdg = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
return join(xdg, "plannotator");
}

/** Ephemeral state: drafts, sessions, debug logs. */
export function getStateBase(): string {
if (existsSync(LEGACY_DIR)) return LEGACY_DIR;
const xdg = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
return join(xdg, "plannotator");
}

/** User configuration: config.json, improvement hooks. */
export function getConfigBase(): string {
if (existsSync(LEGACY_DIR)) return LEGACY_DIR;
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
return join(xdg, "plannotator");
}
17 changes: 9 additions & 8 deletions packages/shared/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Plan Storage Utility
*
* Saves plans and annotations to ~/.plannotator/plans/
* Saves plans and annotations to the plannotator data directory.
* Cross-platform: works on Windows, macOS, and Linux.
*
* Runtime-agnostic: uses only node:fs, node:path, node:os.
Expand All @@ -11,6 +11,7 @@ import { homedir } from "os";
import { join, resolve, sep } from "path";
import { mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, existsSync } from "fs";
import { sanitizeTag } from "./project";
import { getDataBase } from "./paths";

/**
* Get the plan storage directory, creating it if needed.
Expand All @@ -26,7 +27,7 @@ export function getPlanDir(customPath?: string | null): string {
? join(homedir(), customPath.slice(1))
: customPath;
} else {
planDir = join(homedir(), ".plannotator", "plans");
planDir = join(getDataBase(), "plans");
}

planDir = resolve(planDir);
Expand Down Expand Up @@ -202,7 +203,7 @@ export function readArchivedPlan(filename: string, customPath?: string | null):
* Not affected by the customPath setting (that only affects decision saves).
*/
export function getHistoryDir(project: string, slug: string): string {
const historyDir = join(homedir(), ".plannotator", "history", project, slug);
const historyDir = join(getDataBase(), "history", project, slug);
mkdirSync(historyDir, { recursive: true });
return historyDir;
}
Expand Down Expand Up @@ -269,7 +270,7 @@ export function getPlanVersion(
slug: string,
version: number
): string | null {
const historyDir = join(homedir(), ".plannotator", "history", project, slug);
const historyDir = join(getDataBase(), "history", project, slug);
const fileName = `${String(version).padStart(3, "0")}.md`;
const filePath = join(historyDir, fileName);

Expand All @@ -289,7 +290,7 @@ export function getPlanVersionPath(
slug: string,
version: number
): string | null {
const historyDir = join(homedir(), ".plannotator", "history", project, slug);
const historyDir = join(getDataBase(), "history", project, slug);
const fileName = `${String(version).padStart(3, "0")}.md`;
const filePath = join(historyDir, fileName);
return existsSync(filePath) ? filePath : null;
Expand All @@ -300,7 +301,7 @@ export function getPlanVersionPath(
* Returns 0 if the directory doesn't exist.
*/
export function getVersionCount(project: string, slug: string): number {
const historyDir = join(homedir(), ".plannotator", "history", project, slug);
const historyDir = join(getDataBase(), "history", project, slug);
try {
const entries = readdirSync(historyDir);
return entries.filter((e) => /^\d+\.md$/.test(e)).length;
Expand All @@ -317,7 +318,7 @@ export function listVersions(
project: string,
slug: string
): Array<{ version: number; timestamp: string }> {
const historyDir = join(homedir(), ".plannotator", "history", project, slug);
const historyDir = join(getDataBase(), "history", project, slug);
try {
const entries = readdirSync(historyDir);
const versions: Array<{ version: number; timestamp: string }> = [];
Expand Down Expand Up @@ -347,7 +348,7 @@ export function listVersions(
export function listProjectPlans(
project: string
): Array<{ slug: string; versions: number; lastModified: string }> {
const projectDir = join(homedir(), ".plannotator", "history", project);
const projectDir = join(getDataBase(), "history", project);
try {
const entries = readdirSync(projectDir, { withFileTypes: true });
const plans: Array<{ slug: string; versions: number; lastModified: string }> = [];
Expand Down