Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0d14a5f
feat: add opencode plugin and xdg db defaults
oritwoen Mar 11, 2026
7bad87d
fix: guard tool hook against null output
oritwoen Mar 11, 2026
cd4bbe6
fix: guard event hook against missing payload
oritwoen Mar 11, 2026
34c9ab5
test: tighten plugin and db-path edge cases
oritwoen Mar 11, 2026
16b0b98
fix: handle session idle events explicitly
oritwoen Mar 11, 2026
4d7ca6d
test: track plugin hooks for teardown cleanup
oritwoen Mar 11, 2026
48f24d0
test: align observation cap check with FTS results
oritwoen Mar 11, 2026
4bcaa1b
Merge origin/main into opencode-plugin-xdg-defaults
oritwoen Mar 11, 2026
afe611f
fix: align opencode plugin with async obsxa API
oritwoen Mar 11, 2026
978c941
fix: harden plugin session handling and db path validation
oritwoen Mar 11, 2026
c497d95
fix: create local db directory before opening sqlite file
oritwoen Mar 11, 2026
6fd2812
chore: refresh pnpm lockfile
oritwoen Mar 11, 2026
a3e35cd
chore: apply automated updates
autofix-ci[bot] Mar 11, 2026
87c9629
fix: harden obsxa context injection and project-scoped recall
oritwoen Mar 11, 2026
19c5282
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Mar 11, 2026
c6be4f0
chore: apply automated updates
autofix-ci[bot] Mar 11, 2026
77ef324
Merge branch 'main' into opencode-plugin-xdg-defaults
oritwoen Mar 11, 2026
4771e88
fix: align opencode hook types and harden event/context handling
oritwoen Mar 12, 2026
2808140
Merge remote-tracking branch 'origin/main' into opencode-plugin-xdg-d…
oritwoen Mar 12, 2026
0b1e8dc
chore: apply automated updates
autofix-ci[bot] Mar 12, 2026
5fb1f29
fix: keep opencode plugin input backward compatible
oritwoen Mar 12, 2026
44d0d6e
chore: pin dev dependency versions
oritwoen Mar 12, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### ⚠️ Notes

- **db path:** `createObsxa()` now defaults to an XDG-compliant data path (`~/.local/share/obsxa/obsxa.db` on Linux) instead of `./obsxa.db`. Pass `db: "./obsxa.db"` explicitly to keep legacy location.

## v0.0.2

