@oml/server 0.14.0

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 ADDED
@@ -0,0 +1,94 @@
1
+ <!-- Copyright (c) 2026 Modelware. All rights reserved. -->
2
+
3
+ # @oml/server
4
+
5
+ Standalone OML REST server runtime package.
6
+
7
+ The only user-facing CLI is `oml` from `@oml/cli`. Use `oml server start`, `oml server stop`, and `oml server status` to manage the server.
8
+
9
+ ## Options
10
+
11
+ - `--host`, `-h`: bind host. Default: `127.0.0.1`
12
+ - `--port`, `-p`: bind port. Default: `8080`
13
+ - `--workspace`, `-w`: workspace root used to initialize the server. Default: current working directory
14
+ - `--token`: bearer token required for REST endpoints (except `/health`)
15
+ - `--refresh-token`: optional refresh token used by server auth refresh
16
+ - `--token-expires-at`: optional access-token expiration time (epoch ms)
17
+
18
+ ## Canonical CLI
19
+
20
+ From a local checkout:
21
+
22
+ ```bash
23
+ node packages/cli/bin/cli.js server start
24
+ ```
25
+
26
+ When installed from npm:
27
+
28
+ ```bash
29
+ oml server start
30
+ ```
31
+
32
+ Server management commands:
33
+
34
+ ```bash
35
+ oml server start
36
+ oml server status
37
+ oml server stop
38
+ ```
39
+
40
+ If the requested port is already occupied, startup fails immediately with a clear message naming the occupied host and port.
41
+
42
+ ## REST API
43
+
44
+ The server exposes only these endpoints:
45
+
46
+ - `GET /health`
47
+ - `GET /openapi.json`
48
+ - `GET /v0/models`
49
+ - `POST /v0/query`
50
+ - `POST /v0/update`
51
+ - `POST /v0/fuzzysearch`
52
+ - `POST /v0/lint`
53
+ - `POST /v0/reason`
54
+ - `POST /v0/validate`
55
+ - `POST /v0/render`
56
+ - `POST /v0/export`
57
+
58
+ Health check:
59
+
60
+ ```bash
61
+ curl http://127.0.0.1:8080/health
62
+ ```
63
+
64
+ Run a SPARQL query:
65
+
66
+ ```bash
67
+ curl -X POST http://127.0.0.1:8080/v0/query \
68
+ -H 'content-type: application/json' \
69
+ -d '{
70
+ "modelUri": "file:///absolute/path/to/model.oml",
71
+ "sparql": "SELECT * WHERE { ?s ?p ?o } LIMIT 10"
72
+ }'
73
+ ```
74
+
75
+ Run update request:
76
+
77
+ ```bash
78
+ curl -X POST http://127.0.0.1:8080/v0/update \
79
+ -H 'content-type: application/json' \
80
+ -d '{
81
+ "operations": []
82
+ }'
83
+ ```
84
+
85
+ Run fuzzy search:
86
+
87
+ ```bash
88
+ curl -X POST http://127.0.0.1:8080/v0/fuzzysearch \
89
+ -H 'content-type: application/json' \
90
+ -d '{
91
+ "text": "firefighter",
92
+ "limit": 10
93
+ }'
94
+ ```
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@oml/server",
3
+ "description": "Standalone OML language server host",
4
+ "version": "0.14.0",
5
+ "type": "module",
6
+ "main": "./out/index.js",
7
+ "exports": {
8
+ ".": "./out/index.js",
9
+ "./lsp/language-server": "./out/lsp/language-server.js",
10
+ "./lsp/diagram-server": "./out/lsp/diagram-server.js",
11
+ "./rest/server": "./out/rest/server.js",
12
+ "./lsp/providers/browser-fs-provider": "./out/lsp/providers/browser-fs-provider.js",
13
+ "./lsp/providers/hybrid-fs-provider": "./out/lsp/providers/hybrid-fs-provider.js",
14
+ "./lsp/protocol/reasoner-protocol": "./out/lsp/protocol/reasoner-protocol.js",
15
+ "./lsp/protocol/browser-fs-protocol": "./out/lsp/protocol/browser-fs-protocol.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "scripts": {
19
+ "clean": "shx rm -fr *.tsbuildinfo out",
20
+ "build": "echo 'No build step'",
21
+ "build:clean": "npm run clean && npm run build"
22
+ },
23
+ "dependencies": {
24
+ "@leeoniya/ufuzzy": "^1.0.19",
25
+ "@oml/language": "0.14.0",
26
+ "@oml/markdown": "0.14.0",
27
+ "@oml/owl": "0.14.0",
28
+ "@oml/platform": "^0.5.0",
29
+ "@oml/reasoner": "^0.3.0",
30
+ "langium": "^4.2.1",
31
+ "langium-cli": "^4.2.0",
32
+ "vscode-languageserver": "~9.0.1"
33
+ }
34
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,189 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import * as fs from 'node:fs/promises';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { createHash } from 'node:crypto';
7
+ import { NodeFileSystem } from 'langium/node';
8
+ import { createConnection } from 'vscode-languageserver/node.js';
9
+ import { startOmlLanguageServer, type OmlLanguageServerRuntime } from './lsp/language-server.js';
10
+ import { startOmlRestServer } from './rest/server.js';
11
+
12
+ interface RemoteOptions {
13
+ host: string;
14
+ port: number;
15
+ workspaceRoot: string;
16
+ token?: string;
17
+ }
18
+
19
+ type ServerLockOwner = 'daemon' | 'extension';
20
+
21
+ function formatStartupError(error: unknown, host: string, port: number): string {
22
+ if (error && typeof error === 'object' && 'code' in error) {
23
+ const code = (error as NodeJS.ErrnoException).code;
24
+ if (code === 'EADDRINUSE') {
25
+ return `Port ${port} is already in use on ${host}.`;
26
+ }
27
+ if (code === 'EACCES') {
28
+ return `Port ${port} on ${host} cannot be used due to insufficient permissions.`;
29
+ }
30
+ }
31
+ return error instanceof Error ? error.message : String(error);
32
+ }
33
+
34
+ function readOptionValue(argv: string[], index: number, aliases: string[]): string {
35
+ const current = argv[index];
36
+ const equalsIndex = current.indexOf('=');
37
+ if (equalsIndex >= 0) {
38
+ return current.slice(equalsIndex + 1);
39
+ }
40
+ const next = argv[index + 1];
41
+ if (next === undefined || next.startsWith('-')) {
42
+ throw new Error(`Missing value for ${aliases.join('/')}.`);
43
+ }
44
+ return next;
45
+ }
46
+
47
+ function parseRemoteOptions(argv: string[]): RemoteOptions {
48
+ let portValue: string | undefined;
49
+ let workspaceValue: string | undefined;
50
+ let tokenValue: string | undefined;
51
+ for (let index = 0; index < argv.length; index += 1) {
52
+ const arg = argv[index];
53
+ if (arg === '--node-ipc' || arg.startsWith('--clientProcessId=')) {
54
+ continue;
55
+ }
56
+ if (arg === '--port' || arg === '-p' || arg.startsWith('--port=')) {
57
+ portValue = readOptionValue(argv, index, ['--port', '-p']).trim();
58
+ if (!arg.includes('=')) {
59
+ index += 1;
60
+ }
61
+ continue;
62
+ }
63
+ if (arg === '--workspace' || arg === '-w' || arg.startsWith('--workspace=')) {
64
+ workspaceValue = readOptionValue(argv, index, ['--workspace', '-w']).trim();
65
+ if (!arg.includes('=')) {
66
+ index += 1;
67
+ }
68
+ continue;
69
+ }
70
+ if (arg === '--token' || arg.startsWith('--token=')) {
71
+ tokenValue = readOptionValue(argv, index, ['--token']).trim();
72
+ if (!arg.includes('=')) {
73
+ index += 1;
74
+ }
75
+ continue;
76
+ }
77
+ if (arg.startsWith('-')) {
78
+ throw new Error(`Unknown option '${arg}'.`);
79
+ }
80
+ }
81
+
82
+ const host = '127.0.0.1';
83
+ const rawPort = Number(portValue ?? process.env.OML_SERVER_PORT ?? '8080');
84
+ const port = Number.isFinite(rawPort) ? Math.max(0, Math.min(65535, Math.trunc(rawPort))) : 8080;
85
+ const workspaceRoot = workspaceValue && workspaceValue.length > 0 ? workspaceValue : process.cwd();
86
+ const token = tokenValue && tokenValue.length > 0 ? tokenValue : undefined;
87
+ return { host, port, workspaceRoot, token };
88
+ }
89
+
90
+ function startIpcLanguageServerIfAvailable(): { connection: import('vscode-languageserver').Connection; runtime: OmlLanguageServerRuntime } | undefined {
91
+ if (typeof process.send !== 'function') {
92
+ return undefined;
93
+ }
94
+ const connection = createConnection();
95
+ const runtime = startOmlLanguageServer(connection, {
96
+ fileSystem: NodeFileSystem,
97
+ registerDiagramHandlers: true,
98
+ suppressTransientDiagnostics: true,
99
+ installNodeProcessHandlers: true,
100
+ });
101
+ return { connection, runtime };
102
+ }
103
+
104
+ function workspaceHash(workspaceRoot: string): string {
105
+ return createHash('sha256').update(path.resolve(workspaceRoot)).digest('hex');
106
+ }
107
+
108
+ function lockFilePathForWorkspace(workspaceRoot: string): string {
109
+ return path.join(os.homedir(), '.oml', 'workspaces', workspaceHash(workspaceRoot), 'server.lock');
110
+ }
111
+
112
+ async function writeServerLock(workspaceRoot: string, port: number, owner: ServerLockOwner): Promise<string> {
113
+ const lockFile = lockFilePathForWorkspace(workspaceRoot);
114
+ await fs.mkdir(path.dirname(lockFile), { recursive: true });
115
+ await fs.writeFile(lockFile, JSON.stringify({ port, pid: process.pid, owner }) + '\n', 'utf-8');
116
+ return lockFile;
117
+ }
118
+
119
+ async function removeServerLock(lockFile: string): Promise<void> {
120
+ await fs.rm(lockFile, { force: true });
121
+ }
122
+
123
+ function resolveListeningPort(server: import('node:http').Server): number {
124
+ const address = server.address();
125
+ if (!address || typeof address === 'string') {
126
+ throw new Error('Unable to resolve listening port.');
127
+ }
128
+ return address.port;
129
+ }
130
+
131
+ export function runServerCli(argv: string[] = process.argv.slice(2)): void {
132
+ let options: RemoteOptions;
133
+ try {
134
+ options = parseRemoteOptions(argv);
135
+ } catch (error) {
136
+ const message = error instanceof Error ? error.message : String(error);
137
+ process.stderr.write(`[oml-server] REST startup failed: ${message}\n`);
138
+ process.exitCode = 1;
139
+ process.exit();
140
+ return;
141
+ }
142
+ const ipcServer = startIpcLanguageServerIfAvailable();
143
+ const owner: ServerLockOwner = ipcServer ? 'extension' : 'daemon';
144
+ void startOmlRestServer({
145
+ host: options.host,
146
+ port: options.port,
147
+ workspaceRoot: options.workspaceRoot,
148
+ watchWorkspace: true,
149
+ authToken: options.token,
150
+ runtime: ipcServer?.runtime,
151
+ }).then(async ({ server, updateToken }) => {
152
+ if (ipcServer) {
153
+ ipcServer.connection.onNotification('$/tokenRefreshed', (params: { accessToken: string }) => {
154
+ if (params?.accessToken) {
155
+ updateToken(params.accessToken);
156
+ }
157
+ });
158
+ }
159
+ const listeningPort = resolveListeningPort(server);
160
+ const lockFile = await writeServerLock(options.workspaceRoot, listeningPort, owner);
161
+ let shuttingDown = false;
162
+ const shutdown = (exitCode: number): void => {
163
+ if (shuttingDown) {
164
+ return;
165
+ }
166
+ shuttingDown = true;
167
+ server.close(() => {
168
+ void removeServerLock(lockFile).finally(() => {
169
+ process.exit(exitCode);
170
+ });
171
+ });
172
+ };
173
+
174
+ process.once('SIGINT', () => shutdown(0));
175
+ process.once('SIGTERM', () => shutdown(0));
176
+ server.once('close', () => {
177
+ void removeServerLock(lockFile);
178
+ });
179
+
180
+ process.stdout.write(`[oml-server] REST listening on http://${options.host}:${listeningPort}\n`);
181
+ }).catch((error: unknown) => {
182
+ const message = formatStartupError(error, options.host, options.port);
183
+ process.stderr.write(`[oml-server] REST startup failed: ${message}\n`);
184
+ process.exitCode = 1;
185
+ process.exit();
186
+ });
187
+ }
188
+
189
+ runServerCli();
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ export * from './lsp/language-server.js';
4
+ export * from './lsp/diagram-server.js';
5
+ export * from './rest/server.js';
6
+ export * from './lsp/providers/browser-fs-provider.js';
7
+ export * from './lsp/providers/hybrid-fs-provider.js';
8
+ export * from './lsp/protocol/reasoner-protocol.js';
9
+ export * from './lsp/protocol/browser-fs-protocol.js';
@@ -0,0 +1,48 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import type { SModelRoot } from 'sprotty-protocol';
4
+ import {
5
+ computeLaidOutSModelForUri,
6
+ resolveDefinition,
7
+ resolveOntologyMemberLabels,
8
+ type OntologyMemberLabelEntry,
9
+ type RangeResponse,
10
+ } from '@oml/language';
11
+ import { RequestType } from 'vscode-languageserver-protocol';
12
+
13
+ type ConnectionLike = {
14
+ onRequest: (type: RequestType<any, any, any>, handler: (params: any) => any | Promise<any>) => void;
15
+ };
16
+
17
+ const DiagramModelRequest = new RequestType<{ modelUri: string }, SModelRoot, void>('oml/diagram/model');
18
+ const DefinitionRequest = new RequestType<{ elementId: string; referencingUri?: string }, RangeResponse | null, void>('oml/definition');
19
+ const OntologyLabelsRequest = new RequestType<{ modelUri: string }, OntologyMemberLabelEntry[], void>('oml/labels');
20
+
21
+ export function registerDiagramRequests(connection: ConnectionLike, shared: any, oml?: any): void {
22
+ connection.onRequest(DiagramModelRequest, async ({ modelUri }) => {
23
+ try {
24
+ return await computeLaidOutSModelForUri(shared, modelUri);
25
+ } catch (err) {
26
+ console.error('[oml] diagram model error', err);
27
+ return { id: 'root', type: 'graph', children: [] } as unknown as SModelRoot;
28
+ }
29
+ });
30
+
31
+ connection.onRequest(DefinitionRequest, async ({ elementId, referencingUri }) => {
32
+ try {
33
+ return await resolveDefinition(shared, elementId, referencingUri);
34
+ } catch (err) {
35
+ console.error('[oml] definition error', err);
36
+ return null;
37
+ }
38
+ });
39
+
40
+ connection.onRequest(OntologyLabelsRequest, async ({ modelUri }) => {
41
+ try {
42
+ return await resolveOntologyMemberLabels(shared, oml, modelUri);
43
+ } catch (err) {
44
+ console.error('[oml] labels error', err);
45
+ return [];
46
+ }
47
+ });
48
+ }