graphlin 0.1.0 → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local architecture and activity viewer with passive, bounded event hooks.",
5
5
  "author": {
6
6
  "name": "Graphlin contributors"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local architecture and activity diagrams from observable coding-agent work.",
5
5
  "author": {
6
6
  "name": "Graphlin contributors"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Live architecture and activity diagrams from observable coding-agent work.",
5
5
  "private": false,
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "graphlin",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "description": "Local architecture and activity diagrams from observable coding-agent work.",
6
6
  "extensions": {
7
7
  "com.openai": {
@@ -1,9 +1,8 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { lstat, open } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
3
4
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
5
 
6
- const PLUGIN_ROOT = fileURLToPath(new URL('../../', import.meta.url));
7
6
  const PROFILES = new Set(['claude', 'codex', 'portable']);
8
7
  const MAX_PATH_BYTES = 4096;
9
8
  const MAX_COMMAND_BYTES = 8192;
@@ -68,18 +67,6 @@ async function packageMarker(root) {
68
67
  return PROFILES.has(profile) ? { status: 'present', profile } : { status: 'invalid' };
69
68
  }
70
69
 
71
- async function sourceCheckout(root, current) {
72
- const [claude, codex, builder] = await Promise.all([
73
- json(path.join(root, '.claude-plugin/plugin.json')),
74
- json(path.join(root, '.codex-plugin/plugin.json')),
75
- regularFile(path.join(root, 'scripts/build-packages.mjs')),
76
- ]);
77
- return builder && manifest(claude) && manifest(codex) &&
78
- claude.version === current.version && codex.version === current.version &&
79
- claude.hooks === './adapters/claude/hooks.json' &&
80
- current.extensions?.['com.openai']?.hooks === './adapters/codex/hooks.json';
81
- }
82
-
83
70
  async function hostPackage(root, host, currentVersion) {
84
71
  const [isDirectory, marker, portable, native, mcp, hooks, ...files] = await Promise.all([
85
72
  directory(root), packageMarker(root), json(path.join(root, 'plugin.json')),
@@ -101,17 +88,6 @@ async function hostPackage(root, host, currentVersion) {
101
88
  portable.extensions?.['com.openai']?.hooks === './adapters/codex/hooks.json');
102
89
  }
103
90
 
104
- async function localMarketplace(root, codexRoot) {
105
- const value = await json(path.join(root, '.agents/plugins/marketplace.json'));
106
- if (!record(value) || value.name !== 'graphlin-local' || !Array.isArray(value.plugins)) return false;
107
- const entries = value.plugins.filter(entry => entry?.name === 'graphlin');
108
- if (entries.length !== 1) return false;
109
- const entry = entries[0];
110
- return entry.source?.source === 'local' && entry.source.path === './graphlin' &&
111
- path.join(root, 'graphlin') === codexRoot &&
112
- entry.policy?.installation === 'AVAILABLE' && entry.policy?.authentication === 'ON_INSTALL';
113
- }
114
-
115
91
  export async function inspectInstalledPackages({ dataDir, version: currentVersion }) {
116
92
  if (!version(currentVersion)) return { claude: false, codex: false };
117
93
  const root = path.join(absolute(dataDir), 'plugins', 'graphlin', currentVersion);
@@ -120,116 +96,55 @@ export async function inspectInstalledPackages({ dataDir, version: currentVersio
120
96
  return { claude, codex };
121
97
  }
122
98
 
123
- // Discovery reads only fixed package metadata and checks file availability.
124
- // It never scans the project, reads state/credentials, or executes commands.
125
- async function discover(pluginRoot, dataDir) {
126
- const [marker, current, isDirectory] = await Promise.all([
127
- packageMarker(pluginRoot), json(path.join(pluginRoot, 'plugin.json')), directory(pluginRoot),
128
- ]);
129
- if (!isDirectory || !manifest(current) || marker.status === 'invalid') return { available: false };
130
- if (marker.status === 'missing') {
131
- if (!await sourceCheckout(pluginRoot, current)) return { available: false };
132
- // npm installations can be read-only. Generated host packages belong in
133
- // the same user-owned data directory as the running service, not beside
134
- // installed code. Versioned paths stay stable across projects and restarts.
135
- const output = path.join(dataDir, 'plugins', 'graphlin', current.version);
136
- const claudeRoot = path.join(output, 'claude/graphlin');
137
- const codexRoot = path.join(output, 'codex/graphlin');
138
- const [claudeReady, codexReady, marketplaceReady] = await Promise.all([
139
- hostPackage(claudeRoot, 'claude', current.version),
140
- hostPackage(codexRoot, 'codex', current.version),
141
- localMarketplace(path.dirname(codexRoot), codexRoot),
142
- ]);
143
- return {
144
- available: true,
145
- ...(claudeReady && codexReady && marketplaceReady ? {} : { build: path.join(pluginRoot, 'scripts/build-packages.mjs') }),
146
- output,
147
- claude: claudeRoot,
148
- marketplace: path.join(output, 'codex'),
149
- };
150
- }
151
- const distribution = path.resolve(pluginRoot, '../..');
152
- const claudeRoot = marker.profile === 'claude' ? pluginRoot : path.join(distribution, 'claude/graphlin');
153
- const codexRoot = marker.profile === 'codex' ? pluginRoot : path.join(distribution, 'codex/graphlin');
154
- const marketplaceRoot = path.dirname(codexRoot);
155
- const [claude, codex, marketplace] = await Promise.all([
156
- hostPackage(claudeRoot, 'claude', current.version),
157
- hostPackage(codexRoot, 'codex', current.version),
158
- localMarketplace(marketplaceRoot, codexRoot),
159
- ]);
160
- return {
161
- available: true, claude: claude ? claudeRoot : null,
162
- marketplace: codex && marketplace ? marketplaceRoot : null,
163
- };
164
- }
165
-
166
- function format({ projectRoot, dataDir, mode }, found) {
99
+ // Connection guidance uses only the current instance's trusted startup context.
100
+ // npm onboarding owns package installation; the guide neither discovers local
101
+ // package paths nor reads project settings, credentials, or host configuration.
102
+ function format({ projectRoot, dataDir, mode }) {
167
103
  const result = { projectRoot, mode, instructions: [], notes: [] };
168
- if (mode === 'demo') {
169
- result.notes.push('This viewer is in demo mode and uses fixture classifications. Open a live Graphlin viewer for your own project before connecting your work.');
170
- }
171
- if (!found.available) {
172
- result.notes.push('Connection setup is unavailable because this Graphlin source checkout or package could not be verified. Obtain a complete Graphlin distribution.');
173
- return result;
174
- }
175
- const terminal = words => `cd ${quote(projectRoot)} && GRAPHLIN_DATA_DIR=${quote(dataDir)} ${words}`;
104
+ const demo = mode === 'demo';
105
+ // defaultDataDir() includes the daemon's environment override. Comparing to
106
+ // it would wrongly hide custom directories needed by a second terminal.
107
+ const customDataDir = !demo && dataDir !== path.resolve(homedir(), '.local/state/graphlin');
108
+ const terminal = words => `${demo ? '' : `cd ${quote(projectRoot)} && `}${customDataDir ? `GRAPHLIN_DATA_DIR=${quote(dataDir)} ` : ''}${words}`;
176
109
  const instruction = (id, title, description, steps) => ({ id, title, description, steps });
177
- const fits = entries => entries.every(item => item.steps.every(step =>
178
- Buffer.byteLength(step.command) <= MAX_COMMAND_BYTES));
179
- function append(entries, host) {
180
- if (fits(entries)) result.instructions.push(...entries);
181
- else result.notes.push(`${host} commands are too long to display safely. Use shorter project, package, or data directory paths.`);
182
- }
183
-
184
- if (found.build) {
185
- const build = [instruction('build-packages', 'Build current plugin packages',
186
- 'Run this prerequisite first. It rebuilds both host profiles in your Graphlin data directory; existing generated packages may be out of date.',
187
- [{ label: 'Build packages in Terminal', command: terminal(`node ${quote(found.build)} --out ${quote(found.output)}`) }])];
188
- if (!fits(build)) {
189
- result.notes.push('The build command is too long to display safely. Use shorter project, package, or data directory paths.');
190
- return result;
191
- }
192
- result.instructions.push(...build);
193
- }
194
- const prerequisite = found.build ? 'Complete the build step above first. ' : '';
195
- if (found.claude) {
196
- const launch = terminal(`claude --plugin-dir ${quote(found.claude)}`);
197
- append([
198
- instruction('claude-new', 'Claude: new session',
199
- `${prerequisite}Start a new Claude session with this Graphlin plugin loaded.`,
200
- [{ label: 'Start Claude in Terminal', command: launch }]),
201
- instruction('claude-resume', 'Claude: resume',
202
- `${prerequisite}Instead of starting a new session, continue the most recent conversation in this project.`,
203
- [{ label: 'Resume Claude in Terminal', command: `${launch} --continue` }]),
204
- ], 'Claude');
110
+ const entries = [
111
+ instruction('npm-setup', demo ? '1. Start a live viewer' : 'Set up only if needed',
112
+ demo
113
+ ? 'Open a terminal in your own project. Run guided setup, choose Claude Code or Codex and source or metadata mode, then keep that terminal running. The live viewer opens automatically.'
114
+ : 'This viewer is already running. If you completed guided setup for this project and your chosen host, skip this step. Otherwise, run setup alone in a second terminal; init does not start another server.',
115
+ [{ label: demo ? 'In your project terminal' : 'Set up this project in a second terminal',
116
+ command: terminal(`npx --yes graphlin@latest${demo ? '' : ' init'}`),
117
+ description: 'Choose your host and source or metadata mode. Source mode needs a TypeSafe API key; enter it at the masked prompt if asked. Metadata mode needs no key.' }]),
118
+ instruction('claude-new', demo ? '2. Start Claude Code (choose one agent)' : 'Start Claude Code (choose one agent)',
119
+ 'After setup, open a second terminal in the same project and start a new agent session. Accept the project trust prompt.',
120
+ [{ label: 'Start Claude in the second terminal', command: terminal('claude') },
121
+ { label: 'Inside Claude: /plugin', command: '/plugin',
122
+ description: 'Confirm Graphlin is enabled.' }]),
123
+ instruction('codex-new', demo ? '2. Start Codex (choose one agent)' : 'Start Codex (choose one agent)',
124
+ 'After setup, open a second terminal in the same project and start a new agent session. Accept the project trust prompt.',
125
+ [{ label: 'Start Codex in the second terminal', command: terminal('codex') },
126
+ { label: 'Inside Codex: /hooks', command: '/hooks',
127
+ description: 'Review and trust Graphlin hooks before starting your work.' }]),
128
+ ];
129
+ // Omit the whole guide rather than offering launch commands without setup or
130
+ // truncating a shell argument. The viewer applies the same command limit.
131
+ if (entries.every(item => item.steps.every(step => Buffer.byteLength(step.command) <= MAX_COMMAND_BYTES))) {
132
+ result.instructions.push(...entries);
205
133
  } else {
206
- result.notes.push('Claude connection is unavailable: matching packaged files were not found. Rebuild or obtain the complete Graphlin distribution, including its Claude profile.');
134
+ result.notes.push('Connection commands are too long to display safely. Use shorter project or data directory paths.');
207
135
  }
208
- if (found.marketplace) {
209
- const hooks = () => ({
210
- label: 'Inside Codex: /hooks', command: '/hooks',
211
- description: 'Inside Codex, review and trust Graphlin hooks; then start your work',
212
- });
213
- const launch = terminal(`codex -C ${quote(projectRoot)}`);
214
- append([
215
- instruction('codex-setup', 'Codex: install the local plugin',
216
- `${prerequisite}Run both setup commands before launching. They register this local marketplace and install its Graphlin package in Codex.`,
217
- [
218
- { label: 'Register marketplace in Terminal', command: terminal(`codex plugin marketplace add ${quote(found.marketplace)}`) },
219
- { label: 'Install Graphlin in Terminal', command: terminal(`codex plugin add ${quote('graphlin@graphlin-local')}`) },
220
- ]),
221
- instruction('codex-new', 'Codex: new session',
222
- 'Complete Codex setup above, then start a new session and review the hooks inside Codex.',
223
- [{ label: 'Start Codex in Terminal', command: launch }, hooks()]),
224
- instruction('codex-resume', 'Codex: resume',
225
- 'Complete Codex setup above. Instead of starting a new session, resume the most recent conversation in this project.',
226
- [{ label: 'Resume Codex in Terminal', command: `${launch} resume --last` }, hooks()]),
227
- ], 'Codex');
136
+ if (demo) {
137
+ result.notes.push('This demo uses fixture classifications. Run the commands in your own project; its setup uses the normal data directory, not this demo’s custom directory.');
228
138
  } else {
229
- result.notes.push('Codex connection is unavailable: matching packaged files and a valid local marketplace were not found. Rebuild or obtain the complete Graphlin distribution, including its Codex marketplace.');
139
+ result.notes.push('Next time: npx --yes graphlin@latest in this project, then claude or codex in a second terminal. Keep the viewer running.');
140
+ result.notes.push('Consent or key changes require stopping and restarting this project’s viewer with the same data directory. Until then, its current policy and classifier configuration stay in effect.');
230
141
  }
231
- result.notes.push('Choose either a new session or resume. Keep the Graphlin viewer running while you work.');
232
- result.notes.push('Package discovery does not confirm that host hooks are active. Follow the host prompts to load and trust the plugin.');
142
+ result.notes.push(customDataDir
143
+ ? 'Commands preserve this viewer’s custom data directory. Use the same GRAPHLIN_DATA_DIR for future viewer launches.'
144
+ : 'Default data: ~/.local/state/graphlin. Unset GRAPHLIN_DATA_DIR in both terminals to use it.' +
145
+ (demo ? ' For an intentional custom location, set the same GRAPHLIN_DATA_DIR in both terminals.' : ''));
146
+ result.notes.push('Plugins install across projects; source consent is per project. Source mode sends locally filtered source excerpts, user prompts, and public agent messages to TypeSafe.');
147
+ result.notes.push('Installation does not confirm hook activation. After setup and trust, ask: “Orient yourself in this project: read its main files and explain how the components connect.” Watch for hook delivery and diagram updates.');
233
148
  return result;
234
149
  }
235
150
 
@@ -239,11 +154,7 @@ function format({ projectRoot, dataDir, mode }, found) {
239
154
  * body/query. projectRoot and dataDir are already canonicalized by projectPaths.
240
155
  * No environment, graph, launch URL, or credential is copied into the result.
241
156
  */
242
- export async function createConnectionInfo({
243
- projectRoot, dataDir, mode = 'live', pluginRoot = PLUGIN_ROOT,
244
- } = {}) {
157
+ export async function createConnectionInfo({ projectRoot, dataDir, mode = 'live' } = {}) {
245
158
  if (!['live', 'demo'].includes(mode)) throw new TypeError('invalid_connection_info');
246
- const context = { projectRoot: absolute(projectRoot), dataDir: absolute(dataDir), mode };
247
- const root = absolute(pluginRoot);
248
- return format(context, await discover(root, context.dataDir));
159
+ return format({ projectRoot: absolute(projectRoot), dataDir: absolute(dataDir), mode });
249
160
  }
@@ -876,6 +876,7 @@ export function startConnectionDialog({ load = () => request('/api/connection-in
876
876
  $('connection-project').hidden = true;
877
877
  $('connection-project').textContent = '';
878
878
  $('connection-demo').hidden = true;
879
+ $('connection-dialog-intro').textContent = 'Connect Claude Code or Codex with guided npm setup.';
879
880
  $('connection-copy-status').textContent = '';
880
881
  }
881
882
  function renderInfo(info, ticket) {
@@ -931,6 +932,10 @@ export function startConnectionDialog({ load = () => request('/api/connection-in
931
932
  $('connection-project').textContent = `Project: ${info.projectRoot}`;
932
933
  $('connection-project').hidden = !info.projectRoot;
933
934
  $('connection-demo').hidden = info.mode !== 'demo';
935
+ $('connection-demo').textContent = 'This is an offline demo. Start a live viewer in your own project to connect an agent.';
936
+ $('connection-dialog-intro').textContent = info.mode === 'demo'
937
+ ? 'Start Graphlin in your project, then start your agent in a second terminal in that same project.'
938
+ : 'Keep this viewer running. In a second terminal, set up if needed, then start a new agent session for the project below.';
934
939
  $('connection-notes').replaceChildren(...info.notes.map(note => html('li', note)));
935
940
  $('connection-notes').hidden = !info.notes.length;
936
941
  }
@@ -1864,7 +1869,7 @@ export function startViewer() {
1864
1869
  cancelMovement();
1865
1870
  fitCamera(graphBounds(graph));
1866
1871
  }
1867
- function zoom(factor) {
1872
+ function zoom(factor, anchor = { x: .5, y: .5 }) {
1868
1873
  if (!state.viewport || !state.fitBounds) return;
1869
1874
  cancelMovement();
1870
1875
  finishPan();
@@ -1872,8 +1877,8 @@ export function startViewer() {
1872
1877
  const scale = state.zoom / next;
1873
1878
  const old = state.viewport;
1874
1879
  state.viewport = {
1875
- x: old.x + old.width * (1 - scale) / 2,
1876
- y: old.y + old.height * (1 - scale) / 2,
1880
+ x: old.x + old.width * (1 - scale) * anchor.x,
1881
+ y: old.y + old.height * (1 - scale) * anchor.y,
1877
1882
  width: old.width * scale, height: old.height * scale,
1878
1883
  };
1879
1884
  state.zoom = next;
@@ -2493,6 +2498,28 @@ export function startViewer() {
2493
2498
  });
2494
2499
  $('zoom-in').addEventListener('click', () => zoom(1.25));
2495
2500
  $('zoom-out').addEventListener('click', () => zoom(.8));
2501
+ const onDiagramWheel = event => {
2502
+ if (state.closed || !state.viewport || !currentGraph()?.nodes.length || event.defaultPrevented ||
2503
+ event.shiftKey || !Number.isFinite(event.deltaY) || event.deltaY === 0 ||
2504
+ Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
2505
+ const rect = $('architecture').getBoundingClientRect();
2506
+ if (!(rect.width > 0 && rect.height > 0) ||
2507
+ !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return;
2508
+ // Wheel units are pixels, lines, or pages. Keep trackpad motion continuous,
2509
+ // but cap each event so a coarse wheel or a page delta cannot jump too far.
2510
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? rect.height : 1;
2511
+ const delta = Math.max(-100, Math.min(100, event.deltaY * unit));
2512
+ const box = state.viewport;
2513
+ const scale = Math.min(rect.width / box.width, rect.height / box.height);
2514
+ const width = box.width * scale, height = box.height * scale;
2515
+ const anchor = {
2516
+ x: (event.clientX - rect.left - (rect.width - width) / 2) / width,
2517
+ y: (event.clientY - rect.top - (rect.height - height) / 2) / height,
2518
+ };
2519
+ event.preventDefault();
2520
+ zoom(Math.exp(-delta * .002), anchor);
2521
+ };
2522
+ $('architecture').addEventListener('wheel', onDiagramWheel, { passive: false });
2496
2523
  $('architecture').addEventListener('keydown', event => {
2497
2524
  if (event.target !== $('architecture')) return;
2498
2525
  if (event.key === '+' || event.key === '=') { event.preventDefault(); zoom(1.25); }
@@ -2571,6 +2598,7 @@ export function startViewer() {
2571
2598
  $('onboarding-action').removeEventListener('click', onOnboardingAction);
2572
2599
  $('orientation-copy').removeEventListener('click', onCopyOrientation);
2573
2600
  $('activity-toggle').removeEventListener('click', onToggleActivity);
2601
+ $('architecture').removeEventListener('wheel', onDiagramWheel);
2574
2602
  connectionDialog.dispose();
2575
2603
  diagnosticsDialog.dispose();
2576
2604
  sidebar.destroy();
@@ -39,7 +39,7 @@ async function handle(line) {
39
39
  const supported = ['2024-11-05', '2025-03-26', '2025-06-18'];
40
40
  return result({ protocolVersion: supported.includes(message.params?.protocolVersion) ? message.params.protocolVersion : '2025-06-18',
41
41
  capabilities: { tools: { listChanged: false } },
42
- serverInfo: { name: 'graphlin', version: '0.1.0' },
42
+ serverInfo: { name: 'graphlin', version: '0.1.1' },
43
43
  instructions: 'Controls only. Passive host hooks provide observations when separately activated. No drawing calls after each action.' });
44
44
  }
45
45
  if (message.method === 'ping') return result({});