Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/connect-npx-launcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xnetjs/cli': patch
---

`xnet connect` now registers an `npx -y @xnetjs/cli` MCP server launcher when the `xnet` bin is not on PATH, so the zero-install `npx @xnetjs/cli connect claude-code` on-ramp produces a registration that still works after the npx cache is gone.
5 changes: 1 addition & 4 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,7 @@
{
"name": "site-0432 (astro 4396)",
"runtimeExecutable": "/bin/sh",
"runtimeArgs": [
"-c",
"cd site && exec node_modules/.bin/astro dev --port 4396 --strictPort"
],
"runtimeArgs": ["-c", "cd site && exec node_modules/.bin/astro dev --port 4396 --strictPort"],
"port": 4396
},
{
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ peer-to-peer or through a hub you control, and signed with your own keys.

## Try it

- **[Connect your coding agent](https://xnet.fyi/agents)** — one command,
no install:

```bash
npx @xnetjs/cli connect claude-code # or: connect codex
```

Claude Code or Codex can then read, search, query, and edit your
workspace — read-only until you say otherwise.

- **[Open the demo](https://xnet.fyi/app)** — no signup; sign in with your
device's passkey (Touch ID, Face ID, Windows Hello). Demo data lives in your
browser, with encrypted backups on our demo hub (10MB, expires after 24
Expand All @@ -34,6 +44,23 @@ peer-to-peer or through a hub you control, and signed with your own keys.
> not a maturity signal; what is and isn't stable is written down in
> [STABILITY.md](./STABILITY.md).

## Your agent, your workspace

Coding agents are first-class users of an xNet workspace — through the
filesystem they already know, not a wall of tool definitions. `xnet connect`
installs a ~500-token skill and gives the agent three lanes, cheapest first:
the `xnet` CLI (plain stdout), a scoped **vault checkout** (Markdown/JSONL
files whose edits become schema-validated mutation plans), and a slim MCP
server as the no-shell fallback. On a 15-task benchmark the files+CLI surface
uses [~0.11x the tokens of a traditional MCP
toolset](https://xnet.fyi/docs/guides/agent-interfaces/#benchmark-methodology)
at equal success.

Safety is structural: the server registers **read-only by default**, writes
require an enrolled agent passport or an explicit key, and every change an
agent makes lands in the workspace's signed, hash-chained change log — so you
can verify what your agent did without trusting it.

## Build with it

Everything in xNet is a **node**, and a **schema** describes what a node is:
Expand Down

Large diffs are not rendered by default.

501 changes: 501 additions & 0 deletions docs/explorations/0456_[_]_ENTRY_VECTOR_THE_AGENT_DOOR_FIRST.md

Large diffs are not rendered by default.

525 changes: 525 additions & 0 deletions docs/explorations/0457_[-]_AGENT_FIRST_SITE_REARCHITECTURE.md

Large diffs are not rendered by default.

31 changes: 24 additions & 7 deletions packages/cli/src/__tests__/connect-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import {
MANAGED_BEGIN,
MANAGED_END,
mergeManagedBlock,
NPX_LAUNCHER,
resolveServerLauncher,
runConnect,
writeCodexConfig,
writeMcpJson,
XNET_PATH_LAUNCHER,
type ConnectOptions
} from '../commands/connect.js'

Expand All @@ -36,8 +39,22 @@ describe('xnet connect', () => {
expect(buildServerEntry({ dir, db: '/d.db' }).args).toEqual(['mcp', 'serve', '--db', '/d.db'])
})

it('registers an npx launcher when xnet is not on PATH (zero-install connect)', async () => {
// A PATH with no xnet bin anywhere → the npx fallback, so the registered
// server survives after the `npx @xnetjs/cli connect …` cache is gone.
expect(resolveServerLauncher({ PATH: dir })).toEqual(NPX_LAUNCHER)

// A PATH dir that does hold an xnet bin → register the real thing.
await writeFile(join(dir, 'xnet'), '#!/bin/sh\n')
expect(resolveServerLauncher({ PATH: `${dir}` })).toEqual(XNET_PATH_LAUNCHER)

const entry = buildServerEntry({ dir, db: '/d.db' }, NPX_LAUNCHER)
expect(entry.command).toBe('npx')
expect(entry.args).toEqual(['-y', '@xnetjs/cli', 'mcp', 'serve', '--db', '/d.db'])
})

it('claude-code writes skill, .mcp.json, and CLAUDE.md; is idempotent', async () => {
const changes = await runConnect('claude-code', { ...base, dir })
const changes = await runConnect('claude-code', { ...base, dir }, XNET_PATH_LAUNCHER)
const byPath = Object.fromEntries(changes.map((c) => [c.path.replace(dir, ''), c.status]))
expect(byPath['/.claude/skills/xnet/SKILL.md']).toBe('created')
expect(byPath['/.mcp.json']).toBe('created')
Expand All @@ -48,12 +65,12 @@ describe('xnet connect', () => {
expect(mcp.mcpServers.xnet.env).toEqual({ XNET_READONLY: '1' })

// Re-run: everything unchanged.
const again = await runConnect('claude-code', { ...base, dir })
const again = await runConnect('claude-code', { ...base, dir }, XNET_PATH_LAUNCHER)
expect(again.every((c) => c.status === 'unchanged')).toBe(true)
})

it('codex writes AGENTS.md and .codex/config.toml with a valid server block', async () => {
const changes = await runConnect('codex', { ...base, dir, writes: true })
const changes = await runConnect('codex', { ...base, dir, writes: true }, XNET_PATH_LAUNCHER)
const byPath = Object.fromEntries(changes.map((c) => [c.path.replace(dir, ''), c.status]))
expect(byPath['/AGENTS.md']).toBe('created')
expect(byPath['/.codex/config.toml']).toBe('created')
Expand Down Expand Up @@ -84,7 +101,7 @@ describe('xnet connect', () => {
const original = '# My project\n\n@AGENTS.md\n\nHouse rules that took months.\n'
await writeFile(join(dir, 'CLAUDE.md'), original)

await runConnect('claude-code', { ...base, dir })
await runConnect('claude-code', { ...base, dir }, XNET_PATH_LAUNCHER)
const merged = await readFile(join(dir, 'CLAUDE.md'), 'utf8')
expect(merged).toContain('# My project')
expect(merged).toContain('House rules that took months.')
Expand All @@ -94,11 +111,11 @@ describe('xnet connect', () => {
})

it('rewrites only the managed block on a re-run, leaving edits outside it', async () => {
await runConnect('claude-code', { ...base, dir })
await runConnect('claude-code', { ...base, dir }, XNET_PATH_LAUNCHER)
const first = await readFile(join(dir, 'CLAUDE.md'), 'utf8')
await writeFile(join(dir, 'CLAUDE.md'), `${first}\n## My own section\n\nKeep me.\n`)

const again = await runConnect('claude-code', { ...base, dir })
const again = await runConnect('claude-code', { ...base, dir }, XNET_PATH_LAUNCHER)
const merged = await readFile(join(dir, 'CLAUDE.md'), 'utf8')
expect(merged).toContain('## My own section')
expect(merged).toContain('Keep me.')
Expand All @@ -109,7 +126,7 @@ describe('xnet connect', () => {

it('preserves an existing AGENTS.md on the codex path', async () => {
await writeFile(join(dir, 'AGENTS.md'), '# Existing agent rules\n')
await runConnect('codex', { ...base, dir })
await runConnect('codex', { ...base, dir }, XNET_PATH_LAUNCHER)
const merged = await readFile(join(dir, 'AGENTS.md'), 'utf8')
expect(merged).toContain('# Existing agent rules')
expect(merged).toContain(MANAGED_BEGIN)
Expand Down
44 changes: 38 additions & 6 deletions packages/cli/src/commands/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
* shell-less clients.
*/

import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { delimiter, dirname, join, resolve } from 'node:path'
import { XNET_AGENT_SKILL_MD } from '@xnetjs/plugins/node'
import { Command } from 'commander'
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'
Expand Down Expand Up @@ -52,14 +53,44 @@ export type McpServerEntry = { command: string; args: string[]; env?: Record<str

// ─── MCP server entry ─────────────────────────────────────────────────────────

/** How the registered MCP server should launch the CLI. */
export type ServerLauncher = { command: string; prefixArgs: string[] }

export const XNET_PATH_LAUNCHER: ServerLauncher = { command: 'xnet', prefixArgs: [] }
export const NPX_LAUNCHER: ServerLauncher = { command: 'npx', prefixArgs: ['-y', '@xnetjs/cli'] }

/**
* Pick the launcher the registered server entry should use. When `xnet` is a
* real executable on PATH, register it directly. When connect itself was run
* via `npx @xnetjs/cli connect …` (the zero-install on-ramp), there is no
* global `xnet` — an entry saying `command: "xnet"` would register a server
* that dies the moment the harness tries to launch it. Fall back to an
* `npx -y @xnetjs/cli` entry, which resolves the published package every time.
*/
export function resolveServerLauncher(env: NodeJS.ProcessEnv = process.env): ServerLauncher {
const pathValue = env.PATH ?? env.Path ?? ''
// Windows resolves bins through PATHEXT; cover npm's shim names everywhere.
const candidates = ['xnet', 'xnet.cmd', 'xnet.exe', 'xnet.ps1']
for (const entryDir of pathValue.split(delimiter)) {
if (!entryDir) continue
for (const name of candidates) {
if (existsSync(join(entryDir, name))) return XNET_PATH_LAUNCHER
}
}
return NPX_LAUNCHER
}

/** Build the MCP server registration for the requested backend and mode. */
export function buildServerEntry(options: ConnectOptions): McpServerEntry {
const args = ['mcp', 'serve']
export function buildServerEntry(
options: ConnectOptions,
launcher: ServerLauncher = XNET_PATH_LAUNCHER
): McpServerEntry {
const args = [...launcher.prefixArgs, 'mcp', 'serve']
if (options.agent) args.push('--agent', options.agent)
if (options.db) args.push('--db', options.db)
if (options.apiUrl) args.push('--api-url', options.apiUrl)
const env = options.writes ? undefined : { XNET_READONLY: '1' }
return { command: 'xnet', args, ...(env ? { env } : {}) }
return { command: launcher.command, args, ...(env ? { env } : {}) }
}

// ─── File writers (idempotent; report created/updated/unchanged) ──────────────
Expand Down Expand Up @@ -226,11 +257,12 @@ ${lines.join('\n')}

export async function runConnect(
harness: ConnectHarness,
options: ConnectOptions
options: ConnectOptions,
launcher: ServerLauncher = resolveServerLauncher()
): Promise<ConnectChange[]> {
const dir = resolve(options.dir)
const changes: ConnectChange[] = []
const entry = buildServerEntry(options)
const entry = buildServerEntry(options, launcher)

if (harness === 'claude-code') {
changes.push(
Expand Down
64 changes: 64 additions & 0 deletions site/public/agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# xNet, for the agent reading this

You are (or are about to be) connected to a user's **xNet workspace**: their
documents, databases, and canvases, stored locally on their device in a
SQLite-backed, CRDT-synced store. The store is the source of truth. Every
write is signed and lands in a hash-chained change log the user can audit —
including yours.

## How you were (or will be) connected

```
npx @xnetjs/cli connect claude-code # or: connect codex
```

That command installed a ~500-token `SKILL.md` (the full workflow contract),
registered the xNet MCP server as a fallback lane, and wrote a short section
into `CLAUDE.md` / `AGENTS.md`. It is idempotent and read-only by default;
write access exists only if your human passed `--writes`.

## The three lanes, cheapest first

1. **CLI.** `xnet search "<text>"`, `xnet query <db> --where field=value`,
`xnet db get <id>` — plain TSV/stdout you can grep and pipe. Reach here
first; it costs far fewer tokens than tool schemas.
2. **Vault checkout.** `xnet checkout --query "<text>"` materializes a
_scoped_ slice of the workspace as Markdown + JSONL files. Read and Edit
them like any files, then `xnet commit` turns your edits into
schema-validated mutation plans. Never check out the whole workspace.
3. **MCP.** The registered `xnet` server, for when you have no shell. Slim by
design; read-only unless connected with `--writes`.

## The safety contract (structural, not policy)

- **You cannot corrupt the store.** Every write path — file edit, CLI, MCP —
funnels through the same plan → validate → apply core. Malformed or
stale writes are quarantined as conflict notes in `.xnet/conflicts/`, never
silently applied.
- **You sign as yourself.** Writes require an enrolled agent passport or an
explicit key. The CLI refuses to persist a write under a throwaway
identity.
- **Everything you do is auditable.** Your changes land in the signed change
log next to everyone else's. The user can verify what you did without
trusting you — behave accordingly.

## Key CLI verbs

| Command | Purpose |
| ----------------------------- | ------------------------------------------- |
| `xnet checkout` | Materialize / extend a scoped checkout |
| `xnet status` | Pending plans and conflicts |
| `xnet commit [--apply]` | Turn file edits into applied mutations |
| `xnet search "<text>"` | Ranked full-text search |
| `xnet query <db> --where f=v` | Query tables (TSV default) |
| `xnet db get/set` | Single-cell reads and writes |
| `xnet run <script.js>` | Sandboxed bulk work, write _proposals_ only |
| `xnet skill` | Print the SKILL.md contract |

## Read more

- Full docs, one file: https://xnet.fyi/llms-full.txt
- Connect guide: https://xnet.fyi/docs/guides/coding-agents/
- Agent interfaces (checkout layout, benchmark methodology):
https://xnet.fyi/docs/guides/agent-interfaces/
- Building _apps_ on xNet instead? https://xnet.fyi/docs/ai/understanding-xnet/
Loading
Loading