Files
obelisk/packages/core/src/core.ts
T
tommy0103 90191e4604 feat(cli): extract Obelisk runtime into npm package
Add @obelisk-apps/cli with the existing build, search, query, and attune contract plus official skill installation.

Separate the docs-only skill artifact, bootstrap installer, release layout, cross-platform CI, and package-level regression coverage.
2026-07-16 17:26:23 +08:00

91 lines
3.4 KiB
TypeScript

// Obelisk Core package (see docs/adr/0003-core-typescript-esm-precompiled.md).
//
// The single shared implementation behind every transport. The CLI and later
// the MCP server are thin shells over these four functions;
// none of them re-implement retrieval or own the DB lifecycle.
//
// Authored in TypeScript with erasable-only syntax so Node can run it directly
// via type stripping in development, while the CLI package ships readable,
// non-bundled tsc output. Core source lives in the @obelisk/core workspace.
import { createContext, runInNewContext } from 'node:vm';
import { DB_PATH, openDb, openReadDb, openWriterLeaseDb } from './db.ts';
import { buildIndex, shouldSkipBuild } from './indexer.ts';
import { createQueryApi, createAttuneApi } from './query.ts';
import { acquireWriterLease, writerLockPathFor } from './writer-lease.ts';
export { buildIndex, DB_PATH };
type SandboxApi = Record<string, unknown>;
// Run a user-supplied CodeAct script inside the query/attune sandbox. The script
// body runs as an async IIFE with a 30s timeout; its `return` value is resolved.
function runInSandbox(api: SandboxApi, scriptContent: string): Promise<unknown> {
const sandbox = {
...api, JSON, Math, Array, Object, Set, Map, Date, RegExp,
parseInt, parseFloat, String, Number, Boolean, Error, Promise, console, setTimeout,
};
const ctx = createContext(sandbox);
return runInNewContext(`(async()=>{${scriptContent}})()`, ctx, { timeout: 30000 });
}
// FTS search over indexed message text. Refreshes the index, then queries.
export function searchText(text: string, opts?: Record<string, unknown>): unknown {
buildIndex();
const db = openReadDb();
try {
return createQueryApi(db).search(text, opts);
} finally {
db.close();
}
}
// Execute a read-only CodeAct query script and resolve its returned value.
export async function executeQuery(scriptContent: string): Promise<unknown> {
buildIndex();
const db = openReadDb();
try {
return await runInSandbox(createQueryApi(db), scriptContent);
} finally {
db.close();
}
}
// Execute a memory-mutation CodeAct script (remember/forget only).
export async function executeAttune(scriptContent: string): Promise<unknown> {
const build = buildIndex() as { reason?: string } | undefined;
if (build?.reason === 'daemon_active') {
throw new Error('Obelisk daemon owns index writes; attune is read-only until the daemon stops');
}
if (build?.reason === 'writer_busy' || build?.reason === 'database_busy') {
throw new Error('Obelisk index writer is busy; attune was not applied');
}
const lease = acquireWriterLease({
lockPath: writerLockPathFor(DB_PATH),
openDb: openWriterLeaseDb,
waitMs: 1000,
});
if (!lease) throw new Error('Obelisk index writer is busy; attune was not applied');
try {
// Close the heartbeat TOCTOU window after acquiring the hard lease.
const ownershipDb = openReadDb();
try {
const ownership = shouldSkipBuild(ownershipDb, { ignoreRecentBuild: true });
if (ownership.reason === 'daemon_active') {
throw new Error('Obelisk daemon owns index writes; attune is read-only until the daemon stops');
}
} finally {
ownershipDb.close();
}
const db = openDb();
try {
return await runInSandbox(createAttuneApi(db), scriptContent);
} finally {
db.close();
}
} finally {
lease.release();
}
}