@untitled-devs/wasla 0.1.3 → 1.0.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 (60) hide show
  1. package/README.md +23 -25
  2. package/dist/{utils → apps/cli/src}/cli-output.d.ts +1 -1
  3. package/dist/{cli → apps/cli/src}/commands/config.js +2 -2
  4. package/dist/{cli → apps/cli/src}/commands/install.js +2 -2
  5. package/dist/{cli → apps/cli/src}/commands/register.js +5 -5
  6. package/dist/{cli → apps/cli/src}/commands/status.js +6 -6
  7. package/dist/{cli → apps/cli/src}/commands/sync-to.js +5 -5
  8. package/dist/{cli → apps/cli/src}/commands/sync.js +5 -5
  9. package/dist/{cli → apps/cli/src}/commands/watch.js +6 -6
  10. package/dist/{cli → apps/cli/src}/index.js +19 -7
  11. package/dist/{cli/commands/visualizer.d.ts → apps/cli/src/server/visualizer-server.d.ts} +0 -1
  12. package/dist/{cli/commands/visualizer.js → apps/cli/src/server/visualizer-server.js} +85 -31
  13. package/dist/{adapters → packages/adapters/src}/base.d.ts +1 -1
  14. package/dist/{adapters → packages/adapters/src}/claude.d.ts +1 -1
  15. package/dist/{adapters → packages/adapters/src}/claude.js +7 -4
  16. package/dist/{adapters → packages/adapters/src}/cursor.d.ts +1 -1
  17. package/dist/{adapters → packages/adapters/src}/cursor.js +2 -2
  18. package/dist/{adapters → packages/adapters/src}/factory.d.ts +1 -1
  19. package/dist/{adapters → packages/adapters/src}/gemini.d.ts +1 -1
  20. package/dist/{adapters → packages/adapters/src}/gemini.js +2 -2
  21. package/dist/{adapters → packages/adapters/src}/github-copilot-cli.d.ts +1 -1
  22. package/dist/{adapters → packages/adapters/src}/github-copilot-cli.js +2 -2
  23. package/dist/{adapters → packages/adapters/src}/github-copilot.d.ts +1 -1
  24. package/dist/{adapters → packages/adapters/src}/github-copilot.js +2 -2
  25. package/dist/{adapters → packages/adapters/src}/openclaw.d.ts +1 -1
  26. package/dist/{adapters → packages/adapters/src}/openclaw.js +2 -2
  27. package/dist/{adapters → packages/adapters/src}/opencode.d.ts +1 -1
  28. package/dist/{adapters → packages/adapters/src}/opencode.js +2 -2
  29. package/dist/{core → packages/core/src}/registry.d.ts +1 -1
  30. package/dist/{core → packages/core/src}/registry.js +2 -2
  31. package/dist/{core → packages/core/src}/visualizer-types.d.ts +8 -0
  32. package/dist/{syncer → packages/sync/src}/index.d.ts +12 -3
  33. package/dist/{syncer → packages/sync/src}/index.js +58 -35
  34. package/dist/{core → packages/sync/src}/scanner.d.ts +1 -1
  35. package/dist/{core → packages/sync/src}/scanner.js +3 -3
  36. package/dist/visualizer/assets/index-cU_xphSj.js +144 -0
  37. package/{src/visualizer/dist → dist/visualizer}/index.html +1 -1
  38. package/package.json +77 -63
  39. package/src/visualizer/dist/assets/index-C6aJB2Yl.js +0 -144
  40. /package/dist/{utils → apps/cli/src}/cli-output.js +0 -0
  41. /package/dist/{cli → apps/cli/src}/commands/config.d.ts +0 -0
  42. /package/dist/{cli → apps/cli/src}/commands/install.d.ts +0 -0
  43. /package/dist/{cli → apps/cli/src}/commands/register.d.ts +0 -0
  44. /package/dist/{cli → apps/cli/src}/commands/status.d.ts +0 -0
  45. /package/dist/{cli → apps/cli/src}/commands/sync-to.d.ts +0 -0
  46. /package/dist/{cli → apps/cli/src}/commands/sync.d.ts +0 -0
  47. /package/dist/{cli → apps/cli/src}/commands/watch.d.ts +0 -0
  48. /package/dist/{cli → apps/cli/src}/index.d.ts +0 -0
  49. /package/dist/{adapters → packages/adapters/src}/base.js +0 -0
  50. /package/dist/{adapters → packages/adapters/src}/factory.js +0 -0
  51. /package/dist/{core → packages/core/src}/types.d.ts +0 -0
  52. /package/dist/{core → packages/core/src}/types.js +0 -0
  53. /package/dist/{core → packages/core/src}/visualizer-types.js +0 -0
  54. /package/dist/{utils → packages/shared/src}/config.d.ts +0 -0
  55. /package/dist/{utils → packages/shared/src}/config.js +0 -0
  56. /package/dist/{utils → packages/shared/src}/fs.d.ts +0 -0
  57. /package/dist/{utils → packages/shared/src}/fs.js +0 -0
  58. /package/dist/{utils → packages/shared/src}/paths.d.ts +0 -0
  59. /package/dist/{utils → packages/shared/src}/paths.js +0 -0
  60. /package/{src/visualizer/dist → dist/visualizer}/logo.png +0 -0
