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 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
- Debug, iterate, and manage your prompts across OpenAI, Anthropic, and Google — all from your terminal. No cloud, no accounts, no lock-in.
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
- ### Local First
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
- MIT-licensed. Pluggable provider adapters (OpenAI, Anthropic, Google). YAML-based config with `${ENV_VAR}` substitution.
22
-
23
- ### CLI & SDK
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 Gemini with SSE streaming
32
- - **Universal `.echo` format** — NDJSON-based, human-readable conversation protocol
33
- - **Smart Paste** — auto-detects and converts conversations from ChatGPT, Claude, Gemini, or raw text
34
- - **Timeline / history with branching** — revert to any point in a conversation
35
- - **Tool use support** — `tool_call` / `tool_result` parts
36
- - **Token counting** — tiktoken-based token estimation
37
- - **Image support** — inline images in messages (base64 or URL)
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
- ## Quick Start
43
-
44
- ### 1. Install skills
39
+ ## Getting Started
45
40
 
46
- EchoSpace ships with agent skills that work with any coding agent — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [OpenAI Codex](https://openai.com/index/codex/), or any tool that supports the skills/SKILL.md convention.
41
+ ### 1. Initialize
47
42
 
48
43
  ```bash
49
- npx skills add https://github.com/stonexer/echospace/tree/master/skills/echospace
44
+ npx echospace@alpha init
50
45
  ```
51
46
 
52
- ### 2. Configure providers
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
- Or use the agent skill: `/echospace:init`
62
+ Install agent skills for your coding agent? (Claude Code, Codex, etc.)
63
+ │ ● Yes / ○ No
73
64
 
74
- ### 3. Launch
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
- This will:
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
- EchoSpace v0.1.0
95
- ╠══════════════════════════════════════╣
96
- Workspace: /my-project/.echo
97
- URL: http://localhost:3240
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
- On first launch, EchoSpace creates `~/.echospace/config.yaml`. Fill in the `api_key` for the providers you want to use — unconfigured providers are automatically ignored.
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 also supported for API keys — they are resolved from your environment at runtime. `.env` files in your project directory are auto-loaded.
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 toOpenAIMessage(msg) {
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
- const tr = toolResults[0];
341
- result.content = typeof tr.output === "string" ? tr.output : JSON.stringify(tr.output);
342
- result.tool_call_id = tr.tool_call_id;
343
- return result;
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.content = textParts.map((p2) => p2.text).join("") || null;
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
- result.content = textParts.map((p2) => p2.text).join("");
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.map(toOpenAIMessage),
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.trim()) return "URL is required";
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
- p.outro("You're all set! Run `npx echospace` to start.");
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.3";
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("--no-open", "Don't open browser automatically").action(async (workdir, options) => {
1031
- const baseDir = path5.resolve(process.cwd(), workdir);
1032
- const workspaceDir = path5.join(baseDir, ".echo");
1033
- loadEnvFile(baseDir);
1034
- loadEnvFile(process.cwd());
1035
- if (!fs3.existsSync(workspaceDir)) {
1036
- fs3.mkdirSync(workspaceDir, { recursive: true });
1037
- }
1038
- fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
1039
- ensureConfig(CONFIG_DIR2);
1040
- const port = options.port ? parseInt(options.port, 10) : await getPort({ port: portNumbers(3240, 3249) });
1041
- const url = `http://localhost:${port}`;
1042
- console.log(`
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
- const isDev = process.env.NODE_ENV !== "production" && import.meta.url.includes("/src/");
1052
- startServer({ port, workspaceDir, configDir: CONFIG_DIR2, dev: isDev });
1053
- if (options.open) {
1054
- await open(`http://localhost:${port}`);
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
- const shutdown = () => {
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();