echospace 0.0.1 → 0.1.0-alpha.1

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.
@@ -0,0 +1,405 @@
1
+ import {
2
+ parseEcho
3
+ } from "../chunk-U4VEJP3N.js";
4
+
5
+ // src/core/smart-paste/detector.ts
6
+ function detectFormat(input) {
7
+ const trimmed = input.trim();
8
+ try {
9
+ const parsed = JSON.parse(trimmed);
10
+ if (parsed.request?.prompt && Array.isArray(parsed.request.prompt)) {
11
+ return "vercel";
12
+ }
13
+ if (parsed.request?.messages && Array.isArray(parsed.request.messages)) {
14
+ return "openai";
15
+ }
16
+ if (Array.isArray(parsed)) {
17
+ if (parsed.length > 0 && parsed[0].role && "content" in parsed[0]) {
18
+ return "openai";
19
+ }
20
+ }
21
+ if (parsed.messages && Array.isArray(parsed.messages)) {
22
+ return "openai";
23
+ }
24
+ if (parsed.content && Array.isArray(parsed.content)) {
25
+ if (parsed.content[0]?.type === "text") {
26
+ return "anthropic";
27
+ }
28
+ }
29
+ if (Array.isArray(parsed) && parsed[0]?.content?.[0]?.type === "text") {
30
+ return "anthropic";
31
+ }
32
+ if (parsed.contents && Array.isArray(parsed.contents)) {
33
+ if (parsed.contents[0]?.parts) {
34
+ return "google";
35
+ }
36
+ }
37
+ return "unknown";
38
+ } catch {
39
+ }
40
+ const firstLine = trimmed.split("\n")[0]?.trim() ?? "";
41
+ try {
42
+ const first = JSON.parse(firstLine);
43
+ if (first.kind === "meta" || first.kind === "message") {
44
+ return "echo";
45
+ }
46
+ } catch {
47
+ }
48
+ if (/^(User|Human|System|Assistant):/im.test(trimmed) || /^(###\s*)?(User|Human|System|Assistant)/im.test(trimmed)) {
49
+ return "raw";
50
+ }
51
+ return "unknown";
52
+ }
53
+
54
+ // src/core/smart-paste/parsers.ts
55
+ import { nanoid } from "nanoid";
56
+ function parseOpenAI(input) {
57
+ const parsed = JSON.parse(input);
58
+ const source = parsed.request?.messages ? parsed.request : parsed;
59
+ const requestMessages = Array.isArray(source) ? source : source.messages ?? [];
60
+ const messages = [...requestMessages];
61
+ const respMessages = parsed.response?.messages ?? parsed.response?.choices;
62
+ if (Array.isArray(respMessages)) {
63
+ for (const item of respMessages) {
64
+ messages.push(item.message ?? item);
65
+ }
66
+ }
67
+ return messages.map((msg) => {
68
+ const parts = [];
69
+ const role = msg.role ?? "user";
70
+ if (typeof msg.content === "string") {
71
+ parts.push({ type: "text", text: msg.content });
72
+ } else if (Array.isArray(msg.content)) {
73
+ for (const block of msg.content) {
74
+ if (block.type === "text") {
75
+ parts.push({ type: "text", text: block.text });
76
+ } else if (block.type === "image_url") {
77
+ parts.push({ type: "image", url: block.image_url?.url });
78
+ }
79
+ }
80
+ }
81
+ if (typeof msg.reasoning_content === "string" && msg.reasoning_content) {
82
+ parts.unshift({ type: "thinking", text: msg.reasoning_content });
83
+ }
84
+ if (Array.isArray(msg.tool_calls)) {
85
+ for (const tc of msg.tool_calls) {
86
+ parts.push({
87
+ type: "tool_call",
88
+ id: tc.id ?? nanoid(8),
89
+ name: tc.function?.name ?? "",
90
+ input: tc.function?.arguments ?? ""
91
+ });
92
+ }
93
+ }
94
+ if (role === "tool" && typeof msg.tool_call_id === "string") {
95
+ parts.length = 0;
96
+ parts.push({
97
+ type: "tool_result",
98
+ id: msg.tool_call_id,
99
+ output: msg.content ?? ""
100
+ });
101
+ }
102
+ return {
103
+ kind: "message",
104
+ id: msg.id ?? nanoid(8),
105
+ role,
106
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
107
+ parts
108
+ };
109
+ });
110
+ }
111
+ function parseAnthropic(input) {
112
+ const parsed = JSON.parse(input);
113
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
114
+ const result = [];
115
+ if (!Array.isArray(parsed) && parsed.system) {
116
+ result.push({
117
+ kind: "message",
118
+ id: nanoid(8),
119
+ role: "system",
120
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
121
+ parts: [
122
+ {
123
+ type: "text",
124
+ text: typeof parsed.system === "string" ? parsed.system : parsed.system.filter((b) => b.type === "text").map((b) => b.text).join("\n")
125
+ }
126
+ ]
127
+ });
128
+ }
129
+ const msgList = !Array.isArray(parsed) && parsed.messages ? parsed.messages : messages;
130
+ for (const msg of msgList) {
131
+ const parts = [];
132
+ const role = msg.role ?? "user";
133
+ if (typeof msg.content === "string") {
134
+ parts.push({ type: "text", text: msg.content });
135
+ } else if (Array.isArray(msg.content)) {
136
+ for (const block of msg.content) {
137
+ switch (block.type) {
138
+ case "text":
139
+ parts.push({ type: "text", text: block.text });
140
+ break;
141
+ case "thinking":
142
+ parts.push({
143
+ type: "thinking",
144
+ text: block.thinking ?? ""
145
+ });
146
+ break;
147
+ case "tool_use":
148
+ parts.push({
149
+ type: "tool_call",
150
+ id: block.id ?? nanoid(8),
151
+ name: block.name ?? "",
152
+ input: block.input ?? {}
153
+ });
154
+ break;
155
+ case "tool_result":
156
+ parts.push({
157
+ type: "tool_result",
158
+ id: block.tool_use_id ?? "",
159
+ output: block.content ?? "",
160
+ is_error: block.is_error
161
+ });
162
+ break;
163
+ case "image":
164
+ if (block.source?.type === "base64") {
165
+ const source = block.source;
166
+ parts.push({
167
+ type: "image",
168
+ base64: source.data,
169
+ media_type: source.media_type
170
+ });
171
+ }
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ result.push({
177
+ kind: "message",
178
+ id: msg.id ?? nanoid(8),
179
+ role,
180
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
181
+ parts
182
+ });
183
+ }
184
+ return result;
185
+ }
186
+ function parseGoogle(input) {
187
+ const parsed = JSON.parse(input);
188
+ const contents = parsed.contents ?? [];
189
+ const result = [];
190
+ if (parsed.systemInstruction?.parts) {
191
+ result.push({
192
+ kind: "message",
193
+ id: nanoid(8),
194
+ role: "system",
195
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
196
+ parts: parsed.systemInstruction.parts.map((p) => ({
197
+ type: "text",
198
+ text: p.text ?? ""
199
+ }))
200
+ });
201
+ }
202
+ for (const content of contents) {
203
+ const role = content.role === "model" ? "assistant" : "user";
204
+ const geminiParts = content.parts ?? [];
205
+ const echoParts = [];
206
+ for (const p of geminiParts) {
207
+ if (p.text) {
208
+ echoParts.push({ type: "text", text: p.text });
209
+ }
210
+ if (p.functionCall) {
211
+ const fc = p.functionCall;
212
+ echoParts.push({
213
+ type: "tool_call",
214
+ id: fc.name ?? nanoid(8),
215
+ name: fc.name ?? "",
216
+ input: fc.args ?? {}
217
+ });
218
+ }
219
+ if (p.functionResponse) {
220
+ const fr = p.functionResponse;
221
+ echoParts.push({
222
+ type: "tool_result",
223
+ id: fr.name ?? "",
224
+ output: fr.response ?? ""
225
+ });
226
+ }
227
+ }
228
+ result.push({
229
+ kind: "message",
230
+ id: nanoid(8),
231
+ role,
232
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
233
+ parts: echoParts
234
+ });
235
+ }
236
+ return result;
237
+ }
238
+ function parseVercel(input) {
239
+ const parsed = JSON.parse(input);
240
+ const prompt = parsed.request?.prompt ?? [];
241
+ const response = parsed.response;
242
+ const result = [];
243
+ for (const msg of prompt) {
244
+ const role = msg.role ?? "user";
245
+ const parts = convertVercelParts(msg.content);
246
+ result.push({
247
+ kind: "message",
248
+ id: nanoid(8),
249
+ role,
250
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
251
+ parts
252
+ });
253
+ }
254
+ if (response?.choices && Array.isArray(response.choices)) {
255
+ const choice = response.choices[0];
256
+ const respMsg = choice?.message;
257
+ if (respMsg) {
258
+ const parts = convertVercelParts(respMsg.content);
259
+ if (parts.length > 0) {
260
+ result.push({
261
+ kind: "message",
262
+ id: nanoid(8),
263
+ role: "assistant",
264
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
265
+ parts,
266
+ meta: {
267
+ model: response.model,
268
+ usage: response.usage ? {
269
+ input_tokens: response.usage.promptTokens,
270
+ output_tokens: response.usage.completionTokens
271
+ } : void 0
272
+ }
273
+ });
274
+ }
275
+ }
276
+ }
277
+ return result;
278
+ }
279
+ function convertVercelParts(content) {
280
+ const parts = [];
281
+ if (typeof content === "string") {
282
+ parts.push({ type: "text", text: content });
283
+ return parts;
284
+ }
285
+ if (!Array.isArray(content)) return parts;
286
+ for (const block of content) {
287
+ switch (block.type) {
288
+ case "text":
289
+ parts.push({ type: "text", text: block.text });
290
+ break;
291
+ case "tool-call":
292
+ parts.push({
293
+ type: "tool_call",
294
+ id: block.toolCallId ?? nanoid(8),
295
+ name: block.toolName ?? "",
296
+ input: block.input ?? block.args ?? {}
297
+ });
298
+ break;
299
+ case "tool-result": {
300
+ let output = block.output ?? block.result ?? "";
301
+ if (output && typeof output === "object" && output.type === "json") {
302
+ output = output.value ?? output;
303
+ }
304
+ parts.push({
305
+ type: "tool_result",
306
+ id: block.toolCallId ?? "",
307
+ output
308
+ });
309
+ break;
310
+ }
311
+ case "image":
312
+ parts.push({
313
+ type: "image",
314
+ url: block.url,
315
+ base64: block.base64,
316
+ media_type: block.mediaType
317
+ });
318
+ break;
319
+ case "reasoning":
320
+ parts.push({
321
+ type: "thinking",
322
+ text: block.text ?? ""
323
+ });
324
+ break;
325
+ }
326
+ }
327
+ return parts;
328
+ }
329
+ function parseRaw(input) {
330
+ const lines = input.split("\n");
331
+ const messages = [];
332
+ let currentRole = "user";
333
+ let currentContent = "";
334
+ const rolePatterns = [
335
+ [/^(System|system)\s*:/i, "system"],
336
+ [/^(User|Human|human|user)\s*:/i, "user"],
337
+ [/^(Assistant|AI|Bot|assistant|ai|bot)\s*:/i, "assistant"]
338
+ ];
339
+ function flush() {
340
+ const text = currentContent.trim();
341
+ if (text) {
342
+ messages.push({
343
+ kind: "message",
344
+ id: nanoid(8),
345
+ role: currentRole,
346
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
347
+ parts: [{ type: "text", text }]
348
+ });
349
+ }
350
+ currentContent = "";
351
+ }
352
+ for (const line of lines) {
353
+ let matched = false;
354
+ for (const [pattern, role] of rolePatterns) {
355
+ const match = line.match(pattern);
356
+ if (match) {
357
+ flush();
358
+ currentRole = role;
359
+ currentContent = line.slice(match[0].length).trim() + "\n";
360
+ matched = true;
361
+ break;
362
+ }
363
+ }
364
+ if (!matched) {
365
+ currentContent += line + "\n";
366
+ }
367
+ }
368
+ flush();
369
+ return messages;
370
+ }
371
+
372
+ // src/core/smart-paste/index.ts
373
+ function smartParse(input) {
374
+ const format = detectFormat(input);
375
+ switch (format) {
376
+ case "openai":
377
+ return parseOpenAI(input);
378
+ case "anthropic":
379
+ return parseAnthropic(input);
380
+ case "google":
381
+ return parseGoogle(input);
382
+ case "vercel":
383
+ return parseVercel(input);
384
+ case "echo": {
385
+ const conversation = parseEcho(input);
386
+ return conversation.messages;
387
+ }
388
+ case "raw":
389
+ return parseRaw(input);
390
+ case "unknown":
391
+ return [
392
+ {
393
+ kind: "message",
394
+ id: crypto.randomUUID().slice(0, 8),
395
+ role: "user",
396
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
397
+ parts: [{ type: "text", text: input.trim() }]
398
+ }
399
+ ];
400
+ }
401
+ }
402
+ export {
403
+ detectFormat,
404
+ smartParse
405
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Echo format types — the universal .echo message protocol.
3
+ *
4
+ * An .echo file is NDJSON (newline-delimited JSON).
5
+ * Line 1 is always an EchoMeta record.
6
+ * Subsequent lines are EchoMessage records.
7
+ */
8
+ interface TextPart {
9
+ type: "text";
10
+ text: string;
11
+ }
12
+ interface ThinkingPart {
13
+ type: "thinking";
14
+ text: string;
15
+ }
16
+ interface ToolCallPart {
17
+ type: "tool_call";
18
+ id: string;
19
+ name: string;
20
+ input: unknown;
21
+ }
22
+ interface ToolResultPart {
23
+ type: "tool_result";
24
+ id: string;
25
+ output: unknown;
26
+ is_error?: boolean;
27
+ }
28
+ interface ImagePart {
29
+ type: "image";
30
+ url?: string;
31
+ base64?: string;
32
+ media_type?: string;
33
+ }
34
+ type EchoPart = TextPart | ThinkingPart | ToolCallPart | ToolResultPart | ImagePart;
35
+ interface EchoToolDefinition {
36
+ name: string;
37
+ description?: string;
38
+ parameters: Record<string, unknown>;
39
+ strict?: boolean;
40
+ }
41
+ interface EchoSettings {
42
+ provider?: string;
43
+ model?: string;
44
+ temperature?: number;
45
+ max_tokens?: number;
46
+ top_p?: number;
47
+ response_format?: "text" | "json_object" | "json_schema";
48
+ json_schema?: {
49
+ name: string;
50
+ schema: Record<string, unknown>;
51
+ strict?: boolean;
52
+ };
53
+ tools?: EchoToolDefinition[];
54
+ }
55
+ type EchoRole = "system" | "user" | "assistant" | "tool";
56
+ interface EchoMeta {
57
+ kind: "meta";
58
+ v: 1;
59
+ id: string;
60
+ title?: string;
61
+ created_at: string;
62
+ settings?: EchoSettings;
63
+ }
64
+ interface EchoMessage {
65
+ kind: "message";
66
+ id: string;
67
+ role: EchoRole;
68
+ created_at: string;
69
+ parts: EchoPart[];
70
+ meta?: {
71
+ provider?: string;
72
+ model?: string;
73
+ usage?: {
74
+ input_tokens?: number;
75
+ output_tokens?: number;
76
+ };
77
+ latency?: {
78
+ duration?: number;
79
+ ttft?: number;
80
+ };
81
+ };
82
+ }
83
+ type EchoRecord = EchoMeta | EchoMessage;
84
+ interface EchoConversation {
85
+ meta: EchoMeta;
86
+ messages: EchoMessage[];
87
+ }
88
+
89
+ export type { EchoConversation as E, ImagePart as I, TextPart as T, EchoMessage as a, EchoMeta as b, EchoPart as c, EchoRecord as d, EchoRole as e, EchoSettings as f, EchoToolDefinition as g, ThinkingPart as h, ToolCallPart as i, ToolResultPart as j };
package/package.json CHANGED
@@ -1,8 +1,97 @@
1
1
  {
2
2
  "name": "echospace",
3
- "version": "0.0.1",
4
- "description": "Local-first prompt debugging workspace for LLM developers",
5
- "main": "index.js",
6
- "keywords": ["llm", "prompt", "debugging", "workspace", "echo"],
7
- "license": "MIT"
3
+ "version": "0.1.0-alpha.1",
4
+ "type": "module",
5
+ "description": "Open-source, local-first prompt debugging workspace for LLM developers",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/stonexer/echospace.git"
10
+ },
11
+ "homepage": "https://github.com/stonexer/echospace",
12
+ "bugs": "https://github.com/stonexer/echospace/issues",
13
+ "keywords": [
14
+ "llm",
15
+ "prompt",
16
+ "debugging",
17
+ "openai",
18
+ "anthropic",
19
+ "gemini",
20
+ "chatgpt",
21
+ "claude",
22
+ "ai",
23
+ "developer-tools"
24
+ ],
25
+ "bin": {
26
+ "echospace": "./dist/cli/index.js"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "exports": {
32
+ "./core/echo": {
33
+ "import": "./dist/core/echo/index.js",
34
+ "types": "./dist/core/echo/index.d.ts"
35
+ },
36
+ "./core/smart-paste": {
37
+ "import": "./dist/core/smart-paste/index.js",
38
+ "types": "./dist/core/smart-paste/index.d.ts"
39
+ },
40
+ "./core/providers": {
41
+ "import": "./dist/core/providers/index.js",
42
+ "types": "./dist/core/providers/index.d.ts"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "dev": "tsx watch src/cli/index.ts . -p 3240 --no-open & vite --clearScreen false; kill $!",
47
+ "dev:client": "vite",
48
+ "dev:server": "tsx watch src/cli/index.ts . -p 3240 --no-open",
49
+ "build": "vite build && tsup",
50
+ "preview": "vite preview",
51
+ "start": "node dist/cli/index.js",
52
+ "prepublishOnly": "pnpm run build",
53
+ "test": "vitest run",
54
+ "typecheck": "tsc --noEmit",
55
+ "lint": "eslint .",
56
+ "format": "prettier --write ."
57
+ },
58
+ "dependencies": {
59
+ "@hono/node-server": "^1.13.8",
60
+ "commander": "^13.1.0",
61
+ "get-port": "^7.1.0",
62
+ "hono": "^4.7.4",
63
+ "js-yaml": "^4.1.0",
64
+ "nanoid": "^5.1.2",
65
+ "open": "^10.1.2"
66
+ },
67
+ "devDependencies": {
68
+ "@codemirror/lang-json": "^6.0.1",
69
+ "@codemirror/lang-markdown": "^6.3.3",
70
+ "@codemirror/language-data": "^6.5.1",
71
+ "@hello-pangea/dnd": "^18.0.1",
72
+ "@tailwindcss/vite": "^4.1.3",
73
+ "@types/js-yaml": "^4.0.9",
74
+ "@types/node": "^22.13.10",
75
+ "@types/react": "^19.0.10",
76
+ "@types/react-dom": "^19.0.4",
77
+ "@uiw/react-codemirror": "^4.23.7",
78
+ "@vitejs/plugin-react-swc": "^4.0.0",
79
+ "autoprefixer": "^10.4.20",
80
+ "clsx": "^2.1.1",
81
+ "eslint": "^9.22.0",
82
+ "prettier": "^3.5.3",
83
+ "react": "^19.0.0",
84
+ "react-dom": "^19.0.0",
85
+ "react-resizable-panels": "^2.1.7",
86
+ "sonner": "^1.7.4",
87
+ "tailwind-merge": "^3.0.2",
88
+ "tailwindcss": "^4.1.3",
89
+ "tiktoken": "^1.0.18",
90
+ "tsup": "^8.5.1",
91
+ "tsx": "^4.19.3",
92
+ "typescript": "^5.8.2",
93
+ "vite": "^6.2.4",
94
+ "vitest": "^4.0.18",
95
+ "zustand": "^5.0.3"
96
+ }
8
97
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // coming soon