@webship.co/workspace-graph 0.0.0-stage → 1.0.0-beta1

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,3 +1,25 @@
1
- # Temporary Holding Version
1
+ # @webship.co/workspace-graph
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The **Graphs** workspace and the knowledge-graph feature of the [Webship Workspace](https://github.com/webship/workspace) dashboard:
4
+ the `graphs/` folder (type `code`), its settings file (`core/config/workspace.graphs.settings.yml`),
5
+ its commands and its docs, as a plugin — plus the **Graph** menu on every project row that has a graphify command (build with graphify, open the graph, read its report, download it, serve it over MCP, delete it) and the Obsidian vault made from a graph (build, read the notes in the browser, open the map, download), with the `/graph/…` and `/obsidian/…` pages.
6
+
7
+ ## Use it
8
+
9
+ ```bash
10
+ cd ~/workspace
11
+ bash core/scripts/plugins.sh --require github:webship/workspace-graph
12
+ bash core/scripts/plugins.sh --activate graph # puts `graphs` on the workspaces list of settings.yml
13
+ bash core/scripts/plugins.sh --install graph # the folder, its settings file (once), its commands, its docs (once)
14
+ ```
15
+
16
+ The settings file and the docs are placed once and never overwritten: they hold what you put there.
17
+ `--deactivate` takes the workspace off the list; the folder and its content stay.
18
+
19
+ ## The commands
20
+
21
+ | Command |
22
+ | --- |
23
+ | `cmd-tools-graphs.sh` |
24
+
25
+ The bash functions these commands call live in the dashboard's `core/scripts/functions`.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Map a project in this workspace into a queryable knowledge graph with
4
+ # graphify (https://github.com/Graphify-Labs/graphify), or ask an existing
5
+ # graph a question.
6
+
7
+ # Bootstrap.
8
+ # Find the workspace tooling from this script, so a fresh clone needs no setup.
9
+ WORKSPACE_SCRIPTS="${WORKSPACE_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
10
+ source ${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
11
+
12
+ # Load workspace settings and extra lists.
13
+ eval $(parse_yaml ${WORKSPACE_CONFIG}/workspace.__WORKSPACE__.settings.yml);
14
+
15
+ ARGPARSE_DESCRIPTION="Build or query the knowledge graph of a project in the __WORKSPACE__ workspace"
16
+ source ${WORKSPACE_SCRIPTS}/plugins/graph/args/arg-graphify.sh || exit 1 ;
17
+
18
+ shift $#;
19
+
20
+ if [ ! "${STORE}" == '_none_' ] ; then
21
+ ensure_graphify || exit 1 ;
22
+ graphify_store_url "${WORKSPACE_ROOT}/${doc_name}/${PROJECT_NAME}" "${STORE}" ;
23
+ elif [ -n "${QUERY}" ] ; then
24
+ graphify_query_project ;
25
+ else
26
+ graphify_project ;
27
+ fi
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # workspace-name: Graphs (list, collect, serve over MCP)
4
+
5
+ # Bootstrap.
6
+ # Find the workspace tooling from this script, so a fresh clone needs no setup.
7
+ WORKSPACE_SCRIPTS="${WORKSPACE_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
8
+ source ${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
9
+
10
+ # Load workspace settings.
11
+ eval $(parse_yaml ${WORKSPACE_CONFIG}/workspace.graphs.settings.yml);
12
+
13
+ ARGPARSE_DESCRIPTION="Work with the project knowledge graphs kept in ~/workspace/graphs/"
14
+ source ${WORKSPACE_SCRIPTS}/plugins/graph/args/arg-graphs.sh || exit 1 ;
15
+
16
+ shift $#;
17
+
18
+ graphs_command ;
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Turn a project's knowledge graph into an Obsidian vault — one note per entity,
4
+ # a [[wikilink]] per relation, and a JSON Canvas of the map. Written next to the
5
+ # graph in graphs/dev/<project>/obsidian/.
6
+ #
7
+ # Build the graph first with cmd-tools-graphify.sh: this reads what that wrote
8
+ # rather than parsing the project a second time.
9
+
10
+ # Bootstrap.
11
+ # Find the workspace tooling from this script, so a fresh clone needs no setup.
12
+ WORKSPACE_SCRIPTS="${WORKSPACE_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
13
+ source ${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
14
+
15
+ # Load workspace settings and extra lists.
16
+ eval $(parse_yaml ${WORKSPACE_CONFIG}/workspace.__WORKSPACE__.settings.yml);
17
+
18
+ ARGPARSE_DESCRIPTION="Build an Obsidian vault from the knowledge graph of a project in the __WORKSPACE__ workspace"
19
+ source ${WORKSPACE_SCRIPTS}/plugins/graph/args/arg-obsidian.sh || exit 1 ;
20
+
21
+ shift $#;
22
+
23
+ export MAX_CANVAS_NODES="${MAX_CANVAS_NODES}";
24
+
25
+ if [ "${REMOVE}" == 'yes' ] ; then
26
+ obsidian_remove ;
27
+ elif [ "${STATUS}" == 'yes' ] ; then
28
+ obsidian_status ;
29
+ else
30
+ obsidian_project ;
31
+ fi
@@ -0,0 +1,25 @@
1
+ doc:
2
+ name: graphs
3
+
4
+ # Knowledge graphs built by cmd-tools-graphify.sh, one directory per project:
5
+ # graphs/<workspace>/<project>/{graph.json,GRAPH_REPORT.md,graph.html}
6
+ # They live HERE rather than inside the project on purpose: graphify writes tens of MB, Leiden
7
+ # clustering is not bit-stable so every rebuild diffs in full, and half the projects worth mapping
8
+ # are git repositories we ship — a recipe like products/website_starter must not gain a graphify-out/ or a
9
+ # CLAUDE.md block just because someone mapped it.
10
+ presentation:
11
+ icon: tabler:hierarchy-2
12
+ subtitle: Project Knowledge Graphs
13
+ noun: graph
14
+ kind: files
15
+
16
+ # What this folder's projects are, and which actions their rows offer:
17
+ # sites | code | files | media | ai-items — see core/workspace-app/WORKSPACES.md.
18
+ type: code
19
+
20
+ # What the assistant is called on THIS workspace's page, and what its composer says when empty.
21
+ # `default` in either means "whatever settings.yml says" — the global value, so a rename there
22
+ # still reaches this page. Set a value here to override it for this workspace alone.
23
+ assistant:
24
+ name: default
25
+ placeholder: default
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The knowledge graphs for Webship Workspace: the graphs workspace (`graphs/`) as a plugin, and the
3
+ * Graph menu on every project row that has a graphify command — build the graph, open it, read its
4
+ * report, download it, serve it over MCP, delete it — with the Obsidian vault made from it (build,
5
+ * read the notes, open the map, download, delete), the `/graph/…` and `/obsidian/…` pages, and the
6
+ * graphs list the workspace page shows.
7
+ *
8
+ * Everything the dashboard is asked for goes through `api`; the store lives in the graphs workspace,
9
+ * never inside a project.
10
+ */
11
+ import type { PluginApi } from '@webship.co/workspace-plugin-api';
12
+ declare const graphs: any;
13
+ declare const obsidian: any;
14
+ export declare const WORKSPACE_KEY = "graphs";
15
+ export { graphs, obsidian };
16
+ export declare function register(api: PluginApi): void;
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ /**
3
+ * The knowledge graphs for Webship Workspace: the graphs workspace (`graphs/`) as a plugin, and the
4
+ * Graph menu on every project row that has a graphify command — build the graph, open it, read its
5
+ * report, download it, serve it over MCP, delete it — with the Obsidian vault made from it (build,
6
+ * read the notes, open the map, download, delete), the `/graph/…` and `/obsidian/…` pages, and the
7
+ * graphs list the workspace page shows.
8
+ *
9
+ * Everything the dashboard is asked for goes through `api`; the store lives in the graphs workspace,
10
+ * never inside a project.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.obsidian = exports.graphs = exports.WORKSPACE_KEY = void 0;
17
+ exports.register = register;
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const child_process_1 = require("child_process");
21
+ const graphs = require('./lib/graphs');
22
+ exports.graphs = graphs;
23
+ const obsidian = require('./lib/obsidian');
24
+ exports.obsidian = obsidian;
25
+ exports.WORKSPACE_KEY = 'graphs';
26
+ function register(api) {
27
+ const lib = api.lib;
28
+ const { esc, workspaceDir, listProjects, isValidWorkspace, NAME_RE, ROOT, iconHtml, humanSize, humanTime, commandBlock, runningJobKey, startJob, jobFragment, startInScreen, screenStartedHtml, findScript, cssVersionNow, lists } = lib;
29
+ const { graphStoreDir, hasGraph, listGraphs } = graphs;
30
+ const { vaultDir, indexNote, vaultView, vaultCanvas, vaultFrame, obsidianMenuItems } = obsidian;
31
+ const post = (name) => `/actions/p/${api.name}/${name}`;
32
+ const findGraphifyScript = (dir) => findScript(dir, [/^cmd-tools?-graphify\.sh$/]);
33
+ // ---- The graphs workspace page ------------------------------------------------------------------
34
+ function graphRowsHtml(state) {
35
+ const { SORTS, defaultSort, paginate, listControlsHtml, listPagerHtml } = lists;
36
+ const all = listGraphs();
37
+ const st = state;
38
+ const filtered = st.q ? all.filter((g) => g.name.toLowerCase().includes(st.q.toLowerCase())) : all;
39
+ const sorted = filtered.slice().sort((SORTS[st.sort] || SORTS[defaultSort()] || SORTS.newest).cmp);
40
+ const p = paginate(sorted, st);
41
+ const vals = (g, extra = '') => `hx-vals='{"workspace":"${esc(g.workspace)}","projectName":"${esc(g.project)}"${extra}}' hx-target="#webship-workspace-output" hx-swap="innerHTML"`;
42
+ const rows = p.slice.map((g) => `
43
+ <div class="uk-card uk-card-default uk-card-small uk-card-body uk-margin-small project-row">
44
+ <div class="item-row-body">
45
+ <span class="uk-text-bold">${iconHtml('tabler:hierarchy-2', 0.8)} ${esc(g.project)}
46
+ <span class="uk-text-meta item-machine" title="The workspace the project lives in">${esc(g.workspace)}</span></span>
47
+ <div class="item-meta-line">
48
+ <span class="uk-text-meta item-time">${esc(humanTime(g.mtime))}</span>
49
+ <span class="uk-text-meta">· ${esc(humanSize(g.size))}</span>
50
+ ${g.nodes != null ? `<span class="uk-label" title="Nodes and edges, from graphify's own build file">${g.nodes} nodes${g.links != null ? ` · ${g.links} edges` : ''}</span>` : ''}
51
+ ${g.projectExists ? '' : '<span class="uk-label" title="The project is gone — the graph outlived it, and is safe to delete">project removed</span>'}
52
+ </div>
53
+ <div class="project-actions">
54
+ ${g.hasHtml ? `<a class="uk-button uk-button-default uk-button-small" href="/graph/${esc(g.workspace)}/${esc(g.project)}" target="_blank" title="Loads vis-network from unpkg.com, so it needs internet"><span uk-icon="icon: image; ratio: .7"></span> Open</a>` : ''}
55
+ ${g.hasReport ? `<a class="uk-button uk-button-default uk-button-small" href="/graph/${esc(g.workspace)}/${esc(g.project)}/report" target="_blank"><span uk-icon="icon: file-text; ratio: .7"></span> Report</a>` : ''}
56
+ <a class="uk-button uk-button-default uk-button-small" href="/graph/${esc(g.workspace)}/${esc(g.project)}/download"><span uk-icon="icon: download; ratio: .7"></span> Download</a>
57
+ ${g.projectExists ? `<button class="uk-button uk-button-secondary uk-button-small" hx-post="${post('graphify')}" ${vals(g)} title="Re-extract and re-cluster from the project"><span uk-icon="icon: refresh; ratio: .7"></span> Rebuild</button>` : ''}
58
+ <button class="uk-button uk-button-danger uk-button-small arm-step" hx-post="${post('graph-remove')}" ${vals(g, ',"confirm":"yes"')} hx-trigger="confirmed-remove"><span uk-icon="icon: trash; ratio: .7"></span> Delete</button>
59
+ </div>
60
+ </div>
61
+ </div>`).join('');
62
+ // The controls get the BARE URL: htmx appends the included inputs without deduping, and
63
+ // readPageState reads the FIRST q/sort. The pager keeps the qs form so page links keep the search.
64
+ const bareUrl = `/fragments/p/${api.name}/list`;
65
+ const qs = new URLSearchParams();
66
+ if (st.q)
67
+ qs.set('q', st.q);
68
+ if (st.sort !== defaultSort())
69
+ qs.set('sort', st.sort);
70
+ const url = `${bareUrl}${qs.toString() ? `?${qs}` : ''}`;
71
+ const pager = listPagerHtml(url, '#webship-workspace-projects', p, st, 'graphs');
72
+ return `
73
+ <h3 class="uk-margin-small-bottom">Graphs <span class="uk-badge">${all.length}</span></h3>
74
+ <p class="uk-text-meta uk-margin-small-bottom">One directory per project in <code>${esc(path_1.default.join(ROOT, 'graphs'))}/&lt;workspace&gt;/&lt;project&gt;/</code>,
75
+ built by <code>cmd-tools-graphify.sh</code> from the project's workspace folder. Query one with
76
+ <code>graphify query "…" --graph ${esc(path_1.default.join(ROOT, 'graphs'))}/&lt;workspace&gt;/&lt;project&gt;/graph.json</code>.</p>
77
+ ${listControlsHtml(bareUrl, '#webship-workspace-projects', st, 'graphs')}
78
+ ${pager}
79
+ ${rows || '<p class="uk-text-meta">No graphs yet — build one from the Graph menu on any project row.</p>'}
80
+ ${pager}`;
81
+ }
82
+ api.addWorkspaceView(exports.WORKSPACE_KEY, {
83
+ listHtml: (state) => `
84
+ <div id="webship-workspace-projects" hx-get="/fragments/p/${api.name}/list" hx-trigger="refresh-projects from:body">
85
+ ${graphRowsHtml(state)}
86
+ </div>
87
+ `,
88
+ count: () => listGraphs().length,
89
+ noFilesActions: true,
90
+ noNewForm: true,
91
+ });
92
+ api.addFragment(({ req, send, rest }) => {
93
+ if (rest !== 'list')
94
+ return send('', 404);
95
+ return send(graphRowsHtml(lists.readPageState(req)));
96
+ });
97
+ // ---- The Graph menu on a project row ---------------------------------------------------------
98
+ api.addRowMenu('graph', { label: 'Graph', row: 1 }, ({ key, project: p, dir, allowed, vals }) => {
99
+ if (!allowed.has('graphify') || !findGraphifyScript(dir))
100
+ return null;
101
+ const built = hasGraph(key, p, 'graph.json') || hasGraph(key, p, 'graph.html');
102
+ const busy = !!runningJobKey(`graphify:${key}/${p}`);
103
+ const items = obsidianMenuItems(key, p, vals, busy, post);
104
+ return `<span class="act-with-status"><div class="uk-inline act-menu act-menu-graph">
105
+ <button class="uk-button uk-button-default uk-button-small" type="button" title="Knowledge graph"><span uk-icon="icon: git-fork; ratio: .7"></span> Graph <span uk-icon="icon: chevron-down; ratio: .6"></span></button>
106
+ <div uk-dropdown="mode: click; pos: bottom-right"><ul class="uk-nav uk-dropdown-nav">
107
+ <li data-entry="build the graph"><a href hx-post="${post('graphify')}" ${vals()} class="${busy ? 'uk-disabled' : ''}"><span uk-icon="icon: play; ratio: .7"></span> ${built ? 'Rebuild the graph' : 'Build the graph'}${busy ? ' (running…)' : ''}</a></li>
108
+ ${built ? `<li class="uk-nav-divider"></li>
109
+ ${hasGraph(key, p, 'graph.html') ? `<li><a href="/graph/${esc(key)}/${esc(p)}" target="_blank" title="Loads vis-network from unpkg.com, so it needs internet"><span uk-icon="icon: image; ratio: .7"></span> Open the graph</a></li>` : ''}
110
+ ${hasGraph(key, p, 'GRAPH_REPORT.md') ? `<li><a href="/graph/${esc(key)}/${esc(p)}/report" target="_blank"><span uk-icon="icon: file-text; ratio: .7"></span> Read the report</a></li>` : ''}
111
+ <li><a href="/graph/${esc(key)}/${esc(p)}/download" title="The graph as a .tar.gz, to hand to another workspace"><span uk-icon="icon: download; ratio: .7"></span> Download to share</a></li>
112
+ <li><a href hx-post="${post('graph-mcp-command')}" ${vals()}><span uk-icon="icon: link; ratio: .7"></span> Connect over MCP…</a></li>
113
+ ${items}
114
+ <li class="uk-nav-divider"></li>
115
+ <li><a href class="uk-text-danger arm-step" hx-post="${post('graph-remove')}" ${vals(',"confirm":"yes"')} hx-trigger="confirmed-remove"><span uk-icon="icon: trash; ratio: .7"></span> Delete the graph</a></li>` : ''}
116
+ </ul></div>
117
+ </div><span class="act-status uk-text-meta">${built ? 'built' : 'none'}</span></span>`;
118
+ });
119
+ // ---- Actions ----------------------------------------------------------------------------------
120
+ api.addAction('graphify', async ({ form, send, workspace }) => {
121
+ const dir = workspaceDir(workspace);
122
+ const script = findGraphifyScript(dir);
123
+ const projectName = String(form.projectName || '');
124
+ if (!script)
125
+ return send('<div class="msg error">No graphify script in this workspace.</div>', 400);
126
+ if (!NAME_RE.test(projectName) || !listProjects(dir).includes(projectName))
127
+ return send('<div class="msg error">Unknown project.</div>', 400);
128
+ // graphify has no cross-process lock: two runs against one graphify-out/ race on its files.
129
+ if (runningJobKey(`graphify:${workspace}/${projectName}`))
130
+ return send('<div class="msg error">A graph is already being built for this project.</div>', 409);
131
+ const started = await startInScreen({ workspace, project: projectName, projectDir: dir, ownerDir: path_1.default.join(dir, projectName), verb: 'graphify',
132
+ cmdLine: `bash ${JSON.stringify(script)} ${JSON.stringify(projectName)}`, label: 'graphify' });
133
+ return send(screenStartedHtml(started, 'graphify', projectName), started.ok ? 200 : (started.already ? 409 : 500));
134
+ });
135
+ api.addAction('graph-mcp-command', ({ form, send, workspace }) => {
136
+ const projectName = String(form.projectName || '');
137
+ if (!NAME_RE.test(projectName))
138
+ return send('<div class="msg error">Unknown project.</div>', 400);
139
+ const graph = path_1.default.join(graphStoreDir(workspace, projectName), 'graph.json');
140
+ if (!fs_1.default.existsSync(graph))
141
+ return send('<div class="msg error">No graph yet — build it first.</div>', 409);
142
+ return send(commandBlock(`Serve the ${workspace}/${projectName} graph to the Claude Code CLI`, [`cd ${ROOT}/graphs && bash cmd-tools-graphs.sh ${workspace}/${projectName} -a mcp-register`,
143
+ `claude mcp add --scope user graph-${workspace}-${projectName} -- graphify-mcp ${graph.replace(ROOT, '~/workspace')}`], 'Run either one <strong>on the host</strong>, in a terminal. The first is the workspace command; '
144
+ + 'the second is what it runs. stdio, so there is no port and no daemon — the CLI starts the server '
145
+ + 'when a session needs it. Open a NEW session to see the tools; rebuilding the graph needs no re-registration.'));
146
+ });
147
+ api.addAction('graph-remove', ({ form, send, workspace, res }) => {
148
+ if (form.confirm !== 'yes')
149
+ return send('<div class="msg error">Deleting requires confirmation.</div>', 400);
150
+ const projectName = String(form.projectName || '');
151
+ // The project may be gone — a graph outlives it, and that is exactly a graph worth deleting.
152
+ if (!NAME_RE.test(projectName))
153
+ return send('<div class="msg error">Unknown project.</div>', 400);
154
+ if (runningJobKey(`graphify:${workspace}/${projectName}`))
155
+ return send('<div class="msg error">A graph run is in progress for this project — wait for it to finish.</div>', 409);
156
+ let real;
157
+ try {
158
+ real = fs_1.default.realpathSync(graphStoreDir(workspace, projectName));
159
+ const base = fs_1.default.realpathSync(path_1.default.join(ROOT, 'graphs'));
160
+ if (!real.startsWith(base + path_1.default.sep))
161
+ throw new Error('outside');
162
+ }
163
+ catch (_) {
164
+ return send('<div class="msg assistant">No graph to delete.</div>');
165
+ }
166
+ fs_1.default.rmSync(real, { recursive: true, force: true });
167
+ res.setHeader('HX-Trigger', 'refresh-projects');
168
+ return send(`<div class="msg assistant">🗑️ Deleted the graph for <strong>${esc(projectName)}</strong>. Rebuild it any time from the Graph menu.</div>`);
169
+ });
170
+ api.addAction('obsidian', ({ form, send, workspace }) => {
171
+ const dir = workspaceDir(workspace);
172
+ const script = 'cmd-tools-obsidian.sh';
173
+ const projectName = String(form.projectName || '');
174
+ if (!fs_1.default.existsSync(path_1.default.join(dir, script)))
175
+ return send('<div class="msg error">This workspace has no Obsidian script.</div>', 400);
176
+ if (!NAME_RE.test(projectName) || !listProjects(dir).includes(projectName))
177
+ return send('<div class="msg error">Unknown project.</div>', 400);
178
+ if (!fs_1.default.existsSync(path_1.default.join(graphStoreDir(workspace, projectName), 'graph.json')))
179
+ return send('<div class="msg error">No graph to convert yet — build the graph first, from the same menu.</div>', 409);
180
+ const jobKey = `obsidian:${workspace}/${projectName}`;
181
+ if (runningJobKey(jobKey))
182
+ return send('<div class="msg error">A vault is already being built for this project.</div>', 409);
183
+ const id = startJob(`🗂️ Obsidian <strong>${esc(projectName)}</strong>`, 'bash', [script, projectName], dir, { timeoutMs: 10 * 60 * 1000, echoLine: `bash ${script} ${projectName}`, key: jobKey });
184
+ return send(jobFragment(id).html);
185
+ });
186
+ api.addAction('obsidian-remove', ({ form, send, workspace }) => {
187
+ if (form.confirm !== 'yes')
188
+ return send('<div class="msg error">Deleting requires confirmation.</div>', 400);
189
+ const projectName = String(form.projectName || '');
190
+ if (!NAME_RE.test(projectName))
191
+ return send('<div class="msg error">Unknown project.</div>', 400);
192
+ const dir = vaultDir(workspace, projectName);
193
+ if (!fs_1.default.existsSync(dir))
194
+ return send('<div class="msg">There was no vault to delete.</div>');
195
+ try {
196
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
197
+ }
198
+ catch (e) {
199
+ return send(`<div class="msg error">Could not delete the vault: ${esc(e.message)}</div>`, 500);
200
+ }
201
+ return send('<div class="msg">Vault deleted. The graph it was made from is untouched.</div>');
202
+ });
203
+ // ---- The pages: the graph, its report, its archive; the vault's notes, map, index, archive --------
204
+ const tarOut = (res, real, filename) => {
205
+ res.writeHead(200, { 'Content-Type': 'application/gzip', 'Content-Disposition': `attachment; filename="${filename.replace(/[^A-Za-z0-9._-]/g, '_')}"`, 'X-Content-Type-Options': 'nosniff' });
206
+ const tar = (0, child_process_1.spawn)('tar', ['-czf', '-', '-C', path_1.default.dirname(real), path_1.default.basename(real)], { stdio: ['ignore', 'pipe', 'pipe'] });
207
+ tar.stdout.pipe(res);
208
+ tar.stderr.resume();
209
+ tar.on('error', () => { try {
210
+ res.destroy();
211
+ }
212
+ catch (_) { /* client gone */ } });
213
+ res.on('close', () => { try {
214
+ tar.kill();
215
+ }
216
+ catch (_) { /* already exited */ } });
217
+ };
218
+ // Resolve symlinks before trusting a path: a project directory that is a link would otherwise read
219
+ // outside the workspace, and the dashboard container bind-mounts ~/.claude as well as ~/workspace.
220
+ const insideStore = (p, wantDir) => {
221
+ try {
222
+ const base = fs_1.default.realpathSync(path_1.default.join(ROOT, 'graphs'));
223
+ const real = fs_1.default.realpathSync(p);
224
+ if (!real.startsWith(base + path_1.default.sep))
225
+ return null;
226
+ const st = fs_1.default.statSync(real);
227
+ return (wantDir ? st.isDirectory() : st.isFile()) ? real : null;
228
+ }
229
+ catch (_) {
230
+ return null;
231
+ }
232
+ };
233
+ api.addPage(/^\/obsidian\/([a-z0-9_-]+)\/([a-zA-Z0-9_.-]+)\/(index|download|view|canvas|frame)$/, ({ req, res, match }) => {
234
+ const [, wsKey, project, what] = match;
235
+ if (!isValidWorkspace(wsKey) || project.includes('..')) {
236
+ res.writeHead(400);
237
+ return res.end('bad name');
238
+ }
239
+ const real = insideStore(vaultDir(wsKey, project), true);
240
+ if (!real) {
241
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
242
+ return res.end('No vault yet — build one from the Graph menu on the project row.');
243
+ }
244
+ const q = new URL(String(req.url || '/'), 'http://localhost').searchParams;
245
+ if (what === 'frame') {
246
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
247
+ return res.end(vaultFrame(wsKey, project, String(q.get('what') || 'view')));
248
+ }
249
+ if (what === 'view' || what === 'canvas') {
250
+ let page;
251
+ try {
252
+ page = what === 'view' ? vaultView(wsKey, project, String(q.get('note') || ''), cssVersionNow()) : vaultCanvas(wsKey, project, cssVersionNow());
253
+ }
254
+ catch (e) {
255
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
256
+ return res.end(`Could not read the vault: ${e.message}`);
257
+ }
258
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
259
+ return res.end(page);
260
+ }
261
+ if (what === 'index') {
262
+ let md;
263
+ try {
264
+ md = fs_1.default.readFileSync(path_1.default.join(real, indexNote(project)), 'utf8');
265
+ }
266
+ catch (_) {
267
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
268
+ return res.end('<div class="msg error">The vault has no index note. Rebuild it.</div>');
269
+ }
270
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
271
+ return res.end(`<h3>${esc(project)} <span class="uk-text-meta">Obsidian vault</span></h3>
272
+ <p class="uk-text-meta">${esc(real)}</p>
273
+ <p><a class="uk-button uk-button-primary uk-button-small" href="${esc(`obsidian://open?path=${encodeURIComponent(real)}`)}">Open in Obsidian</a>
274
+ <a class="uk-button uk-button-default uk-button-small" href="/obsidian/${esc(wsKey)}/${esc(project)}/download">Download</a></p>
275
+ <pre class="uk-background-muted uk-padding-small" style="white-space:pre-wrap">${esc(md)}</pre>`);
276
+ }
277
+ return tarOut(res, real, `${wsKey}--${project}--obsidian.tar.gz`);
278
+ });
279
+ api.addPage(/^\/graph\/([a-z0-9_-]+)\/([a-zA-Z0-9_.-]+)\/download$/, ({ res, match }) => {
280
+ const [, wsKey, project] = match;
281
+ if (!isValidWorkspace(wsKey) || project.includes('..')) {
282
+ res.writeHead(400);
283
+ return res.end('bad name');
284
+ }
285
+ const real = insideStore(graphStoreDir(wsKey, project), true);
286
+ if (!real) {
287
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
288
+ return res.end('No graph yet — build one from the Graph menu on the project row.');
289
+ }
290
+ return tarOut(res, real, `${wsKey}--${project}--graph.tar.gz`);
291
+ });
292
+ api.addPage(/^\/graph\/([a-z0-9_-]+)\/([a-zA-Z0-9_-]+)(\/report)?$/, ({ res, match }) => {
293
+ const [, wsKey, project, report] = match;
294
+ if (!isValidWorkspace(wsKey)) {
295
+ res.writeHead(404);
296
+ return res.end();
297
+ }
298
+ const real = insideStore(path_1.default.join(graphStoreDir(wsKey, project), report ? 'GRAPH_REPORT.md' : 'graph.html'), false);
299
+ if (!real) {
300
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
301
+ return res.end('No graph yet — press Graph on the project row to build one.');
302
+ }
303
+ // graph.html is generated from the project's own code: scripts sandboxed away from this origin, streamed.
304
+ res.writeHead(200, { 'Content-Type': report ? 'text/plain; charset=utf-8' : 'text/html; charset=utf-8', 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': 'sandbox allow-scripts allow-popups;' });
305
+ return fs_1.default.createReadStream(real).pipe(res);
306
+ });
307
+ api.addAssistantContext(() => 'The graphs/ workspace holds one knowledge graph per project (built by cmd-tools-graphify.sh, @webship.co/workspace-graph); the Graph menu on a project row builds, opens, downloads, serves over MCP or deletes it, and makes an Obsidian vault from it.');
308
+ }
309
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Where a project's knowledge graph lives, and what is in it.
5
+ *
6
+ * The graph is kept in the graphs workspace rather than inside the project — it is tens of MB, it
7
+ * re-diffs entirely on every rebuild, and half the projects worth mapping are repositories we ship.
8
+ * Everything here reads that store; building one is `cmd-tools-graphify.sh`.
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ // The workspace root: WORKSPACE_ROOT when set, else the nearest ancestor with core/config/settings.yml
13
+ // (the package sits under core/workspace-app/node_modules).
14
+ function findRoot() {
15
+ let dir = __dirname;
16
+ for (let i = 0; i < 12; i++) {
17
+ if (fs.existsSync(path.join(dir, 'core', 'config', 'settings.yml')))
18
+ return dir;
19
+ const up = path.dirname(dir);
20
+ if (up === dir)
21
+ break;
22
+ dir = up;
23
+ }
24
+ return path.resolve(__dirname, '..', '..', '..', '..', '..', '..', '..');
25
+ }
26
+ const ROOT = process.env.WORKSPACE_ROOT || findRoot();
27
+ // Where a project's graph lives: the graphs workspace, never inside the project. graphify only
28
+ // writes <project>/graphify-out, so cmd-tools-graphify.sh moves the result here — a graph is tens
29
+ // of MB, it re-diffs entirely on every rebuild, and half the projects worth mapping are
30
+ // repositories we ship.
31
+ function graphStoreDir(wsKey, project) {
32
+ return path.join(ROOT, 'graphs', wsKey, project);
33
+ }
34
+ // Graph lookups per project row, memoised for 2s: projectRowsHtml is
35
+ // re-rendered on every job poll.
36
+ let _graphCache = new Map();
37
+ let _graphCacheAt = 0;
38
+ function hasGraph(wsKey, project, file) {
39
+ const now = Date.now();
40
+ if (now - _graphCacheAt > 2000) {
41
+ _graphCache = new Map();
42
+ _graphCacheAt = now;
43
+ }
44
+ const k = `${wsKey}/${project}/${file}`;
45
+ if (!_graphCache.has(k))
46
+ _graphCache.set(k, fs.existsSync(path.join(graphStoreDir(wsKey, project), file)));
47
+ return _graphCache.get(k);
48
+ }
49
+ // Node and edge counts, memoised on the graph file's identity so paging a long list re-reads
50
+ // nothing. Keyed on mtime+size: a rebuild changes both.
51
+ const _graphCounts = new Map();
52
+ function graphCounts(file, stat) {
53
+ if (!stat)
54
+ return { nodes: null, links: null };
55
+ const key = `${file}:${stat.mtimeMs}:${stat.size}`;
56
+ if (_graphCounts.has(key))
57
+ return _graphCounts.get(key);
58
+ let counts = { nodes: null, links: null };
59
+ try {
60
+ const d = JSON.parse(fs.readFileSync(file, 'utf8'));
61
+ counts = { nodes: (d.nodes || []).length, links: (d.links || d.edges || []).length };
62
+ }
63
+ catch (_) { /* unreadable or half-written */ }
64
+ _graphCounts.set(key, counts);
65
+ return counts;
66
+ }
67
+ // Every graph in the store, as <workspace>/<project> rows. Reads the two levels the build writes
68
+ // and nothing else, so a stray file in graphs/ cannot become a row.
69
+ function listGraphs() {
70
+ const base = path.join(ROOT, 'graphs');
71
+ const rows = [];
72
+ let workspaces = [];
73
+ try {
74
+ workspaces = fs.readdirSync(base, { withFileTypes: true }).filter((e) => e.isDirectory());
75
+ }
76
+ catch (_) {
77
+ return rows;
78
+ }
79
+ for (const ws of workspaces) {
80
+ let projects = [];
81
+ try {
82
+ projects = fs.readdirSync(path.join(base, ws.name), { withFileTypes: true }).filter((e) => e.isDirectory());
83
+ }
84
+ catch (_) {
85
+ continue;
86
+ }
87
+ for (const pr of projects) {
88
+ const dir = path.join(base, ws.name, pr.name);
89
+ const stat = (f) => { try {
90
+ return fs.statSync(path.join(dir, f));
91
+ }
92
+ catch (_) {
93
+ return null;
94
+ } };
95
+ const json = stat('graph.json');
96
+ // The counts come from graph.json itself — .graphify_build.json carries only the excludes,
97
+ // so the numbers were rendering empty. Parsing is cheap enough (20ms for a 2,430-node graph)
98
+ // and the result is memoised on the file's mtime and size, so a re-render costs nothing.
99
+ const nodes_links = graphCounts(path.join(dir, 'graph.json'), json);
100
+ const nodes = nodes_links.nodes;
101
+ const links = nodes_links.links;
102
+ let size = 0;
103
+ const walk = (d) => {
104
+ let entries = [];
105
+ try {
106
+ entries = fs.readdirSync(d, { withFileTypes: true });
107
+ }
108
+ catch (_) {
109
+ return;
110
+ }
111
+ for (const e of entries) {
112
+ const full = path.join(d, e.name);
113
+ if (e.isDirectory())
114
+ walk(full);
115
+ else {
116
+ try {
117
+ size += fs.statSync(full).size;
118
+ }
119
+ catch (_) { /* gone */ }
120
+ }
121
+ }
122
+ };
123
+ walk(dir);
124
+ rows.push({
125
+ workspace: ws.name,
126
+ project: pr.name,
127
+ name: `${ws.name}/${pr.name}`,
128
+ mtime: json ? json.mtime : new Date(0),
129
+ size,
130
+ nodes,
131
+ links,
132
+ hasHtml: !!stat('graph.html'),
133
+ hasReport: !!stat('GRAPH_REPORT.md'),
134
+ projectExists: fs.existsSync(path.join(ROOT, ws.name, pr.name)),
135
+ });
136
+ }
137
+ }
138
+ return rows;
139
+ }
140
+ module.exports = { ROOT, graphStoreDir, hasGraph, graphCounts, listGraphs };
141
+ //# sourceMappingURL=graphs.js.map