Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c6d214
feat(cloud): add Cloudflare D1 real-time sync backend and streaming w…
fstubner Sep 30, 2026
f0013e4
chore(cloud): link D1 database id and commit package-lock
fstubner Sep 30, 2026
ff5472c
fix(cloud): add top-level try-catch error handler in worker fetch
fstubner Sep 30, 2026
7ba0b9e
chore(cloud): upgrade wrangler to v4 and workers-types to v5
fstubner Sep 30, 2026
0d78edb
feat(cloud): configure mcp.xtctx.com and sync.xtctx.com custom domains
fstubner Sep 30, 2026
04982f1
feat(cloud): configure production GITHUB_CLIENT_ID for device flow
fstubner Sep 30, 2026
54c61a3
chore: update root package-lock with build dependencies
fstubner Sep 30, 2026
0c86754
feat(sync): implement automatic zero-flag incremental diff sync and b…
fstubner Sep 30, 2026
fb84f8f
feat(sync): auto-sync upon login and background sync during MCP startup
fstubner Sep 30, 2026
0d18981
feat(daemon): implement self-healing detached background sync daemon …
fstubner Sep 30, 2026
dd94c03
feat(sync): support JWT decoding and dynamic device identity for env-…
fstubner Sep 30, 2026
f68469e
fix(cloud): fail closed without JWT_SECRET, header-only revocable tok…
fstubner Oct 1, 2026
1ae4243
feat(sync): cloud upload is opt-in per project, runs inside the MCP s…
fstubner Oct 1, 2026
9de8969
fix(cloud)!: authoritative uploads, revocation that survives deletion…
fstubner Oct 1, 2026
ae3c588
fix(sync)!: upload only this project, keep each session's cloud copy …
fstubner Oct 1, 2026
11124de
chore(deps): have dependabot watch cloud/
fstubner Oct 1, 2026
e02357d
docs: describe cloud sync as optional and opt-in per project, and exa…
fstubner Oct 1, 2026
9621409
merge: audit fixes into cloud sync
fstubner Oct 1, 2026
fe9917d
fix(sync): close each cloud connection, so the server does not crash …
fstubner Oct 1, 2026
8cfdb51
merge: main's dependency advisories via audit fixes
fstubner Oct 2, 2026
6ca9d92
Merge branch 'main' into fix/cloud-production
fstubner Oct 2, 2026
d46b393
merge: audit fixes (timing test floor)
fstubner Oct 2, 2026
63c4254
test(sync): resolve the sandbox temp dir, as the index resolves proje…
fstubner Oct 2, 2026
bc85c75
Merge branch 'main' into fix/cloud-production
fstubner Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,15 @@ updates:
interval: weekly
commit-message:
prefix: deps

# The Worker: wrangler, workers-types and the OAuth provider move often, and
# the provider is security-relevant.
- package-ecosystem: npm
directory: /cloud
schedule:
interval: weekly
commit-message:
prefix: deps
groups:
dev-dependencies:
dependency-type: development
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,17 @@ jobs:
cache-dependency-path: |
package-lock.json
landing/package-lock.json
cloud/package-lock.json

- name: Install root dependencies
run: npm ci

- name: Install landing dependencies
run: npm --prefix landing ci

- name: Install cloud dependencies
run: npm --prefix cloud ci

# Every job here loads the real embedding model, so each one pulls the
# 86MB MiniLM from HuggingFace: four jobs a run, and HuggingFace starts
# answering 429. That blocked a merge on 2026-08-28 with nothing wrong
Expand All @@ -133,6 +137,9 @@ jobs:
- name: Typecheck (including the test tree)
run: npm run typecheck

- name: Cloud Worker typecheck and tests
run: npm run test:cloud

- name: Root security tests and checklist
run: |
npm run test:security
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
dist/
landing/dist/
.wrangler/
.xtctx/.store/
.xtctx/config.yaml
.xtctx/skills/
Expand Down
46 changes: 41 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# xtctx — Architecture

