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>
This commit is contained in:
144
src/velum.ts
Normal file
144
src/velum.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user