···11+import type { ApiChatCompletionRequest } from "./types/api";
22+33+/** Base URL of the llama.cpp server (OpenAI-compatible API). */
44+export const API_BASE_URL = "http://localhost:8080";
55+66+/** Shared tone rules appended to every prompt. */
77+const TONE_RULES = `# Tone
88+When responding, you must follow these rules:
99+- follow the original tone and style
1010+- answer directly from your knowledge when you can
1111+- be concise, prioritize clarity, brevity and don't repeat yourself
1212+- admit when you're unsure rather than making things up`;
1313+1414+/**
1515+ * System prompt for grammar/spelling correction.
1616+ * Instructs the model to return only the corrected text.
1717+ */
1818+export const CORRECT_PROMPT = `# Agent Guidelines
1919+You are an agent specialized in english grammar correction.
2020+Correct the grammar, spelling, and punctuation of the submitted text.
2121+2222+# Output
2323+Return ONLY the corrected text. No headings, no explanations, no markdown formatting.
2424+2525+${TONE_RULES}`;
2626+2727+/**
2828+ * System prompt for wording improvement.
2929+ * Instructs the model to return a better-worded version of the text.
3030+ */
3131+export const SUGGEST_PROMPT = `# Agent Guidelines
3232+You are an agent specialized in english writing improvement.
3333+Rewrite the submitted text with better wording and phrasing while preserving the original meaning.
3434+3535+# Output
3636+Return ONLY the improved text, with the same format, return lines, spacing, and punctuation. No headings, no explanations, no markdown formatting.
3737+3838+${TONE_RULES}`;
3939+4040+/** Parameters sent to the chat completions endpoint (generation/sampling subset). */
4141+export const API_PARAMS: Pick<ApiChatCompletionRequest,
4242+ | "temperature"
4343+ | "max_tokens"
4444+ | "top_p"
4545+ | "top_k"
4646+ | "min_p"
4747+ | "repeat_penalty"
4848+ | "frequency_penalty"
4949+ | "presence_penalty"
5050+ | "stream"
5151+> = {
5252+ temperature: 1.0,
5353+ max_tokens: 2048,
5454+ top_p: 0.95,
5555+ top_k: 40,
5656+ min_p: 0.01,
5757+ repeat_penalty: 1.0,
5858+ frequency_penalty: 0.0,
5959+ presence_penalty: 0.0,
6060+ stream: true,
6161+};
6262+6363+/** HTTP request timeout in milliseconds. */
6464+export const API_TIMEOUT_MS = 30_000;
6565+6666+/** Maximum allowed input text length in characters. */
6767+export const MAX_INPUT_LENGTH = 10_000;
···11+import { MAX_INPUT_LENGTH } from "./config";
22+import type { ApiChatCompletionResponse } from "./types/api";
33+44+/** Error thrown when input or output validation fails. */
55+export class ValidationError extends Error {
66+ constructor(message: string) {
77+ super(message);
88+ this.name = "ValidationError";
99+ }
1010+}
1111+1212+/**
1313+ * Validates and sanitises the user-selected text before sending it to the API.
1414+ *
1515+ * @param text - Raw value from `info.selectionText` (may be `undefined` or any type)
1616+ * @returns Trimmed, validated string
1717+ * @throws {@link ValidationError} when the input is invalid
1818+ */
1919+export function validateInput(text: unknown): string {
2020+ if (typeof text !== "string") {
2121+ throw new ValidationError("Selected text is not a valid string.");
2222+ }
2323+2424+ if (text.includes("\0")) {
2525+ throw new ValidationError("Selected text contains invalid characters (null bytes).");
2626+ }
2727+2828+ const trimmed = text.trim();
2929+3030+ if (trimmed.length === 0) {
3131+ throw new ValidationError("Selected text is empty.");
3232+ }
3333+3434+ if (!/\S/.test(trimmed)) {
3535+ throw new ValidationError("Selected text contains no readable content.");
3636+ }
3737+3838+ if (trimmed.length > MAX_INPUT_LENGTH) {
3939+ throw new ValidationError(
4040+ `Selected text is too long (${trimmed.length.toLocaleString()} characters). Maximum is ${MAX_INPUT_LENGTH.toLocaleString()}.`,
4141+ );
4242+ }
4343+4444+ return trimmed;
4545+}
4646+4747+/**
4848+ * Validates the parsed JSON response from the OpenAI-compatible chat completions endpoint.
4949+ *
5050+ * Expected shape:
5151+ * ```json
5252+ * { "choices": [{ "message": { "content": "..." } }] }
5353+ * ```
5454+ *
5555+ * @param data - Parsed JSON from the API response
5656+ * @returns The validated content string
5757+ * @throws {@link ValidationError} when the response is malformed or empty
5858+ */
5959+export function validateOutput(data: unknown): string {
6060+ if (typeof data !== "object" || data === null) {
6161+ throw new ValidationError("Invalid API response: expected a JSON object.");
6262+ }
6363+6464+ const response = data as ApiChatCompletionResponse;
6565+6666+ if (!Array.isArray(response.choices) || response.choices.length === 0) {
6767+ throw new ValidationError("Empty response from API (no choices returned).");
6868+ }
6969+7070+ const firstChoice = response.choices[0];
7171+ if (!firstChoice?.message) {
7272+ throw new ValidationError("Malformed response: missing message object.");
7373+ }
7474+7575+ if (typeof firstChoice.message.content !== "string") {
7676+ throw new ValidationError("Malformed response: content is not a string.");
7777+ }
7878+7979+ const content = firstChoice.message.content.trim();
8080+ if (content.length === 0) {
8181+ throw new ValidationError("API returned an empty correction.");
8282+ }
8383+8484+ return content;
8585+}