Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@
"@hypr/api-client": "workspace:*",
"@hypr/codemirror": "workspace:^",
"@hypr/plugin-analytics": "workspace:*",
"@hypr/plugin-audio-priority": "workspace:*",
"@hypr/plugin-apple-calendar": "workspace:*",
"@hypr/plugin-audio-priority": "workspace:*",
"@hypr/plugin-auth": "workspace:*",
"@hypr/plugin-cli2": "workspace:*",
"@hypr/plugin-db2": "workspace:*",
"@hypr/plugin-deeplink2": "workspace:*",
"@hypr/plugin-detect": "workspace:*",
"@hypr/plugin-export": "workspace:*",
"@hypr/plugin-folder": "workspace:*",
"@hypr/plugin-extensions": "workspace:*",
"@hypr/plugin-folder": "workspace:*",
"@hypr/plugin-hooks": "workspace:*",
"@hypr/plugin-icon": "workspace:*",
"@hypr/plugin-importer": "workspace:*",
"@hypr/plugin-js": "workspace:*",
"@hypr/plugin-listener": "workspace:*",
"@hypr/plugin-listener2": "workspace:*",
"@hypr/plugin-local-stt": "workspace:*",
Expand All @@ -51,15 +52,14 @@
"@hypr/plugin-notify": "workspace:*",
"@hypr/plugin-overlay": "workspace:*",
"@hypr/plugin-path2": "workspace:*",
"@hypr/plugin-tantivy": "workspace:*",
"@hypr/plugin-pdf": "workspace:*",
"@hypr/plugin-permissions": "workspace:*",
"@hypr/plugin-settings": "workspace:*",
"@hypr/plugin-sfx": "workspace:*",
"@hypr/plugin-tantivy": "workspace:*",
"@hypr/plugin-template": "workspace:*",
"@hypr/plugin-updater2": "workspace:*",
"@hypr/plugin-windows": "workspace:*",
"@hypr/plugin-js": "workspace:*",
"@hypr/store": "workspace:*",
"@hypr/tiptap": "workspace:^",
"@hypr/ui": "workspace:^",
Expand Down
60 changes: 60 additions & 0 deletions apps/desktop/src/store/tinybase/persister/human/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";

import type { FrontmatterInput, JsonValue } from "@hypr/plugin-export";
import type { HumanStorage } from "@hypr/store";

import type { CollectorResult, TablesContent } from "../utils";
import { getHumanDir, getHumanFilePath } from "./utils";

export interface HumanCollectorResult extends CollectorResult {
validHumanIds: Set<string>;
}

type HumansTable = Record<string, HumanStorage>;

export function collectHumanWriteOps<Schemas extends OptionalSchemas>(
_store: MergeableStore<Schemas>,
tables: TablesContent,
dataDir: string,
): HumanCollectorResult {
const dirs = new Set<string>();
const operations: CollectorResult["operations"] = [];
const validHumanIds = new Set<string>();

const humansDir = getHumanDir(dataDir);
dirs.add(humansDir);

const humans = (tables as { humans?: HumansTable }).humans ?? {};

const frontmatterItems: [FrontmatterInput, string][] = [];

for (const [humanId, human] of Object.entries(humans)) {
validHumanIds.add(humanId);

const { memo, ...frontmatterFields } = human;

const frontmatter: Record<string, JsonValue> = {
user_id: frontmatterFields.user_id ?? "",
created_at: frontmatterFields.created_at ?? "",
name: frontmatterFields.name ?? "",
email: frontmatterFields.email ?? "",
org_id: frontmatterFields.org_id ?? "",
job_title: frontmatterFields.job_title ?? "",
linkedin_username: frontmatterFields.linkedin_username ?? "",
};

const body = memo ?? "";
const filePath = getHumanFilePath(dataDir, humanId);

frontmatterItems.push([{ frontmatter, content: body }, filePath]);
}

if (frontmatterItems.length > 0) {
operations.push({
type: "frontmatter-batch",
items: frontmatterItems,
});
}

return { dirs, operations, validHumanIds };
}
99 changes: 99 additions & 0 deletions apps/desktop/src/store/tinybase/persister/human/load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { readDir, readTextFile, remove } from "@tauri-apps/plugin-fs";

import type { HumanStorage } from "@hypr/store";

import { isFileNotFoundError, isUUID } from "../utils";
import {
getHumanDir,
getHumanFilePath,
parseMarkdownWithFrontmatter,
} from "./utils";

