repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | import { createWriteStream, promises as fs } from "node:fs"; |
| #2 | import path from "node:path"; |
| #3 | |
| #4 | export type Tier = "KNOWN" | "LEARNED" | "INFERRED"; |
| #5 | |
| #6 | export interface Entry { |
| #7 | tier: Tier; |
| #8 | key: string; |
| #9 | value: unknown; |
| #10 | ts: number; |
| #11 | provenance?: string; |
| #12 | } |
| #13 | |
| #14 | export interface ClawdVaultOpts { |
| #15 | workspace: string; |
| #16 | vaultDir?: string; |
| #17 | } |
| #18 | |
| #19 | export interface VaultSnapshot { |
| #20 | owner: string; |
| #21 | tiers: Record<Tier, Entry[]>; |
| #22 | workspace_manifest: string[]; |
| #23 | } |
| #24 | |
| #25 | const TIER_FILES: Record<Tier, string> = { |
| #26 | KNOWN: "known.jsonl", |
| #27 | LEARNED: "learned.jsonl", |
| #28 | INFERRED: "inferred.jsonl", |
| #29 | }; |
| #30 | |
| #31 | export class ClawdVault { |
| #32 | #opts: Required<ClawdVaultOpts>; |
| #33 | #buffer = new Map<string, Entry[]>(); |
| #34 | #flushTimer: NodeJS.Timeout; |
| #35 | |
| #36 | constructor(opts: ClawdVaultOpts) { |
| #37 | this.#opts = { |
| #38 | workspace: opts.workspace, |
| #39 | vaultDir: opts.vaultDir ?? process.env.CLAWD_VAULT_DIR ?? "/vault", |
| #40 | }; |
| #41 | |
| #42 | void fs.mkdir(this.#opts.vaultDir, { recursive: true }).catch(() => undefined); |
| #43 | this.#flushTimer = setInterval(() => { |
| #44 | void this.flushAll().catch(() => undefined); |
| #45 | }, 30_000); |
| #46 | this.#flushTimer.unref?.(); |
| #47 | } |
| #48 | |
| #49 | writeKnown(owner: string, entry: Omit<Entry, "tier" | "ts">) { |
| #50 | this.#push(owner, { ...entry, tier: "KNOWN", ts: Date.now() }); |
| #51 | } |
| #52 | |
| #53 | writeLearned(owner: string, entry: Omit<Entry, "tier" | "ts">) { |
| #54 | this.#push(owner, { ...entry, tier: "LEARNED", ts: Date.now() }); |
| #55 | } |
| #56 | |
| #57 | writeInferred(owner: string, data: object) { |
| #58 | this.#push(owner, { |
| #59 | tier: "INFERRED", |
| #60 | key: `ev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, |
| #61 | value: data, |
| #62 | ts: Date.now(), |
| #63 | }); |
| #64 | } |
| #65 | |
| #66 | async read(owner: string, tier: Tier): Promise<Entry[]> { |
| #67 | const local = this.#buffer.get(`${owner}:${tier}`) ?? []; |
| #68 | const onDisk = await this.#readFromDisk(owner, tier); |
| #69 | return [...onDisk, ...local]; |
| #70 | } |
| #71 | |
| #72 | async flushAll() { |
| #73 | for (const [key, entries] of this.#buffer.entries()) { |
| #74 | const [, tier] = key.split(":") as [string, Tier]; |
| #75 | if (tier === "INFERRED") { |
| #76 | const cutoff = Date.now() - 5 * 60_000; |
| #77 | this.#buffer.set(key, entries.filter((entry) => entry.ts > cutoff)); |
| #78 | continue; |
| #79 | } |
| #80 | this.#buffer.set(key, []); |
| #81 | } |
| #82 | } |
| #83 | |
| #84 | async snapshot(owner: string): Promise<VaultSnapshot> { |
| #85 | await this.flushAll(); |
| #86 | const [known, learned, inferred, workspace_manifest] = await Promise.all([ |
| #87 | this.read(owner, "KNOWN"), |
| #88 | this.read(owner, "LEARNED"), |
| #89 | this.read(owner, "INFERRED"), |
| #90 | this.#workspaceManifest(owner), |
| #91 | ]); |
| #92 | |
| #93 | return { |
| #94 | owner, |
| #95 | tiers: { KNOWN: known, LEARNED: learned, INFERRED: inferred }, |
| #96 | workspace_manifest, |
| #97 | }; |
| #98 | } |
| #99 | |
| #100 | async rehydrateFromSnapshot(snapshot: Omit<VaultSnapshot, "workspace_manifest">): Promise<void> { |
| #101 | for (const tier of ["KNOWN", "LEARNED", "INFERRED"] as Tier[]) { |
| #102 | const file = path.join(this.#opts.vaultDir, TIER_FILES[tier]); |
| #103 | const stream = createWriteStream(file, { flags: "w" }); |
| #104 | for (const entry of snapshot.tiers[tier] ?? []) { |
| #105 | stream.write(`${JSON.stringify({ owner: snapshot.owner, ...entry })}\n`); |
| #106 | } |
| #107 | await new Promise<void>((resolve, reject) => |
| #108 | stream.end((err?: Error | null) => (err ? reject(err) : resolve())), |
| #109 | ); |
| #110 | } |
| #111 | } |
| #112 | |
| #113 | async brainAsk(owner: string, query: string): Promise<string | null> { |
| #114 | const needle = query.trim().toLowerCase(); |
| #115 | if (!needle) return null; |
| #116 | const [known, learned] = await Promise.all([this.read(owner, "KNOWN"), this.read(owner, "LEARNED")]); |
| #117 | const haystack = [...learned, ...known]; |
| #118 | const matches = haystack.filter((entry) => |
| #119 | `${entry.key} ${renderValue(entry.value)} ${entry.provenance ?? ""}`.toLowerCase().includes(needle), |
| #120 | ); |
| #121 | if (matches.length === 0) return null; |
| #122 | return matches |
| #123 | .slice(-5) |
| #124 | .map((entry) => `[${entry.tier}] ${entry.key}: ${renderValue(entry.value)}`) |
| #125 | .join("\n"); |
| #126 | } |
| #127 | |
| #128 | #push(owner: string, entry: Entry) { |
| #129 | const key = `${owner}:${entry.tier}`; |
| #130 | const entries = this.#buffer.get(key) ?? []; |
| #131 | entries.push(entry); |
| #132 | this.#buffer.set(key, entries); |
| #133 | void this.#appendToDisk(owner, entry).catch(() => undefined); |
| #134 | } |
| #135 | |
| #136 | async #appendToDisk(owner: string, entry: Entry): Promise<void> { |
| #137 | const file = path.join(this.#opts.vaultDir, TIER_FILES[entry.tier]); |
| #138 | await fs.appendFile(file, `${JSON.stringify({ owner, ...entry })}\n`, "utf8"); |
| #139 | } |
| #140 | |
| #141 | async #readFromDisk(owner: string, tier: Tier): Promise<Entry[]> { |
| #142 | const file = path.join(this.#opts.vaultDir, TIER_FILES[tier]); |
| #143 | try { |
| #144 | const text = await fs.readFile(file, "utf8"); |
| #145 | return text |
| #146 | .split("\n") |
| #147 | .filter(Boolean) |
| #148 | .flatMap((line) => { |
| #149 | try { |
| #150 | const parsed = JSON.parse(line) as Entry & { owner?: string }; |
| #151 | if (parsed.owner && parsed.owner !== owner) return []; |
| #152 | return [ |
| #153 | { |
| #154 | tier, |
| #155 | key: parsed.key, |
| #156 | value: parsed.value, |
| #157 | ts: parsed.ts, |
| #158 | provenance: parsed.provenance, |
| #159 | }, |
| #160 | ]; |
| #161 | } catch { |
| #162 | return []; |
| #163 | } |
| #164 | }); |
| #165 | } catch { |
| #166 | return []; |
| #167 | } |
| #168 | } |
| #169 | |
| #170 | async #workspaceManifest(owner: string): Promise<string[]> { |
| #171 | const dir = path.join(this.#opts.workspace, owner); |
| #172 | const files: string[] = []; |
| #173 | |
| #174 | const walk = async (current: string) => { |
| #175 | const entries = await fs.readdir(current, { withFileTypes: true }); |
| #176 | for (const entry of entries) { |
| #177 | const fullPath = path.join(current, entry.name); |
| #178 | if (entry.isDirectory()) { |
| #179 | await walk(fullPath); |
| #180 | } else if (entry.isFile()) { |
| #181 | files.push(fullPath); |
| #182 | } |
| #183 | } |
| #184 | }; |
| #185 | |
| #186 | try { |
| #187 | await walk(dir); |
| #188 | return files.sort(); |
| #189 | } catch { |
| #190 | return []; |
| #191 | } |
| #192 | } |
| #193 | } |
| #194 | |
| #195 | function renderValue(value: unknown): string { |
| #196 | if (typeof value === "string") return value; |
| #197 | try { |
| #198 | return JSON.stringify(value); |
| #199 | } catch { |
| #200 | return String(value); |
| #201 | } |
| #202 | } |
| #203 |