One npm package, no services. Everything runs in the invoking process on the
developer's machine. `docs/architecture.md` describes module internals; this
document fixes the parts, how a handoff actually flows through them, the
boundaries between them, and what each part is allowed to trust.
One npm package, no services on the handoff path. Everything runs in the
invoking process on the developer's machine. The one exception is optional and
off by default: cloud sync, which uploads an opted-in project's sessions to a
separate Worker (`cloud/`) so the same user's agents on other machines can read
them. Handoff never depends on it. `docs/architecture.md` describes module
internals; this document fixes the parts, how a handoff actually flows through
them, the boundaries between them, and what each part is allowed to trust.

## Parts

- **CLI** (`src/cli/`) — `setup`, `status`, `scan`, `export`, `import`,
`calibrate`, `disconnect`, and the internal `--hook session-start` entry point. Bare `xtctx` on a non-TTY stdio pair
`embeddings enable|disable`, `calibrate`, `disconnect`, the cloud-sync
commands `login`, `logout` and `sync`, and the internal `--hook session-start` entry point. Bare `xtctx` on a non-TTY stdio pair
starts the MCP server.
- **MCP server** (`src/mcp/`) — stdio JSON-RPC server exposing exactly five
read-only tools. Spawned by coding agents via `npx -y xtctx`.
Expand Down Expand Up @@ -46,6 +50,17 @@ boundaries between them, and what each part is allowed to trust.
- **Config writers** (`src/config/`) — setup/disconnect logic that edits
other tools' config files (MCP config, managed instruction blocks,
synced skills, the Claude Code hook in `.claude/settings.json`).
- **Cloud sync client** (`src/sync/`) — optional. Uploads a project's
sessions from its index when, and only when, someone is logged in
(`~/.xtctx/credentials.json`) and the project is on the opt-in list
(`~/.xtctx/cloud-projects.json`). Runs inside the MCP server every 10
seconds and once on shutdown, or from `xtctx sync`; reads the index through
its own read-only connection; keeps its position per project and account in
`~/.xtctx/sync/`, never in the index. See `docs/cloud-sync.md`.
- **Cloud Worker** (`cloud/`) — a separate Cloudflare Worker, not shipped in
the npm package. Stores uploads in D1 and serves them back over MCP to the
same account. Has its own tests (`npm run test:cloud`) and deploy steps
(`cloud/README.md`).
- **Landing site** (`landing/`) — static Astro site on GitHub Pages;
no runtime relationship to the package.

Expand Down Expand Up @@ -123,6 +138,17 @@ would otherwise never vectorize anything; `hybrid` deliberately answers from
keyword while the model is still loading, so the first call after a cold start
is fast rather than blocked.

**Cloud sync, when a project opts in.** The MCP server starts an upload loop
next to its scan. Each tick reads the sessions this project's index
attributes to this project, sends what changed under the index's own message
ids, and compares the server's per-session count with the local one; on a
mismatch (a re-read replaced rows under new ids, or deleted some) it sends the
session whole with every id it holds, and the server deletes the rest, so the
cloud copy ends equal to the index. (A session with more ids than fit in one
request is resent but not pruned.) On shutdown the final upload runs
alongside the index close, inside the same bounded grace window, so it never
holds up releasing the scan lease.

