Skip to content

feat(api): export canonical tag helpers via opencode-mem/tags subpath#125

Merged
tickernelz merged 1 commit into
tickernelz:mainfrom
leiverkus:feat/public-tag-helpers
Jun 7, 2026
Merged

feat(api): export canonical tag helpers via opencode-mem/tags subpath#125
tickernelz merged 1 commit into
tickernelz:mainfrom
leiverkus:feat/public-tag-helpers

Conversation

@leiverkus
Copy link
Copy Markdown
Contributor

Summary

Adds a stable opencode-mem/tags subpath export so third-party plugins can compute canonical container tags without reverse-engineering the format from the source.

import { getProjectTagInfo, getUserTagInfo, getTags } from "opencode-mem/tags";

const projectTag = getProjectTagInfo(process.cwd()).tag;
// → "opencode_project_<sha16-of-git-remote-or-path>"

const userTag = getUserTagInfo().tag;
// → "opencode_user_<sha16-of-git-email>"

Why this matters

The canonical format encodes meaning that several handlers depend on:

  • handleListMemories (no-tag path) and handleStats byScope recognize scope via the literal _project_ / _user_ substring
  • Auto-capture writes use these exact tag shapes
  • extractScopeFromTag parses them with the same convention

Third-party plugins that hand-roll their own tags can land in a shadow shard whose container_tag doesn't match either substring. Those rows become invisible to listing endpoints and miscount in stats — easy to do by accident because the helpers were internal-only and the format was only documented in source comments.

Exposing them via a stable subpath turns this into intentional public API. The functions are already exported at the source level; this PR just makes them reachable via package resolution.

What's in the subpath

TagInfo                         (interface)

getProjectTagInfo(directory)    → TagInfo with "opencode_project_<sha>"
getUserTagInfo()                → TagInfo with "opencode_user_<sha>"
getTags(directory)              → { user, project }

getProjectIdentity(directory)   → git-remote-or-path
getProjectRoot(directory)       → repo root or directory
getProjectName(directory)       → human-readable name

getGitEmail()                   → git config user.email
getGitName()                    → git config user.name
getGitRepoUrl(directory)        → remote.origin.url
getGitCommonDir(directory)      → git rev-parse --git-common-dir
getGitTopLevel(directory)       → repo top-level

The main entries are the three tag helpers; the git/path utilities come along for free since they're in the same module. If you'd rather keep the git/path helpers internal and only expose the three tag entry points, I'm happy to add a thin facade — but they're already lifted as export at the source so the surface is "what's already public, just reachable now."

Use case

I needed this while building a sibling plugin that exposes opencode-mem's /api/search + POST /api/memories as opencode LLM tools, so the agent can do explicit memory_search(query) / memory_remember(...) calls mid-conversation (complement to your existing implicit injectOn: "first" injection path — your built-in memory tool covers the same functional surface; mine just splits into single-purpose tools because some agent runtimes choose them more reliably).

Without canonical tag helpers, every such plugin would either hand-roll the convention (and silently end up in shadow shards — I did exactly that on first try, then traced the bug via extractScopeFromTag) or copy-paste the internal tags.ts code. A public subpath makes opencode-mem easier to build on top of.

Verification

  • bun test: 143 pass / 0 fail (no behavior change, pure visibility addition)
  • bun run typecheck: clean
  • bun run build: clean
  • npx prettier --check: clean
  • Subpath resolution verified: node -e "import('opencode-mem/tags')..." returns all 11 helpers including the three documented entry points.

Out of scope

If a future PR wants to address the shadow-shard problem upstream (e.g. have handleAddMemory reject malformed containerTag values), that's a separate, opinion-laden change. This PR just makes the right thing easier to do voluntarily.

Adds a stable subpath export so third-party plugins can compute canonical
container tags without reverse-engineering the format from the source.

```ts
import { getProjectTagInfo, getUserTagInfo, getTags } from "opencode-mem/tags";

const projectTag = getProjectTagInfo(process.cwd()).tag;
//   → "opencode_project_<sha16-of-git-remote-or-path>"

const userTag = getUserTagInfo().tag;
//   → "opencode_user_<sha16-of-git-email>"
```

## Why

The canonical format encodes meaning that several handlers depend on:

- `handleListMemories` (no-tag path) and `handleStats` `byScope` recognize
  scope via the literal `_project_` / `_user_` substring
- Auto-capture writes use these exact tag shapes
- `extractScopeFromTag` parses them with the same convention

Third-party plugins that hand-roll their own tags can land in a shadow
shard whose `container_tag` doesn't match either substring. Those rows
become invisible to listing endpoints and miscount in stats — easy to do
by accident because the docs and helpers were internal-only.

Exposing the helpers via a stable subpath turns this into intentional
public API. The functions are already exported at the source level; this
PR just makes them reachable via package resolution.

## What's in the subpath

```
TagInfo                         (interface)

getProjectTagInfo(directory)    → TagInfo with `opencode_project_<sha>`
getUserTagInfo()                → TagInfo with `opencode_user_<sha>`
getTags(directory)              → { user, project }

getProjectIdentity(directory)   → git-remote-or-path
getProjectRoot(directory)       → repo root or directory
getProjectName(directory)       → human-readable name

getGitEmail()                   → git config user.email
getGitName()                    → git config user.name
getGitRepoUrl(directory)        → remote.origin.url
getGitCommonDir(directory)      → `git rev-parse --git-common-dir`
getGitTopLevel(directory)       → repo top-level
```

The main entries are the three tag helpers; the git/path utilities come
along for free since they're in the same module.

## Verification

- `bun test`: 143 pass / 0 fail
- `bun run typecheck`: clean
- `bun run build`: clean
- `npx prettier --check`: clean
- Subpath resolution verified: `node -e "import('opencode-mem/tags')..."` returns
  all 11 helpers including the three documented entry points.

## Use case

I needed this while building a sibling plugin that exposes opencode-mems
`/api/search` + `POST /api/memories` as opencode LLM tools, so the agent
can do explicit `memory_search(query)` / `memory_remember(...)` calls
mid-conversation (complement to the existing implicit `injectOn: "first"`
path). Without canonical tag helpers, every such plugin would either
hand-roll the convention (and silently end up in shadow shards — I did
exactly that on first try) or copy-paste the internal `tags.ts` code.

A public subpath makes opencode-mem easier to build on top of and
encourages an ecosystem of tool-style plugins around the same data.
@tickernelz tickernelz merged commit 5060d4c into tickernelz:main Jun 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants