|
| 1 | +/** |
| 2 | + * AntigravityUsage - Antigravity's usage history as a usage source. |
| 3 | + * |
| 4 | + * Antigravity keeps conversations in SQLite databases under its data |
| 5 | + * directories and each instance's profile. Parsed databases stay in memory |
| 6 | + * while a database and its WAL keep the same `(size, mtime, ctime)`, so a |
| 7 | + * warm scan decodes only what changed. |
| 8 | + * |
| 9 | + * @module provider/Drivers/AntigravityUsage |
| 10 | + */ |
| 11 | +import * as NodeOS from "node:os"; |
| 12 | + |
| 13 | +import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts"; |
| 14 | +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; |
| 15 | +import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 16 | +import type { |
| 17 | + ProviderUsageInstance, |
| 18 | + ProviderUsageReader, |
| 19 | + ProviderUsageScan, |
| 20 | +} from "@t3tools/provider-core/server/usage"; |
| 21 | +import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; |
| 22 | +import * as Context from "effect/Context"; |
| 23 | +import * as Crypto from "effect/Crypto"; |
| 24 | +import * as Effect from "effect/Effect"; |
| 25 | +import * as FileSystem from "effect/FileSystem"; |
| 26 | +import * as Layer from "effect/Layer"; |
| 27 | +import * as Path from "effect/Path"; |
| 28 | + |
| 29 | +import { resolveAntigravityInstanceDirectories } from "../antigravityAuthSupport.ts"; |
| 30 | +import { makeAntigravityUsageCache, readAntigravityUsage } from "./antigravityUsageReader.ts"; |
| 31 | + |
| 32 | +export class AntigravityUsage extends Context.Service< |
| 33 | + AntigravityUsage, |
| 34 | + { |
| 35 | + /** Every Antigravity store: the data directories and each configured instance's profile. */ |
| 36 | + readonly scan: (input: { |
| 37 | + readonly instances: ReadonlyArray<ProviderUsageInstance<AntigravitySettings>>; |
| 38 | + readonly windowStartMs: number; |
| 39 | + }) => Effect.Effect<ReadonlyArray<ProviderUsageScan>, UsageReadError>; |
| 40 | + } |
| 41 | +>()("t3/provider/Drivers/AntigravityUsage") {} |
| 42 | + |
| 43 | +const make = Effect.gen(function* () { |
| 44 | + const crypto = yield* Crypto.Crypto; |
| 45 | + const fileSystem = yield* FileSystem.FileSystem; |
| 46 | + const path = yield* Path.Path; |
| 47 | + const host = yield* ProviderHost.ProviderHost; |
| 48 | + const hostEnvironment = yield* HostProcessEnvironment; |
| 49 | + const cache = makeAntigravityUsageCache(); |
| 50 | + |
| 51 | + /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */ |
| 52 | + const dataRoots = Effect.gen(function* () { |
| 53 | + const home = NodeOS.homedir(); |
| 54 | + const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"] |
| 55 | + ?.split(",") |
| 56 | + .map((value) => value.trim()) |
| 57 | + .filter(Boolean); |
| 58 | + const defaults = [ |
| 59 | + ...["antigravity", "antigravity-cli", "antigravity-ide", "antigravity-backup"].map((name) => |
| 60 | + path.join(home, ".gemini", name), |
| 61 | + ), |
| 62 | + path.join(home, ".config", "antigravity"), |
| 63 | + ]; |
| 64 | + const canonical = new Set<string>(); |
| 65 | + for (const root of configured?.length ? configured : defaults) { |
| 66 | + const resolved = path.resolve(expandHomePath(root)); |
| 67 | + canonical.add( |
| 68 | + yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)), |
| 69 | + ); |
| 70 | + } |
| 71 | + return [...canonical]; |
| 72 | + }); |
| 73 | + |
| 74 | + const scan: AntigravityUsage["Service"]["scan"] = Effect.fn("AntigravityUsage.scan")(function* ({ |
| 75 | + instances, |
| 76 | + windowStartMs, |
| 77 | + }) { |
| 78 | + const roots = yield* dataRoots; |
| 79 | + // Only configured instances have a profile; the implicit default never ran. |
| 80 | + for (const { instanceId } of instances.filter((instance) => instance.configured)) { |
| 81 | + const directories = yield* resolveAntigravityInstanceDirectories( |
| 82 | + host.paths.stateDir, |
| 83 | + instanceId, |
| 84 | + ).pipe( |
| 85 | + Effect.provideService(Crypto.Crypto, crypto), |
| 86 | + Effect.provideService(Path.Path, path), |
| 87 | + Effect.mapError( |
| 88 | + (cause) => |
| 89 | + new UsageReadError({ |
| 90 | + reason: "scanFailed", |
| 91 | + detail: "Antigravity profile directory could not be resolved.", |
| 92 | + cause, |
| 93 | + }), |
| 94 | + ), |
| 95 | + ); |
| 96 | + roots.push(path.join(directories.profile, "antigravity-acp")); |
| 97 | + } |
| 98 | + const conversationDirs = new Set<string>(); |
| 99 | + for (const root of roots) { |
| 100 | + const resolvedRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root)); |
| 101 | + const nested = path.join(resolvedRoot, "conversations"); |
| 102 | + const dir = (yield* fileSystem |
| 103 | + .exists(nested) |
| 104 | + .pipe(Effect.catchCause(() => Effect.succeed(false)))) |
| 105 | + ? nested |
| 106 | + : resolvedRoot; |
| 107 | + conversationDirs.add(yield* fileSystem.realPath(dir).pipe(Effect.orElseSucceed(() => dir))); |
| 108 | + } |
| 109 | + const result = yield* Effect.promise(() => |
| 110 | + readAntigravityUsage([...conversationDirs], windowStartMs, cache), |
| 111 | + ); |
| 112 | + const scanned: ProviderUsageScan[] = []; |
| 113 | + for (const dir of conversationDirs) { |
| 114 | + const exists = yield* fileSystem |
| 115 | + .exists(dir) |
| 116 | + .pipe(Effect.catchCause(() => Effect.succeed(false))); |
| 117 | + const failed = result.errors.some( |
| 118 | + (error) => error === dir || error.startsWith(`${dir}${path.sep}`), |
| 119 | + ); |
| 120 | + scanned.push({ |
| 121 | + dir, |
| 122 | + files: !exists && !failed ? null : result.files.filter((file) => file.root === dir), |
| 123 | + status: failed ? "partial" : "ok", |
| 124 | + ...(failed ? { message: "Some Antigravity history could not be read." } : {}), |
| 125 | + }); |
| 126 | + } |
| 127 | + return scanned; |
| 128 | + }); |
| 129 | + |
| 130 | + return AntigravityUsage.of({ scan }); |
| 131 | +}); |
| 132 | + |
| 133 | +export const layer = Layer.effect(AntigravityUsage, make); |
| 134 | + |
| 135 | +export const antigravityUsageReader: ProviderUsageReader<AntigravitySettings, AntigravityUsage> = { |
| 136 | + kind: "scan", |
| 137 | + provider: "antigravity", |
| 138 | + scan: ({ instances, windowStartMs }) => |
| 139 | + AntigravityUsage.pipe(Effect.flatMap((usage) => usage.scan({ instances, windowStartMs }))), |
| 140 | +}; |
0 commit comments