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:
@@ -7,7 +7,8 @@
|
||||
> persist layer, not one per binding.
|
||||
|
||||
**Context.** Obelisk had two divergent full indexers — the former
|
||||
`scripts/indexer.mjs` (`node:sqlite`, skill/runtime) and `app/indexer.js`
|
||||
`scripts/indexer.mjs` (`node:sqlite`, the former skill-embedded runtime) and
|
||||
`app/indexer.js`
|
||||
(`better-sqlite3`, Electron
|
||||
app) — that duplicated the same Claude and Codex JSONL parsing and had silently
|
||||
diverged in write semantics (`INSERT OR REPLACE` vs `ON CONFLICT DO UPDATE`,
|
||||
@@ -31,14 +32,14 @@ binding-agnostic and does not need a per-binding implementation.
|
||||
incremental `index_state` bookkeeping, FTS maintenance, and the canonical
|
||||
**upsert** (`ON CONFLICT(uuid) DO UPDATE`) write semantics reconciled from the
|
||||
drift on 2026-07-08. The database handle is *injected*, so `node:sqlite`
|
||||
(skill/CLI) and `better-sqlite3` (app) run the same code — there is no
|
||||
(CLI) and `better-sqlite3` (app) run the same code — there is no
|
||||
per-binding persist layer.
|
||||
|
||||
**Two indexing modes** share all of the above and differ only in trigger:
|
||||
**daemon mode** (app/CLI watches and keeps the index fresh) and **passive pull
|
||||
mode** (skill indexes on invocation when no daemon is active). They never write
|
||||
concurrently — passive mode detects a fresh daemon via heartbeat markers in
|
||||
`index_state` (**daemon arbitration**).
|
||||
**daemon mode** (the app, and potentially a future CLI daemon, watches and keeps
|
||||
the index fresh) and **passive pull mode** (a CLI command indexes on invocation
|
||||
when no daemon is active). They never write concurrently — passive mode detects
|
||||
a fresh daemon via heartbeat markers in `index_state` (**daemon arbitration**).
|
||||
|
||||
**Consequences.** Golden tests anchor on each adapter's `parse` output (feed
|
||||
fixture JSONL, assert the yielded record sequence) — independent of binding and
|
||||
|
||||
@@ -5,8 +5,8 @@ pin what "the contract" is so refactoring cannot silently change observable
|
||||
behavior. The four verbs (`build`/`search`/`query`/`attune`) are only the entry
|
||||
surface; agents actually depend on the *return shapes* of the sandbox helpers
|
||||
(`search`, `overview`, `memories`, …), which are already documented in
|
||||
`references/api-reference.md` and relied on by every example in
|
||||
`references/query-patterns.md`. Current behavior is good and there is no reason to
|
||||
`skill-doc/references/api-reference.md` and relied on by every example in
|
||||
`skill-doc/references/query-patterns.md`. Current behavior is good and there is no reason to
|
||||
change it during migration.
|
||||
|
||||
**Decision.** Freeze the contract in two tiers. **Tier 1 (hard freeze, golden
|
||||
@@ -16,8 +16,8 @@ read-only enforcement, `attune` exposing only `remember`/`forget`, the set of
|
||||
globals/helpers available inside `query`/`attune`). **Tier 2 (locked to
|
||||
api-reference.md):** each helper's documented return shape — not frozen forever,
|
||||
but never allowed to drift silently; contract tests assert the live shape matches
|
||||
`references/api-reference.md`, so changing a helper forces a doc change plus a
|
||||
deliberate version bump. `references/api-reference.md` is therefore promoted from
|
||||
`skill-doc/references/api-reference.md`, so changing a helper forces a doc change plus a
|
||||
deliberate version bump. `skill-doc/references/api-reference.md` is therefore promoted from
|
||||
description to authoritative contract, and Phase 1 becomes "make it authoritative
|
||||
and enforce it," not "write a new contract doc."
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
# Core is authored in TypeScript, shipped as precompiled ESM JavaScript
|
||||
|
||||
**Context.** The extracted Obelisk Core must serve two consumers — the ESM skill
|
||||
runtime (`node:sqlite`) and the CommonJS Electron app (`better-sqlite3`) — while
|
||||
the skill artifact must install with **zero build step** on the user's machine
|
||||
(the clone-and-run, "low-friction skill" goal). Authoring in TS gives the infra
|
||||
its checkable contracts, but raises how the compiled output is shipped and which
|
||||
module format it targets.
|
||||
**Context.** The extracted Obelisk Core must serve two consumers — the ESM CLI
|
||||
runtime (`node:sqlite`) and the Electron app (`better-sqlite3`) — while the CLI
|
||||
must install with **zero build step** on the user's machine. Authoring in TS
|
||||
gives the infrastructure checkable contracts, but raises how compiled output is
|
||||
shipped and which module format it targets. The formal agent skill is a separate
|
||||
docs-only artifact and must not carry a second runtime.
|
||||
|
||||
**Decision.** Author all of Core in the `@obelisk/core` npm workspace
|
||||
(`packages/core`) in TypeScript and compile it ahead-of-time to
|
||||
**ESM JavaScript plus `.d.ts`**. The skill/CLI runtime ships the *precompiled*
|
||||
ESM JS, so installing the skill never runs a build. Rather than have Core
|
||||
**ESM JavaScript plus `.d.ts`**. `@obelisk-apps/cli` ships the *precompiled*
|
||||
ESM JS, so installing the CLI never runs a build. Rather than have Core
|
||||
dual-publish CJS+ESM, the Electron main process migrates to ESM at Phase 5 so it
|
||||
can `import` the same compiled Core. TypeScript source is the single source of
|
||||
truth; the build step lives in the main repo (`build:skill`), never on the user's
|
||||
machine.
|
||||
truth; the package build lives in the main repo (`build:cli`), never on the
|
||||
user's machine. `build:skill` copies only `skill-doc/SKILL.md`, references, and
|
||||
skill metadata.
|
||||
|
||||
**Consequences.** A one-time ESM migration of the Electron main process (Phase 5),
|
||||
in exchange for no dual-build maintenance and a single module format across skill,
|
||||
CLI, and app. The shipped skill artifact contains compiled JS, not TS. The
|
||||
in exchange for no dual-build maintenance and a single module format across the
|
||||
CLI and app. The shipped CLI package contains compiled JS, not TS. The
|
||||
renderer (Vue) is out of scope and stays JavaScript. Phase 3's TS baseline only
|
||||
adds root tooling (package.json, tsconfig, ESLint); it does not touch the app.
|
||||
The app imports Core source so electron-vite can bundle it, while package and
|
||||
skill builds compile the same workspace source to JavaScript.
|
||||
CLI builds compile the same workspace source to JavaScript.
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
# The skill artifact ships readable compiled JS, deliberately not bundled
|
||||
# The CLI ships readable compiled JS; the skill remains docs-only
|
||||
|
||||
**Context.** Obelisk reads a user's entire local Claude Code and Codex history,
|
||||
so auditability is the foundation of trust: before a user lets the skill loose on
|
||||
their data, they must be able to read what it does. The obvious way to shrink a
|
||||
clone-and-run skill artifact is to bundle/minify Core into a single `runtime.js`,
|
||||
but that ships an opaque blob into `.claude/skills` / `.agents/skills`. The
|
||||
"don't drag the whole repo into the user's skills dir" concern is real but
|
||||
separate — it is solved by shipping *only Core*, not by bundling.
|
||||
so auditability is the foundation of trust. Bundling/minifying Core into one
|
||||
opaque file would make the runtime harder to inspect. Shipping executable Core
|
||||
inside `.claude/skills` / `.agents/skills` would also blur the boundary between
|
||||
the agent's instructions and the local data runtime.
|
||||
|
||||
**Decision.** The skill artifact ships **readable, non-bundled, non-minified**
|
||||
**Decision.** `@obelisk-apps/cli` ships **readable, non-bundled, non-minified**
|
||||
compiled JavaScript emitted straight from `tsc` (module structure and comments
|
||||
preserved, ~1:1 with the TypeScript source), plus `schema.sql`, `SKILL.md`, and
|
||||
`references/`. It excludes `app/`, `release/`, `renderer/`, Electron code, and
|
||||
`tests/`, which is what keeps it small. Bundling into one file is deliberately
|
||||
rejected: it trades auditability for marginal size, the wrong trade for a
|
||||
history-reading tool. The public TS source in the main repo allows cross-checking.
|
||||
preserved, ~1:1 with the TypeScript source), plus `schema.sql`. It excludes the
|
||||
app, renderer, release assets, and tests. The separately published agent skill
|
||||
ships only `SKILL.md`, `references/`, and metadata; every executable action in
|
||||
the skill delegates to the installed `obelisk` command. Bundling into one file
|
||||
is deliberately rejected because it trades auditability for marginal size.
|
||||
|
||||
**Consequences.** The installed skill is a few readable files rather than one
|
||||
blob; a future contributor may be tempted to "optimize" by bundling — this ADR
|
||||
records that the un-bundled form is intentional. Small artifact size comes from
|
||||
scoping the artifact to Core, handled by `build:skill`, not from a bundler.
|
||||
**Consequences.** Runtime ownership is unambiguous: npm installs the CLI, while
|
||||
the skills installer installs only agent guidance. A future contributor may be
|
||||
tempted to re-embed Core in the skill or bundle the CLI — this ADR records that
|
||||
both are intentional boundaries. `build:cli` owns compiled code;
|
||||
`build:skill` owns docs-only packaging.
|
||||
|
||||
@@ -25,7 +25,7 @@ decisions within this:
|
||||
injecting `better-sqlite3`. This
|
||||
works because the provider→parsing import graph is node:sqlite-free (ADR-0001),
|
||||
so nothing drags `node:sqlite` into the app. The `dist/` from `build:core`
|
||||
(ADR-0003) remains for the skill artifact; the app does not need it.
|
||||
(ADR-0003) remains for the CLI package; the app does not need it.
|
||||
- **better-sqlite3 stays the app's binding**, externalized (not bundled) and
|
||||
unpacked from the asar.
|
||||
- **The app main + preload source is TypeScript with types at its seams**, but
|
||||
|
||||
@@ -7,7 +7,7 @@ threw over the primary exception and turned a skippable per-file failure into a
|
||||
whole-build failure. The masked exception was not preserved, so contention
|
||||
(`SQLITE_BUSY` / `SQLITE_BUSY_SNAPSHOT`) is the leading explanation rather than
|
||||
a proven historical fact. It is plausible because daemon builds, manual
|
||||
rebuilds, skill passive-pull indexing, heartbeat writes, and reads share one WAL
|
||||
rebuilds, CLI passive-pull indexing, heartbeat writes, and reads share one WAL
|
||||
database.
|
||||
|
||||
`busy_timeout` alone is not a correctness fix. In particular,
|
||||
@@ -35,15 +35,15 @@ layers.
|
||||
failures propagate. `affectedSessionIds` is updated only after the relevant
|
||||
commit. Force cleanup is one atomic, retryable transaction, and finalize is
|
||||
likewise retried as a complete idempotent transaction.
|
||||
- A fresh `__app_heartbeat__` is policy ownership: while it is fresh, the skill
|
||||
- A fresh `__app_heartbeat__` is policy ownership: while it is fresh, the CLI
|
||||
opens no write connection and performs no migration, schema setup, checkpoint,
|
||||
index build, or `attune`. `__app_last_successful_build__` remains an
|
||||
observability/freshness marker and is not required for ownership. The skill
|
||||
observability/freshness marker and is not required for ownership. The CLI
|
||||
checks ownership again after acquiring the hard lease to close the TOCTOU
|
||||
window. Search/query connections are read-only.
|
||||
- A dedicated `.obelisk/writer.lock.sqlite` provides the cross-process safety
|
||||
mutex on every platform. Acquisition is `BEGIN IMMEDIATE` with non-blocking or
|
||||
bounded waiting; release is idempotent. App builds and heartbeats, skill builds
|
||||
bounded waiting; release is idempotent. App builds and heartbeats, CLI builds
|
||||
and attune, app schema/legacy migrations and memory mutations, and manual
|
||||
rebuild all participate. Manual rebuild's main process owns the lease across
|
||||
worker build, atomic target replacement, and database reopen; the worker uses
|
||||
@@ -52,7 +52,7 @@ layers.
|
||||
deferral retains changed paths and schedules a short retry without announcing
|
||||
a successful build. Service start publishes the ownership heartbeat
|
||||
immediately, then refreshes it periodically.
|
||||
- Index-writer and skill read connections use an explicit 250 ms SQLite busy
|
||||
- Index-writer and CLI read connections use an explicit 250 ms SQLite busy
|
||||
timeout inside the larger bounded coordination budget. The long-lived app
|
||||
query connection retains a 5 s timeout; heartbeat is deliberately non-blocking
|
||||
(`0 ms`) so it never stalls the Electron main thread. Builds use
|
||||
|
||||
Reference in New Issue
Block a user