send-context 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -63
- package/dist/core/distiller.js +38 -7
- package/dist/index.js +3 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,58 +1,77 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# send-context
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**Hand off an AI coding-agent session to another developer through an encrypted, ephemeral link.**
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/send-context)
|
|
6
8
|
[](./LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
</div>
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
`send-context` moves live context between AI coding agents — across machines, across people, across tools. One developer exports their session; a teammate runs a single command to pick up exactly where they left off, with the context injected straight into _their_ agent.
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
The session is distilled into a structured **Context Handoff** document, encrypted on your machine, and stored behind a short-lived link. The transport layer only ever sees ciphertext.
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
- [Why](#why)
|
|
20
|
+
- [Features](#features)
|
|
21
|
+
- [How it works](#how-it-works)
|
|
22
|
+
- [Getting started](#getting-started)
|
|
23
|
+
- [Usage](#usage)
|
|
24
|
+
- [Distill with Gemini](#distill-with-gemini)
|
|
25
|
+
- [Deploy the transport](#deploy-the-transport)
|
|
26
|
+
- [Supported agents](#supported-agents)
|
|
27
|
+
- [The Context Handoff document](#the-context-handoff-document)
|
|
28
|
+
- [Project structure](#project-structure)
|
|
29
|
+
- [Tech stack](#tech-stack)
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
Handing off work between agents usually means pasting a wall of chat history and hoping the next agent figures out what matters. That is noisy, leaks whatever was in the log, and lands the receiver in the same dead ends you already hit.
|
|
34
|
+
|
|
35
|
+
`send-context` extracts the session, distills it to what the next agent actually needs, encrypts it client-side, and gives you one link to share. The receiver decrypts locally and launches their own agent with the context already loaded.
|
|
21
36
|
|
|
22
37
|
## Features
|
|
23
38
|
|
|
24
|
-
- **Agent-agnostic** — works with pi, [Claude Code](https://claude.com/claude-code), and [OpenCode](https://opencode.ai)
|
|
25
|
-
- **
|
|
39
|
+
- **Agent-agnostic** — works with pi, [Claude Code](https://claude.com/claude-code), and [OpenCode](https://opencode.ai) through a small adapter per agent.
|
|
40
|
+
- **Structured handoff** — sessions become a dense, six-section brief (objective, state, completed work, failed approaches, next steps, raw appendix) instead of raw chat logs.
|
|
41
|
+
- **Optional Gemini distillation** — point a `GEMINI_API_KEY` at it and the session is summarized automatically; without one, an interactive flow walks you through the brief.
|
|
26
42
|
- **Zero-knowledge transport** — AES-256-GCM encryption happens client-side; the server stores only encrypted blobs that expire after 24 hours.
|
|
27
|
-
- **Wrapper injection** — `
|
|
43
|
+
- **Wrapper injection** — `receive <link> -- <agent> "prompt"` launches the receiver's own agent with the context pre-loaded.
|
|
28
44
|
- **No native dependencies** — pure TypeScript on Node's built-in `crypto`; installs cleanly on any OS.
|
|
29
|
-
- **Serverless backend** — a
|
|
45
|
+
- **Serverless backend** — a small Deno Deploy worker backed by Deno KV. No infrastructure to babysit.
|
|
30
46
|
|
|
31
47
|
## How it works
|
|
32
48
|
|
|
33
|
-
1. **Export** detects the active agent and extracts its session. With a `GEMINI_API_KEY` set, it distills the session into the handoff brief automatically; otherwise it guides you through writing the brief and
|
|
34
|
-
2. The brief is rendered into the Context Handoff
|
|
49
|
+
1. **Export** detects the active agent and extracts its session. With a `GEMINI_API_KEY` set, it distills the session into the handoff brief automatically; otherwise it guides you through writing the brief and choosing which raw messages to attach.
|
|
50
|
+
2. The brief is rendered into the Context Handoff template, encrypted with a password you choose, and uploaded. You get a `send-context://` link.
|
|
35
51
|
3. **Receive** downloads the blob, decrypts it locally, wraps it in an injection prompt, and spawns the receiving agent with that prompt as its opening message.
|
|
36
52
|
|
|
37
53
|
> [!NOTE]
|
|
38
|
-
> The password travels in the link fragment (`#…`), which is only ever processed client-side. Share the link over a channel you trust, or
|
|
54
|
+
> The password travels in the link fragment (`#…`), which is only ever processed client-side. Share the link over a channel you trust, or drop the fragment and share the password separately — `receive` will prompt for it.
|
|
39
55
|
|
|
40
56
|
## Getting started
|
|
41
57
|
|
|
42
58
|
### Prerequisites
|
|
43
59
|
|
|
44
|
-
- Node.js 20
|
|
45
|
-
- One of
|
|
46
|
-
- A Google Gemini API key (optional)
|
|
47
|
-
- [Deno](https://deno.com)
|
|
60
|
+
- Node.js 20 or newer
|
|
61
|
+
- One of `pi`, `claude`, or `opencode`, with at least one session in the project directory
|
|
62
|
+
- A Google Gemini API key (optional) to auto-distill sessions — see [Distill with Gemini](#distill-with-gemini)
|
|
63
|
+
- [Deno](https://deno.com) (optional) only if you want to deploy or run the transport worker yourself
|
|
48
64
|
|
|
49
65
|
### Install
|
|
50
66
|
|
|
51
67
|
```bash
|
|
52
|
-
npm
|
|
68
|
+
npm i -g send-context --registry=https://registry.npmjs.org/
|
|
53
69
|
send-context --help
|
|
54
70
|
```
|
|
55
71
|
|
|
72
|
+
> [!NOTE]
|
|
73
|
+
> The explicit `--registry` flag bypasses any private or proxy registry in your `~/.npmrc` that may not mirror the latest version. On a default npm setup, plain `npm i -g send-context` works too.
|
|
74
|
+
|
|
56
75
|
Or run it without installing:
|
|
57
76
|
|
|
58
77
|
```bash
|
|
@@ -68,28 +87,8 @@ cd context-handoff
|
|
|
68
87
|
npm install && npm run build
|
|
69
88
|
node dist/index.js --help
|
|
70
89
|
```
|
|
71
|
-
</details>
|
|
72
|
-
|
|
73
|
-
## Deploy the transport
|
|
74
|
-
|
|
75
|
-
The transport runs on **Deno Deploy + Deno KV** (`worker/main.ts`). It stores only encrypted payloads, each with a native 24-hour TTL.
|
|
76
|
-
|
|
77
|
-
**From GitHub (no local Deno needed):** push the repo, then create a project at [console.deno.com](https://console.deno.com) linked to it.
|
|
78
90
|
|
|
79
|
-
>
|
|
80
|
-
> Set **App Directory** to `worker` and **Entrypoint** to `main.ts`. If the app directory is left at the repository root, the build auto-detects the Node CLI in `src/` and fails. Leave install/build commands blank.
|
|
81
|
-
|
|
82
|
-
**From the CLI:**
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
deno install -gArf jsr:@deno/deployctl # one-time
|
|
86
|
-
cd worker
|
|
87
|
-
deno task dev # local test at http://localhost:8000
|
|
88
|
-
deno task deploy # deploys --prod, prints your *.deno.net host
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
> [!WARNING]
|
|
92
|
-
> Deno KV caps each value at 64 KiB, so payloads are limited to ~60 KB. A curated context handoff is far smaller; if you hit the limit, attach fewer appendix messages.
|
|
91
|
+
</details>
|
|
93
92
|
|
|
94
93
|
## Usage
|
|
95
94
|
|
|
@@ -101,15 +100,27 @@ SEND_CONTEXT_WORKER=your-project.deno.net send-context export
|
|
|
101
100
|
send-context export --worker your-project.deno.net --agent pi
|
|
102
101
|
```
|
|
103
102
|
|
|
104
|
-
Without a Gemini key, you
|
|
103
|
+
Without a Gemini key, you are guided through choosing the agent, writing the brief, curating the appendix, and setting a password. The command prints a link:
|
|
105
104
|
|
|
106
105
|
```
|
|
107
106
|
send-context://your-project.deno.net/<id>#<password>
|
|
108
107
|
```
|
|
109
108
|
|
|
110
|
-
|
|
109
|
+
### Receive a context handoff
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Launch an agent with the context injected:
|
|
113
|
+
send-context receive 'send-context://…/<id>#<password>' -- pi "continue"
|
|
114
|
+
send-context receive 'send-context://…/<id>#<password>' -- claude "continue"
|
|
115
|
+
send-context receive 'send-context://…/<id>#<password>' -- opencode run "continue"
|
|
116
|
+
|
|
117
|
+
# Or just print the decrypted handoff document:
|
|
118
|
+
send-context receive 'send-context://…/<id>#<password>'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Distill with Gemini
|
|
111
122
|
|
|
112
|
-
Raw sessions are noisy. Set `GEMINI_API_KEY` and `export` runs the session through Gemini first,
|
|
123
|
+
Raw sessions are noisy. Set `GEMINI_API_KEY` and `export` runs the session through Gemini first, distilling it into the five handoff sections and dropping the raw appendix — so the receiver gets a dense brief, not a chat log. The manual section and appendix prompts are skipped.
|
|
113
124
|
|
|
114
125
|
```bash
|
|
115
126
|
GEMINI_API_KEY=… SEND_CONTEXT_WORKER=your-project.deno.net send-context export
|
|
@@ -117,41 +128,59 @@ GEMINI_API_KEY=… SEND_CONTEXT_WORKER=your-project.deno.net send-context export
|
|
|
117
128
|
GEMINI_MODEL=gemini-2.5-pro GEMINI_API_KEY=… send-context export
|
|
118
129
|
```
|
|
119
130
|
|
|
120
|
-
If the key is absent or the call fails, `export` falls back to the manual flow — Gemini is an enhancement, not a hard dependency. The key never leaves your machine; only the encrypted, distilled brief is uploaded
|
|
131
|
+
If the key is absent or the call fails, `export` falls back to the manual flow — Gemini is an enhancement, not a hard dependency. The key never leaves your machine; only the encrypted, distilled brief is uploaded, and it uses Google's OpenAI-compatible endpoint, so no extra SDK is installed.
|
|
121
132
|
|
|
122
|
-
Set `SEND_CONTEXT_PASSWORD` to skip the password prompt
|
|
133
|
+
Set `SEND_CONTEXT_PASSWORD` to skip the password prompt as well. With distillation on and a single detected agent, that makes `export` fully non-interactive — no TTY required:
|
|
123
134
|
|
|
124
135
|
```bash
|
|
125
136
|
GEMINI_API_KEY=… SEND_CONTEXT_PASSWORD=… \
|
|
126
137
|
SEND_CONTEXT_WORKER=your-project.deno.net send-context export --agent pi
|
|
127
138
|
```
|
|
128
139
|
|
|
129
|
-
###
|
|
140
|
+
### Environment variables
|
|
130
141
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
| Variable | Required | Purpose |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `SEND_CONTEXT_WORKER` | yes (or `--worker`) | Transport worker host, e.g. `your-project.deno.net` |
|
|
145
|
+
| `GEMINI_API_KEY` | no | Enables automatic distillation |
|
|
146
|
+
| `GEMINI_MODEL` | no | Override the model (default `gemini-2.5-flash`) |
|
|
147
|
+
| `SEND_CONTEXT_PASSWORD` | no | Skip the interactive password prompt |
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
## Deploy the transport
|
|
150
|
+
|
|
151
|
+
The transport runs on **Deno Deploy + Deno KV** (`worker/main.ts`). It stores only encrypted payloads, each with a native 24-hour TTL.
|
|
152
|
+
|
|
153
|
+
**From GitHub (no local Deno needed):** push the repo, then create a project at [console.deno.com](https://console.deno.com) linked to it.
|
|
154
|
+
|
|
155
|
+
> [!IMPORTANT]
|
|
156
|
+
> Set **App Directory** to `worker` and **Entrypoint** to `main.ts`. If the app directory is left at the repository root, the build auto-detects the Node CLI in `src/` and fails. Leave install and build commands blank.
|
|
157
|
+
|
|
158
|
+
**From the CLI:**
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
deno install -gArf jsr:@deno/deployctl # one-time
|
|
162
|
+
cd worker
|
|
163
|
+
deno task dev # local test at http://localhost:8000
|
|
164
|
+
deno task deploy # deploys --prod, prints your *.deno.net host
|
|
139
165
|
```
|
|
140
166
|
|
|
167
|
+
> [!WARNING]
|
|
168
|
+
> Deno KV caps each value at 64 KiB, so payloads are limited to about 60 KB. A curated handoff is far smaller; if you hit the limit, attach fewer appendix messages.
|
|
169
|
+
|
|
141
170
|
## Supported agents
|
|
142
171
|
|
|
143
172
|
| Agent | Extraction | Notes |
|
|
144
173
|
| --- | --- | --- |
|
|
145
174
|
| **OpenCode** | `opencode session list` + `opencode export <id>` | Uses the native session-export CLI. |
|
|
146
175
|
| **pi** | reads `~/.pi/agent/sessions/<project>/*.jsonl` | No stdout JSON dump exists; reads the documented session transcript. |
|
|
147
|
-
| **Claude Code** | reads `~/.claude/projects/<project>/*.jsonl` |
|
|
176
|
+
| **Claude Code** | reads `~/.claude/projects/<project>/*.jsonl` | Reads the documented JSONL transcript. |
|
|
148
177
|
|
|
149
178
|
> [!TIP]
|
|
150
179
|
> Adding a new agent is a single file implementing the `AgentAdapter` interface (`getName()` + `extractSession()`). Register it in `src/adapters/index.ts`.
|
|
151
180
|
|
|
152
|
-
## The Context Handoff
|
|
181
|
+
## The Context Handoff document
|
|
153
182
|
|
|
154
|
-
Every export is formatted into a fixed Markdown structure so the receiving model gets actionable context immediately:
|
|
183
|
+
Every export is formatted into a fixed Markdown structure, so the receiving model gets actionable context immediately:
|
|
155
184
|
|
|
156
185
|
1. **Primary Objective**
|
|
157
186
|
2. **Current State & Blockers**
|
|
@@ -167,9 +196,10 @@ src/
|
|
|
167
196
|
index.ts CLI entry (commander)
|
|
168
197
|
core/
|
|
169
198
|
crypto.ts AES-256-GCM + scrypt
|
|
199
|
+
distiller.ts optional Gemini distillation
|
|
170
200
|
link.ts send-context:// codec
|
|
171
201
|
transport.ts upload/download client
|
|
172
|
-
formatter.ts Context Handoff
|
|
202
|
+
formatter.ts Context Handoff renderer
|
|
173
203
|
session-store.ts JSONL helpers
|
|
174
204
|
paths.ts exec.ts
|
|
175
205
|
adapters/ pi, claude, opencode + registry
|
|
@@ -183,5 +213,5 @@ worker/
|
|
|
183
213
|
|
|
184
214
|
- **CLI:** TypeScript, [commander](https://github.com/tj/commander.js), [@clack/prompts](https://github.com/bombshell-dev/clack)
|
|
185
215
|
- **Crypto:** Node.js built-in `crypto` (AES-256-GCM, scrypt)
|
|
186
|
-
- **Distillation (optional):** Google Gemini via its OpenAI-compatible Chat Completions endpoint
|
|
216
|
+
- **Distillation (optional):** Google Gemini via its OpenAI-compatible Chat Completions endpoint, with [jsonrepair](https://github.com/josdejong/jsonrepair) for resilient parsing of model output
|
|
187
217
|
- **Transport:** Deno Deploy + Deno KV
|
package/dist/core/distiller.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.geminiAvailable = geminiAvailable;
|
|
4
4
|
exports.distillSession = distillSession;
|
|
5
|
+
const jsonrepair_1 = require("jsonrepair");
|
|
5
6
|
/**
|
|
6
7
|
* Optional Gemini pass that distills a raw session into the structured
|
|
7
8
|
* Context Handoff sections, so the sender ships a dense brief instead of
|
|
@@ -70,13 +71,43 @@ function parseSections(content) {
|
|
|
70
71
|
}
|
|
71
72
|
function extractJson(content) {
|
|
72
73
|
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
74
|
+
const body = fenced ? fenced[1].trim() : content;
|
|
75
|
+
// Isolate the JSON from any surrounding prose, then let jsonrepair fix the
|
|
76
|
+
// common ways models still mangle it: trailing commas, single quotes,
|
|
77
|
+
// unquoted keys, truncation (missing closing brackets), and so on.
|
|
78
|
+
const start = body.indexOf("{");
|
|
79
|
+
const candidate = start === -1 ? body : (firstBalancedObject(body) ?? body.slice(start));
|
|
80
|
+
return (0, jsonrepair_1.jsonrepair)(candidate);
|
|
81
|
+
}
|
|
82
|
+
// Return the first brace-balanced JSON object, ignoring any prose or extra
|
|
83
|
+
// objects the model appends after it (thinking models often do). Tracks string
|
|
84
|
+
// literals and escapes so braces inside strings don't throw off the depth count.
|
|
85
|
+
function firstBalancedObject(text) {
|
|
86
|
+
const start = text.indexOf("{");
|
|
87
|
+
if (start === -1)
|
|
88
|
+
return null;
|
|
89
|
+
let depth = 0;
|
|
90
|
+
let inString = false;
|
|
91
|
+
let escaped = false;
|
|
92
|
+
for (let i = start; i < text.length; i++) {
|
|
93
|
+
const ch = text[i];
|
|
94
|
+
if (inString) {
|
|
95
|
+
if (escaped)
|
|
96
|
+
escaped = false;
|
|
97
|
+
else if (ch === "\\")
|
|
98
|
+
escaped = true;
|
|
99
|
+
else if (ch === '"')
|
|
100
|
+
inString = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"')
|
|
104
|
+
inString = true;
|
|
105
|
+
else if (ch === "{")
|
|
106
|
+
depth++;
|
|
107
|
+
else if (ch === "}" && --depth === 0)
|
|
108
|
+
return text.slice(start, i + 1);
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
80
111
|
}
|
|
81
112
|
function str(v) {
|
|
82
113
|
return typeof v === "string" ? v.trim() : "";
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,12 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const export_js_1 = require("./commands/export.js");
|
|
6
6
|
const receive_js_1 = require("./commands/receive.js");
|
|
7
7
|
const program = new commander_1.Command();
|
|
8
|
+
// Single source of truth: read the version release-it bumps in package.json.
|
|
9
|
+
const { version } = require("../package.json");
|
|
8
10
|
program
|
|
9
11
|
.name("send-context")
|
|
10
12
|
.description("Relay AI coding-agent session context between developers via an encrypted, ephemeral link.")
|
|
11
|
-
.version(
|
|
13
|
+
.version(version)
|
|
12
14
|
.enablePositionalOptions();
|
|
13
15
|
program
|
|
14
16
|
.command("export")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "send-context",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Agent-agnostic CLI to relay AI coding-agent session context between developers via an encrypted, ephemeral edge link.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@clack/prompts": "^0.7.0",
|
|
46
|
-
"commander": "^12.1.0"
|
|
46
|
+
"commander": "^12.1.0",
|
|
47
|
+
"jsonrepair": "^3.14.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/node": "^22.0.0",
|