package/README.md CHANGED
@@ -13,6 +13,9 @@
13
13
 
14
14
  [![MIT License](https://img.shields.io/badge/license-MIT-00C896?style=flat-square)](LICENSE)
15
15
  [![GitHub](https://img.shields.io/badge/github-The--Untitled--Org-00C896?style=flat-square&logo=github)](https://github.com/The-Untitled-Org/wasla-genie)
16
+ [![npm version](https://img.shields.io/npm/v/@untitled-devs/wasla?style=flat-square&logo=npm)](https://www.npmjs.com/package/@untitled-devs/wasla)
17
+ [![npm downloads](https://img.shields.io/npm/dm/@untitled-devs/wasla?style=flat-square&logo=npm)](https://www.npmjs.com/package/@untitled-devs/wasla)
18
+ [![GitHub Release](https://img.shields.io/github/v/release/The-Untitled-Org/wasla-genie?style=flat-square)](https://github.com/The-Untitled-Org/wasla-genie/releases)
16
19
  [![CI & Docs Deployment](https://github.com/The-Untitled-Org/wasla-genie/actions/workflows/ci-docs.yml/badge.svg)](https://github.com/The-Untitled-Org/wasla-genie/actions/workflows/ci-docs.yml)
17
20
  [![Coverage](https://codecov.io/gh/The-Untitled-Org/wasla-genie/branch/main/graph/badge.svg)](https://codecov.io/gh/The-Untitled-Org/wasla-genie)
18
21
  [![Status](https://img.shields.io/badge/status-alpha-orange?style=flat-square)]()
@@ -118,22 +121,21 @@ The same pattern applies across every asset type:
118
121
 
119
122
  ## 🚀 Installation
120
123
 
121
- WaslaGenie is cross-platform via `npx` — no global install required:
124
+ **Install globally:**
122
125
 
123
126
  ```bash
124
- npx @untitled-devs/wasla config --scope workspace
125
- npx @untitled-devs/wasla sync
127
+ npm i -g @untitled-devs/wasla
128
+ waslagenie config --scope workspace
129
+ waslagenie sync
126
130
  ```
127
131
 
128
- Choose `workspace` or `user` once before running operational commands. This runs the CLI directly.
129
- It does not register helper skills inside Claude, Gemini, or other tools.
132
+ Choose `workspace` or `user` once before running operational commands.
130
133
 
131
- **Or install globally:**
134
+ **Or run via `npx` (no global installation required):**
132
135
 
133
136
  ```bash
134
- npm install -g @untitled-devs/wasla
135
- waslagenie config --scope workspace
136
- waslagenie sync
137
+ npx @untitled-devs/wasla config --scope workspace
138
+ npx @untitled-devs/wasla sync
137
139
  ```
138
140
 
139
141
  Optional helper registration:
@@ -194,7 +196,7 @@ Use `npm run ...` while developing because it runs your local code (`dist`) afte
194
196
 
195
197
  If you run through `npm run ...` in this repo: **No reinstall needed**. Just run the script again; it rebuilds.
196
198
 
197
- If you installed globally with `npm install -g wasla-genie`: **Yes**, reinstall (or relink) to test your latest local changes.
199
+ If you installed globally with `npm i -g @untitled-devs/wasla`: **Yes**, reinstall (or relink) to test your latest local changes.
198
200
 
199
201
  For local development without repeated global installs:
200
202
 
@@ -378,22 +380,17 @@ Nothing is forced. Centralization is a convenience, not a requirement.
378
380
 
379
381
  ```
380
382
  wasla-genie/
381
- ├── src/
382
- │ ├── cli/ # CLI entry point and commands
383
- ├── scanner/ # Scans known tool config directories
384
- ├── registry/ # Builds and maintains the asset registry
385
- │ ├── syncer/ # Writes and tracks stub files
386
- │ ├── watcher/ # Daemon / filesystem watcher
387
- └── adapters/ # Per-tool directory knowledge + stub format
388
- ├── claude.js
389
- ├── gemini.js
390
- ├── codex.js
391
- │ ├── openclaw.js
392
- │ └── hermes.js
383
+ ├── apps/
384
+ │ ├── cli/src/ # CLI commands and visualizer server
385
+ └── visualizer/src/ # React visualizer
386
+ ├── packages/
387
+ │ ├── adapters/src/ # Per-tool directory knowledge + stub format
388
+ │ ├── core/src/ # Registry, scanner, and shared types
389
+ ├── shared/src/ # Shared config, filesystem, and path helpers
390
+ └── sync/src/ # Sync orchestration and filesystem watcher
391
+ ├── tests/
392
+ ├── scripts/
393
393
  ├── docs/
394
- │ ├── how-stubs-work.md
395
- │ ├── adapters.md
396
- │ └── roadmap.md
397
394
  ├── package.json
398
395
  └── README.md
399
396
  ```
@@ -418,6 +415,7 @@ Nothing is ever duplicated.
418
415
  git clone https://github.com/The-Untitled-Org/wasla-genie
419
416
  cd wasla-genie
420
417
  npm install
418
+ npm run visualizer:install
421
419
  npm run dev
422
420
  ```
423
421
 
@@ -1,4 +1,4 @@
1
- import type { Asset } from '../core/types.js';
1
+ import type { Asset } from '#core/types.js';
2
2
  export declare function banner(): void;
3
3
  export declare function success(message: string): void;
4
4
  export declare function error(message: string): void;
@@ -1,6 +1,6 @@
1
- import { section, success, error, spacer, info } from '../../utils/cli-output.js';
1
+ import { section, success, error, spacer, info } from '../cli-output.js';
2
2
  import prompts from 'prompts';
3
- import { getConfigPath, getConfiguredRegistryPath, readConfiguredScope, writeConfiguredScope, } from '../../utils/config.js';
3
+ import { getConfigPath, getConfiguredRegistryPath, readConfiguredScope, writeConfiguredScope, } from '#shared/config.js';
4
4
  export async function configCommand(options) {
5
5
  try {
6
6
  const currentScope = await readConfiguredScope();
@@ -1,5 +1,5 @@
1
- import { section, success, error, highlight, spacer } from '../../utils/cli-output.js';
2
- import { readConfiguredScope } from '../../utils/config.js';
1
+ import { section, success, error, highlight, spacer } from '../cli-output.js';
2
+ import { readConfiguredScope } from '#shared/config.js';
3
3
  export async function installCommand() {
4
4
  try {
5
5
  section('Preparing WaslaGenie CLI...');
@@ -1,8 +1,8 @@
1
- import { getInstalledAdapters } from '../../adapters/factory.js';
2
- import { section, success, error, warning, highlight, spacer } from '../../utils/cli-output.js';
3
- import { getRegistryDir } from '../../utils/paths.js';
4
- import { ensureDir } from '../../utils/fs.js';
5
- import { requireConfiguredScope } from '../../utils/config.js';
1
+ import { getInstalledAdapters } from '#adapters/factory.js';
2
+ import { section, success, error, warning, highlight, spacer } from '../cli-output.js';
3
+ import { getRegistryDir } from '#shared/paths.js';
4
+ import { ensureDir } from '#shared/fs.js';
5
+ import { requireConfiguredScope } from '#shared/config.js';
6
6
  export async function registerCommand(options = {}) {
7
7
  try {
8
8
  section('Detecting installed orchestrators...');
@@ -1,9 +1,9 @@
1
- import { RegistryManager } from '../../core/registry.js';
2
- import { Scanner } from '../../core/scanner.js';
3
- import { assetList, section, error, info, metric, spacer } from '../../utils/cli-output.js';
4
- import { fileExists } from '../../utils/fs.js';
5
- import { getRegistryPath } from '../../utils/paths.js';
6
- import { requireConfiguredScope } from '../../utils/config.js';
1
+ import { RegistryManager } from '#core/registry.js';
2
+ import { Scanner } from '#sync/scanner.js';
3
+ import { assetList, section, error, info, metric, spacer } from '../cli-output.js';
4
+ import { fileExists } from '#shared/fs.js';
5
+ import { getRegistryPath } from '#shared/paths.js';
6
+ import { requireConfiguredScope } from '#shared/config.js';
7
7
  export async function statusCommand() {
8
8
  try {
9
9
  const scope = await requireConfiguredScope();
@@ -1,8 +1,8 @@
1
- import { RegistryManager } from '../../core/registry.js';
2
- import { Scanner } from '../../core/scanner.js';
3
- import { Syncer } from '../../syncer/index.js';
4
- import { section, error, highlight, spacer } from '../../utils/cli-output.js';
5
- import { requireConfiguredScope } from '../../utils/config.js';
1
+ import { RegistryManager } from '#core/registry.js';
2
+ import { Scanner } from '#sync/scanner.js';
3
+ import { Syncer } from '#sync/index.js';
4
+ import { section, error, highlight, spacer } from '../cli-output.js';
5
+ import { requireConfiguredScope } from '#shared/config.js';
6
6
  export async function syncToCommand(options) {
7
7
  try {
8
8
  const scope = await requireConfiguredScope();
@@ -1,8 +1,8 @@
1
- import { RegistryManager } from '../../core/registry.js';
2
- import { Scanner } from '../../core/scanner.js';
3
- import { Syncer } from '../../syncer/index.js';
4
- import { assetList, bulletPoint, section, error, highlight, info, metric, spacer, } from '../../utils/cli-output.js';
5
- import { getConfiguredRegistryPath, requireConfiguredScope } from '../../utils/config.js';
1
+ import { RegistryManager } from '#core/registry.js';
2
+ import { Scanner } from '#sync/scanner.js';
3
+ import { Syncer } from '#sync/index.js';
4
+ import { assetList, bulletPoint, section, error, highlight, info, metric, spacer, } from '../cli-output.js';
5
+ import { getConfiguredRegistryPath, requireConfiguredScope } from '#shared/config.js';
6
6
  import { configCommand } from './config.js';
7
7
  export async function syncCommand(options = {}) {
8
8
  try {
@@ -1,10 +1,10 @@
1
1
  import chokidar from 'chokidar';
2
- import { RegistryManager } from '../../core/registry.js';
3
- import { Scanner } from '../../core/scanner.js';
4
- import { Syncer } from '../../syncer/index.js';
5
- import { getAllAdapters } from '../../adapters/factory.js';
6
- import { section, success, error, info, spacer } from '../../utils/cli-output.js';
7
- import { requireConfiguredScope } from '../../utils/config.js';
2
+ import { RegistryManager } from '#core/registry.js';
3
+ import { Scanner } from '#sync/scanner.js';
4
+ import { Syncer } from '#sync/index.js';
5
+ import { getAllAdapters } from '#adapters/factory.js';
6
+ import { section, success, error, info, spacer } from '../cli-output.js';
7
+ import { requireConfiguredScope } from '#shared/config.js';
8
8
  export async function watchCommand() {
9
9
  try {
10
10
  const scope = await requireConfiguredScope();
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { readFileSync } from 'fs';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
4
6
  import { installCommand } from './commands/install.js';
5
7
  import { registerCommand } from './commands/register.js';
6
8
  import { syncCommand } from './commands/sync.js';
@@ -8,14 +10,26 @@ import { syncToCommand } from './commands/sync-to.js';
8
10
  import { statusCommand } from './commands/status.js';
9
11
  import { configCommand } from './commands/config.js';
10
12
  import { watchCommand } from './commands/watch.js';
11
- import { visualizerCommand } from './commands/visualizer.js';
12
- import { banner } from '../utils/cli-output.js';
13
+ import { visualizerCommand } from './server/visualizer-server.js';
14
+ import { banner } from './cli-output.js';
13
15
  const program = new Command();
14
- const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf-8'));
16
+ function readPackageVersion(moduleUrl) {
17
+ let directory = dirname(fileURLToPath(moduleUrl));
18
+ while (true) {
19
+ const packagePath = join(directory, 'package.json');
20
+ if (existsSync(packagePath)) {
21
+ return JSON.parse(readFileSync(packagePath, 'utf-8')).version;
22
+ }
23
+ const parent = dirname(directory);
24
+ if (parent === directory)
25
+ throw new Error('Cannot find package.json');
26
+ directory = parent;
27
+ }
28
+ }
15
29
  program
16
30
  .name('waslagenie')
17
31
  .description('Universal synchronization layer for AI agent orchestrators')
18
- .version(packageVersion.version);
32
+ .version(readPackageVersion(import.meta.url));
19
33
  program.addCommand(new Command('install').description('Prepare WaslaGenie CLI state').action(installCommand));
20
34
  program.addCommand(new Command('register')
21
35
  .option('--to <targets>', 'Target provider(s), comma-separated. Example: claude,gemini')
@@ -41,13 +55,11 @@ program.addCommand(new Command('config')
41
55
  }));
42
56
  program.addCommand(new Command('watch').description('Watch for changes and auto-sync').action(() => watchCommand()));
43
57
  program.addCommand(new Command('visualizer')
44
- .option('--host <host>', 'Host to bind', '127.0.0.1')
45
58
  .option('--port <port>', 'Port to bind', '4072')
46
59
  .option('--no-open', 'Do not open browser automatically')
47
60
  .description('Open interactive sync visualizer with built-in backend')
48
61
  .action((options) => visualizerCommand(options)));
49
62
  program.addCommand(new Command('ui')
50
- .option('--host <host>', 'Host to bind', '127.0.0.1')
51
63
  .option('--port <port>', 'Port to bind', '4072')
52
64
  .option('--no-open', 'Do not open browser automatically')
53
65
  .description('Alias for `visualizer`')
@@ -1,7 +1,6 @@
1
1
  export declare function resolveVisualizerDist(moduleUrl: string): string;
2
2
  interface VisualizerOptions {
3
3
  port?: string;
4
- host?: string;
5
4
  noOpen?: boolean;
6
5
  }
7
6
  export declare const PROVIDER_ICONS: Record<string, string>;
@@ -5,20 +5,25 @@ import { readFile } from 'fs/promises';
5
5
  import { existsSync } from 'fs';
6
6
  import { exec } from 'child_process';
7
7
  import { WebSocketServer } from 'ws';
8
- import { Scanner } from '../../core/scanner.js';
9
- import { RegistryManager } from '../../core/registry.js';
10
- import { getInstalledAdapters } from '../../adapters/factory.js';
11
- import { error, highlight, info, section, spacer } from '../../utils/cli-output.js';
12
- import { Syncer } from '../../syncer/index.js';
13
- import { requireConfiguredScope } from '../../utils/config.js';
8
+ import { RegistryManager } from '#core/registry.js';
9
+ import { getAllAdapters, getInstalledAdapters } from '#adapters/factory.js';
10
+ import { error, highlight, info, section, spacer } from '../cli-output.js';
11
+ import { Syncer } from '#sync/index.js';
12
+ import { Scanner } from '#sync/scanner.js';
13
+ import { requireConfiguredScope } from '#shared/config.js';
14
14
  export function resolveVisualizerDist(moduleUrl) {
15
- return resolve(dirname(fileURLToPath(moduleUrl)), '../../../src/visualizer/dist');
15
+ return resolve(dirname(fileURLToPath(moduleUrl)), '../../../../visualizer');
16
16
  }
17
- async function getEntityContent(scope, type, name) {
17
+ async function getEntityContent(scope, type, name, providerId) {
18
18
  const scanner = new Scanner(scope);
19
19
  await scanner.initialize();
20
- const discovered = await scanner.scanAllTools();
21
- const target = discovered.find((item) => mapType(item.type) === type && item.name === name);
20
+ const assetType = mapEntityType(type);
21
+ const discovered = providerId === 'waslagenie'
22
+ ? await scanner.scanAllTools([assetType])
23
+ : await scanner.scanTool(providerId, [assetType]);
24
+ const target = discovered
25
+ .filter((item) => mapType(item.type) === type && item.name === name)
26
+ .sort((a, b) => b.modifiedAt - a.modifiedAt)[0];
22
27
  if (!target)
23
28
  return null;
24
29
  if (target.content)
@@ -30,11 +35,23 @@ async function getEntityContent(scope, type, name) {
30
35
  return null;
31
36
  }
32
37
  }
38
+ function isVisualizerEntityType(type) {
39
+ return type === 'instruction' || type === 'agent' || type === 'skill' || type === 'mcp';
40
+ }
33
41
  function mapType(type) {
34
42
  if (type === 'context')
35
43
  return 'instruction';
36
44
  return type;
37
45
  }
46
+ function isAllowedOrigin(origin, port) {
47
+ if (!origin)
48
+ return true;
49
+ return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
50
+ }
51
+ function isKnownProvider(scope, providerId) {
52
+ return (providerId === 'waslagenie' ||
53
+ getAllAdapters(scope).some((adapter) => adapter.name === providerId));
54
+ }
38
55
  function mapEntityType(type) {
39
56
  if (type === 'instruction')
40
57
  return 'context';
@@ -85,12 +102,20 @@ async function buildConfig(scope) {
85
102
  await scanner.initialize();
86
103
  const discovered = await scanner.scanAllTools();
87
104
  const installed = await getInstalledAdapters(scope);
105
+ const installedNames = new Set(installed.map((adapter) => adapter.name));
88
106
  const providers = [
89
- { id: 'waslagenie', title: 'WaslaGenie', iconUrl: PROVIDER_ICONS.waslagenie, isHub: true },
90
- ...installed.map((adapter) => ({
107
+ {
108
+ id: 'waslagenie',
109
+ title: 'WaslaGenie',
110
+ iconUrl: PROVIDER_ICONS.waslagenie,
111
+ isHub: true,
112
+ isInstalled: true,
113
+ },
114
+ ...getAllAdapters(scope).map((adapter) => ({
91
115
  id: adapter.name,
92
116
  title: adapter.displayName,
93
117
  iconUrl: PROVIDER_ICONS[adapter.name],
118
+ isInstalled: installedNames.has(adapter.name),
94
119
  })),
95
120
  ];
96
121
  const entityMap = new Map();
@@ -129,7 +154,6 @@ async function buildConfig(scope) {
129
154
  providers,
130
155
  entities: grouped,
131
156
  connections: Array.from(connectionMap.values()),
132
- websocketUrl: '',
133
157
  };
134
158
  }
135
159
  function sendJson(res, statusCode, body) {
@@ -140,7 +164,7 @@ function sendJson(res, statusCode, body) {
140
164
  export async function visualizerCommand(options) {
141
165
  try {
142
166
  const scope = await requireConfiguredScope();
143
- const host = options.host || '127.0.0.1';
167
+ const host = '127.0.0.1';
144
168
  const port = Number(options.port || 4072);
145
169
  const shouldOpen = options.noOpen !== true;
146
170
  section('Starting visualizer...');
@@ -161,36 +185,43 @@ export async function visualizerCommand(options) {
161
185
  };
162
186
  const server = createServer(async (req, res) => {
163
187
  const url = req.url || '/';
164
- if (req.method === 'GET' && url === '/api/config') {
165
- const freshConfig = await buildConfig(scope);
166
- freshConfig.websocketUrl = `ws://${host}:${port}`;
167
- sendJson(res, 200, freshConfig);
188
+ const parsed = new URL(`http://${host}:${port}${url}`);
189
+ const pathname = parsed.pathname;
190
+ if (!isAllowedOrigin(req.headers.origin, port)) {
191
+ sendJson(res, 403, { error: 'Forbidden origin' });
168
192
  return;
169
193
  }
170
- if (req.method === 'GET' && url.startsWith('/api/entity-content')) {
171
- const parsed = new URL(`http://${host}:${port}${url}`);
194
+ if (req.method === 'GET' && pathname === '/api/config') {
195
+ sendJson(res, 200, await buildConfig(scope));
196
+ return;
197
+ }
198
+ if (req.method === 'GET' && pathname === '/api/entity-content') {
172
199
  const type = parsed.searchParams.get('type');
173
200
  const name = parsed.searchParams.get('name');
174
- if (!type || !name) {
175
- sendJson(res, 400, { error: 'type and name are required' });
201
+ const providerId = parsed.searchParams.get('provider');
202
+ if (!isVisualizerEntityType(type) ||
203
+ !name ||
204
+ !providerId ||
205
+ !isKnownProvider(scope, providerId)) {
206
+ sendJson(res, 400, { error: 'valid type, name, and provider are required' });
176
207
  return;
177
208
  }
178
- const content = await getEntityContent(scope, type, name);
209
+ const content = await getEntityContent(scope, type, name, providerId);
179
210
  sendJson(res, 200, { content: content ?? '' });
180
211
  return;
181
212
  }
182
- if (req.method === 'POST' && url === '/api/shutdown') {
213
+ if (req.method === 'POST' && pathname === '/api/shutdown') {
183
214
  sendJson(res, 200, { ok: true });
184
215
  setTimeout(shutdown, 50);
185
216
  return;
186
217
  }
187
- if (url === '/' || url === '/index.html') {
218
+ if (pathname === '/' || pathname === '/index.html') {
188
219
  const html = await readFile(join(visualizerDist, 'index.html'));
189
220
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
190
221
  res.end(html);
191
222
  return;
192
223
  }
193
- const staticPath = join(visualizerDist, url.replace(/^\//, ''));
224
+ const staticPath = join(visualizerDist, pathname.replace(/^\//, ''));
194
225
  if (!staticPath.startsWith(visualizerDist) || !existsSync(staticPath)) {
195
226
  res.writeHead(404);
196
227
  res.end('Not Found');
@@ -200,15 +231,24 @@ export async function visualizerCommand(options) {
200
231
  res.writeHead(200, { 'Content-Type': mimeType(staticPath) });
201
232
  res.end(payload);
202
233
  });
203
- const wss = new WebSocketServer({ server });
234
+ const wss = new WebSocketServer({
235
+ server,
236
+ verifyClient: ({ origin }) => isAllowedOrigin(origin, port),
237
+ });
204
238
  wss.on('connection', (socket) => {
205
239
  socket.on('message', async (raw) => {
240
+ let requestId = '';
206
241
  try {
207
242
  const data = JSON.parse(raw.toString());
208
- if (data.type !== 'CONNECTION_CHANGED' || !data.payload)
243
+ if (data.type !== 'CONNECTION_CHANGED' || !data.requestId || !data.payload)
209
244
  return;
245
+ requestId = data.requestId;
210
246
  const { sourceEntity, entityType, sourceProvider, targetProvider, action } = data.payload;
211
- if (!sourceEntity || !entityType || !sourceProvider || !targetProvider || !action) {
247
+ if (!sourceEntity ||
248
+ !isVisualizerEntityType(entityType) ||
249
+ !sourceProvider ||
250
+ !targetProvider ||
251
+ (action !== 'ATTACH' && action !== 'DETACH')) {
212
252
  return;
213
253
  }
214
254
  info(`[Visualizer] ${action} ${entityType}:${sourceEntity} ${action === 'ATTACH' ? 'to' : 'from'} ${targetProvider}`);
@@ -218,9 +258,23 @@ export async function visualizerCommand(options) {
218
258
  else {
219
259
  await syncer.detachAssetFromTool(sourceEntity, mapEntityType(entityType), targetProvider);
220
260
  }
261
+ const result = {
262
+ type: 'CONNECTION_CHANGED_RESULT',
263
+ requestId,
264
+ ok: true,
265
+ };
266
+ socket.send(JSON.stringify(result));
221
267
  }
222
- catch {
223
- // Ignore invalid websocket messages from the client.
268
+ catch (err) {
269
+ if (!requestId)
270
+ return;
271
+ const result = {
272
+ type: 'CONNECTION_CHANGED_RESULT',
273
+ requestId,
274
+ ok: false,
275
+ error: err instanceof Error ? err.message : String(err),
276
+ };
277
+ socket.send(JSON.stringify(result));
224
278
  }
225
279
  });
226
280
  });
@@ -1,4 +1,4 @@
1
- import { WaslaGenieAdapter, Asset, AssetFormat } from '../core/types.js';
1
+ import { WaslaGenieAdapter, Asset, AssetFormat } from '#core/types.js';
2
2
  export declare abstract class BaseAdapter implements WaslaGenieAdapter {
3
3
  abstract name: string;
4
4
  abstract displayName: string;
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class ClaudeAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { fileExists, writeText, ensureDir } from '../utils/fs.js';
3
- import { join } from 'path';
4
- import { getToolMarkers } from '../utils/paths.js';
2
+ import { fileExists, writeText, ensureDir } from '#shared/fs.js';
3
+ import { dirname, join } from 'path';
4
+ import { getToolMarkers } from '#shared/paths.js';
5
5
  export class ClaudeAdapter extends BaseAdapter {
6
6
  constructor(scope = 'workspace') {
7
7
  super();
@@ -19,13 +19,16 @@ export class ClaudeAdapter extends BaseAdapter {
19
19
  }
20
20
  get paths() {
21
21
  const markers = getToolMarkers(this.scope);
22
+ const workspaceRoot = dirname(markers.claude);
22
23
  return {
23
24
  agent: join(markers.claude, 'agents'),
24
25
  skill: join(markers.claude, 'skills'),
25
26
  mcp: this.scope === 'workspace'
26
27
  ? join(markers.claude, 'mcp.json')
27
28
  : join(markers.claude, 'settings.json'),
28
- context: join(markers.claude, 'CLAUDE.md'),
29
+ context: this.scope === 'workspace'
30
+ ? join(workspaceRoot, 'CLAUDE.md')
31
+ : join(markers.claude, 'CLAUDE.md'),
29
32
  };
30
33
  }
31
34
  get skillDirs() {
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class CursorAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { fileExists, writeText, ensureDir } from '../utils/fs.js';
2
+ import { fileExists, writeText, ensureDir } from '#shared/fs.js';
3
3
  import { dirname, join } from 'path';
4
- import { getToolMarkers } from '../utils/paths.js';
4
+ import { getToolMarkers } from '#shared/paths.js';
5
5
  export class CursorAdapter extends BaseAdapter {
6
6
  constructor(scope = 'workspace') {
7
7
  super();
@@ -1,4 +1,4 @@
1
- import { WaslaGenieAdapter } from '../core/types.js';
1
+ import { WaslaGenieAdapter } from '#core/types.js';
2
2
  export declare function getAdapter(toolName: string, scope?: 'user' | 'workspace'): WaslaGenieAdapter;
3
3
  export declare function getInstalledAdapters(scope?: 'user' | 'workspace'): Promise<WaslaGenieAdapter[]>;
4
4
  export declare function getAllAdapters(scope?: 'user' | 'workspace'): WaslaGenieAdapter[];
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class GeminiAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { fileExists, writeText, ensureDir } from '../utils/fs.js';
2
+ import { fileExists, writeText, ensureDir } from '#shared/fs.js';
3
3
  import { dirname, join } from 'path';
4
- import { getToolMarkers } from '../utils/paths.js';
4
+ import { getToolMarkers } from '#shared/paths.js';
5
5
  export class GeminiAdapter extends BaseAdapter {
6
6
  constructor(scope = 'workspace') {
7
7
  super();
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class GithubCopilotCliAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { fileExists, writeText, ensureDir } from '../utils/fs.js';
2
+ import { fileExists, writeText, ensureDir } from '#shared/fs.js';
3
3
  import { dirname, join } from 'path';
4
- import { getToolMarkers } from '../utils/paths.js';
4
+ import { getToolMarkers } from '#shared/paths.js';
5
5
  export class GithubCopilotCliAdapter extends BaseAdapter {
6
6
  constructor(scope = 'workspace') {
7
7
  super();
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class GithubCopilotAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { fileExists, writeText, ensureDir } from '../utils/fs.js';
2
+ import { fileExists, writeText, ensureDir } from '#shared/fs.js';
3
3
  import { dirname, join } from 'path';
4
- import { getToolMarkers } from '../utils/paths.js';
4
+ import { getToolMarkers } from '#shared/paths.js';
5
5
  export class GithubCopilotAdapter extends BaseAdapter {
6
6
  constructor(scope = 'workspace') {
7
7
  super();
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class OpenclawAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;
@@ -1,7 +1,7 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { fileExists, writeText, ensureDir } from '../utils/fs.js';
2
+ import { fileExists, writeText, ensureDir } from '#shared/fs.js';
3
3
  import { dirname, join } from 'path';
4
- import { getToolMarkers } from '../utils/paths.js';
4
+ import { getToolMarkers } from '#shared/paths.js';
5
5
  export class OpenclawAdapter extends BaseAdapter {
6
6
  constructor(scope = 'workspace') {
7
7
  super();
@@ -1,5 +1,5 @@
1
1
  import { BaseAdapter } from './base.js';
2
- import { Asset } from '../core/types.js';
2
+ import { Asset } from '#core/types.js';
3
3
  export declare class OpenCodeAdapter extends BaseAdapter {
4
4
  name: string;
5
5
  displayName: string;