export async function loadAllHumans(
dataDir: string,
): Promise<Record<string, HumanStorage>> {
const result: Record<string, HumanStorage> = {};
const humansDir = getHumanDir(dataDir);

let entries: { name: string; isDirectory: boolean }[];
try {
entries = await readDir(humansDir);
} catch (error) {
if (isFileNotFoundError(error)) {
return result;
}
throw error;
}

for (const entry of entries) {
if (entry.isDirectory) continue;
if (!entry.name.endsWith(".md")) continue;

const humanId = entry.name.replace(/\.md$/, "");
if (!isUUID(humanId)) {
console.warn(`[HumanPersister] Skipping non-UUID file: ${entry.name}`);
continue;
}

try {
const filePath = getHumanFilePath(dataDir, humanId);
const content = await readTextFile(filePath);
const { frontmatter, body } = await parseMarkdownWithFrontmatter(content);

result[humanId] = {
user_id: String(frontmatter.user_id ?? ""),
created_at: String(frontmatter.created_at ?? ""),
name: String(frontmatter.name ?? ""),
email: String(frontmatter.email ?? ""),
org_id: String(frontmatter.org_id ?? ""),
job_title: String(frontmatter.job_title ?? ""),
linkedin_username: String(frontmatter.linkedin_username ?? ""),
memo: body,
};
} catch (error) {
console.error(`[HumanPersister] Failed to load human ${humanId}:`, error);
continue;
}
}

return result;
}

export async function cleanupOrphanHumanFiles(
dataDir: string,
validHumanIds: Set<string>,
): Promise<void> {
const humansDir = getHumanDir(dataDir);

let entries: { name: string; isDirectory: boolean }[];
try {
entries = await readDir(humansDir);
} catch (error) {
if (isFileNotFoundError(error)) {
return;
}
throw error;
}

for (const entry of entries) {
if (entry.isDirectory) continue;
if (!entry.name.endsWith(".md")) continue;

const humanId = entry.name.replace(/\.md$/, "");
if (!isUUID(humanId)) continue;

if (!validHumanIds.has(humanId)) {
try {
const filePath = getHumanFilePath(dataDir, humanId);
await remove(filePath);
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error(
`[HumanPersister] Failed to remove orphan file ${entry.name}:`,
error,
);
}
}
}
}
}
76 changes: 76 additions & 0 deletions apps/desktop/src/store/tinybase/persister/human/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { sep } from "@tauri-apps/api/path";
import { exists, mkdir, readTextFile, remove } from "@tauri-apps/plugin-fs";

import {
commands as exportCommands,
type FrontmatterInput,
type JsonValue,
} from "@hypr/plugin-export";
import type { HumanStorage } from "@hypr/store";

import { isFileNotFoundError } from "../utils";
import { getHumanDir, getHumanFilePath } from "./utils";

export async function migrateHumansJsonIfNeeded(
dataDir: string,
): Promise<void> {
const humansJsonPath = [dataDir, "humans.json"].join(sep());
const humansDir = getHumanDir(dataDir);

const jsonExists = await exists(humansJsonPath);
if (!jsonExists) {
return;
}

const dirExists = await exists(humansDir);
if (dirExists) {
return;
}
Comment on lines +25 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration incompleteness bug: If both humans.json and the humans/ directory exist, the migration exits early without deleting the old JSON file. This can occur if a previous migration was interrupted after creating the directory but before deleting the JSON file. On subsequent runs, humans.json will persist indefinitely, potentially causing data inconsistency.

Fix: Change the logic to attempt cleanup of humans.json whenever it exists and the directory exists:

const dirExists = await exists(humansDir);
if (dirExists) {
  // Directory exists - cleanup old JSON if present
  try {
    await remove(humansJsonPath);
    console.log("[HumanPersister] Cleaned up old humans.json file");
  } catch (error) {
    if (!isFileNotFoundError(error)) {
      console.error("[HumanPersister] Failed to cleanup humans.json:", error);
    }
  }
  return;
}
Suggested change
const dirExists = await exists(humansDir);
if (dirExists) {
return;
}
const dirExists = await exists(humansDir);
if (dirExists) {
// Directory exists - cleanup old JSON if present
try {
await remove(humansJsonPath);
console.log("[HumanPersister] Cleaned up old humans.json file");
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error("[HumanPersister] Failed to cleanup humans.json:", error);
}
}
return;
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


console.log("[HumanPersister] Migrating from humans.json to humans/*.md");

try {
const content = await readTextFile(humansJsonPath);
const humans = JSON.parse(content) as Record<string, HumanStorage>;

await mkdir(humansDir, { recursive: true });

const batchItems: [FrontmatterInput, string][] = [];

for (const [humanId, human] of Object.entries(humans)) {
const { memo, ...frontmatterFields } = human;

const frontmatter: Record<string, JsonValue> = {
user_id: frontmatterFields.user_id ?? "",
created_at: frontmatterFields.created_at ?? "",
name: frontmatterFields.name ?? "",
email: frontmatterFields.email ?? "",
org_id: frontmatterFields.org_id ?? "",
job_title: frontmatterFields.job_title ?? "",
linkedin_username: frontmatterFields.linkedin_username ?? "",
};

const body = memo ?? "";
const filePath = getHumanFilePath(dataDir, humanId);

batchItems.push([{ frontmatter, content: body }, filePath]);
}

if (batchItems.length > 0) {
const result = await exportCommands.exportFrontmatterBatch(batchItems);
if (result.status === "error") {
throw new Error(`Failed to export migrated humans: ${result.error}`);
}
}

await remove(humansJsonPath);

console.log(
`[HumanPersister] Migration complete: ${Object.keys(humans).length} humans migrated`,
);
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error("[HumanPersister] Migration failed:", error);
}
}
}
Loading
Loading