Files
velum-raycast/src/velum.ts
muena 1843479884 Initial commit: Velum Raycast extension
Pseudonymize-and-AI workflow for handling PII-sensitive text via the
Velum API and Raycast AI. Commands cover end-to-end email summary and
reply, briefing/action-items/structured-data extraction, manual
pseudonymize/depseudonymize on selection or clipboard, and session
management. Includes Raycast 2.0 Beta workarounds for selection capture
and rich-text clipboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:46:42 +02:00

145 lines
4.0 KiB
TypeScript

import { fetchWithAuth } from "./auth";
import { getPreferences } from "./preferences";
import type {
DepseudonymizeResponse,
EntityType,
PlaceholderMapping,
PseudonymizeResponse,
} from "./types";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function normalizePseudonymizeResponse(
response: PseudonymizeResponse,
): PseudonymizeResponse {
const normalizedMapping: PlaceholderMapping = {};
const strippedKeys = new Set<string>();
for (const [key, entry] of Object.entries(response.mapping)) {
const stripped = key.replace(/[<>]/g, "");
normalizedMapping[`<${stripped}>`] = entry;
strippedKeys.add(stripped);
}
let text = response.pseudonymized_text;
const ordered = Array.from(strippedKeys).sort((a, b) => b.length - a.length);
for (const stripped of ordered) {
const bracketed = `<${stripped}>`;
const bareRegex = new RegExp(
`(?<![<\\w])${escapeRegExp(stripped)}(?![\\w>])`,
"g",
);
text = text.replace(bareRegex, bracketed);
}
return {
...response,
pseudonymized_text: text,
mapping: normalizedMapping,
};
}
export function localDepseudonymize(
text: string,
mapping: PlaceholderMapping,
): DepseudonymizeResponse {
let result = text;
let replacementsMade = 0;
const entries = Object.entries(mapping)
.map(([key, entry]) => ({
stripped: key.replace(/[<>]/g, ""),
entry,
}))
.sort((a, b) => b.stripped.length - a.stripped.length);
for (const { stripped, entry } of entries) {
const bracketed = `<${stripped}>`;
const bracketedRegex = new RegExp(escapeRegExp(bracketed), "g");
const bracketedMatches = result.match(bracketedRegex);
if (bracketedMatches) {
replacementsMade += bracketedMatches.length;
result = result.replace(bracketedRegex, entry.original);
}
const bareRegex = new RegExp(`\\b${escapeRegExp(stripped)}\\b`, "g");
const bareMatches = result.match(bareRegex);
if (bareMatches) {
replacementsMade += bareMatches.length;
result = result.replace(bareRegex, entry.original);
}
}
return { original_text: result, replacements_made: replacementsMade };
}
function apiUrl(path: string): string {
return `${getPreferences().velumBaseUrl}${path}`;
}
async function parseJsonResponse<T>(response: Response): Promise<T> {
const contentType = response.headers.get("content-type") ?? "";
if (!response.ok) {
const details = await response.text().catch(() => response.statusText);
throw new Error(
`Velum API request failed: ${response.status} ${response.statusText}: ${details}`,
);
}
if (!contentType.includes("application/json")) {
const details = await response.text().catch(() => "");
throw new Error(
`Velum API returned non-JSON response: ${details.slice(0, 200)}`,
);
}
return (await response.json()) as T;
}
export async function getEntityTypes(): Promise<EntityType[]> {
const response = await fetchWithAuth(apiUrl("/api/entity-types"));
const payload = await parseJsonResponse<{ entity_types: EntityType[] }>(
response,
);
return payload.entity_types;
}
export async function pseudonymize(
text: string,
mapping: PlaceholderMapping,
entityTypes?: EntityType[],
): Promise<PseudonymizeResponse> {
const response = await fetchWithAuth(apiUrl("/api/pseudonymize"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
mapping,
entity_types:
entityTypes && entityTypes.length > 0 ? entityTypes : undefined,
}),
});
return parseJsonResponse<PseudonymizeResponse>(response);
}
export async function depseudonymize(
text: string,
mapping: PlaceholderMapping,
): Promise<DepseudonymizeResponse> {
const response = await fetchWithAuth(apiUrl("/api/depseudonymize"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
mapping,
}),
});
return parseJsonResponse<DepseudonymizeResponse>(response);
}