@aisystemresources/emdee 0.3.0 → 0.3.2

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
@@ -1,10 +1,10 @@
1
1
  # Emdee
2
2
 
3
- Local-first knowledge graph backed by plain markdown. Humans browse it through a Next.js renderer; agents (Claude Code, Claude.ai, Cursor, Codex) read and write the same files through an MCP server. The vault is the source of truth anything an LLM says traces back to a file you wrote.
3
+ A markdown knowledge graph shared by humans and their AI agents. Files you can edit like Obsidian, a vault you can share like Google Drive, and a live MCP endpoint every agent (Claude Code, Claude.ai, Cursor, Codex) reads and writes to natively. One vault, three surfaces with the plain markdown you wrote as the only source of truth.
4
4
 
5
5
  ## Why
6
6
 
7
- LLM agents need a stable, human-readable substrate to read and write their own context over time. Most knowledge-graph tools are either built for humans (Obsidian) or built for agents (vector stores). Emdee is a single substrate for both: the markdown a human edits is the exact bytes an agent reads, with no hidden index, no parallel summaries, no schema gymnastics. Build up a working journal that survives across sessions.
7
+ Every knowledge tool picks two of three: human-friendly, team-shareable, agent-native. Notion is human-friendly and shareable, but its blocks aren't markdown and agents can't natively touch them. Obsidian is human-friendly and agent-touchable (they're just markdown files), but private-by-default — sharing is a paid add-on or a git repo. Google Drive is shareable but not markdown-native. Emdee is all three: the markdown a human edits is the exact bytes an agent reads through MCP and a teammate reads through a share link — no hidden index, no parallel summaries, no schema gymnastics. Build up a working journal that survives across sessions and travels with the people you invite in.
8
8
 
9
9
  ## Install
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisystemresources/emdee",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Local-first document management with markdown rendering, knowledge graph, and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,9 @@ export async function callTool(
37
37
  method: "POST",
38
38
  headers: {
39
39
  "Content-Type": "application/json",
40
+ // MCP streamable-HTTP transport requires BOTH content types in Accept
41
+ // or it 406s with "Client must accept both application/json and
42
+ // text/event-stream". We handle whichever shape the server picks below.
40
43
  "Accept": "application/json, text/event-stream",
41
44
  Authorization: `Bearer ${creds.access_token}`,
42
45
  },
@@ -54,12 +57,33 @@ export async function callTool(
54
57
  throw new Error(`remote call failed: ${res.status} ${body}`);
55
58
  }
56
59
 
57
- const body = (await res.json()) as JsonRpcResponse;
60
+ const body = await parseJsonRpc(res);
58
61
  if (body.error) throw new Error(`remote tool error: ${body.error.message}`);
59
62
  if (!body.result) throw new Error("remote call returned no result");
60
63
  return body.result;
61
64
  }
62
65
 
66
+ // The MCP streamable-HTTP transport content-negotiates on the request Accept:
67
+ // when the client offers both JSON and SSE (as required — see 406 above),
68
+ // the server may reply with either. JSON we parse directly; SSE arrives as
69
+ // one or more `event: message\ndata: <json>\n\n` frames — for a single
70
+ // tools/call response we take the first `data:` payload.
71
+ async function parseJsonRpc(res: Response): Promise<JsonRpcResponse> {
72
+ const ct = res.headers.get("content-type") ?? "";
73
+ if (ct.includes("text/event-stream")) {
74
+ const text = await res.text();
75
+ const dataLine = text.split(/\r?\n/).find((l) => l.startsWith("data:"));
76
+ if (!dataLine) throw new Error(`remote call returned SSE with no data frame: ${text.slice(0, 200)}`);
77
+ const payload = dataLine.slice("data:".length).trim();
78
+ try {
79
+ return JSON.parse(payload) as JsonRpcResponse;
80
+ } catch (e) {
81
+ throw new Error(`remote call SSE data frame is not valid JSON: ${(e as Error).message}`);
82
+ }
83
+ }
84
+ return (await res.json()) as JsonRpcResponse;
85
+ }
86
+
63
87
  /**
64
88
  * Convenience: unwrap the MCP text envelope back to a string. Tools that
65
89
  * return JSON encode it inside `content[0].text`; tools that return plain