just-bash-postgres
v1.0.0
Published
PostgreSQL filesystem provider for just-bash with ltree, FTS, vector search, and RLS
Maintainers
Readme
just-bash-postgres
A PostgreSQL-backed filesystem provider for just-bash. Implements the IFileSystem interface using a single fs_nodes table with ltree for hierarchy, built-in full-text search, optional pgvector semantic search, and row-level security for per-session isolation.
Features
- Full
IFileSystemimplementation -- files, directories, symlinks, hard links, chmod, stat, recursive cp/rm/mv - Full-text search -- PostgreSQL
tsvectorwith weighted filename + content ranking - Semantic search -- pgvector cosine similarity with any embedding provider
- Hybrid search -- combined text + vector ranking with configurable weights
- Multi-tenant isolation -- per-session scoping via
sessionId, enforced by both application logic and RLS - Idempotent schema setup -- safe to call
setup()on every startup
Installation
bun add just-bash-postgresOr with npm:
npm install just-bash-postgresPrerequisites
- Bun >= 1.0
- PostgreSQL 14+ with the ltree extension (included in most distributions)
- pgvector extension (optional, only needed for semantic/hybrid search)
Quick Start
import postgres from "postgres";
import { PgFileSystem } from "just-bash-postgres";
import { Bash } from "just-bash";
const sql = postgres("postgres://user:pass@localhost:5432/mydb");
const fs = new PgFileSystem({ sql, sessionId: 1 });
await fs.setup(); // creates tables, indexes, and RLS policies
const bash = new Bash({ fs, cwd: "/", defenseInDepth: false });
await bash.exec('echo "hello" > /greeting.txt');
const result = await bash.exec("cat /greeting.txt");
console.log(result.stdout); // "hello\n"Note:
defenseInDepth: falseis required when using just-bash with postgres.js because the defense-in-depth sandbox restricts raw network access that postgres.js needs for its connection.
Schema Setup
fs.setup() runs an idempotent migration that creates the fs_nodes table, indexes, and RLS policies, along with the root directory for the session. Safe to call on every startup -- all statements use IF NOT EXISTS guards.
If you pass embeddingDimensions in the options, setup() also creates the pgvector extension and adds an embedding column with an HNSW index.
Search
Three search methods are available beyond the standard IFileSystem interface.
Full-Text Search
Always available. Uses PostgreSQL's tsvector with filename weighted higher than content.
const results = await fs.search("database migration");
// [{ path: "/docs/migration-guide.txt", name: "migration-guide.txt", rank: 0.8, snippet: "..." }]Semantic Search
Requires an embedding provider. Uses pgvector cosine similarity over HNSW indexes.
const fs = new PgFileSystem({
sql,
sessionId: 1,
embed: async (text) =>
openai.embeddings
.create({ input: text, model: "text-embedding-3-small" })
.then((r) => r.data[0].embedding),
embeddingDimensions: 1536,
});
await fs.setup();
const results = await fs.semanticSearch("how to deploy the app");Hybrid Search
Combines full-text and vector search with configurable weights (default: 0.4 text, 0.6 vector).
const results = await fs.hybridSearch("deployment guide", {
textWeight: 0.3,
vectorWeight: 0.7,
limit: 10,
});All search methods accept an optional path parameter to scope results to a subtree:
const results = await fs.search("config", { path: "/app/settings" });SearchResult
interface SearchResult {
path: string;
name: string;
rank: number;
snippet?: string; // only present for full-text search
}Session Isolation
Each PgFileSystem instance is bound to a sessionId. All queries include WHERE session_id = $sessionId, and the database schema enforces the same constraint via RLS policies. Sessions cannot see or modify each other's files.
const sessionAFs = new PgFileSystem({ sql, sessionId: 1 });
const sessionBFs = new PgFileSystem({ sql, sessionId: 2 });
await sessionAFs.setup();
await sessionBFs.setup();
await sessionAFs.writeFile("/secret.txt", "session A data");
await sessionBFs.exists("/secret.txt"); // false -- completely isolatedNo sessions table is required. sessionId is just a positive integer; session management is the consuming application's responsibility.
Configuration
interface PgFileSystemOptions {
/** postgres.js connection instance */
sql: postgres.Sql;
/** Positive integer session ID for isolation. All operations are scoped to this session. */
sessionId: number;
/** Maximum file size in bytes (default: 100MB) */
maxFileSize?: number;
/** Statement timeout in milliseconds (default: 30000) */
statementTimeout?: number;
/** Async function that returns an embedding vector for text content.
When provided, writeFile generates embeddings automatically. */
embed?: (text: string) => Promise<number[]>;
/** Dimension of embedding vectors. Required if embed is provided.
Must match your embed function output (e.g. 1536 for text-embedding-3-small). */
embeddingDimensions?: number;
}API
Filesystem Operations
| Method | Description |
|--------|-------------|
| setup() | Create schema, indexes, RLS policies, and root directory |
| writeFile(path, content) | Create or overwrite a file |
| readFile(path) | Read file as UTF-8 string |
| readFileBuffer(path) | Read file as Uint8Array |
| appendFile(path, content) | Append to a file |
| exists(path) | Check if path exists |
| stat(path) | Get file stats (follows symlinks) |
| lstat(path) | Get file stats (does not follow symlinks) |
| mkdir(path, options?) | Create directory ({ recursive: true } supported) |
| readdir(path) | List directory entries as strings |
| readdirWithFileTypes(path) | List directory entries with type info |
| rm(path, options?) | Delete file or directory ({ recursive: true } supported) |
| mv(src, dest) | Move/rename file or directory |
| cp(src, dest, options?) | Copy file or directory ({ recursive: true } supported) |
| chmod(path, mode) | Change file mode |
| utimes(path, atime, mtime) | Update modification time |
| symlink(target, path) | Create a symbolic link |
| readlink(path) | Read symlink target |
| link(src, dest) | Create a hard link (copies content) |
| realpath(path) | Resolve symlinks (max 16 levels) |
Search Operations
| Method | Description |
|--------|-------------|
| search(query, options?) | Full-text search with websearch syntax |
| semanticSearch(query, options?) | Vector cosine similarity search |
| hybridSearch(query, options?) | Combined text + vector search |
Schema Utilities
| Export | Description |
|--------|-------------|
| setupSchema(sql) | Run schema migration standalone |
| setupVectorColumn(sql, dimensions) | Add vector column standalone |
| FsError | Error class with POSIX codes (ENOENT, EISDIR, etc.) |
Security
Trust Model
The sessionId is trusted without verification. The library assumes the consuming application has validated the session before constructing a PgFileSystem instance.
Connection Security
Use TLS for database connections in production:
const sql = postgres("postgres://user:pass@host:5432/db?sslmode=require");Row-Level Security
Isolation is enforced at two levels: application-level WHERE session_id = ... on every query, and database-level RLS policies. For RLS to be effective, connect as a non-superuser role -- PostgreSQL superusers bypass RLS.
setup() automatically grants permissions to a role named fs_app if it exists:
CREATE ROLE fs_app LOGIN PASSWORD 'your-password';
GRANT CONNECT ON DATABASE your_db TO fs_app;Run setup() once with a superuser connection to create the schema, then use fs_app for normal operations:
const sql = postgres("postgres://fs_app:your-password@localhost:5432/mydb");Defense in Depth
Setting defenseInDepth: false on the just-bash Bash instance disables just-bash's built-in sandbox, which is necessary because postgres.js requires raw network access. Compensate with network-level controls (firewall rules, VPC configuration) to restrict what the host can reach.
Development
Setup
git clone https://github.com/F1nnM/just-bash-postgres.git
cd just-bash-postgres
bun install
docker compose up -dRunning Tests
Tests run against a real PostgreSQL instance (110+ tests across 7 files):
docker compose up -d
bun testBy default, tests connect to postgres://postgres@localhost:5433/just_bash_postgres_test. Override with TEST_DATABASE_URL.
Type Checking
bun run typecheckPublishing
Releases are published to npm automatically via GitHub Actions when you create a release on GitHub.
To publish manually:
npm publishRequires
NPM_TOKENsecret in the repository settings for automated publishing.
