echospace 0.1.0-alpha.3 → 0.1.0-alpha.6
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 +74 -49
- package/dist/cli/index.js +64 -42
- package/dist/client/assets/index-BTjnLaRd.js +151 -0
- package/dist/client/assets/index-Dt4ZMQMX.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/core/chunk-OSTGBTLV.js +110 -0
- package/dist/core/echo/index.js +1 -1
- package/dist/core/providers/index.js +13 -11
- package/dist/core/smart-paste/index.js +5 -5
- package/package.json +1 -1
- package/dist/client/assets/index-gZkfzHQ0.css +0 -1
- package/dist/client/assets/index-ja1djPyy.js +0 -168
package/README.md
CHANGED
|
@@ -6,54 +6,45 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><strong>The open-source, local-first prompt debugging workspace for LLM developers.</strong></p>
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
Debug, iterate, and manage your prompts across multiple LLM providers — all from your terminal.<br/>
|
|
11
|
+
No cloud. No accounts. No lock-in.
|
|
12
|
+
</p>
|
|
10
13
|
|
|
11
14
|
---
|
|
12
15
|
|
|
13
16
|
## Why EchoSpace?
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
All data stored as `.echo` files on disk. No cloud, no accounts. Your prompts stay on your machine, version-controlled alongside your code.
|
|
18
|
-
|
|
19
|
-
### Open Source & Customizable
|
|
18
|
+
Prompt engineering is iterative, but most tools make you choose between a polished UI and full control over your data. EchoSpace gives you both: a browser-based workspace that runs entirely on your machine, stores conversations as plain files you can version-control, and works with any OpenAI-compatible provider out of the box.
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`npx echospace@alpha` to launch. Core TypeScript modules (`parseEcho`, `serializeEcho`, `smartParse`, provider registry) can be imported as a library for building your own tools.
|
|
20
|
+
- **Local-first** — all data stored as `.echo` files on disk, version-controlled alongside your code
|
|
21
|
+
- **Open source** — MIT-licensed, pluggable provider adapters, `${ENV_VAR}` substitution in config
|
|
22
|
+
- **CLI & library** — `npx echospace@alpha` to launch; core modules (`parseEcho`, `serializeEcho`, `smartParse`, provider registry) are importable as a library
|
|
26
23
|
|
|
27
24
|
---
|
|
28
25
|
|
|
29
26
|
## Features
|
|
30
27
|
|
|
31
|
-
- **Multi-provider streaming** — OpenAI, Anthropic, Google
|
|
32
|
-
- **Universal `.echo` format** — NDJSON-based, human-readable conversation protocol
|
|
33
|
-
- **Smart Paste** — auto-detects and converts
|
|
34
|
-
- **Timeline
|
|
35
|
-
- **Tool use support** — `tool_call` / `tool_result` parts
|
|
36
|
-
- **Token counting** — tiktoken-based token estimation
|
|
37
|
-
- **Image support** — inline images in messages
|
|
28
|
+
- **Multi-provider streaming** — SSE streaming across all configured providers (OpenAI, Anthropic, Google, or any OpenAI-compatible gateway)
|
|
29
|
+
- **Universal `.echo` format** — NDJSON-based, human-readable conversation protocol designed for diffing and version control
|
|
30
|
+
- **Smart Paste** — auto-detects and converts conversation exports from major LLM platforms or raw text
|
|
31
|
+
- **Timeline & branching** — revert to any point in a conversation, fork branches, compare results
|
|
32
|
+
- **Tool use support** — first-class `tool_call` / `tool_result` message parts
|
|
33
|
+
- **Token counting** — tiktoken-based token estimation shown in real time
|
|
34
|
+
- **Image support** — inline images in messages via base64 or URL
|
|
38
35
|
- **Per-conversation model settings** — temperature, max_tokens, top_p, response_format, JSON schema, tools
|
|
39
36
|
|
|
40
37
|
---
|
|
41
38
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
### 1. Install skills
|
|
39
|
+
## Getting Started
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
### 1. Initialize
|
|
47
42
|
|
|
48
43
|
```bash
|
|
49
|
-
npx
|
|
44
|
+
npx echospace@alpha init
|
|
50
45
|
```
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
npx echospace init
|
|
56
|
-
```
|
|
47
|
+
The interactive setup wizard configures your provider and API key:
|
|
57
48
|
|
|
58
49
|
```
|
|
59
50
|
◆ Which LLM service do you use?
|
|
@@ -67,11 +58,14 @@ npx echospace init
|
|
|
67
58
|
│ sk-xxxxxxxx
|
|
68
59
|
|
|
69
60
|
✓ Config saved to ~/.echospace/config.yaml
|
|
70
|
-
```
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
◆ Install agent skills for your coding agent? (Claude Code, Codex, etc.)
|
|
63
|
+
│ ● Yes / ○ No
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
✓ Skills installed!
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Launch
|
|
75
69
|
|
|
76
70
|
```bash
|
|
77
71
|
npx echospace@alpha
|
|
@@ -80,32 +74,25 @@ npx echospace@alpha
|
|
|
80
74
|
Or install globally:
|
|
81
75
|
|
|
82
76
|
```bash
|
|
83
|
-
pnpm add -g echospace
|
|
77
|
+
pnpm add -g echospace@alpha
|
|
84
78
|
echospace ./my-project
|
|
85
79
|
```
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
EchoSpace will:
|
|
88
82
|
|
|
89
83
|
1. Create a `.echo/` workspace in your project directory
|
|
90
84
|
2. Start a local server and open the UI in your browser
|
|
91
85
|
|
|
92
86
|
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
██████████████
|
|
88
|
+
██ ██ EchoSpace v0.1.0
|
|
89
|
+
██ ██████ ██
|
|
90
|
+
██ ██ ██ ██ Workspace /my-project/.echo
|
|
91
|
+
██ ██████ ██ URL http://localhost:3240
|
|
92
|
+
██ ██
|
|
93
|
+
██████████████
|
|
99
94
|
```
|
|
100
95
|
|
|
101
|
-
### Available Skills
|
|
102
|
-
|
|
103
|
-
| Skill | Description |
|
|
104
|
-
|-------|-------------|
|
|
105
|
-
| `/echospace:init` | Interactive setup wizard — select providers, enter API keys, generate config |
|
|
106
|
-
| `/echospace:export` | Convert conversation files (OpenAI, Anthropic, Google, Helicone, raw text) into `.echo` format |
|
|
107
|
-
| `/echospace:integrate` | Integrate `.echo` export into your own app |
|
|
108
|
-
|
|
109
96
|
### CLI Options
|
|
110
97
|
|
|
111
98
|
```
|
|
@@ -116,6 +103,26 @@ echospace [workdir] [options]
|
|
|
116
103
|
--no-open Don't open browser automatically
|
|
117
104
|
```
|
|
118
105
|
|
|
106
|
+
### Agent Skills
|
|
107
|
+
|
|
108
|
+
| Skill | Description |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `/echospace-export` | Convert conversation exports from major LLM platforms into `.echo` format |
|
|
111
|
+
| `/echospace-integrate` | Integrate a `.echo` export into your own app |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Tech Stack
|
|
116
|
+
|
|
117
|
+
| Layer | Technology |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| Frontend | React 19, Tailwind CSS v4, Zustand, CodeMirror |
|
|
120
|
+
| Backend | Hono, Node.js |
|
|
121
|
+
| Build | Vite, tsup, TypeScript 5 |
|
|
122
|
+
| Testing | Vitest |
|
|
123
|
+
| Tokenization | tiktoken |
|
|
124
|
+
| Package manager | pnpm |
|
|
125
|
+
|
|
119
126
|
---
|
|
120
127
|
|
|
121
128
|
## The `.echo` Format
|
|
@@ -134,7 +141,7 @@ Each message can contain multiple part types: `text`, `thinking`, `tool_call`, `
|
|
|
134
141
|
|
|
135
142
|
## Configuration
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
Run `npx echospace@alpha init` to create `~/.echospace/config.yaml`, or edit it manually:
|
|
138
145
|
|
|
139
146
|
```yaml
|
|
140
147
|
providers:
|
|
@@ -165,9 +172,26 @@ providers:
|
|
|
165
172
|
- gemini-2.5-pro
|
|
166
173
|
- gemini-2.5-flash
|
|
167
174
|
- gemini-2.0-flash
|
|
175
|
+
|
|
176
|
+
- name: vercel-ai-gateway
|
|
177
|
+
type: openai
|
|
178
|
+
api_key: xxxxxxxx
|
|
179
|
+
base_url: https://ai-gateway.vercel.sh/v1
|
|
180
|
+
models:
|
|
181
|
+
- anthropic/claude-sonnet-4-6
|
|
182
|
+
- openai/gpt-4.1
|
|
183
|
+
- google/gemini-2.5-pro
|
|
184
|
+
|
|
185
|
+
# Custom OpenAI-compatible gateway
|
|
186
|
+
- name: custom
|
|
187
|
+
type: openai # or: anthropic, google
|
|
188
|
+
api_key: xxxxxxxx
|
|
189
|
+
base_url: https://my-proxy.example.com/v1
|
|
190
|
+
models:
|
|
191
|
+
- my-model-name
|
|
168
192
|
```
|
|
169
193
|
|
|
170
|
-
> **Tip:** `${ENV_VAR}` syntax is
|
|
194
|
+
> **Tip:** `${ENV_VAR}` syntax is supported for API keys — values are resolved from your environment at runtime. `.env` files in your project directory are auto-loaded.
|
|
171
195
|
|
|
172
196
|
---
|
|
173
197
|
|
|
@@ -185,6 +209,7 @@ pnpm build # Build for production
|
|
|
185
209
|
pnpm typecheck # Type-check without emitting
|
|
186
210
|
pnpm lint # ESLint
|
|
187
211
|
pnpm format # Prettier
|
|
212
|
+
pnpm test # Run tests
|
|
188
213
|
```
|
|
189
214
|
|
|
190
215
|
---
|
package/dist/cli/index.js
CHANGED
|
@@ -330,20 +330,23 @@ var googleAdapter = {
|
|
|
330
330
|
};
|
|
331
331
|
|
|
332
332
|
// src/core/providers/openai.ts
|
|
333
|
-
function
|
|
334
|
-
const result = { role: msg.role };
|
|
333
|
+
function toOpenAIMessages(msg) {
|
|
335
334
|
const textParts = msg.parts.filter((p2) => p2.type === "text");
|
|
336
335
|
const toolCalls = msg.parts.filter((p2) => p2.type === "tool_call");
|
|
337
336
|
const toolResults = msg.parts.filter((p2) => p2.type === "tool_result");
|
|
338
337
|
const thinkingParts = msg.parts.filter((p2) => p2.type === "thinking");
|
|
339
338
|
if (msg.role === "tool" && toolResults.length > 0) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
return toolResults.map((tr) => ({
|
|
340
|
+
role: "tool",
|
|
341
|
+
tool_call_id: tr.tool_call_id,
|
|
342
|
+
content: typeof tr.output === "string" ? tr.output : JSON.stringify(tr.output)
|
|
343
|
+
}));
|
|
344
344
|
}
|
|
345
345
|
if (msg.role === "assistant") {
|
|
346
|
-
result
|
|
346
|
+
const result = {
|
|
347
|
+
role: "assistant",
|
|
348
|
+
content: textParts.map((p2) => p2.text).join("") || null
|
|
349
|
+
};
|
|
347
350
|
if (thinkingParts.length > 0) {
|
|
348
351
|
result.reasoning_content = thinkingParts.map((p2) => p2.text).join("");
|
|
349
352
|
}
|
|
@@ -357,10 +360,9 @@ function toOpenAIMessage(msg) {
|
|
|
357
360
|
}
|
|
358
361
|
}));
|
|
359
362
|
}
|
|
360
|
-
return result;
|
|
363
|
+
return [result];
|
|
361
364
|
}
|
|
362
|
-
|
|
363
|
-
return result;
|
|
365
|
+
return [{ role: msg.role, content: textParts.map((p2) => p2.text).join("") }];
|
|
364
366
|
}
|
|
365
367
|
var openaiAdapter = {
|
|
366
368
|
type: "openai",
|
|
@@ -369,7 +371,7 @@ var openaiAdapter = {
|
|
|
369
371
|
const url = `${baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
370
372
|
const body = {
|
|
371
373
|
model: settings.model ?? config.models[0],
|
|
372
|
-
messages: messages.
|
|
374
|
+
messages: messages.flatMap(toOpenAIMessages),
|
|
373
375
|
stream: true
|
|
374
376
|
};
|
|
375
377
|
if (settings.temperature != null) body.temperature = settings.temperature;
|
|
@@ -592,7 +594,7 @@ function normalizeMessage(message) {
|
|
|
592
594
|
const needsPartNormalization = msg.parts.some((p2) => {
|
|
593
595
|
if (p2.type !== "tool_result") return false;
|
|
594
596
|
const r = p2;
|
|
595
|
-
return !r.tool_call_id && (r.id || r.tool_use_id);
|
|
597
|
+
return !r.tool_call_id && (r.id || r.tool_use_id || r.call_id);
|
|
596
598
|
});
|
|
597
599
|
if (needsPartNormalization) {
|
|
598
600
|
msg = {
|
|
@@ -600,9 +602,9 @@ function normalizeMessage(message) {
|
|
|
600
602
|
parts: msg.parts.map((p2) => {
|
|
601
603
|
if (p2.type !== "tool_result") return p2;
|
|
602
604
|
const r = p2;
|
|
603
|
-
if (!r.tool_call_id && (r.id || r.tool_use_id)) {
|
|
604
|
-
const { id, tool_use_id, ...rest } = r;
|
|
605
|
-
return { ...rest, tool_call_id: id ?? tool_use_id };
|
|
605
|
+
if (!r.tool_call_id && (r.id || r.tool_use_id || r.call_id)) {
|
|
606
|
+
const { id, tool_use_id, call_id, ...rest } = r;
|
|
607
|
+
return { ...rest, tool_call_id: id ?? tool_use_id ?? call_id };
|
|
606
608
|
}
|
|
607
609
|
return p2;
|
|
608
610
|
})
|
|
@@ -864,6 +866,7 @@ function startServer(options) {
|
|
|
864
866
|
|
|
865
867
|
// src/cli/init.ts
|
|
866
868
|
import * as p from "@clack/prompts";
|
|
869
|
+
import { execSync } from "child_process";
|
|
867
870
|
import { homedir } from "os";
|
|
868
871
|
import path4 from "path";
|
|
869
872
|
var CONFIG_DIR = path4.join(homedir(), ".echospace");
|
|
@@ -940,7 +943,7 @@ async function runInit() {
|
|
|
940
943
|
const baseUrl = await p.text({
|
|
941
944
|
message: "Base URL (e.g. https://my-proxy.example.com/v1):",
|
|
942
945
|
validate: (v) => {
|
|
943
|
-
if (!v
|
|
946
|
+
if (!v?.trim()) return "URL is required";
|
|
944
947
|
try {
|
|
945
948
|
new URL(v.trim());
|
|
946
949
|
} catch {
|
|
@@ -1004,11 +1007,25 @@ async function runInit() {
|
|
|
1004
1007
|
saveConfig(CONFIG_DIR, config);
|
|
1005
1008
|
const configPath = path4.join(CONFIG_DIR, "config.yaml");
|
|
1006
1009
|
p.log.success(`Config saved to ${configPath}`);
|
|
1007
|
-
|
|
1010
|
+
const installSkills = await p.confirm({
|
|
1011
|
+
message: "Install agent skills for Claude Code / Cursor? (npx skills add stonexer/echospace)"
|
|
1012
|
+
});
|
|
1013
|
+
if (installSkills && !isCancel2(installSkills)) {
|
|
1014
|
+
const s = p.spinner();
|
|
1015
|
+
s.start("Installing skills...");
|
|
1016
|
+
try {
|
|
1017
|
+
execSync("npx skills add stonexer/echospace", { stdio: "pipe" });
|
|
1018
|
+
s.stop("Skills installed!");
|
|
1019
|
+
} catch {
|
|
1020
|
+
s.stop("Failed to install skills");
|
|
1021
|
+
p.log.warn("Run manually: npx skills add stonexer/echospace");
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
p.outro("You're all set! Run `npx echospace@alpha` to start.");
|
|
1008
1025
|
}
|
|
1009
1026
|
|
|
1010
1027
|
// src/cli/index.ts
|
|
1011
|
-
var VERSION = "0.1.0-alpha.
|
|
1028
|
+
var VERSION = "0.1.0-alpha.6";
|
|
1012
1029
|
var CONFIG_DIR2 = path5.join(homedir2(), ".echospace");
|
|
1013
1030
|
function loadEnvFile(dir) {
|
|
1014
1031
|
const envPath = path5.join(dir, ".env");
|
|
@@ -1027,19 +1044,23 @@ function loadEnvFile(dir) {
|
|
|
1027
1044
|
}
|
|
1028
1045
|
}
|
|
1029
1046
|
var program = new Command();
|
|
1030
|
-
program.name("echospace").description("The best open-source local prompt debugging tool").version(VERSION).argument("[workdir]", "Workspace directory", ".").option("-p, --port <port>", "Port to serve on").option(
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1047
|
+
program.name("echospace").description("The best open-source local prompt debugging tool").version(VERSION).argument("[workdir]", "Workspace directory", ".").option("-p, --port <port>", "Port to serve on").option(
|
|
1048
|
+
"-d, --dir <path>",
|
|
1049
|
+
"Open this directory directly as the workspace (skips the .echo subdir)"
|
|
1050
|
+
).option("--no-open", "Don't open browser automatically").action(
|
|
1051
|
+
async (workdir, options) => {
|
|
1052
|
+
const baseDir = path5.resolve(process.cwd(), workdir);
|
|
1053
|
+
const workspaceDir = options.dir ? path5.resolve(process.cwd(), options.dir) : path5.join(baseDir, ".echo");
|
|
1054
|
+
loadEnvFile(baseDir);
|
|
1055
|
+
loadEnvFile(process.cwd());
|
|
1056
|
+
if (!fs3.existsSync(workspaceDir)) {
|
|
1057
|
+
fs3.mkdirSync(workspaceDir, { recursive: true });
|
|
1058
|
+
}
|
|
1059
|
+
fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
1060
|
+
ensureConfig(CONFIG_DIR2);
|
|
1061
|
+
const port = options.port ? parseInt(options.port, 10) : await getPort({ port: portNumbers(3240, 3249) });
|
|
1062
|
+
const url = `http://localhost:${port}`;
|
|
1063
|
+
console.log(`
|
|
1043
1064
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
1044
1065
|
\u2588\u2588 \u2588\u2588 EchoSpace v${VERSION}
|
|
1045
1066
|
\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588
|
|
@@ -1048,17 +1069,18 @@ program.name("echospace").description("The best open-source local prompt debuggi
|
|
|
1048
1069
|
\u2588\u2588 \u2588\u2588
|
|
1049
1070
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588
|
|
1050
1071
|
`);
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1072
|
+
const isDev = process.env.NODE_ENV !== "production" && import.meta.url.includes("/src/");
|
|
1073
|
+
startServer({ port, workspaceDir, configDir: CONFIG_DIR2, dev: isDev });
|
|
1074
|
+
if (options.open) {
|
|
1075
|
+
await open(`http://localhost:${port}`);
|
|
1076
|
+
}
|
|
1077
|
+
const shutdown = () => {
|
|
1078
|
+
console.log("\nShutting down EchoSpace...");
|
|
1079
|
+
process.exit(0);
|
|
1080
|
+
};
|
|
1081
|
+
process.on("SIGINT", shutdown);
|
|
1082
|
+
process.on("SIGTERM", shutdown);
|
|
1055
1083
|
}
|
|
1056
|
-
|
|
1057
|
-
console.log("\nShutting down EchoSpace...");
|
|
1058
|
-
process.exit(0);
|
|
1059
|
-
};
|
|
1060
|
-
process.on("SIGINT", shutdown);
|
|
1061
|
-
process.on("SIGTERM", shutdown);
|
|
1062
|
-
});
|
|
1084
|
+
);
|
|
1063
1085
|
program.command("init").description("Interactive setup for LLM provider configuration").action(runInit);
|
|
1064
1086
|
program.parse();
|