**What comes back is raw.** Sessions, message text, and pointers — never a
generated summary. A recap is the lossy artefact this exists to replace, and
the transcripts remain authoritative.
Expand All @@ -148,6 +174,11 @@ the transcripts remain authoritative.
Managed markdown blocks touch nothing outside their markers — including the
tail of the file, which is why setup does not trim it and removal gives back
exactly the separator it added.
- **Index → cloud:** only with a login and a per-project opt-in, both in the
user's home directory. Metadata is cut to an allowlist with absolute paths
dropped, `source_pointer` is never sent, and environment credentials that
differ from the saved login refuse to upload unless explicitly allowed.
Message text goes as written.
- **Process boundary:** the MCP server writes logs to stderr only — stdout
is the JSON-RPC transport. The session-start hook fails open: it must
never break a host agent's startup.
Expand Down Expand Up @@ -183,6 +214,11 @@ the transcripts remain authoritative.
`xtctx import` checks the header and every line before writing it, binds
every value as a SQL parameter, and its content reaches agents through the
same fenced MCP output as any other transcript text.
- **The opt-in is the user's, never the repository's.** Cloud sync reads its
login and opt-in list from the home directory, not `.xtctx/config.yaml`, so
a cloned repository cannot turn uploading on. The Worker treats every upload
as untrusted input: it validates the body, caps sizes, and scopes rows to
the authenticated account.
- **The registry and npm supply chain** are trusted at install time; CI
pins action SHAs and publishes via OIDC with provenance, no long-lived
tokens.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ leaves this heading in place and empties it when a version is cut.
* **mcp:** name an unconfigured project instead of answering with silence ([#315](https://github.com/fstubner/xtctx/issues/315))
* **index:** `xtctx export` and `xtctx import`; migrate an older index in place and carry sessions forward from one set aside; `xtctx status` counts sessions that exist only in the index
* **embeddings:** semantic search is an optional add-on, off until `xtctx embeddings enable` installs the local model; keyword-only is a working state that status reports
* **cloud:** optional cloud sync, off by default and opt-in per project: `xtctx login` then `xtctx sync enable`, after which the MCP server uploads that project's sessions every 10 seconds and once on shutdown, and agents on your other machines read them from the cloud's MCP endpoint (OAuth sign-in for MCP clients, `xtctx sync token` for those without it). `xtctx status` and `xtctx sync status` say whether it is on and when it last uploaded or failed; `xtctx logout --delete-data` removes what was sent. What is and is not uploaded is listed in `docs/cloud-sync.md`
* **cloud:** each session's cloud copy is kept equal to the index, including after a scraper upgrade re-reads sessions under new message ids; metadata is cut to an allowlist with absolute paths dropped

### Bug Fixes

Expand Down
39 changes: 26 additions & 13 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ them after 30 days by default). Deleting the index loses those, so xtctx
never deletes it: schema upgrades migrate it in place, a corrupt one is set
aside and its sessions carried into the rebuilt one, and `xtctx export` /
`xtctx import` keep a copy elsewhere. It sends transcript content nowhere
unless a project opts in to an external embedding endpoint, written into
`.xtctx/config.yaml` by hand, trusted by the user in
`XTCTX_TRUSTED_EMBEDDING_ENDPOINTS` (a repository cannot set that), and
reported by `xtctx status`.
unless the user opts a project in to one of two things, both off by default
and both reported by `xtctx status`: cloud sync (`xtctx login` and then
`xtctx sync enable` in that project), or an external embedding endpoint,
written into `.xtctx/config.yaml` by hand and trusted by the user in
`XTCTX_TRUSTED_EMBEDDING_ENDPOINTS` (a repository cannot set that).

## Users

Expand All @@ -30,7 +31,11 @@ reported by `xtctx status`.
tools) that need stable session references and raw-detail pointers —
served by `xtctx_handoff_manifest`.

Single-user, single-machine. There is no team, sync, or server component.
Single-user. Handoff itself is single-machine and needs no server. The one
exception is optional cloud sync, opt-in per project: a logged-in user can
upload an opted-in project's transcripts so agents on their other machines can
read them over MCP ([docs/cloud-sync.md](docs/cloud-sync.md)). There is no
team or shared component.

## Success

Expand Down Expand Up @@ -68,11 +73,15 @@ Single-user, single-machine. There is no team, sync, or server component.
index now, `--embed` to finish vectorizing too), `embeddings enable|disable`
(install or remove the optional local model; keyword search needs none of
it), `calibrate` (time the embedding model on this machine's devices and use
the fastest), `disconnect`.
the fastest), `export` / `import` (keep a copy of the index's sessions
outside it), `disconnect`, and for the optional cloud sync `login`,
`logout` and `sync` (`sync enable` opts the current project in).

