This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: vault sync via ob + obsidian-headless package fix

Container now syncs both vault (ob sync) and carry (git pull) on startup:
- Fixed obsidian-cli → obsidian-headless (correct npm package name)
- Fixed ob sync path syntax (--path flag, not positional arg)
- Container writes auth token + vault config to ~/.config/obsidian-headless/
- Secrets: OBSIDIAN_AUTH_TOKEN, OBSIDIAN_ENCRYPTION_KEY, OBSIDIAN_ENCRYPTION_SALT
- Container health endpoint shows vaultFiles, carryFiles, token status
- Sync endpoint returns vault/carry log output for debugging

Result: 654 vault files + 42 carry files synced in container.
Analysis now finds 279 touchpoints (vault + carry) against island vertices.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

author
nandi
co-author
Letta Code
date (May 14, 2026, 7:44 AM UTC) commit 5d86c1fb parent 45f0ad3a
+116 -14
+2 -2
container/Dockerfile
··· 9 9 10 10 WORKDIR /app 11 11 12 - # Install ob (Obsidian CLI) globally 13 - RUN npm install -g obsidian-cli 12 + # Install ob (Obsidian headless CLI) globally 13 + RUN npm install -g obsidian-headless 14 14 15 15 # Install deps 16 16 COPY package.json ./
+32 -4
container/server.ts
··· 22 22 // ── Health ── 23 23 if (url.pathname === "/health" && req.method === "GET") { 24 24 // Include data status in health check 25 - const { readdirSync, statSync } = await import("fs"); 25 + const { readdirSync, statSync, existsSync } = await import("fs"); 26 26 const { join } = await import("path"); 27 27 let vaultFiles = 0, carryFiles = 0, carryDomains: string[] = []; 28 28 try { ··· 49 49 } catch {} 50 50 } catch {} 51 51 res.writeHead(200, { "Content-Type": "application/json" }); 52 - res.end(JSON.stringify({ status: "ok", vaultFiles, carryFiles, carryDomains, carryTokenSet: !!process.env.CARRY_GITHUB_TOKEN })); 52 + res.end(JSON.stringify({ 53 + status: "ok", 54 + vaultFiles, 55 + carryFiles, 56 + carryDomains, 57 + carryTokenSet: !!process.env.CARRY_GITHUB_TOKEN, 58 + obsidianTokenSet: !!process.env.OBSIDIAN_AUTH_TOKEN, 59 + obsidianConfigDir: existsSync("/root/.config/obsidian-headless/auth_token"), 60 + })); 53 61 return; 54 62 } 55 63 56 64 // ── Sync ── 57 65 if (url.pathname === "/sync" && req.method === "POST") { 58 66 try { 59 - await syncAll(); 67 + const { execSync } = await import("child_process"); 68 + const { existsSync, readFileSync } = await import("fs"); 69 + 70 + // Sync vault 71 + let vaultLog = ""; 72 + try { 73 + const out = execSync("ob sync --path /data/vault 2>&1", { timeout: 120000, encoding: "utf-8" }); 74 + vaultLog = out.slice(-500); 75 + } catch (e: any) { 76 + vaultLog = e.stdout?.slice(-500) || "" + e.stderr?.slice(-500) || "" || e.message?.slice(-500) || ""; 77 + } 78 + 79 + // Sync carry 80 + let carryLog = "already synced"; 81 + try { 82 + const out = execSync("git -C /data/carry pull origin main 2>&1", { timeout: 30000, encoding: "utf-8" }); 83 + carryLog = out.slice(-200); 84 + } catch (e: any) { 85 + carryLog = e.message?.slice(-200) || "pull failed"; 86 + } 87 + 60 88 res.writeHead(200, { "Content-Type": "application/json" }); 61 - res.end(JSON.stringify({ ok: true })); 89 + res.end(JSON.stringify({ ok: true, vaultLog, carryLog })); 62 90 } catch (e: any) { 63 91 res.writeHead(500, { "Content-Type": "application/json" }); 64 92 res.end(JSON.stringify({ error: e.message }));
+67 -6
container/sync.ts
··· 1 1 // ─── Sync: pull vault + carry into container ────────────────────── 2 2 // 3 - // vault: ob sync (Obsidian CLI) 3 + // vault: ob sync (Obsidian CLI) — needs auth token + vault config 4 4 // carry: git pull from GitHub private repo (auth via CARRY_GITHUB_TOKEN) 5 5 6 6 import { execSync } from "child_process"; 7 + import { existsSync, writeFileSync, mkdirSync } from "fs"; 8 + import { join } from "path"; 7 9 8 10 const VAULT_PATH = process.env.VAULT_PATH || "/data/vault"; 9 11 const CARRY_PATH = process.env.CARRY_PATH || "/data/carry"; 10 12 const CARRY_GITHUB_TOKEN = process.env.CARRY_GITHUB_TOKEN || ""; 11 13 const CARRY_BRANCH = process.env.CARRY_BRANCH || "main"; 12 - const CARRY_REPO = "codegod100/carry-vault"; 14 + const CARRY_REPO = "codegod100/carry-vault.git"; 15 + const OBSIDIAN_AUTH_TOKEN = process.env.OBSIDIAN_AUTH_TOKEN || ""; 16 + const OBSIDIAN_VAULT_ID = process.env.OBSIDIAN_VAULT_ID || "6472c50492efb918c2878e61048218f2"; 17 + const OBSIDIAN_VAULT_NAME = process.env.OBSIDIAN_VAULT_NAME || "notes"; 18 + const OBSIDIAN_SYNC_HOST = process.env.OBSIDIAN_SYNC_HOST || "sync-53.obsidian.md"; 19 + const OBSIDIAN_ENCRYPTION_KEY = process.env.OBSIDIAN_ENCRYPTION_KEY || ""; 20 + const OBSIDIAN_ENCRYPTION_SALT = process.env.OBSIDIAN_ENCRYPTION_SALT || ""; 13 21 14 22 function getCarryRemote(): string { 15 23 if (CARRY_GITHUB_TOKEN) { 16 - return `https://${CARRY_GITHUB_TOKEN}@github.com/${CARRY_REPO}.git`; 24 + return `https://${CARRY_GITHUB_TOKEN}@github.com/${CARRY_REPO}`; 17 25 } 18 26 return ""; 19 27 } 20 28 29 + function setupObsidianAuth(): boolean { 30 + if (!OBSIDIAN_AUTH_TOKEN) { 31 + console.error("[sync] OBSIDIAN_AUTH_TOKEN not set, skipping vault sync"); 32 + return false; 33 + } 34 + 35 + try { 36 + // Write auth token 37 + const configDir = "/root/.config/obsidian-headless"; 38 + mkdirSync(configDir, { recursive: true }); 39 + writeFileSync(join(configDir, "auth_token"), OBSIDIAN_AUTH_TOKEN, { mode: 0o600 }); 40 + 41 + // Write vault sync config 42 + if (OBSIDIAN_ENCRYPTION_KEY && OBSIDIAN_ENCRYPTION_SALT) { 43 + const syncDir = join(configDir, "sync", OBSIDIAN_VAULT_ID); 44 + mkdirSync(syncDir, { recursive: true }); 45 + const vaultConfig = { 46 + vaultId: OBSIDIAN_VAULT_ID, 47 + vaultName: OBSIDIAN_VAULT_NAME, 48 + vaultPath: VAULT_PATH, 49 + host: OBSIDIAN_SYNC_HOST, 50 + encryptionVersion: 3, 51 + encryptionKey: OBSIDIAN_ENCRYPTION_KEY, 52 + encryptionSalt: OBSIDIAN_ENCRYPTION_SALT, 53 + conflictStrategy: "merge", 54 + deviceName: "strata-container", 55 + }; 56 + writeFileSync(join(syncDir, "config.json"), JSON.stringify(vaultConfig, null, 2), { mode: 0o600 }); 57 + } 58 + 59 + console.log("[sync] obsidian auth configured"); 60 + return true; 61 + } catch (e: any) { 62 + console.error("[sync] obsidian auth setup failed:", e.message); 63 + return false; 64 + } 65 + } 66 + 21 67 export async function syncVault(): Promise<void> { 68 + if (!setupObsidianAuth()) return; 69 + 22 70 try { 23 - execSync(`ob sync ${VAULT_PATH}`, { stdio: "pipe", timeout: 60000 }); 71 + // First time: setup sync link 72 + if (!existsSync(join(VAULT_PATH, ".obsidian"))) { 73 + try { 74 + execSync(`ob sync-setup --vault ${OBSIDIAN_VAULT_ID} --path ${VAULT_PATH}`, { 75 + stdio: "pipe", 76 + timeout: 30000, 77 + }); 78 + } catch (e: any) { 79 + // sync-setup may fail if already linked; try sync directly 80 + console.error("[sync] sync-setup failed (may already be linked):", e.message?.slice(0, 100)); 81 + } 82 + } 83 + 84 + execSync(`ob sync --path ${VAULT_PATH}`, { stdio: "pipe", timeout: 120000 }); 24 85 console.log("[sync] vault synced via ob"); 25 86 } catch (e: any) { 26 - console.error("[sync] vault sync failed:", e.message); 87 + console.error("[sync] vault sync failed:", e.message?.slice(0, 200)); 27 88 } 28 89 } 29 90 ··· 54 115 console.log("[sync] carry cloned via git"); 55 116 } 56 117 } catch (e: any) { 57 - console.error("[sync] carry sync failed:", e.message); 118 + console.error("[sync] carry sync failed:", e.message?.slice(0, 200)); 58 119 } 59 120 } 60 121
+12 -2
src/container.ts
··· 3 3 4 4 interface ContainerEnv { 5 5 CARRY_GITHUB_TOKEN: string; 6 + OBSIDIAN_AUTH_TOKEN: string; 7 + OBSIDIAN_ENCRYPTION_KEY: string; 8 + OBSIDIAN_ENCRYPTION_SALT: string; 6 9 } 7 10 8 11 export class StrataContainer extends Container<ContainerEnv> { ··· 11 14 12 15 constructor(ctx: DurableObject["ctx"], env: ContainerEnv) { 13 16 super(ctx, env); 14 - // Set env vars before container starts — inject secret from worker binding 17 + // Set env vars before container starts — inject secrets from worker bindings 15 18 this.envVars = { 16 19 VAULT_PATH: "/data/vault", 17 20 CARRY_PATH: "/data/carry", 18 21 CARRY_BRANCH: "main", 19 22 ...(env.CARRY_GITHUB_TOKEN ? { CARRY_GITHUB_TOKEN: env.CARRY_GITHUB_TOKEN } : {}), 23 + ...(env.OBSIDIAN_AUTH_TOKEN ? { OBSIDIAN_AUTH_TOKEN: env.OBSIDIAN_AUTH_TOKEN } : {}), 24 + OBSIDIAN_VAULT_ID: "6472c50492efb918c2878e61048218f2", 25 + OBSIDIAN_VAULT_NAME: "notes", 26 + OBSIDIAN_SYNC_HOST: "sync-53.obsidian.md", 27 + ...(env.OBSIDIAN_ENCRYPTION_KEY ? { OBSIDIAN_ENCRYPTION_KEY: env.OBSIDIAN_ENCRYPTION_KEY } : {}), 28 + ...(env.OBSIDIAN_ENCRYPTION_SALT ? { OBSIDIAN_ENCRYPTION_SALT: env.OBSIDIAN_ENCRYPTION_SALT } : {}), 20 29 }; 21 30 } 22 31 23 32 async onStart() { 24 - console.log("[strata-container] started, carry token:", this.envVars?.CARRY_GITHUB_TOKEN ? "set" : "missing"); 33 + const has = (k: string) => !!this.envVars?.[k]; 34 + console.log("[strata-container] started, carry:", has("CARRY_GITHUB_TOKEN") ? "set" : "missing", "obsidian:", has("OBSIDIAN_AUTH_TOKEN") ? "set" : "missing"); 25 35 } 26 36 27 37 async onStop() {
+3
src/worker.ts
··· 16 16 STRATA_CONTAINER: DurableObjectNamespace; 17 17 PDS_APP_PASSWORD: string; 18 18 CARRY_GITHUB_TOKEN: string; 19 + OBSIDIAN_AUTH_TOKEN: string; 20 + OBSIDIAN_ENCRYPTION_KEY: string; 21 + OBSIDIAN_ENCRYPTION_SALT: string; 19 22 } 20 23 21 24 // ─── Contrail setup ─────────────────────────────────────────────────