backpack-viewer 0.6.0 → 0.7.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.
Files changed (54) hide show
  1. package/bin/serve.js +159 -396
  2. package/dist/app/assets/index-D-H7agBH.js +12 -0
  3. package/dist/app/assets/index-DE73ngo-.css +1 -0
  4. package/dist/app/assets/index-Lvl7EMM_.js +6 -0
  5. package/dist/app/index.html +2 -2
  6. package/dist/bridge.d.ts +22 -0
  7. package/dist/bridge.js +41 -0
  8. package/dist/config.js +10 -0
  9. package/dist/copy-prompt.d.ts +17 -0
  10. package/dist/copy-prompt.js +81 -0
  11. package/dist/default-config.json +4 -0
  12. package/dist/dom-utils.d.ts +46 -0
  13. package/dist/dom-utils.js +57 -0
  14. package/dist/empty-state.js +63 -31
  15. package/dist/extensions/api.d.ts +15 -0
  16. package/dist/extensions/api.js +185 -0
  17. package/dist/extensions/chat/backpack-extension.json +23 -0
  18. package/dist/extensions/chat/src/index.js +32 -0
  19. package/dist/extensions/chat/src/panel.js +306 -0
  20. package/dist/extensions/chat/src/providers/anthropic.js +158 -0
  21. package/dist/extensions/chat/src/providers/types.js +15 -0
  22. package/dist/extensions/chat/src/tools.js +281 -0
  23. package/dist/extensions/chat/style.css +147 -0
  24. package/dist/extensions/event-bus.d.ts +12 -0
  25. package/dist/extensions/event-bus.js +30 -0
  26. package/dist/extensions/loader.d.ts +32 -0
  27. package/dist/extensions/loader.js +71 -0
  28. package/dist/extensions/manifest.d.ts +54 -0
  29. package/dist/extensions/manifest.js +116 -0
  30. package/dist/extensions/panel-mount.d.ts +26 -0
  31. package/dist/extensions/panel-mount.js +377 -0
  32. package/dist/extensions/share/backpack-extension.json +20 -0
  33. package/dist/extensions/share/src/index.js +357 -0
  34. package/dist/extensions/share/style.css +151 -0
  35. package/dist/extensions/taskbar.d.ts +29 -0
  36. package/dist/extensions/taskbar.js +64 -0
  37. package/dist/extensions/types.d.ts +182 -0
  38. package/dist/extensions/types.js +8 -0
  39. package/dist/info-panel.d.ts +2 -1
  40. package/dist/info-panel.js +78 -87
  41. package/dist/main.js +189 -29
  42. package/dist/search.js +1 -1
  43. package/dist/server-api-routes.d.ts +56 -0
  44. package/dist/server-api-routes.js +460 -0
  45. package/dist/server-extensions.d.ts +126 -0
  46. package/dist/server-extensions.js +272 -0
  47. package/dist/server-viewer-state.d.ts +18 -0
  48. package/dist/server-viewer-state.js +33 -0
  49. package/dist/sidebar.js +19 -7
  50. package/dist/style.css +356 -74
  51. package/dist/tools-pane.js +31 -14
  52. package/package.json +4 -3
  53. package/dist/app/assets/index-B3z5bBGl.css +0 -1
  54. package/dist/app/assets/index-BROJmzot.js +0 -35
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Tiny DOM helpers used across the viewer to avoid `innerHTML` for
3
+ * static SVG icons and other markup that previously got built as
4
+ * strings. Keeps every call site CSP-clean and XSS-safe by construction
5
+ * (no string concatenation, no parsing of user content).
6
+ */
7
+ const SVG_NS = "http://www.w3.org/2000/svg";
8
+ /**
9
+ * Build an SVG icon from a list of child element specs. Each spec is
10
+ * `{ tag, attrs }` where tag is one of the standard SVG element names.
11
+ *
12
+ * Example:
13
+ * makeSvgIcon({ size: 14 }, [
14
+ * { tag: "polyline", attrs: { points: "11 17 6 12 11 7" } },
15
+ * { tag: "polyline", attrs: { points: "18 17 13 12 18 7" } },
16
+ * ])
17
+ */
18
+ export function makeSvgIcon(opts, children) {
19
+ const svg = document.createElementNS(SVG_NS, "svg");
20
+ const size = opts.size ?? 16;
21
+ svg.setAttribute("width", String(size));
22
+ svg.setAttribute("height", String(size));
23
+ svg.setAttribute("viewBox", opts.viewBox ?? "0 0 24 24");
24
+ svg.setAttribute("fill", "none");
25
+ svg.setAttribute("stroke", "currentColor");
26
+ svg.setAttribute("stroke-width", String(opts.strokeWidth ?? 2));
27
+ if (opts.strokeLinecap)
28
+ svg.setAttribute("stroke-linecap", opts.strokeLinecap);
29
+ if (opts.strokeLinejoin)
30
+ svg.setAttribute("stroke-linejoin", opts.strokeLinejoin);
31
+ if (opts.className)
32
+ svg.setAttribute("class", opts.className);
33
+ for (const child of children) {
34
+ const el = document.createElementNS(SVG_NS, child.tag);
35
+ for (const [k, v] of Object.entries(child.attrs)) {
36
+ el.setAttribute(k, String(v));
37
+ }
38
+ svg.appendChild(el);
39
+ }
40
+ return svg;
41
+ }
42
+ /**
43
+ * Snapshot the current children of an element so they can be restored
44
+ * later via `restoreChildren()`. Used by inline-edit flows that
45
+ * temporarily replace a row's contents with an input.
46
+ *
47
+ * Returns a frozen array of cloned nodes — the original references are
48
+ * NOT preserved (which would break if the parent is mutated). Cloning
49
+ * is fine because the snapshotted markup is static — no event handlers
50
+ * to lose.
51
+ */
52
+ export function snapshotChildren(el) {
53
+ return Array.from(el.childNodes).map((n) => n.cloneNode(true));
54
+ }
55
+ export function restoreChildren(el, snapshot) {
56
+ el.replaceChildren(...snapshot);
57
+ }
@@ -1,37 +1,69 @@
1
+ import { makeSvgIcon } from "./dom-utils";
1
2
  export function initEmptyState(container) {
2
3
  const el = document.createElement("div");
3
4
  el.className = "empty-state";
4
- el.innerHTML = `
5
- <div class="empty-state-bg">
6
- <div class="empty-state-circle c1"></div>
7
- <div class="empty-state-circle c2"></div>
8
- <div class="empty-state-circle c3"></div>
9
- <div class="empty-state-circle c4"></div>
10
- <div class="empty-state-circle c5"></div>
11
- <svg class="empty-state-lines" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
12
- <line x1="80" y1="60" x2="220" y2="140" stroke="currentColor" stroke-width="0.5" opacity="0.15"/>
13
- <line x1="220" y1="140" x2="320" y2="80" stroke="currentColor" stroke-width="0.5" opacity="0.15"/>
14
- <line x1="220" y1="140" x2="160" y2="240" stroke="currentColor" stroke-width="0.5" opacity="0.15"/>
15
- <line x1="160" y1="240" x2="300" y2="220" stroke="currentColor" stroke-width="0.5" opacity="0.15"/>
16
- </svg>
17
- </div>
18
- <div class="empty-state-content">
19
- <div class="empty-state-icon">
20
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
21
- <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0022 16z"/>
22
- <polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
23
- <line x1="12" y1="22.08" x2="12" y2="12"/>
24
- </svg>
25
- </div>
26
- <h2 class="empty-state-title">No learning graphs yet</h2>
27
- <p class="empty-state-desc">Connect Backpack to Claude, then start a conversation. Claude will build your first learning graph automatically.</p>
28
- <div class="empty-state-setup">
29
- <div class="empty-state-label">Add Backpack to Claude Code:</div>
30
- <code class="empty-state-code">claude mcp add backpack-local -s user -- npx backpack-ontology@latest</code>
31
- </div>
32
- <p class="empty-state-hint">Press <kbd>?</kbd> for keyboard shortcuts</p>
33
- </div>
34
- `;
5
+ // Background circles + decorative line art
6
+ const bg = document.createElement("div");
7
+ bg.className = "empty-state-bg";
8
+ for (const cls of ["c1", "c2", "c3", "c4", "c5"]) {
9
+ const circle = document.createElement("div");
10
+ circle.className = `empty-state-circle ${cls}`;
11
+ bg.appendChild(circle);
12
+ }
13
+ bg.appendChild(makeSvgIcon({ size: 0, viewBox: "0 0 400 300", className: "empty-state-lines" }, [
14
+ { tag: "line", attrs: { x1: 80, y1: 60, x2: 220, y2: 140, "stroke-width": "0.5", opacity: "0.15" } },
15
+ { tag: "line", attrs: { x1: 220, y1: 140, x2: 320, y2: 80, "stroke-width": "0.5", opacity: "0.15" } },
16
+ { tag: "line", attrs: { x1: 220, y1: 140, x2: 160, y2: 240, "stroke-width": "0.5", opacity: "0.15" } },
17
+ { tag: "line", attrs: { x1: 160, y1: 240, x2: 300, y2: 220, "stroke-width": "0.5", opacity: "0.15" } },
18
+ ]));
19
+ // makeSvgIcon sets width/height attrs; the lines layer is sized by CSS
20
+ // (preserveAspectRatio comes from the empty-state-lines class).
21
+ const linesSvg = bg.querySelector(".empty-state-lines");
22
+ if (linesSvg) {
23
+ linesSvg.removeAttribute("width");
24
+ linesSvg.removeAttribute("height");
25
+ linesSvg.setAttribute("preserveAspectRatio", "xMidYMid slice");
26
+ }
27
+ el.appendChild(bg);
28
+ // Content card
29
+ const content = document.createElement("div");
30
+ content.className = "empty-state-content";
31
+ const iconWrap = document.createElement("div");
32
+ iconWrap.className = "empty-state-icon";
33
+ iconWrap.appendChild(makeSvgIcon({ size: 48, strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }, [
34
+ { tag: "path", attrs: { d: "M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0022 16z" } },
35
+ { tag: "polyline", attrs: { points: "3.27 6.96 12 12.01 20.73 6.96" } },
36
+ { tag: "line", attrs: { x1: 12, y1: 22.08, x2: 12, y2: 12 } },
37
+ ]));
38
+ content.appendChild(iconWrap);
39
+ const title = document.createElement("h2");
40
+ title.className = "empty-state-title";
41
+ title.textContent = "No learning graphs yet";
42
+ content.appendChild(title);
43
+ const desc = document.createElement("p");
44
+ desc.className = "empty-state-desc";
45
+ desc.textContent =
46
+ "Connect Backpack to Claude, then start a conversation. Claude will build your first learning graph automatically.";
47
+ content.appendChild(desc);
48
+ const setup = document.createElement("div");
49
+ setup.className = "empty-state-setup";
50
+ const setupLabel = document.createElement("div");
51
+ setupLabel.className = "empty-state-label";
52
+ setupLabel.textContent = "Add Backpack to Claude Code:";
53
+ const setupCode = document.createElement("code");
54
+ setupCode.className = "empty-state-code";
55
+ setupCode.textContent = "claude mcp add backpack-local -s user -- npx backpack-ontology@latest";
56
+ setup.append(setupLabel, setupCode);
57
+ content.appendChild(setup);
58
+ const hint = document.createElement("p");
59
+ hint.className = "empty-state-hint";
60
+ hint.append("Press ");
61
+ const kbd = document.createElement("kbd");
62
+ kbd.textContent = "?";
63
+ hint.appendChild(kbd);
64
+ hint.append(" for keyboard shortcuts");
65
+ content.appendChild(hint);
66
+ el.appendChild(content);
35
67
  container.appendChild(el);
36
68
  return {
37
69
  show() { el.classList.remove("hidden"); },
@@ -0,0 +1,15 @@
1
+ import type { ViewerExtensionAPI, ViewerHost } from "./types";
2
+ import type { Taskbar } from "./taskbar";
3
+ import type { PanelMount } from "./panel-mount";
4
+ /**
5
+ * Construct a per-extension `ViewerExtensionAPI` instance. Each loaded
6
+ * extension gets its own API object whose closures know its name — that
7
+ * scoping is what makes per-extension settings + per-extension fetch
8
+ * proxies work.
9
+ *
10
+ * The API surface is intentionally minimal in v1: graph reads, graph
11
+ * mutations (auto-undo, auto-persist, auto-rerender), viewer driving,
12
+ * mount surfaces, settings, network proxy. Anything else extensions
13
+ * need will get added in v2 with a viewerApi version bump.
14
+ */
15
+ export declare function createExtensionAPI(extensionName: string, host: ViewerHost, taskbar: Taskbar, panelMount: PanelMount): ViewerExtensionAPI;
@@ -0,0 +1,185 @@
1
+ import { VIEWER_API_VERSION } from "./types";
2
+ /**
3
+ * Construct a per-extension `ViewerExtensionAPI` instance. Each loaded
4
+ * extension gets its own API object whose closures know its name — that
5
+ * scoping is what makes per-extension settings + per-extension fetch
6
+ * proxies work.
7
+ *
8
+ * The API surface is intentionally minimal in v1: graph reads, graph
9
+ * mutations (auto-undo, auto-persist, auto-rerender), viewer driving,
10
+ * mount surfaces, settings, network proxy. Anything else extensions
11
+ * need will get added in v2 with a viewerApi version bump.
12
+ */
13
+ export function createExtensionAPI(extensionName, host, taskbar, panelMount) {
14
+ function newId() {
15
+ // Sufficient for client-side ids; backpack-ontology accepts arbitrary
16
+ // string ids and we already use a similar pattern in main.ts callbacks.
17
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
18
+ }
19
+ function ensureGraph() {
20
+ const data = host.getGraph();
21
+ if (!data)
22
+ throw new Error("no graph loaded in viewer");
23
+ return data;
24
+ }
25
+ return {
26
+ name: extensionName,
27
+ viewerApiVersion: VIEWER_API_VERSION,
28
+ // --- Graph reads ---
29
+ getGraph: () => host.getGraph(),
30
+ getGraphName: () => host.getGraphName(),
31
+ getSelection: () => host.getSelection(),
32
+ getFocus: () => host.getFocus(),
33
+ // --- Events ---
34
+ on(event, callback) {
35
+ return host.subscribe(event, callback);
36
+ },
37
+ // --- Graph mutations ---
38
+ async addNode(type, properties) {
39
+ if (!type)
40
+ throw new Error("addNode: type is required");
41
+ const data = ensureGraph();
42
+ host.snapshotForUndo();
43
+ const id = newId();
44
+ const now = new Date().toISOString();
45
+ data.nodes.push({
46
+ id,
47
+ type,
48
+ properties: properties,
49
+ createdAt: now,
50
+ updatedAt: now,
51
+ });
52
+ await host.saveCurrentGraph();
53
+ return id;
54
+ },
55
+ async updateNode(nodeId, properties) {
56
+ const data = ensureGraph();
57
+ const node = data.nodes.find((n) => n.id === nodeId);
58
+ if (!node)
59
+ throw new Error(`updateNode: node not found: ${nodeId}`);
60
+ host.snapshotForUndo();
61
+ node.properties = { ...node.properties, ...properties };
62
+ node.updatedAt = new Date().toISOString();
63
+ await host.saveCurrentGraph();
64
+ },
65
+ async removeNode(nodeId) {
66
+ const data = ensureGraph();
67
+ const node = data.nodes.find((n) => n.id === nodeId);
68
+ if (!node)
69
+ throw new Error(`removeNode: node not found: ${nodeId}`);
70
+ host.snapshotForUndo();
71
+ data.nodes = data.nodes.filter((n) => n.id !== nodeId);
72
+ data.edges = data.edges.filter((e) => e.sourceId !== nodeId && e.targetId !== nodeId);
73
+ await host.saveCurrentGraph();
74
+ },
75
+ async addEdge(sourceId, targetId, type) {
76
+ if (!sourceId || !targetId || !type) {
77
+ throw new Error("addEdge: sourceId, targetId, and type are required");
78
+ }
79
+ const data = ensureGraph();
80
+ if (!data.nodes.find((n) => n.id === sourceId)) {
81
+ throw new Error(`addEdge: source not found: ${sourceId}`);
82
+ }
83
+ if (!data.nodes.find((n) => n.id === targetId)) {
84
+ throw new Error(`addEdge: target not found: ${targetId}`);
85
+ }
86
+ host.snapshotForUndo();
87
+ const id = newId();
88
+ data.edges.push({ id, sourceId, targetId, type });
89
+ await host.saveCurrentGraph();
90
+ return id;
91
+ },
92
+ async removeEdge(edgeId) {
93
+ const data = ensureGraph();
94
+ const edge = data.edges.find((e) => e.id === edgeId);
95
+ if (!edge)
96
+ throw new Error(`removeEdge: edge not found: ${edgeId}`);
97
+ host.snapshotForUndo();
98
+ data.edges = data.edges.filter((e) => e.id !== edgeId);
99
+ await host.saveCurrentGraph();
100
+ },
101
+ // --- Viewer driving ---
102
+ panToNode: (nodeId) => host.panToNode(nodeId),
103
+ focusNodes: (nodeIds, hops) => host.focusNodes(nodeIds, hops),
104
+ exitFocus: () => host.exitFocus(),
105
+ // --- UI mounting ---
106
+ registerTaskbarIcon(opts) {
107
+ return taskbar.register(opts);
108
+ },
109
+ mountPanel(element, opts) {
110
+ return panelMount.mount(extensionName, element, opts);
111
+ },
112
+ // --- Settings ---
113
+ settings: {
114
+ async get(key) {
115
+ const url = `/api/extensions/${encodeURIComponent(extensionName)}/settings`;
116
+ const res = await fetch(url);
117
+ if (!res.ok)
118
+ return null;
119
+ const all = (await res.json());
120
+ return key in all ? all[key] : null;
121
+ },
122
+ async set(key, value) {
123
+ const url = `/api/extensions/${encodeURIComponent(extensionName)}/settings/${encodeURIComponent(key)}`;
124
+ const res = await fetch(url, {
125
+ method: "PUT",
126
+ headers: { "Content-Type": "application/json" },
127
+ body: JSON.stringify({ value }),
128
+ });
129
+ if (!res.ok) {
130
+ const err = await res.text().catch(() => "");
131
+ throw new Error(`settings.set failed: ${err || res.status}`);
132
+ }
133
+ },
134
+ async remove(key) {
135
+ const url = `/api/extensions/${encodeURIComponent(extensionName)}/settings/${encodeURIComponent(key)}`;
136
+ const res = await fetch(url, { method: "DELETE" });
137
+ if (!res.ok) {
138
+ const err = await res.text().catch(() => "");
139
+ throw new Error(`settings.remove failed: ${err || res.status}`);
140
+ }
141
+ },
142
+ },
143
+ // --- Network ---
144
+ async fetch(url, init = {}) {
145
+ // The browser-side viewer.fetch wraps the call into a POST against
146
+ // the per-extension proxy endpoint. The proxy validates the URL
147
+ // against the manifest's network allowlist and injects any
148
+ // configured headers (env-var or literal) server-side.
149
+ const proxyUrl = `/api/extensions/${encodeURIComponent(extensionName)}/fetch`;
150
+ const headers = {};
151
+ if (init.headers) {
152
+ if (init.headers instanceof Headers) {
153
+ init.headers.forEach((v, k) => {
154
+ headers[k] = v;
155
+ });
156
+ }
157
+ else if (Array.isArray(init.headers)) {
158
+ for (const [k, v] of init.headers)
159
+ headers[k] = v;
160
+ }
161
+ else {
162
+ Object.assign(headers, init.headers);
163
+ }
164
+ }
165
+ const body = typeof init.body === "string" || init.body == null
166
+ ? init.body
167
+ : // RequestInit body can be many things; for v1 we only support
168
+ // strings (which is what JSON-based APIs use). Throw on
169
+ // anything else so extension authors get a clear error.
170
+ (() => {
171
+ throw new Error("viewer.fetch only supports string bodies in v1");
172
+ })();
173
+ return fetch(proxyUrl, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify({
177
+ url,
178
+ method: init.method ?? "POST",
179
+ headers,
180
+ body,
181
+ }),
182
+ });
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "chat",
3
+ "version": "0.1.0",
4
+ "viewerApi": "1",
5
+ "displayName": "Chat",
6
+ "description": "Chat with Claude (or another LLM provider) about your graph. The model can read nodes, follow edges, and add/edit/remove graph data with your approval.",
7
+ "entry": "src/index.js",
8
+ "stylesheet": "style.css",
9
+ "permissions": {
10
+ "graph": ["read", "write"],
11
+ "viewer": ["focus", "pan"],
12
+ "settings": true,
13
+ "network": [
14
+ {
15
+ "origin": "https://api.anthropic.com",
16
+ "injectHeaders": {
17
+ "x-api-key": { "fromEnv": "ANTHROPIC_API_KEY" },
18
+ "anthropic-version": { "literal": "2023-06-01" }
19
+ }
20
+ }
21
+ ]
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ import { createAnthropicProvider } from "./providers/anthropic.js";
2
+ import { createChatPanelController } from "./panel.js";
3
+ /**
4
+ * Chat extension entry. The viewer's loader dynamic-imports this file
5
+ * and calls `activate(viewer)`.
6
+ *
7
+ * Wires up:
8
+ * - One LLM provider (Anthropic for v1; future: OpenAI, Ollama, …)
9
+ * - The chat panel controller (mounts/unmounts the panel)
10
+ * - A taskbar icon that toggles the panel
11
+ *
12
+ * Everything goes through the viewer extension API. There is no
13
+ * special-case wiring into viewer internals — this extension uses
14
+ * exactly the same surface a third-party extension would.
15
+ */
16
+ export function activate(viewer) {
17
+ const provider = createAnthropicProvider(viewer.fetch.bind(viewer));
18
+ const controller = createChatPanelController(viewer, provider);
19
+ viewer.registerTaskbarIcon({
20
+ label: "Chat",
21
+ // Top-right groups the chat toggle with the existing top-bar
22
+ // controls (zoom, copy-prompt, theme) and aligns visually with the
23
+ // chat panel itself, which docks to the right side of the canvas.
24
+ position: "top-right",
25
+ onClick: () => controller.toggle(),
26
+ });
27
+ // Restore any persisted history before the user opens the panel.
28
+ // Errors are non-fatal — first run has nothing to restore.
29
+ controller.loadHistory().catch((err) => {
30
+ console.warn("[chat] failed to load history:", err);
31
+ });
32
+ }