Out of scope (deliberately, and documented everywhere the product speaks):
no daemon, no API server, no dashboard, no generated summaries or briefs,
no durable memory, no write-back tools, no cloud anything.
no durable memory, no write-back tools, and nothing leaves the machine unless
the user opts a project in to cloud sync, which is optional and off by
default.

## Constraints

Expand All @@ -90,13 +99,17 @@ no durable memory, no write-back tools, no cloud anything.
atomic, merge-preserving, and never clobber unparsable user content.
- Transcript content handed to a model is untrusted data; the MCP layer
fences it and never grows write capabilities.
- Everything runs local by default. Three network dependencies exist. Two are
- Everything runs local by default. Four network dependencies exist. Two are
unavoidable and narrow: the one-time embedding-model download from Hugging
Face (and the runtime from npm), made only when the user runs
`xtctx embeddings enable`, and loopback-only HTTPS calls to Antigravity's local language server
(127.0.0.1, exact-PID + CSRF matched; certificate verification is off
because the server is self-signed). The third is opt-in and is the only one
that carries transcript text: an OpenAI-compatible embedding endpoint named
in `.xtctx/config.yaml`. It is never inferred from the environment, the API
key is never stored in that file, and `xtctx status` prints the endpoint
whenever one is set.
because the server is self-signed). The other two are opt-in, and they are
the only ones that carry transcript text. One is an OpenAI-compatible
embedding endpoint named in `.xtctx/config.yaml`: it is never inferred from
the environment, the API key is never stored in that file, and `xtctx
status` prints the endpoint whenever one is set. The other is cloud sync,
which uploads an opted-in project's sessions to the xtctx cloud Worker
(`cloud/`) only while someone is logged in; the opt-in list lives in the
user's home directory, so a repository cannot opt itself in, and `xtctx
status` says whether it is on ([docs/cloud-sync.md](docs/cloud-sync.md)).
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ xtctx is local cross-tool handoff for AI coding agents.
It indexes the transcript files your local coding agents already write, and
exposes them over MCP so the next tool you open can find recent sessions and
read the raw messages. It does not run a daemon, host an API, generate
summaries, or maintain durable project memory.
summaries, or maintain durable project memory. Everything stays on your
machine unless you opt a project in to cloud sync, which is optional and off
by default ([`docs/cloud-sync.md`](docs/cloud-sync.md)).

Each project opts in once with `xtctx setup`. The MCP server resolves the
project from the working directory, and in a project that has not opted in it
Expand Down Expand Up @@ -167,7 +169,8 @@ target drift, and tools that do not have a verified skill surface.
It reports the current local index rather than forcing a transcript scan. If
the index is empty, ask a configured agent to call `xtctx_recent_sessions`.
When the index holds sessions whose transcripts are gone, it says how many and
points at `xtctx export`.
points at `xtctx export`. Its `Cloud` line says whether cloud sync is on for
the project, and when it last uploaded or failed.

`xtctx disconnect <tool>` stops xtctx from managing one tool for the project.
It removes the xtctx MCP entry for that tool, removes managed instruction
Expand Down Expand Up @@ -307,7 +310,14 @@ startup hooks; others receive MCP config plus managed instructions only.
## Limits

- xtctx is local-only by default: it never uploads transcripts and runs no
telemetry. A project can opt into an external embedding endpoint by writing
telemetry. Cloud sync is optional and opt-in per project: it sends nothing
until you log in (`xtctx login`) *and* opt a project in (`xtctx sync enable`),
and then sends that project's transcript text, including whatever paths or
output the agents wrote into it, to the xtctx cloud server, where your other
machines' agents can read it over MCP. `xtctx status` says whether it is on
for the project and when it last uploaded
([`docs/cloud-sync.md`](docs/cloud-sync.md)).
A project can opt into an external embedding endpoint by writing
one into `.xtctx/config.yaml`, in which case window text is sent there to be
vectorized — never inferred from an environment variable, and `xtctx status`
names the endpoint in full whenever one is configured.
Expand Down
Loading
Loading