diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b1509ee..8ec3ea4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,10 +3,18 @@ name: Publish to npm # Trusted publishing (OIDC): no npm token is stored anywhere. npm accepts the publish # because this repo + this workflow file are registered as the package's trusted publisher # (`npm trust github opencode-context-tree --file publish.yml --repo navbytes/opencode-tree`). +# Registering a new package: `npm publish` its v0.x by hand once with a real token, then +# `npm trust github --file publish.yml --repo navbytes/opencode-tree`. on: release: types: [published] - workflow_dispatch: # dispatched on the new tag by release.yml + workflow_dispatch: # dispatched on the new tag by release.yml, or run directly + inputs: + package: + description: "Package to publish (only used when run directly, not via release)" + type: choice + default: context-tree + options: [context-tree] permissions: id-token: write # mint the OIDC token npm verifies @@ -35,16 +43,36 @@ jobs: - run: bun run typecheck - run: bun test - # The tag is the version: stamp it into package.json in this checkout only. A prerelease - # (0.3.0-beta.1) goes under the `beta` dist-tag so `latest` keeps pointing at the last stable. - - name: Version from the release tag - id: version + # The tag is -v: derive both, stamp the version into that package's + # package.json in this checkout only. A prerelease (0.3.0-beta.1) goes under the `beta` + # dist-tag so `latest` keeps pointing at the last stable. Old-style bare `v*` tags (the + # transition case) have no `-v` in the middle and resolve to context-tree. + - name: Determine package and version + id: meta run: | - tag="${GITHUB_REF_NAME#v}" - echo "$tag" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$' || { echo "ref '$GITHUB_REF_NAME' is not a version tag" >&2; exit 1; } - npm version "$tag" --no-git-tag-version --allow-same-version - case "$tag" in *-*) echo "dist_tag=beta" ;; *) echo "dist_tag=latest" ;; esac >> "$GITHUB_OUTPUT" + set -euo pipefail + case "$GITHUB_REF_NAME" in + *-v*) + package="${GITHUB_REF_NAME%-v*}" + version="${GITHUB_REF_NAME#*-v}" + ;; + v*) + package="context-tree" + version="${GITHUB_REF_NAME#v}" + ;; + *) + package="${{ inputs.package }}" + version="" + ;; + esac + package="${package:-context-tree}" + echo "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$' || { echo "ref '$GITHUB_REF_NAME' is not a version tag (package=$package, version=$version)" >&2; exit 1; } + case "$version" in *-*) dist_tag=beta ;; *) dist_tag=latest ;; esac + cd "packages/$package" && npm version "$version" --no-git-tag-version --allow-same-version + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "dist_tag=$dist_tag" >> "$GITHUB_OUTPUT" # OIDC trusted publishing needs the package to already exist on npm: the very first # version of a new package is published once by hand, every later one lands here. - - run: npm publish --provenance --access public --tag "${{ steps.version.outputs.dist_tag }}" + - run: cd "packages/${{ steps.meta.outputs.package }}" && npm publish --provenance --access public --tag "${{ steps.meta.outputs.dist_tag }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9f263f..81c2e45 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,13 @@ name: Release on: workflow_dispatch: inputs: + package: + description: "Package to release" + type: choice + default: context-tree + options: [context-tree] bump: - description: "Version bump relative to the latest v* tag" + description: "Version bump relative to the latest -v* tag" type: choice default: patch options: [patch, minor, major] @@ -26,7 +31,7 @@ permissions: contents: write # push the tag, create the release actions: write # dispatch publish.yml -concurrency: release +concurrency: release-${{ inputs.package }} jobs: release: @@ -48,8 +53,16 @@ jobs: id: v run: | set -euo pipefail - latest="$(git tag --list 'v*' --sort=-v:refname | head -1)" - latest="${latest#v}"; latest="${latest:-0.0.0}" + package="${{ inputs.package }}" + prefix="${package}-v" + latest_tag="$(git tag --list "${prefix}*" --sort=-v:refname | head -1)" + latest="${latest_tag#$prefix}" + if [ -z "$latest" ] && [ "$package" = context-tree ]; then + # transition shim: context-tree's pre-monorepo history used bare v* tags + latest_tag="$(git tag --list 'v*' --sort=-v:refname | head -1)" + latest="${latest_tag#v}" + fi + latest="${latest:-0.0.0}" explicit="${{ inputs.version }}" if [ -n "$explicit" ]; then next="${explicit#v}" @@ -62,13 +75,16 @@ jobs: esac fi echo "$next" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$' || { echo "not a semver version: $next" >&2; exit 1; } - git rev-parse -q --verify "refs/tags/v$next" >/dev/null && { echo "tag v$next already exists" >&2; exit 1; } - if ! grep -q "^## ${next}" CHANGELOG.md; then - echo "::warning::CHANGELOG.md has no '## $next' section — add one on main before or after the release" + git rev-parse -q --verify "refs/tags/${prefix}${next}" >/dev/null && { echo "tag ${prefix}${next} already exists" >&2; exit 1; } + if ! grep -q "^## ${next}" "packages/${package}/CHANGELOG.md"; then + echo "::warning::packages/${package}/CHANGELOG.md has no '## $next' section — add one on main before or after the release" fi + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "prefix=$prefix" >> "$GITHUB_OUTPUT" echo "latest=$latest" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" echo "next=$next" >> "$GITHUB_OUTPUT" - echo "Latest tag v$latest → releasing v$next from $(git rev-parse --short HEAD)" + echo "Latest tag ${latest_tag:-none} → releasing ${prefix}${next} from $(git rev-parse --short HEAD)" - name: Tag, release, publish if: ${{ !inputs.dry_run }} @@ -76,16 +92,22 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail + package="${{ steps.v.outputs.package }}" + prefix="${{ steps.v.outputs.prefix }}" v="${{ steps.v.outputs.next }}" + tag="${prefix}${v}" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -a "v$v" -m "v$v" - git push origin "v$v" + git tag -a "$tag" -m "$tag" + git push origin "$tag" prerelease=""; case "$v" in *-*) prerelease="--prerelease" ;; esac - gh release create "v$v" --title "v$v" --generate-notes $prerelease - gh workflow run publish.yml --ref "v$v" - echo "Released v$v; publish workflow dispatched on the tag." + notes_args=() + latest_tag="${{ steps.v.outputs.latest_tag }}" + [ -n "$latest_tag" ] && notes_args=(--notes-start-tag "$latest_tag") + gh release create "$tag" --title "$tag" --generate-notes "${notes_args[@]}" $prerelease + gh workflow run publish.yml --ref "$tag" -f package="$package" + echo "Released $tag; publish workflow dispatched on the tag." - name: Dry run if: ${{ inputs.dry_run }} - run: echo "Dry run — would tag v${{ steps.v.outputs.next }} (latest v${{ steps.v.outputs.latest }}), create the release, and dispatch publish.yml." + run: echo "Dry run — would tag ${{ steps.v.outputs.prefix }}${{ steps.v.outputs.next }} (latest ${{ steps.v.outputs.latest_tag || 'none' }}), create the release, and dispatch publish.yml." diff --git a/README.md b/README.md index 800df32..be24190 100644 --- a/README.md +++ b/README.md @@ -1,369 +1,26 @@ -
+# opencode-tree -# opencode-context-tree - -**See your whole [OpenCode](https://opencode.ai) session as a tree — then branch it, merge it, and crop it like source code.** - -A Pi-style context tree with git-style controls (`/branch`, `/merge`, `/crop`, `/undo`) -and a DeepSeek-Harness-style trajectory view, in one screen, inside your terminal. - -[![npm](https://img.shields.io/npm/v/opencode-context-tree?color=cb3837&logo=npm)](https://www.npmjs.com/package/opencode-context-tree) [![CI](https://github.com/navbytes/opencode-tree/actions/workflows/ci.yml/badge.svg)](https://github.com/navbytes/opencode-tree/actions/workflows/ci.yml) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -[![OpenCode](https://img.shields.io/badge/OpenCode-%E2%89%A5%201.18-black)](https://opencode.ai) - -
- -![The context tree, viewed from the trunk](docs/screenshots/tree-trunk.png) - -## Why - -A long OpenCode session is a straight line that only grows. You cannot see what is -filling the context window, side quests are stuck in the transcript forever, and -auto-compaction eventually decides for you what gets forgotten. - -This plugin gives that session a shape and a set of controls: - -- **See it.** One screen shows every message and tool step in the session, with the - branches drawn at the points they forked from, and what each one costs in tokens. -- **Branch it.** Take a side quest onto its own branch — optionally on a cheaper model — - so the main thread never sees the noise. -- **Merge it.** Close a branch with a decision record you confirm yourself, so the trunk - gets the conclusion instead of the twenty turns that produced it. -- **Crop it.** Stub the 40k-token `bash` output that is squatting in your context, and - put it back with one key when you were wrong. - -The screen stays close to Pi and to -[`pi-context-tree`](https://github.com/navbytes/pi-context-tree), so it is familiar if you -are coming from either, and the trajectory view follows the DeepSeek Harness. - -> **Your transcript is never rewritten.** Crops are applied per request, so the model -> sees a stub while the stored message keeps its full text. Merges *append* a record; -> they never delete turns. Every branch, merge and crop is undoable with `u`. - -## Contents - -- [Install](#install) -- [Quick start](#quick-start) -- [The screen](#the-screen) -- [Features](#features) -- [Keys](#keys) -- [Commands](#commands) -- [Configuration](#configuration) -- [How it maps onto OpenCode](#how-it-maps-onto-opencode) -- [Performance](#performance) -- [Troubleshooting](#troubleshooting) -- [Development](#development) -- [License](#license) - -## Install - -Requires **OpenCode 1.18 or newer**. One command registers both halves of the plugin: - -```sh -opencode plugin opencode-context-tree -g # every project (~/.config/opencode) -opencode plugin opencode-context-tree # this project only (.opencode/) -``` - -Restart OpenCode, then press `ctrl+q` or run `/tree`. - -
-Registering by hand - -The plugin has two halves, and the package name must be listed in **both** config files. -`opencode.json` loads the server half (crops, branch model, headless `/ctree` commands); -`tui.json` loads the TUI half (`/tree`, `/branch`, `/merge`, the gauge, the sidebar card). -Listing it in only the first gives you `/ctree` but no `/tree`. - -```jsonc -// opencode.json (or ~/.config/opencode/opencode.jsonc) -{ "plugin": ["opencode-context-tree"] } - -// tui.json (or ~/.config/opencode/tui.json) -{ "plugin": ["opencode-context-tree"] } -``` - -
- -
-Upgrading - -OpenCode caches the version it installed and does not re-resolve `@latest` on restart. -Pin the new release, which also rewrites both config entries: - -```sh -opencode plugin opencode-context-tree@ -g --force # drop -g for this project only -``` - -The npm badge at the top of this page is the current version. Or delete `~/.cache/opencode/packages/opencode-context-tree@latest` and restart. -`?` inside `/tree` and `/ctree status` both print the version you are running. - -
- -## Quick start - -The whole workflow is four moves: - -``` -/branch fix-flaky name it → you are on a real OpenCode session, forked here - …side quest… the noisy turns live on the branch, not in your main thread -/merge Squash → the model drafts a ◆ decision record → your $EDITOR → - save to confirm → the conclusion lands on the trunk as one message -/tree see where you are, what it costs, and jump anywhere -c space ⏎ crop a fat tool result — the model sees "[cropped: bash …]" from - the next turn; u puts it back -``` - -Pressing `⏎` on any earlier message forks from that point. This is Pi's fork flow whole: -one question with Pi's three answers — fork clean, summarize everything below that point, -or summarize it with your own prompt — and the answer is also the confirmation. The -summary covers exactly the turns the move leaves behind. - -Every turn but the one you are in folds to a single row, so the outline reads as an outline -rather than a wall of tool calls: - -``` -● ▸6 T5 add a retry to the flaky test 1✗ 2⚠ ~12k -● T6 now make it pass on CI ~310 -``` - -`▸6` between the `●` and the text is the fold: the row's own disclosure control, at the row's -own left edge, saying it stands for six hidden rows. The token column already counts them. - -`za` folds or opens the turn you are on, `l` opens the folded one under the cursor (vim opens -a fold on a horizontal move), `zm` folds them all, `zr` opens them all — vim's own fold keys. -And you do not have to know them: the row the cursor is on names the key for the one thing it -affords, and only that row. Nothing is lost either — the marker counts what is inside, the -timeline still shows every event (a folded turn lights the whole span it stands for), and crop -mode opens everything while you pick targets. - -Drafting one takes a model call, so the status line shows it happening — -`⠹ summarizing 3 turns · ~14k · Progress · 1.2k chars · 4s · esc cancels`: the step, the -draft as it streams in, how long it has been, and the way out. The `◆` record a `/merge` -drafts reports the same way. - -## The screen - -`/tree` is an outline of the whole session. Every message and tool call is one -content-forward row, branches hang off the message they were forked from, and your -current branch is open while the rest stay folded: - -``` -┌ Context tree · Fix flaky test · trunk ctx ~46k/200k · filling -│ filter: default 24 rows -│ ● user: build yourself a tool that reads the context window… ~1.2k -│ ○ assistant: I'll start by inspecting my environment… 0.3k -│ ⚙ [bash $ ls -la ~/Documents/] → total 744 … ~2.1k -│ ● user: decompress the session and show the structure ~0.2k -│ ╰⎇ try-redis ▸ squashed · 9 turns ~22k -│ ╰⎇ fix-flaky ▾ open · 6 turns ← here ~14k -│ │ ● user: the bun test is flaky, find the race ~0.4k -│ │ ⚙ [bash $ bun test src/foo.test.ts] ⚠ ~4.7k -│ ◆ Decision: try-redis · Outcome: switched to a write-through cache… ~0.9k -└ ⏎ go b branch m merge c crop u undo s consumers ? help q back -``` - -The footer always says what `⏎` will do for the row under the cursor. A leading `~` on a -token count means it is estimated; assistant steps use the model's own numbers. -Markers: `⚠` over 10k tokens · `✂` cropped · `✗` tool error · `◆` decision record. -## Features +A monorepo of independently versioned and published [OpenCode](https://opencode.ai) +plugins. Each publishable package lives under `packages//` with its own +`package.json`, README, and release history; the root only holds shared tooling +(TypeScript base config, the `harness/` PTY test rig) and CI. -### Branches you can see - -From inside a branch, `← here` marks your position. Trunk rows past the fork point are -dimmed under `── not in this branch's context ──`, because the model is not sent them. - -![The tree, viewed from inside a branch](docs/screenshots/tree-from-a-branch.png) - -Sessions you create with OpenCode's own `/fork` are adopted into the tree automatically. - -### Search and filters - -`/` filters as you type, highlights the matches and counts the rows; `n` and `N` step -through them. `f` opens a filter picker — default, no-tools, user-only, labeled, all — and -the timeline lanes follow it, so `tools-only` becomes a "what did I run" view in both. - -![Live search inside the tree](docs/screenshots/search.png) - -### The trajectory view - -The DeepSeek-Harness trajectory is one keystroke away rather than in your way. `1` and -`2` bring in the Input / Model / Tools lanes — one pill per event, coloured by lane and red -for a failed tool call — laid out either by duration (`1`) or one cell per event (`2`). -`0` hides them again. `i` opens the inspector, with each step's payload, result and timing. - -The lanes appear once the session has three turns to plot, and on a long session they show -a window of the timeline that follows your cursor. - -![The tree with trajectory lanes and the inspector open](docs/screenshots/tree-trajectory.png) - -### Finding what fills your context - -`s` breaks the context down by share of the tree and of the model window, expandable into -entries you can crop in place. It counts the system prompt too, broken down by part, so -you can see what your `AGENTS.md` actually costs. `D` renders the decision records. - -| | | +| Package | Description | |---|---| -| ![The consumers view](docs/screenshots/consumers.png) | ![The decisions panel](docs/screenshots/decisions.png) | -| ![The help pane](docs/screenshots/help.png) | ![The merge picker](docs/screenshots/merge-picker.png) | - -### The gauge - -On the prompt line: the context of your next prompt (the same figure OpenCode's own -sidebar shows), its band, and how much of it the provider served from cache. The bar's -dim cells are the cached part. `0% cached` right after a crop, merge or fork means the -cache was reset. - -![The context gauge showing the provider cache share](docs/screenshots/gauge-cache.png) - -## Keys - -Vim-aligned, inside `/tree`: a key means here what it means in vim, and the verbs vim has no -word for live behind `g` the way LSP plugins put theirs (`gd`, `gr`, `gi`). Press `?` for the -full list without leaving the screen. Every key is rebindable — see `keybinds` in -[Configuration](#configuration). - -| Key | Action | -|---|---| -| `j` `k` · `ctrl+f` `ctrl+b` · `ctrl+d` `ctrl+u` · `gg` `G` | move · page · half page · top / bottom | -| `{` `}` | previous / next turn row — the outline's own unit; the lanes scrub with it | -| `[[` `]]` (or `[` `]`) | previous / next branch row | -| `h` `l` · `Tab` | fold / unfold a branch inline (`l` also opens a folded turn) | -| `za` · `zo` `zc` | fold / open / close the turn you are on | -| `zr` `zm` · `zj` `zk` | open every fold / fold every turn · move between folds | -| `H` `M` `L` | top / middle / bottom of the screen | -| `⏎` | go here — the footer names what it will do for this row | -| `gb` | branch here, naming it and optionally picking a model | -| `gm` | merge: squash, squash without the model, discard, or tournament | -| `c` | crop mode — `space` mark, `a` auto-mark, `t` result⇄turn, `⏎` apply | -| `u` | undo the last branch / merge / crop | -| `m` | mark: label the selected message | -| `/` · `n` `N` | live search · next / previous match | -| `gf` | filter picker | -| `i` `I` | inspector in the side pane / full screen (`PgUp` `PgDn` to page) | -| `g1` `g2` · `g0` | timeline lanes, x-axis by duration / one cell per event · off | -| `gs` | what is filling the context | -| `gd` `ge` | decisions panel · export to `ctree-decisions.md` | -| `y` | copy the selected text | -| `?` `q` | help · back | - -## Commands - -Not sure which one you want? [**Choosing what to do**](docs/USAGE.md#choosing-what-to-do) -compares them: what each preserves, what each costs, and what to press at 80% context. - -| Command | What it does | When | -|---|---|---| -| `/tree` (`ctrl+q`) | open the combined tree and trajectory view | [the loop](docs/USAGE.md#the-loop) | -| `/branch [model]` | fork here into a named branch, optionally on a cheaper model | [before the risky thing](docs/USAGE.md#branch--try-it-on-a-copy) | -| `/merge [--pick \| --no-llm \| --discard \| --tournament]` | close the branch — see below | [when a branch has finished](docs/USAGE.md#merge--keep-the-conclusion-not-the-noise) | -| `/crop [--top \| --auto …]` | stub fat tool results or drop whole turns from what the model sees | [when you can name the fat thing](docs/USAGE.md#crop--stop-sending-something-you-no-longer-need) | -| `/undo` | revert the last branch, merge or crop | [what it does and does not cover](docs/USAGE.md#summarize-on-a-jump--carry-the-gist-back) | -| `/decisions [--export]` | list or export decision records | — | - -`/merge` offers four ways to close a branch. **Squash** has the branch model draft a ◆ -decision record that you confirm in `$EDITOR`. **Squash without LLM** hands you the empty -template to write yourself. **Discard** lands nothing. **Tournament** keeps one of several -sibling branches. In every case the record is appended to the trunk as a normal message. - -For desktop, web or scripts there is a headless equivalent that needs no TUI: -`/ctree status`, `/ctree branch`, `/ctree merge --discard`, `/ctree crop`, `/ctree undo` -and `/ctree decisions`. See [docs/USAGE.md](docs/USAGE.md#headless-desktop--web--scripts). - -## Configuration +| [`packages/context-tree`](packages/context-tree/README.md) | Pi-style context tree for OpenCode: branch, merge, crop, undo, plus a trajectory view | -Options go in the plugin entry of either config file. Both halves read `storage`, so if -you change it, **set the same value in both files** — otherwise the TUI and the server -keep two different journals and crops written by one never reach the other. - -```jsonc -{ "plugin": [["opencode-context-tree", { "storage": "global", "jumpSummary": "never" }]] } -``` - -| Option | Values | Default | What it does | -|---|---|---|---| -| `storage` | `"local"` · `"global"` | `"local"` | where the journal lives — `.opencode/context-tree/` in the worktree (gitignored), or OpenCode's state dir | -| `jumpSummary` | `"ask"` · `"never"` | `"ask"` | whether jumping offers to summarize the turns you leave behind | -| `hardCrop` | `true` · `false` | `false` | also set OpenCode's own "compacted" flag on cropped parts, so the transcript itself shows them cleared — reversible, but it touches OpenCode storage | -| `keybinds` | object | — | override any key by command name, e.g. `{ "open": "ctrl+t", "copy": "none" }` | - -The full list of rebindable command names is in -[docs/USAGE.md](docs/USAGE.md#install). - -## How it maps onto OpenCode - -Nothing here is a private data format bolted on the side. Each feature is one of -OpenCode's own primitives: - -| Feature | Implementation | -|---|---| -| **branch** | a real OpenCode session created with `session.fork`; the plugin records `(parent, anchor)` in an append-only journal and mirrors it into `session.metadata` | -| **crop** | applied per request in `experimental.chat.messages.transform`, so the transcript keeps the originals and only the model sees stubs | -| **merge** | writes the confirmed record with `session.prompt({ noReply: true })` | -| **the UI** | a TUI plugin (`@opencode-ai/plugin/tui`): one route, two slots (gauge, sidebar card), dialogs and a keymap layer | - -Because a branch is just a session and a decision record is just a message, everything -stays readable to OpenCode — and to you — if you ever remove the plugin. - -## Performance - -Measured against OpenCode 1.18.26 on a 120-column pty. Full method and the rest of the -numbers are in [docs/USAGE.md](docs/USAGE.md#performance). - -| Session | `/tree` opens in | Search | Consumers | TUI memory | -|---|---|---|---|---| -| 57 messages | 37 ms | 2 ms | 4 ms | — | -| 117 messages | 34 ms | 2 ms | 3 ms | 37 MB | -| 467 messages | 32 ms | 1 ms | 4 ms | 37 MB | - -Opening the tree does not get slower as the session grows. The crop transform that runs on -every model request costs 0.78 ms at 484 messages with 66 active crops, and 0.03–0.27 ms on -a 50-message session. Startup to the prompt grows by about 70 ms. - -## Troubleshooting - -**`/ctree` works but `/tree` does not exist.** The TUI half is not registered. Run the -install command again, or add the package name to `tui.json` as well as `opencode.json`. - -**A crop or a branch is missing from one half.** The two halves are reading different -journals. Set the same `storage` value in both config files. - -**A turn looks stuck.** `/ctree` subcommands are dispatched as turns, so they queue behind -one that is already running. `/tree` opens synchronously from the local journal, so reach -for that one instead. - -**An upgrade did not take.** OpenCode kept the cached version. Pin the release with -`--force`, as described under [Install](#install). +To add a new plugin, create `packages//` with its own `package.json` and +release scripts, then add it to the `workflow_dispatch.inputs.package` choice +list in `.github/workflows/release.yml` and `publish.yml`. ## Development ```sh bun install -bun run build -bun test # unit tests -bun run typecheck -bun run test:e2e # pty-driven end-to-end tests against a real OpenCode TUI -``` - -To run your checkout instead of the published package, list the built files by absolute -path rather than the package name: - -```jsonc -// opencode.json → "plugin": ["/abs/path/opencode-tree/dist/server.js"] -// tui.json → "plugin": ["/abs/path/opencode-tree/dist/tui.js"] +bun run build # builds every package +bun run typecheck # typechecks every package +bun test # runs every package's tests ``` - -[DESIGN.md](./DESIGN.md) is the long version: the research behind the design (Pi, -[`pi-context-tree`](https://github.com/navbytes/pi-context-tree), the OpenCode plugin and -SDK surface, the DeepSeek Harness trajectory view), the end-user flows, the data model, -the architecture, the edge cases and the roadmap. [CHANGELOG.md](./CHANGELOG.md) records -what changed in each release. - -Issues and pull requests are welcome. Please run the typecheck and the unit tests before -opening one. - -## License - -[MIT](./LICENSE) © Naveen (navbytes) diff --git a/bun.lock b/bun.lock index 46244a0..4a0c719 100644 --- a/bun.lock +++ b/bun.lock @@ -1,9 +1,20 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { + "name": "opencode-tree-monorepo", + "devDependencies": { + "@opentui/core": "^0.5.10", + "@opentui/keymap": "^0.5.10", + "@opentui/solid": "^0.5.10", + "@types/bun": "latest", + "solid-js": "^1.9.12", + "typescript": "^5.8.2", + }, + }, + "packages/context-tree": { "name": "opencode-context-tree", + "version": "0.0.0-dev", "dependencies": { "@opencode-ai/plugin": "1.18.26", "@opencode-ai/sdk": "1.18.26", @@ -116,33 +127,33 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.26", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-1TnlEZty7V2NTIZHbJOt2O+v6YsJ/Bt2pSYgXb98G/RkSVOjlDd/35+ewfYtM4fmMroaa6cEG6S58FihKO4K0w=="], - "@opentui/core": ["@opentui/core@0.5.10", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.10", "@opentui/core-darwin-x64": "0.5.10", "@opentui/core-linux-arm64": "0.5.10", "@opentui/core-linux-arm64-musl": "0.5.10", "@opentui/core-linux-x64": "0.5.10", "@opentui/core-linux-x64-musl": "0.5.10", "@opentui/core-win32-arm64": "0.5.10", "@opentui/core-win32-x64": "0.5.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-C3a2UbmefeAjIxAgm4BqjuSxKT4oqutfvYFwVvUgMxmGRHkNbBc/s7sukV0JgwcxFcV3uMFrXxo+E+BQtvuOiw=="], + "@opentui/core": ["@opentui/core@0.5.11", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.11", "@opentui/core-darwin-x64": "0.5.11", "@opentui/core-linux-arm64": "0.5.11", "@opentui/core-linux-arm64-musl": "0.5.11", "@opentui/core-linux-x64": "0.5.11", "@opentui/core-linux-x64-musl": "0.5.11", "@opentui/core-win32-arm64": "0.5.11", "@opentui/core-win32-x64": "0.5.11" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-pImMfjCNx7JUp9Df1LRZBDLisWqgzOLXZKXO+hh3jA9ujBFPQnZOqbG+/N5uVAAX8IZGaqhYSQRwSZIFBmcfbQ=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Vyb+nTbhab8ZcRy5gg1loEEGwRcIbjAeVRIBfHBcbFDqmITBOg7x2gqJ+x/TnoOy4uwMhCmICUN2wiyREw3r1Q=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DRXY5ioq+n1ZNAMAcaFaBunr0cmi2gqucjbTW7lgFp8t9uN3fNZnLTDOqkCTQtT2XhrU4GXqySaPSQ4qFG/EAQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tTFLcM7Oj1gTyhm/bUdAt3C6grZdCxPk6+/g2azcZBUlI3/62LwbeRS6HbQKFFmm+1fUmX8cq6kWrtul885mVg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-yP/8GliJDiJNm8YYJKvgWuy6xyCEd8d4GwBVOIzCFOI7ZIbGP8GOTvmwIjRW4Paw20pTttWMWyRoQiCvOXvH2g=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-ncJXcgudhBf2GdJyF3xVQN/Ec+1F7GOL+pRrURmgBYSj2v1w6EyoDQFAACtPTK2c3R38W6fvZwL4JSLlm4EFXQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-zBIsRFHlLUYFNhapRSNt9dz4mC8gZ4Wxcfy3A+2AwqsgCipcr2FkIuAXYqN08q+IvqFX7DfqgIFGWDNedHTPUg=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-dGMphDKexSdeYqwl0wgoFBP88Ta/cdi1Zc1mk29/ENkSCGz+74zlCHgqTHRNGLmI8W5TfuUtCyktQH11/Z+TBQ=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-x+xeR2LYibvIi/qQetRjJR008sFRve60QuDcO8ItxUwzFeKTDzl5CEiZpBXfm5I4FhRNuyuj0TSPIFadMvrjFQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-5qtYaOgwVycZD1GaGshTRsi0rXPAmVExO03N1JQaHu+NYxK/vXSOc7Bu4QW0sPXx3Sp0SpzpP+FHjXABfoK66g=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-pSOXqOADrv+zINOgR3FDFA9zVRaim3zl8/yhtO+X9rEJ6f34z3gDund0Gf88hNJSMpZK5xWtipEVm28RY5VF8w=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Oj4H9hApuvuTKPWxh4SoZAgGJorR7vbvnrZA/cAkSMAk2VGSoHRRcqeXQbcH8IcdjVZ0KFpv8Zkl/D5Ye+2mew=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-MyqOnSs8pTYG2xmFr1xt6xZIuHu2Xu4pkle9my9JdE+WClmusHf0YN9Eas6jQLAq5XUi34r22wMeK0juk93zyw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-A9VhgvTxQoUdZ+8LmUumEng1sQNbj9QQQT3NYG9mSxI54qTANi7vOWNSphMiY6RMVsr22pgm6nUvSSvJXv7Jog=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-MGGRXIDJ//HyaqC5ndSr7/CUl+ICdYEAMcjbA00UWsthh4ZO/rYhPkyqOaTn5ck6+G4ca3/+M76J+jImMYTjNg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-sMEGX9rhiPd1gBa190jzj5uIdzKCMImxHmr22hJLxIRWqejxWvqGnRBkW68wT8NfMViDiMtZWavgWL7GgmYCAQ=="], - "@opentui/keymap": ["@opentui/keymap@0.5.10", "", { "dependencies": { "@opentui/core": "0.5.10" }, "peerDependencies": { "@opentui/react": "0.5.10", "@opentui/solid": "0.5.10", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-8vDJF+ltXscSnLEv3rgCa4m7PcoYZeUT9BngugpFCmVoNevbaRtYijjdfiUuLmXfT61lO5QbR6nEhn2RZMK8ow=="], + "@opentui/keymap": ["@opentui/keymap@0.5.11", "", { "dependencies": { "@opentui/core": "0.5.11" }, "peerDependencies": { "@opentui/react": "0.5.11", "@opentui/solid": "0.5.11", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-2c7Qsi2H5tg2ODxf1uOhEEao6JsaBAZRQ43z7nmHdtX5XK5VTNPgScZKBGaYTx/zBIaq/MkCCgPpr/m5wHdssw=="], - "@opentui/solid": ["@opentui/solid@0.5.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-KrmMIsHiKBHOABTC0brOwqWm+sGq1ZX2sGCAx6WgtBbE3STMup9n8TAy/6gUYhwcjC9zugT53ytfSVwCwVWZUg=="], + "@opentui/solid": ["@opentui/solid@0.5.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.11", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-u8RJ4UMwzi+r9M1sopZdnT+7XFSHmLzVmhPhc6N2LfBrA9Db4lZ9sQA+ywgdSDkzdANpAOwGLNUQlY/F1OOCLA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], - "@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="], + "@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="], "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], @@ -154,15 +165,15 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="], "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], - "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + "browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="], "bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="], - "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], @@ -180,7 +191,7 @@ "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="], - "electron-to-chromium": ["electron-to-chromium@1.5.420", "", {}, "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.425", "", {}, "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -248,7 +259,9 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], + "node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="], + + "opencode-context-tree": ["opencode-context-tree@workspace:packages/context-tree"], "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], @@ -300,7 +313,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="], diff --git a/package.json b/package.json index 3206e1a..9ea737a 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,11 @@ { - "name": "opencode-context-tree", - "version": "0.0.0-dev", - "private": false, - "type": "module", - "description": "Pi-style context tree for OpenCode: branch, merge (human-confirmed decision records), crop, undo, plus a DeepSeek-Harness-style trajectory view", - "license": "MIT", - "exports": { - ".": { - "types": "./src/server/index.ts", - "import": "./dist/server.js" - }, - "./server": { - "types": "./src/server/index.ts", - "import": "./dist/server.js" - }, - "./tui": { - "types": "./src/tui/index.tsx", - "import": "./dist/tui.js" - } - }, - "files": [ - "dist", - "src", - "README.md", - "LICENSE", - "docs/USAGE.md", - "CHANGELOG.md" - ], + "name": "opencode-tree-monorepo", + "private": true, + "workspaces": ["packages/*"], "scripts": { - "build": "bun run scripts/build.ts", - "prepack": "bun run build", - "typecheck": "bunx tsc --noEmit -p tsconfig.json", - "test": "bun test", - "test:e2e": "CTREE_E2E=1 bun test --timeout 120000 test/e2e" - }, - "engines": { - "opencode": ">=1.18.0" - }, - "dependencies": { - "@opencode-ai/plugin": "1.18.26", - "@opencode-ai/sdk": "1.18.26", - "zod": "^4.0.0" + "build": "for d in packages/*/; do (cd \"$d\" && bun run build) || exit 1; done", + "typecheck": "for d in packages/*/; do (cd \"$d\" && bun run typecheck) || exit 1; done", + "test": "bun test" }, "devDependencies": { "@opentui/core": "^0.5.10", @@ -49,45 +14,5 @@ "@types/bun": "latest", "solid-js": "^1.9.12", "typescript": "^5.8.2" - }, - "peerDependencies": { - "@opentui/core": ">=0.5.10", - "@opentui/keymap": ">=0.5.10", - "@opentui/solid": ">=0.5.10", - "solid-js": ">=1.9.12" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/keymap": { - "optional": true - }, - "@opentui/solid": { - "optional": true - }, - "solid-js": { - "optional": true - } - }, - "author": "Naveen (navbytes)", - "repository": { - "type": "git", - "url": "git+https://github.com/navbytes/opencode-tree.git" - }, - "homepage": "https://github.com/navbytes/opencode-tree#readme", - "keywords": [ - "opencode", - "opencode-plugin", - "context-tree", - "context-engineering", - "branch", - "merge", - "crop", - "trajectory", - "tui" - ], - "publishConfig": { - "access": "public" } } diff --git a/CHANGELOG.md b/packages/context-tree/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/context-tree/CHANGELOG.md diff --git a/DESIGN.md b/packages/context-tree/DESIGN.md similarity index 100% rename from DESIGN.md rename to packages/context-tree/DESIGN.md diff --git a/packages/context-tree/LICENSE b/packages/context-tree/LICENSE new file mode 100644 index 0000000..3076d67 --- /dev/null +++ b/packages/context-tree/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Naveen (navbytes) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/context-tree/README.md b/packages/context-tree/README.md new file mode 100644 index 0000000..298c4e3 --- /dev/null +++ b/packages/context-tree/README.md @@ -0,0 +1,369 @@ +
+ +# opencode-context-tree + +**See your whole [OpenCode](https://opencode.ai) session as a tree — then branch it, merge it, and crop it like source code.** + +A Pi-style context tree with git-style controls (`/branch`, `/merge`, `/crop`, `/undo`) +and a DeepSeek-Harness-style trajectory view, in one screen, inside your terminal. + +[![npm](https://img.shields.io/npm/v/opencode-context-tree?color=cb3837&logo=npm)](https://www.npmjs.com/package/opencode-context-tree) +[![CI](https://github.com/navbytes/opencode-tree/actions/workflows/ci.yml/badge.svg)](https://github.com/navbytes/opencode-tree/actions/workflows/ci.yml) +[![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +[![OpenCode](https://img.shields.io/badge/OpenCode-%E2%89%A5%201.18-black)](https://opencode.ai) + +
+ +![The context tree, viewed from the trunk](docs/screenshots/tree-trunk.png) + +## Why + +A long OpenCode session is a straight line that only grows. You cannot see what is +filling the context window, side quests are stuck in the transcript forever, and +auto-compaction eventually decides for you what gets forgotten. + +This plugin gives that session a shape and a set of controls: + +- **See it.** One screen shows every message and tool step in the session, with the + branches drawn at the points they forked from, and what each one costs in tokens. +- **Branch it.** Take a side quest onto its own branch — optionally on a cheaper model — + so the main thread never sees the noise. +- **Merge it.** Close a branch with a decision record you confirm yourself, so the trunk + gets the conclusion instead of the twenty turns that produced it. +- **Crop it.** Stub the 40k-token `bash` output that is squatting in your context, and + put it back with one key when you were wrong. + +The screen stays close to Pi and to +[`pi-context-tree`](https://github.com/navbytes/pi-context-tree), so it is familiar if you +are coming from either, and the trajectory view follows the DeepSeek Harness. + +> **Your transcript is never rewritten.** Crops are applied per request, so the model +> sees a stub while the stored message keeps its full text. Merges *append* a record; +> they never delete turns. Every branch, merge and crop is undoable with `u`. + +## Contents + +- [Install](#install) +- [Quick start](#quick-start) +- [The screen](#the-screen) +- [Features](#features) +- [Keys](#keys) +- [Commands](#commands) +- [Configuration](#configuration) +- [How it maps onto OpenCode](#how-it-maps-onto-opencode) +- [Performance](#performance) +- [Troubleshooting](#troubleshooting) +- [Development](#development) +- [License](#license) + +## Install + +Requires **OpenCode 1.18 or newer**. One command registers both halves of the plugin: + +```sh +opencode plugin opencode-context-tree -g # every project (~/.config/opencode) +opencode plugin opencode-context-tree # this project only (.opencode/) +``` + +Restart OpenCode, then press `ctrl+q` or run `/tree`. + +
+Registering by hand + +The plugin has two halves, and the package name must be listed in **both** config files. +`opencode.json` loads the server half (crops, branch model, headless `/ctree` commands); +`tui.json` loads the TUI half (`/tree`, `/branch`, `/merge`, the gauge, the sidebar card). +Listing it in only the first gives you `/ctree` but no `/tree`. + +```jsonc +// opencode.json (or ~/.config/opencode/opencode.jsonc) +{ "plugin": ["opencode-context-tree"] } + +// tui.json (or ~/.config/opencode/tui.json) +{ "plugin": ["opencode-context-tree"] } +``` + +
+ +
+Upgrading + +OpenCode caches the version it installed and does not re-resolve `@latest` on restart. +Pin the new release, which also rewrites both config entries: + +```sh +opencode plugin opencode-context-tree@ -g --force # drop -g for this project only +``` + +The npm badge at the top of this page is the current version. Or delete `~/.cache/opencode/packages/opencode-context-tree@latest` and restart. +`?` inside `/tree` and `/ctree status` both print the version you are running. + +
+ +## Quick start + +The whole workflow is four moves: + +``` +/branch fix-flaky name it → you are on a real OpenCode session, forked here + …side quest… the noisy turns live on the branch, not in your main thread +/merge Squash → the model drafts a ◆ decision record → your $EDITOR → + save to confirm → the conclusion lands on the trunk as one message +/tree see where you are, what it costs, and jump anywhere +c space ⏎ crop a fat tool result — the model sees "[cropped: bash …]" from + the next turn; u puts it back +``` + +Pressing `⏎` on any earlier message forks from that point. This is Pi's fork flow whole: +one question with Pi's three answers — fork clean, summarize everything below that point, +or summarize it with your own prompt — and the answer is also the confirmation. The +summary covers exactly the turns the move leaves behind. + +Every turn but the one you are in folds to a single row, so the outline reads as an outline +rather than a wall of tool calls: + +``` +● ▸6 T5 add a retry to the flaky test 1✗ 2⚠ ~12k +● T6 now make it pass on CI ~310 +``` + +`▸6` between the `●` and the text is the fold: the row's own disclosure control, at the row's +own left edge, saying it stands for six hidden rows. The token column already counts them. + +`za` folds or opens the turn you are on, `l` opens the folded one under the cursor (vim opens +a fold on a horizontal move), `zm` folds them all, `zr` opens them all — vim's own fold keys. +And you do not have to know them: the row the cursor is on names the key for the one thing it +affords, and only that row. Nothing is lost either — the marker counts what is inside, the +timeline still shows every event (a folded turn lights the whole span it stands for), and crop +mode opens everything while you pick targets. + +Drafting one takes a model call, so the status line shows it happening — +`⠹ summarizing 3 turns · ~14k · Progress · 1.2k chars · 4s · esc cancels`: the step, the +draft as it streams in, how long it has been, and the way out. The `◆` record a `/merge` +drafts reports the same way. + +## The screen + +`/tree` is an outline of the whole session. Every message and tool call is one +content-forward row, branches hang off the message they were forked from, and your +current branch is open while the rest stay folded: + +``` +┌ Context tree · Fix flaky test · trunk ctx ~46k/200k · filling +│ filter: default 24 rows +│ ● user: build yourself a tool that reads the context window… ~1.2k +│ ○ assistant: I'll start by inspecting my environment… 0.3k +│ ⚙ [bash $ ls -la ~/Documents/] → total 744 … ~2.1k +│ ● user: decompress the session and show the structure ~0.2k +│ ╰⎇ try-redis ▸ squashed · 9 turns ~22k +│ ╰⎇ fix-flaky ▾ open · 6 turns ← here ~14k +│ │ ● user: the bun test is flaky, find the race ~0.4k +│ │ ⚙ [bash $ bun test src/foo.test.ts] ⚠ ~4.7k +│ ◆ Decision: try-redis · Outcome: switched to a write-through cache… ~0.9k +└ ⏎ go b branch m merge c crop u undo s consumers ? help q back +``` + +The footer always says what `⏎` will do for the row under the cursor. A leading `~` on a +token count means it is estimated; assistant steps use the model's own numbers. +Markers: `⚠` over 10k tokens · `✂` cropped · `✗` tool error · `◆` decision record. + +## Features + +### Branches you can see + +From inside a branch, `← here` marks your position. Trunk rows past the fork point are +dimmed under `── not in this branch's context ──`, because the model is not sent them. + +![The tree, viewed from inside a branch](docs/screenshots/tree-from-a-branch.png) + +Sessions you create with OpenCode's own `/fork` are adopted into the tree automatically. + +### Search and filters + +`/` filters as you type, highlights the matches and counts the rows; `n` and `N` step +through them. `f` opens a filter picker — default, no-tools, user-only, labeled, all — and +the timeline lanes follow it, so `tools-only` becomes a "what did I run" view in both. + +![Live search inside the tree](docs/screenshots/search.png) + +### The trajectory view + +The DeepSeek-Harness trajectory is one keystroke away rather than in your way. `1` and +`2` bring in the Input / Model / Tools lanes — one pill per event, coloured by lane and red +for a failed tool call — laid out either by duration (`1`) or one cell per event (`2`). +`0` hides them again. `i` opens the inspector, with each step's payload, result and timing. + +The lanes appear once the session has three turns to plot, and on a long session they show +a window of the timeline that follows your cursor. + +![The tree with trajectory lanes and the inspector open](docs/screenshots/tree-trajectory.png) + +### Finding what fills your context + +`s` breaks the context down by share of the tree and of the model window, expandable into +entries you can crop in place. It counts the system prompt too, broken down by part, so +you can see what your `AGENTS.md` actually costs. `D` renders the decision records. + +| | | +|---|---| +| ![The consumers view](docs/screenshots/consumers.png) | ![The decisions panel](docs/screenshots/decisions.png) | +| ![The help pane](docs/screenshots/help.png) | ![The merge picker](docs/screenshots/merge-picker.png) | + +### The gauge + +On the prompt line: the context of your next prompt (the same figure OpenCode's own +sidebar shows), its band, and how much of it the provider served from cache. The bar's +dim cells are the cached part. `0% cached` right after a crop, merge or fork means the +cache was reset. + +![The context gauge showing the provider cache share](docs/screenshots/gauge-cache.png) + +## Keys + +Vim-aligned, inside `/tree`: a key means here what it means in vim, and the verbs vim has no +word for live behind `g` the way LSP plugins put theirs (`gd`, `gr`, `gi`). Press `?` for the +full list without leaving the screen. Every key is rebindable — see `keybinds` in +[Configuration](#configuration). + +| Key | Action | +|---|---| +| `j` `k` · `ctrl+f` `ctrl+b` · `ctrl+d` `ctrl+u` · `gg` `G` | move · page · half page · top / bottom | +| `{` `}` | previous / next turn row — the outline's own unit; the lanes scrub with it | +| `[[` `]]` (or `[` `]`) | previous / next branch row | +| `h` `l` · `Tab` | fold / unfold a branch inline (`l` also opens a folded turn) | +| `za` · `zo` `zc` | fold / open / close the turn you are on | +| `zr` `zm` · `zj` `zk` | open every fold / fold every turn · move between folds | +| `H` `M` `L` | top / middle / bottom of the screen | +| `⏎` | go here — the footer names what it will do for this row | +| `gb` | branch here, naming it and optionally picking a model | +| `gm` | merge: squash, squash without the model, discard, or tournament | +| `c` | crop mode — `space` mark, `a` auto-mark, `t` result⇄turn, `⏎` apply | +| `u` | undo the last branch / merge / crop | +| `m` | mark: label the selected message | +| `/` · `n` `N` | live search · next / previous match | +| `gf` | filter picker | +| `i` `I` | inspector in the side pane / full screen (`PgUp` `PgDn` to page) | +| `g1` `g2` · `g0` | timeline lanes, x-axis by duration / one cell per event · off | +| `gs` | what is filling the context | +| `gd` `ge` | decisions panel · export to `ctree-decisions.md` | +| `y` | copy the selected text | +| `?` `q` | help · back | + +## Commands + +Not sure which one you want? [**Choosing what to do**](docs/USAGE.md#choosing-what-to-do) +compares them: what each preserves, what each costs, and what to press at 80% context. + +| Command | What it does | When | +|---|---|---| +| `/tree` (`ctrl+q`) | open the combined tree and trajectory view | [the loop](docs/USAGE.md#the-loop) | +| `/branch [model]` | fork here into a named branch, optionally on a cheaper model | [before the risky thing](docs/USAGE.md#branch--try-it-on-a-copy) | +| `/merge [--pick \| --no-llm \| --discard \| --tournament]` | close the branch — see below | [when a branch has finished](docs/USAGE.md#merge--keep-the-conclusion-not-the-noise) | +| `/crop [--top \| --auto …]` | stub fat tool results or drop whole turns from what the model sees | [when you can name the fat thing](docs/USAGE.md#crop--stop-sending-something-you-no-longer-need) | +| `/undo` | revert the last branch, merge or crop | [what it does and does not cover](docs/USAGE.md#summarize-on-a-jump--carry-the-gist-back) | +| `/decisions [--export]` | list or export decision records | — | + +`/merge` offers four ways to close a branch. **Squash** has the branch model draft a ◆ +decision record that you confirm in `$EDITOR`. **Squash without LLM** hands you the empty +template to write yourself. **Discard** lands nothing. **Tournament** keeps one of several +sibling branches. In every case the record is appended to the trunk as a normal message. + +For desktop, web or scripts there is a headless equivalent that needs no TUI: +`/ctree status`, `/ctree branch`, `/ctree merge --discard`, `/ctree crop`, `/ctree undo` +and `/ctree decisions`. See [docs/USAGE.md](docs/USAGE.md#headless-desktop--web--scripts). + +## Configuration + +Options go in the plugin entry of either config file. Both halves read `storage`, so if +you change it, **set the same value in both files** — otherwise the TUI and the server +keep two different journals and crops written by one never reach the other. + +```jsonc +{ "plugin": [["opencode-context-tree", { "storage": "global", "jumpSummary": "never" }]] } +``` + +| Option | Values | Default | What it does | +|---|---|---|---| +| `storage` | `"local"` · `"global"` | `"local"` | where the journal lives — `.opencode/context-tree/` in the worktree (gitignored), or OpenCode's state dir | +| `jumpSummary` | `"ask"` · `"never"` | `"ask"` | whether jumping offers to summarize the turns you leave behind | +| `hardCrop` | `true` · `false` | `false` | also set OpenCode's own "compacted" flag on cropped parts, so the transcript itself shows them cleared — reversible, but it touches OpenCode storage | +| `keybinds` | object | — | override any key by command name, e.g. `{ "open": "ctrl+t", "copy": "none" }` | + +The full list of rebindable command names is in +[docs/USAGE.md](docs/USAGE.md#install). + +## How it maps onto OpenCode + +Nothing here is a private data format bolted on the side. Each feature is one of +OpenCode's own primitives: + +| Feature | Implementation | +|---|---| +| **branch** | a real OpenCode session created with `session.fork`; the plugin records `(parent, anchor)` in an append-only journal and mirrors it into `session.metadata` | +| **crop** | applied per request in `experimental.chat.messages.transform`, so the transcript keeps the originals and only the model sees stubs | +| **merge** | writes the confirmed record with `session.prompt({ noReply: true })` | +| **the UI** | a TUI plugin (`@opencode-ai/plugin/tui`): one route, two slots (gauge, sidebar card), dialogs and a keymap layer | + +Because a branch is just a session and a decision record is just a message, everything +stays readable to OpenCode — and to you — if you ever remove the plugin. + +## Performance + +Measured against OpenCode 1.18.26 on a 120-column pty. Full method and the rest of the +numbers are in [docs/USAGE.md](docs/USAGE.md#performance). + +| Session | `/tree` opens in | Search | Consumers | TUI memory | +|---|---|---|---|---| +| 57 messages | 37 ms | 2 ms | 4 ms | — | +| 117 messages | 34 ms | 2 ms | 3 ms | 37 MB | +| 467 messages | 32 ms | 1 ms | 4 ms | 37 MB | + +Opening the tree does not get slower as the session grows. The crop transform that runs on +every model request costs 0.78 ms at 484 messages with 66 active crops, and 0.03–0.27 ms on +a 50-message session. Startup to the prompt grows by about 70 ms. + +## Troubleshooting + +**`/ctree` works but `/tree` does not exist.** The TUI half is not registered. Run the +install command again, or add the package name to `tui.json` as well as `opencode.json`. + +**A crop or a branch is missing from one half.** The two halves are reading different +journals. Set the same `storage` value in both config files. + +**A turn looks stuck.** `/ctree` subcommands are dispatched as turns, so they queue behind +one that is already running. `/tree` opens synchronously from the local journal, so reach +for that one instead. + +**An upgrade did not take.** OpenCode kept the cached version. Pin the release with +`--force`, as described under [Install](#install). + +## Development + +```sh +bun install +bun run build +bun test # unit tests +bun run typecheck +bun run test:e2e # pty-driven end-to-end tests against a real OpenCode TUI +``` + +To run your checkout instead of the published package, list the built files by absolute +path rather than the package name: + +```jsonc +// opencode.json → "plugin": ["/abs/path/opencode-tree/packages/context-tree/dist/server.js"] +// tui.json → "plugin": ["/abs/path/opencode-tree/packages/context-tree/dist/tui.js"] +``` + +[DESIGN.md](./DESIGN.md) is the long version: the research behind the design (Pi, +[`pi-context-tree`](https://github.com/navbytes/pi-context-tree), the OpenCode plugin and +SDK surface, the DeepSeek Harness trajectory view), the end-user flows, the data model, +the architecture, the edge cases and the roadmap. [CHANGELOG.md](./CHANGELOG.md) records +what changed in each release. + +Issues and pull requests are welcome. Please run the typecheck and the unit tests before +opening one. + +## License + +[MIT](./LICENSE) © Naveen (navbytes) diff --git a/docs/M0.md b/packages/context-tree/docs/M0.md similarity index 100% rename from docs/M0.md rename to packages/context-tree/docs/M0.md diff --git a/docs/USAGE.md b/packages/context-tree/docs/USAGE.md similarity index 99% rename from docs/USAGE.md rename to packages/context-tree/docs/USAGE.md index 00ebfa6..84837fd 100644 --- a/docs/USAGE.md +++ b/packages/context-tree/docs/USAGE.md @@ -25,8 +25,8 @@ options only matter to the half that implements them (`storage` both; `jumpSumma `hardCrop`, `keybinds` TUI-only). Plain `{ "plugin": ["opencode-context-tree"] }` in both files is fine and uses the defaults. -From a checkout: `bun install && bun run build`, then list `/abs/path/dist/server.js` and -`/abs/path/dist/tui.js` instead of the package name. +From a checkout: `bun install && bun run build`, then list `/abs/path/packages/context-tree/dist/server.js` and +`/abs/path/packages/context-tree/dist/tui.js` instead of the package name. Options: `storage` `"local"` (default, `.opencode/context-tree/` in the worktree, gitignored) or `"global"` (OpenCode's state dir); `jumpSummary` `"ask"` (default, Pi behaviour) or `"never"`; diff --git a/docs/screenshots/consumers.png b/packages/context-tree/docs/screenshots/consumers.png similarity index 100% rename from docs/screenshots/consumers.png rename to packages/context-tree/docs/screenshots/consumers.png diff --git a/docs/screenshots/decisions.png b/packages/context-tree/docs/screenshots/decisions.png similarity index 100% rename from docs/screenshots/decisions.png rename to packages/context-tree/docs/screenshots/decisions.png diff --git a/docs/screenshots/gauge-cache.png b/packages/context-tree/docs/screenshots/gauge-cache.png similarity index 100% rename from docs/screenshots/gauge-cache.png rename to packages/context-tree/docs/screenshots/gauge-cache.png diff --git a/docs/screenshots/help.png b/packages/context-tree/docs/screenshots/help.png similarity index 100% rename from docs/screenshots/help.png rename to packages/context-tree/docs/screenshots/help.png diff --git a/docs/screenshots/merge-picker.png b/packages/context-tree/docs/screenshots/merge-picker.png similarity index 100% rename from docs/screenshots/merge-picker.png rename to packages/context-tree/docs/screenshots/merge-picker.png diff --git a/docs/screenshots/search.png b/packages/context-tree/docs/screenshots/search.png similarity index 100% rename from docs/screenshots/search.png rename to packages/context-tree/docs/screenshots/search.png diff --git a/docs/screenshots/tree-from-a-branch.png b/packages/context-tree/docs/screenshots/tree-from-a-branch.png similarity index 100% rename from docs/screenshots/tree-from-a-branch.png rename to packages/context-tree/docs/screenshots/tree-from-a-branch.png diff --git a/docs/screenshots/tree-trajectory.png b/packages/context-tree/docs/screenshots/tree-trajectory.png similarity index 100% rename from docs/screenshots/tree-trajectory.png rename to packages/context-tree/docs/screenshots/tree-trajectory.png diff --git a/docs/screenshots/tree-trunk.png b/packages/context-tree/docs/screenshots/tree-trunk.png similarity index 100% rename from docs/screenshots/tree-trunk.png rename to packages/context-tree/docs/screenshots/tree-trunk.png diff --git a/packages/context-tree/package.json b/packages/context-tree/package.json new file mode 100644 index 0000000..6723fb7 --- /dev/null +++ b/packages/context-tree/package.json @@ -0,0 +1,94 @@ +{ + "name": "opencode-context-tree", + "version": "0.0.0-dev", + "private": false, + "type": "module", + "description": "Pi-style context tree for OpenCode: branch, merge (human-confirmed decision records), crop, undo, plus a DeepSeek-Harness-style trajectory view", + "license": "MIT", + "exports": { + ".": { + "types": "./src/server/index.ts", + "import": "./dist/server.js" + }, + "./server": { + "types": "./src/server/index.ts", + "import": "./dist/server.js" + }, + "./tui": { + "types": "./src/tui/index.tsx", + "import": "./dist/tui.js" + } + }, + "files": [ + "dist", + "src", + "README.md", + "LICENSE", + "docs/USAGE.md", + "CHANGELOG.md" + ], + "scripts": { + "build": "bun run scripts/build.ts", + "prepack": "bun run build", + "typecheck": "bunx tsc --noEmit -p tsconfig.json", + "test": "bun test", + "test:e2e": "CTREE_E2E=1 bun test --timeout 120000 test/e2e" + }, + "engines": { + "opencode": ">=1.18.0" + }, + "dependencies": { + "@opencode-ai/plugin": "1.18.26", + "@opencode-ai/sdk": "1.18.26", + "zod": "^4.0.0" + }, + "devDependencies": { + "@opentui/core": "^0.5.10", + "@opentui/keymap": "^0.5.10", + "@opentui/solid": "^0.5.10", + "@types/bun": "latest", + "solid-js": "^1.9.12", + "typescript": "^5.8.2" + }, + "peerDependencies": { + "@opentui/core": ">=0.5.10", + "@opentui/keymap": ">=0.5.10", + "@opentui/solid": ">=0.5.10", + "solid-js": ">=1.9.12" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + }, + "solid-js": { + "optional": true + } + }, + "author": "Naveen (navbytes)", + "repository": { + "type": "git", + "url": "git+https://github.com/navbytes/opencode-tree.git", + "directory": "packages/context-tree" + }, + "homepage": "https://github.com/navbytes/opencode-tree#readme", + "keywords": [ + "opencode", + "opencode-plugin", + "context-tree", + "context-engineering", + "branch", + "merge", + "crop", + "trajectory", + "tui" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/scripts/build.ts b/packages/context-tree/scripts/build.ts similarity index 100% rename from scripts/build.ts rename to packages/context-tree/scripts/build.ts diff --git a/src/core/actions.ts b/packages/context-tree/src/core/actions.ts similarity index 100% rename from src/core/actions.ts rename to packages/context-tree/src/core/actions.ts diff --git a/src/core/adopt.ts b/packages/context-tree/src/core/adopt.ts similarity index 100% rename from src/core/adopt.ts rename to packages/context-tree/src/core/adopt.ts diff --git a/src/core/consumers.ts b/packages/context-tree/src/core/consumers.ts similarity index 100% rename from src/core/consumers.ts rename to packages/context-tree/src/core/consumers.ts diff --git a/src/core/crop.ts b/packages/context-tree/src/core/crop.ts similarity index 100% rename from src/core/crop.ts rename to packages/context-tree/src/core/crop.ts diff --git a/src/core/cropplan.ts b/packages/context-tree/src/core/cropplan.ts similarity index 100% rename from src/core/cropplan.ts rename to packages/context-tree/src/core/cropplan.ts diff --git a/src/core/ctree-args.ts b/packages/context-tree/src/core/ctree-args.ts similarity index 100% rename from src/core/ctree-args.ts rename to packages/context-tree/src/core/ctree-args.ts diff --git a/src/core/decision.ts b/packages/context-tree/src/core/decision.ts similarity index 100% rename from src/core/decision.ts rename to packages/context-tree/src/core/decision.ts diff --git a/src/core/fold.ts b/packages/context-tree/src/core/fold.ts similarity index 100% rename from src/core/fold.ts rename to packages/context-tree/src/core/fold.ts diff --git a/src/core/gauge.ts b/packages/context-tree/src/core/gauge.ts similarity index 100% rename from src/core/gauge.ts rename to packages/context-tree/src/core/gauge.ts diff --git a/src/core/help.ts b/packages/context-tree/src/core/help.ts similarity index 100% rename from src/core/help.ts rename to packages/context-tree/src/core/help.ts diff --git a/src/core/journal.ts b/packages/context-tree/src/core/journal.ts similarity index 100% rename from src/core/journal.ts rename to packages/context-tree/src/core/journal.ts diff --git a/src/core/lanes.ts b/packages/context-tree/src/core/lanes.ts similarity index 100% rename from src/core/lanes.ts rename to packages/context-tree/src/core/lanes.ts diff --git a/src/core/navigation.ts b/packages/context-tree/src/core/navigation.ts similarity index 100% rename from src/core/navigation.ts rename to packages/context-tree/src/core/navigation.ts diff --git a/src/core/progress.ts b/packages/context-tree/src/core/progress.ts similarity index 100% rename from src/core/progress.ts rename to packages/context-tree/src/core/progress.ts diff --git a/src/core/tokens.ts b/packages/context-tree/src/core/tokens.ts similarity index 100% rename from src/core/tokens.ts rename to packages/context-tree/src/core/tokens.ts diff --git a/src/core/transcript.ts b/packages/context-tree/src/core/transcript.ts similarity index 100% rename from src/core/transcript.ts rename to packages/context-tree/src/core/transcript.ts diff --git a/src/core/tree.ts b/packages/context-tree/src/core/tree.ts similarity index 100% rename from src/core/tree.ts rename to packages/context-tree/src/core/tree.ts diff --git a/src/core/undo.ts b/packages/context-tree/src/core/undo.ts similarity index 100% rename from src/core/undo.ts rename to packages/context-tree/src/core/undo.ts diff --git a/src/server/index.ts b/packages/context-tree/src/server/index.ts similarity index 100% rename from src/server/index.ts rename to packages/context-tree/src/server/index.ts diff --git a/src/shared/adopt.ts b/packages/context-tree/src/shared/adopt.ts similarity index 100% rename from src/shared/adopt.ts rename to packages/context-tree/src/shared/adopt.ts diff --git a/src/shared/debug.ts b/packages/context-tree/src/shared/debug.ts similarity index 100% rename from src/shared/debug.ts rename to packages/context-tree/src/shared/debug.ts diff --git a/src/shared/sdk.ts b/packages/context-tree/src/shared/sdk.ts similarity index 100% rename from src/shared/sdk.ts rename to packages/context-tree/src/shared/sdk.ts diff --git a/src/shared/store.ts b/packages/context-tree/src/shared/store.ts similarity index 100% rename from src/shared/store.ts rename to packages/context-tree/src/shared/store.ts diff --git a/src/shared/version.ts b/packages/context-tree/src/shared/version.ts similarity index 100% rename from src/shared/version.ts rename to packages/context-tree/src/shared/version.ts diff --git a/src/tui/actions.ts b/packages/context-tree/src/tui/actions.ts similarity index 100% rename from src/tui/actions.ts rename to packages/context-tree/src/tui/actions.ts diff --git a/src/tui/editor.ts b/packages/context-tree/src/tui/editor.ts similarity index 100% rename from src/tui/editor.ts rename to packages/context-tree/src/tui/editor.ts diff --git a/src/tui/gauge.tsx b/packages/context-tree/src/tui/gauge.tsx similarity index 100% rename from src/tui/gauge.tsx rename to packages/context-tree/src/tui/gauge.tsx diff --git a/src/tui/index.tsx b/packages/context-tree/src/tui/index.tsx similarity index 100% rename from src/tui/index.tsx rename to packages/context-tree/src/tui/index.tsx diff --git a/src/tui/route.tsx b/packages/context-tree/src/tui/route.tsx similarity index 100% rename from src/tui/route.tsx rename to packages/context-tree/src/tui/route.tsx diff --git a/src/tui/transcripts.ts b/packages/context-tree/src/tui/transcripts.ts similarity index 100% rename from src/tui/transcripts.ts rename to packages/context-tree/src/tui/transcripts.ts diff --git a/test/abandoned.test.ts b/packages/context-tree/test/abandoned.test.ts similarity index 100% rename from test/abandoned.test.ts rename to packages/context-tree/test/abandoned.test.ts diff --git a/test/actions.test.ts b/packages/context-tree/test/actions.test.ts similarity index 100% rename from test/actions.test.ts rename to packages/context-tree/test/actions.test.ts diff --git a/test/adopt.test.ts b/packages/context-tree/test/adopt.test.ts similarity index 100% rename from test/adopt.test.ts rename to packages/context-tree/test/adopt.test.ts diff --git a/test/consumers.test.ts b/packages/context-tree/test/consumers.test.ts similarity index 100% rename from test/consumers.test.ts rename to packages/context-tree/test/consumers.test.ts diff --git a/test/core-purity.test.ts b/packages/context-tree/test/core-purity.test.ts similarity index 100% rename from test/core-purity.test.ts rename to packages/context-tree/test/core-purity.test.ts diff --git a/test/crop.test.ts b/packages/context-tree/test/crop.test.ts similarity index 100% rename from test/crop.test.ts rename to packages/context-tree/test/crop.test.ts diff --git a/test/cropplan.test.ts b/packages/context-tree/test/cropplan.test.ts similarity index 100% rename from test/cropplan.test.ts rename to packages/context-tree/test/cropplan.test.ts diff --git a/test/ctree-args.test.ts b/packages/context-tree/test/ctree-args.test.ts similarity index 100% rename from test/ctree-args.test.ts rename to packages/context-tree/test/ctree-args.test.ts diff --git a/test/decision.test.ts b/packages/context-tree/test/decision.test.ts similarity index 100% rename from test/decision.test.ts rename to packages/context-tree/test/decision.test.ts diff --git a/test/docs-links.test.ts b/packages/context-tree/test/docs-links.test.ts similarity index 100% rename from test/docs-links.test.ts rename to packages/context-tree/test/docs-links.test.ts diff --git a/test/e2e/harness.ts b/packages/context-tree/test/e2e/harness.ts similarity index 98% rename from test/e2e/harness.ts rename to packages/context-tree/test/e2e/harness.ts index 71c3013..949019d 100644 --- a/test/e2e/harness.ts +++ b/packages/context-tree/test/e2e/harness.ts @@ -13,9 +13,12 @@ import { tmpdir } from "node:os" import path from "node:path" import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk" -export const REPO_ROOT = path.resolve(import.meta.dir, "../..") +export const REPO_ROOT = path.resolve(import.meta.dir, "../../../..") export const HARNESS_DIR = path.join(REPO_ROOT, "harness") export const TEMPLATE_PROJECT_DIR = path.join(HARNESS_DIR, "project") +// Where this package's own scripts/build.ts and dist/ live — distinct from REPO_ROOT +// now that the package is nested under packages/context-tree. +export const PACKAGE_DIR = path.resolve(import.meta.dir, "../..") function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/test/e2e/server.test.ts b/packages/context-tree/test/e2e/server.test.ts similarity index 98% rename from test/e2e/server.test.ts rename to packages/context-tree/test/e2e/server.test.ts index 1467750..95460d0 100644 --- a/test/e2e/server.test.ts +++ b/packages/context-tree/test/e2e/server.test.ts @@ -15,7 +15,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test" import { cp, mkdir } from "node:fs/promises" import path from "node:path" import { existsSync, readFileSync, readdirSync } from "node:fs" -import { createProject, startMock, startServer, TEMPLATE_PROJECT_DIR, type StartedMock, type StartedServer, REPO_ROOT, installPlugins } from "./harness.js" +import { createProject, startMock, startServer, TEMPLATE_PROJECT_DIR, type StartedMock, type StartedServer, PACKAGE_DIR, installPlugins } from "./harness.js" const e2e = process.env.CTREE_E2E === "1" @@ -231,13 +231,13 @@ describe.skipIf(!e2e)("server e2e: built plugin headless /ctree commands", () => let dir: string beforeAll(async () => { - const build = Bun.spawnSync({ cmd: ["bun", "run", "scripts/build.ts"], cwd: REPO_ROOT, stdio: ["ignore", "pipe", "pipe"] }) + const build = Bun.spawnSync({ cmd: ["bun", "run", "scripts/build.ts"], cwd: PACKAGE_DIR, stdio: ["ignore", "pipe", "pipe"] }) if (build.exitCode !== 0) throw new Error(`build failed: ${build.stderr.toString()}`) mock = await startMock({ tool: true }) const project = await createProject({ mockPort: mock.port }) cleanupProject = project.cleanup dir = project.dir - await installPlugins({ projectDir: project.dir, server: [path.join(REPO_ROOT, "dist", "server.js")] }) + await installPlugins({ projectDir: project.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")] }) server = await startServer({ projectDir: project.dir }) }) @@ -374,13 +374,13 @@ describe.skipIf(!e2e)('server e2e: storage "global" option', () => { let dir: string beforeAll(async () => { - const build = Bun.spawnSync({ cmd: ["bun", "run", "scripts/build.ts"], cwd: REPO_ROOT, stdio: ["ignore", "pipe", "pipe"] }) + const build = Bun.spawnSync({ cmd: ["bun", "run", "scripts/build.ts"], cwd: PACKAGE_DIR, stdio: ["ignore", "pipe", "pipe"] }) if (build.exitCode !== 0) throw new Error(`build failed: ${build.stderr.toString()}`) mock = await startMock({ tool: false }) const project = await createProject({ mockPort: mock.port }) cleanupProject = project.cleanup dir = project.dir - await installPlugins({ projectDir: project.dir, server: [[path.join(REPO_ROOT, "dist", "server.js"), { storage: "global" }]] }) + await installPlugins({ projectDir: project.dir, server: [[path.join(PACKAGE_DIR, "dist", "server.js"), { storage: "global" }]] }) server = await startServer({ projectDir: project.dir }) }) diff --git a/test/e2e/tui.test.ts b/packages/context-tree/test/e2e/tui.test.ts similarity index 95% rename from test/e2e/tui.test.ts rename to packages/context-tree/test/e2e/tui.test.ts index 3ff26f9..8cb8487 100644 --- a/test/e2e/tui.test.ts +++ b/packages/context-tree/test/e2e/tui.test.ts @@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test" import path from "node:path" import { tmpdir } from "node:os" import { existsSync, readFileSync, readdirSync } from "node:fs" -import { createProject, installPlugins, REPO_ROOT, runTui, runTuiScreens, startMock, type StartedMock } from "./harness.js" +import { createProject, installPlugins, PACKAGE_DIR, REPO_ROOT, runTui, runTuiScreens, startMock, type StartedMock } from "./harness.js" const e2e = process.env["CTREE_E2E"] === "1" @@ -26,14 +26,14 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { let project: Awaited> beforeAll(async () => { - const build = Bun.spawnSync({ cmd: ["bun", "run", "scripts/build.ts"], cwd: REPO_ROOT, stdio: ["ignore", "pipe", "pipe"] }) + const build = Bun.spawnSync({ cmd: ["bun", "run", "scripts/build.ts"], cwd: PACKAGE_DIR, stdio: ["ignore", "pipe", "pipe"] }) if (build.exitCode !== 0) throw new Error(`build failed: ${build.stderr.toString()}`) mock = await startMock({ tool: false }) project = await createProject({ mockPort: mock.port }) await installPlugins({ projectDir: project.dir, - server: [path.join(REPO_ROOT, "dist", "server.js")], - tui: [path.join(REPO_ROOT, "dist", "tui.js")], + server: [path.join(PACKAGE_DIR, "dist", "server.js")], + tui: [path.join(PACKAGE_DIR, "dist", "tui.js")], }) }) @@ -45,7 +45,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { test("crop in the tree hides a tool result from the model; undo restores it", async () => { const toolMock = await startMock({ tool: true }) const proj = await createProject({ mockPort: toolMock.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { await runTui({ projectDir: proj.dir, @@ -96,7 +96,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { test("/branch, /merge (squash via $EDITOR) lands a ◆ record in the trunk; undo re-opens", async () => { const m = await startMock({ tool: false }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { await runTui({ projectDir: proj.dir, @@ -141,7 +141,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { test("⏎ on an earlier turn offers Pi's three fork choices; summarize lands a ≣ summary in the fork", async () => { const m = await startMock({ tool: false }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const text = await runTui({ projectDir: proj.dir, @@ -196,7 +196,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { test("esc in the custom-prompt editor loops back to Pi's choices instead of cancelling the whole jump", async () => { const m = await startMock({ tool: false }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const { screens } = await runTuiScreens({ projectDir: proj.dir, @@ -246,7 +246,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { test("the server captures the real system prompt; consumers shows it as a bucket", async () => { const m = await startMock({ tool: false }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const log = path.join(proj.dir, "ctree-debug.log") const { screens } = await runTuiScreens({ @@ -300,7 +300,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { // the cursor's row and nowhere else const toolMock = await startMock({ tool: true }) const proj = await createProject({ mockPort: toolMock.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const { screens } = await runTuiScreens({ projectDir: proj.dir, @@ -376,7 +376,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { // something else repaints), and that it is still moving while the model thinks. const m = await startMock({ tool: false, slowSummaryMs: 20000 }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const { screens } = await runTuiScreens({ projectDir: proj.dir, @@ -432,7 +432,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { test("the ? pane teaches the verbs, and scrolls to the rest", async () => { const m = await startMock({ tool: false }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const { screens } = await runTuiScreens({ projectDir: proj.dir, @@ -474,7 +474,7 @@ describe.skipIf(!e2e)("tui e2e: built plugin", () => { // `g`, so this proves the sequence tree branches rather than `gg` shadowing them const m = await startMock({ tool: false }) const proj = await createProject({ mockPort: m.port }) - await installPlugins({ projectDir: proj.dir, server: [path.join(REPO_ROOT, "dist", "server.js")], tui: [path.join(REPO_ROOT, "dist", "tui.js")] }) + await installPlugins({ projectDir: proj.dir, server: [path.join(PACKAGE_DIR, "dist", "server.js")], tui: [path.join(PACKAGE_DIR, "dist", "tui.js")] }) try { const { screens } = await runTuiScreens({ projectDir: proj.dir, diff --git a/test/fixtures/tree.ts b/packages/context-tree/test/fixtures/tree.ts similarity index 100% rename from test/fixtures/tree.ts rename to packages/context-tree/test/fixtures/tree.ts diff --git a/test/fold.test.ts b/packages/context-tree/test/fold.test.ts similarity index 100% rename from test/fold.test.ts rename to packages/context-tree/test/fold.test.ts diff --git a/test/gauge.test.ts b/packages/context-tree/test/gauge.test.ts similarity index 100% rename from test/gauge.test.ts rename to packages/context-tree/test/gauge.test.ts diff --git a/test/help.test.ts b/packages/context-tree/test/help.test.ts similarity index 100% rename from test/help.test.ts rename to packages/context-tree/test/help.test.ts diff --git a/test/journal-decisions.test.ts b/packages/context-tree/test/journal-decisions.test.ts similarity index 100% rename from test/journal-decisions.test.ts rename to packages/context-tree/test/journal-decisions.test.ts diff --git a/test/journal.test.ts b/packages/context-tree/test/journal.test.ts similarity index 100% rename from test/journal.test.ts rename to packages/context-tree/test/journal.test.ts diff --git a/test/lanes.test.ts b/packages/context-tree/test/lanes.test.ts similarity index 100% rename from test/lanes.test.ts rename to packages/context-tree/test/lanes.test.ts diff --git a/test/merge-dialog.test.ts b/packages/context-tree/test/merge-dialog.test.ts similarity index 100% rename from test/merge-dialog.test.ts rename to packages/context-tree/test/merge-dialog.test.ts diff --git a/test/navigation.test.ts b/packages/context-tree/test/navigation.test.ts similarity index 100% rename from test/navigation.test.ts rename to packages/context-tree/test/navigation.test.ts diff --git a/test/progress-feedback.test.ts b/packages/context-tree/test/progress-feedback.test.ts similarity index 100% rename from test/progress-feedback.test.ts rename to packages/context-tree/test/progress-feedback.test.ts diff --git a/test/progress.test.ts b/packages/context-tree/test/progress.test.ts similarity index 100% rename from test/progress.test.ts rename to packages/context-tree/test/progress.test.ts diff --git a/test/prompt-at.test.ts b/packages/context-tree/test/prompt-at.test.ts similarity index 100% rename from test/prompt-at.test.ts rename to packages/context-tree/test/prompt-at.test.ts diff --git a/test/spine.test.ts b/packages/context-tree/test/spine.test.ts similarity index 100% rename from test/spine.test.ts rename to packages/context-tree/test/spine.test.ts diff --git a/test/store.test.ts b/packages/context-tree/test/store.test.ts similarity index 100% rename from test/store.test.ts rename to packages/context-tree/test/store.test.ts diff --git a/test/tokens.test.ts b/packages/context-tree/test/tokens.test.ts similarity index 100% rename from test/tokens.test.ts rename to packages/context-tree/test/tokens.test.ts diff --git a/test/transcripts.test.ts b/packages/context-tree/test/transcripts.test.ts similarity index 100% rename from test/transcripts.test.ts rename to packages/context-tree/test/transcripts.test.ts diff --git a/test/tree.test.ts b/packages/context-tree/test/tree.test.ts similarity index 100% rename from test/tree.test.ts rename to packages/context-tree/test/tree.test.ts diff --git a/test/undo.test.ts b/packages/context-tree/test/undo.test.ts similarity index 100% rename from test/undo.test.ts rename to packages/context-tree/test/undo.test.ts diff --git a/packages/context-tree/tsconfig.json b/packages/context-tree/tsconfig.json new file mode 100644 index 0000000..0789a30 --- /dev/null +++ b/packages/context-tree/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@opentui/solid" + }, + "include": ["src", "scripts", "test"] +} diff --git a/tsconfig.json b/tsconfig.base.json similarity index 76% rename from tsconfig.json rename to tsconfig.base.json index b8f01f3..4682ce9 100644 --- a/tsconfig.json +++ b/tsconfig.base.json @@ -4,8 +4,6 @@ "module": "esnext", "moduleResolution": "bundler", "lib": ["esnext"], - "jsx": "preserve", - "jsxImportSource": "@opentui/solid", "strict": true, "skipLibCheck": true, "esModuleInterop": true, @@ -14,6 +12,5 @@ "isolatedModules": true, "noEmit": true, "types": ["bun"] - }, - "include": ["src", "scripts", "test"] + } }