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.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Obelisk CLI
|
||||
|
||||
The local Obelisk runtime used by coding agents. It indexes Claude Code and
|
||||
Codex transcripts into `~/.obelisk/obelisk.sqlite` and exposes the stable
|
||||
`build`, `search`, `query`, and `attune` process interface.
|
||||
|
||||
```bash
|
||||
npm install --global @obelisk-apps/cli
|
||||
obelisk --version
|
||||
obelisk install
|
||||
obelisk --query /tmp/query.mjs
|
||||
```
|
||||
|
||||
`obelisk install` installs the separate docs-only agent skill from
|
||||
`tommy0103/obelisk-skill`. The CLI itself remains daemon-free: each command
|
||||
refreshes the local index when write ownership is available, then exits.
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@obelisk-apps/cli",
|
||||
"version": "0.1.0",
|
||||
"description": "Local Obelisk runtime for coding agents.",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"obelisk": "dist/cli/src/obelisk.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { copyFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const repoRoot = resolve(cliRoot, '../..');
|
||||
const outDir = resolve(cliRoot, 'dist');
|
||||
const tsc = resolve(repoRoot, 'node_modules/typescript/bin/tsc');
|
||||
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
execFileSync(process.execPath, [tsc, '-p', resolve(cliRoot, 'tsconfig.build.json')], {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
const schemaTarget = resolve(outDir, 'core/src/schema.sql');
|
||||
mkdirSync(dirname(schemaTarget), { recursive: true });
|
||||
copyFileSync(resolve(repoRoot, 'packages/core/src/schema.sql'), schemaTarget);
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
import {
|
||||
DB_PATH,
|
||||
buildIndex,
|
||||
searchText,
|
||||
executeQuery,
|
||||
executeAttune,
|
||||
} from '../../core/src/core.ts';
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const fail = (value: unknown): void => {
|
||||
const error = value instanceof Error ? value : new Error(String(value));
|
||||
process.stdout.write(JSON.stringify({ error: error.message, stack: error.stack }) + '\n');
|
||||
process.exitCode = 1;
|
||||
};
|
||||
const emit = (value: unknown): void => {
|
||||
process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
||||
};
|
||||
|
||||
if (args[0] === '--version' || args[0] === '-v') {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
|
||||
) as { version: string };
|
||||
process.stdout.write(`${packageJson.version}\n`);
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--build') {
|
||||
try {
|
||||
buildIndex({ force: true });
|
||||
process.stdout.write(JSON.stringify({ ok: true, db: DB_PATH }) + '\n');
|
||||
} catch (error) { fail(error); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--search' && args[1]) {
|
||||
try { emit(searchText(args.slice(1).join(' '))); } catch (error) { fail(error); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--query' && args[1]) {
|
||||
try { emit(await executeQuery(readFileSync(resolve(args[1]), 'utf8'))); } catch (error) { fail(error); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--attune' && args[1]) {
|
||||
try { emit(await executeAttune(readFileSync(resolve(args[1]), 'utf8'))); } catch (error) { fail(error); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === 'install') {
|
||||
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
const child = spawnSync(
|
||||
npx,
|
||||
['--yes', 'skills', 'add', 'tommy0103/obelisk-skill', ...args.slice(1)],
|
||||
{ stdio: 'inherit', shell: process.platform === 'win32' },
|
||||
);
|
||||
if (child.error) {
|
||||
process.stderr.write(`Unable to run the skills installer: ${child.error.message}\n`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.exitCode = child.status ?? 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
process.stderr.write('Usage:\n obelisk install [skills options]\n obelisk --build\n obelisk --search "text"\n obelisk --query <file.js>\n obelisk --attune <file.js>\n');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "..",
|
||||
"declaration": false,
|
||||
"rewriteRelativeImportExtensions": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../core/src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../core/src/runtime.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// Obelisk Core package (see docs/adr/0003-core-typescript-esm-precompiled.md).
|
||||
//
|
||||
// The single shared implementation behind every transport. runtime.js (skill),
|
||||
// and later the CLI and MCP server, are thin shells over these four functions;
|
||||
// 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 skill artifact ships readable,
|
||||
// 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';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// Provider-agnostic and binding-agnostic: it consumes the IndexRecord stream
|
||||
// from any adapter's parse() and writes rows into the injected database handle
|
||||
// (node:sqlite for the skill/CLI, better-sqlite3 for the app — they share the
|
||||
// (node:sqlite for the CLI, better-sqlite3 for the app — they share the
|
||||
// prepare/run/get API). It is the ONLY layer that touches the database and the
|
||||
// only place that knows the schema. Adapters stay pure.
|
||||
//
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Skill transport: a typed thin CLI shell over the Obelisk Core package.
|
||||
// It only parses args, reads script files, prints JSON, and owns the uniform
|
||||
// { error, stack } + exit-1 error envelope. All logic lives in Core.
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
import { DB_PATH, buildIndex, searchText, executeQuery, executeAttune } from './core.ts';
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
// Uniform error envelope across all four verbs: a failure is reported as
|
||||
// { error, stack } on stdout with exit code 1, never a raw crash on stderr.
|
||||
const fail = (e: unknown): void => {
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
process.stdout.write(JSON.stringify({ error: error.message, stack: error.stack }) + '\n');
|
||||
process.exitCode = 1;
|
||||
};
|
||||
const emit = (r: unknown): void => {
|
||||
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
||||
};
|
||||
|
||||
if (args[0] === '--build') {
|
||||
try {
|
||||
buildIndex({ force: true });
|
||||
process.stdout.write(JSON.stringify({ ok: true, db: DB_PATH }) + '\n');
|
||||
} catch (e) { fail(e); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--search' && args[1]) {
|
||||
try { emit(searchText(args.slice(1).join(' '))); } catch (e) { fail(e); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--query' && args[1]) {
|
||||
try { emit(await executeQuery(fs.readFileSync(path.resolve(args[1]), 'utf8'))); } catch (e) { fail(e); }
|
||||
return;
|
||||
}
|
||||
if (args[0] === '--attune' && args[1]) {
|
||||
try { emit(await executeAttune(fs.readFileSync(path.resolve(args[1]), 'utf8'))); } catch (e) { fail(e); }
|
||||
return;
|
||||
}
|
||||
process.stderr.write('Usage:\n node runtime.js --build\n node runtime.js --search "text"\n node runtime.js --query <file.js>\n node runtime.js --attune <file.js>\n');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,6 +1,6 @@
|
||||
// Binding-agnostic SQLite write plumbing shared from the Core package
|
||||
// (docs/adr/0006). The injected db must expose `exec(sql)`; this works for both
|
||||
// node:sqlite (skill/CLI) and better-sqlite3 (app), same injection model as
|
||||
// node:sqlite (CLI) and better-sqlite3 (app), same injection model as
|
||||
// `persist`.
|
||||
|
||||
export interface WriteTxDb {
|
||||
|
||||
Reference in New Issue
Block a user