### 🏡 Chore
Expand Down
7 changes: 6 additions & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ export default defineBuildConfig({
rollup: {
emitCJS: false,
},
entries: [{ type: "bundle", input: ["./src/index.ts", "./src/cli.ts", "./src/ai.ts"] }],
entries: [
{
type: "bundle",
input: ["./src/index.ts", "./src/cli.ts", "./src/ai.ts", "./src/opencode.ts"],
},
],
});
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"./ai": {
"types": "./dist/ai.d.mts",
"import": "./dist/ai.mjs"
},
"./opencode": {
"types": "./dist/opencode.d.mts",
"import": "./dist/opencode.mjs"
}
},
"scripts": {
Expand All @@ -51,8 +55,9 @@
"drizzle-orm": "^0.44.0"
},
"devDependencies": {
"@opencode-ai/plugin": "1.2.24",
"@types/node": "^25.3.0",
"@typescript/native-preview": "latest",
"@typescript/native-preview": "7.0.0-dev.20260310.1",
"ai": "^6.0.116",
"changelogen": "^0.6.2",
"drizzle-kit": "^0.31.0",
Expand All @@ -64,10 +69,14 @@
"zod": "^4.3.6"
},
"peerDependencies": {
"@opencode-ai/plugin": "*",
"ai": ">=6.0.0",
"zod": ">=4.0.0"
},
"peerDependenciesMeta": {
"@opencode-ai/plugin": {
"optional": true
},
"ai": {
"optional": true
},
Expand Down
23 changes: 22 additions & 1 deletion pnpm-lock.yaml

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

10 changes: 8 additions & 2 deletions src/ai.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { isAbsolute } from "node:path";
import { tool } from "ai";
import { z } from "zod/v4";
import { getDefaultDbPath } from "./core/db-path.ts";
import { createObsxa } from "./index.ts";
import type { ObsxaInstance } from "./index.ts";

function sanitizeDbPath(path?: string): string {
const dbPath = path ?? "./obsxa.db";
if (isAbsolute(dbPath)) throw new Error("Absolute database paths are not allowed");
const dbPath = path ?? getDefaultDbPath();
if (path && /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(path)) {
throw new Error("Database path must be relative");
}
if (path && (isAbsolute(path) || /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\"))) {
throw new Error("Database path must be relative");
}
if (dbPath.includes("..")) throw new Error("Database path must not contain '..'");
if (!dbPath.endsWith(".db")) throw new Error("Database path must end with '.db'");
Comment thread
oritwoen marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return dbPath;
Expand Down
4 changes: 2 additions & 2 deletions src/commands/_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { consola } from "consola";
import { createObsxa } from "../index.ts";

export const dbArgs = {
db: { type: "string" as const, description: "Path to SQLite database", default: "./obsxa.db" },
db: { type: "string" as const, description: "Path to SQLite database" },
json: { type: "boolean" as const, description: "Output as JSON", default: false },
toon: { type: "boolean" as const, description: "Output as TOON", default: false },
};

export async function open(dbPath: string) {
export async function open(dbPath?: string) {
return createObsxa({ db: dbPath });
}

Expand Down
5 changes: 3 additions & 2 deletions src/commands/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineCommand } from "citty";
import { consola } from "consola";
import { dbArgs, output } from "./_db.ts";
import { backupDatabase, restoreDatabase } from "../backup.ts";
import { getDefaultDbPath } from "../core/db-path.ts";

export default defineCommand({
meta: { name: "backup", description: "Backup or restore obsxa SQLite database files" },
Expand All @@ -16,7 +17,7 @@ export default defineCommand({
},
run({ args }) {
try {
const result = backupDatabase(args.db, args.out);
const result = backupDatabase(args.db ?? getDefaultDbPath(), args.out);
if (args.toon || args.json) return output(result, args.toon);
consola.success(`Backup created: ${result.basePath}`);
} catch (err) {
Expand All @@ -41,7 +42,7 @@ export default defineCommand({
},
run({ args }) {
try {
const result = restoreDatabase(args.db, args.from);
const result = restoreDatabase(args.db ?? getDefaultDbPath(), args.from);
if (args.toon || args.json) return output(result, args.toon);
consola.success(`Database restored from: ${result.restoredFrom}`);
if (result.preRestoreBackup) {
Expand Down
42 changes: 42 additions & 0 deletions src/core/db-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { homedir } from "node:os";
import { posix, win32 } from "node:path";

function isAbsoluteForPlatform(path: string, platform: NodeJS.Platform): boolean {
return platform === "win32" ? win32.isAbsolute(path) : posix.isAbsolute(path);
}

export function getDefaultDbPath(
env: NodeJS.ProcessEnv = process.env,
home = homedir(),
platform: NodeJS.Platform = process.platform,
): string {
const xdgDataHome = env.XDG_DATA_HOME?.trim();
const localAppData = env.LOCALAPPDATA?.trim();

if (xdgDataHome && xdgDataHome.length > 0 && isAbsoluteForPlatform(xdgDataHome, platform)) {
return platform === "win32"
? win32.join(xdgDataHome, "obsxa", "obsxa.db")
: posix.join(xdgDataHome, "obsxa", "obsxa.db");
}

if (platform === "win32") {
if (localAppData && localAppData.length > 0 && win32.isAbsolute(localAppData)) {
return win32.join(localAppData, "obsxa", "obsxa.db");
}
if (!home || home.trim().length === 0 || !win32.isAbsolute(home)) {
throw new Error("Home directory must not be empty");
}
return win32.join(home, "AppData", "Local", "obsxa", "obsxa.db");
}

if (!home || home.trim().length === 0 || !posix.isAbsolute(home)) {
throw new Error("Home directory must not be empty");
}

const fallbackDataHome =
platform === "darwin"
? posix.join(home, "Library", "Application Support")
: posix.join(home, ".local", "share");

return posix.join(fallbackDataHome, "obsxa", "obsxa.db");
}
9 changes: 9 additions & 0 deletions src/core/observation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ export function createObservationStore(db: ObsxaDB) {
return row ? toObservation(row) : null;
},

async getByInputHash(projectId: string, inputHash: string): Promise<Observation | null> {
const row = await db
.select()
.from(observations)
.where(and(eq(observations.projectId, projectId), eq(observations.inputHash, inputHash)))
.get();
return row ? toObservation(row) : null;
},

async list(
projectId: string,
opts?: {
Expand Down
19 changes: 13 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { drizzle } from "drizzle-orm/libsql/node";
import { migrate } from "drizzle-orm/libsql/migrator";
import { createAnalysisStore } from "./core/analysis.ts";
import { createClusterStore } from "./core/cluster.ts";
import { getDefaultDbPath } from "./core/db-path.ts";
import { createDedupStore } from "./core/dedup.ts";
import { createObservationStore } from "./core/observation.ts";
import { createProjectStore } from "./core/project.ts";
Expand Down Expand Up @@ -52,6 +53,9 @@ export type {
UpdateObservation,
} from "./types.ts";

export { createObsxaPlugin } from "./opencode.ts";
export type { ObsxaPluginOptions } from "./opencode.ts";

function findMigrationsFolder(): string {
const start = dirname(fileURLToPath(import.meta.url));
let current = start;
Expand Down Expand Up @@ -79,7 +83,7 @@ async function ensureMetaTable(client: Client): Promise<void> {
}

async function getSchemaVersion(client: Client): Promise<number | null> {
let result;
let result: Awaited<ReturnType<Client["execute"]>>;
try {
result = await client.execute({
sql: "SELECT value FROM obsxa_meta WHERE key = ?",
Expand Down Expand Up @@ -146,6 +150,7 @@ const CUSTOM_SQL = [
"CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type)",
"CREATE INDEX IF NOT EXISTS idx_observations_source_type ON observations(source_type)",
"CREATE INDEX IF NOT EXISTS idx_observations_triage ON observations(triage_score)",
"CREATE INDEX IF NOT EXISTS idx_observations_project_input_hash ON observations(project_id, input_hash)",
"CREATE INDEX IF NOT EXISTS idx_rel_from ON observation_relations(from_observation_id)",
"CREATE INDEX IF NOT EXISTS idx_rel_to ON observation_relations(to_observation_id)",
"CREATE INDEX IF NOT EXISTS idx_rel_type ON observation_relations(type)",
Expand Down Expand Up @@ -209,17 +214,19 @@ export interface ObsxaInstance {
* await obsxa.close()
* ```
*/
export async function createObsxa(
options: ObsxaOptions = { db: "./obsxa.db" },
): Promise<ObsxaInstance> {
export async function createObsxa(options: ObsxaOptions = {}): Promise<ObsxaInstance> {
const dbPath = options.db ?? getDefaultDbPath();
const resolved = {
autoMigrate: options.autoMigrate ?? true,
autoBackup: options.autoBackup ?? true,
backupDir: options.backupDir,
};

const dbUrl = toLibsqlUrl(options.db);
const backupDbPath = toBackupDbPath(options.db);
const dbUrl = toLibsqlUrl(dbPath);
const backupDbPath = toBackupDbPath(dbPath);
Comment thread
oritwoen marked this conversation as resolved.
if (backupDbPath) {
mkdirSync(dirname(backupDbPath), { recursive: true });
}
const client = createClient({ url: dbUrl });
try {
try {
Expand Down
Loading
Loading