OpenSAC is a terminal AI coding assistant. This repository is the TypeScript
implementation, ported 1:1 from the Go implementation at
/home/free/src/mothx, and runs on the Node runtime (the sources are plain
TypeScript executed directly, with no build step for development).
The Go port is functionally complete for the shipping surface: src/ holds the
agent core, runtime, providers, tools, sessions, TUI, CLI, ACP, and Core host,
and CI runs check, lint, fmt:check, test, and test:architecture on
every push and pull request. The dependency-ordered backlog and migration ledger
live in
docs/proposal/go-to-typescript-migration.md.
Not yet in this repository: the planned desktop/ Electron app, the pypi/
installer, a WebUI, and the bilingual docs/en/docs/zh trees. AGENTS.md
describes those as target architecture; treat them as planned rather than
existing code.
Local checks:
npm run check # type-check src/, sdk/, examples/, scripts/
npm run lint # eslint
npm run fmt:check # formatting
npm test # full suite (includes src/architecture guards)
npm run test:architecture # boundary guards alonePorted packages (all type-checked, linted, and tested):
| Go package | TS target | Notes |
|---|---|---|
internal/util |
src/util |
UTF-8 truncation, symlink-aware path helpers |
internal/version |
src/version |
build/VCS version resolution |
internal/ua |
src/ua |
User-Agent strings |
internal/platform |
src/platform |
OS/arch, dirs, shells, sandbox paths, the Node runtime helpers |
internal/systeminit |
src/systeminit |
/systeminit prompt |
internal/db |
src/db |
process-wide SQLite lifecycle over node:sqlite |
internal/sandbox |
src/sandbox |
policy, git rules, manager, bwrap/seatbelt/windows backends |
internal/config |
src/config |
settings.json/env.json/mcp.json/allow.json schemas, provider presets, sparse load/patch |
internal/imageproc |
src/imageproc |
prepare/resize/crop, provider family inference; imagescript + @jsquash/webp codecs |
internal/skills |
src/skills |
skill discovery (builtin/global/project), references, enable/disable toggles |
internal/contextfiles |
src/contextfiles |
well-known context-file discovery (global/parent/project), .opensac/rule.md load/ensure, system-prompt assembly |
internal/expert |
src/expert |
expert bundle format (manifest + persona frontmatter), layered builtin/global/project ExpertCenter, writable CRUD manager, embedded seed bundles |
internal/tools |
src/tools |
tool Registry + standard tools (read/ls/write/edit/insert/plan/find/grep/bash/jobs/kill/question/skill_ref/image_generation), file diff/atomic write, file locks, background jobs |
top-level agent (public SDK) |
sdk/agent |
public Agent/Provider/Builder/ExternalTool interfaces + types (must not import src/) |
bootstrap |
src/bootstrap |
provider bridge wiring Builder.withProviderByName to src/provider (builder hook deferred to src/agent) |
TUI framework decision: the Go bubbletea/lipgloss TUI is replaced by Ink
(ink@^5) + React 18 (react@^18). src/tui holds the toolchain smoke test
plus the scrollback/streaming skeleton (markdown.ts + transcript.tsx:
completed blocks go to the terminal's own scrollback via <Static>, so
selection/copy/wheel use the terminal natively; only the active streaming block
stays in the managed view). See
docs/proposal/go-to-typescript-migration.md.
The streaming-Markdown renderer github.com/startvibecoding/GoStreamingMarkdown
is ported to src/tsm (node/parser/renderer/stream); it is byte-for-byte
compatible with the Go library for well-formed Markdown (see the module for the
one deliberate code-point deviation).
Node.js 22.18 or later (for node:sqlite and import.meta.main), plus npm.
node --version # >= 22.18
npm --versionnpm run check # type check src/, sdk/, examples/, scripts/
npm test # node --test
npm run lint # eslint
npm run fmt # prettier --write
npm start # run src/main.ts
npm run build:node # esbuild -> dist/node (platform-independent npm package)
npm run pack:node # build + npm pack into dist/npm/There is no build step for development: Node strips TypeScript types directly,
and a small loader (scripts/loader/ts_loader.mjs) transpiles the Ink .tsx
sources. The toolchain lives in package.json, tsconfig.json, and
.prettierrc.json; the Node runtime helpers every module imports live in
src/platform/runtime.ts (with runtime_core.ts/runtime_net.ts), loaded by
scripts/test/preload.mjs, which npm start and the test runner register with
node --import.
A v* git tag is the release. It is published as a single,
platform-independent npm package (plain JavaScript, no per-platform
binaries), bundled with esbuild and run on Node >= 22.18:
make node-build # esbuild -> dist/node
make node-pack # tarball into dist/npm/, publishing nothing
make node-publish # publish opensac-installer under the latest tagThe npm package is named opensac-installer (the bare name opensac is
rejected by npm as too similar to openai); it installs the opensac command.
NODE_SCOPE=@<owner> is optional and publishes @<owner>/opensac-installer
instead — it is required for GitHub Packages
(make node-publish-github NODE_SCOPE=@owner).
The pushed tag produces a GitHub Release, the npm package, and the container
image. ghcr-publish.yml still ships ghcr.io/<owner>/opensac for the shared
Core host.
The earlier per-platform
opensac-installer-*packages, their platform table, and the compiled-binary pipeline have been removed; the single npm package is the only release artifact.
The image runs the shared Core host rather than the TUI, which has no terminal to draw in, and exposes the Core HTTP API on port 27183:
docker run --rm -p 27183:27183 -v "$PWD:/workspace" -v opensac-state:/opensac \
ghcr.io/startvibecoding/opensac:v0.1.0/workspace is where the agent works; /opensac holds settings, sessions, and
credentials, so mount it as a volume to keep them. The image ships a
settings.json that binds the Core to 0.0.0.0, because the default
127.0.0.1 is unreachable from outside the container. There is no
authentication by default: set core.auth and a password in your own
settings.json, or terminate TLS in front of the port.
src/db owns the process-wide SQLite connection lifecycle and is the only
module that opens, configures, caches, or closes a connection. It uses the
node:sqlite driver built into Node 22.5+ (DatabaseSync), which exposes the
SQLite result codes needed for the busy/read-only classification. All SQL
construction and row mapping belongs to src/dao.
There is no permission sandbox on Node. The runtime helpers come from
src/platform/runtime.ts over Node built-ins, and scripts/test/preload.mjs
registers them for the test runner. No --allow-* flags are needed.
The project targets the Node runtime: it depends on node: builtins, npm
packages, and the project-owned src/compat/ shims. The CLI parser is the
project-owned src/cli/command_parser.ts and the package is bundled with
esbuild. Imports are pinned in package-lock.json, which should be committed.